@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.
- package/README.md +27 -6
- package/ROADMAP.md +1 -1
- package/bin/tmct.mjs +167 -20
- package/corpus/generated/README.md +2 -2
- package/corpus/namenet/LICENSE-NOTICE +68 -0
- package/corpus/namenet/generate.mjs +309 -0
- package/corpus/namenet/manifest.json +20 -0
- package/corpus/namenet/namenet.jsonl +7260 -0
- package/corpus/wordnet/LICENSE-NOTICE +49 -0
- package/corpus/wordnet/generate.mjs +333 -0
- package/corpus/wordnet/manifest.json +34 -0
- package/corpus/wordnet/wordnet-full.jsonl +192498 -0
- package/corpus/wordnet/wordnet-xl.jsonl +23805 -0
- package/package.json +6 -2
- package/src/ask-browser-entry.mjs +13 -2
- package/src/ask-browser.bundle.js +272 -19
- package/src/chat.mjs +229 -82
- package/src/cli-args.mjs +20 -1
- package/src/codegraph.mjs +306 -1
- package/src/corpus/conceptnet-map.toml +17 -12
- package/src/corpus/conceptnet.mjs +23 -1
- package/src/extensions.mjs +44 -1
- package/src/init.mjs +99 -46
- package/src/interpret/normalize.mjs +1 -3
- package/src/memory/core.mjs +191 -14
- package/src/memory/trust.mjs +27 -6
- package/src/memory-ask-browser-entry.mjs +36 -0
- package/src/memory-ask-browser.bundle.js +5544 -0
- package/src/toml-config.mjs +11 -1
- package/src/viz.mjs +548 -73
|
@@ -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
|
+
}
|