* 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>
17 KiB
STATE.md スキーマリファレンス
STATE.md は GSD Core のプロジェクト記憶ファイルです — プロジェクトの現在地、直近の作業内容、次に実行すべきコマンドを記録する単一の Markdown ドキュメントです。このページではそのスキーマを説明します。ドキュメントインデックス も参照してください。
概要
GSD Core で管理するプロジェクトはすべて .planning/STATE.md に STATE.md を1つ保持します。このファイルはすべてのワークフロー開始時に読み込まれ、重要なアクションのたびに書き込まれます。ファイルは以下を組み合わせた構成です:
- YAML フロントマター — ステータスラインフック(
parseStateMd)およびgsd-tools stateコマンドが読み取る機械可読フィールド。 - Markdown 本文 — 現在の進捗、蓄積されたコンテキスト、セッション継続性、パフォーマンス指標を記述する人間可読なセクション。
ファイルは意図的にコンパクトに保たれています(目標: 100行以内)。プロジェクト状態のダイジェストであり、アーカイブではありません。
YAML フロントマター
フロントマターはファイルの先頭にある --- デリミタの間に記述します。gsd_state_version と status 以外のフィールドはすべてオプションで、データが未取得の場合は省略できます。
注釈付きサンプル
---
gsd_state_version: '1.0'
milestone: v2.0
milestone_name: Code Quality
status: executing
# フェーズライフサイクルフィールド — すべてオプション (v1.40.0, issue #2833 で追加)
active_phase: "4.5"
next_action: execute-phase
next_phases: ["4.5"]
progress:
total_phases: 17
completed_phases: 10
total_plans: 84
completed_plans: 47
percent: 59
# syncStateFrontmatter が書き込む追加フィールド
current_phase: "4"
current_phase_name: Observability
current_plan: "3"
last_updated: "2026-06-01T12:34:56.789Z"
state_head: 4f3c2b1a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b
last_activity: "2026-06-01"
stopped_at: "Phase 4 P3 execution complete"
paused_at: null
---
フィールドリファレンス
| フィールド | 型 | 設定タイミング | 用途 |
|---|---|---|---|
gsd_state_version |
string ('1.0') |
常時 | スキーマバージョン。syncStateFrontmatter が最初の state.* 呼び出し時に書き込む。 |
milestone |
string (例: v2.0) |
マイルストーン設定時 | 現在のマイルストーンバージョン。プロジェクト設定から読み取る。 |
milestone_name |
string | マイルストーン設定時 | 人間可読なマイルストーンラベル (例: Code Quality)。 |
status |
string | 常時 | 現在のライフサイクルステージ。normalizeStateStatus() によって正規化される — ステータス値 参照。 |
active_phase |
string (例: "4.5") |
このフェーズでオーケストレーターコマンドが実行中の場合 | 現在処理中のフェーズ番号。フェーズ間は null に設定。 |
next_action |
string | アイドル中で推奨コマンドがある場合 | 次に実行するスラッシュコマンド: discuss-phase、plan-phase、execute-phase、verify-phase。オーケストレーターが実行中または推奨なしの場合は null に設定。 |
next_phases |
YAML フロー配列 (例: ["4.5"]) |
next_action と対応して |
next_action が適用されるフェーズ ID(通常1〜2件)。next_action と同条件で null に設定。 |
progress.total_phases |
integer | フェーズデータ取得済みの場合 | 現在のマイルストーンにおける総フェーズ数。ROADMAP.md とフェーズディレクトリから算出。 |
progress.completed_phases |
integer | フェーズデータ取得済みの場合 | すべてのプランサマリーがディスク上に存在するフェーズ数(すなわちすべてのプランが完了したもの)。 |
progress.total_plans |
integer | プランファイルが存在する場合 | 現在のマイルストーンの全フェーズにわたるプランファイルの合計数。 |
progress.completed_plans |
integer | サマリーファイルが存在する場合 | 完了したプランサマリーの合計数(実行済みプランごとに1つの SUMMARY.md)。 |
progress.percent |
integer 0–100 | 進捗データ取得済みの場合 | フェーズ次元 でのマイルストーン進捗(min(completed_plans/total_plans, completed_phases/total_phases))。このフィールドが存在するときのみステータスラインの進捗バーが描画されます — 不在の場合はバーが非表示になります。 |
current_phase |
string | フェーズ実行中 | 本文の Current Phase: フィールドから抽出したフェーズ番号。 |
current_phase_name |
string | フェーズに名前がある場合 | 本文の Current Phase Name: フィールドから抽出したフェーズ名。 |
current_plan |
string | プランが進行中の場合 | 本文の Current Plan: フィールドから抽出したプラン番号。 |
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。 |
フィールドの多重度
| フィールド | 多重度 |
|---|---|
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 |
ステータス値
gsd-core/bin/lib/state-document.cjs の normalizeStateStatus() が本文の生テキストを以下の正規値にマッピングします:
| 正規値 | マッチするテキスト(大文字小文字不問) |
|---|---|
discussing |
discussing を含む |
planning |
planning または ready to plan を含む |
executing |
executing、in progress、または ready to execute を含む |
verifying |
verif を含む |
completed |
complete または done を含む |
paused |
paused または stopped を含む、または paused_at が存在する |
unknown |
上記のいずれにも該当しない |
オーケストレーターコマンドが実行中の場合、慣例(issue #2833)として status にライフサイクルステージを直接書き込みます:
| コマンド | 実行中の status |
|---|---|
/gsd-discuss-phase |
discussing |
/gsd-plan-phase |
planning |
/gsd-execute-phase |
executing |
/gsd-verify-work |
verifying |
ステータスライフサイクル (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).
ステータスライン描画シーン
hooks/gsd-statusline.js の formatGsdState() がパース済みフロントマターを読み取り、最初にマッチしたシーン を出力します。新しいライフサイクルフィールドが適用されない場合は、v1.38.x から一切変更なくオリジナルのフォーマットにフォールスルーします。
| シーン | トリガー | 表示例 |
|---|---|---|
| 1. フェーズアクティブ | active_phase が設定されている |
v2.0 [██░░░░░░░░] 20% · Phase 4.5 executing |
| 2. アイドル・次のアクション推奨 | active_phase が null かつ next_action と next_phases の両方が設定されている |
v2.0 [██░░░░░░░░] 20% · next execute-phase 4.5 |
| 3. マイルストーン完了 | percent が 100 または completed_phases == total_phases |
v2.0 [██████████] 100% · milestone complete |
| 4. デフォルトフォールバック | 上記のいずれにも該当しない | v1.9 Code Quality · executing · ph 1/5(既存フォーマット) |
シーン優先度: active_phase と next_action が両方設定されている場合、シーン1が優先されます — オーケストレーターが実行中であるため「次の推奨」は誤解を招くためです。この優先度は formatGsdState() のチェック順序によって強制され、tests/gsd-statusline.test.cjs の "scene priority" スイートでカバーされています。
進捗バー([██░░░░░░░░] 20%)はフロントマターに progress.percent が存在する場合のみマイルストーンセグメントに追加されます。不在の場合はバーは表示されません。
フロントマターパースの制約
ステータスラインフックは正規表現ベースのパース(完全な YAML ライブラリを使用しない)を使用するため、以下の制約が適用されます。これらは tests/gsd-statusline.test.cjs でテストされています。
-
フロントマターはファイルの先頭文字から始まる必要があります。 コメントを含む何かが開始
---の前にあると、マッチが無効になります。開始---行は末尾のスペースなしで正確にそれだけである必要があります。 -
ネストされたブロック内のコメントはサポートされていません。
progress:ブロックパーサーは次の行が[ \t]+\w+:であることを要求します。progress:と最初のキーの間に# commentを挿入するとマッチが壊れてバーが消えます。ドキュメントはフロントマターブロック内ではなくSTATE.md本文に記載してください。 -
next_phasesの主形式は単一行フローです。 パーサーは最初にnext_phases: ["4.5", "4.6"]を試みます。ブロックシーケンス(- 4.5\n- 4.6)もパースされますが、ステータスライン描画の信頼性は低下します。正規表現ベースのパーサーを予測可能に保つため、next_phasesには単一行フローを使用してください。多数の候補フェーズをドキュメント目的で記録する必要がある場合はSTATE.md本文に格納してください。
将来的に正規表現パーサーを完全な YAML ライブラリに置き換えた場合は、これらの制約を緩和しテストを更新できます。
Markdown 本文セクション
本文(末尾の --- 以降のすべて)は gsd-core/templates/state.md のテンプレートに従います。標準セクションは以下の通りです:
Project Reference
.planning/PROJECT.md へのポインタです。以下を含みます:
- Core value —
PROJECT.mdの Core Value セクションの一行説明。 - Current focus — アクティブなフェーズ。
Current Position
プロジェクトの現在の状況:
| フィールド | フォーマット |
|---|---|
Phase: |
X of Y (Phase name) |
Plan: |
A of B in current phase |
Status: |
自由テキスト。例: Ready to execute、Executing Phase 4、Phase complete — ready for verification |
Last activity: |
ハンドラー書き込み時は ISO 日付(YYYY-MM-DD); エグゼキューター作成時はナラティブ文章 |
Progress: |
ビジュアルバー。例: [████░░░░░░] 40% |
このセクションの Status: および Last activity: フィールドは、既存の値が既知のテンプレートデフォルト値の場合に GSD ハンドラーによって更新されます(クヌース不変式: エグゼキューター作成値は保存されます)。既知のハンドラーデフォルト値の完全なリストは gsd-core/bin/lib/state-document.cjs の KNOWN_TEMPLATE_DEFAULTS に記載されています。
Performance Metrics
実行速度の追跡:
- 完了した総プラン数、プランあたりの平均所要時間。
- フェーズごとの内訳テーブル(
Phase | Plans | Total | Avg/Plan)。 - 最近のトレンド: Improving / Stable / Degrading。
各プラン完了後に更新されます。
Accumulated Context
Decisions — 現在の作業に影響する最近の意思決定のサマリー(完全なログは PROJECT.md に保存)。gsd-tools state add-decision で追加。
Pending Todos — 件数と .planning/todos/pending/ への参照。/gsd-capture で取得。
Blockers/Concerns — 今後の作業に影響する課題。起点フェーズのプレフィックス付き。gsd-tools state add-blocker で追加し、gsd-tools state resolve-blocker で解決。
Session Continuity
即座のセッション再開を可能にします:
Last session:— 最後のセッションの ISO-8601 タイムスタンプ。Stopped at:— 最後に完了したアクションの説明。Resume file:—.continue-here*.mdファイルが存在する場合はそのパス、存在しない場合はNone。
後方互換性
フェーズライフサイクルフィールド(active_phase、next_action、next_phases、バー用の progress.percent)は 追加式でプロジェクトごとにオプトイン です:
- ライフサイクルフィールドが一切設定されていない
STATE.mdは v1.38.x 以前と バイト単位で同一 に描画されます。 - ライフサイクルフィールドの追加はオプトインです — フィールドが不在の場合レンダラーはグレースフルに縮退します。
- 進捗バーは
progressブロックが存在する場合でもオプトインです: バーをトリガーするのはprogress.percentのみで、total_phasesとcompleted_phasesだけではトリガーされません。
tests/gsd-statusline.test.cjs の formatGsdState #2833 backward compatibility テストスイートがこの保証を固定しています。レガシー STATE.md 描画を壊す変更があればスイートが失敗します。