@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.4.0

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.
Files changed (53) hide show
  1. package/README.md +77 -3
  2. package/ROADMAP.md +416 -3
  3. package/bin/tmct.mjs +308 -12
  4. package/corpus/README.md +52 -0
  5. package/corpus/conceptnet/LICENSE-NOTICE +37 -0
  6. package/corpus/conceptnet/README.md +103 -0
  7. package/corpus/conceptnet/fetch-slice.mjs +136 -0
  8. package/corpus/conceptnet/filter-dump.mjs +89 -0
  9. package/corpus/conceptnet/slice.jsonl +14258 -0
  10. package/data/phrasebook/software-phrases.txt +231 -0
  11. package/data/templates/grammar-rules.toml +89 -0
  12. package/data/templates/responses.jsonl +68 -0
  13. package/package.json +40 -3
  14. package/src/ask-nlp.mjs +22 -10
  15. package/src/ask-vocab.mjs +35 -1
  16. package/src/ask.mjs +171 -494
  17. package/src/chat.mjs +709 -81
  18. package/src/corpus/conceptnet-map.toml +251 -0
  19. package/src/corpus/conceptnet.mjs +167 -0
  20. package/src/corpus/templates.mjs +188 -0
  21. package/src/finish.mjs +443 -0
  22. package/src/grammar/ace.mjs +341 -0
  23. package/src/grammar/assert.mjs +40 -0
  24. package/src/grammar/lexicon-core.json +287 -0
  25. package/src/grammar/lexicon.mjs +202 -0
  26. package/src/hash.mjs +32 -0
  27. package/src/index.mjs +21 -5
  28. package/src/init.mjs +264 -0
  29. package/src/interpret/fuzzy.mjs +89 -0
  30. package/src/interpret/merge.mjs +148 -0
  31. package/src/interpret/normalize.mjs +151 -0
  32. package/src/interpret/pipeline.mjs +112 -0
  33. package/src/interpret/strategies/grammar.mjs +137 -0
  34. package/src/interpret/strategies/keywords.mjs +241 -0
  35. package/src/interpret/strategies/noise-strip.mjs +114 -0
  36. package/src/memory/blocks.mjs +221 -0
  37. package/src/memory/core.mjs +533 -0
  38. package/src/memory/fold.mjs +0 -0
  39. package/src/memory/inspect.mjs +141 -0
  40. package/src/memory/trust.mjs +113 -0
  41. package/src/prose-nlp.mjs +14 -16
  42. package/src/providers/bootstrap.mjs +24 -0
  43. package/src/providers/fixture.mjs +118 -0
  44. package/src/providers/graph-service.mjs +312 -0
  45. package/src/repository-interface.mjs +318 -0
  46. package/src/server.mjs +44 -28
  47. package/src/sessions.mjs +137 -4
  48. package/src/source.mjs +44 -5
  49. package/src/syllogise.mjs +0 -0
  50. package/src/toml-config.mjs +14 -0
  51. package/src/tui/app.mjs +173 -0
  52. package/src/wink-model.mjs +74 -0
  53. package/bin/cli.mjs +0 -226
@@ -0,0 +1,341 @@
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.
9
+ //
10
+ // parseAce(sentence, lexicon) → { pattern, triples, residue } | null
11
+ // pattern one of: subClassOf | typeAssertion | relation | someValuesFrom |
12
+ // cardinality | disjointWith | possessive | adjective
13
+ // triples [{ subject, predicate, object, kind, n? }] — OWL-labelled string
14
+ // triples shaped for src/memory/core.mjs's appendFact (which
15
+ // normalizes subject/object via normFactTerm: "tmct:module" is
16
+ // stored as "module"; the predicate keeps its vocabulary casing).
17
+ // residue [] on a clean parse; the unknown tokens when the sentence FITS a
18
+ // pattern structurally but uses undeclared words (triples is then
19
+ // empty — feeds the pipeline's "if you mean X…" surround).
20
+ // null the sentence does not fit the fragment at all.
21
+ //
22
+ // Term style: classes/individuals are `tmct:<lexeme>` CURIEs (lexicon lemma
23
+ // for nouns, canonical spelling for proper names, the literal token for
24
+ // code-shaped references like chat.mjs); predicates are the OWL/RDF(S)
25
+ // vocabulary terms or the lexicon verb's tmct:<3sg> predicate. Restriction
26
+ // and intersection class expressions get READABLE deterministic node names
27
+ // (tmct:some-imports-test, tmct:module-that-imports-test) instead of blank
28
+ // nodes, so the same sentence always re-emits the same triples and appendFact
29
+ // stays idempotent. An intersection is flattened to repeated
30
+ // owl:intersectionOf triples (one per member) — the flat-JSON stand-in for an
31
+ // RDF list, documented in ontology/tmct-core.ttl.
32
+
33
+ import {
34
+ loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName,
35
+ predicateOf, numberOf, classify,
36
+ } from "./lexicon.mjs";
37
+
38
+ const DET = new Set(["a", "an", "the"]);
39
+ // A token SHAPED like a code reference (a path, file, symbol or CURIE) is an
40
+ // individual by form — a deterministic tokenizer rule, not a guess: declared
41
+ // proper names cover words; this covers chat.mjs, src/ask.mjs, Foo#bar.
42
+ const CODE_REF = /[./\\#:@]/;
43
+
44
+ /** Whitespace tokenizer: curly quotes normalized, commas/semicolons dropped,
45
+ * ONE trailing punctuation run stripped (so "chat.mjs." keeps its dots). */
46
+ export function tokenize(sentence) {
47
+ return String(sentence ?? "")
48
+ .replace(/[‘’]/g, "'")
49
+ .replace(/[,;]/g, " ")
50
+ .replace(/[?!.]+\s*$/, "")
51
+ .trim()
52
+ .split(/\s+/)
53
+ .filter(Boolean);
54
+ }
55
+
56
+ const local = (term) => String(term).replace(/^tmct:/, "");
57
+
58
+ const stripDet = (tokens) =>
59
+ tokens.length > 1 && DET.has(tokens[0].toLowerCase()) ? tokens.slice(1) : tokens;
60
+
61
+ /** Resolve a 1–2 word noun phrase: PROPERNAME | code-ref | NOUN | ADJ NOUN.
62
+ * Returns { term, individual, extras, unknown } — `term` null on a miss with
63
+ * the undeclared tokens in `unknown` (empty `unknown` = structurally
64
+ * unparseable phrase → the caller returns a hard null). `extras` carries the
65
+ * pattern-8 adjective triples (subclass axioms / hasValue restriction). */
66
+ function resolveNP(lexicon, tokensIn) {
67
+ const tokens = stripDet(tokensIn);
68
+ if (tokens.length === 1) {
69
+ const t = tokens[0];
70
+ const proper = lookupProperName(lexicon, t);
71
+ if (proper) return { term: `tmct:${proper}`, individual: true, extras: [], unknown: [] };
72
+ if (CODE_REF.test(t)) return { term: `tmct:${t}`, individual: true, extras: [], unknown: [] };
73
+ const noun = lookupNoun(lexicon, t);
74
+ if (noun) return { term: `tmct:${noun.lemma}`, individual: false, noun, extras: [], unknown: [] };
75
+ return { term: null, individual: false, extras: [], unknown: [t] };
76
+ }
77
+ if (tokens.length === 2) {
78
+ const adj = lookupAdjective(lexicon, tokens[0]);
79
+ const noun = lookupNoun(lexicon, tokens[1]);
80
+ if (adj && noun) {
81
+ const term = `tmct:${adj.lemma}-${noun.lemma}`;
82
+ const extras = [
83
+ { subject: term, predicate: "rdfs:subClassOf", object: `tmct:${noun.lemma}`, kind: "rdfs:subClassOf" },
84
+ ];
85
+ if (adj.type === "subclass") {
86
+ // the adjective itself denotes a class: legacy-module ⊑ module, ⊑ legacy
87
+ extras.push({ subject: term, predicate: "rdfs:subClassOf", object: `tmct:${adj.lemma}`, kind: "rdfs:subClassOf" });
88
+ } else {
89
+ // data adjective: subclass-with-restriction on the boolean-ish property
90
+ const r = `tmct:has-${adj.lemma}`;
91
+ extras.push(
92
+ { subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: "owl:hasValue" },
93
+ { subject: r, predicate: "owl:onProperty", object: adj.property || `tmct:${adj.lemma}`, kind: "owl:hasValue" },
94
+ { subject: r, predicate: "owl:hasValue", object: adj.value ?? "true", kind: "owl:hasValue" },
95
+ { subject: term, predicate: "rdfs:subClassOf", object: r, kind: "owl:hasValue" },
96
+ );
97
+ }
98
+ return { term, individual: false, noun, extras, unknown: [] };
99
+ }
100
+ // only genuinely undeclared words are residue — a declared word in the
101
+ // wrong slot ("GitLab pipeline") is a structural miss, not an unknown
102
+ const unknown = tokens.filter((t) => !classify(t, lexicon));
103
+ return { term: null, individual: false, extras: [], unknown };
104
+ }
105
+ // 0 or 3+ tokens: not a fragment NP. Name the undeclared words if any.
106
+ return { term: null, individual: false, extras: [], unknown: tokens.filter((t) => !classify(t, lexicon)) };
107
+ }
108
+
109
+ /** The shared miss result: a structural fit with undeclared words returns the
110
+ * pattern + residue (triples empty); a fit with only declared-but-unusable
111
+ * phrasing returns null — the honest fall-through either way. */
112
+ function missOrNull(pattern, nps, extraUnknown = []) {
113
+ const residue = [...extraUnknown, ...nps.flatMap((np) => np.unknown)];
114
+ return residue.length ? { pattern, triples: [], residue } : null;
115
+ }
116
+
117
+ const hit = (pattern, nps, triples, more = {}) => ({
118
+ pattern,
119
+ triples: [...nps.flatMap((np) => np.extras), ...triples],
120
+ residue: [],
121
+ ...more,
122
+ });
123
+
124
+ /** Pattern 3 — "N1 VERB N2" / "PROPERNAME VERBs PROPERNAME" → object-property
125
+ * assertion. Also the no-declared-verb 3-token shape: both ends resolvable →
126
+ * residue names the middle token (the future "if you mean X…" hook). */
127
+ function parseRelation(lexicon, toks, lower) {
128
+ for (let i = 1; i < toks.length - 1; i += 1) {
129
+ const verb = lookupVerb(lexicon, lower[i]);
130
+ if (!verb) continue;
131
+ let objStart = i + 1;
132
+ if (verb.prep) {
133
+ if (lower[objStart] !== verb.prep) continue;
134
+ objStart += 1;
135
+ if (objStart >= toks.length) continue;
136
+ }
137
+ const np1 = resolveNP(lexicon, toks.slice(0, i));
138
+ const np2 = resolveNP(lexicon, toks.slice(objStart));
139
+ if (np1.term == null || np2.term == null) return missOrNull("relation", [np1, np2]);
140
+ return hit("relation", [np1, np2], [
141
+ { subject: np1.term, predicate: predicateOf(verb), object: np2.term, kind: "owl:ObjectProperty" },
142
+ ]);
143
+ }
144
+ const content = toks.filter((t) => !DET.has(t.toLowerCase()));
145
+ if (content.length === 3 && !classify(content[1], lexicon)) {
146
+ const np1 = resolveNP(lexicon, [content[0]]);
147
+ const np2 = resolveNP(lexicon, [content[2]]);
148
+ if (np1.term != null && np2.term != null) return { pattern: "relation", triples: [], residue: [content[1]] };
149
+ }
150
+ return null;
151
+ }
152
+
153
+ /** Pattern 8 (copula arm) — "X is ADJ": data adjective → datatype-property
154
+ * assertion; subclass adjective → rdf:type (individual) / rdfs:subClassOf. */
155
+ function adjectiveCopula(pattern, np1, adj) {
156
+ if (np1.term == null) return missOrNull(pattern, [np1]);
157
+ if (adj.type === "data") {
158
+ return hit(pattern, [np1], [
159
+ { subject: np1.term, predicate: adj.property || `tmct:${adj.lemma}`, object: adj.value ?? "true", kind: "owl:DatatypeProperty" },
160
+ ]);
161
+ }
162
+ const predicate = np1.individual ? "rdf:type" : "rdfs:subClassOf";
163
+ return hit(pattern, [np1], [
164
+ { subject: np1.term, predicate, object: `tmct:${adj.lemma}`, kind: predicate },
165
+ ]);
166
+ }
167
+
168
+ /** Pattern 4 — "every N1 that VERBs a N2 is a N3" → someValuesFrom restriction:
169
+ * (N1 ⊓ ∃VERB.N2) ⊑ N3, flattened onto readable deterministic node names. */
170
+ function parseRestriction(lexicon, toks, lower, thatIdx) {
171
+ const isIdx = lower.indexOf("is", thatIdx + 2);
172
+ if (isIdx < 0 || thatIdx + 1 >= isIdx) return null;
173
+ const verb = lookupVerb(lexicon, lower[thatIdx + 1]);
174
+ const np1 = resolveNP(lexicon, toks.slice(1, thatIdx));
175
+ let objStart = thatIdx + 2;
176
+ if (verb?.prep) {
177
+ if (lower[objStart] !== verb.prep) return null;
178
+ objStart += 1;
179
+ }
180
+ const np2 = resolveNP(lexicon, toks.slice(objStart, isIdx));
181
+ const np3 = resolveNP(lexicon, toks.slice(isIdx + 1));
182
+ if (!verb) return missOrNull("someValuesFrom", [np1, np2, np3], [toks[thatIdx + 1]]);
183
+ if (np1.term == null || np2.term == null || np3.term == null) {
184
+ return missOrNull("someValuesFrom", [np1, np2, np3]);
185
+ }
186
+ if (np1.individual || np2.individual || np3.individual) return null; // class-level pattern only
187
+ const pred = predicateOf(verb);
188
+ const k = "owl:someValuesFrom";
189
+ const r = `tmct:some-${local(pred)}-${local(np2.term)}`;
190
+ const inter = `tmct:${local(np1.term)}-that-${local(pred)}-${local(np2.term)}`;
191
+ return hit("someValuesFrom", [np1, np2, np3], [
192
+ { subject: r, predicate: "rdf:type", object: "owl:Restriction", kind: k },
193
+ { subject: r, predicate: "owl:onProperty", object: pred, kind: k },
194
+ { subject: r, predicate: "owl:someValuesFrom", object: np2.term, kind: k },
195
+ { subject: inter, predicate: "owl:intersectionOf", object: np1.term, kind: k },
196
+ { subject: inter, predicate: "owl:intersectionOf", object: r, kind: k },
197
+ { subject: inter, predicate: "rdfs:subClassOf", object: np3.term, kind: k },
198
+ ]);
199
+ }
200
+
201
+ /** Pattern 5 — "every N has at least|at most|exactly n N2" → cardinality
202
+ * restriction on tmct:has (owl:onClass records the counted class — the
203
+ * qualified-form question is noted in docs/references/schemas/owl2-vocabulary.md). */
204
+ function parseCardinality(lexicon, toks, lower, hasIdx) {
205
+ let kind = null;
206
+ let nIdx = -1;
207
+ if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "least") { kind = "owl:minCardinality"; nIdx = hasIdx + 3; }
208
+ else if (lower[hasIdx + 1] === "at" && lower[hasIdx + 2] === "most") { kind = "owl:maxCardinality"; nIdx = hasIdx + 3; }
209
+ else if (lower[hasIdx + 1] === "exactly") { kind = "owl:cardinality"; nIdx = hasIdx + 2; }
210
+ else return null;
211
+ const n = numberOf(lower[nIdx]);
212
+ if (n == null || nIdx + 1 >= toks.length) return null;
213
+ const np1 = resolveNP(lexicon, toks.slice(1, hasIdx));
214
+ const np2 = resolveNP(lexicon, toks.slice(nIdx + 1));
215
+ if (np1.term == null || np2.term == null) return missOrNull("cardinality", [np1, np2]);
216
+ if (np1.individual || np2.individual) return null;
217
+ const tag = { "owl:minCardinality": "min", "owl:maxCardinality": "max", "owl:cardinality": "exactly" }[kind];
218
+ const r = `tmct:${tag}-${n}-${local(np2.term)}`;
219
+ return hit("cardinality", [np1, np2], [
220
+ { subject: r, predicate: "rdf:type", object: "owl:Restriction", kind },
221
+ { subject: r, predicate: "owl:onProperty", object: "tmct:has", kind },
222
+ { subject: r, predicate: kind, object: String(n), kind, n },
223
+ { subject: r, predicate: "owl:onClass", object: np2.term, kind },
224
+ { subject: np1.term, predicate: "rdfs:subClassOf", object: r, kind },
225
+ ], { n });
226
+ }
227
+
228
+ /** Patterns 1, 4, 5 and 8's "every …" arm. */
229
+ function parseEvery(lexicon, toks, lower) {
230
+ const thatIdx = lower.indexOf("that");
231
+ if (thatIdx > 1) return parseRestriction(lexicon, toks, lower, thatIdx);
232
+ const hasIdx = lower.indexOf("has");
233
+ if (hasIdx > 1 && (lower[hasIdx + 1] === "at" || lower[hasIdx + 1] === "exactly")) {
234
+ return parseCardinality(lexicon, toks, lower, hasIdx);
235
+ }
236
+ const isIdx = lower.indexOf("is");
237
+ if (isIdx <= 1 || isIdx === toks.length - 1) return null;
238
+ const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
239
+ const rest = toks.slice(isIdx + 1);
240
+ if (rest.length === 1) {
241
+ const adj = lookupAdjective(lexicon, rest[0]);
242
+ if (adj) return adjectiveCopula("adjective", np1, adj);
243
+ }
244
+ const np2 = resolveNP(lexicon, rest);
245
+ if (np1.term == null || np2.term == null) return missOrNull("subClassOf", [np1, np2]);
246
+ if (np1.individual || np2.individual) return null; // "every X is chat.mjs" — not the fragment
247
+ return hit("subClassOf", [np1, np2], [
248
+ { subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
249
+ ]);
250
+ }
251
+
252
+ /** Pattern 6 — "no N1 is a N2" → owl:disjointWith. */
253
+ function parseDisjoint(lexicon, toks, lower) {
254
+ const isIdx = lower.indexOf("is");
255
+ if (isIdx <= 1 || isIdx === toks.length - 1) return null;
256
+ const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
257
+ const np2 = resolveNP(lexicon, toks.slice(isIdx + 1));
258
+ if (np1.term == null || np2.term == null) return missOrNull("disjointWith", [np1, np2]);
259
+ if (np1.individual || np2.individual) return null;
260
+ return hit("disjointWith", [np1, np2], [
261
+ { subject: np1.term, predicate: "owl:disjointWith", object: np2.term, kind: "owl:disjointWith" },
262
+ ]);
263
+ }
264
+
265
+ /** Pattern 7 — "N1's N2 is VALUE" / "the N2 of N1 is VALUE": data or object
266
+ * property assertion per the possessive noun's DECLARED typing (undeclared
267
+ * typing defaults to data — a literal value is the honest floor). */
268
+ function buildPossessive(lexicon, ownerToks, headToks, valueToks) {
269
+ const owner = resolveNP(lexicon, ownerToks);
270
+ if (headToks.length !== 1) return null;
271
+ const head = lookupNoun(lexicon, headToks[0]);
272
+ if (!head) return missOrNull("possessive", [owner], [headToks[0]]);
273
+ if (owner.term == null) return missOrNull("possessive", [owner]);
274
+ if (!valueToks.length) return null;
275
+ const predicate = `tmct:${head.lemma}`;
276
+ if ((head.property || "data") === "object") {
277
+ const value = resolveNP(lexicon, valueToks);
278
+ if (value.term == null) return missOrNull("possessive", [owner, value]);
279
+ return hit("possessive", [owner, value], [
280
+ { subject: owner.term, predicate, object: value.term, kind: "owl:ObjectProperty" },
281
+ ]);
282
+ }
283
+ return hit("possessive", [owner], [
284
+ { subject: owner.term, predicate, object: valueToks.join(" "), kind: "owl:DatatypeProperty" },
285
+ ]);
286
+ }
287
+
288
+ function parsePossessive(lexicon, toks, lower) {
289
+ const ownerRaw = toks[0].replace(/'s$/i, "");
290
+ const isIdx = lower.indexOf("is");
291
+ if (isIdx < 2 || !ownerRaw) return null;
292
+ return buildPossessive(lexicon, [ownerRaw], toks.slice(1, isIdx), toks.slice(isIdx + 1));
293
+ }
294
+
295
+ function parseOfForm(lexicon, toks, lower) {
296
+ const ofIdx = lower.indexOf("of");
297
+ const isIdx = lower.indexOf("is", ofIdx + 1);
298
+ if (ofIdx < 2 || isIdx < ofIdx + 2) return null;
299
+ return buildPossessive(lexicon, toks.slice(ofIdx + 1, isIdx), toks.slice(1, ofIdx), toks.slice(isIdx + 1));
300
+ }
301
+
302
+ /** Patterns 2 (class assertion), 1's bare-copula variant, and 8's copula arm. */
303
+ function parseCopula(lexicon, toks, lower, isIdx) {
304
+ const np1 = resolveNP(lexicon, toks.slice(0, isIdx));
305
+ const rest = toks.slice(isIdx + 1);
306
+ if (!rest.length) return null;
307
+ if (rest.length === 1) {
308
+ const adj = lookupAdjective(lexicon, rest[0]);
309
+ if (adj) return adjectiveCopula("adjective", np1, adj);
310
+ }
311
+ const np2 = resolveNP(lexicon, rest);
312
+ if (np1.term == null || np2.term == null) {
313
+ return missOrNull(np1.individual ? "typeAssertion" : "subClassOf", [np1, np2]);
314
+ }
315
+ if (np2.individual) return null; // "chat.mjs is sessions.mjs" — identity is not in the fragment
316
+ if (np1.individual) {
317
+ return hit("typeAssertion", [np1, np2], [
318
+ { subject: np1.term, predicate: "rdf:type", object: np2.term, kind: "rdf:type" },
319
+ ]);
320
+ }
321
+ return hit("subClassOf", [np1, np2], [
322
+ { subject: np1.term, predicate: "rdfs:subClassOf", object: np2.term, kind: "rdfs:subClassOf" },
323
+ ]);
324
+ }
325
+
326
+ /** Parse one sentence against the 8-pattern ACE-OWL sub-fragment. See the file
327
+ * header for the result contract; `lexicon` defaults to the committed core. */
328
+ export function parseAce(sentence, lexicon = loadLexicon()) {
329
+ const toks = tokenize(sentence);
330
+ if (toks.length < 3) return null;
331
+ const lower = toks.map((t) => t.toLowerCase());
332
+ if (lower[0] === "every") return parseEvery(lexicon, toks, lower);
333
+ if (lower[0] === "no") return parseDisjoint(lexicon, toks, lower);
334
+ if (/'s$/.test(lower[0]) && lower[0].length > 2) return parsePossessive(lexicon, toks, lower);
335
+ if (lower[0] === "the" && lower.includes("of") && lower.includes("is")) {
336
+ return parseOfForm(lexicon, toks, lower);
337
+ }
338
+ const isIdx = lower.indexOf("is");
339
+ if (isIdx > 0) return parseCopula(lexicon, toks, lower, isIdx);
340
+ return parseRelation(lexicon, toks, lower);
341
+ }
@@ -0,0 +1,40 @@
1
+ // grammar/assert.mjs — the grammar→memory bridge: parseAce a sentence and land
2
+ // every emitted triple in tmct's OWN memory graph via memory/core.mjs's
3
+ // appendFact (ROADMAP Phase 2 item 2 meeting Phase 1 item 9).
4
+ //
5
+ // appendFact normalizes each triple's subject/object through normFactTerm
6
+ // (tmct:Legacy-module → "legacy-module"; the predicate keeps its vocabulary
7
+ // casing) and content-addresses the fact id — so re-asserting the same
8
+ // sentence upserts, never duplicates, and different writers (chat, corpus)
9
+ // converge on the same stored term spelling. Provenance is a compact tag
10
+ // ("ace:chat:<sessionId>@<ts>"); core.mjs unions tags "|"-joined when several
11
+ // writers assert the same fact.
12
+
13
+ import { appendFact } from "../memory/core.mjs";
14
+ import { parseAce } from "./ace.mjs";
15
+ import { loadLexicon } from "./lexicon.mjs";
16
+
17
+ /** Render a provenance descriptor {source, sessionId?, ts?} as the stored tag.
18
+ * Deterministic, compact, greppable: ace:chat:0189…abcd@2026-07-04T10:00:00Z */
19
+ export function provenanceTag({ source = "chat", sessionId = "", ts = "" } = {}) {
20
+ return `ace:${source}${sessionId ? `:${sessionId}` : ""}${ts ? `@${ts}` : ""}`;
21
+ }
22
+
23
+ /** Parse `sentence` against the ACE-OWL sub-fragment and append every emitted
24
+ * triple to the memory graph under `dir`. Returns the parse result extended
25
+ * with `ids` (one fact id per triple, same order) and the provenance tag —
26
+ * or null (grammar miss, nothing written), or the residue parse (unknown
27
+ * words: triples empty, ids empty, nothing written). */
28
+ export async function assertSentence(dir, sentence, { lexicon, provenance } = {}) {
29
+ const parse = parseAce(sentence, lexicon ?? loadLexicon());
30
+ if (!parse) return null;
31
+ const tag = provenanceTag(provenance);
32
+ const ids = [];
33
+ for (const t of parse.triples) {
34
+ const { id } = await appendFact(dir, {
35
+ subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag,
36
+ });
37
+ ids.push(id);
38
+ }
39
+ return { ...parse, ids, provenance: tag };
40
+ }
@@ -0,0 +1,287 @@
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
+ "schema": {},
34
+ "database": {},
35
+ "table": {},
36
+ "query": {},
37
+ "index": { "plural": "indices" },
38
+ "cache": {},
39
+ "queue": {},
40
+ "worker": {},
41
+ "job": {},
42
+ "task": {},
43
+ "bug": {},
44
+ "defect": {},
45
+ "issue": {},
46
+ "ticket": {},
47
+ "feature": {},
48
+ "requirement": {},
49
+ "specification": {},
50
+ "document": {},
51
+ "developer": {},
52
+ "engineer": {},
53
+ "user": {},
54
+ "visitor": {},
55
+ "team": {},
56
+ "project": {},
57
+ "codebase": {},
58
+ "file": {},
59
+ "directory": {},
60
+ "folder": {},
61
+ "line": {},
62
+ "symbol": {},
63
+ "identifier": {},
64
+ "comment": {},
65
+ "docstring": {},
66
+ "string": {},
67
+ "number": {},
68
+ "list": {},
69
+ "array": {},
70
+ "graph": {},
71
+ "node": {},
72
+ "edge": {},
73
+ "triple": {},
74
+ "fact": {},
75
+ "ontology": {},
76
+ "lexicon": {},
77
+ "grammar": {},
78
+ "sentence": {},
79
+ "utterance": {},
80
+ "session": {},
81
+ "response": {},
82
+ "request": {},
83
+ "question": {},
84
+ "answer": {},
85
+ "error": {},
86
+ "exception": {},
87
+ "warning": {},
88
+ "log": {},
89
+ "metric": {},
90
+ "benchmark": {},
91
+ "pipeline": {},
92
+ "build": {},
93
+ "deployment": {},
94
+ "environment": {},
95
+ "config": {},
96
+ "configuration": {},
97
+ "setting": {},
98
+ "option": {},
99
+ "flag": {},
100
+ "argument": {},
101
+ "parameter": {},
102
+ "value": {},
103
+ "result": {},
104
+ "output": {},
105
+ "input": {},
106
+ "server": {},
107
+ "client": {},
108
+ "protocol": {},
109
+ "message": {},
110
+ "event": {},
111
+ "hook": {},
112
+ "plugin": {},
113
+ "script": {},
114
+ "tool": {},
115
+ "command": {},
116
+ "prompt": {},
117
+ "token": {},
118
+ "parser": {},
119
+ "compiler": {},
120
+ "linter": {},
121
+ "formatter": {},
122
+ "runtime": {},
123
+ "process": {},
124
+ "thread": {},
125
+ "loop": {},
126
+ "statement": {},
127
+ "expression": {},
128
+ "keyword": {},
129
+ "scope": {},
130
+ "callback": {},
131
+ "promise": {},
132
+ "iterator": {},
133
+ "generator": {},
134
+ "unit": {},
135
+ "risk": {},
136
+ "prototype": {},
137
+ "milestone": {},
138
+ "sprint": {},
139
+ "backlog": {},
140
+ "roadmap": {},
141
+ "phase": {},
142
+ "pattern": {},
143
+ "smell": {},
144
+ "coverage": {},
145
+ "mock": {},
146
+ "stub": {},
147
+ "fixture": {},
148
+ "assertion": {},
149
+ "snapshot": {},
150
+ "regression": {},
151
+ "migration": {},
152
+ "refactor": {},
153
+ "review": {},
154
+ "merge": {},
155
+ "license": { "property": "data" },
156
+ "version": { "property": "data" },
157
+ "name": { "property": "data" },
158
+ "path": { "property": "data" },
159
+ "size": { "property": "data" },
160
+ "status": { "property": "data" },
161
+ "language": { "property": "data" },
162
+ "extension": { "property": "data" },
163
+ "owner": { "property": "object" },
164
+ "maintainer": { "property": "object" },
165
+ "author": { "property": "object" },
166
+ "reviewer": { "property": "object" },
167
+ "parent": { "property": "object" },
168
+ "dependency": { "property": "object" }
169
+ },
170
+ "verbs": {
171
+ "import": {},
172
+ "call": {},
173
+ "test": {},
174
+ "contain": {},
175
+ "extend": {},
176
+ "use": {},
177
+ "depend": { "prep": "on" },
178
+ "rely": { "prep": "on" },
179
+ "inherit": { "prep": "from" },
180
+ "belong": { "prep": "to" },
181
+ "point": { "prep": "to" },
182
+ "implement": {},
183
+ "override": {},
184
+ "export": {},
185
+ "define": {},
186
+ "declare": {},
187
+ "reference": {},
188
+ "invoke": {},
189
+ "wrap": {},
190
+ "mock": {},
191
+ "cover": {},
192
+ "document": {},
193
+ "describe": {},
194
+ "modify": {},
195
+ "touch": {},
196
+ "fix": {},
197
+ "break": {},
198
+ "introduce": {},
199
+ "deprecate": {},
200
+ "replace": {},
201
+ "own": {},
202
+ "maintain": {},
203
+ "review": {},
204
+ "merge": {},
205
+ "revert": {},
206
+ "deploy": {},
207
+ "run": {},
208
+ "execute": {},
209
+ "load": {},
210
+ "parse": {},
211
+ "emit": {},
212
+ "validate": {},
213
+ "log": {},
214
+ "throw": {},
215
+ "catch": {},
216
+ "create": {},
217
+ "delete": {},
218
+ "update": {},
219
+ "expose": {},
220
+ "consume": {},
221
+ "produce": {},
222
+ "generate": {},
223
+ "configure": {},
224
+ "install": {},
225
+ "publish": {},
226
+ "release": {},
227
+ "ship": {},
228
+ "watch": {},
229
+ "trigger": {},
230
+ "build": {},
231
+ "write": {},
232
+ "read": {},
233
+ "have": {}
234
+ },
235
+ "adjectives": {
236
+ "legacy": { "type": "subclass" },
237
+ "internal": { "type": "subclass" },
238
+ "external": { "type": "subclass" },
239
+ "public": { "type": "subclass" },
240
+ "private": { "type": "subclass" },
241
+ "abstract": { "type": "subclass" },
242
+ "static": { "type": "subclass" },
243
+ "async": { "type": "subclass" },
244
+ "experimental": { "type": "subclass" },
245
+ "stable": { "type": "subclass" },
246
+ "core": { "type": "subclass" },
247
+ "shared": { "type": "subclass" },
248
+ "global": { "type": "subclass" },
249
+ "local": { "type": "subclass" },
250
+ "generated": { "type": "subclass" },
251
+ "standalone": { "type": "subclass" },
252
+ "primary": { "type": "subclass" },
253
+ "secondary": { "type": "subclass" },
254
+ "deprecated": { "type": "data" },
255
+ "fast": { "type": "data" },
256
+ "slow": { "type": "data" },
257
+ "large": { "type": "data" },
258
+ "small": { "type": "data" },
259
+ "flaky": { "type": "data" },
260
+ "tested": { "type": "data" },
261
+ "documented": { "type": "data" },
262
+ "buggy": { "type": "data" },
263
+ "broken": { "type": "data" },
264
+ "green": { "type": "data" },
265
+ "deterministic": { "type": "data" },
266
+ "pure": { "type": "data" },
267
+ "empty": { "type": "data" },
268
+ "stale": { "type": "data" }
269
+ },
270
+ "properNames": [
271
+ "tmct",
272
+ "Node",
273
+ "npm",
274
+ "JavaScript",
275
+ "TypeScript",
276
+ "Python",
277
+ "Java",
278
+ "Git",
279
+ "GitHub",
280
+ "GitLab",
281
+ "ESLint",
282
+ "Linux",
283
+ "macOS",
284
+ "Windows",
285
+ "Polycode"
286
+ ]
287
+ }