@polycode-projects/the-mechanical-code-talker 0.9.7 → 0.9.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
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.",
package/src/ask.mjs CHANGED
@@ -38,7 +38,7 @@
38
38
  // edges (mgx:touchedByCommit / mgx:changeCoupledWith), which is a different (and
39
39
  // simpler) question than the browser's time-scrubbing view.
40
40
 
41
- import { relationKind, impactClosure } from "./codegraph.mjs";
41
+ import { relationKind, impactClosure, normPath } from "./codegraph.mjs";
42
42
  import {
43
43
  VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
44
44
  CONTEXT_PRONOUNS, META_MEANING_VERBS,
@@ -70,12 +70,36 @@ export { normalizeQuery, applyNegationFrames };
70
70
  // shipping a ~1MB language model inside the page.
71
71
  import { nlpAdapter } from "./ask-nlp.mjs";
72
72
 
73
+ /** Per-graph, per-kind memo for THIS file's own edgesOfKind copy — same WeakMap<graph,
74
+ * Map<kind, edge[]>> shape as codegraph.mjs's twin (and this file's own qualCache,
75
+ * below), kept as an independent cache rather than sharing codegraph.mjs's (same
76
+ * commit-boundary reasoning as the function copy itself: both derive the identical
77
+ * result from the identical relationKind classification, so two caches can never
78
+ * disagree, only duplicate a little memory). Correctness rests on the same
79
+ * invariant qualCache already relies on: a loaded graph's `relations` are never
80
+ * mutated in place (a refresh always builds a NEW graph object via parseEntities).
81
+ * Deliberately NAMED DIFFERENTLY from codegraph.mjs's `edgesOfKindCache` — the
82
+ * inlined viewer bundle (viz.mjs's askSource) literally CONCATENATES a stripped
83
+ * codegraph.mjs + this file into one classic script (test/ask-nlp.test.mjs pins
84
+ * this), so two `const`s with the same name would be a real SyntaxError there. */
85
+ const askEdgesOfKindCache = new WeakMap();
86
+
73
87
  /** All edges of a classified relation kind, flattened across relation groups —
74
88
  * a local copy of codegraph.mjs's private edgesOfKind (kept local rather than
75
89
  * exported+imported to avoid coupling this file's commit boundary to concurrent
76
90
  * in-flight edits elsewhere in codegraph.mjs; both read the same relationKind
77
- * classification, so they cannot drift in meaning). */
91
+ * classification, so they cannot drift in meaning). Memoized per (graph, kind) —
92
+ * perf lever, HANDOVER follow-up #8: this is the query engine's hottest path,
93
+ * called repeatedly on the same (graph, kind) pair across a single query's
94
+ * compositional evaluation, and at monorepo scale (tens of thousands of modules)
95
+ * the repeated O(relations) scan is a real latency/GC cost — not a correctness
96
+ * fix (the stack-overflow bug this file's twin comment references is already
97
+ * fixed and unrelated). */
78
98
  function edgesOfKind(graph, kind) {
99
+ let byKind = askEdgesOfKindCache.get(graph);
100
+ if (!byKind) { byKind = new Map(); askEdgesOfKindCache.set(graph, byKind); }
101
+ const cached = byKind.get(kind);
102
+ if (cached) return cached;
79
103
  const out = [];
80
104
  // Plain-loop append, NOT out.push(...g.edges): argument spread overflows the call
81
105
  // stack past ~100k edges on graph-scale relation groups (see codegraph.mjs twin).
@@ -83,6 +107,7 @@ function edgesOfKind(graph, kind) {
83
107
  if (relationKind(g) !== kind) continue;
84
108
  for (const e of g.edges) out.push(e);
85
109
  }
110
+ byKind.set(kind, out);
86
111
  return out;
87
112
  }
88
113
 
@@ -980,6 +1005,19 @@ function uniqueById(inds) {
980
1005
  return out;
981
1006
  }
982
1007
 
1008
+ /** The Module individuals whose path lives strictly UNDER the directory named by
1009
+ * `term` — a proper path-segment prefix match (normPath(label).startsWith(dir +
1010
+ * "/")), never a bare substring, so "src/lib" cannot spuriously catch
1011
+ * "src/libfoo/x.mjs". Mirrors renderArchitecture's own pkg-prefix scoping
1012
+ * (codegraph.mjs) but returns individuals rather than a summary string — this is
1013
+ * the "membership" AST node's directory-scope branch (see its call site above). */
1014
+ function directoryScopeModules(graph, term) {
1015
+ const norm = normPath(term);
1016
+ if (!norm) return [];
1017
+ const prefix = `${norm}/`;
1018
+ return graph.individuals.filter((i) => i.class === "Module" && normPath(i.label).startsWith(prefix));
1019
+ }
1020
+
983
1021
  // Per-graph memo for the qualifier attribute/edge sets (exported symbols, tested
984
1022
  // modules, symbol→module map) — computed once, so a qualifier filter over a large
985
1023
  // result set stays cheap and deterministic.
@@ -1242,7 +1280,27 @@ function evalSet(graph, ast, opts) {
1242
1280
  return forwardOverSet(graph, ast.kind, ids);
1243
1281
  }
1244
1282
  case "membership": {
1283
+ // DIRECTORY SCOPE ("modules in src/lib", "files in src/handlers"): a bare
1284
+ // path term with no exact node of its own is a DIRECTORY, not a single
1285
+ // container individual — resolveObject's fuzzy tiers used to land it on ONE
1286
+ // arbitrarily-chosen module whose label merely CONTAINS the path substring
1287
+ // (e.g. "src/lib" fuzzy-matching "src/lib/logger.mjs"), then traversed that
1288
+ // one module's own membership edges for entityType "Module" — which a module
1289
+ // never has, so the answer was a false-empty ("no modules in this index.")
1290
+ // even though several modules genuinely live under the directory. An EXACT
1291
+ // node match (tier 1 — a real file/symbol named that) still wins outright
1292
+ // (unchanged single-container-node behavior, e.g. "methods in widget.mjs");
1293
+ // only when there is no exact match do we try directory-prefix scope first.
1245
1294
  const r = resolveObject(graph, ast.term);
1295
+ if (!(r.match && r.tier === 1)) {
1296
+ const dirMods = directoryScopeModules(graph, ast.term);
1297
+ if (dirMods.length) {
1298
+ if (!ast.entityType || ast.entityType === "Module") return dirMods;
1299
+ const ids = new Set(dirMods.map((m) => m.id));
1300
+ const objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids)));
1301
+ return objs.filter((o) => o.class === ast.entityType);
1302
+ }
1303
+ }
1246
1304
  if (!r.match) return [];
1247
1305
  const ids = new Set([r.match.id]);
1248
1306
  const objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids)));
package/src/chat.mjs CHANGED
@@ -1052,12 +1052,22 @@ async function moduleOrientLane(query, { graph }) {
1052
1052
  return { text: moduleOverviewText(graph, ind), via: "meta" };
1053
1053
  }
1054
1054
 
1055
- async function metaLane(query, { graph, memoryDir }) {
1055
+ async function metaLane(query, { graph, memoryDir, last = null }) {
1056
1056
  const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
1057
1057
  if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
1058
1058
  return { text: await memorySummary(memoryDir, graph), via: "meta" };
1059
1059
  }
1060
- if (META_ORIENT_RE.test(q)) return { text: orientationText(graph), via: "meta" };
1060
+ if (META_ORIENT_RE.test(q)) {
1061
+ // Chat-feel residual (0.8.2 confirmation playtest, follow-up #3): Bug B1 only
1062
+ // taught the isConversational-triggered orientation branch (below, via:"template")
1063
+ // to shorten on an identical repeat — this META_ORIENT_RE branch is a SEPARATE
1064
+ // route to the same class of full-blurb text ("what does this app do" reprinted
1065
+ // orientationText(graph) verbatim on every repeat, never collapsing). Mirrors
1066
+ // ORIENTATION_REPEAT_ONELINER's identity-check pattern exactly, with its own
1067
+ // distinct oneliner text (self-limiting for the same reason).
1068
+ const text = orientationText(graph);
1069
+ return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
1070
+ }
1061
1071
  // Bug E: an arbitrary "what does <term> do" that META_ORIENT_RE's closed noun
1062
1072
  // list didn't claim — try the module-grain overview before falling through to
1063
1073
  // the author-sha check below (disjoint triggers; order doesn't matter, but
@@ -1272,6 +1282,13 @@ const WALL_REPEAT_ONELINER = "still couldn't parse that — /help lists every qu
1272
1282
  * re-offers the full orientation instead of droning the one-liner forever. */
1273
1283
  const ORIENTATION_REPEAT_ONELINER = "still the same overview — /help lists every command and query shape.";
1274
1284
 
1285
+ /** metaLane's own repeat-suppression twin for META_ORIENT_RE ("what does this app
1286
+ * do", etc. — see metaLane's doc above) — a genuinely separate route to the same
1287
+ * orientation-class text that Bug B1 didn't cover. MUST differ from
1288
+ * ORIENTATION_REPEAT_ONELINER (a distinct string, checked by identity) so the two
1289
+ * independent repeat-suppression sites can never be confused with one another. */
1290
+ const META_ORIENT_REPEAT_ONELINER = "still the same overview — /stats for the full one, /help for commands.";
1291
+
1275
1292
  // ---- repo-root resolution: default the target to the GIT ROOT, not raw cwd ----
1276
1293
 
1277
1294
  /** The git top-level for `cwd`, or null if not in a repo (or git is unavailable).
@@ -2455,7 +2472,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
2455
2472
  // codebase", "how do i start") → a summary / orientation, answered before the
2456
2473
  // fact-dump readers so "what do you know" gets a summary, not raw facts.
2457
2474
  if (miss) {
2458
- const meta = await metaLane(query, { graph, memoryDir });
2475
+ const meta = await metaLane(query, { graph, memoryDir, last });
2459
2476
  if (meta) {
2460
2477
  answer = meta.text; via = meta.via; recordMiss = false; handled = true;
2461
2478
  note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
package/src/codegraph.mjs CHANGED
@@ -115,7 +115,7 @@ export function relationKind(group) {
115
115
 
116
116
  // ---- symbol resolution (exact → normalised path → substring) ------------------
117
117
 
118
- function normPath(s) {
118
+ export function normPath(s) {
119
119
  return String(s || "")
120
120
  .trim()
121
121
  .toLowerCase()
@@ -1166,12 +1166,31 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
1166
1166
  // history / call neighbours). Each answers ONE question in one compact call so
1167
1167
  // the agent need not Read/Grep. All keep the bounded-output discipline. -------
1168
1168
 
1169
+ /** Per-graph, per-kind memo for edgesOfKind's own flattened scan — WeakMap<graph,
1170
+ * Map<kind, edge[]>>, mirroring qualCache's (ask.mjs) established per-graph-object
1171
+ * caching convention: a loaded graph's `relations` are never mutated in place after
1172
+ * parseEntities builds it (every refresh constructs a NEW graph object), so caching
1173
+ * keyed on graph object identity is correctness-safe for a graph's whole lifetime —
1174
+ * same invariant qualCache already relies on in production. edgesOfKind is called
1175
+ * repeatedly on the SAME (graph, kind) pair across a single query's traversal
1176
+ * (evalSet/traverse/adjacencyForKinds/renderArchitecture/… all re-derive it), and at
1177
+ * monorepo scale (tens of thousands of modules) that repeated O(relations) scan is a
1178
+ * real latency/GC cost — this collapses every call after the first to an O(1) lookup. */
1179
+ const edgesOfKindCache = new WeakMap();
1180
+
1169
1181
  /** All edges whose relation classifies to `kind`, flattened across relation groups. */
1170
1182
  /** All edges of a classified relation kind (imports/calls/defines/tests/touches/inherits/
1171
1183
  * cochange/reexports/callsSymbol/touchesSymbol/contains — see relationKind/PROP_KIND above),
1172
1184
  * flattened across every raw relation group that classifies to it. Exported for ask.mjs's
1173
- * mechanical NL-query engine (PLAN_MECHANICAL_CHAT.md) to orchestrate rather than duplicate. */
1185
+ * mechanical NL-query engine (PLAN_MECHANICAL_CHAT.md) to orchestrate rather than duplicate.
1186
+ * Memoized per (graph, kind) — see edgesOfKindCache's own doc above (perf lever, HANDOVER
1187
+ * follow-up #8: latency/GC on monorepo-scale graphs, not a correctness fix — the earlier
1188
+ * stack-overflow bug below is already fixed and unrelated). */
1174
1189
  export function edgesOfKind(graph, kind) {
1190
+ let byKind = edgesOfKindCache.get(graph);
1191
+ if (!byKind) { byKind = new Map(); edgesOfKindCache.set(graph, byKind); }
1192
+ const cached = byKind.get(kind);
1193
+ if (cached) return cached;
1175
1194
  const out = [];
1176
1195
  // Plain-loop append, NOT out.push(...g.edges): argument spread materialises every
1177
1196
  // element as a call argument and overflows the stack past ~100k edges (live report:
@@ -1180,6 +1199,7 @@ export function edgesOfKind(graph, kind) {
1180
1199
  if (relationKind(g) !== kind) continue;
1181
1200
  for (const e of g.edges) out.push(e);
1182
1201
  }
1202
+ byKind.set(kind, out);
1183
1203
  return out;
1184
1204
  }
1185
1205
 
@@ -491,6 +491,13 @@ export const STOPWORDS = new Set([
491
491
  // the module alone). Same trade as every other stopword: a symbol literally named
492
492
  // "usually" would be the accepted residual cost.
493
493
  "usually", "typically", "generally", "normally", "often", "commonly", "mostly",
494
+ // modal auxiliaries ("what SHOULD i look at first") — found live: with no modal in
495
+ // this set, "should" reached the cascade's bounded fuzzy-correction step and landed
496
+ // within edit distance of the unrelated closed-vocab word "hold" ("defines" synonym,
497
+ // ask-vocab.mjs), corrupting the whole query into "what hold i at". Same trade as
498
+ // every other stopword: a symbol literally named "should" would be the accepted
499
+ // residual cost.
500
+ "should", "would", "could", "can", "will", "shall", "might", "must",
494
501
  ]);
495
502
 
496
503
  /** Split free text into words: trailing "?" run stripped, commas treated as