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,445 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import {
7
+ CODE_EXTENSIONS,
8
+ DOMAIN_KEYWORDS,
9
+ INVARIANT_PATTERNS,
10
+ collectCodeScenarios,
11
+ extractInvariantTexts,
12
+ parseStoryLikeDocument,
13
+ resolveCodeFiles,
14
+ resolveStorySource
15
+ } from './requirement-consistency.js';
16
+ import { readInferredSpec } from './spec-store.js';
17
+ import { WORKSPACE_DIR } from './workspace.js';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const TEST_GLOB_DIRS = ['test', 'tests', '__tests__', 'e2e'];
21
+ const TEST_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
22
+ const TEST_FILE_PATTERN = /\.(test|spec)\.(?:m|c)?[jt]sx?$/i;
23
+ const MAX_TEST_FILES = 120;
24
+ const MAX_TEST_CASES_PER_FILE = 24;
25
+ const MAX_ARCHITECTURE_DOCS = 12;
26
+ const ARCHITECTURE_DOC_DIRS = [
27
+ path.join('docs', 'architecture'),
28
+ path.join('docs', 'adr'),
29
+ path.join('docs', 'design')
30
+ ];
31
+
32
+ export async function buildSpecFingerprint(repoRoot, options = {}) {
33
+ const root = path.resolve(repoRoot);
34
+ const storyId = options.storyId ?? null;
35
+ const storyOption = options.story ?? null;
36
+
37
+ const codeFiles = await resolveCodeFiles(root, options);
38
+ const storyForResolution = storyOption
39
+ ?? (storyId ? { story_id: storyId, title: options.storyTitle ?? null } : null);
40
+ const storySource = await resolveStorySource(root, {
41
+ story: storyForResolution,
42
+ storySource: options.storySource,
43
+ codeFiles
44
+ });
45
+ const codeScenarios = await collectCodeScenarios(root, codeFiles);
46
+ const testFingerprint = await collectTestFingerprint(root);
47
+ const previousSpec = storyId ? await readInferredSpec(root, storyId) : null;
48
+ const architectureFingerprint = await collectArchitectureFingerprint(root, storySource, storyId);
49
+ const schema = await readSchema();
50
+ const instructions = options.includeInstructions ? await readInstructions() : null;
51
+
52
+ const story = buildStoryFingerprint(storySource, storyOption, storyId);
53
+ const codeFingerprint = buildCodeFingerprint(codeScenarios);
54
+ const inputsDigest = buildInputsDigest({ story, codeFingerprint, testFingerprint, architectureFingerprint });
55
+
56
+ return {
57
+ schema_version: '0.1.0',
58
+ generated_at: new Date().toISOString(),
59
+ story_id: storyId,
60
+ story,
61
+ architecture_fingerprint: architectureFingerprint,
62
+ code_fingerprint: codeFingerprint,
63
+ test_fingerprint: testFingerprint,
64
+ previous_spec: previousSpec,
65
+ inputs_digest: inputsDigest,
66
+ extraction_hints: {
67
+ invariant_patterns: INVARIANT_PATTERNS.map((pattern) => pattern.source),
68
+ domain_keywords: DOMAIN_KEYWORDS,
69
+ story_invariant_hints: extractInvariantTexts(storySource)
70
+ },
71
+ schema_for_your_output: schema,
72
+ instructions
73
+ };
74
+ }
75
+
76
+ function buildStoryFingerprint(storySource, storyOption, storyId) {
77
+ return {
78
+ story_id: storyId ?? storyOption?.story_id ?? storySource?.story_id ?? null,
79
+ title: storyOption?.title ?? storySource?.title ?? null,
80
+ path: storySource?.path ?? null,
81
+ frontmatter: storySource?.frontmatter ?? null,
82
+ background: storySource?.background ?? null,
83
+ policy: storySource?.policy ?? null,
84
+ acceptance_criteria: storySource?.acceptance_criteria ?? []
85
+ };
86
+ }
87
+
88
+ function buildCodeFingerprint(codeScenarios) {
89
+ const branches = [];
90
+ const externalEffects = [];
91
+ const stateTransitions = [];
92
+ const responseMessages = [];
93
+ for (const scenario of codeScenarios) {
94
+ for (const branch of scenario.branches) {
95
+ branches.push({
96
+ file: scenario.file,
97
+ kind: branch.kind,
98
+ condition: branch.condition,
99
+ domain_keywords: scenario.domain_keywords
100
+ });
101
+ }
102
+ for (const effect of scenario.external_effects) {
103
+ externalEffects.push({ file: scenario.file, type: effect.type, evidence: effect.evidence });
104
+ }
105
+ for (const transition of scenario.state_transitions) {
106
+ stateTransitions.push({ file: scenario.file, key: transition.key, value: transition.value });
107
+ }
108
+ for (const message of scenario.response_messages) {
109
+ responseMessages.push({ file: scenario.file, message });
110
+ }
111
+ }
112
+ return {
113
+ files_scanned: codeScenarios.length,
114
+ branches: branches.slice(0, 120),
115
+ state_transitions: stateTransitions.slice(0, 80),
116
+ external_effects: externalEffects.slice(0, 80),
117
+ response_messages: responseMessages.slice(0, 60),
118
+ files: codeScenarios.map((scenario) => scenario.file)
119
+ };
120
+ }
121
+
122
+ async function collectTestFingerprint(repoRoot) {
123
+ const testFiles = await listTestFiles(repoRoot);
124
+ const files = [];
125
+ for (const relative of testFiles.slice(0, MAX_TEST_FILES)) {
126
+ let content = '';
127
+ try {
128
+ content = await readFile(path.join(repoRoot, relative), 'utf8');
129
+ } catch {
130
+ continue;
131
+ }
132
+ const describes = extractCallStrings(content, /\bdescribe\s*\(\s*(['"`])([^'"`]{1,160})\1/g);
133
+ const cases = extractTestCases(content);
134
+ if (describes.length === 0 && cases.length === 0) continue;
135
+ files.push({
136
+ path: relative,
137
+ describes,
138
+ cases: cases.slice(0, MAX_TEST_CASES_PER_FILE)
139
+ });
140
+ }
141
+ return {
142
+ files_scanned: files.length,
143
+ files
144
+ };
145
+ }
146
+
147
+ async function collectArchitectureFingerprint(repoRoot, storySource, storyId) {
148
+ const explicitRefs = extractArchitectureDocRefs(storySource);
149
+ const relatedRefs = await findRelatedArchitectureDocs(repoRoot, storyId);
150
+ const paths = [...new Set([...explicitRefs, ...relatedRefs])].slice(0, MAX_ARCHITECTURE_DOCS);
151
+ const docs = [];
152
+ for (const relative of paths) {
153
+ let content = '';
154
+ try {
155
+ content = await readFile(path.join(repoRoot, relative), 'utf8');
156
+ } catch {
157
+ continue;
158
+ }
159
+ docs.push({
160
+ path: relative,
161
+ title: findMarkdownTitle(content),
162
+ evidence_kind: inferArchitectureEvidenceKind(content),
163
+ flow_state_boundary_snippets: extractArchitectureSnippets(content)
164
+ });
165
+ }
166
+ return {
167
+ files_scanned: docs.length,
168
+ files: docs
169
+ };
170
+ }
171
+
172
+ function extractArchitectureDocRefs(storySource) {
173
+ const refs = [];
174
+ const content = storySource?.content ?? '';
175
+ const storyDir = storySource?.path ? path.posix.dirname(storySource.path) : '.';
176
+ for (const ref of extractFrontmatterRefs(content, 'architecture_docs')) {
177
+ refs.push(resolveStoryDocRef(storyDir, ref));
178
+ }
179
+ const frontmatterRef = storySource?.frontmatter?.architecture_docs;
180
+ if (typeof frontmatterRef === 'string') refs.push(resolveStoryDocRef(storyDir, frontmatterRef));
181
+ return refs.filter(Boolean);
182
+ }
183
+
184
+ function extractFrontmatterRefs(content, key) {
185
+ const match = String(content ?? '').match(/^---\n([\s\S]*?)\n---/);
186
+ if (!match) return [];
187
+ const lines = match[1].split('\n');
188
+ const refs = [];
189
+ let inBlock = false;
190
+ for (const line of lines) {
191
+ const direct = line.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`));
192
+ if (direct) {
193
+ const value = direct[1].trim();
194
+ if (value && value !== '|' && value !== '>') refs.push(stripYamlQuote(value));
195
+ inBlock = value === '' || value === '|' || value === '>';
196
+ continue;
197
+ }
198
+ if (inBlock) {
199
+ const item = line.match(/^\s*-\s+(.+?)\s*$/);
200
+ if (item) {
201
+ refs.push(stripYamlQuote(item[1]));
202
+ continue;
203
+ }
204
+ if (/^[A-Za-z0-9_-]+:/.test(line)) inBlock = false;
205
+ }
206
+ }
207
+ return refs;
208
+ }
209
+
210
+ function stripYamlQuote(value) {
211
+ return String(value).replace(/^['"]|['"]$/g, '').trim();
212
+ }
213
+
214
+ function resolveStoryDocRef(storyDir, ref) {
215
+ const clean = stripYamlQuote(ref);
216
+ if (!clean || /^reason\s*:/.test(clean)) return null;
217
+ const resolved = clean.startsWith('.')
218
+ ? path.posix.normalize(path.posix.join(storyDir, clean))
219
+ : path.posix.normalize(clean);
220
+ return resolved.replace(/\\/g, '/').replace(/^\.\//, '');
221
+ }
222
+
223
+ async function findRelatedArchitectureDocs(repoRoot, storyId) {
224
+ if (!storyId) return [];
225
+ const needle = String(storyId).toLowerCase();
226
+ const shortNeedle = needle.replace(/^story-/, '');
227
+ const files = [];
228
+ for (const dir of ARCHITECTURE_DOC_DIRS) {
229
+ const absolute = path.join(repoRoot, dir);
230
+ try {
231
+ await walkMarkdownFiles(absolute, repoRoot, files);
232
+ } catch {
233
+ // optional architecture directories may not exist
234
+ }
235
+ }
236
+ const matches = [];
237
+ for (const relative of files) {
238
+ const lower = relative.toLowerCase();
239
+ if (lower.includes(needle) || lower.includes(shortNeedle)) {
240
+ matches.push(relative);
241
+ continue;
242
+ }
243
+ try {
244
+ const content = await readFile(path.join(repoRoot, relative), 'utf8');
245
+ if (content.toLowerCase().includes(needle)) matches.push(relative);
246
+ } catch {
247
+ // ignore unreadable docs
248
+ }
249
+ }
250
+ return matches;
251
+ }
252
+
253
+ async function walkMarkdownFiles(absolute, repoRoot, collected) {
254
+ let entries = [];
255
+ try {
256
+ entries = await readdir(absolute, { withFileTypes: true });
257
+ } catch {
258
+ return;
259
+ }
260
+ for (const entry of entries) {
261
+ const next = path.join(absolute, entry.name);
262
+ if (entry.isDirectory()) {
263
+ await walkMarkdownFiles(next, repoRoot, collected);
264
+ continue;
265
+ }
266
+ if (/\.mdx?$/i.test(entry.name)) {
267
+ collected.push(path.relative(repoRoot, next).split(path.sep).join('/'));
268
+ }
269
+ }
270
+ }
271
+
272
+ function inferArchitectureEvidenceKind(content) {
273
+ const text = String(content ?? '');
274
+ const kinds = [];
275
+ if (/(IA|information architecture|情報構造|navigation|ナビゲーション|route|screen|画面)/i.test(text)) kinds.push('information_architecture');
276
+ if (/(flow|journey|画面遷移|遷移|導線|workflow|sequence)/i.test(text)) kinds.push('flow');
277
+ if (/(state|status|状態|ステート)/i.test(text)) kinds.push('state');
278
+ if (/(boundary|responsibility|責務|境界|dependency|依存)/i.test(text)) kinds.push('boundary');
279
+ return kinds.length > 0 ? kinds : ['architecture'];
280
+ }
281
+
282
+ function extractArchitectureSnippets(content) {
283
+ const headings = [
284
+ 'Intent',
285
+ 'Boundary',
286
+ 'Data Flow',
287
+ 'Information Architecture',
288
+ 'IA',
289
+ 'UI Flow',
290
+ 'Flow',
291
+ 'Journey',
292
+ 'State',
293
+ 'Responsibilities',
294
+ '責務',
295
+ '境界',
296
+ '情報構造',
297
+ '画面遷移',
298
+ '導線',
299
+ '状態'
300
+ ];
301
+ const snippets = [];
302
+ for (const heading of headings) {
303
+ const section = extractMarkdownSection(content, heading);
304
+ if (section) snippets.push({ section: heading, text: compactSnippet(section) });
305
+ if (snippets.length >= 6) break;
306
+ }
307
+ if (snippets.length === 0) snippets.push({ section: 'summary', text: compactSnippet(content) });
308
+ return snippets;
309
+ }
310
+
311
+ function extractMarkdownSection(content, heading) {
312
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
313
+ const match = String(content ?? '').match(new RegExp(`^##+\\s+.*${escaped}.*\\n([\\s\\S]*?)(?=^##+\\s+|(?![\\s\\S]))`, 'im'));
314
+ return match?.[1] ?? null;
315
+ }
316
+
317
+ function compactSnippet(value) {
318
+ return String(value ?? '')
319
+ .split('\n')
320
+ .map((line) => line.trim())
321
+ .filter((line) => line && !line.startsWith('---'))
322
+ .join(' ')
323
+ .replace(/\s+/g, ' ')
324
+ .slice(0, 700);
325
+ }
326
+
327
+ function extractCallStrings(content, regex) {
328
+ const results = [];
329
+ for (const match of content.matchAll(regex)) {
330
+ const value = match[2]?.trim();
331
+ if (value && value.length >= 2) results.push(value);
332
+ }
333
+ return [...new Set(results)].slice(0, 30);
334
+ }
335
+
336
+ function extractTestCases(content) {
337
+ const cases = [];
338
+ const nameRegex = /\b(?:it|test)\s*\(\s*(['"`])([^'"`]{1,200})\1/g;
339
+ for (const match of content.matchAll(nameRegex)) {
340
+ cases.push({ name: match[2].trim(), expects: [] });
341
+ }
342
+ const expectRegex = /\bexpect\(([^)]{1,180})\)\s*\.\s*([A-Za-z0-9_$]+)\s*\(([^)]{0,160})\)/g;
343
+ for (const match of content.matchAll(expectRegex)) {
344
+ const expectText = `expect(${match[1].trim()}).${match[2]}(${match[3].trim()})`;
345
+ if (cases.length === 0) continue;
346
+ const lastCase = cases[cases.length - 1];
347
+ if (lastCase.expects.length < 6) lastCase.expects.push(expectText);
348
+ }
349
+ const assertRegex = /\bassert(?:\.[A-Za-z0-9_$]+)?\s*\(([^)]{1,160})\)/g;
350
+ for (const match of content.matchAll(assertRegex)) {
351
+ if (cases.length === 0) continue;
352
+ const lastCase = cases[cases.length - 1];
353
+ if (lastCase.expects.length < 6) lastCase.expects.push(`assert(${match[1].trim()})`);
354
+ }
355
+ return cases;
356
+ }
357
+
358
+ async function listTestFiles(repoRoot) {
359
+ const collected = new Set();
360
+ for (const dir of TEST_GLOB_DIRS) {
361
+ const absolute = path.join(repoRoot, dir);
362
+ let stats;
363
+ try {
364
+ stats = await stat(absolute);
365
+ } catch {
366
+ continue;
367
+ }
368
+ if (!stats.isDirectory()) continue;
369
+ await walkTests(absolute, repoRoot, collected);
370
+ }
371
+ return [...collected];
372
+ }
373
+
374
+ async function walkTests(absolute, repoRoot, collected) {
375
+ let entries = [];
376
+ try {
377
+ entries = await readdir(absolute, { withFileTypes: true });
378
+ } catch {
379
+ return;
380
+ }
381
+ for (const entry of entries) {
382
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === WORKSPACE_DIR) continue;
383
+ const next = path.join(absolute, entry.name);
384
+ if (entry.isDirectory()) {
385
+ await walkTests(next, repoRoot, collected);
386
+ continue;
387
+ }
388
+ if (!TEST_EXTENSIONS.has(path.extname(entry.name))) continue;
389
+ if (!TEST_FILE_PATTERN.test(entry.name)) continue;
390
+ collected.add(path.relative(repoRoot, next).split(path.sep).join('/'));
391
+ }
392
+ }
393
+
394
+ function buildInputsDigest({ story, codeFingerprint, testFingerprint, architectureFingerprint }) {
395
+ return {
396
+ story_sha: sha256({
397
+ story_id: story?.story_id ?? null,
398
+ title: story?.title ?? null,
399
+ acceptance_criteria: story?.acceptance_criteria ?? [],
400
+ background: story?.background ?? null,
401
+ policy: story?.policy ?? null
402
+ }),
403
+ code_sha: sha256({
404
+ files: codeFingerprint.files,
405
+ branches: codeFingerprint.branches,
406
+ state_transitions: codeFingerprint.state_transitions,
407
+ external_effects: codeFingerprint.external_effects
408
+ }),
409
+ test_sha: sha256({
410
+ files: testFingerprint.files.map((file) => ({
411
+ path: file.path,
412
+ describes: file.describes,
413
+ cases: file.cases.map((entry) => entry.name)
414
+ }))
415
+ }),
416
+ architecture_sha: sha256({
417
+ files: (architectureFingerprint?.files ?? []).map((file) => ({
418
+ path: file.path,
419
+ evidence_kind: file.evidence_kind,
420
+ snippets: file.flow_state_boundary_snippets
421
+ }))
422
+ })
423
+ };
424
+ }
425
+
426
+ function findMarkdownTitle(content) {
427
+ const match = String(content ?? '').match(/^#\s+(.+)$/m);
428
+ return match?.[1]?.trim() ?? null;
429
+ }
430
+
431
+ function sha256(value) {
432
+ const hash = createHash('sha256');
433
+ hash.update(JSON.stringify(value));
434
+ return `sha256:${hash.digest('hex')}`;
435
+ }
436
+
437
+ async function readSchema() {
438
+ const schemaPath = path.join(__dirname, 'spec-schema.json');
439
+ return JSON.parse(await readFile(schemaPath, 'utf8'));
440
+ }
441
+
442
+ async function readInstructions() {
443
+ const promptPath = path.join(__dirname, 'spec-prompt-template.md');
444
+ return readFile(promptPath, 'utf8');
445
+ }
@@ -0,0 +1,155 @@
1
+ # VibePro Spec authoring instructions (for the calling AI)
2
+
3
+ You are receiving a JSON payload from `vibepro spec fingerprint`. Your job is to
4
+ emit an updated `spec.json` and pipe it back into `vibepro spec write --from-stdin`.
5
+
6
+ VibePro will validate your output against `schema_for_your_output`. If any
7
+ clause fails validation, VibePro returns a `validation_report.errors[]` and you
8
+ must regenerate only the failing clauses.
9
+
10
+ ## What a clause is
11
+
12
+ A clause is a single machine-checkable statement about the system. Four types:
13
+
14
+ - `invariant` — a property that must hold always (e.g., "premium user keeps
15
+ `userType=2` until `current_period_end`").
16
+ - `scenario` — a concrete given/when/then path (e.g., "Stripe webhook with
17
+ invalid signature returns 400 without DB write"). Derive scenario clauses
18
+ from Story acceptance criteria plus Architecture / IA / route-flow / state /
19
+ boundary evidence when available.
20
+ - `contract` — an interface obligation (e.g., "`GET /api/foo` returns 200 with
21
+ shape X").
22
+ - `sla` — a measurable bound (e.g., "p95 of /api/foo < 200ms").
23
+
24
+ ## Rules
25
+
26
+ 1. **One statement per clause.** Split compound sentences.
27
+ 2. **Every clause must cite at least one origin.** `origin.story_refs[]`,
28
+ `origin.architecture_refs[]`, `origin.code_refs[]`, or
29
+ `origin.test_refs[]` must be non-empty.
30
+ 3. **`code_refs[].file` must be a real path** in the repo (relative to repo
31
+ root). `code_refs[].anchor` must be a substring grep-findable in that file.
32
+ 4. **`verifiable_by` patterns must actually match** when run by the validator.
33
+ Prefer narrow `file_glob` and specific `must_contain` / `must_not_contain` /
34
+ `must_cover`. Do not use overly generic globs like `src/**/*`.
35
+ 5. **Reuse clause ids when possible.** If `previous_spec` contains a clause
36
+ whose statement is semantically equivalent, copy its `id`. Otherwise use
37
+ `INV-NEW-<n>` / `S-NEW-<n>` etc. — the validator will assign stable ids.
38
+ 6. **No prose blocks, no markdown.** Output strict JSON only.
39
+ 7. **Use `open_questions[]`** when Story / Code / Test conflict or the spec
40
+ cannot be determined. Do not invent clauses to paper over ambiguity.
41
+
42
+ ## Output format
43
+
44
+ ```jsonc
45
+ {
46
+ "schema_version": "0.1.0",
47
+ "story_id": "<copy from fingerprint.story_id>",
48
+ "generated_by": { "caller": "<your name, e.g. claude-code>", "stage": "ai_synthesis" },
49
+ "clauses": [
50
+ {
51
+ "id": "INV-NEW-1",
52
+ "type": "invariant",
53
+ "statement": "Premium ユーザーは current_period_end まで userType=2 を保持する",
54
+ "rationale": "Story acceptance_criteria[2] と src/billing.ts:142 cancelAtPeriodEnd 分岐から",
55
+ "origin": {
56
+ "story_refs": [{ "kind": "acceptance_criteria", "index": 2 }],
57
+ "architecture_refs": [],
58
+ "code_refs": [{ "file": "src/billing.ts", "anchor": "cancelAtPeriodEnd" }],
59
+ "test_refs": []
60
+ },
61
+ "confidence": 0.9,
62
+ "verifiable_by": {
63
+ "code_pattern": [
64
+ { "file_glob": "src/**/billing*.ts",
65
+ "must_not_contain": "userType: 1, cancelAtPeriodEnd: true" }
66
+ ],
67
+ "test_pattern": [
68
+ { "file_glob": "test/**/billing*.{test,spec}.{js,ts}",
69
+ "must_cover": "cancelAtPeriodEnd" }
70
+ ]
71
+ }
72
+ }
73
+ ],
74
+ "open_questions": []
75
+ }
76
+ ```
77
+
78
+ ## Design diagrams (MUST-HAVE, change-type-triggered)
79
+
80
+ Some change types make a design diagram mandatory. VibePro detects the trigger
81
+ and the `gate:design_diagrams` Gate blocks PR creation until the listed kinds
82
+ are present in `diagrams[]`. The 8 kinds are:
83
+
84
+ | kind | trigger | mermaid prefix |
85
+ |---|---|---|
86
+ | `er` | DB schema diff (`prisma/schema.prisma`, `db/migrations/**`, `*.sql` with CREATE/ALTER TABLE) | `erDiagram` |
87
+ | `state` | `status`/`state` enum or column, `xstate` / `state-machine` / `workflow` paths | `stateDiagram-v2` / `stateDiagram` |
88
+ | `sequence` | webhook route, queue/topic dep, 3rd party SDK (stripe/twilio/etc.) | `sequenceDiagram` |
89
+ | `flow` | multi-step user workflow (Story.AC >= 3 with checkout/onboarding/wizard keyword, or `**/checkout/**` path) | `flowchart` / `graph` |
90
+ | `c4_context` | new package boundary (`packages/<new>/package.json`), new `services/<new>/` | `C4Context` / `C4Container` |
91
+ | `deployment` | IaC diff (`*.tf`, `infra/**`, `pulumi/**`), `fly.toml`/`vercel.json`/`serverless.yml`, k8s manifest | `flowchart` / `graph` / `C4Deployment` |
92
+ | `threat_model` | auth/authz/PII/payment paths or deps (bcrypt/argon2/jose/stripe), PII column hints (email/phone/ssn/payment) | `flowchart` / `graph` |
93
+ | `dfd` | async pipeline (cron paths, stream deps: kafkajs/inngest/temporal, etl/pipeline/ingest paths) | `flowchart` / `graph` |
94
+
95
+ For `er`, `state`, `sequence`, `c4_context`, the `entities[]` field is **required and non-empty**.
96
+ Names in `entities[]` should also appear in at least one clause statement or rationale
97
+ (otherwise the validator emits a `diagram_entity_clause_mismatch` warning).
98
+
99
+ Example diagrams[] entry:
100
+
101
+ ```jsonc
102
+ "diagrams": [
103
+ {
104
+ "kind": "er",
105
+ "mermaid": "erDiagram\n USER ||--o{ SUBSCRIPTION : has\n SUBSCRIPTION { string id PK; int userType; string status }",
106
+ "entities": ["USER", "SUBSCRIPTION"],
107
+ "rationale": "schema diff at prisma/schema.prisma touches User and Subscription"
108
+ }
109
+ ]
110
+ ```
111
+
112
+ If no trigger fires, omit `diagrams[]` entirely. Do not add a diagram just because
113
+ it might be nice — only the triggered MUST-HAVE kinds should be present.
114
+
115
+ ## BDD-style scenario guidance
116
+
117
+ VibePro does not require an external BDD runner. Do not output free-form Gherkin
118
+ documents. Use `type: "scenario"` clauses as the machine-checkable BDD surface.
119
+
120
+ For each scenario clause:
121
+
122
+ - Include one concrete user/system state, one action/event, and one expected
123
+ result in `statement`.
124
+ - Prefer Story acceptance criteria as the primary origin.
125
+ - Add `origin.architecture_refs[]` when Architecture / IA / route-flow / state /
126
+ boundary docs explain the path.
127
+ - If the Story and Architecture imply a path but the expected result is
128
+ ambiguous, add a blocker `open_questions[]` item instead of inventing behavior.
129
+ - Make the clause easy to connect to tests by keeping stable `S-<n>` ids.
130
+
131
+ ## What VibePro does with your output
132
+
133
+ - Runs JSON schema validation.
134
+ - Verifies every `code_refs[].file` exists and `anchor` is grep-findable.
135
+ - Runs each `verifiable_by.code_pattern` / `test_pattern` against the actual
136
+ repo. If `must_contain` / `must_not_contain` / `must_cover` fails to match
137
+ as declared, the clause is rejected.
138
+ - Assigns stable clause ids by comparing your `statement` text to the previous
139
+ spec (text similarity > 0.7 → preserve id; first_seen_at preserved).
140
+ - Writes `.vibepro/spec/<story-id>/spec.json` and rotates history.
141
+ - Optionally runs `vibepro spec drift` to detect Spec↔Code↔Test↔PR
142
+ inconsistencies. Drift items are surfaced via Gate DAG and PR body.
143
+
144
+ ## Common mistakes
145
+
146
+ - **Citing a file that does not exist.** Always use the actual paths from
147
+ `code_fingerprint.branches[].file` or grep the repo first.
148
+ - **Vague statements.** "The system should be secure" is not a clause. Be
149
+ specific: "Stripe webhook signatures must be verified before any DB write."
150
+ - **Patterns that don't match anything.** Test your `file_glob` mentally —
151
+ does `src/**/*.ts` actually contain a file that matches `must_contain`?
152
+ - **Trying to author Story.** You are not editing Story. Story is the input.
153
+ Treat NocoDB Story acceptance criteria as immutable for this session.
154
+ - **Turning IA into Spec.** IA and route-flow evidence explain structure and
155
+ navigation. The Spec clause must still state verifiable behavior.