@polycode-projects/the-mechanical-code-talker 0.6.0 → 0.7.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.
@@ -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
- if (line) void submit(line);
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
- if (key.ctrl || key.meta || key.escape || key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) return;
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).