@polycode-projects/the-mechanical-code-talker 0.2.0

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 ADDED
@@ -0,0 +1,209 @@
1
+ # tmct ROADMAP
2
+
3
+ tmct v0.1.0 was a **whole-package lift** of the seonix chat surface (published
4
+ as `@polycode-projects/mct`): identical shape, green tests, new branding. That
5
+ was deliberate — it gave every ambition below a working, tested starting point
6
+ instead of a green field. v0.2.0 is the **reshape**: the lift's LLM fallback,
7
+ extraction stack, and MCP server are gone, and the package, naming, and license
8
+ now match the product this document describes.
9
+
10
+ This roadmap is organized into phases. The original 16 ambition items are
11
+ mapped into them (item numbers retained for traceability); the seven sketches
12
+ formerly held in `code-talker-ideas.txt` are folded into items 8–11 below and
13
+ the file has been deleted.
14
+
15
+ ## The umbrella product definition (item 1)
16
+
17
+ **A tolerant, ELIZA/PARRY-style chat, obsessed with software.** A best-efforts
18
+ conversational surface that guides users toward precision queries.
19
+ ELIZA/PARRY-style pattern reflection, but domain-obsessed the way PARRY was
20
+ obsessed with the mafia — tmct may heavily assume a narrow context (you are
21
+ asking about *this* codebase, or about what tmct itself remembers) and exploit
22
+ that assumption to answer cheaply and confidently. Tolerant of loose, fuzzy,
23
+ misspelled input; never silently wrong; **no LLM anywhere in the product**.
24
+ Every phase below serves this definition.
25
+
26
+ ---
27
+
28
+ ## Phase 0 — Reshape (v0.2.0) — the current work
29
+
30
+ One commit per step, `npm test` green at each.
31
+
32
+ - **DONE — Strip the LLM fallback** (`--with-claude` / `--with-copilot` and the
33
+ `hook-augment` mode removed; the product path is now provably model-free).
34
+ - **DONE — Drop the extraction/viz stack** *(item 12: shed the codebase-index
35
+ dependency)*: Python `ast`, tree-sitter, Roslyn/Java extractors, walk/viz/
36
+ timeline/temporal modules, `roslyn/`, `java/`, `templates/` all deleted. tmct
37
+ consumes a graph via the provider seam; it produces none.
38
+ - **DONE — Drop the MCP server**: `@modelcontextprotocol/sdk` removed;
39
+ `dispatchTool` survives as the plain internal tool switch.
40
+ - **DONE — Carve `buildEntities`** into `src/graph-build.mjs`: the pure
41
+ in-memory graph assembly function, kept as the future memory writer
42
+ primitive.
43
+ - **DONE — Empty-graph bootstrap** *(item 14, partial)*: a missing graph file
44
+ is no longer an error; tmct starts empty, says so, and creates
45
+ `.tmct/graph.json` from the conversation. The full provider adapter contract
46
+ is Phase 1.
47
+ - **DONE — tmct naming purge** *(item 15, widened)*: seonix AND the interim
48
+ "mct" replaced throughout — package
49
+ `@polycode-projects/the-mechanical-code-talker`, bin `tmct`
50
+ (`bin/tmct.mjs`), tool prefix `tmct_*`, artifact dir `.tmct/`, env
51
+ `TMCT_GRAPH_FILE`, prompt `tmct>`.
52
+ - **DONE — License swap to MPL-2.0** (from AGPL-3.0): free commercial use,
53
+ file-level publish-and-attribute copyleft.
54
+ - **DONE — README rewrite + GitLab Pages home page** (`public/index.html`,
55
+ `pages` CI job): https://polycode-projects.gitlab.io/the-mechanical-code-talker/
56
+ - **DONE — `docs/references/` skeleton + `ontology/`**: the reference-library
57
+ index (canonical URL / retrieval date / licence / consumer per entry), OWL 2
58
+ vocabulary notes, ACE-OWL sub-fragment pattern table, ConceptNet relation
59
+ list, and the `ontology/tmct-core.ttl` placeholder. This feeds the Phase 2
60
+ grammar work; the library grows as sources are web-verified.
61
+ - **DONE — Publish 0.2.0** under the new name; deprecate
62
+ `@polycode-projects/mct@0.1.0` with a rename pointer.
63
+
64
+ ---
65
+
66
+ ## Phase 1 — Interpretation pipeline + memory foundations
67
+
68
+ ### Item 8 — Multi-strategy request classification and ranking → `src/interpret/`
69
+ Instead of a single best-guess parse, run the request through **all the classes
70
+ of thing it could be**, parse it with each class's own strategy (grammar parse,
71
+ keyword picking, noise-word removal, fuzzy matching — later the ACE strategy
72
+ from Phase 2), execute the strategies that look like winners, then **merge
73
+ same-class results** and surround **distinct-class results** with "if you mean
74
+ X then …". Grows from `ask.mjs`'s existing 2-way merge into
75
+ `interpret/pipeline.mjs` + `interpret/merge.mjs` + `interpret/strategies/*`.
76
+ *(Covers sketch 1 of the former `code-talker-ideas.txt`: "request → all the
77
+ classes of things it could be → parse using each class-specific strategy →
78
+ execute winners → combine similar result classes and rank".)*
79
+
80
+ ### Item 13 — The clean chat / primitives split
81
+ Pull the movable conversational grammar out of the core primitives
82
+ (`resolveObject`, `edgesOfKind`, `refineToEntities`, `traverse`) so the chat
83
+ engine stands alone. `chat.mjs` slims to the conversational layer + `runTurn`
84
+ orchestration.
85
+
86
+ ### Item 10 — Input normalization pass (grammar / spell / style checks)
87
+ Run a grammar check, spell check, and style check over input as a
88
+ normalization pass alongside classification (item 8), so misspelled or
89
+ ungrammatical input is repaired or scored before template matching — the
90
+ concrete mechanism behind item 1's "tolerant of loose, fuzzy, misspelled
91
+ input" promise. The same checks later serve the "observe" ambition (item 6)
92
+ over repo prose. *(Covers sketches 3, 4, and 5 of the former ideas file:
93
+ grammar check, spell check, style check.)*
94
+
95
+ ### Item 9 — Conversational memory as its own graph → `src/memory/`
96
+ Record every parsed request as an "a-visitor-said" item and every response
97
+ alongside it, in tmct's own OWL-labelled graph (`memory/core.mjs`), with text
98
+ blocks under a PageRank-style relevance index (`memory/blocks.mjs`) and
99
+ session-log cleaning/folding (`memory/fold.mjs`). Future input can then match
100
+ against **prior questions** by similarity, not just against a provided code
101
+ graph. This is tmct's own data under `.tmct/`, distinct from any
102
+ provider-supplied graph and not written back through the provider adapter.
103
+ *(Covers sketch 2 of the former ideas file: "once parsed the text is added to
104
+ the graph as a-visitor-said item; responses from queries go in the graph; text
105
+ matching may find similar questions".)*
106
+
107
+ ### Item 14 (finish) — The graph-provider adapter contract
108
+ Define the provider touchpoint interface — a loader yielding
109
+ `{ individuals, byId, relations, proseIndex }` plus the published primitives —
110
+ so seonix or any other producer can feed tmct without tmct importing an
111
+ indexer. Phase 0 shipped the bootstrap seam; this finishes the contract.
112
+
113
+ ### Item 16 — Library-first design for extension
114
+ Keep the `exports` map and the primitives stable and documented as the
115
+ internals are refactored.
116
+
117
+ ### Shell work
118
+ - **OpenTUI console shell** around `runTurn`; readline `runChat` stays as
119
+ `--plain` and as the test surface.
120
+ - Fold the surviving `bin/cli.mjs` arms into `bin/tmct.mjs`; delete `cli.mjs`.
121
+
122
+ ---
123
+
124
+ ## Phase 2 — Grammar → OWL + corpus
125
+
126
+ ### Item 2 — Its own well-defined grammar → `src/grammar/ace.mjs`
127
+ A first-class, documented, testable grammar — an **ACE-inspired controlled
128
+ fragment** (~8 sentence patterns; see
129
+ `docs/references/schemas/ace-owl-fragment.md`) that emits **OWL-labelled
130
+ triples** when text fits it, backed by a declared lexicon
131
+ (`grammar/lexicon.mjs`, TOML/JSONL data). Plugs into the item-8 pipeline as
132
+ one strategy among several: fitting the grammar is a strong signal, missing it
133
+ falls back to the tolerant strategies.
134
+
135
+ ### Item 3 — Ontology grounding: core OWL/RDF + SE vocabularies
136
+ Ground the memory vocabulary in real ontologies: core **OWL 2 / RDF / RDFS**
137
+ scaffolding plus software-entity concepts (the SEON-derived terms the graph
138
+ already uses — `seon:`, `mgx:` prefixes — with **OWL-SEON** and **FAMIX** as
139
+ reference vocabularies). Deliverable: `ontology/tmct-core.ttl`.
140
+
141
+ ### Items 4 + 7 — Template libraries, phrase book, plain data formats
142
+ Sentence-fragment template libraries for matching input and generating
143
+ responses, plus a software-engineering phrase book — all in plain, diffable
144
+ formats (**JSONL**, **TOML**, **.txt** line files).
145
+
146
+ ### ConceptNet corpus slice
147
+ A committed, filtered English/tech-domain **ConceptNet slice** (CC-BY-SA 4.0
148
+ notice, size-budgeted) with the ~35-row relation→ACE-OWL-pattern mapping table
149
+ (`src/corpus/conceptnet.mjs` + `conceptnet-map.toml`; relation list in
150
+ `docs/references/schemas/conceptnet-relations.md`). The corpus seeds the
151
+ bootstrap graph so an empty tmct still has a vocabulary.
152
+
153
+ ### Reference library growth
154
+ Grow `docs/references/` with web-verified sources: ACE/APE papers, ConceptNet
155
+ docs, ELIZA/PARRY lineage papers (only redistributable licences committed) —
156
+ and finish `ontology/tmct-core.ttl` alongside the grammar work.
157
+
158
+ ---
159
+
160
+ ## Phase 3 — Chat tuning cycle (autonomous)
161
+
162
+ The measurement loop that turns the above into a tunable product — specified
163
+ in `SKILL_TUNING_CYCLE.md`:
164
+
165
+ - a fixed, versioned **chatbench case set** (`chatbench/cases.jsonl`);
166
+ - a **deterministic replay runner** over `runTurn` (the product is
167
+ deterministic — one run per arm suffices);
168
+ - **LLM-as-judge** scoring (N≥3 samples per case; groundedness / correctness /
169
+ honesty-on-miss / rephrase-hint helpfulness). The judge lives in the **eval
170
+ harness only** — the product stays no-LLM;
171
+ - `CHATBENCH_0NN.md` artifacts and an autonomous cycle loop (no hard pause;
172
+ each cycle logs its ranked decision menu and continues).
173
+
174
+ Inside this loop, two earlier ambitions become **tuning levers** rather than
175
+ standalone features:
176
+
177
+ ### Item 5 — Calculation surfaced as reasoning
178
+ Derived facts presented as lightweight reasoning ("there are a lot of tests
179
+ for a codebase of that size", "this module is unusually central") —
180
+ calculations, not inference: deterministic, explainable, cheap.
181
+
182
+ ### Item 6 — Optionally running linters/tests to *observe*
183
+ Let tmct run linters or tests to observe whether something actually worked,
184
+ reporting the observation — measurement, not reasoning.
185
+
186
+ ### Item 11 — Formal logical reasoning over the ontology (Prolog / Progol) — exploratory, gated
187
+ Apply real rules of inference (modus tollens, etc.) to formulas extracted from
188
+ parsed prose, checked against the axioms in the ontology (item 3) — a step
189
+ beyond item 5's arithmetic. The sketch: map OWL constructs into templates over
190
+ parameter expressions (Prolog terms or similar), then use a theorem prover —
191
+ Progol (inductive logic programming) is the named candidate — to prove goals
192
+ against parsed prose. Materially bigger than item 5 and dependent on the
193
+ Phase 2 ontology landing; **exploratory until a spike confirms the
194
+ OWL-to-template mapping is tractable**. *(Covers sketches 6 and 7 of the
195
+ former ideas file: "reasoning as the application of logic rules to the formula
196
+ created from prose against the set of axioms in the graph, possibly search
197
+ using Prolog" and "fit OWL constructs into templates / parameter expressions,
198
+ then use Progol to theorem-prove against parsed prose".)*
199
+
200
+ ---
201
+
202
+ ## Explicitly out of scope (for now)
203
+
204
+ - No AWS, no benchmark rig — tmct is a published npm library + CLI with a
205
+ static GitLab Pages home page only.
206
+ - No auto-publish: releasing a version is gated on a deliberate version-bump
207
+ commit plus a configured `NPM_TOKEN` in CI.
208
+ - No MCP server, no LLM in the product path — permanently out of scope, not
209
+ just "for now".
package/bin/cli.mjs ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ // tmct — local typed-edge code-graph tools over a pre-built graph artifact:
3
+ //
4
+ // tmct cli digest '{"repo_path":"<abs>","modules":[…]}' → architecture map + per-module
5
+ // context bundles to stdout
6
+ // tmct cli digest '{"repo_path":"<abs>","query":"<free text>"}' → same, but auto-locates +
7
+ // score-gap-selects the modules (R1b, the shipped default) instead of requiring an explicit
8
+ // `modules` list — one call from a question to a digest. `modules` wins if both are given.
9
+ // tmct cli <toolName> '{…args}' → invoke ANY tool via Bash
10
+ // (e.g. tmct cli tmct_ask '{"query":"<free text>"}' — mechanical, no-LLM NL question
11
+ // over the graph; no bespoke wiring needed, this fallback covers it)
12
+ // tmct chat [--repo <abs>] → interactive prompt over the mechanical tmct_ask engine; /exit to leave; session log → <repo>/.tmct/session-<uuidv7>.log
13
+ //
14
+ // The graph artifact lives at <repo_path>/.tmct/graph.json; the tools (run with
15
+ // cwd = that repo) load it by default. No flags, no config files.
16
+
17
+ import { join } from "node:path";
18
+ import { dispatchTool, buildContextBundle } from "../src/server.mjs";
19
+ import { loadConfig, DEFAULT_GRAPH_REL } from "../src/config.mjs";
20
+ import * as source from "../src/source.mjs";
21
+ import { parseEntities, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP } from "../src/codegraph.mjs";
22
+
23
+ /** Build a config pointed at a specific repo's artifact (for `cli` sub-commands that
24
+ * take a repo_path), or fall back to the cwd-derived default. */
25
+ function configFor(repoPath) {
26
+ return repoPath ? { graphFile: join(repoPath, DEFAULT_GRAPH_REL) } : loadConfig();
27
+ }
28
+
29
+ /** Parse the trailing JSON payload of a `cli` sub-command (best-effort). */
30
+ function parsePayload(payload) {
31
+ if (!payload) return {};
32
+ try { return JSON.parse(payload); }
33
+ catch { return null; }
34
+ }
35
+
36
+ const DIGEST_MODULE_CAP = 12; // bound the digest — a handful of changed modules
37
+ const DIGEST_SECONDARY_CAP = 2; // B2: at most this many SECONDARY modules get a (trimmed) bundle
38
+ const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
39
+
40
+ /** `cli digest` — print a machine-readable header + a repo architecture map + the
41
+ * tmct_context edit bundle for each requested module to stdout (reuses the server's exact
42
+ * renderer via buildContextBundle, so no render logic is duplicated). This stdout is injected
43
+ * into a caller's prompt.
44
+ *
45
+ * Two ways to say which modules: an explicit `modules` array (unchanged), or a `query` string —
46
+ * auto-locate + score-gap-select (R1b, the shipped default as of 2026-07-02) in one call, so a
47
+ * real caller no longer has to run `tmct_locate` and hand-pick a module themselves. `modules`
48
+ * wins if both are given. The header reports which modules were actually selected either way.
49
+ *
50
+ * B2: the FIRST (primary) module gets a full size-adaptive bundle; the remaining modules are
51
+ * RANKED by import/cochange proximity to the primary and only the top few get a TRIMMED
52
+ * (signatures + insertion region) bundle — so a 2-module task no longer pays for two full
53
+ * bundles. The leading header line lets the rig record tier/topup telemetry. */
54
+ async function runDigest(args) {
55
+ const repoPath = args.repo_path;
56
+ if (!repoPath) { process.stderr.write("tmct: digest requires repo_path\n"); process.exit(2); }
57
+ let modules = Array.isArray(args.modules) ? args.modules.slice(0, DIGEST_MODULE_CAP) : [];
58
+ let autoSelected = null; // for the header, when `query` drove selection
59
+ if (!modules.length && args.query) {
60
+ const graph = parseEntities(await source.fetchEntities(configFor(repoPath)));
61
+ // SHIPPED DEFAULT (0.5.0): the digest's query-mode auto-locate resolves literal-mention ON
62
+ // (a fresh invocation with no tmct.toml), disable-able via `literal_mention:false`. Kept in
63
+ // lockstep with the `tmct_locate` handler so `cli digest '{query}'` ≡ `cli tmct_locate` for the
64
+ // same query. A strict no-op unless the query carries a ≥3-component dotted path / repo-relative
65
+ // path; searchModulesRanked derives rawQuery from the query when literalMention is on.
66
+ const ranked = searchModulesRanked(graph, args.query, { literalMention: args.literal_mention !== false });
67
+ const scoreGapK = args.score_gap === false ? null : (Number.isFinite(args.score_gap) ? args.score_gap : DEFAULT_SCORE_GAP);
68
+ modules = selectRankedModules(ranked, { top_k: Number.isFinite(args.top_k) ? args.top_k : 2, scoreGapK }).slice(0, DIGEST_MODULE_CAP);
69
+ autoSelected = modules;
70
+ if (!modules.length) process.stderr.write(`tmct: digest query "${args.query}" matched no modules — empty digest\n`);
71
+ }
72
+ // Tuning-flag contract (threaded to buildContextBundle → sizeBundle): `min` → leanest TINY/no
73
+ // top-up; `untuned` → the earlier escalation. Neither → the tuned default. The digest header still
74
+ // reports the EFFECTIVE tier/topup returned per module, so rig telemetry stays correct.
75
+ const min = Boolean(args.min);
76
+ const untuned = Boolean(args.untuned);
77
+ // tmct-max: the injection CEILING — every requested module gets a FULL (untrimmed) bundle,
78
+ // not just the primary + 2 trimmed secondaries. Tests whether maximal injection re-bloats.
79
+ const max = Boolean(args.max);
80
+ const secondaryCap = max ? modules.length : DIGEST_SECONDARY_CAP;
81
+ const config = configFor(repoPath);
82
+ const body = [];
83
+ let effTier = "NONE"; // largest tier emitted across all modules
84
+ let topup = false; // whether any module's auto-sizing escalated above TINY
85
+ let emitted = 0; // module bundles actually emitted (primary + trimmed secondaries)
86
+
87
+ try {
88
+ body.push("# Repository architecture\n" + (await dispatchTool("tmct_architecture", {}, { config })));
89
+ } catch (e) {
90
+ body.push(`# Repository architecture\n(unavailable: ${e?.message || e})`);
91
+ }
92
+
93
+ const emit = async (m, trim) => {
94
+ try {
95
+ const { text, tier, topup: t } = await buildContextBundle({ symbol: m, min, untuned, max }, { config, source, trim });
96
+ body.push(`\n# Context bundle: ${m}${trim ? " (secondary, trimmed)" : ""}\n` + text);
97
+ if ((TIER_RANK[tier] || 0) > (TIER_RANK[effTier] || 0)) effTier = tier;
98
+ if (t) topup = true;
99
+ emitted += 1;
100
+ } catch (e) {
101
+ body.push(`\n# Context bundle: ${m}\n(no bundle: ${e?.message || e})`);
102
+ }
103
+ };
104
+
105
+ if (modules.length) {
106
+ const [primary, ...rest] = modules;
107
+ // rank the secondaries by proximity to the primary (best-effort: keep input order on error)
108
+ let ranked = rest;
109
+ if (rest.length) {
110
+ try { ranked = rankModulesByProximity(parseEntities(await source.fetchEntities(config)), primary, rest); }
111
+ catch { ranked = rest; }
112
+ }
113
+ const secondaries = ranked.slice(0, secondaryCap);
114
+ const overflow = ranked.slice(secondaryCap);
115
+ await emit(primary, false);
116
+ for (const m of secondaries) await emit(m, max ? false : true);
117
+ if (overflow.length) body.push(`\n# Related modules (not expanded; query tmct_context if needed): ${overflow.join(", ")}`);
118
+ }
119
+
120
+ // HARD CONTRACT: first line is the machine-readable digest header the rig greps. Fields are
121
+ // append-only — `selected=` is new (query mode only) and never changes the existing ones the
122
+ // rig's own parser depends on.
123
+ const header = `# tmct-digest tier=${effTier} topup=${topup} modules=${emitted}`
124
+ + (autoSelected ? ` selected=${autoSelected.join(",") || "(none)"}` : "");
125
+ process.stdout.write([header, ...body].join("\n") + "\n");
126
+ }
127
+
128
+ async function main() {
129
+ const [mode, sub, payload] = process.argv.slice(2);
130
+
131
+ if (mode === "cli") {
132
+ // digest mode: architecture map + per-module context bundles → stdout
133
+ if (sub === "digest") {
134
+ const args = parsePayload(payload);
135
+ if (args === null) {
136
+ process.stderr.write("tmct: digest expects a JSON arg, e.g. '{\"repo_path\":\"/abs\",\"modules\":[…]}'\n");
137
+ process.exit(2);
138
+ }
139
+ await runDigest(args);
140
+ return;
141
+ }
142
+
143
+ // locate mode (TUNING #3): `cli tmct_locate '{"query":"…","repo_path":"<abs>"}'` emits the
144
+ // ranked modules as `<relpath>\t<score>`, one per line (highest first), using renderSearch's
145
+ // exact ranking. The rig keeps rank-1 always and rank-2 only when score2/score1 is close — it
146
+ // needs the raw scores, which the text renderer hides. Independent of the tuning flags.
147
+ if (sub === "tmct_locate") {
148
+ const args = parsePayload(payload);
149
+ if (args === null) {
150
+ process.stderr.write("tmct: tmct_locate expects a JSON arg, e.g. '{\"query\":\"…\",\"repo_path\":\"/abs\"}'\n");
151
+ process.exit(2);
152
+ }
153
+ const config = configFor(args.repo_path);
154
+ try {
155
+ const graph = parseEntities(await source.fetchEntities(config));
156
+ // B016 recall-lever flags (LOCATE-phase, per-arm; output byte-identical when absent).
157
+ const ranked = searchModulesRanked(graph, args.query || "", {
158
+ demoteNonProd: !!args.demote_nonprod, // R1a: demote examples//fixtures//test-* paths
159
+ callAdjacency: !!args.call_adjacency, // E1a: resolved-call adjacency bonus
160
+ implOfInterface: !!args.impl_of_interface, // E1b: C# impl-of-interface boost
161
+ beamSearch: !!args.beam_search, // §5.15: multi-ply discriminative expansion
162
+ ...(Number.isFinite(Number(args.beam_width)) ? { beamWidth: Number(args.beam_width) } : {}),
163
+ // B018 §8.1.3 literal-mention lever: match verbatim dotted-name/path mentions in the RAW
164
+ // query (which the locate tokenizer destroys). searchModulesRanked derives rawQuery from the
165
+ // query arg when literalMention is on; the rig passes the raw problem as the query, so literal
166
+ // matching keys off the untokenized text. raw_query is forwarded too for callers that normalize
167
+ // the query arg separately from the raw problem text.
168
+ // SHIPPED DEFAULT (0.5.0): literal-mention is ON for a fresh invocation (no arg, no
169
+ // tmct.toml) — pass `literal_mention:false` to disable. It is a strict no-op on queries
170
+ // with no ≥3-component dotted path / repo-relative path, so it never perturbs the cells the
171
+ // headline B018 numbers were measured on. The low-level scoreModules default (codegraph.mjs)
172
+ // stays literalMention=false; the product surface opts in explicitly, right here.
173
+ literalMention: args.literal_mention !== false,
174
+ ...(args.raw_query != null ? { rawQuery: String(args.raw_query) } : {}),
175
+ });
176
+ process.stdout.write(ranked.map((r) => `${r.path}\t${r.score}`).join("\n") + "\n");
177
+ } catch (e) {
178
+ process.stderr.write(`tmct: ${e?.message || e}\n`);
179
+ process.exit(1);
180
+ }
181
+ return;
182
+ }
183
+
184
+ // tool-query fallback: any other `cli <toolName> '{…}'` routes to dispatchTool,
185
+ // so "cold" tools are invokable from Bash directly.
186
+ if (sub) {
187
+ const args = parsePayload(payload);
188
+ if (args === null) {
189
+ process.stderr.write(`tmct: ${sub} expects a JSON arg, e.g. '{"symbol":"<name>"}'\n`);
190
+ process.exit(2);
191
+ }
192
+ const config = configFor(args.repo_path);
193
+ try {
194
+ const text = await dispatchTool(sub, args, { config });
195
+ process.stdout.write(text + "\n");
196
+ } catch (e) {
197
+ process.stderr.write(`tmct: ${e?.message || e}\n`);
198
+ process.exit(1);
199
+ }
200
+ return;
201
+ }
202
+
203
+ process.stderr.write("tmct: `cli` needs a sub-command (digest | <toolName>)\n");
204
+ process.exit(2);
205
+ }
206
+
207
+ if (mode === "chat") {
208
+ const { runChat } = await import("../src/chat.mjs");
209
+ const i = process.argv.indexOf("--repo");
210
+ await runChat({
211
+ repoPath: i !== -1 ? process.argv[i + 1] : undefined,
212
+ });
213
+ return;
214
+ }
215
+
216
+ // Bare invocation: no server to start any more — print the usage line and exit 2.
217
+ // (bin/tmct.mjs splices in `chat` for bare invocations, so nothing user-facing is lost.)
218
+ process.stderr.write(`tmct: ${mode === undefined ? "missing mode" : `unknown invocation "${process.argv.slice(2).join(" ")}"`}. ` +
219
+ "Use `cli digest …`, `cli <tool> …`, or `chat`.\n");
220
+ process.exit(2);
221
+ }
222
+
223
+ main().catch((e) => {
224
+ process.stderr.write(`tmct: ${e?.message || e}\n`);
225
+ process.exit(1);
226
+ });
package/bin/tmct.mjs ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // tmct — The Mechanical Code Talker. The headline entry is CHAT: a bare
3
+ // invocation drops you into a tolerant, offline, $0 prompt that guides you
4
+ // toward precision queries about a repository (ELIZA/PARRY-style, but obsessed
5
+ // with software). No model calls; tmct keeps no codebase index of its own.
6
+ //
7
+ // tmct → interactive chat (the headline)
8
+ // tmct chat [--repo <abs>] → same, explicit
9
+ // tmct cli <tool> '{…json}' → invoke a graph tool directly (de-emphasized carry-over)
10
+ // tmct --help → this help
11
+ //
12
+ // tmct began as a whole-package lift of an earlier chat surface (see README
13
+ // provenance): internal module filenames and symbols were kept to preserve the
14
+ // shape and the green test suite. The non-chat modes are carried but
15
+ // de-emphasized — see README.md for what tmct is and deliberately is NOT, and
16
+ // ROADMAP.md for where it is going.
17
+
18
+ const HELP = `tmct — The Mechanical Code Talker
19
+
20
+ A tolerant, offline, $0 chat that guides you toward precision queries about a
21
+ software repository. No model calls; no codebase index of its own.
22
+
23
+ Usage:
24
+ tmct interactive chat (the headline surface)
25
+ tmct chat [--repo <abs>] chat over a specific repo's graph
26
+ tmct cli <tool> '{…}' invoke a graph tool directly (carry-over, de-emphasized)
27
+ tmct --help show this help
28
+
29
+ In chat: /help lists slash-commands; /exit leaves. Session log → <repo>/.tmct/session-<id>.log.
30
+ `;
31
+
32
+ const args = process.argv.slice(2);
33
+
34
+ if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
35
+ process.stdout.write(HELP);
36
+ process.exit(0);
37
+ }
38
+
39
+ // Headline behaviour: a bare invocation is CHAT. We rewrite argv so the carried
40
+ // dispatcher (bin/cli.mjs, kept verbatim for the test suite) sees `chat`, then
41
+ // hand off to it. Any explicit mode passes straight through unchanged.
42
+ if (args.length === 0) {
43
+ process.argv.splice(2, 0, "chat");
44
+ }
45
+
46
+ // Delegate to the carried dispatcher. It runs its own main() on import.
47
+ await import("./cli.mjs");
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@polycode-projects/the-mechanical-code-talker",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
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.",
7
+ "license": "MPL-2.0",
8
+ "author": "Polycode Limited",
9
+ "homepage": "https://polycode-projects.gitlab.io/the-mechanical-code-talker/",
10
+ "engines": {
11
+ "node": ">=24"
12
+ },
13
+ "bin": {
14
+ "tmct": "./bin/tmct.mjs"
15
+ },
16
+ "main": "./src/index.mjs",
17
+ "exports": {
18
+ ".": "./src/index.mjs",
19
+ "./chat": "./src/chat.mjs",
20
+ "./resolveObject": "./src/ask.mjs",
21
+ "./ask": "./src/ask.mjs",
22
+ "./relationKind": "./src/codegraph.mjs",
23
+ "./impactClosure": "./src/codegraph.mjs",
24
+ "./dispatchTool": "./src/server.mjs",
25
+ "./fetchEntities": "./src/source.mjs"
26
+ },
27
+ "files": [
28
+ "bin/",
29
+ "src/",
30
+ "README.md",
31
+ "ROADMAP.md",
32
+ "LICENSE"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "smol-toml": "^1.7.0",
39
+ "wink-eng-lite-web-model": "^1.8.1",
40
+ "wink-nlp": "^2.4.0"
41
+ },
42
+ "scripts": {
43
+ "test": "node --test \"test/**/*.test.mjs\"",
44
+ "chat": "node bin/tmct.mjs"
45
+ }
46
+ }
@@ -0,0 +1,73 @@
1
+ // ask-nlp.mjs — the OPTIONAL wink-nlp adapter behind ask.mjs's lemma/POS tier.
2
+ //
3
+ // BOUNDARY (hard, do not move): this file is Node-only and is NEVER inlined into
4
+ // the viewer bundle. viz.mjs's askSource() inlines codegraph.mjs + ask-vocab.mjs +
5
+ // ask.mjs ONLY and strips their import lines, so the portable single-file HTML has
6
+ // no wink, no model, and no `nlpAdapter` binding at all — ask.mjs reaches this
7
+ // module exclusively through a `typeof nlpAdapter === "function"` guard and
8
+ // degrades to adapter-less parsing (lemma/POS tiers off; the curated tables and
9
+ // the bounded edit-distance tier still work, browser and Node alike). Keeping the
10
+ // ~1MB CJS model out of the page is the point of the split.
11
+ //
12
+ // wink-nlp and wink-eng-lite-web-model are CJS — loaded via createRequire, the
13
+ // same Node-module-resolution approach viz.mjs uses to locate cytoscape (resolve
14
+ // through the module system, never a guessed path). The require happens lazily on
15
+ // first use and failure is cached as null: a checkout without the optional deps
16
+ // installed answers exactly like the browser bundle, it never throws.
17
+
18
+ import { createRequire } from "node:module";
19
+
20
+ let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
21
+
22
+ /** Lazily build the {lemma, posTags} adapter, or null when wink isn't loadable.
23
+ * Deterministic: wink-nlp's tagger/lemmatiser is a fixed model with no sampling,
24
+ * so the same input always yields the same tags/lemmas across processes. */
25
+ export function nlpAdapter() {
26
+ if (cached !== undefined) return cached;
27
+ try {
28
+ const require = createRequire(import.meta.url);
29
+ const winkNLP = require("wink-nlp");
30
+ const model = require("wink-eng-lite-web-model");
31
+ const nlp = winkNLP(model);
32
+ const its = nlp.its;
33
+ cached = {
34
+ /** Lowercase lemma of a single token ("imported" -> "import"); the word
35
+ * itself when wink has nothing better (unknown words come back as-is). */
36
+ lemma(word) {
37
+ const w = String(word || "");
38
+ try {
39
+ const out = nlp.readDoc(w).tokens().out(its.lemma);
40
+ return String(out[0] || w).toLowerCase();
41
+ } catch {
42
+ return w.toLowerCase();
43
+ }
44
+ },
45
+ /** UPOS tags aligned to the CALLER's word array. wink re-tokenizes (it
46
+ * splits "walk.mjs" into three tokens), so each input word is greedily
47
+ * matched to the run of wink tokens that spell it and takes its FIRST
48
+ * sub-token's tag; null per word on any surprise, never a throw. */
49
+ posTags(words) {
50
+ try {
51
+ const toks = nlp.readDoc(words.join(" ")).tokens();
52
+ const texts = toks.out();
53
+ const tags = toks.out(its.pos);
54
+ const out = [];
55
+ let k = 0;
56
+ for (const w of words) {
57
+ if (k >= texts.length) { out.push(null); continue; }
58
+ out.push(tags[k]);
59
+ let acc = texts[k];
60
+ k += 1;
61
+ while (acc.length < w.length && k < texts.length) { acc += texts[k]; k += 1; }
62
+ }
63
+ return out;
64
+ } catch {
65
+ return words.map(() => null);
66
+ }
67
+ },
68
+ };
69
+ } catch {
70
+ cached = null;
71
+ }
72
+ return cached;
73
+ }