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,459 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const CLAUSE_TYPES = new Set(['invariant', 'scenario', 'contract', 'sla']);
|
|
5
|
+
const ORIGIN_KINDS = new Set(['acceptance_criteria', 'background', 'policy', 'frontmatter', 'other']);
|
|
6
|
+
const DIAGRAM_KINDS = new Set(['er', 'state', 'sequence', 'flow', 'c4_context', 'deployment', 'threat_model', 'dfd']);
|
|
7
|
+
const DIAGRAM_PREFIXES = {
|
|
8
|
+
er: ['erDiagram'],
|
|
9
|
+
state: ['stateDiagram-v2', 'stateDiagram'],
|
|
10
|
+
sequence: ['sequenceDiagram'],
|
|
11
|
+
flow: ['flowchart ', 'flowchart\n', 'graph ', 'graph\n'],
|
|
12
|
+
c4_context: ['C4Context', 'C4Container'],
|
|
13
|
+
deployment: ['flowchart ', 'flowchart\n', 'graph ', 'graph\n', 'C4Deployment'],
|
|
14
|
+
threat_model: ['flowchart ', 'flowchart\n', 'graph ', 'graph\n'],
|
|
15
|
+
dfd: ['flowchart ', 'flowchart\n', 'graph ', 'graph\n']
|
|
16
|
+
};
|
|
17
|
+
const DIAGRAM_KINDS_REQUIRING_ENTITIES = new Set(['er', 'state', 'sequence', 'c4_context']);
|
|
18
|
+
|
|
19
|
+
export async function validateSpec(repoRoot, spec, options = {}) {
|
|
20
|
+
const root = path.resolve(repoRoot);
|
|
21
|
+
const errors = [];
|
|
22
|
+
const warnings = [];
|
|
23
|
+
|
|
24
|
+
if (!spec || typeof spec !== 'object') {
|
|
25
|
+
return { ok: false, errors: [{ code: 'invalid_root', message: 'spec must be a JSON object' }], warnings };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (spec.schema_version !== '0.1.0') {
|
|
29
|
+
errors.push({ code: 'schema_version', message: `expected schema_version "0.1.0", got "${spec.schema_version}"` });
|
|
30
|
+
}
|
|
31
|
+
if (!spec.story_id || typeof spec.story_id !== 'string') {
|
|
32
|
+
errors.push({ code: 'story_id', message: 'story_id must be a non-empty string' });
|
|
33
|
+
}
|
|
34
|
+
if (options.expectedStoryId && spec.story_id !== options.expectedStoryId) {
|
|
35
|
+
errors.push({
|
|
36
|
+
code: 'story_id_mismatch',
|
|
37
|
+
message: `story_id "${spec.story_id}" does not match expected "${options.expectedStoryId}"`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(spec.clauses)) {
|
|
41
|
+
errors.push({ code: 'clauses_missing', message: 'clauses must be an array' });
|
|
42
|
+
return { ok: false, errors, warnings, spec };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const clauseReports = [];
|
|
46
|
+
for (let index = 0; index < spec.clauses.length; index += 1) {
|
|
47
|
+
const clause = spec.clauses[index];
|
|
48
|
+
const report = await validateClause(root, clause, index);
|
|
49
|
+
if (report.errors.length > 0) errors.push(...report.errors);
|
|
50
|
+
if (report.warnings.length > 0) warnings.push(...report.warnings);
|
|
51
|
+
clauseReports.push(report);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (spec.diagrams !== undefined) {
|
|
55
|
+
if (!Array.isArray(spec.diagrams)) {
|
|
56
|
+
errors.push({ code: 'diagrams_shape', message: 'diagrams must be an array when present' });
|
|
57
|
+
} else {
|
|
58
|
+
const clauseTexts = (spec.clauses ?? [])
|
|
59
|
+
.map((c) => `${c?.statement ?? ''}\n${c?.rationale ?? ''}`)
|
|
60
|
+
.join('\n');
|
|
61
|
+
for (let index = 0; index < spec.diagrams.length; index += 1) {
|
|
62
|
+
const diagram = spec.diagrams[index];
|
|
63
|
+
const report = validateDiagram(diagram, index, clauseTexts);
|
|
64
|
+
if (report.errors.length > 0) errors.push(...report.errors);
|
|
65
|
+
if (report.warnings.length > 0) warnings.push(...report.warnings);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(spec.open_questions)) {
|
|
71
|
+
for (let index = 0; index < spec.open_questions.length; index += 1) {
|
|
72
|
+
const question = spec.open_questions[index];
|
|
73
|
+
if (!question || typeof question !== 'object') {
|
|
74
|
+
errors.push({ code: 'open_question_shape', message: `open_questions[${index}] must be an object` });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!question.id || typeof question.id !== 'string') {
|
|
78
|
+
errors.push({ code: 'open_question_id', message: `open_questions[${index}].id must be a string` });
|
|
79
|
+
}
|
|
80
|
+
if (!question.question || typeof question.question !== 'string') {
|
|
81
|
+
errors.push({ code: 'open_question_text', message: `open_questions[${index}].question must be a string` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
ok: errors.length === 0,
|
|
88
|
+
errors,
|
|
89
|
+
warnings,
|
|
90
|
+
clause_reports: clauseReports,
|
|
91
|
+
spec
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function validateClause(repoRoot, clause, index) {
|
|
96
|
+
const errors = [];
|
|
97
|
+
const warnings = [];
|
|
98
|
+
|
|
99
|
+
if (!clause || typeof clause !== 'object') {
|
|
100
|
+
errors.push({ code: 'clause_shape', message: `clauses[${index}] must be an object` });
|
|
101
|
+
return { index, errors, warnings };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const locator = clause.id ? `clauses[${index}] (${clause.id})` : `clauses[${index}]`;
|
|
105
|
+
if (!clause.id || typeof clause.id !== 'string') {
|
|
106
|
+
errors.push({ code: 'clause_id_missing', message: `${locator}.id is required` });
|
|
107
|
+
}
|
|
108
|
+
if (!CLAUSE_TYPES.has(clause.type)) {
|
|
109
|
+
errors.push({
|
|
110
|
+
code: 'clause_type',
|
|
111
|
+
message: `${locator}.type must be one of invariant|scenario|contract|sla`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (typeof clause.statement !== 'string' || clause.statement.trim().length < 4) {
|
|
115
|
+
errors.push({ code: 'clause_statement', message: `${locator}.statement must be a non-empty string` });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const origin = clause.origin ?? {};
|
|
119
|
+
const storyRefs = Array.isArray(origin.story_refs) ? origin.story_refs : [];
|
|
120
|
+
const architectureRefs = Array.isArray(origin.architecture_refs) ? origin.architecture_refs : [];
|
|
121
|
+
const codeRefs = Array.isArray(origin.code_refs) ? origin.code_refs : [];
|
|
122
|
+
const testRefs = Array.isArray(origin.test_refs) ? origin.test_refs : [];
|
|
123
|
+
if (storyRefs.length === 0 && architectureRefs.length === 0 && codeRefs.length === 0 && testRefs.length === 0) {
|
|
124
|
+
errors.push({
|
|
125
|
+
code: 'origin_empty',
|
|
126
|
+
message: `${locator}.origin must include at least one of story_refs / architecture_refs / code_refs / test_refs`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < storyRefs.length; i += 1) {
|
|
131
|
+
const ref = storyRefs[i];
|
|
132
|
+
if (!ref || !ORIGIN_KINDS.has(ref.kind)) {
|
|
133
|
+
errors.push({
|
|
134
|
+
code: 'story_ref_kind',
|
|
135
|
+
message: `${locator}.origin.story_refs[${i}].kind must be one of ${[...ORIGIN_KINDS].join('|')}`
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < architectureRefs.length; i += 1) {
|
|
141
|
+
const ref = architectureRefs[i];
|
|
142
|
+
if (!ref?.file || typeof ref.file !== 'string') {
|
|
143
|
+
errors.push({ code: 'architecture_ref_file', message: `${locator}.origin.architecture_refs[${i}].file required` });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const fileResult = await verifyFileExists(repoRoot, ref.file);
|
|
147
|
+
if (!fileResult.exists) {
|
|
148
|
+
errors.push({
|
|
149
|
+
code: 'architecture_ref_missing',
|
|
150
|
+
message: `${locator}.origin.architecture_refs[${i}].file "${ref.file}" not found in repository`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < codeRefs.length; i += 1) {
|
|
156
|
+
const ref = codeRefs[i];
|
|
157
|
+
if (!ref?.file || typeof ref.file !== 'string') {
|
|
158
|
+
errors.push({ code: 'code_ref_file', message: `${locator}.origin.code_refs[${i}].file required` });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const fileResult = await verifyFileExists(repoRoot, ref.file);
|
|
162
|
+
if (!fileResult.exists) {
|
|
163
|
+
errors.push({
|
|
164
|
+
code: 'code_ref_missing',
|
|
165
|
+
message: `${locator}.origin.code_refs[${i}].file "${ref.file}" not found in repository`
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ref.anchor) {
|
|
170
|
+
const anchorHit = await fileIncludes(repoRoot, ref.file, ref.anchor);
|
|
171
|
+
if (!anchorHit) {
|
|
172
|
+
errors.push({
|
|
173
|
+
code: 'code_ref_anchor',
|
|
174
|
+
message: `${locator}.origin.code_refs[${i}].anchor "${ref.anchor}" not present in ${ref.file}`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < testRefs.length; i += 1) {
|
|
181
|
+
const ref = testRefs[i];
|
|
182
|
+
if (!ref?.file || typeof ref.file !== 'string') {
|
|
183
|
+
errors.push({ code: 'test_ref_file', message: `${locator}.origin.test_refs[${i}].file required` });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const fileResult = await verifyFileExists(repoRoot, ref.file);
|
|
187
|
+
if (!fileResult.exists) {
|
|
188
|
+
errors.push({
|
|
189
|
+
code: 'test_ref_missing',
|
|
190
|
+
message: `${locator}.origin.test_refs[${i}].file "${ref.file}" not found in repository`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (clause.verifiable_by) {
|
|
196
|
+
const codePatterns = Array.isArray(clause.verifiable_by.code_pattern) ? clause.verifiable_by.code_pattern : [];
|
|
197
|
+
const testPatterns = Array.isArray(clause.verifiable_by.test_pattern) ? clause.verifiable_by.test_pattern : [];
|
|
198
|
+
for (let i = 0; i < codePatterns.length; i += 1) {
|
|
199
|
+
const issues = await verifyPattern(repoRoot, codePatterns[i], { locator: `${locator}.verifiable_by.code_pattern[${i}]` });
|
|
200
|
+
for (const issue of issues) {
|
|
201
|
+
if (issue.severity === 'error') errors.push(issue);
|
|
202
|
+
else warnings.push(issue);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (let i = 0; i < testPatterns.length; i += 1) {
|
|
206
|
+
const issues = await verifyPattern(repoRoot, testPatterns[i], { locator: `${locator}.verifiable_by.test_pattern[${i}]` });
|
|
207
|
+
for (const issue of issues) {
|
|
208
|
+
if (issue.severity === 'error') errors.push(issue);
|
|
209
|
+
else warnings.push(issue);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { index, id: clause.id ?? null, errors, warnings };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function verifyPattern(repoRoot, pattern, { locator }) {
|
|
218
|
+
const issues = [];
|
|
219
|
+
if (!pattern || typeof pattern !== 'object' || typeof pattern.file_glob !== 'string') {
|
|
220
|
+
issues.push({ severity: 'error', code: 'pattern_shape', message: `${locator}.file_glob required` });
|
|
221
|
+
return issues;
|
|
222
|
+
}
|
|
223
|
+
const matched = await matchGlob(repoRoot, pattern.file_glob);
|
|
224
|
+
if (matched.length === 0) {
|
|
225
|
+
issues.push({
|
|
226
|
+
severity: 'error',
|
|
227
|
+
code: 'pattern_no_files',
|
|
228
|
+
message: `${locator}: file_glob "${pattern.file_glob}" matched no files`
|
|
229
|
+
});
|
|
230
|
+
return issues;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (pattern.must_contain) {
|
|
234
|
+
const found = await anyFileContains(repoRoot, matched, pattern.must_contain);
|
|
235
|
+
if (!found) {
|
|
236
|
+
issues.push({
|
|
237
|
+
severity: 'error',
|
|
238
|
+
code: 'pattern_must_contain',
|
|
239
|
+
message: `${locator}: must_contain "${pattern.must_contain}" not found in ${matched.length} matched file(s)`
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (pattern.must_not_contain) {
|
|
244
|
+
const offender = await firstFileContaining(repoRoot, matched, pattern.must_not_contain);
|
|
245
|
+
if (offender) {
|
|
246
|
+
issues.push({
|
|
247
|
+
severity: 'error',
|
|
248
|
+
code: 'pattern_must_not_contain',
|
|
249
|
+
message: `${locator}: must_not_contain "${pattern.must_not_contain}" found in ${offender}`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (pattern.must_cover) {
|
|
254
|
+
const found = await anyFileContains(repoRoot, matched, pattern.must_cover);
|
|
255
|
+
if (!found) {
|
|
256
|
+
issues.push({
|
|
257
|
+
severity: 'warning',
|
|
258
|
+
code: 'pattern_must_cover',
|
|
259
|
+
message: `${locator}: must_cover "${pattern.must_cover}" not found in matched files`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return issues;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function verifyFileExists(repoRoot, relativeFile) {
|
|
268
|
+
const absolute = path.join(repoRoot, relativeFile);
|
|
269
|
+
try {
|
|
270
|
+
const stats = await stat(absolute);
|
|
271
|
+
return { exists: stats.isFile(), absolute };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error.code === 'ENOENT') return { exists: false, absolute };
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function fileIncludes(repoRoot, relativeFile, anchor) {
|
|
279
|
+
try {
|
|
280
|
+
const content = await readFile(path.join(repoRoot, relativeFile), 'utf8');
|
|
281
|
+
return content.includes(anchor);
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function anyFileContains(repoRoot, files, needle) {
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
if (await fileIncludes(repoRoot, file, needle)) return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function firstFileContaining(repoRoot, files, needle) {
|
|
295
|
+
for (const file of files) {
|
|
296
|
+
if (await fileIncludes(repoRoot, file, needle)) return file;
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function matchGlob(repoRoot, glob) {
|
|
302
|
+
const root = path.resolve(repoRoot);
|
|
303
|
+
const regex = globToRegExp(glob);
|
|
304
|
+
const collected = [];
|
|
305
|
+
await walkRepo(root, root, (relative) => {
|
|
306
|
+
if (regex.test(relative)) collected.push(relative);
|
|
307
|
+
});
|
|
308
|
+
return collected;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function walkRepo(currentDir, root, visit) {
|
|
312
|
+
let entries = [];
|
|
313
|
+
try {
|
|
314
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
315
|
+
} catch {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.vibepro') continue;
|
|
320
|
+
const next = path.join(currentDir, entry.name);
|
|
321
|
+
if (entry.isDirectory()) {
|
|
322
|
+
await walkRepo(next, root, visit);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const relative = path.relative(root, next).split(path.sep).join('/');
|
|
326
|
+
visit(relative);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function globToRegExp(glob) {
|
|
331
|
+
let pattern = '';
|
|
332
|
+
let i = 0;
|
|
333
|
+
while (i < glob.length) {
|
|
334
|
+
const char = glob[i];
|
|
335
|
+
if (char === '*') {
|
|
336
|
+
if (glob[i + 1] === '*') {
|
|
337
|
+
if (glob[i + 2] === '/') {
|
|
338
|
+
pattern += '(?:.*/)?';
|
|
339
|
+
i += 3;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
pattern += '.*';
|
|
343
|
+
i += 2;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
pattern += '[^/]*';
|
|
347
|
+
i += 1;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (char === '?') {
|
|
351
|
+
pattern += '[^/]';
|
|
352
|
+
i += 1;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (char === '{') {
|
|
356
|
+
const end = glob.indexOf('}', i);
|
|
357
|
+
if (end === -1) {
|
|
358
|
+
pattern += '\\{';
|
|
359
|
+
i += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const options = glob.slice(i + 1, end).split(',').map((opt) => escapeRegExp(opt));
|
|
363
|
+
pattern += `(?:${options.join('|')})`;
|
|
364
|
+
i = end + 1;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if ('.+^$()|[]\\'.includes(char)) {
|
|
368
|
+
pattern += `\\${char}`;
|
|
369
|
+
i += 1;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
pattern += char;
|
|
373
|
+
i += 1;
|
|
374
|
+
}
|
|
375
|
+
return new RegExp(`^${pattern}$`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function escapeRegExp(value) {
|
|
379
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function validateDiagram(diagram, index, clauseTexts) {
|
|
383
|
+
const errors = [];
|
|
384
|
+
const warnings = [];
|
|
385
|
+
const locator = `diagrams[${index}]`;
|
|
386
|
+
|
|
387
|
+
if (!diagram || typeof diagram !== 'object') {
|
|
388
|
+
errors.push({ code: 'diagram_shape', message: `${locator} must be an object` });
|
|
389
|
+
return { errors, warnings };
|
|
390
|
+
}
|
|
391
|
+
if (!DIAGRAM_KINDS.has(diagram.kind)) {
|
|
392
|
+
errors.push({
|
|
393
|
+
code: 'diagram_kind',
|
|
394
|
+
message: `${locator}.kind must be one of ${[...DIAGRAM_KINDS].join(', ')}`
|
|
395
|
+
});
|
|
396
|
+
return { errors, warnings };
|
|
397
|
+
}
|
|
398
|
+
if (typeof diagram.mermaid !== 'string' || diagram.mermaid.trim().length === 0) {
|
|
399
|
+
errors.push({ code: 'diagram_mermaid_missing', message: `${locator}.mermaid must be a non-empty string` });
|
|
400
|
+
return { errors, warnings };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const firstLine = diagram.mermaid.split(/\r?\n/).find((line) => line.trim().length > 0) ?? '';
|
|
404
|
+
const trimmed = firstLine.trimStart();
|
|
405
|
+
const prefixes = DIAGRAM_PREFIXES[diagram.kind] ?? [];
|
|
406
|
+
const matchesPrefix = prefixes.some((prefix) => trimmed.startsWith(prefix.trim()));
|
|
407
|
+
if (!matchesPrefix) {
|
|
408
|
+
errors.push({
|
|
409
|
+
code: 'diagram_mermaid_prefix',
|
|
410
|
+
message: `${locator}.mermaid first line must start with one of: ${prefixes.map((p) => p.trim()).join(' | ')} (got "${trimmed.slice(0, 40)}")`
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (DIAGRAM_KINDS_REQUIRING_ENTITIES.has(diagram.kind)) {
|
|
415
|
+
if (!Array.isArray(diagram.entities) || diagram.entities.length === 0) {
|
|
416
|
+
errors.push({
|
|
417
|
+
code: 'diagram_entities_required',
|
|
418
|
+
message: `${locator}.entities is required and non-empty for kind=${diagram.kind}`
|
|
419
|
+
});
|
|
420
|
+
} else if (clauseTexts) {
|
|
421
|
+
for (const entity of diagram.entities) {
|
|
422
|
+
if (typeof entity !== 'string' || entity.length === 0) continue;
|
|
423
|
+
if (!clauseTexts.includes(entity)) {
|
|
424
|
+
warnings.push({
|
|
425
|
+
code: 'diagram_entity_clause_mismatch',
|
|
426
|
+
message: `${locator}.entities entry "${entity}" does not appear in any clause statement or rationale`
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { errors, warnings };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Compute the design_diagrams gate verdict by comparing required vs provided.
|
|
437
|
+
// Pure function — no I/O.
|
|
438
|
+
export function evaluateDesignDiagramsGate({ required_diagrams = [], reasons = [], spec } = {}) {
|
|
439
|
+
const providedKinds = new Set(
|
|
440
|
+
Array.isArray(spec?.diagrams)
|
|
441
|
+
? spec.diagrams.map((d) => d?.kind).filter((k) => DIAGRAM_KINDS.has(k))
|
|
442
|
+
: []
|
|
443
|
+
);
|
|
444
|
+
const missing = required_diagrams.filter((kind) => !providedKinds.has(kind));
|
|
445
|
+
let status;
|
|
446
|
+
if (required_diagrams.length === 0) status = 'not_applicable';
|
|
447
|
+
else if (missing.length === 0) status = 'pass';
|
|
448
|
+
else status = 'blocked';
|
|
449
|
+
return {
|
|
450
|
+
id: 'gate:design_diagrams',
|
|
451
|
+
label: 'Design Diagrams (MUST-HAVE)',
|
|
452
|
+
blocking: true,
|
|
453
|
+
status,
|
|
454
|
+
required: [...required_diagrams],
|
|
455
|
+
provided: [...providedKinds],
|
|
456
|
+
missing,
|
|
457
|
+
reasons
|
|
458
|
+
};
|
|
459
|
+
}
|