@polycode-projects/the-mechanical-code-talker 1.0.3 → 1.0.4
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/package.json +1 -1
- package/src/ask.mjs +124 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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/ask.mjs
CHANGED
|
@@ -311,7 +311,8 @@ function parseSimpleClause(text, nlp) {
|
|
|
311
311
|
function parseComposite(text, nlp) {
|
|
312
312
|
const w = splitWords(text);
|
|
313
313
|
const lc = w.map((x) => x.toLowerCase());
|
|
314
|
-
return
|
|
314
|
+
return parseExistence(w, lc)
|
|
315
|
+
|| parseNegation(text, nlp, 0)
|
|
315
316
|
|| parseForwardNegation(w, lc, nlp)
|
|
316
317
|
|| parseTemporal(w, lc, nlp, 0)
|
|
317
318
|
|| parseAnaphora(w, lc, nlp)
|
|
@@ -580,6 +581,72 @@ function parsePredicateFilter(words, nlp) {
|
|
|
580
581
|
return undefined;
|
|
581
582
|
}
|
|
582
583
|
|
|
584
|
+
/** EXISTENCE: "is there a/an <kind> [called|named <term>] [in <module>] [anywhere]"
|
|
585
|
+
* and "are there any <kind>(s) [called|named <term>] [in <module>]" — a genuine
|
|
586
|
+
* existence question ("does this kind/name exist at all", optionally scoped to a
|
|
587
|
+
* module), answered directly against class/kind membership rather than routed
|
|
588
|
+
* through the relation-verb machinery. Triage bug (2026-07-09, seonix dogfooding):
|
|
589
|
+
* with no dedicated recognizer, "is there a class called Store anywhere" fell
|
|
590
|
+
* through to the legacy keyword-spot strategy, whose lemma tier canonicalizes
|
|
591
|
+
* "called" -> "call" (a `calls` verb — ask-vocab.mjs) and silently answered a
|
|
592
|
+
* DIFFERENT question ("which classes call Store") with a confidently-wrong-shaped
|
|
593
|
+
* negative, even though a class named Store genuinely exists. "is there a class in
|
|
594
|
+
* <module>" walled out the same way — no marker in this grammar recognized it at
|
|
595
|
+
* all. Scoped to a tight closed shape: a leading "is there a/an" or "are there any"
|
|
596
|
+
* immediately followed by a recognized entity-kind noun, then ONLY "called"/"named
|
|
597
|
+
* <term>", "in <module>", the two combined, or an empty/"anywhere"/"at all" tail —
|
|
598
|
+
* anything else (a relative clause, a verb phrase: "is there a class THAT CALLS
|
|
599
|
+
* Store") is a genuine relationship question and is left untouched for the
|
|
600
|
+
* relation parsers below, never swallowed here. */
|
|
601
|
+
function parseExistence(w, lc) {
|
|
602
|
+
let i;
|
|
603
|
+
if (lc[0] === "is" && lc[1] === "there") i = 2;
|
|
604
|
+
else if (lc[0] === "are" && lc[1] === "there") i = 2;
|
|
605
|
+
else return null;
|
|
606
|
+
const article = lc[i];
|
|
607
|
+
if (article === "a" || article === "an" || article === "any") i += 1;
|
|
608
|
+
else return null;
|
|
609
|
+
const noun = i < lc.length ? entityNoun(lc[i]) : null;
|
|
610
|
+
if (!noun || noun.placeholder || !noun.entityType) return null;
|
|
611
|
+
const entityType = noun.entityType;
|
|
612
|
+
i += 1;
|
|
613
|
+
|
|
614
|
+
let rest = lc.slice(i);
|
|
615
|
+
let restW = w.slice(i);
|
|
616
|
+
// trailing filler — "anywhere" / "at all" — stripped so it never gets misread as
|
|
617
|
+
// a (nonexistent) module/name term below.
|
|
618
|
+
if (rest.length && rest[rest.length - 1] === "anywhere") {
|
|
619
|
+
rest = rest.slice(0, -1); restW = restW.slice(0, -1);
|
|
620
|
+
} else if (rest.length >= 2 && rest[rest.length - 2] === "at" && rest[rest.length - 1] === "all") {
|
|
621
|
+
rest = rest.slice(0, -2); restW = restW.slice(0, -2);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!rest.length) return { node: "exists", entityType, term: null, scopeModule: null };
|
|
625
|
+
|
|
626
|
+
if (rest[0] === "called" || rest[0] === "named") {
|
|
627
|
+
if (rest.length < 2) return { node: "miss", reason: `"${rest[0]}" needs a name afterward` };
|
|
628
|
+
const inIdx = rest.indexOf("in", 1);
|
|
629
|
+
if (inIdx > 0) {
|
|
630
|
+
const term = restW.slice(1, inIdx).join(" ").trim();
|
|
631
|
+
const scopeModule = restW.slice(inIdx + 1).join(" ").trim();
|
|
632
|
+
if (!term || !scopeModule) return { node: "miss", reason: `a named existence check needs both a name and a module after "in"` };
|
|
633
|
+
return { node: "exists", entityType, term, scopeModule };
|
|
634
|
+
}
|
|
635
|
+
const term = restW.slice(1).join(" ").trim();
|
|
636
|
+
return term ? { node: "exists", entityType, term, scopeModule: null }
|
|
637
|
+
: { node: "miss", reason: `"${rest[0]}" needs a name afterward` };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (rest[0] === "in") {
|
|
641
|
+
const scopeModule = restW.slice(1).join(" ").trim();
|
|
642
|
+
return scopeModule ? { node: "exists", entityType, term: null, scopeModule }
|
|
643
|
+
: { node: "miss", reason: `"in" needs a module afterward` };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return null; // a relative clause / verb phrase / anything else — genuinely a
|
|
647
|
+
// different (relationship) question; leave it for the parsers below.
|
|
648
|
+
}
|
|
649
|
+
|
|
583
650
|
/** Trailing "and that's the whole question" filler an aggregate/list tail can carry
|
|
584
651
|
* ("how many classes are there", "list functions in total", "which classes exist in
|
|
585
652
|
* the index") — a count/list over a bare kind is frequently phrased with such a tail,
|
|
@@ -1479,10 +1546,42 @@ function evalSuperlative(graph, ast) {
|
|
|
1479
1546
|
return { compositeKind: "superlative", entityType: ast.entityType, metricNoun: ast.metricNoun, extreme: ast.extreme, score: best, matches: winners };
|
|
1480
1547
|
}
|
|
1481
1548
|
|
|
1549
|
+
/** EXISTENCE eval — "is there a/an <kind> [called/named <term>] [in <module>]": a
|
|
1550
|
+
* direct membership/name check against the graph, never routed through the
|
|
1551
|
+
* relation-verb machinery. A named check resolves the term against the SAME
|
|
1552
|
+
* tiered resolveObject() every other named-lookup shape uses (expectedClass pins
|
|
1553
|
+
* the pool to the asked kind, so "is there a class called Store" can never
|
|
1554
|
+
* resolve to a same-named function/module); a scope clause resolves the module
|
|
1555
|
+
* the same way and narrows the check to that module's own `defines` edges
|
|
1556
|
+
* (refineToEntities — the same primitive members-of-a-module questions use). */
|
|
1557
|
+
function evalExists(graph, ast) {
|
|
1558
|
+
const { entityType, term, scopeModule } = ast;
|
|
1559
|
+
let scopeMatch = null;
|
|
1560
|
+
if (scopeModule) {
|
|
1561
|
+
const r = resolveObject(graph, scopeModule, { expectedClass: "Module" });
|
|
1562
|
+
if (!r.match) return { compositeKind: "exists", entityType, term, scopeModule, scopeMiss: true, matches: [] };
|
|
1563
|
+
scopeMatch = r.match;
|
|
1564
|
+
}
|
|
1565
|
+
if (term) {
|
|
1566
|
+
const r = resolveObject(graph, term, { expectedClass: entityType });
|
|
1567
|
+
const inScope = !scopeMatch || (r.match && moduleIdOf(graph, r.match) === scopeMatch.id);
|
|
1568
|
+
const hit = r.match && inScope;
|
|
1569
|
+
return {
|
|
1570
|
+
compositeKind: "exists", entityType, term, scopeModule, scopeMatch,
|
|
1571
|
+
matches: hit ? [r.match] : [],
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
const pool = scopeMatch
|
|
1575
|
+
? refineToEntities(graph, new Set([scopeMatch.id]), entityType)
|
|
1576
|
+
: graph.individuals.filter((i) => i.class === entityType);
|
|
1577
|
+
return { compositeKind: "exists", entityType, term: null, scopeModule, scopeMatch, matches: pool };
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1482
1580
|
/** Compile any compositional AST to a result object traverse() returns for the
|
|
1483
1581
|
* simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
|
|
1484
1582
|
export function evalComposite(graph, ast, opts = {}) {
|
|
1485
1583
|
if (ast.node === "miss") return { compositeMiss: true, reason: ast.reason || null, matches: [] };
|
|
1584
|
+
if (ast.node === "exists") return evalExists(graph, ast);
|
|
1486
1585
|
if (ast.node === "count") return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
|
|
1487
1586
|
if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
|
|
1488
1587
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
@@ -1532,6 +1631,30 @@ function renderComposite(parsed, result) {
|
|
|
1532
1631
|
}
|
|
1533
1632
|
return { content: `couldn't compile this compositional question${result.reason ? ` (${result.reason})` : ""}. ${compositionalHint()}.`, miss: true, ambiguous: false };
|
|
1534
1633
|
}
|
|
1634
|
+
// exists: "is there a/an <kind> [called/named <term>] [in <module>]" — an
|
|
1635
|
+
// honest Yes/No membership check, never routed through the relation-verb
|
|
1636
|
+
// machinery (see parseExistence's own doc for the bug this fixes).
|
|
1637
|
+
if (result.compositeKind === "exists") {
|
|
1638
|
+
if (result.scopeMiss) {
|
|
1639
|
+
return { content: `no module matching "${result.scopeModule}" found in the index.`, miss: true, ambiguous: false };
|
|
1640
|
+
}
|
|
1641
|
+
const kindSingular = nounFor(result.entityType, 1);
|
|
1642
|
+
const kindPlural = nounFor(result.entityType, 2);
|
|
1643
|
+
const scopeSuffix = result.scopeMatch ? ` in ${result.scopeMatch.label}` : "";
|
|
1644
|
+
if (result.term) {
|
|
1645
|
+
if (!result.matches.length) {
|
|
1646
|
+
return { content: `No — no ${kindSingular} named "${result.term}" found${scopeSuffix}.`, miss: true, ambiguous: false };
|
|
1647
|
+
}
|
|
1648
|
+
const hit = result.matches[0];
|
|
1649
|
+
const modLabel = moduleLabelOf(hit);
|
|
1650
|
+
const definedIn = hit.class === "Module" ? "" : (modLabel && modLabel !== "(unknown module)" ? `, defined in ${modLabel}` : "");
|
|
1651
|
+
return { content: `Yes — ${hit.label} is a ${kindSingular}${definedIn}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1652
|
+
}
|
|
1653
|
+
if (!result.matches.length) {
|
|
1654
|
+
return { content: `No — no ${kindPlural} found${scopeSuffix}.`, miss: true, ambiguous: false };
|
|
1655
|
+
}
|
|
1656
|
+
return { content: `Yes — ${compositeList(result.matches)}${scopeSuffix}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1657
|
+
}
|
|
1535
1658
|
if (result.compositeKind === "count") {
|
|
1536
1659
|
const noun = result.entityType ? nounFor(result.entityType, result.count) : (result.count === 1 ? "result" : "results");
|
|
1537
1660
|
return { content: `${result.count} ${noun}.`, miss: false, ambiguous: false, matches: [] };
|