Files
msd-core/docs/how-to/change-the-state-md-schema.md
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

6.4 KiB

How to change the STATE.md schema

Every key in .planning/STATE.md's frontmatter is declared once, in src/state-md-schema.cts. The field-classification tables, the shipped template and the five reference documents are all derived from it. This page is how you add, change or remove a key without any of those falling out of step.

If you only want to know what the keys are, read the STATE.md reference instead.

Add a key

1. Declare it in src/state-md-schema.cts.

my_new_key: {
  type: 'string',
  cardinality: 'optional',
  source: 'body',
  preservation: 'preserve-when-unchanged',
  bodySource: ['My New Key'],
  bodyLabel: 'My New Key',
  emitted: 'when-present',
},

Every column is required except enum, guard, mergeStrategy, bodySource, bodyLabel and acceptedShapes. What each means is documented on the type itself — read it there rather than copying a neighbouring row and hoping.

2. Build, then regenerate. In that order.

npm run build:lib
npm run regen:derived

The order matters and getting it wrong fails quietly. The generator reads the compiled msd-core/bin/lib/state-md-schema.cjs, not the TypeScript source. Regenerating before building regenerates against the previous schema, produces artifacts that look plausible, and commits a document that disagrees with the code you just wrote. If you are ever unsure whether the build is current, run npm run build:lib again — it is cheap and idempotent.

3. Commit the regenerated artifacts. They are generated and committed:

  • msd-core/templates/state.md
  • docs/reference/state-md.md and its ja-JP, zh-CN, ko-KR, pt-BR siblings

4. Add the human-facing rows by hand. Two tables in the reference documents are deliberately not generated — see What is generated and what is not. Add your key's row to the Field reference table in each locale. The parity check will tell you if you miss one.

Change or remove a key

Same two commands. Removing a key also means removing its row from the Field-reference tables in all five locales, or the parity check fails naming each one.

Before you change a key's preservation or source, read ADR-3408 §8 — those columns drive what survives a STATE.md write, and a change there is a behavior change, not a documentation edit.

What the check is telling you

npm run lint:generated-sync runs node scripts/gen-state-md-docs.cjs --check, and lint:ci runs it for you. It exits non-zero with a reason code and the file and region involved.

Reason What happened What to do
region_stale A generated region does not match what the schema would produce. npm run build:lib && npm run regen:derived, then commit the result.
markers_missing A target file has no STATE-MD-SCHEMA marker pair for a region. Add the marker pair where the region belongs. The generator never invents a location.
marker_unclosed A :START: marker has no matching :END:. Fix the markers. The generator refuses to write rather than guess where the region ends — a wrong guess would eat hand-written prose.
field_reference_drift The Field-reference table's row set disagrees with the schema. Add the missing row, or remove the row for a key that no longer exists.
status_values_drift The Status-values table disagrees with the schema's status enum. Same.

--json gives you the same information structurally if you are scripting against it.

What is generated and what is not

Region Generated?
The template's frontmatter block yes
### Status lifecycle yes, in all five locales
### Field cardinality yes, in all five locales
Field reference table no — row set parity-checked only
Status values table no — row set parity-checked only
All prose outside a marked region no, ever

The last three are the point. The Field-reference and Status-values tables carry per-row prose — Purpose, When populated, Matched text — that is genuinely hand-translated. The Japanese Matched-text column reads `discussing` を含む, not the English. Generating those tables from a single English source would overwrite four languages' translations every time anyone regenerated. So the schema owns the row set, which is what drift actually means, and translators own the prose.

If you edit inside a marked region, the next --write will overwrite you and --check will report it first. If you edit outside one, nothing touches it.

Adding a language

Copy an existing locale's reference/state-md.md, translate the prose, and keep the STATE-MD-SCHEMA marker pairs where they are. Then run the two commands above; the generator fills every marked region for the new locale, and the parity check starts holding it to the same row set as the rest.

Column headers come from a per-locale string table in the generator — add yours there so the generated tables are not headed in English.

Keys the schema does not model

active_phase, next_action and next_phases are real frontmatter keys (#2833) that are documented but sit outside the schema. They are grandfathered by exact name in KNOWN_SCHEMA_GAP_FIELDS, so the parity check tolerates those three and no others — a fourth undocumented key fails, which is what stops the list quietly becoming a wildcard.

If you are adding one of those three to the schema properly, remove its name from that list in the same change.

Why the schema exists

Before it, this key set was declared in four places that had to agree by hand, and they did not: one table carried last_activity, another did not. The five reference documents disagreed too — the section documenting the status values was missing from all four translations, and it is the section that matters for #3853.

ADR-3473 §8.8 is the contract, and its governing idea is worth keeping in mind when you edit the schema: the schema declares what the code does, not what it should do. If you find yourself writing a row that describes intended behavior, you are writing a document that lies — declare today's behavior and fix the code separately.