bantamkit-mcp 0.29.2 → 0.30.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 +1 -1
- package/assets/tools/memory_dream.json +20 -0
- package/assets/tools/repo_map.json +44 -0
- package/dist/eventlog.js +5 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/mcp/pyargs.d.ts +1 -1
- package/dist/mcp/pyargs.js +68 -0
- package/dist/mcp/server.d.ts +36 -6
- package/dist/mcp/server.js +152 -5
- package/dist/memory/cli.js +1 -1
- package/dist/memory/component.d.ts +73 -2
- package/dist/memory/component.js +157 -5
- package/dist/memory/dream.d.ts +247 -0
- package/dist/memory/dream.js +903 -0
- package/dist/memory/store.d.ts +45 -1
- package/dist/memory/store.js +107 -2
- package/dist/repomap.d.ts +247 -0
- package/dist/repomap.js +1040 -0
- package/package.json +1 -1
package/dist/memory/component.js
CHANGED
|
@@ -29,7 +29,8 @@ import { BantamError } from '../errors.js';
|
|
|
29
29
|
import { pyStrip } from './factfile.js';
|
|
30
30
|
import { countFacts, loadGrants, MEMORY_DIR_ENV, PROJECT_STORE, resolveProjectStore, } from './layers.js';
|
|
31
31
|
import { pyHome, pyJoin, pyName, pyParent, pyResolve, PyOSError, PyUnicodeDecodeError } from './pyfs.js';
|
|
32
|
-
import { DEFAULT_INDEX_BUDGET, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyHashKey, pyText, } from './store.js';
|
|
32
|
+
import { DEFAULT_INDEX_BUDGET, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, } from './store.js';
|
|
33
|
+
import { dream as runDream, formatFixed3, SUPERSEDED_HEADING } from './dream.js';
|
|
33
34
|
/**
|
|
34
35
|
* The model's spelling adapted to the store's contract: lowercase, `_`/space -> `-`.
|
|
35
36
|
*
|
|
@@ -177,8 +178,8 @@ export class Memory {
|
|
|
177
178
|
* fact. The floor is the operator's configured default, not a constant, so a consumer who
|
|
178
179
|
* really wants top-1 says so once at construction.
|
|
179
180
|
*/
|
|
180
|
-
recall(query, k = null) {
|
|
181
|
-
return this.recallOutcome(query, k).reply;
|
|
181
|
+
recall(query, k = null, minRatio = RECALL_MIN_SCORE_RATIO) {
|
|
182
|
+
return this.recallOutcome(query, k, minRatio).reply;
|
|
182
183
|
}
|
|
183
184
|
/**
|
|
184
185
|
* `recall`, carrying the walk it performed as numbers rather than as prose.
|
|
@@ -196,8 +197,18 @@ export class Memory {
|
|
|
196
197
|
* refuses to list contributes nothing and raises the `unreadable` count instead of being
|
|
197
198
|
* scored as empty; that distinction is the whole subject of `nothingToReport` below and
|
|
198
199
|
* must not be undone here.
|
|
200
|
+
*
|
|
201
|
+
* `minRatio` (roadmap #6) rides through to every layer's `MemoryStore.recall` unchanged,
|
|
202
|
+
* so the gate is measured against EACH LAYER's own best score and never across layers: a
|
|
203
|
+
* profile fact does not have to out-score the project store's top hit to be admitted,
|
|
204
|
+
* because the two stores are answering as two stores. Its default is
|
|
205
|
+
* `RECALL_MIN_SCORE_RATIO` = 0.0, which keeps every fact `recall` was going to return,
|
|
206
|
+
* and nothing on the tool path passes anything else today. A ratio outside `[0.0, 1.0]`
|
|
207
|
+
* raises out of the FIRST layer, which is the writable project store, so it surfaces as
|
|
208
|
+
* the error it is rather than as an `unreadable` count — the read-only-layer `catch`
|
|
209
|
+
* below would otherwise file a caller's bad argument as a corrupt grant.
|
|
199
210
|
*/
|
|
200
|
-
recallOutcome(query, k = null) {
|
|
211
|
+
recallOutcome(query, k = null, minRatio = RECALL_MIN_SCORE_RATIO) {
|
|
201
212
|
const budget = k === null ? this.k : Math.max(k, this.k);
|
|
202
213
|
const picked = [];
|
|
203
214
|
const seen = new Set();
|
|
@@ -210,7 +221,7 @@ export class Memory {
|
|
|
210
221
|
reached += 1;
|
|
211
222
|
let facts;
|
|
212
223
|
try {
|
|
213
|
-
facts = store.recall(query, budget, writable);
|
|
224
|
+
facts = store.recall(query, budget, writable, minRatio);
|
|
214
225
|
}
|
|
215
226
|
catch (e) {
|
|
216
227
|
if (!(e instanceof BantamError || e instanceof PyOSError || e instanceof PyUnicodeDecodeError)) {
|
|
@@ -312,6 +323,147 @@ export class Memory {
|
|
|
312
323
|
budget: result.budget,
|
|
313
324
|
};
|
|
314
325
|
}
|
|
326
|
+
/** Consolidate what the project and profile layers hold under the same name. */
|
|
327
|
+
dream(dryRun = true) {
|
|
328
|
+
return this.dreamOutcome(dryRun).reply;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* `dream`, with the decision it took carried beside the sentence it wrote.
|
|
332
|
+
*
|
|
333
|
+
* ONLY THE PROFILE LAYER IS CONSUMED. `layers` also carries read-only GRANTS, and a grant
|
|
334
|
+
* is another operator's store: consolidating a fact out of one is not this person's move
|
|
335
|
+
* to make, so `dream` never looks at them. The label is matched exactly (`profile`), never
|
|
336
|
+
* by prefix, because a grant is labelled `extra:<name>` and a prefix match on a directory
|
|
337
|
+
* called `profile-something` would reach one.
|
|
338
|
+
*
|
|
339
|
+
* A `Memory` constructed directly — not through `layered` — has no profile layer at all,
|
|
340
|
+
* and that is `no-profile-layer` rather than an error: there is nothing to consolidate
|
|
341
|
+
* ACROSS when only one layer is bound.
|
|
342
|
+
*
|
|
343
|
+
* `dryRun` DEFAULTS TO TRUE. This is the only op in this component that writes into the
|
|
344
|
+
* user's home directory, and it is the only one whose effect is machine-wide: a fact
|
|
345
|
+
* archived out of the profile store stops answering for every other project on this
|
|
346
|
+
* machine that has no store of its own. A destructive consolidation nobody can preview is
|
|
347
|
+
* not shippable, so the safe call is the short one.
|
|
348
|
+
*/
|
|
349
|
+
dreamOutcome(dryRun = true) {
|
|
350
|
+
const profile = this.layers.find(([label]) => label === 'profile')?.[1] ?? null;
|
|
351
|
+
if (profile === null) {
|
|
352
|
+
return {
|
|
353
|
+
reply: 'nothing to consolidate: no profile layer is bound, so the project ' +
|
|
354
|
+
`store ${this.store.root} is the only layer there is.`,
|
|
355
|
+
status: 'no-profile-layer',
|
|
356
|
+
dryRun,
|
|
357
|
+
merged: 0,
|
|
358
|
+
consumed: 0,
|
|
359
|
+
absolutised: 0,
|
|
360
|
+
superseded: 0,
|
|
361
|
+
indexBefore: 0,
|
|
362
|
+
indexAfter: 0,
|
|
363
|
+
budget: this.store.indexBudget,
|
|
364
|
+
result: null,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const result = runDream(this.store, profile, dryRun);
|
|
368
|
+
let status;
|
|
369
|
+
if (result.overBudget)
|
|
370
|
+
status = 'refused-budget';
|
|
371
|
+
else if (result.changes === 0)
|
|
372
|
+
status = 'nothing-to-consolidate';
|
|
373
|
+
else if (result.applied)
|
|
374
|
+
status = 'consolidated';
|
|
375
|
+
else
|
|
376
|
+
status = 'previewed';
|
|
377
|
+
return {
|
|
378
|
+
reply: Memory.dreamReply(status, result),
|
|
379
|
+
status,
|
|
380
|
+
dryRun: result.dryRun,
|
|
381
|
+
merged: result.merged.length,
|
|
382
|
+
consumed: result.consumed.length,
|
|
383
|
+
absolutised: result.absolutised.length,
|
|
384
|
+
superseded: result.superseded.length,
|
|
385
|
+
indexBefore: result.indexBefore,
|
|
386
|
+
indexAfter: result.indexAfter,
|
|
387
|
+
budget: result.budget,
|
|
388
|
+
result,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The whole diff as prose: what merged, what was dated, what moved, what it cost.
|
|
393
|
+
*
|
|
394
|
+
* THE HONESTY SENTENCE AT THE END IS NOT DECORATION. Row 5 was planned as a token saving
|
|
395
|
+
* and J45-1 measured that it is not one: the profile store has no `index.md` on disk, its
|
|
396
|
+
* index is derived at read time, and nothing loads it into a prompt — so deduplicating it
|
|
397
|
+
* frees approximately zero prompt bytes. The value is that one ruling now has one copy
|
|
398
|
+
* instead of two that can disagree, and the reply says which of those two things the
|
|
399
|
+
* operator just bought.
|
|
400
|
+
*
|
|
401
|
+
* `formatFixed3` and not `toFixed(3)` for the two similarity figures: see the header of
|
|
402
|
+
* `dream.ts`. CPython's `.3f` rounds ties to EVEN and they are reachable.
|
|
403
|
+
*/
|
|
404
|
+
static dreamReply(status, result) {
|
|
405
|
+
const lines = [];
|
|
406
|
+
if (status === 'nothing-to-consolidate') {
|
|
407
|
+
lines.push(`nothing to consolidate: ${result.projectRoot} and ${result.profileRoot} ` +
|
|
408
|
+
'hold no fact under the same name, and no project fact carries a relative ' +
|
|
409
|
+
'date this pass will resolve.');
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const identical = result.merged.filter((merge) => merge.kind === 'identical').length;
|
|
413
|
+
const diverged = result.merged.length - identical;
|
|
414
|
+
const verb = status === 'consolidated' ? 'consolidated' : 'would consolidate';
|
|
415
|
+
lines.push(`${verb} ${result.merged.length} cross-layer name collision(s) — ${identical} ` +
|
|
416
|
+
`byte-identical, ${diverged} diverged and unioned — and rewrote ` +
|
|
417
|
+
`${result.rewritten.length} project fact(s). The survivor stays in the ` +
|
|
418
|
+
`project layer; the profile copy moves to ${result.archiveDir} and is NOT ` +
|
|
419
|
+
'deleted: restore it by name.');
|
|
420
|
+
}
|
|
421
|
+
for (const merge of result.merged) {
|
|
422
|
+
lines.push(`- ${pyText(merge.name)} (${merge.kind}, name+description similarity ` +
|
|
423
|
+
`${formatFixed3(merge.jaccard)}) — body ${merge.bodyBefore} -> ${merge.bodyAfter} ` +
|
|
424
|
+
`bytes, ${merge.blocksAdded} block(s) kept from the ${merge.consumedLayer} ` +
|
|
425
|
+
`copy, survivor in ${merge.survivorLayer}`);
|
|
426
|
+
}
|
|
427
|
+
for (const record of result.superseded) {
|
|
428
|
+
lines.push(`- superseded '${record.subject}': the ${record.lostLayer} copy ` +
|
|
429
|
+
`(${record.lostDate}) lost to the ${record.keptLayer} copy ` +
|
|
430
|
+
`(${record.keptDate}); the older claim is kept verbatim under ` +
|
|
431
|
+
`'${SUPERSEDED_HEADING}'`);
|
|
432
|
+
}
|
|
433
|
+
for (const hit of result.absolutised) {
|
|
434
|
+
lines.push(`- dated ${pyText(hit.name)} (${hit.layer}): '${hit.term}' -> ${hit.resolved}, ` +
|
|
435
|
+
`resolved against that fact's own mtime ${hit.basis}, not today`);
|
|
436
|
+
}
|
|
437
|
+
for (const hit of result.unresolved) {
|
|
438
|
+
lines.push(`- left alone in ${pyText(hit.name)} (${hit.layer}): '${hit.term}' — no exact day ` +
|
|
439
|
+
'follows from an mtime, so nothing was substituted');
|
|
440
|
+
}
|
|
441
|
+
for (const [name, reason] of result.refused) {
|
|
442
|
+
lines.push(`- refused ${pyText(name)}: ${reason}`);
|
|
443
|
+
}
|
|
444
|
+
for (const pair of result.similarUnmerged) {
|
|
445
|
+
lines.push(`- similar but NOT merged: ${pyText(pair.projectName)} (project) and ` +
|
|
446
|
+
`${pyText(pair.profileName)} (profile) score ${formatFixed3(pair.jaccard)}; this pass merges ` +
|
|
447
|
+
'on name equality only, so nothing was done about it');
|
|
448
|
+
}
|
|
449
|
+
lines.push(`project index ${result.indexBefore} -> ${result.indexAfter} bytes against a ` +
|
|
450
|
+
`${result.budget}-byte budget; the profile index (derived, no file on disk) ` +
|
|
451
|
+
`${result.profileIndexBefore} -> ${result.profileIndexAfter}; fact bytes ` +
|
|
452
|
+
`across both layers ${result.factBytesBefore} -> ${result.factBytesAfter}.`);
|
|
453
|
+
if (status === 'refused-budget') {
|
|
454
|
+
lines.push(`NOTHING WAS WRITTEN: the merged index would be ${result.indexAfter} bytes ` +
|
|
455
|
+
`against a ${result.budget}-byte budget. Call \`memory_compact\` to archive ` +
|
|
456
|
+
'the stalest facts, then run this again.');
|
|
457
|
+
}
|
|
458
|
+
else if (result.dryRun) {
|
|
459
|
+
lines.push('DRY RUN: nothing was written. Re-run with dry_run false to apply it.');
|
|
460
|
+
}
|
|
461
|
+
lines.push('This is a correctness pass, not a token saving: the profile index is derived ' +
|
|
462
|
+
'at read time and is not loaded from a file, so consolidating it frees ' +
|
|
463
|
+
'approximately no prompt bytes. What it buys is one copy of a ruling instead ' +
|
|
464
|
+
'of two that can diverge.');
|
|
465
|
+
return lines.join('\n');
|
|
466
|
+
}
|
|
315
467
|
/**
|
|
316
468
|
* `(index bytes, budget)` for the writable project store; bytes may be `null`.
|
|
317
469
|
*
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { FactValue } from './factfile.js';
|
|
2
|
+
import { MemoryStore } from './store.js';
|
|
3
|
+
/**
|
|
4
|
+
* The two layer names this pass knows. A read-only GRANT is deliberately not one of them: a
|
|
5
|
+
* grant is another operator's store, and consuming a fact out of it is not this person's to
|
|
6
|
+
* do. Grants are neither merged nor scanned.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PROJECT_LAYER = "project";
|
|
9
|
+
export declare const PROFILE_LAYER = "profile";
|
|
10
|
+
/**
|
|
11
|
+
* The heading the union writes above claims a contradiction retired. Distinctive on
|
|
12
|
+
* purpose: a body that already contains an ordinary `## superseded` section must not be
|
|
13
|
+
* mistaken for one this pass wrote.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SUPERSEDED_HEADING = "## superseded by a dream merge";
|
|
16
|
+
/**
|
|
17
|
+
* One relative term found in a body, and what — if anything — it resolved to.
|
|
18
|
+
*
|
|
19
|
+
* `resolved` is `''` for a term in `UNRESOLVED`: the term was FOUND and REPORTED and the body
|
|
20
|
+
* was not touched. `basis` is always the ISO date the arithmetic ran against, which is the
|
|
21
|
+
* fact file's own mtime and never today's date — a fact written in August that says "today"
|
|
22
|
+
* means a day in August, and resolving it against the day the dream runs is how a
|
|
23
|
+
* consolidation pass invents history.
|
|
24
|
+
*/
|
|
25
|
+
export interface DateHit {
|
|
26
|
+
readonly name: FactValue;
|
|
27
|
+
readonly layer: string;
|
|
28
|
+
readonly term: string;
|
|
29
|
+
readonly resolved: string;
|
|
30
|
+
readonly basis: string;
|
|
31
|
+
}
|
|
32
|
+
/** A claim that lost a contradiction, kept verbatim so nothing is silently dropped. */
|
|
33
|
+
export interface Superseded {
|
|
34
|
+
readonly subject: string;
|
|
35
|
+
readonly kept: string;
|
|
36
|
+
readonly keptLayer: string;
|
|
37
|
+
readonly keptDate: string;
|
|
38
|
+
readonly lost: string;
|
|
39
|
+
readonly lostLayer: string;
|
|
40
|
+
readonly lostDate: string;
|
|
41
|
+
}
|
|
42
|
+
/** One consolidated name: what it was, what it became, and where each half went. */
|
|
43
|
+
export interface DreamMerge {
|
|
44
|
+
readonly name: FactValue;
|
|
45
|
+
readonly kind: 'identical' | 'diverged';
|
|
46
|
+
readonly jaccard: number;
|
|
47
|
+
readonly survivorLayer: string;
|
|
48
|
+
readonly consumedLayer: string;
|
|
49
|
+
readonly bodyBefore: number;
|
|
50
|
+
readonly bodyAfter: number;
|
|
51
|
+
readonly blocksAdded: number;
|
|
52
|
+
readonly superseded: readonly Superseded[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A cross-layer pair similarity would have merged and NAME EQUALITY DID NOT FIND.
|
|
56
|
+
*
|
|
57
|
+
* Reported, never acted on. J45-1 measured this population at ZERO on the live stores. It is
|
|
58
|
+
* carried so a future store that grows a paraphrase is visible rather than silently
|
|
59
|
+
* unmerged, and so that "similarity contributes nothing today" is a number a reader can
|
|
60
|
+
* check instead of a sentence they have to trust.
|
|
61
|
+
*/
|
|
62
|
+
export interface SimilarPair {
|
|
63
|
+
readonly projectName: FactValue;
|
|
64
|
+
readonly profileName: FactValue;
|
|
65
|
+
readonly jaccard: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The diff. Everything a caller needs to see what a dream did, or would do.
|
|
69
|
+
*
|
|
70
|
+
* `applied` is FALSE for a dry run and for a plan refused by the budget, and it is the only
|
|
71
|
+
* field that says whether anything on disk moved. The byte fields are projections computed
|
|
72
|
+
* the same way in both modes, so a preview and the run it previews report the same
|
|
73
|
+
* arithmetic rather than two numbers a reader has to reconcile.
|
|
74
|
+
*
|
|
75
|
+
* `overBudget`, `changes` and `superseded` are `@property` on the reference and plain fields
|
|
76
|
+
* here — the values are the same and nothing mutates the result.
|
|
77
|
+
*/
|
|
78
|
+
export interface DreamResult {
|
|
79
|
+
readonly applied: boolean;
|
|
80
|
+
readonly dryRun: boolean;
|
|
81
|
+
readonly merged: readonly DreamMerge[];
|
|
82
|
+
readonly refused: readonly (readonly [FactValue, string])[];
|
|
83
|
+
readonly absolutised: readonly DateHit[];
|
|
84
|
+
readonly unresolved: readonly DateHit[];
|
|
85
|
+
readonly similarUnmerged: readonly SimilarPair[];
|
|
86
|
+
readonly consumed: readonly FactValue[];
|
|
87
|
+
readonly rewritten: readonly FactValue[];
|
|
88
|
+
readonly archiveDir: string;
|
|
89
|
+
readonly indexBefore: number;
|
|
90
|
+
readonly indexAfter: number;
|
|
91
|
+
readonly budget: number;
|
|
92
|
+
readonly profileIndexBefore: number;
|
|
93
|
+
readonly profileIndexAfter: number;
|
|
94
|
+
readonly factBytesBefore: number;
|
|
95
|
+
readonly factBytesAfter: number;
|
|
96
|
+
readonly projectRoot: string;
|
|
97
|
+
readonly profileRoot: string;
|
|
98
|
+
/** Would the project index not fit after this merge? Then nothing is written. */
|
|
99
|
+
readonly overBudget: boolean;
|
|
100
|
+
/** Merges plus rewritten facts — the count that decides `nothing-to-consolidate`. */
|
|
101
|
+
readonly changes: number;
|
|
102
|
+
readonly superseded: readonly Superseded[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `int(text)` for a run of `\p{Nd}` digits, which is what CPython's `int()` accepts.
|
|
106
|
+
*
|
|
107
|
+
* A decimal-digit block is exactly ten consecutive codepoints, so the value of a digit is
|
|
108
|
+
* its distance from the block's zero and the block's zero is found by walking back while the
|
|
109
|
+
* previous codepoint is still `Nd` — no table, and correct for every script including the
|
|
110
|
+
* ones assigned after this file was written.
|
|
111
|
+
*/
|
|
112
|
+
export declare function pyIntDigits(text: string): number;
|
|
113
|
+
/**
|
|
114
|
+
* `f"{value:.3f}"` — round HALF TO EVEN on the exact binary value of the double.
|
|
115
|
+
*
|
|
116
|
+
* NOT `toFixed(3)`, which rounds ties AWAY from zero. The reachable ties are exactly the
|
|
117
|
+
* odd sixteenths (a tie at three places needs the exact value to be `k/10000` with `k`
|
|
118
|
+
* ending in 5, and only `j/16` is both that and a double), and four of the eight disagree:
|
|
119
|
+
* CPython prints `0.062`, `0.312`, `0.562`, `0.812` where `toFixed` prints `0.063`, `0.313`,
|
|
120
|
+
* `0.563`, `0.813`. A Jaccard of `1/16` is one shared token in a sixteen-token union, which
|
|
121
|
+
* is an ordinary pair and not a contrived one.
|
|
122
|
+
*
|
|
123
|
+
* The arithmetic is exact: the double is decomposed to `mantissa * 2**exponent` over
|
|
124
|
+
* `BigInt`, so nothing here rounds twice.
|
|
125
|
+
*/
|
|
126
|
+
export declare function formatFixed3(value: number): string;
|
|
127
|
+
/**
|
|
128
|
+
* `sorted(values)` over what a `Fact.name` can hold — CPython's `listsort` for a single run.
|
|
129
|
+
*
|
|
130
|
+
* Every name a `str` is the total order `cmpCodepoint` gives. Anything else can RAISE, and
|
|
131
|
+
* then WHICH pair CPython happened to probe decides the sentence, so the slow path is
|
|
132
|
+
* `count_run` + `binarysort` exactly as `sortScored` spells it: pivot on the LEFT of every
|
|
133
|
+
* probe, midpoint `l + ((r - l) >> 1)`, `==` short-circuiting `<` so two facts both named
|
|
134
|
+
* `None` order rather than raise.
|
|
135
|
+
*
|
|
136
|
+
* THE EDGE, STATED: the reference sorts a `set`, whose iteration order is hash-derived, so
|
|
137
|
+
* for a list that RAISES the two runtimes can name a different pair of types. Where the
|
|
138
|
+
* order exists — every name a `str`, which is every store `save` has ever written — the
|
|
139
|
+
* answer is identical.
|
|
140
|
+
*/
|
|
141
|
+
export declare function sortedNames(values: readonly FactValue[]): FactValue[];
|
|
142
|
+
/** One line, single-spaced. The basis of every comparison key in this module. */
|
|
143
|
+
export declare function collapse(text: string): string;
|
|
144
|
+
/**
|
|
145
|
+
* The identity of a block for set-union purposes: collapsed whitespace, lowercased.
|
|
146
|
+
*
|
|
147
|
+
* Punctuation is deliberately NOT stripped. A rule that ignored it would call two sentences
|
|
148
|
+
* the same because one ends in a question mark, and this pass is allowed to repeat a claim
|
|
149
|
+
* but never to drop one.
|
|
150
|
+
*/
|
|
151
|
+
export declare function blockKey(text: string): string;
|
|
152
|
+
/**
|
|
153
|
+
* Paragraphs: non-empty runs separated by a blank line, each stripped.
|
|
154
|
+
*
|
|
155
|
+
* Line endings are normalised first, so the same body blocks identically whether it was
|
|
156
|
+
* written on Windows or on POSIX — the two runtimes must agree on the block list before they
|
|
157
|
+
* can agree on the union of two of them.
|
|
158
|
+
*/
|
|
159
|
+
export declare function splitBlocks(text: string): string[];
|
|
160
|
+
/**
|
|
161
|
+
* `[subject key, value key]` when a block is a single-line `Subject: value`, else `null`.
|
|
162
|
+
*
|
|
163
|
+
* See `SLOT` for why this is narrow. It exists only so that a genuine CONTRADICTION — the
|
|
164
|
+
* same slot given two different values — can be told apart from two claims that are merely
|
|
165
|
+
* different, which is the whole of what union already handles.
|
|
166
|
+
*/
|
|
167
|
+
export declare function claimSlot(block: string): [string, string] | null;
|
|
168
|
+
/**
|
|
169
|
+
* `[body, resolved hits, unresolved hits]` — a relative date annotated with its day.
|
|
170
|
+
*
|
|
171
|
+
* THE TERM IS KEPT AND THE DATE IS ADDED: `today` becomes `today (2026-08-27)`. Deleting the
|
|
172
|
+
* word would rewrite the sentence a person wrote; appending the day says which date was
|
|
173
|
+
* substituted and leaves the reader able to disagree with it. A term already carrying a
|
|
174
|
+
* stamp is skipped, which is what makes a second dream over the same body a no-op.
|
|
175
|
+
*
|
|
176
|
+
* A term whose arithmetic leaves the calendar (see `addDays`) is REPORTED as unresolved and
|
|
177
|
+
* the body is left alone — the same treatment `recently` and `last month` get, for the same
|
|
178
|
+
* reason: this pass may say which day it substituted, or say it could not, and never invent
|
|
179
|
+
* one. Those hits come FIRST in the unresolved list, in the order the relative scan met
|
|
180
|
+
* them, followed by the `UNRESOLVED` matches in the order the second scan met them, which
|
|
181
|
+
* is the order `dream.py` builds the same list in.
|
|
182
|
+
*/
|
|
183
|
+
export declare function absolutise(body: string, basis: string, name: FactValue, layer: string): [string, DateHit[], DateHit[]];
|
|
184
|
+
/**
|
|
185
|
+
* Both descriptions, `; `-joined, unless they are the same line of text.
|
|
186
|
+
*
|
|
187
|
+
* The description is what `recall` SCORES against, so dropping one side's would cost the
|
|
188
|
+
* survivor the query vocabulary that half of the pair used to answer. Equality is on the
|
|
189
|
+
* collapsed, lowercased key — the same key every other comparison here uses — and nothing
|
|
190
|
+
* cleverer: a containment or similarity rule would be a second thing to reproduce byte for
|
|
191
|
+
* byte, and the index budget already bounds the cost of being literal.
|
|
192
|
+
*/
|
|
193
|
+
export declare function mergeDescriptions(base: FactValue, other: FactValue): string;
|
|
194
|
+
/** Ordered set union, base first, exact string identity. Order is the port's contract. */
|
|
195
|
+
export declare function mergeLinks(base: readonly FactValue[], other: readonly FactValue[]): FactValue[];
|
|
196
|
+
/**
|
|
197
|
+
* UNION, never a winner. `[body, blocks taken from other, superseded records]`.
|
|
198
|
+
*
|
|
199
|
+
* THE ALGORITHM, stated as the contract both runtimes reproduce exactly:
|
|
200
|
+
*
|
|
201
|
+
* 1. Split both bodies into blocks (`splitBlocks`).
|
|
202
|
+
* 2. Find CONTRADICTIONS: a `claimSlot` subject present on both sides with two different
|
|
203
|
+
* value keys. The side whose FACT FILE has the later mtime wins; a tie goes to `base`,
|
|
204
|
+
* which is the writable project layer. The loser is removed from the body sequence and
|
|
205
|
+
* recorded in a `Superseded` entry — never dropped.
|
|
206
|
+
* 3. Emit every surviving BASE block in source order, skipping any whose `blockKey` has
|
|
207
|
+
* already been emitted.
|
|
208
|
+
* 4. Emit every surviving OTHER block in source order, same skip. Count these.
|
|
209
|
+
* 5. If anything was superseded, append one block: `SUPERSEDED_HEADING`, a blank line, and
|
|
210
|
+
* one `- from <layer> (<date>): <collapsed text>` line per record, sorted by subject.
|
|
211
|
+
* 6. Join with a blank line.
|
|
212
|
+
*
|
|
213
|
+
* WHY MTIME AND NOT `created` DECIDES A CONTRADICTION. J45-1 measured all three obvious
|
|
214
|
+
* clocks on the real diverged pair and every one picks wrong: newest `created` and longest
|
|
215
|
+
* body each pick the profile copy and lose the project copy's amended paragraph,
|
|
216
|
+
* project-layer-wins drops ~1.7 kB of the profile copy, and `last_recalled` is the same date
|
|
217
|
+
* on both. `created` is FIRST-LANDING and the older-created fact there holds the NEWER
|
|
218
|
+
* content, so it is actively backwards. The file's mtime is the only clock on disk that
|
|
219
|
+
* records when the text was last written, and it is the same clock `absolutise` uses.
|
|
220
|
+
*
|
|
221
|
+
* WHY UNION MAY REPEAT A CLAIM. Two paragraphs that say the same thing in different words
|
|
222
|
+
* have different `blockKey`s and both survive. That is the deliberate direction of the
|
|
223
|
+
* error: a repeated claim is something the user can delete in one edit, and a dropped ruling
|
|
224
|
+
* is not recoverable from the merged fact at all.
|
|
225
|
+
*/
|
|
226
|
+
export declare function mergeBodies(baseBody: string, otherBody: string, baseLayer: string, otherLayer: string, baseDate: string, otherDate: string): [string, number, Superseded[]];
|
|
227
|
+
/**
|
|
228
|
+
* Consolidate the facts the two layers hold under the same NAME. Reversible, bounded.
|
|
229
|
+
*
|
|
230
|
+
* `dryRun` defaults to TRUE because this pass writes into the user's home directory and a
|
|
231
|
+
* destructive consolidation nobody can preview is not shippable. A dry run performs every
|
|
232
|
+
* read, every merge and every projection and writes nothing.
|
|
233
|
+
*
|
|
234
|
+
* THE BUDGET IS `compact()`'s BUDGET AND NOT A SECOND CAP. The only index this pass can grow
|
|
235
|
+
* is the project one, and it can grow it only by the bytes a unioned DESCRIPTION adds —
|
|
236
|
+
* every merged name is already a line in that index, so no line is ever added. The projected
|
|
237
|
+
* index is measured exactly the way `checkIndexBudget` measures it, and a plan that would
|
|
238
|
+
* not fit is returned unapplied with `overBudget` set, for the caller to report. It is not
|
|
239
|
+
* raised: a preview whose answer is an exception has told the operator nothing about the
|
|
240
|
+
* plan they asked to see.
|
|
241
|
+
*
|
|
242
|
+
* WRITE ORDER IS SURVIVORS FIRST, CONSUMPTION SECOND, and it is chosen for the direction of
|
|
243
|
+
* its failure. If the process dies between the two, the survivor holds everything and the
|
|
244
|
+
* profile copy is still live — a duplicate that the next dream consolidates. The other order
|
|
245
|
+
* would leave a window in which the profile copy is archived and the union has not landed.
|
|
246
|
+
*/
|
|
247
|
+
export declare function dream(project: MemoryStore, profile: MemoryStore, dryRun?: boolean): DreamResult;
|