@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 +64 -28
- package/ROADMAP.md +6 -6
- 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/grammar-rules.toml +18 -10
- package/data/templates/responses.jsonl +2 -0
- package/package.json +10 -2
- package/src/ask-vocab.mjs +19 -1
- package/src/ask.mjs +90 -6
- package/src/chat.mjs +647 -60
- package/src/codegraph.mjs +28 -5
- package/src/conformance.mjs +166 -0
- package/src/corpus/conceptnet.mjs +24 -6
- package/src/grammar/lexicon-core.json +8 -0
- 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
|
|
@@ -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
|
|
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
|
|
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")
|
|
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
|
|
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/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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|