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,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderNoteMarkdown = renderNoteMarkdown;
4
+ exports.destinationRelPath = destinationRelPath;
5
+ /**
6
+ * Produce the bytes to write for a note.
7
+ *
8
+ * The source's **original Markdown is preferred whenever it is reachable**: it is the
9
+ * source of truth, and carrying a document is not re-parsing it (the constraint on this
10
+ * path is that no re-parse, re-chunk or re-embed is run — not that a document may not
11
+ * be carried).
12
+ *
13
+ * Reconstruction is the fallback for an index whose content tree is unavailable. It
14
+ * concatenates the source's own chunk texts in order with the heading structure their
15
+ * `heading_path` values already describe — not a re-parse, but a projection: it is
16
+ * lossy for YAML comments and key order, and a caller that falls back is told so by
17
+ * `fromOriginalBytes: false`.
18
+ *
19
+ * Chunk spans are **located in the bytes actually written**, not copied from the source
20
+ * index. Measured on a real 22,774-chunk index, only 475 of 594 sampled stored spans
21
+ * contained their own chunk text (the engine's paragraph splitter accumulates its
22
+ * offsets approximately), while 593 of 594 chunk texts were found in the note body. A
23
+ * copied span would therefore mis-cite a fifth of chunks; a located span is true of the
24
+ * file being written by construction.
25
+ */
26
+ function renderNoteMarkdown(note) {
27
+ const fromOriginalBytes = typeof note.originalContent === 'string';
28
+ const content = fromOriginalBytes
29
+ ? note.originalContent
30
+ : reconstructNoteMarkdown(note);
31
+ const located = locateChunkLines(content, note.chunks);
32
+ let chunksNotLocated = 0;
33
+ let chunksSpanInherited = 0;
34
+ const chunkLines = located.lines.map((span, index) => {
35
+ if (located.found[index])
36
+ return span;
37
+ chunksNotLocated += 1;
38
+ const chunk = note.chunks[index];
39
+ // The source's stored span describes the source's own bytes. That is exactly what is
40
+ // being written on the original-bytes path, so it may be inherited there. On the
41
+ // reconstruction path the written file has different bytes (frontmatter re-rendered,
42
+ // headings materialised, empty chunks dropped), so an inherited coordinate would
43
+ // point at unrelated text — report it as unknown instead.
44
+ if (fromOriginalBytes && typeof chunk?.startLine === 'number' && typeof chunk.endLine === 'number') {
45
+ chunksSpanInherited += 1;
46
+ return { startLine: chunk.startLine, endLine: chunk.endLine };
47
+ }
48
+ return { startLine: 0, endLine: 0 };
49
+ });
50
+ return { content, chunkLines, fromOriginalBytes, chunksNotLocated, chunksSpanInherited };
51
+ }
52
+ /** Half-open line spans for each chunk, found by locating its text in the content. */
53
+ function locateChunkLines(content, chunks) {
54
+ const lines = [];
55
+ const found = [];
56
+ const newlinesBefore = newlineCounter(content);
57
+ let cursor = 0;
58
+ for (const chunk of chunks) {
59
+ const needle = chunk.text.replace(/[ \t]+$/gm, '').trim();
60
+ if (!needle) {
61
+ lines.push({ startLine: 0, endLine: 0 });
62
+ found.push(false);
63
+ continue;
64
+ }
65
+ let at = content.indexOf(needle, cursor);
66
+ if (at < 0)
67
+ at = content.indexOf(needle);
68
+ if (at < 0) {
69
+ lines.push({ startLine: 0, endLine: 0 });
70
+ found.push(false);
71
+ continue;
72
+ }
73
+ const startLine = newlinesBefore(at);
74
+ lines.push({ startLine, endLine: startLine + needle.split('\n').length });
75
+ found.push(true);
76
+ cursor = at + needle.length;
77
+ }
78
+ return { lines, found };
79
+ }
80
+ /**
81
+ * Counts the newlines before an offset, resuming from where the previous count stopped.
82
+ *
83
+ * Chunks are located in document order, so each count reads only the text since the last one
84
+ * and the pass over a note is linear in its size. Rescanning from offset 0 for every chunk cost
85
+ * chunks x bytes: a 13 MB note with 8,000 chunks took minutes to preview. An offset before the
86
+ * last stop (a chunk found out of order) is reached from whichever end is nearer, so no count
87
+ * reads more characters than the rescan did.
88
+ */
89
+ function newlineCounter(content) {
90
+ let stop = 0; // `newlines` is exact for content[0, stop)
91
+ let newlines = 0;
92
+ return index => {
93
+ if (index >= stop)
94
+ newlines += countNewlines(content, stop, index);
95
+ else if (index < stop - index)
96
+ newlines = countNewlines(content, 0, index);
97
+ else
98
+ newlines -= countNewlines(content, index, stop);
99
+ stop = index;
100
+ return newlines;
101
+ };
102
+ }
103
+ function countNewlines(content, from, to) {
104
+ let count = 0;
105
+ for (let i = from; i < to; i += 1) {
106
+ if (content.charCodeAt(i) === 10)
107
+ count += 1;
108
+ }
109
+ return count;
110
+ }
111
+ /**
112
+ * Rebuild a note body from its chunks, materialising heading paths that the chunk text
113
+ * does not itself carry. The lines are joined verbatim — no blank-line collapsing —
114
+ * because the chunk spans are computed against these exact bytes.
115
+ */
116
+ function reconstructNoteMarkdown(note) {
117
+ const lines = [];
118
+ const frontmatter = renderFrontmatter(note.frontmatter);
119
+ if (frontmatter.length > 0)
120
+ lines.push('---', ...frontmatter, '---', '');
121
+ let stack = [];
122
+ const first = note.chunks[0];
123
+ if (first && !/^#{1,3}\s/.test(first.text.trimStart()) && note.title) {
124
+ lines.push(`# ${note.title}`, '');
125
+ stack = [note.title];
126
+ }
127
+ for (const chunk of note.chunks) {
128
+ const text = chunk.text.replace(/[ \t]+$/gm, '').trim();
129
+ if (!text)
130
+ continue;
131
+ const startsWithHeading = /^#{1,3}\s/.test(text);
132
+ const headingStack = chunk.headingPath
133
+ ? chunk.headingPath.split(' > ').map(s => s.trim()).filter(Boolean)
134
+ : [];
135
+ if (startsWithHeading) {
136
+ if (headingStack.length > 0)
137
+ stack = headingStack;
138
+ }
139
+ else if (headingStack.length > 0) {
140
+ let common = 0;
141
+ while (common < Math.min(headingStack.length, stack.length) && headingStack[common] === stack[common])
142
+ common += 1;
143
+ for (let level = common; level < headingStack.length; level += 1) {
144
+ lines.push(`${'#'.repeat(Math.min(level + 1, 3))} ${headingStack[level]}`);
145
+ }
146
+ stack = headingStack.slice();
147
+ }
148
+ if (lines.length > 0 && lines[lines.length - 1] !== '')
149
+ lines.push('');
150
+ lines.push(...text.split('\n'));
151
+ }
152
+ return lines.length > 0 ? lines.join('\n') + '\n' : '';
153
+ }
154
+ /**
155
+ * Frontmatter as YAML key/scalar lines. Values are JSON-encoded, which is a valid YAML
156
+ * scalar or flow collection for every JSON value, so a round-trip through the engine's
157
+ * own frontmatter parser cannot produce malformed YAML.
158
+ */
159
+ function renderFrontmatter(frontmatter) {
160
+ const out = [];
161
+ for (const [key, value] of Object.entries(frontmatter)) {
162
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(key))
163
+ continue;
164
+ if (value === undefined)
165
+ continue;
166
+ const encoded = JSON.stringify(value);
167
+ if (encoded === undefined || encoded.length > 4096)
168
+ continue;
169
+ out.push(`${key}: ${encoded}`);
170
+ }
171
+ return out;
172
+ }
173
+ /** `namespace/relPath`, with every segment sanitized so a source path cannot escape. */
174
+ function destinationRelPath(namespace, sourceRelPath) {
175
+ const cleanNamespace = sanitizeSegments(namespace);
176
+ const clean = sanitizeSegments(sourceRelPath);
177
+ if (!clean)
178
+ throw new Error(`source path ${JSON.stringify(sourceRelPath)} has no usable segments`);
179
+ return cleanNamespace ? `${cleanNamespace}/${clean}` : clean;
180
+ }
181
+ function sanitizeSegments(value) {
182
+ return value
183
+ .split('/')
184
+ .map(segment => segment.trim())
185
+ .filter(segment => segment !== '' && segment !== '.' && segment !== '..')
186
+ .map(segment => segment.replace(/[\u0000-\u001F\u007F]/g, '_'))
187
+ .join('/');
188
+ }
@@ -0,0 +1,463 @@
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.toRelative = toRelative;
7
+ exports.isSelected = isSelected;
8
+ exports.readRenorIndexSource = readRenorIndexSource;
9
+ exports.readBundleSource = readBundleSource;
10
+ exports.readSource = readSource;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const client_1 = require("../db/client");
14
+ const types_1 = require("./types");
15
+ /** Relative path with forward slashes, or the input unchanged when it is not under the root. */
16
+ function toRelative(sourceRoot, absolutePath) {
17
+ const root = path_1.default.resolve(sourceRoot);
18
+ const abs = path_1.default.resolve(absolutePath);
19
+ const rel = path_1.default.relative(root, abs);
20
+ if (rel === '' || rel.startsWith('..') || path_1.default.isAbsolute(rel))
21
+ return absolutePath;
22
+ return rel.split(path_1.default.sep).join('/');
23
+ }
24
+ function matchesPrefix(relPath, prefixes) {
25
+ return prefixes.some(prefix => {
26
+ const clean = prefix.replace(/^(\.?\/)+/, '').replace(/\/+$/, '');
27
+ return relPath === clean || relPath.startsWith(clean + '/');
28
+ });
29
+ }
30
+ /** Selection is explicit: a path must pass `include` (when given) and survive `exclude`. */
31
+ function isSelected(relPath, filter) {
32
+ if (filter.include.length > 0 && !matchesPrefix(relPath, filter.include))
33
+ return false;
34
+ if (filter.exclude.length > 0 && matchesPrefix(relPath, filter.exclude))
35
+ return false;
36
+ return true;
37
+ }
38
+ /**
39
+ * Read a Renor-shaped index (any store that uses the engine's `notes`/`chunks`/`facts`
40
+ * tables, including another Renor workspace or the personal brain) as an import bundle.
41
+ *
42
+ * The database is opened `readonly` so a source can never be written by an import —
43
+ * including the WAL side files a write-capable open would touch.
44
+ */
45
+ function readRenorIndexSource(options) {
46
+ // Through the sanctioned opener (`src/db/client.ts`), which enforces read-only at the
47
+ // SQLite level so an import can never write to the store it is reading.
48
+ const db = (0, client_1.openReadOnlyDb)(options.dbPath);
49
+ try {
50
+ const filter = {
51
+ include: options.include ?? [],
52
+ exclude: options.exclude ?? [],
53
+ limit: options.limit,
54
+ };
55
+ const allNotes = db.prepare('SELECT path, title, kind, modified_at, frontmatter_json FROM notes').all();
56
+ const selected = [];
57
+ let filteredOut = 0;
58
+ for (const row of allNotes) {
59
+ const rel = toRelative(options.sourceRoot, row.path);
60
+ if (!isSelected(rel, filter)) {
61
+ filteredOut += 1;
62
+ continue;
63
+ }
64
+ // The lifecycle's synthetic `_mcp_` row is not a note document.
65
+ if (rel === '_mcp_') {
66
+ filteredOut += 1;
67
+ continue;
68
+ }
69
+ selected.push({ row, rel });
70
+ }
71
+ selected.sort((a, b) => a.rel.localeCompare(b.rel));
72
+ const limited = options.limit !== undefined && options.limit >= 0
73
+ ? selected.slice(0, options.limit)
74
+ : selected;
75
+ const chunksByNote = new Map();
76
+ const entitiesByNote = new Map();
77
+ const contentsByNote = new Map();
78
+ const chunkStmt = db.prepare('SELECT note_path, heading_path, text, token_count, start_line, end_line FROM chunks WHERE note_path = ? ORDER BY id');
79
+ // The lifecycle sentinel owns canonical entities; carrying those would duplicate
80
+ // the destination's own lifecycle entity for the same subject.
81
+ const entityStmt = db.prepare(`SELECT type, canonical_name, aliases_json, confidence FROM entities
82
+ WHERE source_note = ? AND source_note != '_mcp_'`);
83
+ const contentRoot = path_1.default.resolve(options.contentRoot ?? options.sourceRoot);
84
+ for (const { row, rel } of limited) {
85
+ const rows = chunkStmt.all(row.path);
86
+ chunksByNote.set(row.path, rows.map(r => ({
87
+ headingPath: r.heading_path ?? '',
88
+ text: r.text,
89
+ tokenCount: r.token_count,
90
+ // The same span rule as the bundle adapter: a coordinate that cannot be a citation
91
+ // is dropped rather than inherited into the destination's `chunks` rows.
92
+ ...normalizedSpan(r.start_line, r.end_line),
93
+ })));
94
+ const entityRows = entityStmt.all(row.path);
95
+ entitiesByNote.set(row.path, entityRows.map(e => ({
96
+ type: e.type,
97
+ canonicalName: e.canonical_name,
98
+ aliases: parseAliases(e.aliases_json),
99
+ confidence: typeof e.confidence === 'number' ? e.confidence : 1,
100
+ })));
101
+ // Prefer the source's own bytes: they are the source of truth, they keep the
102
+ // stored chunk line ranges truthful, and copying a document is not re-parsing it.
103
+ const original = readOriginalContent(contentRoot, rel);
104
+ if (original !== null)
105
+ contentsByNote.set(row.path, original);
106
+ }
107
+ const notes = limited.map(({ row, rel }) => ({
108
+ sourcePath: row.path,
109
+ relPath: rel,
110
+ title: row.title,
111
+ kind: row.kind ?? null,
112
+ modifiedAt: row.modified_at,
113
+ frontmatter: parseFrontmatter(row.frontmatter_json),
114
+ chunks: chunksByNote.get(row.path) ?? [],
115
+ entities: entitiesByNote.get(row.path) ?? [],
116
+ originalContent: contentsByNote.get(row.path),
117
+ }));
118
+ // Only CURRENT facts are carried: a superseded value is history the destination
119
+ // would have to replay in order, and the source's own ledger is the authority for
120
+ // its lifecycle. Carrying closed rows would duplicate history under new ids.
121
+ const factsAvailable = db.prepare('SELECT count(*) AS c FROM facts WHERE valid_to IS NULL').get().c;
122
+ const factRows = db.prepare(`SELECT e.canonical_name AS subject, f.predicate AS predicate, f.object_text AS object_text,
123
+ f.scope AS scope, f.valid_from AS valid_from, f.valid_to AS valid_to,
124
+ f.source_path AS source_path
125
+ FROM facts f
126
+ JOIN entities e ON e.id = f.subject_entity_id
127
+ WHERE f.valid_to IS NULL AND f.object_text IS NOT NULL AND e.canonical_name IS NOT NULL
128
+ ORDER BY f.valid_from DESC, f.id DESC`).all();
129
+ const selectedNotePaths = new Set(limited.map(({ row }) => row.path));
130
+ const facts = [];
131
+ for (const row of factRows) {
132
+ const isLedgerFact = row.source_path === '_mcp_';
133
+ // Ledger-derived facts are the owner's explicitly stated memory: a small, global,
134
+ // high-value set. They are carried by default (the point of an import is to see
135
+ // what Renor already holds) and skipped only when the caller excludes them.
136
+ if (isLedgerFact) {
137
+ if (matchesPrefix('_mcp_', filter.exclude) || matchesPrefix('ledger', filter.exclude))
138
+ continue;
139
+ }
140
+ else if (!selectedNotePaths.has(row.source_path)) {
141
+ continue;
142
+ }
143
+ facts.push({
144
+ subject: String(row.subject),
145
+ predicate: String(row.predicate),
146
+ object: String(row.object_text),
147
+ scope: row.scope ?? 'global',
148
+ validFrom: row.valid_from,
149
+ sourcePath: row.source_path,
150
+ origin: isLedgerFact ? 'ledger' : 'note',
151
+ ledgerSource: isLedgerFact ? 'import:renor-index' : 'import:renor-index',
152
+ });
153
+ }
154
+ return {
155
+ format: types_1.BUNDLE_FORMAT,
156
+ sourceLabel: options.label ?? 'renor-index',
157
+ notes,
158
+ facts,
159
+ stats: {
160
+ notesAvailable: allNotes.length,
161
+ notesSelected: notes.length,
162
+ notesFilteredOut: filteredOut + (selected.length - limited.length),
163
+ notesWithOriginalBytes: notes.filter(n => n.originalContent !== undefined).length,
164
+ chunksSelected: notes.reduce((sum, n) => sum + n.chunks.length, 0),
165
+ factsAvailable,
166
+ factsSelected: facts.length,
167
+ },
168
+ };
169
+ }
170
+ finally {
171
+ db.close();
172
+ }
173
+ }
174
+ /**
175
+ * The source note's original bytes, when the content tree is reachable. Returns null
176
+ * when the file is absent, so the caller falls back to reconstruction and reports it.
177
+ */
178
+ function readOriginalContent(contentRoot, relPath) {
179
+ const candidate = path_1.default.resolve(contentRoot, relPath);
180
+ const root = path_1.default.resolve(contentRoot);
181
+ const rel = path_1.default.relative(root, candidate);
182
+ if (rel === '' || rel.startsWith('..') || path_1.default.isAbsolute(rel))
183
+ return null;
184
+ try {
185
+ return fs_1.default.readFileSync(candidate, 'utf8');
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ function parseAliases(raw) {
192
+ try {
193
+ const parsed = JSON.parse(raw);
194
+ return Array.isArray(parsed) ? parsed.filter((v) => typeof v === 'string') : [];
195
+ }
196
+ catch {
197
+ return [];
198
+ }
199
+ }
200
+ function parseFrontmatter(raw) {
201
+ try {
202
+ const parsed = JSON.parse(raw);
203
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
204
+ ? parsed
205
+ : {};
206
+ }
207
+ catch {
208
+ return {};
209
+ }
210
+ }
211
+ /**
212
+ * Read a `renor-memory-export/v1` JSONL bundle written by any provider.
213
+ * One JSON record per line: `{"type":"note",…}` / `{"type":"fact",…}` / `{"type":"meta",…}`.
214
+ *
215
+ * `filter` is applied HERE, in the adapter, exactly as the index adapter applies it —
216
+ * `include`/`exclude` select note records by `relPath`, `limit` caps the selection, and
217
+ * `_mcp_`/`ledger` in `exclude` drops the owner's ledger-derived facts. An adapter that
218
+ * returned everything would make the CLI's selection flags a silent no-op, and
219
+ * `--exclude Private` would import the private notes anyway. `filter` is optional so the
220
+ * adapter can still be used as a plain bundle reader.
221
+ */
222
+ function readBundleSource(filePath, filter = { include: [], exclude: [] }) {
223
+ const include = filter.include ?? [];
224
+ const exclude = filter.exclude ?? [];
225
+ const raw = fs_1.default.readFileSync(filePath, 'utf8');
226
+ const parsedNotes = [];
227
+ const parsedFacts = [];
228
+ let sourceLabel = path_1.default.basename(filePath, path_1.default.extname(filePath));
229
+ let format = types_1.BUNDLE_FORMAT;
230
+ const lines = raw.split('\n');
231
+ for (let i = 0; i < lines.length; i++) {
232
+ const line = lines[i].trim();
233
+ if (!line)
234
+ continue;
235
+ let record;
236
+ try {
237
+ record = JSON.parse(line);
238
+ }
239
+ catch {
240
+ throw new Error(`bundle line ${i + 1} is not valid JSON`);
241
+ }
242
+ if (record?.type === 'meta') {
243
+ if (typeof record.format === 'string')
244
+ format = record.format;
245
+ if (typeof record.source === 'string')
246
+ sourceLabel = record.source;
247
+ continue;
248
+ }
249
+ if (record?.type === 'note') {
250
+ // Every field that reaches canonical bytes is validated where it is read: `String(x)`
251
+ // accepted anything, so a malformed record became the literal text `[object Object]`,
252
+ // which is then written into Markdown, indexed, and (for a fact) recorded in the
253
+ // ledger as though it were memory.
254
+ const note = `note ${bundleLabel(record.relPath ?? record.path)}`;
255
+ const at = bundleWhere(note, i);
256
+ const relPathFromRecord = record.relPath ?? record.path;
257
+ parsedNotes.push({
258
+ sourcePath: bundleString(record.path, '', 'path', at),
259
+ relPath: bundleString(relPathFromRecord, '', 'relPath', at),
260
+ title: bundleString(record.title, '', 'title', at),
261
+ kind: typeof record.kind === 'string' ? record.kind : null,
262
+ modifiedAt: bundleString(record.modifiedAt, '', 'modifiedAt', at),
263
+ frontmatter: record.frontmatter && typeof record.frontmatter === 'object' ? record.frontmatter : {},
264
+ chunks: Array.isArray(record.chunks)
265
+ ? record.chunks.map((c, index) => {
266
+ const chunk = bundleWhere(note, i, `chunk ${index}`);
267
+ return {
268
+ headingPath: bundleString(c?.headingPath, '', 'headingPath', chunk),
269
+ text: bundleString(c?.text, '', 'text', chunk),
270
+ tokenCount: bundleNumber(c?.tokenCount, 0, 'tokenCount', chunk),
271
+ // Spans must survive the round trip: they are the only fallback for a chunk
272
+ // whose text cannot be located, so dropping them would silently make a
273
+ // bundle-sourced import behave differently from a direct index read. A span
274
+ // that cannot be a citation — negative, fractional, or reversed — is dropped
275
+ // here instead of being inherited as a coordinate.
276
+ ...normalizedSpan(c?.startLine, c?.endLine),
277
+ };
278
+ })
279
+ : [],
280
+ entities: Array.isArray(record.entities)
281
+ ? record.entities.map((e, index) => {
282
+ const entity = bundleWhere(note, i, `entity ${index}`);
283
+ return {
284
+ type: bundleString(e?.type, 'note', 'type', entity),
285
+ canonicalName: bundleString(e?.canonicalName, '', 'canonicalName', entity),
286
+ aliases: Array.isArray(e?.aliases)
287
+ ? e.aliases.map((a, aliasIndex) => (bundleString(a, '', `alias ${aliasIndex}`, entity)))
288
+ : [],
289
+ confidence: bundleNumber(e?.confidence, 1, 'confidence', entity),
290
+ };
291
+ })
292
+ : [],
293
+ originalContent: typeof record.originalContent === 'string' ? record.originalContent : undefined,
294
+ });
295
+ continue;
296
+ }
297
+ if (record?.type === 'fact') {
298
+ // `String(x)` on a fact field fabricated `[object Object]` as a canonical ledger value.
299
+ const at = bundleWhere('fact', i);
300
+ parsedFacts.push({
301
+ subject: bundleString(record.subject, '', 'subject', at),
302
+ predicate: bundleString(record.predicate, '', 'predicate', at),
303
+ object: bundleString(record.object, '', 'object', at),
304
+ scope: String(record.scope ?? 'global'),
305
+ validFrom: String(record.validFrom ?? ''),
306
+ sourcePath: String(record.sourcePath ?? ''),
307
+ origin: record.origin === 'ledger' ? 'ledger' : 'note',
308
+ eventId: typeof record.eventId === 'string' ? record.eventId : undefined,
309
+ closedEvent: record.closedEvent ?? null,
310
+ ledgerSource: typeof record.ledgerSource === 'string' ? record.ledgerSource : undefined,
311
+ });
312
+ continue;
313
+ }
314
+ throw new Error(`bundle line ${i + 1} has unknown record type ${JSON.stringify(record?.type)}`);
315
+ }
316
+ if (format !== types_1.BUNDLE_FORMAT) {
317
+ throw new Error(`unsupported bundle format ${JSON.stringify(format)}; expected ${types_1.BUNDLE_FORMAT}`);
318
+ }
319
+ // Selection, in the same order and with the same rule the index adapter uses: the index
320
+ // adapter sorts by relative path BEFORE applying `limit`, so a bundle is sorted here too
321
+ // and `--limit N` selects the same records whichever order the provider wrote them in.
322
+ const selection = { include, exclude };
323
+ const selected = [];
324
+ let notesFilteredOut = 0;
325
+ for (const note of parsedNotes) {
326
+ if (!isSelected(note.relPath, selection)) {
327
+ notesFilteredOut += 1;
328
+ continue;
329
+ }
330
+ selected.push(note);
331
+ }
332
+ selected.sort((a, b) => a.relPath.localeCompare(b.relPath));
333
+ const notes = filter.limit !== undefined && filter.limit >= 0
334
+ ? selected.slice(0, filter.limit)
335
+ : selected;
336
+ notesFilteredOut += selected.length - notes.length;
337
+ // Facts follow the notes: a note-derived fact for a note that was not selected is not
338
+ // carried, and the owner's ledger-derived facts (`_mcp_`) are carried by default and
339
+ // dropped only when explicitly excluded — the same rule the index adapter applies.
340
+ // Both a note's source path and its relative path count as naming it, because a
341
+ // provider may write either.
342
+ const selectedPaths = new Set();
343
+ for (const note of notes) {
344
+ selectedPaths.add(note.sourcePath);
345
+ selectedPaths.add(note.relPath);
346
+ }
347
+ const facts = [];
348
+ for (const fact of parsedFacts) {
349
+ if (fact.sourcePath === '_mcp_') {
350
+ if (matchesPrefix('_mcp_', exclude) || matchesPrefix('ledger', exclude))
351
+ continue;
352
+ facts.push(fact);
353
+ continue;
354
+ }
355
+ // A fact that names no path at all carries no note association that selection could
356
+ // be applied to; one that names a path must belong to a selected note.
357
+ if (fact.sourcePath !== '' && !selectedPaths.has(fact.sourcePath))
358
+ continue;
359
+ facts.push(fact);
360
+ }
361
+ return {
362
+ format,
363
+ sourceLabel,
364
+ notes,
365
+ facts,
366
+ stats: {
367
+ notesAvailable: parsedNotes.length,
368
+ notesSelected: notes.length,
369
+ notesFilteredOut,
370
+ notesWithOriginalBytes: notes.filter(n => n.originalContent !== undefined).length,
371
+ chunksSelected: notes.reduce((sum, n) => sum + n.chunks.length, 0),
372
+ factsAvailable: parsedFacts.length,
373
+ factsSelected: facts.length,
374
+ },
375
+ };
376
+ }
377
+ /** A label for a record whose own identity may be malformed, safe to print in an error. */
378
+ function bundleLabel(raw) {
379
+ return typeof raw === 'string' ? JSON.stringify(raw) : JSON.stringify(raw ?? '');
380
+ }
381
+ /**
382
+ * A string field of a bundle record, validated where the record is read.
383
+ *
384
+ * An absent/null field keeps its default, so existing bundles stay readable; a
385
+ * present-but-non-string field is refused, naming the line, the record and the field.
386
+ */
387
+ function bundleString(raw, fallback, field, where) {
388
+ if (raw === undefined || raw === null)
389
+ return fallback;
390
+ if (typeof raw !== 'string') {
391
+ throw new Error(`${where} has an invalid ${field} ${JSON.stringify(raw)}: expected a string`);
392
+ }
393
+ return raw;
394
+ }
395
+ /** Where a bundle record's field lives, so a malformed value is named precisely. */
396
+ function bundleWhere(origin, lineIndex, part) {
397
+ return `bundle line ${lineIndex + 1}: ${origin}${part ? ` ${part}` : ''}`;
398
+ }
399
+ /**
400
+ * A line span, or nothing when it cannot be a citation.
401
+ *
402
+ * A negative or fractional span was copied verbatim and could be inherited into
403
+ * `chunks.start_line`/`end_line` and handed back as a citation range, so a span that is not
404
+ * a non-negative, ordered integer pair is treated exactly like "text not located": unknown.
405
+ */
406
+ function normalizedSpan(startLine, endLine) {
407
+ if (typeof startLine !== 'number' || typeof endLine !== 'number')
408
+ return {};
409
+ if (!Number.isInteger(startLine) || !Number.isInteger(endLine))
410
+ return {};
411
+ if (startLine < 0 || endLine < startLine)
412
+ return {};
413
+ return { startLine, endLine };
414
+ }
415
+ /**
416
+ * A numeric field of a bundle record, validated where the record is read.
417
+ *
418
+ * `Number(record.field ?? default)` accepted anything: a malformed `tokenCount` became
419
+ * `NaN`, travelled through the plan, and only failed at the write layer as
420
+ * `NOT NULL constraint failed: chunks.token_count` — after canonical note bytes had been
421
+ * written, and reported as a *ledger replay* rejection. Invalid input is a property of the
422
+ * bundle, so it is rejected here, before any write, naming the line, the note and the
423
+ * field. An absent/null field keeps its default, so existing bundles stay readable.
424
+ */
425
+ function bundleNumber(raw, fallback, field, where) {
426
+ if (raw === undefined || raw === null)
427
+ return fallback;
428
+ const value = typeof raw === 'number'
429
+ ? raw
430
+ : typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : Number.NaN;
431
+ if (!Number.isFinite(value) || value < 0) {
432
+ throw new Error(`${where} has an invalid ${field} ${JSON.stringify(raw)}: expected a non-negative finite number`);
433
+ }
434
+ return value;
435
+ }
436
+ /** Open a source from a `kind:path` spec. */
437
+ function readSource(spec, filter) {
438
+ const separator = spec.indexOf(':');
439
+ if (separator < 0) {
440
+ throw new Error('source must be given as kind:path (for example renor-index:/path/index.db)');
441
+ }
442
+ const kind = spec.slice(0, separator);
443
+ const target = spec.slice(separator + 1);
444
+ if (kind === 'renor-index') {
445
+ return readRenorIndexSource({
446
+ dbPath: target,
447
+ sourceRoot: filter.sourceRoot ?? path_1.default.dirname(path_1.default.dirname(target)),
448
+ contentRoot: filter.contentRoot,
449
+ include: filter.include,
450
+ exclude: filter.exclude,
451
+ limit: filter.limit,
452
+ label: filter.label,
453
+ });
454
+ }
455
+ if (kind === 'bundle') {
456
+ return readBundleSource(target, {
457
+ include: filter.include,
458
+ exclude: filter.exclude,
459
+ limit: filter.limit,
460
+ });
461
+ }
462
+ throw new Error(`unknown source kind ${JSON.stringify(kind)}; expected renor-index or bundle`);
463
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /** Portable memory bundle: the interface every import source must produce. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.BUNDLE_FORMAT = void 0;
5
+ /** The bundle format this module reads and writes. */
6
+ exports.BUNDLE_FORMAT = 'renor-memory-export/v1';