arcvision 0.2.25 → 0.2.26

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 ADDED
@@ -0,0 +1,59 @@
1
+ # ArcVision CLI
2
+
3
+ **Architectural Governance and Invariant Detection Tool**
4
+
5
+ ArcVision is a powerful 4-pass structural analysis engine designed to map, monitor, and enforce architectural integrity in modern codebases.
6
+
7
+ ## Key Features
8
+
9
+ - **4-Pass Structural Analysis**: Deeply analyzes facts, semantics, structural roles, and signals.
10
+ - **Invariant Detection**: Automatically identifies architectural constraints and assertions.
11
+ - **Change Impact Gating**: Evaluates the blast radius and architectural risk of proposed code changes.
12
+ - **Authority Ledger**: Maintains a verifiable audit trail of architectural decisions and overrides.
13
+ - **Dashboard Integration**: Syncs architectural maps and health metrics to the ArcVision Dashboard.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g arcvision
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Scan a repository
24
+ Generate a comprehensive architectural context and visualize your system structure.
25
+ ```bash
26
+ arcvision scan .
27
+ ```
28
+
29
+ ### 2. Evaluate changes
30
+ Assess the impact of changes against established invariants.
31
+ ```bash
32
+ arcvision evaluate . --simulate-changes src/critical-module.js
33
+ ```
34
+
35
+ ### 3. Record decisions
36
+ Manage architectural decisions through the Authority Ledger.
37
+ ```bash
38
+ arcvision override --event-id <event-id> --reason "Intentional architectural shift" --owner "arch-lead"
39
+ ```
40
+
41
+ ## Commands
42
+
43
+ - `scan <directory>`: Analyze repository and generate `arcvision.context.json`.
44
+ - `evaluate <directory>`: Run Authoritative Change Impact Gate (ACIG).
45
+ - `diff <old> <new>`: Compare two architectural snapshots.
46
+ - `ledger`: Manage and audit architectural overrides.
47
+ - `link <token>`: Connect your local environment to the ArcVision Dashboard.
48
+
49
+ ## Architectural Health Assessment
50
+
51
+ ArcVision computes an architectural health score based on:
52
+ - Invariant coverage
53
+ - Coupling degree
54
+ - Criticality signals
55
+ - Structural complexity
56
+
57
+ ## License
58
+
59
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcvision",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
4
4
  "description": "ArcVision CLI - Architectural Governance and Invariant Detection Tool",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9,9 +9,10 @@ const crypto = require('crypto');
9
9
 
10
10
  class AuthorityLedger {
11
11
  constructor(ledgerPath) {
12
- // Use arcvision_context directory by default
13
- const defaultPath = path.join(process.cwd(), 'arcvision_context', 'architecture.authority.ledger.json');
14
- this.ledgerPath = ledgerPath || process.env.AUTHORITY_LEDGER_PATH || defaultPath;
12
+ if (!ledgerPath) {
13
+ throw new Error('AuthorityLedger requires an explicit ledgerPath');
14
+ }
15
+ this.ledgerPath = ledgerPath;
15
16
  this.ensureLedgerExists();
16
17
  }
17
18
 
@@ -26,7 +27,7 @@ class AuthorityLedger {
26
27
  fs.mkdirSync(dirPath, { recursive: true });
27
28
  console.log(`📁 Created directory for authority ledger: ${dirPath}`);
28
29
  }
29
-
30
+
30
31
  if (!fs.existsSync(this.ledgerPath)) {
31
32
  const initialLedger = {
32
33
  schema_version: process.env.ARCHITECTURE_LEDGER_SCHEMA_VERSION || "1.0",
@@ -48,7 +49,7 @@ class AuthorityLedger {
48
49
  */
49
50
  getSystemId() {
50
51
  try {
51
- const cwd = process.cwd();
52
+ const cwd = path.dirname(path.dirname(this.ledgerPath)); // One level up from arcvision_context
52
53
  const projectName = path.basename(cwd);
53
54
  return projectName;
54
55
  } catch (error) {
@@ -115,8 +116,8 @@ class AuthorityLedger {
115
116
  isOverridden(commitHash) {
116
117
  try {
117
118
  const ledger = this.readLedger();
118
- return ledger.ledger.some(event =>
119
- event.decision === 'OVERRIDDEN' &&
119
+ return ledger.ledger.some(event =>
120
+ event.decision === 'OVERRIDDEN' &&
120
121
  event.commit === commitHash
121
122
  );
122
123
  } catch (error) {
@@ -150,7 +151,7 @@ class AuthorityLedger {
150
151
 
151
152
  // Filter out events that have been overridden
152
153
  return blockedEvents.filter(blockedEvent => {
153
- return !ledger.ledger.some(overrideEvent =>
154
+ return !ledger.ledger.some(overrideEvent =>
154
155
  overrideEvent.decision === 'OVERRIDDEN' &&
155
156
  overrideEvent.original_blocked_event === blockedEvent.event_id
156
157
  );
@@ -174,7 +175,7 @@ class AuthorityLedger {
174
175
  appendEvent(event) {
175
176
  try {
176
177
  const ledger = this.readLedger();
177
-
178
+
178
179
  // Validate event structure
179
180
  if (!event.event_id || !event.timestamp || !event.decision) {
180
181
  throw new Error('Invalid event structure');
@@ -182,7 +183,7 @@ class AuthorityLedger {
182
183
 
183
184
  ledger.ledger.push(event);
184
185
  this.writeLedger(ledger);
185
-
186
+
186
187
  console.log(`Recorded ${event.decision} decision in authority ledger`);
187
188
  } catch (error) {
188
189
  console.error('Failed to append to authority ledger:', error.message);
@@ -197,12 +198,12 @@ class AuthorityLedger {
197
198
  try {
198
199
  const content = fs.readFileSync(this.ledgerPath, 'utf8');
199
200
  const ledger = JSON.parse(content);
200
-
201
+
201
202
  // Validate structure
202
203
  if (!ledger.schema_version || !Array.isArray(ledger.ledger)) {
203
204
  throw new Error('Invalid ledger structure');
204
205
  }
205
-
206
+
206
207
  return ledger;
207
208
  } catch (error) {
208
209
  console.error('Failed to read authority ledger:', error.message);
@@ -232,7 +233,7 @@ class AuthorityLedger {
232
233
  const ledger = this.readLedger();
233
234
  const blockedCount = ledger.ledger.filter(e => e.decision === 'BLOCKED').length;
234
235
  const overriddenCount = ledger.ledger.filter(e => e.decision === 'OVERRIDDEN').length;
235
-
236
+
236
237
  return {
237
238
  total_events: ledger.ledger.length,
238
239
  blocked_decisions: blockedCount,
@@ -256,7 +257,7 @@ class AuthorityLedger {
256
257
  exportLedger(format = 'json') {
257
258
  try {
258
259
  const ledger = this.readLedger();
259
-
260
+
260
261
  switch (format.toLowerCase()) {
261
262
  case 'json':
262
263
  return JSON.stringify(ledger, null, 2);
@@ -284,17 +285,14 @@ class AuthorityLedger {
284
285
  event.author,
285
286
  event.violations ? event.violations.length : 0
286
287
  ]);
287
-
288
+
288
289
  const csvContent = [
289
290
  headers.join(','),
290
291
  ...rows.map(row => row.join(','))
291
292
  ].join('\n');
292
-
293
+
293
294
  return csvContent;
294
295
  }
295
296
  }
296
297
 
297
- // Export singleton instance
298
- const authorityLedger = new AuthorityLedger();
299
-
300
- module.exports = { AuthorityLedger, authorityLedger };
298
+ module.exports = { AuthorityLedger };
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * Change Impact Evaluator
3
3
  * Connects existing ArcVision data to invariants
4
4
  */
@@ -16,16 +16,16 @@ function evaluateChange(input) {
16
16
  }
17
17
 
18
18
  const { changedFiles, dependencyGraph, invariants, context } = input;
19
-
19
+
20
20
  // Validate required parameters with enhanced checks
21
21
  if (!Array.isArray(changedFiles)) {
22
22
  return createEnhancedErrorResult('Invalid input: changedFiles must be an array');
23
23
  }
24
-
24
+
25
25
  if (!dependencyGraph) {
26
26
  return createEnhancedErrorResult('Invalid input: dependencyGraph is required');
27
27
  }
28
-
28
+
29
29
  if (!Array.isArray(invariants)) {
30
30
  return createEnhancedErrorResult('Invalid input: invariants must be an array');
31
31
  }
@@ -48,10 +48,10 @@ function evaluateChange(input) {
48
48
  }
49
49
 
50
50
  const matchesScope = matchesInvariantScope(sanitizedChangedFiles, invariant);
51
-
51
+
52
52
  if (matchesScope) {
53
53
  const isViolated = checkInvariantRule(invariant, dependencyGraph, sanitizedChangedFiles);
54
-
54
+
55
55
  if (isViolated) {
56
56
  violations.push({
57
57
  ...invariant,
@@ -60,7 +60,7 @@ function evaluateChange(input) {
60
60
  });
61
61
  }
62
62
  }
63
-
63
+
64
64
  processedInvariants.push(invariant.id);
65
65
  } catch (ruleError) {
66
66
  console.warn(`Warning: Error evaluating invariant ${invariant?.id || 'unknown'}:`, ruleError.message);
@@ -75,7 +75,7 @@ function evaluateChange(input) {
75
75
  // Determine decision based on violations with enhanced severity handling
76
76
  const blockingViolations = violations.filter(v => v.severity === 'block' || v.severity === 'BLOCK');
77
77
  const riskyViolations = violations.filter(v => v.severity === 'risk' || v.severity === 'RISK');
78
-
78
+
79
79
  if (blockingViolations.length > 0) {
80
80
  return formatEnhancedResult('BLOCKED', violations, input);
81
81
  }
@@ -116,7 +116,7 @@ function matchesInvariantScope(changedFiles, invariant) {
116
116
  }
117
117
 
118
118
  const scope = invariant.scope;
119
-
119
+
120
120
  // If no scope restrictions, always matches
121
121
  if (!scope.files && !scope.components && !scope.flows) {
122
122
  return true;
@@ -210,25 +210,25 @@ function checkInvariantRule(invariant, dependencyGraph, changedFiles) {
210
210
  */
211
211
  function checkGenericRule(condition, dependencyGraph, changedFiles) {
212
212
  if (!condition) return false;
213
-
213
+
214
214
  try {
215
215
  // Handle different condition types dynamically
216
216
  if (condition.forbiddenDependency && typeof condition.forbiddenDependency === 'object') {
217
217
  return checkForbiddenDependency(condition.forbiddenDependency, dependencyGraph, changedFiles);
218
218
  }
219
-
219
+
220
220
  if (condition.mustExist) {
221
221
  return checkRequirement(condition.mustExist, dependencyGraph, changedFiles);
222
222
  }
223
-
223
+
224
224
  if (condition.mustNotExist) {
225
225
  return checkProhibition(condition.mustNotExist, dependencyGraph, changedFiles);
226
226
  }
227
-
227
+
228
228
  if (condition.pattern) {
229
229
  return checkPatternMatch(condition.pattern, dependencyGraph, changedFiles);
230
230
  }
231
-
231
+
232
232
  // Default: if condition exists and has properties, consider it potentially violated
233
233
  return typeof condition === 'object' && Object.keys(condition).length > 0;
234
234
  } catch (error) {
@@ -252,38 +252,38 @@ function checkForbiddenDependency(forbiddenDep, dependencyGraph, changedFiles) {
252
252
  if (!forbiddenDep || typeof forbiddenDep !== 'object') {
253
253
  return false;
254
254
  }
255
-
255
+
256
256
  if (!forbiddenDep.from || !forbiddenDep.to) {
257
257
  return false;
258
258
  }
259
-
259
+
260
260
  try {
261
261
  // Use enhanced circular dependency detector
262
262
  const circularDetector = new CircularDependencyDetector();
263
-
263
+
264
264
  // Check if the forbidden dependency pattern is violated
265
265
  const result = circularDetector.checkEnhancedForbiddenDependency(
266
- forbiddenDep,
267
- dependencyGraph.nodes || [],
266
+ forbiddenDep,
267
+ dependencyGraph.nodes || [],
268
268
  dependencyGraph.edges || []
269
269
  );
270
-
270
+
271
271
  if (result.violated) {
272
272
  console.log(`🚨 Forbidden dependency violation detected: ${result.description}`);
273
273
  return true;
274
274
  }
275
-
275
+
276
276
  // Fallback to original method for backward compatibility
277
277
  const { patternMatcher } = require('./pattern-matcher');
278
-
278
+
279
279
  return changedFiles.some(file => {
280
280
  if (typeof file !== 'string') return false;
281
-
281
+
282
282
  // Check if file matches the 'from' scope using robust pattern matching
283
283
  const matchesFrom = patternMatcher.match(file, forbiddenDep.from, { matchBase: true }) ||
284
- patternMatcher.match(file, `**/${forbiddenDep.from}/**`, { matchBase: false }) ||
285
- file.toLowerCase().includes(forbiddenDep.from.toLowerCase());
286
-
284
+ patternMatcher.match(file, `**/${forbiddenDep.from}/**`, { matchBase: false }) ||
285
+ file.toLowerCase().includes(forbiddenDep.from.toLowerCase());
286
+
287
287
  if (matchesFrom) {
288
288
  // Check if this file now depends on the 'to' component
289
289
  return hasDependencyTo(dependencyGraph, file, forbiddenDep.to);
@@ -333,20 +333,20 @@ function checkPatternRule(invariant, dependencyGraph, changedFiles) {
333
333
  const { patternMatcher } = require('./pattern-matcher');
334
334
  const fs = require('fs');
335
335
  const path = require('path');
336
-
336
+
337
337
  // Convert pattern to regex for proper matching
338
338
  let regexPattern;
339
339
  try {
340
340
  // Handle the pipe (OR) operator and other regex constructs
341
341
  const escapedPattern = pattern.replace(/\./g, '\\.')
342
- .replace(/\*/g, '.*')
343
- .replace(/\+/g, '\\+');
342
+ .replace(/\*/g, '.*')
343
+ .replace(/\+/g, '\\+');
344
344
  regexPattern = new RegExp(escapedPattern, 'gi');
345
345
  } catch (regexError) {
346
346
  console.warn(`Warning: Could not create regex from pattern "${pattern}": ${regexError.message}`);
347
347
  return false;
348
348
  }
349
-
349
+
350
350
  // Check if any node content matches the pattern by reading actual files
351
351
  if (dependencyGraph.nodes && Array.isArray(dependencyGraph.nodes)) {
352
352
  return dependencyGraph.nodes.some(node => {
@@ -356,20 +356,23 @@ function checkPatternRule(invariant, dependencyGraph, changedFiles) {
356
356
  console.log(`🚨 Pattern violation detected in ${node.path}: matches pattern "${pattern}"`);
357
357
  return true;
358
358
  }
359
-
359
+
360
360
  // Read actual file content to check for pattern violations
361
361
  try {
362
- // Resolve the full file path
363
- const fullPath = path.resolve(node.path);
362
+ // Resolve the full file path relative to system root if available
363
+ const rootPath = dependencyGraph.system && dependencyGraph.system.root_path ?
364
+ dependencyGraph.system.root_path :
365
+ process.cwd();
366
+ const fullPath = path.resolve(rootPath, node.path);
364
367
  if (fs.existsSync(fullPath)) {
365
368
  const fileContent = fs.readFileSync(fullPath, 'utf8');
366
-
369
+
367
370
  // Check if file content matches the regex pattern
368
371
  if (regexPattern.test(fileContent)) {
369
372
  console.log(`🚨 Pattern violation detected in ${node.path}: content matches pattern "${pattern}"`);
370
373
  return true;
371
374
  }
372
-
375
+
373
376
  // Also check with pattern matcher for glob patterns
374
377
  if (patternMatcher.match(fileContent, `*${pattern}*`, { matchBase: false })) {
375
378
  console.log(`🚨 Pattern violation detected in ${node.path}: content matches pattern "${pattern}"`);
@@ -380,7 +383,7 @@ function checkPatternRule(invariant, dependencyGraph, changedFiles) {
380
383
  // Silently continue if file can't be read
381
384
  }
382
385
  }
383
-
386
+
384
387
  return false;
385
388
  });
386
389
  }
@@ -388,7 +391,7 @@ function checkPatternRule(invariant, dependencyGraph, changedFiles) {
388
391
  } catch (error) {
389
392
  console.warn(`Warning: Error in pattern rule check:`, error.message);
390
393
  }
391
-
394
+
392
395
  return false;
393
396
  }
394
397
 
@@ -400,7 +403,7 @@ function checkExistenceRule(invariant, dependencyGraph, changedFiles) {
400
403
  if (invariant.rule.condition && invariant.rule.condition.mustExist) {
401
404
  const requiredElement = invariant.rule.condition.mustExist;
402
405
  const { patternMatcher } = require('./pattern-matcher');
403
-
406
+
404
407
  // Check if required element exists
405
408
  if (dependencyGraph.nodes && Array.isArray(dependencyGraph.nodes)) {
406
409
  const exists = dependencyGraph.nodes.some(node => {
@@ -409,18 +412,18 @@ function checkExistenceRule(invariant, dependencyGraph, changedFiles) {
409
412
  }
410
413
  return false;
411
414
  });
412
-
415
+
413
416
  if (!exists) {
414
417
  console.log(`🚨 Existence violation: Required element "${requiredElement}" not found`);
415
418
  return true; // Violation - required element doesn't exist
416
419
  }
417
420
  }
418
421
  }
419
-
422
+
420
423
  if (invariant.rule.condition && invariant.rule.condition.mustNotExist) {
421
424
  const forbiddenElement = invariant.rule.condition.mustNotExist;
422
425
  const { patternMatcher } = require('./pattern-matcher');
423
-
426
+
424
427
  // Check if forbidden element exists
425
428
  if (dependencyGraph.nodes && Array.isArray(dependencyGraph.nodes)) {
426
429
  const exists = dependencyGraph.nodes.some(node => {
@@ -429,7 +432,7 @@ function checkExistenceRule(invariant, dependencyGraph, changedFiles) {
429
432
  }
430
433
  return false;
431
434
  });
432
-
435
+
433
436
  if (exists) {
434
437
  console.log(`🚨 Existence violation: Forbidden element "${forbiddenElement}" found`);
435
438
  return true; // Violation - forbidden element exists
@@ -439,7 +442,7 @@ function checkExistenceRule(invariant, dependencyGraph, changedFiles) {
439
442
  } catch (error) {
440
443
  console.warn(`Warning: Error in existence rule check:`, error.message);
441
444
  }
442
-
445
+
443
446
  return false;
444
447
  }
445
448
 
@@ -459,11 +462,11 @@ function checkOwnershipRule(invariant, dependencyGraph, changedFiles) {
459
462
  // Check if changed files fall under this invariant's scope and if ownership is violated
460
463
  if (invariant.rule.condition && invariant.scope.files) {
461
464
  const { authorizedOwners } = invariant.rule.condition;
462
-
465
+
463
466
  if (authorizedOwners && Array.isArray(authorizedOwners)) {
464
467
  return changedFiles.some(file => {
465
468
  if (typeof file !== 'string') return false;
466
-
469
+
467
470
  // Check if file matches the scope
468
471
  const matchesScope = invariant.scope.files.some(pattern => {
469
472
  if (typeof pattern !== 'string') return false;
@@ -475,7 +478,7 @@ function checkOwnershipRule(invariant, dependencyGraph, changedFiles) {
475
478
  return false;
476
479
  }
477
480
  });
478
-
481
+
479
482
  if (matchesScope) {
480
483
  // Check if current owner is authorized (this would require additional context about who made the change)
481
484
  // For now, we'll just check if the invariant has an owner specified and it's different
@@ -489,7 +492,7 @@ function checkOwnershipRule(invariant, dependencyGraph, changedFiles) {
489
492
  console.warn(`Warning: Error in ownership rule check:`, error.message);
490
493
  return false;
491
494
  }
492
-
495
+
493
496
  return false;
494
497
  }
495
498
 
@@ -516,22 +519,22 @@ function hasDependencyTo(dependencyGraph, fromFile, toComponent) {
516
519
  if (typeof fromFile !== 'string' || typeof toComponent !== 'string') {
517
520
  return false;
518
521
  }
519
-
522
+
520
523
  try {
521
524
  // Simplified implementation - would need to analyze actual dependency graph
522
525
  if (dependencyGraph && dependencyGraph.edges && Array.isArray(dependencyGraph.edges)) {
523
526
  return dependencyGraph.edges.some((edge) => {
524
527
  if (!edge || !edge.source || !edge.target) return false;
525
-
526
- return edge.source === fromFile &&
527
- (edge.target.includes(toComponent) ||
528
- edge.target.toLowerCase().includes(toComponent.toLowerCase()));
528
+
529
+ return edge.source === fromFile &&
530
+ (edge.target.includes(toComponent) ||
531
+ edge.target.toLowerCase().includes(toComponent.toLowerCase()));
529
532
  });
530
533
  }
531
534
  } catch (error) {
532
535
  console.warn(`Warning: Error checking dependency graph:`, error.message);
533
536
  }
534
-
537
+
535
538
  return false;
536
539
  }
537
540