@polycode-projects/the-mechanical-code-talker 0.9.10 → 0.9.12

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/ROADMAP.md CHANGED
@@ -650,13 +650,45 @@ outward from high-trust premises (the provenance primitive) toward novel, releva
650
650
  timestamping each so recency and novelty stay computable. The mechanics are the easy half
651
651
  (bounded forward chaining materializes entailments; backward chaining from frequent query shapes
652
652
  pre-derives likely answers). **The residual hard half is still the FRAME PROBLEM / relevance
653
- realization — unsolved in the general case and not pretended otherwise;** trust+novelty+relevance
654
- are the tractable approximation, not a solution. The plan's job is
655
- to make it TRACTABLE in tmct's narrow, closed world, not to solve it: usefulness is approximated
656
- from what the system actually gets asked (query-shape frequency), what connects to recent focus,
657
- what a cheap forward step yields that isn't already stored, and a hard
658
- budget (inference is bounded, its output trust-scored via the provenance primitive, and anything
659
- speculative is retractable and never outranks a stated fact). Everything else is deferred to the
653
+ realization — unsolved in the general case and not pretended otherwise.** This is not one problem
654
+ but two, of different hardness (full literature + citations in `PLAN_CAPABILITY_ROUTER.md`'s "The
655
+ open-world boundary" section):
656
+
657
+ - **The frame-*axiom* problem solved, inside a declared world.** McCarthy & Hayes named it in
658
+ 1969 ("Some Philosophical Problems from the Standpoint of Artificial Intelligence", *Machine
659
+ Intelligence* 4); Reiter's 1991 successor-state axioms and Kowalski & Sergot's 1986 event
660
+ calculus (*New Generation Computing* 4(1)) both solve the narrow reading — stating what changes
661
+ without enumerating what doesn't — inside a **declared** effect/predicate model. That's exactly
662
+ the OWL base the Syllogist forward-chains over: the axioms and rules are declared, so applying
663
+ them is mechanical, bounded, and already shipped (`src/syllogise.mjs`).
664
+ - **The relevance-*bounding* problem — genuinely open, and possibly not just unbuilt.** Given the
665
+ unbounded set of entailments a rich KB licenses, which ones are worth materializing *before
666
+ anyone asks* — without an oracle telling the pass what matters? That is McCarthy's deeper,
667
+ unsolved reading, and it has no known algorithm. It is also, independently, the central problem
668
+ a live cognitive-science literature has converged on: Vervaeke, Lillicrap & Richards ("Relevance
669
+ Realization and the Emerging Framework in Cognitive Science", *Journal of Logic and Computation*
670
+ 22(1):79–99, 2012) frame it as the pervasive problem cognitive science keeps rediscovering: Jaeger,
671
+ Riedl, Djedovic, Vervaeke & Walsh ("Naturalizing relevance realization: why agency and cognition
672
+ are fundamentally not computational", *Frontiers in Psychology*, 2024) go further and argue —
673
+ contestably, but rigorously, not as a popular-science claim — that relevance realization
674
+ *cannot* be an algorithmic process at all, by an analogy to Gödelian incompleteness. Take that as
675
+ a live, unresolved argument, not a proof: the honest position is that tmct doesn't know whether
676
+ this is "hard" or "impossible", and says so.
677
+
678
+ trust+novelty+relevance (query-shape frequency, recent-focus connectivity, a hard depth/budget
679
+ cap) are the **tractable approximation** the plan actually ships — a proxy for relevance, not a
680
+ solution to it, and openly so. **A speculative angle, still respecting no-LLM-in-product:** the
681
+ same bounded-region trick sketched for the router's open-world goal recognition
682
+ (`PLAN_CAPABILITY_ROUTER.md`) applies here. Instead of trying to bound relevance globally (the
683
+ open problem above), bound it *per query-shape*: a query shape already declares which
684
+ predicates/fluents it touches — it's how `parseQuery` resolves it — so restricting speculative
685
+ forward-chaining to premises reachable within N hops of an **observed** query shape's declared
686
+ predicates is a *structurally*-bounded relevance filter, not a learned or statistical one. It is
687
+ narrower than "what's relevant in general" (that stays open) but might be enough to keep
688
+ materialization from drifting into computing entailments nobody will ever ask about — trading
689
+ "relevant to anyone" (unsolved) for "relevant to what this system has actually been asked"
690
+ (a proxy, but a principled, deterministic one). This has not been built or measured; it is a
691
+ candidate for the plan's next spike, not a claimed result. Everything else is deferred to the
660
692
  plan's open questions, where the relevance problem is named as the open research risk it is.
661
693
 
662
694
  ### Open-source the ACE-OWL parser as a standalone library
@@ -811,7 +843,72 @@ critical build path:
811
843
  ## Phase LATER — recognized, deferred, not now
812
844
 
813
845
  Features we have deliberately shaped seams for but will not build until the phases above have
814
- earned them:
846
+ earned them. **Not everything below is deferred for the same reason** — the design horizon,
847
+ stated explicitly (2026-07-08 research pass):
848
+
849
+ ### The design horizon
850
+
851
+ **Before the horizon — known-how, not-yet-built, no research risk.** Sequencing or engineering
852
+ debt: the technique exists (in tmct's own prior work or the wider literature), building it is a
853
+ matter of scheduling and effort, not discovery. Everything shipped this session lives here, plus:
854
+ tone-of-voice adaptation (below — deliberately dropped by design choice, not unsolved);
855
+ tier-4 learn-on-miss (below — prerequisites not yet met, not research-blocked); `PLAN_CODE.md`
856
+ Tracks 2–4 (mutation search/repair, JS/HTML/CSS synthesis — APR and CEGIS are established
857
+ techniques); `PLAN_OSS_ACE_PARSER.md` (pure extraction/packaging); OWL 2 RL forward-chaining and
858
+ DL tableau consistency checking (`PLAN_INFERENCE_TESTING.md` stages 3–5 — the W3C's own OWL 2 RL
859
+ profile is a published, complete rule table; Pellet/HermiT/RDFox/Jena are real production
860
+ reasoners built on solved theory); RETE/incremental forward-chaining (same doc — Forgy 1982 is a
861
+ citable, portable algorithm); contingent/conformant planning under initial-state uncertainty
862
+ (`PLAN_CAPABILITY_ROUTER.md` — Bonet & Geffner 2000, Hoffmann & Brafman 2006, Petrick & Bacchus
863
+ 2002 all have working algorithms); ordinary closed-domain anaphora resolution (`nextFocus`,
864
+ already shipped, plus a real theoretical grounding available in Grosz/Joshi/Weinstein's centering
865
+ theory, 1995).
866
+
867
+ **After the horizon — genuinely unsolved in the field, or abandoned by the field in favor of
868
+ approaches tmct's no-LLM ethos rules out.** Named as real research targets, with citations, not
869
+ stop signs (full detail + full citation lists in each owning doc):
870
+ - **The frame problem / relevance realization** — the open-world planning boundary
871
+ (`PLAN_CAPABILITY_ROUTER.md`'s "The open-world boundary" section; this doc's tier-5 Syllogist
872
+ paragraph below). McCarthy & Hayes 1969 named it; Jaeger, Riedl, Djedovic, Vervaeke & Walsh
873
+ (2024) argue it may not be algorithmically solvable in the general case at all. Speculative
874
+ angle recorded: bounded (N+1) goal recognition — recognize declared goal 1..N, or reject to an
875
+ explicit "escalate" class, via parse-shape membership (the same mechanism Bug 8's domain gate
876
+ already uses) — not published anywhere found.
877
+ - **Symbolic (non-neural) dependency parsing at real coverage** — `PLAN_ADVANCED_GRAMMAR.md`
878
+ track (c). Largely abandoned by mainstream NLP research once neural parsers won CoNLL
879
+ 2017/2018, not disproven at any fixed data budget. Speculative angle: a hand-built,
880
+ closed-vocabulary disjunct/category dictionary (Link Grammar/CCG-style) scoped only to tmct's
881
+ own closed relation vocabulary, registered as another additive interpretation strategy.
882
+ - **Winograd-hard commonsense coreference** — `PLAN_ADVANCED_GRAMMAR.md` track (g). Genuinely
883
+ open without either massive statistical priors (ruled out) or a full commonsense KB (Cyc's
884
+ decades-long cautionary history). Speculative angle: tmct's own closed, complete graph makes a
885
+ *narrow slice* of Winograd-shaped ambiguity a graph-query-filtering problem rather than
886
+ open-domain commonsense reasoning — explicitly not the same as solving Winograd.
887
+ - **Bounded, incremental, trust-tiered, retraction-safe justification tracking** —
888
+ `PLAN_INFERENCE_TESTING.md`'s stage-3/4/5 discussion. Doyle's JTMS (1979) and de Kleer's ATMS
889
+ (1986) solve retraction; DRed/RDFox's Backward-Forward solve incremental Datalog maintenance;
890
+ nobody has published the specific combination with tmct's multi-trust-tier, hard-budget
891
+ requirement. Speculative angle: an ATMS-lite extension to `syllogise.mjs`'s currently-flat
892
+ provenance tag, sketched but unbuilt.
893
+ - **A shared ~2M-word cross-domain ontology (1M general-English base + 1M
894
+ technical/scientific/engineering/programming-language/slang)** — `PLAN_ontology-hierarchies.md`
895
+ §7, additive to (not a revision of) that doc's existing track (e), which stays about importing
896
+ raw WordNet into tmct's own small tier-1 corpus specifically. Walked into, not avoided: merging
897
+ two 1M-word vocabularies collides senses of lexically-shared words (`class`, `cache`, `thread`,
898
+ `wave`, `cell`, `field`, `state`, …) across general/CS/physics/biology/slang registers —
899
+ knowledge-based (non-neural) WSD is real but measurably weaker than supervised/neural WSD (Lesk
900
+ 1986; Raganato, Camacho-Collados & Navigli, EACL 2017), and BabelNet (Navigli & Ponzetto,
901
+ *Artificial Intelligence* 193, 2012) proves automatic cross-resource sense merging at this scale
902
+ is achievable — but its own pipeline moved toward statistical/graph-ML methods as it scaled,
903
+ solves the cross-*lingual* not cross-*domain* axis, and carries a non-commercial licence, so it
904
+ is a precedent, not a usable vehicle. Speculative angle recorded: mutual disambiguation from
905
+ already-resolved neighbouring terms in tmct's own closed graph (a structurally-bounded,
906
+ deterministic reading of Gale/Church/Yarowsky's "one sense per discourse/collocation"
907
+ regularities) — not published anywhere found for this application.
908
+
909
+ Every item above is honestly labeled speculative — a direction recorded so it isn't
910
+ re-discovered from scratch, not a committed build plan. None of it is scheduled; the phases above
911
+ this line are still the actual near-term work.
815
912
 
816
913
  ### Tone-of-voice adaptation (dropped from Phase 6, 2026-07-05)
817
914
  Per-voice synonym/phrase substitution over prose spans. Dropped because tmct's protected-span
@@ -60,10 +60,12 @@
60
60
  {"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
61
61
  {"id":"conversational-thanks","class":"conversational","register":"friendly","template":"Any time. Ask another, or /help for what I can do."}
62
62
  {"id":"conversational-farewell","class":"conversational","register":"friendly","template":"Bye — flushing the session log. Come back with a question any time."}
63
- {"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import {example1}\n what calls {example2}\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
63
+ {"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I'm tmct — a deterministic, offline code-graph assistant (no LLM). I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import {example1}\n what calls {example2}\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
64
64
  {"id":"miss-no-previous-answer","class":"miss","register":"friendly","template":"No previous answer to expand yet — ask me a question first, then say \"why\" or \"say more\"."}
65
- {"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. There's no code graph loaded here — for code structure (imports, calls, definitions) I need a `.tmct/graph.json`: point me at one with `--repo <path>`, or try the shipped example `npm run example:mini`. (tmct reads graphs; it doesn't index code itself.) For general vocabulary, `tmct init` seeds concepts — try \"what is a cache\". /help for commands."}
66
- {"id":"orientation-empty","class":"orientation","register":"friendly","template":"There's no code graph loaded here, so I can't answer structure questions (imports, calls, definitions) yet.\nFor those I need a `.tmct/graph.json` produced by a graph producer — point me at one with `--repo <path>`, or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself.\nFor general vocabulary, `tmct init` seeds concepts — try \"what is a cache\". /help for commands, /memory for what I remember."}
65
+ {"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. I'm tmct. {vocabHint} Point me at a repo with `--repo <path>` for code-structure questions too (imports, calls, definitions). /help for commands."}
66
+ {"id":"orientation-empty","class":"orientation","register":"friendly","template":"I'm tmct — a deterministic, offline chat assistant (no LLM). {vocabHint} /memory for what I remember.\nFor code structure (imports, calls, definitions) point me at a repo: `--repo <path>`, or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself.\n/help for commands."}
67
+ {"id":"identity-self","class":"conversational","register":"friendly","template":"I'm tmct — a deterministic, offline chat assistant. No LLM: wink-nlp parsing over a seeded ontology/lexicon, plus a code graph when you point me at a repo with `--repo <path>`. /help for commands, /stats for an overview."}
68
+ {"id":"identity-not-an-llm","class":"conversational","register":"friendly","template":"No — no LLM involved. tmct is deterministic: wink-nlp parsing over a graph/ontology, not a language model. /help for commands."}
67
69
  {"id":"technical-density","class":"count","register":"technical","template":"{subject} carries {count} {noun} across {scope} — a concentration well above what a codebase of this size typically sustains ({provenance})."}
68
70
  {"id":"technical-comparison","class":"count","register":"technical","template":"At {count} {noun}, {subject} sits {comparison} the comparable-project baseline, a divergence that reflects deliberate structure rather than measurement noise ({provenance})."}
69
71
  {"id":"technical-superlative","class":"count","register":"technical","template":"No {noun} in {scope} is more {metric} than {subject}; it leads the next candidate by a clear margin of {count} ({provenance})."}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
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
@@ -54,6 +54,7 @@ import { loadTemplates, render as renderTemplate } from "./corpus/templates.mjs"
54
54
  import { finish } from "./finish.mjs";
55
55
  import { VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE } from "./ask-vocab.mjs";
56
56
  import { COUNTERFACTUAL_RE } from "./interpret/normalize.mjs";
57
+ import { fuzzyMatchInSet, fuzzyBound } from "./interpret/fuzzy.mjs";
57
58
 
58
59
  // uuidv7 lives in ./uuid.mjs (shared with telemetry + the bench stamp); re-exported
59
60
  // here because callers/tests still import it from chat.mjs.
@@ -497,15 +498,38 @@ export function renderStats(graph) {
497
498
 
498
499
  // ---- friendly handling of non-structural / conversational input ----
499
500
 
500
- /** Greetings and small-talk openers that should get a friendly orientation line,
501
- * never the raw grammar-miss hint. */
502
- const GREETINGS = new Set([
503
- "hi", "hello", "hey", "yo", "sup", "hiya", "howdy", "hey there", "hi there", "hello there",
504
- "thanks", "thank you", "thankyou", "thx", "ty", "cheers", "ok", "okay", "cool",
505
- ]);
506
- const HELP_PHRASES = [
501
+ /** CAPABILITY questions ("what can you do") distinct from IDENTITY questions
502
+ * ("who are you") below. Both used to be conflated into one HELP_PHRASES list,
503
+ * which meant "who are you" always got the "here's what I can query" blurb and
504
+ * never a self-description split so each gets the answer it actually asked for. */
505
+ const CAPABILITY_PHRASES = [
507
506
  /^what can (you|u) do\??$/i, /^what do you do\??$/i, /^help( me)?\??$/i, /^\?+$/,
508
- /^who are you\??$/i, /^what (is|are|r) (this|you)\??$/i, /^how do (i|you) work\??$/i,
507
+ /^how do (i|you) work\??$/i, /^how does (this|it) work\??$/i,
508
+ // unix-habit openers typed inside the REPL out of muscle memory — argv-only
509
+ // today (bin/tmct.mjs), dead once inside the chat loop; route to the same
510
+ // capability answer a plain "help" gets.
511
+ /^--help$/i, /^-h$/i, /^man( tmct)?\??$/i,
512
+ ];
513
+ /** IDENTITY questions — "who/what are you", by name, in plain or ESL-ish phrasing.
514
+ * Routed to a self-description (identity-self) that works regardless of graph
515
+ * state, never the code-graph deflection. */
516
+ const IDENTITY_PHRASES = [
517
+ /^who are you\??$/i, /^what (is|are|r) (this|you)\??$/i,
518
+ /^what('?s| is) your name\??$/i, /^what exactly are you\??$/i,
519
+ /^(tell me about|introduce) yourself\??$/i, /^what is this thing\??$/i,
520
+ /^what am i (talking|speaking|chatting) (to|with)\??$/i,
521
+ /^you are what\??$/i, /^what thing (are|is) you\??$/i,
522
+ /^explain( to me)? what (you are|this is)\??$/i,
523
+ /^whoami\??$/i,
524
+ ];
525
+ /** "Are you an LLM/AI/bot" — tmct's actual positioning (no LLM, deterministic) is
526
+ * a genuinely different, more specific answer than the generic self-description,
527
+ * and this is a very likely first question given how most chat tools work today. */
528
+ const AI_IDENTITY_PHRASES = [
529
+ /^(are you|r u) (an? )?(ai|a bot|chatgpt|gpt|an? llm|a language model|a robot)\??$/i,
530
+ /^is this (chatgpt|gpt|claude|an? ai|an? llm)\??$/i,
531
+ /^do you use ai\??$/i, /^what language model are you( using)?\??$/i,
532
+ /^am i (talking|speaking|chatting) (to|with) a (real )?(person|human|bot|ai)\??$/i,
509
533
  ];
510
534
  /** The structural verbs/nouns that mark a near-miss code question (→ keep the
511
535
  * precise grammar hint, not the friendly nudge). */
@@ -523,16 +547,24 @@ const STRUCT_WORDS = new Set([
523
547
  "testing", "defining", "touching", "extending", "inheritance", "coverage", "member", "members",
524
548
  ]);
525
549
 
550
+ /** Is this raw/normalized query "code-ish" (a dotted/pathed/CamelCase name, "()",
551
+ * or a structural keyword)? Shared by isConversational and the fuzzy-typo fallback
552
+ * so neither ever grabs a genuine near-miss structural question. */
553
+ function looksCodeish(raw, q) {
554
+ return /[a-z][A-Z]|[_./]|\(\)/.test(raw) || q.split(/\s+/).some((w) => STRUCT_WORDS.has(w));
555
+ }
556
+
526
557
  /** Does this look like small-talk / an orientation request rather than a
527
- * (near-miss) structural question? Greetings & help-phrases always qualify; a
528
- * very short input with no code-ish token (dotted/pathed/CamelCase name, "()",
529
- * or a structural keyword) does too. */
558
+ * (near-miss) structural question? Greetings & help/identity phrases always
559
+ * qualify; a very short input with no code-ish token does too. */
530
560
  export function isConversational(query) {
531
561
  const raw = String(query).trim();
532
562
  const q = raw.toLowerCase().replace(/[.!?]+$/, "").trim();
533
- if (GREETINGS.has(q)) return true;
534
- if (HELP_PHRASES.some((re) => re.test(raw))) return true;
535
- const codeish = /[a-z][A-Z]|[_./]|\(\)/.test(raw) || q.split(/\s+/).some((w) => STRUCT_WORDS.has(w));
563
+ if (GREET.has(q) || THANKS.has(q) || OK_ACK.has(q)) return true;
564
+ if (CAPABILITY_PHRASES.some((re) => re.test(raw))) return true;
565
+ if (IDENTITY_PHRASES.some((re) => re.test(raw))) return true;
566
+ if (AI_IDENTITY_PHRASES.some((re) => re.test(raw))) return true;
567
+ const codeish = looksCodeish(raw, q);
536
568
  return q.split(/\s+/).filter(Boolean).length <= 3 && !codeish;
537
569
  }
538
570
 
@@ -562,6 +594,11 @@ const T_WHY_EMPTY = "miss-no-previous-answer";
562
594
  * over-promising "ask me about this codebase". */
563
595
  const T_GREETING_EMPTY = "conversational-greeting-empty";
564
596
  const T_ORIENTATION_EMPTY = "orientation-empty";
597
+ /** IDENTITY answers — self-description and the "no LLM" clarification. Both work
598
+ * regardless of graph state (no empty/populated variant): what tmct IS doesn't
599
+ * depend on whether a repo is loaded. */
600
+ const T_IDENTITY_SELF = "identity-self";
601
+ const T_IDENTITY_NOT_LLM = "identity-not-an-llm";
565
602
  /** THE CONCEPT FORCE (concept.mjs): the three-band answer to a vague "what is a X"
566
603
  * that names a known concept WITH instances — {definition}/{examples}/{followups}. */
567
604
  const T_CONCEPT = "concept-force";
@@ -589,30 +626,115 @@ function tRender(templates, id, slots = {}) {
589
626
  // they record as plain turns with empty resolvedIds and never become mgx:asksAbout
590
627
  // graph edges (same as /help). Register stays plain and short: this is a code tool.
591
628
 
592
- /** Greetings → a short friendly line + one nudge. A couple carry a tasteful nod. */
629
+ /** Greetings → a short friendly line + one nudge. A couple carry a tasteful nod.
630
+ * Deliberately broad across register/dialect (UK/US/AU/NZ, formal, slang, texting
631
+ * abbreviation) — a CLOSED curated list, same "never guess" ethos as the rest of
632
+ * the file, just a bigger one; see collapseRuns/fuzzyMatchInSet below for the
633
+ * typo/elongation multiplier layered on top instead of enumerating every typo. */
593
634
  const GREET = new Set([
594
635
  "hi", "hello", "hey", "yo", "hiya", "howdy", "sup", "greetings",
595
636
  "g'day", "gday", "hey there", "hi there", "hello there",
596
637
  "good morning", "good afternoon", "good evening", "morning",
638
+ // UK/AU/NZ
639
+ "alright", "you alright", "alright mate", "morning all", "yeah nah",
640
+ // US
641
+ "hey y'all", "howdy there", "hiya there",
642
+ // formal
643
+ "good day", "salutations", "good to meet you", "pleased to meet you",
644
+ // slang
645
+ "yo yo", "ayy", "wassup", "sup fam", "heya", "hiya!",
646
+ // texting abbreviation
647
+ "gm", "ge",
597
648
  ]);
598
649
  /** Acknowledgements → an "any time" style reply. */
599
650
  const THANKS = new Set([
600
651
  "thanks", "thank you", "thankyou", "thx", "ty", "ta", "cheers", "nice one",
601
- "much appreciated", "cool thanks",
652
+ "much appreciated", "cool thanks", "many thanks", "much obliged", "ta very much",
653
+ "cheers mate", "cheers for that", "tks", "sweet thanks", "nice",
602
654
  ]);
603
655
  /** Farewells → a goodbye AND a clean end of session (same path as /exit). */
604
656
  const BYE = new Set([
605
657
  "bye", "goodbye", "quit", "exit", "see ya", "see you", "cya", "later", "farewell",
658
+ "peace", "peace out", "im off", "i'm off", "gtg", "gotta go", "catch you later",
659
+ "good day to you", "farewell then",
606
660
  ]);
607
661
  /** Elaboration asks → RE-RENDER the last answer verbosely (traversal + matches). */
608
662
  const WHY = new Set([
609
663
  "why", "how", "how so", "how come", "explain", "say more", "go on",
610
664
  "elaborate", "tell me more", "more detail", "expand",
611
665
  ]);
666
+ /** Bare acknowledgements — routed identically to THANKS (an "ok"/"cool" after an
667
+ * answer reads the same as a thanks, not a new question). Kept separate from
668
+ * THANKS/GREET because these aren't greetings or gratitude, just closing a beat. */
669
+ const OK_ACK = new Set([
670
+ "ok", "okay", "cool", "aight", "fair enough", "got it", "gotcha", "noted",
671
+ "sounds good", "sure", "cool cool", "right",
672
+ ]);
673
+ /** New-user / confused openers — "I don't know what this is" reads as an
674
+ * orientation request, not small-talk and not a grammar-wall near-miss; routed
675
+ * the same as CAPABILITY_PHRASES (→ orientationAnswer). */
676
+ const ORIENT_OPENERS = new Set([
677
+ "what", "huh", "confused", "i dont know what this is", "i don't know what this is",
678
+ "i'm lost", "im lost", "no idea what this does", "just installed this",
679
+ "just installed you", "i just installed this", "i just installed you",
680
+ "first time here", "just started", "new to this", "new here",
681
+ ]);
612
682
 
613
683
  // (Greeting/thanks/farewell wording moved to data/templates/responses.jsonl — W1.
614
684
  // The expression-specific greeting variants map through T_GREETING_BY_PHRASE above.)
615
685
 
686
+ /** Aggressive char-run collapse (2+ identical chars → 1) — used ONLY to build a
687
+ * lookup key, never to change what's actually said back. Lets a typed-out
688
+ * elongation ("heyyyy", "hellooo", "thanksss") match its canonical phrase for
689
+ * free: both the canonical phrase and the elongated input collapse to the same
690
+ * key (a legitimate double letter like "hello"'s "ll" collapses identically on
691
+ * both sides, so there's no canonical/typed asymmetry to get wrong). */
692
+ const collapseRuns = (s) => s.replace(/(.)\1+/g, "$1");
693
+
694
+ /** phrase(collapsed) → canonical phrase, built once per closed set. */
695
+ function collapsedIndex(set) {
696
+ const idx = new Map();
697
+ for (const phrase of set) if (!idx.has(collapseRuns(phrase))) idx.set(collapseRuns(phrase), phrase);
698
+ return idx;
699
+ }
700
+ const GREET_COLLAPSED = collapsedIndex(GREET);
701
+ const THANKS_COLLAPSED = collapsedIndex(THANKS);
702
+ const BYE_COLLAPSED = collapsedIndex(BYE);
703
+
704
+ /** Exact match, else the elongation-collapsed match, else null — the canonical
705
+ * phrase either way, so callers never see the raw (possibly elongated) input. */
706
+ function closedOrCollapsed(q, set, idx) {
707
+ if (set.has(q)) return q;
708
+ return idx.get(collapseRuns(q)) ?? null;
709
+ }
710
+
711
+ /** The fuzzy-typo fallback's candidate pool: every canonical phrase across the
712
+ * closed conversational sets, flattened once. Consulted only after every exact/
713
+ * collapsed lookup misses (see fuzzyConversationalMatch). */
714
+ const CONVERSATIONAL_PHRASES = [
715
+ ...GREET, ...THANKS, ...BYE,
716
+ "what can you do", "what do you do", "help", "how do you work",
717
+ "who are you", "what are you", "what is your name",
718
+ ];
719
+ function classifyConversational(phrase) {
720
+ if (GREET.has(phrase)) return "greet";
721
+ if (THANKS.has(phrase)) return "thanks";
722
+ if (BYE.has(phrase)) return "bye";
723
+ if (phrase === "who are you" || phrase === "what are you" || phrase === "what is your name") return "identity";
724
+ return "capability";
725
+ }
726
+ /** UNIQUE within-bound fuzzy match of the whole trimmed line against
727
+ * CONVERSATIONAL_PHRASES — the "helo"/"thnx"/"wat r u"/"byee" tier. Restricted to
728
+ * short (≤4-word), non-code-ish inputs (looksCodeish, shared with
729
+ * isConversational) so a genuine near-miss structural question is never grabbed;
730
+ * a distance tie is refused, never guessed (same discipline as fuzzyVocabWord). */
731
+ function fuzzyConversationalMatch(raw) {
732
+ const q = collapseRuns(raw.toLowerCase().replace(/[.!?]+$/, "").trim());
733
+ const words = q.split(/\s+/).filter(Boolean);
734
+ if (!words.length || words.length > 4 || looksCodeish(raw, q)) return null;
735
+ return fuzzyMatchInSet(q, CONVERSATIONAL_PHRASES, Math.min(2, fuzzyBound(q)));
736
+ }
737
+
616
738
  /** Re-render the last answer in verbose form: the previous query + its full answer
617
739
  * plus the ask envelope's traversal receipt and the matched entities (the detail a
618
740
  * terse render trims). `empty:true` when there's no previous answer to expand. */
@@ -643,7 +765,7 @@ export function renderVerbose(last) {
643
765
  function conversationalTurn(line, ctx) {
644
766
  const raw = String(line);
645
767
  const q = raw.toLowerCase().replace(/[.!?]+$/, "").replace(/\s+/g, " ").trim();
646
- const t = (id) => tRender(ctx.templates, id) ?? TEMPLATES_UNAVAILABLE;
768
+ const t = (id, slots = {}) => tRender(ctx.templates, id, slots) ?? TEMPLATES_UNAVAILABLE;
647
769
  const mk = (answer, { end = false, miss = false, via = "template" } = {}) => {
648
770
  const ts = new Date().toISOString();
649
771
  return {
@@ -675,27 +797,61 @@ function conversationalTurn(line, ctx) {
675
797
  note(ctx.trace, `result: re-rendering the previous answer to "${ctx.last?.query ?? "?"}" verbosely`);
676
798
  return mk(v.text, { via: "conversational" });
677
799
  }
678
- if (GREET.has(q)) {
679
- note(ctx.trace, "goal: casual/social greeting, no graph intent");
680
- note(ctx.trace, "lane: conversational — greeting (GREET closed set)");
681
- // #3 empty/degenerate-graph greeting: a plain "hi"/"hello" over a graph with 0
682
- // modules orients toward --repo/tmct init instead of over-promising "ask me
683
- // about this codebase". Phrase-specific variants (good morning, hello there)
684
- // keep their wording; only the default greeting swaps.
685
- const id = (!T_GREETING_BY_PHRASE[q] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[q] || T_GREETING);
686
- note(ctx.trace, `pattern: template "${id}" (data/templates/responses.jsonl)`);
687
- return mk(t(id));
688
- }
689
- if (THANKS.has(q)) {
690
- note(ctx.trace, "goal: casual/social — acknowledgement, no graph intent");
691
- note(ctx.trace, "lane: conversational — thanks/acknowledgement (THANKS closed set)");
692
- note(ctx.trace, `pattern: template "${T_THANKS}" (data/templates/responses.jsonl)`);
693
- return mk(t(T_THANKS));
694
- }
695
- if (q === "help" || q === "?" || HELP_PHRASES.some((re) => re.test(raw))) {
800
+ {
801
+ const greetHit = closedOrCollapsed(q, GREET, GREET_COLLAPSED);
802
+ if (greetHit) {
803
+ note(ctx.trace, "goal: casual/social greeting, no graph intent");
804
+ note(ctx.trace, `lane: conversational greeting (GREET closed set${greetHit === q ? "" : ", elongation-collapsed"})`);
805
+ // #3 empty/degenerate-graph greeting: a plain "hi"/"hello" over a graph with 0
806
+ // modules leads with the (now provably-correct) vocabulary hint instead of
807
+ // over-promising "ask me about this codebase". Phrase-specific variants (good
808
+ // morning, hello there) keep their wording; only the default greeting swaps.
809
+ const id = (!T_GREETING_BY_PHRASE[greetHit] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[greetHit] || T_GREETING);
810
+ note(ctx.trace, `pattern: template "${id}" (data/templates/responses.jsonl)`);
811
+ return mk(t(id, { vocabHint: ctx.vocabHint }));
812
+ }
813
+ }
814
+ {
815
+ const thanksHit = closedOrCollapsed(q, THANKS, THANKS_COLLAPSED) || (OK_ACK.has(q) ? q : null);
816
+ if (thanksHit) {
817
+ note(ctx.trace, "goal: casual/social acknowledgement, no graph intent");
818
+ note(ctx.trace, `lane: conversational — thanks/acknowledgement (${OK_ACK.has(q) ? "OK_ACK" : "THANKS"} closed set${thanksHit === q ? "" : ", elongation-collapsed"})`);
819
+ note(ctx.trace, `pattern: template "${T_THANKS}" (data/templates/responses.jsonl)`);
820
+ return mk(t(T_THANKS));
821
+ }
822
+ }
823
+ if (AI_IDENTITY_PHRASES.some((re) => re.test(raw))) {
824
+ note(ctx.trace, "goal: identity — is tmct an AI/LLM (a very likely first question)");
825
+ note(ctx.trace, "lane: conversational — identity/AI (AI_IDENTITY_PHRASES closed set)");
826
+ return mk(t(T_IDENTITY_NOT_LLM));
827
+ }
828
+ if (IDENTITY_PHRASES.some((re) => re.test(raw))) {
829
+ note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
830
+ note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
831
+ return mk(t(T_IDENTITY_SELF));
832
+ }
833
+ if (q === "help" || q === "?" || CAPABILITY_PHRASES.some((re) => re.test(raw)) || ORIENT_OPENERS.has(q)) {
696
834
  note(ctx.trace, "goal: get oriented — what can tmct answer, how do I start");
697
- note(ctx.trace, "lane: conversational — help/orientation (HELP_PHRASES / bare help / ?)");
698
- return mk(orientationAnswer(ctx.templates, ctx.graph));
835
+ note(ctx.trace, "lane: conversational — help/orientation (CAPABILITY_PHRASES/ORIENT_OPENERS / bare help / ?)");
836
+ return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint));
837
+ }
838
+ // Fuzzy-typo fallback (A4): every exact/collapsed closed-set lookup above missed —
839
+ // try a bounded edit-distance match against the flattened conversational phrase
840
+ // pool ("helo", "thnx", "wat r u", "byee"), restricted to short non-code-ish
841
+ // input so a genuine near-miss structural question is never grabbed.
842
+ {
843
+ const fuzzyHit = fuzzyConversationalMatch(raw);
844
+ if (fuzzyHit) {
845
+ const bucket = classifyConversational(fuzzyHit);
846
+ note(ctx.trace, `goal: casual/social or orientation — fuzzy-typo match "${raw}" → "${fuzzyHit}"`);
847
+ note(ctx.trace, `lane: conversational — fuzzy typo tolerance (${bucket})`);
848
+ if (bucket === "bye") return mk(t(T_FAREWELL), { end: true });
849
+ if (bucket === "thanks") return mk(t(T_THANKS));
850
+ if (bucket === "identity") return mk(t(T_IDENTITY_SELF));
851
+ if (bucket === "capability") return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint));
852
+ const id = (!T_GREETING_BY_PHRASE[fuzzyHit] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[fuzzyHit] || T_GREETING);
853
+ return mk(t(id, { vocabHint: ctx.vocabHint }));
854
+ }
699
855
  }
700
856
  return null;
701
857
  }
@@ -754,22 +910,30 @@ function orientationExamples(graph) {
754
910
  return { example1, example2 };
755
911
  }
756
912
 
757
- /** The orientation surface, module-aware: the empty variant (→ --repo/tmct init +
758
- * seeded vocabulary) when there's no code graph, the standard one (with live
759
- * {example1}/{example2} query examples from the loaded graph) otherwise. */
760
- function orientationAnswer(templates, graph) {
761
- if (noCodeGraph(graph)) return tRender(templates, T_ORIENTATION_EMPTY) ?? TEMPLATES_UNAVAILABLE;
913
+ /** The orientation surface, module-aware: the empty variant (→ the provably-correct
914
+ * vocabulary hint + --repo/tmct init) when there's no code graph, the standard one
915
+ * (with live {example1}/{example2} query examples from the loaded graph) otherwise. */
916
+ function orientationAnswer(templates, graph, vocabHint) {
917
+ if (noCodeGraph(graph)) return tRender(templates, T_ORIENTATION_EMPTY, { vocabHint }) ?? TEMPLATES_UNAVAILABLE;
762
918
  return tRender(templates, T_ORIENTATION, orientationExamples(graph)) ?? TEMPLATES_UNAVAILABLE;
763
919
  }
764
920
 
921
+ /** A minimal, still identity-led fallback for orientationText's empty-graph branch
922
+ * — used ONLY if the template library itself failed to load (tRender returned
923
+ * null), matching the file's "never crash, always degrade to one honest line"
924
+ * ethos. Kept short and hand-written so it never drifts silently. */
925
+ const ORIENTATION_EMPTY_FALLBACK = "I'm tmct — a deterministic, offline chat assistant (no LLM). "
926
+ + "For code structure (imports, calls, definitions) point me at a repo with `--repo <path>`, "
927
+ + "or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself. /help for commands.";
928
+
765
929
  /** A dynamic orientation string for the meta/self lane: a /stats-style overview
766
- * when a code graph is loaded, else the honest empty-graph orientation. */
767
- function orientationText(graph) {
930
+ * when a code graph is loaded, else the honest empty-graph orientation — rendered
931
+ * through the SAME template (T_ORIENTATION_EMPTY) conversationalTurn's orientation
932
+ * branch uses, so there is exactly one copy of that wording to keep in sync, not
933
+ * two hand-duplicated strings. */
934
+ function orientationText(graph, templates, vocabHint) {
768
935
  if (noCodeGraph(graph)) {
769
- return "There's no code graph loaded here, so I can't answer structure questions (imports, calls, definitions) yet. "
770
- + "For those I need a `.tmct/graph.json` produced by a graph producer — point me at one with `--repo <path>`, "
771
- + "or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself. "
772
- + 'For general vocabulary, `tmct init` seeds concepts — try "what is a cache". /help for commands.';
936
+ return tRender(templates, T_ORIENTATION_EMPTY, { vocabHint }) ?? ORIENTATION_EMPTY_FALLBACK;
773
937
  }
774
938
  const by = (cls) => (graph.individuals || []).filter((i) => (i.class || "") === cls).length;
775
939
  const parts = [];
@@ -1008,13 +1172,18 @@ const WHAT_KNOW_RE = /^what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?$/;
1008
1172
  // first-touch question gets the live overview instead of the grammar wall.
1009
1173
  const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+(?:this|the)\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin))$/;
1010
1174
 
1011
- /** A SHORT memory summary (never a fact dump) for the bare "what do you know". */
1175
+ /** A SHORT memory summary (never a fact dump) for the bare "what do you know".
1176
+ * This branch only fires when rows.length === 0 — i.e. precisely the case where
1177
+ * vocabulary seeding either hasn't run or produced nothing, so the hook makes NO
1178
+ * term-specific promise (an unconditionally-true pointer: the teach lane and
1179
+ * `tmct init` both work with zero preconditions), rather than suggesting a
1180
+ * vocabulary example that would be guaranteed to miss right after being offered. */
1012
1181
  async function memorySummary(memoryDir, graph) {
1013
1182
  const rows = memoryDir ? await memoryFacts(memoryDir) : [];
1014
1183
  if (!rows.length) {
1015
1184
  const hook = moduleCountOf(graph) > 0
1016
1185
  ? 'ask about this codebase\'s structure (imports, calls, definitions), or teach me with "every X is a Y"'
1017
- : 'teach me with "every X is a Y", or try general vocabulary like "what is a cache"';
1186
+ : 'run `tmct init` to seed a starter vocabulary, or teach me directly with "every X is a Y"';
1018
1187
  return `I haven't been told any facts yet — ${hook}. /memory to inspect, /help for commands.`;
1019
1188
  }
1020
1189
  const preds = new Set(rows.map((f) => f.predicate).filter(Boolean));
@@ -1052,7 +1221,7 @@ async function moduleOrientLane(query, { graph }) {
1052
1221
  return { text: moduleOverviewText(graph, ind), via: "meta" };
1053
1222
  }
1054
1223
 
1055
- async function metaLane(query, { graph, memoryDir, last = null }) {
1224
+ async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null }) {
1056
1225
  const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
1057
1226
  if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
1058
1227
  return { text: await memorySummary(memoryDir, graph), via: "meta" };
@@ -1065,7 +1234,7 @@ async function metaLane(query, { graph, memoryDir, last = null }) {
1065
1234
  // orientationText(graph) verbatim on every repeat, never collapsing). Mirrors
1066
1235
  // ORIENTATION_REPEAT_ONELINER's identity-check pattern exactly, with its own
1067
1236
  // distinct oneliner text (self-limiting for the same reason).
1068
- const text = orientationText(graph);
1237
+ const text = orientationText(graph, templates, vocabHint);
1069
1238
  return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
1070
1239
  }
1071
1240
  // Bug E: an arbitrary "what does <term> do" that META_ORIENT_RE's closed noun
@@ -1530,6 +1699,23 @@ function renderFactLine(f) {
1530
1699
  return `i learned: ${factPhrase(f)}${cite}`;
1531
1700
  }
1532
1701
 
1702
+ /** PROOF-CHAIN RECEIPT (PLAN_INFERENCE_TESTING.md §4 stage 2; ROADMAP L788's
1703
+ * "renderable as a chain of thought in words"): render an ordered list of
1704
+ * premise Fact rows as one continuous argument — "cache is a kind of store;
1705
+ * store is a kind of component; so redis.mjs is a component" — each premise
1706
+ * cited via the SAME factPhrase + "(source: …)" convention renderFactLine
1707
+ * uses, just without its "you told me"/"i learned" framing (a chain reads as
1708
+ * one derivation, not a list of standalone recollections). The conclusion
1709
+ * clause is spelled directly from the first premise's subject and the last
1710
+ * premise's object — sound for any chain length, though today's only caller
1711
+ * (the live cax-sco/scm-sco chase below) ever passes exactly two. */
1712
+ function renderIsaChain(premises) {
1713
+ const step = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${f.provenance})` : ""}`;
1714
+ const first = premises[0];
1715
+ const last = premises[premises.length - 1];
1716
+ return `${premises.map(step).join("; ")}; so ${first.subject} is a ${last.object}`;
1717
+ }
1718
+
1533
1719
  /** Read every reified Fact out of the memory graph as plain {subject, predicate,
1534
1720
  * object, provenance} rows. Lazy + failure-tolerated: no memory → []. */
1535
1721
  async function memoryFacts(memoryDir) {
@@ -1892,6 +2078,40 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
1892
2078
  };
1893
2079
  }
1894
2080
  }
2081
+ // LIVE cax-sco / scm-sco PROOF CHASE (PLAN_INFERENCE_TESTING.md INF-A2,
2082
+ // §4 stage 1): a direct isa fact and the graph inherits-bridge both
2083
+ // missed — chase a chain over TWO TAUGHT isa-family facts (§1's PARTIAL
2084
+ // note: "cax-sco over two TAUGHT facts is NOT implemented"; the band's own
2085
+ // "Rules needed" column: "⊑-chain of length 2") via syllogise.mjs's
2086
+ // findIsaChain, a rooted proof search built on the SAME two rule kernels,
2087
+ // LIVE and READ-ONLY (nothing is written — the offline `tmct syllogise`
2088
+ // batch pass, materializing the same two rules with `entailed:*`
2089
+ // provenance, is the persisting counterpart). Deliberately narrow, twice
2090
+ // over, to stay exactly in INF-A2's scope and not silently answer bands
2091
+ // this stage doesn't (yet) certify:
2092
+ // - maxHops:2 — a longer taught chain is INF-B2's multi-hop +
2093
+ // proof-chain-materialization territory (§4 stage 2 proper), which
2094
+ // INFBENCH pins as an honest ceiling until it lands — answering
2095
+ // "yes" there today would be graded FABRICATION, not credit.
2096
+ // - CORPUS-sourced edges excluded — the bulk background corpus band
2097
+ // (trust 0.7) can coincidentally chain two unrelated classes into a
2098
+ // technically-true-per-ConceptNet "yes" that has nothing to do with
2099
+ // what the OPERATOR taught; only operator/teach/entailed-sourced isa
2100
+ // facts are chased, matching "TAUGHT" in the gap's own name.
2101
+ const { findIsaChain, SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE } = await import("./syllogise.mjs");
2102
+ const isTaught = (f) => !f.sourceTypes?.includes("corpus") && !f.sourceTypes?.includes("web");
2103
+ const chainSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE && isTaught(f));
2104
+ const chainTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE && isTaught(f));
2105
+ const chainSubClassEdges = chainSubClassRows.map((f) => [f.subject, f.object]);
2106
+ const chainTypeEdges = chainTypeRows.map((f) => [f.subject, f.object]);
2107
+ const factForStep = (step) => (step.predicate === SC_PREDICATE ? chainSubClassRows : chainTypeRows)
2108
+ .find((f) => f.subject === step.subject && f.object === step.object);
2109
+ for (const subj of subjCandidates) {
2110
+ const chain = findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassEdges, { maxHops: 2 });
2111
+ if (!chain) continue;
2112
+ const premises = chain.map(factForStep);
2113
+ if (premises.every(Boolean)) return { text: `yes — ${renderIsaChain(premises)}`, replace: true };
2114
+ }
1895
2115
  return null; // no remembered fact — the honest miss stands (never a guessed "no")
1896
2116
  }
1897
2117
 
@@ -2347,7 +2567,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
2347
2567
  * otherwise the unchanged dispatchTool path (which also yields the no-graph error).
2348
2568
  * A hit updates the focus to the resolved object. Grammar miss / ToolError → a
2349
2569
  * normal answer, never a crash. */
2350
- async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace }) {
2570
+ async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null }) {
2351
2571
  const ts = new Date().toISOString();
2352
2572
  // DISCOURSE ANAPHORA (CHATBENCH_006 levers 1+2): a follow-up like "which of those
2353
2573
  // are tested" / "how many of those" / "count them" filters or counts the PREVIOUS
@@ -2472,7 +2692,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
2472
2692
  // codebase", "how do i start") → a summary / orientation, answered before the
2473
2693
  // fact-dump readers so "what do you know" gets a summary, not raw facts.
2474
2694
  if (miss) {
2475
- const meta = await metaLane(query, { graph, memoryDir, last });
2695
+ const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint });
2476
2696
  if (meta) {
2477
2697
  answer = meta.text; via = meta.via; recordMiss = false; handled = true;
2478
2698
  note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
@@ -2498,7 +2718,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
2498
2718
  // structural (by construction, not word-count guesswork); its own composed
2499
2719
  // answer — hit, honest empty, or "it needs a referent" — is always more
2500
2720
  // truthful than the orientation card.
2501
- const orientation = orientationAnswer(templates, graph);
2721
+ const orientation = orientationAnswer(templates, graph, vocabHint);
2502
2722
  const repeat = last?.answer === orientation;
2503
2723
  answer = repeat ? ORIENTATION_REPEAT_ONELINER : orientation;
2504
2724
  via = "template"; handled = true;
@@ -2954,7 +3174,7 @@ function morePage(query, { last, focus }) {
2954
3174
  return turn;
2955
3175
  }
2956
3176
 
2957
- export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false } = {}) {
3177
+ export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null } = {}) {
2958
3178
  const line = String(input ?? "").trim();
2959
3179
  const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
2960
3180
  // narrate mode: allocate the mutable trace array ONLY when on (`null` when off,
@@ -2964,7 +3184,12 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
2964
3184
  // the narrate:false path allocates nothing extra and renders byte-identically to
2965
3185
  // before this feature existed — see the "---- narrate mode ----" section above.
2966
3186
  const trace = narrate ? [] : null;
2967
- const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate };
3187
+ // vocabHint: createSession computes this ONCE per session (a marker-file check)
3188
+ // and threads it in; a direct runTurn() caller (tests, library use) that doesn't
3189
+ // pass one gets it computed here instead, so "try this vocabulary example" is
3190
+ // never wrong regardless of caller.
3191
+ const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
3192
+ const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint };
2968
3193
  // A DISPATCHED turn (count / slash-command / ask) becomes the new "last answer"
2969
3194
  // that why/say-more re-renders; a conversational turn does not (it preserves it).
2970
3195
  // FINISH SEAM (PLAN_RESPONSE_FINISHING §"Where it lives"): every dispatched turn's
@@ -3117,6 +3342,35 @@ async function seedBootstrapMemory(repo) {
3117
3342
  }
3118
3343
  }
3119
3344
 
3345
+ /** Whether THIS repo's memory actually carries the corpus seed — the marker is
3346
+ * authoritative regardless of whether the CURRENT run performed the seeding or
3347
+ * an earlier run (or `tmct init`) did (seedBootstrapMemory short-circuits on an
3348
+ * existing marker without re-reading the slice). The one signal every "try this
3349
+ * vocabulary example" surface must check before offering a term-specific query —
3350
+ * see vocabExampleHint. A cheap fs check, negligible next to the per-turn
3351
+ * template load. */
3352
+ async function hasSeededVocabulary(repo) {
3353
+ if (!repo) return false;
3354
+ try { await readFile(join(repo, SEED_MARKER_REL), "utf8"); return true; }
3355
+ catch { return false; }
3356
+ }
3357
+
3358
+ /** A "try this" vocabulary-example clause that's PROVABLY correct in the session
3359
+ * it's shown, mirroring the discipline orientationExamples() already applies to
3360
+ * structural examples (never offer an example that isn't confirmed to resolve).
3361
+ * `cache` is confirmed live: present in corpus/seon/definitions.jsonl, backed by
3362
+ * a corpus:seon concept fact, and a recognized lexicon noun — but only actually
3363
+ * answerable once the seed has run. When it hasn't (TMCT_NO_SEED=1,
3364
+ * seed.enabled=false, or corpus load failure), offering it would be a lie worse
3365
+ * than no example — swap to an unconditionally-true pointer instead (the teach
3366
+ * lane and `tmct init` both work with zero preconditions). Computed ONCE per
3367
+ * session (createSession), not per turn. */
3368
+ function vocabExampleHint(seeded) {
3369
+ return seeded
3370
+ ? 'Try "what is a cache" for general vocabulary.'
3371
+ : 'Run `tmct init` to seed a starter vocabulary, or teach me directly with "every X is a Y".';
3372
+ }
3373
+
3120
3374
  /** Trim a focus label for the prompt so a long module path can't run the line off. */
3121
3375
  const shortLabel = (l) => { const s = String(l); return s.length > 40 ? "…" + s.slice(-39) : s; };
3122
3376
  const promptFor = (focus) => (focus ? `tmct(${shortLabel(focus.label)})> ` : PROMPT);
@@ -3272,6 +3526,13 @@ export async function createSession({
3272
3526
  if (empty && String(env.TMCT_NO_SEED || "") !== "1") {
3273
3527
  seeded = await seedBootstrapMemory(repo);
3274
3528
  }
3529
+ // vocabHint: computed ONCE per session (not per-turn — see runTurn's own
3530
+ // per-call fallback for direct/library callers). `seeded` is only truthy when
3531
+ // THIS run performed the seeding; a repo seeded by an EARLIER run (or `tmct
3532
+ // init`) still needs the marker check, so this covers both — see
3533
+ // hasSeededVocabulary's docblock.
3534
+ const vocabSeeded = Boolean(seeded) || (await hasSeededVocabulary(repo));
3535
+ const vocabHint = vocabExampleHint(vocabSeeded);
3275
3536
  // #3/#5: 0 modules means no code graph to answer structure questions from —
3276
3537
  // whether the graph file is absent (empty bootstrap) OR present with no code
3277
3538
  // entities (the degenerate trap). Both get orienting, non-over-promising banner
@@ -3287,9 +3548,11 @@ export async function createSession({
3287
3548
  // is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band.
3288
3549
  ...(seeded ? [`seeded ${seeded.appended} starter facts (${seeded.seon} curated SEON + ${seeded.conceptnet} ConceptNet) — /memory to inspect`] : []),
3289
3550
  // no code graph → point at how to GET one (a graph producer / --repo / the shipped
3290
- // example), honest that `tmct init` seeds VOCABULARY, not a code graph, and at what
3291
- // IS answerable now. tmct reads graphs; it never indexes code itself.
3292
- ...(noCodeGraph ? ['for code structure, point me at a .tmct/graph.json with --repo <path> or try `npm run example:mini` (tmct reads graphs, it doesn\'t index code); `tmct init` only seeds vocabulary — try "what is a cache"'] : []),
3551
+ // example), and at what IS answerable now `vocabHint` is only ever a term
3552
+ // confirmed to resolve in THIS session's actual seed state (see vocabExampleHint),
3553
+ // never a hardcoded example that might not have been seeded. tmct reads graphs;
3554
+ // it never indexes code itself.
3555
+ ...(noCodeGraph ? [`for code structure, point me at a .tmct/graph.json with --repo <path> or try \`npm run example:mini\` (tmct reads graphs, it doesn't index code). ${vocabHint}`] : []),
3293
3556
  "pass --repo <path> to target a different repo",
3294
3557
  "ask a question, or /help for commands (/stats for an overview) — /exit to leave",
3295
3558
  ];
@@ -3319,7 +3582,7 @@ export async function createSession({
3319
3582
  async turn(line) {
3320
3583
  let result;
3321
3584
  try {
3322
- result = await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId, env, lexicon, narrate: narrateOn });
3585
+ result = await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId, env, lexicon, narrate: narrateOn, vocabHint });
3323
3586
  } catch (e) {
3324
3587
  const ts = new Date().toISOString();
3325
3588
  const message = e instanceof Error ? e.message : String(e);
@@ -76,11 +76,21 @@ export function eligibleForCanon(w) {
76
76
  * distinct target words at the same distance is refused outright (the honest-miss
77
77
  * discipline at the vocabulary level; cf. MISSPELLINGS' curated "calss" decision). */
78
78
  export function fuzzyVocabWord(w) {
79
- const bound = fuzzyBound(w);
79
+ return fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w));
80
+ }
81
+
82
+ /** GENERIC unique-within-bound fuzzy match of `w` against an arbitrary candidate
83
+ * list, or null — same discipline as fuzzyVocabWord/resolveObject's tier-5 pass
84
+ * (a distance tie between two distinct candidates is refused, never guessed),
85
+ * factored out so callers outside the ask/keyword pipeline (e.g. chat.mjs's
86
+ * conversational recognizers) can reuse the primitive without a bespoke
87
+ * target-list wrapper. `bound` defaults to fuzzyBound(w) but callers may pass a
88
+ * tighter budget (e.g. to keep short conversational tokens conservative). */
89
+ export function fuzzyMatchInSet(w, candidates, bound = fuzzyBound(w)) {
80
90
  let best = bound + 1;
81
91
  let hit = null;
82
92
  let tied = false;
83
- for (const target of FUZZY_TARGET_WORDS) {
93
+ for (const target of candidates) {
84
94
  const d = editDistance(w, target, Math.min(best, bound));
85
95
  if (d < best) { best = d; hit = target; tied = false; }
86
96
  else if (d === best && d <= bound && target !== hit) tied = true;
package/src/syllogise.mjs CHANGED
Binary file