* test(#3762): failing-first roster gate for docs/INVENTORY.md rows
Anchors the human half of the inventory-drift rule: every entry in
docs/INVENTORY-MANIFEST.json must have a hand-written row in
docs/INVENTORY.md. Expected RED on this commit -- next carries 32
unrostered surfaces, which is the defect the gate exists to catch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#3762): enforce docs/INVENTORY.md roster rows, backfill 32 gaps
docs/INVENTORY.md calls itself the authoritative roster of every shipped
GSD surface, and CLAUDE.md's inventory-drift rule requires both a roster
row and a manifest regen. Only the manifest half was anchored, so a PR
could ship a surface, regenerate the manifest, omit the row, and stay
green -- as PR #3758 did with gsd-core/references/planner-coupling.md.
Adds the roster half to tests/inventory-manifest-sync.test.cjs, backed by
a pure matcher in tests/helpers/inventory-roster.cjs, and backfills the 32
surfaces already missing rows on next.
Fixes #3762
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(#3762): harden roster matcher against fenced blocks and trim exports
Skips fenced code regions when splitting level-2 sections so a documented
'## ' example inside a fence cannot truncate a family section (false red)
or contribute a phantom row (false pass); makes the heading pattern linear
rather than a backtracking lazy match; narrows the module surface to the
three names the gate consumes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#3762): close two false-RED gaps found in orthogonal review
Indented headings: CommonMark permits an ATX heading to carry 1-3 leading
spaces, and a ^##-anchored pattern read such a document as having no family
sections at all -- reporting all six missing, a structural red for zero real
drift. Reproduced, then fixed and pinned.
Link-wrapped cells: a row written as [`x.md`](../x.md) was not recognized,
though docs/INVENTORY.md already uses that form elsewhere. Unwrapping now
peels a whole-cell link and a whole-cell code span, and only layers that
wrap the cell entirely -- a file mentioned mid-prose is still not a row, so
the false red is not traded for a false pass.
Adds a second fast-check property over the commands source-link rule, the
one family whose matching rule differs from the other five.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(#3762): boundary trio for the fence-marker length limit
RULESET.TESTS.boundary-coverage — the matcher's only numeric limit is the
fence marker's {3,}. Exercises 2 (inline markup, swallows nothing), 3, and
4 characters, for both backtick and tilde delimiters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: remove stray pwned_cmdsub injection-test canary from the repo root
A zero-byte file committed by 0e6fa2e2c (#3124) while remediating the
#3118 command-substitution injection — the marker a test wrote into cwd to
prove a substitution had NOT executed, left behind when the run ended.
Nothing in the tree references it (verified by Grep across the repo),
package.json's files array excludes root-level files so it never shipped,
and lint-removed-but-needed confirms no surviving reference.
Found while auditing the repo root for #3762; fixed inline rather than
deferred, per CLAUDE.md's no-deferrals rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: backfill changeset pr number to 3766
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/eager-birds-sprint.md
Normal file
5
.changeset/eager-birds-sprint.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3766
|
||||
---
|
||||
**docs/INVENTORY.md rows are now enforced** — a shipped agent, command, workflow, reference, CLI module, or hook could be added to the generated manifest with no row in the authoritative roster and still pass CI; the roster is now anchored the same way the manifest is, and 32 pre-existing gaps are backfilled. (#3762)
|
||||
@@ -611,7 +611,7 @@ The prompt-level data/instruction isolation seam for untrusted web/document ingr
|
||||
`RULESET.SHARED-HELPERS-LINT-VS-TEST=when a lint script and test suite both implement same constant (CANONICAL_TOOLS) or parser (parseFrontmatter, executionContextRefs), extract to scripts/*-helpers.cjs required by both — silent divergence otherwise`
|
||||
|
||||
`RULESET.ADR-HEADER=every docs/adr/NNNN-*.md must open with - **Status:** Accepted|Proposed|Superseded (by [ADR-NNNN](file.md))|Legacy + - **Date:** YYYY-MM-DD immediately after title`
|
||||
`RULESET.MANIFEST-CANONICAL-KEY=docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL EIGHT families.* arrays (agents/commands/workflows/references/cli_modules/hooks flat, plus workflow_modes/workflow_steps nested — #2996, epic #1671 Phase 6.5) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all eight, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the six flat families are keyed by BARE BASENAME while the two nested families are keyed by <workflow>/<subdir>/<file> path, deliberately, because two workflows may each own a same-named step file and a basename key would silently drop one under a JSON-equality comparison; recursion is bounded at exactly one named subdirectory, never a general walk; the family tables live ONCE in scripts/gen-inventory-manifest.cjs and are IMPORTED by the test (the test formerly redeclared them, a DEFECT.GENERATIVE-FIX divergence that let a new family be verified by nobody while still reporting green); the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write, AFTER build:lib`
|
||||
`RULESET.MANIFEST-CANONICAL-KEY=docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL EIGHT families.* arrays (agents/commands/workflows/references/cli_modules/hooks flat, plus workflow_modes/workflow_steps nested — #2996, epic #1671 Phase 6.5) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all eight, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the six flat families are keyed by BARE BASENAME while the two nested families are keyed by <workflow>/<subdir>/<file> path, deliberately, because two workflows may each own a same-named step file and a basename key would silently drop one under a JSON-equality comparison; recursion is bounded at exactly one named subdirectory, never a general walk; the family tables live ONCE in scripts/gen-inventory-manifest.cjs and are IMPORTED by the test (the test formerly redeclared them, a DEFECT.GENERATIVE-FIX divergence that let a new family be verified by nobody while still reporting green); the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write, AFTER build:lib; #3762 added the ROSTER half — tests/inventory-manifest-sync.test.cjs now also asserts every manifest entry has a hand-written row in docs/INVENTORY.md, via the pure matcher in tests/helpers/inventory-roster.cjs. Scope is the SIX FLAT families only, each searched inside its own `## ` section; workflow_steps/workflow_modes are DELIBERATELY exempt because docs/INVENTORY.md §"Workflow Sub-Files" is a shipped decision that they carry no hand-written per-file rows. Matching is whole-CELL-exact (never substring — the rostered host-integration-adapters/imperative-hook-bus.cjs must not satisfy the separate top-level hook-bus.cjs) and section-scoped (smart-entry.md and smart-entry.cjs are different families), EXCEPT commands, which match on the row's Source-column link to ../commands/gsd/<file>.md because the six ns-* namespace routers deliberately RENDER a name that is not their file stem (/gsd-workflow ← ns-workflow.md) — DEFECT.DISPLAY-VALUE-AS-IDENTITY. Landing the gate required backfilling 32 pre-existing unrostered surfaces on next`
|
||||
|
||||
`RULESET.PR-SCOPE.one-concern-per-pr=split unrelated changes into separate PRs; cherry-pick doc changes to dedicated docs/ branch immediately, then force-push original to remove the commit`
|
||||
|
||||
@@ -911,9 +911,9 @@ Full detail in `~/.claude/skills/gsd-pr-fix-discipline/SKILL.md`. AI agents MUST
|
||||
|
||||
### INVENTORY / manifest drift
|
||||
|
||||
- **Symptom:** `tests/inventory-manifest-sync.test.cjs` fails — `"New surfaces not in manifest"`; or `tests/inventory-headings-countfree.test.cjs` fails if a `(N shipped)` count was re-added to a heading
|
||||
- **Symptom:** `tests/inventory-manifest-sync.test.cjs` fails — `"New surfaces not in manifest"` (the MANIFEST half) or `"Shipped surfaces in docs/INVENTORY-MANIFEST.json with NO row in docs/INVENTORY.md"` (the ROSTER half, #3762); or `tests/inventory-headings-countfree.test.cjs` fails if a `(N shipped)` count was re-added to a heading
|
||||
- **Affected this session:** #154, #156, #143, #155, #169
|
||||
- **Fix:** Add row to `docs/INVENTORY.md` + `node scripts/gen-inventory-manifest.cjs --write`
|
||||
- **Fix:** Add row to `docs/INVENTORY.md` + `node scripts/gen-inventory-manifest.cjs --write`. The two halves have DIFFERENT remedies and the roster half cannot be regenerated — a role sentence is hand-written by design, so re-running the generator never clears it.
|
||||
|
||||
### Slash command two-tier confusion
|
||||
|
||||
|
||||
@@ -1012,7 +1012,7 @@
|
||||
{
|
||||
"id": "RULESET.MANIFEST-CANONICAL-KEY",
|
||||
"klass": "RULESET",
|
||||
"value": "docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL EIGHT families.* arrays (agents/commands/workflows/references/cli_modules/hooks flat, plus workflow_modes/workflow_steps nested — #2996, epic #1671 Phase 6.5) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all eight, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the six flat families are keyed by BARE BASENAME while the two nested families are keyed by <workflow>/<subdir>/<file> path, deliberately, because two workflows may each own a same-named step file and a basename key would silently drop one under a JSON-equality comparison; recursion is bounded at exactly one named subdirectory, never a general walk; the family tables live ONCE in scripts/gen-inventory-manifest.cjs and are IMPORTED by the test (the test formerly redeclared them, a DEFECT.GENERATIVE-FIX divergence that let a new family be verified by nobody while still reporting green); the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write, AFTER build:lib"
|
||||
"value": "docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL EIGHT families.* arrays (agents/commands/workflows/references/cli_modules/hooks flat, plus workflow_modes/workflow_steps nested — #2996, epic #1671 Phase 6.5) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all eight, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the six flat families are keyed by BARE BASENAME while the two nested families are keyed by <workflow>/<subdir>/<file> path, deliberately, because two workflows may each own a same-named step file and a basename key would silently drop one under a JSON-equality comparison; recursion is bounded at exactly one named subdirectory, never a general walk; the family tables live ONCE in scripts/gen-inventory-manifest.cjs and are IMPORTED by the test (the test formerly redeclared them, a DEFECT.GENERATIVE-FIX divergence that let a new family be verified by nobody while still reporting green); the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write, AFTER build:lib; #3762 added the ROSTER half — tests/inventory-manifest-sync.test.cjs now also asserts every manifest entry has a hand-written row in docs/INVENTORY.md, via the pure matcher in tests/helpers/inventory-roster.cjs. Scope is the SIX FLAT families only, each searched inside its own `## ` section; workflow_steps/workflow_modes are DELIBERATELY exempt because docs/INVENTORY.md §\"Workflow Sub-Files\" is a shipped decision that they carry no hand-written per-file rows. Matching is whole-CELL-exact (never substring — the rostered host-integration-adapters/imperative-hook-bus.cjs must not satisfy the separate top-level hook-bus.cjs) and section-scoped (smart-entry.md and smart-entry.cjs are different families), EXCEPT commands, which match on the row's Source-column link to ../commands/gsd/<file>.md because the six ns-* namespace routers deliberately RENDER a name that is not their file stem (/gsd-workflow ← ns-workflow.md) — DEFECT.DISPLAY-VALUE-AS-IDENTITY. Landing the gate required backfilling 32 pre-existing unrostered surfaces on next"
|
||||
},
|
||||
{
|
||||
"id": "RULESET.PR-FLOW.docker-before-push",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- The machine-readable roster lives in `docs/INVENTORY-MANIFEST.json` (regenerated by `scripts/gen-inventory-manifest.cjs --write`). For live counts, run `ls agents/gsd-*.md | wc -l` etc. against the checkout.
|
||||
- This file enumerates every shipped surface across all six families (agents, commands, workflows, references, CLI modules, hooks). Broad docs may render narrative or curated subsets; when they disagree with the filesystem, this file and the directory listings are authoritative.
|
||||
- New surfaces should land here first, then propagate to the broad docs. The drift-control test in `tests/inventory-manifest-sync.test.cjs` anchors the roster contents against the filesystem.
|
||||
- New surfaces should land here first, then propagate to the broad docs. `tests/inventory-manifest-sync.test.cjs` anchors both halves: the manifest against the filesystem, and this file's rows against the manifest (#3762). The second half cannot be regenerated — a role sentence is hand-written by design, so re-running `gen-inventory-manifest.cjs` never clears it.
|
||||
|
||||
This is the authoritative roster of every shipped GSD Core surface. See the [docs index](README.md) to navigate by topic.
|
||||
|
||||
@@ -251,6 +251,7 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that
|
||||
| `ship.md` | Create PR, run review, and prepare for merge after verification. | `/gsd-ship` |
|
||||
| `sketch.md` | Explore design directions through throwaway HTML mockups with 2-3 variants per sketch. | `/gsd-sketch` |
|
||||
| `sketch-wrap-up.md` | Curate sketch findings and package them as a persistent `sketch-findings-[project]` skill. | `/gsd-sketch --wrap-up` |
|
||||
| `smart-entry.md` | State-aware front door — classify the current project situation, present the matching menu, and dispatch exactly one existing GSD command (ADR-1787). | `/gsd-next` |
|
||||
| `spec-phase.md` | Socratic spec refinement with ambiguity scoring; produces SPEC.md. | `/gsd-spec-phase` |
|
||||
| `spike.md` | Rapid feasibility validation through focused, throwaway experiments. | `/gsd-spike` |
|
||||
| `spike-wrap-up.md` | Curate spike findings and package them as a persistent `spike-findings-[project]` skill. | `/gsd-spike --wrap-up` |
|
||||
@@ -324,6 +325,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum
|
||||
| `debugger-techniques.md` | Full step-by-step bodies for the 10 debugging techniques (binary search, delta debugging, git bisect, …) routed by `gsd-debugger`'s technique-selection table. |
|
||||
| `verifier-wiring-patterns.md` | Data-flow trace procedure and the four wiring patterns (Component→API, API→Database, Form→Handler, State→Render) loaded by `gsd-verifier`. |
|
||||
| `mandatory-initial-read.md` | Shared required-reading boilerplate injected into agent prompts. |
|
||||
| `gsd-run-resolver.md` | Canonical `gsd_run` bootstrap resolver block; workflows reference this file instead of copying the shell probe. |
|
||||
| `agent-skills-bootstrap.md` | Shared agent_skills self-load contract (query + Read + dedup guard) injected into all 22 consumer agents. |
|
||||
| `project-skills-discovery.md` | Shared project-skills-discovery boilerplate injected into agent prompts. |
|
||||
| `research-documentation-lookup.md` | Shared documentation-lookup protocol (Context7 MCP + guarded CLI fallback) injected into all researcher agents. |
|
||||
@@ -341,6 +343,9 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum
|
||||
| `execute-phase-requirement-revert.md` | Gap-report step for `execute-phase` — reverts this phase's own shared requirement IDs out of `Complete` in REQUIREMENTS.md before rendering a `gaps_found` report, scoped to `PHASE_REQ_IDS` so other phases' rows are untouched (#2388). |
|
||||
| `execute-phase-response-language.md` | Response-language directive for `execute-phase` orchestrator output (questions, narration, report-template prose); extracted to keep the workflow under the frozen pre-phase-6 byte ceiling (#2402). |
|
||||
| `execute-phase-quota-recovery.md` | Step 7.1 detail for `execute-phase` — `quota-exceeded` recovery: the opt-in `dynamic_routing.provider_escalation` ladder (swap provider, honor `Retry-After`, fail loudly when spent) and the default manual wait-for-reset prompt (#2296). |
|
||||
| `execute-phase-between-wave-reset.md` | Between-wave manifest reset and worktree base refresh for waves 2+, plus the pre-wave cross-plan key-links dependency check (#1369). |
|
||||
| `execute-phase-wave-guard.md` | Inter-wave worktree base re-check for wave N+1 — the harness caches the fork base, so a fresh worktree would otherwise be cut from the stale pre-wave base (#1369, #2652). |
|
||||
| `offer-next.md` | The `offer_next` step body extracted from `execute-phase.md` — auto-advance routing and the no-transition check (#2537). |
|
||||
| `continuation-format.md` | Session continuation/resume format. |
|
||||
| `domain-probes.md` | Domain-specific probing questions for discuss-phase. |
|
||||
| `edge-probe.md` | Spec-phase edge-completeness probe — 8-category edge taxonomy, shape classification, and the `requirements → checks → verifier` resolution model (Step 5.5). |
|
||||
@@ -365,6 +370,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum
|
||||
| `user-profiling.md` | User behavioral profiling detection heuristics. |
|
||||
| `thinking-partner.md` | Conditional thinking-partner activation at decision points. |
|
||||
| `autonomous-smart-discuss.md` | Smart-discuss logic for autonomous mode. |
|
||||
| `autonomous-ui-design-contract.md` | Autonomous-mode step 3a.5 — resolve whether a frontend phase needs a UI-SPEC.md and generate one through active `plan:pre` hooks; always non-blocking. |
|
||||
| `ios-scaffold.md` | iOS application scaffolding patterns. |
|
||||
| `ai-evals.md` | AI evaluation design reference for `/gsd-ai-integration-phase`. |
|
||||
| `api-coverage.md` | API-coverage gate reference (full-coverage-by-default) for the `ai-integration` capability's `verify:pre` blocking gate (#1562) — matrix format, trigger, tuning, detector CLI. |
|
||||
@@ -445,11 +451,15 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `installer-migrations/008-cursor-retire-commands-surface.cjs` | Installer migration: retires Cursor's duplicate `commands/` surface now that skills are the sole workflow surface (#2644) |
|
||||
| `installer-migrations/009-pi-retire-reserved-hooks-dir.cjs` | Installer migration: retires pi's legacy `hooks/` directory after GSD's shared hook bundle moved to `gsd-hooks/` (#3023) |
|
||||
| `active-workstream-store.cjs` | Workstream source precedence and selection (CLI `--ws` > `GSD_WORKSTREAM` env > stored pointer); name validation and environment propagation |
|
||||
| `adapter-declarative.cjs` | Declarative host-integration adapter — projects workflow artifacts through the install engine for hosts that declare a descriptor-driven surface (#1680) |
|
||||
| `adapter-imperative.cjs` | Imperative host-integration adapter — binds the composed capability registry in-process for hosts that drive emission themselves (#1680) |
|
||||
| `adr-parser.cjs` | ADR decision parser for plan-phase ingest express path; normalizes section synonyms, parses status/decision/scope fences, and enforces status rejection gates |
|
||||
| `agent-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools agent` |
|
||||
| `agent-install-check.cjs` | Agent-installation probe — owns `getAgentsDir` and `checkAgentsInstalled`, the single resolution the health-diagnostic agent-install rule consumes (ADR-857, #1268) |
|
||||
| `health-diagnostic-rules/agent-install.cjs` | Health-diagnostic rule: agent-installation-completeness check (W010) — the single `checkAgentsInstalled` call site's four mutually exclusive conditions, ported behavior-preserving from `cmdValidateHealth` (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
|
||||
| `api-coverage.cjs` | API-coverage detector + matrix validator (#1562, #2365) — pure `detectApiIntegration` (fail-closed: same-clause verb+noun signal + `<Service> API/SDK` surface naming a real service; strips fenced code, inline code, and path-shaped tokens; external hosts count, first-party route paths do not) and `validateCoverageMatrix`/`parseCoverageMatrix`/`renderCoverageMatrix` for the COVERAGE.md artifact (incl. the `No external API integration: <reason>` declaration); STDIN CLI (`echo "$SCOPE" \| node .../api-coverage.cjs [--json]`, exit 0=detected/1=none/2=error); consumed by the `ai-integration` capability's `plan:pre` contribution and blocking `verify:pre` gate (`check api-coverage.verify-pre`) |
|
||||
| `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint |
|
||||
| `assumption-delta.cjs` | Detects identity-model assumption transitions in phase text for discuss-phase assumptions mode (#1561) |
|
||||
| `audit-command-router.cjs` | ADR-959 capability command router for `gsd-tools audit-uat` and `gsd-tools audit-open` — extracted from hardcoded cases in `gsd-tools.cjs`; dispatches to `uat.cjs:cmdAuditUat` and `audit.cjs:{auditOpenArtifacts,formatAuditReport}`; phase 4d-impl-3 |
|
||||
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
|
||||
| `capability-activation.cjs` | Capability activation resolver shared by config validation and capability-state consumers — resolves registry-owned config keys from raw runtime config without re-centralizing migrated settings |
|
||||
@@ -467,9 +477,11 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `broken-windows.cjs` | Broken-windows ledger library (issue #1950) — typed IR + I/O for `.planning/WINDOWS.md` (cross-phase defect register); pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed`/`openCount` + I/O `cmdWindowsStatus`/`cmdWindowsAppend`/`cmdWindowsWaive`/`cmdWindowsMarkFixed`; frozen `REASON` enum for typed-error assertions; CLI surface `gsd-tools windows status\|append\|waive\|fixed`. Generated from `src/broken-windows.cts` |
|
||||
| `capability-writer.cjs` | Capability State Writer (ADR-1213) — write-side inverse of the resolver; projects desired per-capability enabled/gates onto surface + config substrates, then re-resolves (assert-and-report); exports `setCapabilityState` and I/O handler `cmdCapabilitySet`; command surface: `gsd-tools capability set <id> [--on\|--off] [--gate <key>=<true\|false>]` |
|
||||
| `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` |
|
||||
| `claude-orchestration-command-router.cjs` | ADR-959 capability command router for Claude orchestration — workflow-backend detection and emission (#1143) |
|
||||
| `claude-orchestration.cjs` | Claude Orchestration capability (#1143) — Workflow-tool backend detection + emitter; `detectWorkflowBackend` fail-closed gate (`{available, backend: 'workflow'\|'inline', reason}`, degrades to today's inline behavior unless every gate opens) and `emitWorkflowScript` (maps GSD's wave/plan model onto Workflow primitives: wave → sequential `parallel()` barriers, plan → `agent(...)` with per-plan worktree isolation mirroring the inline path). Pure, zero external dependencies, never throws; never invokes the Workflow tool itself |
|
||||
| `cli-exit.cjs` | `ExitError` class and `runMain()` helper — CLI entrypoints throw `ExitError` instead of calling `process.exit()`; `runMain()` translates the outcome into `process.exitCode` so output flushes cleanly |
|
||||
| `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers |
|
||||
| `cli-skew-check.cjs` | Detects version skew when a stale global CLI shadows the project-local install (#1754) |
|
||||
| `host-integration-adapters/cline-sdk-binding.cjs` | Cline SDK binding — pure AgentPlugin `beforeTool` planning-artifact guard and `createAgentModel` model-override resolution adapters, no `@cline/sdk` import (ADR-1239 Phase D, #2090) |
|
||||
| `clock.cjs` | Injectable clock seam (now/sleep) for deterministic lock testing |
|
||||
| `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) |
|
||||
@@ -482,6 +494,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `command-roster.cjs` | Read-only discovery of canonical `commands/gsd/*.md` command stems for runtime artifact conversion and namespace rewrites |
|
||||
| `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) |
|
||||
| `complexity-trigger.cjs` | Pure leaf for the complexity-triggered refactor capability — analyzer, evaluator, and baseline persistence; wrapped by `refactor-trigger-command-router.cjs` (#1953) |
|
||||
| `config-loader.cjs` | Project config loading — defaults merge, legacy-key migration, workstream overlay, unknown-key/profile-override validation (extracted from `core.cjs`, ADR-857) |
|
||||
| `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-types.cjs` | TypeScript type definitions for the `model_policy` config block — `ModelPolicyConfig`, `TierEntry`, `RuntimeTiers`; compiled from `src/config-types.cts` at publish time (ADR-457) |
|
||||
@@ -499,20 +512,27 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `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 |
|
||||
| `edge-probe.cjs` | Spec-completeness edge probe (compiled from `src/edge-probe.cts`, gitignored) — the first adapter of the `probe-core` resolution model (ADR-550 Decision 7): shape classification, applicable-category relevance filter, edge proposal, and the `{explicit, backstop}` verification validators; delegates merge/rollup/CLI to `probe-core`; exports `classifyShape`, `applicableCategories`, `proposeEdges`, `analyzeCoverage`, `validateResolution`, `TAXONOMY` (#550) |
|
||||
| `embedding-adapter.cjs` | Defines the minimal `HostIntegrationInterface` contract every embedding adapter implements (#1680) |
|
||||
| `eval-command-router.cjs` | Routes the `eval.score` verb (compiled from `src/eval-command-router.cts`, gitignored) — thin dispatcher into the eval scoring module (#1579) |
|
||||
| `eval.cjs` | Deterministic eval scoring (compiled from `src/eval.cts`, gitignored) — `computeEvalScore` (coverage*0.6 + infra*0.4, bands 80/60/40) + `cmdEvalScore` CLI domain guard; moves the gsd-eval-auditor's weighted arithmetic out of the prompt into code (#10 / #1579) |
|
||||
| `estimate-cli.cjs` | I/O seam over `phase-estimation.cjs` — the `estimate-check` and `estimate-calibration` query verbs; reads the `workflow.smart_zone_tokens` budget and `.planning/estimation-calibration.json`, both degrading to defaults rather than failing planning (#2630) |
|
||||
| `observability/event.cjs` | DispatchEvent shape factory for every Hub dispatch — traceId/parentTraceId/command/result/timestamp record consumed by DispatchLogger (#177, ADR-0174 P1.3/P1.4) |
|
||||
| `external-descriptor-trust.cjs` | Defense-in-depth path-containment check for third-party plugin descriptors (#1681) |
|
||||
| `external-job.cjs` | Produces scheduler manifests for asynchronous external jobs; SLURM is the first backend (#1164) |
|
||||
| `fallow-runner.cjs` | Fallow audit adapter for `/gsd-code-review`: binary resolution (`node_modules/.bin` then `PATH`), actionable missing-binary errors, and structural findings normalization |
|
||||
| `federated-config.cjs` | Defensive merge of capability-declared config slices into the loadConfig return value — ADR-857 phase 3b; exports `mergeFederatedConfig({ configSchema, isCentralKey, userConfig })` → `{ values, validKeys, warnings }`; live for migrated Capability keys that are atomically removed from the central config schema |
|
||||
| `frontmatter.cjs` | YAML frontmatter CRUD operations |
|
||||
| `gap-checker.cjs` | Post-planning gap analysis (#2493): unified REQUIREMENTS.md + CONTEXT.md decisions vs PLAN.md coverage report (`gsd-tools gap-analysis`) |
|
||||
| `gate-predicate-evaluator.cjs` | Evaluates capability gate predicates — `command-exit-zero` and `artifact-frontmatter` (#2008) |
|
||||
| `git-base-branch.cjs` | Single base-branch resolver (`gsd_run query git.base-branch`) with full precedence ladder: config override → origin/HEAD symref → `git remote show origin` → local branch presence → "main". Eliminates per-workflow duplicated bash detection (#1146) |
|
||||
| `graphify.cjs` | Knowledge-graph build/query/status/diff for `/gsd-graphify` |
|
||||
| `graphify-command-router.cjs` | ADR-959 capability command router for `gsd-tools graphify` — dispatches build/query/status/diff subcommands; first real capability command cutover (phase 4d-impl-2) |
|
||||
| `gsd2-import.cjs` | External-plan ingest for `/gsd-import --from-gsd2` |
|
||||
| `handshake-serialized.cjs` | Serialized handshake for out-of-process host integration; JSON wire-safe (#1683) |
|
||||
| `health-diagnostic-types.cjs` | Shared, dependency-free `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums and `Diagnostic`/`Remedy`/`Rule` types for `validate health` — split out of `health-diagnostic.cjs` so its rule-group files can depend on the enums/types without a CJS circular require back into the evaluator (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
|
||||
| `health-diagnostic.cjs` | Frozen rule-table contract for `validate health` — `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums, `Diagnostic`/`Remedy`/`Rule` shapes, the fully-wired `RULES` table (the static concatenation of the 31 rules exported by the eight `health-diagnostic-rules/*.cjs` group files), `evaluateRules` (throws on duplicate rule codes), and `applyRepairs` (the real `--repair`/`--backfill` dispatcher with real per-action handlers — refuses `DESTRUCTIVE`-risk remedies) (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
|
||||
| `hook-bus.cjs` | Abstracts the hook-bus ownership model — engine pub-sub, host-owned, or none (#1680) |
|
||||
| `host-integration-sdk.cjs` | Versioned public API surface for external host-plugin authors (#1683) |
|
||||
| `host-integration.cjs` | Host-Integration Interface (ADR-1239 Phase A) — negotiated capability contract over the six host-integration points; `negotiateHostCapabilities` fail-closes on undeclared/unknown/`undocumented` values, typed degradation ladder, host-capability profiles; the 8 `runtime.hostIntegration` axes are validated in `capability-validator.cjs` and sourced per-CLI in `docs/reference/host-integration-capability-matrix.md` |
|
||||
| `host-runtime-detection.cjs` | Host Runtime Detection Module (ADR-2313 Phase 5, #3245) — the detection rung beneath `GSD_RUNTIME` and `.planning/config.json` `runtime` that lets `init` report `agent_runtime: codex` inside a Codex session instead of the hardcoded `claude` default; `detectHostRuntime` returns the typed `{runtime, source, signal}` from citation-backed Codex signals (`CODEX_SANDBOX`/`CODEX_SANDBOX_NETWORK_DISABLED`, else `CODEX_HOME` + `config.toml`), `resolveReportedRuntime` composes the full ladder. Pure, injectable, never writes, never shells out |
|
||||
| `host-integration-adapters/imperative-hook-bus.cjs` | Imperative hook-bus adapter — descriptor-driven `hooks.json` binding generalized from the Cursor-specific writer, resolving the negotiated `hookBus` axis against a host's documented `hostBehaviors.managedHookEvents` list; pure, no I/O (ADR-1239 Phase D, #2089) |
|
||||
@@ -540,11 +560,15 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `loop-resolver.cjs` | Loop Extension Point resolver — ADR-857 phase 3c/6 registry-consuming query; given a canonical loop point, filters `byLoopPoint` by resolved Capability State plus config activation (`when` key traversal with prototype-pollution guard), returns `{ point, activeHooks, rendered }` envelope; `resolveLoopHooks` and `renderLoopHooks` are pure (no I/O); command surface: `gsd-tools loop render-hooks <point> [--config-dir <path>]` |
|
||||
| `markdown-sectionizer.cjs` | Canonical markdown-structure parsing seam (ADR-1372, epic #1372) — pure, Node built-ins only; exports `stripFencedCode` (CommonMark-correct fence stripper, CRLF-safe), `stripInlineCode` (per-line CommonMark inline-code-span stripper, #2365), `tokenizeHeadings` (ATX headings outside fenced blocks), `collectSections`/`collectSection` (line-by-line section collection with `bodyStart`/`bodyEnd` offsets), `iterateBullets` (dash/checkbox/numbered markers), `extractTaggedBlocks` (inner text of `<tag>…</tag>` blocks, caller decides fence-stripping), `replaceSection` (pure character-offset body splice for read-modify-write callers), and `withSection` (resolve a section by heading/predicate and run an edit callback against ONLY its body, splicing the result back — ADR-2143 §4 bounded mutation); foundation for T0–T7 migration tiers retiring 8+ ad-hoc parsers |
|
||||
| `markdown-table.cjs` | Canonical GFM table model + `TABLE_SCHEMAS` registry seam (ADR-2143, epic #2143) — pure, Node built-ins only; exports `parseMarkdownTable(sectionText) → Result<MarkdownTable>` (parses the first GFM pipe table, typed parse errors for ragged/malformed rows rather than silent coercion), `MarkdownTable` (`{columns, rows}`, rows addressed by column name), `Result<T>` (`{ok:true,value}\|{ok:false,reason}` — distinct from command-routing-hub's dispatch `Result`), `TABLE_SCHEMAS` (canonical column-header variants for `RoadmapProgress`/`RequirementsTraceability`/`QuickTasks`/`Security` tables), and `matchTableSchema(columns) → {id,label}\|null` (resolves parsed headers back to a canonical schema); consumed by `phase-lifecycle.cts`'s `deriveProgressFromRoadmap` (fixes #2137, the 5-column milestone-grouped Progress table) |
|
||||
| `mcp-catalog.cjs` | Serves GSD workflows, references, and commands as MCP resources and prompts (#3072) |
|
||||
| `mcp-server.cjs` | Minimal MCP server exposing the command surface and state IO to any MCP host (#1681) |
|
||||
| `health-diagnostic-rules/milestone-archive-hygiene.cjs` | Health-diagnostic rules: milestone archive + root hygiene checks (W018, W019), ported behavior-preserving from `cmdValidateHealth` (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
|
||||
| `milestone.cjs` | Milestone archival, requirements marking |
|
||||
| `model-adapter.cjs` | Abstracts model routing — passive tier-based selection or active host-supplied resolution (#1680) |
|
||||
| `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-resolver.cjs` | Model/effort resolution policy — resolves model, tier, granularity, effort, and fast-mode for an agent from config + model profiles/catalog (extracted from `core.cjs`, ADR-857) |
|
||||
| `onboard-projection.cjs` | Projects the onboarding situation from docs, package, and codebase-map readiness (#1671) |
|
||||
| `package-identity.cjs` | Generated single source for GSD's published-package coordinates (npm name, bin name, repo slug, changelog URL, manual-install command), derived from package.json; read by the update worker, `check-latest-version`, and installer (#498) |
|
||||
| `package-legitimacy.cjs` | Registry-API package legitimacy verdicts (OK/SUS/SLOP) from npm/PyPI/crates, slopcheck optional |
|
||||
| `pattern.cjs` | The pattern-construction seam — `escapeRegex` (delegates to the built-in `RegExp.escape`) and `literalPattern`; sole owner of building a `RegExp` from a runtime value (ADR-3212 §1, epic #3212 Phase 1, #3412) |
|
||||
@@ -558,6 +582,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `phases-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools phases` |
|
||||
| `plan-dependency-graph.cjs` | Shared halt-propagation over a plan's `depends_on` DAG — the single topological-order + halt-propagation engine used by both `phase.cjs`'s wave-grouping and `phase-locator.cjs`'s phase-location primitive, so the two can never diverge on which plans a halted plan blocks (#2830) |
|
||||
| `plan-document.cjs` | Canonical parser for a `*-PLAN.md` document BODY (compiled from `src/plan-document.cts`, gitignored; #2790) — `parsePlanDocument(content, planPath?)` returns `<objective>`, the `<task>` block rows (with the legacy `## Task N` heading fallback), per-task `<files>`/`<acceptance_criteria>`/`<done>`, and the frontmatter scheduling metadata (`wave`, `depends_on`, `autonomous`, `agent_hint`, `files_modified`); frozen `TASK_KIND` (`AUTO`/`CHECKPOINT`) so a `checkpoint:*` block is reported as its own kind rather than a malformed auto task. Extracted from `cmdPhasePlanIndex`'s inline loop so `phase.plan-index` and `planning.inspect` cannot drift; the invariant `taskCount === tasks.length === (xmlTaskCount \|\| mdTaskCount)` is preserved byte-for-behaviour |
|
||||
| `plan-drift-guard.cjs` | Classifies symbol-verification severity against the ADR-22 authority ladder |
|
||||
| `plan-scan.cjs` | Canonical phase-plan scanner for detecting plan and summary files in flat and nested layouts (k014) |
|
||||
| `planning-command-router.cjs` | Thin CJS subcommand router for `gsd-tools planning` (compiled from `src/planning-command-router.cts`, gitignored; #2790) — one subcommand, `inspect`; v1 accepts no arguments, so a stray positional or unknown flag is a fail-loud `ERROR_REASON.USAGE` rather than a silently-ignored one |
|
||||
| `planning-inspect.cjs` | Read-only schema-v1 canonical planning snapshot (compiled from `src/planning-inspect.cts`, gitignored; #2790) — `buildPlanningInspect(cwd)` / `cmdPlanningInspect`; `PLANNING_INSPECT_SCHEMA_VERSION = 1` is the wire contract consumers must reject other values of. Composed from the ADR-3180 §7 owners plus `parsePlanDocument`/`parseRequirements`/`parseUatItems`, and read through the Markdown Sectionizer + Markdown Table Model seams; deliberately declares its own flat external schema rather than serializing the still-growing `PlanningSnapshot`. Frozen `INSPECT_DIAGNOSTIC`/`TASK_STATUS`/`PROVENANCE`/`AGREEMENT` enums carry every non-answer — unknown or conflicting evidence is reported with a coded diagnostic, never inferred |
|
||||
@@ -573,6 +598,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `refactor-trigger-command-router.cjs` | ADR-959 capability command router for `gsd-tools refactor` (issue #1953) — dispatches evaluate/status/accept/decline subcommands for the complexity-triggered refactor capability; owns capability-activation gating, git invocation (via the `git-base-branch.cjs` `phaseStartCommit`/`changedFilesSince` adapters), config reads, phase-directory resolution, and the optional broken-windows ledger integration around the pure `complexity-trigger.cjs` leaf |
|
||||
| `research-provider.cjs` | Research provider waterfall, confidence tiers, and planResearch (cache-hits + fetch plan) |
|
||||
| `research-store.cjs` | Content-addressed research cache: sha256 keys, per-source TTL staleness, two-tier (user ~/.gsd / project .planning) store |
|
||||
| `resolution.cjs` | Defines the `Resolution` envelope carried by config-reading verbs, with provenance (#1411) |
|
||||
| `retired-artifact-cleanup.cjs` | Manifest-safe cleanup for descriptor-declared retired runtime artifact surfaces; shared by install and profile/surface apply so removed layout kinds converge without deleting modified or unknown user files (#2644) |
|
||||
| `probe-core.cjs` | Generic spec-phase probe resolution model (compiled from `src/probe-core.cts`, gitignored; ADR-550 Decision 7) — the status×verification re-cut (`status: resolved/dismissed/unresolved` × per-probe `verification`), `validateResolution`/`validateRequirement`, `analyzeCoverage(items, resolutions?, validators)` merge/rollup/orphan-reject, the `byVerification` rollup, and the `runProbeCli` I/O scaffold; the shared seam consumed by `edge-probe` (and the prohibition probe #644); exports `VALID_STATUS`, `validateResolution`, `validateRequirement`, `analyzeCoverage`, `runProbeCli` (#550) |
|
||||
| `prohibition-enforcement.cjs` | Deterministic test-tier prohibition PRODUCER/gate (compiled from `src/prohibition-enforcement.cts`, gitignored; #1259, ADR-550 D5d "heavy half") — locates the wired mechanical check (`node-test` or `lint-rule`), confirms it is fail-first, runs it via an injectable runner, builds typed `enforcementEvidence`, and emits the `dispositionForProhibition` verdict; a passing wired check disposes green, a missing/failing/non-fail-first check hard-gates (flagged, non-green) in both interactive and autonomous modes; exports `runProhibitionEnforcement`, `routeProhibitionEnforcement`; CLI surface `gsd_run check prohibition-enforcement <request.json>` |
|
||||
@@ -600,14 +626,19 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `semver-compare.cjs` | Shared semver comparison policy helpers (`compareSemverCore`, stable-triplet validation, normalized tuple parsing) consumed by update-check hooks, statusline dev-install detection, and changeset extract range logic (#10) |
|
||||
| `security.cjs` | Path traversal prevention, prompt injection detection, safe JSON/shell helpers |
|
||||
| `shell-command-projection.cjs` | Runtime-aware shell command projection for managed hook serialization: decides PowerShell call-operator usage by runtime/platform and normalizes Windows script path tokens |
|
||||
| `smart-entry.cjs` | Classifies workflow state into the enumerated smart-entry situations and their next actions (ADR-1787) |
|
||||
| `spec-section.cjs` | SPEC section-status helper (compiled from `src/spec-section.cts`, gitignored) — the single source of truth for the canonical SPEC headings (suffix-tolerant) and markdown-table row counting; `specSectionStatus`/`countSectionDataRows` decide per-section "supplied" for plan-phase's spec-less probe fallback, replacing ad-hoc awk (contract pinned by `tests/spec-section.test.cjs`) |
|
||||
| `stale-bake-guard.cjs` | Warns when configuration changes after a static-frontmatter runtime bake (#1688) |
|
||||
| `state-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` |
|
||||
| `health-diagnostic-rules/state-consistency.cjs` | Health-diagnostic rules: STATE.md consistency checks (W002, W011, W021, W026) against config/ROADMAP/disk, ported behavior-preserving from `cmdValidateHealth`; W024 (state_head freshness) is a documented gap, deliberately not migrated (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
|
||||
| `state-io.cjs` | Abstracts state IO modes — filesystem, sandboxed storage, or session log (#1680) |
|
||||
| `state-transition.cjs` | Implements the STATE.md field-classification table and the pure `transitionCore` mutation path (#1769) |
|
||||
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
|
||||
| `state-document.cjs` | Pure STATE.md field extraction, replacement, status normalization, and progress calculation transforms |
|
||||
| `milestone-lock.cjs` | Milestone lock (compiled from `src/milestone-lock.cts`, gitignored) — advisory (phase, session id) claim over STATE.md's single Current Position slot: `.planning/milestone.lock` claim IO, liveness (TTL + heartbeat), conflict detection, and the shared stderr warning; consumed by `state.begin-phase` / `state.advance-plan` / `phase.complete` so parallel phases in one working tree get a visible conflict instead of silently overwriting each other (#3311) |
|
||||
| `surface.cjs` | Runtime surface module — manages the runtime enable/disable surface state independently of the install-time profile marker (ADR-0011 Phase 2) |
|
||||
| `task-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools task` |
|
||||
| `teams-status.cjs` | Detects agent-teams status from environment and runtime; pure core (#1355) |
|
||||
| `template.cjs` | Template selection and filling with variable substitution |
|
||||
| `text-lines.cjs` | Line-terminator handling seam — `splitLines`/`normalizeEol`/`detectEol`/`joinLines`, the sole owner of `\r?\n` splitting and CRLF normalization; closes #3360's split-then-match fix in `frontmatter.cjs` (ADR-3212 §3, epic #3212 Phase 2, #3413) |
|
||||
| `token-scanner.cjs` | Tokenizer-first seam for stateful grammars — `tokenizeShellLike` (quote-aware shell tokenizer, the primitive `hooks/lib/git-cmd.js` migrated onto) and `indentWidth` (bullet-nesting depth, closes #3169's cross-reference-vs-declaration false positive in `decisions.cts`) (ADR-3212 §4, epic #3212 Phase 3, #3414) |
|
||||
@@ -617,6 +648,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `ui-consideration-probe.cjs` | Spec-completeness UI-consideration probe (compiled from `src/ui-consideration-probe.cts`, gitignored) — the third adapter of the `probe-core` resolution model (ADR-550 Decision 7): element-kind classification, applicable-category relevance filter, consideration proposal, `proposeElements`/`autoResolve` (propose-then-confirm + the `--auto` never-dismiss floor), and the `{explicit, backstop}` validators; delegates merge/rollup/CLI to `probe-core`; exports `classifyElement`, `applicableCategories`, `proposeConsiderations`, `proposeElements`, `autoResolve`, `analyzeCoverage`, `UI_TAXONOMY` (#1867) |
|
||||
| `ui-safety-gate.cjs` | Shell-free word-boundary UI token detector (#3706, #3718); reads phase-section text from stdin, exits 0 (UI found) or 1 (no UI); also deployed to `gsd-core/bin/lib/` so the GSD installer ships it to `$RUNTIME_DIR` (#448) |
|
||||
| `ui-frontend-evidence.cjs` | Static frontend-evidence detector (compiled from `src/ui-frontend-evidence.cts`, gitignored) — plan-time structural corroboration for `computeUiPlanGate` (#3312): a `package.json` UI-framework dependency or a component-framework file (`*.tsx/*.jsx/*.vue/*.svelte`) in the tree, so a UI-token match on a hyphenated proper noun (e.g. repo `dashboard-financeiro`) cannot block planning in a repo with no frontend; mirrors the post-wave `computeUiSafetyGate` git-diff corroboration |
|
||||
| `unusable-input.cjs` | Deduplicates stderr diagnostics for corrupt or unreadable configuration (#1879) |
|
||||
| `update-context.cjs` | Pure install-context resolver for `/gsd-update` — runtime/scope/config-dir/version detection (LOCAL/GLOBAL/UNKNOWN) ported from update.md bash; backs `gsd-tools update-context` (#498) |
|
||||
| `user-artifact-staging.cjs` | Durable, on-disk staging for `USER_OWNED_ARTIFACTS` across the preserve → wipe → restore window (compiled from `src/user-artifact-staging.cts`, gitignored; #2875, epic #2866 Phase 6, ADR-3574) — closes #1874-F19 (in-memory-only preservation lost on a crash between wipe and restore); `recoverOrphanedUserArtifacts` is wired at the start of `bin/install.js`'s `install()` so an orphaned staged copy from a prior crashed run is recovered before the ordinary preserve step, closing the #1879-F15 inert-fix failure mode; exports `stageUserArtifacts`, `restoreStagedUserArtifacts`, `discardStagedUserArtifacts`, `recoverOrphanedUserArtifacts` |
|
||||
| `validate-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools validate` |
|
||||
@@ -679,7 +711,7 @@ Full listing: `hooks/`.
|
||||
## Maintenance
|
||||
|
||||
- When a new command, agent, workflow, reference, CLI module, or hook ships, update the corresponding section here before the release is cut.
|
||||
- The drift-guard tests under `tests/` (see "How To Use This File" above) assert that every shipped file is enumerated in this inventory. A new file without a matching row here will fail CI.
|
||||
- The drift-guard tests under `tests/` (see "How To Use This File" above) assert that every shipped file in the six flat families — agents, commands, workflows, references, CLI modules, hooks — is enumerated in this inventory. A new file in one of those without a matching row here will fail CI. Workflow `steps/` and `modes/` sub-files are the deliberate exception; see "Workflow Sub-Files" above.
|
||||
- When the filesystem diverges from `docs/ARCHITECTURE.md` counts or from curated-subset docs (e.g. `docs/AGENTS.md`'s primary roster), this file is the source of truth.
|
||||
|
||||
## Related
|
||||
|
||||
File diff suppressed because one or more lines are too long
272
tests/helpers/inventory-roster.cjs
Normal file
272
tests/helpers/inventory-roster.cjs
Normal file
@@ -0,0 +1,272 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* inventory-roster.cjs — the pure half of the `docs/INVENTORY.md` roster gate (#3762).
|
||||
*
|
||||
* `docs/INVENTORY-MANIFEST.json` is anchored against the filesystem by
|
||||
* `tests/inventory-manifest-sync.test.cjs`. This module supplies the second
|
||||
* comparison that test runs: every manifest entry must also have a hand-written row
|
||||
* in `docs/INVENTORY.md`, which calls itself "Authoritative roster of every shipped
|
||||
* GSD surface" (`docs/INVENTORY.md:3`) and promises "a new file without a matching
|
||||
* row here will fail CI" (`docs/INVENTORY.md:681`). Until #3762 neither claim was
|
||||
* true: PR #3758 shipped `gsd-core/references/planner-coupling.md` with a manifest
|
||||
* entry, no roster row, and fully green CI.
|
||||
*
|
||||
* Everything here is PURE — text and the manifest object in, findings out, no
|
||||
* filesystem access — so the matcher is drivable by fixture. A drift guard that can
|
||||
* only be exercised against a clean repo proves nothing about its own comparison.
|
||||
*
|
||||
* ── SCOPE ──────────────────────────────────────────────────────────────────────
|
||||
*
|
||||
* IN: the SIX FLAT families — `agents`, `commands`, `workflows`, `references`,
|
||||
* `cli_modules`, `hooks`. `docs/INVENTORY.md:7` says the file "enumerates every
|
||||
* shipped surface across all six families", and each has its own `##` section.
|
||||
*
|
||||
* OUT: the two NESTED families — `workflow_steps`, `workflow_modes`.
|
||||
* `docs/INVENTORY.md` §"Workflow Sub-Files" is an explicit shipped decision that
|
||||
* these carry no hand-written per-file rows: "Adding a step or mode file requires no
|
||||
* hand-written row here … The per-file roster deliberately lives in
|
||||
* `docs/INVENTORY-MANIFEST.json` rather than being duplicated in this table — 60
|
||||
* rows that must be hand-maintained in lockstep with a generated artifact is the
|
||||
* drift this file exists to catch." Demanding rows there would override that
|
||||
* decision, not enforce the roster.
|
||||
*
|
||||
* ── WHAT COUNTS AS A ROW ───────────────────────────────────────────────────────
|
||||
*
|
||||
* Deliberately narrow, because this runs on every PR and must tolerate formatting it
|
||||
* has no business policing:
|
||||
*
|
||||
* • Each family is searched ONLY inside its own `##` section. A cell in another
|
||||
* family's section does not count — `smart-entry.md` (a workflow) and
|
||||
* `smart-entry.cjs` (a CLI module) are different surfaces. A MISSING section is
|
||||
* reported as one structural failure, never as one phantom row per entry.
|
||||
* • Matching is on a whole table CELL equal to the manifest entry after stripping
|
||||
* WRAPPING backticks — never a substring. The real roster's
|
||||
* `host-integration-adapters/imperative-hook-bus.cjs` row must not satisfy the
|
||||
* separate top-level `hook-bus.cjs` entry, which is genuinely unrostered.
|
||||
* • Subsections (`###`), column count, column order, CRLF, and trailing HTML
|
||||
* comments (`| … |<!-- gsd-allow-legacy-name -->`) are all irrelevant.
|
||||
* • COMMANDS MATCH ON THEIR SOURCE LINK, not their rendered name. Six namespace
|
||||
* routers deliberately render `/gsd-workflow` while sourcing
|
||||
* `commands/gsd/ns-workflow.md`; the display value is not the file identity.
|
||||
* `docs/INVENTORY.md:63` documents every command row as carrying "a link to the
|
||||
* source file", so the link is both the documented shape and the only
|
||||
* collision-free key.
|
||||
*
|
||||
* Restructuring `docs/INVENTORY.md`'s `##` headings, or dropping the command Source
|
||||
* column, is what ROSTER_SECTIONS below has to be updated for — in the same change.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Manifest family name → the level-2 heading in `docs/INVENTORY.md` that rosters it.
|
||||
* The two nested families are absent BY DESIGN (see above).
|
||||
*/
|
||||
const ROSTER_SECTIONS = Object.freeze({
|
||||
agents: 'Agents',
|
||||
commands: 'Commands',
|
||||
workflows: 'Workflows',
|
||||
references: 'References',
|
||||
cli_modules: 'CLI Modules',
|
||||
hooks: 'Hooks',
|
||||
});
|
||||
|
||||
/** A manifest `commands` entry (`/gsd-ns-workflow`) → its source basename (`ns-workflow.md`). */
|
||||
function commandSourceBasename(entry) {
|
||||
return entry.replace(/^\/gsd-/, '') + '.md';
|
||||
}
|
||||
|
||||
/**
|
||||
* Split `text` into a Map of `heading → lines[]` keyed by level-2 (`##`) heading.
|
||||
* Level-3 headings stay INSIDE their parent section, so a family's rows are found
|
||||
* regardless of which `###` subsection a contributor files them under. Split on
|
||||
* `\r?\n`: a Windows checkout is a supported working tree, and a `\n`-only split
|
||||
* would leave a trailing `\r` on every cell and red the whole gate there.
|
||||
*
|
||||
* Fenced code blocks (``` or ~~~) are skipped wholesale — neither their headings
|
||||
* nor their pipe-delimited lines participate. `docs/INVENTORY.md` carries no fence
|
||||
* today, which is exactly why this is worth writing down: the day someone documents
|
||||
* a `## ` example inside one, an unfenced scanner would silently truncate a family
|
||||
* section and red the gate on a document that is perfectly correct.
|
||||
*
|
||||
* The heading pattern is deliberately `(.*)` + trim rather than a lazy `(.+?)` with
|
||||
* a trailing `[ \t]*$`: the lazy form backtracks quadratically on a heading line
|
||||
* padded with thousands of spaces, and this gate parses a file any contributor can
|
||||
* write.
|
||||
*
|
||||
* Both the heading and the fence marker allow CommonMark's LEADING INDENT of up to
|
||||
* three spaces. Anchoring hard at `^##` looks harmless and is not: an author who
|
||||
* indents a heading by one space writes a perfectly valid document that this gate
|
||||
* would read as having NO family sections at all, reporting all six as missing —
|
||||
* a structural red for zero real drift.
|
||||
*/
|
||||
function splitLevel2Sections(text) {
|
||||
const sections = new Map();
|
||||
let current = null;
|
||||
let fence = null;
|
||||
for (const line of String(text).split(/\r?\n/)) {
|
||||
const fenceMark = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
||||
if (fenceMark) {
|
||||
if (fence === null) {
|
||||
fence = fenceMark[1][0];
|
||||
continue;
|
||||
}
|
||||
if (fenceMark[1][0] === fence) fence = null;
|
||||
continue;
|
||||
}
|
||||
if (fence !== null) continue;
|
||||
|
||||
const m = /^ {0,3}##[ \t]+(.*)$/.exec(line);
|
||||
if (m) {
|
||||
current = m[1].trim();
|
||||
if (!sections.has(current)) sections.set(current, []);
|
||||
continue;
|
||||
}
|
||||
if (current !== null) sections.get(current).push(line);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peel the presentation off a whole table cell, one layer at a time, and return
|
||||
* every form encountered — the raw text and each unwrapped result.
|
||||
*
|
||||
* Two layers are recognized, and only when they wrap the cell ENTIRELY:
|
||||
* • a code span: `` `x.md` `` → `x.md`
|
||||
* • a markdown link: `` [`x.md`](../x.md) `` → `` `x.md` `` → `x.md`
|
||||
*
|
||||
* ENTIRELY is the whole safety property. A role cell reading "superseded by
|
||||
* `ghost.md`" keeps its text intact, so a file merely MENTIONED in prose can never
|
||||
* masquerade as a row — which is the false PASS this gate exists to prevent, and is
|
||||
* strictly worse than the false red that motivated handling the link form. The link
|
||||
* text may not itself contain `]`, so a cell holding two links degrades to "no
|
||||
* unwrap" rather than to a garbled middle substring.
|
||||
*/
|
||||
function unwrapCellForms(cell) {
|
||||
const forms = [cell];
|
||||
let current = cell;
|
||||
// At most two peels: link → code span. Bounded, so no pathological input can
|
||||
// turn this into a loop.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const code = /^`(.+)`$/.exec(current);
|
||||
if (code) {
|
||||
current = code[1].trim();
|
||||
forms.push(current);
|
||||
continue;
|
||||
}
|
||||
const link = /^\[([^\]]+)\]\([^)]*\)$/.exec(current);
|
||||
if (link) {
|
||||
current = link[1].trim();
|
||||
forms.push(current);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every whole table cell in `lines`, trimmed, plus the unwrapped forms of each (see
|
||||
* `unwrapCellForms`). Only cells are collected — a line that is not a table row
|
||||
* contributes nothing.
|
||||
*/
|
||||
function tableCells(lines) {
|
||||
const cells = new Set();
|
||||
for (const line of lines) {
|
||||
if (!line.trimStart().startsWith('|')) continue;
|
||||
for (const raw of line.split('|')) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) continue;
|
||||
for (const form of unwrapCellForms(trimmed)) cells.add(form);
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basenames of every `commands/gsd/<file>.md` markdown-link target in `lines`,
|
||||
* regardless of how many `../` hops the relative path takes or whether it carries a
|
||||
* `#anchor` / `?query`.
|
||||
*/
|
||||
function commandSourceLinks(lines) {
|
||||
const basenames = new Set();
|
||||
for (const line of lines) {
|
||||
const linkRe = /\]\(([^)\s]+)/g;
|
||||
let m;
|
||||
while ((m = linkRe.exec(line)) !== null) {
|
||||
const target = m[1].split('#')[0].split('?')[0];
|
||||
const hit = /(?:^|\/)commands\/gsd\/([^/]+\.md)$/.exec(target);
|
||||
if (hit) basenames.add(hit[1]);
|
||||
}
|
||||
}
|
||||
return basenames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare `docs/INVENTORY.md`'s rows against the manifest's family arrays.
|
||||
*
|
||||
* @param {string} inventoryText contents of `docs/INVENTORY.md`
|
||||
* @param {object} families the manifest's `families` object
|
||||
* @returns {{missingSections: string[], missingRows: string[]}}
|
||||
* `missingRows` entries read `"<family>/<entry>"`, matching the phrasing the
|
||||
* manifest half of the gate already uses for additions/removals.
|
||||
*/
|
||||
function findMissingRosterRows(inventoryText, families) {
|
||||
const sections = splitLevel2Sections(inventoryText);
|
||||
const missingSections = [];
|
||||
const missingRows = [];
|
||||
|
||||
for (const [family, heading] of Object.entries(ROSTER_SECTIONS)) {
|
||||
const lines = sections.get(heading);
|
||||
if (lines === undefined) {
|
||||
missingSections.push(heading);
|
||||
continue;
|
||||
}
|
||||
const entries = (families || {})[family] || [];
|
||||
if (family === 'commands') {
|
||||
const links = commandSourceLinks(lines);
|
||||
for (const entry of entries) {
|
||||
if (!links.has(commandSourceBasename(entry))) missingRows.push(family + '/' + entry);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const cells = tableCells(lines);
|
||||
for (const entry of entries) {
|
||||
if (!cells.has(entry)) missingRows.push(family + '/' + entry);
|
||||
}
|
||||
}
|
||||
|
||||
return { missingSections, missingRows };
|
||||
}
|
||||
|
||||
/** Human-readable remediation for a non-empty `findMissingRosterRows` result. */
|
||||
function formatRosterFailure({ missingSections, missingRows }) {
|
||||
return [
|
||||
missingSections.length
|
||||
? 'docs/INVENTORY.md is missing a "## <heading>" section this gate rosters against:\n' +
|
||||
missingSections.map((h) => ' ## ' + h).join('\n') +
|
||||
'\nIf the section was deliberately renamed or merged, update ROSTER_SECTIONS in\n' +
|
||||
'tests/helpers/inventory-roster.cjs in the same change.'
|
||||
: '',
|
||||
missingRows.length
|
||||
? 'Shipped surfaces in docs/INVENTORY-MANIFEST.json with NO row in docs/INVENTORY.md (#3762):\n' +
|
||||
missingRows.map((e) => ' ! ' + e).join('\n') +
|
||||
'\nAdd a row to the matching "## " section of docs/INVENTORY.md: the file name in a cell,\n' +
|
||||
'plus the one-line role that section\'s table asks for. For a command, the row\'s Source\n' +
|
||||
'column must link to ../commands/gsd/<file>.md — that link, not the rendered /gsd-name,\n' +
|
||||
'is what this gate reads. Regenerating the manifest does NOT satisfy this: the roster row\n' +
|
||||
'is hand-written by design, because a role sentence cannot be generated.'
|
||||
: '',
|
||||
].filter(Boolean).join('\n\n');
|
||||
}
|
||||
|
||||
// Only the three names the gate actually consumes are exported. The parsing
|
||||
// helpers above stay module-private on purpose: exporting them would create a
|
||||
// surface nothing calls, and every one of their behaviors is already pinned
|
||||
// through `findMissingRosterRows` by the fixture rows in
|
||||
// `tests/inventory-manifest-sync.test.cjs` — asserting them directly would test
|
||||
// the implementation instead of the contract.
|
||||
module.exports = {
|
||||
ROSTER_SECTIONS,
|
||||
findMissingRosterRows,
|
||||
formatRosterFailure,
|
||||
};
|
||||
@@ -1,10 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Asserts docs/INVENTORY-MANIFEST.json is in sync with the filesystem.
|
||||
* A stale manifest means a surface shipped without updating INVENTORY.md.
|
||||
* Fix by running: node scripts/gen-inventory-manifest.cjs --write
|
||||
* then adding the corresponding row(s) in docs/INVENTORY.md.
|
||||
* Anchors BOTH halves of the inventory-drift rule (`CLAUDE.md` → Inventory Drift:
|
||||
* "requires updating `docs/INVENTORY.md` AND running
|
||||
* `node scripts/gen-inventory-manifest.cjs --write`"):
|
||||
*
|
||||
* 1. `docs/INVENTORY-MANIFEST.json` matches the filesystem — the machine half.
|
||||
* Fix by running: node scripts/gen-inventory-manifest.cjs --write
|
||||
* 2. `docs/INVENTORY.md` carries a roster row for every manifest entry — the human
|
||||
* half (#3762). Fix by hand-writing the row.
|
||||
*
|
||||
* Until #3762 only half 1 existed, so a PR could ship a surface, regenerate the
|
||||
* manifest, omit the roster row, and pass CI green — while `docs/INVENTORY.md:3`
|
||||
* called itself "Authoritative roster of every shipped GSD surface" and
|
||||
* `docs/INVENTORY.md:681` promised "a new file without a matching row here will fail
|
||||
* CI". PR #3758 is the live instance: `gsd-core/references/planner-coupling.md`,
|
||||
* manifest entry present, roster row absent, CI 30 success / 0 failing.
|
||||
*
|
||||
* The roster comparison itself lives in `tests/helpers/inventory-roster.cjs` — read
|
||||
* its header for exactly which families are enforced, which two are deliberately
|
||||
* exempt, and what shapes a row may legally take. The fixture rows at the bottom of
|
||||
* this file drive that matcher directly, because the live assertion passes whenever
|
||||
* the repo is clean and therefore proves nothing about the comparison on its own.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
@@ -12,8 +29,16 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
const {
|
||||
ROSTER_SECTIONS,
|
||||
findMissingRosterRows,
|
||||
formatRosterFailure,
|
||||
} = require('./helpers/inventory-roster.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
|
||||
const INVENTORY_PATH = path.join(ROOT, 'docs', 'INVENTORY.md');
|
||||
|
||||
// #2996: FAMILIES and NESTED_FAMILIES are IMPORTED, never redeclared. This file used to
|
||||
// carry its own copy of the family table — the `DEFECT.GENERATIVE-FIX` divergence class:
|
||||
@@ -64,3 +89,387 @@ test('docs/INVENTORY-MANIFEST.json matches the filesystem', () => {
|
||||
|
||||
assert.ok(additions.length === 0 && removals.length === 0, msg);
|
||||
});
|
||||
|
||||
// ─── #3762: the roster half ──────────────────────────────────────────────────
|
||||
|
||||
test('docs/INVENTORY.md carries a roster row for every manifest entry', () => {
|
||||
const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
||||
const inventory = fs.readFileSync(INVENTORY_PATH, 'utf8');
|
||||
const findings = findMissingRosterRows(inventory, committed.families);
|
||||
|
||||
assert.ok(
|
||||
findings.missingSections.length === 0 && findings.missingRows.length === 0,
|
||||
formatRosterFailure(findings),
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Fixture rows: the gate must be able to FAIL, and only for the right reason ──
|
||||
|
||||
const FIXTURE_HEAD = '# Fixture\n\n';
|
||||
|
||||
/** Assemble a fixture document from `{heading: bodyText}`, optionally with CRLF endings. */
|
||||
function doc(sectionsBySpec, { crlf = false, indent = '' } = {}) {
|
||||
let out = FIXTURE_HEAD;
|
||||
for (const [heading, body] of Object.entries(sectionsBySpec)) {
|
||||
out += indent + '## ' + heading + '\n\n' + body + '\n\n';
|
||||
}
|
||||
return crlf ? out.replace(/\n/g, '\r\n') : out;
|
||||
}
|
||||
|
||||
/** Every in-scope section present and empty, so a row supplies only the table it cares about. */
|
||||
function emptySections(overrides = {}, opts) {
|
||||
const spec = {};
|
||||
for (const heading of Object.values(ROSTER_SECTIONS)) spec[heading] = '(no rows)';
|
||||
return doc({ ...spec, ...overrides }, opts);
|
||||
}
|
||||
|
||||
test('row 2 — the PR #3758 shape: a manifest reference with no roster row is reported', () => {
|
||||
// gsd-core/references/planner-coupling.md shipped in PR #3758 with a manifest entry,
|
||||
// all 19 tests/fixtures/install-tree/*.json updated, and no row in
|
||||
// §"Modular Planner Decomposition". That PR's CI was 30 success / 3 skipped / 0
|
||||
// failing. This row is the reason it would not be.
|
||||
const text = emptySections({
|
||||
References: '| Reference | Role |\n|---|---|\n| `checkpoints.md` | Checkpoint types. |',
|
||||
});
|
||||
const { missingSections, missingRows } = findMissingRosterRows(text, {
|
||||
references: ['checkpoints.md', 'planner-coupling.md'],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(missingSections, []);
|
||||
assert.deepStrictEqual(missingRows, ['references/planner-coupling.md']);
|
||||
});
|
||||
|
||||
test('row 3 — adding the row clears the report', () => {
|
||||
const text = emptySections({
|
||||
References:
|
||||
'| Reference | Role |\n|---|---|\n| `checkpoints.md` | Checkpoint types. |\n' +
|
||||
'| `planner-coupling.md` | Planner coupling rules. |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['checkpoints.md', 'planner-coupling.md'] }).missingRows,
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 4 — a substring of a longer rostered cell is not a row', () => {
|
||||
// The real docs/INVENTORY.md rosters `host-integration-adapters/imperative-hook-bus.cjs`.
|
||||
// A substring match would report the separate, genuinely-unrostered top-level
|
||||
// `hook-bus.cjs` as satisfied — a false pass on a shipped module.
|
||||
const text = emptySections({
|
||||
'CLI Modules':
|
||||
'| Module | Responsibility |\n|---|---|\n' +
|
||||
'| `host-integration-adapters/imperative-hook-bus.cjs` | Imperative hook-bus adapter. |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, {
|
||||
cli_modules: ['host-integration-adapters/imperative-hook-bus.cjs', 'hook-bus.cjs'],
|
||||
}).missingRows,
|
||||
['cli_modules/hook-bus.cjs'],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 5 — a row in another family section does not satisfy this family', () => {
|
||||
// `smart-entry.md` (a workflow) and `smart-entry.cjs` (a CLI module) are different
|
||||
// surfaces sharing a stem; an unscoped document-wide search conflates two families
|
||||
// the moment they ever do share a full name.
|
||||
const text = emptySections({
|
||||
'CLI Modules': '| Module | Responsibility |\n|---|---|\n| `smart-entry.md` | Wrong section. |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { workflows: ['smart-entry.md'] }).missingRows,
|
||||
['workflows/smart-entry.md'],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 6 — a command row is matched on its Source link, not its display name', () => {
|
||||
const text = emptySections({
|
||||
Commands:
|
||||
'| Command | Role | Source |\n|---|---|---|\n' +
|
||||
'| `/gsd-workflow` | Phase pipeline router. | [commands/gsd/ns-workflow.md](../commands/gsd/ns-workflow.md) |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { commands: ['/gsd-ns-workflow'] }).missingRows,
|
||||
[],
|
||||
'the six namespace routers deliberately render a name that is not their file stem',
|
||||
);
|
||||
});
|
||||
|
||||
test('row 7 — a command row without a Source link is not a row', () => {
|
||||
const text = emptySections({
|
||||
Commands: '| Command | Role | Source |\n|---|---|---|\n| `/gsd-quick` | Quick task. | (todo) |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { commands: ['/gsd-quick'] }).missingRows,
|
||||
['commands//gsd-quick'],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 8 — an unbackticked agent cell is a row', () => {
|
||||
// Agents is the one family whose identity column carries a bare, unbackticked name.
|
||||
const text = emptySections({
|
||||
Agents:
|
||||
'| Agent | Role | Spawned by | Primary doc |\n|---|---|---|---|\n' +
|
||||
'| gsd-planner | Creates executable phase plans. | `/gsd-plan-phase` | primary |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(findMissingRosterRows(text, { agents: ['gsd-planner'] }).missingRows, []);
|
||||
});
|
||||
|
||||
test('row 9 — trailing HTML comments and extra columns are tolerated', () => {
|
||||
const text = emptySections({
|
||||
'CLI Modules':
|
||||
'| Module | Responsibility | Extra |\n|---|---|---|\n' +
|
||||
'| `installer-migrations/003-rename.cjs` | Migration. | n/a |<!-- gsd-allow-legacy-name -->',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { cli_modules: ['installer-migrations/003-rename.cjs'] }).missingRows,
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 10 — CRLF line endings are tolerated', () => {
|
||||
const spec = {};
|
||||
for (const heading of Object.values(ROSTER_SECTIONS)) spec[heading] = '(no rows)';
|
||||
spec.Hooks = '| Hook | Event | Purpose |\n|---|---|---|\n| `gsd-statusline.js` | `statusLine` | Statusline. |';
|
||||
const text = doc(spec, { crlf: true });
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { hooks: ['gsd-statusline.js'] }).missingRows,
|
||||
[],
|
||||
'a \\n-only split leaves a trailing \\r on every cell and reds every Windows checkout',
|
||||
);
|
||||
});
|
||||
|
||||
test('row 10b — a fenced code block does not split a section or contribute cells', () => {
|
||||
// docs/INVENTORY.md carries no fence today. The day someone documents a `## `
|
||||
// example inside one, an unfenced scanner truncates the family section at that
|
||||
// line and reds a document that is perfectly correct — and a pipe-delimited line
|
||||
// inside the fence would count as a row it is not.
|
||||
const text = emptySections({
|
||||
References:
|
||||
'```\n## References\n| `imposter.md` | not a row |\n```\n\n' +
|
||||
'| Reference | Role |\n|---|---|\n| `real.md` | Real. |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['real.md', 'imposter.md'] }).missingRows,
|
||||
['references/imposter.md'],
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Fence-length boundary: the matcher's only numeric limit is `{3,}` ──────────
|
||||
//
|
||||
// RULESET.TESTS.boundary-coverage — exercise limit-1 / limit / limit+1 on the fence
|
||||
// marker, for both delimiter characters. Two backticks is a plain inline code span
|
||||
// and must NOT swallow the rows beneath it; three and four must.
|
||||
|
||||
test('row 10f — a TWO-character run is not a fence, and swallows nothing (limit-1)', () => {
|
||||
for (const mark of ['``', '~~']) {
|
||||
const text = emptySections({
|
||||
References: mark + '\n| `kept.md` | Role. |\n' + mark,
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['kept.md'] }).missingRows,
|
||||
[],
|
||||
'a two-' + mark[0] + ' run is inline markup, not a fence — treating it as one hides a real row',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('row 10g — a THREE-character run opens and closes a fence (limit)', () => {
|
||||
for (const mark of ['```', '~~~']) {
|
||||
const text = emptySections({
|
||||
References: mark + '\n| `swallowed.md` | Role. |\n' + mark + '\n\n| `kept.md` | Role. |',
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['kept.md', 'swallowed.md'] }).missingRows,
|
||||
['references/swallowed.md'],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('row 10h — a FOUR-character run opens and closes a fence (limit+1)', () => {
|
||||
for (const mark of ['````', '~~~~']) {
|
||||
const text = emptySections({
|
||||
References: mark + '\n| `swallowed.md` | Role. |\n' + mark + '\n\n| `kept.md` | Role. |',
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['kept.md', 'swallowed.md'] }).missingRows,
|
||||
['references/swallowed.md'],
|
||||
'a longer run is still a fence; only `{3,}` matters, not an exact count',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('row 10c — a heading indented up to 3 spaces is still a heading (CommonMark)', (t) => {
|
||||
// Anchoring hard at /^##/ looks harmless and is not. CommonMark permits an ATX
|
||||
// heading to carry 1-3 leading spaces, so an author who indents one writes a
|
||||
// perfectly valid document that a `^##`-anchored scanner reads as having NO family
|
||||
// sections at all — all six reported missing, a structural red for zero real drift.
|
||||
const text = emptySections(
|
||||
{ References: '| Reference | Role |\n|---|---|\n| `only.md` | Only. |' },
|
||||
{ indent: ' ' },
|
||||
);
|
||||
const { missingSections, missingRows } = findMissingRosterRows(text, { references: ['only.md'] });
|
||||
|
||||
assert.deepStrictEqual(missingSections, [], 'a 3-space indent must not erase every family section');
|
||||
assert.deepStrictEqual(missingRows, [], 'rows under an indented heading are still rows');
|
||||
t.diagnostic('CommonMark 4.2: an ATX heading may be indented 0-3 spaces');
|
||||
});
|
||||
|
||||
test('row 10d — a cell that is a link wrapping a code span is a row', () => {
|
||||
// docs/INVENTORY.md already writes file references as [`docs/AGENTS.md`](AGENTS.md)
|
||||
// in prose. The first contributor who writes a FAMILY ROW that way gets an
|
||||
// inexplicable red on a row that plainly documents the file.
|
||||
const text = emptySections({
|
||||
References: '| Reference | Role |\n|---|---|\n| [`linked.md`](../references/linked.md) | Linked. |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(findMissingRosterRows(text, { references: ['linked.md'] }).missingRows, []);
|
||||
});
|
||||
|
||||
test('row 10e — a file merely MENTIONED in a role cell is still not a row', () => {
|
||||
// The guard on row 10d. Unwrapping presentation must peel only layers that wrap the
|
||||
// cell ENTIRELY; the moment a code span mentioned mid-prose counts, the gate has
|
||||
// traded a false red for a false pass, which is the strictly worse failure.
|
||||
const text = emptySections({
|
||||
References: '| Reference | Role |\n|---|---|\n| `real.md` | Superseded by `ghost.md` in the role prose. |',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['real.md', 'ghost.md'] }).missingRows,
|
||||
['references/ghost.md'],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 11 — a missing family section is reported as a section failure', () => {
|
||||
const text = '# Fixture\n\n## Agents\n\n| Agent |\n|---|\n| gsd-planner |\n';
|
||||
const { missingSections, missingRows } = findMissingRosterRows(text, {
|
||||
agents: ['gsd-planner'],
|
||||
cli_modules: ['a.cjs', 'b.cjs', 'c.cjs'],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(missingSections, ['Commands', 'Workflows', 'References', 'CLI Modules', 'Hooks']);
|
||||
assert.deepStrictEqual(
|
||||
missingRows,
|
||||
[],
|
||||
'a missing section is ONE structural failure, not one phantom row per entry inside it',
|
||||
);
|
||||
});
|
||||
|
||||
test('row 12 — an empty family contributes nothing (limit-1)', () => {
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(emptySections(), { references: [] }),
|
||||
{ missingSections: [], missingRows: [] },
|
||||
);
|
||||
});
|
||||
|
||||
test('row 13 — exactly one entry, rostered (limit)', () => {
|
||||
const text = emptySections({ References: '| Reference | Role |\n|---|---|\n| `only.md` | Only. |' });
|
||||
assert.deepStrictEqual(findMissingRosterRows(text, { references: ['only.md'] }).missingRows, []);
|
||||
});
|
||||
|
||||
test('row 14 — one more entry than the roster carries is the one reported (limit+1)', () => {
|
||||
const text = emptySections({ References: '| Reference | Role |\n|---|---|\n| `only.md` | Only. |' });
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(text, { references: ['only.md', 'extra.md'] }).missingRows,
|
||||
['references/extra.md'],
|
||||
);
|
||||
});
|
||||
|
||||
test('row 16 — nested step/mode families are deliberately out of scope', () => {
|
||||
// docs/INVENTORY.md §"Workflow Sub-Files": "Adding a step or mode file requires no
|
||||
// hand-written row here". If that decision is ever reversed, this row is what has to
|
||||
// change first — deliberately and in the open, rather than by widening
|
||||
// ROSTER_SECTIONS and discovering 62 red rows.
|
||||
assert.deepStrictEqual(
|
||||
findMissingRosterRows(emptySections(), {
|
||||
workflow_steps: ['quick/steps/a.md', 'quick/steps/b.md'],
|
||||
workflow_modes: ['discuss-phase/modes/default.md'],
|
||||
}),
|
||||
{ missingSections: [], missingRows: [] },
|
||||
);
|
||||
});
|
||||
|
||||
test('the failure message names every missing path and the remedy that satisfies it', () => {
|
||||
const rendered = formatRosterFailure({
|
||||
missingSections: [],
|
||||
missingRows: ['references/planner-coupling.md', 'cli_modules/hook-bus.cjs'],
|
||||
});
|
||||
|
||||
assert.match(rendered, /references\/planner-coupling\.md/);
|
||||
assert.match(rendered, /cli_modules\/hook-bus\.cjs/);
|
||||
assert.match(
|
||||
rendered,
|
||||
/Regenerating the manifest does NOT satisfy this/,
|
||||
'the predictable wrong guess is "run the generator"; the message has to close that door',
|
||||
);
|
||||
});
|
||||
|
||||
test('property — a command is reported missing exactly when no Source link names its file', () => {
|
||||
// The cell-equality property below covers the five families that share one rule.
|
||||
// `commands` is the family with its OWN rule, and therefore the one where an
|
||||
// untested edge is most likely — so it gets its own property rather than riding on
|
||||
// the five hand-written command fixtures.
|
||||
const stemArb = fc.stringMatching(/^[a-z][a-z0-9-]{0,10}$/);
|
||||
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.uniqueArray(stemArb, { minLength: 1, maxLength: 8 }),
|
||||
fc.uniqueArray(stemArb, { maxLength: 8 }),
|
||||
(linked, candidates) => {
|
||||
const absent = candidates.filter((c) => !linked.includes(c));
|
||||
// Render each row with a DISPLAY NAME that is deliberately not the file stem,
|
||||
// mirroring the six real namespace routers: if the matcher ever falls back to
|
||||
// the rendered name, this property fails.
|
||||
const rows = linked
|
||||
.map((s) => '| `/gsd-alias-' + s + '` | role | [src](../commands/gsd/' + s + '.md) |')
|
||||
.join('\n');
|
||||
const text = emptySections({ Commands: '| Command | Role | Source |\n|---|---|---|\n' + rows });
|
||||
|
||||
const { missingRows } = findMissingRosterRows(text, {
|
||||
commands: [...linked, ...absent].map((s) => '/gsd-' + s),
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
missingRows.slice().sort(),
|
||||
absent.map((s) => 'commands//gsd-' + s).sort(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('property — an entry is reported missing exactly when no cell in its section equals it', () => {
|
||||
const nameArb = fc.stringMatching(/^[a-z][a-z0-9-]{0,10}\.md$/);
|
||||
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.uniqueArray(nameArb, { minLength: 1, maxLength: 8 }),
|
||||
fc.uniqueArray(nameArb, { maxLength: 8 }),
|
||||
(rostered, candidates) => {
|
||||
// `candidates` may overlap `rostered`; the entries genuinely absent from the
|
||||
// table are exactly the set difference, and that is what the matcher must
|
||||
// return — no more (false red) and no fewer (false pass).
|
||||
const absent = candidates.filter((c) => !rostered.includes(c));
|
||||
const rows = rostered.map((n) => '| `' + n + '` | role |').join('\n');
|
||||
const text = emptySections({ References: '| Reference | Role |\n|---|---|\n' + rows });
|
||||
|
||||
const { missingRows } = findMissingRosterRows(text, {
|
||||
references: [...rostered, ...absent],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
missingRows.slice().sort(),
|
||||
absent.map((e) => 'references/' + e).sort(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user