vibepro 0.1.0-alpha.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 (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
@@ -0,0 +1,1035 @@
1
+ import { localizedText } from './language.js';
2
+
3
+ export function renderPrPrepareHtml({ preparation, bodyPath, gateDagPath, splitPlanPath, language = 'ja' }) {
4
+ const gateDag = preparation.pr_context.gate_dag;
5
+ const requirement = preparation.pr_context.requirement_consistency;
6
+ const splitPlan = preparation.split_plan;
7
+ const executionGate = preparation.pr_context.execution_gate ?? preparation.gate_status?.execution_gate ?? null;
8
+ const agentReviews = preparation.pr_context.agent_reviews ?? null;
9
+ const labels = prPrepareLabels(language);
10
+ const cards = [
11
+ metricCard(labels.scope, preparation.scope.status, preparation.scope.recommended_strategy),
12
+ metricCard('Gate DAG', gateDag.overall_status, localizedText(language, {
13
+ ja: `${gateDag.summary.needs_evidence_count}件 未解決`,
14
+ en: `${gateDag.summary.needs_evidence_count} unresolved`
15
+ })),
16
+ metricCard(labels.executionGate, executionGate?.status ?? 'unknown', executionGate?.pr_create_allowed ? labels.prCreateAllowed : labels.prCreateBlocked),
17
+ metricCard(labels.agentReviews, agentReviews?.status ?? 'not_generated', localizedText(language, {
18
+ ja: `${(agentReviews?.summary?.unmet_required_review_count ?? 0) + (agentReviews?.summary?.unmet_checkpoint_review_count ?? 0)}件 未充足`,
19
+ en: `${(agentReviews?.summary?.unmet_required_review_count ?? 0) + (agentReviews?.summary?.unmet_checkpoint_review_count ?? 0)} unmet`
20
+ })),
21
+ metricCard(labels.requirement, requirement?.status ?? 'not_generated', localizedText(language, {
22
+ ja: `${requirement?.summary?.contradiction_count ?? 0}件 矛盾`,
23
+ en: `${requirement?.summary?.contradiction_count ?? 0} contradictions`
24
+ })),
25
+ metricCard(labels.splitPlan, splitPlan.status, localizedText(language, {
26
+ ja: `${splitPlan.lanes.length}レーン`,
27
+ en: `${splitPlan.lanes.length} lanes`
28
+ }))
29
+ ].join('');
30
+ const flow = renderFlow([
31
+ flowStep(labels.story, preparation.story.story_id, preparation.pr_context.story_source.path ? 'present' : 'transient'),
32
+ flowStep(labels.architecture, preparation.pr_context.architecture_decision, gateStatus(gateDag, 'architecture')),
33
+ flowStep(labels.spec, localizedText(language, {
34
+ ja: `${preparation.file_groups.specifications.count}ファイル`,
35
+ en: `${preparation.file_groups.specifications.count} files`
36
+ }), gateStatus(gateDag, 'spec')),
37
+ flowStep(labels.code, localizedText(language, {
38
+ ja: `${preparation.file_groups.source.count}ファイル`,
39
+ en: `${preparation.file_groups.source.count} files`
40
+ }), gateStatus(gateDag, 'code')),
41
+ flowStep(labels.gates, gateDag.overall_status, gateDag.overall_status)
42
+ ]);
43
+ const risks = renderCards(localizedText(language, { ja: '人間レビューの焦点', en: 'Human Review Focus' }), [
44
+ ...preparation.pr_context.risks.map((risk) => ({ title: 'Risk', detail: risk, tone: 'danger' })),
45
+ ...preparation.pr_context.review_points.map((point) => ({ title: 'Review', detail: point, tone: 'info' }))
46
+ ]);
47
+ const requirementSection = renderRequirementPanel(requirement, language);
48
+ const networkSection = renderNetworkContractPanel(preparation.pr_context.network_contracts, language);
49
+ const fileGroups = renderFileGroups(preparation.file_groups, language);
50
+ const graphSummary = renderGraphSummary(splitPlan.graph_context, language);
51
+ const artifacts = renderKeyValueTable([
52
+ ['PR body draft', bodyPath],
53
+ ['Gate DAG HTML', gateDagPath],
54
+ ['Split Plan HTML', splitPlanPath]
55
+ ]);
56
+ const nextCommands = renderCommandList(preparation.next_commands);
57
+
58
+ return renderDocument({
59
+ title: 'VibePro PR Prepare',
60
+ reportType: 'pr-prepare',
61
+ generatedAt: preparation.created_at,
62
+ language,
63
+ body: `
64
+ <section class="hero" data-overall-status="${escapeAttr(gateDag.overall_status)}">
65
+ <div>
66
+ <p class="eyebrow">${escapeHtml(labels.eyebrow)}</p>
67
+ <h2>${escapeHtml(preparation.story.title)}</h2>
68
+ <p class="muted">${escapeHtml(preparation.story.story_id)} / ${escapeHtml(preparation.git.base_ref)} -> ${escapeHtml(preparation.git.head_ref)}</p>
69
+ </div>
70
+ <span class="${statusClass(gateDag.overall_status)}">${escapeHtml(gateDag.overall_status)}</span>
71
+ </section>
72
+ <section class="metrics">${cards}</section>
73
+ ${renderPrPrepareGuide({ preparation, bodyPath, gateDagPath, splitPlanPath, language })}
74
+ <section>
75
+ <h2>${labels.flowTitle}</h2>
76
+ ${flow}
77
+ </section>
78
+ ${risks}
79
+ ${renderExecutionGatePanel(executionGate, language)}
80
+ ${renderAgentReviewPanel(agentReviews, language)}
81
+ ${requirementSection}
82
+ ${networkSection}
83
+ <section>
84
+ <h2>${escapeHtml(labels.graphifyImpact)}</h2>
85
+ ${graphSummary}
86
+ </section>
87
+ <section>
88
+ <h2>${escapeHtml(labels.changedFileGroups)}</h2>
89
+ ${fileGroups}
90
+ </section>
91
+ <section class="grid-2">
92
+ <div>
93
+ <h2>${escapeHtml(localizedText(language, { ja: '成果物', en: 'Artifacts' }))}</h2>
94
+ ${artifacts}
95
+ </div>
96
+ <div>
97
+ <h2>${escapeHtml(localizedText(language, { ja: '次のコマンド', en: 'Next Commands' }))}</h2>
98
+ ${nextCommands}
99
+ </div>
100
+ </section>
101
+ `
102
+ });
103
+ }
104
+
105
+ export function renderGateDagHtml(gateDag, options = {}) {
106
+ const requiredGates = gateDag.nodes.filter((node) => node.required);
107
+ const unresolved = requiredGates.filter((node) => isUnresolvedStatus(node.status));
108
+ return renderDocument({
109
+ title: 'VibePro Gate DAG',
110
+ reportType: 'gate-dag',
111
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
112
+ language: options.language ?? 'ja',
113
+ body: `
114
+ <section class="hero" data-overall-status="${escapeAttr(gateDag.overall_status)}">
115
+ <div>
116
+ <p class="eyebrow">Completion dependency map</p>
117
+ <h2>${escapeHtml(gateDag.story_id)}</h2>
118
+ <p class="muted">${escapeHtml(gateDag.model)}</p>
119
+ </div>
120
+ <span class="${statusClass(gateDag.overall_status)}">${escapeHtml(gateDag.overall_status)}</span>
121
+ </section>
122
+ <section class="metrics">
123
+ ${metricCard('Acceptance', gateDag.summary.acceptance_criteria_count, 'criteria')}
124
+ ${metricCard('Required Gates', gateDag.summary.required_gate_count, 'gates')}
125
+ ${metricCard('Needs Evidence', gateDag.summary.needs_evidence_count, 'unresolved')}
126
+ ${metricCard('Requirement', gateDag.summary.requirement_status, 'status')}
127
+ </section>
128
+ ${renderCards('Unresolved Gates', unresolved.map((node) => ({
129
+ title: node.label ?? node.id,
130
+ detail: node.reason ?? node.command ?? node.id,
131
+ meta: `${node.id} / ${node.status}`,
132
+ tone: toneForStatus(node.status)
133
+ })))}
134
+ <section>
135
+ <h2>Visual DAG</h2>
136
+ ${renderGateDagSvg(gateDag)}
137
+ </section>
138
+ <section>
139
+ <h2>Gate Nodes</h2>
140
+ ${renderNodeGrid(gateDag.nodes)}
141
+ </section>
142
+ `
143
+ });
144
+ }
145
+
146
+ export function renderSplitPlanHtml(splitPlan, options = {}) {
147
+ const lanePlans = new Map(splitPlan.stacked_gate_plan.lane_plans.map((lane) => [lane.lane_id, lane]));
148
+ const laneBoard = splitPlan.lanes
149
+ .slice()
150
+ .sort((a, b) => a.order - b.order)
151
+ .map((lane) => renderLaneCard(lane, lanePlans.get(lane.id)))
152
+ .join('');
153
+ return renderDocument({
154
+ title: 'VibePro PR Split Plan',
155
+ reportType: 'split-plan',
156
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
157
+ language: options.language ?? 'ja',
158
+ body: `
159
+ <section class="hero" data-split-status="${escapeAttr(splitPlan.status)}">
160
+ <div>
161
+ <p class="eyebrow">Review lane board</p>
162
+ <h2>${escapeHtml(splitPlan.story_id)}</h2>
163
+ <p class="muted">${escapeHtml(splitPlan.recommended_strategy)}</p>
164
+ </div>
165
+ <span class="${statusClass(splitPlan.status)}">${escapeHtml(splitPlan.status)}</span>
166
+ </section>
167
+ <section class="metrics">
168
+ ${metricCard('Lanes', splitPlan.lanes.length, 'review lanes')}
169
+ ${metricCard('Graphify', splitPlan.graph_context.available ? 'available' : 'missing', `${splitPlan.graph_context.related_file_count} related`)}
170
+ ${metricCard('Cumulative Gates', splitPlan.stacked_gate_plan.summary.cumulative_gate_count, 'lanes')}
171
+ ${metricCard('Final Validation', splitPlan.stacked_gate_plan.final_validation.required ? 'required' : 'not required', splitPlan.stacked_gate_plan.final_validation.trigger)}
172
+ </section>
173
+ <section>
174
+ <h2>PR Lanes</h2>
175
+ <div class="lane-board">${laneBoard}</div>
176
+ </section>
177
+ <section class="grid-2">
178
+ <div>
179
+ <h2>Merge Order</h2>
180
+ ${renderOrderedList(splitPlan.merge_order)}
181
+ </div>
182
+ <div>
183
+ <h2>Rationale</h2>
184
+ ${renderList(splitPlan.rationale)}
185
+ </div>
186
+ </section>
187
+ <section>
188
+ <h2>Graphify Investigation Scope</h2>
189
+ ${renderGraphImpactTable(splitPlan.graph_context.impact_by_file)}
190
+ </section>
191
+ <section>
192
+ <h2>Next Actions</h2>
193
+ ${renderCommandList(splitPlan.next_actions.map((action) => typeof action === 'string' ? action : action.command))}
194
+ </section>
195
+ `
196
+ });
197
+ }
198
+
199
+ export function renderPrCreateHtml(execution, options = {}) {
200
+ const results = execution.results.length === 0
201
+ ? [{ command: 'dry-run', exit_code: 0, stdout: '', stderr: '' }]
202
+ : execution.results;
203
+ return renderDocument({
204
+ title: 'VibePro PR Create',
205
+ reportType: 'pr-create',
206
+ generatedAt: execution.created_at,
207
+ language: options.language ?? execution.output?.language ?? 'ja',
208
+ body: `
209
+ <section class="hero" data-dry-run="${escapeAttr(String(execution.dry_run))}">
210
+ <div>
211
+ <p class="eyebrow">PR creation audit</p>
212
+ <h2>${escapeHtml(execution.title)}</h2>
213
+ <p class="muted">${escapeHtml(execution.base)} -> ${escapeHtml(execution.head)}</p>
214
+ </div>
215
+ <span class="${statusClass(execution.dry_run ? 'dry_run' : 'executed')}">${execution.dry_run ? 'dry-run' : 'executed'}</span>
216
+ </section>
217
+ <section class="metrics">
218
+ ${metricCard('Story', execution.story.story_id, execution.task_context?.task?.id ?? 'no task')}
219
+ ${metricCard('PR URL', execution.pr_url ?? '-', execution.dry_run ? 'not created' : 'created')}
220
+ ${metricCard('Gate Override', execution.gate_override?.allowed ? 'allowed' : 'none', execution.gate_override?.reason ?? '-')}
221
+ ${metricCard('Commands', execution.commands.length, 'planned')}
222
+ </section>
223
+ ${renderGateOverridePanel(execution.gate_override)}
224
+ <section>
225
+ <h2>Command Timeline</h2>
226
+ <div class="timeline">
227
+ ${results.map((item, index) => `
228
+ <article class="timeline-item" data-command-index="${index}">
229
+ <strong>${escapeHtml(item.command)}</strong>
230
+ <span class="${statusClass(item.exit_code === 0 ? 'pass' : 'failed')}">exit=${escapeHtml(item.exit_code)}</span>
231
+ ${item.stdout ? `<pre>${escapeHtml(item.stdout)}</pre>` : ''}
232
+ ${item.stderr ? `<pre>${escapeHtml(item.stderr)}</pre>` : ''}
233
+ </article>
234
+ `).join('')}
235
+ </div>
236
+ </section>
237
+ <section>
238
+ <h2>Warnings</h2>
239
+ ${renderList(execution.warnings.length > 0 ? execution.warnings : ['なし'])}
240
+ </section>
241
+ `
242
+ });
243
+ }
244
+
245
+ export function renderPrMergeHtml(merge, options = {}) {
246
+ const results = merge.results.length === 0
247
+ ? [{ command: 'dry-run', exit_code: 0, stdout: '', stderr: '' }]
248
+ : merge.results;
249
+ return renderDocument({
250
+ title: 'VibePro Execute Merge',
251
+ reportType: 'pr-merge',
252
+ generatedAt: merge.created_at,
253
+ language: options.language ?? merge.output?.language ?? 'ja',
254
+ body: `
255
+ <section class="hero" data-dry-run="${escapeAttr(String(merge.dry_run))}">
256
+ <div>
257
+ <p class="eyebrow">PR merge audit</p>
258
+ <h2>${escapeHtml(merge.story?.story_id ?? '-')}</h2>
259
+ <p class="muted">${escapeHtml(merge.pr?.url ?? merge.pr?.selector ?? '-')}</p>
260
+ </div>
261
+ <span class="${statusClass(merge.status)}">${escapeHtml(merge.status)}</span>
262
+ </section>
263
+ <section class="metrics">
264
+ ${metricCard('Strategy', merge.strategy, merge.delete_branch ? 'delete branch' : 'keep branch')}
265
+ ${metricCard('Base', merge.base ?? '-', merge.pr?.base_ref_name ?? '-')}
266
+ ${metricCard('Merge commit', merge.merge_commit_sha ?? '-', merge.merged_at ?? 'not merged')}
267
+ ${metricCard('Checks', merge.pr?.checks?.length ?? 0, merge.preconditions?.checks_ready?.status ?? '-')}
268
+ </section>
269
+ <section class="grid-2">
270
+ <div>
271
+ <h2>Preconditions</h2>
272
+ ${renderList([
273
+ `gate_ready: ${merge.preconditions?.gate_ready ? 'passed' : 'blocked'}`,
274
+ `clean_worktree: ${merge.preconditions?.clean_worktree ? 'passed' : 'blocked'}`,
275
+ `base_freshness: ${merge.preconditions?.base_freshness?.status ?? '-'}`,
276
+ `remote_head_match: ${merge.preconditions?.remote_head_match?.status ?? '-'}`,
277
+ `checks_ready: ${merge.preconditions?.checks_ready?.status ?? '-'}`,
278
+ `review_policy: ${merge.preconditions?.review_policy?.status ?? '-'}`,
279
+ `open_pull_request: ${merge.preconditions?.open_pull_request?.status ?? '-'}`
280
+ ])}
281
+ </div>
282
+ <div>
283
+ <h2>Warnings</h2>
284
+ ${renderList(merge.warnings?.length ? merge.warnings : ['なし'])}
285
+ </div>
286
+ </section>
287
+ <section>
288
+ <h2>Check Rollup</h2>
289
+ ${renderList((merge.pr?.checks ?? []).map((check) => `${check.name}: ${check.status}/${check.conclusion || '-'}`))}
290
+ </section>
291
+ <section>
292
+ <h2>Command Timeline</h2>
293
+ <div class="timeline">
294
+ ${results.map((item, index) => `
295
+ <article class="timeline-item" data-command-index="${index}">
296
+ <strong>${escapeHtml(item.command)}</strong>
297
+ <span class="${statusClass(item.exit_code === 0 ? 'pass' : 'failed')}">exit=${escapeHtml(item.exit_code)}</span>
298
+ ${item.stdout ? `<pre>${escapeHtml(item.stdout)}</pre>` : ''}
299
+ ${item.stderr ? `<pre>${escapeHtml(item.stderr)}</pre>` : ''}
300
+ </article>
301
+ `).join('')}
302
+ </div>
303
+ </section>
304
+ `
305
+ });
306
+ }
307
+
308
+ function renderPrPrepareGuide({ preparation, bodyPath, gateDagPath, splitPlanPath, language }) {
309
+ const gateDag = preparation.pr_context.gate_dag;
310
+ const requirement = preparation.pr_context.requirement_consistency;
311
+ const requirementHint = buildRequirementGuide(requirement, language);
312
+ const unresolved = gateDag.summary?.needs_evidence_count ?? 0;
313
+ const agentHandoff = [
314
+ bodyPath,
315
+ gateDagPath,
316
+ splitPlanPath
317
+ ].filter(Boolean).join(' / ');
318
+ return `
319
+ <section>
320
+ <h2>${escapeHtml(localizedText(language, { ja: 'まず見る場所', en: 'Where To Look First' }))}</h2>
321
+ <div class="cards">
322
+ <article class="card ${escapeAttr(unresolved > 0 ? 'warn' : 'good')}">
323
+ <h3>${escapeHtml(localizedText(language, { ja: '1. Gateの未解決', en: '1. Gate blockers' }))}</h3>
324
+ <p>${escapeHtml(localizedText(language, {
325
+ ja: unresolved > 0
326
+ ? `${unresolved}件の未解決Gateがあります。Execution GateとUnresolved Gatesから先に確認してください。`
327
+ : '未解決Gateはありません。PR作成前にscopeとbranchだけ確認してください。',
328
+ en: unresolved > 0
329
+ ? `${unresolved} unresolved gate(s). Start from Execution Gate and Unresolved Gates.`
330
+ : 'No unresolved gates. Check scope and branch before PR creation.'
331
+ }))}</p>
332
+ </article>
333
+ <article class="card info">
334
+ <h3>${escapeHtml(localizedText(language, { ja: '2. AIエージェントへの渡し方', en: '2. Agent handoff' }))}</h3>
335
+ <p>${escapeHtml(localizedText(language, {
336
+ ja: `実装エージェントには ${agentHandoff} を渡してください。pr-body.mdが要約、gate-dagが完了条件、split-planがPR分割方針です。`,
337
+ en: `Hand ${agentHandoff} to the coding agent. pr-body.md is the summary, gate-dag is the completion contract, and split-plan is the PR split guide.`
338
+ }))}</p>
339
+ </article>
340
+ <article class="card ${escapeAttr(requirementHint.tone)}">
341
+ <h3>${escapeHtml(localizedText(language, { ja: '3. Requirement Consistency', en: '3. Requirement Consistency' }))}</h3>
342
+ <p>${escapeHtml(requirementHint.text)}</p>
343
+ </article>
344
+ </div>
345
+ </section>
346
+ `;
347
+ }
348
+
349
+ function prPrepareLabels(language) {
350
+ return {
351
+ eyebrow: localizedText(language, {
352
+ ja: 'StoryからPR前確認までのレビュー成果物',
353
+ en: 'Story-driven review artifact'
354
+ }),
355
+ scope: localizedText(language, { ja: 'スコープ', en: 'Scope' }),
356
+ executionGate: localizedText(language, { ja: '実行Gate', en: 'Execution Gate' }),
357
+ agentReviews: localizedText(language, { ja: 'Agent Review', en: 'Agent Reviews' }),
358
+ requirement: localizedText(language, { ja: '要件整合性', en: 'Requirement' }),
359
+ splitPlan: localizedText(language, { ja: '分割計画', en: 'Split Plan' }),
360
+ prCreateAllowed: localizedText(language, { ja: 'PR作成可能', en: 'pr create allowed' }),
361
+ prCreateBlocked: localizedText(language, { ja: 'PR作成ブロック', en: 'pr create blocked' }),
362
+ story: localizedText(language, { ja: 'Story', en: 'Story' }),
363
+ architecture: localizedText(language, { ja: 'Architecture', en: 'Architecture' }),
364
+ spec: localizedText(language, { ja: 'Spec', en: 'Spec' }),
365
+ code: localizedText(language, { ja: 'Code', en: 'Code' }),
366
+ gates: localizedText(language, { ja: 'Gate', en: 'Gates' }),
367
+ flowTitle: localizedText(language, {
368
+ ja: 'Story -> Architecture -> Spec -> Code -> Gate',
369
+ en: 'Story -> Architecture -> Spec -> Code -> Gate'
370
+ }),
371
+ graphifyImpact: localizedText(language, { ja: 'Graphify影響範囲', en: 'Graphify Impact' }),
372
+ changedFileGroups: localizedText(language, { ja: '変更ファイル分類', en: 'Changed File Groups' })
373
+ };
374
+ }
375
+
376
+ function renderDocument({ title, reportType, generatedAt, body, language = 'ja' }) {
377
+ return trimTrailingWhitespace(`<!doctype html>
378
+ <html lang="${escapeAttr(language)}">
379
+ <head>
380
+ <meta charset="utf-8">
381
+ <meta name="viewport" content="width=device-width, initial-scale=1">
382
+ <title>${escapeHtml(title)}</title>
383
+ <style>${baseCss()}</style>
384
+ </head>
385
+ <body>
386
+ <main data-vibepro-report="${escapeAttr(reportType)}">
387
+ <header class="topbar">
388
+ <div>
389
+ <h1>${escapeHtml(title)}</h1>
390
+ <p class="meta">Generated by VibePro at ${escapeHtml(generatedAt)}</p>
391
+ </div>
392
+ <span class="brand">VibePro</span>
393
+ </header>
394
+ ${body}
395
+ </main>
396
+ </body>
397
+ </html>
398
+ `);
399
+ }
400
+
401
+ function trimTrailingWhitespace(html) {
402
+ return html
403
+ .split('\n')
404
+ .map((line) => line.trimEnd())
405
+ .join('\n');
406
+ }
407
+
408
+ function baseCss() {
409
+ return `
410
+ :root {
411
+ color-scheme: light dark;
412
+ --bg: #f6f7f9;
413
+ --fg: #14171f;
414
+ --muted: #667085;
415
+ --panel: #ffffff;
416
+ --panel-2: #f0f3f8;
417
+ --border: #d7dee8;
418
+ --good: #0f766e;
419
+ --warn: #a16207;
420
+ --danger: #b42318;
421
+ --info: #1d4ed8;
422
+ --neutral: #475467;
423
+ --code: #eef2f7;
424
+ }
425
+ @media (prefers-color-scheme: dark) {
426
+ :root {
427
+ --bg: #0b1220;
428
+ --fg: #e8edf5;
429
+ --muted: #9aa8bc;
430
+ --panel: #101827;
431
+ --panel-2: #172235;
432
+ --border: #2d3a4f;
433
+ --code: #1d2939;
434
+ }
435
+ }
436
+ * { box-sizing: border-box; }
437
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
438
+ main { width: min(1220px, calc(100% - 24px)); margin: 16px auto 40px; }
439
+ .topbar, section, article.card, .lane, .timeline-item {
440
+ background: var(--panel);
441
+ border: 1px solid var(--border);
442
+ border-radius: 8px;
443
+ }
444
+ .topbar { display: flex; justify-content: space-between; gap: 16px; align-items: center; padding: 18px 20px; margin-bottom: 12px; }
445
+ .brand { font-weight: 700; color: var(--info); }
446
+ section { padding: 18px; margin: 12px 0; }
447
+ h1, h2, h3 { margin: 0 0 10px; line-height: 1.25; letter-spacing: 0; }
448
+ h1 { font-size: 24px; }
449
+ h2 { font-size: 18px; }
450
+ h3 { font-size: 15px; }
451
+ .meta, .muted, .eyebrow { color: var(--muted); }
452
+ .meta, .eyebrow { margin: 0; font-size: 12px; }
453
+ .eyebrow { text-transform: uppercase; letter-spacing: 0.08em; }
454
+ .hero { display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; }
455
+ .hero h2 { font-size: 22px; margin: 4px 0; }
456
+ .metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; background: transparent; border: 0; padding: 0; }
457
+ .metric { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; min-height: 92px; }
458
+ .metric span { display: block; color: var(--muted); font-size: 12px; }
459
+ .metric strong { display: block; margin: 6px 0; font-size: 20px; overflow-wrap: anywhere; }
460
+ .grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; background: transparent; border: 0; padding: 0; }
461
+ .flow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
462
+ .flow-step { position: relative; padding: 14px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; min-height: 110px; overflow-wrap: anywhere; }
463
+ .flow-step:not(:last-child)::after { content: "->"; position: absolute; right: -12px; top: 44px; color: var(--muted); }
464
+ .cards, .node-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
465
+ .card { padding: 14px; overflow-wrap: anywhere; }
466
+ .card p { margin: 6px 0 0; }
467
+ .lane-board { display: grid; grid-template-columns: repeat(4, minmax(240px, 1fr)); gap: 10px; overflow-x: auto; }
468
+ .lane { padding: 14px; min-width: 240px; }
469
+ .lane ul, .card ul { padding-left: 18px; }
470
+ table { width: 100%; border-collapse: collapse; margin-top: 8px; overflow-wrap: anywhere; }
471
+ th, td { border: 1px solid var(--border); padding: 8px; text-align: left; vertical-align: top; }
472
+ th { background: var(--panel-2); }
473
+ code, pre { background: var(--code); border-radius: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
474
+ code { padding: 0.1rem 0.3rem; }
475
+ pre { padding: 10px; overflow: auto; white-space: pre-wrap; }
476
+ .timeline { display: grid; gap: 10px; }
477
+ .timeline-item { padding: 12px; }
478
+ .status { display: inline-block; border-radius: 999px; padding: 4px 10px; font-weight: 700; font-size: 12px; border: 1px solid currentColor; }
479
+ .good { color: var(--good); }
480
+ .warn { color: var(--warn); }
481
+ .danger { color: var(--danger); }
482
+ .info { color: var(--info); }
483
+ .neutral { color: var(--neutral); }
484
+ .dag-svg { width: 100%; height: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel-2); }
485
+ .dag-node { fill: var(--panel); stroke: var(--border); }
486
+ .dag-text { fill: var(--fg); font: 12px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
487
+ .dag-edge { stroke: var(--muted); stroke-width: 1.5; marker-end: url(#arrow); }
488
+ @media (max-width: 860px) {
489
+ .metrics, .grid-2, .flow, .cards, .node-grid { grid-template-columns: 1fr; }
490
+ .flow-step:not(:last-child)::after { display: none; }
491
+ .hero, .topbar { flex-direction: column; }
492
+ }
493
+ `;
494
+ }
495
+
496
+ function metricCard(label, value, detail) {
497
+ return `<article class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong><span>${escapeHtml(detail)}</span></article>`;
498
+ }
499
+
500
+ function renderFlow(steps) {
501
+ return `<div class="flow">${steps.map((step) => `
502
+ <article class="flow-step" data-flow-step="${escapeAttr(step.label)}">
503
+ <h3>${escapeHtml(step.label)}</h3>
504
+ <p>${escapeHtml(step.detail)}</p>
505
+ <span class="${statusClass(step.status)}">${escapeHtml(step.status)}</span>
506
+ </article>
507
+ `).join('')}</div>`;
508
+ }
509
+
510
+ function flowStep(label, detail, status) {
511
+ return { label, detail, status };
512
+ }
513
+
514
+ function renderCards(title, cards) {
515
+ const visible = cards.length > 0 ? cards : [{ title: 'No items', detail: 'なし', tone: 'neutral' }];
516
+ return `
517
+ <section>
518
+ <h2>${escapeHtml(title)}</h2>
519
+ <div class="cards">
520
+ ${visible.map((card) => `
521
+ <article class="card ${escapeAttr(card.tone ?? 'neutral')}">
522
+ <h3>${escapeHtml(card.title)}</h3>
523
+ ${card.meta ? `<p class="muted">${escapeHtml(card.meta)}</p>` : ''}
524
+ <p>${escapeHtml(card.detail)}</p>
525
+ </article>
526
+ `).join('')}
527
+ </div>
528
+ </section>
529
+ `;
530
+ }
531
+
532
+ function renderRequirementPanel(requirement, language = 'ja') {
533
+ if (!requirement) {
534
+ return renderCards('Requirement Consistency', [{
535
+ title: localizedText(language, { ja: '未生成', en: 'Not generated' }),
536
+ detail: buildRequirementGuide(requirement, language).text,
537
+ tone: 'warn'
538
+ }]);
539
+ }
540
+ const guide = buildRequirementGuide(requirement, language);
541
+ const cards = [
542
+ ...(requirement.contradictions ?? []).slice(0, 6).map((item) => ({
543
+ title: item.title ?? 'Potential Contradiction',
544
+ detail: item.detail,
545
+ meta: item.id,
546
+ tone: 'danger'
547
+ })),
548
+ ...(requirement.scenario_gaps ?? []).slice(0, 6).map((item) => ({
549
+ title: item.title ?? 'Scenario Gap',
550
+ detail: item.detail,
551
+ meta: item.id,
552
+ tone: 'warn'
553
+ })),
554
+ ...(requirement.invariants ?? []).slice(0, 4).map((item) => ({
555
+ title: 'Invariant',
556
+ detail: item.text,
557
+ meta: item.source ? `${item.source.kind}:${item.source.path ?? '-'}` : item.id,
558
+ tone: 'info'
559
+ }))
560
+ ];
561
+ return `
562
+ <section>
563
+ <h2>Requirement Consistency</h2>
564
+ <div class="metrics">
565
+ ${metricCard('Status', requirement.status, 'requirement gate')}
566
+ ${metricCard('Invariants', requirement.summary?.invariant_count ?? 0, 'extracted')}
567
+ ${metricCard('Scenario Gaps', requirement.summary?.scenario_gap_count ?? 0, 'needs review')}
568
+ ${metricCard('Contradictions', requirement.summary?.contradiction_count ?? 0, 'potential bugs')}
569
+ </div>
570
+ <article class="card ${escapeAttr(guide.tone)}">
571
+ <h3>${escapeHtml(localizedText(language, { ja: '次に足すもの', en: 'What To Add Next' }))}</h3>
572
+ <p>${escapeHtml(guide.text)}</p>
573
+ </article>
574
+ <div class="cards">${cards.map((card) => `
575
+ <article class="card ${escapeAttr(card.tone)}">
576
+ <h3>${escapeHtml(card.title)}</h3>
577
+ <p class="muted">${escapeHtml(card.meta ?? '')}</p>
578
+ <p>${escapeHtml(card.detail)}</p>
579
+ </article>
580
+ `).join('') || '<article class="card good"><h3>No findings</h3><p>Story / Architecture / Spec とコード差分の既知矛盾は検出されていません。</p></article>'}</div>
581
+ </section>
582
+ `;
583
+ }
584
+
585
+ function buildRequirementGuide(requirement, language = 'ja') {
586
+ if (!requirement) {
587
+ return {
588
+ tone: 'warn',
589
+ text: localizedText(language, {
590
+ ja: 'Requirement Consistencyは未生成です。まずStoryに受け入れ基準を置き、必要に応じてSpec/Architectureを追加してから `vibepro pr prepare` を再実行してください。',
591
+ en: 'Requirement Consistency was not generated. Add Story acceptance criteria, then add Spec/Architecture when needed, and rerun `vibepro pr prepare`.'
592
+ })
593
+ };
594
+ }
595
+ if (requirement.status === 'not_applicable') {
596
+ return {
597
+ tone: 'warn',
598
+ text: localizedText(language, {
599
+ ja: 'Story/Spec/Architectureから判定に使える不変条件が十分に取れていません。Storyに受け入れ基準、Specに守るべき挙動、Architectureに境界やADR要否を書くと、このGateが有効になります。',
600
+ en: 'VibePro could not extract enough invariants from Story/Spec/Architecture. Add acceptance criteria to the Story, behavioral invariants to the Spec, and boundary/ADR notes to Architecture to activate this gate.'
601
+ })
602
+ };
603
+ }
604
+ if (requirement.status === 'needs_review') {
605
+ return {
606
+ tone: 'warn',
607
+ text: localizedText(language, {
608
+ ja: 'Storyに明示されていないシナリオがあります。意図した挙動ならStory/Specへ追記し、意図しないなら実装かテストを直してください。',
609
+ en: 'Some scenarios are not explicit in the Story. If intended, add them to Story/Spec; otherwise fix the implementation or tests.'
610
+ })
611
+ };
612
+ }
613
+ if (requirement.status === 'contradicted') {
614
+ return {
615
+ tone: 'danger',
616
+ text: localizedText(language, {
617
+ ja: 'Story/Spec/Architectureと実装の矛盾候補があります。PR前に矛盾を解消し、必要ならSpecを更新してください。',
618
+ en: 'Potential contradictions exist between Story/Spec/Architecture and implementation. Resolve them before PR and update Spec when needed.'
619
+ })
620
+ };
621
+ }
622
+ return {
623
+ tone: 'good',
624
+ text: localizedText(language, {
625
+ ja: 'Story/Spec/Architectureと既知の実装分岐に明確な矛盾はありません。',
626
+ en: 'No clear contradiction was found between Story/Spec/Architecture and known implementation branches.'
627
+ })
628
+ };
629
+ }
630
+
631
+ function renderExecutionGatePanel(executionGate, language = 'ja') {
632
+ if (!executionGate) {
633
+ return renderCards(localizedText(language, { ja: '実行Gate', en: 'Execution Gate' }), [{
634
+ title: localizedText(language, { ja: '未生成', en: 'Unknown' }),
635
+ detail: localizedText(language, { ja: 'Execution Gateは未生成です。', en: 'Execution Gate was not generated.' }),
636
+ tone: 'warn'
637
+ }]);
638
+ }
639
+ const cards = executionGate.blocking_gates?.length > 0
640
+ ? executionGate.blocking_gates.map((gate) => ({
641
+ title: `${gate.label ?? gate.id}: ${gate.status}`,
642
+ detail: gate.reason ?? gate.command ?? gate.id,
643
+ meta: gate.id,
644
+ tone: 'danger'
645
+ }))
646
+ : [{
647
+ title: localizedText(language, { ja: '準備完了', en: 'Ready' }),
648
+ detail: localizedText(language, {
649
+ ja: 'VibePro PR作成に対するCritical blockerは解消されています。',
650
+ en: 'Critical blockers are resolved for VibePro PR creation.'
651
+ }),
652
+ tone: 'good'
653
+ }];
654
+ const actions = executionGate.required_actions?.length > 0
655
+ ? renderList(executionGate.required_actions)
656
+ : `<p class="muted">${escapeHtml(localizedText(language, { ja: 'ブロック中の対応はありません。', en: 'No blocking actions.' }))}</p>`;
657
+ return `
658
+ <section>
659
+ <h2>${escapeHtml(localizedText(language, { ja: '実行Gate', en: 'Execution Gate' }))}</h2>
660
+ <div class="metrics">
661
+ ${metricCard(localizedText(language, { ja: '状態', en: 'Status' }), executionGate.status, executionGate.pr_create_allowed ? localizedText(language, { ja: 'PR作成可能', en: 'pr create allowed' }) : localizedText(language, { ja: 'PR作成ブロック', en: 'pr create blocked' }))}
662
+ ${metricCard(localizedText(language, { ja: 'Blocking Gate', en: 'Blocking Gates' }), executionGate.blocking_gate_count ?? 0, 'critical')}
663
+ ${metricCard(localizedText(language, { ja: 'PR作成', en: 'PR Create' }), executionGate.pr_create_allowed ? localizedText(language, { ja: 'allowed', en: 'allowed' }) : localizedText(language, { ja: 'blocked', en: 'blocked' }), 'VibePro gate')}
664
+ ${metricCard('Schema', executionGate.schema_version ?? '-', 'execution gate')}
665
+ </div>
666
+ <div class="cards">${cards.map((card) => `
667
+ <article class="card ${escapeAttr(card.tone)}">
668
+ <h3>${escapeHtml(card.title)}</h3>
669
+ ${card.meta ? `<p class="muted">${escapeHtml(card.meta)}</p>` : ''}
670
+ <p>${escapeHtml(card.detail)}</p>
671
+ </article>
672
+ `).join('')}</div>
673
+ <h3>${escapeHtml(localizedText(language, { ja: '必要な対応', en: 'Required Actions' }))}</h3>
674
+ ${actions}
675
+ </section>
676
+ `;
677
+ }
678
+
679
+ function renderAgentReviewPanel(agentReviews, language = 'ja') {
680
+ if (!agentReviews) {
681
+ return renderCards(localizedText(language, { ja: 'Agent Review Gate', en: 'Agent Review Gate' }), [{
682
+ title: localizedText(language, { ja: '未生成', en: 'Not generated' }),
683
+ detail: localizedText(language, { ja: 'Agent Reviewは未生成です。', en: 'Agent Review was not generated.' }),
684
+ tone: 'warn'
685
+ }]);
686
+ }
687
+ const unmet = agentReviews.unmet_required_reviews ?? [];
688
+ const checkpointUnmet = agentReviews.unmet_checkpoint_reviews ?? [];
689
+ const stageCards = (agentReviews.stages ?? []).map((stage) => ({
690
+ title: `${stage.stage}: ${stage.status}`,
691
+ detail: `pass=${stage.pass_count}, missing=${stage.missing_count}, stale=${stage.stale_count}, block=${stage.block_count}`,
692
+ meta: stage.stage,
693
+ tone: toneForStatus(stage.status)
694
+ }));
695
+ const unmetCards = unmet.slice(0, 8).map((item) => ({
696
+ title: `PR-final ${item.stage}:${item.role}`,
697
+ detail: item.detail ?? item.reason,
698
+ meta: `${item.status} / ${item.policy}`,
699
+ tone: item.status === 'block' ? 'danger' : 'warn'
700
+ }));
701
+ const checkpointCards = checkpointUnmet.slice(0, 8).map((item) => ({
702
+ title: `Checkpoint ${item.stage}:${item.role}`,
703
+ detail: item.detail ?? item.reason,
704
+ meta: `${item.status} / ${item.policy}`,
705
+ tone: item.status === 'block' ? 'danger' : 'warn'
706
+ }));
707
+ const allUnmetCards = [...unmetCards, ...checkpointCards];
708
+ return `
709
+ <section>
710
+ <h2>${escapeHtml(localizedText(language, { ja: 'Agent Review Gate', en: 'Agent Review Gate' }))}</h2>
711
+ <div class="metrics">
712
+ ${metricCard(localizedText(language, { ja: '状態', en: 'Status' }), agentReviews.status, agentReviews.required ? localizedText(language, { ja: 'required', en: 'required' }) : localizedText(language, { ja: 'not required', en: 'not required' }))}
713
+ ${metricCard(localizedText(language, { ja: '必須ロール', en: 'Required Roles' }), agentReviews.summary?.required_review_count ?? 0, 'policy')}
714
+ ${metricCard(localizedText(language, { ja: '未充足ロール', en: 'Unmet Roles' }), agentReviews.summary?.unmet_required_review_count ?? 0, 'missing/stale/block')}
715
+ ${metricCard(localizedText(language, { ja: 'Checkpoint未充足', en: 'Checkpoint Unmet' }), agentReviews.summary?.unmet_checkpoint_review_count ?? 0, 'checkpoint')}
716
+ ${metricCard(localizedText(language, { ja: '古い結果', en: 'Stale Results' }), agentReviews.summary?.stale_result_count ?? 0, 'current git binding')}
717
+ </div>
718
+ <h3>${escapeHtml(localizedText(language, { ja: '未充足の必須レビュー', en: 'Unmet Required Reviews' }))}</h3>
719
+ <div class="cards">${(allUnmetCards.length > 0 ? allUnmetCards : [{
720
+ title: localizedText(language, { ja: '未充足レビューなし', en: 'No unmet reviews' }),
721
+ detail: localizedText(language, {
722
+ ja: '現在のgit状態に対するPR-final/checkpoint agent review roleは通過しています。',
723
+ en: 'PR-final and checkpoint agent review roles passed for the current git state.'
724
+ }),
725
+ tone: 'good'
726
+ }]).map((card) => `
727
+ <article class="card ${escapeAttr(card.tone)}">
728
+ <h3>${escapeHtml(card.title)}</h3>
729
+ ${card.meta ? `<p class="muted">${escapeHtml(card.meta)}</p>` : ''}
730
+ <p>${escapeHtml(card.detail)}</p>
731
+ </article>
732
+ `).join('')}</div>
733
+ <h3>${escapeHtml(localizedText(language, { ja: 'ステージ概要', en: 'Stage Summary' }))}</h3>
734
+ <div class="cards">${(stageCards.length > 0 ? stageCards : [{
735
+ title: localizedText(language, { ja: 'ステージなし', en: 'No stages' }),
736
+ detail: localizedText(language, { ja: 'Agent review stageは記録されていません。', en: 'No agent review stages recorded.' }),
737
+ tone: 'neutral'
738
+ }]).map((card) => `
739
+ <article class="card ${escapeAttr(card.tone)}">
740
+ <h3>${escapeHtml(card.title)}</h3>
741
+ ${card.meta ? `<p class="muted">${escapeHtml(card.meta)}</p>` : ''}
742
+ <p>${escapeHtml(card.detail)}</p>
743
+ </article>
744
+ `).join('')}</div>
745
+ </section>
746
+ `;
747
+ }
748
+
749
+ function renderNetworkContractPanel(networkContracts, language = 'ja') {
750
+ if (!networkContracts) {
751
+ return renderCards(localizedText(language, { ja: 'Network Contract検出', en: 'Network Contract Findings' }), [{
752
+ title: localizedText(language, { ja: '未生成', en: 'Not generated' }),
753
+ detail: localizedText(language, { ja: 'Network Contractは未生成です。', en: 'Network Contract was not generated.' }),
754
+ tone: 'warn'
755
+ }]);
756
+ }
757
+ const missing = networkContracts.missing_routes ?? [];
758
+ const dynamic = networkContracts.dynamic_calls ?? [];
759
+ const replacements = networkContracts.high_risk_replacements ?? [];
760
+ const cards = [
761
+ ...missing.slice(0, 8).map((item) => ({
762
+ title: `Missing route: ${item.api_path}`,
763
+ detail: `${item.method ?? '-'} ${item.callee ?? '-'} in ${item.file}:${item.line ?? '-'}`,
764
+ meta: item.cause_candidates?.map((candidate) => candidate.commit).join('; ') ?? '',
765
+ tone: 'danger'
766
+ })),
767
+ ...replacements.slice(0, 6).map((item) => ({
768
+ title: 'Server function replaced by API',
769
+ detail: `${item.file}: ${item.removed_calls.join(', ')} -> ${item.introduced_api_calls.map((call) => call.api_path.value).join(', ')}`,
770
+ meta: item.risk,
771
+ tone: 'warn'
772
+ })),
773
+ ...dynamic.slice(0, 6).map((item) => ({
774
+ title: `Dynamic API path: ${item.api_path}`,
775
+ detail: `${item.callee ?? '-'} in ${item.file}:${item.line ?? '-'}`,
776
+ meta: 'route cannot be proven statically',
777
+ tone: 'warn'
778
+ }))
779
+ ];
780
+ return `
781
+ <section>
782
+ <h2>${escapeHtml(localizedText(language, { ja: 'Network Contract検出', en: 'Network Contract Findings' }))}</h2>
783
+ <div class="metrics">
784
+ ${metricCard(localizedText(language, { ja: '状態', en: 'Status' }), networkContracts.status, 'route contract')}
785
+ ${metricCard(localizedText(language, { ja: 'API呼び出し', en: 'API Calls' }), networkContracts.api_client_call_count ?? 0, 'detected')}
786
+ ${metricCard(localizedText(language, { ja: 'Route不足', en: 'Missing Routes' }), missing.length, 'block')}
787
+ ${metricCard(localizedText(language, { ja: '新規呼び出し', en: 'Introduced Calls' }), networkContracts.introduced_api_client_call_count ?? 0, 'diff')}
788
+ </div>
789
+ <div class="cards">${cards.map((card) => `
790
+ <article class="card ${escapeAttr(card.tone)}">
791
+ <h3>${escapeHtml(card.title)}</h3>
792
+ <p class="muted">${escapeHtml(card.meta ?? '')}</p>
793
+ <p>${escapeHtml(card.detail)}</p>
794
+ </article>
795
+ `).join('') || `<article class="card good"><h3>${escapeHtml(localizedText(language, { ja: '検出なし', en: 'No findings' }))}</h3><p>${escapeHtml(localizedText(language, { ja: 'API client callとroute fileは整合しています。', en: 'API client calls and route files are aligned.' }))}</p></article>`}</div>
796
+ </section>
797
+ `;
798
+ }
799
+
800
+ function renderFileGroups(fileGroups, language = 'ja') {
801
+ const rows = Object.entries(fileGroups)
802
+ .filter(([, group]) => group.count > 0)
803
+ .map(([name, group]) => [name, group.count, group.files.slice(0, 10).join('<br>')]);
804
+ return renderTable([
805
+ localizedText(language, { ja: '分類', en: 'Group' }),
806
+ localizedText(language, { ja: '件数', en: 'Count' }),
807
+ localizedText(language, { ja: 'ファイル', en: 'Files' })
808
+ ], rows.length > 0 ? rows : [['-', 0, '-']]);
809
+ }
810
+
811
+ function renderGraphSummary(graphContext, language = 'ja') {
812
+ return `
813
+ <div class="metrics">
814
+ ${metricCard('Graph', graphContext.available ? 'available' : 'missing', graphContext.graph_path ?? '-')}
815
+ ${metricCard(localizedText(language, { ja: '一致ファイル', en: 'Matched Files' }), graphContext.matched_file_count, localizedText(language, { ja: '変更ファイル', en: 'changed files' }))}
816
+ ${metricCard(localizedText(language, { ja: '関連ファイル', en: 'Related Files' }), graphContext.related_file_count, localizedText(language, { ja: '確認候補', en: 'inspect candidates' }))}
817
+ ${metricCard(localizedText(language, { ja: 'Graphサイズ', en: 'Graph Size' }), `${graphContext.node_count}/${graphContext.edge_count}`, 'nodes / edges')}
818
+ </div>
819
+ ${graphContext.available ? renderGraphImpactTable(graphContext.impact_by_file.slice(0, 8), language) : `<p class="muted">${escapeHtml(graphContext.reason)}</p>`}
820
+ `;
821
+ }
822
+
823
+ function renderGraphImpactTable(items, language = 'ja') {
824
+ const rows = items.map((item) => [
825
+ item.file,
826
+ item.matched_nodes.join('<br>') || '-',
827
+ item.related_files.join('<br>') || '-'
828
+ ]);
829
+ return renderTable([
830
+ localizedText(language, { ja: '変更ファイル', en: 'Changed file' }),
831
+ localizedText(language, { ja: '一致したgraph node', en: 'Matched graph nodes' }),
832
+ localizedText(language, { ja: '確認する関連ファイル', en: 'Related files to inspect' })
833
+ ], rows.length > 0 ? rows : [['-', '-', '-']]);
834
+ }
835
+
836
+ function renderKeyValueTable(rows) {
837
+ return renderTable(['Item', 'Value'], rows);
838
+ }
839
+
840
+ function renderTable(headers, rows) {
841
+ return `
842
+ <table>
843
+ <thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join('')}</tr></thead>
844
+ <tbody>
845
+ ${rows.map((row) => `<tr>${row.map((cell) => `<td>${renderCell(cell)}</td>`).join('')}</tr>`).join('')}
846
+ </tbody>
847
+ </table>
848
+ `;
849
+ }
850
+
851
+ function renderCell(value) {
852
+ return escapeHtml(value).replace(/&lt;br&gt;/g, '<br>');
853
+ }
854
+
855
+ function renderList(items) {
856
+ return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`;
857
+ }
858
+
859
+ function renderOrderedList(items) {
860
+ return `<ol>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ol>`;
861
+ }
862
+
863
+ function renderCommandList(commands) {
864
+ return `<div>${commands.map((command) => `<p><code>${escapeHtml(command)}</code></p>`).join('')}</div>`;
865
+ }
866
+
867
+ function renderGateDagSvg(gateDag) {
868
+ const nodes = gateDag.nodes;
869
+ const positions = new Map();
870
+ const layers = [
871
+ nodes.filter((node) => node.id === 'story'),
872
+ nodes.filter((node) => ['architecture', 'spec'].includes(node.id) || node.type === 'acceptance_criterion'),
873
+ nodes.filter((node) => node.id === 'code'),
874
+ nodes.filter((node) => node.id.startsWith('gate:') && node.id !== 'gate:agent_review'),
875
+ nodes.filter((node) => node.type === 'agent_review_prepare_gate'),
876
+ nodes.filter((node) => node.type === 'agent_review_role_gate'),
877
+ nodes.filter((node) => node.type === 'agent_review_record_gate'),
878
+ nodes.filter((node) => node.type === 'agent_review_stage_join_gate'),
879
+ nodes.filter((node) => node.id === 'gate:agent_review'),
880
+ nodes.filter((node) => node.id === 'pr')
881
+ ];
882
+ const width = 1120;
883
+ const columnWidth = width / layers.length;
884
+ const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
885
+ const height = Math.max(300, maxRows * 82 + 40);
886
+ layers.forEach((layer, column) => {
887
+ const gap = height / (layer.length + 1);
888
+ layer.forEach((node, row) => {
889
+ positions.set(node.id, { x: 24 + column * columnWidth, y: Math.round(gap * (row + 1)) });
890
+ });
891
+ });
892
+ const edges = gateDag.edges
893
+ .filter((edge) => positions.has(edge.from) && positions.has(edge.to))
894
+ .map((edge) => {
895
+ const from = positions.get(edge.from);
896
+ const to = positions.get(edge.to);
897
+ return `<line class="dag-edge" x1="${from.x + 170}" y1="${from.y + 24}" x2="${to.x}" y2="${to.y + 24}"></line>`;
898
+ })
899
+ .join('');
900
+ const boxes = nodes.map((node) => {
901
+ const pos = positions.get(node.id);
902
+ if (!pos) return '';
903
+ return `
904
+ <g data-node-id="${escapeAttr(node.id)}">
905
+ <rect class="dag-node" x="${pos.x}" y="${pos.y}" width="170" height="48" rx="6"></rect>
906
+ <text class="dag-text" x="${pos.x + 10}" y="${pos.y + 20}">${escapeHtml(shorten(node.label ?? node.id, 22))}</text>
907
+ <text class="dag-text" x="${pos.x + 10}" y="${pos.y + 38}">${escapeHtml(shorten(node.status ?? '-', 20))}</text>
908
+ </g>
909
+ `;
910
+ }).join('');
911
+ return `
912
+ <svg class="dag-svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="Gate DAG">
913
+ <defs>
914
+ <marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
915
+ <path d="M0,0 L0,6 L6,3 z" fill="currentColor"></path>
916
+ </marker>
917
+ </defs>
918
+ ${edges}
919
+ ${boxes}
920
+ </svg>
921
+ `;
922
+ }
923
+
924
+ function renderNodeGrid(nodes) {
925
+ return `<div class="node-grid">${nodes.map((node) => `
926
+ <article class="card ${escapeAttr(toneForStatus(node.status))}" data-node-id="${escapeAttr(node.id)}">
927
+ <h3>${escapeHtml(node.label ?? node.id)}</h3>
928
+ <p class="muted">${escapeHtml(node.id)} / ${escapeHtml(node.type)}</p>
929
+ <p><span class="${statusClass(node.status ?? 'unknown')}">${escapeHtml(node.status ?? 'unknown')}</span></p>
930
+ <p>${escapeHtml(node.reason ?? node.command ?? node.artifact ?? '-')}</p>
931
+ </article>
932
+ `).join('')}</div>`;
933
+ }
934
+
935
+ function renderLaneCard(lane, plan) {
936
+ return `
937
+ <article class="lane" data-lane-id="${escapeAttr(lane.id)}">
938
+ <h3>${escapeHtml(lane.title)}</h3>
939
+ <p><span class="${statusClass(lane.recommendation)}">${escapeHtml(lane.recommendation)}</span></p>
940
+ <p class="muted">${escapeHtml(lane.category)} / ${escapeHtml(lane.file_count)} files</p>
941
+ <h3>Review Focus</h3>
942
+ ${renderList(lane.review_focus)}
943
+ <h3>Files</h3>
944
+ ${renderList(lane.files.slice(0, 8))}
945
+ ${lane.graph_investigation_files.length > 0 ? `<h3>Graphify Related</h3>${renderList(lane.graph_investigation_files)}` : ''}
946
+ ${plan ? `<h3>Gate Mode</h3><p><span class="${statusClass(plan.gate_mode)}">${escapeHtml(plan.gate_mode)}</span></p><p>${escapeHtml(plan.review_note)}</p>` : ''}
947
+ </article>
948
+ `;
949
+ }
950
+
951
+ function renderGateOverridePanel(gateOverride) {
952
+ if (!gateOverride?.allowed) {
953
+ return renderCards('Gate Override', [{ title: 'None', detail: 'Gate overrideは使われていません。', tone: 'good' }]);
954
+ }
955
+ const critical = gateOverride.critical_unresolved_gates ?? [];
956
+ const evidence = gateOverride.required_evidence ?? [];
957
+ const toolchain = gateOverride.toolchain;
958
+ return `
959
+ ${renderCards('Gate Override', [{
960
+ title: `Override Allowed (${gateOverride.severity ?? 'warning'})`,
961
+ detail: gateOverride.reason,
962
+ meta: `policy=${gateOverride.waiver_policy ?? 'unknown'}; overall=${gateOverride.overall_status}; unresolved=${gateOverride.unresolved_gates?.length ?? 0}; critical=${critical.length}`,
963
+ tone: gateOverride.severity === 'critical' ? 'danger' : 'warn'
964
+ }])}
965
+ <section>
966
+ <h2>Critical Unresolved Gates</h2>
967
+ ${renderList(critical.length > 0
968
+ ? critical.map((gate) => `${gate.label ?? gate.id}: ${gate.status} - ${gate.reason ?? gate.command ?? '-'}`)
969
+ : ['なし'])}
970
+ </section>
971
+ <section>
972
+ <h2>Completion Quality Waiver Evidence</h2>
973
+ <p><span class="${statusClass(gateOverride.completion_quality?.status ?? 'unknown')}">${escapeHtml(gateOverride.completion_quality?.status ?? 'unknown')}</span></p>
974
+ ${renderList(evidence.length > 0 ? evidence : ['不足証跡なし'])}
975
+ </section>
976
+ <section>
977
+ <h2>VibePro Runtime</h2>
978
+ ${renderList([
979
+ `package: ${toolchain?.package?.name ?? 'vibepro'}@${toolchain?.package?.version ?? 'unknown'}`,
980
+ `root: ${toolchain?.package?.root ?? '-'}`,
981
+ `commit: ${toolchain?.source_git?.commit ?? '-'}`,
982
+ `branch: ${toolchain?.source_git?.branch ?? '-'}`,
983
+ `dirty: ${toolchain?.source_git?.dirty == null ? '-' : String(toolchain.source_git.dirty)}`
984
+ ])}
985
+ </section>
986
+ `;
987
+ }
988
+
989
+ function gateStatus(gateDag, id) {
990
+ return gateDag.nodes.find((node) => node.id === id)?.status ?? 'unknown';
991
+ }
992
+
993
+ function isUnresolvedStatus(status) {
994
+ return ['missing', 'needs_evidence', 'needs_setup', 'needs_review', 'contradicted', 'not_generated'].includes(status);
995
+ }
996
+
997
+ function statusClass(status) {
998
+ return `status ${toneForStatus(status)}`;
999
+ }
1000
+
1001
+ function toneForStatus(status) {
1002
+ if (['pass', 'passed', 'present', 'satisfied', 'ready_for_review', 'single_pr_ok', 'primary_pr', 'same_pr_allowed', 'executed'].includes(status)) return 'good';
1003
+ if (['needs_evidence', 'needs_setup', 'needs_review', 'needs_verification', 'split_recommended', 'separate_pr', 'cumulative_after_dependencies', 'dry_run'].includes(status)) return 'warn';
1004
+ if (['missing', 'contradicted', 'failed', 'blocked'].includes(status)) return 'danger';
1005
+ if (['candidate', 'available'].includes(status)) return 'info';
1006
+ return 'neutral';
1007
+ }
1008
+
1009
+ function shorten(value, maxLength) {
1010
+ const text = String(value);
1011
+ return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
1012
+ }
1013
+
1014
+ function escapeAttr(value) {
1015
+ return escapeHtml(value).replace(/'/g, '&#39;');
1016
+ }
1017
+
1018
+ function escapeHtml(value) {
1019
+ return String(value ?? '')
1020
+ .replace(/&/g, '&amp;')
1021
+ .replace(/</g, '&lt;')
1022
+ .replace(/>/g, '&gt;')
1023
+ .replace(/"/g, '&quot;');
1024
+ }
1025
+
1026
+ export const __testing__ = {
1027
+ baseCss,
1028
+ renderDocument,
1029
+ metricCard,
1030
+ renderTable,
1031
+ escapeHtml,
1032
+ escapeAttr,
1033
+ statusClass,
1034
+ toneForStatus
1035
+ };