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,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
|
+
}
|