@tenphi/akno-core 0.4.0 → 0.6.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/dist/index/derive.d.ts.map +1 -1
- package/dist/index/derive.js +1 -10
- package/dist/index/derive.js.map +1 -1
- package/dist/index/indexer.d.ts +3 -0
- package/dist/index/indexer.d.ts.map +1 -1
- package/dist/index/indexer.js +33 -3
- package/dist/index/indexer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kb/managed-lines.d.ts +7 -0
- package/dist/kb/managed-lines.d.ts.map +1 -0
- package/dist/kb/managed-lines.js +52 -0
- package/dist/kb/managed-lines.js.map +1 -0
- package/dist/maintenance/conflicts.d.ts +1 -1
- package/dist/ops/answer.d.ts.map +1 -1
- package/dist/ops/answer.js +49 -4
- package/dist/ops/answer.js.map +1 -1
- package/dist/ops/context.d.ts.map +1 -1
- package/dist/ops/context.js +49 -19
- package/dist/ops/context.js.map +1 -1
- package/dist/ops/read.d.ts.map +1 -1
- package/dist/ops/read.js +2 -1
- package/dist/ops/read.js.map +1 -1
- package/dist/ops/remember.d.ts +57 -0
- package/dist/ops/remember.d.ts.map +1 -1
- package/dist/ops/remember.js +40 -38
- package/dist/ops/remember.js.map +1 -1
- package/dist/ops/retain.d.ts.map +1 -1
- package/dist/ops/retain.js +281 -31
- package/dist/ops/retain.js.map +1 -1
- package/dist/ops/timeline.d.ts +2 -5
- package/dist/ops/timeline.d.ts.map +1 -1
- package/dist/ops/timeline.js +334 -82
- package/dist/ops/timeline.js.map +1 -1
- package/dist/recall/assemble.d.ts.map +1 -1
- package/dist/recall/assemble.js +4 -2
- package/dist/recall/assemble.js.map +1 -1
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +11 -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/timeline/clock.d.ts +25 -0
- package/dist/timeline/clock.d.ts.map +1 -0
- package/dist/timeline/clock.js +257 -0
- package/dist/timeline/clock.js.map +1 -0
- package/dist/timeline/documents.d.ts +2 -0
- package/dist/timeline/documents.d.ts.map +1 -1
- package/dist/timeline/documents.js +28 -0
- package/dist/timeline/documents.js.map +1 -1
- package/dist/timeline/eligibility.d.ts +16 -0
- package/dist/timeline/eligibility.d.ts.map +1 -0
- package/dist/timeline/eligibility.js +31 -0
- package/dist/timeline/eligibility.js.map +1 -0
- package/dist/timeline/projection.d.ts +11 -0
- package/dist/timeline/projection.d.ts.map +1 -0
- package/dist/timeline/projection.js +77 -0
- package/dist/timeline/projection.js.map +1 -0
- package/dist/timeline/recurrence.d.ts +14 -0
- package/dist/timeline/recurrence.d.ts.map +1 -0
- package/dist/timeline/recurrence.js +238 -0
- package/dist/timeline/recurrence.js.map +1 -0
- package/dist/write/managed-memory.d.ts +8 -0
- package/dist/write/managed-memory.d.ts.map +1 -1
- package/dist/write/managed-memory.js +20 -0
- package/dist/write/managed-memory.js.map +1 -1
- package/dist/write/placement.d.ts +3 -1
- package/dist/write/placement.d.ts.map +1 -1
- package/dist/write/placement.js +4 -1
- package/dist/write/placement.js.map +1 -1
- package/dist/write/retain.d.ts +158 -30
- package/dist/write/retain.d.ts.map +1 -1
- package/dist/write/retain.js +723 -180
- package/dist/write/retain.js.map +1 -1
- package/package.json +2 -2
package/dist/ops/retain.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fsp from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { RetainInput, } from '@tenphi/akno-protocol';
|
|
4
|
+
import { ModelClient } from "../models/client.js";
|
|
4
5
|
import { folderCatalog } from "../kb/folders.js";
|
|
5
6
|
import { parseFrontmatter, serializeYamlString } from "../kb/frontmatter.js";
|
|
6
7
|
import { parsePage } from "../kb/page.js";
|
|
@@ -10,8 +11,11 @@ import { restoreFile, writeFileAtomic } from "../write/atomic.js";
|
|
|
10
11
|
import { detectConflict } from "../write/conflict.js";
|
|
11
12
|
import { fileEntry } from "../write/journal.js";
|
|
12
13
|
import { managedMemoryBlock, managedMemoryFingerprint, markerFromProvidedCandidate, parseManagedMemoryMarker, renderManagedMemoryMarker, renderManagedMemoryPayload, sameManagedMemorySemantics, } from "../write/managed-memory.js";
|
|
13
|
-
import { appendManagedBlock, managedSourceReference } from "../write/placement.js";
|
|
14
|
+
import { appendManagedBlock, managedSourceReference, placeManagedItems } from "../write/placement.js";
|
|
15
|
+
import { modelCallReceipt, runRetain, } from "../write/retain.js";
|
|
16
|
+
import { resolveRememberFallback } from "../write/remember-fallback.js";
|
|
14
17
|
import { reconcileRetainManagedSources } from "../write/retain-supports.js";
|
|
18
|
+
import { routeAutomaticCandidate } from "./remember.js";
|
|
15
19
|
import { normalizeSlug } from "./write.js";
|
|
16
20
|
export async function retain(ctx, rawInput) {
|
|
17
21
|
const input = RetainInput.parse(rawInput);
|
|
@@ -19,7 +23,15 @@ export async function retain(ctx, rawInput) {
|
|
|
19
23
|
for (const source of input.sources) {
|
|
20
24
|
try {
|
|
21
25
|
results.push('input' in source
|
|
22
|
-
?
|
|
26
|
+
? source.retention.mode === 'extract'
|
|
27
|
+
? await retainExtracted(ctx, source, input.dry_run ?? false)
|
|
28
|
+
: await retainCandidates(ctx, source, source.retention.candidates, {
|
|
29
|
+
dryRun: input.dry_run ?? false,
|
|
30
|
+
selection: 'provided',
|
|
31
|
+
placement: source.retention.placement,
|
|
32
|
+
initialResults: [],
|
|
33
|
+
modelUsage: { extraction: null, verification: null, placement: [] },
|
|
34
|
+
})
|
|
23
35
|
: await retractSource(ctx, source, input.dry_run ?? false));
|
|
24
36
|
}
|
|
25
37
|
catch (error) {
|
|
@@ -27,43 +39,121 @@ export async function retain(ctx, rawInput) {
|
|
|
27
39
|
source_id: source.source_id,
|
|
28
40
|
revision: source.revision,
|
|
29
41
|
outcome: 'held',
|
|
42
|
+
status: 'degraded',
|
|
43
|
+
degraded: ['retain_apply_failed'],
|
|
44
|
+
reason_code: 'apply_failed',
|
|
30
45
|
candidates: [],
|
|
31
46
|
note: `source apply failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
32
47
|
});
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
|
-
const
|
|
50
|
+
const incomplete = results.some((result) => result.outcome === 'held' ||
|
|
51
|
+
result.outcome === 'revision_conflict' ||
|
|
52
|
+
result.candidates.some((candidate) => candidate.outcome === 'held' || candidate.outcome === 'not_found'));
|
|
36
53
|
const effective = results.filter((result) => result.outcome === 'ok').length;
|
|
54
|
+
const degraded = [...new Set(results.flatMap((result) => result.degraded ?? []))];
|
|
55
|
+
const allEmpty = results.length > 0 && results.every((result) => result.status === 'empty');
|
|
37
56
|
return {
|
|
38
|
-
status: 'ok',
|
|
39
|
-
|
|
57
|
+
status: degraded.length > 0 ? 'degraded' : allEmpty ? 'empty' : 'ok',
|
|
58
|
+
...(degraded.length > 0 ? { degraded } : {}),
|
|
59
|
+
outcome: incomplete && effective > 0 ? 'partial' : effective > 0 ? 'ok' : 'noop',
|
|
40
60
|
sources: results,
|
|
41
61
|
};
|
|
42
62
|
}
|
|
43
|
-
async function
|
|
63
|
+
async function retainExtracted(ctx, source, dryRun) {
|
|
64
|
+
if (source.retention.mode !== 'extract')
|
|
65
|
+
throw new Error('retain extract received a provided source');
|
|
44
66
|
const sourceHash = sha256(JSON.stringify(source.input));
|
|
45
67
|
const requestHash = sha256(JSON.stringify(source));
|
|
46
68
|
const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
|
|
47
69
|
if (replay)
|
|
48
70
|
return replay;
|
|
49
71
|
const groupIssue = sourceGroupIssue(ctx, source.source_id, source.source_group);
|
|
50
|
-
if (groupIssue)
|
|
72
|
+
if (groupIssue)
|
|
51
73
|
return conflictResult(source.source_id, source.revision, groupIssue);
|
|
74
|
+
const curator = retentionModel(ctx);
|
|
75
|
+
const text = 'text' in source.input ? source.input.text : source.input.items.map((item) => item.text).join('\n');
|
|
76
|
+
const extracted = await runRetain(text, curator, {
|
|
77
|
+
...(source.mentioned_at ? { mentionedAt: source.mentioned_at } : {}),
|
|
78
|
+
...(source.timezone ? { timezone: source.timezone } : {}),
|
|
79
|
+
...(source.retention.mission ? { mission: source.retention.mission } : {}),
|
|
80
|
+
...('items' in source.input ? { sourceItems: source.input.items } : {}),
|
|
81
|
+
folders: folderCatalog(ctx.config, ctx.store),
|
|
82
|
+
sourceId: source.source_id,
|
|
83
|
+
revision: source.revision,
|
|
84
|
+
});
|
|
85
|
+
if (extracted.sourceHold) {
|
|
86
|
+
return retainCandidates(ctx, source, [], {
|
|
87
|
+
dryRun,
|
|
88
|
+
selection: 'extracted',
|
|
89
|
+
placement: 'automatic',
|
|
90
|
+
initialResults: [],
|
|
91
|
+
modelUsage: { ...extracted.modelUsage, placement: [] },
|
|
92
|
+
sourceHold: extracted.sourceHold,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const initialResults = extracted.held.map(heldCandidateResult);
|
|
96
|
+
if (extracted.error) {
|
|
97
|
+
return {
|
|
98
|
+
source_id: source.source_id,
|
|
99
|
+
revision: source.revision,
|
|
100
|
+
outcome: 'held',
|
|
101
|
+
status: 'degraded',
|
|
102
|
+
degraded: [extracted.degradedReason ?? 'derive_failed'],
|
|
103
|
+
candidates: initialResults,
|
|
104
|
+
model_usage: { ...extracted.modelUsage, placement: [] },
|
|
105
|
+
note: `automatic retention could not complete: ${extracted.error}`,
|
|
106
|
+
};
|
|
52
107
|
}
|
|
108
|
+
return retainCandidates(ctx, source, extracted.candidates, {
|
|
109
|
+
dryRun,
|
|
110
|
+
selection: 'extracted',
|
|
111
|
+
placement: 'automatic',
|
|
112
|
+
initialResults,
|
|
113
|
+
modelUsage: { ...extracted.modelUsage, placement: [] },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function retainCandidates(ctx, source, suppliedCandidates, options) {
|
|
117
|
+
const sourceHash = sha256(JSON.stringify(source.input));
|
|
118
|
+
const requestHash = sha256(JSON.stringify(source));
|
|
119
|
+
const replay = replayResult(ctx, source.source_id, source.revision, requestHash, sourceHash);
|
|
120
|
+
if (replay)
|
|
121
|
+
return replay;
|
|
122
|
+
const groupIssue = sourceGroupIssue(ctx, source.source_id, source.source_group);
|
|
123
|
+
if (groupIssue)
|
|
124
|
+
return conflictResult(source.source_id, source.revision, groupIssue);
|
|
125
|
+
const resolved = options.placement === 'automatic'
|
|
126
|
+
? await resolveAutomaticCandidates(ctx, suppliedCandidates)
|
|
127
|
+
: {
|
|
128
|
+
candidates: suppliedCandidates.filter(hasDestination),
|
|
129
|
+
held: suppliedCandidates
|
|
130
|
+
.filter((candidate) => !candidate.destination)
|
|
131
|
+
.map((candidate) => ({
|
|
132
|
+
candidate_id: candidate.candidate_id,
|
|
133
|
+
outcome: 'held',
|
|
134
|
+
reason_code: 'validation_failed',
|
|
135
|
+
reason: 'provided exact candidates require a destination',
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
const candidates = resolved.candidates;
|
|
53
139
|
const receiptFingerprint = managedMemoryFingerprint(`receipt:${source.source_id}:${source.revision}:${requestHash}`);
|
|
54
140
|
const proofGroup = managedMemoryFingerprint(`proof:${source.source_group ?? source.source_id}`);
|
|
55
141
|
const stages = new Map();
|
|
56
142
|
const pendingSupports = [];
|
|
57
|
-
const candidateResults = [];
|
|
58
|
-
const candidateIds = new Set(
|
|
59
|
-
const candidateOrder = new Map(
|
|
143
|
+
const candidateResults = [...options.initialResults, ...resolved.held];
|
|
144
|
+
const candidateIds = new Set(suppliedCandidates.map((candidate) => candidate.candidate_id));
|
|
145
|
+
const candidateOrder = new Map([
|
|
146
|
+
...options.initialResults.map((result) => result.candidate_id),
|
|
147
|
+
...suppliedCandidates.map((candidate) => candidate.candidate_id),
|
|
148
|
+
].map((candidateId, index) => [candidateId, index]));
|
|
60
149
|
const candidateMemoryIds = new Map();
|
|
61
|
-
const ordered = dependencyOrder(
|
|
62
|
-
for (const candidate of
|
|
150
|
+
const ordered = dependencyOrder(candidates);
|
|
151
|
+
for (const candidate of candidates) {
|
|
63
152
|
if (ordered.blocked.has(candidate.candidate_id)) {
|
|
64
153
|
candidateResults.push({
|
|
65
154
|
candidate_id: candidate.candidate_id,
|
|
66
155
|
outcome: 'held',
|
|
156
|
+
reason_code: 'validation_failed',
|
|
67
157
|
reason: 'candidate relation cycle or blocked dependency',
|
|
68
158
|
});
|
|
69
159
|
}
|
|
@@ -71,7 +161,12 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
71
161
|
for (const candidate of ordered.candidates) {
|
|
72
162
|
const issue = candidateIssue(ctx, source, candidate, candidateIds);
|
|
73
163
|
if (issue) {
|
|
74
|
-
candidateResults.push({
|
|
164
|
+
candidateResults.push({
|
|
165
|
+
candidate_id: candidate.candidate_id,
|
|
166
|
+
outcome: 'held',
|
|
167
|
+
reason_code: candidateIssueReasonCode(issue),
|
|
168
|
+
reason: issue,
|
|
169
|
+
});
|
|
75
170
|
continue;
|
|
76
171
|
}
|
|
77
172
|
const missingTarget = (candidate.relations ?? []).find((relation) => 'candidate_id' in relation.target && !candidateMemoryIds.has(relation.target.candidate_id));
|
|
@@ -79,6 +174,7 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
79
174
|
candidateResults.push({
|
|
80
175
|
candidate_id: candidate.candidate_id,
|
|
81
176
|
outcome: 'held',
|
|
177
|
+
reason_code: 'validation_failed',
|
|
82
178
|
reason: `relation target candidate ${missingTarget.target.candidate_id} was not retained`,
|
|
83
179
|
});
|
|
84
180
|
continue;
|
|
@@ -90,6 +186,7 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
90
186
|
candidate_id: candidate.candidate_id,
|
|
91
187
|
outcome: 'held',
|
|
92
188
|
slug,
|
|
189
|
+
reason_code: stage.issue.includes('unreadable') ? 'source_unavailable' : 'no_writable_destination',
|
|
93
190
|
reason: stage.issue,
|
|
94
191
|
});
|
|
95
192
|
continue;
|
|
@@ -104,7 +201,7 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
104
201
|
receipt: receiptFingerprint,
|
|
105
202
|
candidate: candidateFingerprint,
|
|
106
203
|
proofGroup,
|
|
107
|
-
selection:
|
|
204
|
+
selection: options.selection,
|
|
108
205
|
};
|
|
109
206
|
const proposedId = newPrefixedId('mem');
|
|
110
207
|
const marker = markerFromProvidedCandidate(proposedId, candidate, support, candidateMemoryIds);
|
|
@@ -124,6 +221,7 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
124
221
|
outcome: 'held',
|
|
125
222
|
memory_id: memoryId,
|
|
126
223
|
slug,
|
|
224
|
+
reason_code: 'support_limit',
|
|
127
225
|
reason: 'support_limit',
|
|
128
226
|
});
|
|
129
227
|
continue;
|
|
@@ -136,13 +234,15 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
136
234
|
}
|
|
137
235
|
else {
|
|
138
236
|
const row = ctx.store.db.prepare('SELECT id FROM pages WHERE slug = ?').get(slug);
|
|
139
|
-
if (
|
|
237
|
+
if (options.placement === 'exact' &&
|
|
238
|
+
stage.before !== null &&
|
|
140
239
|
candidate.destination.section &&
|
|
141
240
|
countSection(stage.after, candidate.destination.section) !== 1) {
|
|
142
241
|
candidateResults.push({
|
|
143
242
|
candidate_id: candidate.candidate_id,
|
|
144
243
|
outcome: 'held',
|
|
145
244
|
slug,
|
|
245
|
+
reason_code: 'validation_failed',
|
|
146
246
|
reason: 'exact destination section is absent or ambiguous',
|
|
147
247
|
});
|
|
148
248
|
continue;
|
|
@@ -162,12 +262,32 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
162
262
|
candidate_id: candidate.candidate_id,
|
|
163
263
|
outcome: 'held',
|
|
164
264
|
slug,
|
|
265
|
+
reason_code: 'conflict',
|
|
165
266
|
reason: 'conflict',
|
|
166
267
|
});
|
|
167
268
|
continue;
|
|
168
269
|
}
|
|
169
270
|
}
|
|
170
|
-
|
|
271
|
+
if (options.placement === 'automatic') {
|
|
272
|
+
const curator = retentionModel(ctx);
|
|
273
|
+
const placed = await placeManagedItems(stage.after, [{ id: memoryId, marker, text: payload }], curator);
|
|
274
|
+
stage.after = placed.content;
|
|
275
|
+
if (placed.modelOutcome)
|
|
276
|
+
options.modelUsage.placement.push(modelCallReceipt(curator, placed.modelOutcome));
|
|
277
|
+
if (placed.error) {
|
|
278
|
+
candidateResults.push({
|
|
279
|
+
candidate_id: candidate.candidate_id,
|
|
280
|
+
outcome,
|
|
281
|
+
memory_id: memoryId,
|
|
282
|
+
slug,
|
|
283
|
+
reason_code: 'placement_degraded',
|
|
284
|
+
reason: `written under Unsorted because semantic section placement degraded: ${placed.error}`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
stage.after = appendBlock(stage.after, candidate.destination.section ?? 'Unsorted', managedMemoryBlock(marker, payload));
|
|
290
|
+
}
|
|
171
291
|
}
|
|
172
292
|
const evidence = sourceEvidence(candidate);
|
|
173
293
|
pendingSupports.push({
|
|
@@ -177,31 +297,56 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
177
297
|
proof_group: proofGroup,
|
|
178
298
|
memory_id: memoryId,
|
|
179
299
|
slug,
|
|
180
|
-
selection:
|
|
300
|
+
selection: options.selection,
|
|
181
301
|
evidence,
|
|
182
302
|
evidence_hash: sha256(evidence),
|
|
183
303
|
source_ref: managedSourceReference(source.locator ?? source.source_id),
|
|
184
304
|
origin: sourceOrigin(candidate.attribution.source_role),
|
|
185
305
|
input_hash: sourceHash,
|
|
186
306
|
});
|
|
187
|
-
candidateResults.
|
|
188
|
-
candidate_id: candidate.candidate_id,
|
|
189
|
-
|
|
190
|
-
memory_id: memoryId,
|
|
191
|
-
slug,
|
|
192
|
-
});
|
|
307
|
+
if (!candidateResults.some((result) => result.candidate_id === candidate.candidate_id)) {
|
|
308
|
+
candidateResults.push({ candidate_id: candidate.candidate_id, outcome, memory_id: memoryId, slug });
|
|
309
|
+
}
|
|
193
310
|
candidateMemoryIds.set(candidate.candidate_id, memoryId);
|
|
194
311
|
}
|
|
195
|
-
candidateResults.sort((left, right) => candidateOrder.get(left.candidate_id)
|
|
312
|
+
candidateResults.sort((left, right) => (candidateOrder.get(left.candidate_id) ?? Number.MAX_SAFE_INTEGER) -
|
|
313
|
+
(candidateOrder.get(right.candidate_id) ?? Number.MAX_SAFE_INTEGER));
|
|
196
314
|
const changed = [...stages.values()].filter((stage) => stage.before !== stage.after);
|
|
315
|
+
const placementDegraded = candidateResults.some((result) => result.reason_code === 'placement_degraded');
|
|
316
|
+
const degraded = placementDegraded
|
|
317
|
+
? [retentionModel(ctx).available ? 'derive_failed' : 'no_derive_model']
|
|
318
|
+
: [];
|
|
197
319
|
const preview = {
|
|
198
320
|
source_id: source.source_id,
|
|
199
321
|
revision: source.revision,
|
|
200
|
-
outcome:
|
|
322
|
+
outcome: options.sourceHold
|
|
323
|
+
? 'held'
|
|
324
|
+
: changed.length > 0
|
|
325
|
+
? 'ok'
|
|
326
|
+
: candidateResults.some((item) => item.outcome === 'held')
|
|
327
|
+
? 'held'
|
|
328
|
+
: 'noop',
|
|
201
329
|
candidates: candidateResults,
|
|
202
|
-
...(
|
|
330
|
+
...(options.sourceHold ? { reason_code: options.sourceHold.reason_code } : {}),
|
|
331
|
+
status: degraded.length > 0
|
|
332
|
+
? 'degraded'
|
|
333
|
+
: options.sourceHold
|
|
334
|
+
? 'ok'
|
|
335
|
+
: candidateResults.length === 0
|
|
336
|
+
? 'empty'
|
|
337
|
+
: 'ok',
|
|
338
|
+
...(degraded.length > 0 ? { degraded } : {}),
|
|
339
|
+
...(options.placement === 'automatic' ? { model_usage: options.modelUsage } : {}),
|
|
340
|
+
...(options.sourceHold ? { note: options.sourceHold.reason } : {}),
|
|
341
|
+
...(options.dryRun
|
|
342
|
+
? {
|
|
343
|
+
note: options.sourceHold
|
|
344
|
+
? `${options.sourceHold.reason}; dry run — no replay receipt was created`
|
|
345
|
+
: 'dry run — nothing was written and no replay receipt was created',
|
|
346
|
+
}
|
|
347
|
+
: {}),
|
|
203
348
|
};
|
|
204
|
-
if (dryRun)
|
|
349
|
+
if (options.dryRun)
|
|
205
350
|
return preview;
|
|
206
351
|
const committed = await commitStages(ctx, changed, `retained ${candidateResults.filter((item) => ['written', 'support_added'].includes(item.outcome)).length} item(s) from one source revision`);
|
|
207
352
|
const changeId = committed?.changeId ?? null;
|
|
@@ -212,7 +357,11 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
212
357
|
sourceHash,
|
|
213
358
|
requestHash,
|
|
214
359
|
receiptFingerprint,
|
|
215
|
-
mode: '
|
|
360
|
+
mode: options.selection === 'extracted'
|
|
361
|
+
? 'extract_automatic'
|
|
362
|
+
: options.placement === 'automatic'
|
|
363
|
+
? 'provided_automatic'
|
|
364
|
+
: 'provided_exact',
|
|
216
365
|
result,
|
|
217
366
|
changeId,
|
|
218
367
|
supports: pendingSupports,
|
|
@@ -225,6 +374,67 @@ async function retainProvidedExact(ctx, source, dryRun) {
|
|
|
225
374
|
await reindexStages(ctx, changed);
|
|
226
375
|
return result;
|
|
227
376
|
}
|
|
377
|
+
async function resolveAutomaticCandidates(ctx, candidates) {
|
|
378
|
+
const resolved = [];
|
|
379
|
+
const held = [];
|
|
380
|
+
const catalog = folderCatalog(ctx.config, ctx.store);
|
|
381
|
+
const fallback = await resolveRememberFallback(ctx, catalog);
|
|
382
|
+
for (const candidate of candidates) {
|
|
383
|
+
const suggested = candidate.destination?.slug;
|
|
384
|
+
const routed = await routeAutomaticCandidate(ctx, {
|
|
385
|
+
...candidate,
|
|
386
|
+
...(suggested ? { page: suggested } : {}),
|
|
387
|
+
}, { constrainToSuggestedFolder: suggested !== undefined });
|
|
388
|
+
let slug = routed.slug;
|
|
389
|
+
if (!slug && suggested && !pageExists(ctx, suggested) && admittedAutomaticSlug(catalog, suggested)) {
|
|
390
|
+
slug = suggested;
|
|
391
|
+
}
|
|
392
|
+
if (!slug && fallback && fallback.status !== 'unavailable')
|
|
393
|
+
slug = fallback.slug;
|
|
394
|
+
if (!slug) {
|
|
395
|
+
const reasonCode = routed.blocked || (!suggested && routed.nearest.length === 0)
|
|
396
|
+
? 'no_writable_destination'
|
|
397
|
+
: 'routing_uncertain';
|
|
398
|
+
held.push({
|
|
399
|
+
candidate_id: candidate.candidate_id,
|
|
400
|
+
outcome: 'held',
|
|
401
|
+
reason_code: reasonCode,
|
|
402
|
+
reason: routed.blocked
|
|
403
|
+
? `the strongest semantic destination is read-only: ${routed.blocked}`
|
|
404
|
+
: suggested && pageExists(ctx, suggested)
|
|
405
|
+
? 'the suggested existing page was not judged a safe semantic destination'
|
|
406
|
+
: 'no admitted writable destination could be established',
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
resolved.push({ ...candidate, destination: { slug } });
|
|
411
|
+
}
|
|
412
|
+
return { candidates: resolved, held };
|
|
413
|
+
}
|
|
414
|
+
function hasDestination(candidate) {
|
|
415
|
+
return candidate.destination !== undefined;
|
|
416
|
+
}
|
|
417
|
+
function pageExists(ctx, slug) {
|
|
418
|
+
return ctx.store.db.prepare('SELECT 1 FROM pages WHERE slug = ?').get(slug) !== undefined;
|
|
419
|
+
}
|
|
420
|
+
function admittedAutomaticSlug(catalog, slug) {
|
|
421
|
+
const parent = slug.slice(0, slug.lastIndexOf('/'));
|
|
422
|
+
return catalog.some((folder) => folder.path === parent &&
|
|
423
|
+
folder.role === 'knowledge' &&
|
|
424
|
+
folder.remember === 'integrate' &&
|
|
425
|
+
folder.creatable);
|
|
426
|
+
}
|
|
427
|
+
function retentionModel(ctx) {
|
|
428
|
+
return ctx.config.maintenance.model ? new ModelClient(ctx.config.maintenance.model) : ctx.models.derive;
|
|
429
|
+
}
|
|
430
|
+
function heldCandidateResult(candidate) {
|
|
431
|
+
return {
|
|
432
|
+
candidate_id: candidate.candidate_id,
|
|
433
|
+
outcome: 'held',
|
|
434
|
+
reason_code: candidate.reason_code,
|
|
435
|
+
reason: candidate.reason,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
228
438
|
async function retractSource(ctx, source, dryRun) {
|
|
229
439
|
const sourceHash = sha256('retraction');
|
|
230
440
|
const requestHash = sha256(JSON.stringify(source));
|
|
@@ -239,6 +449,7 @@ async function retractSource(ctx, source, dryRun) {
|
|
|
239
449
|
source_id: source.source_id,
|
|
240
450
|
revision: source.revision,
|
|
241
451
|
outcome: 'held',
|
|
452
|
+
reason_code: 'source_unavailable',
|
|
242
453
|
candidates: [],
|
|
243
454
|
note: 'target_revision was not retained',
|
|
244
455
|
};
|
|
@@ -262,17 +473,32 @@ async function retractSource(ctx, source, dryRun) {
|
|
|
262
473
|
}
|
|
263
474
|
const stage = await existingPageStage(ctx, stages, support.slug);
|
|
264
475
|
if ('issue' in stage) {
|
|
265
|
-
results.push({
|
|
476
|
+
results.push({
|
|
477
|
+
candidate_id: candidateId,
|
|
478
|
+
outcome: 'held',
|
|
479
|
+
reason_code: 'source_unavailable',
|
|
480
|
+
reason: stage.issue,
|
|
481
|
+
});
|
|
266
482
|
continue;
|
|
267
483
|
}
|
|
268
484
|
const block = managedBlocks(stage.after).find((entry) => entry.marker.id === support.memory_id);
|
|
269
485
|
if (!block) {
|
|
270
|
-
results.push({
|
|
486
|
+
results.push({
|
|
487
|
+
candidate_id: candidateId,
|
|
488
|
+
outcome: 'held',
|
|
489
|
+
reason_code: 'conflict',
|
|
490
|
+
reason: 'owned memory block changed',
|
|
491
|
+
});
|
|
271
492
|
continue;
|
|
272
493
|
}
|
|
273
494
|
const nextSupports = block.marker.supports.filter((entry) => !(entry.receipt === support.receipt_fingerprint && entry.candidate === support.candidate_fingerprint));
|
|
274
495
|
if (nextSupports.length === block.marker.supports.length) {
|
|
275
|
-
results.push({
|
|
496
|
+
results.push({
|
|
497
|
+
candidate_id: candidateId,
|
|
498
|
+
outcome: 'held',
|
|
499
|
+
reason_code: 'conflict',
|
|
500
|
+
reason: 'support marker changed',
|
|
501
|
+
});
|
|
276
502
|
continue;
|
|
277
503
|
}
|
|
278
504
|
stage.after =
|
|
@@ -389,6 +615,22 @@ function candidateIssue(ctx, source, candidate, candidateIds) {
|
|
|
389
615
|
return attributionIssue;
|
|
390
616
|
if (sourceEvidence(candidate).length > 1200)
|
|
391
617
|
return 'discourse frame exceeds the evidence limit';
|
|
618
|
+
if (RELATIVE_TIME.test(sourceEvidence(candidate))) {
|
|
619
|
+
const sourceItems = 'items' in source.input ? source.input.items : [];
|
|
620
|
+
const supportedMentionTimes = new Set([
|
|
621
|
+
...(source.mentioned_at ? [source.mentioned_at] : []),
|
|
622
|
+
...candidate.discourse_frame.flatMap((span) => {
|
|
623
|
+
const item = sourceItems.find((entry) => entry.item_id === span.item_id);
|
|
624
|
+
return item?.mentioned_at ? [item.mentioned_at] : [];
|
|
625
|
+
}),
|
|
626
|
+
]);
|
|
627
|
+
if (!source.timezone ||
|
|
628
|
+
!candidate.time?.mentioned_at ||
|
|
629
|
+
candidate.time.timezone !== source.timezone ||
|
|
630
|
+
!supportedMentionTimes.has(candidate.time.mentioned_at)) {
|
|
631
|
+
return 'relative time is unresolved without the exact source mention time and timezone';
|
|
632
|
+
}
|
|
633
|
+
}
|
|
392
634
|
if (candidate.subject_ref &&
|
|
393
635
|
!ctx.store.db.prepare('SELECT 1 FROM graph_entities WHERE id = ?').get(candidate.subject_ref.entity_id)) {
|
|
394
636
|
return 'subject_ref does not name an existing canonical entity';
|
|
@@ -419,6 +661,14 @@ function candidateIssue(ctx, source, candidate, candidateIds) {
|
|
|
419
661
|
}
|
|
420
662
|
return null;
|
|
421
663
|
}
|
|
664
|
+
function candidateIssueReasonCode(issue) {
|
|
665
|
+
if (issue.startsWith('relative time is unresolved'))
|
|
666
|
+
return 'time_unresolved';
|
|
667
|
+
return /(?:support|discourse_frame|source|evidence).*(?:missing|ambiguous|unavailable)/i.test(issue)
|
|
668
|
+
? 'source_unavailable'
|
|
669
|
+
: 'validation_failed';
|
|
670
|
+
}
|
|
671
|
+
const RELATIVE_TIME = /\b(today|tomorrow|yesterday|tonight|next\s+(?:day|week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)|last\s+(?:night|week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)|this\s+(?:morning|afternoon|evening|week|month|year))\b/i;
|
|
422
672
|
function structuredAttributionIssue(source, candidate) {
|
|
423
673
|
if (!('items' in source.input))
|
|
424
674
|
return null;
|