@polycode-projects/the-mechanical-code-talker 1.5.4 → 1.8.3
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 +123 -14
- package/ROADMAP.md +233 -1392
- package/bin/tmct.mjs +479 -98
- package/corpus/README.md +3 -0
- package/corpus/generated/README.md +43 -0
- package/corpus/generated/ace-surface-variants.jsonl +17 -0
- package/corpus/generated/manifest.json +9 -0
- package/corpus/tier2/generate.mjs +14668 -0
- package/corpus/tier2/human-examples-large.jsonl +1928 -0
- package/corpus/tier2/human-examples-medium.jsonl +356 -0
- package/corpus/tier2/human-examples.jsonl +120 -0
- package/corpus/tier2/human-large.jsonl +12001 -0
- package/corpus/tier2/human-medium.jsonl +944 -0
- package/corpus/tier2/human.jsonl +664 -0
- package/corpus/tier2/manifest.json +42 -0
- package/package.json +14 -8
- package/src/answer-variants.json +47 -0
- package/src/answer-variants.mjs +67 -0
- package/src/ask-browser-entry.mjs +34 -0
- package/src/ask-browser.bundle.js +5095 -0
- package/src/ask-vocab.mjs +93 -8
- package/src/ask.mjs +451 -49
- package/src/chat.mjs +1391 -141
- package/src/cli-args.mjs +164 -0
- package/src/codegraph.mjs +170 -32
- package/src/completions/graph-adapter.mjs +118 -0
- package/src/extensions.mjs +100 -19
- package/src/grammar/ace.mjs +85 -3
- package/src/grammar/lexicon-core.json +9531 -63
- package/src/grammar/lexicon.mjs +58 -8
- package/src/graph-merge.mjs +114 -0
- package/src/index.mjs +14 -0
- package/src/init.mjs +40 -14
- package/src/interpret/normalize.mjs +88 -3
- package/src/interpret/strategies/grammar.mjs +10 -0
- package/src/interpret/strategies/keywords.mjs +20 -0
- package/src/interpret/strategies/noise-strip.mjs +73 -4
- package/src/memory/core.mjs +466 -8
- package/src/router/goal-reasoner.mjs +41 -7
- package/src/router/guardrail.mjs +37 -7
- package/src/router/resolver.mjs +50 -4
- package/src/sessions.mjs +5 -1
- package/src/source.mjs +54 -1
- package/src/syllogise.mjs +398 -27
- package/src/toml-config.mjs +13 -4
- package/src/viz.mjs +541 -0
package/src/extensions.mjs
CHANGED
|
@@ -5,23 +5,41 @@
|
|
|
5
5
|
//
|
|
6
6
|
// resolveExtensions(repoRoot) → { entries: Map<name, ResolvedEntry>, biasByBundle }
|
|
7
7
|
//
|
|
8
|
-
// BUILTIN_EXTENSIONS
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// BUILTIN_EXTENSIONS' DEFAULT ACTIVE BUNDLE IS `human` (PLAN_SEED.md, the
|
|
9
|
+
// persona flip): a fresh repo now seeds an everyday-world vocabulary (people,
|
|
10
|
+
// places, objects, nature, time/events, body/food, mind — hand-curated from
|
|
11
|
+
// Open English WordNet + Schema.org) rather than the old implicit code-domain
|
|
12
|
+
// default. `seon` and `conceptnet` are SHIPPED but now INACTIVE — both are
|
|
13
|
+
// equally code/tech-domain-biased (conceptnet's committed slice was filtered
|
|
14
|
+
// via a tech-domain seed-term match, PLAN_SEED.md §2), so BOTH flip together,
|
|
15
|
+
// not just seon — a repo that wants the old behavior asks for it explicitly
|
|
16
|
+
// (`tmct init --with-persona code`, or `[extensions.seon]`/`[extensions.
|
|
17
|
+
// conceptnet]` `active = true`). Four more shipped-but-INACTIVE tier-2 bundles
|
|
18
|
+
// (`tier2-aws` / `tier2-python` / `tier2-java` / `tier2-general`) round out the
|
|
19
|
+
// catalog. Activating any of these is a config-only edit (`tmct init --corpus
|
|
12
20
|
// aws`, or a `[extensions.tier2-aws] active = true` in tmct.toml) — zero code
|
|
13
|
-
// change. `tier2-general` (PLAN_AGENTS.md
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
21
|
+
// change. `tier2-general`'s own 49-fact animal/weather set (PLAN_AGENTS.md
|
|
22
|
+
// Phase 1) is superseded IN DEFAULT ROLE by `human`'s much larger
|
|
23
|
+
// `human-nature` clump, but stays shipped/selectable on its own for a caller
|
|
24
|
+
// that wants that narrow slice without the rest of the human persona.
|
|
25
|
+
//
|
|
26
|
+
// `human-medium`/`human-large` (also shipped-but-INACTIVE) are SIZE TIERS of
|
|
27
|
+
// the SAME `human` bundle, not separate personas (PLAN_SEED.md §3) — each
|
|
28
|
+
// holds only the facts that size adds beyond the previous one. Activated
|
|
29
|
+
// together via `tmct init --persona-size medium|large` (bin/tmct.mjs), which
|
|
30
|
+
// resolves them through this same registry.
|
|
17
31
|
//
|
|
18
32
|
// A `tmct.toml` may carry a top-level `[extensions]` table-of-tables
|
|
19
33
|
// (`[extensions.tier2-aws]`, …): a RECOGNIZED name (one of the builtins above)
|
|
20
34
|
// may override `active`/paths/etc; an UNRECOGNIZED name declares a brand new
|
|
21
|
-
// host entry and MUST carry a `kind` (corpus | lexicon | templates | pack
|
|
22
|
-
// `pack` entry may combine any of corpus_path/lexicon_path/
|
|
23
|
-
// phrasebook_path under one `active` flag and one provenance
|
|
24
|
-
// third-party vocabulary package hands tmct.
|
|
35
|
+
// host entry and MUST carry a `kind` (corpus | lexicon | templates | pack |
|
|
36
|
+
// ontology) — a `pack` entry may combine any of corpus_path/lexicon_path/
|
|
37
|
+
// templates_path/phrasebook_path under one `active` flag and one provenance
|
|
38
|
+
// name, the shape a third-party vocabulary package hands tmct. `ontology` is a
|
|
39
|
+
// DISTINCT, nameable kind for an ontology bundle (as opposed to a plain
|
|
40
|
+
// `corpus` bundle) — its `ontology_path` key is just an alias populating the
|
|
41
|
+
// SAME internal `corpusPath` field a corpus entry uses, so every downstream
|
|
42
|
+
// seeder/loader needs zero branching by kind name.
|
|
25
43
|
//
|
|
26
44
|
// A SEPARATE top-level `[bias]` table (flat: bundle-name → number) feeds
|
|
27
45
|
// src/memory/bias.mjs's ranking — never nested under `[extensions.*]`.
|
|
@@ -45,7 +63,7 @@ import {
|
|
|
45
63
|
toFacts,
|
|
46
64
|
} from "./corpus/conceptnet.mjs";
|
|
47
65
|
|
|
48
|
-
export const EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack"]);
|
|
66
|
+
export const EXTENSION_KINDS = Object.freeze(["corpus", "lexicon", "templates", "pack", "ontology"]);
|
|
49
67
|
|
|
50
68
|
// The definitional-band-first predicate order chat.mjs's bootstrap has always
|
|
51
69
|
// passed for the ConceptNet seed (SEED_PREFER) — re-declared here (not
|
|
@@ -57,15 +75,20 @@ const CONCEPTNET_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:pa
|
|
|
57
75
|
* accidentally mutate a module-level singleton. */
|
|
58
76
|
function builtinExtensions() {
|
|
59
77
|
return {
|
|
78
|
+
// WAS active:true (the implicit code-domain default) — now opt-in.
|
|
79
|
+
// PLAN_SEED.md §2: re-activate explicitly (`tmct init --with-persona
|
|
80
|
+
// code`, or `[extensions.seon] active = true`) for the old behavior.
|
|
60
81
|
seon: {
|
|
61
82
|
kind: "corpus",
|
|
62
|
-
active:
|
|
83
|
+
active: false,
|
|
63
84
|
corpusPath: SEON_CONCEPTS_FILE,
|
|
64
85
|
provenancePrefix: "corpus:seon",
|
|
65
86
|
},
|
|
87
|
+
// WAS active:true — now opt-in too, not just seon (PLAN_SEED.md §2: the
|
|
88
|
+
// committed slice is itself tech-domain-filtered, equally biased).
|
|
66
89
|
conceptnet: {
|
|
67
90
|
kind: "corpus",
|
|
68
|
-
active:
|
|
91
|
+
active: false,
|
|
69
92
|
corpusPath: CONCEPTNET_SLICE_FILE,
|
|
70
93
|
provenancePrefix: "corpus:conceptnet",
|
|
71
94
|
// matches chat.mjs's seedBootstrapMemory exactly: uncapped, definitional
|
|
@@ -73,6 +96,40 @@ function builtinExtensions() {
|
|
|
73
96
|
limit: undefined,
|
|
74
97
|
prefer: CONCEPTNET_PREFER,
|
|
75
98
|
},
|
|
99
|
+
// NEW — the default active bundle (PLAN_SEED.md). Everyday-world
|
|
100
|
+
// vocabulary: people, places, objects, nature, time/events, body/food,
|
|
101
|
+
// mind, plus the human-base/human-bridge scaffolding connecting WordNet's
|
|
102
|
+
// and Schema.org's independently-built taxonomies (PLAN_SEED.md §3, §8).
|
|
103
|
+
human: {
|
|
104
|
+
kind: "corpus",
|
|
105
|
+
active: true,
|
|
106
|
+
corpusPath: join(TIER2_DIR, "human.jsonl"),
|
|
107
|
+
provenancePrefix: "corpus:human",
|
|
108
|
+
},
|
|
109
|
+
// NEW — Medium/Large SIZE tiers of the SAME `human` bundle (PLAN_SEED.md
|
|
110
|
+
// §3), not separate personas: each file holds ONLY the facts that size
|
|
111
|
+
// adds beyond the previous one (Medium beyond Small, Large beyond
|
|
112
|
+
// Medium), so activating them is purely ADDITIVE alongside `human`
|
|
113
|
+
// (never a replacement for it). Both ship INACTIVE — Small stays the
|
|
114
|
+
// unconditional default — and are activated together via `tmct init
|
|
115
|
+
// --persona-size medium|large` (bin/tmct.mjs), which resolves them
|
|
116
|
+
// through this SAME BUILTIN_EXTENSIONS lookup and the ordinary
|
|
117
|
+
// `--corpus <id>` activation seam (activatePluggableInput). "large"
|
|
118
|
+
// activates BOTH human-medium and human-large (Large's facts are
|
|
119
|
+
// Medium's plus its own — both bundles must be active to reach the
|
|
120
|
+
// full ~13,600-fact total).
|
|
121
|
+
"human-medium": {
|
|
122
|
+
kind: "corpus",
|
|
123
|
+
active: false,
|
|
124
|
+
corpusPath: join(TIER2_DIR, "human-medium.jsonl"),
|
|
125
|
+
provenancePrefix: "corpus:human-medium",
|
|
126
|
+
},
|
|
127
|
+
"human-large": {
|
|
128
|
+
kind: "corpus",
|
|
129
|
+
active: false,
|
|
130
|
+
corpusPath: join(TIER2_DIR, "human-large.jsonl"),
|
|
131
|
+
provenancePrefix: "corpus:human-large",
|
|
132
|
+
},
|
|
76
133
|
"tier2-aws": {
|
|
77
134
|
kind: "corpus",
|
|
78
135
|
active: false,
|
|
@@ -118,6 +175,9 @@ export function validateExtensionEntry(name, entry) {
|
|
|
118
175
|
if (entry.kind === "corpus" && !entry.corpusPath) {
|
|
119
176
|
throw new Error(`extension "${name}": a "corpus" entry needs corpus_path`);
|
|
120
177
|
}
|
|
178
|
+
if (entry.kind === "ontology" && !entry.corpusPath) {
|
|
179
|
+
throw new Error(`extension "${name}": an "ontology" entry needs ontology_path`);
|
|
180
|
+
}
|
|
121
181
|
if (entry.kind === "lexicon" && !entry.lexiconPath) {
|
|
122
182
|
throw new Error(`extension "${name}": a "lexicon" entry needs lexicon_path`);
|
|
123
183
|
}
|
|
@@ -154,6 +214,7 @@ function mergeExtensionEntry(name, builtin, override, repoRoot) {
|
|
|
154
214
|
};
|
|
155
215
|
const paths = [
|
|
156
216
|
["corpus_path", "corpusPath"],
|
|
217
|
+
["ontology_path", "corpusPath"], // alias: an "ontology" entry's own path key, same internal field as "corpus"
|
|
157
218
|
["lexicon_path", "lexiconPath"],
|
|
158
219
|
["templates_path", "templatesPath"],
|
|
159
220
|
["phrasebook_path", "phrasebookPath"],
|
|
@@ -184,9 +245,13 @@ function mergeExtensionEntry(name, builtin, override, repoRoot) {
|
|
|
184
245
|
* table (default {} — every bundle then ranks at bias 1, see bias.mjs).
|
|
185
246
|
* No `tmct.toml` (or one with no `[extensions]`/`[bias]` tables) resolves to
|
|
186
247
|
* exactly today's implicit seon+conceptnet default, byte-identical.
|
|
248
|
+
*
|
|
249
|
+
* `configFile` (optional): an explicit tmct.toml path override — `tmct extend
|
|
250
|
+
* --validate <dir> --config <path>` — read INSTEAD of `<repoRoot>/tmct.toml`;
|
|
251
|
+
* `repoRoot` still anchors every resource path (unchanged).
|
|
187
252
|
*/
|
|
188
|
-
export async function resolveExtensions(repoRoot) {
|
|
189
|
-
const raw = repoRoot ? await loadTomlConfig(repoRoot) : null;
|
|
253
|
+
export async function resolveExtensions(repoRoot, { configFile } = {}) {
|
|
254
|
+
const raw = repoRoot ? await loadTomlConfig(repoRoot, configFile ? { file: configFile } : {}) : null;
|
|
190
255
|
const defs = builtinExtensions();
|
|
191
256
|
const rawExtensions = (raw && raw.extensions && typeof raw.extensions === "object") ? raw.extensions : {};
|
|
192
257
|
const rawBias = (raw && raw.bias && typeof raw.bias === "object") ? raw.bias : {};
|
|
@@ -216,12 +281,19 @@ export async function resolveExtensions(repoRoot) {
|
|
|
216
281
|
|
|
217
282
|
// ---- Part 2: the unified corpus loader loop ---------------------------------
|
|
218
283
|
|
|
219
|
-
/** Seed every ACTIVE `corpus`-kind entry
|
|
284
|
+
/** Seed every ACTIVE `corpus`/`ontology`-kind entry, plus any ACTIVE `pack`-kind
|
|
285
|
+
* entry that declares a `corpusPath` (in the Map's own fixed order — seon,
|
|
220
286
|
* conceptnet, then the rest sorted by name) into `repo`'s memory, ONE
|
|
221
287
|
* seedMemory() call per bundle. Shared by chat.mjs's first-run bootstrap,
|
|
222
288
|
* `tmct init`'s seed step and `tmct init --corpus <id>` — so all three read
|
|
223
289
|
* the SAME loop instead of three independent hardcoded call sites.
|
|
224
290
|
*
|
|
291
|
+
* BUGFIX (this batch): a `pack`-kind entry's `corpusPath` used to be silently
|
|
292
|
+
* skipped here despite this module's own docblock claiming pack entries
|
|
293
|
+
* combine corpus_path/lexicon_path/etc — a pack's corpus facts never made it
|
|
294
|
+
* into memory. Fixed by seeding any active pack entry that declares a
|
|
295
|
+
* corpusPath, alongside corpus/ontology entries.
|
|
296
|
+
*
|
|
225
297
|
* FAILURE-TOLERANT per bundle (init.mjs's own doctrine: a missing/broken
|
|
226
298
|
* corpus degrades to "not seeded", never a crash): one bad third-party pack's
|
|
227
299
|
* seedMemory throw is CAUGHT and recorded as `perBundle[name].error` — logged
|
|
@@ -235,7 +307,16 @@ export async function seedActiveCorpusEntries(repo, entries) {
|
|
|
235
307
|
let skipped = 0;
|
|
236
308
|
let total = 0;
|
|
237
309
|
for (const [name, entry] of entries instanceof Map ? entries : new Map()) {
|
|
238
|
-
|
|
310
|
+
// PLAN_SEED.md §2 bug fix: a "pack"-kind entry with its own corpusPath
|
|
311
|
+
// combines corpus/lexicon/templates under one active flag (this module's
|
|
312
|
+
// own docblock says so) but was previously never actually seeded here —
|
|
313
|
+
// only bare `kind: "corpus"` entries were. Broadened, not narrowed: every
|
|
314
|
+
// existing `kind: "corpus"`/`kind: "ontology"` entry (seon/conceptnet/
|
|
315
|
+
// human/tier2-*) behaves identically to before; only a pack entry that
|
|
316
|
+
// DOES carry a corpusPath newly qualifies.
|
|
317
|
+
if (!entry.active) continue;
|
|
318
|
+
const seedable = entry.kind === "corpus" || entry.kind === "ontology" || (entry.kind === "pack" && entry.corpusPath);
|
|
319
|
+
if (!seedable) continue;
|
|
239
320
|
try {
|
|
240
321
|
const res = await seedMemory(repo, {
|
|
241
322
|
slicePath: entry.corpusPath,
|
package/src/grammar/ace.mjs
CHANGED
|
@@ -39,6 +39,11 @@ import {
|
|
|
39
39
|
predicateOf, numberOf, classify,
|
|
40
40
|
} from "./lexicon.mjs";
|
|
41
41
|
|
|
42
|
+
// "a"/"an" are the only ACE determiners that are grammatically SINGULAR-ONLY —
|
|
43
|
+
// "the" and a bare/no determiner are number-neutral (see resolveNP's
|
|
44
|
+
// singularOnly below, and lexicon.mjs's lookupNoun doc for what this prunes).
|
|
45
|
+
const SINGULAR_ONLY_DET = new Set(["a", "an"]);
|
|
46
|
+
|
|
42
47
|
export const PATTERN_SUB_CLASS_OF = "subClassOf";
|
|
43
48
|
export const PATTERN_TYPE_ASSERTION = "typeAssertion";
|
|
44
49
|
export const PATTERN_RELATION = "relation";
|
|
@@ -89,22 +94,29 @@ const stripDet = (tokens) =>
|
|
|
89
94
|
* Returns { term, individual, extras, unknown } — `term` null on a miss with
|
|
90
95
|
* the undeclared tokens in `unknown` (empty `unknown` = structurally
|
|
91
96
|
* unparseable phrase → the caller returns a hard null). `extras` carries the
|
|
92
|
-
* pattern-8 adjective triples (subclass axioms / hasValue restriction).
|
|
97
|
+
* pattern-8 adjective triples (subclass axioms / hasValue restriction).
|
|
98
|
+
*
|
|
99
|
+
* `singularOnly` is grammatical-agreement pruning (see lexicon.mjs's
|
|
100
|
+
* lookupNoun doc): true only when the ORIGINAL (pre-strip) phrase opened
|
|
101
|
+
* with "a"/"an" — the one signal that a singular-plural-fold collision
|
|
102
|
+
* (die/dice, person/people, tooth/teeth) can be resolved by, rather than
|
|
103
|
+
* silently committing to whichever the lexicon happens to fold to first. */
|
|
93
104
|
function resolveNP(lexicon, tokensIn) {
|
|
94
105
|
const ns = lexicon.ns;
|
|
106
|
+
const singularOnly = tokensIn.length > 1 && SINGULAR_ONLY_DET.has(tokensIn[0].toLowerCase());
|
|
95
107
|
const tokens = stripDet(tokensIn);
|
|
96
108
|
if (tokens.length === 1) {
|
|
97
109
|
const t = tokens[0];
|
|
98
110
|
const proper = lookupProperName(lexicon, t);
|
|
99
111
|
if (proper) return { term: `${ns}${proper}`, individual: true, extras: [], unknown: [] };
|
|
100
112
|
if (CODE_REF.test(t)) return { term: `${ns}${t}`, individual: true, extras: [], unknown: [] };
|
|
101
|
-
const noun = lookupNoun(lexicon, t);
|
|
113
|
+
const noun = lookupNoun(lexicon, t, { singularOnly });
|
|
102
114
|
if (noun) return { term: `${ns}${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
|
|
103
115
|
return { term: null, individual: false, extras: [], unknown: [t] };
|
|
104
116
|
}
|
|
105
117
|
if (tokens.length === 2) {
|
|
106
118
|
const adj = lookupAdjective(lexicon, tokens[0]);
|
|
107
|
-
const noun = lookupNoun(lexicon, tokens[1]);
|
|
119
|
+
const noun = lookupNoun(lexicon, tokens[1], { singularOnly });
|
|
108
120
|
if (adj && noun) {
|
|
109
121
|
const term = `${ns}${adj.lemma}-${noun.lemma}`;
|
|
110
122
|
const extras = [
|
|
@@ -178,6 +190,76 @@ function parseRelation(lexicon, toks, lower) {
|
|
|
178
190
|
return null;
|
|
179
191
|
}
|
|
180
192
|
|
|
193
|
+
// ---- ambiguity: breadth-first candidate parses, dead ends pruned, survivors
|
|
194
|
+
// surfaced rather than guessed (the operator's own framing — see
|
|
195
|
+
// PLAN_DID_YOU_SEE_HER_DUCK.md's Origin section). parseRelation just above is
|
|
196
|
+
// UNCHANGED — it is still the greedy, first-verb-position-wins fast path
|
|
197
|
+
// every existing caller keeps using, so every single-reading sentence (the
|
|
198
|
+
// overwhelming majority) is completely unaffected. parseRelationHits and
|
|
199
|
+
// parseAceAmbiguous below are a separate, ADDITIVE scan that a caller opts
|
|
200
|
+
// into only when it wants to know whether more than one reading survives. ----
|
|
201
|
+
|
|
202
|
+
/** Pattern 3 — EVERY verb-position split, not just the first: for each token
|
|
203
|
+
* index that lookupVerb recognizes, resolve both sides and keep it ONLY if
|
|
204
|
+
* it is a complete, valid parse (a genuine hit — a missOrNull/null split is
|
|
205
|
+
* a dead end, pruned here rather than surfaced as "ambiguity"). Duplicate
|
|
206
|
+
* logic with parseRelation is deliberate: parseRelation must stay byte-for-
|
|
207
|
+
* byte unchanged for every existing caller, so this is a standalone reader,
|
|
208
|
+
* not a refactor of shared internals. */
|
|
209
|
+
function parseRelationHits(lexicon, toks, lower) {
|
|
210
|
+
const hits = [];
|
|
211
|
+
for (let i = 1; i < toks.length - 1; i += 1) {
|
|
212
|
+
const verb = lookupVerb(lexicon, lower[i]);
|
|
213
|
+
if (!verb) continue;
|
|
214
|
+
let objStart = i + 1;
|
|
215
|
+
if (verb.prep) {
|
|
216
|
+
if (lower[objStart] !== verb.prep) continue;
|
|
217
|
+
objStart += 1;
|
|
218
|
+
if (objStart >= toks.length) continue;
|
|
219
|
+
}
|
|
220
|
+
const np1 = resolveNP(lexicon, toks.slice(0, i));
|
|
221
|
+
const np2 = resolveNP(lexicon, toks.slice(objStart));
|
|
222
|
+
if (np1.term == null || np2.term == null) continue; // dead end
|
|
223
|
+
hits.push({
|
|
224
|
+
i,
|
|
225
|
+
verbLemma: verb.lemma,
|
|
226
|
+
subject: np1.term,
|
|
227
|
+
object: np2.term,
|
|
228
|
+
result: hit(PATTERN_RELATION, [np1, np2], [
|
|
229
|
+
{ subject: np1.term, predicate: predicateOf(verb, lexicon.ns), object: np2.term, kind: "owl:ObjectProperty" },
|
|
230
|
+
]),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return hits;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Public ambiguity surface: parse `sentence` and, ONLY when more than one
|
|
237
|
+
* independent, COMPLETE relation-pattern reading survives (parseRelationHits
|
|
238
|
+
* above), return them all, each labeled by the token it read as the verb.
|
|
239
|
+
* Returns null for the overwhelming majority of sentences: anything not
|
|
240
|
+
* relation-shaped (mirrors parseAce's own dispatch gate exactly, so this
|
|
241
|
+
* only ever fires on a sentence parseAce would ALSO route to parseRelation),
|
|
242
|
+
* and any relation-shaped sentence with 0 or 1 surviving readings — the
|
|
243
|
+
* ordinary parseAce path is authoritative and untouched either way. */
|
|
244
|
+
export function parseAceAmbiguous(sentence, lexicon = loadLexicon()) {
|
|
245
|
+
const toks = tokenize(sentence);
|
|
246
|
+
if (toks.length < 4) return null; // 3 tokens: exactly one verb position is even possible
|
|
247
|
+
const lower = toks.map((t) => t.toLowerCase());
|
|
248
|
+
if (lower[0] === "every" || lower[0] === "no") return null;
|
|
249
|
+
if (/'s$/.test(lower[0]) && lower[0].length > 2) return null;
|
|
250
|
+
if (lower[0] === "the" && lower.includes("of") && lower.includes("is")) return null;
|
|
251
|
+
if (lower.indexOf("is") > 0) return null;
|
|
252
|
+
const hits = parseRelationHits(lexicon, toks, lower);
|
|
253
|
+
if (hits.length < 2) return null;
|
|
254
|
+
return {
|
|
255
|
+
pattern: PATTERN_RELATION,
|
|
256
|
+
sentence,
|
|
257
|
+
readings: hits.map(({ i, verbLemma, subject, object, result }) => ({
|
|
258
|
+
i, verbLemma, subject, object, ...result,
|
|
259
|
+
})),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
181
263
|
/** Pattern 8 (copula arm) — "X is ADJ": data adjective → datatype-property
|
|
182
264
|
* assertion; subclass adjective → rdf:type (individual) / rdfs:subClassOf. */
|
|
183
265
|
function adjectiveCopula(lexicon, pattern, np1, adj) {
|