@polycode-projects/the-mechanical-code-talker 1.4.1 → 1.5.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 +1 -1
- package/ROADMAP.md +89 -18
- 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 +6 -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 +87 -53
- package/src/grammar/lexicon.mjs +39 -24
- 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/ace.mjs
CHANGED
|
@@ -8,8 +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
|
|
12
|
-
// cardinality | disjointWith | possessive | adjective
|
|
11
|
+
// pattern one of the PATTERNS below (also exported individually).
|
|
13
12
|
// triples [{ subject, predicate, object, kind, n? }] — OWL-labelled string
|
|
14
13
|
// triples shaped for src/memory/core.mjs's appendFact (which
|
|
15
14
|
// normalizes subject/object via normFactTerm: "tmct:module" is
|
|
@@ -22,19 +21,39 @@
|
|
|
22
21
|
// Term style: classes/individuals are `tmct:<lexeme>` CURIEs (lexicon lemma
|
|
23
22
|
// for nouns, canonical spelling for proper names, the literal token for
|
|
24
23
|
// code-shaped references like chat.mjs); predicates are the OWL/RDF(S)
|
|
25
|
-
// vocabulary terms or the lexicon verb's tmct:<3sg> predicate
|
|
26
|
-
// and intersection class
|
|
27
|
-
// (tmct:some-imports-test,
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// owl:intersectionOf triples (one
|
|
31
|
-
// RDF list, documented in
|
|
24
|
+
// vocabulary terms or the lexicon verb's tmct:<3sg> predicate
|
|
25
|
+
// (lexicon.mjs's predicateOf). Restriction and intersection class
|
|
26
|
+
// expressions get READABLE deterministic node names (tmct:some-imports-test,
|
|
27
|
+
// tmct:module-that-imports-test) instead of blank nodes, so the same
|
|
28
|
+
// sentence always re-emits the same triples and appendFact stays idempotent.
|
|
29
|
+
// An intersection is flattened to repeated owl:intersectionOf triples (one
|
|
30
|
+
// per member) — the flat-JSON stand-in for an RDF list, documented in
|
|
31
|
+
// ontology/tmct-core.ttl. `lexicon.ns` is always "tmct:" here (lexicon.mjs's
|
|
32
|
+
// DEFAULT_NS) — every term this module mints is namespaced off `lexicon.ns`
|
|
33
|
+
// rather than a hardcoded literal purely so a caller can supply its own
|
|
34
|
+
// already-namespaced lexicon (extensions.mjs's mergedLexiconExtra); tmct
|
|
35
|
+
// itself only ever runs one namespace.
|
|
32
36
|
|
|
33
37
|
import {
|
|
34
38
|
loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName,
|
|
35
39
|
predicateOf, numberOf, classify,
|
|
36
40
|
} from "./lexicon.mjs";
|
|
37
41
|
|
|
42
|
+
export const PATTERN_SUB_CLASS_OF = "subClassOf";
|
|
43
|
+
export const PATTERN_TYPE_ASSERTION = "typeAssertion";
|
|
44
|
+
export const PATTERN_RELATION = "relation";
|
|
45
|
+
export const PATTERN_SOME_VALUES_FROM = "someValuesFrom";
|
|
46
|
+
export const PATTERN_CARDINALITY = "cardinality";
|
|
47
|
+
export const PATTERN_DISJOINT_WITH = "disjointWith";
|
|
48
|
+
export const PATTERN_POSSESSIVE = "possessive";
|
|
49
|
+
export const PATTERN_ADJECTIVE = "adjective";
|
|
50
|
+
|
|
51
|
+
/** The pattern field's full domain, in the README's table order. */
|
|
52
|
+
export const PATTERNS = Object.freeze([
|
|
53
|
+
PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM,
|
|
54
|
+
PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE,
|
|
55
|
+
]);
|
|
56
|
+
|
|
38
57
|
const DET = new Set(["a", "an", "the"]);
|
|
39
58
|
// A token SHAPED like a code reference (a path, file, symbol or CURIE) is an
|
|
40
59
|
// individual by form — a deterministic tokenizer rule, not a guess: declared
|
|
@@ -53,7 +72,15 @@ export function tokenize(sentence) {
|
|
|
53
72
|
.filter(Boolean);
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
/** Strip a lexicon's own namespace prefix off a term, for use inside a
|
|
76
|
+
* synthesized deterministic node name (so "${ns}some-${ns}imports-${ns}test"
|
|
77
|
+
* reads as "${ns}some-imports-test"). A term outside the lexicon's own
|
|
78
|
+
* namespace (a rare cross-namespace reference) is returned unchanged. */
|
|
79
|
+
function local(lexicon, term) {
|
|
80
|
+
const s = String(term);
|
|
81
|
+
const ns = lexicon.ns;
|
|
82
|
+
return ns && s.startsWith(ns) ? s.slice(ns.length) : s;
|
|
83
|
+
}
|
|
57
84
|
|
|
58
85
|
const stripDet = (tokens) =>
|
|
59
86
|
tokens.length > 1 && DET.has(tokens[0].toLowerCase()) ? tokens.slice(1) : tokens;
|
|
@@ -64,33 +91,34 @@ const stripDet = (tokens) =>
|
|
|
64
91
|
* unparseable phrase → the caller returns a hard null). `extras` carries the
|
|
65
92
|
* pattern-8 adjective triples (subclass axioms / hasValue restriction). */
|
|
66
93
|
function resolveNP(lexicon, tokensIn) {
|
|
94
|
+
const ns = lexicon.ns;
|
|
67
95
|
const tokens = stripDet(tokensIn);
|
|
68
96
|
if (tokens.length === 1) {
|
|
69
97
|
const t = tokens[0];
|
|
70
98
|
const proper = lookupProperName(lexicon, t);
|
|
71
|
-
if (proper) return { term:
|
|
72
|
-
if (CODE_REF.test(t)) return { term:
|
|
99
|
+
if (proper) return { term: `${ns}${proper}`, individual: true, extras: [], unknown: [] };
|
|
100
|
+
if (CODE_REF.test(t)) return { term: `${ns}${t}`, individual: true, extras: [], unknown: [] };
|
|
73
101
|
const noun = lookupNoun(lexicon, t);
|
|
74
|
-
if (noun) return { term:
|
|
102
|
+
if (noun) return { term: `${ns}${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
|
|
75
103
|
return { term: null, individual: false, extras: [], unknown: [t] };
|
|
76
104
|
}
|
|
77
105
|
if (tokens.length === 2) {
|
|
78
106
|
const adj = lookupAdjective(lexicon, tokens[0]);
|
|
79
107
|
const noun = lookupNoun(lexicon, tokens[1]);
|
|
80
108
|
if (adj && noun) {
|
|
81
|
-
const term =
|
|
109
|
+
const term = `${ns}${adj.lemma}-${noun.lemma}`;
|
|
82
110
|
const extras = [
|
|
83
|
-
{ subject: term, predicate: "rdfs:subClassOf", object:
|
|
111
|
+
{ subject: term, predicate: "rdfs:subClassOf", object: `${ns}${noun.lemma}`, kind: "rdfs:subClassOf" },
|
|
84
112
|
];
|
|
85
113
|
if (adj.type === "subclass") {
|
|
86
114
|
// the adjective itself denotes a class: legacy-module ⊑ module, ⊑ legacy
|
|
87
|
-
extras.push({ subject: term, predicate: "rdfs:subClassOf", object:
|
|
115
|
+
extras.push({ subject: term, predicate: "rdfs:subClassOf", object: `${ns}${adj.lemma}`, kind: "rdfs:subClassOf" });
|
|
88
116
|
} else {
|
|
89
117
|
// data adjective: subclass-with-restriction on the boolean-ish property
|
|
90
|
-
const r =
|
|
118
|
+
const r = `${ns}has-${adj.lemma}`;
|
|
91
119
|
extras.push(
|
|
92
120
|
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: "owl:hasValue" },
|
|
93
|
-
{ subject: r, predicate: "owl:onProperty", object: adj.property ||
|
|
121
|
+
{ subject: r, predicate: "owl:onProperty", object: adj.property || `${ns}${adj.lemma}`, kind: "owl:hasValue" },
|
|
94
122
|
{ subject: r, predicate: "owl:hasValue", object: adj.value ?? "true", kind: "owl:hasValue" },
|
|
95
123
|
{ subject: term, predicate: "rdfs:subClassOf", object: r, kind: "owl:hasValue" },
|
|
96
124
|
);
|
|
@@ -136,38 +164,40 @@ function parseRelation(lexicon, toks, lower) {
|
|
|
136
164
|
}
|
|
137
165
|
const np1 = resolveNP(lexicon, toks.slice(0, i));
|
|
138
166
|
const np2 = resolveNP(lexicon, toks.slice(objStart));
|
|
139
|
-
if (np1.term == null || np2.term == null) return missOrNull(
|
|
140
|
-
return hit(
|
|
141
|
-
{ subject: np1.term, predicate: predicateOf(verb), object: np2.term, kind: "owl:ObjectProperty" },
|
|
167
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_RELATION, [np1, np2]);
|
|
168
|
+
return hit(PATTERN_RELATION, [np1, np2], [
|
|
169
|
+
{ subject: np1.term, predicate: predicateOf(verb, lexicon.ns), object: np2.term, kind: "owl:ObjectProperty" },
|
|
142
170
|
]);
|
|
143
171
|
}
|
|
144
172
|
const content = toks.filter((t) => !DET.has(t.toLowerCase()));
|
|
145
173
|
if (content.length === 3 && !classify(content[1], lexicon)) {
|
|
146
174
|
const np1 = resolveNP(lexicon, [content[0]]);
|
|
147
175
|
const np2 = resolveNP(lexicon, [content[2]]);
|
|
148
|
-
if (np1.term != null && np2.term != null) return { pattern:
|
|
176
|
+
if (np1.term != null && np2.term != null) return { pattern: PATTERN_RELATION, triples: [], residue: [content[1]] };
|
|
149
177
|
}
|
|
150
178
|
return null;
|
|
151
179
|
}
|
|
152
180
|
|
|
153
181
|
/** Pattern 8 (copula arm) — "X is ADJ": data adjective → datatype-property
|
|
154
182
|
* assertion; subclass adjective → rdf:type (individual) / rdfs:subClassOf. */
|
|
155
|
-
function adjectiveCopula(pattern, np1, adj) {
|
|
183
|
+
function adjectiveCopula(lexicon, pattern, np1, adj) {
|
|
184
|
+
const ns = lexicon.ns;
|
|
156
185
|
if (np1.term == null) return missOrNull(pattern, [np1]);
|
|
157
186
|
if (adj.type === "data") {
|
|
158
187
|
return hit(pattern, [np1], [
|
|
159
|
-
{ subject: np1.term, predicate: adj.property ||
|
|
188
|
+
{ subject: np1.term, predicate: adj.property || `${ns}${adj.lemma}`, object: adj.value ?? "true", kind: "owl:DatatypeProperty" },
|
|
160
189
|
]);
|
|
161
190
|
}
|
|
162
191
|
const predicate = np1.individual ? "rdf:type" : "rdfs:subClassOf";
|
|
163
192
|
return hit(pattern, [np1], [
|
|
164
|
-
{ subject: np1.term, predicate, object:
|
|
193
|
+
{ subject: np1.term, predicate, object: `${ns}${adj.lemma}`, kind: predicate },
|
|
165
194
|
]);
|
|
166
195
|
}
|
|
167
196
|
|
|
168
197
|
/** Pattern 4 — "every N1 that VERBs a N2 is a N3" → someValuesFrom restriction:
|
|
169
198
|
* (N1 ⊓ ∃VERB.N2) ⊑ N3, flattened onto readable deterministic node names. */
|
|
170
199
|
function parseRestriction(lexicon, toks, lower, thatIdx) {
|
|
200
|
+
const ns = lexicon.ns;
|
|
171
201
|
const isIdx = lower.indexOf("is", thatIdx + 2);
|
|
172
202
|
if (isIdx < 0 || thatIdx + 1 >= isIdx) return null;
|
|
173
203
|
const verb = lookupVerb(lexicon, lower[thatIdx + 1]);
|
|
@@ -179,16 +209,16 @@ function parseRestriction(lexicon, toks, lower, thatIdx) {
|
|
|
179
209
|
}
|
|
180
210
|
const np2 = resolveNP(lexicon, toks.slice(objStart, isIdx));
|
|
181
211
|
const np3 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
182
|
-
if (!verb) return missOrNull(
|
|
212
|
+
if (!verb) return missOrNull(PATTERN_SOME_VALUES_FROM, [np1, np2, np3], [toks[thatIdx + 1]]);
|
|
183
213
|
if (np1.term == null || np2.term == null || np3.term == null) {
|
|
184
|
-
return missOrNull(
|
|
214
|
+
return missOrNull(PATTERN_SOME_VALUES_FROM, [np1, np2, np3]);
|
|
185
215
|
}
|
|
186
216
|
if (np1.individual || np2.individual || np3.individual) return null; // class-level pattern only
|
|
187
|
-
const pred = predicateOf(verb);
|
|
217
|
+
const pred = predicateOf(verb, ns);
|
|
188
218
|
const k = "owl:someValuesFrom";
|
|
189
|
-
const r =
|
|
190
|
-
const inter =
|
|
191
|
-
return hit(
|
|
219
|
+
const r = `${ns}some-${local(lexicon, pred)}-${local(lexicon, np2.term)}`;
|
|
220
|
+
const inter = `${ns}${local(lexicon, np1.term)}-that-${local(lexicon, pred)}-${local(lexicon, np2.term)}`;
|
|
221
|
+
return hit(PATTERN_SOME_VALUES_FROM, [np1, np2, np3], [
|
|
192
222
|
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: k },
|
|
193
223
|
{ subject: r, predicate: "owl:onProperty", object: pred, kind: k },
|
|
194
224
|
{ subject: r, predicate: "owl:someValuesFrom", object: np2.term, kind: k },
|
|
@@ -199,9 +229,10 @@ function parseRestriction(lexicon, toks, lower, thatIdx) {
|
|
|
199
229
|
}
|
|
200
230
|
|
|
201
231
|
/** Pattern 5 — "every N has at least|at most|exactly n N2" → cardinality
|
|
202
|
-
* restriction on
|
|
203
|
-
* qualified-form question is
|
|
232
|
+
* restriction on `${ns}has` (owl:onClass records the counted class — the
|
|
233
|
+
* qualified-form question is left as a documented open point, see README). */
|
|
204
234
|
function parseCardinality(lexicon, toks, lower, hasIdx) {
|
|
235
|
+
const ns = lexicon.ns;
|
|
205
236
|
let kind = null;
|
|
206
237
|
let nIdx = -1;
|
|
207
238
|
if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "least") { kind = "owl:minCardinality"; nIdx = hasIdx + 3; }
|
|
@@ -212,13 +243,13 @@ function parseCardinality(lexicon, toks, lower, hasIdx) {
|
|
|
212
243
|
if (n == null || nIdx + 1 >= toks.length) return null;
|
|
213
244
|
const np1 = resolveNP(lexicon, toks.slice(1, hasIdx));
|
|
214
245
|
const np2 = resolveNP(lexicon, toks.slice(nIdx + 1));
|
|
215
|
-
if (np1.term == null || np2.term == null) return missOrNull(
|
|
246
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_CARDINALITY, [np1, np2]);
|
|
216
247
|
if (np1.individual || np2.individual) return null;
|
|
217
248
|
const tag = { "owl:minCardinality": "min", "owl:maxCardinality": "max", "owl:cardinality": "exactly" }[kind];
|
|
218
|
-
const r =
|
|
219
|
-
return hit(
|
|
249
|
+
const r = `${ns}${tag}-${n}-${local(lexicon, np2.term)}`;
|
|
250
|
+
return hit(PATTERN_CARDINALITY, [np1, np2], [
|
|
220
251
|
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind },
|
|
221
|
-
{ subject: r, predicate: "owl:onProperty", object:
|
|
252
|
+
{ subject: r, predicate: "owl:onProperty", object: `${ns}has`, kind },
|
|
222
253
|
{ subject: r, predicate: kind, object: String(n), kind, n },
|
|
223
254
|
{ subject: r, predicate: "owl:onClass", object: np2.term, kind },
|
|
224
255
|
{ subject: np1.term, predicate: "rdfs:subClassOf", object: r, kind },
|
|
@@ -239,12 +270,12 @@ function parseEvery(lexicon, toks, lower) {
|
|
|
239
270
|
const rest = toks.slice(isIdx + 1);
|
|
240
271
|
if (rest.length === 1) {
|
|
241
272
|
const adj = lookupAdjective(lexicon, rest[0]);
|
|
242
|
-
if (adj) return adjectiveCopula(
|
|
273
|
+
if (adj) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, adj);
|
|
243
274
|
}
|
|
244
275
|
const np2 = resolveNP(lexicon, rest);
|
|
245
|
-
if (np1.term == null || np2.term == null) return missOrNull(
|
|
276
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
246
277
|
if (np1.individual || np2.individual) return null; // "every X is chat.mjs" — not the fragment
|
|
247
|
-
return hit(
|
|
278
|
+
return hit(PATTERN_SUB_CLASS_OF, [np1, np2], [
|
|
248
279
|
{ subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
|
|
249
280
|
]);
|
|
250
281
|
}
|
|
@@ -255,9 +286,9 @@ function parseDisjoint(lexicon, toks, lower) {
|
|
|
255
286
|
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
256
287
|
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
257
288
|
const np2 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
258
|
-
if (np1.term == null || np2.term == null) return missOrNull(
|
|
289
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_DISJOINT_WITH, [np1, np2]);
|
|
259
290
|
if (np1.individual || np2.individual) return null;
|
|
260
|
-
return hit(
|
|
291
|
+
return hit(PATTERN_DISJOINT_WITH, [np1, np2], [
|
|
261
292
|
{ subject: np1.term, predicate: "owl:disjointWith", object: np2.term, kind: "owl:disjointWith" },
|
|
262
293
|
]);
|
|
263
294
|
}
|
|
@@ -266,21 +297,22 @@ function parseDisjoint(lexicon, toks, lower) {
|
|
|
266
297
|
* property assertion per the possessive noun's DECLARED typing (undeclared
|
|
267
298
|
* typing defaults to data — a literal value is the honest floor). */
|
|
268
299
|
function buildPossessive(lexicon, ownerToks, headToks, valueToks) {
|
|
300
|
+
const ns = lexicon.ns;
|
|
269
301
|
const owner = resolveNP(lexicon, ownerToks);
|
|
270
302
|
if (headToks.length !== 1) return null;
|
|
271
303
|
const head = lookupNoun(lexicon, headToks[0]);
|
|
272
|
-
if (!head) return missOrNull(
|
|
273
|
-
if (owner.term == null) return missOrNull(
|
|
304
|
+
if (!head) return missOrNull(PATTERN_POSSESSIVE, [owner], [headToks[0]]);
|
|
305
|
+
if (owner.term == null) return missOrNull(PATTERN_POSSESSIVE, [owner]);
|
|
274
306
|
if (!valueToks.length) return null;
|
|
275
|
-
const predicate =
|
|
307
|
+
const predicate = `${ns}${head.lemma}`;
|
|
276
308
|
if ((head.property || "data") === "object") {
|
|
277
309
|
const value = resolveNP(lexicon, valueToks);
|
|
278
|
-
if (value.term == null) return missOrNull(
|
|
279
|
-
return hit(
|
|
310
|
+
if (value.term == null) return missOrNull(PATTERN_POSSESSIVE, [owner, value]);
|
|
311
|
+
return hit(PATTERN_POSSESSIVE, [owner, value], [
|
|
280
312
|
{ subject: owner.term, predicate, object: value.term, kind: "owl:ObjectProperty" },
|
|
281
313
|
]);
|
|
282
314
|
}
|
|
283
|
-
return hit(
|
|
315
|
+
return hit(PATTERN_POSSESSIVE, [owner], [
|
|
284
316
|
{ subject: owner.term, predicate, object: valueToks.join(" "), kind: "owl:DatatypeProperty" },
|
|
285
317
|
]);
|
|
286
318
|
}
|
|
@@ -306,25 +338,27 @@ function parseCopula(lexicon, toks, lower, isIdx) {
|
|
|
306
338
|
if (!rest.length) return null;
|
|
307
339
|
if (rest.length === 1) {
|
|
308
340
|
const adj = lookupAdjective(lexicon, rest[0]);
|
|
309
|
-
if (adj) return adjectiveCopula(
|
|
341
|
+
if (adj) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, adj);
|
|
310
342
|
}
|
|
311
343
|
const np2 = resolveNP(lexicon, rest);
|
|
312
344
|
if (np1.term == null || np2.term == null) {
|
|
313
|
-
return missOrNull(np1.individual ?
|
|
345
|
+
return missOrNull(np1.individual ? PATTERN_TYPE_ASSERTION : PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
314
346
|
}
|
|
315
347
|
if (np2.individual) return null; // "chat.mjs is sessions.mjs" — identity is not in the fragment
|
|
316
348
|
if (np1.individual) {
|
|
317
|
-
return hit(
|
|
349
|
+
return hit(PATTERN_TYPE_ASSERTION, [np1, np2], [
|
|
318
350
|
{ subject: np1.term, predicate: "rdf:type", object: np2.term, kind: "rdf:type" },
|
|
319
351
|
]);
|
|
320
352
|
}
|
|
321
|
-
return hit(
|
|
353
|
+
return hit(PATTERN_SUB_CLASS_OF, [np1, np2], [
|
|
322
354
|
{ subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
|
|
323
355
|
]);
|
|
324
356
|
}
|
|
325
357
|
|
|
326
358
|
/** 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
|
|
359
|
+
* header for the result contract; `lexicon` defaults to the committed core
|
|
360
|
+
* under the library's own neutral DEFAULT_NS ("ex:") when the caller doesn't
|
|
361
|
+
* supply one. */
|
|
328
362
|
export function parseAce(sentence, lexicon = loadLexicon()) {
|
|
329
363
|
const toks = tokenize(sentence);
|
|
330
364
|
if (toks.length < 3) return null;
|
package/src/grammar/lexicon.mjs
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
// grammar/lexicon.mjs — the declared lexicon behind tmct's ACE-OWL
|
|
2
|
-
// (ROADMAP Phase 2, item 2;
|
|
1
|
+
// grammar/lexicon.mjs — the declared lexicon behind tmct's ACE-OWL
|
|
2
|
+
// sub-fragment parser (ROADMAP Phase 2, item 2; see ace.mjs). The lexicon is
|
|
3
|
+
// LOAD-BEARING: the grammar is only deterministic because every noun, verb
|
|
4
|
+
// (with any preposition), adjective (with its declared type) and proper name
|
|
5
|
+
// is DECLARED — the parser never guesses a word's category. Undeclared words
|
|
6
|
+
// route a sentence out of the grammar (a miss is a feature, not a bug — see
|
|
7
|
+
// ace.mjs).
|
|
3
8
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// strategy (a miss is a feature: the interpretation pipeline falls through to
|
|
9
|
-
// the tolerant strategies).
|
|
9
|
+
// Data lives in lexicon-core.json (plain, diffable), tmct's starter
|
|
10
|
+
// software-domain vocabulary. Extend it via loadLexicon(extra) with the same
|
|
11
|
+
// JSON shape (extensions.mjs's mergedLexiconExtra); extra entries win on
|
|
12
|
+
// conflict.
|
|
10
13
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
+
// Namespace: every lexicon carries a `.ns` field (the CURIE prefix ace.mjs
|
|
15
|
+
// stamps onto every term it mints) — always "tmct:" here (DEFAULT_NS).
|
|
16
|
+
// ace.mjs and predicateOf() read it off the lexicon rather than hardcoding
|
|
17
|
+
// the prefix inline, purely so a caller can supply its own already-namespaced
|
|
18
|
+
// lexicon; tmct itself only ever runs the one namespace.
|
|
14
19
|
//
|
|
15
20
|
// Morphology is deliberately tiny and deterministic (no NLP dependency): a
|
|
16
21
|
// suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
|
|
@@ -24,6 +29,9 @@ import { dirname, join } from "node:path";
|
|
|
24
29
|
|
|
25
30
|
const CORE_FILE = join(dirname(fileURLToPath(import.meta.url)), "lexicon-core.json");
|
|
26
31
|
|
|
32
|
+
/** The CURIE namespace every tmct lexicon mints terms under. */
|
|
33
|
+
export const DEFAULT_NS = "tmct:";
|
|
34
|
+
|
|
27
35
|
/** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
|
|
28
36
|
export const DETERMINERS = Object.freeze({
|
|
29
37
|
every: "universal",
|
|
@@ -52,9 +60,8 @@ export function numberOf(word) {
|
|
|
52
60
|
return NUMBER_WORDS[w] ?? null;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
/** 3rd-person-singular surface form of a verb lemma
|
|
56
|
-
*
|
|
57
|
-
* the code graph's 3sg relation-kind convention (ask.mjs §verbs). */
|
|
63
|
+
/** 3rd-person-singular surface form of a verb lemma ("import"→imports,
|
|
64
|
+
* "rely"→relies, "catch"→catches, "have"→has) — the predicate spelling. */
|
|
58
65
|
export function thirdPerson(base) {
|
|
59
66
|
const b = String(base);
|
|
60
67
|
if (b === "have") return "has";
|
|
@@ -63,12 +70,14 @@ export function thirdPerson(base) {
|
|
|
63
70
|
return `${b}s`;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
/** The URI-style predicate a verb entry emits: a declared override
|
|
67
|
-
*
|
|
68
|
-
|
|
73
|
+
/** The URI-style predicate a verb entry emits: a declared override
|
|
74
|
+
* (verbEntry.predicate, namespace-independent), or `${ns}<3sg lemma>` with
|
|
75
|
+
* any preposition camel-appended ("depend on" → `${ns}dependsOn`). `ns`
|
|
76
|
+
* defaults to DEFAULT_NS for a caller that doesn't thread one through. */
|
|
77
|
+
export function predicateOf(verbEntry, ns = DEFAULT_NS) {
|
|
69
78
|
if (verbEntry.predicate) return verbEntry.predicate;
|
|
70
79
|
const prep = verbEntry.prep ? verbEntry.prep[0].toUpperCase() + verbEntry.prep.slice(1) : "";
|
|
71
|
-
return
|
|
80
|
+
return `${ns}${thirdPerson(verbEntry.lemma)}${prep}`;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/** Deterministic singular/base-form candidates for a surface word, most
|
|
@@ -115,13 +124,18 @@ function ingest(lex, raw = {}) {
|
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
// Cache keyed by namespace — a no-extra load is immutable at runtime and
|
|
128
|
+
// cached per-ns, so two consumers requesting different namespaces (or the
|
|
129
|
+
// same one repeatedly) each get a stable, shared lexicon object.
|
|
130
|
+
const coreCacheByNs = new Map();
|
|
119
131
|
|
|
120
132
|
/** Load the lexicon: the committed core vocabulary, optionally merged with a
|
|
121
133
|
* caller-supplied `extra` block of the same JSON shape (extra entries win).
|
|
122
|
-
*
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
* `ns` (default DEFAULT_NS) is stamped onto the returned lexicon as `.ns` —
|
|
135
|
+
* the CURIE prefix ace.mjs mints new terms under. The no-extra result is
|
|
136
|
+
* cached per-ns (the JSON is committed, immutable at runtime). */
|
|
137
|
+
export function loadLexicon(extra, ns = DEFAULT_NS) {
|
|
138
|
+
if (!extra && coreCacheByNs.has(ns)) return coreCacheByNs.get(ns);
|
|
125
139
|
const raw = JSON.parse(readFileSync(CORE_FILE, "utf8"));
|
|
126
140
|
const lex = {
|
|
127
141
|
nouns: new Map(),
|
|
@@ -129,13 +143,14 @@ export function loadLexicon(extra) {
|
|
|
129
143
|
verbs: new Map(),
|
|
130
144
|
adjectives: new Map(),
|
|
131
145
|
properNames: new Map(), // lowercased → canonical spelling
|
|
146
|
+
ns,
|
|
132
147
|
};
|
|
133
148
|
ingest(lex, raw);
|
|
134
149
|
if (extra) {
|
|
135
150
|
ingest(lex, extra);
|
|
136
151
|
return lex;
|
|
137
152
|
}
|
|
138
|
-
|
|
153
|
+
coreCacheByNs.set(ns, lex);
|
|
139
154
|
return lex;
|
|
140
155
|
}
|
|
141
156
|
|
|
@@ -194,7 +209,7 @@ export function classify(word, lexicon = loadLexicon()) {
|
|
|
194
209
|
}
|
|
195
210
|
const verb = lookupVerb(lexicon, lower);
|
|
196
211
|
if (verb) {
|
|
197
|
-
return { pos: "verb", type: "objectProperty", lemma: verb.lemma, predicate: predicateOf(verb), ...(verb.prep ? { prep: verb.prep } : {}) };
|
|
212
|
+
return { pos: "verb", type: "objectProperty", lemma: verb.lemma, predicate: predicateOf(verb, lexicon.ns), ...(verb.prep ? { prep: verb.prep } : {}) };
|
|
198
213
|
}
|
|
199
214
|
const adj = lookupAdjective(lexicon, lower);
|
|
200
215
|
if (adj) return { pos: "adjective", type: adj.type, lemma: adj.lemma };
|
|
@@ -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
|