@tenphi/akno-core 0.1.1 → 0.3.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.
Files changed (101) hide show
  1. package/config/default.jsonc +22 -5
  2. package/dist/config/load.js +17 -0
  3. package/dist/config/load.js.map +1 -1
  4. package/dist/config/schema.d.ts +25 -1
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/config/schema.js +37 -2
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/doctor.d.ts +13 -0
  9. package/dist/doctor.d.ts.map +1 -1
  10. package/dist/doctor.js +18 -0
  11. package/dist/doctor.js.map +1 -1
  12. package/dist/index/derive.d.ts.map +1 -1
  13. package/dist/index/derive.js +26 -3
  14. package/dist/index/derive.js.map +1 -1
  15. package/dist/index/indexer.d.ts +12 -6
  16. package/dist/index/indexer.d.ts.map +1 -1
  17. package/dist/index/indexer.js +41 -24
  18. package/dist/index/indexer.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/ingest/fetch.d.ts +16 -13
  23. package/dist/ingest/fetch.d.ts.map +1 -1
  24. package/dist/ingest/fetch.js +176 -52
  25. package/dist/ingest/fetch.js.map +1 -1
  26. package/dist/kb/frontmatter.d.ts +15 -0
  27. package/dist/kb/frontmatter.d.ts.map +1 -1
  28. package/dist/kb/frontmatter.js +105 -9
  29. package/dist/kb/frontmatter.js.map +1 -1
  30. package/dist/maintenance/adopt.d.ts.map +1 -1
  31. package/dist/maintenance/adopt.js +2 -1
  32. package/dist/maintenance/adopt.js.map +1 -1
  33. package/dist/maintenance/brain-migration.d.ts +20 -0
  34. package/dist/maintenance/brain-migration.d.ts.map +1 -0
  35. package/dist/maintenance/brain-migration.js +234 -0
  36. package/dist/maintenance/brain-migration.js.map +1 -0
  37. package/dist/maintenance/curate.js +3 -3
  38. package/dist/maintenance/curate.js.map +1 -1
  39. package/dist/maintenance/dream.d.ts.map +1 -1
  40. package/dist/maintenance/dream.js +27 -17
  41. package/dist/maintenance/dream.js.map +1 -1
  42. package/dist/maintenance/managed-item-sources.d.ts +0 -1
  43. package/dist/maintenance/managed-item-sources.d.ts.map +1 -1
  44. package/dist/maintenance/managed-item-sources.js +0 -1
  45. package/dist/maintenance/managed-item-sources.js.map +1 -1
  46. package/dist/maintenance/managed-items.d.ts +1 -7
  47. package/dist/maintenance/managed-items.d.ts.map +1 -1
  48. package/dist/maintenance/managed-items.js +15 -23
  49. package/dist/maintenance/managed-items.js.map +1 -1
  50. package/dist/open.d.ts +3 -0
  51. package/dist/open.d.ts.map +1 -1
  52. package/dist/open.js +5 -0
  53. package/dist/open.js.map +1 -1
  54. package/dist/ops/forget.d.ts.map +1 -1
  55. package/dist/ops/forget.js +3 -0
  56. package/dist/ops/forget.js.map +1 -1
  57. package/dist/ops/ingest.d.ts.map +1 -1
  58. package/dist/ops/ingest.js +11 -5
  59. package/dist/ops/ingest.js.map +1 -1
  60. package/dist/ops/remember.d.ts.map +1 -1
  61. package/dist/ops/remember.js +47 -11
  62. package/dist/ops/remember.js.map +1 -1
  63. package/dist/ops/retain.d.ts +4 -0
  64. package/dist/ops/retain.d.ts.map +1 -0
  65. package/dist/ops/retain.js +743 -0
  66. package/dist/ops/retain.js.map +1 -0
  67. package/dist/ops/undo.d.ts +1 -1
  68. package/dist/ops/undo.d.ts.map +1 -1
  69. package/dist/ops/undo.js +20 -1
  70. package/dist/ops/undo.js.map +1 -1
  71. package/dist/ops/write.d.ts.map +1 -1
  72. package/dist/ops/write.js +11 -7
  73. package/dist/ops/write.js.map +1 -1
  74. package/dist/store/db.d.ts.map +1 -1
  75. package/dist/store/db.js +12 -1
  76. package/dist/store/db.js.map +1 -1
  77. package/dist/store/migrations.d.ts +8 -6
  78. package/dist/store/migrations.d.ts.map +1 -1
  79. package/dist/store/migrations.js +60 -8
  80. package/dist/store/migrations.js.map +1 -1
  81. package/dist/write/journal.d.ts +4 -8
  82. package/dist/write/journal.d.ts.map +1 -1
  83. package/dist/write/journal.js +215 -55
  84. package/dist/write/journal.js.map +1 -1
  85. package/dist/write/managed-memory.d.ts +45 -0
  86. package/dist/write/managed-memory.d.ts.map +1 -0
  87. package/dist/write/managed-memory.js +462 -0
  88. package/dist/write/managed-memory.js.map +1 -0
  89. package/dist/write/placement.d.ts +3 -2
  90. package/dist/write/placement.d.ts.map +1 -1
  91. package/dist/write/placement.js +3 -5
  92. package/dist/write/placement.js.map +1 -1
  93. package/dist/write/retain-supports.d.ts +6 -0
  94. package/dist/write/retain-supports.d.ts.map +1 -0
  95. package/dist/write/retain-supports.js +48 -0
  96. package/dist/write/retain-supports.js.map +1 -0
  97. package/dist/write/retain.d.ts +8 -2
  98. package/dist/write/retain.d.ts.map +1 -1
  99. package/dist/write/retain.js +30 -9
  100. package/dist/write/retain.js.map +1 -1
  101. package/package.json +2 -2
@@ -0,0 +1,743 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { RetainInput, } from '@tenphi/akno-protocol';
4
+ import { folderCatalog } from "../kb/folders.js";
5
+ import { parseFrontmatter, serializeYamlString } from "../kb/frontmatter.js";
6
+ import { parsePage } from "../kb/page.js";
7
+ import { isReserved } from "../reserved.js";
8
+ import { newPrefixedId, sha256 } from "../store/ids.js";
9
+ import { restoreFile, writeFileAtomic } from "../write/atomic.js";
10
+ import { detectConflict } from "../write/conflict.js";
11
+ import { fileEntry } from "../write/journal.js";
12
+ import { managedMemoryBlock, managedMemoryFingerprint, markerFromProvidedCandidate, parseManagedMemoryMarker, renderManagedMemoryMarker, renderManagedMemoryPayload, sameManagedMemorySemantics, } from "../write/managed-memory.js";
13
+ import { appendManagedBlock, managedSourceReference } from "../write/placement.js";
14
+ import { reconcileRetainManagedSources } from "../write/retain-supports.js";
15
+ import { normalizeSlug } from "./write.js";
16
+ export async function retain(ctx, rawInput) {
17
+ const input = RetainInput.parse(rawInput);
18
+ const results = [];
19
+ for (const source of input.sources) {
20
+ try {
21
+ results.push('input' in source
22
+ ? await retainProvidedExact(ctx, source, input.dry_run ?? false)
23
+ : await retractSource(ctx, source, input.dry_run ?? false));
24
+ }
25
+ catch (error) {
26
+ results.push({
27
+ source_id: source.source_id,
28
+ revision: source.revision,
29
+ outcome: 'held',
30
+ candidates: [],
31
+ note: `source apply failed: ${error instanceof Error ? error.message : String(error)}`,
32
+ });
33
+ }
34
+ }
35
+ const held = results.filter((result) => result.outcome === 'held' || result.outcome === 'revision_conflict').length;
36
+ const effective = results.filter((result) => result.outcome === 'ok').length;
37
+ return {
38
+ status: 'ok',
39
+ outcome: held > 0 && effective > 0 ? 'partial' : effective > 0 ? 'ok' : 'noop',
40
+ sources: results,
41
+ };
42
+ }
43
+ async function retainProvidedExact(ctx, source, dryRun) {
44
+ const sourceHash = sha256(JSON.stringify(source.input));
45
+ const requestHash = sha256(JSON.stringify(source));
46
+ const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
47
+ if (replay)
48
+ return replay;
49
+ const groupIssue = sourceGroupIssue(ctx, source.source_id, source.source_group);
50
+ if (groupIssue) {
51
+ return conflictResult(source.source_id, source.revision, groupIssue);
52
+ }
53
+ const receiptFingerprint = managedMemoryFingerprint(`receipt:${source.source_id}:${source.revision}:${requestHash}`);
54
+ const proofGroup = managedMemoryFingerprint(`proof:${source.source_group ?? source.source_id}`);
55
+ const stages = new Map();
56
+ const pendingSupports = [];
57
+ const candidateResults = [];
58
+ const candidateIds = new Set(source.retention.candidates.map((candidate) => candidate.candidate_id));
59
+ const candidateOrder = new Map(source.retention.candidates.map((candidate, index) => [candidate.candidate_id, index]));
60
+ const candidateMemoryIds = new Map();
61
+ const ordered = dependencyOrder(source.retention.candidates);
62
+ for (const candidate of source.retention.candidates) {
63
+ if (ordered.blocked.has(candidate.candidate_id)) {
64
+ candidateResults.push({
65
+ candidate_id: candidate.candidate_id,
66
+ outcome: 'held',
67
+ reason: 'candidate relation cycle or blocked dependency',
68
+ });
69
+ }
70
+ }
71
+ for (const candidate of ordered.candidates) {
72
+ const issue = candidateIssue(ctx, source, candidate, candidateIds);
73
+ if (issue) {
74
+ candidateResults.push({ candidate_id: candidate.candidate_id, outcome: 'held', reason: issue });
75
+ continue;
76
+ }
77
+ const missingTarget = (candidate.relations ?? []).find((relation) => 'candidate_id' in relation.target && !candidateMemoryIds.has(relation.target.candidate_id));
78
+ if (missingTarget && 'candidate_id' in missingTarget.target) {
79
+ candidateResults.push({
80
+ candidate_id: candidate.candidate_id,
81
+ outcome: 'held',
82
+ reason: `relation target candidate ${missingTarget.target.candidate_id} was not retained`,
83
+ });
84
+ continue;
85
+ }
86
+ const slug = normalizeSlug(candidate.destination.slug);
87
+ const stage = await pageStage(ctx, stages, slug, candidate.subject);
88
+ if ('issue' in stage) {
89
+ candidateResults.push({
90
+ candidate_id: candidate.candidate_id,
91
+ outcome: 'held',
92
+ slug,
93
+ reason: stage.issue,
94
+ });
95
+ continue;
96
+ }
97
+ const candidateFingerprint = managedMemoryFingerprint({
98
+ source: source.source_id,
99
+ revision: source.revision,
100
+ candidate: candidate.candidate_id,
101
+ semantics: candidate,
102
+ });
103
+ const support = {
104
+ receipt: receiptFingerprint,
105
+ candidate: candidateFingerprint,
106
+ proofGroup,
107
+ selection: 'provided',
108
+ };
109
+ const proposedId = newPrefixedId('mem');
110
+ const marker = markerFromProvidedCandidate(proposedId, candidate, support, candidateMemoryIds);
111
+ const payload = renderManagedMemoryPayload(candidate.text, marker);
112
+ const duplicate = managedBlocks(stage.after).find((block) => block.payload === payload && sameManagedMemorySemantics(block.marker, marker));
113
+ let memoryId = proposedId;
114
+ let outcome = 'written';
115
+ if (duplicate) {
116
+ memoryId = duplicate.marker.id;
117
+ const alreadySupported = duplicate.marker.supports.some((current) => current.receipt === support.receipt && current.candidate === support.candidate);
118
+ if (alreadySupported) {
119
+ outcome = 'duplicate';
120
+ }
121
+ else if (duplicate.marker.supports.length >= 8) {
122
+ candidateResults.push({
123
+ candidate_id: candidate.candidate_id,
124
+ outcome: 'held',
125
+ memory_id: memoryId,
126
+ slug,
127
+ reason: 'support_limit',
128
+ });
129
+ continue;
130
+ }
131
+ else {
132
+ duplicate.marker.supports.push(support);
133
+ stage.after = replaceLine(stage.after, duplicate.markerIndex, renderManagedMemoryMarker(duplicate.marker));
134
+ outcome = 'support_added';
135
+ }
136
+ }
137
+ else {
138
+ const row = ctx.store.db.prepare('SELECT id FROM pages WHERE slug = ?').get(slug);
139
+ if (stage.before !== null &&
140
+ candidate.destination.section &&
141
+ countSection(stage.after, candidate.destination.section) !== 1) {
142
+ candidateResults.push({
143
+ candidate_id: candidate.candidate_id,
144
+ outcome: 'held',
145
+ slug,
146
+ reason: 'exact destination section is absent or ambiguous',
147
+ });
148
+ continue;
149
+ }
150
+ if (row) {
151
+ const parsed = parsePage(stage.relPath, stage.after);
152
+ const conflict = detectConflict({
153
+ store: ctx.store,
154
+ pageId: row.id,
155
+ slug,
156
+ body: parsed.body,
157
+ bodyStartLine: parsed.bodyLine,
158
+ incoming: candidate.text,
159
+ });
160
+ if (conflict) {
161
+ candidateResults.push({
162
+ candidate_id: candidate.candidate_id,
163
+ outcome: 'held',
164
+ slug,
165
+ reason: 'conflict',
166
+ });
167
+ continue;
168
+ }
169
+ }
170
+ stage.after = appendBlock(stage.after, candidate.destination.section ?? 'Unsorted', managedMemoryBlock(marker, payload));
171
+ }
172
+ const evidence = sourceEvidence(candidate);
173
+ pendingSupports.push({
174
+ receipt_fingerprint: receiptFingerprint,
175
+ candidate_id: candidate.candidate_id,
176
+ candidate_fingerprint: candidateFingerprint,
177
+ proof_group: proofGroup,
178
+ memory_id: memoryId,
179
+ slug,
180
+ selection: 'provided',
181
+ evidence,
182
+ evidence_hash: sha256(evidence),
183
+ source_ref: managedSourceReference(source.locator ?? source.source_id),
184
+ origin: sourceOrigin(candidate.attribution.source_role),
185
+ input_hash: sourceHash,
186
+ });
187
+ candidateResults.push({
188
+ candidate_id: candidate.candidate_id,
189
+ outcome,
190
+ memory_id: memoryId,
191
+ slug,
192
+ });
193
+ candidateMemoryIds.set(candidate.candidate_id, memoryId);
194
+ }
195
+ candidateResults.sort((left, right) => candidateOrder.get(left.candidate_id) - candidateOrder.get(right.candidate_id));
196
+ const changed = [...stages.values()].filter((stage) => stage.before !== stage.after);
197
+ const preview = {
198
+ source_id: source.source_id,
199
+ revision: source.revision,
200
+ outcome: changed.length > 0 ? 'ok' : candidateResults.some((item) => item.outcome === 'held') ? 'held' : 'noop',
201
+ candidates: candidateResults,
202
+ ...(dryRun ? { note: 'dry run — nothing was written and no replay receipt was created' } : {}),
203
+ };
204
+ if (dryRun)
205
+ return preview;
206
+ const committed = await commitStages(ctx, changed, `retained ${candidateResults.filter((item) => ['written', 'support_added'].includes(item.outcome)).length} item(s) from one source revision`);
207
+ const changeId = committed?.changeId ?? null;
208
+ const result = { ...preview, ...(changeId ? { change_id: changeId } : {}) };
209
+ try {
210
+ persistReceipt(ctx, {
211
+ source,
212
+ sourceHash,
213
+ requestHash,
214
+ receiptFingerprint,
215
+ mode: 'provided_exact',
216
+ result,
217
+ changeId,
218
+ supports: pendingSupports,
219
+ retracted: [],
220
+ });
221
+ }
222
+ catch (error) {
223
+ await rollbackCommittedStages(ctx, committed, error);
224
+ }
225
+ await reindexStages(ctx, changed);
226
+ return result;
227
+ }
228
+ async function retractSource(ctx, source, dryRun) {
229
+ const sourceHash = sha256('retraction');
230
+ const requestHash = sha256(JSON.stringify(source));
231
+ const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
232
+ if (replay)
233
+ return replay;
234
+ const target = ctx.store.db
235
+ .prepare('SELECT * FROM retain_receipts WHERE source_id = ? AND revision = ?')
236
+ .get(source.source_id, source.retention.target_revision);
237
+ if (!target) {
238
+ return {
239
+ source_id: source.source_id,
240
+ revision: source.revision,
241
+ outcome: 'held',
242
+ candidates: [],
243
+ note: 'target_revision was not retained',
244
+ };
245
+ }
246
+ const selected = ctx.store.db
247
+ .prepare(`SELECT * FROM retain_supports
248
+ WHERE receipt_fingerprint = ? AND retracted_by IS NULL AND forgotten_by IS NULL
249
+ ORDER BY candidate_id`)
250
+ .all(target.receipt_fingerprint);
251
+ const wanted = source.retention.candidate_ids
252
+ ? new Set(source.retention.candidate_ids)
253
+ : new Set(selected.map((row) => row.candidate_id));
254
+ const stages = new Map();
255
+ const results = [];
256
+ const removed = [];
257
+ for (const candidateId of wanted) {
258
+ const support = selected.find((row) => row.candidate_id === candidateId);
259
+ if (!support) {
260
+ results.push({ candidate_id: candidateId, outcome: 'not_found' });
261
+ continue;
262
+ }
263
+ const stage = await existingPageStage(ctx, stages, support.slug);
264
+ if ('issue' in stage) {
265
+ results.push({ candidate_id: candidateId, outcome: 'held', reason: stage.issue });
266
+ continue;
267
+ }
268
+ const block = managedBlocks(stage.after).find((entry) => entry.marker.id === support.memory_id);
269
+ if (!block) {
270
+ results.push({ candidate_id: candidateId, outcome: 'held', reason: 'owned memory block changed' });
271
+ continue;
272
+ }
273
+ const nextSupports = block.marker.supports.filter((entry) => !(entry.receipt === support.receipt_fingerprint && entry.candidate === support.candidate_fingerprint));
274
+ if (nextSupports.length === block.marker.supports.length) {
275
+ results.push({ candidate_id: candidateId, outcome: 'held', reason: 'support marker changed' });
276
+ continue;
277
+ }
278
+ stage.after =
279
+ nextSupports.length > 0
280
+ ? replaceLine(stage.after, block.markerIndex, renderManagedMemoryMarker({ ...block.marker, supports: nextSupports }))
281
+ : removeBlock(stage.after, block.markerIndex, block.payloadIndex);
282
+ removed.push(support);
283
+ results.push({
284
+ candidate_id: candidateId,
285
+ outcome: 'retracted',
286
+ memory_id: support.memory_id,
287
+ slug: support.slug,
288
+ });
289
+ }
290
+ const changed = [...stages.values()].filter((stage) => stage.before !== stage.after);
291
+ const preview = {
292
+ source_id: source.source_id,
293
+ revision: source.revision,
294
+ outcome: changed.length > 0 ? 'ok' : results.some((item) => item.outcome === 'held') ? 'held' : 'noop',
295
+ candidates: results,
296
+ ...(dryRun ? { note: 'dry run — nothing was retracted and no replay receipt was created' } : {}),
297
+ };
298
+ if (dryRun)
299
+ return preview;
300
+ const committed = await commitStages(ctx, changed, `retracted ${removed.length} source support(s)`);
301
+ const changeId = committed?.changeId ?? null;
302
+ const result = { ...preview, ...(changeId ? { change_id: changeId } : {}) };
303
+ const receiptFingerprint = managedMemoryFingerprint(`receipt:${source.source_id}:${source.revision}:${requestHash}`);
304
+ try {
305
+ persistReceipt(ctx, {
306
+ source,
307
+ sourceHash,
308
+ requestHash,
309
+ receiptFingerprint,
310
+ mode: 'retract',
311
+ result,
312
+ changeId,
313
+ supports: [],
314
+ retracted: removed,
315
+ });
316
+ }
317
+ catch (error) {
318
+ await rollbackCommittedStages(ctx, committed, error);
319
+ }
320
+ await reindexStages(ctx, changed);
321
+ return result;
322
+ }
323
+ function replayResult(ctx, sourceId, revision, requestHash, sourceHash) {
324
+ const row = ctx.store.db
325
+ .prepare('SELECT * FROM retain_receipts WHERE source_id = ? AND revision = ?')
326
+ .get(sourceId, revision);
327
+ if (!row)
328
+ return null;
329
+ if (row.request_hash !== requestHash || row.source_hash !== sourceHash) {
330
+ return conflictResult(sourceId, revision, 'revision_conflict');
331
+ }
332
+ try {
333
+ const stored = JSON.parse(row.result);
334
+ const retired = new Set(ctx.store.db
335
+ .prepare(`SELECT candidate_id FROM retain_supports
336
+ WHERE receipt_fingerprint = ? AND (retracted_by IS NOT NULL OR forgotten_by IS NOT NULL)`)
337
+ .all(row.receipt_fingerprint).map((support) => support.candidate_id));
338
+ return {
339
+ ...stored,
340
+ outcome: 'replayed',
341
+ candidates: stored.candidates.map((candidate) => retired.has(candidate.candidate_id) ? { ...candidate, outcome: 'retracted' } : candidate),
342
+ note: retired.size > 0
343
+ ? 'identical source revision already completed; some support was later retracted or forgotten'
344
+ : 'identical source revision already completed',
345
+ };
346
+ }
347
+ catch {
348
+ return conflictResult(sourceId, revision, 'stored receipt is unreadable');
349
+ }
350
+ }
351
+ function conflictResult(sourceId, revision, note) {
352
+ return { source_id: sourceId, revision, outcome: 'revision_conflict', candidates: [], note };
353
+ }
354
+ function sourceGroupIssue(ctx, sourceId, group) {
355
+ const row = ctx.store.db
356
+ .prepare(`SELECT source_group FROM retain_receipts
357
+ WHERE source_id = ? AND source_group IS NOT NULL ORDER BY created_at LIMIT 1`)
358
+ .get(sourceId);
359
+ return row && row.source_group !== (group ?? sourceId) ? 'source_group_conflict' : null;
360
+ }
361
+ function candidateIssue(ctx, source, candidate, candidateIds) {
362
+ if (candidate.text.includes('\n') || candidate.text.includes('\0'))
363
+ return 'candidate text must be one line';
364
+ if (candidate.subject.includes('\r') ||
365
+ candidate.subject.includes('\n') ||
366
+ candidate.subject.includes('\0')) {
367
+ return 'candidate subject must be one line';
368
+ }
369
+ if (candidate.destination.section &&
370
+ (candidate.destination.section.includes('\r') ||
371
+ candidate.destination.section.includes('\n') ||
372
+ candidate.destination.section.includes('\0') ||
373
+ !/^[^#[\]<>]{1,100}$/.test(candidate.destination.section) ||
374
+ candidate.destination.section.trim().length === 0)) {
375
+ return 'destination section is not a safe heading';
376
+ }
377
+ const supportIssue = spansIssue(source, candidate.support);
378
+ if (supportIssue)
379
+ return `support: ${supportIssue}`;
380
+ const frameIssue = spansIssue(source, candidate.discourse_frame);
381
+ if (frameIssue)
382
+ return `discourse_frame: ${frameIssue}`;
383
+ const frameKeys = new Set(candidate.discourse_frame.map(spanKey));
384
+ if (candidate.support.some((span) => !frameKeys.has(spanKey(span)))) {
385
+ return 'discourse_frame must contain every support span';
386
+ }
387
+ const attributionIssue = structuredAttributionIssue(source, candidate);
388
+ if (attributionIssue)
389
+ return attributionIssue;
390
+ if (sourceEvidence(candidate).length > 1200)
391
+ return 'discourse frame exceeds the evidence limit';
392
+ if (candidate.subject_ref &&
393
+ !ctx.store.db.prepare('SELECT 1 FROM graph_entities WHERE id = ?').get(candidate.subject_ref.entity_id)) {
394
+ return 'subject_ref does not name an existing canonical entity';
395
+ }
396
+ for (const relation of candidate.relations ?? []) {
397
+ const issue = spansIssue(source, relation.support);
398
+ if (issue)
399
+ return `relation support: ${issue}`;
400
+ if ('candidate_id' in relation.target && !candidateIds.has(relation.target.candidate_id)) {
401
+ return `relation target candidate ${relation.target.candidate_id} is not in this source revision`;
402
+ }
403
+ if ('memory_id' in relation.target &&
404
+ !ctx.store.db
405
+ .prepare('SELECT 1 FROM retain_supports WHERE memory_id = ? AND retracted_by IS NULL AND forgotten_by IS NULL')
406
+ .get(relation.target.memory_id)) {
407
+ return `relation target memory ${relation.target.memory_id} does not exist`;
408
+ }
409
+ if ('fact_id' in relation.target &&
410
+ !ctx.store.db.prepare('SELECT 1 FROM facts WHERE id = ?').get(relation.target.fact_id)) {
411
+ return `relation target fact ${relation.target.fact_id} does not exist`;
412
+ }
413
+ }
414
+ if (candidate.epistemic.basis === 'cited_evidence') {
415
+ for (const evidence of candidate.epistemic.evidence ?? []) {
416
+ if (!durableEvidenceExists(ctx, evidence))
417
+ return 'cited evidence is unavailable';
418
+ }
419
+ }
420
+ return null;
421
+ }
422
+ function structuredAttributionIssue(source, candidate) {
423
+ if (!('items' in source.input))
424
+ return null;
425
+ const items = new Map(source.input.items.map((item) => [item.item_id, item]));
426
+ for (const span of candidate.support) {
427
+ const item = span.item_id ? items.get(span.item_id) : undefined;
428
+ if (!item)
429
+ continue;
430
+ const itemRole = item.role === 'system' ? 'unknown' : item.role;
431
+ if (itemRole &&
432
+ candidate.attribution.source_role !== 'unknown' &&
433
+ itemRole !== candidate.attribution.source_role) {
434
+ return 'source attribution conflicts with the structured support item role';
435
+ }
436
+ if (item.speaker &&
437
+ candidate.attribution.source_speaker &&
438
+ item.speaker !== candidate.attribution.source_speaker) {
439
+ return 'source attribution conflicts with the structured support item speaker';
440
+ }
441
+ }
442
+ return null;
443
+ }
444
+ function spansIssue(source, spans) {
445
+ if ('text' in source.input) {
446
+ for (const span of spans) {
447
+ if (span.item_id)
448
+ return 'item_id is invalid for unstructured text';
449
+ if (occurrences(source.input.text, span.quote) !== 1)
450
+ return 'quote is missing or ambiguous';
451
+ }
452
+ return null;
453
+ }
454
+ const items = new Map(source.input.items.map((item) => [item.item_id, item]));
455
+ if (items.size !== source.input.items.length)
456
+ return 'source item ids are not unique';
457
+ for (const span of spans) {
458
+ if (!span.item_id)
459
+ return 'structured source spans require item_id';
460
+ const item = items.get(span.item_id);
461
+ if (!item || occurrences(item.text, span.quote) !== 1)
462
+ return 'quote is missing or ambiguous in its item';
463
+ }
464
+ return null;
465
+ }
466
+ function durableEvidenceExists(ctx, evidence) {
467
+ if ('fact_id' in evidence)
468
+ return Boolean(ctx.store.db.prepare('SELECT 1 FROM facts WHERE id = ?').get(evidence.fact_id));
469
+ if ('page_slug' in evidence) {
470
+ return Boolean(ctx.store.db
471
+ .prepare(`SELECT 1 FROM facts f JOIN pages p ON p.id = f.page_id
472
+ WHERE p.slug = ? AND f.source_line_hash = ?`)
473
+ .get(evidence.page_slug, evidence.line_hash));
474
+ }
475
+ if ('document_id' in evidence) {
476
+ return Boolean(ctx.store.db
477
+ .prepare('SELECT 1 FROM chunks WHERE document_id = ? AND CAST(id AS TEXT) = ?')
478
+ .get(evidence.document_id, evidence.passage_id));
479
+ }
480
+ return Boolean(ctx.store.db.prepare('SELECT 1 FROM changes WHERE id = ?').get(evidence.journal_event_id));
481
+ }
482
+ async function pageStage(ctx, stages, slug, subject) {
483
+ const cached = stages.get(slug);
484
+ if (cached)
485
+ return cached;
486
+ if (isReserved(slug, ctx.config))
487
+ return { issue: 'reserved_path' };
488
+ const row = ctx.store.db
489
+ .prepare('SELECT rel_path, role, remember_management FROM pages WHERE slug = ?')
490
+ .get(slug);
491
+ if (row && (row.role !== 'knowledge' || row.remember_management !== 'integrate')) {
492
+ return { issue: 'destination is not knowledge + remember: integrate' };
493
+ }
494
+ if (!row) {
495
+ const parent = slug.slice(0, slug.lastIndexOf('/'));
496
+ const admitted = folderCatalog(ctx.config, ctx.store).some((folder) => folder.path === parent &&
497
+ folder.role === 'knowledge' &&
498
+ folder.remember === 'integrate' &&
499
+ folder.creatable);
500
+ if (!admitted)
501
+ return { issue: 'destination folder is not admitted for managed memory' };
502
+ }
503
+ const relPath = row?.rel_path ?? `${slug}.md`;
504
+ const absolute = path.join(ctx.config.aknoPath, relPath);
505
+ const before = row ? await fsp.readFile(absolute, 'utf8').catch(() => null) : null;
506
+ if (row && before === null)
507
+ return { issue: 'destination page is unreadable' };
508
+ if (!row && (await fsp.stat(absolute).catch(() => null))) {
509
+ return { issue: 'unindexed_page_exists' };
510
+ }
511
+ const title = safeTitle(subject) || slugTitle(slug);
512
+ const stage = {
513
+ slug,
514
+ relPath,
515
+ before,
516
+ after: before ?? newManagedPage(title),
517
+ };
518
+ stages.set(slug, stage);
519
+ return stage;
520
+ }
521
+ async function existingPageStage(ctx, stages, slug) {
522
+ const cached = stages.get(slug);
523
+ if (cached)
524
+ return cached;
525
+ const row = ctx.store.db.prepare('SELECT rel_path FROM pages WHERE slug = ?').get(slug);
526
+ if (!row)
527
+ return { issue: 'owned page is unavailable' };
528
+ const before = await fsp.readFile(path.join(ctx.config.aknoPath, row.rel_path), 'utf8').catch(() => null);
529
+ if (before === null)
530
+ return { issue: 'owned page is unreadable' };
531
+ const stage = { slug, relPath: row.rel_path, before, after: before };
532
+ stages.set(slug, stage);
533
+ return stage;
534
+ }
535
+ function appendBlock(content, heading, block) {
536
+ const frontmatter = parseFrontmatter(content);
537
+ return (content.slice(0, frontmatter.bodyOffset) +
538
+ appendManagedBlock(content.slice(frontmatter.bodyOffset), heading, block));
539
+ }
540
+ function countSection(content, heading) {
541
+ const frontmatter = parseFrontmatter(content);
542
+ const wanted = heading.trim().toLowerCase();
543
+ return content
544
+ .slice(frontmatter.bodyOffset)
545
+ .split('\n')
546
+ .filter((line) => /^##\s+(.+?)\s*$/.exec(line)?.[1]?.trim().toLowerCase() === wanted).length;
547
+ }
548
+ function managedBlocks(content) {
549
+ const frontmatter = parseFrontmatter(content);
550
+ const lines = content.slice(frontmatter.bodyOffset).split('\n');
551
+ const out = [];
552
+ for (let index = 0; index < lines.length; index++) {
553
+ const marker = parseManagedMemoryMarker(lines[index]);
554
+ if (!marker)
555
+ continue;
556
+ let payloadIndex = null;
557
+ for (let next = index + 1; next < lines.length; next++) {
558
+ const line = lines[next];
559
+ if (!line.trim())
560
+ continue;
561
+ if (/^\s*(?:<!--|#{1,6}\s)/.test(line))
562
+ break;
563
+ payloadIndex = next;
564
+ break;
565
+ }
566
+ if (payloadIndex !== null) {
567
+ out.push({ marker, markerIndex: index, payloadIndex, payload: lines[payloadIndex] });
568
+ }
569
+ }
570
+ return out;
571
+ }
572
+ function replaceLine(content, bodyIndex, replacement) {
573
+ const frontmatter = parseFrontmatter(content);
574
+ const prefix = content.slice(0, frontmatter.bodyOffset);
575
+ const lines = content.slice(frontmatter.bodyOffset).split('\n');
576
+ lines[bodyIndex] = replacement;
577
+ return prefix + lines.join('\n');
578
+ }
579
+ function removeBlock(content, markerIndex, payloadIndex) {
580
+ const frontmatter = parseFrontmatter(content);
581
+ const prefix = content.slice(0, frontmatter.bodyOffset);
582
+ const lines = content.slice(frontmatter.bodyOffset).split('\n');
583
+ lines.splice(markerIndex, payloadIndex - markerIndex + 1);
584
+ return prefix + lines.join('\n').replace(/\n{3,}/g, '\n\n');
585
+ }
586
+ async function commitStages(ctx, stages, summary) {
587
+ if (stages.length === 0)
588
+ return null;
589
+ const files = [];
590
+ try {
591
+ for (const stage of stages) {
592
+ files.push(fileEntry(await writeFileAtomic(ctx.config.aknoPath, stage.relPath, stage.after)));
593
+ }
594
+ const changeId = ctx.journal.record({ actor: ctx.actor, op: 'retain', summary, files });
595
+ return { changeId, files };
596
+ }
597
+ catch (error) {
598
+ const failures = [];
599
+ for (const file of [...files].reverse()) {
600
+ try {
601
+ await restoreFile(ctx.config.aknoPath, file.relPath, file.before);
602
+ }
603
+ catch (rollback) {
604
+ failures.push(rollback instanceof Error ? rollback.message : String(rollback));
605
+ }
606
+ }
607
+ if (failures.length > 0) {
608
+ throw new Error(`retain failed and rollback was incomplete: ${error instanceof Error ? error.message : String(error)}; ${failures.join('; ')}`, { cause: error });
609
+ }
610
+ throw error;
611
+ }
612
+ }
613
+ async function rollbackCommittedStages(ctx, committed, cause) {
614
+ if (!committed)
615
+ throw cause;
616
+ const failures = [];
617
+ for (const file of [...committed.files].reverse()) {
618
+ try {
619
+ await restoreFile(ctx.config.aknoPath, file.relPath, file.before);
620
+ }
621
+ catch (rollback) {
622
+ failures.push(rollback instanceof Error ? rollback.message : String(rollback));
623
+ }
624
+ }
625
+ if (failures.length === 0) {
626
+ ctx.store.db
627
+ .prepare("UPDATE changes SET status = 'undone', undone_at = ? WHERE id = ?")
628
+ .run(new Date().toISOString(), committed.changeId);
629
+ }
630
+ throw new Error(`retain receipt failed${failures.length > 0 ? ' and file rollback was incomplete' : '; brain bytes were rolled back'}: ${cause instanceof Error ? cause.message : String(cause)}${failures.length > 0 ? `; ${failures.join('; ')}` : ''}`, { cause });
631
+ }
632
+ async function reindexStages(ctx, stages) {
633
+ if (stages.length === 0)
634
+ return;
635
+ const paths = stages.map((stage) => stage.relPath);
636
+ await ctx.indexer.runForeground({ only: paths, modelPaths: [] });
637
+ ctx.derive.schedule(paths);
638
+ }
639
+ function persistReceipt(ctx, input) {
640
+ const now = new Date().toISOString();
641
+ ctx.store.transaction(() => {
642
+ ctx.store.db
643
+ .prepare(`INSERT INTO retain_receipts(
644
+ source_id, revision, request_hash, source_hash, source_group, receipt_fingerprint,
645
+ mode, result, change_id, created_at
646
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
647
+ .run(input.source.source_id, input.source.revision, input.requestHash, input.sourceHash, 'source_group' in input.source ? (input.source.source_group ?? input.source.source_id) : null, input.receiptFingerprint, input.mode, JSON.stringify(input.result), input.changeId, now);
648
+ const supportInsert = ctx.store.db.prepare(`INSERT INTO retain_supports(
649
+ receipt_fingerprint, candidate_id, candidate_fingerprint, proof_group, memory_id,
650
+ slug, selection, source_ref, origin, input_hash, evidence, evidence_hash,
651
+ retracted_by, forgotten_by
652
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL)`);
653
+ for (const support of input.supports) {
654
+ supportInsert.run(support.receipt_fingerprint, support.candidate_id, support.candidate_fingerprint, support.proof_group, support.memory_id, support.slug, support.selection, support.source_ref, support.origin, support.input_hash, support.evidence, support.evidence_hash);
655
+ }
656
+ const retract = ctx.store.db.prepare(`UPDATE retain_supports SET retracted_by = ?
657
+ WHERE receipt_fingerprint = ? AND candidate_id = ? AND retracted_by IS NULL`);
658
+ for (const support of input.retracted) {
659
+ retract.run(input.receiptFingerprint, support.receipt_fingerprint, support.candidate_id);
660
+ }
661
+ });
662
+ reconcileRetainManagedSources(ctx, [
663
+ ...input.supports.map((support) => support.memory_id),
664
+ ...input.retracted.map((support) => support.memory_id),
665
+ ]);
666
+ }
667
+ function sourceEvidence(candidate) {
668
+ return candidate.discourse_frame.map((span) => span.quote).join('\n…\n');
669
+ }
670
+ function sourceOrigin(role) {
671
+ return role === 'user' || role === 'assistant' ? role : 'unknown';
672
+ }
673
+ function dependencyOrder(candidates) {
674
+ const byId = new Map(candidates.map((candidate) => [candidate.candidate_id, candidate]));
675
+ const ordered = [];
676
+ const blocked = new Set();
677
+ const visited = new Set();
678
+ const visiting = [];
679
+ const visit = (candidate) => {
680
+ if (visited.has(candidate.candidate_id))
681
+ return !blocked.has(candidate.candidate_id);
682
+ const cycleAt = visiting.indexOf(candidate.candidate_id);
683
+ if (cycleAt >= 0) {
684
+ for (const id of visiting.slice(cycleAt))
685
+ blocked.add(id);
686
+ return false;
687
+ }
688
+ visiting.push(candidate.candidate_id);
689
+ let ready = true;
690
+ for (const relation of candidate.relations ?? []) {
691
+ if (!('candidate_id' in relation.target))
692
+ continue;
693
+ const target = byId.get(relation.target.candidate_id);
694
+ if (!target || !visit(target))
695
+ ready = false;
696
+ }
697
+ visiting.pop();
698
+ visited.add(candidate.candidate_id);
699
+ if (!ready)
700
+ blocked.add(candidate.candidate_id);
701
+ else
702
+ ordered.push(candidate);
703
+ return ready;
704
+ };
705
+ for (const candidate of candidates)
706
+ visit(candidate);
707
+ return { candidates: ordered, blocked };
708
+ }
709
+ function spanKey(span) {
710
+ return `${span.item_id ?? ''}\0${span.quote}`;
711
+ }
712
+ function occurrences(text, needle) {
713
+ let count = 0;
714
+ let at = 0;
715
+ while ((at = text.indexOf(needle, at)) >= 0) {
716
+ count += 1;
717
+ at += Math.max(1, needle.length);
718
+ }
719
+ return count;
720
+ }
721
+ function newManagedPage(title) {
722
+ return `---\ntitle: ${serializeYamlString(title, 'title')}\nakno:\n role: knowledge\n management:\n remember: integrate\n---\n\n# ${title}\n`;
723
+ }
724
+ function slugTitle(slug) {
725
+ const name = slug.slice(slug.lastIndexOf('/') + 1).replaceAll('-', ' ');
726
+ return name.replace(/\b\w/g, (letter) => letter.toUpperCase());
727
+ }
728
+ function safeTitle(value) {
729
+ return value
730
+ .replaceAll('\r', ' ')
731
+ .replaceAll('\n', ' ')
732
+ .replaceAll('\0', ' ')
733
+ .replaceAll('#', ' ')
734
+ .replaceAll('[', ' ')
735
+ .replaceAll(']', ' ')
736
+ .replaceAll('<', ' ')
737
+ .replaceAll('>', ' ')
738
+ .replaceAll('`', ' ')
739
+ .replace(/\s+/g, ' ')
740
+ .trim()
741
+ .slice(0, 120);
742
+ }
743
+ //# sourceMappingURL=retain.js.map