okstra 0.144.0 → 0.145.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.
Files changed (40) hide show
  1. package/README.md +4 -1
  2. package/docs/architecture.md +18 -2
  3. package/docs/cli.md +39 -2
  4. package/docs/project-structure-overview.md +16 -4
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/prompts/lead/convergence.md +11 -3
  8. package/runtime/prompts/lead/okstra-lead-contract.md +7 -1
  9. package/runtime/prompts/profiles/_common-contract.md +1 -1
  10. package/runtime/prompts/profiles/change-impact-analysis.md +24 -0
  11. package/runtime/prompts/profiles/feature-analysis.md +24 -0
  12. package/runtime/prompts/profiles/forbidden-actions.json +18 -0
  13. package/runtime/prompts/profiles/project-analysis.md +24 -0
  14. package/runtime/prompts/wizard/prompts.ko.json +44 -1
  15. package/runtime/python/okstra_ctl/analysis_inputs.py +369 -0
  16. package/runtime/python/okstra_ctl/clarification_items.py +74 -1
  17. package/runtime/python/okstra_ctl/render.py +77 -4
  18. package/runtime/python/okstra_ctl/render_final_report.py +13 -4
  19. package/runtime/python/okstra_ctl/report_views.py +134 -3
  20. package/runtime/python/okstra_ctl/run.py +118 -0
  21. package/runtime/python/okstra_ctl/run_context.py +34 -2
  22. package/runtime/python/okstra_ctl/schema_excerpt.py +12 -4
  23. package/runtime/python/okstra_ctl/user_response.py +309 -3
  24. package/runtime/python/okstra_ctl/wizard.py +545 -32
  25. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
  26. package/runtime/python/okstra_ctl/workflow.py +22 -0
  27. package/runtime/schemas/final-report-v1.0.schema.json +849 -3
  28. package/runtime/skills/okstra-run/SKILL.md +13 -1
  29. package/runtime/templates/reports/change-impact-analysis-input.template.md +58 -0
  30. package/runtime/templates/reports/feature-analysis-input.template.md +59 -0
  31. package/runtime/templates/reports/final-report.template.md +220 -0
  32. package/runtime/templates/reports/i18n/en.json +8 -0
  33. package/runtime/templates/reports/i18n/ko.json +8 -0
  34. package/runtime/templates/reports/project-analysis-input.template.md +58 -0
  35. package/runtime/templates/reports/report.js +84 -5
  36. package/runtime/templates/reports/user-response.template.md +19 -1
  37. package/runtime/validators/validate-report-views.py +61 -7
  38. package/runtime/validators/validate-run.py +42 -0
  39. package/runtime/validators/validate_analysis_report.py +864 -0
  40. package/src/commands/execute/render-bundle.mjs +3 -0
@@ -3,7 +3,8 @@
3
3
  (``scripts/okstra-render-report-views.py``).
4
4
 
5
5
  Checks, for a given final-report MD path:
6
- 1. ``*.html`` sibling exists.
6
+ 1. ``*.html`` sibling exists when the report has clarification rows,
7
+ an analysis-review contract, or a plan-approval contract.
7
8
  2. HTML's ``source-sha256`` in run-meta matches the current MD body —
8
9
  stale html detection.
9
10
  3. HTML's §5.6 / §5.7 / §5.8 deliverable regions contain no
@@ -13,6 +14,8 @@ Checks, for a given final-report MD path:
13
14
  ``<img src=>`` — self-contained guarantee.
14
15
  5. Every Response ID in HTML matches the §1 Clarification Items table
15
16
  of the source MD (1:1).
17
+ 6. Analysis reports contain all three Analysis Review decisions and an
18
+ affected-ID selector exactly matching the structured IDs in data.json.
16
19
 
17
20
  Exit codes: 0 on success, 1 on any failure. Failures are printed one
18
21
  per line to stderr.
@@ -34,6 +37,7 @@ from okstra_ctl.clarification_items import ( # noqa: E402
34
37
  section_1_present_but_unparsed,
35
38
  )
36
39
  from okstra_ctl.report_views import ( # noqa: E402
40
+ analysis_review_context,
37
41
  extract_html_digest,
38
42
  plan_approval_context,
39
43
  source_digest,
@@ -47,6 +51,18 @@ _EXTERNAL_URL_RE = re.compile(
47
51
  )
48
52
 
49
53
  _RESPONSE_ID_ATTR_RE = re.compile(r'data-response-id="(C-\d+)"')
54
+ _ANALYSIS_REVIEW_SECTION_RE = re.compile(
55
+ r'<section id="analysis-review">(?P<body>.*?)</section>', re.DOTALL
56
+ )
57
+ _ANALYSIS_REVIEW_STATUS_RE = re.compile(
58
+ r'name="analysis-review-status"\s+value="([^"]+)"'
59
+ )
60
+ _ANALYSIS_REVIEW_SELECTOR_RE = re.compile(
61
+ r'<select id="analysis-review-affected-ids"[^>]*>(?P<body>.*?)</select>',
62
+ re.DOTALL,
63
+ )
64
+ _OPTION_VALUE_RE = re.compile(r'<option value="([^"]+)">')
65
+ _ANALYSIS_REVIEW_STATUSES = ["accepted", "rejected", "revision-requested"]
50
66
 
51
67
 
52
68
  def _main_body(html_text: str) -> str:
@@ -78,6 +94,34 @@ def _no_form_sections(html_body: str) -> list[str]:
78
94
  return chunks
79
95
 
80
96
 
97
+ def _validate_analysis_review_html(
98
+ html_text: str, expected_ids: tuple[str, ...]
99
+ ) -> list[str]:
100
+ section = _ANALYSIS_REVIEW_SECTION_RE.search(html_text)
101
+ if section is None:
102
+ return ["analysis report html is missing the Analysis Review control"]
103
+ body = section.group("body")
104
+ failures: list[str] = []
105
+ statuses = sorted(_ANALYSIS_REVIEW_STATUS_RE.findall(body))
106
+ if statuses != _ANALYSIS_REVIEW_STATUSES:
107
+ failures.append(
108
+ "Analysis Review controls must be exactly accepted, "
109
+ "revision-requested, and rejected"
110
+ )
111
+ selector = _ANALYSIS_REVIEW_SELECTOR_RE.search(body)
112
+ actual_ids = (
113
+ sorted(_OPTION_VALUE_RE.findall(selector.group("body")))
114
+ if selector is not None
115
+ else []
116
+ )
117
+ if actual_ids != sorted(expected_ids):
118
+ failures.append(
119
+ f"Analysis Review selector mismatch: data.json has "
120
+ f"{sorted(expected_ids)}, HTML has {actual_ids}"
121
+ )
122
+ return failures
123
+
124
+
81
125
  def validate(report_path: Path) -> list[str]:
82
126
  failures: list[str] = []
83
127
  if not report_path.is_file():
@@ -94,14 +138,16 @@ def validate(report_path: Path) -> list[str]:
94
138
  "form parity. Re-render the report so §1 matches the schema."
95
139
  ]
96
140
  md_ids = _md_response_ids(md)
141
+ review_ctx = analysis_review_context(report_path)
97
142
 
98
- # (1) sibling artifact exists — conditional on §1 clarification rows.
99
- # The html view's sole value over the MD is its embedded form widgets
100
- # for §1 C-* rows, so a clarification-free report intentionally has no
101
- # html sibling (see report_views.report_has_clarification_items). When
102
- # there are no C-* rows and no html, that is the expected skip state.
143
+ # (1) sibling artifact exists when clarification rows or a structured
144
+ # analysis-review / plan-approval contract require interactive controls.
103
145
  if not html_path.is_file():
104
- if not md_ids and plan_approval_context(report_path, md) is None:
146
+ if (
147
+ not md_ids
148
+ and plan_approval_context(report_path, md) is None
149
+ and review_ctx is None
150
+ ):
105
151
  return []
106
152
  return [f"missing html artifact: {html_path}"]
107
153
 
@@ -146,6 +192,14 @@ def validate(report_path: Path) -> list[str]:
146
192
  f"Response ID mismatch: MD §1 has {md_ids}, HTML has {html_ids}"
147
193
  )
148
194
 
195
+ review_section_present = _ANALYSIS_REVIEW_SECTION_RE.search(html_text) is not None
196
+ if review_ctx is not None:
197
+ failures.extend(
198
+ _validate_analysis_review_html(html_text, review_ctx.selector_ids)
199
+ )
200
+ elif review_section_present:
201
+ failures.append("non-analysis report html must not contain Analysis Review controls")
202
+
149
203
  return failures
150
204
 
151
205
 
@@ -21,6 +21,8 @@ from typing import Any
21
21
  # copy under ~/.okstra/lib. The manifests advertise the bare script path, so
22
22
  # it must self-bootstrap rather than rely on an inherited PYTHONPATH.
23
23
  _VALIDATORS_DIR = Path(__file__).resolve().parent
24
+ if str(_VALIDATORS_DIR) not in sys.path:
25
+ sys.path.insert(0, str(_VALIDATORS_DIR))
24
26
  for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
25
27
  if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
26
28
  sys.path.insert(0, str(_ssot_dir))
@@ -96,6 +98,7 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
96
98
  PromptRecord,
97
99
  validate_initial_prompt_records,
98
100
  )
101
+ from validate_analysis_report import validate_analysis_report # noqa: E402
99
102
  from okstra_ctl.convergence_engine import ( # noqa: E402
100
103
  grouped_input_digest,
101
104
  validate_final_state,
@@ -2988,6 +2991,9 @@ def validate_final_report_data(
2988
2991
  failures: list[str],
2989
2992
  *,
2990
2993
  report_contracts: set[str] | None = None,
2994
+ run_manifest: dict | None = None,
2995
+ project_root: Path | None = None,
2996
+ clarification_text: str = "",
2991
2997
  ) -> Mapping[str, Any] | None:
2992
2998
  """Validate the final-report data.json against the v1.0 schema.
2993
2999
 
@@ -3036,6 +3042,21 @@ def validate_final_report_data(
3036
3042
  errors = schema_validate(data, schema)
3037
3043
  for err in errors:
3038
3044
  failures.append(f"final-report data.json: {err}")
3045
+ if errors:
3046
+ return data
3047
+
3048
+ analysis_result = validate_analysis_report(
3049
+ data=data,
3050
+ report_path=report_path,
3051
+ project_root=project_root or report_path.parent,
3052
+ run_manifest=run_manifest or {},
3053
+ clarification_text=clarification_text,
3054
+ )
3055
+ manifest_task_type = str((run_manifest or {}).get("taskType") or "")
3056
+ task_type = manifest_task_type or str(
3057
+ (data.get("header") or {}).get("taskType") or ""
3058
+ )
3059
+ failures.extend(f"{task_type}: {error}" for error in analysis_result.errors)
3039
3060
 
3040
3061
  _validate_rationale_evidence(data, failures)
3041
3062
  _validate_no_opaque_id_references(data, failures)
@@ -4117,6 +4138,22 @@ def _carry_in_source_for_run(run_manifest_path: Path) -> str:
4117
4138
  return raw.strip() if isinstance(raw, str) else ""
4118
4139
 
4119
4140
 
4141
+ def _clarification_text_for_run(
4142
+ run_manifest_path: Path, report_path: Path
4143
+ ) -> str:
4144
+ """Read only the clarification input explicitly attached to this run."""
4145
+ if not _carry_in_source_for_run(run_manifest_path):
4146
+ return ""
4147
+ task_root = _task_root_from_run_dir(report_path.parent.parent)
4148
+ staged = task_root / "instruction-set" / "clarification-response.md"
4149
+ if not staged.is_file():
4150
+ return ""
4151
+ try:
4152
+ return staged.read_text(encoding="utf-8")
4153
+ except OSError:
4154
+ return ""
4155
+
4156
+
4120
4157
  def _validate_clarification_carry_in_recorded(
4121
4158
  data: dict,
4122
4159
  report_path: Path,
@@ -7066,6 +7103,11 @@ def main() -> int:
7066
7103
  report_path,
7067
7104
  failures,
7068
7105
  report_contracts=report_contracts,
7106
+ run_manifest=run_manifest,
7107
+ project_root=project_root,
7108
+ clarification_text=_clarification_text_for_run(
7109
+ run_manifest_path, report_path
7110
+ ),
7069
7111
  )
7070
7112
  validation_data = report_data if isinstance(report_data, Mapping) else {}
7071
7113
  _validate_fix_cycle(run_manifest, validation_data, failures)