@rengler33/prov 0.1.1
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/README.md +314 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +381 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agent.d.ts +107 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +197 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agent.test.d.ts +5 -0
- package/dist/commands/agent.test.d.ts.map +1 -0
- package/dist/commands/agent.test.js +199 -0
- package/dist/commands/agent.test.js.map +1 -0
- package/dist/commands/constraint.d.ts +100 -0
- package/dist/commands/constraint.d.ts.map +1 -0
- package/dist/commands/constraint.js +763 -0
- package/dist/commands/constraint.js.map +1 -0
- package/dist/commands/constraint.test.d.ts +9 -0
- package/dist/commands/constraint.test.d.ts.map +1 -0
- package/dist/commands/constraint.test.js +470 -0
- package/dist/commands/constraint.test.js.map +1 -0
- package/dist/commands/graph.d.ts +99 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +552 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/graph.test.d.ts +2 -0
- package/dist/commands/graph.test.d.ts.map +1 -0
- package/dist/commands/graph.test.js +258 -0
- package/dist/commands/graph.test.js.map +1 -0
- package/dist/commands/impact.d.ts +83 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +319 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/impact.test.d.ts +2 -0
- package/dist/commands/impact.test.d.ts.map +1 -0
- package/dist/commands/impact.test.js +234 -0
- package/dist/commands/impact.test.js.map +1 -0
- package/dist/commands/init.d.ts +45 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +7 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +174 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/integration.test.d.ts +10 -0
- package/dist/commands/integration.test.d.ts.map +1 -0
- package/dist/commands/integration.test.js +456 -0
- package/dist/commands/integration.test.js.map +1 -0
- package/dist/commands/mcp.d.ts +21 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +616 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/mcp.test.d.ts +7 -0
- package/dist/commands/mcp.test.d.ts.map +1 -0
- package/dist/commands/mcp.test.js +132 -0
- package/dist/commands/mcp.test.js.map +1 -0
- package/dist/commands/plan.d.ts +218 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +1307 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/plan.test.d.ts +9 -0
- package/dist/commands/plan.test.d.ts.map +1 -0
- package/dist/commands/plan.test.js +569 -0
- package/dist/commands/plan.test.js.map +1 -0
- package/dist/commands/spec.d.ts +94 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +635 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/spec.test.d.ts +9 -0
- package/dist/commands/spec.test.d.ts.map +1 -0
- package/dist/commands/spec.test.js +407 -0
- package/dist/commands/spec.test.js.map +1 -0
- package/dist/commands/trace.d.ts +157 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +847 -0
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/trace.test.d.ts +9 -0
- package/dist/commands/trace.test.d.ts.map +1 -0
- package/dist/commands/trace.test.js +524 -0
- package/dist/commands/trace.test.js.map +1 -0
- package/dist/graph.d.ts +204 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +496 -0
- package/dist/graph.js.map +1 -0
- package/dist/graph.test.d.ts +2 -0
- package/dist/graph.test.d.ts.map +1 -0
- package/dist/graph.test.js +382 -0
- package/dist/graph.test.js.map +1 -0
- package/dist/hash.d.ts +72 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +137 -0
- package/dist/hash.js.map +1 -0
- package/dist/hash.test.d.ts +2 -0
- package/dist/hash.test.d.ts.map +1 -0
- package/dist/hash.test.js +227 -0
- package/dist/hash.test.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +11 -0
- package/dist/index.test.js.map +1 -0
- package/dist/output.d.ts +84 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +175 -0
- package/dist/output.js.map +1 -0
- package/dist/output.test.d.ts +7 -0
- package/dist/output.test.d.ts.map +1 -0
- package/dist/output.test.js +146 -0
- package/dist/output.test.js.map +1 -0
- package/dist/staleness.d.ts +162 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +309 -0
- package/dist/staleness.js.map +1 -0
- package/dist/staleness.test.d.ts +2 -0
- package/dist/staleness.test.d.ts.map +1 -0
- package/dist/staleness.test.js +448 -0
- package/dist/staleness.test.js.map +1 -0
- package/dist/storage.d.ts +267 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +623 -0
- package/dist/storage.js.map +1 -0
- package/dist/storage.test.d.ts +5 -0
- package/dist/storage.test.d.ts.map +1 -0
- package/dist/storage.test.js +434 -0
- package/dist/storage.test.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/types.test.d.ts +2 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +232 -0
- package/dist/types.test.js.map +1 -0
- package/dist/watcher.d.ts +139 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +406 -0
- package/dist/watcher.js.map +1 -0
- package/dist/watcher.test.d.ts +5 -0
- package/dist/watcher.test.d.ts.map +1 -0
- package/dist/watcher.test.js +327 -0
- package/dist/watcher.test.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prov constraint commands implementation.
|
|
3
|
+
*
|
|
4
|
+
* Commands for managing constraints:
|
|
5
|
+
* - constraint add: Add a constraint to tracking
|
|
6
|
+
* - constraint list: List tracked constraints
|
|
7
|
+
* - constraint check: Run constraint verification
|
|
8
|
+
*
|
|
9
|
+
* @see req:cli:constraint-add
|
|
10
|
+
* @see req:cli:constraint-list
|
|
11
|
+
* @see req:cli:constraint-check
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { join, resolve, extname } from 'node:path';
|
|
16
|
+
import { isInitialized, loadGraph, saveGraph } from '../storage.js';
|
|
17
|
+
import { addConstraintToGraph } from '../graph.js';
|
|
18
|
+
import { parseYaml, computeHash } from '../hash.js';
|
|
19
|
+
import { output, error, success, warn, resolveFormat } from '../output.js';
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Validation Helpers
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Validate constraint ID format: constraint:{name}:v{number}
|
|
25
|
+
*/
|
|
26
|
+
function isValidConstraintId(id) {
|
|
27
|
+
if (typeof id !== 'string')
|
|
28
|
+
return false;
|
|
29
|
+
return /^constraint:[a-z0-9-]+:v\d+$/.test(id);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate invariant ID format: inv:{constraint}:{name}
|
|
33
|
+
*/
|
|
34
|
+
function isValidInvariantId(id) {
|
|
35
|
+
if (typeof id !== 'string')
|
|
36
|
+
return false;
|
|
37
|
+
return /^inv:[a-z0-9-]+:[a-z0-9-]+$/.test(id);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate entity status.
|
|
41
|
+
*/
|
|
42
|
+
function isValidStatus(status) {
|
|
43
|
+
return status === 'draft' || status === 'active' || status === 'deprecated' || status === 'archived';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate verification type.
|
|
47
|
+
*/
|
|
48
|
+
function isValidVerificationType(type) {
|
|
49
|
+
return type === 'command' || type === 'test' || type === 'assertion' || type === 'manual';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate a raw verification object.
|
|
53
|
+
*/
|
|
54
|
+
function validateVerification(raw, invId) {
|
|
55
|
+
const errors = [];
|
|
56
|
+
if (raw.type === undefined || raw.type === null) {
|
|
57
|
+
errors.push(`Invariant ${invId}: verification missing required field: type`);
|
|
58
|
+
}
|
|
59
|
+
else if (!isValidVerificationType(raw.type)) {
|
|
60
|
+
errors.push(`Invariant ${invId}: invalid verification type: ${String(raw.type)} (expected command|test|assertion|manual)`);
|
|
61
|
+
}
|
|
62
|
+
if (raw.value === undefined || raw.value === null) {
|
|
63
|
+
errors.push(`Invariant ${invId}: verification missing required field: value`);
|
|
64
|
+
}
|
|
65
|
+
else if (typeof raw.value !== 'string') {
|
|
66
|
+
errors.push(`Invariant ${invId}: verification value must be a string`);
|
|
67
|
+
}
|
|
68
|
+
if (raw.expect !== undefined && typeof raw.expect !== 'string') {
|
|
69
|
+
errors.push(`Invariant ${invId}: verification expect must be a string`);
|
|
70
|
+
}
|
|
71
|
+
if (errors.length > 0) {
|
|
72
|
+
return { errors };
|
|
73
|
+
}
|
|
74
|
+
const verification = {
|
|
75
|
+
type: raw.type,
|
|
76
|
+
value: raw.value,
|
|
77
|
+
...(raw.expect !== undefined ? { expect: raw.expect } : {}),
|
|
78
|
+
};
|
|
79
|
+
return { verification, errors: [] };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate a raw constraint object and convert to typed Constraint.
|
|
83
|
+
*/
|
|
84
|
+
function validateConstraint(raw, _filePath) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
// Required fields
|
|
87
|
+
if (raw.id === undefined || raw.id === null) {
|
|
88
|
+
errors.push('Missing required field: id');
|
|
89
|
+
}
|
|
90
|
+
else if (!isValidConstraintId(raw.id)) {
|
|
91
|
+
errors.push(`Invalid constraint ID format: ${String(raw.id)} (expected constraint:{name}:v{number})`);
|
|
92
|
+
}
|
|
93
|
+
if (raw.version === undefined || raw.version === null) {
|
|
94
|
+
errors.push('Missing required field: version');
|
|
95
|
+
}
|
|
96
|
+
else if (typeof raw.version !== 'string') {
|
|
97
|
+
errors.push(`Invalid version type: expected string, got ${typeof raw.version}`);
|
|
98
|
+
}
|
|
99
|
+
if (raw.title === undefined || raw.title === null) {
|
|
100
|
+
errors.push('Missing required field: title');
|
|
101
|
+
}
|
|
102
|
+
else if (typeof raw.title !== 'string') {
|
|
103
|
+
errors.push(`Invalid title type: expected string, got ${typeof raw.title}`);
|
|
104
|
+
}
|
|
105
|
+
if (raw.status !== undefined && !isValidStatus(raw.status)) {
|
|
106
|
+
errors.push(`Invalid status: ${String(raw.status)} (expected draft|active|deprecated|archived)`);
|
|
107
|
+
}
|
|
108
|
+
if (raw.description === undefined || raw.description === null) {
|
|
109
|
+
errors.push('Missing required field: description');
|
|
110
|
+
}
|
|
111
|
+
else if (typeof raw.description !== 'string') {
|
|
112
|
+
errors.push(`Invalid description type: expected string, got ${typeof raw.description}`);
|
|
113
|
+
}
|
|
114
|
+
if (raw.invariants === undefined || raw.invariants === null) {
|
|
115
|
+
errors.push('Missing required field: invariants');
|
|
116
|
+
}
|
|
117
|
+
else if (!Array.isArray(raw.invariants)) {
|
|
118
|
+
errors.push(`Invalid invariants type: expected array, got ${typeof raw.invariants}`);
|
|
119
|
+
}
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
return { errors };
|
|
122
|
+
}
|
|
123
|
+
// Validate invariants
|
|
124
|
+
const invariants = [];
|
|
125
|
+
const invIds = new Set();
|
|
126
|
+
for (let i = 0; i < raw.invariants.length; i++) {
|
|
127
|
+
const rawInv = raw.invariants[i];
|
|
128
|
+
if (rawInv.id === undefined || rawInv.id === null) {
|
|
129
|
+
errors.push(`Invariant ${i}: missing required field: id`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!isValidInvariantId(rawInv.id)) {
|
|
133
|
+
errors.push(`Invariant ${i}: invalid ID format: ${String(rawInv.id)} (expected inv:{constraint}:{name})`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const invIdStr = rawInv.id;
|
|
137
|
+
if (invIds.has(invIdStr)) {
|
|
138
|
+
errors.push(`Invariant ${i}: duplicate ID: ${invIdStr}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
invIds.add(invIdStr);
|
|
142
|
+
if (rawInv.rule === undefined || typeof rawInv.rule !== 'string') {
|
|
143
|
+
errors.push(`Invariant ${invIdStr}: missing or invalid rule`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (rawInv.verification === undefined || typeof rawInv.verification !== 'object') {
|
|
147
|
+
errors.push(`Invariant ${invIdStr}: missing or invalid verification`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const { verification, errors: verifyErrors } = validateVerification(rawInv.verification, invIdStr);
|
|
151
|
+
if (verifyErrors.length > 0) {
|
|
152
|
+
errors.push(...verifyErrors);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const inv = {
|
|
156
|
+
id: invIdStr,
|
|
157
|
+
rule: rawInv.rule,
|
|
158
|
+
verification: verification,
|
|
159
|
+
...(typeof rawInv.blocking === 'boolean' ? { blocking: rawInv.blocking } : {}),
|
|
160
|
+
};
|
|
161
|
+
invariants.push(inv);
|
|
162
|
+
}
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
return { errors };
|
|
165
|
+
}
|
|
166
|
+
const constraint = {
|
|
167
|
+
id: raw.id,
|
|
168
|
+
version: raw.version,
|
|
169
|
+
title: raw.title,
|
|
170
|
+
status: raw.status ?? 'draft',
|
|
171
|
+
description: raw.description,
|
|
172
|
+
invariants,
|
|
173
|
+
};
|
|
174
|
+
return { constraint, errors: [] };
|
|
175
|
+
}
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// File Discovery
|
|
178
|
+
// ============================================================================
|
|
179
|
+
/**
|
|
180
|
+
* Find all constraint files in the constraint directory.
|
|
181
|
+
*/
|
|
182
|
+
function findConstraintFiles(projectRoot) {
|
|
183
|
+
const constraintDir = join(projectRoot, 'constraint');
|
|
184
|
+
const files = [];
|
|
185
|
+
if (!existsSync(constraintDir)) {
|
|
186
|
+
return files;
|
|
187
|
+
}
|
|
188
|
+
function walkDir(dir) {
|
|
189
|
+
const entries = readdirSync(dir);
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const fullPath = join(dir, entry);
|
|
192
|
+
const stat = statSync(fullPath);
|
|
193
|
+
if (stat.isDirectory()) {
|
|
194
|
+
walkDir(fullPath);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const ext = extname(entry).toLowerCase();
|
|
198
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
199
|
+
// Include files with .constraint. in the name or in constraint directory
|
|
200
|
+
if (entry.includes('.constraint.') || dir.includes('/constraint')) {
|
|
201
|
+
files.push(fullPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
walkDir(constraintDir);
|
|
208
|
+
return files.sort();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Load and parse a constraint file.
|
|
212
|
+
*/
|
|
213
|
+
function loadConstraintFile(filePath) {
|
|
214
|
+
try {
|
|
215
|
+
const content = readFileSync(filePath, 'utf8');
|
|
216
|
+
const raw = parseYaml(content);
|
|
217
|
+
if (raw === null || typeof raw !== 'object') {
|
|
218
|
+
return { errors: ['File does not contain a valid YAML object'] };
|
|
219
|
+
}
|
|
220
|
+
return validateConstraint(raw, filePath);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return {
|
|
224
|
+
errors: [`Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Verification Execution
|
|
230
|
+
// ============================================================================
|
|
231
|
+
/**
|
|
232
|
+
* Execute a verification command and return the result.
|
|
233
|
+
*/
|
|
234
|
+
async function executeVerification(invariant, projectRoot) {
|
|
235
|
+
const { id, rule, verification, blocking = true } = invariant;
|
|
236
|
+
// Manual verification always passes (requires human check)
|
|
237
|
+
if (verification.type === 'manual') {
|
|
238
|
+
return {
|
|
239
|
+
invariantId: id,
|
|
240
|
+
rule,
|
|
241
|
+
passed: true,
|
|
242
|
+
blocking,
|
|
243
|
+
output: 'Manual verification required',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// Assertion verification - check if expression is truthy
|
|
247
|
+
if (verification.type === 'assertion') {
|
|
248
|
+
try {
|
|
249
|
+
// Simple assertion evaluation (could be expanded)
|
|
250
|
+
const result = verification.value.trim().toLowerCase();
|
|
251
|
+
const passed = result === 'true' || result === '1' || result === 'yes';
|
|
252
|
+
return {
|
|
253
|
+
invariantId: id,
|
|
254
|
+
rule,
|
|
255
|
+
passed,
|
|
256
|
+
blocking,
|
|
257
|
+
output: `Assertion: ${verification.value}`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
return {
|
|
262
|
+
invariantId: id,
|
|
263
|
+
rule,
|
|
264
|
+
passed: false,
|
|
265
|
+
blocking,
|
|
266
|
+
error: `Assertion error: ${err instanceof Error ? err.message : String(err)}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Test reference - treat as command
|
|
271
|
+
if (verification.type === 'test') {
|
|
272
|
+
// Tests are typically run as commands
|
|
273
|
+
// e.g., "npm test -- --grep 'pattern'"
|
|
274
|
+
}
|
|
275
|
+
// Command verification - execute and check result
|
|
276
|
+
const command = verification.value;
|
|
277
|
+
const expect = verification.expect ?? 'success';
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
const spawnOptions = {
|
|
280
|
+
cwd: projectRoot,
|
|
281
|
+
shell: true,
|
|
282
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
283
|
+
};
|
|
284
|
+
const proc = spawn(command, [], spawnOptions);
|
|
285
|
+
let stdout = '';
|
|
286
|
+
let stderr = '';
|
|
287
|
+
proc.stdout?.on('data', (data) => {
|
|
288
|
+
stdout += data.toString();
|
|
289
|
+
});
|
|
290
|
+
proc.stderr?.on('data', (data) => {
|
|
291
|
+
stderr += data.toString();
|
|
292
|
+
});
|
|
293
|
+
proc.on('error', (err) => {
|
|
294
|
+
resolve({
|
|
295
|
+
invariantId: id,
|
|
296
|
+
rule,
|
|
297
|
+
passed: false,
|
|
298
|
+
blocking,
|
|
299
|
+
error: `Command failed to start: ${err.message}`,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
proc.on('close', (code) => {
|
|
303
|
+
const output = stdout.trim() || stderr.trim();
|
|
304
|
+
let passed = false;
|
|
305
|
+
if (expect === 'success') {
|
|
306
|
+
passed = code === 0;
|
|
307
|
+
}
|
|
308
|
+
else if (expect === 'failure') {
|
|
309
|
+
passed = code !== 0;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Custom pattern matching
|
|
313
|
+
passed = output.includes(expect);
|
|
314
|
+
}
|
|
315
|
+
resolve({
|
|
316
|
+
invariantId: id,
|
|
317
|
+
rule,
|
|
318
|
+
passed,
|
|
319
|
+
blocking,
|
|
320
|
+
output: output.slice(0, 500), // Limit output size
|
|
321
|
+
...(code !== 0 && !passed ? { error: `Exit code: ${code}` } : {}),
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
// Timeout after 30 seconds
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
proc.kill();
|
|
327
|
+
resolve({
|
|
328
|
+
invariantId: id,
|
|
329
|
+
rule,
|
|
330
|
+
passed: false,
|
|
331
|
+
blocking,
|
|
332
|
+
error: 'Command timed out after 30 seconds',
|
|
333
|
+
});
|
|
334
|
+
}, 30000);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// constraint add Command
|
|
339
|
+
// ============================================================================
|
|
340
|
+
/**
|
|
341
|
+
* Execute the constraint add command.
|
|
342
|
+
*
|
|
343
|
+
* @see req:cli:constraint-add
|
|
344
|
+
*/
|
|
345
|
+
export function runConstraintAdd(globalOpts, file, _options) {
|
|
346
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
347
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
348
|
+
// Check if prov is initialized
|
|
349
|
+
if (!isInitialized(projectRoot)) {
|
|
350
|
+
const result = {
|
|
351
|
+
success: false,
|
|
352
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
353
|
+
};
|
|
354
|
+
if (fmt === 'json') {
|
|
355
|
+
output(result, { format: 'json' });
|
|
356
|
+
}
|
|
357
|
+
else if (fmt === 'yaml') {
|
|
358
|
+
output(result, { format: 'yaml' });
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
error(result.error);
|
|
362
|
+
}
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
// Resolve file path
|
|
366
|
+
const filePath = resolve(projectRoot, file);
|
|
367
|
+
if (!existsSync(filePath)) {
|
|
368
|
+
const result = {
|
|
369
|
+
success: false,
|
|
370
|
+
error: `File not found: ${file}`,
|
|
371
|
+
};
|
|
372
|
+
if (fmt === 'json') {
|
|
373
|
+
output(result, { format: 'json' });
|
|
374
|
+
}
|
|
375
|
+
else if (fmt === 'yaml') {
|
|
376
|
+
output(result, { format: 'yaml' });
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
error(result.error);
|
|
380
|
+
}
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
// Load and validate constraint file
|
|
384
|
+
const { constraint, errors } = loadConstraintFile(filePath);
|
|
385
|
+
if (errors.length > 0 || constraint === undefined) {
|
|
386
|
+
const result = {
|
|
387
|
+
success: false,
|
|
388
|
+
error: `Invalid constraint file:\n ${errors.join('\n ')}`,
|
|
389
|
+
};
|
|
390
|
+
if (fmt === 'json') {
|
|
391
|
+
output({ ...result, validationErrors: errors }, { format: 'json' });
|
|
392
|
+
}
|
|
393
|
+
else if (fmt === 'yaml') {
|
|
394
|
+
output({ ...result, validationErrors: errors }, { format: 'yaml' });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
error(result.error);
|
|
398
|
+
}
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
// Compute hash
|
|
402
|
+
const hash = computeHash(constraint);
|
|
403
|
+
const constraintWithHash = { ...constraint, hash };
|
|
404
|
+
// Load existing graph
|
|
405
|
+
const loadResult = loadGraph(projectRoot);
|
|
406
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
407
|
+
const result = {
|
|
408
|
+
success: false,
|
|
409
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
410
|
+
};
|
|
411
|
+
if (fmt === 'json') {
|
|
412
|
+
output(result, { format: 'json' });
|
|
413
|
+
}
|
|
414
|
+
else if (fmt === 'yaml') {
|
|
415
|
+
output(result, { format: 'yaml' });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
error(result.error);
|
|
419
|
+
}
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
const graph = loadResult.data;
|
|
423
|
+
// Check if constraint already exists
|
|
424
|
+
const existingNode = graph.getNode(constraint.id);
|
|
425
|
+
if (existingNode !== undefined) {
|
|
426
|
+
const existingConstraint = existingNode.data;
|
|
427
|
+
if (existingNode.hash === hash) {
|
|
428
|
+
// Same content, no update needed
|
|
429
|
+
const result = {
|
|
430
|
+
success: true,
|
|
431
|
+
constraintId: constraint.id,
|
|
432
|
+
hash,
|
|
433
|
+
invariantCount: constraint.invariants.length,
|
|
434
|
+
};
|
|
435
|
+
if (fmt === 'json') {
|
|
436
|
+
output({ ...result, unchanged: true }, { format: 'json' });
|
|
437
|
+
}
|
|
438
|
+
else if (fmt === 'yaml') {
|
|
439
|
+
output({ ...result, unchanged: true }, { format: 'yaml' });
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
success(`Constraint ${constraint.id} is already tracked (unchanged)`);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Different content - update by removing old and adding new
|
|
447
|
+
// First remove old invariants
|
|
448
|
+
for (const inv of existingConstraint.invariants) {
|
|
449
|
+
graph.removeNode(inv.id);
|
|
450
|
+
}
|
|
451
|
+
graph.removeNode(constraint.id);
|
|
452
|
+
}
|
|
453
|
+
// Add constraint to graph
|
|
454
|
+
addConstraintToGraph(graph, constraintWithHash);
|
|
455
|
+
// Save graph
|
|
456
|
+
const saveResult = saveGraph(graph, projectRoot);
|
|
457
|
+
if (!saveResult.success) {
|
|
458
|
+
const result = {
|
|
459
|
+
success: false,
|
|
460
|
+
error: saveResult.error ?? 'Failed to save graph',
|
|
461
|
+
};
|
|
462
|
+
if (fmt === 'json') {
|
|
463
|
+
output(result, { format: 'json' });
|
|
464
|
+
}
|
|
465
|
+
else if (fmt === 'yaml') {
|
|
466
|
+
output(result, { format: 'yaml' });
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
error(result.error);
|
|
470
|
+
}
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
const result = {
|
|
474
|
+
success: true,
|
|
475
|
+
constraintId: constraint.id,
|
|
476
|
+
hash,
|
|
477
|
+
invariantCount: constraint.invariants.length,
|
|
478
|
+
};
|
|
479
|
+
if (fmt === 'json') {
|
|
480
|
+
output(result, { format: 'json' });
|
|
481
|
+
}
|
|
482
|
+
else if (fmt === 'yaml') {
|
|
483
|
+
output(result, { format: 'yaml' });
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
success(`Added constraint ${constraint.id} (${constraint.invariants.length} invariants)`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// ============================================================================
|
|
490
|
+
// constraint list Command
|
|
491
|
+
// ============================================================================
|
|
492
|
+
/**
|
|
493
|
+
* Execute the constraint list command.
|
|
494
|
+
*
|
|
495
|
+
* @see req:cli:constraint-list
|
|
496
|
+
*/
|
|
497
|
+
export function runConstraintList(globalOpts, options) {
|
|
498
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
499
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
500
|
+
// Check if prov is initialized
|
|
501
|
+
if (!isInitialized(projectRoot)) {
|
|
502
|
+
const result = {
|
|
503
|
+
success: false,
|
|
504
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
505
|
+
};
|
|
506
|
+
if (fmt === 'json') {
|
|
507
|
+
output(result, { format: 'json' });
|
|
508
|
+
}
|
|
509
|
+
else if (fmt === 'yaml') {
|
|
510
|
+
output(result, { format: 'yaml' });
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
error(result.error);
|
|
514
|
+
}
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
// Load graph
|
|
518
|
+
const loadResult = loadGraph(projectRoot);
|
|
519
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
520
|
+
const result = {
|
|
521
|
+
success: false,
|
|
522
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
523
|
+
};
|
|
524
|
+
if (fmt === 'json') {
|
|
525
|
+
output(result, { format: 'json' });
|
|
526
|
+
}
|
|
527
|
+
else if (fmt === 'yaml') {
|
|
528
|
+
output(result, { format: 'yaml' });
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
error(result.error);
|
|
532
|
+
}
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
const graph = loadResult.data;
|
|
536
|
+
// Get all constraint nodes
|
|
537
|
+
const constraintNodes = graph.getNodesByType('constraint');
|
|
538
|
+
// Build constraint entries
|
|
539
|
+
const constraints = [];
|
|
540
|
+
for (const node of constraintNodes) {
|
|
541
|
+
const constraint = node.data;
|
|
542
|
+
// Determine status - check if constraint file still exists and matches
|
|
543
|
+
let displayStatus = constraint.status;
|
|
544
|
+
// Find matching constraint file
|
|
545
|
+
const constraintFiles = findConstraintFiles(projectRoot);
|
|
546
|
+
let fileMatch;
|
|
547
|
+
for (const filePath of constraintFiles) {
|
|
548
|
+
const { constraint: fileConstraint } = loadConstraintFile(filePath);
|
|
549
|
+
if (fileConstraint?.id === constraint.id) {
|
|
550
|
+
fileMatch = filePath;
|
|
551
|
+
const fileHash = computeHash(fileConstraint);
|
|
552
|
+
if (fileHash !== node.hash) {
|
|
553
|
+
displayStatus = 'stale';
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (fileMatch === undefined) {
|
|
559
|
+
displayStatus = 'missing';
|
|
560
|
+
}
|
|
561
|
+
// Filter by status if specified
|
|
562
|
+
if (options.status !== undefined && displayStatus !== options.status) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
constraints.push({
|
|
566
|
+
id: constraint.id,
|
|
567
|
+
version: constraint.version,
|
|
568
|
+
title: constraint.title,
|
|
569
|
+
status: displayStatus,
|
|
570
|
+
hash: node.hash ?? '',
|
|
571
|
+
invariants: constraint.invariants.length,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// Sort by ID
|
|
575
|
+
constraints.sort((a, b) => a.id.localeCompare(b.id));
|
|
576
|
+
const result = { constraints };
|
|
577
|
+
if (fmt === 'json') {
|
|
578
|
+
output(result, { format: 'json' });
|
|
579
|
+
}
|
|
580
|
+
else if (fmt === 'yaml') {
|
|
581
|
+
output(result, { format: 'yaml' });
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
if (constraints.length === 0) {
|
|
585
|
+
success('No constraints tracked');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const columns = [
|
|
589
|
+
{ header: 'ID', key: 'id', minWidth: 24 },
|
|
590
|
+
{ header: 'Version', key: 'version', minWidth: 8 },
|
|
591
|
+
{ header: 'Title', key: 'title', maxWidth: 30 },
|
|
592
|
+
{ header: 'Status', key: 'status', minWidth: 8 },
|
|
593
|
+
{ header: 'Invs', key: 'invariants', minWidth: 4, align: 'right' },
|
|
594
|
+
{ header: 'Hash', key: 'hash', minWidth: 16 },
|
|
595
|
+
];
|
|
596
|
+
output(constraints, { format: 'table', columns });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// ============================================================================
|
|
600
|
+
// constraint check Command
|
|
601
|
+
// ============================================================================
|
|
602
|
+
/**
|
|
603
|
+
* Execute the constraint check command.
|
|
604
|
+
*
|
|
605
|
+
* @see req:cli:constraint-check
|
|
606
|
+
*/
|
|
607
|
+
export async function runConstraintCheck(globalOpts, constraintId, _options) {
|
|
608
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
609
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
610
|
+
// Check if prov is initialized
|
|
611
|
+
if (!isInitialized(projectRoot)) {
|
|
612
|
+
const result = {
|
|
613
|
+
success: false,
|
|
614
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
615
|
+
};
|
|
616
|
+
if (fmt === 'json') {
|
|
617
|
+
output(result, { format: 'json' });
|
|
618
|
+
}
|
|
619
|
+
else if (fmt === 'yaml') {
|
|
620
|
+
output(result, { format: 'yaml' });
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
error(result.error);
|
|
624
|
+
}
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
// Load graph
|
|
628
|
+
const loadResult = loadGraph(projectRoot);
|
|
629
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
630
|
+
const result = {
|
|
631
|
+
success: false,
|
|
632
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
633
|
+
};
|
|
634
|
+
if (fmt === 'json') {
|
|
635
|
+
output(result, { format: 'json' });
|
|
636
|
+
}
|
|
637
|
+
else if (fmt === 'yaml') {
|
|
638
|
+
output(result, { format: 'yaml' });
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
error(result.error);
|
|
642
|
+
}
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
const graph = loadResult.data;
|
|
646
|
+
// Find constraints to check
|
|
647
|
+
let constraintsToCheck = [];
|
|
648
|
+
if (constraintId !== undefined) {
|
|
649
|
+
// Check specific constraint
|
|
650
|
+
const node = graph.getNode(constraintId);
|
|
651
|
+
if (node === undefined || node.type !== 'constraint') {
|
|
652
|
+
const result = {
|
|
653
|
+
success: false,
|
|
654
|
+
results: [],
|
|
655
|
+
passed: 0,
|
|
656
|
+
failed: 0,
|
|
657
|
+
skipped: 0,
|
|
658
|
+
error: `Constraint not found: ${constraintId}`,
|
|
659
|
+
};
|
|
660
|
+
if (fmt === 'json') {
|
|
661
|
+
output(result, { format: 'json' });
|
|
662
|
+
}
|
|
663
|
+
else if (fmt === 'yaml') {
|
|
664
|
+
output(result, { format: 'yaml' });
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
error(result.error);
|
|
668
|
+
}
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
constraintsToCheck = [node.data];
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Check all constraints
|
|
675
|
+
const constraintNodes = graph.getNodesByType('constraint');
|
|
676
|
+
constraintsToCheck = constraintNodes.map((n) => n.data);
|
|
677
|
+
}
|
|
678
|
+
if (constraintsToCheck.length === 0) {
|
|
679
|
+
const result = {
|
|
680
|
+
success: true,
|
|
681
|
+
results: [],
|
|
682
|
+
passed: 0,
|
|
683
|
+
failed: 0,
|
|
684
|
+
skipped: 0,
|
|
685
|
+
};
|
|
686
|
+
if (fmt === 'json') {
|
|
687
|
+
output(result, { format: 'json' });
|
|
688
|
+
}
|
|
689
|
+
else if (fmt === 'yaml') {
|
|
690
|
+
output(result, { format: 'yaml' });
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
warn('No constraints to check');
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
// Run verification for each invariant
|
|
698
|
+
const allResults = [];
|
|
699
|
+
let passed = 0;
|
|
700
|
+
let failed = 0;
|
|
701
|
+
const skipped = 0; // TODO: implement skipping for manual verifications or other cases
|
|
702
|
+
let hasBlockingFailure = false;
|
|
703
|
+
for (const constraint of constraintsToCheck) {
|
|
704
|
+
if (fmt !== 'json' && fmt !== 'yaml') {
|
|
705
|
+
process.stdout.write(`\nChecking ${constraint.id}...\n`);
|
|
706
|
+
}
|
|
707
|
+
for (const invariant of constraint.invariants) {
|
|
708
|
+
const result = await executeVerification(invariant, projectRoot);
|
|
709
|
+
allResults.push(result);
|
|
710
|
+
if (result.passed) {
|
|
711
|
+
passed++;
|
|
712
|
+
if (fmt !== 'json' && fmt !== 'yaml') {
|
|
713
|
+
process.stdout.write(` ✓ ${invariant.id}: ${invariant.rule}\n`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
failed++;
|
|
718
|
+
if (result.blocking) {
|
|
719
|
+
hasBlockingFailure = true;
|
|
720
|
+
}
|
|
721
|
+
if (fmt !== 'json' && fmt !== 'yaml') {
|
|
722
|
+
const marker = result.blocking ? '✗' : '⚠';
|
|
723
|
+
process.stdout.write(` ${marker} ${invariant.id}: ${invariant.rule}\n`);
|
|
724
|
+
if (result.error !== undefined) {
|
|
725
|
+
process.stdout.write(` ${result.error}\n`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const result = {
|
|
732
|
+
success: !hasBlockingFailure,
|
|
733
|
+
results: allResults,
|
|
734
|
+
passed,
|
|
735
|
+
failed,
|
|
736
|
+
skipped,
|
|
737
|
+
};
|
|
738
|
+
if (constraintId !== undefined) {
|
|
739
|
+
result.constraintId = constraintId;
|
|
740
|
+
}
|
|
741
|
+
if (fmt === 'json') {
|
|
742
|
+
output(result, { format: 'json' });
|
|
743
|
+
}
|
|
744
|
+
else if (fmt === 'yaml') {
|
|
745
|
+
output(result, { format: 'yaml' });
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
process.stdout.write('\n');
|
|
749
|
+
if (hasBlockingFailure) {
|
|
750
|
+
error(`${failed} invariant(s) failed (${passed} passed)`);
|
|
751
|
+
}
|
|
752
|
+
else if (failed > 0) {
|
|
753
|
+
warn(`${failed} non-blocking invariant(s) failed (${passed} passed)`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
success(`All ${passed} invariant(s) passed`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (hasBlockingFailure) {
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=constraint.js.map
|