diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index 41893a1c3..e921e8ea0 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -1,5 +1,5 @@ { - "generated": "2026-05-24", + "generated": "2026-05-25", "families": { "agents": [ "gsd-advisor-researcher", @@ -268,16 +268,15 @@ "cjs-sdk-bridge.cjs", "clusters.cjs", "code-review-flags.cjs", - "command-aliases.generated.cjs", + "command-aliases.cjs", "command-routing-hub.cjs", "commands.cjs", "config-schema.cjs", "config.cjs", - "configuration.generated.cjs", + "configuration.cjs", "context-utilization.cjs", "core.cjs", "decisions.cjs", - "decisions.generated.cjs", "docs.cjs", "drift.cjs", "fallow-runner.cjs", @@ -297,17 +296,14 @@ "model-catalog.cjs", "model-profiles.cjs", "phase-command-router.cjs", - "phase-lifecycle-policy.generated.cjs", - "phase-lifecycle.generated.cjs", + "phase-lifecycle.cjs", "phase.cjs", - "phase.generated.cjs", "phases-command-router.cjs", "plan-scan.cjs", - "plan-scan.generated.cjs", "planning-workspace.cjs", "profile-output.cjs", "profile-pipeline.cjs", - "project-root.generated.cjs", + "project-root.cjs", "prompt-budget.cjs", "review-reviewer-selection.cjs", "roadmap-command-router.cjs", @@ -317,27 +313,23 @@ "runtime-name-policy.cjs", "runtime-slash.cjs", "schema-detect.cjs", - "schema-detect.generated.cjs", "secrets.cjs", - "secrets.generated.cjs", "security.cjs", "semver-compare.cjs", "shell-command-projection.cjs", "state-command-router.cjs", "state-document.cjs", - "state-document.generated.cjs", "state.cjs", "surface.cjs", "template.cjs", "uat.cjs", "validate-command-router.cjs", - "validate.generated.cjs", + "validate.cjs", "verify-command-router.cjs", "verify.cjs", - "workstream-inventory-builder.generated.cjs", + "workstream-inventory-builder.cjs", "workstream-inventory.cjs", "workstream-name-policy.cjs", - "workstream-name-policy.generated.cjs", "workstream.cjs", "worktree-safety.cjs" ], diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 603a3cd4e..f96b2f834 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -362,7 +362,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t --- -## CLI Modules (80 shipped) +## CLI Modules (72 shipped) Full listing: `get-shit-done/bin/lib/*.cjs`. @@ -376,16 +376,15 @@ Full listing: `get-shit-done/bin/lib/*.cjs`. | `cjs-sdk-bridge.cjs` | Shared SDK runtime-bridge loader (`tryLoadSdk`/`getExecuteForCjs`); consumed by every CJS router and `gsd-tools.cjs` to delegate canonical commands to the SDK in-process | | `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) | | `code-review-flags.cjs` | Typed flag parser for `/gsd:code-review`; exports `parseCodeReviewFlags(argv)` (→ `{ fix, all, auto, depth, files }`) and `resolveCodeReviewWorkflow(flags)` (→ `'code-review.md' \| 'code-review-fix.md'`); canonical dispatch seam for `--fix`/`--all`/`--auto` routing | -| `command-aliases.generated.cjs` | Generated CJS alias/subcommand metadata for manifest-backed family routers | +| `command-aliases.cjs` | Alias/subcommand metadata for manifest-backed family routers | | `command-routing-hub.cjs` | Pure-result dispatch hub that centralizes mode decision (SDK vs CJS), error taxonomy, and no-throw contract for all command-family routers (#3788) | | `commands.cjs` | Misc CLI commands (slug, timestamp, todos, scaffolding, stats) | | `config-schema.cjs` | Single source of truth for `VALID_CONFIG_KEYS` and dynamic key patterns; imported by both the validator and the config-schema-docs parity test | | `config.cjs` | `config.json` read/write, section initialization; imports validator from `config-schema.cjs` | -| `configuration.generated.cjs` | Generated Configuration Module — canonical config loading, legacy-key normalization, defaults merge, and explicit on-disk migration; source of truth for both SDK and CJS consumers | +| `configuration.cjs` | Configuration Module — canonical config loading, legacy-key normalization, defaults merge, and explicit on-disk migration; source of truth for both SDK and CJS consumers | | `context-utilization.cjs` | Pure classifier for `gsd-health --context` — turns (tokensUsed, contextWindow) into a `{ percent, state }` triage result against the 60%/70% fracture-point thresholds (#2792) | | `core.cjs` | Error handling, output formatting, shared utilities, runtime fallbacks; compatibility re-exports for planning-workspace helpers | -| `decisions.cjs` | CJS shim adapter — re-exports from `decisions.generated.cjs` (Phase 6/#3575 Shared Module migration) | -| `decisions.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/decisions.ts` via `sdk/scripts/gen-decisions.mjs`; parses CONTEXT.md `` blocks, accepts numeric (D-42) and alphanumeric (D-INFRA-01) IDs, returns `{id, text, category, tags, trackable}`; do not edit directly | +| `decisions.cjs` | Parses CONTEXT.md `` blocks; accepts numeric (D-42) and alphanumeric (D-INFRA-01) IDs; returns `{id, text, category, tags, trackable}` | | `docs.cjs` | Docs-update workflow init, Markdown scanning, monorepo detection | | `drift.cjs` | Post-execute codebase structural drift detector (#2003): classifies file changes into new-dir/barrel/migration/route categories and round-trips `last_mapped_commit` frontmatter | | `fallow-runner.cjs` | Fallow audit adapter for `/gsd-code-review`: binary resolution (`PATH` then `node_modules/.bin`), actionable missing-binary errors, and structural findings normalization | @@ -405,15 +404,12 @@ Full listing: `get-shit-done/bin/lib/*.cjs`. | `model-catalog.cjs` | CJS adapter over the shared model catalog JSON; exports canonical runtime tier defaults, agent profile maps, alias maps, and routing metadata for all CLI consumers | | `model-profiles.cjs` | Backward-compatible profile helpers derived from `model-catalog.cjs`; no longer owns its own model table | | `phase-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools phase` | -| `phase-lifecycle-policy.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/phase-lifecycle-policy.ts` via `sdk/scripts/gen-phase-lifecycle-policy.mjs`; pure computation helpers for phase directory naming, roadmap entry generation, decimal-phase management, and ID computation; do not edit directly | -| `phase-lifecycle.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/phase-lifecycle.ts` via `sdk/scripts/gen-phase-lifecycle.mjs`; pure-computation phase lifecycle helpers extracted from the phase-lifecycle SDK handler; do not edit directly | +| `phase-lifecycle.cjs` | Pure-computation phase lifecycle helpers extracted from the phase-lifecycle SDK handler | | `phase.cjs` | Phase directory operations, decimal numbering, plan indexing | -| `phase.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/phase.ts` via `sdk/scripts/gen-phase.mjs`; pure helper functions for phase query shared between the CJS CLI and SDK; no I/O, no async, no filesystem operations; do not edit directly | | `phases-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools phases` | -| `plan-scan.cjs` | CJS shim adapter — re-exports from `plan-scan.generated.cjs` (Phase 6/#3575 Shared Module migration) | -| `plan-scan.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/plan-scan.ts` via `sdk/scripts/gen-plan-scan.mjs`; canonical phase-plan scanner for detecting plan and summary files in flat and nested layouts (k014); do not edit directly | +| `plan-scan.cjs` | Canonical phase-plan scanner for detecting plan and summary files in flat and nested layouts (k014) | | `planning-workspace.cjs` | Planning path/workstream seam (`planningDir`, `planningPaths`, active-workstream routing, `.planning/.lock` orchestration) | -| `project-root.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/project-root/index.ts` via `sdk/scripts/gen-project-root.mjs`; resolves a project root from a starting directory using four heuristics (own `.planning/` guard, `sub_repos` config, `multiRepo` flag, `.git` heuristic); do not edit directly | +| `project-root.cjs` | Resolves a project root from a starting directory using four heuristics (own `.planning/` guard, `sub_repos` config, `multiRepo` flag, `.git` heuristic) | | `profile-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation | | `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning | | `prompt-budget.cjs` | Pure token-budget accounting for review prompts — estimates tokens, applies deterministic trim priority (head-shrink PROJECT.md, proportional plan truncation, drop context/research/requirements, hard-fail guard), returns structured metadata for `review.max_prompt_tokens` (#3081) | @@ -424,28 +420,24 @@ Full listing: `get-shit-done/bin/lib/*.cjs`. | `runtime-name-policy.cjs` | Runtime name normalization policy — canonical token sanitization for runtime identifiers used in path construction and display | | `runtime-homes.cjs` | Canonical runtime → global config/skills directory mapping; first-class support for all 15 runtimes including Hermes nested layout and Cline rules-based exclusion (#3126) | | `runtime-slash.cjs` | Runtime-aware slash-command formatter — single source of truth for emitting `/gsd-` (skills-based runtimes) and `$gsd-` (codex) in user-facing output and persisted artifacts (#3584) | -| `schema-detect.cjs` | CJS shim adapter — re-exports from `schema-detect.generated.cjs` (Phase 6/#3575 Shared Module migration) | -| `schema-detect.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/schema-detect.ts` via `sdk/scripts/gen-schema-detect.mjs`; schema-drift detection for ORM patterns (Prisma, Drizzle, Supabase, TypeORM, Payload); exports `detectSchemaFiles`, `detectSchemaOrm`, `checkSchemaDrift`, `SCHEMA_PATTERNS`, `ORM_INFO`; do not edit directly | -| `secrets.cjs` | CJS shim adapter — re-exports from `secrets.generated.cjs` (Phase 6/#3575 Shared Module migration) | -| `secrets.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/secrets.ts` via `sdk/scripts/gen-secrets.mjs`; secret-config masking convention (`****`) for integration keys; exports `SECRET_CONFIG_KEYS`, `isSecretKey`, `maskSecret`, `maskIfSecret`; do not edit directly | +| `schema-detect.cjs` | Schema-drift detection for ORM patterns (Prisma, Drizzle, Supabase, TypeORM, Payload); exports `detectSchemaFiles`, `detectSchemaOrm`, `checkSchemaDrift`, `SCHEMA_PATTERNS`, `ORM_INFO` | +| `secrets.cjs` | Secret-config masking convention (`****`) for integration keys; exports `SECRET_CONFIG_KEYS`, `isSecretKey`, `maskSecret`, `maskIfSecret` | | `semver-compare.cjs` | Shared semver comparison policy helpers (`compareSemverCore`, stable-triplet validation, normalized tuple parsing) consumed by update-check hooks, statusline dev-install detection, and changeset extract range logic (#10) | | `security.cjs` | Path traversal prevention, prompt injection detection, safe JSON/shell helpers | | `shell-command-projection.cjs` | Runtime-aware shell command projection for managed hook serialization: decides PowerShell call-operator usage by runtime/platform and normalizes Windows script path tokens | | `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/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 | | `validate-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools validate` | -| `validate.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/validate.ts` via `sdk/scripts/gen-validate.mjs`; pure phase variant normalization helpers (`phaseVariants`, `buildRoadmapPhaseVariants`, `buildNotStartedPhaseVariants`) used by `verify.cjs` for W006/W007 checks; no I/O, no async; do not edit directly | +| `validate.cjs` | Pure phase variant normalization helpers (`phaseVariants`, `buildRoadmapPhaseVariants`, `buildNotStartedPhaseVariants`) used by `verify.cjs` for W006/W007 checks; no I/O, no async | | `verify-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools verify` | | `verify.cjs` | Plan structure, phase completeness, reference, commit validation | -| `workstream-inventory-builder.generated.cjs` | GENERATED — pure workstream inventory projection builder; CJS artifact emitted from `sdk/src/workstream/builder.ts` via `sdk/scripts/gen-workstream-inventory-builder.mjs`; do not edit directly | -| `workstream-inventory.cjs` | Shared workstream inventory projection: state fields, phase/plan/summary counts, roadmap phase count, and active marker — thin orchestrator that delegates pure projection to `workstream-inventory-builder.generated.cjs` | -| `workstream-name-policy.cjs` | CJS shim adapter — re-exports from `workstream-name-policy.generated.cjs` (Phase 6/#3575 Shared Module migration) | -| `workstream-name-policy.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/workstream-name-policy.ts` via `sdk/scripts/gen-workstream-name-policy.mjs`; canonical workstream name validation (`isValidActiveWorkstreamName`, `hasInvalidPathSegment`, `validateWorkstreamName`) and slug normalization (`toWorkstreamSlug`); do not edit directly | +| `workstream-inventory-builder.cjs` | Pure workstream inventory projection builder | +| `workstream-inventory.cjs` | Shared workstream inventory projection: state fields, phase/plan/summary counts, roadmap phase count, and active marker — thin orchestrator that delegates pure projection to `workstream-inventory-builder.cjs` | +| `workstream-name-policy.cjs` | Canonical workstream name validation (`isValidActiveWorkstreamName`, `hasInvalidPathSegment`, `validateWorkstreamName`) and slug normalization (`toWorkstreamSlug`) | | `workstream.cjs` | Workstream CRUD, migration, session-scoped active pointer | | `worktree-safety.cjs` | Worktree-root resolution and non-destructive prune policy decisions; owns W017 health-check logic | diff --git a/get-shit-done/bin/lib/command-aliases.generated.cjs b/get-shit-done/bin/lib/command-aliases.cjs similarity index 100% rename from get-shit-done/bin/lib/command-aliases.generated.cjs rename to get-shit-done/bin/lib/command-aliases.cjs diff --git a/get-shit-done/bin/lib/config-schema.cjs b/get-shit-done/bin/lib/config-schema.cjs index 16641d030..6e57dc918 100644 --- a/get-shit-done/bin/lib/config-schema.cjs +++ b/get-shit-done/bin/lib/config-schema.cjs @@ -17,7 +17,7 @@ const { VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS, -} = require('./configuration.generated.cjs'); +} = require('./configuration.cjs'); /** * Returns true if keyPath is a valid config key (exact, dynamic pattern, or runtime state). diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 2b6e48fa8..73bd72a58 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -671,7 +671,7 @@ function cmdConfigPath(cwd) { * summary when --raw is set. Exits 0 in all cases (including no-op). */ async function cmdMigrateConfig(cwd, raw) { - const { migrateOnDisk } = require('./configuration.generated.cjs'); + const { migrateOnDisk } = require('./configuration.cjs'); const ws = process.env.GSD_WORKSTREAM || null; const report = await migrateOnDisk(cwd, ws || undefined); diff --git a/get-shit-done/bin/lib/configuration.generated.cjs b/get-shit-done/bin/lib/configuration.cjs similarity index 100% rename from get-shit-done/bin/lib/configuration.generated.cjs rename to get-shit-done/bin/lib/configuration.cjs diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 305ac8bd6..4a71f2014 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -25,7 +25,7 @@ const { setActiveWorkstream, findContextMdIn, } = require('./planning-workspace.cjs'); -const { findProjectRoot } = require('./project-root.generated.cjs'); +const { findProjectRoot } = require('./project-root.cjs'); // ─── Configuration Module (generated CJS mirror) ──────────────────────────── // Cycle 4: import canonical defaults + normalization primitives from the @@ -36,7 +36,7 @@ const { findProjectRoot } = require('./project-root.generated.cjs'); const { CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS, normalizeLegacyKeys, -} = require('./configuration.generated.cjs'); +} = require('./configuration.cjs'); // ─── Path helpers ──────────────────────────────────────────────────────────── diff --git a/get-shit-done/bin/lib/decisions.cjs b/get-shit-done/bin/lib/decisions.cjs index 68e3ee959..efb2c3f13 100644 --- a/get-shit-done/bin/lib/decisions.cjs +++ b/get-shit-done/bin/lib/decisions.cjs @@ -1,19 +1,121 @@ 'use strict'; /** - * Decisions Module — CJS adapter. - * - * The implementation is generated from sdk/src/query/decisions.ts and - * lives in decisions.generated.cjs. This file is a thin re-export so - * that existing call sites (gap-checker.cjs, tests) can continue to - * require('./decisions') unchanged. - * - * Exports (from generated file): - * - parseDecisions(content) — parse blocks, returns {id, text, category, tags, trackable}[] - * CJS callers using only {id, text} safely ignore the extra fields. - * Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs. + * GENERATED FILE — DO NOT EDIT. * + * Source: sdk/src/query/decisions.ts * Regenerate: cd sdk && npm run gen:decisions + * + * Shared parser for CONTEXT.md blocks. + * Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs. + * Returns {id, text, category, tags, trackable} per decision. + * CJS callers that only use {id, text} safely ignore the extra fields. */ -module.exports = require('./decisions.generated.cjs'); +const DISCRETION_HEADINGS = new Set([ + "claude's discretion", + 'claudes discretion', + 'claude discretion', +]); +const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']); +/** + * Strip fenced code blocks from `content` so example `` snippets + * inside ```` ``` ```` do not pollute the parser (review F11). + */ +function stripFencedCode(content) { + return content.replace(/```[\s\S]*?```/g, ' ').replace(/~~~[\s\S]*?~~~/g, ' '); +} +/** + * Extract the inner text of EVERY `...` block in + * order, concatenated by `\n\n`. Returns null when no block is present. + * + * CONTEXT.md may legitimately contain more than one block (for example, a + * "current decisions" block plus a "carry-over from prior phase" block); + * dropping all-but-the-first silently lost the second batch (review F13). + */ +function extractDecisionsBlock(content) { + const cleaned = stripFencedCode(content); + const matches = [...cleaned.matchAll(/([\s\S]*?)<\/decisions>/g)]; + if (matches.length === 0) + return null; + return matches.map((m) => m[1]).join('\n\n'); +} +/** + * Parse trackable decisions from CONTEXT.md content. + * + * Returns ALL D-NN decisions found inside `` (including + * non-trackable ones, with `trackable: false`). Callers that only want the + * gate-enforced decisions should filter `.filter(d => d.trackable)`. + */ +function parseDecisions(content) { + if (!content || typeof content !== 'string') + return []; + const block = extractDecisionsBlock(content); + if (block === null) + return []; + const lines = block.split(/\r?\n/); + const out = []; + let category = ''; + let inDiscretion = false; + // Bullet line: `- **D-NN[ [tags]]:** text` + // Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR) + // in addition to numeric-only IDs (D-42). The first character after `D-` must + // be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected. + // CJS callers consume {id, text} and ignore the optional extras. + const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/; + let current = null; + const flush = () => { + if (current) { + current.text = current.text.trim(); + out.push(current); + current = null; + } + }; + for (const line of lines) { + const trimmed = line.trim(); + // Track category headings (`### Heading`) + const headingMatch = trimmed.match(/^###\s+(.+?)\s*$/); + if (headingMatch) { + flush(); + category = headingMatch[1]; + // Strip the full unicode-quote family so any rendering of "Claude's + // Discretion" (ASCII apostrophe, curly U+2019, U+2018, U+201A, U+201B, + // double-quote variants U+201C/D/E/F, etc.) collapses to the same key + // (review F20). + const normalized = category + .toLowerCase() + .replace(/[\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F'"`]/g, '') + .trim(); + inDiscretion = DISCRETION_HEADINGS.has(normalized); + continue; + } + const bulletMatch = line.match(bulletRe); + if (bulletMatch) { + flush(); + const id = `D-${bulletMatch[1]}`; + const tags = bulletMatch[2] + ? bulletMatch[2] + .split(',') + .map((t) => t.trim().toLowerCase()) + .filter(Boolean) + : []; + const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t)); + current = { id, text: bulletMatch[3], category, tags, trackable }; + continue; + } + // Continuation line for current decision (indented with space OR tab, + // non-bullet, non-empty) — tab indentation must work too (review F12). + if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) { + current.text += ' ' + trimmed; + continue; + } + // Blank line or unrelated content terminates the current decision + if (trimmed === '') { + flush(); + } + } + flush(); + return out; +} + +module.exports = { parseDecisions }; diff --git a/get-shit-done/bin/lib/decisions.generated.cjs b/get-shit-done/bin/lib/decisions.generated.cjs deleted file mode 100644 index efb2c3f13..000000000 --- a/get-shit-done/bin/lib/decisions.generated.cjs +++ /dev/null @@ -1,121 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/query/decisions.ts - * Regenerate: cd sdk && npm run gen:decisions - * - * Shared parser for CONTEXT.md blocks. - * Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs. - * Returns {id, text, category, tags, trackable} per decision. - * CJS callers that only use {id, text} safely ignore the extra fields. - */ - -const DISCRETION_HEADINGS = new Set([ - "claude's discretion", - 'claudes discretion', - 'claude discretion', -]); -const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']); -/** - * Strip fenced code blocks from `content` so example `` snippets - * inside ```` ``` ```` do not pollute the parser (review F11). - */ -function stripFencedCode(content) { - return content.replace(/```[\s\S]*?```/g, ' ').replace(/~~~[\s\S]*?~~~/g, ' '); -} -/** - * Extract the inner text of EVERY `...` block in - * order, concatenated by `\n\n`. Returns null when no block is present. - * - * CONTEXT.md may legitimately contain more than one block (for example, a - * "current decisions" block plus a "carry-over from prior phase" block); - * dropping all-but-the-first silently lost the second batch (review F13). - */ -function extractDecisionsBlock(content) { - const cleaned = stripFencedCode(content); - const matches = [...cleaned.matchAll(/([\s\S]*?)<\/decisions>/g)]; - if (matches.length === 0) - return null; - return matches.map((m) => m[1]).join('\n\n'); -} -/** - * Parse trackable decisions from CONTEXT.md content. - * - * Returns ALL D-NN decisions found inside `` (including - * non-trackable ones, with `trackable: false`). Callers that only want the - * gate-enforced decisions should filter `.filter(d => d.trackable)`. - */ -function parseDecisions(content) { - if (!content || typeof content !== 'string') - return []; - const block = extractDecisionsBlock(content); - if (block === null) - return []; - const lines = block.split(/\r?\n/); - const out = []; - let category = ''; - let inDiscretion = false; - // Bullet line: `- **D-NN[ [tags]]:** text` - // Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR) - // in addition to numeric-only IDs (D-42). The first character after `D-` must - // be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected. - // CJS callers consume {id, text} and ignore the optional extras. - const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/; - let current = null; - const flush = () => { - if (current) { - current.text = current.text.trim(); - out.push(current); - current = null; - } - }; - for (const line of lines) { - const trimmed = line.trim(); - // Track category headings (`### Heading`) - const headingMatch = trimmed.match(/^###\s+(.+?)\s*$/); - if (headingMatch) { - flush(); - category = headingMatch[1]; - // Strip the full unicode-quote family so any rendering of "Claude's - // Discretion" (ASCII apostrophe, curly U+2019, U+2018, U+201A, U+201B, - // double-quote variants U+201C/D/E/F, etc.) collapses to the same key - // (review F20). - const normalized = category - .toLowerCase() - .replace(/[\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F'"`]/g, '') - .trim(); - inDiscretion = DISCRETION_HEADINGS.has(normalized); - continue; - } - const bulletMatch = line.match(bulletRe); - if (bulletMatch) { - flush(); - const id = `D-${bulletMatch[1]}`; - const tags = bulletMatch[2] - ? bulletMatch[2] - .split(',') - .map((t) => t.trim().toLowerCase()) - .filter(Boolean) - : []; - const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t)); - current = { id, text: bulletMatch[3], category, tags, trackable }; - continue; - } - // Continuation line for current decision (indented with space OR tab, - // non-bullet, non-empty) — tab indentation must work too (review F12). - if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) { - current.text += ' ' + trimmed; - continue; - } - // Blank line or unrelated content terminates the current decision - if (trimmed === '') { - flush(); - } - } - flush(); - return out; -} - -module.exports = { parseDecisions }; diff --git a/get-shit-done/bin/lib/init-command-router.cjs b/get-shit-done/bin/lib/init-command-router.cjs index 1d55770ef..69d6e72c3 100644 --- a/get-shit-done/bin/lib/init-command-router.cjs +++ b/get-shit-done/bin/lib/init-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { INIT_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { INIT_SUBCOMMANDS } = require('./command-aliases.cjs'); const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs'); const { output } = require('./core.cjs'); diff --git a/get-shit-done/bin/lib/phase-command-router.cjs b/get-shit-done/bin/lib/phase-command-router.cjs index 8f935480f..721e5761c 100644 --- a/get-shit-done/bin/lib/phase-command-router.cjs +++ b/get-shit-done/bin/lib/phase-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { PHASE_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { PHASE_SUBCOMMANDS } = require('./command-aliases.cjs'); // ─── CommandRoutingHub (issue #3788, simplified in #175, typed in #176) ─────── const { createHub, ERROR_KINDS, makeInvalidArgs } = require('./command-routing-hub.cjs'); diff --git a/get-shit-done/bin/lib/phase-lifecycle-policy.generated.cjs b/get-shit-done/bin/lib/phase-lifecycle-policy.generated.cjs deleted file mode 100644 index 8af82abab..000000000 --- a/get-shit-done/bin/lib/phase-lifecycle-policy.generated.cjs +++ /dev/null @@ -1,202 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/query/phase-lifecycle-policy.ts - * Regenerate: cd sdk && npm run gen:phase-lifecycle-policy - * - * Phase Lifecycle Policy — pure computation helpers for phase directory naming, - * roadmap entry generation, decimal-phase management, and ID computation. - * No I/O. No async. No filesystem operations. - * - * I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK; - * GSDError is replaced with plain throws that CJS callers can catch. - * - * References: - * - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - * - Issue #4 (open-gsd/get-shit-done-redux) - */ - -// Lightweight stub replacing sdk/src/errors.js GSDError. -// CJS callers that need to translate to process.exit(1) should catch these. -class GSDError extends Error { - constructor(message, classification) { - super(message); - this.name = 'GSDError'; - this.classification = classification; - } -} -// ErrorClassification values used by policy functions -const ErrorClassification = { Validation: 'Validation', Internal: 'Internal' }; - -// escapeRegex — inlined from sdk/dist/query/helpers.js -function escapeRegex(value) { - return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -function assertNoNullBytes(value, label) { - if (value.includes('\0')) { - throw new GSDError(`${label} contains null byte`, ErrorClassification.Validation); - } -} - -function assertSafePhaseDirName(dirName, label = 'phase directory') { - if (/[/\\]|\.\./.test(dirName)) { - throw new GSDError(`${label} contains invalid path segments`, ErrorClassification.Validation); - } -} - -function assertSafeProjectCode(code) { - if (code && /[/\\]|\.\./.test(code)) { - throw new GSDError('project_code contains invalid characters', ErrorClassification.Validation); - } -} - -function generatePhaseSlug(text) { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .substring(0, 60); -} - -function parseMultiwordArg(args, flag) { - const idx = args.indexOf(`--${flag}`); - if (idx === -1) - return null; - const tokens = []; - for (let i = idx + 1; i < args.length; i++) { - if (args[i].startsWith('--')) - break; - tokens.push(args[i]); - } - return tokens.length > 0 ? tokens.join(' ') : null; -} - -function extractOneLinerFromBody(content) { - if (!content) - return null; - const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, ''); - // Find the first heading of any level (GFM section ATX headings) - const headingMatch = body.match(/^(#{1,6}\s[^\n]+\n)/m); - if (!headingMatch || headingMatch.index === undefined) - return null; - const afterHeading = body.slice(headingMatch.index + headingMatch[0].length); - // Bound to the first section: truncate at the next heading of any level - const nextHeadingMatch = afterHeading.match(/^#{1,6}\s/m); - const sectionScope = nextHeadingMatch && nextHeadingMatch.index !== undefined - ? afterHeading.slice(0, nextHeadingMatch.index) - : afterHeading; - const boldMatch = sectionScope.match(/\*\*([^*]+)\*\*/); - return boldMatch ? boldMatch[1].trim() : null; -} - -function scanSequentialMaxPhaseFromMilestone(milestoneContent) { - const phasePattern = /(?:^|\n)\s*(?:[-*]\s*(?:\[[x ]\]\s*)?|#{2,4}\s*|\*{1,2}\s*)Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi; - let maxPhase = 0; - let m; - while ((m = phasePattern.exec(milestoneContent)) !== null) { - const num = parseInt(m[1], 10); - if (num === 999) - continue; - if (num > maxPhase) - maxPhase = num; - } - return maxPhase; -} - -function scanSequentialMaxPhaseFromDirs(dirNames) { - let maxPhase = 0; - const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)[A-Z]?(?:\.\d+)*-/i; - for (const dirName of dirNames) { - const match = dirNumPattern.exec(dirName); - if (!match) - continue; - const num = parseInt(match[1], 10); - if (num === 999) - continue; - if (num > maxPhase) - maxPhase = num; - } - return maxPhase; -} - -function computeNextSequentialPhaseId(milestoneContent, dirNames) { - return Math.max(scanSequentialMaxPhaseFromMilestone(milestoneContent), scanSequentialMaxPhaseFromDirs(dirNames)) + 1; -} - -function computePhaseDirectory(namingMode, descriptionSlug, prefix, nextSequentialPhaseId, customId) { - if (customId || namingMode === 'custom') { - const phaseId = customId || descriptionSlug.toUpperCase().replace(/-/g, '_'); - if (!phaseId) { - throw new GSDError('--id required when phase_naming is "custom"', ErrorClassification.Validation); - } - assertSafePhaseDirName(String(phaseId), 'custom phase id'); - const dirName = `${prefix}${phaseId}-${descriptionSlug}`; - assertSafePhaseDirName(dirName); - return { phaseId, dirName }; - } - const phaseId = nextSequentialPhaseId; - const paddedNum = String(phaseId).padStart(2, '0'); - const dirName = `${prefix}${paddedNum}-${descriptionSlug}`; - assertSafePhaseDirName(dirName); - return { phaseId, dirName }; -} - -function buildPhaseRoadmapEntry(phaseId, description, namingMode) { - const prevPhase = typeof phaseId === 'number' ? phaseId - 1 : null; - const dependsOn = namingMode === 'custom' || prevPhase === null || prevPhase < 1 - ? '' - : `\n**Depends on:** Phase ${prevPhase}`; - return `\n### Phase ${phaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${phaseId} to break down)\n`; -} - -function collectDecimalSuffixesFromDirNames(basePhase, dirNames) { - const decimalSet = new Set(); - const decimalPattern = new RegExp(`^(?:[A-Z][A-Z0-9]*-)?${escapeRegex(basePhase)}\\.(\\d+)`, 'i'); - for (const dir of dirNames) { - const match = dir.match(decimalPattern); - if (match) - decimalSet.add(parseInt(match[1], 10)); - } - return decimalSet; -} - -function collectDecimalSuffixesFromRoadmap(basePhase, roadmapContent) { - const decimalSet = new Set(); - const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${escapeRegex(basePhase)}\\.(\\d+)\\s*:`, 'gi'); - let match; - while ((match = phasePattern.exec(roadmapContent)) !== null) { - decimalSet.add(parseInt(match[1], 10)); - } - return decimalSet; -} - -function computeNextDecimalPhase(basePhase, decimalSet) { - const existing = Array.from(decimalSet) - .sort((a, b) => a - b) - .map((n) => `${basePhase}.${n}`); - const next = decimalSet.size === 0 - ? `${basePhase}.1` - : `${basePhase}.${Math.max(...decimalSet) + 1}`; - return { next, existing }; -} - -module.exports = { - GSDError, - assertNoNullBytes, - assertSafePhaseDirName, - assertSafeProjectCode, - generatePhaseSlug, - parseMultiwordArg, - extractOneLinerFromBody, - scanSequentialMaxPhaseFromMilestone, - scanSequentialMaxPhaseFromDirs, - computeNextSequentialPhaseId, - computePhaseDirectory, - buildPhaseRoadmapEntry, - collectDecimalSuffixesFromDirNames, - collectDecimalSuffixesFromRoadmap, - computeNextDecimalPhase, -}; diff --git a/get-shit-done/bin/lib/phase-lifecycle.generated.cjs b/get-shit-done/bin/lib/phase-lifecycle.cjs similarity index 100% rename from get-shit-done/bin/lib/phase-lifecycle.generated.cjs rename to get-shit-done/bin/lib/phase-lifecycle.cjs diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index b60ea0174..6cd94e8a2 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -22,7 +22,7 @@ const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceFie const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs'); // Generated pure-computation helpers for cmdPhaseComplete (issue #4 fix). // Source: sdk/src/query/phase-lifecycle.ts. Regenerate: node sdk/scripts/gen-phase-lifecycle.mjs -const { deriveProgressFromRoadmap, clampPercent } = require('./phase-lifecycle.generated.cjs'); +const { deriveProgressFromRoadmap, clampPercent } = require('./phase-lifecycle.cjs'); // #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`. // Documented in agents/gsd-planner.md (write_phase_prompt step). The wider diff --git a/get-shit-done/bin/lib/phase.generated.cjs b/get-shit-done/bin/lib/phase.generated.cjs deleted file mode 100644 index 7baa446b6..000000000 --- a/get-shit-done/bin/lib/phase.generated.cjs +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/query/phase.ts - * Regenerate: cd sdk && npm run gen:phase - * - * Phase Query Module — pure helper functions shared between the CJS CLI and SDK. - * No I/O. No async. No filesystem operations. - * - * Scope: isCanonicalPlanFile, describeNonCanonicalPlans. - * Async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain - * per-side per ADR-3524 §4. - * - * References: - * - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md) - * - Issue #4 (open-gsd/get-shit-done-redux) - */ - -const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md'; - -// Regex constants closed over by describeNonCanonicalPlans (from phase.js module scope) -const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i; -const PLAN_PRE_BOUNCE_RE = /-PLAN.*\.pre-bounce\.md$/i; -const looksLikePlanFile = (f) => - /\.md$/i.test(f) - && /PLAN/i.test(f) - && !PLAN_OUTLINE_RE.test(f) - && !PLAN_PRE_BOUNCE_RE.test(f); - -function describeNonCanonicalPlans(dirFiles, matchedFiles) { - const matched = new Set(matchedFiles); - const offenders = dirFiles.filter((f) => looksLikePlanFile(f) && !matched.has(f)); - if (offenders.length === 0) - return null; - return (`Found ${offenders.length} plan-shaped file(s) in this phase that don't match the canonical ` - + `naming convention "{padded_phase}-{NN}-PLAN.md" (or bare "PLAN.md") and were skipped: ` - + offenders.map((f) => `"${f}"`).join(', ') - + `. Rename to the canonical form (e.g. "01-01-PLAN.md") so the executor can detect them. ` - + `See agents/gsd-planner.md write_phase_prompt step for the full contract.`); -} - -module.exports = { - isCanonicalPlanFile, - describeNonCanonicalPlans, -}; diff --git a/get-shit-done/bin/lib/phases-command-router.cjs b/get-shit-done/bin/lib/phases-command-router.cjs index 84407869b..a38a8bb03 100644 --- a/get-shit-done/bin/lib/phases-command-router.cjs +++ b/get-shit-done/bin/lib/phases-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { PHASES_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { PHASES_SUBCOMMANDS } = require('./command-aliases.cjs'); const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs'); const { output } = require('./core.cjs'); diff --git a/get-shit-done/bin/lib/plan-scan.cjs b/get-shit-done/bin/lib/plan-scan.cjs index ece997d85..e58004a82 100644 --- a/get-shit-done/bin/lib/plan-scan.cjs +++ b/get-shit-done/bin/lib/plan-scan.cjs @@ -1,26 +1,97 @@ 'use strict'; /** - * Plan Scan Module — CJS adapter. - * - * The implementation is generated from sdk/src/query/plan-scan.ts and - * lives in plan-scan.generated.cjs. This file is a thin re-export so - * that existing call sites (state.cjs, roadmap.cjs, init.cjs, - * workstream-inventory.cjs, and tests) can continue to require('./plan-scan') - * unchanged. - * - * Exports (from generated file): - * - scanPhasePlans(phaseDir) — canonical phase-plan scanner - * - isRootPlanFile(fileName) — extended filter including /PLAN/i slug layouts - * - isNestedPlanFile(fileName) — nested plans/ subdir filter - * - isRootSummaryFile(fileName) — flat summary file filter - * - isNestedSummaryFile(fileName) — nested summary file filter - * - * The isRootPlanFile helper uses /PLAN/i to match the extended slug layout - * (e.g. 5-PLAN-01-setup-database.md) in addition to bare and canonical forms. - * This was the fix for bug #3128 (roadmap.cjs plan-count regression). + * GENERATED FILE — DO NOT EDIT. * + * Source: sdk/src/query/plan-scan.ts * Regenerate: cd sdk && npm run gen:plan-scan + * + * Plan Scan Module — detects plan and summary files in a phase directory. + * Supports both flat (pre-#3139) and nested (post-#3139) layouts. */ -module.exports = require('./plan-scan.generated.cjs'); +const { existsSync, readdirSync } = require('node:fs'); +const { join } = require('node:path'); + +// Excluded derivative files +const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i; +const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i; + +function isRootPlanFile(fileName) { + if (PLAN_OUTLINE_RE.test(fileName)) + return false; + if (PLAN_PRE_BOUNCE_RE.test(fileName)) + return false; + if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md') + return true; + return /\.md$/i.test(fileName) && /PLAN/i.test(fileName); +} + +function isNestedPlanFile(fileName) { + if (PLAN_OUTLINE_RE.test(fileName)) + return false; + if (PLAN_PRE_BOUNCE_RE.test(fileName)) + return false; + return /^PLAN-\d+.*\.md$/i.test(fileName) || /-PLAN-\d+.*\.md$/i.test(fileName); +} + +function isRootSummaryFile(fileName) { + return fileName.endsWith('-SUMMARY.md') || fileName === 'SUMMARY.md'; +} + +function isNestedSummaryFile(fileName) { + return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName); +} + +function scanPhasePlans(phaseDir) { + let rootFiles; + try { + rootFiles = readdirSync(phaseDir); + } + catch { + return { + planCount: 0, + summaryCount: 0, + completed: false, + hasNestedPlans: false, + planFiles: [], + summaryFiles: [], + }; + } + const rootPlanFiles = rootFiles.filter(isRootPlanFile); + const rootSummaryFiles = rootFiles.filter(isRootSummaryFile); + let nestedPlanFiles = []; + let nestedSummaryFiles = []; + let hasNestedPlans = false; + const nestedDir = join(phaseDir, 'plans'); + if (existsSync(nestedDir)) { + try { + const nestedFiles = readdirSync(nestedDir); + nestedPlanFiles = nestedFiles.filter(isNestedPlanFile); + nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile); + hasNestedPlans = nestedPlanFiles.length > 0; + } + catch { /* ignore unreadable nested layout */ } + } + const planFiles = rootPlanFiles.concat(nestedPlanFiles); + const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles); + const planCount = planFiles.length; + const summaryCount = summaryFiles.length; + return { + planCount, + summaryCount, + completed: planCount > 0 && summaryCount >= planCount, + hasNestedPlans, + planFiles, + summaryFiles, + }; +} + +// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs') +// and also destructure named exports — support both call styles. +module.exports = scanPhasePlans; +module.exports.scanPhasePlans = scanPhasePlans; +module.exports.isRootPlanFile = isRootPlanFile; +module.exports.isNestedPlanFile = isNestedPlanFile; +module.exports.isRootSummaryFile = isRootSummaryFile; +module.exports.isNestedSummaryFile = isNestedSummaryFile; diff --git a/get-shit-done/bin/lib/plan-scan.generated.cjs b/get-shit-done/bin/lib/plan-scan.generated.cjs deleted file mode 100644 index e58004a82..000000000 --- a/get-shit-done/bin/lib/plan-scan.generated.cjs +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/query/plan-scan.ts - * Regenerate: cd sdk && npm run gen:plan-scan - * - * Plan Scan Module — detects plan and summary files in a phase directory. - * Supports both flat (pre-#3139) and nested (post-#3139) layouts. - */ - -const { existsSync, readdirSync } = require('node:fs'); -const { join } = require('node:path'); - -// Excluded derivative files -const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i; -const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i; - -function isRootPlanFile(fileName) { - if (PLAN_OUTLINE_RE.test(fileName)) - return false; - if (PLAN_PRE_BOUNCE_RE.test(fileName)) - return false; - if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md') - return true; - return /\.md$/i.test(fileName) && /PLAN/i.test(fileName); -} - -function isNestedPlanFile(fileName) { - if (PLAN_OUTLINE_RE.test(fileName)) - return false; - if (PLAN_PRE_BOUNCE_RE.test(fileName)) - return false; - return /^PLAN-\d+.*\.md$/i.test(fileName) || /-PLAN-\d+.*\.md$/i.test(fileName); -} - -function isRootSummaryFile(fileName) { - return fileName.endsWith('-SUMMARY.md') || fileName === 'SUMMARY.md'; -} - -function isNestedSummaryFile(fileName) { - return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName); -} - -function scanPhasePlans(phaseDir) { - let rootFiles; - try { - rootFiles = readdirSync(phaseDir); - } - catch { - return { - planCount: 0, - summaryCount: 0, - completed: false, - hasNestedPlans: false, - planFiles: [], - summaryFiles: [], - }; - } - const rootPlanFiles = rootFiles.filter(isRootPlanFile); - const rootSummaryFiles = rootFiles.filter(isRootSummaryFile); - let nestedPlanFiles = []; - let nestedSummaryFiles = []; - let hasNestedPlans = false; - const nestedDir = join(phaseDir, 'plans'); - if (existsSync(nestedDir)) { - try { - const nestedFiles = readdirSync(nestedDir); - nestedPlanFiles = nestedFiles.filter(isNestedPlanFile); - nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile); - hasNestedPlans = nestedPlanFiles.length > 0; - } - catch { /* ignore unreadable nested layout */ } - } - const planFiles = rootPlanFiles.concat(nestedPlanFiles); - const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles); - const planCount = planFiles.length; - const summaryCount = summaryFiles.length; - return { - planCount, - summaryCount, - completed: planCount > 0 && summaryCount >= planCount, - hasNestedPlans, - planFiles, - summaryFiles, - }; -} - -// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs') -// and also destructure named exports — support both call styles. -module.exports = scanPhasePlans; -module.exports.scanPhasePlans = scanPhasePlans; -module.exports.isRootPlanFile = isRootPlanFile; -module.exports.isNestedPlanFile = isNestedPlanFile; -module.exports.isRootSummaryFile = isRootSummaryFile; -module.exports.isNestedSummaryFile = isNestedSummaryFile; diff --git a/get-shit-done/bin/lib/project-root.generated.cjs b/get-shit-done/bin/lib/project-root.cjs similarity index 100% rename from get-shit-done/bin/lib/project-root.generated.cjs rename to get-shit-done/bin/lib/project-root.cjs diff --git a/get-shit-done/bin/lib/roadmap-command-router.cjs b/get-shit-done/bin/lib/roadmap-command-router.cjs index c7ab33a28..3740d255b 100644 --- a/get-shit-done/bin/lib/roadmap-command-router.cjs +++ b/get-shit-done/bin/lib/roadmap-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.cjs'); const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs'); const { output } = require('./core.cjs'); diff --git a/get-shit-done/bin/lib/schema-detect.cjs b/get-shit-done/bin/lib/schema-detect.cjs index 27cca4b16..b1652a6c9 100644 --- a/get-shit-done/bin/lib/schema-detect.cjs +++ b/get-shit-done/bin/lib/schema-detect.cjs @@ -1,21 +1,170 @@ 'use strict'; /** - * Schema Detect Module — CJS adapter. - * - * The implementation is generated from sdk/src/query/schema-detect.ts and - * lives in schema-detect.generated.cjs. This file is a thin re-export so - * that existing call sites (verify.cjs and tests) can continue to - * require('./schema-detect') unchanged. - * - * Exports (from generated file): - * - SCHEMA_PATTERNS — ORM file pattern list - * - ORM_INFO — ORM push commands and evidence patterns - * - detectSchemaFiles(files) — detect schema-relevant files - * - detectSchemaOrm(ormName) — get ORM-specific push command info - * - checkSchemaDrift(changedFiles, executionLog, options) — check for drift + * GENERATED FILE — DO NOT EDIT. * + * Source: sdk/src/query/schema-detect.ts * Regenerate: cd sdk && npm run gen:schema-detect + * + * Schema Drift Detection — detects schema-relevant file changes and verifies + * that the appropriate database push command was executed during a phase. + * This module does not read the filesystem directly. */ -module.exports = require('./schema-detect.generated.cjs'); +// ─── ORM Patterns ─────────────────────────────────────────────────────────── +const SCHEMA_PATTERNS = [ + { pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' }, + { pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' }, + { pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' }, + { pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' }, + { pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' }, + { pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' }, + { pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' }, + { pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' }, + { pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' }, + { pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' }, +]; + +// ─── Push Commands & Evidence Patterns ────────────────────────────────────── +const ORM_INFO = { + payload: { + pushCommand: 'npx payload migrate', + envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate', + interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress', + evidencePatterns: [/payload\s+migrate/i, /PAYLOAD_MIGRATING/], + }, + prisma: { + pushCommand: 'npx prisma db push', + envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)', + interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass', + evidencePatterns: [/prisma\s+db\s+push/i, /prisma\s+migrate\s+deploy/i, /prisma\s+migrate\s+dev/i], + }, + drizzle: { + pushCommand: 'npx drizzle-kit push', + envHint: 'npx drizzle-kit push', + interactiveWarning: null, + evidencePatterns: [/drizzle-kit\s+push/i, /drizzle-kit\s+migrate/i], + }, + supabase: { + pushCommand: 'supabase db push', + envHint: 'supabase db push', + interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set', + evidencePatterns: [/supabase\s+db\s+push/i, /supabase\s+migration\s+up/i], + }, + typeorm: { + pushCommand: 'npx typeorm migration:run', + envHint: 'npx typeorm migration:run -d src/data-source.ts', + interactiveWarning: null, + evidencePatterns: [/typeorm\s+migration:run/i, /typeorm\s+schema:sync/i], + }, +}; + +// ─── Public API ────────────────────────────────────────────────────────────── +function detectSchemaFiles(files) { + const matches = []; + const orms = new Set(); + for (const rawFile of files) { + const file = rawFile.replace(/\\/g, '/'); + for (const { pattern, orm } of SCHEMA_PATTERNS) { + if (pattern.test(file)) { + matches.push(rawFile); + orms.add(orm); + break; + } + } + } + return { + detected: matches.length > 0, + matches, + orms: [...orms], + }; +} + +function detectSchemaOrm(ormName) { + return ORM_INFO[ormName] || null; +} + +function checkSchemaDrift(changedFiles, executionLog, options = {}) { + const { skipCheck = false } = options; + const detection = detectSchemaFiles(changedFiles); + if (!detection.detected) { + return { + driftDetected: false, + blocking: false, + schemaFiles: [], + orms: [], + unpushedOrms: [], + message: '', + }; + } + const pushedOrms = new Set(); + const unpushedOrms = []; + for (const orm of detection.orms) { + const info = ORM_INFO[orm]; + if (!info) + continue; + const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog)); + if (hasPushEvidence) { + pushedOrms.add(orm); + } + else { + unpushedOrms.push(orm); + } + } + const driftDetected = unpushedOrms.length > 0; + if (!driftDetected) { + return { + driftDetected: false, + blocking: false, + schemaFiles: detection.matches, + orms: detection.orms, + unpushedOrms: [], + message: '', + }; + } + const pushCommands = unpushedOrms + .map(orm => { + const info = ORM_INFO[orm]; + return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null; + }) + .filter(Boolean) + .join('\n'); + const message = [ + 'Schema drift detected: schema-relevant files changed but no database push was executed.', + '', + `Schema files changed: ${detection.matches.join(', ')}`, + `ORMs requiring push: ${unpushedOrms.join(', ')}`, + '', + 'Required push commands:', + pushCommands, + '', + 'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.', + ].join('\n'); + if (skipCheck) { + return { + driftDetected: true, + blocking: false, + skipped: true, + schemaFiles: detection.matches, + orms: detection.orms, + unpushedOrms, + message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).', + }; + } + return { + driftDetected: true, + blocking: true, + schemaFiles: detection.matches, + orms: detection.orms, + unpushedOrms, + message, + }; +} + +module.exports = { + SCHEMA_PATTERNS, + ORM_INFO, + detectSchemaFiles, + detectSchemaOrm, + checkSchemaDrift, +}; diff --git a/get-shit-done/bin/lib/schema-detect.generated.cjs b/get-shit-done/bin/lib/schema-detect.generated.cjs deleted file mode 100644 index b1652a6c9..000000000 --- a/get-shit-done/bin/lib/schema-detect.generated.cjs +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/query/schema-detect.ts - * Regenerate: cd sdk && npm run gen:schema-detect - * - * Schema Drift Detection — detects schema-relevant file changes and verifies - * that the appropriate database push command was executed during a phase. - * This module does not read the filesystem directly. - */ - -// ─── ORM Patterns ─────────────────────────────────────────────────────────── -const SCHEMA_PATTERNS = [ - { pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' }, - { pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' }, - { pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' }, - { pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' }, - { pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' }, - { pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' }, - { pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' }, - { pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' }, - { pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' }, - { pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' }, -]; - -// ─── Push Commands & Evidence Patterns ────────────────────────────────────── -const ORM_INFO = { - payload: { - pushCommand: 'npx payload migrate', - envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate', - interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress', - evidencePatterns: [/payload\s+migrate/i, /PAYLOAD_MIGRATING/], - }, - prisma: { - pushCommand: 'npx prisma db push', - envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)', - interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass', - evidencePatterns: [/prisma\s+db\s+push/i, /prisma\s+migrate\s+deploy/i, /prisma\s+migrate\s+dev/i], - }, - drizzle: { - pushCommand: 'npx drizzle-kit push', - envHint: 'npx drizzle-kit push', - interactiveWarning: null, - evidencePatterns: [/drizzle-kit\s+push/i, /drizzle-kit\s+migrate/i], - }, - supabase: { - pushCommand: 'supabase db push', - envHint: 'supabase db push', - interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set', - evidencePatterns: [/supabase\s+db\s+push/i, /supabase\s+migration\s+up/i], - }, - typeorm: { - pushCommand: 'npx typeorm migration:run', - envHint: 'npx typeorm migration:run -d src/data-source.ts', - interactiveWarning: null, - evidencePatterns: [/typeorm\s+migration:run/i, /typeorm\s+schema:sync/i], - }, -}; - -// ─── Public API ────────────────────────────────────────────────────────────── -function detectSchemaFiles(files) { - const matches = []; - const orms = new Set(); - for (const rawFile of files) { - const file = rawFile.replace(/\\/g, '/'); - for (const { pattern, orm } of SCHEMA_PATTERNS) { - if (pattern.test(file)) { - matches.push(rawFile); - orms.add(orm); - break; - } - } - } - return { - detected: matches.length > 0, - matches, - orms: [...orms], - }; -} - -function detectSchemaOrm(ormName) { - return ORM_INFO[ormName] || null; -} - -function checkSchemaDrift(changedFiles, executionLog, options = {}) { - const { skipCheck = false } = options; - const detection = detectSchemaFiles(changedFiles); - if (!detection.detected) { - return { - driftDetected: false, - blocking: false, - schemaFiles: [], - orms: [], - unpushedOrms: [], - message: '', - }; - } - const pushedOrms = new Set(); - const unpushedOrms = []; - for (const orm of detection.orms) { - const info = ORM_INFO[orm]; - if (!info) - continue; - const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog)); - if (hasPushEvidence) { - pushedOrms.add(orm); - } - else { - unpushedOrms.push(orm); - } - } - const driftDetected = unpushedOrms.length > 0; - if (!driftDetected) { - return { - driftDetected: false, - blocking: false, - schemaFiles: detection.matches, - orms: detection.orms, - unpushedOrms: [], - message: '', - }; - } - const pushCommands = unpushedOrms - .map(orm => { - const info = ORM_INFO[orm]; - return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null; - }) - .filter(Boolean) - .join('\n'); - const message = [ - 'Schema drift detected: schema-relevant files changed but no database push was executed.', - '', - `Schema files changed: ${detection.matches.join(', ')}`, - `ORMs requiring push: ${unpushedOrms.join(', ')}`, - '', - 'Required push commands:', - pushCommands, - '', - 'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.', - ].join('\n'); - if (skipCheck) { - return { - driftDetected: true, - blocking: false, - skipped: true, - schemaFiles: detection.matches, - orms: detection.orms, - unpushedOrms, - message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).', - }; - } - return { - driftDetected: true, - blocking: true, - schemaFiles: detection.matches, - orms: detection.orms, - unpushedOrms, - message, - }; -} - -module.exports = { - SCHEMA_PATTERNS, - ORM_INFO, - detectSchemaFiles, - detectSchemaOrm, - checkSchemaDrift, -}; diff --git a/get-shit-done/bin/lib/secrets.cjs b/get-shit-done/bin/lib/secrets.cjs index 7e28d4bc3..af6ed35c2 100644 --- a/get-shit-done/bin/lib/secrets.cjs +++ b/get-shit-done/bin/lib/secrets.cjs @@ -1,20 +1,37 @@ 'use strict'; /** - * Secrets Module — CJS adapter. - * - * The implementation is generated from sdk/src/query/secrets.ts and - * lives in secrets.generated.cjs. This file is a thin re-export so - * that existing call sites (config.cjs, init.cjs, and tests) can - * continue to require('./secrets') unchanged. - * - * Exports (from generated file): - * - SECRET_CONFIG_KEYS — Set of secret key paths - * - isSecretKey(keyPath) — returns true if keyPath is a secret - * - maskSecret(value) — masks a secret value - * - maskIfSecret(keyPath, value) — masks value only if keyPath is secret + * GENERATED FILE — DO NOT EDIT. * + * Source: sdk/src/query/secrets.ts * Regenerate: cd sdk && npm run gen:secrets + * + * Secrets handling — masking convention for API keys and other + * credentials managed via /gsd-settings-integrations. + * This module does not read the filesystem. */ -module.exports = require('./secrets.generated.cjs'); +const SECRET_CONFIG_KEYS = new Set([ + 'brave_search', + 'firecrawl', + 'exa_search', +]); + +function isSecretKey(keyPath) { + return SECRET_CONFIG_KEYS.has(keyPath); +} + +function maskSecret(value) { + if (value === null || value === undefined || value === '') + return '(unset)'; + const s = String(value); + if (s.length < 8) + return '****'; + return '****' + s.slice(-4); +} + +function maskIfSecret(keyPath, value) { + return isSecretKey(keyPath) ? maskSecret(value) : value; +} + +module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret }; diff --git a/get-shit-done/bin/lib/secrets.generated.cjs b/get-shit-done/bin/lib/secrets.generated.cjs deleted file mode 100644 index af6ed35c2..000000000 --- a/get-shit-done/bin/lib/secrets.generated.cjs +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/query/secrets.ts - * Regenerate: cd sdk && npm run gen:secrets - * - * Secrets handling — masking convention for API keys and other - * credentials managed via /gsd-settings-integrations. - * This module does not read the filesystem. - */ - -const SECRET_CONFIG_KEYS = new Set([ - 'brave_search', - 'firecrawl', - 'exa_search', -]); - -function isSecretKey(keyPath) { - return SECRET_CONFIG_KEYS.has(keyPath); -} - -function maskSecret(value) { - if (value === null || value === undefined || value === '') - return '(unset)'; - const s = String(value); - if (s.length < 8) - return '****'; - return '****' + s.slice(-4); -} - -function maskIfSecret(keyPath, value) { - return isSecretKey(keyPath) ? maskSecret(value) : value; -} - -module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret }; diff --git a/get-shit-done/bin/lib/state-command-router.cjs b/get-shit-done/bin/lib/state-command-router.cjs index ff7b3829f..ddd3034ad 100644 --- a/get-shit-done/bin/lib/state-command-router.cjs +++ b/get-shit-done/bin/lib/state-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { STATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { STATE_SUBCOMMANDS } = require('./command-aliases.cjs'); const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs'); const { output } = require('./core.cjs'); const { diff --git a/get-shit-done/bin/lib/state-document.cjs b/get-shit-done/bin/lib/state-document.cjs index d7b6b53dd..eff4b2e9b 100644 --- a/get-shit-done/bin/lib/state-document.cjs +++ b/get-shit-done/bin/lib/state-document.cjs @@ -1,12 +1,127 @@ 'use strict'; /** - * STATE.md Document Module — CJS adapter. + * GENERATED FILE — DO NOT EDIT. * - * 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. + * Source: sdk/src/state/index.ts + * Regenerate: cd sdk && npm run gen:state-document + * + * STATE.md Document Module — pure transforms for STATE.md text. + * This module does not read the filesystem and does not own persistence or locking. */ -module.exports = require('./state-document.generated.cjs'); +// Internal helpers +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function toFiniteNumber(value) { + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function existingProgressExceedsDerived(existingProgress, derivedProgress, key) { + const existing = toFiniteNumber(existingProgress[key]); + const derived = toFiniteNumber(derivedProgress[key]); + return existing !== null && derived !== null && existing > derived; +} + +function stateExtractField(content, fieldName) { + const escaped = escapeRegex(fieldName); + const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i'); + const boldMatch = content.match(boldPattern); + if (boldMatch) + return boldMatch[1].trim(); + const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im'); + const plainMatch = content.match(plainPattern); + return plainMatch ? plainMatch[1].trim() : null; +} + +function stateReplaceField(content, fieldName, newValue) { + const escaped = escapeRegex(fieldName); + const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i'); + if (boldPattern.test(content)) { + return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`); + } + const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im'); + if (plainPattern.test(content)) { + return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`); + } + return null; +} + +function stateReplaceFieldWithFallback(content, primary, fallback, value) { + let result = stateReplaceField(content, primary, value); + if (result) + return result; + if (fallback) { + result = stateReplaceField(content, fallback, value); + if (result) + return result; + } + return content; +} + +function normalizeStateStatus(status, pausedAt) { + let normalizedStatus = status || 'unknown'; + const statusLower = (status || '').toLowerCase(); + if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) { + normalizedStatus = 'paused'; + } + else if (statusLower.includes('executing') || statusLower.includes('in progress')) { + normalizedStatus = 'executing'; + } + else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) { + normalizedStatus = 'planning'; + } + else if (statusLower.includes('discussing')) { + normalizedStatus = 'discussing'; + } + else if (statusLower.includes('verif')) { + normalizedStatus = 'verifying'; + } + else if (statusLower.includes('complete') || statusLower.includes('done')) { + normalizedStatus = 'completed'; + } + else if (statusLower.includes('ready to execute')) { + normalizedStatus = 'executing'; + } + return normalizedStatus; +} + +function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) { + const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null; + const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null; + if (!hasPlanData && !hasPhaseData) + return null; + const planFraction = hasPlanData ? completedPlans / totalPlans : 1; + const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1; + return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100)); +} + +function shouldPreserveExistingProgress(existingProgress, derivedProgress) { + if (!existingProgress || typeof existingProgress !== 'object') + return false; + if (!derivedProgress || typeof derivedProgress !== 'object') + return false; + const existing = existingProgress; + const derived = derivedProgress; + return (existingProgressExceedsDerived(existing, derived, 'total_phases') || + existingProgressExceedsDerived(existing, derived, 'completed_phases') || + existingProgressExceedsDerived(existing, derived, 'total_plans') || + existingProgressExceedsDerived(existing, derived, 'completed_plans')); +} + +function normalizeProgressNumbers(progress) { + if (!progress || typeof progress !== 'object') + return progress; + const normalized = { ...progress }; + for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) { + const number = toFiniteNumber(normalized[key]); + if (number !== null) + normalized[key] = number; + } + return normalized; +} + +module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers }; diff --git a/get-shit-done/bin/lib/state-document.generated.cjs b/get-shit-done/bin/lib/state-document.generated.cjs deleted file mode 100644 index eff4b2e9b..000000000 --- a/get-shit-done/bin/lib/state-document.generated.cjs +++ /dev/null @@ -1,127 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/state/index.ts - * Regenerate: cd sdk && npm run gen:state-document - * - * STATE.md Document Module — pure transforms for STATE.md text. - * This module does not read the filesystem and does not own persistence or locking. - */ - -// Internal helpers -function escapeRegex(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -function toFiniteNumber(value) { - const number = Number(value); - return Number.isFinite(number) ? number : null; -} - -function existingProgressExceedsDerived(existingProgress, derivedProgress, key) { - const existing = toFiniteNumber(existingProgress[key]); - const derived = toFiniteNumber(derivedProgress[key]); - return existing !== null && derived !== null && existing > derived; -} - -function stateExtractField(content, fieldName) { - const escaped = escapeRegex(fieldName); - const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i'); - const boldMatch = content.match(boldPattern); - if (boldMatch) - return boldMatch[1].trim(); - const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im'); - const plainMatch = content.match(plainPattern); - return plainMatch ? plainMatch[1].trim() : null; -} - -function stateReplaceField(content, fieldName, newValue) { - const escaped = escapeRegex(fieldName); - const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i'); - if (boldPattern.test(content)) { - return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`); - } - const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im'); - if (plainPattern.test(content)) { - return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`); - } - return null; -} - -function stateReplaceFieldWithFallback(content, primary, fallback, value) { - let result = stateReplaceField(content, primary, value); - if (result) - return result; - if (fallback) { - result = stateReplaceField(content, fallback, value); - if (result) - return result; - } - return content; -} - -function normalizeStateStatus(status, pausedAt) { - let normalizedStatus = status || 'unknown'; - const statusLower = (status || '').toLowerCase(); - if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) { - normalizedStatus = 'paused'; - } - else if (statusLower.includes('executing') || statusLower.includes('in progress')) { - normalizedStatus = 'executing'; - } - else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) { - normalizedStatus = 'planning'; - } - else if (statusLower.includes('discussing')) { - normalizedStatus = 'discussing'; - } - else if (statusLower.includes('verif')) { - normalizedStatus = 'verifying'; - } - else if (statusLower.includes('complete') || statusLower.includes('done')) { - normalizedStatus = 'completed'; - } - else if (statusLower.includes('ready to execute')) { - normalizedStatus = 'executing'; - } - return normalizedStatus; -} - -function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) { - const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null; - const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null; - if (!hasPlanData && !hasPhaseData) - return null; - const planFraction = hasPlanData ? completedPlans / totalPlans : 1; - const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1; - return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100)); -} - -function shouldPreserveExistingProgress(existingProgress, derivedProgress) { - if (!existingProgress || typeof existingProgress !== 'object') - return false; - if (!derivedProgress || typeof derivedProgress !== 'object') - return false; - const existing = existingProgress; - const derived = derivedProgress; - return (existingProgressExceedsDerived(existing, derived, 'total_phases') || - existingProgressExceedsDerived(existing, derived, 'completed_phases') || - existingProgressExceedsDerived(existing, derived, 'total_plans') || - existingProgressExceedsDerived(existing, derived, 'completed_plans')); -} - -function normalizeProgressNumbers(progress) { - if (!progress || typeof progress !== 'object') - return progress; - const normalized = { ...progress }; - for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) { - const number = toFiniteNumber(normalized[key]); - if (number !== null) - normalized[key] = number; - } - return normalized; -} - -module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers }; diff --git a/get-shit-done/bin/lib/validate-command-router.cjs b/get-shit-done/bin/lib/validate-command-router.cjs index 60fc61ae4..dc8d1e309 100644 --- a/get-shit-done/bin/lib/validate-command-router.cjs +++ b/get-shit-done/bin/lib/validate-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.cjs'); const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs'); const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs'); const { output } = require('./core.cjs'); diff --git a/get-shit-done/bin/lib/validate.generated.cjs b/get-shit-done/bin/lib/validate.cjs similarity index 100% rename from get-shit-done/bin/lib/validate.generated.cjs rename to get-shit-done/bin/lib/validate.cjs diff --git a/get-shit-done/bin/lib/verify-command-router.cjs b/get-shit-done/bin/lib/verify-command-router.cjs index c581b830a..918955bc5 100644 --- a/get-shit-done/bin/lib/verify-command-router.cjs +++ b/get-shit-done/bin/lib/verify-command-router.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { VERIFY_SUBCOMMANDS } = require('./command-aliases.generated.cjs'); +const { VERIFY_SUBCOMMANDS } = require('./command-aliases.cjs'); const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs'); const { output } = require('./core.cjs'); diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 4acb47416..4383e7b57 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -7,7 +7,7 @@ const { phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants, // Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper) phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem, -} = require('./validate.generated.cjs'); +} = require('./validate.cjs'); const fs = require('fs'); const path = require('path'); diff --git a/get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs b/get-shit-done/bin/lib/workstream-inventory-builder.cjs similarity index 100% rename from get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs rename to get-shit-done/bin/lib/workstream-inventory-builder.cjs diff --git a/get-shit-done/bin/lib/workstream-inventory.cjs b/get-shit-done/bin/lib/workstream-inventory.cjs index 8e84424ab..a8cc32049 100644 --- a/get-shit-done/bin/lib/workstream-inventory.cjs +++ b/get-shit-done/bin/lib/workstream-inventory.cjs @@ -17,7 +17,7 @@ const { toPosixPath, readSubdirectories } = require('./core.cjs'); const scanPhasePlans = require('./plan-scan.cjs'); const { planningPaths, planningRoot, getActiveWorkstream } = require('./planning-workspace.cjs'); const { stateExtractField } = require('./state-document.cjs'); -const { buildWorkstreamInventory, isCompletedInventory } = require('./workstream-inventory-builder.generated.cjs'); +const { buildWorkstreamInventory, isCompletedInventory } = require('./workstream-inventory-builder.cjs'); function workstreamsRoot(cwd) { return path.join(planningRoot(cwd), 'workstreams'); diff --git a/get-shit-done/bin/lib/workstream-name-policy.cjs b/get-shit-done/bin/lib/workstream-name-policy.cjs index 61c58e7e8..27f1ec23e 100644 --- a/get-shit-done/bin/lib/workstream-name-policy.cjs +++ b/get-shit-done/bin/lib/workstream-name-policy.cjs @@ -1,19 +1,61 @@ +'use strict'; + /** - * Workstream Name Policy Module — CJS adapter. - * - * The implementation is generated from sdk/src/workstream-name-policy.ts and - * lives in workstream-name-policy.generated.cjs. This file is a thin re-export - * so that existing call sites (active-workstream-store.cjs, - * planning-workspace.cjs, workstream.cjs, and tests) can continue to - * require('./workstream-name-policy') unchanged. - * - * Exports (from generated file): - * - toWorkstreamSlug(name) — normalize to URL/filesystem slug - * - hasInvalidPathSegment(name) — true if name has slashes or dot-dot - * - isValidActiveWorkstreamName(name) — true if name passes all policy rules - * - validateWorkstreamName(name) — SDK alias for isValidActiveWorkstreamName + * GENERATED FILE — DO NOT EDIT. * + * Source: sdk/src/workstream-name-policy.ts * Regenerate: cd sdk && npm run gen:workstream-name-policy + * + * Canonical workstream name validation and slug normalization. + * Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs. */ -module.exports = require('./workstream-name-policy.generated.cjs'); +const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; +/** + * Validate a workstream name. + * Allowed: alphanumeric, hyphens, underscores, dots. + * Disallowed: empty, spaces, slashes, special chars, path traversal. + * + * Alias for isValidActiveWorkstreamName; provided for SDK-layer callers. + */ +function validateWorkstreamName(name) { + return isValidActiveWorkstreamName(name); +} +/** + * Convert a display name to a URL/filesystem-safe workstream slug. + * Lowercases, collapses non-alphanumeric runs to hyphens, strips leading/trailing hyphens. + */ +function toWorkstreamSlug(name) { + return String(name || '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} +/** + * Returns true when `name` contains a path separator, a bare dot, or a + * dot-dot sequence — any of which would make the name unsafe for use as a + * filesystem path segment. + */ +function hasInvalidPathSegment(name) { + const value = String(name || ''); + return /[/\\]/.test(value) || value === '.' || value === '..' || value.includes('..'); +} +/** + * Returns true when `name` is a valid active workstream name: + * - Must start with alphanumeric + * - May contain alphanumeric, dots, underscores, hyphens + * - Must not contain path traversal sequences (..) + */ +function isValidActiveWorkstreamName(name) { + const value = String(name || ''); + if (value === '..' || value.startsWith('../') || value.includes('..')) + return false; + return ACTIVE_WORKSTREAM_RE.test(value); +} + +module.exports = { + validateWorkstreamName, + toWorkstreamSlug, + hasInvalidPathSegment, + isValidActiveWorkstreamName, +}; diff --git a/get-shit-done/bin/lib/workstream-name-policy.generated.cjs b/get-shit-done/bin/lib/workstream-name-policy.generated.cjs deleted file mode 100644 index 27f1ec23e..000000000 --- a/get-shit-done/bin/lib/workstream-name-policy.generated.cjs +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -/** - * GENERATED FILE — DO NOT EDIT. - * - * Source: sdk/src/workstream-name-policy.ts - * Regenerate: cd sdk && npm run gen:workstream-name-policy - * - * Canonical workstream name validation and slug normalization. - * Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs. - */ - -const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; -/** - * Validate a workstream name. - * Allowed: alphanumeric, hyphens, underscores, dots. - * Disallowed: empty, spaces, slashes, special chars, path traversal. - * - * Alias for isValidActiveWorkstreamName; provided for SDK-layer callers. - */ -function validateWorkstreamName(name) { - return isValidActiveWorkstreamName(name); -} -/** - * Convert a display name to a URL/filesystem-safe workstream slug. - * Lowercases, collapses non-alphanumeric runs to hyphens, strips leading/trailing hyphens. - */ -function toWorkstreamSlug(name) { - return String(name || '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); -} -/** - * Returns true when `name` contains a path separator, a bare dot, or a - * dot-dot sequence — any of which would make the name unsafe for use as a - * filesystem path segment. - */ -function hasInvalidPathSegment(name) { - const value = String(name || ''); - return /[/\\]/.test(value) || value === '.' || value === '..' || value.includes('..'); -} -/** - * Returns true when `name` is a valid active workstream name: - * - Must start with alphanumeric - * - May contain alphanumeric, dots, underscores, hyphens - * - Must not contain path traversal sequences (..) - */ -function isValidActiveWorkstreamName(name) { - const value = String(name || ''); - if (value === '..' || value.startsWith('../') || value.includes('..')) - return false; - return ACTIVE_WORKSTREAM_RE.test(value); -} - -module.exports = { - validateWorkstreamName, - toWorkstreamSlug, - hasInvalidPathSegment, - isValidActiveWorkstreamName, -}; diff --git a/package-lock.json b/package-lock.json index 7bf7b013c..7fdefccec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "c8": "^11.0.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.0.0", + "npm": ">=10.0.0" }, "optionalDependencies": { "fallow": "^2.70.0" diff --git a/package.json b/package.json index 5b15b0ed0..302af8374 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "scripts": { "check:env": "bash scripts/check-env.sh", "check:integrity": "./scripts/check-npm-integrity.sh", - "build": "tsc -b", + "build": "npm run build:sdk", "build:hooks": "node scripts/build-hooks.js", "build:sdk": "cd sdk && npm ci && npm run build", "prepublishOnly": "npm run build:hooks && npm run build:sdk", diff --git a/scripts/shared-module-handsync-allowlist.json b/scripts/shared-module-handsync-allowlist.json index 160c3db6e..76f1bee76 100644 --- a/scripts/shared-module-handsync-allowlist.json +++ b/scripts/shared-module-handsync-allowlist.json @@ -32,6 +32,12 @@ "classification": "cooperating-sibling", "justification": "CJS phase.cjs implements sync mutation handlers (phaseAdd, phaseInsert, phaseRemove, phaseComplete) and query handlers with sync I/O; SDK phase.ts provides the native async query handler. The async mutation handlers (phase-lifecycle.ts) and pure policy helpers (phase-lifecycle-policy.ts) are I/O-bound or pure and remain per-side (ADR-3524 §4). Issue #4 fix: phase.cjs cmdPhaseComplete now consumes pure helpers from phase-lifecycle.generated.cjs (generated from sdk/src/query/phase-lifecycle.ts) for idempotent Completed Phases counting and clamped percent. *.generated.cjs files are excluded from lint scanner; see sdk/scripts/check-phase-lifecycle-fresh.mjs for freshness enforcement." }, + { + "cjs": "get-shit-done/bin/lib/phase-lifecycle.cjs", + "ts": "sdk/src/query/phase-lifecycle.ts", + "classification": "ADAPTER-OVER-MODULE", + "justification": "CJS phase-lifecycle.cjs is the pure helper module consumed by phase.cjs; sdk/src/query/phase-lifecycle.ts remains the typed source counterpart. This pair is intentionally tracked as adapter-over-module rather than an unauthorized hand-sync pair." + }, { "cjs": "get-shit-done/bin/lib/profile-output.cjs", "ts": "sdk/src/query/profile-output.ts", @@ -74,6 +80,12 @@ "classification": "cooperating-sibling", "justification": "CJS verify.cjs is the full verify implementation; SDK verify.ts provides the native handler. Phase 5.2+ candidate for further delegation. Check 8 (W006/W007) helpers now generated from sdk/src/query/validate.ts via sdk/scripts/gen-validate.mjs (issue #6); freshness check: sdk/scripts/check-validate-fresh.mjs." }, + { + "cjs": "get-shit-done/bin/lib/validate.cjs", + "ts": "sdk/src/query/validate.ts", + "classification": "ADAPTER-OVER-MODULE", + "justification": "CJS validate.cjs provides the pure validation helpers consumed by verify.cjs while sdk/src/query/validate.ts is the SDK handler-side counterpart. Keep this explicit adapter pair allowlisted to avoid false-positive hand-sync lint failures." + }, { "cjs": "get-shit-done/bin/lib/workstream.cjs", "ts": "sdk/src/query/workstream.ts", diff --git a/tests/26-w005-w006-i001-cjs-drift-regression.test.cjs b/tests/26-w005-w006-i001-cjs-drift-regression.test.cjs index 134d00e67..1dc51f4c9 100644 --- a/tests/26-w005-w006-i001-cjs-drift-regression.test.cjs +++ b/tests/26-w005-w006-i001-cjs-drift-regression.test.cjs @@ -7,7 +7,7 @@ * I001 canonicalPlanStem. * * After the generator migration, all three helpers are sourced from - * validate.generated.cjs. If they diverge from validate.ts, these + * validate.cjs. If they diverge from validate.ts, these * tests go RED. * * References: @@ -62,7 +62,7 @@ function writeConfigJson(planningDir) { // node .claude/get-shit-done/bin/gsd-tools.cjs validate health // # Bug: emits W005 about 999.1-foo not following NN-name format // -// verify.cjs must consume phaseDirNameRe from validate.generated.cjs so +// verify.cjs must consume phaseDirNameRe from validate.cjs so // the regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ is the single source of truth. describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W005', () => { @@ -95,10 +95,10 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W `Expected zero W005 for 999.1-foo, got: ${JSON.stringify(w005)}`); }); - test('phaseDirNameRe is exported from validate.generated.cjs', () => { - const gen = require('../get-shit-done/bin/lib/validate.generated.cjs'); + test('phaseDirNameRe is exported from validate.cjs', () => { + const gen = require('../get-shit-done/bin/lib/validate.cjs'); assert.ok(gen.phaseDirNameRe instanceof RegExp, - 'validate.generated.cjs must export phaseDirNameRe as a RegExp'); + 'validate.cjs must export phaseDirNameRe as a RegExp'); const re = gen.phaseDirNameRe; assert.ok(re.test('01-setup'), 'should accept 01-setup'); assert.ok(re.test('999-longphase'), 'should accept 999-longphase (3-digit prefix)'); @@ -110,7 +110,7 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W // ── Drift Item W006-archived: PHASE_TOKEN_FROM_DIR_RE / MILESTONE_ARCHIVE_DIR_RE ─ // // forEachArchivedPhaseToken() in verify.cjs uses two inline regex constants. -// After migration both are sourced from validate.generated.cjs: +// After migration both are sourced from validate.cjs: // PHASE_TOKEN_FROM_DIR_RE — extracts token from dir name like "64-auth-service" // MILESTONE_ARCHIVE_DIR_RE — matches archive dirs like "v1.0-phases" // @@ -173,9 +173,9 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_ }); test('MILESTONE_ARCHIVE_DIR_RE is exported and matches vN.N-phases dirs', () => { - const gen = require('../get-shit-done/bin/lib/validate.generated.cjs'); + const gen = require('../get-shit-done/bin/lib/validate.cjs'); assert.ok(gen.MILESTONE_ARCHIVE_DIR_RE instanceof RegExp, - 'validate.generated.cjs must export MILESTONE_ARCHIVE_DIR_RE'); + 'validate.cjs must export MILESTONE_ARCHIVE_DIR_RE'); const re = gen.MILESTONE_ARCHIVE_DIR_RE; assert.ok(re.test('v1.0-phases'), 'should match v1.0-phases'); assert.ok(re.test('v1.10-phases'), 'should match v1.10-phases'); @@ -184,9 +184,9 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_ }); test('PHASE_TOKEN_FROM_DIR_RE is exported and extracts phase tokens correctly', () => { - const gen = require('../get-shit-done/bin/lib/validate.generated.cjs'); + const gen = require('../get-shit-done/bin/lib/validate.cjs'); assert.ok(gen.PHASE_TOKEN_FROM_DIR_RE instanceof RegExp, - 'validate.generated.cjs must export PHASE_TOKEN_FROM_DIR_RE'); + 'validate.cjs must export PHASE_TOKEN_FROM_DIR_RE'); const re = gen.PHASE_TOKEN_FROM_DIR_RE; assert.strictEqual(re.exec('64-auth-service')?.[1], '64'); assert.strictEqual(re.exec('03B-feature')?.[1], '03B'); @@ -199,7 +199,7 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_ // // validate.ts Check 7: canonicalPlanStem('68-01-scaffolding') → '68-01' // verify.cjs had an inline copy. After migration, canonicalPlanStem is -// sourced from validate.generated.cjs. +// sourced from validate.cjs. // // Test: "68-01-scaffolding-PLAN.md" + "68-01-SUMMARY.md" → no I001 // Both stems canonicalize to "68-01" → match found → I001 suppressed. @@ -238,10 +238,10 @@ describe('Drift item I001 — canonicalPlanStem: long PLAN stem matches short SU `Expected zero I001, got: ${JSON.stringify(i001)}`); }); - test('canonicalPlanStem is exported from validate.generated.cjs', () => { - const gen = require('../get-shit-done/bin/lib/validate.generated.cjs'); + test('canonicalPlanStem is exported from validate.cjs', () => { + const gen = require('../get-shit-done/bin/lib/validate.cjs'); assert.strictEqual(typeof gen.canonicalPlanStem, 'function', - 'validate.generated.cjs must export canonicalPlanStem as a function'); + 'validate.cjs must export canonicalPlanStem as a function'); assert.strictEqual(gen.canonicalPlanStem('68-01-scaffolding'), '68-01'); assert.strictEqual(gen.canonicalPlanStem('68-01'), '68-01'); assert.strictEqual(gen.canonicalPlanStem('3A-01-feature'), '3A-01'); diff --git a/tests/bug-3571-configuration-manifest-install-path.test.cjs b/tests/bug-3571-configuration-manifest-install-path.test.cjs index ab16aba89..a1e86d5c0 100644 --- a/tests/bug-3571-configuration-manifest-install-path.test.cjs +++ b/tests/bug-3571-configuration-manifest-install-path.test.cjs @@ -1,5 +1,5 @@ /** - * Regression test for #3571: configuration.generated.cjs used the source + * Regression test for #3571: configuration.cjs used the source * checkout sdk/shared path only, which breaks installed gsd-tools.cjs because * runtime installs copy get-shit-done/ but not sdk/. */ @@ -15,7 +15,7 @@ const os = require('node:os'); const path = require('node:path'); const REPO_ROOT = path.join(__dirname, '..'); -const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.generated.cjs'); +const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.cjs'); const SDK_SHARED_DIR = path.join(REPO_ROOT, 'sdk', 'shared'); const { install } = require('../bin/install.js'); @@ -68,14 +68,14 @@ describe('bug #3571: configuration generated manifests resolve in install layout fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); }); - test('co-located bin/shared manifests let configuration.generated.cjs load without sdk/shared', () => { + test('co-located bin/shared manifests let configuration.cjs load without sdk/shared', () => { const gsdBinDir = path.join(tmpRoot, '.codex', 'get-shit-done', 'bin'); const gsdLibDir = path.join(gsdBinDir, 'lib'); const gsdSharedDir = path.join(gsdBinDir, 'shared'); fs.mkdirSync(gsdLibDir, { recursive: true }); fs.mkdirSync(gsdSharedDir, { recursive: true }); - const installedCjs = path.join(gsdLibDir, 'configuration.generated.cjs'); + const installedCjs = path.join(gsdLibDir, 'configuration.cjs'); fs.copyFileSync(CONFIGURATION_CJS, installedCjs); fs.copyFileSync( path.join(SDK_SHARED_DIR, 'config-defaults.manifest.json'), @@ -90,7 +90,7 @@ describe('bug #3571: configuration generated manifests resolve in install layout let mod; assert.doesNotThrow(() => { mod = require(installedCjs); - }, 'installed configuration.generated.cjs must not require ~/.codex/sdk/shared'); + }, 'installed configuration.cjs must not require ~/.codex/sdk/shared'); assert.ok(mod.VALID_CONFIG_KEYS.has('workflow.plan_review_convergence')); }); @@ -118,12 +118,12 @@ describe('bug #3571: configuration generated manifests resolve in install layout 'get-shit-done', 'bin', 'lib', - 'configuration.generated.cjs' + 'configuration.cjs' ); delete require.cache[installedCjs]; assert.doesNotThrow(() => { require(installedCjs); - }, 'post-install configuration.generated.cjs must load from co-located manifests'); + }, 'post-install configuration.cjs must load from co-located manifests'); }); }); diff --git a/tests/feat-3251-command-aliases-manifest-coverage.test.cjs b/tests/feat-3251-command-aliases-manifest-coverage.test.cjs index 487bfd7c4..fd3e44b59 100644 --- a/tests/feat-3251-command-aliases-manifest-coverage.test.cjs +++ b/tests/feat-3251-command-aliases-manifest-coverage.test.cjs @@ -1,7 +1,7 @@ 'use strict'; /** * Regression guard for issue #3251: - * 14 commands used in workflows must be present in command-aliases.generated.cjs. + * 14 commands used in workflows must be present in command-aliases.cjs. * * Asserts structurally by requiring the manifest and checking each canonical * command appears in either the family arrays or the non-family array. @@ -22,7 +22,7 @@ const COMMAND_ALIASES_FILE = path.join( 'get-shit-done', 'bin', 'lib', - 'command-aliases.generated.cjs', + 'command-aliases.cjs', ); const GSD_TOOLS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs'); @@ -43,7 +43,7 @@ const MISSING_14 = [ 'workstream.list', ]; -describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => { +describe('feat-3251: command-aliases.cjs manifest coverage', () => { let manifest; test('manifest file can be required without error', () => { @@ -59,7 +59,7 @@ describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => { manifest = manifest ?? require(COMMAND_ALIASES_FILE); assert.ok( Array.isArray(manifest.NON_FAMILY_COMMAND_ALIASES), - 'NON_FAMILY_COMMAND_ALIASES must be an exported array in command-aliases.generated.cjs', + 'NON_FAMILY_COMMAND_ALIASES must be an exported array in command-aliases.cjs', ); }); diff --git a/tests/graphify-auto-update.test.cjs b/tests/graphify-auto-update.test.cjs index cee8a4956..8eef4f30d 100644 --- a/tests/graphify-auto-update.test.cjs +++ b/tests/graphify-auto-update.test.cjs @@ -26,7 +26,7 @@ const { const { CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS, -} = require('../get-shit-done/bin/lib/configuration.generated.cjs'); +} = require('../get-shit-done/bin/lib/configuration.cjs'); const { makeStatusProject, diff --git a/tests/gsd-sdk-query-registry-integration.test.cjs b/tests/gsd-sdk-query-registry-integration.test.cjs index 0d6f3e495..1d571353a 100644 --- a/tests/gsd-sdk-query-registry-integration.test.cjs +++ b/tests/gsd-sdk-query-registry-integration.test.cjs @@ -14,7 +14,7 @@ const path = require('path'); const REPO_ROOT = path.join(__dirname, '..'); const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'registry-assembly.ts'); -const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs'); +const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.cjs'); // Prose tokens that repeatedly appear after `gsd-sdk query` in English // documentation but aren't real command names.