okstra 0.189.2 → 0.189.4
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/architecture.md +1 -1
- package/docs/cli.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/report-writer.md +8 -1
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +5 -1
- package/runtime/python/okstra_ctl/plan_items_cli.py +12 -0
- package/runtime/python/okstra_ctl/render.py +24 -7
- package/runtime/python/okstra_ctl/report_assembly.py +10 -4
- package/runtime/python/okstra_ctl/report_html/common.py +241 -113
- package/runtime/python/okstra_ctl/report_html/context_links.py +121 -0
- package/runtime/python/okstra_ctl/report_html/models.py +11 -5
- package/runtime/python/okstra_ctl/report_html/render.py +54 -13
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +8 -1
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +8 -1
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +11 -1
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +13 -1
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +9 -1
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +29 -2
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -9
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +8 -1
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +15 -1
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +7 -1
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +9 -1
- package/runtime/python/okstra_ctl/report_translation.py +58 -1
- package/runtime/python/okstra_ctl/run.py +6 -0
- package/runtime/python/okstra_ctl/worker_audit_check.py +23 -8
- package/runtime/schemas/final-report-v2.0.schema.json +4 -0
- package/runtime/schemas/final-report-v3.0.schema.json +4 -0
- package/runtime/templates/reports/html/base.template.html +13 -3
- package/runtime/templates/reports/html/i18n/en.json +43 -5
- package/runtime/templates/reports/html/i18n/ko.json +43 -5
- package/runtime/templates/reports/html/macros/forms.html +4 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +11 -1
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +16 -15
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +5 -5
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +1 -1
- package/runtime/validators/validate-report-views.py +30 -1
- package/runtime/validators/validate-run.py +220 -3
|
@@ -13,8 +13,16 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from okstra_ctl.worker_audit_ledger import (
|
|
16
|
-
check_worker_results_audit,
|
|
17
16
|
worker_result_files,
|
|
17
|
+
worker_results_audit_findings,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Phase 7 은 같은 규칙을 두 등급으로 판정한다. 여기서 그 등급을 함께 내지 않으면
|
|
21
|
+
# exit 2 하나로 "run 이 실패한다" 고 읽힌다 — 2026-09-05 실측(dev-10626 stage-1)에서
|
|
22
|
+
# 리드가 권고 등급의 미매칭 인용을 차단 실패로 읽고 결과를 거절·재배치했다.
|
|
23
|
+
RUN_IMPACT = (
|
|
24
|
+
"blocking rows fail the run at Phase 7 (validate-run); advisory rows do not "
|
|
25
|
+
"fail the run at Phase 7 and can only be fixed now, while the worker is alive"
|
|
18
26
|
)
|
|
19
27
|
|
|
20
28
|
|
|
@@ -25,16 +33,19 @@ _CLI_EPILOG = r"""Usage:
|
|
|
25
33
|
--run-dir the run directory; worker-results/ and prompts/ hang off it
|
|
26
34
|
--worker check only this worker (default: every worker in the run)
|
|
27
35
|
|
|
28
|
-
Emits one JSON object — `{ok,
|
|
29
|
-
|
|
36
|
+
Emits one JSON object — `{ok, inspected, inspectedFiles[], failures[],
|
|
37
|
+
blocking[], advisory[], runImpact}` — and exits 2 when failures[] is
|
|
38
|
+
non-empty, 0 otherwise. `failures` is `blocking` followed by `advisory`.
|
|
30
39
|
|
|
31
40
|
Runs the Phase 7 audit-sidecar rules now, while the worker session is still
|
|
32
41
|
alive: that every result file carries no `## 0. Reading Confirmation` heading,
|
|
33
|
-
that its audit sidecar exists, and that every backticked `path:line`
|
|
34
|
-
has a matching Evidence read row in that sidecar.
|
|
42
|
+
that its audit sidecar exists (blocking), and that every backticked `path:line`
|
|
43
|
+
citation has a matching Evidence read row in that sidecar (advisory).
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
Exit 2 means "fix it now", not "the run fails": only `blocking` rows fail the
|
|
46
|
+
run at Phase 7, and `advisory` rows are the ones nothing can repair once the
|
|
47
|
+
worker session is gone. Do not reject or re-dispatch a result over an advisory
|
|
48
|
+
row alone.
|
|
38
49
|
"""
|
|
39
50
|
|
|
40
51
|
|
|
@@ -58,9 +69,10 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
58
69
|
|
|
59
70
|
def main(argv: list[str] | None = None) -> int:
|
|
60
71
|
args = _parser().parse_args(argv)
|
|
61
|
-
|
|
72
|
+
blocking, advisory = worker_results_audit_findings(
|
|
62
73
|
args.run_dir, args.task_type, args.seq, worker=args.worker
|
|
63
74
|
)
|
|
75
|
+
failures = blocking + advisory
|
|
64
76
|
# A selector that narrows to nothing also produces no failures, so `ok`
|
|
65
77
|
# alone cannot tell a real pass from a check that judged zero files —
|
|
66
78
|
# a mistyped `--worker` used to read as a clean bill of health. The count
|
|
@@ -77,6 +89,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
77
89
|
"inspected": len(inspected),
|
|
78
90
|
"inspectedFiles": inspected,
|
|
79
91
|
"failures": failures,
|
|
92
|
+
"blocking": blocking,
|
|
93
|
+
"advisory": advisory,
|
|
94
|
+
"runImpact": RUN_IMPACT,
|
|
80
95
|
},
|
|
81
96
|
ensure_ascii=False,
|
|
82
97
|
indent=2,
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
</header>
|
|
45
45
|
<main id="main-content" data-report-role="human-main">
|
|
46
46
|
{% block human_content %}{% endblock %}
|
|
47
|
-
{% if
|
|
47
|
+
{% if endStates %}
|
|
48
48
|
<section data-report-section="brief-end-states">
|
|
49
49
|
<h2>{{ t('base.brief-end-states') }}</h2>
|
|
50
50
|
<p class="section-lede">{{ t('base.brief-end-states-intro') }}</p>
|
|
51
51
|
<table>
|
|
52
|
-
<thead><tr><th scope="col">{{ t('macros.layout.id') }}</th><th scope="col">{{ t('base.brief-statement') }}</th></tr></thead>
|
|
53
|
-
<tbody>{% for row in
|
|
52
|
+
<thead><tr><th scope="col">{{ t('macros.layout.id') }}</th><th scope="col">{{ t('base.brief-statement') }}</th><th scope="col">{{ t('base.end-state-coverage') }}</th></tr></thead>
|
|
53
|
+
<tbody>{% for row in endStates %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('base.brief-section'), row.section | enum_label('briefSection'))]) }}<td>{{ row.statement | inline_code }}</td><td>{% if row.coverage %}<span class="status status-{{ row.coverage.disposition }}">{{ row.coverage.disposition | enum_label('endStateDisposition') }}</span>{% if row.coverage.get("coveredBy") %} · {{ t('base.end-state-covered-by') }} {{ row.coverage.coveredBy | inline_code }}{% endif %}{% if row.coverage.get("blockedBy") %} · {{ t('base.end-state-blocked-by') }} {{ (row.coverage.blockedBy.get("kind") if row.coverage.blockedBy is mapping else row.coverage.blockedBy) | inline_code }}{% if row.coverage.blockedBy is mapping and row.coverage.blockedBy.get("ref") %} {{ row.coverage.blockedBy.ref | inline_code }}{% endif %}{% endif %}{% if row.coverage.get("rationale") %}<p class="evidence-refs">{{ row.coverage.rationale | inline_code }}</p>{% endif %}{% else %}{{ t('base.end-state-not-judged') }}{% endif %}</td></tr>{% endfor %}</tbody>
|
|
54
54
|
</table>
|
|
55
55
|
</section>
|
|
56
56
|
{% endif %}
|
|
@@ -67,6 +67,16 @@
|
|
|
67
67
|
{% endif %}
|
|
68
68
|
</section>
|
|
69
69
|
{% endif %}
|
|
70
|
+
{% if agentActivity %}
|
|
71
|
+
<section data-report-section="agent-activity" data-reader-kind="audit">
|
|
72
|
+
<h2>{{ t('base.agent-activity') }}</h2>
|
|
73
|
+
<p class="section-lede">{{ t('base.agent-activity-intro') }}</p>
|
|
74
|
+
<table>
|
|
75
|
+
<thead><tr><th scope="col">{{ t('base.agent') }}</th><th scope="col">{{ t('base.activity') }}</th></tr></thead>
|
|
76
|
+
<tbody>{% for row in agentActivity %}<tr id="id-{{ row.activityId }}">{{ row_key(pairs=[(t('macros.layout.id'), row.activityId), (t('base.agent'), row.agent), (t('macros.layout.kind'), row.kind | enum_label('activityKind'))], status_name=t('macros.layout.status'), status_raw=row.outcome, status_text=row.outcome | enum_label('activityOutcome')) }}<td>{{ row.summary | inline_code }}{% set refs = (row.get("evidenceRefs") or []) + (row.get("clarificationRefs") or []) %}{% if refs %}<p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ refs | evidence_refs }}</p>{% endif %}</td></tr>{% endfor %}</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
</section>
|
|
79
|
+
{% endif %}
|
|
70
80
|
{{ clarification_responses(clarificationItems) }}
|
|
71
81
|
{% if evidenceIndex %}
|
|
72
82
|
<section data-report-section="evidence-ledger" data-reader-kind="audit">
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"cross-check-dissent": "Workers disagreed",
|
|
89
89
|
"missing-information": "Missing information",
|
|
90
90
|
"follow-up": "Follow-up",
|
|
91
|
-
"
|
|
91
|
+
"record-row": "Record row",
|
|
92
|
+
"summary-point": "Summary point"
|
|
92
93
|
},
|
|
93
94
|
"optionCriterion": {
|
|
94
95
|
"requirement-fit": "Requirement fit",
|
|
@@ -128,6 +129,28 @@
|
|
|
128
129
|
"Expected Behavior": "Expected behavior",
|
|
129
130
|
"Preserved Behavior": "Preserved behavior",
|
|
130
131
|
"Expected Outcome": "Expected outcome"
|
|
132
|
+
},
|
|
133
|
+
"activityKind": {
|
|
134
|
+
"worker-dispatched": "Worker dispatched",
|
|
135
|
+
"worker-completed": "Worker completed",
|
|
136
|
+
"verification-round-completed": "Verification round completed",
|
|
137
|
+
"self-fix-applied": "Self-fix applied",
|
|
138
|
+
"lead-correction-applied": "Lead correction applied",
|
|
139
|
+
"user-decision-required": "User decision required",
|
|
140
|
+
"user-decision-evaluated": "User decision evaluated"
|
|
141
|
+
},
|
|
142
|
+
"activityOutcome": {
|
|
143
|
+
"pending": "Pending",
|
|
144
|
+
"completed": "Completed",
|
|
145
|
+
"failed": "Failed",
|
|
146
|
+
"blocked": "Blocked",
|
|
147
|
+
"resolved": "Resolved"
|
|
148
|
+
},
|
|
149
|
+
"endStateDisposition": {
|
|
150
|
+
"addressed": "Addressed",
|
|
151
|
+
"deferred": "Deferred",
|
|
152
|
+
"not-applicable": "Not applicable",
|
|
153
|
+
"blocked": "Blocked"
|
|
131
154
|
}
|
|
132
155
|
},
|
|
133
156
|
"enumHint": {
|
|
@@ -163,9 +186,17 @@
|
|
|
163
186
|
"drop-that-file-into": ". Drop that file into",
|
|
164
187
|
"and-the-next-run-picks-your-answers-up-on-it": "and the next run picks your answers up on its own.",
|
|
165
188
|
"brief-end-states": "End states the brief pinned",
|
|
166
|
-
"brief-end-states-intro": "The
|
|
189
|
+
"brief-end-states-intro": "The end states this report cites (EB, PB, EO): the brief's own wording, and what this run concluded about each.",
|
|
167
190
|
"brief-section": "Section",
|
|
168
|
-
"brief-statement": "Statement"
|
|
191
|
+
"brief-statement": "Statement",
|
|
192
|
+
"end-state-coverage": "This run's verdict",
|
|
193
|
+
"end-state-covered-by": "covered by",
|
|
194
|
+
"end-state-blocked-by": "blocked by",
|
|
195
|
+
"end-state-not-judged": "not judged in this run",
|
|
196
|
+
"agent-activity": "Agent activity",
|
|
197
|
+
"agent-activity-intro": "What each agent was asked to do in this run and how it ended. Decision cards and prose cite these rows by id.",
|
|
198
|
+
"agent": "Agent",
|
|
199
|
+
"activity": "Activity"
|
|
169
200
|
},
|
|
170
201
|
"runUsage": {
|
|
171
202
|
"heading": "What this run cost",
|
|
@@ -359,7 +390,13 @@
|
|
|
359
390
|
"uncommitted-state": "Uncommitted state",
|
|
360
391
|
"per-stage-reports": "Per-stage reports",
|
|
361
392
|
"release-path": "Release path",
|
|
362
|
-
"the-current-verdict-does-not-allow-release-h": "The current verdict does not allow release-handoff to start. Clear the blocker or the condition first."
|
|
393
|
+
"the-current-verdict-does-not-allow-release-h": "The current verdict does not allow release-handoff to start. Clear the blocker or the condition first.",
|
|
394
|
+
"next-step": "Next step",
|
|
395
|
+
"conditions-for-acceptance": "Conditions for acceptance",
|
|
396
|
+
"conditions-for-acceptance-intro": "Acceptance stands only while each condition below is met; a row that blocks release-handoff must be evidenced before handoff.",
|
|
397
|
+
"condition": "Condition",
|
|
398
|
+
"evidence-required": "Evidence required",
|
|
399
|
+
"blocks-release-handoff": "Blocks release-handoff"
|
|
363
400
|
},
|
|
364
401
|
"implementation-option-selection": {
|
|
365
402
|
"original-requirements": "Original requirement coverage",
|
|
@@ -513,7 +550,8 @@
|
|
|
513
550
|
"stages": "Stages",
|
|
514
551
|
"status": "Status",
|
|
515
552
|
"extract": "Extract",
|
|
516
|
-
"keep": "Keep as-is"
|
|
553
|
+
"keep": "Keep as-is",
|
|
554
|
+
"planning-invariants": "Planning invariants"
|
|
517
555
|
},
|
|
518
556
|
"implementation": {
|
|
519
557
|
"what-was-delivered": "What was delivered",
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"cross-check-dissent": "작업자 간 이견",
|
|
89
89
|
"missing-information": "빠진 정보",
|
|
90
90
|
"follow-up": "후속 작업",
|
|
91
|
-
"
|
|
91
|
+
"record-row": "레코드 행",
|
|
92
|
+
"summary-point": "요약 항목"
|
|
92
93
|
},
|
|
93
94
|
"optionCriterion": {
|
|
94
95
|
"requirement-fit": "요구사항 적합",
|
|
@@ -128,6 +129,28 @@
|
|
|
128
129
|
"Expected Behavior": "기대 동작",
|
|
129
130
|
"Preserved Behavior": "보존 동작",
|
|
130
131
|
"Expected Outcome": "기대 산출"
|
|
132
|
+
},
|
|
133
|
+
"activityKind": {
|
|
134
|
+
"worker-dispatched": "워커 배치",
|
|
135
|
+
"worker-completed": "워커 완료",
|
|
136
|
+
"verification-round-completed": "검증 라운드 완료",
|
|
137
|
+
"self-fix-applied": "자체 수정 적용",
|
|
138
|
+
"lead-correction-applied": "리드 교정 적용",
|
|
139
|
+
"user-decision-required": "사용자 결정 필요",
|
|
140
|
+
"user-decision-evaluated": "사용자 결정 평가"
|
|
141
|
+
},
|
|
142
|
+
"activityOutcome": {
|
|
143
|
+
"pending": "대기",
|
|
144
|
+
"completed": "완료",
|
|
145
|
+
"failed": "실패",
|
|
146
|
+
"blocked": "차단",
|
|
147
|
+
"resolved": "해결"
|
|
148
|
+
},
|
|
149
|
+
"endStateDisposition": {
|
|
150
|
+
"addressed": "충족",
|
|
151
|
+
"deferred": "보류",
|
|
152
|
+
"not-applicable": "해당 없음",
|
|
153
|
+
"blocked": "차단"
|
|
131
154
|
}
|
|
132
155
|
},
|
|
133
156
|
"enumHint": {
|
|
@@ -163,9 +186,17 @@
|
|
|
163
186
|
"drop-that-file-into": ". 그 파일을 여기에 두면",
|
|
164
187
|
"and-the-next-run-picks-your-answers-up-on-it": "다음 run 이 알아서 답변을 읽어 갑니다.",
|
|
165
188
|
"brief-end-states": "브리프가 고정한 최종 상태",
|
|
166
|
-
"brief-end-states-intro": "이 리포트가 인용하는
|
|
189
|
+
"brief-end-states-intro": "이 리포트가 인용하는 최종 상태 아이디(EB·PB·EO)입니다. 문장은 태스크 브리프 원문이고, 오른쪽은 이번 run 이 그 상태에 내린 판정입니다.",
|
|
167
190
|
"brief-section": "절",
|
|
168
|
-
"brief-statement": "내용"
|
|
191
|
+
"brief-statement": "내용",
|
|
192
|
+
"end-state-coverage": "이번 run 의 판정",
|
|
193
|
+
"end-state-covered-by": "덮은 항목",
|
|
194
|
+
"end-state-blocked-by": "막힌 사유",
|
|
195
|
+
"end-state-not-judged": "이번 run 에서 판정하지 않음",
|
|
196
|
+
"agent-activity": "에이전트 활동",
|
|
197
|
+
"agent-activity-intro": "이번 run 에서 각 에이전트가 맡은 일과 그 결과입니다. 결정 카드와 본문이 이 행을 아이디로 인용합니다.",
|
|
198
|
+
"agent": "에이전트",
|
|
199
|
+
"activity": "활동"
|
|
169
200
|
},
|
|
170
201
|
"runUsage": {
|
|
171
202
|
"heading": "이번 실행에 든 비용",
|
|
@@ -359,7 +390,13 @@
|
|
|
359
390
|
"uncommitted-state": "커밋되지 않은 상태",
|
|
360
391
|
"per-stage-reports": "stage 별 리포트",
|
|
361
392
|
"release-path": "릴리스 경로",
|
|
362
|
-
"the-current-verdict-does-not-allow-release-h": "현재 판정으로는 release-handoff 를 시작할 수 없습니다. 차단 항목이나 조건을 먼저 해소하세요."
|
|
393
|
+
"the-current-verdict-does-not-allow-release-h": "현재 판정으로는 release-handoff 를 시작할 수 없습니다. 차단 항목이나 조건을 먼저 해소하세요.",
|
|
394
|
+
"next-step": "다음 단계",
|
|
395
|
+
"conditions-for-acceptance": "수용 조건",
|
|
396
|
+
"conditions-for-acceptance-intro": "아래 조건이 모두 충족되는 동안에만 수용이 유지됩니다. release-handoff 를 막는 행은 인계 전에 근거가 있어야 합니다.",
|
|
397
|
+
"condition": "조건",
|
|
398
|
+
"evidence-required": "필요한 근거",
|
|
399
|
+
"blocks-release-handoff": "release-handoff 차단"
|
|
363
400
|
},
|
|
364
401
|
"implementation-option-selection": {
|
|
365
402
|
"original-requirements": "원본 요구사항 커버리지",
|
|
@@ -513,7 +550,8 @@
|
|
|
513
550
|
"stages": "stage",
|
|
514
551
|
"status": "상태",
|
|
515
552
|
"extract": "추출",
|
|
516
|
-
"keep": "그대로 둠"
|
|
553
|
+
"keep": "그대로 둠",
|
|
554
|
+
"planning-invariants": "계획 불변 조건"
|
|
517
555
|
},
|
|
518
556
|
"implementation": {
|
|
519
557
|
"what-was-delivered": "무엇을 만들었나",
|
|
@@ -80,13 +80,13 @@
|
|
|
80
80
|
<ol class="clarification-options">
|
|
81
81
|
{% for option in options %}
|
|
82
82
|
<li class="clarification-option{% if option.role == 'recommended' %} is-recommended{% endif %}" data-option-value="{{ option.answer }}">
|
|
83
|
-
<p class="clarification-option-answer">{{ option.answer }}{% if option.role == 'recommended' %} <span class="badge">{{ t('macros.forms.recommended') }}</span>{% endif %}</p>
|
|
83
|
+
<p class="clarification-option-answer">{{ option.answer | inline_code }}{% if option.role == 'recommended' %} <span class="badge">{{ t('macros.forms.recommended') }}</span>{% endif %}</p>
|
|
84
84
|
{% if option.disposition | default(None) %}<p class="clarification-option-disposition"><code>{{ option.disposition }}</code></p>{% endif %}
|
|
85
|
-
<p class="clarification-option-rationale">{{ option.rationale }}</p>
|
|
85
|
+
<p class="clarification-option-rationale">{{ option.rationale | inline_code }}</p>
|
|
86
86
|
<dl class="clarification-option-impact">
|
|
87
87
|
<dt>{{ t('macros.forms.scope-impact') }}</dt><dd>{% if option.reach | default(None) %}{{ option.reach }}{% if option.scopeEffects | default([]) %}, {{ option.scopeEffects | join(', ') }}{% endif %}{% else %}{{ option.scopeImpact | join(', ') }}{% endif %}</dd>
|
|
88
|
-
<dt>{{ t('macros.forms.added-work') }}</dt><dd>{{ option.addedWork }}</dd>
|
|
89
|
-
<dt>{{ t('macros.forms.direction-change') }}</dt><dd>{{ option.directionChange }}</dd>
|
|
88
|
+
<dt>{{ t('macros.forms.added-work') }}</dt><dd>{{ option.addedWork | inline_code }}</dd>
|
|
89
|
+
<dt>{{ t('macros.forms.direction-change') }}</dt><dd>{{ option.directionChange | inline_code }}</dd>
|
|
90
90
|
</dl>
|
|
91
91
|
</li>
|
|
92
92
|
{% endfor %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{% extends "html/base.template.html" %}
|
|
2
|
-
{% from "html/macros/layout.html" import narrative as render_narrative %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, row_key %}
|
|
3
3
|
{% from "html/macros/visualizations.html" import figure %}
|
|
4
4
|
|
|
5
5
|
{% block human_content %}
|
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
<h2>{{ t('tasks.final-verification.verification-verdict') }}</h2>
|
|
8
8
|
<p><span class="status status-{{ verdictToken }}">{{ verdictToken }}</span></p>
|
|
9
9
|
{{ render_narrative(narrative.verdictExplanation, "finalVerification.userNarrative.verdictExplanation") }}
|
|
10
|
+
<p data-report-field="finalVerdict.finalConclusion">{{ finalVerdict.finalConclusion | inline_code }}</p>
|
|
11
|
+
<p data-report-field="finalVerdict.nextStep"><strong>{{ t('tasks.final-verification.next-step') }}</strong> {{ finalVerdict.nextStep | inline_code }}</p>
|
|
10
12
|
</section>
|
|
11
13
|
|
|
14
|
+
{% if finalVerdict.get("conditionalAcceptanceConditions") %}
|
|
15
|
+
<section data-report-section="acceptance-conditions" data-report-field="finalVerdict.conditionalAcceptanceConditions">
|
|
16
|
+
<h2>{{ t('tasks.final-verification.conditions-for-acceptance') }}</h2>
|
|
17
|
+
<p class="section-lede">{{ t('tasks.final-verification.conditions-for-acceptance-intro') }}</p>
|
|
18
|
+
<table><thead><tr><th>{{ t('macros.layout.id') }}</th><th>{{ t('tasks.final-verification.condition') }}</th><th>{{ t('tasks.final-verification.evidence-required') }}</th></tr></thead><tbody>{% for row in finalVerdict.conditionalAcceptanceConditions %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id)], status_name=t('tasks.final-verification.blocks-release-handoff'), status_raw=('blocked' if row.blocksReleaseHandoff else 'covered'), status_text=(t('macros.layout.checked') if row.blocksReleaseHandoff else t('macros.layout.none'))) }}<td>{{ row.condition | inline_code }}</td><td>{{ row.evidenceRequired | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
19
|
+
</section>
|
|
20
|
+
{% endif %}
|
|
21
|
+
|
|
12
22
|
<section data-report-section="requirement-results" data-report-field="finalVerification.validationEvidence">
|
|
13
23
|
<h2>{{ t('tasks.final-verification.result-per-requirement') }}</h2>
|
|
14
24
|
{{ render_narrative(narrative.coverageExplanation, "finalVerification.userNarrative.coverageExplanation") }}
|
|
@@ -8,42 +8,43 @@
|
|
|
8
8
|
and the coverage table and left the mechanism, scores, votes, commitments
|
|
9
9
|
and invariants in the record only. #}
|
|
10
10
|
{% macro option_card(row, rank, criteria) -%}
|
|
11
|
+
{% set extra = cardLinks.get(row.id, {}) %}
|
|
11
12
|
<article class="summary-card" id="id-{{ row.id }}">
|
|
12
|
-
<p class="eyebrow">#{{ rank }} · {{ row.id | inline_code }} · {{ t('macros.layout.proposed-by') }} {{ row.proposedBy | inline_code }}</p>
|
|
13
|
-
<h3>{{ row.name | inline_code }}</h3>
|
|
13
|
+
<p class="eyebrow">#{{ rank }} · {{ row.id | inline_code(extra) }} · {{ t('macros.layout.proposed-by') }} {{ row.proposedBy | inline_code(extra) }}</p>
|
|
14
|
+
<h3>{{ row.name | inline_code(extra) }}</h3>
|
|
14
15
|
<div class="option-detail">
|
|
15
16
|
<h4>{{ t('tasks.implementation-option-selection.what-changes') }}</h4>
|
|
16
|
-
<p><strong>{{ t('tasks.implementation-option-selection.goal') }}</strong> {{ row.goal | inline_code }}</p>
|
|
17
|
+
<p><strong>{{ t('tasks.implementation-option-selection.goal') }}</strong> {{ row.goal | inline_code(extra) }}</p>
|
|
17
18
|
<p><strong>{{ t('tasks.implementation-option-selection.change-areas') }}</strong></p>
|
|
18
|
-
<ul>{% for area in row.expectedChangeAreas %}<li>{{ area | inline_code }}</li>{% endfor %}</ul>
|
|
19
|
-
<table data-report-field="implementationOptionSelection.rankedOptions.scopeCommitments"><thead><tr><th>{{ t('tasks.implementation-option-selection.commitment') }}</th><th>{{ t('tasks.implementation-option-selection.commitment-body') }}</th><th>{{ t('tasks.implementation-option-selection.requirements') }}</th></tr></thead><tbody>{% for item in row.scopeCommitments %}<tr>{{ row_key(pairs=[(t('macros.layout.id'), item.id), (t('macros.layout.kind'), item.kind | enum_label('commitmentKind'))]) }}<td>{{ item.commitment | inline_code }}</td><td>{{ item.requirementIds | join(", ") | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
19
|
+
<ul>{% for area in row.expectedChangeAreas %}<li>{{ area | inline_code(extra) }}</li>{% endfor %}</ul>
|
|
20
|
+
<table data-report-field="implementationOptionSelection.rankedOptions.scopeCommitments"><thead><tr><th>{{ t('tasks.implementation-option-selection.commitment') }}</th><th>{{ t('tasks.implementation-option-selection.commitment-body') }}</th><th>{{ t('tasks.implementation-option-selection.requirements') }}</th></tr></thead><tbody>{% for item in row.scopeCommitments %}<tr id="id-{{ row.id }}-{{ item.id }}">{{ row_key(pairs=[(t('macros.layout.id'), item.id), (t('macros.layout.kind'), item.kind | enum_label('commitmentKind'))]) }}<td>{{ item.commitment | inline_code(extra) }}</td><td>{{ item.requirementIds | join(", ") | inline_code(extra) }}</td></tr>{% endfor %}</tbody></table>
|
|
20
21
|
</div>
|
|
21
22
|
<div class="option-detail">
|
|
22
23
|
<h4>{{ t('tasks.implementation-option-selection.how-it-works') }}</h4>
|
|
23
|
-
<p>{{ row.coreMechanism | inline_code }}</p>
|
|
24
|
+
<p>{{ row.coreMechanism | inline_code(extra) }}</p>
|
|
24
25
|
<p><strong>{{ t('tasks.implementation-option-selection.boundaries') }}</strong></p>
|
|
25
|
-
<ul>{% for boundary in row.architectureBoundaries %}<li>{{ boundary | inline_code }}</li>{% endfor %}</ul>
|
|
26
|
+
<ul>{% for boundary in row.architectureBoundaries %}<li>{{ boundary | inline_code(extra) }}</li>{% endfor %}</ul>
|
|
26
27
|
</div>
|
|
27
28
|
<div class="option-detail">
|
|
28
29
|
<h4>{{ t('tasks.implementation-option-selection.why-this-direction') }}</h4>
|
|
29
30
|
<p><strong>{{ t('tasks.implementation-option-selection.weighted-score') }}</strong> {{ row.weightedScore }} / 5</p>
|
|
30
|
-
<table data-report-field="implementationOptionSelection.rankedOptions.criterionScores"><thead><tr><th>{{ t('tasks.implementation-option-selection.criterion') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.weight') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.score') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th></tr></thead><tbody>{% for score in row.criterionScores %}<tr><td>{{ score.criterion | enum_label('optionCriterion') }}</td><td class="figure">{{ criteria.get(score.criterion, '') }}</td><td class="figure">{{ score.score }}</td><td>{{ score.rationale | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
31
|
+
<table data-report-field="implementationOptionSelection.rankedOptions.criterionScores"><thead><tr><th>{{ t('tasks.implementation-option-selection.criterion') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.weight') }}</th><th class="figure">{{ t('tasks.implementation-option-selection.score') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th></tr></thead><tbody>{% for score in row.criterionScores %}<tr><td>{{ score.criterion | enum_label('optionCriterion') }}</td><td class="figure">{{ criteria.get(score.criterion, '') }}</td><td class="figure">{{ score.score }}</td><td>{{ score.rationale | inline_code(extra) }}</td></tr>{% endfor %}</tbody></table>
|
|
31
32
|
<p><strong>{{ t('tasks.implementation-option-selection.feasibility-votes') }}</strong></p>
|
|
32
|
-
<table data-report-field="implementationOptionSelection.rankedOptions.feasibilityVotes"><thead><tr><th>{{ t('tasks.implementation-option-selection.worker') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('tasks.implementation-option-selection.counterevidence') }}</th></tr></thead><tbody>{% for vote in row.feasibilityVotes %}<tr>{{ row_key(pairs=[(t('tasks.implementation-option-selection.worker'), vote.worker)], status_name=t('tasks.implementation-option-selection.verdict'), status_raw=vote.verdict, status_text=vote.verdict | enum_label('feasibilityVerdict')) }}<td>{{ vote.rationale | inline_code }}</td><td>{{ vote.counterevidence | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
33
|
+
<table data-report-field="implementationOptionSelection.rankedOptions.feasibilityVotes"><thead><tr><th>{{ t('tasks.implementation-option-selection.worker') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('tasks.implementation-option-selection.counterevidence') }}</th></tr></thead><tbody>{% for vote in row.feasibilityVotes %}<tr>{{ row_key(pairs=[(t('tasks.implementation-option-selection.worker'), vote.worker)], status_name=t('tasks.implementation-option-selection.verdict'), status_raw=vote.verdict, status_text=vote.verdict | enum_label('feasibilityVerdict')) }}<td>{{ vote.rationale | inline_code(extra) }}</td><td>{{ vote.counterevidence | inline_code(extra) }}</td></tr>{% endfor %}</tbody></table>
|
|
33
34
|
{% if row.safetyBlockers %}<p><strong>{{ t('tasks.implementation-option-selection.safety-blockers') }}</strong></p>
|
|
34
|
-
<ul>{% for blocker in row.safetyBlockers %}<li><span class="status status-risk">{{ blocker.category | enum_label('safetyCategory') }}</span> {{ blocker.statement | inline_code }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ blocker.evidence | evidence_refs }}</span></li>{% endfor %}</ul>{% endif %}
|
|
35
|
+
<ul>{% for blocker in row.safetyBlockers %}<li><span class="status status-risk">{{ blocker.category | enum_label('safetyCategory') }}</span> {{ blocker.statement | inline_code(extra) }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ blocker.evidence | evidence_refs(extra) }}</span></li>{% endfor %}</ul>{% endif %}
|
|
35
36
|
{% if row.unresolvedFeasibilityFacts %}<p><strong>{{ t('tasks.implementation-option-selection.unresolved-facts') }}</strong></p>
|
|
36
|
-
<ul>{% for fact in row.unresolvedFeasibilityFacts %}<li>{{ fact.fact | inline_code }} — {{ t('tasks.implementation-option-selection.why-it-matters') }} {{ fact.whyItMatters | inline_code }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ fact.evidence | evidence_refs }}</span></li>{% endfor %}</ul>{% endif %}
|
|
37
|
+
<ul>{% for fact in row.unresolvedFeasibilityFacts %}<li>{{ fact.fact | inline_code(extra) }} — {{ t('tasks.implementation-option-selection.why-it-matters') }} {{ fact.whyItMatters | inline_code(extra) }} <span class="evidence-refs">{{ t('macros.layout.evidence') }} {{ fact.evidence | evidence_refs(extra) }}</span></li>{% endfor %}</ul>{% endif %}
|
|
37
38
|
</div>
|
|
38
39
|
<div class="option-detail">
|
|
39
40
|
<h4>{{ t('tasks.implementation-option-selection.expected-outcome') }}</h4>
|
|
40
41
|
<div data-report-field="implementationOptionSelection.rankedOptions.coverageSummary"><p>{{ t('tasks.implementation-option-selection.coverage-line') | replace('{covered}', row.coverageSummary.coveredCount) | replace('{total}', row.coverageSummary.totalCount) | replace('{verdict}', row.coverageSummary.coverageVerdict | enum_label('coverageVerdict')) }}</p>
|
|
41
42
|
<p><strong>{{ t('macros.layout.coverage-percent') }}</strong> <span data-report-metric="coveragePercent">{{ row.coverageSummary.coveragePercent }}</span>% · <strong>{{ t('macros.layout.scope-precision-percent') }}</strong> <span data-report-metric="scopePrecisionPercent">{{ row.coverageSummary.scopePrecisionPercent }}</span>%</p></div>
|
|
42
|
-
<table data-report-field="implementationOptionSelection.rankedOptions.requirementCoverage"><thead><tr><th>{{ t('tasks.implementation-option-selection.requirement') }}</th><th>{{ t('tasks.implementation-option-selection.satisfaction') }}</th><th>{{ t('tasks.implementation-option-selection.verification') }}</th></tr></thead><tbody>{% for coverage in row.requirementCoverage %}<tr>{{ row_key(pairs=[(t('macros.layout.id'), coverage.requirementId), (t('macros.layout.status'), coverage.status)]) }}<td>{{ coverage.satisfaction | inline_code }}</td><td>{{ coverage.expectedVerification | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
43
|
+
<table data-report-field="implementationOptionSelection.rankedOptions.requirementCoverage"><thead><tr><th>{{ t('tasks.implementation-option-selection.requirement') }}</th><th>{{ t('tasks.implementation-option-selection.satisfaction') }}</th><th>{{ t('tasks.implementation-option-selection.verification') }}</th></tr></thead><tbody>{% for coverage in row.requirementCoverage %}<tr>{{ row_key(pairs=[(t('macros.layout.id'), coverage.requirementId), (t('macros.layout.status'), coverage.status)]) }}<td>{{ coverage.satisfaction | inline_code(extra) }}</td><td>{{ coverage.expectedVerification | inline_code(extra) }}</td></tr>{% endfor %}</tbody></table>
|
|
43
44
|
{% if row.planningInvariants %}<p><strong>{{ t('tasks.implementation-option-selection.planning-invariants') }}</strong></p>
|
|
44
|
-
<ul data-report-field="implementationOptionSelection.rankedOptions.planningInvariants">{% for invariant in row.planningInvariants %}<li>{{ invariant.id | inline_code }} · {{ invariant.statement | inline_code }} ({{ invariant.requirementIds | join(", ") | inline_code }})</li>{% endfor %}</ul>{% endif %}
|
|
45
|
+
<ul data-report-field="implementationOptionSelection.rankedOptions.planningInvariants">{% for invariant in row.planningInvariants %}<li id="id-{{ row.id }}-{{ invariant.id }}">{{ invariant.id | inline_code(extra) }} · {{ invariant.statement | inline_code(extra) }} ({{ invariant.requirementIds | join(", ") | inline_code(extra) }})</li>{% endfor %}</ul>{% endif %}
|
|
45
46
|
</div>
|
|
46
|
-
<p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.codeEvidence | evidence_refs }}</p>
|
|
47
|
+
<p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.codeEvidence | evidence_refs(extra) }}</p>
|
|
47
48
|
</article>
|
|
48
49
|
{%- endmacro %}
|
|
49
50
|
|
|
@@ -88,6 +89,6 @@
|
|
|
88
89
|
|
|
89
90
|
<section data-report-section="candidate-audit" data-report-field="implementationOptionSelection.candidateAudit">
|
|
90
91
|
<h2>{{ t('tasks.implementation-option-selection.candidate-audit') }}</h2>
|
|
91
|
-
<table><thead><tr><th>{{ t('tasks.implementation-option-selection.candidate') }}</th><th>{{ t('tasks.implementation-option-selection.goal-column') }}</th><th>{{ t('tasks.implementation-option-selection.disposition') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('macros.layout.coverage-percent') }}</th><th>{{ t('macros.layout.scope-precision-percent') }}</th></tr></thead><tbody>{% for row in selection.candidateAudit %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('macros.layout.proposed-by'), row.proposedBy)]) }}<td>{{ row.goal | inline_code }}</td><td>{{ row.disposition }}{% if row.mergedInto %} → {{ row.mergedInto | inline_code }}{% endif %}</td><td>{{ row.reason | inline_code }}</td><td>{{ row.coverageSummary.coveragePercent }}</td><td>{{ row.coverageSummary.scopePrecisionPercent }}</td></tr>{% else %}<tr><td colspan="6">{{ t('tasks.implementation-option-selection.no-audit-entries') }}</td></tr>{% endfor %}</tbody></table>
|
|
92
|
+
<table><thead><tr><th>{{ t('tasks.implementation-option-selection.candidate') }}</th><th>{{ t('tasks.implementation-option-selection.goal-column') }}</th><th>{{ t('tasks.implementation-option-selection.disposition') }}</th><th>{{ t('tasks.implementation-option-selection.reason') }}</th><th>{{ t('macros.layout.coverage-percent') }}</th><th>{{ t('macros.layout.scope-precision-percent') }}</th></tr></thead><tbody>{% for row in selection.candidateAudit %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('macros.layout.proposed-by'), row.proposedBy)]) }}<td>{{ row.goal | inline_code(cardLinks.get(row.id, {})) }}{% if row.get("scopeCommitments") %}<ul class="audit-commitments">{% for item in row.scopeCommitments %}<li id="id-{{ row.id }}-{{ item.id }}">{{ item.id | inline_code(cardLinks.get(row.id, {})) }} · {{ item.commitment | inline_code(cardLinks.get(row.id, {})) }}</li>{% endfor %}</ul>{% endif %}</td><td>{{ row.disposition }}{% if row.mergedInto %} → {{ row.mergedInto | inline_code }}{% endif %}</td><td>{{ row.reason | inline_code }}</td><td>{{ row.coverageSummary.coveragePercent }}</td><td>{{ row.coverageSummary.scopePrecisionPercent }}</td></tr>{% else %}<tr><td colspan="6">{{ t('tasks.implementation-option-selection.no-audit-entries') }}</td></tr>{% endfor %}</tbody></table>
|
|
92
93
|
</section>
|
|
93
94
|
{% endblock %}
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
<section data-report-section="direction-realization" data-report-field="implementationPlanning.directionRealization">
|
|
20
20
|
<h2>{{ t('tasks.implementation-planning.direction-realization') }}</h2>
|
|
21
21
|
<article class="summary-card"><h3>{{ planning.directionRealization.goal | inline_code }}</h3><p><strong>{{ t('tasks.implementation-planning.core-mechanism') }}</strong> {{ planning.directionRealization.coreMechanism | inline_code }}</p><p><strong>{{ t('tasks.implementation-planning.interfaces') }}</strong> {{ planning.directionRealization.interfaces | inline_code }}</p><p><strong>{{ t('tasks.implementation-planning.blast-radius') }}</strong> {{ planning.directionRealization.blastRadius | inline_code }}</p><ul>{% for file in planning.directionRealization.fileStructure %}<li id="id-{{ file.id }}">{{ file.action }} <code>{{ file.path }}</code> — {{ file.summary | inline_code }}</li>{% endfor %}</ul></article>
|
|
22
|
+
{% if planning.directionRealization.get("planningInvariants") %}
|
|
23
|
+
<h3>{{ t('tasks.implementation-planning.planning-invariants') }}</h3>
|
|
24
|
+
<ul data-report-field="implementationPlanning.directionRealization.planningInvariants">{% for row in planning.directionRealization.planningInvariants %}<li id="id-{{ row.id }}">{{ row.id | inline_code }} · {{ row.statement | inline_code }}{% if row.get("requirementIds") %} ({{ row.requirementIds | join(", ") | inline_code }}){% endif %}</li>{% endfor %}</ul>
|
|
25
|
+
{% endif %}
|
|
22
26
|
</section>
|
|
23
27
|
<section data-report-section="plan-coverage" data-report-field="implementationPlanning.coverageSummary">
|
|
24
28
|
<h2>{{ t('tasks.implementation-planning.plan-coverage') }}</h2>
|
|
@@ -90,7 +94,7 @@
|
|
|
90
94
|
<section data-report-section="requirement-coverage" data-report-field="implementationPlanning.requirementCoverage">
|
|
91
95
|
<h2>{{ t('tasks.implementation-planning.how-each-requirement-gets-met') }}</h2>
|
|
92
96
|
{% if planning.get("planningContract") == "selected-direction" %}
|
|
93
|
-
<table><thead><tr><th>{{ t('tasks.implementation-planning.original-requirement') }}</th><th>{{ t('tasks.implementation-planning.stages') }}</th><th>{{ t('tasks.implementation-planning.status') }}</th></tr></thead><tbody>{% for row in planning.requirementCoverage %}<tr><td>{{ row.originalRequirementId | inline_code }}</td><td>{{ row.stageRefs | join(", ") | inline_code }}</td><td>{{ row.status | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
97
|
+
<table><thead><tr><th>{{ t('tasks.implementation-planning.original-requirement') }}</th><th>{{ t('tasks.implementation-planning.stages') }}</th><th>{{ t('tasks.implementation-planning.status') }}</th></tr></thead><tbody>{% for row in planning.requirementCoverage %}<tr{% if row.get("id") %} id="id-{{ row.id }}"{% endif %}><td>{% if row.get("id") %}{{ row.id | inline_code }} · {% endif %}{{ row.originalRequirementId | inline_code }}</td><td>{{ row.stageRefs | join(", ") | inline_code }}</td><td>{{ row.status | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
94
98
|
{% else %}
|
|
95
99
|
<table><thead><tr><th>{{ t('tasks.implementation-planning.requirement') }}</th><th>{{ t('tasks.implementation-planning.body') }}</th><th>{{ t('tasks.implementation-planning.covered-by') }}</th></tr></thead><tbody>{% for row in planning.requirementCoverage %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[(t('macros.layout.id'), row.id), (t('macros.layout.source'), row.source)], status_name=t('macros.layout.status'), status_raw=row.status, status_text=row.status) }}<td>{{ row.requirement | inline_code }}</td><td>{{ row.coveredBy | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
96
100
|
{% endif %}
|
|
@@ -145,10 +149,6 @@
|
|
|
145
149
|
{% endif %}
|
|
146
150
|
{% endif %}
|
|
147
151
|
|
|
148
|
-
{% if agentActivities %}
|
|
149
|
-
<div hidden>{% for row in agentActivities %}<span id="id-{{ row.activityId }}"></span>{% endfor %}</div>
|
|
150
|
-
{% endif %}
|
|
151
|
-
|
|
152
152
|
<section data-report-section="open-decisions">
|
|
153
153
|
<h2>{{ t('tasks.implementation-planning.open-decisions-before-approval') }}</h2>
|
|
154
154
|
<div class="summary-grid">{% for row in openDecisions %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important", anchor=row.id) }}{% else %}<p>{{ t('tasks.implementation-planning.no-open-decision-is-holding-approval') }}</p>{% endfor %}</div>
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
{{ render_narrative(narrative.confirmedRequirements, "requirementsDiscovery.userNarrative.confirmedRequirements") }}
|
|
39
39
|
<div class="summary-grid" data-report-field="requirementsDiscovery.confirmedRequirements">{% for row in requirements.confirmedRequirements %}{{ summary_card(row.id ~ " · " ~ t('tasks.requirements-discovery.settled'), row.statement, anchor=row.id) }}{% endfor %}</div>
|
|
40
40
|
{{ render_narrative(narrative.unresolvedRequirements, "requirementsDiscovery.userNarrative.unresolvedRequirements") }}
|
|
41
|
-
<div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · " ~ t('tasks.requirements-discovery.decision-needed'), row.statement, "important") }}{% else %}<p>{{ t('tasks.requirements-discovery.nothing-is-left-unsettled') }}</p>{% endfor %}</div>
|
|
41
|
+
<div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% set clarification_ids = clarificationItems | map(attribute='id') | list %}{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · " ~ t('tasks.requirements-discovery.decision-needed'), row.statement, "important", anchor=(row.id if row.id not in clarification_ids else '')) }}{% else %}<p>{{ t('tasks.requirements-discovery.nothing-is-left-unsettled') }}</p>{% endfor %}</div>
|
|
42
42
|
</section>
|
|
43
43
|
|
|
44
44
|
<section data-report-section="routing">
|
|
@@ -14,7 +14,9 @@ second rendering of that same record:
|
|
|
14
14
|
4. each visualization's svg node ids match its fallback ids;
|
|
15
15
|
5. the Response IDs in the HTML match ``clarificationItems[]`` 1:1;
|
|
16
16
|
6. no external URL appears in ``<script src=>`` / ``<link href=>`` /
|
|
17
|
-
``<img src=>`` — the self-contained guarantee
|
|
17
|
+
``<img src=>`` — the self-contained guarantee;
|
|
18
|
+
7. every in-page ``href="#…"`` lands on an element id of the page — the
|
|
19
|
+
renderer's index and the template's anchors agree.
|
|
18
20
|
|
|
19
21
|
A schema-v1 report keeps the Markdown as its source, so its checks read the
|
|
20
22
|
§1 Clarification Items table (fail-closed when the heading is present but the
|
|
@@ -133,9 +135,36 @@ def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
|
|
|
133
135
|
)
|
|
134
136
|
if _EXTERNAL_URL_RE.search(html_text):
|
|
135
137
|
failures.append("html contains external URL in script/link/img — must be self-contained")
|
|
138
|
+
failures.extend(_dead_in_page_links(html_text))
|
|
136
139
|
return failures
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
_ANCHOR_HREF_RE = re.compile(r'href="#([^"]+)"')
|
|
143
|
+
_ELEMENT_ID_RE = re.compile(r'\bid="([^"]+)"')
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _dead_in_page_links(html_text: str) -> list[str]:
|
|
147
|
+
"""(7) every in-page link lands on an element of this page.
|
|
148
|
+
|
|
149
|
+
The renderer links every id the record defines (`report_html/common.py`
|
|
150
|
+
`anchor_index`); a link to a missing anchor means the index claimed a row
|
|
151
|
+
the template never drew — 2026-09-05 audit of 28 shipped reports found
|
|
152
|
+
`A-`, `UQ-`, `PI-` and `IC-` links dead this way. The cause is always
|
|
153
|
+
renderer-side, so the finding stays with the page, not the author.
|
|
154
|
+
"""
|
|
155
|
+
body = html_text.split("<body", 1)[-1]
|
|
156
|
+
body = re.sub(r"<script.*?</script>|<style.*?</style>", "", body, flags=re.DOTALL)
|
|
157
|
+
ids = set(_ELEMENT_ID_RE.findall(body))
|
|
158
|
+
dead = sorted({target for target in _ANCHOR_HREF_RE.findall(body) if target not in ids})
|
|
159
|
+
if not dead:
|
|
160
|
+
return []
|
|
161
|
+
return [
|
|
162
|
+
"dead in-page link(s): "
|
|
163
|
+
+ ", ".join(f"#{target}" for target in dead[:12])
|
|
164
|
+
+ (f" (+{len(dead) - 12} more)" if len(dead) > 12 else "")
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
139
168
|
def _no_form_sections(html_body: str) -> list[str]:
|
|
140
169
|
"""Return a list of strings, each being the rendered chunk of a
|
|
141
170
|
no-form section (5.6 / 5.7 / 5.8) up to the next h2/h3. Used to
|