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.
130 lines
6.4 KiB
Markdown
130 lines
6.4 KiB
Markdown
# 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](../reference/state-md.md) instead.
|
|
|
|
## Add a key
|
|
|
|
**1. Declare it in `src/state-md-schema.cts`.**
|
|
|
|
```ts
|
|
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.**
|
|
|
|
```bash
|
|
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](#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](../adr/3408-state-write-path-preservation.md) §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](https://github.com/open-gsd/gsd-core/issues/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](https://github.com/open-gsd/gsd-core/issues/3853).
|
|
|
|
[ADR-3473](../adr/3473-enforcement-by-construction.md) §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.
|