@polycode-projects/the-mechanical-code-talker 0.9.2 → 0.9.4

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.2",
3
+ "version": "0.9.4",
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/chat.mjs CHANGED
@@ -2052,6 +2052,32 @@ function relationTermOf(query, envelope) {
2052
2052
  return null;
2053
2053
  }
2054
2054
 
2055
+ /** A closed "describe"-intent wrapper: "can you describe X for me", "could you
2056
+ * tell me about X", "tell me more about X" → attempt tmct_describe(X). Found
2057
+ * live (playtest sprint round 2, SKILL_PLAYTEST_SPRINT.md): a describe-intent
2058
+ * question wrapped in an ordinary polite request ("can you tell me more about
2059
+ * Controller") fell all the way to the generic wall despite naming a real,
2060
+ * just-listed entity — nothing recognized the wrapper at all. Same closed
2061
+ * lead-in-alternation discipline as GREETING_PREAMBLE_RE/THANKS_PREAMBLE_RE
2062
+ * (normalize.mjs). Deliberately used only as a LAST-RESORT lane (see its call
2063
+ * site below) — "tell me about X" is ALSO the relation/concept force's own
2064
+ * trigger phrase for enumerable concepts ("tell me about inheritance"), so
2065
+ * this must never run before those have had their chance. */
2066
+ const DESCRIBE_WRAPPER_RE =
2067
+ /^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?about|describe)\s+(.+?)(?:\s+for\s+me)?\s*\??$/i;
2068
+
2069
+ async function describeWrapperAnswer(query, { config, source }) {
2070
+ const m = DESCRIBE_WRAPPER_RE.exec(String(query || "").trim());
2071
+ const term = m?.[1]?.trim();
2072
+ if (!term) return null;
2073
+ try {
2074
+ const text = await dispatchTool("tmct_describe", { symbol: term }, { config, source });
2075
+ return text ? { text } : null;
2076
+ } catch {
2077
+ return null; // unresolvable term — decline, the ordinary wall stands unchanged
2078
+ }
2079
+ }
2080
+
2055
2081
  /** THE RELATION CONCEPT FORCE — compose the three-band answer (curated relation
2056
2082
  * definition + real example EDGES + pre-validated follow-ups) for a vague touch on a
2057
2083
  * relation/edge kind ("what about imports", "what are the calls", "tell me about
@@ -2369,6 +2395,20 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
2369
2395
  const nudged = nudgeAnswer(query, newFocus);
2370
2396
  if (nudged) { answer = nudged; via = "miss"; }
2371
2397
  }
2398
+ // (4d) DESCRIBE-WRAPPER RESCUE (playtest sprint round 2, SKILL_PLAYTEST_SPRINT.md)
2399
+ // — "can you describe X for me" / "tell me more about X": a closed wrapper
2400
+ // around an ordinary polite request naming a symbol. Tried ONLY here, after
2401
+ // EVERY other lane (concept force, relation force, teach, author,
2402
+ // presupposition, capability nudges) has already declined — "tell me about
2403
+ // inheritance" must keep reaching the relation force's richer answer
2404
+ // unmolested (that regression is exactly why this ISN'T inside asBareCommand,
2405
+ // which runs first and would preempt every lane above). A last-resort rescue
2406
+ // for what would otherwise become the generic wall, never a competing route:
2407
+ // it only claims the turn if /describe actually resolves the captured term.
2408
+ if (miss && recordMiss && via === "composed") {
2409
+ const described = await describeWrapperAnswer(query, { config, source });
2410
+ if (described) { answer = described.text; via = "describe"; recordMiss = false; }
2411
+ }
2372
2412
  // (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
2373
2413
  // wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
2374
2414
  // WALL KINDNESS (0.8.2 WS4 (a)): when the PREVIOUS turn's answer was already a
@@ -485,6 +485,12 @@ export const STOPWORDS = new Set([
485
485
  // literally named "last" would be the accepted residual cost, same trade as
486
486
  // every other stopword.
487
487
  "last",
488
+ // frequency-adverb filler ("what does X usually change together with", "what does
489
+ // X typically call") — found live: "usually" glued onto the object term instead of
490
+ // being stripped, corrupting resolution ("src/core/store.mjs usually" instead of
491
+ // the module alone). Same trade as every other stopword: a symbol literally named
492
+ // "usually" would be the accepted residual cost.
493
+ "usually", "typically", "generally", "normally", "often", "commonly", "mostly",
488
494
  ]);
489
495
 
490
496
  /** Split free text into words: trailing "?" run stripped, commas treated as
@@ -150,6 +150,21 @@ export function parseKeywordSpot(text, nlp = null) {
150
150
  const consumed = new Set();
151
151
  const mark = (hit) => { if (hit) for (let i = hit.start; i < hit.end; i += 1) consumed.add(i); };
152
152
  mark(verbHit);
153
+ // A redundant SAME-KIND verb immediately after the matched one ("what TESTS
154
+ // cover X" — "tests" is read as the relation trigger, but "cover" — also a
155
+ // `tests` verb, ask-vocab.mjs's own synonym list — is the word the sentence
156
+ // actually intends as the verb) would otherwise fall into afterText raw and
157
+ // corrupt the object ("cover src/core/model.mjs" instead of the module alone).
158
+ // Anchored to the exact position right after verbHit (never a general re-scan),
159
+ // and gated on matching the SAME kind, so this can only strip a genuine
160
+ // restatement, never eat a real object term. Found live: "what tests cover X",
161
+ // "which tests test X" both misparsed this way before this fix.
162
+ for (const [phrase, kind] of Object.entries(VERB_TO_KIND)) {
163
+ if (kind !== verbHit.kind) continue;
164
+ const pWords = phrase.split(" ");
165
+ const start = verbHit.end;
166
+ if (pWords.every((w, j) => canonWords[start + j] === w)) { mark({ start, end: start + pWords.length }); break; }
167
+ }
153
168
  const entityHit = findPhrase(canonWords, ENTITY_TO_TYPE, consumed);
154
169
  mark(entityHit);
155
170
  const modifierHit = findPhrase(canonWords, MODIFIER_TO_KIND, consumed);