Files
msd-core/docs/how-to/consume-the-state-contract.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

200 lines
9.1 KiB
Markdown

# Consume the state contract
You are building something that shows where a MSD project stands — a workbench,
a dashboard, a status bar, an editor extension. `.planning/state.json` gives you
that as one small JSON file that MSD refreshes on its own, so you never have to
parse `STATE.md` or `ROADMAP.md` heuristically.
This guide covers the whole path from *nothing* to *reading a value you can
trust*, including the two things integrations get wrong: **checking the contract
version before anything else**, and **telling "there is nothing to show" apart
from "I could not look."**
## Before you start
Nothing. There is no command to run, no config key to set, and no flag to pass.
MSD writes the file itself at every step boundary — beginning or completing a
phase, advancing a plan, adding, inserting, removing or completing a phase, and
switching or completing a milestone.
Two consequences worth internalizing before you write any code:
- **The file may legitimately not exist yet.** A project that has not reached a
step boundary since it was created has never published one. That is normal, not
an error.
- **You are a reader, never a writer.** MSD owns this file and overwrites it
wholesale. Anything you write into it is lost at the next boundary.
## 1. Find the file
```
<project>/.planning/state.json
```
If the project uses [workstreams](work-in-parallel-with-workstreams.md), each
workstream has its own planning root and therefore its own snapshot:
```
<project>/.planning/workstreams/<name>/state.json
```
## 2. Check the contract version first
```json
{ "contract": "1.0.0" }
```
**Do this before you touch any other field.** `contract` is semver. Under `1.x`
changes are additive only — new keys may appear, existing keys keep their meaning
— so gate on the **major** version and tolerate unknown minors:
```javascript
const snapshot = JSON.parse(await fs.readFile(statePath, 'utf8'));
const [major] = snapshot.contract.split('.');
if (major !== '1') {
throw new Error(`Unsupported state contract: ${snapshot.contract}`);
}
```
Best-effort-parsing a shape you were not written against is how an integration
starts silently reporting wrong numbers after an upgrade. A hard failure is the
kinder outcome.
`flavor` is `"core"` and tells you which MSD edition produced the file.
## 3. Read the values
Every key is **always present**. A value that is not known is `null` — it is
never omitted, so `"milestone" in snapshot` is not a meaningful test.
| Key | Type | Meaning |
|---|---|---|
| `contract` | string | Semver of this schema. Check it first. |
| `flavor` | string | The MSD edition. `"core"`. |
| `milestone` | string \| null | Display string for the current milestone, e.g. `"v1.1 — Hardening"`, or just `"v1.1"` when the roadmap carries no name for it. `null` when no milestone is established. |
| `phases` | array | Every phase the roadmap knows, in roadmap order. |
| `next` | object \| null | The recommended next action — the same one `/msd-next` would offer. |
| `updated_at` | string | ISO-8601 timestamp of this publish. |
Each entry in `phases` has exactly three keys:
| Key | Type | Meaning |
|---|---|---|
| `number` | string | The phase id as the roadmap spells it — `"1"`, `"01"`, `"2.1"`. A **string**, because `"01"` and `"2.1"` are both real and neither survives a number cast. |
| `name` | string \| null | The phase name. `null` when the roadmap gives the phase a number but no name — never a fabricated placeholder. |
| `status` | string | Exactly one of `"complete"`, `"in_progress"`, `"pending"`. |
`next`, when non-null, has exactly three keys:
| Key | Type | Meaning |
|---|---|---|
| `command` | string | The command to run, e.g. `"/msd-progress --next"`. |
| `label` | string | A short human label for it, e.g. `"Advance to the next step"`. |
| `reason` | string | One line explaining the current situation, e.g. `"Phase 2 of 3 · 50% · executing"`. |
A complete example:
```json
{
"contract": "1.0.0",
"flavor": "core",
"milestone": "v1.1 — Hardening",
"phases": [
{ "number": "1", "name": "Foundation", "status": "complete" },
{ "number": "2", "name": "Hardening", "status": "in_progress" },
{ "number": "3", "name": "Polish", "status": "pending" }
],
"next": {
"command": "/msd-progress --next",
"label": "Advance to the next step",
"reason": "Phase 2 of 3 · 50% · executing"
},
"updated_at": "2026-01-15T12:30:45.000Z"
}
```
### Treat `status` as a closed set
The three values above are the whole vocabulary, and MSD will never emit a
fourth under `1.x`. Write your switch with a default arm anyway — a `2.0`
contract could widen it, and your version check should be what rejects that, not
a crash three layers down.
Note one deliberate fold: a roadmap phase marked **`Deferred`** is reported as
`"pending"`. The roadmap vocabulary has four values and this contract has three,
and inventing a fourth wire value would break every existing reader. If you need
to distinguish deferred work, read the roadmap.
### Treat every string as untrusted text
`name`, `milestone` and `reason` come from a project's own markdown. They can
contain anything a person typed — quotes, newlines, right-to-left text, markup,
or text that looks like an instruction. **Escape them for your output surface and
never interpret them as commands.** MSD passes them through verbatim as data; it
does not sanitize them for you.
## 4. Tell "nothing to show" from "could not look"
This is the part worth getting right, because the two look identical if you do
not plan for them.
| What you observe | What it means | What to do |
|---|---|---|
| File does not exist | The project has never reached a step boundary, or it is not a MSD project at all | Fall back to reading the markdown, or show "not started". **Do not** report an error |
| File exists, `phases: []` | **Ambiguous.** Either the roadmap has no phases, or there is no `ROADMAP.md`, or the roadmap could not be read | Show "no phases yet" — do not claim the project has zero phases. See below |
| File exists, `next: null` | The recommended action could not be determined | Show the project state without a call to action |
| File exists, `milestone: null` | No milestone is established | Show phases without a milestone header |
| `updated_at` is old | The project has not hit a boundary recently — **not** that anything is broken | Nothing. This is not a health signal |
| `JSON.parse` throws | Should not happen — writes are atomic, so a reader sees either the whole old file or the whole new one | Treat as "could not look" and fall back. Do not delete or repair the file |
**On the `phases: []` ambiguity.** The `1.0` contract has no diagnostic channel,
so it cannot tell you *why* the list is empty. If your surface needs that
distinction, [`planning inspect`](consume-the-planning-snapshot.md) is the
surface that carries it — it reports per-document scope and coded diagnostics.
The rule of thumb: `state.json` is for *"show me where this project is"*;
`planning inspect` is for *"tell me exactly what is and is not knowable."*
## 5. Stay fresh
The file changes only when MSD reaches a step boundary, so polling it hard buys
you nothing. Watch it instead — `fs.watch`, `chokidar`, or your editor's own file
watcher — and re-read on change. Debounce briefly: a single boundary command
produces one write, but a workflow may cross several boundaries in quick
succession.
## Troubleshooting
**The file never appears.** Confirm `.planning/` exists in the directory you are
watching. MSD deliberately does **not** create `.planning/` in order to publish —
a directory that is not a MSD project stays untouched. Then run any boundary
command (`msd-tools phase complete 1`, say) and check again.
**The file appears somewhere I did not expect.** You are probably in a workstream
project; see the path in step 1. `MSD_WORKSTREAM` selects which planning root is
current.
**A phase I can see in `ROADMAP.md` is missing from `phases`.** Phases numbered
`0.x` and `999.x` are sentinels — backlog and icebox — and are excluded by
design, consistently with every other MSD surface. A row whose `Phase` cell does
not begin with a digit is also skipped.
**`phases` disagrees with what the roadmap shows.** It should not: phase status
is read from the roadmap's `## Progress` table, the same source MSD's own
progress counters use. If the roadmap has no `## Progress` table, the `## Phases`
checkbox list is used instead, and a checkbox can only say complete or not — the
in-progress phase is identified from `STATE.md`. Regenerating the progress table
resolves most disagreements.
**Should I commit `state.json`?** Your call. MSD does not add it to
`.gitignore`. It is a derived cache — safe to delete, regenerated at the next
boundary — so committing it mostly creates merge noise. If you keep `.planning/`
out of your repo entirely, see
[Keep planning docs out of a shared repo](keep-planning-docs-private.md).
## Related
- [Consume the planning snapshot](consume-the-planning-snapshot.md) — the richer,
diagnostic-carrying, pull-based surface
- [Features](../FEATURES.md) — why this contract is shaped the way it is
- [Work in parallel with workstreams](work-in-parallel-with-workstreams.md)