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
|
@@ -12,12 +12,18 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoes
|
|
|
12
12
|
from ..final_report_paths import final_report_data_path, translation_sidecar_path
|
|
13
13
|
from ..final_report_schema import load_schema_for_data, validate
|
|
14
14
|
from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
|
|
15
|
-
from ..report_translation import overlay
|
|
15
|
+
from ..report_translation import overlay, rendered_english_lines
|
|
16
16
|
from ..report_view_artifacts import user_responses_dir_for_report
|
|
17
17
|
from ..usage_cells import format_duration_ms
|
|
18
18
|
from ..json_boundary import load_owned_object
|
|
19
|
-
from .common import anchor_index
|
|
20
|
-
from .context_links import
|
|
19
|
+
from .common import anchor_index, worker_finding_links
|
|
20
|
+
from .context_links import (
|
|
21
|
+
approved_plan_links,
|
|
22
|
+
brief_end_states,
|
|
23
|
+
carry_in_links,
|
|
24
|
+
end_state_table,
|
|
25
|
+
selected_direction_links,
|
|
26
|
+
)
|
|
21
27
|
from .filters import (
|
|
22
28
|
code_evidence,
|
|
23
29
|
enum_label,
|
|
@@ -136,21 +142,41 @@ def render_v2_html_view(
|
|
|
136
142
|
# report's clarifications only fill ids this page has no row for.
|
|
137
143
|
links = {
|
|
138
144
|
row_id: f"#{name}"
|
|
139
|
-
for row_id, name in anchor_index(
|
|
145
|
+
for row_id, name in anchor_index(
|
|
146
|
+
data, view.anchored_fields, view.scoped_anchor_fields
|
|
147
|
+
).items()
|
|
140
148
|
}
|
|
141
149
|
brief_rows = brief_end_states(data_path)
|
|
142
150
|
for row in brief_rows:
|
|
143
151
|
links.setdefault(row["id"], f"#id-{row['id']}")
|
|
144
152
|
links.update(carry_in_links(data, data_path, exclude=links))
|
|
153
|
+
links.update(selected_direction_links(data, data_path, exclude=links))
|
|
154
|
+
links.update(approved_plan_links(data, data_path, exclude=links))
|
|
155
|
+
# 워커 finding 번호(`F-NNN`)는 그 번호를 출처로 적은 승격 근거 행이 하나뿐일
|
|
156
|
+
# 때만 그 행으로 간다. 이 페이지가 정의한 id 가 우선이다.
|
|
157
|
+
links.update({
|
|
158
|
+
finding: href
|
|
159
|
+
for finding, href in worker_finding_links(data).items()
|
|
160
|
+
if finding not in links
|
|
161
|
+
})
|
|
145
162
|
chrome = load_dictionary(lang, HTML_DICTIONARY_REL)
|
|
146
163
|
translate = make_jinja_global(chrome)
|
|
147
164
|
env.globals["t"] = translate
|
|
148
165
|
env.filters["code_evidence"] = code_evidence
|
|
149
166
|
env.filters["enum_label"] = lambda value, vocabulary: enum_label(value, vocabulary, chrome)
|
|
150
167
|
env.filters["enum_legend"] = lambda vocabulary: enum_legend(vocabulary, chrome)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
# `extra` is a card-scoped link map (`common.scoped_anchor_map`): a
|
|
169
|
+
# direction's own commitment rows outrank the page index for the ids its
|
|
170
|
+
# prose cites, and nothing else on the page can see them.
|
|
171
|
+
env.filters["evidence_refs"] = lambda refs, extra=None: evidence_refs(
|
|
172
|
+
refs, {**links, **(extra or {})}
|
|
173
|
+
)
|
|
174
|
+
env.filters["inline_code"] = lambda value, extra=None: inline_code(
|
|
175
|
+
value, {**links, **(extra or {})}
|
|
176
|
+
)
|
|
177
|
+
env.filters["paragraphs"] = lambda value, extra=None: paragraphs(
|
|
178
|
+
value, {**links, **(extra or {})}
|
|
179
|
+
)
|
|
154
180
|
response_js = (root / "report.js").read_text(encoding="utf-8")
|
|
155
181
|
base_js = (root / "html/assets/base.js").read_text(encoding="utf-8")
|
|
156
182
|
source_data = final_report_data_path(Path(run_meta.source_report)).as_posix()
|
|
@@ -163,9 +189,15 @@ def render_v2_html_view(
|
|
|
163
189
|
"sourceData": source_data,
|
|
164
190
|
"dataSha256": _sha256(data_path),
|
|
165
191
|
"clarificationItems": data.get("clarificationItems", []),
|
|
166
|
-
# 브리프의 최종 상태
|
|
167
|
-
#
|
|
168
|
-
"
|
|
192
|
+
# 브리프의 최종 상태 행과 이 run 의 `endStateCoverage` 판정을 한 표로.
|
|
193
|
+
# 모든 phase 가 EB/PB/EO 아이디를 인용하지만 문장은 브리프에만 있고,
|
|
194
|
+
# "이 run 이 그 상태를 덮었는가" 는 coverage 표에만 있다.
|
|
195
|
+
"endStates": end_state_table(brief_rows, data),
|
|
196
|
+
# 에이전트 활동 표. 계획 리포트의 결정 카드가 `#id-A-NNN` 으로 가리키고
|
|
197
|
+
# 다른 phase 의 산문도 인용하므로 모든 타입의 페이지가 같은 절을 그린다.
|
|
198
|
+
"agentActivity": [
|
|
199
|
+
row for row in data.get("agentActivity") or [] if isinstance(row, dict)
|
|
200
|
+
],
|
|
169
201
|
# 합의·이견 근거는 태스크 본문과 같이 기본 화면에 올린다. 근거 대장
|
|
170
202
|
# 감사 모드에만 두면 판정이 사용자에게 안 보인다.
|
|
171
203
|
"crossVerification": data.get("crossVerification") or {},
|
|
@@ -178,9 +210,18 @@ def render_v2_html_view(
|
|
|
178
210
|
}
|
|
179
211
|
output_path = _html_path(data_path)
|
|
180
212
|
document = env.get_template(route.template_name).render(**context)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
213
|
+
document = inject_report_index(document, label=translate("base.contents"))
|
|
214
|
+
output_path.write_text(document, encoding="utf-8")
|
|
215
|
+
if lang != "en":
|
|
216
|
+
# 사이드카 보고("left in English")는 추출 범위 안만 센다. 페이지에 실제로
|
|
217
|
+
# 남은 영어 문장은 여기서 세어 같은 stdout 에 낸다 — 두 숫자가 갈리면
|
|
218
|
+
# `PROSE_KEYS` 밖의 필드가 화면에 나가고 있다는 뜻이다.
|
|
219
|
+
english = rendered_english_lines(document)
|
|
220
|
+
sys.stdout.write(
|
|
221
|
+
f"rendered {len(english)} line(s) still in English on the {lang} page"
|
|
222
|
+
+ (f" (e.g. {english[0][:80]!r})" if english else "")
|
|
223
|
+
+ "\n"
|
|
224
|
+
)
|
|
184
225
|
if context["clarificationItems"] or context.get("directionSelection"):
|
|
185
226
|
# The footer tells the reader to drop the exported file here, so the
|
|
186
227
|
# directory has to exist before they go looking for it.
|
|
@@ -5,6 +5,12 @@ from ..common import analysis_review_ids, evidence_index
|
|
|
5
5
|
from ..models import HumanReportView, VisualEdge, VisualNode
|
|
6
6
|
from ..visualizations import flow_figure
|
|
7
7
|
|
|
8
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
9
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
10
|
+
ANCHORED_FIELDS = (
|
|
11
|
+
"changeImpactAnalysis.impactItems",
|
|
12
|
+
)
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
def _impact_figure(change: dict):
|
|
10
16
|
nodes = [
|
|
@@ -39,11 +45,12 @@ def build_change_impact_analysis_view(data: dict) -> HumanReportView:
|
|
|
39
45
|
"narrative": change["userNarrative"],
|
|
40
46
|
"blastRadiusFigure": figure,
|
|
41
47
|
"analysisReviewIds": analysis_review_ids(data),
|
|
42
|
-
"evidenceIndex": evidence_index(data),
|
|
48
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
43
49
|
}
|
|
44
50
|
return HumanReportView(
|
|
45
51
|
task_type="change-impact-analysis",
|
|
46
52
|
template_name="html/tasks/change-impact-analysis.template.html",
|
|
47
53
|
context=context,
|
|
48
54
|
figures=(figure,),
|
|
55
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
49
56
|
)
|
|
@@ -7,6 +7,12 @@ from ..common import evidence_index
|
|
|
7
7
|
from ..models import HumanReportView, VisualEdge, VisualNode
|
|
8
8
|
from ..visualizations import cause_graph_figure
|
|
9
9
|
|
|
10
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
11
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
12
|
+
ANCHORED_FIELDS = (
|
|
13
|
+
"errorAnalysis.causeCandidates",
|
|
14
|
+
)
|
|
15
|
+
|
|
10
16
|
|
|
11
17
|
_LEAD_CLAUSE = re.compile(r"\s[—–-]\s|(?<=[.。!?])\s")
|
|
12
18
|
_MAX_LABEL_CHARS = 44
|
|
@@ -92,11 +98,12 @@ def build_error_analysis_view(data: dict) -> HumanReportView:
|
|
|
92
98
|
"error": error,
|
|
93
99
|
"narrative": error["userNarrative"],
|
|
94
100
|
"causeFigure": figure,
|
|
95
|
-
"evidenceIndex": evidence_index(data),
|
|
101
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
96
102
|
}
|
|
97
103
|
return HumanReportView(
|
|
98
104
|
"error-analysis",
|
|
99
105
|
"html/tasks/error-analysis.template.html",
|
|
100
106
|
context,
|
|
101
107
|
(figure,),
|
|
108
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
102
109
|
)
|
|
@@ -5,6 +5,15 @@ from ..common import analysis_review_ids, evidence_index
|
|
|
5
5
|
from ..models import HumanReportView, VisualNode
|
|
6
6
|
from ..visualizations import flow_figure
|
|
7
7
|
|
|
8
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
9
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
10
|
+
ANCHORED_FIELDS = (
|
|
11
|
+
"featureAnalysis.flows",
|
|
12
|
+
"featureAnalysis.domainRules",
|
|
13
|
+
"featureAnalysis.stateChanges",
|
|
14
|
+
"featureAnalysis.testCoverage",
|
|
15
|
+
)
|
|
16
|
+
|
|
8
17
|
|
|
9
18
|
_FLOW_KINDS = {"normal": "Normal path", "alternative": "Alternative path", "failure": "Failure path"}
|
|
10
19
|
|
|
@@ -31,11 +40,12 @@ def build_feature_analysis_view(data: dict) -> HumanReportView:
|
|
|
31
40
|
"narrative": feature["userNarrative"],
|
|
32
41
|
"flowFigure": figure,
|
|
33
42
|
"analysisReviewIds": analysis_review_ids(data),
|
|
34
|
-
"evidenceIndex": evidence_index(data),
|
|
43
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
35
44
|
}
|
|
36
45
|
return HumanReportView(
|
|
37
46
|
task_type="feature-analysis",
|
|
38
47
|
template_name="html/tasks/feature-analysis.template.html",
|
|
39
48
|
context=context,
|
|
40
49
|
figures=(figure,),
|
|
50
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
41
51
|
)
|
|
@@ -6,6 +6,16 @@ from ..common import evidence_index
|
|
|
6
6
|
from ..models import HumanReportView, VisualNode
|
|
7
7
|
from ..visualizations import coverage_figure
|
|
8
8
|
|
|
9
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
10
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
11
|
+
ANCHORED_FIELDS = (
|
|
12
|
+
"finalVerification.validationEvidence",
|
|
13
|
+
"finalVerification.addedSurfaceAudit",
|
|
14
|
+
"finalVerification.acceptanceBlockers",
|
|
15
|
+
"finalVerification.residualRisk",
|
|
16
|
+
"finalVerdict.conditionalAcceptanceConditions",
|
|
17
|
+
)
|
|
18
|
+
|
|
9
19
|
|
|
10
20
|
def _coverage_nodes(final: dict) -> tuple[VisualNode, ...]:
|
|
11
21
|
return tuple(
|
|
@@ -31,15 +41,17 @@ def build_final_verification_view(data: dict) -> HumanReportView:
|
|
|
31
41
|
"humanSummary": data["humanSummary"],
|
|
32
42
|
"verdict": data["verdictCard"],
|
|
33
43
|
"verdictToken": verdict_token,
|
|
44
|
+
"finalVerdict": data["finalVerdict"],
|
|
34
45
|
"final": final,
|
|
35
46
|
"narrative": final["userNarrative"],
|
|
36
47
|
"coverageFigure": figure,
|
|
37
48
|
"releaseAllowed": release_handoff_allowed(data),
|
|
38
|
-
"evidenceIndex": evidence_index(data),
|
|
49
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
39
50
|
}
|
|
40
51
|
return HumanReportView(
|
|
41
52
|
"final-verification",
|
|
42
53
|
"html/tasks/final-verification.template.html",
|
|
43
54
|
context,
|
|
44
55
|
(figure,),
|
|
56
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
45
57
|
)
|
|
@@ -5,6 +5,13 @@ from ..common import evidence_index
|
|
|
5
5
|
from ..models import HumanReportView, VisualNode
|
|
6
6
|
from ..visualizations import change_map_figure
|
|
7
7
|
|
|
8
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
9
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
10
|
+
ANCHORED_FIELDS = (
|
|
11
|
+
"implementation.requirementCoverage",
|
|
12
|
+
"implementation.outOfPlanEdits",
|
|
13
|
+
)
|
|
14
|
+
|
|
8
15
|
|
|
9
16
|
_FILE_ACTIONS = {"created": "Created", "modified": "Modified", "deleted": "Deleted"}
|
|
10
17
|
|
|
@@ -33,11 +40,12 @@ def build_implementation_view(data: dict) -> HumanReportView:
|
|
|
33
40
|
"implementation": implementation,
|
|
34
41
|
"narrative": implementation["userNarrative"],
|
|
35
42
|
"changeFigure": figure,
|
|
36
|
-
"evidenceIndex": evidence_index(data),
|
|
43
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
37
44
|
}
|
|
38
45
|
return HumanReportView(
|
|
39
46
|
"implementation",
|
|
40
47
|
"html/tasks/implementation.template.html",
|
|
41
48
|
context,
|
|
42
49
|
(figure,),
|
|
50
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
43
51
|
)
|
package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
"""Human-first implementation-direction comparison view."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from ..common import evidence_index
|
|
4
|
+
from ..common import evidence_index, scoped_anchor_map
|
|
5
5
|
from ..models import HumanReportView
|
|
6
6
|
|
|
7
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
8
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
9
|
+
ANCHORED_FIELDS = (
|
|
10
|
+
"implementationOptionSelection.rankedOptions",
|
|
11
|
+
"implementationOptionSelection.candidateAudit",
|
|
12
|
+
)
|
|
13
|
+
SCOPED_ANCHOR_FIELDS = (
|
|
14
|
+
"implementationOptionSelection.rankedOptions.scopeCommitments",
|
|
15
|
+
"implementationOptionSelection.rankedOptions.planningInvariants",
|
|
16
|
+
"implementationOptionSelection.candidateAudit.scopeCommitments",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _card_links(data: dict) -> dict[str, dict[str, str]]:
|
|
21
|
+
links: dict[str, dict[str, str]] = {}
|
|
22
|
+
for path in SCOPED_ANCHOR_FIELDS:
|
|
23
|
+
for parent_id, children in scoped_anchor_map(data, path).items():
|
|
24
|
+
links.setdefault(parent_id, {}).update(
|
|
25
|
+
{child_id: f"#{anchor}" for child_id, anchor in children.items()}
|
|
26
|
+
)
|
|
27
|
+
return links
|
|
28
|
+
|
|
7
29
|
|
|
8
30
|
def build_implementation_option_selection_view(data: dict) -> HumanReportView:
|
|
9
31
|
selection = data["implementationOptionSelection"]
|
|
@@ -29,11 +51,16 @@ def build_implementation_option_selection_view(data: dict) -> HumanReportView:
|
|
|
29
51
|
),
|
|
30
52
|
None,
|
|
31
53
|
),
|
|
32
|
-
"evidenceIndex": evidence_index(data),
|
|
54
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS, SCOPED_ANCHOR_FIELDS),
|
|
55
|
+
# 카드별 링크 맵. 방향마다 `IC-001` 이 다시 시작하므로 그 카드 안의
|
|
56
|
+
# 인용은 그 카드의 행으로만 간다.
|
|
57
|
+
"cardLinks": _card_links(data),
|
|
33
58
|
}
|
|
34
59
|
return HumanReportView(
|
|
35
60
|
task_type="implementation-option-selection",
|
|
36
61
|
template_name="html/tasks/implementation-option-selection.template.html",
|
|
37
62
|
context=context,
|
|
38
63
|
figures=(),
|
|
64
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
65
|
+
scoped_anchor_fields=SCOPED_ANCHOR_FIELDS,
|
|
39
66
|
)
|
|
@@ -89,13 +89,18 @@ def _stage_figure(planning: dict):
|
|
|
89
89
|
return stage_map_figure(nodes=nodes, edges=edges, title="Implementation stage dependencies")
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
"
|
|
92
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
93
|
+
# `HumanReportView.anchored_fields`). The implementer-facing tables the
|
|
94
|
+
# approver does not read — validation checklist, cross-project dependencies,
|
|
95
|
+
# migration risk, rollback — are not drawn, so their rows land in the ledger.
|
|
96
|
+
ANCHORED_FIELDS = (
|
|
97
|
+
"implementationPlanning.directionRealization.fileStructure",
|
|
98
|
+
"implementationPlanning.directionRealization.planningInvariants",
|
|
99
|
+
"implementationPlanning.optionCandidates.fileStructure",
|
|
100
|
+
"implementationPlanning.designPreparation.items",
|
|
101
|
+
"implementationPlanning.requirementCoverage",
|
|
102
|
+
"implementationPlanning.planBodyVerification.planItems",
|
|
103
|
+
"implementationPlanning.supersessionLedger",
|
|
99
104
|
)
|
|
100
105
|
|
|
101
106
|
|
|
@@ -131,12 +136,12 @@ def build_implementation_planning_view(data: dict) -> HumanReportView:
|
|
|
131
136
|
row["activityId"]: row for row in activities if row.get("activityId")
|
|
132
137
|
},
|
|
133
138
|
"decisionCards": decision_cards,
|
|
134
|
-
"evidenceIndex": evidence_index(data,
|
|
139
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
135
140
|
}
|
|
136
141
|
return HumanReportView(
|
|
137
142
|
"implementation-planning",
|
|
138
143
|
"html/tasks/implementation-planning.template.html",
|
|
139
144
|
context,
|
|
140
145
|
(figure,) if figure is not None else (),
|
|
141
|
-
|
|
146
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
142
147
|
)
|
|
@@ -5,6 +5,12 @@ from ..common import evidence_index
|
|
|
5
5
|
from ..models import HumanReportView, VisualNode
|
|
6
6
|
from ..visualizations import matrix_figure
|
|
7
7
|
|
|
8
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
9
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
10
|
+
ANCHORED_FIELDS = (
|
|
11
|
+
"improvementDiscovery.candidates",
|
|
12
|
+
)
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
def _candidate_nodes(improvement: dict) -> tuple[VisualNode, ...]:
|
|
10
16
|
return tuple(
|
|
@@ -32,11 +38,12 @@ def build_improvement_discovery_view(data: dict) -> HumanReportView:
|
|
|
32
38
|
"improvement": improvement,
|
|
33
39
|
"narrative": improvement["userNarrative"],
|
|
34
40
|
"matrixFigure": figure,
|
|
35
|
-
"evidenceIndex": evidence_index(data),
|
|
41
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
36
42
|
}
|
|
37
43
|
return HumanReportView(
|
|
38
44
|
"improvement-discovery",
|
|
39
45
|
"html/tasks/improvement-discovery.template.html",
|
|
40
46
|
context,
|
|
41
47
|
(figure,),
|
|
48
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
42
49
|
)
|
|
@@ -5,6 +5,19 @@ from ..common import evidence_index
|
|
|
5
5
|
from ..models import HumanReportView, VisualEdge, VisualNode
|
|
6
6
|
from ..visualizations import dependency_figure, infrastructure_figure, workflow_figure
|
|
7
7
|
|
|
8
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
9
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger. The
|
|
10
|
+
# architecture figure anchors component nodes; the infrastructure figure's
|
|
11
|
+
# store and external nodes carry synthetic ids and anchor nothing.
|
|
12
|
+
ANCHORED_FIELDS = (
|
|
13
|
+
"projectAnalysis.components",
|
|
14
|
+
"projectAnalysis.internalInterfaces",
|
|
15
|
+
"projectAnalysis.workflows",
|
|
16
|
+
"projectAnalysis.rankedHotspots",
|
|
17
|
+
"projectAnalysis.qualityCoverage",
|
|
18
|
+
"projectAnalysis.featureIndex",
|
|
19
|
+
)
|
|
20
|
+
|
|
8
21
|
|
|
9
22
|
def _component_nodes(project: dict) -> tuple[VisualNode, ...]:
|
|
10
23
|
return tuple(
|
|
@@ -130,11 +143,12 @@ def build_project_analysis_view(data: dict) -> HumanReportView:
|
|
|
130
143
|
"workflowFigure": workflows,
|
|
131
144
|
"narrative": project["userNarrative"],
|
|
132
145
|
"architectureFigure": architecture,
|
|
133
|
-
"evidenceIndex": evidence_index(data),
|
|
146
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
134
147
|
}
|
|
135
148
|
return HumanReportView(
|
|
136
149
|
task_type="project-analysis",
|
|
137
150
|
template_name="html/tasks/project-analysis.template.html",
|
|
138
151
|
context=context,
|
|
139
152
|
figures=tuple(f for f in (architecture, infrastructure, workflows) if f),
|
|
153
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
140
154
|
)
|
|
@@ -7,6 +7,11 @@ from ..common import evidence_index
|
|
|
7
7
|
from ..models import HumanReportView, VisualNode
|
|
8
8
|
from ..visualizations import timeline_figure
|
|
9
9
|
|
|
10
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
11
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
12
|
+
ANCHORED_FIELDS = (
|
|
13
|
+
)
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
def _safe_http_url(value: str) -> str:
|
|
12
17
|
parsed = urlparse(value)
|
|
@@ -37,11 +42,12 @@ def build_release_handoff_view(data: dict) -> HumanReportView:
|
|
|
37
42
|
"narrative": handoff["userNarrative"],
|
|
38
43
|
"handoffFigure": figure,
|
|
39
44
|
"prUrl": _safe_http_url(handoff["pullRequestOutcome"].get("url", "")),
|
|
40
|
-
"evidenceIndex": evidence_index(data),
|
|
45
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
41
46
|
}
|
|
42
47
|
return HumanReportView(
|
|
43
48
|
"release-handoff",
|
|
44
49
|
"html/tasks/release-handoff.template.html",
|
|
45
50
|
context,
|
|
46
51
|
(figure,),
|
|
52
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
47
53
|
)
|
|
@@ -5,6 +5,13 @@ from ..common import evidence_index
|
|
|
5
5
|
from ..models import HumanReportView, VisualEdge, VisualNode
|
|
6
6
|
from ..visualizations import decision_flow_figure
|
|
7
7
|
|
|
8
|
+
# Record fields this template anchors as `id-<row id>` (see
|
|
9
|
+
# `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
|
|
10
|
+
ANCHORED_FIELDS = (
|
|
11
|
+
"requirementsDiscovery.confirmedRequirements",
|
|
12
|
+
"requirementsDiscovery.unresolvedRequirements",
|
|
13
|
+
)
|
|
14
|
+
|
|
8
15
|
|
|
9
16
|
def _routing_figure(data: dict, requirements: dict):
|
|
10
17
|
current = data["header"]["taskKey"]
|
|
@@ -47,11 +54,12 @@ def build_requirements_discovery_view(data: dict) -> HumanReportView:
|
|
|
47
54
|
"narrative": requirements["userNarrative"],
|
|
48
55
|
"routingFigure": figure,
|
|
49
56
|
"clarificationItems": data.get("clarificationItems", []),
|
|
50
|
-
"evidenceIndex": evidence_index(data),
|
|
57
|
+
"evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
|
|
51
58
|
}
|
|
52
59
|
return HumanReportView(
|
|
53
60
|
"requirements-discovery",
|
|
54
61
|
"html/tasks/requirements-discovery.template.html",
|
|
55
62
|
context,
|
|
56
63
|
(figure,),
|
|
64
|
+
anchored_fields=ANCHORED_FIELDS,
|
|
57
65
|
)
|
|
@@ -24,6 +24,8 @@ from typing import Any, Iterator, Mapping, NamedTuple
|
|
|
24
24
|
# Values the renderer reads as text and nothing else.
|
|
25
25
|
PROSE_KEYS = frozenset({
|
|
26
26
|
"acceptance",
|
|
27
|
+
"action",
|
|
28
|
+
"actions",
|
|
27
29
|
"addedWork",
|
|
28
30
|
"alternativesConsidered",
|
|
29
31
|
"answer",
|
|
@@ -31,18 +33,23 @@ PROSE_KEYS = frozenset({
|
|
|
31
33
|
"approvalDisposition",
|
|
32
34
|
"approvalEvidence",
|
|
33
35
|
"architectureBoundaries",
|
|
36
|
+
"assumptions",
|
|
34
37
|
"behavior",
|
|
35
38
|
"blastRadius",
|
|
36
39
|
"blockReason",
|
|
37
40
|
"blockedReason",
|
|
41
|
+
"blockers",
|
|
38
42
|
"body",
|
|
43
|
+
"boundary",
|
|
39
44
|
"briefEvidence",
|
|
45
|
+
"canonical",
|
|
40
46
|
"carryIn",
|
|
41
47
|
"causeSummary",
|
|
42
48
|
"change",
|
|
43
49
|
"check",
|
|
44
50
|
"claim",
|
|
45
51
|
"commitment",
|
|
52
|
+
"complexity",
|
|
46
53
|
"condition",
|
|
47
54
|
"confirmingSignal",
|
|
48
55
|
"conformanceExemption",
|
|
@@ -53,6 +60,7 @@ PROSE_KEYS = frozenset({
|
|
|
53
60
|
"coreReason",
|
|
54
61
|
"counterevidence",
|
|
55
62
|
"decision",
|
|
63
|
+
"decisions",
|
|
56
64
|
"declinedFixRecommendations",
|
|
57
65
|
"description",
|
|
58
66
|
"details",
|
|
@@ -75,15 +83,22 @@ PROSE_KEYS = frozenset({
|
|
|
75
83
|
"expectedOutcome",
|
|
76
84
|
"expectedResult",
|
|
77
85
|
"expectedVerification",
|
|
86
|
+
"explanation",
|
|
78
87
|
"fact",
|
|
79
88
|
"failureHandling",
|
|
89
|
+
"falsifyingEvidenceChecked",
|
|
80
90
|
"finalConclusion",
|
|
81
91
|
"goal",
|
|
92
|
+
"guardrails",
|
|
82
93
|
"headline",
|
|
83
94
|
"howToStart",
|
|
84
95
|
"hypothesis",
|
|
96
|
+
"ifStillOpen",
|
|
97
|
+
"injectedAs",
|
|
98
|
+
"interfaces",
|
|
85
99
|
"item",
|
|
86
100
|
"justification",
|
|
101
|
+
"knownFacts",
|
|
87
102
|
"label",
|
|
88
103
|
"mitigation",
|
|
89
104
|
"motivation",
|
|
@@ -108,15 +123,23 @@ PROSE_KEYS = frozenset({
|
|
|
108
123
|
"reason",
|
|
109
124
|
"rejectedSummary",
|
|
110
125
|
"rejectingSignal",
|
|
126
|
+
"relation",
|
|
127
|
+
"replacedInTest",
|
|
128
|
+
"replacedWith",
|
|
129
|
+
"requestKind",
|
|
111
130
|
"requiredDecision",
|
|
112
131
|
"requiredWork",
|
|
113
132
|
"requirement",
|
|
114
133
|
"resolution",
|
|
115
134
|
"responsibility",
|
|
135
|
+
"reversibility",
|
|
136
|
+
"risk",
|
|
116
137
|
"rolloutCost",
|
|
117
138
|
"routingRecommendation",
|
|
118
|
-
"selfFixNote",
|
|
119
139
|
"satisfaction",
|
|
140
|
+
"scope",
|
|
141
|
+
"selfFixNote",
|
|
142
|
+
"sliceValue",
|
|
120
143
|
"stageTitle",
|
|
121
144
|
"stageValidation",
|
|
122
145
|
"statement",
|
|
@@ -136,6 +159,7 @@ PROSE_KEYS = frozenset({
|
|
|
136
159
|
"triggerSignal",
|
|
137
160
|
"unblockCondition",
|
|
138
161
|
"userChoice",
|
|
162
|
+
"userConstraints",
|
|
139
163
|
"userInput",
|
|
140
164
|
"userText",
|
|
141
165
|
"verification",
|
|
@@ -426,6 +450,39 @@ def overlay(
|
|
|
426
450
|
HANGUL_PROSE_LIMIT = 0.20
|
|
427
451
|
|
|
428
452
|
|
|
453
|
+
_RENDERED_SKIP_RE = re.compile(
|
|
454
|
+
r"<script.*?</script>|<style.*?</style>|<svg.*?</svg>|<pre.*?</pre>|<option.*?</option>",
|
|
455
|
+
re.DOTALL,
|
|
456
|
+
)
|
|
457
|
+
_TEXT_NODE_RE = re.compile(r">([^<>]{40,})<")
|
|
458
|
+
_ENTITY_RE = re.compile(r"&[#A-Za-z0-9]+;")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def rendered_english_lines(html: str, *, min_words: int = 6) -> list[str]:
|
|
462
|
+
"""Text nodes of a rendered page that carry no Hangul and read as a sentence.
|
|
463
|
+
|
|
464
|
+
`OverlayReport.untranslated` only sees the pointers the extractor offered,
|
|
465
|
+
so a reader-facing field outside `PROSE_KEYS` never shows up there — the
|
|
466
|
+
2026-09-05 audit found 40 plan-item explanations and 29 step actions in
|
|
467
|
+
English on a page that reported "0 left in English". This reads the page
|
|
468
|
+
itself. Scripts, styles, figures, code blocks and `<option>` values are
|
|
469
|
+
skipped: figure labels and response values are meant to stay verbatim.
|
|
470
|
+
Inline `<code>` is stripped so a sentence around a path still counts.
|
|
471
|
+
"""
|
|
472
|
+
body = html.split("<main", 1)[-1]
|
|
473
|
+
body = _RENDERED_SKIP_RE.sub(" ", body)
|
|
474
|
+
body = re.sub(r"</?code>", "", body)
|
|
475
|
+
lines: list[str] = []
|
|
476
|
+
for match in _TEXT_NODE_RE.finditer(body):
|
|
477
|
+
text = _ENTITY_RE.sub(" ", match.group(1)).strip()
|
|
478
|
+
if len(text.split()) < min_words:
|
|
479
|
+
continue
|
|
480
|
+
if any("가" <= char <= "힣" for char in text):
|
|
481
|
+
continue
|
|
482
|
+
lines.append(text)
|
|
483
|
+
return lines
|
|
484
|
+
|
|
485
|
+
|
|
429
486
|
def hangul_share(data: Mapping[str, Any]) -> tuple[float, int]:
|
|
430
487
|
"""Return the Hangul share of the report's authored prose, and its length."""
|
|
431
488
|
text = "".join(extract(data).values())
|
|
@@ -4742,6 +4742,10 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4742
4742
|
relative_to_project_root(Path(inp.selected_direction_path), project_root)
|
|
4743
4743
|
if inp.selected_direction_path else ""
|
|
4744
4744
|
)
|
|
4745
|
+
approved_plan_relative = (
|
|
4746
|
+
relative_to_project_root(Path(inp.approved_plan_path), project_root)
|
|
4747
|
+
if inp.approved_plan_path else ""
|
|
4748
|
+
)
|
|
4745
4749
|
|
|
4746
4750
|
forbidden_by_phase = load_phase_forbidden(workspace_root)
|
|
4747
4751
|
|
|
@@ -4761,6 +4765,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
4761
4765
|
"CLARIFICATION_RESPONSE_RELATIVE_PATH": clarification_relative,
|
|
4762
4766
|
"SELECTED_DIRECTION_PATH": inp.selected_direction_path,
|
|
4763
4767
|
"SELECTED_DIRECTION_RELATIVE_PATH": selected_direction_relative,
|
|
4768
|
+
"APPROVED_PLAN_PATH": inp.approved_plan_path,
|
|
4769
|
+
"APPROVED_PLAN_RELATIVE_PATH": approved_plan_relative,
|
|
4764
4770
|
**_reverify_scope_ctx(inp.reverify_scope),
|
|
4765
4771
|
"BRIEF_FILE_PATH": str(inp.brief_path),
|
|
4766
4772
|
"BRIEF_RELATIVE_PATH": brief_relative,
|