@polycode-projects/the-mechanical-code-talker 0.4.0 → 0.6.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
@@ -37,7 +36,7 @@ fit the grammar still gets the tolerant strategies; nothing is rejected for
37
36
  being loose, fuzzy, or misspelled.
38
37
 
39
38
  **Negation and passive.** "Which modules do *not* import X?" computes a bounded
40
- **set complement** over the graph and an honestly empty result stays a miss
39
+ **set complement** over the graph, and an honestly empty result stays a miss
41
40
  rather than a fabricated list (a non-enumerable type, like arbitrary *changes*,
42
41
  is refused outright). Reversible-passive questions traverse the right direction:
43
42
  "what is imported by Y" and "what does Y import" are understood as opposite
@@ -73,8 +72,8 @@ web scrape, a rule-derived entailment — and a fact links back to *all* of them
73
72
  (`mgx:derivedFrom` / `mgx:statedBy` / `mgx:canonicalisedFrom`), timestamped with
74
73
  `mgx:createdAt`. From those links tmct computes a **deterministic, explainable
75
74
  trust score** — a source-type prior combined with corroboration (how many
76
- independent sources agree) and recency never hand-set, always traceable to its
77
- inputs. Retrieval then ranks by **relevance × trust**, so a corroborated,
75
+ independent sources agree) and recency. It is never hand-set, always traceable
76
+ to its inputs. Retrieval then ranks by **relevance × trust**, so a corroborated,
78
77
  operator-stated fact outranks a lone web scrape on the same question. When two
79
78
  trusted sources *disagree*, the `/memory` inspector shows **both sides with their
80
79
  provenance** rather than silently picking a winner.
@@ -95,7 +94,7 @@ on the chat's hot path.
95
94
  a code graph is out of scope. tmct's job is the *conversation*.
96
95
  - **It is not a reasoning model.** Where it "reasons", it does so by
97
96
  *calculation* surfaced as prose ("there are a lot of tests for a codebase of
98
- that size") deterministic, explainable, cheap. Even its forward-chaining
97
+ that size"). It is deterministic, explainable, and cheap. Even its forward-chaining
99
98
  entailment (`tmct syllogise`) is mechanical OWL rule materialization applied
100
99
  offline, rule-by-rule and retractable — not an LLM. There is **no LLM anywhere
101
100
  in the product**. (An LLM-as-judge exists only in the offline eval harness
@@ -125,6 +124,41 @@ or a bare user gets a working install in one command.
125
124
  > Install-size note: tmct depends on wink-nlp's deterministic English language
126
125
  > model (~3.8 MB installed). That model is a lookup table, not an LLM.
127
126
 
127
+ ### Try it on an example graph
128
+
129
+ tmct *consumes* a code graph at `<repo>/.tmct/graph.json` — it does not build
130
+ one. Two ready-made example graphs ship in `examples/` so you can see it answer
131
+ real questions with no setup:
132
+
133
+ ```bash
134
+ npm run example:mini # "Questboard" — a small task-tracker web app (12 modules)
135
+ npm run example:polyglot # one shared OWL vocabulary across Java / Python / C#
136
+ npm run chat:repo -- ./any/path # chat over any repo that has a .tmct/graph.json
137
+ ```
138
+
139
+ Questions the **mini-webapp** graph answers:
140
+
141
+ ```
142
+ what classes are there
143
+ describe Task
144
+ how many modules
145
+ which modules import src/core/model.mjs
146
+ what tests cover src/handlers/tasks.mjs
147
+ ```
148
+
149
+ The **polyglot** graph shows the language-neutral idea — Java, Python and C#
150
+ entities all typed to the same `seon:Class` / `seon:Method` / `seon:Module`
151
+ concepts, so one query reasons across every language at once:
152
+
153
+ ```
154
+ how many classes # 9 — Java + Python + C# counted as one concept
155
+ what classes are there # Order (Java), Inventory (Python), PaymentService (C#), …
156
+ which modules define PaymentService
157
+ ```
158
+
159
+ See `examples/mini-webapp/README.md` and `examples/polyglot/README.md` for the
160
+ full tours.
161
+
128
162
  ### As a library
129
163
 
130
164
  ```js
@@ -138,29 +172,31 @@ extension surface.
138
172
  ## The repository interface
139
173
 
140
174
  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.
175
+ producer can implement. That contract is first-class: a **versioned (1.0.0),
176
+ OWL-grounded, machine-readable service definition** (`docs/repository-interface.md`
177
+ plus a JSON schema) of every service, its arguments, result types, and error
178
+ contract. A **miss is a value, not a throw**: the interface models "no answer"
179
+ explicitly. tmct ships **reference providers** (a fixture graph and the
180
+ empty/bootstrap graph) that implement every service, and a **runnable conformance
181
+ suite**. tmct's own providers pass it in `npm test`. Any external graph producer
182
+ (seonix first) runs the same suite against its native implementation to claim
183
+ conformance. Conformance is the suite, not prose. This inverts the original
184
+ relationship: tmct was lifted out of seonix, and seonix now reorients as a *user*
185
+ that imports the tmct library and exposes its graph to tmct as a service. The LLM
186
+ agent stays outside tmct, as the no-LLM ethos requires.
154
187
 
155
188
  ## Security and supply chain
156
189
 
157
190
  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.
191
+ hardened:
192
+
193
+ - CI runs **SAST and secret detection**.
194
+ - A nightly **`npm audit` + OSV-Scanner** job watches dependencies.
195
+ - Releases are published with **npm provenance** (`--provenance`).
196
+ - A coordinated-disclosure `SECURITY.md` policy covers reports.
197
+
198
+ The content-address hash is single-sourced in `src/hash.mjs`, so the
199
+ cross-version-stable fact-id contract has exactly one definition.
164
200
 
165
201
  ## Provenance
166
202
 
package/ROADMAP.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  tmct v0.1.0 was a **whole-package lift** of the seonix chat surface (published
4
4
  as `@polycode-projects/mct`): identical shape, green tests, new branding. That
5
- was deliberate it gave every ambition below a working, tested starting point
5
+ was deliberate. It gave every ambition below a working, tested starting point
6
6
  instead of a green field. v0.2.0 is the **reshape**: the lift's LLM fallback,
7
7
  extraction stack, and MCP server are gone, and the package, naming, and license
8
8
  now match the product this document describes.
@@ -228,7 +228,7 @@ then use Progol to theorem-prove against parsed prose".)*
228
228
 
229
229
  ## Phase 4 — The wiring wave (operator-directed 2026-07-04)
230
230
 
231
- Five subsystems are built, tested, and consumed by NOTHING in the answer path they measured
231
+ Five subsystems are built, tested, and consumed by NOTHING in the answer path. They measured
232
232
  zero on case-set v1 because no case could see them. This phase wires each into answering, with
233
233
  **unit tests at the seam AND graded-benchmark cells that measure it** (the graded pool creates
234
234
  the cases that make each lever visible). Wired as one operator-directed wave; cycle-level
@@ -406,7 +406,7 @@ answer EXISTS as a stable phrasing in technical prose is not a ceiling — it is
406
406
  > Detailed plan: **`PLAN_RESPONSE_FINISHING.md`**.
407
407
 
408
408
  *(Refined 2026-07-05; decisions settled with the operator. Fact invariance is achieved by
409
- CONSTRUCTION, not by hope finishing operates over a SEGMENTED answer, never a raw string.
409
+ CONSTRUCTION, not by hope. Finishing operates over a SEGMENTED answer, never a raw string.
410
410
  Tone-of-voice synonym substitution is DROPPED: once every term with technical significance is
411
411
  protected — entities, paths, vocabulary, receipts, provenance — the substitutable surface is
412
412
  mostly connectives: high accuracy risk, thin reward. "Keen on the trickery to make a helpful
@@ -466,7 +466,7 @@ a passive payload loader into the product's primary integration surface.)*
466
466
  - **tmct defines the adapter shape** — not the producer. Rationale: tmct is the brittle side
467
467
  (query interpretation), so it must own and optimize around a STABLE interface; because the
468
468
  vocabulary is OWL-grounded, the human/code world is already quantized into types both sides
469
- understand the interface is built from those shared types, not ad-hoc JSON.
469
+ understand, so the interface is built from those shared types, not ad-hoc JSON.
470
470
  - **A rich instruction set, translated from what seonix already exposes**: survey seonix's
471
471
  native tool surface (describe / members / subclasses / impact / callers / callees / tests-for /
472
472
  untested / history / exports / architecture / search / context / snippet / locate / digest —
@@ -521,7 +521,7 @@ exactly when pre-deriving the useful closure pays off). Never on the chat's hot
521
521
 
522
522
  **The selection criterion, sharpened by the operator (2026-07-05):** the guiding question is
523
523
  *"what do the assertions of the sources I TRUST allow me to infer about this topic that is of
524
- RELEVANCE"* — so **novelty × trust is the primary driver**, not an afterthought: the pass walks
524
+ RELEVANCE"* — so **novelty × trust is the primary driver**: the pass walks
525
525
  outward from high-trust premises (the provenance primitive) toward novel, relevant conclusions,
526
526
  timestamping each so recency and novelty stay computable. The mechanics are the easy half
527
527
  (bounded forward chaining materializes entailments; backward chaining from frequent query shapes
@@ -530,7 +530,7 @@ realization — unsolved in the general case and not pretended otherwise;** trus
530
530
  are the tractable approximation, not a solution. The plan's job is
531
531
  to make it TRACTABLE in tmct's narrow, closed world, not to solve it: usefulness is approximated
532
532
  from what the system actually gets asked (query-shape frequency), what connects to recent focus,
533
- what a cheap forward step yields that isn't already stored, and — the honest guardrail — a hard
533
+ what a cheap forward step yields that isn't already stored, and a hard
534
534
  budget (inference is bounded, its output trust-scored via the provenance primitive, and anything
535
535
  speculative is retractable and never outranks a stated fact). Everything else is deferred to the
536
536
  plan's open questions, where the relevance problem is named as the open research risk it is.
package/bin/tmct.mjs CHANGED
@@ -40,6 +40,10 @@ Usage:
40
40
  [--verbose] folded blocks (the /memory chat command, from the shell)
41
41
  tmct init [--force] initialize the current directory for tmct: .tmct/,
42
42
  tmct.toml, tier-1 corpus seed, provenance record
43
+ [--corpus <id>] also seed a tier-2 corpus (aws|python|java) — opt-in,
44
+ offline, $0; init is tier-1-only unless asked
45
+ [--detect] suggest a tier-2 corpus from the repo's manifests
46
+ (pyproject.toml → python, pom.xml → java); never seeds unasked
43
47
  tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
44
48
  [--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
45
49
  bounded, low-trust, retractable entailed facts (never on the chat path)
@@ -295,10 +299,62 @@ async function main() {
295
299
  // `tmct init` — the Repository-Interface onboarding surface: scaffold .tmct/,
296
300
  // write tmct.toml, seed the tier-1 corpus (offline, opt-out via TMCT_NO_SEED),
297
301
  // and record provenance. Idempotent; --force rewrites config + re-records.
302
+ //
303
+ // TIERING POLICY: init is OFFLINE, $0 and TIER-1-ONLY by default. A tier-2
304
+ // domain/language corpus (corpus/tier2/: aws, python, java) is added ONLY when
305
+ // explicitly asked via `--corpus <id>`. The `--detect` auto-detect is a
306
+ // documented STUB: it inspects the repo's manifests (pyproject.toml → python,
307
+ // pom.xml → java) and SUGGESTS the matching corpus, but never seeds it unasked.
298
308
  const rest = process.argv.slice(3);
299
309
  const { initRepo } = await import("../src/init.mjs");
300
310
  const res = await initRepo(process.cwd(), { force: rest.includes("--force") });
301
311
  process.stdout.write(res.message + "\n");
312
+
313
+ const ci = rest.indexOf("--corpus");
314
+ const corpusId = ci !== -1 ? rest[ci + 1] : undefined;
315
+ if (corpusId) {
316
+ // Seed a tier-2 corpus by id from corpus/tier2/ (same slice shape as tier-1;
317
+ // provenance-tagged corpus:tier2:<id>). Idempotent (content-hashed fact ids).
318
+ const { readFile } = await import("node:fs/promises");
319
+ const { join, dirname } = await import("node:path");
320
+ const { seedMemory, TIER2_MANIFEST_FILE } = await import("../src/corpus/conceptnet.mjs");
321
+ let manifest;
322
+ try { manifest = JSON.parse(await readFile(TIER2_MANIFEST_FILE, "utf8")); }
323
+ catch (e) { process.stderr.write(`tmct init: cannot read the tier-2 manifest — ${e?.message || e}\n`); process.exit(1); }
324
+ const entry = (manifest.corpuses || []).find((c) => c.id === corpusId);
325
+ if (!entry) {
326
+ const ids = (manifest.corpuses || []).map((c) => c.id).join(", ");
327
+ process.stderr.write(`tmct init: unknown --corpus "${corpusId}". Available tier-2 corpuses: ${ids}.\n`);
328
+ process.exit(2);
329
+ }
330
+ const slicePath = join(dirname(TIER2_MANIFEST_FILE), entry.file);
331
+ const seeded = await seedMemory(process.cwd(), { slicePath, provenancePrefix: `corpus:tier2:${entry.id}` });
332
+ process.stdout.write(
333
+ `seeded tier-2 corpus "${entry.id}" (${entry.kind}) — ${seeded.appended} fact(s) added`
334
+ + `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${entry.file} (${entry.license}).\n`,
335
+ );
336
+ return;
337
+ }
338
+
339
+ if (rest.includes("--detect")) {
340
+ // AUTO-DETECT STUB (documented, non-seeding): map a build manifest to the
341
+ // tier-2 corpus that fits, and tell the operator how to add it. Kept a stub on
342
+ // purpose — the $0/offline default never expands the corpus without an ask.
343
+ const { access } = await import("node:fs/promises");
344
+ const { join } = await import("node:path");
345
+ const has = (f) => access(join(process.cwd(), f)).then(() => true, () => false);
346
+ const DETECT = [["pyproject.toml", "python"], ["pom.xml", "java"]];
347
+ const found = [];
348
+ for (const [file, id] of DETECT) if (await has(file)) found.push([file, id]);
349
+ if (!found.length) {
350
+ process.stdout.write("no tier-2 corpus auto-detected (looked for pyproject.toml → python, pom.xml → java).\n");
351
+ } else {
352
+ for (const [file, id] of found) {
353
+ process.stdout.write(`detected ${file} — run \`tmct init --corpus ${id}\` to add the ${id} tier-2 corpus (offline, $0).\n`);
354
+ }
355
+ }
356
+ return;
357
+ }
302
358
  return;
303
359
  }
304
360
 
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.
@@ -46,9 +46,61 @@ see `LICENSE-NOTICE` in this directory for the full attribution.
46
46
  facts.
47
47
  6. Deterministic output order: `(rel, start, end)`.
48
48
 
49
- ## Row counts (committed slice, 2026-07-04)
49
+ ## Quality-filter pass (2026-07-05)
50
+
51
+ `filter-dump.mjs` keeps the DATA honest (tech-seed match, canonical relations,
52
+ budget) but not the SEMANTICS: ConceptNet's crowd-sourced "Verbosity"/Open-Mind
53
+ rows leave sentence-fragment "concepts" and opinion axioms that read as nonsense
54
+ once seeded ("a computer is a kind of dumb", "a class is a kind of elegance",
55
+ "mouse AtLocation taloned_grip_of_owl", "2 is a kind of software"). A second
56
+ pass, `quality-filter.mjs`, removes those by term/relation shape (never per
57
+ row):
58
+
59
+ - **numeric endpoint** — bare term all digits (`2`, `1000`, `80386`)
60
+ - **single-char endpoint** — bare term length ≤ 1 (`a`, `r`, `m`)
61
+ - **sentence fragment** — ≥ 4 underscore-words on either endpoint
62
+ (`taloned_grip_of_owl`, `worlds_largest_interconnected_network_of_networks`)
63
+ - **definitional phrase** — `/r/DefinedAs` with a ≥ 3-word object (real
64
+ `DefinedAs` is a synonym: `cpu → processor`)
65
+ - **opinion object** — `/r/IsA` / `/r/DefinedAs` whose object is in a small
66
+ evidence-based set (`elegance, evil, gloom, unreality, universalism, dumb,
67
+ free, junk`) — never a class
68
+
69
+ **Result: 14,258 → 13,880 rows (378 cut), 1,399,979 → 1,348,361 bytes.** Cuts by
70
+ reason: sentence-fragment 328, single-char 27, numeric 11, opinion-object 8,
71
+ definitional-phrase 4. Of the 4,170 seedable (mapped, `ace≠none`) facts, 286
72
+ noise facts were removed, leaving **3,884 clean seedable facts**. No relation
73
+ disappeared entirely (the drift guard stays satisfied). Re-run any time with:
50
74
 
51
- **14,258 assertions, 1,399,979 bytes** (34,074,917 dump lines scanned;
75
+ ```bash
76
+ node corpus/conceptnet/quality-filter.mjs --in-place corpus/conceptnet/slice.jsonl
77
+ ```
78
+
79
+ ## Growing the slice toward ~40k facts — the budget blocker
80
+
81
+ Growing tier-1 to the operator's ~40k-fact target is **feasible data-wise but
82
+ blocked by the committed budget**: the ConceptNet dump S3 endpoint is reachable,
83
+ but `test/corpus-conceptnet.test.mjs` asserts `size <= 1_500_000`, and the clean
84
+ slice is already 1.35 MB (13,880 rows). 40k facts is ≈ 4 MB — 2.6× over the cap.
85
+ Reaching it needs a **Wave-2 policy change** (raise `MAX_BYTES` in
86
+ `filter-dump.mjs` AND the budget assertion in the test), which is out of scope
87
+ for a data-only pass. When the budget is raised, regrow with a bigger seed list
88
+ + budget, then re-run the quality filter:
89
+
90
+ ```bash
91
+ # 1. widen the domain and the budget (edit SEED_TERMS in fetch-slice.mjs, MAX_BYTES in filter-dump.mjs)
92
+ curl -s https://s3.amazonaws.com/conceptnet/downloads/2019/edges/conceptnet-assertions-5.7.0.csv.gz \
93
+ | gunzip -c | node corpus/conceptnet/filter-dump.mjs > corpus/conceptnet/slice.jsonl
94
+ # 2. re-apply the semantic quality filter
95
+ node corpus/conceptnet/quality-filter.mjs --in-place corpus/conceptnet/slice.jsonl
96
+ # 3. raise the test's budget assertion to match
97
+ ```
98
+
99
+ ## Row counts (pre-filter baseline, 2026-07-04)
100
+
101
+ **14,258 assertions, 1,399,979 bytes** — the raw filter-dump output, BEFORE the
102
+ 2026-07-05 quality-filter pass above trimmed it to 13,880 rows / 1,348,361 bytes
103
+ (34,074,917 dump lines scanned;
52
104
  28,802 unique en→en seed assertions matched = 4,170 mappable + 24,632
53
105
  `ace="none"`; ALL 4,170 mappable kept, 10,088 none-rows fill the budget).
54
106
  29 of the 31 non-filtered canonical relations are present:
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // quality-filter.mjs — a SECOND-PASS noise filter over corpus/conceptnet/slice.jsonl.
3
+ // NOT part of the product path — a maintainer tool, run by hand, result committed.
4
+ //
5
+ // The committed slice is stream-filtered from the ConceptNet dump by
6
+ // filter-dump.mjs (tech-domain seed match + canonical relations + budget). That
7
+ // pass keeps the DATA honest but not the SEMANTICS: ConceptNet's crowd-sourced
8
+ // "Verbosity"/Open-Mind rows leave sentence-fragment "concepts" and opinion
9
+ // axioms in the slice that read as nonsense once they become memory facts —
10
+ // e.g. "a computer is a kind of dumb", "a class is a kind of elegance",
11
+ // "mouse AtLocation taloned_grip_of_owl", "2 is a kind of software".
12
+ //
13
+ // This pass removes exactly those, by term/relation shape only (never by hand
14
+ // per row), so it is reproducible:
15
+ //
16
+ // node corpus/conceptnet/quality-filter.mjs < corpus/conceptnet/slice.jsonl > slice.clean.jsonl
17
+ // # or in place (what produced the committed clean slice):
18
+ // node corpus/conceptnet/quality-filter.mjs --in-place corpus/conceptnet/slice.jsonl
19
+ //
20
+ // Cut rules (a row is DROPPED when ANY applies):
21
+ // 1. numeric endpoint — start/end bare term is all digits ("2", "1000")
22
+ // 2. single-char endpoint — bare term length <= 1 ("a", "r", "m")
23
+ // 3. sentence fragment — bare term is >= 4 underscore-words on EITHER
24
+ // endpoint ("taloned_grip_of_owl",
25
+ // "worlds_largest_interconnected_network_of_networks")
26
+ // 4. definitional phrase — /r/DefinedAs whose object is >= 3 words
27
+ // (real DefinedAs is a synonym: cpu->processor)
28
+ // 5. opinion object — /r/IsA or /r/DefinedAs whose object is one of a
29
+ // small, evidence-based OPINION set (adjectives /
30
+ // value words that never name a class)
31
+ //
32
+ // Rules 1-3 apply to every relation (they only ever remove junk); 4-5 are the
33
+ // definitional band the sims flagged. Stats land on stderr; JSONL on stdout.
34
+
35
+ import { readFile, writeFile } from "node:fs/promises";
36
+ import { createInterface } from "node:readline";
37
+
38
+ const bareTerm = (uri) => String(uri || "").replace(/^\/c\/en\//, "");
39
+ const words = (t) => t.split("_").filter(Boolean).length;
40
+
41
+ // Evidence-based: the only 1-word IsA/DefinedAs objects in the committed slice
42
+ // that are opinions/adjectives rather than classes. Kept explicit (not a POS
43
+ // heuristic) so it never cuts a legitimate abstract class like "abstraction",
44
+ // "cognition" or "relation".
45
+ export const OPINION_OBJECTS = new Set([
46
+ "elegance", "evil", "gloom", "unreality", "universalism", "dumb", "free", "junk",
47
+ ]);
48
+
49
+ /** Why this row is noise, or null to keep it. Pure function of the row shape. */
50
+ export function cutReason(row) {
51
+ const s = bareTerm(row.start);
52
+ const e = bareTerm(row.end);
53
+ for (const t of [s, e]) {
54
+ if (/^\d+$/.test(t)) return "numeric-endpoint";
55
+ if (t.length <= 1) return "single-char-endpoint";
56
+ if (words(t) >= 4) return "sentence-fragment";
57
+ }
58
+ if (row.rel === "/r/DefinedAs" && words(e) >= 3) return "definitional-phrase";
59
+ if ((row.rel === "/r/IsA" || row.rel === "/r/DefinedAs") && OPINION_OBJECTS.has(e)) return "opinion-object";
60
+ return null;
61
+ }
62
+
63
+ async function readLines(stream) {
64
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
65
+ const rows = [];
66
+ for await (const raw of rl) {
67
+ const line = raw.trim();
68
+ if (line) rows.push(JSON.parse(line));
69
+ }
70
+ return rows;
71
+ }
72
+
73
+ const isMain = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href;
74
+ if (isMain) {
75
+ const inPlace = process.argv.includes("--in-place");
76
+ const fileArg = process.argv.slice(2).find((a) => !a.startsWith("--"));
77
+ const rows = inPlace
78
+ ? (await readFile(fileArg, "utf8")).split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l))
79
+ : await readLines(process.stdin);
80
+
81
+ const byReason = new Map();
82
+ const kept = [];
83
+ for (const row of rows) {
84
+ const reason = cutReason(row);
85
+ if (reason) { byReason.set(reason, (byReason.get(reason) || 0) + 1); continue; }
86
+ kept.push(row);
87
+ }
88
+ const text = kept.map((r) => JSON.stringify(r)).join("\n") + "\n";
89
+ if (inPlace) await writeFile(fileArg, text);
90
+ else process.stdout.write(text);
91
+
92
+ const cut = rows.length - kept.length;
93
+ console.error(`quality-filter: ${rows.length} rows in, ${kept.length} kept, ${cut} cut (${text.length} bytes out)`);
94
+ for (const [reason, n] of [...byReason.entries()].sort((a, b) => b[1] - a[1])) console.error(` ${reason}: ${n}`);
95
+ }