Tom Boucher 55fba5f7ce feat(#4917): add the PlanningDoc parse → mutate → serialize seam — Phase 1 of #4906 (#4918)
* feat(#4917): add the PlanningDoc parse -> mutate -> serialize seam

Phase 1 of epic #4906, implementing ADR-4910 and its 2026-09-21 amendment. Net-new
leaf module; NO call site is migrated, so nothing in the twelve absorbed issues
changes behavior yet.

src/planning-document.cts composes the seams that already exist rather than
reimplementing them: markdown-sectionizer for structure (fences and code spans
come from stripFencedCode / scanInlineCodeSpans, never a second scanner),
markdown-table for tables, frontmatter for frontmatter, write-set for Result<T>.

What is structural rather than conventional:

- A field node carries labelSpan, valueSpan and trailingSpan separately, and the
  only write entry point takes a node id and writes into valueSpan. trailingSpan
  is readable and has no exported writer, so the #2853/#3584/#4852 rule ("the verb
  owns the count token ONLY") stops depending on an author remembering a third
  capture group.
- Mutation is node-addressed. A handle is minted by the parser, so a caller cannot
  name a node the parser did not find. No path strings — a path is a grammar, and a
  grammar needs a parser.
- serialize splices staged spans into the ORIGINAL buffer. A no-edit serialize is
  byte-identical, which is what eliminates the #4499 defect without a targeted fix, and which also
  makes an already-escaped table cell impossible to double-escape on a round trip.
- A node that fails to parse carries its own error and span; siblings stay readable.
- Per the ADR amendment, serialize REFUSES whenever any node carries a parse error,
  even with zero staged edits, naming the offender.

One defect was found while building and fixed in place rather than deferred:
PLANNING_ARTIFACTS first derived from isCanonicalPlanningFile unfiltered, so
config.json, state.json, milestone.lock and skill-manifest.json were accepted and
returned {ok:true, nodes:[]} — "this document records nothing" when the truth was
"I have no grammar for this file". That is the exact empty-vs-error confusion this
epic exists to remove (#4899, #4900), reproduced inside the seam built to prevent
it. The registry now filters to .md while still DERIVING from
isCanonicalPlanningFile, because hand-writing a second list is the divergence this
epic is about, and parsePlanningDoc refuses a non-markdown kind at the document
level per ADR-4910 section 5.

New bin/lib module bookkeeping: .gitignore, eslint.config.mjs ignore (ADR-457 —
lint the .cts, not the emitted .cjs), docs/INVENTORY.md row, regenerated
docs/INVENTORY-MANIFEST.json, and the CONTEXT.md glossary entry.

Refs #4906

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#4917): cover the PlanningDoc seam across 28 input classes

30 cases in 14 describe blocks, one per row of the phase test matrix.

The two load-bearing tests are the fast-check properties (seed 20260921,
numRuns 200):

- a single node mutation leaves every byte outside that node's valueSpan
  identical to the source
- serialize with zero staged edits is the identity function

Both are DOCUMENT-SHAPED per CONTRIBUTING.md fixture provenance (#2371): the
generator assembles arbitrary frontmatter, heading, label and value text with
join(), and never calls serialize or any other function from the module under
test to build a fixture. Seeding the generator from the module's own writer
would make the document shape a constant, and the property could then never
explore a document the writer would not itself emit.

Verified the byte-range assertion is not vacuous with a control run: it passes
against the real writer and FAILS against a simulated #4852 writer (one capture
group, replace-to-end-of-line), which visibly drops the trailing annotation.

Boundary coverage is zero / one / two staged edits. Negative space carries its
own rows — bold emphasis in prose, a field-shaped line inside a fenced block,
the same inside an inline code span, and a horizontal rule mid-body all
correctly mint no node. Row 24 is the Generative-Fix-Divergence parity
assertion: PLANNING_ARTIFACTS must not diverge from isCanonicalPlanningFile.
Row 28 covers the non-markdown canonical file found during the build.

Assertions are structural throughout — a typed Result / NodeRead /
SerializeOutcome shape, or a byte range computed from the node's own Span.
Full-string equality appears only where the contract IS byte equality.

Refs #4906

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#4917): apply review findings — refuse unrepresentable values, compose the layers the seam claimed

Three review passes ran against this branch: /security-review, an isolated
adversarial pass, and a standards+spec pass. Five findings, all fixed here. Every
one is the epic's own failure class reproduced inside the seam built to end it,
which is the thing worth noticing.

1. setFieldValue accepted a value containing a newline. It survived serialization
   and reparsed as a REAL sibling field — one write to Plans forged a second
   Owner into a document that already had one. Content became structure, which
   defeats ADR-4910 Decision 2's "cannot reach past its own token by
   construction": the token boundary is a LINE boundary.

2. setFieldValue accepted a value containing the trailing separator and SILENTLY
   TRUNCATED it. Staged "sneaky - annotation", read back "sneaky", with the
   remainder reclassified as trailing prose. No error, nothing unreadable, both
   resulting nodes parsing perfectly. Worse than (1) because it loses the
   caller's own value rather than adding something visible.

   Both are fixed by ONE general check, deliberately not a blacklist:
   setFieldValue rebuilds the candidate line, re-parses it through the same field
   grammar, and refuses unless the value reads back identical. Blacklisting the
   separator would close this instance and leave the class open for whatever
   separator the grammar grows next. The round-trip check is ADR-4910 Decision 4
   stated executably.

3. The module reimplemented two layers it claims to compose. Checklist detection
   hand-rolled a checkbox regex that markdown-sectionizer's iterateBullets
   already owns. Frontmatter span detection re-derived fence handling because
   frontmatter.cts's frontmatterRegion was module-private — so ADR-4910 section 1's
   stated layering was UNREACHABLE as written, and the first implementation
   routed around it silently instead of surfacing the gap.

   frontmatterRegion is now exported (additive only; ADR-2143 section 2's
   extend-never-mutate lock is inherited) and both layers are consumed.

4. The CONTEXT.md glossary entry asserted "the Frontmatter Module supplies
   frontmatter" while zero frontmatter.cts code was invoked. That was a false
   claim in the repo's vocabulary of record, written by me, and it is now true
   rather than edited away.

5. Adopting iterateBullets narrowed GFM coverage: it classifies only
   dash-prefixed task items as checkboxes, so "* [ ] x" and "+ [x] y" stopped
   becoming checklist nodes. Widening the sectionizer is forbidden by the
   inherited lock, so the task-list MARKER is interpreted in this module while
   bullet STRUCTURE still comes from the sectionizer.

The sharpest finding was not a defect. The fast-check generator constrained
values to [A-Za-z0-9 .,!?], so it could not emit an em-dash, newline, backtick,
pipe or asterisk — precisely where (1) and (2) lived. The property was real,
seeded and non-vacuous, and structurally blind to the module's actual bug class.
The generator now spans the grammar's own metacharacters, and a new property
asserts that every value setFieldValue ACCEPTS round-trips identically. Proven
able to fail: against a scratch copy with the guard stripped it fails after 7
cases on newValue "\n".

Recorded as a measured boundary, not fixed: a bare CR inside a field line leaves
that field unrecognised. Measured — sibling fields still parse, no error node,
and serialize stays byte-identical, so the worst case is an unreadable field and
never a damaged document. Flagged for Phase 4's empty-vs-error census.

Refs #4906

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#4917): backfill changeset pr number to 4918

Refs #4906

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 11:17:58 -04:00

GSD Core

Git. Ship. Done.

English · Português · 简体中文 · 日本語 · 한국어

A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.

npm version npm downloads Tests Discord GitHub stars License


What is GSD Core

GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves context rot — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean.


How it works

Each milestone repeats the same five-step loop, one phase at a time:

  1. Discuss — capture implementation decisions before anything is planned
  2. Plan — research, decompose, and verify the plan fits a fresh context window
  3. Execute — run plans in parallel waves; each executor starts with a clean 200k-token context
  4. Verify — walk through what was built; diagnose and fix before declaring done
  5. Ship — create the PR, archive the phase, repeat for the next one

Quickstart

npx @opengsd/gsd-core@latest

The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from agents/ or commands/ directly.

On another runtime or without Node.js? See Install on your runtime.

Once installed, start a new project or onboard an existing repo:

/gsd-new-project   # greenfield project
/gsd-onboard       # existing codebase

New here? Follow Your first project for a guided walkthrough from install to first shipped phase, or Onboarding an existing codebase for brownfield setup.


Documentation

What's new in 1.7.0 → docs/whats-new-1.7.0.md

Tutorials — learning by doing:

How-to guides — task-focused recipes:

Reference — authoritative facts:

Explanation — concepts and design decisions:

Full index: docs/README.md. Other languages: 日本語 · 한국어 · Português · 简体中文.


Why it works

Most AI-coding setups fail at scale because context bloat silently degrades output quality, there is no shared memory between sessions, and nothing verifies that code actually works. GSD Core solves all three: heavy work runs in fresh subagents, structured artifacts like STATE.md and CONTEXT.md survive session boundaries, and the verify step walks through what was built and generates fix plans before a phase is declared done. See docs/explanation/context-engineering.md for the full reasoning.

Troubleshooting? See docs/how-to/recover-and-troubleshoot.md.


Community

Project Platform
gsd-opencode Original OpenCode port
Discord Community support

Star History

Star History Chart

License

MIT License. See LICENSE for details.


Claude Code is powerful. GSD Core makes it reliable.

Description
No description provided
Readme MIT 77 MiB
Languages
JavaScript 82.3%
TypeScript 17.4%
Shell 0.3%