okstra 0.199.3 → 0.199.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +5 -0
- package/runtime/python/okstra_ctl/plan_items_cli.py +23 -4
- package/runtime/python/okstra_ctl/report_assembly.py +12 -18
- package/runtime/python/okstra_ctl/report_narrative.py +22 -0
- package/runtime/validators/validate-run.py +3 -0
package/docs/cli.md
CHANGED
|
@@ -816,7 +816,7 @@ The `okstra` Node CLI (`bin/okstra`) provides both installer/admin commands and
|
|
|
816
816
|
| `okstra plan-items <prepare\|prompt\|validate-prepared> --run-manifest <path> …` | Bind the implementation-planning verification queue to the run manifest. `prepare` extracts the exact queue from `--narrative` and, when `designPreparation.mode` is `no-design-inputs` and the Stage Map has one row, flips `convergence.planBodyVerification.gating` to `false` (stdout `Gating`). `prompt` emits its fixed lossless view, ending with the parser-facing `## Response format` block so the block reaches every verifier with the queue; `validate-prepared` proves the prepared queue still matches the narrative. With `--state <plan-body-verification.json>` the round is a re-verification: `prepare` also carries each queued item's recorded votes and `selfFixNote` into the envelope as `priorRounds`, `prompt` renders them as that item's `**Prior round dissent**` block behind a re-verification preamble, and `validate-prepared` re-derives the carry and rejects an envelope that dropped it. Python resolves the convergence-owned state path, so model callers never choose it. |
|
|
817
817
|
| `okstra plan-items seed --narrative <report-narrative.md> --state <plan-body-verification.json> [--prior-state <previous plan-body-verification.json>]` | Create the convergence-owned `planBodyVerification.planItems[]` rows every verdict lands in, from the same deterministic extraction `extract` uses. The historical v2 form is `--data <data.json>`. Idempotent by id: an existing row keeps its verdicts and carried fields. Reports `seeded` / `existing` counts. `--prior-state` carries the previous **run**'s verdicts into this one: a newly seeded item whose `contentHash` equals that run's `verifiedContentHash` for the same id inherits its `verdicts[]` and is tagged `carriedForwardFromSeq` with the seq read off the prior filename, so round 1 does not re-judge text nobody changed. A matching id alone never carries — `P-*` ids are positional and shift. It requires `--state`, refuses a prior state whose task root differs from the one `--state` lives under (the state file carries no task identity, so its path is the only identity there is), and when it carries anything it rewrites the sibling `plan-items-*.json` `dispatchQueue` the way `incremental-carry` does. Adds `carried` / `carriedForwardFromSeq` to the reported counts. |
|
|
818
818
|
| `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md>… --round <N>` | Read each worker's Markdown verdict directly, validate every current `P-*` id, reject duplicate worker submissions, and overwrite that round's stored verdicts. `--items <plan-items artifact>` narrows and persists the assignment shared by verdict validation and round completion to what a partial round actually dispatched — pass the `--tie-vote` artifact on a critic tie round, or every item the critic was never given is refused as unanswered. The unanswered check still applies inside the narrowed set. Earlier verdicts and completed-round history outside that set remain unchanged. The historical `--verdicts <file>` form remains automation compatibility only. Without `--append` every recorded verdict row of the queued items is replaced; when a row belongs to a round `complete-round` never closed, the command refuses before writing and names the `complete-round --round <M>` to run first. `--discard-open-rounds` replaces anyway — the recovery path when those rounds are being re-applied from their result files in order (the discarded rows are printed); it also restores `dispatchQueue` to the items those result files answer, since the persisted queue belongs to the latest round and an earlier round's verdicts would otherwise be refused as outside it. |
|
|
819
|
-
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--items <plan-items artifact>] [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched.
|
|
819
|
+
| `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N> [--items <plan-items artifact>] [--self-fix-note <item-id>=<markdown-file>]… [--self-fix-group <cause-file>=<item-id>[,<item-id>...]]… [--self-fix-stop-reason <all-resolved\|no-progress\|max-rounds-reached>]` | After `plan-verify` succeeds, atomically derive and record the round's per-item votes, gate result, participant counts from the actual assigned roster, immutable completion time, convergence history, and optional self-fix notes/groups read from Markdown files. `--self-fix-group` requires `--self-fix-stop-reason` — there is no default. `--self-fix-stop-reason` alone records a stop for a round that rewrote nothing and leaves `selfFixGroups` / `selfFixRoundsApplied` untouched. When `--items` is omitted, completion automatically reads this run's canonical prepared queue and restores it when it includes all votes recorded for the requested round. If that file belongs to a narrower later batch, the persisted queue stays authoritative. This recovers saved critic results after an upgrade without requiring the lead to add a recovery flag. An explicit `--items` still selects a prepared artifact. Only the assigned items need verdicts for this round; earlier verdicts elsewhere remain unchanged. Missing-verdict errors name the round and exact dispatched item ids. Models do not write the state JSON. Stdout also carries `nextDispatch`. With `gating=false`, critic corrections are exempt from the analyser round limit and do not require an even split. A later `--append` updates the critic verdict while retaining analyser votes and completed round history. A second automatic rewrite is rejected; the one rewrite may contain several cause groups. `selfFixRoundsApplied` remains its verification round number, not a rewrite count. |
|
|
820
820
|
| `okstra plan-items next-dispatch --state <plan-body-verification.json> [--run-manifest <path>]` | After `apply-verdicts`, decide whether this round opens a worker batch. `kind: none` — missing-dependency `UNVERIFIABLE` only, no new batch. `kind: worker-correction` — re-prompt only those workers; peers stay idle. `kind: critic-tie` — unsettled analyser 1-1 on a run that rostered a critic, `critic-worker` on those item ids only. `kind: user-decision` — the same 1-1 on a run with no critic rostered: no in-band vote can break it, so open one `okstra approval-decision open` per item (classification `correctness-critical` for unresolved kind `a` outside `P-Var-*` or kind `f` on `P-Req-*`, otherwise `noncritical-dissent`) plus its `## 1. Clarification Items` row and dispatch no further verification for them. `--run-manifest` is what tells the two apart (`invocationAssignments` `critic/*`); without it the answer stays `critic-tie`. A missing path is never environment-unverifiable. After the single automatic self-fix, remaining items route to `lead-decision` first, then `user-decision`, with no further automatic worker batch. |
|
|
821
821
|
| `okstra plan-items resolve-dissent --state <plan-body-verification.json> --item <P-id> --decision-file <markdown-file>` | Record the lead decision, its authority within agreed scope, and cited evidence after the one self-fix. Accepts only current, noncritical design judgements; facts, user-input requirements and non-results require user confirmation. Keeps original votes and appends the decision to the report dissent log. The decision expires when its content, scope or verdicts change. |
|
|
822
822
|
| `okstra plan-items correction-prompt --state <plan-body-verification.json> --run-manifest <path> --worker <id>` | Emit the planning-time environment-gap paragraph, then the assigned queue. The environment exception is first. Exits 2 unless `next-dispatch` named that worker as a blanket-UNVERIFIABLE correction target. |
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -391,7 +391,7 @@ For contract 3.0, `prepare` checks the selected-direction draft with the same se
|
|
|
391
391
|
- `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE`. On a blocking kind (`b` / `c` / `e`, and kind `a` on `P-Var-*`) this is scored `majority-disagree` and **blocks approval**. Advisory-only `DISAGREE(d)` and `P-Rb-*` stay recorded dissent and do not block. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs.)
|
|
392
392
|
- `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes; rollback-ordering `DISAGREE(d)` votes are advisory and excluded from the tally), OR any blocking-kind dissent with ≥2 participating votes (a minority `DISAGREE` is not outvoted), OR an unresolved single-vote-blocking kind fires: one reproduced `DISAGREE(a)` on any item other than a `P-Var-*` one, or one reproduced `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**. A valid critic correction is scored before these blocking rules, whether or not the analysers split evenly.
|
|
393
393
|
- `needs-reverify` — one of two shapes the round could not settle.
|
|
394
|
-
- **An even split on a blocking kind.** A panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) needs a critic decision. An unresolved single-vote-blocking kind remains `majority-disagree`; other unresolved splits are `needs-reverify`. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Read the answer with `okstra plan-items collect-verdicts --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path> --output <envelope>` — `--items` takes that artifact, whose `dispatchQueue` is the tie items, so pointing the next step at the raw result is refused against this round's full queue. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path>` — `--items` persists the exact partial `dispatchQueue` used by verdict validation and `complete-round`. Earlier verdicts and completed-round history outside that queue remain unchanged. If a previous version
|
|
394
|
+
- **An even split on a blocking kind.** A panel splitting evenly (1-AGREE / 1-DISAGREE, 2-2, …) needs a critic decision. An unresolved single-vote-blocking kind remains `majority-disagree`; other unresolved splits are `needs-reverify`. Do **not** re-run the original two. Dispatch `critic-worker` immediately on those items only (`okstra plan-items prepare --tie-vote`, then `okstra plan-items prompt`). The prompt carries the analyser split and no other plan items. Read the answer with `okstra plan-items collect-verdicts --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path> --output <envelope>` — `--items` takes that artifact, whose `dispatchQueue` is the tie items, so pointing the next step at the raw result is refused against this round's full queue. Record the critic vote as `verdicts[].worker = critic-worker` with `okstra plan-items apply-verdicts --append --items <the `--tie-vote` plan-items artifact> --result critic-worker=<path>` — `--items` persists the exact partial `dispatchQueue` used by verdict validation and `complete-round`. Earlier verdicts and completed-round history outside that queue remain unchanged. If a previous version saved the critic votes but left the full queue in state, the ordinary `okstra plan-items complete-round --state <state> --run-manifest <manifest> --round <N>` automatically reads this run's canonical prepared queue. It uses that queue when all votes recorded for this round are included, preserving a wider recorded batch when a later prepared queue excludes its votes. An explicit `--items <prepared artifact>` remains available for selecting an artifact. Enforced by `plan_items_cli._round_inputs` and `tests/run/test_plan_items.py` completion coverage. Do not fabricate new-round votes for already agreed items. Without it the result is checked against the whole persisted round queue and refused for every item the critic was never given (measured 2026-09-10: a 7-item tie round refused against 44 items), and the only way through was `--verdicts`, which the CLI's own help calls a historical envelope. Critic `AGREE` / `SUPPLEMENT` settles the split to `has-dissent`, including an earlier `DISAGREE(a)` or `DISAGREE(f)` on `P-Req-*`. This decision corrects the disputed judgement before single-vote blocking is evaluated; it does not delete the original dissent. Critic `DISAGREE` on a blocking kind is `majority-disagree`. **Enforced:** `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` fails an in-scope item that carries an even split on a blocking kind and has neither a `critic-worker` vote nor a `blocks: approval` clarification row, `_classify_plan_item_gate` scores the tie shape and fails a settled classification the votes do not support, and `okstra_ctl.plan_items.next_dispatch` returns kind `critic-tie` for exactly these items, so the tie round is the queue the CLI hands you rather than one you assemble. **With no critic on the roster the split is a user decision, not another round.** `okstra plan-items next-dispatch --run-manifest <current-run-manifest.json>` answers `user-decision` (not `critic-tie`) when the run's `invocationAssignments` carries no `critic/*` entry, and its `itemIds` are the tie items. For each of them do what step 8 does for a surviving `majority-disagree`: `okstra approval-decision open` with `approvalContext.classification` set to `correctness-critical` when `_is_correctness_critical` is true, or `noncritical-dissent` otherwise, plus the matching `## 1. Clarification Items` row at `Blocks=approval`. Dispatch no further verification for those items. The `Blocks=approval` row is what withholds approval until the user disposes, exactly as for any other approval row; the gate retains unresolved single-vote blockers as `majority-disagree` and folds other `needs-reverify` items into `passed-with-dissent`, so the round closes on the gate it actually scored. A tie left with neither a critic vote nor a decision row surfaces as recorded dissent plus an `advisories[]` entry, not a round-blocking failure. **Enforced:** `okstra_ctl.plan_items.critic_is_rostered` reads the roster and `next_dispatch` returns the kind; `validators/validate-run.py` `_validate_unresolved_tie_was_reverified` reads a `blocks: approval` row linked to the item as the settlement and emits its advisory only when neither settlement is recorded.
|
|
395
395
|
- **A lone dissent nobody cross-verified** — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not, and on a `P-Var-*` item none fires at all — its kind `a` never blocks on one vote and takes a majority like `b` / `e`. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
|
|
396
396
|
- `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
|
|
397
397
|
5. Gate result resolution:
|
|
@@ -25,6 +25,7 @@ from ..activity import (
|
|
|
25
25
|
from ...final_report_schema import load_schema_version
|
|
26
26
|
from ...implementation_options import validate_implementation_option_selection
|
|
27
27
|
from ...report_contract import TASK_TYPE_DATA_PROPERTY
|
|
28
|
+
from ...report_assembly import validate_plan_draft
|
|
28
29
|
from ...report_corrections import (
|
|
29
30
|
CorrectionsCheck,
|
|
30
31
|
check_corrections,
|
|
@@ -103,6 +104,10 @@ def run_corrections_check(
|
|
|
103
104
|
return validate_implementation_option_selection(
|
|
104
105
|
block if isinstance(block, Mapping) else {}, original_ids, analysers,
|
|
105
106
|
)
|
|
107
|
+
elif task_type == "implementation-planning":
|
|
108
|
+
|
|
109
|
+
def semantic_validator(data: dict[str, Any]) -> list[str]:
|
|
110
|
+
return validate_plan_draft(data, project_root, manifest)
|
|
106
111
|
|
|
107
112
|
return check_corrections(
|
|
108
113
|
ledger=ledger,
|
|
@@ -292,7 +292,8 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
292
292
|
complete.add_argument(
|
|
293
293
|
"--items", type=Path,
|
|
294
294
|
help="restore this round's dispatched queue from its prepared items artifact; "
|
|
295
|
-
"
|
|
295
|
+
"defaults to this run's canonical prepared queue when it covers the round's "
|
|
296
|
+
"recorded votes; earlier verdicts outside that queue remain unchanged",
|
|
296
297
|
)
|
|
297
298
|
complete.add_argument("--self-fix-note", action="append", default=[], metavar="<item-id>=<markdown-file>")
|
|
298
299
|
complete.add_argument(
|
|
@@ -419,7 +420,7 @@ def _prepare(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
419
420
|
failures = validate_plan_draft(source, authority.project_root, authority.payload)
|
|
420
421
|
if failures:
|
|
421
422
|
raise PlanItemContractError(
|
|
422
|
-
"owner=report-writer
|
|
423
|
+
"owner=report-writer: "
|
|
423
424
|
+ "; ".join(failures)
|
|
424
425
|
+ "; correct the narrative in this run, then retry plan-items prepare"
|
|
425
426
|
)
|
|
@@ -1726,6 +1727,17 @@ def _round_inputs(args: argparse.Namespace) -> tuple[dict[str, Any], list[dict[s
|
|
|
1726
1727
|
if not isinstance(audit, list) or not isinstance(history, list):
|
|
1727
1728
|
raise PlanItemContractError("state planItems and roundHistory must be arrays")
|
|
1728
1729
|
current = _state_plan_body_items(data, args.state)
|
|
1730
|
+
if args.command == "complete-round" and args.items is None:
|
|
1731
|
+
manifest = _load_json_object(args.run_manifest)
|
|
1732
|
+
if manifest.get("planBodyVerificationPath"):
|
|
1733
|
+
prepared = _prepared_items_path(args.run_manifest)
|
|
1734
|
+
if prepared.is_file():
|
|
1735
|
+
voted = {item["id"] for item in current
|
|
1736
|
+
if _round_snapshot(item, args.round_number)["votes"]}
|
|
1737
|
+
# 이후 회차용 준비 파일이 이미 저장된 현재 회차의 표를 제외하면 쓰지 않는다.
|
|
1738
|
+
if voted <= set(_assigned_item_ids(prepared)):
|
|
1739
|
+
args = copy.copy(args)
|
|
1740
|
+
args.items = prepared
|
|
1729
1741
|
_narrow_dispatch_queue(
|
|
1730
1742
|
args, data["planBodyVerification"], {item.get("id") for item in current},
|
|
1731
1743
|
)
|
|
@@ -1742,8 +1754,15 @@ def _round_snapshots(current: list[dict[str, Any]], verification: Mapping[str, A
|
|
|
1742
1754
|
if isinstance(item, Mapping) and item.get("id") in allowed
|
|
1743
1755
|
]
|
|
1744
1756
|
snapshots = {str(item.get("id")): _round_snapshot(item, round_number) for item in scoped}
|
|
1745
|
-
if not snapshots
|
|
1746
|
-
raise PlanItemContractError("
|
|
1757
|
+
if not snapshots:
|
|
1758
|
+
raise PlanItemContractError(f"round {round_number} dispatch queue has no current plan items")
|
|
1759
|
+
missing = [item_id for item_id, row in snapshots.items() if not row["votes"]]
|
|
1760
|
+
if missing:
|
|
1761
|
+
raise PlanItemContractError(
|
|
1762
|
+
f"round {round_number} has no verdict for dispatched plan items: {', '.join(missing)}. "
|
|
1763
|
+
"Earlier-round verdicts remain recorded; check this round's prepared queue "
|
|
1764
|
+
"before requesting new verdicts."
|
|
1765
|
+
)
|
|
1747
1766
|
summary = _plan_gate_summary(verification)
|
|
1748
1767
|
classes = {row["id"]: row["stateClassification"] for row in summary["items"]}
|
|
1749
1768
|
for item_id, snapshot in snapshots.items():
|
|
@@ -18,7 +18,7 @@ from .clarification_items import (
|
|
|
18
18
|
from .final_report_schema import load_schema_version, validate
|
|
19
19
|
from .report_inputs import ReportInputPath, report_input_paths, uses_report_contract_v3
|
|
20
20
|
from .json_boundary import JsonBoundaryError, load_owned_object, serialize_owned_object
|
|
21
|
-
from .report_narrative import parse_narrative
|
|
21
|
+
from .report_narrative import parse_narrative, task_narrative_errors
|
|
22
22
|
from .report_synthesis_packet import (
|
|
23
23
|
report_synthesis_packet_paths,
|
|
24
24
|
verify_report_synthesis_packet_sources,
|
|
@@ -112,23 +112,12 @@ def _input_preflight_issues(
|
|
|
112
112
|
try:
|
|
113
113
|
if row.key == "narrative":
|
|
114
114
|
narrative = parse_narrative(row.path.read_text(encoding="utf-8"), schema)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if verdict is None:
|
|
119
|
-
continue
|
|
120
|
-
errors = validate(
|
|
121
|
-
{**narrative, "header": {"taskType": task_type}},
|
|
122
|
-
{
|
|
123
|
-
"$defs": schema.get("$defs", {}),
|
|
124
|
-
"if": branch["if"],
|
|
125
|
-
"then": {"properties": {"finalVerdict": verdict}},
|
|
126
|
-
},
|
|
127
|
-
)
|
|
128
|
-
issues.extend(
|
|
129
|
-
AssemblyIssue(row.owner, str(row.path), "finalVerdict", error)
|
|
130
|
-
for error in errors
|
|
115
|
+
issues.extend(
|
|
116
|
+
AssemblyIssue(
|
|
117
|
+
row.owner, str(row.path), error.split(":", 1)[0], error,
|
|
131
118
|
)
|
|
119
|
+
for error in task_narrative_errors(narrative, schema, task_type)
|
|
120
|
+
)
|
|
132
121
|
elif row.key == "agent-activity":
|
|
133
122
|
row.path.read_text(encoding="utf-8")
|
|
134
123
|
else:
|
|
@@ -601,7 +590,12 @@ def validate_plan_draft(
|
|
|
601
590
|
draft = copy.deepcopy(dict(data))
|
|
602
591
|
_apply_selected_direction_snapshot(draft, project_root, manifest)
|
|
603
592
|
_attach_metadata(draft, manifest)
|
|
604
|
-
return
|
|
593
|
+
return [
|
|
594
|
+
*task_narrative_errors(
|
|
595
|
+
draft, load_schema_version("3.0"), str(manifest.get("taskType", "")),
|
|
596
|
+
),
|
|
597
|
+
*selected_direction_plan_errors(draft, project_root, manifest),
|
|
598
|
+
]
|
|
605
599
|
|
|
606
600
|
|
|
607
601
|
def _identity(manifest: Mapping[str, Any]) -> tuple[str, str, str]:
|
|
@@ -179,6 +179,28 @@ def validate_writer_owned(data: Mapping[str, Any], schema: Mapping[str, Any]) ->
|
|
|
179
179
|
return validate(dict(data), writer_owned_schema(schema))
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
def task_narrative_errors(
|
|
183
|
+
data: Mapping[str, Any], schema: Mapping[str, Any], task_type: str,
|
|
184
|
+
) -> list[str]:
|
|
185
|
+
"""실행 유형에 따른 판정·후속 작업 규칙을 정본 스키마에서 검사한다."""
|
|
186
|
+
context = {**data, "header": {"taskType": task_type}}
|
|
187
|
+
errors: list[str] = []
|
|
188
|
+
for branch in schema.get("allOf", []):
|
|
189
|
+
fields = {
|
|
190
|
+
key: value
|
|
191
|
+
for key, value in branch.get("then", {}).get("properties", {}).items()
|
|
192
|
+
if key in {"finalVerdict", "followUpTasks"}
|
|
193
|
+
}
|
|
194
|
+
if not fields:
|
|
195
|
+
continue
|
|
196
|
+
errors.extend(validate(context, {
|
|
197
|
+
"$defs": schema.get("$defs", {}),
|
|
198
|
+
"if": branch["if"],
|
|
199
|
+
"then": {"properties": fields},
|
|
200
|
+
}))
|
|
201
|
+
return list(dict.fromkeys(errors))
|
|
202
|
+
|
|
203
|
+
|
|
182
204
|
def writer_owned_path_defect(path: str) -> str | None:
|
|
183
205
|
"""이 필드 경로에 작성자가 쓸 수 없는 이유, 쓸 수 있으면 None.
|
|
184
206
|
|
|
@@ -9420,6 +9420,9 @@ def run_preflight(report_path: Path, run_manifest_path: Path) -> int:
|
|
|
9420
9420
|
failures.extend(selected_direction_plan_errors(
|
|
9421
9421
|
data, Path(str(manifest.get("projectRoot") or run_manifest_path.parent)), manifest
|
|
9422
9422
|
))
|
|
9423
|
+
_append_stage_data_failures(
|
|
9424
|
+
data, failures, _task_root_from_run_dir(report_path.parent.parent),
|
|
9425
|
+
)
|
|
9423
9426
|
_validate_planning_conformance_declared(report_path, failures)
|
|
9424
9427
|
print(
|
|
9425
9428
|
json.dumps(
|