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,478 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NO_ANSWER_STATEMENT = exports.DEFAULT_EVIDENCE_BAR = exports.READ_HONESTY_FLAG = void 0;
4
+ exports.readHonestyEnabled = readHonestyEnabled;
5
+ exports.resolveEvidenceBar = resolveEvidenceBar;
6
+ exports.contentTerms = contentTerms;
7
+ exports.termStem = termStem;
8
+ exports.evidenceCoverage = evidenceCoverage;
9
+ exports.factProvenance = factProvenance;
10
+ exports.noteProvenance = noteProvenance;
11
+ exports.questionKey = questionKey;
12
+ exports.acrossScopes = acrossScopes;
13
+ exports.currentFactsForSubjects = currentFactsForSubjects;
14
+ exports.detectFactConflicts = detectFactConflicts;
15
+ exports.conflictAcrossScopes = conflictAcrossScopes;
16
+ exports.fitConflicts = fitConflicts;
17
+ exports.querySubjects = querySubjects;
18
+ exports.assessAbstention = assessAbstention;
19
+ exports.applyReadHonesty = applyReadHonesty;
20
+ exports.readHonestyForItems = readHonestyForItems;
21
+ const rowStore_1 = require("./rowStore");
22
+ const factConflicts_1 = require("../memory/factConflicts");
23
+ const scope_1 = require("../mvp/scope");
24
+ /**
25
+ * Read-time honesty (LANE 3): provenance, contradiction, abstention.
26
+ *
27
+ * Three behaviours on the product read path, all gated by ONE flag
28
+ * (RENOR_READ_HONESTY, default OFF — nothing here runs unless an operator
29
+ * arms it):
30
+ *
31
+ * 1. provenance — every returned item says where it came from and when it
32
+ * was last verified, so a consuming agent can decide
33
+ * whether to trust or discount it.
34
+ * 2. conflicts — when two CURRENT facts about the same subject+predicate
35
+ * disagree, the read names both values and says which one
36
+ * is newer, instead of silently returning both as if they
37
+ * were compatible. This is the agent-dogfood D2 want: two
38
+ * contradictory statements in one source, where nothing
39
+ * surfaced the conflict. A read that names no scope
40
+ * compares every scope and names each value with every
41
+ * scope it is current in; a scoped read compares its own
42
+ * scope only, as get_context's default conflicts do
43
+ * (memory/factConflicts.ts).
44
+ * 3. abstention — when the query's content terms are mostly absent from the
45
+ * returned evidence, the read says so explicitly
46
+ * ("no good answer on file for this") instead of presenting
47
+ * weak hits as if they answered the question. D6: weak hits
48
+ * train an agent to trust noise.
49
+ *
50
+ * Everything here is gold-blind and product-observable: the inputs are the
51
+ * query text and rows the product itself stored (notes, chunks, facts,
52
+ * entities). No benchmark category, gold label, `shouldAbstain` or session id
53
+ * is readable from this module's signatures.
54
+ */
55
+ /** RENOR_READ_HONESTY (default OFF). Opt in with 1/true/yes/on. */
56
+ exports.READ_HONESTY_FLAG = 'RENOR_READ_HONESTY';
57
+ function readHonestyEnabled(env = process.env) {
58
+ const raw = (env[exports.READ_HONESTY_FLAG] ?? '').trim().toLowerCase();
59
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
60
+ }
61
+ /**
62
+ * Default evidence bar: the share of the query's content terms that must be
63
+ * present in the returned evidence before a hit counts as an answer.
64
+ *
65
+ * MEASURED, not invented (scripts/lane3-probe.ts, synthetic 10-note vault,
66
+ * eval-hash embeddings): on-topic questions cover 0.67–1.00 of their content
67
+ * terms, off-topic questions 0.00–0.33. 0.5 sits between the two populations
68
+ * and is deliberately the conservative side — it abstains only when most of
69
+ * what the question is about is nowhere in the returned evidence.
70
+ *
71
+ * Vector similarity alone does NOT separate these populations on that fixture
72
+ * (an off-topic question scored 0.555 while an on-topic one scored 0.364), so
73
+ * a similarity threshold would be the wrong instrument.
74
+ */
75
+ exports.DEFAULT_EVIDENCE_BAR = 0.5;
76
+ function resolveEvidenceBar(env = process.env) {
77
+ const raw = Number(env.RENOR_READ_HONESTY_BAR);
78
+ if (Number.isFinite(raw) && raw >= 0 && raw <= 1)
79
+ return raw;
80
+ return exports.DEFAULT_EVIDENCE_BAR;
81
+ }
82
+ /**
83
+ * Query words that carry no answer content. Deliberately a stoplist, not a
84
+ * stemmer: the bar must be deterministic and explainable.
85
+ */
86
+ const STOPWORDS = new Set([
87
+ 'the', 'and', 'for', 'are', 'was', 'were', 'did', 'does', 'how', 'what',
88
+ 'when', 'which', 'who', 'why', 'many', 'much', 'about', 'from', 'into',
89
+ 'with', 'this', 'that', 'there', 'their', 'have', 'has', 'had', 'you',
90
+ 'your', 'our', 'his', 'her', 'its', 'not', 'but', 'any', 'all', 'can',
91
+ 'will', 'would', 'should', 'use', 'used', 'take', 'took', 'name', 'start',
92
+ 'here', 'near', 'last', 'over', 'under', 'each', 'than', 'then',
93
+ ]);
94
+ /** Content terms of a query: >=3 chars, not a stopword, de-duplicated in order.
95
+ *
96
+ * DELIBERATELY ASCII (`[^a-z0-9%.]`), even though `rowStore.splitWords` now cuts every
97
+ * other read-side tokenizer on the Unicode write-side rule. This feeds `evidenceCoverage`
98
+ * and `assessAbstention`, and those terms are the DENOMINATOR of the abstention bar: every
99
+ * term must be findable in the evidence or coverage falls and the read abstains. Widening
100
+ * the cut therefore mints content terms the English-only stoplist cannot cover — measured
101
+ * here: `Quando são as reuniões?` against the on-file `Eu prefiro reuniões às sextas-feiras`
102
+ * went from terms `[quando, reuni]` (coverage 0.50, answers) to `[quando, são, reuniões]`
103
+ * (coverage 0.33, abstains "no good answer on file for this"), because `são` is a Portuguese
104
+ * function word that no evidence sentence contains. Widening this lane trades a false
105
+ * abstention for a false answer in every language the stoplist does not cover.
106
+ *
107
+ * A stoplist per language is the real fix and it is a separate, measured change; the
108
+ * entity-resolution lane below is what this PR widens. Pinned by
109
+ * `tests/context/readSideUnicodeTokens.test.ts`, which also asserts the abstention outcome
110
+ * of Portuguese query/fact pairs is unchanged. */
111
+ function contentTerms(query) {
112
+ return Array.from(new Set((query ?? '')
113
+ .toLowerCase()
114
+ .split(/[^a-z0-9%.]+/)
115
+ .filter((t) => t.length >= 3 && !STOPWORDS.has(t))));
116
+ }
117
+ /** The same rule as `contentTerms`, cut on the Unicode write-side class instead. Used ONLY
118
+ * by `querySubjects`, whose terms SELECT entity names rather than score evidence coverage:
119
+ * a name and the query that names it are cut by the same rule, so a subject in any script
120
+ * resolves. Widening is safe here precisely because nothing divides by the term count.
121
+ * Kept as a separate function rather than a flag so the abstention lane above cannot be
122
+ * widened by accident. */
123
+ function subjectTerms(query) {
124
+ return Array.from(new Set((0, rowStore_1.splitWords)(query, true).filter((t) => t.length >= 3 && !STOPWORDS.has(t))));
125
+ }
126
+ /**
127
+ * Length-2 trailing trim ("powered" -> "power", "flights" -> "flight").
128
+ * A prefix test, not a linguistic stemmer — measured to cover the plural and
129
+ * participle forms in the fixture without matching unrelated words.
130
+ */
131
+ function termStem(term) {
132
+ return term.slice(0, Math.max(4, term.length - 2));
133
+ }
134
+ /** Fraction of the query's content terms that appear in the returned evidence. */
135
+ function evidenceCoverage(query, texts) {
136
+ const terms = contentTerms(query);
137
+ if (terms.length === 0)
138
+ return 1; // nothing contentful to require
139
+ const haystack = texts.join('\n').toLowerCase();
140
+ const covered = terms.filter((t) => haystack.includes(termStem(t))).length;
141
+ return covered / terms.length;
142
+ }
143
+ /** Provenance of a lifecycle fact: the ledger is the source of truth, valid_from is its verification date. */
144
+ function factProvenance(validFrom, ledgerRelPath) {
145
+ return { source: ledgerRelPath, lastVerified: validFrom ?? null, kind: 'ledger' };
146
+ }
147
+ /** Provenance of a note-backed item: the note is the source, its session/note date is the verification date. */
148
+ function noteProvenance(notePath, lastVerified) {
149
+ return { source: notePath, lastVerified: lastVerified ?? null, kind: 'note' };
150
+ }
151
+ function instantOf(value, fallbackId) {
152
+ const ms = value ? Date.parse(value) : Number.NaN;
153
+ return Number.isFinite(ms) ? ms : Number.NEGATIVE_INFINITY + fallbackId / 1e6;
154
+ }
155
+ /** One question: a subject and a predicate, compared as the structured-row store compares values. */
156
+ function questionKey(subject, predicate) {
157
+ return `${(0, rowStore_1.normDedupeValue)(subject)}\u0000${(0, rowStore_1.normDedupeValue)(predicate)}`;
158
+ }
159
+ /** Whether a conflict's values are current in different scopes: its sides then carry their scope. */
160
+ function acrossScopes(conflict) {
161
+ return conflict.newer.scope !== undefined;
162
+ }
163
+ function newestFirst(a, b) {
164
+ return instantOf(b.validFrom, b.id) - instantOf(a.validFrom, a.id);
165
+ }
166
+ /** Where a side's value is on file: its source, its scope (when the values span scopes) and its date. */
167
+ function where(side) {
168
+ return `${side.source}${side.scope ? `, scope ${side.scope}` : ''}${side.lastVerified ? `, ${side.lastVerified}` : ', date unknown'}`;
169
+ }
170
+ /**
171
+ * Current facts for a subject set, all source paths included.
172
+ *
173
+ * Reads `valid_to IS NULL` rows across the WHOLE table, not just the
174
+ * `_mcp_` lifecycle stream: the contradiction that matters is cross-source
175
+ * (a note asserting one value while another note or the ledger asserts
176
+ * another). Restricting to one writer would hide exactly the conflict this
177
+ * behaviour exists to surface.
178
+ *
179
+ * `scope` is the read's scope: a scoped read sees that scope's facts only
180
+ * (under any spelling of it, savedScopeSqlPredicate), so it is never told what
181
+ * another scope holds; null (a read that names no scope) sees every scope.
182
+ */
183
+ function currentFactsForSubjects(db, subjects, scope = null) {
184
+ const names = Array.from(new Set(subjects.map((s) => (s ?? '').trim()).filter(Boolean)));
185
+ if (names.length === 0)
186
+ return [];
187
+ const placeholders = names.map(() => '?').join(', ');
188
+ const inScope = scope === null ? null : (0, scope_1.savedScopeSqlPredicate)((0, scope_1.normalizeScope)(scope), 'f.scope'); // any spelling of it
189
+ const rows = db
190
+ .prepare(`SELECT f.id AS id, e.canonical_name AS subject, f.predicate AS predicate,
191
+ f.object_text AS object, f.source_path AS sourcePath,
192
+ COALESCE(f.scope, 'global') AS scope, f.valid_from AS validFrom
193
+ FROM facts f
194
+ JOIN entities e ON e.id = f.subject_entity_id
195
+ WHERE f.valid_to IS NULL
196
+ AND e.canonical_name COLLATE NOCASE IN (${placeholders})${inScope ? `
197
+ AND ${inScope.sql}` : ''}
198
+ ORDER BY f.valid_from DESC, f.id DESC`)
199
+ .all(...names, ...(inScope?.params ?? []));
200
+ return rows.map((r) => ({
201
+ id: r.id,
202
+ subject: r.subject,
203
+ predicate: r.predicate,
204
+ object: r.object ?? '',
205
+ sourcePath: r.sourcePath,
206
+ scope: r.scope ?? 'global',
207
+ validFrom: r.validFrom ?? null,
208
+ }));
209
+ }
210
+ /**
211
+ * Detect read-time contradictions: >=2 CURRENT facts sharing subject+predicate
212
+ * but carrying different values. Values are compared after the same
213
+ * NFKD/ASCII/lowercase normalization the structured-row store uses, so case
214
+ * and accent differences are not reported as disagreements.
215
+ *
216
+ * Deterministic: within a group, sides are ordered by instant desc then id desc.
217
+ */
218
+ function detectFactConflicts(rows) {
219
+ const groups = new Map();
220
+ for (const row of rows) {
221
+ if (!row.object.trim())
222
+ continue;
223
+ // Subject and predicate key the group, across scopes: the rows are the
224
+ // read's own (currentFactsForSubjects keeps a scoped read inside its
225
+ // scope), so a read that names no scope is told when one question has
226
+ // different current values in two scopes, as get_context's default
227
+ // conflicts tell it (memory scout finding 13).
228
+ const key = questionKey(row.subject, row.predicate);
229
+ const bucket = groups.get(key);
230
+ if (bucket)
231
+ bucket.push(row);
232
+ else
233
+ groups.set(key, [row]);
234
+ }
235
+ const conflicts = [];
236
+ for (const bucket of groups.values()) {
237
+ const byValue = new Map();
238
+ for (const row of bucket) {
239
+ const vkey = (0, rowStore_1.normDedupeValue)(row.object);
240
+ const list = byValue.get(vkey);
241
+ if (list)
242
+ list.push(row);
243
+ else
244
+ byValue.set(vkey, [row]);
245
+ }
246
+ if (byValue.size < 2)
247
+ continue; // a single value, however many sources: no disagreement
248
+ // One representative per distinct value: the newest row carrying it.
249
+ const reps = Array.from(byValue.values()).map((rowsForValue) => [...rowsForValue].sort(newestFirst)[0]);
250
+ reps.sort(newestFirst);
251
+ // Values current in different scopes: each value is named in every scope it is current in, with that scope, as
252
+ // get_context's default conflicts list them (memory/factConflicts.ts). Every value once, newest first, then the
253
+ // other scopes that hold one of them; within a scope a value's newest row stands for it, as it does across
254
+ // sources. A conflict inside one scope keeps its shape.
255
+ if (new Set(bucket.map((r) => r.scope)).size > 1) {
256
+ const valueInScope = (r) => `${(0, rowStore_1.normDedupeValue)(r.object)}\u0000${r.scope}`;
257
+ const named = new Set(reps.map(valueInScope));
258
+ const elsewhere = [...bucket].sort(newestFirst).filter((r) => {
259
+ if (named.has(valueInScope(r)))
260
+ return false;
261
+ named.add(valueInScope(r));
262
+ return true;
263
+ });
264
+ const sides = [...reps, ...elsewhere].map((r) => ({ value: r.object, source: r.sourcePath, lastVerified: r.validFrom, scope: r.scope }));
265
+ conflicts.push(conflictAcrossScopes(reps[0].subject, reps[0].predicate, sides.slice(0, factConflicts_1.MAX_DRIFT_VALUES), sides.length));
266
+ continue;
267
+ }
268
+ const sides = reps.map((r) => ({
269
+ value: r.object,
270
+ source: r.sourcePath,
271
+ lastVerified: r.validFrom,
272
+ }));
273
+ if (sides.length < 2)
274
+ continue;
275
+ const [newer, older] = sides;
276
+ conflicts.push({
277
+ subject: reps[0].subject,
278
+ predicate: reps[0].predicate,
279
+ values: sides.map((s) => s.value),
280
+ newer,
281
+ older,
282
+ statement: `Contradiction on ${reps[0].subject} / ${reps[0].predicate}: ` +
283
+ `"${newer.value}" (${where(newer)}) is newer than ` +
284
+ `"${older.value}" (${where(older)}). ` +
285
+ `Both are current in the store — do not treat them as compatible.`,
286
+ });
287
+ }
288
+ // Stable order: subject, then predicate, then newest value.
289
+ conflicts.sort((a, b) => {
290
+ if (a.subject !== b.subject)
291
+ return a.subject < b.subject ? -1 : 1;
292
+ if (a.predicate !== b.predicate)
293
+ return a.predicate < b.predicate ? -1 : 1;
294
+ return a.newer.value < b.newer.value ? -1 : a.newer.value > b.newer.value ? 1 : 0;
295
+ });
296
+ return conflicts;
297
+ }
298
+ /**
299
+ * A contradiction among values current in different scopes, naming each of its `sides` with its scope, in the order
300
+ * `FactConflict.alsoCurrent` describes (so the first two disagree). `total` is how many sides there are when `sides`
301
+ * holds only the first of them (the MAX_DRIFT_VALUES cap, or `fitConflicts`); the statement then counts the rest.
302
+ */
303
+ function conflictAcrossScopes(subject, predicate, sides, total = sides.length) {
304
+ const [newer, older, ...also] = sides;
305
+ const values = [];
306
+ const seen = new Set();
307
+ for (const side of sides) {
308
+ if (seen.has((0, rowStore_1.normDedupeValue)(side.value)))
309
+ continue;
310
+ seen.add((0, rowStore_1.normDedupeValue)(side.value));
311
+ values.push(side.value);
312
+ }
313
+ const unnamed = total - sides.length;
314
+ const named = also.map((side) => `"${side.value}" (${where(side)})`).join(', ');
315
+ const rest = `${named}${unnamed > 0 ? `${named ? ' and ' : ''}${unnamed} more` : ''}`;
316
+ return {
317
+ subject,
318
+ predicate,
319
+ values,
320
+ newer,
321
+ older,
322
+ ...(also.length > 0 ? { alsoCurrent: also } : {}),
323
+ ...(unnamed > 0 ? { total } : {}),
324
+ statement: `Contradiction on ${subject} / ${predicate}: ` +
325
+ `"${newer.value}" (${where(newer)}) is newer than ` +
326
+ `"${older.value}" (${where(older)})${rest ? `; also current: ${rest}` : ''}. ` +
327
+ `${total === 2 ? 'Both are' : `All ${total} are`} current in the store — do not treat them as compatible.`,
328
+ };
329
+ }
330
+ /**
331
+ * The conflicts cut to `budget`: a number of characters of their JSON, or whether a list of them fits (get_context: its
332
+ * whole reply inside max_tokens, as the engine counts tokens). `conflicts` are as detectFactConflicts returns them, and
333
+ * come back unchanged when they fit. Otherwise a conflict across scopes names the two values that disagree (newer,
334
+ * older), then more of its sides while the list fits, first conflict first, and `total` counts them all; a conflict
335
+ * inside one scope is kept as it is. So however many scopes hold a value, the conflicts stay within the budget, except
336
+ * for the two values that make each contradiction: those are always named.
337
+ */
338
+ function fitConflicts(conflicts, budget) {
339
+ const fits = typeof budget === 'number' ? (shown) => JSON.stringify(shown).length <= budget : budget;
340
+ if (fits(conflicts))
341
+ return conflicts;
342
+ const sides = conflicts.map((c) => [c.newer, c.older, ...(c.alsoCurrent ?? [])]);
343
+ const naming = (i, n) => {
344
+ const c = conflicts[i];
345
+ return n >= sides[i].length ? c : conflictAcrossScopes(c.subject, c.predicate, sides[i].slice(0, n), c.total ?? sides[i].length);
346
+ };
347
+ const shown = conflicts.map((c, i) => (c.alsoCurrent ? naming(i, 2) : c));
348
+ for (let i = 0; i < shown.length; i++) {
349
+ if (!conflicts[i].alsoCurrent)
350
+ continue;
351
+ for (let n = 3; n <= sides[i].length; n++) {
352
+ const next = naming(i, n);
353
+ if (!fits(shown.map((c, j) => (j === i ? next : c))))
354
+ break;
355
+ shown[i] = next;
356
+ }
357
+ }
358
+ return shown;
359
+ }
360
+ /** Same rule as `subjectTerms`, applied to a canonical name or alias, so a name and the query
361
+ * that names it are cut identically and a subject in any script resolves. */
362
+ function entityTokens(value) {
363
+ return (0, rowStore_1.splitWords)(value).filter((t) => t.length >= 3 && !STOPWORDS.has(t));
364
+ }
365
+ /**
366
+ * Gold-blind query -> subject resolution: canonical names (or aliases) that
367
+ * share a content token with the query, plus any canonical name stated whole in
368
+ * the query. Reads only the query text and the product's own entity table.
369
+ */
370
+ function querySubjects(db, query, limit = 20) {
371
+ const terms = subjectTerms(query);
372
+ if (terms.length === 0)
373
+ return [];
374
+ const lowerQuery = ` ${(0, rowStore_1.matchForm)(query)} `;
375
+ const found = new Set();
376
+ const select = db.prepare(`SELECT DISTINCT canonical_name AS name, aliases_json AS aliases
377
+ FROM entities
378
+ WHERE lower(canonical_name) LIKE '%' || ? || '%'
379
+ LIMIT 200`);
380
+ for (const term of terms.slice(0, 8)) {
381
+ const rows = select.all(term);
382
+ for (const row of rows) {
383
+ const name = String(row.name);
384
+ const tokens = entityTokens(name);
385
+ let aliases = [];
386
+ try {
387
+ const parsed = JSON.parse(row.aliases ?? '[]');
388
+ if (Array.isArray(parsed))
389
+ aliases = parsed.map(String);
390
+ }
391
+ catch {
392
+ aliases = [];
393
+ }
394
+ const aliasTokens = aliases.flatMap(entityTokens);
395
+ const wholeName = name.length >= 3 && lowerQuery.includes(` ${(0, rowStore_1.matchForm)(name)} `);
396
+ if (wholeName || tokens.includes(term) || aliasTokens.includes(term)) {
397
+ found.add(name);
398
+ }
399
+ if (found.size >= limit)
400
+ return Array.from(found);
401
+ }
402
+ }
403
+ return Array.from(found);
404
+ }
405
+ exports.NO_ANSWER_STATEMENT = 'no good answer on file for this';
406
+ /**
407
+ * Honest abstention. Returns null when the evidence clears the bar (the read
408
+ * may answer), or an explicit no-answer record when it does not. An empty
409
+ * evidence set is always a no-answer — there is nothing on file at all.
410
+ */
411
+ function assessAbstention(input) {
412
+ const bar = input.bar ?? exports.DEFAULT_EVIDENCE_BAR;
413
+ const considered = input.considered ?? input.texts.length;
414
+ const terms = contentTerms(input.query);
415
+ if (considered === 0) {
416
+ return {
417
+ status: 'no-answer',
418
+ reason: 'no-evidence',
419
+ statement: exports.NO_ANSWER_STATEMENT,
420
+ coverage: 0,
421
+ bar,
422
+ terms,
423
+ considered: 0,
424
+ };
425
+ }
426
+ const coverage = evidenceCoverage(input.query, input.texts);
427
+ if (coverage < bar) {
428
+ return {
429
+ status: 'no-answer',
430
+ reason: 'below-evidence-bar',
431
+ statement: exports.NO_ANSWER_STATEMENT,
432
+ coverage,
433
+ bar,
434
+ terms,
435
+ considered,
436
+ };
437
+ }
438
+ return null;
439
+ }
440
+ function applyReadHonesty(options) {
441
+ const bar = resolveEvidenceBar(options.env);
442
+ const subjects = Array.from(new Set([...(options.subjects ?? []), ...querySubjects(options.db, options.query)]));
443
+ const returned = options.facts && new Set(options.facts.map((f) => questionKey(f.subject, f.predicate)));
444
+ const conflicts = detectFactConflicts(currentFactsForSubjects(options.db, subjects, options.scope ?? null))
445
+ .filter((c) => !returned || !acrossScopes(c) || returned.has(questionKey(c.subject, c.predicate)));
446
+ const abstention = assessAbstention({
447
+ query: options.query,
448
+ texts: options.texts,
449
+ considered: options.considered,
450
+ bar,
451
+ });
452
+ // A contradiction is itself evidence that something is on file: an agent
453
+ // told "no good answer" while being handed a conflict would be misled in the
454
+ // opposite direction. The conflict is reported, the abstention stands down.
455
+ if (abstention && conflicts.length > 0)
456
+ return { conflicts, abstention: null };
457
+ return { conflicts, abstention };
458
+ }
459
+ /**
460
+ * Read honesty for a retrieval-style read (the CLI `search` path and any other
461
+ * surface that returns note hits rather than lifecycle facts). Subjects come
462
+ * from the product's own entity table, joined to the query text — never from a
463
+ * caller-supplied list, so this cannot be fed gold labels.
464
+ */
465
+ function readHonestyForItems(options) {
466
+ const honesty = applyReadHonesty({
467
+ db: options.db,
468
+ query: options.query,
469
+ ledgerRelPath: options.ledgerRelPath,
470
+ texts: options.items.map((i) => i.text),
471
+ considered: options.items.length,
472
+ env: options.env,
473
+ });
474
+ return {
475
+ ...honesty,
476
+ provenance: options.items.map((i) => noteProvenance(i.notePath, i.session_date ?? null)),
477
+ };
478
+ }