@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 +105 -8
- package/data/templates/responses.jsonl +5 -3
- package/package.json +1 -1
- package/src/chat.mjs +324 -61
- package/src/interpret/fuzzy.mjs +12 -2
- package/src/syllogise.mjs +0 -0
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
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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.
|
|
66
|
-
{"id":"orientation-empty","class":"orientation","register":"friendly","template":"
|
|
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.
|
|
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
|
-
/**
|
|
501
|
-
*
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
/^
|
|
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
|
|
528
|
-
* very short input with no code-ish token
|
|
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 (
|
|
534
|
-
if (
|
|
535
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 (
|
|
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 (→
|
|
758
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
: '
|
|
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
|
-
|
|
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),
|
|
3291
|
-
//
|
|
3292
|
-
|
|
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);
|
package/src/interpret/fuzzy.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|