okstra 0.223.2 → 0.224.0
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/coding-preflight/clean-code.md +8 -1
- package/runtime/python/okstra_ctl/incremental_carry.py +10 -16
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-diff-review.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +2 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +98 -33
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +22 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +33 -0
- package/runtime/python/okstra_ctl/plan_item_evidence.py +237 -0
- package/runtime/python/okstra_ctl/plan_items.py +12 -9
- package/runtime/python/okstra_ctl/plan_review_context.py +82 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +17 -1
- package/runtime/python/okstra_ctl/wizard/engine.py +26 -5
- package/runtime/python/okstra_ctl/wizard/registry.py +2 -0
- package/runtime/python/okstra_ctl/wizard/state.py +5 -0
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +99 -16
- package/runtime/python/okstra_ctl/wizard/steps_options.py +11 -0
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +14 -0
- package/runtime/schemas/final-report-v2.0.schema.json +3 -1
- package/runtime/schemas/final-report-v3.0.schema.json +3 -1
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -20,7 +20,7 @@ Good:
|
|
|
20
20
|
const sum = (arr) => arr.reduce((acc, x) => acc + x, 0);
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Caveat: do not extract until the duplication is real (rule of three). Premature abstraction is also a code smell.
|
|
23
|
+
Caveat: do not extract until the duplication is real (rule of three). Premature abstraction is also a code smell. The caveat does not cover a literal copy: a new block byte-identical to one already in the repository — test helpers included — reuses or moves the existing one.
|
|
24
24
|
|
|
25
25
|
Two more forms of duplication that hide from symbol-level grep:
|
|
26
26
|
|
|
@@ -325,6 +325,13 @@ For every source file this change touches, follow the paths the change creates o
|
|
|
325
325
|
- **concurrent** — something else is still writing, or the deadline fired and the work did not stop.
|
|
326
326
|
- **selection** — when several candidates fail, which one's evidence survives.
|
|
327
327
|
|
|
328
|
+
The failures the plan names are where the trace starts, not where it stops. Two orderings produce most error-path defects:
|
|
329
|
+
|
|
330
|
+
- **cleanup registered after a call that can throw** — inside a `try` whose `catch` / `finally` cleans up, the object to clean up must be registered before any call that can throw: a property write, a read, a call that starts other work whose completion the cleanup must wait for. Registered after it, the failure leaves the object behind or lets the cleanup run before that work settles.
|
|
331
|
+
- **order changed by a move** — when the change moves or restructures existing code, compare the order of throwing calls and cleanup registration with the base version; a refactor that swaps them is a behaviour change.
|
|
332
|
+
|
|
333
|
+
A failure the test fakes cannot produce is not covered by a passing test. Name it as unverified rather than calling the path clean.
|
|
334
|
+
|
|
328
335
|
**A finding names the input or state that produces the wrong result.** *"If the archive yields no entries, line 42 reports success and stores an empty result"* is a finding. Code that works as written is `clean`, however you would have written it differently: alternative structures, guards for states no caller can reach, extra tests for covered paths, and "consider extracting / renaming / memoizing" are improvements, not findings. A real failure that is small and cheap to fix is still a finding.
|
|
329
336
|
|
|
330
337
|
## Untrusted input and security
|
|
@@ -13,12 +13,13 @@ from .convergence_store import write_json_atomic
|
|
|
13
13
|
from .final_report_schema import load_schema_version
|
|
14
14
|
from .report_narrative import parse_narrative, writer_owned_data
|
|
15
15
|
from .json_boundary import load_owned_object
|
|
16
|
+
from .plan_item_evidence import dependency_scopes, reusable_item_ids
|
|
16
17
|
from .plan_items import (
|
|
17
18
|
PlanItemContractError,
|
|
18
19
|
content_hash,
|
|
20
|
+
dispatch_item_ids,
|
|
19
21
|
extract_plan_items,
|
|
20
22
|
planning_stage_ledger,
|
|
21
|
-
reverify_item_ids,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
|
|
@@ -386,7 +387,6 @@ def _carry_unchanged_checklists(
|
|
|
386
387
|
|
|
387
388
|
def _recompute_dispatch_queue(
|
|
388
389
|
narrative: Mapping[str, Any],
|
|
389
|
-
state_items: dict[str, dict],
|
|
390
390
|
stage_ledger: Mapping[str, Any] | None = None,
|
|
391
391
|
) -> list[str]:
|
|
392
392
|
"""이월 뒤 남은 디스패치 큐.
|
|
@@ -401,18 +401,6 @@ def _recompute_dispatch_queue(
|
|
|
401
401
|
extracted = extract_plan_items(_planning(dict(narrative)))
|
|
402
402
|
except (CarryError, PlanItemContractError, KeyError, TypeError):
|
|
403
403
|
return []
|
|
404
|
-
by_id = {
|
|
405
|
-
item["id"]: item
|
|
406
|
-
for item in extracted
|
|
407
|
-
if isinstance(item.get("id"), str)
|
|
408
|
-
}
|
|
409
|
-
previous_hashes = {
|
|
410
|
-
item_id: content_hash(by_id[item_id])
|
|
411
|
-
for item_id, row in state_items.items()
|
|
412
|
-
if row.get("carriedForwardFromSeq") and item_id in by_id
|
|
413
|
-
}
|
|
414
|
-
if not previous_hashes:
|
|
415
|
-
return []
|
|
416
404
|
ledger = planning_stage_ledger(
|
|
417
405
|
_planning(dict(narrative)),
|
|
418
406
|
{
|
|
@@ -420,7 +408,7 @@ def _recompute_dispatch_queue(
|
|
|
420
408
|
for stage, status in (stage_ledger or {}).items()
|
|
421
409
|
},
|
|
422
410
|
)
|
|
423
|
-
return
|
|
411
|
+
return dispatch_item_ids(extracted, ledger)
|
|
424
412
|
|
|
425
413
|
|
|
426
414
|
def _writer_stage_rows(data: Mapping[str, Any], *, snapshot: str) -> dict[int, dict]:
|
|
@@ -495,7 +483,7 @@ def merge_v3_plan_state(
|
|
|
495
483
|
state_items[item_id] for item_id in sorted(state_items)
|
|
496
484
|
]
|
|
497
485
|
queue = _recompute_dispatch_queue(
|
|
498
|
-
narrative,
|
|
486
|
+
narrative, pbv.get("stageLedger"),
|
|
499
487
|
)
|
|
500
488
|
if queue:
|
|
501
489
|
pbv["dispatchQueue"] = queue
|
|
@@ -533,6 +521,12 @@ def sync_prepared_dispatch_queue(state_path: Path, merged: Mapping[str, Any]) ->
|
|
|
533
521
|
if not isinstance(envelope, dict):
|
|
534
522
|
return
|
|
535
523
|
envelope["dispatchQueue"] = queue
|
|
524
|
+
inputs = envelope.get("reviewInputs")
|
|
525
|
+
if isinstance(inputs, dict):
|
|
526
|
+
rows = merged.get("planBodyVerification", {}).get("planItems", [])
|
|
527
|
+
inputs["dependencies"] = dependency_scopes(rows)
|
|
528
|
+
inputs["priorReviewHashes"] = {str(row["id"]): str(row.get("verifiedReviewHash", "")) for row in rows}
|
|
529
|
+
envelope["reusedItems"] = sorted(reusable_item_ids(rows, inputs) - set(queue))
|
|
536
530
|
write_json_atomic(prepared, envelope)
|
|
537
531
|
|
|
538
532
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- no new defects introduced — the diff does not break previously-working behaviour and adds no new bug (logic/off-by-one, null/empty handling, resource leaks, broken error paths)
|
|
15
15
|
- no security defect on input from outside the process — for every changed file that reads a request body, query, header, upload, message or webhook, or acts on its behalf: work that grows faster than the input (pairwise uniqueness, nested scans, a backtracking regex, input-driven recursion) runs only after a size/count bound has rejected oversized input — check the validation framework's configuration, since class-validator without `stopAtFirstError` still runs `@ArrayUnique` after `@ArrayMaxSize` fails and a byte-size body limit does not bound element count; input reaches SQL, shell commands, file paths, server-side fetch targets, templates and regexes only through parameters, allowlists or escaping; a new route / handler / resolver carries the project's auth guard and compares the caller with the owner of an input-chosen id; no secret, full token, password or personal data reaches source, logs, error messages or responses; nothing evaluates or unsafely deserializes input. Each hit is an Acceptance Blocker citing the triggering input and its effect. The same rules, with examples, are `clean-code.md` "Untrusted input and security" in the installed coding-preflight pack (`~/.okstra/prompts/coding-preflight/clean-code.md`).
|
|
16
16
|
- scope conformance — the delivered diff stays within the approved plan's scope; flag out-of-scope edits, unrelated file changes, leftover debug/commented-out code, and unintended deletions
|
|
17
|
-
- project review-rule packs — a pack applies when either source names it: the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` lists it under `reviewRulePacks` (the project's standing standard, applying whether or not the brief mentions it). The two sources are a union. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an acceptance overlay (record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited`). This is a static review pass, not a PR-comment workflow — do NOT dispatch reviewer subagents. Because this phase verifies the **whole-task merged diff**, it is the gate that catches **cross-stage findings a per-stage `implementation` verifier
|
|
17
|
+
- project review-rule packs — a pack applies when either source names it: the task brief's `Source Material` / `Reporter Confirmations` cites its exact `SKILL.md` path, or `<PROJECT_ROOT>/.okstra/project.json` lists it under `reviewRulePacks` (the project's standing standard, applying whether or not the brief mentions it). The two sources are a union. Read only those files and the `references/*.md` files they directly name. Do not search parent directories or host skill catalogs. Apply the rules as an acceptance overlay (record `project-review-rules: <paths read>`, `project-review-rules: declared <path> unreadable`, or `project-review-rules: none declared or cited`). This is a static review pass, not a PR-comment workflow — do NOT dispatch reviewer subagents. Because this phase verifies the **whole-task merged diff**, it is the gate that catches **cross-stage findings a per-stage `implementation` verifier can miss** (each implementation run reviews only its own stage diff; it compares new blocks with code already on its base, but not with stages that land after it): most importantly two cross-stage conditions: (a) the same helper stack / transform / domain rule duplicated across stages or services, test helpers and test support code included — byte-identical duplication is always an Acceptance Blocker, and semantically-equivalent transforms across services are blockers unless the approved plan explicitly justified keeping them separate; (b) an API newly orphaned because its only caller was removed in a different stage. A confirmed cross-stage duplication of this kind is an Acceptance Blocker (`major`+) that cites every `path:line` location and names the shared-module location to converge on. (Single-stage scope sees only one stage, so it cannot raise cross-stage findings — note that limitation rather than implying coverage.)
|
|
18
18
|
- Residual-tracked — note as Residual Risk unless severe enough to block:
|
|
19
19
|
- unresolved edge cases
|
|
20
20
|
- regression risk in adjacent code paths not directly changed
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-diff-review.md
CHANGED
|
@@ -38,7 +38,7 @@ Do not scan holistically and stop when it "looks fine". Work the matrix exhausti
|
|
|
38
38
|
- port / adapter / domain file, when the hexagonal overlay is loaded → `architectures/hexagonal.md`: no business logic in a port body, adapter methods are I/O only (no post-fetch filtering on domain state, no `findValid*`/`findActive*` names hiding a rule), domain objects declared under the domain boundary, no changed domain file importing an ORM / framework / adapter / service (read the import list — mechanical), and a service dependency you add or modify goes through a port rather than a concrete adapter (advisory only while the project has not declared `architecture.style = hexagonal` in `.okstra/project.json` — record it with the port sketch; blocking under that declaration, so fix it in place before the commit rather than record-and-pass, exactly as `_implementation-verifier.md` re-grades this same diff. Either way, the codebase already injecting concrete classes is the debt this pays down, not a reason to skip it).
|
|
39
39
|
- a service file, when the hexagonal overlay is loaded → `architectures/hexagonal.md` Rule H6: a decision this diff puts in a service — an `if`/`else` chain over domain fields, a business formula, a private method named for a domain concept — belongs in a domain function. Orchestration control flow, DTO mapping and calling a domain predicate are not violations.
|
|
40
40
|
- a file that reads input from outside the process (request body, query, header, upload, message, webhook) or acts on its behalf → `clean-code.md` "Untrusted input and security": a size/count bound rejects oversized input before any check that grows faster than the input (a validator that runs every rule, such as class-validator without `stopAtFirstError`, does not stop at a failed `@ArrayMaxSize`), input reaches SQL / shell / paths / server-side fetches / templates / regexes only through parameters, allowlists or escaping, a new entry point carries the auth guard and compares the caller with the owner of an input-chosen id, no secret or personal data reaches code, logs or responses, and nothing evaluates or unsafely deserializes input.
|
|
41
|
-
- every changed source file → `clean-code.md` "Trace what this change can do wrong": walk the error, partial, concurrent and selection paths this diff creates to their end and fix wherever a wrong result comes out. Name the input that produces it — if you cannot name one, there is nothing to fix here and you move on rather than restructuring code that works.
|
|
41
|
+
- every changed source file → `clean-code.md` "Trace what this change can do wrong": walk the error, partial, concurrent and selection paths this diff creates to their end — past the failures the plan lists, and including where a cleaning `try` registers its cleanup target relative to each call that can throw — and fix wherever a wrong result comes out. Name the input that produces it — if you cannot name one, there is nothing to fix here and you move on rather than restructuring code that works.
|
|
42
42
|
A file can hold several roles — apply every rule set that fits it.
|
|
43
43
|
3. **Decide clean-or-finding for each cell.** Read the full file when a rule needs context (never judge a port/adapter/domain or a naming rule from the hunk alone).
|
|
44
44
|
4. **Fix each finding in place** before the commit. When a readability finding is real, the fix is a named helper or named intermediate value — sketch the cleaner shape (a few lines) in your audit note, then apply it. When the fix is genuinely out of this stage's scope, record it as an `Out-of-plan` note instead of silently leaving it.
|
package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md
CHANGED
|
@@ -194,12 +194,12 @@ Re-running commands proves the diff *builds and passes*; it does NOT prove the d
|
|
|
194
194
|
- **Declared architecture style promotes the placement overlay from advisory to binding.** Take `Architecture style` from that projection and record `architecture-style: <hexagonal|layered|none>` in the worker result next to the `coding-conventions:` line. A declared `hexagonal` counts the overlay as loaded even when none of the router's Stage 3 layout signals matched, so the **Hexagonal** blocking check below applies in full, and the concrete-adapter injection listed under Advisory findings is promoted to a blocking finding → verdict `FAIL`, not a `should-fix`. A declared `layered` has no pack resource; its binding invariant is direction — an upper layer may import a lower one, never the reverse — so a changed file whose import list reaches back up a layer, or around a layer boundary, is a blocking placement violation cited `path:line` from that import list. The `layered` half is worker judgement: no machine check reads layer names, so a missed reverse dependency is a missed finding, not a validator failure. A `none` or absent projected style leaves Stage 3 detection-driven and the placement items advisory. **Enforced:** `scripts/okstra_project/resolver.py` `resolve_architecture` reads the same stored field for the planning-side rule in `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_variation_point_analysis`, and `_validate_verifier_fail_blocks_verdict` keeps the resulting `FAIL` from being dropped during synthesis.
|
|
195
195
|
- **Blocking checks (any hit → verdict `FAIL`, cited `path:line` + rule name, recommended fix recorded — the verifier does NOT apply it):**
|
|
196
196
|
- **Security (every changed file that reads input from outside the process or acts on its behalf — `clean-code.md` "Untrusted input and security"):** work that grows faster than the input — pairwise uniqueness, nested scans, a backtracking regex, input-driven recursion — reachable before a size/count bound has rejected oversized input (read the validation framework's configuration: class-validator without `stopAtFirstError` still runs `@ArrayUnique` after `@ArrayMaxSize` fails, and a byte-size body limit does not bound element count); input concatenated into SQL, a shell command, a file path, a server-side fetch target, a template or a regex; a new route / handler / resolver without the project's auth guard, or a lookup by an input-chosen id that never compares the caller with the owner; a secret, full token, password or personal data written to source, logs, an error message or a response; `eval`, dynamic code, or unsafe deserialization applied to input. Cite the input that triggers it and its effect — for unbounded work, the input size and the measured or derived cost.
|
|
197
|
-
- **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. Recommend the shared module location.
|
|
197
|
+
- **New duplication / DRY:** two or more newly added or meaningfully modified blocks implement the same helper stack, transform, or domain rule. Literal copy-paste is always blocking; semantically equivalent transforms across services are blocking unless the approved plan explicitly justified keeping them separate. The comparison is not limited to this diff: a newly added block that is byte-identical to a block already in the repository — base code, or a copy an earlier stage landed — is the same blocking finding, cited at both the new `path:line` and the existing one, because `final-verification` grades that cross-stage copy as an Acceptance Blocker and a per-stage `clean` only defers it. Test helpers and test support code count. Recommend the shared module location.
|
|
198
198
|
- **Self-mocking:** a test for `Foo` stubs/spies a method on the `Foo` instance under test (`jest.spyOn(sut, ...)`, `spyOn(FooService.prototype, ...)` in `foo.*.spec.*`, `vi.mocked(sut)` + stub). Mocking injected collaborators is fine.
|
|
199
199
|
- **Interaction-only assertion:** a test whose only/primary assertion is `toHaveBeenCalled*` / `toHaveBeenCalledTimes` on an internal helper or a non-side-effecting collaborator, with no assertion on the returned value / resulting state / persisted row / emitted event.
|
|
200
200
|
- **Tautological delegation assertion:** a test asserts the SUT result equals a direct call to the same pure helper/collaborator that the SUT delegates to, instead of asserting an independent literal value or observable state.
|
|
201
201
|
- **Untruthful name:** a read-named function (`get*` / `find*` / `load*`) that writes/inserts/mutates; an adapter or repository name encoding the caller's use-case (`*ForInit`) or hiding a domain rule (`findValid*` / `findActive*`).
|
|
202
|
-
- **Wrong-result trace (every changed source file):** follow the paths this diff creates or alters to their end — error, partial, concurrent, selection — and name the input or state that produces a wrong result (`clean-code.md` §"Trace what this change can do wrong"). This one is an obligation, not a pattern: the defects that reach production are usually ordinary code with no name on this list. The bar is also the noise filter — a finding names the failing input; an alternative structure, a guard for a state no caller reaches, or a "consider extracting" is an improvement and verdicts `clean`. **The unit is the changed source file**: record `clean` or findings for each one, list every file you excluded with its reason (lockfiles, generated code, pure config), and close the section with `general: <N> files — all verdicted, <M> excluded`.
|
|
202
|
+
- **Wrong-result trace (every changed source file):** follow the paths this diff creates or alters to their end — error, partial, concurrent, selection — and name the input or state that produces a wrong result (`clean-code.md` §"Trace what this change can do wrong"). This one is an obligation, not a pattern: the defects that reach production are usually ordinary code with no name on this list. The bar is also the noise filter — a finding names the failing input; an alternative structure, a guard for a state no caller reaches, or a "consider extracting" is an improvement and verdicts `clean`. The failure modes the plan lists are the floor of this trace, not its ceiling: a verdict scoped to "the modeled failures" is not a verdict on the file. For every `try` whose `catch` / `finally` cleans up, check at each call inside it that can throw (a property write, a read, a call that starts other work) whether the thing to clean up is already registered at that point; when the diff moves or restructures existing code, compare that order with the base version and cite the base `path:line` when it changed. A failure the test fakes cannot produce (a throwing setter or read the fake does not model) is recorded as unverified with the failure named, never folded into `clean`. **The unit is the changed source file**: record `clean` or findings for each one, list every file you excluded with its reason (lockfiles, generated code, pure config), and close the section with `general: <N> files — all verdicted, <M> excluded`.
|
|
203
203
|
- **Business rules in an application service (only when the hexagonal overlay is loaded):** changed service code that decides a business outcome inline — an `if`/`else` chain over domain fields, a business formula in arithmetic, a private method named for a domain concept — instead of calling a domain function (`architectures/hexagonal.md` Rule H6). Orchestration control flow, DTO mapping, and calling a domain predicate are not violations. Blocking when the embedded rule is substantial: money, permissions, a state machine.
|
|
204
204
|
- **Hexagonal (only when the overlay is loaded):** business logic inside a port body; an adapter method that is not pure I/O (post-fetch JS filtering on domain state, domain-rule evaluation); a domain object declared outside the `domain/` boundary; a changed file under the domain folder importing an ORM / DB layer, a framework or its DI decorators, or anything under adapters / infrastructure / services (read the import list — this verdict is mechanical, not a judgment). **The routed pack's project-convention latitude does not reach this cell.** `architectures/hexagonal.md` lets a documented convention resolve conflicts in the project's favour elsewhere in that overlay; here the import list decides, and "matches existing convention" is not an answer to it (see that file's §"How far a project convention reaches"). A run that resolved exactly this finding as project convention returned `clean` and the team's PR review flagged the same import.
|
|
205
205
|
- **gitignored file committed to the branch:** any path in the `git diff --name-only <base>...HEAD` enumeration that `.gitignore` excludes — enumerate them by piping that list through `git check-ignore --stdin --no-index`. A committed ignored file means the executor bulk-added (`git add .`/`-A`) or force-staged (`git add -f`) it, leaking build output, scratch files, or verification artifacts into the eventual PR. This explicitly includes `.okstra/` paths (and `.project-docs/` when the legacy symlink is present): `.okstra/**` is gitignored, so a committed okstra file (qa scripts, conformance results) is always this defect. Cite each path; recommend `git rm --cached <path>` to untrack it while keeping the file on disk. Conformance/qa evidence belongs in the carry sidecar / verifier result, never in git history.
|
|
@@ -54,6 +54,14 @@ from okstra_ctl.plan_items import (
|
|
|
54
54
|
with_response_format,
|
|
55
55
|
)
|
|
56
56
|
from okstra_ctl.paths import RunRef
|
|
57
|
+
from okstra_ctl.plan_item_evidence import (
|
|
58
|
+
capture_review_inputs,
|
|
59
|
+
dependency_scopes,
|
|
60
|
+
reusable_item_ids,
|
|
61
|
+
review_fingerprint,
|
|
62
|
+
review_files_unchanged,
|
|
63
|
+
)
|
|
64
|
+
from okstra_ctl.plan_review_context import render_review_context
|
|
57
65
|
from okstra_ctl.stage_ledger import build_stage_ledger
|
|
58
66
|
from okstra_ctl.claim_reproduction import NOT_RUNNABLE, reproduce
|
|
59
67
|
from okstra_ctl.final_report_schema import load_schema_version
|
|
@@ -118,13 +126,39 @@ def _queue_for(
|
|
|
118
126
|
run_manifest: Path | None,
|
|
119
127
|
previous_hashes: Mapping[str, str] | None = None,
|
|
120
128
|
tied_ids: Sequence[str] | None = None,
|
|
129
|
+
previous_rows: Sequence[Mapping[str, Any]] = (),
|
|
130
|
+
review_inputs: Mapping[str, Any] | None = None,
|
|
121
131
|
) -> list[str]:
|
|
122
132
|
ledger = _merged_ledger(planning, run_manifest)
|
|
123
133
|
if tied_ids is not None:
|
|
124
134
|
return tie_vote_item_ids(items, ledger, tied_ids)
|
|
135
|
+
inputs = review_inputs or _review_inputs(items, planning, run_manifest, previous_rows)
|
|
136
|
+
reusable = reusable_item_ids(previous_rows, inputs)
|
|
137
|
+
candidates = set(dispatch_item_ids(items, ledger))
|
|
125
138
|
if previous_hashes:
|
|
126
|
-
|
|
127
|
-
return
|
|
139
|
+
candidates.update(reverify_item_ids(items, previous_hashes, ledger))
|
|
140
|
+
return [item["id"] for item in items if item["id"] in candidates - reusable]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _review_inputs(
|
|
144
|
+
items: list[dict[str, Any]], planning: Mapping[str, Any],
|
|
145
|
+
run_manifest: Path | None, rows: Sequence[Mapping[str, Any]],
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
if run_manifest is None:
|
|
148
|
+
return {}
|
|
149
|
+
authority = validated_run_authority(run_manifest)
|
|
150
|
+
return capture_review_inputs(items, planning, authority.payload, authority.project_root, rows)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _checked_review_inputs(run_manifest: Path | None) -> dict[str, Any]:
|
|
154
|
+
root = _probe_project_root(run_manifest)
|
|
155
|
+
if root is None:
|
|
156
|
+
return {}
|
|
157
|
+
path = _prepared_items_path(run_manifest)
|
|
158
|
+
inputs = _load_json_object(path).get("reviewInputs", {}) if path.is_file() else {}
|
|
159
|
+
if not review_files_unchanged(inputs, root):
|
|
160
|
+
raise PlanItemContractError("review evidence changed during dispatch; prepare and verify again")
|
|
161
|
+
return inputs
|
|
128
162
|
|
|
129
163
|
|
|
130
164
|
def _queue_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
|
@@ -133,7 +167,9 @@ def _queue_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
133
167
|
if state is None:
|
|
134
168
|
raise PlanItemContractError("--tie-vote requires --state")
|
|
135
169
|
return {"tied_ids": _tied_ids(state)}
|
|
136
|
-
|
|
170
|
+
state = getattr(args, "state", None)
|
|
171
|
+
rows = _state_plan_body_items(_load_json_object(state), state) if state and state.is_file() else []
|
|
172
|
+
return {"previous_hashes": _previous_hashes(state), "previous_rows": rows}
|
|
137
173
|
|
|
138
174
|
|
|
139
175
|
def _tied_ids(state_path: Path) -> list[str]:
|
|
@@ -170,8 +206,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
170
206
|
prepare.add_argument(
|
|
171
207
|
"--state",
|
|
172
208
|
type=Path,
|
|
173
|
-
help="previous plan-body state;
|
|
174
|
-
"
|
|
209
|
+
help="previous plan-body state; only matching body and complete "
|
|
210
|
+
"evidence fingerprints shrink the self-fix reverify queue, and "
|
|
175
211
|
"each queued item's recorded votes and selfFixNote are carried "
|
|
176
212
|
"into the prompt as its `**Prior round dissent**` block",
|
|
177
213
|
)
|
|
@@ -251,8 +287,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
251
287
|
"--prior-state",
|
|
252
288
|
type=Path,
|
|
253
289
|
help="the previous run's plan-body-verification-<task-type>-<seq>.json. "
|
|
254
|
-
"A newly seeded item
|
|
255
|
-
"
|
|
290
|
+
"A newly seeded item with matching body and complete evidence "
|
|
291
|
+
"fingerprints inherits its verdicts and is tagged "
|
|
256
292
|
"carriedForwardFromSeq, so round 1 does not re-judge text nobody "
|
|
257
293
|
"changed. Requires --state, refuses a state file belonging to "
|
|
258
294
|
"another task, and never carries on a matching id alone",
|
|
@@ -474,12 +510,21 @@ def _prepare(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
474
510
|
+ "; correct the narrative in this run, then retry plan-items prepare"
|
|
475
511
|
)
|
|
476
512
|
envelope = _envelope(source)
|
|
513
|
+
kwargs = _queue_kwargs(args)
|
|
514
|
+
envelope["reviewInputs"] = _review_inputs(
|
|
515
|
+
envelope["items"], _planning(source), args.run_manifest,
|
|
516
|
+
kwargs.get("previous_rows", []),
|
|
517
|
+
)
|
|
477
518
|
envelope["dispatchQueue"] = _queue_for(
|
|
478
519
|
envelope["items"],
|
|
479
520
|
_planning(source),
|
|
480
521
|
args.run_manifest,
|
|
481
|
-
**
|
|
522
|
+
**kwargs,
|
|
523
|
+
review_inputs=envelope["reviewInputs"],
|
|
482
524
|
)
|
|
525
|
+
envelope["reusedItems"] = sorted(reusable_item_ids(
|
|
526
|
+
kwargs.get("previous_rows", []), envelope["reviewInputs"],
|
|
527
|
+
))
|
|
483
528
|
if getattr(args, "tie_vote", False):
|
|
484
529
|
envelope["dispatchKind"] = "critic-tie"
|
|
485
530
|
envelope["tieSplits"] = _tie_splits_from_state(
|
|
@@ -998,6 +1043,7 @@ def _render_inputs(run_manifest: Path) -> str:
|
|
|
998
1043
|
|
|
999
1044
|
|
|
1000
1045
|
def _prompt(args: argparse.Namespace) -> str:
|
|
1046
|
+
inputs = _checked_review_inputs(args.run_manifest)
|
|
1001
1047
|
envelope = _load_json_object(
|
|
1002
1048
|
_prepared_items_path(args.run_manifest, require_regular=True)
|
|
1003
1049
|
)
|
|
@@ -1021,6 +1067,11 @@ def _prompt(args: argparse.Namespace) -> str:
|
|
|
1021
1067
|
line("Item count", len(items)),
|
|
1022
1068
|
_render_inputs(args.run_manifest),
|
|
1023
1069
|
]
|
|
1070
|
+
authority = validated_run_authority(args.run_manifest)
|
|
1071
|
+
rows.append(render_review_context(
|
|
1072
|
+
[item["id"] for item in items], inputs, authority.payload, authority.project_root,
|
|
1073
|
+
))
|
|
1074
|
+
_checked_review_inputs(args.run_manifest)
|
|
1024
1075
|
for index, item in enumerate(items, 1):
|
|
1025
1076
|
if not isinstance(item, Mapping):
|
|
1026
1077
|
continue
|
|
@@ -1075,6 +1126,12 @@ def _validate_prepared(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1075
1126
|
)
|
|
1076
1127
|
if actual.get("dispatchQueue") != expected_queue:
|
|
1077
1128
|
raise PlanItemContractError("prepared dispatch queue does not match")
|
|
1129
|
+
current_inputs = _review_inputs(
|
|
1130
|
+
expected["items"], _planning(source), args.run_manifest,
|
|
1131
|
+
_queue_kwargs(args).get("previous_rows", []),
|
|
1132
|
+
)
|
|
1133
|
+
if actual.get("reviewInputs") != current_inputs:
|
|
1134
|
+
raise PlanItemContractError("prepared review evidence changed; prepare again")
|
|
1078
1135
|
if not getattr(args, "tie_vote", False):
|
|
1079
1136
|
# 라운드 2+ 의 반대 의견 반입은 여기서만 검사된다. 봉투에서 빠지면
|
|
1080
1137
|
# 프롬프트에도 빠지고, 반대한 워커는 자기 판정을 그대로 다시 적는다.
|
|
@@ -1134,6 +1191,7 @@ def _verdict_row(worker: str, block: VerdictBlock) -> dict[str, Any]:
|
|
|
1134
1191
|
("fixability", block.fixability),
|
|
1135
1192
|
("note", block.note),
|
|
1136
1193
|
("explanation", block.explanation),
|
|
1194
|
+
("dependencies", block.dependencies),
|
|
1137
1195
|
):
|
|
1138
1196
|
if value:
|
|
1139
1197
|
row[key] = value
|
|
@@ -1367,24 +1425,6 @@ def _carry_prior_run_verdicts(
|
|
|
1367
1425
|
added: list[dict[str, Any]],
|
|
1368
1426
|
hashes: Mapping[str, str],
|
|
1369
1427
|
) -> _PriorRunCarry | None:
|
|
1370
|
-
"""직전 run 이 같은 본문에 내린 판정을 새로 시드된 행에 옮긴다.
|
|
1371
|
-
|
|
1372
|
-
실패한 계획 run 을 다시 돌면 판정이 하나도 넘어오지 않았다. 이월은 답변된
|
|
1373
|
-
clarification 이 있는 경로에만 있고(`incremental_scope.decide_scope` 는
|
|
1374
|
-
답변된 `C-NNN` 이 없으면 `full` 을 되돌려 `incremental_carry` 를 도달 불가로
|
|
1375
|
-
만든다), 그래서 아무도 고치지 않은 문장이 매 재실행마다 다시 채점됐다.
|
|
1376
|
-
|
|
1377
|
-
행 모양은 `incremental_carry._stamp_carried` 와 같다 — `verdicts` 를 그대로
|
|
1378
|
-
옮기고, `carriedForwardFromSeq` 에 직전 seq 를 찍고, `verifiedContentHash` 를
|
|
1379
|
-
이번 추출 해시로 맞춘다. 그 해시가 시드의 `dispatchQueue` 계산에 그대로
|
|
1380
|
-
들어가므로(`_queue_for` → `reverify_item_ids`), 이월된 행은 라운드 1 큐에서
|
|
1381
|
-
빠진다. 큐를 따로 줄이지 않는 이유가 이것이다: incremental 경로가
|
|
1382
|
-
`incremental_carry._recompute_dispatch_queue` 에서 쓰는 것과 같은 장치다.
|
|
1383
|
-
|
|
1384
|
-
id 만 같은 행은 절대 이월하지 않는다. `P-*` id 는 위치에서 나오므로 계획이
|
|
1385
|
-
한 줄만 밀려도 같은 번호가 다른 문장을 가리킨다. 해시가 같아야 그 문장이
|
|
1386
|
-
같은 문장이다.
|
|
1387
|
-
"""
|
|
1388
1428
|
prior = getattr(args, "prior_state", None)
|
|
1389
1429
|
if prior is None:
|
|
1390
1430
|
return None
|
|
@@ -1420,9 +1460,17 @@ def _carry_prior_run_verdicts(
|
|
|
1420
1460
|
current_hash = hashes.get(item_id)
|
|
1421
1461
|
if not current_hash or previous.get("verifiedContentHash") != current_hash:
|
|
1422
1462
|
continue
|
|
1463
|
+
manifest = getattr(args, "run_manifest", None)
|
|
1464
|
+
inputs = (
|
|
1465
|
+
_load_json_object(_prepared_items_path(manifest)).get("reviewInputs", {})
|
|
1466
|
+
if manifest is not None else {}
|
|
1467
|
+
)
|
|
1468
|
+
if item_id not in reusable_item_ids([previous], inputs):
|
|
1469
|
+
continue
|
|
1423
1470
|
row["verdicts"] = copy.deepcopy(verdicts)
|
|
1424
1471
|
row["carriedForwardFromSeq"] = prev_seq
|
|
1425
1472
|
row["verifiedContentHash"] = current_hash
|
|
1473
|
+
row["verifiedReviewHash"] = previous["verifiedReviewHash"]
|
|
1426
1474
|
count += 1
|
|
1427
1475
|
return _PriorRunCarry(prev_seq, count)
|
|
1428
1476
|
|
|
@@ -1526,6 +1574,7 @@ def _seed(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1526
1574
|
_planning(source),
|
|
1527
1575
|
getattr(args, "run_manifest", None),
|
|
1528
1576
|
previous,
|
|
1577
|
+
previous_rows=recorded,
|
|
1529
1578
|
)
|
|
1530
1579
|
verification["gating"] = (
|
|
1531
1580
|
verification.get("gating") is True
|
|
@@ -1609,8 +1658,21 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1609
1658
|
f"gate is re-derived from that table, so a verdict with nowhere to "
|
|
1610
1659
|
f"land would be scored as if it were never cast"
|
|
1611
1660
|
)
|
|
1661
|
+
review_inputs = _checked_review_inputs(getattr(args, "run_manifest", None))
|
|
1612
1662
|
if args.round_number < 1:
|
|
1613
1663
|
raise PlanItemContractError("--round must be 1 or greater")
|
|
1664
|
+
_write_round_verdicts(args, data, recorded, rows, review_inputs)
|
|
1665
|
+
if requires_plan_repair(verification):
|
|
1666
|
+
verification["gating"] = True
|
|
1667
|
+
_validate_advisory_round(verification, args.round_number)
|
|
1668
|
+
write_json_atomic(target, data)
|
|
1669
|
+
return {"ok": True, "operation": "apply-verdicts", "path": str(target)}
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def _write_round_verdicts(
|
|
1673
|
+
args: argparse.Namespace, data: Mapping[str, Any], recorded: list[dict[str, Any]],
|
|
1674
|
+
rows: Mapping[str, list[dict[str, Any]]], review_inputs: Mapping[str, Any],
|
|
1675
|
+
) -> None:
|
|
1614
1676
|
project_root = _probe_project_root(getattr(args, "run_manifest", None))
|
|
1615
1677
|
append = bool(getattr(args, "append", False))
|
|
1616
1678
|
replaces_critic = append and any(
|
|
@@ -1627,18 +1689,20 @@ def _apply_verdicts(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
1627
1689
|
rows,
|
|
1628
1690
|
data.get("roundHistory"),
|
|
1629
1691
|
args.round_number,
|
|
1630
|
-
|
|
1631
|
-
discard_open_rounds=discard_open_rounds,
|
|
1692
|
+
args.state or args.data,
|
|
1693
|
+
discard_open_rounds=bool(getattr(args, "discard_open_rounds", False)),
|
|
1632
1694
|
)
|
|
1633
1695
|
writer = _append_item_verdicts if append else _replace_item_verdicts
|
|
1634
1696
|
for item in recorded:
|
|
1635
1697
|
if isinstance(item, Mapping) and item.get("id") in rows:
|
|
1636
1698
|
writer(item, rows[item["id"]], args.round_number, project_root)
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1699
|
+
review_inputs = {**review_inputs, "dependencies": dependency_scopes(recorded)}
|
|
1700
|
+
for item in recorded:
|
|
1701
|
+
if isinstance(item, Mapping) and item.get("id") in rows:
|
|
1702
|
+
item.pop("verifiedReviewHash", None)
|
|
1703
|
+
fingerprint = review_fingerprint(item, review_inputs)
|
|
1704
|
+
if fingerprint is not None:
|
|
1705
|
+
item["verifiedReviewHash"] = fingerprint
|
|
1642
1706
|
|
|
1643
1707
|
|
|
1644
1708
|
def _stamped_verdicts(
|
|
@@ -2110,6 +2174,7 @@ def _reject_unbacked_self_fix_stop(
|
|
|
2110
2174
|
|
|
2111
2175
|
|
|
2112
2176
|
def _complete_round(args: argparse.Namespace) -> dict[str, Any]:
|
|
2177
|
+
_checked_review_inputs(args.run_manifest)
|
|
2113
2178
|
if args.round_number < 1:
|
|
2114
2179
|
raise PlanItemContractError("--round must be 1 or greater")
|
|
2115
2180
|
data, current, audit, history = _round_inputs(args)
|
|
@@ -413,11 +413,11 @@ round before any host or provider process starts.
|
|
|
413
413
|
|
|
414
414
|
For contract 3.0, `prepare` checks the selected-direction draft with the same semantic validator as publication before writing the queue. Correct all reported references, coverage rows and planned commands through the writer correction procedure, then retry in this run. **Enforced:** `report_assembly.validate_plan_draft` and `plan_items_cli._prepare`; `tests/report/test_report_assembly.py` checks that invalid coverage creates no prepared queue.
|
|
415
415
|
|
|
416
|
-
1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared
|
|
416
|
+
1. Lead runs `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, places the fixed output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then runs `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. After a self-fix rewrite, pass `--state <plan-body-verification.json>` on prepare and validate-prepared. The runtime reuses a verdict only when its body, complete declared evidence closure, shared analysis packet, plan metadata, reviewer assignments and review contract have the same fingerprint. A missing or uncertain dependency declaration, unresolved reference, non-result or executable fact probe stays in the re-verification queue. Detailed verdicts and their original rounds remain unchanged for reused items. File dependencies cover whole files; step items also include preceding steps; plan-wide and preparation items include all plan items. Python resolves the one convergence-owned state path from that run identity. Dispatch only after exact-match validation succeeds. `observed` / `deferred` stages remain omitted. An empty queue opens no worker dispatch and creates no verification round. **Enforced:** `implementation_planning.authoring._queue_for`, `plan_item_evidence.review_fingerprint`, `_validate_prepared` and `tests/run/test_plan_items.py`. An incremental carry without evidence snapshots keeps active items queued until prepare can validate their dependencies.
|
|
417
417
|
|
|
418
418
|
**Then seed the landing table (BLOCKING):** `okstra plan-items seed --narrative <report-writer-narrative.md> --state <plan-body-verification.json> --run-manifest <run-manifest.json>`. `apply-verdicts` in step 8 refuses a verdict whose item has no `planBodyVerification.planItems[]` row. The report writer never owns that state, so the deterministic seed is the only creator of its rows. The seed is idempotent by id and never touches existing verdicts, so it is safe to re-run between rounds and after a self-fix re-extraction. It does refresh `contentHash` from the current extract. Skipping it makes step 8 fail with `plan-body state has no row for [...]`.
|
|
419
419
|
|
|
420
|
-
**On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item
|
|
420
|
+
**On a re-run, add `--prior-state <the previous run's plan-body-verification-<task-type>-<seq>.json>`.** A newly seeded item inherits verdicts only when its body and verified evidence fingerprint match the prepared snapshot for the same task. Reused rows carry `carriedForwardFromSeq`; their full verdicts are preserved. Legacy body-only records re-enter verification. A matching id alone never carries: `P-*` ids are positional. The flag requires `--state` and refuses another task's state. **Enforced:** `implementation_planning.authoring._carry_prior_run_verdicts` and `plan_item_evidence.reusable_item_ids`.
|
|
421
421
|
|
|
422
422
|
**`--run-manifest` is what scopes the gate to the stage you are starting.** Seed uses it to overlay disk `done` / `active` onto `planBodyVerification.stageLedger`. The current plan's depends-on fills `ready` / `blocked` when no prior plan exists, so a first run does not treat every stage as in-scope. **Enforced:** `okstra_ctl.plan_items.planning_stage_ledger`.
|
|
423
423
|
2. For each analyser worker in the roster, use `okstra plan-items prompt` output verbatim as the instruction body in the materialization sequence above. Read §"Re-verification rounds (round 2+)" before preparing a later round; read §"Plan-body reverify prompt" only when diagnosing a prompt-contract failure.
|
|
@@ -693,6 +693,23 @@ and a `- Plan narrative:` line from its `reportNarrativePath`. The worker's
|
|
|
693
693
|
are what let a verifier read the requirement a selected-direction `P-Req-*` row
|
|
694
694
|
names by `originalRequirementId`. The lead does not add them by hand.
|
|
695
695
|
|
|
696
|
+
When a complete input snapshot is available, the renderer also provides inline
|
|
697
|
+
review context. Read that context and the assigned payloads first. It preserves
|
|
698
|
+
the packet except its navigation index and generated analysis Coverage Census,
|
|
699
|
+
and carries plan metadata plus supporting items as lossless JSON. Supporting
|
|
700
|
+
items are narrowed only when the previous dependency closure is complete and
|
|
701
|
+
every body in that closure is unchanged; otherwise all other plan items are
|
|
702
|
+
included. A changed body, missing declaration, unresolved reference or uncertain
|
|
703
|
+
cross-item claim requires the full context or the original Inputs paths.
|
|
704
|
+
Read those paths when the supplied context does not resolve a question, and
|
|
705
|
+
record the additional dependencies. Omission never proves absence.
|
|
706
|
+
Packets containing finding-like headings are quoted as source data, preserving
|
|
707
|
+
their text without adding foreign items to the plan-verification queue.
|
|
708
|
+
|
|
709
|
+
Enforcement: `render_review_context` in `okstra_ctl.plan_review_context` selects
|
|
710
|
+
the context; `test_plan_review_context_preserves_facts_and_falls_back_for_unknown_dependencies`
|
|
711
|
+
in `tests/run/test_plan_items.py` checks retained facts and conservative fallback.
|
|
712
|
+
|
|
696
713
|
Carrying the packet does not license re-analysis. It is there so the verifier
|
|
697
714
|
can resolve a plan item back to the requirement it claims to satisfy; the
|
|
698
715
|
posture in §"Adversarial plan-body posture" still applies, and this round does
|
|
@@ -847,6 +864,9 @@ never recorded, and the round is scored on the items that remain.
|
|
|
847
864
|
**Probe** (only when Claim is fact): <one line of JSON, e.g. {"kind": "path-absent", "path": "src/missing.ts"}>
|
|
848
865
|
**Note**: <the falsification candidate considered and why it is excluded; required even for AGREE>
|
|
849
866
|
**Explanation**: <2-3 sentences>
|
|
867
|
+
**Dependencies**: {"complete": false, "itemIds": [], "paths": []}
|
|
868
|
+
|
|
869
|
+
Keep the detailed Note and Explanation. Declare all direct and transitive plan-item and whole-file evidence dependencies. Set `complete` to true only when that boundary is known. Include source files behind references. External or dynamic evidence, unlisted narrative dependencies and unresolved references keep `complete` false. This declaration controls reuse eligibility, not the verdict itself. Missing and malformed declarations are treated as unknown by `verdict_blocks._block` and `plan_item_evidence.review_fingerprint`.
|
|
850
870
|
|
|
851
871
|
### P-Opt-2
|
|
852
872
|
...
|
|
@@ -68,6 +68,7 @@ from okstra_ctl.design_surfaces import DesignSurfaceError # noqa: E402
|
|
|
68
68
|
|
|
69
69
|
from okstra_ctl.plan_items import ( # noqa: E402
|
|
70
70
|
content_hash,
|
|
71
|
+
PlanItemContractError,
|
|
71
72
|
expected_plan_item_ids,
|
|
72
73
|
extract_plan_items,
|
|
73
74
|
)
|
|
@@ -2543,6 +2544,7 @@ def validate_plan_body_section(
|
|
|
2543
2544
|
_validate_round_recorded_verdicts(data, failures)
|
|
2544
2545
|
_validate_verdicts_match_current_subjects(data, failures)
|
|
2545
2546
|
_validate_verdict_rounds_outlive_self_fix(data, failures)
|
|
2547
|
+
_validate_review_evidence(data, report_path, failures)
|
|
2546
2548
|
_validate_unresolved_tie_was_reverified(data, failures)
|
|
2547
2549
|
_validate_advisory_plan_body_gating(data, failures)
|
|
2548
2550
|
_validate_plan_item_extraction_completeness(data, failures)
|
|
@@ -2553,6 +2555,37 @@ def validate_plan_body_section(
|
|
|
2553
2555
|
return [*_detect_self_fix_recurrence(pbv), *_detect_uniform_verifier(pbv)]
|
|
2554
2556
|
|
|
2555
2557
|
|
|
2558
|
+
def _validate_review_evidence(data: dict, report_path: Path, failures: list[str]) -> None:
|
|
2559
|
+
from okstra_ctl.convergence import validated_run_authority, ConvergenceContractError
|
|
2560
|
+
from okstra_ctl.plan_item_evidence import capture_review_inputs, review_fingerprint
|
|
2561
|
+
|
|
2562
|
+
planning = data.get("implementationPlanning") or {}
|
|
2563
|
+
pbv = planning.get("planBodyVerification") or {}
|
|
2564
|
+
rows = pbv.get("planItems") or []
|
|
2565
|
+
if not any(row.get("verifiedReviewHash") for row in rows):
|
|
2566
|
+
return
|
|
2567
|
+
sequences = _manifest_seq_for_report(report_path, "manifests")
|
|
2568
|
+
if len(sequences) != 1:
|
|
2569
|
+
failures.append("plan review evidence has no unambiguous run manifest")
|
|
2570
|
+
return
|
|
2571
|
+
path = report_path.parent.parent / "manifests" / (
|
|
2572
|
+
f"run-manifest-implementation-planning-{next(iter(sequences))}.json"
|
|
2573
|
+
)
|
|
2574
|
+
try:
|
|
2575
|
+
authority = validated_run_authority(path)
|
|
2576
|
+
inputs = capture_review_inputs(
|
|
2577
|
+
extract_plan_items(planning), planning, authority.payload, authority.project_root, rows,
|
|
2578
|
+
)
|
|
2579
|
+
except (ConvergenceContractError, PlanItemContractError) as exc:
|
|
2580
|
+
failures.append(f"cannot validate plan review evidence: {exc}")
|
|
2581
|
+
return
|
|
2582
|
+
stale = [row["id"] for row in rows if _item_stage_scope_bucket(row, pbv) == "in-scope"
|
|
2583
|
+
and row.get("verifiedReviewHash")
|
|
2584
|
+
and row["verifiedReviewHash"] != review_fingerprint(row, inputs)]
|
|
2585
|
+
if stale:
|
|
2586
|
+
failures.append(f"plan review evidence changed for {stale}; prepare and re-verify those items")
|
|
2587
|
+
|
|
2588
|
+
|
|
2556
2589
|
def _gate_summary_item(
|
|
2557
2590
|
item: dict,
|
|
2558
2591
|
pbv: dict,
|