@polycode-projects/the-mechanical-code-talker 2.0.2 → 2.2.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 CHANGED
@@ -103,12 +103,15 @@ The test suite replays every runnable example in this README against the
103
103
  live product, this transcript included. If the chat behavior ever drifts
104
104
  from the output above, the suite fails and says so.
105
105
 
106
- ```session
107
- $ tmct
108
- tmct> what talks to the payment module?
109
-
110
- tmct> /callers checkout
111
-
106
+ Point it at a codebase's graph and the same engine answers structural questions.
107
+ `examples/mini-webapp` ships in this repo, so this runs as written:
108
+
109
+ ```session cwd=repo
110
+ $ node bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral
111
+ tmct> what does app.mjs talk to?
112
+ src/server/router.mjs and src/handlers/tasks.mjs and src/handlers/users.mjs and src/lib/logger.mjs.
113
+ tmct> what talks to store.mjs?
114
+ src/handlers/tasks.mjs and src/handlers/users.mjs.
112
115
  tmct> /exit
113
116
  ```
114
117
 
@@ -177,7 +180,7 @@ resolves to a real graph traversal or declines honestly:
177
180
  (*because/although/while*), conditionals, and false-premise flags ("why
178
181
  does X still import Y" when it no longer does).
179
182
 
180
- The full catalog with measured coverage lives in `CAPABILITIES_1.7.3.md` and
183
+ The full catalog with measured coverage lives in `CAPABILITIES_2.0.3.md` and
181
184
  the `BENCHMARK_*.md` reports.
182
185
 
183
186
  **Response finishing.** Before an answer prints, it is segmented into typed
package/ROADMAP.md CHANGED
@@ -100,7 +100,30 @@ blending web-sourced facts with graph/operator facts.
100
100
 
101
101
  ## Design docs
102
102
 
103
- Every substantial design lives in its own `PLAN_*.md` at the repo root (active) or `archive/`
104
- (shipped and closed) this file points to them, it doesn't repeat their content. `SKILL_*.md` docs
105
- specify the repeatable measurement/build cycles (benchmarks, the background strategy advisor,
106
- plain-prose writing). `HANDOVER.md` is the single current-open-items list.
103
+ Every substantial design lives in its own `PLAN_*.md` at the repo root; `archive/` holds the shipped
104
+ and closed ones. This file points to them, it doesn't repeat their content. Each plan states its own
105
+ status in its opening lines — read it there, because a status quoted here would rot.
106
+
107
+ | Plan | What it's for |
108
+ | --- | --- |
109
+ | [PLAN_ADVENTURE.md](PLAN_ADVENTURE.md) | a text adventure as an architectural stretch: imperative command grammar, NPC turn scheduler, room-look digest |
110
+ | [PLAN_AGENTS.md](PLAN_AGENTS.md) | the governing plan for the multi-repo arc (marginalia, seonix, a pluggable LLM rung), with its own phase sequencing |
111
+ | [PLAN_CHILD_CORPUS.md](PLAN_CHILD_CORPUS.md) | a wider default seed corpus, chosen by age of acquisition |
112
+ | [PLAN_CLASS_QUERY.md](PLAN_CLASS_QUERY.md) | "list/count all X of class Y", reconciled against what already shipped |
113
+ | [PLAN_CODE.md](PLAN_CODE.md) | program synthesis over tmct's closed DSLs, plus JS/HTML/CSS fragments and goal-directed program repair |
114
+ | [PLAN_CONSISTENCY_CHECK.md](PLAN_CONSISTENCY_CHECK.md) | tmct as a consistency service for an LLM tool loop |
115
+ | [PLAN_EMBEDDINGS.md](PLAN_EMBEDDINGS.md) | the semantic-similarity axis, and the way back to it |
116
+ | [PLAN_GRAPH_SCAN.md](PLAN_GRAPH_SCAN.md) | seed and query cost at `init:xl`/`init:xxl` corpus scale |
117
+ | [PLAN_GUESS_NUMBER.md](PLAN_GUESS_NUMBER.md) | closed-loop planning over hidden state, via belief-interval bisection |
118
+ | [PLAN_MUD.md](PLAN_MUD.md) | persistent, shared tmct worlds over a `server:` memory backend |
119
+ | [PLAN_NLU_BENCHMARKS.md](PLAN_NLU_BENCHMARKS.md) | scoring tmct on the CLINC150 and HWU64 intent sets |
120
+ | [PLAN_OPEN_ITEMS.md](PLAN_OPEN_ITEMS.md) | the build order closing the backlog `HANDOVER.md` carries |
121
+ | [PLAN_PARAPHRASE_VERIFICATION.md](PLAN_PARAPHRASE_VERIFICATION.md) | checking a paraphrase against the graph before it prints |
122
+ | [PLAN_PURGE.md](PLAN_PURGE.md) | promote the load-bearing code, delete the dead weight |
123
+ | [PLAN_REPO_INDEX.md](PLAN_REPO_INDEX.md) | tmct grows its own code parsers, ported from seonix |
124
+ | [PLAN_SYLLOGIST.md](PLAN_SYLLOGIST.md) | the reasoning engine's incrementality and retraction horizon |
125
+ | [PLAN_SYLLOGIST_EL_DL.md](PLAN_SYLLOGIST_EL_DL.md) | beyond OWL 2 RL: an EL classifier, then a DL tableau prover |
126
+
127
+ `SKILL_*.md` docs specify the repeatable measurement and build cycles (the benchmarks, the capability
128
+ audit, the background strategy advisor, plain-prose writing). `HANDOVER.md` is the single
129
+ current-open-items list.
@@ -1,3 +1,4 @@
1
+ {"kind":"rescue","sentence":"Code search is a retrieval task","rescued":"Code hunt is a retrieval task","from":"search","to":"hunt","pos":"noun","synsetId":"00947217-n","sourceFile":"PLAN_EMBEDDINGS.md","provenance":"wordnet:00947217-n"}
1
2
  {"kind":"variant","seed":"the ace of trumps is a sure winner","generated":"the one of trumps is a sure winner","from":"ace","to":"one","pos":"noun","synsetId":"13764713-n","sourceCorpus":"corpus/tier2/human-examples-large.jsonl","provenance":"wordnet:13764713-n"}
2
3
  {"kind":"variant","seed":"the ace of trumps is a sure winner","generated":"the single of trumps is a sure winner","from":"ace","to":"single","pos":"noun","synsetId":"13764713-n","sourceCorpus":"corpus/tier2/human-examples-large.jsonl","provenance":"wordnet:13764713-n"}
3
4
  {"kind":"variant","seed":"the ace of trumps is a sure winner","generated":"the unity of trumps is a sure winner","from":"ace","to":"unity","pos":"noun","synsetId":"13764713-n","sourceCorpus":"corpus/tier2/human-examples-large.jsonl","provenance":"wordnet:13764713-n"}
@@ -2,8 +2,8 @@
2
2
  "version": 1,
3
3
  "generated": "by scripts/generate-template-variants.mjs",
4
4
  "file": "ace-surface-variants.jsonl",
5
- "rows": 17,
6
- "bytes": 4774,
7
- "sha256": "5364415f73f7a366371f1fdcc072b5059f81871ef48b106088f05b1441def569",
5
+ "rows": 18,
6
+ "bytes": 5012,
7
+ "sha256": "28fae5a3353f5dd1df365b4407e0ac2a5c66b98b232d8bf454fe3573f2c16760",
8
8
  "license": "CC-BY-4.0 (WordNet-derived synonym substitutions of Open English WordNet / SemCor example sentences and this repo's own MPL-2.0 docs prose — see corpus/generated/README.md)"
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
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.",
@@ -139,7 +139,6 @@ export async function normalizeConfig(raw, { configDir } = {}) {
139
139
  if (t.impl_of_interface !== undefined) tune.implOfInterface = t.impl_of_interface;
140
140
  if (t.beam_search !== undefined) tune.beamSearch = t.beam_search;
141
141
  if (t.beam_width !== undefined) tune.beamWidth = t.beam_width;
142
- if (t.embed_rank !== undefined) tune.embedRank = t.embed_rank;
143
142
  if (t.prose_layers !== undefined) tune.proseLayers = t.prose_layers;
144
143
  const exp = t.expansion || {};
145
144
  const expansion = {};
@@ -52,6 +52,9 @@ export const RELATIONS = {
52
52
  comment: "query-side union: imports (Module->Module) + calls (Module->Module) + callsSymbol (fn->fn).",
53
53
  verbs: [
54
54
  "uses", "use", "makes use of", "make use of",
55
+ // casual: "what does app.mjs talk to" is the union question a newcomer
56
+ // actually asks — imports and calls together, which is what "uses" is.
57
+ "talks to", "talk to",
55
58
  // gerund (g-drop normalization)
56
59
  "using",
57
60
  ],
@@ -2779,7 +2779,13 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2779
2779
  }
2780
2780
  const edges = fwdKinds.flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.subject === objMatch.id);
2781
2781
  const targets = edges.map((e) => graph.byId.get(e.object)).filter(Boolean);
2782
- const deduped = subjIsFineSymbol ? uniqueById(targets) : targets;
2782
+ // Dedupe unconditionally: a forward answer enumerates distinct targets, so
2783
+ // reaching one twice is never information. It matters for a query-side
2784
+ // union (KIND_UNIONS' "uses" scans imports+calls+callsSymbol), where a
2785
+ // module that both imports AND calls another was listed twice — the
2786
+ // reverse traversal already collapses these, so the two directions
2787
+ // disagreed about the same pair.
2788
+ const deduped = uniqueById(targets);
2783
2789
  // Keep only matches of the asked class, so a forward answer never leaks
2784
2790
  // a wrong-class match once an entityType was actually asked for.
2785
2791
  let matches = deduped;
@@ -1,5 +1,4 @@
1
1
  import { lookupByProseTokens, proseLayerHits, splitIdentifierWords } from "./prose.mjs";
2
- import { cosine } from "./vector.mjs";
3
2
  import { CREATED_AT_PROP, UPDATED_AT_PROP } from "./memory/trust.mjs";
4
3
 
5
4
  // Pure (no-network, no-fs) query logic over the typed `entities` payload that the
@@ -511,15 +510,6 @@ const LIT_COMP_CAP = 4;
511
510
  const LIT_FRAC = 1.0;
512
511
  const LIT_CAP_FRAC = 0.9;
513
512
 
514
- // opt-in via embedRank + an injected embedder: static-embedding re-rank over
515
- // path/symbol/doc text read from the graph (never source), cached per-process in EMB_CACHE
516
- const EMB_FRAC = 0.2;
517
- const EMB_CAP_FRAC = 0.35;
518
- const EMB_TEXT_SYMBOL_CAP = 64;
519
- const EMB_TEXT_DOC_CAP = 12;
520
- const EMB_CACHE = new WeakMap(); // graph -> { embedder, texts, vecs: Map<moduleId, Float32Array> }
521
- let embedWarned = false;
522
-
523
513
  // opt-in via beamSearch: multi-ply adaptive expansion of the proximity nudge above.
524
514
  // Beam width is a margin relative to each ply's best score (not a fixed count), so a
525
515
  // weak-then-strong candidate isn't prematurely discarded. Successors are generated
@@ -544,30 +534,6 @@ const SPIRAL_HOP_DECAY = 0.6;
544
534
  const SPIRAL_PROX_FRAC = 0.2;
545
535
  const SPIRAL_PROX_CAP_FRAC = 0.35;
546
536
 
547
- /** embedRank: per-module embeddable text from path components + defined symbol
548
- * names + doc first-lines, cached alongside the vectors in EMB_CACHE. */
549
- function moduleEmbedTexts(graph) {
550
- const texts = new Map(); // moduleId -> text
551
- const defIdx = definesIndex(graph);
552
- const docs = new Map(); // moduleId -> [doc first-lines]
553
- for (const ind of graph.individuals) {
554
- const doc = (ind.attributes || []).find((a) => a.key === "doc")?.value;
555
- if (!doc) continue;
556
- const modId = (ind.class || "") === "Module" ? ind.id : moduleIdOf(graph, ind);
557
- if (!modId) continue;
558
- let arr = docs.get(modId);
559
- if (!arr) docs.set(modId, (arr = []));
560
- if (arr.length < EMB_TEXT_DOC_CAP) arr.push(String(doc).split("\n")[0]);
561
- }
562
- for (const ind of graph.individuals) {
563
- if ((ind.class || "") !== "Module") continue;
564
- const parts = String(ind.label).split(/[^a-zA-Z0-9_]+/).filter(Boolean);
565
- const syms = (defIdx.get(ind.id) || []).slice(0, EMB_TEXT_SYMBOL_CAP);
566
- texts.set(ind.id, [...parts, ...syms, ...(docs.get(ind.id) || [])].join(" "));
567
- }
568
- return texts;
569
- }
570
-
571
537
  /** Split a lowercased path label into boundary components: django/utils/text.py →
572
538
  * {django,utils,text,py}. Component equality (not substring) stops "text" matching "ci<text>". */
573
539
  function pathComponents(labelLc) {
@@ -773,7 +739,7 @@ export function spiralExpand(graph, scored = [], {
773
739
  * IDF-weights each query token, scores path/symbol/exact-symbol matches, and
774
740
  * re-ranks with a bounded import-proximity bonus. Pure; deterministic. */
775
741
  function scoreModules(graph, tokens, opts = {}) {
776
- const { demoteNonProd = false, callAdjacency = false, implOfInterface = false, beamSearch = false, spiral = false, proseBoost = false, proseLayers = false, literalMention = false, embedRank = false, rawQuery = "" } = opts;
742
+ const { demoteNonProd = false, callAdjacency = false, implOfInterface = false, beamSearch = false, spiral = false, proseBoost = false, proseLayers = false, literalMention = false, rawQuery = "" } = opts;
777
743
  const beamWidth = Number.isFinite(opts.beamWidth) && opts.beamWidth > 0 ? opts.beamWidth : 8;
778
744
  const defIdx = definesIndex(graph);
779
745
  // Precompute each module's path components + defined-symbol exact/component sets, once.
@@ -967,36 +933,6 @@ function scoreModules(graph, tokens, opts = {}) {
967
933
  s.score += Math.min(signal * PROSE_LAYER_FRAC, s.score * PROSE_LAYER_CAP_FRAC);
968
934
  }
969
935
  }
970
- // embedRank: the embedder is injected so this module stays fs-free; absent -> a one-time
971
- // stderr note, never a failure
972
- if (embedRank) {
973
- if (!opts.embedder) {
974
- if (!embedWarned) {
975
- embedWarned = true;
976
- process.stderr.write("tmct: embedRank requested but no embedder available (weights not fetched? see `npm run refs:embeddings`) — flag is a no-op\n");
977
- }
978
- } else if (scored.length) {
979
- const embedder = opts.embedder;
980
- let cache = EMB_CACHE.get(graph);
981
- if (!cache || cache.embedder !== embedder) {
982
- cache = { embedder, texts: moduleEmbedTexts(graph), vecs: new Map() };
983
- EMB_CACHE.set(graph, cache);
984
- }
985
- const qv = embedder.embed(rawQuery || tokens.join(" "));
986
- let maxBase = 0;
987
- for (const s of scored) maxBase = Math.max(maxBase, s.score);
988
- for (const s of scored) {
989
- let v = cache.vecs.get(s.ind.id);
990
- if (!v) {
991
- v = embedder.embed(cache.texts.get(s.ind.id) || String(s.ind.label));
992
- cache.vecs.set(s.ind.id, v);
993
- }
994
- const sim = Math.max(0, cosine(qv, v)); // negative similarity never penalises
995
- if (!sim) continue;
996
- s.score += Math.min(sim * maxBase * EMB_FRAC, s.score * EMB_CAP_FRAC);
997
- }
998
- }
999
- }
1000
936
  // beamSearch (opt-in): multi-ply generalization of the single-hop families above.
1001
937
  if (beamSearch && scored.length > 1) beamExpand(graph, scored, beamWidth);
1002
938
  // SPIRAL (opt-in): bounded-radius ego walk that may introduce lexically-invisible modules — runs
@@ -1024,9 +960,9 @@ export function searchModulesRanked(graph, query, opts = {}) {
1024
960
  const tokens = raw.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
1025
961
  if (!tokens.length) return [];
1026
962
  // literalMention needs the query BEFORE tokenization (the tokenizer destroys the dotted refs
1027
- // it matches on) and embedRank embeds the raw phrasing; threaded only when a flag that
1028
- // consumes it is on, so the OFF path is provably unchanged.
1029
- const effOpts = (opts.literalMention || opts.embedRank) ? { ...opts, rawQuery: raw } : opts;
963
+ // it matches on); threaded only when a flag that consumes it is on, so the OFF path is
964
+ // provably unchanged.
965
+ const effOpts = opts.literalMention ? { ...opts, rawQuery: raw } : opts;
1030
966
  return scoreModules(graph, tokens, effOpts).map((s) => ({ path: String(s.ind.label), score: s.score }));
1031
967
  }
1032
968
 
@@ -0,0 +1,55 @@
1
+ // markdown-links.mjs — pull the relative link targets out of a markdown string.
2
+ // Pure: a string in, `[{ target, line }]` out, no filesystem and no imports, so
3
+ // the CI jobs that run without `npm ci` can reach it.
4
+ //
5
+ // Code is not prose. A doc that writes `[text](target)` inside backticks is
6
+ // showing you what a link looks like, not making one, and a checker that cannot
7
+ // tell the difference reports the example as a broken link to a file named
8
+ // "target". So the spans are blanked before the link patterns run — blanked
9
+ // rather than cut, because every offset behind them still has to name the right
10
+ // line number.
11
+
12
+ const blank = (line) => line.replace(/[^\n]/g, " ");
13
+ const FENCE = /^[ \t]{0,3}(`{3,}|~{3,})/;
14
+
15
+ /** Replace every fenced block and inline code span with spaces, keeping the
16
+ * string's length and its newlines so later offsets still map to their line.
17
+ * Fences are walked line by line: a lazy multiline regex stops at the end of
18
+ * the opening fence's own line and blanks only the markers. */
19
+ export function blankCodeSpans(markdown) {
20
+ let fence = null;
21
+ const lines = markdown.split("\n").map((line) => {
22
+ const marker = line.match(FENCE)?.[1];
23
+ if (fence) {
24
+ if (marker && marker[0] === fence[0] && marker.length >= fence.length) fence = null;
25
+ return blank(line);
26
+ }
27
+ if (marker) fence = marker;
28
+ return marker ? blank(line) : line;
29
+ });
30
+ return lines.join("\n").replace(/(`+)[\s\S]*?\1/g, blank);
31
+ }
32
+
33
+ // Inline links/images: [text](target "title") — target ends at the first
34
+ // whitespace or closing paren. Reference definitions: [label]: target.
35
+ const INLINE_LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
36
+ const REFERENCE_DEF = /^\s{0,3}\[[^\]]+\]:\s+(\S+)/gm;
37
+
38
+ /** Every repo-relative link target in `markdown`, with the 1-based line it sits
39
+ * on. External URLs, bare #anchors and absolute paths are out of scope. */
40
+ export function relativeTargets(markdown) {
41
+ const prose = blankCodeSpans(markdown);
42
+ const targets = [];
43
+ for (const regex of [INLINE_LINK, REFERENCE_DEF]) {
44
+ for (const match of prose.matchAll(regex)) {
45
+ let target = match[1];
46
+ if (/^(https?|mailto|ftp):/i.test(target)) continue;
47
+ if (target.startsWith("#") || target.startsWith("/") || target.startsWith("<")) continue;
48
+ target = decodeURIComponent(target.split("#")[0].split("?")[0]);
49
+ if (!target) continue;
50
+ const line = prose.slice(0, match.index).split("\n").length;
51
+ targets.push({ target, line });
52
+ }
53
+ }
54
+ return targets;
55
+ }