okstra 0.31.0 → 0.32.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.
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +3 -3
- package/runtime/agents/workers/report-writer-worker.md +45 -67
- 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/okstra_ctl/final_report_schema.py +253 -0
- package/runtime/python/okstra_ctl/render_final_report.py +201 -0
- package/runtime/python/okstra_ctl/report_views.py +108 -305
- package/runtime/python/okstra_ctl/wizard.py +16 -5
- 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/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 +29 -28
- package/runtime/templates/reports/final-report.template.md +370 -411
- package/runtime/templates/reports/report.css +12 -6
- 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 +20 -1
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
"""Render
|
|
1
|
+
"""Render the self-contained HTML view of an okstra final-report markdown.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Single product, single source of truth:
|
|
4
4
|
|
|
5
|
-
* ``slim_markdown(src_md)`` — deterministic token-saving stripper that
|
|
6
|
-
removes presentational fluff while preserving every substring the
|
|
7
|
-
``validators/validate-run.py`` substring checks rely on. Intended as
|
|
8
|
-
the AI consumption surface (next-phase lead prompt input).
|
|
9
5
|
* ``render_html(src_md, *, run_meta)`` — deterministic self-contained
|
|
10
6
|
HTML renderer for human readers. Sections §5/§6/§7 user-actionable
|
|
11
7
|
rows (those reachable from §5 ``C-*`` IDs) get embedded ``<form>``
|
|
@@ -18,44 +14,9 @@ response`` button (client-side JS, single-reference-point with the
|
|
|
18
14
|
Python ``serialize_user_response`` function below) and the user pastes
|
|
19
15
|
it to ``runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md``.
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
zero is meaningful audit info).
|
|
25
|
-
- ``STRIP_DECORATIVE_HR`` — collapse 3+ consecutive horizontal rules
|
|
26
|
-
to a single ``---``.
|
|
27
|
-
- ``STRIP_VERDICT_DETAIL`` — drop ``### 4.5.9 Verdict details`` wide
|
|
28
|
-
matrix body when the preceding ``Verdict summary`` card is present.
|
|
29
|
-
Keep the heading line itself so heading-set invariants hold.
|
|
30
|
-
- ``STRIP_INTRO_RECIPE`` — drop the ``> **읽는 법**:`` quoted recipe
|
|
31
|
-
paragraph beneath the Token Usage Summary table. The definition is
|
|
32
|
-
audience-facing; the slim AI consumer already knows the schema.
|
|
33
|
-
- ``COLLAPSE_BLANK_LINES`` — normalise 3+ blank lines to one.
|
|
34
|
-
|
|
35
|
-
Absolutely-preserve invariants (validator hard-fail prevention):
|
|
36
|
-
- ``## Verdict Card`` block content byte-identical
|
|
37
|
-
- ``## 2. Final Verdict`` table rows for ``Verdict Token`` /
|
|
38
|
-
``Direction`` / ``Next Step``
|
|
39
|
-
- ``## 0. Clarification Response Carried In`` (when present)
|
|
40
|
-
- implementation-planning 9 substrings (Option Candidates,
|
|
41
|
-
Trade-off, Recommended Option, Stepwise Execution Order,
|
|
42
|
-
Dependency, Validation Checklist, Rollback, User Approval Request,
|
|
43
|
-
Plan Body Verification + Gate result:)
|
|
44
|
-
- implementation §4.7 8 sub-section substrings (Approved Plan
|
|
45
|
-
Reference, Commit List, Diff Summary, Out-of-plan Edits,
|
|
46
|
-
Validation Evidence, Verifier Results, Rollback Verification,
|
|
47
|
-
Routing Recommendation)
|
|
48
|
-
- final-verification §4.8 6 sub-section substrings (Source
|
|
49
|
-
Implementation Report, Acceptance Blockers, Residual Risk,
|
|
50
|
-
Validation Evidence, Read-only Command Log, Routing Recommendation)
|
|
51
|
-
- release-handoff §4.6.1–4.6.7 sub-section substrings (Source
|
|
52
|
-
Verification Report, Feature Branch & Working-Tree State, User
|
|
53
|
-
Selections, Executed Commands, Commit List, Merge Conflict Probe,
|
|
54
|
-
Pull Request Outcome, Routing Recommendation)
|
|
55
|
-
|
|
56
|
-
The substring list is sourced from validate-run.py via
|
|
57
|
-
``preserved_substrings()`` so a future contract change touches one
|
|
58
|
-
place only.
|
|
17
|
+
The next-phase lead prompt now consumes the source MD directly — the
|
|
18
|
+
former ``*.slim.md`` derived artefact was removed (its strip rules
|
|
19
|
+
saved no tokens worth the contract maintenance cost).
|
|
59
20
|
"""
|
|
60
21
|
from __future__ import annotations
|
|
61
22
|
|
|
@@ -68,27 +29,12 @@ from typing import Iterable, Optional
|
|
|
68
29
|
|
|
69
30
|
|
|
70
31
|
def source_digest(src_md: str) -> str:
|
|
71
|
-
"""Stable identifier for a final-report markdown body.
|
|
72
|
-
|
|
73
|
-
|
|
32
|
+
"""Stable identifier for a final-report markdown body. The HTML
|
|
33
|
+
view embeds this in its run-meta block so the validator can detect
|
|
34
|
+
a stale html that was generated from an older MD."""
|
|
74
35
|
return hashlib.sha256(src_md.encode("utf-8")).hexdigest()
|
|
75
36
|
|
|
76
37
|
|
|
77
|
-
_SOURCE_DIGEST_COMMENT_RE = re.compile(
|
|
78
|
-
r"^<!--\s*source-sha256:\s*([0-9a-f]{64})\s*-->\s*$", re.MULTILINE
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def extract_slim_digest(slim_text: str) -> Optional[str]:
|
|
83
|
-
"""Return the source-sha256 token embedded in a slim MD's first
|
|
84
|
-
comment line, or None if absent (older artifacts)."""
|
|
85
|
-
first_nonblank = next(
|
|
86
|
-
(ln for ln in slim_text.splitlines() if ln.strip()), ""
|
|
87
|
-
)
|
|
88
|
-
m = _SOURCE_DIGEST_COMMENT_RE.match(first_nonblank)
|
|
89
|
-
return m.group(1) if m else None
|
|
90
|
-
|
|
91
|
-
|
|
92
38
|
_HTML_DIGEST_RE = re.compile(r'"source-sha256":"([0-9a-f]{64})"')
|
|
93
39
|
|
|
94
40
|
|
|
@@ -97,17 +43,6 @@ def extract_html_digest(html_text: str) -> Optional[str]:
|
|
|
97
43
|
return m.group(1) if m else None
|
|
98
44
|
|
|
99
45
|
|
|
100
|
-
def _strip_leading_digest_comment(text: str) -> str:
|
|
101
|
-
"""Remove a leading ``<!-- source-sha256: <hex> -->\\n`` line if
|
|
102
|
-
present. Used so ``slim_markdown`` and ``render_html`` can be called
|
|
103
|
-
on their own previously-stripped output without producing a
|
|
104
|
-
different digest the second time around."""
|
|
105
|
-
head, _, rest = text.partition("\n")
|
|
106
|
-
if _SOURCE_DIGEST_COMMENT_RE.match(head):
|
|
107
|
-
return rest
|
|
108
|
-
return text
|
|
109
|
-
|
|
110
|
-
|
|
111
46
|
_LEADING_FRONTMATTER_RE = re.compile(
|
|
112
47
|
r"\A---\s*\n.*?\n---\s*\n?", re.DOTALL
|
|
113
48
|
)
|
|
@@ -116,8 +51,8 @@ _LEADING_FRONTMATTER_RE = re.compile(
|
|
|
116
51
|
def _strip_leading_frontmatter(text: str) -> str:
|
|
117
52
|
"""Remove a leading YAML frontmatter block (``---\\n…\\n---\\n``) if
|
|
118
53
|
present. Used by ``render_html`` so the HTML view does not surface
|
|
119
|
-
the Obsidian-side frontmatter as a paragraph
|
|
120
|
-
|
|
54
|
+
the Obsidian-side frontmatter as a paragraph; the source MD keeps
|
|
55
|
+
the frontmatter for Obsidian/Templater consumers."""
|
|
121
56
|
return _LEADING_FRONTMATTER_RE.sub("", text, count=1)
|
|
122
57
|
|
|
123
58
|
from .clarification_items import (
|
|
@@ -129,205 +64,10 @@ from .clarification_items import (
|
|
|
129
64
|
|
|
130
65
|
|
|
131
66
|
# --------------------------------------------------------------------------- #
|
|
132
|
-
#
|
|
133
|
-
# validators/validate-run.py).
|
|
134
|
-
# --------------------------------------------------------------------------- #
|
|
135
|
-
|
|
136
|
-
PRESERVED_SUBSTRINGS_PLANNING: tuple[str, ...] = (
|
|
137
|
-
"Option Candidates",
|
|
138
|
-
"Trade-off",
|
|
139
|
-
"Recommended Option",
|
|
140
|
-
"Stepwise Execution Order",
|
|
141
|
-
"Dependency",
|
|
142
|
-
"Validation Checklist",
|
|
143
|
-
"Rollback",
|
|
144
|
-
"User Approval Request",
|
|
145
|
-
"Plan Body Verification",
|
|
146
|
-
"Gate result:",
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
PRESERVED_SUBSTRINGS_IMPLEMENTATION: tuple[str, ...] = (
|
|
150
|
-
"Approved Plan Reference",
|
|
151
|
-
"Commit List",
|
|
152
|
-
"Diff Summary",
|
|
153
|
-
"Out-of-plan Edits",
|
|
154
|
-
"Validation Evidence",
|
|
155
|
-
"Verifier Results",
|
|
156
|
-
"Rollback Verification",
|
|
157
|
-
"Routing Recommendation",
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
PRESERVED_SUBSTRINGS_FINAL_VERIFICATION: tuple[str, ...] = (
|
|
161
|
-
"Source Implementation Report",
|
|
162
|
-
"Acceptance Blockers",
|
|
163
|
-
"Residual Risk",
|
|
164
|
-
"Validation Evidence",
|
|
165
|
-
"Read-only Command Log",
|
|
166
|
-
"Routing Recommendation",
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
PRESERVED_SUBSTRINGS_RELEASE_HANDOFF: tuple[str, ...] = (
|
|
170
|
-
"Source Verification Report",
|
|
171
|
-
"Feature Branch & Working-Tree State",
|
|
172
|
-
"User Selections",
|
|
173
|
-
"Executed Commands",
|
|
174
|
-
"Commit List",
|
|
175
|
-
"Merge Conflict Probe",
|
|
176
|
-
"Pull Request Outcome",
|
|
177
|
-
"Routing Recommendation",
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
PRESERVED_HEADINGS_ALWAYS: tuple[str, ...] = (
|
|
181
|
-
"## Verdict Card",
|
|
182
|
-
"## 0. Clarification Response Carried In",
|
|
183
|
-
"## 2. Final Verdict",
|
|
184
|
-
"## 5. Clarification Items",
|
|
185
|
-
"## 6. Recommended Next Steps",
|
|
186
|
-
"## 7. Follow-up Tasks",
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def preserved_substrings(task_type: Optional[str]) -> tuple[str, ...]:
|
|
191
|
-
"""Return every substring slim_markdown MUST keep byte-identical."""
|
|
192
|
-
extras: tuple[str, ...] = ()
|
|
193
|
-
if task_type == "implementation-planning":
|
|
194
|
-
extras = PRESERVED_SUBSTRINGS_PLANNING
|
|
195
|
-
elif task_type == "implementation":
|
|
196
|
-
extras = PRESERVED_SUBSTRINGS_IMPLEMENTATION
|
|
197
|
-
elif task_type == "final-verification":
|
|
198
|
-
extras = PRESERVED_SUBSTRINGS_FINAL_VERIFICATION
|
|
199
|
-
elif task_type == "release-handoff":
|
|
200
|
-
extras = PRESERVED_SUBSTRINGS_RELEASE_HANDOFF
|
|
201
|
-
return PRESERVED_HEADINGS_ALWAYS + extras
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# --------------------------------------------------------------------------- #
|
|
205
|
-
# Slim stripper
|
|
67
|
+
# HTML renderer — minimal markdown-to-HTML converter (no external deps)
|
|
206
68
|
# --------------------------------------------------------------------------- #
|
|
207
69
|
|
|
208
|
-
_HR_PATTERN = re.compile(r"^-{3,}\s*$")
|
|
209
70
|
_BLANK_PATTERN = re.compile(r"^\s*$")
|
|
210
|
-
_RECIPE_QUOTE_PATTERN = re.compile(r"^>\s*\*\*읽는 법\*\*")
|
|
211
|
-
_CLI_COST_ROW_HINT = "Codex/Gemini CLI 추가 비용"
|
|
212
|
-
# Verdict details heading historically lived under ``### 4.5.9 Verdict
|
|
213
|
-
# details`` and is now (post-c2c603d split) written as ``#### Verdict
|
|
214
|
-
# details`` directly under §4.5.9 Plan Body Verification. Match both
|
|
215
|
-
# forms so the slim stripper drops the wide matrix body regardless of
|
|
216
|
-
# which template the report was generated against. The trailing ``$``
|
|
217
|
-
# anchor avoids accidentally matching ``Verdict details (excerpt)`` etc.
|
|
218
|
-
_VERDICT_DETAIL_RE = re.compile(
|
|
219
|
-
r"^#{3,4}\s+(?:4\.5\.9\s+)?Verdict details\s*$"
|
|
220
|
-
)
|
|
221
|
-
_VERDICT_SUMMARY_RE = re.compile(
|
|
222
|
-
r"^#{3,4}\s+(?:4\.5\.9\s+)?Verdict summary\s*$"
|
|
223
|
-
)
|
|
224
|
-
_ANY_HEADING_RE = re.compile(r"^#{1,6}\s+\S")
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def slim_markdown(src_md: str, *, task_type: Optional[str] = None) -> str:
|
|
228
|
-
"""Return a slimmed copy of ``src_md`` per the rules in the module
|
|
229
|
-
docstring. Deterministic — same input always yields the same output.
|
|
230
|
-
|
|
231
|
-
Idempotency note: the returned slim begins with a
|
|
232
|
-
``<!-- source-sha256: ... -->`` comment so the validator can detect
|
|
233
|
-
a stale slim against a later MD. Re-running slim on its own output
|
|
234
|
-
must produce a byte-identical result, so we first strip the leading
|
|
235
|
-
digest comment (if any) before re-deriving everything from the
|
|
236
|
-
normalized body.
|
|
237
|
-
"""
|
|
238
|
-
src_md = _strip_leading_digest_comment(src_md)
|
|
239
|
-
lines = src_md.splitlines()
|
|
240
|
-
out: list[str] = []
|
|
241
|
-
|
|
242
|
-
hr_run = 0
|
|
243
|
-
blank_run = 0
|
|
244
|
-
in_verdict_detail = False # inside the §4.5.9 Verdict details wide matrix
|
|
245
|
-
|
|
246
|
-
for line in lines:
|
|
247
|
-
# STRIP_VERDICT_DETAIL — drop body of Verdict details (both
|
|
248
|
-
# ``### 4.5.9 Verdict details`` and ``#### Verdict details``
|
|
249
|
-
# forms), keep the heading line itself.
|
|
250
|
-
if _VERDICT_DETAIL_RE.match(line):
|
|
251
|
-
in_verdict_detail = True
|
|
252
|
-
out.append(line)
|
|
253
|
-
continue
|
|
254
|
-
if in_verdict_detail:
|
|
255
|
-
if _ANY_HEADING_RE.match(line):
|
|
256
|
-
in_verdict_detail = False
|
|
257
|
-
# fall through and emit this heading
|
|
258
|
-
else:
|
|
259
|
-
continue
|
|
260
|
-
|
|
261
|
-
# STRIP_INTRO_RECIPE — drop the "> **읽는 법**:" quote paragraph.
|
|
262
|
-
if _RECIPE_QUOTE_PATTERN.match(line):
|
|
263
|
-
continue
|
|
264
|
-
|
|
265
|
-
# STRIP_TOKEN_PLACEHOLDER_ZEROS — drop $0.00 rows except CLI cost.
|
|
266
|
-
if _is_zero_cost_row(line) and _CLI_COST_ROW_HINT not in line:
|
|
267
|
-
continue
|
|
268
|
-
|
|
269
|
-
# STRIP_DECORATIVE_HR — collapse runs of horizontal rules.
|
|
270
|
-
if _HR_PATTERN.match(line):
|
|
271
|
-
hr_run += 1
|
|
272
|
-
if hr_run > 1:
|
|
273
|
-
continue
|
|
274
|
-
else:
|
|
275
|
-
hr_run = 0
|
|
276
|
-
|
|
277
|
-
# COLLAPSE_BLANK_LINES — collapse 3+ blank lines to one.
|
|
278
|
-
if _BLANK_PATTERN.match(line):
|
|
279
|
-
blank_run += 1
|
|
280
|
-
if blank_run > 1:
|
|
281
|
-
continue
|
|
282
|
-
else:
|
|
283
|
-
blank_run = 0
|
|
284
|
-
|
|
285
|
-
out.append(line)
|
|
286
|
-
|
|
287
|
-
result = "\n".join(out)
|
|
288
|
-
# STRIP_DECORATIVE_HR (post-process) — collapse runs of HRs that
|
|
289
|
-
# were separated only by blank lines (the line scanner above can't
|
|
290
|
-
# reach across blanks without losing blank-line semantics elsewhere).
|
|
291
|
-
result = re.sub(r"(?:^---\s*\n(?:\s*\n)*){2,}", "---\n", result, flags=re.MULTILINE)
|
|
292
|
-
if src_md.endswith("\n") and not result.endswith("\n"):
|
|
293
|
-
result += "\n"
|
|
294
|
-
|
|
295
|
-
_assert_preserved(src_md, result, task_type)
|
|
296
|
-
# Prepend the source-digest comment so the validator can detect a
|
|
297
|
-
# stale slim that was generated from an older MD. The digest is
|
|
298
|
-
# over the slim BODY itself, not over the original MD, so the
|
|
299
|
-
# validator's expected-slim recomputation embeds the same digest
|
|
300
|
-
# (any byte difference between expected and actual body shows up
|
|
301
|
-
# as a mismatched digest).
|
|
302
|
-
digest_line = f"<!-- source-sha256: {source_digest(result)} -->\n"
|
|
303
|
-
return digest_line + result
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _is_zero_cost_row(line: str) -> bool:
|
|
307
|
-
"""True iff this is a markdown table row whose cost cell is exactly
|
|
308
|
-
``$0.00`` and no other meaningful cell content suggests audit
|
|
309
|
-
relevance.
|
|
310
|
-
"""
|
|
311
|
-
if not line.lstrip().startswith("|"):
|
|
312
|
-
return False
|
|
313
|
-
if "$0.00" not in line:
|
|
314
|
-
return False
|
|
315
|
-
# Heuristic — defer to the CLI-cost guard at the caller.
|
|
316
|
-
return True
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def _assert_preserved(original: str, slim: str, task_type: Optional[str]) -> None:
|
|
320
|
-
"""Raise AssertionError if any preserved substring was dropped."""
|
|
321
|
-
missing = [s for s in preserved_substrings(task_type) if s in original and s not in slim]
|
|
322
|
-
if missing:
|
|
323
|
-
raise AssertionError(
|
|
324
|
-
"slim_markdown dropped preserved substring(s): " + ", ".join(missing)
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# --------------------------------------------------------------------------- #
|
|
329
|
-
# HTML renderer — minimal markdown-to-HTML converter (no external deps)
|
|
330
|
-
# --------------------------------------------------------------------------- #
|
|
331
71
|
|
|
332
72
|
_HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.*?)\s*$")
|
|
333
73
|
_CODEFENCE_PATTERN = re.compile(r"^```(.*)$")
|
|
@@ -357,12 +97,9 @@ def render_html(src_md: str, *, run_meta: RunMeta, css: str, js: str) -> str:
|
|
|
357
97
|
into the document (validator enforces this — see
|
|
358
98
|
validate-report-views.py).
|
|
359
99
|
"""
|
|
360
|
-
#
|
|
361
|
-
# previous slim cycle doesn't leak into the rendered HTML body, then
|
|
362
|
-
# compute the digest over the normalised MD body. The validator
|
|
100
|
+
# Compute the digest over the source MD body. The validator
|
|
363
101
|
# recomputes the same digest from the current MD and compares it to
|
|
364
102
|
# the value embedded in run-meta below.
|
|
365
|
-
src_md = _strip_leading_digest_comment(src_md)
|
|
366
103
|
digest = source_digest(src_md)
|
|
367
104
|
body_md = _strip_leading_frontmatter(src_md)
|
|
368
105
|
body_html, toc_headings = _markdown_to_html(body_md)
|
|
@@ -571,9 +308,20 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
|
|
|
571
308
|
and "Status" in header_cells
|
|
572
309
|
)
|
|
573
310
|
|
|
311
|
+
narrow_cols = _narrow_columns(header_cells, rows)
|
|
312
|
+
# `User input` carries the embedded form widget (textarea / select /
|
|
313
|
+
# input) which needs all the horizontal space it can get; its
|
|
314
|
+
# markdown plain text is empty or a short placeholder so the auto
|
|
315
|
+
# detector would otherwise flag it as narrow. Force it wide.
|
|
316
|
+
if "User input" in header_cells:
|
|
317
|
+
narrow_cols.discard(header_cells.index("User input"))
|
|
318
|
+
|
|
574
319
|
head = (
|
|
575
320
|
"<thead><tr>"
|
|
576
|
-
+ "".join(
|
|
321
|
+
+ "".join(
|
|
322
|
+
f"<th{_col_class(idx, narrow_cols)}>{_inline(c)}</th>"
|
|
323
|
+
for idx, c in enumerate(header_cells)
|
|
324
|
+
)
|
|
577
325
|
+ "</tr></thead>"
|
|
578
326
|
)
|
|
579
327
|
|
|
@@ -611,7 +359,9 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
|
|
|
611
359
|
f"<td>{_form_control(response_id, kind, status, cell, statement)}</td>"
|
|
612
360
|
)
|
|
613
361
|
else:
|
|
614
|
-
cells_html.append(
|
|
362
|
+
cells_html.append(
|
|
363
|
+
f"<td{_col_class(idx, narrow_cols)}>{_inline(cell)}</td>"
|
|
364
|
+
)
|
|
615
365
|
body_rows.append(
|
|
616
366
|
f'<tr data-response-id="{html.escape(response_id)}" '
|
|
617
367
|
f'data-kind="{html.escape(kind)}" '
|
|
@@ -622,7 +372,10 @@ def _emit_table(lines: list[str], start: int, section_path: list[str]) -> tuple[
|
|
|
622
372
|
else:
|
|
623
373
|
body_rows.append(
|
|
624
374
|
"<tr>"
|
|
625
|
-
+ "".join(
|
|
375
|
+
+ "".join(
|
|
376
|
+
f"<td{_col_class(idx, narrow_cols)}>{_inline(c)}</td>"
|
|
377
|
+
for idx, c in enumerate(row)
|
|
378
|
+
)
|
|
626
379
|
+ "</tr>"
|
|
627
380
|
)
|
|
628
381
|
|
|
@@ -756,23 +509,84 @@ def _emit_list(lines: list[str], start: int) -> tuple[str, int]:
|
|
|
756
509
|
return f"<{tag}>" + "".join(items) + f"</{tag}>", i - start
|
|
757
510
|
|
|
758
511
|
|
|
759
|
-
|
|
512
|
+
_NARROW_COL_MAX_PLAIN_LEN = 30
|
|
760
513
|
|
|
761
514
|
_INLINE_MD_STRIP_RE = re.compile(r"[`*_]")
|
|
762
515
|
|
|
516
|
+
# Header-name whitelist: columns whose width is effectively fixed by
|
|
517
|
+
# convention even when one row carries a long body cell (e.g. "출처"
|
|
518
|
+
# starts with a short token but the prose body of a single citation
|
|
519
|
+
# row may run 60+ chars). Matched as case-insensitive prefix on the
|
|
520
|
+
# header's first plain token, so "Ticket ID" / "출처 (brief/source/worker)"
|
|
521
|
+
# / "환산 토큰 (input 기준)" all hit the same entry. Keep this list
|
|
522
|
+
# tight — every entry is a promise that the column is *always* one of
|
|
523
|
+
# a handful of canonical short values across every final-report.
|
|
524
|
+
_NARROW_HEADER_PREFIXES: tuple[str, ...] = (
|
|
525
|
+
# English / latin
|
|
526
|
+
"id",
|
|
527
|
+
"ticket id",
|
|
528
|
+
"kind",
|
|
529
|
+
"status",
|
|
530
|
+
"blocks",
|
|
531
|
+
"origin",
|
|
532
|
+
"priority",
|
|
533
|
+
"auto-spawn",
|
|
534
|
+
# Korean
|
|
535
|
+
"출처",
|
|
536
|
+
"에이전트",
|
|
537
|
+
"역할",
|
|
538
|
+
"모델",
|
|
539
|
+
"상태",
|
|
540
|
+
"항목",
|
|
541
|
+
"처리 토큰",
|
|
542
|
+
"환산 토큰",
|
|
543
|
+
"비용",
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _plain_len(raw_cell: str) -> int:
|
|
548
|
+
return len(_INLINE_MD_STRIP_RE.sub("", raw_cell or "").strip())
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _matches_narrow_whitelist(header: str) -> bool:
|
|
552
|
+
plain = _INLINE_MD_STRIP_RE.sub("", header or "").strip().lower()
|
|
553
|
+
return any(plain.startswith(p) for p in _NARROW_HEADER_PREFIXES)
|
|
554
|
+
|
|
763
555
|
|
|
764
|
-
def
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
556
|
+
def _narrow_columns(
|
|
557
|
+
header_cells: list[str], rows: list[list[str]]
|
|
558
|
+
) -> set[int]:
|
|
559
|
+
"""Return the set of column indices that should render with the
|
|
560
|
+
fixed-narrow ``td-narrow`` width (5% under report.css).
|
|
768
561
|
|
|
769
|
-
|
|
770
|
-
|
|
562
|
+
A column qualifies when EITHER:
|
|
563
|
+
(a) its header matches `_NARROW_HEADER_PREFIXES` — these are
|
|
564
|
+
``ID`` / ``출처`` / ``에이전트`` etc. whose values are
|
|
565
|
+
conventionally short across every final-report even when a
|
|
566
|
+
single row drops in 60+ chars of prose; OR
|
|
567
|
+
(b) every cell in the column (header + body) fits within
|
|
568
|
+
``_NARROW_COL_MAX_PLAIN_LEN`` plain chars — this catches
|
|
569
|
+
ad-hoc short columns that the whitelist did not anticipate.
|
|
570
|
+
|
|
571
|
+
Per-column (not per-cell) so the verdict applies uniformly across
|
|
572
|
+
the column even when one neighbouring column holds long-form text.
|
|
771
573
|
"""
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
574
|
+
narrow: set[int] = set()
|
|
575
|
+
for col, header in enumerate(header_cells):
|
|
576
|
+
if _matches_narrow_whitelist(header):
|
|
577
|
+
narrow.add(col)
|
|
578
|
+
continue
|
|
579
|
+
max_len = _plain_len(header)
|
|
580
|
+
for row in rows:
|
|
581
|
+
if col < len(row):
|
|
582
|
+
max_len = max(max_len, _plain_len(row[col]))
|
|
583
|
+
if max_len <= _NARROW_COL_MAX_PLAIN_LEN:
|
|
584
|
+
narrow.add(col)
|
|
585
|
+
return narrow
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _col_class(col_idx: int, narrow_cols: set[int]) -> str:
|
|
589
|
+
return ' class="td-narrow"' if col_idx in narrow_cols else ""
|
|
776
590
|
|
|
777
591
|
|
|
778
592
|
def _inline(text: str) -> str:
|
|
@@ -850,28 +664,17 @@ def serialize_user_response(
|
|
|
850
664
|
# Convenience entrypoint for tests + CLI.
|
|
851
665
|
# --------------------------------------------------------------------------- #
|
|
852
666
|
|
|
853
|
-
def
|
|
667
|
+
def render_html_view(
|
|
854
668
|
src_md_path: Path,
|
|
855
669
|
*,
|
|
856
670
|
run_meta: RunMeta,
|
|
857
671
|
css: str,
|
|
858
672
|
js: str,
|
|
859
|
-
) ->
|
|
860
|
-
"""Write
|
|
861
|
-
|
|
673
|
+
) -> Path:
|
|
674
|
+
"""Write ``<stem>.html`` next to ``src_md_path`` and return its
|
|
675
|
+
path. Idempotent — overwrites the existing html sibling."""
|
|
862
676
|
src_text = src_md_path.read_text(encoding="utf-8")
|
|
863
|
-
|
|
864
|
-
slim_path = src_md_path.with_suffix(".slim.md")
|
|
865
|
-
if src_md_path.suffix == ".md":
|
|
866
|
-
# ``with_suffix`` replaces the LAST suffix only; we want
|
|
867
|
-
# ``foo.md`` → ``foo.slim.md`` (insert before ``.md``).
|
|
868
|
-
slim_path = src_md_path.with_name(src_md_path.stem + ".slim.md")
|
|
869
|
-
|
|
870
677
|
html_path = src_md_path.with_name(src_md_path.stem + ".html")
|
|
871
|
-
|
|
872
|
-
slim_text = slim_markdown(src_text, task_type=run_meta.task_type)
|
|
873
678
|
html_text = render_html(src_text, run_meta=run_meta, css=css, js=js)
|
|
874
|
-
|
|
875
|
-
slim_path.write_text(slim_text, encoding="utf-8")
|
|
876
679
|
html_path.write_text(html_text, encoding="utf-8")
|
|
877
|
-
return
|
|
680
|
+
return html_path
|
|
@@ -1314,11 +1314,15 @@ STEPS: list[Step] = [
|
|
|
1314
1314
|
and s.use_defaults is None),
|
|
1315
1315
|
build=_build_defaults_or_custom, submit=_submit_defaults_or_custom,
|
|
1316
1316
|
owns=("use_defaults",)),
|
|
1317
|
-
#
|
|
1318
|
-
# has analyser-candidate workers
|
|
1317
|
+
# Worker roster is ALWAYS prompted (independent of the defaults /
|
|
1318
|
+
# customize branch) when the profile has analyser-candidate workers
|
|
1319
|
+
# (required ∪ optional, minus report-writer). Users repeatedly hit
|
|
1320
|
+
# the failure mode where the lead silently picked "Use defaults"
|
|
1321
|
+
# and the worker prompt never appeared — defaults should govern
|
|
1322
|
+
# model choice, not worker selection. `implementation` task-type is
|
|
1323
|
+
# still skipped because it runs lead + executor only.
|
|
1319
1324
|
Step(S_WORKERS_OVERRIDE,
|
|
1320
|
-
applies=lambda s: (s.
|
|
1321
|
-
and s.task_type != "implementation"
|
|
1325
|
+
applies=lambda s: (s.task_type != "implementation"
|
|
1322
1326
|
and any(
|
|
1323
1327
|
w != "report-writer"
|
|
1324
1328
|
for w in (s.profile_workers
|
|
@@ -1451,10 +1455,17 @@ def _identity_ready(s: WizardState) -> bool:
|
|
|
1451
1455
|
def _ready_for_confirm(s: WizardState) -> bool:
|
|
1452
1456
|
if s.use_defaults is None:
|
|
1453
1457
|
return False
|
|
1458
|
+
# Worker roster is required regardless of the defaults / customize
|
|
1459
|
+
# branch. Skipping this check let the lead drop into confirm with no
|
|
1460
|
+
# worker prompt ever shown — the very failure mode this gate exists
|
|
1461
|
+
# to prevent.
|
|
1462
|
+
workers_step = STEP_BY_ID[S_WORKERS_OVERRIDE]
|
|
1463
|
+
if workers_step.applies(s):
|
|
1464
|
+
return False
|
|
1454
1465
|
if s.use_defaults:
|
|
1455
1466
|
return True
|
|
1456
1467
|
# customize: every customize-branch step must be answered or not-applicable.
|
|
1457
|
-
custom_ids = [
|
|
1468
|
+
custom_ids = [S_LEAD_MODEL, S_EXECUTOR_MODEL,
|
|
1458
1469
|
S_CLAUDE_MODEL, S_CODEX_MODEL, S_GEMINI_MODEL,
|
|
1459
1470
|
S_REPORT_WRITER_MODEL, S_DIRECTIVE, S_RELATED_TASKS,
|
|
1460
1471
|
S_CLARIFICATION, S_PR_TEMPLATE, S_PR_TEMPLATE_SCOPE]
|