* feat(#3840): generate docs/FEATURES.md from per-feature fragments docs/FEATURES.md was hand-maintained, and every feature PR wrote into two shared mutable cells: the '### N.' heading whose integer was hand-allocated at authoring time, and the hand-maintained table of contents. Concurrent PRs all picked the same next integer, and two PRs adding differently numbered features still collided on the TOC. #3831 was renumbered 165 -> 166 -> 167 -> 168 across successive rebases, each collision also costing a full matrix verification run because the sha-keyed pass marker dies with the rebase. Mechanism: one fragment per feature at docs/features/<slug>.md carrying id/title/group (and an optional order) in frontmatter, consolidated by scripts/gen-features.cjs --write|--check into a marker-delimited region of docs/FEATURES.md that holds BOTH the TOC and every section body. Group headings and their order are derived too - a group sorts by its lowest-ordered member - so there is no shared registry to edit either; optional per-group prose lives in docs/features/_groups/<slug>.md. A contributor adds exactly one new file. Wired into regen:derived and lint:generated-sync alongside the eight existing generators, matching gen-adr-index.cjs's CLI shape and typed-REASON reporting. Migration froze all 168 existing numbers verbatim: identical section set, identical order, identical bodies. Two defects found in the tree are fixed inline rather than carried forward - the '## Related' block had been spliced into the middle of the document, orphaning §142's Reference line, and four inbound anchors were already broken on next (FEATURES.md#runtime-identity in two files, and #143-spec-phase-edge-completeness-probe off by one). Since the repo has no link checker, --check now validates every inbound FEATURES.md#anchor by resolved target, so that class cannot ship silently again; locale FEATURES.md files resolve elsewhere and stay out of scope. Refs #3840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3840): carry upstream §69 delta into its fragment and harden the generator Review found section 69 missing '[--strict]' and REQ-STATE-05/06 versus origin/next. Root cause was a stale base, not extraction loss: those lines landed in394bf384b(#3844) AFTER this branch forked at63abcface, and 'git diff63abcfaceorigin/next -- docs/FEATURES.md' is exactly that hunk. Merging origin/next auto-applied the hunk into the GENERATED region, which --check immediately reported as stale; the delta is now carried in docs/features/statemd-consistency-gates.md and regenerated from there. --write is now fail-closed. It previously rendered the region even with violations outstanding, warning only on stderr and exiting 0, so a '--write && git commit' chain could commit a FEATURES.md carrying two colliding sections. It now refuses and exits 1; --force is the explicit override and says so in the report. The test that pinned the old behavior now pins the refusal, plus the --force override and its scoping. Marker forgery is rejected at two layers. A fragment body containing '<!-- FEATURES:START' or '<!-- FEATURES:END' is a typed body_forges_region_marker violation (fragments and group notes alike), and spliceIntoFeatures anchors the end boundary with lastIndexOf instead of indexOf, so a marker that reaches the document by any other route can only make the generated region grow, never shrink. Matching is on marker PREFIXES, so a decorated variant comment cannot slip past. Symlinked corpus entries are refused with a typed dirent_not_regular_file rather than read. A fork PR could otherwise commit docs/features/evil.md as a symlink to any readable path and have the generator inline those bytes into the committed docs/FEATURES.md on the next regen. Equivalence re-verified with a method that cannot cancel out. The first check extracted both operands with the same body-normalising helper, so anything that helper dropped was dropped on both sides. The replacement runs two independent passes: a global content-line multiset diff with no per-section logic at all (0 gained, 19 lost, all 19 the stale hand-written mini-TOC links this change deliberately deletes), and a per-section byte-exact body diff carrying a coverage assertion that fails loudly per file when the extractor accounts for fewer lines than the file contains. That assertion caught two blind spots in the checker itself. 168/168 sections present, order identical, one intended body difference (§142 regains the Reference line orphaned by the misplaced '## Related' block). Refs #3840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3840): backfill changeset PR number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
3.4 KiB
id, title, group
| id | title | group |
|---|---|---|
| 163 | Read-Only Planning Snapshot (`planning inspect`) | v1.7.0 Features |
Command: gsd-tools query planning inspect
Purpose: Give downstream consumers — harness UIs, mission-control surfaces, dashboards, bots — one schema-versioned JSON document describing everything .planning/ knows, so nothing outside gsd-core has to parse ROADMAP.md / REQUIREMENTS.md / *-PLAN.md / *-SUMMARY.md a second time. gsd-core is the single source of .planning/ truth; a second parser is a second answer.
Requirements:
- REQ-INSP-01:
PLANNING_INSPECT_SCHEMA_VERSION = 1is emitted asschema_version. Consumers MUST reject any other value rather than best-effort-parse an unknown shape. - REQ-INSP-02: Read-only. The command mutates no planning state, and mutates nothing on disk, under any input.
- REQ-INSP-03: Unknown or conflicting evidence serializes as
null/"unknown"with a coded entry indiagnostics[]— never inferred, reconciled, or defaulted. Every key is always present; a key is never omitted to signal absence. - REQ-INSP-04: Argument errors fail loud (non-zero exit, typed
ERROR_REASON); data gaps do not. v1 takes no arguments, and a stray positional or unknown flag is a usage error rather than a silently-ignored one. - REQ-INSP-05: Roadmap acceptance, verification status, and UAT items are reported side by side per phase and are never folded into a single verdict. A ROADMAP checkbox carries
authoritative: false— completion is derived from disk state. - REQ-INSP-06:
accepted_phasesandcompleted_plansare independent fractions.percentisnullwhenever the scope is notcomplete, per the same rule the roadmap and progress surfaces follow. - REQ-INSP-07: Payloads over ~50 KB use the existing
@file:spill channel, resolved transparently before stdout.
Why it does not simply serialize the internal snapshot. PlanningSnapshot (the diagnostic-rule subject introduced by ADR-3180 §8.1) is deliberately additive and still growing — four fields at Phase 10, twenty-plus by Phase 12. Handing that shape to external consumers would freeze an internal contract by accident. planning inspect declares its own flat schema and maps into it, so a field added to PlanningSnapshot never changes what this command emits.
Composed, never re-derived. Milestone identity and phase enumeration arrive via buildPlanningSnapshot; completion from isPhaseComplete (disk-strict); live-plan counting from scanPhasePlans; the percentage arithmetic from clampPercent; STATE fields from stateFieldValue; plan bodies from the Plan Document Module; requirement IDs from parseRequirements; UAT items from parseUatItems. Markdown structure is read through the Markdown Sectionizer and Markdown Table Model seams, so the Traceability table is resolved by column name against its registered schema rather than by a position-anchored regex.
Known limit — task-scoped file provenance. A <task> declares the files it plans to touch, but SUMMARY.md's ## Files Created/Modified describes the whole plan. Spreading that list across a plan's tasks would be inference, so a task's changed_files is populated only where the summary attributes files to that specific task; otherwise it is null with provenance: "plan_scoped". Closing this needs a change to the SUMMARY format, not to the reader.
Reference: CLI Tools · Consume the planning snapshot