@polycode-projects/the-mechanical-code-talker 1.9.1 → 1.10.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/README.md +441 -217
- package/bin/tmct.mjs +126 -1
- package/corpus/seon/README.md +1 -2
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2874
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- package/src/wink-model.mjs +10 -24
package/src/memory/core.mjs
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
|
-
// memory/core.mjs — tmct's OWN conversational memory graph
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// provider's graph (docs/adapter-contract.md); memory writes land ONLY here.
|
|
8
|
-
//
|
|
9
|
-
// What goes in:
|
|
10
|
-
// - every parsed inbound request becomes an "a-visitor-said" individual
|
|
11
|
-
// (class `Utterance`, role=visitor) and every response an "a-tmct-said"
|
|
12
|
-
// individual (role=tmct), each carrying text/ts/role attributes, an
|
|
13
|
-
// `mgx:saidInSession` edge to its Session anchor, and — for a response —
|
|
14
|
-
// an `mgx:inReplyTo` edge to the visitor utterance it answers;
|
|
15
|
-
// - grammar-derived OWL triples via appendFact() (subject/predicate/object +
|
|
16
|
-
// provenance), reified RDF-style (rdf:subject / rdf:predicate / rdf:object
|
|
17
|
-
// on a `Fact` individual) — the Phase-2 ACE parser's write point.
|
|
18
|
-
//
|
|
19
|
-
// OWL labelling: individuals are rdf-ish typed twice — the payload-level `class`
|
|
20
|
-
// field (Utterance / Fact / Session, counted in `classes[]` like every other
|
|
21
|
-
// graph class) AND an `rdf:type` attribute naming the OWL term
|
|
22
|
-
// (owl:NamedIndividual for utterances, rdf:Statement for reified facts), with
|
|
23
|
-
// the owl/rdf/rdfs prefixes declared in the payload's `prefixes` block —
|
|
24
|
-
// consistent with graph-build.mjs's JSON-label-only vocabulary style.
|
|
25
|
-
//
|
|
26
|
-
// Every append is crash-safe (fresh read → mutate → temp-file + rename, the
|
|
27
|
-
// sessions.mjs discipline) and IDEMPOTENT: utterance ids are deterministic
|
|
28
|
-
// (utt:<session>#<ts>#<role>) and fact ids hash the triple, so the per-turn
|
|
29
|
-
// re-append sessions.mjs performs replaces rather than duplicates.
|
|
1
|
+
// memory/core.mjs — tmct's OWN conversational memory graph: a dedicated
|
|
2
|
+
// OWL-labelled store at <repo>/.tmct/memory/graph.json, distinct from any
|
|
3
|
+
// provider-supplied code graph. Utterances, Facts (reified RDF triples via
|
|
4
|
+
// appendFact), and Sessions are all typed twice — payload `class` and an
|
|
5
|
+
// `rdf:type` attribute. Every append is crash-safe and idempotent (utterance
|
|
6
|
+
// ids are deterministic, fact ids hash the triple).
|
|
30
7
|
|
|
31
8
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
32
9
|
import { dirname, join } from "node:path";
|
|
@@ -43,37 +20,29 @@ export const UTTERANCE_CLASS = "Utterance";
|
|
|
43
20
|
export const FACT_CLASS = "Fact";
|
|
44
21
|
export const MEMORY_SESSION_CLASS = "Session";
|
|
45
22
|
export const SOURCE_CLASS = "Source";
|
|
46
|
-
//
|
|
47
|
-
//
|
|
23
|
+
// A taught RULE (a composed/filtered/recursive relation-shape) — a sibling
|
|
24
|
+
// of Fact, never a taught concept itself.
|
|
48
25
|
export const RULE_CLASS = "Rule";
|
|
49
26
|
|
|
50
27
|
export const SAID_IN_SESSION_PROP = "mgx:saidInSession";
|
|
51
28
|
export const IN_REPLY_TO_PROP = "mgx:inReplyTo";
|
|
52
29
|
|
|
53
|
-
// The provenance-link predicate family
|
|
54
|
-
//
|
|
55
|
-
//
|
|
30
|
+
// The provenance-link predicate family: one umbrella object property with two
|
|
31
|
+
// workhorse subproperties, minted in the owned mgx: namespace to match
|
|
32
|
+
// tmct-core.ttl's object-property style.
|
|
56
33
|
export const DERIVED_FROM_PROP = "mgx:derivedFrom"; // umbrella: Fact → Source|Fact
|
|
57
34
|
export const STATED_BY_PROP = "mgx:statedBy"; // a Source directly asserts a Fact
|
|
58
35
|
export const CANONICALISED_FROM_PROP = "mgx:canonicalisedFrom"; // a canonical Fact ← its raw form
|
|
59
36
|
export const CREATED_AT_PROP = "mgx:createdAt"; // first-write-wins ISO-8601 on every individual
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// without necessarily touching an edge (upsertSession, recomputeFactTrust,
|
|
64
|
-
// recomputeSourceReliability), where the derived rule alone can't see the change (PLAN_VIZ.md §2).
|
|
37
|
+
// For call sites that mutate an individual's own attributes without touching
|
|
38
|
+
// an edge (upsertSession, recomputeFactTrust, recomputeSourceReliability),
|
|
39
|
+
// where codegraph.mjs's derived-updatedAt rule alone can't see the change.
|
|
65
40
|
export const UPDATED_AT_PROP = "mgx:updatedAt";
|
|
66
41
|
export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (session-scoped) trust nudge on a Source, [0.5,1.5]
|
|
67
42
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// that never threaded a session id through). Once a provenance tag DOES carry
|
|
72
|
-
// a session-id segment (every real chat/teach write does — see
|
|
73
|
-
// grammar/assert.mjs's provenanceTag / chat.mjs's teachProvenanceTag), each
|
|
74
|
-
// session mints its OWN Source individual instead: `${ID}:<sessionId>`
|
|
75
|
-
// (sourceIdFor below) — actor-level (session-scoped) trust, unconditional,
|
|
76
|
-
// no config flag (PLAN_PROVENANCE_TRUST Part B).
|
|
43
|
+
// Bare (session-less) singleton Source ids — fallback for a provenance tag
|
|
44
|
+
// with no session-id segment. A tag that does carry one mints its own
|
|
45
|
+
// per-session Source instead (`${ID}:<sessionId>`, sourceIdFor below).
|
|
77
46
|
export const OPERATOR_SOURCE_ID = "src:operator-chat";
|
|
78
47
|
export const TEACH_SOURCE_ID = "src:teach-chat";
|
|
79
48
|
|
|
@@ -139,14 +108,9 @@ export function emptyMemory() {
|
|
|
139
108
|
};
|
|
140
109
|
}
|
|
141
110
|
|
|
142
|
-
/** Resolve the on-disk path of a memory graph file for `dir`. `version
|
|
143
|
-
* (
|
|
144
|
-
*
|
|
145
|
-
* `version` resolves a SNAPSHOT copy (`graph.v{version}.json`, see
|
|
146
|
-
* snapshotMemory below) — never the live file. The single source of truth for
|
|
147
|
-
* "where does the memory graph live on disk", closing the desync risk of two
|
|
148
|
-
* independent path-resolution copies (core.mjs's mutateMemory and fold.mjs's
|
|
149
|
-
* writeMemoryGraph used to compute this path separately). */
|
|
111
|
+
/** Resolve the on-disk path of a memory graph file for `dir`. `version` null
|
|
112
|
+
* (default) is the live graph; a numeric version resolves a snapshot copy
|
|
113
|
+
* (see snapshotMemory below). The single source of truth for this path. */
|
|
150
114
|
export function resolveMemoryGraphFile(dir, version = null) {
|
|
151
115
|
if (isMemoryHandle(dir) || isSqliteHandle(dir)) {
|
|
152
116
|
throw new Error("resolveMemoryGraphFile: dir is a memory/sqlite handle, not a file path (Backend A only)");
|
|
@@ -157,30 +121,11 @@ export function resolveMemoryGraphFile(dir, version = null) {
|
|
|
157
121
|
|
|
158
122
|
const memoryGraphFile = (dir) => resolveMemoryGraphFile(dir);
|
|
159
123
|
|
|
160
|
-
// ---- Storage-backend seam
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// every existing caller that never opts into anything else).
|
|
166
|
-
//
|
|
167
|
-
// `dir` may now ALSO be a memory HANDLE: a small tagged object created by
|
|
168
|
-
// createInMemoryStore() (Backend B, pure in-memory, zero disk I/O) or
|
|
169
|
-
// createSqliteMemoryStore() (Backend C, a live node:sqlite connection kept
|
|
170
|
-
// open for the session's lifetime). loadMemory/mutateMemory below recognize
|
|
171
|
-
// both and dispatch the LOAD/PERSIST steps only; every other function in this
|
|
172
|
-
// file (appendFact, appendFacts, appendUtterance(s), appendRule,
|
|
173
|
-
// readFactRows, findRuleByName, resolveRelationChase(Reverse),
|
|
174
|
-
// findContradictions) takes `memory`/`dir` exactly as before and never
|
|
175
|
-
// branches on backend — they operate on the plain JS payload object
|
|
176
|
-
// mutateMemory hands them, regardless of where it came from or where it goes
|
|
177
|
-
// next. That is the whole point of the seam: id hashing, provenance/trust
|
|
178
|
-
// computation, migrateLegacyProvenance, recomputeSourceReliability and
|
|
179
|
-
// buildProseIndex are backend-agnostic logic, unchanged either way.
|
|
180
|
-
//
|
|
181
|
-
// snapshotMemory (manifest-versioned snapshots) and resolveMemoryGraphFile
|
|
182
|
-
// stay Backend-A-only (a handle has no on-disk file to snapshot) — both throw
|
|
183
|
-
// a clear error if given a handle rather than silently doing the wrong thing.
|
|
124
|
+
// ---- Storage-backend seam --------------------------------------------------
|
|
125
|
+
// `dir` is either a plain repo-path string (Backend A, file-backed) or a
|
|
126
|
+
// handle from createInMemoryStore() (Backend B) or createSqliteMemoryStore()
|
|
127
|
+
// (Backend C). Only loadMemory/mutateMemory dispatch on backend; every other
|
|
128
|
+
// function operates on the plain payload object they hand back.
|
|
184
129
|
|
|
185
130
|
const BACKEND_MEMORY = "memory";
|
|
186
131
|
const BACKEND_SQLITE = "sqlite";
|
|
@@ -195,80 +140,18 @@ function isMemoryOrSqliteHandle(dir) {
|
|
|
195
140
|
return isMemoryHandle(dir) || isSqliteHandle(dir);
|
|
196
141
|
}
|
|
197
142
|
|
|
198
|
-
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* sessions in one process): `{ backend: "memory", payload }`. loadMemory
|
|
202
|
-
* returns `payload` directly (the live reference, not a fresh parse — there
|
|
203
|
-
* is nothing to parse); mutateMemory's persist step is a no-op assignment
|
|
204
|
-
* (`handle.payload = out` — already the same object in every real caller,
|
|
205
|
-
* since none of appendFact/appendFacts/appendUtterance(s)/appendRule ever
|
|
206
|
-
* return a NEW object from their mutateMemory callback, they all mutate the
|
|
207
|
-
* payload in place). ZERO readFile/writeFile/JSON.parse/JSON.stringify calls
|
|
208
|
-
* ever happen for this backend — verified directly by this module's own
|
|
209
|
-
* dispatch (no fs import is even reachable from this path) and by
|
|
210
|
-
* test/memory-backend-memory.test.mjs's fs-spy assertions.
|
|
211
|
-
*
|
|
212
|
-
* Distinct from `--ephemeral` (createSession): ephemeral mode still does real
|
|
213
|
-
* readFile/JSON.parse/writeFile round-trips against a throwaway mkdtemp temp
|
|
214
|
-
* dir every turn — "disposable disk," not "no disk." Backend B is genuinely
|
|
215
|
-
* disk-free.
|
|
216
|
-
*/
|
|
143
|
+
/** Backend B — pure in-memory store: `{ backend: "memory", payload }` held by
|
|
144
|
+
* the caller (never module-global). Zero file I/O; distinct from
|
|
145
|
+
* `--ephemeral`, which still round-trips a throwaway temp dir. */
|
|
217
146
|
export function createInMemoryStore() {
|
|
218
147
|
return { backend: BACKEND_MEMORY, payload: emptyMemory() };
|
|
219
148
|
}
|
|
220
149
|
|
|
221
|
-
// ---- Backend C — SQLite
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
// zero external dependency): an `ids`/`nodes`/`relations`/`edges`/`meta` table
|
|
227
|
-
// set. Its WRITE MODEL is a full rebuild-and-atomic-swap on every write — correct
|
|
228
|
-
// for seonix's problem (read-latency on a relatively static, rebuild-on-change
|
|
229
|
-
// code graph), wrong for tmct's (write-heavy, one-fact-at-a-time accumulation
|
|
230
|
-
// across a session's lifetime): lifting it as-is would just replace "rewrite the
|
|
231
|
-
// whole JSON file per turn" with "rebuild the whole SQLite file per turn."
|
|
232
|
-
//
|
|
233
|
-
// tmct's Backend C reuses the SHAPE, not the write model: real per-row
|
|
234
|
-
// INSERT/REPLACE/DELETE against a LIVE, OPEN connection kept for the session's
|
|
235
|
-
// lifetime (see createSqliteMemoryStore/closeSqliteMemoryStore below),
|
|
236
|
-
// diffed against whatever is already on that row so only touched
|
|
237
|
-
// individuals/edges are ever written — not seonix's rebuild-and-swap.
|
|
238
|
-
//
|
|
239
|
-
// Schema, adapted (not ported) to tmct's actual payload shape (emptyMemory(),
|
|
240
|
-
// above — { generated_at, memory, prefixes, vocabulary, classes,
|
|
241
|
-
// objectProperties, individuals, proseIndex }, distinct from seonix's code-graph
|
|
242
|
-
// `entities` shape): seonix's separate integer-interning `ids` table exists to
|
|
243
|
-
// cover edge endpoints that AREN'T always node ids (e.g. an `inherits` edge's
|
|
244
|
-
// `ext:<Base>` target). tmct's own edge groups (saidInSession, inReplyTo,
|
|
245
|
-
// statedBy, canonicalisedFrom) only ever link two individuals-table ids, so
|
|
246
|
-
// that interning table is dropped here — individuals/edges reference each
|
|
247
|
-
// other by their natural TEXT id directly, a deliberate simplification over a
|
|
248
|
-
// literal port. A Fact's reified rdf:subject/rdf:predicate/rdf:object live as
|
|
249
|
-
// ATTRIBUTES on the individual (tmct's own reification style, no seonix
|
|
250
|
-
// equivalent), so they ride inside that individual's own JSON blob column —
|
|
251
|
-
// no separate fact-triple columns needed.
|
|
252
|
-
//
|
|
253
|
-
// Cached, incrementally patched reads (closes the PLAN_SEED.md §6 gap the
|
|
254
|
-
// prior "honest shortcut" comment used to flag here): the READ side
|
|
255
|
-
// (readSqlitePayload) reconstructs the FULL in-memory payload shape from real
|
|
256
|
-
// SQL SELECTs only ONCE — the first call for a given handle, or the first
|
|
257
|
-
// call after a failed/rolled-back write — and stashes the result on
|
|
258
|
-
// `handle.cachedPayload`. Every later call returns a deep clone of that cache
|
|
259
|
-
// directly, with ZERO SQL queries: no re-SELECT of individuals, no
|
|
260
|
-
// per-relation edge SELECT. The WRITE side (persistSqlitePayload) was never a
|
|
261
|
-
// shortcut: it already diffs the incoming payload against what is already in
|
|
262
|
-
// each row/edge-group and only issues a real INSERT/REPLACE/DELETE for what
|
|
263
|
-
// actually changed (write cost proportional to what changed this turn, not to
|
|
264
|
-
// the total store size). It now ALSO applies that exact same diff to
|
|
265
|
-
// `handle.cachedPayload` in lockstep — patching only the individuals/edges it
|
|
266
|
-
// actually wrote to SQLite, in the same order SQLite itself would reorder
|
|
267
|
-
// them (a changed row gets a fresh rowid and sorts last) — so the cache never
|
|
268
|
-
// goes stale, and never needs a re-query to catch up either. If a write fails
|
|
269
|
-
// mid-transaction (ROLLBACK), the partially-patched cache is not trusted: it
|
|
270
|
-
// is invalidated so the NEXT read does an honest full rebuild instead of
|
|
271
|
-
// risking a state that was never actually committed.
|
|
150
|
+
// ---- Backend C — SQLite: a live node:sqlite connection, per-row
|
|
151
|
+
// INSERT/REPLACE/DELETE diffed against what's already stored (write cost
|
|
152
|
+
// proportional to what changed, not total store size). Reads are cached on
|
|
153
|
+
// `handle.cachedPayload` and incrementally patched in lockstep with writes;
|
|
154
|
+
// a failed write invalidates the cache so the next read rebuilds honestly.
|
|
272
155
|
|
|
273
156
|
const SQLITE_DDL = `
|
|
274
157
|
CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);
|
|
@@ -278,24 +161,13 @@ CREATE TABLE IF NOT EXISTS edges (prop TEXT NOT NULL, subject TEXT NOT NULL, obj
|
|
|
278
161
|
CREATE INDEX IF NOT EXISTS edges_by_prop ON edges(prop);
|
|
279
162
|
`;
|
|
280
163
|
|
|
281
|
-
// Edge keys with dedicated columns; any other key
|
|
282
|
-
// (none exist in core.mjs's own edge groups today, but a future/external
|
|
283
|
-
// writer might add one) round-trips via the `extra` JSON column, same
|
|
284
|
-
// discipline as seonix's own STD_EDGE_KEYS.
|
|
164
|
+
// Edge keys with dedicated columns; any other key round-trips via `extra`.
|
|
285
165
|
const STD_EDGE_KEYS = new Set(["subject", "object", "subjectLabel", "objectLabel"]);
|
|
286
166
|
|
|
287
|
-
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* loaded, so a caller that never opts into this backend never even imports it
|
|
292
|
-
* (matching seonix's own SEONIX_STORE=sqlite gating discipline, and tmct's
|
|
293
|
-
* minimal-deps philosophy: zero external dependency either way).
|
|
294
|
-
*
|
|
295
|
-
* The connection is meant to be opened ONCE per session and kept open for the
|
|
296
|
-
* session's lifetime (not re-opened per call) — close it via
|
|
297
|
-
* closeSqliteMemoryStore when the session ends.
|
|
298
|
-
*/
|
|
167
|
+
/** Open (creating if absent) a resident node:sqlite connection: a Backend C
|
|
168
|
+
* handle `{ backend: "sqlite", db, dbPath }`. `node:sqlite` is imported
|
|
169
|
+
* lazily — only opting into this backend ever loads it. Meant to be opened
|
|
170
|
+
* once per session; close via closeSqliteMemoryStore at session end. */
|
|
299
171
|
export async function createSqliteMemoryStore(dbPath) {
|
|
300
172
|
const { DatabaseSync } = await import("node:sqlite");
|
|
301
173
|
const db = new DatabaseSync(dbPath);
|
|
@@ -312,30 +184,10 @@ export function closeSqliteMemoryStore(handle) {
|
|
|
312
184
|
if (isSqliteHandle(handle)) handle.db.close();
|
|
313
185
|
}
|
|
314
186
|
|
|
315
|
-
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* appendFacts, seedMemory via src/corpus/conceptnet.mjs, …) already accepts —
|
|
320
|
-
* a plain repo-path string for Backend A, or a live Backend B/C handle — plus
|
|
321
|
-
* an idempotent `close()` cleanup (a no-op for A/B; closes the sqlite
|
|
322
|
-
* connection for C).
|
|
323
|
-
*
|
|
324
|
-
* The ONE shared resolver for the storage-backend seam (PLAN_SEED.md §6):
|
|
325
|
-
* originally inlined only in chat.mjs's createSession, factored out here so
|
|
326
|
-
* `tmct init`'s corpus seed (src/init.mjs) and bin/tmct.mjs's
|
|
327
|
-
* `--corpus`/`--ontology`/`--lexicon` activation seed the SAME backend a
|
|
328
|
-
* later `tmct chat` in that repo will read — a caller resolving "sqlite"
|
|
329
|
-
* independently and picking a different db path (or a different in-memory
|
|
330
|
-
* store) would silently split a repo's memory across two places that can
|
|
331
|
-
* never see each other. `backendChoice` is a precedence result the CALLER
|
|
332
|
-
* already computed (CLI flag > env > tmct.toml > default, or whatever subset
|
|
333
|
-
* applies); this function does no precedence resolution of its own — it only
|
|
334
|
-
* maps the resolved token to a handle. `repoRoot` is used only for Backend C's
|
|
335
|
-
* db path (`<repoRoot>/.tmct/memory/graph.sqlite`); Backend A returns
|
|
336
|
-
* `repoRoot` itself unchanged, byte-identical to every existing plain-string
|
|
337
|
-
* caller.
|
|
338
|
-
*/
|
|
187
|
+
/** Resolve a backend token ("memory" | "sqlite" | anything else) into
|
|
188
|
+
* `{ dir, close }` — the ONE shared resolver, so every entry point (init's
|
|
189
|
+
* corpus seed, bin/tmct.mjs, chat) picks the same backend for a repo rather
|
|
190
|
+
* than silently splitting its memory across two stores. */
|
|
339
191
|
export async function openMemoryBackend(repoRoot, backendChoice) {
|
|
340
192
|
if (backendChoice === BACKEND_MEMORY) {
|
|
341
193
|
return { dir: createInMemoryStore(), close: async () => {} };
|
|
@@ -349,25 +201,13 @@ export async function openMemoryBackend(repoRoot, backendChoice) {
|
|
|
349
201
|
return { dir: repoRoot, close: async () => {} };
|
|
350
202
|
}
|
|
351
203
|
|
|
352
|
-
/** Deep-clone a JSON-safe value
|
|
353
|
-
*
|
|
354
|
-
* itself, so this backend keeps the same "fresh object every call" contract
|
|
355
|
-
* Backend A's JSON.parse(readFile()) always had — nothing outside this
|
|
356
|
-
* module can mutate handle.cachedPayload by mutating what loadMemory
|
|
357
|
-
* returned; and (2) persistSqlitePayload clones a value INTO the cache so
|
|
358
|
-
* the cache never ends up aliasing a piece of the caller's own payload
|
|
359
|
-
* object (which mutateMemory's caller may go on to mutate further). */
|
|
204
|
+
/** Deep-clone a JSON-safe value — keeps every cache read/write from aliasing
|
|
205
|
+
* the caller's own payload object. */
|
|
360
206
|
const cloneJson = (v) => (v === undefined ? v : structuredClone(v));
|
|
361
207
|
|
|
362
|
-
/** The loadMemory-equivalent read for Backend C
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* `handle.cachedPayload`. Every later call, with no write in between, skips
|
|
366
|
-
* SQL entirely and returns a clone of that cache (see the module-comment
|
|
367
|
-
* above SQLITE_DDL for the full mechanism). A brand-new store (no meta rows
|
|
368
|
-
* written yet) reconstructs to the same shape emptyMemory() returns, so a
|
|
369
|
-
* fresh handle behaves like Backend A's ENOENT-bootstrap and Backend B's
|
|
370
|
-
* fresh createInMemoryStore(). */
|
|
208
|
+
/** The loadMemory-equivalent read for Backend C: reconstructs from SQL once
|
|
209
|
+
* per handle (or after a failed write invalidates the cache), then returns a
|
|
210
|
+
* clone of `handle.cachedPayload` with zero SQL. */
|
|
371
211
|
function readSqlitePayload(handle) {
|
|
372
212
|
if (!handle.cachedPayload) handle.cachedPayload = buildSqlitePayloadFromRows(handle);
|
|
373
213
|
return cloneJson(handle.cachedPayload);
|
|
@@ -414,14 +254,8 @@ function buildSqlitePayloadFromRows(handle) {
|
|
|
414
254
|
};
|
|
415
255
|
}
|
|
416
256
|
|
|
417
|
-
// ---- handle.cachedPayload mirrors
|
|
418
|
-
//
|
|
419
|
-
// right beside each call — same condition (only when the SQL write actually
|
|
420
|
-
// runs), same effect, so the cache always ends up holding exactly what a
|
|
421
|
-
// fresh SQL reconstruction would now produce. `cache.individuals`/
|
|
422
|
-
// `cache.objectProperties` are mutated in place; persistSqlitePayload is
|
|
423
|
-
// responsible for invalidating the whole cache on a rolled-back write (these
|
|
424
|
-
// helpers assume the surrounding transaction succeeds).
|
|
257
|
+
// ---- handle.cachedPayload mirrors: applied in lockstep with each SQL write
|
|
258
|
+
// so the cache always matches a fresh SQL reconstruction. ----------------
|
|
425
259
|
|
|
426
260
|
/** Mirrors `INSERT OR REPLACE INTO individuals(...)`: an existing id is
|
|
427
261
|
* replaced IN PLACE (same array position, matching how SQL keeps that row's
|
|
@@ -451,17 +285,10 @@ function cacheGroupFor(cache, prop) {
|
|
|
451
285
|
return g;
|
|
452
286
|
}
|
|
453
287
|
|
|
454
|
-
/** Mirrors `INSERT OR REPLACE INTO edges(...)`:
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* ordering exactly, without a re-SELECT. Rebuilds the cached edge shape the
|
|
459
|
-
* same way buildSqlitePayloadFromRows does (subject/object/labels + any
|
|
460
|
-
* extra keys), from the same `extraKeys` persistSqlitePayload already
|
|
461
|
-
* computed for the SQL `extra` column, so it never re-derives them. Same
|
|
462
|
-
* NUL-delimited (subject,object) key discipline as the SQL diff beside it
|
|
463
|
-
* (a space could be forged by a term that contains one; NUL never occurs in
|
|
464
|
-
* a normalized term). */
|
|
288
|
+
/** Mirrors `INSERT OR REPLACE INTO edges(...)`: a changed/new row sorts LAST
|
|
289
|
+
* under `ORDER BY rowid`, so this moves the entry to the end of `examples`
|
|
290
|
+
* rather than replacing it in place. NUL-delimited (subject,object) key,
|
|
291
|
+
* matching the SQL diff beside it — collision-proof, unlike a space. */
|
|
465
292
|
function cacheUpsertEdge(group, edge, extraKeys) {
|
|
466
293
|
const key = `${edge.subject}\u0000${edge.object}`;
|
|
467
294
|
group.examples = group.examples.filter((e) => `${e.subject}\u0000${e.object}` !== key);
|
|
@@ -485,19 +312,10 @@ function cacheDropGroupsExcept(cache, seenProps) {
|
|
|
485
312
|
cache.objectProperties = cache.objectProperties.filter((g) => seenProps.has(g?.prop));
|
|
486
313
|
}
|
|
487
314
|
|
|
488
|
-
/** Persist a mutated payload into a Backend C handle:
|
|
489
|
-
* INSERT/REPLACE/DELETE
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* never observes (or leaves on disk) a half-applied mutation.
|
|
493
|
-
*
|
|
494
|
-
* Also patches `handle.cachedPayload` (if one exists yet — it always will in
|
|
495
|
-
* practice, since mutateMemory always calls loadMemory before this) in the
|
|
496
|
-
* same lockstep as the SQL diff below, so a subsequent loadMemory() never has
|
|
497
|
-
* to re-query SQLite to see this write (see the module comment above
|
|
498
|
-
* SQLITE_DDL). On a rolled-back write, the cache is invalidated rather than
|
|
499
|
-
* left holding a partially-applied patch — the next read does an honest
|
|
500
|
-
* full rebuild instead. */
|
|
315
|
+
/** Persist a mutated payload into a Backend C handle: per-row
|
|
316
|
+
* INSERT/REPLACE/DELETE diffed against what's already stored, in one
|
|
317
|
+
* transaction. Patches `handle.cachedPayload` in lockstep; a rolled-back
|
|
318
|
+
* write invalidates the cache instead of leaving a partial patch. */
|
|
501
319
|
function persistSqlitePayload(handle, payload) {
|
|
502
320
|
const db = handle.db;
|
|
503
321
|
const empty = emptyMemory();
|
|
@@ -634,42 +452,12 @@ export const DEFAULT_RETENTION = 5;
|
|
|
634
452
|
|
|
635
453
|
const resolveManifestFile = (dir) => join(dir, MEMORY_MANIFEST_REL);
|
|
636
454
|
|
|
637
|
-
/** Snapshot the
|
|
638
|
-
*
|
|
639
|
-
* the
|
|
640
|
-
* the
|
|
641
|
-
*
|
|
642
|
-
* `
|
|
643
|
-
* live file every mutator (mutateMemory / fold.mjs's writeMemoryGraph) reads
|
|
644
|
-
* and writes; only a COPY of its pre-snapshot content becomes the new
|
|
645
|
-
* numbered version. NOT called from mutateMemory, writeMemoryGraph, or
|
|
646
|
-
* anywhere else in this codebase — it has zero callers today by design; a
|
|
647
|
-
* future CLI command or maintenance hook calls it explicitly.
|
|
648
|
-
*
|
|
649
|
-
* Manifest bootstrap (no manifest.json yet): `{ version: 0, retentionVersions:
|
|
650
|
-
* opts.retentionVersions ?? DEFAULT_RETENTION }` — the optional
|
|
651
|
-
* `retentionVersions` lets a caller that already loaded tmct.toml's
|
|
652
|
-
* `[memory] retention_versions` seed the bootstrap default without this
|
|
653
|
-
* module doing its own config I/O (core.mjs has no toml-loading precedent;
|
|
654
|
-
* toml-config.mjs stays the one place that reads tmct.toml). Once a
|
|
655
|
-
* manifest.json exists on disk, ITS retentionVersions is authoritative and a
|
|
656
|
-
* later opts.retentionVersions is ignored (the persisted setting wins over a
|
|
657
|
-
* possibly-stale caller default).
|
|
658
|
-
*
|
|
659
|
-
* "No graph.json exists yet" is handled as a clean no-op: `{ skipped: true,
|
|
660
|
-
* version: null }` — nothing to snapshot is not an error, it is the honest
|
|
661
|
-
* bootstrap state (a brand-new repo that has never written a memory graph).
|
|
662
|
-
*
|
|
663
|
-
* Retention: after writing `graph.v{N}.json` and bumping the manifest to
|
|
664
|
-
* N+1, the snapshot at `graph.v{N - retentionVersions}.json` (if it exists)
|
|
665
|
-
* is deleted (best-effort — ENOENT is swallowed). Using N (the version just
|
|
666
|
-
* written), not N+1, for the prune target keeps a clean sliding window of
|
|
667
|
-
* exactly `retentionVersions` files on disk at all times, with no orphaned
|
|
668
|
-
* v0 ever left behind once the window starts sliding.
|
|
669
|
-
*
|
|
670
|
-
* Returns `{ skipped, version, prunedVersion }` — `version` is the number of
|
|
671
|
-
* the snapshot just written (or null if skipped); `prunedVersion` is the
|
|
672
|
-
* number pruned, or null if nothing was in range to prune yet. */
|
|
455
|
+
/** Snapshot the current live graph.json into a numbered `graph.v{N}.json`,
|
|
456
|
+
* advance the manifest, and best-effort prune the snapshot that falls
|
|
457
|
+
* outside the retention window. graph.json itself is never touched — only a
|
|
458
|
+
* copy becomes the new version. No graph.json yet -> `{ skipped: true }`.
|
|
459
|
+
* Once a manifest exists, its retentionVersions is authoritative over
|
|
460
|
+
* `opts.retentionVersions`. Returns `{ skipped, version, prunedVersion }`. */
|
|
673
461
|
export async function snapshotMemory(dir, { retentionVersions } = {}) {
|
|
674
462
|
if (isMemoryOrSqliteHandle(dir)) {
|
|
675
463
|
throw new Error("snapshotMemory only supports the flat-JSON backend (Backend A) — a memory/sqlite handle has no on-disk graph.json to snapshot");
|
|
@@ -734,12 +522,9 @@ export async function loadMemory(dir) {
|
|
|
734
522
|
return JSON.parse(text);
|
|
735
523
|
}
|
|
736
524
|
|
|
737
|
-
/** Persist a mutated payload back to `dir
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
* assignment is a documented no-op safety net, never I/O. Backend C: a real,
|
|
741
|
-
* diffed per-row INSERT/UPDATE/DELETE against the live connection — see
|
|
742
|
-
* persistSqlitePayload. */
|
|
525
|
+
/** Persist a mutated payload back to `dir`: an atomic file write (Backend A),
|
|
526
|
+
* a no-op assignment (Backend B, already the live object), or a diffed
|
|
527
|
+
* per-row SQL write (Backend C, persistSqlitePayload). */
|
|
743
528
|
async function persistMemory(dir, payload) {
|
|
744
529
|
if (isMemoryHandle(dir)) { dir.payload = payload; return; }
|
|
745
530
|
if (isSqliteHandle(dir)) { persistSqlitePayload(dir, payload); return; }
|
|
@@ -747,43 +532,18 @@ async function persistMemory(dir, payload) {
|
|
|
747
532
|
await atomicWriteJson(memoryGraphFile(dir), payload);
|
|
748
533
|
}
|
|
749
534
|
|
|
750
|
-
/** Fresh read
|
|
751
|
-
* goes through here
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
* `fn` may be async (PLAN_AGENTS.md §2.1's SHACL ingest gate: appendFact/
|
|
759
|
-
* appendRule build their candidate individual, `await assertIndividualValid`
|
|
760
|
-
* it, and only then upsert — all inside `fn`, so a rejection throws before
|
|
761
|
-
* ANY mutation of `payload` happens and this function's write is never
|
|
762
|
-
* reached). `await fn(payload)` is a documented no-op for every existing
|
|
763
|
-
* SYNC caller (appendUtterance(s), appendFacts) — awaiting a non-Promise
|
|
764
|
-
* value just resolves to it, byte-identical behaviour to calling it plain. */
|
|
765
|
-
// ---- mutateMemory-scoped lookup index (PLAN_GRAPH_SCAN.md Phase 1) ----------
|
|
766
|
-
// syncFactSources's per-fact bookkeeping (upsertSource, upsertIndividual,
|
|
767
|
-
// upsertEdge's statedBy path, statedByObjectsFor, sourcesByIdMap) used to each
|
|
768
|
-
// re-scan payload.individuals or the statedBy edge list from scratch, turning
|
|
769
|
-
// one appendFacts batch of n facts into O(n^2) work. mutateMemory now builds
|
|
770
|
-
// three lookup Maps once per call (one O(n) pass) and attaches them to payload
|
|
771
|
-
// under a Symbol key — JSON.stringify skips Symbol-keyed properties
|
|
772
|
-
// automatically, so persistMemory's graph.json write is byte-identical to
|
|
773
|
-
// before. Every helper below checks for the Symbol slot: present → O(1) Map
|
|
774
|
-
// lookup; absent (a bare payload object built outside mutateMemory, e.g. a
|
|
775
|
-
// test fixture) → today's exact linear-scan fallback, so nothing outside
|
|
776
|
-
// mutateMemory's own call chain can observe a behaviour change. The index is
|
|
777
|
-
// discarded when mutateMemory returns — it never survives across calls, so
|
|
778
|
-
// there is no invalidation logic to get wrong.
|
|
535
|
+
/** Fresh read -> mutate -> atomic write. Serialized per call; every public
|
|
536
|
+
* append goes through here, including the lazy legacy-provenance migration
|
|
537
|
+
* and actor-level Source reliability recompute. `fn` may be async (the
|
|
538
|
+
* SHACL ingest gate awaits validation before ever mutating `payload`). */
|
|
539
|
+
// Per-call lookup index (individualsById/sourcesById/statedByBySubject),
|
|
540
|
+
// attached to payload under a Symbol key (skipped by JSON.stringify) so
|
|
541
|
+
// upsertIndividual/upsertSource/upsertEdge/appendFacts get O(1) lookups
|
|
542
|
+
// instead of re-scanning; discarded when mutateMemory returns.
|
|
779
543
|
const MEMORY_INDEX = Symbol("mutateMemory lookup index");
|
|
780
544
|
|
|
781
545
|
/** Build the three lookup Maps from the just-loaded payload and attach them
|
|
782
|
-
* under MEMORY_INDEX.
|
|
783
|
-
* payload.individuals, or a new statedBy edge, must also write the matching
|
|
784
|
-
* index entry in that same statement (see upsertIndividual/upsertSource/
|
|
785
|
-
* upsertEdge/appendFacts below) — the same discipline appendFacts's own
|
|
786
|
-
* local `byId` Map already used for the Fact upsert, generalised here. */
|
|
546
|
+
* under MEMORY_INDEX. */
|
|
787
547
|
function buildMemoryIndex(payload) {
|
|
788
548
|
const individualsById = new Map();
|
|
789
549
|
const sourcesById = new Map();
|
|
@@ -839,27 +599,17 @@ function setAttr(ind, prop, key, value) {
|
|
|
839
599
|
|
|
840
600
|
// ---- Sources (step (b)): first-class provenance individuals -----------------
|
|
841
601
|
|
|
842
|
-
/** Deterministic Source id + type over the closed kind set
|
|
843
|
-
* unknown kind
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
* (unconditional — every real chat/teach provenance tag carries one; see
|
|
847
|
-
* provenanceTagToSource): `${OPERATOR_SOURCE_ID}:<sessionId>` /
|
|
848
|
-
* `${TEACH_SOURCE_ID}:<sessionId>` instead of the bare singleton, so each
|
|
849
|
-
* session's operator/teach facts attach to their OWN Source individual
|
|
850
|
-
* rather than every session ever collapsing onto one. Session ids are
|
|
851
|
-
* uuidv7s (hex + hyphens only — see uuid.mjs), so `:`/`@` never collide with
|
|
852
|
-
* this id scheme's own delimiters. */
|
|
602
|
+
/** Deterministic Source id + type over the closed kind set; null for an
|
|
603
|
+
* unknown kind. operator/teach are session-scoped when `desc.sessionId` is
|
|
604
|
+
* present, so each session gets its own Source rather than collapsing onto
|
|
605
|
+
* one singleton. */
|
|
853
606
|
function sourceIdFor(desc) {
|
|
854
607
|
switch (desc?.kind) {
|
|
855
608
|
case "operator": return { id: desc.sessionId ? `${OPERATOR_SOURCE_ID}:${desc.sessionId}` : OPERATOR_SOURCE_ID, type: "operator" };
|
|
856
609
|
case "teach": return { id: desc.sessionId ? `${TEACH_SOURCE_ID}:${desc.sessionId}` : TEACH_SOURCE_ID, type: "teach" };
|
|
857
610
|
case "provider": return { id: `src:provider:${desc.name}`, type: "provider" };
|
|
858
611
|
case "corpus": return { id: `src:corpus:${desc.name}`, type: "corpus" };
|
|
859
|
-
// One Source per source-file basename
|
|
860
|
-
// extraction run — re-running scripts/extract-facts-from-text.mjs over the
|
|
861
|
-
// SAME file collapses onto the same Source instead of minting a new one
|
|
862
|
-
// every time, matching corpus's "one Source per named dataset" idiom.
|
|
612
|
+
// One Source per source-file basename, not per extraction run.
|
|
863
613
|
case "extracted": return { id: `src:extracted:${desc.name}`, type: "extracted" };
|
|
864
614
|
case "web": return { id: `src:learned:web:${fnv1aHex(String(desc.url || ""))}`, type: "web", url: String(desc.url || "") };
|
|
865
615
|
case "entailed": return { id: `src:entailed:${desc.rule}`, type: "entailed", rule: String(desc.rule || "") };
|
|
@@ -894,12 +644,8 @@ function upsertSource(payload, desc, createdAtCandidate) {
|
|
|
894
644
|
return info.id;
|
|
895
645
|
}
|
|
896
646
|
|
|
897
|
-
/** Parse the "chat" shape both provenanceTag
|
|
898
|
-
*
|
|
899
|
-
* their kind prefix has already been stripped — into { createdAt, sessionId? }.
|
|
900
|
-
* `sessionId` is present only when the tag actually carried one (every real
|
|
901
|
-
* chat/teach write does; a hand-authored/legacy tag without one degrades to
|
|
902
|
-
* the bare singleton Source, honestly — see sourceIdFor). */
|
|
647
|
+
/** Parse the "chat" shape both provenanceTag and teachProvenanceTag emit —
|
|
648
|
+
* `<source>[:<sessionId>][@<ts>]` — into { createdAt, sessionId? }. */
|
|
903
649
|
function parseChatTagRest(rest) {
|
|
904
650
|
const at = rest.indexOf("@");
|
|
905
651
|
const beforeAt = at >= 0 ? rest.slice(0, at) : rest;
|
|
@@ -910,30 +656,16 @@ function parseChatTagRest(rest) {
|
|
|
910
656
|
}
|
|
911
657
|
|
|
912
658
|
/**
|
|
913
|
-
* Parse one legacy provenance TAG into a Source descriptor over the closed
|
|
914
|
-
* set
|
|
915
|
-
*
|
|
916
|
-
* corpus:conceptnet /r/
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* chat:/session: refs map to the operator; an unknown tag → null (no Source).
|
|
924
|
-
* `corpus-weak:` is the SAME corpus provenance shape as `corpus:`, just naming
|
|
925
|
-
* a lower trust-prior kind (memory/trust.mjs SOURCE_PRIOR.corpusWeak) for
|
|
926
|
-
* facts whose underlying relation is real but low-precision (e.g. ConceptNet's
|
|
927
|
-
* undirected /r/RelatedTo) — trust stays computed from the Source's kind, never
|
|
928
|
-
* hand-set on the Fact.
|
|
929
|
-
* The session-id segment (Part B: session-scoped actor-level trust) feeds
|
|
930
|
-
* sourceIdFor, which mints a PER-SESSION Source id when present, instead of
|
|
931
|
-
* collapsing every session onto one singleton operator/teach Source.
|
|
932
|
-
* `extracted:` is scripts/extract-facts-from-text.mjs's own audit tag, layered
|
|
933
|
-
* ADDITIVELY (via appendFact's provenance union) on top of whatever the
|
|
934
|
-
* runTurn recognizer already wrote (ace:/teach:) — see that script for why:
|
|
935
|
-
* it distinguishes "this document evidenced this fact" from ordinary chat
|
|
936
|
-
* speech, at its own trust-prior tier (memory/trust.mjs SOURCE_PRIOR.extracted).
|
|
659
|
+
* Parse one legacy provenance TAG into a Source descriptor over the closed
|
|
660
|
+
* kind set:
|
|
661
|
+
* corpus:conceptnet /r/IsA -> { kind:"corpus", name:"conceptnet" }
|
|
662
|
+
* corpus-weak:conceptnet /r/RelatedTo -> { kind:"corpusWeak", name:"conceptnet" }
|
|
663
|
+
* ace:chat:<session>@<ts> -> { kind:"operator", createdAt:<ts>, sessionId:<session> }
|
|
664
|
+
* teach:chat:<session>@<ts> -> { kind:"teach", createdAt:<ts>, sessionId:<session> }
|
|
665
|
+
* web:<url> | url:<url> -> { kind:"web", url:<url> }
|
|
666
|
+
* extracted:<file-basename> -> { kind:"extracted", name:<file-basename> }
|
|
667
|
+
* entailed:<rule> -> { kind:"entailed", rule:<rule> }
|
|
668
|
+
* chat:/session: refs map to the operator; an unknown tag -> null (no Source).
|
|
937
669
|
*/
|
|
938
670
|
export function provenanceTagToSource(tag) {
|
|
939
671
|
const t = String(tag || "").trim();
|
|
@@ -978,17 +710,9 @@ function statedByObjectsFor(payload, factId) {
|
|
|
978
710
|
return (g?.examples || []).filter((e) => e?.subject === factId).map((e) => e.object);
|
|
979
711
|
}
|
|
980
712
|
|
|
981
|
-
/** Recompute + materialise a Fact's trust cache (mgx:trustScore
|
|
982
|
-
* mgx:
|
|
983
|
-
*
|
|
984
|
-
* `premiseTrusts`/`ruleConfidence`) — threaded through from appendFact/
|
|
985
|
-
* appendFacts's own opts so a rule (e.g. syllogise.mjs's cax-dw) can make its
|
|
986
|
-
* conclusion's trust premise-derived (`min(premiseTrusts) × ruleConfidence`)
|
|
987
|
-
* instead of riding the bare entailed prior. Absent (the default, `{}`), this
|
|
988
|
-
* is a no-op passthrough — every existing caller's score is byte-identical
|
|
989
|
-
* (PLAN_INFERENCE_TESTING.md §4 stage 2's exit criterion). Also stamps
|
|
990
|
-
* `mgx:updatedAt` (PLAN_VIZ.md §2) — this mutates the Fact's own attributes in place without
|
|
991
|
-
* necessarily touching an edge, so the derived "max over edges" updatedAt rule can't see it. */
|
|
713
|
+
/** Recompute + materialise a Fact's trust cache (mgx:trustScore/mgx:trustInputs)
|
|
714
|
+
* and stamp mgx:updatedAt. `trustOpts` optionally threads the entailed hook's
|
|
715
|
+
* premiseTrusts/ruleConfidence through from appendFact/appendFacts. */
|
|
992
716
|
function recomputeFactTrust(payload, fact, nowMs = Date.now(), trustOpts = {}) {
|
|
993
717
|
const sourceIds = statedByObjectsFor(payload, fact.id);
|
|
994
718
|
const createdAt = (fact.attributes || []).find((a) => a?.prop === CREATED_AT_PROP)?.value || "";
|
|
@@ -1054,33 +778,11 @@ const isSessionScopedSourceId = (id) =>
|
|
|
1054
778
|
|
|
1055
779
|
/**
|
|
1056
780
|
* Recompute + materialise mgx:sourceReliability on every session-scoped
|
|
1057
|
-
* operator/teach Source
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
* Contradiction membership is evaluated against the CURRENT trust scores at
|
|
1064
|
-
* the point this runs (already materialised by this same mutation's
|
|
1065
|
-
* syncFactSources/migrateLegacyProvenance calls) — it does NOT recursively
|
|
1066
|
-
* re-evaluate contradictions after reliability changes shift trust scores
|
|
1067
|
-
* (no fixed-point iteration; one pass is enough for a monotonic, self-
|
|
1068
|
-
* correcting signal that only ever gets more accurate on the NEXT write).
|
|
1069
|
-
*
|
|
1070
|
-
* Every individual (Fact OR RULE) touched by a recomputed Source then has its
|
|
1071
|
-
* OWN trust re-materialised (recomputeFactTrust — class-agnostic, same as
|
|
1072
|
-
* syncFactSources: neither ever checks `.class`) so mgx:trustScore reflects
|
|
1073
|
-
* the fresh reliability within THIS SAME mutation cycle — a session's
|
|
1074
|
-
* reliability shift is visible immediately, not just on some future
|
|
1075
|
-
* unrelated re-write. This refresh is scanned off the statedBy edge group
|
|
1076
|
-
* DIRECTLY (every individual it names, any class), not off readFactRows
|
|
1077
|
-
* (Fact-only) — a Rule can never be "contradicted" (findContradictions is a
|
|
1078
|
-
* Fact-shape concept, so contradiction ACCOUNTING stays Fact-scoped above),
|
|
1079
|
-
* but it rides the identical Source-derivation + trust pipeline a Fact does
|
|
1080
|
-
* (appendRule's own doc comment), so it must not go stale here either.
|
|
1081
|
-
*
|
|
1082
|
-
* Called from mutateMemory itself (below), riding every mutation's existing
|
|
1083
|
-
* bookkeeping cycle — not a separate write path.
|
|
781
|
+
* operator/teach Source: count facts stated vs. contradicted
|
|
782
|
+
* (findContradictions), run sessionReliabilityFrom, write the bounded result.
|
|
783
|
+
* One pass, no fixed-point iteration. Every individual (Fact or Rule) a
|
|
784
|
+
* recomputed Source touches then gets its own trust re-materialised
|
|
785
|
+
* (recomputeFactTrust) so the shift is visible within this same mutation.
|
|
1084
786
|
*/
|
|
1085
787
|
function recomputeSourceReliability(payload) {
|
|
1086
788
|
if (!Array.isArray(payload?.individuals) || !Array.isArray(payload?.objectProperties)) return;
|
|
@@ -1105,7 +807,7 @@ function recomputeSourceReliability(payload) {
|
|
|
1105
807
|
const source = idx ? idx.individualsById.get(sid) : payload.individuals.find((i) => i?.id === sid);
|
|
1106
808
|
if (!source) continue;
|
|
1107
809
|
setAttr(source, SOURCE_RELIABILITY_PROP, "sourceReliability", String(sessionReliabilityFrom(counts)));
|
|
1108
|
-
// Own-attribute mutation in place
|
|
810
|
+
// Own-attribute mutation in place — same reasoning as recomputeFactTrust.
|
|
1109
811
|
setAttr(source, UPDATED_AT_PROP, "updatedAt", new Date().toISOString());
|
|
1110
812
|
}
|
|
1111
813
|
|
|
@@ -1121,16 +823,7 @@ function recomputeSourceReliability(payload) {
|
|
|
1121
823
|
}
|
|
1122
824
|
|
|
1123
825
|
/** Upsert an individual by id (replace-in-place keeps ordering stable).
|
|
1124
|
-
* Returns the
|
|
1125
|
-
* caller (e.g. upsertSource) should index THAT reference, not `ind` itself,
|
|
1126
|
-
* since the indexed path below merges into the prior object in place rather
|
|
1127
|
-
* than replacing the array slot. When a lookup index is present (built by
|
|
1128
|
-
* mutateMemory), an existing individual is updated via Object.assign — same
|
|
1129
|
-
* array position AND same object identity as before, so it stays trivially
|
|
1130
|
-
* in sync with individualsById without a second Map write, and a brand-new
|
|
1131
|
-
* individual is pushed + indexed in the same statement. Absent an index
|
|
1132
|
-
* (a bare payload built outside mutateMemory), this is EXACTLY the original
|
|
1133
|
-
* findIndex + replace-or-push code. */
|
|
826
|
+
* Returns the stored reference — callers should index THAT, not `ind`. */
|
|
1134
827
|
function upsertIndividual(payload, ind) {
|
|
1135
828
|
const idx = memoryIndexOf(payload);
|
|
1136
829
|
if (idx) {
|
|
@@ -1160,8 +853,8 @@ function upsertEdge(payload, { predicate, prop }, edge) {
|
|
|
1160
853
|
group = { predicate, prop, count: 0, examples: [] };
|
|
1161
854
|
payload.objectProperties.push(group);
|
|
1162
855
|
}
|
|
1163
|
-
// statedBy-only fast path
|
|
1164
|
-
//
|
|
856
|
+
// statedBy-only fast path: statedByBySubject tracks, per fact, the small
|
|
857
|
+
// list of Source ids already stated it (almost
|
|
1165
858
|
// always 0-1 during a seed), so the overwhelmingly common case — a brand
|
|
1166
859
|
// new (subject,object) statedBy pair — can append directly without the
|
|
1167
860
|
// find+filter scan of the WHOLE statedBy edge list below. Every other
|
|
@@ -1294,23 +987,10 @@ export async function appendUtterances(dir, utterances) {
|
|
|
1294
987
|
}
|
|
1295
988
|
|
|
1296
989
|
/** Normalize a fact TERM (subject/object) so every writer converges on one
|
|
1297
|
-
* spelling
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
* Tier-5 playtest fix (2026-07-09): also strips a leading "the"/"a"/"an" —
|
|
1303
|
-
* found live via "remember that THE logger module is deprecated" (teach-side
|
|
1304
|
-
* already stripped it before this ran, so storage was unaffected) followed by
|
|
1305
|
-
* "what do you know about THE logger module" / "who maintains THE tasks
|
|
1306
|
-
* handler" (recall-side queries, which do NOT pre-strip their own captured
|
|
1307
|
-
* term before calling this) genuinely missing the just-taught fact — every
|
|
1308
|
-
* recall regex in chat.mjs (KNOW_ABOUT_RE, WHO_OWNS_RE, ISA_ASK_RE, …) calls
|
|
1309
|
-
* factTermVariants -> normFactTerm on the raw captured term, so fixing it
|
|
1310
|
-
* ONCE here closes the gap for all of them instead of patching each site.
|
|
1311
|
-
* Safe for storage too (idempotent — an already-stripped subject is
|
|
1312
|
-
* unaffected); the article is a determiner, never semantically distinguishing
|
|
1313
|
-
* for a code-entity or common-noun term in this domain. */
|
|
990
|
+
* spelling: ConceptNet's /c/en/foo_bar, tmct:Foo_bar, and bare "Foo bar" all
|
|
991
|
+
* become "foo bar". Also strips a leading "the"/"a"/"an" (idempotent — safe
|
|
992
|
+
* for storage too). The predicate is never normalized this way — its casing
|
|
993
|
+
* is meaningful controlled vocabulary. */
|
|
1314
994
|
export function normFactTerm(t) {
|
|
1315
995
|
let s = normText(t);
|
|
1316
996
|
s = s.replace(/^\/c\/[a-z]{2,3}\//i, "");
|
|
@@ -1320,39 +1000,24 @@ export function normFactTerm(t) {
|
|
|
1320
1000
|
return s.toLowerCase();
|
|
1321
1001
|
}
|
|
1322
1002
|
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
// one). appendFact hashes the SAME `${s}\0${p}\0${o}` inline; appendFacts routes
|
|
1327
|
-
// through here so the batch path can never drift to a space and silently re-key
|
|
1328
|
-
// every seeded fact — the golden-equivalence test pins the two paths together.
|
|
1003
|
+
// A Fact is content-addressed by its NUL-delimited (s, p, o) — NUL never
|
|
1004
|
+
// occurs in a normalized term/predicate, so it's collision-proof unlike a
|
|
1005
|
+
// space. appendFact hashes the same `${s}\0${p}\0${o}` inline; keep both in sync.
|
|
1329
1006
|
const factIdFor = (s, p, o) => `fact:${fnv1aHex(`${s}\0${p}\0${o}`)}`;
|
|
1330
1007
|
|
|
1331
|
-
/** Content-address a fact's id from
|
|
1332
|
-
* writing it —
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* syllogise.mjs's justification-tracking retraction machinery, PLAN_SYLLOGIST.md
|
|
1336
|
-
* §3) can compute it deterministically without an extra read — ids are
|
|
1337
|
-
* content-addressed, never sequence-assigned, so this is safe to call
|
|
1338
|
-
* before, instead of, or in place of an actual append. Pure, no I/O. */
|
|
1008
|
+
/** Content-address a fact's id from (subject, predicate, object) without
|
|
1009
|
+
* writing it — same contract as factIdFor. Lets a caller (e.g.
|
|
1010
|
+
* syllogise.mjs's retraction machinery) name a not-yet-written fact's id
|
|
1011
|
+
* deterministically, without an extra read. Pure, no I/O. */
|
|
1339
1012
|
export function factIdForTriple(subject, predicate, object) {
|
|
1340
1013
|
return factIdFor(normFactTerm(subject), normText(predicate), normFactTerm(object));
|
|
1341
1014
|
}
|
|
1342
1015
|
|
|
1343
|
-
/** Append one grammar-derived OWL triple, RDF-reified
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
* syllogise.mjs) passes these, a plain taught/asserted write omits them and
|
|
1349
|
-
* is byte-identical to before.
|
|
1350
|
-
*
|
|
1351
|
-
* PLAN_AGENTS.md 2.1's SHACL ingest gate: the candidate Fact individual is
|
|
1352
|
-
* validated against ontology/memory-shapes.ttl (memory/shacl.mjs) BEFORE
|
|
1353
|
-
* upsertIndividual runs -- a violation throws here, inside mutateMemory's
|
|
1354
|
-
* `fn`, so the write never happens (mutateMemory's atomic write is never
|
|
1355
|
-
* reached; the on-disk graph is untouched). Returns { id }. */
|
|
1016
|
+
/** Append one grammar-derived OWL triple, RDF-reified as a `Fact` individual.
|
|
1017
|
+
* Same (s,p,o) -> same id -> upsert, never a duplicate. `premiseTrusts`/
|
|
1018
|
+
* `ruleConfidence` optionally engage trust.mjs's entailed hook. Validated
|
|
1019
|
+
* against ontology/memory-shapes.ttl (memory/shacl.mjs) before the write.
|
|
1020
|
+
* Returns { id }. */
|
|
1356
1021
|
export async function appendFact(dir, { subject, predicate, object, provenance = "", createdAt = "", quantifier = "", premiseTrusts, ruleConfidence } = {}) {
|
|
1357
1022
|
const s = normFactTerm(subject);
|
|
1358
1023
|
const p = normText(predicate);
|
|
@@ -1397,35 +1062,13 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
1397
1062
|
return { id };
|
|
1398
1063
|
}
|
|
1399
1064
|
|
|
1400
|
-
/** Batch append of grammar/corpus-derived triples — ONE read-modify-write for
|
|
1401
|
-
* whole seed
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
* mutateMemory upserts each Fact through an id→individual Map (O(1) upsert, so
|
|
1408
|
-
* the growing individuals array is never rescanned per fact), reconciles each
|
|
1409
|
-
* touched fact's Sources + trust via the SAME syncFactSources appendFact uses,
|
|
1410
|
-
* and recountClasses ONCE at the end. The result is deep-equal (modulo array
|
|
1411
|
-
* order) to looping appendFact: same fact ids, same mgx:factProvenance union,
|
|
1412
|
-
* same statedBy Source edges, same mgx:trustScore, same first-write-wins
|
|
1413
|
-
* createdAt. Malformed facts (missing subject/predicate/object) are SKIPPED (a
|
|
1414
|
-
* bad row never aborts a 6 k-fact seed), not thrown as appendFact does.
|
|
1415
|
-
* Each fact may also carry `premiseTrusts`/`ruleConfidence` (optional) —
|
|
1416
|
-
* appendFact's own entailed-hook passthrough, batched: syllogise.mjs's
|
|
1417
|
-
* materializing pass is this function's main caller, so this is the write
|
|
1418
|
-
* path a rule's conclusion trust actually rides (recomputeFactTrust, above).
|
|
1419
|
-
* A fact may also carry `justification` (optional, array of premise fact
|
|
1420
|
-
* ids — PLAN_SYLLOGIST.md §3's persisted-justification step): stored
|
|
1421
|
-
* verbatim as `mgx:factJustification` (space-joined; fact ids never contain
|
|
1422
|
-
* a space), last-write-wins per id (a re-derivation via a DIFFERENT premise
|
|
1423
|
-
* pair, after the original was retracted and this fact re-earned its place
|
|
1424
|
-
* some other way, should overwrite the stale justification, not keep it) —
|
|
1425
|
-
* never written at all for a plain taught/asserted fact (`justification`
|
|
1426
|
-
* omitted), which stays byte-identical to before this field existed.
|
|
1427
|
-
* Returns { ids, appended, skipped } — ids one per applied fact (in order),
|
|
1428
|
-
* appended = ids.length, skipped = malformed count. */
|
|
1065
|
+
/** Batch append of grammar/corpus-derived triples — ONE read-modify-write for
|
|
1066
|
+
* a whole seed, collapsing looping appendFact's O(N²) I/O to a single
|
|
1067
|
+
* mutate (same resulting ids/provenance/trust). Malformed facts are skipped,
|
|
1068
|
+
* not thrown. Optional per-fact `premiseTrusts`/`ruleConfidence` (batched
|
|
1069
|
+
* entailed-hook passthrough) and `justification` (premise fact ids, stored
|
|
1070
|
+
* as mgx:factJustification, last-write-wins). Returns
|
|
1071
|
+
* { ids, appended, skipped }. */
|
|
1429
1072
|
export async function appendFacts(dir, facts) {
|
|
1430
1073
|
const prepared = [];
|
|
1431
1074
|
let skipped = 0;
|
|
@@ -1515,7 +1158,7 @@ export async function appendFacts(dir, facts) {
|
|
|
1515
1158
|
return { ids, appended: ids.length, skipped };
|
|
1516
1159
|
}
|
|
1517
1160
|
|
|
1518
|
-
// ---- Rules
|
|
1161
|
+
// ---- Rules ------------------------------------------------------------------
|
|
1519
1162
|
// A taught RULE — a composed/filtered/recursive relation SHAPE, distinct from a
|
|
1520
1163
|
// plain Fact triple. Same convention as a Fact's subject/predicate/object: every
|
|
1521
1164
|
// slot is a plain string ATTRIBUTE, never an edge to a per-term individual.
|
|
@@ -1529,50 +1172,25 @@ export const RULE_NAME_PROP = "mgx:ruleName";
|
|
|
1529
1172
|
export const RULE_KIND_PROP = "mgx:ruleKind";
|
|
1530
1173
|
|
|
1531
1174
|
// Per-kind slot contract: JS slot key -> the mgx: attribute it's written under.
|
|
1532
|
-
// filter's "base" slot deliberately reuses ruleBase1 (not a fresh "ruleBase")
|
|
1533
|
-
//
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
// slot2) order the content-address hash below uses — fixed and load-bearing.
|
|
1175
|
+
// filter's "base" slot deliberately reuses ruleBase1 (not a fresh "ruleBase")
|
|
1176
|
+
// — the same attribute name compose2's first hop already uses, since both
|
|
1177
|
+
// play the identical "base relation this rule builds on" role. Order within
|
|
1178
|
+
// each array is the (slot1, slot2) order the content-address hash below
|
|
1179
|
+
// uses — fixed and load-bearing.
|
|
1538
1180
|
const RULE_SLOT_SPEC = {
|
|
1539
1181
|
[RULE_KIND_COMPOSE2]: [["base1", "mgx:ruleBase1"], ["base2", "mgx:ruleBase2"]],
|
|
1540
1182
|
[RULE_KIND_FILTER]: [["base", "mgx:ruleBase1"], ["property", "mgx:ruleFilterProperty"]],
|
|
1541
1183
|
[RULE_KIND_RECURSIVE]: [["baseCase", "mgx:ruleBaseCase"], ["recStep", "mgx:ruleRecStep"]],
|
|
1542
1184
|
};
|
|
1543
1185
|
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
// IDENTICAL rule (same kind + name + slots) upserts, never duplicates; teaching
|
|
1547
|
-
// a DIFFERENT rule under the SAME name (different slots) hashes to a distinct
|
|
1548
|
-
// id — both individuals exist side by side, the same way two different Facts
|
|
1549
|
-
// sharing a subject are two distinct Fact individuals, never a silent overwrite.
|
|
1186
|
+
// Content-addressed over (kind, name, slot1, slot2), mirroring factIdFor's
|
|
1187
|
+
// NUL-delimited discipline: identical rules upsert, different ones coexist.
|
|
1550
1188
|
const ruleIdFor = (kind, name, slot1, slot2) => `rule:${fnv1aHex(`${kind}\0${name}\0${slot1}\0${slot2}`)}`;
|
|
1551
1189
|
|
|
1552
1190
|
/** Append one taught RULE (compose2 | filter | recursive) — a sibling of
|
|
1553
|
-
* appendFact
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
* upsert convention as appendFact.
|
|
1557
|
-
*
|
|
1558
|
-
* { name, kind, slots, provenance = "", createdAt = "" }: `kind` is the ONE
|
|
1559
|
-
* closed vocabulary this store needs to know — compose2 | filter | recursive,
|
|
1560
|
-
* three STRUCTURAL tags describing the SHAPE of what was taught (never a
|
|
1561
|
-
* domain word, the same way "Fact"/"Rule" describe the store's own shape, not
|
|
1562
|
-
* what's stored in it). `slots` is the matching per-kind object (RULE_SLOT_SPEC
|
|
1563
|
-
* above). `name` and every slot value are normFactTerm-normalized, exactly like
|
|
1564
|
-
* a Fact's subject/object.
|
|
1565
|
-
*
|
|
1566
|
-
* Provenance/trust ride the EXACT SAME syncFactSources/recomputeFactTrust
|
|
1567
|
-
* pipeline appendFact uses, unmodified — neither function ever checks
|
|
1568
|
-
* `individual.class`, so a Rule carrying the same mgx:factProvenance compat
|
|
1569
|
-
* attribute + CREATED_AT_PROP gets the same Source-derivation + trust score an
|
|
1570
|
-
* ordinary Fact would.
|
|
1571
|
-
*
|
|
1572
|
-
* PLAN_AGENTS.md 2.1's SHACL ingest gate: the candidate Rule individual is
|
|
1573
|
-
* validated against ontology/memory-shapes.ttl (memory/shacl.mjs) BEFORE
|
|
1574
|
-
* upsertIndividual runs, same discipline as appendFact -- a violation
|
|
1575
|
-
* throws before mutateMemory's write is ever reached. Returns { id }. */
|
|
1191
|
+
* appendFact storing a `Rule` individual, same upsert/provenance/trust/SHACL
|
|
1192
|
+
* discipline (neither pipeline ever checks `individual.class`). `slots` is
|
|
1193
|
+
* the matching per-kind object (RULE_SLOT_SPEC above). Returns { id }. */
|
|
1576
1194
|
export async function appendRule(dir, { name, kind, slots, provenance = "", createdAt = "" } = {}) {
|
|
1577
1195
|
const spec = RULE_SLOT_SPEC[kind];
|
|
1578
1196
|
if (!spec) throw new Error(`a rule kind must be one of ${RULE_KINDS.join(", ")}, got ${JSON.stringify(kind)}`);
|
|
@@ -1614,13 +1232,11 @@ export async function appendRule(dir, { name, kind, slots, provenance = "", crea
|
|
|
1614
1232
|
return { id };
|
|
1615
1233
|
}
|
|
1616
1234
|
|
|
1617
|
-
/** Genericity lookup for the
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
* actual kind-dispatch (compose2/filter/recursive branching) on top of this.
|
|
1623
|
-
* Returns the raw individual, or undefined if no Rule has that name. */
|
|
1235
|
+
/** Genericity lookup for the query-dispatcher: "what kind of thing is name X"
|
|
1236
|
+
* — scan for the Rule individual whose mgx:ruleName matches, the SAME
|
|
1237
|
+
* lookup serving every taught rule name uniformly (no per-rule-name
|
|
1238
|
+
* branch). Returns the raw individual, or undefined if no Rule has that
|
|
1239
|
+
* name. */
|
|
1624
1240
|
export function findRuleByName(memory, name) {
|
|
1625
1241
|
const n = normFactTerm(name);
|
|
1626
1242
|
return (memory?.individuals || []).find(
|
|
@@ -1628,41 +1244,25 @@ export function findRuleByName(memory, name) {
|
|
|
1628
1244
|
);
|
|
1629
1245
|
}
|
|
1630
1246
|
|
|
1631
|
-
// ---- Relation chase
|
|
1632
|
-
// PLAN_TAUGHT_RELATIONS.md Phase 2/4/5; PLAN_COMPLETIONS.md Stage 1
|
|
1633
|
-
// prerequisite) --------------------------------------------------------------
|
|
1247
|
+
// ---- Relation chase ---------------------------------------------------------
|
|
1634
1248
|
//
|
|
1635
|
-
// `resolveRelationChase` and `resolveRelationChaseReverse`
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
// `
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
// closures they used to capture are now explicit parameters: `memory` (an
|
|
1643
|
-
// already-loaded loadMemory() payload — callers load it once, not per
|
|
1644
|
-
// recursive call) and a `helpers` bag carrying every chat.mjs-local piece
|
|
1645
|
-
// they relied on (`relationFactsFor`, `renderFactLine`, `factPhrase`,
|
|
1646
|
-
// `factTermVariants`, `byTrust`, the trust-bearing `rows` array, and
|
|
1647
|
-
// `HAS_PROPERTY_PREDICATE`). No other chat.mjs coupling remains — dynamic
|
|
1648
|
-
// imports of this file's own findRuleByName/RULE_KIND_* and of
|
|
1649
|
-
// planning.mjs's findActionPath/findReachableSet are now direct references/
|
|
1650
|
-
// static imports, since both now live alongside or are reachable from here
|
|
1651
|
-
// without a cycle (planning.mjs imports nothing).
|
|
1249
|
+
// `resolveRelationChase` and `resolveRelationChaseReverse` are standalone,
|
|
1250
|
+
// importable functions taking an already-loaded `memory` (a loadMemory()
|
|
1251
|
+
// payload — callers load it once, not per recursive call) and a `helpers`
|
|
1252
|
+
// bag (`relationFactsFor`, `renderFactLine`, `factPhrase`, `factTermVariants`,
|
|
1253
|
+
// `byTrust`, the trust-bearing `rows` array, and `HAS_PROPERTY_PREDICATE`),
|
|
1254
|
+
// so callers outside chat.mjs's own dispatch context can reuse the same
|
|
1255
|
+
// resolution logic.
|
|
1652
1256
|
//
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
//
|
|
1257
|
+
// Dispatch order: direct/alias fact hit → compose2 rule chase → filter rule
|
|
1258
|
+
// chase → honest miss (OWA discipline: null / [] on a miss, never a guessed
|
|
1259
|
+
// "no").
|
|
1656
1260
|
|
|
1657
1261
|
/**
|
|
1658
|
-
* RELATION CHASE
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
* list), (iii) a hop-counted compose2 Rule chase (exactly 2 hops: base1 then
|
|
1663
|
-
* base2), or (iv) a filter Rule chase (recursively resolve the base, then
|
|
1664
|
-
* require the subject also carry the taught property). Returns
|
|
1665
|
-
* `{ citation: string[] }` on a genuine hit, or null on an honest miss.
|
|
1262
|
+
* RELATION CHASE — given a relation/rule NAME and a fixed (subject, object)
|
|
1263
|
+
* pair, resolve whether it holds via (i) a direct taught fact, (ii) an
|
|
1264
|
+
* alias-chased predicate, (iii) a 2-hop compose2 Rule chase, or (iv) a filter
|
|
1265
|
+
* Rule chase. Returns `{ citation: string[] }` on a hit, null on an honest miss.
|
|
1666
1266
|
*/
|
|
1667
1267
|
export async function resolveRelationChase(memory, name, subjectTerm, objectTerm, helpers) {
|
|
1668
1268
|
const { relationFactsFor, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE } = helpers;
|
|
@@ -1736,13 +1336,9 @@ export async function resolveRelationChase(memory, name, subjectTerm, objectTerm
|
|
|
1736
1336
|
}
|
|
1737
1337
|
|
|
1738
1338
|
/**
|
|
1739
|
-
* RELATION "WHO" REVERSE CHASE
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
* yes/no for a fixed (subject, object) pair. Recursion is bounded the SAME way
|
|
1743
|
-
* resolveRelationChase's own filter chase is: a filter rule's base is always
|
|
1744
|
-
* either a plain relation (terminal) or another rule (one level deeper), never
|
|
1745
|
-
* itself.
|
|
1339
|
+
* RELATION "WHO" REVERSE CHASE — the mirror image of resolveRelationChase:
|
|
1340
|
+
* given a relation/rule name and a fixed OBJECT, return every
|
|
1341
|
+
* `{ subject, citation }` pair that satisfies it, instead of one yes/no.
|
|
1746
1342
|
*/
|
|
1747
1343
|
export async function resolveRelationChaseReverse(memory, name, objectTerm, helpers) {
|
|
1748
1344
|
const { relationFactsFor, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE } = helpers;
|
|
@@ -1868,33 +1464,21 @@ export function readFactRows(memory) {
|
|
|
1868
1464
|
id: ind.id,
|
|
1869
1465
|
subject: get("subject"), predicate: get("predicate"), object: get("object"),
|
|
1870
1466
|
provenance: get("provenance"), // legacy compat string, verbatim
|
|
1871
|
-
quantifier: get("quantifier"), // "" unless a plural class-membership teach set one
|
|
1467
|
+
quantifier: get("quantifier"), // "" unless a plural class-membership teach set one
|
|
1872
1468
|
sourceIds, sourceTypes,
|
|
1873
1469
|
trust: Number((ind.attributes || []).find((a) => a?.prop === TRUST_SCORE_PROP)?.value) || 0,
|
|
1874
|
-
// [] unless a rule persisted its premise fact ids (
|
|
1875
|
-
//
|
|
1470
|
+
// [] unless a rule persisted its premise fact ids (justification-tracking,
|
|
1471
|
+
// scm-sco only today; see syllogise.mjs).
|
|
1876
1472
|
justification: justificationRaw ? justificationRaw.split(" ").filter(Boolean) : [],
|
|
1877
1473
|
});
|
|
1878
1474
|
}
|
|
1879
1475
|
return rows;
|
|
1880
1476
|
}
|
|
1881
1477
|
|
|
1882
|
-
/**
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
* build, nothing in this file actually implemented: un-believing something used
|
|
1887
|
-
* to mean re-running the whole batch pass and hoping dedup naturally sorted it
|
|
1888
|
-
* out, with no targeted removal at all. Drops each matching Fact individual and
|
|
1889
|
-
* scrubs any edge group (`statedBy`, etc.) that referenced it as subject OR
|
|
1890
|
-
* object, so no dangling edge survives the delete — then recounts classes once.
|
|
1891
|
-
* A Source left with zero remaining statedBy edges is NOT itself deleted (an
|
|
1892
|
-
* orphaned Source individual is harmless — it materialises nothing and costs
|
|
1893
|
-
* nothing to leave — so this stays a pure, minimal retraction, not a GC pass).
|
|
1894
|
-
* Ids that don't resolve to a live Fact are silently skipped (an idempotent,
|
|
1895
|
-
* honest no-op — never an error: a caller may retry a retraction against a
|
|
1896
|
-
* concurrently-mutated store). Returns { removed } — the ids ACTUALLY deleted,
|
|
1897
|
-
* a possibly-smaller set than the input. */
|
|
1478
|
+
/** Retract Fact individuals by id — a real DELETE (syllogise.mjs's
|
|
1479
|
+
* retractability mechanism). Scrubs any edge referencing the id as subject
|
|
1480
|
+
* or object; an orphaned Source is left in place (not a GC pass). Unknown
|
|
1481
|
+
* ids are silently skipped. Returns { removed } (may be smaller than input). */
|
|
1898
1482
|
export async function removeFacts(dir, ids) {
|
|
1899
1483
|
const idSet = new Set((ids || []).filter(Boolean));
|
|
1900
1484
|
const removed = [];
|
|
@@ -1920,13 +1504,9 @@ export async function removeFacts(dir, ids) {
|
|
|
1920
1504
|
* contradiction (below it the fact is too weak to contradict anything). */
|
|
1921
1505
|
export const CONTRADICTION_TRUST_FLOOR = 0.5;
|
|
1922
1506
|
|
|
1923
|
-
/**
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
* answer/inspection layer surfaces BOTH with their provenance and NEVER silently
|
|
1927
|
-
* picks the higher-trust one. Same (s,p,o) from two writers is corroboration,
|
|
1928
|
-
* not contradiction — one Fact id, N statedBy edges — so it never appears here.
|
|
1929
|
-
*/
|
|
1507
|
+
/** Facts that CONTRADICT: same (subject, predicate), different object, each
|
|
1508
|
+
* above the trust floor. Returns groups (trust-desc) so callers surface both,
|
|
1509
|
+
* never silently pick one. Same (s,p,o) is corroboration, not contradiction. */
|
|
1930
1510
|
export function findContradictions(memory, { floor = CONTRADICTION_TRUST_FLOOR } = {}) {
|
|
1931
1511
|
const rows = readFactRows(memory).filter((r) => r.trust >= floor);
|
|
1932
1512
|
const byKey = new Map();
|