@rigour-labs/core 6.5.3 → 6.6.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.
- package/README.md +1 -1
- package/dist/context/automatic-index-cache.test.d.ts +1 -0
- package/dist/context/automatic-index-cache.test.js +30 -0
- package/dist/context/automatic-index.js +0 -4
- package/dist/context/cache-engine.d.ts +7 -38
- package/dist/context/cache-engine.js +7 -70
- package/dist/context/cache-engine.test.js +2 -46
- package/dist/context/index-bridge.d.ts +1 -7
- package/dist/context/index-bridge.js +1 -79
- package/dist/deep/risk.d.ts +6 -0
- package/dist/deep/risk.js +7 -1
- package/dist/deep/risk.test.js +12 -1
- package/dist/firewall/arbitration-token.d.ts +12 -3
- package/dist/firewall/arbitration-token.js +30 -23
- package/dist/firewall/attestation.d.ts +7 -2
- package/dist/firewall/attestation.js +49 -28
- package/dist/firewall/execution-receipt.d.ts +5 -0
- package/dist/firewall/execution-receipt.js +10 -1
- package/dist/firewall/trust-boundaries.test.d.ts +1 -0
- package/dist/firewall/trust-boundaries.test.js +64 -0
- package/dist/firewall/trusted-control.test.js +10 -0
- package/dist/gates/deep-analysis.d.ts +6 -0
- package/dist/gates/deep-analysis.js +8 -1
- package/dist/gates/trusted-reviews.test.d.ts +1 -0
- package/dist/gates/trusted-reviews.test.js +16 -0
- package/dist/hooks/checker.js +40 -0
- package/dist/hooks/checker.test.js +18 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/session-state.d.ts +8 -0
- package/dist/hooks/session-state.js +61 -0
- package/dist/hooks/stop-review.d.ts +6 -4
- package/dist/hooks/stop-review.js +5 -3
- package/dist/hooks/templates.js +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/review/agent-fixes.d.ts +25 -2
- package/dist/review/agent-fixes.js +27 -6
- package/dist/review/check-outcomes.d.ts +2 -1
- package/dist/review/check-outcomes.js +11 -2
- package/dist/review/effectiveness.d.ts +13 -0
- package/dist/review/git-diff.d.ts +3 -0
- package/dist/review/git-diff.js +5 -0
- package/dist/review/learning-events.d.ts +14 -0
- package/dist/review/learning-events.js +21 -0
- package/dist/review/migration-order.js +1 -1
- package/dist/review/quiet.d.ts +3 -2
- package/dist/review/quiet.js +10 -6
- package/dist/review/review.d.ts +4 -0
- package/dist/review/review.js +14 -2
- package/dist/review/review.test.js +19 -0
- package/dist/review/stories.d.ts +25 -0
- package/dist/review/stories.js +75 -0
- package/dist/review/stories.test.d.ts +1 -0
- package/dist/review/stories.test.js +58 -0
- package/dist/review/trusted-state.d.ts +6 -0
- package/dist/review/trusted-state.js +35 -0
- package/dist/storage/cache-cleanup.d.ts +19 -0
- package/dist/storage/cache-cleanup.js +90 -0
- package/dist/storage/cache-cleanup.test.d.ts +1 -0
- package/dist/storage/cache-cleanup.test.js +54 -0
- package/dist/storage/db.js +3 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/local-encryption.js +2 -2
- package/dist/storage/team-store.js +2 -2
- package/dist/types/index.d.ts +5 -0
- package/dist/utils/user-state.d.ts +7 -0
- package/dist/utils/user-state.js +21 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Rigour's deep analysis is not a wrapper around a generic LLM. The model operates
|
|
|
46
46
|
2. **Interpret** — the model receives structured facts (not raw source), focuses on SOLID, design patterns, language idioms, architecture. Constrained input prevents hallucination.
|
|
47
47
|
3. **Verify** — every LLM finding is cross-referenced against all five signal streams. Wrong line numbers, phantom patterns, non-existent functions → discarded. Only verified findings with confidence scores reach the report.
|
|
48
48
|
|
|
49
|
-
The lite and deep tiers
|
|
49
|
+
Deep review is built for frontier models: your own key, or the coding agent's own model. The small local models (lite and deep tiers fine-tuned via the [DriftBench RLAIF pipeline](https://github.com/rigour-labs/driftbench); max is the stock Qwen2.5-Coder-7B) remain available offline but are no longer developed.
|
|
50
50
|
|
|
51
51
|
### Temporal Drift Engine (v5.1)
|
|
52
52
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
const writes = vi.hoisted(() => ({ count: 0 }));
|
|
6
|
+
vi.mock('../storage/context-telemetry.js', async (importOriginal) => ({
|
|
7
|
+
...(await importOriginal()),
|
|
8
|
+
setContextCacheRecord: async () => { writes.count++; },
|
|
9
|
+
}));
|
|
10
|
+
const { ensureAutomaticIndex, updateAutomaticIndexForFiles } = await import('./automatic-index.js');
|
|
11
|
+
let cwd;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
writes.count = 0;
|
|
14
|
+
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'auto-index-'));
|
|
15
|
+
fs.mkdirSync(path.join(cwd, 'src'));
|
|
16
|
+
fs.writeFileSync(path.join(cwd, 'src/a.ts'), 'export function alpha() { return 1; }\n');
|
|
17
|
+
fs.writeFileSync(path.join(cwd, 'src/b.ts'), 'export function beta() { return 2; }\n');
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
|
|
20
|
+
describe('automatic indexing', () => {
|
|
21
|
+
it('writes nothing to the context cache, however many edits it sees', async () => {
|
|
22
|
+
await ensureAutomaticIndex(cwd, { backgroundSemantic: false });
|
|
23
|
+
for (let edit = 0; edit < 5; edit++) {
|
|
24
|
+
fs.writeFileSync(path.join(cwd, 'src/a.ts'), `export function alpha() { return ${edit}; }\n`);
|
|
25
|
+
await updateAutomaticIndexForFiles(cwd, ['src/a.ts']);
|
|
26
|
+
}
|
|
27
|
+
expect(writes.count).toBe(0);
|
|
28
|
+
expect(fs.existsSync(path.join(cwd, '.rigour', 'patterns.json'))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -3,7 +3,6 @@ import path from 'path';
|
|
|
3
3
|
import { PatternIndexer, getDefaultIndexPath, loadPatternIndex, savePatternIndex } from '../pattern-index/indexer.js';
|
|
4
4
|
import { EMBEDDING_TEXT_VERSION } from '../pattern-index/embeddings.js';
|
|
5
5
|
import { affectedDependents, buildDependencyGraph, updateDependencyGraph } from './dependency-graph.js';
|
|
6
|
-
import { syncIndexToContextCache } from './index-bridge.js';
|
|
7
6
|
const enrichmentJobs = new Map();
|
|
8
7
|
function projectRelativeFiles(cwd, files) {
|
|
9
8
|
const root = path.resolve(cwd);
|
|
@@ -78,7 +77,6 @@ async function enrichSemantic(cwd, index) {
|
|
|
78
77
|
const merged = mergeSemanticEmbeddings(latest, enriched);
|
|
79
78
|
const embedded = merged.patterns.filter((pattern) => (pattern.embedding?.length ?? 0) > 0).length;
|
|
80
79
|
await savePatternIndex(merged, indexPath);
|
|
81
|
-
await syncIndexToContextCache(cwd, merged);
|
|
82
80
|
await writeStatus(cwd, {
|
|
83
81
|
structural: 'ready',
|
|
84
82
|
graph: 'ready',
|
|
@@ -114,7 +112,6 @@ export async function ensureAutomaticIndex(cwd, options = {}) {
|
|
|
114
112
|
const index = existing ?? await indexer.buildIndex();
|
|
115
113
|
if (!existing)
|
|
116
114
|
await savePatternIndex(index, indexPath);
|
|
117
|
-
await syncIndexToContextCache(cwd, index);
|
|
118
115
|
if (!(await fs.pathExists(path.join(cwd, '.rigour', 'dependency-graph.json')))) {
|
|
119
116
|
await buildDependencyGraph(cwd, index);
|
|
120
117
|
}
|
|
@@ -144,7 +141,6 @@ export async function updateAutomaticIndexForFiles(cwd, changedFiles) {
|
|
|
144
141
|
const indexer = new PatternIndexer(cwd, { useEmbeddings: false });
|
|
145
142
|
const index = await indexer.updateFiles(existing, affectedFiles);
|
|
146
143
|
await savePatternIndex(index, getDefaultIndexPath(cwd));
|
|
147
|
-
await syncIndexToContextCache(cwd, index);
|
|
148
144
|
if (graph)
|
|
149
145
|
await updateDependencyGraph(cwd, index, graph, affectedFiles);
|
|
150
146
|
else
|
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Context caching for Rigour: the two layers something reads.
|
|
3
|
+
* - Semantic-query cache: the edit and validation scope resolved for a task query.
|
|
4
|
+
* - Task checkpoint cache: compact handoff packets between agents.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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)
|
|
6
|
+
* The static (per-file) and component layers were removed: nothing read them, and the index sync
|
|
7
|
+
* that wrote them added a fresh row per component on every edit (millions of rows in real use).
|
|
9
8
|
*/
|
|
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
9
|
export interface SemanticQueryEntry {
|
|
31
10
|
query: string;
|
|
32
11
|
intentEmbedding?: number[];
|
|
@@ -78,17 +57,7 @@ export interface TaskCheckpointPacket {
|
|
|
78
57
|
*/
|
|
79
58
|
export declare function hashContent(content: string): string;
|
|
80
59
|
/**
|
|
81
|
-
*
|
|
82
|
-
*/
|
|
83
|
-
export declare function getStaticCache(repo: string, branch: string, filePath: string, fileContent: string, cwd?: string): Promise<StaticCacheEntry | null>;
|
|
84
|
-
export declare function setStaticCache(repo: string, branch: string, filePath: string, fileContent: string, entry: StaticCacheEntry, cwd?: string): Promise<void>;
|
|
85
|
-
/**
|
|
86
|
-
* Layer 2: Component Context Cache
|
|
87
|
-
*/
|
|
88
|
-
export declare function getComponentCache(componentName: string, commitSha: string, profileVersion?: string, cwd?: string): Promise<ComponentDossier | null>;
|
|
89
|
-
export declare function setComponentCache(componentName: string, commitSha: string, dossier: ComponentDossier, dependencyFingerprint: string, profileVersion?: string, cwd?: string): Promise<void>;
|
|
90
|
-
/**
|
|
91
|
-
* Layer 3: Semantic-Query Cache
|
|
60
|
+
* Semantic-Query Cache
|
|
92
61
|
*/
|
|
93
62
|
export declare function normalizeQuery(query: string): string;
|
|
94
63
|
export declare function getSemanticQueryCache(query: string, commitSha: string, cwd?: string): Promise<SemanticQueryEntry | null>;
|
|
@@ -119,7 +88,7 @@ export declare function filterExistingEditScope(editScope: string[], cwd: string
|
|
|
119
88
|
missing: string[];
|
|
120
89
|
}>;
|
|
121
90
|
/**
|
|
122
|
-
*
|
|
91
|
+
* Task Checkpoint Cache
|
|
123
92
|
*/
|
|
124
93
|
export declare function setTaskCheckpointCache(packet: TaskCheckpointPacket, rawStateTokens: number, cwd?: string): Promise<void>;
|
|
125
94
|
export declare function getTaskCheckpointCache(taskId: string, agentId: string, phase: string, cwd?: string): Promise<TaskCheckpointPacket | null>;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Context caching for Rigour: the two layers something reads.
|
|
3
|
+
* - Semantic-query cache: the edit and validation scope resolved for a task query.
|
|
4
|
+
* - Task checkpoint cache: compact handoff packets between agents.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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)
|
|
6
|
+
* The static (per-file) and component layers were removed: nothing read them, and the index sync
|
|
7
|
+
* that wrote them added a fresh row per component on every edit (millions of rows in real use).
|
|
9
8
|
*/
|
|
10
9
|
import { createHash } from 'crypto';
|
|
11
10
|
import fs from 'fs-extra';
|
|
@@ -18,69 +17,7 @@ export function hashContent(content) {
|
|
|
18
17
|
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
19
18
|
}
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
|
-
export async function getStaticCache(repo, branch, filePath, fileContent, cwd) {
|
|
24
|
-
const contentSha = hashContent(fileContent);
|
|
25
|
-
const cacheKey = `static:${repo}:${branch}:${filePath}:${contentSha}:v1`;
|
|
26
|
-
const record = await getContextCacheRecord(cacheKey, cwd);
|
|
27
|
-
if (!record)
|
|
28
|
-
return null;
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(record.payloadJson);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export async function setStaticCache(repo, branch, filePath, fileContent, entry, cwd) {
|
|
37
|
-
const contentSha = hashContent(fileContent);
|
|
38
|
-
const cacheKey = `static:${repo}:${branch}:${filePath}:${contentSha}:v1`;
|
|
39
|
-
const payloadJson = JSON.stringify(entry);
|
|
40
|
-
const payloadTokens = Math.ceil(payloadJson.length / 4);
|
|
41
|
-
await setContextCacheRecord({
|
|
42
|
-
cacheKey,
|
|
43
|
-
cacheType: 'static',
|
|
44
|
-
repo,
|
|
45
|
-
branch,
|
|
46
|
-
commitSha: contentSha,
|
|
47
|
-
dependencyFingerprint: `sha-${contentSha}`,
|
|
48
|
-
payloadJson,
|
|
49
|
-
payloadTokens,
|
|
50
|
-
}, cwd);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Layer 2: Component Context Cache
|
|
54
|
-
*/
|
|
55
|
-
export async function getComponentCache(componentName, commitSha, profileVersion = '3', cwd) {
|
|
56
|
-
const cacheKey = `component:${componentName}:${commitSha}:v${profileVersion}`;
|
|
57
|
-
const record = await getContextCacheRecord(cacheKey, cwd);
|
|
58
|
-
if (!record)
|
|
59
|
-
return null;
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(record.payloadJson);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export async function setComponentCache(componentName, commitSha, dossier, dependencyFingerprint, profileVersion = '3', cwd) {
|
|
68
|
-
const cacheKey = `component:${componentName}:${commitSha}:v${profileVersion}`;
|
|
69
|
-
const payloadJson = JSON.stringify(dossier);
|
|
70
|
-
const payloadTokens = Math.ceil(payloadJson.length / 4);
|
|
71
|
-
await setContextCacheRecord({
|
|
72
|
-
cacheKey,
|
|
73
|
-
cacheType: 'component',
|
|
74
|
-
repo: dossier.component,
|
|
75
|
-
branch: 'main',
|
|
76
|
-
commitSha,
|
|
77
|
-
dependencyFingerprint,
|
|
78
|
-
payloadJson,
|
|
79
|
-
payloadTokens,
|
|
80
|
-
}, cwd);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Layer 3: Semantic-Query Cache
|
|
20
|
+
* Semantic-Query Cache
|
|
84
21
|
*/
|
|
85
22
|
export function normalizeQuery(query) {
|
|
86
23
|
return query.toLowerCase().trim().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ');
|
|
@@ -193,7 +130,7 @@ export async function filterExistingEditScope(editScope, cwd) {
|
|
|
193
130
|
return { valid, missing };
|
|
194
131
|
}
|
|
195
132
|
/**
|
|
196
|
-
*
|
|
133
|
+
* Task Checkpoint Cache
|
|
197
134
|
*/
|
|
198
135
|
export async function setTaskCheckpointCache(packet, rawStateTokens, cwd) {
|
|
199
136
|
const cacheKey = `checkpoint:${packet.taskId}:${packet.agentId}:${packet.phase}`;
|
|
@@ -8,8 +8,8 @@ vi.mock('./db.js', () => ({
|
|
|
8
8
|
DB_PATH: '/tmp/rigour-test.db',
|
|
9
9
|
RIGOUR_DIR: '/tmp/.rigour',
|
|
10
10
|
}));
|
|
11
|
-
import {
|
|
12
|
-
describe('
|
|
11
|
+
import { getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, queryTokenOverlap, distinctiveTokenAgreement, filterExistingEditScope, getTaskCheckpointCache, setTaskCheckpointCache, hashContent, normalizeQuery } from './cache-engine.js';
|
|
12
|
+
describe('Context cache engine', () => {
|
|
13
13
|
const testCwd = path.join(os.tmpdir(), `rigour-cache-test-${Date.now()}`);
|
|
14
14
|
beforeEach(async () => {
|
|
15
15
|
await fs.ensureDir(testCwd);
|
|
@@ -26,35 +26,6 @@ describe('4-Layer Context Cache Engine', () => {
|
|
|
26
26
|
expect(normalizeQuery(' Add priority to task!! ')).toBe('add priority to task');
|
|
27
27
|
expect(normalizeQuery('Task SHOULD Support Priority.')).toBe('task should support priority');
|
|
28
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
29
|
it('Layer 3: should store and retrieve semantic query scope', async () => {
|
|
59
30
|
const entry = {
|
|
60
31
|
query: 'task priority persistence',
|
|
@@ -125,19 +96,4 @@ describe('4-Layer Context Cache Engine', () => {
|
|
|
125
96
|
expect(result.valid).toEqual(['alive.ts']);
|
|
126
97
|
expect(result.missing).toEqual(['gone.ts']);
|
|
127
98
|
});
|
|
128
|
-
it('Layer 1: invalidates static cache when file content changes', async () => {
|
|
129
|
-
const originalContent = 'export function processTask() { return true; }';
|
|
130
|
-
const entry = {
|
|
131
|
-
exports: ['processTask'],
|
|
132
|
-
imports: [],
|
|
133
|
-
ownership: 'platform-team',
|
|
134
|
-
endpoints: ['/api/task'],
|
|
135
|
-
};
|
|
136
|
-
await setStaticCache('rigour-repo', 'main', 'services/task.ts', originalContent, entry, testCwd);
|
|
137
|
-
const cached = await getStaticCache('rigour-repo', 'main', 'services/task.ts', originalContent, testCwd);
|
|
138
|
-
expect(cached?.exports).toEqual(['processTask']);
|
|
139
|
-
const changedContent = 'export function processTask() { return false; }';
|
|
140
|
-
const stale = await getStaticCache('rigour-repo', 'main', 'services/task.ts', changedContent, testCwd);
|
|
141
|
-
expect(stale).toBeNull();
|
|
142
|
-
});
|
|
143
99
|
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Syncs indexed patterns into static cache entries and component dossiers.
|
|
2
|
+
* Health of the pattern index (.rigour/patterns.json): present, readable, fresh.
|
|
4
3
|
*/
|
|
5
|
-
import type { PatternIndex } from '../pattern-index/types.js';
|
|
6
4
|
export interface IndexHealthReport {
|
|
7
5
|
indexPath: string;
|
|
8
6
|
exists: boolean;
|
|
@@ -14,10 +12,6 @@ export interface IndexHealthReport {
|
|
|
14
12
|
totalFiles?: number;
|
|
15
13
|
message: string;
|
|
16
14
|
}
|
|
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
15
|
/**
|
|
22
16
|
* Check patterns.json freshness relative to the project directory.
|
|
23
17
|
*/
|
|
@@ -1,90 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Syncs indexed patterns into static cache entries and component dossiers.
|
|
2
|
+
* Health of the pattern index (.rigour/patterns.json): present, readable, fresh.
|
|
4
3
|
*/
|
|
5
4
|
import fs from 'fs-extra';
|
|
6
5
|
import path from 'path';
|
|
7
|
-
import { setStaticCache, setComponentCache, hashContent } from './cache-engine.js';
|
|
8
|
-
import { getRepositoryId } from '../storage/lessons.js';
|
|
9
6
|
function getIndexPath(cwd) {
|
|
10
7
|
return path.join(cwd, '.rigour', 'patterns.json');
|
|
11
8
|
}
|
|
12
9
|
const DEFAULT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
13
|
-
async function resolveGitBranch(cwd) {
|
|
14
|
-
try {
|
|
15
|
-
let gitDir = path.join(cwd, '.git');
|
|
16
|
-
const gitStat = await fs.stat(gitDir);
|
|
17
|
-
if (gitStat.isFile()) {
|
|
18
|
-
const pointer = await fs.readFile(gitDir, 'utf8');
|
|
19
|
-
const match = pointer.match(/^gitdir:\s*(.+)$/m);
|
|
20
|
-
if (!match)
|
|
21
|
-
return 'workspace';
|
|
22
|
-
gitDir = path.resolve(cwd, match[1].trim());
|
|
23
|
-
}
|
|
24
|
-
const head = (await fs.readFile(path.join(gitDir, 'HEAD'), 'utf8')).trim();
|
|
25
|
-
return head.startsWith('ref: refs/heads/') ? head.slice('ref: refs/heads/'.length) : head.slice(0, 12);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return 'workspace';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function groupByComponent(patterns) {
|
|
32
|
-
const groups = new Map();
|
|
33
|
-
for (const p of patterns) {
|
|
34
|
-
const key = p.type === 'component' ? p.name : p.category || 'uncategorized';
|
|
35
|
-
const list = groups.get(key) ?? [];
|
|
36
|
-
list.push(p);
|
|
37
|
-
groups.set(key, list);
|
|
38
|
-
}
|
|
39
|
-
return groups;
|
|
40
|
-
}
|
|
41
|
-
function buildDossier(component, patterns) {
|
|
42
|
-
const canonicalFiles = [...new Set(patterns.map(p => p.file))];
|
|
43
|
-
const contracts = patterns
|
|
44
|
-
.filter(p => p.signature)
|
|
45
|
-
.map(p => p.signature)
|
|
46
|
-
.slice(0, 20);
|
|
47
|
-
const descriptions = patterns.map(p => p.description).filter(Boolean);
|
|
48
|
-
const responsibility = descriptions[0] || `Indexed ${patterns.length} patterns for ${component}`;
|
|
49
|
-
return {
|
|
50
|
-
component,
|
|
51
|
-
responsibility,
|
|
52
|
-
canonicalFiles,
|
|
53
|
-
contracts,
|
|
54
|
-
directConsumers: [],
|
|
55
|
-
validationCommands: [],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Sync pattern index entries into static cache and component dossier layers.
|
|
60
|
-
*/
|
|
61
|
-
export async function syncIndexToContextCache(cwd, index) {
|
|
62
|
-
const resolved = path.resolve(cwd);
|
|
63
|
-
const repo = await getRepositoryId(resolved);
|
|
64
|
-
const branch = await resolveGitBranch(resolved);
|
|
65
|
-
const commitSha = hashContent(index.lastUpdated);
|
|
66
|
-
let synced = 0;
|
|
67
|
-
for (const file of index.files) {
|
|
68
|
-
const filePatterns = index.patterns.filter(p => p.file === file.path);
|
|
69
|
-
if (filePatterns.length === 0)
|
|
70
|
-
continue;
|
|
71
|
-
const payload = {
|
|
72
|
-
rigourPatterns: filePatterns.map(p => `${p.type}:${p.name}`),
|
|
73
|
-
exports: filePatterns.filter(p => p.exported).map(p => p.name),
|
|
74
|
-
};
|
|
75
|
-
const fileContent = JSON.stringify(payload);
|
|
76
|
-
await setStaticCache(repo, branch, file.path, fileContent, payload, resolved);
|
|
77
|
-
synced++;
|
|
78
|
-
}
|
|
79
|
-
const componentGroups = groupByComponent(index.patterns);
|
|
80
|
-
for (const [component, patterns] of componentGroups) {
|
|
81
|
-
const dossier = buildDossier(component, patterns);
|
|
82
|
-
const fingerprint = hashContent(JSON.stringify(patterns.map(p => p.id)));
|
|
83
|
-
await setComponentCache(component, commitSha, dossier, fingerprint, '3', resolved);
|
|
84
|
-
synced++;
|
|
85
|
-
}
|
|
86
|
-
return synced;
|
|
87
|
-
}
|
|
88
10
|
/**
|
|
89
11
|
* Check patterns.json freshness relative to the project directory.
|
|
90
12
|
*/
|
package/dist/deep/risk.d.ts
CHANGED
|
@@ -38,4 +38,10 @@ export declare function findFunction(cwd: string, file: string, name: string): {
|
|
|
38
38
|
start: number;
|
|
39
39
|
end: number;
|
|
40
40
|
} | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* What a review record is bound to: the function's exact source. Collapsing whitespace once let
|
|
43
|
+
* `"a b"` and `"a b"` share a hash, so a string changed after review still counted as reviewed.
|
|
44
|
+
* Only line endings are normalised (a Windows checkout is the same code); any other change,
|
|
45
|
+
* formatting included, asks for a new review.
|
|
46
|
+
*/
|
|
41
47
|
export declare function functionHash(text: string): string;
|
package/dist/deep/risk.js
CHANGED
|
@@ -92,8 +92,14 @@ export function findFunction(cwd, file, name) {
|
|
|
92
92
|
visit(sourceFile);
|
|
93
93
|
return found;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* What a review record is bound to: the function's exact source. Collapsing whitespace once let
|
|
97
|
+
* `"a b"` and `"a b"` share a hash, so a string changed after review still counted as reviewed.
|
|
98
|
+
* Only line endings are normalised (a Windows checkout is the same code); any other change,
|
|
99
|
+
* formatting included, asks for a new review.
|
|
100
|
+
*/
|
|
95
101
|
export function functionHash(text) {
|
|
96
|
-
return crypto.createHash('sha256').update(text.replace(/\
|
|
102
|
+
return crypto.createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex');
|
|
97
103
|
}
|
|
98
104
|
const GUARD = /\b(?:if|else|return|throw|break|continue|catch)\b|\?\?|\?\./g;
|
|
99
105
|
function guards(lines) {
|
package/dist/deep/risk.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { rankChangedFunctions, scoreRisk } from './risk.js';
|
|
5
|
+
import { functionHash, rankChangedFunctions, scoreRisk } from './risk.js';
|
|
6
6
|
import { routeFiles } from './router.js';
|
|
7
7
|
let dir;
|
|
8
8
|
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'risk-')); });
|
|
@@ -61,3 +61,14 @@ describe('routeFiles', () => {
|
|
|
61
61
|
expect(stale.stats).toMatchObject({ routed: 1, already_reviewed: 0 });
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
|
+
describe('functionHash', () => {
|
|
65
|
+
it('changes when anything inside the function changes, strings included', () => {
|
|
66
|
+
const a = 'function f() { return "a b"; }';
|
|
67
|
+
const b = 'function f() { return "a b"; }';
|
|
68
|
+
expect(functionHash(a)).not.toBe(functionHash(b));
|
|
69
|
+
expect(functionHash('function f() { return `x ${y}`; }')).not.toBe(functionHash('function f() { return `x ${y}`; }'));
|
|
70
|
+
});
|
|
71
|
+
it('treats a Windows checkout of the same code as the same code', () => {
|
|
72
|
+
expect(functionHash('function f() {\r\n return 1;\r\n}')).toBe(functionHash('function f() {\n return 1;\n}'));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One-time arbitration tokens — bind Studio approve/reject to the
|
|
2
|
+
* One-time arbitration tokens — bind a Studio approve/reject to the MCP process that asked.
|
|
3
|
+
*
|
|
4
|
+
* The token lives only in the requesting process's memory and in a store outside the workspace
|
|
5
|
+
* (never in .rigour/ or the event log, which agents write). Studio consumes it once and signs the
|
|
6
|
+
* decision with it; the requester accepts only a decision whose signature verifies, so appending
|
|
7
|
+
* a decision line to .rigour/events.jsonl approves nothing.
|
|
3
8
|
*/
|
|
9
|
+
export type ArbitrationDecision = 'approve' | 'reject';
|
|
4
10
|
export declare function issueArbitrationToken(cwd: string, requestId: string, ttlMs?: number): Promise<string>;
|
|
5
11
|
/**
|
|
6
|
-
*
|
|
12
|
+
* Studio side: consume the request's token once and sign the human's decision with it.
|
|
13
|
+
* Null when the request is unknown, expired or already decided.
|
|
7
14
|
*/
|
|
8
|
-
export declare function
|
|
15
|
+
export declare function signArbitrationDecision(cwd: string, requestId: string, decision: ArbitrationDecision): Promise<string | null>;
|
|
16
|
+
/** Requester side: true only for a decision signed with the token this process issued. */
|
|
17
|
+
export declare function verifyArbitrationDecision(token: string, requestId: string, decision: unknown, proof: unknown): boolean;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One-time arbitration tokens — bind Studio approve/reject to the
|
|
2
|
+
* One-time arbitration tokens — bind a Studio approve/reject to the MCP process that asked.
|
|
3
|
+
*
|
|
4
|
+
* The token lives only in the requesting process's memory and in a store outside the workspace
|
|
5
|
+
* (never in .rigour/ or the event log, which agents write). Studio consumes it once and signs the
|
|
6
|
+
* decision with it; the requester accepts only a decision whose signature verifies, so appending
|
|
7
|
+
* a decision line to .rigour/events.jsonl approves nothing.
|
|
3
8
|
*/
|
|
4
|
-
import { randomBytes, timingSafeEqual } from 'crypto';
|
|
9
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
|
5
10
|
import fs from 'fs-extra';
|
|
6
11
|
import path from 'path';
|
|
12
|
+
import { repoStateDir } from '../utils/user-state.js';
|
|
7
13
|
function storePath(cwd) {
|
|
8
|
-
return path.join(cwd, '
|
|
14
|
+
return path.join(repoStateDir(cwd), 'arbitration-tokens.json');
|
|
9
15
|
}
|
|
10
16
|
async function readStore(cwd) {
|
|
11
|
-
const p = storePath(cwd);
|
|
12
|
-
if (!(await fs.pathExists(p)))
|
|
13
|
-
return {};
|
|
14
17
|
try {
|
|
15
|
-
return await fs.readJson(
|
|
18
|
+
return await fs.readJson(storePath(cwd));
|
|
16
19
|
}
|
|
17
20
|
catch {
|
|
18
21
|
return {};
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
async function writeStore(cwd, store) {
|
|
22
|
-
await fs.ensureDir(path.
|
|
23
|
-
await fs.writeJson(storePath(cwd), store, { spaces: 2 });
|
|
25
|
+
await fs.ensureDir(path.dirname(storePath(cwd)), { mode: 0o700 });
|
|
26
|
+
await fs.writeJson(storePath(cwd), store, { spaces: 2, mode: 0o600 });
|
|
24
27
|
}
|
|
25
28
|
export async function issueArbitrationToken(cwd, requestId, ttlMs = 60_000) {
|
|
26
29
|
const token = randomBytes(24).toString('hex');
|
|
@@ -34,25 +37,29 @@ export async function issueArbitrationToken(cwd, requestId, ttlMs = 60_000) {
|
|
|
34
37
|
await writeStore(cwd, store);
|
|
35
38
|
return token;
|
|
36
39
|
}
|
|
40
|
+
function decisionProof(token, requestId, decision) {
|
|
41
|
+
return createHmac('sha256', token).update(`${requestId}:${decision}`).digest('hex');
|
|
42
|
+
}
|
|
37
43
|
/**
|
|
38
|
-
*
|
|
44
|
+
* Studio side: consume the request's token once and sign the human's decision with it.
|
|
45
|
+
* Null when the request is unknown, expired or already decided.
|
|
39
46
|
*/
|
|
40
|
-
export async function
|
|
41
|
-
if (!
|
|
42
|
-
return
|
|
47
|
+
export async function signArbitrationDecision(cwd, requestId, decision) {
|
|
48
|
+
if (!requestId)
|
|
49
|
+
return null;
|
|
43
50
|
const store = await readStore(cwd);
|
|
44
51
|
const entry = store[requestId];
|
|
45
52
|
if (!entry)
|
|
46
|
-
return
|
|
47
|
-
if (Date.now() > entry.expiresAt) {
|
|
48
|
-
delete store[requestId];
|
|
49
|
-
await writeStore(cwd, store);
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
const a = Buffer.from(entry.token);
|
|
53
|
-
const b = Buffer.from(token);
|
|
54
|
-
const ok = a.length === b.length && timingSafeEqual(a, b);
|
|
53
|
+
return null;
|
|
55
54
|
delete store[requestId];
|
|
56
55
|
await writeStore(cwd, store);
|
|
57
|
-
return
|
|
56
|
+
return Date.now() > entry.expiresAt ? null : decisionProof(entry.token, requestId, decision);
|
|
57
|
+
}
|
|
58
|
+
/** Requester side: true only for a decision signed with the token this process issued. */
|
|
59
|
+
export function verifyArbitrationDecision(token, requestId, decision, proof) {
|
|
60
|
+
if ((decision !== 'approve' && decision !== 'reject') || typeof proof !== 'string')
|
|
61
|
+
return false;
|
|
62
|
+
const expected = Buffer.from(decisionProof(token, requestId, decision));
|
|
63
|
+
const given = Buffer.from(proof);
|
|
64
|
+
return expected.length === given.length && timingSafeEqual(expected, given);
|
|
58
65
|
}
|
|
@@ -30,9 +30,14 @@ export declare function createAttestation(cwd: string, input: {
|
|
|
30
30
|
export declare function verifyAttestation(cwd: string, bundle: AttestationBundle): Promise<boolean>;
|
|
31
31
|
export declare function loadLatestAttestation(cwd: string): Promise<AttestationBundle | null>;
|
|
32
32
|
/**
|
|
33
|
-
* CI admission
|
|
33
|
+
* CI admission. Admits only a bundle that is signed with a key outside the workspace, records PASS
|
|
34
|
+
* gates, is fresh, is bound to this checkout (commit or tree, and at least one must be present and
|
|
35
|
+
* match), whose artifact digest recomputes from the files here, and, when the caller names the
|
|
36
|
+
* policy it requires, was produced under that policy.
|
|
34
37
|
*/
|
|
35
|
-
export declare function admitForCi(cwd: string
|
|
38
|
+
export declare function admitForCi(cwd: string, options?: {
|
|
39
|
+
policyHash?: string;
|
|
40
|
+
}): Promise<{
|
|
36
41
|
admit: boolean;
|
|
37
42
|
reason: string;
|
|
38
43
|
}>;
|