@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.
@@ -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: subClassOf | typeAssertion | relation | someValuesFrom |
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. 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.
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
- const local = (term) => String(term).replace(/^tmct:/, "");
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: `tmct:${proper}`, individual: true, extras: [], unknown: [] };
72
- if (CODE_REF.test(t)) return { term: `tmct:${t}`, individual: true, extras: [], unknown: [] };
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: `tmct:${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
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 = `tmct:${adj.lemma}-${noun.lemma}`;
109
+ const term = `${ns}${adj.lemma}-${noun.lemma}`;
82
110
  const extras = [
83
- { subject: term, predicate: "rdfs:subClassOf", object: `tmct:${noun.lemma}`, kind: "rdfs:subClassOf" },
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: `tmct:${adj.lemma}`, kind: "rdfs:subClassOf" });
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 = `tmct:has-${adj.lemma}`;
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 || `tmct:${adj.lemma}`, kind: "owl:hasValue" },
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("relation", [np1, np2]);
140
- return hit("relation", [np1, np2], [
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: "relation", triples: [], residue: [content[1]] };
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 || `tmct:${adj.lemma}`, object: adj.value ?? "true", kind: "owl:DatatypeProperty" },
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: `tmct:${adj.lemma}`, kind: predicate },
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("someValuesFrom", [np1, np2, np3], [toks[thatIdx + 1]]);
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("someValuesFrom", [np1, np2, np3]);
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 = `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], [
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 tmct:has (owl:onClass records the counted class — the
203
- * qualified-form question is noted in docs/references/schemas/owl2-vocabulary.md). */
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("cardinality", [np1, np2]);
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 = `tmct:${tag}-${n}-${local(np2.term)}`;
219
- return hit("cardinality", [np1, np2], [
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: "tmct:has", kind },
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("adjective", np1, adj);
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("subClassOf", [np1, np2]);
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("subClassOf", [np1, np2], [
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("disjointWith", [np1, np2]);
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("disjointWith", [np1, np2], [
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("possessive", [owner], [headToks[0]]);
273
- if (owner.term == null) return missOrNull("possessive", [owner]);
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 = `tmct:${head.lemma}`;
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("possessive", [owner, value]);
279
- return hit("possessive", [owner, value], [
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("possessive", [owner], [
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("adjective", np1, adj);
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 ? "typeAssertion" : "subClassOf", [np1, np2]);
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("typeAssertion", [np1, np2], [
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("subClassOf", [np1, np2], [
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;
@@ -1,16 +1,21 @@
1
- // grammar/lexicon.mjs — the declared lexicon behind tmct's ACE-OWL sub-fragment
2
- // (ROADMAP Phase 2, item 2; docs/references/schemas/ace-owl-fragment.md).
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
- // The lexicon is LOAD-BEARING: the grammar (grammar/ace.mjs) is only
5
- // deterministic because every noun, verb (with any preposition), adjective
6
- // (with its declared type) and proper name is DECLARED — tmct never guesses a
7
- // word's category. Undeclared words route a sentence out of the grammar
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
- // Data lives in lexicon-core.json (plain, diffable the item-4/7 format
12
- // discipline), a starter software-domain vocabulary. Callers extend it via
13
- // loadLexicon(extra) with the same JSON shape; user entries win on conflict.
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 — 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). */
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, or
67
- * tmct:<3sg lemma> with any preposition camel-appended ("depend on"→tmct:dependsOn). */
68
- export function predicateOf(verbEntry) {
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 `tmct:${thirdPerson(verbEntry.lemma)}${prep}`;
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
- let coreCache = null;
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
- * The no-extra result is cached (the JSON is committed, immutable at runtime). */
123
- export function loadLexicon(extra) {
124
- if (!extra && coreCache) return coreCache;
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
- coreCache = lex;
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 = typeof aceStrategy !== "undefined" ? [aceStrategy] : [];
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