memorix 1.1.13 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +3 -2
- package/README.zh-CN.md +3 -2
- package/dist/cli/index.js +36313 -31189
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -30
- package/dist/index.js +5348 -674
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +3661 -293
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +19 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +5346 -672
- 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 +25 -6
- package/docs/CONFIGURATION.md +21 -3
- package/docs/README.md +17 -2
- package/docs/dev-log/progress.txt +120 -40
- 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 +112 -9
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/doctor.ts +73 -4
- package/src/cli/commands/knowledge.ts +282 -0
- package/src/cli/commands/serve-http.ts +12 -1
- 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 +171 -9
- package/src/codegraph/code-state.ts +95 -0
- package/src/codegraph/context-pack.ts +82 -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/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 +15 -1
- 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 +537 -0
- package/src/knowledge/markdown.ts +129 -0
- package/src/knowledge/types.ts +157 -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 +743 -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/observations.ts +19 -0
- 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 +354 -14
- 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
package/src/dashboard/server.ts
CHANGED
|
@@ -322,9 +322,15 @@ async function handleApi(
|
|
|
322
322
|
};
|
|
323
323
|
|
|
324
324
|
let maintenance = { total: 0, pending: 0, running: 0, retrying: 0, completed: 0, failed: 0 };
|
|
325
|
+
let lifecycle: unknown;
|
|
325
326
|
try {
|
|
326
327
|
const { MaintenanceJobStore } = await import('../runtime/maintenance-jobs.js');
|
|
327
328
|
maintenance = new MaintenanceJobStore(effectiveDataDir).summary(effectiveProjectId);
|
|
329
|
+
const { collectLifecycleDiagnostics } = await import('../runtime/lifecycle-status.js');
|
|
330
|
+
lifecycle = await collectLifecycleDiagnostics({
|
|
331
|
+
dataDir: effectiveDataDir,
|
|
332
|
+
projectId: effectiveProjectId,
|
|
333
|
+
});
|
|
328
334
|
} catch { /* optional maintenance diagnostics */ }
|
|
329
335
|
|
|
330
336
|
sendJson(res, {
|
|
@@ -338,6 +344,7 @@ async function handleApi(
|
|
|
338
344
|
embedding: embeddingStatus,
|
|
339
345
|
storage: storageInfo,
|
|
340
346
|
maintenance,
|
|
347
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
341
348
|
gitSummary: {
|
|
342
349
|
total: gitMemories.length,
|
|
343
350
|
recentWeek: recentGitCount,
|
|
@@ -349,7 +356,10 @@ async function handleApi(
|
|
|
349
356
|
}
|
|
350
357
|
|
|
351
358
|
case '/maintenance': {
|
|
352
|
-
const { MaintenanceJobStore } = await
|
|
359
|
+
const [{ MaintenanceJobStore }, { collectLifecycleDiagnostics }] = await Promise.all([
|
|
360
|
+
import('../runtime/maintenance-jobs.js'),
|
|
361
|
+
import('../runtime/lifecycle-status.js'),
|
|
362
|
+
]);
|
|
353
363
|
const jobs = new MaintenanceJobStore(effectiveDataDir).list({
|
|
354
364
|
projectId: effectiveProjectId,
|
|
355
365
|
limit: 50,
|
|
@@ -357,6 +367,10 @@ async function handleApi(
|
|
|
357
367
|
sendJson(res, {
|
|
358
368
|
summary: new MaintenanceJobStore(effectiveDataDir).summary(effectiveProjectId),
|
|
359
369
|
jobs,
|
|
370
|
+
lifecycle: await collectLifecycleDiagnostics({
|
|
371
|
+
dataDir: effectiveDataDir,
|
|
372
|
+
projectId: effectiveProjectId,
|
|
373
|
+
}),
|
|
360
374
|
});
|
|
361
375
|
break;
|
|
362
376
|
}
|
|
@@ -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.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
3
|
+
import { getDatabase } from '../store/sqlite-db.js';
|
|
4
|
+
import type {
|
|
5
|
+
ClaimConflict,
|
|
6
|
+
ClaimEvent,
|
|
7
|
+
ClaimEvidenceRef,
|
|
8
|
+
KnowledgeClaim,
|
|
9
|
+
KnowledgeClaimStatus,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
|
|
12
|
+
function optionalText(value: unknown): string | undefined {
|
|
13
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rowToClaim(row: any): KnowledgeClaim {
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
projectId: row.projectId,
|
|
20
|
+
subject: row.subject,
|
|
21
|
+
predicate: row.predicate,
|
|
22
|
+
objectValue: row.objectValue,
|
|
23
|
+
scope: row.scope,
|
|
24
|
+
claimKey: row.claimKey,
|
|
25
|
+
conflictKey: row.conflictKey,
|
|
26
|
+
status: row.status,
|
|
27
|
+
confidence: Number(row.confidence),
|
|
28
|
+
observedAt: row.observedAt,
|
|
29
|
+
...(optionalText(row.validFrom) ? { validFrom: row.validFrom } : {}),
|
|
30
|
+
...(optionalText(row.validTo) ? { validTo: row.validTo } : {}),
|
|
31
|
+
...(optionalText(row.supersededBy) ? { supersededBy: row.supersededBy } : {}),
|
|
32
|
+
reviewState: row.reviewState,
|
|
33
|
+
origin: row.origin,
|
|
34
|
+
createdAt: row.createdAt,
|
|
35
|
+
updatedAt: row.updatedAt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rowToEvidence(row: any): ClaimEvidenceRef {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
claimId: row.claimId,
|
|
43
|
+
evidenceKind: row.evidenceKind,
|
|
44
|
+
evidenceId: row.evidenceId,
|
|
45
|
+
relation: row.relation,
|
|
46
|
+
...(optionalText(row.snapshotId) ? { snapshotId: row.snapshotId } : {}),
|
|
47
|
+
...(optionalText(row.locator) ? { locator: row.locator } : {}),
|
|
48
|
+
...(optionalText(row.capturedHash) ? { capturedHash: row.capturedHash } : {}),
|
|
49
|
+
createdAt: row.createdAt,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rowToEvent(row: any): ClaimEvent {
|
|
54
|
+
return {
|
|
55
|
+
id: row.id,
|
|
56
|
+
projectId: row.projectId,
|
|
57
|
+
claimId: row.claimId,
|
|
58
|
+
kind: row.kind,
|
|
59
|
+
...(optionalText(row.fromStatus) ? { fromStatus: row.fromStatus } : {}),
|
|
60
|
+
...(optionalText(row.toStatus) ? { toStatus: row.toStatus } : {}),
|
|
61
|
+
...(optionalText(row.relatedClaimId) ? { relatedClaimId: row.relatedClaimId } : {}),
|
|
62
|
+
...(optionalText(row.detail) ? { detail: row.detail } : {}),
|
|
63
|
+
createdAt: row.createdAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function evidenceKey(input: Omit<ClaimEvidenceRef, 'id' | 'claimId' | 'createdAt'>): string {
|
|
68
|
+
return createHash('sha256').update(JSON.stringify([
|
|
69
|
+
input.evidenceKind,
|
|
70
|
+
input.evidenceId,
|
|
71
|
+
input.relation,
|
|
72
|
+
input.snapshotId ?? '',
|
|
73
|
+
input.locator ?? '',
|
|
74
|
+
input.capturedHash ?? '',
|
|
75
|
+
])).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* SQLite access for the claim ledger. Domain policy belongs in claims.ts; this
|
|
80
|
+
* class only preserves records and their audit trail atomically.
|
|
81
|
+
*/
|
|
82
|
+
export class ClaimStore {
|
|
83
|
+
private db: any = null;
|
|
84
|
+
|
|
85
|
+
async init(dataDir: string): Promise<void> {
|
|
86
|
+
this.db = getDatabase(dataDir);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
transaction<T>(fn: () => T): T {
|
|
90
|
+
return this.db.transaction(fn)();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getClaim(id: string): KnowledgeClaim | undefined {
|
|
94
|
+
const row = this.db.prepare('SELECT * FROM knowledge_claims WHERE id = ?').get(id);
|
|
95
|
+
return row ? rowToClaim(row) : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
listClaims(
|
|
99
|
+
projectId: string,
|
|
100
|
+
options: { statuses?: readonly KnowledgeClaimStatus[]; limit?: number } = {},
|
|
101
|
+
): KnowledgeClaim[] {
|
|
102
|
+
const limit = Math.max(1, Math.min(1_000, Math.floor(options.limit ?? 500)));
|
|
103
|
+
if (options.statuses && options.statuses.length > 0) {
|
|
104
|
+
return this.db.prepare(`
|
|
105
|
+
SELECT * FROM knowledge_claims
|
|
106
|
+
WHERE projectId = ? AND status IN (SELECT value FROM json_each(?))
|
|
107
|
+
ORDER BY updatedAt DESC, id
|
|
108
|
+
LIMIT ?
|
|
109
|
+
`).all(projectId, JSON.stringify([...new Set(options.statuses)]), limit).map(rowToClaim);
|
|
110
|
+
}
|
|
111
|
+
return this.db.prepare(`
|
|
112
|
+
SELECT * FROM knowledge_claims
|
|
113
|
+
WHERE projectId = ?
|
|
114
|
+
ORDER BY updatedAt DESC, id
|
|
115
|
+
LIMIT ?
|
|
116
|
+
`).all(projectId, limit).map(rowToClaim);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
findReusableClaim(projectId: string, claimKey: string): KnowledgeClaim | undefined {
|
|
120
|
+
const row = this.db.prepare(`
|
|
121
|
+
SELECT * FROM knowledge_claims
|
|
122
|
+
WHERE projectId = ? AND claimKey = ? AND status IN ('active', 'disputed')
|
|
123
|
+
ORDER BY updatedAt DESC
|
|
124
|
+
LIMIT 1
|
|
125
|
+
`).get(projectId, claimKey);
|
|
126
|
+
return row ? rowToClaim(row) : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
listClaimsByConflictKey(projectId: string, conflictKey: string): KnowledgeClaim[] {
|
|
130
|
+
return this.db.prepare(`
|
|
131
|
+
SELECT * FROM knowledge_claims
|
|
132
|
+
WHERE projectId = ? AND conflictKey = ?
|
|
133
|
+
ORDER BY createdAt ASC, id
|
|
134
|
+
`).all(projectId, conflictKey).map(rowToClaim);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
listConflicts(projectId: string): ClaimConflict[] {
|
|
138
|
+
const rows = this.db.prepare(`
|
|
139
|
+
SELECT conflictKey
|
|
140
|
+
FROM knowledge_claims
|
|
141
|
+
WHERE projectId = ? AND status IN ('active', 'disputed') AND reviewState = 'approved'
|
|
142
|
+
GROUP BY conflictKey
|
|
143
|
+
HAVING COUNT(DISTINCT claimKey) > 1
|
|
144
|
+
ORDER BY conflictKey
|
|
145
|
+
`).all(projectId) as Array<{ conflictKey: string }>;
|
|
146
|
+
return rows.map(({ conflictKey }) => ({
|
|
147
|
+
conflictKey,
|
|
148
|
+
claims: this.listClaimsByConflictKey(projectId, conflictKey)
|
|
149
|
+
.filter(claim => claim.status === 'active' || claim.status === 'disputed'),
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
insertClaim(claim: KnowledgeClaim): void {
|
|
154
|
+
this.db.prepare(`
|
|
155
|
+
INSERT INTO knowledge_claims (
|
|
156
|
+
id, projectId, subject, predicate, objectValue, scope, claimKey, conflictKey,
|
|
157
|
+
status, confidence, observedAt, validFrom, validTo, supersededBy,
|
|
158
|
+
reviewState, origin, createdAt, updatedAt
|
|
159
|
+
) VALUES (
|
|
160
|
+
@id, @projectId, @subject, @predicate, @objectValue, @scope, @claimKey, @conflictKey,
|
|
161
|
+
@status, @confidence, @observedAt, @validFrom, @validTo, @supersededBy,
|
|
162
|
+
@reviewState, @origin, @createdAt, @updatedAt
|
|
163
|
+
)
|
|
164
|
+
`).run({
|
|
165
|
+
...claim,
|
|
166
|
+
validFrom: claim.validFrom ?? null,
|
|
167
|
+
validTo: claim.validTo ?? null,
|
|
168
|
+
supersededBy: claim.supersededBy ?? null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
updateClaim(claim: KnowledgeClaim): void {
|
|
173
|
+
this.db.prepare(`
|
|
174
|
+
UPDATE knowledge_claims SET
|
|
175
|
+
subject = @subject,
|
|
176
|
+
predicate = @predicate,
|
|
177
|
+
objectValue = @objectValue,
|
|
178
|
+
scope = @scope,
|
|
179
|
+
claimKey = @claimKey,
|
|
180
|
+
conflictKey = @conflictKey,
|
|
181
|
+
status = @status,
|
|
182
|
+
confidence = @confidence,
|
|
183
|
+
observedAt = @observedAt,
|
|
184
|
+
validFrom = @validFrom,
|
|
185
|
+
validTo = @validTo,
|
|
186
|
+
supersededBy = @supersededBy,
|
|
187
|
+
reviewState = @reviewState,
|
|
188
|
+
origin = @origin,
|
|
189
|
+
updatedAt = @updatedAt
|
|
190
|
+
WHERE id = @id
|
|
191
|
+
`).run({
|
|
192
|
+
...claim,
|
|
193
|
+
validFrom: claim.validFrom ?? null,
|
|
194
|
+
validTo: claim.validTo ?? null,
|
|
195
|
+
supersededBy: claim.supersededBy ?? null,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
touchClaim(id: string, updatedAt: string): void {
|
|
200
|
+
this.db.prepare('UPDATE knowledge_claims SET updatedAt = ? WHERE id = ?').run(updatedAt, id);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
insertEvidence(input: Omit<ClaimEvidenceRef, 'id' | 'createdAt'> & { createdAt?: string }): boolean {
|
|
204
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
205
|
+
const key = evidenceKey(input);
|
|
206
|
+
const result = this.db.prepare(`
|
|
207
|
+
INSERT OR IGNORE INTO knowledge_claim_evidence (
|
|
208
|
+
id, claimId, evidenceKind, evidenceId, relation, snapshotId,
|
|
209
|
+
locator, capturedHash, evidenceKey, createdAt
|
|
210
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
211
|
+
`).run(
|
|
212
|
+
randomUUID(),
|
|
213
|
+
input.claimId,
|
|
214
|
+
input.evidenceKind,
|
|
215
|
+
input.evidenceId,
|
|
216
|
+
input.relation,
|
|
217
|
+
input.snapshotId ?? null,
|
|
218
|
+
input.locator ? sanitizeCredentials(input.locator) : null,
|
|
219
|
+
input.capturedHash ?? null,
|
|
220
|
+
key,
|
|
221
|
+
createdAt,
|
|
222
|
+
);
|
|
223
|
+
return Number(result.changes ?? 0) > 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
listEvidence(claimId: string): ClaimEvidenceRef[] {
|
|
227
|
+
return this.db.prepare(`
|
|
228
|
+
SELECT * FROM knowledge_claim_evidence
|
|
229
|
+
WHERE claimId = ?
|
|
230
|
+
ORDER BY createdAt ASC, id
|
|
231
|
+
`).all(claimId).map(rowToEvidence);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
recordEvent(input: Omit<ClaimEvent, 'id' | 'createdAt'> & { createdAt?: string }): ClaimEvent {
|
|
235
|
+
const event: ClaimEvent = {
|
|
236
|
+
id: randomUUID(),
|
|
237
|
+
...input,
|
|
238
|
+
...(input.detail ? { detail: sanitizeCredentials(input.detail) } : {}),
|
|
239
|
+
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
240
|
+
};
|
|
241
|
+
this.db.prepare(`
|
|
242
|
+
INSERT INTO knowledge_claim_events (
|
|
243
|
+
id, projectId, claimId, kind, fromStatus, toStatus,
|
|
244
|
+
relatedClaimId, detail, createdAt
|
|
245
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
246
|
+
`).run(
|
|
247
|
+
event.id,
|
|
248
|
+
event.projectId,
|
|
249
|
+
event.claimId,
|
|
250
|
+
event.kind,
|
|
251
|
+
event.fromStatus ?? null,
|
|
252
|
+
event.toStatus ?? null,
|
|
253
|
+
event.relatedClaimId ?? null,
|
|
254
|
+
event.detail ?? null,
|
|
255
|
+
event.createdAt,
|
|
256
|
+
);
|
|
257
|
+
return event;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
listEvents(claimId: string): ClaimEvent[] {
|
|
261
|
+
return this.db.prepare(`
|
|
262
|
+
SELECT * FROM knowledge_claim_events
|
|
263
|
+
WHERE claimId = ?
|
|
264
|
+
ORDER BY createdAt ASC, id
|
|
265
|
+
`).all(claimId).map(rowToEvent);
|
|
266
|
+
}
|
|
267
|
+
}
|