okstra 0.106.1 → 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/docs/for-ai/skills/okstra-brief.md +6 -3
- package/docs/for-ai/skills/okstra-container-build.md +1 -1
- package/docs/for-ai/skills/okstra-inspect.md +5 -5
- package/docs/for-ai/skills/okstra-manager.md +9 -3
- package/docs/for-ai/skills/okstra-run.md +18 -2
- package/docs/for-ai/skills/okstra-schedule.md +11 -7
- package/docs/for-ai/skills/okstra-setup.md +2 -2
- package/docs/kr/architecture.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/report-writer-worker.md +1 -1
- package/runtime/bin/okstra-antigravity-exec.sh +24 -3
- package/runtime/bin/okstra-claude-exec.sh +30 -3
- package/runtime/bin/okstra-codex-exec.sh +27 -4
- package/runtime/prompts/coding-preflight/clean-code.md +2 -0
- package/runtime/prompts/coding-preflight/overview.md +2 -0
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +2 -2
- package/runtime/prompts/lead/convergence.md +9 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- 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 +27 -9
- package/runtime/skills/okstra-container-build/SKILL.md +1 -1
- package/runtime/templates/reports/final-report.template.md +14 -4
- package/runtime/templates/reports/i18n/en.json +6 -1
- package/runtime/templates/reports/i18n/ko.json +6 -1
- 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
|
@@ -335,7 +335,7 @@ Lead's responsibilities in this sub-step (in order):
|
|
|
335
335
|
2. Dispatch a single plan-body reverify round to every analyser worker in the roster (`claude`, `codex`, and `antigravity` when opted in). `Report writer worker` is NOT a participant in this round.
|
|
336
336
|
3. Aggregate verdicts and resolve the gate result to one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`.
|
|
337
337
|
4. Write `runs/<task-type>/state/plan-body-verification.json` (schema in the convergence contract).
|
|
338
|
-
5. Populate `### 5.5.9 Plan Body Verification` in the final-report file (template at `templates/reports/final-report.template.md` §5.5.9 — Round count, Gate result,
|
|
338
|
+
5. Populate `### 5.5.9 Plan Body Verification` in the final-report file (template at `templates/reports/final-report.template.md` §5.5.9 — Round count, Gate result, per-item verdict tables grouped under each plan item's `subject`, Dissent log).
|
|
339
339
|
6. For every `majority-disagree` plan item, append a row to `## 1. Clarification Items` with `Blocks=approval` and the 1:1 ID match in the verdict table's `Classification` column (`majority-disagree → C-<N>`). Do NOT create a parallel `Open Questions` block — see `prompts/profiles/implementation-planning.md` self-review step 6 for the orphan-on-either-side contract.
|
|
340
340
|
7. Conditionally render the top-of-report `- [ ] Approved` marker line: present iff gate ∈ {passed, passed-with-dissent}, absent iff gate ∈ {blocked-by-disagreement, aborted-non-result}. `validators/validate-run.py` `validate_phase_boundary` enforces this correspondence. Manually flipping a blocked gate to passing in order to render the marker is a contract violation.
|
|
341
341
|
|
|
@@ -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 가 책임진다.
|
|
@@ -1458,7 +1458,7 @@
|
|
|
1458
1458
|
|
|
1459
1459
|
"PlanBodyVerification": {
|
|
1460
1460
|
"type": "object",
|
|
1461
|
-
"required": ["roundCount", "gateResult", "
|
|
1461
|
+
"required": ["roundCount", "gateResult", "planItems", "dissentLog"],
|
|
1462
1462
|
"additionalProperties": false,
|
|
1463
1463
|
"properties": {
|
|
1464
1464
|
"roundCount": { "type": "integer", "minimum": 0 },
|
|
@@ -1470,20 +1470,38 @@
|
|
|
1470
1470
|
"aborted-non-result"
|
|
1471
1471
|
]
|
|
1472
1472
|
},
|
|
1473
|
-
"
|
|
1473
|
+
"planItems": {
|
|
1474
1474
|
"type": "array",
|
|
1475
1475
|
"items": {
|
|
1476
1476
|
"type": "object",
|
|
1477
|
-
"required": ["
|
|
1477
|
+
"required": ["id", "subject", "verdicts"],
|
|
1478
1478
|
"additionalProperties": false,
|
|
1479
1479
|
"properties": {
|
|
1480
|
-
"
|
|
1481
|
-
"
|
|
1482
|
-
"
|
|
1483
|
-
|
|
1480
|
+
"id": { "type": "string", "minLength": 1 },
|
|
1481
|
+
"subject": { "type": "string", "minLength": 1 },
|
|
1482
|
+
"sourceSection": { "type": "string" },
|
|
1483
|
+
"clarificationId": {
|
|
1484
|
+
"anyOf": [
|
|
1485
|
+
{ "type": "null" },
|
|
1486
|
+
{ "type": "string", "pattern": "^C-\\d{3,}$" }
|
|
1487
|
+
]
|
|
1484
1488
|
},
|
|
1485
|
-
"
|
|
1486
|
-
|
|
1489
|
+
"verdicts": {
|
|
1490
|
+
"type": "array",
|
|
1491
|
+
"items": {
|
|
1492
|
+
"type": "object",
|
|
1493
|
+
"required": ["worker", "verdict"],
|
|
1494
|
+
"additionalProperties": false,
|
|
1495
|
+
"properties": {
|
|
1496
|
+
"worker": { "type": "string", "minLength": 1 },
|
|
1497
|
+
"verdict": {
|
|
1498
|
+
"enum": ["AGREE", "DISAGREE", "SUPPLEMENT", "verification-error"]
|
|
1499
|
+
},
|
|
1500
|
+
"breakageKind": { "type": "string" },
|
|
1501
|
+
"note": { "type": "string" }
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1487
1505
|
}
|
|
1488
1506
|
}
|
|
1489
1507
|
},
|
|
@@ -156,7 +156,7 @@ Every container group in the project (no task-key needed):
|
|
|
156
156
|
okstra container down --project-root <projectRoot> --all
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
Parse the stdout JSON (`{downed, orphanPanesReaped}`). Report each torn-down `projectName` and the orphan panes reaped. `down`
|
|
159
|
+
Parse the stdout JSON (`{downed, orphanPanesReaped}`). Report each torn-down `projectName` and the orphan panes reaped. `down` runs `docker compose -p <name> down --remove-orphans` — a compose-native teardown that removes the project's containers and the compose network, but deliberately keeps named volumes (e.g. DB data) by NOT passing `-v`. Confirm with the user before running `--all`, since it takes down every okstra container group in the project at once. A single-task `down` is safe to run directly once the task-key is resolved.
|
|
160
160
|
|
|
161
161
|
---
|
|
162
162
|
|
|
@@ -332,14 +332,24 @@ implementation-option: {{ frontmatter.implementationOption | yaml_scalar }}
|
|
|
332
332
|
- **Round count**: `{{ implementationPlanning.planBodyVerification.roundCount }}`
|
|
333
333
|
- **Gate result**: `{{ implementationPlanning.planBodyVerification.gateResult }}`
|
|
334
334
|
|
|
335
|
+
> {{ t("implementationPlanning.planBodyGateLegend") }}
|
|
336
|
+
|
|
335
337
|
#### Verdict details
|
|
336
338
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
{
|
|
340
|
-
|
|
339
|
+
{{ t("implementationPlanning.planBodyVerdictLegend") }}
|
|
340
|
+
|
|
341
|
+
{{ t("implementationPlanning.planBodyBreakageLegend") }}
|
|
342
|
+
|
|
343
|
+
{% for item in implementationPlanning.planBodyVerification.planItems %}
|
|
344
|
+
**{{ item.id }} — {{ item.subject }}**{% if item.sourceSection %} ({{ t("implementationPlanning.planBodySourceLabel") }} {{ item.sourceSection }}){% endif %}{% if item.clarificationId %} — {{ t("implementationPlanning.planBodyBlockerLabel") }} {{ item.clarificationId }}{% endif %}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
| Worker | Verdict | Breakage kind | Note |
|
|
348
|
+
|--------|---------|---------------|------|
|
|
349
|
+
{% for v in item.verdicts %}| {{ v.worker | mdcell }} | {{ v.verdict | mdcell }} | {{ (v.breakageKind or '--') | mdcell }} | {{ (v.note or '') | mdcell }} |
|
|
341
350
|
{% endfor %}
|
|
342
351
|
|
|
352
|
+
{% endfor %}
|
|
343
353
|
#### Dissent log
|
|
344
354
|
|
|
345
355
|
{% if implementationPlanning.planBodyVerification.dissentLog | length == 0 -%}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ticketCoverage": "Summary of the core problems, requirements, and verification targets this run covered.",
|
|
49
49
|
"executionStatus": "At-a-glance table of each worker's status, assigned model, and key findings.",
|
|
50
50
|
"sourceItemsRule": "The `Source items` column shows which worker items each consensus row was synthesised from, as `<worker>:<item-id>` pairs.",
|
|
51
|
-
"planBodyVerification": "
|
|
51
|
+
"planBodyVerification": "Per-item cross-verification of each plan-body item (option, stage, step, dependency, validation, rollback, requirement coverage) for whether it is executable as written and internally consistent. Whether each option actually *solves* the problem (requirement satisfaction) is judged separately under Requirement Coverage, not here.",
|
|
52
52
|
"clarificationItems": "Items that need your answer or supporting material before the next step."
|
|
53
53
|
},
|
|
54
54
|
"tokenSummary": {
|
|
@@ -91,6 +91,11 @@
|
|
|
91
91
|
"noRoundsNote": "No reverify rounds executed (all findings reached consensus at grouping)."
|
|
92
92
|
},
|
|
93
93
|
"implementationPlanning": {
|
|
94
|
+
"planBodyGateLegend": "Gate values — `passed`: agreed, no dissent · `passed-with-dissent`: a minority dissent remains but the gate passes (a majority dissent would block approval) · `blocked-by-disagreement`: majority dissent blocks approval · `aborted-non-result`: verification itself produced no result.",
|
|
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
|
+
"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
|
+
"planBodySourceLabel": "source §",
|
|
98
|
+
"planBodyBlockerLabel": "blocks approval →",
|
|
94
99
|
"optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
|
|
95
100
|
"optionBlastRadiusLabel": "Blast radius estimate",
|
|
96
101
|
"recommendedTableHeaderLabel": "Item",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ticketCoverage": "이 run 이 다룬 핵심 문제·요구사항·검증 대상 요약입니다.",
|
|
49
49
|
"executionStatus": "각 worker 의 상태·배정 모델·핵심 finding 을 한눈에 보는 표입니다.",
|
|
50
50
|
"sourceItemsRule": "`Source items` 열은 이 합의 항목이 어느 워커의 어느 항목에서 합성됐는지를 `<worker>:<item-id>` 형식으로 표기합니다.",
|
|
51
|
-
"planBodyVerification": "계획 본문의 각
|
|
51
|
+
"planBodyVerification": "계획 본문의 각 항목(옵션·스테이지·스텝·의존성·검증·롤백·요구사항 커버리지)이 적힌 대로 실행 가능하고 서로 모순이 없는지 워커들이 항목별로 교차 검증한 결과입니다. 각 옵션이 '문제를 실제로 푸는지'(요구사항 충족)는 여기가 아니라 Requirement Coverage 에서 별도로 판정합니다.",
|
|
52
52
|
"clarificationItems": "다음 진행 전에 사용자의 답변이나 자료 첨부가 필요한 항목입니다."
|
|
53
53
|
},
|
|
54
54
|
"tokenSummary": {
|
|
@@ -91,6 +91,11 @@
|
|
|
91
91
|
"noRoundsNote": "재검증 라운드 미실행 (그룹핑 단계에서 전부 합의)."
|
|
92
92
|
},
|
|
93
93
|
"implementationPlanning": {
|
|
94
|
+
"planBodyGateLegend": "게이트 결과 값 뜻 — `passed`: 이견 없이 통과 · `passed-with-dissent`: 소수 워커의 반대가 남았으나 통과(반대가 다수였다면 승인 차단) · `blocked-by-disagreement`: 다수 반대로 승인 차단 · `aborted-non-result`: 검증 자체가 결과를 내지 못함.",
|
|
95
|
+
"planBodyVerdictLegend": "판정(Verdict) — **AGREE**: 적힌 대로 실행 가능하고 다른 항목과 내부적으로 일관됨 · **SUPPLEMENT**: 항목 자체는 타당하나 의존성·엣지케이스·전제조건이 누락됨 · **DISAGREE**: 결함이 있음(결함 유형 참조) · **verification-error**: 워커 검증이 결과를 내지 못함.",
|
|
96
|
+
"planBodyBreakageLegend": "결함 유형(Breakage kind) — a: 인용한 파일 경로/심볼이 다른 스텝·옵션과 불일치 · b: 명령이 실행 불가하거나 모호함 · c: 검증 신호가 관측 불가 · d: 롤백이 커밋/의존성 순서를 위반 · e: 트레이드오프 매트릭스와 모순 · f: 요구사항 커버리지 행이 요구사항을 실제로 충족하는 옵션/스테이지/스텝에 매핑되지 않음. (`--` = 해당 없음)",
|
|
97
|
+
"planBodySourceLabel": "출처 §",
|
|
98
|
+
"planBodyBlockerLabel": "승인 차단 →",
|
|
94
99
|
"optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
|
|
95
100
|
"optionBlastRadiusLabel": "폭발 반경 추정",
|
|
96
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
|
|