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,534 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ getWorkspaceDir,
6
+ initWorkspace,
7
+ readManifest,
8
+ toWorkspaceRelative,
9
+ writeManifest
10
+ } from './workspace.js';
11
+ import { renderRefactoringDeltaCompact } from './refactoring-delta-reporter.js';
12
+ import { resolveStoryContext } from './story-manager.js';
13
+ import { readStoryTasks } from './story-task-generator.js';
14
+
15
+ export async function createBrainbaseImport(repoRoot) {
16
+ await initWorkspace(repoRoot);
17
+ const root = path.resolve(repoRoot);
18
+ const manifest = await readManifest(root);
19
+ const config = await readConfig(root);
20
+ const storyContext = resolveStoryContext(config);
21
+ const latestRun = findLatestRun(manifest, storyContext.currentStory.story_id);
22
+ const evidence = await readLatestEvidence(root, latestRun);
23
+ const taskState = await readStoryTasks(root, latestRun.artifacts?.story_tasks_json);
24
+
25
+ const brainbaseDir = path.join(getWorkspaceDir(root), 'brainbase');
26
+ await mkdir(brainbaseDir, { recursive: true });
27
+
28
+ const importState = buildImportState({ manifest, storyContext, latestRun, evidence, taskState });
29
+ const importStatePath = path.join(brainbaseDir, 'import-state.json');
30
+ const importSummaryPath = path.join(brainbaseDir, 'import-summary.md');
31
+
32
+ await writeFile(importStatePath, `${JSON.stringify(importState, null, 2)}\n`);
33
+ await writeFile(importSummaryPath, renderImportSummary(importState));
34
+
35
+ manifest.artifacts = {
36
+ ...(manifest.artifacts ?? {}),
37
+ brainbase_import_state: toWorkspaceRelative(root, importStatePath),
38
+ brainbase_import_summary: toWorkspaceRelative(root, importSummaryPath)
39
+ };
40
+ manifest.brainbase = {
41
+ ...(manifest.brainbase ?? {}),
42
+ last_export: {
43
+ exported_at: importState.generated_at,
44
+ story_id: importState.story.story_id,
45
+ latest_run_id: importState.latest_run.run_id,
46
+ latest_run_story_id: importState.latest_run.story_id,
47
+ gate_status: importState.latest_run.gate_status,
48
+ import_state: toWorkspaceRelative(root, importStatePath)
49
+ }
50
+ };
51
+ await writeManifest(root, manifest);
52
+
53
+ return { brainbaseDir, importStatePath, importSummaryPath, importState };
54
+ }
55
+
56
+ function findLatestRun(manifest, storyId) {
57
+ const latestRunId = manifest.latest_run;
58
+ const runs = Array.isArray(manifest.runs) ? manifest.runs : [];
59
+ const latestStoryRunId = storyId ? manifest.latest_run_by_story?.[storyId] : null;
60
+ const latestRun = runs.find((run) => run.run_id === latestStoryRunId)
61
+ ?? runs.find((run) => run.story_id === storyId)
62
+ ?? runs.find((run) => run.run_id === latestRunId)
63
+ ?? runs[0];
64
+ if (!latestRun) {
65
+ throw new Error('VibePro diagnosis run not found. Run `vibepro diagnose` first.');
66
+ }
67
+ return latestRun;
68
+ }
69
+
70
+ async function readLatestEvidence(repoRoot, latestRun) {
71
+ const evidencePath = latestRun.artifacts?.evidence;
72
+ if (!evidencePath) {
73
+ throw new Error(`evidence artifact is missing for run: ${latestRun.run_id}`);
74
+ }
75
+ return JSON.parse(await readFile(path.resolve(repoRoot, evidencePath), 'utf8'));
76
+ }
77
+
78
+ async function readConfig(repoRoot) {
79
+ return JSON.parse(await readFile(path.join(getWorkspaceDir(repoRoot), 'config.json'), 'utf8'));
80
+ }
81
+
82
+ function buildImportState({ manifest, storyContext, latestRun, evidence, taskState }) {
83
+ const graphify = evidence.graphify ?? {};
84
+ const architectureProfile = evidence.architecture_profile ?? {};
85
+ const checkCatalog = evidence.check_catalog ?? {};
86
+ const apiBoundary = evidence.api_boundary ?? {};
87
+ const staticSite = evidence.static_site ?? {};
88
+ const componentStyle = evidence.component_style ?? {};
89
+ const databaseAccess = evidence.database_access ?? {};
90
+ const codeQuality = evidence.code_quality ?? {};
91
+ const findings = Array.isArray(evidence.findings) ? evidence.findings : [];
92
+ const findingReview = evidence.finding_review ?? {};
93
+ const actionCandidates = Array.isArray(evidence.action_candidates) ? evidence.action_candidates : [];
94
+ const refactoringOpportunities = Array.isArray(evidence.refactoring_opportunities)
95
+ ? evidence.refactoring_opportunities
96
+ : [];
97
+ const refactoringCampaigns = Array.isArray(evidence.refactoring_campaigns)
98
+ ? evidence.refactoring_campaigns
99
+ : [];
100
+ const refactoringDelta = evidence.refactoring_delta ?? null;
101
+ const stories = storyContext.stories;
102
+ const primaryStory = storyContext.currentStory;
103
+
104
+ return {
105
+ schema_version: '0.1.0',
106
+ generated_at: new Date().toISOString(),
107
+ source: {
108
+ tool: 'vibepro',
109
+ manifest: '.vibepro/vibepro-manifest.json',
110
+ repo: manifest.repo ?? { root: '.' }
111
+ },
112
+ story: primaryStory,
113
+ stories,
114
+ latest_run: {
115
+ run_id: latestRun.run_id,
116
+ story_id: latestRun.story_id ?? evidence.story_id ?? null,
117
+ created_at: latestRun.created_at ?? null,
118
+ gate_status: latestRun.gate_status ?? evidence.gates?.[0]?.status ?? 'unknown',
119
+ artifacts: latestRun.artifacts ?? {}
120
+ },
121
+ signals: {
122
+ graphify: {
123
+ node_count: graphify.node_count ?? 0,
124
+ edge_count: graphify.edge_count ?? 0,
125
+ edge_source_key: graphify.edge_source_key ?? null,
126
+ extracted_edges_count: graphify.extracted_edges?.length ?? 0,
127
+ inferred_edges_count: graphify.inferred_edges?.length ?? 0,
128
+ ambiguous_edges_count: graphify.ambiguous_edges?.length ?? 0,
129
+ quality_notices: graphify.quality_notices ?? []
130
+ },
131
+ architecture_profile: {
132
+ app_type: architectureProfile.app_type ?? 'unknown',
133
+ system_type: architectureProfile.system_type ?? 'unknown',
134
+ rendering: architectureProfile.rendering ?? null,
135
+ frameworks: architectureProfile.frameworks ?? [],
136
+ package_manager: architectureProfile.package_manager ?? null,
137
+ languages: architectureProfile.languages ?? [],
138
+ views: architectureProfile.views ?? {},
139
+ has_api_routes: Boolean(architectureProfile.has_api_routes),
140
+ has_database: Boolean(architectureProfile.has_database),
141
+ database: architectureProfile.database ?? [],
142
+ has_auth: Boolean(architectureProfile.has_auth),
143
+ auth: architectureProfile.auth ?? [],
144
+ deployment: architectureProfile.deployment ?? []
145
+ },
146
+ check_catalog: {
147
+ selected_views: checkCatalog.selected_views ?? architectureProfile.selected_views ?? [],
148
+ applicable_checks: checkCatalog.applicable_checks ?? architectureProfile.applicable_checks ?? []
149
+ },
150
+ api_boundary: {
151
+ route_count: apiBoundary.route_count ?? 0,
152
+ summary: apiBoundary.summary ?? {},
153
+ protection_summary: apiBoundary.protection_summary ?? {},
154
+ risk_hint_count: Array.isArray(apiBoundary.routes)
155
+ ? apiBoundary.routes.reduce((count, route) => count + (route.risk_hints?.length ?? 0), 0)
156
+ : 0
157
+ },
158
+ static_site: {
159
+ has_index_html: Boolean(staticSite.has_index_html),
160
+ scanned_files: staticSite.scanned_files ?? 0,
161
+ secret_hits_count: staticSite.secret_hits?.length ?? 0,
162
+ secret_hits_gate_summary: staticSite.risk_summary?.secret_hits ?? summarizeGateEffects(staticSite.secret_hits),
163
+ xss_risk_hits_count: staticSite.xss_risk_hits?.length ?? 0,
164
+ xss_risk_hits_gate_summary: staticSite.risk_summary?.xss_risk_hits ?? summarizeGateEffects(staticSite.xss_risk_hits),
165
+ external_resources_count: staticSite.external_resources?.length ?? 0,
166
+ non_static_files_count: staticSite.non_static_files?.length ?? 0
167
+ },
168
+ component_style: {
169
+ scanned_files: componentStyle.scanned_files ?? 0,
170
+ component_kinds: componentStyle.component_kinds ?? [],
171
+ component_inventory_count: componentStyle.component_inventory?.length ?? 0,
172
+ legacy_style_hits_count: componentStyle.legacy_style_hits?.length ?? 0,
173
+ legacy_style_hits_gate_summary: componentStyle.risk_summary?.legacy_style_hits
174
+ ?? summarizeGateEffects(componentStyle.legacy_style_hits),
175
+ design_system_markers_count: componentStyle.design_system_markers?.length ?? 0,
176
+ replacement_observable: Boolean(componentStyle.coverage?.replacement_observable)
177
+ },
178
+ database_access: {
179
+ scanned_files: databaseAccess.scanned_files ?? 0,
180
+ unbounded_find_many_count: databaseAccess.unbounded_find_many?.length ?? 0,
181
+ unbounded_find_many_gate_summary: databaseAccess.risk_summary?.unbounded_find_many
182
+ ?? summarizeGateEffects(databaseAccess.unbounded_find_many)
183
+ },
184
+ code_quality: {
185
+ scanned_files: codeQuality.scanned_files ?? 0,
186
+ authorization_order_risks_count: codeQuality.authorization_order_risks?.length ?? 0,
187
+ authorization_order_risks_gate_summary: codeQuality.risk_summary?.authorization_order_risks
188
+ ?? summarizeGateEffects(codeQuality.authorization_order_risks),
189
+ duplicate_query_shapes_count: codeQuality.duplicate_query_shapes?.length ?? 0,
190
+ duplicate_query_shapes_gate_summary: codeQuality.risk_summary?.duplicate_query_shapes
191
+ ?? summarizeGateEffects(codeQuality.duplicate_query_shapes),
192
+ responsibility_hotspots_count: codeQuality.responsibility_hotspots?.length ?? 0,
193
+ responsibility_hotspots_gate_summary: codeQuality.risk_summary?.responsibility_hotspots
194
+ ?? summarizeGateEffects(codeQuality.responsibility_hotspots)
195
+ },
196
+ finding_review: {
197
+ status: findingReview.status ?? 'unknown',
198
+ summary: findingReview.summary ?? {},
199
+ items: Array.isArray(findingReview.items) ? findingReview.items : []
200
+ },
201
+ refactoring_opportunities: refactoringOpportunities.map((opportunity) => ({
202
+ id: opportunity.id,
203
+ finding_id: opportunity.finding_id,
204
+ source: opportunity.source,
205
+ title: opportunity.title,
206
+ refactoring_intent: opportunity.refactoring_intent,
207
+ rank: opportunity.rank ?? null,
208
+ score: opportunity.score ?? null,
209
+ priority_reasons: opportunity.priority_reasons ?? [],
210
+ target_count: opportunity.target_count,
211
+ target_files: opportunity.target_files ?? [],
212
+ confidence: opportunity.confidence,
213
+ priority: opportunity.priority,
214
+ suggested_abstraction: opportunity.suggested_abstraction ?? null,
215
+ evidence_refs: opportunity.evidence_refs ?? {},
216
+ graph_context: opportunity.graph_context ?? emptyGraphContext(),
217
+ story_blueprint: opportunity.story_blueprint ?? null
218
+ })),
219
+ refactoring_campaigns: refactoringCampaigns.map((campaign) => ({
220
+ id: campaign.id,
221
+ rank: campaign.rank ?? null,
222
+ title: campaign.title,
223
+ refactoring_intent: campaign.refactoring_intent,
224
+ domain: campaign.domain,
225
+ priority: campaign.priority,
226
+ score: campaign.score ?? null,
227
+ opportunity_count: campaign.opportunity_count,
228
+ opportunity_ids: campaign.opportunity_ids ?? [],
229
+ finding_ids: campaign.finding_ids ?? [],
230
+ target_count: campaign.target_count,
231
+ target_files: campaign.target_files ?? [],
232
+ recommended_first_opportunity_id: campaign.recommended_first_opportunity_id ?? null,
233
+ expected_diagnostic_delta: campaign.expected_diagnostic_delta ?? {},
234
+ priority_reasons: campaign.priority_reasons ?? [],
235
+ graph_context: campaign.graph_context ?? emptyGraphContext(),
236
+ story_blueprint: campaign.story_blueprint ?? null
237
+ })),
238
+ refactoring_delta: refactoringDelta,
239
+ tasks: Array.isArray(taskState?.tasks) ? taskState.tasks : [],
240
+ action_candidates: actionCandidates.map((candidate) => ({
241
+ id: candidate.id,
242
+ finding_id: candidate.finding_id,
243
+ scope: candidate.scope,
244
+ title: candidate.title,
245
+ target_count: candidate.target_count,
246
+ execution_policy: candidate.execution_policy,
247
+ mutates_repository: candidate.mutates_repository,
248
+ confidence: candidate.confidence,
249
+ recommendation: candidate.recommendation,
250
+ target_files: candidate.target_files ?? [],
251
+ refactoring_opportunity_id: candidate.refactoring_opportunity_id ?? null,
252
+ refactoring_campaign_id: candidate.refactoring_campaign_id ?? null,
253
+ story_blueprint: candidate.story_blueprint ?? null,
254
+ route_examples: candidate.route_examples ?? [],
255
+ graph_context: candidate.graph_context ?? emptyGraphContext(),
256
+ implementation_plan: candidate.implementation_plan ?? emptyImplementationPlan()
257
+ }))
258
+ },
259
+ gates: evidence.gates ?? [],
260
+ findings: findings.map((finding) => ({
261
+ id: finding.id,
262
+ severity: finding.severity,
263
+ category: finding.category,
264
+ title: finding.title,
265
+ review: findReviewItem(findingReview, finding.id),
266
+ graph_context: finding.graph_context ?? null
267
+ }))
268
+ };
269
+ }
270
+
271
+ function renderImportSummary(importState) {
272
+ return `# Portfolio Dashboard import state
273
+
274
+ | 項目 | 内容 |
275
+ |------|------|
276
+ | Story | ${importState.story.title} |
277
+ | Story ID | ${importState.story.story_id} |
278
+ | Story数 | ${importState.stories.length} |
279
+ | Run ID | ${importState.latest_run.run_id} |
280
+ | Run Story ID | ${importState.latest_run.story_id ?? '-'} |
281
+ | Gate | ${importState.latest_run.gate_status} |
282
+ | 種別 | ${importState.signals.architecture_profile.app_type} |
283
+ | System type | ${importState.signals.architecture_profile.system_type} |
284
+ | 描画方式 | ${importState.signals.architecture_profile.rendering ?? '-'} |
285
+ | 選択View | ${importState.signals.check_catalog.selected_views.join(', ') || '-'} |
286
+ | 適用チェック | ${importState.signals.check_catalog.applicable_checks.join(', ') || '-'} |
287
+ | graphify nodes | ${importState.signals.graphify.node_count} |
288
+ | graphify edges | ${importState.signals.graphify.edge_count} |
289
+ | API route | ${importState.signals.api_boundary.route_count}件 |
290
+ | API境界risk hints | ${importState.signals.api_boundary.risk_hint_count}件 |
291
+ | 共通スキャン対象 | ${importState.signals.static_site.scanned_files}件 |
292
+ | 秘密情報候補 | ${formatRiskCount(importState.signals.static_site.secret_hits_count, importState.signals.static_site.secret_hits_gate_summary)} |
293
+ | XSSリスク候補 | ${formatRiskCount(importState.signals.static_site.xss_risk_hits_count, importState.signals.static_site.xss_risk_hits_gate_summary)} |
294
+ | 認可前bulk DB候補 | ${formatRiskCount(importState.signals.code_quality.authorization_order_risks_count, importState.signals.code_quality.authorization_order_risks_gate_summary)} |
295
+ | 重複query形状候補 | ${formatRiskCount(importState.signals.code_quality.duplicate_query_shapes_count, importState.signals.code_quality.duplicate_query_shapes_gate_summary)} |
296
+ | 責務混在候補 | ${formatRiskCount(importState.signals.code_quality.responsibility_hotspots_count, importState.signals.code_quality.responsibility_hotspots_gate_summary)} |
297
+ | リファクタリング機会 | ${importState.signals.refactoring_opportunities.length}件 |
298
+ | リファクタリングcampaign | ${importState.signals.refactoring_campaigns.length}件 |
299
+ | リファクタリング差分 | ${importState.signals.refactoring_delta?.status ?? '-'} |
300
+ | 検出事項 | ${importState.findings.length}件 |
301
+
302
+ ## API境界
303
+
304
+ ${renderApiBoundaryImportSummary(importState.signals.api_boundary)}
305
+
306
+ ## リファクタリング差分
307
+
308
+ ${renderRefactoringDeltaCompact(importState.signals.refactoring_delta)}
309
+
310
+ ## 成果物
311
+
312
+ ${Object.entries(importState.latest_run.artifacts).map(([key, value]) => `- ${key}: ${value}`).join('\n')}
313
+
314
+ ## 対象Story
315
+
316
+ ${importState.stories.map((story) => `- ${story.title} (${story.story_id}) / Horizon: ${story.horizon ?? '-'} / View: ${story.view ?? '-'} / Period: ${story.period ?? '-'} / ${story.started_at ?? '-'} - ${story.due_at ?? '-'}`).join('\n')}
317
+
318
+ ## 検出事項
319
+
320
+ ${importState.findings.length === 0 ? '- なし' : importState.findings.map((finding) => `- ${finding.id}: ${finding.title}(${finding.severity})`).join('\n')}
321
+
322
+ ## 診断レビュー
323
+
324
+ ${renderFindingReviewSummary(importState.signals.finding_review)}
325
+
326
+ ## 次アクション候補
327
+
328
+ ${renderActionCandidates(importState.signals.action_candidates)}
329
+
330
+ ## 生成タスク
331
+
332
+ ${renderGeneratedTasks(importState.signals.tasks)}
333
+ `;
334
+ }
335
+
336
+ function findReviewItem(findingReview, findingId) {
337
+ const items = Array.isArray(findingReview?.items) ? findingReview.items : [];
338
+ return items.find((item) => item.finding_id === findingId) ?? null;
339
+ }
340
+
341
+ function renderFindingReviewSummary(findingReview) {
342
+ const summary = findingReview?.summary ?? {};
343
+ return `- Status: ${findingReview?.status ?? 'unknown'}
344
+ - 未レビュー: ${summary.unreviewed ?? 0}件
345
+ - suggested implementation_gap: ${summary.implementation_gap ?? 0}件
346
+ - suggested detector_gap: ${summary.detector_gap ?? 0}件`;
347
+ }
348
+
349
+ function renderApiBoundaryImportSummary(apiBoundary) {
350
+ const classificationRows = Object.entries(apiBoundary.summary ?? {})
351
+ .map(([classification, count]) => `| ${classification} | ${count} |`)
352
+ .join('\n');
353
+ const protectionRows = Object.entries(apiBoundary.protection_summary ?? {})
354
+ .map(([status, count]) => `| ${status} | ${count} |`)
355
+ .join('\n');
356
+ return `### 分類別
357
+
358
+ | 分類 | 件数 |
359
+ |------|------|
360
+ ${classificationRows || '| - | 0 |'}
361
+
362
+ ### 保護状態別
363
+
364
+ | 保護状態 | 件数 |
365
+ |----------|------|
366
+ ${protectionRows || '| - | 0 |'}`;
367
+ }
368
+
369
+ function renderActionCandidates(candidates) {
370
+ if (!Array.isArray(candidates) || candidates.length === 0) return '- なし';
371
+ return `| ID | 対応する検出事項 | 候補 | 対象 | Impact | Community | 読むファイル | 方針 |
372
+ |----|------------------|------|------|--------|-----------|------------|------|
373
+ ${candidates.map((candidate) => `| ${candidate.id} | ${candidate.finding_id} | ${candidate.title} | ${candidate.target_count}件 | ${formatGraphImpact(candidate.graph_context)} | ${formatGraphCommunities(candidate.graph_context)} | ${formatReadFirstFiles(candidate.implementation_plan)} | ${candidate.execution_policy} / mutates_repository=${candidate.mutates_repository} |`).join('\n')}
374
+
375
+ ${renderImplementationPlans(candidates)}`;
376
+ }
377
+
378
+ function renderGeneratedTasks(tasks) {
379
+ if (!Array.isArray(tasks) || tasks.length === 0) return '- なし';
380
+ return `| ID | 対応する検出事項 | 優先度 | 対象 | グループ | 方針 |
381
+ |----|------------------|--------|------|----------|------|
382
+ ${tasks.map((task) => `| ${task.id} | ${task.finding_id ?? '-'} | ${task.priority} | ${task.target_count ?? task.target_files?.length ?? 0}件 | ${formatTargetGroups(task.target_groups)} | ${task.recommended_strategy?.id ?? '-'} |`).join('\n')}`;
383
+ }
384
+
385
+ function formatTargetGroups(groups = []) {
386
+ if (!Array.isArray(groups) || groups.length === 0) return '-';
387
+ return groups.map((group) => `${group.id}(${group.route_count})`).join(', ');
388
+ }
389
+
390
+ function emptyGraphContext() {
391
+ return {
392
+ matched_route_count: 0,
393
+ matched_node_count: 0,
394
+ affected_communities: [],
395
+ hub_nodes: [],
396
+ related_edge_count: 0,
397
+ impact_score: 0
398
+ };
399
+ }
400
+
401
+ function emptyImplementationPlan() {
402
+ return {
403
+ priority: 'low',
404
+ rationale: '',
405
+ read_first_files: [],
406
+ steps: [],
407
+ acceptance_criteria: [],
408
+ pre_fix_briefing: null
409
+ };
410
+ }
411
+
412
+ function formatGraphImpact(graphContext) {
413
+ if (!graphContext) return '-';
414
+ return `${graphContext.impact_score ?? 0} (${graphContext.related_edge_count ?? 0} edges)`;
415
+ }
416
+
417
+ function formatGraphCommunities(graphContext) {
418
+ const communities = graphContext?.affected_communities ?? [];
419
+ if (communities.length === 0) return '-';
420
+ return communities
421
+ .slice(0, 3)
422
+ .map((community) => {
423
+ const scope = (community.route_count ?? 0) > 0
424
+ ? `route: ${community.route_count}`
425
+ : `file: ${community.file_count ?? 0}`;
426
+ return `${community.id}(${scope}, node: ${community.node_count}, edge: ${community.edge_count})`;
427
+ })
428
+ .join(', ');
429
+ }
430
+
431
+ function formatReadFirstFiles(implementationPlan) {
432
+ const files = implementationPlan?.read_first_files ?? [];
433
+ if (files.length === 0) return '-';
434
+ return selectRepresentativeReadFirstFiles(files, implementationPlan?.pre_fix_briefing).map((item) => item.file).join('<br>');
435
+ }
436
+
437
+ function selectRepresentativeReadFirstFiles(files, briefing) {
438
+ const selected = [];
439
+ const seen = new Set();
440
+ const add = (item) => {
441
+ if (!item || seen.has(item.file)) return;
442
+ seen.add(item.file);
443
+ selected.push(item);
444
+ };
445
+ const helpers = briefing?.auth_helpers ?? [];
446
+ const helperFiles = new Set(helpers.map((helper) => helper.file));
447
+ const hasSignatureHelper = helpers.some((helper) => helper.category === 'signature');
448
+ add(files[0]);
449
+ add(files.find((item) => helperFiles.has(item.file)));
450
+ add(files.find((item) => item.reason.includes('graphify hub') && helperFiles.has(item.file)));
451
+ if (!hasSignatureHelper) add(files.find((item) => item.reason.includes('middleware')));
452
+ for (const item of files) add(item);
453
+ return selected.slice(0, 3);
454
+ }
455
+
456
+ function renderImplementationPlans(candidates) {
457
+ const items = candidates.filter((candidate) => candidate.implementation_plan);
458
+ if (items.length === 0) return '';
459
+ return `### 実装手順
460
+
461
+ ${items.map((candidate) => renderImplementationPlan(candidate)).join('\n\n')}`;
462
+ }
463
+
464
+ function renderImplementationPlan(candidate) {
465
+ const plan = candidate.implementation_plan;
466
+ return `#### ${candidate.id}: ${candidate.title}
467
+
468
+ - 優先度: ${plan.priority}
469
+ - 理由: ${plan.rationale}
470
+ - 読むファイル: ${plan.read_first_files.length === 0 ? '-' : plan.read_first_files.map((item) => `${item.file}(${item.reason})`).join(', ')}
471
+
472
+ ${renderPreFixBriefing(plan.pre_fix_briefing)}
473
+
474
+ ${plan.steps.map((step, index) => `${index + 1}. ${step.title}: ${step.detail}`).join('\n')}
475
+
476
+ 完了条件:
477
+ ${plan.acceptance_criteria.map((item) => `- ${item}`).join('\n')}`;
478
+ }
479
+
480
+ function renderPreFixBriefing(briefing) {
481
+ if (!briefing) return '';
482
+ if (briefing.opportunity) {
483
+ return `修正前ブリーフィング:
484
+ - リファクタリング機会: ${briefing.opportunity.id} / ${briefing.opportunity.refactoring_intent}
485
+ - Campaign: ${briefing.campaign?.id ?? '-'} / rank=${briefing.campaign?.rank ?? '-'}
486
+ - 推奨抽象化: ${briefing.opportunity.suggested_abstraction?.label ?? '-'}
487
+ - 対象ファイル: ${briefing.target_files?.slice(0, 5).join(', ') || '-'}
488
+ - 推奨方針: ${briefing.recommended_strategy?.id ?? '-'} - ${briefing.recommended_strategy?.reason ?? '-'}
489
+ - 方針: ${briefing.strategy_options?.map((option) => option.label).join(' / ') || '-'}`;
490
+ }
491
+ return `修正前ブリーフィング:
492
+ - 現在の境界: middleware excludes_api=${briefing.current_boundary?.middleware?.excludes_api ?? false}, route protection=${formatInlineSummary(briefing.current_boundary?.route_protection ?? {})}
493
+ - 認証/署名候補: ${formatAuthHelpers(briefing.auth_helpers)}
494
+ - 対象route: ${briefing.target_routes?.slice(0, 5).map((route) => `${route.route_path} (${route.methods.join(', ') || '-'})`).join(', ') || '-'}
495
+ - 推奨方針: ${briefing.recommended_strategy?.id ?? '-'} - ${briefing.recommended_strategy?.reason ?? '-'}
496
+ - 方針: ${briefing.strategy_options?.map((option) => option.label).join(' / ') || '-'}`;
497
+ }
498
+
499
+ function formatAuthHelpers(helpers = []) {
500
+ if (helpers.length === 0) return '-';
501
+ return helpers
502
+ .slice(0, 5)
503
+ .map((helper) => `${formatHelperCategory(helper.category)}${helper.file}${helper.functions.length > 0 ? `:${helper.functions.slice(0, 3).join(',')}` : ''}`)
504
+ .join(', ');
505
+ }
506
+
507
+ function formatHelperCategory(category) {
508
+ const labels = {
509
+ auth: '認証:',
510
+ signature: '署名:',
511
+ environment: '環境:'
512
+ };
513
+ return labels[category] ?? '';
514
+ }
515
+
516
+ function formatInlineSummary(summary = {}) {
517
+ const entries = Object.entries(summary);
518
+ if (entries.length === 0) return '-';
519
+ return entries.map(([key, count]) => `${key}: ${count}件`).join(', ');
520
+ }
521
+
522
+ function summarizeGateEffects(hits = []) {
523
+ const summary = { block: 0, review: 0, info: 0 };
524
+ for (const hit of hits ?? []) {
525
+ if (hit.gate_effect === 'block') summary.block += 1;
526
+ else if (hit.gate_effect === 'review') summary.review += 1;
527
+ else summary.info += 1;
528
+ }
529
+ return summary;
530
+ }
531
+
532
+ function formatRiskCount(count, summary = {}) {
533
+ return `${count}件 (block: ${summary.block ?? 0}件, review: ${summary.review ?? 0}件, info: ${summary.info ?? 0}件)`;
534
+ }