@rigour-labs/core 5.4.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/cache-engine.d.ts +25 -0
- package/dist/context/cache-engine.js +79 -1
- package/dist/context/cache-engine.test.js +37 -1
- package/dist/gates/hallucinated-imports/js-resolver.js +8 -15
- package/dist/gates/hallucinated-imports/ts-path-target.d.ts +1 -0
- package/dist/gates/hallucinated-imports/ts-path-target.js +27 -0
- package/dist/gates/hallucinated-imports.test.js +37 -1
- package/dist/hooks/dlp-feedback.d.ts +11 -5
- package/dist/hooks/dlp-feedback.js +61 -10
- package/dist/hooks/dlp-feedback.test.js +93 -1
- package/dist/hooks/dlp-templates.d.ts +2 -2
- package/dist/hooks/dlp-templates.js +13 -16
- package/dist/hooks/input-validator.d.ts +2 -0
- package/dist/hooks/input-validator.js +25 -11
- package/dist/hooks/standalone-dlp-checker.d.ts +2 -2
- package/dist/hooks/standalone-dlp-checker.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/score-history.d.ts +5 -1
- package/dist/services/score-history.js +12 -16
- package/dist/services/score-history.test.d.ts +1 -0
- package/dist/services/score-history.test.js +53 -0
- package/dist/settings.d.ts +15 -2
- package/dist/settings.js +56 -6
- package/dist/storage/context-telemetry.d.ts +8 -0
- package/dist/storage/context-telemetry.js +61 -0
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/templates/universal-config.js +1 -1
- package/package.json +6 -6
|
@@ -68,6 +68,31 @@ export declare function setComponentCache(componentName: string, commitSha: stri
|
|
|
68
68
|
export declare function normalizeQuery(query: string): string;
|
|
69
69
|
export declare function getSemanticQueryCache(query: string, commitSha: string, cwd?: string): Promise<SemanticQueryEntry | null>;
|
|
70
70
|
export declare function setSemanticQueryCache(query: string, commitSha: string, entry: SemanticQueryEntry, cwd?: string): Promise<void>;
|
|
71
|
+
/** Jaccard similarity of normalized query tokens — used for quality-safe partial reuse. */
|
|
72
|
+
export declare function queryTokenOverlap(a: string, b: string): number;
|
|
73
|
+
/**
|
|
74
|
+
* Distinctive tokens (len >= 5) must mostly agree — blocks "task service" ≈ "payment service".
|
|
75
|
+
*/
|
|
76
|
+
export declare function distinctiveTokenAgreement(a: string, b: string): number;
|
|
77
|
+
export interface RelatedSemanticHit {
|
|
78
|
+
entry: SemanticQueryEntry;
|
|
79
|
+
overlap: number;
|
|
80
|
+
sourceQuery: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Quality-safe partial semantic reuse: same commit only.
|
|
84
|
+
* Requires high Jaccard (>= 0.75) AND distinctive-token agreement (>= 0.67).
|
|
85
|
+
* Never crosses commits — avoids stale scopes after code moves.
|
|
86
|
+
* File existence is re-validated by the caller before serving.
|
|
87
|
+
*/
|
|
88
|
+
export declare function findRelatedSemanticQueryCache(query: string, commitSha: string, cwd?: string, minOverlap?: number, minDistinctive?: number): Promise<RelatedSemanticHit | null>;
|
|
89
|
+
/**
|
|
90
|
+
* Drop cached scopes that point at files no longer on disk (quality guard).
|
|
91
|
+
*/
|
|
92
|
+
export declare function filterExistingEditScope(editScope: string[], cwd: string): Promise<{
|
|
93
|
+
valid: string[];
|
|
94
|
+
missing: string[];
|
|
95
|
+
}>;
|
|
71
96
|
/**
|
|
72
97
|
* Layer 4: Task Checkpoint Cache
|
|
73
98
|
*/
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* 4. Task checkpoint cache (Subagent handoff packets)
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
|
-
import
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric } from '../storage/index.js';
|
|
12
14
|
/**
|
|
13
15
|
* Generate SHA-256 content hash.
|
|
14
16
|
*/
|
|
@@ -114,6 +116,82 @@ export async function setSemanticQueryCache(query, commitSha, entry, cwd) {
|
|
|
114
116
|
payloadTokens,
|
|
115
117
|
}, cwd);
|
|
116
118
|
}
|
|
119
|
+
function queryTokenSet(query) {
|
|
120
|
+
return new Set(normalizeQuery(query).split(' ').filter((t) => t.length > 1));
|
|
121
|
+
}
|
|
122
|
+
/** Jaccard similarity of normalized query tokens — used for quality-safe partial reuse. */
|
|
123
|
+
export function queryTokenOverlap(a, b) {
|
|
124
|
+
const left = queryTokenSet(a);
|
|
125
|
+
const right = queryTokenSet(b);
|
|
126
|
+
if (left.size === 0 || right.size === 0)
|
|
127
|
+
return 0;
|
|
128
|
+
let inter = 0;
|
|
129
|
+
for (const t of left) {
|
|
130
|
+
if (right.has(t))
|
|
131
|
+
inter++;
|
|
132
|
+
}
|
|
133
|
+
const union = left.size + right.size - inter;
|
|
134
|
+
return union === 0 ? 0 : inter / union;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Distinctive tokens (len >= 5) must mostly agree — blocks "task service" ≈ "payment service".
|
|
138
|
+
*/
|
|
139
|
+
export function distinctiveTokenAgreement(a, b) {
|
|
140
|
+
const left = [...queryTokenSet(a)].filter((t) => t.length >= 5);
|
|
141
|
+
const right = new Set([...queryTokenSet(b)].filter((t) => t.length >= 5));
|
|
142
|
+
if (left.length === 0 || right.size === 0)
|
|
143
|
+
return 0;
|
|
144
|
+
const hits = left.filter((t) => right.has(t)).length;
|
|
145
|
+
return hits / Math.max(left.length, right.size);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Quality-safe partial semantic reuse: same commit only.
|
|
149
|
+
* Requires high Jaccard (>= 0.75) AND distinctive-token agreement (>= 0.67).
|
|
150
|
+
* Never crosses commits — avoids stale scopes after code moves.
|
|
151
|
+
* File existence is re-validated by the caller before serving.
|
|
152
|
+
*/
|
|
153
|
+
export async function findRelatedSemanticQueryCache(query, commitSha, cwd, minOverlap = 0.75, minDistinctive = 0.67) {
|
|
154
|
+
const records = await listContextCacheRecords({ cacheType: 'semantic', commitSha, limit: 80 }, cwd);
|
|
155
|
+
let best = null;
|
|
156
|
+
for (const record of records) {
|
|
157
|
+
try {
|
|
158
|
+
const entry = JSON.parse(record.payloadJson);
|
|
159
|
+
const sourceQuery = entry.query || '';
|
|
160
|
+
const overlap = queryTokenOverlap(query, sourceQuery);
|
|
161
|
+
const distinctive = distinctiveTokenAgreement(query, sourceQuery);
|
|
162
|
+
if (overlap < minOverlap || distinctive < minDistinctive)
|
|
163
|
+
continue;
|
|
164
|
+
if (normalizeQuery(query) === normalizeQuery(sourceQuery))
|
|
165
|
+
continue; // exact path uses getSemanticQueryCache
|
|
166
|
+
if (!best || overlap > best.overlap) {
|
|
167
|
+
best = { entry, overlap, sourceQuery };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// skip bad payloads
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return best;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Drop cached scopes that point at files no longer on disk (quality guard).
|
|
178
|
+
*/
|
|
179
|
+
export async function filterExistingEditScope(editScope, cwd) {
|
|
180
|
+
const valid = [];
|
|
181
|
+
const missing = [];
|
|
182
|
+
for (const file of editScope) {
|
|
183
|
+
try {
|
|
184
|
+
if (await fs.pathExists(path.join(cwd, file)))
|
|
185
|
+
valid.push(file);
|
|
186
|
+
else
|
|
187
|
+
missing.push(file);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
missing.push(file);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { valid, missing };
|
|
194
|
+
}
|
|
117
195
|
/**
|
|
118
196
|
* Layer 4: Task Checkpoint Cache
|
|
119
197
|
*/
|
|
@@ -8,7 +8,7 @@ vi.mock('./db.js', () => ({
|
|
|
8
8
|
DB_PATH: '/tmp/rigour-test.db',
|
|
9
9
|
RIGOUR_DIR: '/tmp/.rigour',
|
|
10
10
|
}));
|
|
11
|
-
import { getStaticCache, setStaticCache, getComponentCache, setComponentCache, getSemanticQueryCache, setSemanticQueryCache, getTaskCheckpointCache, setTaskCheckpointCache, hashContent, normalizeQuery } from './cache-engine.js';
|
|
11
|
+
import { getStaticCache, setStaticCache, getComponentCache, setComponentCache, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, queryTokenOverlap, distinctiveTokenAgreement, filterExistingEditScope, getTaskCheckpointCache, setTaskCheckpointCache, hashContent, normalizeQuery } from './cache-engine.js';
|
|
12
12
|
describe('4-Layer Context Cache Engine', () => {
|
|
13
13
|
const testCwd = path.join(os.tmpdir(), `rigour-cache-test-${Date.now()}`);
|
|
14
14
|
beforeEach(async () => {
|
|
@@ -89,6 +89,42 @@ describe('4-Layer Context Cache Engine', () => {
|
|
|
89
89
|
expect(retrieved?.taskId).toBe('CTP-142');
|
|
90
90
|
expect(retrieved?.decisions).toContain('Added priority field');
|
|
91
91
|
});
|
|
92
|
+
it('Layer 3: finds related semantic queries at same commit for partial reuse', async () => {
|
|
93
|
+
expect(queryTokenOverlap('task priority persistence service', 'task priority persistence service layer')).toBeGreaterThan(0.7);
|
|
94
|
+
expect(distinctiveTokenAgreement('task service', 'payment service')).toBeLessThan(0.67);
|
|
95
|
+
await setSemanticQueryCache('task priority persistence service', 'commit-rel', {
|
|
96
|
+
query: 'task priority persistence service',
|
|
97
|
+
resolvedOwner: 'task-team',
|
|
98
|
+
editScope: ['services/task.ts'],
|
|
99
|
+
validationScope: ['npm test'],
|
|
100
|
+
evidence: ['related'],
|
|
101
|
+
commitSha: 'commit-rel',
|
|
102
|
+
confidence: 0.9,
|
|
103
|
+
}, testCwd);
|
|
104
|
+
const related = await findRelatedSemanticQueryCache('task priority persistence service layer', 'commit-rel', testCwd);
|
|
105
|
+
expect(related).not.toBeNull();
|
|
106
|
+
expect(related?.entry.editScope).toContain('services/task.ts');
|
|
107
|
+
// Same generic stem, different distinctive noun → reject
|
|
108
|
+
await setSemanticQueryCache('payment service billing', 'commit-rel', {
|
|
109
|
+
query: 'payment service billing',
|
|
110
|
+
resolvedOwner: 'pay',
|
|
111
|
+
editScope: ['services/pay.ts'],
|
|
112
|
+
validationScope: [],
|
|
113
|
+
evidence: [],
|
|
114
|
+
commitSha: 'commit-rel',
|
|
115
|
+
confidence: 0.9,
|
|
116
|
+
}, testCwd);
|
|
117
|
+
const crossDomain = await findRelatedSemanticQueryCache('task service billing', 'commit-rel', testCwd);
|
|
118
|
+
expect(crossDomain).toBeNull();
|
|
119
|
+
const otherCommit = await findRelatedSemanticQueryCache('task priority persistence service layer', 'commit-other', testCwd);
|
|
120
|
+
expect(otherCommit).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
it('filters missing edit-scope files for quality', async () => {
|
|
123
|
+
await fs.writeFile(path.join(testCwd, 'alive.ts'), 'export const ok = 1;\n');
|
|
124
|
+
const result = await filterExistingEditScope(['alive.ts', 'gone.ts'], testCwd);
|
|
125
|
+
expect(result.valid).toEqual(['alive.ts']);
|
|
126
|
+
expect(result.missing).toEqual(['gone.ts']);
|
|
127
|
+
});
|
|
92
128
|
it('Layer 1: invalidates static cache when file content changes', async () => {
|
|
93
129
|
const originalContent = 'export function processTask() { return true; }';
|
|
94
130
|
const entry = {
|
|
@@ -12,6 +12,7 @@ import fs from 'fs-extra';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import ts from 'typescript';
|
|
14
14
|
import { isNodeBuiltin } from '../hallucinated-imports-stdlib.js';
|
|
15
|
+
import { resolveTsPathTarget } from './ts-path-target.js';
|
|
15
16
|
/**
|
|
16
17
|
* Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
|
|
17
18
|
*/
|
|
@@ -148,7 +149,7 @@ export async function resolveTsPathAlias(file, importPath, cwd, projectFiles, ts
|
|
|
148
149
|
continue;
|
|
149
150
|
for (const target of rule.targets) {
|
|
150
151
|
const candidatePattern = rule.hasWildcard ? target.replace('*', wildcard) : target;
|
|
151
|
-
if (resolveTsPathTarget(config.baseDir, candidatePattern, cwd, projectFiles)) {
|
|
152
|
+
if (await resolveTsPathTarget(config.baseDir, candidatePattern, cwd, projectFiles)) {
|
|
152
153
|
return true;
|
|
153
154
|
}
|
|
154
155
|
}
|
|
@@ -169,20 +170,6 @@ function matchTsPathRule(rule, importPath) {
|
|
|
169
170
|
}
|
|
170
171
|
return importPath.slice(rule.prefix.length, importPath.length - rule.suffix.length);
|
|
171
172
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Check if a TypeScript path target resolves to an actual project file.
|
|
174
|
-
*/
|
|
175
|
-
function resolveTsPathTarget(baseDir, candidatePattern, cwd, projectFiles) {
|
|
176
|
-
const absolute = path.resolve(baseDir, candidatePattern);
|
|
177
|
-
const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
|
|
178
|
-
const normalized = relative.replace(/\/$/, '');
|
|
179
|
-
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
|
|
180
|
-
const candidates = [
|
|
181
|
-
...extensions.map(ext => normalized + ext),
|
|
182
|
-
...extensions.map(ext => `${normalized}/index${ext}`),
|
|
183
|
-
];
|
|
184
|
-
return candidates.some(c => projectFiles.has(c));
|
|
185
|
-
}
|
|
186
173
|
/**
|
|
187
174
|
* Find and load TypeScript/JavaScript config for a file.
|
|
188
175
|
* Walks up the directory tree to find tsconfig.json or jsconfig.json.
|
|
@@ -248,6 +235,12 @@ async function loadTsPathConfig(searchDir) {
|
|
|
248
235
|
continue;
|
|
249
236
|
rules.push({ key, hasWildcard, prefix, suffix, targets });
|
|
250
237
|
}
|
|
238
|
+
rules.sort((left, right) => {
|
|
239
|
+
if (left.hasWildcard !== right.hasWildcard)
|
|
240
|
+
return left.hasWildcard ? 1 : -1;
|
|
241
|
+
return (right.prefix.length + right.suffix.length)
|
|
242
|
+
- (left.prefix.length + left.suffix.length);
|
|
243
|
+
});
|
|
251
244
|
if (rules.length === 0)
|
|
252
245
|
continue;
|
|
253
246
|
return { baseDir: resolvedBaseDir, rules };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveTsPathTarget(baseDir: string, candidatePattern: string, cwd: string, projectFiles: Set<string>): Promise<boolean>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pathExists, stat } from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const sourceExtensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
|
|
4
|
+
export async function resolveTsPathTarget(baseDir, candidatePattern, cwd, projectFiles) {
|
|
5
|
+
const absolute = path.resolve(baseDir, candidatePattern);
|
|
6
|
+
const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
|
|
7
|
+
if (relative === '..' || relative.startsWith('../') || path.isAbsolute(relative))
|
|
8
|
+
return false;
|
|
9
|
+
const normalized = relative.replace(/\/$/, '');
|
|
10
|
+
const candidates = [
|
|
11
|
+
...sourceExtensions.map(extension => normalized + extension),
|
|
12
|
+
...sourceExtensions.map(extension => `${normalized}/index${extension}`),
|
|
13
|
+
];
|
|
14
|
+
if (candidates.some(candidate => projectFiles.has(candidate)))
|
|
15
|
+
return true;
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
const candidatePath = path.resolve(cwd, candidate);
|
|
18
|
+
if (!await pathExists(candidatePath))
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
if ((await stat(candidatePath)).isFile())
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch { /* inaccessible targets do not resolve */ }
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
@@ -11,13 +11,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
11
11
|
import { HallucinatedImportsGate } from './hallucinated-imports/index.js';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
// Mock fs-extra — vi.hoisted ensures these are available when vi.mock runs (hoisted)
|
|
14
|
-
const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson, mockReaddirSync } = vi.hoisted(() => ({
|
|
14
|
+
const { mockPathExists, mockPathExistsSync, mockReadFile, mockReadFileSync, mockReadJson, mockReaddirSync, mockStat } = vi.hoisted(() => ({
|
|
15
15
|
mockPathExists: vi.fn(),
|
|
16
16
|
mockPathExistsSync: vi.fn(),
|
|
17
17
|
mockReadFile: vi.fn(),
|
|
18
18
|
mockReadFileSync: vi.fn(),
|
|
19
19
|
mockReadJson: vi.fn(),
|
|
20
20
|
mockReaddirSync: vi.fn().mockReturnValue([]),
|
|
21
|
+
mockStat: vi.fn(),
|
|
21
22
|
}));
|
|
22
23
|
vi.mock('fs-extra', () => {
|
|
23
24
|
const mock = {
|
|
@@ -27,6 +28,7 @@ vi.mock('fs-extra', () => {
|
|
|
27
28
|
readFileSync: mockReadFileSync,
|
|
28
29
|
readJson: mockReadJson,
|
|
29
30
|
readdirSync: mockReaddirSync,
|
|
31
|
+
stat: mockStat,
|
|
30
32
|
};
|
|
31
33
|
return {
|
|
32
34
|
...mock,
|
|
@@ -365,6 +367,7 @@ import { cfg } from '~shared/config';
|
|
|
365
367
|
return normalized === `${testCwdNormalized}/apps/desktop/tsconfig.json`
|
|
366
368
|
|| normalized === `${testCwdNormalized}/package.json`;
|
|
367
369
|
});
|
|
370
|
+
mockStat.mockResolvedValue({ isFile: () => true });
|
|
368
371
|
mockReadJson.mockResolvedValue({
|
|
369
372
|
dependencies: {},
|
|
370
373
|
devDependencies: {},
|
|
@@ -410,6 +413,39 @@ import { cfg } from '~shared/config';
|
|
|
410
413
|
expect(details.includes("Path alias '@/utils/missing' does not resolve to a project file")
|
|
411
414
|
|| details.includes("Package '@/utils' not in package.json dependencies")).toBe(true);
|
|
412
415
|
});
|
|
416
|
+
it('should resolve configured aliases to hidden generated files', async () => {
|
|
417
|
+
const jsContent = `import { docs } from '@/.source';`;
|
|
418
|
+
const tsconfigContent = `{
|
|
419
|
+
"compilerOptions": {
|
|
420
|
+
"paths": {
|
|
421
|
+
"@/*": ["./*"],
|
|
422
|
+
"@/.source": ["./.source/index.ts"]
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}`;
|
|
426
|
+
FileScanner.findFiles.mockResolvedValue(['docs-site/lib/source.ts']);
|
|
427
|
+
mockReadFile.mockImplementation(async (p) => {
|
|
428
|
+
const normalized = normalizePath(p);
|
|
429
|
+
if (normalized.endsWith('/docs-site/tsconfig.json'))
|
|
430
|
+
return tsconfigContent;
|
|
431
|
+
return jsContent;
|
|
432
|
+
});
|
|
433
|
+
mockPathExists.mockImplementation(async (p) => {
|
|
434
|
+
const normalized = normalizePath(p);
|
|
435
|
+
return normalized === `${testCwdNormalized}/docs-site/tsconfig.json`
|
|
436
|
+
|| normalized === `${testCwdNormalized}/docs-site/.source/index.ts`
|
|
437
|
+
|| normalized === `${testCwdNormalized}/package.json`;
|
|
438
|
+
});
|
|
439
|
+
mockStat.mockResolvedValue({ isFile: () => true });
|
|
440
|
+
mockReadJson.mockResolvedValue({
|
|
441
|
+
dependencies: {},
|
|
442
|
+
devDependencies: {},
|
|
443
|
+
peerDependencies: {},
|
|
444
|
+
optionalDependencies: {},
|
|
445
|
+
});
|
|
446
|
+
const failures = await gate.run(context);
|
|
447
|
+
expect(failures).toHaveLength(0);
|
|
448
|
+
});
|
|
413
449
|
it('should NOT flag ESM .js specifiers that resolve to .ts source files', async () => {
|
|
414
450
|
const jsContent = `
|
|
415
451
|
import { helper } from './utils.js';
|
|
@@ -14,11 +14,14 @@ export interface DLPBlockManifest {
|
|
|
14
14
|
timestamp: string;
|
|
15
15
|
detections: Array<{
|
|
16
16
|
type: string;
|
|
17
|
-
match: string;
|
|
18
17
|
fingerprint: string;
|
|
19
|
-
|
|
18
|
+
confidence: number;
|
|
20
19
|
}>;
|
|
21
20
|
}
|
|
21
|
+
export declare function isLearnableDLPDetection(detection: {
|
|
22
|
+
type: string;
|
|
23
|
+
confidence?: number;
|
|
24
|
+
}): boolean;
|
|
22
25
|
/** Normalize a matched value to a shape for learning (preserves structure, not secret content). */
|
|
23
26
|
export declare function shapeValue(value: string): string;
|
|
24
27
|
export declare function fingerprintDetection(type: string, match: string, line?: string): string;
|
|
@@ -28,9 +31,10 @@ export declare function lastBlockPath(cwd: string): string;
|
|
|
28
31
|
export declare function recordDLPFeedback(cwd: string, entry: {
|
|
29
32
|
type: string;
|
|
30
33
|
match: string;
|
|
34
|
+
confidence?: number;
|
|
31
35
|
line?: string;
|
|
32
36
|
source?: DLPFeedbackEntry['source'];
|
|
33
|
-
}): Promise<string>;
|
|
37
|
+
}): Promise<string | null>;
|
|
34
38
|
export declare function isLearnedAllow(cwd: string | undefined, type: string, match: string, _line?: string): Promise<boolean>;
|
|
35
39
|
export declare function isLearnedAllowSync(store: DLPFeedbackStore, type: string, match: string, _line?: string): boolean;
|
|
36
40
|
export declare function loadDLPFeedbackStore(cwd: string): Promise<DLPFeedbackStore>;
|
|
@@ -39,10 +43,12 @@ export declare function getLearnedAllowCount(cwd: string): Promise<number>;
|
|
|
39
43
|
export declare function loadDLPFeedbackStoreSync(cwd: string): DLPFeedbackStore;
|
|
40
44
|
export declare function writeDLPBlockManifest(cwd: string, detections: Array<{
|
|
41
45
|
type: string;
|
|
42
|
-
match
|
|
46
|
+
match?: string;
|
|
47
|
+
feedback_fingerprint?: string;
|
|
48
|
+
confidence?: number;
|
|
43
49
|
position?: {
|
|
44
50
|
start: number;
|
|
45
51
|
end: number;
|
|
46
52
|
};
|
|
47
|
-
}>,
|
|
53
|
+
}>, _input: string): Promise<void>;
|
|
48
54
|
export declare function allowLastDLPBlock(cwd: string, source?: DLPFeedbackEntry['source']): Promise<number>;
|
|
@@ -5,6 +5,31 @@
|
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
const DLP_BLOCK_MANIFEST_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
const MAX_LEARNABLE_CONFIDENCE = 79;
|
|
10
|
+
const NON_LEARNABLE_TYPES = new Set([
|
|
11
|
+
'private_key',
|
|
12
|
+
'private_key_full',
|
|
13
|
+
'gcp_service_account',
|
|
14
|
+
'bearer_token',
|
|
15
|
+
'jwt_token',
|
|
16
|
+
'aws_secret_key',
|
|
17
|
+
'azure_key',
|
|
18
|
+
'anthropic_key',
|
|
19
|
+
'slack_token',
|
|
20
|
+
'sendgrid_key',
|
|
21
|
+
'aws_access_key',
|
|
22
|
+
'openai_key',
|
|
23
|
+
'github_token',
|
|
24
|
+
'stripe_key',
|
|
25
|
+
'twilio_key',
|
|
26
|
+
'high_entropy_secret',
|
|
27
|
+
]);
|
|
28
|
+
export function isLearnableDLPDetection(detection) {
|
|
29
|
+
const confidence = detection.confidence ?? 100;
|
|
30
|
+
return !NON_LEARNABLE_TYPES.has(detection.type)
|
|
31
|
+
&& confidence <= MAX_LEARNABLE_CONFIDENCE;
|
|
32
|
+
}
|
|
8
33
|
/** Normalize a matched value to a shape for learning (preserves structure, not secret content). */
|
|
9
34
|
export function shapeValue(value) {
|
|
10
35
|
return value
|
|
@@ -55,8 +80,9 @@ async function saveStore(cwd, store) {
|
|
|
55
80
|
await fs.writeJson(feedbackPath(cwd), store, { spaces: 2 });
|
|
56
81
|
}
|
|
57
82
|
export async function recordDLPFeedback(cwd, entry) {
|
|
83
|
+
if (!isLearnableDLPDetection(entry))
|
|
84
|
+
return null;
|
|
58
85
|
const fingerprint = feedbackFingerprint(entry.type, entry.match);
|
|
59
|
-
const shape = extractValueShape(entry.match);
|
|
60
86
|
const store = await loadStore(cwd);
|
|
61
87
|
const existing = store.entries.find(e => e.fingerprint === fingerprint);
|
|
62
88
|
const now = new Date().toISOString();
|
|
@@ -68,7 +94,7 @@ export async function recordDLPFeedback(cwd, entry) {
|
|
|
68
94
|
store.entries.push({
|
|
69
95
|
fingerprint,
|
|
70
96
|
type: entry.type,
|
|
71
|
-
shape,
|
|
97
|
+
shape: '[redacted]',
|
|
72
98
|
hits: 1,
|
|
73
99
|
lastSeenAt: now,
|
|
74
100
|
source: entry.source ?? 'hook',
|
|
@@ -119,37 +145,62 @@ function getLineAt(input, start) {
|
|
|
119
145
|
const lineEnd = input.indexOf('\n', start);
|
|
120
146
|
return input.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
|
|
121
147
|
}
|
|
122
|
-
export async function writeDLPBlockManifest(cwd, detections,
|
|
148
|
+
export async function writeDLPBlockManifest(cwd, detections, _input) {
|
|
123
149
|
const seen = new Set();
|
|
124
150
|
const manifestDetections = [];
|
|
125
151
|
for (const d of detections) {
|
|
126
|
-
|
|
152
|
+
if (!isLearnableDLPDetection(d))
|
|
153
|
+
continue;
|
|
154
|
+
const fp = d.feedback_fingerprint
|
|
155
|
+
?? (d.match ? feedbackFingerprint(d.type, d.match) : null);
|
|
156
|
+
if (!fp)
|
|
157
|
+
continue;
|
|
127
158
|
if (seen.has(fp))
|
|
128
159
|
continue;
|
|
129
160
|
seen.add(fp);
|
|
130
161
|
manifestDetections.push({
|
|
131
162
|
type: d.type,
|
|
132
|
-
match: d.match.slice(0, 80),
|
|
133
163
|
fingerprint: fp,
|
|
134
|
-
|
|
164
|
+
confidence: d.confidence ?? 100,
|
|
135
165
|
});
|
|
136
166
|
}
|
|
167
|
+
const manifestPath = lastBlockPath(cwd);
|
|
168
|
+
if (manifestDetections.length === 0) {
|
|
169
|
+
await fs.remove(manifestPath);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
137
172
|
const manifest = {
|
|
138
173
|
timestamp: new Date().toISOString(),
|
|
139
174
|
detections: manifestDetections,
|
|
140
175
|
};
|
|
141
|
-
await fs.ensureDir(path.dirname(
|
|
142
|
-
await fs.writeJson(
|
|
176
|
+
await fs.ensureDir(path.dirname(manifestPath));
|
|
177
|
+
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
143
178
|
}
|
|
144
179
|
export async function allowLastDLPBlock(cwd, source = 'hook') {
|
|
145
180
|
const manifestPath = lastBlockPath(cwd);
|
|
146
181
|
if (!await fs.pathExists(manifestPath))
|
|
147
182
|
return 0;
|
|
148
|
-
|
|
183
|
+
let manifest;
|
|
184
|
+
try {
|
|
185
|
+
manifest = await fs.readJson(manifestPath);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
await fs.remove(manifestPath);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
await fs.remove(manifestPath);
|
|
192
|
+
const manifestAge = Date.now() - Date.parse(manifest.timestamp);
|
|
193
|
+
if (!Number.isFinite(manifestAge)
|
|
194
|
+
|| manifestAge < 0
|
|
195
|
+
|| manifestAge > DLP_BLOCK_MANIFEST_TTL_MS
|
|
196
|
+
|| !Array.isArray(manifest.detections))
|
|
197
|
+
return 0;
|
|
149
198
|
const store = await loadStore(cwd);
|
|
150
199
|
const now = new Date().toISOString();
|
|
151
200
|
let count = 0;
|
|
152
201
|
for (const d of manifest.detections ?? []) {
|
|
202
|
+
if (!isLearnableDLPDetection(d))
|
|
203
|
+
continue;
|
|
153
204
|
const existing = store.entries.find(e => e.fingerprint === d.fingerprint);
|
|
154
205
|
if (existing) {
|
|
155
206
|
existing.hits += 1;
|
|
@@ -159,7 +210,7 @@ export async function allowLastDLPBlock(cwd, source = 'hook') {
|
|
|
159
210
|
store.entries.push({
|
|
160
211
|
fingerprint: d.fingerprint,
|
|
161
212
|
type: d.type,
|
|
162
|
-
shape:
|
|
213
|
+
shape: '[redacted]',
|
|
163
214
|
hits: 1,
|
|
164
215
|
lastSeenAt: now,
|
|
165
216
|
source,
|
|
@@ -4,6 +4,7 @@ import os from 'os';
|
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
5
|
import { recordDLPFeedback, feedbackFingerprint, loadDLPFeedbackStoreSync, isLearnedAllowSync, writeDLPBlockManifest, allowLastDLPBlock, } from './dlp-feedback.js';
|
|
6
6
|
import { scanInputForCredentials } from './input-validator.js';
|
|
7
|
+
import { generateDLPHookFiles } from './dlp-templates.js';
|
|
7
8
|
describe('DLP feedback learning', () => {
|
|
8
9
|
const testCwd = path.join(os.tmpdir(), `rigour-dlp-fb-${Date.now()}`);
|
|
9
10
|
beforeEach(async () => {
|
|
@@ -19,7 +20,12 @@ describe('DLP feedback learning', () => {
|
|
|
19
20
|
});
|
|
20
21
|
it('records and applies learned false positives', async () => {
|
|
21
22
|
const match = 'api_key = "Z9y8X7w6V5u4T3s2R1q0P9o8N7m6B5c4"';
|
|
22
|
-
await recordDLPFeedback(testCwd, {
|
|
23
|
+
await recordDLPFeedback(testCwd, {
|
|
24
|
+
type: 'password_assignment',
|
|
25
|
+
match,
|
|
26
|
+
confidence: 55,
|
|
27
|
+
source: 'hook',
|
|
28
|
+
});
|
|
23
29
|
const store = loadDLPFeedbackStoreSync(testCwd);
|
|
24
30
|
expect(isLearnedAllowSync(store, 'password_assignment', match)).toBe(true);
|
|
25
31
|
const result = scanInputForCredentials(match, { cwd: testCwd });
|
|
@@ -36,4 +42,90 @@ describe('DLP feedback learning', () => {
|
|
|
36
42
|
const retry = scanInputForCredentials(text, { cwd: testCwd });
|
|
37
43
|
expect(retry.status).toBe('clean');
|
|
38
44
|
});
|
|
45
|
+
it('never learns provider or high-confidence detections', async () => {
|
|
46
|
+
const provider = await recordDLPFeedback(testCwd, {
|
|
47
|
+
type: 'openai_key',
|
|
48
|
+
match: 'sk-proj-Z9Y8X7W6V5U4T3S2R1Q0P9O8N7M6',
|
|
49
|
+
confidence: 92,
|
|
50
|
+
});
|
|
51
|
+
const highConfidence = await recordDLPFeedback(testCwd, {
|
|
52
|
+
type: 'password_assignment',
|
|
53
|
+
match: 'password = "Z9y8X7w6V5u4T3s2R1q0P9o8N7m6B5c4"',
|
|
54
|
+
confidence: 95,
|
|
55
|
+
});
|
|
56
|
+
expect(provider).toBeNull();
|
|
57
|
+
expect(highConfidence).toBeNull();
|
|
58
|
+
expect(loadDLPFeedbackStoreSync(testCwd).entries).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
it('does not retain raw credential fragments in manifests', async () => {
|
|
61
|
+
const text = 'api_key = "Z9y8X7w6V5u4T3s2R1q0P9o8N7m6B5c4"';
|
|
62
|
+
const result = scanInputForCredentials(text);
|
|
63
|
+
expect(result.detections[0].match).toBe(result.detections[0].redacted);
|
|
64
|
+
expect(result.detections[0].match).not.toContain('Z9y8X7w6V5u4T3s2R1q0P9o8N7m6B5c4');
|
|
65
|
+
await writeDLPBlockManifest(testCwd, result.detections, text);
|
|
66
|
+
const manifest = fs.readFileSync(path.join(testCwd, '.rigour', 'dlp-last-block.json'), 'utf-8');
|
|
67
|
+
expect(manifest).not.toContain('Z9y8X7w6V5u4T3s2R1q0P9o8N7m6B5c4');
|
|
68
|
+
expect(manifest).not.toContain('match');
|
|
69
|
+
expect(manifest).not.toContain('shape');
|
|
70
|
+
});
|
|
71
|
+
it('rejects and consumes expired manifests', async () => {
|
|
72
|
+
const text = 'api_key = "Z9y8X7w6V5u4T3s2R1q0P9o8N7m6B5c4"';
|
|
73
|
+
const result = scanInputForCredentials(text);
|
|
74
|
+
await writeDLPBlockManifest(testCwd, result.detections, text);
|
|
75
|
+
const manifestPath = path.join(testCwd, '.rigour', 'dlp-last-block.json');
|
|
76
|
+
const manifest = fs.readJsonSync(manifestPath);
|
|
77
|
+
manifest.timestamp = '2020-01-01T00:00:00.000Z';
|
|
78
|
+
fs.writeJsonSync(manifestPath, manifest);
|
|
79
|
+
expect(await allowLastDLPBlock(testCwd, 'hook')).toBe(0);
|
|
80
|
+
expect(fs.existsSync(manifestPath)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it('rejects and consumes malformed manifests', async () => {
|
|
83
|
+
const manifestPath = path.join(testCwd, '.rigour', 'dlp-last-block.json');
|
|
84
|
+
fs.writeFileSync(manifestPath, '{not-json');
|
|
85
|
+
expect(await allowLastDLPBlock(testCwd, 'hook')).toBe(0);
|
|
86
|
+
expect(fs.existsSync(manifestPath)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('DLP scanner URL boundaries', () => {
|
|
90
|
+
it('detects genuine HTTP basic-auth credentials', () => {
|
|
91
|
+
const result = scanInputForCredentials('https://alice:S3cr3t-Prod-9842@example.com/v1');
|
|
92
|
+
expect(result.status).toBe('warning');
|
|
93
|
+
expect(result.detections.some(d => d.type === 'credentials_in_url')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('allows signed GCS URLs without URL user-info', () => {
|
|
96
|
+
const url = 'https://storage.googleapis.com/example/archive.zip'
|
|
97
|
+
+ '?X-Goog-Credential=service%40example.invalid&X-Goog-Signature=redacted';
|
|
98
|
+
expect(scanInputForCredentials(url).status).toBe('clean');
|
|
99
|
+
});
|
|
100
|
+
it('does not cross multiline JSON boundaries while scanning URLs', () => {
|
|
101
|
+
const input = `{
|
|
102
|
+
"task_archive_url": "https://storage.googleapis.com/example/archive.zip",
|
|
103
|
+
"provider": { "npm": "@ai-sdk/openai-compatible" }
|
|
104
|
+
}`;
|
|
105
|
+
expect(scanInputForCredentials(input).status).toBe('clean');
|
|
106
|
+
});
|
|
107
|
+
it('allows nested OpenCode configuration with scoped npm packages', () => {
|
|
108
|
+
const config = JSON.stringify({
|
|
109
|
+
$schema: 'https://opencode.ai/config.json',
|
|
110
|
+
provider: {
|
|
111
|
+
moonshot: {
|
|
112
|
+
npm: '@ai-sdk/openai-compatible',
|
|
113
|
+
options: {
|
|
114
|
+
baseURL: 'https://api.moonshot.ai/v1',
|
|
115
|
+
apiKey: '{env:OPENAI_API_KEY}',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}, null, 2);
|
|
120
|
+
expect(scanInputForCredentials(config).status).toBe('clean');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('DLP hook warning templates', () => {
|
|
124
|
+
it.each(['cursor', 'cline', 'windsurf'])('generates non-blocking %s warnings', (tool) => {
|
|
125
|
+
const content = generateDLPHookFiles(tool, 'rigour hooks check')
|
|
126
|
+
.map(file => file.content)
|
|
127
|
+
.join('\n');
|
|
128
|
+
expect(content).toContain('possible credential');
|
|
129
|
+
expect(content).not.toContain('process.exit(2)');
|
|
130
|
+
});
|
|
39
131
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DLP Hook Templates — Pre-Input Credential
|
|
2
|
+
* DLP Hook Templates — Pre-Input Credential Warnings
|
|
3
3
|
*
|
|
4
4
|
* Generates tool-native hook configs that intercept user input
|
|
5
|
-
*
|
|
5
|
+
* before agent actions. Complements the existing
|
|
6
6
|
* post-output templates in templates.ts.
|
|
7
7
|
*
|
|
8
8
|
* Hook events:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DLP Hook Templates — Pre-Input Credential
|
|
2
|
+
* DLP Hook Templates — Pre-Input Credential Warnings
|
|
3
3
|
*
|
|
4
4
|
* Generates tool-native hook configs that intercept user input
|
|
5
|
-
*
|
|
5
|
+
* before agent actions. Complements the existing
|
|
6
6
|
* post-output templates in templates.ts.
|
|
7
7
|
*
|
|
8
8
|
* Hook events:
|
|
@@ -32,7 +32,7 @@ export function generateDLPHookFiles(tool, checkerCommand) {
|
|
|
32
32
|
// ── Claude Code DLP Hook ──────────────────────────────────────────
|
|
33
33
|
function generateClaudeDLPHooks(checkerCommand) {
|
|
34
34
|
// Claude Code supports PreToolUse hooks that fire BEFORE tool execution.
|
|
35
|
-
//
|
|
35
|
+
// Scan tool input and report warnings without blocking by default.
|
|
36
36
|
const settings = {
|
|
37
37
|
hooks: {
|
|
38
38
|
PreToolUse: [
|
|
@@ -102,13 +102,12 @@ process.stdin.on('end', async () => {
|
|
|
102
102
|
if (proc.error) throw proc.error;
|
|
103
103
|
|
|
104
104
|
const result = JSON.parse((proc.stdout || '').trim());
|
|
105
|
-
if (result.status
|
|
106
|
-
process.stderr.write('[rigour/dlp] ' + result.detections.length + ' credential(s)
|
|
105
|
+
if (result.status !== 'clean') {
|
|
106
|
+
process.stderr.write('[rigour/dlp] ' + result.detections.length + ' possible credential(s) detected\\n');
|
|
107
107
|
for (const d of result.detections) {
|
|
108
|
-
process.stderr.write(' [
|
|
108
|
+
process.stderr.write(' [WARNING] ' + d.description + '\\n');
|
|
109
109
|
process.stderr.write(' → ' + d.recommendation + '\\n');
|
|
110
110
|
}
|
|
111
|
-
process.exit(2); // Block the operation
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
process.stdout.write(JSON.stringify({ status: 'ok' }));
|
|
@@ -128,7 +127,7 @@ process.stdin.on('end', async () => {
|
|
|
128
127
|
path: '.cursor/rigour-dlp-hook.js',
|
|
129
128
|
content: wrapper,
|
|
130
129
|
executable: true,
|
|
131
|
-
description: 'Cursor DLP hook wrapper — credential
|
|
130
|
+
description: 'Cursor DLP hook wrapper — credential warnings',
|
|
132
131
|
},
|
|
133
132
|
];
|
|
134
133
|
}
|
|
@@ -178,15 +177,14 @@ process.stdin.on('end', async () => {
|
|
|
178
177
|
if (proc.error) throw proc.error;
|
|
179
178
|
|
|
180
179
|
const result = JSON.parse((proc.stdout || '').trim());
|
|
181
|
-
if (result.status
|
|
180
|
+
if (result.status !== 'clean') {
|
|
182
181
|
const msgs = result.detections
|
|
183
182
|
.map(d => '[rigour/dlp/' + d.type + '] ' + d.description + ' → ' + d.recommendation)
|
|
184
183
|
.join('\\n');
|
|
185
184
|
|
|
186
185
|
process.stdout.write(JSON.stringify({
|
|
187
|
-
contextModification: '\\n
|
|
186
|
+
contextModification: '\\n⚠️ [Rigour DLP] ' + result.detections.length + ' possible credential(s) detected:\\n' + msgs,
|
|
188
187
|
}));
|
|
189
|
-
process.exit(2);
|
|
190
188
|
} else {
|
|
191
189
|
process.stdout.write(JSON.stringify({}));
|
|
192
190
|
}
|
|
@@ -201,7 +199,7 @@ process.stdin.on('end', async () => {
|
|
|
201
199
|
path: '.clinerules/hooks/PreToolUse',
|
|
202
200
|
content: script,
|
|
203
201
|
executable: true,
|
|
204
|
-
description: 'Cline PreToolUse DLP hook — credential
|
|
202
|
+
description: 'Cline PreToolUse DLP hook — credential warnings before agent execution',
|
|
205
203
|
},
|
|
206
204
|
];
|
|
207
205
|
}
|
|
@@ -249,12 +247,11 @@ process.stdin.on('end', async () => {
|
|
|
249
247
|
if (proc.error) throw proc.error;
|
|
250
248
|
|
|
251
249
|
const result = JSON.parse((proc.stdout || '').trim());
|
|
252
|
-
if (result.status
|
|
250
|
+
if (result.status !== 'clean') {
|
|
253
251
|
for (const d of result.detections) {
|
|
254
|
-
process.stderr.write('[rigour/dlp]
|
|
252
|
+
process.stderr.write('[rigour/dlp] ⚠ possible credential: ' + d.description + '\\n');
|
|
255
253
|
process.stderr.write(' → ' + d.recommendation + '\\n');
|
|
256
254
|
}
|
|
257
|
-
process.exit(2); // Block
|
|
258
255
|
}
|
|
259
256
|
} catch (err) {
|
|
260
257
|
process.stderr.write('Rigour DLP hook error: ' + err.message + '\\n');
|
|
@@ -271,7 +268,7 @@ process.stdin.on('end', async () => {
|
|
|
271
268
|
path: '.windsurf/rigour-dlp-hook.js',
|
|
272
269
|
content: wrapper,
|
|
273
270
|
executable: true,
|
|
274
|
-
description: 'Windsurf DLP hook wrapper — credential
|
|
271
|
+
description: 'Windsurf DLP hook wrapper — credential warnings before Cascade',
|
|
275
272
|
},
|
|
276
273
|
];
|
|
277
274
|
}
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
export interface CredentialDetection {
|
|
20
20
|
type: string;
|
|
21
21
|
severity: 'critical' | 'high' | 'medium';
|
|
22
|
+
/** Redacted match only. Raw credential material never leaves the scanner. */
|
|
22
23
|
match: string;
|
|
24
|
+
feedback_fingerprint?: string;
|
|
23
25
|
redacted: string;
|
|
24
26
|
description: string;
|
|
25
27
|
recommendation: string;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
*/
|
|
19
19
|
import { classifyDLPDetection } from './dlp-confidence.js';
|
|
20
|
-
import { isLearnedAllowSync, loadDLPFeedbackStoreSync } from './dlp-feedback.js';
|
|
20
|
+
import { feedbackFingerprint, isLearnableDLPDetection, isLearnedAllowSync, loadDLPFeedbackStoreSync, } from './dlp-feedback.js';
|
|
21
21
|
const CREDENTIAL_PATTERNS = [
|
|
22
22
|
// ── Cloud Provider Keys ───────────────────────────────────
|
|
23
23
|
{
|
|
@@ -174,7 +174,7 @@ const CREDENTIAL_PATTERNS = [
|
|
|
174
174
|
// ── IP Addresses with Credentials ─────────────────────────
|
|
175
175
|
{
|
|
176
176
|
type: 'credentials_in_url',
|
|
177
|
-
regex: /\b(https?:\/\/[
|
|
177
|
+
regex: /\b(https?:\/\/[^/\s:@]+:[^/\s@]+@[^\s'"`,;}{)]+)\b/gi,
|
|
178
178
|
severity: 'critical',
|
|
179
179
|
description: 'URL with embedded credentials detected',
|
|
180
180
|
recommendation: 'Remove credentials from URLs. Use environment variables for auth',
|
|
@@ -406,24 +406,25 @@ export function scanInputForCredentials(input, config = {}) {
|
|
|
406
406
|
? loadDLPFeedbackStoreSync(config.cwd)
|
|
407
407
|
: null;
|
|
408
408
|
const classified = detections.map(d => {
|
|
409
|
-
|
|
409
|
+
const baseline = {
|
|
410
|
+
...d,
|
|
411
|
+
...classifyDLPDetection(d, input),
|
|
412
|
+
};
|
|
413
|
+
if (feedbackStore && isLearnableDLPDetection(baseline)) {
|
|
410
414
|
const start = d.position?.start ?? 0;
|
|
411
415
|
const lineStart = input.lastIndexOf('\n', start) + 1;
|
|
412
416
|
const lineEnd = input.indexOf('\n', start);
|
|
413
417
|
const line = input.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
|
|
414
418
|
if (isLearnedAllowSync(feedbackStore, d.type, d.match, line)) {
|
|
415
419
|
return {
|
|
416
|
-
...
|
|
420
|
+
...baseline,
|
|
417
421
|
confidence: 12,
|
|
418
422
|
decision: 'allow',
|
|
419
423
|
reason_codes: ['learned_false_positive'],
|
|
420
424
|
};
|
|
421
425
|
}
|
|
422
426
|
}
|
|
423
|
-
return
|
|
424
|
-
...d,
|
|
425
|
-
...classifyDLPDetection(d, input),
|
|
426
|
-
};
|
|
427
|
+
return baseline;
|
|
427
428
|
});
|
|
428
429
|
// Line-level learned expansion: one false-positive mark allows sibling patterns on same line
|
|
429
430
|
const learnedStarts = new Set(classified
|
|
@@ -431,7 +432,10 @@ export function scanInputForCredentials(input, config = {}) {
|
|
|
431
432
|
.map(d => d.position?.start ?? -1));
|
|
432
433
|
const expanded = classified.map(d => {
|
|
433
434
|
const start = d.position?.start ?? -1;
|
|
434
|
-
if (start >= 0
|
|
435
|
+
if (start >= 0
|
|
436
|
+
&& learnedStarts.has(start)
|
|
437
|
+
&& d.decision !== 'allow'
|
|
438
|
+
&& isLearnableDLPDetection(d)) {
|
|
435
439
|
return {
|
|
436
440
|
...d,
|
|
437
441
|
confidence: 12,
|
|
@@ -458,8 +462,10 @@ export function scanInputForCredentials(input, config = {}) {
|
|
|
458
462
|
: 'warning';
|
|
459
463
|
return {
|
|
460
464
|
status,
|
|
461
|
-
detections: deduped,
|
|
462
|
-
...(allowedDeduped.length > 0
|
|
465
|
+
detections: deduped.map(toPublicDetection),
|
|
466
|
+
...(allowedDeduped.length > 0
|
|
467
|
+
? { allowed_detections: allowedDeduped.map(toPublicDetection) }
|
|
468
|
+
: {}),
|
|
463
469
|
duration_ms: Date.now() - start,
|
|
464
470
|
scanned_length: input.length,
|
|
465
471
|
};
|
|
@@ -467,6 +473,14 @@ export function scanInputForCredentials(input, config = {}) {
|
|
|
467
473
|
/**
|
|
468
474
|
* Deduplicate overlapping detections — keep the higher severity one.
|
|
469
475
|
*/
|
|
476
|
+
function toPublicDetection(detection) {
|
|
477
|
+
const { match, ...safe } = detection;
|
|
478
|
+
return {
|
|
479
|
+
...safe,
|
|
480
|
+
match: detection.redacted,
|
|
481
|
+
feedback_fingerprint: feedbackFingerprint(detection.type, match),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
470
484
|
function deduplicateDetections(detections) {
|
|
471
485
|
if (detections.length <= 1)
|
|
472
486
|
return detections;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* InputValidationGate, and outputs JSON result to stdout.
|
|
7
7
|
*
|
|
8
8
|
* Exit codes:
|
|
9
|
-
* 0 — clean
|
|
10
|
-
* 2 — blocked
|
|
9
|
+
* 0 — clean or warning (default)
|
|
10
|
+
* 2 — blocked when --block is explicitly set
|
|
11
11
|
*
|
|
12
12
|
* Usage:
|
|
13
13
|
* echo "my api_key = sk-abc123..." | node standalone-dlp-checker.js
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* InputValidationGate, and outputs JSON result to stdout.
|
|
7
7
|
*
|
|
8
8
|
* Exit codes:
|
|
9
|
-
* 0 — clean
|
|
10
|
-
* 2 — blocked
|
|
9
|
+
* 0 — clean or warning (default)
|
|
10
|
+
* 2 — blocked when --block is explicitly set
|
|
11
11
|
*
|
|
12
12
|
* Usage:
|
|
13
13
|
* echo "my api_key = sk-abc123..." | node standalone-dlp-checker.js
|
|
@@ -20,7 +20,7 @@ import path from 'path';
|
|
|
20
20
|
async function main() {
|
|
21
21
|
const args = process.argv.slice(2);
|
|
22
22
|
const isJson = args.includes('--json');
|
|
23
|
-
const block =
|
|
23
|
+
const block = args.includes('--block');
|
|
24
24
|
const agent = args.find(a => a.startsWith('--agent='))?.split('=')[1] || 'unknown';
|
|
25
25
|
// Read stdin
|
|
26
26
|
const chunks = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export * from './utils/logger.js';
|
|
|
12
12
|
export { FileScanner } from './utils/scanner.js';
|
|
13
13
|
export * from './services/score-history.js';
|
|
14
14
|
export * from './hooks/index.js';
|
|
15
|
-
export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, updateCursorApiKey } from './settings.js';
|
|
15
|
+
export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, isCursorApiKeyConfigured, getCursorApiKeyHint, updateCursorApiKey, removeCursorApiKey, } from './settings.js';
|
|
16
16
|
export type { RigourSettings, ResolvedDeepOptions, CLIDeepOptions } from './settings.js';
|
|
17
17
|
export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
18
18
|
export { createProvider } from './inference/index.js';
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { FileScanner } from './utils/scanner.js';
|
|
|
13
13
|
export * from './services/score-history.js';
|
|
14
14
|
export * from './hooks/index.js';
|
|
15
15
|
// Settings Module (Global user config at ~/.rigour/settings.json)
|
|
16
|
-
export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, updateCursorApiKey } from './settings.js';
|
|
16
|
+
export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, isCursorApiKeyConfigured, getCursorApiKeyHint, updateCursorApiKey, removeCursorApiKey, } from './settings.js';
|
|
17
17
|
// Deep Analysis Pipeline (v4.0+)
|
|
18
18
|
export { DeepAnalysisGate } from './gates/deep-analysis.js';
|
|
19
19
|
export { createProvider } from './inference/index.js';
|
|
@@ -20,7 +20,10 @@ export interface ScoreEntry {
|
|
|
20
20
|
}
|
|
21
21
|
export interface ScoreTrend {
|
|
22
22
|
direction: 'improving' | 'stable' | 'degrading';
|
|
23
|
+
/** Difference between recent and previous window averages. */
|
|
23
24
|
delta: number;
|
|
25
|
+
/** Difference between the first and last score displayed to the user. */
|
|
26
|
+
visibleDelta: number;
|
|
24
27
|
recentAvg: number;
|
|
25
28
|
previousAvg: number;
|
|
26
29
|
recentScores: number[];
|
|
@@ -48,6 +51,7 @@ export declare function recordScore(cwd: string, report: {
|
|
|
48
51
|
export declare function getScoreHistory(cwd: string, limit?: number): ScoreEntry[];
|
|
49
52
|
/**
|
|
50
53
|
* Calculate score trend from history.
|
|
51
|
-
*
|
|
54
|
+
* Direction and delta describe the same recent scores shown to users.
|
|
55
|
+
* Window averages remain available for detailed historical comparison.
|
|
52
56
|
*/
|
|
53
57
|
export declare function getScoreTrend(cwd: string): ScoreTrend | null;
|
|
@@ -65,7 +65,8 @@ export function getScoreHistory(cwd, limit = 20) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Calculate score trend from history.
|
|
68
|
-
*
|
|
68
|
+
* Direction and delta describe the same recent scores shown to users.
|
|
69
|
+
* Window averages remain available for detailed historical comparison.
|
|
69
70
|
*/
|
|
70
71
|
export function getScoreTrend(cwd) {
|
|
71
72
|
const history = getScoreHistory(cwd, 20);
|
|
@@ -75,27 +76,21 @@ export function getScoreTrend(cwd) {
|
|
|
75
76
|
const recentScores = scores.slice(-5);
|
|
76
77
|
const previousScores = scores.slice(-10, -5);
|
|
77
78
|
const recentAvg = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
previousAvg: Math.round(recentAvg),
|
|
84
|
-
recentScores,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
const previousAvg = previousScores.reduce((a, b) => a + b, 0) / previousScores.length;
|
|
88
|
-
const delta = recentAvg - previousAvg;
|
|
79
|
+
const previousAvg = previousScores.length > 0
|
|
80
|
+
? previousScores.reduce((a, b) => a + b, 0) / previousScores.length
|
|
81
|
+
: recentAvg;
|
|
82
|
+
const windowDelta = recentAvg - previousAvg;
|
|
83
|
+
const visibleDelta = recentScores[recentScores.length - 1] - recentScores[0];
|
|
89
84
|
// If all recent scores are the same (e.g. all 0 or all 100), trend is stable
|
|
90
85
|
const allSame = recentScores.every(s => s === recentScores[0]);
|
|
91
86
|
let direction;
|
|
92
|
-
if (allSame
|
|
87
|
+
if (allSame) {
|
|
93
88
|
direction = 'stable';
|
|
94
89
|
}
|
|
95
|
-
else if (
|
|
90
|
+
else if (visibleDelta > 3) {
|
|
96
91
|
direction = 'improving';
|
|
97
92
|
}
|
|
98
|
-
else if (
|
|
93
|
+
else if (visibleDelta < -3) {
|
|
99
94
|
direction = 'degrading';
|
|
100
95
|
}
|
|
101
96
|
else {
|
|
@@ -103,7 +98,8 @@ export function getScoreTrend(cwd) {
|
|
|
103
98
|
}
|
|
104
99
|
return {
|
|
105
100
|
direction,
|
|
106
|
-
delta: Math.round(
|
|
101
|
+
delta: Math.round(windowDelta * 10) / 10,
|
|
102
|
+
visibleDelta: Math.round(visibleDelta * 10) / 10,
|
|
107
103
|
recentAvg: Math.round(recentAvg),
|
|
108
104
|
previousAvg: Math.round(previousAvg),
|
|
109
105
|
recentScores,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { getScoreTrend } from './score-history.js';
|
|
6
|
+
describe('getScoreTrend', () => {
|
|
7
|
+
let testDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'score-history-test-'));
|
|
10
|
+
fs.mkdirSync(path.join(testDir, '.rigour'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
function writeScores(scores) {
|
|
16
|
+
const entries = scores.map((score, index) => ({
|
|
17
|
+
timestamp: new Date(index * 1000).toISOString(),
|
|
18
|
+
status: 'FAIL',
|
|
19
|
+
score,
|
|
20
|
+
failureCount: 1,
|
|
21
|
+
severity_breakdown: {},
|
|
22
|
+
provenance_breakdown: {},
|
|
23
|
+
}));
|
|
24
|
+
fs.writeFileSync(path.join(testDir, '.rigour', 'score-history.jsonl'), entries.map(entry => JSON.stringify(entry)).join('\n') + '\n');
|
|
25
|
+
}
|
|
26
|
+
it('labels the displayed rising scores as improving despite a higher older window', () => {
|
|
27
|
+
writeScores([90, 92, 94, 96, 98, 34, 29, 29, 52, 59]);
|
|
28
|
+
expect(getScoreTrend(testDir)).toMatchObject({
|
|
29
|
+
direction: 'improving',
|
|
30
|
+
delta: -53.4,
|
|
31
|
+
visibleDelta: 25,
|
|
32
|
+
recentScores: [34, 29, 29, 52, 59],
|
|
33
|
+
recentAvg: 41,
|
|
34
|
+
previousAvg: 94,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
it('labels a falling displayed series as degrading', () => {
|
|
38
|
+
writeScores([80, 75, 70, 64, 59]);
|
|
39
|
+
expect(getScoreTrend(testDir)).toMatchObject({
|
|
40
|
+
direction: 'degrading',
|
|
41
|
+
delta: 0,
|
|
42
|
+
visibleDelta: -21,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it('treats small visible changes as stable', () => {
|
|
46
|
+
writeScores([50, 49, 52, 51, 53]);
|
|
47
|
+
expect(getScoreTrend(testDir)).toMatchObject({
|
|
48
|
+
direction: 'stable',
|
|
49
|
+
delta: 0,
|
|
50
|
+
visibleDelta: 3,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
package/dist/settings.d.ts
CHANGED
|
@@ -106,10 +106,23 @@ export declare function updateProviderKey(provider: string, apiKey: string): voi
|
|
|
106
106
|
*/
|
|
107
107
|
export declare function removeProviderKey(provider: string): void;
|
|
108
108
|
/**
|
|
109
|
-
* Get Cursor Admin API key
|
|
109
|
+
* Get Cursor Admin API key — prefer env over file-stored secret.
|
|
110
|
+
* Env: RIGOUR_CURSOR_API_KEY or CURSOR_ADMIN_API_KEY
|
|
110
111
|
*/
|
|
111
112
|
export declare function getCursorApiKey(): string | undefined;
|
|
112
113
|
/**
|
|
113
|
-
*
|
|
114
|
+
* Whether a Cursor vendor adapter key is available (env or file).
|
|
115
|
+
*/
|
|
116
|
+
export declare function isCursorApiKeyConfigured(): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Last-4 hint for UI status — never return the full key.
|
|
119
|
+
*/
|
|
120
|
+
export declare function getCursorApiKeyHint(): string | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* Persist Cursor Admin API key to settings (optional; prefer env in production).
|
|
114
123
|
*/
|
|
115
124
|
export declare function updateCursorApiKey(apiKey: string): void;
|
|
125
|
+
/**
|
|
126
|
+
* Remove file-stored Cursor Admin API key (env vars are untouched).
|
|
127
|
+
*/
|
|
128
|
+
export declare function removeCursorApiKey(): void;
|
package/dist/settings.js
CHANGED
|
@@ -45,10 +45,22 @@ export function saveSettings(settings) {
|
|
|
45
45
|
const settingsPath = getSettingsPath();
|
|
46
46
|
const settingsDir = path.dirname(settingsPath);
|
|
47
47
|
try {
|
|
48
|
-
// Ensure directory exists
|
|
48
|
+
// Ensure directory exists with owner-only access
|
|
49
49
|
fs.ensureDirSync(settingsDir);
|
|
50
|
+
try {
|
|
51
|
+
fs.chmodSync(settingsDir, 0o700);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore chmod failures on platforms that do not support it
|
|
55
|
+
}
|
|
50
56
|
// Write with pretty formatting (2-space indent)
|
|
51
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
57
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
58
|
+
try {
|
|
59
|
+
fs.chmodSync(settingsPath, 0o600);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore chmod failures on platforms that do not support it
|
|
63
|
+
}
|
|
52
64
|
Logger.debug(`Settings saved to ${settingsPath}`);
|
|
53
65
|
}
|
|
54
66
|
catch (error) {
|
|
@@ -185,20 +197,58 @@ export function removeProviderKey(provider) {
|
|
|
185
197
|
}
|
|
186
198
|
}
|
|
187
199
|
/**
|
|
188
|
-
* Get Cursor Admin API key
|
|
200
|
+
* Get Cursor Admin API key — prefer env over file-stored secret.
|
|
201
|
+
* Env: RIGOUR_CURSOR_API_KEY or CURSOR_ADMIN_API_KEY
|
|
189
202
|
*/
|
|
190
203
|
export function getCursorApiKey() {
|
|
204
|
+
const fromEnv = process.env.RIGOUR_CURSOR_API_KEY?.trim() ||
|
|
205
|
+
process.env.CURSOR_ADMIN_API_KEY?.trim();
|
|
206
|
+
if (fromEnv)
|
|
207
|
+
return fromEnv;
|
|
191
208
|
const settings = loadSettings();
|
|
192
|
-
|
|
209
|
+
const fromFile = settings.cursor?.apiKey?.trim();
|
|
210
|
+
return fromFile || undefined;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Whether a Cursor vendor adapter key is available (env or file).
|
|
214
|
+
*/
|
|
215
|
+
export function isCursorApiKeyConfigured() {
|
|
216
|
+
return Boolean(getCursorApiKey());
|
|
193
217
|
}
|
|
194
218
|
/**
|
|
195
|
-
*
|
|
219
|
+
* Last-4 hint for UI status — never return the full key.
|
|
220
|
+
*/
|
|
221
|
+
export function getCursorApiKeyHint() {
|
|
222
|
+
const key = getCursorApiKey();
|
|
223
|
+
if (!key || key.length < 4)
|
|
224
|
+
return undefined;
|
|
225
|
+
return `••••${key.slice(-4)}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Persist Cursor Admin API key to settings (optional; prefer env in production).
|
|
196
229
|
*/
|
|
197
230
|
export function updateCursorApiKey(apiKey) {
|
|
231
|
+
const trimmed = apiKey.trim();
|
|
232
|
+
if (!trimmed || trimmed.length > 512) {
|
|
233
|
+
throw new Error('Invalid Cursor Admin API key');
|
|
234
|
+
}
|
|
198
235
|
const settings = loadSettings();
|
|
199
236
|
if (!settings.cursor) {
|
|
200
237
|
settings.cursor = {};
|
|
201
238
|
}
|
|
202
|
-
settings.cursor.apiKey =
|
|
239
|
+
settings.cursor.apiKey = trimmed;
|
|
203
240
|
saveSettings(settings);
|
|
204
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Remove file-stored Cursor Admin API key (env vars are untouched).
|
|
244
|
+
*/
|
|
245
|
+
export function removeCursorApiKey() {
|
|
246
|
+
const settings = loadSettings();
|
|
247
|
+
if (settings.cursor?.apiKey) {
|
|
248
|
+
delete settings.cursor.apiKey;
|
|
249
|
+
if (Object.keys(settings.cursor).length === 0) {
|
|
250
|
+
delete settings.cursor;
|
|
251
|
+
}
|
|
252
|
+
saveSettings(settings);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -62,6 +62,14 @@ export declare function recordModelUsage(usage: ModelUsage, cwd?: string): Promi
|
|
|
62
62
|
* Insert or update cache entry.
|
|
63
63
|
*/
|
|
64
64
|
export declare function setContextCacheRecord(record: ContextCacheRecord, cwd?: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* List cache records by type (and optional commit). Does not increment hit counts.
|
|
67
|
+
*/
|
|
68
|
+
export declare function listContextCacheRecords(opts: {
|
|
69
|
+
cacheType?: ContextCacheRecord['cacheType'];
|
|
70
|
+
commitSha?: string;
|
|
71
|
+
limit?: number;
|
|
72
|
+
}, cwd?: string): Promise<ContextCacheRecord[]>;
|
|
65
73
|
/**
|
|
66
74
|
* Fetch cached context record by cache key. Increments hit count.
|
|
67
75
|
*/
|
|
@@ -112,6 +112,67 @@ export async function setContextCacheRecord(record, cwd) {
|
|
|
112
112
|
cacheData[record.cacheKey] = { ...record, createdAt: timestamp };
|
|
113
113
|
await fs.writeJson(fallbackPath, cacheData, { spaces: 2 });
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* List cache records by type (and optional commit). Does not increment hit counts.
|
|
117
|
+
*/
|
|
118
|
+
export async function listContextCacheRecords(opts, cwd) {
|
|
119
|
+
const limit = opts.limit ?? 50;
|
|
120
|
+
if (isSQLiteAvailable()) {
|
|
121
|
+
try {
|
|
122
|
+
const db = await openDatabase();
|
|
123
|
+
if (db) {
|
|
124
|
+
const clauses = [];
|
|
125
|
+
const params = [];
|
|
126
|
+
if (opts.cacheType) {
|
|
127
|
+
clauses.push('cache_type = ?');
|
|
128
|
+
params.push(opts.cacheType);
|
|
129
|
+
}
|
|
130
|
+
if (opts.commitSha) {
|
|
131
|
+
clauses.push('commit_sha = ?');
|
|
132
|
+
params.push(opts.commitSha);
|
|
133
|
+
}
|
|
134
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
135
|
+
const rows = await db.all(`SELECT * FROM context_cache ${where} ORDER BY COALESCE(last_hit_at, created_at) DESC LIMIT ?`, ...params, limit);
|
|
136
|
+
await db.close();
|
|
137
|
+
return (rows || []).map((row) => ({
|
|
138
|
+
cacheKey: row.cache_key,
|
|
139
|
+
cacheType: row.cache_type,
|
|
140
|
+
repo: row.repo,
|
|
141
|
+
branch: row.branch,
|
|
142
|
+
commitSha: row.commit_sha,
|
|
143
|
+
dependencyFingerprint: row.dependency_fingerprint,
|
|
144
|
+
payloadJson: row.payload_json,
|
|
145
|
+
payloadTokens: row.payload_tokens,
|
|
146
|
+
hitCount: row.hit_count || 0,
|
|
147
|
+
createdAt: row.created_at,
|
|
148
|
+
lastHitAt: row.last_hit_at,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// fallback
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const fallbackPath = path.join(getFallbackDir(cwd), 'context-cache.json');
|
|
157
|
+
if (!(await fs.pathExists(fallbackPath)))
|
|
158
|
+
return [];
|
|
159
|
+
try {
|
|
160
|
+
const cacheData = await fs.readJson(fallbackPath);
|
|
161
|
+
return Object.values(cacheData)
|
|
162
|
+
.filter((entry) => {
|
|
163
|
+
if (opts.cacheType && entry.cacheType !== opts.cacheType)
|
|
164
|
+
return false;
|
|
165
|
+
if (opts.commitSha && entry.commitSha !== opts.commitSha)
|
|
166
|
+
return false;
|
|
167
|
+
return true;
|
|
168
|
+
})
|
|
169
|
+
.sort((a, b) => (b.lastHitAt || b.createdAt || 0) - (a.lastHitAt || a.createdAt || 0))
|
|
170
|
+
.slice(0, limit);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
115
176
|
/**
|
|
116
177
|
* Fetch cached context record by cache key. Increments hit count.
|
|
117
178
|
*/
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.
|
|
|
9
9
|
export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
|
|
10
10
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
|
|
11
11
|
export type { ProjectStats } from './local-memory.js';
|
|
12
|
-
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
12
|
+
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
13
13
|
export type { ContextEvent, ModelUsage, ContextCacheRecord, CheckpointMetric } from './context-telemetry.js';
|
package/dist/storage/index.js
CHANGED
|
@@ -7,4 +7,4 @@ export { insertScan, getRecentScans, getScoreTrendFromDB, getTopIssues } from '.
|
|
|
7
7
|
export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.js';
|
|
8
8
|
export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
|
|
9
9
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
|
|
10
|
-
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
10
|
+
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.1",
|
|
4
4
|
"description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"@xenova/transformers": "^2.17.2",
|
|
67
67
|
"sqlite3": "^5.1.7",
|
|
68
68
|
"openai": "^4.104.0",
|
|
69
|
-
"@rigour-labs/brain-darwin-
|
|
70
|
-
"@rigour-labs/brain-
|
|
71
|
-
"@rigour-labs/brain-
|
|
72
|
-
"@rigour-labs/brain-
|
|
73
|
-
"@rigour-labs/brain-
|
|
69
|
+
"@rigour-labs/brain-darwin-x64": "5.5.1",
|
|
70
|
+
"@rigour-labs/brain-linux-arm64": "5.5.1",
|
|
71
|
+
"@rigour-labs/brain-darwin-arm64": "5.5.1",
|
|
72
|
+
"@rigour-labs/brain-win-x64": "5.5.1",
|
|
73
|
+
"@rigour-labs/brain-linux-x64": "5.5.1"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/fs-extra": "^11.0.4",
|