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,175 @@
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.withDailyFileLock = withDailyFileLock;
7
+ exports.entryClock = entryClock;
8
+ exports.appendDailyMemory = appendDailyMemory;
9
+ exports.appendDailyEntry = appendDailyEntry;
10
+ const path_1 = __importDefault(require("path"));
11
+ const limits_1 = require("./limits");
12
+ const invisibleText_1 = require("./invisibleText");
13
+ const dailyNote_1 = require("./dailyNote");
14
+ const sourceName_1 = require("./sourceName");
15
+ const secretGuard_1 = require("../extraction/secretGuard");
16
+ const toolRefusal_1 = require("../toolRefusal");
17
+ const controls_1 = require("./controls");
18
+ const confinedWrite_1 = require("./confinedWrite");
19
+ /**
20
+ * A note entry is the user's own words, so it is kept whole: nothing is trimmed, stripped or cut. What
21
+ * cannot be kept is refused instead, which makes a save that succeeds a save that stored every
22
+ * character, and a refused save one that wrote nothing.
23
+ */
24
+ function checkedNoteText(text) {
25
+ if (typeof text !== 'string')
26
+ throw (0, toolRefusal_1.refusal)('invalid_argument', 'the note must be a string');
27
+ if (text.length > limits_1.MAX_STATEMENT_LENGTH) {
28
+ throw (0, toolRefusal_1.refusal)('field_too_long', `the note exceeds the maximum length of ${limits_1.MAX_STATEMENT_LENGTH} (got ${text.length}); nothing was written`);
29
+ }
30
+ // An unpaired surrogate is not text: UTF-8 cannot hold it, so writing it would store U+FFFD instead.
31
+ if (/\p{Cs}/u.test(text)) {
32
+ throw (0, toolRefusal_1.refusal)('invalid_text', 'the note contains an unpaired UTF-16 surrogate, which cannot be stored as text; nothing was written');
33
+ }
34
+ // A control character is not text either, and the filesystem and SQLite both accept one, so a note
35
+ // holding NUL would be written, indexed and served as if it were ordinary words. Refused instead
36
+ // (memory/controls.ts), so a save that succeeds is a save that stored nothing but the words.
37
+ (0, controls_1.assertNoUnstorableControls)('the note', text);
38
+ if ((0, invisibleText_1.isBlankOrInvisible)(text))
39
+ throw (0, toolRefusal_1.refusal)('invalid_argument', 'the note is blank (only whitespace or invisible characters); nothing was written');
40
+ return text;
41
+ }
42
+ /**
43
+ * The forms of a source the secret guard judges: the one the header stores (headerSource: letters, digits and hyphens,
44
+ * the other characters written as hyphens) and the same name with every other character deleted, which is how the header
45
+ * stored it before. Spacing a key apart does not stop it being a key: the stored form joins the pieces with hyphens, and
46
+ * the deleted form joins them whole.
47
+ */
48
+ function sourceFormsForGuard(source) {
49
+ if (typeof source !== 'string')
50
+ return [];
51
+ return [(0, sourceName_1.headerSource)(source), (0, sourceName_1.cleanSource)(source).replace(/[^\p{L}\p{N}-]/gu, '')];
52
+ }
53
+ function isValidCalendarDate(date) {
54
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
55
+ if (!match)
56
+ return false;
57
+ const year = Number(match[1]);
58
+ const month = Number(match[2]);
59
+ const day = Number(match[3]);
60
+ const parsed = new Date(Date.UTC(year, month - 1, day));
61
+ return parsed.getUTCFullYear() === year
62
+ && parsed.getUTCMonth() === month - 1
63
+ && parsed.getUTCDate() === day;
64
+ }
65
+ /**
66
+ * In-process mutual exclusion per daily file. Every writer of a daily file (append here, and the
67
+ * memory map's retire/restore, which rewrites one) runs inside this, so a rewrite can never drop
68
+ * an append that landed between its read and its write. One tenant runs one engine process.
69
+ */
70
+ const dailyFileLocks = new Map();
71
+ function withDailyFileLock(filePath, fn) {
72
+ const key = path_1.default.resolve(filePath);
73
+ const prev = dailyFileLocks.get(key) ?? Promise.resolve();
74
+ const run = prev.then(fn, fn);
75
+ const settled = run.then(() => undefined, () => undefined);
76
+ dailyFileLocks.set(key, settled);
77
+ void settled.then(() => {
78
+ if (dailyFileLocks.get(key) === settled)
79
+ dailyFileLocks.delete(key);
80
+ });
81
+ return run;
82
+ }
83
+ /** A fixed offset as a client may give it: `-03:00`, `-0300`, or `Z`. */
84
+ const FIXED_OFFSET_RE = /^(?:Z|([+-])(\d{2}):?(\d{2}))$/;
85
+ const invalidTimeZone = (timeZone) => new Error(`timezone ${JSON.stringify(timeZone.slice(0, 64))} is not a time zone: give an IANA name such as "America/Sao_Paulo" or a UTC offset such as "-03:00"; nothing was written`);
86
+ /**
87
+ * The day and time to stamp an entry with, read once from `now`. In UTC unless the client names the user's time zone,
88
+ * an IANA name ("America/Sao_Paulo") or a fixed offset ("-03:00"): the entry is then filed under the user's own day at
89
+ * their own time, so a note said at 21:00 in São Paulo belongs to that evening, not to the next UTC day. A name that is
90
+ * neither is refused rather than read as UTC, so nothing is filed under a day the user did not mean.
91
+ */
92
+ function entryClock(now, timeZone) {
93
+ if (timeZone === undefined) {
94
+ const iso = now.toISOString();
95
+ return { date: iso.slice(0, 10), time: iso.slice(11, 19) };
96
+ }
97
+ let minutes;
98
+ const fixed = FIXED_OFFSET_RE.exec(timeZone.trim());
99
+ if (fixed) {
100
+ minutes = fixed[1] ? (fixed[1] === '-' ? -1 : 1) * (Number(fixed[2]) * 60 + Number(fixed[3])) : 0;
101
+ if (Number(fixed[3] ?? 0) > 59 || Math.abs(minutes) > 14 * 60)
102
+ throw invalidTimeZone(timeZone);
103
+ }
104
+ else {
105
+ let named;
106
+ try {
107
+ named = new Intl.DateTimeFormat('en-US', { timeZone: timeZone.trim(), timeZoneName: 'longOffset' })
108
+ .formatToParts(now).find(p => p.type === 'timeZoneName')?.value;
109
+ }
110
+ catch {
111
+ throw invalidTimeZone(timeZone);
112
+ }
113
+ // `GMT-03:00`, or `GMT` alone at UTC itself.
114
+ const m = named === undefined ? null : /^GMT(?:([+-])(\d{2}):(\d{2}))?$/.exec(named);
115
+ if (!m)
116
+ throw invalidTimeZone(timeZone);
117
+ minutes = m[1] ? (m[1] === '-' ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) : 0;
118
+ }
119
+ const iso = new Date(now.getTime() + minutes * 60_000).toISOString();
120
+ if (minutes === 0)
121
+ return { date: iso.slice(0, 10), time: iso.slice(11, 19) };
122
+ const abs = Math.abs(minutes);
123
+ const offset = `${minutes < 0 ? '-' : '+'}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
124
+ return { date: iso.slice(0, 10), time: iso.slice(11, 19), offset };
125
+ }
126
+ /**
127
+ * Appends one entry to the daily note of `date`. `clock` stamps it (its time, and its offset when not UTC); without
128
+ * one it is stamped with the current time in UTC. Returns the file's path.
129
+ */
130
+ async function appendDailyMemory(vaultPath, date, entry, memoryDir = 'Memory/Daily', clock) {
131
+ return (await appendDailyEntry(vaultPath, date, entry, memoryDir, clock)).filePath;
132
+ }
133
+ /**
134
+ * `appendDailyMemory`, saying what it wrote: the file, and the entry exactly as it went in (without the line break
135
+ * that separates it from the one above), which is what the memory map's id for it is made from.
136
+ */
137
+ async function appendDailyEntry(vaultPath, date, entry, memoryDir = 'Memory/Daily', clock) {
138
+ if (!isValidCalendarDate(date)) {
139
+ throw new Error('date must be a valid YYYY-MM-DD calendar date');
140
+ }
141
+ // The header must read back as an entry, or the entry would be folded into the one above it.
142
+ if (clock && (!/^\d{2}:\d{2}:\d{2}$/.test(clock.time) || (clock.offset !== undefined && !/^[+-]\d{2}:\d{2}$/.test(clock.offset)))) {
143
+ throw new Error('entry time must be HH:MM:SS, with an offset of the form +HH:MM; nothing was written');
144
+ }
145
+ const text = checkedNoteText(entry.text);
146
+ // RENOR_SECRET_GUARD (default ON): refuse a note that looks like it carries a secret before anything is
147
+ // written, the directory included. The source is scanned as it will be stored, and joined: removing characters
148
+ // can join the pieces of a key that were spaced apart (sourceFormsForGuard).
149
+ if ((0, secretGuard_1.secretGuardEnabled)()) {
150
+ await (0, secretGuard_1.assertStorable)([
151
+ { field: 'note text', text },
152
+ ...sourceFormsForGuard(entry.source).map(form => ({ field: 'note source', text: form })),
153
+ ]);
154
+ }
155
+ const dir = path_1.default.join(vaultPath, memoryDir);
156
+ const filePath = path_1.default.join(dir, `${date}.md`);
157
+ const resolvedPath = path_1.default.resolve(filePath);
158
+ const resolvedBase = path_1.default.resolve(dir);
159
+ if (!resolvedPath.startsWith(resolvedBase)) {
160
+ throw new Error('Invalid date: path traversal detected');
161
+ }
162
+ // The source's name, in the alphabet a header keeps: letters, digits and hyphens only (sourceName.ts)
163
+ const sanitizedSource = (0, sourceName_1.headerSource)(entry.source);
164
+ const time = clock?.time ?? new Date().toISOString().substring(11, 19);
165
+ // The user's words go in as said, except that a later line which would start like an entry is kept behind a
166
+ // backslash so it cannot forge one, and the entry's header then says so (see dailyNote.ts). Both are
167
+ // reversible, and every reader undoes them. A file outside the daily-note layout is written as said.
168
+ const written = (0, dailyNote_1.formatEntry)(time, sanitizedSource, text, (0, dailyNote_1.isDailyNotePath)(filePath), clock?.offset);
169
+ const line = `\n${written}`;
170
+ // Appended in place, or created with its frontmatter, through confinedWrite: a link, a hard link or a FIFO planted in the
171
+ // vault is refused, never followed, and a failed save leaves the note as it was (see appendConfined).
172
+ const relFile = path_1.default.relative(path_1.default.resolve(vaultPath), resolvedPath);
173
+ await withDailyFileLock(filePath, () => (0, confinedWrite_1.appendConfined)(vaultPath, relFile, line, (0, dailyNote_1.engineDailyHeader)(date)));
174
+ return { filePath, entry: written };
175
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UNNAMED_WRITER = void 0;
4
+ exports.writerOf = writerOf;
5
+ exports.displacementsAfter = displacementsAfter;
6
+ exports.hasOpenDisplacementBy = hasOpenDisplacementBy;
7
+ exports.writerConflictLine = writerConflictLine;
8
+ exports.writerConflictLines = writerConflictLines;
9
+ const statements_1 = require("../db/statements");
10
+ /**
11
+ * Writer conflicts: when a writer replaces a value another writer saved, both values stay visible, marked, until the
12
+ * writer that lost the value saves the same subject and predicate again.
13
+ *
14
+ * The writer of a fact is the identity the gateway (or the OAuth client) authenticated for the connection that saved it
15
+ * (`agentOf` in mvp/server.ts). It is never the `source` the caller typed, which says nothing a caller cannot choose. A
16
+ * fact with no writer recorded (written before this table, or by a path that names none) was saved by its source, so its
17
+ * writer is its source, or `unnamed` when it has none.
18
+ *
19
+ * Each fact a write leaves current carries the displacements still open on it, in `fact_writers` keyed by its event:
20
+ * - a save by the same writer keeps every open displacement (the other writer has not confirmed yet);
21
+ * - a save by another writer clears the displacements that writer owns, whichever value it saves: that is its
22
+ * confirmation. A save by a third writer clears nothing it does not own, so the other displacements stay open;
23
+ * - it opens a displacement for the value it replaces (the value, its writer and its date), unless that value is the
24
+ * one that displaced the writer's own displacement (it answered its counter-claim) or the two values are the same text.
25
+ * Replay derives the same rows from the ledger. A live index has no rows for facts saved before migration 26, so those
26
+ * have no displacements on it; a rebuild from the ledger derives rows for them, so a rebuilt index can show a line the
27
+ * live one does not. A fact with no row has no displacements.
28
+ */
29
+ /** The writer a write has when it has no authenticated identity and no source. */
30
+ exports.UNNAMED_WRITER = 'unnamed';
31
+ /** The writer a fact event recorded, and the displacements open on it. */
32
+ function writerOf(db, eventId, source) {
33
+ const row = eventId === null ? undefined : (0, statements_1.prepareOnce)(db, 'SELECT writer, displaced FROM fact_writers WHERE event_id = ?').get(eventId);
34
+ if (row)
35
+ return { writer: row.writer, displaced: JSON.parse(row.displaced) };
36
+ return { writer: source || exports.UNNAMED_WRITER, displaced: [] };
37
+ }
38
+ /** Two values agree when they are the same text once composed and trimmed. */
39
+ const sameValue = (a, b) => a.normalize('NFC').trim() === b.normalize('NFC').trim();
40
+ /**
41
+ * The displacements a save leaves open on the fact it creates: `write` is that save (its event, writer and value) and
42
+ * `closed` the fact it replaces, null when it replaces none.
43
+ */
44
+ function displacementsAfter(db, closed, write) {
45
+ if (closed === null)
46
+ return [];
47
+ const before = writerOf(db, closed.eventId, closed.source);
48
+ if (before.writer === write.writer)
49
+ return before.displaced;
50
+ const rest = before.displaced.filter(d => d.writer !== write.writer);
51
+ const confirms = before.displaced.some(d => d.writer === write.writer && d.byEvent === closed.eventId);
52
+ if (confirms || sameValue(closed.object, write.object))
53
+ return rest;
54
+ return [...rest, { writer: before.writer, object: closed.object, validFrom: closed.validFrom, byEvent: write.eventId }];
55
+ }
56
+ /** Whether `writer` has a displacement open on the fact `eventId`, which its next save of the subject and predicate confirms. */
57
+ function hasOpenDisplacementBy(db, eventId, writer) {
58
+ return writerOf(db, eventId, null).displaced.some(d => d.writer === writer);
59
+ }
60
+ /**
61
+ * One line a read shows beside a current value that replaced another writer's value: the values it displaced, each with
62
+ * its writer and date, and what clears them. Plain words, so an agent reads it as it reads the rest of the reply.
63
+ */
64
+ function writerConflictLine(open) {
65
+ const values = open.map(d => `${JSON.stringify(d.object)} by ${d.writer}, ${d.validFrom.slice(0, 10)}`).join('; ');
66
+ return `CONFLICT: ${values}. Replaced by a different writer; it stays a conflict until that writer saves this subject and predicate again.`;
67
+ }
68
+ /**
69
+ * The conflict line of each current fact (by event id) that has displacements open, read in one query. A fact with none
70
+ * has no entry, so a reply about it is what it was before.
71
+ */
72
+ function writerConflictLines(db, eventIds) {
73
+ const ids = [...new Set(eventIds.filter((id) => typeof id === 'string'))];
74
+ const lines = new Map();
75
+ if (ids.length === 0)
76
+ return lines;
77
+ const rows = (0, statements_1.prepareOnce)(db, 'SELECT event_id, displaced FROM fact_writers WHERE event_id IN (SELECT value FROM json_each(?))')
78
+ .all(JSON.stringify(ids));
79
+ for (const row of rows) {
80
+ const open = JSON.parse(row.displaced);
81
+ if (open.length > 0)
82
+ lines.set(row.event_id, writerConflictLine(open));
83
+ }
84
+ return lines;
85
+ }