@polycode-projects/the-mechanical-code-talker 2.11.0 → 2.11.1
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/data/templates/responses.jsonl +1 -0
- package/package.json +1 -1
- package/src/domain/ask-vocab.mjs +17 -0
- package/src/domain/ask.mjs +51 -1
- package/src/domain/grammar/ace.mjs +43 -3
- package/src/domain/interpret/normalize.mjs +6 -2
- package/src/domain/interpret/strategies/grammar.mjs +47 -17
- package/src/domain/interpret/strategies/keywords.mjs +53 -4
- package/src/domain/router/registry.mjs +8 -1
- package/src/services/chat.mjs +189 -40
- package/src/surfaces/web/memory-ask-browser.bundle.js +120 -120
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
{"id":"identity-self","class":"conversational","register":"friendly","template":"I'm tmct — a deterministic, offline chat assistant. No LLM: wink-nlp parsing over a seeded ontology/lexicon, plus a code graph when you point me at a repo with `--repo <path>`. /help for commands, /stats for an overview."}
|
|
69
69
|
{"id":"identity-not-an-llm","class":"conversational","register":"friendly","template":"No — no LLM involved. tmct is deterministic: wink-nlp parsing over a graph/ontology, not a language model. /help for commands."}
|
|
70
70
|
{"id":"identity-no-feelings","class":"conversational","register":"friendly","template":"No — I don't have feelings, opinions, or consciousness. tmct is deterministic: wink-nlp parsing over a graph/ontology, not a mind. /help for commands."}
|
|
71
|
+
{"id":"identity-honest-miss","class":"conversational","register":"friendly","template":"No — I never make up an answer. If a question doesn't ground to a taught fact or a real graph entity, I say so plainly instead of guessing. /help for commands."}
|
|
71
72
|
{"id":"technical-density","class":"count","register":"technical","template":"{subject} carries {count} {noun} across {scope} — a concentration well above what a codebase of this size typically sustains ({provenance})."}
|
|
72
73
|
{"id":"technical-comparison","class":"count","register":"technical","template":"At {count} {noun}, {subject} sits {comparison} the comparable-project baseline, a divergence that reflects deliberate structure rather than measurement noise ({provenance})."}
|
|
73
74
|
{"id":"technical-superlative","class":"count","register":"technical","template":"No {noun} in {scope} is more {metric} than {subject}; it leads the next candidate by a clear margin of {count} ({provenance})."}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.1",
|
|
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.",
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -246,6 +246,23 @@ export const VERB_TO_KIND = Object.freeze(
|
|
|
246
246
|
),
|
|
247
247
|
);
|
|
248
248
|
|
|
249
|
+
/** The bare possessive "has"/"have"/"holds"/"hold" is bucketed onto `defines`
|
|
250
|
+
* (RELATIONS.defines.verbs above) for genuine code shapes ("what modules
|
|
251
|
+
* does app.mjs have", "does createTask have tests") — but those reach the
|
|
252
|
+
* grammar/keyword-spot strategies pre-rewritten onto a different verb by
|
|
253
|
+
* normalize.mjs's PHRASING_FRAMES (has-tests -> "what tests X", members ->
|
|
254
|
+
* "what does X contain"), and the possession/property sense of "have" over a
|
|
255
|
+
* TAUGHT individual ("does whiskers have fur") is answered entirely by
|
|
256
|
+
* chat.mjs's own has-a/hasProperty readers, upstream of both strategies. So
|
|
257
|
+
* a bare have-family verb reaching either strategy's TWO-NAMED-ROLE "ask"
|
|
258
|
+
* shape ("does X have Y", free-text subject and object, no grain check) has
|
|
259
|
+
* no legitimate code question left to mean, and confidently framing it as
|
|
260
|
+
* "locate what a module/class defines" is worse than an honest miss — both
|
|
261
|
+
* grammar.mjs (T1/T2/T3) and keywords.mjs check this set to decline instead.
|
|
262
|
+
* Shared here (rather than declared per-file) so the two strategies' bundled
|
|
263
|
+
* builds never collide on the same top-level identifier. */
|
|
264
|
+
export const HAS_FAMILY_VERBS = Object.freeze(new Set(["has", "have", "holds", "hold"]));
|
|
265
|
+
|
|
249
266
|
/** "what is a kind of X" / "what is a subclass of X" collision fix: some
|
|
250
267
|
* inherits verbs are themselves phrased "is a <continuation>", which would
|
|
251
268
|
* otherwise collide with grammar.mjs's literal meta-whatis reading and
|
package/src/domain/ask.mjs
CHANGED
|
@@ -1236,6 +1236,29 @@ function reverseOverSet(graph, kind, entityType, objectIds) {
|
|
|
1236
1236
|
return [];
|
|
1237
1237
|
}
|
|
1238
1238
|
|
|
1239
|
+
const GIT_PROV_REF_RE = /^git:(.+)$/i;
|
|
1240
|
+
|
|
1241
|
+
/** How many distinct commits are attested to have touched a SET of entities —
|
|
1242
|
+
* the same "touched by N commit(s)" convention renderDescribe's attestation
|
|
1243
|
+
* line already uses (codegraph.mjs's turnRefCount), not the narrower "touches
|
|
1244
|
+
* edge count" reverseOverSet(kind="touches") returns. A commit can be recorded
|
|
1245
|
+
* in an entity's own `derived_from` provenance with no full Commit individual
|
|
1246
|
+
* of its own (the ingester's touches-edge and provenance-ref writes can drift,
|
|
1247
|
+
* e.g. a truncated commit walk) — reverseOverSet alone then undercounts, or
|
|
1248
|
+
* misses entirely when NO touches edge survived. Deduped by short sha so a
|
|
1249
|
+
* provenance ref naming a commit that DOES have a touches-edge individual
|
|
1250
|
+
* isn't double-counted. */
|
|
1251
|
+
function commitTouchCount(graph, objectIds) {
|
|
1252
|
+
const shas = new Set(reverseOverSet(graph, "touches", "Commit", objectIds).map((c) => String(c.label || "").toLowerCase()));
|
|
1253
|
+
for (const id of objectIds) {
|
|
1254
|
+
for (const ref of graph.byId.get(id)?.derived_from || []) {
|
|
1255
|
+
const m = GIT_PROV_REF_RE.exec(String(ref || ""));
|
|
1256
|
+
if (m) shas.add(m[1].toLowerCase());
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return shas.size;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1239
1262
|
/** Forward traversal over a SET of subject ids (the "things {X…} call/define" step). */
|
|
1240
1263
|
function forwardOverSet(graph, kind, subjectIds) {
|
|
1241
1264
|
const edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => subjectIds.has(e.subject));
|
|
@@ -1934,6 +1957,24 @@ function evalUniversal(graph, ast, opts) {
|
|
|
1934
1957
|
};
|
|
1935
1958
|
}
|
|
1936
1959
|
|
|
1960
|
+
/** The object-id set a "how many commits touched <X>" count restricts to,
|
|
1961
|
+
* covering both AST shapes that phrasing compiles to: a flat single-object
|
|
1962
|
+
* reverse clause ("how many commits touched app/lib/a.mjs") or a composed
|
|
1963
|
+
* reverseSet over a nested inner set ("… touched the module that defines
|
|
1964
|
+
* fnAlpha"). Null for every other count shape (including an unresolved or
|
|
1965
|
+
* ambiguous object), so the caller falls back to its plain evalSet(base)
|
|
1966
|
+
* count unchanged. */
|
|
1967
|
+
function commitTouchObjectIds(graph, base, opts) {
|
|
1968
|
+
if (base.node === "reverseSet" && base.kind === "touches" && base.entityType === "Commit") {
|
|
1969
|
+
return new Set(evalSet(graph, base.inner, opts).map((i) => i.id));
|
|
1970
|
+
}
|
|
1971
|
+
if (base.node === "clause" && base.clause?.shape === "reverse" && base.clause.kind === "touches" && base.clause.entityType === "Commit") {
|
|
1972
|
+
const { objMatch, ambiguous } = traverse(graph, base.clause, opts);
|
|
1973
|
+
return objMatch && !ambiguous ? new Set([objMatch.id]) : null;
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1937
1978
|
/** Compile any compositional AST to a result object traverse() returns for the
|
|
1938
1979
|
* simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
|
|
1939
1980
|
function evalComposite(graph, ast, opts = {}) {
|
|
@@ -1941,7 +1982,16 @@ function evalComposite(graph, ast, opts = {}) {
|
|
|
1941
1982
|
if (ast.node === "exists") return evalExists(graph, ast);
|
|
1942
1983
|
if (ast.node === "qualCheck") return evalQualCheck(graph, ast, opts);
|
|
1943
1984
|
if (ast.node === "universal") return evalUniversal(graph, ast, opts);
|
|
1944
|
-
if (ast.node === "count")
|
|
1985
|
+
if (ast.node === "count") {
|
|
1986
|
+
// "how many commits touched <X>" — count against provenance attestation
|
|
1987
|
+
// (commitTouchCount), not the bare touches-edge set evalSet(base) would
|
|
1988
|
+
// give: see commitTouchCount's own doc for why the two can disagree.
|
|
1989
|
+
const commitTouchIds = commitTouchObjectIds(graph, ast.base, opts);
|
|
1990
|
+
if (commitTouchIds) {
|
|
1991
|
+
return { compositeKind: "count", count: commitTouchCount(graph, commitTouchIds), entityType: ast.entityType, matches: [] };
|
|
1992
|
+
}
|
|
1993
|
+
return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
|
|
1994
|
+
}
|
|
1945
1995
|
if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
|
|
1946
1996
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
1947
1997
|
if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
|
|
@@ -106,7 +106,12 @@ function resolveNP(lexicon, tokensIn, { allowCompound = false } = {}) {
|
|
|
106
106
|
if (proper) return { term: `${ns}${proper}`, individual: true, extras: [], unknown: [] };
|
|
107
107
|
if (CODE_REF.test(t)) return { term: `${ns}${t}`, individual: true, extras: [], unknown: [] };
|
|
108
108
|
const noun = lookupNoun(lexicon, t, { singularOnly });
|
|
109
|
-
|
|
109
|
+
// `folded` marks a match that only exists because lookupNoun's own
|
|
110
|
+
// trailing-"-s" strip or irregular-plural table rewrote the surface word
|
|
111
|
+
// — never an exact lexicon hit. parseCopula (below) uses this to keep a
|
|
112
|
+
// proper name that happens to fold to an unrelated dictionary word
|
|
113
|
+
// ("whiskers" -> "whisker") from silently losing its spelling.
|
|
114
|
+
if (noun) return { term: `${ns}${noun.lemma}`, individual: false, noun, folded: noun.lemma.toLowerCase() !== t.toLowerCase(), extras: [], unknown: [] };
|
|
110
115
|
return { term: null, individual: false, extras: [], unknown: [t] };
|
|
111
116
|
}
|
|
112
117
|
if (tokens.length === 2) {
|
|
@@ -161,6 +166,26 @@ function resolveNP(lexicon, tokensIn, { allowCompound = false } = {}) {
|
|
|
161
166
|
return { term: null, individual: false, extras: [], unknown: tokens.filter((t) => !classify(t, lexicon)) };
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
/** A bare single-token subject (no determiner of its own) that resolveNP only
|
|
170
|
+
* resolved via a fold, immediately followed by an indefinite-article object
|
|
171
|
+
* ("whiskers is A CAT", "every whiskers is A CAT"), is the canonical
|
|
172
|
+
* individual-naming shape ("john is a man") wearing a proper name that
|
|
173
|
+
* happens to fold to an unrelated dictionary word ("whiskers" ->
|
|
174
|
+
* "whisker"). Trusting the fold here silently rewrites the taught subject's
|
|
175
|
+
* spelling; declining it and reporting the token as unknown lets the
|
|
176
|
+
* caller's own novel-individual fallback store the literal typed word
|
|
177
|
+
* instead — the same honest-miss-over-guess call this file's
|
|
178
|
+
* exact/irregular-only folds already make everywhere else. Shared by
|
|
179
|
+
* parseCopula and parseEvery, the two patterns whose subject slot can hold
|
|
180
|
+
* a bare single token immediately followed by "a"/"an". */
|
|
181
|
+
function declineFoldedBareSubject(np, subjectToks, objectHead) {
|
|
182
|
+
if (np.term != null && !np.individual && np.folded
|
|
183
|
+
&& subjectToks.length === 1 && /^an?$/i.test(objectHead)) {
|
|
184
|
+
return { term: null, individual: false, extras: [], unknown: [subjectToks[0]] };
|
|
185
|
+
}
|
|
186
|
+
return np;
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
/** The shared miss result: a structural fit with undeclared words returns the
|
|
165
190
|
* pattern + residue (triples empty); a fit with only declared-but-unusable
|
|
166
191
|
* phrasing returns null — the honest fall-through either way. */
|
|
@@ -349,7 +374,16 @@ function parseEvery(lexicon, toks, lower) {
|
|
|
349
374
|
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
350
375
|
const rest = toks.slice(isIdx + 1);
|
|
351
376
|
const everyAdjOnly = rest.length === 1 ? lookupAdjective(lexicon, rest[0]) : null;
|
|
352
|
-
const
|
|
377
|
+
const subjectToks = toks.slice(1, isIdx);
|
|
378
|
+
// Same fold-vs-individual ambiguity parseCopula guards against — a
|
|
379
|
+
// determiner-less "every whiskers is a cat" reaches the identical
|
|
380
|
+
// single-token resolveNP fold ("whiskers" -> "whisker") assertCandidates
|
|
381
|
+
// manufactures as a candidate phrasing whenever the bare payload has no
|
|
382
|
+
// determiner of its own, so this needs the same declineFoldedBareSubject
|
|
383
|
+
// guard or that candidate alone re-opens the bug parseCopula just closed.
|
|
384
|
+
const np1 = declineFoldedBareSubject(
|
|
385
|
+
resolveNP(lexicon, subjectToks, { allowCompound: !everyAdjOnly }), subjectToks, rest[0],
|
|
386
|
+
);
|
|
353
387
|
if (everyAdjOnly) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, everyAdjOnly);
|
|
354
388
|
const np2 = resolveNP(lexicon, rest, { allowCompound: true });
|
|
355
389
|
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
@@ -414,7 +448,13 @@ function parseOfForm(lexicon, toks, lower) {
|
|
|
414
448
|
function parseCopula(lexicon, toks, lower, isIdx) {
|
|
415
449
|
const rest = toks.slice(isIdx + 1);
|
|
416
450
|
if (!rest.length) return null;
|
|
417
|
-
const
|
|
451
|
+
const subjectToks = toks.slice(0, isIdx);
|
|
452
|
+
// parseAce never reaches here for "are", only singular "is" — see
|
|
453
|
+
// declineFoldedBareSubject's own docblock for why a bare single-token
|
|
454
|
+
// subject that only resolves by folding needs this guard.
|
|
455
|
+
const np1 = declineFoldedBareSubject(
|
|
456
|
+
resolveNP(lexicon, subjectToks, { allowCompound: rest.length > 1 }), subjectToks, rest[0],
|
|
457
|
+
);
|
|
418
458
|
if (rest.length === 1) {
|
|
419
459
|
const adj = lookupAdjective(lexicon, rest[0]);
|
|
420
460
|
if (adj) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, adj);
|
|
@@ -128,8 +128,12 @@ const GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy|g'?day|yeah\s+nah|g
|
|
|
128
128
|
* the "thanks" word family: "thanks so much, <Q>" -> "<Q>". */
|
|
129
129
|
const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(?:\s+(?:so\s+much|a\s+lot|very\s+much|a\s+bunch))?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
|
|
130
130
|
/** Acknowledgement lead-in with a delimiter ("ok cool, <Q>"), repeating (`+`)
|
|
131
|
-
* so a stack of ack-words peels in one pass.
|
|
132
|
-
|
|
131
|
+
* so a stack of ack-words peels in one pass. "one more"/"another one"/"just
|
|
132
|
+
* one more" join the ack-word alternation as the same discourse move under a
|
|
133
|
+
* different wording — a throwaway counting aside before the real content,
|
|
134
|
+
* never part of the content itself ("ok, one more, teach me: no server is a
|
|
135
|
+
* client" must peel exactly as "ok cool, <Q>" already does). */
|
|
136
|
+
const ACK_PREAMBLE_RE = /^(?:(?:ok(?:ay)?|aight|cool|alright|sure|right|fine|great|nice|got it|gotcha|sounds good|no worries|no problem|(?:just\s+)?(?:one|another)\s+more|another\s+one)[\s,]+)+(.+)$/i;
|
|
133
137
|
/** Self-orientation lead-in with a delimiter — "just poking around, <Q>",
|
|
134
138
|
* "first time using this, <Q>". */
|
|
135
139
|
const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here)|i'?m\s+new\s+(?:here|around\s+here|to\s+(?:this|all\s+this)(?:\s+(?:repo|codebase|project|app|tool|thing))?))\s*[,.—–-]\s*(.+)$/i;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
|
|
9
9
|
META_MEANING_VERBS, WHERE_MARKERS, MENTION_MARKERS,
|
|
10
10
|
INHERITS_REVERSE_VERBS, stripTrailingScopeFiller, stripTrailingDiscourseTag,
|
|
11
|
-
ARTICLE_RELATION_CONTINUATIONS,
|
|
11
|
+
ARTICLE_RELATION_CONTINUATIONS, HAS_FAMILY_VERBS,
|
|
12
12
|
} from "../../ask-vocab.mjs";
|
|
13
13
|
import { escapeRegex } from "../normalize.mjs";
|
|
14
14
|
|
|
@@ -17,6 +17,11 @@ const ENTITY_ALT = Object.keys(ENTITY_TO_TYPE).sort((a, b) => b.length - a.lengt
|
|
|
17
17
|
const MODIFIER_ALT = Object.keys(MODIFIER_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|
|
18
18
|
const META_ALT = META_MEANING_VERBS.slice().sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|
|
19
19
|
|
|
20
|
+
// See ask-vocab.mjs's own HAS_FAMILY_VERBS for why a bare have-family verb
|
|
21
|
+
// never resolves to `defines` in this template's "ask"/"reverse"/"forward"
|
|
22
|
+
// shapes below.
|
|
23
|
+
const isHasFamilyDefines = (kind, verb) => kind === "defines" && HAS_FAMILY_VERBS.has(verb);
|
|
24
|
+
|
|
20
25
|
const TEMPLATES = [
|
|
21
26
|
// T1 ASK: "does X import Y" -> Yes/No. REVERSE VERB SWAP: a semantically-reverse
|
|
22
27
|
// verb ("superclass of") means the opposite of its forward counterpart, so
|
|
@@ -27,6 +32,7 @@ const TEMPLATES = [
|
|
|
27
32
|
build: (m) => {
|
|
28
33
|
const verb = m[2].toLowerCase();
|
|
29
34
|
const kind = VERB_TO_KIND[verb];
|
|
35
|
+
if (isHasFamilyDefines(kind, verb)) return null;
|
|
30
36
|
let subject = m[1].trim();
|
|
31
37
|
let object = m[3].trim();
|
|
32
38
|
if (INHERITS_REVERSE_VERBS.includes(verb)) [subject, object] = [object, subject];
|
|
@@ -37,23 +43,30 @@ const TEMPLATES = [
|
|
|
37
43
|
{
|
|
38
44
|
name: "reverse",
|
|
39
45
|
re: new RegExp(`^which\\s+(${ENTITY_ALT})\\s+(?:(${MODIFIER_ALT})\\s+)?(${VERB_ALT})\\s+(.+?)\\??$`, "i"),
|
|
40
|
-
build: (m) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
build: (m) => {
|
|
47
|
+
const verb = m[3].toLowerCase();
|
|
48
|
+
const kind = VERB_TO_KIND[verb];
|
|
49
|
+
if (isHasFamilyDefines(kind, verb)) return null;
|
|
50
|
+
return {
|
|
51
|
+
shape: "reverse",
|
|
52
|
+
entityType: ENTITY_TO_TYPE[m[1].toLowerCase()],
|
|
53
|
+
modifier: m[2] ? MODIFIER_TO_KIND[m[2].toLowerCase()] : "direct",
|
|
54
|
+
kind,
|
|
55
|
+
object: m[4].trim(),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
47
58
|
},
|
|
48
59
|
// T3 forward: "what does <object> <verb>" — X is given, list its R-related things.
|
|
49
60
|
// "did" joins does/do for the past-tense commit forms ("what did commit <sha> touch").
|
|
50
61
|
{
|
|
51
62
|
name: "forward",
|
|
52
63
|
re: new RegExp(`^what\\s+(?:does|do|did)\\s+(.+?)\\s+(${VERB_ALT})\\??$`, "i"),
|
|
53
|
-
build: (m) =>
|
|
54
|
-
|
|
55
|
-
kind
|
|
56
|
-
|
|
64
|
+
build: (m) => {
|
|
65
|
+
const verb = m[2].toLowerCase();
|
|
66
|
+
const kind = VERB_TO_KIND[verb];
|
|
67
|
+
if (isHasFamilyDefines(kind, verb)) return null;
|
|
68
|
+
return { shape: "forward", entityType: null, modifier: "direct", kind, object: m[1].trim() };
|
|
69
|
+
},
|
|
57
70
|
},
|
|
58
71
|
// T4 meta: "what does <term> mean" — a question about the graph's own vocabulary,
|
|
59
72
|
// not a graph traversal. VERB_ALT and META_ALT are disjoint tables, so this never
|
|
@@ -65,16 +78,33 @@ const TEMPLATES = [
|
|
|
65
78
|
},
|
|
66
79
|
// T5 meta: "what is a/an <term>" — the bare (no-article) form is restricted to
|
|
67
80
|
// the closed ENTITY_TO_TYPE vocabulary (build() -> null otherwise, falling
|
|
68
|
-
// through); the WITH-article form is unrestricted.
|
|
81
|
+
// through); the WITH-article form is unrestricted. A "the"-article form is
|
|
82
|
+
// ALSO accepted, but only for a single-token term ("the Task") — the same
|
|
83
|
+
// schema-then-code-entity lookup a bare "Task" would reach (traverse()'s
|
|
84
|
+
// shape:"meta" handling, via metaFallbackEntityAnswer). Multi-word "the …"
|
|
85
|
+
// phrases stay excluded on purpose: "what is the meaning of this codebase"/
|
|
86
|
+
// "the purpose of X" are existential framings with their own decline
|
|
87
|
+
// elsewhere, never a literal term to look up (see the out-of-grammar test
|
|
88
|
+
// this guards).
|
|
69
89
|
{
|
|
70
90
|
name: "meta-whatis",
|
|
71
|
-
re: new RegExp(`^what\\s+(?:is|are)\\s+(?:(an
|
|
91
|
+
re: new RegExp(`^what\\s+(?:is|are)\\s+(?:(an?|the)\\s+)?(.+?)\\??$`, "i"),
|
|
72
92
|
build: (m) => {
|
|
93
|
+
const article = m[1] ? m[1].toLowerCase() : null;
|
|
73
94
|
const object = stripTrailingDiscourseTag(m[2].trim());
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
const isSingleToken = !/\s/.test(object);
|
|
96
|
+
if (article === "the" && !isSingleToken) return null;
|
|
97
|
+
if (!article && !ENTITY_TO_TYPE[object.toLowerCase()]) return null; // bare form: closed-set only
|
|
76
98
|
const objLower = object.toLowerCase();
|
|
77
|
-
|
|
99
|
+
// "what is a kind/subclass of X" is an inherits phrasing, not a term to
|
|
100
|
+
// define — ARTICLE_RELATION_CONTINUATIONS only ever derives from the
|
|
101
|
+
// "is a/an <continuation>" verb forms, so it's checked only for those;
|
|
102
|
+
// "the"-definite reverse-inherits forms ("is the superclass of") are a
|
|
103
|
+
// separate, deliberately unfolded set (ask-vocab.mjs's own comment on
|
|
104
|
+
// INHERITS_REVERSE_VERB_LIST) — moot here since those are always
|
|
105
|
+
// multi-word and already excluded by the single-token check above, but
|
|
106
|
+
// named for the same reason ARTICLE_RELATION_CONTINUATIONS is.
|
|
107
|
+
if (article && article !== "the" && ARTICLE_RELATION_CONTINUATIONS.some(
|
|
78
108
|
(c) => objLower === c || objLower.startsWith(`${c} `),
|
|
79
109
|
)) return null;
|
|
80
110
|
return { shape: "meta", entityType: null, modifier: "direct", kind: "meta", object: stripTrailingScopeFiller(object) };
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
|
|
10
10
|
WHERE_MARKERS, MENTION_MARKERS, PLACEHOLDER_NOUNS, PASSIVE_PARTICIPLE_TO_KIND,
|
|
11
|
-
INHERITS_REVERSE_VERBS,
|
|
11
|
+
INHERITS_REVERSE_VERBS, HAS_FAMILY_VERBS,
|
|
12
12
|
} from "../../ask-vocab.mjs";
|
|
13
13
|
import { STOPWORDS } from "../normalize.mjs";
|
|
14
14
|
import { VOCAB_WORDS, eligibleForCanon, fuzzyVocabWord } from "../fuzzy.mjs";
|
|
@@ -19,6 +19,10 @@ import { VOCAB_WORDS, eligibleForCanon, fuzzyVocabWord } from "../fuzzy.mjs";
|
|
|
19
19
|
const PASSIVE_AUX = new Set(["is", "are", "was", "were", "be", "been", "being", "get", "gets", "got"]);
|
|
20
20
|
const WH_WORDS = new Set(["which", "what", "who", "whom", "whose"]);
|
|
21
21
|
const PLACEHOLDER_SET = new Set(PLACEHOLDER_NOUNS.map((w) => w.toLowerCase()));
|
|
22
|
+
// See ask-vocab.mjs's own HAS_FAMILY_VERBS for why a bare have-family verb
|
|
23
|
+
// never resolves to `defines` in this strategy's two-named-role "ask" shape
|
|
24
|
+
// below (the forward/reverse branches keep their own tested grain-check
|
|
25
|
+
// decline, per that constant's own docblock).
|
|
22
26
|
|
|
23
27
|
/** Find the longest phrase from `table`'s keys that appears as a contiguous
|
|
24
28
|
* run of `words` (case already lowercased by the caller). Longest-match-first
|
|
@@ -107,6 +111,13 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
107
111
|
fuzzyVerb = { from: lcWords[at], to: fuzzyWords[at] };
|
|
108
112
|
}
|
|
109
113
|
}
|
|
114
|
+
// Tracks whether verbHit came from PASSIVE_PARTICIPLE_TO_KIND's fallback
|
|
115
|
+
// rather than an active VERB_TO_KIND entry — that table's own header says
|
|
116
|
+
// it's only meant to fire once a passive auxiliary AND an agent-marking
|
|
117
|
+
// "by" are confirmed. A direct complement with no "by" ("my cat is called
|
|
118
|
+
// whiskers") is the naming sense of "call", not the invoke-relation passive,
|
|
119
|
+
// so this flag gates the ask-shape SVO fallback below from misreading it.
|
|
120
|
+
let verbFromParticiple = false;
|
|
110
121
|
if (!verbHit) {
|
|
111
122
|
// A participle with no active verb entry still marks a passive when a passive
|
|
112
123
|
// auxiliary precedes it — with or without an agent "by" phrase. "is http.mjs
|
|
@@ -125,10 +136,28 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
125
136
|
for (let i = 0; i < lcWords.length; i += 1) {
|
|
126
137
|
const k = PASSIVE_PARTICIPLE_TO_KIND[lcWords[i]];
|
|
127
138
|
if (k && lcWords[i] === "used" && lcWords[i + 1] === "for") continue;
|
|
128
|
-
if (k && lcWords.slice(0, i).some((w) => PASSIVE_AUX.has(w))) {
|
|
139
|
+
if (k && lcWords.slice(0, i).some((w) => PASSIVE_AUX.has(w))) {
|
|
140
|
+
verbHit = { kind: k, start: i, end: i + 1 };
|
|
141
|
+
verbFromParticiple = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
146
|
if (!verbHit) return null;
|
|
147
|
+
// A single-word verbHit whose LITERAL surface text (not the lemma the tier-2
|
|
148
|
+
// pass may have rewritten it to) is itself a PASSIVE_PARTICIPLE_TO_KIND entry,
|
|
149
|
+
// preceded by a passive auxiliary, is the same "needs a confirmed 'by' agent"
|
|
150
|
+
// case the fallback loop above already flags — checked here too because the
|
|
151
|
+
// lemma tier reaches it independently: lemma("called") is "call", an ACTIVE
|
|
152
|
+
// verb-table entry in its own right, so tier 2 resolves verbHit before the
|
|
153
|
+
// fallback loop ever runs, silently losing the participle reading. "my cat is
|
|
154
|
+
// called whiskers" (the naming sense of "call") must not be read as though
|
|
155
|
+
// "cat" were the active subject invoking "whiskers".
|
|
156
|
+
if (!verbFromParticiple && verbHit.end - verbHit.start === 1
|
|
157
|
+
&& PASSIVE_PARTICIPLE_TO_KIND[lcWords[verbHit.start]]
|
|
158
|
+
&& lcWords.slice(0, verbHit.start).some((w) => PASSIVE_AUX.has(w))) {
|
|
159
|
+
verbFromParticiple = true;
|
|
160
|
+
}
|
|
132
161
|
// A tier-3 verb is a REPAIR, not a reading — downstream consumers (the teach
|
|
133
162
|
// lane's canonical receipt, and the chat surface's fuzzy-verb decline) need
|
|
134
163
|
// to know the difference AND which word was rewritten, so {from, to} rides
|
|
@@ -221,15 +250,35 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
221
250
|
const [patient, agent] = agentIsFronted
|
|
222
251
|
? [roleText(passiveAuxIdx + 1, words.length), roleText(byIdx + 1, passiveAuxIdx)]
|
|
223
252
|
: [roleText(0, byIdx), roleText(byIdx + 1, words.length)];
|
|
224
|
-
if (patient && agent)
|
|
253
|
+
if (patient && agent) {
|
|
254
|
+
if (kind === "defines" && HAS_FAMILY_VERBS.has(canonWords.slice(verbHit.start, verbHit.end).join(" "))) return null;
|
|
255
|
+
return stamp({ shape: "ask", entityType: null, modifier: "direct", kind, subject: agent, object: patient });
|
|
256
|
+
}
|
|
225
257
|
if (agent) return stamp({ shape: "forward", entityType, modifier, kind, object: agent });
|
|
226
258
|
if (patient) return stamp({ shape: "reverse", entityType, modifier, kind, object: patient });
|
|
227
259
|
}
|
|
228
260
|
|
|
261
|
+
// "called" specifically (never the rest of the participle family — "was it
|
|
262
|
+
// touched recently"/"is X used anywhere" are genuine bare-passive code
|
|
263
|
+
// queries with a trailing adverb, not a competing sense, and must keep
|
|
264
|
+
// reaching their existing reading) carries a whole separate NAMING sense
|
|
265
|
+
// ("my cat is called whiskers") distinct from the invoke-relation passive
|
|
266
|
+
// this table exists for. A following complement with no "by" agent is that
|
|
267
|
+
// naming sense, not "whiskers calls cat" — falling through to the
|
|
268
|
+
// active-SVO/reverse branches below would read the participle as if it
|
|
269
|
+
// were an active verb and answer a code-graph question nobody asked, so
|
|
270
|
+
// this misses honestly instead.
|
|
271
|
+
if (verbFromParticiple && byIdx < 0 && afterText && lcWords[verbHit.start] === "called") return null;
|
|
272
|
+
|
|
229
273
|
if (beforeText && afterText) {
|
|
274
|
+
const verbPhrase = canonWords.slice(verbHit.start, verbHit.end).join(" ");
|
|
275
|
+
// The bare have-family "ask" shape ("does X have Y") declines here, same
|
|
276
|
+
// reasoning as grammar.mjs's T1 (see HAS_FAMILY_VERBS above) — never for
|
|
277
|
+
// the entityType-driven forward/reverse branches below, which keep their
|
|
278
|
+
// own tested grain-check decline.
|
|
279
|
+
if (kind === "defines" && HAS_FAMILY_VERBS.has(verbPhrase)) return null;
|
|
230
280
|
// A semantically-reverse verb ("superclass of") swaps subject/object, same as
|
|
231
281
|
// grammar.mjs's T1.
|
|
232
|
-
const verbPhrase = canonWords.slice(verbHit.start, verbHit.end).join(" ");
|
|
233
282
|
let subject = beforeText;
|
|
234
283
|
let object = afterText;
|
|
235
284
|
if (INHERITS_REVERSE_VERBS.includes(verbPhrase)) [subject, object] = [object, subject];
|
|
@@ -242,11 +242,18 @@ export function registerCapability(cap) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// ---- unregistered dispatch tools ---------------------------------------------
|
|
245
|
-
// Dispatch tools not yet registered; each names the
|
|
245
|
+
// Dispatch tools not yet registered; each names the work it needs first — a
|
|
246
|
+
// precondition/effect design, or resolver wiring, before it can join the registry.
|
|
246
247
|
export const EXCLUDED_FROM_REGISTRY = Object.freeze({
|
|
247
248
|
tmct_context: "unbounded edit-context bundle (multi-file); needs a size/budget precondition",
|
|
248
249
|
tmct_context_more: "unbounded context continuation; same as tmct_context",
|
|
249
250
|
tmct_snippet: "raw source-file read (reads the filesystem); needs a file-read + span precondition",
|
|
251
|
+
tmct_ask: "the plain-English question entry point itself — calls ask.mjs directly and bypasses the capability planner; not one of the planner's operators",
|
|
252
|
+
tmct_export: "reads the memory store's whole fact set with no discriminating param and no resolver goal frame; needs NL-reachability wiring before it can join",
|
|
253
|
+
tmct_ingest: "writes into the memory store (grounds facts); capability() only models read-only operators, so a write path needs its own precondition/effect design first",
|
|
254
|
+
tmct_file_history: "a module-grain cut of the same history edge tmct_history already models; no NL frame distinguishes the granularities yet",
|
|
255
|
+
tmct_method_history: "a method-grain cut of the same history edge tmct_history already models; no NL frame distinguishes the granularities yet",
|
|
256
|
+
tmct_class_history: "a class-grain cut of the same history edge tmct_history already models; no NL frame distinguishes the granularities yet",
|
|
250
257
|
});
|
|
251
258
|
|
|
252
259
|
/** The full registry as a plain frozen object (facts + index), for callers that
|