tribunal-kit 9.2.6 → 9.2.7

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 (63) hide show
  1. package/.agent/ARCHITECTURE.md +2 -2
  2. package/.agent/history/architecture-intelligence.html +1487 -401
  3. package/.agent/history/architecture-model.json +9365 -5697
  4. package/.agent/history/architecture-verification.json +37 -9
  5. package/.agent/routing_index.json +2069 -3340
  6. package/.agent/rules/GEMINI.md +1 -1
  7. package/.agent/scripts/adaptive_execution.js +511 -0
  8. package/.agent/scripts/architecture_diff.js +303 -260
  9. package/.agent/scripts/architecture_extractor.js +792 -753
  10. package/.agent/scripts/architecture_health.js +293 -256
  11. package/.agent/scripts/architecture_intelligence.js +3100 -0
  12. package/.agent/scripts/architecture_verifier.js +303 -270
  13. package/.agent/scripts/architecture_visualizer.js +880 -873
  14. package/.agent/scripts/blast_radius_engine.js +274 -234
  15. package/.agent/scripts/capability_evolution.js +453 -0
  16. package/.agent/scripts/concept_extractor.js +240 -0
  17. package/.agent/scripts/consideration_engine.js +284 -0
  18. package/.agent/scripts/coverage_engine.js +102 -0
  19. package/.agent/scripts/cross_domain_engine.js +1883 -0
  20. package/.agent/scripts/engineering_memory.js +1313 -0
  21. package/.agent/scripts/integrity_manifest.js +1 -5
  22. package/.agent/scripts/judgment_engine.js +790 -0
  23. package/.agent/scripts/materiality_engine.js +251 -0
  24. package/.agent/scripts/outcome_engine.js +624 -0
  25. package/.agent/scripts/outcome_optimization_engine.js +1632 -0
  26. package/.agent/scripts/simulation_engine.js +1478 -0
  27. package/.agent/scripts/skill_intelligence.js +1371 -0
  28. package/.agent/scripts/validation_engine.js +236 -236
  29. package/.agent/scripts/verification_auditor.js +266 -0
  30. package/.agent/skill-intelligence/taxonomy/engineering-concepts.json +555 -0
  31. package/.agent/skill_topic_map.json +301 -341
  32. package/.agent/skills_inventory.json +4272 -6922
  33. package/.agent/workflows/arch.md +96 -80
  34. package/.agent/workflows/skill-intelligence.md +216 -0
  35. package/CONTRIBUTING.md +2 -2
  36. package/README.md +24 -24
  37. package/bin/adapter-install.js +1 -1
  38. package/package.json +8 -9
  39. package/plugins/core-plugin.json +1 -1
  40. package/plugins/gemini/extension.json +1 -1
  41. package/src/browser/index.js +2 -1
  42. package/src/browser/launcher.js +41 -1
  43. package/src/cli.js +3 -1
  44. package/src/commands/arch.js +261 -172
  45. package/src/commands/context.js +27 -8
  46. package/src/commands/learn.js +21 -6
  47. package/src/commands/memory.js +10 -1
  48. package/src/commands/native.js +12 -3
  49. package/src/commands/skill-intelligence.js +1165 -0
  50. package/src/commands/system1.js +105 -4
  51. package/src/context/ranker.js +337 -339
  52. package/src/evolution/engine.js +296 -295
  53. package/src/synthesis/claim_check_separator.js +294 -271
  54. package/src/system1/browser_intelligence.js +300 -0
  55. package/src/system1/constants.js +49 -0
  56. package/src/system1/decision_engine.js +498 -0
  57. package/src/system1/evidence_engine.js +602 -0
  58. package/src/system1/evolution_pipeline.js +581 -0
  59. package/src/system1/index.d.ts +114 -0
  60. package/src/system1/index.js +102 -0
  61. package/src/system1/reviewer_orchestrator.js +477 -0
  62. package/src/tui/banner.js +1 -1
  63. package/src/utils/fs.js +27 -0
@@ -1,753 +1,792 @@
1
- #!/usr/bin/env node
2
- /**
3
- * architecture_extractor.js — Tribunal Architecture Fact Extractor
4
- * ═════════════════════════════════════════════════════════════════════════════
5
- * Deterministic static analysis engine that extracts the full Tribunal
6
- * Architectural Fact Model (TAFM) directly from repository source code.
7
- *
8
- * Extracts:
9
- * - Services, Modules, and Entry Points
10
- * - HTTP / API Endpoints (Express, Next.js, Fastify, Hono, Koa)
11
- * - Data Stores & Queries (Prisma, Drizzle, Mongoose, Sequelize, Raw SQL, Redis)
12
- * - Message Queues & Events (BullMQ, RabbitMQ, Kafka, EventEmitters)
13
- * - External Services (fetch, axios, Stripe, AWS SDK, Firebase, SendGrid)
14
- * - Authentication Boundaries (JWT, next-auth, Passport, Clerk, bcrypt)
15
- * - Call-graphs, Failure Modes (try/catch, timeouts, retries), and Trust Zones
16
- *
17
- * Emits: .agent/history/architecture-model.json with verifiable provenance & L1-L3 confidence.
18
- * Zero external dependencies.
19
- */
20
-
21
- 'use strict';
22
-
23
- const fs = require('fs');
24
- const path = require('path');
25
- const crypto = require('crypto');
26
- const { GREEN, CYAN, BOLD, DIM, RESET, BOX, banner, timer, formatMs } = require('./_colors');
27
-
28
- // ── Default Exclusions ────────────────────────────────────────────────────────
29
- const EXCLUDED_DIRS = new Set([
30
- 'node_modules',
31
- '.git',
32
- '.next',
33
- 'dist',
34
- 'build',
35
- 'coverage',
36
- '.agent',
37
- '.agents',
38
- 'archify',
39
- 'artifacts',
40
- 'target',
41
- ]);
42
-
43
- const SUPPORTED_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
44
-
45
- // ── Helpers ───────────────────────────────────────────────────────────────────
46
- function getSha256(content) {
47
- return crypto.createHash('sha256').update(content).digest('hex');
48
- }
49
-
50
- function normalizePosixPath(filePath) {
51
- return filePath.replace(/\\/g, '/');
52
- }
53
-
54
- function walkRepository(dir, rootDir = dir, results = []) {
55
- if (!fs.existsSync(dir)) return results;
56
- let entries;
57
- try {
58
- entries = fs.readdirSync(dir, { withFileTypes: true });
59
- } catch (_e) {
60
- return results;
61
- }
62
-
63
- for (const entry of entries) {
64
- const fullPath = path.join(dir, entry.name);
65
- const relPath = normalizePosixPath(path.relative(rootDir, fullPath));
66
-
67
- if (entry.isDirectory()) {
68
- if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
69
- walkRepository(fullPath, rootDir, results);
70
- }
71
- } else if (entry.isFile()) {
72
- const ext = path.extname(entry.name).toLowerCase();
73
- if (SUPPORTED_EXTS.has(ext)) {
74
- results.push({ fullPath, relPath, ext });
75
- }
76
- }
77
- }
78
- return results;
79
- }
80
-
81
- // ── Extraction Patterns ───────────────────────────────────────────────────────
82
- const ROUTE_DETECTORS = [
83
- {
84
- regex: /(?:app|router|server)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
85
- framework: 'express_or_fastify',
86
- },
87
- {
88
- regex: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g,
89
- framework: 'nextjs_app_router',
90
- },
91
- {
92
- regex: /\.(get|post|put|patch|delete)\s*\(\s*['"`](\/[^'"`]*)['"`]/gi,
93
- framework: 'hono_or_elysia',
94
- },
95
- ];
96
-
97
- const DATASTORE_DETECTORS = [
98
- {
99
- regex: /prisma\.(\w+)\.(findMany|findUnique|findFirst|create|update|delete|upsert|aggregate|groupBy|count)\s*\(/g,
100
- engine: 'postgres_prisma',
101
- kind: 'datastore',
102
- },
103
- {
104
- regex: /(?:db|drizzle)\.(select|insert|update|delete)\s*\(/g,
105
- engine: 'sql_drizzle',
106
- kind: 'datastore',
107
- },
108
- {
109
- regex: /\.(query|execute|raw|sql)\s*\(\s*['"`]?\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE)/gi,
110
- engine: 'raw_sql',
111
- kind: 'datastore',
112
- },
113
- {
114
- regex: /(?:redis|ioredis|createClient)\s*\(/gi,
115
- engine: 'redis_cache',
116
- kind: 'cache',
117
- },
118
- {
119
- regex: /(?:redis|client|redisClient|kv)\.(?:get|set|del|hget|hset|expire|incr|lpush|rpush|publish|subscribe)\s*\(|\.(?:hget|hset|expire|incr|lpush|rpush)\s*\(/g,
120
- engine: 'redis_ops',
121
- kind: 'cache',
122
- },
123
- ];
124
-
125
- const QUEUE_DETECTORS = [
126
- {
127
- regex: /new\s+(?:Queue|Worker|QueueScheduler)\s*\(\s*['"`](\w+)['"`]/g,
128
- engine: 'bullmq',
129
- kind: 'queue',
130
- },
131
- {
132
- regex: /(?:amqplib|amqp\.connect)\s*\(/g,
133
- engine: 'rabbitmq',
134
- kind: 'queue',
135
- },
136
- {
137
- regex: /new\s+Kafka\s*\(/g,
138
- engine: 'kafka',
139
- kind: 'queue',
140
- },
141
- {
142
- regex: /\.(?:emit|on|once)\s*\(\s*['"`]([\w:.-]+)['"`]/g,
143
- engine: 'event_emitter',
144
- kind: 'event_bus',
145
- },
146
- ];
147
-
148
- const EXTERNAL_SERVICE_DETECTORS = [
149
- {
150
- regex: /(?:fetch|axios|got|ky|ofetch)\s*\(\s*['"`]?(https?:\/\/[^'"`\s,)]+)/gi,
151
- type: 'http_client',
152
- },
153
- {
154
- regex: /new\s+(?:AWS\.)?(?:S3|DynamoDB|SQS|SNS|Lambda|SES|CloudWatch)\s*\(/g,
155
- type: 'aws_sdk',
156
- },
157
- {
158
- regex: /@aws-sdk\/client-(\w+)/g,
159
- type: 'aws_sdk',
160
- },
161
- {
162
- regex: /(?:stripe|Stripe)\s*\(/g,
163
- type: 'stripe_payments',
164
- },
165
- {
166
- regex: /(?:sendgrid|twilio|resend)\s*/gi,
167
- type: 'messaging_saas',
168
- },
169
- ];
170
-
171
- const AUTH_DETECTORS = [
172
- {
173
- regex: /(?:jwt|jsonwebtoken)\.(?:sign|verify|decode)\s*\(/g,
174
- authKind: 'jwt',
175
- },
176
- {
177
- regex: /(?:NextAuth|getServerSession|auth\(\)|getSession)\s*\(/g,
178
- authKind: 'next_auth',
179
- },
180
- {
181
- regex: /passport\.(?:authenticate|use|initialize)\s*\(/g,
182
- authKind: 'passport',
183
- },
184
- {
185
- regex: /(?:bcrypt|argon2)\.(?:hash|compare|verify)\s*\(/g,
186
- authKind: 'password_hashing',
187
- },
188
- {
189
- regex: /(?:clerkMiddleware|currentUser|ClerkProvider)\s*/g,
190
- authKind: 'clerk',
191
- },
192
- ];
193
-
194
- const IMPORT_DETECTOR = /(?:import\s+(?:[\w*\s{},]+)\s+from\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\))/g;
195
-
196
- // ── Architecture Extractor Class ──────────────────────────────────────────────
197
- class ArchitectureExtractor {
198
- constructor(repoRoot = process.cwd()) {
199
- this.repoRoot = path.resolve(repoRoot);
200
- this.entities = new Map();
201
- this.relations = new Map();
202
- this.trustBoundaries = new Map();
203
- this.failurePaths = [];
204
- this.contradictions = [];
205
- }
206
-
207
- registerEntity(entity) {
208
- if (!this.entities.has(entity.id)) {
209
- this.entities.set(entity.id, entity);
210
- } else {
211
- const existing = this.entities.get(entity.id);
212
- if (entity.sources && entity.sources.length) {
213
- existing.sources = [...(existing.sources || []), ...entity.sources];
214
- }
215
- }
216
- return this.entities.get(entity.id);
217
- }
218
-
219
- registerRelation(relation) {
220
- const key = `${relation.sourceId}->${relation.targetId}:${relation.relationType}`;
221
- if (!this.relations.has(key)) {
222
- this.relations.set(key, { id: `rel.${crypto.createHash('md5').update(key).digest('hex').slice(0, 10)}`, ...relation });
223
- } else {
224
- const existing = this.relations.get(key);
225
- if (relation.evidence) {
226
- existing.evidence = [...(existing.evidence || []), ...relation.evidence];
227
- }
228
- }
229
- return this.relations.get(key);
230
- }
231
-
232
- scan() {
233
- const files = walkRepository(this.repoRoot, this.repoRoot);
234
-
235
- // Initialize Default Trust Zones
236
- this.initDefaultTrustBoundaries();
237
-
238
- // Pass 1: Extract module entities and file contents
239
- const fileDataMap = new Map();
240
- for (const file of files) {
241
- let content;
242
- try {
243
- content = fs.readFileSync(file.fullPath, 'utf8');
244
- } catch (_e) {
245
- continue;
246
- }
247
- const lines = content.split('\n');
248
- const hash = getSha256(content);
249
- fileDataMap.set(file.relPath, { ...file, content, lines, hash });
250
-
251
- // Register Module Entity
252
- const entityId = `mod.${file.relPath.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
253
- const isTest = file.relPath.includes('test') || file.relPath.includes('spec');
254
- const isRoute = file.relPath.includes('route') || file.relPath.includes('api') || file.relPath.includes('controllers');
255
- const isSkill = file.relPath.startsWith('skills/') || file.relPath.startsWith('.agents/skills/');
256
-
257
- this.registerEntity({
258
- id: entityId,
259
- kind: isRoute ? 'endpoint' : isTest ? 'test_suite' : 'module',
260
- name: path.basename(file.relPath),
261
- scope: path.dirname(file.relPath),
262
- role: isSkill ? 'Skill Template / Resource' : isRoute ? 'API Route Handler' : isTest ? 'Test Suite' : 'Internal Component',
263
- tags: [isTest ? 'test' : isSkill ? 'skill' : 'core', path.extname(file.relPath).replace('.', '')],
264
- trustZone: isRoute ? 'dmz_gateway' : 'internal_service',
265
- sources: [
266
- {
267
- file: file.relPath,
268
- startLine: 1,
269
- endLine: Math.min(lines.length, 50),
270
- contentHash: hash.substring(0, 16),
271
- extractionMethod: 'ast_visitor',
272
- confidence: 'L1',
273
- verified: true,
274
- verifiedAt: new Date().toISOString(),
275
- },
276
- ],
277
- blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.1 },
278
- confidence: 'L1',
279
- temporalState: 'current',
280
- });
281
- }
282
-
283
- // Pass 2: Extract Architectural Features & Relationships
284
- for (const [relPath, fileInfo] of fileDataMap.entries()) {
285
- const sourceEntityId = `mod.${relPath.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
286
- const { content, lines } = fileInfo;
287
-
288
- // 1. Analyze Imports & Internal Call Dependencies
289
- IMPORT_DETECTOR.lastIndex = 0;
290
- let impMatch;
291
- while ((impMatch = IMPORT_DETECTOR.exec(content)) !== null) {
292
- const importTarget = impMatch[1] || impMatch[2];
293
- if (importTarget && importTarget.startsWith('.')) {
294
- // Local import
295
- const resolvedRel = normalizePosixPath(
296
- path.relative(this.repoRoot, path.resolve(path.dirname(fileInfo.fullPath), importTarget))
297
- );
298
- // Try finding matching file
299
- const targetKey = Array.from(fileDataMap.keys()).find(
300
- (k) => k === resolvedRel || k.startsWith(resolvedRel + '.') || k === resolvedRel + '/index.js' || k === resolvedRel + '/index.ts'
301
- );
302
-
303
- if (targetKey) {
304
- const targetEntityId = `mod.${targetKey.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
305
- const lineNum = content.slice(0, impMatch.index).split('\n').length;
306
- this.registerRelation({
307
- sourceId: sourceEntityId,
308
- targetId: targetEntityId,
309
- relationType: 'imports',
310
- protocol: 'In-Memory Module',
311
- evidence: [
312
- {
313
- file: relPath,
314
- startLine: lineNum,
315
- endLine: lineNum,
316
- symbol: importTarget,
317
- snippet: lines[lineNum - 1]?.trim(),
318
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
319
- extractionMethod: 'ast_visitor',
320
- confidence: 'L1',
321
- verified: true,
322
- verifiedAt: new Date().toISOString(),
323
- },
324
- ],
325
- confidence: 'L1',
326
- });
327
- }
328
- }
329
- }
330
-
331
- // 2. Analyze API Routes (JS/TS web endpoints)
332
- if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(fileInfo.ext)) {
333
- for (const detector of ROUTE_DETECTORS) {
334
- detector.regex.lastIndex = 0;
335
- let routeMatch;
336
- while ((routeMatch = detector.regex.exec(content)) !== null) {
337
- const method = routeMatch[1]?.toUpperCase() || 'GET';
338
- const endpointPath = routeMatch[2] || `/${relPath.replace(/^src\//, '').replace(/\.[^.]+$/, '')}`;
339
- const lineNum = content.slice(0, routeMatch.index).split('\n').length;
340
-
341
- const endpointEntityId = `endpoint.${method.toLowerCase()}.${endpointPath.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
342
- this.registerEntity({
343
- id: endpointEntityId,
344
- kind: 'endpoint',
345
- name: `${method} ${endpointPath}`,
346
- scope: relPath,
347
- role: `Public/Internal Endpoint (${detector.framework})`,
348
- tags: ['api', method.toLowerCase()],
349
- trustZone: 'dmz_gateway',
350
- sources: [
351
- {
352
- file: relPath,
353
- startLine: lineNum,
354
- endLine: Math.min(lineNum + 10, lines.length),
355
- snippet: lines[lineNum - 1]?.trim(),
356
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
357
- extractionMethod: 'ast_visitor',
358
- confidence: 'L1',
359
- verified: true,
360
- verifiedAt: new Date().toISOString(),
361
- },
362
- ],
363
- blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.2 },
364
- confidence: 'L1',
365
- temporalState: 'current',
366
- });
367
-
368
- this.registerRelation({
369
- sourceId: endpointEntityId,
370
- targetId: sourceEntityId,
371
- relationType: 'routes_to',
372
- protocol: 'HTTP',
373
- evidence: [
374
- {
375
- file: relPath,
376
- startLine: lineNum,
377
- endLine: lineNum,
378
- snippet: lines[lineNum - 1]?.trim(),
379
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
380
- extractionMethod: 'ast_visitor',
381
- confidence: 'L1',
382
- verified: true,
383
- verifiedAt: new Date().toISOString(),
384
- },
385
- ],
386
- confidence: 'L1',
387
- });
388
- }
389
- }
390
- }
391
-
392
- // 3. Analyze Data Stores (Prisma, SQL, Redis)
393
- for (const detector of DATASTORE_DETECTORS) {
394
- detector.regex.lastIndex = 0;
395
- let dbMatch;
396
- while ((dbMatch = detector.regex.exec(content)) !== null) {
397
- const lineNum = content.slice(0, dbMatch.index).split('\n').length;
398
- const storeId = `datastore.${detector.engine}`;
399
-
400
- this.registerEntity({
401
- id: storeId,
402
- kind: detector.kind === 'cache' ? 'cache' : 'datastore',
403
- name: detector.engine.toUpperCase(),
404
- scope: 'storage',
405
- role: `${detector.kind === 'cache' ? 'Ephemeral In-Memory Cache' : 'Persistent Relational/NoSQL Database'}`,
406
- tags: ['persistence', detector.engine],
407
- trustZone: 'isolated_datastore',
408
- sources: [
409
- {
410
- file: relPath,
411
- startLine: lineNum,
412
- endLine: lineNum,
413
- snippet: lines[lineNum - 1]?.trim(),
414
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
415
- extractionMethod: 'ast_visitor',
416
- confidence: 'L1',
417
- verified: true,
418
- verifiedAt: new Date().toISOString(),
419
- },
420
- ],
421
- blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.7 },
422
- confidence: 'L1',
423
- temporalState: 'current',
424
- });
425
-
426
- this.registerRelation({
427
- sourceId: sourceEntityId,
428
- targetId: storeId,
429
- relationType: detector.kind === 'cache' ? 'reads' : 'writes',
430
- protocol: detector.engine.includes('redis') ? 'Redis-RESP' : 'SQL-Wire',
431
- evidence: [
432
- {
433
- file: relPath,
434
- startLine: lineNum,
435
- endLine: lineNum,
436
- snippet: lines[lineNum - 1]?.trim(),
437
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
438
- extractionMethod: 'ast_visitor',
439
- confidence: 'L1',
440
- verified: true,
441
- verifiedAt: new Date().toISOString(),
442
- },
443
- ],
444
- confidence: 'L1',
445
- });
446
- }
447
- }
448
-
449
- // 4. Analyze Message Queues & Event Busses
450
- for (const detector of QUEUE_DETECTORS) {
451
- detector.regex.lastIndex = 0;
452
- let qMatch;
453
- while ((qMatch = detector.regex.exec(content)) !== null) {
454
- const queueName = qMatch[1] || detector.engine;
455
- const lineNum = content.slice(0, qMatch.index).split('\n').length;
456
- const qEntityId = `queue.${detector.engine}.${queueName}`;
457
-
458
- this.registerEntity({
459
- id: qEntityId,
460
- kind: detector.kind === 'event_bus' ? 'event_bus' : 'queue',
461
- name: `${detector.engine}: ${queueName}`,
462
- scope: 'async-broker',
463
- role: 'Asynchronous Job Queue / Message Channel',
464
- tags: ['messaging', detector.engine],
465
- trustZone: 'internal_service',
466
- sources: [
467
- {
468
- file: relPath,
469
- startLine: lineNum,
470
- endLine: lineNum,
471
- snippet: lines[lineNum - 1]?.trim(),
472
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
473
- extractionMethod: 'ast_visitor',
474
- confidence: 'L1',
475
- verified: true,
476
- verifiedAt: new Date().toISOString(),
477
- },
478
- ],
479
- blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.4 },
480
- confidence: 'L1',
481
- temporalState: 'current',
482
- });
483
-
484
- this.registerRelation({
485
- sourceId: sourceEntityId,
486
- targetId: qEntityId,
487
- relationType: content.includes('.on(') || content.includes('.subscribe(') ? 'subscribes' : 'publishes',
488
- protocol: detector.engine,
489
- evidence: [
490
- {
491
- file: relPath,
492
- startLine: lineNum,
493
- endLine: lineNum,
494
- snippet: lines[lineNum - 1]?.trim(),
495
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
496
- extractionMethod: 'ast_visitor',
497
- confidence: 'L1',
498
- verified: true,
499
- verifiedAt: new Date().toISOString(),
500
- },
501
- ],
502
- confidence: 'L1',
503
- });
504
- }
505
- }
506
-
507
- // 5. Analyze External Services (Third-Party SaaS & Clouds)
508
- for (const detector of EXTERNAL_SERVICE_DETECTORS) {
509
- detector.regex.lastIndex = 0;
510
- let extMatch;
511
- while ((extMatch = detector.regex.exec(content)) !== null) {
512
- const lineNum = content.slice(0, extMatch.index).split('\n').length;
513
- const serviceName = extMatch[1] || detector.type;
514
- const extEntityId = `ext.${crypto.createHash('md5').update(serviceName).digest('hex').slice(0, 8)}`;
515
-
516
- this.registerEntity({
517
- id: extEntityId,
518
- kind: 'external_api',
519
- name: serviceName.length > 35 ? serviceName.slice(0, 32) + '...' : serviceName,
520
- scope: 'third_party',
521
- role: `External Third-Party Service (${detector.type})`,
522
- tags: ['external', detector.type],
523
- trustZone: 'external_untrusted',
524
- sources: [
525
- {
526
- file: relPath,
527
- startLine: lineNum,
528
- endLine: lineNum,
529
- snippet: lines[lineNum - 1]?.trim(),
530
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
531
- extractionMethod: 'ast_visitor',
532
- confidence: 'L1',
533
- verified: true,
534
- verifiedAt: new Date().toISOString(),
535
- },
536
- ],
537
- blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.3 },
538
- confidence: 'L1',
539
- temporalState: 'current',
540
- });
541
-
542
- // Check for Failure Resilience (try/catch, timeout, retry)
543
- const surroundingContext = lines.slice(Math.max(0, lineNum - 5), Math.min(lines.length, lineNum + 5)).join('\n');
544
- const hasTryCatch = surroundingContext.includes('try') || surroundingContext.includes('.catch');
545
- const hasTimeout = surroundingContext.includes('timeout') || surroundingContext.includes('AbortController');
546
-
547
- this.registerRelation({
548
- sourceId: sourceEntityId,
549
- targetId: extEntityId,
550
- relationType: 'calls',
551
- protocol: 'HTTPS',
552
- evidence: [
553
- {
554
- file: relPath,
555
- startLine: lineNum,
556
- endLine: lineNum,
557
- snippet: lines[lineNum - 1]?.trim(),
558
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
559
- extractionMethod: 'ast_visitor',
560
- confidence: 'L1',
561
- verified: true,
562
- verifiedAt: new Date().toISOString(),
563
- },
564
- ],
565
- confidence: 'L1',
566
- failureMode: {
567
- hasTimeout,
568
- hasRetry: surroundingContext.includes('retry'),
569
- hasCircuitBreaker: surroundingContext.includes('circuitBreaker') || surroundingContext.includes('opossum'),
570
- fallbackTarget: hasTryCatch ? 'handled_exception' : undefined,
571
- },
572
- });
573
- }
574
- }
575
-
576
- // 6. Analyze Authentication Boundaries
577
- for (const detector of AUTH_DETECTORS) {
578
- detector.regex.lastIndex = 0;
579
- let authMatch;
580
- while ((authMatch = detector.regex.exec(content)) !== null) {
581
- const lineNum = content.slice(0, authMatch.index).split('\n').length;
582
- const authEntityId = `auth.${detector.authKind}`;
583
-
584
- this.registerEntity({
585
- id: authEntityId,
586
- kind: 'identity_provider',
587
- name: `Auth: ${detector.authKind.toUpperCase()}`,
588
- scope: 'security',
589
- role: 'Authentication & Session Token Verification',
590
- tags: ['security', 'auth', detector.authKind],
591
- trustZone: 'dmz_gateway',
592
- sources: [
593
- {
594
- file: relPath,
595
- startLine: lineNum,
596
- endLine: lineNum,
597
- snippet: lines[lineNum - 1]?.trim(),
598
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
599
- extractionMethod: 'ast_visitor',
600
- confidence: 'L1',
601
- verified: true,
602
- verifiedAt: new Date().toISOString(),
603
- },
604
- ],
605
- blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.8 },
606
- confidence: 'L1',
607
- temporalState: 'current',
608
- });
609
-
610
- this.registerRelation({
611
- sourceId: sourceEntityId,
612
- targetId: authEntityId,
613
- relationType: 'authenticates',
614
- protocol: 'Crypto-Token',
615
- evidence: [
616
- {
617
- file: relPath,
618
- startLine: lineNum,
619
- endLine: lineNum,
620
- snippet: lines[lineNum - 1]?.trim(),
621
- contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
622
- extractionMethod: 'ast_visitor',
623
- confidence: 'L1',
624
- verified: true,
625
- verifiedAt: new Date().toISOString(),
626
- },
627
- ],
628
- confidence: 'L1',
629
- });
630
- }
631
- }
632
- }
633
-
634
- // Populate Trust Zone Members
635
- for (const entity of this.entities.values()) {
636
- const zone = this.trustBoundaries.get(entity.trustZone);
637
- if (zone && !zone.members.includes(entity.id)) {
638
- zone.members.push(entity.id);
639
- }
640
- }
641
-
642
- return this.buildModel();
643
- }
644
-
645
- initDefaultTrustBoundaries() {
646
- const boundaries = [
647
- {
648
- id: 'public_untrusted',
649
- name: 'Public Untrusted Zone',
650
- tier: 'public_untrusted',
651
- enforcesAuth: false,
652
- members: [],
653
- ingressRules: [{ allowedSources: ['*'] }],
654
- },
655
- {
656
- id: 'dmz_gateway',
657
- name: 'DMZ & API Gateway',
658
- tier: 'dmz_gateway',
659
- enforcesAuth: true,
660
- authMethod: 'jwt',
661
- members: [],
662
- ingressRules: [{ allowedSources: ['public_untrusted'] }],
663
- },
664
- {
665
- id: 'internal_service',
666
- name: 'Internal Service Mesh',
667
- tier: 'authenticated_service',
668
- enforcesAuth: true,
669
- members: [],
670
- ingressRules: [{ allowedSources: ['dmz_gateway', 'internal_service'] }],
671
- },
672
- {
673
- id: 'isolated_datastore',
674
- name: 'Isolated Persistent Datastore',
675
- tier: 'isolated_datastore',
676
- enforcesAuth: true,
677
- members: [],
678
- ingressRules: [{ allowedSources: ['internal_service'] }],
679
- },
680
- {
681
- id: 'external_untrusted',
682
- name: 'External Third-Party APIs',
683
- tier: 'public_untrusted',
684
- enforcesAuth: false,
685
- members: [],
686
- ingressRules: [{ allowedSources: ['internal_service'] }],
687
- },
688
- ];
689
-
690
- for (const b of boundaries) {
691
- this.trustBoundaries.set(b.id, b);
692
- }
693
- }
694
-
695
- buildModel() {
696
- const entitiesArr = Array.from(this.entities.values());
697
- const relationsArr = Array.from(this.relations.values());
698
- const trustArr = Array.from(this.trustBoundaries.values());
699
-
700
- return {
701
- schemaVersion: 1,
702
- engine: 'tribunal-architecture-intelligence',
703
- generatedAt: new Date().toISOString(),
704
- repositoryRoot: this.repoRoot,
705
- summary: {
706
- totalEntities: entitiesArr.length,
707
- totalRelations: relationsArr.length,
708
- totalTrustZones: trustArr.length,
709
- endpointsCount: entitiesArr.filter((e) => e.kind === 'endpoint').length,
710
- datastoresCount: entitiesArr.filter((e) => e.kind === 'datastore' || e.kind === 'cache').length,
711
- queuesCount: entitiesArr.filter((e) => e.kind === 'queue' || e.kind === 'event_bus').length,
712
- externalServicesCount: entitiesArr.filter((e) => e.kind === 'external_api').length,
713
- },
714
- entities: entitiesArr,
715
- relationships: relationsArr,
716
- trustBoundaries: trustArr,
717
- contradictions: this.contradictions,
718
- };
719
- }
720
-
721
- save(outputPath) {
722
- const defaultPath = path.join(this.repoRoot, '.agent', 'history', 'architecture-model.json');
723
- const dest = outputPath || defaultPath;
724
- fs.mkdirSync(path.dirname(dest), { recursive: true });
725
- const model = this.buildModel();
726
- fs.writeFileSync(dest, JSON.stringify(model, null, 2), 'utf8');
727
- return dest;
728
- }
729
- }
730
-
731
- // ── Standalone CLI ────────────────────────────────────────────────────────────
732
- if (require.main === module) {
733
- const t = timer();
734
- const repoRoot = process.argv[2] || process.cwd();
735
- console.log(banner('Tribunal Architecture Fact Extractor (TAFE)'));
736
- console.log(` ${CYAN}Scanning repository:${RESET} ${repoRoot}`);
737
-
738
- const extractor = new ArchitectureExtractor(repoRoot);
739
- const model = extractor.scan();
740
- const savedPath = extractor.save();
741
-
742
- console.log(` ${GREEN}${BOX.check} Fact Extraction Complete${RESET} in ${formatMs(t())}`);
743
- console.log(` ${DIM}Entities:${RESET} ${BOLD}${model.summary.totalEntities}${RESET}`);
744
- console.log(` ${DIM}Relationships:${RESET} ${BOLD}${model.summary.totalRelations}${RESET}`);
745
- console.log(` ${DIM}Endpoints:${RESET} ${BOLD}${model.summary.endpointsCount}${RESET}`);
746
- console.log(` ${DIM}Data Stores:${RESET} ${BOLD}${model.summary.datastoresCount}${RESET}`);
747
- console.log(` ${DIM}Saved Model:${RESET} ${CYAN}${savedPath}${RESET}\n`);
748
- }
749
-
750
- module.exports = {
751
- ArchitectureExtractor,
752
- walkRepository,
753
- };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * architecture_extractor.js — Tribunal Architecture Fact Extractor
4
+ * ═════════════════════════════════════════════════════════════════════════════
5
+ * Deterministic static analysis engine that extracts the full Tribunal
6
+ * Architectural Fact Model (TAFM) directly from repository source code.
7
+ *
8
+ * Extracts:
9
+ * - Services, Modules, and Entry Points
10
+ * - HTTP / API Endpoints (Express, Next.js, Fastify, Hono, Koa)
11
+ * - Data Stores & Queries (Prisma, Drizzle, Mongoose, Sequelize, Raw SQL, Redis)
12
+ * - Message Queues & Events (BullMQ, RabbitMQ, Kafka, EventEmitters)
13
+ * - External Services (fetch, axios, Stripe, AWS SDK, Firebase, SendGrid)
14
+ * - Authentication Boundaries (JWT, next-auth, Passport, Clerk, bcrypt)
15
+ * - Call-graphs, Failure Modes (try/catch, timeouts, retries), and Trust Zones
16
+ *
17
+ * Emits: .agent/history/architecture-model.json with verifiable provenance & L1-L3 confidence.
18
+ * Zero external dependencies.
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const crypto = require('crypto');
26
+ const { GREEN, CYAN, BOLD, DIM, RESET, BOX, banner, timer, formatMs } = require('./_colors');
27
+
28
+ // ── Default Exclusions ────────────────────────────────────────────────────────
29
+ const EXCLUDED_DIRS = new Set([
30
+ 'node_modules',
31
+ '.git',
32
+ '.next',
33
+ 'dist',
34
+ 'build',
35
+ 'coverage',
36
+ '.agent',
37
+ '.agents',
38
+ 'archify',
39
+ 'artifacts',
40
+ 'target',
41
+ ]);
42
+
43
+ const SUPPORTED_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
44
+
45
+ // ── Helpers ───────────────────────────────────────────────────────────────────
46
+ function getSha256(content) {
47
+ return crypto.createHash('sha256').update(content).digest('hex');
48
+ }
49
+
50
+ function normalizePosixPath(filePath) {
51
+ return filePath.replace(/\\/g, '/');
52
+ }
53
+
54
+ function walkRepository(dir, rootDir = dir, results = []) {
55
+ if (!fs.existsSync(dir)) return results;
56
+ let entries;
57
+ try {
58
+ entries = fs.readdirSync(dir, { withFileTypes: true });
59
+ } catch (_e) {
60
+ return results;
61
+ }
62
+
63
+ for (const entry of entries) {
64
+ const fullPath = path.join(dir, entry.name);
65
+ const relPath = normalizePosixPath(path.relative(rootDir, fullPath));
66
+
67
+ if (entry.isDirectory()) {
68
+ if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
69
+ walkRepository(fullPath, rootDir, results);
70
+ }
71
+ } else if (entry.isFile()) {
72
+ const ext = path.extname(entry.name).toLowerCase();
73
+ if (SUPPORTED_EXTS.has(ext)) {
74
+ results.push({ fullPath, relPath, ext });
75
+ }
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+
81
+ // ── Extraction Patterns ───────────────────────────────────────────────────────
82
+ const ROUTE_DETECTORS = [
83
+ {
84
+ regex: /(?:app|router|server)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
85
+ framework: 'express_or_fastify',
86
+ },
87
+ {
88
+ regex: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g,
89
+ framework: 'nextjs_app_router',
90
+ },
91
+ {
92
+ regex: /\.(get|post|put|patch|delete)\s*\(\s*['"`](\/[^'"`]*)['"`]/gi,
93
+ framework: 'hono_or_elysia',
94
+ },
95
+ ];
96
+
97
+ const DATASTORE_DETECTORS = [
98
+ {
99
+ regex:
100
+ /prisma\.(\w+)\.(findMany|findUnique|findFirst|create|update|delete|upsert|aggregate|groupBy|count)\s*\(/g,
101
+ engine: 'postgres_prisma',
102
+ kind: 'datastore',
103
+ },
104
+ {
105
+ regex: /(?:db|drizzle)\.(select|insert|update|delete)\s*\(/g,
106
+ engine: 'sql_drizzle',
107
+ kind: 'datastore',
108
+ },
109
+ {
110
+ regex:
111
+ /\.(query|execute|raw|sql)\s*\(\s*['"`]?\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE)/gi,
112
+ engine: 'raw_sql',
113
+ kind: 'datastore',
114
+ },
115
+ {
116
+ regex: /(?:redis|ioredis|createClient)\s*\(/gi,
117
+ engine: 'redis_cache',
118
+ kind: 'cache',
119
+ },
120
+ {
121
+ regex:
122
+ /(?:redis|client|redisClient|kv)\.(?:get|set|del|hget|hset|expire|incr|lpush|rpush|publish|subscribe)\s*\(|\.(?:hget|hset|expire|incr|lpush|rpush)\s*\(/g,
123
+ engine: 'redis_ops',
124
+ kind: 'cache',
125
+ },
126
+ ];
127
+
128
+ const QUEUE_DETECTORS = [
129
+ {
130
+ regex: /new\s+(?:Queue|Worker|QueueScheduler)\s*\(\s*['"`](\w+)['"`]/g,
131
+ engine: 'bullmq',
132
+ kind: 'queue',
133
+ },
134
+ {
135
+ regex: /(?:amqplib|amqp\.connect)\s*\(/g,
136
+ engine: 'rabbitmq',
137
+ kind: 'queue',
138
+ },
139
+ {
140
+ regex: /new\s+Kafka\s*\(/g,
141
+ engine: 'kafka',
142
+ kind: 'queue',
143
+ },
144
+ {
145
+ regex: /\.(?:emit|on|once)\s*\(\s*['"`]([\w:.-]+)['"`]/g,
146
+ engine: 'event_emitter',
147
+ kind: 'event_bus',
148
+ },
149
+ ];
150
+
151
+ const EXTERNAL_SERVICE_DETECTORS = [
152
+ {
153
+ regex: /(?:fetch|axios|got|ky|ofetch)\s*\(\s*['"`]?(https?:\/\/[^'"`\s,)]+)/gi,
154
+ type: 'http_client',
155
+ },
156
+ {
157
+ regex: /new\s+(?:AWS\.)?(?:S3|DynamoDB|SQS|SNS|Lambda|SES|CloudWatch)\s*\(/g,
158
+ type: 'aws_sdk',
159
+ },
160
+ {
161
+ regex: /@aws-sdk\/client-(\w+)/g,
162
+ type: 'aws_sdk',
163
+ },
164
+ {
165
+ regex: /(?:stripe|Stripe)\s*\(/g,
166
+ type: 'stripe_payments',
167
+ },
168
+ {
169
+ regex: /(?:sendgrid|twilio|resend)\s*/gi,
170
+ type: 'messaging_saas',
171
+ },
172
+ ];
173
+
174
+ const AUTH_DETECTORS = [
175
+ {
176
+ regex: /(?:jwt|jsonwebtoken)\.(?:sign|verify|decode)\s*\(/g,
177
+ authKind: 'jwt',
178
+ },
179
+ {
180
+ regex: /(?:NextAuth|getServerSession|auth\(\)|getSession)\s*\(/g,
181
+ authKind: 'next_auth',
182
+ },
183
+ {
184
+ regex: /passport\.(?:authenticate|use|initialize)\s*\(/g,
185
+ authKind: 'passport',
186
+ },
187
+ {
188
+ regex: /(?:bcrypt|argon2)\.(?:hash|compare|verify)\s*\(/g,
189
+ authKind: 'password_hashing',
190
+ },
191
+ {
192
+ regex: /(?:clerkMiddleware|currentUser|ClerkProvider)\s*/g,
193
+ authKind: 'clerk',
194
+ },
195
+ ];
196
+
197
+ const IMPORT_DETECTOR =
198
+ /(?:import\s+(?:[\w*\s{},]+)\s+from\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\))/g;
199
+
200
+ // ── Architecture Extractor Class ──────────────────────────────────────────────
201
+ class ArchitectureExtractor {
202
+ constructor(repoRoot = process.cwd()) {
203
+ this.repoRoot = path.resolve(repoRoot);
204
+ this.entities = new Map();
205
+ this.relations = new Map();
206
+ this.trustBoundaries = new Map();
207
+ this.failurePaths = [];
208
+ this.contradictions = [];
209
+ }
210
+
211
+ registerEntity(entity) {
212
+ if (!this.entities.has(entity.id)) {
213
+ this.entities.set(entity.id, entity);
214
+ } else {
215
+ const existing = this.entities.get(entity.id);
216
+ if (entity.sources && entity.sources.length) {
217
+ existing.sources = [...(existing.sources || []), ...entity.sources];
218
+ }
219
+ }
220
+ return this.entities.get(entity.id);
221
+ }
222
+
223
+ registerRelation(relation) {
224
+ const key = `${relation.sourceId}->${relation.targetId}:${relation.relationType}`;
225
+ if (!this.relations.has(key)) {
226
+ this.relations.set(key, {
227
+ id: `rel.${crypto.createHash('md5').update(key).digest('hex').slice(0, 10)}`,
228
+ ...relation,
229
+ });
230
+ } else {
231
+ const existing = this.relations.get(key);
232
+ if (relation.evidence) {
233
+ existing.evidence = [...(existing.evidence || []), ...relation.evidence];
234
+ }
235
+ }
236
+ return this.relations.get(key);
237
+ }
238
+
239
+ scan() {
240
+ const files = walkRepository(this.repoRoot, this.repoRoot);
241
+
242
+ // Initialize Default Trust Zones
243
+ this.initDefaultTrustBoundaries();
244
+
245
+ // Pass 1: Extract module entities and file contents
246
+ const fileDataMap = new Map();
247
+ for (const file of files) {
248
+ let content;
249
+ try {
250
+ content = fs.readFileSync(file.fullPath, 'utf8');
251
+ } catch (_e) {
252
+ continue;
253
+ }
254
+ const lines = content.split('\n');
255
+ const hash = getSha256(content);
256
+ fileDataMap.set(file.relPath, { ...file, content, lines, hash });
257
+
258
+ // Register Module Entity
259
+ const entityId = `mod.${file.relPath.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
260
+ const isTest = file.relPath.includes('test') || file.relPath.includes('spec');
261
+ const isRoute =
262
+ file.relPath.includes('route') ||
263
+ file.relPath.includes('api') ||
264
+ file.relPath.includes('controllers');
265
+ const isSkill =
266
+ file.relPath.startsWith('skills/') || file.relPath.startsWith('.agents/skills/');
267
+
268
+ this.registerEntity({
269
+ id: entityId,
270
+ kind: isRoute ? 'endpoint' : isTest ? 'test_suite' : 'module',
271
+ name: path.basename(file.relPath),
272
+ scope: path.dirname(file.relPath),
273
+ role: isSkill
274
+ ? 'Skill Template / Resource'
275
+ : isRoute
276
+ ? 'API Route Handler'
277
+ : isTest
278
+ ? 'Test Suite'
279
+ : 'Internal Component',
280
+ tags: [
281
+ isTest ? 'test' : isSkill ? 'skill' : 'core',
282
+ path.extname(file.relPath).replace('.', ''),
283
+ ],
284
+ trustZone: isRoute ? 'dmz_gateway' : 'internal_service',
285
+ sources: [
286
+ {
287
+ file: file.relPath,
288
+ startLine: 1,
289
+ endLine: Math.min(lines.length, 50),
290
+ contentHash: hash.substring(0, 16),
291
+ extractionMethod: 'ast_visitor',
292
+ confidence: 'L1',
293
+ verified: true,
294
+ verifiedAt: new Date().toISOString(),
295
+ },
296
+ ],
297
+ blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.1 },
298
+ confidence: 'L1',
299
+ temporalState: 'current',
300
+ });
301
+ }
302
+
303
+ // Pass 2: Extract Architectural Features & Relationships
304
+ for (const [relPath, fileInfo] of fileDataMap.entries()) {
305
+ const sourceEntityId = `mod.${relPath.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
306
+ const { content, lines } = fileInfo;
307
+
308
+ // 1. Analyze Imports & Internal Call Dependencies
309
+ IMPORT_DETECTOR.lastIndex = 0;
310
+ let impMatch;
311
+ while ((impMatch = IMPORT_DETECTOR.exec(content)) !== null) {
312
+ const importTarget = impMatch[1] || impMatch[2];
313
+ if (importTarget && importTarget.startsWith('.')) {
314
+ // Local import
315
+ const resolvedRel = normalizePosixPath(
316
+ path.relative(
317
+ this.repoRoot,
318
+ path.resolve(path.dirname(fileInfo.fullPath), importTarget),
319
+ ),
320
+ );
321
+ // Try finding matching file
322
+ const targetKey = Array.from(fileDataMap.keys()).find(
323
+ k =>
324
+ k === resolvedRel ||
325
+ k.startsWith(resolvedRel + '.') ||
326
+ k === resolvedRel + '/index.js' ||
327
+ k === resolvedRel + '/index.ts',
328
+ );
329
+
330
+ if (targetKey) {
331
+ const targetEntityId = `mod.${targetKey.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
332
+ const lineNum = content.slice(0, impMatch.index).split('\n').length;
333
+ this.registerRelation({
334
+ sourceId: sourceEntityId,
335
+ targetId: targetEntityId,
336
+ relationType: 'imports',
337
+ protocol: 'In-Memory Module',
338
+ evidence: [
339
+ {
340
+ file: relPath,
341
+ startLine: lineNum,
342
+ endLine: lineNum,
343
+ symbol: importTarget,
344
+ snippet: lines[lineNum - 1]?.trim(),
345
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
346
+ extractionMethod: 'ast_visitor',
347
+ confidence: 'L1',
348
+ verified: true,
349
+ verifiedAt: new Date().toISOString(),
350
+ },
351
+ ],
352
+ confidence: 'L1',
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ // 2. Analyze API Routes (JS/TS web endpoints)
359
+ if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(fileInfo.ext)) {
360
+ for (const detector of ROUTE_DETECTORS) {
361
+ detector.regex.lastIndex = 0;
362
+ let routeMatch;
363
+ while ((routeMatch = detector.regex.exec(content)) !== null) {
364
+ const method = routeMatch[1]?.toUpperCase() || 'GET';
365
+ const endpointPath =
366
+ routeMatch[2] || `/${relPath.replace(/^src\//, '').replace(/\.[^.]+$/, '')}`;
367
+ const lineNum = content.slice(0, routeMatch.index).split('\n').length;
368
+
369
+ const endpointEntityId = `endpoint.${method.toLowerCase()}.${endpointPath.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
370
+ this.registerEntity({
371
+ id: endpointEntityId,
372
+ kind: 'endpoint',
373
+ name: `${method} ${endpointPath}`,
374
+ scope: relPath,
375
+ role: `Public/Internal Endpoint (${detector.framework})`,
376
+ tags: ['api', method.toLowerCase()],
377
+ trustZone: 'dmz_gateway',
378
+ sources: [
379
+ {
380
+ file: relPath,
381
+ startLine: lineNum,
382
+ endLine: Math.min(lineNum + 10, lines.length),
383
+ snippet: lines[lineNum - 1]?.trim(),
384
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
385
+ extractionMethod: 'ast_visitor',
386
+ confidence: 'L1',
387
+ verified: true,
388
+ verifiedAt: new Date().toISOString(),
389
+ },
390
+ ],
391
+ blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.2 },
392
+ confidence: 'L1',
393
+ temporalState: 'current',
394
+ });
395
+
396
+ this.registerRelation({
397
+ sourceId: endpointEntityId,
398
+ targetId: sourceEntityId,
399
+ relationType: 'routes_to',
400
+ protocol: 'HTTP',
401
+ evidence: [
402
+ {
403
+ file: relPath,
404
+ startLine: lineNum,
405
+ endLine: lineNum,
406
+ snippet: lines[lineNum - 1]?.trim(),
407
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
408
+ extractionMethod: 'ast_visitor',
409
+ confidence: 'L1',
410
+ verified: true,
411
+ verifiedAt: new Date().toISOString(),
412
+ },
413
+ ],
414
+ confidence: 'L1',
415
+ });
416
+ }
417
+ }
418
+ }
419
+
420
+ // 3. Analyze Data Stores (Prisma, SQL, Redis)
421
+ for (const detector of DATASTORE_DETECTORS) {
422
+ detector.regex.lastIndex = 0;
423
+ let dbMatch;
424
+ while ((dbMatch = detector.regex.exec(content)) !== null) {
425
+ const lineNum = content.slice(0, dbMatch.index).split('\n').length;
426
+ const storeId = `datastore.${detector.engine}`;
427
+
428
+ this.registerEntity({
429
+ id: storeId,
430
+ kind: detector.kind === 'cache' ? 'cache' : 'datastore',
431
+ name: detector.engine.toUpperCase(),
432
+ scope: 'storage',
433
+ role: `${detector.kind === 'cache' ? 'Ephemeral In-Memory Cache' : 'Persistent Relational/NoSQL Database'}`,
434
+ tags: ['persistence', detector.engine],
435
+ trustZone: 'isolated_datastore',
436
+ sources: [
437
+ {
438
+ file: relPath,
439
+ startLine: lineNum,
440
+ endLine: lineNum,
441
+ snippet: lines[lineNum - 1]?.trim(),
442
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
443
+ extractionMethod: 'ast_visitor',
444
+ confidence: 'L1',
445
+ verified: true,
446
+ verifiedAt: new Date().toISOString(),
447
+ },
448
+ ],
449
+ blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.7 },
450
+ confidence: 'L1',
451
+ temporalState: 'current',
452
+ });
453
+
454
+ this.registerRelation({
455
+ sourceId: sourceEntityId,
456
+ targetId: storeId,
457
+ relationType: detector.kind === 'cache' ? 'reads' : 'writes',
458
+ protocol: detector.engine.includes('redis') ? 'Redis-RESP' : 'SQL-Wire',
459
+ evidence: [
460
+ {
461
+ file: relPath,
462
+ startLine: lineNum,
463
+ endLine: lineNum,
464
+ snippet: lines[lineNum - 1]?.trim(),
465
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
466
+ extractionMethod: 'ast_visitor',
467
+ confidence: 'L1',
468
+ verified: true,
469
+ verifiedAt: new Date().toISOString(),
470
+ },
471
+ ],
472
+ confidence: 'L1',
473
+ });
474
+ }
475
+ }
476
+
477
+ // 4. Analyze Message Queues & Event Busses
478
+ for (const detector of QUEUE_DETECTORS) {
479
+ detector.regex.lastIndex = 0;
480
+ let qMatch;
481
+ while ((qMatch = detector.regex.exec(content)) !== null) {
482
+ const queueName = qMatch[1] || detector.engine;
483
+ const lineNum = content.slice(0, qMatch.index).split('\n').length;
484
+ const qEntityId = `queue.${detector.engine}.${queueName}`;
485
+
486
+ this.registerEntity({
487
+ id: qEntityId,
488
+ kind: detector.kind === 'event_bus' ? 'event_bus' : 'queue',
489
+ name: `${detector.engine}: ${queueName}`,
490
+ scope: 'async-broker',
491
+ role: 'Asynchronous Job Queue / Message Channel',
492
+ tags: ['messaging', detector.engine],
493
+ trustZone: 'internal_service',
494
+ sources: [
495
+ {
496
+ file: relPath,
497
+ startLine: lineNum,
498
+ endLine: lineNum,
499
+ snippet: lines[lineNum - 1]?.trim(),
500
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
501
+ extractionMethod: 'ast_visitor',
502
+ confidence: 'L1',
503
+ verified: true,
504
+ verifiedAt: new Date().toISOString(),
505
+ },
506
+ ],
507
+ blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.4 },
508
+ confidence: 'L1',
509
+ temporalState: 'current',
510
+ });
511
+
512
+ this.registerRelation({
513
+ sourceId: sourceEntityId,
514
+ targetId: qEntityId,
515
+ relationType:
516
+ content.includes('.on(') || content.includes('.subscribe(')
517
+ ? 'subscribes'
518
+ : 'publishes',
519
+ protocol: detector.engine,
520
+ evidence: [
521
+ {
522
+ file: relPath,
523
+ startLine: lineNum,
524
+ endLine: lineNum,
525
+ snippet: lines[lineNum - 1]?.trim(),
526
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
527
+ extractionMethod: 'ast_visitor',
528
+ confidence: 'L1',
529
+ verified: true,
530
+ verifiedAt: new Date().toISOString(),
531
+ },
532
+ ],
533
+ confidence: 'L1',
534
+ });
535
+ }
536
+ }
537
+
538
+ // 5. Analyze External Services (Third-Party SaaS & Clouds)
539
+ for (const detector of EXTERNAL_SERVICE_DETECTORS) {
540
+ detector.regex.lastIndex = 0;
541
+ let extMatch;
542
+ while ((extMatch = detector.regex.exec(content)) !== null) {
543
+ const lineNum = content.slice(0, extMatch.index).split('\n').length;
544
+ const serviceName = extMatch[1] || detector.type;
545
+ const extEntityId = `ext.${crypto.createHash('md5').update(serviceName).digest('hex').slice(0, 8)}`;
546
+
547
+ this.registerEntity({
548
+ id: extEntityId,
549
+ kind: 'external_api',
550
+ name: serviceName.length > 35 ? serviceName.slice(0, 32) + '...' : serviceName,
551
+ scope: 'third_party',
552
+ role: `External Third-Party Service (${detector.type})`,
553
+ tags: ['external', detector.type],
554
+ trustZone: 'external_untrusted',
555
+ sources: [
556
+ {
557
+ file: relPath,
558
+ startLine: lineNum,
559
+ endLine: lineNum,
560
+ snippet: lines[lineNum - 1]?.trim(),
561
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
562
+ extractionMethod: 'ast_visitor',
563
+ confidence: 'L1',
564
+ verified: true,
565
+ verifiedAt: new Date().toISOString(),
566
+ },
567
+ ],
568
+ blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.3 },
569
+ confidence: 'L1',
570
+ temporalState: 'current',
571
+ });
572
+
573
+ // Check for Failure Resilience (try/catch, timeout, retry)
574
+ const surroundingContext = lines
575
+ .slice(Math.max(0, lineNum - 5), Math.min(lines.length, lineNum + 5))
576
+ .join('\n');
577
+ const hasTryCatch =
578
+ surroundingContext.includes('try') || surroundingContext.includes('.catch');
579
+ const hasTimeout =
580
+ surroundingContext.includes('timeout') ||
581
+ surroundingContext.includes('AbortController');
582
+
583
+ this.registerRelation({
584
+ sourceId: sourceEntityId,
585
+ targetId: extEntityId,
586
+ relationType: 'calls',
587
+ protocol: 'HTTPS',
588
+ evidence: [
589
+ {
590
+ file: relPath,
591
+ startLine: lineNum,
592
+ endLine: lineNum,
593
+ snippet: lines[lineNum - 1]?.trim(),
594
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
595
+ extractionMethod: 'ast_visitor',
596
+ confidence: 'L1',
597
+ verified: true,
598
+ verifiedAt: new Date().toISOString(),
599
+ },
600
+ ],
601
+ confidence: 'L1',
602
+ failureMode: {
603
+ hasTimeout,
604
+ hasRetry: surroundingContext.includes('retry'),
605
+ hasCircuitBreaker:
606
+ surroundingContext.includes('circuitBreaker') ||
607
+ surroundingContext.includes('opossum'),
608
+ fallbackTarget: hasTryCatch ? 'handled_exception' : undefined,
609
+ },
610
+ });
611
+ }
612
+ }
613
+
614
+ // 6. Analyze Authentication Boundaries
615
+ for (const detector of AUTH_DETECTORS) {
616
+ detector.regex.lastIndex = 0;
617
+ let authMatch;
618
+ while ((authMatch = detector.regex.exec(content)) !== null) {
619
+ const lineNum = content.slice(0, authMatch.index).split('\n').length;
620
+ const authEntityId = `auth.${detector.authKind}`;
621
+
622
+ this.registerEntity({
623
+ id: authEntityId,
624
+ kind: 'identity_provider',
625
+ name: `Auth: ${detector.authKind.toUpperCase()}`,
626
+ scope: 'security',
627
+ role: 'Authentication & Session Token Verification',
628
+ tags: ['security', 'auth', detector.authKind],
629
+ trustZone: 'dmz_gateway',
630
+ sources: [
631
+ {
632
+ file: relPath,
633
+ startLine: lineNum,
634
+ endLine: lineNum,
635
+ snippet: lines[lineNum - 1]?.trim(),
636
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
637
+ extractionMethod: 'ast_visitor',
638
+ confidence: 'L1',
639
+ verified: true,
640
+ verifiedAt: new Date().toISOString(),
641
+ },
642
+ ],
643
+ blastRadius: { directDependents: 0, transitiveDependents: 0, riskScore: 0.8 },
644
+ confidence: 'L1',
645
+ temporalState: 'current',
646
+ });
647
+
648
+ this.registerRelation({
649
+ sourceId: sourceEntityId,
650
+ targetId: authEntityId,
651
+ relationType: 'authenticates',
652
+ protocol: 'Crypto-Token',
653
+ evidence: [
654
+ {
655
+ file: relPath,
656
+ startLine: lineNum,
657
+ endLine: lineNum,
658
+ snippet: lines[lineNum - 1]?.trim(),
659
+ contentHash: getSha256(lines[lineNum - 1] || '').substring(0, 16),
660
+ extractionMethod: 'ast_visitor',
661
+ confidence: 'L1',
662
+ verified: true,
663
+ verifiedAt: new Date().toISOString(),
664
+ },
665
+ ],
666
+ confidence: 'L1',
667
+ });
668
+ }
669
+ }
670
+ }
671
+
672
+ // Populate Trust Zone Members
673
+ for (const entity of this.entities.values()) {
674
+ const zone = this.trustBoundaries.get(entity.trustZone);
675
+ if (zone && !zone.members.includes(entity.id)) {
676
+ zone.members.push(entity.id);
677
+ }
678
+ }
679
+
680
+ return this.buildModel();
681
+ }
682
+
683
+ initDefaultTrustBoundaries() {
684
+ const boundaries = [
685
+ {
686
+ id: 'public_untrusted',
687
+ name: 'Public Untrusted Zone',
688
+ tier: 'public_untrusted',
689
+ enforcesAuth: false,
690
+ members: [],
691
+ ingressRules: [{ allowedSources: ['*'] }],
692
+ },
693
+ {
694
+ id: 'dmz_gateway',
695
+ name: 'DMZ & API Gateway',
696
+ tier: 'dmz_gateway',
697
+ enforcesAuth: true,
698
+ authMethod: 'jwt',
699
+ members: [],
700
+ ingressRules: [{ allowedSources: ['public_untrusted'] }],
701
+ },
702
+ {
703
+ id: 'internal_service',
704
+ name: 'Internal Service Mesh',
705
+ tier: 'authenticated_service',
706
+ enforcesAuth: true,
707
+ members: [],
708
+ ingressRules: [{ allowedSources: ['dmz_gateway', 'internal_service'] }],
709
+ },
710
+ {
711
+ id: 'isolated_datastore',
712
+ name: 'Isolated Persistent Datastore',
713
+ tier: 'isolated_datastore',
714
+ enforcesAuth: true,
715
+ members: [],
716
+ ingressRules: [{ allowedSources: ['internal_service'] }],
717
+ },
718
+ {
719
+ id: 'external_untrusted',
720
+ name: 'External Third-Party APIs',
721
+ tier: 'public_untrusted',
722
+ enforcesAuth: false,
723
+ members: [],
724
+ ingressRules: [{ allowedSources: ['internal_service'] }],
725
+ },
726
+ ];
727
+
728
+ for (const b of boundaries) {
729
+ this.trustBoundaries.set(b.id, b);
730
+ }
731
+ }
732
+
733
+ buildModel() {
734
+ const entitiesArr = Array.from(this.entities.values());
735
+ const relationsArr = Array.from(this.relations.values());
736
+ const trustArr = Array.from(this.trustBoundaries.values());
737
+
738
+ return {
739
+ schemaVersion: 1,
740
+ engine: 'tribunal-architecture-intelligence',
741
+ generatedAt: new Date().toISOString(),
742
+ repositoryRoot: this.repoRoot,
743
+ summary: {
744
+ totalEntities: entitiesArr.length,
745
+ totalRelations: relationsArr.length,
746
+ totalTrustZones: trustArr.length,
747
+ endpointsCount: entitiesArr.filter(e => e.kind === 'endpoint').length,
748
+ datastoresCount: entitiesArr.filter(e => e.kind === 'datastore' || e.kind === 'cache')
749
+ .length,
750
+ queuesCount: entitiesArr.filter(e => e.kind === 'queue' || e.kind === 'event_bus').length,
751
+ externalServicesCount: entitiesArr.filter(e => e.kind === 'external_api').length,
752
+ },
753
+ entities: entitiesArr,
754
+ relationships: relationsArr,
755
+ trustBoundaries: trustArr,
756
+ contradictions: this.contradictions,
757
+ };
758
+ }
759
+
760
+ save(outputPath) {
761
+ const defaultPath = path.join(this.repoRoot, '.agent', 'history', 'architecture-model.json');
762
+ const dest = outputPath || defaultPath;
763
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
764
+ const model = this.buildModel();
765
+ fs.writeFileSync(dest, JSON.stringify(model, null, 2), 'utf8');
766
+ return dest;
767
+ }
768
+ }
769
+
770
+ // ── Standalone CLI ────────────────────────────────────────────────────────────
771
+ if (require.main === module) {
772
+ const t = timer();
773
+ const repoRoot = process.argv[2] || process.cwd();
774
+ console.log(banner('Tribunal Architecture Fact Extractor (TAFE)'));
775
+ console.log(` ${CYAN}Scanning repository:${RESET} ${repoRoot}`);
776
+
777
+ const extractor = new ArchitectureExtractor(repoRoot);
778
+ const model = extractor.scan();
779
+ const savedPath = extractor.save();
780
+
781
+ console.log(` ${GREEN}${BOX.check} Fact Extraction Complete${RESET} in ${formatMs(t())}`);
782
+ console.log(` ${DIM}Entities:${RESET} ${BOLD}${model.summary.totalEntities}${RESET}`);
783
+ console.log(` ${DIM}Relationships:${RESET} ${BOLD}${model.summary.totalRelations}${RESET}`);
784
+ console.log(` ${DIM}Endpoints:${RESET} ${BOLD}${model.summary.endpointsCount}${RESET}`);
785
+ console.log(` ${DIM}Data Stores:${RESET} ${BOLD}${model.summary.datastoresCount}${RESET}`);
786
+ console.log(` ${DIM}Saved Model:${RESET} ${CYAN}${savedPath}${RESET}\n`);
787
+ }
788
+
789
+ module.exports = {
790
+ ArchitectureExtractor,
791
+ walkRepository,
792
+ };