renor-engine 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. package/package.json +120 -4
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keywordSearch = keywordSearch;
4
+ const readCache_1 = require("../db/readCache");
5
+ const statements_1 = require("../db/statements");
6
+ const semantic_1 = require("./semantic");
7
+ const ROWS = 'SELECT c.id AS chunkId, c.note_path AS notePath, c.heading_path AS headingPath, c.text, f.score AS score';
8
+ /**
9
+ * get_context's first keyword window is `topK * 10` (60 for the 6 it asks) and the next read, when that window is
10
+ * turned away, is four times that (240). A wider page is a walk toward every match; keeping its text is not worth it.
11
+ */
12
+ const CACHED_PAGE_LIMIT = 256;
13
+ /** Distinct pages kept per connection. The oldest goes when another is kept. */
14
+ const CACHED_PAGE_COUNT = 256;
15
+ const cachedPages = new WeakMap();
16
+ function copyHits(hits) {
17
+ return hits.map(hit => ({ chunkId: hit.chunkId, notePath: hit.notePath, headingPath: hit.headingPath, text: hit.text, score: hit.score }));
18
+ }
19
+ /**
20
+ * The `limit` chunks that best match an FTS5 `match` expression, best first, and of equal scores by place (byPlace).
21
+ * BM25 ties often (one sentence in several notes, one word once in chunks of one length), and FTS5 gives ties in
22
+ * row-id order, which is the order indexing wrote the notes in: concurrent indexing does not fix it.
23
+ *
24
+ * Ranked and cut inside the full-text query, which reads no chunk row, one row past the page: joining `chunks` before
25
+ * the cut would read the text of every matching chunk (thousands, for a common word) to keep a few. Only when that
26
+ * extra row scores like the last one does the page end inside a tie; then every chunk with that score is read, so the
27
+ * tie is cut by place too.
28
+ *
29
+ * The same page is kept until this connection or another writes. The stamp is taken before the ranking and the page is
30
+ * kept only when that stamp is still current: a write during the ranking is stored under neither stamp, and nothing is
31
+ * kept inside a transaction. Callers may change the hits they are given, so both the kept page and a hit are copies.
32
+ */
33
+ function keywordSearch(db, match, limit) {
34
+ if (limit < 1)
35
+ return [];
36
+ const key = `${limit}\0${match}`;
37
+ const before = limit <= CACHED_PAGE_LIMIT ? (0, readCache_1.databaseStamp)(db) : null;
38
+ if (before !== null) {
39
+ const held = cachedPages.get(db)?.get(key);
40
+ if (held && held.stamp === before)
41
+ return copyHits(held.hits);
42
+ }
43
+ const rows = rankKeywordHits(db, match, limit);
44
+ if (before !== null && (0, readCache_1.databaseStamp)(db) === before) {
45
+ let pages = cachedPages.get(db);
46
+ if (!pages)
47
+ cachedPages.set(db, pages = new Map());
48
+ if (!pages.has(key) && pages.size >= CACHED_PAGE_COUNT)
49
+ pages.delete(pages.keys().next().value);
50
+ pages.set(key, { stamp: before, hits: copyHits(rows) });
51
+ }
52
+ return rows;
53
+ }
54
+ function rankKeywordHits(db, match, limit) {
55
+ // The page is the `limit` best rows, read with one more ranked row that is not returned: its score (`next`) says whether
56
+ // the page ends inside a tie. The ranking is read once, and the extra row costs no chunk text and no row of its own.
57
+ const paged = (0, statements_1.prepareOnce)(db, `WITH ranked AS MATERIALIZED (
58
+ SELECT rowid AS rid, bm25(chunks_fts) AS score
59
+ FROM chunks_fts
60
+ WHERE chunks_fts MATCH ?
61
+ ORDER BY score
62
+ LIMIT ?
63
+ )
64
+ SELECT c.id AS chunkId, c.note_path AS notePath, c.heading_path AS headingPath, c.text, r.score AS score,
65
+ (SELECT score FROM ranked ORDER BY score LIMIT 1 OFFSET ?) AS next
66
+ FROM ranked r
67
+ JOIN chunks c ON c.id = r.rid
68
+ ORDER BY r.score
69
+ LIMIT ?`).all(match, limit + 1, limit, limit);
70
+ const page = paged.map(({ chunkId, notePath, headingPath, text, score }) => ({ chunkId, notePath, headingPath, text, score }));
71
+ const edge = page.length === limit && paged[0].next === page[limit - 1].score ? page[limit - 1].score : null;
72
+ const rows = edge === null ? page : [
73
+ ...page.filter(hit => hit.score < edge),
74
+ ...(0, statements_1.prepareOnce)(db, `${ROWS}
75
+ FROM (
76
+ SELECT rowid, bm25(chunks_fts) AS score
77
+ FROM chunks_fts
78
+ WHERE chunks_fts MATCH ?
79
+ ) f
80
+ JOIN chunks c ON c.id = f.rowid
81
+ WHERE f.score = ?`).all(match, edge),
82
+ ];
83
+ return rows.sort((a, b) => a.score - b.score || (0, semantic_1.byPlace)(a, b)).slice(0, limit);
84
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLedgerEventClock = createLedgerEventClock;
4
+ const readCache_1 = require("../db/readCache");
5
+ const statements_1 = require("../db/statements");
6
+ const lifecycle_1 = require("../memory/lifecycle");
7
+ const ledgerLiveness_1 = require("./ledgerLiveness");
8
+ /**
9
+ * When the ledger events a hit carries happened.
10
+ *
11
+ * The ledger is ONE note, so the date of the note (when the file was last written) is the same for every event
12
+ * in it, and says nothing about when any of them was saved. Each event has its own time: the lifecycle layer
13
+ * projects every event into `facts` (event id, `valid_from` = the event's time), the same projection a scoped
14
+ * read asks which scope an event belongs to (mvp/readScope.ts). That row, not the header text of a chunk, is
15
+ * what makes an id an event, so a line that only looks like an event header in some other note dates nothing.
16
+ *
17
+ * `valid_from` is the canonical UTC ISO form (ms precision, `Z`) the ledger writer and replay both enforce, so
18
+ * comparing two of them as strings is comparing the times.
19
+ *
20
+ * Nothing is read until a hit carries an event id: text that holds none costs no query, and the one statement
21
+ * is prepared once however many hits ask.
22
+ */
23
+ function createLedgerEventClock(db) {
24
+ let lookup; // undefined: not needed yet; null: no projection to ask
25
+ const known = new Map();
26
+ const timeOf = (id) => {
27
+ const key = id.toUpperCase(); // projected ids are upper-case, and the unique index on facts(event_id) serves the lookup
28
+ if (!known.has(key)) {
29
+ if (lookup === undefined)
30
+ lookup = prepareLookup(db);
31
+ known.set(key, lookup?.get(key, lifecycle_1.LIFECYCLE_NOTE_PATH)?.at);
32
+ }
33
+ return known.get(key);
34
+ };
35
+ // A chunk is one event (the chunker splits at every event heading); one that carries several, as a legacy or a
36
+ // hand-merged note can, is as new as the newest of them, the way a note is as new as its last write.
37
+ return hit => {
38
+ let newest;
39
+ for (const id of (0, ledgerLiveness_1.ledgerEventIds)(hit)) {
40
+ const at = timeOf(id);
41
+ if (at !== undefined && (newest === undefined || at > newest))
42
+ newest = at;
43
+ }
44
+ return newest;
45
+ };
46
+ }
47
+ function prepareLookup(db) {
48
+ if (!(0, readCache_1.tableHasColumns)(db, 'facts', ['event_id', 'source_path']))
49
+ return null;
50
+ return (0, statements_1.prepareOnce)(db, 'SELECT valid_from AS at FROM facts WHERE event_id = ? AND source_path = ?');
51
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SUPERSEDED_LABEL = void 0;
4
+ exports.closedLedgerEvents = closedLedgerEvents;
5
+ exports.ledgerEventIds = ledgerEventIds;
6
+ exports.resolveLedgerHits = resolveLedgerHits;
7
+ const readCache_1 = require("../db/readCache");
8
+ const statements_1 = require("../db/statements");
9
+ /**
10
+ * The lifecycle projection is updated on write, independently of the chunk
11
+ * index. Resolve event identity here rather than comparing object strings:
12
+ * A -> B -> A still closes the first A, and unindexed corrections take effect.
13
+ */
14
+ function closedLedgerEvents(db) {
15
+ if (!(0, readCache_1.tableHasColumns)(db, 'facts', ['event_id']))
16
+ return Object.assign(new Set(), { hidden: new Set() });
17
+ // One read serves every ask until a fact is written: a search asks several times (each lane, each page of a read).
18
+ return (0, readCache_1.readUntilWrite)(db, 'closedLedgerEvents', () => {
19
+ const rows = (0, statements_1.prepareOnce)(db, `SELECT event_id FROM facts
20
+ WHERE source_path = '_mcp_' AND event_id IS NOT NULL AND valid_to IS NOT NULL`).all();
21
+ const hidden = (0, readCache_1.tableHasColumns)(db, 'retired_facts', ['event_id']) && (0, readCache_1.tableHasColumns)(db, 'fact_retirements', ['event_id'])
22
+ ? (0, statements_1.prepareOnce)(db, 'SELECT event_id FROM retired_facts UNION SELECT event_id FROM fact_retirements').all()
23
+ : [];
24
+ const hiddenIds = new Set(hidden.map(r => r.event_id.toLowerCase()));
25
+ return Object.assign(new Set([...rows.map(r => r.event_id.toLowerCase()), ...hiddenIds]), { hidden: hiddenIds });
26
+ });
27
+ }
28
+ /**
29
+ * The ledger events (lower-case ids) a hit carries: the ids on its `## MEM_… · ` headers or, for a chunk cut
30
+ * below its header, the id in its heading path. Empty for anything that is not a ledger event.
31
+ */
32
+ function ledgerEventIds(hit) {
33
+ // A text without `mem_` has no `## MEM_… · ` header (NAMES_AN_EVENT, below): most hits, asked about several times each.
34
+ const ids = NAMES_AN_EVENT.test(hit.text) ? [...hit.text.matchAll(/^## (MEM_[A-Za-z0-9]+) · /gim)].map(m => m[1].toLowerCase()) : [];
35
+ if (ids.length)
36
+ return ids;
37
+ const fromHeading = hit.headingPath?.match(/\b(MEM_[A-Za-z0-9]+)\b/i)?.[1];
38
+ return fromHeading ? [fromHeading.toLowerCase()] : [];
39
+ }
40
+ /** Whether a text can name a ledger event at all: every pattern that reads an event id below starts with `MEM_`, in either case. */
41
+ const NAMES_AN_EVENT = /mem_/i;
42
+ /** The line that opens a superseded event shown on request (include_history). */
43
+ exports.SUPERSEDED_LABEL = '[Superseded memory event; historical evidence only]';
44
+ /**
45
+ * Also handles legacy/large chunks containing several ledger event blocks. An event in `closed.hidden`
46
+ * (closedLedgerEvents) is dropped even with includeHistory.
47
+ */
48
+ function resolveLedgerHits(hits, closed, includeHistory) {
49
+ if (!closed.size)
50
+ return hits;
51
+ const hidden = closed.hidden;
52
+ return hits.flatMap(hit => {
53
+ // An event is named by `## MEM_… · ` in the text or by `MEM_…` in the heading path, in either case. A hit with
54
+ // neither holds no block to close, so it comes back as it is, without the split and the matches below.
55
+ if (!NAMES_AN_EVENT.test(hit.text) && !(hit.headingPath && NAMES_AN_EVENT.test(hit.headingPath)))
56
+ return [hit];
57
+ let superseded = false;
58
+ let dropped = false;
59
+ const blocks = hit.text.replace(/\r\n/g, '\n').split(/(?=^## MEM_[A-Za-z0-9]+ · )/im);
60
+ const kept = blocks.flatMap(block => {
61
+ const id = block.match(/^## (MEM_[A-Za-z0-9]+) · /im)?.[1]
62
+ ?? (blocks.length === 1 ? hit.headingPath?.match(/\b(MEM_[A-Za-z0-9]+)\b/i)?.[1] : undefined);
63
+ const isClosed = !!id && closed.has(id.toLowerCase());
64
+ if (!isClosed)
65
+ return [block];
66
+ if (hidden?.has(id.toLowerCase())) {
67
+ dropped = true;
68
+ return [];
69
+ }
70
+ superseded = true;
71
+ return includeHistory ? [`${exports.SUPERSEDED_LABEL}\n${block}`] : [];
72
+ });
73
+ if (!superseded && !dropped)
74
+ return [hit];
75
+ const text = kept.join('').trim();
76
+ const history = includeHistory && superseded;
77
+ if (!text || (!history && !kept.some(b => /^## MEM_/im.test(b))))
78
+ return [];
79
+ return [{ ...hit, text,
80
+ ...(!history ? { headingPath: text.match(/^## (MEM_[A-Za-z0-9]+ · .+)$/im)?.[1] ?? hit.headingPath } : {}),
81
+ reason: history ? `${hit.reason}+superseded` : hit.reason }];
82
+ });
83
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ledgerNoteMatcher = ledgerNoteMatcher;
7
+ exports.isLedgerPreamble = isLedgerPreamble;
8
+ exports.dropLedgerPreamble = dropLedgerPreamble;
9
+ exports.keywordLedgerHits = keywordLedgerHits;
10
+ exports.presentLedgerEvents = presentLedgerEvents;
11
+ exports.withoutLedgerMetadata = withoutLedgerMetadata;
12
+ exports.presentEvents = presentEvents;
13
+ exports.factText = factText;
14
+ const path_1 = __importDefault(require("path"));
15
+ const inventory_1 = require("../memory/inventory");
16
+ const ledgerLiveness_1 = require("./ledgerLiveness");
17
+ function ledgerNoteMatcher(vault) {
18
+ const stored = (0, inventory_1.ledgerNotePath)(vault);
19
+ if (stored === null)
20
+ return () => false; // a path that cannot name a file names no ledger
21
+ const shown = path_1.default.relative(path_1.default.resolve(vault.memoryDir), stored);
22
+ return notePath => notePath === stored || notePath === shown;
23
+ }
24
+ /** A chunk of the ledger that no event owns: its header (nothing else comes before the first event). */
25
+ function isLedgerPreamble(hit, isLedger) {
26
+ return isLedger !== undefined && isLedger(hit.notePath) && (0, ledgerLiveness_1.ledgerEventIds)(hit).length === 0;
27
+ }
28
+ /**
29
+ * The hits without the ledger's header. For a caller that cuts a page: the header has to come off before the cut, as
30
+ * a closed event does, so that it never takes a place that a memory should have had.
31
+ */
32
+ function dropLedgerPreamble(hits, isLedger) {
33
+ return isLedger === undefined ? hits : hits.filter(hit => !isLedgerPreamble(hit, isLedger));
34
+ }
35
+ /**
36
+ * The hits a keyword search may show, before its page is cut: no header of the ledger, and an event of it only when a
37
+ * word of the question is in what the event says. The search matched the record's markup too ("kind", "store", "ledger",
38
+ * a year), and a hit that way is no match for the question.
39
+ */
40
+ function keywordLedgerHits(hits, isLedger, query, held) {
41
+ if (isLedger === undefined)
42
+ return hits;
43
+ const words = wordsOf(query);
44
+ return dropLedgerPreamble(hits, isLedger).filter(hit => {
45
+ if (!isLedger(hit.notePath) || words.length === 0)
46
+ return true;
47
+ const events = parseEvents(hit.text, held);
48
+ return events.length === 0 || events.some(event => {
49
+ const said = says(event);
50
+ return words.some(word => said.has(word));
51
+ });
52
+ });
53
+ }
54
+ /** The hits as a client is shown them: no header of the ledger, and each event of it as the user's words (or the fact). */
55
+ function presentLedgerEvents(hits, isLedger, held) {
56
+ if (isLedger === undefined)
57
+ return hits;
58
+ return dropLedgerPreamble(hits, isLedger).flatMap(hit => {
59
+ if (!isLedger(hit.notePath))
60
+ return [hit];
61
+ const text = presentEvents(hit.text, held);
62
+ // An event is one chunk that opens with its own header line (the chunker cuts at every `##` heading and never inside
63
+ // a paragraph, and a record has no blank line), so a ledger text with no record in it is one already presented.
64
+ if (text === null)
65
+ return [hit];
66
+ return text === '' ? [] : [{ ...hit, text }]; // a record that says nothing is not a memory
67
+ });
68
+ }
69
+ /**
70
+ * The hits as a client is shown their metadata: a hit of the ledger names no note and no heading (both empty). Its path
71
+ * is the engine's own file and its heading the record's (`Memory Events Ledger > MEM_… · time`): bookkeeping too, which
72
+ * every ledger hit carried, at about 46 tokens (live dogfood, 2026-10-03, D8). What it says is its text, and when, its
73
+ * session_date. Run last: liveness, scope and a repeat of a fact are judged by the event id in the heading.
74
+ */
75
+ function withoutLedgerMetadata(hits, isLedger) {
76
+ if (isLedger === undefined)
77
+ return hits;
78
+ return hits.map(hit => (isLedger(hit.notePath) ? { ...hit, notePath: '', headingPath: '' } : hit));
79
+ }
80
+ const EVENT_HEADER = /^## MEM_[A-Za-z0-9]+ · /i;
81
+ // A field value is the rest of its physical line (dotAll: U+2028/U+2029 and a lone \r belong to a value, as in the ledger parser).
82
+ const FIELD = /^- ([A-Za-z][\w-]*): (.*)$/s;
83
+ const RECORDS_HELD_MAX = 256;
84
+ const RECORDS_HELD_MAX_CHARS = 8 * 1024;
85
+ /** The events in a chunk's text, as the lines of their records say. Tolerant: it only reads, and never throws. */
86
+ function parseEvents(text, held) {
87
+ const kept = held?.get(text);
88
+ if (kept !== undefined)
89
+ return kept;
90
+ const events = readEvents(text);
91
+ if (held && held.size < RECORDS_HELD_MAX && text.length <= RECORDS_HELD_MAX_CHARS)
92
+ held.set(text, events);
93
+ return events;
94
+ }
95
+ function readEvents(text) {
96
+ const events = [];
97
+ let labelled = false;
98
+ for (const line of text.replace(/\r\n/g, '\n').split('\n')) {
99
+ if (line === ledgerLiveness_1.SUPERSEDED_LABEL) {
100
+ labelled = true;
101
+ }
102
+ else if (EVENT_HEADER.test(line)) {
103
+ events.push({ fields: new Map(), superseded: labelled });
104
+ labelled = false;
105
+ }
106
+ else if (events.length > 0) {
107
+ const field = FIELD.exec(line);
108
+ const fields = events[events.length - 1].fields;
109
+ if (field && !fields.has(field[1]))
110
+ fields.set(field[1], field[2]);
111
+ }
112
+ }
113
+ return events;
114
+ }
115
+ /**
116
+ * The events in a chunk's text, one after another: the user's statement as they said it, else `subject — predicate:
117
+ * object`. A superseded event (shown only on request) keeps the label that says so. Null when the text holds no event,
118
+ * so a text that is already presented is left as it is.
119
+ */
120
+ function presentEvents(text, held) {
121
+ const events = parseEvents(text, held);
122
+ return events.length ? events.map(show).filter(Boolean).join('\n\n') : null;
123
+ }
124
+ function show({ fields, superseded }) {
125
+ const said = statementOf(fields.get('statement'));
126
+ const body = said ?? factText(fields.get('subject'), fields.get('predicate'), fields.get('object'));
127
+ return body && superseded ? `${ledgerLiveness_1.SUPERSEDED_LABEL}\n${body}` : body;
128
+ }
129
+ /**
130
+ * A fact as it is shown when no words of the user's were kept: `subject — predicate: object`. The one form for a fact,
131
+ * whether an indexed event of the ledger shows it or a read that finds it in the facts table before the index does.
132
+ */
133
+ function factText(subject, predicate, object) {
134
+ return [subject, [predicate, object].filter(Boolean).join(': ')].filter(Boolean).join(' — ');
135
+ }
136
+ /** The statement the ledger kept: one JSON string literal. Anything else, or nothing said, is not the user's words. */
137
+ function statementOf(raw) {
138
+ if (raw === undefined)
139
+ return undefined;
140
+ try {
141
+ const value = JSON.parse(raw);
142
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined;
143
+ }
144
+ catch {
145
+ return undefined;
146
+ }
147
+ }
148
+ /** The words of what an event says: the statement and the fact, without the record's markup. Worked out once for a record. */
149
+ function says(record) {
150
+ const { fields } = record;
151
+ return record.said ??= new Set(wordsOf([statementOf(fields.get('statement')), fields.get('subject'), fields.get('predicate'), fields.get('object')]
152
+ .filter(Boolean)
153
+ .join(' ')));
154
+ }
155
+ /**
156
+ * The words of a text as the keyword index reads them: runs of letters and digits, lower case, accents off. Whole
157
+ * words, as the index matches them, so that "mem" of an event id is not found in "remember".
158
+ */
159
+ function wordsOf(text) {
160
+ return text.normalize('NFD').replace(/\p{M}+/gu, '').toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
161
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Note-identity recall for exact terms that chunk full-text search cannot match.
4
+ *
5
+ * `chunks_fts` indexes chunk text and heading path; `note_path` is UNINDEXED and
6
+ * `parseNote` drops YAML frontmatter from the indexed body. So an identifier that
7
+ * *names* a note — a task id, a slug, a file name, a frontmatter `id` — was invisible
8
+ * to a keyword search even though the note is right there in the index.
9
+ *
10
+ * Migration 13 indexes those names in `note_identity_fts`, one row per note, with
11
+ * `-_.%` as token characters so an identifier is one whole token. This module asks
12
+ * that index for notes the term names. Callers keep their normal full-text page
13
+ * untouched and add these notes beside it, so no prose hit is ever lost or reordered.
14
+ *
15
+ * Only terms shaped like an identifier (a `-`, `_` or `.` between word characters)
16
+ * are asked: `md`, `notes` or `OR` are words, not names, and would match every note.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.NOTE_IDENTITY_EXTRA = void 0;
20
+ exports.selectNoteIdentityHits = selectNoteIdentityHits;
21
+ const statements_1 = require("../db/statements");
22
+ /** Most named notes added beside a full prose page — the page itself is never shortened. */
23
+ exports.NOTE_IDENTITY_EXTRA = 5;
24
+ /** `deploy-unit-required.test.sh`, `MVP_MCP_JSON`, `literally-100%` — not `md`, `.md`, `notes`. */
25
+ function looksLikeIdentifier(term) {
26
+ return /^\w[\w.%-]*[\w%]$/.test(term) && /[-_.]/.test(term);
27
+ }
28
+ /**
29
+ * The opening chunk of every note named by `term`, ordered by path so the result is
30
+ * deterministic. `exclude` lists note paths the caller already returns; `accept`
31
+ * judges a candidate by its opening chunk (e.g. by scope: a ledger opens with an event, and
32
+ * the ledger's path says nothing about that event's scope) *before* the limit applies.
33
+ */
34
+ function selectNoteIdentityHits(db, term, limit, exclude = new Set(), accept = () => true) {
35
+ const q = (term ?? '').trim();
36
+ const max = Math.max(1, Math.floor(limit) || 1);
37
+ if (!looksLikeIdentifier(q))
38
+ return [];
39
+ const named = (0, statements_1.prepareOnce)(db, `SELECT n.path FROM note_identity_fts f JOIN notes n ON n.rowid = f.rowid
40
+ WHERE note_identity_fts MATCH ? ORDER BY n.path`).all(`"${q.replace(/"/g, '""')}"`);
41
+ const first = (0, statements_1.prepareOnce)(db, `SELECT id AS chunkId, note_path AS notePath, heading_path AS headingPath, text
42
+ FROM chunks WHERE note_path = ? ORDER BY start_line, id LIMIT 1`);
43
+ const rows = [];
44
+ for (const { path: notePath } of named) {
45
+ if (rows.length === max)
46
+ break;
47
+ if (exclude.has(notePath))
48
+ continue;
49
+ const opening = first.get(notePath);
50
+ if (opening && accept(notePath, opening))
51
+ rows.push(opening);
52
+ }
53
+ // bm25() does not apply to a name match. Consumers read `score` as "higher is
54
+ // better", so hand back a descending ordinal rather than a fake BM25 value: the
55
+ // ordering is real (path order), the magnitude is not.
56
+ return rows.map((r, i) => ({ ...r, score: rows.length - i, reason: 'note-identity' }));
57
+ }