corpus-core 0.1.0

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 (55) hide show
  1. package/dist/autofix.d.ts +41 -0
  2. package/dist/autofix.js +159 -0
  3. package/dist/constants.d.ts +9 -0
  4. package/dist/constants.js +9 -0
  5. package/dist/cve-database.json +396 -0
  6. package/dist/cve-patterns.d.ts +54 -0
  7. package/dist/cve-patterns.js +124 -0
  8. package/dist/engine.d.ts +6 -0
  9. package/dist/engine.js +71 -0
  10. package/dist/graph-engine.d.ts +56 -0
  11. package/dist/graph-engine.js +412 -0
  12. package/dist/index.d.ts +17 -0
  13. package/dist/index.js +17 -0
  14. package/dist/log.d.ts +7 -0
  15. package/dist/log.js +33 -0
  16. package/dist/logger.d.ts +1 -0
  17. package/dist/logger.js +12 -0
  18. package/dist/memory.d.ts +67 -0
  19. package/dist/memory.js +261 -0
  20. package/dist/pattern-learner.d.ts +82 -0
  21. package/dist/pattern-learner.js +420 -0
  22. package/dist/scanners/code-safety.d.ts +13 -0
  23. package/dist/scanners/code-safety.js +114 -0
  24. package/dist/scanners/confidence-calibrator.d.ts +25 -0
  25. package/dist/scanners/confidence-calibrator.js +58 -0
  26. package/dist/scanners/context-poisoning.d.ts +18 -0
  27. package/dist/scanners/context-poisoning.js +48 -0
  28. package/dist/scanners/cross-user-firewall.d.ts +10 -0
  29. package/dist/scanners/cross-user-firewall.js +24 -0
  30. package/dist/scanners/dependency-checker.d.ts +15 -0
  31. package/dist/scanners/dependency-checker.js +203 -0
  32. package/dist/scanners/exfiltration-guard.d.ts +19 -0
  33. package/dist/scanners/exfiltration-guard.js +49 -0
  34. package/dist/scanners/index.d.ts +12 -0
  35. package/dist/scanners/index.js +12 -0
  36. package/dist/scanners/injection-firewall.d.ts +12 -0
  37. package/dist/scanners/injection-firewall.js +71 -0
  38. package/dist/scanners/scope-enforcer.d.ts +10 -0
  39. package/dist/scanners/scope-enforcer.js +30 -0
  40. package/dist/scanners/secret-detector.d.ts +34 -0
  41. package/dist/scanners/secret-detector.js +188 -0
  42. package/dist/scanners/session-hijack.d.ts +16 -0
  43. package/dist/scanners/session-hijack.js +53 -0
  44. package/dist/scanners/trust-score.d.ts +34 -0
  45. package/dist/scanners/trust-score.js +164 -0
  46. package/dist/scanners/undo-integrity.d.ts +9 -0
  47. package/dist/scanners/undo-integrity.js +38 -0
  48. package/dist/subprocess.d.ts +10 -0
  49. package/dist/subprocess.js +103 -0
  50. package/dist/types.d.ts +117 -0
  51. package/dist/types.js +16 -0
  52. package/dist/yaml-evaluator.d.ts +12 -0
  53. package/dist/yaml-evaluator.js +105 -0
  54. package/package.json +36 -0
  55. package/src/cve-database.json +396 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Corpus Graph Engine
3
+ *
4
+ * Auto-scans a TypeScript/JavaScript codebase and builds a structural graph.
5
+ * Nodes = functions, modules, classes. Edges = calls, imports, exports.
6
+ * This is the "immune system memory" -- Corpus learns what your codebase looks like.
7
+ *
8
+ * No manual configuration. No YAML to write. It figures it out.
9
+ */
10
+ import { readFileSync, readdirSync, statSync, existsSync, mkdirSync, writeFileSync } from 'fs';
11
+ import path from 'path';
12
+ // ── AST-Free Parser (no dependencies, hackathon-fast) ────────────────────────
13
+ // Uses regex + structural analysis instead of ts-morph to avoid dependency hell.
14
+ // Good enough for function signatures, exports, imports, and guard clauses.
15
+ const FUNCTION_PATTERNS = [
16
+ // export function name(params): returnType
17
+ /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g,
18
+ // export const name = (params): returnType =>
19
+ /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)(?:\s*:\s*([^=]+))?\s*=>/g,
20
+ // export const name = function(params)
21
+ /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/g,
22
+ // class method: name(params): returnType
23
+ /^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?\s*\{/gm,
24
+ ];
25
+ const CLASS_PATTERN = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+(\w+(?:\s*,\s*\w+)*))?/g;
26
+ const IMPORT_PATTERN = /import\s+(?:(?:\{([^}]+)\})|(?:(\w+)))\s+from\s+['"]([^'"]+)['"]/g;
27
+ const EXPORT_PATTERN = /export\s+(?:(?:default\s+)?(?:function|class|const|let|var|async)\s+(\w+)|(?:\{([^}]+)\}))/g;
28
+ const GUARD_PATTERNS = [
29
+ /if\s*\(\s*!(\w+)\s*\)\s*(?:throw|return)/,
30
+ /if\s*\(\s*(\w+)\s*===?\s*(?:null|undefined|false|''|0)\s*\)\s*(?:throw|return)/,
31
+ /if\s*\(\s*!(\w+)\s*\|\|\s*/,
32
+ /(\w+)\s*\?\?\s*(?:throw|return)/,
33
+ ];
34
+ function parseFile(filePath, content) {
35
+ const lines = content.split('\n');
36
+ const functions = [];
37
+ const imports = [];
38
+ const exports = [];
39
+ const classes = [];
40
+ // Parse imports
41
+ let match;
42
+ const importRegex = new RegExp(IMPORT_PATTERN.source, 'g');
43
+ while ((match = importRegex.exec(content)) !== null) {
44
+ const namedImports = match[1]?.split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean) ?? [];
45
+ const defaultImport = match[2] ? [match[2]] : [];
46
+ imports.push({
47
+ names: [...namedImports, ...defaultImport],
48
+ source: match[3],
49
+ });
50
+ }
51
+ // Parse exports
52
+ const exportRegex = new RegExp(EXPORT_PATTERN.source, 'g');
53
+ while ((match = exportRegex.exec(content)) !== null) {
54
+ if (match[1])
55
+ exports.push(match[1]);
56
+ if (match[2]) {
57
+ match[2].split(',').forEach(e => {
58
+ const name = e.trim().split(/\s+as\s+/)[0].trim();
59
+ if (name)
60
+ exports.push(name);
61
+ });
62
+ }
63
+ }
64
+ // Parse classes
65
+ const classRegex = new RegExp(CLASS_PATTERN.source, 'g');
66
+ while ((match = classRegex.exec(content)) !== null) {
67
+ const lineNum = content.substring(0, match.index).split('\n').length;
68
+ classes.push({
69
+ name: match[1],
70
+ extends_: match[2] || null,
71
+ line: lineNum,
72
+ });
73
+ }
74
+ // Parse functions
75
+ for (const pattern of FUNCTION_PATTERNS) {
76
+ const regex = new RegExp(pattern.source, pattern.flags);
77
+ while ((match = regex.exec(content)) !== null) {
78
+ const name = match[1];
79
+ if (!name || name === 'if' || name === 'for' || name === 'while' || name === 'switch')
80
+ continue;
81
+ const lineNum = content.substring(0, match.index).split('\n').length;
82
+ const params = match[2]
83
+ ? match[2].split(',').map(p => p.trim()).filter(Boolean)
84
+ : [];
85
+ const returnType = match[3]?.trim() || null;
86
+ const exported = content.substring(Math.max(0, match.index - 20), match.index).includes('export') ||
87
+ exports.includes(name);
88
+ // Find guard clauses in the function body (first 10 lines)
89
+ const bodyStart = lineNum - 1;
90
+ const bodyLines = lines.slice(bodyStart, bodyStart + 10).join('\n');
91
+ const guards = [];
92
+ for (const guardPattern of GUARD_PATTERNS) {
93
+ const guardMatch = bodyLines.match(guardPattern);
94
+ if (guardMatch) {
95
+ guards.push(guardMatch[0].trim());
96
+ }
97
+ }
98
+ // Find function calls in the body (next 50 lines)
99
+ const fullBody = lines.slice(bodyStart, bodyStart + 50).join('\n');
100
+ const callPattern = /(?<!\w)(\w+)\s*\(/g;
101
+ const calls = [];
102
+ let callMatch;
103
+ while ((callMatch = callPattern.exec(fullBody)) !== null) {
104
+ const calledName = callMatch[1];
105
+ if (calledName !== name && !['if', 'for', 'while', 'switch', 'return', 'throw', 'new', 'catch', 'typeof', 'instanceof', 'await', 'async', 'console', 'require', 'import'].includes(calledName)) {
106
+ if (!calls.includes(calledName))
107
+ calls.push(calledName);
108
+ }
109
+ }
110
+ functions.push({
111
+ name,
112
+ params,
113
+ returnType,
114
+ exported,
115
+ line: lineNum,
116
+ guards,
117
+ calls,
118
+ });
119
+ }
120
+ }
121
+ return { filePath, functions, imports, exports, classes };
122
+ }
123
+ // ── File Discovery ───────────────────────────────────────────────────────────
124
+ const SKIP_DIRS = new Set([
125
+ 'node_modules', '.git', 'dist', '.next', '__pycache__', '.corpus',
126
+ 'coverage', '.turbo', '.cache', 'build', '.vercel',
127
+ ]);
128
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
129
+ function discoverFiles(rootDir) {
130
+ const files = [];
131
+ function walk(dir) {
132
+ const entries = readdirSync(dir);
133
+ for (const entry of entries) {
134
+ if (SKIP_DIRS.has(entry) || entry.startsWith('.'))
135
+ continue;
136
+ const fullPath = path.join(dir, entry);
137
+ try {
138
+ const stat = statSync(fullPath);
139
+ if (stat.isDirectory()) {
140
+ walk(fullPath);
141
+ }
142
+ else if (CODE_EXTENSIONS.has(path.extname(entry))) {
143
+ files.push(fullPath);
144
+ }
145
+ }
146
+ catch {
147
+ // Skip unreadable files
148
+ }
149
+ }
150
+ }
151
+ walk(rootDir);
152
+ return files;
153
+ }
154
+ // ── Graph Builder ────────────────────────────────────────────────────────────
155
+ export function buildGraph(projectRoot) {
156
+ const files = discoverFiles(projectRoot);
157
+ const nodes = [];
158
+ const edges = [];
159
+ const parsedFiles = [];
160
+ // Parse all files
161
+ for (const filePath of files) {
162
+ try {
163
+ const content = readFileSync(filePath, 'utf-8');
164
+ const parsed = parseFile(filePath, content);
165
+ parsedFiles.push(parsed);
166
+ }
167
+ catch {
168
+ // Skip unparseable files
169
+ }
170
+ }
171
+ // Build node map for lookups
172
+ const functionMap = new Map(); // functionName -> nodeId
173
+ // Create nodes
174
+ for (const file of parsedFiles) {
175
+ const relPath = path.relative(projectRoot, file.filePath);
176
+ // Module node
177
+ const moduleId = `mod:${relPath}`;
178
+ nodes.push({
179
+ id: moduleId,
180
+ type: 'module',
181
+ name: path.basename(relPath, path.extname(relPath)),
182
+ file: relPath,
183
+ line: 1,
184
+ exported: true,
185
+ params: [],
186
+ returnType: null,
187
+ guards: [],
188
+ health: 'verified',
189
+ trustScore: 100,
190
+ });
191
+ // Function nodes
192
+ for (const fn of file.functions) {
193
+ const nodeId = `fn:${relPath}:${fn.name}`;
194
+ nodes.push({
195
+ id: nodeId,
196
+ type: 'function',
197
+ name: fn.name,
198
+ file: relPath,
199
+ line: fn.line,
200
+ exported: fn.exported,
201
+ params: fn.params,
202
+ returnType: fn.returnType,
203
+ guards: fn.guards,
204
+ health: 'verified',
205
+ trustScore: 100,
206
+ });
207
+ functionMap.set(`${relPath}:${fn.name}`, nodeId);
208
+ if (fn.exported) {
209
+ functionMap.set(fn.name, nodeId);
210
+ }
211
+ // Edge: function belongs to module
212
+ edges.push({ source: moduleId, target: nodeId, type: 'exports' });
213
+ }
214
+ // Class nodes
215
+ for (const cls of file.classes) {
216
+ const nodeId = `cls:${relPath}:${cls.name}`;
217
+ nodes.push({
218
+ id: nodeId,
219
+ type: 'class',
220
+ name: cls.name,
221
+ file: relPath,
222
+ line: cls.line,
223
+ exported: file.exports.includes(cls.name),
224
+ params: [],
225
+ returnType: null,
226
+ guards: [],
227
+ health: 'verified',
228
+ trustScore: 100,
229
+ });
230
+ if (cls.extends_) {
231
+ // We'll resolve the edge after all nodes are created
232
+ edges.push({ source: nodeId, target: `cls:*:${cls.extends_}`, type: 'extends' });
233
+ }
234
+ }
235
+ // Import edges (module -> module)
236
+ for (const imp of file.imports) {
237
+ if (imp.source.startsWith('.')) {
238
+ // Local import: resolve relative path
239
+ const targetPath = resolveImportPath(file.filePath, imp.source, projectRoot);
240
+ if (targetPath) {
241
+ const targetModuleId = `mod:${path.relative(projectRoot, targetPath)}`;
242
+ edges.push({ source: moduleId, target: targetModuleId, type: 'imports' });
243
+ }
244
+ }
245
+ }
246
+ }
247
+ // Build call edges
248
+ for (const file of parsedFiles) {
249
+ const relPath = path.relative(projectRoot, file.filePath);
250
+ for (const fn of file.functions) {
251
+ const sourceId = `fn:${relPath}:${fn.name}`;
252
+ for (const calledName of fn.calls) {
253
+ // Try to resolve the called function
254
+ const targetId = functionMap.get(`${relPath}:${calledName}`) ||
255
+ functionMap.get(calledName);
256
+ if (targetId) {
257
+ edges.push({ source: sourceId, target: targetId, type: 'calls' });
258
+ }
259
+ }
260
+ }
261
+ }
262
+ // Resolve wildcard class extensions
263
+ for (const edge of edges) {
264
+ if (edge.target.startsWith('cls:*:')) {
265
+ const className = edge.target.replace('cls:*:', '');
266
+ const resolved = nodes.find(n => n.type === 'class' && n.name === className);
267
+ if (resolved) {
268
+ edge.target = resolved.id;
269
+ }
270
+ }
271
+ }
272
+ // Compute health score
273
+ const totalFunctions = nodes.filter(n => n.type === 'function').length;
274
+ const totalExports = nodes.filter(n => n.exported && n.type === 'function').length;
275
+ const healthScore = nodes.length > 0
276
+ ? Math.round(nodes.reduce((sum, n) => sum + n.trustScore, 0) / nodes.length)
277
+ : 100;
278
+ return {
279
+ version: 1,
280
+ created: new Date().toISOString(),
281
+ updated: new Date().toISOString(),
282
+ projectRoot,
283
+ nodes,
284
+ edges,
285
+ stats: {
286
+ totalFiles: files.length,
287
+ totalFunctions,
288
+ totalExports,
289
+ healthScore,
290
+ },
291
+ };
292
+ }
293
+ function resolveImportPath(fromFile, importPath, projectRoot) {
294
+ const dir = path.dirname(fromFile);
295
+ const candidates = [
296
+ path.resolve(dir, importPath + '.ts'),
297
+ path.resolve(dir, importPath + '.tsx'),
298
+ path.resolve(dir, importPath + '.js'),
299
+ path.resolve(dir, importPath + '.jsx'),
300
+ path.resolve(dir, importPath, 'index.ts'),
301
+ path.resolve(dir, importPath, 'index.js'),
302
+ ];
303
+ for (const candidate of candidates) {
304
+ if (existsSync(candidate))
305
+ return candidate;
306
+ }
307
+ // Try without extension (might already have one)
308
+ if (existsSync(path.resolve(dir, importPath))) {
309
+ return path.resolve(dir, importPath);
310
+ }
311
+ return null;
312
+ }
313
+ export function diffFile(graph, filePath, newContent) {
314
+ const relPath = path.relative(graph.projectRoot, filePath);
315
+ const oldNodes = graph.nodes.filter(n => n.file === relPath && n.type === 'function');
316
+ const parsed = parseFile(filePath, newContent);
317
+ const newFunctions = parsed.functions;
318
+ const added = [];
319
+ const removed = [];
320
+ const modified = [];
321
+ const violations = [];
322
+ // Find removed functions
323
+ for (const oldNode of oldNodes) {
324
+ const stillExists = newFunctions.find(f => f.name === oldNode.name);
325
+ if (!stillExists) {
326
+ removed.push(oldNode);
327
+ if (oldNode.exported) {
328
+ violations.push(`REMOVED: Exported function '${oldNode.name}' was removed from ${relPath}. ` +
329
+ `This function may be used by other modules. Restore it or update all callers.`);
330
+ }
331
+ }
332
+ }
333
+ // Find added and modified functions
334
+ for (const newFn of newFunctions) {
335
+ const oldNode = oldNodes.find(n => n.name === newFn.name);
336
+ if (!oldNode) {
337
+ // New function
338
+ added.push({
339
+ id: `fn:${relPath}:${newFn.name}`,
340
+ type: 'function',
341
+ name: newFn.name,
342
+ file: relPath,
343
+ line: newFn.line,
344
+ exported: newFn.exported,
345
+ params: newFn.params,
346
+ returnType: newFn.returnType,
347
+ guards: newFn.guards,
348
+ health: 'uncertain',
349
+ trustScore: 80,
350
+ });
351
+ }
352
+ else {
353
+ // Check for modifications
354
+ const changes = [];
355
+ // Check params changed
356
+ if (JSON.stringify(oldNode.params) !== JSON.stringify(newFn.params)) {
357
+ changes.push(`Parameters changed: [${oldNode.params.join(', ')}] -> [${newFn.params.join(', ')}]`);
358
+ }
359
+ // Check return type changed
360
+ if (oldNode.returnType !== newFn.returnType) {
361
+ changes.push(`Return type changed: ${oldNode.returnType || 'unknown'} -> ${newFn.returnType || 'unknown'}`);
362
+ }
363
+ // Check guard clauses removed (CRITICAL)
364
+ for (const oldGuard of oldNode.guards) {
365
+ if (!newFn.guards.some(g => g.includes(oldGuard.split('(')[0]))) {
366
+ changes.push(`Guard clause REMOVED: ${oldGuard}`);
367
+ violations.push(`GUARD REMOVED: Function '${newFn.name}' in ${relPath} had a safety guard '${oldGuard}' ` +
368
+ `that was removed. This may introduce a security vulnerability. Restore the guard clause.`);
369
+ }
370
+ }
371
+ if (changes.length > 0) {
372
+ modified.push({
373
+ before: oldNode,
374
+ after: {
375
+ ...oldNode,
376
+ params: newFn.params,
377
+ returnType: newFn.returnType,
378
+ guards: newFn.guards,
379
+ health: violations.length > 0 ? 'violates' : 'uncertain',
380
+ trustScore: Math.max(0, oldNode.trustScore - (violations.length * 20)),
381
+ },
382
+ changes,
383
+ });
384
+ }
385
+ }
386
+ }
387
+ const verdict = violations.length > 0 ? 'VIOLATES' :
388
+ (added.length > 0 || modified.length > 0) ? 'UNCERTAIN' :
389
+ 'VERIFIED';
390
+ return { added, removed, modified, verdict, violations };
391
+ }
392
+ // ── Save/Load ────────────────────────────────────────────────────────────────
393
+ export function saveGraph(graph, projectRoot) {
394
+ const corpusDir = path.join(projectRoot, '.corpus');
395
+ if (!existsSync(corpusDir)) {
396
+ mkdirSync(corpusDir, { recursive: true });
397
+ }
398
+ const graphPath = path.join(corpusDir, 'graph.json');
399
+ writeFileSync(graphPath, JSON.stringify(graph, null, 2));
400
+ return graphPath;
401
+ }
402
+ export function loadGraph(projectRoot) {
403
+ const graphPath = path.join(projectRoot, '.corpus', 'graph.json');
404
+ if (!existsSync(graphPath))
405
+ return null;
406
+ try {
407
+ return JSON.parse(readFileSync(graphPath, 'utf-8'));
408
+ }
409
+ catch {
410
+ return null;
411
+ }
412
+ }
@@ -0,0 +1,17 @@
1
+ export { evaluatePolicies } from './engine.js';
2
+ export type { EvalResult } from './engine.js';
3
+ export { evaluateYamlPolicies, loadPolicyFile } from './yaml-evaluator.js';
4
+ export { runJacWalker } from './subprocess.js';
5
+ export { buildLogEntry, sendLogEntry } from './log.js';
6
+ export * from './types.js';
7
+ export * from './constants.js';
8
+ export * from './scanners/index.js';
9
+ export { buildGraph, diffFile, saveGraph, loadGraph } from './graph-engine.js';
10
+ export type { CodebaseGraph, GraphNode, GraphEdge, GraphDiff } from './graph-engine.js';
11
+ export { checkFile, getHealthSummary } from './autofix.js';
12
+ export type { FixInstruction, ViolationDetail } from './autofix.js';
13
+ export { checkForCVEs, loadCVEPatterns, getCVESummary, type CVEPattern, type CVEFinding } from './cve-patterns.js';
14
+ export { learnFromFindings, getLearnedPatterns, shouldSuppress, getPatternIntelligence, addKnownPackages, categorizeRepo } from './pattern-learner.js';
15
+ export type { PatternSignature, LearnedPatterns } from './pattern-learner.js';
16
+ export { recordMemory, getFlagCount, getRecentViolations, getMemoryStats, getAllMemories, syncToBackboard, getBackboardMemories } from './memory.js';
17
+ export type { MemoryEntry, ImmuneMemory } from './memory.js';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ export { evaluatePolicies } from './engine.js';
2
+ export { evaluateYamlPolicies, loadPolicyFile } from './yaml-evaluator.js';
3
+ export { runJacWalker } from './subprocess.js';
4
+ export { buildLogEntry, sendLogEntry } from './log.js';
5
+ export * from './types.js';
6
+ export * from './constants.js';
7
+ export * from './scanners/index.js';
8
+ // Corpus Graph Engine (immune system)
9
+ export { buildGraph, diffFile, saveGraph, loadGraph } from './graph-engine.js';
10
+ // Corpus Auto-Fix Engine
11
+ export { checkFile, getHealthSummary } from './autofix.js';
12
+ // CVE Pattern Detection
13
+ export { checkForCVEs, loadCVEPatterns, getCVESummary } from './cve-patterns.js';
14
+ // Corpus Pattern Learner (evolving immune system)
15
+ export { learnFromFindings, getLearnedPatterns, shouldSuppress, getPatternIntelligence, addKnownPackages, categorizeRepo } from './pattern-learner.js';
16
+ // Corpus Immune Memory (Backboard.io + local fallback)
17
+ export { recordMemory, getFlagCount, getRecentViolations, getMemoryStats, getAllMemories, syncToBackboard, getBackboardMemories } from './memory.js';
package/dist/log.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { ActionLogEntry, PolicyResult, CorpusConfig, CorpusInput } from './types.js';
2
+ export declare function buildLogEntry(input: CorpusInput, result: PolicyResult, verdict: string, userDecision: 'CONFIRMED' | 'CANCELLED' | null, projectSlug: string, durationMs: number): ActionLogEntry;
3
+ /**
4
+ * Fire-and-forget log sender. Never awaited. Never throws.
5
+ * Sends only behavioral metadata, never actionPayload.
6
+ */
7
+ export declare function sendLogEntry(entry: ActionLogEntry, config: CorpusConfig): void;
package/dist/log.js ADDED
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { debug } from './logger.js';
3
+ import { INGEST_PATH } from './constants.js';
4
+ export function buildLogEntry(input, result, verdict, userDecision, projectSlug, durationMs) {
5
+ return {
6
+ id: randomUUID(),
7
+ projectSlug,
8
+ actionType: input.actionType,
9
+ verdict: verdict,
10
+ userDecision,
11
+ policyTriggered: result.policyName,
12
+ blockReason: result.blockReason,
13
+ durationMs,
14
+ timestamp: new Date().toISOString(),
15
+ };
16
+ }
17
+ /**
18
+ * Fire-and-forget log sender. Never awaited. Never throws.
19
+ * Sends only behavioral metadata, never actionPayload.
20
+ */
21
+ export function sendLogEntry(entry, config) {
22
+ const url = `${config.apiEndpoint}${INGEST_PATH}`;
23
+ fetch(url, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'x-corpus-key': config.apiKey,
28
+ },
29
+ body: JSON.stringify(entry),
30
+ }).catch((err) => {
31
+ debug('Failed to send log entry', err instanceof Error ? err.message : err);
32
+ });
33
+ }
@@ -0,0 +1 @@
1
+ export declare function debug(message: string, data?: unknown): void;
package/dist/logger.js ADDED
@@ -0,0 +1,12 @@
1
+ const enabled = process.env.CORPUS_DEBUG === 'true';
2
+ export function debug(message, data) {
3
+ if (!enabled)
4
+ return;
5
+ const prefix = `[corpus ${new Date().toISOString()}]`;
6
+ if (data !== undefined) {
7
+ process.stderr.write(`${prefix} ${message} ${JSON.stringify(data)}\n`);
8
+ }
9
+ else {
10
+ process.stderr.write(`${prefix} ${message}\n`);
11
+ }
12
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Corpus Immune Memory -- Backboard.io Integration
3
+ *
4
+ * Stores behavioral baselines and health snapshots persistently across sessions.
5
+ * The longer you use Corpus, the smarter it gets. Memory powers:
6
+ * - "This function was flagged 3 times before"
7
+ * - Cross-session pattern recognition
8
+ * - Behavioral drift detection over time
9
+ *
10
+ * Falls back to local .corpus/memory.json when Backboard is unavailable.
11
+ */
12
+ export interface MemoryEntry {
13
+ id: string;
14
+ type: 'baseline' | 'violation' | 'fix' | 'pattern';
15
+ timestamp: string;
16
+ file: string;
17
+ functionName?: string;
18
+ content: string;
19
+ metadata: Record<string, unknown>;
20
+ flagCount?: number;
21
+ }
22
+ export interface ImmuneMemory {
23
+ entries: MemoryEntry[];
24
+ stats: {
25
+ totalEntries: number;
26
+ totalViolations: number;
27
+ totalFixes: number;
28
+ sessionsTracked: number;
29
+ lastUpdated: string;
30
+ };
31
+ }
32
+ /**
33
+ * Record a memory entry (violation, fix, baseline, pattern).
34
+ * Stores in Backboard.io if available, always stores locally.
35
+ */
36
+ export declare function recordMemory(projectRoot: string, entry: Omit<MemoryEntry, 'id' | 'timestamp'>): Promise<void>;
37
+ /**
38
+ * Get the flag count for a specific function across all sessions.
39
+ * "This function was flagged 3 times before."
40
+ */
41
+ export declare function getFlagCount(projectRoot: string, file: string, functionName: string): number;
42
+ /**
43
+ * Get recent violations for the dashboard.
44
+ */
45
+ export declare function getRecentViolations(projectRoot: string, limit?: number): MemoryEntry[];
46
+ /**
47
+ * Get immune memory stats for display.
48
+ */
49
+ export declare function getMemoryStats(projectRoot: string): ImmuneMemory['stats'];
50
+ /**
51
+ * Get all memories (for syncing to Backboard.io or display).
52
+ */
53
+ export declare function getAllMemories(projectRoot: string): ImmuneMemory;
54
+ /**
55
+ * Get memories stored in Backboard.io for this project.
56
+ */
57
+ export declare function getBackboardMemories(projectRoot: string): Promise<{
58
+ memories: any[];
59
+ total_count: number;
60
+ } | null>;
61
+ /**
62
+ * Sync local memory to Backboard.io (for initial setup or recovery).
63
+ */
64
+ export declare function syncToBackboard(projectRoot: string): Promise<{
65
+ synced: number;
66
+ errors: number;
67
+ }>;