* 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>
15 KiB
Referência do esquema STATE.md
STATE.md é o arquivo de memória viva do projeto do GSD Core — um único documento Markdown que registra em que ponto o projeto se encontra, o que aconteceu por último e o que executar a seguir. Esta página documenta sua estrutura. Consulte o índice da documentação.
Visão geral
Todo projeto gerenciado pelo GSD Core mantém um único STATE.md em .planning/STATE.md. Ele é lido no início de todo fluxo de trabalho e escrito após toda ação significativa. O arquivo combina:
- Frontmatter YAML — campos legíveis por máquina consumidos pelo hook de linha de status (
parseStateMd) e pelos comandosgsd-tools state. - Corpo Markdown — seções legíveis por humanos cobrindo a posição atual, contexto acumulado, continuidade de sessão e métricas de desempenho.
O arquivo é intencionalmente pequeno (meta: menos de 100 linhas). Ele é um resumo do estado do projeto, não um arquivo histórico.
Frontmatter YAML
O frontmatter aparece entre delimitadores --- no início do arquivo. Todos os campos, exceto gsd_state_version e status, são opcionais; os campos podem estar ausentes quando seus dados ainda não estão disponíveis.
Exemplo comentado
---
gsd_state_version: '1.0'
milestone: v2.0
milestone_name: Code Quality
status: executing
# Campos de ciclo de vida de fase — todos opcionais (adicionados na 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
# Campos adicionais escritos por 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
---
Referência de campos
| Campo | Tipo | Quando populado | Finalidade |
|---|---|---|---|
gsd_state_version |
string ('1.0') |
Sempre | Versão do esquema; escrito na primeira chamada state.* por syncStateFrontmatter. |
milestone |
string (ex.: v2.0) |
Quando um milestone está configurado | Versão do milestone atual, lida da configuração do projeto. |
milestone_name |
string | Quando um milestone está configurado | Rótulo legível do milestone (ex.: Code Quality). |
status |
string | Sempre | Estágio atual do ciclo de vida. Normalizado por normalizeStateStatus() — veja valores de status. |
active_phase |
string (ex.: "4.5") |
Um comando do orquestrador está em andamento nesta fase | O número da fase atualmente sendo processada. Definido como null entre fases. |
next_action |
string | Ocioso, com um comando recomendado | O slash command a executar a seguir: discuss-phase, plan-phase, execute-phase ou verify-phase. Definido como null quando um orquestrador está em andamento ou nenhuma recomendação está disponível. |
next_phases |
array YAML flow (ex.: ["4.5"]) |
Acompanha next_action |
Os IDs de fase aos quais o next_action se aplica (tipicamente 1–2 entradas). Definido como null nas mesmas condições que next_action. |
progress.total_phases |
inteiro | Quando dados de fase estão disponíveis | Número total de fases no milestone atual, derivado do ROADMAP.md e do diretório de fases. |
progress.completed_phases |
inteiro | Quando dados de fase estão disponíveis | Número de fases que têm todos os resumos de planos em disco (ou seja, todos os planos concluídos). |
progress.total_plans |
inteiro | Quando arquivos de plano existem | Soma de todos os arquivos de plano nas fases do milestone atual. |
progress.completed_plans |
inteiro | Quando arquivos de resumo existem | Soma dos resumos de planos concluídos (um SUMMARY.md por plano executado). |
progress.percent |
inteiro 0–100 | Quando dados de progresso estão disponíveis | Progresso do milestone na dimensão de fases (min(completed_plans/total_plans, completed_phases/total_phases)). A barra de progresso da linha de status é renderizada somente quando este campo está presente — sua ausência suprime a barra. |
current_phase |
string | Quando uma fase está em execução | Número da fase extraído do campo Current Phase: do corpo. |
current_phase_name |
string | Quando uma fase tem nome | Nome da fase extraído do campo Current Phase Name: do corpo. |
current_plan |
string | Quando um plano está em andamento | Número do plano extraído do campo Current Plan: do corpo. |
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. |
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 |
Valores de status
normalizeStateStatus() em gsd-core/bin/lib/state-document.cjs mapeia o texto bruto do corpo para estes valores canônicos:
| Valor canônico | Texto correspondente (sem diferenciação de maiúsculas/minúsculas) |
|---|---|
discussing |
contém discussing |
planning |
contém planning ou ready to plan |
executing |
contém executing, in progress ou ready to execute |
verifying |
contém verif |
completed |
contém complete ou done |
paused |
contém paused ou stopped, ou paused_at está presente |
unknown |
nenhuma das anteriores |
Quando um comando do orquestrador está em andamento, a convenção (issue #2833) é escrever o estágio do ciclo de vida diretamente em status:
| Comando | status durante a execução |
|---|---|
/gsd-discuss-phase |
discussing |
/gsd-plan-phase |
planning |
/gsd-execute-phase |
executing |
/gsd-verify-work |
verifying |
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).
Cenas de renderização da linha de status
formatGsdState() em hooks/gsd-statusline.js lê o frontmatter analisado e emite a primeira cena correspondente. Se nenhum campo novo do ciclo de vida se aplicar, a renderização cai para o formato original byte a byte, inalterado desde a v1.38.x.
| Cena | Gatilho | Exemplo de exibição |
|---|---|---|
| 1. Fase ativa | active_phase está populado |
v2.0 [██░░░░░░░░] 20% · Phase 4.5 executing |
| 2. Ocioso, próximo recomendado | active_phase é null E tanto next_action quanto next_phases estão populados |
v2.0 [██░░░░░░░░] 20% · next execute-phase 4.5 |
| 3. Milestone completo | percent é 100 OU completed_phases == total_phases |
v2.0 [██████████] 100% · milestone complete |
| 4. Fallback padrão | Nenhuma das anteriores corresponde | v1.9 Code Quality · executing · ph 1/5 (formato existente) |
Prioridade de cena: quando active_phase e next_action estão populados, a Cena 1 prevalece — um orquestrador está em andamento, portanto uma "próxima recomendação" seria enganosa. Essa prioridade é imposta pela ordem de verificação em formatGsdState() e coberta pelo conjunto "scene priority" em tests/gsd-statusline.test.cjs.
A barra de progresso ([██░░░░░░░░] 20%) é anexada ao segmento do milestone somente quando progress.percent está presente no frontmatter; ausente significa sem barra.
Restrições de análise do frontmatter
O hook de linha de status usa análise baseada em regex (sem biblioteca YAML completa), portanto as seguintes restrições se aplicam. Elas são testadas em tests/gsd-statusline.test.cjs.
-
O frontmatter deve começar no primeiro caractere do arquivo. Qualquer coisa — incluindo comentários — acima do
---de abertura invalida a correspondência. A linha---de abertura deve ser exatamente isso, sem espaços no final. -
Comentários dentro de blocos aninhados não são suportados. O analisador do bloco
progress:requer que a próxima linha seja[ \t]+\w+:. Inserir um# commententreprogress:e sua primeira chave quebra a correspondência e a barra desaparece. Qualquer documentação pertence ao corpo doSTATE.md, não dentro dos blocos do frontmatter. -
O formato primário de
next_phasesé flow de linha única. O analisador tenta primeironext_phases: ["4.5", "4.6"]. Sequências em bloco (- 4.5\n- 4.6) também são analisadas, mas são menos confiáveis para renderização da linha de status. Prefira flow de linha única paranext_phasespara manter o analisador baseado em regex previsível. Se muitas fases candidatas precisarem ser registradas para fins de documentação, armazene-as no corpo doSTATE.md.
Se uma mudança futura substituir o analisador de regex por uma biblioteca YAML completa, essas restrições poderão ser relaxadas e os testes atualizados adequadamente.
Seções do corpo Markdown
O corpo (tudo após o --- de fechamento) segue o template em gsd-core/templates/state.md. As seções padrão são:
Referência do Projeto
Aponta para .planning/PROJECT.md. Contém:
- Valor central — a frase de uma linha da seção Core Value do
PROJECT.md. - Foco atual — qual fase está ativa.
Posição Atual
Onde o projeto está agora:
| Campo | Formato |
|---|---|
Phase: |
X of Y (Phase name) |
Plan: |
A of B in current phase |
Status: |
Texto livre, ex.: Ready to execute, Executing Phase 4, Phase complete — ready for verification |
Last activity: |
Data ISO (YYYY-MM-DD) quando escrito por handler; prosa narrativa quando elaborado pelo executor |
Progress: |
Barra visual, ex.: [████░░░░░░] 40% |
Os campos Status: e Last activity: nesta seção são atualizados pelos handlers do GSD quando o valor existente é um padrão de template conhecido (invariante de Knuth: valores elaborados pelo executor são preservados). A lista completa de padrões de handler conhecidos está em KNOWN_TEMPLATE_DEFAULTS dentro de gsd-core/bin/lib/state-document.cjs.
Métricas de Desempenho
Rastreamento de velocidade de execução:
- Total de planos concluídos, duração média por plano.
- Tabela de detalhamento por fase (
Phase | Plans | Total | Avg/Plan). - Tendência recente: Improving / Stable / Degrading.
Atualizado após cada conclusão de plano.
Contexto Acumulado
Decisões — um resumo das decisões recentes que afetam o trabalho atual (o log completo vive em PROJECT.md). Adicionado via gsd-tools state add-decision.
Todos Pendentes — contagem e referência a .planning/todos/pending/. Capturado via /gsd-capture.
Bloqueadores/Preocupações — problemas que afetam trabalhos futuros, prefixados com a fase de origem. Adicionado via gsd-tools state add-blocker; resolvido via gsd-tools state resolve-blocker.
Continuidade de Sessão
Permite retomada instantânea de sessão:
Last session:— timestamp ISO-8601 da última sessão.Stopped at:— descrição da última ação concluída.Resume file:— caminho para um arquivo.continue-here*.mdse existir, caso contrárioNone.
Compatibilidade retroativa
Os campos de ciclo de vida de fase (active_phase, next_action, next_phases e progress.percent para a barra) são aditivos e opt-in por projeto:
- Um
STATE.mdsem nenhum dos campos de ciclo de vida populados é renderizado byte a byte de forma idêntica à v1.38.x e anteriores. - Adicionar qualquer campo de ciclo de vida é opt-in — o renderizador degrada graciosamente quando os campos estão ausentes.
- A barra de progresso é opt-in mesmo quando o bloco
progressexiste: somenteprogress.percentativa a barra;total_phasesecompleted_phasessozinhos não ativam.
O conjunto de testes formatGsdState #2833 backward compatibility em tests/gsd-statusline.test.cjs garante essa promessa; qualquer mudança que quebre a renderização legada do STATE.md fará o conjunto falhar.