okstra 0.180.0 → 0.183.2
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/dist/cli-registry.mjs +16 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture.md +11 -0
- package/docs/cli.md +26 -13
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +29 -26
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +3 -3
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +3 -7
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +8 -6
- package/runtime/prompts/lead/okstra-lead-contract.md +25 -35
- package/runtime/prompts/lead/plan-body-verification.md +187 -14
- package/runtime/prompts/lead/report-writer.md +20 -1
- package/runtime/prompts/lead/team-contract.md +8 -53
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +8 -2
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +6 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +118 -35
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +65 -18
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +81 -1
- package/runtime/python/okstra_ctl/approval_decisions.py +3 -2
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/dispatch_core.py +35 -65
- package/runtime/python/okstra_ctl/dispatch_state.py +134 -59
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/final_report_schema.py +2 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +2 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +8 -3
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/paths.py +9 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +513 -18
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +26 -15
- package/runtime/python/okstra_ctl/render_final_report.py +3 -2
- package/runtime/python/okstra_ctl/report_assembly.py +172 -17
- package/runtime/python/okstra_ctl/report_finalize.py +7 -10
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +13 -1
- package/runtime/python/okstra_ctl/report_narrative.py +40 -8
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +159 -56
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +4 -2
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -2
- package/runtime/python/okstra_ctl/stage_map.py +26 -6
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +2 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -7
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +9 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +17 -1
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +379 -63
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -2997,6 +2997,9 @@ def validate_report(
|
|
|
2997
2997
|
|
|
2998
2998
|
|
|
2999
2999
|
_REPORT_BASENAME_SEQ_RE = re.compile(r"-(?P<seq>\d{3})(?:\.data)?\.(?:md|json)$")
|
|
3000
|
+
_REPORT_BASENAME_TASK_TYPE_RE = re.compile(
|
|
3001
|
+
r"^final-report-(?P<task_type>[a-z][a-z-]*?)-\d{3}(?:\.data)?\.(?:md|json)$"
|
|
3002
|
+
)
|
|
3000
3003
|
|
|
3001
3004
|
|
|
3002
3005
|
def _report_run_seq(report_path: Path) -> str | None:
|
|
@@ -3007,6 +3010,25 @@ def _report_run_seq(report_path: Path) -> str | None:
|
|
|
3007
3010
|
return match.group("seq") if match else None
|
|
3008
3011
|
|
|
3009
3012
|
|
|
3013
|
+
def _report_task_type(report_path: Path) -> str:
|
|
3014
|
+
"""This run's task type, read off `final-report-<task-type>-<seq>`.
|
|
3015
|
+
|
|
3016
|
+
The report's own `header.taskType` is the first source, but it is not
|
|
3017
|
+
always reachable. `report_narrative._allowed_top_level()` has no `header` —
|
|
3018
|
+
it is not a writer-owned block — so a gate scored from a narrative, which is
|
|
3019
|
+
the only input a report-contract-3.0 run has before assembly, carries no
|
|
3020
|
+
task type at all. Globbing with an empty one matched nothing and reported
|
|
3021
|
+
every verdict as unbacked under a `runs//worker-results/` path.
|
|
3022
|
+
|
|
3023
|
+
The filename carries it in every caller: the full-run path passes the report
|
|
3024
|
+
itself, and `_report_path_for_state` builds the same canonical name from the
|
|
3025
|
+
state file. Returns `""` when the name does not carry one, so a caller can
|
|
3026
|
+
tell "not resolvable" from a real task type.
|
|
3027
|
+
"""
|
|
3028
|
+
match = _REPORT_BASENAME_TASK_TYPE_RE.match(report_path.name)
|
|
3029
|
+
return match.group("task_type") if match else ""
|
|
3030
|
+
|
|
3031
|
+
|
|
3010
3032
|
def validate_worker_results_audit(
|
|
3011
3033
|
report_path: Path,
|
|
3012
3034
|
task_type: str,
|
|
@@ -3090,29 +3112,21 @@ PLAN_VERIFY_GATE_VALUES = (
|
|
|
3090
3112
|
# the approved-frontmatter gate (the `.match` would otherwise return None).
|
|
3091
3113
|
_FRONTMATTER_BLOCK_RE = re.compile(r"\A\ufeff?\s*---\n(.*?)\n---\n", re.DOTALL)
|
|
3092
3114
|
def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
|
|
3093
|
-
"""
|
|
3094
|
-
|
|
3095
|
-
The markdown check above compares only the Verdict Token, so a Card whose
|
|
3096
|
-
Direction contradicted §7 shipped silently. `nextStep` is deliberately NOT
|
|
3097
|
-
compared: every shipped fixture authors the Card's cell as the actionable
|
|
3098
|
-
command (`/okstra-run task-type=release-handoff`) while §7 states the same
|
|
3099
|
-
action as prose ("Proceed to release-handoff."), so a byte-match rule there
|
|
3100
|
-
would reject the reference reports.
|
|
3101
|
-
"""
|
|
3115
|
+
"""Verdict Card의 정본 표시 값이 §7의 값과 같은지 모두 검사한다."""
|
|
3102
3116
|
card = data.get("verdictCard")
|
|
3103
3117
|
final = data.get("finalVerdict")
|
|
3104
3118
|
if not isinstance(card, dict) or not isinstance(final, dict):
|
|
3105
3119
|
return
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3120
|
+
for field in ("finalConclusion", "direction", "nextStep"):
|
|
3121
|
+
card_value = str(card.get(field) or "").strip()
|
|
3122
|
+
final_value = str(final.get(field) or "").strip()
|
|
3123
|
+
if not card_value or not final_value or card_value == final_value:
|
|
3124
|
+
continue
|
|
3125
|
+
failures.append(
|
|
3126
|
+
f"final-report data.json: verdictCard.{field} value `{card_value}` "
|
|
3127
|
+
f"does not match finalVerdict.{field} value `{final_value}` — the "
|
|
3128
|
+
"Card is a non-authoritative index and must preserve the §7 value."
|
|
3129
|
+
)
|
|
3116
3130
|
|
|
3117
3131
|
|
|
3118
3132
|
def _route_target_matches(value: Any, target: str, *, command: bool) -> bool:
|
|
@@ -3585,8 +3599,12 @@ def validate_final_report_data(
|
|
|
3585
3599
|
# namespaced audit ref (claude:F-005), or a §section reference (§5.4) — any one
|
|
3586
3600
|
# satisfies "this claim is anchored". A bare brief/worker ID (RC-*/RF-*/F-*) is
|
|
3587
3601
|
# handled separately by `_validate_no_opaque_id_references`.
|
|
3588
|
-
|
|
3589
|
-
|
|
3602
|
+
_PATH_LINE_EVIDENCE_TOKEN = re.compile(r"[\w./-]+\.\w+:\d+")
|
|
3603
|
+
_SECTION_EVIDENCE_TOKEN = re.compile(r"§\s*\d")
|
|
3604
|
+
_IN_REPORT_ID_TOKEN = re.compile(r"\b[A-Z]{1,3}(?:-[a-z]+)?-\d+\b")
|
|
3605
|
+
_NAMESPACED_EVIDENCE_TOKEN = re.compile(
|
|
3606
|
+
r"(?<![\w-])[a-z][a-z0-9-]*:[A-Z]{1,3}(?:-[a-z]+)?-\d+(?![\w-])"
|
|
3607
|
+
)
|
|
3590
3608
|
# Explicit "I don't know" escapes — anti-fabrication's other valid answer.
|
|
3591
3609
|
_INSUFFICIENCY_MARKERS = (
|
|
3592
3610
|
"근거 불충분", "근거가 불충분", "증거 불충분", "증거가 불충분",
|
|
@@ -3596,6 +3614,44 @@ _INSUFFICIENCY_MARKERS = (
|
|
|
3596
3614
|
_RATIONALE_FIELDS = ("motivation", "problem", "approach", "justification")
|
|
3597
3615
|
|
|
3598
3616
|
|
|
3617
|
+
def _report_reference_ids(data: Mapping[str, Any]) -> set[str]:
|
|
3618
|
+
references: set[str] = set()
|
|
3619
|
+
|
|
3620
|
+
def visit(value: Any, key: str = "") -> None:
|
|
3621
|
+
if isinstance(value, Mapping):
|
|
3622
|
+
for child_key, child_value in value.items():
|
|
3623
|
+
visit(child_value, str(child_key))
|
|
3624
|
+
return
|
|
3625
|
+
if isinstance(value, list):
|
|
3626
|
+
for child in value:
|
|
3627
|
+
visit(child, key)
|
|
3628
|
+
return
|
|
3629
|
+
if not isinstance(value, str):
|
|
3630
|
+
return
|
|
3631
|
+
if (key == "id" or key.endswith("Id")) and _IN_REPORT_ID_TOKEN.fullmatch(value):
|
|
3632
|
+
references.add(value)
|
|
3633
|
+
if key == "sourceItems":
|
|
3634
|
+
references.update(_NAMESPACED_EVIDENCE_TOKEN.findall(value))
|
|
3635
|
+
|
|
3636
|
+
visit(data)
|
|
3637
|
+
return references
|
|
3638
|
+
|
|
3639
|
+
|
|
3640
|
+
def _rationale_reference_status(
|
|
3641
|
+
text: str,
|
|
3642
|
+
known_references: set[str],
|
|
3643
|
+
) -> tuple[bool, tuple[str, ...]]:
|
|
3644
|
+
if _PATH_LINE_EVIDENCE_TOKEN.search(text) or _SECTION_EVIDENCE_TOKEN.search(text):
|
|
3645
|
+
return True, ()
|
|
3646
|
+
namespaced = tuple(_NAMESPACED_EVIDENCE_TOKEN.findall(text))
|
|
3647
|
+
without_namespaced = _NAMESPACED_EVIDENCE_TOKEN.sub("", text)
|
|
3648
|
+
report_ids = tuple(_IN_REPORT_ID_TOKEN.findall(without_namespaced))
|
|
3649
|
+
candidates = (*namespaced, *report_ids)
|
|
3650
|
+
if any(candidate in known_references for candidate in candidates):
|
|
3651
|
+
return True, ()
|
|
3652
|
+
return False, tuple(dict.fromkeys(candidates))
|
|
3653
|
+
|
|
3654
|
+
|
|
3599
3655
|
def _validate_rationale_evidence(data: dict, failures: list[str]) -> None:
|
|
3600
3656
|
"""Every `## 작업 배경과 근거` field must anchor its claim: carry at least
|
|
3601
3657
|
one evidence reference (path:line, report ID, §section) OR an explicit
|
|
@@ -3605,14 +3661,25 @@ def _validate_rationale_evidence(data: dict, failures: list[str]) -> None:
|
|
|
3605
3661
|
rationale = data.get("rationale")
|
|
3606
3662
|
if not isinstance(rationale, dict):
|
|
3607
3663
|
return # absence/shape is the schema's job; don't double-report.
|
|
3664
|
+
known_references = _report_reference_ids(data)
|
|
3608
3665
|
for field in _RATIONALE_FIELDS:
|
|
3609
3666
|
text = rationale.get(field)
|
|
3610
3667
|
if not isinstance(text, str):
|
|
3611
3668
|
continue
|
|
3612
|
-
|
|
3669
|
+
grounded, unknown_references = _rationale_reference_status(
|
|
3670
|
+
text,
|
|
3671
|
+
known_references,
|
|
3672
|
+
)
|
|
3673
|
+
if grounded:
|
|
3613
3674
|
continue
|
|
3614
3675
|
if any(m in text.lower() for m in _INSUFFICIENCY_MARKERS):
|
|
3615
3676
|
continue
|
|
3677
|
+
if unknown_references:
|
|
3678
|
+
failures.append(
|
|
3679
|
+
f"final-report data.json: rationale.{field} cites unknown "
|
|
3680
|
+
"in-report reference(s): " + ", ".join(unknown_references) + "."
|
|
3681
|
+
)
|
|
3682
|
+
continue
|
|
3616
3683
|
failures.append(
|
|
3617
3684
|
f"final-report data.json: rationale.{field} cites no evidence "
|
|
3618
3685
|
f"(expected a path:line, an in-report ID like C-001, or a §"
|
|
@@ -3787,6 +3854,42 @@ def _is_variation_point_item(item: dict) -> bool:
|
|
|
3787
3854
|
return str(item.get("id") or "").upper().startswith("P-VAR")
|
|
3788
3855
|
|
|
3789
3856
|
|
|
3857
|
+
def _single_vote_dissents(item: dict, kinds: set[str]) -> list[dict]:
|
|
3858
|
+
"""이 항목에서 1표 차단을 주장하는 DISAGREE 행들."""
|
|
3859
|
+
return [
|
|
3860
|
+
row for row in (item.get("verdicts") or [])
|
|
3861
|
+
if isinstance(row, dict)
|
|
3862
|
+
and str(row.get("verdict") or "").strip().upper() == "DISAGREE"
|
|
3863
|
+
and str(row.get("breakageKind") or "").strip().lower() in kinds
|
|
3864
|
+
]
|
|
3865
|
+
|
|
3866
|
+
|
|
3867
|
+
def _single_vote_block_survives(item: dict, kinds: set[str]) -> bool:
|
|
3868
|
+
"""1표 차단이 성립하는지.
|
|
3869
|
+
|
|
3870
|
+
1표 차단에는 근거가 있다 — 명시된 두 인용이 서로 모순이라는 것은 한 명이
|
|
3871
|
+
실측으로 확정할 수 있는 사실이고, 사실을 다수결로 기각하면 안 된다. 문제는
|
|
3872
|
+
1표라는 것이 아니라 **1표에 재현 요구가 없었다**는 것이다. "이 경로는 존재하지
|
|
3873
|
+
않는다" 라고 쓰기만 하면 그대로 차단이 됐다.
|
|
3874
|
+
|
|
3875
|
+
이제 주장이 스스로 `fact` 를 선언하고 okstra 가 그것을 재현했을 때만 1표로
|
|
3876
|
+
막는다. 선언했는데 재현되지 않았거나 `judgement` 였다면 정족수로 내려간다.
|
|
3877
|
+
|
|
3878
|
+
아무 행도 `claimKind` 를 선언하지 않았으면 종전대로 막는다. 그 필드를 실을 수
|
|
3879
|
+
없던 시절의 판정을 뒤에서 뒤집지 않기 위해서다 — 도입은 완화 방향으로만
|
|
3880
|
+
작동하고, 선언한 주장만 재현을 요구받는다.
|
|
3881
|
+
"""
|
|
3882
|
+
dissents = _single_vote_dissents(item, kinds)
|
|
3883
|
+
declared = [row for row in dissents if row.get("claimKind")]
|
|
3884
|
+
if not declared:
|
|
3885
|
+
return bool(dissents)
|
|
3886
|
+
return any(
|
|
3887
|
+
str(row.get("claimKind") or "") == "fact"
|
|
3888
|
+
and str(row.get("reproductionResult") or "") == "reproduced"
|
|
3889
|
+
for row in declared
|
|
3890
|
+
)
|
|
3891
|
+
|
|
3892
|
+
|
|
3790
3893
|
def _classify_plan_item_gate(item: dict) -> str:
|
|
3791
3894
|
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
3792
3895
|
per `prompts/lead/plan-body-verification.md` "Round protocol". Returns one of
|
|
@@ -3831,8 +3934,11 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3831
3934
|
# `a` for any item except `P-Var-*` (majority-gated, see
|
|
3832
3935
|
# `_is_variation_point_item`); `f` only for P-Req items (requirement coverage).
|
|
3833
3936
|
is_req = str(item.get("id") or "").upper().startswith("P-REQ")
|
|
3834
|
-
|
|
3835
|
-
|
|
3937
|
+
single_vote_kinds = set(_SINGLE_VOTE_BLOCKING_KINDS) | ({"f"} if is_req else set())
|
|
3938
|
+
if (
|
|
3939
|
+
not _is_variation_point_item(item)
|
|
3940
|
+
and blocking_kinds & single_vote_kinds
|
|
3941
|
+
and _single_vote_block_survives(item, single_vote_kinds)
|
|
3836
3942
|
):
|
|
3837
3943
|
# "One confirmed DISAGREE" presupposes the item was actually
|
|
3838
3944
|
# cross-verified. When the peer returned a non-result nothing confirmed
|
|
@@ -4029,6 +4135,103 @@ def _is_dissent_downgraded(
|
|
|
4029
4135
|
)
|
|
4030
4136
|
|
|
4031
4137
|
|
|
4138
|
+
_STARTABLE_STAGE_STATUSES = frozenset({"ready", "active"})
|
|
4139
|
+
|
|
4140
|
+
|
|
4141
|
+
def _stage_scope_bucket(item: dict, pbv: dict) -> str:
|
|
4142
|
+
"""Whether this item has standing to block the stage about to start.
|
|
4143
|
+
|
|
4144
|
+
The plan covers every stage; implementation runs one at a time. Judging all
|
|
4145
|
+
of them at once means a defect in a stage nobody has reached, or in one
|
|
4146
|
+
already frozen, stops the next stage from starting — and a frozen stage's
|
|
4147
|
+
item cannot be fixed at all, because the Stage Ledger forbids editing its
|
|
4148
|
+
commands. Measured on one run, 9 of 13 blockers were that shape, 6 of them
|
|
4149
|
+
frozen.
|
|
4150
|
+
|
|
4151
|
+
Returns `in-scope` (may block), `observed` (only frozen stages), or
|
|
4152
|
+
`deferred` (only stages not yet startable). Anything unresolvable is
|
|
4153
|
+
`in-scope`: an absent ledger is no basis to narrow, and an item with no
|
|
4154
|
+
`stageScope` belongs to the plan as a whole — `P-Opt-*` and `P-Var-*` live
|
|
4155
|
+
there permanently, and scoping them out would stop an unrequested-work
|
|
4156
|
+
verdict from blocking a start.
|
|
4157
|
+
"""
|
|
4158
|
+
ledger = pbv.get("stageLedger")
|
|
4159
|
+
if not isinstance(ledger, dict) or not ledger:
|
|
4160
|
+
return "in-scope"
|
|
4161
|
+
scope = item.get("stageScope")
|
|
4162
|
+
stages = [
|
|
4163
|
+
value for value in scope
|
|
4164
|
+
if isinstance(value, int) and not isinstance(value, bool)
|
|
4165
|
+
] if isinstance(scope, list) else []
|
|
4166
|
+
if not stages:
|
|
4167
|
+
return "in-scope"
|
|
4168
|
+
statuses = {str(ledger.get(str(stage)) or "") for stage in stages}
|
|
4169
|
+
if statuses & _STARTABLE_STAGE_STATUSES:
|
|
4170
|
+
return "in-scope"
|
|
4171
|
+
return "observed" if "done" in statuses else "deferred"
|
|
4172
|
+
|
|
4173
|
+
|
|
4174
|
+
def _set_aside_reason(item: dict, pbv: dict, accepted_item_ids: set[str]) -> str | None:
|
|
4175
|
+
"""Why this item stopped blocking, or ``None`` if it never did.
|
|
4176
|
+
|
|
4177
|
+
A gate that passes while defects were set aside has to say which ones and on
|
|
4178
|
+
what grounds. Without that the two halves of the acceptance condition — the
|
|
4179
|
+
next stage can start, and the known risks are written down — collapse into
|
|
4180
|
+
the first, and a defect deferred for a good reason is indistinguishable in
|
|
4181
|
+
the record from one nobody found.
|
|
4182
|
+
"""
|
|
4183
|
+
raw = (
|
|
4184
|
+
"has-dissent"
|
|
4185
|
+
if _is_dissent_downgraded(item, pbv, accepted_item_ids)
|
|
4186
|
+
else _classify_plan_item_gate(item)
|
|
4187
|
+
)
|
|
4188
|
+
if raw != "majority-disagree":
|
|
4189
|
+
return None
|
|
4190
|
+
bucket = _stage_scope_bucket(item, pbv)
|
|
4191
|
+
if bucket != "in-scope":
|
|
4192
|
+
return bucket
|
|
4193
|
+
return "record" if str(item.get("block") or "") == "record" else None
|
|
4194
|
+
|
|
4195
|
+
|
|
4196
|
+
def _set_aside_register(pbv: dict, accepted_item_ids: set[str]) -> list[dict]:
|
|
4197
|
+
"""Every set-aside item, in id order, as the gate records them."""
|
|
4198
|
+
register = [
|
|
4199
|
+
{"id": str(item.get("id") or ""), "reason": reason}
|
|
4200
|
+
for item in (pbv.get("planItems") or [])
|
|
4201
|
+
if isinstance(item, dict)
|
|
4202
|
+
for reason in [_set_aside_reason(item, pbv, accepted_item_ids)]
|
|
4203
|
+
if reason is not None
|
|
4204
|
+
]
|
|
4205
|
+
return sorted(register, key=lambda row: row["id"])
|
|
4206
|
+
|
|
4207
|
+
|
|
4208
|
+
def _plan_item_gate_class(
|
|
4209
|
+
item: dict, pbv: dict, accepted_item_ids: set[str],
|
|
4210
|
+
) -> str:
|
|
4211
|
+
"""The gate class for one item, after stage scope is applied.
|
|
4212
|
+
|
|
4213
|
+
An out-of-scope blocker is not dropped — it lands on `has-dissent`, so the
|
|
4214
|
+
gate still reads `passed-with-dissent` rather than `passed` and the record
|
|
4215
|
+
says something is outstanding. Silently scoring it `passed` would hide the
|
|
4216
|
+
defect instead of deferring it.
|
|
4217
|
+
"""
|
|
4218
|
+
classification = (
|
|
4219
|
+
"has-dissent"
|
|
4220
|
+
if _is_dissent_downgraded(item, pbv, accepted_item_ids)
|
|
4221
|
+
else _classify_plan_item_gate(item)
|
|
4222
|
+
)
|
|
4223
|
+
if classification != "majority-disagree":
|
|
4224
|
+
return classification
|
|
4225
|
+
if _stage_scope_bucket(item, pbv) != "in-scope":
|
|
4226
|
+
return "has-dissent"
|
|
4227
|
+
if str(item.get("block") or "") == "record":
|
|
4228
|
+
# 자기 기록의 부정확은 기록되고 다음 run 의 입력이 되지, 구현 착수를 막지
|
|
4229
|
+
# 않는다. 요구사항이 실제로 안 만들어지는 경우는 이 경로가 아니라
|
|
4230
|
+
# `_independent_coverage_blockers` 의 `coverage-gap` 이 계속 막는다.
|
|
4231
|
+
return "has-dissent"
|
|
4232
|
+
return classification
|
|
4233
|
+
|
|
4234
|
+
|
|
4032
4235
|
def _recompute_plan_body_gate(
|
|
4033
4236
|
pbv: dict,
|
|
4034
4237
|
accepted_item_ids: set[str] | None = None,
|
|
@@ -4038,9 +4241,7 @@ def _recompute_plan_body_gate(
|
|
|
4038
4241
|
no plan items to judge (disabled / empty round)."""
|
|
4039
4242
|
accepted = accepted_item_ids or set()
|
|
4040
4243
|
classes = [
|
|
4041
|
-
|
|
4042
|
-
if _is_dissent_downgraded(it, pbv, accepted)
|
|
4043
|
-
else _classify_plan_item_gate(it)
|
|
4244
|
+
_plan_item_gate_class(it, pbv, accepted)
|
|
4044
4245
|
for it in (pbv.get("planItems") or [])
|
|
4045
4246
|
if isinstance(it, dict)
|
|
4046
4247
|
]
|
|
@@ -4062,6 +4263,60 @@ def _recompute_plan_body_gate(
|
|
|
4062
4263
|
return "passed"
|
|
4063
4264
|
|
|
4064
4265
|
|
|
4266
|
+
def _validate_set_aside_register(
|
|
4267
|
+
data: dict, failures: list[str], accepted_item_ids: set[str] | None = None,
|
|
4268
|
+
) -> None:
|
|
4269
|
+
"""A gate that set defects aside must say which ones and why.
|
|
4270
|
+
|
|
4271
|
+
The gate stops blocking on a defect that belongs to a frozen or unreached
|
|
4272
|
+
stage, or to the plan's own record. That is the point — but a reader of the
|
|
4273
|
+
report cannot tell such a defect from one that was never raised unless the
|
|
4274
|
+
run writes the register down. `gateBlockedBy` names what blocked; this names
|
|
4275
|
+
what did not, and why.
|
|
4276
|
+
|
|
4277
|
+
Declared against recomputed, the same shape as `_validate_gate_blocked_by`:
|
|
4278
|
+
a hand-written register drifts from the verdicts it claims to summarise, and
|
|
4279
|
+
the drift is invisible precisely because nothing else reads it.
|
|
4280
|
+
"""
|
|
4281
|
+
ip = data.get("implementationPlanning")
|
|
4282
|
+
if not isinstance(ip, dict):
|
|
4283
|
+
return
|
|
4284
|
+
pbv = ip.get("planBodyVerification")
|
|
4285
|
+
if not isinstance(pbv, dict):
|
|
4286
|
+
return
|
|
4287
|
+
round_count = pbv.get("roundCount")
|
|
4288
|
+
if not isinstance(round_count, int) or round_count < 1:
|
|
4289
|
+
return
|
|
4290
|
+
accepted = (
|
|
4291
|
+
_resolved_noncritical_dissent_ids(data)
|
|
4292
|
+
if accepted_item_ids is None
|
|
4293
|
+
else accepted_item_ids
|
|
4294
|
+
)
|
|
4295
|
+
expected = _set_aside_register(pbv, accepted)
|
|
4296
|
+
declared_raw = pbv.get("setAside")
|
|
4297
|
+
declared = sorted(
|
|
4298
|
+
(
|
|
4299
|
+
{"id": str(row.get("id") or ""), "reason": str(row.get("reason") or "")}
|
|
4300
|
+
for row in declared_raw
|
|
4301
|
+
if isinstance(row, dict)
|
|
4302
|
+
),
|
|
4303
|
+
key=lambda row: row["id"],
|
|
4304
|
+
) if isinstance(declared_raw, list) else None
|
|
4305
|
+
if declared == expected:
|
|
4306
|
+
return
|
|
4307
|
+
if declared is None and not expected:
|
|
4308
|
+
return
|
|
4309
|
+
failures.append(
|
|
4310
|
+
"final-report data.json: planBodyVerification.setAside is "
|
|
4311
|
+
f"{declared!r} but the recorded verdicts set aside {expected!r}. The "
|
|
4312
|
+
"gate stopped blocking on those items — a frozen or unreached stage, or "
|
|
4313
|
+
"the plan's own record — and a run that passes without listing them "
|
|
4314
|
+
"leaves a deferred defect indistinguishable from one nobody raised. "
|
|
4315
|
+
"Re-record the round with `okstra plan-items complete-round`, which "
|
|
4316
|
+
"writes this register from the same computation."
|
|
4317
|
+
)
|
|
4318
|
+
|
|
4319
|
+
|
|
4065
4320
|
def _validate_plan_body_gate_recompute(
|
|
4066
4321
|
data: dict,
|
|
4067
4322
|
failures: list[str],
|
|
@@ -4121,6 +4376,29 @@ def _blocks_approval(row: dict) -> bool:
|
|
|
4121
4376
|
return status == "documented-deviation" and disposition.startswith("blocked C-")
|
|
4122
4377
|
|
|
4123
4378
|
|
|
4379
|
+
def _plan_item_clarification_ids(item: object) -> set[str]:
|
|
4380
|
+
"""이 plan item 이 가리키는 `C-NNN` 들.
|
|
4381
|
+
|
|
4382
|
+
계약 v3 에서 리포트 정본의 이 링크는 복수형 `clarificationRefs[]` 다 —
|
|
4383
|
+
`report_assembly` 가 활동 원장의 `clarificationRefs[]` + `planItemIds[]` 에서
|
|
4384
|
+
유도해 쓰고, v3.0 스키마의 `planItems[]` 는 `additionalProperties: false` 아래
|
|
4385
|
+
그 이름만 허용한다. 단수형 `clarificationId` 는 lead 가 쓰는 plan-body 상태
|
|
4386
|
+
파일과 v2 리포트에 남아 있으므로 읽을 때는 둘 다 받는다
|
|
4387
|
+
(`incremental_scope` 가 이미 그렇게 한다).
|
|
4388
|
+
"""
|
|
4389
|
+
if not isinstance(item, dict):
|
|
4390
|
+
return set()
|
|
4391
|
+
ids = {
|
|
4392
|
+
str(ref).strip()
|
|
4393
|
+
for ref in (item.get("clarificationRefs") or [])
|
|
4394
|
+
if str(ref).strip()
|
|
4395
|
+
}
|
|
4396
|
+
single = item.get("clarificationId")
|
|
4397
|
+
if isinstance(single, str) and single.strip():
|
|
4398
|
+
ids.add(single.strip())
|
|
4399
|
+
return ids
|
|
4400
|
+
|
|
4401
|
+
|
|
4124
4402
|
def _plan_body_promoted_clarification_ids(pbv: dict) -> set[str]:
|
|
4125
4403
|
"""`C-NNN` ids this run's own plan-body round created by promoting a
|
|
4126
4404
|
majority-disagree item (step 8). Used to break the Requirement Coverage
|
|
@@ -4128,11 +4406,9 @@ def _plan_body_promoted_clarification_ids(pbv: dict) -> set[str]:
|
|
|
4128
4406
|
the gate already counted, rather than contributing an independent one.
|
|
4129
4407
|
"""
|
|
4130
4408
|
return {
|
|
4131
|
-
|
|
4409
|
+
clarification_id
|
|
4132
4410
|
for item in (pbv.get("planItems") or [])
|
|
4133
|
-
|
|
4134
|
-
and isinstance(item.get("clarificationId"), str)
|
|
4135
|
-
and item["clarificationId"].strip()
|
|
4411
|
+
for clarification_id in _plan_item_clarification_ids(item)
|
|
4136
4412
|
}
|
|
4137
4413
|
|
|
4138
4414
|
|
|
@@ -5793,8 +6069,7 @@ def _has_clarification_backtrace(
|
|
|
5793
6069
|
resolver cannot disagree about what counts as a link.
|
|
5794
6070
|
"""
|
|
5795
6071
|
if isinstance(plan_items, list) and any(
|
|
5796
|
-
|
|
5797
|
-
for item in plan_items
|
|
6072
|
+
row_id in _plan_item_clarification_ids(item) for item in plan_items
|
|
5798
6073
|
):
|
|
5799
6074
|
return True
|
|
5800
6075
|
return isinstance(coverage, list) and any(
|
|
@@ -6278,6 +6553,7 @@ def _validate_plan_body_state_file(
|
|
|
6278
6553
|
data: dict,
|
|
6279
6554
|
report_path: Path,
|
|
6280
6555
|
failures: list[str],
|
|
6556
|
+
state_path: Path | None = None,
|
|
6281
6557
|
) -> None:
|
|
6282
6558
|
"""The per-round state file must exist once a round has run.
|
|
6283
6559
|
|
|
@@ -6301,20 +6577,34 @@ def _validate_plan_body_state_file(
|
|
|
6301
6577
|
round_count = pbv.get("roundCount")
|
|
6302
6578
|
if not isinstance(round_count, int) or round_count < 1:
|
|
6303
6579
|
return
|
|
6304
|
-
seq = _report_run_seq(report_path)
|
|
6305
|
-
if seq is None:
|
|
6306
|
-
return
|
|
6307
6580
|
state_dir = report_path.parent.parent / "state"
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6581
|
+
if state_path is not None:
|
|
6582
|
+
# 호출자가 경로를 넘겼으면 그걸 본다. 리드는 launch 프롬프트의
|
|
6583
|
+
# `Run Paths` 에서 정본 경로를 받으므로, 여기서 이름을 다시 만들면
|
|
6584
|
+
# 그 정본과 어긋날 수 있다 — 실제로 그랬다.
|
|
6585
|
+
written = [state_path] if state_path.is_file() else []
|
|
6586
|
+
else:
|
|
6587
|
+
# 이름을 유도할 근거가 없다. run 은 seq 계열을 둘 갖고(`state` /
|
|
6588
|
+
# `reports`) 리포트 정본은 자기 run 의 state seq 를 담지 않으므로,
|
|
6589
|
+
# 리포트 seq 로 만든 이름은 추측이다. 이 검사가 묻는 것은 "덮어써진
|
|
6590
|
+
# 라운드의 기록이 남았는가" 이지 파일 이름이 아니므로, 이 run 의 상태
|
|
6591
|
+
# 디렉터리에 사이드카가 있는지만 본다. 이름의 정본은 `paths.py` 다.
|
|
6592
|
+
written = sorted(
|
|
6593
|
+
state_dir.glob("plan-body-verification-implementation-planning-*.json")
|
|
6594
|
+
)
|
|
6595
|
+
if not written:
|
|
6596
|
+
failures.append(
|
|
6597
|
+
f"plan-body verification ran ({round_count} round(s)) but no "
|
|
6598
|
+
f"`state/plan-body-verification-*.json` was written. It is the only "
|
|
6599
|
+
"record of superseded rounds — data.json keeps just the final "
|
|
6600
|
+
"verdicts, so without it a self-fixed run leaves no trace of what "
|
|
6601
|
+
'the earlier rounds found (plan-body-verification.md §"schema"). '
|
|
6602
|
+
"The path is rendered into the launch prompt's `Run Paths` block; "
|
|
6603
|
+
"write it there rather than deriving a name."
|
|
6604
|
+
)
|
|
6605
|
+
return
|
|
6606
|
+
# 여럿이면 가장 최신(seq 가 큰) 것이 이 run 의 것이다.
|
|
6607
|
+
expected = written[-1]
|
|
6318
6608
|
try:
|
|
6319
6609
|
state = json.loads(expected.read_text(encoding="utf-8"))
|
|
6320
6610
|
except (OSError, json.JSONDecodeError) as exc:
|
|
@@ -7132,7 +7422,15 @@ def _validate_plan_body_verdict_provenance(
|
|
|
7132
7422
|
if not voters:
|
|
7133
7423
|
return
|
|
7134
7424
|
|
|
7135
|
-
task_type =
|
|
7425
|
+
task_type = (
|
|
7426
|
+
str((data.get("header") or {}).get("taskType") or "")
|
|
7427
|
+
or _report_task_type(report_path)
|
|
7428
|
+
)
|
|
7429
|
+
if not task_type:
|
|
7430
|
+
# Neither source names it, so the glob below would be built from an
|
|
7431
|
+
# empty segment and match nothing — reporting every verdict as unbacked
|
|
7432
|
+
# on the strength of a path this check could not construct.
|
|
7433
|
+
return
|
|
7136
7434
|
dispatched = _plan_verify_result_workers(report_path, task_type)
|
|
7137
7435
|
if dispatched is None:
|
|
7138
7436
|
return
|
|
@@ -7800,15 +8098,20 @@ def _validate_plan_body_clarification_matching(
|
|
|
7800
8098
|
if _is_dissent_downgraded(item, pbv, accepted):
|
|
7801
8099
|
continue
|
|
7802
8100
|
item_id = item.get("id") or "<unknown>"
|
|
7803
|
-
|
|
7804
|
-
if not
|
|
8101
|
+
cids = _plan_item_clarification_ids(item)
|
|
8102
|
+
if not cids:
|
|
7805
8103
|
failures.append(
|
|
7806
8104
|
f"final-report data.json: plan item `{item_id}` is majority-disagree "
|
|
7807
|
-
"but carries no `
|
|
8105
|
+
"but carries no `clarificationRefs`. A blocking disagreement MUST "
|
|
7808
8106
|
"surface as a `## 1. Clarification Items` row (blocks=approval) so "
|
|
7809
|
-
"the user sees the blocker (implementation-planning.md self-review
|
|
8107
|
+
"the user sees the blocker (implementation-planning.md self-review "
|
|
8108
|
+
"step 12). Report assembly derives this link from the activity "
|
|
8109
|
+
"ledger's `clarificationRefs[]` + `planItemIds[]`, so record the "
|
|
8110
|
+
"decision through `okstra approval-decision` rather than editing "
|
|
8111
|
+
"the report."
|
|
7810
8112
|
)
|
|
7811
|
-
|
|
8113
|
+
continue
|
|
8114
|
+
for cid in sorted(cids - approval_ids):
|
|
7812
8115
|
reason = (
|
|
7813
8116
|
"references a non-existent §1 row"
|
|
7814
8117
|
if cid not in all_ids
|
|
@@ -7816,8 +8119,9 @@ def _validate_plan_body_clarification_matching(
|
|
|
7816
8119
|
)
|
|
7817
8120
|
failures.append(
|
|
7818
8121
|
f"final-report data.json: plan item `{item_id}` (majority-disagree) "
|
|
7819
|
-
f"has
|
|
7820
|
-
"item MUST
|
|
8122
|
+
f"has clarificationRefs entry `{cid}` which {reason}. Every "
|
|
8123
|
+
"majority-disagree item MUST reach a `blocks: approval` "
|
|
8124
|
+
"Clarification row."
|
|
7821
8125
|
)
|
|
7822
8126
|
|
|
7823
8127
|
|
|
@@ -7984,6 +8288,7 @@ def validate_plan_body_section(
|
|
|
7984
8288
|
_validate_aborted_gate_has_clarification(data, failures)
|
|
7985
8289
|
_validate_round_recorded_verdicts(data, failures)
|
|
7986
8290
|
_validate_verdicts_match_current_subjects(data, failures)
|
|
8291
|
+
_validate_set_aside_register(data, failures, accepted_item_ids)
|
|
7987
8292
|
_validate_verdict_rounds_outlive_self_fix(data, failures)
|
|
7988
8293
|
_validate_unresolved_tie_was_reverified(data, failures)
|
|
7989
8294
|
_validate_plan_item_extraction_completeness(data, failures)
|
|
@@ -8000,16 +8305,16 @@ def _gate_summary_item(
|
|
|
8000
8305
|
accepted_item_ids: set[str],
|
|
8001
8306
|
) -> dict:
|
|
8002
8307
|
"""One `gate.items[]` row: the gate class plus its state-file counterpart."""
|
|
8003
|
-
classification = (
|
|
8004
|
-
"has-dissent"
|
|
8005
|
-
if _is_dissent_downgraded(item, pbv, accepted_item_ids)
|
|
8006
|
-
else _classify_plan_item_gate(item)
|
|
8007
|
-
)
|
|
8308
|
+
classification = _plan_item_gate_class(item, pbv, accepted_item_ids)
|
|
8008
8309
|
return {
|
|
8009
8310
|
"id": item.get("id"),
|
|
8010
8311
|
"classification": classification,
|
|
8011
8312
|
"stateClassification": _state_classification(item, classification),
|
|
8012
8313
|
"correctnessCritical": _is_correctness_critical(item),
|
|
8314
|
+
# 왜 안 막는지가 기록에 남아야 한다. 이 값이 없으면 범위 밖 강등과
|
|
8315
|
+
# 실제 합의가 산출물에서 같은 모양으로 읽힌다.
|
|
8316
|
+
"stageScope": _stage_scope_bucket(item, pbv),
|
|
8317
|
+
"block": str(item.get("block") or "execution"),
|
|
8013
8318
|
}
|
|
8014
8319
|
|
|
8015
8320
|
|
|
@@ -8050,6 +8355,7 @@ def plan_body_gate_summary(data: dict) -> dict | None:
|
|
|
8050
8355
|
_gate_blocking_causes(pbv, coverage_blockers, accepted_item_ids)
|
|
8051
8356
|
),
|
|
8052
8357
|
"coverageBlockers": coverage_blockers,
|
|
8358
|
+
"setAside": _set_aside_register(pbv, accepted_item_ids),
|
|
8053
8359
|
"blockingItems": [
|
|
8054
8360
|
item["id"] for item in items if item["classification"] == "majority-disagree"
|
|
8055
8361
|
],
|
|
@@ -9425,7 +9731,9 @@ SECTION_PLAN_BODY = "plan-body"
|
|
|
9425
9731
|
_FULL_ONLY_REQUIRED_FLAGS = ("team_state", "run_manifest", "task_manifest")
|
|
9426
9732
|
|
|
9427
9733
|
|
|
9428
|
-
def run_plan_body_section(
|
|
9734
|
+
def run_plan_body_section(
|
|
9735
|
+
report_path: Path, state_path: Path | None = None
|
|
9736
|
+
) -> int:
|
|
9429
9737
|
"""`--section plan-body` — the §5.5.9 checks a self-fix round can run on
|
|
9430
9738
|
its own, plus the recomputed gate the lead records for that round.
|
|
9431
9739
|
|
|
@@ -9452,7 +9760,7 @@ def run_plan_body_section(report_path: Path) -> int:
|
|
|
9452
9760
|
return 2
|
|
9453
9761
|
failures = _data_schema_failures(data)
|
|
9454
9762
|
warnings = validate_plan_body_section(data, report_path, failures)
|
|
9455
|
-
_validate_plan_body_state_file(data, report_path, failures)
|
|
9763
|
+
_validate_plan_body_state_file(data, report_path, failures, state_path)
|
|
9456
9764
|
payload = {
|
|
9457
9765
|
"ok": not failures,
|
|
9458
9766
|
"section": SECTION_PLAN_BODY,
|
|
@@ -9588,7 +9896,10 @@ def main() -> int:
|
|
|
9588
9896
|
Path(args.narrative).resolve(), Path(args.state).resolve()
|
|
9589
9897
|
)
|
|
9590
9898
|
if args.report and not args.narrative and not args.state:
|
|
9591
|
-
return run_plan_body_section(
|
|
9899
|
+
return run_plan_body_section(
|
|
9900
|
+
Path(args.report).resolve(),
|
|
9901
|
+
Path(args.state).resolve() if args.state else None,
|
|
9902
|
+
)
|
|
9592
9903
|
parser.error(
|
|
9593
9904
|
"--section plan-body requires either --report or both --narrative and --state"
|
|
9594
9905
|
)
|
|
@@ -9798,7 +10109,12 @@ def main() -> int:
|
|
|
9798
10109
|
validation_data, project_root, failures
|
|
9799
10110
|
)
|
|
9800
10111
|
if task_type == "implementation-planning":
|
|
9801
|
-
_validate_plan_body_state_file(
|
|
10112
|
+
_validate_plan_body_state_file(
|
|
10113
|
+
validation_data,
|
|
10114
|
+
report_path,
|
|
10115
|
+
failures,
|
|
10116
|
+
Path(args.state).resolve() if args.state else None,
|
|
10117
|
+
)
|
|
9802
10118
|
if task_type == "final-verification":
|
|
9803
10119
|
_validate_verification_target_match(
|
|
9804
10120
|
validation_data,
|