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,2144 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ buildGraphContextForFiles,
6
+ buildGraphIndex,
7
+ normalizeGraphEdges
8
+ } from './graph-context.js';
9
+ import { generateStoryCatalog, renderStoryCatalogMap } from './story-catalog-generator.js';
10
+ import { renderStoryReportHtml } from './story-html.js';
11
+ import { DEFAULT_BRAINBASE_STORIES, getWorkspaceDir, initWorkspace, readManifest, toWorkspaceRelative, writeManifest, WORKSPACE_DIR } from './workspace.js';
12
+ import { readStoryTasks } from './story-task-generator.js';
13
+
14
+ const STORY_FIELDS = [
15
+ ['--id', 'story_id'],
16
+ ['--title', 'title'],
17
+ ['--horizon', 'horizon'],
18
+ ['--view', 'view'],
19
+ ['--period', 'period'],
20
+ ['--started-at', 'started_at'],
21
+ ['--due-at', 'due_at']
22
+ ];
23
+
24
+ export async function addStory(repoRoot, options = {}) {
25
+ const root = path.resolve(repoRoot);
26
+ const config = await readConfig(root);
27
+ const story = buildStory(options);
28
+ const stories = getStories(config);
29
+ if (stories.some((item) => item.story_id === story.story_id)) {
30
+ throw new Error(`Story already exists: ${story.story_id}`);
31
+ }
32
+ config.brainbase = {
33
+ ...(config.brainbase ?? {}),
34
+ stories: [...stories, story]
35
+ };
36
+ await writeConfig(root, config);
37
+ return story;
38
+ }
39
+
40
+ export async function listStories(repoRoot, options = {}) {
41
+ const config = await readConfig(path.resolve(repoRoot));
42
+ const stories = getStories(config);
43
+ const visibleStories = options.includeArchived ? stories : stories.filter((story) => !isArchived(story));
44
+ return {
45
+ current_story_id: config.brainbase?.current_story_id ?? null,
46
+ stories: visibleStories
47
+ };
48
+ }
49
+
50
+ export async function selectStory(repoRoot, storyId) {
51
+ if (!storyId) throw new Error('--id is required');
52
+ const root = path.resolve(repoRoot);
53
+ const config = await readConfig(root);
54
+ const story = getStories(config).find((item) => item.story_id === storyId);
55
+ if (!story) throw new Error(`Story not found: ${storyId}`);
56
+ if (isArchived(story)) throw new Error(`Archived story cannot be selected: ${storyId}`);
57
+ config.brainbase = {
58
+ ...(config.brainbase ?? {}),
59
+ current_story_id: storyId
60
+ };
61
+ await writeConfig(root, config);
62
+ return story;
63
+ }
64
+
65
+ export async function archiveStory(repoRoot, storyId) {
66
+ if (!storyId) throw new Error('--id is required');
67
+ const root = path.resolve(repoRoot);
68
+ const config = await readConfig(root);
69
+ const stories = getStories(config);
70
+ const story = stories.find((item) => item.story_id === storyId);
71
+ if (!story) throw new Error(`Story not found: ${storyId}`);
72
+ story.status = 'archived';
73
+ if (config.brainbase?.current_story_id === storyId) {
74
+ config.brainbase.current_story_id = null;
75
+ }
76
+ await writeConfig(root, config);
77
+ return story;
78
+ }
79
+
80
+ export async function getStoryRuns(repoRoot, storyId = null) {
81
+ const root = path.resolve(repoRoot);
82
+ const config = await readConfig(root);
83
+ const manifest = await readManifest(root);
84
+ const story = resolveStory(config, storyId);
85
+ const runs = getRunsForStory(manifest, story.story_id);
86
+ return { story, runs };
87
+ }
88
+
89
+ export async function getStoryStatus(repoRoot, storyId = null) {
90
+ const root = path.resolve(repoRoot);
91
+ const config = await readConfig(root);
92
+ const manifest = await readManifest(root);
93
+ const story = resolveStory(config, storyId);
94
+ const runs = getRunsForStory(manifest, story.story_id);
95
+ const latestRun = findLatestStoryRun(manifest, story.story_id, runs);
96
+ const evidence = latestRun ? await readRunEvidence(root, latestRun) : null;
97
+ return {
98
+ story,
99
+ latestRun,
100
+ runs,
101
+ findingCount: evidence?.findings?.length ?? 0,
102
+ artifacts: latestRun?.artifacts ?? {}
103
+ };
104
+ }
105
+
106
+ export async function createStoryReport(repoRoot, storyId = null) {
107
+ const root = path.resolve(repoRoot);
108
+ const config = await readConfig(root);
109
+ const manifest = await readManifest(root);
110
+ const story = resolveStory(config, storyId);
111
+ const runs = getRunsForStory(manifest, story.story_id);
112
+ const latestRun = findLatestStoryRun(manifest, story.story_id, runs);
113
+ if (!latestRun) throw new Error(`Story diagnosis run not found: ${story.story_id}`);
114
+ const evidence = await readRunEvidence(root, latestRun);
115
+ const taskState = await readStoryTasks(root, latestRun.artifacts?.story_tasks_json);
116
+ const storyDir = path.join(getWorkspaceDir(root), 'stories', story.story_id);
117
+ await mkdir(storyDir, { recursive: true });
118
+ const reportPath = path.join(storyDir, 'story-report.md');
119
+ await writeFile(reportPath, renderStoryReport({ story, latestRun, runs, evidence, taskState }));
120
+ const htmlPath = path.join(storyDir, 'index.html');
121
+ const graphHtmlRel = path.join(WORKSPACE_DIR, 'graphify', 'graph.html');
122
+ await writeFile(htmlPath, renderStoryReportHtml({
123
+ story,
124
+ latestRun,
125
+ runs,
126
+ evidence,
127
+ repoRoot: root,
128
+ storyDir,
129
+ graphHtmlPath: graphHtmlRel,
130
+ storyReportMdPath: reportPath,
131
+ storyTasksMdPath: latestRun.artifacts?.story_tasks_markdown ?? null
132
+ }));
133
+ manifest.stories = {
134
+ ...(manifest.stories ?? {}),
135
+ [story.story_id]: {
136
+ ...(manifest.stories?.[story.story_id] ?? {}),
137
+ latest_report: toWorkspaceRelative(root, reportPath),
138
+ latest_report_html: toWorkspaceRelative(root, htmlPath),
139
+ latest_report_run_id: latestRun.run_id,
140
+ latest_report_generated_at: new Date().toISOString()
141
+ }
142
+ };
143
+ await writeManifest(root, manifest);
144
+ return { story, latestRun, reportPath, htmlPath };
145
+ }
146
+
147
+ export async function deriveStories(repoRoot, options = {}) {
148
+ const root = path.resolve(repoRoot);
149
+ const config = await readConfig(root);
150
+ const manifest = await readManifest(root);
151
+ const previousCatalog = await readExistingStoryCatalog(root);
152
+ let catalog;
153
+ try {
154
+ catalog = await generateStoryCatalog(root, {
155
+ config,
156
+ manifest,
157
+ fromRunId: options.fromRunId,
158
+ preset: options.preset
159
+ });
160
+ } catch (error) {
161
+ await writeStoryDeriveFailure(root, error, {
162
+ fromRunId: options.fromRunId,
163
+ preset: options.preset
164
+ });
165
+ throw error;
166
+ }
167
+ const storyDir = path.join(getWorkspaceDir(root), 'stories');
168
+ await mkdir(storyDir, { recursive: true });
169
+ const catalogPath = path.join(storyDir, 'story-catalog.json');
170
+ const mapPath = path.join(storyDir, 'story-map.md');
171
+ const mergeResult = mergeDerivedStories(config, catalog.stories, previousCatalog);
172
+
173
+ await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
174
+ await writeFile(mapPath, renderStoryCatalogMap(catalog));
175
+ await writeConfig(root, config);
176
+
177
+ manifest.artifacts = {
178
+ ...(manifest.artifacts ?? {}),
179
+ story_catalog: toWorkspaceRelative(root, catalogPath),
180
+ story_map: toWorkspaceRelative(root, mapPath)
181
+ };
182
+ manifest.story_catalog = {
183
+ generated_at: catalog.generated_at,
184
+ story_count: catalog.story_count,
185
+ added_count: mergeResult.added_count,
186
+ archived_count: mergeResult.archived_count,
187
+ updated_count: mergeResult.updated_count,
188
+ skipped_count: mergeResult.skipped_count,
189
+ artifact: toWorkspaceRelative(root, catalogPath)
190
+ };
191
+ await writeManifest(root, manifest);
192
+
193
+ return {
194
+ catalog,
195
+ catalogPath,
196
+ mapPath,
197
+ added_count: mergeResult.added_count,
198
+ archived_count: mergeResult.archived_count,
199
+ updated_count: mergeResult.updated_count,
200
+ skipped_count: mergeResult.skipped_count
201
+ };
202
+ }
203
+
204
+ async function writeStoryDeriveFailure(root, error, options = {}) {
205
+ const runId = `story-derive-failure-${new Date().toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '')}`;
206
+ const runDir = path.join(getWorkspaceDir(root), 'diagnostics', runId);
207
+ await mkdir(runDir, { recursive: true });
208
+ const graphStats = await readGraphStatsForFailure(root);
209
+ const failure = {
210
+ schema_version: '0.1.0',
211
+ run_id: runId,
212
+ command: 'story derive',
213
+ status: 'failed',
214
+ created_at: new Date().toISOString(),
215
+ options,
216
+ error: {
217
+ name: error?.name ?? 'Error',
218
+ message: error?.message ?? String(error),
219
+ stack: error?.stack ?? null
220
+ },
221
+ graphify: graphStats
222
+ };
223
+ const jsonPath = path.join(runDir, 'failure.json');
224
+ const markdownPath = path.join(runDir, 'failure.md');
225
+ await writeFile(jsonPath, `${JSON.stringify(failure, null, 2)}\n`);
226
+ await writeFile(markdownPath, renderStoryDeriveFailure(failure));
227
+ }
228
+
229
+ async function readGraphStatsForFailure(root) {
230
+ try {
231
+ const graph = JSON.parse(await readFile(path.join(getWorkspaceDir(root), 'graphify', 'graph.json'), 'utf8'));
232
+ const edges = Array.isArray(graph?.edges) ? graph.edges : Array.isArray(graph?.links) ? graph.links : [];
233
+ return {
234
+ available: true,
235
+ node_count: Array.isArray(graph?.nodes) ? graph.nodes.length : 0,
236
+ edge_count: edges.length,
237
+ edge_source_key: Array.isArray(graph?.edges) ? 'edges' : Array.isArray(graph?.links) ? 'links' : null
238
+ };
239
+ } catch (graphError) {
240
+ return {
241
+ available: false,
242
+ reason: graphError.code === 'ENOENT' ? 'graphify graph.json not found' : graphError.message
243
+ };
244
+ }
245
+ }
246
+
247
+ function renderStoryDeriveFailure(failure) {
248
+ return `# Story Derive Failure
249
+
250
+ - run_id: ${failure.run_id}
251
+ - status: ${failure.status}
252
+ - error: ${failure.error.message}
253
+ - graph nodes: ${failure.graphify?.node_count ?? '-'}
254
+ - graph edges: ${failure.graphify?.edge_count ?? '-'}
255
+
256
+ ## Next Actions
257
+
258
+ - Re-run \`vibepro story derive\` after fixing the reported error.
259
+ - If this happened on a graphify graph with cycles or many nodes, attach this failure directory to the issue.
260
+ `;
261
+ }
262
+
263
+ export async function readStoryMap(repoRoot) {
264
+ const root = path.resolve(repoRoot);
265
+ await initWorkspace(root);
266
+ const catalogPath = path.join(getWorkspaceDir(root), 'stories', 'story-catalog.json');
267
+ try {
268
+ const catalog = JSON.parse(await readFile(catalogPath, 'utf8'));
269
+ return { catalog, catalogPath };
270
+ } catch (error) {
271
+ if (error.code === 'ENOENT') {
272
+ throw new Error('Story catalog not found. Run `vibepro story derive` first.');
273
+ }
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ export async function createStoryPlan(repoRoot, options = {}) {
279
+ const root = path.resolve(repoRoot);
280
+ const manifest = await readManifest(root);
281
+ const { catalog, catalogPath } = await readStoryMap(root);
282
+ const graphIndex = await readStoryPlanGraphIndex(root);
283
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 5;
284
+ const explicitStoryTasks = await readExplicitStoryTasks(root, catalog);
285
+ const plan = buildStoryExecutionPlan(catalog, { limit, graphIndex, explicitStoryTasks });
286
+ const storyDir = path.join(getWorkspaceDir(root), 'stories');
287
+ await mkdir(storyDir, { recursive: true });
288
+ const planPath = path.join(storyDir, 'story-plan.json');
289
+ const markdownPath = path.join(storyDir, 'story-plan.md');
290
+ await writeFile(planPath, `${JSON.stringify(plan, null, 2)}\n`);
291
+ await writeFile(markdownPath, renderStoryPlan(plan));
292
+ manifest.artifacts = {
293
+ ...(manifest.artifacts ?? {}),
294
+ story_plan: toWorkspaceRelative(root, planPath),
295
+ story_plan_markdown: toWorkspaceRelative(root, markdownPath)
296
+ };
297
+ manifest.story_plan = {
298
+ generated_at: plan.generated_at,
299
+ source_catalog: toWorkspaceRelative(root, catalogPath),
300
+ priority_story_count: plan.priority_stories.length,
301
+ question_count: plan.questions.length,
302
+ artifact: toWorkspaceRelative(root, planPath)
303
+ };
304
+ await writeManifest(root, manifest);
305
+ return { plan, planPath, markdownPath };
306
+ }
307
+
308
+ async function readStoryPlanGraphIndex(root) {
309
+ try {
310
+ const graph = JSON.parse(await readFile(path.join(getWorkspaceDir(root), 'graphify', 'graph.json'), 'utf8'));
311
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
312
+ const { edges } = normalizeGraphEdges(graph);
313
+ return buildGraphIndex({ nodes, edges });
314
+ } catch (error) {
315
+ if (error.code === 'ENOENT') return null;
316
+ throw error;
317
+ }
318
+ }
319
+
320
+ export function parseStoryOptions(args) {
321
+ const options = {};
322
+ for (const [flag, key] of STORY_FIELDS) {
323
+ const value = getOption(args, flag);
324
+ if (value !== null) options[key] = value;
325
+ }
326
+ return options;
327
+ }
328
+
329
+ export function renderStoryDeriveSummary(result) {
330
+ return `# Story Derive
331
+
332
+ | 項目 | 内容 |
333
+ |------|------|
334
+ | Story候補 | ${result.catalog.story_count} |
335
+ | 追加 | ${result.added_count} |
336
+ | 意味づけ更新 | ${result.updated_count} |
337
+ | 不要化してアーカイブ | ${result.archived_count} |
338
+ | 既存のためスキップ | ${result.skipped_count} |
339
+ | Catalog | ${toWorkspaceRelativeFromAny(result.catalogPath)} |
340
+ | Map | ${toWorkspaceRelativeFromAny(result.mapPath)} |
341
+
342
+ ${renderStoryDeriveWarnings(result.catalog)}
343
+
344
+ ${renderStoryMapCatalog(result.catalog)}`;
345
+ }
346
+
347
+ function renderStoryDeriveWarnings(catalog) {
348
+ const warnings = catalog.source?.warnings ?? [];
349
+ if (warnings.length === 0) return 'Warnings: なし';
350
+ return `Warnings:\n${warnings.map((warning) => `- ${warning.message ?? warning.code}`).join('\n')}`;
351
+ }
352
+
353
+ export function renderStoryMap(result) {
354
+ return renderStoryMapCatalog(result.catalog);
355
+ }
356
+
357
+ export function renderStoryPlanSummary(result) {
358
+ return `# Story Plan
359
+
360
+ | 項目 | 内容 |
361
+ |------|------|
362
+ | 生成日時 | ${result.plan.generated_at} |
363
+ | Story数 | ${result.plan.summary.story_count} |
364
+ | Coverage | ${result.plan.summary.coverage_status} (${formatPercent(result.plan.summary.coverage_ratio)}) |
365
+ | 確認質問 | ${result.plan.questions.length} |
366
+ | 優先Story | ${result.plan.priority_stories.length} |
367
+ | Plan | ${toWorkspaceRelativeFromAny(result.planPath)} |
368
+ | Markdown | ${toWorkspaceRelativeFromAny(result.markdownPath)} |
369
+
370
+ ${renderStoryPlan(result.plan)}`;
371
+ }
372
+
373
+ export function renderStoryList(result) {
374
+ if (result.stories.length === 0) return 'No active stories.\n';
375
+ return `${result.stories.map((story) => {
376
+ const marker = story.story_id === result.current_story_id ? '*' : '-';
377
+ const status = story.status ?? 'active';
378
+ const view = story.view ?? '-';
379
+ const period = story.period ?? '-';
380
+ return `${marker} ${story.story_id} | ${story.title} | ${status} | view:${view} | period:${period}`;
381
+ }).join('\n')}\n`;
382
+ }
383
+
384
+ export function renderStoryRuns(result) {
385
+ if (result.runs.length === 0) {
386
+ return `# Story Runs\n\n| Story ID | ${result.story.story_id} |\n| Latest run | - |\n\nNo diagnosis runs.\n`;
387
+ }
388
+ return `# Story Runs
389
+
390
+ | Story ID | ${result.story.story_id} |
391
+ | Story | ${result.story.title} |
392
+
393
+ | Run ID | Created At | Gate | Evidence |
394
+ |--------|------------|------|----------|
395
+ ${result.runs.map((run) => `| ${run.run_id} | ${run.created_at ?? '-'} | ${run.gate_status ?? '-'} | ${run.artifacts?.evidence ?? '-'} |`).join('\n')}
396
+ `;
397
+ }
398
+
399
+ export function renderStoryStatus(result) {
400
+ const latestRun = result.latestRun;
401
+ return `# Story Status
402
+
403
+ | 項目 | 内容 |
404
+ |------|------|
405
+ | Story ID | ${result.story.story_id} |
406
+ | Story | ${result.story.title} |
407
+ | Status | ${result.story.status ?? 'active'} |
408
+ | View | ${result.story.view ?? '-'} |
409
+ | Period | ${result.story.period ?? '-'} |
410
+ | Latest run | ${latestRun?.run_id ?? '-'} |
411
+ | Gate | ${latestRun?.gate_status ?? '-'} |
412
+ | Findings | ${result.findingCount} |
413
+ | Runs | ${result.runs.length} |
414
+
415
+ ## Artifacts
416
+
417
+ ${Object.entries(result.artifacts).length === 0 ? '- なし' : Object.entries(result.artifacts).map(([key, value]) => `- ${key}: ${value}`).join('\n')}
418
+ `;
419
+ }
420
+
421
+ export function renderStoryReport({ story, latestRun, runs, evidence, taskState = null }) {
422
+ const graphify = evidence?.graphify ?? {};
423
+ const architectureProfile = evidence?.architecture_profile ?? {};
424
+ const applicableChecks = evidence?.check_catalog?.applicable_checks ?? architectureProfile.applicable_checks ?? [];
425
+ const apiBoundary = evidence?.api_boundary ?? null;
426
+ const staticSite = evidence?.static_site ?? {};
427
+ const findings = Array.isArray(evidence?.findings) ? evidence.findings : [];
428
+ const findingReview = evidence?.finding_review ?? {};
429
+ const actionCandidates = Array.isArray(evidence?.action_candidates) ? evidence.action_candidates : [];
430
+ const refactoringCampaigns = Array.isArray(evidence?.refactoring_campaigns) ? evidence.refactoring_campaigns : [];
431
+ const tasks = Array.isArray(taskState?.tasks) ? taskState.tasks : [];
432
+ const artifacts = latestRun.artifacts ?? {};
433
+ const scanHeading = architectureProfile.app_type === 'static_site' ? '静的サイト診断' : '共通スキャン';
434
+ return `# Story診断レポート
435
+
436
+ ## Story
437
+
438
+ | 項目 | 内容 |
439
+ |------|------|
440
+ | Story ID | ${story.story_id} |
441
+ | Story | ${story.title} |
442
+ | Status | ${story.status ?? 'active'} |
443
+ | View | ${story.view ?? '-'} |
444
+ | Period | ${story.period ?? '-'} |
445
+
446
+ ## 最新run
447
+
448
+ | 項目 | 内容 |
449
+ |------|------|
450
+ | Run ID | ${latestRun.run_id} |
451
+ | Gate | ${latestRun.gate_status ?? '-'} |
452
+ | Created At | ${latestRun.created_at ?? '-'} |
453
+ | Story run数 | ${runs.length} |
454
+
455
+ ## graphify集計
456
+
457
+ | 項目 | 内容 |
458
+ |------|------|
459
+ | graphify nodes | ${graphify.node_count ?? 0} |
460
+ | graphify edges | ${graphify.edge_count ?? 0} |
461
+ | extracted edges | ${graphify.extracted_edges?.length ?? 0} |
462
+ | inferred edges | ${graphify.inferred_edges?.length ?? 0} |
463
+ | ambiguous edges | ${graphify.ambiguous_edges?.length ?? 0} |
464
+
465
+ ## 構造プロファイル
466
+
467
+ | 項目 | 内容 |
468
+ |------|------|
469
+ | 種別 | ${architectureProfile.app_type ?? 'unknown'} |
470
+ | System type | ${architectureProfile.system_type ?? 'unknown'} |
471
+ | 描画方式 | ${architectureProfile.rendering ?? '-'} |
472
+ | API route | ${architectureProfile.has_api_routes ? 'あり' : 'なし'} |
473
+ | DB | ${architectureProfile.has_database ? (architectureProfile.database ?? []).join(', ') || 'あり' : 'なし'} |
474
+ | 認証 | ${architectureProfile.has_auth ? (architectureProfile.auth ?? []).join(', ') || 'あり' : 'なし'} |
475
+ | 適用チェック | ${applicableChecks.join(', ') || '-'} |
476
+
477
+ ### View
478
+
479
+ ${renderStoryArchitectureViews(architectureProfile.views ?? {})}
480
+
481
+ ## API境界
482
+
483
+ ${renderStoryApiBoundary(apiBoundary)}
484
+
485
+ ## ${scanHeading}
486
+
487
+ | 項目 | 内容 |
488
+ |------|------|
489
+ | index.html | ${staticSite.has_index_html ? 'あり' : 'なし'} |
490
+ | scanned files | ${staticSite.scanned_files ?? 0} |
491
+ | secret hits | ${formatRiskCount(staticSite.secret_hits?.length ?? 0, staticSite.risk_summary?.secret_hits)} |
492
+ | XSS risk hits | ${formatRiskCount(staticSite.xss_risk_hits?.length ?? 0, staticSite.risk_summary?.xss_risk_hits)} |
493
+ | external resources | ${staticSite.external_resources?.length ?? 0} |
494
+ | non static files | ${staticSite.non_static_files?.length ?? 0} |
495
+ | refactoring campaigns | ${refactoringCampaigns.length} |
496
+
497
+ ## 検出事項
498
+
499
+ ${findings.length === 0 ? '- なし' : findings.map((finding) => `- ${finding.id}: ${finding.title}(${finding.severity})`).join('\n')}
500
+
501
+ ## 診断レビュー
502
+
503
+ ${renderStoryFindingReview(findingReview)}
504
+
505
+ ## 次アクション候補
506
+
507
+ ${renderStoryActionCandidates(actionCandidates)}
508
+
509
+ ## 生成タスク
510
+
511
+ ${renderGeneratedTasks(tasks)}
512
+
513
+ ## Artifacts
514
+
515
+ ${Object.entries(artifacts).length === 0 ? '- なし' : Object.entries(artifacts).map(([key, value]) => `- ${key}: ${value}`).join('\n')}
516
+
517
+ ## 次に見るファイル
518
+
519
+ - ${artifacts.summary ?? '-'}
520
+ - ${artifacts.risk_register ?? '-'}
521
+ - ${artifacts.evidence ?? '-'}
522
+ `;
523
+ }
524
+
525
+ function renderStoryFindingReview(findingReview) {
526
+ const summary = findingReview?.summary ?? {};
527
+ const items = Array.isArray(findingReview?.items) ? findingReview.items : [];
528
+ return `- Status: ${findingReview?.status ?? 'unknown'}
529
+ - 未レビュー: ${summary.unreviewed ?? 0}件
530
+ - suggested implementation_gap: ${summary.implementation_gap ?? 0}件
531
+ - suggested detector_gap: ${summary.detector_gap ?? 0}件
532
+
533
+ | Finding | Status | Suggested |
534
+ |---------|--------|-----------|
535
+ ${items.length === 0 ? '| - | - | - |' : items.map((item) => `| ${item.finding_id} | ${item.review_status} | ${item.suggested_classification} |`).join('\n')}`;
536
+ }
537
+
538
+ function renderStoryApiBoundary(apiBoundary) {
539
+ if (!apiBoundary) return '- api-boundary は適用されていない';
540
+ const rows = Object.entries(apiBoundary.summary ?? {})
541
+ .map(([classification, count]) => `| ${classification} | ${count} |`)
542
+ .join('\n');
543
+ const protectionRows = Object.entries(apiBoundary.protection_summary ?? {})
544
+ .map(([status, count]) => `| ${status} | ${count} |`)
545
+ .join('\n');
546
+ return `### 分類別
547
+
548
+ | 分類 | 件数 |
549
+ |------|------|
550
+ ${rows || '| - | 0 |'}
551
+
552
+ ### 保護状態別
553
+
554
+ | 保護状態 | 件数 |
555
+ |----------|------|
556
+ ${protectionRows || '| - | 0 |'}`;
557
+ }
558
+
559
+ function renderStoryActionCandidates(candidates) {
560
+ if (candidates.length === 0) return '- なし';
561
+ return `| ID | 対応する検出事項 | 候補 | 対象 | Impact | Community | 読むファイル | 方針 |
562
+ |----|------------------|------|------|--------|-----------|------------|------|
563
+ ${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')}
564
+
565
+ ${renderImplementationPlans(candidates)}`;
566
+ }
567
+
568
+ function renderGeneratedTasks(tasks) {
569
+ if (tasks.length === 0) return '- なし';
570
+ return `| ID | 対応する検出事項 | 優先度 | 対象 | グループ | 方針 |
571
+ |----|------------------|--------|------|----------|------|
572
+ ${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')}`;
573
+ }
574
+
575
+ function formatTargetGroups(groups = []) {
576
+ if (!Array.isArray(groups) || groups.length === 0) return '-';
577
+ return groups.map((group) => `${group.id}(${group.route_count})`).join(', ');
578
+ }
579
+
580
+ function formatRiskCount(count, summary = {}) {
581
+ return `${count}件 (block: ${summary.block ?? 0}件, review: ${summary.review ?? 0}件, info: ${summary.info ?? 0}件)`;
582
+ }
583
+
584
+ function formatGraphImpact(graphContext) {
585
+ if (!graphContext) return '-';
586
+ return `${graphContext.impact_score ?? 0} (${graphContext.related_edge_count ?? 0} edges)`;
587
+ }
588
+
589
+ function formatGraphCommunities(graphContext) {
590
+ const communities = graphContext?.affected_communities ?? [];
591
+ if (communities.length === 0) return '-';
592
+ return communities
593
+ .slice(0, 3)
594
+ .map((community) => {
595
+ const scope = (community.route_count ?? 0) > 0
596
+ ? `route: ${community.route_count}`
597
+ : `file: ${community.file_count ?? 0}`;
598
+ return `${community.id}(${scope}, node: ${community.node_count}, edge: ${community.edge_count})`;
599
+ })
600
+ .join(', ');
601
+ }
602
+
603
+ function formatReadFirstFiles(implementationPlan) {
604
+ const files = implementationPlan?.read_first_files ?? [];
605
+ if (files.length === 0) return '-';
606
+ return selectRepresentativeReadFirstFiles(files, implementationPlan?.pre_fix_briefing).map((item) => item.file).join('<br>');
607
+ }
608
+
609
+ function selectRepresentativeReadFirstFiles(files, briefing) {
610
+ const selected = [];
611
+ const seen = new Set();
612
+ const add = (item) => {
613
+ if (!item || seen.has(item.file)) return;
614
+ seen.add(item.file);
615
+ selected.push(item);
616
+ };
617
+ const helpers = briefing?.auth_helpers ?? [];
618
+ const helperFiles = new Set(helpers.map((helper) => helper.file));
619
+ const hasSignatureHelper = helpers.some((helper) => helper.category === 'signature');
620
+ add(files[0]);
621
+ add(files.find((item) => helperFiles.has(item.file)));
622
+ add(files.find((item) => item.reason.includes('graphify hub') && helperFiles.has(item.file)));
623
+ if (!hasSignatureHelper) add(files.find((item) => item.reason.includes('middleware')));
624
+ for (const item of files) add(item);
625
+ return selected.slice(0, 3);
626
+ }
627
+
628
+ function renderImplementationPlans(candidates) {
629
+ const items = candidates.filter((candidate) => candidate.implementation_plan);
630
+ if (items.length === 0) return '';
631
+ return `### 実装手順
632
+
633
+ ${items.map((candidate) => renderImplementationPlan(candidate)).join('\n\n')}`;
634
+ }
635
+
636
+ function renderImplementationPlan(candidate) {
637
+ const plan = candidate.implementation_plan;
638
+ return `#### ${candidate.id}: ${candidate.title}
639
+
640
+ - 優先度: ${plan.priority}
641
+ - 理由: ${plan.rationale}
642
+ - 読むファイル: ${plan.read_first_files.length === 0 ? '-' : plan.read_first_files.map((item) => `${item.file}(${item.reason})`).join(', ')}
643
+
644
+ ${renderPreFixBriefing(plan.pre_fix_briefing)}
645
+
646
+ ${plan.steps.map((step, index) => `${index + 1}. ${step.title}: ${step.detail}`).join('\n')}
647
+
648
+ 完了条件:
649
+ ${plan.acceptance_criteria.map((item) => `- ${item}`).join('\n')}`;
650
+ }
651
+
652
+ function renderPreFixBriefing(briefing) {
653
+ if (!briefing) return '';
654
+ if (briefing.opportunity) {
655
+ return `修正前ブリーフィング:
656
+ - リファクタリング機会: ${briefing.opportunity.id} / ${briefing.opportunity.refactoring_intent}
657
+ - Campaign: ${briefing.campaign?.id ?? '-'} / rank=${briefing.campaign?.rank ?? '-'}
658
+ - 推奨抽象化: ${briefing.opportunity.suggested_abstraction?.label ?? '-'}
659
+ - 対象ファイル: ${briefing.target_files?.slice(0, 5).join(', ') || '-'}
660
+ - 推奨方針: ${briefing.recommended_strategy?.id ?? '-'} - ${briefing.recommended_strategy?.reason ?? '-'}
661
+ - 方針: ${briefing.strategy_options?.map((option) => option.label).join(' / ') || '-'}`;
662
+ }
663
+ return `修正前ブリーフィング:
664
+ - 現在の境界: middleware excludes_api=${briefing.current_boundary?.middleware?.excludes_api ?? false}, route protection=${formatInlineSummary(briefing.current_boundary?.route_protection ?? {})}
665
+ - 認証/署名候補: ${formatAuthHelpers(briefing.auth_helpers)}
666
+ - 対象route: ${briefing.target_routes?.slice(0, 5).map((route) => `${route.route_path} (${route.methods.join(', ') || '-'})`).join(', ') || '-'}
667
+ - 推奨方針: ${briefing.recommended_strategy?.id ?? '-'} - ${briefing.recommended_strategy?.reason ?? '-'}
668
+ - 方針: ${briefing.strategy_options?.map((option) => option.label).join(' / ') || '-'}`;
669
+ }
670
+
671
+ function formatAuthHelpers(helpers = []) {
672
+ if (helpers.length === 0) return '-';
673
+ return helpers
674
+ .slice(0, 5)
675
+ .map((helper) => `${formatHelperCategory(helper.category)}${helper.file}${helper.functions.length > 0 ? `:${helper.functions.slice(0, 3).join(',')}` : ''}`)
676
+ .join(', ');
677
+ }
678
+
679
+ function formatHelperCategory(category) {
680
+ const labels = {
681
+ auth: '認証:',
682
+ signature: '署名:',
683
+ environment: '環境:'
684
+ };
685
+ return labels[category] ?? '';
686
+ }
687
+
688
+ function formatInlineSummary(summary = {}) {
689
+ const entries = Object.entries(summary);
690
+ if (entries.length === 0) return '-';
691
+ return entries.map(([key, count]) => `${key}: ${count}件`).join(', ');
692
+ }
693
+
694
+ async function readExplicitStoryTasks(root, catalog) {
695
+ const tasks = [];
696
+ for (const story of catalog?.stories ?? []) {
697
+ const docs = story.derived?.meaning?.evidence_by_type?.docs_evidence ?? [];
698
+ const storyDocs = docs.filter((file) => /^docs\/management\/stories\//.test(file));
699
+ for (const file of storyDocs) {
700
+ try {
701
+ const content = await readFile(path.join(root, file), 'utf8');
702
+ tasks.push(...extractExplicitStoryTasks(content, { story, file }));
703
+ } catch (error) {
704
+ if (error.code !== 'ENOENT') throw error;
705
+ }
706
+ }
707
+ }
708
+ return tasks;
709
+ }
710
+
711
+ function extractExplicitStoryTasks(content, { story, file }) {
712
+ const section = extractRawMarkdownSection(content, [
713
+ '初期タスク',
714
+ '実装タスク',
715
+ 'タスク',
716
+ 'Initial Tasks',
717
+ 'Implementation Tasks',
718
+ 'Tasks'
719
+ ]);
720
+ if (!section) return [];
721
+ const items = [];
722
+ let current = null;
723
+ const flush = () => {
724
+ if (!current) return;
725
+ const title = cleanMarkdownInline(current.title);
726
+ if (!title) return;
727
+ const acceptance = current.details
728
+ .map((line) => cleanMarkdownInline(line.replace(/^[-*]\s+/, '').trim()))
729
+ .filter(Boolean);
730
+ const id = `${story.story_id}-${String(items.length + 1).padStart(2, '0')}-${slugifyTaskId(title)}`;
731
+ items.push({
732
+ id,
733
+ story_id: story.story_id,
734
+ title,
735
+ purpose: acceptance[0] ?? `${title}を実装可能なタスクとして進める`,
736
+ acceptance,
737
+ priority: 'medium',
738
+ source_type: 'story_explicit_task',
739
+ source_file: file,
740
+ target_files: [],
741
+ read_first_files: [{ file, reason: 'Story本文の明示タスク定義' }],
742
+ graph_context: null,
743
+ recommended_strategy: {
744
+ id: 'story-explicit-task',
745
+ reason: 'Story本文の明示タスクとして定義されている'
746
+ },
747
+ implementation_steps: acceptance.length === 0
748
+ ? [{ id: 'implement-task', title, detail: `${title}を実装し、Storyの受け入れ基準と対応させる` }]
749
+ : acceptance.map((detail, index) => ({
750
+ id: `step-${index + 1}`,
751
+ title: detail,
752
+ detail
753
+ })),
754
+ suggested_command: `vibepro task create . --from-plan --id ${story.story_id} --task ${id}`,
755
+ execution_policy: 'proposal_only',
756
+ mutates_repository: false
757
+ });
758
+ current = null;
759
+ };
760
+ for (const line of section.split(/\r?\n/)) {
761
+ const ordered = line.match(/^\s*(\d+)[.)]\s+(.+?)\s*$/);
762
+ if (ordered) {
763
+ flush();
764
+ current = { title: ordered[2], details: [] };
765
+ continue;
766
+ }
767
+ const bullet = line.match(/^\s{1,}[-*]\s+(.+?)\s*$/);
768
+ if (bullet && current) {
769
+ current.details.push(bullet[1]);
770
+ }
771
+ }
772
+ flush();
773
+ return dedupeBy(items, (item) => item.id);
774
+ }
775
+
776
+ function extractRawMarkdownSection(content, headings) {
777
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
778
+ const escaped = headings.map(escapeRegExp).join('|');
779
+ const pattern = new RegExp(`^#{2,4}\\s+(?:${escaped})\\s*\\n([\\s\\S]*?)(?=^#{2,4}\\s+|(?![\\s\\S]))`, 'im');
780
+ const match = body.match(pattern);
781
+ return match?.[1]?.trim() ?? null;
782
+ }
783
+
784
+ function escapeRegExp(value) {
785
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
786
+ }
787
+
788
+ function cleanMarkdownInline(value) {
789
+ return String(value ?? '')
790
+ .replace(/\*\*/g, '')
791
+ .replace(/`/g, '')
792
+ .replace(/^\[[ xX]\]\s+/, '')
793
+ .trim();
794
+ }
795
+
796
+ function slugifyTaskId(value) {
797
+ const ascii = String(value ?? '')
798
+ .replace(/`/g, '')
799
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
800
+ .toLowerCase()
801
+ .replace(/[^a-z0-9]+/g, '-')
802
+ .replace(/^-+|-+$/g, '');
803
+ if (ascii) return ascii;
804
+ let hash = 0;
805
+ for (const char of String(value ?? 'task')) {
806
+ hash = ((hash << 5) - hash + char.codePointAt(0)) | 0;
807
+ }
808
+ return `task-${Math.abs(hash).toString(36)}`;
809
+ }
810
+
811
+ function dedupeBy(items, keyFn) {
812
+ const seen = new Set();
813
+ const result = [];
814
+ for (const item of items) {
815
+ const key = keyFn(item);
816
+ if (seen.has(key)) continue;
817
+ seen.add(key);
818
+ result.push(item);
819
+ }
820
+ return result;
821
+ }
822
+
823
+ function buildStoryExecutionPlan(catalog, options = {}) {
824
+ const stories = Array.isArray(catalog?.stories) ? catalog.stories : [];
825
+ const scoredStories = stories
826
+ .map((story) => scoreStoryForExecution(story, catalog, options.graphIndex))
827
+ .sort((a, b) => b.score - a.score || a.story_id.localeCompare(b.story_id));
828
+ const priorityStories = scoredStories.slice(0, options.limit ?? 5);
829
+ const baseTaskCandidates = [
830
+ ...buildWarningTaskCandidates(catalog),
831
+ ...priorityStories.flatMap((story) => buildTaskCandidatesForStory(story)),
832
+ ...selectExplicitStoryTasks(options.explicitStoryTasks ?? [], scoredStories)
833
+ ];
834
+ const warnings = catalog.source?.warnings ?? [];
835
+ const sourceConsistency = summarizeSourceConsistency(scoredStories);
836
+ const sourceRecoveryMap = buildSourceRecoveryMap(scoredStories, baseTaskCandidates);
837
+ const sourceAlignmentFindings = buildSourceAlignmentFindings(scoredStories);
838
+ const taskCandidates = [
839
+ ...baseTaskCandidates,
840
+ ...buildSourceAlignmentTaskCandidates(sourceAlignmentFindings.items, scoredStories)
841
+ ];
842
+ const questions = buildPlanQuestions(catalog, priorityStories, sourceAlignmentFindings);
843
+ return {
844
+ schema_version: '0.1.0',
845
+ generated_at: new Date().toISOString(),
846
+ source: {
847
+ tool: 'vibepro',
848
+ catalog_generated_at: catalog.generated_at ?? null,
849
+ run_id: catalog.source?.run_id ?? null,
850
+ warnings
851
+ },
852
+ summary: {
853
+ story_count: stories.length,
854
+ coverage_status: catalog.coverage?.status ?? 'unavailable',
855
+ coverage_ratio: catalog.coverage?.totals?.coverage_ratio ?? null,
856
+ uncovered_files: catalog.coverage?.totals?.uncovered_files ?? 0,
857
+ open_question_count: Array.isArray(catalog.open_questions) ? catalog.open_questions.length : 0,
858
+ warning_count: warnings.length,
859
+ source_consistency_status: sourceConsistency.status,
860
+ source_recovery_story_count: sourceConsistency.needs_recovery_story_count,
861
+ source_missing_spec_count: sourceRecoveryMap.counts.missing_spec,
862
+ source_missing_architecture_count: sourceRecoveryMap.counts.missing_architecture,
863
+ source_alignment_finding_count: sourceAlignmentFindings.counts.total,
864
+ source_alignment_high_count: sourceAlignmentFindings.counts.high
865
+ },
866
+ source_consistency: sourceConsistency,
867
+ source_recovery_map: sourceRecoveryMap,
868
+ source_alignment_findings: sourceAlignmentFindings,
869
+ questions,
870
+ priority_stories: priorityStories,
871
+ task_candidates: taskCandidates,
872
+ next_commands: buildStoryPlanNextCommands(priorityStories)
873
+ };
874
+ }
875
+
876
+ function scoreStoryForExecution(story, catalog, graphIndex = null) {
877
+ const fields = new Set((story.derived?.open_questions ?? []).map((item) => item.field));
878
+ const meaning = story.derived?.meaning ?? {};
879
+ const storyContract = story.derived?.story_contract ?? null;
880
+ const sourceType = story.source?.type ?? '';
881
+ const targetFiles = resolveStoryPlanTargetFiles(story, graphIndex);
882
+ const graphContext = buildGraphContextForFiles(targetFiles, graphIndex);
883
+ const reasons = [];
884
+ let score = 0;
885
+
886
+ const add = (points, reason) => {
887
+ score += points;
888
+ reasons.push(reason);
889
+ };
890
+
891
+ if (sourceType === 'diagnosis') add(40, '診断Finding由来で短期修正候補');
892
+ if (story.category === 'security') add(30, 'security Storyは公開前リスクに直結');
893
+ if (catalog.coverage?.status === 'warn') add(15, 'Graph CoverageがwarnでStory根拠の見直しが必要');
894
+ if (fields.has('missing_spec')) add(24, 'コード由来だが仕様/Story根拠が不足');
895
+ if (fields.has('business_metric')) add(14, 'KPIまたは効果測定指標が未定');
896
+ if (fields.has('business_context')) add(10, 'biz視点の意味づけが不足');
897
+ if (fields.has('story_contract_source_role')) add(32, 'Story Contractでsource roleの誤読リスクが未解決');
898
+ if (storyContract?.status === 'needs_clarification') add(18, 'Story Contractが開発可能な契約として未解決');
899
+ if (fields.has('period')) add(6, 'NocoDB Periodが未確定');
900
+ if (meaning.confidence === 'low') add(18, 'Story意味づけの総合信頼度が低い');
901
+ if (meaning.confidence === 'medium') add(8, 'Story意味づけに確認余地がある');
902
+ if (sourceType === 'code_surface') add(8, 'コードから逆算したStoryで人間レビューが必要');
903
+ if (story.view === 'business' && story.category === 'product') add(5, 'ユーザー価値と事業価値に近いproduct Story');
904
+ score += workflowStageWeight(meaning.workflow_position?.stage);
905
+ const sourceRecovery = buildSourceRecoveryForStory(story, graphContext);
906
+ if (sourceRecovery.status !== 'aligned') add(18, 'Story/Spec/Architecture正本の復元または確認が必要');
907
+
908
+ return {
909
+ story_id: story.story_id,
910
+ title: story.title,
911
+ score,
912
+ category: story.category ?? null,
913
+ view: story.view ?? null,
914
+ horizon: story.horizon ?? null,
915
+ period: story.period ?? null,
916
+ source_type: sourceType,
917
+ confidence: meaning.confidence ?? story.derived?.confidence ?? 'unknown',
918
+ story_type: storyContract?.story_type ?? null,
919
+ story_contract: storyContract,
920
+ workflow_stage: meaning.workflow_position?.stage ?? 'unknown',
921
+ target_files: targetFiles,
922
+ read_first_files: resolveStoryPlanReadFirstFiles(story, graphContext),
923
+ graph_context: graphContext,
924
+ acceptance_focus: story.derived?.story_definition?.acceptance_focus ?? [],
925
+ derived: {
926
+ open_questions: story.derived?.open_questions ?? [],
927
+ story_contract: storyContract
928
+ },
929
+ source_recovery: sourceRecovery,
930
+ reasons,
931
+ next_command: `vibepro story select . --id ${story.story_id}`
932
+ };
933
+ }
934
+
935
+ function buildSourceRecoveryForStory(story, graphContext = null) {
936
+ const meaning = story.derived?.meaning ?? {};
937
+ const docs = meaning.evidence_by_type?.docs_evidence ?? [];
938
+ const codeFiles = [
939
+ ...(meaning.evidence_by_type?.code_evidence ?? []),
940
+ ...(story.source?.paths ?? []).filter((item) => typeof item === 'string' && item.startsWith('src/'))
941
+ ];
942
+ const definition = story.derived?.story_definition ?? {};
943
+ const openQuestions = story.derived?.open_questions ?? [];
944
+ const storyDocs = docs.filter(isStoryDocPath);
945
+ const specDocs = docs.filter(isSpecDocPath);
946
+ const architectureDocs = docs.filter(isArchitectureDocPath);
947
+ const hasMissingSpec = openQuestions.some((item) => item.field === 'missing_spec');
948
+ const boundarySignals = inferArchitectureBoundarySignals(story, codeFiles);
949
+ const requiresDesignFirstSources = isDesignFirstStory(story);
950
+ const storyStatus = storyDocs.length > 0 ? 'present' : story.source?.type === 'code_surface' ? 'derived' : 'implicit';
951
+ const specStatus = specDocs.length > 0
952
+ ? 'present'
953
+ : storyDocs.length > 0 && !hasMissingSpec && !requiresDesignFirstSources
954
+ ? 'story_backed'
955
+ : 'needs_recovery';
956
+ const architectureStatus = architectureDocs.length > 0
957
+ ? 'present'
958
+ : boundarySignals.length > 0 || requiresDesignFirstSources
959
+ ? 'needs_decision'
960
+ : 'implicit';
961
+ const status = specStatus === 'needs_recovery' || architectureStatus === 'needs_decision'
962
+ ? 'needs_recovery'
963
+ : storyStatus === 'derived'
964
+ ? 'needs_review'
965
+ : 'aligned';
966
+ const drafts = [];
967
+ if (specStatus === 'needs_recovery') {
968
+ drafts.push(buildSpecRecoveryDraft({ story, definition, codeFiles, graphContext }));
969
+ }
970
+ if (architectureStatus === 'needs_decision') {
971
+ drafts.push(buildArchitectureRecoveryDraft({ story, codeFiles, boundarySignals, graphContext }));
972
+ }
973
+ return {
974
+ status,
975
+ graph_context: graphContext,
976
+ sources: {
977
+ story: {
978
+ status: storyStatus,
979
+ refs: storyDocs
980
+ },
981
+ spec: {
982
+ status: specStatus,
983
+ refs: specDocs
984
+ },
985
+ architecture: {
986
+ status: architectureStatus,
987
+ refs: architectureDocs,
988
+ signals: boundarySignals
989
+ }
990
+ },
991
+ drafts,
992
+ checks: [
993
+ 'Storyのwho/problem/outcomeが人間レビュー済みか',
994
+ 'Specの受け入れ基準がコード分岐と対応しているか',
995
+ 'Architecture/ADRの境界判断がGraphと変更範囲に対応しているか',
996
+ ...(requiresDesignFirstSources ? ['設計変更Storyでは、実装前にArchitecture判断とSpec契約を正本化しているか'] : [])
997
+ ]
998
+ };
999
+ }
1000
+
1001
+ function isDesignFirstStory(story) {
1002
+ const text = [
1003
+ story.story_id,
1004
+ story.title,
1005
+ story.category,
1006
+ story.source?.type,
1007
+ ...(story.derived?.story_definition?.acceptance_focus ?? []),
1008
+ ...(story.derived?.meaning?.counter_evidence ?? []),
1009
+ ...(story.derived?.open_questions ?? []).map((item) => item.question)
1010
+ ]
1011
+ .filter(Boolean)
1012
+ .join('\n')
1013
+ .toLowerCase();
1014
+ if (story.category === 'architecture') return true;
1015
+ if (/(architecture|adr|spec|contract|gate|preset|story derive|repo profile|applicability|boundary|design[-_ ]?first)/i.test(text)) return true;
1016
+ return false;
1017
+ }
1018
+
1019
+ function buildSpecRecoveryDraft({ story, definition, codeFiles, graphContext }) {
1020
+ return {
1021
+ kind: 'spec',
1022
+ status: 'draft_from_code',
1023
+ suggested_path: `docs/specs/${slugifyStoryId(story.story_id)}.md`,
1024
+ title: `${story.title} Spec`,
1025
+ must_include: [
1026
+ `対象ユーザー: ${definition.who ?? 'TODO'}`,
1027
+ `課題: ${definition.problem ?? 'TODO'}`,
1028
+ `成功状態: ${definition.outcome ?? definition.want ?? 'TODO'}`,
1029
+ ...(definition.acceptance_focus ?? []).map((item) => `受け入れ基準: ${item}`)
1030
+ ].slice(0, 12),
1031
+ evidence_files: buildRecoveryEvidenceFiles(codeFiles, graphContext),
1032
+ graph_evidence: buildRecoveryGraphEvidence(graphContext),
1033
+ unresolved_questions: [
1034
+ 'このSpecをStory正本から確定できるか',
1035
+ 'コードから逆算した条件のうち、意図ではなく偶然の実装はどれか',
1036
+ 'Graphify上の関連ファイルも同じ受け入れ基準に含めるべきか',
1037
+ '成功指標または運用上の完了条件は何か'
1038
+ ]
1039
+ };
1040
+ }
1041
+
1042
+ function buildArchitectureRecoveryDraft({ story, codeFiles, boundarySignals, graphContext }) {
1043
+ return {
1044
+ kind: 'architecture',
1045
+ status: 'decision_needed',
1046
+ suggested_path: `docs/architecture/ADR-${slugifyStoryId(story.story_id)}.md`,
1047
+ title: `${story.title} Architecture Decision`,
1048
+ decision_needed: [
1049
+ '既存Architecture内の変更として扱えるか、新しいADRが必要か',
1050
+ `境界シグナル: ${boundarySignals.join(', ')}`,
1051
+ '変更対象の責務境界、データ境界、外部連携境界をどこに置くか'
1052
+ ],
1053
+ evidence_files: buildRecoveryEvidenceFiles(codeFiles, graphContext),
1054
+ graph_evidence: buildRecoveryGraphEvidence(graphContext),
1055
+ unresolved_questions: [
1056
+ 'Graph上のhubやcommunityと変更範囲が一致しているか',
1057
+ 'API/DB/Auth/外部連携の境界をどの層で守るか',
1058
+ 'ADR不要とする場合、その理由をStory frontmatterに残せるか'
1059
+ ]
1060
+ };
1061
+ }
1062
+
1063
+ function buildRecoveryEvidenceFiles(codeFiles, graphContext) {
1064
+ return [
1065
+ ...(codeFiles ?? []),
1066
+ ...(graphContext?.related_files ?? []),
1067
+ ...(graphContext?.hub_nodes ?? []).map((node) => node.source_file).filter(Boolean)
1068
+ ]
1069
+ .filter(Boolean)
1070
+ .filter((file, index, files) => files.indexOf(file) === index)
1071
+ .slice(0, 12);
1072
+ }
1073
+
1074
+ function buildRecoveryGraphEvidence(graphContext) {
1075
+ if (!graphContext) return null;
1076
+ return {
1077
+ matched_files: graphContext.matched_files ?? [],
1078
+ related_files: graphContext.related_files ?? [],
1079
+ hub_nodes: graphContext.hub_nodes ?? [],
1080
+ affected_communities: graphContext.affected_communities ?? [],
1081
+ related_edge_count: graphContext.related_edge_count ?? 0,
1082
+ impact_score: graphContext.impact_score ?? 0,
1083
+ cross_community: Boolean(graphContext.cross_community)
1084
+ };
1085
+ }
1086
+
1087
+ function summarizeSourceConsistency(stories) {
1088
+ const items = stories.map((story) => ({
1089
+ story_id: story.story_id,
1090
+ title: story.title,
1091
+ status: story.source_recovery?.status ?? 'unknown',
1092
+ story_status: story.source_recovery?.sources?.story?.status ?? 'unknown',
1093
+ spec_status: story.source_recovery?.sources?.spec?.status ?? 'unknown',
1094
+ architecture_status: story.source_recovery?.sources?.architecture?.status ?? 'unknown'
1095
+ }));
1096
+ const counts = countBy(items, (item) => item.status);
1097
+ const needsRecovery = items.filter((item) => item.status === 'needs_recovery');
1098
+ return {
1099
+ schema_version: '0.1.0',
1100
+ status: needsRecovery.length > 0 ? 'needs_recovery' : counts.needs_review > 0 ? 'needs_review' : 'aligned',
1101
+ counts,
1102
+ needs_recovery_story_count: needsRecovery.length,
1103
+ top_needs_recovery: needsRecovery.slice(0, 10),
1104
+ stories: items
1105
+ };
1106
+ }
1107
+
1108
+ function buildSourceRecoveryMap(stories, taskCandidates = []) {
1109
+ const taskIds = new Set(taskCandidates.map((candidate) => candidate.id));
1110
+ const rows = stories
1111
+ .map((story) => buildSourceRecoveryMapRow(story, taskIds))
1112
+ .sort((a, b) => sourceRecoveryMapRank(a) - sourceRecoveryMapRank(b) || b.score - a.score || a.story_id.localeCompare(b.story_id));
1113
+ const missingRows = rows.filter((row) => row.spec.status === 'needs_recovery' || row.architecture.status === 'needs_decision');
1114
+ return {
1115
+ schema_version: '0.1.0',
1116
+ status: missingRows.length > 0 ? 'needs_recovery' : rows.some((row) => row.status === 'needs_review') ? 'needs_review' : 'aligned',
1117
+ counts: {
1118
+ stories: rows.length,
1119
+ needs_recovery: missingRows.length,
1120
+ missing_spec: rows.filter((row) => row.spec.status === 'needs_recovery').length,
1121
+ missing_architecture: rows.filter((row) => row.architecture.status === 'needs_decision').length,
1122
+ aligned: rows.filter((row) => row.status === 'aligned').length
1123
+ },
1124
+ missing: missingRows,
1125
+ rows
1126
+ };
1127
+ }
1128
+
1129
+ function buildSourceRecoveryMapRow(story, taskIds) {
1130
+ const recovery = story.source_recovery ?? {};
1131
+ const specDraft = (recovery.drafts ?? []).find((draft) => draft.kind === 'spec');
1132
+ const architectureDraft = (recovery.drafts ?? []).find((draft) => draft.kind === 'architecture');
1133
+ const graph = story.graph_context ?? recovery.graph_context ?? {};
1134
+ const specTaskId = `${story.story_id}-spec-recovery`;
1135
+ const architectureTaskId = `${story.story_id}-architecture-recovery`;
1136
+ return {
1137
+ story_id: story.story_id,
1138
+ title: story.title,
1139
+ score: story.score,
1140
+ status: recovery.status ?? 'unknown',
1141
+ story_source: {
1142
+ status: recovery.sources?.story?.status ?? 'unknown',
1143
+ refs: recovery.sources?.story?.refs ?? []
1144
+ },
1145
+ spec: {
1146
+ status: recovery.sources?.spec?.status ?? 'unknown',
1147
+ refs: recovery.sources?.spec?.refs ?? [],
1148
+ suggested_path: specDraft?.suggested_path ?? null,
1149
+ draft_title: specDraft?.title ?? null,
1150
+ suggested_task_id: specDraft ? specTaskId : null,
1151
+ task_candidate_id: taskIds.has(specTaskId) ? specTaskId : null
1152
+ },
1153
+ architecture: {
1154
+ status: recovery.sources?.architecture?.status ?? 'unknown',
1155
+ refs: recovery.sources?.architecture?.refs ?? [],
1156
+ signals: recovery.sources?.architecture?.signals ?? [],
1157
+ suggested_path: architectureDraft?.suggested_path ?? null,
1158
+ draft_title: architectureDraft?.title ?? null,
1159
+ suggested_task_id: architectureDraft ? architectureTaskId : null,
1160
+ task_candidate_id: taskIds.has(architectureTaskId) ? architectureTaskId : null
1161
+ },
1162
+ graph: {
1163
+ matched_file_count: graph.matched_file_count ?? 0,
1164
+ matched_files: (graph.matched_files ?? []).slice(0, 12),
1165
+ related_edge_count: graph.related_edge_count ?? 0,
1166
+ related_files: (graph.related_files ?? []).slice(0, 8),
1167
+ hub_files: (graph.hub_nodes ?? []).map((node) => node.source_file).filter(Boolean).slice(0, 5),
1168
+ affected_communities: graph.affected_communities ?? [],
1169
+ cross_community: Boolean(graph.cross_community)
1170
+ }
1171
+ };
1172
+ }
1173
+
1174
+ function sourceRecoveryMapRank(row) {
1175
+ if (row.spec.status === 'needs_recovery' && row.architecture.status === 'needs_decision') return 0;
1176
+ if (row.architecture.status === 'needs_decision') return 1;
1177
+ if (row.spec.status === 'needs_recovery') return 2;
1178
+ if (row.status === 'needs_review') return 3;
1179
+ return 4;
1180
+ }
1181
+
1182
+ function buildSourceAlignmentFindings(stories) {
1183
+ const items = stories
1184
+ .flatMap((story) => buildSourceAlignmentFindingsForStory(story))
1185
+ .sort((a, b) => sourceAlignmentSeverityRank(a.severity) - sourceAlignmentSeverityRank(b.severity)
1186
+ || (b.graph?.impact_score ?? 0) - (a.graph?.impact_score ?? 0)
1187
+ || (b.graph?.related_edge_count ?? 0) - (a.graph?.related_edge_count ?? 0)
1188
+ || a.story_id.localeCompare(b.story_id)
1189
+ || a.type.localeCompare(b.type));
1190
+ const countsBySeverity = countBy(items, (item) => item.severity);
1191
+ const countsByType = countBy(items, (item) => item.type);
1192
+ return {
1193
+ schema_version: '0.1.0',
1194
+ status: items.some((item) => item.severity === 'high')
1195
+ ? 'needs_review'
1196
+ : items.length > 0
1197
+ ? 'watch'
1198
+ : 'aligned',
1199
+ counts: {
1200
+ total: items.length,
1201
+ high: countsBySeverity.high ?? 0,
1202
+ medium: countsBySeverity.medium ?? 0,
1203
+ low: countsBySeverity.low ?? 0,
1204
+ by_type: countsByType
1205
+ },
1206
+ top: items.slice(0, 10),
1207
+ items: items.slice(0, 50)
1208
+ };
1209
+ }
1210
+
1211
+ function buildSourceAlignmentFindingsForStory(story) {
1212
+ const recovery = story.source_recovery ?? {};
1213
+ const storyContract = story.story_contract ?? story.derived?.story_contract ?? null;
1214
+ const sourceStatus = recovery.sources?.story?.status ?? 'unknown';
1215
+ const specStatus = recovery.sources?.spec?.status ?? 'unknown';
1216
+ const architectureStatus = recovery.sources?.architecture?.status ?? 'unknown';
1217
+ const graph = story.graph_context ?? recovery.graph_context ?? {};
1218
+ const fields = new Set((story.derived?.open_questions ?? []).map((item) => item.field));
1219
+ const refs = {
1220
+ story: recovery.sources?.story?.refs ?? [],
1221
+ spec: recovery.sources?.spec?.refs ?? [],
1222
+ architecture: recovery.sources?.architecture?.refs ?? []
1223
+ };
1224
+ const findings = [];
1225
+ const add = (type, severity, reason, potentialBug, recommendedReview, extra = {}) => {
1226
+ findings.push({
1227
+ id: `${story.story_id}-${type}`,
1228
+ story_id: story.story_id,
1229
+ title: story.title,
1230
+ type,
1231
+ severity,
1232
+ reason,
1233
+ potential_bug: potentialBug,
1234
+ recommended_review: recommendedReview,
1235
+ evidence: buildSourceAlignmentEvidence(story, graph, refs, extra.evidence),
1236
+ refs,
1237
+ graph: buildRecoveryGraphEvidence(graph),
1238
+ execution_policy: 'proposal_only',
1239
+ mutates_repository: false
1240
+ });
1241
+ };
1242
+
1243
+ if (fields.has('story_contract_source_role') || storyContractCheckStatus(storyContract, 'source_role_integrity') === 'needs_clarification') {
1244
+ add(
1245
+ 'story_contract_source_role_mismatch',
1246
+ 'high',
1247
+ 'Story Contractがsource roleの不一致を検出している。',
1248
+ '内部ツールや開発運用の文書を、ユーザー向けproduct storyとして誤読し、不要または誤った実装へ進む可能性がある。',
1249
+ '根拠文書が本当にproduct要求か、開発者向け内部仕様かを確認し、必要ならStory ID、category、preset、または根拠文書を修正する。',
1250
+ {
1251
+ evidence: {
1252
+ open_question: 'story_contract_source_role',
1253
+ story_contract: summarizeStoryContractForEvidence(storyContract)
1254
+ }
1255
+ }
1256
+ );
1257
+ }
1258
+
1259
+ if (specStatus === 'needs_recovery') {
1260
+ add(
1261
+ 'missing_spec_source',
1262
+ 'high',
1263
+ 'Spec正本が未復元のため、受け入れ基準がコード由来の仮説に留まっている。',
1264
+ '実装上の分岐や表示が、そのまま本来の要件だと誤認されている可能性がある。',
1265
+ 'Spec草案を作り、コード由来条件と人間が承認する受け入れ基準を分ける。'
1266
+ );
1267
+ }
1268
+
1269
+ if (architectureStatus === 'needs_decision') {
1270
+ add(
1271
+ 'missing_architecture_decision',
1272
+ 'high',
1273
+ 'Architecture/ADR判断が未確定のまま境界シグナルが検出されている。',
1274
+ 'API/Auth/Billing/Data/外部連携の責務境界が曖昧で、要件外の副作用を作る可能性がある。',
1275
+ 'ADRが必要か、Story内のADR不要理由で足りるかをGraphify影響範囲と照合して判断する。'
1276
+ );
1277
+ }
1278
+
1279
+ if ((sourceStatus === 'derived' || sourceStatus === 'implicit') && specStatus === 'present') {
1280
+ add(
1281
+ 'spec_from_unreviewed_story',
1282
+ sourceStatus === 'derived' ? 'medium' : 'low',
1283
+ `Spec正本はあるが、Story正本は${sourceStatus}のため意図が人間承認済みとは限らない。`,
1284
+ 'コードから逆算した画面/分岐を、ユーザー要求として誤って固定している可能性がある。',
1285
+ 'Specの受け入れ基準ごとに、Storyのwho/problem/outcomeと一致するか確認する。'
1286
+ );
1287
+ }
1288
+
1289
+ if ((sourceStatus === 'derived' || sourceStatus === 'implicit') && architectureStatus === 'present') {
1290
+ add(
1291
+ 'adr_from_unreviewed_story',
1292
+ sourceStatus === 'derived' ? 'medium' : 'low',
1293
+ `Architecture/ADRはあるが、Story正本は${sourceStatus}のため設計判断の前提が未承認の可能性がある。`,
1294
+ 'ADRの境界判断は正しくても、解こうとしている要件や運用制約が違う可能性がある。',
1295
+ 'ADRの前提、非目標、境界判断がStoryの成果状態と対応しているか確認する。'
1296
+ );
1297
+ }
1298
+
1299
+ if (graph.cross_community && architectureStatus !== 'present') {
1300
+ add(
1301
+ 'cross_community_without_architecture',
1302
+ 'high',
1303
+ 'Graphify上で複数communityに跨るが、Architecture/ADR正本が未確定。',
1304
+ '局所変更のつもりで責務境界を跨ぎ、別flowや共有serviceの挙動を壊す可能性がある。',
1305
+ 'hub/related fileを先に読み、ADRが必要な境界変更か、Storyをflow単位に分割すべきか判断する。'
1306
+ );
1307
+ }
1308
+
1309
+ if (graph.cross_community && architectureStatus === 'present') {
1310
+ add(
1311
+ 'cross_community_architecture_review',
1312
+ sourceStatus === 'present' ? 'medium' : 'high',
1313
+ 'Graphify上で複数communityに跨り、ADRは存在するため、ADRと実依存の対応確認が必要。',
1314
+ 'ADRで想定した境界より実コードの影響範囲が広く、要件上は無関係な画面/APIまで変える可能性がある。',
1315
+ 'ADRの対象境界とGraphifyのaffected_communities/hub_nodesが一致しているか確認する。'
1316
+ );
1317
+ }
1318
+
1319
+ if (isHighGraphImpact(graph) && sourceStatus !== 'present') {
1320
+ add(
1321
+ 'high_graph_impact_unreviewed_source',
1322
+ 'high',
1323
+ 'Graphifyの影響度が高いが、Story正本が人間レビュー済みではない。',
1324
+ '大きな影響範囲を持つ実装を、コード由来の仮説だけで正しい要件として扱う可能性がある。',
1325
+ '変更前にStory正本をレビューし、影響範囲に含まれる各flowが同じ受け入れ基準でよいか確認する。'
1326
+ );
1327
+ }
1328
+
1329
+ if (fields.has('business_context')) {
1330
+ add(
1331
+ 'business_context_gap',
1332
+ story.category === 'product' ? 'high' : 'medium',
1333
+ 'Storyの事業/利用文脈が未確定。',
1334
+ 'コードとしては正しいUIやAPIでも、ユーザーが本当に達成したい成果とずれている可能性がある。',
1335
+ '対象ユーザー、利用場面、成功状態をStory正本に明記してからSpecを承認する。',
1336
+ { evidence: { open_question: 'business_context' } }
1337
+ );
1338
+ }
1339
+
1340
+ if (fields.has('business_metric')) {
1341
+ add(
1342
+ 'business_metric_gap',
1343
+ 'medium',
1344
+ 'Storyの成功指標または観測観点が未確定。',
1345
+ '受け入れ基準を満たしても、プロダクト上の改善を検知できない可能性がある。',
1346
+ '少なくとも1つのKPI、ログ、運用確認観点をSpecまたはStoryへ紐づける。',
1347
+ { evidence: { open_question: 'business_metric' } }
1348
+ );
1349
+ }
1350
+
1351
+ if ((story.acceptance_focus ?? []).length === 0 && specStatus !== 'present') {
1352
+ add(
1353
+ 'acceptance_criteria_gap',
1354
+ 'medium',
1355
+ '受け入れ基準の焦点が薄く、Spec正本も未確定。',
1356
+ '実装完了の判定がコード差分や見た目確認に寄り、要件バグを見逃す可能性がある。',
1357
+ 'Spec草案に「何ができれば完了か」「何を壊してはいけないか」を追加する。'
1358
+ );
1359
+ }
1360
+
1361
+ return findings;
1362
+ }
1363
+
1364
+ function buildSourceAlignmentEvidence(story, graph, refs, extra = {}) {
1365
+ return {
1366
+ story_source_status: story.source_recovery?.sources?.story?.status ?? 'unknown',
1367
+ spec_status: story.source_recovery?.sources?.spec?.status ?? 'unknown',
1368
+ architecture_status: story.source_recovery?.sources?.architecture?.status ?? 'unknown',
1369
+ source_type: story.source_type ?? null,
1370
+ confidence: story.confidence ?? null,
1371
+ open_questions: (story.derived?.open_questions ?? []).map((item) => item.field).slice(0, 8),
1372
+ story_contract: summarizeStoryContractForEvidence(story.story_contract ?? story.derived?.story_contract ?? null),
1373
+ docs: [...refs.story, ...refs.spec, ...refs.architecture].slice(0, 12),
1374
+ files: [
1375
+ ...(graph?.matched_files ?? []),
1376
+ ...(graph?.related_files ?? []),
1377
+ ...(graph?.hub_nodes ?? []).map((node) => node.source_file).filter(Boolean)
1378
+ ].filter((file, index, files) => file && files.indexOf(file) === index).slice(0, 12),
1379
+ ...extra
1380
+ };
1381
+ }
1382
+
1383
+ function storyContractCheckStatus(storyContract, checkId) {
1384
+ return (storyContract?.checks ?? []).find((check) => check.id === checkId)?.status ?? null;
1385
+ }
1386
+
1387
+ function summarizeStoryContractForEvidence(storyContract) {
1388
+ if (!storyContract) return null;
1389
+ return {
1390
+ status: storyContract.status ?? null,
1391
+ story_type: storyContract.story_type ?? null,
1392
+ unresolved_checks: (storyContract.checks ?? [])
1393
+ .filter((check) => check.status === 'needs_clarification')
1394
+ .map((check) => check.id)
1395
+ };
1396
+ }
1397
+
1398
+ function isHighGraphImpact(graph = {}) {
1399
+ return (graph.impact_score ?? 0) >= 20
1400
+ || (graph.related_edge_count ?? 0) >= 20
1401
+ || (graph.matched_file_count ?? 0) >= 6
1402
+ || (graph.community_span ?? 0) >= 3;
1403
+ }
1404
+
1405
+ function sourceAlignmentSeverityRank(severity) {
1406
+ return { high: 0, medium: 1, low: 2 }[severity] ?? 3;
1407
+ }
1408
+
1409
+ function buildSourceAlignmentTaskCandidates(findings, stories) {
1410
+ const storyById = new Map(stories.map((story) => [story.story_id, story]));
1411
+ return [...groupSourceAlignmentFindingsByStory(findings).entries()]
1412
+ .filter(([, items]) => items.some((item) => item.severity === 'high' || item.severity === 'medium'))
1413
+ .slice(0, 10)
1414
+ .map(([storyId, items]) => {
1415
+ const story = storyById.get(storyId) ?? {};
1416
+ const topFinding = items[0];
1417
+ const files = topFinding.evidence?.files ?? [];
1418
+ return {
1419
+ id: `${storyId}-source-alignment-review`,
1420
+ story_id: storyId,
1421
+ title: 'Story/Spec/ADR不整合をレビューする',
1422
+ purpose: 'Story、Spec、Architecture/ADR、Graphify影響範囲を照合し、要件として間違っている可能性を潰す',
1423
+ acceptance: [
1424
+ '各潜在バグ候補について、Story/Spec/ADR/コードのどれを修正するか判断している',
1425
+ 'Graphifyのhub/communityを読んだ上で影響範囲を説明できる',
1426
+ '要件が正しい場合はレビュー済み理由を正本またはPR本文に残している'
1427
+ ],
1428
+ priority: topFinding.severity === 'high' ? 'high' : 'medium',
1429
+ source_type: 'source_alignment_finding',
1430
+ target_files: files.slice(0, 12),
1431
+ read_first_files: files.slice(0, 8).map((file) => ({
1432
+ file,
1433
+ reason: 'Source Alignment FindingのGraphify/コード証跡'
1434
+ })),
1435
+ graph_context: story.graph_context ?? topFinding.graph ?? null,
1436
+ source_alignment_findings: items,
1437
+ recommended_strategy: {
1438
+ id: 'source-alignment-review',
1439
+ reason: topFinding.potential_bug
1440
+ },
1441
+ implementation_steps: buildPlanCandidateSteps('source-alignment-review'),
1442
+ suggested_command: `vibepro story select . --id ${storyId}`,
1443
+ execution_policy: 'proposal_only',
1444
+ mutates_repository: false
1445
+ };
1446
+ });
1447
+ }
1448
+
1449
+ function selectExplicitStoryTasks(explicitTasks, stories) {
1450
+ const storyIds = new Set(stories.map((story) => story.story_id));
1451
+ return explicitTasks
1452
+ .filter((task) => storyIds.has(task.story_id))
1453
+ .sort((a, b) => a.story_id.localeCompare(b.story_id) || a.id.localeCompare(b.id));
1454
+ }
1455
+
1456
+ function groupSourceAlignmentFindingsByStory(findings) {
1457
+ const groups = new Map();
1458
+ for (const finding of findings) {
1459
+ const items = groups.get(finding.story_id) ?? [];
1460
+ items.push(finding);
1461
+ groups.set(finding.story_id, items);
1462
+ }
1463
+ return groups;
1464
+ }
1465
+
1466
+ function inferArchitectureBoundarySignals(story, codeFiles) {
1467
+ const text = [story.story_id, story.title, ...codeFiles].join(' ').toLowerCase();
1468
+ const signals = [];
1469
+ if (/api|route\.ts|webhook/.test(text)) signals.push('api_boundary');
1470
+ if (/auth|session|user|identity|middleware/.test(text)) signals.push('auth_boundary');
1471
+ if (/billing|stripe|subscription|payment|premium/.test(text)) signals.push('billing_boundary');
1472
+ if (/prisma|database|db|repository|model/.test(text)) signals.push('data_boundary');
1473
+ if (/webhook|stripe|resend|external|oauth/.test(text)) signals.push('external_integration_boundary');
1474
+ return [...new Set(signals)];
1475
+ }
1476
+
1477
+ function isStoryDocPath(filePath) {
1478
+ return /^docs\/management\/stories\//.test(filePath);
1479
+ }
1480
+
1481
+ function isSpecDocPath(filePath) {
1482
+ return /^docs\/(specs|requirements|features|user_stories)\//.test(filePath);
1483
+ }
1484
+
1485
+ function isArchitectureDocPath(filePath) {
1486
+ return /^docs\/(architecture|management\/architecture)\//.test(filePath);
1487
+ }
1488
+
1489
+ function slugifyStoryId(storyId) {
1490
+ return String(storyId ?? 'story')
1491
+ .replace(/^story-/, '')
1492
+ .replace(/[^A-Za-z0-9_-]+/g, '-')
1493
+ .replace(/^-+|-+$/g, '')
1494
+ .toLowerCase();
1495
+ }
1496
+
1497
+ function countBy(items, keyFn) {
1498
+ return items.reduce((acc, item) => {
1499
+ const key = keyFn(item);
1500
+ acc[key] = (acc[key] ?? 0) + 1;
1501
+ return acc;
1502
+ }, {});
1503
+ }
1504
+
1505
+ function resolveStoryPlanTargetFiles(story, graphIndex = null) {
1506
+ const meaning = story.derived?.meaning ?? {};
1507
+ const paths = [
1508
+ ...(meaning.code_scope?.evidence ?? []),
1509
+ ...(story.source?.paths ?? []).filter((item) => typeof item === 'string' && item.startsWith('src/'))
1510
+ ];
1511
+ const explicitFiles = [...new Set(paths)].slice(0, 12);
1512
+ if (explicitFiles.length > 0) return explicitFiles;
1513
+ return resolveStoryPlanGraphFallbackFiles(story, graphIndex);
1514
+ }
1515
+
1516
+ function resolveStoryPlanReadFirstFiles(story, graphContext = null) {
1517
+ const files = graphContext?.matched_files?.length > 0
1518
+ ? graphContext.matched_files
1519
+ : resolveStoryPlanTargetFiles(story);
1520
+ const items = files.slice(0, 6).map((file) => ({
1521
+ file,
1522
+ reason: `Story ${story.story_id} の根拠コード`
1523
+ }));
1524
+ for (const file of graphContext?.related_files ?? []) {
1525
+ addReadFirstFile(items, file, 'Graphifyで対象Storyの周辺依存として検出');
1526
+ }
1527
+ for (const hub of graphContext?.hub_nodes ?? []) {
1528
+ if (hub.source_file) addReadFirstFile(items, hub.source_file, `Graphify hub: ${hub.label ?? hub.id} / degree=${hub.degree ?? 0}`);
1529
+ }
1530
+ return items.slice(0, 10);
1531
+ }
1532
+
1533
+ function addReadFirstFile(items, file, reason) {
1534
+ if (!file || items.some((item) => item.file === file)) return;
1535
+ items.push({ file, reason });
1536
+ }
1537
+
1538
+ function resolveStoryPlanGraphFallbackFiles(story, graphIndex) {
1539
+ if (!graphIndex?.nodesBySourceFile) return [];
1540
+ const storyText = [story.story_id, story.title, story.category, story.view].filter(Boolean).join(' ').toLowerCase();
1541
+ const tokens = [...new Set(storyText.split(/[^a-z0-9]+/).filter((token) => token.length >= 4))];
1542
+ const boundaryMatchers = buildStoryBoundaryGraphMatchers(storyText);
1543
+ return [...graphIndex.nodesBySourceFile.keys()]
1544
+ .filter((file) => file.startsWith('src/'))
1545
+ .map((file) => ({
1546
+ file,
1547
+ score: scoreGraphFallbackFile(file, tokens, boundaryMatchers)
1548
+ }))
1549
+ .filter((item) => item.score > 0)
1550
+ .sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
1551
+ .slice(0, 12)
1552
+ .map((item) => item.file);
1553
+ }
1554
+
1555
+ function buildStoryBoundaryGraphMatchers(storyText) {
1556
+ const matchers = [];
1557
+ if (/auth|security|session|identity|user/.test(storyText)) matchers.push(/auth|session|identity|user|middleware/);
1558
+ if (/api|route|webhook|boundary/.test(storyText)) matchers.push(/\/api\/|route\.ts|webhook|middleware/);
1559
+ if (/billing|stripe|subscription|payment|premium/.test(storyText)) matchers.push(/billing|stripe|subscription|payment|premium/);
1560
+ if (/data|database|db|repository|model/.test(storyText)) matchers.push(/database|db|repository|model|prisma/);
1561
+ return matchers;
1562
+ }
1563
+
1564
+ function scoreGraphFallbackFile(file, tokens, boundaryMatchers) {
1565
+ const text = file.toLowerCase();
1566
+ let score = 0;
1567
+ for (const matcher of boundaryMatchers) {
1568
+ if (matcher.test(text)) score += 10;
1569
+ }
1570
+ for (const token of tokens) {
1571
+ if (text.includes(token)) score += 2;
1572
+ }
1573
+ if (/\/api\/|route\.ts|middleware/.test(text)) score += 1;
1574
+ return score;
1575
+ }
1576
+
1577
+ function workflowStageWeight(stage) {
1578
+ const weights = {
1579
+ risk_control: 16,
1580
+ decision: 12,
1581
+ activation: 10,
1582
+ monetization: 10,
1583
+ discovery: 8,
1584
+ entry: 7,
1585
+ acquisition: 6,
1586
+ personalization: 6,
1587
+ conversion_support: 6,
1588
+ retention: 4,
1589
+ operations: 4,
1590
+ architecture: 3,
1591
+ quality_gate: 3,
1592
+ knowledge_recovery: 2
1593
+ };
1594
+ return weights[stage] ?? 0;
1595
+ }
1596
+
1597
+ function buildPlanQuestions(catalog, priorityStories, sourceAlignmentFindings = null) {
1598
+ const priorityIds = new Set(priorityStories.map((story) => story.story_id));
1599
+ const rawQuestions = Array.isArray(catalog.open_questions) ? catalog.open_questions : [];
1600
+ const warningQuestions = (catalog.source?.warnings ?? []).map((warning) => ({
1601
+ story_id: 'story-docs-story-ssot-recovery',
1602
+ field: warning.code ?? 'warning',
1603
+ question: warning.message ?? `Story Map生成時の警告を確認する: ${warning.code ?? 'warning'}`,
1604
+ priority: 'high'
1605
+ }));
1606
+ const coverageQuestions = (catalog.coverage?.uncovered ?? []).slice(0, 10).map((item) => ({
1607
+ story_id: null,
1608
+ field: 'coverage',
1609
+ question: `${item.path} はGraph上で主要コードだがStory根拠に未接続。既存Storyへ吸収するか、新Storyにするか確認する。`,
1610
+ priority: 'high'
1611
+ }));
1612
+ const questions = rawQuestions.map((item) => ({
1613
+ story_id: item.story_id,
1614
+ field: item.field,
1615
+ question: item.question,
1616
+ priority: priorityIds.has(item.story_id) ? questionPriority(item.field) : 'medium'
1617
+ }));
1618
+ const sourceQuestions = priorityStories.flatMap((story) => buildSourceRecoveryQuestions(story));
1619
+ const alignmentQuestions = (sourceAlignmentFindings?.top ?? [])
1620
+ .filter((finding) => finding.severity === 'high')
1621
+ .slice(0, 5)
1622
+ .map((finding) => ({
1623
+ story_id: finding.story_id,
1624
+ field: 'source_alignment',
1625
+ question: `${finding.reason} 潜在バグ: ${finding.potential_bug}`,
1626
+ priority: 'high'
1627
+ }));
1628
+ return [...warningQuestions, ...coverageQuestions, ...alignmentQuestions, ...sourceQuestions, ...questions]
1629
+ .sort((a, b) => questionPriorityRank(a.priority) - questionPriorityRank(b.priority) || String(a.story_id ?? '').localeCompare(String(b.story_id ?? '')))
1630
+ .slice(0, 20);
1631
+ }
1632
+
1633
+ function buildSourceRecoveryQuestions(story) {
1634
+ const recovery = story.source_recovery;
1635
+ if (!recovery || recovery.status === 'aligned') return [];
1636
+ const questions = [];
1637
+ if (recovery.sources?.spec?.status === 'needs_recovery') {
1638
+ questions.push({
1639
+ story_id: story.story_id,
1640
+ field: 'source_spec_recovery',
1641
+ question: 'Spec正本が不足している。コードから逆算した受け入れ基準をSpecとして確定するか、既存Specへリンクする必要がある。',
1642
+ priority: 'high'
1643
+ });
1644
+ }
1645
+ if (recovery.sources?.architecture?.status === 'needs_decision') {
1646
+ questions.push({
1647
+ story_id: story.story_id,
1648
+ field: 'source_architecture_recovery',
1649
+ question: `Architecture/ADR判断が未確定。${recovery.sources.architecture.signals.join(', ')} の境界判断をStoryまたはADRに残す必要がある。`,
1650
+ priority: 'high'
1651
+ });
1652
+ }
1653
+ return questions;
1654
+ }
1655
+
1656
+ function questionPriority(field) {
1657
+ if (String(field ?? '').startsWith('story_contract_')) return field === 'story_contract_source_role' ? 'high' : 'medium';
1658
+ if (field === 'coverage' || field === 'missing_spec' || field === 'missing_evidence' || field === 'source_spec_recovery' || field === 'source_architecture_recovery' || field === 'source_alignment') return 'high';
1659
+ if (field === 'business_metric' || field === 'business_context') return 'medium';
1660
+ return 'low';
1661
+ }
1662
+
1663
+ function questionPriorityRank(priority) {
1664
+ return { high: 0, medium: 1, low: 2 }[priority] ?? 3;
1665
+ }
1666
+
1667
+ function buildTaskCandidatesForStory(story) {
1668
+ const fields = new Set((story.reasons ?? []).flatMap((reason) => reason.includes('仕様/Story') ? ['missing_spec'] : []));
1669
+ const candidates = [];
1670
+ const push = (suffix, title, purpose, acceptance, extra = {}) => {
1671
+ candidates.push({
1672
+ id: `${story.story_id}-${suffix}`,
1673
+ story_id: story.story_id,
1674
+ title,
1675
+ purpose,
1676
+ acceptance,
1677
+ priority: story.score >= 90 ? 'critical' : story.score >= 80 ? 'high' : story.score >= 60 ? 'medium' : 'low',
1678
+ source_type: 'story_plan_candidate',
1679
+ target_files: story.target_files ?? [],
1680
+ read_first_files: story.read_first_files ?? [],
1681
+ graph_context: story.graph_context ?? null,
1682
+ recommended_strategy: {
1683
+ id: suffix,
1684
+ reason: purpose
1685
+ },
1686
+ implementation_steps: buildPlanCandidateSteps(suffix),
1687
+ suggested_command: `vibepro story select . --id ${story.story_id}`,
1688
+ ...extra
1689
+ });
1690
+ };
1691
+
1692
+ if (fields.has('missing_spec') || story.source_type === 'code_surface' || story.source_recovery?.sources?.spec?.status === 'needs_recovery') {
1693
+ push('spec-recovery', 'Spec正本を復元する', 'コードから逆算したStoryの受け入れ基準をSpec正本として確定し、Storyとリンクする', [
1694
+ 'missing_spec が残る理由を確認済みにする',
1695
+ 'Storyのwho/problem/outcomeが人間レビュー済みになる',
1696
+ 'Spec草案の受け入れ基準がコード分岐と対応する',
1697
+ '必要なら仕様書またはNocoDB Storyを作る'
1698
+ ], {
1699
+ source_recovery: story.source_recovery,
1700
+ recovery_drafts: (story.source_recovery?.drafts ?? []).filter((draft) => draft.kind === 'spec')
1701
+ });
1702
+ }
1703
+ if (story.story_contract?.status === 'needs_clarification') {
1704
+ push('story-contract-recovery', 'Story Contractを確定する', 'ビジネス意図、開発境界、source role、受け入れ例、検証方針を実装前に確認する', [
1705
+ 'story_contract の未解決checkが説明できる',
1706
+ 'source roleがproduct要求、内部開発仕様、運用変更のどれかに分類されている',
1707
+ '開発境界と検証方針がStory/Spec/Architectureのいずれかに残る',
1708
+ '誤読リスクがある場合はStory ID、category、preset、または根拠文書を修正する'
1709
+ ], {
1710
+ story_contract: story.story_contract,
1711
+ source_recovery: story.source_recovery
1712
+ });
1713
+ }
1714
+ if (story.source_recovery?.sources?.architecture?.status === 'needs_decision') {
1715
+ push('architecture-recovery', 'Architecture/ADR正本を復元する', 'Graphと変更対象コードから境界判断を復元し、ADR要否と理由をStoryまたはArchitecture文書に残す', [
1716
+ 'Architecture/ADRが必要か、不要なら理由が明示されている',
1717
+ 'API/Auth/Billing/Data/外部連携の境界判断がGraph文脈と対応する',
1718
+ 'Requirement GateでArchitecture SourceまたはADR不要理由を追跡できる'
1719
+ ], {
1720
+ source_recovery: story.source_recovery,
1721
+ recovery_drafts: (story.source_recovery?.drafts ?? []).filter((draft) => draft.kind === 'architecture')
1722
+ });
1723
+ }
1724
+ if (story.category === 'security' || story.source_type === 'diagnosis') {
1725
+ push('risk-fix', '診断Findingを修正候補に落とす', 'security/diagnosis Storyの検出事項を修正可能なタスクに分解する', [
1726
+ '対象Findingと影響ファイルが特定される',
1727
+ '修正前ブリーフィングがある',
1728
+ 'テストまたは手動確認のGateが定義される'
1729
+ ]);
1730
+ }
1731
+ if (story.view === 'business') {
1732
+ push('kpi-period', 'KPIとPeriodを確定する', 'biz価値と実行期をNocoDB同期可能な形にする', [
1733
+ '主要KPIまたは効果測定観点が1つ以上ある',
1734
+ 'Periodを確定するか未定として扱う判断がある',
1735
+ '優先度の根拠が残る'
1736
+ ]);
1737
+ }
1738
+ if (candidates.length === 0) {
1739
+ push('review', 'Story仮説をレビューする', '意味づけ、根拠、反証を確認し次アクションを決める', [
1740
+ 'meaning confidenceを確認する',
1741
+ '次に診断するか、仕様を補うか、実装するか決める'
1742
+ ]);
1743
+ }
1744
+ return candidates;
1745
+ }
1746
+
1747
+ function buildWarningTaskCandidates(catalog) {
1748
+ return (catalog.source?.warnings ?? [])
1749
+ .filter((warning) => warning.code === 'missing_evidence')
1750
+ .map((warning) => ({
1751
+ id: 'story-docs-story-ssot-recovery-missing-evidence-cleanup',
1752
+ story_id: 'story-docs-story-ssot-recovery',
1753
+ title: '欠けた診断evidence参照を整理する',
1754
+ purpose: 'manifestが参照する診断evidenceの欠落を確認し、run成果物を復元するか不要なrun参照を整理する',
1755
+ acceptance: [
1756
+ '欠けているevidence参照のrun_idとpathが特定されている',
1757
+ 'run成果物を復元するか、不要なrun参照を整理する判断が残っている',
1758
+ 'story deriveを再実行してmissing_evidence警告が消えるか、残す理由が説明されている'
1759
+ ],
1760
+ priority: 'high',
1761
+ source_type: 'story_plan_warning',
1762
+ warning,
1763
+ target_files: ['.vibepro/vibepro-manifest.json'],
1764
+ read_first_files: [{
1765
+ file: '.vibepro/vibepro-manifest.json',
1766
+ reason: `欠けた診断evidence参照を確認する: ${warning.path ?? '-'}`
1767
+ }],
1768
+ recommended_strategy: {
1769
+ id: 'missing-evidence-cleanup',
1770
+ reason: warning.message ?? 'manifestが参照する診断evidenceが見つからないため'
1771
+ },
1772
+ implementation_steps: buildPlanCandidateSteps('missing-evidence-cleanup'),
1773
+ suggested_command: 'vibepro story plan .'
1774
+ }));
1775
+ }
1776
+
1777
+ function buildPlanCandidateSteps(suffix) {
1778
+ const common = {
1779
+ 'spec-recovery': [
1780
+ { id: 'review-meaning', title: 'Story意味づけを確認する', detail: 'derived.meaning の価値仮説、根拠、反証、不足情報を確認する' },
1781
+ { id: 'recover-spec', title: 'Spec草案を復元する', detail: 'source_recovery.drafts のspec草案を読み、コード由来条件と人間が確定すべき条件を分ける' },
1782
+ { id: 'link-source', title: 'StoryとSpecをリンクする', detail: 'Story frontmatterまたは本文にSpec参照を残し、Requirement Gateが正本として読めるようにする' },
1783
+ { id: 'rerun-gate', title: 'Requirement Gateを再実行する', detail: 'vibepro pr prepare または diagnose でSpec Sourceが拾われるか確認する' }
1784
+ ],
1785
+ 'architecture-recovery': [
1786
+ { id: 'read-graph', title: 'Graph文脈を読む', detail: '対象ファイルのhub/community/依存方向を確認し、境界変更か局所変更かを判定する' },
1787
+ { id: 'decide-adr', title: 'ADR要否を決める', detail: 'API/Auth/Billing/Data/外部連携の境界判断が必要ならArchitecture/ADR草案に落とす' },
1788
+ { id: 'record-decision', title: '判断を正本へ記録する', detail: 'ADR文書を作るか、ADR不要理由をStory frontmatterへ明示する' },
1789
+ { id: 'rerun-gate', title: 'Requirement Gateを再実行する', detail: 'Architecture SourceまたはADR不要理由がPR本文とGate DAGに出るか確認する' }
1790
+ ],
1791
+ 'risk-fix': [
1792
+ { id: 'read-finding', title: '診断Findingを読む', detail: '対象Finding、影響範囲、既存保護境界を確認する' },
1793
+ { id: 'define-fix', title: '修正方針を決める', detail: 'route単位、middleware、環境変数、署名検証のどれで直すか決める' },
1794
+ { id: 'define-gate', title: '検証Gateを決める', detail: '再診断、unit/API/E2Eのどれで完了確認するか決める' }
1795
+ ],
1796
+ 'kpi-period': [
1797
+ { id: 'define-kpi', title: 'KPIを決める', detail: 'Storyの成果を測る指標または観測観点を1つ以上決める' },
1798
+ { id: 'define-period', title: 'Periodを決める', detail: 'NocoDB同期可能な実行期を確定するか、未定として扱う判断を残す' }
1799
+ ],
1800
+ 'source-alignment-review': [
1801
+ { id: 'read-sources', title: '正本を読む', detail: 'Story、Spec、Architecture/ADRの参照を読み、要件・受け入れ基準・境界判断を並べる' },
1802
+ { id: 'read-graph', title: 'Graph影響範囲を読む', detail: 'Graphifyのmatched/related/hub/communityを読み、実際の影響範囲を確認する' },
1803
+ { id: 'classify-mismatch', title: '不整合を分類する', detail: 'Storyが誤り、Specが誤り、ADRが不足、コードが意図外のどれかを判定する' },
1804
+ { id: 'record-outcome', title: '判断を記録する', detail: '修正する場合はタスク化し、正しい場合はレビュー済み理由を正本またはPR本文へ残す' }
1805
+ ],
1806
+ review: [
1807
+ { id: 'review-story', title: 'Story仮説をレビューする', detail: 'meaning confidenceとcounter_evidenceを確認して次アクションを決める' }
1808
+ ],
1809
+ 'missing-evidence-cleanup': [
1810
+ { id: 'inspect-manifest', title: 'manifest参照を確認する', detail: 'missing_evidence warningのrun_idとpathを .vibepro/vibepro-manifest.json で確認する' },
1811
+ { id: 'choose-policy', title: '復元か整理かを決める', detail: '診断runを残す必要があればevidenceを復元し、不要ならmanifestのrun参照を整理する' },
1812
+ { id: 'rerun-derive', title: 'Story Mapを再生成する', detail: 'vibepro story derive を再実行し、warningが消えたか、残す理由が説明できるか確認する' }
1813
+ ]
1814
+ };
1815
+ return common[suffix] ?? common.review;
1816
+ }
1817
+
1818
+ function buildStoryPlanNextCommands(priorityStories) {
1819
+ const firstStory = priorityStories[0];
1820
+ return [
1821
+ 'vibepro story map .',
1822
+ 'vibepro story plan .',
1823
+ firstStory ? `vibepro story select . --id ${firstStory.story_id}` : null,
1824
+ firstStory ? `vibepro story diagnose . --id ${firstStory.story_id} --run-graphify` : null
1825
+ ].filter(Boolean);
1826
+ }
1827
+
1828
+ export function renderStoryPlan(plan) {
1829
+ const questions = plan.questions.length === 0
1830
+ ? '- なし'
1831
+ : plan.questions.map((item) => `- [${item.priority}] ${item.story_id ? `\`${item.story_id}\`: ` : ''}${item.question}`).join('\n');
1832
+ const stories = plan.priority_stories.length === 0
1833
+ ? '- なし'
1834
+ : plan.priority_stories.map((story, index) => `### ${index + 1}. ${story.title}
1835
+
1836
+ - Story ID: \`${story.story_id}\`
1837
+ - Score: ${story.score}
1838
+ - Stage: ${story.workflow_stage}
1839
+ - Confidence: ${story.confidence}
1840
+ - Source: ${story.source_type}
1841
+ - Source Consistency: ${story.source_recovery?.status ?? '-'} (story=${story.source_recovery?.sources?.story?.status ?? '-'}, spec=${story.source_recovery?.sources?.spec?.status ?? '-'}, architecture=${story.source_recovery?.sources?.architecture?.status ?? '-'})
1842
+ - 理由:
1843
+ ${story.reasons.map((reason) => ` - ${reason}`).join('\n') || ' - -'}
1844
+ - 次コマンド: \`${story.next_command}\``).join('\n\n');
1845
+ const tasks = plan.task_candidates.length === 0
1846
+ ? '| Story | Task | Purpose |\n|-------|------|---------|\n| - | - | - |'
1847
+ : `| Story | Task | Purpose |
1848
+ |-------|------|---------|
1849
+ ${plan.task_candidates.map((task) => `| ${task.story_id} | ${task.title} | ${task.purpose} |`).join('\n')}`;
1850
+ const sourceRecoveryMap = renderSourceRecoveryMap(plan.source_recovery_map);
1851
+ return `# Story実行計画
1852
+
1853
+ ## サマリー
1854
+
1855
+ | 項目 | 内容 |
1856
+ |------|------|
1857
+ | 生成日時 | ${plan.generated_at} |
1858
+ | Story数 | ${plan.summary.story_count} |
1859
+ | Coverage | ${plan.summary.coverage_status} (${formatPercent(plan.summary.coverage_ratio)}) |
1860
+ | 未カバー | ${plan.summary.uncovered_files} |
1861
+ | 警告 | ${plan.summary.warning_count ?? 0} |
1862
+ | 未決事項 | ${plan.summary.open_question_count} |
1863
+ | Source Consistency | ${plan.summary.source_consistency_status ?? '-'} |
1864
+ | 正本復元対象Story | ${plan.summary.source_recovery_story_count ?? 0} |
1865
+ | Spec欠落 | ${plan.summary.source_missing_spec_count ?? 0} |
1866
+ | Architecture/ADR判断欠落 | ${plan.summary.source_missing_architecture_count ?? 0} |
1867
+ | 潜在バグ候補 | ${plan.summary.source_alignment_finding_count ?? 0} |
1868
+ | 高リスク潜在バグ候補 | ${plan.summary.source_alignment_high_count ?? 0} |
1869
+
1870
+ ## まず確認する質問
1871
+
1872
+ ${questions}
1873
+
1874
+ ## 優先Story
1875
+
1876
+ ${stories}
1877
+
1878
+ ## 正本欠落マップ
1879
+
1880
+ ${sourceRecoveryMap}
1881
+
1882
+ ## 潜在バグ候補
1883
+
1884
+ ${renderSourceAlignmentFindings(plan.source_alignment_findings)}
1885
+
1886
+ ## タスク候補
1887
+
1888
+ ${tasks}
1889
+
1890
+ ## 次コマンド
1891
+
1892
+ ${plan.next_commands.map((command) => `- \`${command}\``).join('\n') || '-'}
1893
+ `;
1894
+ }
1895
+
1896
+ function renderSourceAlignmentFindings(findings) {
1897
+ const rows = findings?.top ?? [];
1898
+ if (rows.length === 0) return '- なし';
1899
+ return `| Severity | Story | Type | Potential Bug | Review |
1900
+ |----------|-------|------|---------------|--------|
1901
+ ${rows.map((row) => `| ${escapeTableCell(row.severity)} | ${escapeTableCell(row.story_id)} | ${escapeTableCell(row.type)} | ${escapeTableCell(row.potential_bug)} | ${escapeTableCell(row.recommended_review)} |`).join('\n')}`;
1902
+ }
1903
+
1904
+ function renderSourceRecoveryMap(map) {
1905
+ const rows = map?.missing ?? [];
1906
+ if (rows.length === 0) return '- なし';
1907
+ return `| Story | Spec | Spec復元先 | Architecture | ADR復元先 | Graph | Task |
1908
+ |-------|------|------------|--------------|-----------|-------|------|
1909
+ ${rows.map((row) => `| ${escapeTableCell(row.story_id)} | ${escapeTableCell(row.spec.status)} | ${escapeTableCell(row.spec.suggested_path ?? '-')} | ${escapeTableCell(row.architecture.status)} | ${escapeTableCell(row.architecture.suggested_path ?? '-')} | ${escapeTableCell(formatSourceRecoveryMapGraph(row.graph))} | ${escapeTableCell(formatSourceRecoveryMapTasks(row))} |`).join('\n')}`;
1910
+ }
1911
+
1912
+ function formatSourceRecoveryMapGraph(graph) {
1913
+ return `${graph?.matched_file_count ?? 0} files / ${graph?.related_edge_count ?? 0} edges${graph?.cross_community ? ' / cross-community' : ''}`;
1914
+ }
1915
+
1916
+ function formatSourceRecoveryMapTasks(row) {
1917
+ return [
1918
+ row.spec.task_candidate_id ?? row.spec.suggested_task_id,
1919
+ row.architecture.task_candidate_id ?? row.architecture.suggested_task_id
1920
+ ].filter(Boolean).join(', ') || '-';
1921
+ }
1922
+
1923
+ function escapeTableCell(value) {
1924
+ return String(value ?? '-').replace(/\|/g, '\\|').replace(/\n/g, '<br>');
1925
+ }
1926
+
1927
+ function formatPercent(value) {
1928
+ if (typeof value !== 'number') return '-';
1929
+ return `${Math.round(value * 1000) / 10}%`;
1930
+ }
1931
+
1932
+ function renderStoryArchitectureViews(views) {
1933
+ return `| View | 判定 |
1934
+ |------|------|
1935
+ | Structure | ${[
1936
+ ...(views.structure?.containers ?? []),
1937
+ ...(views.structure?.components ?? []),
1938
+ ...(views.structure?.frameworks ?? [])
1939
+ ].join(', ') || '-'} |
1940
+ | Runtime | ${[
1941
+ `${views.runtime?.entrypoints?.length ?? 0} entrypoints`,
1942
+ ...(views.runtime?.server_boundaries ?? [])
1943
+ ].join(', ')} |
1944
+ | Data | ${[
1945
+ ...(views.data?.stores ?? []),
1946
+ ...(views.data?.access_patterns ?? [])
1947
+ ].join(', ') || '-'} |
1948
+ | Security | ${[
1949
+ `${views.security?.auth_boundaries?.length ?? 0} auth boundaries`,
1950
+ `${views.security?.secret_files?.length ?? 0} secret files`
1951
+ ].join(', ')} |
1952
+ | Deployment | ${(views.deployment?.targets ?? []).join(', ') || '-'} |
1953
+ | Quality | ${[
1954
+ ...(views.quality?.test_tools ?? []),
1955
+ ...(views.quality?.ci ?? [])
1956
+ ].join(', ') || '-'} |`;
1957
+ }
1958
+
1959
+ export function resolveStoryContext(config) {
1960
+ const stories = normalizeActiveStories(config.brainbase?.stories);
1961
+ const currentStoryId = config.brainbase?.current_story_id ?? null;
1962
+ const currentStory = stories.find((story) => story.story_id === currentStoryId) ?? stories[0];
1963
+ return { stories, currentStory };
1964
+ }
1965
+
1966
+ function resolveStory(config, storyId = null) {
1967
+ const stories = normalizeActiveStories(config.brainbase?.stories);
1968
+ const targetStoryId = storyId ?? config.brainbase?.current_story_id ?? null;
1969
+ const story = targetStoryId
1970
+ ? stories.find((item) => item.story_id === targetStoryId)
1971
+ : stories[0];
1972
+ if (!story) throw new Error(`Story not found: ${targetStoryId}`);
1973
+ return story;
1974
+ }
1975
+
1976
+ function getRunsForStory(manifest, storyId) {
1977
+ const runs = Array.isArray(manifest.runs) ? manifest.runs : [];
1978
+ return runs.filter((run) => run.story_id === storyId);
1979
+ }
1980
+
1981
+ function findLatestStoryRun(manifest, storyId, runs) {
1982
+ const latestRunId = manifest.latest_run_by_story?.[storyId] ?? null;
1983
+ return runs.find((run) => run.run_id === latestRunId) ?? runs[0] ?? null;
1984
+ }
1985
+
1986
+ async function readRunEvidence(repoRoot, run) {
1987
+ const evidencePath = run.artifacts?.evidence;
1988
+ if (!evidencePath) return null;
1989
+ return JSON.parse(await readFile(path.resolve(repoRoot, evidencePath), 'utf8'));
1990
+ }
1991
+
1992
+ export function normalizeActiveStories(stories) {
1993
+ const sourceStories = Array.isArray(stories) && stories.length > 0 ? stories : DEFAULT_BRAINBASE_STORIES;
1994
+ const activeStories = sourceStories.filter((story) => !isArchived(story));
1995
+ if (activeStories.length === 0) {
1996
+ throw new Error('At least one active story is required');
1997
+ }
1998
+ return activeStories.map((story) => ({
1999
+ story_id: story.story_id,
2000
+ title: story.title,
2001
+ ssot: story.ssot ?? 'NocoDB',
2002
+ status: story.status ?? 'active',
2003
+ horizon: story.horizon ?? null,
2004
+ view: typeof story.view === 'string' ? story.view : null,
2005
+ period: typeof story.period === 'string' ? story.period : null,
2006
+ started_at: story.started_at ?? null,
2007
+ due_at: story.due_at ?? null,
2008
+ category: story.category ?? null
2009
+ }));
2010
+ }
2011
+
2012
+ async function readConfig(repoRoot) {
2013
+ await initWorkspace(repoRoot);
2014
+ return JSON.parse(await readFile(getConfigPath(repoRoot), 'utf8'));
2015
+ }
2016
+
2017
+ async function writeConfig(repoRoot, config) {
2018
+ await writeFile(getConfigPath(repoRoot), `${JSON.stringify(config, null, 2)}\n`);
2019
+ }
2020
+
2021
+ function getConfigPath(repoRoot) {
2022
+ return path.join(getWorkspaceDir(repoRoot), 'config.json');
2023
+ }
2024
+
2025
+ function getStories(config) {
2026
+ return Array.isArray(config.brainbase?.stories) ? config.brainbase.stories : [];
2027
+ }
2028
+
2029
+ async function readExistingStoryCatalog(repoRoot) {
2030
+ const catalogPath = path.join(getWorkspaceDir(repoRoot), 'stories', 'story-catalog.json');
2031
+ try {
2032
+ return JSON.parse(await readFile(catalogPath, 'utf8'));
2033
+ } catch (error) {
2034
+ if (error.code === 'ENOENT') return null;
2035
+ throw error;
2036
+ }
2037
+ }
2038
+
2039
+ function mergeDerivedStories(config, derivedStories, previousCatalog = null) {
2040
+ const stories = getStories(config);
2041
+ const existingIds = new Set(stories.map((story) => story.story_id));
2042
+ const derivedById = new Map(derivedStories.map((story) => [story.story_id, story]));
2043
+ const currentDerivedIds = new Set(derivedStories.map((story) => story.story_id));
2044
+ const previousDerivedIds = new Set((previousCatalog?.stories ?? []).map((story) => story.story_id));
2045
+ let archivedCount = 0;
2046
+ let updatedCount = 0;
2047
+ for (const story of stories) {
2048
+ const derivedStory = derivedById.get(story.story_id);
2049
+ if (derivedStory && shouldUpdateDerivedStory(story, previousDerivedIds)) {
2050
+ Object.assign(story, toConfigStory(derivedStory));
2051
+ updatedCount += 1;
2052
+ continue;
2053
+ }
2054
+ if (!shouldArchiveStaleDerivedStory(story, currentDerivedIds, previousDerivedIds)) continue;
2055
+ story.status = 'archived';
2056
+ archivedCount += 1;
2057
+ }
2058
+ const additions = derivedStories
2059
+ .filter((story) => !existingIds.has(story.story_id))
2060
+ .map(toConfigStory);
2061
+ config.brainbase = {
2062
+ ...(config.brainbase ?? {}),
2063
+ stories: [...stories, ...additions]
2064
+ };
2065
+ return {
2066
+ added_count: additions.length,
2067
+ archived_count: archivedCount,
2068
+ updated_count: updatedCount,
2069
+ skipped_count: derivedStories.length - additions.length - updatedCount
2070
+ };
2071
+ }
2072
+
2073
+ function toConfigStory(story) {
2074
+ return {
2075
+ story_id: story.story_id,
2076
+ title: story.title,
2077
+ ssot: story.ssot ?? 'local',
2078
+ status: story.status ?? 'active',
2079
+ horizon: story.horizon ?? null,
2080
+ view: story.view ?? null,
2081
+ period: story.period ?? null,
2082
+ started_at: story.started_at ?? null,
2083
+ due_at: story.due_at ?? null,
2084
+ category: story.category ?? null,
2085
+ derived_by: 'vibepro-story-derive'
2086
+ };
2087
+ }
2088
+
2089
+ function shouldArchiveStaleDerivedStory(story, currentDerivedIds, previousDerivedIds) {
2090
+ if (story.ssot !== 'local') return false;
2091
+ if (currentDerivedIds.has(story.story_id)) return false;
2092
+ if (story.derived_by === 'vibepro-story-derive') return true;
2093
+ if (previousDerivedIds.has(story.story_id)) return true;
2094
+ return isLikelyObsoleteDocumentStory(story);
2095
+ }
2096
+
2097
+ function shouldUpdateDerivedStory(story, previousDerivedIds) {
2098
+ if (story.ssot !== 'local') return false;
2099
+ if (story.derived_by === 'vibepro-story-derive') return true;
2100
+ if (previousDerivedIds.has(story.story_id)) return true;
2101
+ return false;
2102
+ }
2103
+
2104
+ function isLikelyObsoleteDocumentStory(story) {
2105
+ if (!/^story-(product|architecture)-/.test(story.story_id)) return false;
2106
+ return /(仕様|要件|REQ-\d+|US-\d+|アーキテクチャ|設計|ガイド|ロードマップ|システムドキュメント|現在の実装|セットアップチェックリスト|インターフェース|テクノロジースタック|シーケンス図|sequence diagram|関係図|バージョン情報|フロー|構造)/i.test(story.title ?? '');
2107
+ }
2108
+
2109
+ function renderStoryMapCatalog(catalog) {
2110
+ return renderStoryCatalogMap(catalog);
2111
+ }
2112
+
2113
+ function toWorkspaceRelativeFromAny(filePath) {
2114
+ const marker = `${path.sep}.vibepro${path.sep}`;
2115
+ const index = filePath.indexOf(marker);
2116
+ if (index === -1) return filePath;
2117
+ return `.vibepro/${filePath.slice(index + marker.length).split(path.sep).join('/')}`;
2118
+ }
2119
+
2120
+ function buildStory(options) {
2121
+ if (!options.story_id) throw new Error('--id is required');
2122
+ if (!options.title) throw new Error('--title is required');
2123
+ return {
2124
+ story_id: options.story_id,
2125
+ title: options.title,
2126
+ ssot: 'local',
2127
+ status: 'active',
2128
+ horizon: options.horizon ?? null,
2129
+ view: options.view ?? null,
2130
+ period: options.period ?? null,
2131
+ started_at: options.started_at ?? null,
2132
+ due_at: options.due_at ?? null
2133
+ };
2134
+ }
2135
+
2136
+ function getOption(args, name) {
2137
+ const index = args.indexOf(name);
2138
+ if (index === -1) return null;
2139
+ return args[index + 1] ?? null;
2140
+ }
2141
+
2142
+ function isArchived(story) {
2143
+ return story.status === 'archived' || story.status === 'アーカイブ';
2144
+ }