@polycode-projects/the-mechanical-code-talker 1.8.20 → 1.9.1

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.
@@ -0,0 +1,49 @@
1
+ LICENSE NOTICE — corpus/wordnet/wordnet-xl.jsonl, corpus/wordnet/wordnet-full.jsonl
2
+ ================================================================================
3
+
4
+ The files wordnet-xl.jsonl and wordnet-full.jsonl in this directory are a
5
+ mechanical, ConceptNet-shape conversion of structural relation data (hypernym,
6
+ meronymy, causation, attribute, similarity, "also see", and member/synonym
7
+ lists) from the Open English WordNet project, and are licensed under the
8
+ Creative Commons Attribution 4.0 International License (CC-BY-4.0), NOT under
9
+ this repository's MPL-2.0.
10
+
11
+ https://creativecommons.org/licenses/by/4.0/
12
+
13
+ Attribution
14
+ -----------
15
+
16
+ This work includes data derived from the Open English WordNet, a fork and
17
+ continuation of Princeton WordNet, made available under CC-BY-4.0 by the
18
+ Globalwordnet / Open English Wordnet team.
19
+
20
+ - Project: https://github.com/globalwordnet/english-wordnet
21
+ - Princeton WordNet: https://wordnet.princeton.edu/
22
+ - See that project's own LICENSE.md for the full attribution statement.
23
+
24
+ Source
25
+ ------
26
+
27
+ - Dataset: Open English WordNet YAML source (`src/yaml/noun.*.yaml`,
28
+ `verb.*.yaml`, `adj.*.yaml`, `adv.*.yaml` — 107,526 synsets), read from a
29
+ LOCAL, uncommitted checkout — never vendored into this repository.
30
+ - Conversion tool: corpus/wordnet/generate.mjs (tmct code, MPL-2.0) — reads
31
+ each synset's `hypernym`/`mero_part`/`mero_member`/`mero_substance`/
32
+ `causes`/`attribute`/`similar`/`also`/`members` fields (never `definition`/
33
+ `example` free prose) and emits one ConceptNet-shape row per relation edge
34
+ or synonym-chain pair. See that file's own header comment for the full
35
+ relation-mapping table and direction rules.
36
+ - wordnet-xl.jsonl is a bounded ~24,000-fact PRIORITIZED SUBSET of
37
+ wordnet-full.jsonl (the hypernym backbone plus synonym chains for the most
38
+ commonly-referenced synsets); wordnet-full.jsonl is the complete conversion.
39
+
40
+ Share-alike
41
+ -----------
42
+
43
+ CC-BY-4.0 does NOT require share-alike (unlike ConceptNet's CC-BY-SA 4.0 in
44
+ corpus/conceptnet/) — a CC-BY-4.0 derivative may be relicensed, provided this
45
+ attribution is preserved. This repository chooses to keep redistributing
46
+ wordnet-xl.jsonl/wordnet-full.jsonl themselves under CC-BY-4.0 regardless, to
47
+ stay unambiguous about their origin. The code in this directory and in
48
+ corpus/wordnet/ (generate.mjs) is tmct code under the repository's MPL-2.0;
49
+ only the two data files carry CC-BY-4.0.
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ // corpus/wordnet/generate.mjs — converts a LOCAL Open English WordNet (OEWN)
3
+ // checkout into ConceptNet-shape fact rows. Curated + committed, tier-2-shaped
4
+ // — NOT "tier-3" (corpus/README.md's tiering policy already uses that name
5
+ // for something else, runtime-learned facts that are never committed); this
6
+ // bundle is just too large to hand-author like the other tier-2 corpuses.
7
+ // NOT part of the product path — a maintainer tool, run by hand, offline, $0;
8
+ // its OUTPUT (corpus/wordnet/wordnet-{xl,full}.jsonl + manifest.json) is what
9
+ // gets committed, never the source YAML itself.
10
+ //
11
+ // node corpus/wordnet/generate.mjs [yamlDir]
12
+ // TMCT_WORDNET_YAML_DIR=/path/to/yaml node corpus/wordnet/generate.mjs
13
+ //
14
+ // Input: `~/projects/globalwordnet/english-wordnet/src/yaml/` by default (a
15
+ // LOCAL clone, never vendored/committed — CC-BY-4.0, see LICENSE-NOTICE in
16
+ // this directory) — Princeton WordNet's 107,526 synsets across
17
+ // `noun.*.yaml`/`verb.*.yaml`/`adj.*.yaml`/`adv.*.yaml` (45 of the checkout's
18
+ // 73 yaml files; the other 28 are `entries-<letter>.yaml` word-form indexes
19
+ // this converter doesn't need — synset records alone carry every relation and
20
+ // member list this converter reads).
21
+ //
22
+ // Two passes, same discipline as corpus/conceptnet/fetch-slice.mjs and
23
+ // corpus/tier2/generate.mjs (deterministic, sorted, one JSON object per
24
+ // line, `{start, rel, end, weight, surfaceText}` — the exact tier-1 slice
25
+ // shape):
26
+ // 1. load every synset (across ALL 45 files) into one global
27
+ // synsetId -> record map — synset ids ("00034778-n") are globally
28
+ // unique, POS suffix included, so a single flat Map is correct.
29
+ // 2. walk every synset's structural relations + its own `members` list,
30
+ // emitting one row per edge (see RELATION_MAP / synonymPairs below).
31
+ //
32
+ // The hand-rolled `parseYaml` this file reuses (imported, not duplicated) is
33
+ // scripts/extract-persona-sources.mjs's own tiny YAML-subset reader — already
34
+ // proven against this exact OEWN dump shape by scripts/build-persona-tiers.mjs
35
+ // and scripts/build-persona-examples.mjs. Reusing it (rather than adding a
36
+ // general YAML dependency, or re-deriving a second hand-rolled parser) keeps
37
+ // this converter self-consistent with the rest of the persona-tier tooling
38
+ // that already reads this same source.
39
+ //
40
+ // Licence: Open English WordNet content is CC-BY-4.0 (Princeton WordNet +
41
+ // Open English Wordnet team) — see LICENSE-NOTICE in this directory. The
42
+ // code in this file is tmct code under the repository's MPL-2.0; only the
43
+ // generated data (corpus/wordnet/*.jsonl) carries CC-BY-4.0.
44
+
45
+ import { readFile, readdir, writeFile, mkdir } from "node:fs/promises";
46
+ import { homedir } from "node:os";
47
+ import { createHash } from "node:crypto";
48
+ import { fileURLToPath } from "node:url";
49
+ import { dirname, join } from "node:path";
50
+ import { parseYaml } from "../../scripts/extract-persona-sources.mjs";
51
+
52
+ const HERE = dirname(fileURLToPath(import.meta.url));
53
+ export const WORDNET_OUT_DIR = HERE;
54
+
55
+ export const DEFAULT_YAML_DIR = join(homedir(), "projects", "globalwordnet", "english-wordnet", "src", "yaml");
56
+
57
+ /** Resolve the input yaml directory: CLI positional arg > env var > default.
58
+ * Exposed as a pure function (argv/env injectable) so it's unit-testable
59
+ * without touching real process.argv/env. */
60
+ export function resolveYamlDir(argv = process.argv.slice(2), env = process.env) {
61
+ return argv[0] || env.TMCT_WORDNET_YAML_DIR || DEFAULT_YAML_DIR;
62
+ }
63
+
64
+ // ---- relation mapping table (pure, unit-tested) ----------------------------
65
+ // WordNet relation key -> { rel: ConceptNet relation URI, flip }. `flip`
66
+ // governs which side of the WordNet edge becomes ConceptNet's `start` vs
67
+ // `end` — WordNet's meronymy relations point WHOLE -> part (the synset that
68
+ // OWNS a `mero_part`/`mero_member` list is the whole; its listed target is
69
+ // the part/member — confirmed via scripts/wordnet.py's own
70
+ // inverse_synset_rels table: mero_part's inverse is holo_part, so "A mero_part
71
+ // B" reads "B is a part-meronym of A", i.e. A HAS PART B), which is the
72
+ // OPPOSITE of ConceptNet's own /r/PartOf convention (start=part, end=whole —
73
+ // confirmed against a real corpus/conceptnet/slice.jsonl row: "action PartOf
74
+ // keyboard" — action, the PART, is start). mero_part/mero_member both need
75
+ // `flip: true` for exactly this reason. mero_substance needs NO flip:
76
+ // ConceptNet's /r/MadeOf is start=whole, end=substance (confirmed against
77
+ // slice.jsonl's "computer MadeOf hardware" — computer, the WHOLE, is start),
78
+ // which already matches WordNet's own A(whole) -> mero_substance -> B
79
+ // (substance) direction untouched. hypernym/causes/attribute/similar/also
80
+ // are all natural A->B mappings with no direction conflict (the specific
81
+ // synset/adjective source relation IS the ConceptNet start side already).
82
+ export const RELATION_MAP = Object.freeze({
83
+ hypernym: { rel: "/r/IsA", flip: false },
84
+ mero_part: { rel: "/r/PartOf", flip: true },
85
+ mero_member: { rel: "/r/PartOf", flip: true },
86
+ mero_substance: { rel: "/r/MadeOf", flip: false },
87
+ causes: { rel: "/r/Causes", flip: false },
88
+ attribute: { rel: "/r/HasProperty", flip: false },
89
+ similar: { rel: "/r/SimilarTo", flip: false },
90
+ also: { rel: "/r/RelatedTo", flip: false },
91
+ });
92
+
93
+ // Deliberately excluded — no mapped ConceptNet relation, and task scope is to
94
+ // use ONLY the relations Phase 1 already added to conceptnet-map.toml, never
95
+ // invent a new map row: `entails` (verb implication — no ConceptNet analog)
96
+ // and `exemplifies` (instance-of-category — closest is /r/IsA, but that would
97
+ // blur "kind of" and "example of", a real semantic difference ConceptNet
98
+ // itself keeps separate via /r/InstanceOf, which conceptnet-map.toml doesn't
99
+ // carry). `definition`/`example` are free prose, never structured facts.
100
+ export const SKIPPED_RELATIONS = Object.freeze(["entails", "exemplifies"]);
101
+
102
+ // ---- term encoding (pure, unit-tested) -------------------------------------
103
+ // Exact inverse of src/corpus/conceptnet.mjs's termText() decode
104
+ // (`/^\/c\/en\/([^/]+)/` then `.replace(/_/g, " ")`): lowercase, spaces ->
105
+ // underscores, wrapped as `/c/en/<term>`. Nothing else is touched — any other
106
+ // punctuation (apostrophes, hyphens) round-trips through termText() unchanged
107
+ // because termText only ever substitutes underscores back to spaces.
108
+ export function encodeTerm(raw) {
109
+ const t = String(raw ?? "").trim().toLowerCase().replace(/\s+/g, "_");
110
+ return t ? `/c/en/${t}` : null;
111
+ }
112
+
113
+ /** `/c/en/ice_cream` -> "ice cream" — the human-readable form used in
114
+ * surfaceText, mirroring corpus/tier2/generate.mjs's own `humanize`. */
115
+ export const humanize = (term) => String(term ?? "").replace(/_/g, " ");
116
+
117
+ // ---- synonym chaining (pure, unit-tested) ----------------------------------
118
+ // A synset's `members` list are synonyms of each other. Chained N-1 (member
119
+ // [0] paired with each of member[1..N-1]) rather than the full N*(N-1)/2
120
+ // cross product — members average 1.72/synset (185,149 member-slots across
121
+ // 107,526 synsets), and the chain already connects every member into one
122
+ // component (a synonym-chases-synonym graph read), so the cross product would
123
+ // roughly double the fact count for no new information the chain doesn't
124
+ // already encode transitively.
125
+ export function synonymPairs(members) {
126
+ if (!Array.isArray(members) || members.length < 2) return [];
127
+ const [first, ...rest] = members;
128
+ return rest.map((m) => [first, m]);
129
+ }
130
+
131
+ // ---- pass 1: load every synset across every yaml file ----------------------
132
+ const SYNSET_FILE_RE = /^(noun|verb|adj|adv)\..+\.yaml$/;
133
+
134
+ export async function loadAllSynsets(yamlDir) {
135
+ const all = await readdir(yamlDir);
136
+ const files = all.filter((f) => SYNSET_FILE_RE.test(f)).sort();
137
+ if (!files.length) {
138
+ throw new Error(`${yamlDir}: no noun./verb./adj./adv. yaml files found — wrong path?`);
139
+ }
140
+ const bySynset = new Map();
141
+ for (const f of files) {
142
+ const text = await readFile(join(yamlDir, f), "utf8");
143
+ const parsed = parseYaml(text);
144
+ for (const [id, rec] of Object.entries(parsed)) bySynset.set(id, rec);
145
+ }
146
+ return { bySynset, files };
147
+ }
148
+
149
+ const repTerm = (synset) => {
150
+ const m = Array.isArray(synset?.members) ? synset.members : [];
151
+ return m.length ? m[0] : null;
152
+ };
153
+
154
+ // ---- pass 2: synset map -> deduped, sorted ConceptNet-shape rows -----------
155
+
156
+ function makeRowBuilder() {
157
+ const rows = new Map(); // dedupe key -> row
158
+ const add = (rawSubject, rel, rawObject) => {
159
+ const start = encodeTerm(rawSubject);
160
+ const end = encodeTerm(rawObject);
161
+ if (!start || !end || start === end) return; // self-loop / empty term — noise, not a fact
162
+ const key = `${rel} ${start} ${end}`;
163
+ if (rows.has(key)) return;
164
+ rows.set(key, {
165
+ start,
166
+ rel,
167
+ end,
168
+ weight: 1,
169
+ surfaceText: `[[${humanize(rawSubject)}]] ${rel.replace("/r/", "")} [[${humanize(rawObject)}]]`,
170
+ });
171
+ };
172
+ return { rows, add };
173
+ }
174
+
175
+ const sortRows = (rows) => rows.slice().sort((a, b) => (
176
+ a.rel !== b.rel ? (a.rel < b.rel ? -1 : 1)
177
+ : a.start !== b.start ? (a.start < b.start ? -1 : 1)
178
+ : a.end < b.end ? -1 : a.end > b.end ? 1 : 0
179
+ ));
180
+
181
+ /** Every structural + synonym-chain fact, deterministically sorted — the
182
+ * wordnet-full.jsonl content. */
183
+ export function buildFullFacts(bySynset) {
184
+ const { rows, add } = makeRowBuilder();
185
+ for (const synset of bySynset.values()) {
186
+ const A = repTerm(synset);
187
+ if (!A) continue;
188
+ for (const [wnRel, { rel, flip }] of Object.entries(RELATION_MAP)) {
189
+ const targets = Array.isArray(synset[wnRel]) ? synset[wnRel] : [];
190
+ for (const targetId of targets) {
191
+ const B = repTerm(bySynset.get(targetId));
192
+ if (!B) continue;
193
+ if (flip) add(B, rel, A); else add(A, rel, B);
194
+ }
195
+ }
196
+ for (const [m0, mi] of synonymPairs(synset.members)) add(m0, "/r/Synonym", mi);
197
+ }
198
+ return sortRows([...rows.values()]);
199
+ }
200
+
201
+ // ---- XL slice: hypernym backbone + synonym chains for the most commonly-
202
+ // referenced synsets, budget-bounded ----------------------------------------
203
+
204
+ /** How many times each synset appears as a hypernym TARGET — a synset's
205
+ * "commonly referenced" proxy: a category many other synsets specialize
206
+ * (e.g. "person", "act", "object") is referenced far more often than a
207
+ * narrow leaf synset. */
208
+ export function hypernymRefCounts(bySynset) {
209
+ const refCount = new Map();
210
+ for (const synset of bySynset.values()) {
211
+ const targets = Array.isArray(synset.hypernym) ? synset.hypernym : [];
212
+ for (const t of targets) refCount.set(t, (refCount.get(t) || 0) + 1);
213
+ }
214
+ return refCount;
215
+ }
216
+
217
+ /** Build the bounded XL slice: the `budget` fact rows are split between the
218
+ * hypernym backbone and synonym chains in the SAME proportion the full
219
+ * corpus's real hypernym-edge-count : synonym-chain-fact-count ratio has
220
+ * (measured from `full`, not a hand-picked constant) — the backbone gets the
221
+ * hypernym edges whose TARGET is most commonly referenced first; the
222
+ * synonym budget goes to the full chain of the most commonly-referenced
223
+ * synsets (same ranking), walked in ranked order until the budget is spent. */
224
+ export function buildXlFacts(bySynset, full, budget = 24000) {
225
+ const refCount = hypernymRefCounts(bySynset);
226
+
227
+ const hypernymTotal = full.filter((r) => r.rel === "/r/IsA").length;
228
+ const synonymTotal = full.filter((r) => r.rel === "/r/Synonym").length;
229
+ const denom = hypernymTotal + synonymTotal || 1;
230
+ const hypernymBudget = Math.round(budget * (hypernymTotal / denom));
231
+ const synonymBudget = budget - hypernymBudget;
232
+
233
+ // Backbone: every hypernym EDGE (not yet a fact row), ranked by how
234
+ // commonly-referenced its TARGET synset is, ties broken deterministically.
235
+ const edges = [];
236
+ for (const [id, synset] of bySynset) {
237
+ const targets = Array.isArray(synset.hypernym) ? synset.hypernym : [];
238
+ for (const t of targets) edges.push({ source: id, target: t });
239
+ }
240
+ edges.sort((a, b) => {
241
+ const byRef = (refCount.get(b.target) || 0) - (refCount.get(a.target) || 0);
242
+ if (byRef) return byRef;
243
+ if (a.target !== b.target) return a.target < b.target ? -1 : 1;
244
+ return a.source < b.source ? -1 : 1;
245
+ });
246
+
247
+ const { rows, add } = makeRowBuilder();
248
+ for (const { source, target } of edges) {
249
+ if (rows.size >= hypernymBudget) break;
250
+ const A = repTerm(bySynset.get(source));
251
+ const B = repTerm(bySynset.get(target));
252
+ if (!A || !B) continue;
253
+ add(A, "/r/IsA", B);
254
+ }
255
+
256
+ // Synonym chains: rank EVERY synset by the same "commonly referenced"
257
+ // proxy (0 for a synset that never appears as a hypernym target), then walk
258
+ // down taking each synset's FULL N-1 chain until the synonym budget is met
259
+ // (a synset's chain is never split — "~budget", not an exact cap).
260
+ const rankedSynsets = [...bySynset.entries()].sort((a, b) => {
261
+ const byRef = (refCount.get(b[0]) || 0) - (refCount.get(a[0]) || 0);
262
+ if (byRef) return byRef;
263
+ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
264
+ });
265
+ let synonymCount = 0;
266
+ for (const [, synset] of rankedSynsets) {
267
+ if (synonymCount >= synonymBudget) break;
268
+ const pairs = synonymPairs(synset.members);
269
+ if (!pairs.length) continue;
270
+ for (const [m0, mi] of pairs) add(m0, "/r/Synonym", mi);
271
+ synonymCount += pairs.length;
272
+ }
273
+
274
+ return sortRows([...rows.values()]);
275
+ }
276
+
277
+ // ---- output ------------------------------------------------------------
278
+
279
+ const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
280
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
281
+
282
+ async function main() {
283
+ const yamlDir = resolveYamlDir();
284
+ process.stderr.write(`corpus/wordnet/generate.mjs: reading ${yamlDir}\n`);
285
+ const { bySynset, files } = await loadAllSynsets(yamlDir);
286
+ process.stderr.write(` loaded ${bySynset.size} synsets across ${files.length} files\n`);
287
+
288
+ const full = buildFullFacts(bySynset);
289
+ const xl = buildXlFacts(bySynset, full);
290
+ process.stderr.write(` wordnet-full: ${full.length} facts\n`);
291
+ process.stderr.write(` wordnet-xl: ${xl.length} facts\n`);
292
+
293
+ await mkdir(WORDNET_OUT_DIR, { recursive: true });
294
+ const fullText = toJsonl(full);
295
+ const xlText = toJsonl(xl);
296
+ await writeFile(join(WORDNET_OUT_DIR, "wordnet-full.jsonl"), fullText);
297
+ await writeFile(join(WORDNET_OUT_DIR, "wordnet-xl.jsonl"), xlText);
298
+
299
+ const manifest = {
300
+ version: 1,
301
+ generated: "by corpus/wordnet/generate.mjs",
302
+ corpuses: [
303
+ {
304
+ id: "wordnet-xl",
305
+ kind: "language",
306
+ description: "A bounded ~24,000-fact slice of Princeton WordNet / Open English WordNet, prioritizing the hypernym (IsA) backbone and synonym chains for the most commonly-referenced synsets. Mechanically derived from Open English WordNet (CC-BY-4.0).",
307
+ source: { kind: "curated", tool: "corpus/wordnet/generate.mjs" },
308
+ file: "wordnet-xl.jsonl",
309
+ facts: xl.length,
310
+ bytes: Buffer.byteLength(xlText),
311
+ sha256: sha256(xlText),
312
+ license: "CC-BY-4.0",
313
+ },
314
+ {
315
+ id: "wordnet-full",
316
+ kind: "language",
317
+ description: "The complete ConceptNet-shape conversion of Princeton WordNet / Open English WordNet's structural relations (hypernym, meronymy, causes, attribute, similar, also) plus member-synonym chains. Derived from Open English WordNet (CC-BY-4.0).",
318
+ source: { kind: "curated", tool: "corpus/wordnet/generate.mjs" },
319
+ file: "wordnet-full.jsonl",
320
+ facts: full.length,
321
+ bytes: Buffer.byteLength(fullText),
322
+ sha256: sha256(fullText),
323
+ license: "CC-BY-4.0",
324
+ },
325
+ ],
326
+ };
327
+ const manifestText = JSON.stringify(manifest, null, 2) + "\n";
328
+ await writeFile(join(WORDNET_OUT_DIR, "manifest.json"), manifestText);
329
+ process.stderr.write(`wrote corpus/wordnet/manifest.json (${manifest.corpuses.length} corpuses)\n`);
330
+ }
331
+
332
+ const isMain = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href;
333
+ if (isMain) await main();
@@ -0,0 +1,34 @@
1
+ {
2
+ "version": 1,
3
+ "generated": "by corpus/wordnet/generate.mjs",
4
+ "corpuses": [
5
+ {
6
+ "id": "wordnet-xl",
7
+ "kind": "language",
8
+ "description": "A bounded ~24,000-fact slice of Princeton WordNet / Open English WordNet, prioritizing the hypernym (IsA) backbone and synonym chains for the most commonly-referenced synsets. Mechanically derived from Open English WordNet (CC-BY-4.0).",
9
+ "source": {
10
+ "kind": "curated",
11
+ "tool": "corpus/wordnet/generate.mjs"
12
+ },
13
+ "file": "wordnet-xl.jsonl",
14
+ "facts": 23805,
15
+ "bytes": 3035376,
16
+ "sha256": "4bbd7cdb0ccb7268a3e8ccfbefad3012cebb56ef3debb954eda6bf9e2006f541",
17
+ "license": "CC-BY-4.0"
18
+ },
19
+ {
20
+ "id": "wordnet-full",
21
+ "kind": "language",
22
+ "description": "The complete ConceptNet-shape conversion of Princeton WordNet / Open English WordNet's structural relations (hypernym, meronymy, causes, attribute, similar, also) plus member-synonym chains. Derived from Open English WordNet (CC-BY-4.0).",
23
+ "source": {
24
+ "kind": "curated",
25
+ "tool": "corpus/wordnet/generate.mjs"
26
+ },
27
+ "file": "wordnet-full.jsonl",
28
+ "facts": 192498,
29
+ "bytes": 25531520,
30
+ "sha256": "2ff54e666664087014a875cc1d91bdcc210ac6265820df94d65da54236c6e7fb",
31
+ "license": "CC-BY-4.0"
32
+ }
33
+ ]
34
+ }