@polycode-projects/the-mechanical-code-talker 0.9.12 → 1.0.1
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/corpus/seon/relations.jsonl +1 -0
- package/package.json +1 -1
- package/src/ask-vocab.mjs +25 -2
- package/src/ask.mjs +67 -11
- package/src/chat.mjs +889 -66
- package/src/concept.mjs +50 -9
- package/src/finish.mjs +1 -1
- package/src/interpret/normalize.mjs +90 -1
- package/src/memory/core.mjs +14 -1
package/src/concept.mjs
CHANGED
|
@@ -249,6 +249,14 @@ export const RELATION_TERM = Object.freeze({
|
|
|
249
249
|
define: "defines", defines: "defines", defining: "defines", defined: "defines", definition: "defines", definitions: "defines", declaration: "defines",
|
|
250
250
|
touch: "touches", touches: "touches", touching: "touches", touched: "touches",
|
|
251
251
|
cochange: "cochange", "co-change": "cochange", "change-coupling": "cochange", coupled: "cochange",
|
|
252
|
+
// "export"/"exports" is ALSO a curated seon lexicon noun (corpus/seon/definitions.jsonl
|
|
253
|
+
// "export"), same shape as "imports" — but that meta reading only owns the "what
|
|
254
|
+
// does export mean"/"what is an export" shape; vagueTouchTermOf/relationTermOf are
|
|
255
|
+
// deliberately scoped to the NON-meta "what about X"/"tell me about X" touch (see
|
|
256
|
+
// relationTermOf's own docblock, frozen case am-meta-imports), so no conflict here.
|
|
257
|
+
export: "reexports", exports: "reexports", exporting: "reexports", exported: "reexports",
|
|
258
|
+
reexport: "reexports", reexports: "reexports", reexporting: "reexports",
|
|
259
|
+
"re-export": "reexports", "re-exports": "reexports", "re-exporting": "reexports",
|
|
252
260
|
});
|
|
253
261
|
|
|
254
262
|
/** concept key → the relationKind()s whose edges it enumerates. A concept can span
|
|
@@ -263,6 +271,7 @@ const RELATION_KINDS = Object.freeze({
|
|
|
263
271
|
defines: ["defines"],
|
|
264
272
|
touches: ["touches", "touchesSymbol"],
|
|
265
273
|
cochange: ["cochange"],
|
|
274
|
+
reexports: ["reexports"],
|
|
266
275
|
});
|
|
267
276
|
|
|
268
277
|
/** concept key → the verb phrase that renders an edge as an English sentence
|
|
@@ -276,6 +285,7 @@ const RELATION_RENDER = Object.freeze({
|
|
|
276
285
|
defines: { verb: "defines", edgeNoun: "definition" },
|
|
277
286
|
touches: { verb: "touches", edgeNoun: "touch" },
|
|
278
287
|
cochange: { verb: "changes together with", edgeNoun: "change-coupling" },
|
|
288
|
+
reexports: { verb: "re-exports", edgeNoun: "re-export" },
|
|
279
289
|
});
|
|
280
290
|
|
|
281
291
|
/** Per concept key, the candidate follow-up shapes in priority order. Each shape
|
|
@@ -316,6 +326,10 @@ const RELATION_FOLLOWUP_SHAPES = Object.freeze({
|
|
|
316
326
|
{ side: "obj", make: (x) => `where is ${x} defined` },
|
|
317
327
|
{ side: "subj", make: (x) => `which modules import ${x}` },
|
|
318
328
|
],
|
|
329
|
+
reexports: [
|
|
330
|
+
{ side: "subj", make: (x) => `what does ${x} export` },
|
|
331
|
+
{ side: "obj", make: (x) => `where is ${x} defined` },
|
|
332
|
+
],
|
|
319
333
|
});
|
|
320
334
|
|
|
321
335
|
/** How many example edges the relation force shows before the remainder is held for
|
|
@@ -347,12 +361,24 @@ function buildRelationFollowups(graph, key, subjLabels, objLabels) {
|
|
|
347
361
|
}
|
|
348
362
|
|
|
349
363
|
/** Compose the three bands for a RELATION concept term, or null when it is NOT a
|
|
350
|
-
* relation-force case — the term is not a known enumerable relation, has no
|
|
351
|
-
* definition
|
|
352
|
-
* fabricated edge). Returns the same string-band shape composeConcept does:
|
|
364
|
+
* relation-force case at all — the term is not a known enumerable relation, or has no
|
|
365
|
+
* curated definition. Returns the same string-band shape composeConcept does:
|
|
353
366
|
* { definition, examples, followups, followupQueries, remainder, noun }
|
|
354
|
-
* `examples` is always non-empty when non-null
|
|
355
|
-
*
|
|
367
|
+
* `examples` is always non-empty when non-null; `followups` is "" when no validated
|
|
368
|
+
* next-question exists.
|
|
369
|
+
*
|
|
370
|
+
* A known relation whose graph has ZERO edges of that kind is NOT null — it degrades
|
|
371
|
+
* to a two-band answer (the definition + an explicit "this codebase has no X edges"
|
|
372
|
+
* line, `examples`-shaped so the caller renders it identically). Found live (an
|
|
373
|
+
* advisor tick on the 0.9.14 Tier-2 playtest cycle): returning null here for the
|
|
374
|
+
* zero-edge case let the caller's OWN raw grammar attempt at the vague-touch text
|
|
375
|
+
* ("what about exports", "tell me about reexports") stand instead — but that text was
|
|
376
|
+
* never meant to be parsed as an object search, so on a graph with no reexports edges
|
|
377
|
+
* (the realistic case for most repos, e.g. examples/mini-webapp) it fell through to a
|
|
378
|
+
* garbled `no module matching "about"/"exports" found`, not an honest miss. A relation
|
|
379
|
+
* kind the graph has NEVER SEEN AT ALL (relationKind returns nothing in RELATION_KINDS
|
|
380
|
+
* for this graph's shape) still degrades the same way — the definition is always
|
|
381
|
+
* worth stating; only the fabricated edge is refused. */
|
|
356
382
|
export function composeRelation(graph, relTerm, { definition = null } = {}) {
|
|
357
383
|
const key = RELATION_TERM[String(relTerm || "").toLowerCase()];
|
|
358
384
|
if (!key || !definition) return null;
|
|
@@ -360,14 +386,29 @@ export function composeRelation(graph, relTerm, { definition = null } = {}) {
|
|
|
360
386
|
const groups = (graph && Array.isArray(graph.relations) ? graph.relations : [])
|
|
361
387
|
.filter((g) => kinds.includes(relationKind(g)));
|
|
362
388
|
const edges = groups.flatMap((g) => (Array.isArray(g.edges) ? g.edges : []));
|
|
363
|
-
if (!edges.length) return null; // no edges of this kind → honest miss stands
|
|
364
|
-
const total = groups.reduce((s, g) => s + (Number(g.count) || (g.edges || []).length), 0);
|
|
365
|
-
|
|
366
389
|
const { verb, edgeNoun } = RELATION_RENDER[key] || { verb: key, edgeNoun: key };
|
|
367
390
|
|
|
368
|
-
// BAND 1 — the fact (the relation defined as a verb/relationship).
|
|
391
|
+
// BAND 1 — the fact (the relation defined as a verb/relationship). Stated
|
|
392
|
+
// regardless of whether the graph has any edges of this kind — only the
|
|
393
|
+
// fabricated EXAMPLE is refused when there are none.
|
|
369
394
|
const bandDefinition = leadSentence(definition);
|
|
370
395
|
|
|
396
|
+
if (!edges.length) {
|
|
397
|
+
// No fabricated edge, but an honest, ON-TOPIC miss — never the caller's raw,
|
|
398
|
+
// unrelated object-search text.
|
|
399
|
+
return {
|
|
400
|
+
definition: bandDefinition,
|
|
401
|
+
examples: `This codebase has no ${edgeNoun} edges in the index.`,
|
|
402
|
+
followups: "",
|
|
403
|
+
followupQueries: [],
|
|
404
|
+
relation: key,
|
|
405
|
+
remainder: [],
|
|
406
|
+
noun: `${edgeNoun} edges`,
|
|
407
|
+
empty: true,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const total = groups.reduce((s, g) => s + (Number(g.count) || (g.edges || []).length), 0);
|
|
411
|
+
|
|
371
412
|
// BAND 2 — the example edges, rendered as English sentences with a count. The
|
|
372
413
|
// first MAX_EDGE_EXAMPLES are shown; the remainder is held for "more" pagination.
|
|
373
414
|
const rendered = edges.map((e) => `${edgeSubjectLabel(e)} ${verb} ${edgeObjectLabel(e)}`);
|
package/src/finish.mjs
CHANGED
|
@@ -222,7 +222,7 @@ function leadingWord(text) {
|
|
|
222
222
|
|
|
223
223
|
/** Does `word` begin with a VOWEL SOUND? true → "an", false → "a", null → cannot
|
|
224
224
|
* tell (refuse). Spelling-vs-sound exceptions come from the rule's TOML row. */
|
|
225
|
-
function beginsWithVowelSound(word, rule) {
|
|
225
|
+
export function beginsWithVowelSound(word, rule) {
|
|
226
226
|
const w = String(word).toLowerCase().replace(/^[^a-z]+/, "");
|
|
227
227
|
if (!w) return null;
|
|
228
228
|
for (const ex of rule.vowel_sound_consonants || []) if (w.startsWith(ex)) return true;
|
|
@@ -44,6 +44,19 @@ const correctionRe = (table) => new RegExp(
|
|
|
44
44
|
const MISSPELLING_RE = correctionRe(MISSPELLINGS);
|
|
45
45
|
const WRONG_WORD_RE = correctionRe(WRONG_WORDS);
|
|
46
46
|
|
|
47
|
+
/** Just the curated MISSPELLINGS correction (ask-vocab.mjs), standalone — for a
|
|
48
|
+
* caller that needs typo-tolerant ANCHOR-WORD matching (a closed regex shape
|
|
49
|
+
* keyed on a literal "what"/"which"/"where" etc.) without running the rest of
|
|
50
|
+
* normalizeQuery's pipeline (contractions, preamble/subordination/conditional
|
|
51
|
+
* frame rewrites, filler-word stripping) — those can restructure the sentence
|
|
52
|
+
* in ways a shape-matcher never expects (0.9.14 Tier-2 playtest: normalizeQuery
|
|
53
|
+
* turns "tell me about Controller" into "about Controller", which would break
|
|
54
|
+
* chat.mjs's OWN "^tell me about …" shape regex if fed through wholesale). Pure,
|
|
55
|
+
* idempotent, same table as normalizeQuery's own first correction step. */
|
|
56
|
+
export function correctMisspellings(text) {
|
|
57
|
+
return String(text || "").replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
// ---- closed PREAMBLE frames (0.8.2 feel wave, PLAN_CHAT_FEEL item 2) — the
|
|
48
61
|
// conversational wrapping a developer puts AROUND a real question: a greeting
|
|
49
62
|
// lead-in with a delimiter ("hey there, quick question - …"), a thanks lead-in
|
|
@@ -88,7 +101,25 @@ const isListingRemainder = (rest) => {
|
|
|
88
101
|
* remainder are REQUIRED, so a bare "hey there" stays a greeting for chat's
|
|
89
102
|
* conversational lane, and "hey tmct, …" (a vocative, no delimiter after the
|
|
90
103
|
* greeting word) is left for the noise-strip tier that already owns it. */
|
|
91
|
-
|
|
104
|
+
// "g'day"/"gday" (AU/NZ dialect, §3b): the same lead-in-with-delimiter shape as
|
|
105
|
+
// hi/hey/howdy — 0.9.14 Tier-2 playtest §3b spot-check found "g'day, what does
|
|
106
|
+
// Base contain" fell through to a bogus "'g'day Base' matches more than one
|
|
107
|
+
// module" object search instead of stripping the greeting.
|
|
108
|
+
// "good morning"/"good afternoon"/"good evening"/"good day"/"greetings"/
|
|
109
|
+
// "salutations" (formal register, §3b) — chat.mjs's own bare-turn GREETINGS/
|
|
110
|
+
// IDENTITY_PHRASES closed sets already recognize these exact phrases stand-
|
|
111
|
+
// alone, but this LEAD-IN regex (a greeting fused onto a real question in the
|
|
112
|
+
// same turn) didn't carry the multi-word formal forms at all: a second Tier-2
|
|
113
|
+
// playtest pass (0.9.14) found "Good day, what is a method" hit the grammar
|
|
114
|
+
// wall outright, and "Good morning, what about tests" fell through to a bogus
|
|
115
|
+
// "no module matching 'Good morning' found" object search — the exact same
|
|
116
|
+
// failure g'day had before its own fix, just for the formal register instead
|
|
117
|
+
// of the AU/NZ dialect. Formal register also plausibly types the lead-in as
|
|
118
|
+
// its OWN full sentence ("Good day. What is a method?") rather than a comma
|
|
119
|
+
// splice — "." joins the delimiter class for exactly this reason; a bare
|
|
120
|
+
// greeting alone ("hi.") still can't match since the regex also requires a
|
|
121
|
+
// non-empty remainder AFTER the delimiter.
|
|
122
|
+
const GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy|g'?day|good\s+(?:morning|afternoon|evening|day)|greetings|salutations)(?:\s+there)?\s*[,.—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
|
|
92
123
|
/** Thanks lead-in with a delimiter (+ optional "quick question" bridge), the
|
|
93
124
|
* sibling of GREETING_PREAMBLE_RE for the "thanks" word family (Bug B2, 0.8.2
|
|
94
125
|
* follow-up): "thanks, <Q>" / "thanks so much, <Q>" -> "<Q>". chat.mjs's
|
|
@@ -107,6 +138,17 @@ const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(
|
|
|
107
138
|
* passes, so "can you tell me a joke" -> "tell me a joke" -> (FILLER) "a joke"
|
|
108
139
|
* — byte-identical to what the bare form normalizes to (the hm-joke wall). */
|
|
109
140
|
const MODAL_WRAPPER_RE = /^(?:can|could|would|will)\s+you\s+(?:please\s+)?(.+?)(?:[,\s]+please)?\??$/i;
|
|
141
|
+
/** "explain" politeness/ESL wrapper: "explain [to me|please]* <Q>" -> "<Q>"
|
|
142
|
+
* (0.9.13 Tier-1 playtest, §3b surface-variation axis) — a formal/ESL lead-in
|
|
143
|
+
* around an ordinary structural question ("explain please where is it
|
|
144
|
+
* defined") used to leave "explain"/"please" as noise words that corrupted the
|
|
145
|
+
* object term into a bogus search ("no module matching 'explain it' found").
|
|
146
|
+
* Anchored to an INTERROGATIVE remainder (same guard as the show/give-me
|
|
147
|
+
* bridge below) so this frame only unwraps a real WH-question underneath —
|
|
148
|
+
* chat.mjs's own IDENTITY_PHRASES ("explain what is this") and the bare
|
|
149
|
+
* "explain" elaboration request (WHY set) are matched on the RAW turn text
|
|
150
|
+
* before normalizeQuery ever runs, so neither is touched by this frame. */
|
|
151
|
+
const EXPLAIN_WRAPPER_RE = /^explain\s+(?:to\s+me\s+|please\s+)*(.+?)\??$/i;
|
|
110
152
|
/** show/give-me presentation bridge: "show me [the] <thing>". Three-way:
|
|
111
153
|
* a KIND-listing remainder is left untouched (the compositional list grammar
|
|
112
154
|
* owns "show me untested modules"); a remainder carrying a relation verb or an
|
|
@@ -129,6 +171,8 @@ export function applyPreambleFrames(text) {
|
|
|
129
171
|
if (m) q = m[1].trim();
|
|
130
172
|
m = q.match(MODAL_WRAPPER_RE);
|
|
131
173
|
if (m) q = m[1].trim();
|
|
174
|
+
m = q.match(EXPLAIN_WRAPPER_RE);
|
|
175
|
+
if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
|
|
132
176
|
m = q.match(SHOW_GIVE_ME_RE);
|
|
133
177
|
if (m) {
|
|
134
178
|
const rest = m[1].trim();
|
|
@@ -192,6 +236,36 @@ export function applySubordinationFrames(text) {
|
|
|
192
236
|
return q;
|
|
193
237
|
}
|
|
194
238
|
|
|
239
|
+
// ---- SELF-CORRECTION (found live, playtest sprint round 5, HANDOVER item 12.1): a
|
|
240
|
+
// mid-sentence false start, abandoned and restarted — "what -- sorry, who inherits
|
|
241
|
+
// from Record", "what is a class -- i mean, what is a module". Same species as the
|
|
242
|
+
// preamble/subordination frames above (closed, delimiter-anchored, first-match-wins,
|
|
243
|
+
// unmatched text passes through byte-unchanged), sized for a false-start clause
|
|
244
|
+
// instead of a leading wrapper clause. The delimiter is the marker phrase "sorry" or
|
|
245
|
+
// "i mean" — REQUIRED, with an optional interruption dash ("--"/"—"/"-") in front of
|
|
246
|
+
// it and a required trailing separator (dash/comma/colon) after it. Deliberately does
|
|
247
|
+
// NOT match on a bare dash alone with no marker word: an ordinary em-dash aside
|
|
248
|
+
// ("modules — like Base — that inherit from X") is common prose, not a restart, and
|
|
249
|
+
// treating every dash as a delimiter would be a guess this file's discipline forbids
|
|
250
|
+
// everywhere else. ----
|
|
251
|
+
const SELF_CORRECTION_RE =
|
|
252
|
+
/^.+?(?:\s*(?:--|—|-)\s*)?\b(?:sorry|i\s+mean)\b\s*(?:--|—|-|,|:)\s*(.+)$/i;
|
|
253
|
+
|
|
254
|
+
/** Apply the self-correction strip to a small fixpoint (a stacked restart —
|
|
255
|
+
* "what -- sorry, who -- sorry, what inherits from Record" — peels to the final
|
|
256
|
+
* restart). Pure; unmatched text passes through byte-unchanged. */
|
|
257
|
+
export function applySelfCorrectionFrames(text) {
|
|
258
|
+
let q = String(text || "");
|
|
259
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
260
|
+
const m = q.match(SELF_CORRECTION_RE);
|
|
261
|
+
if (!m) break;
|
|
262
|
+
const next = m[1].trim();
|
|
263
|
+
if (!next || next === q) break;
|
|
264
|
+
q = next;
|
|
265
|
+
}
|
|
266
|
+
return q;
|
|
267
|
+
}
|
|
268
|
+
|
|
195
269
|
/** relation-verb (bare 3rd-person singular, the RELATIONS table's own primary
|
|
196
270
|
* form) -> gerund, the shape the compositional grammar's proven
|
|
197
271
|
* "<kind> <gerund> <object> and <qualifier>" pattern needs. A small, closed,
|
|
@@ -274,6 +348,12 @@ export function normalizeQuery(text) {
|
|
|
274
348
|
// bridge) — AFTER the correction tables (a repaired "give me"/"show me" still
|
|
275
349
|
// feeds the bridge) but BEFORE the filler strip erases their anchor words.
|
|
276
350
|
q = applyPreambleFrames(q);
|
|
351
|
+
// self-correction (strip an abandoned false-start clause) BEFORE subordination/
|
|
352
|
+
// conditional: a false start can itself look like the OPENING of a subordination
|
|
353
|
+
// clause ("since -- sorry, which modules import X" — "since" would otherwise be
|
|
354
|
+
// read as SUBORDINATION_FRAMES_RE's own anchor), so peeling the restart first
|
|
355
|
+
// means the real remainder is all either frame ever sees.
|
|
356
|
+
q = applySelfCorrectionFrames(q);
|
|
277
357
|
// subordination (strip a leading framing clause) THEN conditional (compile
|
|
278
358
|
// "if …" to an existing working shape) — subordination first so a stacked
|
|
279
359
|
// "since we're refactoring, if a module imports X, is it tested" peels its
|
|
@@ -342,6 +422,15 @@ export const PHRASING_FRAMES = Object.freeze([
|
|
|
342
422
|
{ re: /^what\s+(?:defined|declared)\s+(?:the\s+)?(?:function\s+|method\s+|class\s+|module\s+|variable\s+|constant\s+)?(.+?)\??$/i, to: (m) => `where is ${m[1]} defined` },
|
|
343
423
|
// "where's X defined" (the "where's" contraction is not in the contraction table)
|
|
344
424
|
{ re: /^where'?s\s+(?:the\s+)?(.+?)\s+(defined|declared|located|implemented)\??$/i, to: (m) => `where is ${m[1]} ${m[2]}` },
|
|
425
|
+
// "were is X defined" (0.9.13 Tier-1 playtest: the missing-h typo of "where").
|
|
426
|
+
// NOT curated as a blanket MISSPELLINGS entry — "were" is a real word already
|
|
427
|
+
// load-bearing as the TEMPORAL_AUX auxiliary ("when were the modules last
|
|
428
|
+
// touched"), so a global word-boundary rewrite would clobber that reading.
|
|
429
|
+
// This frame is anchored to the WHERE-DEFINED shape specifically ("were is
|
|
430
|
+
// … defined/declared/located/implemented"), a construction no legitimate
|
|
431
|
+
// temporal query produces ("were" as an auxiliary never leads directly into
|
|
432
|
+
// a bare "is").
|
|
433
|
+
{ re: /^were\s+is\s+(?:the\s+)?(.+?)\s+(defined|declared|located|implemented)\??$/i, to: (m) => `where is ${m[1]} ${m[2]}` },
|
|
345
434
|
|
|
346
435
|
// PREDICATIVE QUALIFIER → the ATTRIBUTIVE form the grammar already answers. The
|
|
347
436
|
// adjective-qualifier post-filters (ask-vocab.mjs QUALIFIERS: tested/untested,
|
package/src/memory/core.mjs
CHANGED
|
@@ -74,6 +74,7 @@ const MEMORY_VOCABULARY = [
|
|
|
74
74
|
{ prop: "rdf:predicate", note: "reified fact: the triple's predicate term" },
|
|
75
75
|
{ prop: "rdf:object", note: "reified fact: the triple's object term" },
|
|
76
76
|
{ prop: "mgx:factProvenance", note: "LEGACY COMPAT SHIM: the ' | '-joined provenance tag string a fact came from; the source-of-truth is now the mgx:statedBy edges derived from it" },
|
|
77
|
+
{ prop: "mgx:factQuantifier", note: "OPTIONAL: the quantifier word a plural class-membership teach used ('every'/'some'/'a few'), for literal recall by 'how many Xs are Ys' — never real cardinality counting" },
|
|
77
78
|
{ prop: CREATED_AT_PROP, note: "when an individual was FIRST written, ISO-8601 (first-write-wins on upsert); the audit 'when', the recency input to trust, the novelty signal" },
|
|
78
79
|
{ prop: DERIVED_FROM_PROP, predicate: "derivedFrom", note: "umbrella: a Fact derived from a Source (or another Fact). ext ref prov:wasDerivedFrom (UNVERIFIED-pending-web-check)" },
|
|
79
80
|
{ prop: STATED_BY_PROP, predicate: "statedBy", note: "subPropertyOf derivedFrom: a Source directly asserts this Fact (one edge per independent source — replaces the factProvenance union)" },
|
|
@@ -444,7 +445,7 @@ const factIdFor = (s, p, o) => `fact:${fnv1aHex(`${s}\0${p}\0${o}`)}`;
|
|
|
444
445
|
* carrying rdf:subject / rdf:predicate / rdf:object (+ provenance). The
|
|
445
446
|
* Phase-2 ACE parser's write point. Same (s,p,o) → same id → upsert, never a
|
|
446
447
|
* duplicate. Returns { id }. */
|
|
447
|
-
export async function appendFact(dir, { subject, predicate, object, provenance = "", createdAt = "" } = {}) {
|
|
448
|
+
export async function appendFact(dir, { subject, predicate, object, provenance = "", createdAt = "", quantifier = "" } = {}) {
|
|
448
449
|
const s = normFactTerm(subject);
|
|
449
450
|
const p = normText(predicate);
|
|
450
451
|
const o = normFactTerm(object);
|
|
@@ -452,6 +453,7 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
452
453
|
const id = `fact:${fnv1aHex(`${s}${p}${o}`)}`;
|
|
453
454
|
const text = `${s} ${p} ${o}`;
|
|
454
455
|
const tokens = proseTokensFor({ doc: text });
|
|
456
|
+
const q = normText(quantifier);
|
|
455
457
|
await mutateMemory(dir, (payload) => {
|
|
456
458
|
const prior = payload.individuals.find((x) => x?.id === id);
|
|
457
459
|
const priorProv = prior?.attributes?.find((a) => a?.prop === "mgx:factProvenance")?.value || "";
|
|
@@ -459,6 +461,10 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
459
461
|
// still key on); the Source edges below are DERIVED from it, purely additive.
|
|
460
462
|
const provs = [...new Set([...priorProv.split(" | "), normText(provenance)].filter(Boolean))];
|
|
461
463
|
const createdAtVal = firstWriteCreatedAt(prior, createdAt); // first-write-wins
|
|
464
|
+
// first-write-wins for the quantifier too (a re-assert with none, e.g. a
|
|
465
|
+
// plain re-teach, never SILENTLY erases an already-recorded quantifier).
|
|
466
|
+
const priorQ = prior?.attributes?.find((a) => a?.prop === "mgx:factQuantifier")?.value || "";
|
|
467
|
+
const qVal = q || priorQ;
|
|
462
468
|
upsertIndividual(payload, {
|
|
463
469
|
id, label: labelOf(text), class: FACT_CLASS,
|
|
464
470
|
derived_from: [], mentions: [],
|
|
@@ -470,6 +476,7 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
470
476
|
{ prop: CREATED_AT_PROP, key: "createdAt", value: createdAtVal },
|
|
471
477
|
...(provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []),
|
|
472
478
|
...(tokens.length ? [{ prop: "mgx:hasProseTokens", key: "prose_tokens", value: tokens.join(" ") }] : []),
|
|
479
|
+
...(qVal ? [{ prop: "mgx:factQuantifier", key: "quantifier", value: qVal }] : []),
|
|
473
480
|
],
|
|
474
481
|
});
|
|
475
482
|
// Derive Source individuals + statedBy edges from the provenance union and
|
|
@@ -512,6 +519,7 @@ export async function appendFacts(dir, facts) {
|
|
|
512
519
|
tokens: proseTokensFor({ doc: text }),
|
|
513
520
|
provenance: normText(f?.provenance),
|
|
514
521
|
createdAt: f?.createdAt || "",
|
|
522
|
+
quantifier: normText(f?.quantifier),
|
|
515
523
|
});
|
|
516
524
|
}
|
|
517
525
|
const ids = [];
|
|
@@ -528,6 +536,9 @@ export async function appendFacts(dir, facts) {
|
|
|
528
536
|
// compat shim); the Source edges below are DERIVED from it, purely additive.
|
|
529
537
|
const provs = [...new Set([...priorProv.split(" | "), f.provenance].filter(Boolean))];
|
|
530
538
|
const createdAtVal = firstWriteCreatedAt(prior, f.createdAt); // first-write-wins
|
|
539
|
+
// first-write-wins for the quantifier too — same discipline as appendFact.
|
|
540
|
+
const priorQ = prior?.attributes?.find((a) => a?.prop === "mgx:factQuantifier")?.value || "";
|
|
541
|
+
const qVal = f.quantifier || priorQ;
|
|
531
542
|
const ind = {
|
|
532
543
|
id: f.id, label: labelOf(f.text), class: FACT_CLASS,
|
|
533
544
|
derived_from: [], mentions: [],
|
|
@@ -539,6 +550,7 @@ export async function appendFacts(dir, facts) {
|
|
|
539
550
|
{ prop: CREATED_AT_PROP, key: "createdAt", value: createdAtVal },
|
|
540
551
|
...(provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []),
|
|
541
552
|
...(f.tokens.length ? [{ prop: "mgx:hasProseTokens", key: "prose_tokens", value: f.tokens.join(" ") }] : []),
|
|
553
|
+
...(qVal ? [{ prop: "mgx:factQuantifier", key: "quantifier", value: qVal }] : []),
|
|
542
554
|
],
|
|
543
555
|
};
|
|
544
556
|
// Upsert into BOTH the array (replace-in-place keeps order) and the index.
|
|
@@ -588,6 +600,7 @@ export function readFactRows(memory) {
|
|
|
588
600
|
id: ind.id,
|
|
589
601
|
subject: get("subject"), predicate: get("predicate"), object: get("object"),
|
|
590
602
|
provenance: get("provenance"), // legacy compat string, verbatim
|
|
603
|
+
quantifier: get("quantifier"), // "" unless a plural class-membership teach set one (Feature A pt.3)
|
|
591
604
|
sourceIds, sourceTypes,
|
|
592
605
|
trust: Number((ind.attributes || []).find((a) => a?.prop === TRUST_SCORE_PROP)?.value) || 0,
|
|
593
606
|
});
|