okstra 0.148.1 → 0.150.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 (78) hide show
  1. package/docs/architecture.md +1 -1
  2. package/docs/cli.md +1 -1
  3. package/docs/project-structure-overview.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/translator-worker.md +67 -0
  7. package/runtime/bin/okstra-render-final-report.py +0 -11
  8. package/runtime/bin/okstra-render-report-views.py +20 -0
  9. package/runtime/bin/okstra-report-translate.py +158 -0
  10. package/runtime/prompts/lead/adapters/claude-code.md +1 -1
  11. package/runtime/prompts/lead/okstra-lead-contract.md +1 -0
  12. package/runtime/prompts/lead/report-writer.md +14 -13
  13. package/runtime/prompts/lead/team-contract.md +2 -2
  14. package/runtime/prompts/profiles/project-analysis.md +18 -0
  15. package/runtime/prompts/profiles/release-handoff.md +3 -0
  16. package/runtime/prompts/profiles/requirements-discovery.md +7 -0
  17. package/runtime/prompts/wizard/prompts.ko.json +17 -1
  18. package/runtime/python/okstra_ctl/analysis_inputs.py +24 -9
  19. package/runtime/python/okstra_ctl/analysis_packet.py +23 -1
  20. package/runtime/python/okstra_ctl/clarification_items.py +241 -44
  21. package/runtime/python/okstra_ctl/codex_dispatch.py +2 -1
  22. package/runtime/python/okstra_ctl/dispatch_core.py +2 -2
  23. package/runtime/python/okstra_ctl/dispatch_state.py +12 -1
  24. package/runtime/python/okstra_ctl/final_report_paths.py +22 -1
  25. package/runtime/python/okstra_ctl/i18n.py +12 -7
  26. package/runtime/python/okstra_ctl/render_final_report.py +18 -17
  27. package/runtime/python/okstra_ctl/report_html/common.py +77 -47
  28. package/runtime/python/okstra_ctl/report_html/filters.py +63 -30
  29. package/runtime/python/okstra_ctl/report_html/models.py +15 -0
  30. package/runtime/python/okstra_ctl/report_html/render.py +86 -5
  31. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +14 -4
  32. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +3 -3
  33. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +5 -3
  34. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -7
  35. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +5 -9
  36. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +2 -5
  37. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +1 -2
  38. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +90 -4
  39. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +1 -8
  40. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +6 -4
  41. package/runtime/python/okstra_ctl/report_html/visualizations.py +146 -11
  42. package/runtime/python/okstra_ctl/report_translation.py +440 -0
  43. package/runtime/python/okstra_ctl/report_view_artifacts.py +5 -0
  44. package/runtime/python/okstra_ctl/report_views.py +23 -9
  45. package/runtime/python/okstra_ctl/run.py +1 -1
  46. package/runtime/python/okstra_ctl/time_report.py +2 -2
  47. package/runtime/python/okstra_ctl/usage_report.py +2 -2
  48. package/runtime/python/okstra_ctl/user_response.py +11 -6
  49. package/runtime/python/okstra_ctl/wizard.py +100 -25
  50. package/runtime/python/okstra_ctl/worker_liveness.py +130 -36
  51. package/runtime/schemas/final-report-v2.0.schema.json +229 -1
  52. package/runtime/templates/reports/final-report.template.md +55 -0
  53. package/runtime/templates/reports/html/assets/base.css +59 -9
  54. package/runtime/templates/reports/html/base.template.html +23 -44
  55. package/runtime/templates/reports/html/i18n/en.json +395 -0
  56. package/runtime/templates/reports/html/i18n/ko.json +395 -0
  57. package/runtime/templates/reports/html/macros/forms.html +20 -20
  58. package/runtime/templates/reports/html/macros/layout.html +18 -5
  59. package/runtime/templates/reports/html/macros/visualizations.html +7 -5
  60. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +30 -15
  61. package/runtime/templates/reports/html/tasks/error-analysis.template.html +22 -15
  62. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +35 -15
  63. package/runtime/templates/reports/html/tasks/final-verification.template.html +21 -14
  64. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +78 -19
  65. package/runtime/templates/reports/html/tasks/implementation.template.html +34 -16
  66. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +11 -11
  67. package/runtime/templates/reports/html/tasks/project-analysis.template.html +65 -25
  68. package/runtime/templates/reports/html/tasks/release-handoff.template.html +28 -14
  69. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +35 -15
  70. package/runtime/templates/reports/report.js +8 -5
  71. package/runtime/validators/validate-report-views.py +1 -1
  72. package/runtime/validators/validate-run.py +28 -31
  73. package/runtime/validators/validate_analysis_report.py +36 -0
  74. package/src/cli-registry.mjs +11 -0
  75. package/src/commands/inspect/worker-liveness.mjs +9 -7
  76. package/src/commands/report/translate.mjs +31 -0
  77. package/src/lib/helper-scripts.mjs +1 -0
  78. package/runtime/templates/reports/i18n/ko.json +0 -273
@@ -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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.tech-stack') }}</h2>
28
+ <h3>{{ t('tasks.project-analysis.language') }}</h3>
29
+ <table><thead><tr><th>{{ t('tasks.project-analysis.language') }}</th><th>{{ t('tasks.project-analysis.version') }}</th><th>{{ t('tasks.project-analysis.version-source') }}</th><th>{{ t('tasks.project-analysis.files') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.framework') }}</h3>
31
+ <table><thead><tr><th>{{ t('tasks.project-analysis.framework') }}</th><th>{{ t('tasks.project-analysis.role') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.tool') }}</h3>
33
+ <table><thead><tr><th>{{ t('tasks.project-analysis.tool') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.internal-interfaces') }}</h2>
40
+ <table><thead><tr><th>{{ t('tasks.project-analysis.interface') }}</th><th>{{ t('tasks.project-analysis.signature') }}</th><th>{{ t('tasks.project-analysis.consumers') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.starts-with') }}</strong> — {{ flow.trigger | inline_code }}{% if flow.get("outcome") %} · <strong>{{ t('tasks.project-analysis.result') }}</strong> — {{ flow.outcome | inline_code }}{% endif %}</p>
53
+ <table><thead><tr><th>{{ t('tasks.project-analysis.order') }}</th><th>{{ t('tasks.project-analysis.action') }}</th><th>{{ t('tasks.project-analysis.component') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.system-boundaries') }}</h2>
61
+ <p class="scan-coverage" data-report-field="projectAnalysis.scanCoverage"><strong>{{ project.scanCoverage.scannedPathCount }}</strong> {{ t('tasks.project-analysis.paths-read') }} <strong>{{ project.scanCoverage.unscannedPathCount }}</strong> left unread — {{ project.scanCoverage.reason | inline_code }}</p>
62
+ <h3>{{ t('tasks.project-analysis.entry-point') }}</h3>
63
+ <table data-report-field="projectAnalysis.entryPoints"><thead><tr><th>{{ t('tasks.project-analysis.entry-point') }}</th><th>{{ t('tasks.project-analysis.path') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.data-stores') }}</h3>
65
+ <table data-report-field="projectAnalysis.dataStores"><thead><tr><th>{{ t('tasks.project-analysis.store') }}</th><th>{{ t('tasks.project-analysis.reads') }}</th><th>{{ t('tasks.project-analysis.writes') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.external-system') }}</h3>
67
+ <table data-report-field="projectAnalysis.externalSystems"><thead><tr><th>{{ t('tasks.project-analysis.external-system') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.quality-and-test-coverage') }}</h2>
82
+ {{ render_narrative(narrative.qualityExplanation, "projectAnalysis.userNarrative.qualityExplanation") }}
83
+ <table><thead><tr><th>{{ t('tasks.project-analysis.area') }}</th><th>{{ t('tasks.project-analysis.detail') }}</th><th>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.project-analysis.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>{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.the-behaviour-you-chose') }}</h2>
21
+ <dl><dt>{{ t('tasks.release-handoff.handoff-mode') }}</dt><dd>{{ handoff.userSelections.h1 }}</dd><dt>{{ t('tasks.release-handoff.conflict-check') }}</dt><dd>{{ handoff.userSelections.h2b }}</dd><dt>{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.branch-and-pr') }}</h2>
26
+ {{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation") }}
27
+ <p><strong>{{ t('tasks.release-handoff.branch') }}</strong> {{ handoff.featureBranchState.branchName }}</p>
28
+ <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>{{ t('tasks.release-handoff.pr-result') }}</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.commits-delivered') }}</h2>
40
+ {% if handoff.commitList is mapping %}<p>{{ t('tasks.release-handoff.there-are-no-commits') }}</p>{% else %}
41
+ <table><thead><tr><th>{{ t('tasks.release-handoff.commit') }}</th><th>{{ t('tasks.release-handoff.subject') }}</th><th>{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.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>{{ t('tasks.release-handoff.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>{{ t('tasks.requirements-discovery.the-request-as-understood') }}</h2>
8
+ {{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding") }}
9
+ <h3>{{ t('tasks.requirements-discovery.your-request-verbatim') }}</h3>
10
10
  <p data-report-field="requirementsDiscovery.requestVerbatim">{{ requirements.requestVerbatim | inline_code }}</p>
11
- <h3>시스템 해석</h3>
11
+ <h3>{{ t('tasks.requirements-discovery.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>{{ t('tasks.requirements-discovery.what-would-prove-this-wrong') }}</h2>
19
+ {% if requirements.rejectionCriteria.source == "absent-not-material" %}
20
+ <p>{{ t('tasks.requirements-discovery.the-requester-left-no-rejection-criteria-eve') }}</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>{{ t('tasks.requirements-discovery.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>{{ t('tasks.requirements-discovery.as-the-brief-put-it') }}</th><th>{{ t('tasks.requirements-discovery.settled-term') }}</th><th>{{ t('tasks.requirements-discovery.settled-by') }}</th><th>{{ t('tasks.requirements-discovery.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">{{ t('tasks.requirements-discovery.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>{{ t('tasks.requirements-discovery.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>{{ t('tasks.requirements-discovery.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>{{ t('tasks.requirements-discovery.next-tasks-and-dependencies') }}</h2>
46
+ {{ render_narrative(narrative.routingExplanation, "requirementsDiscovery.userNarrative.routingExplanation") }}
47
+ <p><strong>{{ t('tasks.requirements-discovery.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">{{ t('tasks.requirements-discovery.this-requirements-task-stands-alone-nothing') }}</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>{{ t('tasks.requirements-discovery.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>{{ t('tasks.requirements-discovery.no-further-decision-is-needed') }}</p>{% endfor %}</div>
35
55
  {{ action_list(humanSummary.actions) }}
36
56
  </section>
37
57
  {% endblock %}
@@ -331,10 +331,7 @@
331
331
  if (/Reader Summary|Verdict Card|Clarification Items|Recommended Next Steps|Follow-up Tasks/.test(text)) {
332
332
  return "action";
333
333
  }
334
- // "토큰 사용량" is tokenSummary.heading from templates/reports/i18n/ko.json —
335
- // the only audit heading the ko dictionary translates away from its English
336
- // form. tests/report/test_report_views.py pins the two in sync.
337
- if (/Evidence|Cross Verification|Execution Status|Token Usage|토큰 사용량|Plan Body Verification|Round History/.test(text)) {
334
+ if (/Evidence|Cross Verification|Execution Status|Token Usage|Plan Body Verification|Round History/.test(text)) {
338
335
  return "audit";
339
336
  }
340
337
  return "detail";
@@ -356,7 +353,13 @@
356
353
  // the run-metadata list and table of contents that trail it are document
357
354
  // chrome — classifying them would hide the title in action mode, because
358
355
  // the title text ("… Cross Verification Final Report") reads as audit.
359
- if ((node.tagName || "") === "H2") {
356
+ // A section that declares its own kind wins. Heading text is a fallback:
357
+ // the HTML's headings render in the reader's language, so "Evidence
358
+ // ledger" reads as audit and "근거 대장" reads as nothing at all.
359
+ var declared = node.getAttribute && node.getAttribute("data-reader-kind");
360
+ if (declared) {
361
+ current = declared;
362
+ } else if ((node.tagName || "") === "H2") {
360
363
  current = readerSectionKind(node.textContent || "");
361
364
  }
362
365
  node.setAttribute("data-reader-section", current);
@@ -227,7 +227,7 @@ def validate(report_path: Path) -> list[str]:
227
227
  if not html_path.is_file():
228
228
  if (
229
229
  not md_ids
230
- and plan_approval_context(report_path, md) is None
230
+ and plan_approval_context(report_path) is None
231
231
  and review_ctx is None
232
232
  ):
233
233
  return []
@@ -7058,42 +7058,39 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
7058
7058
  """Re-render the ``*.html`` sibling against the just-substituted MD.
7059
7059
  Returns a short status note for the autofix message (empty on no-op,
7060
7060
  descriptive on failure).
7061
+
7062
+ Delegates to ``scripts/okstra-render-report-views.py`` rather than calling
7063
+ a renderer directly: that script is the single reference point that routes
7064
+ schema-v2 reports to the data.json-driven task view and schema-v1 reports
7065
+ to the legacy markdown renderer. Rendering here would have to re-derive
7066
+ both the routing and the run-meta, and getting either wrong overwrites a
7067
+ schema-v2 report's HTML — the user's only way to answer its clarifications
7068
+ — with a legacy render that carries no answer controls.
7061
7069
  """
7070
+ import subprocess
7071
+
7062
7072
  if not report_path.is_file():
7063
7073
  return ""
7064
- try:
7065
- # Late import — keeps validate-run.py importable in environments
7066
- # that don't ship report_views (older installs).
7067
- scripts_dir = Path(__file__).resolve().parent.parent / "scripts"
7068
- if str(scripts_dir) not in sys.path:
7069
- sys.path.insert(0, str(scripts_dir))
7070
- from okstra_ctl.report_views import RunMeta, render_html_view
7071
- templates_dir = (
7072
- Path(__file__).resolve().parent.parent / "templates" / "reports"
7073
- )
7074
- css = (templates_dir / "report.css").read_text(encoding="utf-8")
7075
- js = (templates_dir / "report.js").read_text(encoding="utf-8")
7076
- except Exception as exc: # noqa: BLE001 — best-effort
7077
- return f"report-views re-render skipped ({exc})"
7078
-
7079
- # Infer task-key / task-type / seq from path + body when possible;
7080
- # fall back to placeholders so the digest comparison (the actual
7081
- # contract) still works.
7082
- text = report_path.read_text(encoding="utf-8")
7083
- task_type_m = re.search(r"^- Task Type:\s*(\S+)", text, re.MULTILINE)
7084
- task_key_m = re.search(r"^- Task Key:\s*(\S+)", text, re.MULTILINE)
7085
- seq_m = re.search(r"-(\d+)\.md$", report_path.name)
7086
- meta = RunMeta(
7087
- task_key=task_key_m.group(1) if task_key_m else "unknown",
7088
- task_type=task_type_m.group(1) if task_type_m else "unknown",
7089
- seq=seq_m.group(1) if seq_m else "000",
7090
- source_report=report_path.name,
7074
+ renderer = (
7075
+ Path(__file__).resolve().parent.parent
7076
+ / "scripts" / "okstra-render-report-views.py"
7091
7077
  )
7078
+ if not renderer.is_file():
7079
+ return f"report-views re-render skipped (renderer missing under {renderer.parent})"
7092
7080
  try:
7093
- rendered = render_html_view(report_path, run_meta=meta, css=css, js=js)
7094
- except Exception as exc: # noqa: BLE001
7095
- return f"report-views re-render failed: {exc}"
7096
- if rendered is None:
7081
+ proc = subprocess.run(
7082
+ [sys.executable, str(renderer), str(report_path)],
7083
+ capture_output=True,
7084
+ text=True,
7085
+ timeout=60,
7086
+ )
7087
+ except subprocess.TimeoutExpired:
7088
+ return "report-views re-render failed: renderer timed out (60s)"
7089
+ if proc.returncode != 0:
7090
+ detail = (proc.stderr or proc.stdout).strip().splitlines()
7091
+ return f"report-views re-render failed: {detail[-1] if detail else 'unknown error'}"
7092
+ emitted = (proc.stdout or "").strip().splitlines()
7093
+ if emitted and emitted[-1].startswith("html: skipped"):
7097
7094
  return "report-views skipped (no §1 clarification rows)"
7098
7095
  return "report-views re-rendered"
7099
7096
 
@@ -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:
@@ -333,6 +333,17 @@ export const COMMAND_REGISTRY = [
333
333
  category: "introspection",
334
334
  summary: ["Render slim AI + self-contained HTML views of a final report"],
335
335
  },
336
+ {
337
+ name: "report-translate",
338
+ module: "./commands/report/translate.mjs",
339
+ export: "run",
340
+ category: "introspection",
341
+ summary: [
342
+ "Build and verify the translation sidecar the HTML",
343
+ "renderer overlays when the report language is not",
344
+ "English (extract | check).",
345
+ ],
346
+ },
336
347
  {
337
348
  name: "report-finalize",
338
349
  module: "./commands/report/finalize.mjs",
@@ -3,15 +3,17 @@ import { runPythonModule } from "../../lib/python-helper.mjs";
3
3
  const USAGE = `okstra worker-liveness — report whether pending workers are still alive
4
4
 
5
5
  Usage:
6
- okstra worker-liveness [--audit <path>]...
7
- [--team-state <path> --worker <worker-id>]...
6
+ okstra worker-liveness [--team-state <path> --worker <worker-id>]...
8
7
  [--max-idle <seconds>] [--launch-grace <seconds>] [--json]
9
8
 
10
- --audit an in-process worker audit sidecar path; stale past the heartbeat cadence
11
- means the worker hung.
12
- --team-state and --worker select a CLI-wrapper assignment. No prompt sibling
13
- .log or .status.json past the launch grace means the wrapper never
14
- started. The grace begins at workers[].startedAt, not prompt mtime.
9
+ --team-state and --worker select one pending worker. The worker row's
10
+ livenessMode picks the probe: audit-heartbeat reads the in-process
11
+ worker audit sidecar path and reports stalled past the heartbeat
12
+ cadence; wrapper-status reports did-not-launch when no prompt sibling
13
+ .log or .status.json exists past the launch grace. Both graces begin
14
+ at workers[].startedAt, never at an artifact mtime — the audit sidecar
15
+ is reused on re-dispatch, so the previous attempt's last heartbeat is
16
+ not this dispatch's signal.
15
17
 
16
18
  Output: JSON { ok, checkedAt, probes[], unhealthy[] }. Exit 1 when any worker is
17
19
  stalled or did not launch, so a poll loop can branch on the exit code. Read-only:
@@ -0,0 +1,31 @@
1
+ import { runInstalledScript } from "../../lib/python-helper.mjs";
2
+
3
+ const USAGE = `okstra report-translate — build and verify a final-report translation sidecar
4
+
5
+ A thin spawn shim over \`scripts/okstra-report-translate.py\` (installed at
6
+ \`$HOME/.okstra/bin/okstra-report-translate.py\`).
7
+
8
+ The final-report data.json is authored in English and stays the SSOT. When the
9
+ resolved report language is not English, Phase 7 dispatches a translator that
10
+ writes a sidecar beside the report; \`render-views\` overlays it so only the
11
+ human HTML is localized.
12
+
13
+ Usage:
14
+ okstra report-translate extract <path-to-final-report.data.json>
15
+ Write <stem>.translation-source.json — every pointer the report holds a
16
+ translatable string at, paired with its English text. The translator
17
+ fills in the values, so a sidecar cannot cite a path the report lacks.
18
+
19
+ okstra report-translate check <path-to-final-report.i18n.<lang>.json>
20
+ Resolve every pointer in a filled sidecar against the report it names
21
+ and report what is still English. Exits non-zero on a pointer that
22
+ resolves nowhere — the translator's own gate before it returns.
23
+ `;
24
+
25
+ export async function run(args) {
26
+ return runInstalledScript({
27
+ scriptName: "okstra-report-translate.py",
28
+ args,
29
+ usage: USAGE,
30
+ });
31
+ }
@@ -12,6 +12,7 @@ export const HELPER_SCRIPT_NAMES = Object.freeze([
12
12
  "okstra-inject-report-index.py",
13
13
  "okstra-render-final-report.py",
14
14
  "okstra-render-report-views.py",
15
+ "okstra-report-translate.py",
15
16
  "okstra-spawn-followups.py",
16
17
  "okstra-token-usage.py",
17
18
  ]);