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.
- package/LICENSE +661 -0
- package/NOTICE +73 -0
- package/README.md +105 -2
- package/dist/BUILD.json +6 -0
- package/dist/api/apiAuth.js +43 -0
- package/dist/api/server.js +172 -0
- package/dist/buildIdentity.js +132 -0
- package/dist/cli-extractors.js +46 -0
- package/dist/cli.js +491 -0
- package/dist/config.js +65 -0
- package/dist/context/composer.js +320 -0
- package/dist/context/readHonesty.js +478 -0
- package/dist/context/rowStore.js +238 -0
- package/dist/conversation/formatter.js +81 -0
- package/dist/conversation/parser.js +189 -0
- package/dist/conversation/types.js +2 -0
- package/dist/conversation/writer.js +42 -0
- package/dist/db/client.js +147 -0
- package/dist/db/indexSalvage.js +81 -0
- package/dist/db/migrations.js +642 -0
- package/dist/db/readCache.js +109 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/statements.js +89 -0
- package/dist/digests/digestCache.js +142 -0
- package/dist/digests/digests.js +597 -0
- package/dist/digests/worker.js +348 -0
- package/dist/embeddings/caching.js +337 -0
- package/dist/embeddings/coalescing.js +177 -0
- package/dist/embeddings/interactive.js +103 -0
- package/dist/embeddings/local.js +68 -0
- package/dist/embeddings/openai.js +105 -0
- package/dist/embeddings/rebuild.js +166 -0
- package/dist/embeddings/simdScan.js +258 -0
- package/dist/embeddings/space.js +21 -0
- package/dist/embeddings/types.js +14 -0
- package/dist/embeddings/vectorIndex.js +675 -0
- package/dist/embeddings/vertex.js +224 -0
- package/dist/eval/artifacts.js +42 -0
- package/dist/eval/cacheReport.js +139 -0
- package/dist/eval/comparison.js +182 -0
- package/dist/eval/context700-candidate.js +145 -0
- package/dist/eval/evalCache.js +525 -0
- package/dist/eval/hashEmbedding.js +35 -0
- package/dist/eval/longmemeval.js +56 -0
- package/dist/eval/metrics.js +181 -0
- package/dist/eval/renorbenchBaseline.js +486 -0
- package/dist/eval/renorbenchDocuments.js +134 -0
- package/dist/eval/renorbenchProduct.js +182 -0
- package/dist/eval/runLedger.js +279 -0
- package/dist/eval/runManifest.js +479 -0
- package/dist/eval/scoring.js +197 -0
- package/dist/evidence/attach.js +267 -0
- package/dist/evidence/background.js +147 -0
- package/dist/evidence/evidenceCache.js +182 -0
- package/dist/evidence/gate.js +37 -0
- package/dist/evidence/keyPool.js +85 -0
- package/dist/evidence/ollamaChat.js +84 -0
- package/dist/evidence/optin.js +165 -0
- package/dist/evidence/options.js +130 -0
- package/dist/evidence/secondPass.js +456 -0
- package/dist/extraction/classifyCapture.js +34 -0
- package/dist/extraction/decision-model.js +174 -0
- package/dist/extraction/eventExtractor.js +113 -0
- package/dist/extraction/extractionClient.js +122 -0
- package/dist/extraction/factExtractor.js +46 -0
- package/dist/extraction/factsWritePath.js +179 -0
- package/dist/extraction/preferenceExtractor.js +153 -0
- package/dist/extraction/secretGuard.js +176 -0
- package/dist/extraction/secretScanner.js +665 -0
- package/dist/extraction/typedFacts.js +224 -0
- package/dist/extraction/writePathAdjudication.js +110 -0
- package/dist/importing/apply.js +644 -0
- package/dist/importing/bundle-write.js +80 -0
- package/dist/importing/cli.js +407 -0
- package/dist/importing/existing.js +49 -0
- package/dist/importing/facts.js +123 -0
- package/dist/importing/forgetImported.js +79 -0
- package/dist/importing/hosted.js +467 -0
- package/dist/importing/index.js +29 -0
- package/dist/importing/journalLog.js +96 -0
- package/dist/importing/markdown.js +98 -0
- package/dist/importing/plan.js +206 -0
- package/dist/importing/secretScreen.js +113 -0
- package/dist/importing/serialize.js +188 -0
- package/dist/importing/source.js +463 -0
- package/dist/importing/types.js +6 -0
- package/dist/indexer/chunker.js +319 -0
- package/dist/indexer/dailyEntries.js +63 -0
- package/dist/indexer/hasher.js +10 -0
- package/dist/indexer/laneRuns.js +48 -0
- package/dist/indexer/o200k.js +481 -0
- package/dist/indexer/o200k.json +8 -0
- package/dist/indexer/outsideRoot.js +77 -0
- package/dist/indexer/parser.js +167 -0
- package/dist/indexer/pipeline.js +1168 -0
- package/dist/indexer/staleness.js +201 -0
- package/dist/indexer/tokenizer.js +77 -0
- package/dist/indexer/walker.js +184 -0
- package/dist/mcp/hive.js +88 -0
- package/dist/mcp/requestErrors.js +47 -0
- package/dist/mcp/server.js +396 -0
- package/dist/memory/agentLease.js +43 -0
- package/dist/memory/confinedWrite.js +315 -0
- package/dist/memory/controls.js +73 -0
- package/dist/memory/dailyNote.js +265 -0
- package/dist/memory/durableAppend.js +57 -0
- package/dist/memory/factConflicts.js +203 -0
- package/dist/memory/factTerms.js +321 -0
- package/dist/memory/hiveNameHeader.cjs +10 -0
- package/dist/memory/inventory.js +55 -0
- package/dist/memory/invisibleText.js +20 -0
- package/dist/memory/lifecycle.js +1756 -0
- package/dist/memory/limits.js +13 -0
- package/dist/memory/requests.js +183 -0
- package/dist/memory/sourceName.js +200 -0
- package/dist/memory/whatsNew.js +158 -0
- package/dist/memory/writer.js +175 -0
- package/dist/memory/writerConflicts.js +85 -0
- package/dist/memorymap/memoryMap.js +1568 -0
- package/dist/memorymap/replacedJournal.js +58 -0
- package/dist/memorymap/replacements.js +405 -0
- package/dist/mvp/activity.js +86 -0
- package/dist/mvp/agentMemory.js +1744 -0
- package/dist/mvp/auth.js +531 -0
- package/dist/mvp/cli.js +73 -0
- package/dist/mvp/client-store.js +462 -0
- package/dist/mvp/compileCache.js +249 -0
- package/dist/mvp/compileSeed.js +27 -0
- package/dist/mvp/config.js +288 -0
- package/dist/mvp/connectionStatus.js +80 -0
- package/dist/mvp/contextRelevance.js +413 -0
- package/dist/mvp/engine.js +1065 -0
- package/dist/mvp/exportAll.js +359 -0
- package/dist/mvp/factLanePack.js +77 -0
- package/dist/mvp/factMarkers.js +49 -0
- package/dist/mvp/factsLane.js +203 -0
- package/dist/mvp/hiddenSpans.js +585 -0
- package/dist/mvp/history.js +413 -0
- package/dist/mvp/hitDates.js +31 -0
- package/dist/mvp/hitFloor.js +36 -0
- package/dist/mvp/identity.js +18 -0
- package/dist/mvp/idle.js +113 -0
- package/dist/mvp/memoryDir.js +98 -0
- package/dist/mvp/posthogMcp.js +210 -0
- package/dist/mvp/readScope.js +104 -0
- package/dist/mvp/readUsefulness.js +222 -0
- package/dist/mvp/sanitize.js +48 -0
- package/dist/mvp/schemaRejections.js +91 -0
- package/dist/mvp/scope.js +127 -0
- package/dist/mvp/server.js +2089 -0
- package/dist/mvp/serverCard.js +29 -0
- package/dist/mvp/storedSize.js +139 -0
- package/dist/mvp/telemetry.js +441 -0
- package/dist/retrieval/adviceLines.js +195 -0
- package/dist/retrieval/adviceQuestion.js +44 -0
- package/dist/retrieval/conversationShape.js +44 -0
- package/dist/retrieval/dailyEntryTime.js +55 -0
- package/dist/retrieval/entityLookup.js +113 -0
- package/dist/retrieval/factsFirst.js +188 -0
- package/dist/retrieval/keyword.js +84 -0
- package/dist/retrieval/ledgerEventTime.js +51 -0
- package/dist/retrieval/ledgerLiveness.js +83 -0
- package/dist/retrieval/ledgerPresentation.js +161 -0
- package/dist/retrieval/noteIdentity.js +57 -0
- package/dist/retrieval/orchestrator.js +1086 -0
- package/dist/retrieval/queryHelpers.js +142 -0
- package/dist/retrieval/questionSpan.js +475 -0
- package/dist/retrieval/readerEvents.js +690 -0
- package/dist/retrieval/readerLayout.js +503 -0
- package/dist/retrieval/relativeTimeHints.js +113 -0
- package/dist/retrieval/semantic.js +67 -0
- package/dist/router/routerCache.js +106 -0
- package/dist/router/sessionRouter.js +726 -0
- package/dist/toolRefusal.js +120 -0
- package/dist/watcher/watcher.js +75 -0
- 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
|
+
}
|