@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.3.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.
- package/ROADMAP.md +5 -2
- package/bin/tmct.mjs +253 -12
- package/corpus/README.md +52 -0
- package/corpus/conceptnet/LICENSE-NOTICE +37 -0
- package/corpus/conceptnet/README.md +103 -0
- package/corpus/conceptnet/fetch-slice.mjs +136 -0
- package/corpus/conceptnet/filter-dump.mjs +89 -0
- package/corpus/conceptnet/slice.jsonl +14258 -0
- package/data/phrasebook/software-phrases.txt +231 -0
- package/data/templates/responses.jsonl +55 -0
- package/package.json +12 -3
- package/src/ask-nlp.mjs +14 -0
- package/src/ask-vocab.mjs +13 -1
- package/src/ask.mjs +92 -493
- package/src/chat.mjs +147 -45
- package/src/corpus/conceptnet-map.toml +251 -0
- package/src/corpus/conceptnet.mjs +155 -0
- package/src/corpus/templates.mjs +104 -0
- package/src/grammar/ace.mjs +341 -0
- package/src/grammar/assert.mjs +40 -0
- package/src/grammar/lexicon-core.json +287 -0
- package/src/grammar/lexicon.mjs +202 -0
- package/src/index.mjs +21 -5
- package/src/interpret/fuzzy.mjs +89 -0
- package/src/interpret/merge.mjs +148 -0
- package/src/interpret/normalize.mjs +117 -0
- package/src/interpret/pipeline.mjs +112 -0
- package/src/interpret/strategies/grammar.mjs +137 -0
- package/src/interpret/strategies/keywords.mjs +185 -0
- package/src/interpret/strategies/noise-strip.mjs +114 -0
- package/src/memory/blocks.mjs +201 -0
- package/src/memory/core.mjs +292 -0
- package/src/memory/fold.mjs +105 -0
- package/src/sessions.mjs +125 -3
- package/src/source.mjs +44 -5
- package/src/tui/app.mjs +173 -0
- package/bin/cli.mjs +0 -226
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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).
|
|
3
|
+
//
|
|
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).
|
|
10
|
+
//
|
|
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
|
+
//
|
|
15
|
+
// Morphology is deliberately tiny and deterministic (no NLP dependency): a
|
|
16
|
+
// suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
|
|
17
|
+
// "relies"→rely, "classes"→class, "uses"→use) plus an optional declared
|
|
18
|
+
// irregular `plural` ("indices"). Anything the fold can't reach is simply not
|
|
19
|
+
// in the lexicon — honest, not clever.
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
|
|
25
|
+
const CORE_FILE = join(dirname(fileURLToPath(import.meta.url)), "lexicon-core.json");
|
|
26
|
+
|
|
27
|
+
/** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
|
|
28
|
+
export const DETERMINERS = Object.freeze({
|
|
29
|
+
every: "universal",
|
|
30
|
+
a: "indefinite",
|
|
31
|
+
an: "indefinite",
|
|
32
|
+
the: "definite",
|
|
33
|
+
no: "negative",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** The cardinality quantifier phrases (pattern 5) → the OWL term they select. */
|
|
37
|
+
export const QUANTIFIERS = Object.freeze({
|
|
38
|
+
"at least": "owl:minCardinality",
|
|
39
|
+
"at most": "owl:maxCardinality",
|
|
40
|
+
exactly: "owl:cardinality",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const NUMBER_WORDS = Object.freeze({
|
|
44
|
+
one: 1, two: 2, three: 3, four: 4, five: 5,
|
|
45
|
+
six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** Parse a cardinality count token: a digit run or a small number word. */
|
|
49
|
+
export function numberOf(word) {
|
|
50
|
+
const w = String(word ?? "").trim().toLowerCase();
|
|
51
|
+
if (/^\d+$/.test(w)) return Number(w);
|
|
52
|
+
return NUMBER_WORDS[w] ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 3rd-person-singular surface form of a verb lemma — the predicate spelling
|
|
56
|
+
* ("import"→imports, "rely"→relies, "catch"→catches, "have"→has), matching
|
|
57
|
+
* the code graph's 3sg relation-kind convention (ask.mjs §verbs). */
|
|
58
|
+
export function thirdPerson(base) {
|
|
59
|
+
const b = String(base);
|
|
60
|
+
if (b === "have") return "has";
|
|
61
|
+
if (/[^aeiou]y$/.test(b)) return `${b.slice(0, -1)}ies`;
|
|
62
|
+
if (/(s|x|z|ch|sh)$/.test(b)) return `${b}es`;
|
|
63
|
+
return `${b}s`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The URI-style predicate a verb entry emits: a declared override, or
|
|
67
|
+
* tmct:<3sg lemma> with any preposition camel-appended ("depend on"→tmct:dependsOn). */
|
|
68
|
+
export function predicateOf(verbEntry) {
|
|
69
|
+
if (verbEntry.predicate) return verbEntry.predicate;
|
|
70
|
+
const prep = verbEntry.prep ? verbEntry.prep[0].toUpperCase() + verbEntry.prep.slice(1) : "";
|
|
71
|
+
return `tmct:${thirdPerson(verbEntry.lemma)}${prep}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Deterministic singular/base-form candidates for a surface word, most
|
|
75
|
+
* specific first: as-is, -ies→y, -(s|x|z|ch|sh)es→stem, -s→stem. The FIRST
|
|
76
|
+
* candidate found in the relevant map wins ("classes"→class before "classe";
|
|
77
|
+
* "uses"→"us" misses, "use" hits). */
|
|
78
|
+
function foldCandidates(word) {
|
|
79
|
+
const w = String(word);
|
|
80
|
+
const out = [w];
|
|
81
|
+
if (w.length > 4 && /[a-z]ies$/.test(w)) out.push(`${w.slice(0, -3)}y`);
|
|
82
|
+
if (/(ses|xes|zes|ches|shes)$/.test(w)) out.push(w.slice(0, -2));
|
|
83
|
+
if (/[a-z]s$/.test(w) && !/ss$/.test(w)) out.push(w.slice(0, -1));
|
|
84
|
+
if (w === "has") out.push("have");
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const NOUN_PROPERTY_TYPES = new Set(["data", "object"]);
|
|
89
|
+
const ADJECTIVE_TYPES = new Set(["subclass", "data"]);
|
|
90
|
+
|
|
91
|
+
/** Merge one raw lexicon block ({nouns, verbs, adjectives, properNames}) into
|
|
92
|
+
* the lookup maps, validating the declared typings (bad declarations throw —
|
|
93
|
+
* a lexicon that lies would make the grammar guess). */
|
|
94
|
+
function ingest(lex, raw = {}) {
|
|
95
|
+
for (const [lemma, e] of Object.entries(raw.nouns || {})) {
|
|
96
|
+
const entry = { lemma, ...(e || {}) };
|
|
97
|
+
if (entry.property && !NOUN_PROPERTY_TYPES.has(entry.property)) {
|
|
98
|
+
throw new Error(`lexicon noun "${lemma}": property must be "data" or "object", got ${JSON.stringify(entry.property)}`);
|
|
99
|
+
}
|
|
100
|
+
lex.nouns.set(lemma, entry);
|
|
101
|
+
if (entry.plural) lex.nounPlurals.set(entry.plural, lemma);
|
|
102
|
+
}
|
|
103
|
+
for (const [lemma, e] of Object.entries(raw.verbs || {})) {
|
|
104
|
+
lex.verbs.set(lemma, { lemma, ...(e || {}) });
|
|
105
|
+
}
|
|
106
|
+
for (const [lemma, e] of Object.entries(raw.adjectives || {})) {
|
|
107
|
+
const entry = { lemma, ...(e || {}) };
|
|
108
|
+
if (!ADJECTIVE_TYPES.has(entry.type)) {
|
|
109
|
+
throw new Error(`lexicon adjective "${lemma}": type must be "subclass" or "data", got ${JSON.stringify(entry.type)}`);
|
|
110
|
+
}
|
|
111
|
+
lex.adjectives.set(lemma, entry);
|
|
112
|
+
}
|
|
113
|
+
for (const name of raw.properNames || []) {
|
|
114
|
+
lex.properNames.set(String(name).toLowerCase(), String(name));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let coreCache = null;
|
|
119
|
+
|
|
120
|
+
/** Load the lexicon: the committed core vocabulary, optionally merged with a
|
|
121
|
+
* caller-supplied `extra` block of the same JSON shape (extra entries win).
|
|
122
|
+
* The no-extra result is cached (the JSON is committed, immutable at runtime). */
|
|
123
|
+
export function loadLexicon(extra) {
|
|
124
|
+
if (!extra && coreCache) return coreCache;
|
|
125
|
+
const raw = JSON.parse(readFileSync(CORE_FILE, "utf8"));
|
|
126
|
+
const lex = {
|
|
127
|
+
nouns: new Map(),
|
|
128
|
+
nounPlurals: new Map(),
|
|
129
|
+
verbs: new Map(),
|
|
130
|
+
adjectives: new Map(),
|
|
131
|
+
properNames: new Map(), // lowercased → canonical spelling
|
|
132
|
+
};
|
|
133
|
+
ingest(lex, raw);
|
|
134
|
+
if (extra) {
|
|
135
|
+
ingest(lex, extra);
|
|
136
|
+
return lex;
|
|
137
|
+
}
|
|
138
|
+
coreCache = lex;
|
|
139
|
+
return lex;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Noun lookup with plural folding; returns the entry ({lemma, property?}) or null. */
|
|
143
|
+
export function lookupNoun(lexicon, word) {
|
|
144
|
+
const w = String(word ?? "").toLowerCase();
|
|
145
|
+
const irregular = lexicon.nounPlurals.get(w);
|
|
146
|
+
if (irregular) return lexicon.nouns.get(irregular) ?? null;
|
|
147
|
+
for (const cand of foldCandidates(w)) {
|
|
148
|
+
const hit = lexicon.nouns.get(cand);
|
|
149
|
+
if (hit) return hit;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Verb lookup with 3sg folding; returns the entry ({lemma, prep?, predicate?}) or null. */
|
|
155
|
+
export function lookupVerb(lexicon, word) {
|
|
156
|
+
const w = String(word ?? "").toLowerCase();
|
|
157
|
+
for (const cand of foldCandidates(w)) {
|
|
158
|
+
const hit = lexicon.verbs.get(cand);
|
|
159
|
+
if (hit) return hit;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Adjective lookup (exact lemma); returns {lemma, type, property?, value?} or null. */
|
|
165
|
+
export function lookupAdjective(lexicon, word) {
|
|
166
|
+
return lexicon.adjectives.get(String(word ?? "").toLowerCase()) ?? null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Proper-name lookup, case-insensitive; returns the CANONICAL spelling or null. */
|
|
170
|
+
export function lookupProperName(lexicon, word) {
|
|
171
|
+
return lexicon.properNames.get(String(word ?? "").toLowerCase()) ?? null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Classify one word (or a two-word quantifier phrase) against the lexicon.
|
|
175
|
+
* Returns {pos, type?, …} or null for an undeclared word. Priority when a
|
|
176
|
+
* word is declared in several categories (e.g. "test" noun+verb): closed-class
|
|
177
|
+
* tokens, then properName > noun > verb > adjective — the grammar itself
|
|
178
|
+
* disambiguates by position, this is the standalone answer. */
|
|
179
|
+
export function classify(word, lexicon = loadLexicon()) {
|
|
180
|
+
const w = String(word ?? "").trim();
|
|
181
|
+
if (!w) return null;
|
|
182
|
+
const lower = w.toLowerCase();
|
|
183
|
+
if (DETERMINERS[lower]) return { pos: "determiner", type: DETERMINERS[lower] };
|
|
184
|
+
if (QUANTIFIERS[lower]) return { pos: "quantifier", type: QUANTIFIERS[lower] };
|
|
185
|
+
const n = numberOf(lower);
|
|
186
|
+
if (n != null) return { pos: "number", type: "cardinal", value: n };
|
|
187
|
+
const proper = lookupProperName(lexicon, w);
|
|
188
|
+
if (proper) return { pos: "properName", type: "individual", canonical: proper };
|
|
189
|
+
const noun = lookupNoun(lexicon, lower);
|
|
190
|
+
if (noun) {
|
|
191
|
+
return noun.property
|
|
192
|
+
? { pos: "noun", type: `${noun.property}-property`, lemma: noun.lemma, property: noun.property }
|
|
193
|
+
: { pos: "noun", type: "class", lemma: noun.lemma };
|
|
194
|
+
}
|
|
195
|
+
const verb = lookupVerb(lexicon, lower);
|
|
196
|
+
if (verb) {
|
|
197
|
+
return { pos: "verb", type: "objectProperty", lemma: verb.lemma, predicate: predicateOf(verb), ...(verb.prep ? { prep: verb.prep } : {}) };
|
|
198
|
+
}
|
|
199
|
+
const adj = lookupAdjective(lexicon, lower);
|
|
200
|
+
if (adj) return { pos: "adjective", type: adj.type, lemma: adj.lemma };
|
|
201
|
+
return null;
|
|
202
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// shape and its green test suite; the branding throughout is now `tmct`.
|
|
6
6
|
//
|
|
7
7
|
// This entry re-exports the adapter primitives a library consumer needs. The
|
|
8
|
-
// clean chat/primitives split
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// clean chat/primitives split (ROADMAP item 13) is done: the movable
|
|
9
|
+
// conversational grammar lives in src/interpret/ (normalization pre-pass, the
|
|
10
|
+
// registered parsing strategies, the merge rule), while ask.mjs keeps the core
|
|
11
|
+
// primitives (resolveObject, traverse, render) and the ask() orchestration.
|
|
11
12
|
|
|
12
13
|
// Chat surface (also reachable as the `./chat` subpath export).
|
|
13
14
|
export { runChat, COMMANDS, answerCount, renderStats } from "./chat.mjs";
|
|
@@ -15,11 +16,26 @@ export { runChat, COMMANDS, answerCount, renderStats } from "./chat.mjs";
|
|
|
15
16
|
// Grammar / NL-over-graph primitives.
|
|
16
17
|
export { ask, resolveObject } from "./ask.mjs";
|
|
17
18
|
|
|
19
|
+
// The interpretation pipeline (ROADMAP item 8): normalize once, run every
|
|
20
|
+
// registered strategy (grammar, keyword-spot, …) over the text, merge same-class
|
|
21
|
+
// results, surround distinct-class results — no graph access; pair it with ask()
|
|
22
|
+
// or the primitives to answer. `interpret(text, ctx)` returns the full record
|
|
23
|
+
// ({raw, normalized, normalizationChanged, results, parsed, class, alternates}).
|
|
24
|
+
export { interpret } from "./interpret/pipeline.mjs";
|
|
25
|
+
|
|
18
26
|
// Graph traversal primitives.
|
|
19
27
|
export { relationKind, impactClosure } from "./codegraph.mjs";
|
|
20
28
|
|
|
21
29
|
// Tool dispatch (slash-commands and CLI tool calls route through here).
|
|
22
30
|
export { dispatchTool } from "./server.mjs";
|
|
23
31
|
|
|
24
|
-
//
|
|
25
|
-
|
|
32
|
+
// Conversational memory (ROADMAP item 9) — tmct's OWN OWL-labelled graph under
|
|
33
|
+
// .tmct/memory/, distinct from any provider-supplied code graph.
|
|
34
|
+
export { loadMemory, appendUtterance, appendFact } from "./memory/core.mjs";
|
|
35
|
+
export { retrieveBlocks, saveBlock, rankBlocks } from "./memory/blocks.mjs";
|
|
36
|
+
export { foldSessionLogs } from "./memory/fold.mjs";
|
|
37
|
+
|
|
38
|
+
// The single graph-load choke point — the adapter's data-provider seam
|
|
39
|
+
// (docs/adapter-contract.md): registerProvider() plugs a producer in;
|
|
40
|
+
// fetchEntities() is the one read path.
|
|
41
|
+
export { fetchEntities, registerProvider } from "./source.mjs";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// interpret/fuzzy.mjs — the bounded-edit-distance fuzzy tier (two-level fuzzy,
|
|
2
|
+
// 2026-07-02), extracted MOVE-only from ask.mjs (item 13). A reusable SERVICE the
|
|
3
|
+
// strategies call, not a strategy itself: the keyword-spotting strategy's tier-3
|
|
4
|
+
// vocabulary rewrite and resolveObject's tier-5 label pass both read editDistance/
|
|
5
|
+
// fuzzyBound from here, and the "assuming you meant …" announcement discipline
|
|
6
|
+
// (a unique within-bound hit is announced, a tie is refused or surfaced as
|
|
7
|
+
// ambiguity, never a silently-broken guess) is enforced by the callers off these
|
|
8
|
+
// primitives. Deliberately coupled to the curated vocab tables via explicit
|
|
9
|
+
// imports — the fuzzy TARGETS are a closed, curated set, same ethos as the
|
|
10
|
+
// tables themselves. Pure JS, no deps.
|
|
11
|
+
|
|
12
|
+
import { VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND } from "../ask-vocab.mjs";
|
|
13
|
+
import { STOPWORDS } from "./normalize.mjs";
|
|
14
|
+
|
|
15
|
+
// ---- bounded edit distance — hand-rolled Damerau-Levenshtein (optimal string
|
|
16
|
+
// alignment: substitution/insertion/deletion + adjacent transposition), bounded
|
|
17
|
+
// with an early row-minimum exit. Fires only after every exact/curated tier
|
|
18
|
+
// missed, and a distance TIE is refused (keyword) or surfaced as ambiguity
|
|
19
|
+
// (object), never broken by a guess. ----
|
|
20
|
+
|
|
21
|
+
/** Distance between a and b, or max+1 as soon as it provably exceeds `max`. */
|
|
22
|
+
export function editDistance(a, b, max) {
|
|
23
|
+
if (a === b) return 0;
|
|
24
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
25
|
+
let prev2 = null;
|
|
26
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
27
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
28
|
+
const cur = [i];
|
|
29
|
+
let rowMin = i;
|
|
30
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
31
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
32
|
+
let v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
33
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, prev2[j - 2] + cost);
|
|
34
|
+
cur[j] = v;
|
|
35
|
+
if (v < rowMin) rowMin = v;
|
|
36
|
+
}
|
|
37
|
+
if (rowMin > max) return max + 1;
|
|
38
|
+
prev2 = prev;
|
|
39
|
+
prev = cur;
|
|
40
|
+
}
|
|
41
|
+
return prev[b.length];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The curated distance budget: 1 edit for short tokens, 2 for longer ones. */
|
|
45
|
+
export const fuzzyBound = (s) => (s.length <= 5 ? 1 : 2);
|
|
46
|
+
|
|
47
|
+
/** Every single word appearing in the three parse tables — the "is this word
|
|
48
|
+
* already vocabulary?" gate for the lemma/fuzzy canonicalization passes (an
|
|
49
|
+
* exact vocab word is NEVER rewritten: exact curated match always wins). */
|
|
50
|
+
export const VOCAB_WORDS = new Set(
|
|
51
|
+
[...Object.keys(VERB_TO_KIND), ...Object.keys(ENTITY_TO_TYPE), ...Object.keys(MODIFIER_TO_KIND)]
|
|
52
|
+
.flatMap((p) => p.split(" ")),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/** Fuzzy-correction TARGETS: verb-phrase and modifier constituents only, length ≥4.
|
|
56
|
+
* Entity nouns are deliberately excluded — real identifiers collide with them at
|
|
57
|
+
* distance ≤2 far too easily ("myfile" is 2 edits from "file", "caller" 2 from
|
|
58
|
+
* "calls"-family words), and entity-noun typos are already owned by the curated
|
|
59
|
+
* MISSPELLINGS table where such calls are made deliberately. Short constituents
|
|
60
|
+
* ("of", "to", "in", "on") are excluded for the same reason: at bound 1 half of
|
|
61
|
+
* English is adjacent to them. */
|
|
62
|
+
const FUZZY_TARGET_WORDS = [...new Set(
|
|
63
|
+
[...Object.keys(VERB_TO_KIND), ...Object.keys(MODIFIER_TO_KIND)]
|
|
64
|
+
.flatMap((p) => p.split(" "))
|
|
65
|
+
.filter((w) => w.length >= 4),
|
|
66
|
+
)];
|
|
67
|
+
|
|
68
|
+
/** A query word may be canonicalized only if it is plain alphabetic, not a
|
|
69
|
+
* stopword, and not already vocabulary. Dotted/digit terms (file names, shas)
|
|
70
|
+
* are never touched. */
|
|
71
|
+
export function eligibleForCanon(w) {
|
|
72
|
+
return /^[a-z]+$/.test(w) && !STOPWORDS.has(w) && !VOCAB_WORDS.has(w);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** UNIQUE within-bound fuzzy vocab keyword for `w`, or null — a tie between two
|
|
76
|
+
* distinct target words at the same distance is refused outright (the honest-miss
|
|
77
|
+
* discipline at the vocabulary level; cf. MISSPELLINGS' curated "calss" decision). */
|
|
78
|
+
export function fuzzyVocabWord(w) {
|
|
79
|
+
const bound = fuzzyBound(w);
|
|
80
|
+
let best = bound + 1;
|
|
81
|
+
let hit = null;
|
|
82
|
+
let tied = false;
|
|
83
|
+
for (const target of FUZZY_TARGET_WORDS) {
|
|
84
|
+
const d = editDistance(w, target, Math.min(best, bound));
|
|
85
|
+
if (d < best) { best = d; hit = target; tied = false; }
|
|
86
|
+
else if (d === best && d <= bound && target !== hit) tied = true;
|
|
87
|
+
}
|
|
88
|
+
return best <= bound && !tied ? hit : null;
|
|
89
|
+
}
|