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.
Files changed (97) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +6 -3
  4. package/dist/cli/index.js +36639 -31279
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +50 -30
  7. package/dist/index.js +6636 -1778
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +3775 -302
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +33 -1
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +6634 -1776
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
  17. package/docs/1.2.0-CODE-STATE.md +61 -0
  18. package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
  19. package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
  20. package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
  21. package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
  22. package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
  23. package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
  24. package/docs/1.2.0-PRODUCT-STORY.md +234 -0
  25. package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
  26. package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
  27. package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
  28. package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
  29. package/docs/API_REFERENCE.md +27 -6
  30. package/docs/CONFIGURATION.md +21 -3
  31. package/docs/DEVELOPMENT.md +4 -0
  32. package/docs/README.md +17 -2
  33. package/docs/SETUP.md +7 -1
  34. package/docs/dev-log/progress.txt +120 -40
  35. package/docs/knowledge/workflows/memorix-release.md +57 -0
  36. package/llms-full.txt +16 -2
  37. package/llms.txt +9 -4
  38. package/package.json +3 -2
  39. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  40. package/src/cli/capability-map.ts +1 -0
  41. package/src/cli/commands/codegraph.ts +111 -9
  42. package/src/cli/commands/context.ts +2 -0
  43. package/src/cli/commands/doctor.ts +73 -4
  44. package/src/cli/commands/knowledge.ts +322 -0
  45. package/src/cli/commands/serve-http.ts +26 -42
  46. package/src/cli/commands/setup.ts +9 -3
  47. package/src/cli/index.ts +3 -1
  48. package/src/cli/tui/App.tsx +1 -1
  49. package/src/cli/tui/Panels.tsx +8 -8
  50. package/src/cli/tui/theme.ts +1 -1
  51. package/src/cli/tui/views/GraphView.tsx +8 -7
  52. package/src/cli/tui/views/KnowledgeView.tsx +9 -9
  53. package/src/codegraph/auto-context.ts +169 -19
  54. package/src/codegraph/code-state.ts +95 -0
  55. package/src/codegraph/context-pack.ts +82 -1
  56. package/src/codegraph/current-facts.ts +19 -1
  57. package/src/codegraph/external-provider.ts +581 -0
  58. package/src/codegraph/lite-provider.ts +64 -19
  59. package/src/codegraph/project-context.ts +9 -1
  60. package/src/codegraph/store.ts +154 -6
  61. package/src/codegraph/task-lens.ts +49 -5
  62. package/src/codegraph/types.ts +117 -0
  63. package/src/config/resolved-config.ts +28 -0
  64. package/src/config/toml-loader.ts +3 -0
  65. package/src/config/yaml-loader.ts +6 -0
  66. package/src/dashboard/server.ts +30 -47
  67. package/src/evaluation/workset-evaluation.ts +120 -0
  68. package/src/hooks/handler.ts +48 -6
  69. package/src/knowledge/claim-store.ts +267 -0
  70. package/src/knowledge/claims.ts +587 -0
  71. package/src/knowledge/markdown.ts +129 -0
  72. package/src/knowledge/types.ts +158 -0
  73. package/src/knowledge/wiki.ts +524 -0
  74. package/src/knowledge/workflow-store.ts +168 -0
  75. package/src/knowledge/workflow-types.ts +95 -0
  76. package/src/knowledge/workflows.ts +774 -0
  77. package/src/knowledge/workset.ts +515 -0
  78. package/src/knowledge/workspace-store.ts +220 -0
  79. package/src/knowledge/workspace-types.ts +106 -0
  80. package/src/knowledge/workspace.ts +220 -0
  81. package/src/memory/auto-relations.ts +21 -0
  82. package/src/memory/graph-scope.ts +46 -0
  83. package/src/memory/observations.ts +19 -0
  84. package/src/orchestrate/verify-gate.ts +33 -10
  85. package/src/runtime/control-plane-maintenance.ts +5 -0
  86. package/src/runtime/isolated-maintenance.ts +5 -0
  87. package/src/runtime/lifecycle-status.ts +102 -0
  88. package/src/runtime/lifecycle.ts +107 -0
  89. package/src/runtime/maintenance-jobs.ts +5 -0
  90. package/src/runtime/project-maintenance.ts +190 -0
  91. package/src/server/tool-profile.ts +3 -2
  92. package/src/server.ts +424 -22
  93. package/src/store/file-lock.ts +24 -4
  94. package/src/store/sqlite-db.ts +307 -0
  95. package/src/wiki/generator.ts +4 -2
  96. package/src/wiki/knowledge-graph.ts +7 -4
  97. package/src/wiki/types.ts +16 -4
@@ -0,0 +1,587 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { countTextTokens } from '../compact/token-budget.js';
3
+ import type { CodeGraphStore } from '../codegraph/store.js';
4
+ import type { CodeStateSnapshot, ObservationCodeRef } from '../codegraph/types.js';
5
+ import { sanitizeCredentials } from '../memory/secret-filter.js';
6
+ import type { Observation, ObservationType } from '../types.js';
7
+ import { ClaimStore } from './claim-store.js';
8
+ import type {
9
+ ClaimConflict,
10
+ ClaimEvidenceInput,
11
+ ClaimEvidenceRef,
12
+ ClaimSelection,
13
+ ClaimWriteResult,
14
+ KnowledgeClaim,
15
+ KnowledgeClaimInput,
16
+ KnowledgeClaimReviewState,
17
+ KnowledgeClaimStatus,
18
+ } from './types.js';
19
+
20
+ const MAX_TERM_LENGTH = 2_000;
21
+ const MAX_EVIDENCE_ID_LENGTH = 1_000;
22
+ const TOKEN_PATTERN = /[\p{L}\p{N}_./:-]+/gu;
23
+
24
+ const PREDICATE_BY_OBSERVATION_TYPE: Partial<Record<ObservationType, string>> = {
25
+ decision: 'decision',
26
+ 'trade-off': 'trade-off',
27
+ 'why-it-exists': 'rationale',
28
+ 'what-changed': 'changed',
29
+ 'problem-solution': 'workaround',
30
+ gotcha: 'caution',
31
+ 'how-it-works': 'behavior',
32
+ discovery: 'finding',
33
+ reasoning: 'rationale',
34
+ };
35
+
36
+ function compactText(value: string, field: string): string {
37
+ if (typeof value !== 'string') throw new Error('Claim ' + field + ' must be text');
38
+ const sanitized = sanitizeCredentials(value).trim().replace(/\s+/g, ' ');
39
+ if (!sanitized) throw new Error('Claim ' + field + ' is required');
40
+ if (sanitized.length > MAX_TERM_LENGTH) throw new Error('Claim ' + field + ' is too long');
41
+ return sanitized;
42
+ }
43
+
44
+ function normalizedTerm(value: string): string {
45
+ return compactText(value, 'identity').toLocaleLowerCase('en-US');
46
+ }
47
+
48
+ function hashIdentity(parts: string[]): string {
49
+ return createHash('sha256').update(parts.join(String.fromCharCode(31))).digest('hex');
50
+ }
51
+
52
+ export function buildClaimIdentity(input: Pick<KnowledgeClaimInput, 'subject' | 'predicate' | 'objectValue' | 'scope'>): {
53
+ claimKey: string;
54
+ conflictKey: string;
55
+ } {
56
+ const subject = normalizedTerm(input.subject);
57
+ const predicate = normalizedTerm(input.predicate);
58
+ const objectValue = normalizedTerm(input.objectValue);
59
+ return {
60
+ claimKey: hashIdentity([subject, predicate, objectValue, input.scope]),
61
+ conflictKey: hashIdentity([subject, predicate, input.scope]),
62
+ };
63
+ }
64
+
65
+ function clampConfidence(value: number | undefined): number {
66
+ if (value === undefined) return 0.7;
67
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
68
+ throw new Error('Claim confidence must be a number between 0 and 1');
69
+ }
70
+ return value;
71
+ }
72
+
73
+ function validateEvidence(input: ClaimEvidenceInput[]): ClaimEvidenceInput[] {
74
+ if (!Array.isArray(input) || input.length === 0) {
75
+ throw new Error('A knowledge claim requires at least one evidence reference');
76
+ }
77
+ return input.map((evidence) => {
78
+ if (!evidence || typeof evidence !== 'object') {
79
+ throw new Error('Claim evidence must be an object');
80
+ }
81
+ const evidenceId = compactText(evidence.evidenceId, 'evidence id');
82
+ if (evidenceId.length > MAX_EVIDENCE_ID_LENGTH) {
83
+ throw new Error('Claim evidence id is too long');
84
+ }
85
+ if (!['observation', 'git', 'code', 'test', 'document', 'workflow', 'run'].includes(evidence.evidenceKind)) {
86
+ throw new Error('Claim evidence kind is invalid');
87
+ }
88
+ if (!['supports', 'contradicts', 'derives', 'verifies'].includes(evidence.relation)) {
89
+ throw new Error('Claim evidence relation is invalid');
90
+ }
91
+ return {
92
+ evidenceKind: evidence.evidenceKind,
93
+ evidenceId,
94
+ relation: evidence.relation,
95
+ ...(evidence.snapshotId ? { snapshotId: compactText(evidence.snapshotId, 'evidence snapshot id') } : {}),
96
+ ...(evidence.locator ? { locator: compactText(evidence.locator, 'evidence locator') } : {}),
97
+ ...(evidence.capturedHash ? { capturedHash: compactText(evidence.capturedHash, 'evidence hash') } : {}),
98
+ };
99
+ });
100
+ }
101
+
102
+ function normalizeClaimInput(input: KnowledgeClaimInput): {
103
+ claim: KnowledgeClaim;
104
+ evidence: ClaimEvidenceInput[];
105
+ } {
106
+ if (!['project', 'workspace', 'team', 'workflow', 'task'].includes(input.scope)) {
107
+ throw new Error('Claim scope is invalid');
108
+ }
109
+ const projectId = compactText(input.projectId, 'project id');
110
+ const subject = compactText(input.subject, 'subject');
111
+ const predicate = compactText(input.predicate, 'predicate');
112
+ const objectValue = compactText(input.objectValue, 'object value');
113
+ const identity = buildClaimIdentity({ subject, predicate, objectValue, scope: input.scope });
114
+ const origin = input.origin ?? 'explicit';
115
+ let reviewState: KnowledgeClaimReviewState = input.reviewState ?? 'approved';
116
+ let status: KnowledgeClaimStatus = input.status ?? 'active';
117
+ if (origin === 'model') {
118
+ if (input.reviewState === 'approved' || input.status === 'active') {
119
+ throw new Error('A model-origin claim cannot be approved or active without explicit review');
120
+ }
121
+ reviewState = input.reviewState ?? 'draft';
122
+ status = input.status ?? 'unknown';
123
+ }
124
+ if (!['approved', 'needs-review', 'draft', 'rejected'].includes(reviewState)) {
125
+ throw new Error('Claim review state is invalid');
126
+ }
127
+ if (!['active', 'superseded', 'disputed', 'unknown'].includes(status)) {
128
+ throw new Error('Claim status is invalid');
129
+ }
130
+ const now = new Date().toISOString();
131
+ return {
132
+ claim: {
133
+ id: randomUUID(),
134
+ projectId,
135
+ subject,
136
+ predicate,
137
+ objectValue,
138
+ scope: input.scope,
139
+ claimKey: identity.claimKey,
140
+ conflictKey: identity.conflictKey,
141
+ status,
142
+ confidence: clampConfidence(input.confidence),
143
+ observedAt: input.observedAt ?? now,
144
+ ...(input.validFrom ? { validFrom: input.validFrom } : {}),
145
+ ...(input.validTo ? { validTo: input.validTo } : {}),
146
+ reviewState,
147
+ origin,
148
+ createdAt: now,
149
+ updatedAt: now,
150
+ },
151
+ evidence: validateEvidence(input.evidence),
152
+ };
153
+ }
154
+
155
+ function activeConflictClaims(store: ClaimStore, projectId: string, conflictKey: string): KnowledgeClaim[] {
156
+ return store.listClaimsByConflictKey(projectId, conflictKey).filter(claim =>
157
+ (claim.status === 'active' || claim.status === 'disputed') && claim.reviewState === 'approved');
158
+ }
159
+
160
+ function reconcileConflicts(
161
+ store: ClaimStore,
162
+ projectId: string,
163
+ conflictKey: string,
164
+ now: string,
165
+ ): ClaimConflict[] {
166
+ const claims = activeConflictClaims(store, projectId, conflictKey);
167
+ const hasConflict = new Set(claims.map(claim => claim.claimKey)).size > 1;
168
+ for (const claim of claims) {
169
+ const nextStatus: KnowledgeClaimStatus = hasConflict ? 'disputed' : 'active';
170
+ if (claim.status === nextStatus) continue;
171
+ store.updateClaim({ ...claim, status: nextStatus, updatedAt: now });
172
+ store.recordEvent({
173
+ projectId,
174
+ claimId: claim.id,
175
+ kind: hasConflict ? 'conflicted' : 'requalified',
176
+ fromStatus: claim.status,
177
+ toStatus: nextStatus,
178
+ detail: hasConflict
179
+ ? 'A different approved assertion has the same subject, predicate, and scope.'
180
+ : 'No remaining approved competing assertion exists.',
181
+ createdAt: now,
182
+ });
183
+ }
184
+ return store.listConflicts(projectId).filter(conflict => conflict.conflictKey === conflictKey);
185
+ }
186
+
187
+ function persistEvidence(store: ClaimStore, claim: KnowledgeClaim, evidence: ClaimEvidenceInput[], now: string): void {
188
+ let added = false;
189
+ for (const item of evidence) {
190
+ added = store.insertEvidence({ claimId: claim.id, ...item, createdAt: now }) || added;
191
+ }
192
+ if (added) {
193
+ store.touchClaim(claim.id, now);
194
+ store.recordEvent({
195
+ projectId: claim.projectId,
196
+ claimId: claim.id,
197
+ kind: 'evidence-added',
198
+ detail: 'Additional source evidence was attached.',
199
+ createdAt: now,
200
+ });
201
+ }
202
+ }
203
+
204
+ /** Write one claim without allowing a summary to replace its source evidence. */
205
+ export function writeClaim(store: ClaimStore, input: KnowledgeClaimInput): ClaimWriteResult {
206
+ const normalized = normalizeClaimInput(input);
207
+ return store.transaction(() => {
208
+ const existing = store.findReusableClaim(normalized.claim.projectId, normalized.claim.claimKey);
209
+ if (existing) {
210
+ persistEvidence(store, existing, normalized.evidence, normalized.claim.updatedAt);
211
+ return {
212
+ claim: store.getClaim(existing.id) ?? existing,
213
+ created: false,
214
+ conflicts: store.listConflicts(existing.projectId)
215
+ .filter(conflict => conflict.conflictKey === existing.conflictKey),
216
+ };
217
+ }
218
+
219
+ store.insertClaim(normalized.claim);
220
+ for (const evidence of normalized.evidence) {
221
+ store.insertEvidence({ claimId: normalized.claim.id, ...evidence, createdAt: normalized.claim.createdAt });
222
+ }
223
+ store.recordEvent({
224
+ projectId: normalized.claim.projectId,
225
+ claimId: normalized.claim.id,
226
+ kind: 'created',
227
+ toStatus: normalized.claim.status,
228
+ detail: 'Claim created with source evidence.',
229
+ createdAt: normalized.claim.createdAt,
230
+ });
231
+ const conflicts = reconcileConflicts(
232
+ store,
233
+ normalized.claim.projectId,
234
+ normalized.claim.conflictKey,
235
+ normalized.claim.updatedAt,
236
+ );
237
+ return {
238
+ claim: store.getClaim(normalized.claim.id) ?? normalized.claim,
239
+ created: true,
240
+ conflicts,
241
+ };
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Deliberately approve or reject a source-backed claim after its evidence has
247
+ * been checked. Derived agent observations never cross this boundary by
248
+ * themselves.
249
+ */
250
+ export function reviewClaim(
251
+ store: ClaimStore,
252
+ input: {
253
+ claimId: string;
254
+ reviewState: Extract<KnowledgeClaimReviewState, 'approved' | 'rejected'>;
255
+ detail: string;
256
+ },
257
+ ): KnowledgeClaim {
258
+ if (input.reviewState !== 'approved' && input.reviewState !== 'rejected') {
259
+ throw new Error('A claim review must approve or reject the claim');
260
+ }
261
+ const detail = compactText(input.detail, 'review detail');
262
+ return store.transaction(() => {
263
+ const claim = store.getClaim(input.claimId);
264
+ if (!claim) throw new Error('Claim was not found');
265
+ if (input.reviewState === 'approved' && claim.status === 'superseded') {
266
+ throw new Error('A superseded claim cannot be approved');
267
+ }
268
+ const now = new Date().toISOString();
269
+ const nextStatus: KnowledgeClaimStatus = input.reviewState === 'rejected'
270
+ ? 'unknown'
271
+ : claim.status === 'unknown' && claim.reviewState === 'rejected'
272
+ ? 'active'
273
+ : claim.status;
274
+ const updated: KnowledgeClaim = {
275
+ ...claim,
276
+ status: nextStatus,
277
+ reviewState: input.reviewState,
278
+ updatedAt: now,
279
+ };
280
+ store.updateClaim(updated);
281
+ store.recordEvent({
282
+ projectId: claim.projectId,
283
+ claimId: claim.id,
284
+ kind: 'reviewed',
285
+ fromStatus: claim.status,
286
+ toStatus: updated.status,
287
+ detail: 'Review: ' + claim.reviewState + ' -> ' + input.reviewState + '. ' + detail,
288
+ createdAt: now,
289
+ });
290
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now);
291
+ return store.getClaim(claim.id) ?? updated;
292
+ });
293
+ }
294
+
295
+ export function supersedeClaim(
296
+ store: ClaimStore,
297
+ input: {
298
+ claimId: string;
299
+ replacementClaimId: string;
300
+ evidence: ClaimEvidenceInput[];
301
+ },
302
+ ): { superseded: KnowledgeClaim; replacement: KnowledgeClaim } {
303
+ const evidence = validateEvidence(input.evidence);
304
+ return store.transaction(() => {
305
+ const claim = store.getClaim(input.claimId);
306
+ const replacement = store.getClaim(input.replacementClaimId);
307
+ if (!claim || !replacement) throw new Error('Both claims must exist before supersession');
308
+ if (claim.projectId !== replacement.projectId || claim.conflictKey !== replacement.conflictKey) {
309
+ throw new Error('A replacement claim must address the same project assertion');
310
+ }
311
+ if (claim.id === replacement.id) throw new Error('A claim cannot supersede itself');
312
+ const now = new Date().toISOString();
313
+ persistEvidence(store, claim, evidence, now);
314
+ const updated: KnowledgeClaim = {
315
+ ...claim,
316
+ status: 'superseded',
317
+ validTo: now,
318
+ supersededBy: replacement.id,
319
+ updatedAt: now,
320
+ };
321
+ store.updateClaim(updated);
322
+ store.recordEvent({
323
+ projectId: claim.projectId,
324
+ claimId: claim.id,
325
+ kind: 'superseded',
326
+ fromStatus: claim.status,
327
+ toStatus: 'superseded',
328
+ relatedClaimId: replacement.id,
329
+ detail: 'Superseded by an evidence-backed replacement claim.',
330
+ createdAt: now,
331
+ });
332
+ reconcileConflicts(store, claim.projectId, claim.conflictKey, now);
333
+ return {
334
+ superseded: store.getClaim(claim.id) ?? updated,
335
+ replacement: store.getClaim(replacement.id) ?? replacement,
336
+ };
337
+ });
338
+ }
339
+
340
+ function hashObservation(observation: Observation): string {
341
+ return createHash('sha256').update(JSON.stringify([
342
+ observation.title,
343
+ observation.narrative,
344
+ observation.facts,
345
+ observation.filesModified,
346
+ observation.updatedAt ?? observation.createdAt,
347
+ ])).digest('hex');
348
+ }
349
+
350
+ function evidenceFromCodeRef(ref: ObservationCodeRef): ClaimEvidenceInput {
351
+ return {
352
+ evidenceKind: 'code',
353
+ evidenceId: 'code-ref:' + ref.id,
354
+ relation: 'supports',
355
+ ...(ref.snapshotId ? { snapshotId: ref.snapshotId } : {}),
356
+ locator: 'code-ref/' + ref.id,
357
+ ...(ref.capturedSymbolHash || ref.capturedFileHash
358
+ ? { capturedHash: ref.capturedSymbolHash ?? ref.capturedFileHash }
359
+ : {}),
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Converts only explicit or Git-ingested observations into conservative,
365
+ * reviewable source claims. Hook/model-derived memories remain ordinary memory
366
+ * until an agent or operator explicitly promotes them.
367
+ */
368
+ export function deriveLowRiskClaimsFromObservation(
369
+ store: ClaimStore,
370
+ observation: Observation,
371
+ codeStore?: CodeGraphStore,
372
+ ): KnowledgeClaim[] {
373
+ const predicate = PREDICATE_BY_OBSERVATION_TYPE[observation.type];
374
+ const isExplicit = observation.sourceDetail === 'explicit';
375
+ const isGit = observation.source === 'git' || observation.sourceDetail === 'git-ingest';
376
+ if (!predicate || observation.status !== 'active' || (!isExplicit && !isGit)) return [];
377
+
378
+ const evidence: ClaimEvidenceInput[] = [{
379
+ evidenceKind: 'observation',
380
+ evidenceId: 'observation:' + observation.id,
381
+ relation: 'supports',
382
+ locator: 'observation/' + observation.id,
383
+ capturedHash: hashObservation(observation),
384
+ }];
385
+ if (observation.commitHash) {
386
+ evidence.push({
387
+ evidenceKind: 'git',
388
+ evidenceId: 'git:' + observation.commitHash,
389
+ relation: 'verifies',
390
+ locator: 'git:' + observation.commitHash,
391
+ capturedHash: observation.commitHash,
392
+ });
393
+ }
394
+ const refs = codeStore?.listObservationRefs(observation.projectId, observation.id) ?? [];
395
+ for (const ref of refs) evidence.push(evidenceFromCodeRef(ref));
396
+
397
+ const confidence = Math.min(0.9, (isGit ? 0.75 : 0.7) + refs.filter(ref => ref.status === 'current').length * 0.05);
398
+ const result = writeClaim(store, {
399
+ projectId: observation.projectId,
400
+ subject: observation.entityName,
401
+ predicate,
402
+ objectValue: observation.title,
403
+ scope: 'project',
404
+ confidence,
405
+ observedAt: observation.updatedAt ?? observation.createdAt,
406
+ reviewState: isGit ? 'approved' : 'needs-review',
407
+ origin: isGit ? 'git' : 'derived',
408
+ evidence,
409
+ });
410
+ return [result.claim];
411
+ }
412
+
413
+ function incompleteSnapshot(snapshot: CodeStateSnapshot | undefined): boolean {
414
+ if (!snapshot) return false;
415
+ return snapshot.completeness.skippedOversizedFiles > 0
416
+ || (snapshot.completeness.unreadableFiles ?? 0) > 0
417
+ || snapshot.completeness.removalScanDeferred;
418
+ }
419
+
420
+ function requalification(
421
+ claim: KnowledgeClaim,
422
+ refs: Map<string, ObservationCodeRef>,
423
+ evidence: ClaimEvidenceRef[],
424
+ snapshot: CodeStateSnapshot | undefined,
425
+ ): { status: KnowledgeClaimStatus; confidence: number; reviewState: KnowledgeClaimReviewState; reason?: string } | undefined {
426
+ if (claim.status === 'superseded' || claim.reviewState === 'draft' || claim.reviewState === 'rejected') return undefined;
427
+ const codeEvidence = evidence.filter(item => item.evidenceKind === 'code');
428
+ if (codeEvidence.length === 0) return undefined;
429
+ const linkedRefs = codeEvidence
430
+ .map(item => refs.get(item.evidenceId.replace(/^code-ref:/, '')))
431
+ .filter((ref): ref is ObservationCodeRef => !!ref);
432
+ const missingOrStale = linkedRefs.length !== codeEvidence.length || linkedRefs.some(ref => ref.status === 'stale');
433
+ if (missingOrStale) {
434
+ return {
435
+ status: 'unknown',
436
+ confidence: Math.min(claim.confidence, 0.2),
437
+ reviewState: 'needs-review',
438
+ reason: 'Bound code evidence is no longer current.',
439
+ };
440
+ }
441
+ if (linkedRefs.some(ref => ref.status === 'suspect')) {
442
+ return {
443
+ status: claim.status === 'disputed' ? 'disputed' : 'active',
444
+ confidence: Math.min(claim.confidence, 0.5),
445
+ reviewState: 'needs-review',
446
+ reason: 'Bound file evidence changed and needs review.',
447
+ };
448
+ }
449
+ if (incompleteSnapshot(snapshot)) {
450
+ return {
451
+ status: claim.status === 'disputed' ? 'disputed' : 'active',
452
+ confidence: Math.min(claim.confidence, 0.55),
453
+ reviewState: 'needs-review',
454
+ reason: 'The latest code scan is incomplete.',
455
+ };
456
+ }
457
+ return undefined;
458
+ }
459
+
460
+ /** Re-score code-bound claims after a completed CodeGraph refresh. */
461
+ export function requalifyClaimsForCodeState(
462
+ store: ClaimStore,
463
+ codeStore: CodeGraphStore,
464
+ projectId: string,
465
+ ): { requalified: number; incompleteSnapshot: boolean } {
466
+ const snapshot = codeStore.latestSnapshot(projectId);
467
+ const refs = new Map(codeStore.listProjectObservationRefs(projectId).map(ref => [ref.id, ref]));
468
+ let requalified = 0;
469
+ store.transaction(() => {
470
+ for (const claim of store.listClaims(projectId)) {
471
+ const next = requalification(claim, refs, store.listEvidence(claim.id), snapshot);
472
+ if (!next) continue;
473
+ if (
474
+ next.status === claim.status
475
+ && next.confidence === claim.confidence
476
+ && next.reviewState === claim.reviewState
477
+ ) {
478
+ continue;
479
+ }
480
+ const updated = {
481
+ ...claim,
482
+ status: next.status,
483
+ confidence: next.confidence,
484
+ reviewState: next.reviewState,
485
+ updatedAt: new Date().toISOString(),
486
+ };
487
+ store.updateClaim(updated);
488
+ store.recordEvent({
489
+ projectId,
490
+ claimId: claim.id,
491
+ kind: 'requalified',
492
+ fromStatus: claim.status,
493
+ toStatus: next.status,
494
+ detail: next.reason,
495
+ createdAt: updated.updatedAt,
496
+ });
497
+ requalified += 1;
498
+ }
499
+ });
500
+ return { requalified, incompleteSnapshot: incompleteSnapshot(snapshot) };
501
+ }
502
+
503
+ function normalizedTokens(value: string): Set<string> {
504
+ const raw = sanitizeCredentials(value)
505
+ .toLocaleLowerCase('en-US')
506
+ .match(TOKEN_PATTERN)
507
+ ?? [];
508
+ return new Set(raw
509
+ .map(token => token.replace(/^[,;:!?]+|[,;:!?.]+$/g, ''))
510
+ .filter(token => token.length > 1));
511
+ }
512
+
513
+ function taskTokens(task: string): Set<string> {
514
+ return normalizedTokens(task);
515
+ }
516
+
517
+ function scoreClaim(claim: KnowledgeClaim, tokens: Set<string>): number {
518
+ const text = [claim.subject, claim.predicate, claim.objectValue]
519
+ .join(' ')
520
+ .toLocaleLowerCase('en-US');
521
+ const words = normalizedTokens(text);
522
+ let matches = 0;
523
+ for (const token of tokens) {
524
+ if (words.has(token)) matches += 1;
525
+ }
526
+ if (matches === 0) return 0;
527
+ const statusWeight = claim.status === 'active' ? 0.25 : claim.status === 'disputed' ? 0.2 : 0.1;
528
+ return matches * 10 + claim.confidence + statusWeight;
529
+ }
530
+
531
+ function formatClaim(claim: KnowledgeClaim): string {
532
+ return sanitizeCredentials([claim.subject, claim.predicate, claim.objectValue].join(' '));
533
+ }
534
+
535
+ /**
536
+ * A deterministic compact selector used by the later Workset builder. It
537
+ * returns no generic claim dump when no task terms match.
538
+ */
539
+ export function selectClaimsForTask(
540
+ store: ClaimStore,
541
+ input: { projectId: string; task: string; limit?: number; maxTokens?: number },
542
+ ): ClaimSelection {
543
+ const limit = Math.max(1, Math.min(12, Math.floor(input.limit ?? 4)));
544
+ const maxTokens = Math.max(16, Math.min(1_000, Math.floor(input.maxTokens ?? 160)));
545
+ const tokens = taskTokens(input.task);
546
+ if (tokens.size === 0) return { claims: [], cautions: [], tokenCount: 0, reasons: {} };
547
+
548
+ const candidates = store.listClaims(input.projectId, {
549
+ statuses: ['active', 'disputed', 'unknown'],
550
+ limit: 1_000,
551
+ }).filter(claim => claim.reviewState !== 'draft' && claim.reviewState !== 'rejected');
552
+ const scored = candidates
553
+ .map(claim => ({ claim, score: scoreClaim(claim, tokens) }))
554
+ .filter(item => item.score > 0)
555
+ .sort((left, right) => right.score - left.score || left.claim.id.localeCompare(right.claim.id));
556
+ if (scored.length === 0) return { claims: [], cautions: [], tokenCount: 0, reasons: {} };
557
+
558
+ const selected = new Map<string, KnowledgeClaim>();
559
+ for (const { claim } of scored.slice(0, limit)) selected.set(claim.id, claim);
560
+ for (const claim of [...selected.values()]) {
561
+ for (const sibling of store.listClaimsByConflictKey(input.projectId, claim.conflictKey)) {
562
+ if (sibling.status === 'active' || sibling.status === 'disputed') selected.set(sibling.id, sibling);
563
+ }
564
+ }
565
+
566
+ const ordered = [...selected.values()]
567
+ .sort((left, right) => scoreClaim(right, tokens) - scoreClaim(left, tokens) || left.id.localeCompare(right.id));
568
+ const included: KnowledgeClaim[] = [];
569
+ let tokenCount = 0;
570
+ for (const claim of ordered) {
571
+ const nextTokens = countTextTokens(formatClaim(claim));
572
+ if (included.length > 0 && tokenCount + nextTokens > maxTokens) continue;
573
+ included.push(claim);
574
+ tokenCount += nextTokens;
575
+ }
576
+
577
+ const cautions = new Set<string>();
578
+ const reasons: Record<string, string> = {};
579
+ for (const claim of included) {
580
+ reasons[claim.id] = 'task terms matched source-qualified claim';
581
+ if (claim.status === 'disputed') cautions.add('claim-conflict');
582
+ if (claim.status === 'unknown' || claim.reviewState === 'needs-review') {
583
+ cautions.add('claim-needs-review');
584
+ }
585
+ }
586
+ return { claims: included, cautions: [...cautions].sort(), tokenCount, reasons };
587
+ }