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,80 @@
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.writeBundle = writeBundle;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const types_1 = require("./types");
10
+ /**
11
+ * Write a bundle as `renor-memory-export/v1` JSONL.
12
+ *
13
+ * This is the seam that lets an import run where the workspace lives: a source that is
14
+ * only reachable read-only can be exported once, and the resulting file is the whole
15
+ * input the importer needs. It is also the documented interface a third-party provider
16
+ * can implement directly instead of shipping an adapter.
17
+ *
18
+ * The file contains memory content, so it is written `0600` and must be treated as
19
+ * private material — never committed, never a fixture.
20
+ */
21
+ function writeBundle(bundle, filePath) {
22
+ const dir = path_1.default.dirname(path_1.default.resolve(filePath));
23
+ fs_1.default.mkdirSync(dir, { recursive: true });
24
+ const lines = [];
25
+ lines.push(JSON.stringify({
26
+ type: 'meta',
27
+ format: types_1.BUNDLE_FORMAT,
28
+ source: bundle.sourceLabel,
29
+ notes: bundle.notes.length,
30
+ facts: bundle.facts.length,
31
+ }));
32
+ for (const note of bundle.notes) {
33
+ lines.push(JSON.stringify({
34
+ type: 'note',
35
+ path: note.sourcePath,
36
+ relPath: note.relPath,
37
+ title: note.title,
38
+ kind: note.kind,
39
+ modifiedAt: note.modifiedAt,
40
+ frontmatter: note.frontmatter,
41
+ chunks: note.chunks.map(chunk => ({
42
+ headingPath: chunk.headingPath,
43
+ text: chunk.text,
44
+ tokenCount: chunk.tokenCount,
45
+ startLine: chunk.startLine,
46
+ endLine: chunk.endLine,
47
+ })),
48
+ entities: note.entities,
49
+ // Carried when present so the importer can prefer the original bytes.
50
+ originalContent: note.originalContent,
51
+ }));
52
+ }
53
+ for (const fact of bundle.facts) {
54
+ lines.push(JSON.stringify({
55
+ type: 'fact',
56
+ subject: fact.subject,
57
+ predicate: fact.predicate,
58
+ object: fact.object,
59
+ scope: fact.scope,
60
+ validFrom: fact.validFrom,
61
+ sourcePath: fact.sourcePath,
62
+ origin: fact.origin,
63
+ ledgerSource: fact.ledgerSource,
64
+ }));
65
+ }
66
+ const body = lines.join('\n') + '\n';
67
+ // `mode` is only honoured when the file is created, so an export over an existing
68
+ // path (or one a shell redirect created) would keep whatever permissions it already
69
+ // had — while this command claims the bundle is 0600. Create, write and chmod so the
70
+ // guarantee holds on overwrite too.
71
+ const fd = fs_1.default.openSync(filePath, 'w', 0o600);
72
+ try {
73
+ fs_1.default.writeFileSync(fd, body, { encoding: 'utf8' });
74
+ fs_1.default.fchmodSync(fd, 0o600);
75
+ }
76
+ finally {
77
+ fs_1.default.closeSync(fd);
78
+ }
79
+ return { bytes: Buffer.byteLength(body, 'utf8'), records: lines.length };
80
+ }
@@ -0,0 +1,407 @@
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.parseArgv = parseArgv;
7
+ exports.run = run;
8
+ const path_1 = __importDefault(require("path"));
9
+ const plan_1 = require("./plan");
10
+ const existing_1 = require("./existing");
11
+ const apply_1 = require("./apply");
12
+ const source_1 = require("./source");
13
+ const bundle_write_1 = require("./bundle-write");
14
+ const markdown_1 = require("./markdown");
15
+ const USAGE = `renor-import — import existing memory into a Renor workspace
16
+
17
+ Usage:
18
+ renor-import --from <source> --to <workspace-memory-dir> [--apply] [options]
19
+
20
+ Source spec:
21
+ markdown:<folder> an Obsidian vault or Markdown folder; no source index needed.
22
+ Uses the normal vault ignore rules and .semanticignore.
23
+ Only lowercase .md files; no attachments or symlinks.
24
+ A file or folder name that is not valid UTF-8 cannot be imported:
25
+ such files are listed, not imported; every other note still is.
26
+ renor-index:<path-to-index.db> a Renor-shaped index (another workspace, or a personal
27
+ brain). Opened READ-ONLY. Combine with --source-root.
28
+ bundle:<path-to.jsonl> a renor-memory-export/v1 bundle, so any provider can
29
+ produce the input for this command.
30
+
31
+ Destination:
32
+ --source-root <dir> directory the index's stored absolute paths live under
33
+ (used to build the relative path).
34
+ --content-root <dir> tree holding the source's original Markdown (defaults to
35
+ --source-root). Originals are carried byte-for-byte when
36
+ present; a missing file falls back to reconstruction.
37
+
38
+ --to <memory-dir> the workspace's memory directory (its canonical store).
39
+ --db <path> derived index path (default <memory-dir>/.memory-index/index.db)
40
+ --ledger-rel <rel> ledger path relative to the memory dir
41
+ (default System/Memory Events Ledger.md)
42
+
43
+ Selection (both repeatable; applied to every source kind, by each record's own
44
+ relative path — for renor-index: that is relative to --source-root, for bundle: it is
45
+ the record's relPath):
46
+ --include <prefix> include only this prefix (paths are relative to the source
47
+ root and must be matched by the note to be carried).
48
+ --exclude <prefix> exclude this prefix. Applied after --include.
49
+
50
+ Ledger-derived facts (the owner-stated memory, source path '_mcp_') are carried by default
51
+ because they are the smallest and highest-value set. Exclude them with --exclude _mcp_.
52
+ --limit <n> cap the number of selected notes.
53
+
54
+ Writing:
55
+ --namespace <rel> additive destination subdirectory under Imported/
56
+ (default Imported/<source>)
57
+ --apply perform the writes (default: plan only)
58
+ --canonical-only write Markdown + ledger events only; copy no index rows
59
+
60
+ Other:
61
+ --event-source <label> provenance recorded in the ledger (default import:<source>)
62
+ --emit-bundle <file> write the selected source as a renor-memory-export/v1
63
+ JSONL bundle and exit. This is how an import runs where
64
+ the workspace lives: export once from a source you can
65
+ only read, then import the file next to the workspace.
66
+ --json emit the machine-readable plan/summary only
67
+ --help show this message
68
+ `;
69
+ function parseArgv(argv) {
70
+ const options = {
71
+ namespace: '',
72
+ ledgerRel: 'System/Memory Events Ledger.md',
73
+ apply: false,
74
+ canonicalOnly: false,
75
+ json: false,
76
+ };
77
+ const include = [];
78
+ const exclude = [];
79
+ for (let i = 0; i < argv.length; i += 1) {
80
+ const arg = argv[i];
81
+ const value = () => {
82
+ const next = argv[i + 1];
83
+ if (next === undefined)
84
+ throw new Error(`${arg} requires a value`);
85
+ i += 1;
86
+ return next;
87
+ };
88
+ switch (arg) {
89
+ case '--help':
90
+ case '-h': return { help: true };
91
+ case '--from':
92
+ options.from = value();
93
+ break;
94
+ case '--to':
95
+ options.to = value();
96
+ break;
97
+ case '--source-root':
98
+ options.sourceRoot = value();
99
+ break;
100
+ case '--content-root':
101
+ options.contentRoot = value();
102
+ break;
103
+ case '--namespace':
104
+ options.namespace = value();
105
+ break;
106
+ case '--include':
107
+ include.push(value());
108
+ break;
109
+ case '--exclude':
110
+ exclude.push(value());
111
+ break;
112
+ case '--limit':
113
+ options.limit = Number(value());
114
+ break;
115
+ case '--ledger-rel':
116
+ options.ledgerRel = value();
117
+ break;
118
+ case '--db':
119
+ options.dbPath = value();
120
+ break;
121
+ case '--event-source':
122
+ options.eventSource = value();
123
+ break;
124
+ case '--apply':
125
+ options.apply = true;
126
+ break;
127
+ case '--canonical-only':
128
+ options.canonicalOnly = true;
129
+ break;
130
+ case '--json':
131
+ options.json = true;
132
+ break;
133
+ case '--emit-bundle':
134
+ options.emitBundle = value();
135
+ break;
136
+ default:
137
+ throw new Error(`unknown argument ${arg}`);
138
+ }
139
+ }
140
+ if (options.namespace)
141
+ (0, plan_1.assertImportNamespace)(options.namespace);
142
+ if (!options.from)
143
+ throw new Error('--from is required (see --help)');
144
+ if (!options.to)
145
+ throw new Error('--to is required (see --help)');
146
+ if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 0)) {
147
+ throw new Error('--limit must be a non-negative integer');
148
+ }
149
+ return { ...options, include, exclude };
150
+ }
151
+ /** Source label used in the default namespace and provenance label. */
152
+ function sourceKindAndLabel(from) {
153
+ const separator = from.indexOf(':');
154
+ const kind = separator < 0 ? from : from.slice(0, separator);
155
+ const target = separator < 0 ? '' : from.slice(separator + 1);
156
+ const base = path_1.default.basename(target || kind).replace(/\.(db|jsonl|json)$/i, '');
157
+ return { kind, label: base || kind };
158
+ }
159
+ /** The Markdown files a source could not carry because a file or folder name is not valid UTF-8. */
160
+ function printNotImported(names, labelWidth) {
161
+ if (names.length === 0)
162
+ return;
163
+ console.log(` ${'not imported'.padEnd(labelWidth)}: ${names.length} Markdown file(s) whose file or folder name is not valid UTF-8`
164
+ + ' (bad bytes are shown as \\xNN); rename them to UTF-8 names, then run the import again');
165
+ for (const name of names.slice(0, 10))
166
+ console.log(` ! ${name}`);
167
+ if (names.length > 10)
168
+ console.log(` ! …and ${names.length - 10} more (--json lists them all)`);
169
+ }
170
+ /**
171
+ * What the secret guard left out of the plan (RENOR_SECRET_GUARD is on): a path or a fact id and the field that
172
+ * looked like a secret, never text. A path that itself looks like a secret is withheld.
173
+ */
174
+ function printGuardSkips(skips, labelWidth) {
175
+ if (!skips || skips.notes.length + skips.events.length === 0)
176
+ return;
177
+ console.log(` ${'secret guard'.padEnd(labelWidth)}: ${skips.notes.length} note(s) and ${skips.events.length} fact(s) not imported: they appear to`
178
+ + ' contain a credential or personal identifier (RENOR_SECRET_GUARD is on); every other note still is');
179
+ const lines = [
180
+ ...skips.notes.map(skip => `${skip.ref ?? '(path withheld)'} (${skip.field}: ${skip.reason})`),
181
+ ...skips.events.map(skip => `fact ${skip.ref} (${skip.field}: ${skip.reason})`),
182
+ ];
183
+ for (const line of lines.slice(0, 10))
184
+ console.log(` ! ${line}`);
185
+ if (lines.length > 10)
186
+ console.log(` ! …and ${lines.length - 10} more (--json lists them all)`);
187
+ }
188
+ async function run(argv) {
189
+ let options;
190
+ try {
191
+ options = parseArgv(argv);
192
+ }
193
+ catch (err) {
194
+ console.error(err instanceof Error ? err.message : String(err));
195
+ console.error(USAGE);
196
+ return 2;
197
+ }
198
+ if ('help' in options) {
199
+ console.log(USAGE);
200
+ return 0;
201
+ }
202
+ const { label } = sourceKindAndLabel(options.from);
203
+ const include = options.include ?? [];
204
+ const exclude = options.exclude ?? [];
205
+ const namespace = options.namespace || path_1.default.posix.join('Imported', label);
206
+ const memoryDir = path_1.default.resolve(options.to);
207
+ const dbPath = options.dbPath ?? path_1.default.join(memoryDir, '.memory-index', 'index.db');
208
+ const ledgerPath = path_1.default.join(memoryDir, ...options.ledgerRel.split('/'));
209
+ const eventSource = options.eventSource || `import:${label}`;
210
+ let bundle;
211
+ try {
212
+ const filter = {
213
+ include,
214
+ exclude,
215
+ limit: options.limit,
216
+ sourceRoot: options.sourceRoot,
217
+ contentRoot: options.contentRoot,
218
+ label,
219
+ };
220
+ if (options.from.startsWith('markdown:')) {
221
+ const root = path_1.default.resolve(options.from.slice('markdown:'.length));
222
+ // Do not recursively import the destination on a later run. Use real paths
223
+ // for the source and nearest existing destination ancestor (including links).
224
+ const fs = await import('fs');
225
+ let ancestor = memoryDir;
226
+ while (!fs.existsSync(ancestor) && path_1.default.dirname(ancestor) !== ancestor)
227
+ ancestor = path_1.default.dirname(ancestor);
228
+ const destination = path_1.default.resolve(fs.realpathSync(ancestor), path_1.default.relative(ancestor, memoryDir));
229
+ const source = fs.realpathSync(root);
230
+ const contains = (parent, child) => {
231
+ const rel = path_1.default.relative(parent, child);
232
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${path_1.default.sep}`) && !path_1.default.isAbsolute(rel));
233
+ };
234
+ if (contains(source, destination) || contains(destination, source)) {
235
+ throw new Error('Markdown source and destination must be separate folders');
236
+ }
237
+ }
238
+ bundle = options.from.startsWith('markdown:')
239
+ ? await (0, markdown_1.readMarkdownSource)(options.from.slice('markdown:'.length), filter)
240
+ : (0, source_1.readSource)(options.from, filter);
241
+ }
242
+ catch (err) {
243
+ console.error(`source read failed: ${err instanceof Error ? err.message : String(err)}`);
244
+ return 1;
245
+ }
246
+ const nonUtf8Names = bundle.nonUtf8Names ?? [];
247
+ if (options.emitBundle) {
248
+ const written = (0, bundle_write_1.writeBundle)(bundle, options.emitBundle);
249
+ if (options.json) {
250
+ console.log(JSON.stringify({
251
+ mode: 'emit-bundle',
252
+ source: { spec: options.from, label: bundle.sourceLabel, format: bundle.format },
253
+ bundle: { file: options.emitBundle, bytes: written.bytes, records: written.records },
254
+ counts: {
255
+ notes: bundle.notes.length,
256
+ chunks: bundle.stats.chunksSelected,
257
+ facts: bundle.facts.length,
258
+ notesWithOriginalBytes: bundle.stats.notesWithOriginalBytes,
259
+ notesSkippedNonUtf8Name: nonUtf8Names.length,
260
+ },
261
+ skipped: { nonUtf8Names },
262
+ warning: 'the bundle contains memory content; it is written 0600 and must not be committed',
263
+ }, null, 2));
264
+ }
265
+ else {
266
+ console.log('renor-import: EMIT BUNDLE');
267
+ console.log(` source : ${options.from}`);
268
+ console.log(` bundle : ${options.emitBundle} (${written.bytes} bytes, ${written.records} records)`);
269
+ console.log(` counts : notes ${bundle.notes.length} (${bundle.stats.notesWithOriginalBytes} with original bytes), chunks ${bundle.stats.chunksSelected}, facts ${bundle.facts.length}`);
270
+ printNotImported(nonUtf8Names, 12);
271
+ console.log(' note : the bundle contains memory content; it is written 0600 and must not be committed');
272
+ }
273
+ return 0;
274
+ }
275
+ const plan = (0, plan_1.buildImportPlan)(bundle, {
276
+ namespace,
277
+ label,
278
+ eventSource,
279
+ existingStreams: (0, existing_1.readExistingStreams)(dbPath, ledgerPath),
280
+ });
281
+ const warnings = [];
282
+ if (options.from.startsWith('markdown:')) {
283
+ warnings.push('Only lowercase .md files are imported. Attachments and symlinks are not imported; '
284
+ + 'vault ignore rules and .semanticignore apply. Markdown is preserved, but this import '
285
+ + 'creates no extracted entities or structured facts, and makes no embedding call itself. '
286
+ + 'Use search_text with an exact phrase from a note to verify recall.');
287
+ }
288
+ if (plan.counts.multiValuedFactsDropped > 0) {
289
+ warnings.push(`${plan.counts.multiValuedFactsDropped} fact row(s) belong to multi-valued streams: the `
290
+ + `lifecycle layer holds at most one current value per subject+predicate, so they are `
291
+ + `carried as notes only and not as current facts.`);
292
+ }
293
+ warnings.push('Embeddings are not transferred: a vector is bound to the model that produced it, so a vector from '
294
+ + 'another model would be silent garbage rather than a saving. Imported notes are lexically '
295
+ + '(full-text) searchable at once. Applied to a workspace with semantic search, the import marks '
296
+ + 'its index unfinished: the workspace service embeds the imported notes with its own model, once, '
297
+ + 'when it next starts, and its semantic search is off until they are embedded.');
298
+ warnings.push('Each imported note row carries the hash of the exact bytes written, which is the indexer\'s own '
299
+ + '"unchanged" condition. With no extraction lane armed (this workspace\'s configuration) a later '
300
+ + 'index pass is therefore a no-op for imported notes: no re-chunk, no re-embed, no re-extract. '
301
+ + 'If a fact/chronicle/preference extraction lane is armed later, its cache key is that same hash '
302
+ + 'and the lane has no rows for these notes yet, so that pass would re-extract and re-embed them. '
303
+ + 'Content is durable in Markdown and the ledger; the indexed rows are derived and a rebuild would '
304
+ + 're-derive them.');
305
+ const summary = {
306
+ mode: options.apply ? 'apply' : 'plan',
307
+ source: { spec: options.from, label: bundle.sourceLabel, format: bundle.format },
308
+ destination: { memoryDir, dbPath, ledgerRel: options.ledgerRel, namespace },
309
+ selection: {
310
+ include,
311
+ exclude,
312
+ limit: options.limit ?? null,
313
+ },
314
+ counts: {
315
+ ...plan.counts,
316
+ // What the selection actually removed, measured by the adapter rather than echoed
317
+ // back from the flags. A filter that is silently ignored is indistinguishable from
318
+ // a filter that matched nothing, and `--exclude` failing quietly is a privacy bug.
319
+ notesFilteredOut: bundle.stats.notesFilteredOut,
320
+ factsFilteredOut: Math.max(0, bundle.stats.factsAvailable - bundle.stats.factsSelected),
321
+ eventsAlreadyInDestination: 0,
322
+ notesSkippedNonUtf8Name: nonUtf8Names.length,
323
+ },
324
+ skipped: { ...plan.skipped, nonUtf8Names },
325
+ warnings,
326
+ };
327
+ let exitCode = 0;
328
+ if (options.apply) {
329
+ const result = await (0, apply_1.applyImportPlan)(plan, bundle, {
330
+ memoryDir,
331
+ dbPath,
332
+ ledgerRelPath: options.ledgerRel,
333
+ canonicalOnly: options.canonicalOnly,
334
+ });
335
+ summary.applied = result;
336
+ summary.counts = {
337
+ ...summary.counts,
338
+ eventsAlreadyInDestination: result.eventsAlreadyPresent,
339
+ };
340
+ summary.after = result.destination;
341
+ }
342
+ else {
343
+ summary.after = null;
344
+ }
345
+ if (options.json) {
346
+ console.log(JSON.stringify(summary, null, 2));
347
+ }
348
+ else {
349
+ const mode = options.apply ? 'APPLIED' : 'PLAN (dry run — pass --apply to write)';
350
+ console.log(`renor-import: ${mode}`);
351
+ console.log(` source : ${options.from} (label ${bundle.sourceLabel})`);
352
+ console.log(` destination : ${memoryDir}`);
353
+ console.log(` namespace : ${namespace}`);
354
+ console.log(` notes : ${plan.counts.selectedNotes} selected of ${plan.counts.notesInBundle} in bundle`
355
+ + ` (${plan.skipped.duplicateNotePath} duplicate path, ${plan.skipped.emptyNoteBody} empty)`);
356
+ if (bundle.stats.notesFilteredOut > 0 || bundle.stats.factsAvailable > bundle.stats.factsSelected) {
357
+ console.log(` selection : ${bundle.stats.notesFilteredOut} note(s) and `
358
+ + `${bundle.stats.factsAvailable - bundle.stats.factsSelected} fact(s) left out by --include/--exclude/--limit`);
359
+ }
360
+ printNotImported(nonUtf8Names, 14);
361
+ printGuardSkips(plan.skipped.secretGuard, 14);
362
+ console.log(` chunks : ${plan.counts.selectedChunks} from the source parse (no re-chunking)`);
363
+ console.log(` note bytes : ${plan.counts.notesFromOriginalBytes} carried verbatim, ${plan.counts.notesReconstructed} reconstructed from chunks`);
364
+ if (plan.counts.chunksNotLocated > 0) {
365
+ console.log(` ! ${plan.counts.chunksNotLocated} chunk(s) not locatable in the carried bytes; ${plan.counts.chunksSpanInherited} inherited the source span, the rest have an unknown span`);
366
+ }
367
+ console.log(` ledger events : ${plan.counts.eventsToAppend} new`
368
+ + (options.apply ? ` (${summary.applied.eventsAppended} appended, ${summary.applied.eventsAlreadyPresent} already present)` : ''));
369
+ console.log(` facts : ${plan.counts.factsInBundle} current in source, ${plan.counts.eventsToAppend} carried, `
370
+ + `${plan.counts.multiValuedFactsDropped} multi-valued dropped, ${plan.counts.factsSkipped} other skips`);
371
+ for (const skip of plan.skipped.events) {
372
+ console.log(` - ${skip.count} × ${skip.reason}`);
373
+ }
374
+ if (options.apply) {
375
+ const applied = summary.applied;
376
+ console.log(` files : ${applied.notesWritten} written, ${applied.notesUnchanged} already identical, ${applied.notesConflicting} conflicting (never overwritten or indexed)`);
377
+ for (const conflict of (applied.conflicts ?? []).slice(0, 10)) {
378
+ console.log(` ! conflict (left as-is, not indexed): ${conflict}`);
379
+ }
380
+ if ((applied.conflicts ?? []).length > 10) {
381
+ console.log(` ! …and ${applied.conflicts.length - 10} more`);
382
+ }
383
+ console.log(` index rows : ${applied.indexRows.notesInserted} notes, ${applied.indexRows.chunksInserted} chunks, ${applied.indexRows.entitiesInserted} entities inserted`);
384
+ console.log(` replayed : ${applied.ledgerReplayed} ledger event(s) applied to the index`);
385
+ console.log(` workspace now : notes ${applied.destination.notes} · chunks ${applied.destination.chunks} · entities ${applied.destination.entities} · facts ${applied.destination.facts} (${applied.destination.currentFacts} current)`);
386
+ }
387
+ for (const warning of warnings)
388
+ console.log(` note : ${warning}`);
389
+ }
390
+ if (options.apply && summary.applied.notesConflicting > 0)
391
+ exitCode = 3;
392
+ // Like a conflict, a note the guard left out is something the operator asked for that was not imported.
393
+ const guardSkips = plan.skipped.secretGuard;
394
+ if (options.apply && guardSkips && guardSkips.notes.length + guardSkips.events.length > 0)
395
+ exitCode = 3;
396
+ return exitCode;
397
+ }
398
+ /* c8 ignore start — process wiring; `run` above holds the testable behavior. */
399
+ if (require.main === module) {
400
+ run(process.argv.slice(2))
401
+ .then(code => { process.exitCode = code; })
402
+ .catch(err => {
403
+ console.error(err instanceof Error ? err.stack ?? err.message : String(err));
404
+ process.exitCode = 1;
405
+ });
406
+ }
407
+ /* c8 ignore stop */
@@ -0,0 +1,49 @@
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.readExistingStreams = readExistingStreams;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const client_1 = require("../db/client");
9
+ const lifecycle_1 = require("../memory/lifecycle");
10
+ const plan_1 = require("./plan");
11
+ /**
12
+ * Fact streams a destination already represents.
13
+ *
14
+ * Two sources, because they disagree in a way that matters: the **ledger** is canonical
15
+ * and the index is derived. A stream a ledger event already declares is represented even
16
+ * when the projection has not caught up — and importing an older value for it would not
17
+ * merely duplicate, it would fail the engine's per-stream monotonicity check during
18
+ * replay, which runs at service STARTUP and refuses to serve. Skipping it is the only
19
+ * safe move, and the skip is counted and reported rather than silent.
20
+ */
21
+ function readExistingStreams(dbPath, ledgerPath) {
22
+ const streams = new Set();
23
+ let db = null;
24
+ try {
25
+ db = (0, client_1.openReadOnlyDb)(dbPath);
26
+ const rows = db.prepare(`SELECT e.canonical_name AS subject, f.predicate AS predicate, f.scope AS scope
27
+ FROM facts f JOIN entities e ON e.id = f.subject_entity_id
28
+ WHERE f.valid_to IS NULL AND f.source_path = '_mcp_'`).all();
29
+ for (const row of rows)
30
+ streams.add((0, plan_1.streamKey)(row.subject, row.predicate, row.scope ?? 'global'));
31
+ }
32
+ catch {
33
+ // A destination with no index yet simply has no indexed streams.
34
+ }
35
+ finally {
36
+ db?.close();
37
+ }
38
+ try {
39
+ const content = fs_1.default.readFileSync(ledgerPath, 'utf8');
40
+ for (const event of (0, lifecycle_1.parseLedgerEvents)(content)) {
41
+ streams.add((0, plan_1.streamKey)(event.subject, event.predicate, event.scope ?? 'global'));
42
+ }
43
+ }
44
+ catch {
45
+ // No ledger yet, or an unreadable one: nothing declared that this read can vouch for.
46
+ // Replay still fails closed on the real content, so this cannot launder a bad ledger.
47
+ }
48
+ return streams;
49
+ }
@@ -0,0 +1,123 @@
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.EVENT_ID_RE = void 0;
7
+ exports.deterministicEventId = deterministicEventId;
8
+ exports.toCanonicalInstant = toCanonicalInstant;
9
+ exports.tryNormalizeScope = tryNormalizeScope;
10
+ exports.validateCarriedFact = validateCarriedFact;
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const scope_1 = require("../mvp/scope");
13
+ /**
14
+ * Identity and validation rules for carried facts.
15
+ *
16
+ * A carried fact becomes a canonical ledger event, because the engine's lifecycle
17
+ * layer (and therefore the dashboard's fact list and graph) reads only facts whose
18
+ * `source_path` is the lifecycle sentinel — facts derived from notes are invisible
19
+ * to those readers.
20
+ *
21
+ * Two hard requirements come from `engine/src/memory/lifecycle.ts`:
22
+ * - event `id` and `at` decide replay idempotency (`applied_ledger_events`), so a
23
+ * re-run must produce a byte-identical event or replay either duplicates it or
24
+ * fails with "reused with a conflicting payload" — and replay runs at service
25
+ * STARTUP, so a mutated payload would break the workspace's readiness gate;
26
+ * - `at` must be the canonical instant form and ids must match `^mem_[A-Za-z0-9]+$`.
27
+ *
28
+ * Scope and field limits are therefore imported from the engine rather than mirrored
29
+ * here: a validator that drifts looser fails at boot, and one that drifts stricter
30
+ * silently loses memory.
31
+ */
32
+ exports.EVENT_ID_RE = /^mem_[A-Za-z0-9]+$/;
33
+ /** Field limits as enforced by `lifecycle.ts` `validateField`. */
34
+ const FIELD_LIMITS = { subject: 512, predicate: 256, object: 8192, source: 256 };
35
+ const CONTROL_CHARS = /[\u0000-\u001F\u007F]/;
36
+ const CANONICAL_TS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
37
+ /** The instant forms the engine's own normalizer accepts: an explicit zone, or date-only. */
38
+ const ZONED_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
39
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
40
+ /**
41
+ * Deterministic event id for a carried fact.
42
+ *
43
+ * It is a function of **everything replay compares** — source label, subject,
44
+ * predicate, scope, object and `at` — because the id's only job is to make a re-run a
45
+ * no-op. A narrower key would mint the same id for a different payload when the source
46
+ * changed between runs, which replay rejects as corruption.
47
+ */
48
+ function deterministicEventId(input) {
49
+ const digest = crypto_1.default
50
+ .createHash('sha256')
51
+ .update([
52
+ 'renor-memory-import/v1',
53
+ input.label,
54
+ input.subject,
55
+ input.predicate,
56
+ input.scope,
57
+ input.object,
58
+ input.at,
59
+ ].join('\u0000'))
60
+ .digest('hex')
61
+ .slice(0, 40);
62
+ return `mem_imp${digest}`;
63
+ }
64
+ /**
65
+ * Canonical instant, or null when the input cannot be one. Mirrors the engine's
66
+ * accepted inputs (an ISO instant with an explicit timezone, or date-only) and rejects
67
+ * timezone-less datetimes, which the engine treats as ambiguous.
68
+ */
69
+ function toCanonicalInstant(value) {
70
+ if (typeof value !== 'string')
71
+ return null;
72
+ const trimmed = value.trim();
73
+ if (CANONICAL_TS.test(trimmed) || ZONED_INSTANT.test(trimmed) || DATE_ONLY.test(trimmed)) {
74
+ const parsed = new Date(trimmed);
75
+ if (Number.isNaN(parsed.getTime()))
76
+ return null;
77
+ const year = parsed.getUTCFullYear();
78
+ if (year < 1970 || year > 2999)
79
+ return null;
80
+ return parsed.toISOString();
81
+ }
82
+ return null;
83
+ }
84
+ /** `normalizeScope` from the engine, or null when it rejects the value. */
85
+ function tryNormalizeScope(raw) {
86
+ try {
87
+ return (0, scope_1.normalizeScope)(raw);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Validate a fact once, before anything is written, and normalize its scope and
95
+ * instant. A rejection is counted by reason rather than surfacing at the destination's
96
+ * readiness gate.
97
+ */
98
+ function validateCarriedFact(fact) {
99
+ if (!fact.subject.trim())
100
+ return { ok: false, reason: 'empty subject' };
101
+ if (!fact.predicate.trim())
102
+ return { ok: false, reason: 'empty predicate' };
103
+ if (!fact.object.trim())
104
+ return { ok: false, reason: 'empty object' };
105
+ if (CONTROL_CHARS.test(fact.subject) || CONTROL_CHARS.test(fact.predicate)
106
+ || CONTROL_CHARS.test(fact.object) || CONTROL_CHARS.test(fact.scope)) {
107
+ return { ok: false, reason: 'control character in a lifecycle field' };
108
+ }
109
+ if (fact.subject.length > FIELD_LIMITS.subject)
110
+ return { ok: false, reason: 'subject exceeds 512 characters' };
111
+ if (fact.predicate.length > FIELD_LIMITS.predicate)
112
+ return { ok: false, reason: 'predicate exceeds 256 characters' };
113
+ if (fact.object.length > FIELD_LIMITS.object)
114
+ return { ok: false, reason: 'object exceeds 8192 characters' };
115
+ const scope = tryNormalizeScope(fact.scope);
116
+ if (scope === null)
117
+ return { ok: false, reason: 'scope is not a valid scope, scope:<slug> or project:<slug>' };
118
+ const at = toCanonicalInstant(fact.validFrom);
119
+ if (at === null) {
120
+ return { ok: false, reason: 'valid_from is not an ISO instant with an explicit timezone' };
121
+ }
122
+ return { ok: true, scope, at };
123
+ }