enhance(#3873): the STATE.md schema — one owner, generated artifacts (#3880)

* test(#3873): failing-first locale parity, plus tripwires for what must not move

Pins ADR-3473 §8.8 at the artifact a reader actually sees. The English STATE.md
reference carries a Status lifecycle section that is missing from all four
translations — the section documenting the status enum whose clobbering is
#3853. The test derives the heading set rather than hard-coding the missing
one, and names the locale and the heading when it fails.

Two tripwires that must pass today and after. The field-drift guard still
catches a re-derived fallback ladder: §8.8 instructs deleting that script, and
that instruction rests on a wrong premise about what it guards, so the test
stops a future reader from deleting it on the ADR's word. And last_activity's
label resolution is pinned to what ships today, because it is declared in one
of the two tables this phase consolidates and not the other — the
consolidation must not silently pick a side.

The locale test buckets under docs rather than state, which is what it tests;
that bucket is allowlisted with justification rather than folded into an
unrelated docs suite. It reads only markdown, so it carries no allow-test-rule
marker — a marker there would suppress nothing and would grow the unverified
pool against its ceiling.

Refs #3873

* feat(#3873): one schema owns the STATE.md key set, three tables become projections

ADR-3473 §8.8. The key set was declared in four places that had to agree by
hand and already did not: FIELD_CLASSIFICATION, FRONTMATTER_BODY_SOURCE,
FRONTMATTER_KEY_TO_BODY_LABEL and buildStateFrontmatter's emit behavior. One
frozen null-prototype schema now declares each key's type, enum, cardinality,
source, preservation, body source, body label, accepted parse shapes and
whether it is emitted unconditionally; the three tables are derived from it at
module load.

The projections are byte-identical to the literals they replace, key order
included, and the parity tests compare against verbatim copies of today's
tables rather than re-deriving both sides from the schema — a parity test fed
from one source proves nothing, which is how a consolidation ships a changed
policy under a green test.

last_activity was the live disagreement: present in one table, absent from the
other. The schema declares what ships today rather than the tidier answer, and
a test pins it.

The schema is a leaf module and owns the four field-policy types, re-exported
from state-transition so existing importers are untouched — the same split
health-diagnostic-types made to break a CJS require cycle.

Refs #3873

* feat(#3873): generate the schema-derived regions, parity-check the prose tables

ADR-3473 §8.8's generator half. gen-state-md-docs.cjs owns marked regions in
the shipped template and all five reference docs, follows gen-features.cjs's
fail-closed contract, and is wired into regen:derived and lint:generated-sync.

The Status lifecycle section was missing from all four translations — the
section documenting the status enum behind #3853 — and is now generated into
every locale. Field cardinality is a new generated table: pure schema data,
no prose, so nothing to lose.

The Field-reference and Status-values tables are parity-CHECKED rather than
generated. Their Purpose, When-populated and Matched-text columns are
genuinely hand-translated per locale, and §8.8 itself says prose stays
hand-translated; generating them from an English registry would overwrite four
locales' translations on every write. The row set is checked against the schema
instead, so a key added to one and not the other fails, which is what field
drift actually means. Building that check found last_activity_desc
undocumented in all five tables.

Three keys the docs describe are absent from the schema — active_phase,
next_action, next_phases. They are grandfathered by name, not by wildcard, so a
fourth fails: a declared gap with a forcing function rather than a silent one.

Refs #3873

* fix(#3873): declare what the parsers do, and close the shape-parity gap

Two declarations in the new schema described intended behavior rather than
actual — the defect class this epic exists to end, committed inside the epic.
Both were caught by executing the parsers instead of reading their docstrings.

current_plan.acceptedShapes claimed ['N', 'N of M']. Standalone, the hybrid
shape errors; the path that looks like support is parseInt truncating '2 of 5'
to 2 and discarding the rest. Narrowed to ['N']. The parser is deliberately NOT
fixed here: that is #3784 and PR #3791 is already doing it. When #3791 lands
this row must widen, and the shape test will go red until it does — the schema
and the parser cannot drift apart quietly, which is what §8.8's checked-not-
generated rule is for.

STATUS_LIFECYCLE_ENUM claimed to be the closed set status can hold.
normalizeStateStatus passes unrecognized prose through unchanged, so it is not
closed at runtime. The seven members are the canonical values it maps onto; the
docstring now says that and the test asserts the real lenient contract.

Closes the acceptance item that a test asserts the parsers accept exactly the
declared shapes: the check is table-driven over every row carrying
acceptedShapes, guarded against passing vacuously on an empty set, and fails
loudly if a future row has no registered driver. Adds the unwired-label throw
and the fast-check property that every projection agrees with its schema row.

Refs #3873

* fix(#3873): keep the shipped template's frontmatter first, and make row 27 able to fail

The remote matrix caught 12 failures with one cause. Making the template's
frontmatter a generated region wrapped it in its own yaml fence ahead of the
markdown fence, so extractFileTemplate and readShippedStateTemplateBody — which
both match the single markdown block — found the heading first, not the
frontmatter. That breaks the contract every new project's STATE.md is created
from: bug #21 and epic #1969 B8 pin that the File Template block starts with
frontmatter and carries gsd_state_version.

The markers now sit inside the single markdown fence, so the fence opens before
the frontmatter and the region still ends ahead of the heading. Same layout as
before this phase, with markers embedded rather than a second fence.

Row 27 existed to catch exactly this and did not, because it was writer-seeded:
it asserted against the generator's own output shape, so it passed on the broken
template. It now parses the fence the way production does and was verified to
fail against the broken shape before being trusted against the fixed one. A test
that would not have caught the bug it exists to prevent is worse than no test.

The emitted-attribution failure was separate and the fragment was the wrong
remedy: gsd-core/templates/state.md self-attributes under a verbatim-copy
identity rule, so a diff touching it needs no acknowledgment. Fragment deleted
rather than left explaining nothing.

Refs #3873

* docs(#3873): how to change the STATE.md schema

The phase gate was right and my docs artifact was wrong. I listed
lint:generated-sync as the second enablement step, which is a verification
command dressed as one, and then claimed a one-step sequence owed no how-to.

The real sequence is build:lib then regen:derived, and the ordering is a trap:
the generator reads the COMPILED schema, so regenerating before building
regenerates against the previous schema and commits artifacts that look
plausible while disagreeing with the code just written. A reference table
cannot carry an ordering dependency; that is what the how-to test is for.

The page covers adding, changing and removing a key, every reason code the
check emits and what to do about each, what is generated versus hand-translated
and why the two prose-bearing tables are parity-checked instead of generated,
adding a language, and the three grandfathered keys. Indexed from docs/README.md.

Refs #3873

* chore(#3873): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-26 01:57:47 -04:00
committed by GitHub
parent 3b18eff388
commit ddde001af6
27 changed files with 2654 additions and 101 deletions

View File

@@ -508,6 +508,7 @@
"state-contract.cjs",
"state-document.cjs",
"state-io.cjs",
"state-md-schema.cjs",
"state-transition.cjs",
"state.cjs",
"surface.cjs",

View File

@@ -639,6 +639,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `health-diagnostic-rules/state-consistency.cjs` | Health-diagnostic rules: STATE.md consistency checks (W002, W011, W021, W026) against config/ROADMAP/disk, ported behavior-preserving from `cmdValidateHealth`; W024 (state_head freshness) is a documented gap, deliberately not migrated (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
| `state-io.cjs` | Abstracts state IO modes — filesystem, sandboxed storage, or session log (#1680) |
| `state-transition.cjs` | Implements the STATE.md field-classification table and the pure `transitionCore` mutation path (#1769) |
| `state-md-schema.cjs` | The STATE.md field schema (compiled from `src/state-md-schema.cts`, gitignored; ADR-3473 §8.8, #3873) — one frozen, null-prototype `STATE_FIELD_SCHEMA` row per STATE.md key (type, cardinality, source, preservation, guard, mergeStrategy, body source/label, accepted value shapes, emitted-vs-guarded), replacing three previously hand-maintained tables (`FIELD_CLASSIFICATION` / `FRONTMATTER_BODY_SOURCE` in `state-transition.cjs`, `FRONTMATTER_KEY_TO_BODY_LABEL` in `state.cjs`) that now project from it at load time with byte-identical shape/order. Leaf module: imports from neither of its two consumers, avoiding the CJS require-cycle `health-diagnostic-types.cjs` was split out to break |
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
| `state-document.cjs` | Pure STATE.md field extraction, replacement, status normalization, and progress calculation transforms |
| `milestone-lock.cjs` | Milestone lock (compiled from `src/milestone-lock.cts`, gitignored) — advisory (phase, session id) claim over STATE.md's single Current Position slot: `.planning/milestone.lock` claim IO, liveness (TTL + heartbeat), conflict detection, and the shared stderr warning; consumed by `state.begin-phase` / `state.advance-plan` / `phase.complete` so parallel phases in one working tree get a visible conflict instead of silently overwriting each other (#3311) |

View File

@@ -25,6 +25,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
- [Probe edges in a non-English project](how-to/probe-edges-in-a-non-english-project.md) — get real edge coverage on a spec written in another language, and tell "no edges here" apart from "the probe could not read it"
- [Resolve prohibition findings](how-to/resolve-prohibition-findings.md) — turn the spec phase's surfaced must-NOT constraints into resolved, dismissed, or deferred spec decisions
- [Resolve an unreachable-workflow finding](how-to/resolve-unreachable-workflow-findings.md) — wire or fully sweep a shipped workflow that no command, agent, or skill references
- [Change the STATE.md schema](how-to/change-the-state-md-schema.md) — add, change or remove a STATE.md frontmatter key and keep the template and all five reference documents in step
- [Resolve verify-command path findings](how-to/resolve-verify-command-path-findings.md) — fix an `<automated>` verify command whose target directory does not resolve from the executor's cwd
- [State a failing direction](how-to/state-a-failing-direction.md) — say what output constitutes failure for an `<automated>` verify command, and migrate a phase planned before the rule
- [Resolve a contract-drift finding](how-to/resolve-contract-drift-findings.md) — bring an agent's completion contract, read-tag gate, or deleted-file test reference back into agreement with the registry

View File

@@ -240,7 +240,13 @@ Both close #3349 and #3360, which are **read-side** defects a real parser fixes
**Rule — parsers are checked, not generated.** Parse functions stay hand-written. A test asserts they accept **exactly** the shapes the schema declares — no more, no fewer. Generating a parser is a substrate decision this epic does not take; the shape-proliferation family (#3784's three spellings of "plan N of M") is closed by declaring the accepted set, not by emitting the matcher.
**Consequence for the guard.** `scripts/lint-state-field-drift.cjs` (805 lines) is **deleted** in the same PR. Field drift between the table, the template and the docs stops being detectable because it stops being representable.
**Consequence for the guard.** Field drift between the schema, the template and the docs stops being *representable*, because the artifacts are generated from the schema and a drift check refuses a stale one.
> **Amendment, 2026-08-26 (Phase 3, #3873) — this paragraph originally instructed deleting `scripts/lint-state-field-drift.cjs` (805 lines) on the grounds that it detected that drift. Verified against `next`: it does not, and never did.** That script's own header declares it the drift guard for the **STATE.md field-extraction fallback chain** — epic #3180, issue #3187, ADR-3180 Decision 4 §7.7. It detects re-derivations of the *"prefer the frontmatter scalar, else fall back to the body field"* coercion ladder across `src/**` and the prompt layer. It contains no reference to `FIELD_CLASSIFICATION`, to the template, or to the reference docs. Nor does any **other** script own field/template/docs drift — the full `scripts/` inventory carries none. The instruction named a guard surface that did not exist.
>
> **The guard is therefore retained**, and `tests/lint-state-field-drift-retained.test.cjs` pins that decision so a future reader does not delete it on this ADR's earlier word. A key-set schema makes a *key-set disagreement* unrepresentable; it does nothing about a *code-shape* re-derivation of a coercion ladder. Those are orthogonal, and Decision 6 sanctions retiring a guard the change makes **redundant** — which this one is not.
>
> **This is the second guard-retirement claim in this ADR to rest on a wrong premise**, after §8.6's (see its own amendment). Both described a guard surface their author believed existed. The pattern is worth naming: a retirement claim in this ADR is a *hypothesis about a guard's contents*, and Decision 6's ledger requirement should be read as obliging the implementing phase to verify that hypothesis before acting on it — not merely to count the result.
#### 8.9 Each subsumed child is driven fail-first — *Required — every phase*
@@ -261,8 +267,9 @@ Both close #3349 and #3360, which are **read-side** defects a real parser fixes
| Phase | Guard | Δ |
|---|---|---|
| 1 (§8.6) | `lint-state-write-path-drift.cjs` — two `sanctioned-permanent` baseline entries retired; raw-write check retained | shrinks |
| 3 (§8.8) | `lint-state-field-drift.cjs` — **deleted** | **−805 lines, −1 guard** |
| 1 (§8.6) | `lint-state-write-path-drift.cjs` — seam-bypass `writeStateMd(` arm and its whole ratchet apparatus retired, baseline file deleted; composition-bypass arm retained and made terminal; raw-write check **added net-new** | **−665 lines, −1 file, +1 check** — net shrink |
| 2 (§8.7) | none — the reporting diff makes no guard redundant | **0**, stated rather than omitted |
| 3 (§8.8) | `lint-state-field-drift.cjs` — **retained**, see §8.8's amendment; a generated-artifact drift check and a locale-parity check are **added** | **+2 checks, 0 retired — this phase GROWS** |
| — | `lint-planning-snapshot-bypass-drift.cjs` — extended to the write side by ADR-3180 Amendment 8 | **growth, declared** |
Net across the set: one guard retired, one increase recorded honestly. The increase belongs to a concurrent lane under a different ADR and is listed here so the accounting is complete rather than flattering.
@@ -297,7 +304,7 @@ Net across the set: one guard retired, one increase recorded honestly. The incre
| Guard | Status under this ADR |
|---|---|
| `scripts/lint-state-write-path-drift.cjs` | retained, shrunk (§8.6) — seam-bypass `writeStateMd(` arm and its ratchet retired at Phase 1; composition-bypass arm retained and made terminal; raw-write check added net-new. See §8.6's amendment. |
| `scripts/lint-state-field-drift.cjs` | **retired at Phase 3** (§8.8) |
| `scripts/lint-state-field-drift.cjs` | **RETAINED** — the Phase-3 retirement instruction rested on a wrong premise about what this guard does; see §8.8's amendment. It guards the ADR-3180 §7.7 / #3187 coercion ladder, which no schema makes unrepresentable. |
| `scripts/lint-vendored-deps.cjs` | reused as-is for §8.1's vendoring rule |
| `local/no-external-require-in-bin` | reused as-is; enforces §8.1's packaging rule |
| `local/no-adhoc-markdown-parsing` | widened past `src/**/*.cts` per Decision 5 (coverage fix, tracked on #3426/#3239) |

View File

@@ -0,0 +1,129 @@
# 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*
`gsd-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*:
- `gsd-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.

View File

@@ -74,9 +74,35 @@ paused_at: null
| `last_updated` | ISO-8601 タイムスタンプ | 書き込み時に常時 | 最後の `syncStateFrontmatter` 呼び出しのタイムスタンプ。`realClock.nowIso()` によって書き込まれる。 |
| `state_head` | string (40-char sha) | On write, when the project's own git repo resolves | Full commit sha STATE.md was written against (#2573). Omitted entirely outside a git repo, or when the resolved repo is not the project's own — an unverifiable stamp degrades to absent rather than asserting provenance the file does not have. Recomputed on every write and never carried forward. |
| `last_activity` | string | 本文に設定されている場合 | 本文の `Last Activity:` フィールドから抽出した最終活動日。 |
| `last_activity_desc` | string | 本文に設定されている場合 | 本文の `Last Activity Description:` フィールドから抽出した最終活動の説明。 |
| `stopped_at` | string | 停止ポイントが記録された場合 | 最後に完了したアクションの説明。アーカイブの文章とのマッチを避けるため `## Session` 本文セクションにスコープを限定。 |
| `paused_at` | string | プロジェクトが一時停止中の場合 | 一時停止ポイントの自由形式の説明。一時停止していない場合は省略または `null`。 |
<!-- STATE-MD-SCHEMA:START:cardinality — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### フィールドの多重度
| フィールド | 多重度 |
|---|---|
| `gsd_state_version` | one |
| `milestone` | optional |
| `milestone_name` | optional |
| `current_phase` | optional |
| `current_phase_name` | optional |
| `current_plan` | optional |
| `status` | one |
| `stopped_at` | optional |
| `paused_at` | optional |
| `last_updated` | one |
| `last_activity` | optional |
| `last_activity_desc` | optional |
| `state_head` | optional |
| `progress.total_phases` | optional |
| `progress.completed_phases` | optional |
| `progress.total_plans` | optional |
| `progress.completed_plans` | optional |
| `progress.percent` | optional |
<!-- STATE-MD-SCHEMA:END:cardinality -->
### ステータス値
`gsd-core/bin/lib/state-document.cjs` の `normalizeStateStatus()` が本文の生テキストを以下の正規値にマッピングします:
@@ -100,6 +126,21 @@ paused_at: null
| `/gsd-execute-phase` | `executing` |
| `/gsd-verify-work` | `verifying` |
<!-- STATE-MD-SCHEMA:START:status-lifecycle — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### ステータスライフサイクル (ADR-2207)
The `Status` field follows a strict lifecycle across phase and milestone boundaries:
| 値 | 書き込み元 | 意味 |
|---|---|---|
| `Ready to plan` | `completePhaseCore` (non-last phase) | Next phase is ready for planning |
| `All phases complete` | `completePhaseCore` (last phase) | All phases done; milestone awaiting formal close |
| `<version> milestone complete` | `milestoneCompleteCore` | Milestone formally closed and archived |
| `Awaiting next milestone` | `milestoneCompleteCore` | Terminal/archived state |
Phase-completion verbs never write `Milestone complete` (the overloaded bare value was removed in #2204 per ADR-2207 to decouple phase-level writes from milestone termination).
<!-- STATE-MD-SCHEMA:END:status-lifecycle -->
---
## ステータスライン描画シーン

View File

@@ -74,9 +74,35 @@ paused_at: null
| `last_updated` | ISO-8601 타임스탬프 | 항상 (쓰기 시) | 마지막 `syncStateFrontmatter` 호출의 타임스탬프. `realClock.nowIso()`에 의해 기록됩니다. |
| `state_head` | string (40-char sha) | On write, when the project's own git repo resolves | Full commit sha STATE.md was written against (#2573). Omitted entirely outside a git repo, or when the resolved repo is not the project's own — an unverifiable stamp degrades to absent rather than asserting provenance the file does not have. Recomputed on every write and never carried forward. |
| `last_activity` | string | 본문에 설정된 경우 | 본문 `Last Activity:` 필드에서 추출된 마지막 활동 날짜. |
| `last_activity_desc` | string | 본문에 설정된 경우 | 본문 `Last Activity Description:` 필드에서 추출된 마지막 활동 설명. |
| `stopped_at` | string | 중단점이 기록된 경우 | 마지막으로 완료된 작업의 설명. 아카이브 산문과의 매칭을 피하기 위해 `## Session` 본문 섹션으로 범위가 제한됩니다. |
| `paused_at` | string | 프로젝트가 일시 정지된 경우 | 일시 정지 지점에 대한 자유형 설명. 일시 정지 상태가 아닐 때는 없거나 `null`. |
<!-- STATE-MD-SCHEMA:START:cardinality — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### 필드 카디널리티
| 필드 | 카디널리티 |
|---|---|
| `gsd_state_version` | one |
| `milestone` | optional |
| `milestone_name` | optional |
| `current_phase` | optional |
| `current_phase_name` | optional |
| `current_plan` | optional |
| `status` | one |
| `stopped_at` | optional |
| `paused_at` | optional |
| `last_updated` | one |
| `last_activity` | optional |
| `last_activity_desc` | optional |
| `state_head` | optional |
| `progress.total_phases` | optional |
| `progress.completed_phases` | optional |
| `progress.total_plans` | optional |
| `progress.completed_plans` | optional |
| `progress.percent` | optional |
<!-- STATE-MD-SCHEMA:END:cardinality -->
### 상태 값
`gsd-core/bin/lib/state-document.cjs`의 `normalizeStateStatus()`는 본문의 원시 텍스트를 다음 표준 값으로 매핑합니다:
@@ -100,6 +126,21 @@ paused_at: null
| `/gsd-execute-phase` | `executing` |
| `/gsd-verify-work` | `verifying` |
<!-- STATE-MD-SCHEMA:START:status-lifecycle — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### 상태 라이프사이클 (ADR-2207)
The `Status` field follows a strict lifecycle across phase and milestone boundaries:
| 값 | 작성자 | 의미 |
|---|---|---|
| `Ready to plan` | `completePhaseCore` (non-last phase) | Next phase is ready for planning |
| `All phases complete` | `completePhaseCore` (last phase) | All phases done; milestone awaiting formal close |
| `<version> milestone complete` | `milestoneCompleteCore` | Milestone formally closed and archived |
| `Awaiting next milestone` | `milestoneCompleteCore` | Terminal/archived state |
Phase-completion verbs never write `Milestone complete` (the overloaded bare value was removed in #2204 per ADR-2207 to decouple phase-level writes from milestone termination).
<!-- STATE-MD-SCHEMA:END:status-lifecycle -->
---
## 상태 표시줄 렌더링 장면

View File

@@ -74,9 +74,35 @@ paused_at: null
| `last_updated` | timestamp ISO-8601 | Sempre (na escrita) | Timestamp da última chamada a `syncStateFrontmatter`; escrito por `realClock.nowIso()`. |
| `state_head` | string (40-char sha) | On write, when the project's own git repo resolves | Full commit sha STATE.md was written against (#2573). Omitted entirely outside a git repo, or when the resolved repo is not the project's own — an unverifiable stamp degrades to absent rather than asserting provenance the file does not have. Recomputed on every write and never carried forward. |
| `last_activity` | string | Quando definido no corpo | Data da última atividade, extraída do campo `Last Activity:` do corpo. |
| `last_activity_desc` | string | Quando definido no corpo | Descrição da última atividade, extraída do campo `Last Activity Description:` do corpo. |
| `stopped_at` | string | Quando um ponto de parada foi registrado | Descrição da última ação concluída; limitada à seção `## Session` do corpo para evitar correspondência com prosa de arquivo. |
| `paused_at` | string | Quando o projeto está pausado | Descrição de forma livre do ponto de pausa; ausente ou `null` quando não pausado. |
<!-- STATE-MD-SCHEMA:START:cardinality — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### Cardinalidade dos campos
| Campo | Cardinalidade |
|---|---|
| `gsd_state_version` | one |
| `milestone` | optional |
| `milestone_name` | optional |
| `current_phase` | optional |
| `current_phase_name` | optional |
| `current_plan` | optional |
| `status` | one |
| `stopped_at` | optional |
| `paused_at` | optional |
| `last_updated` | one |
| `last_activity` | optional |
| `last_activity_desc` | optional |
| `state_head` | optional |
| `progress.total_phases` | optional |
| `progress.completed_phases` | optional |
| `progress.total_plans` | optional |
| `progress.completed_plans` | optional |
| `progress.percent` | optional |
<!-- STATE-MD-SCHEMA:END:cardinality -->
### Valores de status
`normalizeStateStatus()` em `gsd-core/bin/lib/state-document.cjs` mapeia o texto bruto do corpo para estes valores canônicos:
@@ -100,6 +126,21 @@ Quando um comando do orquestrador está em andamento, a convenção (issue #2833
| `/gsd-execute-phase` | `executing` |
| `/gsd-verify-work` | `verifying` |
<!-- STATE-MD-SCHEMA:START:status-lifecycle — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### Ciclo de vida do status (ADR-2207)
The `Status` field follows a strict lifecycle across phase and milestone boundaries:
| Valor | Escrito por | Significado |
|---|---|---|
| `Ready to plan` | `completePhaseCore` (non-last phase) | Next phase is ready for planning |
| `All phases complete` | `completePhaseCore` (last phase) | All phases done; milestone awaiting formal close |
| `<version> milestone complete` | `milestoneCompleteCore` | Milestone formally closed and archived |
| `Awaiting next milestone` | `milestoneCompleteCore` | Terminal/archived state |
Phase-completion verbs never write `Milestone complete` (the overloaded bare value was removed in #2204 per ADR-2207 to decouple phase-level writes from milestone termination).
<!-- STATE-MD-SCHEMA:END:status-lifecycle -->
---
## Cenas de renderização da linha de status

View File

@@ -74,9 +74,35 @@ paused_at: null
| `last_updated` | ISO-8601 timestamp | Always (on write) | Timestamp of the last `syncStateFrontmatter` call; written by `realClock.nowIso()`. |
| `state_head` | string (40-char sha) | On write, when the project's own git repo resolves | Full commit sha STATE.md was written against (#2573). Omitted entirely outside a git repo, when the resolved repo is not the project's own, or in a `planning.sub_repos` workspace — an unverifiable stamp degrades to absent rather than asserting provenance the file does not have. Recomputed on every write and never carried forward. |
| `last_activity` | string | When set in body | Date of the last activity, extracted from the body `Last Activity:` field. |
| `last_activity_desc` | string | When set in body | Description of the last activity, extracted from the body `Last Activity Description:` field. |
| `stopped_at` | string | When a stop point was recorded | Description of the last completed action; scoped to the `## Session` body section to avoid matching archive prose. |
| `paused_at` | string | When the project is paused | Freeform description of the pause point; absent or `null` when not paused. |
<!-- STATE-MD-SCHEMA:START:cardinality — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### Field cardinality
| Field | Cardinality |
|---|---|
| `gsd_state_version` | one |
| `milestone` | optional |
| `milestone_name` | optional |
| `current_phase` | optional |
| `current_phase_name` | optional |
| `current_plan` | optional |
| `status` | one |
| `stopped_at` | optional |
| `paused_at` | optional |
| `last_updated` | one |
| `last_activity` | optional |
| `last_activity_desc` | optional |
| `state_head` | optional |
| `progress.total_phases` | optional |
| `progress.completed_phases` | optional |
| `progress.total_plans` | optional |
| `progress.completed_plans` | optional |
| `progress.percent` | optional |
<!-- STATE-MD-SCHEMA:END:cardinality -->
> **Known limitation — multi-repo workspaces.** In a workspace configured with
> [`planning.sub_repos`](../CONFIGURATION.md#planning), the freshness hint reports *unknown*
> rather than a commit age, and `state_head` is omitted. The outer workspace can own both
@@ -109,6 +135,7 @@ When an orchestrator command is in flight, the convention (issue #2833) is to wr
| `/gsd-execute-phase` | `executing` |
| `/gsd-verify-work` | `verifying` |
<!-- STATE-MD-SCHEMA:START:status-lifecycle — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### Status lifecycle (ADR-2207)
The `Status` field follows a strict lifecycle across phase and milestone boundaries:
@@ -121,6 +148,7 @@ The `Status` field follows a strict lifecycle across phase and milestone boundar
| `Awaiting next milestone` | `milestoneCompleteCore` | Terminal/archived state |
Phase-completion verbs never write `Milestone complete` (the overloaded bare value was removed in #2204 per ADR-2207 to decouple phase-level writes from milestone termination).
<!-- STATE-MD-SCHEMA:END:status-lifecycle -->
---

View File

@@ -74,9 +74,35 @@ paused_at: null
| `last_updated` | ISO-8601 时间戳 | 始终(写入时) | 最后一次 `syncStateFrontmatter` 调用的时间戳;由 `realClock.nowIso()` 写入。 |
| `state_head` | string (40-char sha) | On write, when the project's own git repo resolves | Full commit sha STATE.md was written against (#2573). Omitted entirely outside a git repo, or when the resolved repo is not the project's own — an unverifiable stamp degrades to absent rather than asserting provenance the file does not have. Recomputed on every write and never carried forward. |
| `last_activity` | 字符串 | 正文中设置时 | 最后活动日期,从正文 `Last Activity:` 字段提取。 |
| `last_activity_desc` | 字符串 | 正文中设置时 | 最后活动描述,从正文 `Last Activity Description:` 字段提取。 |
| `stopped_at` | 字符串 | 记录了停止点时 | 最后完成操作的描述;限定在 `## Session` 正文章节内,以避免匹配存档文本。 |
| `paused_at` | 字符串 | 项目已暂停时 | 暂停点的自由描述;未暂停时缺失或为 `null`。 |
<!-- STATE-MD-SCHEMA:START:cardinality — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### 字段基数
| 字段 | 基数 |
|---|---|
| `gsd_state_version` | one |
| `milestone` | optional |
| `milestone_name` | optional |
| `current_phase` | optional |
| `current_phase_name` | optional |
| `current_plan` | optional |
| `status` | one |
| `stopped_at` | optional |
| `paused_at` | optional |
| `last_updated` | one |
| `last_activity` | optional |
| `last_activity_desc` | optional |
| `state_head` | optional |
| `progress.total_phases` | optional |
| `progress.completed_phases` | optional |
| `progress.total_plans` | optional |
| `progress.completed_plans` | optional |
| `progress.percent` | optional |
<!-- STATE-MD-SCHEMA:END:cardinality -->
### 状态值
`gsd-core/bin/lib/state-document.cjs` 中的 `normalizeStateStatus()` 将原始正文文本映射到以下规范值:
@@ -100,6 +126,21 @@ paused_at: null
| `/gsd-execute-phase` | `executing` |
| `/gsd-verify-work` | `verifying` |
<!-- STATE-MD-SCHEMA:START:status-lifecycle — generated by scripts/gen-state-md-docs.cjs from src/state-md-schema.cts; do not edit by hand -->
### 状态生命周期 (ADR-2207)
The `Status` field follows a strict lifecycle across phase and milestone boundaries:
| 值 | 写入方 | 含义 |
|---|---|---|
| `Ready to plan` | `completePhaseCore` (non-last phase) | Next phase is ready for planning |
| `All phases complete` | `completePhaseCore` (last phase) | All phases done; milestone awaiting formal close |
| `<version> milestone complete` | `milestoneCompleteCore` | Milestone formally closed and archived |
| `Awaiting next milestone` | `milestoneCompleteCore` | Terminal/archived state |
Phase-completion verbs never write `Milestone complete` (the overloaded bare value was removed in #2204 per ADR-2207 to decouple phase-level writes from milestone termination).
<!-- STATE-MD-SCHEMA:END:status-lifecycle -->
---
## 状态行渲染场景