@tenphi/akno-core 0.7.0 → 0.9.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/README.md +4 -1
- package/config/default.jsonc +6 -3
- package/dist/config/load.js +1 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index/chunk.d.ts.map +1 -1
- package/dist/index/chunk.js +54 -17
- package/dist/index/chunk.js.map +1 -1
- package/dist/index/graph.d.ts +2 -0
- package/dist/index/graph.d.ts.map +1 -1
- package/dist/index/graph.js +81 -3
- package/dist/index/graph.js.map +1 -1
- package/dist/index/indexer.d.ts +6 -0
- package/dist/index/indexer.d.ts.map +1 -1
- package/dist/index/indexer.js +32 -0
- package/dist/index/indexer.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kb/managed-lines.d.ts +3 -0
- package/dist/kb/managed-lines.d.ts.map +1 -1
- package/dist/kb/managed-lines.js +21 -1
- package/dist/kb/managed-lines.js.map +1 -1
- package/dist/maintenance/dream.d.ts +3 -0
- package/dist/maintenance/dream.d.ts.map +1 -1
- package/dist/maintenance/dream.js +5 -0
- package/dist/maintenance/dream.js.map +1 -1
- package/dist/maintenance/log.d.ts.map +1 -1
- package/dist/maintenance/log.js +1 -0
- package/dist/maintenance/log.js.map +1 -1
- package/dist/memory/intent.d.ts +22 -0
- package/dist/memory/intent.d.ts.map +1 -0
- package/dist/memory/intent.js +59 -0
- package/dist/memory/intent.js.map +1 -0
- package/dist/memory/projection.d.ts +23 -0
- package/dist/memory/projection.d.ts.map +1 -0
- package/dist/memory/projection.js +151 -0
- package/dist/memory/projection.js.map +1 -0
- package/dist/models/client.d.ts.map +1 -1
- package/dist/models/client.js +8 -3
- package/dist/models/client.js.map +1 -1
- package/dist/models/provider-api.d.ts.map +1 -1
- package/dist/models/provider-api.js +4 -2
- package/dist/models/provider-api.js.map +1 -1
- package/dist/models/provider-request.d.ts +22 -0
- package/dist/models/provider-request.d.ts.map +1 -0
- package/dist/models/provider-request.js +95 -0
- package/dist/models/provider-request.js.map +1 -0
- package/dist/ops/answer.d.ts.map +1 -1
- package/dist/ops/answer.js +113 -8
- package/dist/ops/answer.js.map +1 -1
- package/dist/ops/context.d.ts.map +1 -1
- package/dist/ops/context.js +21 -3
- package/dist/ops/context.js.map +1 -1
- package/dist/ops/graph.d.ts.map +1 -1
- package/dist/ops/graph.js +79 -6
- package/dist/ops/graph.js.map +1 -1
- package/dist/ops/read.js +1 -1
- package/dist/ops/read.js.map +1 -1
- package/dist/ops/recall.d.ts.map +1 -1
- package/dist/ops/recall.js +120 -69
- package/dist/ops/recall.js.map +1 -1
- package/dist/ops/remember.d.ts.map +1 -1
- package/dist/ops/remember.js +68 -180
- package/dist/ops/remember.js.map +1 -1
- package/dist/ops/retain.d.ts +31 -1
- package/dist/ops/retain.d.ts.map +1 -1
- package/dist/ops/retain.js +488 -70
- package/dist/ops/retain.js.map +1 -1
- package/dist/recall/assemble.d.ts +3 -1
- package/dist/recall/assemble.d.ts.map +1 -1
- package/dist/recall/assemble.js +27 -4
- package/dist/recall/assemble.js.map +1 -1
- package/dist/recall/graph-arm.js +9 -1
- package/dist/recall/graph-arm.js.map +1 -1
- package/dist/recall/search.d.ts.map +1 -1
- package/dist/recall/search.js +2 -1
- package/dist/recall/search.js.map +1 -1
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +7 -1
- package/dist/store/db.js.map +1 -1
- package/dist/store/migrations.d.ts +3 -1
- package/dist/store/migrations.d.ts.map +1 -1
- package/dist/store/migrations.js +103 -1
- package/dist/store/migrations.js.map +1 -1
- package/dist/write/retain-supports.d.ts +16 -0
- package/dist/write/retain-supports.d.ts.map +1 -1
- package/dist/write/retain-supports.js +65 -0
- package/dist/write/retain-supports.js.map +1 -1
- package/package.json +2 -2
package/dist/ops/retain.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fsp from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { RetainInput, } from '@tenphi/akno-protocol';
|
|
3
|
+
import { AknoError, RetainInput, } from '@tenphi/akno-protocol';
|
|
4
4
|
import { ModelClient } from "../models/client.js";
|
|
5
5
|
import { folderCatalog } from "../kb/folders.js";
|
|
6
6
|
import { parseFrontmatter, serializeYamlString } from "../kb/frontmatter.js";
|
|
7
7
|
import { parsePage } from "../kb/page.js";
|
|
8
|
+
import { contentWords } from "../kb/words.js";
|
|
9
|
+
import { declaringRule, effectiveRule } from "../rules/compile.js";
|
|
8
10
|
import { isReserved } from "../reserved.js";
|
|
9
11
|
import { newPrefixedId, sha256 } from "../store/ids.js";
|
|
10
12
|
import { restoreFile, writeFileAtomic } from "../write/atomic.js";
|
|
@@ -14,25 +16,33 @@ import { managedMemoryBlock, managedMemoryFingerprint, markerFromProvidedCandida
|
|
|
14
16
|
import { appendManagedBlock, managedSourceReference, placeManagedItems } from "../write/placement.js";
|
|
15
17
|
import { modelCallReceipt, runRetain, } from "../write/retain.js";
|
|
16
18
|
import { resolveRememberFallback } from "../write/remember-fallback.js";
|
|
17
|
-
import { reconcileRetainManagedSources } from "../write/retain-supports.js";
|
|
19
|
+
import { pruneRetainEvidence, reconcileRetainManagedSources } from "../write/retain-supports.js";
|
|
18
20
|
import { routeAutomaticCandidate } from "./remember.js";
|
|
19
|
-
import {
|
|
21
|
+
import { read } from "./read.js";
|
|
22
|
+
import { normalizeSlug, titleFromSlug } from "./write.js";
|
|
20
23
|
export async function retain(ctx, rawInput) {
|
|
21
24
|
const input = RetainInput.parse(rawInput);
|
|
22
25
|
const results = [];
|
|
23
26
|
for (const source of input.sources) {
|
|
24
27
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
if (!('input' in source)) {
|
|
29
|
+
results.push(await retractSource(ctx, source, input.dry_run ?? false));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const resolved = await resolveRetainSource(ctx, source);
|
|
33
|
+
if ('result' in resolved) {
|
|
34
|
+
results.push(resolved.result);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
results.push(source.retention.mode === 'extract'
|
|
38
|
+
? await retainExtracted(ctx, resolved, input.dry_run ?? false)
|
|
39
|
+
: await retainCandidates(ctx, resolved, source.retention.candidates, {
|
|
40
|
+
dryRun: input.dry_run ?? false,
|
|
41
|
+
selection: 'provided',
|
|
42
|
+
placement: source.retention.placement,
|
|
43
|
+
initialResults: [],
|
|
44
|
+
modelUsage: { extraction: null, verification: null, placement: [] },
|
|
45
|
+
}));
|
|
36
46
|
}
|
|
37
47
|
catch (error) {
|
|
38
48
|
results.push({
|
|
@@ -53,24 +63,137 @@ export async function retain(ctx, rawInput) {
|
|
|
53
63
|
const effective = results.filter((result) => result.outcome === 'ok').length;
|
|
54
64
|
const degraded = [...new Set(results.flatMap((result) => result.degraded ?? []))];
|
|
55
65
|
const allEmpty = results.length > 0 && results.every((result) => result.status === 'empty');
|
|
66
|
+
const allUnavailable = results.length > 0 && results.every((result) => result.status === 'unavailable');
|
|
56
67
|
return {
|
|
57
|
-
status: degraded.length > 0 ? 'degraded' : allEmpty ? 'empty' : 'ok',
|
|
68
|
+
status: allUnavailable ? 'unavailable' : degraded.length > 0 ? 'degraded' : allEmpty ? 'empty' : 'ok',
|
|
58
69
|
...(degraded.length > 0 ? { degraded } : {}),
|
|
59
70
|
outcome: incomplete && effective > 0 ? 'partial' : effective > 0 ? 'ok' : 'noop',
|
|
60
71
|
sources: results,
|
|
61
72
|
};
|
|
62
73
|
}
|
|
63
|
-
async function
|
|
74
|
+
async function resolveRetainSource(ctx, requested) {
|
|
75
|
+
const input = requested.input;
|
|
76
|
+
if ('text' in input || 'items' in input) {
|
|
77
|
+
const kind = 'text' in input ? 'text' : 'items';
|
|
78
|
+
return {
|
|
79
|
+
source: { ...requested, input },
|
|
80
|
+
requested,
|
|
81
|
+
sourceHash: sha256(JSON.stringify(input)),
|
|
82
|
+
sourceRef: requested.locator ?? requested.source_id,
|
|
83
|
+
binding: {
|
|
84
|
+
kind,
|
|
85
|
+
availability: 'available',
|
|
86
|
+
reextractable: requested.preserve_source !== undefined,
|
|
87
|
+
...(requested.preserve_source
|
|
88
|
+
? { preservedSlug: normalizeSlug(requested.preserve_source.slug) }
|
|
89
|
+
: {}),
|
|
90
|
+
},
|
|
91
|
+
degraded: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if ('page_slug' in input) {
|
|
95
|
+
const slug = normalizeSlug(input.page_slug);
|
|
96
|
+
const row = ctx.store.db.prepare('SELECT rel_path, role FROM pages WHERE slug = ?').get(slug);
|
|
97
|
+
if (!row)
|
|
98
|
+
return { result: unavailableSourceResult(requested, 'page', slug, 'source page is not indexed') };
|
|
99
|
+
if (row.role !== 'source') {
|
|
100
|
+
return {
|
|
101
|
+
result: unavailableSourceResult(requested, 'page', slug, 'page_slug accepts only a page whose effective role is source', 'validation_failed'),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const bytes = await fsp.readFile(path.join(ctx.config.aknoPath, row.rel_path), 'utf8').catch(() => null);
|
|
105
|
+
if (bytes === null) {
|
|
106
|
+
return { result: unavailableSourceResult(requested, 'page', slug, 'source page is unreadable') };
|
|
107
|
+
}
|
|
108
|
+
const sourceText = parsePage(row.rel_path, bytes).body;
|
|
109
|
+
if (!sourceText.trim()) {
|
|
110
|
+
return { result: unavailableSourceResult(requested, 'page', slug, 'source page has no readable body') };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
source: { ...requested, input: { text: sourceText } },
|
|
114
|
+
requested,
|
|
115
|
+
sourceHash: sha256(bytes),
|
|
116
|
+
sourceRef: requested.locator ?? slug,
|
|
117
|
+
binding: { kind: 'page', availability: 'available', reextractable: true, reference: slug },
|
|
118
|
+
degraded: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const output = await read(ctx, { document: input.document_id });
|
|
123
|
+
if (!output.document) {
|
|
124
|
+
return {
|
|
125
|
+
result: unavailableSourceResult(requested, 'document', input.document_id, 'document reference did not resolve to readable document state'),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const document = output.document;
|
|
129
|
+
if (!document.text) {
|
|
130
|
+
return {
|
|
131
|
+
result: unavailableSourceResult(requested, 'document', document.id, output.note ?? 'document has no readable extraction or rendition'),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const degraded = output.status === 'ok' ? [] : (output.degraded ?? ['no_document_text']);
|
|
135
|
+
const documentParts = ctx.store.db
|
|
136
|
+
.prepare(`SELECT rel_path, sha256 FROM documents
|
|
137
|
+
WHERE group_key = (
|
|
138
|
+
SELECT group_key FROM documents WHERE id = ?
|
|
139
|
+
) AND renders IS NULL
|
|
140
|
+
ORDER BY part`)
|
|
141
|
+
.all(document.id);
|
|
142
|
+
return {
|
|
143
|
+
source: { ...requested, input: { text: document.text } },
|
|
144
|
+
requested,
|
|
145
|
+
sourceHash: sha256(JSON.stringify({ id: document.id, parts: documentParts, text: document.text })),
|
|
146
|
+
sourceRef: requested.locator ?? document.rel_path,
|
|
147
|
+
binding: {
|
|
148
|
+
kind: 'document',
|
|
149
|
+
availability: output.status === 'ok' ? 'available' : 'degraded',
|
|
150
|
+
reextractable: true,
|
|
151
|
+
reference: document.id,
|
|
152
|
+
},
|
|
153
|
+
degraded,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const note = error instanceof AknoError ? error.message : `document source failed: ${String(error)}`;
|
|
158
|
+
return { result: unavailableSourceResult(requested, 'document', input.document_id, note) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function unavailableSourceResult(source, kind, reference, note, reasonCode = 'source_unavailable') {
|
|
162
|
+
return {
|
|
163
|
+
source_id: source.source_id,
|
|
164
|
+
revision: source.revision,
|
|
165
|
+
outcome: 'held',
|
|
166
|
+
status: 'unavailable',
|
|
167
|
+
reason_code: reasonCode,
|
|
168
|
+
candidates: [],
|
|
169
|
+
source: { kind, availability: 'unavailable', reextractable: true, reference },
|
|
170
|
+
note,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function sourceResultBinding(binding) {
|
|
174
|
+
return {
|
|
175
|
+
kind: binding.kind,
|
|
176
|
+
availability: binding.availability,
|
|
177
|
+
reextractable: binding.reextractable,
|
|
178
|
+
...(binding.reference ? { reference: binding.reference } : {}),
|
|
179
|
+
...(binding.preservedSlug ? { preserved_slug: binding.preservedSlug } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function retainExtracted(ctx, resolved, dryRun) {
|
|
183
|
+
const source = resolved.source;
|
|
64
184
|
if (source.retention.mode !== 'extract')
|
|
65
185
|
throw new Error('retain extract received a provided source');
|
|
66
|
-
const sourceHash =
|
|
67
|
-
const requestHash = sha256(JSON.stringify(
|
|
186
|
+
const sourceHash = resolved.sourceHash;
|
|
187
|
+
const requestHash = sha256(JSON.stringify(resolved.requested));
|
|
68
188
|
const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
|
|
69
189
|
if (replay)
|
|
70
190
|
return replay;
|
|
71
191
|
const groupIssue = sourceGroupIssue(ctx, source.source_id, source.source_group);
|
|
72
192
|
if (groupIssue)
|
|
73
193
|
return conflictResult(source.source_id, source.revision, groupIssue);
|
|
194
|
+
const correctionIssue = validateCorrectionTarget(ctx, source);
|
|
195
|
+
if (correctionIssue)
|
|
196
|
+
return correctionHoldResult(resolved, correctionIssue);
|
|
74
197
|
const curator = retentionModel(ctx);
|
|
75
198
|
const text = 'text' in source.input ? source.input.text : source.input.items.map((item) => item.text).join('\n');
|
|
76
199
|
const extracted = await runRetain(text, curator, {
|
|
@@ -83,7 +206,7 @@ async function retainExtracted(ctx, source, dryRun) {
|
|
|
83
206
|
revision: source.revision,
|
|
84
207
|
});
|
|
85
208
|
if (extracted.sourceHold) {
|
|
86
|
-
return retainCandidates(ctx,
|
|
209
|
+
return retainCandidates(ctx, resolved, [], {
|
|
87
210
|
dryRun,
|
|
88
211
|
selection: 'extracted',
|
|
89
212
|
placement: 'automatic',
|
|
@@ -94,18 +217,20 @@ async function retainExtracted(ctx, source, dryRun) {
|
|
|
94
217
|
}
|
|
95
218
|
const initialResults = extracted.held.map(heldCandidateResult);
|
|
96
219
|
if (extracted.error) {
|
|
97
|
-
return {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
220
|
+
return retainCandidates(ctx, resolved, [], {
|
|
221
|
+
dryRun,
|
|
222
|
+
selection: 'extracted',
|
|
223
|
+
placement: 'automatic',
|
|
224
|
+
initialResults,
|
|
225
|
+
modelUsage: { ...extracted.modelUsage, placement: [] },
|
|
226
|
+
additionalDegraded: [extracted.degradedReason ?? 'derive_failed'],
|
|
227
|
+
sourceHold: {
|
|
228
|
+
reason_code: initialResults[0]?.reason_code ?? 'apply_failed',
|
|
229
|
+
reason: `automatic retention could not complete: ${extracted.error}`,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
107
232
|
}
|
|
108
|
-
return retainCandidates(ctx,
|
|
233
|
+
return retainCandidates(ctx, resolved, extracted.candidates, {
|
|
109
234
|
dryRun,
|
|
110
235
|
selection: 'extracted',
|
|
111
236
|
placement: 'automatic',
|
|
@@ -113,16 +238,20 @@ async function retainExtracted(ctx, source, dryRun) {
|
|
|
113
238
|
modelUsage: { ...extracted.modelUsage, placement: [] },
|
|
114
239
|
});
|
|
115
240
|
}
|
|
116
|
-
async function retainCandidates(ctx,
|
|
117
|
-
const
|
|
118
|
-
const
|
|
241
|
+
async function retainCandidates(ctx, resolvedSource, suppliedCandidates, options) {
|
|
242
|
+
const source = resolvedSource.source;
|
|
243
|
+
const sourceHash = resolvedSource.sourceHash;
|
|
244
|
+
const requestHash = sha256(JSON.stringify(resolvedSource.requested));
|
|
119
245
|
const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
|
|
120
246
|
if (replay)
|
|
121
247
|
return replay;
|
|
122
248
|
const groupIssue = sourceGroupIssue(ctx, source.source_id, source.source_group);
|
|
123
249
|
if (groupIssue)
|
|
124
250
|
return conflictResult(source.source_id, source.revision, groupIssue);
|
|
125
|
-
const
|
|
251
|
+
const correctionIssue = validateCorrectionTarget(ctx, source);
|
|
252
|
+
if (correctionIssue)
|
|
253
|
+
return correctionHoldResult(resolvedSource, correctionIssue);
|
|
254
|
+
const resolved = options.placement === 'automatic' && !options.skipAutomaticRouting
|
|
126
255
|
? await resolveAutomaticCandidates(ctx, suppliedCandidates)
|
|
127
256
|
: {
|
|
128
257
|
candidates: suppliedCandidates.filter(hasDestination),
|
|
@@ -139,9 +268,74 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
139
268
|
const receiptFingerprint = managedMemoryFingerprint(`receipt:${source.source_id}:${source.revision}:${requestHash}`);
|
|
140
269
|
const proofGroup = managedMemoryFingerprint(`proof:${source.source_group ?? source.source_id}`);
|
|
141
270
|
const stages = new Map();
|
|
271
|
+
const effectiveBinding = options.sourceHold && resolvedSource.binding.preservedSlug
|
|
272
|
+
? {
|
|
273
|
+
kind: resolvedSource.binding.kind,
|
|
274
|
+
availability: resolvedSource.binding.availability,
|
|
275
|
+
reextractable: false,
|
|
276
|
+
...(resolvedSource.binding.reference ? { reference: resolvedSource.binding.reference } : {}),
|
|
277
|
+
}
|
|
278
|
+
: resolvedSource.binding;
|
|
279
|
+
if (effectiveBinding.preservedSlug) {
|
|
280
|
+
if (candidates.some((candidate) => normalizeSlug(candidate.destination.slug) === effectiveBinding.preservedSlug)) {
|
|
281
|
+
return {
|
|
282
|
+
source_id: source.source_id,
|
|
283
|
+
revision: source.revision,
|
|
284
|
+
outcome: 'held',
|
|
285
|
+
status: 'ok',
|
|
286
|
+
reason_code: 'validation_failed',
|
|
287
|
+
candidates: [],
|
|
288
|
+
source: sourceResultBinding({
|
|
289
|
+
kind: effectiveBinding.kind,
|
|
290
|
+
availability: effectiveBinding.availability,
|
|
291
|
+
reextractable: false,
|
|
292
|
+
...(effectiveBinding.reference ? { reference: effectiveBinding.reference } : {}),
|
|
293
|
+
}),
|
|
294
|
+
note: 'a preserved source page cannot also be a retained-memory destination',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const preserved = await preservedSourceStage(ctx, stages, effectiveBinding.preservedSlug, resolvedSource.requested.input);
|
|
298
|
+
if ('issue' in preserved) {
|
|
299
|
+
return {
|
|
300
|
+
source_id: source.source_id,
|
|
301
|
+
revision: source.revision,
|
|
302
|
+
outcome: 'held',
|
|
303
|
+
status: 'ok',
|
|
304
|
+
reason_code: preserved.reasonCode,
|
|
305
|
+
candidates: [],
|
|
306
|
+
source: sourceResultBinding({
|
|
307
|
+
kind: resolvedSource.binding.kind,
|
|
308
|
+
availability: resolvedSource.binding.availability,
|
|
309
|
+
reextractable: false,
|
|
310
|
+
...(resolvedSource.binding.reference ? { reference: resolvedSource.binding.reference } : {}),
|
|
311
|
+
}),
|
|
312
|
+
note: preserved.issue,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const correction = source.retracts
|
|
317
|
+
? await stageRetractions(ctx, source.source_id, source.retracts.target_revision, source.retracts.candidate_ids, stages)
|
|
318
|
+
: { results: [], removed: [], valid: true };
|
|
319
|
+
if (!correction.valid) {
|
|
320
|
+
return {
|
|
321
|
+
source_id: source.source_id,
|
|
322
|
+
revision: source.revision,
|
|
323
|
+
outcome: 'held',
|
|
324
|
+
status: 'ok',
|
|
325
|
+
reason_code: 'conflict',
|
|
326
|
+
candidates: correction.results,
|
|
327
|
+
source: sourceResultBinding(effectiveBinding),
|
|
328
|
+
note: 'correction targets no longer match their owned memory blocks; nothing was changed',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
142
331
|
const pendingSupports = [];
|
|
143
|
-
const
|
|
144
|
-
const
|
|
332
|
+
const suppliedCandidateIds = new Set(suppliedCandidates.map((candidate) => candidate.candidate_id));
|
|
333
|
+
const candidateResults = [
|
|
334
|
+
...correction.results.filter((result) => !suppliedCandidateIds.has(result.candidate_id)),
|
|
335
|
+
...options.initialResults,
|
|
336
|
+
...resolved.held,
|
|
337
|
+
];
|
|
338
|
+
const candidateIds = suppliedCandidateIds;
|
|
145
339
|
const candidateOrder = new Map([
|
|
146
340
|
...options.initialResults.map((result) => result.candidate_id),
|
|
147
341
|
...suppliedCandidates.map((candidate) => candidate.candidate_id),
|
|
@@ -263,7 +457,7 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
263
457
|
outcome: 'held',
|
|
264
458
|
slug,
|
|
265
459
|
reason_code: 'conflict',
|
|
266
|
-
reason:
|
|
460
|
+
reason: conflict.existing,
|
|
267
461
|
});
|
|
268
462
|
continue;
|
|
269
463
|
}
|
|
@@ -300,7 +494,7 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
300
494
|
selection: options.selection,
|
|
301
495
|
evidence,
|
|
302
496
|
evidence_hash: sha256(evidence),
|
|
303
|
-
source_ref: managedSourceReference(
|
|
497
|
+
source_ref: managedSourceReference(resolvedSource.sourceRef),
|
|
304
498
|
origin: sourceOrigin(candidate.attribution.source_role),
|
|
305
499
|
input_hash: sourceHash,
|
|
306
500
|
});
|
|
@@ -313,9 +507,42 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
313
507
|
(candidateOrder.get(right.candidate_id) ?? Number.MAX_SAFE_INTEGER));
|
|
314
508
|
const changed = [...stages.values()].filter((stage) => stage.before !== stage.after);
|
|
315
509
|
const placementDegraded = candidateResults.some((result) => result.reason_code === 'placement_degraded');
|
|
316
|
-
const degraded =
|
|
317
|
-
|
|
318
|
-
|
|
510
|
+
const degraded = [
|
|
511
|
+
...resolvedSource.degraded,
|
|
512
|
+
...(options.additionalDegraded ?? []),
|
|
513
|
+
...(placementDegraded
|
|
514
|
+
? [retentionModel(ctx).available ? 'derive_failed' : 'no_derive_model']
|
|
515
|
+
: []),
|
|
516
|
+
];
|
|
517
|
+
const acceptedCandidateIds = new Set(candidateResults
|
|
518
|
+
.filter((result) => ['written', 'support_added', 'duplicate'].includes(result.outcome))
|
|
519
|
+
.map((result) => result.candidate_id));
|
|
520
|
+
const correctionAdmissionFailed = source.retracts !== undefined &&
|
|
521
|
+
(suppliedCandidateIds.size === 0 ||
|
|
522
|
+
[...suppliedCandidateIds].some((candidateId) => !acceptedCandidateIds.has(candidateId)) ||
|
|
523
|
+
candidateResults.some((result) => result.outcome === 'held' || result.outcome === 'not_found'));
|
|
524
|
+
if (correctionAdmissionFailed) {
|
|
525
|
+
return {
|
|
526
|
+
source_id: source.source_id,
|
|
527
|
+
revision: source.revision,
|
|
528
|
+
outcome: 'held',
|
|
529
|
+
status: degraded.length > 0 ? 'degraded' : 'ok',
|
|
530
|
+
reason_code: 'validation_failed',
|
|
531
|
+
candidates: candidateResults.map((result) => result.outcome === 'held' || result.outcome === 'not_found'
|
|
532
|
+
? result
|
|
533
|
+
: {
|
|
534
|
+
candidate_id: result.candidate_id,
|
|
535
|
+
outcome: 'held',
|
|
536
|
+
...(result.memory_id ? { memory_id: result.memory_id } : {}),
|
|
537
|
+
...(result.slug ? { slug: result.slug } : {}),
|
|
538
|
+
reason_code: 'validation_failed',
|
|
539
|
+
reason: 'not applied because the compound correction was not fully admissible',
|
|
540
|
+
}),
|
|
541
|
+
source: sourceResultBinding(effectiveBinding),
|
|
542
|
+
...(degraded.length > 0 ? { degraded } : {}),
|
|
543
|
+
note: 'compound correction was not fully admissible; neither removal nor replacement was written',
|
|
544
|
+
};
|
|
545
|
+
}
|
|
319
546
|
const preview = {
|
|
320
547
|
source_id: source.source_id,
|
|
321
548
|
revision: source.revision,
|
|
@@ -327,6 +554,7 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
327
554
|
? 'held'
|
|
328
555
|
: 'noop',
|
|
329
556
|
candidates: candidateResults,
|
|
557
|
+
source: sourceResultBinding(effectiveBinding),
|
|
330
558
|
...(options.sourceHold ? { reason_code: options.sourceHold.reason_code } : {}),
|
|
331
559
|
status: degraded.length > 0
|
|
332
560
|
? 'degraded'
|
|
@@ -348,7 +576,7 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
348
576
|
};
|
|
349
577
|
if (options.dryRun)
|
|
350
578
|
return preview;
|
|
351
|
-
const committed = await commitStages(ctx, changed,
|
|
579
|
+
const committed = await commitStages(ctx, changed, `${options.journalOp === 'remember' ? 'remembered' : 'retained'} ${candidateResults.filter((item) => ['written', 'support_added'].includes(item.outcome)).length} item(s) from one source revision`, options.journalOp);
|
|
352
580
|
const changeId = committed?.changeId ?? null;
|
|
353
581
|
const result = { ...preview, ...(changeId ? { change_id: changeId } : {}) };
|
|
354
582
|
try {
|
|
@@ -365,15 +593,58 @@ async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
|
365
593
|
result,
|
|
366
594
|
changeId,
|
|
367
595
|
supports: pendingSupports,
|
|
368
|
-
retracted:
|
|
596
|
+
retracted: correction.removed,
|
|
597
|
+
binding: effectiveBinding,
|
|
369
598
|
});
|
|
370
599
|
}
|
|
371
600
|
catch (error) {
|
|
372
601
|
await rollbackCommittedStages(ctx, committed, error);
|
|
373
602
|
}
|
|
374
|
-
await reindexStages(ctx, changed);
|
|
603
|
+
options.onIndexed?.(await reindexStages(ctx, changed));
|
|
375
604
|
return result;
|
|
376
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Compatibility adapter entrypoint: remember keeps its public routing/action response, while
|
|
608
|
+
* validation, owned-block staging, source archives, journaling, receipts, and indexing execute
|
|
609
|
+
* through the same engine as keyed retain.
|
|
610
|
+
*/
|
|
611
|
+
export async function retainRememberCandidates(ctx, input) {
|
|
612
|
+
const inputHash = sha256(input.text);
|
|
613
|
+
const sourceId = `remember:${newPrefixedId('src')}`;
|
|
614
|
+
const requested = {
|
|
615
|
+
source_id: sourceId,
|
|
616
|
+
revision: '1',
|
|
617
|
+
source_group: `remember:${managedMemoryFingerprint(`${input.source ?? 'remember'}:${inputHash}`)}`,
|
|
618
|
+
source_kind: 'conversation',
|
|
619
|
+
...(input.mentionedAt ? { mentioned_at: input.mentionedAt } : {}),
|
|
620
|
+
...(input.timezone ? { timezone: input.timezone } : {}),
|
|
621
|
+
...(input.source ? { locator: input.source } : {}),
|
|
622
|
+
input: { text: input.text },
|
|
623
|
+
retention: { mode: 'extract', ...(input.mission ? { mission: input.mission } : {}) },
|
|
624
|
+
};
|
|
625
|
+
const resolved = {
|
|
626
|
+
source: requested,
|
|
627
|
+
requested,
|
|
628
|
+
sourceHash: inputHash,
|
|
629
|
+
sourceRef: input.source ?? 'remember',
|
|
630
|
+
binding: { kind: 'text', availability: 'available', reextractable: false },
|
|
631
|
+
degraded: [],
|
|
632
|
+
};
|
|
633
|
+
let factsAdded = 0;
|
|
634
|
+
const result = await retainCandidates(ctx, resolved, input.candidates, {
|
|
635
|
+
dryRun: input.dryRun,
|
|
636
|
+
selection: 'extracted',
|
|
637
|
+
placement: 'automatic',
|
|
638
|
+
skipAutomaticRouting: true,
|
|
639
|
+
journalOp: 'remember',
|
|
640
|
+
onIndexed: (factsDerived) => {
|
|
641
|
+
factsAdded = factsDerived;
|
|
642
|
+
},
|
|
643
|
+
initialResults: [...input.held],
|
|
644
|
+
modelUsage: { ...input.modelUsage, placement: [] },
|
|
645
|
+
});
|
|
646
|
+
return { result, factsAdded };
|
|
647
|
+
}
|
|
377
648
|
async function resolveAutomaticCandidates(ctx, candidates) {
|
|
378
649
|
const resolved = [];
|
|
379
650
|
const held = [];
|
|
@@ -435,34 +706,45 @@ function heldCandidateResult(candidate) {
|
|
|
435
706
|
reason: candidate.reason,
|
|
436
707
|
};
|
|
437
708
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
709
|
+
function validateCorrectionTarget(ctx, source) {
|
|
710
|
+
if (!source.retracts)
|
|
711
|
+
return null;
|
|
712
|
+
const target = ctx.store.db
|
|
713
|
+
.prepare('SELECT receipt_fingerprint FROM retain_receipts WHERE source_id = ? AND revision = ?')
|
|
714
|
+
.get(source.source_id, source.retracts.target_revision);
|
|
715
|
+
if (!target)
|
|
716
|
+
return 'correction target_revision was not retained for this source';
|
|
717
|
+
const active = new Set(ctx.store.db
|
|
718
|
+
.prepare(`SELECT candidate_id FROM retain_supports
|
|
719
|
+
WHERE receipt_fingerprint = ? AND retracted_by IS NULL AND forgotten_by IS NULL`)
|
|
720
|
+
.all(target.receipt_fingerprint).map((row) => row.candidate_id));
|
|
721
|
+
const missing = source.retracts.candidate_ids.find((candidateId) => !active.has(candidateId));
|
|
722
|
+
return missing ? `correction target candidate ${missing} is not active` : null;
|
|
723
|
+
}
|
|
724
|
+
function correctionHoldResult(source, note) {
|
|
725
|
+
return {
|
|
726
|
+
source_id: source.source.source_id,
|
|
727
|
+
revision: source.source.revision,
|
|
728
|
+
outcome: 'held',
|
|
729
|
+
status: 'ok',
|
|
730
|
+
reason_code: 'conflict',
|
|
731
|
+
candidates: [],
|
|
732
|
+
source: sourceResultBinding(source.binding),
|
|
733
|
+
note,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async function stageRetractions(ctx, sourceId, targetRevision, candidateIds, stages) {
|
|
444
737
|
const target = ctx.store.db
|
|
445
738
|
.prepare('SELECT * FROM retain_receipts WHERE source_id = ? AND revision = ?')
|
|
446
|
-
.get(
|
|
447
|
-
if (!target)
|
|
448
|
-
return {
|
|
449
|
-
source_id: source.source_id,
|
|
450
|
-
revision: source.revision,
|
|
451
|
-
outcome: 'held',
|
|
452
|
-
reason_code: 'source_unavailable',
|
|
453
|
-
candidates: [],
|
|
454
|
-
note: 'target_revision was not retained',
|
|
455
|
-
};
|
|
456
|
-
}
|
|
739
|
+
.get(sourceId, targetRevision);
|
|
740
|
+
if (!target)
|
|
741
|
+
return { results: [], removed: [], valid: false };
|
|
457
742
|
const selected = ctx.store.db
|
|
458
743
|
.prepare(`SELECT * FROM retain_supports
|
|
459
744
|
WHERE receipt_fingerprint = ? AND retracted_by IS NULL AND forgotten_by IS NULL
|
|
460
745
|
ORDER BY candidate_id`)
|
|
461
746
|
.all(target.receipt_fingerprint);
|
|
462
|
-
const wanted =
|
|
463
|
-
? new Set(source.retention.candidate_ids)
|
|
464
|
-
: new Set(selected.map((row) => row.candidate_id));
|
|
465
|
-
const stages = new Map();
|
|
747
|
+
const wanted = candidateIds ?? selected.map((row) => row.candidate_id);
|
|
466
748
|
const results = [];
|
|
467
749
|
const removed = [];
|
|
468
750
|
for (const candidateId of wanted) {
|
|
@@ -513,6 +795,46 @@ async function retractSource(ctx, source, dryRun) {
|
|
|
513
795
|
slug: support.slug,
|
|
514
796
|
});
|
|
515
797
|
}
|
|
798
|
+
return {
|
|
799
|
+
results,
|
|
800
|
+
removed,
|
|
801
|
+
valid: results.length === wanted.length && results.every((result) => result.outcome === 'retracted'),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
async function retractSource(ctx, source, dryRun) {
|
|
805
|
+
const sourceHash = sha256('retraction');
|
|
806
|
+
const requestHash = sha256(JSON.stringify(source));
|
|
807
|
+
const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
|
|
808
|
+
if (replay)
|
|
809
|
+
return replay;
|
|
810
|
+
const targetExists = ctx.store.db
|
|
811
|
+
.prepare('SELECT 1 FROM retain_receipts WHERE source_id = ? AND revision = ?')
|
|
812
|
+
.get(source.source_id, source.retention.target_revision);
|
|
813
|
+
if (!targetExists) {
|
|
814
|
+
return {
|
|
815
|
+
source_id: source.source_id,
|
|
816
|
+
revision: source.revision,
|
|
817
|
+
outcome: 'held',
|
|
818
|
+
reason_code: 'source_unavailable',
|
|
819
|
+
candidates: [],
|
|
820
|
+
note: 'target_revision was not retained',
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const stages = new Map();
|
|
824
|
+
const staged = await stageRetractions(ctx, source.source_id, source.retention.target_revision, source.retention.candidate_ids, stages);
|
|
825
|
+
const results = staged.results;
|
|
826
|
+
const removed = staged.removed;
|
|
827
|
+
if (!staged.valid) {
|
|
828
|
+
return {
|
|
829
|
+
source_id: source.source_id,
|
|
830
|
+
revision: source.revision,
|
|
831
|
+
outcome: 'held',
|
|
832
|
+
status: 'ok',
|
|
833
|
+
reason_code: 'conflict',
|
|
834
|
+
candidates: results,
|
|
835
|
+
note: 'one or more retraction targets no longer match owned memory; nothing was changed',
|
|
836
|
+
};
|
|
837
|
+
}
|
|
516
838
|
const changed = [...stages.values()].filter((stage) => stage.before !== stage.after);
|
|
517
839
|
const preview = {
|
|
518
840
|
source_id: source.source_id,
|
|
@@ -758,7 +1080,7 @@ async function pageStage(ctx, stages, slug, subject) {
|
|
|
758
1080
|
if (!row && (await fsp.stat(absolute).catch(() => null))) {
|
|
759
1081
|
return { issue: 'unindexed_page_exists' };
|
|
760
1082
|
}
|
|
761
|
-
const title =
|
|
1083
|
+
const title = managedPageTitle(subject, slug);
|
|
762
1084
|
const stage = {
|
|
763
1085
|
slug,
|
|
764
1086
|
relPath,
|
|
@@ -768,6 +1090,80 @@ async function pageStage(ctx, stages, slug, subject) {
|
|
|
768
1090
|
stages.set(slug, stage);
|
|
769
1091
|
return stage;
|
|
770
1092
|
}
|
|
1093
|
+
async function preservedSourceStage(ctx, stages, slug, input) {
|
|
1094
|
+
if (!('text' in input || 'items' in input)) {
|
|
1095
|
+
return { issue: 'only inline source input can be preserved', reasonCode: 'validation_failed' };
|
|
1096
|
+
}
|
|
1097
|
+
if (isReserved(slug, ctx.config)) {
|
|
1098
|
+
return { issue: 'preserved source destination is reserved', reasonCode: 'no_writable_destination' };
|
|
1099
|
+
}
|
|
1100
|
+
const content = serializePreservedSource(slug, input);
|
|
1101
|
+
const row = ctx.store.db
|
|
1102
|
+
.prepare('SELECT rel_path, role, remember_management FROM pages WHERE slug = ?')
|
|
1103
|
+
.get(slug);
|
|
1104
|
+
if (row && (row.role !== 'source' || row.remember_management !== 'deny')) {
|
|
1105
|
+
return {
|
|
1106
|
+
issue: 'preserved source destination must be source + remember: deny',
|
|
1107
|
+
reasonCode: 'no_writable_destination',
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const relPath = row?.rel_path ?? `${slug}.md`;
|
|
1111
|
+
const absolute = path.join(ctx.config.aknoPath, relPath);
|
|
1112
|
+
if (row) {
|
|
1113
|
+
const before = await fsp.readFile(absolute, 'utf8').catch(() => null);
|
|
1114
|
+
if (before === null) {
|
|
1115
|
+
return { issue: 'preserved source destination is unreadable', reasonCode: 'source_unavailable' };
|
|
1116
|
+
}
|
|
1117
|
+
if (before !== content) {
|
|
1118
|
+
return {
|
|
1119
|
+
issue: 'preserved source destination already contains different bytes',
|
|
1120
|
+
reasonCode: 'conflict',
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
const stage = { slug, relPath, before, after: before };
|
|
1124
|
+
stages.set(slug, stage);
|
|
1125
|
+
return stage;
|
|
1126
|
+
}
|
|
1127
|
+
if (await fsp.stat(absolute).catch(() => null)) {
|
|
1128
|
+
return { issue: 'an unindexed page exists at the preserved source destination', reasonCode: 'conflict' };
|
|
1129
|
+
}
|
|
1130
|
+
const policy = effectiveRule(slug, ctx.config.rules);
|
|
1131
|
+
const roleRule = declaringRule(slug, ctx.config.rules, 'role');
|
|
1132
|
+
const rememberRule = declaringRule(slug, ctx.config.rules, 'remember');
|
|
1133
|
+
if (policy.role !== 'source' || policy.remember !== 'deny' || !roleRule || !rememberRule) {
|
|
1134
|
+
return {
|
|
1135
|
+
issue: 'preserved source folder must explicitly declare role: source and remember: deny',
|
|
1136
|
+
reasonCode: 'no_writable_destination',
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
const gate = ctx.gate.check(slug, ctx.actor);
|
|
1140
|
+
if (!gate.allowed) {
|
|
1141
|
+
return {
|
|
1142
|
+
issue: `preserved source folder is not declared: ${gate.requiresFolder.folder}`,
|
|
1143
|
+
reasonCode: 'no_writable_destination',
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
const stage = { slug, relPath, before: null, after: content };
|
|
1147
|
+
stages.set(slug, stage);
|
|
1148
|
+
return stage;
|
|
1149
|
+
}
|
|
1150
|
+
function serializePreservedSource(slug, input) {
|
|
1151
|
+
const title = slugTitle(slug);
|
|
1152
|
+
const body = 'text' in input
|
|
1153
|
+
? input.text
|
|
1154
|
+
: input.items
|
|
1155
|
+
.map((item) => {
|
|
1156
|
+
const attributes = [
|
|
1157
|
+
`id=${encodeURIComponent(item.item_id)}`,
|
|
1158
|
+
...(item.role ? [`role=${item.role}`] : []),
|
|
1159
|
+
...(item.speaker ? [`speaker=${encodeURIComponent(item.speaker)}`] : []),
|
|
1160
|
+
...(item.mentioned_at ? [`mentioned-at=${encodeURIComponent(item.mentioned_at)}`] : []),
|
|
1161
|
+
];
|
|
1162
|
+
return `<!-- akno:source-item ${attributes.join(' ')} -->\n${item.text}`;
|
|
1163
|
+
})
|
|
1164
|
+
.join('\n\n');
|
|
1165
|
+
return `---\ntitle: ${serializeYamlString(title, 'title')}\nakno:\n role: source\n management:\n remember: deny\n---\n\n# ${title}\n\n## Source\n\n${body}\n`;
|
|
1166
|
+
}
|
|
771
1167
|
async function existingPageStage(ctx, stages, slug) {
|
|
772
1168
|
const cached = stages.get(slug);
|
|
773
1169
|
if (cached)
|
|
@@ -833,7 +1229,7 @@ function removeBlock(content, markerIndex, payloadIndex) {
|
|
|
833
1229
|
lines.splice(markerIndex, payloadIndex - markerIndex + 1);
|
|
834
1230
|
return prefix + lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
835
1231
|
}
|
|
836
|
-
async function commitStages(ctx, stages, summary) {
|
|
1232
|
+
async function commitStages(ctx, stages, summary, op = 'retain') {
|
|
837
1233
|
if (stages.length === 0)
|
|
838
1234
|
return null;
|
|
839
1235
|
const files = [];
|
|
@@ -841,7 +1237,7 @@ async function commitStages(ctx, stages, summary) {
|
|
|
841
1237
|
for (const stage of stages) {
|
|
842
1238
|
files.push(fileEntry(await writeFileAtomic(ctx.config.aknoPath, stage.relPath, stage.after)));
|
|
843
1239
|
}
|
|
844
|
-
const changeId = ctx.journal.record({ actor: ctx.actor, op
|
|
1240
|
+
const changeId = ctx.journal.record({ actor: ctx.actor, op, summary, files });
|
|
845
1241
|
return { changeId, files };
|
|
846
1242
|
}
|
|
847
1243
|
catch (error) {
|
|
@@ -881,10 +1277,11 @@ async function rollbackCommittedStages(ctx, committed, cause) {
|
|
|
881
1277
|
}
|
|
882
1278
|
async function reindexStages(ctx, stages) {
|
|
883
1279
|
if (stages.length === 0)
|
|
884
|
-
return;
|
|
1280
|
+
return 0;
|
|
885
1281
|
const paths = stages.map((stage) => stage.relPath);
|
|
886
|
-
await ctx.indexer.runForeground({ only: paths, modelPaths: [] });
|
|
1282
|
+
const report = await ctx.indexer.runForeground({ only: paths, modelPaths: [] });
|
|
887
1283
|
ctx.derive.schedule(paths);
|
|
1284
|
+
return report.factsDerived;
|
|
888
1285
|
}
|
|
889
1286
|
function persistReceipt(ctx, input) {
|
|
890
1287
|
const now = new Date().toISOString();
|
|
@@ -908,11 +1305,21 @@ function persistReceipt(ctx, input) {
|
|
|
908
1305
|
for (const support of input.retracted) {
|
|
909
1306
|
retract.run(input.receiptFingerprint, support.receipt_fingerprint, support.candidate_id);
|
|
910
1307
|
}
|
|
1308
|
+
if (input.binding) {
|
|
1309
|
+
ctx.store.db
|
|
1310
|
+
.prepare(`INSERT INTO retain_source_bindings(
|
|
1311
|
+
receipt_fingerprint, input_kind, input_ref, preserved_slug,
|
|
1312
|
+
availability, reextractable, created_at
|
|
1313
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
1314
|
+
.run(input.receiptFingerprint, input.binding.kind, input.binding.reference ?? null, input.binding.preservedSlug ?? null, input.binding.availability, input.binding.reextractable ? 1 : 0, now);
|
|
1315
|
+
}
|
|
911
1316
|
});
|
|
912
1317
|
reconcileRetainManagedSources(ctx, [
|
|
913
1318
|
...input.supports.map((support) => support.memory_id),
|
|
914
1319
|
...input.retracted.map((support) => support.memory_id),
|
|
915
1320
|
]);
|
|
1321
|
+
if (input.retracted.length > 0)
|
|
1322
|
+
pruneRetainEvidence(ctx, { apply: true });
|
|
916
1323
|
}
|
|
917
1324
|
function sourceEvidence(candidate) {
|
|
918
1325
|
return candidate.discourse_frame.map((span) => span.quote).join('\n…\n');
|
|
@@ -975,6 +1382,17 @@ function slugTitle(slug) {
|
|
|
975
1382
|
const name = slug.slice(slug.lastIndexOf('/') + 1).replaceAll('-', ' ');
|
|
976
1383
|
return name.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
977
1384
|
}
|
|
1385
|
+
function managedPageTitle(subject, slug) {
|
|
1386
|
+
const cleaned = safeTitle(subject);
|
|
1387
|
+
if (!cleaned)
|
|
1388
|
+
return titleFromSlug(slug);
|
|
1389
|
+
const named = contentWords(slug.slice(slug.lastIndexOf('/') + 1));
|
|
1390
|
+
for (const word of contentWords(cleaned)) {
|
|
1391
|
+
if (named.has(word))
|
|
1392
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
1393
|
+
}
|
|
1394
|
+
return titleFromSlug(slug);
|
|
1395
|
+
}
|
|
978
1396
|
function safeTitle(value) {
|
|
979
1397
|
return value
|
|
980
1398
|
.replaceAll('\r', ' ')
|