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
@@ -0,0 +1,602 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * evidence_engine.js — System-1 Capability 1: Evidence Intelligence
5
+ * =================================================================
6
+ * Deterministic, explainable, bounded evidence ranking layer.
7
+ * Determines what information is actually relevant to a decision.
8
+ *
9
+ * Evidence Hierarchy:
10
+ * 1. Changed files (directly modified)
11
+ * 2. Changed symbols/functions (extracted from diff/AST)
12
+ * 3. Direct imports/dependencies
13
+ * 4. Relevant tests (covering changed behavior)
14
+ * 5. Relevant configuration (controlling behavior)
15
+ * 6. Related security boundaries (auth, crypto, permissions)
16
+ * 7. Relevant historical memory (procedural & semantic)
17
+ * 8. Broader repository context (siblings, public entrypoints)
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { normalizePath, estimateTokens } = require('../context/ranker');
23
+ const { HIGH_RISK_PATTERNS, HIGH_RISK_EXTENSIONS } = require('./constants');
24
+
25
+ // Security boundary patterns
26
+ const SECURITY_BOUNDARIES = [
27
+ /[\\/](?:auth|security|jwt|crypto|secrets?|tokens?|passwords?|sessions?)[\\/.]/i,
28
+ /[\\/](?:roles?|access-control|rbac|oauth|sso|permissions?)[\\/.]/i,
29
+ /[\\/]SECURITY\.md$/i,
30
+ /[\\/]\.env/i,
31
+ ];
32
+
33
+ // Type hierarchy priority for deterministic ordering
34
+ const TYPE_PRIORITY = {
35
+ changed_file: 1,
36
+ changed_symbol: 2,
37
+ direct_dependency: 3,
38
+ direct_importer: 3,
39
+ relevant_test: 4,
40
+ configuration: 5,
41
+ security_boundary: 6,
42
+ historical_memory: 7,
43
+ previous_finding: 8,
44
+ repository_context: 9,
45
+ };
46
+
47
+ /**
48
+ * Redact sensitive credentials and tokens from evidence snippets
49
+ */
50
+ function redactSecrets(text) {
51
+ if (!text) return '';
52
+ return text
53
+ .replace(
54
+ /(?:-----BEGIN [A-Z ]+KEY-----[\s\S]*?-----END [A-Z ]+KEY-----)/g,
55
+ '[REDACTED_PRIVATE_KEY]',
56
+ )
57
+ .replace(/(eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+)/g, '[REDACTED_JWT_TOKEN]')
58
+ .replace(/(Bearer\s+)[A-Za-z0-9-_.~+/]+=*/gi, '$1[REDACTED_BEARER_TOKEN]')
59
+ .replace(
60
+ /((?:api[_-]?key|secret|password|passwd|token|auth[_-]?token|access[_-]?token|credentials?|private[_-]?key)\s*[:=]\s*['"]?)[^'"\s\n,;]+/gi,
61
+ '$1[REDACTED_SECRET]',
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Extract symbols/functions modified in diff or file
67
+ * @param {string} diff
68
+ * @param {string} content
69
+ * @returns {Array<{ name: string, type: string, line?: number }>}
70
+ */
71
+ function extractChangedSymbols(diff = '', content = '') {
72
+ const symbols = [];
73
+ const symbolNames = new Set();
74
+ const MAX_DIFF_LINES_TO_SCAN = 1000;
75
+ const MAX_SYMBOLS_PER_FILE = 16;
76
+
77
+ const functionRegex =
78
+ /(?:function\s+([a-zA-Z0-9_$]+)|(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>|(?:async\s+)?([a-zA-Z0-9_$]+)\s*\([^)]*\)\s*\{|class\s+([a-zA-Z0-9_$]+))/g;
79
+
80
+ // 1. Extract from added diff lines (bounded scan)
81
+ if (diff) {
82
+ const lines = diff.split('\n');
83
+ const scanLimit = Math.min(lines.length, MAX_DIFF_LINES_TO_SCAN);
84
+ for (let idx = 0; idx < scanLimit; idx++) {
85
+ if (symbols.length >= MAX_SYMBOLS_PER_FILE) break;
86
+ const line = lines[idx];
87
+ if (line.startsWith('+') && !line.startsWith('+++')) {
88
+ let match;
89
+ while ((match = functionRegex.exec(line)) !== null) {
90
+ const sym = match[1] || match[2] || match[3] || match[4];
91
+ if (sym && !symbolNames.has(sym) && sym.length > 1) {
92
+ symbolNames.add(sym);
93
+ symbols.push({
94
+ name: sym,
95
+ type: match[0].includes('class') ? 'class' : 'function',
96
+ diff_line: idx + 1,
97
+ });
98
+ if (symbols.length >= MAX_SYMBOLS_PER_FILE) break;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ // 2. If content is available and symbols are scarce, extract top definitions
106
+ if (symbols.length === 0 && content) {
107
+ const scanContent = content.length > 50000 ? content.slice(0, 50000) : content;
108
+ let match;
109
+ while ((match = functionRegex.exec(scanContent)) !== null) {
110
+ const sym = match[1] || match[2] || match[3] || match[4];
111
+ if (sym && !symbolNames.has(sym) && sym.length > 1) {
112
+ symbolNames.add(sym);
113
+ symbols.push({ name: sym, type: match[0].includes('class') ? 'class' : 'function' });
114
+ if (symbols.length >= 8) break;
115
+ }
116
+ }
117
+ }
118
+
119
+ return symbols;
120
+ }
121
+
122
+ /**
123
+ * Extract bounded snippet from file content around specific line or symbol
124
+ */
125
+ function extractBoundedSnippet(content, targetLine = null, targetSymbol = null, maxLines = 10) {
126
+ if (!content) return '';
127
+ const MAX_SNIPPET_CHARS = 2000;
128
+ const lines = content.split('\n');
129
+ let snippet = content;
130
+ if (lines.length > maxLines) {
131
+ let center = 0;
132
+ if (targetLine !== null && targetLine > 0 && targetLine <= lines.length) {
133
+ center = targetLine - 1;
134
+ } else if (targetSymbol) {
135
+ const foundIdx = lines.findIndex(l => l.includes(targetSymbol));
136
+ if (foundIdx !== -1) center = foundIdx;
137
+ }
138
+
139
+ const start = Math.max(0, center - Math.floor(maxLines / 2));
140
+ const end = Math.min(lines.length, start + maxLines);
141
+ snippet = lines.slice(start, end).join('\n');
142
+ }
143
+ if (snippet.length > MAX_SNIPPET_CHARS) {
144
+ snippet = snippet.slice(0, MAX_SNIPPET_CHARS) + '\n... [TRUNCATED]';
145
+ }
146
+ return redactSecrets(snippet);
147
+ }
148
+
149
+ /**
150
+ * Score and collect evidence items
151
+ * @param {Object} options
152
+ * @param {string} [options.task]
153
+ * @param {string} [options.diff]
154
+ * @param {string[]|string} [options.files]
155
+ * @param {string} [options.repoRoot]
156
+ * @param {Array} [options.memory]
157
+ * @param {Array} [options.previousFindings]
158
+ * @param {number} [options.maxItems]
159
+ * @param {number} [options.maxTokens]
160
+ * @returns {Object} Evidence Intelligence Result
161
+ */
162
+ function collectAndRankEvidence(options = {}) {
163
+ const repoRoot = options.repoRoot ? path.resolve(options.repoRoot) : process.cwd();
164
+ const rawFiles = options.files;
165
+ const changedFiles = (
166
+ Array.isArray(rawFiles) ? rawFiles : typeof rawFiles === 'string' ? rawFiles.split(',') : []
167
+ )
168
+ .map(f => (typeof f === 'string' ? f.trim() : ''))
169
+ .filter(Boolean)
170
+ .map(normalizePath);
171
+
172
+ const task = typeof options.task === 'string' ? options.task : '';
173
+ const diff = typeof options.diff === 'string' ? options.diff : '';
174
+ const memoryItems = Array.isArray(options.memory) ? options.memory : [];
175
+ const prevFindings = Array.isArray(options.previousFindings) ? options.previousFindings : [];
176
+ const maxItems =
177
+ typeof options.maxItems === 'number' && options.maxItems > 0 ? options.maxItems : 20;
178
+ const maxTokens =
179
+ typeof options.maxTokens === 'number' && options.maxTokens > 0 ? options.maxTokens : 4000;
180
+
181
+ const rawEvidence = [];
182
+ const visitedPaths = new Set();
183
+
184
+ const MAX_FILE_SIZE = 1024 * 1024; // 1MB limit for bounded evidence extraction
185
+
186
+ function isPathInsideRepo(absPath, root) {
187
+ const rel = path.relative(root, absPath);
188
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
189
+ }
190
+
191
+ // Helper to load file safely
192
+ const evidenceTimestamp = Date.now();
193
+ const fileCache = new Map();
194
+ function readFileSafe(relPath) {
195
+ let norm = normalizePath(relPath);
196
+ try {
197
+ norm = decodeURIComponent(norm);
198
+ } catch (_) {}
199
+ if (fileCache.has(norm)) return fileCache.get(norm);
200
+ const abs = path.resolve(repoRoot, norm);
201
+ try {
202
+ if (!isPathInsideRepo(abs, repoRoot)) {
203
+ fileCache.set(norm, null);
204
+ return null;
205
+ }
206
+ if (fs.existsSync(abs)) {
207
+ const stat = fs.statSync(abs);
208
+ if (stat.isFile() && stat.size <= MAX_FILE_SIZE) {
209
+ const text = fs.readFileSync(abs, 'utf8');
210
+ fileCache.set(norm, text);
211
+ return text;
212
+ }
213
+ }
214
+ } catch (_) {}
215
+ fileCache.set(norm, null);
216
+ return null;
217
+ }
218
+
219
+ // Helper to check if file exists
220
+ function fileExists(relPath) {
221
+ let norm = normalizePath(relPath);
222
+ try {
223
+ norm = decodeURIComponent(norm);
224
+ } catch (_) {}
225
+ const abs = path.resolve(repoRoot, norm);
226
+ try {
227
+ if (!isPathInsideRepo(abs, repoRoot)) return false;
228
+ return fs.existsSync(abs) && fs.statSync(abs).isFile();
229
+ } catch (_) {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ // 1. Process Directly Changed Files with Priority & Budgeting
235
+ const MAX_DEEP_INSPECT_FILES = 50;
236
+
237
+ // Prioritize security boundaries and critical schema files first
238
+ changedFiles.sort((a, b) => {
239
+ const aRisk =
240
+ SECURITY_BOUNDARIES.some(p => p.test('/' + a)) || HIGH_RISK_EXTENSIONS.test(a) ? 1 : 0;
241
+ const bRisk =
242
+ SECURITY_BOUNDARIES.some(p => p.test('/' + b)) || HIGH_RISK_EXTENSIONS.test(b) ? 1 : 0;
243
+ return bRisk - aRisk;
244
+ });
245
+
246
+ for (let fIdx = 0; fIdx < changedFiles.length; fIdx++) {
247
+ const file = changedFiles[fIdx];
248
+ visitedPaths.add(file);
249
+ const isDeep = fIdx < MAX_DEEP_INSPECT_FILES;
250
+ const content = isDeep ? readFileSafe(file) : null;
251
+ const isSecurity =
252
+ SECURITY_BOUNDARIES.some(p => p.test('/' + file)) || HIGH_RISK_PATTERNS.test(file);
253
+ const isHighRiskExt = HIGH_RISK_EXTENSIONS.test(file);
254
+ const exists = isDeep ? fileExists(file) : true;
255
+
256
+ const reasons = ['directly modified file in change set'];
257
+ let score = 0.95;
258
+ const confidence = exists ? 'L1' : 'L2';
259
+
260
+ if (isSecurity || isHighRiskExt) {
261
+ score = 1.0;
262
+ reasons.push('touches critical security / schema boundary');
263
+ }
264
+
265
+ const snippet = content
266
+ ? extractBoundedSnippet(content, 1, null, 12)
267
+ : redactSecrets(diff.slice(0, 500) || '');
268
+
269
+ rawEvidence.push({
270
+ id: `file:${file}`,
271
+ type: 'changed_file',
272
+ path: file,
273
+ score,
274
+ reasons,
275
+ confidence,
276
+ provenance: 'OBSERVED',
277
+ timestamp: evidenceTimestamp,
278
+ is_mandatory: true,
279
+ estimated_tokens: estimateTokens(snippet || 100),
280
+ snippet,
281
+ });
282
+
283
+ if (!isDeep) continue; // For overflow files beyond capacity, skip deep AST symbol & dependency checks
284
+
285
+ // 2. Extract changed symbols/functions for this file
286
+ const symbols = extractChangedSymbols(diff, content);
287
+ for (const sym of symbols) {
288
+ const symSnippet = content ? extractBoundedSnippet(content, sym.diff_line, sym.name, 8) : '';
289
+ const symIsSecurity = HIGH_RISK_PATTERNS.test(sym.name);
290
+ const symReasons = [`modified ${sym.type} "${sym.name}" in ${file}`];
291
+ let symScore = 0.88;
292
+ if (symIsSecurity) {
293
+ symScore = 0.96;
294
+ symReasons.push('symbol name matches high-risk pattern');
295
+ }
296
+
297
+ rawEvidence.push({
298
+ id: `symbol:${file}:${sym.name}`,
299
+ type: 'changed_symbol',
300
+ path: file,
301
+ symbol: sym.name,
302
+ score: symScore,
303
+ reasons: symReasons,
304
+ confidence: exists ? 'L1' : 'L2',
305
+ provenance: 'INFERRED',
306
+ timestamp: evidenceTimestamp,
307
+ is_mandatory: false,
308
+ estimated_tokens: estimateTokens(symSnippet || 60),
309
+ snippet: symSnippet,
310
+ });
311
+ }
312
+
313
+ // 3. Direct imports & dependencies
314
+ if (content) {
315
+ const scanContent = content.length > 50000 ? content.slice(0, 50000) : content;
316
+ const importRegex =
317
+ /(?:import\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
318
+ let match;
319
+ while ((match = importRegex.exec(scanContent)) !== null) {
320
+ const importTarget = match[1] || match[2];
321
+ if (importTarget && (importTarget.startsWith('.') || importTarget.startsWith('/'))) {
322
+ // Resolve relative to file directory
323
+ const fileDir = path.dirname(file);
324
+ const resolvedRel = normalizePath(path.join(fileDir, importTarget));
325
+ // Check extensions (.js, .ts, /index.js, etc.)
326
+ const candidates = [
327
+ resolvedRel,
328
+ `${resolvedRel}.js`,
329
+ `${resolvedRel}.ts`,
330
+ `${resolvedRel}.json`,
331
+ `${resolvedRel}/index.js`,
332
+ `${resolvedRel}/index.ts`,
333
+ ];
334
+ const actualTarget = candidates.find(c => fileExists(c)) || resolvedRel;
335
+
336
+ if (!visitedPaths.has(actualTarget)) {
337
+ visitedPaths.add(actualTarget);
338
+ const depContent = readFileSafe(actualTarget);
339
+ const depIsSecurity = SECURITY_BOUNDARIES.some(p => p.test('/' + actualTarget));
340
+ const depReasons = [`direct dependency imported by ${file}`];
341
+ const depScore = depIsSecurity ? 0.9 : 0.8;
342
+ if (depIsSecurity) depReasons.push('dependency operates in security boundary');
343
+
344
+ rawEvidence.push({
345
+ id: `dep:${actualTarget}`,
346
+ type: 'direct_dependency',
347
+ path: actualTarget,
348
+ score: depScore,
349
+ reasons: depReasons,
350
+ confidence: fileExists(actualTarget) ? 'L2' : 'L3',
351
+ provenance: 'INFERRED',
352
+ timestamp: evidenceTimestamp,
353
+ is_mandatory: depIsSecurity,
354
+ estimated_tokens: estimateTokens(
355
+ depContent ? extractBoundedSnippet(depContent, 1, null, 8) : 80,
356
+ ),
357
+ snippet: depContent ? extractBoundedSnippet(depContent, 1, null, 8) : '',
358
+ });
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ // 4. Relevant tests for this changed file
365
+ const baseName = path.basename(file).replace(/\.[^.]+$/, '');
366
+ const possibleTestPaths = [
367
+ `test/unit/${baseName}.test.js`,
368
+ `test/unit/${baseName}.spec.js`,
369
+ `test/integration/${baseName}.test.js`,
370
+ `test/${baseName}.test.js`,
371
+ `tests/${baseName}.test.js`,
372
+ `${path.dirname(file)}/__tests__/${baseName}.test.js`,
373
+ `${path.dirname(file)}/${baseName}.test.js`,
374
+ ];
375
+
376
+ for (const testPath of possibleTestPaths) {
377
+ const normTest = normalizePath(testPath);
378
+ if (fileExists(normTest) && !visitedPaths.has(normTest)) {
379
+ visitedPaths.add(normTest);
380
+ const testContent = readFileSafe(normTest);
381
+ rawEvidence.push({
382
+ id: `test:${normTest}`,
383
+ type: 'relevant_test',
384
+ path: normTest,
385
+ score: 0.78,
386
+ reasons: [`test suite covering modified behavior in ${file}`],
387
+ confidence: 'L1',
388
+ provenance: 'OBSERVED',
389
+ timestamp: evidenceTimestamp,
390
+ is_mandatory: false,
391
+ estimated_tokens: estimateTokens(
392
+ testContent ? extractBoundedSnippet(testContent, 1, null, 10) : 100,
393
+ ),
394
+ snippet: testContent ? extractBoundedSnippet(testContent, 1, null, 10) : '',
395
+ });
396
+ }
397
+ }
398
+ }
399
+
400
+ // 5. Relevant configuration files (package.json, tsconfig.json, docker, etc.)
401
+ const commonConfigs = [
402
+ 'package.json',
403
+ 'tsconfig.json',
404
+ '.eslintrc.js',
405
+ 'eslint.config.js',
406
+ 'Dockerfile',
407
+ '.github/workflows/ci.yml',
408
+ ];
409
+ for (const cfg of commonConfigs) {
410
+ if (fileExists(cfg) && !visitedPaths.has(cfg)) {
411
+ const cfgContent = readFileSafe(cfg);
412
+ const isConfigRelevant =
413
+ changedFiles.some(cf => cf.endsWith('.json') || cf.endsWith('.js') || cf.endsWith('.ts')) ||
414
+ task.toLowerCase().includes('dependency') ||
415
+ task.toLowerCase().includes('package');
416
+
417
+ if (isConfigRelevant || cfg === 'package.json') {
418
+ visitedPaths.add(cfg);
419
+ rawEvidence.push({
420
+ id: `config:${cfg}`,
421
+ type: 'configuration',
422
+ path: cfg,
423
+ score: cfg === 'package.json' ? 0.72 : 0.65,
424
+ reasons: [`project configuration controlling runtime behavior (${cfg})`],
425
+ confidence: 'L1',
426
+ provenance: 'OBSERVED',
427
+ timestamp: evidenceTimestamp,
428
+ is_mandatory: false,
429
+ estimated_tokens: estimateTokens(
430
+ cfgContent ? extractBoundedSnippet(cfgContent, 1, null, 10) : 100,
431
+ ),
432
+ snippet: cfgContent ? extractBoundedSnippet(cfgContent, 1, null, 10) : '',
433
+ });
434
+ }
435
+ }
436
+ }
437
+
438
+ // 6. Security Boundaries (Root SECURITY.md, auth policies)
439
+ const rootSecurityFiles = ['SECURITY.md', '.env.example'];
440
+ for (const secFile of rootSecurityFiles) {
441
+ if (fileExists(secFile) && !visitedPaths.has(secFile)) {
442
+ visitedPaths.add(secFile);
443
+ const secContent = readFileSafe(secFile);
444
+ rawEvidence.push({
445
+ id: `security:${secFile}`,
446
+ type: 'security_boundary',
447
+ path: secFile,
448
+ score: 0.85,
449
+ reasons: [`repository root security and credential policy (${secFile})`],
450
+ confidence: 'L1',
451
+ provenance: 'OBSERVED',
452
+ timestamp: evidenceTimestamp,
453
+ is_mandatory: true,
454
+ estimated_tokens: estimateTokens(
455
+ secContent ? extractBoundedSnippet(secContent, 1, null, 10) : 120,
456
+ ),
457
+ snippet: secContent ? extractBoundedSnippet(secContent, 1, null, 10) : '',
458
+ });
459
+ }
460
+ }
461
+
462
+ // 7. Historical Memory from Memory Engine
463
+ for (const mem of memoryItems) {
464
+ const memContent = typeof mem === 'string' ? mem : mem.content || mem.summary || '';
465
+ if (!memContent) continue;
466
+ const memTags = Array.isArray(mem.tags) ? mem.tags : [];
467
+ const memIsRelevant =
468
+ changedFiles.some(f => {
469
+ const baseNoExt = path.basename(f, path.extname(f)).toLowerCase();
470
+ const baseWithExt = path.basename(f).toLowerCase();
471
+ return (
472
+ memContent.toLowerCase().includes(baseNoExt) ||
473
+ memContent.toLowerCase().includes(baseWithExt) ||
474
+ memTags.some(
475
+ t => baseNoExt.includes(t.toLowerCase()) || f.toLowerCase().includes(t.toLowerCase()),
476
+ )
477
+ );
478
+ }) ||
479
+ (task &&
480
+ (memContent.toLowerCase().includes(task.toLowerCase().slice(0, 15)) ||
481
+ memTags.some(t => task.toLowerCase().includes(t.toLowerCase()))));
482
+
483
+ if (memIsRelevant) {
484
+ rawEvidence.push({
485
+ id: `memory:${mem.id || Math.abs(memContent.length)}`,
486
+ type: 'historical_memory',
487
+ path: mem.tags ? mem.tags.join(',') : 'memory-store',
488
+ score: 0.68,
489
+ reasons: ['relevant historical memory from past sessions/decisions'],
490
+ confidence: 'L2',
491
+ provenance: 'REPORTED',
492
+ timestamp: evidenceTimestamp,
493
+ is_mandatory: false,
494
+ estimated_tokens: estimateTokens(memContent),
495
+ snippet: memContent.slice(0, 200),
496
+ });
497
+ }
498
+ }
499
+
500
+ // 8. Previous Findings
501
+ for (const f of prevFindings) {
502
+ const fDesc = f.desc || f.description || f.message || '';
503
+ if (!fDesc) continue;
504
+ rawEvidence.push({
505
+ id: `finding:${f.id || 'prev'}`,
506
+ type: 'previous_finding',
507
+ path: f.location || f.file || 'audit-result',
508
+ score: f.severity === 'Critical' ? 0.92 : f.severity === 'Important' ? 0.82 : 0.65,
509
+ reasons: [`previous finding: [${f.severity || 'Finding'}] ${fDesc}`],
510
+ confidence: 'L2',
511
+ provenance: 'REPORTED',
512
+ timestamp: evidenceTimestamp,
513
+ is_mandatory: f.severity === 'Critical',
514
+ estimated_tokens: estimateTokens(fDesc),
515
+ snippet: fDesc,
516
+ });
517
+ }
518
+
519
+ // Deterministic stable sorting:
520
+ // 1. score DESCENDING
521
+ // 2. TYPE_PRIORITY ASCENDING (1 to 9)
522
+ // 3. id / path ASCENDING (alphabetic)
523
+ rawEvidence.sort((a, b) => {
524
+ if (Math.abs(b.score - a.score) > 0.001) return b.score - a.score;
525
+ const prioA = TYPE_PRIORITY[a.type] || 99;
526
+ const prioB = TYPE_PRIORITY[b.type] || 99;
527
+ if (prioA !== prioB) return prioA - prioB;
528
+ return a.id.localeCompare(b.id);
529
+ });
530
+
531
+ // Apply budget constraints (maxItems & maxTokens)
532
+ const selectedEvidence = [];
533
+ const truncatedEvidence = [];
534
+ let tokenCount = 0;
535
+
536
+ // Step 1: Retain all mandatory items first
537
+ for (const item of rawEvidence) {
538
+ if (item.is_mandatory) {
539
+ selectedEvidence.push(item);
540
+ tokenCount += item.estimated_tokens;
541
+ }
542
+ }
543
+
544
+ // Step 2: Include non-mandatory items up to budget
545
+ for (const item of rawEvidence) {
546
+ if (item.is_mandatory) continue;
547
+ if (selectedEvidence.length < maxItems && tokenCount + item.estimated_tokens <= maxTokens) {
548
+ selectedEvidence.push(item);
549
+ tokenCount += item.estimated_tokens;
550
+ } else {
551
+ truncatedEvidence.push(item);
552
+ }
553
+ }
554
+
555
+ // Deterministic re-sort of selected items
556
+ selectedEvidence.sort((a, b) => {
557
+ if (Math.abs(b.score - a.score) > 0.001) return b.score - a.score;
558
+ const prioA = TYPE_PRIORITY[a.type] || 99;
559
+ const prioB = TYPE_PRIORITY[b.type] || 99;
560
+ if (prioA !== prioB) return prioA - prioB;
561
+ return a.id.localeCompare(b.id);
562
+ });
563
+
564
+ // Aggregate overall confidence and risk indicators
565
+ const touchesSecurityBoundary =
566
+ changedFiles.some(
567
+ f => SECURITY_BOUNDARIES.some(p => p.test('/' + f)) || HIGH_RISK_PATTERNS.test(f),
568
+ ) ||
569
+ (diff && HIGH_RISK_PATTERNS.test(diff));
570
+ const criticalCount = selectedEvidence.filter(e => e.is_mandatory).length;
571
+
572
+ let overallConfidence = 'L1';
573
+ if (selectedEvidence.length === 0) {
574
+ overallConfidence = 'L4';
575
+ } else if (selectedEvidence.some(e => e.confidence === 'L4')) {
576
+ overallConfidence = 'L3';
577
+ } else if (selectedEvidence.some(e => e.confidence === 'L3')) {
578
+ overallConfidence = 'L2';
579
+ }
580
+
581
+ return {
582
+ evidence: selectedEvidence,
583
+ truncated: truncatedEvidence,
584
+ summary: {
585
+ total_items_evaluated: rawEvidence.length,
586
+ total_items_selected: selectedEvidence.length,
587
+ total_tokens: tokenCount,
588
+ critical_evidence_count: criticalCount,
589
+ has_security_boundary: touchesSecurityBoundary,
590
+ confidence: overallConfidence,
591
+ },
592
+ };
593
+ }
594
+
595
+ module.exports = {
596
+ collectAndRankEvidence,
597
+ extractChangedSymbols,
598
+ extractBoundedSnippet,
599
+ redactSecrets,
600
+ TYPE_PRIORITY,
601
+ SECURITY_BOUNDARIES,
602
+ };