okstra 0.140.0 → 0.141.1

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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture.md +1 -1
  3. package/docs/cli.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/report-writer-worker.md +4 -2
  7. package/runtime/prompts/launch.template.md +7 -5
  8. package/runtime/prompts/lead/convergence.md +22 -0
  9. package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
  10. package/runtime/prompts/lead/plan-body-verification.md +97 -26
  11. package/runtime/prompts/lead/report-writer.md +2 -0
  12. package/runtime/prompts/profiles/error-analysis.md +1 -0
  13. package/runtime/prompts/profiles/final-verification.md +1 -1
  14. package/runtime/prompts/profiles/implementation-planning.md +6 -4
  15. package/runtime/prompts/profiles/improvement-discovery.md +2 -1
  16. package/runtime/prompts/profiles/requirements-discovery.md +2 -1
  17. package/runtime/python/okstra_ctl/build_tools.py +95 -0
  18. package/runtime/python/okstra_ctl/incremental_scope.py +95 -0
  19. package/runtime/python/okstra_ctl/render.py +26 -11
  20. package/runtime/python/okstra_ctl/render_final_report.py +24 -1
  21. package/runtime/python/okstra_ctl/rollup.py +49 -0
  22. package/runtime/python/okstra_ctl/scope_provenance.py +98 -1
  23. package/runtime/python/okstra_ctl/stage_citations.py +47 -0
  24. package/runtime/python/okstra_ctl/workflow.py +7 -4
  25. package/runtime/schemas/final-report-v1.0.schema.json +60 -1
  26. package/runtime/skills/okstra-brief-gen/SKILL.md +38 -11
  27. package/runtime/skills/okstra-rollup/SKILL.md +3 -2
  28. package/runtime/templates/reports/brief.template.md +42 -11
  29. package/runtime/templates/reports/fan-out-unit.template.md +9 -4
  30. package/runtime/templates/reports/final-report.template.md +20 -0
  31. package/runtime/templates/reports/final-verification-input.template.md +4 -2
  32. package/runtime/templates/reports/i18n/en.json +11 -4
  33. package/runtime/templates/reports/i18n/ko.json +11 -4
  34. package/runtime/templates/reports/improvement-discovery-input.template.md +2 -2
  35. package/runtime/validators/validate-brief.py +171 -26
  36. package/runtime/validators/validate-run.py +528 -86
  37. package/runtime/validators/validate_fanout.py +19 -10
  38. package/runtime/validators/validate_improvement_report.py +17 -4
@@ -18,8 +18,9 @@ for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "
18
18
  from okstra_ctl.work_categories import WORK_CATEGORIES # noqa: E402
19
19
  from okstra_ctl.fanout import topological_order, CycleError # noqa: E402
20
20
  from okstra_ctl.scope_provenance import ( # noqa: E402
21
+ brief_citation_problem,
22
+ brief_end_state_ids,
21
23
  brief_headings,
22
- normalize_heading,
23
24
  parse_source,
24
25
  )
25
26
 
@@ -37,7 +38,11 @@ _PROVENANCE_BULLET_RE = re.compile(r"^\s*[-*]\s+(?P<src>.+?)\s*$", re.MULTILINE)
37
38
 
38
39
 
39
40
  def _check_provenance(
40
- pkt_name: str, text: str, errors: list[str], headings: set[str]
41
+ pkt_name: str,
42
+ text: str,
43
+ errors: list[str],
44
+ headings: set[str],
45
+ end_state: set[str],
41
46
  ) -> None:
42
47
  """A fan-out unit must cite the brief line that demanded it.
43
48
 
@@ -47,6 +52,8 @@ def _check_provenance(
47
52
 
48
53
  `headings` empty means the brief was unavailable — the grammar still
49
54
  applies, only the heading-existence half degrades to unverifiable.
55
+ `end_state` empty means the brief predates the end-state sections, so the
56
+ older heading form is still the admissible one.
50
57
  """
51
58
  section = _PROVENANCE_SECTION_RE.search(text)
52
59
  if not section:
@@ -61,7 +68,7 @@ def _check_provenance(
61
68
  if not bullets:
62
69
  errors.append(
63
70
  f"{pkt_name}: `## Requirement Provenance` is empty — cite at least one "
64
- "`brief:<heading>` or `contract:<rule>`"
71
+ "`brief:EB-001` (or the legacy `brief:<heading>`) or `contract:<rule>`"
65
72
  )
66
73
  return
67
74
  for raw in bullets:
@@ -69,14 +76,15 @@ def _check_provenance(
69
76
  if ref.kind not in ("brief", "contract"):
70
77
  errors.append(
71
78
  f"{pkt_name}: unrecognized provenance `{raw}` — a fan-out unit's "
72
- "source must be `brief:<heading>` or `contract:<rule>`"
79
+ "source must be `brief:EB-001` (or the legacy `brief:<heading>`) "
80
+ "or `contract:<rule>`"
73
81
  )
74
- elif ref.kind == "brief" and headings:
75
- if normalize_heading(ref.value) not in headings:
82
+ elif ref.kind == "brief":
83
+ problem = brief_citation_problem(ref, headings, end_state)
84
+ if problem:
76
85
  errors.append(
77
- f"{pkt_name}: cites brief heading `{ref.value}` which does not "
78
- "exist in the brief. A fan-out unit becomes the brief for a whole "
79
- "downstream task — it must trace to a line the reporter wrote."
86
+ f"{pkt_name}: {problem}. A fan-out unit becomes the brief for a "
87
+ "whole downstream task it must trace to a line the reporter wrote."
80
88
  )
81
89
 
82
90
 
@@ -117,13 +125,14 @@ def validate_fanout(run_dir: Path, brief_path: Path | None = None) -> Validation
117
125
  return ValidationResult(ok=True)
118
126
 
119
127
  headings = brief_headings(brief_path) if brief_path is not None else set()
128
+ end_state = brief_end_state_ids(brief_path) if brief_path is not None else set()
120
129
 
121
130
  errors: list[str] = []
122
131
  units: dict[str, list[str]] = {}
123
132
  for pkt in sorted(fo.glob("unit-*.md")):
124
133
  text = pkt.read_text(encoding="utf-8")
125
134
  fm = _frontmatter(text)
126
- _check_provenance(pkt.name, text, errors, headings)
135
+ _check_provenance(pkt.name, text, errors, headings, end_state)
127
136
  uid = fm.get("unit-id", "")
128
137
  if uid != pkt.stem:
129
138
  errors.append(f"{pkt.name}: unit-id {uid!r} != filename stem {pkt.stem!r}")
@@ -181,10 +181,13 @@ def _check_candidate_row(
181
181
  errors: list[str],
182
182
  ) -> None:
183
183
  """Items 2, 3, 4, 6, 7 — validate a single candidate row."""
184
- if len(row) != 10:
185
- errors.append(f"row {idx} has {len(row)} columns, expected 10")
184
+ if len(row) != 11:
185
+ errors.append(f"row {idx} has {len(row)} columns, expected 11")
186
186
  return
187
- cand_id, lens_cell, _title, scope, _sev, _eff, consensus, source_workers, next_phase, _evidence = row
187
+ (
188
+ cand_id, lens_cell, _title, scope, _sev, _eff,
189
+ consensus, source_workers, next_phase, expected_after, _evidence,
190
+ ) = row
188
191
 
189
192
  if not _CAND_ID_RE.match(cand_id):
190
193
  errors.append(f"row {idx}: Cand ID '{cand_id}' must match I-NNN")
@@ -209,6 +212,15 @@ def _check_candidate_row(
209
212
  if next_phase not in _NEXT_PHASES:
210
213
  errors.append(f"row {idx}: Recommended next-phase '{next_phase}' must be one of {_NEXT_PHASES}")
211
214
 
215
+ # A candidate nobody can describe an observable change for is not a finding;
216
+ # it is a preference. Making this a cell rather than prose is what stops the
217
+ # candidate list from growing past what a downstream brief can pin.
218
+ if not expected_after.strip():
219
+ errors.append(
220
+ f"row {idx}: `Expected behavior after` is empty — state what becomes "
221
+ "observably different once this candidate is applied, or drop the row"
222
+ )
223
+
212
224
 
213
225
  def _check_candidate_rows(
214
226
  data_rows: list[list[str]],
@@ -233,7 +245,8 @@ def _check_candidates_table(
233
245
  header, *data = rows
234
246
  expected_columns = [
235
247
  "Cand ID", "Lens", "Title", "Scope", "Severity", "Effort",
236
- "Consensus", "Source workers", "Recommended next-phase", "Evidence",
248
+ "Consensus", "Source workers", "Recommended next-phase",
249
+ "Expected behavior after", "Evidence",
237
250
  ]
238
251
  if header != expected_columns:
239
252
  errors.append(