@polycode-projects/the-mechanical-code-talker 1.5.2 → 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/ROADMAP.md +13 -7
- package/package.json +6 -5
- package/src/grammar/ace.mjs +372 -24
- package/src/grammar/lexicon-core.json +302 -0
- package/src/grammar/lexicon.mjs +214 -42
package/ROADMAP.md
CHANGED
|
@@ -1052,19 +1052,25 @@ candidate for the plan's next spike, not a claimed result. Everything else is de
|
|
|
1052
1052
|
plan's open questions, where the relevance problem is named as the open research risk it is.
|
|
1053
1053
|
|
|
1054
1054
|
### Open-source the ACE-OWL parser as a standalone library
|
|
1055
|
-
> **STATUS:
|
|
1056
|
-
>
|
|
1055
|
+
> **STATUS: tried, reverted (2026-07-10).** A session extracted `ace.mjs`/`lexicon.mjs` into
|
|
1056
|
+
> `packages/ace-owl`, a new npm workspace, and pointed tmct's own `package.json` at it as a
|
|
1057
|
+
> registry dependency — but never published the package. That broke `npm install` for tmct
|
|
1058
|
+
> itself (`@polycode-projects/ace-owl` 404s on the public registry, since it only ever existed as
|
|
1059
|
+
> a local workspace symlink). Folded back into `src/grammar/` the same day, on operator
|
|
1060
|
+
> instruction, once the break was found. If this is worth doing again, publish the package FIRST
|
|
1061
|
+
> (or in the same change), never split the two steps across a batch. See `PLAN_OSS_ACE_PARSER.md`.
|
|
1057
1062
|
|
|
1058
1063
|
*(Operator-specified 2026-07-05, from the dependency audit's publish-not-replace finding.)* The
|
|
1059
1064
|
pure-JS, ESM, dependency-free ACE-OWL controlled-grammar parser (`src/grammar/ace.mjs` +
|
|
1060
1065
|
`lexicon.mjs`) that turns controlled-English sentences into OWL-labelled triples is a RARE thing:
|
|
1061
1066
|
the reference implementation (APE) is GPL + SWI-Prolog (native), so there is no permissive,
|
|
1062
1067
|
browser-capable, npm-installable ACE→OWL parser in the JS ecosystem. tmct's is exactly that.
|
|
1063
|
-
|
|
1064
|
-
JS community
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
PageRank+IDF block ranker)
|
|
1068
|
+
Extracting it to its own MPL-2.0 package (tmct depending on it back) would give the wider
|
|
1069
|
+
RDF/OWL/semantic-web JS community a controlled-natural-language front-end that runs in the
|
|
1070
|
+
browser — genuine value, but only once actually published; see `PLAN_OSS_ACE_PARSER.md` for the
|
|
1071
|
+
full design and the note above for what went wrong the first attempt. Sibling publish-candidates
|
|
1072
|
+
(the bounded-Damerau fuzzy matcher, the PageRank+IDF block ranker) are lower priority than getting
|
|
1073
|
+
this one right before trying another.
|
|
1068
1074
|
|
|
1069
1075
|
## Phase 10 — Conversational competence & onboarding (0.6.0 → 0.7.0)
|
|
1070
1076
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -36,9 +36,6 @@
|
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=24"
|
|
38
38
|
},
|
|
39
|
-
"workspaces": [
|
|
40
|
-
"packages/*"
|
|
41
|
-
],
|
|
42
39
|
"bin": {
|
|
43
40
|
"tmct": "./bin/tmct.mjs"
|
|
44
41
|
},
|
|
@@ -71,7 +68,6 @@
|
|
|
71
68
|
"access": "public"
|
|
72
69
|
},
|
|
73
70
|
"dependencies": {
|
|
74
|
-
"@polycode-projects/ace-owl": "^0.1.0",
|
|
75
71
|
"ink": "^7.1.0",
|
|
76
72
|
"react": "^19.2.7",
|
|
77
73
|
"smol-toml": "^1.7.0",
|
|
@@ -83,6 +79,11 @@
|
|
|
83
79
|
"chat": "node bin/tmct.mjs",
|
|
84
80
|
"chat:repo": "node bin/tmct.mjs chat --repo",
|
|
85
81
|
"init": "node bin/tmct.mjs init",
|
|
82
|
+
"init:persona:code": "node bin/tmct.mjs init --with-persona code",
|
|
83
|
+
"init:corpus:general": "node bin/tmct.mjs init --corpus general",
|
|
84
|
+
"init:corpus:aws": "node bin/tmct.mjs init --corpus aws",
|
|
85
|
+
"init:corpus:python": "node bin/tmct.mjs init --corpus python",
|
|
86
|
+
"init:corpus:java": "node bin/tmct.mjs init --corpus java",
|
|
86
87
|
"memory": "node bin/tmct.mjs memory",
|
|
87
88
|
"syllogise": "node bin/tmct.mjs syllogise",
|
|
88
89
|
"example:mini": "node bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
|
package/src/grammar/ace.mjs
CHANGED
|
@@ -1,27 +1,375 @@
|
|
|
1
|
-
// grammar/ace.mjs —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `parseAce(sentence)` with no lexicon argument keeps getting "tmct:"-
|
|
10
|
-
// prefixed triples, byte-identical to before the extraction.
|
|
1
|
+
// grammar/ace.mjs — tmct's deterministic ACE-OWL sub-fragment parser (ROADMAP
|
|
2
|
+
// Phase 2, item 2). Implements the 8 controlled-English sentence patterns of
|
|
3
|
+
// docs/references/schemas/ace-owl-fragment.md and nothing more: fitting the
|
|
4
|
+
// grammar is a strong signal, missing it is a FEATURE — parseAce returns null
|
|
5
|
+
// (or an empty-triples result carrying the unknown words as `residue`) and the
|
|
6
|
+
// interpretation pipeline (src/interpret/) falls through to the tolerant
|
|
7
|
+
// strategies. No NLP dependency: tokenization is whitespace + trailing
|
|
8
|
+
// punctuation, morphology is the lexicon's suffix fold.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
// parseAce(sentence, lexicon) → { pattern, triples, residue } | null
|
|
11
|
+
// pattern one of the PATTERNS below (also exported individually).
|
|
12
|
+
// triples [{ subject, predicate, object, kind, n? }] — OWL-labelled string
|
|
13
|
+
// triples shaped for src/memory/core.mjs's appendFact (which
|
|
14
|
+
// normalizes subject/object via normFactTerm: "tmct:module" is
|
|
15
|
+
// stored as "module"; the predicate keeps its vocabulary casing).
|
|
16
|
+
// residue [] on a clean parse; the unknown tokens when the sentence FITS a
|
|
17
|
+
// pattern structurally but uses undeclared words (triples is then
|
|
18
|
+
// empty — feeds the pipeline's "if you mean X…" surround).
|
|
19
|
+
// null the sentence does not fit the fragment at all.
|
|
20
|
+
//
|
|
21
|
+
// Term style: classes/individuals are `tmct:<lexeme>` CURIEs (lexicon lemma
|
|
22
|
+
// for nouns, canonical spelling for proper names, the literal token for
|
|
23
|
+
// code-shaped references like chat.mjs); predicates are the OWL/RDF(S)
|
|
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.
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName,
|
|
39
|
+
predicateOf, numberOf, classify,
|
|
40
|
+
} from "./lexicon.mjs";
|
|
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
|
+
|
|
57
|
+
const DET = new Set(["a", "an", "the"]);
|
|
58
|
+
// A token SHAPED like a code reference (a path, file, symbol or CURIE) is an
|
|
59
|
+
// individual by form — a deterministic tokenizer rule, not a guess: declared
|
|
60
|
+
// proper names cover words; this covers chat.mjs, src/ask.mjs, Foo#bar.
|
|
61
|
+
const CODE_REF = /[./\\#:@]/;
|
|
62
|
+
|
|
63
|
+
/** Whitespace tokenizer: curly quotes normalized, commas/semicolons dropped,
|
|
64
|
+
* ONE trailing punctuation run stripped (so "chat.mjs." keeps its dots). */
|
|
65
|
+
export function tokenize(sentence) {
|
|
66
|
+
return String(sentence ?? "")
|
|
67
|
+
.replace(/[‘’]/g, "'")
|
|
68
|
+
.replace(/[,;]/g, " ")
|
|
69
|
+
.replace(/[?!.]+\s*$/, "")
|
|
70
|
+
.trim()
|
|
71
|
+
.split(/\s+/)
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
|
|
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
|
+
}
|
|
84
|
+
|
|
85
|
+
const stripDet = (tokens) =>
|
|
86
|
+
tokens.length > 1 && DET.has(tokens[0].toLowerCase()) ? tokens.slice(1) : tokens;
|
|
87
|
+
|
|
88
|
+
/** Resolve a 1–2 word noun phrase: PROPERNAME | code-ref | NOUN | ADJ NOUN.
|
|
89
|
+
* Returns { term, individual, extras, unknown } — `term` null on a miss with
|
|
90
|
+
* the undeclared tokens in `unknown` (empty `unknown` = structurally
|
|
91
|
+
* unparseable phrase → the caller returns a hard null). `extras` carries the
|
|
92
|
+
* pattern-8 adjective triples (subclass axioms / hasValue restriction). */
|
|
93
|
+
function resolveNP(lexicon, tokensIn) {
|
|
94
|
+
const ns = lexicon.ns;
|
|
95
|
+
const tokens = stripDet(tokensIn);
|
|
96
|
+
if (tokens.length === 1) {
|
|
97
|
+
const t = tokens[0];
|
|
98
|
+
const proper = lookupProperName(lexicon, t);
|
|
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: [] };
|
|
101
|
+
const noun = lookupNoun(lexicon, t);
|
|
102
|
+
if (noun) return { term: `${ns}${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
|
|
103
|
+
return { term: null, individual: false, extras: [], unknown: [t] };
|
|
104
|
+
}
|
|
105
|
+
if (tokens.length === 2) {
|
|
106
|
+
const adj = lookupAdjective(lexicon, tokens[0]);
|
|
107
|
+
const noun = lookupNoun(lexicon, tokens[1]);
|
|
108
|
+
if (adj && noun) {
|
|
109
|
+
const term = `${ns}${adj.lemma}-${noun.lemma}`;
|
|
110
|
+
const extras = [
|
|
111
|
+
{ subject: term, predicate: "rdfs:subClassOf", object: `${ns}${noun.lemma}`, kind: "rdfs:subClassOf" },
|
|
112
|
+
];
|
|
113
|
+
if (adj.type === "subclass") {
|
|
114
|
+
// the adjective itself denotes a class: legacy-module ⊑ module, ⊑ legacy
|
|
115
|
+
extras.push({ subject: term, predicate: "rdfs:subClassOf", object: `${ns}${adj.lemma}`, kind: "rdfs:subClassOf" });
|
|
116
|
+
} else {
|
|
117
|
+
// data adjective: subclass-with-restriction on the boolean-ish property
|
|
118
|
+
const r = `${ns}has-${adj.lemma}`;
|
|
119
|
+
extras.push(
|
|
120
|
+
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: "owl:hasValue" },
|
|
121
|
+
{ subject: r, predicate: "owl:onProperty", object: adj.property || `${ns}${adj.lemma}`, kind: "owl:hasValue" },
|
|
122
|
+
{ subject: r, predicate: "owl:hasValue", object: adj.value ?? "true", kind: "owl:hasValue" },
|
|
123
|
+
{ subject: term, predicate: "rdfs:subClassOf", object: r, kind: "owl:hasValue" },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return { term, individual: false, noun, extras, unknown: [] };
|
|
127
|
+
}
|
|
128
|
+
// only genuinely undeclared words are residue — a declared word in the
|
|
129
|
+
// wrong slot ("GitLab pipeline") is a structural miss, not an unknown
|
|
130
|
+
const unknown = tokens.filter((t) => !classify(t, lexicon));
|
|
131
|
+
return { term: null, individual: false, extras: [], unknown };
|
|
132
|
+
}
|
|
133
|
+
// 0 or 3+ tokens: not a fragment NP. Name the undeclared words if any.
|
|
134
|
+
return { term: null, individual: false, extras: [], unknown: tokens.filter((t) => !classify(t, lexicon)) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The shared miss result: a structural fit with undeclared words returns the
|
|
138
|
+
* pattern + residue (triples empty); a fit with only declared-but-unusable
|
|
139
|
+
* phrasing returns null — the honest fall-through either way. */
|
|
140
|
+
function missOrNull(pattern, nps, extraUnknown = []) {
|
|
141
|
+
const residue = [...extraUnknown, ...nps.flatMap((np) => np.unknown)];
|
|
142
|
+
return residue.length ? { pattern, triples: [], residue } : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hit = (pattern, nps, triples, more = {}) => ({
|
|
146
|
+
pattern,
|
|
147
|
+
triples: [...nps.flatMap((np) => np.extras), ...triples],
|
|
148
|
+
residue: [],
|
|
149
|
+
...more,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** Pattern 3 — "N1 VERB N2" / "PROPERNAME VERBs PROPERNAME" → object-property
|
|
153
|
+
* assertion. Also the no-declared-verb 3-token shape: both ends resolvable →
|
|
154
|
+
* residue names the middle token (the future "if you mean X…" hook). */
|
|
155
|
+
function parseRelation(lexicon, toks, lower) {
|
|
156
|
+
for (let i = 1; i < toks.length - 1; i += 1) {
|
|
157
|
+
const verb = lookupVerb(lexicon, lower[i]);
|
|
158
|
+
if (!verb) continue;
|
|
159
|
+
let objStart = i + 1;
|
|
160
|
+
if (verb.prep) {
|
|
161
|
+
if (lower[objStart] !== verb.prep) continue;
|
|
162
|
+
objStart += 1;
|
|
163
|
+
if (objStart >= toks.length) continue;
|
|
164
|
+
}
|
|
165
|
+
const np1 = resolveNP(lexicon, toks.slice(0, i));
|
|
166
|
+
const np2 = resolveNP(lexicon, toks.slice(objStart));
|
|
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" },
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
const content = toks.filter((t) => !DET.has(t.toLowerCase()));
|
|
173
|
+
if (content.length === 3 && !classify(content[1], lexicon)) {
|
|
174
|
+
const np1 = resolveNP(lexicon, [content[0]]);
|
|
175
|
+
const np2 = resolveNP(lexicon, [content[2]]);
|
|
176
|
+
if (np1.term != null && np2.term != null) return { pattern: PATTERN_RELATION, triples: [], residue: [content[1]] };
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Pattern 8 (copula arm) — "X is ADJ": data adjective → datatype-property
|
|
182
|
+
* assertion; subclass adjective → rdf:type (individual) / rdfs:subClassOf. */
|
|
183
|
+
function adjectiveCopula(lexicon, pattern, np1, adj) {
|
|
184
|
+
const ns = lexicon.ns;
|
|
185
|
+
if (np1.term == null) return missOrNull(pattern, [np1]);
|
|
186
|
+
if (adj.type === "data") {
|
|
187
|
+
return hit(pattern, [np1], [
|
|
188
|
+
{ subject: np1.term, predicate: adj.property || `${ns}${adj.lemma}`, object: adj.value ?? "true", kind: "owl:DatatypeProperty" },
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
const predicate = np1.individual ? "rdf:type" : "rdfs:subClassOf";
|
|
192
|
+
return hit(pattern, [np1], [
|
|
193
|
+
{ subject: np1.term, predicate, object: `${ns}${adj.lemma}`, kind: predicate },
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Pattern 4 — "every N1 that VERBs a N2 is a N3" → someValuesFrom restriction:
|
|
198
|
+
* (N1 ⊓ ∃VERB.N2) ⊑ N3, flattened onto readable deterministic node names. */
|
|
199
|
+
function parseRestriction(lexicon, toks, lower, thatIdx) {
|
|
200
|
+
const ns = lexicon.ns;
|
|
201
|
+
const isIdx = lower.indexOf("is", thatIdx + 2);
|
|
202
|
+
if (isIdx < 0 || thatIdx + 1 >= isIdx) return null;
|
|
203
|
+
const verb = lookupVerb(lexicon, lower[thatIdx + 1]);
|
|
204
|
+
const np1 = resolveNP(lexicon, toks.slice(1, thatIdx));
|
|
205
|
+
let objStart = thatIdx + 2;
|
|
206
|
+
if (verb?.prep) {
|
|
207
|
+
if (lower[objStart] !== verb.prep) return null;
|
|
208
|
+
objStart += 1;
|
|
209
|
+
}
|
|
210
|
+
const np2 = resolveNP(lexicon, toks.slice(objStart, isIdx));
|
|
211
|
+
const np3 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
212
|
+
if (!verb) return missOrNull(PATTERN_SOME_VALUES_FROM, [np1, np2, np3], [toks[thatIdx + 1]]);
|
|
213
|
+
if (np1.term == null || np2.term == null || np3.term == null) {
|
|
214
|
+
return missOrNull(PATTERN_SOME_VALUES_FROM, [np1, np2, np3]);
|
|
215
|
+
}
|
|
216
|
+
if (np1.individual || np2.individual || np3.individual) return null; // class-level pattern only
|
|
217
|
+
const pred = predicateOf(verb, ns);
|
|
218
|
+
const k = "owl:someValuesFrom";
|
|
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], [
|
|
222
|
+
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: k },
|
|
223
|
+
{ subject: r, predicate: "owl:onProperty", object: pred, kind: k },
|
|
224
|
+
{ subject: r, predicate: "owl:someValuesFrom", object: np2.term, kind: k },
|
|
225
|
+
{ subject: inter, predicate: "owl:intersectionOf", object: np1.term, kind: k },
|
|
226
|
+
{ subject: inter, predicate: "owl:intersectionOf", object: r, kind: k },
|
|
227
|
+
{ subject: inter, predicate: "rdfs:subClassOf", object: np3.term, kind: k },
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Pattern 5 — "every N has at least|at most|exactly n N2" → cardinality
|
|
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). */
|
|
234
|
+
function parseCardinality(lexicon, toks, lower, hasIdx) {
|
|
235
|
+
const ns = lexicon.ns;
|
|
236
|
+
let kind = null;
|
|
237
|
+
let nIdx = -1;
|
|
238
|
+
if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "least") { kind = "owl:minCardinality"; nIdx = hasIdx + 3; }
|
|
239
|
+
else if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "most") { kind = "owl:maxCardinality"; nIdx = hasIdx + 3; }
|
|
240
|
+
else if (lower[hasIdx + 1] === "exactly") { kind = "owl:cardinality"; nIdx = hasIdx + 2; }
|
|
241
|
+
else return null;
|
|
242
|
+
const n = numberOf(lower[nIdx]);
|
|
243
|
+
if (n == null || nIdx + 1 >= toks.length) return null;
|
|
244
|
+
const np1 = resolveNP(lexicon, toks.slice(1, hasIdx));
|
|
245
|
+
const np2 = resolveNP(lexicon, toks.slice(nIdx + 1));
|
|
246
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_CARDINALITY, [np1, np2]);
|
|
247
|
+
if (np1.individual || np2.individual) return null;
|
|
248
|
+
const tag = { "owl:minCardinality": "min", "owl:maxCardinality": "max", "owl:cardinality": "exactly" }[kind];
|
|
249
|
+
const r = `${ns}${tag}-${n}-${local(lexicon, np2.term)}`;
|
|
250
|
+
return hit(PATTERN_CARDINALITY, [np1, np2], [
|
|
251
|
+
{ subject: r, predicate: "rdf:type", object: "owl:Restriction", kind },
|
|
252
|
+
{ subject: r, predicate: "owl:onProperty", object: `${ns}has`, kind },
|
|
253
|
+
{ subject: r, predicate: kind, object: String(n), kind, n },
|
|
254
|
+
{ subject: r, predicate: "owl:onClass", object: np2.term, kind },
|
|
255
|
+
{ subject: np1.term, predicate: "rdfs:subClassOf", object: r, kind },
|
|
256
|
+
], { n });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Patterns 1, 4, 5 and 8's "every …" arm. */
|
|
260
|
+
function parseEvery(lexicon, toks, lower) {
|
|
261
|
+
const thatIdx = lower.indexOf("that");
|
|
262
|
+
if (thatIdx > 1) return parseRestriction(lexicon, toks, lower, thatIdx);
|
|
263
|
+
const hasIdx = lower.indexOf("has");
|
|
264
|
+
if (hasIdx > 1 && (lower[hasIdx + 1] === "at" || lower[hasIdx + 1] === "exactly")) {
|
|
265
|
+
return parseCardinality(lexicon, toks, lower, hasIdx);
|
|
266
|
+
}
|
|
267
|
+
const isIdx = lower.indexOf("is");
|
|
268
|
+
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
269
|
+
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
270
|
+
const rest = toks.slice(isIdx + 1);
|
|
271
|
+
if (rest.length === 1) {
|
|
272
|
+
const adj = lookupAdjective(lexicon, rest[0]);
|
|
273
|
+
if (adj) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, adj);
|
|
274
|
+
}
|
|
275
|
+
const np2 = resolveNP(lexicon, rest);
|
|
276
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
277
|
+
if (np1.individual || np2.individual) return null; // "every X is chat.mjs" — not the fragment
|
|
278
|
+
return hit(PATTERN_SUB_CLASS_OF, [np1, np2], [
|
|
279
|
+
{ subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Pattern 6 — "no N1 is a N2" → owl:disjointWith. */
|
|
284
|
+
function parseDisjoint(lexicon, toks, lower) {
|
|
285
|
+
const isIdx = lower.indexOf("is");
|
|
286
|
+
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
287
|
+
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
288
|
+
const np2 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
289
|
+
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_DISJOINT_WITH, [np1, np2]);
|
|
290
|
+
if (np1.individual || np2.individual) return null;
|
|
291
|
+
return hit(PATTERN_DISJOINT_WITH, [np1, np2], [
|
|
292
|
+
{ subject: np1.term, predicate: "owl:disjointWith", object: np2.term, kind: "owl:disjointWith" },
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Pattern 7 — "N1's N2 is VALUE" / "the N2 of N1 is VALUE": data or object
|
|
297
|
+
* property assertion per the possessive noun's DECLARED typing (undeclared
|
|
298
|
+
* typing defaults to data — a literal value is the honest floor). */
|
|
299
|
+
function buildPossessive(lexicon, ownerToks, headToks, valueToks) {
|
|
300
|
+
const ns = lexicon.ns;
|
|
301
|
+
const owner = resolveNP(lexicon, ownerToks);
|
|
302
|
+
if (headToks.length !== 1) return null;
|
|
303
|
+
const head = lookupNoun(lexicon, headToks[0]);
|
|
304
|
+
if (!head) return missOrNull(PATTERN_POSSESSIVE, [owner], [headToks[0]]);
|
|
305
|
+
if (owner.term == null) return missOrNull(PATTERN_POSSESSIVE, [owner]);
|
|
306
|
+
if (!valueToks.length) return null;
|
|
307
|
+
const predicate = `${ns}${head.lemma}`;
|
|
308
|
+
if ((head.property || "data") === "object") {
|
|
309
|
+
const value = resolveNP(lexicon, valueToks);
|
|
310
|
+
if (value.term == null) return missOrNull(PATTERN_POSSESSIVE, [owner, value]);
|
|
311
|
+
return hit(PATTERN_POSSESSIVE, [owner, value], [
|
|
312
|
+
{ subject: owner.term, predicate, object: value.term, kind: "owl:ObjectProperty" },
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
return hit(PATTERN_POSSESSIVE, [owner], [
|
|
316
|
+
{ subject: owner.term, predicate, object: valueToks.join(" "), kind: "owl:DatatypeProperty" },
|
|
317
|
+
]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parsePossessive(lexicon, toks, lower) {
|
|
321
|
+
const ownerRaw = toks[0].replace(/'s$/i, "");
|
|
322
|
+
const isIdx = lower.indexOf("is");
|
|
323
|
+
if (isIdx < 2 || !ownerRaw) return null;
|
|
324
|
+
return buildPossessive(lexicon, [ownerRaw], toks.slice(1, isIdx), toks.slice(isIdx + 1));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseOfForm(lexicon, toks, lower) {
|
|
328
|
+
const ofIdx = lower.indexOf("of");
|
|
329
|
+
const isIdx = lower.indexOf("is", ofIdx + 1);
|
|
330
|
+
if (ofIdx < 2 || isIdx < ofIdx + 2) return null;
|
|
331
|
+
return buildPossessive(lexicon, toks.slice(ofIdx + 1, isIdx), toks.slice(1, ofIdx), toks.slice(isIdx + 1));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Patterns 2 (class assertion), 1's bare-copula variant, and 8's copula arm. */
|
|
335
|
+
function parseCopula(lexicon, toks, lower, isIdx) {
|
|
336
|
+
const np1 = resolveNP(lexicon, toks.slice(0, isIdx));
|
|
337
|
+
const rest = toks.slice(isIdx + 1);
|
|
338
|
+
if (!rest.length) return null;
|
|
339
|
+
if (rest.length === 1) {
|
|
340
|
+
const adj = lookupAdjective(lexicon, rest[0]);
|
|
341
|
+
if (adj) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, adj);
|
|
342
|
+
}
|
|
343
|
+
const np2 = resolveNP(lexicon, rest);
|
|
344
|
+
if (np1.term == null || np2.term == null) {
|
|
345
|
+
return missOrNull(np1.individual ? PATTERN_TYPE_ASSERTION : PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
346
|
+
}
|
|
347
|
+
if (np2.individual) return null; // "chat.mjs is sessions.mjs" — identity is not in the fragment
|
|
348
|
+
if (np1.individual) {
|
|
349
|
+
return hit(PATTERN_TYPE_ASSERTION, [np1, np2], [
|
|
350
|
+
{ subject: np1.term, predicate: "rdf:type", object: np2.term, kind: "rdf:type" },
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
return hit(PATTERN_SUB_CLASS_OF, [np1, np2], [
|
|
354
|
+
{ subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Parse one sentence against the 8-pattern ACE-OWL sub-fragment. See the file
|
|
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. */
|
|
25
362
|
export function parseAce(sentence, lexicon = loadLexicon()) {
|
|
26
|
-
|
|
363
|
+
const toks = tokenize(sentence);
|
|
364
|
+
if (toks.length < 3) return null;
|
|
365
|
+
const lower = toks.map((t) => t.toLowerCase());
|
|
366
|
+
if (lower[0] === "every") return parseEvery(lexicon, toks, lower);
|
|
367
|
+
if (lower[0] === "no") return parseDisjoint(lexicon, toks, lower);
|
|
368
|
+
if (/'s$/.test(lower[0]) && lower[0].length > 2) return parsePossessive(lexicon, toks, lower);
|
|
369
|
+
if (lower[0] === "the" && lower.includes("of") && lower.includes("is")) {
|
|
370
|
+
return parseOfForm(lexicon, toks, lower);
|
|
371
|
+
}
|
|
372
|
+
const isIdx = lower.indexOf("is");
|
|
373
|
+
if (isIdx > 0) return parseCopula(lexicon, toks, lower, isIdx);
|
|
374
|
+
return parseRelation(lexicon, toks, lower);
|
|
27
375
|
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comment": "tmct's starter software-domain lexicon (ROADMAP Phase 2, item 2). Every word the ACE-OWL sub-fragment parser (src/grammar/ace.mjs) is allowed to understand is DECLARED here — tmct never guesses a word's category. Nouns may declare a possessive property typing ('data' or 'object', pattern 7); adjectives MUST declare a type ('subclass' forms a class, 'data' asserts a boolean-ish datatype property, pattern 8); verbs may declare a preposition ('depend' + 'on' → dependsOn). Extend via loadLexicon(extra) with this same shape.",
|
|
3
|
+
"nouns": {
|
|
4
|
+
"module": {},
|
|
5
|
+
"class": {},
|
|
6
|
+
"function": {},
|
|
7
|
+
"method": {},
|
|
8
|
+
"attribute": {},
|
|
9
|
+
"variable": {},
|
|
10
|
+
"constant": {},
|
|
11
|
+
"test": {},
|
|
12
|
+
"suite": {},
|
|
13
|
+
"service": {},
|
|
14
|
+
"repository": {},
|
|
15
|
+
"branch": {},
|
|
16
|
+
"commit": {},
|
|
17
|
+
"tag": {},
|
|
18
|
+
"release": {},
|
|
19
|
+
"package": {},
|
|
20
|
+
"library": {},
|
|
21
|
+
"framework": {},
|
|
22
|
+
"api": {},
|
|
23
|
+
"endpoint": {},
|
|
24
|
+
"route": {},
|
|
25
|
+
"handler": {},
|
|
26
|
+
"controller": {},
|
|
27
|
+
"model": {},
|
|
28
|
+
"view": {},
|
|
29
|
+
"template": {},
|
|
30
|
+
"component": {},
|
|
31
|
+
"interface": {},
|
|
32
|
+
"type": {},
|
|
33
|
+
"category": {},
|
|
34
|
+
"kind": {},
|
|
35
|
+
"artifact": {},
|
|
36
|
+
"routine": {},
|
|
37
|
+
"part": {},
|
|
38
|
+
"helper": {},
|
|
39
|
+
"operation": {},
|
|
40
|
+
"change": {},
|
|
41
|
+
"schema": {},
|
|
42
|
+
"database": {},
|
|
43
|
+
"table": {},
|
|
44
|
+
"query": {},
|
|
45
|
+
"index": { "plural": "indices" },
|
|
46
|
+
"cache": {},
|
|
47
|
+
"queue": {},
|
|
48
|
+
"worker": {},
|
|
49
|
+
"job": {},
|
|
50
|
+
"task": {},
|
|
51
|
+
"bug": {},
|
|
52
|
+
"defect": {},
|
|
53
|
+
"issue": {},
|
|
54
|
+
"ticket": {},
|
|
55
|
+
"feature": {},
|
|
56
|
+
"requirement": {},
|
|
57
|
+
"specification": {},
|
|
58
|
+
"document": {},
|
|
59
|
+
"developer": {},
|
|
60
|
+
"engineer": {},
|
|
61
|
+
"user": {},
|
|
62
|
+
"visitor": {},
|
|
63
|
+
"team": {},
|
|
64
|
+
"project": {},
|
|
65
|
+
"codebase": {},
|
|
66
|
+
"file": {},
|
|
67
|
+
"directory": {},
|
|
68
|
+
"folder": {},
|
|
69
|
+
"line": {},
|
|
70
|
+
"symbol": {},
|
|
71
|
+
"identifier": {},
|
|
72
|
+
"comment": {},
|
|
73
|
+
"docstring": {},
|
|
74
|
+
"string": {},
|
|
75
|
+
"number": {},
|
|
76
|
+
"list": {},
|
|
77
|
+
"array": {},
|
|
78
|
+
"graph": {},
|
|
79
|
+
"node": {},
|
|
80
|
+
"edge": {},
|
|
81
|
+
"triple": {},
|
|
82
|
+
"fact": {},
|
|
83
|
+
"ontology": {},
|
|
84
|
+
"lexicon": {},
|
|
85
|
+
"grammar": {},
|
|
86
|
+
"sentence": {},
|
|
87
|
+
"utterance": {},
|
|
88
|
+
"session": {},
|
|
89
|
+
"response": {},
|
|
90
|
+
"request": {},
|
|
91
|
+
"question": {},
|
|
92
|
+
"answer": {},
|
|
93
|
+
"error": {},
|
|
94
|
+
"exception": {},
|
|
95
|
+
"warning": {},
|
|
96
|
+
"log": {},
|
|
97
|
+
"metric": {},
|
|
98
|
+
"benchmark": {},
|
|
99
|
+
"pipeline": {},
|
|
100
|
+
"build": {},
|
|
101
|
+
"deployment": {},
|
|
102
|
+
"environment": {},
|
|
103
|
+
"config": {},
|
|
104
|
+
"configuration": {},
|
|
105
|
+
"setting": {},
|
|
106
|
+
"option": {},
|
|
107
|
+
"flag": {},
|
|
108
|
+
"argument": {},
|
|
109
|
+
"parameter": {},
|
|
110
|
+
"value": {},
|
|
111
|
+
"result": {},
|
|
112
|
+
"output": {},
|
|
113
|
+
"input": {},
|
|
114
|
+
"server": {},
|
|
115
|
+
"client": {},
|
|
116
|
+
"protocol": {},
|
|
117
|
+
"message": {},
|
|
118
|
+
"event": {},
|
|
119
|
+
"hook": {},
|
|
120
|
+
"plugin": {},
|
|
121
|
+
"script": {},
|
|
122
|
+
"tool": {},
|
|
123
|
+
"command": {},
|
|
124
|
+
"prompt": {},
|
|
125
|
+
"token": {},
|
|
126
|
+
"parser": {},
|
|
127
|
+
"compiler": {},
|
|
128
|
+
"linter": {},
|
|
129
|
+
"formatter": {},
|
|
130
|
+
"runtime": {},
|
|
131
|
+
"process": {},
|
|
132
|
+
"thread": {},
|
|
133
|
+
"loop": {},
|
|
134
|
+
"statement": {},
|
|
135
|
+
"expression": {},
|
|
136
|
+
"keyword": {},
|
|
137
|
+
"scope": {},
|
|
138
|
+
"callback": {},
|
|
139
|
+
"promise": {},
|
|
140
|
+
"iterator": {},
|
|
141
|
+
"generator": {},
|
|
142
|
+
"unit": {},
|
|
143
|
+
"risk": {},
|
|
144
|
+
"prototype": {},
|
|
145
|
+
"milestone": {},
|
|
146
|
+
"sprint": {},
|
|
147
|
+
"backlog": {},
|
|
148
|
+
"roadmap": {},
|
|
149
|
+
"phase": {},
|
|
150
|
+
"pattern": {},
|
|
151
|
+
"smell": {},
|
|
152
|
+
"coverage": {},
|
|
153
|
+
"mock": {},
|
|
154
|
+
"stub": {},
|
|
155
|
+
"fixture": {},
|
|
156
|
+
"assertion": {},
|
|
157
|
+
"snapshot": {},
|
|
158
|
+
"regression": {},
|
|
159
|
+
"migration": {},
|
|
160
|
+
"refactor": {},
|
|
161
|
+
"review": {},
|
|
162
|
+
"merge": {},
|
|
163
|
+
"license": { "property": "data" },
|
|
164
|
+
"version": { "property": "data" },
|
|
165
|
+
"name": { "property": "data" },
|
|
166
|
+
"path": { "property": "data" },
|
|
167
|
+
"size": { "property": "data" },
|
|
168
|
+
"status": { "property": "data" },
|
|
169
|
+
"language": { "property": "data" },
|
|
170
|
+
"extension": { "property": "data" },
|
|
171
|
+
"churn": { "property": "data" },
|
|
172
|
+
"impact": { "property": "data" },
|
|
173
|
+
"complexity": { "property": "data" },
|
|
174
|
+
"latency": { "property": "data" },
|
|
175
|
+
"duration": { "property": "data" },
|
|
176
|
+
"frequency": { "property": "data" },
|
|
177
|
+
"severity": { "property": "data" },
|
|
178
|
+
"owner": { "property": "object" },
|
|
179
|
+
"maintainer": { "property": "object" },
|
|
180
|
+
"author": { "property": "object" },
|
|
181
|
+
"reviewer": { "property": "object" },
|
|
182
|
+
"parent": { "property": "object" },
|
|
183
|
+
"dependency": { "property": "object" }
|
|
184
|
+
},
|
|
185
|
+
"verbs": {
|
|
186
|
+
"import": {},
|
|
187
|
+
"call": {},
|
|
188
|
+
"test": {},
|
|
189
|
+
"contain": {},
|
|
190
|
+
"extend": {},
|
|
191
|
+
"use": {},
|
|
192
|
+
"depend": { "prep": "on" },
|
|
193
|
+
"rely": { "prep": "on" },
|
|
194
|
+
"inherit": { "prep": "from" },
|
|
195
|
+
"belong": { "prep": "to" },
|
|
196
|
+
"point": { "prep": "to" },
|
|
197
|
+
"implement": {},
|
|
198
|
+
"override": {},
|
|
199
|
+
"export": {},
|
|
200
|
+
"define": {},
|
|
201
|
+
"declare": {},
|
|
202
|
+
"reference": {},
|
|
203
|
+
"invoke": {},
|
|
204
|
+
"wrap": {},
|
|
205
|
+
"mock": {},
|
|
206
|
+
"cover": {},
|
|
207
|
+
"document": {},
|
|
208
|
+
"describe": {},
|
|
209
|
+
"modify": {},
|
|
210
|
+
"touch": {},
|
|
211
|
+
"fix": {},
|
|
212
|
+
"break": {},
|
|
213
|
+
"introduce": {},
|
|
214
|
+
"deprecate": {},
|
|
215
|
+
"replace": {},
|
|
216
|
+
"own": {},
|
|
217
|
+
"maintain": {},
|
|
218
|
+
"review": {},
|
|
219
|
+
"merge": {},
|
|
220
|
+
"revert": {},
|
|
221
|
+
"deploy": {},
|
|
222
|
+
"run": {},
|
|
223
|
+
"execute": {},
|
|
224
|
+
"load": {},
|
|
225
|
+
"parse": {},
|
|
226
|
+
"emit": {},
|
|
227
|
+
"validate": {},
|
|
228
|
+
"log": {},
|
|
229
|
+
"throw": {},
|
|
230
|
+
"catch": {},
|
|
231
|
+
"create": {},
|
|
232
|
+
"delete": {},
|
|
233
|
+
"update": {},
|
|
234
|
+
"expose": {},
|
|
235
|
+
"consume": {},
|
|
236
|
+
"produce": {},
|
|
237
|
+
"generate": {},
|
|
238
|
+
"configure": {},
|
|
239
|
+
"install": {},
|
|
240
|
+
"publish": {},
|
|
241
|
+
"release": {},
|
|
242
|
+
"ship": {},
|
|
243
|
+
"watch": {},
|
|
244
|
+
"trigger": {},
|
|
245
|
+
"build": {},
|
|
246
|
+
"write": {},
|
|
247
|
+
"read": {},
|
|
248
|
+
"have": {}
|
|
249
|
+
},
|
|
250
|
+
"adjectives": {
|
|
251
|
+
"legacy": { "type": "subclass" },
|
|
252
|
+
"internal": { "type": "subclass" },
|
|
253
|
+
"external": { "type": "subclass" },
|
|
254
|
+
"public": { "type": "subclass" },
|
|
255
|
+
"private": { "type": "subclass" },
|
|
256
|
+
"abstract": { "type": "subclass" },
|
|
257
|
+
"static": { "type": "subclass" },
|
|
258
|
+
"async": { "type": "subclass" },
|
|
259
|
+
"experimental": { "type": "subclass" },
|
|
260
|
+
"stable": { "type": "subclass" },
|
|
261
|
+
"core": { "type": "subclass" },
|
|
262
|
+
"shared": { "type": "subclass" },
|
|
263
|
+
"global": { "type": "subclass" },
|
|
264
|
+
"local": { "type": "subclass" },
|
|
265
|
+
"generated": { "type": "subclass" },
|
|
266
|
+
"standalone": { "type": "subclass" },
|
|
267
|
+
"primary": { "type": "subclass" },
|
|
268
|
+
"secondary": { "type": "subclass" },
|
|
269
|
+
"deprecated": { "type": "data" },
|
|
270
|
+
"fast": { "type": "data" },
|
|
271
|
+
"slow": { "type": "data" },
|
|
272
|
+
"large": { "type": "data" },
|
|
273
|
+
"small": { "type": "data" },
|
|
274
|
+
"flaky": { "type": "data" },
|
|
275
|
+
"tested": { "type": "data" },
|
|
276
|
+
"documented": { "type": "data" },
|
|
277
|
+
"buggy": { "type": "data" },
|
|
278
|
+
"broken": { "type": "data" },
|
|
279
|
+
"green": { "type": "data" },
|
|
280
|
+
"deterministic": { "type": "data" },
|
|
281
|
+
"pure": { "type": "data" },
|
|
282
|
+
"empty": { "type": "data" },
|
|
283
|
+
"stale": { "type": "data" }
|
|
284
|
+
},
|
|
285
|
+
"properNames": [
|
|
286
|
+
"tmct",
|
|
287
|
+
"Node",
|
|
288
|
+
"npm",
|
|
289
|
+
"JavaScript",
|
|
290
|
+
"TypeScript",
|
|
291
|
+
"Python",
|
|
292
|
+
"Java",
|
|
293
|
+
"Git",
|
|
294
|
+
"GitHub",
|
|
295
|
+
"GitLab",
|
|
296
|
+
"ESLint",
|
|
297
|
+
"Linux",
|
|
298
|
+
"macOS",
|
|
299
|
+
"Windows",
|
|
300
|
+
"Polycode"
|
|
301
|
+
]
|
|
302
|
+
}
|
package/src/grammar/lexicon.mjs
CHANGED
|
@@ -1,45 +1,217 @@
|
|
|
1
|
-
// grammar/lexicon.mjs —
|
|
2
|
-
// (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// tests, …) gets BYTE-IDENTICAL behaviour to before the extraction, with a
|
|
9
|
-
// single shared implementation instead of two copies drifting apart.
|
|
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).
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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.
|
|
19
|
+
//
|
|
20
|
+
// Morphology is deliberately tiny and deterministic (no NLP dependency): a
|
|
21
|
+
// suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
|
|
22
|
+
// "relies"→rely, "classes"→class, "uses"→use) plus an optional declared
|
|
23
|
+
// irregular `plural` ("indices"). Anything the fold can't reach is simply not
|
|
24
|
+
// in the lexicon — honest, not clever.
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
|
|
30
|
+
const CORE_FILE = join(dirname(fileURLToPath(import.meta.url)), "lexicon-core.json");
|
|
31
|
+
|
|
32
|
+
/** The CURIE namespace every tmct lexicon mints terms under. */
|
|
33
|
+
export const DEFAULT_NS = "tmct:";
|
|
34
|
+
|
|
35
|
+
/** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
|
|
36
|
+
export const DETERMINERS = Object.freeze({
|
|
37
|
+
every: "universal",
|
|
38
|
+
a: "indefinite",
|
|
39
|
+
an: "indefinite",
|
|
40
|
+
the: "definite",
|
|
41
|
+
no: "negative",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** The cardinality quantifier phrases (pattern 5) → the OWL term they select. */
|
|
45
|
+
export const QUANTIFIERS = Object.freeze({
|
|
46
|
+
"at least": "owl:minCardinality",
|
|
47
|
+
"at most": "owl:maxCardinality",
|
|
48
|
+
exactly: "owl:cardinality",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const NUMBER_WORDS = Object.freeze({
|
|
52
|
+
one: 1, two: 2, three: 3, four: 4, five: 5,
|
|
53
|
+
six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Parse a cardinality count token: a digit run or a small number word. */
|
|
57
|
+
export function numberOf(word) {
|
|
58
|
+
const w = String(word ?? "").trim().toLowerCase();
|
|
59
|
+
if (/^\d+$/.test(w)) return Number(w);
|
|
60
|
+
return NUMBER_WORDS[w] ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 3rd-person-singular surface form of a verb lemma ("import"→imports,
|
|
64
|
+
* "rely"→relies, "catch"→catches, "have"→has) — the predicate spelling. */
|
|
65
|
+
export function thirdPerson(base) {
|
|
66
|
+
const b = String(base);
|
|
67
|
+
if (b === "have") return "has";
|
|
68
|
+
if (/[^aeiou]y$/.test(b)) return `${b.slice(0, -1)}ies`;
|
|
69
|
+
if (/(s|x|z|ch|sh)$/.test(b)) return `${b}es`;
|
|
70
|
+
return `${b}s`;
|
|
71
|
+
}
|
|
72
|
+
|
|
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) {
|
|
78
|
+
if (verbEntry.predicate) return verbEntry.predicate;
|
|
79
|
+
const prep = verbEntry.prep ? verbEntry.prep[0].toUpperCase() + verbEntry.prep.slice(1) : "";
|
|
80
|
+
return `${ns}${thirdPerson(verbEntry.lemma)}${prep}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Deterministic singular/base-form candidates for a surface word, most
|
|
84
|
+
* specific first: as-is, -ies→y, -(s|x|z|ch|sh)es→stem, -s→stem. The FIRST
|
|
85
|
+
* candidate found in the relevant map wins ("classes"→class before "classe";
|
|
86
|
+
* "uses"→"us" misses, "use" hits). */
|
|
87
|
+
function foldCandidates(word) {
|
|
88
|
+
const w = String(word);
|
|
89
|
+
const out = [w];
|
|
90
|
+
if (w.length > 4 && /[a-z]ies$/.test(w)) out.push(`${w.slice(0, -3)}y`);
|
|
91
|
+
if (/(ses|xes|zes|ches|shes)$/.test(w)) out.push(w.slice(0, -2));
|
|
92
|
+
if (/[a-z]s$/.test(w) && !/ss$/.test(w)) out.push(w.slice(0, -1));
|
|
93
|
+
if (w === "has") out.push("have");
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const NOUN_PROPERTY_TYPES = new Set(["data", "object"]);
|
|
98
|
+
const ADJECTIVE_TYPES = new Set(["subclass", "data"]);
|
|
99
|
+
|
|
100
|
+
/** Merge one raw lexicon block ({nouns, verbs, adjectives, properNames}) into
|
|
101
|
+
* the lookup maps, validating the declared typings (bad declarations throw —
|
|
102
|
+
* a lexicon that lies would make the grammar guess). */
|
|
103
|
+
function ingest(lex, raw = {}) {
|
|
104
|
+
for (const [lemma, e] of Object.entries(raw.nouns || {})) {
|
|
105
|
+
const entry = { lemma, ...(e || {}) };
|
|
106
|
+
if (entry.property && !NOUN_PROPERTY_TYPES.has(entry.property)) {
|
|
107
|
+
throw new Error(`lexicon noun "${lemma}": property must be "data" or "object", got ${JSON.stringify(entry.property)}`);
|
|
108
|
+
}
|
|
109
|
+
lex.nouns.set(lemma, entry);
|
|
110
|
+
if (entry.plural) lex.nounPlurals.set(entry.plural, lemma);
|
|
111
|
+
}
|
|
112
|
+
for (const [lemma, e] of Object.entries(raw.verbs || {})) {
|
|
113
|
+
lex.verbs.set(lemma, { lemma, ...(e || {}) });
|
|
114
|
+
}
|
|
115
|
+
for (const [lemma, e] of Object.entries(raw.adjectives || {})) {
|
|
116
|
+
const entry = { lemma, ...(e || {}) };
|
|
117
|
+
if (!ADJECTIVE_TYPES.has(entry.type)) {
|
|
118
|
+
throw new Error(`lexicon adjective "${lemma}": type must be "subclass" or "data", got ${JSON.stringify(entry.type)}`);
|
|
119
|
+
}
|
|
120
|
+
lex.adjectives.set(lemma, entry);
|
|
121
|
+
}
|
|
122
|
+
for (const name of raw.properNames || []) {
|
|
123
|
+
lex.properNames.set(String(name).toLowerCase(), String(name));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
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();
|
|
131
|
+
|
|
132
|
+
/** Load the lexicon: the committed core vocabulary, optionally merged with a
|
|
133
|
+
* caller-supplied `extra` block of the same JSON shape (extra entries win).
|
|
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);
|
|
139
|
+
const raw = JSON.parse(readFileSync(CORE_FILE, "utf8"));
|
|
140
|
+
const lex = {
|
|
141
|
+
nouns: new Map(),
|
|
142
|
+
nounPlurals: new Map(),
|
|
143
|
+
verbs: new Map(),
|
|
144
|
+
adjectives: new Map(),
|
|
145
|
+
properNames: new Map(), // lowercased → canonical spelling
|
|
146
|
+
ns,
|
|
147
|
+
};
|
|
148
|
+
ingest(lex, raw);
|
|
149
|
+
if (extra) {
|
|
150
|
+
ingest(lex, extra);
|
|
151
|
+
return lex;
|
|
152
|
+
}
|
|
153
|
+
coreCacheByNs.set(ns, lex);
|
|
154
|
+
return lex;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Noun lookup with plural folding; returns the entry ({lemma, property?}) or null. */
|
|
158
|
+
export function lookupNoun(lexicon, word) {
|
|
159
|
+
const w = String(word ?? "").toLowerCase();
|
|
160
|
+
const irregular = lexicon.nounPlurals.get(w);
|
|
161
|
+
if (irregular) return lexicon.nouns.get(irregular) ?? null;
|
|
162
|
+
for (const cand of foldCandidates(w)) {
|
|
163
|
+
const hit = lexicon.nouns.get(cand);
|
|
164
|
+
if (hit) return hit;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Verb lookup with 3sg folding; returns the entry ({lemma, prep?, predicate?}) or null. */
|
|
170
|
+
export function lookupVerb(lexicon, word) {
|
|
171
|
+
const w = String(word ?? "").toLowerCase();
|
|
172
|
+
for (const cand of foldCandidates(w)) {
|
|
173
|
+
const hit = lexicon.verbs.get(cand);
|
|
174
|
+
if (hit) return hit;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Adjective lookup (exact lemma); returns {lemma, type, property?, value?} or null. */
|
|
180
|
+
export function lookupAdjective(lexicon, word) {
|
|
181
|
+
return lexicon.adjectives.get(String(word ?? "").toLowerCase()) ?? null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Proper-name lookup, case-insensitive; returns the CANONICAL spelling or null. */
|
|
185
|
+
export function lookupProperName(lexicon, word) {
|
|
186
|
+
return lexicon.properNames.get(String(word ?? "").toLowerCase()) ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Classify one word (or a two-word quantifier phrase) against the lexicon.
|
|
190
|
+
* Returns {pos, type?, …} or null for an undeclared word. Priority when a
|
|
191
|
+
* word is declared in several categories (e.g. "test" noun+verb): closed-class
|
|
192
|
+
* tokens, then properName > noun > verb > adjective — the grammar itself
|
|
193
|
+
* disambiguates by position, this is the standalone answer. */
|
|
43
194
|
export function classify(word, lexicon = loadLexicon()) {
|
|
44
|
-
|
|
195
|
+
const w = String(word ?? "").trim();
|
|
196
|
+
if (!w) return null;
|
|
197
|
+
const lower = w.toLowerCase();
|
|
198
|
+
if (DETERMINERS[lower]) return { pos: "determiner", type: DETERMINERS[lower] };
|
|
199
|
+
if (QUANTIFIERS[lower]) return { pos: "quantifier", type: QUANTIFIERS[lower] };
|
|
200
|
+
const n = numberOf(lower);
|
|
201
|
+
if (n != null) return { pos: "number", type: "cardinal", value: n };
|
|
202
|
+
const proper = lookupProperName(lexicon, w);
|
|
203
|
+
if (proper) return { pos: "properName", type: "individual", canonical: proper };
|
|
204
|
+
const noun = lookupNoun(lexicon, lower);
|
|
205
|
+
if (noun) {
|
|
206
|
+
return noun.property
|
|
207
|
+
? { pos: "noun", type: `${noun.property}-property`, lemma: noun.lemma, property: noun.property }
|
|
208
|
+
: { pos: "noun", type: "class", lemma: noun.lemma };
|
|
209
|
+
}
|
|
210
|
+
const verb = lookupVerb(lexicon, lower);
|
|
211
|
+
if (verb) {
|
|
212
|
+
return { pos: "verb", type: "objectProperty", lemma: verb.lemma, predicate: predicateOf(verb, lexicon.ns), ...(verb.prep ? { prep: verb.prep } : {}) };
|
|
213
|
+
}
|
|
214
|
+
const adj = lookupAdjective(lexicon, lower);
|
|
215
|
+
if (adj) return { pos: "adjective", type: adj.type, lemma: adj.lemma };
|
|
216
|
+
return null;
|
|
45
217
|
}
|