Adds a dedicated regression (tests/fix-2136-clock-local-today.test.cjs):
- realClock.localToday() returns the LOCAL calendar day under a pinned instant
+ TZ (America/Chicago → 2020-06-14, the issue's repro instant).
- realClock.today() is unchanged (UTC) — internal/cosmetic stamps stay UTC.
- localToday === today on a UTC host (no spurious divergence).
- makeFakeClock mirrors localToday (drop-in Clock substitute).
- field-level: a 'sync' transition with a split clock (today≠localToday)
writes the LOCAL day into Last Activity, proving the wiring uses localToday.
Mirrors localToday() in the fake clock helper and the inline fixedClock stubs
in state-transition.test.cjs / state-rebuild.test.cjs (the Clock interface now
requires it).
Two hard errors in src/state-transition.cts:
- reconcileCurrentPosition: `!== null` guards on `Record<string,unknown>`
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 <noreply@anthropic.com>
Phase 1 of approved feature #1817. Implements the body-structure
derivability contract landed in ADR-1817 (Phase 0, PR #1828).
Source changes (src/state-transition.cts):
- Add `rebuild` as the 11th intent in StateTransitionIntent (ADR-1769
transition set extended per ADR-1817 §1).
- Add `phaseInventoryProvider` optional dep + PhaseInventoryRecord type
(Leaky-Abstractions guard: pure core stays testable without disk I/O;
rebuild skips table reconciliation when the provider is absent).
- Add `case 'rebuild':` dispatch arm to transitionCore (the missing-case
compile-time guarantee extends to the 11th case).
- Implement rebuildCore orchestrator + four drift-class helpers per
ADR-1817 §2:
* reconcileCurrentPosition — body prose re-derived from frontmatter
* reconcileByPhaseTable — **By Phase:** table re-derived from disk
inventory via the new dep
* stripTemplatePlaceholders — `**Field:** [placeholder]` →
`**Field:** (pending)` (no canonical source available)
* deduplicateSessionArchive — keep most-recent 3 archived H3 blocks
- Implement appendRebuildLogSection per ADR-1817 §3 — every mutation
appends a structured entry (timestamp/kind/section/before/after/reason)
to `## Rebuild Log`. Idempotency guarantee (ADR-1817 §4): a no-mutation
rebuild appends NO log entry, so two successive runs on a clean file
are byte-identical.
Compiled output (gsd-core/bin/lib/state-transition.cjs): regenerated via
`npm run build:lib` (tsc -p tsconfig.build.json).
Tests (tests/state-rebuild.test.cjs, 18 cases):
- Dispatch + idempotency contract (3 tests, including the load-bearing
'rebuild on a clean file is a no-op' that pins §4).
- Current Position prose reconciliation, criterion #1 (3 tests).
- Template-placeholder removal, criterion #3 (3 tests).
- Session Continuity Archive de-duplication, criterion #4 (4 tests).
- **By Phase:** table reconciliation via phaseInventoryProvider, criterion
#2 (3 tests, including the Leaky-Abstractions no-op guard).
- Regression guard for sync + prune, criterion #7 (2 tests).
Verified: node --test tests/state-rebuild.test.cjs → 18/18 pass.
Verified: node --test tests/state-transition.test.cjs → 85/85 pass (no
regression on the existing 10 transitions).
Phase 2 (#1826) wires the CLI surface (cmdStateRebuild + --dry-run +
integration tests + docs + changeset). This PR adds the engine only — no
user-visible command yet, so no-changelog label applied.