@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.4.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/README.md +77 -3
- package/ROADMAP.md +416 -3
- package/bin/tmct.mjs +308 -12
- package/corpus/README.md +52 -0
- package/corpus/conceptnet/LICENSE-NOTICE +37 -0
- package/corpus/conceptnet/README.md +103 -0
- package/corpus/conceptnet/fetch-slice.mjs +136 -0
- package/corpus/conceptnet/filter-dump.mjs +89 -0
- package/corpus/conceptnet/slice.jsonl +14258 -0
- package/data/phrasebook/software-phrases.txt +231 -0
- package/data/templates/grammar-rules.toml +89 -0
- package/data/templates/responses.jsonl +68 -0
- package/package.json +40 -3
- package/src/ask-nlp.mjs +22 -10
- package/src/ask-vocab.mjs +35 -1
- package/src/ask.mjs +171 -494
- package/src/chat.mjs +709 -81
- package/src/corpus/conceptnet-map.toml +251 -0
- package/src/corpus/conceptnet.mjs +167 -0
- package/src/corpus/templates.mjs +188 -0
- package/src/finish.mjs +443 -0
- package/src/grammar/ace.mjs +341 -0
- package/src/grammar/assert.mjs +40 -0
- package/src/grammar/lexicon-core.json +287 -0
- package/src/grammar/lexicon.mjs +202 -0
- package/src/hash.mjs +32 -0
- package/src/index.mjs +21 -5
- package/src/init.mjs +264 -0
- package/src/interpret/fuzzy.mjs +89 -0
- package/src/interpret/merge.mjs +148 -0
- package/src/interpret/normalize.mjs +151 -0
- package/src/interpret/pipeline.mjs +112 -0
- package/src/interpret/strategies/grammar.mjs +137 -0
- package/src/interpret/strategies/keywords.mjs +241 -0
- package/src/interpret/strategies/noise-strip.mjs +114 -0
- package/src/memory/blocks.mjs +221 -0
- package/src/memory/core.mjs +533 -0
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +141 -0
- package/src/memory/trust.mjs +113 -0
- package/src/prose-nlp.mjs +14 -16
- package/src/providers/bootstrap.mjs +24 -0
- package/src/providers/fixture.mjs +118 -0
- package/src/providers/graph-service.mjs +312 -0
- package/src/repository-interface.mjs +318 -0
- package/src/server.mjs +44 -28
- package/src/sessions.mjs +137 -4
- package/src/source.mjs +44 -5
- package/src/syllogise.mjs +0 -0
- package/src/toml-config.mjs +14 -0
- package/src/tui/app.mjs +173 -0
- package/src/wink-model.mjs +74 -0
- package/bin/cli.mjs +0 -226
package/bin/tmct.mjs
CHANGED
|
@@ -5,15 +5,26 @@
|
|
|
5
5
|
// with software). No model calls; tmct keeps no codebase index of its own.
|
|
6
6
|
//
|
|
7
7
|
// tmct → interactive chat (the headline)
|
|
8
|
-
// tmct chat [--repo <abs>]
|
|
8
|
+
// tmct chat [--repo <abs>] [--plain] → same, explicit
|
|
9
9
|
// tmct cli <tool> '{…json}' → invoke a graph tool directly (de-emphasized carry-over)
|
|
10
|
+
// tmct cli digest '{…json}' → architecture map + per-module context bundles
|
|
10
11
|
// tmct --help → this help
|
|
11
12
|
//
|
|
13
|
+
// On a real terminal, chat is the full-screen Ink TUI (src/tui/app.mjs);
|
|
14
|
+
// `--plain` — or a non-TTY stdin/stdout (pipes, scripts, the test suite) —
|
|
15
|
+
// gets the classic readline shell. BOTH run the same session sink
|
|
16
|
+
// (src/chat.mjs createSession), so logs, sidecars and graph memory are
|
|
17
|
+
// identical either way.
|
|
18
|
+
//
|
|
19
|
+
// The `cli` arms (digest / tmct_locate / any-tool fallback) are the carried,
|
|
20
|
+
// de-emphasized non-chat modes, folded in here from the former bin/cli.mjs.
|
|
21
|
+
// The graph artifact lives at <repo_path>/.tmct/graph.json; the tools (run
|
|
22
|
+
// with cwd = that repo) load it by default. No flags, no config files.
|
|
23
|
+
//
|
|
12
24
|
// tmct began as a whole-package lift of an earlier chat surface (see README
|
|
13
25
|
// provenance): internal module filenames and symbols were kept to preserve the
|
|
14
|
-
// shape and the green test suite.
|
|
15
|
-
//
|
|
16
|
-
// ROADMAP.md for where it is going.
|
|
26
|
+
// shape and the green test suite. See README.md for what tmct is and
|
|
27
|
+
// deliberately is NOT, and ROADMAP.md for where it is going.
|
|
17
28
|
|
|
18
29
|
const HELP = `tmct — The Mechanical Code Talker
|
|
19
30
|
|
|
@@ -23,25 +34,310 @@ software repository. No model calls; no codebase index of its own.
|
|
|
23
34
|
Usage:
|
|
24
35
|
tmct interactive chat (the headline surface)
|
|
25
36
|
tmct chat [--repo <abs>] chat over a specific repo's graph
|
|
37
|
+
[--plain] force the plain readline shell (the default when
|
|
38
|
+
stdin/stdout is not a terminal)
|
|
39
|
+
tmct memory [--repo <abs>] what tmct remembers: facts, utterances, sessions,
|
|
40
|
+
[--verbose] folded blocks (the /memory chat command, from the shell)
|
|
41
|
+
tmct init [--force] initialize the current directory for tmct: .tmct/,
|
|
42
|
+
tmct.toml, tier-1 corpus seed, provenance record
|
|
43
|
+
tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
|
|
44
|
+
[--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
|
|
45
|
+
bounded, low-trust, retractable entailed facts (never on the chat path)
|
|
26
46
|
tmct cli <tool> '{…}' invoke a graph tool directly (carry-over, de-emphasized)
|
|
47
|
+
tmct cli digest '{…}' architecture map + per-module context bundles
|
|
27
48
|
tmct --help show this help
|
|
28
49
|
|
|
50
|
+
On a terminal, chat opens the full-screen TUI; piped input gets the plain shell.
|
|
29
51
|
In chat: /help lists slash-commands; /exit leaves. Session log → <repo>/.tmct/session-<id>.log.
|
|
30
52
|
`;
|
|
31
53
|
|
|
32
|
-
const
|
|
54
|
+
const argv = process.argv.slice(2);
|
|
33
55
|
|
|
34
|
-
if (
|
|
56
|
+
if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
|
|
35
57
|
process.stdout.write(HELP);
|
|
36
58
|
process.exit(0);
|
|
37
59
|
}
|
|
38
60
|
|
|
39
|
-
// Headline behaviour: a bare invocation is CHAT. We rewrite argv so the
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
if (args.length === 0) {
|
|
61
|
+
// Headline behaviour: a bare invocation is CHAT. We rewrite argv so the mode
|
|
62
|
+
// dispatch below (and anything downstream reading process.argv) sees `chat`.
|
|
63
|
+
if (argv.length === 0) {
|
|
43
64
|
process.argv.splice(2, 0, "chat");
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
/** Parse the trailing JSON payload of a `cli` sub-command (best-effort). */
|
|
68
|
+
function parsePayload(payload) {
|
|
69
|
+
if (!payload) return {};
|
|
70
|
+
try { return JSON.parse(payload); }
|
|
71
|
+
catch { return null; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DIGEST_MODULE_CAP = 12; // bound the digest — a handful of changed modules
|
|
75
|
+
const DIGEST_SECONDARY_CAP = 2; // B2: at most this many SECONDARY modules get a (trimmed) bundle
|
|
76
|
+
const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
|
|
77
|
+
|
|
78
|
+
/** `cli digest` — print a machine-readable header + a repo architecture map + the
|
|
79
|
+
* tmct_context edit bundle for each requested module to stdout (reuses the server's exact
|
|
80
|
+
* renderer via buildContextBundle, so no render logic is duplicated). This stdout is injected
|
|
81
|
+
* into a caller's prompt.
|
|
82
|
+
*
|
|
83
|
+
* Two ways to say which modules: an explicit `modules` array (unchanged), or a `query` string —
|
|
84
|
+
* auto-locate + score-gap-select (R1b, the shipped default as of 2026-07-02) in one call, so a
|
|
85
|
+
* real caller no longer has to run `tmct_locate` and hand-pick a module themselves. `modules`
|
|
86
|
+
* wins if both are given. The header reports which modules were actually selected either way.
|
|
87
|
+
*
|
|
88
|
+
* B2: the FIRST (primary) module gets a full size-adaptive bundle; the remaining modules are
|
|
89
|
+
* RANKED by import/cochange proximity to the primary and only the top few get a TRIMMED
|
|
90
|
+
* (signatures + insertion region) bundle — so a 2-module task no longer pays for two full
|
|
91
|
+
* bundles. The leading header line lets the rig record tier/topup telemetry. */
|
|
92
|
+
async function runDigest(args, { dispatchTool, buildContextBundle, source, configFor, codegraph }) {
|
|
93
|
+
const { parseEntities, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP } = codegraph;
|
|
94
|
+
const repoPath = args.repo_path;
|
|
95
|
+
if (!repoPath) { process.stderr.write("tmct: digest requires repo_path\n"); process.exit(2); }
|
|
96
|
+
let modules = Array.isArray(args.modules) ? args.modules.slice(0, DIGEST_MODULE_CAP) : [];
|
|
97
|
+
let autoSelected = null; // for the header, when `query` drove selection
|
|
98
|
+
if (!modules.length && args.query) {
|
|
99
|
+
const graph = parseEntities(await source.fetchEntities(configFor(repoPath)));
|
|
100
|
+
// SHIPPED DEFAULT (0.5.0): the digest's query-mode auto-locate resolves literal-mention ON
|
|
101
|
+
// (a fresh invocation with no tmct.toml), disable-able via `literal_mention:false`. Kept in
|
|
102
|
+
// lockstep with the `tmct_locate` handler so `cli digest '{query}'` ≡ `cli tmct_locate` for the
|
|
103
|
+
// same query. A strict no-op unless the query carries a ≥3-component dotted path / repo-relative
|
|
104
|
+
// path; searchModulesRanked derives rawQuery from the query when literalMention is on.
|
|
105
|
+
const ranked = searchModulesRanked(graph, args.query, { literalMention: args.literal_mention !== false });
|
|
106
|
+
const scoreGapK = args.score_gap === false ? null : (Number.isFinite(args.score_gap) ? args.score_gap : DEFAULT_SCORE_GAP);
|
|
107
|
+
modules = selectRankedModules(ranked, { top_k: Number.isFinite(args.top_k) ? args.top_k : 2, scoreGapK }).slice(0, DIGEST_MODULE_CAP);
|
|
108
|
+
autoSelected = modules;
|
|
109
|
+
if (!modules.length) process.stderr.write(`tmct: digest query "${args.query}" matched no modules — empty digest\n`);
|
|
110
|
+
}
|
|
111
|
+
// Tuning-flag contract (threaded to buildContextBundle → sizeBundle): `min` → leanest TINY/no
|
|
112
|
+
// top-up; `untuned` → the earlier escalation. Neither → the tuned default. The digest header still
|
|
113
|
+
// reports the EFFECTIVE tier/topup returned per module, so rig telemetry stays correct.
|
|
114
|
+
const min = Boolean(args.min);
|
|
115
|
+
const untuned = Boolean(args.untuned);
|
|
116
|
+
// tmct-max: the injection CEILING — every requested module gets a FULL (untrimmed) bundle,
|
|
117
|
+
// not just the primary + 2 trimmed secondaries. Tests whether maximal injection re-bloats.
|
|
118
|
+
const max = Boolean(args.max);
|
|
119
|
+
const secondaryCap = max ? modules.length : DIGEST_SECONDARY_CAP;
|
|
120
|
+
const config = configFor(repoPath);
|
|
121
|
+
const body = [];
|
|
122
|
+
let effTier = "NONE"; // largest tier emitted across all modules
|
|
123
|
+
let topup = false; // whether any module's auto-sizing escalated above TINY
|
|
124
|
+
let emitted = 0; // module bundles actually emitted (primary + trimmed secondaries)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
body.push("# Repository architecture\n" + (await dispatchTool("tmct_architecture", {}, { config })));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
body.push(`# Repository architecture\n(unavailable: ${e?.message || e})`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const emit = async (m, trim) => {
|
|
133
|
+
try {
|
|
134
|
+
const { text, tier, topup: t } = await buildContextBundle({ symbol: m, min, untuned, max }, { config, source, trim });
|
|
135
|
+
body.push(`\n# Context bundle: ${m}${trim ? " (secondary, trimmed)" : ""}\n` + text);
|
|
136
|
+
if ((TIER_RANK[tier] || 0) > (TIER_RANK[effTier] || 0)) effTier = tier;
|
|
137
|
+
if (t) topup = true;
|
|
138
|
+
emitted += 1;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
body.push(`\n# Context bundle: ${m}\n(no bundle: ${e?.message || e})`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (modules.length) {
|
|
145
|
+
const [primary, ...rest] = modules;
|
|
146
|
+
// rank the secondaries by proximity to the primary (best-effort: keep input order on error)
|
|
147
|
+
let ranked = rest;
|
|
148
|
+
if (rest.length) {
|
|
149
|
+
try { ranked = rankModulesByProximity(parseEntities(await source.fetchEntities(config)), primary, rest); }
|
|
150
|
+
catch { ranked = rest; }
|
|
151
|
+
}
|
|
152
|
+
const secondaries = ranked.slice(0, secondaryCap);
|
|
153
|
+
const overflow = ranked.slice(secondaryCap);
|
|
154
|
+
await emit(primary, false);
|
|
155
|
+
for (const m of secondaries) await emit(m, max ? false : true);
|
|
156
|
+
if (overflow.length) body.push(`\n# Related modules (not expanded; query tmct_context if needed): ${overflow.join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// HARD CONTRACT: first line is the machine-readable digest header the rig greps. Fields are
|
|
160
|
+
// append-only — `selected=` is new (query mode only) and never changes the existing ones the
|
|
161
|
+
// rig's own parser depends on.
|
|
162
|
+
const header = `# tmct-digest tier=${effTier} topup=${topup} modules=${emitted}`
|
|
163
|
+
+ (autoSelected ? ` selected=${autoSelected.join(",") || "(none)"}` : "");
|
|
164
|
+
process.stdout.write([header, ...body].join("\n") + "\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The carried `cli` dispatcher (digest / tmct_locate / any-tool fallback).
|
|
168
|
+
* Imports are lazy so `tmct --help` and chat startup never pay for the tool
|
|
169
|
+
* stack. */
|
|
170
|
+
async function runCliMode() {
|
|
171
|
+
const [, sub, payload] = process.argv.slice(2);
|
|
172
|
+
const { join } = await import("node:path");
|
|
173
|
+
const { dispatchTool, buildContextBundle } = await import("../src/server.mjs");
|
|
174
|
+
const { loadConfig, DEFAULT_GRAPH_REL } = await import("../src/config.mjs");
|
|
175
|
+
const source = await import("../src/source.mjs");
|
|
176
|
+
const codegraph = await import("../src/codegraph.mjs");
|
|
177
|
+
const { parseEntities, searchModulesRanked } = codegraph;
|
|
178
|
+
|
|
179
|
+
/** Build a config pointed at a specific repo's artifact (for `cli` sub-commands that
|
|
180
|
+
* take a repo_path), or fall back to the cwd-derived default. */
|
|
181
|
+
const configFor = (repoPath) =>
|
|
182
|
+
repoPath ? { graphFile: join(repoPath, DEFAULT_GRAPH_REL) } : loadConfig();
|
|
183
|
+
|
|
184
|
+
// digest mode: architecture map + per-module context bundles → stdout
|
|
185
|
+
if (sub === "digest") {
|
|
186
|
+
const args = parsePayload(payload);
|
|
187
|
+
if (args === null) {
|
|
188
|
+
process.stderr.write("tmct: digest expects a JSON arg, e.g. '{\"repo_path\":\"/abs\",\"modules\":[…]}'\n");
|
|
189
|
+
process.exit(2);
|
|
190
|
+
}
|
|
191
|
+
await runDigest(args, { dispatchTool, buildContextBundle, source, configFor, codegraph });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// locate mode (TUNING #3): `cli tmct_locate '{"query":"…","repo_path":"<abs>"}'` emits the
|
|
196
|
+
// ranked modules as `<relpath>\t<score>`, one per line (highest first), using renderSearch's
|
|
197
|
+
// exact ranking. The rig keeps rank-1 always and rank-2 only when score2/score1 is close — it
|
|
198
|
+
// needs the raw scores, which the text renderer hides. Independent of the tuning flags.
|
|
199
|
+
if (sub === "tmct_locate") {
|
|
200
|
+
const args = parsePayload(payload);
|
|
201
|
+
if (args === null) {
|
|
202
|
+
process.stderr.write("tmct: tmct_locate expects a JSON arg, e.g. '{\"query\":\"…\",\"repo_path\":\"/abs\"}'\n");
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
const config = configFor(args.repo_path);
|
|
206
|
+
try {
|
|
207
|
+
const graph = parseEntities(await source.fetchEntities(config));
|
|
208
|
+
// B016 recall-lever flags (LOCATE-phase, per-arm; output byte-identical when absent).
|
|
209
|
+
const ranked = searchModulesRanked(graph, args.query || "", {
|
|
210
|
+
demoteNonProd: !!args.demote_nonprod, // R1a: demote examples//fixtures//test-* paths
|
|
211
|
+
callAdjacency: !!args.call_adjacency, // E1a: resolved-call adjacency bonus
|
|
212
|
+
implOfInterface: !!args.impl_of_interface, // E1b: C# impl-of-interface boost
|
|
213
|
+
beamSearch: !!args.beam_search, // §5.15: multi-ply discriminative expansion
|
|
214
|
+
...(Number.isFinite(Number(args.beam_width)) ? { beamWidth: Number(args.beam_width) } : {}),
|
|
215
|
+
// B018 §8.1.3 literal-mention lever: match verbatim dotted-name/path mentions in the RAW
|
|
216
|
+
// query (which the locate tokenizer destroys). searchModulesRanked derives rawQuery from the
|
|
217
|
+
// query arg when literalMention is on; the rig passes the raw problem as the query, so literal
|
|
218
|
+
// matching keys off the untokenized text. raw_query is forwarded too for callers that normalize
|
|
219
|
+
// the query arg separately from the raw problem text.
|
|
220
|
+
// SHIPPED DEFAULT (0.5.0): literal-mention is ON for a fresh invocation (no arg, no
|
|
221
|
+
// tmct.toml) — pass `literal_mention:false` to disable. It is a strict no-op on queries
|
|
222
|
+
// with no ≥3-component dotted path / repo-relative path, so it never perturbs the cells the
|
|
223
|
+
// headline B018 numbers were measured on. The low-level scoreModules default (codegraph.mjs)
|
|
224
|
+
// stays literalMention=false; the product surface opts in explicitly, right here.
|
|
225
|
+
literalMention: args.literal_mention !== false,
|
|
226
|
+
...(args.raw_query != null ? { rawQuery: String(args.raw_query) } : {}),
|
|
227
|
+
});
|
|
228
|
+
process.stdout.write(ranked.map((r) => `${r.path}\t${r.score}`).join("\n") + "\n");
|
|
229
|
+
} catch (e) {
|
|
230
|
+
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// tool-query fallback: any other `cli <toolName> '{…}'` routes to dispatchTool,
|
|
237
|
+
// so "cold" tools are invokable from Bash directly.
|
|
238
|
+
if (sub) {
|
|
239
|
+
const args = parsePayload(payload);
|
|
240
|
+
if (args === null) {
|
|
241
|
+
process.stderr.write(`tmct: ${sub} expects a JSON arg, e.g. '{"symbol":"<name>"}'\n`);
|
|
242
|
+
process.exit(2);
|
|
243
|
+
}
|
|
244
|
+
const config = configFor(args.repo_path);
|
|
245
|
+
try {
|
|
246
|
+
const text = await dispatchTool(sub, args, { config });
|
|
247
|
+
process.stdout.write(text + "\n");
|
|
248
|
+
} catch (e) {
|
|
249
|
+
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
process.stderr.write("tmct: `cli` needs a sub-command (digest | <toolName>)\n");
|
|
256
|
+
process.exit(2);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function main() {
|
|
260
|
+
const mode = process.argv[2];
|
|
261
|
+
|
|
262
|
+
if (mode === "chat") {
|
|
263
|
+
const rest = process.argv.slice(3);
|
|
264
|
+
const i = rest.indexOf("--repo");
|
|
265
|
+
const repoPath = i !== -1 ? rest[i + 1] : undefined;
|
|
266
|
+
// The shell gate: a real terminal gets the full-screen Ink TUI; `--plain` or a
|
|
267
|
+
// non-TTY stream (pipes, scripts, the test suite) gets the readline shell. Both
|
|
268
|
+
// drive the same createSession sink — only the drawing differs.
|
|
269
|
+
const plain = rest.includes("--plain") || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
270
|
+
if (plain) {
|
|
271
|
+
const { runChat } = await import("../src/chat.mjs");
|
|
272
|
+
await runChat({ repoPath });
|
|
273
|
+
} else {
|
|
274
|
+
const { runTui } = await import("../src/tui/app.mjs");
|
|
275
|
+
await runTui({ repoPath });
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (mode === "memory") {
|
|
281
|
+
// `tmct memory` — the /memory chat command from the shell: same renderer
|
|
282
|
+
// (src/memory/inspect.mjs), same repo resolution as chat (git root default).
|
|
283
|
+
const rest = process.argv.slice(3);
|
|
284
|
+
const i = rest.indexOf("--repo");
|
|
285
|
+
const repoPath = i !== -1 ? rest[i + 1] : undefined;
|
|
286
|
+
const verbose = rest.includes("--verbose") || rest.includes("-v");
|
|
287
|
+
const { gitToplevel } = await import("../src/chat.mjs");
|
|
288
|
+
const { inspectMemory } = await import("../src/memory/inspect.mjs");
|
|
289
|
+
const repo = repoPath || gitToplevel(process.cwd()) || process.cwd();
|
|
290
|
+
process.stdout.write(await inspectMemory(repo, { verbose }) + "\n");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (mode === "init") {
|
|
295
|
+
// `tmct init` — the Repository-Interface onboarding surface: scaffold .tmct/,
|
|
296
|
+
// write tmct.toml, seed the tier-1 corpus (offline, opt-out via TMCT_NO_SEED),
|
|
297
|
+
// and record provenance. Idempotent; --force rewrites config + re-records.
|
|
298
|
+
const rest = process.argv.slice(3);
|
|
299
|
+
const { initRepo } = await import("../src/init.mjs");
|
|
300
|
+
const res = await initRepo(process.cwd(), { force: rest.includes("--force") });
|
|
301
|
+
process.stdout.write(res.message + "\n");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (mode === "syllogise") {
|
|
306
|
+
// `tmct syllogise` — the explicit speculative-inference batch (never on the chat
|
|
307
|
+
// hot path): forward-chain the memory's rdfs:subClassOf closure into bounded,
|
|
308
|
+
// low-trust, retractable entailed facts. Same repo resolution as `memory`.
|
|
309
|
+
const rest = process.argv.slice(3);
|
|
310
|
+
const i = rest.indexOf("--repo");
|
|
311
|
+
const repoPath = i !== -1 ? rest[i + 1] : undefined;
|
|
312
|
+
const numFlag = (name, dflt) => {
|
|
313
|
+
const j = rest.indexOf(name);
|
|
314
|
+
const v = j !== -1 ? Number(rest[j + 1]) : NaN;
|
|
315
|
+
return Number.isFinite(v) ? v : dflt;
|
|
316
|
+
};
|
|
317
|
+
const { gitToplevel } = await import("../src/chat.mjs");
|
|
318
|
+
const { syllogise } = await import("../src/syllogise.mjs");
|
|
319
|
+
const repo = repoPath || gitToplevel(process.cwd()) || process.cwd();
|
|
320
|
+
const res = await syllogise(repo, { depth: numFlag("--depth", 32), budget: numFlag("--budget", 50) });
|
|
321
|
+
process.stdout.write(
|
|
322
|
+
`tmct syllogise — derived ${res.count} entailed fact(s) (subClassOf closure, depth ${res.depth}, budget ${res.budget})`
|
|
323
|
+
+ (res.truncated ? " — budget reached, more available" : "") + "\n",
|
|
324
|
+
);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (mode === "cli") {
|
|
329
|
+
await runCliMode();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// An unknown mode gets the instructive usage line and exit 2. (A bare invocation
|
|
334
|
+
// never lands here — the argv splice above rewrote it to `chat`.)
|
|
335
|
+
process.stderr.write(`tmct: unknown invocation "${process.argv.slice(2).join(" ")}". ` +
|
|
336
|
+
"Use `cli digest …`, `cli <tool> …`, `memory`, or `chat`.\n");
|
|
337
|
+
process.exit(2);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main().catch((e) => {
|
|
341
|
+
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|
package/corpus/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# corpus/ — committed corpus data
|
|
2
|
+
|
|
3
|
+
The corpuses tmct ships so that an **empty** tmct still has a vocabulary
|
|
4
|
+
(ROADMAP Phase 2). Everything here is plain, diffable data; the loaders live
|
|
5
|
+
in `src/corpus/`. Related committed data lives in `data/` (response templates
|
|
6
|
+
+ the SE phrase book — items 4+7).
|
|
7
|
+
|
|
8
|
+
## What's here
|
|
9
|
+
|
|
10
|
+
| Path | What | Size | Licence |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| `conceptnet/slice.jsonl` | filtered English/tech-domain ConceptNet 5.7 slice (one assertion per line) | ~1.4 MB | **CC-BY-SA 4.0** (see `conceptnet/LICENSE-NOTICE`) |
|
|
13
|
+
| `conceptnet/fetch-slice.mjs` | regeneration tool — the ConceptNet **API** route (polite, ~1 req/s) | — | MPL-2.0 |
|
|
14
|
+
| `conceptnet/filter-dump.mjs` | regeneration tool — the ConceptNet **dump** route (produced the committed slice; the API was down) | — | MPL-2.0 |
|
|
15
|
+
| `conceptnet/README.md` | provenance, retrieval date, seed terms, filter rules, row counts | — | — |
|
|
16
|
+
|
|
17
|
+
And alongside (same phase, different directory because it is tmct-original
|
|
18
|
+
data, not a derived corpus):
|
|
19
|
+
|
|
20
|
+
| Path | What | Licence |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `../data/templates/responses.jsonl` | ~56 response templates ({id, class, template, register}) | MPL-2.0 |
|
|
23
|
+
| `../data/phrasebook/software-phrases.txt` | ~170 SE phrase patterns + 31 synonym families | MPL-2.0 |
|
|
24
|
+
|
|
25
|
+
## How seeding works
|
|
26
|
+
|
|
27
|
+
`src/corpus/conceptnet.mjs` turns the slice into tmct memory facts:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { seedMemory } from "./src/corpus/conceptnet.mjs";
|
|
31
|
+
await seedMemory(repoDir); // writes <repoDir>/.tmct/memory/graph.json
|
|
32
|
+
await seedMemory(repoDir, { limit: 500 }); // capped (fast bootstrap)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Each assertion whose relation maps to an ACE-OWL pattern
|
|
36
|
+
(`src/corpus/conceptnet-map.toml`, `ace != "none"`) becomes one reified
|
|
37
|
+
fact via `src/memory/core.mjs` `appendFact`:
|
|
38
|
+
`{subject:"software bug", predicate:"rdfs:subClassOf", object:"error",
|
|
39
|
+
provenance:"corpus:conceptnet /r/IsA"}`.
|
|
40
|
+
- **Idempotent**: fact ids are content-hashed from the normalized triple, and
|
|
41
|
+
`seedMemory` pre-reads the store once and skips triples already present —
|
|
42
|
+
re-seeding costs one read, not N rewrites. Provenance from different
|
|
43
|
+
writers of the same triple is unioned, never overwritten.
|
|
44
|
+
- `ace = "none"` relations (RelatedTo, Synonym, FormOf, …) are deliberately
|
|
45
|
+
NOT seeded — they are kept in the slice for future lexicon/fuzzy-match use.
|
|
46
|
+
|
|
47
|
+
## How to regenerate / extend
|
|
48
|
+
|
|
49
|
+
See `conceptnet/README.md` — one command per route (API vs dump), plus the
|
|
50
|
+
seed-term list to extend. The test suite (`test/corpus-conceptnet.test.mjs`,
|
|
51
|
+
`test/corpus-templates.test.mjs`) guards the contracts: slice/mapping drift,
|
|
52
|
+
en→en shape, the ≤ 1.5 MB budget, template ids/slots, and end-to-end seeding.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
LICENSE NOTICE — corpus/conceptnet/slice.jsonl
|
|
2
|
+
==============================================
|
|
3
|
+
|
|
4
|
+
The file slice.jsonl in this directory is a filtered excerpt of ConceptNet 5.7,
|
|
5
|
+
and is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
|
6
|
+
International License (CC-BY-SA 4.0), NOT under this repository's MPL-2.0.
|
|
7
|
+
|
|
8
|
+
https://creativecommons.org/licenses/by-sa/4.0/
|
|
9
|
+
|
|
10
|
+
Attribution
|
|
11
|
+
-----------
|
|
12
|
+
|
|
13
|
+
This work includes data from ConceptNet 5, which was compiled by the
|
|
14
|
+
Commonsense Computing Initiative. ConceptNet 5 is freely available under the
|
|
15
|
+
Creative Commons Attribution-ShareAlike license (CC-BY-SA 4.0) from
|
|
16
|
+
https://conceptnet.io. The included data was created by contributors to
|
|
17
|
+
Commonsense Computing projects, contributors to Wikimedia projects, Games
|
|
18
|
+
with a Purpose, Princeton University's WordNet, DBPedia, OpenCyc, and Umbel.
|
|
19
|
+
|
|
20
|
+
Source
|
|
21
|
+
------
|
|
22
|
+
|
|
23
|
+
- Dataset: ConceptNet 5.7.0 assertions dump
|
|
24
|
+
https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-assertions-5.7.0.csv.gz
|
|
25
|
+
(dump published 2019-07-03; retrieved and filtered 2026-07-04)
|
|
26
|
+
- Project: https://conceptnet.io / https://github.com/commonsense/conceptnet5
|
|
27
|
+
- The filtering applied (English-only, tech-domain seed terms, canonical
|
|
28
|
+
relations, size budget) is described in README.md in this directory and is
|
|
29
|
+
reproducible via filter-dump.mjs.
|
|
30
|
+
|
|
31
|
+
Share-alike
|
|
32
|
+
-----------
|
|
33
|
+
|
|
34
|
+
If you redistribute slice.jsonl (modified or not), you must do so under
|
|
35
|
+
CC-BY-SA 4.0 with this attribution. The code in this directory
|
|
36
|
+
(fetch-slice.mjs, filter-dump.mjs) is tmct code under the repository's
|
|
37
|
+
MPL-2.0; only the data file carries CC-BY-SA 4.0.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# corpus/conceptnet — the committed tech-domain ConceptNet slice
|
|
2
|
+
|
|
3
|
+
`slice.jsonl` is a filtered excerpt of **real ConceptNet 5.7.0 data** — one
|
|
4
|
+
assertion per line:
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{"start":"/c/en/bug","rel":"/r/IsA","end":"/c/en/insect","weight":6.32,"surfaceText":"[[a bug]] is [[an insect]]"}
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
**Licence: CC-BY-SA 4.0** (ConceptNet-derived data; NOT this repo's MPL-2.0) —
|
|
11
|
+
see `LICENSE-NOTICE` in this directory for the full attribution.
|
|
12
|
+
|
|
13
|
+
## Provenance and retrieval
|
|
14
|
+
|
|
15
|
+
- **Source:** ConceptNet 5.7.0 assertions dump,
|
|
16
|
+
`https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-assertions-5.7.0.csv.gz`
|
|
17
|
+
(published 2019-07-03).
|
|
18
|
+
- **Retrieved + filtered:** 2026-07-04.
|
|
19
|
+
- **Why the dump, not the API:** the public API (`api.conceptnet.io`) was
|
|
20
|
+
hard-down (HTTP 502 from its nginx front-end on every request across ~15
|
|
21
|
+
attempts over 10+ minutes on 2026-07-04), so the slice was stream-filtered
|
|
22
|
+
from the published dump instead — same data, same licence. `fetch-slice.mjs`
|
|
23
|
+
(the API route) is kept for when the API is healthy; `filter-dump.mjs` is
|
|
24
|
+
the route that actually produced the committed slice.
|
|
25
|
+
|
|
26
|
+
## Filter rules
|
|
27
|
+
|
|
28
|
+
1. **English only**: `start` AND `end` are `/c/en/…` concepts; word-sense
|
|
29
|
+
suffixes are stripped (`/c/en/bug/n` → `/c/en/bug`); self-loops after
|
|
30
|
+
stripping are dropped.
|
|
31
|
+
2. **Canonical relations only**: the closed set of 34 relations (the same set
|
|
32
|
+
mapped in `src/corpus/conceptnet-map.toml`), minus three filtered by
|
|
33
|
+
policy: `/r/EtymologicallyRelatedTo`, `/r/EtymologicallyDerivedFrom`,
|
|
34
|
+
`/r/ExternalURL` (etymology noise and link-outs — no consumer in tmct).
|
|
35
|
+
3. **Tech-domain seed terms**: at least one endpoint's bare term is in the
|
|
36
|
+
90-term software/tech seed list exported as `SEED_TERMS` from
|
|
37
|
+
`fetch-slice.mjs` (software, computer, program, code, module, function,
|
|
38
|
+
database, server, network, bug, test, file, memory, algorithm, keyboard,
|
|
39
|
+
programmer, repository, commit, …).
|
|
40
|
+
4. **Dedupe** by `(start, rel, end)`, keeping the higher weight.
|
|
41
|
+
5. **Size budget** (committed slice ≤ 1.5 MB; target ~1.4 MB), **two-tier**:
|
|
42
|
+
assertions whose relation maps to an ACE-OWL pattern (`ace != "none"` in
|
|
43
|
+
`conceptnet-map.toml`) are kept first, weight-descending; `ace = "none"`
|
|
44
|
+
relations (`RelatedTo`, `Synonym`, …) fill the remaining budget — they
|
|
45
|
+
are kept for future lexicon/fuzzy-match use but never crowd out seedable
|
|
46
|
+
facts.
|
|
47
|
+
6. Deterministic output order: `(rel, start, end)`.
|
|
48
|
+
|
|
49
|
+
## Row counts (committed slice, 2026-07-04)
|
|
50
|
+
|
|
51
|
+
**14,258 assertions, 1,399,979 bytes** (34,074,917 dump lines scanned;
|
|
52
|
+
28,802 unique en→en seed assertions matched = 4,170 mappable + 24,632
|
|
53
|
+
`ace="none"`; ALL 4,170 mappable kept, 10,088 none-rows fill the budget).
|
|
54
|
+
29 of the 31 non-filtered canonical relations are present:
|
|
55
|
+
|
|
56
|
+
| Relation | Rows | | Relation | Rows |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| `/r/RelatedTo` | 4911 | | `/r/HasA` | 39 |
|
|
59
|
+
| `/r/HasContext` | 2634 | | `/r/HasProperty` | 27 |
|
|
60
|
+
| `/r/IsA` | 2594 | | `/r/Antonym` | 25 |
|
|
61
|
+
| `/r/DerivedFrom` | 1906 | | `/r/MotivatedByGoal` | 25 |
|
|
62
|
+
| `/r/AtLocation` | 459 | | `/r/SimilarTo` | 20 |
|
|
63
|
+
| `/r/Synonym` | 368 | | `/r/DistinctFrom` | 19 |
|
|
64
|
+
| `/r/UsedFor` | 312 | | `/r/DefinedAs` | 16 |
|
|
65
|
+
| `/r/FormOf` | 224 | | `/r/MadeOf` | 14 |
|
|
66
|
+
| `/r/CapableOf` | 164 | | `/r/CausesDesire` | 12 |
|
|
67
|
+
| `/r/MannerOf` | 141 | | `/r/HasLastSubevent` | 12 |
|
|
68
|
+
| `/r/PartOf` | 115 | | `/r/Causes` | 11 |
|
|
69
|
+
| `/r/HasPrerequisite` | 103 | | `/r/CreatedBy` | 10 |
|
|
70
|
+
| `/r/HasSubevent` | 42 | | `/r/Desires` | 7 |
|
|
71
|
+
| `/r/ReceivesAction` | 40 | | `/r/HasFirstSubevent` | 7 |
|
|
72
|
+
| | | | `/r/LocatedNear` | 1 |
|
|
73
|
+
|
|
74
|
+
Absent from the slice (nothing matched the seed terms): `/r/ObstructedBy`,
|
|
75
|
+
`/r/SymbolOf` — both still have mapping rows, so a regenerated slice that
|
|
76
|
+
surfaces them stays covered.
|
|
77
|
+
|
|
78
|
+
## How to regenerate / extend
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# preferred when api.conceptnet.io is healthy (polite, ~1 req/s, several minutes):
|
|
82
|
+
node corpus/conceptnet/fetch-slice.mjs corpus/conceptnet/slice.jsonl
|
|
83
|
+
|
|
84
|
+
# the dump route (used for the committed slice; ~500 MB streamed, nothing stored):
|
|
85
|
+
curl -s https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-assertions-5.7.0.csv.gz \
|
|
86
|
+
| gunzip -c \
|
|
87
|
+
| node corpus/conceptnet/filter-dump.mjs > corpus/conceptnet/slice.jsonl
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To extend the domain, add seed terms to `SEED_TERMS` in `fetch-slice.mjs` and
|
|
91
|
+
re-run. `npm test` guards the contract: every relation present in the slice
|
|
92
|
+
must have a row in `src/corpus/conceptnet-map.toml` (drift guard), en→en shape
|
|
93
|
+
and the ≤ 1.5 MB budget are asserted, and the seeding path is exercised
|
|
94
|
+
end-to-end.
|
|
95
|
+
|
|
96
|
+
## Consumers
|
|
97
|
+
|
|
98
|
+
- `src/corpus/conceptnet.mjs` — `loadSlice()` / `toFacts()` / `seedMemory()`:
|
|
99
|
+
maps mappable assertions onto memory facts
|
|
100
|
+
(`{subject, predicate, object, provenance:"corpus:conceptnet /r/…"}`)
|
|
101
|
+
and seeds `.tmct/memory/` via `appendFact` (idempotent).
|
|
102
|
+
- `src/corpus/conceptnet-map.toml` — the relation → ACE-OWL pattern table
|
|
103
|
+
deciding which relations emit facts and under which predicate URI.
|