@rigour-labs/core 5.5.4 → 6.1.0-beta.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/automatic-index.d.ts +18 -0
- package/dist/context/automatic-index.js +142 -0
- package/dist/context/automatic-index.test.d.ts +1 -0
- package/dist/context/automatic-index.test.js +45 -0
- package/dist/context/cache-engine.d.ts +25 -0
- package/dist/context/dependency-graph.d.ts +13 -0
- package/dist/context/dependency-graph.js +97 -0
- package/dist/context/dependency-graph.test.d.ts +1 -0
- package/dist/context/dependency-graph.test.js +13 -0
- package/dist/context/index-bridge.js +21 -2
- package/dist/firewall/adversarial.d.ts +13 -0
- package/dist/firewall/adversarial.js +125 -0
- package/dist/firewall/arbitration-token.d.ts +8 -0
- package/dist/firewall/arbitration-token.js +58 -0
- package/dist/firewall/attestation.d.ts +38 -0
- package/dist/firewall/attestation.js +197 -0
- package/dist/firewall/capability-broker.d.ts +47 -0
- package/dist/firewall/capability-broker.js +209 -0
- package/dist/firewall/firewall.test.d.ts +1 -0
- package/dist/firewall/firewall.test.js +102 -0
- package/dist/firewall/index.d.ts +10 -0
- package/dist/firewall/index.js +10 -0
- package/dist/firewall/mcp-gateway.d.ts +40 -0
- package/dist/firewall/mcp-gateway.js +64 -0
- package/dist/firewall/policy-hash.d.ts +1 -0
- package/dist/firewall/policy-hash.js +5 -0
- package/dist/firewall/scope-enforcement.d.ts +26 -0
- package/dist/firewall/scope-enforcement.js +155 -0
- package/dist/firewall/transaction.d.ts +43 -0
- package/dist/firewall/transaction.js +216 -0
- package/dist/firewall/typed-command.d.ts +16 -0
- package/dist/firewall/typed-command.js +129 -0
- package/dist/firewall/types.d.ts +96 -0
- package/dist/firewall/types.js +5 -0
- package/dist/hooks/checker.d.ts +2 -0
- package/dist/hooks/checker.js +25 -1
- package/dist/hooks/standalone-checker.js +5 -1
- package/dist/hooks/templates.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/pattern-index/embeddings.js +10 -3
- package/dist/pattern-index/indexer.d.ts +2 -0
- package/dist/pattern-index/indexer.js +72 -0
- package/dist/services/agent-history.d.ts +76 -0
- package/dist/services/agent-history.js +201 -0
- package/dist/services/agent-history.test.d.ts +1 -0
- package/dist/services/agent-history.test.js +69 -0
- package/dist/services/context-telemetry-service.d.ts +21 -0
- package/dist/services/context-telemetry-service.js +36 -7
- package/dist/services/context-telemetry-service.test.js +13 -0
- package/dist/services/engineering-knowledge-graph.d.ts +55 -0
- package/dist/services/engineering-knowledge-graph.js +177 -0
- package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
- package/dist/services/engineering-knowledge-graph.test.js +59 -0
- package/dist/services/model-pricing.d.ts +2 -0
- package/dist/services/model-pricing.js +2 -0
- package/dist/services/system-health.d.ts +34 -0
- package/dist/services/system-health.js +53 -0
- package/dist/storage/db.js +130 -1
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/lessons.d.ts +60 -0
- package/dist/storage/lessons.js +322 -0
- package/dist/storage/lessons.test.d.ts +1 -0
- package/dist/storage/lessons.test.js +50 -0
- package/dist/storage/local-encryption.d.ts +2 -0
- package/dist/storage/local-encryption.js +41 -0
- package/dist/storage/team-store.d.ts +75 -0
- package/dist/storage/team-store.js +321 -0
- package/dist/storage/team-store.test.d.ts +1 -0
- package/dist/storage/team-store.test.js +11 -0
- package/dist/storage/team-vector-store.d.ts +25 -0
- package/dist/storage/team-vector-store.js +183 -0
- package/dist/storage/team-vector-store.test.d.ts +1 -0
- package/dist/storage/team-vector-store.test.js +42 -0
- package/package.json +8 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PatternIndex } from '../pattern-index/types.js';
|
|
2
|
+
export type SemanticIndexStatus = 'disabled' | 'warming' | 'ready' | 'degraded';
|
|
3
|
+
export interface AutomaticIndexStatus {
|
|
4
|
+
structural: 'missing' | 'ready' | 'degraded';
|
|
5
|
+
graph: 'missing' | 'ready' | 'degraded';
|
|
6
|
+
semantic: SemanticIndexStatus;
|
|
7
|
+
message?: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function mergeSemanticEmbeddings(latest: PatternIndex, enriched: PatternIndex): PatternIndex;
|
|
11
|
+
export declare function getAutomaticIndexStatus(cwd: string): Promise<AutomaticIndexStatus>;
|
|
12
|
+
export declare function ensureAutomaticIndex(cwd: string, options?: {
|
|
13
|
+
backgroundSemantic?: boolean;
|
|
14
|
+
}): Promise<PatternIndex>;
|
|
15
|
+
export declare function updateAutomaticIndexForFiles(cwd: string, changedFiles: string[]): Promise<{
|
|
16
|
+
index: PatternIndex;
|
|
17
|
+
affectedFiles: string[];
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PatternIndexer, getDefaultIndexPath, loadPatternIndex, savePatternIndex } from '../pattern-index/indexer.js';
|
|
4
|
+
import { affectedDependents, buildDependencyGraph, updateDependencyGraph } from './dependency-graph.js';
|
|
5
|
+
import { syncIndexToContextCache } from './index-bridge.js';
|
|
6
|
+
const enrichmentJobs = new Map();
|
|
7
|
+
function projectRelativeFiles(cwd, files) {
|
|
8
|
+
const root = path.resolve(cwd);
|
|
9
|
+
return files.flatMap(file => {
|
|
10
|
+
const relative = path.relative(root, path.resolve(root, file));
|
|
11
|
+
if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
12
|
+
return [];
|
|
13
|
+
return [relative.split(path.sep).join('/')];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function mergeSemanticEmbeddings(latest, enriched) {
|
|
17
|
+
const embeddings = new Map(enriched.patterns
|
|
18
|
+
.filter(pattern => pattern.embedding?.length)
|
|
19
|
+
.map(pattern => [pattern.id, pattern.embedding]));
|
|
20
|
+
return {
|
|
21
|
+
...latest,
|
|
22
|
+
lastUpdated: new Date().toISOString(),
|
|
23
|
+
patterns: latest.patterns.map(pattern => ({
|
|
24
|
+
...pattern,
|
|
25
|
+
embedding: embeddings.get(pattern.id) ?? pattern.embedding,
|
|
26
|
+
})),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function statusPath(cwd) {
|
|
30
|
+
return path.join(cwd, '.rigour', 'index-status.json');
|
|
31
|
+
}
|
|
32
|
+
async function writeStatus(cwd, status) {
|
|
33
|
+
await fs.ensureDir(path.join(cwd, '.rigour'));
|
|
34
|
+
await fs.writeJson(statusPath(cwd), status, { spaces: 2 });
|
|
35
|
+
}
|
|
36
|
+
export async function getAutomaticIndexStatus(cwd) {
|
|
37
|
+
try {
|
|
38
|
+
return await fs.readJson(statusPath(cwd));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {
|
|
42
|
+
structural: 'missing',
|
|
43
|
+
graph: 'missing',
|
|
44
|
+
semantic: 'disabled',
|
|
45
|
+
updatedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function enrichSemantic(cwd, index) {
|
|
50
|
+
const indexPath = getDefaultIndexPath(cwd);
|
|
51
|
+
try {
|
|
52
|
+
await writeStatus(cwd, {
|
|
53
|
+
structural: 'ready', graph: 'ready', semantic: 'warming', updatedAt: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
const enriched = await new PatternIndexer(cwd, { useEmbeddings: true }).enrichIndex(index);
|
|
56
|
+
const latest = await loadPatternIndex(indexPath) ?? index;
|
|
57
|
+
const merged = mergeSemanticEmbeddings(latest, enriched);
|
|
58
|
+
const embedded = merged.patterns.filter((pattern) => (pattern.embedding?.length ?? 0) > 0).length;
|
|
59
|
+
await savePatternIndex(merged, indexPath);
|
|
60
|
+
await syncIndexToContextCache(cwd, merged);
|
|
61
|
+
await writeStatus(cwd, {
|
|
62
|
+
structural: 'ready',
|
|
63
|
+
graph: 'ready',
|
|
64
|
+
semantic: embedded > 0 ? 'ready' : 'degraded',
|
|
65
|
+
message: embedded > 0 ? `${embedded} patterns enriched` : 'Local embedding model unavailable; text matching remains active.',
|
|
66
|
+
updatedAt: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
await writeStatus(cwd, {
|
|
71
|
+
structural: 'ready',
|
|
72
|
+
graph: 'ready',
|
|
73
|
+
semantic: 'degraded',
|
|
74
|
+
message: error instanceof Error ? error.message : String(error),
|
|
75
|
+
updatedAt: new Date().toISOString(),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
enrichmentJobs.delete(path.resolve(cwd));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function startEnrichment(cwd, index) {
|
|
83
|
+
const key = path.resolve(cwd);
|
|
84
|
+
if (enrichmentJobs.has(key))
|
|
85
|
+
return;
|
|
86
|
+
const job = enrichSemantic(cwd, index);
|
|
87
|
+
enrichmentJobs.set(key, job);
|
|
88
|
+
}
|
|
89
|
+
export async function ensureAutomaticIndex(cwd, options = {}) {
|
|
90
|
+
const indexPath = getDefaultIndexPath(cwd);
|
|
91
|
+
const existing = await loadPatternIndex(indexPath);
|
|
92
|
+
const indexer = new PatternIndexer(cwd, { useEmbeddings: false });
|
|
93
|
+
const index = existing ?? await indexer.buildIndex();
|
|
94
|
+
if (!existing)
|
|
95
|
+
await savePatternIndex(index, indexPath);
|
|
96
|
+
await syncIndexToContextCache(cwd, index);
|
|
97
|
+
if (!(await fs.pathExists(path.join(cwd, '.rigour', 'dependency-graph.json')))) {
|
|
98
|
+
await buildDependencyGraph(cwd, index);
|
|
99
|
+
}
|
|
100
|
+
const hasEmbeddings = index.patterns.some((pattern) => (pattern.embedding?.length ?? 0) > 0);
|
|
101
|
+
await writeStatus(cwd, {
|
|
102
|
+
structural: 'ready',
|
|
103
|
+
graph: 'ready',
|
|
104
|
+
semantic: hasEmbeddings ? 'ready' : options.backgroundSemantic === false ? 'disabled' : 'warming',
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
});
|
|
107
|
+
if (!hasEmbeddings && options.backgroundSemantic !== false)
|
|
108
|
+
startEnrichment(cwd, index);
|
|
109
|
+
return index;
|
|
110
|
+
}
|
|
111
|
+
export async function updateAutomaticIndexForFiles(cwd, changedFiles) {
|
|
112
|
+
const normalizedChangedFiles = projectRelativeFiles(cwd, changedFiles);
|
|
113
|
+
const existing = await loadPatternIndex(getDefaultIndexPath(cwd));
|
|
114
|
+
if (!existing)
|
|
115
|
+
return { index: await ensureAutomaticIndex(cwd), affectedFiles: normalizedChangedFiles };
|
|
116
|
+
const previousStatus = await getAutomaticIndexStatus(cwd);
|
|
117
|
+
let graph = null;
|
|
118
|
+
try {
|
|
119
|
+
graph = await fs.readJson(path.join(cwd, '.rigour', 'dependency-graph.json'));
|
|
120
|
+
}
|
|
121
|
+
catch { /* baseline below */ }
|
|
122
|
+
const affectedFiles = graph ? affectedDependents(graph, normalizedChangedFiles) : normalizedChangedFiles;
|
|
123
|
+
const indexer = new PatternIndexer(cwd, { useEmbeddings: false });
|
|
124
|
+
const index = await indexer.updateFiles(existing, affectedFiles);
|
|
125
|
+
await savePatternIndex(index, getDefaultIndexPath(cwd));
|
|
126
|
+
await syncIndexToContextCache(cwd, index);
|
|
127
|
+
if (graph)
|
|
128
|
+
await updateDependencyGraph(cwd, index, graph, affectedFiles);
|
|
129
|
+
else
|
|
130
|
+
await buildDependencyGraph(cwd, index);
|
|
131
|
+
await writeStatus(cwd, {
|
|
132
|
+
structural: 'ready', graph: 'ready',
|
|
133
|
+
semantic: index.patterns.some(pattern => (pattern.embedding?.length ?? 0) > 0)
|
|
134
|
+
? 'ready'
|
|
135
|
+
: previousStatus.semantic === 'disabled' ? 'disabled' : 'warming',
|
|
136
|
+
message: `${normalizedChangedFiles.length} changed file(s); ${affectedFiles.length} affected file(s) refreshed without a full scan.`,
|
|
137
|
+
updatedAt: new Date().toISOString(),
|
|
138
|
+
});
|
|
139
|
+
if (previousStatus.semantic !== 'disabled')
|
|
140
|
+
startEnrichment(cwd, index);
|
|
141
|
+
return { index, affectedFiles };
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { ensureAutomaticIndex, mergeSemanticEmbeddings, updateAutomaticIndexForFiles } from './automatic-index.js';
|
|
6
|
+
describe('automatic incremental index', () => {
|
|
7
|
+
const cwd = path.join(os.tmpdir(), `rigour-auto-index-${process.pid}`);
|
|
8
|
+
afterEach(() => fs.remove(cwd));
|
|
9
|
+
it('builds once and refreshes only changed files and their dependents', async () => {
|
|
10
|
+
await fs.outputFile(path.join(cwd, 'src/value.ts'), 'export const value = 1;');
|
|
11
|
+
await fs.outputFile(path.join(cwd, 'src/consumer.ts'), "import { value } from './value';\nexport const doubled = value * 2;");
|
|
12
|
+
const baseline = await ensureAutomaticIndex(cwd, { backgroundSemantic: false });
|
|
13
|
+
const untouched = baseline.files.find(file => file.path === 'src/consumer.ts')?.indexedAt;
|
|
14
|
+
await fs.writeFile(path.join(cwd, 'src/value.ts'), 'export const value = 2;');
|
|
15
|
+
const result = await updateAutomaticIndexForFiles(cwd, ['src/value.ts']);
|
|
16
|
+
expect(result.affectedFiles.sort()).toEqual(['src/consumer.ts', 'src/value.ts']);
|
|
17
|
+
expect(result.index.files.find(file => file.path === 'src/consumer.ts')?.indexedAt).not.toBe(untouched);
|
|
18
|
+
});
|
|
19
|
+
it('ignores changed-file paths outside the repository', async () => {
|
|
20
|
+
await fs.outputFile(path.join(cwd, 'src/value.ts'), 'export const value = 1;');
|
|
21
|
+
const baseline = await ensureAutomaticIndex(cwd, { backgroundSemantic: false });
|
|
22
|
+
const result = await updateAutomaticIndexForFiles(cwd, [path.join(cwd, '..', 'outside.ts')]);
|
|
23
|
+
expect(result.affectedFiles).toEqual([]);
|
|
24
|
+
expect(result.index.patterns).toEqual(baseline.patterns);
|
|
25
|
+
});
|
|
26
|
+
it('merges background embeddings onto the latest structural snapshot', () => {
|
|
27
|
+
const pattern = (id, file, embedding) => ({
|
|
28
|
+
id, file, embedding, type: 'function', name: id, line: 1, endLine: 1,
|
|
29
|
+
signature: '', description: '', keywords: [], hash: id, exported: true,
|
|
30
|
+
usageCount: 0, indexedAt: '2026-09-10T00:00:00.000Z',
|
|
31
|
+
});
|
|
32
|
+
const makeIndex = (patterns) => ({
|
|
33
|
+
version: '1.0.0', lastUpdated: '2026-09-10T00:00:00.000Z', rootDir: cwd,
|
|
34
|
+
patterns,
|
|
35
|
+
files: patterns.map(entry => ({ path: entry.file, hash: entry.hash, patternCount: 1, indexedAt: entry.indexedAt })),
|
|
36
|
+
stats: { totalPatterns: patterns.length, totalFiles: patterns.length, byType: { function: patterns.length }, indexDurationMs: 1 },
|
|
37
|
+
});
|
|
38
|
+
const latest = makeIndex([pattern('changed', 'src/changed.ts'), pattern('new', 'src/new.ts')]);
|
|
39
|
+
const enriched = makeIndex([pattern('changed', 'src/old.ts', [0.1, 0.2]), pattern('deleted', 'src/deleted.ts', [0.3])]);
|
|
40
|
+
const merged = mergeSemanticEmbeddings(latest, enriched);
|
|
41
|
+
expect(merged.patterns.map(entry => entry.id)).toEqual(['changed', 'new']);
|
|
42
|
+
expect(merged.patterns[0].file).toBe('src/changed.ts');
|
|
43
|
+
expect(merged.patterns[0].embedding).toEqual([0.1, 0.2]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -36,6 +36,31 @@ export interface SemanticQueryEntry {
|
|
|
36
36
|
evidence: string[];
|
|
37
37
|
commitSha: string;
|
|
38
38
|
confidence: number;
|
|
39
|
+
sourceTokens?: number;
|
|
40
|
+
candidateFiles?: number;
|
|
41
|
+
returnedFiles?: number;
|
|
42
|
+
guidance?: {
|
|
43
|
+
kind: 'context-scope' | 'pattern' | 'memory';
|
|
44
|
+
query?: string;
|
|
45
|
+
recommendation: string;
|
|
46
|
+
patternRefs?: Array<{
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
file?: string;
|
|
50
|
+
}>;
|
|
51
|
+
lessonRefs?: Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
visibility?: string;
|
|
55
|
+
}>;
|
|
56
|
+
memoryRefs?: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
}>;
|
|
60
|
+
selectedFiles?: string[];
|
|
61
|
+
excludedFileCount?: number;
|
|
62
|
+
conflicts?: number;
|
|
63
|
+
};
|
|
39
64
|
}
|
|
40
65
|
export interface TaskCheckpointPacket {
|
|
41
66
|
taskId: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PatternIndex } from '../pattern-index/types.js';
|
|
2
|
+
export interface DependencyGraph {
|
|
3
|
+
version: 1;
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
nodes: string[];
|
|
6
|
+
edges: Array<{
|
|
7
|
+
from: string;
|
|
8
|
+
to: string;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildDependencyGraph(cwd: string, index: PatternIndex): Promise<DependencyGraph>;
|
|
12
|
+
export declare function updateDependencyGraph(cwd: string, index: PatternIndex, existing: DependencyGraph, changedFiles: string[]): Promise<DependencyGraph>;
|
|
13
|
+
export declare function affectedDependents(graph: DependencyGraph, changedFiles: string[]): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const IMPORT_PATTERNS = [
|
|
4
|
+
/(?:import|export)\s+(?:[^'"\n]+?\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
5
|
+
/require\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
6
|
+
/from\s+([\w.]+)\s+import\s+/g,
|
|
7
|
+
];
|
|
8
|
+
function resolveImport(from, specifier, files) {
|
|
9
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/'))
|
|
10
|
+
return null;
|
|
11
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier));
|
|
12
|
+
const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.js`, `${base}.jsx`, `${base}.py`];
|
|
13
|
+
for (const indexName of ['index.ts', 'index.tsx', 'index.js', 'index.jsx']) {
|
|
14
|
+
candidates.push(path.posix.join(base, indexName));
|
|
15
|
+
}
|
|
16
|
+
return candidates.find((candidate) => files.has(candidate)) ?? null;
|
|
17
|
+
}
|
|
18
|
+
function importsFrom(content) {
|
|
19
|
+
const imports = new Set();
|
|
20
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
21
|
+
pattern.lastIndex = 0;
|
|
22
|
+
let match;
|
|
23
|
+
while ((match = pattern.exec(content)) !== null)
|
|
24
|
+
imports.add(match[1]);
|
|
25
|
+
}
|
|
26
|
+
return [...imports];
|
|
27
|
+
}
|
|
28
|
+
export async function buildDependencyGraph(cwd, index) {
|
|
29
|
+
const files = new Set(index.files.map((file) => file.path.split(path.sep).join('/')));
|
|
30
|
+
const edges = [];
|
|
31
|
+
for (const from of files) {
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.readFile(path.join(cwd, from), 'utf8');
|
|
34
|
+
for (const specifier of importsFrom(content)) {
|
|
35
|
+
const to = resolveImport(from, specifier, files);
|
|
36
|
+
if (to)
|
|
37
|
+
edges.push({ from, to });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// A disappearing file is handled by the next incremental index update.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const graph = {
|
|
45
|
+
version: 1,
|
|
46
|
+
generatedAt: new Date().toISOString(),
|
|
47
|
+
nodes: [...files],
|
|
48
|
+
edges,
|
|
49
|
+
};
|
|
50
|
+
const graphPath = path.join(cwd, '.rigour', 'dependency-graph.json');
|
|
51
|
+
await fs.ensureDir(path.dirname(graphPath));
|
|
52
|
+
await fs.writeJson(graphPath, graph, { spaces: 2 });
|
|
53
|
+
return graph;
|
|
54
|
+
}
|
|
55
|
+
export async function updateDependencyGraph(cwd, index, existing, changedFiles) {
|
|
56
|
+
const files = new Set(index.files.map(file => file.path.split(path.sep).join('/')));
|
|
57
|
+
const root = path.resolve(cwd);
|
|
58
|
+
const changed = new Set(changedFiles.flatMap(file => {
|
|
59
|
+
const relative = path.relative(root, path.resolve(root, file));
|
|
60
|
+
if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
61
|
+
return [];
|
|
62
|
+
return [relative.split(path.sep).join('/')];
|
|
63
|
+
}));
|
|
64
|
+
const edges = existing.edges.filter(edge => !changed.has(edge.from) && files.has(edge.from) && files.has(edge.to));
|
|
65
|
+
for (const from of changed) {
|
|
66
|
+
if (!files.has(from))
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(path.join(cwd, from), 'utf8');
|
|
70
|
+
for (const specifier of importsFrom(content)) {
|
|
71
|
+
const to = resolveImport(from, specifier, files);
|
|
72
|
+
if (to)
|
|
73
|
+
edges.push({ from, to });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Deleted files are removed from the node and edge sets.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const graph = { version: 1, generatedAt: new Date().toISOString(), nodes: [...files], edges };
|
|
81
|
+
await fs.writeJson(path.join(cwd, '.rigour', 'dependency-graph.json'), graph, { spaces: 2 });
|
|
82
|
+
return graph;
|
|
83
|
+
}
|
|
84
|
+
export function affectedDependents(graph, changedFiles) {
|
|
85
|
+
const affected = new Set(changedFiles);
|
|
86
|
+
let changed = true;
|
|
87
|
+
while (changed) {
|
|
88
|
+
changed = false;
|
|
89
|
+
for (const edge of graph.edges) {
|
|
90
|
+
if (affected.has(edge.to) && !affected.has(edge.from)) {
|
|
91
|
+
affected.add(edge.from);
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...affected];
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { affectedDependents } from './dependency-graph.js';
|
|
3
|
+
describe('affectedDependents', () => {
|
|
4
|
+
it('includes direct and transitive consumers', () => {
|
|
5
|
+
const affected = affectedDependents({
|
|
6
|
+
version: 1,
|
|
7
|
+
generatedAt: '2026-09-10T00:00:00Z',
|
|
8
|
+
nodes: ['a.ts', 'b.ts', 'c.ts'],
|
|
9
|
+
edges: [{ from: 'b.ts', to: 'a.ts' }, { from: 'c.ts', to: 'b.ts' }],
|
|
10
|
+
}, ['a.ts']);
|
|
11
|
+
expect(new Set(affected)).toEqual(new Set(['a.ts', 'b.ts', 'c.ts']));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -5,10 +5,29 @@
|
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { setStaticCache, setComponentCache, hashContent } from './cache-engine.js';
|
|
8
|
+
import { getRepositoryId } from '../storage/lessons.js';
|
|
8
9
|
function getIndexPath(cwd) {
|
|
9
10
|
return path.join(cwd, '.rigour', 'patterns.json');
|
|
10
11
|
}
|
|
11
12
|
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
|
+
}
|
|
12
31
|
function groupByComponent(patterns) {
|
|
13
32
|
const groups = new Map();
|
|
14
33
|
for (const p of patterns) {
|
|
@@ -41,8 +60,8 @@ function buildDossier(component, patterns) {
|
|
|
41
60
|
*/
|
|
42
61
|
export async function syncIndexToContextCache(cwd, index) {
|
|
43
62
|
const resolved = path.resolve(cwd);
|
|
44
|
-
const repo =
|
|
45
|
-
const branch =
|
|
63
|
+
const repo = await getRepositoryId(resolved);
|
|
64
|
+
const branch = await resolveGitBranch(resolved);
|
|
46
65
|
const commitSha = hashContent(index.lastUpdated);
|
|
47
66
|
let synced = 0;
|
|
48
67
|
for (const file of index.files) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial replay — deterministic corpus against the firewall kernel.
|
|
3
|
+
* Successful unexpected allows emit suggested deny regressions (not LLM verdicts).
|
|
4
|
+
*/
|
|
5
|
+
import type { AdversarialCase, AdversarialCaseResult } from './types.js';
|
|
6
|
+
export declare const DEFAULT_ADVERSARIAL_CORPUS: AdversarialCase[];
|
|
7
|
+
export declare function runAdversarialCase(c: AdversarialCase): AdversarialCaseResult;
|
|
8
|
+
export declare function runAdversarialCorpus(cases?: AdversarialCase[]): {
|
|
9
|
+
passed: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
results: AdversarialCaseResult[];
|
|
12
|
+
};
|
|
13
|
+
export declare function persistAdversarialReport(cwd: string, report: ReturnType<typeof runAdversarialCorpus>): Promise<string>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial replay — deterministic corpus against the firewall kernel.
|
|
3
|
+
* Successful unexpected allows emit suggested deny regressions (not LLM verdicts).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { evaluateActionDeterministic } from './capability-broker.js';
|
|
8
|
+
export const DEFAULT_ADVERSARIAL_CORPUS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'deny-shell-meta',
|
|
11
|
+
description: 'Shell metacharacters must be denied',
|
|
12
|
+
proposedAction: {
|
|
13
|
+
action: 'shell.exec',
|
|
14
|
+
resource: 'shell',
|
|
15
|
+
command: 'ls; rm -rf /',
|
|
16
|
+
},
|
|
17
|
+
expectedDecision: 'deny',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'deny-undeclared-mcp',
|
|
21
|
+
description: 'Undeclared MCP tool must be denied',
|
|
22
|
+
proposedAction: {
|
|
23
|
+
action: 'mcp.call',
|
|
24
|
+
resource: 'github.merge_pull_request',
|
|
25
|
+
},
|
|
26
|
+
expectedDecision: 'deny',
|
|
27
|
+
allowlist: ['rigour_check'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'deny-out-of-scope-write',
|
|
31
|
+
description: 'Write outside agent scope must be scope-violation',
|
|
32
|
+
proposedAction: {
|
|
33
|
+
action: 'filesystem.write',
|
|
34
|
+
resource: '.github/workflows/deploy.yml',
|
|
35
|
+
},
|
|
36
|
+
expectedDecision: 'scope-violation',
|
|
37
|
+
agentScope: ['packages/rigour-core/**'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'deny-git-force-push',
|
|
41
|
+
description: 'git push --force must be denied',
|
|
42
|
+
proposedAction: {
|
|
43
|
+
action: 'shell.exec',
|
|
44
|
+
resource: 'shell',
|
|
45
|
+
command: 'git push --force origin main',
|
|
46
|
+
},
|
|
47
|
+
expectedDecision: 'deny',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'allow-scoped-write',
|
|
51
|
+
description: 'In-scope write is allowed',
|
|
52
|
+
proposedAction: {
|
|
53
|
+
action: 'filesystem.write',
|
|
54
|
+
resource: 'packages/rigour-core/src/firewall/types.ts',
|
|
55
|
+
},
|
|
56
|
+
expectedDecision: 'allow',
|
|
57
|
+
agentScope: ['packages/rigour-core/**'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'allow-typed-test',
|
|
61
|
+
description: 'Allowlisted vitest command is allowed',
|
|
62
|
+
proposedAction: {
|
|
63
|
+
action: 'shell.exec',
|
|
64
|
+
resource: 'shell',
|
|
65
|
+
command: 'vitest run packages/rigour-core/src/firewall',
|
|
66
|
+
},
|
|
67
|
+
expectedDecision: 'allow',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
function normalizeDecision(d) {
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
export function runAdversarialCase(c) {
|
|
74
|
+
const agentScopes = c.agentScope
|
|
75
|
+
? [{ agentId: 'adversarial-agent', taskScope: c.agentScope }]
|
|
76
|
+
: [];
|
|
77
|
+
const evaluation = evaluateActionDeterministic({
|
|
78
|
+
action: c.proposedAction.action,
|
|
79
|
+
resource: c.proposedAction.resource,
|
|
80
|
+
command: c.proposedAction.command,
|
|
81
|
+
toolAllowlist: c.allowlist ?? [],
|
|
82
|
+
agentScopes,
|
|
83
|
+
agentId: c.agentScope ? 'adversarial-agent' : undefined,
|
|
84
|
+
});
|
|
85
|
+
const actual = normalizeDecision(evaluation.decision);
|
|
86
|
+
const passed = actual === c.expectedDecision;
|
|
87
|
+
let suggestedRule;
|
|
88
|
+
if (!passed && (actual === 'allow')) {
|
|
89
|
+
suggestedRule = [
|
|
90
|
+
'deny:',
|
|
91
|
+
` - tool: ${c.proposedAction.action}`,
|
|
92
|
+
` resource: ${JSON.stringify(c.proposedAction.resource)}`,
|
|
93
|
+
c.proposedAction.command ? ` command_matches: ${JSON.stringify(c.proposedAction.command)}` : null,
|
|
94
|
+
].filter(Boolean).join('\n');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
caseId: c.id,
|
|
98
|
+
expected: c.expectedDecision,
|
|
99
|
+
actual,
|
|
100
|
+
passed,
|
|
101
|
+
reason: evaluation.reason,
|
|
102
|
+
suggestedRule,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function runAdversarialCorpus(cases = DEFAULT_ADVERSARIAL_CORPUS) {
|
|
106
|
+
const results = cases.map(runAdversarialCase);
|
|
107
|
+
return {
|
|
108
|
+
passed: results.filter(r => r.passed).length,
|
|
109
|
+
failed: results.filter(r => !r.passed).length,
|
|
110
|
+
results,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export async function persistAdversarialReport(cwd, report) {
|
|
114
|
+
const dir = path.join(cwd, '.rigour');
|
|
115
|
+
await fs.ensureDir(dir);
|
|
116
|
+
const out = path.join(dir, 'adversarial-report.json');
|
|
117
|
+
await fs.writeJson(out, { ...report, generatedAt: new Date().toISOString() }, { spaces: 2 });
|
|
118
|
+
const regressions = report.results.filter(r => !r.passed && r.suggestedRule);
|
|
119
|
+
if (regressions.length > 0) {
|
|
120
|
+
const fixturePath = path.join(dir, 'adversarial-regressions.yml');
|
|
121
|
+
const body = regressions.map(r => `# from ${r.caseId}\n${r.suggestedRule}`).join('\n\n');
|
|
122
|
+
await fs.writeFile(fixturePath, body + '\n');
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time arbitration tokens — bind Studio approve/reject to the requesting MCP process.
|
|
3
|
+
*/
|
|
4
|
+
export declare function issueArbitrationToken(cwd: string, requestId: string, ttlMs?: number): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Consume a one-time token. Returns false if missing, expired, or mismatched.
|
|
7
|
+
*/
|
|
8
|
+
export declare function consumeArbitrationToken(cwd: string, requestId: string, token: string | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time arbitration tokens — bind Studio approve/reject to the requesting MCP process.
|
|
3
|
+
*/
|
|
4
|
+
import { randomBytes, timingSafeEqual } from 'crypto';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
function storePath(cwd) {
|
|
8
|
+
return path.join(cwd, '.rigour', 'arbitration-tokens.json');
|
|
9
|
+
}
|
|
10
|
+
async function readStore(cwd) {
|
|
11
|
+
const p = storePath(cwd);
|
|
12
|
+
if (!(await fs.pathExists(p)))
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
return await fs.readJson(p);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function writeStore(cwd, store) {
|
|
22
|
+
await fs.ensureDir(path.join(cwd, '.rigour'));
|
|
23
|
+
await fs.writeJson(storePath(cwd), store, { spaces: 2 });
|
|
24
|
+
}
|
|
25
|
+
export async function issueArbitrationToken(cwd, requestId, ttlMs = 60_000) {
|
|
26
|
+
const token = randomBytes(24).toString('hex');
|
|
27
|
+
const store = await readStore(cwd);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const [k, v] of Object.entries(store)) {
|
|
30
|
+
if (v.expiresAt < now)
|
|
31
|
+
delete store[k];
|
|
32
|
+
}
|
|
33
|
+
store[requestId] = { token, expiresAt: now + ttlMs };
|
|
34
|
+
await writeStore(cwd, store);
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Consume a one-time token. Returns false if missing, expired, or mismatched.
|
|
39
|
+
*/
|
|
40
|
+
export async function consumeArbitrationToken(cwd, requestId, token) {
|
|
41
|
+
if (!token || !requestId)
|
|
42
|
+
return false;
|
|
43
|
+
const store = await readStore(cwd);
|
|
44
|
+
const entry = store[requestId];
|
|
45
|
+
if (!entry)
|
|
46
|
+
return false;
|
|
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);
|
|
55
|
+
delete store[requestId];
|
|
56
|
+
await writeStore(cwd, store);
|
|
57
|
+
return ok;
|
|
58
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed attestation bundles for completed transactions.
|
|
3
|
+
* Keys prefer env / home directory — not agent-writable workspace keys for CI admit.
|
|
4
|
+
*/
|
|
5
|
+
import type { AttestationBundle, TransactionRecord } from './types.js';
|
|
6
|
+
export declare function resolveAttestationKey(cwd: string): Promise<{
|
|
7
|
+
key: Buffer;
|
|
8
|
+
source: 'env' | 'home' | 'workspace';
|
|
9
|
+
}>;
|
|
10
|
+
/** @deprecated use resolveAttestationKey */
|
|
11
|
+
export declare function ensureAttestationKey(cwd: string): Promise<Buffer>;
|
|
12
|
+
export declare function computeArtifactDigest(files: string[], contents: Map<string, string>): string;
|
|
13
|
+
export declare function getGitCommitSha(dir: string): Promise<string | null>;
|
|
14
|
+
export declare function getGitTreeDigest(dir: string): Promise<string | null>;
|
|
15
|
+
export declare function createAttestation(cwd: string, input: {
|
|
16
|
+
transaction: TransactionRecord;
|
|
17
|
+
gateResults: {
|
|
18
|
+
status: string;
|
|
19
|
+
score?: number;
|
|
20
|
+
failedGates: string[];
|
|
21
|
+
};
|
|
22
|
+
toolsUsed?: string[];
|
|
23
|
+
overrides?: string[];
|
|
24
|
+
userId?: string;
|
|
25
|
+
fileContents?: Map<string, string>;
|
|
26
|
+
commitSha?: string;
|
|
27
|
+
treeDigest?: string;
|
|
28
|
+
verifyRoot?: string;
|
|
29
|
+
}): Promise<AttestationBundle>;
|
|
30
|
+
export declare function verifyAttestation(cwd: string, bundle: AttestationBundle): Promise<boolean>;
|
|
31
|
+
export declare function loadLatestAttestation(cwd: string): Promise<AttestationBundle | null>;
|
|
32
|
+
/**
|
|
33
|
+
* CI admission: valid signature, PASS gates, bound commit/tree, fresh, non-workspace-key unless allowed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function admitForCi(cwd: string): Promise<{
|
|
36
|
+
admit: boolean;
|
|
37
|
+
reason: string;
|
|
38
|
+
}>;
|