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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- 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.
|