Merge pull request #266 from open-gsd/fix/189-require-dist-generated
refactor(#189): retire generated CJS module artifacts
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated": "2026-05-24",
|
"generated": "2026-05-25",
|
||||||
"families": {
|
"families": {
|
||||||
"agents": [
|
"agents": [
|
||||||
"gsd-advisor-researcher",
|
"gsd-advisor-researcher",
|
||||||
@@ -268,16 +268,15 @@
|
|||||||
"cjs-sdk-bridge.cjs",
|
"cjs-sdk-bridge.cjs",
|
||||||
"clusters.cjs",
|
"clusters.cjs",
|
||||||
"code-review-flags.cjs",
|
"code-review-flags.cjs",
|
||||||
"command-aliases.generated.cjs",
|
"command-aliases.cjs",
|
||||||
"command-routing-hub.cjs",
|
"command-routing-hub.cjs",
|
||||||
"commands.cjs",
|
"commands.cjs",
|
||||||
"config-schema.cjs",
|
"config-schema.cjs",
|
||||||
"config.cjs",
|
"config.cjs",
|
||||||
"configuration.generated.cjs",
|
"configuration.cjs",
|
||||||
"context-utilization.cjs",
|
"context-utilization.cjs",
|
||||||
"core.cjs",
|
"core.cjs",
|
||||||
"decisions.cjs",
|
"decisions.cjs",
|
||||||
"decisions.generated.cjs",
|
|
||||||
"docs.cjs",
|
"docs.cjs",
|
||||||
"drift.cjs",
|
"drift.cjs",
|
||||||
"fallow-runner.cjs",
|
"fallow-runner.cjs",
|
||||||
@@ -297,17 +296,14 @@
|
|||||||
"model-catalog.cjs",
|
"model-catalog.cjs",
|
||||||
"model-profiles.cjs",
|
"model-profiles.cjs",
|
||||||
"phase-command-router.cjs",
|
"phase-command-router.cjs",
|
||||||
"phase-lifecycle-policy.generated.cjs",
|
"phase-lifecycle.cjs",
|
||||||
"phase-lifecycle.generated.cjs",
|
|
||||||
"phase.cjs",
|
"phase.cjs",
|
||||||
"phase.generated.cjs",
|
|
||||||
"phases-command-router.cjs",
|
"phases-command-router.cjs",
|
||||||
"plan-scan.cjs",
|
"plan-scan.cjs",
|
||||||
"plan-scan.generated.cjs",
|
|
||||||
"planning-workspace.cjs",
|
"planning-workspace.cjs",
|
||||||
"profile-output.cjs",
|
"profile-output.cjs",
|
||||||
"profile-pipeline.cjs",
|
"profile-pipeline.cjs",
|
||||||
"project-root.generated.cjs",
|
"project-root.cjs",
|
||||||
"prompt-budget.cjs",
|
"prompt-budget.cjs",
|
||||||
"review-reviewer-selection.cjs",
|
"review-reviewer-selection.cjs",
|
||||||
"roadmap-command-router.cjs",
|
"roadmap-command-router.cjs",
|
||||||
@@ -317,27 +313,23 @@
|
|||||||
"runtime-name-policy.cjs",
|
"runtime-name-policy.cjs",
|
||||||
"runtime-slash.cjs",
|
"runtime-slash.cjs",
|
||||||
"schema-detect.cjs",
|
"schema-detect.cjs",
|
||||||
"schema-detect.generated.cjs",
|
|
||||||
"secrets.cjs",
|
"secrets.cjs",
|
||||||
"secrets.generated.cjs",
|
|
||||||
"security.cjs",
|
"security.cjs",
|
||||||
"semver-compare.cjs",
|
"semver-compare.cjs",
|
||||||
"shell-command-projection.cjs",
|
"shell-command-projection.cjs",
|
||||||
"state-command-router.cjs",
|
"state-command-router.cjs",
|
||||||
"state-document.cjs",
|
"state-document.cjs",
|
||||||
"state-document.generated.cjs",
|
|
||||||
"state.cjs",
|
"state.cjs",
|
||||||
"surface.cjs",
|
"surface.cjs",
|
||||||
"template.cjs",
|
"template.cjs",
|
||||||
"uat.cjs",
|
"uat.cjs",
|
||||||
"validate-command-router.cjs",
|
"validate-command-router.cjs",
|
||||||
"validate.generated.cjs",
|
"validate.cjs",
|
||||||
"verify-command-router.cjs",
|
"verify-command-router.cjs",
|
||||||
"verify.cjs",
|
"verify.cjs",
|
||||||
"workstream-inventory-builder.generated.cjs",
|
"workstream-inventory-builder.cjs",
|
||||||
"workstream-inventory.cjs",
|
"workstream-inventory.cjs",
|
||||||
"workstream-name-policy.cjs",
|
"workstream-name-policy.cjs",
|
||||||
"workstream-name-policy.generated.cjs",
|
|
||||||
"workstream.cjs",
|
"workstream.cjs",
|
||||||
"worktree-safety.cjs"
|
"worktree-safety.cjs"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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`.
|
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 |
|
| `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) |
|
| `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 |
|
| `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) |
|
| `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) |
|
| `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-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` |
|
| `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) |
|
| `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 |
|
| `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.cjs` | Parses CONTEXT.md `<decisions>` blocks; accepts numeric (D-42) and alphanumeric (D-INFRA-01) IDs; returns `{id, text, category, tags, trackable}` |
|
||||||
| `decisions.generated.cjs` | GENERATED — CJS artifact emitted from `sdk/src/query/decisions.ts` via `sdk/scripts/gen-decisions.mjs`; parses CONTEXT.md `<decisions>` blocks, accepts numeric (D-42) and alphanumeric (D-INFRA-01) IDs, returns `{id, text, category, tags, trackable}`; do not edit directly |
|
|
||||||
| `docs.cjs` | Docs-update workflow init, Markdown scanning, monorepo detection |
|
| `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 |
|
| `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 |
|
| `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-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 |
|
| `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-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.cjs` | Pure-computation phase lifecycle helpers extracted from the phase-lifecycle SDK handler |
|
||||||
| `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.cjs` | Phase directory operations, decimal numbering, plan indexing |
|
| `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` |
|
| `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.cjs` | Canonical phase-plan scanner for detecting plan and summary files in flat and nested layouts (k014) |
|
||||||
| `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 |
|
|
||||||
| `planning-workspace.cjs` | Planning path/workstream seam (`planningDir`, `planningPaths`, active-workstream routing, `.planning/.lock` orchestration) |
|
| `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-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation |
|
||||||
| `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning |
|
| `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) |
|
| `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-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-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-<cmd>` (skills-based runtimes) and `$gsd-<cmd>` (codex) in user-facing output and persisted artifacts (#3584) |
|
| `runtime-slash.cjs` | Runtime-aware slash-command formatter — single source of truth for emitting `/gsd-<cmd>` (skills-based runtimes) and `$gsd-<cmd>` (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.cjs` | Schema-drift detection for ORM patterns (Prisma, Drizzle, Supabase, TypeORM, Payload); exports `detectSchemaFiles`, `detectSchemaOrm`, `checkSchemaDrift`, `SCHEMA_PATTERNS`, `ORM_INFO` |
|
||||||
| `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` | Secret-config masking convention (`****<last-4>`) for integration keys; exports `SECRET_CONFIG_KEYS`, `isSecretKey`, `maskSecret`, `maskIfSecret` |
|
||||||
| `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 (`****<last-4>`) for integration keys; exports `SECRET_CONFIG_KEYS`, `isSecretKey`, `maskSecret`, `maskIfSecret`; do not edit directly |
|
|
||||||
| `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) |
|
| `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 |
|
| `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 |
|
| `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-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` |
|
||||||
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
|
| `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.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) |
|
| `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 |
|
| `template.cjs` | Template selection and filling with variable substitution |
|
||||||
| `uat.cjs` | UAT file parsing, verification debt tracking, audit-uat support |
|
| `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-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-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools verify` |
|
||||||
| `verify.cjs` | Plan structure, phase completeness, reference, commit validation |
|
| `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-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.generated.cjs` |
|
| `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` | CJS shim adapter — re-exports from `workstream-name-policy.generated.cjs` (Phase 6/#3575 Shared Module migration) |
|
| `workstream-name-policy.cjs` | Canonical workstream name validation (`isValidActiveWorkstreamName`, `hasInvalidPathSegment`, `validateWorkstreamName`) and slug normalization (`toWorkstreamSlug`) |
|
||||||
| `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.cjs` | Workstream CRUD, migration, session-scoped active pointer |
|
| `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 |
|
| `worktree-safety.cjs` | Worktree-root resolution and non-destructive prune policy decisions; owns W017 health-check logic |
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const {
|
|||||||
VALID_CONFIG_KEYS,
|
VALID_CONFIG_KEYS,
|
||||||
RUNTIME_STATE_KEYS,
|
RUNTIME_STATE_KEYS,
|
||||||
DYNAMIC_KEY_PATTERNS,
|
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).
|
* Returns true if keyPath is a valid config key (exact, dynamic pattern, or runtime state).
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ function cmdConfigPath(cwd) {
|
|||||||
* summary when --raw is set. Exits 0 in all cases (including no-op).
|
* summary when --raw is set. Exits 0 in all cases (including no-op).
|
||||||
*/
|
*/
|
||||||
async function cmdMigrateConfig(cwd, raw) {
|
async function cmdMigrateConfig(cwd, raw) {
|
||||||
const { migrateOnDisk } = require('./configuration.generated.cjs');
|
const { migrateOnDisk } = require('./configuration.cjs');
|
||||||
const ws = process.env.GSD_WORKSTREAM || null;
|
const ws = process.env.GSD_WORKSTREAM || null;
|
||||||
const report = await migrateOnDisk(cwd, ws || undefined);
|
const report = await migrateOnDisk(cwd, ws || undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const {
|
|||||||
setActiveWorkstream,
|
setActiveWorkstream,
|
||||||
findContextMdIn,
|
findContextMdIn,
|
||||||
} = require('./planning-workspace.cjs');
|
} = require('./planning-workspace.cjs');
|
||||||
const { findProjectRoot } = require('./project-root.generated.cjs');
|
const { findProjectRoot } = require('./project-root.cjs');
|
||||||
|
|
||||||
// ─── Configuration Module (generated CJS mirror) ────────────────────────────
|
// ─── Configuration Module (generated CJS mirror) ────────────────────────────
|
||||||
// Cycle 4: import canonical defaults + normalization primitives from the
|
// Cycle 4: import canonical defaults + normalization primitives from the
|
||||||
@@ -36,7 +36,7 @@ const { findProjectRoot } = require('./project-root.generated.cjs');
|
|||||||
const {
|
const {
|
||||||
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
|
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
|
||||||
normalizeLegacyKeys,
|
normalizeLegacyKeys,
|
||||||
} = require('./configuration.generated.cjs');
|
} = require('./configuration.cjs');
|
||||||
|
|
||||||
// ─── Path helpers ────────────────────────────────────────────────────────────
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,121 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decisions Module — CJS adapter.
|
* GENERATED FILE — DO NOT EDIT.
|
||||||
*
|
|
||||||
* 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 <decisions> 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.
|
|
||||||
*
|
*
|
||||||
|
* Source: sdk/src/query/decisions.ts
|
||||||
* Regenerate: cd sdk && npm run gen:decisions
|
* Regenerate: cd sdk && npm run gen:decisions
|
||||||
|
*
|
||||||
|
* Shared parser for CONTEXT.md <decisions> 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 `<decisions>` 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 `<decisions>...</decisions>` 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(/<decisions>([\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 `<decisions>` (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 };
|
||||||
|
|||||||
@@ -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 <decisions> 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 `<decisions>` 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 `<decisions>...</decisions>` 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(/<decisions>([\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 `<decisions>` (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 };
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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 { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||||
const { output } = require('./core.cjs');
|
const { output } = require('./core.cjs');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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) ───────
|
// ─── CommandRoutingHub (issue #3788, simplified in #175, typed in #176) ───────
|
||||||
const { createHub, ERROR_KINDS, makeInvalidArgs } = require('./command-routing-hub.cjs');
|
const { createHub, ERROR_KINDS, makeInvalidArgs } = require('./command-routing-hub.cjs');
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -22,7 +22,7 @@ const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceFie
|
|||||||
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
|
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
|
||||||
// Generated pure-computation helpers for cmdPhaseComplete (issue #4 fix).
|
// Generated pure-computation helpers for cmdPhaseComplete (issue #4 fix).
|
||||||
// Source: sdk/src/query/phase-lifecycle.ts. Regenerate: node sdk/scripts/gen-phase-lifecycle.mjs
|
// 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`.
|
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
|
||||||
// Documented in agents/gsd-planner.md (write_phase_prompt step). The wider
|
// Documented in agents/gsd-planner.md (write_phase_prompt step). The wider
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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 { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||||
const { output } = require('./core.cjs');
|
const { output } = require('./core.cjs');
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,97 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plan Scan Module — CJS adapter.
|
* GENERATED FILE — DO NOT EDIT.
|
||||||
*
|
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
|
* Source: sdk/src/query/plan-scan.ts
|
||||||
* Regenerate: cd sdk && npm run gen:plan-scan
|
* 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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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 { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||||
const { output } = require('./core.cjs');
|
const { output } = require('./core.cjs');
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,170 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema Detect Module — CJS adapter.
|
* GENERATED FILE — DO NOT EDIT.
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
*
|
||||||
|
* Source: sdk/src/query/schema-detect.ts
|
||||||
* Regenerate: cd sdk && npm run gen:schema-detect
|
* 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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -1,20 +1,37 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Secrets Module — CJS adapter.
|
* GENERATED FILE — DO NOT EDIT.
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
*
|
||||||
|
* Source: sdk/src/query/secrets.ts
|
||||||
* Regenerate: cd sdk && npm run gen:secrets
|
* 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 };
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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 { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||||
const { output } = require('./core.cjs');
|
const { output } = require('./core.cjs');
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,12 +1,127 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STATE.md Document Module — CJS adapter.
|
* GENERATED FILE — DO NOT EDIT.
|
||||||
*
|
*
|
||||||
* The implementation is generated from sdk/src/state/index.ts and
|
* Source: sdk/src/state/index.ts
|
||||||
* lives in state-document.generated.cjs. This file is a thin re-export so
|
* Regenerate: cd sdk && npm run gen:state-document
|
||||||
* that existing call sites (state.cjs, workstream-inventory.cjs, init.cjs,
|
*
|
||||||
* and tests) can continue to require('./state-document') unchanged.
|
* 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 };
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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 { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
|
||||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||||
const { output } = require('./core.cjs');
|
const { output } = require('./core.cjs');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'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 { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||||
const { output } = require('./core.cjs');
|
const { output } = require('./core.cjs');
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const {
|
|||||||
phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants,
|
phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants,
|
||||||
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
||||||
phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem,
|
phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem,
|
||||||
} = require('./validate.generated.cjs');
|
} = require('./validate.cjs');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const { toPosixPath, readSubdirectories } = require('./core.cjs');
|
|||||||
const scanPhasePlans = require('./plan-scan.cjs');
|
const scanPhasePlans = require('./plan-scan.cjs');
|
||||||
const { planningPaths, planningRoot, getActiveWorkstream } = require('./planning-workspace.cjs');
|
const { planningPaths, planningRoot, getActiveWorkstream } = require('./planning-workspace.cjs');
|
||||||
const { stateExtractField } = require('./state-document.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) {
|
function workstreamsRoot(cwd) {
|
||||||
return path.join(planningRoot(cwd), 'workstreams');
|
return path.join(planningRoot(cwd), 'workstreams');
|
||||||
|
|||||||
@@ -1,19 +1,61 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workstream Name Policy Module — CJS adapter.
|
* GENERATED FILE — DO NOT EDIT.
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
*
|
||||||
|
* Source: sdk/src/workstream-name-policy.ts
|
||||||
* Regenerate: cd sdk && npm run gen:workstream-name-policy
|
* 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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -21,7 +21,8 @@
|
|||||||
"c8": "^11.0.0"
|
"c8": "^11.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0",
|
||||||
|
"npm": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fallow": "^2.70.0"
|
"fallow": "^2.70.0"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"check:env": "bash scripts/check-env.sh",
|
"check:env": "bash scripts/check-env.sh",
|
||||||
"check:integrity": "./scripts/check-npm-integrity.sh",
|
"check:integrity": "./scripts/check-npm-integrity.sh",
|
||||||
"build": "tsc -b",
|
"build": "npm run build:sdk",
|
||||||
"build:hooks": "node scripts/build-hooks.js",
|
"build:hooks": "node scripts/build-hooks.js",
|
||||||
"build:sdk": "cd sdk && npm ci && npm run build",
|
"build:sdk": "cd sdk && npm ci && npm run build",
|
||||||
"prepublishOnly": "npm run build:hooks && npm run build:sdk",
|
"prepublishOnly": "npm run build:hooks && npm run build:sdk",
|
||||||
|
|||||||
@@ -32,6 +32,12 @@
|
|||||||
"classification": "cooperating-sibling",
|
"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."
|
"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",
|
"cjs": "get-shit-done/bin/lib/profile-output.cjs",
|
||||||
"ts": "sdk/src/query/profile-output.ts",
|
"ts": "sdk/src/query/profile-output.ts",
|
||||||
@@ -74,6 +80,12 @@
|
|||||||
"classification": "cooperating-sibling",
|
"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."
|
"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",
|
"cjs": "get-shit-done/bin/lib/workstream.cjs",
|
||||||
"ts": "sdk/src/query/workstream.ts",
|
"ts": "sdk/src/query/workstream.ts",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* I001 canonicalPlanStem.
|
* I001 canonicalPlanStem.
|
||||||
*
|
*
|
||||||
* After the generator migration, all three helpers are sourced from
|
* 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.
|
* tests go RED.
|
||||||
*
|
*
|
||||||
* References:
|
* References:
|
||||||
@@ -62,7 +62,7 @@ function writeConfigJson(planningDir) {
|
|||||||
// node .claude/get-shit-done/bin/gsd-tools.cjs validate health
|
// node .claude/get-shit-done/bin/gsd-tools.cjs validate health
|
||||||
// # Bug: emits W005 about 999.1-foo not following NN-name format
|
// # 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.
|
// 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', () => {
|
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)}`);
|
`Expected zero W005 for 999.1-foo, got: ${JSON.stringify(w005)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phaseDirNameRe is exported from validate.generated.cjs', () => {
|
test('phaseDirNameRe is exported from validate.cjs', () => {
|
||||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
const gen = require('../get-shit-done/bin/lib/validate.cjs');
|
||||||
assert.ok(gen.phaseDirNameRe instanceof RegExp,
|
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;
|
const re = gen.phaseDirNameRe;
|
||||||
assert.ok(re.test('01-setup'), 'should accept 01-setup');
|
assert.ok(re.test('01-setup'), 'should accept 01-setup');
|
||||||
assert.ok(re.test('999-longphase'), 'should accept 999-longphase (3-digit prefix)');
|
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 ─
|
// ── Drift Item W006-archived: PHASE_TOKEN_FROM_DIR_RE / MILESTONE_ARCHIVE_DIR_RE ─
|
||||||
//
|
//
|
||||||
// forEachArchivedPhaseToken() in verify.cjs uses two inline regex constants.
|
// 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"
|
// 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"
|
// 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', () => {
|
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,
|
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;
|
const re = gen.MILESTONE_ARCHIVE_DIR_RE;
|
||||||
assert.ok(re.test('v1.0-phases'), 'should match v1.0-phases');
|
assert.ok(re.test('v1.0-phases'), 'should match v1.0-phases');
|
||||||
assert.ok(re.test('v1.10-phases'), 'should match v1.10-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', () => {
|
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,
|
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;
|
const re = gen.PHASE_TOKEN_FROM_DIR_RE;
|
||||||
assert.strictEqual(re.exec('64-auth-service')?.[1], '64');
|
assert.strictEqual(re.exec('64-auth-service')?.[1], '64');
|
||||||
assert.strictEqual(re.exec('03B-feature')?.[1], '03B');
|
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'
|
// validate.ts Check 7: canonicalPlanStem('68-01-scaffolding') → '68-01'
|
||||||
// verify.cjs had an inline copy. After migration, canonicalPlanStem is
|
// 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
|
// Test: "68-01-scaffolding-PLAN.md" + "68-01-SUMMARY.md" → no I001
|
||||||
// Both stems canonicalize to "68-01" → match found → I001 suppressed.
|
// 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)}`);
|
`Expected zero I001, got: ${JSON.stringify(i001)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('canonicalPlanStem is exported from validate.generated.cjs', () => {
|
test('canonicalPlanStem is exported from validate.cjs', () => {
|
||||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
const gen = require('../get-shit-done/bin/lib/validate.cjs');
|
||||||
assert.strictEqual(typeof gen.canonicalPlanStem, 'function',
|
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-scaffolding'), '68-01');
|
||||||
assert.strictEqual(gen.canonicalPlanStem('68-01'), '68-01');
|
assert.strictEqual(gen.canonicalPlanStem('68-01'), '68-01');
|
||||||
assert.strictEqual(gen.canonicalPlanStem('3A-01-feature'), '3A-01');
|
assert.strictEqual(gen.canonicalPlanStem('3A-01-feature'), '3A-01');
|
||||||
|
|||||||
@@ -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
|
* checkout sdk/shared path only, which breaks installed gsd-tools.cjs because
|
||||||
* runtime installs copy get-shit-done/ but not sdk/.
|
* runtime installs copy get-shit-done/ but not sdk/.
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +15,7 @@ const os = require('node:os');
|
|||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const REPO_ROOT = path.join(__dirname, '..');
|
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 SDK_SHARED_DIR = path.join(REPO_ROOT, 'sdk', 'shared');
|
||||||
|
|
||||||
const { install } = require('../bin/install.js');
|
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 });
|
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 gsdBinDir = path.join(tmpRoot, '.codex', 'get-shit-done', 'bin');
|
||||||
const gsdLibDir = path.join(gsdBinDir, 'lib');
|
const gsdLibDir = path.join(gsdBinDir, 'lib');
|
||||||
const gsdSharedDir = path.join(gsdBinDir, 'shared');
|
const gsdSharedDir = path.join(gsdBinDir, 'shared');
|
||||||
fs.mkdirSync(gsdLibDir, { recursive: true });
|
fs.mkdirSync(gsdLibDir, { recursive: true });
|
||||||
fs.mkdirSync(gsdSharedDir, { 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(CONFIGURATION_CJS, installedCjs);
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
path.join(SDK_SHARED_DIR, 'config-defaults.manifest.json'),
|
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;
|
let mod;
|
||||||
assert.doesNotThrow(() => {
|
assert.doesNotThrow(() => {
|
||||||
mod = require(installedCjs);
|
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'));
|
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',
|
'get-shit-done',
|
||||||
'bin',
|
'bin',
|
||||||
'lib',
|
'lib',
|
||||||
'configuration.generated.cjs'
|
'configuration.cjs'
|
||||||
);
|
);
|
||||||
|
|
||||||
delete require.cache[installedCjs];
|
delete require.cache[installedCjs];
|
||||||
assert.doesNotThrow(() => {
|
assert.doesNotThrow(() => {
|
||||||
require(installedCjs);
|
require(installedCjs);
|
||||||
}, 'post-install configuration.generated.cjs must load from co-located manifests');
|
}, 'post-install configuration.cjs must load from co-located manifests');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* Regression guard for issue #3251:
|
* 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
|
* Asserts structurally by requiring the manifest and checking each canonical
|
||||||
* command appears in either the family arrays or the non-family array.
|
* 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',
|
'get-shit-done',
|
||||||
'bin',
|
'bin',
|
||||||
'lib',
|
'lib',
|
||||||
'command-aliases.generated.cjs',
|
'command-aliases.cjs',
|
||||||
);
|
);
|
||||||
const GSD_TOOLS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
const GSD_TOOLS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ const MISSING_14 = [
|
|||||||
'workstream.list',
|
'workstream.list',
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => {
|
describe('feat-3251: command-aliases.cjs manifest coverage', () => {
|
||||||
let manifest;
|
let manifest;
|
||||||
|
|
||||||
test('manifest file can be required without error', () => {
|
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);
|
manifest = manifest ?? require(COMMAND_ALIASES_FILE);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Array.isArray(manifest.NON_FAMILY_COMMAND_ALIASES),
|
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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
|
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
|
||||||
} = require('../get-shit-done/bin/lib/configuration.generated.cjs');
|
} = require('../get-shit-done/bin/lib/configuration.cjs');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
makeStatusProject,
|
makeStatusProject,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const path = require('path');
|
|||||||
|
|
||||||
const REPO_ROOT = path.join(__dirname, '..');
|
const REPO_ROOT = path.join(__dirname, '..');
|
||||||
const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'registry-assembly.ts');
|
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
|
// Prose tokens that repeatedly appear after `gsd-sdk query` in English
|
||||||
// documentation but aren't real command names.
|
// documentation but aren't real command names.
|
||||||
|
|||||||
Reference in New Issue
Block a user