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,96 @@
|
|
|
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.JournalLog = void 0;
|
|
7
|
+
exports.journalLogName = journalLogName;
|
|
8
|
+
exports.readJournalLog = readJournalLog;
|
|
9
|
+
exports.removeJournalLog = removeJournalLog;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const toolRefusal_1 = require("../toolRefusal");
|
|
13
|
+
/** The log's file name for a batch, beside its manifest. */
|
|
14
|
+
function journalLogName(batchId) {
|
|
15
|
+
return `${batchId}.jsonl`;
|
|
16
|
+
}
|
|
17
|
+
/** Opened on the first append, so a batch that writes no note creates no file. Each line is fsynced before the next. */
|
|
18
|
+
class JournalLog {
|
|
19
|
+
file;
|
|
20
|
+
fd = null;
|
|
21
|
+
constructor(file) {
|
|
22
|
+
this.file = file;
|
|
23
|
+
}
|
|
24
|
+
append(entry) {
|
|
25
|
+
if (this.fd === null)
|
|
26
|
+
this.fd = openLog(this.file);
|
|
27
|
+
const bytes = Buffer.from(`${JSON.stringify(entry)}\n`, 'utf8');
|
|
28
|
+
for (let done = 0; done < bytes.length;)
|
|
29
|
+
done += fs_1.default.writeSync(this.fd, bytes, done, bytes.length - done);
|
|
30
|
+
fs_1.default.fsyncSync(this.fd);
|
|
31
|
+
}
|
|
32
|
+
/** Idempotent. The bytes were fsynced when they were appended, so a failed close loses nothing. */
|
|
33
|
+
close() {
|
|
34
|
+
if (this.fd === null)
|
|
35
|
+
return;
|
|
36
|
+
const fd = this.fd;
|
|
37
|
+
this.fd = null;
|
|
38
|
+
try {
|
|
39
|
+
fs_1.default.closeSync(fd);
|
|
40
|
+
}
|
|
41
|
+
catch { /* every appended line is already on disk */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.JournalLog = JournalLog;
|
|
45
|
+
function openLog(file) {
|
|
46
|
+
const isNew = !fs_1.default.existsSync(file);
|
|
47
|
+
// O_NOFOLLOW: a log reached through a symbolic link is refused, as a manifest is.
|
|
48
|
+
const flags = fs_1.default.constants.O_WRONLY | fs_1.default.constants.O_APPEND | fs_1.default.constants.O_CREAT | fs_1.default.constants.O_NOFOLLOW;
|
|
49
|
+
const fd = fs_1.default.openSync(file, flags, 0o600);
|
|
50
|
+
if (isNew) {
|
|
51
|
+
// The new name is durable before its first line is acknowledged.
|
|
52
|
+
const dirFd = fs_1.default.openSync(path_1.default.dirname(file), 'r');
|
|
53
|
+
try {
|
|
54
|
+
fs_1.default.fsyncSync(dirFd);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs_1.default.closeSync(dirFd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return fd;
|
|
61
|
+
}
|
|
62
|
+
/** The log's entries in the order they were appended, or undefined when the batch has no log. */
|
|
63
|
+
function readJournalLog(file) {
|
|
64
|
+
const stat = fs_1.default.lstatSync(file, { throwIfNoEntry: false });
|
|
65
|
+
if (!stat)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (!stat.isFile())
|
|
68
|
+
throw (0, toolRefusal_1.refusal)('unsafe_path', 'Unsafe import journal');
|
|
69
|
+
const lines = fs_1.default.readFileSync(file, 'utf8').split('\n');
|
|
70
|
+
// The text after the last newline: empty when the log ends whole, otherwise the cut line a crash left.
|
|
71
|
+
lines.pop();
|
|
72
|
+
return lines.map((line, i) => parseEntry(line, i + 1));
|
|
73
|
+
}
|
|
74
|
+
function parseEntry(line, lineNo) {
|
|
75
|
+
let value;
|
|
76
|
+
try {
|
|
77
|
+
value = JSON.parse(line);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
value = undefined;
|
|
81
|
+
}
|
|
82
|
+
if (!value || (value.list !== 'created' && value.list !== 'reasserted') || typeof value.relPath !== 'string' || typeof value.hash !== 'string') {
|
|
83
|
+
throw new Error(`an import journal line (${lineNo}) cannot be read; undo_import stopped before changing anything`);
|
|
84
|
+
}
|
|
85
|
+
return { list: value.list, relPath: value.relPath, hash: value.hash };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Removes a log that no longer records an applying batch. Best effort: a finished batch's manifest holds the same notes,
|
|
89
|
+
* so a log that survives a crash is ignored by every reader of that batch.
|
|
90
|
+
*/
|
|
91
|
+
function removeJournalLog(file) {
|
|
92
|
+
try {
|
|
93
|
+
fs_1.default.unlinkSync(file);
|
|
94
|
+
}
|
|
95
|
+
catch { /* absent, or left for the next manifest rewrite to remove */ }
|
|
96
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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.readMarkdownSource = readMarkdownSource;
|
|
7
|
+
exports.parseMarkdownImportNote = parseMarkdownImportNote;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const walker_1 = require("../indexer/walker");
|
|
11
|
+
const parser_1 = require("../indexer/parser");
|
|
12
|
+
const chunker_1 = require("../indexer/chunker");
|
|
13
|
+
const source_1 = require("./source");
|
|
14
|
+
const controls_1 = require("../memory/controls");
|
|
15
|
+
const types_1 = require("./types");
|
|
16
|
+
/** Plain vault input: use the shipped walker/parser/chunker, without a source index. */
|
|
17
|
+
async function readMarkdownSource(folder, filter) {
|
|
18
|
+
const root = fs_1.default.realpathSync(folder);
|
|
19
|
+
if (!fs_1.default.statSync(root).isDirectory())
|
|
20
|
+
throw new Error('Markdown source must be a folder');
|
|
21
|
+
const nonUtf8Names = [];
|
|
22
|
+
const files = (await (0, walker_1.walkVault)(root, { onNonUtf8Markdown: name => nonUtf8Names.push(name) })).sort((a, b) => (0, source_1.toRelative)(root, a).localeCompare((0, source_1.toRelative)(root, b)));
|
|
23
|
+
const selected = files.filter(file => (0, source_1.isSelected)((0, source_1.toRelative)(root, file), filter));
|
|
24
|
+
const limited = filter.limit === undefined ? selected : selected.slice(0, filter.limit);
|
|
25
|
+
const notes = limited.map(file => {
|
|
26
|
+
const relPath = (0, source_1.toRelative)(root, file);
|
|
27
|
+
// Refuse a file replaced with a symlink after walking, and do not follow a
|
|
28
|
+
// substituted ancestor outside the chosen vault.
|
|
29
|
+
const resolved = fs_1.default.realpathSync(file);
|
|
30
|
+
const relative = path_1.default.relative(root, resolved);
|
|
31
|
+
if (relative === '..' || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative)) {
|
|
32
|
+
throw new Error(`Markdown source escapes the chosen folder: ${relPath}`);
|
|
33
|
+
}
|
|
34
|
+
const fd = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | fs_1.default.constants.O_NOFOLLOW);
|
|
35
|
+
try {
|
|
36
|
+
const stat = fs_1.default.fstatSync(fd);
|
|
37
|
+
if (!stat.isFile())
|
|
38
|
+
throw new Error(`Markdown source is not a regular file: ${relPath}`);
|
|
39
|
+
const bytes = fs_1.default.readFileSync(fd);
|
|
40
|
+
try {
|
|
41
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`Markdown source is not UTF-8: ${relPath}`);
|
|
45
|
+
}
|
|
46
|
+
const content = bytes.toString('utf8');
|
|
47
|
+
if (content.includes('\0'))
|
|
48
|
+
throw new Error(`Markdown source contains binary data: ${relPath}`);
|
|
49
|
+
return parseMarkdownImportNote(file, relPath, content, stat.mtime.toISOString());
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
fs_1.default.closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
format: types_1.BUNDLE_FORMAT, sourceLabel: path_1.default.basename(root), notes, facts: [],
|
|
57
|
+
// A name the selection excludes anyway is not reported; every other one could not be imported.
|
|
58
|
+
nonUtf8Names: nonUtf8Names.filter(name => (0, source_1.isSelected)(name, filter)).sort(),
|
|
59
|
+
stats: {
|
|
60
|
+
notesAvailable: files.length, notesSelected: notes.length,
|
|
61
|
+
// Everything the selection left out, the --limit cut included, as the other adapters count it.
|
|
62
|
+
notesFilteredOut: files.length - limited.length, notesWithOriginalBytes: notes.length,
|
|
63
|
+
chunksSelected: notes.reduce((sum, note) => sum + note.chunks.length, 0),
|
|
64
|
+
factsAvailable: 0, factsSelected: 0,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Shared adapter for local files and bounded hosted Markdown uploads. */
|
|
69
|
+
function parseMarkdownImportNote(sourcePath, relPath, content, modifiedAt) {
|
|
70
|
+
// A control character is refused as it is in a note or a statement (memory/controls.ts): the note is
|
|
71
|
+
// written into the vault and indexed, so what this funnel admits the workspace holds afterwards. One
|
|
72
|
+
// rule for a folder import and a hosted upload both.
|
|
73
|
+
(0, controls_1.assertNoUnstorableControls)(`imported note ${relPath}`, content);
|
|
74
|
+
// gray-matter ships a JavaScript engine that evaluates frontmatter. Remote
|
|
75
|
+
// Markdown is data: admit only default/YAML or JSON before calling the parser.
|
|
76
|
+
const opening = content.replace(/^\uFEFF/, '').match(/^---([^\r\n]*)/);
|
|
77
|
+
if (opening && !['', 'yaml', 'json'].includes(opening[1].trim())) {
|
|
78
|
+
throw new Error(`Unsupported Markdown frontmatter language: ${relPath}. Use YAML between --- lines, or remove the block.`);
|
|
79
|
+
}
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = (0, parser_1.parseNote)(sourcePath, content);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Do not expose private YAML text in an error.
|
|
86
|
+
throw new Error(`Cannot parse Markdown frontmatter: ${relPath}. Fix the YAML between the --- lines or remove that block, then send again.`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
sourcePath, relPath, title: parsed.title,
|
|
90
|
+
kind: typeof parsed.frontmatter.kind === 'string' ? parsed.frontmatter.kind : null,
|
|
91
|
+
modifiedAt, frontmatter: parsed.frontmatter, originalContent: content,
|
|
92
|
+
// Spans are body-relative; the renderer locates chunks in original bytes.
|
|
93
|
+
chunks: (0, chunker_1.chunkNote)(parsed).map(chunk => ({
|
|
94
|
+
headingPath: chunk.headingPath, text: chunk.text, tokenCount: chunk.tokenCount,
|
|
95
|
+
})),
|
|
96
|
+
entities: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IMPORT_ROOT = void 0;
|
|
4
|
+
exports.streamKey = streamKey;
|
|
5
|
+
exports.assertImportNamespace = assertImportNamespace;
|
|
6
|
+
exports.buildImportPlan = buildImportPlan;
|
|
7
|
+
exports.contentHash = contentHash;
|
|
8
|
+
const hasher_1 = require("../indexer/hasher");
|
|
9
|
+
const queryHelpers_1 = require("../retrieval/queryHelpers");
|
|
10
|
+
const orchestrator_1 = require("../retrieval/orchestrator");
|
|
11
|
+
const facts_1 = require("./facts");
|
|
12
|
+
const scope_1 = require("../mvp/scope");
|
|
13
|
+
const serialize_1 = require("./serialize");
|
|
14
|
+
const secretGuard_1 = require("../extraction/secretGuard");
|
|
15
|
+
const secretScreen_1 = require("./secretScreen");
|
|
16
|
+
const sourceName_1 = require("../memory/sourceName");
|
|
17
|
+
/**
|
|
18
|
+
* The ledger source of a carried fact. A bundle file can name any source, and so can the `--event-source` flag, so without
|
|
19
|
+
* this an imported fact could read as the agent that saved it (its author is what get_context shows and marks); an import
|
|
20
|
+
* always says it is one.
|
|
21
|
+
*/
|
|
22
|
+
const importedLedgerSource = (carried) => ((0, sourceName_1.isImportSource)(carried) ? carried : `${sourceName_1.IMPORT_SOURCE_PREFIX}${carried}`);
|
|
23
|
+
/**
|
|
24
|
+
* Normalized stream identity — the SAME rule the lifecycle uses to decide a stream.
|
|
25
|
+
*
|
|
26
|
+
* `identityKey` applies the lifecycle's Unicode subject fold (NFC + case, explicit
|
|
27
|
+
* aliases unified) so a stream the lifecycle considers one subject is skipped rather
|
|
28
|
+
* than carried — carrying it either failed the whole import (older value, replay
|
|
29
|
+
* monotonicity) or silently superseded the destination's owner-stated value (newer
|
|
30
|
+
* value). The predicate is kept **exact**, matching `currentFact`'s `predicate = ?`:
|
|
31
|
+
* lowercasing it here collapsed `Primary Goal` and `primary goal`, two streams the
|
|
32
|
+
* lifecycle holds separately, so a real stream was silently dropped. The scope is
|
|
33
|
+
* normalized exactly as the ledger/replay normalizes it, so raw variants cannot
|
|
34
|
+
* collide later, and keyed by its slug: `scope:x`, `project:x` and `x` are one
|
|
35
|
+
* stream, whose current value a write closes whatever spelling it was saved
|
|
36
|
+
* with (scopeSpellings). Nothing here rewrites the bytes that are emitted or hashed.
|
|
37
|
+
*/
|
|
38
|
+
function streamKey(subject, predicate, scope) {
|
|
39
|
+
const normalized = (0, facts_1.tryNormalizeScope)(scope);
|
|
40
|
+
const key = normalized ? (0, scope_1.scopeSpellings)(normalized)[0] : scope.trim().toLowerCase();
|
|
41
|
+
return `${(0, queryHelpers_1.identityKey)(subject)}\u0000${predicate}\u0000${key}`;
|
|
42
|
+
}
|
|
43
|
+
/** Every import writes under this folder: a read marks a memory there as imported (mvp/agentMemory.ts, IMPORTED_PATH). */
|
|
44
|
+
exports.IMPORT_ROOT = 'Imported';
|
|
45
|
+
/**
|
|
46
|
+
* Refuses a destination outside Imported/. The import mark is read off a note's path, so an import written elsewhere read
|
|
47
|
+
* as the workspace's own notes, and one written where the engine keeps its daily log (`Memory`, or the root) turned an
|
|
48
|
+
* imported file's entry lines into saves by whatever source the file named.
|
|
49
|
+
*/
|
|
50
|
+
function assertImportNamespace(namespace) {
|
|
51
|
+
if ((0, serialize_1.destinationRelPath)(namespace, 'x').split('/')[0] !== exports.IMPORT_ROOT) {
|
|
52
|
+
throw new Error(`the import destination must be under ${exports.IMPORT_ROOT}/ (got ${JSON.stringify(namespace)}): a read marks an imported memory by that folder`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function buildImportPlan(bundle, options) {
|
|
56
|
+
assertImportNamespace(options.namespace);
|
|
57
|
+
const existingStreams = options.existingStreams ?? new Set();
|
|
58
|
+
const destinationNow = options.now ?? new Date().toISOString();
|
|
59
|
+
const notes = [];
|
|
60
|
+
const seenPaths = new Set();
|
|
61
|
+
let duplicateNotePath = 0;
|
|
62
|
+
let emptyNoteBody = 0;
|
|
63
|
+
let notesFromOriginalBytes = 0;
|
|
64
|
+
for (const note of bundle.notes) {
|
|
65
|
+
const relPath = (0, serialize_1.destinationRelPath)(options.namespace, note.relPath);
|
|
66
|
+
if (seenPaths.has(relPath)) {
|
|
67
|
+
duplicateNotePath += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
seenPaths.add(relPath);
|
|
71
|
+
const rendered = (0, serialize_1.renderNoteMarkdown)(note);
|
|
72
|
+
if (!rendered.content.trim()) {
|
|
73
|
+
emptyNoteBody += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (rendered.fromOriginalBytes)
|
|
77
|
+
notesFromOriginalBytes += 1;
|
|
78
|
+
notes.push({
|
|
79
|
+
relPath,
|
|
80
|
+
content: rendered.content,
|
|
81
|
+
contentHash: (0, hasher_1.hashContent)(rendered.content),
|
|
82
|
+
fromOriginalBytes: rendered.fromOriginalBytes,
|
|
83
|
+
chunkLines: rendered.chunkLines,
|
|
84
|
+
chunksNotLocated: rendered.chunksNotLocated,
|
|
85
|
+
chunksSpanInherited: rendered.chunksSpanInherited,
|
|
86
|
+
note,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const skipEvents = new Map();
|
|
90
|
+
const bump = (reason) => skipEvents.set(reason, (skipEvents.get(reason) ?? 0) + 1);
|
|
91
|
+
const byStream = new Map();
|
|
92
|
+
for (const fact of bundle.facts) {
|
|
93
|
+
const key = streamKey(fact.subject, fact.predicate, fact.scope);
|
|
94
|
+
const list = byStream.get(key);
|
|
95
|
+
if (list)
|
|
96
|
+
list.push(fact);
|
|
97
|
+
else
|
|
98
|
+
byStream.set(key, [fact]);
|
|
99
|
+
}
|
|
100
|
+
let multiValuedFactsDropped = 0;
|
|
101
|
+
const events = [];
|
|
102
|
+
const emittedStreams = new Set();
|
|
103
|
+
for (const [key, facts] of byStream) {
|
|
104
|
+
// The lifecycle layer holds at most ONE current value per (subject, predicate, scope).
|
|
105
|
+
// The engine deliberately allows note-derived facts to be multi-valued, so a stream
|
|
106
|
+
// with several distinct current values cannot become one current fact. Picking a
|
|
107
|
+
// winner would misrepresent which value is current — and row order is not a
|
|
108
|
+
// decision — so the whole stream is reported and dropped.
|
|
109
|
+
const distinctObjects = new Set(facts.map(f => f.object));
|
|
110
|
+
if (distinctObjects.size > 1) {
|
|
111
|
+
multiValuedFactsDropped += facts.length;
|
|
112
|
+
bump('multi-valued stream: the lifecycle holds one current value per subject+predicate');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (existingStreams.has(key)) {
|
|
116
|
+
bump('destination already represents this subject+predicate');
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (emittedStreams.has(key)) {
|
|
120
|
+
bump('duplicate stream inside the bundle');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const fact = facts[0];
|
|
124
|
+
const validation = (0, facts_1.validateCarriedFact)({
|
|
125
|
+
subject: fact.subject,
|
|
126
|
+
predicate: fact.predicate,
|
|
127
|
+
object: fact.object,
|
|
128
|
+
scope: fact.scope,
|
|
129
|
+
validFrom: fact.validFrom,
|
|
130
|
+
});
|
|
131
|
+
if (!validation.ok || !validation.scope || !validation.at) {
|
|
132
|
+
bump(validation.reason ?? 'invalid fact');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Bound the bundle's claimed time by the destination's clock BEFORE the id is derived, so the planned event, the
|
|
136
|
+
// stored valid_from and every read of it are bounded. Same rule and tolerance as every other content-derived clock.
|
|
137
|
+
const at = (0, orchestrator_1.clampSessionDate)(validation.at, destinationNow) ?? destinationNow;
|
|
138
|
+
// The event id is derived from every field replay compares, so a re-run against an
|
|
139
|
+
// unchanged source re-appends nothing and a changed source yields a new event
|
|
140
|
+
// instead of mutating one that replay already recorded.
|
|
141
|
+
const id = (0, facts_1.deterministicEventId)({
|
|
142
|
+
label: options.label,
|
|
143
|
+
subject: fact.subject,
|
|
144
|
+
predicate: fact.predicate,
|
|
145
|
+
scope: validation.scope,
|
|
146
|
+
object: fact.object,
|
|
147
|
+
at,
|
|
148
|
+
});
|
|
149
|
+
events.push({
|
|
150
|
+
id,
|
|
151
|
+
at,
|
|
152
|
+
kind: 'store',
|
|
153
|
+
subject: fact.subject,
|
|
154
|
+
predicate: fact.predicate,
|
|
155
|
+
object: fact.object,
|
|
156
|
+
scope: validation.scope,
|
|
157
|
+
source: importedLedgerSource(fact.ledgerSource ?? options.eventSource),
|
|
158
|
+
});
|
|
159
|
+
emittedStreams.add(key);
|
|
160
|
+
}
|
|
161
|
+
// RENOR_SECRET_GUARD (default ON): a note or fact that looks like a secret is left out of the plan, with the
|
|
162
|
+
// reason, so the dry run shows it and every other note is still imported. With the guard off none of this runs.
|
|
163
|
+
let guardSkips = null;
|
|
164
|
+
if ((0, secretGuard_1.secretGuardEnabled)()) {
|
|
165
|
+
const screened = (0, secretScreen_1.leaveOutFlagged)(notes, events);
|
|
166
|
+
// In place and without spreading: a bundle can hold more notes than a call can take as arguments.
|
|
167
|
+
notes.length = 0;
|
|
168
|
+
for (const kept of screened.notes)
|
|
169
|
+
notes.push(kept);
|
|
170
|
+
events.length = 0;
|
|
171
|
+
for (const kept of screened.events)
|
|
172
|
+
events.push(kept);
|
|
173
|
+
notesFromOriginalBytes = notes.filter(n => n.fromOriginalBytes).length;
|
|
174
|
+
guardSkips = screened.skips;
|
|
175
|
+
}
|
|
176
|
+
// Deterministic append order, so two runs produce an identical ledger.
|
|
177
|
+
events.sort((a, b) => streamKey(a.subject, a.predicate, a.scope).localeCompare(streamKey(b.subject, b.predicate, b.scope)));
|
|
178
|
+
const skippedFacts = [...skipEvents.entries()]
|
|
179
|
+
.map(([reason, count]) => ({ reason, count }))
|
|
180
|
+
.sort((a, b) => b.count - a.count);
|
|
181
|
+
return {
|
|
182
|
+
sourceLabel: bundle.sourceLabel,
|
|
183
|
+
namespace: options.namespace,
|
|
184
|
+
notes,
|
|
185
|
+
events,
|
|
186
|
+
skipped: { duplicateNotePath, emptyNoteBody, events: skippedFacts, ...(guardSkips ? { secretGuard: guardSkips } : {}) },
|
|
187
|
+
counts: {
|
|
188
|
+
notesInBundle: bundle.notes.length,
|
|
189
|
+
chunksInBundle: bundle.stats.chunksSelected,
|
|
190
|
+
factsInBundle: bundle.facts.length,
|
|
191
|
+
selectedNotes: notes.length,
|
|
192
|
+
selectedChunks: notes.reduce((sum, n) => sum + n.note.chunks.length, 0),
|
|
193
|
+
notesFromOriginalBytes,
|
|
194
|
+
notesReconstructed: notes.length - notesFromOriginalBytes,
|
|
195
|
+
chunksNotLocated: notes.reduce((sum, n) => sum + n.chunksNotLocated, 0),
|
|
196
|
+
chunksSpanInherited: notes.reduce((sum, n) => sum + n.chunksSpanInherited, 0),
|
|
197
|
+
eventsToAppend: events.length,
|
|
198
|
+
multiValuedFactsDropped,
|
|
199
|
+
factsSkipped: skippedFacts.reduce((sum, s) => sum + s.count, 0),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** The engine's own note change-detection hash, re-exported for callers. */
|
|
204
|
+
function contentHash(content) {
|
|
205
|
+
return (0, hasher_1.hashContent)(content);
|
|
206
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ImportRefusedError = void 0;
|
|
4
|
+
exports.screenNote = screenNote;
|
|
5
|
+
exports.screenEvent = screenEvent;
|
|
6
|
+
exports.screenImportPlan = screenImportPlan;
|
|
7
|
+
exports.refuseFlaggedPlan = refuseFlaggedPlan;
|
|
8
|
+
exports.leaveOutFlagged = leaveOutFlagged;
|
|
9
|
+
const secretGuard_1 = require("../extraction/secretGuard");
|
|
10
|
+
/** Every string `applyImportPlan` would store for this note. */
|
|
11
|
+
function noteTexts(planned) {
|
|
12
|
+
const note = planned.note;
|
|
13
|
+
const texts = [
|
|
14
|
+
{ field: 'path', text: planned.relPath },
|
|
15
|
+
{ field: 'content', text: planned.content },
|
|
16
|
+
{ field: 'title', text: note.title },
|
|
17
|
+
{ field: 'kind', text: note.kind ?? '' },
|
|
18
|
+
{ field: 'frontmatter', text: JSON.stringify(note.frontmatter ?? {}) },
|
|
19
|
+
];
|
|
20
|
+
for (const chunk of note.chunks) {
|
|
21
|
+
texts.push({ field: 'chunk', text: chunk.text }, { field: 'chunk heading', text: chunk.headingPath });
|
|
22
|
+
}
|
|
23
|
+
for (const entity of note.entities) {
|
|
24
|
+
texts.push({ field: 'entity', text: entity.canonicalName }, { field: 'entity', text: entity.type });
|
|
25
|
+
for (const alias of entity.aliases ?? [])
|
|
26
|
+
texts.push({ field: 'entity', text: alias });
|
|
27
|
+
}
|
|
28
|
+
return texts;
|
|
29
|
+
}
|
|
30
|
+
/** Every string `applyImportPlan` would append to the ledger for this fact. */
|
|
31
|
+
function eventTexts(event) {
|
|
32
|
+
return [
|
|
33
|
+
{ field: 'subject, predicate and object', text: `${event.subject} ${event.predicate}: ${event.object}` },
|
|
34
|
+
{ field: 'source', text: event.source },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function screenNote(planned) {
|
|
38
|
+
const hit = (0, secretGuard_1.findSensitive)(noteTexts(planned));
|
|
39
|
+
if (hit === null)
|
|
40
|
+
return null;
|
|
41
|
+
// The path was scanned first: if it is what was flagged, it must not be printed.
|
|
42
|
+
return { ref: hit.field === 'path' ? null : planned.relPath, field: hit.field, reason: hit.reason };
|
|
43
|
+
}
|
|
44
|
+
function screenEvent(event) {
|
|
45
|
+
const hit = (0, secretGuard_1.findSensitive)(eventTexts(event));
|
|
46
|
+
return hit === null ? null : { ref: event.id, field: hit.field, reason: hit.reason };
|
|
47
|
+
}
|
|
48
|
+
/** What a plan still holds that the guard would refuse. Pure: it changes nothing. */
|
|
49
|
+
function screenImportPlan(plan) {
|
|
50
|
+
const notes = [];
|
|
51
|
+
for (const planned of plan.notes) {
|
|
52
|
+
const skip = screenNote(planned);
|
|
53
|
+
if (skip)
|
|
54
|
+
notes.push(skip);
|
|
55
|
+
}
|
|
56
|
+
const events = [];
|
|
57
|
+
for (const event of plan.events) {
|
|
58
|
+
const skip = screenEvent(event);
|
|
59
|
+
if (skip)
|
|
60
|
+
events.push(skip);
|
|
61
|
+
}
|
|
62
|
+
return { notes, events };
|
|
63
|
+
}
|
|
64
|
+
const describeSkip = (skip) => `${skip.ref ?? '(path withheld)'} (${skip.field}: ${skip.reason})`;
|
|
65
|
+
/** The apply-time refusal. Counts and names only: the message carries no text from the plan. */
|
|
66
|
+
class ImportRefusedError extends Error {
|
|
67
|
+
skips;
|
|
68
|
+
constructor(skips) {
|
|
69
|
+
const all = [...skips.notes, ...skips.events];
|
|
70
|
+
const shown = all.slice(0, 5).map(describeSkip).join('; ');
|
|
71
|
+
super(`import refused: ${skips.notes.length} note(s) and ${skips.events.length} fact(s) in this plan appear to contain a credential `
|
|
72
|
+
+ 'or personal identifier, and the secret guard (RENOR_SECRET_GUARD) is on, so nothing was written. '
|
|
73
|
+
+ 'Build the plan again (it leaves them out) or switch the guard off. '
|
|
74
|
+
+ `First: ${shown}${all.length > 5 ? `; and ${all.length - 5} more` : ''}`);
|
|
75
|
+
this.name = 'ImportRefusedError';
|
|
76
|
+
this.skips = skips;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.ImportRefusedError = ImportRefusedError;
|
|
80
|
+
/** Throws ImportRefusedError, and writes nothing, when the plan still holds anything the guard would refuse. */
|
|
81
|
+
function refuseFlaggedPlan(plan) {
|
|
82
|
+
const skips = screenImportPlan(plan);
|
|
83
|
+
if (skips.notes.length === 0 && skips.events.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
for (const skip of [...skips.notes, ...skips.events])
|
|
86
|
+
(0, secretGuard_1.logSkip)(skip.field, skip.reason);
|
|
87
|
+
throw new ImportRefusedError(skips);
|
|
88
|
+
}
|
|
89
|
+
/** The plan-time half: split `notes` and `events` into what stays and what is left out, in order. */
|
|
90
|
+
function leaveOutFlagged(notes, events) {
|
|
91
|
+
const skips = { notes: [], events: [] };
|
|
92
|
+
const keptNotes = [];
|
|
93
|
+
for (const planned of notes) {
|
|
94
|
+
const skip = screenNote(planned);
|
|
95
|
+
if (skip) {
|
|
96
|
+
skips.notes.push(skip);
|
|
97
|
+
(0, secretGuard_1.logSkip)(skip.field, skip.reason);
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
keptNotes.push(planned);
|
|
101
|
+
}
|
|
102
|
+
const keptEvents = [];
|
|
103
|
+
for (const event of events) {
|
|
104
|
+
const skip = screenEvent(event);
|
|
105
|
+
if (skip) {
|
|
106
|
+
skips.events.push(skip);
|
|
107
|
+
(0, secretGuard_1.logSkip)(skip.field, skip.reason);
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
keptEvents.push(event);
|
|
111
|
+
}
|
|
112
|
+
return { notes: keptNotes, events: keptEvents, skips };
|
|
113
|
+
}
|