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,1066 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export const MAX_SCAN_FILES = 80;
5
+ export const CODE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
6
+ export const DEFAULT_STORY_DIRS = [
7
+ path.join('docs', 'user_stories', 'active'),
8
+ path.join('docs', 'user_stories'),
9
+ path.join('docs', 'management', 'stories', 'active'),
10
+ path.join('docs', 'management', 'stories'),
11
+ path.join('docs', 'stories')
12
+ ];
13
+ export const STORY_DIR_PREFIXES = DEFAULT_STORY_DIRS.map((dir) => dir.split(path.sep).join('/'));
14
+ const REQUIREMENT_SOURCE_DIRS = [
15
+ { kind: 'spec', dir: path.join('docs', 'specs') },
16
+ { kind: 'spec', dir: path.join('docs', 'features', 'specifications') },
17
+ { kind: 'architecture', dir: path.join('docs', 'architecture') },
18
+ { kind: 'architecture', dir: path.join('docs', 'management', 'architecture') },
19
+ { kind: 'policy', dir: path.join('docs', 'management', 'policies') },
20
+ { kind: 'policy', dir: path.join('docs', 'frames') },
21
+ { kind: 'policy', dir: path.join('docs', '00-glossary') }
22
+ ];
23
+ export const INVARIANT_PATTERNS = [
24
+ /\bmust\b/i,
25
+ /\bshall\b/i,
26
+ /\bnever\b/i,
27
+ /\bkeep\b/i,
28
+ /\buntil\b/i,
29
+ /必ず/,
30
+ /維持/,
31
+ /保持/,
32
+ /禁止/,
33
+ /してはいけない/,
34
+ /変えない/,
35
+ /一致/,
36
+ /同じ/,
37
+ /期間終了/,
38
+ /認可/,
39
+ /署名/,
40
+ /重複/,
41
+ /正規化/,
42
+ /1件/,
43
+ /一意/,
44
+ /分離/,
45
+ /境界/,
46
+ /責務/,
47
+ /扱う/,
48
+ /premium|プレミアム/i,
49
+ /subscription|サブスクリプション/i
50
+ ];
51
+
52
+ export const DOMAIN_KEYWORDS = [
53
+ 'auth',
54
+ '認証',
55
+ '認可',
56
+ 'session',
57
+ 'user',
58
+ 'identity',
59
+ 'billing',
60
+ 'stripe',
61
+ 'subscription',
62
+ 'premium',
63
+ 'webhook',
64
+ '署名',
65
+ 'onboarding',
66
+ 'profile'
67
+ ];
68
+
69
+ const GENERIC_CONDITION_TOKENS = new Set([
70
+ 'true',
71
+ 'false',
72
+ 'null',
73
+ 'undefined',
74
+ 'return',
75
+ 'status',
76
+ 'state',
77
+ 'error',
78
+ 'result',
79
+ 'value',
80
+ 'data',
81
+ 'body',
82
+ 'function'
83
+ ]);
84
+
85
+ const INHERITED_BEHAVIOR_PATTERNS = [
86
+ /\binherited\b/i,
87
+ /\bexisting\b/i,
88
+ /\bunchanged\b/i,
89
+ /\bremain(?:s|ed)?\b/i,
90
+ /\bcontinue(?:s|d)?\b/i,
91
+ /\bas before\b/i,
92
+ /\bdo not change\b/i,
93
+ /\bnot changed\b/i,
94
+ /既存/,
95
+ /維持/,
96
+ /変更しない/,
97
+ /従来/,
98
+ /そのまま/
99
+ ];
100
+
101
+ export async function buildRequirementConsistency(repoRoot, options = {}) {
102
+ const root = path.resolve(repoRoot);
103
+ const codeFiles = await resolveCodeFiles(root, options);
104
+ const storySource = await resolveStorySource(root, options);
105
+ const requirementSources = await collectRequirementSources(root, {
106
+ story: options.story,
107
+ storySource,
108
+ codeFiles
109
+ });
110
+ const policyRefs = requirementSources.filter((source) => source.kind === 'policy');
111
+ const invariants = options.inferredSpec
112
+ ? extractInvariantsFromInferredSpec(options.inferredSpec, storySource)
113
+ : extractInvariants(storySource, requirementSources);
114
+ const codeScenarios = await collectCodeScenarios(root, codeFiles);
115
+ const scenarioGaps = buildScenarioGaps({
116
+ invariants,
117
+ codeScenarios,
118
+ storySource,
119
+ requirementSources,
120
+ inferredSpec: options.inferredSpec
121
+ });
122
+ const contradictions = buildContradictions({ invariants, codeScenarios, storySource });
123
+ const status = contradictions.length > 0
124
+ ? 'contradicted'
125
+ : scenarioGaps.length > 0
126
+ ? 'needs_review'
127
+ : invariants.length > 0
128
+ ? 'pass'
129
+ : 'not_applicable';
130
+
131
+ return {
132
+ schema_version: '0.1.0',
133
+ status,
134
+ story_source: {
135
+ path: storySource?.path ?? null,
136
+ title: storySource?.title ?? null
137
+ },
138
+ summary: {
139
+ invariant_count: invariants.length,
140
+ scenario_gap_count: scenarioGaps.length,
141
+ contradiction_count: contradictions.length,
142
+ scanned_code_files: codeScenarios.length,
143
+ requirement_source_count: requirementSources.length,
144
+ spec_ref_count: requirementSources.filter((source) => source.kind === 'spec').length,
145
+ architecture_ref_count: requirementSources.filter((source) => source.kind === 'architecture').length,
146
+ policy_ref_count: policyRefs.length
147
+ },
148
+ invariants,
149
+ scenario_gaps: scenarioGaps,
150
+ contradictions,
151
+ requirement_sources: requirementSources.map(toRequirementSourceRef),
152
+ policy_refs: policyRefs.map(toRequirementSourceRef),
153
+ code_scenarios: codeScenarios
154
+ };
155
+ }
156
+
157
+ export function renderRequirementConsistencyReport(requirement) {
158
+ if (!requirement) return '# Requirement Consistency\n\n- 未生成\n';
159
+ return `# Requirement Consistency
160
+
161
+ | 項目 | 内容 |
162
+ |------|------|
163
+ | Status | ${requirement.status} |
164
+ | Invariants | ${requirement.summary?.invariant_count ?? 0} |
165
+ | Scenario Gaps | ${requirement.summary?.scenario_gap_count ?? 0} |
166
+ | Contradictions | ${requirement.summary?.contradiction_count ?? 0} |
167
+ | Scanned Code Files | ${requirement.summary?.scanned_code_files ?? 0} |
168
+ | Requirement Sources | ${requirement.summary?.requirement_source_count ?? 0} |
169
+ | Spec Refs | ${requirement.summary?.spec_ref_count ?? 0} |
170
+ | Architecture Refs | ${requirement.summary?.architecture_ref_count ?? 0} |
171
+ | Policy Refs | ${requirement.summary?.policy_ref_count ?? 0} |
172
+
173
+ ## Invariants
174
+
175
+ ${formatItems(requirement.invariants, (item) => `- ${item.id}: ${item.text} (${formatSourceRef(item.source)})`)}
176
+
177
+ ## Scenario Gaps
178
+
179
+ ${formatItems(requirement.scenario_gaps, (item) => `- ${item.id}: ${item.title} - ${item.detail}`)}
180
+
181
+ ## Potential Contradictions
182
+
183
+ ${formatItems(requirement.contradictions, (item) => `- ${item.id}: ${item.title} - ${item.detail}`)}
184
+
185
+ ## Requirement Sources
186
+
187
+ ${formatItems(requirement.requirement_sources, (item) => `- ${item.kind}: ${item.path}: ${item.title ?? '-'}`)}
188
+ `;
189
+ }
190
+
191
+ export function renderRequirementGateSummary(requirement) {
192
+ if (!requirement) return '- Requirement Gate: not_generated';
193
+ const detail = [
194
+ `${requirement.summary?.invariant_count ?? 0} invariants`,
195
+ `${requirement.summary?.scenario_gap_count ?? 0} scenario gaps`,
196
+ `${requirement.summary?.contradiction_count ?? 0} contradictions`
197
+ ].join(', ');
198
+ return `- Requirement Gate: ${requirement.status} - ${detail}`;
199
+ }
200
+
201
+ export async function resolveStoryDirs(repoRoot) {
202
+ try {
203
+ const { getWorkspaceDir } = await import('./workspace.js');
204
+ const configPath = path.join(getWorkspaceDir(repoRoot), 'config.json');
205
+ const raw = await readFile(configPath, 'utf8');
206
+ const config = JSON.parse(raw);
207
+ const override = config?.doc_paths?.stories;
208
+ if (Array.isArray(override) && override.length > 0) {
209
+ return override.map((entry) => String(entry));
210
+ }
211
+ } catch (error) {
212
+ if (error.code !== 'ENOENT') {
213
+ // fall through to defaults
214
+ }
215
+ }
216
+ return [...DEFAULT_STORY_DIRS];
217
+ }
218
+
219
+ export async function findStorySource(repoRoot, story) {
220
+ const storyId = story?.story_id ?? null;
221
+ const storyDirs = await resolveStoryDirs(repoRoot);
222
+ const candidates = [];
223
+ for (const dir of storyDirs) {
224
+ const files = await listFiles(path.join(repoRoot, dir));
225
+ candidates.push(...files.filter((file) => /\.(md|mdx)$/i.test(file)));
226
+ }
227
+ if (candidates.length === 0) {
228
+ return {
229
+ path: null,
230
+ title: story?.title ?? null,
231
+ content: '',
232
+ acceptance_criteria: [],
233
+ background: null,
234
+ policy: null
235
+ };
236
+ }
237
+ if (storyId) {
238
+ const byFrontmatter = await findCandidateByFrontmatter(repoRoot, candidates, storyId);
239
+ if (byFrontmatter) return parseStoryLikeDocument(repoRoot, byFrontmatter, 'story');
240
+ const bySubstring = candidates.find((file) => normalizePath(file).includes(storyId));
241
+ if (bySubstring) return parseStoryLikeDocument(repoRoot, bySubstring, 'story');
242
+ return {
243
+ path: null,
244
+ title: story?.title ?? null,
245
+ content: '',
246
+ acceptance_criteria: [],
247
+ background: null,
248
+ policy: null
249
+ };
250
+ }
251
+ return parseStoryLikeDocument(repoRoot, candidates[0], 'story');
252
+ }
253
+
254
+ async function findCandidateByFrontmatter(repoRoot, candidates, storyId) {
255
+ for (const file of candidates) {
256
+ let content;
257
+ try {
258
+ content = await readFile(file, 'utf8');
259
+ } catch {
260
+ continue;
261
+ }
262
+ const frontmatter = parseFrontmatter(content);
263
+ const candidateIds = [
264
+ frontmatter.story_id,
265
+ frontmatter.vibepro_story_id,
266
+ frontmatter.story_ref,
267
+ frontmatter.story,
268
+ frontmatter.requirement_id
269
+ ].filter(Boolean).map((value) => String(value));
270
+ if (candidateIds.includes(storyId)) return file;
271
+ }
272
+ return null;
273
+ }
274
+
275
+ export async function resolveStorySource(repoRoot, options) {
276
+ if (options.storySource?.path) {
277
+ try {
278
+ const parsed = await parseStoryLikeDocument(repoRoot, options.storySource.path, 'story');
279
+ return {
280
+ ...parsed,
281
+ ...Object.fromEntries(Object.entries(options.storySource).filter(([, value]) => value !== null && value !== undefined)),
282
+ content: parsed.content
283
+ };
284
+ } catch {
285
+ return { ...options.storySource, kind: 'story', content: options.storySource.content ?? '' };
286
+ }
287
+ }
288
+ return options.storySource
289
+ ? { ...options.storySource, kind: 'story', content: options.storySource.content ?? '' }
290
+ : findStorySource(repoRoot, options.story);
291
+ }
292
+
293
+ async function collectRequirementSources(repoRoot, { story, storySource, codeFiles }) {
294
+ const docs = [];
295
+ for (const sourceDir of REQUIREMENT_SOURCE_DIRS) {
296
+ const files = await listFiles(path.join(repoRoot, sourceDir.dir));
297
+ docs.push(...files
298
+ .filter((file) => /\.(md|mdx)$/i.test(file))
299
+ .map((file) => ({ file, kind: sourceDir.kind })));
300
+ }
301
+
302
+ const linkedPaths = new Set(extractLinkedDocPaths(storySource?.content ?? ''));
303
+ const storyId = story?.story_id ?? storySource?.story_id ?? null;
304
+ const sourceText = [
305
+ storyId,
306
+ storySource?.title,
307
+ storySource?.background,
308
+ storySource?.policy,
309
+ ...(storySource?.acceptance_criteria ?? []),
310
+ ...codeFiles
311
+ ].filter(Boolean).join(' ').toLowerCase();
312
+ const sourceKeywords = DOMAIN_KEYWORDS.filter((keyword) => sourceText.includes(keyword.toLowerCase()));
313
+ const refs = [];
314
+ const seen = new Set();
315
+ for (const { file, kind } of docs.slice(0, 240)) {
316
+ const parsed = await parseStoryLikeDocument(repoRoot, file, kind);
317
+ if (seen.has(parsed.path) || parsed.path === storySource?.path) continue;
318
+ const linked = linkedPaths.has(parsed.path) || linkedPaths.has(`./${parsed.path}`);
319
+ const storyMatched = storyId && parsed.frontmatter?.story_id === storyId;
320
+ const refMatched = storyId && [
321
+ parsed.frontmatter?.story_ref,
322
+ parsed.frontmatter?.story,
323
+ parsed.frontmatter?.requirement_id
324
+ ].filter(Boolean).some((value) => String(value) === storyId);
325
+ const haystack = [parsed.path, parsed.title, parsed.content.slice(0, 1600)].filter(Boolean).join(' ').toLowerCase();
326
+ const invariantHints = extractInvariantTexts(parsed).slice(0, 6);
327
+ const keywordHits = sourceKeywords.filter((keyword) => haystack.includes(keyword.toLowerCase()));
328
+ const keywordMatched = sourceKeywords.length > 0
329
+ && keywordHits.length >= 2
330
+ && invariantHints.length > 0;
331
+ if (!linked && !storyMatched && !refMatched && !keywordMatched) continue;
332
+ seen.add(parsed.path);
333
+ refs.push({
334
+ ...parsed,
335
+ linked_from_story: linked,
336
+ matched_by_story_id: Boolean(storyMatched || refMatched),
337
+ invariant_hints: invariantHints
338
+ });
339
+ }
340
+ return refs.sort(compareRequirementSources).slice(0, 20);
341
+ }
342
+
343
+ function toRequirementSourceRef(source) {
344
+ return {
345
+ kind: source.kind ?? inferSourceKind(source.path),
346
+ path: source.path ?? null,
347
+ title: source.title ?? null,
348
+ linked_from_story: source.linked_from_story === true,
349
+ matched_by_story_id: source.matched_by_story_id === true,
350
+ invariant_count: source.invariant_hints?.length ?? 0
351
+ };
352
+ }
353
+
354
+ function formatSourceRef(source) {
355
+ if (!source) return 'source:unknown';
356
+ const kind = source.kind ?? inferSourceKind(source.path);
357
+ return `${kind}:${source.path ?? '-'}`;
358
+ }
359
+
360
+ export function inferSourceKind(filePath) {
361
+ const normalized = normalizePath(filePath ?? '');
362
+ if (
363
+ normalized.includes('/stories/')
364
+ || normalized.includes('/user_stories/')
365
+ || normalized.startsWith('user_stories/')
366
+ || normalized.startsWith('docs/management/stories/')
367
+ ) {
368
+ return 'story';
369
+ }
370
+ if (normalized.startsWith('docs/specs/') || normalized.startsWith('docs/features/specifications/')) return 'spec';
371
+ if (
372
+ normalized.startsWith('docs/architecture/')
373
+ || normalized.startsWith('docs/management/architecture/')
374
+ || /^docs\/.+\/ADR-[^/]+\.md$/i.test(normalized)
375
+ ) {
376
+ return 'architecture';
377
+ }
378
+ if (
379
+ normalized.startsWith('docs/management/policies/')
380
+ || normalized.startsWith('docs/frames/')
381
+ || normalized.startsWith('docs/00-glossary/')
382
+ ) {
383
+ return 'policy';
384
+ }
385
+ return 'requirement';
386
+ }
387
+
388
+ export function isStoryDocPath(filePath) {
389
+ const normalized = normalizePath(filePath ?? '');
390
+ if (!normalized) return false;
391
+ for (const prefix of STORY_DIR_PREFIXES) {
392
+ if (normalized.startsWith(`${prefix}/`)) return true;
393
+ }
394
+ return /^docs\/.+\/stories\//.test(normalized);
395
+ }
396
+
397
+ function extractLinkedDocPaths(content) {
398
+ const refs = new Set();
399
+ const source = String(content ?? '');
400
+ const patterns = [
401
+ /\[[^\]]+\]\(([^)\s]+\.mdx?)(?:#[^)]+)?\)/gi,
402
+ /\b(?:path|doc|file|adr|specification|architecture)\s*:\s*['"]?([^'"\n]+\.mdx?)(?:#[^\s'"]*)?['"]?/gi,
403
+ /\b(docs\/[^\s)'"]+\.mdx?)(?:#[^\s)'"]*)?/gi
404
+ ];
405
+ for (const pattern of patterns) {
406
+ for (const match of source.matchAll(pattern)) {
407
+ const cleaned = cleanupLinkedPath(match[1]);
408
+ if (cleaned) refs.add(cleaned);
409
+ }
410
+ }
411
+ return [...refs];
412
+ }
413
+
414
+ function cleanupLinkedPath(value) {
415
+ const cleaned = normalizePath(String(value ?? '')
416
+ .trim()
417
+ .replace(/^['"]|['"]$/g, '')
418
+ .replace(/[),.。]$/g, '')
419
+ .split('#')[0]
420
+ .split('?')[0]);
421
+ if (!cleaned || /^n\/?a$/i.test(cleaned) || cleaned === '-') return null;
422
+ return cleaned;
423
+ }
424
+
425
+ function compareRequirementSources(a, b) {
426
+ if (a.linked_from_story !== b.linked_from_story) return a.linked_from_story ? -1 : 1;
427
+ const kindOrder = { spec: 0, architecture: 1, policy: 2, requirement: 3 };
428
+ const kindDelta = (kindOrder[a.kind] ?? 9) - (kindOrder[b.kind] ?? 9);
429
+ if (kindDelta !== 0) return kindDelta;
430
+ return String(a.path ?? '').localeCompare(String(b.path ?? ''));
431
+ }
432
+
433
+ function extractInvariantsFromInferredSpec(spec, storySource) {
434
+ if (!spec || !Array.isArray(spec.clauses)) return [];
435
+ return spec.clauses
436
+ .filter((clause) => clause && typeof clause.statement === 'string')
437
+ .map((clause) => ({
438
+ id: clause.id,
439
+ text: clause.statement.slice(0, 240),
440
+ source: {
441
+ kind: 'inferred_spec',
442
+ path: storySource?.path ?? null,
443
+ clause_type: clause.type ?? 'invariant'
444
+ }
445
+ }))
446
+ .slice(0, 32);
447
+ }
448
+
449
+ function extractInvariants(storySource, requirementSources) {
450
+ const storyInvariants = extractInvariantTexts(storySource).map((text, index) => ({
451
+ id: `REQ-INV-${String(index + 1).padStart(3, '0')}`,
452
+ text,
453
+ source: { kind: 'story', path: storySource?.path ?? null }
454
+ }));
455
+ const sourceInvariants = requirementSources
456
+ .flatMap((source) => (source.invariant_hints ?? []).map((text) => ({
457
+ text,
458
+ source: { kind: source.kind, path: source.path }
459
+ })))
460
+ .filter((item) => !storyInvariants.some((invariant) => normalizeText(invariant.text) === normalizeText(item.text)))
461
+ .map((item, index) => ({
462
+ id: `REQ-SRC-${String(index + 1).padStart(3, '0')}`,
463
+ ...item
464
+ }));
465
+ return [...storyInvariants, ...sourceInvariants].slice(0, 24);
466
+ }
467
+
468
+ export function extractInvariantTexts(doc) {
469
+ const sourceKind = doc?.kind ?? inferSourceKind(doc?.path);
470
+ const content = [
471
+ doc?.policy,
472
+ ...(doc?.acceptance_criteria ?? []),
473
+ ...(sourceKind === 'architecture' && doc?.content ? extractDecisionLines(doc.content) : []),
474
+ ...(doc?.content ? extractImportantLines(doc.content) : [])
475
+ ].filter(Boolean);
476
+ const values = [];
477
+ for (const text of content) {
478
+ for (const sentence of splitSentences(text)) {
479
+ const clean = cleanupLine(sentence);
480
+ if (!clean || clean.length < 8) continue;
481
+ if (isDiagnosticNarrative(clean)) continue;
482
+ if (INVARIANT_PATTERNS.some((pattern) => pattern.test(clean))) {
483
+ values.push(clean);
484
+ }
485
+ }
486
+ }
487
+ return [...new Set(values.map((item) => item.slice(0, 240)))].slice(0, 12);
488
+ }
489
+
490
+ function extractDecisionLines(content) {
491
+ return [
492
+ extractRawSection(content, ['Decision', '判断', '決定']),
493
+ extractRawSection(content, ['Consequences', '影響', '結果', '制約'])
494
+ ]
495
+ .filter(Boolean)
496
+ .flatMap((section) => section.split('\n'))
497
+ .map((line) => line.trim())
498
+ .filter((line) => /^[-*]\s+/.test(line))
499
+ .map((line) => line.replace(/^[-*]\s+/, ''))
500
+ .slice(0, 24);
501
+ }
502
+
503
+ function isDiagnosticNarrative(text) {
504
+ return /^--/.test(text)
505
+ || /npm\s+(?:run\s+)?(?:test|type-?check)|vibepro|graphify|diagnostic|diagnose|hotspot|refactor|runtime file|責務混在|診断|候補|スコア|出現|差分/.test(String(text).toLowerCase());
506
+ }
507
+
508
+ function extractImportantLines(content) {
509
+ return stripFrontmatter(content)
510
+ .split('\n')
511
+ .map((line) => line.trim())
512
+ .filter((line) => /^[-*]\s+/.test(line))
513
+ .map((line) => line.replace(/^[-*]\s+/, ''))
514
+ .slice(0, 80);
515
+ }
516
+
517
+ export async function resolveCodeFiles(repoRoot, options) {
518
+ const files = options.files?.length > 0
519
+ ? options.files
520
+ : options.fileGroups
521
+ ? [...(options.fileGroups.source?.files ?? [])]
522
+ : await resolveInferredSpecCodeFiles(repoRoot, options.inferredSpec);
523
+ const effectiveFiles = files.length > 0 ? files : await listLikelyRuntimeFiles(repoRoot);
524
+ return [...new Set(effectiveFiles.map(normalizePath))]
525
+ .filter((file) => CODE_EXTENSIONS.has(path.extname(file)))
526
+ .filter((file) => !file.includes('/node_modules/') && !file.startsWith('.vibepro/'))
527
+ .slice(0, MAX_SCAN_FILES);
528
+ }
529
+
530
+ async function resolveInferredSpecCodeFiles(repoRoot, spec) {
531
+ const patterns = extractInferredSpecFilePatterns(spec);
532
+ if (patterns.length === 0) return [];
533
+ const exactFiles = patterns.filter((file) => !file.includes('*'));
534
+ const globPatterns = patterns.filter((file) => file.includes('*'));
535
+ if (globPatterns.length === 0) return exactFiles;
536
+ const repoFiles = (await listFiles(repoRoot))
537
+ .filter((file) => CODE_EXTENSIONS.has(path.extname(file)))
538
+ .map((file) => normalizePath(path.relative(repoRoot, file)));
539
+ return [
540
+ ...exactFiles,
541
+ ...repoFiles.filter((file) => globPatterns.some((pattern) => pathPatternMatches(pattern, file)))
542
+ ];
543
+ }
544
+
545
+ function extractInferredSpecFilePatterns(spec) {
546
+ if (!spec || !Array.isArray(spec.clauses)) return [];
547
+ return [...new Set(spec.clauses.flatMap((clause) => [
548
+ ...(Array.isArray(clause?.origin?.code_refs) ? clause.origin.code_refs.map((ref) => ref?.file) : []),
549
+ ...(Array.isArray(clause?.verifiable_by?.code_pattern) ? clause.verifiable_by.code_pattern.map((pattern) => pattern?.file_glob) : [])
550
+ ])
551
+ .filter(Boolean)
552
+ .map(normalizePath))];
553
+ }
554
+
555
+ async function listLikelyRuntimeFiles(repoRoot) {
556
+ const roots = [
557
+ path.join(repoRoot, 'src', 'app', 'api'),
558
+ path.join(repoRoot, 'src', 'lib', 'services'),
559
+ path.join(repoRoot, 'src', 'lib', 'actions')
560
+ ];
561
+ const files = [];
562
+ for (const root of roots) {
563
+ files.push(...await listFiles(root));
564
+ }
565
+ return files
566
+ .filter((file) => CODE_EXTENSIONS.has(path.extname(file)))
567
+ .map((file) => normalizePath(path.relative(repoRoot, file)))
568
+ .slice(0, MAX_SCAN_FILES);
569
+ }
570
+
571
+ export async function collectCodeScenarios(repoRoot, files) {
572
+ const scenarios = [];
573
+ for (const file of files) {
574
+ const absolute = path.join(repoRoot, file);
575
+ let content = '';
576
+ try {
577
+ content = await readFile(absolute, 'utf8');
578
+ } catch {
579
+ continue;
580
+ }
581
+ const code = stripBlockComments(content);
582
+ scenarios.push({
583
+ file,
584
+ branches: extractBranches(code),
585
+ state_transitions: extractStateTransitions(code),
586
+ external_effects: extractExternalEffects(code),
587
+ response_messages: extractResponseMessages(code),
588
+ domain_keywords: DOMAIN_KEYWORDS.filter((keyword) => code.toLowerCase().includes(keyword.toLowerCase()))
589
+ });
590
+ }
591
+ return scenarios.filter((scenario) => (
592
+ scenario.branches.length > 0
593
+ || scenario.state_transitions.length > 0
594
+ || scenario.external_effects.length > 0
595
+ || scenario.response_messages.length > 0
596
+ ));
597
+ }
598
+
599
+ function extractBranches(content) {
600
+ const branches = [];
601
+ for (const match of content.matchAll(/\bif\s*\(([^)]{1,180})\)/g)) {
602
+ branches.push({ kind: 'if', condition: cleanupLine(match[1]) });
603
+ }
604
+ for (const match of content.matchAll(/\bcase\s+([^:]{1,120}):/g)) {
605
+ branches.push({ kind: 'case', condition: cleanupLine(match[1]) });
606
+ }
607
+ return branches.slice(0, 30);
608
+ }
609
+
610
+ function extractStateTransitions(content) {
611
+ const transitions = [];
612
+ const patterns = [
613
+ { key: 'userType', pattern: /\bUserType\s*:\s*([0-9]+)/g },
614
+ { key: 'userType', pattern: /\buserType\s*:\s*([0-9]+)/g },
615
+ { key: 'cancelAtPeriodEnd', pattern: /\bcancel(?:_|A)tPeriodEnd\s*:\s*(true|false)/gi },
616
+ { key: 'subscriptionCancelAtPeriodEnd', pattern: /\bSubscriptionCancelAtPeriodEnd\s*:\s*(true|false)/g },
617
+ { key: 'status', pattern: /\bstatus\s*:\s*['"]([^'"]+)['"]/g }
618
+ ];
619
+ for (const { key, pattern } of patterns) {
620
+ for (const match of content.matchAll(pattern)) {
621
+ transitions.push({ key, value: match[1] });
622
+ }
623
+ }
624
+ return transitions.slice(0, 30);
625
+ }
626
+
627
+ function extractExternalEffects(content) {
628
+ const effects = [];
629
+ const patterns = [
630
+ { type: 'db_update', pattern: /\bprisma\.[A-Za-z0-9_$.]+\.update(?:Many)?\s*\(/g },
631
+ { type: 'db_delete', pattern: /\bprisma\.[A-Za-z0-9_$.]+\.delete(?:Many)?\s*\(/g },
632
+ { type: 'stripe_subscription_update', pattern: /\bstripe\.subscriptions\.update\s*\(/g },
633
+ { type: 'webhook_signature', pattern: /\bconstructEvent|verify(?:Webhook|Signature)?\b/g },
634
+ { type: 'notification', pattern: /\b(send|notify|notification|email|resend)\b/gi }
635
+ ];
636
+ for (const { type, pattern } of patterns) {
637
+ for (const match of content.matchAll(pattern)) {
638
+ effects.push({ type, evidence: match[0] });
639
+ }
640
+ }
641
+ return effects.slice(0, 30);
642
+ }
643
+
644
+ function extractResponseMessages(content) {
645
+ return [...content.matchAll(/message\s*:\s*['"`]([^'"`]{4,120})['"`]/g)]
646
+ .map((match) => match[1])
647
+ .slice(0, 20);
648
+ }
649
+
650
+ function buildScenarioGaps({ invariants, codeScenarios, storySource, requirementSources, inferredSpec = null }) {
651
+ if (invariants.length === 0) return [];
652
+ const gaps = [];
653
+ const inferredSpecContext = buildInferredSpecContext(inferredSpec);
654
+ const scopeContext = buildRequirementScopeContext({ storySource, requirementSources, inferredSpecContext });
655
+ const acceptanceText = [
656
+ ...(storySource?.acceptance_criteria ?? []),
657
+ storySource?.policy,
658
+ ...requirementSources.flatMap((source) => [
659
+ ...(source.acceptance_criteria ?? []),
660
+ source.policy
661
+ ]),
662
+ ...inferredSpecContext.texts
663
+ ].filter(Boolean).join('\n').toLowerCase();
664
+ for (const scenario of codeScenarios) {
665
+ for (const branch of scenario.branches) {
666
+ const condition = branch.condition.toLowerCase();
667
+ if (!isDomainBranch(condition)) continue;
668
+ if (isImplementationGuardBranch(branch.condition)) continue;
669
+ if (acceptanceText && acceptanceText.includes(condition.slice(0, 24))) continue;
670
+ if (isBranchCoveredByRequirementScope({ branch, scopeContext })) continue;
671
+ if (isBranchCoveredByInferredSpec({
672
+ branch,
673
+ scenario,
674
+ invariants,
675
+ inferredSpecContext
676
+ })) continue;
677
+ gaps.push({
678
+ id: `REQ-GAP-${String(gaps.length + 1).padStart(3, '0')}`,
679
+ title: 'Requirement Sourcesに明示されていない重要分岐がある',
680
+ detail: `${scenario.file} の \`${branch.condition}\` 分岐が、Story/Spec/Architecture/Policyの受け入れ基準または方針で明示されているか確認が必要。`,
681
+ file: scenario.file,
682
+ evidence: branch,
683
+ related_invariants: relatedInvariantIds(invariants, branch.condition)
684
+ });
685
+ if (gaps.length >= 12) return gaps;
686
+ }
687
+ }
688
+ return gaps;
689
+ }
690
+
691
+ function buildRequirementScopeContext({ storySource, requirementSources, inferredSpecContext }) {
692
+ const texts = [
693
+ storySource?.background,
694
+ storySource?.policy,
695
+ storySource?.content,
696
+ ...(storySource?.acceptance_criteria ?? []),
697
+ ...requirementSources.flatMap((source) => [
698
+ source.background,
699
+ source.policy,
700
+ source.content,
701
+ ...(source.acceptance_criteria ?? [])
702
+ ]),
703
+ ...inferredSpecContext.texts
704
+ ].filter(Boolean);
705
+ return {
706
+ texts: texts.map((text) => ({
707
+ raw: String(text),
708
+ normalized: normalizeComparableText(text),
709
+ inherited_behavior: INHERITED_BEHAVIOR_PATTERNS.some((pattern) => pattern.test(String(text)))
710
+ }))
711
+ };
712
+ }
713
+
714
+ function isBranchCoveredByRequirementScope({ branch, scopeContext }) {
715
+ const conditionTokens = meaningfulConditionTokens(branch.condition);
716
+ if (conditionTokens.length === 0) return false;
717
+ return scopeContext.texts.some((entry) => (
718
+ entry.inherited_behavior && tokensCoveredByText(conditionTokens, entry.normalized)
719
+ ));
720
+ }
721
+
722
+ function buildInferredSpecContext(spec) {
723
+ if (!spec || !Array.isArray(spec.clauses)) {
724
+ return { clauses: [], texts: [] };
725
+ }
726
+ const clauses = spec.clauses
727
+ .filter((clause) => clause && typeof clause.statement === 'string')
728
+ .map((clause) => {
729
+ const codeRefs = Array.isArray(clause.origin?.code_refs) ? clause.origin.code_refs : [];
730
+ const codePatterns = Array.isArray(clause.verifiable_by?.code_pattern) ? clause.verifiable_by.code_pattern : [];
731
+ const files = [
732
+ ...codeRefs.map((ref) => ref?.file),
733
+ ...codePatterns.map((pattern) => pattern?.file_glob)
734
+ ].filter(Boolean).map((file) => normalizePath(file));
735
+ const fragments = [
736
+ ...codeRefs.map((ref) => ref?.anchor),
737
+ ...codePatterns.map((pattern) => pattern?.must_contain)
738
+ ].filter(Boolean).map((value) => String(value));
739
+ const text = [
740
+ clause.id,
741
+ clause.type,
742
+ clause.statement,
743
+ ...files,
744
+ ...fragments
745
+ ].filter(Boolean).join('\n');
746
+ return {
747
+ id: clause.id,
748
+ type: clause.type ?? 'invariant',
749
+ statement: clause.statement,
750
+ files,
751
+ fragments,
752
+ text,
753
+ normalized_text: normalizeComparableText(text)
754
+ };
755
+ });
756
+ return {
757
+ clauses,
758
+ texts: clauses.map((clause) => clause.text)
759
+ };
760
+ }
761
+
762
+ function isBranchCoveredByInferredSpec({ branch, scenario, invariants, inferredSpecContext }) {
763
+ if (!inferredSpecContext?.clauses?.length) return false;
764
+ const inferredInvariants = invariants.filter((invariant) => invariant.source?.kind === 'inferred_spec');
765
+ const relatedIds = new Set(relatedInvariantIds(inferredInvariants, branch.condition));
766
+ const condition = normalizeComparableCode(branch.condition);
767
+ const conditionTokens = meaningfulConditionTokens(branch.condition);
768
+
769
+ for (const clause of inferredSpecContext.clauses) {
770
+ const appliesToFile = clauseAppliesToScenario(clause, scenario.file);
771
+ if (appliesToFile && clause.fragments.some((fragment) => codeFragmentCoversCondition(fragment, condition))) {
772
+ return true;
773
+ }
774
+ if (tokensCoveredByText(conditionTokens, clause.normalized_text)) {
775
+ return true;
776
+ }
777
+ if (appliesToFile && relatedIds.has(clause.id) && conditionTokens.some((token) => textIncludesToken(clause.normalized_text, token))) {
778
+ return true;
779
+ }
780
+ }
781
+ return false;
782
+ }
783
+
784
+ function clauseAppliesToScenario(clause, scenarioFile) {
785
+ if (!clause.files.length) return true;
786
+ return clause.files.some((filePattern) => pathPatternMatches(filePattern, scenarioFile));
787
+ }
788
+
789
+ function pathPatternMatches(pattern, filePath) {
790
+ const normalizedPattern = normalizePath(pattern);
791
+ const normalizedFile = normalizePath(filePath);
792
+ if (!normalizedPattern) return false;
793
+ if (normalizedPattern === normalizedFile) return true;
794
+ if (!normalizedPattern.includes('*')) return normalizedFile.endsWith(normalizedPattern);
795
+ const regex = new RegExp(`^${escapeRegExp(normalizedPattern)
796
+ .replace(/\\\*\\\*/g, '.*')
797
+ .replace(/\\\*/g, '[^/]*')}$`);
798
+ return regex.test(normalizedFile);
799
+ }
800
+
801
+ function codeFragmentCoversCondition(fragment, normalizedCondition) {
802
+ const normalizedFragment = normalizeComparableCode(fragment);
803
+ if (!normalizedFragment || !normalizedCondition) return false;
804
+ return normalizedCondition.includes(normalizedFragment) || normalizedFragment.includes(normalizedCondition);
805
+ }
806
+
807
+ function tokensCoveredByText(tokens, normalizedText) {
808
+ if (tokens.length === 0 || !normalizedText) return false;
809
+ const matches = tokens.filter((token) => textIncludesToken(normalizedText, token));
810
+ if (matches.some((token) => token.length >= 8)) return true;
811
+ return matches.length >= Math.min(2, tokens.length);
812
+ }
813
+
814
+ function textIncludesToken(normalizedText, token) {
815
+ if (normalizedText.includes(token)) return true;
816
+ return tokenVariants(token).some((variant) => variant.length >= 5 && normalizedText.includes(variant));
817
+ }
818
+
819
+ function tokenVariants(token) {
820
+ const variants = new Set();
821
+ variants.add(token);
822
+ for (const suffix of ['ing', 'ed', 'ion', 'ions', 'ive', 'ives', 'ed']) {
823
+ if (token.length > suffix.length + 4 && token.endsWith(suffix)) {
824
+ variants.add(token.slice(0, -suffix.length));
825
+ }
826
+ }
827
+ if (token.endsWith('ation') && token.length > 9) variants.add(token.slice(0, -5));
828
+ if (token.endsWith('ating') && token.length > 9) variants.add(token.slice(0, -3));
829
+ if (token.endsWith('archive') && token.length > 9) variants.add(token.slice(0, -1));
830
+ return [...variants];
831
+ }
832
+
833
+ function meaningfulConditionTokens(value) {
834
+ return normalizeComparableText(value)
835
+ .split(' ')
836
+ .filter((token) => token.length >= 4 || DOMAIN_KEYWORDS.includes(token))
837
+ .filter((token) => !GENERIC_CONDITION_TOKENS.has(token))
838
+ .slice(0, 8);
839
+ }
840
+
841
+ function normalizeComparableCode(value) {
842
+ return String(value ?? '')
843
+ .toLowerCase()
844
+ .replace(/[?!]/g, '')
845
+ .replace(/\s+/g, '')
846
+ .replace(/['"`]/g, '');
847
+ }
848
+
849
+ function normalizeComparableText(value) {
850
+ return String(value ?? '')
851
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
852
+ .toLowerCase()
853
+ .replace(/[^a-z0-9_]+/gi, ' ')
854
+ .replace(/\s+/g, ' ')
855
+ .trim();
856
+ }
857
+
858
+ function isImplementationGuardBranch(condition) {
859
+ const normalized = String(condition ?? '').trim();
860
+ const compact = normalized.replace(/\s+/g, ' ');
861
+ const lower = compact.toLowerCase();
862
+ return /^typeof\s+[\w$.[\]?()]+(?:\?\.[\w$.[\]?()]+)*\s*(?:!==|===)\s*['"]function['"]$/.test(lower)
863
+ || /^(?:req|request)\.body\.[\w$.[\]?]+\s*!==\s*undefined$/.test(lower)
864
+ || /^this\.[\w$.[\]?]+\.has\([^)]{1,80}\)?$/.test(lower)
865
+ || /^typeof\s+this\.[\w$.[\]?]+\s*(?:!==|===)\s*['"]function['"]$/.test(lower)
866
+ || /^([a-z_$][\w$]*)\.id\s*!==\s*\1id$/.test(lower);
867
+ }
868
+
869
+ function buildContradictions({ invariants, codeScenarios }) {
870
+ const contradictions = [];
871
+ const premiumUntilEndInvariants = invariants.filter(isPremiumUntilEndInvariant);
872
+ if (premiumUntilEndInvariants.length > 0) {
873
+ for (const scenario of codeScenarios) {
874
+ const userTypes = new Set(
875
+ scenario.state_transitions
876
+ .filter((item) => item.key === 'userType')
877
+ .map((item) => String(item.value))
878
+ );
879
+ if (userTypes.has('1') && userTypes.has('2')) {
880
+ contradictions.push({
881
+ id: `REQ-CON-${String(contradictions.length + 1).padStart(3, '0')}`,
882
+ title: 'premium維持要件と状態遷移が分岐している可能性',
883
+ detail: `${scenario.file} は同じ変更範囲で userType=1 と userType=2 の両方を返す/更新する。期間終了までpremium維持する要件と矛盾しないか確認が必要。`,
884
+ file: scenario.file,
885
+ related_invariants: premiumUntilEndInvariants.map((invariant) => invariant.id).slice(0, 5)
886
+ });
887
+ }
888
+ }
889
+ }
890
+ return contradictions.slice(0, 8);
891
+ }
892
+
893
+ function isPremiumUntilEndInvariant(invariant) {
894
+ const text = normalizeText(invariant?.text ?? '');
895
+ if (/shape|レスポンスshape|response\s*shape/.test(text)) return false;
896
+ return /premium|プレミアム/.test(text)
897
+ && /維持|keep|期間終了|current_period_end|until/.test(text);
898
+ }
899
+
900
+ function isDomainBranch(condition) {
901
+ const normalized = String(condition ?? '').trim().toLowerCase();
902
+ if (!normalized) return false;
903
+ if (isGenericImplementationGuard(normalized)) return false;
904
+ return /user|auth|session|subscription|premium|stripe|webhook|signature|customer|cancel/i.test(normalized);
905
+ }
906
+
907
+ function isGenericImplementationGuard(condition) {
908
+ return /^error\s+instanceof\s+error\b/.test(condition)
909
+ || /^!?found$/.test(condition)
910
+ || /^!?session(?:id)?(?:\s*\|\|\s*![a-z0-9_.$]+)?$/.test(condition)
911
+ || /^!?isinsecureheaderauthallowed\(/.test(condition)
912
+ || /^session\.[a-z0-9_?.]+$/.test(condition)
913
+ || /^sessions\[[^\]]+\]\.[a-z0-9_?.]+\s*!==/.test(condition)
914
+ || /^!?normalizedsessionid$/.test(condition)
915
+ || /^changesnotpushed\s*>/.test(condition)
916
+ || /^result\.notfound\s*\|\|\s*!result\.success$/.test(condition)
917
+ || /^message\.includes\(/.test(condition)
918
+ || /^value\s*===\s*['"][a-z0-9_:-]+['"]$/.test(condition);
919
+ }
920
+
921
+ function relatedInvariantIds(invariants, text) {
922
+ const haystack = text.toLowerCase();
923
+ return invariants
924
+ .filter((invariant) => DOMAIN_KEYWORDS.some((keyword) => (
925
+ haystack.includes(keyword.toLowerCase()) && invariant.text.toLowerCase().includes(keyword.toLowerCase())
926
+ )))
927
+ .map((invariant) => invariant.id)
928
+ .slice(0, 5);
929
+ }
930
+
931
+ export async function parseStoryLikeDocument(repoRoot, absoluteOrRelativeFile, kind = null) {
932
+ const absolute = path.isAbsolute(absoluteOrRelativeFile)
933
+ ? absoluteOrRelativeFile
934
+ : path.join(repoRoot, absoluteOrRelativeFile);
935
+ const content = await readFile(absolute, 'utf8');
936
+ const relative = normalizePath(path.relative(repoRoot, absolute));
937
+ const frontmatter = parseFrontmatter(content);
938
+ return {
939
+ kind: kind ?? inferSourceKind(relative),
940
+ path: relative,
941
+ frontmatter,
942
+ story_id: frontmatter.story_id ?? null,
943
+ title: findMarkdownTitle(content),
944
+ content,
945
+ background: extractSectionText(content, ['背景', '現状', '課題']),
946
+ policy: extractSectionText(content, ['方針', '実装方針', '実装戦略', 'ポリシー']),
947
+ acceptance_criteria: extractAcceptanceCriteria(content)
948
+ };
949
+ }
950
+
951
+ function parseFrontmatter(content) {
952
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
953
+ if (!match) return {};
954
+ const result = {};
955
+ for (const line of match[1].split('\n')) {
956
+ const item = line.match(/^\s*([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
957
+ if (!item) continue;
958
+ result[item[1]] = item[2].replace(/^['"]|['"]$/g, '');
959
+ }
960
+ return result;
961
+ }
962
+
963
+ function findMarkdownTitle(content) {
964
+ const match = content.match(/^#\s+(.+)$/m);
965
+ return match?.[1]?.trim() ?? null;
966
+ }
967
+
968
+ function extractSectionText(content, headings) {
969
+ for (const heading of headings) {
970
+ const escaped = escapeRegExp(heading);
971
+ const match = content.match(new RegExp(`^##+\\s+.*${escaped}.*\\n([\\s\\S]*?)(?=^##+\\s+|(?![\\s\\S]))`, 'm'));
972
+ if (!match) continue;
973
+ const paragraph = match[1]
974
+ .split('\n')
975
+ .map((line) => line.trim())
976
+ .filter((line) => line && !line.startsWith('|') && !line.startsWith('---'))
977
+ .join(' ')
978
+ .replace(/\s+/g, ' ')
979
+ .slice(0, 1200);
980
+ if (paragraph) return paragraph;
981
+ }
982
+ return null;
983
+ }
984
+
985
+ function extractAcceptanceCriteria(content) {
986
+ const section = extractRawSection(content, ['受け入れ基準', '完了定義', 'Acceptance Criteria']);
987
+ const source = section ?? content;
988
+ return source
989
+ .split('\n')
990
+ .map((line) => line.trim())
991
+ .filter((line) => /^-\s+(?:\[[ xX]\]\s+)?/.test(line))
992
+ .map((line) => line.replace(/^-\s+(?:\[[ xX]\]\s+)?/, '').trim())
993
+ .filter(Boolean)
994
+ .slice(0, 16);
995
+ }
996
+
997
+ function extractRawSection(content, headings) {
998
+ for (const heading of headings) {
999
+ const escaped = escapeRegExp(heading);
1000
+ const match = content.match(new RegExp(`^##+\\s+.*${escaped}.*\\n([\\s\\S]*?)(?=^##+\\s+|(?![\\s\\S]))`, 'm'));
1001
+ if (match) return match[1];
1002
+ }
1003
+ return null;
1004
+ }
1005
+
1006
+ async function listFiles(root) {
1007
+ const result = [];
1008
+ async function walk(dir) {
1009
+ let entries = [];
1010
+ try {
1011
+ entries = await readdir(dir, { withFileTypes: true });
1012
+ } catch {
1013
+ return;
1014
+ }
1015
+ for (const entry of entries) {
1016
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.vibepro') continue;
1017
+ const fullPath = path.join(dir, entry.name);
1018
+ if (entry.isDirectory()) await walk(fullPath);
1019
+ else result.push(fullPath);
1020
+ }
1021
+ }
1022
+ await walk(root);
1023
+ return result;
1024
+ }
1025
+
1026
+ function splitSentences(text) {
1027
+ return String(text)
1028
+ .split(/(?<=[。.!?])\s+|\n+/)
1029
+ .flatMap((line) => line.split(/(?<=。)/))
1030
+ .map((line) => line.trim())
1031
+ .filter(Boolean);
1032
+ }
1033
+
1034
+ function cleanupLine(value) {
1035
+ return String(value)
1036
+ .replace(/^[-*]\s+/, '')
1037
+ .replace(/^\[[ xX]\]\s+/, '')
1038
+ .replace(/`/g, '')
1039
+ .replace(/\s+/g, ' ')
1040
+ .trim();
1041
+ }
1042
+
1043
+ function normalizeText(value) {
1044
+ return cleanupLine(value).toLowerCase();
1045
+ }
1046
+
1047
+ function normalizePath(value) {
1048
+ return String(value).replace(/\\/g, '/').replace(/^\.\//, '');
1049
+ }
1050
+
1051
+ function stripBlockComments(content) {
1052
+ return content.replace(/\/\*[\s\S]*?\*\//g, '');
1053
+ }
1054
+
1055
+ function stripFrontmatter(content) {
1056
+ return String(content).replace(/^---\n[\s\S]*?\n---\n?/, '');
1057
+ }
1058
+
1059
+ function formatItems(items, formatter) {
1060
+ if (!Array.isArray(items) || items.length === 0) return '- なし';
1061
+ return items.map(formatter).join('\n');
1062
+ }
1063
+
1064
+ function escapeRegExp(value) {
1065
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1066
+ }