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,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExtractorParseError = exports.MAX_EXTRACT_ATTEMPTS = exports.FACTS_EXTRACTION_VERSION = exports.FACTS_MODEL = void 0;
4
+ exports.repairMalformedUnicodeEscapes = repairMalformedUnicodeEscapes;
5
+ exports.stripExtractorCodeFence = stripExtractorCodeFence;
6
+ exports.typedFactsEnabled = typedFactsEnabled;
7
+ exports.contentDateFromFrontmatter = contentDateFromFrontmatter;
8
+ exports.extractNumericFacts = extractNumericFacts;
9
+ exports.extractNumericFactsWithRetry = extractNumericFactsWithRetry;
10
+ /**
11
+ * C2 typed numeric-fact extraction (HYP-20260921-facts-product-join, ADR-0003
12
+ * §4 payload schema for the fact lane). v1 schema only — value, unit, verb,
13
+ * object, event — frozen prompt + model. Ledger KILLs bound this module:
14
+ * N12/N14/N16b/N20/N25 (extractor-prompt variants are a structural ceiling —
15
+ * no v2/v3/v4 prompt iteration) and H5 (one-pass multi-head — 48% row loss,
16
+ * so this extracts facts only). Parse failures are EXPLICIT
17
+ * extractor-parse-errors, retried ≤3 times, never silently `[]`, and never
18
+ * cached (pipeline resets the note hash so the next pass retries).
19
+ */
20
+ exports.FACTS_MODEL = 'gemma4:31b';
21
+ /** extraction_version = schema:model (ADR §4): hash-keyed cache key component. */
22
+ exports.FACTS_EXTRACTION_VERSION = `facts-v1:${exports.FACTS_MODEL}`;
23
+ /** ≤3 attempts total on parse failure; a final failure is thrown, never swallowed. */
24
+ exports.MAX_EXTRACT_ATTEMPTS = 3;
25
+ class ExtractorParseError extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'ExtractorParseError';
29
+ }
30
+ }
31
+ exports.ExtractorParseError = ExtractorParseError;
32
+ /**
33
+ * Strip a leading/trailing markdown code fence from an LLM extraction response
34
+ * before JSON.parse. The frozen gemma4:31b extractor emits ```json fences on a
35
+ * fraction of responses despite strict-JSON prompt instructions (measured:
36
+ * ~10–25% of notes fail as raw JSON on real LongMemEval session corpora).
37
+ * JSON-strictness is preserved: everything that is not a fenced or bare JSON
38
+ * object still fails hard as an explicit ExtractorParseError.
39
+ */
40
+ /**
41
+ * Repair a specific, measured malformation in gemma4:31b's extraction JSON: the
42
+ * model renders a mixed-number literal (e.g. "1 1/2" as a unicode fraction) as a
43
+ * TRUNCATED `\uXXXX` escape — `"1 \u00frac{1}{2}"`, i.e. `\u00f` followed by
44
+ * `rac`. `\u00f` is not a valid escape because `r` is not a hex digit, so
45
+ * JSON.parse throws "Bad Unicode escape".
46
+ *
47
+ * Measured on the failing LongMemEval note `09b651aa_3` (a long recipe session
48
+ * with several mixed-number amounts): 5 of 6 identical temperature-0 calls
49
+ * returned this malformation with finish_reason=stop and a well-formed tail —
50
+ * it is NOT truncation and NOT a transient error, and the existing 3-attempt
51
+ * retry therefore fails ~58% of the time on such a note. One unrecovered note
52
+ * aborts the whole product run (the index precondition is fail-closed), which is
53
+ * why structured-rows arms died mid-flight.
54
+ *
55
+ * The repair is deliberately minimal and lossless for every other input: it
56
+ * rewrites only a `\u` escape whose four hex digits are NOT all hex. The intent
57
+ * is unambiguous (the model tried to emit a hex escape and ran past it), so the
58
+ * malformed escape is replaced with the replacement character rather than being
59
+ * silently dropped or guessed. Valid escapes, including astral pairs, are
60
+ * untouched, so this cannot alter a well-formed response.
61
+ *
62
+ * It repairs EVERY malformed escape in the response, and that is the whole point
63
+ * of the global flag below. The measured note had SEVERAL mixed-number amounts
64
+ * in one response, and a single response can carry several of these escapes; a
65
+ * non-global replace left all but the first in place, so JSON.parse still threw
66
+ * "Bad Unicode escape" and the note still aborted the run — the repair appeared
67
+ * to work on the one-escape fixture in the test while the real input kept
68
+ * failing. Found by the independent review of PR #156.
69
+ *
70
+ * This is a TRANSPORT repair, not a parse-strictness relaxation: after it runs,
71
+ * a response that is not valid JSON still fails hard as ExtractorParseError.
72
+ */
73
+ function repairMalformedUnicodeEscapes(content) {
74
+ // A malformed escape is a backslash that INTRODUCES an escape — i.e. is not
75
+ // itself escaped — followed by `u`, at most three hex digits, then a non-hex
76
+ // character. Two things this must leave alone, and the second one is why the
77
+ // run of backslashes is examined rather than just the last two characters:
78
+ //
79
+ // - a VALID `\uXXXX` escape: at four hex digits the lookahead can never
80
+ // match, so it is skipped;
81
+ // - a LITERAL `u` after an ESCAPED backslash, e.g. the Windows path
82
+ // `"C:\\user"`. `\\` is ONE escaped backslash and the `u` is content, so
83
+ // the run of backslashes before that `u` is even; only an ODD run ends on
84
+ // an escape introducer. Matching the final two characters regardless
85
+ // rewrote the second backslash of the pair and corrupted real content:
86
+ // `C:\user` became `C:\ufffdser`. Silently changing valid user text is a
87
+ // worse failure than the malformation this repair exists to fix, and the
88
+ // independent review of PR #156 caught it.
89
+ return content.replace(/(\\+)u([0-9a-fA-F]{0,3})(?![0-9a-fA-F])/g, (match, backslashes, hex) => {
90
+ // Only an ODD run of backslashes ends on an escape introducer; an even run
91
+ // is entirely escaped backslashes, so the `u` after it is ordinary content
92
+ // and the match is left exactly as it was.
93
+ if (backslashes.length % 2 === 0)
94
+ return match;
95
+ const introducer = backslashes.slice(0, -1);
96
+ if (hex.length === 0) {
97
+ // TWO SHAPES, TWO REPAIRS. With NO hex digit the model emitted a bare
98
+ // backslash before an ordinary character (a Windows path, a regex, a
99
+ // price like `\u`), and nothing was consumed as escape payload, so the
100
+ // text is recoverable exactly: escape the backslash. Substituting
101
+ // U+FFFD here DELETES the character that followed it, which is how
102
+ // `C:\user` became `C:\ufffdser`.
103
+ return `${introducer}\\\\u`;
104
+ }
105
+ // One to three hex digits: the model tried to emit a `\uXXXX` escape and
106
+ // ran past it (the measured gemma4:31b truncated-fraction malformation).
107
+ // Those characters really were escape payload, so the unambiguous repair
108
+ // is the replacement character.
109
+ return `${introducer}\\ufffd`;
110
+ });
111
+ }
112
+ function stripExtractorCodeFence(content) {
113
+ const s = content.trim();
114
+ const m = /^```[a-zA-Z]*\s*\n([\s\S]*?)\n?```$/.exec(s);
115
+ return m ? m[1].trim() : s;
116
+ }
117
+ /** RENOR_TYPED_FACTS (default OFF). Opt in with 1/true/yes/on. No default flip. */
118
+ function typedFactsEnabled(env = process.env) {
119
+ const raw = (env.RENOR_TYPED_FACTS ?? '').trim().toLowerCase();
120
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
121
+ }
122
+ /**
123
+ * Content date for valid_from (ADR §4): frontmatter `session_date`, else
124
+ * `date`, when parsable. NULL = unknown, never `now()`, never invented —
125
+ * an unparsable date string stays NULL (unknown must remain distinguishable).
126
+ * gray-matter parses bare YAML dates into JS Date objects; dates normalize to
127
+ * day precision (content dates are dates, not timestamps).
128
+ */
129
+ function contentDateFromFrontmatter(frontmatter) {
130
+ for (const key of ['session_date', 'date']) {
131
+ const raw = frontmatter[key];
132
+ if (raw === undefined || raw === null)
133
+ continue;
134
+ if (raw instanceof Date) {
135
+ if (Number.isNaN(raw.getTime()))
136
+ continue;
137
+ return raw.toISOString().slice(0, 10);
138
+ }
139
+ const s = String(raw).trim();
140
+ if (!s)
141
+ continue;
142
+ const isoDay = s.match(/^(\d{4}-\d{2}-\d{2})/);
143
+ if (isoDay)
144
+ return isoDay[1];
145
+ const t = Date.parse(s);
146
+ if (!Number.isNaN(t))
147
+ return new Date(t).toISOString().slice(0, 10);
148
+ }
149
+ return null;
150
+ }
151
+ const SYSTEM_PROMPT = `Extract numeric facts from the note. Return STRICT JSON only — no YAML, no markdown code fences, no commentary.
152
+
153
+ Shape: {"facts": [{"value": "<number exactly as written>", "unit": "<unit or omit>", "verb": "<short verb phrase or omit>", "object": "<what the number is about>", "event": "<optional event label or omit>"}]}
154
+
155
+ Rules:
156
+ - value: the numeric quantity as written (e.g. "3:45", "42", "R$ 1.200", "15%")
157
+ - unit: measurement unit when stated (km, kg, hours, BRL, %, ...)
158
+ - verb: short snake_case verb phrase (e.g. "ran", "weighs", "costs", "deadline")
159
+ - object: the thing the number is about, in plain text
160
+ - Only numeric facts clearly stated in the note body. Extract 0-15 facts.
161
+ - Do NOT extract from frontmatter metadata.`;
162
+ async function extractNumericFacts(client, noteTitle, noteBody) {
163
+ // Truncate body to keep the prompt bounded (same budget as the legacy extractor).
164
+ const body = noteBody.substring(0, 3000);
165
+ const response = await client.chat.completions.create({
166
+ model: exports.FACTS_MODEL,
167
+ temperature: 0,
168
+ response_format: { type: 'json_object' },
169
+ messages: [
170
+ { role: 'system', content: SYSTEM_PROMPT },
171
+ { role: 'user', content: `Note: "${noteTitle}"\n\n${body}` },
172
+ ],
173
+ });
174
+ const content = response.choices?.[0]?.message?.content;
175
+ if (!content || !content.trim()) {
176
+ throw new ExtractorParseError('extractor-parse-error: fact extraction returned empty content');
177
+ }
178
+ // JSON-strict after fence-strip: a fenced or bare response parses; YAML,
179
+ // truncated JSON, or commentary still fails hard as an explicit parse error.
180
+ let parsed;
181
+ try {
182
+ parsed = JSON.parse(repairMalformedUnicodeEscapes(stripExtractorCodeFence(content)));
183
+ }
184
+ catch {
185
+ throw new ExtractorParseError(`extractor-parse-error: fact extraction returned non-JSON response (head: ${content.slice(0, 80)})`);
186
+ }
187
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.facts)) {
188
+ throw new ExtractorParseError('extractor-parse-error: fact extraction response is missing a facts array');
189
+ }
190
+ const facts = [];
191
+ for (const raw of parsed.facts) {
192
+ const value = raw?.value === undefined || raw?.value === null ? '' : String(raw.value).trim();
193
+ const object = raw?.object === undefined || raw?.object === null ? '' : String(raw.object).trim();
194
+ if (!value || !object)
195
+ continue; // malformed rows are dropped, not fatal
196
+ const fact = { value, object };
197
+ for (const key of ['unit', 'verb', 'event']) {
198
+ const v = raw[key];
199
+ if (v !== undefined && v !== null && String(v).trim())
200
+ fact[key] = String(v).trim();
201
+ }
202
+ facts.push(fact);
203
+ }
204
+ return facts;
205
+ }
206
+ /**
207
+ * Retry wrapper: parse failures are retried up to MAX_EXTRACT_ATTEMPTS total;
208
+ * the final failure is rethrown explicitly (never cached, never silent []).
209
+ * Non-parse errors (network, auth) propagate immediately.
210
+ */
211
+ async function extractNumericFactsWithRetry(extract, noteTitle, noteBody) {
212
+ let lastError;
213
+ for (let attempt = 1; attempt <= exports.MAX_EXTRACT_ATTEMPTS; attempt++) {
214
+ try {
215
+ return await extract(noteTitle, noteBody);
216
+ }
217
+ catch (err) {
218
+ if (!(err instanceof ExtractorParseError))
219
+ throw err;
220
+ lastError = err;
221
+ }
222
+ }
223
+ throw lastError;
224
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chunkForCandidate = chunkForCandidate;
4
+ exports.adjudicateAndWriteRows = adjudicateAndWriteRows;
5
+ const rowStore_1 = require("../context/rowStore");
6
+ const decision_model_1 = require("./decision-model");
7
+ /**
8
+ * Attribute a candidate to the chunk whose text contains its key phrase
9
+ * (fact: value; event: event phrase; preference: statement). Plain,
10
+ * deterministic substring match — provenance-correct when it matches, and the
11
+ * caller falls back to the fleet's first-chunk attribution when it doesn't.
12
+ */
13
+ function chunkForCandidate(chunks, payload) {
14
+ const phrase = 'statement' in payload
15
+ ? payload.statement
16
+ : 'when' in payload
17
+ ? payload.event
18
+ : 'value' in payload
19
+ ? payload.value
20
+ : undefined;
21
+ if (!phrase)
22
+ return null;
23
+ const needle = phrase.toLowerCase();
24
+ for (const chunk of chunks) {
25
+ if (chunk.text.toLowerCase().includes(needle)) {
26
+ return { chunkId: chunk.id, text: chunk.text };
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ /**
32
+ * Decide, then write — the harmonized single write path for one note's lane
33
+ * candidates. Rows are written AT MOST ONCE per candidate: identical facts
34
+ * bump the existing row's confirm_count (plus the cross-note coexist write),
35
+ * everything else goes through `insertStructuredRows` (identity-keyed
36
+ * supersession + migration v11's partial unique index closing the
37
+ * same-identity insert race).
38
+ */
39
+ async function adjudicateAndWriteRows(db, args, model = (0, decision_model_1.defaultDecisionModel)()) {
40
+ const findCurrent = db.prepare(`SELECT id, source_path, payload_json FROM structured_rows
41
+ WHERE row_kind = ? AND scope = 'global' AND dedupe_key = ? AND valid_to IS NULL
42
+ ORDER BY id LIMIT 1`);
43
+ const bumpConfirm = db.prepare(`UPDATE structured_rows SET confirm_count = confirm_count + 1 WHERE row_kind = ? AND scope = 'global' AND dedupe_key = ? AND valid_to IS NULL`);
44
+ // Does THIS note already hold a current row for the identity? Guards the
45
+ // cross-note confirm write against double-inserting when several candidates
46
+ // of the same note adjudicate `identical` against the original note's row.
47
+ const findOwnCurrent = db.prepare(`SELECT id FROM structured_rows
48
+ WHERE row_kind = ? AND scope = 'global' AND dedupe_key = ? AND source_path = ? AND valid_to IS NULL
49
+ LIMIT 1`);
50
+ let rowsWritten = 0;
51
+ let confirmBumps = 0;
52
+ let decisions = 0;
53
+ let blocked = 0;
54
+ for (const candidate of args.candidates) {
55
+ // 1. Capture gate / sensitivity guardrail — BEFORE any write.
56
+ const gate = await model.decide({ task: 'capture_gate', text: candidate.sourceText });
57
+ decisions++;
58
+ if (gate.task !== 'capture_gate')
59
+ continue;
60
+ if (gate.decision === 'sensitive') {
61
+ blocked++;
62
+ continue;
63
+ }
64
+ const row = {
65
+ rowKind: args.rowKind,
66
+ sourcePath: args.notePath,
67
+ sourceChunkId: candidate.sourceChunkId,
68
+ sourceHash: args.sourceHash,
69
+ extractionVersion: args.extractionVersion,
70
+ validFrom: args.validFrom,
71
+ payload: candidate.payload,
72
+ };
73
+ const dedupeKey = (0, rowStore_1.dedupeKeyFor)(row);
74
+ const existing = findCurrent.get(args.rowKind, dedupeKey);
75
+ const adjudication = await model.decide({
76
+ task: 'dedupe_adjudication',
77
+ rowKind: args.rowKind,
78
+ candidate: candidate.payload,
79
+ existing: existing ? JSON.parse(existing.payload_json) : null,
80
+ });
81
+ decisions++;
82
+ if (adjudication.task !== 'dedupe_adjudication')
83
+ continue;
84
+ if (adjudication.decision === 'identical' && args.rowKind === 'fact') {
85
+ // Facts: an identical restatement corroborates — bump confirm_count,
86
+ // never double-write the same identity from the same adjudication.
87
+ bumpConfirm.run(args.rowKind, dedupeKey);
88
+ confirmBumps++;
89
+ if (!existing || existing.source_path === args.notePath)
90
+ continue;
91
+ // Cross-note `identical` must ALSO write this note's own source-scoped
92
+ // row (review FIX 1): the dedupe key has no source, so a bump-only
93
+ // write leaves the fact pinned solely to the original note's row, whose
94
+ // FK cascade dies with that note while this note still asserts it.
95
+ const own = findOwnCurrent.get(args.rowKind, dedupeKey, args.notePath);
96
+ if (own)
97
+ continue; // already captured from another chunk/run of this note
98
+ (0, rowStore_1.insertStructuredRows)(db, [{ ...row, sourceChunkId: candidate.sourceChunkId }], { skipSupersession: true });
99
+ rowsWritten++;
100
+ continue;
101
+ }
102
+ // 'updated'/'distinct' — and events/preferences restating the same
103
+ // identity — write through the rowStore: identity-keyed supersession
104
+ // (never blanket newest-wins; NULL dates never supersede) + the v11
105
+ // partial unique index closing the same-identity insert race.
106
+ (0, rowStore_1.insertStructuredRows)(db, [row]);
107
+ rowsWritten++;
108
+ }
109
+ return { rowsWritten, confirmBumps, decisions, blocked };
110
+ }