okstra 0.30.3 → 0.32.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/docs/kr/architecture.md +2 -2
- package/docs/kr/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +7 -5
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +23 -6
- package/runtime/agents/workers/gemini-worker.md +23 -6
- package/runtime/agents/workers/report-writer-worker.md +45 -66
- package/runtime/bin/okstra-codex-exec.sh +31 -0
- package/runtime/bin/okstra-gemini-exec.sh +26 -0
- package/runtime/bin/okstra-render-final-report.py +101 -0
- package/runtime/bin/okstra-render-report-views.py +17 -10
- package/runtime/bin/okstra-token-usage.py +3 -1
- package/runtime/python/lib/okstra/globals.sh +1 -1
- package/runtime/python/lib/okstra/usage.sh +2 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +253 -0
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +201 -0
- package/runtime/python/okstra_ctl/report_views.py +276 -297
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +53 -14
- package/runtime/python/okstra_ctl/workers.py +45 -11
- package/runtime/python/okstra_token_usage/__init__.py +5 -1
- package/runtime/python/okstra_token_usage/cli.py +66 -36
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/python/okstra_token_usage/report.py +148 -65
- package/runtime/python/okstra_vendor/__init__.py +37 -0
- package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
- package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
- package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
- package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
- package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
- package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
- package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
- package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
- package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
- package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
- package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
- package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
- package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
- package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
- package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
- package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
- package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
- package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
- package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
- package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
- package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
- package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
- package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
- package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
- package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
- package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
- package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
- package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +31 -30
- package/runtime/skills/okstra-run/SKILL.md +6 -4
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -3
- package/runtime/templates/reports/final-report.template.md +370 -405
- package/runtime/templates/reports/report.css +57 -4
- package/runtime/templates/reports/report.js +63 -7
- package/runtime/templates/reports/settings.template.json +1 -0
- package/runtime/validators/lib/fixtures.sh +7 -7
- package/runtime/validators/validate-report-views.py +24 -153
- package/runtime/validators/validate-run.py +102 -19
- package/src/install.mjs +21 -1
|
@@ -41,11 +41,38 @@ body {
|
|
|
41
41
|
.report-header > div { flex: 1; font-weight: 600; }
|
|
42
42
|
|
|
43
43
|
main {
|
|
44
|
-
max-width:
|
|
44
|
+
max-width: 120ch;
|
|
45
45
|
margin: 1.5rem auto;
|
|
46
46
|
padding: 0 1rem 4rem;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
nav.toc {
|
|
50
|
+
margin: 1rem 0 2rem;
|
|
51
|
+
padding: 0.6em 1em;
|
|
52
|
+
border: 1px solid color-mix(in srgb, GrayText 40%, transparent);
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
background: color-mix(in srgb, CanvasText 4%, transparent);
|
|
55
|
+
}
|
|
56
|
+
nav.toc .toc-title {
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
margin-bottom: 0.4em;
|
|
59
|
+
font-size: 0.95rem;
|
|
60
|
+
}
|
|
61
|
+
nav.toc ul {
|
|
62
|
+
list-style: none;
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
columns: 2;
|
|
66
|
+
column-gap: 1.5em;
|
|
67
|
+
}
|
|
68
|
+
@media (max-width: 60ch) {
|
|
69
|
+
nav.toc ul { columns: 1; }
|
|
70
|
+
}
|
|
71
|
+
nav.toc li { margin: 0.15em 0; break-inside: avoid; }
|
|
72
|
+
nav.toc li.toc-h3 { padding-left: 1.2em; font-size: 0.92em; }
|
|
73
|
+
nav.toc a { color: inherit; text-decoration: none; }
|
|
74
|
+
nav.toc a:hover { text-decoration: underline; }
|
|
75
|
+
|
|
49
76
|
h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin-top: 1.6em; margin-bottom: 0.4em; }
|
|
50
77
|
h1 { font-size: 1.7rem; }
|
|
51
78
|
h2 { font-size: 1.35rem; border-bottom: 1px solid GrayText; padding-bottom: 0.2em; }
|
|
@@ -88,6 +115,22 @@ th, td {
|
|
|
88
115
|
padding: 0.45em 0.6em;
|
|
89
116
|
vertical-align: top;
|
|
90
117
|
text-align: left;
|
|
118
|
+
word-break: keep-all;
|
|
119
|
+
overflow-wrap: anywhere;
|
|
120
|
+
min-width: 5ch;
|
|
121
|
+
/* No max-width: cells that carry prose (statement / source / scope)
|
|
122
|
+
* absorb the remaining horizontal space within `main`'s 120ch. */
|
|
123
|
+
}
|
|
124
|
+
/* Narrow columns — header AND every body cell of this column fit
|
|
125
|
+
* within 30 plain chars (per `_narrow_columns()` in report_views.py).
|
|
126
|
+
* Examples: ID, Ticket ID, Kind, Status, Priority, Auto-spawn?,
|
|
127
|
+
* 에이전트, 역할, 모델, 상태. Pinning `width: 5%` makes these
|
|
128
|
+
* columns shrink toward the suggestion while wide neighbours absorb
|
|
129
|
+
* the remainder, so a long-prose row never squashes ID-style stacks
|
|
130
|
+
* into one-character ladders. */
|
|
131
|
+
td.td-narrow, th.td-narrow {
|
|
132
|
+
width: 5%;
|
|
133
|
+
white-space: nowrap;
|
|
91
134
|
}
|
|
92
135
|
thead th {
|
|
93
136
|
position: sticky;
|
|
@@ -105,18 +148,28 @@ tr[data-response-id][data-status="obsolete"] {
|
|
|
105
148
|
opacity: 0.65;
|
|
106
149
|
}
|
|
107
150
|
|
|
108
|
-
textarea
|
|
151
|
+
textarea,
|
|
152
|
+
select[data-response-id],
|
|
153
|
+
input[data-response-id],
|
|
154
|
+
input[data-other-for] {
|
|
109
155
|
width: 100%;
|
|
110
|
-
min-height: 2.2em;
|
|
111
156
|
font: inherit;
|
|
112
157
|
padding: 0.3em 0.4em;
|
|
113
158
|
border: 1px solid GrayText;
|
|
114
159
|
border-radius: 3px;
|
|
115
160
|
background: Canvas;
|
|
116
161
|
color: CanvasText;
|
|
162
|
+
}
|
|
163
|
+
textarea {
|
|
164
|
+
min-height: 2.2em;
|
|
117
165
|
resize: vertical;
|
|
118
166
|
}
|
|
119
|
-
|
|
167
|
+
input[data-other-for] {
|
|
168
|
+
margin-top: 0.35em;
|
|
169
|
+
}
|
|
170
|
+
textarea[disabled],
|
|
171
|
+
select[disabled],
|
|
172
|
+
input[disabled] { opacity: 0.55; }
|
|
120
173
|
|
|
121
174
|
button[data-action] {
|
|
122
175
|
font: inherit;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/* Client-side glue for the okstra final-report HTML view.
|
|
2
2
|
*
|
|
3
3
|
* Responsibilities:
|
|
4
|
-
* 1. Collect
|
|
5
|
-
* Status is open/answered (disabled rows
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 1. Collect entry values for every <tr data-response-id> whose
|
|
5
|
+
* Status is open/answered (disabled rows skipped automatically).
|
|
6
|
+
* Widgets: <select> (enum decision), <input data-other-for> (기타
|
|
7
|
+
* input revealed when select == "__other__"), <input
|
|
8
|
+
* data-response-id> (material/data-point single-line), <textarea>
|
|
9
|
+
* (everything else / fallback).
|
|
10
|
+
* 2. Serialise the entries into markdown whose bytes are IDENTICAL
|
|
11
|
+
* to scripts/okstra_ctl/report_views.py serialize_user_response.
|
|
8
12
|
* 3. Write the result to <pre id="user-response-output"> and offer a
|
|
9
13
|
* [Copy] button.
|
|
10
14
|
*
|
|
@@ -36,14 +40,46 @@
|
|
|
36
40
|
return String(s == null ? "" : s).replace(/^\s+|\s+$/g, "");
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
// Read the user-supplied value out of one clarification row. Returns
|
|
44
|
+
// the empty string when the row has no usable widget, the widget is
|
|
45
|
+
// disabled, the select sits on its blank "(선택)" placeholder, or the
|
|
46
|
+
// "기타" branch has an empty input. Caller decides whether to emit
|
|
47
|
+
// an entry.
|
|
48
|
+
function readRowValue(row) {
|
|
49
|
+
var sel = row.querySelector("select[data-response-id]");
|
|
50
|
+
if (sel) {
|
|
51
|
+
if (sel.disabled) return "";
|
|
52
|
+
var picked = sel.value;
|
|
53
|
+
if (picked === "") return "";
|
|
54
|
+
if (picked === "__other__") {
|
|
55
|
+
var rid = sel.getAttribute("data-response-id") || "";
|
|
56
|
+
var other = row.querySelector('input[data-other-for="' + rid + '"]');
|
|
57
|
+
return other ? trimMultiline(other.value) : "";
|
|
58
|
+
}
|
|
59
|
+
var opt = sel.options[sel.selectedIndex];
|
|
60
|
+
// Use the visible option text ("(a) one-time backfill") so the
|
|
61
|
+
// user-response sidecar stays human-readable, not just "a".
|
|
62
|
+
return opt ? trimMultiline(opt.textContent) : picked;
|
|
63
|
+
}
|
|
64
|
+
var ta = row.querySelector("textarea[data-response-id]");
|
|
65
|
+
if (ta) {
|
|
66
|
+
if (ta.disabled) return "";
|
|
67
|
+
return trimMultiline(ta.value);
|
|
68
|
+
}
|
|
69
|
+
var inp = row.querySelector("input[data-response-id]");
|
|
70
|
+
if (inp) {
|
|
71
|
+
if (inp.disabled) return "";
|
|
72
|
+
return trimMultiline(inp.value);
|
|
73
|
+
}
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
function collectEntries() {
|
|
40
78
|
var entries = [];
|
|
41
79
|
var rows = document.querySelectorAll("tr[data-response-id]");
|
|
42
80
|
for (var i = 0; i < rows.length; i++) {
|
|
43
81
|
var row = rows[i];
|
|
44
|
-
var
|
|
45
|
-
if (!ta || ta.disabled) continue;
|
|
46
|
-
var value = trimMultiline(ta.value);
|
|
82
|
+
var value = readRowValue(row);
|
|
47
83
|
if (!value) continue;
|
|
48
84
|
entries.push({
|
|
49
85
|
responseId: row.getAttribute("data-response-id") || "",
|
|
@@ -55,6 +91,25 @@
|
|
|
55
91
|
return entries;
|
|
56
92
|
}
|
|
57
93
|
|
|
94
|
+
// Toggle the visibility of the "기타" companion input next to each
|
|
95
|
+
// select whose current value is "__other__". Wired at bind() time and
|
|
96
|
+
// also called once for the initial state.
|
|
97
|
+
function bindOtherInputToggle() {
|
|
98
|
+
var selects = document.querySelectorAll("select[data-response-id]");
|
|
99
|
+
for (var i = 0; i < selects.length; i++) {
|
|
100
|
+
(function (sel) {
|
|
101
|
+
var rid = sel.getAttribute("data-response-id") || "";
|
|
102
|
+
var other = document.querySelector('input[data-other-for="' + rid + '"]');
|
|
103
|
+
if (!other) return;
|
|
104
|
+
var update = function () {
|
|
105
|
+
other.hidden = sel.value !== "__other__";
|
|
106
|
+
};
|
|
107
|
+
sel.addEventListener("change", update);
|
|
108
|
+
update();
|
|
109
|
+
})(selects[i]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
58
113
|
function buildUserResponseMarkdown(runMeta, entries, createdAt) {
|
|
59
114
|
var head =
|
|
60
115
|
"---\n" +
|
|
@@ -132,6 +187,7 @@
|
|
|
132
187
|
btn.addEventListener("click", copyUserResponse);
|
|
133
188
|
}
|
|
134
189
|
}
|
|
190
|
+
bindOtherInputToggle();
|
|
135
191
|
}
|
|
136
192
|
|
|
137
193
|
if (typeof window !== "undefined") {
|
|
@@ -366,21 +366,21 @@ report_lines.extend(
|
|
|
366
366
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
367
367
|
report_path.write_text("\n".join(report_lines) + "\n")
|
|
368
368
|
|
|
369
|
-
# Phase 7 step 1.5 (BLOCKING) — render the
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
369
|
+
# Phase 7 step 1.5 (BLOCKING) — render the html sibling artifact next
|
|
370
|
+
# to the final-report so validate-run.py's report-views hook passes.
|
|
371
|
+
# The workflow validator's fixture predates that step; we materialise
|
|
372
|
+
# the html file in-place using the same single-reference-point helper
|
|
373
|
+
# the CLI uses.
|
|
374
374
|
import os
|
|
375
375
|
WORKSPACE_ROOT = os.environ.get("OKSTRA_WORKSPACE_ROOT_FOR_FIXTURE", "")
|
|
376
376
|
if WORKSPACE_ROOT:
|
|
377
377
|
import sys as _sys
|
|
378
378
|
_sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
|
|
379
379
|
try:
|
|
380
|
-
from okstra_ctl.report_views import RunMeta,
|
|
380
|
+
from okstra_ctl.report_views import RunMeta, render_html_view
|
|
381
381
|
css = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.css").read_text(encoding="utf-8")
|
|
382
382
|
js = (Path(WORKSPACE_ROOT) / "templates" / "reports" / "report.js").read_text(encoding="utf-8")
|
|
383
|
-
|
|
383
|
+
render_html_view(
|
|
384
384
|
report_path,
|
|
385
385
|
run_meta=RunMeta(
|
|
386
386
|
task_key=str(task_manifest.get("taskKey", "validation/fixture")),
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Validate the
|
|
3
|
-
|
|
2
|
+
"""Validate the self-contained HTML view produced by Phase 7 step 1.5
|
|
3
|
+
(``scripts/okstra-render-report-views.py``).
|
|
4
4
|
|
|
5
5
|
Checks, for a given final-report MD path:
|
|
6
|
-
1. ``*.
|
|
7
|
-
2.
|
|
8
|
-
|
|
9
|
-
3. HTML
|
|
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
|
|
6
|
+
1. ``*.html`` sibling exists.
|
|
7
|
+
2. HTML's ``source-sha256`` in run-meta matches the current MD body —
|
|
8
|
+
stale html detection.
|
|
9
|
+
3. HTML's §4.6 / §4.7 / §4.8 deliverable regions contain no
|
|
13
10
|
``<textarea>`` / ``<input>`` / ``<select>`` (form-attach is
|
|
14
11
|
restricted to §5 clarification rows).
|
|
15
|
-
|
|
12
|
+
4. HTML has no external URLs in ``<script src=>`` / ``<link href=>`` /
|
|
16
13
|
``<img src=>`` — self-contained guarantee.
|
|
17
|
-
|
|
14
|
+
5. Every Response ID in HTML matches the §5 Clarification Items table
|
|
18
15
|
of the source MD (1:1).
|
|
19
16
|
|
|
20
17
|
Exit codes: 0 on success, 1 on any failure. Failures are printed one
|
|
@@ -23,12 +20,9 @@ per line to stderr.
|
|
|
23
20
|
from __future__ import annotations
|
|
24
21
|
|
|
25
22
|
import argparse
|
|
26
|
-
import html as html_lib
|
|
27
|
-
import importlib.util
|
|
28
23
|
import re
|
|
29
24
|
import sys
|
|
30
25
|
from pathlib import Path
|
|
31
|
-
from typing import Iterable
|
|
32
26
|
|
|
33
27
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
34
28
|
SCRIPTS_DIR = REPO_ROOT / "scripts"
|
|
@@ -37,31 +31,17 @@ if str(SCRIPTS_DIR) not in sys.path:
|
|
|
37
31
|
|
|
38
32
|
from okstra_ctl.clarification_items import parse_clarification_items # noqa: E402
|
|
39
33
|
from okstra_ctl.report_views import ( # noqa: E402
|
|
40
|
-
_strip_leading_digest_comment,
|
|
41
34
|
extract_html_digest,
|
|
42
|
-
extract_slim_digest,
|
|
43
|
-
preserved_substrings,
|
|
44
|
-
slim_markdown,
|
|
45
35
|
source_digest,
|
|
46
36
|
)
|
|
47
37
|
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
39
|
+
_EXTERNAL_URL_RE = re.compile(
|
|
40
|
+
r'<(?:script|link|img|iframe|source|video|audio)\s[^>]*?(?:src|href)\s*=\s*["\']https?://',
|
|
41
|
+
re.IGNORECASE,
|
|
42
|
+
)
|
|
61
43
|
|
|
62
|
-
|
|
63
|
-
m = re.search(r"^- Task Type:\s*(\S+)", md, re.MULTILINE)
|
|
64
|
-
return m.group(1).strip() if m else None
|
|
44
|
+
_RESPONSE_ID_ATTR_RE = re.compile(r'data-response-id="(C-\d+)"')
|
|
65
45
|
|
|
66
46
|
|
|
67
47
|
def _main_body(html_text: str) -> str:
|
|
@@ -72,25 +52,6 @@ def _main_body(html_text: str) -> str:
|
|
|
72
52
|
return html_text[start + len("<main>"): end]
|
|
73
53
|
|
|
74
54
|
|
|
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
55
|
def _no_form_sections(html_body: str) -> list[str]:
|
|
95
56
|
"""Return a list of strings, each being the rendered chunk of a
|
|
96
57
|
no-form section (4.6 / 4.7 / 4.8) up to the next h2/h3. Used to
|
|
@@ -118,50 +79,21 @@ def validate(report_path: Path) -> list[str]:
|
|
|
118
79
|
return [f"final-report not found: {report_path}"]
|
|
119
80
|
|
|
120
81
|
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
82
|
html_path = report_path.with_name(report_path.stem + ".html")
|
|
124
83
|
|
|
125
|
-
# (1) sibling
|
|
126
|
-
if not slim_path.is_file():
|
|
127
|
-
failures.append(f"missing slim artifact: {slim_path}")
|
|
84
|
+
# (1) sibling artifact exists
|
|
128
85
|
if not html_path.is_file():
|
|
129
|
-
|
|
130
|
-
if failures:
|
|
131
|
-
return failures
|
|
86
|
+
return [f"missing html artifact: {html_path}"]
|
|
132
87
|
|
|
133
|
-
slim = slim_path.read_text(encoding="utf-8")
|
|
134
88
|
html_text = html_path.read_text(encoding="utf-8")
|
|
135
89
|
html_body = _main_body(html_text)
|
|
136
|
-
html_body_text = html_lib.unescape(html_body)
|
|
137
90
|
|
|
138
|
-
# (
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
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)
|
|
91
|
+
# (2) source-digest staleness — html's run-meta source-sha256 must
|
|
92
|
+
# match the current MD's digest. Mismatch means the html was
|
|
93
|
+
# rendered against an older MD body and Phase 7 step 1.5 was not
|
|
94
|
+
# re-run.
|
|
95
|
+
expected_md_digest = source_digest(md)
|
|
155
96
|
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
97
|
if html_digest is None:
|
|
166
98
|
failures.append(
|
|
167
99
|
"html missing source-sha256 in run-meta — re-render with okstra render-views"
|
|
@@ -172,31 +104,7 @@ def validate(report_path: Path) -> list[str]:
|
|
|
172
104
|
f"current MD {expected_md_digest[:12]}…"
|
|
173
105
|
)
|
|
174
106
|
|
|
175
|
-
# (
|
|
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
|
|
107
|
+
# (3) deliverable sections contain no form controls
|
|
200
108
|
for chunk in _no_form_sections(html_body):
|
|
201
109
|
if "<textarea" in chunk or "<input" in chunk or "<select" in chunk:
|
|
202
110
|
failures.append(
|
|
@@ -204,11 +112,11 @@ def validate(report_path: Path) -> list[str]:
|
|
|
204
112
|
)
|
|
205
113
|
break
|
|
206
114
|
|
|
207
|
-
# (
|
|
115
|
+
# (4) no external URLs in <script src> / <link href> / etc.
|
|
208
116
|
if _EXTERNAL_URL_RE.search(html_text):
|
|
209
117
|
failures.append("html contains external URL in script/link/img — must be self-contained")
|
|
210
118
|
|
|
211
|
-
# (
|
|
119
|
+
# (5) Response ID parity: HTML form rows ↔ §5 C-* rows in MD.
|
|
212
120
|
# Bidirectional — catches both "MD has C-* the HTML lost" AND
|
|
213
121
|
# "HTML has stale C-* that the current MD no longer declares".
|
|
214
122
|
md_ids = _md_response_ids(md)
|
|
@@ -221,43 +129,6 @@ def validate(report_path: Path) -> list[str]:
|
|
|
221
129
|
return failures
|
|
222
130
|
|
|
223
131
|
|
|
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
132
|
def _md_response_ids(md: str) -> list[str]:
|
|
262
133
|
items = parse_clarification_items(md) or []
|
|
263
134
|
return sorted({it.row_id for it in items if re.fullmatch(r"C-\d+", it.row_id)})
|
|
@@ -265,7 +136,7 @@ def _md_response_ids(md: str) -> list[str]:
|
|
|
265
136
|
|
|
266
137
|
def main(argv: list[str] | None = None) -> int:
|
|
267
138
|
parser = argparse.ArgumentParser(
|
|
268
|
-
description="Validate
|
|
139
|
+
description="Validate the self-contained HTML view of an okstra final-report."
|
|
269
140
|
)
|
|
270
141
|
parser.add_argument(
|
|
271
142
|
"report_path",
|