okstra 0.186.4 → 0.186.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.186.4",
3
+ "version": "0.186.5",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.186.4",
3
- "builtAt": "2026-08-24T05:51:42.489Z",
2
+ "package": "0.186.5",
3
+ "builtAt": "2026-08-24T06:52:38.243Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -131,10 +131,65 @@ def _clarifications(
131
131
  if not isinstance(row, Mapping):
132
132
  _fail("lead", path, f"activeClarifications[{index}]", "must be an object")
133
133
  activity_by_id = {row.get("activityId"): row for row in activities}
134
- return [
134
+ rows = [
135
135
  _clarification_row(row, activity_by_id, path)
136
136
  for row in active
137
137
  ]
138
+ rows.extend(_carried_clarification_rows(ledger, {row.get("id") for row in rows}, path))
139
+ return rows
140
+
141
+
142
+ def _carried_clarification_rows(
143
+ ledger: Mapping[str, Any], seen_ids: set[object], path: Path,
144
+ ) -> list[dict[str, Any]]:
145
+ """이월 결정은 active 질문이 아니다. 이번 런 활동 원장을 요구하지 않는다."""
146
+ carried = ledger.get("carriedDecisions")
147
+ if not isinstance(carried, list):
148
+ return []
149
+ rows: list[dict[str, Any]] = []
150
+ seen = set(seen_ids)
151
+ for index, entry in enumerate(carried):
152
+ if not isinstance(entry, Mapping):
153
+ _fail("lead", path, f"carriedDecisions[{index}]", "must be an object")
154
+ decision = entry.get("decision")
155
+ if not isinstance(decision, Mapping):
156
+ _fail(
157
+ "lead", path, f"carriedDecisions[{index}].decision",
158
+ "must be an object",
159
+ )
160
+ cid = decision.get("id")
161
+ if cid in seen:
162
+ continue
163
+ rows.append(_carried_clarification_row(decision))
164
+ seen.add(cid)
165
+ return rows
166
+
167
+
168
+ def _carried_clarification_row(source: Mapping[str, Any]) -> dict[str, Any]:
169
+ row = {
170
+ key: source[key]
171
+ for key in (
172
+ "id", "ticketId", "kind", "statement", "expectedForm", "blocks",
173
+ "origin", "userConfirmation", "options",
174
+ )
175
+ if key in source
176
+ }
177
+ if "approval" in source:
178
+ row["approvalContext"] = source["approval"]
179
+ resolution = source.get("resolutionInput")
180
+ if not isinstance(resolution, Mapping):
181
+ row["status"] = "resolved"
182
+ return row
183
+ row.update(
184
+ status="resolved",
185
+ userInput=resolution.get("userText", ""),
186
+ resolution={
187
+ "disposition": resolution.get("disposition"),
188
+ "userText": resolution.get("userText"),
189
+ "checkRefs": list(resolution.get("checkRefs") or []),
190
+ },
191
+ )
192
+ return row
138
193
 
139
194
 
140
195
  def _project_relative(project_root: Path, path: Path) -> str:
@@ -3492,8 +3492,8 @@ def validate_final_report_data(
3492
3492
  errors = schema_validate(data, schema)
3493
3493
  for err in errors:
3494
3494
  failures.append(f"final-report data.json: {err}")
3495
- if errors:
3496
- return data
3495
+ # 스키마 실패가 계약 스캔을 가리지 않는다. 빈 decisionRefs 가
3496
+ # minItems 에서 막히면 종료상태 표 누락이 안 보였다.
3497
3497
 
3498
3498
  manifest = run_manifest or {}
3499
3499
  if data.get("executionIdentityVersion") == 2 or data.get("executionRoles"):
@@ -3595,8 +3595,11 @@ def validate_final_report_data(
3595
3595
  data, _project_root_from_report(report_path)
3596
3596
  ):
3597
3597
  print(f"validate-run: warning: {warning}", file=sys.stderr)
3598
+ carried = _carried_decision_map(
3599
+ manifest, project_root=project_root, report_path=report_path
3600
+ )
3598
3601
  _validate_supersession_ledger(data, failures)
3599
- _validate_clarification_evidence_note(data, failures)
3602
+ _validate_clarification_evidence_note(data, failures, carried=carried)
3600
3603
  _validate_approval_clarification_backtrace(data, failures)
3601
3604
  _validate_rerun_guidance(data, failures)
3602
3605
  _validate_approval_guidance(data, failures)
@@ -3606,7 +3609,11 @@ def validate_final_report_data(
3606
3609
  failures,
3607
3610
  )
3608
3611
  if not selected_direction_contract:
3609
- _validate_requirement_deviations(data, failures)
3612
+ _validate_requirement_deviations(
3613
+ data,
3614
+ failures,
3615
+ carried=carried,
3616
+ )
3610
3617
  _validate_requirement_coverage_covered_by(data, failures)
3611
3618
  warnings = _validate_design_prep_contract(
3612
3619
  data,
@@ -5778,6 +5785,14 @@ def _validate_self_fix_rewrite_scope(data: dict, failures: list[str]) -> None:
5778
5785
  )
5779
5786
 
5780
5787
 
5788
+ def _analyser_key(worker: str) -> str:
5789
+ """`codex` 와 `codex-worker` 는 같은 분석기다."""
5790
+ name = worker.strip()
5791
+ if name.endswith("-worker"):
5792
+ return name[: -len("-worker")]
5793
+ return name
5794
+
5795
+
5781
5796
  def _validate_participating_analysers(data: dict, failures: list[str]) -> None:
5782
5797
  """The gate's own arithmetic base, checked against the votes it ran on.
5783
5798
 
@@ -5809,12 +5824,13 @@ def _validate_participating_analysers(data: dict, failures: list[str]) -> None:
5809
5824
  return
5810
5825
 
5811
5826
  observed = {
5812
- str(v.get("worker"))
5827
+ _analyser_key(str(v.get("worker")))
5813
5828
  for item in (pbv.get("planItems") or [])
5814
5829
  if isinstance(item, dict)
5815
5830
  for v in (item.get("verdicts") or [])
5816
5831
  if isinstance(v, dict) and str(v.get("verdict") or "") != "verification-error"
5817
5832
  }
5833
+ observed.discard("")
5818
5834
  if observed and voting != len(observed):
5819
5835
  failures.append(
5820
5836
  "final-report data.json: planBodyVerification.participatingAnalysers "
@@ -5917,7 +5933,9 @@ _EVIDENCE_NONE_RE = re.compile(r"^none\s*[—-]\s*\S")
5917
5933
  _EVIDENCE_PATH_LINE_RE = re.compile(r"\S+\.\w+:\d+")
5918
5934
 
5919
5935
 
5920
- def _validate_clarification_evidence_note(data: dict, failures: list[str]) -> None:
5936
+ def _validate_clarification_evidence_note(
5937
+ data: dict, failures: list[str], *, carried: dict | None = None,
5938
+ ) -> None:
5921
5939
  """Every clarification row must show its codebase-first work.
5922
5940
 
5923
5941
  The profile requires any ambiguity answerable by `Read` / `Grep` to be
@@ -5930,10 +5948,13 @@ def _validate_clarification_evidence_note(data: dict, failures: list[str]) -> No
5930
5948
  """
5931
5949
  if (data.get("header") or {}).get("taskType") != "implementation-planning":
5932
5950
  return
5951
+ carried_ids = set((carried or {}).keys())
5933
5952
  for row in data.get("clarificationItems") or []:
5934
5953
  if not isinstance(row, dict):
5935
5954
  continue
5936
5955
  row_id = str(row.get("id") or "<unknown>")
5956
+ if row_id in carried_ids:
5957
+ continue
5937
5958
  statement = str(row.get("statement") or "")
5938
5959
  match = _EVIDENCE_NOTE_RE.search(statement)
5939
5960
  if not match:
@@ -8628,14 +8649,83 @@ _DEVIATION_DECISION_REF_RE = re.compile(r"^(C-\d{3,}|D-\d{4,})$")
8628
8649
  _DEVIATION_BLOCKED_DISPOSITION_RE = re.compile(r"^blocked (C-\d{3,})$")
8629
8650
 
8630
8651
 
8652
+ def _carried_decision_map(
8653
+ run_manifest: Mapping[str, Any] | None,
8654
+ *,
8655
+ project_root: Path | None,
8656
+ report_path: Path,
8657
+ ) -> dict[str, dict]:
8658
+ """이전 런에서 carry 한 결정. active `clarificationItems` 에 다시 올리지 않는다."""
8659
+ raw = (run_manifest or {}).get("approvalDecisionsPath")
8660
+ if not isinstance(raw, str) or not raw.strip():
8661
+ return {}
8662
+ path = Path(raw.strip())
8663
+ if not path.is_absolute():
8664
+ root = project_root or _project_root_from_report(report_path)
8665
+ path = root / path
8666
+ if not path.is_file():
8667
+ return {}
8668
+ try:
8669
+ ledger = json.loads(path.read_text(encoding="utf-8"))
8670
+ except (OSError, json.JSONDecodeError):
8671
+ return {}
8672
+ if not isinstance(ledger, dict):
8673
+ return {}
8674
+ carried: dict[str, dict] = {}
8675
+ for row in ledger.get("carriedDecisions") or []:
8676
+ if not isinstance(row, dict):
8677
+ continue
8678
+ decision = row.get("decision")
8679
+ if not isinstance(decision, dict):
8680
+ continue
8681
+ cid = decision.get("id")
8682
+ if isinstance(cid, str) and cid:
8683
+ carried[cid] = decision
8684
+ return carried
8685
+
8686
+
8687
+ def _deviation_target(
8688
+ ref: str,
8689
+ clarifications: dict,
8690
+ decisions: dict,
8691
+ carried: dict,
8692
+ ) -> dict | None:
8693
+ if ref.startswith("C-"):
8694
+ row = clarifications.get(ref) or carried.get(ref)
8695
+ return row if isinstance(row, dict) else None
8696
+ row = decisions.get(ref)
8697
+ return row if isinstance(row, dict) else None
8698
+
8699
+
8700
+ def _deviation_is_user_confirmed(
8701
+ ref: str, clarifications: dict, carried: dict,
8702
+ ) -> bool:
8703
+ row = clarifications.get(ref)
8704
+ if (
8705
+ isinstance(row, dict)
8706
+ and row.get("status") in {"answered", "resolved"}
8707
+ and str(row.get("userInput") or "").strip()
8708
+ ):
8709
+ return True
8710
+ carried_row = carried.get(ref)
8711
+ if not isinstance(carried_row, dict):
8712
+ return False
8713
+ resolution = carried_row.get("resolutionInput")
8714
+ if isinstance(resolution, dict) and str(resolution.get("userText") or "").strip():
8715
+ return True
8716
+ return bool(str(carried_row.get("userConfirmation") or "").strip())
8717
+
8718
+
8631
8719
  def _resolved_deviation_refs(
8632
8720
  row_id: str,
8633
8721
  refs: object,
8634
8722
  clarifications: dict,
8635
8723
  decisions: dict,
8636
8724
  failures: list[str],
8725
+ carried: dict | None = None,
8637
8726
  ) -> list[str]:
8638
8727
  valid_refs: list[str] = []
8728
+ carried_rows = carried or {}
8639
8729
  for ref in refs if isinstance(refs, list) else []:
8640
8730
  if not isinstance(ref, str) or not _DEVIATION_DECISION_REF_RE.fullmatch(ref):
8641
8731
  failures.append(
@@ -8643,12 +8733,7 @@ def _resolved_deviation_refs(
8643
8733
  f"unsupported decisionRef `{ref}`; expected C-NNN or D-NNNN."
8644
8734
  )
8645
8735
  continue
8646
- target = (
8647
- clarifications.get(ref)
8648
- if ref.startswith("C-")
8649
- else decisions.get(ref)
8650
- )
8651
- if target is None:
8736
+ if _deviation_target(ref, clarifications, decisions, carried_rows) is None:
8652
8737
  failures.append(
8653
8738
  f"final-report data.json: requirementCoverage `{row_id}` "
8654
8739
  f"decisionRef `{ref}` does not exist in this report."
@@ -8664,12 +8749,13 @@ def _validate_deviation_disposition(
8664
8749
  refs: list[str],
8665
8750
  clarifications: dict,
8666
8751
  failures: list[str],
8752
+ carried: dict | None = None,
8667
8753
  ) -> None:
8754
+ carried_rows = carried or {}
8668
8755
  if disposition == "accepted":
8669
8756
  confirmed = any(
8670
8757
  ref.startswith("C-")
8671
- and clarifications[ref].get("status") in {"answered", "resolved"}
8672
- and bool(str(clarifications[ref].get("userInput") or "").strip())
8758
+ and _deviation_is_user_confirmed(ref, clarifications, carried_rows)
8673
8759
  for ref in refs
8674
8760
  )
8675
8761
  if not confirmed:
@@ -8688,7 +8774,9 @@ def _validate_deviation_disposition(
8688
8774
  if blocked is None:
8689
8775
  return
8690
8776
  clarification_id = blocked.group(1)
8691
- clarification = clarifications.get(clarification_id)
8777
+ clarification = clarifications.get(clarification_id) or carried_rows.get(
8778
+ clarification_id
8779
+ )
8692
8780
  if clarification is None:
8693
8781
  failures.append(
8694
8782
  f"final-report data.json: requirementCoverage `{row_id}` "
@@ -8706,7 +8794,9 @@ def _validate_deviation_disposition(
8706
8794
  )
8707
8795
 
8708
8796
 
8709
- def _validate_requirement_deviations(data: dict, failures: list[str]) -> None:
8797
+ def _validate_requirement_deviations(
8798
+ data: dict, failures: list[str], *, carried: dict | None = None,
8799
+ ) -> None:
8710
8800
  """Require documented deviations to reference real decisions and approval."""
8711
8801
  planning = data.get("implementationPlanning")
8712
8802
  if not isinstance(planning, dict):
@@ -8721,15 +8811,26 @@ def _validate_requirement_deviations(data: dict, failures: list[str]) -> None:
8721
8811
  for row in (planning.get("decisionDrafts") or [])
8722
8812
  if isinstance(row, dict) and row.get("number")
8723
8813
  }
8814
+ carried_rows = carried or {}
8724
8815
  for row in planning.get("requirementCoverage") or []:
8725
8816
  if not isinstance(row, dict) or row.get("status") != "documented-deviation":
8726
8817
  continue
8727
8818
  row_id = row.get("id") or "<row>"
8728
8819
  refs = _resolved_deviation_refs(
8729
- row_id, row.get("decisionRefs"), clarifications, decisions, failures
8820
+ row_id,
8821
+ row.get("decisionRefs"),
8822
+ clarifications,
8823
+ decisions,
8824
+ failures,
8825
+ carried_rows,
8730
8826
  )
8731
8827
  _validate_deviation_disposition(
8732
- row_id, row.get("approvalDisposition"), refs, clarifications, failures
8828
+ row_id,
8829
+ row.get("approvalDisposition"),
8830
+ refs,
8831
+ clarifications,
8832
+ failures,
8833
+ carried_rows,
8733
8834
  )
8734
8835
 
8735
8836
 
@@ -9518,15 +9619,35 @@ def _validate_convergence_rounds_match_manifest(
9518
9619
  )
9519
9620
 
9520
9621
 
9521
- def _validate_convergence_states(run_dir, failures) -> None:
9522
- """Replay shared engine invariants for every public final state artifact."""
9622
+ def _validate_convergence_states(
9623
+ run_dir, failures, run_manifest: dict | None = None, project_root: Path | None = None,
9624
+ ) -> None:
9625
+ """이번 런이 가리키는 수렴 상태만 본다. 디렉터리의 옛 seq 파일은 건너뛴다."""
9523
9626
  from pathlib import Path as _Path
9524
9627
 
9525
- state_dir = _Path(run_dir) / "state"
9526
- if not state_dir.is_dir():
9527
- return
9528
- for state_path in sorted(state_dir.glob("convergence-*.json")):
9529
- if state_path.name.startswith(_CONVERGENCE_INTERMEDIATE_PREFIXES):
9628
+ declared = (run_manifest or {}).get("convergenceStatePath")
9629
+ if isinstance(declared, str) and declared.strip():
9630
+ path = _Path(declared.strip())
9631
+ if not path.is_absolute():
9632
+ if project_root is None:
9633
+ return
9634
+ path = project_root / path
9635
+ paths = [path]
9636
+ else:
9637
+ state_dir = _Path(run_dir) / "state"
9638
+ if not state_dir.is_dir():
9639
+ return
9640
+ paths = [
9641
+ state_path
9642
+ for state_path in sorted(state_dir.glob("convergence-*.json"))
9643
+ if not state_path.name.startswith(_CONVERGENCE_INTERMEDIATE_PREFIXES)
9644
+ ]
9645
+ for state_path in paths:
9646
+ if not state_path.is_file():
9647
+ if isinstance(declared, str) and declared.strip():
9648
+ failures.append(
9649
+ f"convergence state {state_path.name}: missing declared artifact"
9650
+ )
9530
9651
  continue
9531
9652
  try:
9532
9653
  state = json.loads(state_path.read_text(encoding="utf-8"))
@@ -10355,7 +10476,9 @@ def main() -> int:
10355
10476
  run_dir = report_path.parent.parent
10356
10477
  _validate_requirements_discovery_fanout(run_dir, failures, brief_path)
10357
10478
  # Phase-agnostic: convergence runs in every finding-producing phase.
10358
- _validate_convergence_states(report_path.parent.parent, failures)
10479
+ _validate_convergence_states(
10480
+ report_path.parent.parent, failures, run_manifest, project_root
10481
+ )
10359
10482
  _validate_convergence_rounds_match_manifest(
10360
10483
  report_path.parent.parent, run_manifest, failures
10361
10484
  )