okstra 0.183.2 → 0.184.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/README.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +7 -4
- package/docs/project-structure-overview.md +1 -1
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +2 -3
- package/docs/task-process/error-analysis.md +3 -4
- package/docs/task-process/final-verification.md +2 -3
- package/docs/task-process/implementation-planning.md +2 -3
- package/docs/task-process/implementation.md +2 -3
- package/docs/task-process/release-handoff.md +3 -4
- package/docs/task-process/requirements-discovery.md +3 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +8 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +27 -19
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_implementation-executor.md +1 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +9 -11
- package/runtime/prompts/wizard/prompts.ko.json +7 -10
- package/runtime/python/okstra_ctl/conformance.py +37 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +84 -39
- package/runtime/python/okstra_ctl/plan_items.py +410 -1
- package/runtime/python/okstra_ctl/plan_items_cli.py +346 -31
- package/runtime/python/okstra_ctl/render.py +4 -0
- package/runtime/python/okstra_ctl/wizard.py +39 -73
- package/runtime/schemas/final-report-v2.0.schema.json +12 -0
- package/runtime/schemas/final-report-v3.0.schema.json +12 -0
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/validators/validate-run.py +164 -66
- package/runtime/validators/validate_session_conformance.py +24 -5
|
@@ -46,6 +46,7 @@ from okstra_ctl.conformance import ( # noqa: E402
|
|
|
46
46
|
detect_surfaces,
|
|
47
47
|
evaluate_conformance,
|
|
48
48
|
manifest_required_surfaces,
|
|
49
|
+
missing_declared_scripts,
|
|
49
50
|
normalize_conformance_script as _normalize_conformance_script,
|
|
50
51
|
parse_conformance_tests as _parse_conformance_tests,
|
|
51
52
|
qa_result_from_dict,
|
|
@@ -78,6 +79,10 @@ from okstra_ctl.report_translation import ( # noqa: E402
|
|
|
78
79
|
hangul_share,
|
|
79
80
|
)
|
|
80
81
|
from okstra_ctl.stage_citations import enumerated_stage_numbers # noqa: E402
|
|
82
|
+
from okstra_ctl.plan_items import ( # noqa: E402
|
|
83
|
+
advisory_plan_body_gating,
|
|
84
|
+
stage_scope_bucket as _item_stage_scope_bucket,
|
|
85
|
+
)
|
|
81
86
|
from okstra_ctl.incremental_scope import ( # noqa: E402
|
|
82
87
|
coverage_row_blocked_on,
|
|
83
88
|
stages_for_clarification,
|
|
@@ -2167,14 +2172,11 @@ def _planning_conformance_declarations(
|
|
|
2167
2172
|
|
|
2168
2173
|
|
|
2169
2174
|
def _validate_planning_conformance_declared(report_path: Path, failures: list[str]) -> None:
|
|
2170
|
-
"""
|
|
2171
|
-
|
|
2172
|
-
`
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
was never materialized only surfaces much later at the `implementation`
|
|
2176
|
-
entry gate. Matches by stageKey suffix (`-stage-<N>`) like
|
|
2177
|
-
`_scope_manifest_entries`; skips stages that took a `Conformance exemption:`.
|
|
2175
|
+
"""계획 단계는 `Conformance tests:` / `Conformance exemption:` 선언 형식만 본다.
|
|
2176
|
+
|
|
2177
|
+
스크립트 파일과 `runCommand` 는 매칭 implementation stage 가 만든다.
|
|
2178
|
+
선언만 있고 파일이 없는 것은 계획 게이트 실패가 아니다. 형식이 깨진
|
|
2179
|
+
`conformanceTests` 는 여전히 실패한다.
|
|
2178
2180
|
"""
|
|
2179
2181
|
data_path = report_path.with_suffix(".data.json")
|
|
2180
2182
|
if not data_path.is_file():
|
|
@@ -2186,28 +2188,7 @@ def _validate_planning_conformance_declared(report_path: Path, failures: list[st
|
|
|
2186
2188
|
ip = data.get("implementationPlanning")
|
|
2187
2189
|
if not isinstance(ip, dict):
|
|
2188
2190
|
return
|
|
2189
|
-
|
|
2190
|
-
if not declarations:
|
|
2191
|
-
return
|
|
2192
|
-
task_root = _task_root_from_run_dir(report_path.parent.parent)
|
|
2193
|
-
manifest_path = task_root / "qa" / "conformance-manifest.json"
|
|
2194
|
-
entries = []
|
|
2195
|
-
if manifest_path.is_file():
|
|
2196
|
-
try:
|
|
2197
|
-
manifest = json.loads(manifest_path.read_text())
|
|
2198
|
-
entries = manifest.get("entries") or [] if isinstance(manifest, dict) else []
|
|
2199
|
-
except (OSError, json.JSONDecodeError):
|
|
2200
|
-
entries = []
|
|
2201
|
-
for error in _declared_conformance_errors(
|
|
2202
|
-
{"entries": declarations},
|
|
2203
|
-
{"entries": entries},
|
|
2204
|
-
None,
|
|
2205
|
-
):
|
|
2206
|
-
failures.append(
|
|
2207
|
-
f"final-report data.json: conformance declaration {error} in "
|
|
2208
|
-
f"{manifest_path}. Emitting `Conformance tests:` MUST also write "
|
|
2209
|
-
"one matching manifest entry (implementation-planning.md §Conformance)."
|
|
2210
|
-
)
|
|
2191
|
+
_planning_conformance_declarations(ip.get("stages"), failures)
|
|
2211
2192
|
|
|
2212
2193
|
|
|
2213
2194
|
def _validate_conformance_surfaces(
|
|
@@ -2280,6 +2261,12 @@ def _validate_conformance(
|
|
|
2280
2261
|
)
|
|
2281
2262
|
if declared_manifest is None:
|
|
2282
2263
|
return warnings
|
|
2264
|
+
if declared_manifest is not None:
|
|
2265
|
+
scoped_declared = _scope_manifest_entries(declared_manifest, stage_name)
|
|
2266
|
+
for error in missing_declared_scripts(
|
|
2267
|
+
scoped_declared.get("entries"), task_root
|
|
2268
|
+
):
|
|
2269
|
+
failures.append(f"conformance gate BLOCKING: {error}")
|
|
2283
2270
|
if not manifest_path.is_file():
|
|
2284
2271
|
empty_scoped_manifest = {"entries": []}
|
|
2285
2272
|
if declared_manifest is not None:
|
|
@@ -3894,9 +3881,11 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3894
3881
|
"""Recompute one plan item's gate class from its per-worker verdicts,
|
|
3895
3882
|
per `prompts/lead/plan-body-verification.md` "Round protocol". Returns one of
|
|
3896
3883
|
``majority-disagree`` / ``needs-reverify`` / ``has-dissent`` /
|
|
3897
|
-
``full-consensus`` / ``all-non-result``.
|
|
3898
|
-
``dissent-isolated``
|
|
3899
|
-
|
|
3884
|
+
``full-consensus`` / ``all-non-result``. Blocking-kind minority dissent
|
|
3885
|
+
(``dissent-isolated`` / ``partial-consensus`` on ``b``/``c``/``e``) is
|
|
3886
|
+
``majority-disagree`` so the user gate sees it. ``has-dissent`` remains
|
|
3887
|
+
advisory-only, rollback items, and a single-vote kind that lost its
|
|
3888
|
+
reproduction.
|
|
3900
3889
|
"""
|
|
3901
3890
|
tokens = [
|
|
3902
3891
|
(
|
|
@@ -3967,6 +3956,18 @@ def _classify_plan_item_gate(item: dict) -> str:
|
|
|
3967
3956
|
if _max_verdict_round(item) >= _TIE_SETTLED_ROUND:
|
|
3968
3957
|
return "majority-disagree"
|
|
3969
3958
|
return "needs-reverify"
|
|
3959
|
+
if (
|
|
3960
|
+
len(non_error) >= 2
|
|
3961
|
+
and blocking_disagree
|
|
3962
|
+
and (
|
|
3963
|
+
not (blocking_kinds & single_vote_kinds)
|
|
3964
|
+
or _is_variation_point_item(item)
|
|
3965
|
+
)
|
|
3966
|
+
):
|
|
3967
|
+
# 판단 종류의 소수 반대는 표로 기각하지 않는다. 양쪽이 표를 냈으면
|
|
3968
|
+
# 사용자가 고른다. 재현에 실패한 1표 종류 `a`/`f` 는 위에서 이미
|
|
3969
|
+
# 근거를 잃었으므로 이 분기에 안 들어온다.
|
|
3970
|
+
return "majority-disagree"
|
|
3970
3971
|
return "has-dissent"
|
|
3971
3972
|
|
|
3972
3973
|
|
|
@@ -4057,11 +4058,10 @@ def _self_fix_budget_exhausted(pbv: dict) -> bool:
|
|
|
4057
4058
|
def _state_classification(item: dict, gate_class: str) -> str:
|
|
4058
4059
|
"""This item's `planItems[].rounds[].classification` for the state file.
|
|
4059
4060
|
|
|
4060
|
-
|
|
4061
|
-
`
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
classifier rather than being re-invented by each lead.
|
|
4061
|
+
Blocking-kind `dissent-isolated` / `partial-consensus` is already
|
|
4062
|
+
`majority-disagree` at the gate. `has-dissent` that remains is advisory
|
|
4063
|
+
or a single-vote kind that lost reproduction; the state file then splits
|
|
4064
|
+
that remainder into `dissent-isolated` vs `partial-consensus`.
|
|
4065
4065
|
|
|
4066
4066
|
*gate_class* is passed in rather than recomputed so that the caller's
|
|
4067
4067
|
effective classification — which may have been downgraded by
|
|
@@ -4135,9 +4135,6 @@ def _is_dissent_downgraded(
|
|
|
4135
4135
|
)
|
|
4136
4136
|
|
|
4137
4137
|
|
|
4138
|
-
_STARTABLE_STAGE_STATUSES = frozenset({"ready", "active"})
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
4138
|
def _stage_scope_bucket(item: dict, pbv: dict) -> str:
|
|
4142
4139
|
"""Whether this item has standing to block the stage about to start.
|
|
4143
4140
|
|
|
@@ -4154,21 +4151,14 @@ def _stage_scope_bucket(item: dict, pbv: dict) -> str:
|
|
|
4154
4151
|
`stageScope` belongs to the plan as a whole — `P-Opt-*` and `P-Var-*` live
|
|
4155
4152
|
there permanently, and scoping them out would stop an unrequested-work
|
|
4156
4153
|
verdict from blocking a start.
|
|
4154
|
+
|
|
4155
|
+
디스패치 큐와 같은 함수를 쓴다. 검증기가 다른 통을 내면 워커가 안 본
|
|
4156
|
+
항목이 승인을 막거나, 본 항목이 게이트에서 빠진다.
|
|
4157
4157
|
"""
|
|
4158
4158
|
ledger = pbv.get("stageLedger")
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
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"
|
|
4159
|
+
return _item_stage_scope_bucket(
|
|
4160
|
+
item, ledger if isinstance(ledger, dict) else None,
|
|
4161
|
+
)
|
|
4172
4162
|
|
|
4173
4163
|
|
|
4174
4164
|
def _set_aside_reason(item: dict, pbv: dict, accepted_item_ids: set[str]) -> str | None:
|
|
@@ -4244,11 +4234,19 @@ def _recompute_plan_body_gate(
|
|
|
4244
4234
|
_plan_item_gate_class(it, pbv, accepted)
|
|
4245
4235
|
for it in (pbv.get("planItems") or [])
|
|
4246
4236
|
if isinstance(it, dict)
|
|
4237
|
+
and (
|
|
4238
|
+
_stage_scope_bucket(it, pbv) == "in-scope"
|
|
4239
|
+
or it.get("verdicts")
|
|
4240
|
+
)
|
|
4247
4241
|
]
|
|
4248
4242
|
if not classes:
|
|
4249
4243
|
return None
|
|
4250
4244
|
if all(c == "all-non-result" for c in classes):
|
|
4251
4245
|
return "aborted-non-result"
|
|
4246
|
+
if pbv.get("gating") is False:
|
|
4247
|
+
if any(c in ("majority-disagree", "has-dissent", "needs-reverify", "all-non-result") for c in classes):
|
|
4248
|
+
return "passed-with-dissent"
|
|
4249
|
+
return "passed"
|
|
4252
4250
|
if any(c == "majority-disagree" for c in classes):
|
|
4253
4251
|
return "blocked-by-disagreement"
|
|
4254
4252
|
if any(c in ("has-dissent", "needs-reverify", "all-non-result") for c in classes):
|
|
@@ -4433,6 +4431,10 @@ def _gate_blocking_causes(
|
|
|
4433
4431
|
"""Which inputs actually block approval, as `gateBlockedBy` enum values."""
|
|
4434
4432
|
causes = set()
|
|
4435
4433
|
recomputed = _recompute_plan_body_gate(pbv, accepted_item_ids)
|
|
4434
|
+
if pbv.get("gating") is False:
|
|
4435
|
+
if recomputed == "aborted-non-result":
|
|
4436
|
+
causes.add("non-result")
|
|
4437
|
+
return causes
|
|
4436
4438
|
if recomputed == "blocked-by-disagreement":
|
|
4437
4439
|
causes.add("majority-disagree")
|
|
4438
4440
|
elif recomputed == "aborted-non-result":
|
|
@@ -6086,16 +6088,16 @@ def _validate_approval_clarification_backtrace(
|
|
|
6086
6088
|
direction — a majority-disagree plan item must cite a `blocks: approval`
|
|
6087
6089
|
row. Nothing walked this way, so a row could withhold approval while
|
|
6088
6090
|
recording no blast radius at all. The cost lands on the re-run:
|
|
6089
|
-
`incremental-scope` resolves impacted stages from these links and
|
|
6090
|
-
id that traces to no stage
|
|
6091
|
-
|
|
6091
|
+
`incremental-scope` resolves impacted stages from these links and will not
|
|
6092
|
+
silently narrow past an id that traces to no stage, so this report fails
|
|
6093
|
+
rather than forcing a full re-run.
|
|
6092
6094
|
|
|
6093
6095
|
The link must also *resolve to a stage*, which is the thing the re-run
|
|
6094
6096
|
actually reads. Checking only that a link exists let a row satisfy this
|
|
6095
|
-
gate
|
|
6096
|
-
position in their own array, so they
|
|
6097
|
-
coverage row whose `coveredBy` is prose
|
|
6098
|
-
|
|
6097
|
+
gate while the next re-run still could not place the answer: `P-Req-*`
|
|
6098
|
+
and `P-Val-*` ids are numbered by position in their own array, so they
|
|
6099
|
+
carry no stage, and a blocked coverage row whose `coveredBy` is prose
|
|
6100
|
+
cites none either.
|
|
6099
6101
|
"""
|
|
6100
6102
|
if (data.get("header") or {}).get("taskType") != "implementation-planning":
|
|
6101
6103
|
return
|
|
@@ -6117,8 +6119,9 @@ def _validate_approval_clarification_backtrace(
|
|
|
6117
6119
|
"but has no back-trace into the plan — no plan item carries it as "
|
|
6118
6120
|
"`clarificationId`, and no requirement-coverage row is `blocked "
|
|
6119
6121
|
f"{row_id}` in its `status` or `approvalDisposition`. An item that "
|
|
6120
|
-
"withholds approval without recording what it affects
|
|
6121
|
-
"next re-run
|
|
6122
|
+
"withholds approval without recording what it affects cannot "
|
|
6123
|
+
"place the next re-run's scope; this report fails rather than "
|
|
6124
|
+
"forcing a full re-run."
|
|
6122
6125
|
)
|
|
6123
6126
|
continue
|
|
6124
6127
|
if stages_for_clarification(data, row_id):
|
|
@@ -6131,8 +6134,8 @@ def _validate_approval_clarification_backtrace(
|
|
|
6131
6134
|
f"blocked coverage row's `coveredBy`. A `P-Req-*` / `P-Val-*` id "
|
|
6132
6135
|
"carries no stage number, so a row linked only that way must cite "
|
|
6133
6136
|
"the stage in `coveredBy`. A blocker whose blast radius resolves to "
|
|
6134
|
-
"no stage
|
|
6135
|
-
"re-
|
|
6137
|
+
"no stage cannot auto-narrow the next re-run; this report fails "
|
|
6138
|
+
"rather than forcing a full re-run."
|
|
6136
6139
|
)
|
|
6137
6140
|
|
|
6138
6141
|
|
|
@@ -7098,7 +7101,10 @@ def _validate_round_recorded_verdicts(data: dict, failures: list[str]) -> None:
|
|
|
7098
7101
|
round_count = pbv.get("roundCount")
|
|
7099
7102
|
if not isinstance(round_count, int) or round_count < 1:
|
|
7100
7103
|
return
|
|
7101
|
-
items = [
|
|
7104
|
+
items = [
|
|
7105
|
+
it for it in (pbv.get("planItems") or [])
|
|
7106
|
+
if isinstance(it, dict) and _stage_scope_bucket(it, pbv) == "in-scope"
|
|
7107
|
+
]
|
|
7102
7108
|
if not items:
|
|
7103
7109
|
return
|
|
7104
7110
|
empty = [str(it.get("id") or "<unnamed>") for it in items if not it.get("verdicts")]
|
|
@@ -7178,6 +7184,85 @@ def _validate_unresolved_tie_was_reverified(
|
|
|
7178
7184
|
)
|
|
7179
7185
|
|
|
7180
7186
|
|
|
7187
|
+
def _validate_tie_received_extra_vote(
|
|
7188
|
+
data: dict,
|
|
7189
|
+
failures: list[str],
|
|
7190
|
+
) -> None:
|
|
7191
|
+
"""동수는 같은 둘을 다시 돌리는 것이 아니라 세 번째 표로 가른다."""
|
|
7192
|
+
ip = data.get("implementationPlanning")
|
|
7193
|
+
if not isinstance(ip, dict):
|
|
7194
|
+
return
|
|
7195
|
+
pbv = ip.get("planBodyVerification")
|
|
7196
|
+
if not isinstance(pbv, dict):
|
|
7197
|
+
return
|
|
7198
|
+
missing = sorted({
|
|
7199
|
+
str(item.get("id") or "").strip()
|
|
7200
|
+
for item in pbv.get("planItems") or []
|
|
7201
|
+
if isinstance(item, dict)
|
|
7202
|
+
and not item.get("carriedForwardFromSeq")
|
|
7203
|
+
and _is_even_blocking_split(item)
|
|
7204
|
+
and _distinct_verdict_workers(item) < 3
|
|
7205
|
+
})
|
|
7206
|
+
if not missing:
|
|
7207
|
+
return
|
|
7208
|
+
failures.append(
|
|
7209
|
+
f"final-report data.json: plan item(s) {missing} carry an even split "
|
|
7210
|
+
"on a blocking breakage kind and have no third vote. Re-running the "
|
|
7211
|
+
"original two does not settle a 1-1 split. Dispatch one extra analyser "
|
|
7212
|
+
"whose prompt is those items only (`okstra plan-items prepare "
|
|
7213
|
+
"--tie-vote`) and record the vote with `okstra plan-items "
|
|
7214
|
+
"apply-verdicts --append --round 2`."
|
|
7215
|
+
)
|
|
7216
|
+
|
|
7217
|
+
|
|
7218
|
+
def _is_even_blocking_split(item: dict) -> bool:
|
|
7219
|
+
"""라운드 승격 없이 차단 kind 의 짝수 분할인지."""
|
|
7220
|
+
forced = {
|
|
7221
|
+
**item,
|
|
7222
|
+
"verdicts": [
|
|
7223
|
+
{**row, "round": 1}
|
|
7224
|
+
for row in (item.get("verdicts") or [])
|
|
7225
|
+
if isinstance(row, dict)
|
|
7226
|
+
],
|
|
7227
|
+
}
|
|
7228
|
+
return _is_unsettled_tie(forced)
|
|
7229
|
+
|
|
7230
|
+
|
|
7231
|
+
def _distinct_verdict_workers(item: dict) -> int:
|
|
7232
|
+
return len({
|
|
7233
|
+
str(row.get("worker") or "")
|
|
7234
|
+
for row in (item.get("verdicts") or [])
|
|
7235
|
+
if isinstance(row, dict) and str(row.get("worker") or "").strip()
|
|
7236
|
+
})
|
|
7237
|
+
|
|
7238
|
+
|
|
7239
|
+
def _validate_advisory_plan_body_gating(data: dict, failures: list[str]) -> None:
|
|
7240
|
+
"""gating=false 는 검출 표면 0 + 스테이지 1 일 때만 받는다."""
|
|
7241
|
+
ip = data.get("implementationPlanning")
|
|
7242
|
+
if not isinstance(ip, dict):
|
|
7243
|
+
return
|
|
7244
|
+
pbv = ip.get("planBodyVerification")
|
|
7245
|
+
if not isinstance(pbv, dict) or pbv.get("gating") is not False:
|
|
7246
|
+
return
|
|
7247
|
+
facts = ip.get("designPreparation") is not None or ip.get("stageMap") or ip.get("stages")
|
|
7248
|
+
if facts and not advisory_plan_body_gating(ip):
|
|
7249
|
+
failures.append(
|
|
7250
|
+
"final-report data.json: implementationPlanning.planBodyVerification "
|
|
7251
|
+
"`gating` is false, but that is only legal when "
|
|
7252
|
+
"designPreparation.mode is `no-design-inputs` (empty items) and the "
|
|
7253
|
+
"Stage Map has exactly one row. Two-or-more stages, a PREP item, or "
|
|
7254
|
+
"non-empty designPreparation items keep the gating contract."
|
|
7255
|
+
)
|
|
7256
|
+
applied = pbv.get("selfFixRoundsApplied")
|
|
7257
|
+
if isinstance(applied, int) and applied > 0:
|
|
7258
|
+
failures.append(
|
|
7259
|
+
"final-report data.json: implementationPlanning.planBodyVerification "
|
|
7260
|
+
"`gating` is false, so the self-fix loop must not run "
|
|
7261
|
+
f"(`selfFixRoundsApplied`={applied}). Keep extraction and one "
|
|
7262
|
+
"verification round."
|
|
7263
|
+
)
|
|
7264
|
+
|
|
7265
|
+
|
|
7181
7266
|
def _validate_verdict_rounds_outlive_self_fix(
|
|
7182
7267
|
data: dict,
|
|
7183
7268
|
failures: list[str],
|
|
@@ -7219,7 +7304,18 @@ def _validate_verdict_rounds_outlive_self_fix(
|
|
|
7219
7304
|
for item in pbv.get("planItems") or []:
|
|
7220
7305
|
if not isinstance(item, dict) or item.get("carriedForwardFromSeq"):
|
|
7221
7306
|
continue
|
|
7307
|
+
if _stage_scope_bucket(item, pbv) != "in-scope":
|
|
7308
|
+
continue
|
|
7222
7309
|
item_id = str(item.get("id") or "").strip()
|
|
7310
|
+
verified = item.get("verifiedContentHash")
|
|
7311
|
+
current = item.get("contentHash")
|
|
7312
|
+
if (
|
|
7313
|
+
isinstance(verified, str)
|
|
7314
|
+
and isinstance(current, str)
|
|
7315
|
+
and verified == current
|
|
7316
|
+
):
|
|
7317
|
+
# 본문이 같으면 라운드 번호가 self-fix 이전이어도 같은 텍스트다.
|
|
7318
|
+
continue
|
|
7223
7319
|
for verdict in item.get("verdicts") or []:
|
|
7224
7320
|
if not isinstance(verdict, dict):
|
|
7225
7321
|
continue
|
|
@@ -8291,6 +8387,8 @@ def validate_plan_body_section(
|
|
|
8291
8387
|
_validate_set_aside_register(data, failures, accepted_item_ids)
|
|
8292
8388
|
_validate_verdict_rounds_outlive_self_fix(data, failures)
|
|
8293
8389
|
_validate_unresolved_tie_was_reverified(data, failures)
|
|
8390
|
+
_validate_tie_received_extra_vote(data, failures)
|
|
8391
|
+
_validate_advisory_plan_body_gating(data, failures)
|
|
8294
8392
|
_validate_plan_item_extraction_completeness(data, failures)
|
|
8295
8393
|
_validate_plan_item_subject_substance(data, failures)
|
|
8296
8394
|
_validate_plan_body_clarification_matching(data, failures, accepted_item_ids)
|
|
@@ -809,7 +809,7 @@ def _check_activity_worker_agents(
|
|
|
809
809
|
)
|
|
810
810
|
|
|
811
811
|
|
|
812
|
-
def _allowed_automatic_rounds(self_fix_rounds: int) -> int:
|
|
812
|
+
def _allowed_automatic_rounds(self_fix_rounds: int, *, gating: bool = True) -> int:
|
|
813
813
|
"""이 run 이 돌아도 되는 자동 plan-body 라운드 수.
|
|
814
814
|
|
|
815
815
|
상한이 2 로 고정돼 있던 동안 다른 규칙과 정면으로 충돌했다.
|
|
@@ -825,25 +825,42 @@ def _allowed_automatic_rounds(self_fix_rounds: int) -> int:
|
|
|
825
825
|
로 그 라운드에 실린다. 자가수정은 여기에 배치를 하나씩 더한다. 표적 재검증은
|
|
826
826
|
기준선의 두 번째 배치가 받고, 늘어나는 것은 그 라운드가 덮지 못한 잔여 항목을
|
|
827
827
|
일소하는 배치다.
|
|
828
|
+
|
|
829
|
+
``gating=false`` 자문 경로는 추출과 1라운드만 남긴다. self-fix 와 일소
|
|
830
|
+
배치는 돌리지 않는다.
|
|
828
831
|
"""
|
|
832
|
+
if not gating:
|
|
833
|
+
return 1
|
|
829
834
|
return 2 + max(self_fix_rounds, 0)
|
|
830
835
|
|
|
831
836
|
|
|
832
|
-
def
|
|
837
|
+
def _plan_body_verification(report_data: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
833
838
|
planning = report_data.get("implementationPlanning")
|
|
834
|
-
|
|
839
|
+
verification = (
|
|
835
840
|
planning.get("planBodyVerification")
|
|
836
841
|
if isinstance(planning, Mapping)
|
|
837
842
|
else None
|
|
838
843
|
)
|
|
844
|
+
return verification if isinstance(verification, Mapping) else None
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _self_fix_rounds_applied(report_data: Mapping[str, Any]) -> int:
|
|
848
|
+
plan_verification = _plan_body_verification(report_data)
|
|
839
849
|
value = (
|
|
840
850
|
plan_verification.get("selfFixRoundsApplied")
|
|
841
|
-
if
|
|
851
|
+
if plan_verification is not None
|
|
842
852
|
else 0
|
|
843
853
|
)
|
|
844
854
|
return value if isinstance(value, int) and value >= 0 else 0
|
|
845
855
|
|
|
846
856
|
|
|
857
|
+
def _plan_body_gating(report_data: Mapping[str, Any]) -> bool:
|
|
858
|
+
plan_verification = _plan_body_verification(report_data)
|
|
859
|
+
if plan_verification is None:
|
|
860
|
+
return True
|
|
861
|
+
return plan_verification.get("gating") is not False
|
|
862
|
+
|
|
863
|
+
|
|
847
864
|
def _matching_user_reverification_round(
|
|
848
865
|
event: LeadEvent | None,
|
|
849
866
|
clarification_id: str,
|
|
@@ -945,7 +962,9 @@ def _check_activity_round_counts(
|
|
|
945
962
|
)
|
|
946
963
|
automatic_rounds = verification_rounds - len(user_reverification_rounds)
|
|
947
964
|
self_fix_rounds = _self_fix_rounds_applied(report_data)
|
|
948
|
-
allowed_rounds = _allowed_automatic_rounds(
|
|
965
|
+
allowed_rounds = _allowed_automatic_rounds(
|
|
966
|
+
self_fix_rounds, gating=_plan_body_gating(report_data),
|
|
967
|
+
)
|
|
949
968
|
if automatic_rounds > allowed_rounds:
|
|
950
969
|
errors.append(
|
|
951
970
|
f"activity contract: at most {allowed_rounds} plan verification "
|