okstra 0.148.1 → 0.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/runtime/BUILD.json +2 -2
  3. package/runtime/bin/okstra-render-report-views.py +20 -0
  4. package/runtime/prompts/profiles/project-analysis.md +18 -0
  5. package/runtime/prompts/profiles/release-handoff.md +3 -0
  6. package/runtime/prompts/profiles/requirements-discovery.md +7 -0
  7. package/runtime/python/okstra_ctl/report_html/common.py +77 -47
  8. package/runtime/python/okstra_ctl/report_html/filters.py +125 -30
  9. package/runtime/python/okstra_ctl/report_html/models.py +15 -0
  10. package/runtime/python/okstra_ctl/report_html/render.py +44 -5
  11. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +14 -4
  12. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +3 -3
  13. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +5 -3
  14. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -7
  15. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +5 -9
  16. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +2 -5
  17. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +1 -2
  18. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +90 -4
  19. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +1 -8
  20. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +6 -4
  21. package/runtime/python/okstra_ctl/report_html/visualizations.py +146 -11
  22. package/runtime/python/okstra_ctl/report_view_artifacts.py +5 -0
  23. package/runtime/python/okstra_ctl/time_report.py +2 -2
  24. package/runtime/python/okstra_ctl/usage_report.py +2 -2
  25. package/runtime/schemas/final-report-v2.0.schema.json +229 -1
  26. package/runtime/templates/reports/final-report.template.md +55 -0
  27. package/runtime/templates/reports/html/assets/base.css +59 -9
  28. package/runtime/templates/reports/html/base.template.html +21 -42
  29. package/runtime/templates/reports/html/macros/forms.html +20 -20
  30. package/runtime/templates/reports/html/macros/layout.html +18 -5
  31. package/runtime/templates/reports/html/macros/visualizations.html +7 -5
  32. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +30 -15
  33. package/runtime/templates/reports/html/tasks/error-analysis.template.html +22 -15
  34. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +35 -15
  35. package/runtime/templates/reports/html/tasks/final-verification.template.html +21 -14
  36. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +78 -19
  37. package/runtime/templates/reports/html/tasks/implementation.template.html +34 -16
  38. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +11 -11
  39. package/runtime/templates/reports/html/tasks/project-analysis.template.html +65 -25
  40. package/runtime/templates/reports/html/tasks/release-handoff.template.html +28 -14
  41. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +35 -15
  42. package/runtime/validators/validate_analysis_report.py +36 -0
@@ -1,29 +1,29 @@
1
1
  {% extends "html/base.template.html" %}
2
- {% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, summary_card, row_key %}
3
3
  {% from "html/macros/visualizations.html" import figure %}
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="opportunities">
7
- <h2>발견한 개선 기회</h2>
8
- {{ render_narrative(narrative.opportunityOverview, "improvementDiscovery.userNarrative.opportunityOverview", evidenceIndex) }}
9
- <table data-report-field="improvementDiscovery.lensCoverage"><thead><tr><th>렌즈</th><th>결과</th><th>설명</th><th>근거</th></tr></thead><tbody>{% for row in improvement.lensCoverage %}<tr><td>{{ row.lens }}</td><td>{{ row.status }}</td><td>{{ row.rationale | inline_code }}</td><td>{{ row.evidence | evidence_refs(evidenceIndex) }}</td></tr>{% endfor %}</tbody></table>
7
+ <h2>Improvements found</h2>
8
+ {{ render_narrative(narrative.opportunityOverview, "improvementDiscovery.userNarrative.opportunityOverview") }}
9
+ <table data-report-field="improvementDiscovery.lensCoverage"><thead><tr><th>Lens</th><th>Detail</th><th>Evidence</th></tr></thead><tbody>{% for row in improvement.lensCoverage %}<tr>{{ row_key(pairs=[("Lens", row.lens), ("Result", row.status | enum_label("lens"))]) }}<td>{{ row.rationale | inline_code }}</td><td>{{ row.evidence | evidence_refs }}</td></tr>{% endfor %}</tbody></table>
10
10
  </section>
11
11
 
12
12
  <section data-report-section="ranked-candidates">
13
- <h2>우선순위 후보</h2>
14
- {{ render_narrative(narrative.prioritizationExplanation, "improvementDiscovery.userNarrative.prioritizationExplanation", evidenceIndex) }}
15
- <div class="summary-grid" data-report-field="improvementDiscovery.candidates">{% for row in improvement.candidates %}<article class="summary-card tone-{{ row.severity }}"><p class="eyebrow">{{ loop.index }}순위 · {{ row.id }} · 영향 {{ row.severity }} · 노력 {{ row.effort }}</p><h3>{{ row.title | inline_code }}</h3><p>{{ row.expectedBehaviorAfter | inline_code }}</p><p><strong>다음 단계:</strong> {{ row.recommendedNextPhase }}</p><p class="evidence-refs">근거: {{ row.evidence | evidence_refs(evidenceIndex) }}</p></article>{% else %}<p>선택할 개선 후보가 없습니다.</p>{% endfor %}</div>
13
+ <h2>Ranked candidates</h2>
14
+ {{ render_narrative(narrative.prioritizationExplanation, "improvementDiscovery.userNarrative.prioritizationExplanation") }}
15
+ <div class="summary-grid" data-report-field="improvementDiscovery.candidates">{% for row in improvement.candidates %}<article class="summary-card tone-{{ row.severity }}" id="id-{{ row.id }}"><p class="eyebrow">#{{ loop.index }} · {{ row.id }} · Impact {{ row.severity }} · Effort {{ row.effort | enum_label("effort") }}</p><h3>{{ row.title | inline_code }}</h3><p>{{ row.expectedBehaviorAfter | inline_code }}</p><p><strong>Next step:</strong> {{ row.recommendedNextPhase }}</p><p class="evidence-refs">Evidence: {{ row.evidence | evidence_refs }}</p></article>{% else %}<p>There is no improvement candidate to pick.</p>{% endfor %}</div>
16
16
  </section>
17
17
 
18
18
  <section data-report-section="impact-effort">
19
- <h2>영향도와 노력도</h2>
19
+ <h2>Impact and effort</h2>
20
20
  {{ figure(matrixFigure) }}
21
21
  </section>
22
22
 
23
23
  <section data-report-section="selection">
24
- <h2>선택 안내</h2>
25
- {{ render_narrative(narrative.selectionGuidance, "improvementDiscovery.userNarrative.selectionGuidance", evidenceIndex) }}
26
- <p><strong>선택 상한:</strong> 최대 {{ improvement.selectionLimit }}개</p>
24
+ <h2>How to choose</h2>
25
+ {{ render_narrative(narrative.selectionGuidance, "improvementDiscovery.userNarrative.selectionGuidance") }}
26
+ <p><strong>Selection cap:</strong> at most {{ improvement.selectionLimit }}</p>
27
27
  <ul>{% for row in improvement.candidates %}<li><label><input type="checkbox" name="improvement-candidate" value="{{ row.id }}"> {{ row.id }} · {{ row.title | inline_code }}</label></li>{% endfor %}</ul>
28
28
  </section>
29
29
  {% endblock %}
@@ -1,57 +1,97 @@
1
1
  {% extends "html/base.template.html" %}
2
- {% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card, row_key %}
3
3
  {% from "html/macros/visualizations.html" import figure %}
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="project-overview">
7
- <h2>프로젝트 개요</h2>
8
- {{ render_narrative(narrative.projectOverview, "projectAnalysis.userNarrative.projectOverview", evidenceIndex) }}
7
+ <h2>Project overview</h2>
8
+ {{ render_narrative(narrative.projectOverview, "projectAnalysis.userNarrative.projectOverview") }}
9
9
  <div class="summary-grid">
10
- {% for reason in humanSummary.whyItMatters %}{{ summary_card("왜 중요한가 " ~ loop.index, reason, "important") }}{% endfor %}
10
+ {% for reason in humanSummary.whyItMatters %}{{ summary_card("", reason, "important") }}{% endfor %}
11
11
  </div>
12
12
  </section>
13
13
 
14
14
  <section data-report-section="architecture" data-report-field="projectAnalysis.components">
15
- <h2>아키텍처와 의존성</h2>
16
- {{ render_narrative(narrative.architectureExplanation, "projectAnalysis.userNarrative.architectureExplanation", evidenceIndex) }}
17
- {{ figure(architectureFigure) }}
15
+ <h2>Architecture and dependencies</h2>
16
+ {{ render_narrative(narrative.architectureExplanation, "projectAnalysis.userNarrative.architectureExplanation") }}
17
+ {{ figure(architectureFigure, anchor_nodes=true) }}
18
18
  </section>
19
19
 
20
+ <section data-report-section="infrastructure">
21
+ <h2>Infrastructure</h2>
22
+ {{ figure(infrastructureFigure) }}
23
+ </section>
24
+
25
+ {% if project.get("techStack") %}
26
+ <section data-report-section="tech-stack" data-report-field="projectAnalysis.techStack">
27
+ <h2>Tech stack</h2>
28
+ <h3>Language</h3>
29
+ <table><thead><tr><th>Language</th><th>Version</th><th>Version source</th><th>Files</th><th>Lines</th></tr></thead><tbody>{% for row in project.techStack.languages %}<tr><td>{{ row.name | inline_code }}</td><td><code>{{ row.version }}</code></td><td>{{ row.versionSource | enum_label("versionSource") }}</td><td>{{ row.get("fileCount") | default("--") }}</td><td>{{ row.get("lineCount") | default("--") }}</td></tr>{% endfor %}</tbody></table>
30
+ {% if project.techStack.get("frameworks") %}<h3>Framework</h3>
31
+ <table><thead><tr><th>Framework</th><th>Role</th><th>Evidence</th></tr></thead><tbody>{% for row in project.techStack.frameworks %}<tr>{{ row_key(pairs=[("Name", row.name), ("Version", row.version), ("Version source", row.versionSource | enum_label("versionSource"))]) }}<td>{{ row.role | inline_code }}</td><td>{{ row.get("currentCodeEvidence") | default([]) | code_evidence }}</td></tr>{% endfor %}</tbody></table>{% endif %}
32
+ {% if project.techStack.get("toolchain") %}<h3>Tool</h3>
33
+ <table><thead><tr><th>Tool</th><th>Command</th></tr></thead><tbody>{% for row in project.techStack.toolchain %}<tr>{{ row_key(pairs=[("Name", row.name), ("Kind", row.kind | enum_label("toolKind")), ("Version", row.get("version"))]) }}<td>{% if row.get("command") %}<code>{{ row.command }}</code>{% else %}--{% endif %}</td></tr>{% endfor %}</tbody></table>{% endif %}
34
+ </section>
35
+ {% endif %}
36
+
37
+ {% if project.get("internalInterfaces") %}
38
+ <section data-report-section="internal-interfaces" data-report-field="projectAnalysis.internalInterfaces">
39
+ <h2>Internal interfaces</h2>
40
+ <table><thead><tr><th>Interface</th><th>Signature</th><th>Consumers</th><th>Evidence</th></tr></thead><tbody>{% for row in project.internalInterfaces %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Name", row.name), ("Kind", row.kind | enum_label("interfaceKind")), ("Owner", row.ownerComponentId)]) }}<td><code>{{ row.signature }}</code></td><td>{{ (row.get("consumers") | default([])) | join(", ") | inline_code }}</td><td>{{ row.get("currentCodeEvidence") | default([]) | code_evidence }}</td></tr>{% endfor %}</tbody></table>
41
+ </section>
42
+ {% endif %}
43
+
44
+ {% if project.get("workflows") %}
45
+ <section data-report-section="workflows" data-report-field="projectAnalysis.workflows">
46
+ <h2>Workflows</h2>
47
+ {{ figure(workflowFigure) }}
48
+ {% for flow in project.workflows %}
49
+ <article class="workflow" id="id-{{ flow.id }}">
50
+ <p class="eyebrow">{{ flow.id }}</p>
51
+ <h3>{{ flow.name | inline_code }}</h3>
52
+ <p><strong>Starts with</strong> — {{ flow.trigger | inline_code }}{% if flow.get("outcome") %} · <strong>Result</strong> — {{ flow.outcome | inline_code }}{% endif %}</p>
53
+ <table><thead><tr><th>Order</th><th>Action</th><th>Component</th><th>External system</th></tr></thead><tbody>{% for step in flow.steps %}<tr><td>{{ step.order }}</td><td>{{ step.action | inline_code }}</td><td>{{ step.componentId | inline_code }}</td><td>{% if step.get("externalSystem") %}{{ step.externalSystem | inline_code }}{% else %}--{% endif %}</td></tr>{% endfor %}</tbody></table>
54
+ </article>
55
+ {% endfor %}
56
+ </section>
57
+ {% endif %}
58
+
20
59
  <section data-report-section="boundaries">
21
- <h2>시스템 경계</h2>
22
- <div class="summary-grid" data-report-field="projectAnalysis.entryPoints">
23
- {% for row in project.entryPoints %}{{ summary_card(row.symbol, row.role) }}{% endfor %}
24
- </div>
25
- <h3>데이터 저장소</h3>
26
- <ul data-report-field="projectAnalysis.dataStores">{% for row in project.dataStores %}<li><strong>{{ row.name | inline_code }}</strong>{{ row.readBoundary | inline_code }} / {{ row.writeBoundary | inline_code }}</li>{% endfor %}</ul>
27
- <h3>외부 시스템</h3>
28
- <ul data-report-field="projectAnalysis.externalSystems">{% for row in project.externalSystems %}<li><strong>{{ row.name | inline_code }}</strong> — {{ row.adapter | inline_code }} ({{ row.direction | inline_code }})</li>{% endfor %}</ul>
60
+ <h2>System boundaries</h2>
61
+ <p class="scan-coverage" data-report-field="projectAnalysis.scanCoverage"><strong>{{ project.scanCoverage.scannedPathCount }}</strong> paths read · <strong>{{ project.scanCoverage.unscannedPathCount }}</strong> left unread — {{ project.scanCoverage.reason | inline_code }}</p>
62
+ <h3>Entry point</h3>
63
+ <table data-report-field="projectAnalysis.entryPoints"><thead><tr><th>Entry point</th><th>Path</th><th>Role</th></tr></thead><tbody>{% for row in project.entryPoints %}<tr>{{ row_key(pairs=[("Symbol", row.symbol), ("Kind", row.kind)]) }}<td><code>{{ row.path }}</code></td><td>{{ row.role | inline_code }}</td></tr>{% endfor %}</tbody></table>
64
+ <h3>Data stores</h3>
65
+ <table data-report-field="projectAnalysis.dataStores"><thead><tr><th>Store</th><th>Reads</th><th>Writes</th><th>Evidence</th></tr></thead><tbody>{% for row in project.dataStores %}<tr>{{ row_key(pairs=[("Name", row.name), ("Owner", row.ownerComponentId)]) }}<td>{{ row.readBoundary | inline_code }}</td><td>{{ row.writeBoundary | inline_code }}</td><td>{{ row.currentCodeEvidence | code_evidence }}</td></tr>{% endfor %}</tbody></table>
66
+ <h3>External system</h3>
67
+ <table data-report-field="projectAnalysis.externalSystems"><thead><tr><th>External system</th><th>Evidence</th></tr></thead><tbody>{% for row in project.externalSystems %}<tr>{{ row_key(pairs=[("Name", row.name), ("Adapter", row.adapter), ("Direction", row.direction)]) }}<td>{{ row.currentCodeEvidence | code_evidence }}</td></tr>{% endfor %}</tbody></table>
29
68
  </section>
30
69
 
31
70
  <section data-report-section="hotspots" data-report-field="projectAnalysis.rankedHotspots">
32
- <h2>우선 검토할 핫스팟</h2>
33
- {{ render_narrative(narrative.hotspotExplanation, "projectAnalysis.userNarrative.hotspotExplanation", evidenceIndex) }}
71
+ <h2>Hotspots to look at first</h2>
72
+ {{ render_narrative(narrative.hotspotExplanation, "projectAnalysis.userNarrative.hotspotExplanation") }}
34
73
  <div class="summary-grid">
35
74
  {% for row in project.rankedHotspots %}
36
- <article class="summary-card tone-{{ row.risk }}"><p class="eyebrow">{{ row.id }} · 영향 {{ row.impact }} · 위험 {{ row.risk }}</p><h3>{{ row.title | inline_code }}</h3><p>{{ row.reason | inline_code }}</p><p class="evidence-refs">근거: {{ row.evidenceRefs | evidence_refs(evidenceIndex) }}</p></article>
75
+ <article class="summary-card tone-{{ row.risk }}" id="id-{{ row.id }}"><p class="eyebrow">{{ row.id }} · Impact {{ row.impact }} · Risk {{ row.risk }}</p><h3>{{ row.title | inline_code }}</h3><p>{{ row.reason | inline_code }}</p><p class="evidence-refs">Evidence: {{ row.evidenceRefs | evidence_refs }}</p></article>
37
76
  {% endfor %}
38
77
  </div>
39
78
  </section>
40
79
 
41
80
  <section data-report-section="quality-and-tests" data-report-field="projectAnalysis.qualityCoverage">
42
- <h2>품질과 테스트 상태</h2>
43
- {{ render_narrative(narrative.qualityExplanation, "projectAnalysis.userNarrative.qualityExplanation", evidenceIndex) }}
44
- <table><thead><tr><th>영역</th><th>상태</th><th>설명</th><th>근거</th></tr></thead><tbody>{% for row in project.qualityCoverage %}<tr><td>{{ row.area | inline_code }}</td><td><span class="status status-{{ row.status }}">{{ row.status }}</span></td><td>{{ row.summary | inline_code }}</td><td>{{ row.evidenceRefs | evidence_refs(evidenceIndex) }}</td></tr>{% endfor %}</tbody></table>
81
+ <h2>Quality and test coverage</h2>
82
+ {{ render_narrative(narrative.qualityExplanation, "projectAnalysis.userNarrative.qualityExplanation") }}
83
+ <table><thead><tr><th>Area</th><th>Detail</th><th>Evidence</th></tr></thead><tbody>{% for row in project.qualityCoverage %}<tr id="id-{{ row.id }}">{{ row_key(pairs=[("ID", row.id), ("Area", row.area)], status_name="Status", status_raw=row.status, status_text=row.status | enum_label("coverage")) }}<td>{{ row.summary | inline_code }}</td><td>{{ row.evidenceRefs | evidence_refs }}</td></tr>{% endfor %}</tbody></table>
84
+ <dl class="enum-legend">{% for label, hint in "coverage" | enum_legend %}<dt>{{ label }}</dt><dd>{{ hint }}</dd>{% endfor %}</dl>
45
85
  </section>
46
86
 
47
87
  <section data-report-section="feature-map" data-report-field="projectAnalysis.featureIndex">
48
- <h2>기능 지도</h2>
49
- <div class="summary-grid">{% for row in project.featureIndex %}{{ summary_card(row.id ~ " · " ~ (row.name | inline_code), row.description) }}{% endfor %}</div>
88
+ <h2>Feature map</h2>
89
+ <div class="summary-grid">{% for row in project.featureIndex %}{{ summary_card(row.id ~ " · " ~ (row.name | inline_code), row.description, anchor=row.id) }}{% endfor %}</div>
50
90
  </section>
51
91
 
52
92
  <section data-report-section="user-actions">
53
- <h2>다음 행동</h2>
93
+ <h2>What to do next</h2>
54
94
  {{ action_list(humanSummary.actions) }}
55
- {% if humanSummary.blockers %}<h3>차단 사항</h3><ul>{% for item in humanSummary.blockers %}<li>{{ item | inline_code }}</li>{% endfor %}</ul>{% endif %}
95
+ {% if humanSummary.blockers %}<h3>Blockers</h3><ul>{% for item in humanSummary.blockers %}<li>{{ item | inline_code }}</li>{% endfor %}</ul>{% endif %}
56
96
  </section>
57
97
  {% endblock %}
@@ -1,36 +1,50 @@
1
1
  {% extends "html/base.template.html" %}
2
- {% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
2
+ {% from "html/macros/layout.html" import narrative as render_narrative, summary_card, row_key %}
3
3
  {% from "html/macros/visualizations.html" import figure %}
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="handoff-outcome">
7
- <h2>인계 결과</h2>
8
- {{ render_narrative(narrative.handoffOutcome, "releaseHandoff.userNarrative.handoffOutcome", evidenceIndex) }}
7
+ <h2>Handoff outcome</h2>
8
+ {{ render_narrative(narrative.handoffOutcome, "releaseHandoff.userNarrative.handoffOutcome") }}
9
9
  {{ figure(handoffFigure) }}
10
10
  </section>
11
11
 
12
+ {% if handoff.get("handoffScope") %}
13
+ <section data-report-section="handoff-scope" data-report-field="releaseHandoff.handoffScope">
14
+ <h2>Handoff scope</h2>
15
+ <p><strong>{{ handoff.handoffScope.mode | enum_label("handoffMode") }}</strong>{% if handoff.handoffScope.get("stages") %} — stages {{ handoff.handoffScope.stages | join(", ") }}{% endif %}{% if handoff.handoffScope.get("collectorBranch") %} · Collector branch <code>{{ handoff.handoffScope.collectorBranch }}</code>{% endif %}</p>
16
+ </section>
17
+ {% endif %}
18
+
12
19
  <section data-report-section="user-selection" data-report-field="releaseHandoff.userSelections">
13
- <h2>사용자가 선택한 동작</h2>
14
- <dl><dt>인계 방식</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>충돌 확인</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>PR 본문</dt><dd>{{ handoff.userSelections.h3 }}</dd></dl>
20
+ <h2>The behaviour you chose</h2>
21
+ <dl><dt>Handoff mode</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>Conflict check</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>PR Body</dt><dd>{{ handoff.userSelections.h3 }}</dd></dl>
15
22
  </section>
16
23
 
17
24
  <section data-report-section="branch-and-pr" data-report-field="releaseHandoff.featureBranchState">
18
- <h2>브랜치와 PR</h2>
19
- {{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation", evidenceIndex) }}
20
- <p><strong>브랜치:</strong> {{ handoff.featureBranchState.branchName }}</p>
21
- <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>PR 결과:</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">Pull request 열기</a>{% endif %}</p>
25
+ <h2>Branch and PR</h2>
26
+ {{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation") }}
27
+ <p><strong>Branch:</strong> {{ handoff.featureBranchState.branchName }}</p>
28
+ <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>PR Result:</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">Pull request Open</a>{% endif %}</p>
22
29
  </section>
23
30
 
24
31
  <section data-report-section="conflicts" data-report-field="releaseHandoff.mergeConflictProbe">
25
- <h2>충돌 판정</h2>
26
- {{ render_narrative(narrative.conflictExplanation, "releaseHandoff.userNarrative.conflictExplanation", evidenceIndex) }}
27
- {{ summary_card(handoff.mergeConflictProbe.kind, "기준 브랜치: " ~ (handoff.mergeConflictProbe.baseBranch | default('not-run'))) }}
32
+ <h2>Conflict verdict</h2>
33
+ {{ render_narrative(narrative.conflictExplanation, "releaseHandoff.userNarrative.conflictExplanation") }}
34
+ {{ summary_card(handoff.mergeConflictProbe.kind, "Base branch: " ~ (handoff.mergeConflictProbe.baseBranch | default('not-run'))) }}
28
35
  {% if handoff.mergeConflictProbe.conflictingPaths %}<ul>{% for path in handoff.mergeConflictProbe.conflictingPaths %}<li>{{ path }}</li>{% endfor %}</ul>{% endif %}
29
36
  </section>
30
37
 
38
+ <section data-report-section="delivered-commits" data-report-field="releaseHandoff.commitList">
39
+ <h2>Commits delivered</h2>
40
+ {% if handoff.commitList is mapping %}<p>There are no commits.</p>{% else %}
41
+ <table><thead><tr><th>Commit</th><th>Subject</th><th>Files</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr>{{ row_key(pairs=[("SHA", row.shortSha), ("Order", loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{% for file in row.files %}<code>{{ file }}</code>{% if not loop.last %} {% endif %}{% endfor %}</td></tr>{% endfor %}</tbody></table>{% endif %}
42
+ <p data-report-field="releaseHandoff.sourceVerificationReport"><strong>The verification report this rests on</strong> — <code>{{ handoff.sourceVerificationReport.path }}</code> · {{ handoff.sourceVerificationReport.verdictTokenQuote | inline_code }}</p>
43
+ </section>
44
+
31
45
  <section data-report-section="next-action">
32
- <h2>다음 행동</h2>
33
- {{ render_narrative(narrative.nextAction, "releaseHandoff.userNarrative.nextAction", evidenceIndex) }}
46
+ <h2>What to do next</h2>
47
+ {{ render_narrative(narrative.nextAction, "releaseHandoff.userNarrative.nextAction") }}
34
48
  <p>{{ handoff.routingRecommendation | inline_code }}</p>
35
49
  </section>
36
50
  {% endblock %}
@@ -4,34 +4,54 @@
4
4
 
5
5
  {% block human_content %}
6
6
  <section data-report-section="request">
7
- <h2>요청 이해</h2>
8
- {{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding", evidenceIndex) }}
9
- <h3>사용자 요청 원문</h3>
7
+ <h2>The request as understood</h2>
8
+ {{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding") }}
9
+ <h3>Your request, verbatim</h3>
10
10
  <p data-report-field="requirementsDiscovery.requestVerbatim">{{ requirements.requestVerbatim | inline_code }}</p>
11
- <h3>시스템 해석</h3>
11
+ <h3>How the system reads it</h3>
12
12
  <p data-report-field="requirementsDiscovery.systemInterpretation">{{ requirements.systemInterpretation | inline_code }}</p>
13
13
  <dl data-report-field="requirementsDiscovery.classification">{% for key, value in requirements.classification.items() %}<dt>{{ key }}</dt><dd>{{ value | inline_code }}</dd>{% endfor %}</dl>
14
14
  </section>
15
15
 
16
+ {% if requirements.get("rejectionCriteria") %}
17
+ <section data-report-section="rejection-criteria" data-report-field="requirementsDiscovery.rejectionCriteria">
18
+ <h2>What would prove this wrong</h2>
19
+ {% if requirements.rejectionCriteria.source == "absent-not-material" %}
20
+ <p>The requester left no rejection criteria, even if there had been, neither the classification nor the next step would change.</p>
21
+ {% else %}
22
+ <p>{{ requirements.rejectionCriteria.get("statement") | inline_code }}</p>
23
+ <p class="evidence-refs">Source: {{ requirements.rejectionCriteria.source | enum_label("rejectionSource") }}{% if requirements.rejectionCriteria.get("clarificationId") %} · {{ requirements.rejectionCriteria.clarificationId | inline_code }}{% endif %}{% if requirements.rejectionCriteria.get("evidenceRefs") %} · {{ requirements.rejectionCriteria.evidenceRefs | evidence_refs }}{% endif %}</p>
24
+ {% endif %}
25
+ </section>
26
+ {% endif %}
27
+
28
+ {% if requirements.get("domainAlignment") %}
29
+ <section data-report-section="domain-alignment" data-report-field="requirementsDiscovery.domainAlignment">
30
+ <h2>Terms settled</h2>
31
+ <p>Glossary {{ "checked" if requirements.domainAlignment.glossaryConsulted else "none" }} · Decision record {{ "checked" if requirements.domainAlignment.decisionsConsulted else "none" }}</p>
32
+ <table><thead><tr><th>As the brief put it</th><th>Settled term</th><th>Settled by</th><th>Evidence</th></tr></thead><tbody>{% for row in requirements.domainAlignment.terms %}<tr><td>{{ row.raw | inline_code }}</td><td>{{ row.canonical | inline_code }}</td><td>{{ row.resolvedBy | enum_label("termSource") }}</td><td>{{ row.get("evidenceRefs") | default([]) | evidence_refs }}</td></tr>{% else %}<tr><td colspan="4">No term was ambiguous.</td></tr>{% endfor %}</tbody></table>
33
+ </section>
34
+ {% endif %}
35
+
16
36
  <section data-report-section="requirements">
17
- <h2>확정 요구사항과 미확정 요구사항</h2>
18
- {{ render_narrative(narrative.confirmedRequirements, "requirementsDiscovery.userNarrative.confirmedRequirements", evidenceIndex) }}
19
- <div class="summary-grid" data-report-field="requirementsDiscovery.confirmedRequirements">{% for row in requirements.confirmedRequirements %}{{ summary_card(row.id ~ " · 확정", row.statement) }}{% endfor %}</div>
20
- {{ render_narrative(narrative.unresolvedRequirements, "requirementsDiscovery.userNarrative.unresolvedRequirements", evidenceIndex) }}
21
- <div class="summary-grid" data-report-field="requirementsDiscovery.unresolvedRequirements">{% for row in requirements.unresolvedRequirements %}{{ summary_card(row.id ~ " · 결정 필요", row.statement, "important") }}{% else %}<p>미확정 요구사항이 없습니다.</p>{% endfor %}</div>
37
+ <h2>Settled and unsettled requirements</h2>
38
+ {{ render_narrative(narrative.confirmedRequirements, "requirementsDiscovery.userNarrative.confirmedRequirements") }}
39
+ <div class="summary-grid" data-report-field="requirementsDiscovery.confirmedRequirements">{% for row in requirements.confirmedRequirements %}{{ summary_card(row.id ~ " · Settled", row.statement, anchor=row.id) }}{% endfor %}</div>
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 ~ " · Decision needed", row.statement, "important") }}{% else %}<p>Nothing is left unsettled.</p>{% endfor %}</div>
22
42
  </section>
23
43
 
24
44
  <section data-report-section="routing">
25
- <h2>다음 태스크와 의존성</h2>
26
- {{ render_narrative(narrative.routingExplanation, "requirementsDiscovery.userNarrative.routingExplanation", evidenceIndex) }}
27
- <p><strong>다음 태스크:</strong> {{ requirements.routing.nextTaskType }} · {{ requirements.routing.rationale | inline_code }}</p>
45
+ <h2>Next tasks and dependencies</h2>
46
+ {{ render_narrative(narrative.routingExplanation, "requirementsDiscovery.userNarrative.routingExplanation") }}
47
+ <p><strong>Next task:</strong> {{ requirements.routing.nextTaskType }} · {{ requirements.routing.rationale | inline_code }}</p>
28
48
  {{ figure(routingFigure) }}
29
- {% if not requirements.taskDependencies %}<p class="empty-state">이 요구사항 태스크는 다른 태스크에 의존하지 않는 단일 작업입니다.</p>{% endif %}
49
+ {% if not requirements.taskDependencies %}<p class="empty-state">This requirements task stands alone; nothing else feeds it.</p>{% endif %}
30
50
  </section>
31
51
 
32
52
  <section data-report-section="decisions">
33
- <h2>사용자가 결정할 항목</h2>
34
- <div class="summary-grid">{% for row in clarificationItems %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>추가 결정이 필요하지 않습니다.</p>{% endfor %}</div>
53
+ <h2>Decisions that are yours</h2>
54
+ <div class="summary-grid">{% for row in clarificationItems %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>No further decision is needed.</p>{% endfor %}</div>
35
55
  {{ action_list(humanSummary.actions) }}
36
56
  </section>
37
57
  {% endblock %}
@@ -213,6 +213,42 @@ def _validate_project_semantics(data: dict, errors: list[str]) -> None:
213
213
  f"projectAnalysis.featureIndex `{row.get('id') or '?'}` representative "
214
214
  "entry point must be inside analysisCommon.scope.includedPaths"
215
215
  )
216
+ _validate_component_references(analysis, errors)
217
+
218
+
219
+ def _validate_component_references(analysis: dict, errors: list[str]) -> None:
220
+ """Every componentId a row names has to be a component this report declares.
221
+
222
+ The reader clicks these — they render as links to the component's row — so
223
+ a reference to a component that was never listed is a dead anchor, and one
224
+ to a component that exists under a different id is worse: it reads as a
225
+ relationship the analysis never found.
226
+ """
227
+ declared = {
228
+ row.get("id")
229
+ for row in analysis.get("components") or []
230
+ if isinstance(row, dict) and row.get("id")
231
+ }
232
+ if not declared:
233
+ return
234
+
235
+ def check(component_id: object, where: str) -> None:
236
+ if isinstance(component_id, str) and component_id and component_id not in declared:
237
+ errors.append(f"{where} names component `{component_id}`, which is not declared")
238
+
239
+ for row in analysis.get("internalInterfaces") or []:
240
+ if not isinstance(row, dict):
241
+ continue
242
+ where = f"projectAnalysis.internalInterfaces `{row.get('id') or '?'}`"
243
+ check(row.get("ownerComponentId"), where)
244
+ for consumer in row.get("consumers") or []:
245
+ check(consumer, where)
246
+ for row in analysis.get("workflows") or []:
247
+ if not isinstance(row, dict):
248
+ continue
249
+ for step in row.get("steps") or []:
250
+ if isinstance(step, dict):
251
+ check(step.get("componentId"), f"projectAnalysis.workflows `{row.get('id') or '?'}`")
216
252
 
217
253
 
218
254
  def _validate_feature_semantics(data: dict, errors: list[str]) -> None: