@polycode-projects/the-mechanical-code-talker 1.4.1 → 1.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 +1 -1
- package/ROADMAP.md +76 -11
- package/corpus/README.md +23 -22
- package/corpus/seon/README.md +7 -6
- package/corpus/seon/concepts.jsonl +119 -0
- package/corpus/tier2/general.jsonl +49 -0
- package/corpus/tier2/generate.mjs +68 -0
- package/corpus/tier2/manifest.json +14 -0
- package/data/templates/constructions/agent-noun-relations.toml +98 -0
- package/data/templates/responses.jsonl +1 -0
- package/package.json +5 -1
- package/src/ask-vocab.mjs +39 -1
- package/src/ask.mjs +278 -32
- package/src/chat.mjs +681 -212
- package/src/completions/complete.mjs +138 -0
- package/src/completions/group.mjs +171 -0
- package/src/completions/infer.mjs +395 -0
- package/src/completions/prune.mjs +156 -0
- package/src/completions/rank.mjs +154 -0
- package/src/completions/search.mjs +85 -0
- package/src/corpus/conceptnet.mjs +36 -3
- package/src/corpus/unknown-ingest.mjs +209 -0
- package/src/extensions.mjs +14 -4
- package/src/finish.mjs +61 -18
- package/src/grammar/ace.mjs +24 -338
- package/src/grammar/lexicon.mjs +37 -194
- package/src/interpret/pipeline.mjs +23 -2
- package/src/interpret/strategies/constructions.mjs +207 -0
- package/src/interpret/strategies/grammar.mjs +24 -3
- package/src/interpret/strategies/keywords.mjs +34 -0
- package/src/memory/blocks.mjs +7 -2
- package/src/memory/core.mjs +283 -20
- package/src/memory/shacl.mjs +114 -0
- package/src/prose.mjs +5 -1
- package/src/syllogise.mjs +0 -0
- package/src/grammar/lexicon-core.json +0 -302
package/src/grammar/ace.mjs
CHANGED
|
@@ -1,341 +1,27 @@
|
|
|
1
|
-
// grammar/ace.mjs —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// grammar/ace.mjs — thin re-export shim over @polycode-projects/ace-owl
|
|
2
|
+
// (PLAN_OSS_ACE_PARSER.md / PLAN_AGENTS.md §3's "ace-owl open-source
|
|
3
|
+
// extraction"). tmct's ACE-OWL sub-fragment parser — the 8 controlled-
|
|
4
|
+
// English sentence patterns of docs/references/schemas/ace-owl-fragment.md
|
|
5
|
+
// — is now the extracted package's ace.mjs (packages/ace-owl/src/ace.mjs);
|
|
6
|
+
// this file exists ONLY to default `lexicon` to tmct's own namespace-bound
|
|
7
|
+
// loadLexicon() (see ./lexicon.mjs), so every existing call site in this
|
|
8
|
+
// repo (chat.mjs, grammar/assert.mjs, the grammar tests, …) that calls
|
|
9
|
+
// `parseAce(sentence)` with no lexicon argument keeps getting "tmct:"-
|
|
10
|
+
// prefixed triples, byte-identical to before the extraction.
|
|
9
11
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// for nouns, canonical spelling for proper names, the literal token for
|
|
24
|
-
// code-shaped references like chat.mjs); predicates are the OWL/RDF(S)
|
|
25
|
-
// vocabulary terms or the lexicon verb's tmct:<3sg> predicate. Restriction
|
|
26
|
-
// and intersection class expressions get READABLE deterministic node names
|
|
27
|
-
// (tmct:some-imports-test, tmct:module-that-imports-test) instead of blank
|
|
28
|
-
// nodes, so the same sentence always re-emits the same triples and appendFact
|
|
29
|
-
// stays idempotent. An intersection is flattened to repeated
|
|
30
|
-
// owl:intersectionOf triples (one per member) — the flat-JSON stand-in for an
|
|
31
|
-
// RDF list, documented in ontology/tmct-core.ttl.
|
|
32
|
-
|
|
33
|
-
import {
|
|
34
|
-
loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName,
|
|
35
|
-
predicateOf, numberOf, classify,
|
|
36
|
-
} from "./lexicon.mjs";
|
|
37
|
-
|
|
38
|
-
const DET = new Set(["a", "an", "the"]);
|
|
39
|
-
// A token SHAPED like a code reference (a path, file, symbol or CURIE) is an
|
|
40
|
-
// individual by form — a deterministic tokenizer rule, not a guess: declared
|
|
41
|
-
// proper names cover words; this covers chat.mjs, src/ask.mjs, Foo#bar.
|
|
42
|
-
const CODE_REF = /[./\\#:@]/;
|
|
43
|
-
|
|
44
|
-
/** Whitespace tokenizer: curly quotes normalized, commas/semicolons dropped,
|
|
45
|
-
* ONE trailing punctuation run stripped (so "chat.mjs." keeps its dots). */
|
|
46
|
-
export function tokenize(sentence) {
|
|
47
|
-
return String(sentence ?? "")
|
|
48
|
-
.replace(/[‘’]/g, "'")
|
|
49
|
-
.replace(/[,;]/g, " ")
|
|
50
|
-
.replace(/[?!.]+\s*$/, "")
|
|
51
|
-
.trim()
|
|
52
|
-
.split(/\s+/)
|
|
53
|
-
.filter(Boolean);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const local = (term) => String(term).replace(/^tmct:/, "");
|
|
57
|
-
|
|
58
|
-
const stripDet = (tokens) =>
|
|
59
|
-
tokens.length > 1 && DET.has(tokens[0].toLowerCase()) ? tokens.slice(1) : tokens;
|
|
60
|
-
|
|
61
|
-
/** Resolve a 1–2 word noun phrase: PROPERNAME | code-ref | NOUN | ADJ NOUN.
|
|
62
|
-
* Returns { term, individual, extras, unknown } — `term` null on a miss with
|
|
63
|
-
* the undeclared tokens in `unknown` (empty `unknown` = structurally
|
|
64
|
-
* unparseable phrase → the caller returns a hard null). `extras` carries the
|
|
65
|
-
* pattern-8 adjective triples (subclass axioms / hasValue restriction). */
|
|
66
|
-
function resolveNP(lexicon, tokensIn) {
|
|
67
|
-
const tokens = stripDet(tokensIn);
|
|
68
|
-
if (tokens.length === 1) {
|
|
69
|
-
const t = tokens[0];
|
|
70
|
-
const proper = lookupProperName(lexicon, t);
|
|
71
|
-
if (proper) return { term: `tmct:${proper}`, individual: true, extras: [], unknown: [] };
|
|
72
|
-
if (CODE_REF.test(t)) return { term: `tmct:${t}`, individual: true, extras: [], unknown: [] };
|
|
73
|
-
const noun = lookupNoun(lexicon, t);
|
|
74
|
-
if (noun) return { term: `tmct:${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
|
|
75
|
-
return { term: null, individual: false, extras: [], unknown: [t] };
|
|
76
|
-
}
|
|
77
|
-
if (tokens.length === 2) {
|
|
78
|
-
const adj = lookupAdjective(lexicon, tokens[0]);
|
|
79
|
-
const noun = lookupNoun(lexicon, tokens[1]);
|
|
80
|
-
if (adj && noun) {
|
|
81
|
-
const term = `tmct:${adj.lemma}-${noun.lemma}`;
|
|
82
|
-
const extras = [
|
|
83
|
-
{ subject: term, predicate: "rdfs:subClassOf", object: `tmct:${noun.lemma}`, kind: "rdfs:subClassOf" },
|
|
84
|
-
];
|
|
85
|
-
if (adj.type === "subclass") {
|
|
86
|
-
// the adjective itself denotes a class: legacy-module ⊑ module, ⊑ legacy
|
|
87
|
-
extras.push({ subject: term, predicate: "rdfs:subClassOf", object: `tmct:${adj.lemma}`, kind: "rdfs:subClassOf" });
|
|
88
|
-
} else {
|
|
89
|
-
// data adjective: subclass-with-restriction on the boolean-ish property
|
|
90
|
-
const r = `tmct:has-${adj.lemma}`;
|
|
91
|
-
extras.push(
|
|
92
|
-
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: "owl:hasValue" },
|
|
93
|
-
{ subject: r, predicate: "owl:onProperty", object: adj.property || `tmct:${adj.lemma}`, kind: "owl:hasValue" },
|
|
94
|
-
{ subject: r, predicate: "owl:hasValue", object: adj.value ?? "true", kind: "owl:hasValue" },
|
|
95
|
-
{ subject: term, predicate: "rdfs:subClassOf", object: r, kind: "owl:hasValue" },
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return { term, individual: false, noun, extras, unknown: [] };
|
|
99
|
-
}
|
|
100
|
-
// only genuinely undeclared words are residue — a declared word in the
|
|
101
|
-
// wrong slot ("GitLab pipeline") is a structural miss, not an unknown
|
|
102
|
-
const unknown = tokens.filter((t) => !classify(t, lexicon));
|
|
103
|
-
return { term: null, individual: false, extras: [], unknown };
|
|
104
|
-
}
|
|
105
|
-
// 0 or 3+ tokens: not a fragment NP. Name the undeclared words if any.
|
|
106
|
-
return { term: null, individual: false, extras: [], unknown: tokens.filter((t) => !classify(t, lexicon)) };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** The shared miss result: a structural fit with undeclared words returns the
|
|
110
|
-
* pattern + residue (triples empty); a fit with only declared-but-unusable
|
|
111
|
-
* phrasing returns null — the honest fall-through either way. */
|
|
112
|
-
function missOrNull(pattern, nps, extraUnknown = []) {
|
|
113
|
-
const residue = [...extraUnknown, ...nps.flatMap((np) => np.unknown)];
|
|
114
|
-
return residue.length ? { pattern, triples: [], residue } : null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const hit = (pattern, nps, triples, more = {}) => ({
|
|
118
|
-
pattern,
|
|
119
|
-
triples: [...nps.flatMap((np) => np.extras), ...triples],
|
|
120
|
-
residue: [],
|
|
121
|
-
...more,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
/** Pattern 3 — "N1 VERB N2" / "PROPERNAME VERBs PROPERNAME" → object-property
|
|
125
|
-
* assertion. Also the no-declared-verb 3-token shape: both ends resolvable →
|
|
126
|
-
* residue names the middle token (the future "if you mean X…" hook). */
|
|
127
|
-
function parseRelation(lexicon, toks, lower) {
|
|
128
|
-
for (let i = 1; i < toks.length - 1; i += 1) {
|
|
129
|
-
const verb = lookupVerb(lexicon, lower[i]);
|
|
130
|
-
if (!verb) continue;
|
|
131
|
-
let objStart = i + 1;
|
|
132
|
-
if (verb.prep) {
|
|
133
|
-
if (lower[objStart] !== verb.prep) continue;
|
|
134
|
-
objStart += 1;
|
|
135
|
-
if (objStart >= toks.length) continue;
|
|
136
|
-
}
|
|
137
|
-
const np1 = resolveNP(lexicon, toks.slice(0, i));
|
|
138
|
-
const np2 = resolveNP(lexicon, toks.slice(objStart));
|
|
139
|
-
if (np1.term == null || np2.term == null) return missOrNull("relation", [np1, np2]);
|
|
140
|
-
return hit("relation", [np1, np2], [
|
|
141
|
-
{ subject: np1.term, predicate: predicateOf(verb), object: np2.term, kind: "owl:ObjectProperty" },
|
|
142
|
-
]);
|
|
143
|
-
}
|
|
144
|
-
const content = toks.filter((t) => !DET.has(t.toLowerCase()));
|
|
145
|
-
if (content.length === 3 && !classify(content[1], lexicon)) {
|
|
146
|
-
const np1 = resolveNP(lexicon, [content[0]]);
|
|
147
|
-
const np2 = resolveNP(lexicon, [content[2]]);
|
|
148
|
-
if (np1.term != null && np2.term != null) return { pattern: "relation", triples: [], residue: [content[1]] };
|
|
149
|
-
}
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Pattern 8 (copula arm) — "X is ADJ": data adjective → datatype-property
|
|
154
|
-
* assertion; subclass adjective → rdf:type (individual) / rdfs:subClassOf. */
|
|
155
|
-
function adjectiveCopula(pattern, np1, adj) {
|
|
156
|
-
if (np1.term == null) return missOrNull(pattern, [np1]);
|
|
157
|
-
if (adj.type === "data") {
|
|
158
|
-
return hit(pattern, [np1], [
|
|
159
|
-
{ subject: np1.term, predicate: adj.property || `tmct:${adj.lemma}`, object: adj.value ?? "true", kind: "owl:DatatypeProperty" },
|
|
160
|
-
]);
|
|
161
|
-
}
|
|
162
|
-
const predicate = np1.individual ? "rdf:type" : "rdfs:subClassOf";
|
|
163
|
-
return hit(pattern, [np1], [
|
|
164
|
-
{ subject: np1.term, predicate, object: `tmct:${adj.lemma}`, kind: predicate },
|
|
165
|
-
]);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Pattern 4 — "every N1 that VERBs a N2 is a N3" → someValuesFrom restriction:
|
|
169
|
-
* (N1 ⊓ ∃VERB.N2) ⊑ N3, flattened onto readable deterministic node names. */
|
|
170
|
-
function parseRestriction(lexicon, toks, lower, thatIdx) {
|
|
171
|
-
const isIdx = lower.indexOf("is", thatIdx + 2);
|
|
172
|
-
if (isIdx < 0 || thatIdx + 1 >= isIdx) return null;
|
|
173
|
-
const verb = lookupVerb(lexicon, lower[thatIdx + 1]);
|
|
174
|
-
const np1 = resolveNP(lexicon, toks.slice(1, thatIdx));
|
|
175
|
-
let objStart = thatIdx + 2;
|
|
176
|
-
if (verb?.prep) {
|
|
177
|
-
if (lower[objStart] !== verb.prep) return null;
|
|
178
|
-
objStart += 1;
|
|
179
|
-
}
|
|
180
|
-
const np2 = resolveNP(lexicon, toks.slice(objStart, isIdx));
|
|
181
|
-
const np3 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
182
|
-
if (!verb) return missOrNull("someValuesFrom", [np1, np2, np3], [toks[thatIdx + 1]]);
|
|
183
|
-
if (np1.term == null || np2.term == null || np3.term == null) {
|
|
184
|
-
return missOrNull("someValuesFrom", [np1, np2, np3]);
|
|
185
|
-
}
|
|
186
|
-
if (np1.individual || np2.individual || np3.individual) return null; // class-level pattern only
|
|
187
|
-
const pred = predicateOf(verb);
|
|
188
|
-
const k = "owl:someValuesFrom";
|
|
189
|
-
const r = `tmct:some-${local(pred)}-${local(np2.term)}`;
|
|
190
|
-
const inter = `tmct:${local(np1.term)}-that-${local(pred)}-${local(np2.term)}`;
|
|
191
|
-
return hit("someValuesFrom", [np1, np2, np3], [
|
|
192
|
-
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: k },
|
|
193
|
-
{ subject: r, predicate: "owl:onProperty", object: pred, kind: k },
|
|
194
|
-
{ subject: r, predicate: "owl:someValuesFrom", object: np2.term, kind: k },
|
|
195
|
-
{ subject: inter, predicate: "owl:intersectionOf", object: np1.term, kind: k },
|
|
196
|
-
{ subject: inter, predicate: "owl:intersectionOf", object: r, kind: k },
|
|
197
|
-
{ subject: inter, predicate: "rdfs:subClassOf", object: np3.term, kind: k },
|
|
198
|
-
]);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Pattern 5 — "every N has at least|at most|exactly n N2" → cardinality
|
|
202
|
-
* restriction on tmct:has (owl:onClass records the counted class — the
|
|
203
|
-
* qualified-form question is noted in docs/references/schemas/owl2-vocabulary.md). */
|
|
204
|
-
function parseCardinality(lexicon, toks, lower, hasIdx) {
|
|
205
|
-
let kind = null;
|
|
206
|
-
let nIdx = -1;
|
|
207
|
-
if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "least") { kind = "owl:minCardinality"; nIdx = hasIdx + 3; }
|
|
208
|
-
else if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "most") { kind = "owl:maxCardinality"; nIdx = hasIdx + 3; }
|
|
209
|
-
else if (lower[hasIdx + 1] === "exactly") { kind = "owl:cardinality"; nIdx = hasIdx + 2; }
|
|
210
|
-
else return null;
|
|
211
|
-
const n = numberOf(lower[nIdx]);
|
|
212
|
-
if (n == null || nIdx + 1 >= toks.length) return null;
|
|
213
|
-
const np1 = resolveNP(lexicon, toks.slice(1, hasIdx));
|
|
214
|
-
const np2 = resolveNP(lexicon, toks.slice(nIdx + 1));
|
|
215
|
-
if (np1.term == null || np2.term == null) return missOrNull("cardinality", [np1, np2]);
|
|
216
|
-
if (np1.individual || np2.individual) return null;
|
|
217
|
-
const tag = { "owl:minCardinality": "min", "owl:maxCardinality": "max", "owl:cardinality": "exactly" }[kind];
|
|
218
|
-
const r = `tmct:${tag}-${n}-${local(np2.term)}`;
|
|
219
|
-
return hit("cardinality", [np1, np2], [
|
|
220
|
-
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind },
|
|
221
|
-
{ subject: r, predicate: "owl:onProperty", object: "tmct:has", kind },
|
|
222
|
-
{ subject: r, predicate: kind, object: String(n), kind, n },
|
|
223
|
-
{ subject: r, predicate: "owl:onClass", object: np2.term, kind },
|
|
224
|
-
{ subject: np1.term, predicate: "rdfs:subClassOf", object: r, kind },
|
|
225
|
-
], { n });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Patterns 1, 4, 5 and 8's "every …" arm. */
|
|
229
|
-
function parseEvery(lexicon, toks, lower) {
|
|
230
|
-
const thatIdx = lower.indexOf("that");
|
|
231
|
-
if (thatIdx > 1) return parseRestriction(lexicon, toks, lower, thatIdx);
|
|
232
|
-
const hasIdx = lower.indexOf("has");
|
|
233
|
-
if (hasIdx > 1 && (lower[hasIdx + 1] === "at" || lower[hasIdx + 1] === "exactly")) {
|
|
234
|
-
return parseCardinality(lexicon, toks, lower, hasIdx);
|
|
235
|
-
}
|
|
236
|
-
const isIdx = lower.indexOf("is");
|
|
237
|
-
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
238
|
-
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
239
|
-
const rest = toks.slice(isIdx + 1);
|
|
240
|
-
if (rest.length === 1) {
|
|
241
|
-
const adj = lookupAdjective(lexicon, rest[0]);
|
|
242
|
-
if (adj) return adjectiveCopula("adjective", np1, adj);
|
|
243
|
-
}
|
|
244
|
-
const np2 = resolveNP(lexicon, rest);
|
|
245
|
-
if (np1.term == null || np2.term == null) return missOrNull("subClassOf", [np1, np2]);
|
|
246
|
-
if (np1.individual || np2.individual) return null; // "every X is chat.mjs" — not the fragment
|
|
247
|
-
return hit("subClassOf", [np1, np2], [
|
|
248
|
-
{ subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
|
|
249
|
-
]);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Pattern 6 — "no N1 is a N2" → owl:disjointWith. */
|
|
253
|
-
function parseDisjoint(lexicon, toks, lower) {
|
|
254
|
-
const isIdx = lower.indexOf("is");
|
|
255
|
-
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
256
|
-
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
257
|
-
const np2 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
258
|
-
if (np1.term == null || np2.term == null) return missOrNull("disjointWith", [np1, np2]);
|
|
259
|
-
if (np1.individual || np2.individual) return null;
|
|
260
|
-
return hit("disjointWith", [np1, np2], [
|
|
261
|
-
{ subject: np1.term, predicate: "owl:disjointWith", object: np2.term, kind: "owl:disjointWith" },
|
|
262
|
-
]);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** Pattern 7 — "N1's N2 is VALUE" / "the N2 of N1 is VALUE": data or object
|
|
266
|
-
* property assertion per the possessive noun's DECLARED typing (undeclared
|
|
267
|
-
* typing defaults to data — a literal value is the honest floor). */
|
|
268
|
-
function buildPossessive(lexicon, ownerToks, headToks, valueToks) {
|
|
269
|
-
const owner = resolveNP(lexicon, ownerToks);
|
|
270
|
-
if (headToks.length !== 1) return null;
|
|
271
|
-
const head = lookupNoun(lexicon, headToks[0]);
|
|
272
|
-
if (!head) return missOrNull("possessive", [owner], [headToks[0]]);
|
|
273
|
-
if (owner.term == null) return missOrNull("possessive", [owner]);
|
|
274
|
-
if (!valueToks.length) return null;
|
|
275
|
-
const predicate = `tmct:${head.lemma}`;
|
|
276
|
-
if ((head.property || "data") === "object") {
|
|
277
|
-
const value = resolveNP(lexicon, valueToks);
|
|
278
|
-
if (value.term == null) return missOrNull("possessive", [owner, value]);
|
|
279
|
-
return hit("possessive", [owner, value], [
|
|
280
|
-
{ subject: owner.term, predicate, object: value.term, kind: "owl:ObjectProperty" },
|
|
281
|
-
]);
|
|
282
|
-
}
|
|
283
|
-
return hit("possessive", [owner], [
|
|
284
|
-
{ subject: owner.term, predicate, object: valueToks.join(" "), kind: "owl:DatatypeProperty" },
|
|
285
|
-
]);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function parsePossessive(lexicon, toks, lower) {
|
|
289
|
-
const ownerRaw = toks[0].replace(/'s$/i, "");
|
|
290
|
-
const isIdx = lower.indexOf("is");
|
|
291
|
-
if (isIdx < 2 || !ownerRaw) return null;
|
|
292
|
-
return buildPossessive(lexicon, [ownerRaw], toks.slice(1, isIdx), toks.slice(isIdx + 1));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function parseOfForm(lexicon, toks, lower) {
|
|
296
|
-
const ofIdx = lower.indexOf("of");
|
|
297
|
-
const isIdx = lower.indexOf("is", ofIdx + 1);
|
|
298
|
-
if (ofIdx < 2 || isIdx < ofIdx + 2) return null;
|
|
299
|
-
return buildPossessive(lexicon, toks.slice(ofIdx + 1, isIdx), toks.slice(1, ofIdx), toks.slice(isIdx + 1));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Patterns 2 (class assertion), 1's bare-copula variant, and 8's copula arm. */
|
|
303
|
-
function parseCopula(lexicon, toks, lower, isIdx) {
|
|
304
|
-
const np1 = resolveNP(lexicon, toks.slice(0, isIdx));
|
|
305
|
-
const rest = toks.slice(isIdx + 1);
|
|
306
|
-
if (!rest.length) return null;
|
|
307
|
-
if (rest.length === 1) {
|
|
308
|
-
const adj = lookupAdjective(lexicon, rest[0]);
|
|
309
|
-
if (adj) return adjectiveCopula("adjective", np1, adj);
|
|
310
|
-
}
|
|
311
|
-
const np2 = resolveNP(lexicon, rest);
|
|
312
|
-
if (np1.term == null || np2.term == null) {
|
|
313
|
-
return missOrNull(np1.individual ? "typeAssertion" : "subClassOf", [np1, np2]);
|
|
314
|
-
}
|
|
315
|
-
if (np2.individual) return null; // "chat.mjs is sessions.mjs" — identity is not in the fragment
|
|
316
|
-
if (np1.individual) {
|
|
317
|
-
return hit("typeAssertion", [np1, np2], [
|
|
318
|
-
{ subject: np1.term, predicate: "rdf:type", object: np2.term, kind: "rdf:type" },
|
|
319
|
-
]);
|
|
320
|
-
}
|
|
321
|
-
return hit("subClassOf", [np1, np2], [
|
|
322
|
-
{ subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
|
|
323
|
-
]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** Parse one sentence against the 8-pattern ACE-OWL sub-fragment. See the file
|
|
327
|
-
* header for the result contract; `lexicon` defaults to the committed core. */
|
|
12
|
+
// tokenize() has no lexicon/namespace dependency at all, so it re-exports
|
|
13
|
+
// unchanged. See packages/ace-owl/README.md for the parser's full contract
|
|
14
|
+
// (pattern table, triple shape, the null-is-a-feature miss discipline).
|
|
15
|
+
import { parseAce as parseAceLib, tokenize } from "@polycode-projects/ace-owl";
|
|
16
|
+
import { loadLexicon } from "./lexicon.mjs";
|
|
17
|
+
|
|
18
|
+
export { tokenize };
|
|
19
|
+
|
|
20
|
+
/** parseAce(sentence, lexicon?) — `lexicon` defaults to this module's own
|
|
21
|
+
* loadLexicon() (the "tmct:"-namespaced core), not the package's neutral
|
|
22
|
+
* default. Every other call site (grammar/assert.mjs, chat.mjs, tests) is
|
|
23
|
+
* free to pass its own already-namespaced lexicon (e.g. from
|
|
24
|
+
* extensions.mjs's mergedLexiconExtra) exactly as before. */
|
|
328
25
|
export function parseAce(sentence, lexicon = loadLexicon()) {
|
|
329
|
-
|
|
330
|
-
if (toks.length < 3) return null;
|
|
331
|
-
const lower = toks.map((t) => t.toLowerCase());
|
|
332
|
-
if (lower[0] === "every") return parseEvery(lexicon, toks, lower);
|
|
333
|
-
if (lower[0] === "no") return parseDisjoint(lexicon, toks, lower);
|
|
334
|
-
if (/'s$/.test(lower[0]) && lower[0].length > 2) return parsePossessive(lexicon, toks, lower);
|
|
335
|
-
if (lower[0] === "the" && lower.includes("of") && lower.includes("is")) {
|
|
336
|
-
return parseOfForm(lexicon, toks, lower);
|
|
337
|
-
}
|
|
338
|
-
const isIdx = lower.indexOf("is");
|
|
339
|
-
if (isIdx > 0) return parseCopula(lexicon, toks, lower, isIdx);
|
|
340
|
-
return parseRelation(lexicon, toks, lower);
|
|
26
|
+
return parseAceLib(sentence, lexicon);
|
|
341
27
|
}
|
package/src/grammar/lexicon.mjs
CHANGED
|
@@ -1,202 +1,45 @@
|
|
|
1
|
-
// grammar/lexicon.mjs —
|
|
2
|
-
// (
|
|
1
|
+
// grammar/lexicon.mjs — thin re-export shim over @polycode-projects/ace-owl
|
|
2
|
+
// (PLAN_OSS_ACE_PARSER.md / PLAN_AGENTS.md §3's "ace-owl open-source
|
|
3
|
+
// extraction"). The declared lexicon behind tmct's ACE-OWL sub-fragment
|
|
4
|
+
// parser now lives in the extracted package (packages/ace-owl/src/
|
|
5
|
+
// lexicon.mjs) — this file exists ONLY to bind the package's neutral,
|
|
6
|
+
// caller-supplied namespace to tmct's own "tmct:" CURIE prefix, so every
|
|
7
|
+
// consumer in this repo (chat.mjs, extensions.mjs, the grammar/ontology
|
|
8
|
+
// tests, …) gets BYTE-IDENTICAL behaviour to before the extraction, with a
|
|
9
|
+
// single shared implementation instead of two copies drifting apart.
|
|
3
10
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import { readFileSync } from "node:fs";
|
|
22
|
-
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { dirname, join } from "node:path";
|
|
24
|
-
|
|
25
|
-
const CORE_FILE = join(dirname(fileURLToPath(import.meta.url)), "lexicon-core.json");
|
|
26
|
-
|
|
27
|
-
/** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
|
|
28
|
-
export const DETERMINERS = Object.freeze({
|
|
29
|
-
every: "universal",
|
|
30
|
-
a: "indefinite",
|
|
31
|
-
an: "indefinite",
|
|
32
|
-
the: "definite",
|
|
33
|
-
no: "negative",
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/** The cardinality quantifier phrases (pattern 5) → the OWL term they select. */
|
|
37
|
-
export const QUANTIFIERS = Object.freeze({
|
|
38
|
-
"at least": "owl:minCardinality",
|
|
39
|
-
"at most": "owl:maxCardinality",
|
|
40
|
-
exactly: "owl:cardinality",
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const NUMBER_WORDS = Object.freeze({
|
|
44
|
-
one: 1, two: 2, three: 3, four: 4, five: 5,
|
|
45
|
-
six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** Parse a cardinality count token: a digit run or a small number word. */
|
|
49
|
-
export function numberOf(word) {
|
|
50
|
-
const w = String(word ?? "").trim().toLowerCase();
|
|
51
|
-
if (/^\d+$/.test(w)) return Number(w);
|
|
52
|
-
return NUMBER_WORDS[w] ?? null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 3rd-person-singular surface form of a verb lemma — the predicate spelling
|
|
56
|
-
* ("import"→imports, "rely"→relies, "catch"→catches, "have"→has), matching
|
|
57
|
-
* the code graph's 3sg relation-kind convention (ask.mjs §verbs). */
|
|
58
|
-
export function thirdPerson(base) {
|
|
59
|
-
const b = String(base);
|
|
60
|
-
if (b === "have") return "has";
|
|
61
|
-
if (/[^aeiou]y$/.test(b)) return `${b.slice(0, -1)}ies`;
|
|
62
|
-
if (/(s|x|z|ch|sh)$/.test(b)) return `${b}es`;
|
|
63
|
-
return `${b}s`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The URI-style predicate a verb entry emits: a declared override, or
|
|
67
|
-
* tmct:<3sg lemma> with any preposition camel-appended ("depend on"→tmct:dependsOn). */
|
|
68
|
-
export function predicateOf(verbEntry) {
|
|
69
|
-
if (verbEntry.predicate) return verbEntry.predicate;
|
|
70
|
-
const prep = verbEntry.prep ? verbEntry.prep[0].toUpperCase() + verbEntry.prep.slice(1) : "";
|
|
71
|
-
return `tmct:${thirdPerson(verbEntry.lemma)}${prep}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Deterministic singular/base-form candidates for a surface word, most
|
|
75
|
-
* specific first: as-is, -ies→y, -(s|x|z|ch|sh)es→stem, -s→stem. The FIRST
|
|
76
|
-
* candidate found in the relevant map wins ("classes"→class before "classe";
|
|
77
|
-
* "uses"→"us" misses, "use" hits). */
|
|
78
|
-
function foldCandidates(word) {
|
|
79
|
-
const w = String(word);
|
|
80
|
-
const out = [w];
|
|
81
|
-
if (w.length > 4 && /[a-z]ies$/.test(w)) out.push(`${w.slice(0, -3)}y`);
|
|
82
|
-
if (/(ses|xes|zes|ches|shes)$/.test(w)) out.push(w.slice(0, -2));
|
|
83
|
-
if (/[a-z]s$/.test(w) && !/ss$/.test(w)) out.push(w.slice(0, -1));
|
|
84
|
-
if (w === "has") out.push("have");
|
|
85
|
-
return out;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const NOUN_PROPERTY_TYPES = new Set(["data", "object"]);
|
|
89
|
-
const ADJECTIVE_TYPES = new Set(["subclass", "data"]);
|
|
90
|
-
|
|
91
|
-
/** Merge one raw lexicon block ({nouns, verbs, adjectives, properNames}) into
|
|
92
|
-
* the lookup maps, validating the declared typings (bad declarations throw —
|
|
93
|
-
* a lexicon that lies would make the grammar guess). */
|
|
94
|
-
function ingest(lex, raw = {}) {
|
|
95
|
-
for (const [lemma, e] of Object.entries(raw.nouns || {})) {
|
|
96
|
-
const entry = { lemma, ...(e || {}) };
|
|
97
|
-
if (entry.property && !NOUN_PROPERTY_TYPES.has(entry.property)) {
|
|
98
|
-
throw new Error(`lexicon noun "${lemma}": property must be "data" or "object", got ${JSON.stringify(entry.property)}`);
|
|
99
|
-
}
|
|
100
|
-
lex.nouns.set(lemma, entry);
|
|
101
|
-
if (entry.plural) lex.nounPlurals.set(entry.plural, lemma);
|
|
102
|
-
}
|
|
103
|
-
for (const [lemma, e] of Object.entries(raw.verbs || {})) {
|
|
104
|
-
lex.verbs.set(lemma, { lemma, ...(e || {}) });
|
|
105
|
-
}
|
|
106
|
-
for (const [lemma, e] of Object.entries(raw.adjectives || {})) {
|
|
107
|
-
const entry = { lemma, ...(e || {}) };
|
|
108
|
-
if (!ADJECTIVE_TYPES.has(entry.type)) {
|
|
109
|
-
throw new Error(`lexicon adjective "${lemma}": type must be "subclass" or "data", got ${JSON.stringify(entry.type)}`);
|
|
110
|
-
}
|
|
111
|
-
lex.adjectives.set(lemma, entry);
|
|
112
|
-
}
|
|
113
|
-
for (const name of raw.properNames || []) {
|
|
114
|
-
lex.properNames.set(String(name).toLowerCase(), String(name));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let coreCache = null;
|
|
119
|
-
|
|
120
|
-
/** Load the lexicon: the committed core vocabulary, optionally merged with a
|
|
121
|
-
* caller-supplied `extra` block of the same JSON shape (extra entries win).
|
|
122
|
-
* The no-extra result is cached (the JSON is committed, immutable at runtime). */
|
|
11
|
+
// Everything else (morphology, classify(), the committed starter vocabulary)
|
|
12
|
+
// is the package's — see packages/ace-owl/README.md for the full API and
|
|
13
|
+
// packages/ace-owl/src/lexicon-core.json for the vocabulary itself (also the
|
|
14
|
+
// canonical copy now; this repo no longer carries its own lexicon-core.json).
|
|
15
|
+
import * as aceOwl from "@polycode-projects/ace-owl";
|
|
16
|
+
|
|
17
|
+
export const TMCT_NS = "tmct:";
|
|
18
|
+
|
|
19
|
+
export const {
|
|
20
|
+
DETERMINERS, QUANTIFIERS, numberOf, thirdPerson,
|
|
21
|
+
lookupNoun, lookupVerb, lookupAdjective, lookupProperName,
|
|
22
|
+
} = aceOwl;
|
|
23
|
+
|
|
24
|
+
/** loadLexicon(extra) — bound to tmct's own "tmct:" namespace, so the
|
|
25
|
+
* no-extra call stays the same cached, byte-identical lexicon it always
|
|
26
|
+
* was (see ace-owl's loadLexicon(extra, ns) — ns defaults to the package's
|
|
27
|
+
* own neutral "ex:" when not passed, which tmct never wants). */
|
|
123
28
|
export function loadLexicon(extra) {
|
|
124
|
-
|
|
125
|
-
const raw = JSON.parse(readFileSync(CORE_FILE, "utf8"));
|
|
126
|
-
const lex = {
|
|
127
|
-
nouns: new Map(),
|
|
128
|
-
nounPlurals: new Map(),
|
|
129
|
-
verbs: new Map(),
|
|
130
|
-
adjectives: new Map(),
|
|
131
|
-
properNames: new Map(), // lowercased → canonical spelling
|
|
132
|
-
};
|
|
133
|
-
ingest(lex, raw);
|
|
134
|
-
if (extra) {
|
|
135
|
-
ingest(lex, extra);
|
|
136
|
-
return lex;
|
|
137
|
-
}
|
|
138
|
-
coreCache = lex;
|
|
139
|
-
return lex;
|
|
29
|
+
return aceOwl.loadLexicon(extra, TMCT_NS);
|
|
140
30
|
}
|
|
141
31
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (irregular) return lexicon.nouns.get(irregular) ?? null;
|
|
147
|
-
for (const cand of foldCandidates(w)) {
|
|
148
|
-
const hit = lexicon.nouns.get(cand);
|
|
149
|
-
if (hit) return hit;
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Verb lookup with 3sg folding; returns the entry ({lemma, prep?, predicate?}) or null. */
|
|
155
|
-
export function lookupVerb(lexicon, word) {
|
|
156
|
-
const w = String(word ?? "").toLowerCase();
|
|
157
|
-
for (const cand of foldCandidates(w)) {
|
|
158
|
-
const hit = lexicon.verbs.get(cand);
|
|
159
|
-
if (hit) return hit;
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Adjective lookup (exact lemma); returns {lemma, type, property?, value?} or null. */
|
|
165
|
-
export function lookupAdjective(lexicon, word) {
|
|
166
|
-
return lexicon.adjectives.get(String(word ?? "").toLowerCase()) ?? null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Proper-name lookup, case-insensitive; returns the CANONICAL spelling or null. */
|
|
170
|
-
export function lookupProperName(lexicon, word) {
|
|
171
|
-
return lexicon.properNames.get(String(word ?? "").toLowerCase()) ?? null;
|
|
32
|
+
/** predicateOf(verbEntry) — bound to tmct's own "tmct:" namespace, same
|
|
33
|
+
* reasoning as loadLexicon above. */
|
|
34
|
+
export function predicateOf(verbEntry) {
|
|
35
|
+
return aceOwl.predicateOf(verbEntry, TMCT_NS);
|
|
172
36
|
}
|
|
173
37
|
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
38
|
+
/** classify(word, lexicon?) — re-exported with `lexicon` re-defaulted to
|
|
39
|
+
* THIS module's own loadLexicon() (the "tmct:"-namespaced core), not the
|
|
40
|
+
* package's neutral default; every no-lexicon call site (chat.mjs,
|
|
41
|
+
* grammar/ace.mjs's own resolveNP, the grammar tests) must keep seeing
|
|
42
|
+
* "tmct:"-prefixed verb predicates in the classification it returns. */
|
|
179
43
|
export function classify(word, lexicon = loadLexicon()) {
|
|
180
|
-
|
|
181
|
-
if (!w) return null;
|
|
182
|
-
const lower = w.toLowerCase();
|
|
183
|
-
if (DETERMINERS[lower]) return { pos: "determiner", type: DETERMINERS[lower] };
|
|
184
|
-
if (QUANTIFIERS[lower]) return { pos: "quantifier", type: QUANTIFIERS[lower] };
|
|
185
|
-
const n = numberOf(lower);
|
|
186
|
-
if (n != null) return { pos: "number", type: "cardinal", value: n };
|
|
187
|
-
const proper = lookupProperName(lexicon, w);
|
|
188
|
-
if (proper) return { pos: "properName", type: "individual", canonical: proper };
|
|
189
|
-
const noun = lookupNoun(lexicon, lower);
|
|
190
|
-
if (noun) {
|
|
191
|
-
return noun.property
|
|
192
|
-
? { pos: "noun", type: `${noun.property}-property`, lemma: noun.lemma, property: noun.property }
|
|
193
|
-
: { pos: "noun", type: "class", lemma: noun.lemma };
|
|
194
|
-
}
|
|
195
|
-
const verb = lookupVerb(lexicon, lower);
|
|
196
|
-
if (verb) {
|
|
197
|
-
return { pos: "verb", type: "objectProperty", lemma: verb.lemma, predicate: predicateOf(verb), ...(verb.prep ? { prep: verb.prep } : {}) };
|
|
198
|
-
}
|
|
199
|
-
const adj = lookupAdjective(lexicon, lower);
|
|
200
|
-
if (adj) return { pos: "adjective", type: adj.type, lemma: adj.lemma };
|
|
201
|
-
return null;
|
|
44
|
+
return aceOwl.classify(word, lexicon);
|
|
202
45
|
}
|
|
@@ -34,6 +34,15 @@ import { noiseStripStrategy } from "./strategies/noise-strip.mjs";
|
|
|
34
34
|
// ace-less registry instead of throwing over an undeclared identifier. (ACE is
|
|
35
35
|
// async-only anyway, so the sync parseQuery path the viewer uses never ran it.)
|
|
36
36
|
import { aceStrategy } from "./strategies/ace.mjs";
|
|
37
|
+
// Optional Node-only construction-grammar bank (PLAN_ADVANCED_GRAMMAR.md track
|
|
38
|
+
// (d)) — same viewer-bundle boundary as ACE and the wink adapter: the loader
|
|
39
|
+
// reads its committed TOML data via Node fs/path (interpret/strategies/
|
|
40
|
+
// constructions.mjs), so an inlining viewer bundle strips this import too; the
|
|
41
|
+
// same `typeof` guard below degrades to a constructions-less registry instead
|
|
42
|
+
// of throwing over an undeclared identifier (test/ask-nlp.test.mjs's "viewer
|
|
43
|
+
// bundle without wink" test proves the boundary — its bundled file list never
|
|
44
|
+
// includes this strategy file, matching ace.mjs's own exclusion there).
|
|
45
|
+
import { constructionsStrategy } from "./strategies/constructions.mjs";
|
|
37
46
|
import { mergeStrategyResults } from "./merge.mjs";
|
|
38
47
|
// Optional Node-only wink adapter — same viewer-bundle boundary as ask.mjs: an
|
|
39
48
|
// inlining bundle strips this import and the `typeof` read below degrades to
|
|
@@ -52,9 +61,21 @@ import { nlpAdapter } from "../ask-nlp.mjs";
|
|
|
52
61
|
* adds declarative-fragment reach to interpret() while leaving the sync spine
|
|
53
62
|
* byte-stable (see strategies/ace.mjs for the full rationale). The `typeof` guard
|
|
54
63
|
* mirrors the nlpAdapter degradation: a stripped ACE import (viewer bundle) leaves
|
|
55
|
-
* the identifier undeclared, so the registry is ace-less there instead of a crash.
|
|
64
|
+
* the identifier undeclared, so the registry is ace-less there instead of a crash.
|
|
65
|
+
* interpret/strategies/constructions.mjs (PLAN_ADVANCED_GRAMMAR.md track (d)) is
|
|
66
|
+
* the construction-grammar template bank — data-driven (data/templates/
|
|
67
|
+
* constructions/*.toml), registered as its own additive, own-class
|
|
68
|
+
* ("construction") strategy at grammar-level confidence (0.9) so it outranks a
|
|
69
|
+
* same-text keyword-spot guess outright instead of colliding with it (see that
|
|
70
|
+
* file's header for why: keyword-spot mis-parses the genitive/compound surface
|
|
71
|
+
* forms this bank exists to fix). Sync (unlike ACE), so it participates on the
|
|
72
|
+
* parseQuery path too, not just interpret(). */
|
|
56
73
|
// eslint-disable-next-line no-undef
|
|
57
|
-
const OPTIONAL_STRATEGIES =
|
|
74
|
+
const OPTIONAL_STRATEGIES = [
|
|
75
|
+
...(typeof aceStrategy !== "undefined" ? [aceStrategy] : []),
|
|
76
|
+
// eslint-disable-next-line no-undef
|
|
77
|
+
...(typeof constructionsStrategy !== "undefined" ? [constructionsStrategy] : []),
|
|
78
|
+
];
|
|
58
79
|
export const STRATEGIES = [grammarStrategy, keywordSpotStrategy, noiseStripStrategy, ...OPTIONAL_STRATEGIES];
|
|
59
80
|
|
|
60
81
|
/** The documented normalization pre-pass: whitespace-collapse + the §3.5
|