@polycode-projects/the-mechanical-code-talker 0.5.0 → 0.7.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 CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  `@polycode-projects/the-mechanical-code-talker`
4
4
 
5
- A pure-JS, **no-LLM**, offline, **$0** chatbot in the ELIZA/PARRY lineage
5
+ A pure-JS, **no-LLM**, offline, **$0** chatbot in the ELIZA/PARRY lineage:
6
6
  pattern-driven, best-efforts, and obsessed with software the way PARRY was
7
- obsessed with the mafia. No model calls anywhere in the product: interpretation
8
- is mechanical (deterministic language libraries, template sets, committed
9
- corpuses), memory is a graph on disk, and every answer is either grounded or an
10
- honest miss.
7
+ obsessed with the mafia. No model calls anywhere. Interpretation is mechanical
8
+ (deterministic language libraries, template sets, committed corpuses). Memory is
9
+ a graph on disk. Every answer is either grounded or an honest miss.
11
10
 
12
11
  ```
13
12
  $ tmct
@@ -50,6 +49,31 @@ under a guard that proves the protected spans came through byte-for-byte. Today
50
49
  that pass fixes the a/an article defect; broader voice and agreement rules are
51
50
  implemented but parked until they earn their place on the benchmark.
52
51
 
52
+ ## How it guides you
53
+
54
+ When you touch a **concept** without asking a precise question — "what is a
55
+ class", "what about imports", "what calls are there" — tmct answers in three
56
+ bands instead of dead-ending:
57
+
58
+ 1. the **definition** (a plain-English one-liner: *"A class is a template that
59
+ defines the structure and behaviour of objects."* / *"To import is to bring
60
+ another module's definitions into the current one."*);
61
+ 2. **real instances from your graph** — *"In this codebase, for example: Record,
62
+ Task and User (10 classes)"*, or actual edges *"src/core/store.mjs imports
63
+ src/core/model.mjs (18 import edges)"*;
64
+ 3. **guided follow-ups** — two or three concrete next questions, each one
65
+ *pre-checked against your graph* so every suggestion is guaranteed to resolve:
66
+ *"Want to go deeper? Try: which classes inherit from Record / what does Task
67
+ contain / where is User defined"*.
68
+
69
+ It fires for both **noun** concepts (class, module, function, method) and
70
+ **relation** concepts (imports, calls, contains, inherits, tests), and only when
71
+ tmct genuinely knows the concept *and* has instances of it — otherwise the honest
72
+ miss stands. The effect is a conversation that drills down from a vague opener to
73
+ a useful answer without ever hitting a wall. Natural phrasings are routed to the
74
+ capability you meant: *"what functions are in Task"* → its members, *"what
75
+ defined saveStore"* → where it's defined.
76
+
53
77
  ## How it remembers
54
78
 
55
79
  tmct's memory has two layers, both fed by every parsed request and response and
@@ -61,9 +85,12 @@ by cleaned session logs:
61
85
  relevance rather than loaded wholesale.
62
86
 
63
87
  With no graph at all, tmct starts empty and remembers what you tell it — the
64
- `.tmct/` graph is created from the conversation. Committed corpuses seed the
65
- vocabulary; a filtered **ConceptNet slice** (CC-BY-SA 4.0) is planned see
66
- `ROADMAP.md` Phase 2.
88
+ `.tmct/` graph is created from the conversation. On a first run it seeds the
89
+ committed vocabulary so it knows what it's talking about from turn one: a curated
90
+ **SEON** software ontology plus the whole filtered **ConceptNet slice**
91
+ (CC-BY-SA 4.0) — every term carries an English definition, so "what is a cache?"
92
+ answers offline, from disk, on turn one. `--ephemeral` (used by the shipped
93
+ `npm run example:*` demos) reads a graph but writes nothing back.
67
94
 
68
95
  ### Provenance and trust
69
96
 
@@ -125,6 +152,41 @@ or a bare user gets a working install in one command.
125
152
  > Install-size note: tmct depends on wink-nlp's deterministic English language
126
153
  > model (~3.8 MB installed). That model is a lookup table, not an LLM.
127
154
 
155
+ ### Try it on an example graph
156
+
157
+ tmct *consumes* a code graph at `<repo>/.tmct/graph.json` — it does not build
158
+ one. Two ready-made example graphs ship in `examples/` so you can see it answer
159
+ real questions with no setup:
160
+
161
+ ```bash
162
+ npm run example:mini # "Questboard" — a small task-tracker web app (12 modules)
163
+ npm run example:polyglot # one shared OWL vocabulary across Java / Python / C#
164
+ npm run chat:repo -- ./any/path # chat over any repo that has a .tmct/graph.json
165
+ ```
166
+
167
+ Questions the **mini-webapp** graph answers:
168
+
169
+ ```
170
+ what classes are there
171
+ describe Task
172
+ how many modules
173
+ which modules import src/core/model.mjs
174
+ what tests cover src/handlers/tasks.mjs
175
+ ```
176
+
177
+ The **polyglot** graph shows the language-neutral idea — Java, Python and C#
178
+ entities all typed to the same `seon:Class` / `seon:Method` / `seon:Module`
179
+ concepts, so one query reasons across every language at once:
180
+
181
+ ```
182
+ how many classes # 9 — Java + Python + C# counted as one concept
183
+ what classes are there # Order (Java), Inventory (Python), PaymentService (C#), …
184
+ which modules define PaymentService
185
+ ```
186
+
187
+ See `examples/mini-webapp/README.md` and `examples/polyglot/README.md` for the
188
+ full tours.
189
+
128
190
  ### As a library
129
191
 
130
192
  ```js
@@ -138,29 +200,31 @@ extension surface.
138
200
  ## The repository interface
139
201
 
140
202
  tmct is not an indexer, so it consumes a graph through a typed contract any
141
- producer can implement. That contract is now first-class: a **versioned
142
- (1.0.0), OWL-grounded, machine-readable service definition**
143
- (`docs/repository-interface.md` + a JSON schema) of every service, its
144
- arguments, result types, and error contract. A **miss is a value, not a throw**
145
- the interface models "no answer" explicitly. tmct ships **reference providers**
146
- (a fixture graph and the empty/bootstrap graph) that implement every service,
147
- and a **runnable conformance suite**: tmct's own providers pass it in `npm test`,
148
- and any external graph producer (seonix first) runs the *same* suite against its
149
- native implementation to claim conformance. Conformance is the suite, not prose.
150
- The design deliberately inverts the original relationship tmct was lifted out
151
- of seonix; now seonix reorients as a *user* that imports the tmct library and
152
- exposes its graph to tmct as a service, with the LLM agent staying outside tmct
153
- exactly as the no-LLM ethos requires.
203
+ producer can implement. That contract is first-class: a **versioned (1.0.0),
204
+ OWL-grounded, machine-readable service definition** (`docs/repository-interface.md`
205
+ plus a JSON schema) of every service, its arguments, result types, and error
206
+ contract. A **miss is a value, not a throw**: the interface models "no answer"
207
+ explicitly. tmct ships **reference providers** (a fixture graph and the
208
+ empty/bootstrap graph) that implement every service, and a **runnable conformance
209
+ suite**. tmct's own providers pass it in `npm test`. Any external graph producer
210
+ (seonix first) runs the same suite against its native implementation to claim
211
+ conformance. Conformance is the suite, not prose. This inverts the original
212
+ relationship: tmct was lifted out of seonix, and seonix now reorients as a *user*
213
+ that imports the tmct library and exposes its graph to tmct as a service. The LLM
214
+ agent stays outside tmct, as the no-LLM ethos requires.
154
215
 
155
216
  ## Security and supply chain
156
217
 
157
218
  tmct is $0 to run and meant to be trusted offline, so the supply chain is
158
- hardened accordingly: CI runs **SAST and secret detection**, a **nightly
159
- `npm audit` + OSV-Scanner** job watches dependencies, releases are published with
160
- **npm provenance** (`--provenance`), and there is a coordinated-disclosure
161
- `SECURITY.md` policy. The content-address hash is single-sourced in
162
- `src/hash.mjs` so the cross-version-stable fact-id contract has exactly one
163
- definition.
219
+ hardened:
220
+
221
+ - CI runs **SAST and secret detection**.
222
+ - A nightly **`npm audit` + OSV-Scanner** job watches dependencies.
223
+ - Releases are published with **npm provenance** (`--provenance`).
224
+ - A coordinated-disclosure `SECURITY.md` policy covers reports.
225
+
226
+ The content-address hash is single-sourced in `src/hash.mjs`, so the
227
+ cross-version-stable fact-id contract has exactly one definition.
164
228
 
165
229
  ## Provenance
166
230
 
package/ROADMAP.md CHANGED
@@ -550,6 +550,51 @@ Repository Interface library-surface work (Phase 8) settling the extraction boun
550
550
  `PLAN_OSS_ACE_PARSER.md`. Sibling publish-candidates (the bounded-Damerau fuzzy matcher, the
551
551
  PageRank+IDF block ranker) may follow the same path if there is demand.
552
552
 
553
+ ## Phase 10 — Conversational competence & onboarding (0.6.0 → 0.7.0)
554
+
555
+ *(Operator-directed, from live new-user testing.)* Once a graph is loaded the engine is strong;
556
+ the weak surfaces were the FIRST RUN and the VAGUE question. This phase makes the miss graceful,
557
+ the empty state honest, and the vague touch a guided answer — realising item 1's "tolerant, guides
558
+ you toward precision" promise on the conversational surface.
559
+
560
+ - **Onboarding UX (shipped 0.6.0):** the grammar wall moved behind `/help` (a short, tailored miss
561
+ instead); intent lanes for memory/teach ("remember that X"), meta/self ("what is this codebase",
562
+ "what do you know"), routed only when a graph query would miss; empty/degenerate-graph
563
+ orientation that distinguishes CODE STRUCTURE (needs a `.tmct/graph.json` via a producer or
564
+ `--repo`; tmct reads graphs, it does not index code) from VOCABULARY (`tmct init`/bootstrap seeds
565
+ concepts); `TMCT_GRAPH_FILE` honoured by chat; slash-optional commands (`stats`≡`/stats`);
566
+ `/memory` explore hooks; up/down-arrow prompt history in the TUI.
567
+ - **Knowledge (shipped 0.6.0 → 0.7.0):** the curated `corpus/seon` ontology — a software-sense
568
+ definition for EVERY lexicon term, language-neutral (Java/C#/Python `class` → one concept); the
569
+ ConceptNet slice quality-filtered (word-sense noise cut) and regrown to ~40k facts; tier-2
570
+ specialised corpuses (aws/python/java) with `tmct init --corpus`; batched `appendFacts` (one
571
+ write, 419s→2.5s) enabling **seed-all** so a fresh repo knows the whole curated vocabulary.
572
+ - **The concept force (shipped 0.7.0):** a vague touch on a concept X, where tmct knows X and has instances,
573
+ answers in three bands — **the definition** (from `corpus/seon`), **the examples** (real code-graph
574
+ + memory instances of X), and **a soft guided follow-up** ("Want to go deeper?" + 2–3 questions
575
+ built from the real instances × the query shapes valid for that kind, each pre-validated to
576
+ resolve). Applies to NOUN concepts (`what is a class` → define + Base/Widget/Button + drill-ins)
577
+ AND **RELATION concepts** (`what about imports` / `what calls are there` → the verb definition +
578
+ example edges + guided queries), fixing the vague-query dead-ends. Fact rendering is
579
+ de-anthropomorphised (no first-person "i learned:" over-claim — corpus facts read as data +
580
+ provenance; `you told me` stays for operator-asserted facts); listings cap at 32 with a "say
581
+ 'more'" pagination that holds the remainder in session state.
582
+ - **Dead-end routing + read-only demos (shipped 0.7.0):** natural drill-down phrasings are routed
583
+ onto the canonical shapes they mean — `what functions are in X` → members-of-class, `what defined
584
+ X` → where-is-X-defined, a no-context `what about X` → the concept/relation force (the discourse
585
+ continuation still wins when there IS a prior answer). `tmct chat --ephemeral` (and the
586
+ `npm run example:*` demos) reads a graph but writes nothing back, so a checked-in example is never
587
+ dirtied by a demo run.
588
+ - **The dialogue-flow loop (`SKILL_CHAT_PLAYTEST.md`, 0.7.0):** a fast, qualitative tuning loop that
589
+ complements the LLM-judge benchmark — Claude plays a curious user, hunts *dead-ends* (walls,
590
+ "unknown qualifier", phrasing-misses, invited follow-ups the engine can't take), fixes them by
591
+ ROUTING to existing capabilities, replays the same conversations until they flow, freezes them as
592
+ regression transcripts, then ratchets the complexity tier. The drill-down transcript above is its
593
+ first frozen fixture (`test/chatflow-drilldown.test.mjs`).
594
+ - **Measured** by the version-matched benchmark (`CHATBENCH_<version>` per `SKILL_TUNING_CYCLE.md`),
595
+ with new graded cells for the miss / empty-graph / concept-touch surfaces so these become
596
+ regression-protected levers, not one-off polish.
597
+
553
598
  ## Phase LATER — recognized, deferred, not now
554
599
 
555
600
  Features we have deliberately shaped seams for but will not build until the phases above have
package/bin/tmct.mjs CHANGED
@@ -34,12 +34,17 @@ software repository. No model calls; no codebase index of its own.
34
34
  Usage:
35
35
  tmct interactive chat (the headline surface)
36
36
  tmct chat [--repo <abs>] chat over a specific repo's graph
37
+ [--ephemeral] read the graph but write nothing back (demo/read-only)
37
38
  [--plain] force the plain readline shell (the default when
38
39
  stdin/stdout is not a terminal)
39
40
  tmct memory [--repo <abs>] what tmct remembers: facts, utterances, sessions,
40
41
  [--verbose] folded blocks (the /memory chat command, from the shell)
41
42
  tmct init [--force] initialize the current directory for tmct: .tmct/,
42
43
  tmct.toml, tier-1 corpus seed, provenance record
44
+ [--corpus <id>] also seed a tier-2 corpus (aws|python|java) — opt-in,
45
+ offline, $0; init is tier-1-only unless asked
46
+ [--detect] suggest a tier-2 corpus from the repo's manifests
47
+ (pyproject.toml → python, pom.xml → java); never seeds unasked
43
48
  tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
44
49
  [--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
45
50
  bounded, low-trust, retractable entailed facts (never on the chat path)
@@ -263,16 +268,21 @@ async function main() {
263
268
  const rest = process.argv.slice(3);
264
269
  const i = rest.indexOf("--repo");
265
270
  const repoPath = i !== -1 ? rest[i + 1] : undefined;
271
+ // `--ephemeral` (used by the shipped `npm run example:*` scripts): read the
272
+ // target graph but write nothing back — no session folded into the committed
273
+ // code graph, no .tmct/memory dropped under it. A demo you can run repeatedly
274
+ // on a checked-in example without ever dirtying it.
275
+ const ephemeral = rest.includes("--ephemeral");
266
276
  // The shell gate: a real terminal gets the full-screen Ink TUI; `--plain` or a
267
277
  // non-TTY stream (pipes, scripts, the test suite) gets the readline shell. Both
268
278
  // drive the same createSession sink — only the drawing differs.
269
279
  const plain = rest.includes("--plain") || !process.stdin.isTTY || !process.stdout.isTTY;
270
280
  if (plain) {
271
281
  const { runChat } = await import("../src/chat.mjs");
272
- await runChat({ repoPath });
282
+ await runChat({ repoPath, ephemeral });
273
283
  } else {
274
284
  const { runTui } = await import("../src/tui/app.mjs");
275
- await runTui({ repoPath });
285
+ await runTui({ repoPath, ephemeral });
276
286
  }
277
287
  return;
278
288
  }
@@ -295,10 +305,62 @@ async function main() {
295
305
  // `tmct init` — the Repository-Interface onboarding surface: scaffold .tmct/,
296
306
  // write tmct.toml, seed the tier-1 corpus (offline, opt-out via TMCT_NO_SEED),
297
307
  // and record provenance. Idempotent; --force rewrites config + re-records.
308
+ //
309
+ // TIERING POLICY: init is OFFLINE, $0 and TIER-1-ONLY by default. A tier-2
310
+ // domain/language corpus (corpus/tier2/: aws, python, java) is added ONLY when
311
+ // explicitly asked via `--corpus <id>`. The `--detect` auto-detect is a
312
+ // documented STUB: it inspects the repo's manifests (pyproject.toml → python,
313
+ // pom.xml → java) and SUGGESTS the matching corpus, but never seeds it unasked.
298
314
  const rest = process.argv.slice(3);
299
315
  const { initRepo } = await import("../src/init.mjs");
300
316
  const res = await initRepo(process.cwd(), { force: rest.includes("--force") });
301
317
  process.stdout.write(res.message + "\n");
318
+
319
+ const ci = rest.indexOf("--corpus");
320
+ const corpusId = ci !== -1 ? rest[ci + 1] : undefined;
321
+ if (corpusId) {
322
+ // Seed a tier-2 corpus by id from corpus/tier2/ (same slice shape as tier-1;
323
+ // provenance-tagged corpus:tier2:<id>). Idempotent (content-hashed fact ids).
324
+ const { readFile } = await import("node:fs/promises");
325
+ const { join, dirname } = await import("node:path");
326
+ const { seedMemory, TIER2_MANIFEST_FILE } = await import("../src/corpus/conceptnet.mjs");
327
+ let manifest;
328
+ try { manifest = JSON.parse(await readFile(TIER2_MANIFEST_FILE, "utf8")); }
329
+ catch (e) { process.stderr.write(`tmct init: cannot read the tier-2 manifest — ${e?.message || e}\n`); process.exit(1); }
330
+ const entry = (manifest.corpuses || []).find((c) => c.id === corpusId);
331
+ if (!entry) {
332
+ const ids = (manifest.corpuses || []).map((c) => c.id).join(", ");
333
+ process.stderr.write(`tmct init: unknown --corpus "${corpusId}". Available tier-2 corpuses: ${ids}.\n`);
334
+ process.exit(2);
335
+ }
336
+ const slicePath = join(dirname(TIER2_MANIFEST_FILE), entry.file);
337
+ const seeded = await seedMemory(process.cwd(), { slicePath, provenancePrefix: `corpus:tier2:${entry.id}` });
338
+ process.stdout.write(
339
+ `seeded tier-2 corpus "${entry.id}" (${entry.kind}) — ${seeded.appended} fact(s) added`
340
+ + `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${entry.file} (${entry.license}).\n`,
341
+ );
342
+ return;
343
+ }
344
+
345
+ if (rest.includes("--detect")) {
346
+ // AUTO-DETECT STUB (documented, non-seeding): map a build manifest to the
347
+ // tier-2 corpus that fits, and tell the operator how to add it. Kept a stub on
348
+ // purpose — the $0/offline default never expands the corpus without an ask.
349
+ const { access } = await import("node:fs/promises");
350
+ const { join } = await import("node:path");
351
+ const has = (f) => access(join(process.cwd(), f)).then(() => true, () => false);
352
+ const DETECT = [["pyproject.toml", "python"], ["pom.xml", "java"]];
353
+ const found = [];
354
+ for (const [file, id] of DETECT) if (await has(file)) found.push([file, id]);
355
+ if (!found.length) {
356
+ process.stdout.write("no tier-2 corpus auto-detected (looked for pyproject.toml → python, pom.xml → java).\n");
357
+ } else {
358
+ for (const [file, id] of found) {
359
+ process.stdout.write(`detected ${file} — run \`tmct init --corpus ${id}\` to add the ${id} tier-2 corpus (offline, $0).\n`);
360
+ }
361
+ }
362
+ return;
363
+ }
302
364
  return;
303
365
  }
304
366
 
package/corpus/README.md CHANGED
@@ -5,14 +5,40 @@ The corpuses tmct ships so that an **empty** tmct still has a vocabulary
5
5
  in `src/corpus/`. Related committed data lives in `data/` (response templates
6
6
  + the SE phrase book — items 4+7).
7
7
 
8
+ ## The tiering policy (tier-1 / tier-2 / tier-3)
9
+
10
+ tmct's knowledge arrives in three tiers, distinguished by **when** it lands and
11
+ **whether it ships in the npm package**:
12
+
13
+ | Tier | What | Ships in the package? | Lands when | Provenance |
14
+ |---|---|---|---|---|
15
+ | **1 — base** | the general English/tech ConceptNet slice + the response templates + the SE phrasebook — the vocabulary every tmct has out of the box | **yes**, committed here | `tmct init` seeds `.tmct/` from committed data (offline, $0) | `corpus:conceptnet /r/…` |
16
+ | **2 — specialised** | LANGUAGE- or DOMAIN-specific fact sets (`aws`, `python`, `java`, …) so tmct can "expand into a concept for an applicable codebase" | **no** — selected per repo | `tmct init` fetches/generates the chosen tier-2 corpuses into `.tmct/` (Wave-2 wiring) | `corpus:tier2:<id> /r/…` |
17
+ | **3 — learned** | facts tmct writes from the actual conversation / the actual codebase it is pointed at | never committed | at runtime, into `.tmct/memory/` | `chat:…`, `codegraph:…` |
18
+
19
+ **Offline / $0 is the default at every tier.** Tier-1 is committed. Tier-2's
20
+ sample corpuses are *curated* (generated locally by `tier2/generate.mjs`, no
21
+ network); a tier-2 corpus too large to curate by hand may declare a `fetch`
22
+ source (a URL + a `sha256`), but the network is **opt-in only** — nothing
23
+ reaches out unless the operator asks for it. Tier-3 is whatever the user says.
24
+
25
+ **Checksums / integrity.** Every tier-2 corpus carries a `sha256` and a `bytes`
26
+ count in `tier2/manifest.json`; a `fetch`-sourced corpus is checksum-verified on
27
+ download (`generate.mjs` `fetchCorpus()`), so a corrupt or tampered fetch fails
28
+ loudly instead of seeding garbage.
29
+
8
30
  ## What's here
9
31
 
10
32
  | Path | What | Size | Licence |
11
33
  |---|---|---|---|
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`) |
34
+ | `conceptnet/slice.jsonl` | tier-1: filtered English/tech-domain ConceptNet 5.7 slice, quality-filtered (one assertion per line) | ~1.35 MB | **CC-BY-SA 4.0** (see `conceptnet/LICENSE-NOTICE`) |
13
35
  | `conceptnet/fetch-slice.mjs` | regeneration tool — the ConceptNet **API** route (polite, ~1 req/s) | — | MPL-2.0 |
14
36
  | `conceptnet/filter-dump.mjs` | regeneration tool — the ConceptNet **dump** route (produced the committed slice; the API was down) | — | MPL-2.0 |
37
+ | `conceptnet/quality-filter.mjs` | second-pass noise filter (drops sentence-fragment/numeric/opinion rows); produced the committed clean slice | — | MPL-2.0 |
15
38
  | `conceptnet/README.md` | provenance, retrieval date, seed terms, filter rules, row counts | — | — |
39
+ | `tier2/manifest.json` | tier-2: index of specialised corpuses (id, kind, description, source, sha256, size) | — | MPL-2.0 |
40
+ | `tier2/generate.mjs` | tier-2: the curated-corpus generator + manifest writer (+ opt-in network-fetch path) | — | MPL-2.0 |
41
+ | `tier2/{aws,python,java}.jsonl` | tier-2 SAMPLE corpuses — same fact shape as the tier-1 slice, loadable via the same path | ~4 KB each | MPL-2.0 |
16
42
 
17
43
  And alongside (same phase, different directory because it is tmct-original
18
44
  data, not a derived corpus):
@@ -44,9 +70,55 @@ await seedMemory(repoDir, { limit: 500 }); // capped (fast bootstrap)
44
70
  - `ace = "none"` relations (RelatedTo, Synonym, FormOf, …) are deliberately
45
71
  NOT seeded — they are kept in the slice for future lexicon/fuzzy-match use.
46
72
 
73
+ ## Tier-2 specialised corpuses (`tier2/`)
74
+
75
+ A tier-2 corpus is a small, curated, LANGUAGE- or DOMAIN-specific fact set in
76
+ the **exact tier-1 fact shape** — one JSON object per line,
77
+ `{"start":"/c/en/…","rel":"/r/…","end":"/c/en/…","weight":N,"surfaceText":"…"}`,
78
+ with `rel` drawn only from the mapped relations in
79
+ `src/corpus/conceptnet-map.toml`. Because the shape is identical, a tier-2 file
80
+ loads and seeds through the very same `loadSlice()`/`toFacts()` path as the
81
+ tier-1 slice — `tier2/generate.mjs --verify` proves it (each sample loads and
82
+ all its facts seed cleanly, no `ace=none` dead rows).
83
+
84
+ The idea: a Python repo pulls the `python` corpus so tmct knows "a dict is a
85
+ kind of hash table"; an AWS project pulls `aws` so it knows "S3 is object
86
+ storage, a bucket is part of S3". Language terms unify onto the **shared CS
87
+ concept vocabulary** (`list → array`, `hashmap → hash table`) so specialised
88
+ knowledge connects to the tier-1 graph instead of floating apart.
89
+
90
+ **To add a corpus:** add an entry to `CORPUSES` in `tier2/generate.mjs` (a list
91
+ of `[subject, relation, concept]` triples) and run `node tier2/generate.mjs
92
+ --verify`. It rewrites `<id>.jsonl` and `manifest.json` (facts count, byte size,
93
+ sha256) in one deterministic pass. Curated data is authored in that file so it
94
+ stays reviewable; a corpus too big to hand-curate is a `fetch` manifest entry
95
+ (URL + sha256, opt-in network — `fetchCorpus()` is the reference downloader).
96
+
97
+ **How the coordinator should wire tier-2 into `tmct init` (Wave-2 — NOT done
98
+ here):**
99
+
100
+ 1. Add a tier-2-aware seeder next to `seedMemory()` (e.g. `seedTier2(dir, id)`)
101
+ that reads `corpus/tier2/manifest.json`, resolves the requested corpus's
102
+ `file` (curated → already on disk; `fetch` → download + `sha256`-verify
103
+ first, only when network is explicitly enabled), then runs the SAME
104
+ `loadSlice → toFacts → appendFact` pipeline — **but stamps provenance
105
+ `corpus:tier2:<id> <rel>`** instead of the hard-coded `corpus:conceptnet …`
106
+ string in `toFacts()`. (Simplest: give `toFacts` an optional
107
+ `provenancePrefix` argument, default `"corpus:conceptnet"`.)
108
+ 2. Give `tmct init` a `--corpus <id>[,<id>…]` flag (and/or codebase
109
+ auto-detection: a `requirements.txt`/`pyproject.toml` → `python`, a
110
+ `pom.xml`/`build.gradle` → `java`, an AWS SDK dep / `serverless.yml` →
111
+ `aws`). Default stays tier-1-only, offline, $0.
112
+ 3. Idempotency is free — `seedMemory`'s content-hashed fact ids and pre-read
113
+ skip already handle re-seeds; tier-2 rides the same path.
114
+
115
+ None of that touches `package.json` or the tier-1 budget; tier-2 files are not
116
+ shipped, so they do not count against the ≤ 1.5 MB slice budget.
117
+
47
118
  ## How to regenerate / extend
48
119
 
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.
120
+ See `conceptnet/README.md` — one command per route (API vs dump), the
121
+ quality-filter pass, plus the seed-term list to extend. The test suite
122
+ (`test/corpus-conceptnet.test.mjs`, `test/corpus-templates.test.mjs`) guards the
123
+ contracts: slice/mapping drift, en→en shape, the ≤ 1.5 MB budget, template
124
+ ids/slots, and end-to-end seeding.
@@ -15,7 +15,9 @@ see `LICENSE-NOTICE` in this directory for the full attribution.
15
15
  - **Source:** ConceptNet 5.7.0 assertions dump,
16
16
  `https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-assertions-5.7.0.csv.gz`
17
17
  (published 2019-07-03).
18
- - **Retrieved + filtered:** 2026-07-04.
18
+ - **Retrieved + filtered:** 2026-07-04; **regrown to the ~40k tier-1 target on
19
+ 2026-07-05** (same dump, same licence) by widening the tech-seed domain and
20
+ raising the size budget — see "Growing the slice" below.
19
21
  - **Why the dump, not the API:** the public API (`api.conceptnet.io`) was
20
22
  hard-down (HTTP 502 from its nginx front-end on every request across ~15
21
23
  attempts over 10+ minutes on 2026-07-04), so the slice was stream-filtered
@@ -33,12 +35,16 @@ see `LICENSE-NOTICE` in this directory for the full attribution.
33
35
  policy: `/r/EtymologicallyRelatedTo`, `/r/EtymologicallyDerivedFrom`,
34
36
  `/r/ExternalURL` (etymology noise and link-outs — no consumer in tmct).
35
37
  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
38
+ tech seed list the ~90-term base `SEED_TERMS` exported from
37
39
  `fetch-slice.mjs` (software, computer, program, code, module, function,
38
40
  database, server, network, bug, test, file, memory, algorithm, keyboard,
39
- programmer, repository, commit, …).
41
+ programmer, repository, commit, …) **plus** the ~230-term `EXTRA_SEEDS`
42
+ growth list in `filter-dump.mjs` (programming languages, frameworks, data
43
+ structures, cloud/infra, protocols, tools, ML — python, java, docker,
44
+ kubernetes, git, neural_network, tcp, kernel, hashtable, …), added
45
+ 2026-07-05 to reach the ~40k tier-1 target while staying in the tech domain.
40
46
  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**:
47
+ 5. **Size budget** (committed slice ≤ 5 MB; target ~4.3 MB), **two-tier**:
42
48
  assertions whose relation maps to an ACE-OWL pattern (`ace != "none"` in
43
49
  `conceptnet-map.toml`) are kept first, weight-descending; `ace = "none"`
44
50
  relations (`RelatedTo`, `Synonym`, …) fill the remaining budget — they
@@ -46,34 +52,86 @@ see `LICENSE-NOTICE` in this directory for the full attribution.
46
52
  facts.
47
53
  6. Deterministic output order: `(rel, start, end)`.
48
54
 
49
- ## Row counts (committed slice, 2026-07-04)
55
+ ## Quality-filter pass (regrown slice, 2026-07-05)
56
+
57
+ `filter-dump.mjs` keeps the DATA honest (tech-seed match, canonical relations,
58
+ budget) but not the SEMANTICS: ConceptNet's crowd-sourced "Verbosity"/Open-Mind
59
+ rows leave sentence-fragment "concepts" and opinion axioms that read as nonsense
60
+ once seeded ("a computer is a kind of dumb", "a class is a kind of elegance",
61
+ "mouse AtLocation taloned_grip_of_owl", "2 is a kind of software"). A second
62
+ pass, `quality-filter.mjs`, removes those by term/relation shape (never per
63
+ row):
64
+
65
+ - **numeric endpoint** — bare term all digits (`2`, `1000`, `80386`)
66
+ - **single-char endpoint** — bare term length ≤ 1 (`a`, `r`, `m`)
67
+ - **sentence fragment** — ≥ 4 underscore-words on either endpoint
68
+ (`taloned_grip_of_owl`, `worlds_largest_interconnected_network_of_networks`)
69
+ - **definitional phrase** — `/r/DefinedAs` with a ≥ 3-word object (real
70
+ `DefinedAs` is a synonym: `cpu → processor`)
71
+ - **opinion object** — `/r/IsA` / `/r/DefinedAs` whose object is in a small
72
+ evidence-based set (`elegance, evil, gloom, unreality, universalism, dumb,
73
+ free, junk`) — never a class
74
+
75
+ **Result (2026-07-05 regrow): 45,633 → 44,947 rows (686 cut), 4,308,850 →
76
+ 4,220,629 bytes.** Cuts by reason: sentence-fragment 613, single-char 48,
77
+ numeric 11, opinion-object 8, definitional-phrase 6. Of the seedable (mapped,
78
+ `ace≠none`) facts, the clean slice carries **6,255** — up from 3,884 in the
79
+ 1.35 MB slice. No relation disappeared entirely (the drift guard stays
80
+ satisfied). Re-run any time with:
50
81
 
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:
82
+ ```bash
83
+ node corpus/conceptnet/quality-filter.mjs --in-place corpus/conceptnet/slice.jsonl
84
+ ```
85
+
86
+ ## Growing the slice toward the ~40k tier-1 target (done 2026-07-05)
87
+
88
+ The operator's ~40k-fact tier-1 target is **shipped**: the slice was regrown
89
+ from the same ConceptNet dump by (a) widening the tech domain with the
90
+ `EXTRA_SEEDS` list in `filter-dump.mjs` (~230 tech terms — languages,
91
+ frameworks, data structures, cloud/infra, protocols, tools, ML) and (b) raising
92
+ `MAX_BYTES` in `filter-dump.mjs` from 1.4 MB to 4.5 MB with the matching test
93
+ budget assertion (`test/corpus-conceptnet.test.mjs`) raised from 1.5 MB to
94
+ 5 MB. The widened seed set matched **45,633 unique en→en assertions** (all under
95
+ budget, so no tier-trimming was needed this pass), and the quality filter
96
+ trimmed them to the committed **44,947 clean facts**. Regenerate with:
97
+
98
+ ```bash
99
+ # 1. stream-filter the dump (widened seeds + raised budget already live in filter-dump.mjs)
100
+ curl -s https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-assertions-5.7.0.csv.gz \
101
+ | gunzip -c | node corpus/conceptnet/filter-dump.mjs > corpus/conceptnet/slice.jsonl
102
+ # 2. re-apply the semantic quality filter
103
+ node corpus/conceptnet/quality-filter.mjs --in-place corpus/conceptnet/slice.jsonl
104
+ ```
105
+
106
+ ## Row counts (regrown slice, 2026-07-05)
107
+
108
+ **44,947 clean assertions, 4,220,629 bytes** — the quality-filtered committed
109
+ slice (raw filter-dump output was 45,633 rows / 4,308,850 bytes; 34,074,917 dump
110
+ lines scanned; 45,633 unique en→en seed assertions matched = 6,670 mappable +
111
+ 38,963 `ace="none"`; ALL kept under the 4.5 MB budget, then 686 noise rows cut).
112
+ 30 of the 31 non-filtered canonical relations are present; **6,255 seedable
113
+ (`ace≠none`) facts**:
55
114
 
56
115
  | Relation | Rows | | Relation | Rows |
57
116
  |---|---|---|---|---|
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.
117
+ | `/r/RelatedTo` | 29016 | | `/r/HasSubevent` | 42 |
118
+ | `/r/HasContext` | 4376 | | `/r/DistinctFrom` | 32 |
119
+ | `/r/IsA` | 4173 | | `/r/HasProperty` | 30 |
120
+ | `/r/DerivedFrom` | 3388 | | `/r/ReceivesAction` | 27 |
121
+ | `/r/Synonym` | 1126 | | `/r/MotivatedByGoal` | 26 |
122
+ | `/r/AtLocation` | 642 | | `/r/MadeOf` | 20 |
123
+ | `/r/FormOf` | 528 | | `/r/Causes` | 18 |
124
+ | `/r/UsedFor` | 378 | | `/r/CreatedBy` | 15 |
125
+ | `/r/CapableOf` | 231 | | `/r/CausesDesire` | 13 |
126
+ | `/r/MannerOf` | 230 | | `/r/Desires` | 9 |
127
+ | `/r/PartOf` | 196 | | `/r/HasLastSubevent` | 8 |
128
+ | `/r/Antonym` | 155 | | `/r/HasFirstSubevent` | 7 |
129
+ | `/r/HasPrerequisite` | 108 | | `/r/LocatedNear` | 3 |
130
+ | `/r/SimilarTo` | 102 | | `/r/DefinedAs` | 2 |
131
+ | `/r/HasA` | 45 | | `/r/SymbolOf` | 1 |
132
+
133
+ Absent from the slice (nothing matched the seed terms): `/r/ObstructedBy` — it
134
+ still has a mapping row, so a regenerated slice that surfaces it stays covered.
77
135
 
78
136
  ## How to regenerate / extend
79
137
 
@@ -87,11 +145,12 @@ curl -s https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-asse
87
145
  | node corpus/conceptnet/filter-dump.mjs > corpus/conceptnet/slice.jsonl
88
146
  ```
89
147
 
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.
148
+ To extend the domain, add seed terms to `EXTRA_SEEDS` in `filter-dump.mjs` (the
149
+ dump route that produces the committed slice) or `SEED_TERMS` in
150
+ `fetch-slice.mjs` (the API route) and re-run. `npm test` guards the contract:
151
+ every relation present in the slice must have a row in
152
+ `src/corpus/conceptnet-map.toml` (drift guard), en→en shape and the ≤ 5 MB
153
+ budget are asserted, and the seeding path is exercised end-to-end.
95
154
 
96
155
  ## Consumers
97
156