@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 +60 -24
- package/bin/tmct.mjs +56 -0
- package/corpus/README.md +77 -5
- package/corpus/conceptnet/README.md +54 -2
- package/corpus/conceptnet/quality-filter.mjs +95 -0
- package/corpus/conceptnet/slice.jsonl +0 -378
- package/corpus/seon/LICENSE-NOTICE +37 -0
- package/corpus/seon/README.md +121 -0
- package/corpus/seon/concepts.jsonl +238 -0
- package/corpus/seon/definitions.jsonl +288 -0
- package/corpus/tier2/aws.jsonl +39 -0
- package/corpus/tier2/generate.mjs +253 -0
- package/corpus/tier2/java.jsonl +31 -0
- package/corpus/tier2/manifest.json +48 -0
- package/corpus/tier2/python.jsonl +30 -0
- package/data/templates/responses.jsonl +2 -0
- package/package.json +10 -2
- package/src/ask-vocab.mjs +19 -1
- package/src/ask.mjs +11 -11
- package/src/chat.mjs +459 -33
- package/src/codegraph.mjs +28 -5
- package/src/conformance.mjs +166 -0
- package/src/corpus/conceptnet.mjs +24 -6
- package/src/memory/inspect.mjs +25 -0
- package/src/server.mjs +88 -7
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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),
|
|
50
|
-
seed-term list to extend. The test suite
|
|
51
|
-
`test/corpus-templates.test.mjs`) guards the
|
|
52
|
-
en→en shape, the ≤ 1.5 MB budget, template
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
+
}
|