From ac7587287b5c0ffac512aff1c3327a5e78a62fdd Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 28 Aug 2026 20:50:15 -0400 Subject: [PATCH] fix(#3812): document how Current Position actually resolves a duplicate field (#4017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(#3812): say that Current Position is single-valued, and pin the behavior that makes it true #3812 shipped CLOSED with half its acceptance unmet. #3873 delivered cardinality for FRONTMATTER keys - current_phase/current_plan render as optional at docs/reference/state-md.md:89,91, covered by tests/gen-state-md-docs.test.cjs:374. The issue's actual ask was the ## Current Position BODY section, and that never landed. Surfaced by an /adr-phase-coverage audit of epic #3473; the issue was reopened rather than noted. The section now states three things: every field is single-valued, the section is overwritten rather than appended to, and a duplicate resolves to the FIRST occurrence with no warning - so a line appended in good faith is silently ignored rather than winning. Progress history belongs in ## Performance Metrics, two headings down, and the text now points there. The third claim is a behavioral promise about the reader, so it was VERIFIED BY EXECUTION before being written rather than inferred from the issue title: stateExtractField(<"Phase: 1 of 5 (First)" ... "Phase: 9 of 9 (Appended later)">, "Phase") -> "1 of 5 (First)" The mechanism is state-document.cjs:405 - the plain-line pattern ^:[ \t]*(.+) carries flags im with NO g, so String.match returns the first hit. Writing "first wins" without running it would have repeated the exact error I had to retract twice in this epic already. A test pins the reader, not the prose. Three rows in tests/state.test.cjs: T1 (load-bearing) asserts the duplicated case resolves first; T2 asserts the ordinary single-field case still works, so a fix that only functions when duplicated cannot pass; T3 puts a Plan: line BETWEEN the two Phase: lines and asserts it resolves independently - negative space, because a reader returning the first line of the SECTION rather than the first matching FIELD would satisfy T1 alone. Proven to discriminate: a last-match variant returns "9 of 9 (Appended later)" and T1 reds. No assertion checks that the document contains a sentence. That is what local/no-source-grep exists to stop, and it would pin wording that is allowed to improve. The point of the test is that if that regex ever gains g and a last-match walk, the test fails - instead of the documentation quietly becoming a lie with nothing to notice. Prose only, no new heading. docs-state-md-locale-parity compares heading-level sequences by LCS rather than text, so added paragraphs cannot fail it while an added HEADING would fail all four locales. The constraint is structural, not stylistic - confirmed by running that comparison after the edit. The four locale copies are translated rather than left stale. They are not gate-enforced for prose, so "nothing fails" was available and is not the same as correct: leaving four documents asserting something the English one now contradicts is a correctness problem. Code spans and the anchor link stay untranslated - they name real tokens. The whole approach rests on one fact, checked first: ## Current Position at :196-208 sits OUTSIDE every generated marker region (:81-104, :138-151), so a hand edit survives --write. Re-confirmed after all five edits - gen-state-md-docs --check reports all 6 targets up to date. Had that been false the fix would have belonged in the generator, and a hand edit would have been silently reverted. One real gate failure fixed inline rather than reported: the new test's comments referenced docs/reference/state-md.md, which was not in that file's registered exempt-docs paths, and lint-docs-guard-registration failed lint:ci correctly. Registered. Known limit, named rather than folded in: gsd-tools validate/health still do NOT warn on a duplicated Phase:. #3812 records that as a "consider", not a requirement, and confirms none of the nine rules in src/health-diagnostic-rules/{state-consistency,phase-structure}.cts counts occurrences. Documenting the silent first-match is the delivered scope; making it loud is new scope and stays unclaimed. Closes #3812 Co-Authored-By: Claude Opus 5 * docs(#3812): the rule I documented was false — replace it with the measured one An isolated review returned two blockers. Both mine, and the first is the worse kind: I wrote a falsifiable rule into a reference page and got it wrong. 1. "A duplicate resolves to the FIRST occurrence" is FALSE. stateExtractField (src/state-document.cts:401-419) tries BOLD `**F:**` across the whole input, THEN plain `^F:`, THEN a pipe-table row. Form precedence beats document order. Measured against the built reader, all intra-section: Phase: A (plain) / **Phase:** B (bold, later) -> B LATER WINS Phase: A (indented) / Phase: B (plain, later) -> B LATER WINS Phase: A (plain) / | Phase | T (table) | -> A first wins My original verification tested plain-versus-plain, saw first-wins, and generalized to all forms. Measuring one case and claiming the general rule is the same error I have had to retract twice already in this epic. It is also worse than silence. The sentence told authors an appended line is safely ignored; a bold line appended "for emphasis" silently overrides the original. Someone trusting the doc would have corrupted their own state file. And #3812 never asked for a resolution rule - it asked for single-valued, overwrite-not-append, and where history goes. The rule was my unrequested addition. Replaced with the measured truth: resolution is by FORM (bold anywhere, then plain at line-start, then table row), and only WITHIN the winning form does the first occurrence win. Both consequences stated plainly - a higher-ranked form wins regardless of position, and an indented `Phase:` is invisible to the plain form. All five claims in the new paragraph verified by execution before being written, including the two I had wrong. 2. The tests tested the wrong case and passed for the wrong reason. T1/T3 put the second `Phase:` under `## Somewhere else` - the INTER-section case, which #2956 already fixed by scoping. #3812 says verbatim that #2956 "fixed the inter-section case and never addressed intra-section duplication", so the case the new prose describes was untested, and the fixtures passed because of section scoping rather than field resolution. They also called bare stateExtractField rather than the production chain, T2 could not discriminate first from last at all, and no fixture mixed forms - which is precisely why the false claim survived to review. Rewritten as four rows, all intra-section, all through the real stateCurrentPositionSlice -> stateExtractField path: plain-then-plain (first wins within a form), plain-then-bold (the bold LATER value wins - the row whose absence let the false claim ship), indented-then-plain (indented invisible), and sibling-field independence. Each proven to fail against a reader that disagrees. 3. Two dead anchors. pt-BR and zh-CN linked `#performance-metrics` while their own headings are `### Métricas de Desempenho` and `### 性能指标`. Both fixed to the anchor their own heading generates. ja-JP/ko-KR kept the English heading, so theirs already resolved. 4. A ja/ko sentence inverted its own meaning. Both rendered "which is the section designed to grow" with a bare demonstrative whose nearest referent read as Current Position - saying the opposite of the point. Rewritten so the clause attaches unambiguously to `## Performance Metrics`. 5. Cross-locale drift, flagged by the implementing agent rather than by me: after fixing EN, the four locales still stated the OLD false rule. Four documents asserting something measured to be wrong is worse than four saying nothing. All four now carry a faithful translation of the corrected paragraph, with code spans, each file's own anchor, and the ja/ko referent fix preserved. Verified: all five claims executed against the built reader; every rewritten test row proven to discriminate; gen-state-md-docs --check reports all 6 targets up to date, so the edits stay outside the generated marker regions; locale heading parity unaffected (prose only, no headings added); build:lib, lint and lint:ci all exit 0. Refs #3812 Co-Authored-By: Claude Opus 5 * docs(#3812): second false rule on the same page — scope the ranking to the section A second isolated review found a second false falsifiable claim, and the failure mode is the same one twice in a row: attempt 1: verified plain-vs-plain, wrote a claim about ALL FORMS attempt 2: verified bare stateExtractField, wrote a claim about THE DOCUMENT Both times the claim covered a wider surface than what was actually executed. The fix each time was not a better sentence, it was executing the surface the sentence describes. BLOCKER — "bold `**Phase:**` anywhere in the DOCUMENT wins" is false. ## Current Position / Phase: 1 of 5 + ## Archive / **Phase:** 88 bare stateExtractField(whole doc) -> "88 (other section)" PRODUCTION (slice then extract) -> "1 of 5 (in section)" #2956's section slice means production never hands another section to the matcher; a bold line in `## Archive`, or in the YAML frontmatter, is simply not seen. The ranking is real but scoped: it applies WITHIN `## Current Position`. I verified against the bare function and wrote a claim about the system. Every existing test placed its bold line inside the section, which is exactly why nothing contradicted the claim. T5 now puts a bold `**Phase:**` in `## Archive` and asserts production returns the in-section plain value, with the unscoped reader asserted to DISAGREE so the row proves the scoping rather than assuming it. BLOCKER — the changeset still shipped the ORIGINAL retracted claim. I corrected the page and left the release note saying "resolves to the first occurrence ... a second entry added in good faith is silently ignored". The note contradicted the page it announces, and the release note is what most people actually read. Rewritten to the corrected rule. MEDIUM — the concession was inverted. It read "wins even if it comes FIRST in the file", which is the vacuous direction; the surprising case, and the one the very next clause illustrates with an APPENDED bold line, is "even if it comes LAST". All four locales reproduced the inversion faithfully, so it was an EN-source defect rather than translation drift. Two sharp edges now named, both measured: a bold `**Phase:**` followed only by trailing spaces resolves to an EMPTY STRING and does not fall through to a valid plain line below (T6 pins it); and `| **Phase:** | 3 of 4 |` short-circuits to the bold form and returns the literal `"| 3 of 4 |"`. A page that teaches form ranking has to say where the ranking bites. Also fixed: all five files labelled the link `## Performance Metrics` while the heading is `### Performance Metrics`. Anchors resolved correctly everywhere; only the label's level was wrong. Every clause in the final paragraph re-verified through the PRODUCTION chain (stateCurrentPositionSlice -> stateExtractField), clause by clause, before being written: bold in another section does not win; bold in frontmatter does not win; bold appended last does win; first wins within one form; trailing-space bold yields empty. All four locales carry the same corrected rule. gen-state-md-docs --check reports all 6 targets up to date; heading counts unchanged at 20/20 across all five files, so locale heading-parity is untouched; build:lib, lint and lint:ci all exit 0. Refs #3812 Co-Authored-By: Claude Opus 5 * chore(#3812): backfill changeset pr number Refs #3812 Co-Authored-By: Claude Opus 5 --------- Co-authored-by: sim Co-authored-by: Claude Opus 5 --- .changeset/steady-otters-roar.md | 5 + docs/ja-JP/reference/state-md.md | 4 + docs/ko-KR/reference/state-md.md | 4 + docs/pt-BR/reference/state-md.md | 4 + docs/reference/state-md.md | 4 + docs/zh-CN/reference/state-md.md | 4 + ...ocs-guard-registration.exempt-baseline.cjs | 2 +- tests/state.test.cjs | 157 +++++++++++++++++- 8 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 .changeset/steady-otters-roar.md diff --git a/.changeset/steady-otters-roar.md b/.changeset/steady-otters-roar.md new file mode 100644 index 000000000..ae4336854 --- /dev/null +++ b/.changeset/steady-otters-roar.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4017 +--- +**`STATE.md`'s `## Current Position` section now documents that its fields are single-valued.** The section is overwritten rather than appended to, and a duplicated `Phase:` line does not simply resolve to the first occurrence — it resolves by form first (bold, then plain, then pipe-table), scoped to the `## Current Position` section, and only within the winning form does the first occurrence win. So a bold line added in good faith after an earlier plain line silently overrides it rather than being ignored. That behavior was always true and was never written down, which is what #3812 reported. Progress history belongs in `## Performance Metrics`, and the reference page now says so in all five languages. (#3812) diff --git a/docs/ja-JP/reference/state-md.md b/docs/ja-JP/reference/state-md.md index 5da41d08d..26a4bf114 100644 --- a/docs/ja-JP/reference/state-md.md +++ b/docs/ja-JP/reference/state-md.md @@ -196,6 +196,10 @@ Phase-completion verbs never write `Milestone complete` (the overloaded bare val | `Last activity:` | ハンドラー書き込み時は ISO 日付(`YYYY-MM-DD`); エグゼキューター作成時はナラティブ文章 | | `Progress:` | ビジュアルバー。例: `[████░░░░░░] 40%` | +**このセクションのすべてのフィールドは単一値であり、このセクションは追記されるのではなく上書きされます。** 2つ目の `Phase:` 行は2つ目の位置ではなく、履歴でもありません — それは不正な入力です。リーダーは重複を文書内の出現順序では解決しません。**フォーム**によって解決します。判定順序は各フォームが `## Current Position` **セクション内**のどこに現れるかに関係なく、このセクション内のどこにあっても太字の `**Phase:** value`、次に行頭の平文 `Phase: value`、最後にパイプテーブルの `| Phase | value |` 行の順です — そして勝者となったフォーム**内**でのみ、**最初の**出現が勝ちます。このセクションの外にある太字または平文の行 — 以前の `## Archive` エントリや YAML フロントマター内の太字行 — は一切参照されません。実装は常にまず `## Current Position` セクションへスコープを絞ってから(#2956)フォームの優先順位を適用します。実務上の帰結は2つあります。上位のフォームで書かれた重複は、セクション内で**後に**現れていても勝ちます。つまり「強調のために」追記された太字行は、無視されるのではなく、先行する平文行を黙って上書きします。また、インデントされた `Phase:` 行は平文フォーム(行頭を基準とする)からは見えず、次に一致するフォームへとフォールスルーします。「上位フォームが勝つ」というルールには2つの鋭い落とし穴が伴います。値が末尾の空白だけの太字行は、下にある有効な平文行にフォールスルーせず、空文字列として解決されます。また、ラベルのセル自体が太字になっているパイプテーブル行(`| **Phase:** | value |`)は太字パターンに先に捕捉され、パイプを含むセルの文字列がそのまま返されます。このセクションを書き込む際は、行を追加するのではなく、置き換えてください。 + +進捗の*履歴*はここには属しません。それは、増えていくように設計されたセクションである以下の [`### Performance Metrics`](#performance-metrics) に蓄積されます。 + このセクションの `Status:` および `Last activity:` フィールドは、既存の値が既知のテンプレートデフォルト値の場合に GSD ハンドラーによって更新されます(クヌース不変式: エグゼキューター作成値は保存されます)。既知のハンドラーデフォルト値の完全なリストは `gsd-core/bin/lib/state-document.cjs` の `KNOWN_TEMPLATE_DEFAULTS` に記載されています。 ### Performance Metrics diff --git a/docs/ko-KR/reference/state-md.md b/docs/ko-KR/reference/state-md.md index 9d75d6ced..c26955c88 100644 --- a/docs/ko-KR/reference/state-md.md +++ b/docs/ko-KR/reference/state-md.md @@ -196,6 +196,10 @@ Phase-completion verbs never write `Milestone complete` (the overloaded bare val | `Last activity:` | 핸들러가 기록할 때 ISO 날짜(`YYYY-MM-DD`); 실행기가 작성할 때 서술형 산문 | | `Progress:` | 시각적 막대, 예: `[████░░░░░░] 40%` | +**이 섹션의 모든 필드는 단일 값이며, 이 섹션은 추가되는 것이 아니라 덮어써집니다.** 두 번째 `Phase:` 줄은 두 번째 위치가 아니며 이력도 아닙니다 — 이는 잘못된 형식의 입력입니다. 리더는 중복을 문서 내 등장 순서로 해석하지 않습니다. **형식(form)**에 따라 해석합니다. 판정 순서는 각 형식이 `## Current Position` **섹션 내** 어디에 나타나든 관계없이, 섹션 내 어디에든 있는 굵은 글씨 `**Phase:** value`, 그다음 줄 시작에 오는 일반 텍스트 `Phase: value`, 마지막으로 파이프 테이블의 `| Phase | value |` 행 순입니다 — 그리고 승리한 형식 **내에서만** **첫 번째** 항목이 우선합니다. 이 섹션 밖에 있는 굵은 글씨나 일반 텍스트 줄 — 이전의 `## Archive` 항목이나 YAML 프런트매터의 굵은 글씨 줄 — 은 전혀 참조되지 않습니다. 실제 구현은 항상 먼저 `## Current Position` 섹션으로 범위를 좁힌 뒤(#2956) 형식 우선순위를 적용합니다. 실무적으로 두 가지 결과가 따릅니다. 상위 형식으로 작성된 중복은 섹션 내에서 **나중에** 등장하더라도 승리합니다. 즉 "강조를 위해" 뒤에 추가된 굵은 글씨 줄은 무시되는 대신 앞선 일반 텍스트 줄을 조용히 덮어씁니다. 또한 들여쓰기된 `Phase:` 줄은 (줄 시작을 기준으로 삼는) 일반 텍스트 형식에는 보이지 않으며, 다음으로 일치하는 형식으로 넘어갑니다. "상위 형식이 이긴다"는 규칙에는 두 가지 주의할 점이 있습니다. 값이 후행 공백뿐인 굵은 글씨 줄은 아래의 유효한 일반 텍스트 줄로 넘어가지 않고 빈 문자열로 해석됩니다. 또한 레이블 셀 자체가 굵게 표시된 파이프 테이블 행(`| **Phase:** | value |`)은 굵은 글씨 패턴에 먼저 포착되어 파이프를 포함한 셀 원문이 그대로 반환됩니다. 이 섹션을 작성할 때는 줄을 추가하지 말고 교체하세요. + +진행 *이력*은 여기에 속하지 않습니다. 이는 계속 늘어나도록 설계된 섹션인 아래의 [`### Performance Metrics`](#performance-metrics)에 누적됩니다. + 이 섹션의 `Status:` 및 `Last activity:` 필드는 기존 값이 알려진 템플릿 기본값인 경우 GSD 핸들러에 의해 업데이트됩니다(크누스 불변량: 실행기가 작성한 값은 보존됩니다). 알려진 핸들러 기본값의 전체 목록은 `gsd-core/bin/lib/state-document.cjs`의 `KNOWN_TEMPLATE_DEFAULTS`에 있습니다. ### Performance Metrics diff --git a/docs/pt-BR/reference/state-md.md b/docs/pt-BR/reference/state-md.md index a79be2010..6b69e4d28 100644 --- a/docs/pt-BR/reference/state-md.md +++ b/docs/pt-BR/reference/state-md.md @@ -196,6 +196,10 @@ Onde o projeto está agora: | `Last activity:` | Data ISO (`YYYY-MM-DD`) quando escrito por handler; prosa narrativa quando elaborado pelo executor | | `Progress:` | Barra visual, ex.: `[████░░░░░░] 40%` | +**Todos os campos nesta seção têm valor único, e a seção é sobrescrita em vez de receber conteúdo anexado.** Uma segunda linha `Phase:` não representa uma segunda posição nem um histórico — é uma entrada malformada. Os leitores não resolvem uma duplicata pela ordem no documento; eles a resolvem pela **forma**, verificada nesta ordem independentemente de onde cada forma apareça **dentro da seção `## Current Position`** — negrito `**Phase:** value` em qualquer ponto da seção, depois texto simples `Phase: value` no início de uma linha, depois uma linha de tabela `| Phase | value |` — e apenas *dentro* da forma vencedora prevalece a **primeira** ocorrência. Uma linha em negrito ou em texto simples fora desta seção — uma entrada anterior em `## Archive`, ou uma linha em negrito no frontmatter YAML — nunca é consultada; a implementação em produção sempre restringe o escopo à seção `## Current Position` primeiro (#2956) antes de aplicar a hierarquia de formas. Duas consequências práticas: uma duplicata escrita em uma forma de prioridade mais alta vence mesmo que apareça **por último** na seção, de modo que uma linha em negrito acrescentada "para dar ênfase" sobrescreve silenciosamente uma linha em texto simples anterior em vez de ser ignorada; e uma linha `Phase:` indentada é invisível para a forma em texto simples (que se ancora no início da linha) e recai sobre a próxima forma que corresponder. A regra "a forma de prioridade mais alta vence" tem duas arestas afiadas: uma linha em negrito cujo valor é apenas espaço em branco final é resolvida como uma string vazia, em vez de recair sobre uma linha em texto simples válida abaixo dela; e uma linha de tabela cuja célula de rótulo está ela mesma em negrito (`| **Phase:** | value |`) é capturada primeiro pelo padrão em negrito, retornando o texto literal da célula, incluindo suas barras verticais. Escreva a seção substituindo-a, nunca adicionando uma linha. + +O *histórico* de progresso não pertence aqui. Ele se acumula em [`### Performance Metrics`](#métricas-de-desempenho) logo abaixo, que é a seção projetada para crescer. + 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 diff --git a/docs/reference/state-md.md b/docs/reference/state-md.md index a41263b99..21a8c4e1a 100644 --- a/docs/reference/state-md.md +++ b/docs/reference/state-md.md @@ -205,6 +205,10 @@ Where the project stands right now: | `Last activity:` | ISO date (`YYYY-MM-DD`) when handler-written; narrative prose when executor-authored | | `Progress:` | Visual bar, e.g. `[████░░░░░░] 40%` | +**Every field in this section is single-valued, and the section is overwritten rather than appended to.** A second `Phase:` line is not a second position and is not history — it is malformed input. Readers do **not** resolve a duplicate by document order; they resolve it by **form**, checked in this order regardless of where each form appears **within the `## Current Position` section** — bold `**Phase:** value` anywhere within the section, then plain `Phase: value` at the start of a line, then a pipe-table `| Phase | value |` row — and only *within* the winning form does the **first** occurrence win. A bold or plain line outside this section — an earlier `## Archive` entry, or a bold line in the YAML frontmatter — is never consulted; production always scopes to the `## Current Position` section first (#2956) before applying the form ranking. Two practical consequences: a duplicate written in a higher-ranked form wins even if it comes **last** in the section, so a bold line appended "for emphasis" silently overrides an earlier plain line instead of being ignored; and an indented `Phase:` line is invisible to the plain form (which anchors at line-start) and falls through to whatever form matches next. Two sharp edges follow from "higher form wins": a bold line whose value is only trailing whitespace resolves to an empty string rather than falling through to a valid plain line below it, and a pipe-table row whose label cell is itself bolded (`| **Phase:** | value |`) is caught by the bold pattern first, returning the literal cell text including its pipes. Write the section by replacing it, never by adding a line. + +Progress *history* does not belong here. It accumulates in [`### Performance Metrics`](#performance-metrics) below, which is the section designed to grow. + The `Status:` and `Last activity:` fields in this section are updated by GSD handlers when the existing value is a known template default (Knuth invariant: executor-authored values are preserved). The full list of known handler defaults is in `KNOWN_TEMPLATE_DEFAULTS` inside `gsd-core/bin/lib/state-document.cjs`. ### Performance Metrics diff --git a/docs/zh-CN/reference/state-md.md b/docs/zh-CN/reference/state-md.md index 4deb45f70..2121e0d90 100644 --- a/docs/zh-CN/reference/state-md.md +++ b/docs/zh-CN/reference/state-md.md @@ -196,6 +196,10 @@ Phase-completion verbs never write `Milestone complete` (the overloaded bare val | `Last activity:` | 处理器写入时为 ISO 日期(`YYYY-MM-DD`);执行器编写时为叙述性文本 | | `Progress:` | 可视化进度条,如 `[████░░░░░░] 40%` | +**本节中的每个字段都是单值的,本节会被覆盖而不是追加。** 第二个 `Phase:` 行并不是第二个位置,也不是历史记录——它是格式错误的输入。读取器并不按文档中的出现顺序来解析重复项,而是按**形式**来解析:判定顺序与各形式在 `## Current Position` **本节内**的位置无关,依次是本节内任意位置的粗体 `**Phase:** value`,其次是位于行首的普通文本 `Phase: value`,最后是表格行 `| Phase | value |`——而且只有在**胜出的形式内部**,才由**第一个**出现的值取胜。本节之外的粗体或普通文本行——例如更早的 `## Archive` 条目,或 YAML frontmatter 中的粗体行——绝不会被读取;生产实现总是先将范围限定到 `## Current Position` 本节(#2956),然后才应用形式优先级。这带来两个实际后果:以更高优先级形式写入的重复项,即使在本节中出现得**更晚**也会获胜,因此为"强调"而追加的粗体行会静默覆盖在它之前的普通文本行,而不是被忽略;此外,缩进的 `Phase:` 行对普通文本形式(以行首为锚点)是不可见的,会被跳过并落到下一个匹配的形式上。"更高形式获胜"这条规则还带来两个需要留意的边界情况:如果粗体行的值只是尾随空白,则会解析为空字符串,而不会回退到下方有效的普通文本行;如果表格行的标签单元格本身是粗体写法(`| **Phase:** | value |`),会被粗体模式优先捕获,返回包含竖线在内的原始单元格文本。写入本节时应替换它,而不是新增一行。 + +进度*历史*不属于这里。它会累积到下方的 [`### Performance Metrics`](#性能指标) 中,那才是设计用于增长的部分。 + 当现有值为已知模板默认值时,该章节中的 `Status:` 和 `Last activity:` 字段由 GSD 处理器更新(Knuth 不变式:执行器编写的值被保留)。已知处理器默认值的完整列表位于 `gsd-core/bin/lib/state-document.cjs` 中的 `KNOWN_TEMPLATE_DEFAULTS`。 ### 性能指标 diff --git a/scripts/lint-docs-guard-registration.exempt-baseline.cjs b/scripts/lint-docs-guard-registration.exempt-baseline.cjs index 680236af2..859837e99 100644 --- a/scripts/lint-docs-guard-registration.exempt-baseline.cjs +++ b/scripts/lint-docs-guard-registration.exempt-baseline.cjs @@ -183,7 +183,7 @@ const DOCS_GUARD_EXEMPT_DOCS_PATHS = { 'runtime-name-policy.test.cjs': ['docs/customize/skills'], 'security-prompt-injection.security.test.cjs': ['docs/notes.md'], 'shipped-reference-cites.test.cjs': [], - 'state.test.cjs': ['docs/CONFIGURATION.md'], + 'state.test.cjs': ['docs/CONFIGURATION.md', 'docs/reference/state-md.md'], 'worktree-safety.test.cjs': ['docs/SUMMARY.md'], }; diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 8175bb5a9..98c844bc6 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -1,4 +1,4 @@ -// docs-guard-exempt: docs/CONFIGURATION.md is cited only in a comment; never read. +// docs-guard-exempt: docs/CONFIGURATION.md and docs/reference/state-md.md are cited only in comments; never read. // allow-test-rule: source-text-is-the-product // Reads .md/.json/.yml product files whose deployed text IS what the // runtime loads — testing text content tests the deployed contract. @@ -1201,6 +1201,161 @@ describe('stateExtractField and stateReplaceField helpers', () => { assert.strictEqual(result, 'Active', 'should match field name case-insensitively'); }); + // (#3812) docs/reference/state-md.md's "### Current Position" section + // promises: every field there is single-valued, and duplicates resolve by + // FORM first (bold `**F:**` anywhere, then plain `^F:`, then pipe-table), + // and only within the winning form does first-occurrence win. #2956 already + // fixed the INTER-section case (a duplicate in a different section never + // shadows the real one) by scoping to `## Current Position`; these rows + // pin the INTRA-section case #2956 never addressed — every fixture here + // duplicates the field WITHIN the same `## Current Position` section, so a + // reader that merely scopes correctly (and gets first-occurrence right by + // accident) cannot pass. Each row is exercised through the real production + // chain — `stateCurrentPositionSlice` (the function `state.cts`'s private + // `matchCurrentPositionSection` delegates to) feeding `stateExtractField` + // — not bare `stateExtractField` over hand-scoped content, so section + // scoping is genuinely exercised rather than assumed. See + // .gsd/phase/docs-3812-current-position-cardinality/50-test-matrix.md. + + function extractViaProductionChain(body, fieldName) { + const scope = stateDocument.stateCurrentPositionSlice(body) ?? body; + return stateExtractField(scope, fieldName); + } + + test('T1: plain-then-plain intra-section duplicate resolves to the first occurrence (#3812)', () => { + const content = [ + '# STATE', + '', + '## Current Position', + '', + 'Phase: 1 of 5 (First, plain)', + 'Plan: 1 of 3', + 'Phase: 9 of 9 (Second, plain)', + ].join('\n'); + + const result = extractViaProductionChain(content, 'Phase'); + assert.strictEqual( + result, + '1 of 5 (First, plain)', + 'within one form (plain), a duplicated Phase field must resolve to the first occurrence' + ); + }); + + test('T2: mixed-form intra-section duplicate — later BOLD line beats an earlier plain line (#3812)', () => { + const content = [ + '# STATE', + '', + '## Current Position', + '', + 'Phase: 1 of 5 (First, plain)', + 'Plan: 1 of 3', + '**Phase:** 9 of 9 (Second, bold)', + ].join('\n'); + + const result = extractViaProductionChain(content, 'Phase'); + assert.strictEqual( + result, + '9 of 9 (Second, bold)', + 'bold form outranks plain form regardless of document order, per docs/reference/state-md.md' + ); + }); + + test('T3: an indented Phase line is invisible to the plain form; the later un-indented line wins (#3812)', () => { + const content = [ + '# STATE', + '', + '## Current Position', + '', + ' Phase: 1 of 5 (Indented, ignored)', + 'Phase: 9 of 9 (Un-indented, matches)', + 'Plan: 1 of 3', + ].join('\n'); + + const result = extractViaProductionChain(content, 'Phase'); + assert.strictEqual( + result, + '9 of 9 (Un-indented, matches)', + 'the plain form anchors at true line-start; an indented line never matches it' + ); + }); + + test('T4: a sibling field between duplicated Phase lines resolves to its own value (#3812)', () => { + const content = [ + '# STATE', + '', + '## Current Position', + '', + 'Phase: 1 of 5 (First, plain)', + 'Plan: 2 of 3', + 'Phase: 9 of 9 (Second, plain)', + ].join('\n'); + + const phase = extractViaProductionChain(content, 'Phase'); + const plan = extractViaProductionChain(content, 'Plan'); + assert.strictEqual( + phase, + '1 of 5 (First, plain)', + 'Phase must still resolve to the first occurrence within its form with a sibling field in between' + ); + assert.strictEqual( + plan, + '2 of 3', + 'Plan must resolve to its own value, not be affected by the duplicated Phase field' + ); + }); + + test('T5: a bold Phase line in a DIFFERENT section never shadows the plain value inside Current Position (#3812)', () => { + const content = [ + '# STATE', + '', + '## Current Position', + '', + 'Phase: 1 of 5 (in section, plain)', + 'Plan: 1 of 3', + '', + '## Archive', + '', + '**Phase:** 88 (bold, other section — must NOT win)', + ].join('\n'); + + const result = extractViaProductionChain(content, 'Phase'); + assert.strictEqual( + result, + '1 of 5 (in section, plain)', + 'the form ranking applies only within the Current Position section — a bold line elsewhere must not outrank the in-section plain value' + ); + + // Discrimination: a reader that runs stateExtractField over the WHOLE + // document (skipping the #2956 section scope) disagrees with production + // here — it lets the out-of-section bold line win. + const wholeDocumentResult = stateExtractField(content, 'Phase'); + assert.strictEqual( + wholeDocumentResult, + '88 (bold, other section — must NOT win)', + 'sanity check: an unscoped reader gets this case wrong, which is exactly the bug this row pins' + ); + assert.notStrictEqual(result, wholeDocumentResult, 'the scoped and unscoped readers must disagree on this fixture'); + }); + + test('T6: a bold Phase line with only trailing whitespace resolves to an empty string, not a fallthrough (#3812)', () => { + const content = [ + '# STATE', + '', + '## Current Position', + '', + '**Phase:** ', + 'Phase: 1 of 5 (plain, must NOT be used)', + 'Plan: 1 of 3', + ].join('\n'); + + const result = extractViaProductionChain(content, 'Phase'); + assert.strictEqual( + result, + '', + 'the bold form wins outright even when its captured value is only trailing whitespace; it must not fall through to the plain line below' + ); + }); + // stateReplaceField tests test('replaces field value', () => {