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,79 @@
|
|
|
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.IMPORT_JOURNAL_DIR = void 0;
|
|
7
|
+
exports.forgetImportedNotes = forgetImportedNotes;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const journalLog_1 = require("./journalLog");
|
|
12
|
+
/**
|
|
13
|
+
* The import journal: one JSON manifest per batch in `.renor-imports/` (hosted.ts writes it and undo_import reads it). While a
|
|
14
|
+
* batch is applying, its notes are in a sidecar log beside the manifest instead (journalLog.ts).
|
|
15
|
+
* It names every note a batch created, by path, and the batch's label, which is the folder name of those notes. A note that is
|
|
16
|
+
* permanently deleted (purgeRetired) leaves no trace there: its path is a name the owner chose, and the label can say more
|
|
17
|
+
* than the note does.
|
|
18
|
+
*/
|
|
19
|
+
exports.IMPORT_JOURNAL_DIR = '.renor-imports';
|
|
20
|
+
async function replaceManifest(file, manifest) {
|
|
21
|
+
const tmp = `${file}.${crypto_1.default.randomUUID()}.tmp`;
|
|
22
|
+
await promises_1.default.writeFile(tmp, JSON.stringify(manifest), { flag: 'wx', mode: 0o600 });
|
|
23
|
+
await promises_1.default.rename(tmp, file);
|
|
24
|
+
// A batch that is no longer applying has no log of its own to keep: a log left beside it would still name the paths removed here.
|
|
25
|
+
await promises_1.default.rm(path_1.default.join(path_1.default.dirname(file), (0, journalLog_1.journalLogName)(path_1.default.basename(file, '.json'))), { force: true }).catch(() => undefined);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove the given note paths from every import manifest: from `created` (so undo_import has nothing to retire for them) and
|
|
29
|
+
* from `result.conflicts`. A batch left with no note is kept as a record that it is over, with its id and state `undone`, but
|
|
30
|
+
* without its label, paths or counts, so undo_import and a second apply say so instead of "not found". A batch that still
|
|
31
|
+
* holds other notes keeps them, and its label, which is their folder. An apply still running is left alone: it writes its own
|
|
32
|
+
* journal. Each file is replaced atomically. Idempotent, so a retry after a crash repeats it harmlessly. A file that is not a
|
|
33
|
+
* plain manifest is left alone.
|
|
34
|
+
*/
|
|
35
|
+
async function forgetImportedNotes(memoryDir, relPaths) {
|
|
36
|
+
if (relPaths.size === 0)
|
|
37
|
+
return;
|
|
38
|
+
const dir = path_1.default.join(memoryDir, exports.IMPORT_JOURNAL_DIR);
|
|
39
|
+
let names;
|
|
40
|
+
try {
|
|
41
|
+
names = await promises_1.default.readdir(dir);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === 'ENOENT')
|
|
45
|
+
return;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
if (!name.endsWith('.json'))
|
|
50
|
+
continue;
|
|
51
|
+
const file = path_1.default.join(dir, name);
|
|
52
|
+
const st = await promises_1.default.lstat(file);
|
|
53
|
+
if (!st.isFile())
|
|
54
|
+
continue;
|
|
55
|
+
let manifest;
|
|
56
|
+
try {
|
|
57
|
+
manifest = JSON.parse(await promises_1.default.readFile(file, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(manifest.created) || manifest.state === 'applying')
|
|
63
|
+
continue;
|
|
64
|
+
const created = manifest.created.filter((c) => !relPaths.has(String(c.relPath)));
|
|
65
|
+
const conflictsWas = Array.isArray(manifest.result?.conflicts) ? manifest.result.conflicts : null;
|
|
66
|
+
const conflicts = conflictsWas?.filter((p) => !relPaths.has(String(p))) ?? null;
|
|
67
|
+
const changed = created.length !== manifest.created.length || (conflicts !== null && conflicts.length !== conflictsWas.length);
|
|
68
|
+
if (!changed)
|
|
69
|
+
continue;
|
|
70
|
+
if (created.length === 0) {
|
|
71
|
+
await replaceManifest(file, { batchId: String(manifest.batchId ?? path_1.default.basename(name, '.json')), state: 'undone', created: [] });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
manifest.created = created;
|
|
75
|
+
if (conflicts !== null)
|
|
76
|
+
manifest.result = { ...manifest.result, conflicts };
|
|
77
|
+
await replaceManifest(file, manifest);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
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.importBatchSchema = exports.importPreviewSchema = exports.IMPORT_LIMITS = void 0;
|
|
7
|
+
exports.folderLabel = folderLabel;
|
|
8
|
+
exports.previewImport = previewImport;
|
|
9
|
+
exports.applyHostedImport = applyHostedImport;
|
|
10
|
+
exports.undoHostedImport = undoHostedImport;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const markdown_1 = require("./markdown");
|
|
16
|
+
const source_1 = require("./source");
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
const plan_1 = require("./plan");
|
|
19
|
+
const apply_1 = require("./apply");
|
|
20
|
+
const memoryMap_1 = require("../memorymap/memoryMap");
|
|
21
|
+
const invisibleText_1 = require("../memory/invisibleText");
|
|
22
|
+
const controls_1 = require("../memory/controls");
|
|
23
|
+
const secretGuard_1 = require("../extraction/secretGuard");
|
|
24
|
+
const toolRefusal_1 = require("../toolRefusal");
|
|
25
|
+
const forgetImported_1 = require("./forgetImported");
|
|
26
|
+
const storedSize_1 = require("../mvp/storedSize");
|
|
27
|
+
const journalLog_1 = require("./journalLog");
|
|
28
|
+
// Fits the existing 1 MiB HTTP ceiling even with JSON escaping. Count UTF-8
|
|
29
|
+
// bytes, including paths, before parsing/selection. Refuse rather than slice.
|
|
30
|
+
exports.IMPORT_LIMITS = { fileBytes: 64 * 1024, batchBytes: 128 * 1024, files: 100 };
|
|
31
|
+
const PREVIEW_MS = 15 * 60 * 1000;
|
|
32
|
+
const batchIdSchema = zod_1.z.string().uuid('batchId must be the batchId returned by preview_import (a UUID)');
|
|
33
|
+
/** Zod's default for a missing field says only "expected array, received undefined"; say what to send instead. */
|
|
34
|
+
function required(message) { return (issue) => (issue.input === undefined ? message : undefined); }
|
|
35
|
+
exports.importPreviewSchema = {
|
|
36
|
+
label: zod_1.z.string().min(1).max(200).describe('Name for this import; notes go to Imported/<label>/. It is a folder name: letters (any alphabet), digits, "-" and "_" are kept; spaces and punctuation become "-".'),
|
|
37
|
+
files: zod_1.z.array(zod_1.z.object({
|
|
38
|
+
relativePath: zod_1.z.string({ error: required("files[] entries need relativePath: the file's path inside the folder, e.g. notes/idea.md") }).min(1).max(512),
|
|
39
|
+
markdown: zod_1.z.string({ error: required("files[] entries need markdown: the file's full text") })
|
|
40
|
+
.max(exports.IMPORT_LIMITS.fileBytes, 'This file is over 64 KiB. Split it into several smaller .md files (by heading) or leave it out.'),
|
|
41
|
+
}).strict(), { error: required('files is required: an array of { relativePath, markdown } holding each file\'s text. The server cannot read your disk; read each file yourself and send its text.') })
|
|
42
|
+
.min(1).max(exports.IMPORT_LIMITS.files, 'Too many files: send at most 100 per call, then call preview_import again for the next group.'),
|
|
43
|
+
include: zod_1.z.array(zod_1.z.string().min(1).max(512)).max(100).optional().describe('Only these folder or file path prefixes, e.g. "Projects" (prefixes, not globs)'),
|
|
44
|
+
exclude: zod_1.z.array(zod_1.z.string().min(1).max(512)).max(100).optional().describe('Leave these folder or file path prefixes out, e.g. "Private" (prefixes, not globs)'),
|
|
45
|
+
stage: zod_1.z.boolean().optional().describe('false: check only. Same answer without a batchId; nothing is held, so nothing can be applied from it'),
|
|
46
|
+
};
|
|
47
|
+
exports.importBatchSchema = { batchId: batchIdSchema };
|
|
48
|
+
const states = new WeakMap();
|
|
49
|
+
function state(db) {
|
|
50
|
+
let value = states.get(db);
|
|
51
|
+
if (!value) {
|
|
52
|
+
value = { previews: new Map(), chain: Promise.resolve() };
|
|
53
|
+
states.set(db, value);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function serial(db, fn) {
|
|
58
|
+
const tenant = state(db);
|
|
59
|
+
const result = tenant.chain.then(fn, fn);
|
|
60
|
+
tenant.chain = result.then(() => undefined, () => undefined);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function invalidUnicode(text) {
|
|
64
|
+
return /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(text);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The folder name Imported/<label> uses. A name the old pattern accepted ([a-zA-Z0-9][a-zA-Z0-9_-]{0,63}) maps to
|
|
68
|
+
* itself, so a re-import lands on the same notes. Only letters, digits, marks, '-' and '_' survive (in NFC, so "café" is
|
|
69
|
+
* one folder however it was typed): no '/', '.', '\\', control character or anything else that changes the target.
|
|
70
|
+
*/
|
|
71
|
+
const MAX_LABEL_BYTES = 200;
|
|
72
|
+
function folderLabel(raw) {
|
|
73
|
+
const kept = raw.normalize('NFC').replace(/[^\p{L}\p{N}\p{M}_-]+/gu, '-').replace(/^[^\p{L}\p{N}]+/u, '');
|
|
74
|
+
const label = [...kept].slice(0, 64).join('');
|
|
75
|
+
if (!label)
|
|
76
|
+
throw (0, toolRefusal_1.refusal)('invalid_argument', 'label needs at least one letter or digit, e.g. "chatgpt-memory" (it becomes a folder name).');
|
|
77
|
+
if (Buffer.byteLength(label, 'utf8') > MAX_LABEL_BYTES)
|
|
78
|
+
throw (0, toolRefusal_1.refusal)('field_too_long', `label is too long as a folder name (over ${MAX_LABEL_BYTES} bytes in UTF-8); shorten it.`);
|
|
79
|
+
return label;
|
|
80
|
+
}
|
|
81
|
+
function validatePath(rel, i) {
|
|
82
|
+
const at = `files[${i}]: `;
|
|
83
|
+
if (rel.startsWith('/') || /^[a-zA-Z]:/.test(rel))
|
|
84
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', `${at}relativePath must be relative to the folder you are importing (e.g. notes/CLAUDE.md), not an absolute path.`);
|
|
85
|
+
if (rel.includes('\\'))
|
|
86
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', `${at}use / between folders in relativePath, not a backslash.`);
|
|
87
|
+
if (rel.split('/').some(part => !part || part === '.' || part === '..' || part.trim() !== part)) {
|
|
88
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', `${at}relativePath cannot hold .., . or empty folder names, or a name with leading or trailing spaces; use a path like notes/idea.md.`);
|
|
89
|
+
}
|
|
90
|
+
if (invalidUnicode(rel) || /[:\x00-\x1f\x7f]/.test(rel))
|
|
91
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', `${at}relativePath has a character that cannot be imported (colon, control character or invalid Unicode); rename the file.`);
|
|
92
|
+
if (rel.split('/').some(part => Buffer.byteLength(part, 'utf8') > 255))
|
|
93
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', `${at}a file or folder name in relativePath is over 255 bytes; shorten it.`);
|
|
94
|
+
if (rel.endsWith('.'))
|
|
95
|
+
throw (0, toolRefusal_1.refusal)('unsupported_import_format', `${at}relativePath cannot end with a dot; the name must end in .md.`);
|
|
96
|
+
if (!rel.endsWith('.md')) {
|
|
97
|
+
throw (0, toolRefusal_1.refusal)('unsupported_import_format', /\.md$/i.test(rel)
|
|
98
|
+
? `${at}rename the extension to lowercase .md.`
|
|
99
|
+
: `${at}only .md files can be imported. Leave attachments out; send text or JSON as Markdown (a \`\`\`json fence works) under a .md name.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Check ancestors too. The tenant process is the only authorized vault writer. */
|
|
103
|
+
function safeTarget(root, rel) {
|
|
104
|
+
let current = fs_1.default.realpathSync(root);
|
|
105
|
+
const parts = rel.split('/');
|
|
106
|
+
for (let i = 0; i < parts.length; i++) {
|
|
107
|
+
current = path_1.default.join(current, parts[i]);
|
|
108
|
+
try {
|
|
109
|
+
const stat = fs_1.default.lstatSync(current);
|
|
110
|
+
if (i < parts.length - 1 && (!stat.isDirectory() || stat.isSymbolicLink()))
|
|
111
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', 'Unsafe import destination ancestor');
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (err.code !== 'ENOENT')
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return current;
|
|
119
|
+
}
|
|
120
|
+
function inspect(ctx, plan) {
|
|
121
|
+
const conflicts = [];
|
|
122
|
+
const unchangedPaths = [];
|
|
123
|
+
for (const note of plan.notes) {
|
|
124
|
+
const target = safeTarget(ctx.memoryDir, note.relPath);
|
|
125
|
+
try {
|
|
126
|
+
const stat = fs_1.default.lstatSync(target);
|
|
127
|
+
// An empty file here is an interrupted earlier import's leftover: apply replaces it, so it is not a conflict.
|
|
128
|
+
if ((0, apply_1.isEmptyLeftover)(stat))
|
|
129
|
+
continue;
|
|
130
|
+
if (!stat.isFile() || stat.isSymbolicLink() || fs_1.default.readFileSync(target, 'utf8') !== note.content)
|
|
131
|
+
conflicts.push(note.relPath);
|
|
132
|
+
else
|
|
133
|
+
unchangedPaths.push(note.relPath);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (err.code !== 'ENOENT')
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { conflicts, unchangedPaths, toWrite: plan.notes.length - unchangedPaths.length - conflicts.length };
|
|
141
|
+
}
|
|
142
|
+
function journalPath(ctx, batchId) {
|
|
143
|
+
batchIdSchema.parse(batchId);
|
|
144
|
+
const target = safeTarget(ctx.memoryDir, `${forgetImported_1.IMPORT_JOURNAL_DIR}/${batchId}.json`);
|
|
145
|
+
const dir = path_1.default.dirname(target);
|
|
146
|
+
if (!fs_1.default.existsSync(dir))
|
|
147
|
+
fs_1.default.mkdirSync(dir, { mode: 0o700 });
|
|
148
|
+
try {
|
|
149
|
+
const stat = fs_1.default.lstatSync(target);
|
|
150
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
151
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', 'Unsafe import journal');
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (err.code !== 'ENOENT')
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
return target;
|
|
158
|
+
}
|
|
159
|
+
/** The sidecar log of a batch, beside its manifest (journalLog.ts). */
|
|
160
|
+
function journalLogPath(ctx, batchId) {
|
|
161
|
+
return path_1.default.join(path_1.default.dirname(journalPath(ctx, batchId)), (0, journalLog_1.journalLogName)(batchId));
|
|
162
|
+
}
|
|
163
|
+
function writeJournal(ctx, manifest) {
|
|
164
|
+
const target = journalPath(ctx, manifest.batchId);
|
|
165
|
+
const tmp = `${target}.${crypto_1.default.randomUUID()}.tmp`;
|
|
166
|
+
// Synced before the rename: the journal is what undo_import and a restart trust to name the notes this batch created.
|
|
167
|
+
const fd = fs_1.default.openSync(tmp, 'wx', 0o600);
|
|
168
|
+
try {
|
|
169
|
+
fs_1.default.writeFileSync(fd, JSON.stringify(manifest));
|
|
170
|
+
fs_1.default.fsyncSync(fd);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
fs_1.default.closeSync(fd);
|
|
174
|
+
}
|
|
175
|
+
fs_1.default.renameSync(tmp, target);
|
|
176
|
+
// A manifest that is no longer applying holds every note, so the batch's log is only a duplicate of it now.
|
|
177
|
+
if (manifest.state !== 'applying')
|
|
178
|
+
(0, journalLog_1.removeJournalLog)(path_1.default.join(path_1.default.dirname(target), (0, journalLog_1.journalLogName)(manifest.batchId)));
|
|
179
|
+
}
|
|
180
|
+
function readJournal(ctx, batchId) {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(fs_1.default.readFileSync(journalPath(ctx, batchId), 'utf8'));
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (err.code === 'ENOENT')
|
|
186
|
+
return undefined;
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function manifests(ctx) {
|
|
191
|
+
const dir = safeTarget(ctx.memoryDir, '.renor-imports');
|
|
192
|
+
try {
|
|
193
|
+
return fs_1.default.readdirSync(dir).flatMap(name => {
|
|
194
|
+
if (!/^[0-9a-f-]{36}\.json$/i.test(name))
|
|
195
|
+
return [];
|
|
196
|
+
return [JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, name), 'utf8'))];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err.code === 'ENOENT')
|
|
201
|
+
return [];
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Dry run: bounded, expiring staging in tenant-local memory; no memory writes. */
|
|
206
|
+
async function previewImport(ctx, input) {
|
|
207
|
+
const parsed = zod_1.z.object(exports.importPreviewSchema).strict().parse(input);
|
|
208
|
+
// "./notes/a.md" is the same file as "notes/a.md".
|
|
209
|
+
const args = { ...parsed, files: parsed.files.map(file => ({ ...file, relativePath: file.relativePath.replace(/^(\.\/)+/, '') })) };
|
|
210
|
+
const label = folderLabel(args.label);
|
|
211
|
+
for (const prefix of [...(args.include ?? []), ...(args.exclude ?? [])]) {
|
|
212
|
+
if (prefix.split('/').includes('..'))
|
|
213
|
+
throw new Error('include/exclude prefixes are folder paths inside the import, like "Projects"; they cannot contain "..".');
|
|
214
|
+
}
|
|
215
|
+
let bytes = 0;
|
|
216
|
+
const paths = new Set();
|
|
217
|
+
for (const [i, file] of args.files.entries()) {
|
|
218
|
+
validatePath(file.relativePath, i);
|
|
219
|
+
if (paths.has(file.relativePath))
|
|
220
|
+
throw (0, toolRefusal_1.refusal)('duplicate_import_path', `files[${i}]: the same relativePath was sent twice; send each file once.`);
|
|
221
|
+
paths.add(file.relativePath);
|
|
222
|
+
const size = Buffer.byteLength(file.markdown, 'utf8');
|
|
223
|
+
bytes += size + Buffer.byteLength(file.relativePath, 'utf8');
|
|
224
|
+
if (size > exports.IMPORT_LIMITS.fileBytes)
|
|
225
|
+
throw (0, toolRefusal_1.refusal)('import_too_large', `files[${i}]: over 64 KiB. Split it into several smaller .md files (by heading) or leave it out.`);
|
|
226
|
+
if (bytes > exports.IMPORT_LIMITS.batchBytes)
|
|
227
|
+
throw (0, toolRefusal_1.refusal)('import_too_large', 'Import is over 128 KiB in total, paths included. Send fewer files per call: call preview_import again for each group; every call gets its own batchId and applies on its own.');
|
|
228
|
+
if (invalidUnicode(file.markdown))
|
|
229
|
+
throw (0, toolRefusal_1.refusal)('invalid_text', `files[${i}]: the text has invalid Unicode; send plain UTF-8 Markdown.`);
|
|
230
|
+
// A control character is refused as it is in a note or a statement (memory/controls.ts): the note is
|
|
231
|
+
// written into the vault and indexed, so what the preview admits the workspace holds afterwards.
|
|
232
|
+
try {
|
|
233
|
+
(0, controls_1.assertNoUnstorableControls)(`files[${i}] (${file.relativePath})`, file.markdown);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
throw (0, toolRefusal_1.refusal)('invalid_text', err instanceof Error ? err.message : String(err));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const skipped = [];
|
|
240
|
+
const notes = args.files.flatMap(file => {
|
|
241
|
+
if (file.relativePath.split('/').some(part => part.startsWith('.') || part === 'node_modules')) {
|
|
242
|
+
skipped.push({ relativePath: file.relativePath, reason: "hidden or node_modules path (a name starts with '.'); to import it, resend under a path without the leading '.' folder, e.g. memory/note.md" });
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
if (!(0, source_1.isSelected)(file.relativePath, { include: args.include ?? [], exclude: args.exclude ?? [] })) {
|
|
246
|
+
skipped.push({ relativePath: file.relativePath, reason: 'selection' });
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
if ((0, invisibleText_1.isBlankOrInvisible)(file.markdown)) {
|
|
250
|
+
skipped.push({ relativePath: file.relativePath, reason: 'empty note' });
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
return [(0, markdown_1.parseMarkdownImportNote)(file.relativePath, file.relativePath, file.markdown, new Date().toISOString())];
|
|
254
|
+
});
|
|
255
|
+
const bundle = {
|
|
256
|
+
format: types_1.BUNDLE_FORMAT, sourceLabel: label, notes, facts: [],
|
|
257
|
+
stats: { notesAvailable: args.files.length, notesSelected: notes.length, notesFilteredOut: skipped.length,
|
|
258
|
+
notesWithOriginalBytes: notes.length, chunksSelected: notes.reduce((sum, note) => sum + note.chunks.length, 0), factsAvailable: 0, factsSelected: 0 },
|
|
259
|
+
};
|
|
260
|
+
const plan = (0, plan_1.buildImportPlan)(bundle, { namespace: `Imported/${label}`, label, eventSource: 'import:markdown' });
|
|
261
|
+
const { conflicts, unchangedPaths, toWrite } = inspect(ctx, plan);
|
|
262
|
+
const report = { namespace: plan.namespace, counts: plan.counts, conflicts, unchanged: unchangedPaths.length, toWrite, skipped, limits: exports.IMPORT_LIMITS,
|
|
263
|
+
// RENOR_SECRET_GUARD on: the notes the plan left out because they look like a secret, by path and field, never text.
|
|
264
|
+
...(plan.skipped.secretGuard ? { secretGuard: plan.skipped.secretGuard } : {}) };
|
|
265
|
+
// A check holds nothing: a reviewer can check every batch of a large import before choosing, which the
|
|
266
|
+
// bounded staging below (8 previews, 15 minutes) could not hold, and the notes leave the process with the reply.
|
|
267
|
+
// It also names the notes already there unchanged, so a reviewer is not offered them as new.
|
|
268
|
+
if (args.stage === false)
|
|
269
|
+
return { ok: true, staged: false, ...report, unchangedPaths };
|
|
270
|
+
const previews = state(ctx.db).previews;
|
|
271
|
+
for (const [id, preview] of previews)
|
|
272
|
+
if (preview.expiresAt <= Date.now())
|
|
273
|
+
previews.delete(id);
|
|
274
|
+
if (previews.size >= 8)
|
|
275
|
+
throw (0, toolRefusal_1.refusal)('too_many_previews', 'Too many pending previews (8). Call apply_import for one of them, or wait up to 15 minutes for the rest to expire.');
|
|
276
|
+
const batchId = crypto_1.default.randomUUID();
|
|
277
|
+
const expiresAt = Date.now() + PREVIEW_MS;
|
|
278
|
+
previews.set(batchId, { bundle, plan, expiresAt });
|
|
279
|
+
return { ok: true, batchId, expiresAt: new Date(expiresAt).toISOString(), ...report };
|
|
280
|
+
}
|
|
281
|
+
function applyHostedImport(ctx, batchId, storageLimitBytes) {
|
|
282
|
+
return serial(ctx.db, async () => {
|
|
283
|
+
const existing = readJournal(ctx, batchId);
|
|
284
|
+
if (existing?.state === 'applied')
|
|
285
|
+
return { ok: true, batchId, ...existing.result };
|
|
286
|
+
if (existing?.state === 'undone')
|
|
287
|
+
throw (0, toolRefusal_1.refusal)('batch_already_applied', 'This batch was undone. Call preview_import again for a new batchId.');
|
|
288
|
+
if (existing)
|
|
289
|
+
throw (0, toolRefusal_1.refusal)('batch_already_applied', 'This batch did not finish applying and cannot be applied again. Call undo_import with this batchId, then preview_import again.');
|
|
290
|
+
const preview = state(ctx.db).previews.get(batchId);
|
|
291
|
+
if (!preview || preview.expiresAt <= Date.now())
|
|
292
|
+
throw (0, toolRefusal_1.refusal)('batch_not_found', 'Preview missing or expired; preview this batch again');
|
|
293
|
+
inspect(ctx, preview.plan);
|
|
294
|
+
const manifest = { batchId, state: 'applying', created: [], reasserted: [] };
|
|
295
|
+
writeJournal(ctx, manifest);
|
|
296
|
+
state(ctx.db).previews.delete(batchId);
|
|
297
|
+
// Each note is logged as it is journalled (journalLog.ts); the manifest is rewritten in full once, when the batch ends.
|
|
298
|
+
const log = new journalLog_1.JournalLog(journalLogPath(ctx, batchId));
|
|
299
|
+
try {
|
|
300
|
+
const result = await (0, apply_1.applyImportPlan)(preview.plan, preview.bundle, {
|
|
301
|
+
memoryDir: ctx.memoryDir, dbPath: ctx.db.name, ledgerRelPath: ctx.ledgerRelPath,
|
|
302
|
+
onNoteCreated(note) {
|
|
303
|
+
manifest.created.push({ relPath: note.relPath, hash: note.contentHash });
|
|
304
|
+
log.append({ list: 'created', relPath: note.relPath, hash: note.contentHash });
|
|
305
|
+
},
|
|
306
|
+
onNoteUnchanged(note) {
|
|
307
|
+
manifest.reasserted.push({ relPath: note.relPath, hash: note.contentHash });
|
|
308
|
+
log.append({ list: 'reasserted', relPath: note.relPath, hash: note.contentHash });
|
|
309
|
+
},
|
|
310
|
+
...(storageLimitBytes === undefined ? {} : { validateBeforeCommit: async () => {
|
|
311
|
+
const bytes = await (0, storedSize_1.measureMarkdownBytes)(ctx.memoryDir);
|
|
312
|
+
if (bytes > storageLimitBytes) {
|
|
313
|
+
throw new apply_1.ImportStorageLimitError(`Renor: this workspace would store ${bytes.toLocaleString('en-US')} bytes of Markdown; the storage limit is ${storageLimitBytes.toLocaleString('en-US')} bytes. This memory write was not run.`);
|
|
314
|
+
}
|
|
315
|
+
} }),
|
|
316
|
+
});
|
|
317
|
+
log.close();
|
|
318
|
+
manifest.result = result;
|
|
319
|
+
manifest.state = 'applied';
|
|
320
|
+
writeJournal(ctx, manifest);
|
|
321
|
+
return { ok: true, batchId, ...result, recall: ctx.indexOptions.embeddingProvider && result.indexRows.chunksInserted > 0
|
|
322
|
+
? 'Keyword recall is ready through search_text. The new notes are embedded in the background: search by meaning finds them once memory_status says semantic: true. No structured facts were generated.'
|
|
323
|
+
: 'Keyword recall is ready through search_text; no embeddings or structured facts were generated.' };
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
log.close();
|
|
327
|
+
manifest.state = 'failed';
|
|
328
|
+
writeJournal(ctx, manifest);
|
|
329
|
+
if (err instanceof apply_1.ImportStorageLimitError)
|
|
330
|
+
throw (0, toolRefusal_1.refusal)('storage_limit', err.message);
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// The owner changed what is on disk since the import, or the secret guard will not move the note (it stays live):
|
|
336
|
+
// keep it and carry on undoing the rest of the batch.
|
|
337
|
+
const UNDO_PRESERVES = new Set([
|
|
338
|
+
'imported note changed; preserved', 'memory path goes through a symbolic link', 'Unsafe import destination ancestor',
|
|
339
|
+
]);
|
|
340
|
+
/** Files can change independently of the journal. Inspect them without letting one bad path abort the batch. */
|
|
341
|
+
function inspectUndoTarget(ctx, relPath) {
|
|
342
|
+
try {
|
|
343
|
+
const target = safeTarget(ctx.memoryDir, relPath);
|
|
344
|
+
const stat = fs_1.default.lstatSync(target, { throwIfNoEntry: false });
|
|
345
|
+
if (!stat)
|
|
346
|
+
return { state: 'gone' };
|
|
347
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
348
|
+
return { state: 'preserved' };
|
|
349
|
+
const bytes = fs_1.default.readFileSync(target, 'utf8');
|
|
350
|
+
return { state: 'file', hash: crypto_1.default.createHash('sha256').update(bytes, 'utf8').digest('hex') };
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
if (err.code === 'ENOENT')
|
|
354
|
+
return { state: 'gone' };
|
|
355
|
+
return { state: 'preserved' };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Uses the existing journalled retirement, including FTS/vector removal. */
|
|
359
|
+
function undoHostedImport(ctx, batchId) {
|
|
360
|
+
return serial(ctx.db, async () => {
|
|
361
|
+
const manifest = readJournal(ctx, batchId);
|
|
362
|
+
if (!manifest)
|
|
363
|
+
throw (0, toolRefusal_1.refusal)('batch_not_found', 'No applied batch has this batchId: it was only previewed (never applied), or it belongs to another workspace. undo_import only reverses an applied batch.');
|
|
364
|
+
// A batch that crashed while applying keeps its notes in its log, not its manifest (journalLog.ts). They move into the
|
|
365
|
+
// manifest before anything is undone, and the batch is then a failed one whose manifest is whole. A batch with no log
|
|
366
|
+
// is left as it is: an older engine wrote its notes into the manifest itself.
|
|
367
|
+
if (manifest.state === 'applying') {
|
|
368
|
+
const logged = (0, journalLog_1.readJournalLog)(journalLogPath(ctx, batchId));
|
|
369
|
+
if (logged) {
|
|
370
|
+
manifest.created = logged.filter(entry => entry.list === 'created').map(({ relPath, hash }) => ({ relPath, hash }));
|
|
371
|
+
manifest.reasserted = logged.filter(entry => entry.list === 'reasserted').map(({ relPath, hash }) => ({ relPath, hash }));
|
|
372
|
+
manifest.state = 'failed';
|
|
373
|
+
writeJournal(ctx, manifest);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const preserved = [];
|
|
377
|
+
const preservedReasons = {};
|
|
378
|
+
let retired = 0;
|
|
379
|
+
const all = manifests(ctx);
|
|
380
|
+
for (const note of manifest.created) {
|
|
381
|
+
if (note.retired)
|
|
382
|
+
continue;
|
|
383
|
+
// A batch can find a created note unchanged only after its creator wrote it, so no wall-clock ordering field is
|
|
384
|
+
// needed (and clock rollback cannot reverse ownership).
|
|
385
|
+
const heldLater = all.some(other => other.batchId !== batchId && other.state === 'applied'
|
|
386
|
+
&& other.reasserted?.some(item => !item.retired && item.relPath === note.relPath && item.hash === note.hash));
|
|
387
|
+
if (heldLater) {
|
|
388
|
+
const target = inspectUndoTarget(ctx, note.relPath);
|
|
389
|
+
if (target.state === 'preserved') {
|
|
390
|
+
preserved.push(note.relPath);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (target.state === 'file' && target.hash === note.hash) {
|
|
394
|
+
preserved.push(note.relPath);
|
|
395
|
+
preservedReasons[note.relPath] = 'kept: re-asserted by a later import';
|
|
396
|
+
note.retired = true; // This creator released its assertion; the later batch now owns final retirement.
|
|
397
|
+
writeJournal(ctx, manifest);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (target.state === 'gone') {
|
|
401
|
+
note.retired = true;
|
|
402
|
+
writeJournal(ctx, manifest);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
let outcome;
|
|
407
|
+
try {
|
|
408
|
+
const target = inspectUndoTarget(ctx, note.relPath);
|
|
409
|
+
if (target.state === 'gone')
|
|
410
|
+
outcome = 'gone';
|
|
411
|
+
else if (target.state === 'preserved') {
|
|
412
|
+
preserved.push(note.relPath);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
await (0, memoryMap_1.retireMemory)(ctx, `note:${note.relPath}`, note.hash);
|
|
417
|
+
outcome = 'retired';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
if (err instanceof secretGuard_1.SecretRefusedError || (err instanceof Error && UNDO_PRESERVES.has(err.message))) {
|
|
422
|
+
preserved.push(note.relPath);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
throw err;
|
|
426
|
+
}
|
|
427
|
+
// Outside the try: a journal fault must abort the undo, never read as a preserved note.
|
|
428
|
+
note.retired = true;
|
|
429
|
+
if (outcome === 'retired')
|
|
430
|
+
retired++;
|
|
431
|
+
writeJournal(ctx, manifest);
|
|
432
|
+
}
|
|
433
|
+
for (const note of manifest.reasserted ?? []) {
|
|
434
|
+
if (note.retired)
|
|
435
|
+
continue;
|
|
436
|
+
const creatorUndone = all.some(other => other.batchId !== batchId && other.state === 'undone'
|
|
437
|
+
&& other.created.some(item => item.relPath === note.relPath && item.hash === note.hash));
|
|
438
|
+
const heldElsewhere = all.some(other => other.batchId !== batchId && other.state === 'applied'
|
|
439
|
+
&& other.reasserted?.some(item => !item.retired && item.relPath === note.relPath && item.hash === note.hash));
|
|
440
|
+
if (creatorUndone && !heldElsewhere) {
|
|
441
|
+
const target = inspectUndoTarget(ctx, note.relPath);
|
|
442
|
+
if (target.state === 'preserved') {
|
|
443
|
+
preserved.push(note.relPath);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
if (target.state === 'file') {
|
|
448
|
+
await (0, memoryMap_1.retireMemory)(ctx, `note:${note.relPath}`, note.hash);
|
|
449
|
+
retired++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
if (err instanceof secretGuard_1.SecretRefusedError || (err instanceof Error && UNDO_PRESERVES.has(err.message))) {
|
|
454
|
+
preserved.push(note.relPath);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
note.retired = true;
|
|
461
|
+
writeJournal(ctx, manifest);
|
|
462
|
+
}
|
|
463
|
+
manifest.state = 'undone';
|
|
464
|
+
writeJournal(ctx, manifest);
|
|
465
|
+
return { ok: true, batchId, retired, preserved, ...(Object.keys(preservedReasons).length ? { preservedReasons } : {}), alreadyUndone: retired === 0 && preserved.length === 0 };
|
|
466
|
+
});
|
|
467
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCarriedFact = exports.tryNormalizeScope = exports.toCanonicalInstant = exports.deterministicEventId = exports.renderNoteMarkdown = exports.destinationRelPath = exports.readExistingStreams = exports.applyImportPlan = exports.streamKey = exports.contentHash = exports.buildImportPlan = exports.toRelative = exports.isSelected = exports.readSource = exports.readRenorIndexSource = exports.readBundleSource = exports.readMarkdownSource = exports.BUNDLE_FORMAT = void 0;
|
|
4
|
+
var types_1 = require("./types");
|
|
5
|
+
Object.defineProperty(exports, "BUNDLE_FORMAT", { enumerable: true, get: function () { return types_1.BUNDLE_FORMAT; } });
|
|
6
|
+
var markdown_1 = require("./markdown");
|
|
7
|
+
Object.defineProperty(exports, "readMarkdownSource", { enumerable: true, get: function () { return markdown_1.readMarkdownSource; } });
|
|
8
|
+
var source_1 = require("./source");
|
|
9
|
+
Object.defineProperty(exports, "readBundleSource", { enumerable: true, get: function () { return source_1.readBundleSource; } });
|
|
10
|
+
Object.defineProperty(exports, "readRenorIndexSource", { enumerable: true, get: function () { return source_1.readRenorIndexSource; } });
|
|
11
|
+
Object.defineProperty(exports, "readSource", { enumerable: true, get: function () { return source_1.readSource; } });
|
|
12
|
+
Object.defineProperty(exports, "isSelected", { enumerable: true, get: function () { return source_1.isSelected; } });
|
|
13
|
+
Object.defineProperty(exports, "toRelative", { enumerable: true, get: function () { return source_1.toRelative; } });
|
|
14
|
+
var plan_1 = require("./plan");
|
|
15
|
+
Object.defineProperty(exports, "buildImportPlan", { enumerable: true, get: function () { return plan_1.buildImportPlan; } });
|
|
16
|
+
Object.defineProperty(exports, "contentHash", { enumerable: true, get: function () { return plan_1.contentHash; } });
|
|
17
|
+
Object.defineProperty(exports, "streamKey", { enumerable: true, get: function () { return plan_1.streamKey; } });
|
|
18
|
+
var apply_1 = require("./apply");
|
|
19
|
+
Object.defineProperty(exports, "applyImportPlan", { enumerable: true, get: function () { return apply_1.applyImportPlan; } });
|
|
20
|
+
var existing_1 = require("./existing");
|
|
21
|
+
Object.defineProperty(exports, "readExistingStreams", { enumerable: true, get: function () { return existing_1.readExistingStreams; } });
|
|
22
|
+
var serialize_1 = require("./serialize");
|
|
23
|
+
Object.defineProperty(exports, "destinationRelPath", { enumerable: true, get: function () { return serialize_1.destinationRelPath; } });
|
|
24
|
+
Object.defineProperty(exports, "renderNoteMarkdown", { enumerable: true, get: function () { return serialize_1.renderNoteMarkdown; } });
|
|
25
|
+
var facts_1 = require("./facts");
|
|
26
|
+
Object.defineProperty(exports, "deterministicEventId", { enumerable: true, get: function () { return facts_1.deterministicEventId; } });
|
|
27
|
+
Object.defineProperty(exports, "toCanonicalInstant", { enumerable: true, get: function () { return facts_1.toCanonicalInstant; } });
|
|
28
|
+
Object.defineProperty(exports, "tryNormalizeScope", { enumerable: true, get: function () { return facts_1.tryNormalizeScope; } });
|
|
29
|
+
Object.defineProperty(exports, "validateCarriedFact", { enumerable: true, get: function () { return facts_1.validateCarriedFact; } });
|