diff --git a/.jules/bolt.md b/.jules/bolt.md index b08b203a..a909c4f8 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -4,3 +4,6 @@ ## 2026-07-12 - Optimize renderTaskRow DOM allocations **Learning:** Caching unattached template nodes and instantiating them via `.cloneNode(false)` reduces DOM instantiation overhead in O(N) render loops significantly. **Action:** Apply this optimization to other hot-path rendering elements such as rows, cells, and stack containers. +## 2026-09-03 - Measure date-formatting micro-optimizations before generalizing +**Learning:** In a bounded Node/V8 microbenchmark, explicit two-digit zero-padding can be faster than `String.prototype.padStart()` for the same formatter output. That measurement does not establish browser Gantt p95, allocation/GC pressure, or a JS/native-boundary root cause. +**Action:** Preserve formatter behavior with executable UTC/local/zero-padding/invalid-date regression coverage. Apply the inline form only where representative profiling supports it, and keep buyer-visible performance claims separate from microbenchmark evidence. diff --git a/app.js b/app.js index a04aae71..0f155ba4 100644 --- a/app.js +++ b/app.js @@ -2684,20 +2684,29 @@ function clamp(value, min, max) { function formatDateInput(date) { const year = date.getUTCFullYear(); - const month = String(date.getUTCMonth() + 1).padStart(2, '0'); - const day = String(date.getUTCDate()).padStart(2, '0'); + const monthRaw = date.getUTCMonth() + 1; + const dayRaw = date.getUTCDate(); + const month = monthRaw < 10 ? '0' + monthRaw : monthRaw; + const day = dayRaw < 10 ? '0' + dayRaw : dayRaw; return `${year}-${month}-${day}`; } function formatLocalDateInput(date) { const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const monthRaw = date.getMonth() + 1; + const dayRaw = date.getDate(); + const month = monthRaw < 10 ? '0' + monthRaw : monthRaw; + const day = dayRaw < 10 ? '0' + dayRaw : dayRaw; return `${year}-${month}-${day}`; } function formatCompactDate(date) { - return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`; + const year = date.getFullYear(); + const monthRaw = date.getMonth() + 1; + const dayRaw = date.getDate(); + const month = monthRaw < 10 ? '0' + monthRaw : monthRaw; + const day = dayRaw < 10 ? '0' + dayRaw : dayRaw; + return `${year}${month}${day}`; } function formatPercent(value, digits) { diff --git a/package.json b/package.json index 8cefdc74..1788011c 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "coverage": "npm run test:coverage", "server": "node server/server.mjs", "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs", - "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs", + "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs && node tests/unit/date-formatters.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", - "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", + "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/date-formatters.test.mjs && npm run test:api", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", "test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js", diff --git a/tests/unit/date-formatters.test.mjs b/tests/unit/date-formatters.test.mjs new file mode 100644 index 00000000..09fca706 --- /dev/null +++ b/tests/unit/date-formatters.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import vm from 'node:vm'; + +const appPath = resolve('app.js'); +const appSource = readFileSync(appPath, 'utf8'); + +function loadFunction(name) { + const marker = `function ${name}(`; + const start = appSource.indexOf(marker); + assert.notEqual(start, -1, `${name} must remain defined in app.js`); + + let depth = 0; + let opened = false; + let end = -1; + for (let index = appSource.indexOf('{', start); index < appSource.length; index += 1) { + if (appSource[index] === '{') { + depth += 1; + opened = true; + } else if (appSource[index] === '}') { + depth -= 1; + if (opened && depth === 0) { + end = index + 1; + break; + } + } + } + assert.notEqual(end, -1, `${name} must have a complete function body`); + + const source = appSource.slice(start, end); + const lineOffset = appSource.slice(0, start).split('\n').length - 1; + return new vm.Script(`(${source})`, { + filename: appPath, + lineOffset, + }).runInThisContext(); +} + +const formatDateInput = loadFunction('formatDateInput'); +const formatLocalDateInput = loadFunction('formatLocalDateInput'); +const formatCompactDate = loadFunction('formatCompactDate'); + +assert.equal(formatDateInput(new Date(Date.UTC(2026, 0, 5))), '2026-01-05'); +assert.equal(formatDateInput(new Date(Date.UTC(2026, 10, 15))), '2026-11-15'); + +assert.equal(formatLocalDateInput(new Date(2026, 0, 5, 12, 0, 0)), '2026-01-05'); +assert.equal(formatLocalDateInput(new Date(2026, 10, 15, 12, 0, 0)), '2026-11-15'); +assert.equal(formatCompactDate(new Date(2026, 0, 5, 12, 0, 0)), '20260105'); +assert.equal(formatCompactDate(new Date(2026, 10, 15, 12, 0, 0)), '20261115'); + +const invalid = new Date(Number.NaN); +assert.equal(formatDateInput(invalid), 'NaN-NaN-NaN'); +assert.equal(formatLocalDateInput(invalid), 'NaN-NaN-NaN'); +assert.equal(formatCompactDate(invalid), 'NaNNaNNaN'); + +console.log('date formatter tests passed');