@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.
Files changed (146) hide show
  1. package/README.md +314 -0
  2. package/dist/cli.d.ts +26 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +381 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/agent.d.ts +107 -0
  7. package/dist/commands/agent.d.ts.map +1 -0
  8. package/dist/commands/agent.js +197 -0
  9. package/dist/commands/agent.js.map +1 -0
  10. package/dist/commands/agent.test.d.ts +5 -0
  11. package/dist/commands/agent.test.d.ts.map +1 -0
  12. package/dist/commands/agent.test.js +199 -0
  13. package/dist/commands/agent.test.js.map +1 -0
  14. package/dist/commands/constraint.d.ts +100 -0
  15. package/dist/commands/constraint.d.ts.map +1 -0
  16. package/dist/commands/constraint.js +763 -0
  17. package/dist/commands/constraint.js.map +1 -0
  18. package/dist/commands/constraint.test.d.ts +9 -0
  19. package/dist/commands/constraint.test.d.ts.map +1 -0
  20. package/dist/commands/constraint.test.js +470 -0
  21. package/dist/commands/constraint.test.js.map +1 -0
  22. package/dist/commands/graph.d.ts +99 -0
  23. package/dist/commands/graph.d.ts.map +1 -0
  24. package/dist/commands/graph.js +552 -0
  25. package/dist/commands/graph.js.map +1 -0
  26. package/dist/commands/graph.test.d.ts +2 -0
  27. package/dist/commands/graph.test.d.ts.map +1 -0
  28. package/dist/commands/graph.test.js +258 -0
  29. package/dist/commands/graph.test.js.map +1 -0
  30. package/dist/commands/impact.d.ts +83 -0
  31. package/dist/commands/impact.d.ts.map +1 -0
  32. package/dist/commands/impact.js +319 -0
  33. package/dist/commands/impact.js.map +1 -0
  34. package/dist/commands/impact.test.d.ts +2 -0
  35. package/dist/commands/impact.test.d.ts.map +1 -0
  36. package/dist/commands/impact.test.js +234 -0
  37. package/dist/commands/impact.test.js.map +1 -0
  38. package/dist/commands/init.d.ts +45 -0
  39. package/dist/commands/init.d.ts.map +1 -0
  40. package/dist/commands/init.js +94 -0
  41. package/dist/commands/init.js.map +1 -0
  42. package/dist/commands/init.test.d.ts +7 -0
  43. package/dist/commands/init.test.d.ts.map +1 -0
  44. package/dist/commands/init.test.js +174 -0
  45. package/dist/commands/init.test.js.map +1 -0
  46. package/dist/commands/integration.test.d.ts +10 -0
  47. package/dist/commands/integration.test.d.ts.map +1 -0
  48. package/dist/commands/integration.test.js +456 -0
  49. package/dist/commands/integration.test.js.map +1 -0
  50. package/dist/commands/mcp.d.ts +21 -0
  51. package/dist/commands/mcp.d.ts.map +1 -0
  52. package/dist/commands/mcp.js +616 -0
  53. package/dist/commands/mcp.js.map +1 -0
  54. package/dist/commands/mcp.test.d.ts +7 -0
  55. package/dist/commands/mcp.test.d.ts.map +1 -0
  56. package/dist/commands/mcp.test.js +132 -0
  57. package/dist/commands/mcp.test.js.map +1 -0
  58. package/dist/commands/plan.d.ts +218 -0
  59. package/dist/commands/plan.d.ts.map +1 -0
  60. package/dist/commands/plan.js +1307 -0
  61. package/dist/commands/plan.js.map +1 -0
  62. package/dist/commands/plan.test.d.ts +9 -0
  63. package/dist/commands/plan.test.d.ts.map +1 -0
  64. package/dist/commands/plan.test.js +569 -0
  65. package/dist/commands/plan.test.js.map +1 -0
  66. package/dist/commands/spec.d.ts +94 -0
  67. package/dist/commands/spec.d.ts.map +1 -0
  68. package/dist/commands/spec.js +635 -0
  69. package/dist/commands/spec.js.map +1 -0
  70. package/dist/commands/spec.test.d.ts +9 -0
  71. package/dist/commands/spec.test.d.ts.map +1 -0
  72. package/dist/commands/spec.test.js +407 -0
  73. package/dist/commands/spec.test.js.map +1 -0
  74. package/dist/commands/trace.d.ts +157 -0
  75. package/dist/commands/trace.d.ts.map +1 -0
  76. package/dist/commands/trace.js +847 -0
  77. package/dist/commands/trace.js.map +1 -0
  78. package/dist/commands/trace.test.d.ts +9 -0
  79. package/dist/commands/trace.test.d.ts.map +1 -0
  80. package/dist/commands/trace.test.js +524 -0
  81. package/dist/commands/trace.test.js.map +1 -0
  82. package/dist/graph.d.ts +204 -0
  83. package/dist/graph.d.ts.map +1 -0
  84. package/dist/graph.js +496 -0
  85. package/dist/graph.js.map +1 -0
  86. package/dist/graph.test.d.ts +2 -0
  87. package/dist/graph.test.d.ts.map +1 -0
  88. package/dist/graph.test.js +382 -0
  89. package/dist/graph.test.js.map +1 -0
  90. package/dist/hash.d.ts +72 -0
  91. package/dist/hash.d.ts.map +1 -0
  92. package/dist/hash.js +137 -0
  93. package/dist/hash.js.map +1 -0
  94. package/dist/hash.test.d.ts +2 -0
  95. package/dist/hash.test.d.ts.map +1 -0
  96. package/dist/hash.test.js +227 -0
  97. package/dist/hash.test.js.map +1 -0
  98. package/dist/index.d.ts +18 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +64 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/index.test.d.ts +2 -0
  103. package/dist/index.test.d.ts.map +1 -0
  104. package/dist/index.test.js +11 -0
  105. package/dist/index.test.js.map +1 -0
  106. package/dist/output.d.ts +84 -0
  107. package/dist/output.d.ts.map +1 -0
  108. package/dist/output.js +175 -0
  109. package/dist/output.js.map +1 -0
  110. package/dist/output.test.d.ts +7 -0
  111. package/dist/output.test.d.ts.map +1 -0
  112. package/dist/output.test.js +146 -0
  113. package/dist/output.test.js.map +1 -0
  114. package/dist/staleness.d.ts +162 -0
  115. package/dist/staleness.d.ts.map +1 -0
  116. package/dist/staleness.js +309 -0
  117. package/dist/staleness.js.map +1 -0
  118. package/dist/staleness.test.d.ts +2 -0
  119. package/dist/staleness.test.d.ts.map +1 -0
  120. package/dist/staleness.test.js +448 -0
  121. package/dist/staleness.test.js.map +1 -0
  122. package/dist/storage.d.ts +267 -0
  123. package/dist/storage.d.ts.map +1 -0
  124. package/dist/storage.js +623 -0
  125. package/dist/storage.js.map +1 -0
  126. package/dist/storage.test.d.ts +5 -0
  127. package/dist/storage.test.d.ts.map +1 -0
  128. package/dist/storage.test.js +434 -0
  129. package/dist/storage.test.js.map +1 -0
  130. package/dist/types.d.ts +270 -0
  131. package/dist/types.d.ts.map +1 -0
  132. package/dist/types.js +12 -0
  133. package/dist/types.js.map +1 -0
  134. package/dist/types.test.d.ts +2 -0
  135. package/dist/types.test.d.ts.map +1 -0
  136. package/dist/types.test.js +232 -0
  137. package/dist/types.test.js.map +1 -0
  138. package/dist/watcher.d.ts +139 -0
  139. package/dist/watcher.d.ts.map +1 -0
  140. package/dist/watcher.js +406 -0
  141. package/dist/watcher.js.map +1 -0
  142. package/dist/watcher.test.d.ts +5 -0
  143. package/dist/watcher.test.d.ts.map +1 -0
  144. package/dist/watcher.test.js +327 -0
  145. package/dist/watcher.test.js.map +1 -0
  146. 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