@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
|
@@ -17,6 +17,7 @@ export interface TaskContextStats {
|
|
|
17
17
|
potentialAvoidedTokens: number;
|
|
18
18
|
cacheHitRate: number;
|
|
19
19
|
repeatedReadsPrevented: number;
|
|
20
|
+
deduplicatedTokens: number;
|
|
20
21
|
checkpointReplayAvoided: number;
|
|
21
22
|
isEstimated: boolean;
|
|
22
23
|
}
|
|
@@ -26,15 +27,35 @@ export interface TaskCostStats {
|
|
|
26
27
|
outputTokens: number;
|
|
27
28
|
costUsd: number;
|
|
28
29
|
source: string;
|
|
30
|
+
models: string[];
|
|
29
31
|
isEstimated: boolean;
|
|
30
32
|
pricingBasis?: string;
|
|
33
|
+
classification: 'observed' | 'modelled estimate' | 'unavailable';
|
|
34
|
+
observedCoverage: number;
|
|
31
35
|
};
|
|
32
36
|
estimated: {
|
|
37
|
+
/** Compatibility alias: retrieval-only avoided tokens. */
|
|
33
38
|
potentialContextAvoided: number;
|
|
39
|
+
/** Compatibility alias: retrieval-only avoided cost. */
|
|
34
40
|
estimatedCostAvoidedUsd: number;
|
|
41
|
+
retrievalAvoidedTokens: number;
|
|
42
|
+
retrievalAvoidedCostUsd: number;
|
|
43
|
+
checkpointReplayAvoidedTokens: number;
|
|
44
|
+
checkpointReplayAvoidedCostUsd: number;
|
|
45
|
+
retrievalAvoidedCostRangeUsd?: {
|
|
46
|
+
min: number;
|
|
47
|
+
max: number;
|
|
48
|
+
};
|
|
49
|
+
checkpointReplayAvoidedCostRangeUsd?: {
|
|
50
|
+
min: number;
|
|
51
|
+
max: number;
|
|
52
|
+
};
|
|
53
|
+
categoriesAreAdditive: false;
|
|
35
54
|
isEstimated: boolean;
|
|
36
55
|
inputPricePerMillionUsd?: number;
|
|
37
56
|
pricingBasis?: string;
|
|
57
|
+
pricingSource: string;
|
|
58
|
+
pricingEffectiveDate: string;
|
|
38
59
|
};
|
|
39
60
|
}
|
|
40
61
|
export interface CachePerformanceStats {
|
|
@@ -8,7 +8,7 @@ import fs from 'fs-extra';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { getContextEvents, getModelUsages, getCheckpointMetrics, recordModelUsage, } from '../storage/index.js';
|
|
10
10
|
import { getCursorApiKey } from '../settings.js';
|
|
11
|
-
import { estimateAvoidedContextCostUsd, estimateTokenCostUsd, computeWeightedInputPricePerMillion, } from './model-pricing.js';
|
|
11
|
+
import { estimateAvoidedContextCostUsd, estimateTokenCostUsd, computeWeightedInputPricePerMillion, MODEL_PRICING_EFFECTIVE_DATE, MODEL_PRICING_SOURCE, } from './model-pricing.js';
|
|
12
12
|
import { fetchCursorUsagePages } from './cursor-usage-client.js';
|
|
13
13
|
import { CURSOR_ADMIN_API_SOURCE, centsToUsd, isCursorAdminApiEvent, normalizeCursorUsageEvent } from './cursor-usage-normalizer.js';
|
|
14
14
|
/** Heuristic token counter (~4 chars per token for code/text) */
|
|
@@ -46,6 +46,7 @@ export async function getTaskContextStats(taskId, cwd) {
|
|
|
46
46
|
let returnedTokens = 0;
|
|
47
47
|
let cacheHits = 0;
|
|
48
48
|
let repeatedReads = 0;
|
|
49
|
+
let deduplicatedTokens = 0;
|
|
49
50
|
for (const e of events) {
|
|
50
51
|
candidateTokens += e.candidateTokens || 0;
|
|
51
52
|
returnedTokens += e.returnedTokens || 0;
|
|
@@ -53,6 +54,7 @@ export async function getTaskContextStats(taskId, cwd) {
|
|
|
53
54
|
cacheHits++;
|
|
54
55
|
}
|
|
55
56
|
const deduped = e.deduplicatedTokens ?? 0;
|
|
57
|
+
deduplicatedTokens += deduped;
|
|
56
58
|
if (deduped > 0) {
|
|
57
59
|
repeatedReads += Math.ceil(deduped / 500);
|
|
58
60
|
}
|
|
@@ -68,6 +70,7 @@ export async function getTaskContextStats(taskId, cwd) {
|
|
|
68
70
|
potentialAvoidedTokens,
|
|
69
71
|
cacheHitRate,
|
|
70
72
|
repeatedReadsPrevented: repeatedReads,
|
|
73
|
+
deduplicatedTokens,
|
|
71
74
|
checkpointReplayAvoided,
|
|
72
75
|
isEstimated: retrievals === 0,
|
|
73
76
|
};
|
|
@@ -83,10 +86,13 @@ export async function getTaskCostStats(taskId, cwd) {
|
|
|
83
86
|
let observedCostUsd = 0;
|
|
84
87
|
let primarySource = 'estimated';
|
|
85
88
|
const hasObservedUsage = usages.length > 0;
|
|
89
|
+
let observedUsageCount = 0;
|
|
86
90
|
for (const u of usages) {
|
|
87
91
|
actualInputTokens += u.inputTokens || 0;
|
|
88
92
|
actualOutputTokens += u.outputTokens || 0;
|
|
89
93
|
observedCostUsd += u.observedCostUsd || 0;
|
|
94
|
+
if ((u.observedCostUsd || 0) > 0)
|
|
95
|
+
observedUsageCount++;
|
|
90
96
|
if (u.source && u.source !== 'estimated') {
|
|
91
97
|
primarySource = u.source;
|
|
92
98
|
}
|
|
@@ -103,23 +109,46 @@ export async function getTaskCostStats(taskId, cwd) {
|
|
|
103
109
|
else if (hasObservedCost) {
|
|
104
110
|
actualPricingBasis = 'observed-cursor-charged-cents';
|
|
105
111
|
}
|
|
106
|
-
const
|
|
107
|
-
const
|
|
112
|
+
const retrievalPricing = estimateAvoidedContextCostUsd(stats.potentialAvoidedTokens, usages);
|
|
113
|
+
const checkpointPricing = estimateAvoidedContextCostUsd(stats.checkpointReplayAvoided, usages);
|
|
114
|
+
const unknownPricing = retrievalPricing.pricing.pricingBasis.includes('fallback');
|
|
115
|
+
const range = (tokens) => ({
|
|
116
|
+
min: parseFloat(((tokens / 1_000_000) * 0.15).toFixed(4)),
|
|
117
|
+
max: parseFloat(((tokens / 1_000_000) * 15).toFixed(4)),
|
|
118
|
+
});
|
|
108
119
|
return {
|
|
109
120
|
actual: {
|
|
110
121
|
inputTokens: actualInputTokens,
|
|
111
122
|
outputTokens: actualOutputTokens,
|
|
112
123
|
costUsd: parseFloat(actualCostUsd.toFixed(4)),
|
|
113
124
|
source: primarySource,
|
|
125
|
+
models: Array.from(new Set(usages.map(usage => usage.model).filter((model) => Boolean(model)))),
|
|
114
126
|
isEstimated: actualIsEstimated,
|
|
115
127
|
pricingBasis: actualPricingBasis,
|
|
128
|
+
classification: hasObservedCost
|
|
129
|
+
? 'observed'
|
|
130
|
+
: hasObservedUsage
|
|
131
|
+
? 'modelled estimate'
|
|
132
|
+
: 'unavailable',
|
|
133
|
+
observedCoverage: usages.length > 0
|
|
134
|
+
? parseFloat((observedUsageCount / usages.length).toFixed(2))
|
|
135
|
+
: 0,
|
|
116
136
|
},
|
|
117
137
|
estimated: {
|
|
118
|
-
potentialContextAvoided:
|
|
119
|
-
estimatedCostAvoidedUsd:
|
|
138
|
+
potentialContextAvoided: stats.potentialAvoidedTokens,
|
|
139
|
+
estimatedCostAvoidedUsd: retrievalPricing.costUsd,
|
|
140
|
+
retrievalAvoidedTokens: stats.potentialAvoidedTokens,
|
|
141
|
+
retrievalAvoidedCostUsd: retrievalPricing.costUsd,
|
|
142
|
+
checkpointReplayAvoidedTokens: stats.checkpointReplayAvoided,
|
|
143
|
+
checkpointReplayAvoidedCostUsd: checkpointPricing.costUsd,
|
|
144
|
+
retrievalAvoidedCostRangeUsd: unknownPricing ? range(stats.potentialAvoidedTokens) : undefined,
|
|
145
|
+
checkpointReplayAvoidedCostRangeUsd: unknownPricing ? range(stats.checkpointReplayAvoided) : undefined,
|
|
146
|
+
categoriesAreAdditive: false,
|
|
120
147
|
isEstimated: true,
|
|
121
|
-
inputPricePerMillionUsd:
|
|
122
|
-
pricingBasis:
|
|
148
|
+
inputPricePerMillionUsd: retrievalPricing.pricing.inputPricePerMillionUsd,
|
|
149
|
+
pricingBasis: retrievalPricing.pricing.pricingBasis,
|
|
150
|
+
pricingSource: MODEL_PRICING_SOURCE,
|
|
151
|
+
pricingEffectiveDate: MODEL_PRICING_EFFECTIVE_DATE,
|
|
123
152
|
},
|
|
124
153
|
};
|
|
125
154
|
}
|
|
@@ -39,6 +39,7 @@ describe('context-telemetry-service', () => {
|
|
|
39
39
|
expect(stats.retrievals).toBe(1);
|
|
40
40
|
expect(stats.potentialAvoidedTokens).toBe(8000);
|
|
41
41
|
expect(stats.repeatedReadsPrevented).toBeGreaterThan(0);
|
|
42
|
+
expect(stats.deduplicatedTokens).toBe(3000);
|
|
42
43
|
expect(stats.isEstimated).toBe(false);
|
|
43
44
|
});
|
|
44
45
|
it('aggregates cache performance from events', async () => {
|
|
@@ -75,6 +76,7 @@ describe('context-telemetry-service', () => {
|
|
|
75
76
|
expect(cost.actual.inputTokens).toBe(510000);
|
|
76
77
|
expect(cost.actual.costUsd).toBe(4.83);
|
|
77
78
|
expect(cost.actual.isEstimated).toBe(false);
|
|
79
|
+
expect(cost.actual.classification).toBe('observed');
|
|
78
80
|
});
|
|
79
81
|
it('deduplicates CSV rows on re-import', async () => {
|
|
80
82
|
const csv = `Date,Model,Input Tokens,Output Tokens,Cost
|
|
@@ -166,4 +168,15 @@ describe('context-telemetry-service', () => {
|
|
|
166
168
|
expect(cost.estimated.inputPricePerMillionUsd).toBe(0.5);
|
|
167
169
|
expect(cost.estimated.estimatedCostAvoidedUsd).toBeGreaterThan(0);
|
|
168
170
|
});
|
|
171
|
+
it('does not combine retrieval and checkpoint counterfactuals', async () => {
|
|
172
|
+
const id = `${taskId}-separate-${Math.random().toString(36).slice(2)}`;
|
|
173
|
+
await recordContextEvent({ taskId: id, toolName: 'rigour_context_scope', cacheStatus: 'miss', candidateTokens: 5000, returnedTokens: 1000 }, testCwd);
|
|
174
|
+
await recordCheckpointMetric({ checkpointId: `${id}-cp`, taskId: id, agentId: 'a1', rawStateTokens: 9000, checkpointTokens: 1000, replayTokensAvoided: 8000 }, testCwd);
|
|
175
|
+
const cost = await getTaskCostStats(id, testCwd);
|
|
176
|
+
expect(cost.estimated.retrievalAvoidedTokens).toBe(4000);
|
|
177
|
+
expect(cost.estimated.checkpointReplayAvoidedTokens).toBe(8000);
|
|
178
|
+
expect(cost.estimated.potentialContextAvoided).toBe(4000);
|
|
179
|
+
expect(cost.estimated.categoriesAreAdditive).toBe(false);
|
|
180
|
+
expect(cost.estimated.retrievalAvoidedCostRangeUsd?.max).toBeGreaterThan(cost.estimated.retrievalAvoidedCostRangeUsd?.min ?? 0);
|
|
181
|
+
});
|
|
169
182
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DependencyGraph } from '../context/dependency-graph.js';
|
|
2
|
+
import type { LessonRecord } from '../storage/lessons.js';
|
|
3
|
+
import type { AgentHistoryEvent, AgentRun } from './agent-history.js';
|
|
4
|
+
export type KnowledgeNodeType = 'repository' | 'file' | 'agent' | 'task' | 'run' | 'advice' | 'pattern' | 'memory' | 'lesson' | 'policy' | 'outcome';
|
|
5
|
+
export interface KnowledgeNode {
|
|
6
|
+
id: string;
|
|
7
|
+
type: KnowledgeNodeType;
|
|
8
|
+
label: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
state?: string;
|
|
11
|
+
repositoryId?: string;
|
|
12
|
+
weight: number;
|
|
13
|
+
evidence?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface KnowledgeEdge {
|
|
16
|
+
id: string;
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
type: 'contains' | 'imports' | 'performed' | 'belongs_to' | 'touched' | 'produced' | 'resulted_in' | 'defined_in' | 'protects' | 'remembered' | 'guided' | 'informed';
|
|
20
|
+
}
|
|
21
|
+
export interface EngineeringKnowledgeGraph {
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
nodes: KnowledgeNode[];
|
|
25
|
+
edges: KnowledgeEdge[];
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
counts: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
interface GraphInput {
|
|
30
|
+
repository: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
dependencyGraph?: DependencyGraph | null;
|
|
35
|
+
events: AgentHistoryEvent[];
|
|
36
|
+
runs: AgentRun[];
|
|
37
|
+
lessons: LessonRecord[];
|
|
38
|
+
patterns?: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
type: string;
|
|
42
|
+
file: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
usageCount?: number;
|
|
45
|
+
}>;
|
|
46
|
+
memories?: Array<{
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
detail?: string;
|
|
50
|
+
source?: string;
|
|
51
|
+
}>;
|
|
52
|
+
maxFiles?: number;
|
|
53
|
+
}
|
|
54
|
+
export declare function buildEngineeringKnowledgeGraph(input: GraphInput): EngineeringKnowledgeGraph;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
function shortPath(value) {
|
|
2
|
+
const parts = value.split('/');
|
|
3
|
+
return parts.length > 3 ? `…/${parts.slice(-3).join('/')}` : value;
|
|
4
|
+
}
|
|
5
|
+
function shortLabel(value, limit = 72) {
|
|
6
|
+
return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
|
|
7
|
+
}
|
|
8
|
+
export function buildEngineeringKnowledgeGraph(input) {
|
|
9
|
+
const nodes = new Map();
|
|
10
|
+
const edges = new Map();
|
|
11
|
+
const addNode = (node) => nodes.set(node.id, node);
|
|
12
|
+
const addEdge = (from, to, type) => {
|
|
13
|
+
const id = `${type}:${from}:${to}`;
|
|
14
|
+
edges.set(id, { id, from, to, type });
|
|
15
|
+
};
|
|
16
|
+
const repoId = `repository:${input.repository.id}`;
|
|
17
|
+
addNode({ id: repoId, type: 'repository', label: input.repository.name, detail: input.repository.id, repositoryId: input.repository.id, weight: 10 });
|
|
18
|
+
const eventFiles = new Set(input.events.flatMap(event => event.files));
|
|
19
|
+
const graph = input.dependencyGraph;
|
|
20
|
+
const degree = new Map();
|
|
21
|
+
for (const edge of graph?.edges ?? []) {
|
|
22
|
+
degree.set(edge.from, (degree.get(edge.from) ?? 0) + 1);
|
|
23
|
+
degree.set(edge.to, (degree.get(edge.to) ?? 0) + 1);
|
|
24
|
+
}
|
|
25
|
+
const maxFiles = input.maxFiles ?? 180;
|
|
26
|
+
const selectedFiles = [...(graph?.nodes ?? [])]
|
|
27
|
+
.sort((a, b) => Number(eventFiles.has(b)) - Number(eventFiles.has(a)) || (degree.get(b) ?? 0) - (degree.get(a) ?? 0))
|
|
28
|
+
.slice(0, maxFiles);
|
|
29
|
+
const selected = new Set(selectedFiles);
|
|
30
|
+
for (const file of selectedFiles) {
|
|
31
|
+
const id = `file:${file}`;
|
|
32
|
+
addNode({ id, type: 'file', label: shortPath(file), detail: file, repositoryId: input.repository.id, weight: Math.min(8, 1 + (degree.get(file) ?? 0)) });
|
|
33
|
+
addEdge(repoId, id, 'contains');
|
|
34
|
+
}
|
|
35
|
+
for (const edge of graph?.edges ?? []) {
|
|
36
|
+
if (selected.has(edge.from) && selected.has(edge.to))
|
|
37
|
+
addEdge(`file:${edge.from}`, `file:${edge.to}`, 'imports');
|
|
38
|
+
}
|
|
39
|
+
for (const run of input.runs.slice(0, 100)) {
|
|
40
|
+
const agentId = `agent:${run.agentId}`;
|
|
41
|
+
const runId = `run:${run.id}`;
|
|
42
|
+
addNode({ id: agentId, type: 'agent', label: run.agentId, weight: 5 });
|
|
43
|
+
addNode({ id: runId, type: 'run', label: run.taskId ?? run.id, detail: `${run.eventCount} events`, state: run.status, weight: 4 });
|
|
44
|
+
addEdge(agentId, runId, 'performed');
|
|
45
|
+
addEdge(runId, repoId, 'belongs_to');
|
|
46
|
+
if (run.taskId) {
|
|
47
|
+
const taskId = `task:${run.taskId}`;
|
|
48
|
+
addNode({ id: taskId, type: 'task', label: run.taskId, weight: 4 });
|
|
49
|
+
addEdge(runId, taskId, 'belongs_to');
|
|
50
|
+
}
|
|
51
|
+
for (const file of run.files)
|
|
52
|
+
if (selected.has(file))
|
|
53
|
+
addEdge(runId, `file:${file}`, 'touched');
|
|
54
|
+
if (run.status !== 'active' && run.status !== 'unknown') {
|
|
55
|
+
const outcomeId = `outcome:${run.id}:${run.status}`;
|
|
56
|
+
addNode({ id: outcomeId, type: 'outcome', label: run.status, state: run.status, weight: 3 });
|
|
57
|
+
addEdge(runId, outcomeId, 'resulted_in');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const lesson of input.lessons.slice(0, 150)) {
|
|
61
|
+
const lessonId = `lesson:${lesson.id}`;
|
|
62
|
+
addNode({
|
|
63
|
+
id: lessonId,
|
|
64
|
+
type: 'lesson',
|
|
65
|
+
label: lesson.subject,
|
|
66
|
+
detail: `${lesson.visibility} · ${lesson.state} · ${Math.round(lesson.confidence * 100)}%`,
|
|
67
|
+
state: lesson.state,
|
|
68
|
+
repositoryId: lesson.repositoryId,
|
|
69
|
+
weight: 4,
|
|
70
|
+
});
|
|
71
|
+
const sourceRepo = lesson.repositoryId === input.repository.id ? repoId : `repository:${lesson.repositoryId}`;
|
|
72
|
+
if (!nodes.has(sourceRepo))
|
|
73
|
+
addNode({ id: sourceRepo, type: 'repository', label: lesson.repositoryName ?? `Repository ${lesson.repositoryId.slice(0, 8)}`, detail: lesson.repositoryId, repositoryId: lesson.repositoryId, weight: 7 });
|
|
74
|
+
addEdge(sourceRepo, lessonId, 'produced');
|
|
75
|
+
}
|
|
76
|
+
for (const pattern of [...(input.patterns ?? [])]
|
|
77
|
+
.sort((a, b) => (b.usageCount ?? 0) - (a.usageCount ?? 0))
|
|
78
|
+
.slice(0, 120)) {
|
|
79
|
+
const patternId = `pattern:${pattern.id}`;
|
|
80
|
+
addNode({
|
|
81
|
+
id: patternId,
|
|
82
|
+
type: 'pattern',
|
|
83
|
+
label: pattern.name,
|
|
84
|
+
detail: `${pattern.type} · ${pattern.description || pattern.file}`,
|
|
85
|
+
repositoryId: input.repository.id,
|
|
86
|
+
weight: Math.min(8, 2 + Math.sqrt((pattern.usageCount ?? 0) + 1)),
|
|
87
|
+
});
|
|
88
|
+
const fileId = `file:${pattern.file}`;
|
|
89
|
+
addEdge(nodes.has(fileId) ? fileId : repoId, patternId, nodes.has(fileId) ? 'defined_in' : 'contains');
|
|
90
|
+
}
|
|
91
|
+
for (const memory of (input.memories ?? []).slice(0, 100)) {
|
|
92
|
+
const memoryId = `memory:${memory.id}`;
|
|
93
|
+
addNode({
|
|
94
|
+
id: memoryId,
|
|
95
|
+
type: 'memory',
|
|
96
|
+
label: memory.label,
|
|
97
|
+
detail: `${memory.source ?? 'local'} · ${memory.detail ?? 'retained memory'}`,
|
|
98
|
+
repositoryId: input.repository.id,
|
|
99
|
+
weight: 3,
|
|
100
|
+
});
|
|
101
|
+
addEdge(repoId, memoryId, 'remembered');
|
|
102
|
+
}
|
|
103
|
+
for (const event of input.events) {
|
|
104
|
+
if (!event.guidance)
|
|
105
|
+
continue;
|
|
106
|
+
const day = event.timestamp.slice(0, 10);
|
|
107
|
+
const runKey = event.sessionId ?? event.taskId ?? event.requestId ?? `${event.agentId}:${day}`;
|
|
108
|
+
const runId = `run:${runKey}`;
|
|
109
|
+
if (!nodes.has(runId))
|
|
110
|
+
continue;
|
|
111
|
+
const adviceId = `advice:${event.requestId ?? event.id}`;
|
|
112
|
+
const telemetry = event.telemetry;
|
|
113
|
+
const avoidedTokens = Math.max(0, (telemetry?.candidateTokens ?? 0) - (telemetry?.returnedTokens ?? 0));
|
|
114
|
+
const knowledgeItems = event.guidance.patternRefs.length + event.guidance.lessonRefs.length + event.guidance.memoryRefs.length;
|
|
115
|
+
addNode({
|
|
116
|
+
id: adviceId,
|
|
117
|
+
type: 'advice',
|
|
118
|
+
label: shortLabel(event.guidance.recommendation),
|
|
119
|
+
detail: `${event.guidance.kind} · ${knowledgeItems} knowledge items · ${avoidedTokens} tokens avoided`,
|
|
120
|
+
state: telemetry?.classification ?? 'measured estimate',
|
|
121
|
+
repositoryId: input.repository.id,
|
|
122
|
+
weight: Math.min(8, 3 + Math.sqrt(knowledgeItems + 1)),
|
|
123
|
+
evidence: {
|
|
124
|
+
recommendation: event.guidance.recommendation,
|
|
125
|
+
candidateTokens: telemetry?.candidateTokens ?? 0,
|
|
126
|
+
returnedTokens: telemetry?.returnedTokens ?? 0,
|
|
127
|
+
avoidedTokens,
|
|
128
|
+
candidateFiles: telemetry?.candidateFiles ?? 0,
|
|
129
|
+
returnedFiles: telemetry?.returnedFiles ?? 0,
|
|
130
|
+
excludedFiles: event.guidance.excludedFileCount,
|
|
131
|
+
cacheStatus: telemetry?.cacheStatus ?? 'none',
|
|
132
|
+
classification: telemetry?.classification ?? 'measured estimate',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
addEdge(runId, adviceId, 'guided');
|
|
136
|
+
for (const reference of event.guidance.patternRefs) {
|
|
137
|
+
const sourceId = `pattern:${reference.id}`;
|
|
138
|
+
if (nodes.has(sourceId))
|
|
139
|
+
addEdge(sourceId, adviceId, 'informed');
|
|
140
|
+
}
|
|
141
|
+
for (const reference of event.guidance.lessonRefs) {
|
|
142
|
+
const sourceId = `lesson:${reference.id}`;
|
|
143
|
+
if (nodes.has(sourceId))
|
|
144
|
+
addEdge(sourceId, adviceId, 'informed');
|
|
145
|
+
}
|
|
146
|
+
for (const reference of event.guidance.memoryRefs) {
|
|
147
|
+
const sourceId = `memory:${reference.id}`;
|
|
148
|
+
if (nodes.has(sourceId))
|
|
149
|
+
addEdge(sourceId, adviceId, 'informed');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const prevented = input.events.filter(event => event.outcome === 'rejected');
|
|
153
|
+
if (prevented.length > 0) {
|
|
154
|
+
const policyId = 'policy:rigour-firewall';
|
|
155
|
+
const outcomeId = 'outcome:prevented-actions';
|
|
156
|
+
addNode({ id: policyId, type: 'policy', label: 'Rigour transaction firewall', detail: 'Deterministic mediation boundary', state: 'active', weight: 8 });
|
|
157
|
+
addNode({ id: outcomeId, type: 'outcome', label: `${prevented.length} prevented actions`, detail: 'Rejected before unsafe execution', state: 'verified', weight: Math.min(10, 4 + prevented.length) });
|
|
158
|
+
addEdge(policyId, repoId, 'protects');
|
|
159
|
+
addEdge(policyId, outcomeId, 'produced');
|
|
160
|
+
}
|
|
161
|
+
const counts = [...nodes.values()].reduce((result, node) => {
|
|
162
|
+
result[node.type] = (result[node.type] ?? 0) + 1;
|
|
163
|
+
return result;
|
|
164
|
+
}, {});
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
generatedAt: new Date().toISOString(),
|
|
168
|
+
nodes: [...nodes.values()],
|
|
169
|
+
edges: [...edges.values()],
|
|
170
|
+
truncated: (graph?.nodes.length ?? 0) > selectedFiles.length
|
|
171
|
+
|| input.runs.length > 100
|
|
172
|
+
|| input.lessons.length > 150
|
|
173
|
+
|| (input.patterns?.length ?? 0) > 120
|
|
174
|
+
|| (input.memories?.length ?? 0) > 100,
|
|
175
|
+
counts,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildEngineeringKnowledgeGraph } from './engineering-knowledge-graph.js';
|
|
3
|
+
describe('engineering knowledge graph', () => {
|
|
4
|
+
it('connects code, agent outcomes and cross-repository lessons', () => {
|
|
5
|
+
const graph = buildEngineeringKnowledgeGraph({
|
|
6
|
+
repository: { id: 'repo-a', name: 'payments' },
|
|
7
|
+
dependencyGraph: { version: 1, generatedAt: '', nodes: ['src/a.ts', 'src/b.ts'], edges: [{ from: 'src/a.ts', to: 'src/b.ts' }] },
|
|
8
|
+
events: [],
|
|
9
|
+
runs: [{ id: 'run-1', agentId: 'agent-1', startedAt: '', endedAt: '', status: 'success', eventCount: 2, files: ['src/a.ts'], tools: ['check'] }],
|
|
10
|
+
lessons: [{ id: 'lesson-1', repositoryId: 'repo-b', repositoryName: 'platform', visibility: 'team', state: 'promoted', kind: 'interaction', subject: 'Use bounded retries', evidence: {}, confidence: 0.9, source: 'mcp', createdAt: 1, updatedAt: 1 }],
|
|
11
|
+
patterns: [{ id: 'pattern-1', name: 'withTransaction', type: 'function', file: 'src/a.ts', usageCount: 8 }],
|
|
12
|
+
memories: [{ id: 'memory-1', label: 'migration-owner', source: 'project' }],
|
|
13
|
+
});
|
|
14
|
+
expect(graph.counts.repository).toBe(2);
|
|
15
|
+
expect(graph.nodes.some(node => node.type === 'outcome' && node.label === 'success')).toBe(true);
|
|
16
|
+
expect(graph.edges.some(edge => edge.type === 'imports')).toBe(true);
|
|
17
|
+
expect(graph.edges.some(edge => edge.type === 'produced')).toBe(true);
|
|
18
|
+
expect(graph.nodes.some(node => node.type === 'pattern')).toBe(true);
|
|
19
|
+
expect(graph.nodes.some(node => node.type === 'memory')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('represents prevented agent actions as policy evidence', () => {
|
|
22
|
+
const graph = buildEngineeringKnowledgeGraph({
|
|
23
|
+
repository: { id: 'repo-a', name: 'payments' },
|
|
24
|
+
events: [{ id: 'event-1', timestamp: '', type: 'tool_denied', agentId: 'agent-1', outcome: 'rejected', files: [], summary: 'write denied' }],
|
|
25
|
+
runs: [],
|
|
26
|
+
lessons: [],
|
|
27
|
+
});
|
|
28
|
+
expect(graph.nodes).toContainEqual(expect.objectContaining({ type: 'policy', state: 'active' }));
|
|
29
|
+
expect(graph.nodes).toContainEqual(expect.objectContaining({ type: 'outcome', state: 'verified' }));
|
|
30
|
+
expect(graph.edges.some(edge => edge.type === 'protects')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('connects issued advice to its run and supporting knowledge', () => {
|
|
33
|
+
const event = {
|
|
34
|
+
id: 'event-1', requestId: 'run-1', timestamp: '2026-01-01T00:00:00Z', type: 'tool_response',
|
|
35
|
+
agentId: 'agent-1', outcome: 'success', files: ['src/a.ts'], summary: 'scope returned',
|
|
36
|
+
guidance: {
|
|
37
|
+
kind: 'context-scope',
|
|
38
|
+
recommendation: 'Read only src/a.ts.',
|
|
39
|
+
patternRefs: [{ id: 'pattern-1', label: 'withTransaction' }],
|
|
40
|
+
lessonRefs: [], memoryRefs: [], selectedFiles: ['src/a.ts'], excludedFileCount: 9, conflicts: 0,
|
|
41
|
+
},
|
|
42
|
+
telemetry: {
|
|
43
|
+
candidateTokens: 1200, returnedTokens: 200, candidateFiles: 10, returnedFiles: 1,
|
|
44
|
+
cacheStatus: 'exact-hit', classification: 'measured estimate',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const graph = buildEngineeringKnowledgeGraph({
|
|
48
|
+
repository: { id: 'repo-a', name: 'payments' },
|
|
49
|
+
dependencyGraph: { version: 1, generatedAt: '', nodes: ['src/a.ts'], edges: [] },
|
|
50
|
+
events: [event],
|
|
51
|
+
runs: [{ id: 'run-1', agentId: 'agent-1', startedAt: event.timestamp, endedAt: event.timestamp, status: 'success', eventCount: 1, files: ['src/a.ts'], tools: ['rigour_context_scope'] }],
|
|
52
|
+
lessons: [],
|
|
53
|
+
patterns: [{ id: 'pattern-1', name: 'withTransaction', type: 'function', file: 'src/a.ts' }],
|
|
54
|
+
});
|
|
55
|
+
expect(graph.nodes).toContainEqual(expect.objectContaining({ type: 'advice', evidence: expect.objectContaining({ avoidedTokens: 1000 }) }));
|
|
56
|
+
expect(graph.edges).toContainEqual(expect.objectContaining({ from: 'run:run-1', to: 'advice:run-1', type: 'guided' }));
|
|
57
|
+
expect(graph.edges).toContainEqual(expect.objectContaining({ from: 'pattern:pattern-1', to: 'advice:run-1', type: 'informed' }));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Observed Cursor costs always take precedence; these rates estimate when absent.
|
|
4
4
|
*/
|
|
5
5
|
import type { ModelUsage } from '../storage/context-telemetry.js';
|
|
6
|
+
export declare const MODEL_PRICING_SOURCE = "Rigour versioned model registry";
|
|
7
|
+
export declare const MODEL_PRICING_EFFECTIVE_DATE = "2026-08-01";
|
|
6
8
|
export interface ModelPricingTier {
|
|
7
9
|
label: string;
|
|
8
10
|
inputPerMillionUsd: number;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Model-aware input pricing for context cost telemetry.
|
|
3
3
|
* Observed Cursor costs always take precedence; these rates estimate when absent.
|
|
4
4
|
*/
|
|
5
|
+
export const MODEL_PRICING_SOURCE = 'Rigour versioned model registry';
|
|
6
|
+
export const MODEL_PRICING_EFFECTIVE_DATE = '2026-08-01';
|
|
5
7
|
/** Per-million-token USD rates. Input rates drive avoided-context estimates. */
|
|
6
8
|
export const MODEL_PRICING = {
|
|
7
9
|
'composer-2.5-standard': {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getTeamModeStatus } from '../storage/team-store.js';
|
|
2
|
+
export interface SystemHealth {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
index: {
|
|
6
|
+
status: string;
|
|
7
|
+
patterns: number;
|
|
8
|
+
files: number;
|
|
9
|
+
updatedAt?: string;
|
|
10
|
+
};
|
|
11
|
+
graph: {
|
|
12
|
+
status: string;
|
|
13
|
+
nodes: number;
|
|
14
|
+
edges: number;
|
|
15
|
+
updatedAt?: string;
|
|
16
|
+
};
|
|
17
|
+
semantic: {
|
|
18
|
+
status: string;
|
|
19
|
+
provider: 'local' | 'pgvector';
|
|
20
|
+
message?: string;
|
|
21
|
+
};
|
|
22
|
+
caches: {
|
|
23
|
+
status: string;
|
|
24
|
+
layers: Record<'static' | 'component' | 'semantic' | 'checkpoint', number>;
|
|
25
|
+
};
|
|
26
|
+
learning: {
|
|
27
|
+
status: string;
|
|
28
|
+
total: number;
|
|
29
|
+
observations: number;
|
|
30
|
+
states: Record<string, number>;
|
|
31
|
+
};
|
|
32
|
+
storage: Awaited<ReturnType<typeof getTeamModeStatus>>;
|
|
33
|
+
}
|
|
34
|
+
export declare function getSystemHealth(cwd: string): Promise<SystemHealth>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getAutomaticIndexStatus } from '../context/automatic-index.js';
|
|
4
|
+
import { getDefaultIndexPath, loadPatternIndex } from '../pattern-index/indexer.js';
|
|
5
|
+
import { listContextCacheRecords } from '../storage/context-telemetry.js';
|
|
6
|
+
import { countInteractionEvidence, listLessons } from '../storage/lessons.js';
|
|
7
|
+
import { getTeamModeStatus } from '../storage/team-store.js';
|
|
8
|
+
export async function getSystemHealth(cwd) {
|
|
9
|
+
const [automatic, index, cacheRecords, lessons, observations, storage, graph] = await Promise.all([
|
|
10
|
+
getAutomaticIndexStatus(cwd),
|
|
11
|
+
loadPatternIndex(getDefaultIndexPath(cwd)).catch(() => null),
|
|
12
|
+
listContextCacheRecords({ limit: 10_000 }, cwd).catch(() => []),
|
|
13
|
+
listLessons(cwd, 10_000).catch(() => []),
|
|
14
|
+
countInteractionEvidence(cwd).catch(() => 0),
|
|
15
|
+
getTeamModeStatus(),
|
|
16
|
+
fs.readJson(path.join(cwd, '.rigour', 'dependency-graph.json')).catch(() => null),
|
|
17
|
+
]);
|
|
18
|
+
const layers = { static: 0, component: 0, semantic: 0, checkpoint: 0 };
|
|
19
|
+
for (const record of cacheRecords)
|
|
20
|
+
layers[record.cacheType]++;
|
|
21
|
+
const states = {};
|
|
22
|
+
for (const lesson of lessons)
|
|
23
|
+
states[lesson.state] = (states[lesson.state] ?? 0) + 1;
|
|
24
|
+
const graphNodes = Array.isArray(graph?.nodes) ? graph.nodes.length : 0;
|
|
25
|
+
const graphEdges = Array.isArray(graph?.edges) ? graph.edges.length : 0;
|
|
26
|
+
return {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
generatedAt: new Date().toISOString(),
|
|
29
|
+
index: {
|
|
30
|
+
status: index ? automatic.structural : 'missing',
|
|
31
|
+
patterns: index?.stats?.totalPatterns ?? index?.patterns?.length ?? 0,
|
|
32
|
+
files: index?.stats?.totalFiles ?? 0,
|
|
33
|
+
updatedAt: index?.lastUpdated,
|
|
34
|
+
},
|
|
35
|
+
graph: { status: graph ? automatic.graph : 'missing', nodes: graphNodes, edges: graphEdges, updatedAt: graph?.generatedAt },
|
|
36
|
+
semantic: storage.semantic
|
|
37
|
+
? {
|
|
38
|
+
status: storage.semantic.status,
|
|
39
|
+
provider: 'pgvector',
|
|
40
|
+
message: storage.semantic.status === 'ready'
|
|
41
|
+
? `pgvector · ${storage.semantic.indexedLessons ?? 0} lessons indexed${storage.semantic.missingLessons ? ` · ${storage.semantic.missingLessons} pending` : ''}`
|
|
42
|
+
: storage.semantic.message,
|
|
43
|
+
}
|
|
44
|
+
: {
|
|
45
|
+
status: automatic.semantic,
|
|
46
|
+
provider: 'local',
|
|
47
|
+
message: automatic.message ?? (automatic.semantic === 'ready' ? 'Local pattern embeddings ready.' : undefined),
|
|
48
|
+
},
|
|
49
|
+
caches: { status: Object.values(layers).every(count => count > 0) ? 'ready' : 'warming', layers },
|
|
50
|
+
learning: { status: observations || lessons.length ? 'active' : 'idle', total: lessons.length, observations, states },
|
|
51
|
+
storage,
|
|
52
|
+
};
|
|
53
|
+
}
|