@polycode-projects/the-mechanical-code-talker 0.6.0 → 0.7.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 +31 -3
- package/ROADMAP.md +45 -0
- package/bin/tmct.mjs +8 -2
- package/corpus/conceptnet/README.md +59 -52
- package/corpus/conceptnet/filter-dump.mjs +59 -2
- package/corpus/conceptnet/slice.jsonl +31067 -0
- package/corpus/seon/relations.jsonl +8 -0
- package/data/templates/responses.jsonl +3 -2
- package/package.json +6 -3
- package/src/ask.mjs +2 -2
- package/src/chat.mjs +306 -39
- package/src/concept.mjs +393 -0
- package/src/corpus/conceptnet.mjs +18 -13
- package/src/init.mjs +12 -9
- package/src/interpret/normalize.mjs +42 -0
- package/src/interpret/pipeline.mjs +2 -2
- package/src/memory/core.mjs +84 -0
- package/src/tui/app.mjs +26 -2
package/src/memory/core.mjs
CHANGED
|
@@ -425,6 +425,14 @@ export function normFactTerm(t) {
|
|
|
425
425
|
return s.toLowerCase();
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
// The fact-id contract: a Fact is content-addressed by its NUL-DELIMITED
|
|
429
|
+
// (s, p, o). NUL never occurs in a normalized term or a predicate URI, so it is
|
|
430
|
+
// a collision-proof separator (a space could be forged by a term that contains
|
|
431
|
+
// one). appendFact hashes the SAME `${s}\0${p}\0${o}` inline; appendFacts routes
|
|
432
|
+
// through here so the batch path can never drift to a space and silently re-key
|
|
433
|
+
// every seeded fact — the golden-equivalence test pins the two paths together.
|
|
434
|
+
const factIdFor = (s, p, o) => `fact:${fnv1aHex(`${s}\0${p}\0${o}`)}`;
|
|
435
|
+
|
|
428
436
|
/** Append one grammar-derived OWL triple, RDF-reified: a `Fact` individual
|
|
429
437
|
* carrying rdf:subject / rdf:predicate / rdf:object (+ provenance). The
|
|
430
438
|
* Phase-2 ACE parser's write point. Same (s,p,o) → same id → upsert, never a
|
|
@@ -465,6 +473,82 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
465
473
|
return { id };
|
|
466
474
|
}
|
|
467
475
|
|
|
476
|
+
/** Batch append of grammar/corpus-derived triples — ONE read-modify-write for a
|
|
477
|
+
* whole seed (the appendUtterances precedent, for facts). The per-fact
|
|
478
|
+
* appendFact does a full read → mutate → prose-reindex → atomic-write PER FACT,
|
|
479
|
+
* so seeding N facts is O(N²) I/O (6 k facts ≈ 7 min); this collapses it to a
|
|
480
|
+
* single mutate.
|
|
481
|
+
*
|
|
482
|
+
* Every fact is normalized + prose-tokenized OUTSIDE the mutate, then a SINGLE
|
|
483
|
+
* mutateMemory upserts each Fact through an id→individual Map (O(1) upsert, so
|
|
484
|
+
* the growing individuals array is never rescanned per fact), reconciles each
|
|
485
|
+
* touched fact's Sources + trust via the SAME syncFactSources appendFact uses,
|
|
486
|
+
* and recountClasses ONCE at the end. The result is deep-equal (modulo array
|
|
487
|
+
* order) to looping appendFact: same fact ids, same mgx:factProvenance union,
|
|
488
|
+
* same statedBy Source edges, same mgx:trustScore, same first-write-wins
|
|
489
|
+
* createdAt. Malformed facts (missing subject/predicate/object) are SKIPPED (a
|
|
490
|
+
* bad row never aborts a 6 k-fact seed), not thrown as appendFact does.
|
|
491
|
+
* Returns { ids, appended, skipped } — ids one per applied fact (in order),
|
|
492
|
+
* appended = ids.length, skipped = malformed count. */
|
|
493
|
+
export async function appendFacts(dir, facts) {
|
|
494
|
+
const prepared = [];
|
|
495
|
+
let skipped = 0;
|
|
496
|
+
for (const f of facts || []) {
|
|
497
|
+
const s = normFactTerm(f?.subject);
|
|
498
|
+
const p = normText(f?.predicate);
|
|
499
|
+
const o = normFactTerm(f?.object);
|
|
500
|
+
if (!s || !p || !o) { skipped += 1; continue; } // batch skips, never throws
|
|
501
|
+
const text = `${s} ${p} ${o}`;
|
|
502
|
+
prepared.push({
|
|
503
|
+
id: factIdFor(s, p, o), // NUL-delimited — byte-identical to appendFact's id
|
|
504
|
+
s, p, o, text,
|
|
505
|
+
tokens: proseTokensFor({ doc: text }),
|
|
506
|
+
provenance: normText(f?.provenance),
|
|
507
|
+
createdAt: f?.createdAt || "",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const ids = [];
|
|
511
|
+
if (!prepared.length) return { ids, appended: 0, skipped };
|
|
512
|
+
await mutateMemory(dir, (payload) => {
|
|
513
|
+
// id → individual index for O(1) upsert (the array grows to thousands).
|
|
514
|
+
const byId = new Map(payload.individuals.map((i) => [i?.id, i]));
|
|
515
|
+
const touched = [];
|
|
516
|
+
const seen = new Set();
|
|
517
|
+
for (const f of prepared) {
|
|
518
|
+
const prior = byId.get(f.id);
|
|
519
|
+
const priorProv = prior?.attributes?.find((a) => a?.prop === "mgx:factProvenance")?.value || "";
|
|
520
|
+
// Same as appendFact: the mgx:factProvenance union stays byte-identical (a
|
|
521
|
+
// compat shim); the Source edges below are DERIVED from it, purely additive.
|
|
522
|
+
const provs = [...new Set([...priorProv.split(" | "), f.provenance].filter(Boolean))];
|
|
523
|
+
const createdAtVal = firstWriteCreatedAt(prior, f.createdAt); // first-write-wins
|
|
524
|
+
const ind = {
|
|
525
|
+
id: f.id, label: labelOf(f.text), class: FACT_CLASS,
|
|
526
|
+
derived_from: [], mentions: [],
|
|
527
|
+
attributes: [
|
|
528
|
+
{ prop: "rdf:type", key: "type", value: "rdf:Statement" },
|
|
529
|
+
{ prop: "rdf:subject", key: "subject", value: f.s },
|
|
530
|
+
{ prop: "rdf:predicate", key: "predicate", value: f.p },
|
|
531
|
+
{ prop: "rdf:object", key: "object", value: f.o },
|
|
532
|
+
{ prop: CREATED_AT_PROP, key: "createdAt", value: createdAtVal },
|
|
533
|
+
...(provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []),
|
|
534
|
+
...(f.tokens.length ? [{ prop: "mgx:hasProseTokens", key: "prose_tokens", value: f.tokens.join(" ") }] : []),
|
|
535
|
+
],
|
|
536
|
+
};
|
|
537
|
+
// Upsert into BOTH the array (replace-in-place keeps order) and the index.
|
|
538
|
+
if (prior) payload.individuals[payload.individuals.indexOf(prior)] = ind;
|
|
539
|
+
else payload.individuals.push(ind);
|
|
540
|
+
byId.set(f.id, ind);
|
|
541
|
+
ids.push(f.id);
|
|
542
|
+
if (!seen.has(f.id)) { seen.add(f.id); touched.push(f.id); }
|
|
543
|
+
}
|
|
544
|
+
// Reconcile each touched fact's Sources + trust once (add-only, idempotent),
|
|
545
|
+
// then recount classes a SINGLE time at the end.
|
|
546
|
+
for (const id of touched) syncFactSources(payload, byId.get(id));
|
|
547
|
+
recountClasses(payload);
|
|
548
|
+
});
|
|
549
|
+
return { ids, appended: ids.length, skipped };
|
|
550
|
+
}
|
|
551
|
+
|
|
468
552
|
// ---- Chat-facing seams (W4 fact lookup + contradiction) ---------------------
|
|
469
553
|
// The W4 fact-lookup THREADING lives in chat.mjs (NOT here); these pure readers
|
|
470
554
|
// are the seam it calls so the answer layer ranks candidates by relevance ×
|
package/src/tui/app.mjs
CHANGED
|
@@ -92,6 +92,10 @@ export function App({ session }) {
|
|
|
92
92
|
const [input, setInput] = useState("");
|
|
93
93
|
const [prompt, setPrompt] = useState(session.promptFor());
|
|
94
94
|
const [busy, setBusy] = useState(false);
|
|
95
|
+
// Command history (up/down arrow recall, readline-style). `history` is oldest→newest;
|
|
96
|
+
// `histCursor` is -1 for the live input, else the offset back from the newest entry.
|
|
97
|
+
const [history, setHistory] = useState([]);
|
|
98
|
+
const [histCursor, setHistCursor] = useState(-1);
|
|
95
99
|
|
|
96
100
|
const submit = async (line) => {
|
|
97
101
|
if (line === "/exit") { exit(); return; }
|
|
@@ -111,14 +115,34 @@ export function App({ session }) {
|
|
|
111
115
|
if (busy) return; // one turn at a time — the engine is deterministic and fast
|
|
112
116
|
const line = String(raw).trim();
|
|
113
117
|
setInput("");
|
|
114
|
-
|
|
118
|
+
setHistCursor(-1); // any submit resets history navigation to the live input
|
|
119
|
+
if (line) {
|
|
120
|
+
// record for up-arrow recall; collapse an immediate duplicate of the last line
|
|
121
|
+
setHistory((h) => (h[h.length - 1] === line ? h : [...h, line]));
|
|
122
|
+
void submit(line);
|
|
123
|
+
}
|
|
115
124
|
};
|
|
116
125
|
|
|
117
126
|
useInput((ch, key) => {
|
|
118
127
|
if (key.return) { trySubmit(input); return; }
|
|
119
128
|
if (key.backspace || key.delete) { setInput((s) => s.slice(0, -1)); return; }
|
|
120
129
|
if (key.ctrl && ch === "u") { setInput(""); return; }
|
|
121
|
-
|
|
130
|
+
// Up/down arrow: recall previous prompts (readline-style), oldest→newest history.
|
|
131
|
+
if (key.upArrow) {
|
|
132
|
+
if (!history.length) return;
|
|
133
|
+
const nc = Math.min(histCursor + 1, history.length - 1);
|
|
134
|
+
setHistCursor(nc);
|
|
135
|
+
setInput(history[history.length - 1 - nc]);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.downArrow) {
|
|
139
|
+
if (histCursor <= 0) { setHistCursor(-1); setInput(""); return; } // back to a fresh line
|
|
140
|
+
const nc = histCursor - 1;
|
|
141
|
+
setHistCursor(nc);
|
|
142
|
+
setInput(history[history.length - 1 - nc]);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow) return;
|
|
122
146
|
if (!ch) return;
|
|
123
147
|
// A PASTED chunk arrives as one multi-char event; a newline inside it means
|
|
124
148
|
// "submit this line" (one line per turn — the readline shell's per-line read).
|