@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.
Files changed (53) hide show
  1. package/README.md +77 -3
  2. package/ROADMAP.md +416 -3
  3. package/bin/tmct.mjs +308 -12
  4. package/corpus/README.md +52 -0
  5. package/corpus/conceptnet/LICENSE-NOTICE +37 -0
  6. package/corpus/conceptnet/README.md +103 -0
  7. package/corpus/conceptnet/fetch-slice.mjs +136 -0
  8. package/corpus/conceptnet/filter-dump.mjs +89 -0
  9. package/corpus/conceptnet/slice.jsonl +14258 -0
  10. package/data/phrasebook/software-phrases.txt +231 -0
  11. package/data/templates/grammar-rules.toml +89 -0
  12. package/data/templates/responses.jsonl +68 -0
  13. package/package.json +40 -3
  14. package/src/ask-nlp.mjs +22 -10
  15. package/src/ask-vocab.mjs +35 -1
  16. package/src/ask.mjs +171 -494
  17. package/src/chat.mjs +709 -81
  18. package/src/corpus/conceptnet-map.toml +251 -0
  19. package/src/corpus/conceptnet.mjs +167 -0
  20. package/src/corpus/templates.mjs +188 -0
  21. package/src/finish.mjs +443 -0
  22. package/src/grammar/ace.mjs +341 -0
  23. package/src/grammar/assert.mjs +40 -0
  24. package/src/grammar/lexicon-core.json +287 -0
  25. package/src/grammar/lexicon.mjs +202 -0
  26. package/src/hash.mjs +32 -0
  27. package/src/index.mjs +21 -5
  28. package/src/init.mjs +264 -0
  29. package/src/interpret/fuzzy.mjs +89 -0
  30. package/src/interpret/merge.mjs +148 -0
  31. package/src/interpret/normalize.mjs +151 -0
  32. package/src/interpret/pipeline.mjs +112 -0
  33. package/src/interpret/strategies/grammar.mjs +137 -0
  34. package/src/interpret/strategies/keywords.mjs +241 -0
  35. package/src/interpret/strategies/noise-strip.mjs +114 -0
  36. package/src/memory/blocks.mjs +221 -0
  37. package/src/memory/core.mjs +533 -0
  38. package/src/memory/fold.mjs +0 -0
  39. package/src/memory/inspect.mjs +141 -0
  40. package/src/memory/trust.mjs +113 -0
  41. package/src/prose-nlp.mjs +14 -16
  42. package/src/providers/bootstrap.mjs +24 -0
  43. package/src/providers/fixture.mjs +118 -0
  44. package/src/providers/graph-service.mjs +312 -0
  45. package/src/repository-interface.mjs +318 -0
  46. package/src/server.mjs +44 -28
  47. package/src/sessions.mjs +137 -4
  48. package/src/source.mjs +44 -5
  49. package/src/syllogise.mjs +0 -0
  50. package/src/toml-config.mjs +14 -0
  51. package/src/tui/app.mjs +173 -0
  52. package/src/wink-model.mjs +74 -0
  53. 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>] → same, explicit
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. 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.
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 args = process.argv.slice(2);
54
+ const argv = process.argv.slice(2);
33
55
 
34
- if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
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 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) {
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
- // Delegate to the carried dispatcher. It runs its own main() on import.
47
- await import("./cli.mjs");
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
+ });
@@ -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.