okstra 0.146.1 → 0.148.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.
- package/README.md +23 -9
- package/docs/architecture/storage-model.md +39 -65
- package/docs/architecture.md +68 -60
- package/docs/cli.md +40 -23
- package/docs/for-ai/skills/okstra-run.md +13 -34
- package/docs/performance-improvement-plan-v2.md +2 -2
- package/docs/pr-template-usage.md +1 -1
- package/docs/project-structure-overview.md +26 -22
- package/docs/task-process/README.md +4 -4
- package/docs/task-process/common-flow.md +12 -12
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +1 -1
- package/package.json +2 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +2 -2
- package/runtime/agents/workers/grok-worker.md +256 -0
- package/runtime/agents/workers/kimi-worker.md +256 -0
- package/runtime/agents/workers/report-writer-worker.md +12 -12
- package/runtime/bin/lib/okstra/cli.sh +13 -1
- package/runtime/bin/lib/okstra/globals.sh +3 -0
- package/runtime/bin/lib/okstra/usage.sh +17 -12
- package/runtime/bin/okstra-grok-exec.sh +5 -0
- package/runtime/bin/okstra-kimi-exec.sh +5 -0
- package/runtime/bin/okstra-provider-exec.py +235 -0
- package/runtime/bin/okstra-render-final-report.py +4 -4
- package/runtime/bin/okstra-render-report-views.py +100 -12
- package/runtime/bin/okstra.sh +3 -0
- package/runtime/prompts/lead/adapters/antigravity.md +48 -0
- package/runtime/prompts/lead/adapters/claude-code.md +13 -11
- package/runtime/prompts/lead/adapters/codex.md +7 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +5 -5
- package/runtime/prompts/lead/report-writer.md +16 -12
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_common-contract.md +16 -10
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +12 -12
- package/runtime/prompts/profiles/_implementation-self-check.md +4 -4
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +2 -0
- package/runtime/prompts/profiles/error-analysis.md +2 -0
- package/runtime/prompts/profiles/feature-analysis.md +2 -0
- package/runtime/prompts/profiles/final-verification.md +3 -1
- package/runtime/prompts/profiles/forbidden-actions.json +4 -4
- package/runtime/prompts/profiles/implementation-planning.md +3 -1
- package/runtime/prompts/profiles/implementation.md +2 -2
- package/runtime/prompts/profiles/improvement-discovery.md +6 -2
- package/runtime/prompts/profiles/project-analysis.md +2 -0
- package/runtime/prompts/profiles/release-handoff.md +7 -7
- package/runtime/prompts/profiles/requirements-discovery.md +2 -0
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/codex_dispatch.py +68 -87
- package/runtime/python/okstra_ctl/dispatch_core.py +4 -22
- package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
- package/runtime/python/okstra_ctl/lead_events.py +1 -1
- package/runtime/python/okstra_ctl/lead_runtime.py +13 -2
- package/runtime/python/okstra_ctl/models.py +156 -8
- package/runtime/python/okstra_ctl/path_hints.py +9 -25
- package/runtime/python/okstra_ctl/paths.py +1 -1
- package/runtime/python/okstra_ctl/render.py +172 -74
- package/runtime/python/okstra_ctl/render_final_report.py +136 -28
- package/runtime/python/okstra_ctl/report_contract.py +124 -0
- package/runtime/python/okstra_ctl/report_finalize.py +1 -1
- package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
- package/runtime/python/okstra_ctl/report_html/common.py +86 -0
- package/runtime/python/okstra_ctl/report_html/filters.py +104 -0
- package/runtime/python/okstra_ctl/report_html/models.py +59 -0
- package/runtime/python/okstra_ctl/report_html/render.py +76 -0
- package/runtime/python/okstra_ctl/report_html/router.py +40 -0
- package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
- package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +39 -0
- package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +49 -0
- package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +39 -0
- package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +47 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +47 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +103 -0
- package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +43 -0
- package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +54 -0
- package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +55 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +139 -0
- package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
- package/runtime/python/okstra_ctl/report_views.py +15 -43
- package/runtime/python/okstra_ctl/run.py +276 -51
- package/runtime/python/okstra_ctl/runner_resolution.py +103 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
- package/runtime/python/okstra_ctl/team.py +2 -7
- package/runtime/python/okstra_ctl/wizard.py +194 -21
- package/runtime/python/okstra_ctl/worker_artifacts.py +46 -0
- package/runtime/python/okstra_ctl/workers.py +3 -1
- package/runtime/python/okstra_ctl/workflow.py +4 -2
- package/runtime/python/okstra_token_usage/__init__.py +1 -0
- package/runtime/python/okstra_token_usage/collect.py +32 -23
- package/runtime/python/okstra_token_usage/pricing.py +35 -3
- package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
- package/runtime/skills/okstra-run/SKILL.md +31 -42
- package/runtime/templates/prd/pr-body.template.md +1 -1
- package/runtime/templates/reports/final-report-v2.template.md +66 -0
- package/runtime/templates/reports/html/assets/base.css +41 -0
- package/runtime/templates/reports/html/assets/base.js +5 -0
- package/runtime/templates/reports/html/base.template.html +79 -0
- package/runtime/templates/reports/html/macros/forms.html +47 -0
- package/runtime/templates/reports/html/macros/layout.html +19 -0
- package/runtime/templates/reports/html/macros/visualizations.html +27 -0
- package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
- package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
- package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
- package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
- package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
- package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
- package/runtime/templates/reports/report.js +21 -4
- package/runtime/templates/reports/settings.template.json +4 -0
- package/runtime/templates/reports/task-brief.template.md +7 -7
- package/runtime/validators/validate-report-views.py +86 -4
- package/runtime/validators/validate-run.py +73 -15
- package/runtime/validators/validate_improvement_report.py +55 -0
- package/runtime/validators/validate_session_conformance.py +2 -1
- package/src/cli-registry.mjs +4 -4
- package/src/commands/execute/codex-dispatch.mjs +7 -10
- package/src/commands/execute/render-bundle.mjs +3 -3
- package/src/commands/execute/run.mjs +17 -52
- package/src/commands/execute/wizard.mjs +4 -1
- package/src/commands/lifecycle/doctor.mjs +6 -3
- package/src/commands/lifecycle/install.mjs +49 -21
- package/src/commands/report/finalize.mjs +2 -3
- package/src/commands/report/render-final-report.mjs +4 -2
- package/src/commands/report/render-views.mjs +8 -8
- package/src/lib/runtime-manifest.mjs +1 -1
- package/src/lib/runtime-resolver.mjs +2 -2
- package/src/lib/worker-agent-render.mjs +50 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="verification-verdict">
|
|
7
|
+
<h2>검증 판정</h2>
|
|
8
|
+
<p><span class="status status-{{ verdict.verdictToken }}">{{ verdict.verdictToken }}</span></p>
|
|
9
|
+
{{ render_narrative(narrative.verdictExplanation, "finalVerification.userNarrative.verdictExplanation", evidenceIndex) }}
|
|
10
|
+
</section>
|
|
11
|
+
|
|
12
|
+
<section data-report-section="requirement-results" data-report-field="finalVerification.validationEvidence">
|
|
13
|
+
<h2>요구사항별 결과</h2>
|
|
14
|
+
{{ render_narrative(narrative.coverageExplanation, "finalVerification.userNarrative.coverageExplanation", evidenceIndex) }}
|
|
15
|
+
{{ figure(coverageFigure) }}
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section data-report-section="blockers" data-report-field="finalVerification.acceptanceBlockers">
|
|
19
|
+
<h2>수락 차단 항목</h2>
|
|
20
|
+
{{ render_narrative(narrative.blockerExplanation, "finalVerification.userNarrative.blockerExplanation", evidenceIndex) }}
|
|
21
|
+
<div class="summary-grid">{% for row in final.acceptanceBlockers %}{{ summary_card(row.id ~ " · " ~ row.severity, row.statement, "important") }}{% else %}<p>수락을 막는 항목이 없습니다.</p>{% endfor %}</div>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section data-report-section="residual-risk" data-report-field="finalVerification.residualRisk">
|
|
25
|
+
<h2>잔여 위험</h2>
|
|
26
|
+
<div class="summary-grid">{% for row in final.residualRisk %}<article class="summary-card"><h3>{{ row.id }}</h3><p>{{ row.item | inline_code }}</p><p><strong>담당:</strong> {{ row.owner | inline_code }}</p><p><strong>상향 조건:</strong> {{ row.escalationTrigger | inline_code }}</p></article>{% else %}<p>기록된 잔여 위험이 없습니다.</p>{% endfor %}</div>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
<section data-report-section="manual-results" data-report-field="finalVerification.manualUserTest">
|
|
30
|
+
<h2>수동 사용자 테스트 결과</h2>
|
|
31
|
+
{% if final.manualUserTest.applicable %}{% for row in final.manualUserTest.results %}<article class="summary-card tone-{{ row.result }}"><h3>{{ row.target | inline_code }} · {{ row.result }}</h3><p>{{ row.performed | inline_code }}</p><p>{{ row.observed | inline_code }}</p></article>{% endfor %}{% else %}<p>{{ final.manualUserTest.exemptionReaffirm | inline_code }}</p>{% endif %}
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
<section data-report-section="release-route">
|
|
35
|
+
<h2>릴리스 경로</h2>
|
|
36
|
+
{{ render_narrative(narrative.releaseReadiness, "finalVerification.userNarrative.releaseReadiness", evidenceIndex) }}
|
|
37
|
+
{% if releaseAllowed %}<p>{{ final.routingRecommendation | inline_code }}</p>{% else %}<p>현재 판정으로는 release-handoff를 시작할 수 없습니다. 차단 또는 조건을 먼저 해소하세요.</p>{% endif %}
|
|
38
|
+
</section>
|
|
39
|
+
{% endblock %}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/forms.html" import plan_approval %}
|
|
3
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
|
|
4
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
5
|
+
|
|
6
|
+
{% block human_content %}
|
|
7
|
+
<section data-report-section="planning-goal">
|
|
8
|
+
<h2>계획 목표</h2>
|
|
9
|
+
<p>{{ humanSummary.outcome | inline_code }}</p>
|
|
10
|
+
<div class="summary-grid">{% for reason in humanSummary.whyItMatters %}{{ summary_card("왜 중요한가 " ~ loop.index, reason, "important") }}{% endfor %}</div>
|
|
11
|
+
</section>
|
|
12
|
+
|
|
13
|
+
<section data-report-section="option-comparison">
|
|
14
|
+
<h2>구현 대안 비교</h2>
|
|
15
|
+
{{ render_narrative(narrative.optionExplanation, "implementationPlanning.userNarrative.optionExplanation", evidenceIndex) }}
|
|
16
|
+
<div class="summary-grid" data-report-field="implementationPlanning.optionCandidates">{% for row in planning.optionCandidates %}<article class="summary-card"><h3>{{ row.name | inline_code }}</h3><p><strong>인터페이스:</strong> {{ row.interfaces | inline_code }}</p><p><strong>영향 범위:</strong> {{ row.blastRadius | inline_code }}</p><ul>{% for file in row.fileStructure %}<li>{{ file.action }} <code>{{ file.path }}</code> — {{ file.summary | inline_code }}</li>{% endfor %}</ul></article>{% endfor %}</div>
|
|
17
|
+
<table data-report-field="implementationPlanning.tradeoffMatrix"><thead><tr><th>옵션</th><th>복잡도</th><th>위험</th><th>가역성</th><th>테스트 비용</th><th>배포 비용</th></tr></thead><tbody>{% for row in planning.tradeoffMatrix %}<tr><td>{{ row.option | inline_code }}</td><td>{{ row.complexity | inline_code }}</td><td>{{ row.risk | inline_code }}</td><td>{{ row.reversibility | inline_code }}</td><td>{{ row.testCoverageCost | inline_code }}</td><td>{{ row.rolloutCost | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<section data-report-section="recommendation" data-report-field="implementationPlanning.recommendedOption">
|
|
21
|
+
<h2>권장안</h2>
|
|
22
|
+
{{ render_narrative(narrative.recommendationExplanation, "implementationPlanning.userNarrative.recommendationExplanation", evidenceIndex) }}
|
|
23
|
+
<article class="summary-card tone-important"><h3>{{ planning.recommendedOption.name | inline_code }}</h3><p>{{ planning.recommendedOption.coreReason | inline_code }}</p><p>{{ planning.recommendedOption.rationale | inline_code }}</p><p><strong>선택하지 않은 안:</strong> {{ planning.recommendedOption.rejectedSummary | inline_code }}</p></article>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<section data-report-section="stage-map" data-report-field="implementationPlanning.stageMap">
|
|
27
|
+
<h2>단계별 실행 지도</h2>
|
|
28
|
+
{{ render_narrative(narrative.stageStrategy, "implementationPlanning.userNarrative.stageStrategy", evidenceIndex) }}
|
|
29
|
+
{{ figure(stageFigure) }}
|
|
30
|
+
<div class="summary-grid">{% for row in planning.stages %}<article class="summary-card"><h3>Stage {{ row.stage }} · {{ row.title | inline_code }}</h3><p>{{ row.sliceValue | inline_code }}</p><p><strong>완료 조건:</strong> {{ row.acceptance | inline_code }}</p><p><strong>검증:</strong> {{ row.stageValidation | inline_code }}</p></article>{% endfor %}</div>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<section data-report-section="validation-and-rollback">
|
|
34
|
+
<h2>검증과 롤백</h2>
|
|
35
|
+
{{ render_narrative(narrative.validationAndRollback, "implementationPlanning.userNarrative.validationAndRollback", evidenceIndex) }}
|
|
36
|
+
<h3>검증 체크리스트</h3><table data-report-field="implementationPlanning.validationChecklist"><thead><tr><th>ID</th><th>시점</th><th>확인</th><th>명령</th><th>기대 결과</th></tr></thead><tbody>{% for row in planning.validationChecklist %}<tr><td>{{ row.id }}</td><td>{{ row.phase }}</td><td>{{ row.check | inline_code }}</td><td><code>{{ row.commandOrObservation }}</code></td><td>{{ row.expectedOutcome | inline_code }}</td></tr>{% endfor %}</tbody></table>
|
|
37
|
+
<h3>롤백 전략</h3><div class="summary-grid" data-report-field="implementationPlanning.rollbackStrategy">{% for row in planning.rollbackStrategy %}{{ summary_card(row.id ~ " · " ~ (row.triggerSignal | inline_code), row.action, "important") }}{% endfor %}</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<section data-report-section="open-decisions">
|
|
41
|
+
<h2>승인 전 열린 결정</h2>
|
|
42
|
+
<div class="summary-grid">{% for row in openDecisions %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important") }}{% else %}<p>승인을 막는 열린 결정이 없습니다.</p>{% endfor %}</div>
|
|
43
|
+
{{ action_list(humanSummary.actions) }}
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
{{ plan_approval(approval) }}
|
|
47
|
+
{% endblock %}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="delivered-outcome">
|
|
7
|
+
<h2>전달된 결과</h2>
|
|
8
|
+
{{ render_narrative(narrative.deliveredOutcome, "implementation.userNarrative.deliveredOutcome", evidenceIndex) }}
|
|
9
|
+
<p>{{ humanSummary.outcome | inline_code }}</p>
|
|
10
|
+
</section>
|
|
11
|
+
|
|
12
|
+
<section data-report-section="change-map" data-report-field="implementation.diffSummary">
|
|
13
|
+
<h2>무엇이 달라졌는가</h2>
|
|
14
|
+
{{ render_narrative(narrative.changeExplanation, "implementation.userNarrative.changeExplanation", evidenceIndex) }}
|
|
15
|
+
{{ figure(changeFigure) }}
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section data-report-section="requirement-coverage">
|
|
19
|
+
<h2>요구사항 충족</h2>
|
|
20
|
+
<table data-report-field="implementation.requirementCoverage"><thead><tr><th>ID</th><th>요구사항</th><th>구현·증거</th><th>상태</th></tr></thead><tbody>{% for row in implementation.requirementCoverage %}<tr><td>{{ row.id }}</td><td>{{ row.requirement | inline_code }}</td><td>{{ row.coveredBy | inline_code }}</td><td><span class="status status-{{ row.status }}">{{ row.status }}</span></td></tr>{% endfor %}</tbody></table>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
<section data-report-section="validation" data-report-field="implementation.validationEvidence">
|
|
24
|
+
<h2>검증 결과</h2>
|
|
25
|
+
{{ render_narrative(narrative.validationExplanation, "implementation.userNarrative.validationExplanation", evidenceIndex) }}
|
|
26
|
+
<div class="summary-grid">{% for row in implementation.validationEvidence %}{{ summary_card(row.phase ~ " · 종료 코드 " ~ row.exitCode, row.outputTail, "important" if row.exitCode else "neutral") }}{% endfor %}</div>
|
|
27
|
+
<p><strong>독립 검증:</strong> {% for row in implementation.verifierResults %}<span class="status status-{{ row.verdict | lower }}">{{ row.verdict }}</span>{% if not loop.last %}, {% endif %}{% endfor %}</p>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<section data-report-section="remaining-issues">
|
|
31
|
+
<h2>남은 문제</h2>
|
|
32
|
+
{{ render_narrative(narrative.remainingIssues, "implementation.userNarrative.remainingIssues", evidenceIndex) }}
|
|
33
|
+
<div class="summary-grid">{% for row in implementation.outOfPlanEdits %}{{ summary_card(row.id ~ " · " ~ row.file, row.rationale, "important") }}{% else %}<p>계획 밖 수정 사항이 없습니다.</p>{% endfor %}</div>
|
|
34
|
+
</section>
|
|
35
|
+
|
|
36
|
+
<section data-report-section="manual-test" data-report-field="implementation.manualUserTest">
|
|
37
|
+
<h2>수동 사용자 테스트</h2>
|
|
38
|
+
{% if implementation.manualUserTest.applicable %}{% for row in implementation.manualUserTest["items"] %}<article class="summary-card"><h3>{{ row.target | inline_code }}</h3><p><strong>환경:</strong> {{ row.environmentSetup | inline_code }}</p><ol>{% for step in row.steps %}<li>{{ step | inline_code }}</li>{% endfor %}</ol><p><strong>기대 결과:</strong> {{ row.expectedResult | inline_code }}</p></article>{% endfor %}{% else %}<p>{{ implementation.manualUserTest.exemptionReason | inline_code }}</p>{% endif %}
|
|
39
|
+
</section>
|
|
40
|
+
{% endblock %}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
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>
|
|
10
|
+
</section>
|
|
11
|
+
|
|
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>
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section data-report-section="impact-effort">
|
|
19
|
+
<h2>영향도와 노력도</h2>
|
|
20
|
+
{{ figure(matrixFigure) }}
|
|
21
|
+
</section>
|
|
22
|
+
|
|
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>
|
|
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
|
+
</section>
|
|
29
|
+
{% endblock %}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="project-overview">
|
|
7
|
+
<h2>프로젝트 개요</h2>
|
|
8
|
+
{{ render_narrative(narrative.projectOverview, "projectAnalysis.userNarrative.projectOverview", evidenceIndex) }}
|
|
9
|
+
<div class="summary-grid">
|
|
10
|
+
{% for reason in humanSummary.whyItMatters %}{{ summary_card("왜 중요한가 " ~ loop.index, reason, "important") }}{% endfor %}
|
|
11
|
+
</div>
|
|
12
|
+
</section>
|
|
13
|
+
|
|
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) }}
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<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>
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
<section data-report-section="hotspots" data-report-field="projectAnalysis.rankedHotspots">
|
|
32
|
+
<h2>우선 검토할 핫스팟</h2>
|
|
33
|
+
{{ render_narrative(narrative.hotspotExplanation, "projectAnalysis.userNarrative.hotspotExplanation", evidenceIndex) }}
|
|
34
|
+
<div class="summary-grid">
|
|
35
|
+
{% 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>
|
|
37
|
+
{% endfor %}
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<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>
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<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>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section data-report-section="user-actions">
|
|
53
|
+
<h2>다음 행동</h2>
|
|
54
|
+
{{ action_list(humanSummary.actions) }}
|
|
55
|
+
{% if humanSummary.blockers %}<h3>차단 사항</h3><ul>{% for item in humanSummary.blockers %}<li>{{ item | inline_code }}</li>{% endfor %}</ul>{% endif %}
|
|
56
|
+
</section>
|
|
57
|
+
{% endblock %}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="handoff-outcome">
|
|
7
|
+
<h2>인계 결과</h2>
|
|
8
|
+
{{ render_narrative(narrative.handoffOutcome, "releaseHandoff.userNarrative.handoffOutcome", evidenceIndex) }}
|
|
9
|
+
{{ figure(handoffFigure) }}
|
|
10
|
+
</section>
|
|
11
|
+
|
|
12
|
+
<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>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<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>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<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'))) }}
|
|
28
|
+
{% if handoff.mergeConflictProbe.conflictingPaths %}<ul>{% for path in handoff.mergeConflictProbe.conflictingPaths %}<li>{{ path }}</li>{% endfor %}</ul>{% endif %}
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
<section data-report-section="next-action">
|
|
32
|
+
<h2>다음 행동</h2>
|
|
33
|
+
{{ render_narrative(narrative.nextAction, "releaseHandoff.userNarrative.nextAction", evidenceIndex) }}
|
|
34
|
+
<p>{{ handoff.routingRecommendation | inline_code }}</p>
|
|
35
|
+
</section>
|
|
36
|
+
{% endblock %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% extends "html/base.template.html" %}
|
|
2
|
+
{% from "html/macros/layout.html" import narrative as render_narrative, action_list, summary_card %}
|
|
3
|
+
{% from "html/macros/visualizations.html" import figure %}
|
|
4
|
+
|
|
5
|
+
{% block human_content %}
|
|
6
|
+
<section data-report-section="request">
|
|
7
|
+
<h2>요청 이해</h2>
|
|
8
|
+
{{ render_narrative(narrative.requestUnderstanding, "requirementsDiscovery.userNarrative.requestUnderstanding", evidenceIndex) }}
|
|
9
|
+
<h3>사용자 요청 원문</h3>
|
|
10
|
+
<p data-report-field="requirementsDiscovery.requestVerbatim">{{ requirements.requestVerbatim | inline_code }}</p>
|
|
11
|
+
<h3>시스템 해석</h3>
|
|
12
|
+
<p data-report-field="requirementsDiscovery.systemInterpretation">{{ requirements.systemInterpretation | inline_code }}</p>
|
|
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
|
+
</section>
|
|
15
|
+
|
|
16
|
+
<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>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<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>
|
|
28
|
+
{{ figure(routingFigure) }}
|
|
29
|
+
{% if not requirements.taskDependencies %}<p class="empty-state">이 요구사항 태스크는 다른 태스크에 의존하지 않는 단일 작업입니다.</p>{% endif %}
|
|
30
|
+
</section>
|
|
31
|
+
|
|
32
|
+
<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>
|
|
35
|
+
{{ action_list(humanSummary.actions) }}
|
|
36
|
+
</section>
|
|
37
|
+
{% endblock %}
|
|
@@ -249,6 +249,16 @@
|
|
|
249
249
|
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// The output element lives in the footer, many screens below the header
|
|
253
|
+
// export button, so writing text into it alone reads as "nothing happened".
|
|
254
|
+
function showOutput(out, text) {
|
|
255
|
+
if (!out) return;
|
|
256
|
+
out.textContent = text;
|
|
257
|
+
out.setAttribute("tabindex", "-1");
|
|
258
|
+
out.scrollIntoView({ block: "center" });
|
|
259
|
+
out.focus();
|
|
260
|
+
}
|
|
261
|
+
|
|
252
262
|
function exportUserResponse() {
|
|
253
263
|
var runMeta = readRunMeta();
|
|
254
264
|
var entries = collectEntries();
|
|
@@ -258,14 +268,14 @@
|
|
|
258
268
|
try {
|
|
259
269
|
analysisReview = collectAnalysisReview();
|
|
260
270
|
} catch (e) {
|
|
261
|
-
|
|
271
|
+
showOutput(out, e.message || String(e));
|
|
262
272
|
showDismiss(true);
|
|
263
273
|
return "";
|
|
264
274
|
}
|
|
265
275
|
var md = buildUserResponseMarkdown(
|
|
266
276
|
runMeta, entries, isoNowUtc(), approval, analysisReview
|
|
267
277
|
);
|
|
268
|
-
|
|
278
|
+
showOutput(out, md);
|
|
269
279
|
showDismiss(true);
|
|
270
280
|
// Nothing answered yet — show the empty serialisation as feedback but
|
|
271
281
|
// don't download a useless file.
|
|
@@ -315,7 +325,10 @@
|
|
|
315
325
|
if (/Reader Summary|Verdict Card|Clarification Items|Recommended Next Steps|Follow-up Tasks/.test(text)) {
|
|
316
326
|
return "action";
|
|
317
327
|
}
|
|
318
|
-
|
|
328
|
+
// "토큰 사용량" is tokenSummary.heading from templates/reports/i18n/ko.json —
|
|
329
|
+
// the only audit heading the ko dictionary translates away from its English
|
|
330
|
+
// form. tests/report/test_report_views.py pins the two in sync.
|
|
331
|
+
if (/Evidence|Cross Verification|Execution Status|Token Usage|토큰 사용량|Plan Body Verification|Round History/.test(text)) {
|
|
319
332
|
return "audit";
|
|
320
333
|
}
|
|
321
334
|
return "detail";
|
|
@@ -333,7 +346,11 @@
|
|
|
333
346
|
current = "action";
|
|
334
347
|
continue;
|
|
335
348
|
}
|
|
336
|
-
|
|
349
|
+
// Only h2 opens a reader section. The single h1 is the report title, and
|
|
350
|
+
// the run-metadata list and table of contents that trail it are document
|
|
351
|
+
// chrome — classifying them would hide the title in action mode, because
|
|
352
|
+
// the title text ("… Cross Verification Final Report") reads as audit.
|
|
353
|
+
if ((node.tagName || "") === "H2") {
|
|
337
354
|
current = readerSectionKind(node.textContent || "");
|
|
338
355
|
}
|
|
339
356
|
node.setAttribute("data-reader-section", current);
|
|
@@ -131,8 +131,8 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
131
131
|
- Should the same task key continue to be used after this run?
|
|
132
132
|
- What is the likely work category for this task right now?
|
|
133
133
|
- Which lifecycle phase should probably happen next after this run?
|
|
134
|
-
- What prior decision or report should
|
|
135
|
-
- What related tasks should
|
|
134
|
+
- What prior decision or report should the Okstra lead compare against?
|
|
135
|
+
- What related tasks should the Okstra lead keep in mind but not merge with this task?
|
|
136
136
|
- Which config files and deployment manifests carry the expected state for this task?
|
|
137
137
|
|
|
138
138
|
## Phase Boundary
|
|
@@ -145,27 +145,27 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
145
145
|
|
|
146
146
|
## Available MCP Servers
|
|
147
147
|
|
|
148
|
-
The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array and rendered into the
|
|
148
|
+
The MCP servers available to this run are declared in `.okstra/project.json`'s `mcpServers` array and rendered into the Okstra lead's launch prompt under `## Available MCP Servers`. The lead is responsible for forwarding the rendered list verbatim into worker prompts (Phase 2) so each assignment can determine whether its selected runner exposes the named tools.
|
|
149
149
|
|
|
150
150
|
To declare servers, add entries shaped `{ "name": "<server>", "description": "...", "tools": ["..."], "notes": "..." }` to that array. If the array is empty or absent, treat MCP as unavailable for this run.
|
|
151
151
|
|
|
152
152
|
How to invoke (worker-by-worker):
|
|
153
153
|
|
|
154
|
-
- **
|
|
155
|
-
- **
|
|
154
|
+
- **Native-session assignments**: invoke an MCP tool directly by its tool name (e.g. `mcp__<server>__<tool>`) only when the selected host adapter exposes it. Do not call an MCP tool name through a shell.
|
|
155
|
+
- **CLI-wrapper assignments**: use only the MCP or tool configuration explicitly supported and forwarded by that provider wrapper. If the wrapper has no matching MCP configuration, record `MCP not available in this CLI` in `Missing Information or Assumptions`; do not attempt a shell fallback.
|
|
156
156
|
- All workers: cite the exact server, tool, and SELECT (or `WHERE` filters) used in the result file. Tool-call failures must be logged in the worker's `*-errors.json` (commandKind `mcp_call`) so the lead can decide whether to retry under a different worker.
|
|
157
157
|
|
|
158
158
|
Usage policy:
|
|
159
159
|
|
|
160
160
|
- **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
|
|
161
161
|
- **`implementation` phase**: read-only MCP queries are permitted as cross-checks; MCP MUST NOT be used as a write path even if a write tool becomes available — schema/data mutations belong in the codebase migration files reviewed by humans.
|
|
162
|
-
- **
|
|
162
|
+
- **CLI-wrapper workers**: can use these MCP servers only if their provider CLI configuration mirrors the same servers. If not configured, the worker should record `MCP not available in this CLI` in `Missing Information or Assumptions` rather than guessing.
|
|
163
163
|
- **Forbidden**: connecting to non-listed databases, running anything that mutates state (server is read-only — flagged write attempts are a contract violation), persisting query results outside the run's own artifact directories.
|
|
164
164
|
- **Limits**: `maxRows: 1000` per query (server-enforced). For larger scans, page via `WHERE` predicates and document the strategy.
|
|
165
165
|
|
|
166
166
|
If MCP evidence contradicts a claim sourced from documentation, code, or prior reports, flag the conflict explicitly in the final report and prefer the live MCP evidence unless the brief says otherwise.
|
|
167
167
|
|
|
168
|
-
## Notes for
|
|
168
|
+
## Notes for Okstra Lead
|
|
169
169
|
|
|
170
170
|
- Which workers should be used?
|
|
171
171
|
- Should all workers receive the same brief or different emphasis?
|
|
@@ -23,6 +23,8 @@ per line to stderr.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
+
import hashlib
|
|
27
|
+
import json
|
|
26
28
|
import re
|
|
27
29
|
import sys
|
|
28
30
|
from pathlib import Path
|
|
@@ -43,6 +45,8 @@ from okstra_ctl.report_views import ( # noqa: E402
|
|
|
43
45
|
source_digest,
|
|
44
46
|
)
|
|
45
47
|
from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
|
|
48
|
+
from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
|
|
49
|
+
from okstra_ctl.report_contract import TASK_TYPE_REQUIRED_HUMAN_FIELDS # noqa: E402
|
|
46
50
|
|
|
47
51
|
|
|
48
52
|
_EXTERNAL_URL_RE = re.compile(
|
|
@@ -66,11 +70,86 @@ _ANALYSIS_REVIEW_STATUSES = ["accepted", "rejected", "revision-requested"]
|
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def _main_body(html_text: str) -> str:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if start < 0 or end < 0:
|
|
73
|
+
match = re.search(r"<main[^>]*>(?P<body>.*?)</main>", html_text, re.DOTALL)
|
|
74
|
+
if match is None:
|
|
72
75
|
return html_text
|
|
73
|
-
return
|
|
76
|
+
return match.group("body")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sha256(path: Path) -> str:
|
|
80
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _digest_from_run_meta(html_text: str, key: str) -> str:
|
|
84
|
+
match = re.search(rf'"{re.escape(key)}"\s*:\s*"([0-9a-f]{{64}})"', html_text)
|
|
85
|
+
return match.group(1) if match else ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_v2_data(report_path: Path) -> tuple[Path, dict] | None:
|
|
89
|
+
data_path = final_report_data_path(report_path)
|
|
90
|
+
if not data_path.is_file():
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
94
|
+
except (OSError, json.JSONDecodeError):
|
|
95
|
+
return None
|
|
96
|
+
if isinstance(data, dict) and data.get("schemaVersion") == "2.0":
|
|
97
|
+
return data_path, data
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
|
|
102
|
+
html_path = html_view_path(report_path)
|
|
103
|
+
if not html_path.is_file():
|
|
104
|
+
return [f"missing html artifact: {html_path}"]
|
|
105
|
+
html_text = html_path.read_text(encoding="utf-8")
|
|
106
|
+
main_body = _main_body(html_text)
|
|
107
|
+
failures: list[str] = []
|
|
108
|
+
|
|
109
|
+
for key, path in (
|
|
110
|
+
("source-data-sha256", data_path),
|
|
111
|
+
("source-md-sha256", report_path),
|
|
112
|
+
):
|
|
113
|
+
recorded = _digest_from_run_meta(html_text, key)
|
|
114
|
+
expected = _sha256(path)
|
|
115
|
+
if not recorded:
|
|
116
|
+
failures.append(f"html missing {key} in run-meta")
|
|
117
|
+
elif recorded != expected:
|
|
118
|
+
failures.append(f"stale html: {key} does not match current source")
|
|
119
|
+
|
|
120
|
+
task_type = data.get("header", {}).get("taskType", "")
|
|
121
|
+
if f'data-task-template="{task_type}"' not in html_text:
|
|
122
|
+
failures.append(f"v2 html task template mismatch for {task_type}")
|
|
123
|
+
actual_fields = set(re.findall(r'data-report-field="([^"]+)"', main_body))
|
|
124
|
+
for field in TASK_TYPE_REQUIRED_HUMAN_FIELDS.get(task_type, ()):
|
|
125
|
+
if field not in actual_fields:
|
|
126
|
+
failures.append(f"missing human field: {field}")
|
|
127
|
+
|
|
128
|
+
if "data-audit-field=" in main_body:
|
|
129
|
+
failures.append("audit field inside human main")
|
|
130
|
+
node_ids = set(re.findall(r'data-node-id="([^"]+)"', html_text))
|
|
131
|
+
fallback_ids = set(re.findall(r'data-fallback-id="([^"]+)"', html_text))
|
|
132
|
+
if node_ids != fallback_ids:
|
|
133
|
+
failures.append(
|
|
134
|
+
f"visualization ID mismatch: svg={sorted(node_ids)}, fallback={sorted(fallback_ids)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
data_ids = []
|
|
138
|
+
for row in data.get("clarificationItems", []):
|
|
139
|
+
if not isinstance(row, dict):
|
|
140
|
+
continue
|
|
141
|
+
response_id = row.get("id")
|
|
142
|
+
if isinstance(response_id, str) and re.fullmatch(r"C-\d+", response_id):
|
|
143
|
+
data_ids.append(response_id)
|
|
144
|
+
data_ids.sort()
|
|
145
|
+
html_ids = sorted(set(_RESPONSE_ID_ATTR_RE.findall(html_text)))
|
|
146
|
+
if data_ids != html_ids:
|
|
147
|
+
failures.append(
|
|
148
|
+
f"Response ID mismatch: data.json has {data_ids}, HTML has {html_ids}"
|
|
149
|
+
)
|
|
150
|
+
if _EXTERNAL_URL_RE.search(html_text):
|
|
151
|
+
failures.append("html contains external URL in script/link/img — must be self-contained")
|
|
152
|
+
return failures
|
|
74
153
|
|
|
75
154
|
|
|
76
155
|
def _no_form_sections(html_body: str) -> list[str]:
|
|
@@ -128,6 +207,9 @@ def validate(report_path: Path) -> list[str]:
|
|
|
128
207
|
return [f"final-report not found: {report_path}"]
|
|
129
208
|
|
|
130
209
|
md = report_path.read_text(encoding="utf-8")
|
|
210
|
+
v2 = _load_v2_data(report_path)
|
|
211
|
+
if v2 is not None:
|
|
212
|
+
return _validate_v2(report_path, v2[0], v2[1])
|
|
131
213
|
html_path = html_view_path(report_path)
|
|
132
214
|
# §1 헤딩이 있는데 파싱이 실패하면 md_ids 가 빈 []이 되어 "clarification 없음
|
|
133
215
|
# → skip" 으로 흘러 HTML form parity 게이트가 조용히 열린다. fail-closed.
|