@polycode-projects/the-mechanical-code-talker 2.3.1 → 2.5.2
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 +131 -32
- package/bin/tmct.mjs +18 -91
- package/corpus/README.md +3 -3
- package/corpus/seon/README.md +1 -0
- package/corpus/tier2/generate.mjs +18 -18
- package/corpus/tier2/manifest.json +3 -3
- package/data/games/hanoi-3.txt +8 -2
- package/package.json +26 -8
- package/src/adapters/corpus-lanes.mjs +13 -0
- package/src/adapters/graph-build.mjs +5 -7
- package/src/adapters/import-closure.mjs +28 -0
- package/src/adapters/memory/blocks.mjs +5 -4
- package/src/adapters/memory/core.mjs +78 -5
- package/src/adapters/memory/shacl.mjs +12 -0
- package/src/adapters/providers/graph-service.mjs +12 -5
- package/src/adapters/tracked-files.mjs +17 -0
- package/src/domain/ask-vocab.mjs +2 -0
- package/src/domain/ask.mjs +225 -13
- package/src/domain/cli-verbs.mjs +201 -0
- package/src/domain/codegraph.mjs +142 -56
- package/src/domain/completions/graph-adapter.mjs +1 -1
- package/src/domain/completions/group.mjs +3 -17
- package/src/domain/completions/infer.mjs +4 -13
- package/src/domain/completions/rank.mjs +6 -19
- package/src/domain/grammar/lexicon-core.json +1 -1
- package/src/domain/hash.mjs +36 -13
- package/src/domain/interpret/fuzzy.mjs +7 -2
- package/src/domain/interpret/normalize.mjs +9 -0
- package/src/domain/interpret/strategies/keywords.mjs +19 -9
- package/src/domain/memory/capability.mjs +22 -3
- package/src/domain/memory/touched-facts.mjs +17 -0
- package/src/domain/module-paths.mjs +9 -0
- package/src/domain/persona/tiers.mjs +1 -1
- package/src/domain/planning.mjs +37 -0
- package/src/domain/prose.mjs +10 -2
- package/src/domain/relative-specifiers.mjs +12 -0
- package/src/domain/router/registry.mjs +3 -2
- package/src/domain/router/results.mjs +5 -18
- package/src/domain/seeded-random.mjs +33 -0
- package/src/domain/syllogise.mjs +10 -7
- package/src/domain/text-stats.mjs +31 -0
- package/src/services/chat.mjs +722 -184
- package/src/services/extract-facts.mjs +155 -0
- package/src/services/import-file.mjs +2 -2
- package/src/services/init.mjs +2 -2
- package/src/services/ledger-viz.mjs +6 -1
- package/src/services/sentences.mjs +26 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +11390 -360
- package/src/tools/graph-load.mjs +7 -1
- package/src/tools/readme-docs.mjs +113 -0
- package/src/tools/schema-docs.mjs +2 -2
- package/ROADMAP.md +0 -129
- package/corpus/namenet/generate.mjs +0 -309
- package/corpus/wordnet/generate.mjs +0 -332
- package/src/adapters/prose-tokens.mjs +0 -98
- package/src/adapters/wordnet-source.mjs +0 -70
- package/src/domain/corpus-matrix.mjs +0 -87
- package/src/domain/inflect.mjs +0 -67
- package/src/domain/licences.mjs +0 -68
- package/src/domain/markdown-links.mjs +0 -55
- package/src/domain/persona/codegen.mjs +0 -123
- package/src/domain/publish-gate.mjs +0 -41
- package/src/domain/schemaorg/turtle.mjs +0 -25
- package/src/domain/semcor/parse.mjs +0 -87
- package/src/domain/version-stamp.mjs +0 -36
- package/src/domain/wordnet/yaml.mjs +0 -133
package/src/domain/licences.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// licences.mjs — the allowlist, the SPDX expression rule, and the two pure
|
|
2
|
-
// reads the licence check needs. Pure: objects and strings in, verdicts out, so
|
|
3
|
-
// the rule that gates CI can be tested without an installed node_modules tree.
|
|
4
|
-
//
|
|
5
|
-
// The check itself (scripts/check-licences.mjs) shells out to `npm ls` and
|
|
6
|
-
// reads each package's package.json off disk. That is the disk half. This is
|
|
7
|
-
// the deciding half, and the deciding half is where the edge cases are.
|
|
8
|
-
|
|
9
|
-
export const ALLOWED = new Set([
|
|
10
|
-
"MIT",
|
|
11
|
-
"ISC",
|
|
12
|
-
"BSD-2-Clause",
|
|
13
|
-
"BSD-3-Clause",
|
|
14
|
-
"Apache-2.0",
|
|
15
|
-
"MPL-2.0",
|
|
16
|
-
"0BSD",
|
|
17
|
-
"CC0-1.0",
|
|
18
|
-
"Unlicense",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
/** The licence `pkg` declares, across the three shapes npm has used: the
|
|
22
|
-
* current string, the legacy `{ type }` object, and the legacy `licenses[]`
|
|
23
|
-
* array (read as a choice, so it joins with OR). */
|
|
24
|
-
export function licenseFromPackageJson(pkg) {
|
|
25
|
-
if (typeof pkg.license === "string") return pkg.license;
|
|
26
|
-
if (pkg.license && typeof pkg.license.type === "string") return pkg.license.type;
|
|
27
|
-
if (Array.isArray(pkg.licenses)) return pkg.licenses.map((l) => l.type).join(" OR ");
|
|
28
|
-
return "(none declared)";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** True iff `license` is inside the allowlist.
|
|
32
|
-
*
|
|
33
|
-
* A flat OR expression passes when any part is allowlisted, because OR is a
|
|
34
|
-
* choice and we can take the allowlisted one. A flat AND expression needs
|
|
35
|
-
* every part. Everything else — nesting, a WITH exception, an OR and an AND in
|
|
36
|
-
* the same expression, even a redundantly parenthesised single licence like
|
|
37
|
-
* "(MIT)" — returns false and gets reviewed by hand. Those are rare enough
|
|
38
|
-
* that a parser would be more code than the reviews it saves, and false is the
|
|
39
|
-
* safe direction: it stops CI and asks a human, rather than waving through an
|
|
40
|
-
* expression it only half understood. */
|
|
41
|
-
export function isAllowed(license) {
|
|
42
|
-
if (ALLOWED.has(license)) return true;
|
|
43
|
-
const inner = license.replace(/^\(/, "").replace(/\)$/, "");
|
|
44
|
-
if (/[()]|\bWITH\b/.test(inner)) return false;
|
|
45
|
-
if (inner.includes(" OR ") && !inner.includes(" AND ")) {
|
|
46
|
-
return inner.split(" OR ").some((part) => ALLOWED.has(part.trim()));
|
|
47
|
-
}
|
|
48
|
-
if (inner.includes(" AND ") && !inner.includes(" OR ")) {
|
|
49
|
-
return inner.split(" AND ").every((part) => ALLOWED.has(part.trim()));
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Every installed package in an `npm ls --json` dependency tree, deduped by
|
|
55
|
-
* name@version and sorted by name. A node with no `path` is a peer or optional
|
|
56
|
-
* dependency that was never installed, so it cannot ship and is skipped. */
|
|
57
|
-
export function installedPackages(deps) {
|
|
58
|
-
const seen = new Map();
|
|
59
|
-
(function walk(level) {
|
|
60
|
-
for (const [name, node] of Object.entries(level ?? {})) {
|
|
61
|
-
if (node && node.path && node.version) {
|
|
62
|
-
seen.set(`${name}@${node.version}`, { name, version: node.version, path: node.path });
|
|
63
|
-
}
|
|
64
|
-
if (node) walk(node.dependencies);
|
|
65
|
-
}
|
|
66
|
-
})(deps);
|
|
67
|
-
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// markdown-links.mjs — pull the relative link targets out of a markdown string.
|
|
2
|
-
// Pure: a string in, `[{ target, line }]` out, no filesystem and no imports, so
|
|
3
|
-
// the CI jobs that run without `npm ci` can reach it.
|
|
4
|
-
//
|
|
5
|
-
// Code is not prose. A doc that writes `[text](target)` inside backticks is
|
|
6
|
-
// showing you what a link looks like, not making one, and a checker that cannot
|
|
7
|
-
// tell the difference reports the example as a broken link to a file named
|
|
8
|
-
// "target". So the spans are blanked before the link patterns run — blanked
|
|
9
|
-
// rather than cut, because every offset behind them still has to name the right
|
|
10
|
-
// line number.
|
|
11
|
-
|
|
12
|
-
const blank = (line) => line.replace(/[^\n]/g, " ");
|
|
13
|
-
const FENCE = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
14
|
-
|
|
15
|
-
/** Replace every fenced block and inline code span with spaces, keeping the
|
|
16
|
-
* string's length and its newlines so later offsets still map to their line.
|
|
17
|
-
* Fences are walked line by line: a lazy multiline regex stops at the end of
|
|
18
|
-
* the opening fence's own line and blanks only the markers. */
|
|
19
|
-
export function blankCodeSpans(markdown) {
|
|
20
|
-
let fence = null;
|
|
21
|
-
const lines = markdown.split("\n").map((line) => {
|
|
22
|
-
const marker = line.match(FENCE)?.[1];
|
|
23
|
-
if (fence) {
|
|
24
|
-
if (marker && marker[0] === fence[0] && marker.length >= fence.length) fence = null;
|
|
25
|
-
return blank(line);
|
|
26
|
-
}
|
|
27
|
-
if (marker) fence = marker;
|
|
28
|
-
return marker ? blank(line) : line;
|
|
29
|
-
});
|
|
30
|
-
return lines.join("\n").replace(/(`+)[\s\S]*?\1/g, blank);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Inline links/images: [text](target "title") — target ends at the first
|
|
34
|
-
// whitespace or closing paren. Reference definitions: [label]: target.
|
|
35
|
-
const INLINE_LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
36
|
-
const REFERENCE_DEF = /^\s{0,3}\[[^\]]+\]:\s+(\S+)/gm;
|
|
37
|
-
|
|
38
|
-
/** Every repo-relative link target in `markdown`, with the 1-based line it sits
|
|
39
|
-
* on. External URLs, bare #anchors and absolute paths are out of scope. */
|
|
40
|
-
export function relativeTargets(markdown) {
|
|
41
|
-
const prose = blankCodeSpans(markdown);
|
|
42
|
-
const targets = [];
|
|
43
|
-
for (const regex of [INLINE_LINK, REFERENCE_DEF]) {
|
|
44
|
-
for (const match of prose.matchAll(regex)) {
|
|
45
|
-
let target = match[1];
|
|
46
|
-
if (/^(https?|mailto|ftp):/i.test(target)) continue;
|
|
47
|
-
if (target.startsWith("#") || target.startsWith("/") || target.startsWith("<")) continue;
|
|
48
|
-
target = decodeURIComponent(target.split("#")[0].split("?")[0]);
|
|
49
|
-
if (!target) continue;
|
|
50
|
-
const line = prose.slice(0, match.index).split("\n").length;
|
|
51
|
-
targets.push({ target, line });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return targets;
|
|
55
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// codegen.mjs — renders a reviewed persona-tier worksheet as JS source, ready
|
|
2
|
-
// to splice into corpus/tier2/generate.mjs's CORPUSES object, and as the
|
|
3
|
-
// lexicon-core.json noun entries those facts need.
|
|
4
|
-
//
|
|
5
|
-
// Pure: worksheet in, source text out, no imports.
|
|
6
|
-
|
|
7
|
-
export const CLUMP_ORDER = [
|
|
8
|
-
"human-core", "human-places", "human-objects", "human-nature",
|
|
9
|
-
"human-time-events", "human-body-food", "human-mind",
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
export const CLUMP_LABEL = {
|
|
13
|
-
"human-core": "people, family, common roles",
|
|
14
|
-
"human-places": "places",
|
|
15
|
-
"human-objects": "objects, clothing, tools",
|
|
16
|
-
"human-nature": "animals, plants, substances",
|
|
17
|
-
"human-time-events": "time and events",
|
|
18
|
-
"human-body-food": "body and food",
|
|
19
|
-
"human-mind": "communication, cognition, feeling",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// The irregular plurals among the words these batches introduce (Latin/Greek-
|
|
23
|
-
// derived cognition/body/nature vocabulary especially). A word absent from this
|
|
24
|
-
// map gets `{}` and relies on the regular -s/-es/-ies suffix fold at LOOKUP
|
|
25
|
-
// time (src/domain/grammar/lexicon.mjs's foldCandidates), same as the vast
|
|
26
|
-
// majority of Small tier's own additions. So this map is not a pluralizer: it
|
|
27
|
-
// is the list of exceptions that folding cannot recover, and declaring a
|
|
28
|
-
// regular plural here would be redundant.
|
|
29
|
-
//
|
|
30
|
-
// This is deliberately NOT src/domain/inflect.mjs's pluralOf, and the two must
|
|
31
|
-
// not be merged. They answer opposite questions. pluralOf GENERATES candidate
|
|
32
|
-
// surface forms for the real-word collision table, where over-generating is the
|
|
33
|
-
// cheap mistake and its own header commits to regular rules only — it wants
|
|
34
|
-
// "foots", because a form it fails to generate is a real word the repair tier
|
|
35
|
-
// may rewrite into a different question. This map DECLARES the one correct
|
|
36
|
-
// plural for a lexicon entry, where "foots" would simply be a lie the grammar
|
|
37
|
-
// then trusts. Teaching pluralOf about "feet" would cost the collision table
|
|
38
|
-
// "foots"; deriving this map from pluralOf would put "foots" in the lexicon.
|
|
39
|
-
export const IRREGULAR_PLURALS = {
|
|
40
|
-
foot: "feet", tooth: "teeth", goose: "geese", ox: "oxen", die: "dice",
|
|
41
|
-
louse: "lice", mouse: "mice", crisis: "crises", analysis: "analyses",
|
|
42
|
-
hypothesis: "hypotheses", thesis: "theses", axis: "axes", basis: "bases",
|
|
43
|
-
oasis: "oases", criterion: "criteria", phenomenon: "phenomena",
|
|
44
|
-
alumnus: "alumni", cactus: "cacti", focus: "foci", radius: "radii",
|
|
45
|
-
fungus: "fungi", nucleus: "nuclei", stimulus: "stimuli", larva: "larvae",
|
|
46
|
-
alga: "algae", vertebra: "vertebrae", antenna: "antennae",
|
|
47
|
-
formula: "formulae", datum: "data", medium: "media", index: "indices",
|
|
48
|
-
matrix: "matrices", appendix: "appendices", curriculum: "curricula",
|
|
49
|
-
memorandum: "memoranda", millennium: "millennia", bacterium: "bacteria",
|
|
50
|
-
stratum: "strata", genus: "genera", species: "species", series: "series",
|
|
51
|
-
sheep: "sheep", deer: "deer", moose: "moose", salmon: "salmon",
|
|
52
|
-
trout: "trout", swine: "swine", offspring: "offspring", spacecraft: "spacecraft",
|
|
53
|
-
aircraft: "aircraft", scissors: "scissors", knife: "knives", wife: "wives",
|
|
54
|
-
life: "lives", leaf: "leaves", loaf: "loaves", thief: "thieves",
|
|
55
|
-
shelf: "shelves", elf: "elves", calf: "calves", half: "halves",
|
|
56
|
-
wolf: "wolves", self: "selves", woman: "women", man: "men",
|
|
57
|
-
child: "children", person: "people", tempo: "tempi",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** One noun's lexicon-core.json entry: an explicit plural only where folding
|
|
61
|
-
* could not recover it. */
|
|
62
|
-
export function lexiconNounEntry(word) {
|
|
63
|
-
const plural = IRREGULAR_PLURALS[word];
|
|
64
|
-
return plural ? { plural } : {};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** One clump's facts as indented JS array-literal source lines. */
|
|
68
|
-
export function factsBlock(facts) {
|
|
69
|
-
return facts.map(([s, r, o]) => ` ["${s}", "${r}", "${o}"],`).join("\n");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** A noun list wrapped at ~8 words/line, matching Small tier's own
|
|
73
|
-
* lexicon-list style. */
|
|
74
|
-
export function nounsListBlock(words) {
|
|
75
|
-
const lines = [];
|
|
76
|
-
for (let i = 0; i < words.length; i += 8) {
|
|
77
|
-
lines.push(` ${words.slice(i, i + 8).map((w) => `"${w}"`).join(", ")},`);
|
|
78
|
-
}
|
|
79
|
-
return lines.join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** One CORPUSES entry as JS source, for `id` ("human-medium"/"human-large")
|
|
83
|
-
* from `byClump`, the reviewed worksheet's per-clump {facts, newNouns}. */
|
|
84
|
-
export function buildCorpusEntry(id, tierLabel, byClump) {
|
|
85
|
-
const nounsSrc = CLUMP_ORDER.map((c) => ` // ${c} (+${byClump[c].newNouns.length} words: ${CLUMP_LABEL[c]})\n${nounsListBlock(byClump[c].newNouns)}`).join("\n");
|
|
86
|
-
const factsSrc = CLUMP_ORDER.map((c) => ` // ---- ${c} (+${byClump[c].facts.length} facts, ${tierLabel}) ----\n${factsBlock(byClump[c].facts)}`).join("\n\n");
|
|
87
|
-
|
|
88
|
-
return `
|
|
89
|
-
// ${tierLabel} tier — INCREMENTAL facts beyond ${id === "human-medium" ? "Small (corpus/tier2/human.jsonl)" : "Medium (corpus/tier2/human-medium.jsonl)"} only
|
|
90
|
-
// (PLAN_SEED.md §3's tier-selection design: Small/Medium/Large are SIZES of
|
|
91
|
-
// one bundle, not separate corpus ids — this file holds only what ${tierLabel}
|
|
92
|
-
// ADDS beyond the previous tier). Built by scripts/build-persona-tiers.mjs
|
|
93
|
-
// from the same two locally-cloned WordNet source files as Small
|
|
94
|
-
// (~/projects/globalwordnet/english-wordnet/src/yaml/), automatically
|
|
95
|
-
// curated: candidate words ranked by WordNet sense-count (a commonness
|
|
96
|
-
// proxy, PLAN_SEED.md §12), restricted to each word's own TOP senses (not
|
|
97
|
-
// some rare/slang meaning that happens to live in this domain), obscure/
|
|
98
|
-
// archaic/offensive/pharmaceutical content excluded via a definition-text
|
|
99
|
-
// blocklist plus an explicit word denylist, reviewed by hand before being
|
|
100
|
-
// spliced in here (scripts/apply-persona-tiers.mjs). ${id === "human-large" ? "Large's own facts walk real multi-hop hypernym chains (up to 4 hops, PLAN_SEED.md §3's own \"surgeon ⊑ doctor ⊑ … ⊑ person\" example) wherever WordNet's real structure supports it, not a flat one-hop-per-word cap." : "Medium stays flat, one hop per word, same style as Small."}
|
|
101
|
-
"${id}": {
|
|
102
|
-
kind: "domain",
|
|
103
|
-
description: "The ${tierLabel} tier of the default human-world persona (PLAN_SEED.md): incremental facts beyond ${id === "human-medium" ? "Small" : "Medium"} only — activated alongside \\"human\\" via --persona-size ${id === "human-medium" ? "medium" : "large"}, never active by default.",
|
|
104
|
-
lexicon: {
|
|
105
|
-
nouns: [
|
|
106
|
-
${nounsSrc}
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
facts: [
|
|
110
|
-
${factsSrc}
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** The CORPUSES source with `entries` spliced in before its closing brace.
|
|
117
|
-
* Throws when the anchor is absent rather than writing a mangled file. */
|
|
118
|
-
export function spliceCorpusEntries(generateSrc, entries) {
|
|
119
|
-
const anchor = "\n};\n\nconst conceptUri = ";
|
|
120
|
-
const idx = generateSrc.indexOf(anchor);
|
|
121
|
-
if (idx === -1) throw new Error("apply-persona-tiers: could not find CORPUSES closing anchor in generate.mjs");
|
|
122
|
-
return generateSrc.slice(0, idx) + entries.join("") + generateSrc.slice(idx + 1);
|
|
123
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// publish-gate.mjs — should this commit publish to npm? Pure: two version
|
|
2
|
-
// strings in, a decision and a reason out, no imports, so CI can ask without
|
|
3
|
-
// npm ci.
|
|
4
|
-
//
|
|
5
|
-
// CI asked this with `[ "$PUBLISHED" = "$LOCAL" ]`, which only answers "same".
|
|
6
|
-
// A local version BELOW what npm already has — a revert, a bad merge, a branch
|
|
7
|
-
// landing behind — reads as "different" and goes to `npm publish`, where the
|
|
8
|
-
// registry rejects it and the job fails on a confusing error instead of a clear
|
|
9
|
-
// skip. Equality cannot tell "ahead" from "behind"; comparing can.
|
|
10
|
-
|
|
11
|
-
const CORE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
12
|
-
|
|
13
|
-
/** [major, minor, patch] for a semver string, or null if it isn't one. */
|
|
14
|
-
function core(version) {
|
|
15
|
-
const found = CORE.exec(String(version ?? "").trim());
|
|
16
|
-
return found ? [Number(found[1]), Number(found[2]), Number(found[3])] : null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** -1 | 0 | 1 comparing the release cores of `a` and `b`. Pre-release and build
|
|
20
|
-
* metadata are ignored: this gate decides whether a release moved, and 2.2.0
|
|
21
|
-
* and 2.2.0-rc.1 are the same release for that purpose. */
|
|
22
|
-
export function compareVersions(a, b) {
|
|
23
|
-
const [x, y] = [core(a), core(b)];
|
|
24
|
-
if (!x || !y) throw new Error(`not a comparable version: "${!x ? a : b}"`);
|
|
25
|
-
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Should `local` publish, given the registry currently serves `published`?
|
|
30
|
-
* `published` is "none" when the package has never been published.
|
|
31
|
-
* Returns { publish, reason } — the reason is what CI prints either way. */
|
|
32
|
-
export function shouldPublish(local, published) {
|
|
33
|
-
if (!core(local)) throw new Error(`not a publishable version: "${local}"`);
|
|
34
|
-
if (published === "none" || published == null || published === "") {
|
|
35
|
-
return { publish: true, reason: `publishing ${local} (nothing published yet)` };
|
|
36
|
-
}
|
|
37
|
-
const order = compareVersions(local, published);
|
|
38
|
-
if (order === 0) return { publish: false, reason: `npm already has ${local} — no version bump in this push, skipping publish` };
|
|
39
|
-
if (order < 0) return { publish: false, reason: `local ${local} is BEHIND npm's ${published} — refusing to publish; the registry would reject it` };
|
|
40
|
-
return { publish: true, reason: `publishing ${local} (npm currently has ${published})` };
|
|
41
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// turtle.mjs — a very small Turtle reader for schema.ttl's OWN regular shape:
|
|
2
|
-
// each class is one `:Name a rdfs:Class ;` block terminated by a line ending
|
|
3
|
-
// in ` .`, with `rdfs:label`/`rdfs:comment`/`rdfs:subClassOf` as `;`-separated
|
|
4
|
-
// predicate lines. Not a general Turtle parser — schema.ttl's own generator
|
|
5
|
-
// emits a single, very regular style (confirmed by direct inspection).
|
|
6
|
-
//
|
|
7
|
-
// Pure: text in, Map out, no imports.
|
|
8
|
-
|
|
9
|
-
/** Every rdfs:Class in `text`, as name -> { name, label, comment, subClassOf }.
|
|
10
|
-
* A class with no rdfs:label falls back to its own name; no rdfs:comment
|
|
11
|
-
* yields "". Blocks that are not classes (properties, say) are skipped. */
|
|
12
|
-
export function parseSchemaClasses(text) {
|
|
13
|
-
const classes = new Map();
|
|
14
|
-
const blocks = text.split(/\n(?=:[A-Za-z])/); // each class/property starts a new top-level block
|
|
15
|
-
for (const block of blocks) {
|
|
16
|
-
const head = /^:([A-Za-z0-9_]+)\s+a\s+rdfs:Class\s*;/.exec(block);
|
|
17
|
-
if (!head) continue;
|
|
18
|
-
const name = head[1];
|
|
19
|
-
const label = /rdfs:label\s+"([^"]*)"/.exec(block)?.[1] || name;
|
|
20
|
-
const comment = /rdfs:comment\s+"([^"]*)"/.exec(block)?.[1] || "";
|
|
21
|
-
const subClassOf = [...block.matchAll(/rdfs:subClassOf\s+:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
|
|
22
|
-
classes.set(name, { name, label, comment, subClassOf });
|
|
23
|
-
}
|
|
24
|
-
return classes;
|
|
25
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// parse.mjs — a targeted reader for SemCor's own regular YAML shape:
|
|
2
|
-
// flow-style lemmas/pos arrays and a folded single-quoted `text` scalar, one
|
|
3
|
-
// record per sentence. Not a general YAML parser (this repo has no YAML
|
|
4
|
-
// dependency), and not the same shape as the WordNet dump's reader in
|
|
5
|
-
// src/domain/wordnet/yaml.mjs — SemCor's flow style is JSON-compatible once
|
|
6
|
-
// isolated, which the WordNet subset never is.
|
|
7
|
-
//
|
|
8
|
-
// Pure: text in, arrays/strings out, no imports, so it runs with no SemCor
|
|
9
|
-
// clone present.
|
|
10
|
-
|
|
11
|
-
/** Split a SemCor YAML file into per-sentence record blocks (top-level
|
|
12
|
-
* "<key>:" lines, skipping the leading "_meta:" schema block). */
|
|
13
|
-
export function splitRecords(text) {
|
|
14
|
-
const lines = text.split("\n");
|
|
15
|
-
const blocks = [];
|
|
16
|
-
let i = 0;
|
|
17
|
-
while (i < lines.length && lines[i] !== "_meta:") i++;
|
|
18
|
-
i += 1;
|
|
19
|
-
while (i < lines.length && (lines[i].startsWith(" ") || lines[i].trim() === "")) i++; // skip rest of _meta
|
|
20
|
-
while (i < lines.length) {
|
|
21
|
-
if (/^[A-Za-z0-9_]+:$/.test(lines[i])) {
|
|
22
|
-
let j = i + 1;
|
|
23
|
-
const block = [];
|
|
24
|
-
while (j < lines.length && !/^[A-Za-z0-9_]+:$/.test(lines[j])) {
|
|
25
|
-
block.push(lines[j]);
|
|
26
|
-
j += 1;
|
|
27
|
-
}
|
|
28
|
-
blocks.push(block.join("\n"));
|
|
29
|
-
i = j;
|
|
30
|
-
} else {
|
|
31
|
-
i += 1;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return blocks;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Extract a flow-style JSON-compatible array value for `key` from one
|
|
38
|
-
* record block (lemmas/pos are double-quoted string arrays — valid JSON
|
|
39
|
-
* once isolated), balancing brackets across a line wrap if one occurs. */
|
|
40
|
-
export function extractArray(block, key) {
|
|
41
|
-
const re = new RegExp(`^\\s*${key}:\\s*(\\[.*)$`, "m");
|
|
42
|
-
const m = re.exec(block);
|
|
43
|
-
if (!m) return null;
|
|
44
|
-
let buf = m[1];
|
|
45
|
-
let depth = (buf.match(/\[/g) || []).length - (buf.match(/\]/g) || []).length;
|
|
46
|
-
const afterIdx = block.indexOf(m[0]) + m[0].length;
|
|
47
|
-
const rest = block.slice(afterIdx).split("\n");
|
|
48
|
-
let ri = 0;
|
|
49
|
-
while (depth > 0 && ri < rest.length) {
|
|
50
|
-
buf += `\n${rest[ri]}`;
|
|
51
|
-
depth += (rest[ri].match(/\[/g) || []).length - (rest[ri].match(/\]/g) || []).length;
|
|
52
|
-
ri += 1;
|
|
53
|
-
}
|
|
54
|
-
try { return JSON.parse(buf); } catch { return null; }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Extract the `text:` folded single-quoted scalar (YAML's own `''` ->
|
|
58
|
-
* literal `'` escape; line breaks folded to spaces). */
|
|
59
|
-
export function extractText(block) {
|
|
60
|
-
const m = /^\s*text:\s*'/m.exec(block);
|
|
61
|
-
if (!m) return null;
|
|
62
|
-
const start = block.indexOf("'", m.index);
|
|
63
|
-
let i = start + 1;
|
|
64
|
-
let raw = "";
|
|
65
|
-
while (i < block.length) {
|
|
66
|
-
if (block[i] === "'") {
|
|
67
|
-
if (block[i + 1] === "'") { raw += "'"; i += 2; continue; }
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
raw += block[i];
|
|
71
|
-
i += 1;
|
|
72
|
-
}
|
|
73
|
-
return raw.replace(/\s+/g, " ").trim();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export const NOUN_POS = new Set(["NN", "NNS"]);
|
|
77
|
-
|
|
78
|
-
/** Simple-grammar filter: short, no semicolons/colons, no embedded quotes
|
|
79
|
-
* (which signal reported speech), no more than one comma — a rough proxy for
|
|
80
|
-
* "no complex embedded clauses". */
|
|
81
|
-
export function isSimpleSentence(text, wordCount) {
|
|
82
|
-
if (wordCount > 18) return false;
|
|
83
|
-
if (/[;:]/.test(text)) return false;
|
|
84
|
-
if ((text.match(/,/g) || []).length > 1) return false;
|
|
85
|
-
if (/"/.test(text)) return false;
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// version-stamp.mjs — the home page's #pkg-version element, written and read
|
|
2
|
-
// from one place. Pure: strings in, strings out, no imports, so the deploy
|
|
3
|
-
// smoke check can reach it without npm ci.
|
|
4
|
-
//
|
|
5
|
-
// This existed three times and the copies had already drifted: the writer
|
|
6
|
-
// matched [^<]*, the smoke check demanded \d+\.\d+\.\d+, and the estate test
|
|
7
|
-
// accepted [^<\s]*. A writer that accepts what its reader rejects is a green
|
|
8
|
-
// build and a failed deploy, so the pattern lives here and all three call it.
|
|
9
|
-
|
|
10
|
-
/** The element that carries the version, and the value inside it. */
|
|
11
|
-
const STAMP = /(id="pkg-version"[^>]*>)\s*v?([^<]*?)\s*(<)/;
|
|
12
|
-
|
|
13
|
-
/** A semver core, which is what the deploy smoke check is entitled to expect. */
|
|
14
|
-
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
15
|
-
|
|
16
|
-
/** True iff `html` carries an element the stamp can be written into. */
|
|
17
|
-
export function hasVersionStamp(html) {
|
|
18
|
-
return STAMP.test(html);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The version `html` displays, or null when the element is absent or holds
|
|
22
|
-
* something that is not a semver core (an unstamped placeholder, say). */
|
|
23
|
-
export function parseVersionStamp(html) {
|
|
24
|
-
const found = STAMP.exec(html);
|
|
25
|
-
if (!found) return null;
|
|
26
|
-
const value = found[2].trim();
|
|
27
|
-
return SEMVER.test(value) ? value : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** `html` with the stamp set to `version`. Throws when there is nothing to
|
|
31
|
-
* stamp — a page that lost its element would otherwise publish blank. */
|
|
32
|
-
export function stampVersion(html, version) {
|
|
33
|
-
if (!SEMVER.test(version)) throw new Error(`not a stampable version: "${version}"`);
|
|
34
|
-
if (!hasVersionStamp(html)) throw new Error("no #pkg-version element to stamp");
|
|
35
|
-
return html.replace(STAMP, `$1${version}$3`);
|
|
36
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// yaml.mjs — a reader for the small YAML subset the Open English WordNet dump
|
|
2
|
-
// uses: 2-space-indented block mappings/sequences, quoted or bare scalars, and
|
|
3
|
-
// long scalar list-items that simply WRAP onto a further-indented continuation
|
|
4
|
-
// line. No block scalars, no anchors, no flow style — confirmed by direct
|
|
5
|
-
// inspection of the dump. This reads exactly that subset; it is not a general
|
|
6
|
-
// YAML parser.
|
|
7
|
-
//
|
|
8
|
-
// Pure: text in, object out, no imports, so it is testable without the WordNet
|
|
9
|
-
// clone the scripts that call it need.
|
|
10
|
-
|
|
11
|
-
/** Non-greedy key group so a MULTI-WORD entry key ("M-1 rifle", "ice cream")
|
|
12
|
-
* still matches — the first ": "/end-of-line colon wins, exactly as real
|
|
13
|
-
* YAML's block-mapping key/value split works. A plain wrapped scalar
|
|
14
|
-
* continuation line (a definition/example fragment) only coincidentally
|
|
15
|
-
* matches this if it ALSO happens to contain a bare "word: " sequence — rare
|
|
16
|
-
* in this corpus's prose, and this feeds a maintainer worksheet whose output
|
|
17
|
-
* is hand-reviewed, not a correctness-critical parser. */
|
|
18
|
-
const KEY_RE = /^(.+?):(\s+(.*)|)$/;
|
|
19
|
-
|
|
20
|
-
const isDash = (t) => t === "-" || t.startsWith("- ");
|
|
21
|
-
|
|
22
|
-
function parseScalar(s) {
|
|
23
|
-
const t = s.trim();
|
|
24
|
-
if ((t.startsWith("'") && t.endsWith("'") && t.length >= 2) || (t.startsWith('"') && t.endsWith('"') && t.length >= 2)) {
|
|
25
|
-
return t.slice(1, -1);
|
|
26
|
-
}
|
|
27
|
-
return t;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function parseYaml(text) {
|
|
31
|
-
const rawLines = text.split("\n");
|
|
32
|
-
const lines = [];
|
|
33
|
-
for (const line of rawLines) {
|
|
34
|
-
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
35
|
-
const indent = line.length - line.trimStart().length;
|
|
36
|
-
lines.push({ indent, text: line.trimStart() });
|
|
37
|
-
}
|
|
38
|
-
let pos = 0;
|
|
39
|
-
|
|
40
|
-
// A scalar that may continue on subsequent MORE-indented lines with no
|
|
41
|
-
// "key:"/"- " marker of their own (WordNet's definition-wrapping style). A
|
|
42
|
-
// QUOTED scalar ('...' or "...") is handled separately: WordNet definitions
|
|
43
|
-
// routinely contain a literal ": " inside the quoted text itself (e.g. "…
|
|
44
|
-
// Matthew, Mark, Luke, and John" split across a line boundary right after a
|
|
45
|
-
// colon) — the bare-scalar heuristic below would misread that continuation
|
|
46
|
-
// line as a new "key:" line and truncate the string. Once inside an open
|
|
47
|
-
// quote, EVERY line is a continuation until one ends with the matching
|
|
48
|
-
// closing quote, full stop — the key/dash heuristic never applies inside it.
|
|
49
|
-
function parseScalarOrContinue(first, minContinIndent) {
|
|
50
|
-
const trimmed = first.trim();
|
|
51
|
-
const quote = trimmed[0] === "'" || trimmed[0] === '"' ? trimmed[0] : null;
|
|
52
|
-
if (quote) {
|
|
53
|
-
const closes = (s) => s.length >= 2 && s.endsWith(quote);
|
|
54
|
-
let buf = trimmed;
|
|
55
|
-
while (!closes(buf) && pos < lines.length && lines[pos].indent >= minContinIndent) {
|
|
56
|
-
buf += " " + lines[pos].text.trim();
|
|
57
|
-
pos += 1;
|
|
58
|
-
}
|
|
59
|
-
return closes(buf) ? buf.slice(1, -1) : buf;
|
|
60
|
-
}
|
|
61
|
-
let s = parseScalar(first);
|
|
62
|
-
while (pos < lines.length && lines[pos].indent >= minContinIndent
|
|
63
|
-
&& !isDash(lines[pos].text) && !KEY_RE.test(lines[pos].text)) {
|
|
64
|
-
s += " " + lines[pos].text.trim();
|
|
65
|
-
pos += 1;
|
|
66
|
-
}
|
|
67
|
-
return s;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The value that follows a "key:" (bare, no inline scalar) — peeks at the
|
|
71
|
-
* next line to decide whether it's a nested sequence (which YAML allows to
|
|
72
|
-
* sit at the SAME indent as the key itself, not just deeper) or a nested
|
|
73
|
-
* mapping (which must be deeper) or simply absent (null). `parentIndent`
|
|
74
|
-
* is the indent of the "key:" line whose value this resolves. */
|
|
75
|
-
function parseValue(parentIndent) {
|
|
76
|
-
if (pos >= lines.length || lines[pos].indent < parentIndent) return null;
|
|
77
|
-
const line = lines[pos];
|
|
78
|
-
if (isDash(line.text)) return parseSeq(line.indent);
|
|
79
|
-
if (line.indent > parentIndent && KEY_RE.test(line.text)) return parseMap(line.indent);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function parseSeq(indent) {
|
|
84
|
-
const arr = [];
|
|
85
|
-
while (pos < lines.length && lines[pos].indent === indent && isDash(lines[pos].text)) {
|
|
86
|
-
const dashIndent = indent;
|
|
87
|
-
const rest = lines[pos].text === "-" ? "" : lines[pos].text.slice(2);
|
|
88
|
-
pos += 1;
|
|
89
|
-
if (rest === "") {
|
|
90
|
-
arr.push(parseValue(dashIndent));
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
// A quoted scalar is classified FIRST, unconditionally — WordNet
|
|
94
|
-
// definitions routinely contain a literal ": " (or a colon at the very
|
|
95
|
-
// end of a wrapped line, e.g. "…including:\n whales, …") inside quoted
|
|
96
|
-
// prose, which KEY_RE would otherwise misread as an inline "- key:"
|
|
97
|
-
// mapping. Only an UNQUOTED rest is even considered for that shape.
|
|
98
|
-
const quoted = rest[0] === "'" || rest[0] === '"';
|
|
99
|
-
const m = quoted ? null : KEY_RE.exec(rest);
|
|
100
|
-
if (m) {
|
|
101
|
-
// "- key: value" or "- key:" — an inline mapping for this list item;
|
|
102
|
-
// sibling keys of the SAME item are indented +2 from the dash.
|
|
103
|
-
const obj = {};
|
|
104
|
-
obj[m[1]] = m[3] !== undefined && m[3] !== "" ? parseScalarOrContinue(m[3], dashIndent + 2) : parseValue(dashIndent + 2);
|
|
105
|
-
while (pos < lines.length && lines[pos].indent === dashIndent + 2 && KEY_RE.test(lines[pos].text)) {
|
|
106
|
-
const mm = KEY_RE.exec(lines[pos].text);
|
|
107
|
-
pos += 1;
|
|
108
|
-
obj[mm[1]] = mm[3] !== undefined && mm[3] !== "" ? parseScalarOrContinue(mm[3], dashIndent + 4) : parseValue(dashIndent + 2);
|
|
109
|
-
}
|
|
110
|
-
arr.push(obj);
|
|
111
|
-
} else {
|
|
112
|
-
// a plain (or quoted) scalar list item — may wrap onto continuation lines
|
|
113
|
-
arr.push(parseScalarOrContinue(rest, dashIndent + 2));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return arr;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function parseMap(indent) {
|
|
120
|
-
const obj = {};
|
|
121
|
-
while (pos < lines.length && lines[pos].indent === indent && KEY_RE.test(lines[pos].text)) {
|
|
122
|
-
const m = KEY_RE.exec(lines[pos].text);
|
|
123
|
-
const key = parseScalar(m[1]);
|
|
124
|
-
pos += 1;
|
|
125
|
-
obj[key] = m[3] !== undefined && m[3] !== "" ? parseScalarOrContinue(m[3], indent + 2) : parseValue(indent);
|
|
126
|
-
// (parseValue(indent) — not indent+2 — so a same-indent sequence value
|
|
127
|
-
// is recognized; parseValue itself accepts child indent >= indent.)
|
|
128
|
-
}
|
|
129
|
-
return obj;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return parseMap(0);
|
|
133
|
-
}
|