okstra 0.27.0 → 0.29.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 (28) hide show
  1. package/bin/okstra +1 -0
  2. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +167 -0
  3. package/package.json +1 -1
  4. package/runtime/BUILD.json +2 -2
  5. package/runtime/agents/workers/claude-worker.md +6 -5
  6. package/runtime/agents/workers/codex-worker.md +5 -4
  7. package/runtime/agents/workers/gemini-worker.md +5 -4
  8. package/runtime/agents/workers/report-writer-worker.md +10 -3
  9. package/runtime/bin/okstra-render-report-views.py +129 -0
  10. package/runtime/prompts/launch.template.md +1 -1
  11. package/runtime/prompts/profiles/_common-contract.md +12 -4
  12. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  13. package/runtime/python/okstra_ctl/report_views.py +701 -0
  14. package/runtime/python/okstra_token_usage/cli.py +9 -2
  15. package/runtime/python/okstra_token_usage/report.py +32 -3
  16. package/runtime/skills/okstra-convergence/SKILL.md +2 -2
  17. package/runtime/skills/okstra-report-writer/SKILL.md +25 -8
  18. package/runtime/skills/okstra-team-contract/SKILL.md +16 -15
  19. package/runtime/templates/reports/final-report.template.md +398 -211
  20. package/runtime/templates/reports/report.css +151 -0
  21. package/runtime/templates/reports/report.js +163 -0
  22. package/runtime/templates/reports/user-response.template.md +69 -0
  23. package/runtime/validators/lib/fixtures.sh +76 -2
  24. package/runtime/validators/validate-report-views.py +283 -0
  25. package/runtime/validators/validate-run.py +564 -4
  26. package/runtime/validators/validate-workflow.sh +4 -0
  27. package/src/install.mjs +1 -0
  28. package/src/render-views.mjs +67 -0
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+ """Validate the two derived final-report views produced by Phase 7 step
3
+ 1.5 (``scripts/okstra-render-report-views.py``).
4
+
5
+ Checks, for a given final-report MD path:
6
+ 1. ``*.slim.md`` and ``*.html`` siblings both exist.
7
+ 2. Slim preserves every phase-required substring from ``validate-run.py``
8
+ byte-identically (drift fence vs the report_views module).
9
+ 3. HTML body (``<main>`` slice) contains the same plain-text
10
+ substrings (after html.unescape and the markdown ``#`` prefix is
11
+ stripped).
12
+ 4. HTML's §4.6 / §4.7 / §4.8 deliverable regions contain no
13
+ ``<textarea>`` / ``<input>`` / ``<select>`` (form-attach is
14
+ restricted to §5 clarification rows).
15
+ 5. HTML has no external URLs in ``<script src=>`` / ``<link href=>`` /
16
+ ``<img src=>`` — self-contained guarantee.
17
+ 6. Every Response ID in HTML matches the §5 Clarification Items table
18
+ of the source MD (1:1).
19
+
20
+ Exit codes: 0 on success, 1 on any failure. Failures are printed one
21
+ per line to stderr.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import html as html_lib
27
+ import importlib.util
28
+ import re
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import Iterable
32
+
33
+ REPO_ROOT = Path(__file__).resolve().parents[1]
34
+ SCRIPTS_DIR = REPO_ROOT / "scripts"
35
+ if str(SCRIPTS_DIR) not in sys.path:
36
+ sys.path.insert(0, str(SCRIPTS_DIR))
37
+
38
+ from okstra_ctl.clarification_items import parse_clarification_items # noqa: E402
39
+ from okstra_ctl.report_views import ( # noqa: E402
40
+ _strip_leading_digest_comment,
41
+ extract_html_digest,
42
+ extract_slim_digest,
43
+ preserved_substrings,
44
+ slim_markdown,
45
+ source_digest,
46
+ )
47
+
48
+
49
+ def _load_validate_run() -> object:
50
+ """Dynamically load validate-run.py — it's a stand-alone script
51
+ with a hyphenated name, not an importable module. We snapshot the
52
+ substring lists from it so drift between the two files surfaces
53
+ here rather than silently in production."""
54
+ path = REPO_ROOT / "validators" / "validate-run.py"
55
+ spec = importlib.util.spec_from_file_location("_okstra_validate_run", path)
56
+ assert spec and spec.loader, f"cannot load {path}"
57
+ mod = importlib.util.module_from_spec(spec)
58
+ spec.loader.exec_module(mod)
59
+ return mod
60
+
61
+
62
+ def _detect_task_type(md: str) -> str | None:
63
+ m = re.search(r"^- Task Type:\s*(\S+)", md, re.MULTILINE)
64
+ return m.group(1).strip() if m else None
65
+
66
+
67
+ def _main_body(html_text: str) -> str:
68
+ start = html_text.find("<main>")
69
+ end = html_text.find("</main>", start) if start >= 0 else -1
70
+ if start < 0 or end < 0:
71
+ return html_text
72
+ return html_text[start + len("<main>"): end]
73
+
74
+
75
+ def _strip_md_prefix(sub: str) -> str:
76
+ s = sub
77
+ while s.startswith("#"):
78
+ s = s[1:]
79
+ return s.strip()
80
+
81
+
82
+ _EXTERNAL_URL_RE = re.compile(
83
+ r'<(?:script|link|img|iframe|source|video|audio)\s[^>]*?(?:src|href)\s*=\s*["\']https?://',
84
+ re.IGNORECASE,
85
+ )
86
+
87
+ _RESPONSE_ID_ATTR_RE = re.compile(r'data-response-id="(C-\d+)"')
88
+
89
+ _NO_FORM_HEADING_RE = re.compile(
90
+ r"<h[23][^>]*>\s*(?:4\.6|4\.7|4\.8)[\s\S]*?</h[23]>"
91
+ )
92
+
93
+
94
+ def _no_form_sections(html_body: str) -> list[str]:
95
+ """Return a list of strings, each being the rendered chunk of a
96
+ no-form section (4.6 / 4.7 / 4.8) up to the next h2/h3. Used to
97
+ check that no form controls live inside them.
98
+ """
99
+ chunks: list[str] = []
100
+ headings = [
101
+ m for m in re.finditer(
102
+ r'<h([23])[^>]*>([^<]*?)</h[23]>', html_body
103
+ )
104
+ ]
105
+ for i, m in enumerate(headings):
106
+ text = m.group(2).strip()
107
+ if not text.startswith(("4.6", "4.7", "4.8")):
108
+ continue
109
+ start = m.end()
110
+ end = headings[i + 1].start() if i + 1 < len(headings) else len(html_body)
111
+ chunks.append(html_body[start:end])
112
+ return chunks
113
+
114
+
115
+ def validate(report_path: Path) -> list[str]:
116
+ failures: list[str] = []
117
+ if not report_path.is_file():
118
+ return [f"final-report not found: {report_path}"]
119
+
120
+ md = report_path.read_text(encoding="utf-8")
121
+ task_type = _detect_task_type(md)
122
+ slim_path = report_path.with_name(report_path.stem + ".slim.md")
123
+ html_path = report_path.with_name(report_path.stem + ".html")
124
+
125
+ # (1) sibling artifacts exist
126
+ if not slim_path.is_file():
127
+ failures.append(f"missing slim artifact: {slim_path}")
128
+ if not html_path.is_file():
129
+ failures.append(f"missing html artifact: {html_path}")
130
+ if failures:
131
+ return failures
132
+
133
+ slim = slim_path.read_text(encoding="utf-8")
134
+ html_text = html_path.read_text(encoding="utf-8")
135
+ html_body = _main_body(html_text)
136
+ html_body_text = html_lib.unescape(html_body)
137
+
138
+ # (2a) source-digest staleness — both derived views must carry the
139
+ # sha256 of the artifact derived from the *current* MD. A mismatch
140
+ # means the views were rendered against an older MD body and Phase
141
+ # 7 step 1.5 was not re-run.
142
+ # - HTML digest is over the MD body (after stripping any stale
143
+ # leading digest comment), so the expected value is the digest
144
+ # of the current MD itself.
145
+ # - slim digest is over the slim BODY (so any byte-difference
146
+ # between expected and actual slim shows up as a digest
147
+ # mismatch). We recompute the expected slim and extract its
148
+ # embedded digest for comparison — equivalent to but cheaper
149
+ # than diffing the full body.
150
+ expected_md_digest = source_digest(_strip_leading_digest_comment(md))
151
+ expected_slim_digest = extract_slim_digest(
152
+ slim_markdown(md, task_type=task_type)
153
+ )
154
+ slim_digest = extract_slim_digest(slim)
155
+ html_digest = extract_html_digest(html_text)
156
+ if slim_digest is None:
157
+ failures.append(
158
+ "slim missing source-sha256 header — re-render with okstra render-views"
159
+ )
160
+ elif slim_digest != expected_slim_digest:
161
+ failures.append(
162
+ f"stale slim: body sha256 {slim_digest[:12]}… does not match "
163
+ f"current MD's rendered slim {str(expected_slim_digest)[:12]}…"
164
+ )
165
+ if html_digest is None:
166
+ failures.append(
167
+ "html missing source-sha256 in run-meta — re-render with okstra render-views"
168
+ )
169
+ elif html_digest != expected_md_digest:
170
+ failures.append(
171
+ f"stale html: source-sha256 {html_digest[:12]}… does not match "
172
+ f"current MD {expected_md_digest[:12]}…"
173
+ )
174
+
175
+ # (2b) drift fence: report_views.preserved_substrings(task_type)
176
+ # must agree with validate-run.py's tuples for the same task_type.
177
+ vr = _load_validate_run()
178
+ drift = _diff_substring_lists(task_type, vr)
179
+ failures.extend(drift)
180
+
181
+ # (3) slim preserves every required substring byte-identically
182
+ for sub in preserved_substrings(task_type):
183
+ if sub in md and sub not in slim:
184
+ failures.append(f"slim dropped required substring: {sub!r}")
185
+ # Also forward-check against validate-run.py's tuples so a
186
+ # future addition there fails this validator immediately.
187
+ for sub in _validate_run_substrings(task_type, vr):
188
+ if sub in md and sub not in slim:
189
+ failures.append(f"slim dropped validate-run substring: {sub!r}")
190
+
191
+ # (4) html preserves the same set (modulo md prefix + html.escape)
192
+ for sub in preserved_substrings(task_type):
193
+ if sub not in md:
194
+ continue
195
+ needle = _strip_md_prefix(sub)
196
+ if needle not in html_body_text:
197
+ failures.append(f"html body dropped substring: {needle!r}")
198
+
199
+ # (5) deliverable sections contain no form controls
200
+ for chunk in _no_form_sections(html_body):
201
+ if "<textarea" in chunk or "<input" in chunk or "<select" in chunk:
202
+ failures.append(
203
+ "html §4.6/§4.7/§4.8 deliverable section contains a form control"
204
+ )
205
+ break
206
+
207
+ # (6) no external URLs in <script src> / <link href> / etc.
208
+ if _EXTERNAL_URL_RE.search(html_text):
209
+ failures.append("html contains external URL in script/link/img — must be self-contained")
210
+
211
+ # (7) Response ID parity: HTML form rows ↔ §5 C-* rows in MD.
212
+ # Bidirectional — catches both "MD has C-* the HTML lost" AND
213
+ # "HTML has stale C-* that the current MD no longer declares".
214
+ md_ids = _md_response_ids(md)
215
+ html_ids = sorted(set(_RESPONSE_ID_ATTR_RE.findall(html_text)))
216
+ if md_ids != html_ids:
217
+ failures.append(
218
+ f"Response ID mismatch: MD §5 has {md_ids}, HTML has {html_ids}"
219
+ )
220
+
221
+ return failures
222
+
223
+
224
+ def _validate_run_substrings(task_type: str | None, vr) -> Iterable[str]:
225
+ if task_type == "implementation":
226
+ return getattr(vr, "IMPLEMENTATION_REQUIRED_SECTIONS", ())
227
+ if task_type == "final-verification":
228
+ return getattr(vr, "FINAL_VERIFICATION_REQUIRED_SECTIONS", ())
229
+ return ()
230
+
231
+
232
+ def _diff_substring_lists(task_type: str | None, vr) -> list[str]:
233
+ """Surface drift between report_views and validate-run substring
234
+ tuples. The two MUST stay in lock-step."""
235
+ failures: list[str] = []
236
+ from okstra_ctl.report_views import (
237
+ PRESERVED_SUBSTRINGS_IMPLEMENTATION,
238
+ PRESERVED_SUBSTRINGS_FINAL_VERIFICATION,
239
+ )
240
+ impl_vr = set(getattr(vr, "IMPLEMENTATION_REQUIRED_SECTIONS", ()))
241
+ impl_rv = set(PRESERVED_SUBSTRINGS_IMPLEMENTATION)
242
+ if impl_vr != impl_rv:
243
+ missing_in_views = impl_vr - impl_rv
244
+ extra_in_views = impl_rv - impl_vr
245
+ failures.append(
246
+ "drift: PRESERVED_SUBSTRINGS_IMPLEMENTATION vs validate-run "
247
+ f"IMPLEMENTATION_REQUIRED_SECTIONS — missing in views {missing_in_views or '∅'}, "
248
+ f"extra in views {extra_in_views or '∅'}"
249
+ )
250
+ fv_vr = set(getattr(vr, "FINAL_VERIFICATION_REQUIRED_SECTIONS", ()))
251
+ fv_rv = set(PRESERVED_SUBSTRINGS_FINAL_VERIFICATION)
252
+ if fv_vr != fv_rv:
253
+ failures.append(
254
+ "drift: PRESERVED_SUBSTRINGS_FINAL_VERIFICATION vs validate-run "
255
+ f"FINAL_VERIFICATION_REQUIRED_SECTIONS — missing in views {fv_vr - fv_rv or '∅'}, "
256
+ f"extra in views {fv_rv - fv_vr or '∅'}"
257
+ )
258
+ return failures
259
+
260
+
261
+ def _md_response_ids(md: str) -> list[str]:
262
+ items = parse_clarification_items(md) or []
263
+ return sorted({it.row_id for it in items if re.fullmatch(r"C-\d+", it.row_id)})
264
+
265
+
266
+ def main(argv: list[str] | None = None) -> int:
267
+ parser = argparse.ArgumentParser(
268
+ description="Validate slim/html derived views of an okstra final-report."
269
+ )
270
+ parser.add_argument(
271
+ "report_path",
272
+ type=Path,
273
+ help="Path to the original final-report markdown.",
274
+ )
275
+ args = parser.parse_args(argv)
276
+ failures = validate(args.report_path)
277
+ for f in failures:
278
+ print(f, file=sys.stderr)
279
+ return 1 if failures else 0
280
+
281
+
282
+ if __name__ == "__main__":
283
+ sys.exit(main())