@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/LICENSE +373 -0
- package/README.md +108 -0
- package/ROADMAP.md +209 -0
- package/bin/cli.mjs +226 -0
- package/bin/tmct.mjs +47 -0
- package/package.json +46 -0
- package/src/ask-nlp.mjs +73 -0
- package/src/ask-vocab.mjs +687 -0
- package/src/ask.mjs +2403 -0
- package/src/chat.mjs +642 -0
- package/src/codegraph.mjs +1972 -0
- package/src/config.mjs +27 -0
- package/src/embed.mjs +191 -0
- package/src/graph-build.mjs +428 -0
- package/src/index.mjs +25 -0
- package/src/prose-nlp.mjs +52 -0
- package/src/prose.mjs +187 -0
- package/src/schema-docs.mjs +254 -0
- package/src/server.mjs +393 -0
- package/src/sessions.mjs +220 -0
- package/src/source.mjs +54 -0
- package/src/telemetry.mjs +90 -0
- package/src/toml-config.mjs +183 -0
- package/src/uuid.mjs +16 -0
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
|
+
}
|
package/src/ask-nlp.mjs
ADDED
|
@@ -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
|
+
}
|