okstra 0.107.0 → 0.107.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/convergence.md +6 -6
- package/runtime/prompts/lead/report-writer.md +12 -8
- package/runtime/prompts/profiles/implementation-planning.md +6 -6
- package/runtime/schemas/final-report-v1.0.schema.json +6 -0
- package/runtime/templates/reports/final-report.template.md +1 -1
- package/runtime/templates/reports/i18n/en.json +1 -0
- package/runtime/templates/reports/i18n/ko.json +1 -0
- package/runtime/validators/validate-brief.py +37 -0
- package/runtime/validators/validate-implementation-plan-stages.py +55 -12
- package/runtime/validators/validate-run.py +354 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -648,7 +648,7 @@ From the report-writer's draft of `## 5.4 Implementation Plan Deliverables`, lea
|
|
|
648
648
|
|
|
649
649
|
Each plan item inherits the `[TICKETID: ...]` tag of its source section (per the standard ticket-tagging contract).
|
|
650
650
|
|
|
651
|
-
When extracting each item, lead also captures a **`subject`** — a plain one-line label (≤12 words) describing *what that item is* in the reader's terms, e.g. `P-Opt-1` → "Option A: upload v2 를 신규 모듈로 분리", `P-Step-1.1` → "Stage 1 Step 2: `npm run test:v2` 로 회귀 검증". This is a label-capture, not new analysis. The `subject` is what §5.5.9 renders as the per-item heading so the reader knows *what* each AGREE/DISAGREE is about without cross-referencing §4.5; a bare `P-*` ID with no subject is a contract violation.
|
|
651
|
+
When extracting each item, lead also captures a **`subject`** — a plain one-line label (≤12 words) describing *what that item is* in the reader's terms, e.g. `P-Opt-1` → "Option A: upload v2 를 신규 모듈로 분리", `P-Step-1.1` → "Stage 1 Step 2: `npm run test:v2` 로 회귀 검증". This is a label-capture, not new analysis. The `subject` is what §5.5.9 renders as the per-item heading so the reader knows *what* each AGREE/DISAGREE is about without cross-referencing §4.5; a bare `P-*` ID with no subject is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_item_subject_substance` fails a subject that is a placeholder — under 3 chars, equal to the item id, or shaped like a bare `P-*` id.
|
|
652
652
|
|
|
653
653
|
### Plan-body verdict semantics
|
|
654
654
|
|
|
@@ -679,9 +679,9 @@ When `config.adversarial == true` (the default for `implementation-planning`; se
|
|
|
679
679
|
- The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
|
|
680
680
|
- The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are executable as written. This is the one allowed widening of the lightweight "judge from internal consistency and stated commands / paths" rule — confirming the existence of cited paths is not "re-analyzing the original requirements".
|
|
681
681
|
- If a cited path / command / validation signal cannot be confirmed, the verifier responds `DISAGREE(<kind>)` with the applicable breakage kind (a–e); uncertainty resolves toward DISAGREE, not AGREE.
|
|
682
|
-
-
|
|
682
|
+
- **Single-vote-blocking kinds.** A single `DISAGREE` is approval-blocking on its own — no majority needed — when the breakage kind is `a` (cited path/symbol mismatch) or `d` (rollback violates commit/dependency order) on *any* plan item, or `f` (requirement-coverage mismatch) on a `P-Req-*` item. These defects are concrete, safety-critical, and adversarially verifiable (the verifier confirmed the cited path / order / requirement), so one correct dissent must not be outvoted. Each creates a `majority-disagree` classification and MUST become a `Blocks=approval` clarification row. Kinds `b` / `c` / `e` still need a majority — `b` especially is prone to planning-vs-implementation environment false positives. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` (`_SINGLE_VOTE_BLOCKING_KINDS = {a, d}` + the P-Req `f` rule).
|
|
683
683
|
|
|
684
|
-
Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden)
|
|
684
|
+
Plan-body verification stays **lightweight** even under this posture — the `verificationMode = "full-reanalysis"` forcing in §"Adversarial Verification Mode" applies to finding convergence only (see §"Mode constraint"); the adversarial posture here only changes verifier behaviour, not the mode. This raises verification *quality* (active refutation, plan-side burden). The gate *threshold* stays majority-based for the majority-gated kinds (`b`/`c`/`e`), with the single-vote-blocking exception above for the concrete, safety-critical kinds (`a`/`d`, and `f` on P-Req). A majority requires at least two participating (non-error) votes, so a lone surviving `DISAGREE` whose peer returned a non-result does NOT block on a majority-gated kind — a worker failure must not make the gate stricter than a healthy roster would.
|
|
685
685
|
|
|
686
686
|
### Round protocol (single round at default `maxRounds=1`)
|
|
687
687
|
|
|
@@ -692,20 +692,20 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
|
|
|
692
692
|
- `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
|
|
693
693
|
- `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
|
|
694
694
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE` — treat as `partial-consensus` for gate purposes; record dissent. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs. Plan-body classifications use this dedicated label to avoid the collision.)
|
|
695
|
-
- `majority-disagree` — majority of analysers `DISAGREE`
|
|
695
|
+
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes), OR any single-vote-blocking kind fires: one `DISAGREE(a)` / `DISAGREE(d)` on any item, or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks the Approval marker**.
|
|
696
696
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
697
697
|
5. Gate result resolution:
|
|
698
698
|
- any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
|
|
699
699
|
- all dispatches non-result → `aborted-non-result`
|
|
700
700
|
- any `partial-consensus` / `dissent-isolated` present, no `majority-disagree` → `passed-with-dissent`
|
|
701
701
|
- all items `full-consensus` → `passed`
|
|
702
|
-
6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
702
|
+
6. Lead writes `runs/<task-type>/state/plan-body-verification-<task-type>-<seq>.json` (schema below) and populates `### 5.5.9 Plan Body Verification` in the final report's data.json (`implementationPlanning.planBodyVerification`, schema `schemas/final-report-v1.0.schema.json`; template at `templates/reports/final-report.template.md`). The §5.5.9 body is **grouped by plan item**: `planItems[]`, each carrying its `id`, its plain-language `subject` (rendered as the item heading), an optional `sourceSection`, an optional `clarificationId` (the `C-<N>` this item blocks on when `majority-disagree`), and a `verdicts[]` list (`worker / verdict / breakageKind / note`) — one verdict row per worker under that item. The renderer prints three fixed legends (gate values, verdict tokens, breakage kinds a–f) so the reader can decode every cell without opening this spec. The older flat `#### Verdict details` table (`Plan item / Worker / …`, one row per plan-item × worker pair) is superseded by the grouped layout — it hid *what* each vote was about behind a bare `P-*` ID; the subject heading is the fix. The validator's `Plan Body Verification` + `Gate result:` substring checks still gate this section.
|
|
703
703
|
7. For every `majority-disagree` item, lead adds a row to `## 1. Clarification Items` with:
|
|
704
704
|
- new `C-<N>` ID (numbering continues from any existing rows)
|
|
705
705
|
- `Statement` summarising the disagreement and the worker breakage `<kind>`
|
|
706
706
|
- `Kind` chosen per the standard policy (usually `decision` for option-level conflicts, `data-point` for path/symbol mismatches)
|
|
707
707
|
- `Blocks=approval`
|
|
708
|
-
- the
|
|
708
|
+
- the item's `planItems[].clarificationId` set to that `C-<N>` (1:1 link). `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item's `clarificationId` is missing, dangling, or points at a non-`approval` row.
|
|
709
709
|
8. The top-of-report `- [ ] Approved` marker line is rendered if and only if the Gate result is `passed` or `passed-with-dissent`. `validators/validate-run.py` `validate_phase_boundary` enforces this correspondence; manually adding the marker line when the gate did not pass is a contract violation.
|
|
710
710
|
|
|
711
711
|
### `plan-body-verification-<task-type>-<seq>.json` schema
|
|
@@ -34,7 +34,7 @@ Agent(
|
|
|
34
34
|
)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The `model:` parameter is **derived from the Report writer worker's `modelExecutionValue`** in `task-manifest.json`, mapped to an Agent family token (`opus` / `sonnet` / `haiku`) per [team-contract](./team-contract.md) "Model Assignment Rules" #3–#4. Do NOT hardcode it — the report-writer-worker definition is `model: inherit`, so without this explicit parameter the worker silently runs on the lead's model instead of its assignment. The same `modelExecutionValue` feeds the prompt header in item
|
|
37
|
+
The `model:` parameter is **derived from the Report writer worker's `modelExecutionValue`** in `task-manifest.json`, mapped to an Agent family token (`opus` / `sonnet` / `haiku`) per [team-contract](./team-contract.md) "Model Assignment Rules" #3–#4. Do NOT hardcode it — the report-writer-worker definition is `model: inherit`, so without this explicit parameter the worker silently runs on the lead's model instead of its assignment. The same `modelExecutionValue` feeds the prompt header in item 7 below, so the spawn model and the recorded `**Model:**` header always agree.
|
|
38
38
|
|
|
39
39
|
The prompt MUST include, in this order at the top:
|
|
40
40
|
|
|
@@ -43,18 +43,22 @@ The prompt MUST include, in this order at the top:
|
|
|
43
43
|
3. `**Result Path:** runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` — canonical JSON SSOT. The renderer produces the sibling `.md` automatically.
|
|
44
44
|
4. `**Worker Result Path:** runs/<task-type>/worker-results/report-writer-worker-<task-type>-<seq>.md` — mandatory validator-checked worker-results audit file
|
|
45
45
|
5. `Assigned worker prompt history path: <absolute-path>`
|
|
46
|
-
6.
|
|
47
|
-
|
|
46
|
+
6. The three BLOCKING dispatch anchor headers the lead MUST inject (per [team-contract](./team-contract.md) headers #5 / #7 / #8 — the worker cannot synthesize the two error paths and must not dead-end when they are absent):
|
|
47
|
+
- `**Worker Preamble Path:** <absolute-path>` — the canonical Required Reading + Error Reporting SSOT at `~/.okstra/templates/worker-prompt-preamble.md`; the worker Reads it end-to-end before producing output.
|
|
48
|
+
- `**Errors log path:** <absolute-path>` — run-level errors JSONL (`logs/errors-<task-type>-<seq>.jsonl`).
|
|
49
|
+
- `**Errors sidecar path:** <absolute-path>` — this worker's per-run sidecar JSON (`worker-results/report-writer-worker-errors-<task-type>-<seq>.json`).
|
|
50
|
+
7. `**Model:** Report writer worker, <modelExecutionValue>` (resolved per Phase 5.5 anchor-header rules)
|
|
51
|
+
8. The full `[Required reading]` clause (see [team-contract](./team-contract.md)) — for Phase 6 it adds two **per-task-type, instruction-set-local** read-only files, both scoped to this run's task-type by `okstra-ctl` at prep time:
|
|
48
52
|
- `<instruction-set>/final-report-schema.json` — a task-type excerpt of the data.json schema (the other task-types' deliverable blocks and their unreachable `$defs` are stripped; ~38% of the full schema is `$defs` alone). This is your authoring contract for the data.json shape. Do **NOT** pull the full `schemas/final-report-v1.0.schema.json` — it carries all task-types and its `schemas/...` path is not part of the task bundle. (Validation still runs against the full schema post-hoc via the renderer, so the excerpt never relaxes the contract.)
|
|
49
53
|
- `<instruction-set>/final-report-template.md` — the **phase-stripped** template (every other task-type's §5.x deliverable block removed by `render.py`'s `_strip_phase_blocks`, leaving only your run's §5.x). Do **NOT** also pull the full `templates/reports/final-report.template.md` source (it re-adds ~330 lines of other phases' deliverables and is not in the task bundle).
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
9. A one-line MCP pointer instead of the verbatim block — `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).` The brief is already in the report-writer's Required reading (item 8), so the verbatim block is redundant.
|
|
55
|
+
10. The convergence classifications (Full/Partial/Contested/Worker-Unique), the round history data (`roundHistory[]`), the `round2SkippedReason` value, and pointers to all worker result files under `worker-results/`. The report-writer worker populates `crossVerification.roundHistory` in the data.json so Section 6 can show which rounds executed, queue sizes, and why Round 2 was (or was not) skipped. The renderer prints the full per-round table only when more than one round ran; single-round or zero-round histories are auto-collapsed to a one-line summary.
|
|
56
|
+
11. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
|
|
53
57
|
has been resolved by the lead from project.json / global config
|
|
54
58
|
before the dispatch is constructed. The worker copies this verbatim
|
|
55
59
|
into `data.json.meta.reportLanguage`.
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
12. For implementation-planning runs: a literal block listing the 8 required English section headings the validator scans for (`Option Candidates`, `Trade-off`, `Recommended Option`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `User Approval Request`). The writer must use these exact substrings as section headings (Korean translation in parentheses is allowed).
|
|
61
|
+
13. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" via Bash so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
|
|
58
62
|
|
|
59
63
|
**Completion detection after dispatch (BLOCKING).** The `Agent(name: ..., run_in_background: true)` call returns `Spawned successfully` immediately; that ack is NOT completion. After dispatching the report-writer (async), Lead MUST detect its completion via the self-scheduled polling protocol in [team-contract](./team-contract.md) "Worker-completion detection (self-scheduled polling)", polling for the appearance of the data.json (Result Path) and the worker-results file (Worker Result Path) — do NOT restate the algorithm here. Report-writer is a single worker, so the pending set has one entry; the SSOT protocol handles that naturally. Lead MUST NOT treat the `Spawned successfully` ack as completion and MUST NOT end its turn with a prose "waiting for the report" statement; that path stalls the run until the user manually nudges it.
|
|
60
64
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
- The YAML frontmatter `approved: true|false` field is the only authorised approval gate. report-writer always emits `approved: false`. The user clears it either by (a) editing the frontmatter line to `approved: true` directly, or (b) invoking the next phase with `--approve` so the CLI flips the frontmatter on the user's behalf. `okstra_ctl.run._validate_approved_plan` reads this field and refuses entry until it is `true`.
|
|
42
42
|
- Cross-verification mode:
|
|
43
43
|
- Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / option) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode".
|
|
44
|
-
- §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/convergence.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is
|
|
44
|
+
- §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/convergence.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kinds `a` (path/symbol mismatch) / `d` (rollback order) — and `f` on `P-Req-*` items. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see `convergence.md` §"Adversarial plan-body posture").
|
|
45
45
|
{{INCLUDE:_coverage-critic.md}}
|
|
46
46
|
- Non-goals:
|
|
47
47
|
- code-level micro-optimization unless it changes the implementation approach
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
Validator S10d rejects a missing/empty line in any of the three categories (skipped only when a `TDD exemption:` line is present). The `RED:` step below must encode these cases, not a single assertion.
|
|
79
79
|
- **Per-stage subsections** (`## 5.5.<i> Stage <i>: <title>` for each `i`), each containing the four required subsections:
|
|
80
80
|
- `### Carry-In` — for `depends-on (none)`: task-brief only. Otherwise: each depended-on stage's static exit contract + runtime sidecar path `runs/<impl-key>/carry/stage-<i>.json` placeholder.
|
|
81
|
-
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change (시간 하한 없음; 함께 바뀌는 여러 파일을 포함할 수 있다); for code steps include actual code or diff sketch. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`expected` = FAIL) — the RED step encodes the case set, not a single happy-path assertion; at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`expected` = PASS); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section (mirrors the executor's per-step exemption in `_implementation-executor.md`). Validator S10c enforces RED-first + GREEN
|
|
81
|
+
- `### Stepwise Execution Order` — bite-sized table with `step | action | files | command | expected`. **Effective row count ≤ 8** (excluding header / divider / blank). Each step is one cohesive, self-contained change (시간 하한 없음; 함께 바뀌는 여러 파일을 포함할 수 있다); for code steps include actual code or diff sketch. **TDD ordering is MUST, not a preference:** the **first** effective step's `action` cell MUST start with the literal `RED:` and describe the failing test(s) that capture this stage's `Acceptance` **and the three declared `Test case (success|boundary|failure)` lines** (`expected` = FAIL) — the RED step encodes the case set, not a single happy-path assertion; at least one later `action` cell MUST start with the literal `GREEN:` and describe the minimal implementation that makes it pass (`expected` = PASS); an optional refactor step starts with `REFACTOR:`. **Exemption:** doc-only / config-only / pure-rename stages with no observable runtime behaviour may omit RED/GREEN by declaring one line `TDD exemption: <reason>` in the stage section (mirrors the executor's per-step exemption in `_implementation-executor.md`). Validator S10c enforces RED-first + GREEN **and** that the `RED:` step's `expected` reads FAIL / the `GREEN:` step's reads PASS; S10e rejects a `TDD exemption:` whose reason is not one of doc-only / config-only / pure-rename (both in `validators/validate-implementation-plan-stages.py`).
|
|
82
82
|
- **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
|
|
83
83
|
- `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — a Tier3 verification script that proves this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. When you emit this line you MUST also (a) write the script to `<task_root>/qa/scripts/stage-<N>.<ext>` and (b) add a matching entry to `<task_root>/qa/conformance-manifest.json` with fields `stageKey` (= `<task-id>-stage-<N>`), `script`, `runCommand`, `requirementIds`, `requires` (subset of `{db, io, http, external}`), `passContract`, `exemption: null`, `waiver: null`. The script's standard interface: a `main` that exits `0`=PASS / non-zero=FAIL, and whose stdout ends with `QA-RESULT: PASS|FAIL` followed by one `REQ <id>: PASS|FAIL: <근거>` line per requirement. When the verification body is a test spec, author it with the project's own test framework (devDependency) invoked via a discovery override at `<task_root>/qa/scripts/` (jest: `--config <project config> --roots <task_root>/qa/scripts`) — never hand-roll `describe`/`expect` and never widen the project's own test config; for TypeScript specs also write `<task_root>/qa/scripts/tsconfig.json` extending the project tsconfig with the runner's `types` entry so editors resolve the file.
|
|
84
84
|
- `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.)
|
|
85
|
-
The manifest lives at the **task level** (`<task_root>/qa/`, path token `TASK_QA_PATH`) and is shared across planning → implementation → final-verification. Layout split: executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at
|
|
85
|
+
The manifest lives at the **task level** (`<task_root>/qa/`, path token `TASK_QA_PATH`) and is shared across planning → implementation → final-verification. Layout split: executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `validators/validate-run.py` `_validate_planning_conformance_declared` fails when a stage that declared `Conformance tests:` has no matching `-stage-<N>` entry in the shared manifest (a declaration that was never materialized); the manifest JSON structure — including each entry's `script` living under `qa/scripts/` — is enforced by `validate_conformance_manifest` (called from both the run path and validate-run); and the result gate (each script's `QA-RESULT`) is enforced by the verifier Tier3 + validate-run.
|
|
86
86
|
- `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
|
|
87
87
|
- `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
|
|
88
88
|
- **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 8, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal.
|
|
@@ -101,13 +101,13 @@
|
|
|
101
101
|
- **recommendedNextSteps 정책:** cross-project 선행/후속의 substance 는 `crossProjectDependencies` 에 두고, `§3 Recommended Next Steps` 에는 그 섹션(`§5.4 Cross-Project Dependencies`)을 가리키는 포인터만 둔다 — 이중 기록 금지.
|
|
102
102
|
- validation checklist (pre / mid / post) — each item is an exact command or observable outcome
|
|
103
103
|
- rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
|
|
104
|
-
- **Requirement Coverage (mandatory, §5.5.8):** one row per concrete requirement from the task brief / packet. Assign stable IDs `R-001`, `R-002`, ... in source order. Columns: `ID | Source | Requirement | Covered by option / stage / step | Status`. `Source` cites the brief heading or file/line where the requirement came from. `Covered by` must name the specific Option Candidate and Stage/Step that satisfies it, not just "recommended option". `Status` is one of `covered`, `gap`, or `blocked C-NNN`. If any row is `gap` or `blocked C-NNN`, the Plan Body Verification gate MUST NOT be `passed` / `passed-with-dissent`; add a matching `Blocks=approval` row for the blocker and keep `approved: false`.
|
|
104
|
+
- **Requirement Coverage (mandatory, §5.5.8):** one row per concrete requirement from the task brief / packet. Assign stable IDs `R-001`, `R-002`, ... in source order. Columns: `ID | Source | Requirement | Covered by option / stage / step | Status`. `Source` cites the brief heading or file/line where the requirement came from. `Covered by` must name the specific Option Candidate and Stage/Step that satisfies it, not just "recommended option". **Enforced:** `validators/validate-run.py` `_validate_requirement_coverage_covered_by` fails a `covered` row whose `coveredBy` is bare "recommended option", names no Option/Stage/Step anchor, or cites a Stage number absent from the Stage Map (whether the cited step *actually satisfies* the requirement remains a worker `DISAGREE(f)` judgment). `Status` is one of `covered`, `gap`, or `blocked C-NNN`. If any row is `gap` or `blocked C-NNN`, the Plan Body Verification gate MUST NOT be `passed` / `passed-with-dissent`; add a matching `Blocks=approval` row for the blocker and keep `approved: false`.
|
|
105
105
|
- **Review-rule compliance plan:** when a project-local review rule pack is found, each Option Candidate MUST include the design implication of those rules in its File Structure / interfaces / blast-radius notes. For any helper or data transform used by more than one changed service, the plan must either place it in a shared module or explicitly justify why duplication is intentional. For any test step, the plan must state the observable behavior being asserted, not the internal collaborator call being pinned. For any exported/public method added or renamed, the step must carry the intended noun/side-effect semantics so implementation names can be reviewed before code is written.
|
|
106
106
|
- the YAML frontmatter MUST include the line `approved: false` (report-writer always emits the unflipped value). The user authorises the next `implementation` run by flipping it to `approved: true` (manual edit or `--approve` CLI). Do NOT recreate any `User Approval Request` body block — the validator fails reports that contain one (see `validators/validate-run.py` deprecated patterns).
|
|
107
107
|
- the YAML frontmatter MUST include the line `implementation-option:` directly under `approved:` (report-writer always emits it with an **empty value**). The user selects which Option Candidate the next `implementation` run executes by filling this line with that option's name (manual edit or `--implementation-option <name>` CLI). When left empty, the `implementation` run falls back to the `Recommended Option`.
|
|
108
108
|
- **the frontmatter `approved: false` line is rendered unconditionally; if the plan-body verification gate (§5.5.9) returns `blocked-by-disagreement` or `aborted-non-result`, the writer MUST keep `approved: false` and the validator refuses any report that ships with `approved: true` under such a gate result.**
|
|
109
109
|
- every ambiguity flagged during pre-planning that the user must resolve before approval registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under the implementation plan body — the unified table is the single home)
|
|
110
|
-
- **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (Option Candidates / Trade-off Matrix / Recommended Option / Stage Map and per-stage sections / Dependency / Validation Checklist / Rollback / Requirement Coverage) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `prompts/lead/convergence.md` "Plan-body verification mode". The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture").
|
|
110
|
+
- **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead MUST run a worker peer-review round on the consolidated plan body (Option Candidates / Trade-off Matrix / Recommended Option / Stage Map and per-stage sections / Dependency / Validation Checklist / Rollback / Requirement Coverage) and populate `### 5.5.9 Plan Body Verification` in the final report. The round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics, gate-result classification, and dissent log format are defined in `prompts/lead/convergence.md` "Plan-body verification mode". The four gate-result values are `passed`, `passed-with-dissent`, `blocked-by-disagreement`, `aborted-non-result`. When the gate would have been `blocked-by-disagreement` or `aborted-non-result`, the lead MUST NOT silently flip it to one of the passing values to "unblock" the run — that is a contract violation. **Enforced:** `validators/validate-run.py` `_validate_plan_body_gate_recompute` re-derives the gate from `planItems[].verdicts` and fails when the declared `gateResult` claims a healthier outcome than the recorded votes support; `_validate_plan_item_extraction_completeness` fails when any plan-body deliverable category is under-extracted into `planItems`, so a dropped item can no longer dodge the gate. When `convergence.adversarial=true` (the default for this phase), this round uses the adversarial posture — verifiers confirm cited paths/commands and the burden of proof is on the plan — but the gate threshold stays `majority-disagree` (see that skill's §"Adversarial plan-body posture").
|
|
111
111
|
- **Decision-record evaluation (sole owner)**: this phase is the **single owner** of decision-record evaluation in the okstra lifecycle. The brief never evaluates or drafts decision records — it only forwards `adr-candidate:*` signals. Every `adr-candidate:*` entry inherited from the brief's `Open Questions` is a mandatory evaluation target. In addition, evaluate every decision the recommended option introduces against the three criteria:
|
|
112
112
|
1. **Hard to reverse** — would changing the decision later cost meaningfully more than deciding now?
|
|
113
113
|
2. **Surprising without context** — would a future reader, seeing only the code, wonder "why was it built this way?"?
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 1. Clarification Items` table as a `Blocks=approval` row.
|
|
128
128
|
5. **Scope check** — if the recommended plan now spans multiple independent subsystems, recommend splitting into separate planning runs rather than shipping an oversized plan.
|
|
129
129
|
6. **Review-rule preflight check** — if a project review rule pack exists, map each relevant rule to the recommended option. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
|
|
130
|
-
7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).**
|
|
130
|
+
7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** For every §5.5.9 `planItems[]` entry whose verdicts make it `majority-disagree`, set that item's `clarificationId` to a `C-<N>` row that MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do NOT create a parallel `Open Questions` block under the implementation plan body — the unified table is the single home. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item has no `clarificationId`, or its `clarificationId` is dangling / points at a non-`approval` row. For `partial-consensus` and `dissent-isolated` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
|
|
131
131
|
8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 8. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
|
|
132
132
|
9. **Cross-project dependency check** — 타 repo / 다른 top-level 배포 모듈 / published 패키지에 대한 의존을 빠뜨리지 않았는지 확인한다. `dependencyMigrationRisk` 에 `kind: cross-project` 행이 있으면 매칭되는 `direction: upstream-precondition` `XP-NNN` 행이 `crossProjectDependencies` 에 있고, 그 `requiredWork` 가 추상 표현("상대 작업 완료")이 아니라 상대가 실제로 만들어야 할 구체 작업인지 reviewer 로서 다시 읽는다(validator S 가 존재만 보므로 구체성은 self-review 가 책임짐). cross-repo 작업을 한 task 의 stage 로 욱여넣지 않고 별도 run + XP 행으로 분리했는지, cross-project substance 가 `§3 Recommended Next Steps` 에 중복되지 않고 `§5.4 Cross-Project Dependencies` 에만 있는지 확인한다.
|
|
133
133
|
10. **Decision-draft materialization check** — `decisionDrafts` 가 비어있지 않으면, 매칭 materialization step(`.okstra/decisions/<NNNN>-<slug>.md` 생성)이 어느 stage 의 stepwise 에 존재하는지, draft 개수와 materialization step 이 1:1 로 대응하는지 reviewer 로서 확인한다. validator 는 step 의 *존재*만 보므로 `<NNNN>-<slug>` 정확성·개수 대응은 self-review 가 책임진다.
|
|
@@ -1480,6 +1480,12 @@
|
|
|
1480
1480
|
"id": { "type": "string", "minLength": 1 },
|
|
1481
1481
|
"subject": { "type": "string", "minLength": 1 },
|
|
1482
1482
|
"sourceSection": { "type": "string" },
|
|
1483
|
+
"clarificationId": {
|
|
1484
|
+
"anyOf": [
|
|
1485
|
+
{ "type": "null" },
|
|
1486
|
+
{ "type": "string", "pattern": "^C-\\d{3,}$" }
|
|
1487
|
+
]
|
|
1488
|
+
},
|
|
1483
1489
|
"verdicts": {
|
|
1484
1490
|
"type": "array",
|
|
1485
1491
|
"items": {
|
|
@@ -341,7 +341,7 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
341
341
|
{{ t("implementationPlanning.planBodyBreakageLegend") }}
|
|
342
342
|
|
|
343
343
|
{% for item in implementationPlanning.planBodyVerification.planItems %}
|
|
344
|
-
**{{ item.id }} — {{ item.subject }}**{% if item.sourceSection %} ({{ t("implementationPlanning.planBodySourceLabel") }} {{ item.sourceSection }}){% endif %}
|
|
344
|
+
**{{ item.id }} — {{ item.subject }}**{% if item.sourceSection %} ({{ t("implementationPlanning.planBodySourceLabel") }} {{ item.sourceSection }}){% endif %}{% if item.clarificationId %} — {{ t("implementationPlanning.planBodyBlockerLabel") }} {{ item.clarificationId }}{% endif %}
|
|
345
345
|
|
|
346
346
|
|
|
347
347
|
| Worker | Verdict | Breakage kind | Note |
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"planBodyVerdictLegend": "Verdict — **AGREE**: executable as written and internally consistent with other items · **SUPPLEMENT**: item is sound but a dependency / edge case / precondition is missing · **DISAGREE**: has a defect (see Breakage kind) · **verification-error**: the worker produced no result.",
|
|
96
96
|
"planBodyBreakageLegend": "Breakage kind — a: cited file path/symbol mismatches another step or option · b: command is not executable or is ambiguous · c: validation signal is not observable · d: rollback violates commit/dependency order · e: contradicts the trade-off matrix · f: requirement-coverage row does not map to an option/stage/step that actually satisfies the requirement. (`--` = not applicable)",
|
|
97
97
|
"planBodySourceLabel": "source §",
|
|
98
|
+
"planBodyBlockerLabel": "blocks approval →",
|
|
98
99
|
"optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
|
|
99
100
|
"optionBlastRadiusLabel": "Blast radius estimate",
|
|
100
101
|
"recommendedTableHeaderLabel": "Item",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"planBodyVerdictLegend": "판정(Verdict) — **AGREE**: 적힌 대로 실행 가능하고 다른 항목과 내부적으로 일관됨 · **SUPPLEMENT**: 항목 자체는 타당하나 의존성·엣지케이스·전제조건이 누락됨 · **DISAGREE**: 결함이 있음(결함 유형 참조) · **verification-error**: 워커 검증이 결과를 내지 못함.",
|
|
96
96
|
"planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음)",
|
|
97
97
|
"planBodySourceLabel": "출처 §",
|
|
98
|
+
"planBodyBlockerLabel": "승인 차단 →",
|
|
98
99
|
"optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
|
|
99
100
|
"optionBlastRadiusLabel": "폭발 반경 추정",
|
|
100
101
|
"recommendedTableHeaderLabel": "항목",
|
|
@@ -35,6 +35,8 @@ Checks performed per brief file:
|
|
|
35
35
|
(`scripts/okstra_ctl/improvement_lenses.py` SSOT).
|
|
36
36
|
12. When present, `Related Task Graph` is a markdown table with the canonical
|
|
37
37
|
columns and relation/direction values from the okstra-brief contract.
|
|
38
|
+
13. The requirement/objective section `## Desired Outcome` (required in every
|
|
39
|
+
brief variant) exists and its body is not blank. `_(none)_` stays valid.
|
|
38
40
|
|
|
39
41
|
Exit code 0 on PASS, 1 on FAIL.
|
|
40
42
|
"""
|
|
@@ -391,6 +393,39 @@ def check_related_task_graph(text: str, errors: list[str]) -> None:
|
|
|
391
393
|
)
|
|
392
394
|
|
|
393
395
|
|
|
396
|
+
REQUIREMENT_SECTION = "Desired Outcome"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def has_section_heading(text: str, heading: str) -> bool:
|
|
400
|
+
"""True when a `## <heading>` line exists (distinct from an empty body)."""
|
|
401
|
+
pattern = re.compile(r"^##\s+" + re.escape(heading) + r"\s*$", re.MULTILINE)
|
|
402
|
+
return pattern.search(text) is not None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def check_requirement_section(text: str, errors: list[str]) -> None:
|
|
406
|
+
"""The requirement/objective section must exist and carry a non-blank body.
|
|
407
|
+
|
|
408
|
+
`## Desired Outcome` is the "shape of success" required by every brief
|
|
409
|
+
variant (okstra-brief SKILL.md Step 5 §"Required sections by variant").
|
|
410
|
+
Without this check a brief that drops the heading — or wipes its body —
|
|
411
|
+
still passes, letting implementation-planning report 100% Requirement
|
|
412
|
+
Coverage against an empty objective. `_(none)_` stays valid: it is the
|
|
413
|
+
contract's explicit placeholder for a deliberately-empty gap (SKILL.md
|
|
414
|
+
Step 4 stop conditions), not a missing requirement.
|
|
415
|
+
"""
|
|
416
|
+
if not has_section_heading(text, REQUIREMENT_SECTION):
|
|
417
|
+
errors.append(
|
|
418
|
+
f"required section '## {REQUIREMENT_SECTION}' is missing "
|
|
419
|
+
"(every brief variant must state the shape of success)"
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
if not section_body(text, REQUIREMENT_SECTION).strip():
|
|
423
|
+
errors.append(
|
|
424
|
+
f"required section '## {REQUIREMENT_SECTION}' has an empty body "
|
|
425
|
+
"(use _(none)_ only for a deliberately-empty gap)"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
394
429
|
def check_reporter_confirmations(
|
|
395
430
|
rc_status: str | None, reporter_rows: list[str], errors: list[str]
|
|
396
431
|
) -> None:
|
|
@@ -452,6 +487,8 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
|
|
|
452
487
|
|
|
453
488
|
check_related_task_graph(text, errors)
|
|
454
489
|
|
|
490
|
+
check_requirement_section(text, errors)
|
|
491
|
+
|
|
455
492
|
# 2. brief-id matches filename stem
|
|
456
493
|
stem = path.stem
|
|
457
494
|
if fm.get("brief-id") and fm["brief-id"] != stem:
|
|
@@ -185,7 +185,10 @@ def _check_each_stage_section(text: str, stages: List[StageMeta]) -> List[Valida
|
|
|
185
185
|
_LABEL_PREFIX = r"^\s*(?:[-*]\s+)?(?:\*\*)?"
|
|
186
186
|
SLICE_VALUE = re.compile(_LABEL_PREFIX + r"Slice value\s*:\s*(?:\*\*)?\s*(.+?)\s*$", re.M)
|
|
187
187
|
ACCEPTANCE = re.compile(_LABEL_PREFIX + r"Acceptance\s*:\s*(?:\*\*)?\s*(.+?)\s*$", re.M)
|
|
188
|
-
TDD_EXEMPTION = re.compile(_LABEL_PREFIX + r"TDD exemption\s*:\s*(?:\*\*)?\s
|
|
188
|
+
TDD_EXEMPTION = re.compile(_LABEL_PREFIX + r"TDD exemption\s*:\s*(?:\*\*)?\s*(.+?)\s*$", re.M)
|
|
189
|
+
# Profile implementation-planning.md:81 limits the exemption to these three
|
|
190
|
+
# categories; any other reason (e.g. "refactor") must not waive RED/GREEN.
|
|
191
|
+
TDD_EXEMPTION_ALLOWED = ("doc-only", "config-only", "pure-rename")
|
|
189
192
|
TEST_CASE_CATEGORIES = ("success", "boundary", "failure")
|
|
190
193
|
TEST_CASE = {
|
|
191
194
|
cat: re.compile(
|
|
@@ -197,17 +200,32 @@ CONFORMANCE_TESTS = re.compile(_LABEL_PREFIX + r"Conformance tests\s*:\s*(?:\*\*
|
|
|
197
200
|
CONFORMANCE_EXEMPTION = re.compile(_LABEL_PREFIX + r"Conformance exemption\s*:\s*(?:\*\*)?\s*\S", re.M)
|
|
198
201
|
|
|
199
202
|
|
|
203
|
+
def _exemption_reason_allowed(section: str) -> bool:
|
|
204
|
+
"""True when a `TDD exemption:` line is present AND its reason names an
|
|
205
|
+
allowed category (doc-only / config-only / pure-rename, case-insensitive).
|
|
206
|
+
A present-but-unlisted reason returns False so S10e can reject it."""
|
|
207
|
+
m = TDD_EXEMPTION.search(section)
|
|
208
|
+
if not m:
|
|
209
|
+
return False
|
|
210
|
+
reason = m.group(1).lower()
|
|
211
|
+
return any(cat in reason for cat in TDD_EXEMPTION_ALLOWED)
|
|
212
|
+
|
|
213
|
+
|
|
200
214
|
def _check_slice_tdd(text: str, stages: List[StageMeta]) -> List[ValidationError]:
|
|
201
215
|
"""S10: each stage declares a vertical slice and follows RED→GREEN ordering.
|
|
202
216
|
|
|
203
217
|
S10a — `Slice value:` line with a non-empty value.
|
|
204
218
|
S10b — `Acceptance:` line with a non-empty value.
|
|
205
|
-
S10c — first effective Stepwise step's action starts with `RED:`
|
|
206
|
-
action starts with `GREEN
|
|
219
|
+
S10c — first effective Stepwise step's action starts with `RED:` (its
|
|
220
|
+
`expected` cell reading FAIL) AND some action starts with `GREEN:`
|
|
221
|
+
(its `expected` cell reading PASS), OR a valid `TDD exemption:` line.
|
|
207
222
|
S10d — stage declares all three `Test case (success|boundary|failure):`
|
|
208
|
-
lines with non-empty values, OR a `TDD exemption:` line
|
|
223
|
+
lines with non-empty values, OR a valid `TDD exemption:` line.
|
|
209
224
|
Forces the plan to address the happy path, edge/boundary inputs, and
|
|
210
225
|
failure/negative inputs instead of a single acceptance assertion.
|
|
226
|
+
S10e — a present `TDD exemption:` line's reason must name an allowed
|
|
227
|
+
category (doc-only / config-only / pure-rename); an arbitrary reason
|
|
228
|
+
must not waive RED/GREEN.
|
|
211
229
|
"""
|
|
212
230
|
errs: List[ValidationError] = []
|
|
213
231
|
for s in stages:
|
|
@@ -223,6 +241,11 @@ def _check_slice_tdd(text: str, stages: List[StageMeta]) -> List[ValidationError
|
|
|
223
241
|
"S10b: 'Acceptance:' line missing or empty"))
|
|
224
242
|
|
|
225
243
|
if TDD_EXEMPTION.search(section):
|
|
244
|
+
if not _exemption_reason_allowed(section):
|
|
245
|
+
errs.append(ValidationError("S10", s.stage_number,
|
|
246
|
+
"S10e: 'TDD exemption:' reason must be one of "
|
|
247
|
+
"doc-only / config-only / pure-rename — an arbitrary reason "
|
|
248
|
+
"cannot waive RED/GREEN"))
|
|
226
249
|
continue
|
|
227
250
|
|
|
228
251
|
missing_cases = [
|
|
@@ -234,14 +257,34 @@ def _check_slice_tdd(text: str, stages: List[StageMeta]) -> List[ValidationError
|
|
|
234
257
|
"line(s) — declare a non-empty success, boundary, and failure "
|
|
235
258
|
"case each, or add a 'TDD exemption:' line"))
|
|
236
259
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
errs.extend(_check_red_green_steps(section, s.stage_number))
|
|
261
|
+
return errs
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationError]:
|
|
265
|
+
"""S10c: RED-first + later GREEN, with each step's `expected` cell matching
|
|
266
|
+
its prefix (RED → FAIL, GREEN → PASS). Only certain violations fire."""
|
|
267
|
+
rows = _effective_step_rows(section)
|
|
268
|
+
steps = [(r[1], r[4]) for r in rows if len(r) > 4]
|
|
269
|
+
actions = [a for a, _ in steps]
|
|
270
|
+
first_is_red = bool(actions) and actions[0].startswith("RED:")
|
|
271
|
+
has_green = any(a.startswith("GREEN:") for a in actions)
|
|
272
|
+
errs: List[ValidationError] = []
|
|
273
|
+
if not (first_is_red and has_green):
|
|
274
|
+
errs.append(ValidationError("S10", stage_number,
|
|
275
|
+
"S10c: first step action must start with 'RED:' and some "
|
|
276
|
+
"step action with 'GREEN:', or add a 'TDD exemption:' line"))
|
|
277
|
+
return errs
|
|
278
|
+
for action, expected in steps:
|
|
279
|
+
exp = expected.upper()
|
|
280
|
+
if action.startswith("RED:") and "FAIL" not in exp:
|
|
281
|
+
errs.append(ValidationError("S10", stage_number,
|
|
282
|
+
"S10c: 'RED:' step's expected cell must read FAIL, got "
|
|
283
|
+
f"'{expected}'"))
|
|
284
|
+
elif action.startswith("GREEN:") and "PASS" not in exp:
|
|
285
|
+
errs.append(ValidationError("S10", stage_number,
|
|
286
|
+
"S10c: 'GREEN:' step's expected cell must read PASS, got "
|
|
287
|
+
f"'{expected}'"))
|
|
245
288
|
return errs
|
|
246
289
|
|
|
247
290
|
|
|
@@ -861,6 +861,57 @@ def _task_root_from_run_dir(run_dir: Path) -> Path:
|
|
|
861
861
|
return run_dir.parent.parent
|
|
862
862
|
|
|
863
863
|
|
|
864
|
+
def _validate_planning_conformance_declared(report_path: Path, failures: list[str]) -> None:
|
|
865
|
+
"""H4-c — at planning time, every stage that DECLARES `Conformance tests:`
|
|
866
|
+
must already carry a matching entry in the shared task-level
|
|
867
|
+
`qa/conformance-manifest.json`. The profile mandates writing the script +
|
|
868
|
+
manifest entry as part of emitting that line
|
|
869
|
+
(implementation-planning.md:83/85); without this check a declaration that
|
|
870
|
+
was never materialized only surfaces much later at the `implementation`
|
|
871
|
+
entry gate. Matches by stageKey suffix (`-stage-<N>`) like
|
|
872
|
+
`_scope_manifest_entries`; skips stages that took a `Conformance exemption:`.
|
|
873
|
+
"""
|
|
874
|
+
data_path = report_path.with_suffix(".data.json")
|
|
875
|
+
if not data_path.is_file():
|
|
876
|
+
return
|
|
877
|
+
try:
|
|
878
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
879
|
+
except (OSError, json.JSONDecodeError):
|
|
880
|
+
return
|
|
881
|
+
ip = data.get("implementationPlanning")
|
|
882
|
+
if not isinstance(ip, dict):
|
|
883
|
+
return
|
|
884
|
+
declaring_stages = [
|
|
885
|
+
s.get("stage")
|
|
886
|
+
for s in (ip.get("stages") or [])
|
|
887
|
+
if isinstance(s, dict) and str(s.get("conformanceTests") or "").strip()
|
|
888
|
+
]
|
|
889
|
+
if not declaring_stages:
|
|
890
|
+
return
|
|
891
|
+
task_root = _task_root_from_run_dir(report_path.parent.parent)
|
|
892
|
+
manifest_path = task_root / "qa" / "conformance-manifest.json"
|
|
893
|
+
entries = []
|
|
894
|
+
if manifest_path.is_file():
|
|
895
|
+
try:
|
|
896
|
+
entries = json.loads(manifest_path.read_text()).get("entries") or []
|
|
897
|
+
except (OSError, json.JSONDecodeError):
|
|
898
|
+
entries = []
|
|
899
|
+
for stage_number in declaring_stages:
|
|
900
|
+
suffix = f"-stage-{stage_number}"
|
|
901
|
+
has_entry = any(
|
|
902
|
+
isinstance(e, dict) and str(e.get("stageKey") or "").endswith(suffix)
|
|
903
|
+
for e in entries
|
|
904
|
+
)
|
|
905
|
+
if not has_entry:
|
|
906
|
+
failures.append(
|
|
907
|
+
f"final-report data.json: stage {stage_number} declares "
|
|
908
|
+
"`Conformance tests:` but no matching entry exists in "
|
|
909
|
+
f"{manifest_path} (stageKey ending `{suffix}`). Emitting the "
|
|
910
|
+
"declaration MUST also write the manifest entry + script "
|
|
911
|
+
"(implementation-planning.md §Conformance)."
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
|
|
864
915
|
def _validate_conformance(report_path: Path, failures: list[str],
|
|
865
916
|
surface_patterns: object = None) -> None:
|
|
866
917
|
"""Tier 3 conformance 게이트(implementation / final-verification).
|
|
@@ -1483,6 +1534,11 @@ def validate_final_report_data(report_path: Path, failures: list[str]) -> None:
|
|
|
1483
1534
|
elif task_type == "implementation-planning":
|
|
1484
1535
|
_validate_implementation_planning_cross_project(data, failures)
|
|
1485
1536
|
_validate_implementation_planning_decision_drafts(data, failures)
|
|
1537
|
+
_validate_plan_body_gate_recompute(data, failures)
|
|
1538
|
+
_validate_plan_item_extraction_completeness(data, failures)
|
|
1539
|
+
_validate_plan_item_subject_substance(data, failures)
|
|
1540
|
+
_validate_plan_body_clarification_matching(data, failures)
|
|
1541
|
+
_validate_requirement_coverage_covered_by(data, failures)
|
|
1486
1542
|
|
|
1487
1543
|
|
|
1488
1544
|
# path:line (foo.service.ts:268), report ID (C-001 / F-013 / G-crit-002 / RC-1),
|
|
@@ -1584,6 +1640,302 @@ def _validate_implementation_planning_decision_drafts(data: dict, failures: list
|
|
|
1584
1640
|
)
|
|
1585
1641
|
|
|
1586
1642
|
|
|
1643
|
+
# Plan-body gate outcomes ranked by how favorable each is to approval.
|
|
1644
|
+
# A higher rank claims a healthier verification result. The recompute check
|
|
1645
|
+
# below fails only when the *declared* gate outranks what the recorded
|
|
1646
|
+
# per-worker verdicts support — i.e. the lead claimed a better outcome than
|
|
1647
|
+
# the votes justify. A lead writing a conservatively *worse* gate is allowed,
|
|
1648
|
+
# so genuine edge cases in this recompute never manufacture false failures.
|
|
1649
|
+
_PLAN_GATE_RANK = {
|
|
1650
|
+
"aborted-non-result": 0,
|
|
1651
|
+
"blocked-by-disagreement": 0,
|
|
1652
|
+
"passed-with-dissent": 1,
|
|
1653
|
+
"passed": 2,
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
# Breakage kinds where a single DISAGREE blocks the gate on its own (no majority
|
|
1657
|
+
# needed), because the defect is concrete, safety-critical, and adversarially
|
|
1658
|
+
# verifiable: `a` = cited path/symbol mismatch, `d` = rollback violates
|
|
1659
|
+
# commit/dependency order. `b`/`c`/`e` still need a majority — `b` in particular
|
|
1660
|
+
# is prone to planning-vs-implementation environment false positives.
|
|
1661
|
+
_SINGLE_VOTE_BLOCKING_KINDS = {"a", "d"}
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def _classify_plan_item_gate(item: dict) -> str:
|
|
1665
|
+
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
1666
|
+
per `prompts/lead/convergence.md` "Round protocol". Returns one of
|
|
1667
|
+
``majority-disagree`` / ``has-dissent`` / ``full-consensus`` /
|
|
1668
|
+
``all-non-result``. Collapses ``partial-consensus`` and
|
|
1669
|
+
``dissent-isolated`` into ``has-dissent`` because they resolve to the
|
|
1670
|
+
same gate value; only the majority-disagree boundary changes the gate.
|
|
1671
|
+
"""
|
|
1672
|
+
tokens = [
|
|
1673
|
+
(
|
|
1674
|
+
str(v.get("verdict") or "").strip().upper(),
|
|
1675
|
+
str(v.get("breakageKind") or "").strip().lower(),
|
|
1676
|
+
)
|
|
1677
|
+
for v in (item.get("verdicts") or [])
|
|
1678
|
+
if isinstance(v, dict)
|
|
1679
|
+
]
|
|
1680
|
+
non_error = [(vd, bk) for (vd, bk) in tokens if vd and vd != "VERIFICATION-ERROR"]
|
|
1681
|
+
if not non_error:
|
|
1682
|
+
return "all-non-result"
|
|
1683
|
+
disagree = [(vd, bk) for (vd, bk) in non_error if vd == "DISAGREE"]
|
|
1684
|
+
agree = [(vd, bk) for (vd, bk) in non_error if vd in ("AGREE", "SUPPLEMENT")]
|
|
1685
|
+
disagree_kinds = {bk for (_vd, bk) in disagree if bk}
|
|
1686
|
+
if not disagree:
|
|
1687
|
+
return "full-consensus"
|
|
1688
|
+
# Single-vote-blocking kinds: one confirmed DISAGREE on a concrete,
|
|
1689
|
+
# safety-critical, adversarially-verifiable defect is enough to block, even
|
|
1690
|
+
# in a two-worker roster — a lone correct dissent must not be outvoted here.
|
|
1691
|
+
# `a`/`d` for any item; `f` only for P-Req items (requirement coverage).
|
|
1692
|
+
is_req = str(item.get("id") or "").upper().startswith("P-REQ")
|
|
1693
|
+
if disagree_kinds & _SINGLE_VOTE_BLOCKING_KINDS or (is_req and "f" in disagree_kinds):
|
|
1694
|
+
return "majority-disagree"
|
|
1695
|
+
# Otherwise a genuine majority is required — and a majority needs at least
|
|
1696
|
+
# two participating votes, so a lone surviving DISAGREE (its peer returned a
|
|
1697
|
+
# non-result) does NOT block. That fixes the paradox where a worker failure
|
|
1698
|
+
# made the gate stricter than a healthy roster would.
|
|
1699
|
+
if len(non_error) >= 2 and len(disagree) > len(agree):
|
|
1700
|
+
return "majority-disagree"
|
|
1701
|
+
return "has-dissent"
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def _recompute_plan_body_gate(pbv: dict) -> str | None:
|
|
1705
|
+
"""Recompute the whole §5.5.9 gate value from ``planItems[].verdicts``.
|
|
1706
|
+
Returns a value in ``PLAN_VERIFY_GATE_VALUES`` or ``None`` when there are
|
|
1707
|
+
no plan items to judge (disabled / empty round)."""
|
|
1708
|
+
classes = [
|
|
1709
|
+
_classify_plan_item_gate(it)
|
|
1710
|
+
for it in (pbv.get("planItems") or [])
|
|
1711
|
+
if isinstance(it, dict)
|
|
1712
|
+
]
|
|
1713
|
+
if not classes:
|
|
1714
|
+
return None
|
|
1715
|
+
if all(c == "all-non-result" for c in classes):
|
|
1716
|
+
return "aborted-non-result"
|
|
1717
|
+
if any(c == "majority-disagree" for c in classes):
|
|
1718
|
+
return "blocked-by-disagreement"
|
|
1719
|
+
if any(c == "has-dissent" for c in classes):
|
|
1720
|
+
return "passed-with-dissent"
|
|
1721
|
+
return "passed"
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def _validate_plan_body_gate_recompute(data: dict, failures: list[str]) -> None:
|
|
1725
|
+
"""H1 — the declared `Gate result` must not claim a healthier outcome than
|
|
1726
|
+
the recorded per-worker verdicts support. Closes the forgery hole where a
|
|
1727
|
+
lead writes `gateResult: passed` while workers actually voted DISAGREE:
|
|
1728
|
+
the verdicts live in `planItems[].verdicts`, so the gate is recomputable
|
|
1729
|
+
and no longer depends on the lead's honesty alone.
|
|
1730
|
+
"""
|
|
1731
|
+
ip = data.get("implementationPlanning")
|
|
1732
|
+
if not isinstance(ip, dict):
|
|
1733
|
+
return
|
|
1734
|
+
pbv = ip.get("planBodyVerification")
|
|
1735
|
+
if not isinstance(pbv, dict):
|
|
1736
|
+
return
|
|
1737
|
+
declared = str(pbv.get("gateResult") or "").strip().lower()
|
|
1738
|
+
recomputed = _recompute_plan_body_gate(pbv)
|
|
1739
|
+
if recomputed is None or declared not in _PLAN_GATE_RANK:
|
|
1740
|
+
return
|
|
1741
|
+
if _PLAN_GATE_RANK[declared] > _PLAN_GATE_RANK[recomputed]:
|
|
1742
|
+
failures.append(
|
|
1743
|
+
"final-report data.json: implementationPlanning.planBodyVerification "
|
|
1744
|
+
f"`gateResult` is `{declared}` but the recorded planItems[].verdicts "
|
|
1745
|
+
f"only support `{recomputed}` (a majority DISAGREE, a DISAGREE(f) on "
|
|
1746
|
+
"a P-Req item, or all-non-result dispatches were recorded). The gate "
|
|
1747
|
+
"value must honestly aggregate the worker votes — do not upgrade it "
|
|
1748
|
+
"to unblock the run (convergence.md Round protocol)."
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
# Each plan-body deliverable array and the P-* verdict-item prefix it must be
|
|
1753
|
+
# extracted into for §5.5.9 cross-verification (convergence.md Plan-item
|
|
1754
|
+
# extraction). A weak item dropped from planItems escapes the gate entirely.
|
|
1755
|
+
_PLAN_ITEM_SOURCES = (
|
|
1756
|
+
("optionCandidates", "P-Opt", "Option Candidates (§4.5.1)"),
|
|
1757
|
+
("stepwiseExecution", "P-Step", "Stepwise Execution Order (§4.5.4)"),
|
|
1758
|
+
("dependencyMigrationRisk", "P-Dep", "Dependency / Migration Risk (§4.5.5)"),
|
|
1759
|
+
("validationChecklist", "P-Val", "Validation Checklist (§4.5.6)"),
|
|
1760
|
+
("rollbackStrategy", "P-Rb", "Rollback Strategy (§4.5.7)"),
|
|
1761
|
+
("requirementCoverage", "P-Req", "Requirement Coverage (§4.5.8)"),
|
|
1762
|
+
)
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
def _validate_plan_item_extraction_completeness(data: dict, failures: list[str]) -> None:
|
|
1766
|
+
"""H2 — when a verification round ran, every plan-body deliverable item must
|
|
1767
|
+
appear as a matching `P-*` verdict item. Closes the omission hole where the
|
|
1768
|
+
lead silently drops a weak item (e.g. an out-of-order rollback) from
|
|
1769
|
+
planItems so no worker ever verifies it, yet the gate still reads passed.
|
|
1770
|
+
Requires only that no category is *under-extracted*; over-extraction is fine.
|
|
1771
|
+
"""
|
|
1772
|
+
ip = data.get("implementationPlanning")
|
|
1773
|
+
if not isinstance(ip, dict):
|
|
1774
|
+
return
|
|
1775
|
+
pbv = ip.get("planBodyVerification")
|
|
1776
|
+
if not isinstance(pbv, dict):
|
|
1777
|
+
return
|
|
1778
|
+
round_count = pbv.get("roundCount")
|
|
1779
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
1780
|
+
return
|
|
1781
|
+
extracted_ids = [
|
|
1782
|
+
str(it.get("id") or "").upper()
|
|
1783
|
+
for it in (pbv.get("planItems") or [])
|
|
1784
|
+
if isinstance(it, dict)
|
|
1785
|
+
]
|
|
1786
|
+
for field, prefix, label in _PLAN_ITEM_SOURCES:
|
|
1787
|
+
source_count = len([r for r in (ip.get(field) or []) if isinstance(r, dict)])
|
|
1788
|
+
if source_count == 0:
|
|
1789
|
+
continue
|
|
1790
|
+
extracted = len(
|
|
1791
|
+
[i for i in extracted_ids if i.startswith(prefix.upper() + "-")]
|
|
1792
|
+
)
|
|
1793
|
+
if extracted < source_count:
|
|
1794
|
+
failures.append(
|
|
1795
|
+
f"final-report data.json: {label} has {source_count} item(s) but "
|
|
1796
|
+
f"planBodyVerification.planItems carries only {extracted} "
|
|
1797
|
+
f"`{prefix}-*` verdict item(s). Every plan-body item MUST be "
|
|
1798
|
+
"extracted for cross-verification — a dropped item escapes the "
|
|
1799
|
+
"gate (convergence.md Plan-item extraction)."
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
# A `subject` this short or shaped like a bare `P-Opt-1` id is a placeholder,
|
|
1804
|
+
# not the plain-language "what this item is" label §5.5.9 renders as a heading.
|
|
1805
|
+
_MIN_SUBJECT_LEN = 3
|
|
1806
|
+
_BARE_PLAN_ITEM_ID_RE = re.compile(r"^P-(?:Opt|Step|Dep|Val|Rb|Req)-\d+$", re.IGNORECASE)
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def _validate_plan_item_subject_substance(data: dict, failures: list[str]) -> None:
|
|
1810
|
+
"""H2 follow-up — `planItems[].subject` must be a real label, not a
|
|
1811
|
+
placeholder. The schema only enforces non-empty, so `"x"` or a copied
|
|
1812
|
+
`P-Opt-1` id would otherwise slip through and defeat the whole point of the
|
|
1813
|
+
subject (letting a reader see *what* each AGREE/DISAGREE is about).
|
|
1814
|
+
"""
|
|
1815
|
+
ip = data.get("implementationPlanning")
|
|
1816
|
+
if not isinstance(ip, dict):
|
|
1817
|
+
return
|
|
1818
|
+
pbv = ip.get("planBodyVerification")
|
|
1819
|
+
if not isinstance(pbv, dict):
|
|
1820
|
+
return
|
|
1821
|
+
for item in pbv.get("planItems") or []:
|
|
1822
|
+
if not isinstance(item, dict):
|
|
1823
|
+
continue
|
|
1824
|
+
item_id = str(item.get("id") or "").strip()
|
|
1825
|
+
subject = str(item.get("subject") or "").strip()
|
|
1826
|
+
if (
|
|
1827
|
+
len(subject) < _MIN_SUBJECT_LEN
|
|
1828
|
+
or subject == item_id
|
|
1829
|
+
or _BARE_PLAN_ITEM_ID_RE.match(subject)
|
|
1830
|
+
):
|
|
1831
|
+
failures.append(
|
|
1832
|
+
f"final-report data.json: plan item `{item_id or '<unknown>'}` has a "
|
|
1833
|
+
f"placeholder subject `{subject}`. Give a plain-language label of "
|
|
1834
|
+
"what the item is (e.g. 'Option A: upload v2 를 신규 모듈로 분리') so "
|
|
1835
|
+
"the §5.5.9 reader knows what each verdict is about "
|
|
1836
|
+
"(convergence.md Plan-item extraction)."
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
|
|
1840
|
+
def _validate_plan_body_clarification_matching(data: dict, failures: list[str]) -> None:
|
|
1841
|
+
"""H5 — every plan item that the recorded verdicts make `majority-disagree`
|
|
1842
|
+
must point (via `clarificationId`) at an existing `blocks: approval`
|
|
1843
|
+
clarification row. Closes the hole where a majority-disagree item blocks the
|
|
1844
|
+
gate but no §1 Clarification row is raised, so the user never sees why
|
|
1845
|
+
approval is withheld (convergence.md self-review step 7).
|
|
1846
|
+
"""
|
|
1847
|
+
ip = data.get("implementationPlanning")
|
|
1848
|
+
if not isinstance(ip, dict):
|
|
1849
|
+
return
|
|
1850
|
+
pbv = ip.get("planBodyVerification")
|
|
1851
|
+
if not isinstance(pbv, dict):
|
|
1852
|
+
return
|
|
1853
|
+
round_count = pbv.get("roundCount")
|
|
1854
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
1855
|
+
return
|
|
1856
|
+
clar_rows = [r for r in (data.get("clarificationItems") or []) if isinstance(r, dict)]
|
|
1857
|
+
all_ids = {r.get("id") for r in clar_rows if r.get("id")}
|
|
1858
|
+
approval_ids = {r.get("id") for r in clar_rows if r.get("blocks") == "approval" and r.get("id")}
|
|
1859
|
+
for item in pbv.get("planItems") or []:
|
|
1860
|
+
if not isinstance(item, dict):
|
|
1861
|
+
continue
|
|
1862
|
+
if _classify_plan_item_gate(item) != "majority-disagree":
|
|
1863
|
+
continue
|
|
1864
|
+
item_id = item.get("id") or "<unknown>"
|
|
1865
|
+
cid = item.get("clarificationId")
|
|
1866
|
+
if not cid:
|
|
1867
|
+
failures.append(
|
|
1868
|
+
f"final-report data.json: plan item `{item_id}` is majority-disagree "
|
|
1869
|
+
"but carries no `clarificationId`. A blocking disagreement MUST "
|
|
1870
|
+
"surface as a `## 1. Clarification Items` row (blocks=approval) so "
|
|
1871
|
+
"the user sees the blocker (convergence.md self-review step 7)."
|
|
1872
|
+
)
|
|
1873
|
+
elif cid not in approval_ids:
|
|
1874
|
+
reason = (
|
|
1875
|
+
"references a non-existent §1 row"
|
|
1876
|
+
if cid not in all_ids
|
|
1877
|
+
else "references a §1 row whose `blocks` is not `approval`"
|
|
1878
|
+
)
|
|
1879
|
+
failures.append(
|
|
1880
|
+
f"final-report data.json: plan item `{item_id}` (majority-disagree) "
|
|
1881
|
+
f"has clarificationId `{cid}` which {reason}. Every majority-disagree "
|
|
1882
|
+
"item MUST map 1:1 to a `blocks: approval` Clarification row."
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
_COVERED_BY_ANCHOR_RE = re.compile(r"option|stage|step", re.IGNORECASE)
|
|
1887
|
+
_COVERED_BY_STAGE_REF_RE = re.compile(r"stage\s*(\d+)", re.IGNORECASE)
|
|
1888
|
+
_COVERED_BY_VAGUE = {"recommended option", "the recommended option", "recommended"}
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _validate_requirement_coverage_covered_by(data: dict, failures: list[str]) -> None:
|
|
1892
|
+
"""H3 (partial) — a `covered` requirement row's `coveredBy` must name a
|
|
1893
|
+
concrete plan element that actually exists, not the spec-forbidden bare
|
|
1894
|
+
"recommended option" nor a phantom stage. Whether the cited step truly
|
|
1895
|
+
*satisfies* the requirement stays a worker DISAGREE(f) judgment; this closes
|
|
1896
|
+
the coarser hole where `coveredBy` points at nothing real.
|
|
1897
|
+
"""
|
|
1898
|
+
ip = data.get("implementationPlanning")
|
|
1899
|
+
if not isinstance(ip, dict):
|
|
1900
|
+
return
|
|
1901
|
+
rows = [r for r in (ip.get("requirementCoverage") or []) if isinstance(r, dict)]
|
|
1902
|
+
if not rows:
|
|
1903
|
+
return
|
|
1904
|
+
stage_numbers = {
|
|
1905
|
+
s.get("stage") for s in (ip.get("stages") or []) if isinstance(s, dict)
|
|
1906
|
+
}
|
|
1907
|
+
for row in rows:
|
|
1908
|
+
if row.get("status") != "covered":
|
|
1909
|
+
continue
|
|
1910
|
+
rid = row.get("id") or "<row>"
|
|
1911
|
+
covered = str(row.get("coveredBy") or "").strip()
|
|
1912
|
+
if covered.lower() in _COVERED_BY_VAGUE:
|
|
1913
|
+
failures.append(
|
|
1914
|
+
f"final-report data.json: requirementCoverage `{rid}` is `covered` "
|
|
1915
|
+
f"but coveredBy is just `{covered}`. Name the specific Option "
|
|
1916
|
+
"Candidate and Stage/Step that satisfies it, not 'recommended "
|
|
1917
|
+
"option' (profile Requirement Coverage)."
|
|
1918
|
+
)
|
|
1919
|
+
continue
|
|
1920
|
+
if not _COVERED_BY_ANCHOR_RE.search(covered):
|
|
1921
|
+
failures.append(
|
|
1922
|
+
f"final-report data.json: requirementCoverage `{rid}` coveredBy "
|
|
1923
|
+
f"`{covered}` names no Option / Stage / Step. A `covered` row must "
|
|
1924
|
+
"cite the concrete plan element that satisfies the requirement."
|
|
1925
|
+
)
|
|
1926
|
+
continue
|
|
1927
|
+
phantom = [
|
|
1928
|
+
n for m in _COVERED_BY_STAGE_REF_RE.finditer(covered)
|
|
1929
|
+
if stage_numbers and (n := int(m.group(1))) not in stage_numbers
|
|
1930
|
+
]
|
|
1931
|
+
if phantom:
|
|
1932
|
+
failures.append(
|
|
1933
|
+
f"final-report data.json: requirementCoverage `{rid}` coveredBy "
|
|
1934
|
+
f"cites Stage {phantom[0]} which does not exist in the Stage Map. "
|
|
1935
|
+
"A coverage row must point at a real stage."
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1938
|
+
|
|
1587
1939
|
def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
|
|
1588
1940
|
"""Enforce verdict ↔ blocker/condition/routing consistency on the
|
|
1589
1941
|
final-verification data.json (SSOT). The schema guarantees field SHAPE;
|
|
@@ -2424,6 +2776,8 @@ def main() -> int:
|
|
|
2424
2776
|
except (OSError, json.JSONDecodeError):
|
|
2425
2777
|
_sp = None
|
|
2426
2778
|
_validate_conformance(report_path, failures, surface_patterns=_sp)
|
|
2779
|
+
if task_type == "implementation-planning":
|
|
2780
|
+
_validate_planning_conformance_declared(report_path, failures)
|
|
2427
2781
|
if task_type == "improvement-discovery":
|
|
2428
2782
|
brief_relative = str(task_manifest.get("taskBriefPath") or "").strip()
|
|
2429
2783
|
brief_path = (
|