@tenphi/akno-core 0.10.1 → 0.12.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/maintenance/dream.d.ts.map +1 -1
- package/dist/maintenance/dream.js +6 -0
- package/dist/maintenance/dream.js.map +1 -1
- package/dist/maintenance/managed-item-routing.d.ts +22 -0
- package/dist/maintenance/managed-item-routing.d.ts.map +1 -1
- package/dist/maintenance/managed-item-routing.js +329 -17
- package/dist/maintenance/managed-item-routing.js.map +1 -1
- package/dist/maintenance/managed-items.d.ts +7 -3
- package/dist/maintenance/managed-items.d.ts.map +1 -1
- package/dist/maintenance/managed-items.js +161 -70
- package/dist/maintenance/managed-items.js.map +1 -1
- package/dist/maintenance/plans.d.ts +2 -1
- package/dist/maintenance/plans.d.ts.map +1 -1
- package/dist/maintenance/plans.js +12 -1
- package/dist/maintenance/plans.js.map +1 -1
- package/dist/memory/subject-resolution.d.ts +8 -0
- package/dist/memory/subject-resolution.d.ts.map +1 -0
- package/dist/memory/subject-resolution.js +27 -0
- package/dist/memory/subject-resolution.js.map +1 -0
- package/dist/memory/temporal-destination.d.ts +11 -0
- package/dist/memory/temporal-destination.d.ts.map +1 -0
- package/dist/memory/temporal-destination.js +73 -0
- package/dist/memory/temporal-destination.js.map +1 -0
- package/dist/ops/forget.d.ts +1 -1
- package/dist/ops/forget.d.ts.map +1 -1
- package/dist/ops/forget.js +58 -1
- package/dist/ops/forget.js.map +1 -1
- package/dist/ops/remember.d.ts +11 -25
- package/dist/ops/remember.d.ts.map +1 -1
- package/dist/ops/remember.js +158 -40
- package/dist/ops/remember.js.map +1 -1
- package/dist/ops/retain.d.ts.map +1 -1
- package/dist/ops/retain.js +80 -23
- package/dist/ops/retain.js.map +1 -1
- package/dist/write/managed-memory.d.ts.map +1 -1
- package/dist/write/managed-memory.js +11 -3
- package/dist/write/managed-memory.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,12 +4,13 @@ import { z } from 'zod';
|
|
|
4
4
|
import { parseFrontmatter } from "../kb/frontmatter.js";
|
|
5
5
|
import { parsePage, resolvePagePolicy } from "../kb/page.js";
|
|
6
6
|
import { parseJsonLoose } from "../models/client.js";
|
|
7
|
+
import { retainedTemporalBoundary } from "../memory/temporal-destination.js";
|
|
7
8
|
import { isReserved } from "../reserved.js";
|
|
8
|
-
import { effectiveRule } from "../rules/compile.js";
|
|
9
|
+
import { declaringRule, effectiveRule } from "../rules/compile.js";
|
|
9
10
|
import { sha256 } from "../store/ids.js";
|
|
10
11
|
import { parseManagedMemoryMarker } from "../write/managed-memory.js";
|
|
11
12
|
import { emptyManagedSourceMetrics, groundedManagedReplacement, managedSourceItemIds, pruneManagedSourceArchives, qualifyManagedSources, } from "./managed-item-sources.js";
|
|
12
|
-
import { emptyManagedRoutingMetrics, hasH2, managedSectionHeading, qualifyManagedItemRouting, } from "./managed-item-routing.js";
|
|
13
|
+
import { emptyManagedRoutingMetrics, hasH2, managedSectionHeading, qualifyManagedItemRouting, qualifyManagedRoutingDestination, repeatsManagedRoutingEdge, } from "./managed-item-routing.js";
|
|
13
14
|
const MANAGED_ITEM_FINDING_CODES = [
|
|
14
15
|
'empty_marker',
|
|
15
16
|
'malformed_marker',
|
|
@@ -22,6 +23,9 @@ const MANAGED_ITEM_FINDING_CODES = [
|
|
|
22
23
|
'routing_deferred',
|
|
23
24
|
'routing_uncertain',
|
|
24
25
|
'routing_unavailable',
|
|
26
|
+
'routing_oscillation',
|
|
27
|
+
'source_vacated',
|
|
28
|
+
'empty_knowledge_page',
|
|
25
29
|
'wording_corrected',
|
|
26
30
|
'wording_uncertain',
|
|
27
31
|
'source_unavailable',
|
|
@@ -131,6 +135,12 @@ export function inspectManagedItems(content) {
|
|
|
131
135
|
const after = prefix + lines.flatMap((line, index) => (remove.has(index) ? [] : [line])).join('\n');
|
|
132
136
|
return { after, inspectedMarkers, findings, repairs, bindings };
|
|
133
137
|
}
|
|
138
|
+
/** A frontmatter-and-heading skeleton has no knowledge of its own to preserve as a standalone page. */
|
|
139
|
+
function isEmptyKnowledgePageShell(content) {
|
|
140
|
+
const frontmatter = parseFrontmatter(content);
|
|
141
|
+
const body = content.slice(frontmatter.bodyOffset).replace(/<!--[^]*?-->/g, '');
|
|
142
|
+
return body.split(/\r?\n/).every((line) => line.trim() === '' || /^\s{0,3}#{1,6}(?:\s+.*)?$/.test(line));
|
|
143
|
+
}
|
|
134
144
|
/** The sealed replacement must be exactly the structural repair and qualified exact-block move it declares. */
|
|
135
145
|
export function managedItemRepairIssue(before, after, placements = [], corrections = []) {
|
|
136
146
|
const inspection = inspectManagedItems(before);
|
|
@@ -179,6 +189,9 @@ export function managedItemOperationsIssue(replacements, placements = [], correc
|
|
|
179
189
|
if (!applied.ok || applied.source !== source.after || applied.destination !== destination.after) {
|
|
180
190
|
return 'the managed-item output is broader than its exact cross-page transfer';
|
|
181
191
|
}
|
|
192
|
+
if (!transfer.sourceIsFallbackQueue && isEmptyKnowledgePageShell(applied.source)) {
|
|
193
|
+
return 'a cross-page managed-item move may not vacate a normal knowledge page';
|
|
194
|
+
}
|
|
182
195
|
return null;
|
|
183
196
|
}
|
|
184
197
|
export async function planManagedItems(ctx, options = {}) {
|
|
@@ -223,19 +236,26 @@ export async function planManagedItems(ctx, options = {}) {
|
|
|
223
236
|
report.eligiblePages += 1;
|
|
224
237
|
const inspection = inspectManagedItems(before);
|
|
225
238
|
report.inspectedMarkers += inspection.inspectedMarkers;
|
|
239
|
+
if (inspection.inspectedMarkers === 0 && isEmptyKnowledgePageShell(before)) {
|
|
240
|
+
addFinding(report, { code: 'empty_knowledge_page', line: page.bodyLine, outcome: 'held' }, 'held', page.slug);
|
|
241
|
+
}
|
|
226
242
|
eligible.push({ row, before, page, inspection });
|
|
227
243
|
}
|
|
228
244
|
if (sourceScanComplete)
|
|
229
245
|
pruneManagedSourceArchives(ctx, liveManagedSourceIds);
|
|
230
246
|
const crossPageCollisions = crossPageItemCollisions(eligible);
|
|
231
|
-
const
|
|
247
|
+
const memories = currentManagedMemories(ctx);
|
|
232
248
|
const prepared = eligible.map((candidate) => ({
|
|
233
249
|
candidate,
|
|
234
|
-
inspection: withBindingFindings(
|
|
250
|
+
inspection: withBindingFindings(candidate, memories, crossPageCollisions, options),
|
|
235
251
|
}));
|
|
252
|
+
const fallbackSlug = ctx.config.maintenance.retain.fallbackPage;
|
|
236
253
|
const routingPages = new Map(prepared
|
|
237
254
|
.filter((entry) => entry.inspection.repairs.length === 0)
|
|
238
|
-
.map(({ candidate }) => [
|
|
255
|
+
.map(({ candidate }) => [
|
|
256
|
+
candidate.page.slug,
|
|
257
|
+
managedRoutingPage(ctx, candidate, candidate.page.slug === fallbackSlug),
|
|
258
|
+
]));
|
|
239
259
|
const occupiedPaths = new Set();
|
|
240
260
|
const suppressedRoutingSources = new Set();
|
|
241
261
|
const routingFindings = new Map();
|
|
@@ -248,17 +268,20 @@ export async function planManagedItems(ctx, options = {}) {
|
|
|
248
268
|
for (const binding of inspection.bindings) {
|
|
249
269
|
if (blockedLines.has(binding.markerLine))
|
|
250
270
|
continue;
|
|
251
|
-
const
|
|
271
|
+
const memory = memories.get(binding.id);
|
|
252
272
|
const removed = removeManagedBlock(candidate.before, binding);
|
|
253
|
-
if (!
|
|
273
|
+
if (!memory || !removed)
|
|
254
274
|
continue;
|
|
255
|
-
const
|
|
275
|
+
const routingSource = managedRoutingPage(ctx, candidate, candidate.page.slug === fallbackSlug);
|
|
276
|
+
const routingItem = {
|
|
256
277
|
id: binding.id,
|
|
257
278
|
payload: binding.payload,
|
|
258
|
-
subject:
|
|
259
|
-
attribute:
|
|
279
|
+
subject: memory.routingSubject,
|
|
280
|
+
attribute: memory.attribute || binding.currentHeading || memory.kind,
|
|
260
281
|
currentHeading: binding.currentHeading,
|
|
261
|
-
|
|
282
|
+
temporalBoundary: binding.temporalBoundary,
|
|
283
|
+
};
|
|
284
|
+
const qualified = await qualifyManagedItemRouting(ctx, routingSource, removed.content, routingItem, routingPages);
|
|
262
285
|
addRoutingMetrics(report.routing, qualified.metrics);
|
|
263
286
|
if (qualified.decision.outcome === 'keep')
|
|
264
287
|
continue;
|
|
@@ -270,7 +293,7 @@ export async function planManagedItems(ctx, options = {}) {
|
|
|
270
293
|
if (!destination ||
|
|
271
294
|
destination.inspection.repairs.length > 0 ||
|
|
272
295
|
occupiedPaths.has(destination.candidate.row.rel_path)) {
|
|
273
|
-
report.routing.
|
|
296
|
+
retractRoutingMove(report.routing, qualified.decision.createHeading === true);
|
|
274
297
|
report.routing.unavailable += 1;
|
|
275
298
|
setRoutingFinding(routingFindings, candidate.row.rel_path, binding.markerLine, 'routing_unavailable', 'held');
|
|
276
299
|
continue;
|
|
@@ -284,15 +307,42 @@ export async function planManagedItems(ctx, options = {}) {
|
|
|
284
307
|
destinationSlug: destination.candidate.page.slug,
|
|
285
308
|
destinationHeading: qualified.decision.targetHeading,
|
|
286
309
|
createDestinationHeading: qualified.decision.createHeading,
|
|
287
|
-
destinationHeadingSource: qualified.decision.createHeading
|
|
310
|
+
destinationHeadingSource: qualified.decision.createHeading
|
|
311
|
+
? memory.attribute || binding.currentHeading || memory.kind
|
|
312
|
+
: undefined,
|
|
313
|
+
sourceIsFallbackQueue: candidate.page.slug === fallbackSlug,
|
|
288
314
|
};
|
|
289
315
|
const applied = applyManagedItemTransfer(candidate.before, destination.candidate.before, transfer);
|
|
290
316
|
if (!applied.ok) {
|
|
291
|
-
report.routing.
|
|
317
|
+
retractRoutingMove(report.routing, qualified.decision.createHeading === true);
|
|
292
318
|
report.routing.unavailable += 1;
|
|
293
319
|
setRoutingFinding(routingFindings, candidate.row.rel_path, binding.markerLine, 'routing_unavailable', 'held');
|
|
294
320
|
continue;
|
|
295
321
|
}
|
|
322
|
+
if (repeatsManagedRoutingEdge(qualified.history, candidate.row.rel_path, destination.candidate.row.rel_path)) {
|
|
323
|
+
retractRoutingMove(report.routing, qualified.decision.createHeading === true);
|
|
324
|
+
report.routing.oscillationHolds += 1;
|
|
325
|
+
setRoutingFinding(routingFindings, candidate.row.rel_path, binding.markerLine, 'routing_oscillation', 'held');
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!transfer.sourceIsFallbackQueue && isEmptyKnowledgePageShell(applied.source)) {
|
|
329
|
+
retractRoutingMove(report.routing, qualified.decision.createHeading === true);
|
|
330
|
+
report.routing.sourceVacatedHolds += 1;
|
|
331
|
+
setRoutingFinding(routingFindings, candidate.row.rel_path, binding.markerLine, 'source_vacated', 'held');
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
report.routing.classifierCalls += 1;
|
|
335
|
+
report.routing.validationCalls += 1;
|
|
336
|
+
const destinationValidation = await qualifyManagedRoutingDestination(ctx, routingSource, removed.content, routingItem, managedRoutingPage(ctx, destination.candidate, destination.candidate.page.slug === fallbackSlug), transfer.destinationHeading, qualified.history);
|
|
337
|
+
if (destinationValidation !== 'accept') {
|
|
338
|
+
retractRoutingMove(report.routing, qualified.decision.createHeading === true);
|
|
339
|
+
if (destinationValidation === 'unavailable')
|
|
340
|
+
report.routing.unavailable += 1;
|
|
341
|
+
else
|
|
342
|
+
report.routing.uncertain += 1;
|
|
343
|
+
setRoutingFinding(routingFindings, candidate.row.rel_path, binding.markerLine, destinationValidation === 'unavailable' ? 'routing_unavailable' : 'routing_uncertain', 'held');
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
296
346
|
const inputHash = managedTransferInputHash(candidate.before, applied.source, destination.candidate.before, applied.destination, transfer);
|
|
297
347
|
const finding = {
|
|
298
348
|
code: 'misrouted_item',
|
|
@@ -347,7 +397,7 @@ export async function planManagedItems(ctx, options = {}) {
|
|
|
347
397
|
// First seal deterministic normalization on its own. Besides keeping the move verifier small,
|
|
348
398
|
// this ensures semantic qualification always sees the exact canonical bytes it is judging.
|
|
349
399
|
if (inspection.repairs.length === 0) {
|
|
350
|
-
const qualified = await qualifyManagedItemPlacement(ctx, candidate, inspection,
|
|
400
|
+
const qualified = await qualifyManagedItemPlacement(ctx, candidate, inspection, memories);
|
|
351
401
|
addPlacementMetrics(report.placement, qualified.metrics);
|
|
352
402
|
const applied = applyQualifiedPlacementFindings(inspection, qualified);
|
|
353
403
|
inspection = applied.inspection;
|
|
@@ -406,14 +456,23 @@ function deferPlacementUntilCanonical(inspection) {
|
|
|
406
456
|
});
|
|
407
457
|
return { inspection: { ...inspection, findings }, fragments };
|
|
408
458
|
}
|
|
409
|
-
function managedRoutingPage(candidate) {
|
|
459
|
+
function managedRoutingPage(ctx, candidate, fallback) {
|
|
460
|
+
const rule = effectiveRule(candidate.page.slug, ctx.config.rules);
|
|
461
|
+
const purposeRule = declaringRule(candidate.page.slug, ctx.config.rules, 'description');
|
|
410
462
|
return {
|
|
411
463
|
id: candidate.row.id,
|
|
412
464
|
slug: candidate.page.slug,
|
|
465
|
+
relPath: candidate.row.rel_path,
|
|
413
466
|
title: candidate.page.title,
|
|
467
|
+
type: candidate.page.type ?? rule.type ?? null,
|
|
468
|
+
about: [...new Set([...candidate.page.about, ...(rule.about ?? [])])],
|
|
469
|
+
folderPath: path.posix.dirname(candidate.page.slug) === '.' ? '' : path.posix.dirname(candidate.page.slug),
|
|
470
|
+
folderRule: purposeRule?.glob ?? null,
|
|
471
|
+
folderPurpose: rule.description ?? null,
|
|
414
472
|
bodyHash: candidate.page.bodyHash,
|
|
415
473
|
body: candidate.page.body,
|
|
416
474
|
headings: uniqueManagedHeadings(candidate.before),
|
|
475
|
+
fallback,
|
|
417
476
|
};
|
|
418
477
|
}
|
|
419
478
|
function setRoutingFinding(findings, relPath, line, code, outcome) {
|
|
@@ -453,12 +512,28 @@ function applyRoutingFindings(inspection, routed) {
|
|
|
453
512
|
}
|
|
454
513
|
return { ...inspection, findings };
|
|
455
514
|
}
|
|
456
|
-
function
|
|
515
|
+
function currentManagedMemories(ctx) {
|
|
457
516
|
const rows = ctx.store.db
|
|
458
|
-
.prepare(`SELECT item_id,
|
|
459
|
-
|
|
517
|
+
.prepare(`SELECT memory.memory_id AS item_id, memory.source_page AS page_id,
|
|
518
|
+
memory.payload_line AS line_start, memory.payload_hash AS source_line_hash,
|
|
519
|
+
COALESCE(fact.subject, entity.label, '') AS routing_subject,
|
|
520
|
+
COALESCE(fact.attribute, '') AS attribute, memory.kind
|
|
521
|
+
FROM managed_memory_entries memory
|
|
522
|
+
LEFT JOIN facts fact ON fact.item_id = memory.memory_id AND fact.valid_to IS NULL
|
|
523
|
+
LEFT JOIN graph_entities entity ON entity.id = memory.subject`)
|
|
460
524
|
.all();
|
|
461
|
-
return new Map(rows.map((row) => [
|
|
525
|
+
return new Map(rows.map((row) => [
|
|
526
|
+
row.item_id,
|
|
527
|
+
{
|
|
528
|
+
item_id: row.item_id,
|
|
529
|
+
page_id: row.page_id,
|
|
530
|
+
line_start: row.line_start,
|
|
531
|
+
source_line_hash: row.source_line_hash,
|
|
532
|
+
routingSubject: row.routing_subject,
|
|
533
|
+
attribute: row.attribute,
|
|
534
|
+
kind: row.kind,
|
|
535
|
+
},
|
|
536
|
+
]));
|
|
462
537
|
}
|
|
463
538
|
function crossPageItemCollisions(pages) {
|
|
464
539
|
const occurrences = new Map();
|
|
@@ -478,7 +553,7 @@ function crossPageItemCollisions(pages) {
|
|
|
478
553
|
}
|
|
479
554
|
return collisions;
|
|
480
555
|
}
|
|
481
|
-
function withBindingFindings(
|
|
556
|
+
function withBindingFindings(candidate, memories, collisions, options) {
|
|
482
557
|
const extra = new Map();
|
|
483
558
|
for (const binding of candidate.inspection.bindings) {
|
|
484
559
|
const collisionKey = `${candidate.row.rel_path}\0${binding.markerLine}`;
|
|
@@ -487,18 +562,14 @@ function withBindingFindings(ctx, candidate, facts, collisions, options) {
|
|
|
487
562
|
extra.set(binding.markerLine, 'item_conflict');
|
|
488
563
|
continue;
|
|
489
564
|
}
|
|
490
|
-
|
|
565
|
+
const memory = memories.get(binding.id);
|
|
566
|
+
if (!memory) {
|
|
491
567
|
extra.set(binding.markerLine, 'source_unavailable');
|
|
492
568
|
continue;
|
|
493
569
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (fact.page_id !== candidate.row.id ||
|
|
500
|
-
fact.line_start !== binding.payloadLine ||
|
|
501
|
-
fact.source_line_hash !== sha256(binding.payload.trim())) {
|
|
570
|
+
if (memory.page_id !== candidate.row.id ||
|
|
571
|
+
memory.line_start !== binding.payloadLine ||
|
|
572
|
+
memory.source_line_hash !== sha256(binding.payload.trim())) {
|
|
502
573
|
extra.set(binding.markerLine, 'item_conflict');
|
|
503
574
|
}
|
|
504
575
|
}
|
|
@@ -512,7 +583,7 @@ function withBindingFindings(ctx, candidate, facts, collisions, options) {
|
|
|
512
583
|
}
|
|
513
584
|
return { ...candidate.inspection, findings };
|
|
514
585
|
}
|
|
515
|
-
async function qualifyManagedItemPlacement(ctx, candidate, inspection,
|
|
586
|
+
async function qualifyManagedItemPlacement(ctx, candidate, inspection, memories) {
|
|
516
587
|
const metrics = emptyPlacementMetrics();
|
|
517
588
|
const blockedIds = new Set(inspection.findings
|
|
518
589
|
.filter((finding) => [
|
|
@@ -520,6 +591,8 @@ async function qualifyManagedItemPlacement(ctx, candidate, inspection, facts) {
|
|
|
520
591
|
'source_unavailable',
|
|
521
592
|
'routing_uncertain',
|
|
522
593
|
'routing_unavailable',
|
|
594
|
+
'routing_oscillation',
|
|
595
|
+
'source_vacated',
|
|
523
596
|
'misrouted_item',
|
|
524
597
|
'routing_deferred',
|
|
525
598
|
].includes(finding.code))
|
|
@@ -544,7 +617,8 @@ async function qualifyManagedItemPlacement(ctx, candidate, inspection, facts) {
|
|
|
544
617
|
}
|
|
545
618
|
const headings = uniqueManagedHeadings(candidate.before);
|
|
546
619
|
const creatableHeadings = new Map(bindings.flatMap((binding) => {
|
|
547
|
-
const
|
|
620
|
+
const memory = memories.get(binding.id);
|
|
621
|
+
const proposed = managedSectionHeading(memory?.attribute || binding.currentHeading || memory?.kind || '');
|
|
548
622
|
return proposed && !hasH2(candidate.page.body, proposed.heading)
|
|
549
623
|
? [[binding.id, proposed]]
|
|
550
624
|
: [];
|
|
@@ -647,7 +721,9 @@ async function qualifyManagedItemPlacement(ctx, candidate, inspection, facts) {
|
|
|
647
721
|
fromHeading: binding.currentHeading,
|
|
648
722
|
toHeading: decision.targetHeading,
|
|
649
723
|
createHeading: decision.createHeading,
|
|
650
|
-
headingSource: decision.createHeading
|
|
724
|
+
headingSource: decision.createHeading
|
|
725
|
+
? memories.get(binding.id)?.attribute || binding.currentHeading || memories.get(binding.id)?.kind
|
|
726
|
+
: undefined,
|
|
651
727
|
});
|
|
652
728
|
}
|
|
653
729
|
}
|
|
@@ -726,6 +802,8 @@ async function qualifyManagedItemSourceBindings(ctx, candidate, inspection) {
|
|
|
726
802
|
'source_unavailable',
|
|
727
803
|
'routing_uncertain',
|
|
728
804
|
'routing_unavailable',
|
|
805
|
+
'routing_oscillation',
|
|
806
|
+
'source_vacated',
|
|
729
807
|
'misrouted_item',
|
|
730
808
|
'routing_deferred',
|
|
731
809
|
].includes(finding.code))
|
|
@@ -957,6 +1035,7 @@ function managedContentKey(marker) {
|
|
|
957
1035
|
}
|
|
958
1036
|
function managedBinding(marker, bodyLine, headings) {
|
|
959
1037
|
const context = headings.get(marker.markerIndex) ?? { heading: null, unique: false };
|
|
1038
|
+
const temporalBoundary = retainedTemporalBoundary(marker.time, marker.payload);
|
|
960
1039
|
return {
|
|
961
1040
|
id: marker.id,
|
|
962
1041
|
origin: marker.sourceRole === 'user' || marker.sourceRole === 'assistant' ? marker.sourceRole : 'unknown',
|
|
@@ -967,6 +1046,7 @@ function managedBinding(marker, bodyLine, headings) {
|
|
|
967
1046
|
payloadIndex: marker.payloadIndex,
|
|
968
1047
|
currentHeading: context.heading,
|
|
969
1048
|
currentHeadingUnique: context.unique && normalizedHeading(context.heading ?? '') !== normalizedHeading('Unsorted'),
|
|
1049
|
+
...(temporalBoundary ? { temporalBoundary } : {}),
|
|
970
1050
|
};
|
|
971
1051
|
}
|
|
972
1052
|
/** A model may judge semantic fit later; these are only the structurally provable placement failures. */
|
|
@@ -1032,7 +1112,7 @@ function uniqueManagedHeadings(content) {
|
|
|
1032
1112
|
function normalizedHeading(value) {
|
|
1033
1113
|
return value.normalize('NFKC').trim().toLowerCase();
|
|
1034
1114
|
}
|
|
1035
|
-
/** Move
|
|
1115
|
+
/** Move the exact owned block and prune only a unique source heading left structurally empty. */
|
|
1036
1116
|
export function applyManagedItemMoves(content, moves) {
|
|
1037
1117
|
let current = content;
|
|
1038
1118
|
for (const move of moves) {
|
|
@@ -1054,7 +1134,7 @@ export function applyManagedItemMoves(content, moves) {
|
|
|
1054
1134
|
}
|
|
1055
1135
|
return { ok: true, content: current };
|
|
1056
1136
|
}
|
|
1057
|
-
/** Move one
|
|
1137
|
+
/** Move one owned block between pages, optionally adding or pruning one structurally bounded heading. */
|
|
1058
1138
|
export function applyManagedItemTransfer(sourceContent, destinationContent, transfer) {
|
|
1059
1139
|
const sourceInspection = inspectManagedItems(sourceContent);
|
|
1060
1140
|
const matches = sourceInspection.bindings.filter((binding) => binding.id === transfer.itemId);
|
|
@@ -1097,43 +1177,13 @@ function applyManagedItemCorrections(content, corrections) {
|
|
|
1097
1177
|
return { ok: true, content: current };
|
|
1098
1178
|
}
|
|
1099
1179
|
function moveManagedBlock(content, binding, targetHeading, createHeading = false, headingSource) {
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
const body = content.slice(frontmatter.bodyOffset);
|
|
1103
|
-
const spans = rawLineSpans(body);
|
|
1104
|
-
const marker = spans[binding.markerIndex];
|
|
1105
|
-
const payload = spans[binding.payloadIndex];
|
|
1106
|
-
if (!marker || !payload || payload.contentEnd < marker.start)
|
|
1180
|
+
const removed = removeManagedBlock(content, binding);
|
|
1181
|
+
if (!removed)
|
|
1107
1182
|
return null;
|
|
1108
|
-
const block = body.slice(marker.start, payload.contentEnd);
|
|
1109
|
-
const without = body.slice(0, marker.start) + body.slice(payload.contentEnd);
|
|
1110
1183
|
if (createHeading) {
|
|
1111
|
-
return insertManagedBlockWithNewHeading(
|
|
1184
|
+
return insertManagedBlockWithNewHeading(removed.content, removed.block, targetHeading, headingSource);
|
|
1112
1185
|
}
|
|
1113
|
-
|
|
1114
|
-
const withoutSpans = rawLineSpans(without);
|
|
1115
|
-
const targets = withoutSpans
|
|
1116
|
-
.map((span, index) => ({
|
|
1117
|
-
index,
|
|
1118
|
-
span,
|
|
1119
|
-
heading: /^\s{0,3}##(?:\s+(.+?)\s*|\s*)$/.exec(span.text)?.[1]?.trim(),
|
|
1120
|
-
}))
|
|
1121
|
-
.filter((entry) => entry.heading && normalizedHeading(entry.heading) === targetKey);
|
|
1122
|
-
if (targets.length !== 1)
|
|
1123
|
-
return null;
|
|
1124
|
-
let insertionOffset = without.length;
|
|
1125
|
-
for (let index = targets[0].index + 1; index < withoutSpans.length; index++) {
|
|
1126
|
-
if (/^\s{0,3}#{1,2}(?:\s+|$)/.test(withoutSpans[index].text)) {
|
|
1127
|
-
insertionOffset = withoutSpans[index].start;
|
|
1128
|
-
break;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
const before = without.slice(0, insertionOffset);
|
|
1132
|
-
const after = without.slice(insertionOffset);
|
|
1133
|
-
const eol = body.includes('\r\n') ? '\r\n' : '\n';
|
|
1134
|
-
const beforeGap = lineGapBefore(before, eol);
|
|
1135
|
-
const afterGap = lineGapAfter(after, eol, body.endsWith(eol));
|
|
1136
|
-
return prefix + before + beforeGap + block + afterGap + after;
|
|
1186
|
+
return insertManagedBlock(removed.content, removed.block, targetHeading);
|
|
1137
1187
|
}
|
|
1138
1188
|
function insertManagedBlockWithNewHeading(content, block, targetHeading, headingSource) {
|
|
1139
1189
|
const guarded = managedSectionHeading(headingSource ?? '');
|
|
@@ -1158,11 +1208,47 @@ function removeManagedBlock(content, binding) {
|
|
|
1158
1208
|
const payload = spans[binding.payloadIndex];
|
|
1159
1209
|
if (!marker || !payload || payload.contentEnd < marker.start)
|
|
1160
1210
|
return null;
|
|
1211
|
+
const block = body.slice(marker.start, payload.contentEnd);
|
|
1212
|
+
const sourceHeadingIndex = sourceHeadingForBinding(spans, binding);
|
|
1213
|
+
if (sourceHeadingIndex !== null) {
|
|
1214
|
+
const heading = spans[sourceHeadingIndex];
|
|
1215
|
+
const sectionContentStart = spans[sourceHeadingIndex + 1]?.start ?? body.length;
|
|
1216
|
+
const sectionEnd = spans.slice(binding.payloadIndex + 1).find((span) => /^\s{0,3}#{1,2}(?:\s+|$)/.test(span.text))
|
|
1217
|
+
?.start ?? body.length;
|
|
1218
|
+
const remainingSection = body.slice(sectionContentStart, marker.start) + body.slice(payload.contentEnd, sectionEnd);
|
|
1219
|
+
if (remainingSection.trim().length === 0) {
|
|
1220
|
+
return {
|
|
1221
|
+
content: prefix + body.slice(0, heading.start) + body.slice(sectionEnd),
|
|
1222
|
+
block,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1161
1226
|
return {
|
|
1162
1227
|
content: prefix + body.slice(0, marker.start) + body.slice(payload.contentEnd),
|
|
1163
|
-
block
|
|
1228
|
+
block,
|
|
1164
1229
|
};
|
|
1165
1230
|
}
|
|
1231
|
+
/** Find only the unique H2 section that structurally owns this binding. */
|
|
1232
|
+
function sourceHeadingForBinding(spans, binding) {
|
|
1233
|
+
if (!binding.currentHeading)
|
|
1234
|
+
return null;
|
|
1235
|
+
for (let index = binding.markerIndex - 1; index >= 0; index--) {
|
|
1236
|
+
const heading = /^\s{0,3}(#{1,2})(?:\s+(.+?)\s*|\s*)$/.exec(spans[index].text);
|
|
1237
|
+
if (!heading)
|
|
1238
|
+
continue;
|
|
1239
|
+
if (heading[1] === '#')
|
|
1240
|
+
return null;
|
|
1241
|
+
const key = normalizedHeading(heading[2]?.trim() ?? '');
|
|
1242
|
+
if (key !== normalizedHeading(binding.currentHeading))
|
|
1243
|
+
return null;
|
|
1244
|
+
const occurrences = spans.filter((span) => {
|
|
1245
|
+
const candidate = /^\s{0,3}##(?:\s+(.+?)\s*|\s*)$/.exec(span.text)?.[1]?.trim();
|
|
1246
|
+
return candidate !== undefined && normalizedHeading(candidate) === key;
|
|
1247
|
+
}).length;
|
|
1248
|
+
return occurrences === 1 ? index : null;
|
|
1249
|
+
}
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1166
1252
|
function insertManagedBlock(content, block, targetHeading) {
|
|
1167
1253
|
const frontmatter = parseFrontmatter(content);
|
|
1168
1254
|
const prefix = content.slice(0, frontmatter.bodyOffset);
|
|
@@ -1307,6 +1393,11 @@ function addRoutingMetrics(target, addition) {
|
|
|
1307
1393
|
target[key] += addition[key];
|
|
1308
1394
|
}
|
|
1309
1395
|
}
|
|
1396
|
+
function retractRoutingMove(metrics, createdHeading) {
|
|
1397
|
+
metrics.moved = Math.max(0, metrics.moved - 1);
|
|
1398
|
+
if (createdHeading)
|
|
1399
|
+
metrics.sectionsCreated = Math.max(0, metrics.sectionsCreated - 1);
|
|
1400
|
+
}
|
|
1310
1401
|
function addSourceMetrics(target, addition) {
|
|
1311
1402
|
for (const key of Object.keys(target)) {
|
|
1312
1403
|
target[key] += addition[key];
|