memorix 1.1.13 → 1.2.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/CHANGELOG.md +33 -1
- package/README.md +6 -3
- package/README.zh-CN.md +6 -3
- package/dist/cli/index.js +36639 -31279
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -30
- package/dist/index.js +6636 -1778
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +3775 -302
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +33 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +6634 -1776
- package/dist/sdk.js.map +1 -1
- package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
- package/docs/1.2.0-CODE-STATE.md +61 -0
- package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
- package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
- package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
- package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
- package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
- package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
- package/docs/1.2.0-PRODUCT-STORY.md +234 -0
- package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
- package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
- package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
- package/docs/API_REFERENCE.md +27 -6
- package/docs/CONFIGURATION.md +21 -3
- package/docs/DEVELOPMENT.md +4 -0
- package/docs/README.md +17 -2
- package/docs/SETUP.md +7 -1
- package/docs/dev-log/progress.txt +120 -40
- package/docs/knowledge/workflows/memorix-release.md +57 -0
- package/llms-full.txt +16 -2
- package/llms.txt +9 -4
- package/package.json +3 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/commands/codegraph.ts +111 -9
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/doctor.ts +73 -4
- package/src/cli/commands/knowledge.ts +322 -0
- package/src/cli/commands/serve-http.ts +26 -42
- package/src/cli/commands/setup.ts +9 -3
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/App.tsx +1 -1
- package/src/cli/tui/Panels.tsx +8 -8
- package/src/cli/tui/theme.ts +1 -1
- package/src/cli/tui/views/GraphView.tsx +8 -7
- package/src/cli/tui/views/KnowledgeView.tsx +9 -9
- package/src/codegraph/auto-context.ts +169 -19
- package/src/codegraph/code-state.ts +95 -0
- package/src/codegraph/context-pack.ts +82 -1
- package/src/codegraph/current-facts.ts +19 -1
- package/src/codegraph/external-provider.ts +581 -0
- package/src/codegraph/lite-provider.ts +64 -19
- package/src/codegraph/project-context.ts +9 -1
- package/src/codegraph/store.ts +154 -6
- package/src/codegraph/task-lens.ts +49 -5
- package/src/codegraph/types.ts +117 -0
- package/src/config/resolved-config.ts +28 -0
- package/src/config/toml-loader.ts +3 -0
- package/src/config/yaml-loader.ts +6 -0
- package/src/dashboard/server.ts +30 -47
- package/src/evaluation/workset-evaluation.ts +120 -0
- package/src/hooks/handler.ts +48 -6
- package/src/knowledge/claim-store.ts +267 -0
- package/src/knowledge/claims.ts +587 -0
- package/src/knowledge/markdown.ts +129 -0
- package/src/knowledge/types.ts +158 -0
- package/src/knowledge/wiki.ts +524 -0
- package/src/knowledge/workflow-store.ts +168 -0
- package/src/knowledge/workflow-types.ts +95 -0
- package/src/knowledge/workflows.ts +774 -0
- package/src/knowledge/workset.ts +515 -0
- package/src/knowledge/workspace-store.ts +220 -0
- package/src/knowledge/workspace-types.ts +106 -0
- package/src/knowledge/workspace.ts +220 -0
- package/src/memory/auto-relations.ts +21 -0
- package/src/memory/graph-scope.ts +46 -0
- package/src/memory/observations.ts +19 -0
- package/src/orchestrate/verify-gate.ts +33 -10
- package/src/runtime/control-plane-maintenance.ts +5 -0
- package/src/runtime/isolated-maintenance.ts +5 -0
- package/src/runtime/lifecycle-status.ts +102 -0
- package/src/runtime/lifecycle.ts +107 -0
- package/src/runtime/maintenance-jobs.ts +5 -0
- package/src/runtime/project-maintenance.ts +190 -0
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +424 -22
- package/src/store/file-lock.ts +24 -4
- package/src/store/sqlite-db.ts +307 -0
- package/src/wiki/generator.ts +4 -2
- package/src/wiki/knowledge-graph.ts +7 -4
- package/src/wiki/types.ts +16 -4
|
@@ -55,6 +55,9 @@ export interface ResolvedMemorixConfig {
|
|
|
55
55
|
codegraph: {
|
|
56
56
|
excludePatterns?: string[];
|
|
57
57
|
maxFileBytes?: number;
|
|
58
|
+
externalContext: 'auto' | 'off';
|
|
59
|
+
externalCommand?: string;
|
|
60
|
+
externalTimeoutMs?: number;
|
|
58
61
|
};
|
|
59
62
|
server: {
|
|
60
63
|
transport?: 'stdio' | 'http';
|
|
@@ -156,6 +159,22 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
156
159
|
codegraph: {
|
|
157
160
|
excludePatterns: firstArray(toml.codegraph?.exclude_patterns, yaml.codegraph?.excludePatterns),
|
|
158
161
|
maxFileBytes: firstNumber(toml.codegraph?.max_file_bytes, yaml.codegraph?.maxFileBytes),
|
|
162
|
+
externalContext: first(
|
|
163
|
+
normalizeExternalContext(process.env.MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT),
|
|
164
|
+
toml.codegraph?.external_context,
|
|
165
|
+
yaml.codegraph?.externalContext,
|
|
166
|
+
'auto',
|
|
167
|
+
)!,
|
|
168
|
+
externalCommand: first(
|
|
169
|
+
process.env.MEMORIX_CODEGRAPH_EXTERNAL_COMMAND,
|
|
170
|
+
toml.codegraph?.external_command,
|
|
171
|
+
yaml.codegraph?.externalCommand,
|
|
172
|
+
),
|
|
173
|
+
externalTimeoutMs: firstNumber(
|
|
174
|
+
parseNumber(process.env.MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS),
|
|
175
|
+
toml.codegraph?.external_timeout_ms,
|
|
176
|
+
yaml.codegraph?.externalTimeoutMs,
|
|
177
|
+
),
|
|
159
178
|
},
|
|
160
179
|
server: {
|
|
161
180
|
transport: first(toml.server?.transport, yaml.server?.transport),
|
|
@@ -254,6 +273,9 @@ function getEnvSourceNames(): string[] {
|
|
|
254
273
|
'MEMORIX_EMBEDDING_BASE_URL',
|
|
255
274
|
'MEMORIX_EMBEDDING_MODEL',
|
|
256
275
|
'MEMORIX_EMBEDDING_DIMENSIONS',
|
|
276
|
+
'MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT',
|
|
277
|
+
'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
|
|
278
|
+
'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
|
|
257
279
|
'OPENROUTER_API_KEY',
|
|
258
280
|
].filter((name) => process.env[name]);
|
|
259
281
|
}
|
|
@@ -267,3 +289,9 @@ function isOpenRouterUrl(value: string | undefined): boolean {
|
|
|
267
289
|
return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
|
|
268
290
|
}
|
|
269
291
|
}
|
|
292
|
+
|
|
293
|
+
function normalizeExternalContext(value: string | undefined): 'auto' | 'off' | undefined {
|
|
294
|
+
const normalized = value?.trim().toLowerCase();
|
|
295
|
+
if (normalized === 'auto' || normalized === 'off') return normalized;
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
@@ -43,6 +43,9 @@ export interface MemorixTomlConfig {
|
|
|
43
43
|
codegraph?: {
|
|
44
44
|
exclude_patterns?: string[];
|
|
45
45
|
max_file_bytes?: number;
|
|
46
|
+
external_context?: 'auto' | 'off';
|
|
47
|
+
external_command?: string;
|
|
48
|
+
external_timeout_ms?: number;
|
|
46
49
|
};
|
|
47
50
|
server?: {
|
|
48
51
|
transport?: 'stdio' | 'http';
|
|
@@ -70,6 +70,12 @@ export interface MemorixYamlConfig {
|
|
|
70
70
|
excludePatterns?: string[];
|
|
71
71
|
/** Maximum source file size to parse into CodeGraph Memory */
|
|
72
72
|
maxFileBytes?: number;
|
|
73
|
+
/** Use a healthy pre-existing local CodeGraph index when present (default: auto). */
|
|
74
|
+
externalContext?: 'auto' | 'off';
|
|
75
|
+
/** Optional CodeGraph executable path when it is not available on PATH. */
|
|
76
|
+
externalCommand?: string;
|
|
77
|
+
/** Bound for one local semantic context request. */
|
|
78
|
+
externalTimeoutMs?: number;
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
/** Behavior settings */
|
package/src/dashboard/server.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { loadDotenv } from '../config/dotenv-loader.js';
|
|
|
22
22
|
import { resetDotenv } from '../config/dotenv-loader.js';
|
|
23
23
|
import { initProjectRoot } from '../config/yaml-loader.js';
|
|
24
24
|
import { clearProjectRoot } from '../config/yaml-loader.js';
|
|
25
|
+
import { scopeKnowledgeGraphToProject } from '../memory/graph-scope.js';
|
|
25
26
|
|
|
26
27
|
// MIME types for static file serving
|
|
27
28
|
const MIME_TYPES: Record<string, string> = {
|
|
@@ -92,22 +93,15 @@ export function prepareDashboardConfig(projectRoot: string | null): void {
|
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Compute project-scoped graph counts from observations.
|
|
95
|
-
*
|
|
96
|
+
* Active observation entities and their explicit cross-references are counted.
|
|
96
97
|
*/
|
|
97
98
|
function computeProjectGraphCounts(
|
|
98
99
|
allEntities: Array<{ name: string }>,
|
|
99
100
|
allRelations: Array<{ from: string; to: string }>,
|
|
100
|
-
projectObs: Array<{ entityName?: string; status?: string }>,
|
|
101
|
+
projectObs: Array<{ entityName?: string; relatedEntities?: string[]; status?: string }>,
|
|
101
102
|
): { entities: number; relations: number; entityNames: Set<string> } {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
.filter(o => (o.status ?? 'active') === 'active' && o.entityName)
|
|
105
|
-
.map(o => o.entityName!),
|
|
106
|
-
);
|
|
107
|
-
const entities = allEntities.filter(e => entityNames.has(e.name));
|
|
108
|
-
const entityNameSet = new Set(entities.map(e => e.name));
|
|
109
|
-
const relations = allRelations.filter(r => entityNameSet.has(r.from) && entityNameSet.has(r.to));
|
|
110
|
-
return { entities: entities.length, relations: relations.length, entityNames };
|
|
103
|
+
const scoped = scopeKnowledgeGraphToProject({ entities: allEntities, relations: allRelations }, projectObs);
|
|
104
|
+
return { entities: scoped.entities.length, relations: scoped.relations.length, entityNames: scoped.entityNames };
|
|
111
105
|
}
|
|
112
106
|
|
|
113
107
|
/**
|
|
@@ -210,17 +204,9 @@ async function handleApi(
|
|
|
210
204
|
await initGraphStore(effectiveDataDir);
|
|
211
205
|
const gStore = getGraphStore();
|
|
212
206
|
const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
graphObs
|
|
217
|
-
.filter(o => o.entityName)
|
|
218
|
-
.map(o => o.entityName!),
|
|
219
|
-
);
|
|
220
|
-
const entities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
|
|
221
|
-
const entityNameSet = new Set(entities.map((e: any) => e.name));
|
|
222
|
-
const relations = graph.relations.filter((r: any) => entityNameSet.has(r.from) && entityNameSet.has(r.to));
|
|
223
|
-
sendJson(res, { entities, relations });
|
|
207
|
+
const graphObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
|
|
208
|
+
const scoped = scopeKnowledgeGraphToProject(graph, graphObs);
|
|
209
|
+
sendJson(res, { entities: scoped.entities, relations: scoped.relations });
|
|
224
210
|
break;
|
|
225
211
|
}
|
|
226
212
|
|
|
@@ -240,11 +226,11 @@ async function handleApi(
|
|
|
240
226
|
case '/stats': {
|
|
241
227
|
await initGraphStore(effectiveDataDir);
|
|
242
228
|
const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
243
|
-
const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; status?: string }>;
|
|
229
|
+
const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }) as Array<{ type?: string; id?: number; createdAt?: string; title?: string; entityName?: string; relatedEntities?: string[]; status?: string }>;
|
|
244
230
|
const nextId = await getObservationStore().loadIdCounter();
|
|
245
231
|
|
|
246
232
|
// Project-scoped graph counts (must match /api/graph and /api/export)
|
|
247
|
-
const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations
|
|
233
|
+
const projectGraphCounts = computeProjectGraphCounts(graph.entities, graph.relations, observations);
|
|
248
234
|
|
|
249
235
|
// Type counts (exclude probe -- operational heartbeats, not durable knowledge)
|
|
250
236
|
const typeCounts: Record<string, number> = {};
|
|
@@ -322,9 +308,15 @@ async function handleApi(
|
|
|
322
308
|
};
|
|
323
309
|
|
|
324
310
|
let maintenance = { total: 0, pending: 0, running: 0, retrying: 0, completed: 0, failed: 0 };
|
|
311
|
+
let lifecycle: unknown;
|
|
325
312
|
try {
|
|
326
313
|
const { MaintenanceJobStore } = await import('../runtime/maintenance-jobs.js');
|
|
327
314
|
maintenance = new MaintenanceJobStore(effectiveDataDir).summary(effectiveProjectId);
|
|
315
|
+
const { collectLifecycleDiagnostics } = await import('../runtime/lifecycle-status.js');
|
|
316
|
+
lifecycle = await collectLifecycleDiagnostics({
|
|
317
|
+
dataDir: effectiveDataDir,
|
|
318
|
+
projectId: effectiveProjectId,
|
|
319
|
+
});
|
|
328
320
|
} catch { /* optional maintenance diagnostics */ }
|
|
329
321
|
|
|
330
322
|
sendJson(res, {
|
|
@@ -338,6 +330,7 @@ async function handleApi(
|
|
|
338
330
|
embedding: embeddingStatus,
|
|
339
331
|
storage: storageInfo,
|
|
340
332
|
maintenance,
|
|
333
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
341
334
|
gitSummary: {
|
|
342
335
|
total: gitMemories.length,
|
|
343
336
|
recentWeek: recentGitCount,
|
|
@@ -349,7 +342,10 @@ async function handleApi(
|
|
|
349
342
|
}
|
|
350
343
|
|
|
351
344
|
case '/maintenance': {
|
|
352
|
-
const { MaintenanceJobStore } = await
|
|
345
|
+
const [{ MaintenanceJobStore }, { collectLifecycleDiagnostics }] = await Promise.all([
|
|
346
|
+
import('../runtime/maintenance-jobs.js'),
|
|
347
|
+
import('../runtime/lifecycle-status.js'),
|
|
348
|
+
]);
|
|
353
349
|
const jobs = new MaintenanceJobStore(effectiveDataDir).list({
|
|
354
350
|
projectId: effectiveProjectId,
|
|
355
351
|
limit: 50,
|
|
@@ -357,6 +353,10 @@ async function handleApi(
|
|
|
357
353
|
sendJson(res, {
|
|
358
354
|
summary: new MaintenanceJobStore(effectiveDataDir).summary(effectiveProjectId),
|
|
359
355
|
jobs,
|
|
356
|
+
lifecycle: await collectLifecycleDiagnostics({
|
|
357
|
+
dataDir: effectiveDataDir,
|
|
358
|
+
projectId: effectiveProjectId,
|
|
359
|
+
}),
|
|
360
360
|
});
|
|
361
361
|
break;
|
|
362
362
|
}
|
|
@@ -447,24 +447,15 @@ async function handleApi(
|
|
|
447
447
|
const allObs = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
|
|
448
448
|
const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
|
|
449
449
|
|
|
450
|
-
// Project-scope graph store (same logic as /api/graph)
|
|
451
450
|
const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
452
|
-
const
|
|
453
|
-
const projectEntityNames = new Set(
|
|
454
|
-
graphObs
|
|
455
|
-
.filter(o => o.entityName)
|
|
456
|
-
.map(o => o.entityName!),
|
|
457
|
-
);
|
|
458
|
-
const scopedEntities = fullGraph.entities.filter((e: any) => projectEntityNames.has(e.name));
|
|
459
|
-
const scopedEntityNameSet = new Set(scopedEntities.map((e: any) => e.name));
|
|
460
|
-
const scopedRelations = fullGraph.relations.filter((r: any) => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
|
|
451
|
+
const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
|
|
461
452
|
|
|
462
453
|
const graph = generateKnowledgeGraph({
|
|
463
454
|
projectId: effectiveProjectId,
|
|
464
455
|
observations: allObs,
|
|
465
456
|
miniSkills: skills,
|
|
466
|
-
graphEntities:
|
|
467
|
-
graphRelations:
|
|
457
|
+
graphEntities: scoped.entities,
|
|
458
|
+
graphRelations: scoped.relations,
|
|
468
459
|
});
|
|
469
460
|
|
|
470
461
|
sendJson(res, graph);
|
|
@@ -733,19 +724,11 @@ async function handleApi(
|
|
|
733
724
|
const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
734
725
|
const observations = await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' });
|
|
735
726
|
const nextId = await getObservationStore().loadIdCounter();
|
|
736
|
-
|
|
737
|
-
const exportEntityNames = new Set(
|
|
738
|
-
observations
|
|
739
|
-
.filter(o => (o.status ?? 'active') === 'active' && o.entityName)
|
|
740
|
-
.map(o => o.entityName!),
|
|
741
|
-
);
|
|
742
|
-
const exportEntities = fullGraph.entities.filter((e: any) => exportEntityNames.has(e.name));
|
|
743
|
-
const exportEntitySet = new Set(exportEntities.map((e: any) => e.name));
|
|
744
|
-
const exportRelations = fullGraph.relations.filter((r: any) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
|
|
727
|
+
const scoped = scopeKnowledgeGraphToProject(fullGraph, observations);
|
|
745
728
|
const exportData = {
|
|
746
729
|
project: { id: effectiveProjectId, name: effectiveProjectName },
|
|
747
730
|
exportedAt: new Date().toISOString(),
|
|
748
|
-
graph: { entities:
|
|
731
|
+
graph: { entities: scoped.entities, relations: scoped.relations },
|
|
749
732
|
observations,
|
|
750
733
|
nextId,
|
|
751
734
|
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { countTextTokens } from '../compact/token-budget.js';
|
|
2
|
+
|
|
3
|
+
export const WORKSET_VARIANTS = [
|
|
4
|
+
'memory-only',
|
|
5
|
+
'current-context',
|
|
6
|
+
'candidate-workset',
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
export type WorksetVariant = typeof WORKSET_VARIANTS[number];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A normalized result shape for evaluating a task context. The production
|
|
13
|
+
* Workset builder will populate this shape in Phase 5; Phase 0 uses it to
|
|
14
|
+
* establish deterministic baseline fixtures without an LLM dependency.
|
|
15
|
+
*/
|
|
16
|
+
export interface EvaluatedWorkset {
|
|
17
|
+
variant: WorksetVariant;
|
|
18
|
+
prompt: string;
|
|
19
|
+
startHere: string[];
|
|
20
|
+
evidenceIds: string[];
|
|
21
|
+
cautions: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorksetExpectation {
|
|
25
|
+
requiredStartHere?: string[];
|
|
26
|
+
requiredEvidenceIds?: string[];
|
|
27
|
+
requiredCautions?: string[];
|
|
28
|
+
maxTokens: number;
|
|
29
|
+
maxTokensByVariant?: Partial<Record<WorksetVariant, number>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorksetEvaluationFixture {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
task: string;
|
|
36
|
+
expectation: WorksetExpectation;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorksetEvaluationResult {
|
|
40
|
+
fixtureId: string;
|
|
41
|
+
variant: WorksetVariant;
|
|
42
|
+
tokenCount: number;
|
|
43
|
+
tokenBudget: number;
|
|
44
|
+
withinBudget: boolean;
|
|
45
|
+
missingStartHere: string[];
|
|
46
|
+
missingEvidenceIds: string[];
|
|
47
|
+
missingCautions: string[];
|
|
48
|
+
passed: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorksetComparison {
|
|
52
|
+
fixtureId: string;
|
|
53
|
+
results: WorksetEvaluationResult[];
|
|
54
|
+
byVariant: Partial<Record<WorksetVariant, WorksetEvaluationResult>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unique(values: string[]): string[] {
|
|
58
|
+
return [...new Set(values)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function missing(required: string[] | undefined, actual: string[]): string[] {
|
|
62
|
+
const available = new Set(actual);
|
|
63
|
+
return unique(required ?? []).filter(value => !available.has(value));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tokenBudgetFor(
|
|
67
|
+
expectation: WorksetExpectation,
|
|
68
|
+
variant: WorksetVariant,
|
|
69
|
+
): number {
|
|
70
|
+
return expectation.maxTokensByVariant?.[variant] ?? expectation.maxTokens;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Evaluate one context against source-backed fixture requirements. This is
|
|
75
|
+
* deliberately mechanical: it reports evidence, safety, and token gaps
|
|
76
|
+
* instead of asking a model to grade another model's answer.
|
|
77
|
+
*/
|
|
78
|
+
export function evaluateWorkset(
|
|
79
|
+
fixture: WorksetEvaluationFixture,
|
|
80
|
+
workset: EvaluatedWorkset,
|
|
81
|
+
): WorksetEvaluationResult {
|
|
82
|
+
const tokenCount = countTextTokens(workset.prompt);
|
|
83
|
+
const missingStartHere = missing(fixture.expectation.requiredStartHere, workset.startHere);
|
|
84
|
+
const missingEvidenceIds = missing(fixture.expectation.requiredEvidenceIds, workset.evidenceIds);
|
|
85
|
+
const missingCautions = missing(fixture.expectation.requiredCautions, workset.cautions);
|
|
86
|
+
const tokenBudget = tokenBudgetFor(fixture.expectation, workset.variant);
|
|
87
|
+
const withinBudget = tokenCount <= tokenBudget;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
fixtureId: fixture.id,
|
|
91
|
+
variant: workset.variant,
|
|
92
|
+
tokenCount,
|
|
93
|
+
tokenBudget,
|
|
94
|
+
withinBudget,
|
|
95
|
+
missingStartHere,
|
|
96
|
+
missingEvidenceIds,
|
|
97
|
+
missingCautions,
|
|
98
|
+
passed: withinBudget
|
|
99
|
+
&& missingStartHere.length === 0
|
|
100
|
+
&& missingEvidenceIds.length === 0
|
|
101
|
+
&& missingCautions.length === 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Compare memory-only, current-context, and candidate Worksets against the
|
|
107
|
+
* same task requirements. Missing variants are intentionally allowed so a
|
|
108
|
+
* new retrieval implementation can be introduced incrementally.
|
|
109
|
+
*/
|
|
110
|
+
export function compareWorksets(
|
|
111
|
+
fixture: WorksetEvaluationFixture,
|
|
112
|
+
worksets: EvaluatedWorkset[],
|
|
113
|
+
): WorksetComparison {
|
|
114
|
+
const results = worksets.map(workset => evaluateWorkset(fixture, workset));
|
|
115
|
+
const byVariant: Partial<Record<WorksetVariant, WorksetEvaluationResult>> = {};
|
|
116
|
+
for (const result of results) {
|
|
117
|
+
byVariant[result.variant] = result;
|
|
118
|
+
}
|
|
119
|
+
return { fixtureId: fixture.id, results, byVariant };
|
|
120
|
+
}
|
package/src/hooks/handler.ts
CHANGED
|
@@ -298,12 +298,12 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
|
|
|
298
298
|
dataDir,
|
|
299
299
|
observations: activeObservations,
|
|
300
300
|
refresh: 'auto',
|
|
301
|
-
enqueueRefresh: () => import('../runtime/
|
|
302
|
-
|
|
301
|
+
enqueueRefresh: () => import('../runtime/lifecycle.js').then(({ enqueueCodegraphRefresh }) => {
|
|
302
|
+
enqueueCodegraphRefresh({
|
|
303
|
+
dataDir,
|
|
303
304
|
projectId: canonicalId,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
payload: { maxFiles: 5_000 },
|
|
305
|
+
source: 'hook-session-start',
|
|
306
|
+
maxFiles: 5_000,
|
|
307
307
|
});
|
|
308
308
|
}),
|
|
309
309
|
});
|
|
@@ -337,7 +337,7 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
|
|
|
337
337
|
* Pipeline: Classify → Policy check → Store → Respond
|
|
338
338
|
* Pattern detection is used for classification only, not storage gating.
|
|
339
339
|
*/
|
|
340
|
-
|
|
340
|
+
async function handleHookEventCore(input: NormalizedHookInput): Promise<{
|
|
341
341
|
observation: ReturnType<typeof buildObservation> | null;
|
|
342
342
|
output: HookOutput;
|
|
343
343
|
}> {
|
|
@@ -431,6 +431,48 @@ export async function handleHookEvent(input: NormalizedHookInput): Promise<{
|
|
|
431
431
|
};
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
/** Queue a later scan after an actual file mutation without scanning in the hook. */
|
|
435
|
+
async function queueCodegraphRefreshForMutation(input: NormalizedHookInput): Promise<void> {
|
|
436
|
+
if (classifyTool(input) !== 'file_modify') return;
|
|
437
|
+
try {
|
|
438
|
+
const [
|
|
439
|
+
{ detectProject },
|
|
440
|
+
{ getProjectDataDir },
|
|
441
|
+
{ initAliasRegistry, registerAlias },
|
|
442
|
+
{ enqueueCodegraphRefresh },
|
|
443
|
+
] = await Promise.all([
|
|
444
|
+
import('../project/detector.js'),
|
|
445
|
+
import('../store/persistence.js'),
|
|
446
|
+
import('../project/aliases.js'),
|
|
447
|
+
import('../runtime/lifecycle.js'),
|
|
448
|
+
]);
|
|
449
|
+
const project = detectProject(input.cwd || process.cwd());
|
|
450
|
+
if (!project) return;
|
|
451
|
+
const dataDir = await getProjectDataDir(project.id);
|
|
452
|
+
initAliasRegistry(dataDir);
|
|
453
|
+
const projectId = await registerAlias(project);
|
|
454
|
+
enqueueCodegraphRefresh({
|
|
455
|
+
dataDir,
|
|
456
|
+
projectId,
|
|
457
|
+
source: 'hook-file-mutation',
|
|
458
|
+
maxFiles: 5_000,
|
|
459
|
+
});
|
|
460
|
+
} catch {
|
|
461
|
+
// Hooks remain capture-first even if optional Code Memory scheduling fails.
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function handleHookEvent(input: NormalizedHookInput): Promise<{
|
|
466
|
+
observation: ReturnType<typeof buildObservation> | null;
|
|
467
|
+
output: HookOutput;
|
|
468
|
+
}> {
|
|
469
|
+
try {
|
|
470
|
+
return await handleHookEventCore(input);
|
|
471
|
+
} finally {
|
|
472
|
+
await queueCodegraphRefreshForMutation(input);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
434
476
|
/**
|
|
435
477
|
* Convert Memorix's neutral hook response into the host-specific response
|
|
436
478
|
* schema expected by each supported agent.
|