@polycode-projects/the-mechanical-code-talker 1.3.2 → 1.4.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/README.md CHANGED
@@ -4,9 +4,77 @@
4
4
 
5
5
  A pure-JS, **no-LLM**, offline, **$0** chatbot in the ELIZA/PARRY lineage:
6
6
  pattern-driven, best-efforts, and obsessed with software the way PARRY was
7
- obsessed with the mafia. No model calls anywhere. Interpretation is mechanical
8
- (deterministic language libraries, template sets, committed corpuses). Memory is
9
- a graph on disk. Every answer is either grounded or an honest miss.
7
+ obsessed with the mafia. No model calls anywhere.
8
+
9
+ tmct turns natural language directly into a graph database. The graph starts
10
+ seeded with an **ontology** (a curated software vocabulary with real
11
+ definitions), a **lexicon** (everyday words mapped onto that vocabulary), and
12
+ a **corpus** (a filtered ConceptNet slice of general-world terms). Teach it a
13
+ fact in plain English and it mints a node. Ask it a question and it answers
14
+ from what it was seeded with, what you taught it, and what it can derive by
15
+ rule from both. Every answer is either grounded or an honest miss.
16
+
17
+ ## Teach it, then ask it to reason
18
+
19
+ This is real, runnable output. No cherry-picking, no model anywhere in the
20
+ loop. Copy it into a file and run it:
21
+
22
+ ```js
23
+ import { runChat } from "@polycode-projects/the-mechanical-code-talker";
24
+ import { Readable, PassThrough } from "node:stream";
25
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
26
+ import { tmpdir } from "node:os";
27
+ import { join } from "node:path";
28
+
29
+ // A graph producer feeds tmct a code graph like this one (see "The repository
30
+ // interface" below). This is a 4-node slice: two modules, a base class, and a
31
+ // class that inherits it.
32
+ const graph = {
33
+ individuals: [
34
+ { id: "mod:src/handlers/base.mjs", label: "src/handlers/base.mjs", class: "Module" },
35
+ { id: "mod:src/handlers/tasks.mjs", label: "src/handlers/tasks.mjs", class: "Module" },
36
+ { id: "fn:src/handlers/base.mjs#Controller", label: "Controller", class: "Class" },
37
+ { id: "fn:src/handlers/tasks.mjs#TaskController", label: "TaskController", class: "Class" },
38
+ ],
39
+ objectProperties: [{
40
+ predicate: "inherits", prop: "seon:hasSuperType", count: 1,
41
+ examples: [{ subject: "fn:src/handlers/tasks.mjs#TaskController", object: "fn:src/handlers/base.mjs#Controller",
42
+ subjectLabel: "TaskController", objectLabel: "Controller" }],
43
+ }],
44
+ };
45
+
46
+ const repoPath = await mkdtemp(join(tmpdir(), "tmct-demo-"));
47
+ await mkdir(join(repoPath, ".tmct"), { recursive: true });
48
+ await writeFile(join(repoPath, ".tmct", "graph.json"), JSON.stringify(graph));
49
+
50
+ // tmct has one API surface for both teaching and asking: a chat turn, in
51
+ // English. Each call below is a short session over the same repo, so what
52
+ // gets taught in the first call is still remembered in the second.
53
+ async function tell(line) {
54
+ const out = new PassThrough();
55
+ let transcript = "";
56
+ out.on("data", (chunk) => { transcript += chunk; });
57
+ await runChat({ repoPath, input: Readable.from([line + "\n", "/exit\n"]), output: out });
58
+ return transcript.split("\n").find((l) => l.startsWith("tmct> "));
59
+ }
60
+
61
+ await tell("a controller is a kind of handler"); // Learn
62
+ console.log(await tell("is TaskController a handler")); // Infer from learnings
63
+ ```
64
+
65
+ Output, captured from an actual run:
66
+
67
+ ```
68
+ tmct> yes — the code graph says TaskController inherits Controller, and you
69
+ told me: controller is a kind of handler (source: ace:chat:<session-id>@<timestamp>)
70
+ ```
71
+
72
+ Nothing here was told that "handler" and "Controller" relate. tmct combined a
73
+ fact already in the graph (`TaskController inherits Controller`) with a fact
74
+ you just taught it in English (`controller is a kind of handler`) and wrote
75
+ the connecting sentence itself, citing both sources. The `source: ace:chat:…`
76
+ part is a real provenance receipt. Every fact tmct stores records where it
77
+ came from and when (more on that below).
10
78
 
11
79
  ```
12
80
  $ tmct
@@ -21,6 +89,67 @@ tmct> /exit
21
89
  is a real, interactive chat demo running client-side. Your browser runs the
22
90
  actual query engine against a small example codebase, no server, no install.
23
91
 
92
+ ## Compared to other JS libraries
93
+
94
+ Nothing found in the JS ecosystem does this exact combination: teach in
95
+ English, reason over a seeded ontology, answer in English, with no model call
96
+ anywhere. Three kinds of library each cover one piece of it well.
97
+
98
+ **[compromise](https://github.com/spencermountain/compromise)** (`npm i
99
+ compromise`, 12k+ GitHub stars, describes itself as "modest natural language
100
+ processing") is the general-purpose NLP toolkit of the three. Its own README
101
+ example:
102
+
103
+ ```js
104
+ import nlp from 'compromise'
105
+ let doc = nlp('she sells seashells by the seashore.')
106
+ doc.verbs().toPastTense()
107
+ doc.text()
108
+ // 'she sold seashells by the seashore.'
109
+ ```
110
+
111
+ It tags part-of-speech, conjugates verbs, and parses fractions and money far
112
+ more broadly than tmct attempts to. It has no graph, no ontology, and no
113
+ memory between calls. Each call is stateless text in, text out.
114
+
115
+ **[N3.js](https://github.com/rdfjs/N3.js)** (`npm i n3`) is a mature,
116
+ spec-compliant RDF/OWL toolkit: parsing, writing, and in-memory storage of
117
+ triples. Its own README example:
118
+
119
+ ```js
120
+ const parser = new N3.Parser();
121
+ parser.parse(tomAndJerry, (error, quad, prefixes) => {
122
+ if (quad) console.log(quad);
123
+ else console.log("That's all, folks!", prefixes);
124
+ });
125
+ ```
126
+
127
+ (`tomAndJerry` is Turtle text the caller writes by hand: `c:Tom a c:Cat.
128
+ c:Jerry a c:Mouse; c:smarterThan c:Tom.`) N3.js is the right choice if you
129
+ already have RDF and need to parse or serialize it fast. It has no
130
+ natural-language front end (you write the triples yourself) and no built-in
131
+ reasoning beyond an optional, limited basic-graph-pattern reasoner.
132
+
133
+ **[elizabot](https://github.com/tkafka/node-elizabot)** (`npm i elizabot`) is
134
+ the direct ELIZA lineage in JS, the same territory tmct's chat surface sits
135
+ in. Its own README example:
136
+
137
+ ```js
138
+ var eliza = new ElizaBot();
139
+ var initial = eliza.getInitial();
140
+ var reply = eliza.transform(inputstring);
141
+ ```
142
+
143
+ It is deterministic and needs no model, same as tmct. But it has no graph and
144
+ no persistent memory: a fact from one line never carries into the next, and
145
+ its "reasoning" is pattern substitution, not a stored, queryable fact.
146
+
147
+ Put together: broad NLP without a graph (compromise), a graph without a
148
+ natural-language front end (N3.js), or a conversational front end without a
149
+ graph (elizabot). Combining ontology-seeded graph memory, English teaching,
150
+ and rule-based inference into one no-model pipeline is what looks distinctive
151
+ about tmct as of this writing.
152
+
24
153
  ## How it interprets you
25
154
 
26
155
  Every message runs through **multiple concurrent interpretation strategies**:
@@ -199,7 +328,8 @@ or a bare user gets a working install in one command.
199
328
  ### Try it on an example graph
200
329
 
201
330
  tmct *consumes* a code graph at `<repo>/.tmct/graph.json`; it does not build
202
- one. Two ready-made example graphs ship in `examples/` so you can see it answer
331
+ one. Two ready-made example graphs live in `examples/` in this repo (not in the
332
+ published npm package — clone the repo to use them) so you can see it answer
203
333
  real questions with no setup:
204
334
 
205
335
  ```bash
@@ -237,9 +367,13 @@ full tours.
237
367
  import { runChat, ask, resolveObject, fetchEntities } from "@polycode-projects/the-mechanical-code-talker";
238
368
  ```
239
369
 
240
- The `exports` map and the chat primitives (`ask`, `resolveObject`,
241
- `relationKind`, `impactClosure`, `dispatchTool`, `fetchEntities`) are the
242
- extension surface.
370
+ `runChat` is the full teach-and-ask surface (see "Teach it, then ask it to
371
+ reason" above it works over injectable streams, so a script can drive a
372
+ session the same way the tests do). `ask`/`resolveObject` are the lower-level,
373
+ read-only query primitives over an already-loaded graph, for a caller that
374
+ wants to query without a chat session. The `exports` map and the chat
375
+ primitives (`ask`, `resolveObject`, `relationKind`, `impactClosure`,
376
+ `dispatchTool`, `fetchEntities`) are the extension surface.
243
377
 
244
378
  ## The repository interface
245
379
 
package/ROADMAP.md CHANGED
@@ -407,6 +407,28 @@ consolidates three near-duplicate "research frontier" essays (the frame problem,
407
407
  disambiguation/ontology scale, Winograd-hard coreference) that had independently grown across the
408
408
  now-archived docs.
409
409
 
410
+ **v1.4.0 — the first PLAN_AGENTS.md uplift batch (2026-07-11)**: built as four parallel,
411
+ worktree-isolated background tracks, merged sequentially, watched throughout by a background
412
+ strategy-advisor agent (full tick-by-tick record in `STRATEGY_ADVISOR.log`). 1543/1543 tests green
413
+ at the final merge. Shipped: the Repository Interface wrapper fixes from the seonix audit (ranked
414
+ `search()`, a real graph-only `context()` — `INTERFACE_VERSION` 1.0.0→1.1.0 — depth-capped
415
+ `impact()`, source-backed `snippet()`, `edges()`/`search()` pagination, telemetry wiring), a
416
+ path-traversal security fix found and closed along the way (`src/source-slice.mjs`), hub-dampened
417
+ memory-fact ranking (on by default — the build found the original "modest degree, modest penalty"
418
+ assumption was mathematically wrong and proved the real bound instead), memory-tree versioning
419
+ (`snapshotMemory()`, manual trigger only), full session-scoped actor-level trust (shipped
420
+ unconditionally, no config flag — operator decision, single consumer), the extension-pack seam
421
+ (`src/extensions.mjs`, `[extensions]`/`[bias]` in `tmct.toml`, `tmct extend --validate`, and a
422
+ deliberate bug fix — `tmct init` now seeds SEON as well as ConceptNet), bias-weighted fact ranking
423
+ (`src/memory/bias.mjs`, verified by control-flow tracing to never drop a fact, only reorder it), and
424
+ `tmct init --with-persona <name>`. One real merge conflict (Track A's `tel` param and Track D's
425
+ `biasByBundle` param both threading through the same `chat.mjs` function signatures — resolved by
426
+ keeping both). One bug the strategy advisor caught that the original brief missed: the
427
+ path-traversal guard failed closed *incorrectly* under a relative `TMCT_GRAPH_FILE`, rejecting
428
+ legitimate reads, not just traversal attempts — fixed at the source and defensively in the guard.
429
+ One scope decision made mid-build: multi-language AST extraction stays in seonix permanently, not
430
+ tmct's job — full detail in `PLAN_AGENTS.md` §13.
431
+
410
432
  ## The umbrella product definition (item 1)
411
433
 
412
434
  **A tolerant, ELIZA/PARRY-style chat, obsessed with software.** A best-efforts
package/bin/tmct.mjs CHANGED
@@ -49,6 +49,11 @@ Usage:
49
49
  offline, $0; init is tier-1-only unless asked
50
50
  [--detect] suggest a tier-2 corpus from the repo's manifests
51
51
  (pyproject.toml → python, pom.xml → java); never seeds unasked
52
+ [--with-persona <name>] write an explicit [extensions]/[bias] preset into tmct.toml
53
+ ("code" — today's implicit default, made explicit)
54
+ tmct extend --validate <dir> validate a third-party extension pack's declared
55
+ resources (corpus/lexicon/templates) before activating
56
+ it in any repo's tmct.toml; exits non-zero on failure
52
57
  tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
53
58
  [--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
54
59
  bounded, low-trust, retractable entailed facts (never on the chat path)
@@ -316,41 +321,96 @@ async function main() {
316
321
 
317
322
  if (mode === "init") {
318
323
  // `tmct init` — the Repository-Interface onboarding surface: scaffold .tmct/,
319
- // write tmct.toml, seed the tier-1 corpus (offline, opt-out via TMCT_NO_SEED),
320
- // and record provenance. Idempotent; --force rewrites config + re-records.
324
+ // write tmct.toml, seed the corpus (offline, opt-out via TMCT_NO_SEED), and
325
+ // record provenance. Idempotent; --force rewrites config + re-records.
321
326
  //
322
- // TIERING POLICY: init is OFFLINE, $0 and TIER-1-ONLY by default. A tier-2
323
- // domain/language corpus (corpus/tier2/: aws, python, java) is added ONLY when
327
+ // TIERING POLICY: init is OFFLINE, $0 and TIER-1-ONLY by default (seon +
328
+ // conceptnet src/extensions.mjs's BUILTIN_EXTENSIONS). A tier-2 domain/
329
+ // language corpus (corpus/tier2/: aws, python, java) is added ONLY when
324
330
  // explicitly asked via `--corpus <id>`. The `--detect` auto-detect is a
325
331
  // documented STUB: it inspects the repo's manifests (pyproject.toml → python,
326
332
  // pom.xml → java) and SUGGESTS the matching corpus, but never seeds it unasked.
327
333
  const rest = process.argv.slice(3);
328
- const { initRepo } = await import("../src/init.mjs");
329
- const res = await initRepo(process.cwd(), { force: rest.includes("--force") });
330
- process.stdout.write(res.message + "\n");
334
+ const { initRepo, defaultConfig, renderTomlConfig, CONFIG_FILE, PERSONA_PRESETS } = await import("../src/init.mjs");
335
+ const { loadTomlConfig } = await import("../src/toml-config.mjs");
331
336
 
332
337
  const ci = rest.indexOf("--corpus");
333
338
  const corpusId = ci !== -1 ? rest[ci + 1] : undefined;
339
+ let manifest = null;
340
+ let manifestEntry = null;
334
341
  if (corpusId) {
335
- // Seed a tier-2 corpus by id from corpus/tier2/ (same slice shape as tier-1;
336
- // provenance-tagged corpus:tier2:<id>). Idempotent (content-hashed fact ids).
342
+ // Validate the id against the tier-2 manifest (same source of truth the
343
+ // old ad hoc path used) BEFORE touching anything on disk.
337
344
  const { readFile } = await import("node:fs/promises");
338
- const { join, dirname } = await import("node:path");
339
- const { seedMemory, TIER2_MANIFEST_FILE } = await import("../src/corpus/conceptnet.mjs");
340
- let manifest;
345
+ const { TIER2_MANIFEST_FILE } = await import("../src/corpus/conceptnet.mjs");
341
346
  try { manifest = JSON.parse(await readFile(TIER2_MANIFEST_FILE, "utf8")); }
342
347
  catch (e) { process.stderr.write(`tmct init: cannot read the tier-2 manifest — ${e?.message || e}\n`); process.exit(1); }
343
- const entry = (manifest.corpuses || []).find((c) => c.id === corpusId);
344
- if (!entry) {
348
+ manifestEntry = (manifest.corpuses || []).find((c) => c.id === corpusId);
349
+ if (!manifestEntry) {
345
350
  const ids = (manifest.corpuses || []).map((c) => c.id).join(", ");
346
351
  process.stderr.write(`tmct init: unknown --corpus "${corpusId}". Available tier-2 corpuses: ${ids}.\n`);
347
352
  process.exit(2);
348
353
  }
349
- const slicePath = join(dirname(TIER2_MANIFEST_FILE), entry.file);
350
- const seeded = await seedMemory(process.cwd(), { slicePath, provenancePrefix: `corpus:tier2:${entry.id}` });
354
+ }
355
+
356
+ // `--with-persona <name>` (Part 7): resolve + validate BEFORE touching
357
+ // disk, mirroring `--corpus`'s own unknown-id error handling — a bad
358
+ // persona name never scaffolds anything.
359
+ const pi = rest.indexOf("--with-persona");
360
+ const personaName = pi !== -1 ? rest[pi + 1] : undefined;
361
+ let personaPreset = null;
362
+ if (personaName) {
363
+ if (!Object.prototype.hasOwnProperty.call(PERSONA_PRESETS, personaName)) {
364
+ const names = Object.keys(PERSONA_PRESETS).join(", ");
365
+ process.stderr.write(`tmct init: unknown --with-persona "${personaName}". Available personas: ${names}.\n`);
366
+ process.exit(2);
367
+ }
368
+ personaPreset = PERSONA_PRESETS[personaName];
369
+ }
370
+
371
+ const res = await initRepo(process.cwd(), { force: rest.includes("--force"), persona: personaPreset });
372
+ process.stdout.write(res.message + "\n");
373
+
374
+ if (manifestEntry) {
375
+ // `--corpus <id>` now means "activate extensions.tier2-<id> and PERSIST
376
+ // that into tmct.toml" — so a second `tmct init` (or the next chat
377
+ // bootstrap) remembers the choice, unlike the old ad hoc path, which had
378
+ // to be repeated every time. This changes the tier-2 provenance tag from
379
+ // the old colon-separated "corpus:tier2:<id>" to the hyphenated
380
+ // "corpus:tier2-<id>" (matching the TOML-legal extension name) — a
381
+ // deliberate, low-risk rename; nothing in chat.mjs's runtime logic keys
382
+ // on the old colon-separated string (verified via grep).
383
+ const extName = `tier2-${manifestEntry.id}`;
384
+ const raw = await loadTomlConfig(process.cwd()); // just-written by initRepo above
385
+ const cfg = { ...defaultConfig() };
386
+ if (raw?.graph_file !== undefined) cfg.graphFile = String(raw.graph_file);
387
+ if (raw?.corpus?.tier !== undefined) cfg.corpus = { tier: raw.corpus.tier };
388
+ if (raw?.seed) {
389
+ cfg.seed = { ...cfg.seed };
390
+ if (raw.seed.enabled !== undefined) cfg.seed.enabled = Boolean(raw.seed.enabled);
391
+ if (raw.seed.limit !== undefined) cfg.seed.limit = Number(raw.seed.limit);
392
+ }
393
+ cfg.extensions = { ...(raw?.extensions || {}), [extName]: { ...(raw?.extensions?.[extName] || {}), active: true } };
394
+ if (raw?.bias !== undefined) cfg.bias = raw.bias;
395
+ const { writeFile } = await import("node:fs/promises");
396
+ const { join } = await import("node:path");
397
+ await writeFile(join(process.cwd(), CONFIG_FILE), renderTomlConfig(cfg));
398
+
399
+ // Seed it now too — through the SAME unified corpus loader every other
400
+ // bundle goes through (src/extensions.mjs), not a bespoke seedMemory call.
401
+ const { resolveExtensions, seedActiveCorpusEntries } = await import("../src/extensions.mjs");
402
+ const { entries } = await resolveExtensions(process.cwd());
403
+ const entry = entries.get(extName);
404
+ const { perBundle } = await seedActiveCorpusEntries(process.cwd(), new Map([[extName, entry]]));
405
+ const seeded = perBundle[extName];
406
+ if (seeded.error) {
407
+ process.stderr.write(`tmct init: could not seed tier-2 corpus "${manifestEntry.id}" — ${seeded.error}\n`);
408
+ process.exit(1);
409
+ }
351
410
  process.stdout.write(
352
- `seeded tier-2 corpus "${entry.id}" (${entry.kind}) — ${seeded.appended} fact(s) added`
353
- + `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${entry.file} (${entry.license}).\n`,
411
+ `seeded tier-2 corpus "${manifestEntry.id}" (${manifestEntry.kind}) — ${seeded.appended} fact(s) added`
412
+ + `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${manifestEntry.file} (${manifestEntry.license}). `
413
+ + `Activated in tmct.toml — future \`tmct init\`/chat sessions seed it automatically.\n`,
354
414
  );
355
415
  return;
356
416
  }
@@ -377,6 +437,56 @@ async function main() {
377
437
  return;
378
438
  }
379
439
 
440
+ if (mode === "extend") {
441
+ // `tmct extend --validate <dir>` — validate a THIRD-PARTY extension pack
442
+ // (the shape a package like seonix/marginalia ships) BEFORE it's activated
443
+ // in any repo's tmct.toml. Reuses existing throw-loudly primitives
444
+ // (loadSlice/loadMap/toFacts, loadLexicon, loadTemplates) via
445
+ // src/extensions.mjs's validateExtensionPack — never invents new
446
+ // shape-checking logic. `<dir>` must carry its own tmct.toml declaring one
447
+ // or more `[extensions.<name>]` host entries (the SAME [extensions] table
448
+ // shape a repo's own tmct.toml uses) naming the resource(s) to validate;
449
+ // the shipped builtins (seon/conceptnet/tier2-*) are never re-validated
450
+ // here — this command is about a PACK's OWN declared resources.
451
+ const rest = process.argv.slice(3);
452
+ const vi = rest.indexOf("--validate");
453
+ const dirArg = vi !== -1 ? rest[vi + 1] : undefined;
454
+ if (!dirArg) {
455
+ process.stderr.write("tmct extend: --validate <dir> requires a directory\n");
456
+ process.exit(2);
457
+ }
458
+ const { resolve: resolvePath } = await import("node:path");
459
+ const target = resolvePath(process.cwd(), dirArg);
460
+ const { resolveExtensions, BUILTIN_EXTENSIONS, validateExtensionPack } = await import("../src/extensions.mjs");
461
+ let entries;
462
+ try {
463
+ ({ entries } = await resolveExtensions(target));
464
+ } catch (e) {
465
+ process.stderr.write(`tmct extend --validate: ${e?.message || e}\n`);
466
+ process.exit(1);
467
+ }
468
+ const hostEntries = [...entries].filter(([name]) => !(name in BUILTIN_EXTENSIONS));
469
+ if (!hostEntries.length) {
470
+ process.stderr.write(`tmct extend --validate: no host-declared [extensions.*] entries found in ${target}/tmct.toml\n`);
471
+ process.exit(1);
472
+ }
473
+ let allOk = true;
474
+ for (const [name, entry] of hostEntries) {
475
+ process.stdout.write(`${name} (${entry.kind}):\n`);
476
+ const { ok, results } = await validateExtensionPack(target, entry);
477
+ if (!ok) allOk = false;
478
+ for (const r of results) {
479
+ const status = r.ok ? "PASS" : "FAIL";
480
+ const detail = r.ok
481
+ ? (r.counts ? ` (${Object.entries(r.counts).map(([k, v]) => `${k}=${v}`).join(", ")})` : "")
482
+ : ` — ${r.error}`;
483
+ process.stdout.write(` [${status}] ${r.kind}: ${r.path}${detail}\n`);
484
+ }
485
+ }
486
+ process.stdout.write(allOk ? "tmct extend --validate: all resources passed.\n" : "tmct extend --validate: one or more resources FAILED.\n");
487
+ process.exit(allOk ? 0 : 1);
488
+ }
489
+
380
490
  if (mode === "syllogise") {
381
491
  // `tmct syllogise` — the explicit speculative-inference batch (never on the chat
382
492
  // hot path): forward-chain the memory's rdfs:subClassOf closure into bounded,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",