@polycode-projects/the-mechanical-code-talker 2.0.3 → 2.3.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 +1 -1
- package/ROADMAP.md +27 -4
- package/bin/tmct.mjs +4 -5
- package/corpus/generated/ace-surface-variants.jsonl +1 -0
- package/corpus/generated/manifest.json +3 -3
- package/corpus/wordnet/generate.mjs +6 -7
- package/package.json +30 -2
- package/src/adapters/corpus/conceptnet.mjs +1 -1
- package/src/adapters/graph-build.mjs +3 -3
- package/src/adapters/memory/blocks.mjs +2 -2
- package/src/adapters/memory/core.mjs +5 -5
- package/src/adapters/providers/bootstrap.mjs +1 -1
- package/src/adapters/providers/fixture.mjs +1 -1
- package/src/adapters/toml-config.mjs +0 -1
- package/src/adapters/wink-model.mjs +1 -1
- package/src/adapters/wordnet-source.mjs +70 -0
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/ask-vocab.mjs +2 -2
- package/src/domain/ask.mjs +4 -4
- package/src/domain/codegraph.mjs +7 -71
- package/src/domain/corpus-matrix.mjs +87 -0
- package/src/domain/grammar/ace.mjs +11 -11
- package/src/domain/grammar/lexicon.mjs +3 -3
- package/src/domain/inflect.mjs +67 -0
- package/src/domain/interpret/fuzzy.mjs +1 -1
- package/src/domain/interpret/merge.mjs +1 -1
- package/src/domain/interpret/normalize.mjs +1 -1
- package/src/domain/licences.mjs +68 -0
- package/src/domain/markdown-links.mjs +55 -0
- package/src/domain/memory/capability.mjs +1 -1
- package/src/domain/memory/trust.mjs +2 -2
- package/src/domain/persona/codegen.mjs +123 -0
- package/src/domain/persona/examples.mjs +26 -0
- package/src/domain/persona/tiers.mjs +270 -0
- package/src/domain/publish-gate.mjs +41 -0
- package/src/domain/router/call-validator.mjs +1 -1
- package/src/domain/router/drive.mjs +3 -4
- package/src/domain/router/registry.mjs +12 -13
- package/src/domain/router/resolver.mjs +18 -5
- package/src/domain/router/results.mjs +3 -3
- package/src/domain/router/taught.mjs +4 -3
- package/src/domain/schemaorg/turtle.mjs +25 -0
- package/src/domain/semcor/parse.mjs +87 -0
- package/src/domain/syllogise.mjs +6 -6
- package/src/domain/version-stamp.mjs +36 -0
- package/src/domain/wordnet/yaml.mjs +133 -0
- package/src/services/chat-session.mjs +2 -2
- package/src/services/chat.mjs +2 -2
- package/src/services/cli-args.mjs +4 -4
- package/src/services/finish.mjs +1 -1
- package/src/services/ledger-viz.mjs +2 -3
- package/src/services/sessions.mjs +4 -4
- package/src/services/viz-theme.mjs +3 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +4 -94
- package/src/adapters/embed.mjs +0 -169
- package/src/domain/router/guardrail.mjs +0 -116
- package/src/domain/vector.mjs +0 -12
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// corpus-matrix.mjs — the fold and the two gap heuristics behind the
|
|
2
|
+
// capability-by-lane coverage matrix, plus the table renderer. Pure: rows in,
|
|
3
|
+
// counts and text out, so the heuristics can be tested against a handful of
|
|
4
|
+
// made-up rows instead of whatever test/corpus happens to hold today.
|
|
5
|
+
//
|
|
6
|
+
// scripts/corpus-matrix.mjs keeps the readdir, the readFile and the printing.
|
|
7
|
+
|
|
8
|
+
/** A row's capability group: the first two dot-segments of its key, so
|
|
9
|
+
* "ask.alias.two-hop" and "ask.alias.miss" are one capability. */
|
|
10
|
+
export const groupOfKey = (key) => key.split(".").slice(0, 2).join(".");
|
|
11
|
+
|
|
12
|
+
/** The key a row is counted under. A row with no key is still a row, and
|
|
13
|
+
* hiding it would understate the lane. */
|
|
14
|
+
export const keyOfRow = (row) => String(row.key ?? "(no key)");
|
|
15
|
+
|
|
16
|
+
/** A key segment naming a miss, a guard or a negation — the row that pins what
|
|
17
|
+
* a capability DECLINES to do, as opposed to its happy path. */
|
|
18
|
+
const NEGATIVE_RE = /(honest-miss|miss|guard|negation|negative|never|decline|refus|unsolvable|unknown|hedge|no-antecedent|untouched|empty)/;
|
|
19
|
+
|
|
20
|
+
export const isNegativeKey = (key) => NEGATIVE_RE.test(key);
|
|
21
|
+
|
|
22
|
+
/** Fold `{ lane, row }` pairs into the two indexes every view needs: the count
|
|
23
|
+
* per group per lane, and the full keys each group was built from. */
|
|
24
|
+
export function tallyRows(entries) {
|
|
25
|
+
const counts = new Map(); // group -> Map<lane, rowCount>
|
|
26
|
+
const fullKeys = new Map(); // group -> Set<full key>
|
|
27
|
+
for (const { lane, row } of entries) {
|
|
28
|
+
const key = keyOfRow(row);
|
|
29
|
+
const group = groupOfKey(key);
|
|
30
|
+
if (!counts.has(group)) counts.set(group, new Map());
|
|
31
|
+
const perLane = counts.get(group);
|
|
32
|
+
perLane.set(lane, (perLane.get(lane) ?? 0) + 1);
|
|
33
|
+
if (!fullKeys.has(group)) fullKeys.set(group, new Set());
|
|
34
|
+
fullKeys.get(group).add(key);
|
|
35
|
+
}
|
|
36
|
+
return { counts, fullKeys };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The groups the gap heuristics judge. bench.* rows assert a rig runs rather
|
|
40
|
+
* than pinning a capability, so "no negative row" says nothing there. */
|
|
41
|
+
export const behaviourGroups = ({ counts }) =>
|
|
42
|
+
[...counts.keys()].filter((g) => !g.startsWith("bench.")).sort();
|
|
43
|
+
|
|
44
|
+
const rowTotal = (counts, group) => [...counts.get(group).values()].reduce((a, b) => a + b, 0);
|
|
45
|
+
|
|
46
|
+
/** Groups a single row pins end to end. A review candidate, not a hole. */
|
|
47
|
+
export function thinGroups(tally) {
|
|
48
|
+
return behaviourGroups(tally).filter((g) => rowTotal(tally.counts, g) === 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Groups whose keys never name a miss, guard or negation — a happy path is
|
|
52
|
+
* pinned and the decline is not. A review candidate, not a hole. */
|
|
53
|
+
export function groupsWithNoNegativeRow(tally) {
|
|
54
|
+
return behaviourGroups(tally).filter((g) => ![...tally.fullKeys.get(g)].some(isNegativeKey));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The lanes a group has rows in, in the order given. */
|
|
58
|
+
export const lanesOfGroup = ({ counts }, group) => [...counts.get(group).keys()];
|
|
59
|
+
|
|
60
|
+
/** One row per group, one column per lane, an empty cell where a lane has no
|
|
61
|
+
* row for that group. The header row comes first. */
|
|
62
|
+
export function matrixRows({ counts }, lanes) {
|
|
63
|
+
const groups = [...counts.keys()].sort();
|
|
64
|
+
return [
|
|
65
|
+
["key", ...lanes],
|
|
66
|
+
...groups.map((group) => [
|
|
67
|
+
group,
|
|
68
|
+
...lanes.map((lane) => {
|
|
69
|
+
const n = counts.get(group).get(lane);
|
|
70
|
+
return n ? String(n) : "";
|
|
71
|
+
}),
|
|
72
|
+
]),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `rows` (header first) as fixed-width text, with a rule under the header.
|
|
77
|
+
* Each column is as wide as its widest cell; trailing padding is trimmed. */
|
|
78
|
+
export function renderTable(rows) {
|
|
79
|
+
const [header, ...body] = rows;
|
|
80
|
+
const widths = header.map((h, col) => Math.max(h.length, ...body.map((r) => r[col].length)));
|
|
81
|
+
const renderLine = (cells) => cells.map((c, col) => c.padEnd(widths[col])).join(" ").trimEnd();
|
|
82
|
+
return [
|
|
83
|
+
renderLine(header),
|
|
84
|
+
renderLine(widths.map((w) => "-".repeat(w))),
|
|
85
|
+
...body.map(renderLine),
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// punctuation, morphology is the lexicon's suffix fold.
|
|
9
9
|
//
|
|
10
10
|
// parseAce(sentence, lexicon) → { pattern, triples, residue } | null
|
|
11
|
-
// pattern one of the PATTERNS below
|
|
11
|
+
// pattern one of the PATTERNS below.
|
|
12
12
|
// triples [{ subject, predicate, object, kind, n? }] — OWL-labelled string
|
|
13
13
|
// triples shaped for src/adapters/memory/core.mjs's appendFact (which
|
|
14
14
|
// normalizes subject/object via normFactTerm: "tmct:module" is
|
|
@@ -36,18 +36,18 @@ import {
|
|
|
36
36
|
// singularOnly below, and lexicon.mjs's lookupNoun doc for what this prunes).
|
|
37
37
|
const SINGULAR_ONLY_DET = new Set(["a", "an"]);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
const PATTERN_SUB_CLASS_OF = "subClassOf";
|
|
40
|
+
const PATTERN_TYPE_ASSERTION = "typeAssertion";
|
|
41
|
+
const PATTERN_RELATION = "relation";
|
|
42
|
+
const PATTERN_SOME_VALUES_FROM = "someValuesFrom";
|
|
43
|
+
const PATTERN_CARDINALITY = "cardinality";
|
|
44
|
+
const PATTERN_DISJOINT_WITH = "disjointWith";
|
|
45
|
+
const PATTERN_POSSESSIVE = "possessive";
|
|
46
|
+
const PATTERN_ADJECTIVE = "adjective";
|
|
47
|
+
const PATTERN_CAPABILITY = "capability";
|
|
48
48
|
|
|
49
49
|
/** The pattern field's full domain, in the README's table order. */
|
|
50
|
-
|
|
50
|
+
const PATTERNS = Object.freeze([
|
|
51
51
|
PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM,
|
|
52
52
|
PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE,
|
|
53
53
|
PATTERN_CAPABILITY,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import coreLexiconRaw from "./lexicon-core.json" with { type: "json" };
|
|
26
26
|
|
|
27
27
|
/** The CURIE namespace every tmct lexicon mints terms under. */
|
|
28
|
-
|
|
28
|
+
const DEFAULT_NS = "tmct:";
|
|
29
29
|
|
|
30
30
|
/** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
|
|
31
31
|
export const DETERMINERS = Object.freeze({
|
|
@@ -160,7 +160,7 @@ export function lookupNoun(lexicon, word, opts = {}) {
|
|
|
160
160
|
/** Every lexicon entry `word` could plausibly resolve to, ranked the same as
|
|
161
161
|
* lookupNoun's top choice but without discarding a genuine alternate (e.g.
|
|
162
162
|
* die/dice returns both entries). */
|
|
163
|
-
|
|
163
|
+
function lookupNounCandidates(lexicon, word, opts = {}) {
|
|
164
164
|
const w = String(word ?? "").toLowerCase();
|
|
165
165
|
const standalone = lexicon.nouns.get(w);
|
|
166
166
|
const irregular = lexicon.nounPlurals.get(w);
|
|
@@ -195,7 +195,7 @@ export function lookupVerb(lexicon, word) {
|
|
|
195
195
|
|
|
196
196
|
/** Every verb entry `word` could plausibly resolve to via foldCandidates,
|
|
197
197
|
* most-specific-fold-first — the verb sibling of lookupNounCandidates. */
|
|
198
|
-
|
|
198
|
+
function lookupVerbCandidates(lexicon, word) {
|
|
199
199
|
const w = String(word ?? "").toLowerCase();
|
|
200
200
|
const out = [];
|
|
201
201
|
const seen = new Set();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// inflect.mjs — the regular English -s/-ed/-ing rules, applied to a lemma.
|
|
2
|
+
//
|
|
3
|
+
// WordNet carries lemmas only ("rest" is present, "rests" is absent), and it is
|
|
4
|
+
// the inflected forms that collide with the fuzzy repair tier's targets —
|
|
5
|
+
// "rests" is one edit from "tests". So the real-word collision table expands
|
|
6
|
+
// every lemma through these rules before it looks for collisions.
|
|
7
|
+
//
|
|
8
|
+
// These are the REGULAR rules and nothing else. No irregular table, no stress
|
|
9
|
+
// model: pastOf("run") is "runned" and pastOf("make") is "maked". That is the
|
|
10
|
+
// intended shape. The table's job is to name words the repair tier must not
|
|
11
|
+
// rewrite, and inflectionsOf is generous on purpose (see below) — an extra form
|
|
12
|
+
// costs one repair we decline to make, and the sentence misses honestly, while
|
|
13
|
+
// a missing form costs a real word rewritten into a different question,
|
|
14
|
+
// answered with confidence. The first is the cheaper mistake.
|
|
15
|
+
|
|
16
|
+
import { STOPWORDS } from "./interpret/normalize.mjs";
|
|
17
|
+
import {
|
|
18
|
+
FUZZY_TARGET_WORDS, FUZZY_REPAIR_MIN_LENGTH, fuzzyMatchInSet, fuzzyBound,
|
|
19
|
+
} from "./interpret/fuzzy.mjs";
|
|
20
|
+
|
|
21
|
+
const VOWELS = new Set(["a", "e", "i", "o", "u"]);
|
|
22
|
+
const isVowel = (c) => VOWELS.has(c);
|
|
23
|
+
|
|
24
|
+
/** A single final consonant after a single vowel doubles before -ed/-ing
|
|
25
|
+
* ("run" -> "running"). w, x and y never double. Stress is not modelled, so a
|
|
26
|
+
* second syllable doubles too ("visit" -> "visitting"). */
|
|
27
|
+
export function doublesFinalConsonant(w) {
|
|
28
|
+
const [c3, c2, c1] = [w.at(-3), w.at(-2), w.at(-1)];
|
|
29
|
+
if (!c3 || isVowel(c1) || "wxy".includes(c1)) return false;
|
|
30
|
+
return isVowel(c2) && !isVowel(c3);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pluralOf(w) {
|
|
34
|
+
if (/(?:s|x|z|ch|sh)$/.test(w)) return `${w}es`;
|
|
35
|
+
if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ies`;
|
|
36
|
+
return `${w}s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pastOf(w) {
|
|
40
|
+
if (w.endsWith("e")) return `${w}d`;
|
|
41
|
+
if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ied`;
|
|
42
|
+
if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ed`;
|
|
43
|
+
return `${w}ed`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function gerundOf(w) {
|
|
47
|
+
if (w.endsWith("ie")) return `${w.slice(0, -2)}ying`;
|
|
48
|
+
if (w.endsWith("e") && !/(?:ee|oe|ye)$/.test(w)) return `${w.slice(0, -1)}ing`;
|
|
49
|
+
if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ing`;
|
|
50
|
+
return `${w}ing`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Every surface form of `w` the collision table counts as real English. */
|
|
54
|
+
export const inflectionsOf = (w) => [w, pluralOf(w), pastOf(w), gerundOf(w)];
|
|
55
|
+
|
|
56
|
+
/** The words in `realWords` that the repair tier would rewrite onto one of its
|
|
57
|
+
* targets: long enough to reach the tier, not a stopword, not a target itself,
|
|
58
|
+
* and within the fuzzy bound of some target. Sorted, so the table it feeds is
|
|
59
|
+
* reproducible. */
|
|
60
|
+
export function collisionsFrom(realWords) {
|
|
61
|
+
return [...realWords]
|
|
62
|
+
.filter((w) => w.length >= FUZZY_REPAIR_MIN_LENGTH)
|
|
63
|
+
.filter((w) => !STOPWORDS.has(w))
|
|
64
|
+
.filter((w) => !FUZZY_TARGET_WORDS.includes(w))
|
|
65
|
+
.filter((w) => fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w)) !== null)
|
|
66
|
+
.sort();
|
|
67
|
+
}
|
|
@@ -80,7 +80,7 @@ const REAL_WORD_COLLISIONS = new Set(collisionData.words);
|
|
|
80
80
|
* what it says, and if we don't record that relation the honest answer is a
|
|
81
81
|
* miss. Only the words the tier could actually reach are tabled; anything else
|
|
82
82
|
* never gets this far. */
|
|
83
|
-
|
|
83
|
+
const isRealEnglishWord = (w) => REAL_WORD_COLLISIONS.has(w);
|
|
84
84
|
|
|
85
85
|
/** A query word may be canonicalized only if it is plain alphabetic, not a
|
|
86
86
|
* stopword, and not already vocabulary. Dotted/digit terms (file names, shas)
|
|
@@ -123,7 +123,7 @@ export function mergeStrategyResults(results) {
|
|
|
123
123
|
* for alternateLines. Template only, reads straight off the parsed fields (the
|
|
124
124
|
* same discipline as ask.mjs's describeParse; callers with richer noun tables
|
|
125
125
|
* may pass their own describe). */
|
|
126
|
-
|
|
126
|
+
function describeAlternate(p) {
|
|
127
127
|
if (!p) return "something else";
|
|
128
128
|
if (p.ambiguousParse) return "one of several readings";
|
|
129
129
|
const obj = p.object ?? p.subject ?? "?";
|
|
@@ -404,7 +404,7 @@ export function applyNegationFrames(text) {
|
|
|
404
404
|
// Phrasing frames: route natural phrasings of a members-of-class or
|
|
405
405
|
// where-defined question onto the canonical shape the grammar answers.
|
|
406
406
|
// First match wins; run after applyNegationFrames.
|
|
407
|
-
|
|
407
|
+
const PHRASING_FRAMES = Object.freeze([
|
|
408
408
|
// MEMBERS-of-class → "what does X contain".
|
|
409
409
|
{ re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:are|is)\s+(?:in|inside|within)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
410
410
|
{ re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:does|do)\s+(.+?)\s+have\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
|
@@ -55,7 +55,7 @@ export const NEG_CAPABLE_OF_PREDICATE = negatedPredicate(CAPABLE_OF_PREDICATE);
|
|
|
55
55
|
* order it lists them in. One constant each, in one place, so the verbosity of
|
|
56
56
|
* every case-4 answer is tuned by editing two lines. */
|
|
57
57
|
export const CAPABILITY_REPORT_CAP = 6;
|
|
58
|
-
|
|
58
|
+
const byTrustThenName = (a, b) => (b.trust || 0) - (a.trust || 0) || String(a.subject).localeCompare(String(b.subject));
|
|
59
59
|
|
|
60
60
|
const asSet = (v) => (v instanceof Set ? v : new Set(Array.isArray(v) ? v : [v]));
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ export const SOURCE_PRIOR = Object.freeze({
|
|
|
69
69
|
entailed: 0.3,
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
73
73
|
export const RECENCY_FLOOR = 0.9; // recency multiplier stays within [0.9, 1.0]
|
|
74
74
|
|
|
75
75
|
// Actor-level (session-scoped) trust — a bounded nudge on a Source's type
|
|
@@ -156,7 +156,7 @@ export function computeTrust(fact, sourcesById = {}, opts = {}) {
|
|
|
156
156
|
|
|
157
157
|
// Laplace/"add-k" pseudo-count: without it a single data point would saturate
|
|
158
158
|
// mgx:sourceReliability to the bare max/min immediately.
|
|
159
|
-
|
|
159
|
+
const RELIABILITY_CONFIDENCE_PSEUDOCOUNT = 19;
|
|
160
160
|
|
|
161
161
|
/** Pure actor-level reliability from a session's asserted-vs-contradicted
|
|
162
162
|
* track record (findContradictions, core.mjs), confidence-scaled by sample
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// examples.mjs — the two rules that decide whether a WordNet `example:` field
|
|
2
|
+
// can serve as a persona example sentence, and what its text actually is.
|
|
3
|
+
//
|
|
4
|
+
// Pure: values in, values out, no imports.
|
|
5
|
+
|
|
6
|
+
/** A handful of WordNet examples are cross-reference stubs ("see table 1"),
|
|
7
|
+
* not real sentences. Filtering them keeps a re-run reproducing the committed
|
|
8
|
+
* corpus/tier2/human-examples.jsonl exactly — this was the one candidate
|
|
9
|
+
* dropped by hand when that file was first curated. */
|
|
10
|
+
export const isRealSentence = (s) => !/^see\s+\w+\s*\d*\.?$/i.test(String(s).trim());
|
|
11
|
+
|
|
12
|
+
/** A WordNet `example:` is usually a plain string, but a few are an ATTRIBUTED
|
|
13
|
+
* LITERARY QUOTE — a `{source, text}` mapping, e.g. "ecstasy"'s example is
|
|
14
|
+
* `{source: "Charles Dickens", text: "listening to sweet music…"}`. That shape
|
|
15
|
+
* is real and was found live while extending coverage past Small tier's own
|
|
16
|
+
* 665-word list; none of Small's words happened to hit it, so it went uncaught
|
|
17
|
+
* until Medium/Large's much wider coverage.
|
|
18
|
+
*
|
|
19
|
+
* Returns the plain sentence text, or null for any other shape. The literary
|
|
20
|
+
* source is real but this corpus wants a plain example sentence, not a
|
|
21
|
+
* citation index. */
|
|
22
|
+
export function normalizeExample(example) {
|
|
23
|
+
if (typeof example === "string") return example;
|
|
24
|
+
if (example && typeof example === "object" && typeof example.text === "string") return example.text;
|
|
25
|
+
return null;
|
|
26
|
+
}
|