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,219 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://vibepro.local/schemas/spec.json",
|
|
4
|
+
"title": "VibePro Inferred Spec",
|
|
5
|
+
"description": "AI-authored spec produced from Story+Code+Test fingerprints. VibePro CLI validates and stores this artifact; humans do not edit it directly.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema_version", "story_id", "clauses"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"const": "0.1.0"
|
|
12
|
+
},
|
|
13
|
+
"story_id": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1
|
|
16
|
+
},
|
|
17
|
+
"generated_at": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "ISO 8601 timestamp. Filled by validator if absent."
|
|
20
|
+
},
|
|
21
|
+
"generated_by": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"caller": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "claude-code | codex | other identifier"
|
|
27
|
+
},
|
|
28
|
+
"stage": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"enum": ["ai_synthesis", "static_fallback"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"inputs_digest": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"story_sha": { "type": "string" },
|
|
38
|
+
"code_sha": { "type": "string" },
|
|
39
|
+
"test_sha": { "type": "string" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"previous_spec_id": {
|
|
43
|
+
"type": ["string", "null"]
|
|
44
|
+
},
|
|
45
|
+
"clauses": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": { "$ref": "#/$defs/clause" }
|
|
48
|
+
},
|
|
49
|
+
"diagrams": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"description": "Design diagrams required by the change type. See diagram-requirement-resolver for which kinds are mandatory.",
|
|
52
|
+
"items": { "$ref": "#/$defs/diagram" }
|
|
53
|
+
},
|
|
54
|
+
"open_questions": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": ["id", "question"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"id": { "type": "string" },
|
|
61
|
+
"question": { "type": "string", "minLength": 1 },
|
|
62
|
+
"blocker": { "type": "boolean" }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"$defs": {
|
|
68
|
+
"clause": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"required": ["id", "type", "statement", "origin"],
|
|
71
|
+
"properties": {
|
|
72
|
+
"id": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Clause id. Use INV-<n>, S-<n>, C-<n>, SLA-<n>. New clauses may use INV-NEW-<n> placeholders; validator assigns stable ids."
|
|
75
|
+
},
|
|
76
|
+
"type": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["invariant", "scenario", "contract", "sla"]
|
|
79
|
+
},
|
|
80
|
+
"statement": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"minLength": 4,
|
|
83
|
+
"description": "One-sentence machine-checkable statement. Avoid ambiguity."
|
|
84
|
+
},
|
|
85
|
+
"rationale": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Why this clause exists. Cite story acceptance criterion / code anchor."
|
|
88
|
+
},
|
|
89
|
+
"origin": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"description": "Where this clause was derived from. At least one of story_refs / architecture_refs / code_refs / test_refs must be non-empty.",
|
|
92
|
+
"properties": {
|
|
93
|
+
"story_refs": {
|
|
94
|
+
"type": "array",
|
|
95
|
+
"items": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"required": ["kind"],
|
|
98
|
+
"properties": {
|
|
99
|
+
"kind": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"enum": ["acceptance_criteria", "background", "policy", "frontmatter", "other"]
|
|
102
|
+
},
|
|
103
|
+
"index": { "type": "integer", "minimum": 0 },
|
|
104
|
+
"text_snippet": { "type": "string" }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"architecture_refs": {
|
|
109
|
+
"type": "array",
|
|
110
|
+
"description": "Architecture, IA, route-flow, state, or boundary evidence that shaped this clause.",
|
|
111
|
+
"items": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"required": ["file"],
|
|
114
|
+
"properties": {
|
|
115
|
+
"file": { "type": "string", "minLength": 1 },
|
|
116
|
+
"section": { "type": "string" },
|
|
117
|
+
"text_snippet": { "type": "string" }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"code_refs": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"required": ["file"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"file": { "type": "string", "minLength": 1 },
|
|
128
|
+
"anchor": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": "Identifier or substring that should appear in the file."
|
|
131
|
+
},
|
|
132
|
+
"line": { "type": "integer", "minimum": 1 }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"test_refs": {
|
|
137
|
+
"type": "array",
|
|
138
|
+
"items": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"required": ["file"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"file": { "type": "string", "minLength": 1 },
|
|
143
|
+
"case": { "type": "string" }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"confidence": {
|
|
150
|
+
"type": "number",
|
|
151
|
+
"minimum": 0,
|
|
152
|
+
"maximum": 1
|
|
153
|
+
},
|
|
154
|
+
"verifiable_by": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"description": "Deterministic patterns the drift detector uses. Optional but recommended.",
|
|
157
|
+
"properties": {
|
|
158
|
+
"code_pattern": {
|
|
159
|
+
"type": "array",
|
|
160
|
+
"items": { "$ref": "#/$defs/pattern" }
|
|
161
|
+
},
|
|
162
|
+
"test_pattern": {
|
|
163
|
+
"type": "array",
|
|
164
|
+
"items": { "$ref": "#/$defs/pattern" }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"first_seen_at": { "type": "string" },
|
|
169
|
+
"last_revised_at": { "type": "string" }
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"diagram": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"required": ["kind", "mermaid"],
|
|
175
|
+
"properties": {
|
|
176
|
+
"kind": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"enum": ["er", "state", "sequence", "flow", "c4_context", "deployment", "threat_model", "dfd"]
|
|
179
|
+
},
|
|
180
|
+
"mermaid": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"minLength": 1,
|
|
183
|
+
"description": "Mermaid source. First non-empty line must match the kind-specific prefix."
|
|
184
|
+
},
|
|
185
|
+
"entities": {
|
|
186
|
+
"type": "array",
|
|
187
|
+
"description": "Names that appear in the diagram. Required non-empty for er/state/sequence/c4_context.",
|
|
188
|
+
"items": { "type": "string", "minLength": 1 }
|
|
189
|
+
},
|
|
190
|
+
"rationale": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "Why this diagram exists. Cite story acceptance criterion / code anchor."
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"pattern": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"required": ["file_glob"],
|
|
199
|
+
"properties": {
|
|
200
|
+
"file_glob": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "Glob relative to repo root. Examples: src/**/billing*.ts, test/**/*.test.{js,ts}"
|
|
203
|
+
},
|
|
204
|
+
"must_contain": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "Substring (case-sensitive) that must appear in at least one matched file."
|
|
207
|
+
},
|
|
208
|
+
"must_not_contain": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"description": "Substring (case-sensitive) that must not appear in any matched file."
|
|
211
|
+
},
|
|
212
|
+
"must_cover": {
|
|
213
|
+
"type": "string",
|
|
214
|
+
"description": "Identifier or substring that must appear in test names or assertion text across matched files."
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { WORKSPACE_DIR } from './workspace.js';
|
|
5
|
+
|
|
6
|
+
export const SPEC_SCHEMA_VERSION = '0.1.0';
|
|
7
|
+
const HISTORY_KEEP = 10;
|
|
8
|
+
const SIMILARITY_THRESHOLD = 0.7;
|
|
9
|
+
|
|
10
|
+
export function getSpecDir(repoRoot, storyId) {
|
|
11
|
+
if (!storyId) throw new Error('storyId is required');
|
|
12
|
+
return path.join(path.resolve(repoRoot), WORKSPACE_DIR, 'spec', storyId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getSpecFile(repoRoot, storyId) {
|
|
16
|
+
return path.join(getSpecDir(repoRoot, storyId), 'spec.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDriftFile(repoRoot, storyId) {
|
|
20
|
+
return path.join(getSpecDir(repoRoot, storyId), 'drift.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDriftMarkdownFile(repoRoot, storyId) {
|
|
24
|
+
return path.join(getSpecDir(repoRoot, storyId), 'drift.md');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getSuppressionsFile(repoRoot, storyId) {
|
|
28
|
+
return path.join(getSpecDir(repoRoot, storyId), 'suppressions.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function ensureSpecDir(repoRoot, storyId) {
|
|
32
|
+
const dir = getSpecDir(repoRoot, storyId);
|
|
33
|
+
await mkdir(path.join(dir, 'spec.history'), { recursive: true });
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function readInferredSpec(repoRoot, storyId) {
|
|
38
|
+
if (!storyId) return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(await readFile(getSpecFile(repoRoot, storyId), 'utf8'));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code === 'ENOENT') return null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function readDrift(repoRoot, storyId) {
|
|
48
|
+
if (!storyId) return null;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(await readFile(getDriftFile(repoRoot, storyId), 'utf8'));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code === 'ENOENT') return null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function readSuppressions(repoRoot, storyId) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(await readFile(getSuppressionsFile(repoRoot, storyId), 'utf8'));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT') return { schema_version: SPEC_SCHEMA_VERSION, items: [] };
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function writeInferredSpec(repoRoot, storyId, spec) {
|
|
67
|
+
await ensureSpecDir(repoRoot, storyId);
|
|
68
|
+
const specPath = getSpecFile(repoRoot, storyId);
|
|
69
|
+
await writeFile(specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
70
|
+
|
|
71
|
+
const historyDir = path.join(getSpecDir(repoRoot, storyId), 'spec.history');
|
|
72
|
+
const stamp = spec.generated_at?.replace(/[:.]/g, '-') ?? new Date().toISOString().replace(/[:.]/g, '-');
|
|
73
|
+
const historyPath = path.join(historyDir, `spec-${stamp}.json`);
|
|
74
|
+
await writeFile(historyPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
75
|
+
await pruneHistory(historyDir);
|
|
76
|
+
return specPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function writeDrift(repoRoot, storyId, drift) {
|
|
80
|
+
await ensureSpecDir(repoRoot, storyId);
|
|
81
|
+
const driftPath = getDriftFile(repoRoot, storyId);
|
|
82
|
+
await writeFile(driftPath, `${JSON.stringify(drift, null, 2)}\n`);
|
|
83
|
+
return driftPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeDriftMarkdown(repoRoot, storyId, markdown) {
|
|
87
|
+
await ensureSpecDir(repoRoot, storyId);
|
|
88
|
+
const driftPath = getDriftMarkdownFile(repoRoot, storyId);
|
|
89
|
+
await writeFile(driftPath, markdown.endsWith('\n') ? markdown : `${markdown}\n`);
|
|
90
|
+
return driftPath;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function writeSuppressions(repoRoot, storyId, suppressions) {
|
|
94
|
+
await ensureSpecDir(repoRoot, storyId);
|
|
95
|
+
const target = getSuppressionsFile(repoRoot, storyId);
|
|
96
|
+
await writeFile(target, `${JSON.stringify(suppressions, null, 2)}\n`);
|
|
97
|
+
return target;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function pruneHistory(historyDir) {
|
|
101
|
+
let entries;
|
|
102
|
+
try {
|
|
103
|
+
entries = await readdir(historyDir);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code === 'ENOENT') return;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const sorted = entries
|
|
109
|
+
.filter((name) => name.startsWith('spec-') && name.endsWith('.json'))
|
|
110
|
+
.sort()
|
|
111
|
+
.reverse();
|
|
112
|
+
const stale = sorted.slice(HISTORY_KEEP);
|
|
113
|
+
if (stale.length === 0) return;
|
|
114
|
+
const { rm } = await import('node:fs/promises');
|
|
115
|
+
await Promise.all(stale.map((name) => rm(path.join(historyDir, name), { force: true })));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function stabilizeClauseIds(newSpec, previousSpec) {
|
|
119
|
+
if (!previousSpec || !Array.isArray(previousSpec.clauses)) {
|
|
120
|
+
return assignFreshIds(newSpec);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const usedPreviousIds = new Set();
|
|
124
|
+
const usedNewIds = new Set();
|
|
125
|
+
const issuedAt = new Date().toISOString();
|
|
126
|
+
|
|
127
|
+
const claimMatch = (clause) => {
|
|
128
|
+
let best = null;
|
|
129
|
+
for (const prev of previousSpec.clauses) {
|
|
130
|
+
if (usedPreviousIds.has(prev.id)) continue;
|
|
131
|
+
if (prev.type !== clause.type) continue;
|
|
132
|
+
const score = similarity(clause.statement, prev.statement);
|
|
133
|
+
if (!best || score > best.score) best = { clause: prev, score };
|
|
134
|
+
}
|
|
135
|
+
if (best && best.score >= SIMILARITY_THRESHOLD) {
|
|
136
|
+
usedPreviousIds.add(best.clause.id);
|
|
137
|
+
return best.clause;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const clauses = newSpec.clauses.map((clause) => {
|
|
143
|
+
const match = claimMatch(clause);
|
|
144
|
+
if (match) {
|
|
145
|
+
usedNewIds.add(match.id);
|
|
146
|
+
return {
|
|
147
|
+
...clause,
|
|
148
|
+
id: match.id,
|
|
149
|
+
first_seen_at: match.first_seen_at ?? issuedAt,
|
|
150
|
+
last_revised_at: issuedAt
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...clause,
|
|
155
|
+
id: nextFreshId(clause, usedNewIds, previousSpec, issuedAt),
|
|
156
|
+
first_seen_at: issuedAt,
|
|
157
|
+
last_revised_at: issuedAt
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return { ...newSpec, clauses };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function assignFreshIds(spec) {
|
|
165
|
+
const usedIds = new Set();
|
|
166
|
+
const issuedAt = new Date().toISOString();
|
|
167
|
+
const clauses = (spec.clauses ?? []).map((clause) => ({
|
|
168
|
+
...clause,
|
|
169
|
+
id: nextFreshId(clause, usedIds, null, issuedAt),
|
|
170
|
+
first_seen_at: clause.first_seen_at ?? issuedAt,
|
|
171
|
+
last_revised_at: issuedAt
|
|
172
|
+
}));
|
|
173
|
+
return { ...spec, clauses };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function nextFreshId(clause, usedIds, previousSpec, issuedAt) {
|
|
177
|
+
const prefix = clausePrefix(clause.type);
|
|
178
|
+
const reserved = new Set(usedIds);
|
|
179
|
+
if (previousSpec && Array.isArray(previousSpec.clauses)) {
|
|
180
|
+
for (const prev of previousSpec.clauses) reserved.add(prev.id);
|
|
181
|
+
}
|
|
182
|
+
let n = 1;
|
|
183
|
+
while (reserved.has(`${prefix}-${pad3(n)}`)) n += 1;
|
|
184
|
+
const id = `${prefix}-${pad3(n)}`;
|
|
185
|
+
usedIds.add(id);
|
|
186
|
+
return id;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function clausePrefix(type) {
|
|
190
|
+
switch (type) {
|
|
191
|
+
case 'invariant': return 'INV';
|
|
192
|
+
case 'scenario': return 'S';
|
|
193
|
+
case 'contract': return 'C';
|
|
194
|
+
case 'sla': return 'SLA';
|
|
195
|
+
default: return 'X';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function pad3(n) {
|
|
200
|
+
return String(n).padStart(3, '0');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function similarity(a, b) {
|
|
204
|
+
const sa = normalizeStatement(a);
|
|
205
|
+
const sb = normalizeStatement(b);
|
|
206
|
+
if (!sa || !sb) return 0;
|
|
207
|
+
if (sa === sb) return 1;
|
|
208
|
+
const tokenSim = jaccardTokens(sa, sb);
|
|
209
|
+
const editSim = 1 - levenshteinNormalized(sa, sb);
|
|
210
|
+
return tokenSim * 0.6 + editSim * 0.4;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeStatement(text) {
|
|
214
|
+
if (typeof text !== 'string') return '';
|
|
215
|
+
return text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function jaccardTokens(a, b) {
|
|
219
|
+
const ta = new Set(tokenize(a));
|
|
220
|
+
const tb = new Set(tokenize(b));
|
|
221
|
+
if (ta.size === 0 && tb.size === 0) return 1;
|
|
222
|
+
let intersection = 0;
|
|
223
|
+
for (const t of ta) if (tb.has(t)) intersection += 1;
|
|
224
|
+
const union = ta.size + tb.size - intersection;
|
|
225
|
+
return union === 0 ? 0 : intersection / union;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function tokenize(text) {
|
|
229
|
+
return text
|
|
230
|
+
.replace(/[\p{P}\p{S}]/gu, ' ')
|
|
231
|
+
.split(/\s+/)
|
|
232
|
+
.filter((token) => token.length >= 2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function levenshteinNormalized(a, b) {
|
|
236
|
+
const max = Math.max(a.length, b.length);
|
|
237
|
+
if (max === 0) return 0;
|
|
238
|
+
return levenshtein(a, b) / max;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function levenshtein(a, b) {
|
|
242
|
+
const m = a.length;
|
|
243
|
+
const n = b.length;
|
|
244
|
+
if (m === 0) return n;
|
|
245
|
+
if (n === 0) return m;
|
|
246
|
+
let prev = new Array(n + 1);
|
|
247
|
+
let curr = new Array(n + 1);
|
|
248
|
+
for (let j = 0; j <= n; j += 1) prev[j] = j;
|
|
249
|
+
for (let i = 1; i <= m; i += 1) {
|
|
250
|
+
curr[0] = i;
|
|
251
|
+
for (let j = 1; j <= n; j += 1) {
|
|
252
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
253
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
254
|
+
}
|
|
255
|
+
[prev, curr] = [curr, prev];
|
|
256
|
+
}
|
|
257
|
+
return prev[n];
|
|
258
|
+
}
|