refactor(#180): migrate STATE.md Document Module to sdk/src/state (#249)

* refactor(#180): migrate state document module to sdk/src/state

* test(#180): ratchet lint allowlists for state module relocation
This commit is contained in:
Tom Boucher
2026-05-24 21:06:50 -04:00
committed by GitHub
parent 9aae41f22d
commit 59bcdf03b6
20 changed files with 50 additions and 34 deletions

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 180
---
migrated the STATE.md Document Module to sdk/src/state/index.ts and rewired generator, import, and hand-sync allowlist paths with no behavior change.

View File

@@ -44,7 +44,7 @@ Adapter Module that satisfies native query dispatch at the Dispatch Policy seam,
Module owning projection from dispatch results/errors to CLI `{ exitCode, stdoutChunks, stderrLines }` output contract.
### STATE.md Document Module
Shared CJS/SDK pure transform Module owning STATE.md parse, field extraction, field replacement, status normalization, and frontmatter reconstruction. It does not scan `.planning/phases` and does not own persistence or locking; phase/plan/summary counts arrive from inventory/progress Modules as inputs, and CJS/SDK read-modify-write paths remain Adapters. Source of truth: `sdk/src/query/state-document.ts`; CJS callers consume the generator-emitted `get-shit-done/bin/lib/state-document.generated.cjs` via the thin re-export at `get-shit-done/bin/lib/state-document.cjs`.
Shared CJS/SDK pure transform Module owning STATE.md parse, field extraction, field replacement, status normalization, and frontmatter reconstruction. It does not scan `.planning/phases` and does not own persistence or locking; phase/plan/summary counts arrive from inventory/progress Modules as inputs, and CJS/SDK read-modify-write paths remain Adapters. Source of truth: `sdk/src/state/index.ts`; CJS callers consume the generator-emitted `get-shit-done/bin/lib/state-document.generated.cjs` via the thin re-export at `get-shit-done/bin/lib/state-document.cjs`.
### Query Execution Policy Module
Module owning query transport routing policy projection (`preferNative`, fallback policy, workstream subprocess forcing) at execution seam.

View File

@@ -434,7 +434,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
| `state-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` |
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
| `state-document.cjs` | Pure STATE.md field extraction, replacement, status normalization, and progress calculation transforms |
| `state-document.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/state-document.ts` via `sdk/scripts/gen-state-document.ts`; do not edit directly |
| `state-document.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/state/index.ts` via `sdk/scripts/gen-state-document.ts`; do not edit directly |
| `surface.cjs` | Runtime surface module — manages the runtime enable/disable surface state independently of the install-time profile marker (ADR-0011 Phase 2) |
| `template.cjs` | Template selection and filling with variable substitution |
| `uat.cjs` | UAT file parsing, verification debt tracking, audit-uat support |

View File

@@ -31,7 +31,7 @@ The table below indexes by Module, not by physical layer. Each row names the sou
| Module | Status | Source of truth | Generated artifacts | Adapters |
|---|---|---|---|---|
| **STATE.md Document Module** | New under this ADR (Phase 1) — see CONTEXT.md "STATE.md Document Module" | `sdk/src/state-document/index.ts` (promoted from `sdk/src/query/state-document.ts`) | `sdk/src/query/state-document.generated.ts`, `get-shit-done/bin/lib/state-document.generated.cjs` | `bin/lib/state.cjs` and `sdk/src/query/state*.ts` import the generated form |
| **STATE.md Document Module** | New under this ADR (Phase 1) — see CONTEXT.md "STATE.md Document Module" | `sdk/src/state/index.ts` (promoted from `sdk/src/query/state-document.ts`) | `sdk/src/query/state-document.generated.ts`, `get-shit-done/bin/lib/state-document.generated.cjs` | `bin/lib/state.cjs` and `sdk/src/query/state*.ts` import the generated form |
| **Configuration Module** | New under this ADR (Phase 2) — definition added to CONTEXT.md as part of Phase 2 | `sdk/src/config/index.ts` plus data manifests `sdk/shared/config-schema.manifest.json` and `sdk/shared/config-defaults.manifest.json` | `sdk/src/query/config-schema.generated.ts`, `get-shit-done/bin/lib/config-schema.generated.cjs`, `get-shit-done/bin/lib/configuration.generated.cjs` | `bin/lib/config.cjs`, `bin/lib/core.cjs:loadConfig`, `sdk/src/config.ts` |
| **Workstream Inventory Module** (Builder) | Amended under this ADR (Phase 3) — Builder split documented in CONTEXT.md update | `sdk/src/workstream-inventory/builder.ts` (pure projection from directory entries + STATE.md text + plan scan results → typed inventory) | `sdk/src/query/workstream-inventory-builder.generated.ts`, `get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs` | Per-side fs Readers (`workstream-inventory.cjs` sync, `workstream-inventory.ts` async) call the Builder. Readers stay hand-authored because the fs idiom legitimately differs. |
| **Project-Root Resolution Module** | New under this ADR (Phase 4) — short CONTEXT.md entry, behavior already de-facto shared | `sdk/src/project-root/index.ts` | `get-shit-done/bin/lib/project-root.generated.cjs` | `bin/lib/core.cjs` (`findProjectRoot`, `findEffectiveRoot`), `sdk/src/helpers.ts` |
@@ -227,4 +227,3 @@ verify.cjs migration scope for generator-pattern coverage.
**What this is NOT:** The full async mutation handlers (`phaseAdd`, `phaseInsert`, `phaseRemove`, `phaseComplete`) are inherently async I/O-bound and are NOT generated — per Section 4. This amendment only extracts the pure-computation kernel.
**Open drift bugs remaining:** Issue #6 (phasePlanIndex drift) and issue #26 (phase.add inline ROADMAP update drift) are separate bug reports; they are referenced here for traceability but not fixed in this amendment. Future amendments should note when those are resolved.

View File

@@ -9,7 +9,7 @@ The CJS↔SDK hard-seam migration (#3524) eliminates a class of config-schema dr
| Phase | PR | Summary |
|-------|----|---------|
| Phase 1 | [#3531](https://github.com/open-gsd/get-shit-done-redux/pull/3531) | `state-document` Shared Module — source-of-truth at `sdk/src/state-document/`, generator, freshness check, CJS Adapter (`state-document.generated.cjs`). Worked example for the pattern. |
| Phase 1 | [#3531](https://github.com/open-gsd/get-shit-done-redux/pull/3531) | `state-document` Shared Module — source-of-truth at `sdk/src/state/` (`index.ts`), generator, freshness check, CJS Adapter (`state-document.generated.cjs`). Worked example for the pattern. |
| Phase 2 | [#3540](https://github.com/open-gsd/get-shit-done-redux/pull/3540) | `configuration` Shared Module — `sdk/shared/config-schema.manifest.json` + `sdk/shared/config-defaults.manifest.json` as data manifests; generator + freshness check + CJS Adapter. |
| Phase 3 | [#3548](https://github.com/open-gsd/get-shit-done-redux/pull/3548) | `workstream-inventory` Shared Module — source-of-truth at `sdk/src/workstream-inventory/`, builder, generator, freshness check, CJS Adapter. |
| Phase 4 | [#3554](https://github.com/open-gsd/get-shit-done-redux/pull/3554) | `project-root` Shared Module — source-of-truth at `sdk/src/project-root/`, generator, freshness check, CJS Adapter. |

View File

@@ -62,10 +62,10 @@ Phases are sized to ship in one to two PRs each. Each phase has its own GitHub i
### Phase 1 — STATE.md Document Module (smallest possible proof)
**Why first.** `bin/lib/state-document.cjs` and `sdk/src/query/state-document.ts` are already a character-identical hand-synced pair of pure transforms (the file headers explicitly say "Pure transforms for STATE.md text. This module does not read the filesystem and does not own persistence or locking."). Deletion test passes on contact: one side can be deleted as soon as the other becomes the generated artifact. This is the safest possible first step and the canonical proof that the generator pattern works for executable logic, not just alias tables.
**Why first.** `bin/lib/state-document.cjs` and `sdk/src/state/index.ts` are already a character-identical hand-synced pair of pure transforms (the file headers explicitly say "Pure transforms for STATE.md text. This module does not read the filesystem and does not own persistence or locking."). Deletion test passes on contact: one side can be deleted as soon as the other becomes the generated artifact. This is the safest possible first step and the canonical proof that the generator pattern works for executable logic, not just alias tables.
**Scope:**
- Promote `sdk/src/query/state-document.ts` to a source under `sdk/src/state-document/index.ts` (or keep in place — decided in implementation).
- Promote `sdk/src/query/state-document.ts` to `sdk/src/state/index.ts` (implemented).
- Write `sdk/scripts/gen-state-document.ts` that emits `get-shit-done/bin/lib/state-document.generated.cjs` (and optionally re-exports the TS form at its existing location).
- Write `sdk/scripts/check-state-document-fresh.mjs` modeled on `check-command-aliases-fresh.mjs`.
- Replace `bin/lib/state-document.cjs` content with a thin re-export from `state-document.generated.cjs`. Keep the existing filename so callers (e.g. `workstream-inventory.cjs:16`) don't need to update imports.
@@ -228,7 +228,7 @@ Phase 5 specifically preserves the in-process model: `QueryRuntimeBridge.execute
### Open questions (resolved before the phase that depends on them)
1. **Phase 1 source location** — `sdk/src/state-document/index.ts` (move) vs `sdk/src/query/state-document.ts` (in place). Decided when Phase 1 PR is drafted.
1. **Phase 1 source location** — resolved to `sdk/src/state/index.ts` (migrated from `sdk/src/query/state-document.ts`).
2. **Phase 2 manifest format** — JSON vs JSONC vs TypeScript-as-source. Decided in Phase 2. JSON wins unless we need comments for invariants documentation.
3. **Phase 3 sibling-Module audit** — exact list of pairs that get Builder-split vs deferred. Decided as a deliverable of Phase 3's spike.
4. **Phase 5 synchronous-bridging mechanism** — `executeForCjs` implementation strategy: `deasync` native module (battle-tested but C++ binding), `Atomics.wait` on a worker channel (zero-binding but spins a Worker), or refactor every async SDK handler to expose a sync entry point (cleanest but largest scope). Decided in the Phase 5 spike issue before any family migration begins.

View File

@@ -3,7 +3,7 @@
/**
* STATE.md Document Module — CJS adapter.
*
* The implementation is generated from sdk/src/query/state-document.ts and
* The implementation is generated from sdk/src/state/index.ts and
* lives in state-document.generated.cjs. This file is a thin re-export so
* that existing call sites (state.cjs, workstream-inventory.cjs, init.cjs,
* and tests) can continue to require('./state-document') unchanged.

View File

@@ -3,7 +3,7 @@
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/state-document.ts
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.

View File

@@ -8,7 +8,7 @@
"verify": { "current": 10, "issue": "3767" },
"install": { "current": 9, "issue": "TBD" },
"init": { "current": 8, "issue": "TBD" },
"state": { "current": 10, "issue": "21" },
"state": { "current": 11, "issue": "180" },
"config": { "current": 8, "issue": "TBD" },
"graphify": { "current": 7, "issue": "TBD" },
"progress": { "current": 6, "issue": "14" },
@@ -16,7 +16,7 @@
"surface": { "current": 5, "issue": "TBD" },
"commit": { "current": 4, "issue": "TBD" },
"frontmatter": { "current": 4, "issue": "TBD" },
"index": { "current": 4, "issue": "TBD" },
"index": { "current": 5, "issue": "180" },
"intel": { "current": 4, "issue": "TBD" },
"mvp": { "current": 4, "issue": "TBD" },
"install-profiles": { "current": 4, "issue": "TBD" },

View File

@@ -52,9 +52,9 @@
},
{
"cjs": "get-shit-done/bin/lib/state-document.cjs",
"ts": "sdk/src/query/state-document.ts",
"classification": "cooperating-sibling",
"justification": "CJS state-document.cjs is the generated Adapter reading from sdk/src/state-document/ Shared Module (Phase 1/#3531). SDK state-document.ts is the corresponding source-of-truth query handler. Freshness check enforces alignment."
"ts": "sdk/src/state/index.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "CJS state-document.cjs is a thin Adapter over get-shit-done/bin/lib/state-document.generated.cjs. That generated artifact is emitted from sdk/src/state/index.ts by sdk/scripts/gen-state-document.ts; freshness is enforced by sdk/scripts/check-state-document-fresh.mjs."
},
{
"cjs": "get-shit-done/bin/lib/template.cjs",
@@ -92,6 +92,18 @@
"classification": "CJS-CLI-ONLY",
"justification": "Phase 2 (#3536) already migrated CONFIG_DEFAULTS and loadConfig/mergeDefaults to the Configuration Module and sdk/src/config.ts. What remains in config.cjs is exclusively CLI command handlers (cmdConfigGet, cmdConfigSet, cmdConfigNewProject, cmdConfigEnsureSection, cmdConfigSetModelProfile, cmdConfigPath, cmdMigrateConfig, buildNewProjectConfig, setConfigValue, ensureConfigFile) that depend on CJS-only APIs (withPlanningLock, platformWriteSync/ReadSync/EnsureDir, sync fs ops, process.exit). sdk/src/config.ts provides only the async loadConfig/mergeDefaults SDK layer. The two files serve disjoint surfaces with no logical overlap — not a hand-sync drift anti-pattern."
},
{
"cjs": "get-shit-done/bin/lib/config.cjs",
"ts": "sdk/src/config/index.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "sdk/src/config/index.ts is the Configuration Module source-of-truth; CJS config.cjs consumes its generated Adapter through core/config-schema integration. This is an intentional seam relationship, not manual hand-sync."
},
{
"cjs": "get-shit-done/bin/lib/state.cjs",
"ts": "sdk/src/state/index.ts",
"classification": "ADAPTER-OVER-MODULE",
"justification": "sdk/src/state/index.ts is the STATE.md Document Module source-of-truth that emits get-shit-done/bin/lib/state-document.generated.cjs. state.cjs consumes that generated module as an Adapter dependency; basename overlap is intentional and not a duplicated implementation."
},
{
"cjs": "get-shit-done/bin/lib/intel.cjs",
"ts": "sdk/src/query/intel.ts",

View File

@@ -20,7 +20,7 @@ const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/state-document.ts
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.
@@ -50,7 +50,7 @@ function extractFunctionFromSource(source, name) {
return source.slice(start, i + 1);
}
const distUrl = new URL('../dist/query/state-document.js', import.meta.url);
const distUrl = new URL('../dist/state/index.js', import.meta.url);
const {
stateExtractField,
stateReplaceField,

View File

@@ -2,7 +2,7 @@
/**
* Generator for the STATE.md Document Module CJS artifact.
*
* Reads the compiled ESM output from sdk/dist/query/state-document.js,
* Reads the compiled ESM output from sdk/dist/state/index.js,
* extracts function source via Function.prototype.toString() for exports
* and via source-text extraction for internal helpers, then emits
* get-shit-done/bin/lib/state-document.generated.cjs.
@@ -19,7 +19,7 @@ const BANNER = `'use strict';
/**
* GENERATED FILE — DO NOT EDIT.
*
* Source: sdk/src/query/state-document.ts
* Source: sdk/src/state/index.ts
* Regenerate: cd sdk && npm run gen:state-document
*
* STATE.md Document Module — pure transforms for STATE.md text.
@@ -63,7 +63,7 @@ function extractFunctionFromSource(source: string, name: string): string {
export async function buildStateDocumentCjs(): Promise<string> {
// Load the compiled ESM module to get exports via Function.prototype.toString()
const distUrl = new URL('../dist/query/state-document.js', import.meta.url);
const distUrl = new URL('../dist/state/index.js', import.meta.url);
const {
stateExtractField,
stateReplaceField,

View File

@@ -26,7 +26,7 @@ export { SUPPORTED_RUNTIMES, type Runtime } from '../model-catalog.js';
import { SUPPORTED_RUNTIMES, type Runtime } from '../model-catalog.js';
import { canonicalizeRuntimeName } from '../runtime-name-policy.js';
import { workspacePlanningPaths, resolveWorkspaceContext, type PlanningPaths } from './workspace.js';
export { stateExtractField } from './state-document.js';
export { stateExtractField } from '../state/index.js';
import { relPlanningPath, validateWorkstreamName } from '../workstream-utils.js';
// ─── Runtime-aware agents directory resolution ─────────────────────────────

View File

@@ -41,7 +41,7 @@ import {
releaseStateLock,
stateReplaceField,
} from './state-mutation.js';
import { stateExtractField, stateReplaceFieldWithFallback } from './state-document.js';
import { stateExtractField, stateReplaceFieldWithFallback } from '../state/index.js';
import type { QueryHandler } from './utils.js';
import {
assertNoNullBytes,

View File

@@ -36,7 +36,7 @@ import {
} from './helpers.js';
import { buildStateFrontmatter, getMilestonePhaseFilter } from './state.js';
import { scanPhasePlans } from './plan-scan.js';
import { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, computeProgressPercent } from './state-document.js';
import { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, computeProgressPercent } from '../state/index.js';
import type { QueryHandler } from './utils.js';
const PROGRESS_FRONTMATTER_FIELDS = new Set(['Progress', 'Total Plans in Phase', 'Total Phases']);

View File

@@ -30,7 +30,7 @@ import {
normalizeStateStatus,
shouldPreserveExistingProgress,
stateExtractField,
} from './state-document.js';
} from '../state/index.js';
import { getMilestoneInfo, extractCurrentMilestone } from './roadmap.js';
import { scanPhasePlans } from './plan-scan.js';
import type { QueryHandler } from './utils.js';

View File

@@ -12,7 +12,7 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { scanPhasePlans } from './plan-scan.js';
import { stateExtractField } from './state-document.js';
import { stateExtractField } from '../state/index.js';
import { readActiveWorkstream } from './active-workstream-store.js';
import { buildWorkstreamInventory } from '../workstream-inventory/builder.js';

View File

@@ -11,7 +11,7 @@ import {
computeProgressPercent,
shouldPreserveExistingProgress,
normalizeProgressNumbers,
} from './state-document.js';
} from './index.js';
describe('stateExtractField', () => {
it('extracts value from bold pattern', () => {

View File

@@ -4,7 +4,7 @@
* Parity test — verifies that state-document.generated.cjs produces identical
* results to the compiled SDK ESM output for all exported functions.
*
* SDK side: require('../sdk/dist/query/state-document.js') via createRequire
* SDK side: require('../sdk/dist/state/index.js') via createRequire
* CJS side: require('../get-shit-done/bin/lib/state-document.generated.cjs')
*/
@@ -20,7 +20,7 @@ const requireFromRoot = createRequire(__filename);
const cjs = requireFromRoot('../get-shit-done/bin/lib/state-document.generated.cjs');
describe('state-document-generator parity: stateReplaceFieldWithFallback', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
@@ -61,7 +61,7 @@ describe('state-document-generator parity: stateReplaceFieldWithFallback', async
});
describe('state-document-generator parity: normalizeStateStatus', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{ label: 'paused via "paused"', status: 'paused', expected: 'paused' },
@@ -91,7 +91,7 @@ describe('state-document-generator parity: normalizeStateStatus', async () => {
});
describe('state-document-generator parity: computeProgressPercent', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{ label: 'only plans data', cp: 3, tp: 10, cf: null, tf: null, expected: 30 },
@@ -113,7 +113,7 @@ describe('state-document-generator parity: computeProgressPercent', async () =>
});
describe('state-document-generator parity: shouldPreserveExistingProgress', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
@@ -154,7 +154,7 @@ describe('state-document-generator parity: shouldPreserveExistingProgress', asyn
});
describe('state-document-generator parity: normalizeProgressNumbers', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
@@ -188,7 +188,7 @@ describe('state-document-generator parity: normalizeProgressNumbers', async () =
// SDK ESM side — dynamically import so we can test both; wrap in a top-level
// async test suite.
describe('state-document-generator parity: stateExtractField', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{
@@ -223,7 +223,7 @@ describe('state-document-generator parity: stateExtractField', async () => {
});
describe('state-document-generator parity: stateReplaceField', async () => {
const sdk = await import('../sdk/dist/query/state-document.js');
const sdk = await import('../sdk/dist/state/index.js');
const fixtures = [
{