@rigour-labs/core 5.2.9 → 5.3.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 +75 -0
- package/dist/context/cache-engine.js +155 -0
- package/dist/context/cache-engine.test.d.ts +1 -0
- package/dist/context/cache-engine.test.js +107 -0
- package/dist/context/context-session.d.ts +25 -0
- package/dist/context/context-session.js +78 -0
- package/dist/context/index-bridge.d.ts +24 -0
- package/dist/context/index-bridge.js +114 -0
- package/dist/hooks/dlp-confidence.d.ts +16 -0
- package/dist/hooks/dlp-confidence.js +302 -0
- package/dist/hooks/dlp-confidence.test.d.ts +1 -0
- package/dist/hooks/dlp-confidence.test.js +51 -0
- package/dist/hooks/dlp-feedback.d.ts +48 -0
- package/dist/hooks/dlp-feedback.js +174 -0
- package/dist/hooks/dlp-feedback.test.d.ts +1 -0
- package/dist/hooks/dlp-feedback.test.js +39 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/input-validator.d.ts +8 -0
- package/dist/hooks/input-validator.js +125 -82
- package/dist/hooks/input-validator.test.js +92 -34
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -1
- package/dist/services/context-telemetry-service.d.ts +121 -0
- package/dist/services/context-telemetry-service.js +375 -0
- package/dist/services/context-telemetry-service.test.d.ts +1 -0
- package/dist/services/context-telemetry-service.test.js +130 -0
- package/dist/settings.d.ts +11 -0
- package/dist/settings.js +18 -0
- package/dist/storage/context-telemetry.d.ts +83 -0
- package/dist/storage/context-telemetry.js +313 -0
- package/dist/storage/context-telemetry.test.d.ts +1 -0
- package/dist/storage/context-telemetry.test.js +80 -0
- package/dist/storage/db.js +140 -2
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/package.json +82 -82
- package/LICENSE +0 -21
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 4-Layer Context Caching Engine for Rigour
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* 1. Content-addressed static cache (AST, exports, imports, dependencies, patterns by content SHA)
|
|
6
|
+
* 2. Component context cache (Compact dossiers per component scope)
|
|
7
|
+
* 3. Semantic-query cache (Intent embeddings & edit/validation scopes)
|
|
8
|
+
* 4. Task checkpoint cache (Subagent handoff packets)
|
|
9
|
+
*/
|
|
10
|
+
export interface StaticCacheEntry {
|
|
11
|
+
astSummary?: any;
|
|
12
|
+
exports?: string[];
|
|
13
|
+
imports?: string[];
|
|
14
|
+
dependencies?: string[];
|
|
15
|
+
ownership?: string;
|
|
16
|
+
schemas?: any[];
|
|
17
|
+
endpoints?: string[];
|
|
18
|
+
eventRelationships?: any[];
|
|
19
|
+
styleFingerprint?: string;
|
|
20
|
+
rigourPatterns?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface ComponentDossier {
|
|
23
|
+
component: string;
|
|
24
|
+
responsibility: string;
|
|
25
|
+
canonicalFiles: string[];
|
|
26
|
+
contracts: string[];
|
|
27
|
+
directConsumers: string[];
|
|
28
|
+
validationCommands: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface SemanticQueryEntry {
|
|
31
|
+
query: string;
|
|
32
|
+
intentEmbedding?: number[];
|
|
33
|
+
resolvedOwner: string;
|
|
34
|
+
editScope: string[];
|
|
35
|
+
validationScope: string[];
|
|
36
|
+
evidence: string[];
|
|
37
|
+
commitSha: string;
|
|
38
|
+
confidence: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TaskCheckpointPacket {
|
|
41
|
+
taskId: string;
|
|
42
|
+
agentId: string;
|
|
43
|
+
phase: string;
|
|
44
|
+
component: string;
|
|
45
|
+
changedFiles: string[];
|
|
46
|
+
decisions: string[];
|
|
47
|
+
validation: string[];
|
|
48
|
+
remainingWork: string[];
|
|
49
|
+
risks: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate SHA-256 content hash.
|
|
53
|
+
*/
|
|
54
|
+
export declare function hashContent(content: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Layer 1: Content-Addressed Static Cache
|
|
57
|
+
*/
|
|
58
|
+
export declare function getStaticCache(repo: string, branch: string, filePath: string, fileContent: string, cwd?: string): Promise<StaticCacheEntry | null>;
|
|
59
|
+
export declare function setStaticCache(repo: string, branch: string, filePath: string, fileContent: string, entry: StaticCacheEntry, cwd?: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Layer 2: Component Context Cache
|
|
62
|
+
*/
|
|
63
|
+
export declare function getComponentCache(componentName: string, commitSha: string, profileVersion?: string, cwd?: string): Promise<ComponentDossier | null>;
|
|
64
|
+
export declare function setComponentCache(componentName: string, commitSha: string, dossier: ComponentDossier, dependencyFingerprint: string, profileVersion?: string, cwd?: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Layer 3: Semantic-Query Cache
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeQuery(query: string): string;
|
|
69
|
+
export declare function getSemanticQueryCache(query: string, commitSha: string, cwd?: string): Promise<SemanticQueryEntry | null>;
|
|
70
|
+
export declare function setSemanticQueryCache(query: string, commitSha: string, entry: SemanticQueryEntry, cwd?: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Layer 4: Task Checkpoint Cache
|
|
73
|
+
*/
|
|
74
|
+
export declare function setTaskCheckpointCache(packet: TaskCheckpointPacket, rawStateTokens: number, cwd?: string): Promise<void>;
|
|
75
|
+
export declare function getTaskCheckpointCache(taskId: string, agentId: string, phase: string, cwd?: string): Promise<TaskCheckpointPacket | null>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 4-Layer Context Caching Engine for Rigour
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* 1. Content-addressed static cache (AST, exports, imports, dependencies, patterns by content SHA)
|
|
6
|
+
* 2. Component context cache (Compact dossiers per component scope)
|
|
7
|
+
* 3. Semantic-query cache (Intent embeddings & edit/validation scopes)
|
|
8
|
+
* 4. Task checkpoint cache (Subagent handoff packets)
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric } from '../storage/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Generate SHA-256 content hash.
|
|
14
|
+
*/
|
|
15
|
+
export function hashContent(content) {
|
|
16
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Layer 1: Content-Addressed Static Cache
|
|
20
|
+
*/
|
|
21
|
+
export async function getStaticCache(repo, branch, filePath, fileContent, cwd) {
|
|
22
|
+
const contentSha = hashContent(fileContent);
|
|
23
|
+
const cacheKey = `static:${repo}:${branch}:${filePath}:${contentSha}:v1`;
|
|
24
|
+
const record = await getContextCacheRecord(cacheKey, cwd);
|
|
25
|
+
if (!record)
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(record.payloadJson);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function setStaticCache(repo, branch, filePath, fileContent, entry, cwd) {
|
|
35
|
+
const contentSha = hashContent(fileContent);
|
|
36
|
+
const cacheKey = `static:${repo}:${branch}:${filePath}:${contentSha}:v1`;
|
|
37
|
+
const payloadJson = JSON.stringify(entry);
|
|
38
|
+
const payloadTokens = Math.ceil(payloadJson.length / 4);
|
|
39
|
+
await setContextCacheRecord({
|
|
40
|
+
cacheKey,
|
|
41
|
+
cacheType: 'static',
|
|
42
|
+
repo,
|
|
43
|
+
branch,
|
|
44
|
+
commitSha: contentSha,
|
|
45
|
+
dependencyFingerprint: `sha-${contentSha}`,
|
|
46
|
+
payloadJson,
|
|
47
|
+
payloadTokens,
|
|
48
|
+
}, cwd);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Layer 2: Component Context Cache
|
|
52
|
+
*/
|
|
53
|
+
export async function getComponentCache(componentName, commitSha, profileVersion = '3', cwd) {
|
|
54
|
+
const cacheKey = `component:${componentName}:${commitSha}:v${profileVersion}`;
|
|
55
|
+
const record = await getContextCacheRecord(cacheKey, cwd);
|
|
56
|
+
if (!record)
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(record.payloadJson);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function setComponentCache(componentName, commitSha, dossier, dependencyFingerprint, profileVersion = '3', cwd) {
|
|
66
|
+
const cacheKey = `component:${componentName}:${commitSha}:v${profileVersion}`;
|
|
67
|
+
const payloadJson = JSON.stringify(dossier);
|
|
68
|
+
const payloadTokens = Math.ceil(payloadJson.length / 4);
|
|
69
|
+
await setContextCacheRecord({
|
|
70
|
+
cacheKey,
|
|
71
|
+
cacheType: 'component',
|
|
72
|
+
repo: dossier.component,
|
|
73
|
+
branch: 'main',
|
|
74
|
+
commitSha,
|
|
75
|
+
dependencyFingerprint,
|
|
76
|
+
payloadJson,
|
|
77
|
+
payloadTokens,
|
|
78
|
+
}, cwd);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Layer 3: Semantic-Query Cache
|
|
82
|
+
*/
|
|
83
|
+
export function normalizeQuery(query) {
|
|
84
|
+
return query.toLowerCase().trim().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ');
|
|
85
|
+
}
|
|
86
|
+
export async function getSemanticQueryCache(query, commitSha, cwd) {
|
|
87
|
+
const normalized = normalizeQuery(query);
|
|
88
|
+
const queryHash = hashContent(normalized);
|
|
89
|
+
const cacheKey = `semantic:${queryHash}:${commitSha}`;
|
|
90
|
+
const record = await getContextCacheRecord(cacheKey, cwd);
|
|
91
|
+
if (!record)
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(record.payloadJson);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function setSemanticQueryCache(query, commitSha, entry, cwd) {
|
|
101
|
+
const normalized = normalizeQuery(query);
|
|
102
|
+
const queryHash = hashContent(normalized);
|
|
103
|
+
const cacheKey = `semantic:${queryHash}:${commitSha}`;
|
|
104
|
+
const payloadJson = JSON.stringify(entry);
|
|
105
|
+
const payloadTokens = Math.ceil(payloadJson.length / 4);
|
|
106
|
+
await setContextCacheRecord({
|
|
107
|
+
cacheKey,
|
|
108
|
+
cacheType: 'semantic',
|
|
109
|
+
repo: 'workspace',
|
|
110
|
+
branch: 'main',
|
|
111
|
+
commitSha,
|
|
112
|
+
dependencyFingerprint: `query-${queryHash}`,
|
|
113
|
+
payloadJson,
|
|
114
|
+
payloadTokens,
|
|
115
|
+
}, cwd);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Layer 4: Task Checkpoint Cache
|
|
119
|
+
*/
|
|
120
|
+
export async function setTaskCheckpointCache(packet, rawStateTokens, cwd) {
|
|
121
|
+
const cacheKey = `checkpoint:${packet.taskId}:${packet.agentId}:${packet.phase}`;
|
|
122
|
+
const payloadJson = JSON.stringify(packet);
|
|
123
|
+
const checkpointTokens = Math.ceil(payloadJson.length / 4);
|
|
124
|
+
const replayTokensAvoided = Math.max(0, rawStateTokens - checkpointTokens);
|
|
125
|
+
await setContextCacheRecord({
|
|
126
|
+
cacheKey,
|
|
127
|
+
cacheType: 'checkpoint',
|
|
128
|
+
repo: packet.taskId,
|
|
129
|
+
branch: packet.phase,
|
|
130
|
+
commitSha: packet.agentId,
|
|
131
|
+
dependencyFingerprint: `cp-${packet.taskId}`,
|
|
132
|
+
payloadJson,
|
|
133
|
+
payloadTokens: checkpointTokens,
|
|
134
|
+
}, cwd);
|
|
135
|
+
await recordCheckpointMetric({
|
|
136
|
+
checkpointId: cacheKey,
|
|
137
|
+
taskId: packet.taskId,
|
|
138
|
+
agentId: packet.agentId,
|
|
139
|
+
rawStateTokens,
|
|
140
|
+
checkpointTokens,
|
|
141
|
+
replayTokensAvoided,
|
|
142
|
+
}, cwd);
|
|
143
|
+
}
|
|
144
|
+
export async function getTaskCheckpointCache(taskId, agentId, phase, cwd) {
|
|
145
|
+
const cacheKey = `checkpoint:${taskId}:${agentId}:${phase}`;
|
|
146
|
+
const record = await getContextCacheRecord(cacheKey, cwd);
|
|
147
|
+
if (!record)
|
|
148
|
+
return null;
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(record.payloadJson);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
vi.mock('./db.js', () => ({
|
|
6
|
+
isSQLiteAvailable: () => false,
|
|
7
|
+
openDatabase: async () => null,
|
|
8
|
+
DB_PATH: '/tmp/rigour-test.db',
|
|
9
|
+
RIGOUR_DIR: '/tmp/.rigour',
|
|
10
|
+
}));
|
|
11
|
+
import { getStaticCache, setStaticCache, getComponentCache, setComponentCache, getSemanticQueryCache, setSemanticQueryCache, getTaskCheckpointCache, setTaskCheckpointCache, hashContent, normalizeQuery } from './cache-engine.js';
|
|
12
|
+
describe('4-Layer Context Cache Engine', () => {
|
|
13
|
+
const testCwd = path.join(os.tmpdir(), `rigour-cache-test-${Date.now()}`);
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await fs.ensureDir(testCwd);
|
|
16
|
+
});
|
|
17
|
+
it('should calculate consistent SHA-256 hashes', () => {
|
|
18
|
+
const hash1 = hashContent('const x = 1;');
|
|
19
|
+
const hash2 = hashContent('const x = 1;');
|
|
20
|
+
const hash3 = hashContent('const x = 2;');
|
|
21
|
+
expect(hash1).toBe(hash2);
|
|
22
|
+
expect(hash1).not.toBe(hash3);
|
|
23
|
+
expect(hash1.length).toBe(16);
|
|
24
|
+
});
|
|
25
|
+
it('should normalize intent queries correctly', () => {
|
|
26
|
+
expect(normalizeQuery(' Add priority to task!! ')).toBe('add priority to task');
|
|
27
|
+
expect(normalizeQuery('Task SHOULD Support Priority.')).toBe('task should support priority');
|
|
28
|
+
});
|
|
29
|
+
it('Layer 1: should store and retrieve content-addressed static cache', async () => {
|
|
30
|
+
const fileContent = 'export function processTask() { return true; }';
|
|
31
|
+
const entry = {
|
|
32
|
+
exports: ['processTask'],
|
|
33
|
+
imports: [],
|
|
34
|
+
ownership: 'platform-team',
|
|
35
|
+
endpoints: ['/api/task']
|
|
36
|
+
};
|
|
37
|
+
await setStaticCache('rigour-repo', 'main', 'services/task.ts', fileContent, entry, testCwd);
|
|
38
|
+
const cached = await getStaticCache('rigour-repo', 'main', 'services/task.ts', fileContent, testCwd);
|
|
39
|
+
expect(cached).not.toBeNull();
|
|
40
|
+
expect(cached?.exports).toEqual(['processTask']);
|
|
41
|
+
expect(cached?.ownership).toBe('platform-team');
|
|
42
|
+
});
|
|
43
|
+
it('Layer 2: should store and retrieve component context dossier', async () => {
|
|
44
|
+
const dossier = {
|
|
45
|
+
component: 'services/task',
|
|
46
|
+
responsibility: 'Task lifecycle and persistence',
|
|
47
|
+
canonicalFiles: ['services/task.ts'],
|
|
48
|
+
contracts: ['TaskContract'],
|
|
49
|
+
directConsumers: ['controllers/task'],
|
|
50
|
+
validationCommands: ['npm test']
|
|
51
|
+
};
|
|
52
|
+
await setComponentCache('services/task', 'abc123commit', dossier, 'dep-fingerprint-v1', '3', testCwd);
|
|
53
|
+
const retrieved = await getComponentCache('services/task', 'abc123commit', '3', testCwd);
|
|
54
|
+
expect(retrieved).not.toBeNull();
|
|
55
|
+
expect(retrieved?.responsibility).toBe('Task lifecycle and persistence');
|
|
56
|
+
expect(retrieved?.canonicalFiles).toContain('services/task.ts');
|
|
57
|
+
});
|
|
58
|
+
it('Layer 3: should store and retrieve semantic query scope', async () => {
|
|
59
|
+
const entry = {
|
|
60
|
+
query: 'task priority persistence',
|
|
61
|
+
resolvedOwner: 'task-team',
|
|
62
|
+
editScope: ['services/task.ts'],
|
|
63
|
+
validationScope: ['npm test'],
|
|
64
|
+
evidence: ['AST match'],
|
|
65
|
+
commitSha: 'commit-99',
|
|
66
|
+
confidence: 0.95
|
|
67
|
+
};
|
|
68
|
+
await setSemanticQueryCache('add priority to task', 'commit-99', entry, testCwd);
|
|
69
|
+
const retrieved = await getSemanticQueryCache('add priority to task', 'commit-99', testCwd);
|
|
70
|
+
expect(retrieved).not.toBeNull();
|
|
71
|
+
expect(retrieved?.resolvedOwner).toBe('task-team');
|
|
72
|
+
expect(retrieved?.editScope).toContain('services/task.ts');
|
|
73
|
+
});
|
|
74
|
+
it('Layer 4: should store and retrieve subagent task checkpoint packet', async () => {
|
|
75
|
+
const packet = {
|
|
76
|
+
taskId: 'CTP-142',
|
|
77
|
+
agentId: 'CTP-142-task',
|
|
78
|
+
phase: 'implementation',
|
|
79
|
+
component: 'services/task',
|
|
80
|
+
changedFiles: ['services/task.ts'],
|
|
81
|
+
decisions: ['Added priority field'],
|
|
82
|
+
validation: ['npm test passed'],
|
|
83
|
+
remainingWork: ['Add integration test'],
|
|
84
|
+
risks: ['Migration needed']
|
|
85
|
+
};
|
|
86
|
+
await setTaskCheckpointCache(packet, 126000, testCwd);
|
|
87
|
+
const retrieved = await getTaskCheckpointCache('CTP-142', 'CTP-142-task', 'implementation', testCwd);
|
|
88
|
+
expect(retrieved).not.toBeNull();
|
|
89
|
+
expect(retrieved?.taskId).toBe('CTP-142');
|
|
90
|
+
expect(retrieved?.decisions).toContain('Added priority field');
|
|
91
|
+
});
|
|
92
|
+
it('Layer 1: invalidates static cache when file content changes', async () => {
|
|
93
|
+
const originalContent = 'export function processTask() { return true; }';
|
|
94
|
+
const entry = {
|
|
95
|
+
exports: ['processTask'],
|
|
96
|
+
imports: [],
|
|
97
|
+
ownership: 'platform-team',
|
|
98
|
+
endpoints: ['/api/task'],
|
|
99
|
+
};
|
|
100
|
+
await setStaticCache('rigour-repo', 'main', 'services/task.ts', originalContent, entry, testCwd);
|
|
101
|
+
const cached = await getStaticCache('rigour-repo', 'main', 'services/task.ts', originalContent, testCwd);
|
|
102
|
+
expect(cached?.exports).toEqual(['processTask']);
|
|
103
|
+
const changedContent = 'export function processTask() { return false; }';
|
|
104
|
+
const stale = await getStaticCache('rigour-repo', 'main', 'services/task.ts', changedContent, testCwd);
|
|
105
|
+
expect(stale).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project context session tracking.
|
|
3
|
+
* Persists file reads and token estimates to .rigour/context-session.json.
|
|
4
|
+
*/
|
|
5
|
+
export interface FileReadRecord {
|
|
6
|
+
path: string;
|
|
7
|
+
contentHash: string;
|
|
8
|
+
tokens: number;
|
|
9
|
+
readAt: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ContextSession {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
lastUpdatedAt: number;
|
|
15
|
+
fileReads: Record<string, FileReadRecord>;
|
|
16
|
+
totalTokens: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load existing session or create a new one for the project.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getOrCreateSession(cwd: string): Promise<ContextSession>;
|
|
22
|
+
/**
|
|
23
|
+
* Record a file read in the current session, deduplicating by content hash.
|
|
24
|
+
*/
|
|
25
|
+
export declare function recordFileRead(cwd: string, filePath: string, content: string): Promise<FileReadRecord>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project context session tracking.
|
|
3
|
+
* Persists file reads and token estimates to .rigour/context-session.json.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { hashContent } from './cache-engine.js';
|
|
9
|
+
function estimateTokens(text) {
|
|
10
|
+
if (!text)
|
|
11
|
+
return 0;
|
|
12
|
+
return Math.ceil(text.length / 4);
|
|
13
|
+
}
|
|
14
|
+
function getSessionPath(cwd) {
|
|
15
|
+
return path.join(cwd, '.rigour', 'context-session.json');
|
|
16
|
+
}
|
|
17
|
+
async function loadSession(cwd) {
|
|
18
|
+
const sessionPath = getSessionPath(cwd);
|
|
19
|
+
if (!await fs.pathExists(sessionPath))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
return await fs.readJson(sessionPath);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function saveSession(cwd, session) {
|
|
29
|
+
const sessionPath = getSessionPath(cwd);
|
|
30
|
+
await fs.ensureDir(path.dirname(sessionPath));
|
|
31
|
+
await fs.writeJson(sessionPath, session, { spaces: 2 });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load existing session or create a new one for the project.
|
|
35
|
+
*/
|
|
36
|
+
export async function getOrCreateSession(cwd) {
|
|
37
|
+
const resolved = path.resolve(cwd);
|
|
38
|
+
const existing = await loadSession(resolved);
|
|
39
|
+
if (existing)
|
|
40
|
+
return existing;
|
|
41
|
+
const session = {
|
|
42
|
+
sessionId: `sess-${randomUUID()}`,
|
|
43
|
+
createdAt: Date.now(),
|
|
44
|
+
lastUpdatedAt: Date.now(),
|
|
45
|
+
fileReads: {},
|
|
46
|
+
totalTokens: 0,
|
|
47
|
+
};
|
|
48
|
+
await saveSession(resolved, session);
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Record a file read in the current session, deduplicating by content hash.
|
|
53
|
+
*/
|
|
54
|
+
export async function recordFileRead(cwd, filePath, content) {
|
|
55
|
+
const resolved = path.resolve(cwd);
|
|
56
|
+
const session = await getOrCreateSession(resolved);
|
|
57
|
+
const contentHash = hashContent(content);
|
|
58
|
+
const tokens = estimateTokens(content);
|
|
59
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
60
|
+
const existing = session.fileReads[normalizedPath];
|
|
61
|
+
if (existing?.contentHash === contentHash) {
|
|
62
|
+
return existing;
|
|
63
|
+
}
|
|
64
|
+
const record = {
|
|
65
|
+
path: normalizedPath,
|
|
66
|
+
contentHash,
|
|
67
|
+
tokens,
|
|
68
|
+
readAt: Date.now(),
|
|
69
|
+
};
|
|
70
|
+
if (existing) {
|
|
71
|
+
session.totalTokens -= existing.tokens;
|
|
72
|
+
}
|
|
73
|
+
session.fileReads[normalizedPath] = record;
|
|
74
|
+
session.totalTokens += tokens;
|
|
75
|
+
session.lastUpdatedAt = Date.now();
|
|
76
|
+
await saveSession(resolved, session);
|
|
77
|
+
return record;
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge between Pattern Index and 4-Layer Context Cache.
|
|
3
|
+
* Syncs indexed patterns into static cache entries and component dossiers.
|
|
4
|
+
*/
|
|
5
|
+
import type { PatternIndex } from '../pattern-index/types.js';
|
|
6
|
+
export interface IndexHealthReport {
|
|
7
|
+
indexPath: string;
|
|
8
|
+
exists: boolean;
|
|
9
|
+
lastUpdated?: string;
|
|
10
|
+
ageMs?: number;
|
|
11
|
+
isStale: boolean;
|
|
12
|
+
staleThresholdMs: number;
|
|
13
|
+
totalPatterns?: number;
|
|
14
|
+
totalFiles?: number;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sync pattern index entries into static cache and component dossier layers.
|
|
19
|
+
*/
|
|
20
|
+
export declare function syncIndexToContextCache(cwd: string, index: PatternIndex): Promise<number>;
|
|
21
|
+
/**
|
|
22
|
+
* Check patterns.json freshness relative to the project directory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getIndexHealth(cwd: string, staleThresholdMs?: number): Promise<IndexHealthReport>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge between Pattern Index and 4-Layer Context Cache.
|
|
3
|
+
* Syncs indexed patterns into static cache entries and component dossiers.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { setStaticCache, setComponentCache, hashContent } from './cache-engine.js';
|
|
8
|
+
function getIndexPath(cwd) {
|
|
9
|
+
return path.join(cwd, '.rigour', 'patterns.json');
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
function groupByComponent(patterns) {
|
|
13
|
+
const groups = new Map();
|
|
14
|
+
for (const p of patterns) {
|
|
15
|
+
const key = p.type === 'component' ? p.name : p.category || 'uncategorized';
|
|
16
|
+
const list = groups.get(key) ?? [];
|
|
17
|
+
list.push(p);
|
|
18
|
+
groups.set(key, list);
|
|
19
|
+
}
|
|
20
|
+
return groups;
|
|
21
|
+
}
|
|
22
|
+
function buildDossier(component, patterns) {
|
|
23
|
+
const canonicalFiles = [...new Set(patterns.map(p => p.file))];
|
|
24
|
+
const contracts = patterns
|
|
25
|
+
.filter(p => p.signature)
|
|
26
|
+
.map(p => p.signature)
|
|
27
|
+
.slice(0, 20);
|
|
28
|
+
const descriptions = patterns.map(p => p.description).filter(Boolean);
|
|
29
|
+
const responsibility = descriptions[0] || `Indexed ${patterns.length} patterns for ${component}`;
|
|
30
|
+
return {
|
|
31
|
+
component,
|
|
32
|
+
responsibility,
|
|
33
|
+
canonicalFiles,
|
|
34
|
+
contracts,
|
|
35
|
+
directConsumers: [],
|
|
36
|
+
validationCommands: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Sync pattern index entries into static cache and component dossier layers.
|
|
41
|
+
*/
|
|
42
|
+
export async function syncIndexToContextCache(cwd, index) {
|
|
43
|
+
const resolved = path.resolve(cwd);
|
|
44
|
+
const repo = path.basename(index.rootDir || resolved);
|
|
45
|
+
const branch = 'main';
|
|
46
|
+
const commitSha = hashContent(index.lastUpdated);
|
|
47
|
+
let synced = 0;
|
|
48
|
+
for (const file of index.files) {
|
|
49
|
+
const filePatterns = index.patterns.filter(p => p.file === file.path);
|
|
50
|
+
if (filePatterns.length === 0)
|
|
51
|
+
continue;
|
|
52
|
+
const payload = {
|
|
53
|
+
rigourPatterns: filePatterns.map(p => `${p.type}:${p.name}`),
|
|
54
|
+
exports: filePatterns.filter(p => p.exported).map(p => p.name),
|
|
55
|
+
};
|
|
56
|
+
const fileContent = JSON.stringify(payload);
|
|
57
|
+
await setStaticCache(repo, branch, file.path, fileContent, payload, resolved);
|
|
58
|
+
synced++;
|
|
59
|
+
}
|
|
60
|
+
const componentGroups = groupByComponent(index.patterns);
|
|
61
|
+
for (const [component, patterns] of componentGroups) {
|
|
62
|
+
const dossier = buildDossier(component, patterns);
|
|
63
|
+
const fingerprint = hashContent(JSON.stringify(patterns.map(p => p.id)));
|
|
64
|
+
await setComponentCache(component, commitSha, dossier, fingerprint, '3', resolved);
|
|
65
|
+
synced++;
|
|
66
|
+
}
|
|
67
|
+
return synced;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check patterns.json freshness relative to the project directory.
|
|
71
|
+
*/
|
|
72
|
+
export async function getIndexHealth(cwd, staleThresholdMs = DEFAULT_STALE_MS) {
|
|
73
|
+
const resolved = path.resolve(cwd);
|
|
74
|
+
const indexPath = getIndexPath(resolved);
|
|
75
|
+
if (!await fs.pathExists(indexPath)) {
|
|
76
|
+
return {
|
|
77
|
+
indexPath,
|
|
78
|
+
exists: false,
|
|
79
|
+
isStale: true,
|
|
80
|
+
staleThresholdMs,
|
|
81
|
+
message: 'patterns.json not found — run pattern index build',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const stat = await fs.stat(indexPath);
|
|
85
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
86
|
+
let index = null;
|
|
87
|
+
try {
|
|
88
|
+
index = await fs.readJson(indexPath);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {
|
|
92
|
+
indexPath,
|
|
93
|
+
exists: true,
|
|
94
|
+
ageMs,
|
|
95
|
+
isStale: true,
|
|
96
|
+
staleThresholdMs,
|
|
97
|
+
message: 'patterns.json is malformed',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const isStale = ageMs > staleThresholdMs;
|
|
101
|
+
return {
|
|
102
|
+
indexPath,
|
|
103
|
+
exists: true,
|
|
104
|
+
lastUpdated: index.lastUpdated,
|
|
105
|
+
ageMs,
|
|
106
|
+
isStale,
|
|
107
|
+
staleThresholdMs,
|
|
108
|
+
totalPatterns: index.stats?.totalPatterns,
|
|
109
|
+
totalFiles: index.stats?.totalFiles,
|
|
110
|
+
message: isStale
|
|
111
|
+
? `Index is stale (${Math.round(ageMs / 3600000)}h old)`
|
|
112
|
+
: 'Index is fresh',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DLPDecision = 'block' | 'warn' | 'allow';
|
|
2
|
+
interface DLPDetectionInput {
|
|
3
|
+
type: string;
|
|
4
|
+
match: string;
|
|
5
|
+
position?: {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
interface DLPClassification {
|
|
11
|
+
confidence: number;
|
|
12
|
+
decision: DLPDecision;
|
|
13
|
+
reason_codes: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function classifyDLPDetection(detection: DLPDetectionInput, input: string): DLPClassification;
|
|
16
|
+
export {};
|