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.
- package/bin/okstra +1 -0
- package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +167 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/codex-worker.md +5 -4
- package/runtime/agents/workers/gemini-worker.md +5 -4
- package/runtime/agents/workers/report-writer-worker.md +10 -3
- package/runtime/bin/okstra-render-report-views.py +129 -0
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +12 -4
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/report_views.py +701 -0
- package/runtime/python/okstra_token_usage/cli.py +9 -2
- package/runtime/python/okstra_token_usage/report.py +32 -3
- package/runtime/skills/okstra-convergence/SKILL.md +2 -2
- package/runtime/skills/okstra-report-writer/SKILL.md +25 -8
- package/runtime/skills/okstra-team-contract/SKILL.md +16 -15
- package/runtime/templates/reports/final-report.template.md +398 -211
- package/runtime/templates/reports/report.css +151 -0
- package/runtime/templates/reports/report.js +163 -0
- package/runtime/templates/reports/user-response.template.md +69 -0
- package/runtime/validators/lib/fixtures.sh +76 -2
- package/runtime/validators/validate-report-views.py +283 -0
- package/runtime/validators/validate-run.py +564 -4
- package/runtime/validators/validate-workflow.sh +4 -0
- package/src/install.mjs +1 -0
- 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())
|