@polycode-projects/the-mechanical-code-talker 2.5.4 → 2.6.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
@@ -285,8 +285,8 @@ steps:
285
285
  - src/handlers/tasks.mjs (imports it) — tests: test/tasks.test.mjs
286
286
  - src/server/router.mjs (imports it) — tests: none recorded
287
287
  depth 2 (2):
288
- - src/handlers/users.mjs (imports it) — tests: none recorded
289
- - src/server/app.mjs (imports it) — tests: none recorded
288
+ - src/handlers/users.mjs (reaches it through an intermediary) — tests: none recorded
289
+ - src/server/app.mjs (reaches it through an intermediary) — tests: none recorded
290
290
  2. tmct_untested {}
291
291
  7 source module(s) with no covering test module:
292
292
  src/core/validate.mjs
@@ -54,7 +54,7 @@
54
54
  {"id":"nudge-commands","class":"nudge","register":"friendly","template":"If prose fails you, the slash commands always work — try {command}."}
55
55
  {"id":"nudge-narrower","class":"nudge","register":"friendly","template":"That matched {count} things — too many to be useful. Narrow it with a module or class name."}
56
56
  {"id":"conversational-greeting","class":"conversational","register":"friendly","template":"Hi. Ask me about this codebase — imports, calls, definitions, history — or /help."}
57
- {"id":"conversational-greeting-hello-there","class":"conversational","register":"friendly","template":"Hello there. (A hollow voice says, \"fool.\") Ask me about this codebase, or /help."}
57
+ {"id":"conversational-greeting-hello-there","class":"conversational","register":"friendly","template":"Hello there. Ask me about this codebase, or /help."}
58
58
  {"id":"conversational-greeting-good-morning","class":"conversational","register":"friendly","template":"Good morning. Ask me about this codebase, or /help."}
59
59
  {"id":"conversational-greeting-good-afternoon","class":"conversational","register":"friendly","template":"Good afternoon. Ask me about this codebase, or /help."}
60
60
  {"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.5.4",
3
+ "version": "2.6.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.",
@@ -225,7 +225,7 @@ function parseComposite(text, nlp) {
225
225
  || parseQualifierCheck(w, lc)
226
226
  || parseUniversal(w, lc, nlp)
227
227
  || parseNegation(text, nlp, 0)
228
- || parseNegatedAsk(w, lc)
228
+ || parseNegatedAsk(w, lc, nlp)
229
229
  || parseForwardNegation(w, lc, nlp)
230
230
  || parseTemporal(w, lc, nlp, 0)
231
231
  || parseCommitFilter(w, lc)
@@ -718,11 +718,18 @@ function parseQualifierCheck(w, lc) {
718
718
  * the same sentence at positive polarity ("not" is no stopword) and the merge
719
719
  * would call the two readings an ambiguity. Declining to a composite production
720
720
  * is what keeps the sentence out of that merge. */
721
- function parseNegatedAsk(w, lc) {
722
- if (lc[0] !== "do" && lc[0] !== "does" && lc[0] !== "did") return null;
721
+ function parseNegatedAsk(w, lc, nlp) {
722
+ // The copular leads carry the PASSIVE twin ("is X not imported by Y")
723
+ // without them the strategies read the sentence at positive polarity (the
724
+ // "not" drops as noise) and the bare "Yes" answers the un-negated question.
725
+ // parseQualifierCheck ran first, so "is X not deprecated" keeps its lane.
726
+ // The positive re-parse runs the SAME simple-clause pair the composer's
727
+ // fragments use — the passive lives in the keyword strategy, which the
728
+ // anchored grammar alone never reaches.
729
+ if (!["do", "does", "did", "is", "are", "was", "were"].includes(lc[0])) return null;
723
730
  const notIdx = lc.indexOf("not", 1);
724
731
  if (notIdx < 0) return null;
725
- const positive = parseAnchored(w.filter((_, i) => i !== notIdx).join(" "));
732
+ const positive = parseSimpleClause(w.filter((_, i) => i !== notIdx).join(" "), nlp);
726
733
  if (!positive || positive.shape !== "ask") return null;
727
734
  return { ...positive, negated: true };
728
735
  }
@@ -1784,7 +1791,15 @@ function evalTemporal(graph, ast, opts) {
1784
1791
  }
1785
1792
 
1786
1793
  function evalSuperlative(graph, ast) {
1787
- const pool = graph.individuals.filter((i) => i.class === ast.entityType);
1794
+ let pool = graph.individuals.filter((i) => i.class === ast.entityType);
1795
+ // A tests-metric ranking over Modules surveys COVERAGE, and a test module
1796
+ // is never a coverage target — the same exclusion renderUntested (the
1797
+ // /untested surface) applies, so the two surveys can't disagree about the
1798
+ // same set ("what most needs a test" used to name b.test.mjs).
1799
+ if (ast.metric?.kind === "tests" && ast.entityType === "Module") {
1800
+ const testSubjects = new Set(edgesOfKind(graph, "tests").map((e) => e.subject));
1801
+ pool = pool.filter((i) => !testSubjects.has(i.id) && !isTestPath(String(i.label).toLowerCase()));
1802
+ }
1788
1803
  const scored = pool.map((ind) => ({ ind, score: degreeMetric(graph, ind, ast.metric) }))
1789
1804
  .sort((a, z) => (ast.extreme === "most" ? z.score - a.score : a.score - z.score));
1790
1805
  if (!scored.length) return { compositeKind: "superlative", entityType: ast.entityType, matches: [] };
@@ -2608,7 +2623,10 @@ export function resolveObject(graph, term, opts = {}) {
2608
2623
  }
2609
2624
  return declineOnUnplacedWords(resolveObjectCore(graph, term, opts), term);
2610
2625
  }
2611
- return resolveObjectCore(graph, term, opts);
2626
+ // The pinned-class branch honors the same contract — a term carrying words
2627
+ // the index has no reading for declines instead of resolving past them
2628
+ // ("the old Task" pinned to Class must not silently swallow "old").
2629
+ return declineOnUnplacedWords(resolveObjectCore(graph, term, opts), term);
2612
2630
  }
2613
2631
 
2614
2632
  /** Resolve a term that may be a context pronoun ("this"/"it"/"that"/"here") —
@@ -2979,7 +2997,10 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2979
2997
  if (entityType && entityType !== "Change") {
2980
2998
  const wantClasses = classesForKinds(graph, fwdKinds);
2981
2999
  const siblingClass = FINE_CLASS_SIBLING[entityType];
2982
- if (!wantClasses.has(entityType) && !(siblingClass && wantClasses.has(siblingClass))) {
3000
+ // An edge-less kind (wantClasses empty) is not a grain mismatch — there
3001
+ // is nothing to name after "only …", and the render leaked a literal
3002
+ // "undefined" there. Fall through to the plain empty-edges answer.
3003
+ if (wantClasses.size && !wantClasses.has(entityType) && !(siblingClass && wantClasses.has(siblingClass))) {
2983
3004
  return {
2984
3005
  matches: [], objMatch, candidates, ambiguous, matchedVia,
2985
3006
  forwardGrainMiss: true, wantClasses: [...wantClasses],
@@ -3451,13 +3472,48 @@ function renderCore(parsed, result, graph) {
3451
3472
  if (result.ambiguous) {
3452
3473
  // Name the actual candidates in the prose, not just the structured field
3453
3474
  // — "narrow the term" isn't actionable if the reader can't see the options.
3454
- const pool = [result.objMatch, ...(result.candidates || [])].filter(Boolean);
3475
+ let pool = [result.objMatch, ...(result.candidates || [])].filter(Boolean);
3476
+ let branches = result.branches;
3477
+ // A relation question's object slot is a code entity, so a Commit/Session
3478
+ // candidate (a prose-tier hit on a commit MESSAGE) is grain noise in the
3479
+ // did-you-mean — dropped, along with its branch, unless the question is
3480
+ // itself about touches/history where a commit genuinely fits.
3481
+ if (parsed.kind && parsed.kind !== "touches" && !pool.every((i) => i.class === "Commit")) {
3482
+ const grainOk = (i) => !["Commit", "Session", "Source", "Utterance"].includes(i?.class);
3483
+ const kept = pool.filter(grainOk);
3484
+ if (kept.length) {
3485
+ pool = kept;
3486
+ if (branches?.length) branches = branches.filter((b) => grainOk(b.candidate));
3487
+ }
3488
+ }
3489
+ // The nearest REAL neighbour joins the list: a misremembered symbol
3490
+ // ("saveTask") shares an identifier word with the real one ("saveStore"),
3491
+ // which no containment/prose tier ever surfaces. Ranked by shared
3492
+ // camelCase-split words (≥3 chars), edit distance breaking ties.
3493
+ if (graph && !/[/.]/.test(String(parsed.object || ""))) {
3494
+ const tLc = String(parsed.object || "").toLowerCase();
3495
+ const termWords = new Set(splitIdentifierWords(String(parsed.object || "")).filter((w) => w.length >= 3));
3496
+ const already = new Set(pool.map((i) => i.id));
3497
+ let nearest = null;
3498
+ let bestShared = 0;
3499
+ let bestD = Infinity;
3500
+ if (termWords.size) {
3501
+ for (const i of graph.individuals) {
3502
+ if (!["Function", "Method", "Class", "GlobalVariable", "Attribute"].includes(i.class) || already.has(i.id)) continue;
3503
+ const shared = splitIdentifierWords(String(i.label)).filter((w) => termWords.has(w)).length;
3504
+ if (!shared || shared < bestShared) continue;
3505
+ const d = editDistance(String(i.label).toLowerCase(), tLc, 8);
3506
+ if (shared > bestShared || d < bestD) { nearest = i; bestShared = shared; bestD = d; }
3507
+ }
3508
+ }
3509
+ if (nearest) pool = [...pool, nearest];
3510
+ }
3455
3511
  const noun = pool.length && pool.every((i) => i.class === "Commit") ? "commit" : "module";
3456
3512
  const shown = pool.slice(0, OVERFLOW_CAP).map((i) => i.label);
3457
3513
  const extra = pool.length > OVERFLOW_CAP ? `, …and ${pool.length - OVERFLOW_CAP} more` : "";
3458
3514
  const lead = `"${parsed.object}" matches more than one ${noun} ambiguously — did you mean ${listJoin(shown)}${extra}? Try one of those. If you're not sure, narrow it to one name.`;
3459
- const content = (result.branches && result.branches.length)
3460
- ? `${lead}\n${result.branches.map((b, i) => `${i + 1}) ${b.candidate.label}: ${b.rendered.content}`).join("\n")}`
3515
+ const content = (branches && branches.length)
3516
+ ? `${lead}\n${branches.map((b, i) => `${i + 1}) ${b.candidate.label}: ${b.rendered.content}`).join("\n")}`
3461
3517
  : lead;
3462
3518
  return {
3463
3519
  content, miss: false, ambiguous: true, candidates: pool.map((i) => i.label),
@@ -413,7 +413,11 @@ export function renderImpact(graph, ind, { maxDepth = 8 } = {}) {
413
413
  const tests = dep.tests.length
414
414
  ? `tests: ${capJoin(dep.tests, IMPACT_TESTS_PER_DEP)}`
415
415
  : "tests: none recorded";
416
- lines.push(` - ${dep.label} (${dep.via} it) ${tests}`);
416
+ // Past depth 1 the via verb describes the hop to the dependent's own
417
+ // parent, not an edge to the changed module — saying "imports it" there
418
+ // would claim a direct edge that does not exist.
419
+ const receipt = i === 0 ? `${dep.via} it` : "reaches it through an intermediary";
420
+ lines.push(` - ${dep.label} (${receipt}) — ${tests}`);
417
421
  }
418
422
  if (level.length > IMPACT_PER_DEPTH) lines.push(` …+${level.length - IMPACT_PER_DEPTH} more at depth ${i + 1}`);
419
423
  });
@@ -425,7 +429,7 @@ export function renderImpact(graph, ind, { maxDepth = 8 } = {}) {
425
429
  lines.push(
426
430
  "warning: partial edge lists (" +
427
431
  truncatedStructural.map((t) => `${t.predicate}: ${t.shown}/${t.count}`).join(", ") +
428
- ") — this closure may be missing edges. Cross-check critical results with tmct_search.",
432
+ ") — this closure may be missing edges. Cross-check critical results with a lexical search (/find <term>).",
429
433
  );
430
434
  }
431
435
  return lines.join("\n");
@@ -132,7 +132,7 @@ const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(
132
132
  const ACK_PREAMBLE_RE = /^(?:(?:ok(?:ay)?|aight|cool|alright|sure|right|fine|great|nice|got it|gotcha|sounds good|no worries|no problem)[\s,]+)+(.+)$/i;
133
133
  /** Self-orientation lead-in with a delimiter — "just poking around, <Q>",
134
134
  * "first time using this, <Q>". */
135
- const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here))\s*[,.—–-]\s*(.+)$/i;
135
+ const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here)|i'?m\s+new\s+(?:here|around\s+here|to\s+(?:this|all\s+this)(?:\s+(?:repo|codebase|project|app|tool|thing))?))\s*[,.—–-]\s*(.+)$/i;
136
136
  /** Repeated leading hedge adverb before a polite request verb ("maybe
137
137
  * possibly tell me <Q>"). No delimiter required, unlike ACK_PREAMBLE_RE. */
138
138
  const HEDGE_ADVERB_PREAMBLE_RE = /^(?:(?:maybe|possibly|perhaps)\s+)+(.+)$/i;
@@ -41,6 +41,7 @@ export const PRECOND = Object.freeze({
41
41
  graphLoaded: "cap:graph-loaded", // a graph artifact is present + parseable
42
42
  resolves: "cap:resolves", // { param, as } — the slot binds to an entity of kind `as`
43
43
  anyPresent: "cap:any-present", // { params } — at least one of these slots is provided
44
+ memoryFacts: "cap:memory-facts", // the conversational-memory store holds relation facts for the term
44
45
  });
45
46
 
46
47
  // ---- capability builder (returns PLAIN FROZEN data) -------------------------
@@ -57,6 +58,11 @@ const resolves = (paramName, as) =>
57
58
  /** any-present(params) — search-style disjunction (query OR kind must be given). */
58
59
  const anyPresent = (params) =>
59
60
  Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.anyPresent, params: Object.freeze([...params]) });
61
+ /** memory-facts — the memory graph holds mgx:synonym / mgx:relatedTo / mgx:similarTo
62
+ * facts for the term. The memory-graph sibling of graph-loaded: the SKOS view
63
+ * answers from the conversational-memory store and misses honestly without it,
64
+ * with or without a code-map graph. */
65
+ const memoryFacts = () => Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.memoryFacts });
60
66
 
61
67
  /** Add-effect: after the call the agent knows `topic` about `?of`. */
62
68
  const knows = (topic, ofParam = null) =>
@@ -80,7 +86,8 @@ function capability({ name, label, question, params = [], preconditions = [], ad
80
86
  // Arg keys verified against src/tools/server.mjs `dispatchTool`'s switch: describe/callers/
81
87
  // callees/tests/history/… take `symbol`; impact/exports take `module`; members/
82
88
  // subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
83
- // architecture takes an optional `package`; untested takes nothing.
89
+ // architecture takes an optional `package`; untested takes nothing; related
90
+ // takes `term` (a memory-graph concept term).
84
91
 
85
92
  const CAPABILITIES = Object.freeze([
86
93
  capability({
@@ -107,9 +114,9 @@ const CAPABILITIES = Object.freeze([
107
114
  add: [knows("signature", "symbol")],
108
115
  }),
109
116
  capability({
110
- name: "tmct_impact", label: "impact", question: "what a change to this module reaches (impact closure)",
111
- params: [param("module", KINDS.Module)],
112
- preconditions: [graphLoaded(), resolves("module", KINDS.Module)],
117
+ name: "tmct_impact", label: "impact", question: "what a change to this module or symbol reaches (impact closure)",
118
+ params: [param("module", KINDS.Symbol, { note: "a Module, or any sited symbol — a fine-grained seed walks callsSymbol dependents and coarsens them to module grain" })],
119
+ preconditions: [graphLoaded(), resolves("module", KINDS.Symbol)],
113
120
  add: [knows("impact", "module")],
114
121
  }),
115
122
  capability({
@@ -178,6 +185,12 @@ const CAPABILITIES = Object.freeze([
178
185
  preconditions: [graphLoaded()],
179
186
  add: [knows("architecture", "package")],
180
187
  }),
188
+ capability({
189
+ name: "tmct_related", label: "related", question: "a term's synonyms and related concepts (the SKOS view over the conversational-memory graph)",
190
+ params: [param("term", KINDS.Query, { note: "a concept term, matched against memory relation facts rather than resolved in the code graph" })],
191
+ preconditions: [memoryFacts()],
192
+ add: [knows("related", "term")],
193
+ }),
181
194
  ]);
182
195
 
183
196
  // The live capability set: the built-in frozen array is the seed; registration
@@ -56,8 +56,10 @@ export const UNMAPPED_KINDS = Object.freeze({
56
56
 
57
57
  // ---- capabilities the NL surface cannot reach today (named, not accidental) ---
58
58
  // A declared capability with no NL/command/frame path is a routing gap and must be tagged
59
- // here with the reason. Every capability is currently reachable, so this is empty.
60
- export const NOT_NL_REACHABLE = Object.freeze({});
59
+ // here with the reason.
60
+ export const NOT_NL_REACHABLE = Object.freeze({
61
+ tmct_related: "the SKOS synonym/related surface is served by the chat lane's own recogniser over the memory graph; a router frame for it needs memory-term binding, which resolveObject (code-graph-only) does not prove yet",
62
+ });
61
63
 
62
64
  // ---- imperative intent FRAMES (fills what the relational grammar and command register
63
65
  // both miss). regex -> { topic, arg | noArg }. Ordered: first match wins.