From d8bc1bc63627a870c95be7b3024641446cf1bc90 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 29 Jun 2026 17:10:55 -0400 Subject: [PATCH 1/2] fix(#1831): add state-rebuild test files to lint-test-file-count allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRs #1829 and #1830 added tests/state-rebuild.test.cjs and tests/state-rebuild-cli.test.cjs but did not add them to the lint-test-file-count allowlist for the 'state' module. The lint-test-file-count.test.cjs harness reported FAIL_NOVEL_FILES with the two files listed as novel. Verified via gsd-test (--base origin/main --head origin/next): without this fix, 2/22097 tests fail (both lint-test-file-count allowlist cases). The state module has 17 test files; allowlist entry now lists all 17 alphabetically. Process note: the original PRs should have updated this allowlist in the same commit that added the test files. Recapture of the golden-install-parity fixtures is NOT required — those fixtures on next are correct (verified: no diff from UPDATE_GOLDEN=1 locally). --- scripts/lint-test-file-count.allowlist.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/lint-test-file-count.allowlist.json b/scripts/lint-test-file-count.allowlist.json index 6758bb796..da4d884af 100644 --- a/scripts/lint-test-file-count.allowlist.json +++ b/scripts/lint-test-file-count.allowlist.json @@ -110,10 +110,12 @@ "bug-3454-state-dollar-backreference-growth.test.cjs", "bug-397-state-preserve-executor-authored.test.cjs", "bug-905-state-syncstatefrontmatter-preserve-scalars.test.cjs", - "bug-948-state-noop-write-guard.test.cjs", - "state-acquirestatelock-non-eexist.test.cjs", - "state-prune.test.cjs", - "state.test.cjs" + "bug-948-state-noop-write-guard.test.cjs", + "state-acquirestatelock-non-eexist.test.cjs", + "state-prune.test.cjs", + "state-rebuild-cli.test.cjs", + "state-rebuild.test.cjs", + "state.test.cjs" ], "issue": "180" }, From f65866f17da6c80a2c77420be536c192218ea5bd Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 29 Jun 2026 20:08:17 -0400 Subject: [PATCH 2/2] fix(#1831): resolve ESLint errors and unused-var warnings from #1829/#1830 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two hard errors in src/state-transition.cts: - reconcileCurrentPosition: `!== null` guards on `Record` values don't narrow the type to a primitive, causing @typescript-eslint/no-base-to-string to fire on String(fm.current_phase) and String(fm.current_phase_name). Extract to typed locals + use typeof narrowing so the rule sees string | number — which is the actual invariant. Four unused-var warnings (warnings are still defects per RULESET): - stripTemplatePlaceholders: `placeholder` was assigned value.trim() but never read — the value came from the loop variable itself, so removed. - deduplicateSessionArchive: `sectionContent` was sliced but the filter below uses the raw hs offsets directly — variable was dead code; removed. - state-rebuild.test.cjs: first transitionCore call in the orphan-row test is covered by the dedicated Leaky-Abstractions guard test below it; remove the no-op call rather than silently ignoring its result. - state-rebuild.test.cjs: `before = state` in the sync regression guard test was never read — remove the dead assignment. Co-Authored-By: Claude Sonnet 4.6 --- src/state-transition.cts | 13 ++++++------- tests/state-rebuild.test.cjs | 4 +--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/state-transition.cts b/src/state-transition.cts index ac6ef6299..f7051d6ab 100644 --- a/src/state-transition.cts +++ b/src/state-transition.cts @@ -1666,8 +1666,9 @@ function reconcileCurrentPosition( // by other transitions (beginPhase / completePhase) and reconstructed from // total-phase counts; rebuild reconciles only the `**Current Phase:**` body // field that frontmatter is the canonical source for. - if (fm.current_phase !== undefined && fm.current_phase !== null) { - const canonicalPhase = String(fm.current_phase); + const fmPhase = fm.current_phase; + if (typeof fmPhase === 'string' || typeof fmPhase === 'number') { + const canonicalPhase = String(fmPhase); const existing = stateExtractField(modified, 'Current Phase'); if (existing !== null && existing !== canonicalPhase) { const replaced = stateReplaceField(modified, 'Current Phase', canonicalPhase); @@ -1686,8 +1687,9 @@ function reconcileCurrentPosition( } // Phase name prose. - if (fm.current_phase_name !== undefined && fm.current_phase_name !== null) { - const canonicalName = String(fm.current_phase_name); + const fmPhaseName = fm.current_phase_name; + if (typeof fmPhaseName === 'string' || typeof fmPhaseName === 'number') { + const canonicalName = String(fmPhaseName); const existing = stateExtractField(modified, 'Current Phase Name'); if (existing !== null && existing !== canonicalName) { const replaced = stateReplaceField(modified, 'Current Phase Name', canonicalName); @@ -1821,7 +1823,6 @@ function stripTemplatePlaceholders( const fieldName = m[1]; const value = m[2]; if (TEMPLATE_PLACEHOLDER_VALUE.test(value)) { - const placeholder = value.trim(); const cleared = `**${fieldName}:** (pending)`; replacements.push({ lineIdx: i, before: line, after: cleared, fieldName }); } @@ -1874,8 +1875,6 @@ function deduplicateSessionArchive( for (let i = sectionIdx + 1; i < hs.length; i++) { if (hs[i].level === 2) { sectionEnd = hs[i].offset; break; } } - const sectionContent = content.slice(sectionStart, sectionEnd); - // Count `### Session — …` H3 sub-headings inside the section. const archiveHeadings = hs.filter( (h) => h.level === 3 && h.offset >= sectionStart && h.offset < sectionEnd && SESSION_ARCHIVE_H3.test(h.text), diff --git a/tests/state-rebuild.test.cjs b/tests/state-rebuild.test.cjs index b2f7e361b..ba4492e85 100644 --- a/tests/state-rebuild.test.cjs +++ b/tests/state-rebuild.test.cjs @@ -367,8 +367,7 @@ describe('ADR-1817 §2: rebuild reconciles **By Phase:** table via phaseInventor { number: '3', name: 'Test Phase', planCount: 5, summaryCount: 4 }, ], }; - const result = transitionCore(drifted, { kind: 'rebuild' }, baseDeps); - // Note: passing baseDeps here (no provider) → reconcile is a no-op. + // First call with no phaseInventoryProvider → no-op (covered by its own test below). // Re-run with the provider-wired deps: const result2 = transitionCore(drifted, { kind: 'rebuild' }, deps); // Strip the audit log so we only inspect LIVE table rows (the log's @@ -421,7 +420,6 @@ describe('ADR-1817 §2: rebuild reconciles **By Phase:** table via phaseInventor describe('ADR-1817 §5/§6: rebuild does not affect sync or prune (regression guard, criterion #7)', () => { test('sync still patches its three frontmatter fields when rebuild is also available', () => { const state = cleanState(); - const before = state; const result = transitionCore( state, { kind: 'sync', totalPlansInPhase: 7, percent: 50 },