@polycode-projects/the-mechanical-code-talker 0.5.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
@@ -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/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
+ }