@polycode-projects/the-mechanical-code-talker 1.12.0 → 2.0.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/README.md +244 -48
- package/ROADMAP.md +23 -34
- package/bin/tmct.mjs +107 -71
- package/corpus/LICENSES.json +118 -0
- package/corpus/README.md +17 -13
- package/corpus/conceptnet/README.md +5 -5
- package/corpus/conceptnet/fetch-slice.mjs +1 -1
- package/corpus/conceptnet/filter-dump.mjs +1 -1
- package/corpus/generated/README.md +9 -10
- package/corpus/namenet/README.md +39 -0
- package/corpus/seon/README.md +2 -2
- package/corpus/tier2/generate.mjs +58 -9
- package/corpus/tier2/manifest.json +44 -0
- package/corpus/wordnet/README.md +37 -0
- package/corpus/wordnet/generate.mjs +1 -1
- package/data/templates/constructions/agent-noun-relations.toml +2 -2
- package/data/templates/grammar-rules.toml +1 -1
- package/package.json +13 -22
- package/src/{ask-nlp.mjs → adapters/ask-nlp.mjs} +1 -1
- package/src/{config.mjs → adapters/config.mjs} +1 -1
- package/src/{corpus → adapters/corpus}/conceptnet-map.toml +4 -4
- package/src/{corpus → adapters/corpus}/conceptnet.mjs +3 -3
- package/src/adapters/corpus/construction-banks.mjs +43 -0
- package/src/{corpus → adapters/corpus}/templates.mjs +1 -1
- package/src/{embed.mjs → adapters/embed.mjs} +1 -11
- package/src/{graph-build.mjs → adapters/graph-build.mjs} +6 -6
- package/src/{memory → adapters/memory}/blocks.mjs +2 -2
- package/src/{memory → adapters/memory}/core.mjs +38 -94
- package/src/adapters/prose-tokens.mjs +98 -0
- package/src/{providers → adapters/providers}/bootstrap.mjs +2 -2
- package/src/{providers → adapters/providers}/fixture.mjs +3 -3
- package/src/{providers → adapters/providers}/graph-service.mjs +9 -4
- package/src/{source-slice.mjs → adapters/source-slice.mjs} +2 -2
- package/src/{source.mjs → adapters/source.mjs} +1 -1
- package/src/{toml-config.mjs → adapters/toml-config.mjs} +1 -1
- package/src/{answer-variants.json → domain/answer-variants.json} +1 -1
- package/src/domain/answer-variants.mjs +23 -0
- package/src/{ask-vocab.mjs → domain/ask-vocab.mjs} +37 -5
- package/src/{ask.mjs → domain/ask.mjs} +216 -52
- package/src/{codegraph.mjs → domain/codegraph.mjs} +31 -315
- package/src/{completions → domain/completions}/complete.mjs +16 -10
- package/src/{completions → domain/completions}/graph-adapter.mjs +10 -4
- package/src/{completions → domain/completions}/group.mjs +14 -6
- package/src/{completions → domain/completions}/infer.mjs +57 -39
- package/src/domain/completions/injected.mjs +21 -0
- package/src/{completions → domain/completions}/rank.mjs +15 -8
- package/src/{completions → domain/completions}/search.mjs +4 -2
- package/src/{grammar → domain/grammar}/ace.mjs +3 -3
- package/src/{grammar → domain/grammar}/assert.mjs +12 -8
- package/src/{grammar → domain/grammar}/lexicon-core.json +1 -1
- package/src/{grammar → domain/grammar}/lexicon.mjs +4 -6
- package/src/domain/hash.mjs +147 -0
- package/src/{interpret → domain/interpret}/fuzzy.mjs +42 -4
- package/src/domain/interpret/nlp-registry.mjs +20 -0
- package/src/{interpret → domain/interpret}/normalize.mjs +35 -5
- package/src/{interpret → domain/interpret}/pipeline.mjs +1 -5
- package/src/{interpret → domain/interpret}/strategies/ace.mjs +1 -1
- package/src/{interpret → domain/interpret}/strategies/constructions.mjs +30 -52
- package/src/{interpret → domain/interpret}/strategies/keywords.mjs +48 -26
- package/src/domain/memory/capability.mjs +235 -0
- package/src/domain/memory/fold.mjs +54 -0
- package/src/domain/memory/session-turns.mjs +7 -0
- package/src/{memory → domain/memory}/trust.mjs +48 -0
- package/src/{paraphrase.mjs → domain/paraphrase.mjs} +2 -2
- package/src/{prose.mjs → domain/prose.mjs} +1 -1
- package/src/domain/real-word-collisions.json +1 -0
- package/src/{router → domain/router}/call-validator.mjs +1 -1
- package/src/{router → domain/router}/drive.mjs +34 -25
- package/src/{router → domain/router}/goal-reasoner.mjs +1 -1
- package/src/{router → domain/router}/guardrail.mjs +1 -1
- package/src/{router → domain/router}/planner.mjs +1 -1
- package/src/{router → domain/router}/registry.mjs +5 -5
- package/src/{router → domain/router}/resolver.mjs +16 -13
- package/src/{router → domain/router}/results.mjs +1 -1
- package/src/{router → domain/router}/set-algebra.mjs +1 -1
- package/src/{router → domain/router}/taught.mjs +10 -9
- package/src/{syllogise.mjs → domain/syllogise.mjs} +21 -4
- package/src/domain/vector.mjs +12 -0
- package/src/services/chat-session.mjs +451 -0
- package/src/{chat.mjs → services/chat.mjs} +1209 -684
- package/src/{cli-args.mjs → services/cli-args.mjs} +2 -2
- package/src/services/completions.mjs +55 -0
- package/src/{extensions.mjs → services/extensions.mjs} +7 -7
- package/src/{finish.mjs → services/finish.mjs} +2 -2
- package/src/{memory → services}/fold.mjs +0 -0
- package/src/{import-file.mjs → services/import-file.mjs} +3 -3
- package/src/{index.mjs → services/index.mjs} +21 -12
- package/src/{init.mjs → services/init.mjs} +9 -9
- package/src/{ledger-viz.mjs → services/ledger-viz.mjs} +3 -3
- package/src/{plan-viz.mjs → services/plan-viz.mjs} +65 -26
- package/src/{sentences.mjs → services/sentences.mjs} +1 -1
- package/src/{sessions.mjs → services/sessions.mjs} +4 -5
- package/src/{telemetry.mjs → services/telemetry.mjs} +1 -1
- package/src/{server-http.mjs → surfaces/http/server-http.mjs} +11 -65
- package/src/{tui → surfaces/tui}/app.mjs +3 -3
- package/src/{memory-ask-browser-entry.mjs → surfaces/web/memory-ask-browser-entry.mjs} +5 -5
- package/src/{memory-ask-browser.bundle.js → surfaces/web/memory-ask-browser.bundle.js} +9465 -6366
- package/src/tools/catalog.mjs +29 -0
- package/src/{conformance.mjs → tools/conformance.mjs} +2 -2
- package/src/tools/definitions.mjs +288 -0
- package/src/tools/graph-load.mjs +20 -0
- package/src/tools/handlers/index.mjs +54 -0
- package/src/tools/handlers/kit.mjs +33 -0
- package/src/tools/handlers/tmct-architecture.mjs +7 -0
- package/src/tools/handlers/tmct-ask.mjs +14 -0
- package/src/tools/handlers/tmct-callees.mjs +6 -0
- package/src/tools/handlers/tmct-callers.mjs +6 -0
- package/src/tools/handlers/tmct-calls.mjs +6 -0
- package/src/tools/handlers/tmct-class-history.mjs +6 -0
- package/src/tools/handlers/tmct-cochanges.mjs +6 -0
- package/src/tools/handlers/tmct-context-more.mjs +9 -0
- package/src/tools/handlers/tmct-context.mjs +163 -0
- package/src/tools/handlers/tmct-describe.mjs +15 -0
- package/src/tools/handlers/tmct-exports.mjs +9 -0
- package/src/tools/handlers/tmct-file-history.mjs +6 -0
- package/src/tools/handlers/tmct-history.mjs +6 -0
- package/src/tools/handlers/tmct-impact.mjs +9 -0
- package/src/tools/handlers/tmct-members.mjs +16 -0
- package/src/tools/handlers/tmct-method-history.mjs +6 -0
- package/src/tools/handlers/tmct-search.mjs +22 -0
- package/src/tools/handlers/tmct-signature.mjs +6 -0
- package/src/tools/handlers/tmct-snippet.mjs +37 -0
- package/src/tools/handlers/tmct-subclasses.mjs +16 -0
- package/src/tools/handlers/tmct-tests-for.mjs +6 -0
- package/src/tools/handlers/tmct-untested.mjs +7 -0
- package/src/tools/memory-fallthrough.mjs +65 -0
- package/src/{schema-docs.mjs → tools/schema-docs.mjs} +1 -1
- package/src/tools/server.mjs +61 -0
- package/src/answer-variants.mjs +0 -39
- package/src/hash.mjs +0 -24
- package/src/server.mjs +0 -501
- /package/src/{corpus → adapters/corpus}/unknown-ingest.mjs +0 -0
- /package/src/{graph-merge.mjs → adapters/graph-merge.mjs} +0 -0
- /package/src/{memory → adapters/memory}/inspect.mjs +0 -0
- /package/src/{memory → adapters/memory}/shacl.mjs +0 -0
- /package/src/{prose-nlp.mjs → adapters/prose-nlp.mjs} +0 -0
- /package/src/{repository-interface.mjs → adapters/repository-interface.mjs} +0 -0
- /package/src/{uuid.mjs → adapters/uuid.mjs} +0 -0
- /package/src/{wink-model.mjs → adapters/wink-model.mjs} +0 -0
- /package/src/{completions → domain/completions}/prune.mjs +0 -0
- /package/src/{concept.mjs → domain/concept.mjs} +0 -0
- /package/src/{domain.mjs → domain/domain.mjs} +0 -0
- /package/src/{interpret → domain/interpret}/merge.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/grammar.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/noise-strip.mjs +0 -0
- /package/src/{memory → domain/memory}/bias.mjs +0 -0
- /package/src/{planning.mjs → domain/planning.mjs} +0 -0
- /package/src/{viz-theme.mjs → services/viz-theme.mjs} +0 -0
|
@@ -16,16 +16,19 @@
|
|
|
16
16
|
// (mgx:touchedByCommit / mgx:changeCoupledWith) — a different, simpler
|
|
17
17
|
// question than temporal.mjs's time-scrubbing Chronograph surface.
|
|
18
18
|
|
|
19
|
-
import { relationKind, impactClosure, normPath, HISTORY_CAP } from "./codegraph.mjs";
|
|
19
|
+
import { relationKind, impactClosure, moduleCountOf, normPath, HISTORY_CAP } from "./codegraph.mjs";
|
|
20
20
|
import {
|
|
21
|
+
RELATIONS,
|
|
21
22
|
VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
|
|
22
23
|
CONTEXT_PRONOUNS, META_MEANING_VERBS,
|
|
23
24
|
WHERE_MARKERS, MENTION_MARKERS,
|
|
24
25
|
RELATIVE_PRONOUNS, PLACEHOLDER_NOUNS, BOOLEAN_CONNECTIVES, QUALIFIERS,
|
|
26
|
+
PASSIVE_PARTICIPLE_TO_KIND, GENERIC_AGENT_WORDS,
|
|
25
27
|
AGGREGATE_TRIGGERS, LIST_TRIGGERS, SUPERLATIVE_EXTREMES, EDGE_NOUN_TO_METRIC, METRIC_IMPLIES_ENTITY, ANAPHORA_TRIGGERS,
|
|
26
28
|
MEMBERSHIP_KINDS, CASCADE_NOISE, CASCADE_SYNONYMS, HELP_TRIGGERS,
|
|
29
|
+
stripTrailingScopeFiller,
|
|
27
30
|
} from "./ask-vocab.mjs";
|
|
28
|
-
import { normalizeQuery, applyNegationFrames, applyPhrasingFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf } from "./interpret/normalize.mjs";
|
|
31
|
+
import { expandContractions, normalizeQuery, applyNegationFrames, applyPhrasingFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf } from "./interpret/normalize.mjs";
|
|
29
32
|
import { editDistance, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
30
33
|
import { parseAnchored } from "./interpret/strategies/grammar.mjs";
|
|
31
34
|
import { parseKeywordSpot, findPhrase } from "./interpret/strategies/keywords.mjs";
|
|
@@ -36,7 +39,7 @@ import { pickPhrase } from "./answer-variants.mjs";
|
|
|
36
39
|
|
|
37
40
|
// Normalization stays importable from its original site (tests + chat surface).
|
|
38
41
|
export { normalizeQuery, applyNegationFrames };
|
|
39
|
-
import {
|
|
42
|
+
import { defaultNlp } from "./interpret/nlp-registry.mjs";
|
|
40
43
|
|
|
41
44
|
// Per-graph, per-kind memo; a local copy of codegraph.mjs's private
|
|
42
45
|
// edgesOfKind. Named differently from codegraph.mjs's own cache: the inlined
|
|
@@ -120,12 +123,6 @@ const LEADING_RELATION_VERB_RE = new RegExp(
|
|
|
120
123
|
"i",
|
|
121
124
|
);
|
|
122
125
|
|
|
123
|
-
/** The default lemma/POS adapter: wink-nlp when Node has the optional deps
|
|
124
|
-
* installed, null otherwise. */
|
|
125
|
-
function defaultNlp() {
|
|
126
|
-
return typeof nlpAdapter === "function" ? nlpAdapter() : null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
126
|
/** Compile a free-text question into {shape, kind, entityType, modifier,
|
|
130
127
|
* object[, subject]}, or null if no strategy fits. When strategies parse and
|
|
131
128
|
* disagree, returns {ambiguousParse: true, candidates: [...]}. `opts.nlp`
|
|
@@ -216,6 +213,7 @@ function parseComposite(text, nlp) {
|
|
|
216
213
|
return parseExistence(w, lc)
|
|
217
214
|
|| parseQualifierCheck(w, lc)
|
|
218
215
|
|| parseNegation(text, nlp, 0)
|
|
216
|
+
|| parseNegatedAsk(w, lc)
|
|
219
217
|
|| parseForwardNegation(w, lc, nlp)
|
|
220
218
|
|| parseTemporal(w, lc, nlp, 0)
|
|
221
219
|
|| parseCommitFilter(w, lc)
|
|
@@ -600,10 +598,28 @@ function parseExistence(w, lc) {
|
|
|
600
598
|
// different (relationship) question; leave it for the parsers below.
|
|
601
599
|
}
|
|
602
600
|
|
|
603
|
-
/**
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
601
|
+
/** The "in <scope>" tail of a qualifier check ("is Task.title public in Task").
|
|
602
|
+
* A tail naming the whole index restricts nothing, so TRAILING_SCOPE_FILLER —
|
|
603
|
+
* the table the meta-whatis reader already strips with — takes "in this
|
|
604
|
+
* codebase" off first and the check runs unscoped, exactly as it reads.
|
|
605
|
+
* Returns {scope}, a miss node, or null when there's no "in" tail at all. */
|
|
606
|
+
function parseQualifierScope(qualifier, tailWords) {
|
|
607
|
+
if (!tailWords.length) return null;
|
|
608
|
+
const withQualifier = `${qualifier} ${tailWords.join(" ")}`;
|
|
609
|
+
const tail = stripTrailingScopeFiller(withQualifier).slice(qualifier.length).trim();
|
|
610
|
+
const rest = splitWords(tail);
|
|
611
|
+
const inIdx = rest.findIndex((x) => x.toLowerCase() === "in");
|
|
612
|
+
if (inIdx < 0) return null;
|
|
613
|
+
const scope = rest.slice(inIdx + 1).join(" ").trim();
|
|
614
|
+
return scope ? { scope } : { node: "miss", reason: `"in" needs a scope afterward` };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Qualifier-check: "is <term> [a/an] <qualifier> [<kind>] [in <scope>]?" — a
|
|
618
|
+
* single-entity yes/no property check ("is Task.title public"), the missing
|
|
619
|
+
* single-entity sibling of the qualifier post-filter over a set. Guarded off
|
|
620
|
+
* "is/are there …" (parseExistence's shape). An "in <scope>" tail rides the
|
|
621
|
+
* parse and eval checks the term is really inside that scope, so
|
|
622
|
+
* "is Task.title public in SomeOtherClass" can't answer off the term alone. */
|
|
607
623
|
function parseQualifierCheck(w, lc) {
|
|
608
624
|
if (lc[0] !== "is" && lc[0] !== "are") return null;
|
|
609
625
|
if (lc[1] === "there") return null; // parseExistence's own shape
|
|
@@ -613,13 +629,46 @@ function parseQualifierCheck(w, lc) {
|
|
|
613
629
|
if (QUALIFIERS[lc[i]]) { qualIdx = i; negated = lc[i - 1] === "not"; break; }
|
|
614
630
|
}
|
|
615
631
|
if (qualIdx < 0) return null; // no qualifier word at all → not this shape
|
|
632
|
+
// "is X tested by logger.mjs" asks about logger.mjs's own edges, not X's
|
|
633
|
+
// coverage property, so it declines here and reads as a passive instead. Two
|
|
634
|
+
// guards keep the decline narrow: the qualifier must have a relation
|
|
635
|
+
// counterpart ("is X public by Y" is not a sentence), and the agent must name
|
|
636
|
+
// something — "is X covered by tests" is still the plain coverage question.
|
|
637
|
+
const byIdx = lc.indexOf("by", qualIdx + 1);
|
|
638
|
+
if (byIdx > 0 && PASSIVE_PARTICIPLE_TO_KIND[lc[qualIdx]]) {
|
|
639
|
+
let agentIdx = byIdx + 1;
|
|
640
|
+
while (agentIdx < lc.length && (lc[agentIdx] === "the" || lc[agentIdx] === "a" || lc[agentIdx] === "an")) agentIdx += 1;
|
|
641
|
+
const agent = lc[agentIdx];
|
|
642
|
+
if (agent && !GENERIC_AGENT_WORDS.has(agent)) return null;
|
|
643
|
+
}
|
|
644
|
+
const tail = parseQualifierScope(lc[qualIdx], w.slice(qualIdx + 1));
|
|
645
|
+
if (tail?.node === "miss") return tail;
|
|
616
646
|
let termStart = 1;
|
|
617
647
|
if (lc[termStart] === "the") termStart += 1;
|
|
618
648
|
let termEnd = negated ? qualIdx - 1 : qualIdx;
|
|
619
649
|
if (termEnd > termStart && (lc[termEnd - 1] === "a" || lc[termEnd - 1] === "an")) termEnd -= 1;
|
|
620
650
|
const term = termEnd > termStart ? w.slice(termStart, termEnd).join(" ").trim() : "";
|
|
621
651
|
if (!term) return { node: "miss", reason: `"is/are <qualifier>" needs a named thing to check first` };
|
|
622
|
-
return { node: "qualCheck", term, qualifier: lc[qualIdx], negated };
|
|
652
|
+
return { node: "qualCheck", term, qualifier: lc[qualIdx], negated, scope: tail?.scope || null };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Negated polar: "does X not <verb> Y" — its positive twin's yes/no question
|
|
656
|
+
* asked for the edge's ABSENCE. The sentence re-parses through the anchored
|
|
657
|
+
* grammar with "not" dropped, and the `negated` flag rides the parse the same
|
|
658
|
+
* way parseQualifierCheck's does: eval folds it into the answer and render
|
|
659
|
+
* states whichever polarity holds.
|
|
660
|
+
*
|
|
661
|
+
* It lives here rather than in the grammar because the strategy pipeline reads
|
|
662
|
+
* the same sentence at positive polarity ("not" is no stopword) and the merge
|
|
663
|
+
* would call the two readings an ambiguity. Declining to a composite production
|
|
664
|
+
* is what keeps the sentence out of that merge. */
|
|
665
|
+
function parseNegatedAsk(w, lc) {
|
|
666
|
+
if (lc[0] !== "do" && lc[0] !== "does" && lc[0] !== "did") return null;
|
|
667
|
+
const notIdx = lc.indexOf("not", 1);
|
|
668
|
+
if (notIdx < 0) return null;
|
|
669
|
+
const positive = parseAnchored(w.filter((_, i) => i !== notIdx).join(" "));
|
|
670
|
+
if (!positive || positive.shape !== "ask") return null;
|
|
671
|
+
return { ...positive, negated: true };
|
|
623
672
|
}
|
|
624
673
|
|
|
625
674
|
/** Trailing filler an aggregate/list tail can carry ("how many classes are
|
|
@@ -1698,14 +1747,38 @@ function evalExists(graph, ast) {
|
|
|
1698
1747
|
return { compositeKind: "exists", entityType, term: null, scopeModule, scopeMatch, matches: pool };
|
|
1699
1748
|
}
|
|
1700
1749
|
|
|
1750
|
+
/** Is `ind` inside a resolved qualifier-check scope? Read off the containment
|
|
1751
|
+
* the graph already carries: a directory or module scope matches the term's
|
|
1752
|
+
* own defining module, and a container scope (a class over its attributes and
|
|
1753
|
+
* methods) matches a MEMBERSHIP_KINDS edge. Anything the graph doesn't state
|
|
1754
|
+
* is not contained. */
|
|
1755
|
+
function withinScope(graph, ind, owner) {
|
|
1756
|
+
const moduleId = moduleIdOf(graph, ind);
|
|
1757
|
+
if (owner.kind === "dir") return owner.mods.some((m) => m.id === ind.id || m.id === moduleId);
|
|
1758
|
+
return moduleId === owner.id || membershipOwnSet(graph, owner.id).some((o) => o.id === ind.id);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1701
1761
|
/** Qualifier-check eval: resolves the term (a context pronoun binds through
|
|
1702
1762
|
* contextId), then reads qualHolds(). `holds` means "the statement as asked
|
|
1703
|
-
* is true" (negation already folded in).
|
|
1763
|
+
* is true" (negation already folded in). An "in <scope>" tail is resolved and
|
|
1764
|
+
* checked first: the property is only reported for a term the scope really
|
|
1765
|
+
* contains, so the tail can never ride along unread. */
|
|
1704
1766
|
function evalQualCheck(graph, ast, opts) {
|
|
1705
|
-
const { term, qualifier, negated } = ast;
|
|
1767
|
+
const { term, qualifier, negated, scope } = ast;
|
|
1706
1768
|
const r = resolveTermOrContext(graph, term, opts.contextId);
|
|
1707
1769
|
if (r.unresolvedPronoun) return { compositeKind: "qualCheck", qualCheckMiss: "pronoun", term, matches: [] };
|
|
1708
1770
|
if (!r.match) return { compositeKind: "qualCheck", qualCheckMiss: "unresolved", term, matches: [] };
|
|
1771
|
+
if (scope) {
|
|
1772
|
+
const owner = resolveMembershipOwner(graph, scope, opts.contextId);
|
|
1773
|
+
if (owner.kind === "miss") return { compositeKind: "qualCheck", qualCheckMiss: "scope", term, scope, matches: [] };
|
|
1774
|
+
if (!withinScope(graph, r.match, owner)) {
|
|
1775
|
+
return {
|
|
1776
|
+
compositeKind: "qualCheck", qualCheckMiss: "outsideScope",
|
|
1777
|
+
term, scope, subjectLabel: r.match.label,
|
|
1778
|
+
scopeLabel: owner.kind === "dir" ? scope : owner.label, matches: [],
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1709
1782
|
const rawHolds = qualHolds(graph, r.match, QUALIFIERS[qualifier]);
|
|
1710
1783
|
const holds = negated ? !rawHolds : rawHolds;
|
|
1711
1784
|
return { compositeKind: "qualCheck", subject: r.match, qualifier, negated, holds, matches: [r.match] };
|
|
@@ -1768,7 +1841,7 @@ function describeFindHit(ind) {
|
|
|
1768
1841
|
return mod && mod !== "(unknown module)" ? `${label} in ${mod}` : label;
|
|
1769
1842
|
}
|
|
1770
1843
|
|
|
1771
|
-
function renderComposite(parsed, result) {
|
|
1844
|
+
function renderComposite(parsed, result, graph) {
|
|
1772
1845
|
if (result.compositeMiss) {
|
|
1773
1846
|
if (result.reason === "no-prev") {
|
|
1774
1847
|
return { content: `"those"/"them" needs a previous answer to refer to — ask a listing question first, then follow up.`, miss: true, ambiguous: false };
|
|
@@ -1803,6 +1876,12 @@ function renderComposite(parsed, result) {
|
|
|
1803
1876
|
if (result.qualCheckMiss === "unresolved") {
|
|
1804
1877
|
return { content: `couldn't find "${result.term}" in the index to check.`, miss: true, ambiguous: false };
|
|
1805
1878
|
}
|
|
1879
|
+
if (result.qualCheckMiss === "scope") {
|
|
1880
|
+
return { content: `couldn't find "${result.scope}" in the index to check inside.`, miss: true, ambiguous: false };
|
|
1881
|
+
}
|
|
1882
|
+
if (result.qualCheckMiss === "outsideScope") {
|
|
1883
|
+
return { content: `No — ${result.subjectLabel} isn't in ${result.scopeLabel}.`, miss: true, ambiguous: false };
|
|
1884
|
+
}
|
|
1806
1885
|
const label = result.subject.label;
|
|
1807
1886
|
const truePhrase = result.negated ? `not ${result.qualifier}` : result.qualifier;
|
|
1808
1887
|
const falsePhrase = result.negated ? result.qualifier : `not ${result.qualifier}`;
|
|
@@ -1831,7 +1910,7 @@ function renderComposite(parsed, result) {
|
|
|
1831
1910
|
// — inherited from <ancestor>: …"), never silently presented as the owner's own.
|
|
1832
1911
|
if (result.compositeKind === "membership") {
|
|
1833
1912
|
if (!result.matches.length) {
|
|
1834
|
-
return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}. ${touchesRephraseHint()}`, miss: true, ambiguous: false, matches: [] };
|
|
1913
|
+
return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}. ${touchesRephraseHint(graph)}`, miss: true, ambiguous: false, matches: [] };
|
|
1835
1914
|
}
|
|
1836
1915
|
if (result.inheritedNotOwn) {
|
|
1837
1916
|
const kindPlural = nounFor(result.entityType, 2);
|
|
@@ -1923,10 +2002,10 @@ function renderComposite(parsed, result) {
|
|
|
1923
2002
|
const setNoun = result.entityType ? nounFor(result.entityType, n || 2) : (n === 1 ? "entity" : "entities");
|
|
1924
2003
|
const wasWere = n === 1 ? "was" : "were";
|
|
1925
2004
|
if (!n) {
|
|
1926
|
-
return { content: `nothing in the index matches the inner set, so there is no change history to date. ${touchesRephraseHint()}`, miss: true, ambiguous: false, matches: [] };
|
|
2005
|
+
return { content: `nothing in the index matches the inner set, so there is no change history to date. ${touchesRephraseHint(graph)}`, miss: true, ambiguous: false, matches: [] };
|
|
1927
2006
|
}
|
|
1928
2007
|
if (!result.matches.length) {
|
|
1929
|
-
return { content: `no recorded commit touched the ${n} ${setNoun} in that set in this index. ${touchesRephraseHint()}`, miss: true, ambiguous: false, matches: [] };
|
|
2008
|
+
return { content: `no recorded commit touched the ${n} ${setNoun} in that set in this index. ${touchesRephraseHint(graph)}`, miss: true, ambiguous: false, matches: [] };
|
|
1930
2009
|
}
|
|
1931
2010
|
const newest = result.matches[0];
|
|
1932
2011
|
const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
|
|
@@ -1943,7 +2022,7 @@ function renderComposite(parsed, result) {
|
|
|
1943
2022
|
}
|
|
1944
2023
|
// set-producing
|
|
1945
2024
|
if (!result.matches.length) {
|
|
1946
|
-
return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}. ${touchesRephraseHint()}`, miss: true, ambiguous: false, matches: [] };
|
|
2025
|
+
return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}. ${touchesRephraseHint(graph)}`, miss: true, ambiguous: false, matches: [] };
|
|
1947
2026
|
}
|
|
1948
2027
|
return { content: `${compositeList(result.matches)}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1949
2028
|
}
|
|
@@ -1961,8 +2040,17 @@ export function rephraseHint() {
|
|
|
1961
2040
|
/** A short, honest nudge for the touches/history-family of correct-but-
|
|
1962
2041
|
* unhelpful misses, pointing at "who touched X" / "/describe X" shapes that
|
|
1963
2042
|
* produce a real answer elsewhere. Reused verbatim by every miss template
|
|
1964
|
-
* in this family.
|
|
1965
|
-
|
|
2043
|
+
* in this family.
|
|
2044
|
+
*
|
|
2045
|
+
* A graph with no modules holds no index to rephrase against, so the shapes
|
|
2046
|
+
* above would send the reader off to look up entities that cannot be there.
|
|
2047
|
+
* It gets a line that says what the store actually holds instead. A NULL
|
|
2048
|
+
* graph is UNKNOWN, not empty (see chat.mjs's noCodeGraph), so it keeps the
|
|
2049
|
+
* index-shaped advice. */
|
|
2050
|
+
export function touchesRephraseHint(graph = null) {
|
|
2051
|
+
if (graph && moduleCountOf(graph) === 0) {
|
|
2052
|
+
return "This store holds no code index, so it records no modules or commits to look through.";
|
|
2053
|
+
}
|
|
1966
2054
|
return 'Try "who touched <a module that actually has commits>" or "/describe <module>" to see what\'s in the index.';
|
|
1967
2055
|
}
|
|
1968
2056
|
|
|
@@ -2057,6 +2145,28 @@ function resolveObjectCore(graph, term, { expectedClass = null } = {}) {
|
|
|
2057
2145
|
}
|
|
2058
2146
|
if (extId && !expectedClass) return { match: { id: extId, label: t, class: null }, candidates: [], tier: 2, ambiguous: false };
|
|
2059
2147
|
|
|
2148
|
+
// "store.mjs and logger.mjs" names two operands, and every tier below reads a
|
|
2149
|
+
// term as ONE. Tier 3's slashStem deliberately resolves past trailing junk, so
|
|
2150
|
+
// the second operand is dropped and the first is answered about with full
|
|
2151
|
+
// confidence: "is model.mjs called by store.mjs and logger.mjs" returns a flat
|
|
2152
|
+
// Yes that is only ever about store.mjs. Nothing here reads coordination, so a
|
|
2153
|
+
// miss is the answer.
|
|
2154
|
+
//
|
|
2155
|
+
// Gated on both sides resolving to DIFFERENT individuals on their own, which is
|
|
2156
|
+
// what makes this a coordination rather than a name: a label that merely
|
|
2157
|
+
// contains the word was already claimed by the exact tier above, and a side that
|
|
2158
|
+
// resolves to nothing was never an operand. Neither part carries an " and " of
|
|
2159
|
+
// its own, so the recursion is one level deep.
|
|
2160
|
+
const andParts = t.split(/\s+and\s+/i).map((p) => p.trim()).filter(Boolean);
|
|
2161
|
+
if (andParts.length > 1) {
|
|
2162
|
+
const operandIds = new Set();
|
|
2163
|
+
for (const part of andParts) {
|
|
2164
|
+
const r = resolveObjectCore(graph, part, { expectedClass });
|
|
2165
|
+
if (r.match && !r.ambiguous) operandIds.add(r.match.id);
|
|
2166
|
+
}
|
|
2167
|
+
if (operandIds.size > 1) return { match: null, candidates: [], tier: null, ambiguous: false };
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2060
2170
|
// Tier 3, two regimes: a dotted term with no slash ("res.json") is
|
|
2061
2171
|
// symbol-shaped, so it matches only symbol labels or a module by exact
|
|
2062
2172
|
// basename equality (never a phantom substring-of-path match); undotted/
|
|
@@ -2418,7 +2528,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
2418
2528
|
if (parsed.ambiguousParse) {
|
|
2419
2529
|
const branches = parsed.candidates.map((c) => {
|
|
2420
2530
|
const branchResult = traverse(graph, c, { contextId, prev });
|
|
2421
|
-
return { parsed: c, result: branchResult, rendered: render(c, branchResult) };
|
|
2531
|
+
return { parsed: c, result: branchResult, rendered: render(c, branchResult, graph) };
|
|
2422
2532
|
});
|
|
2423
2533
|
return { matches: [], objMatch: null, candidates: parsed.candidates, traversal: null, ambiguous: true, branches };
|
|
2424
2534
|
}
|
|
@@ -2502,14 +2612,16 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
2502
2612
|
// touches edges are stored commit -> entity; orient by where the commit
|
|
2503
2613
|
// actually is when it's named on the object side.
|
|
2504
2614
|
let [from, to] = [subj.match, obj.match];
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2615
|
+
if (kind === "touches" && to.class === "Commit" && from.class !== "Commit") [from, to] = [to, from];
|
|
2616
|
+
// Both operands are named, so the yes/no wants the symbol-grain sibling
|
|
2617
|
+
// unconditionally: a callsSymbol-only edge is still a real call, and reading
|
|
2618
|
+
// the coarse kind alone answers No to a question the graph can answer Yes.
|
|
2619
|
+
const sibling = SYMBOL_GRAIN_SIBLING[kind];
|
|
2620
|
+
const kinds = [...new Set(sibling ? [...kindsFor(kind), sibling] : kindsFor(kind))];
|
|
2510
2621
|
const edges = kinds.flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.subject === from.id && e.object === to.id);
|
|
2511
2622
|
return {
|
|
2512
|
-
matches: edges, answer: edges.length
|
|
2623
|
+
matches: edges, answer: parsed.negated ? edges.length === 0 : edges.length > 0,
|
|
2624
|
+
objMatch: obj.match, subjMatch: subj.match,
|
|
2513
2625
|
candidates: [], traversal: `${kinds.join("+")} edge from ${from.label} to ${to.label}`, ambiguous: false,
|
|
2514
2626
|
};
|
|
2515
2627
|
}
|
|
@@ -2569,7 +2681,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
2569
2681
|
const pool = uniqueById([objMatch, ...(candidates || [])]).slice(0, OVERFLOW_CAP);
|
|
2570
2682
|
const branches = pool.map((c) => {
|
|
2571
2683
|
const branchResult = traverse(graph, parsed, { contextId, prev, pinnedObjMatch: c });
|
|
2572
|
-
return { candidate: c, result: branchResult, rendered: render(parsed, branchResult) };
|
|
2684
|
+
return { candidate: c, result: branchResult, rendered: render(parsed, branchResult, graph) };
|
|
2573
2685
|
});
|
|
2574
2686
|
return { matches: [], objMatch, candidates, traversal: null, ambiguous: true, branches };
|
|
2575
2687
|
}
|
|
@@ -2876,11 +2988,46 @@ function describeParse(p) {
|
|
|
2876
2988
|
return `${ent}${p.kind} "${obj}"`;
|
|
2877
2989
|
}
|
|
2878
2990
|
|
|
2991
|
+
/** The base verb form for a relation ("imports" -> "import"), for the frames
|
|
2992
|
+
* that need an infinitive. Unknown kinds fall back to the token itself rather
|
|
2993
|
+
* than being coerced into a guessed base form. */
|
|
2994
|
+
function bareVerbFor(kind) {
|
|
2995
|
+
return RELATIONS[kind]?.bare || kind;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
/** English gloss of a SET-COMPLEMENT AST — the shape parseNegation compiles
|
|
2999
|
+
* "which X do not <verb> Y" into (allOfClass DIFFERENCE the positive set).
|
|
3000
|
+
* Restates the question in the same grammar the positive canonical uses
|
|
3001
|
+
* ('modules that imports "store.mjs"'), so the negated and positive readings
|
|
3002
|
+
* of one question are told apart by reading the line, which is the whole point
|
|
3003
|
+
* of printing it. Returns null for any other boolean shape rather than
|
|
3004
|
+
* glossing it wrongly. */
|
|
3005
|
+
function complementGloss(parsed) {
|
|
3006
|
+
// Exactly two atoms: a third would be silently dropped from the gloss,
|
|
3007
|
+
// restating a narrower question than the one that was asked.
|
|
3008
|
+
if (!Array.isArray(parsed.atoms) || parsed.atoms.length !== 2) return null;
|
|
3009
|
+
const [seed, diff] = parsed.atoms;
|
|
3010
|
+
if (seed.op !== "seed" || seed.ast?.node !== "allOfClass" || diff.op !== "difference") return null;
|
|
3011
|
+
const noun = nounFor(parsed.entityType, 2);
|
|
3012
|
+
if (diff.kind === "qual") return `${noun} that are not ${diff.filters.join(" and not ")}`;
|
|
3013
|
+
if (diff.kind !== "set" || !diff.ast) return null;
|
|
3014
|
+
if (diff.ast.node === "existsEdge") return `${noun} that do not ${bareVerbFor(diff.ast.kind)} anything`;
|
|
3015
|
+
// The positive leg parseNegation differenced off the class: a single clause
|
|
3016
|
+
// ("which modules do not import store.mjs") is the shape this gloss reads.
|
|
3017
|
+
// Anything deeper is a nested composite and gets no gloss rather than a
|
|
3018
|
+
// confidently wrong one.
|
|
3019
|
+
const leaf = diff.ast.node === "clause" ? diff.ast.clause : diff.ast;
|
|
3020
|
+
if (!leaf || leaf.node || !leaf.kind) return null;
|
|
3021
|
+
const obj = leaf.object ?? leaf.subject;
|
|
3022
|
+
if (obj == null) return null;
|
|
3023
|
+
return `${noun} that do not ${bareVerbFor(leaf.kind)} ${JSON.stringify(String(obj))}`;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2879
3026
|
/** The canonical restatement of what a query was understood to mean: an
|
|
2880
3027
|
* English gloss (`english`) and a compact `shape(kind, args...)` notation
|
|
2881
3028
|
* (`machine`), both read off `parsed`'s already-compiled fields. A
|
|
2882
|
-
* compositional AST
|
|
2883
|
-
*
|
|
3029
|
+
* compositional AST with no gloss of its own falls back to naming its node
|
|
3030
|
+
* type — coarse, but never a wrong restatement. */
|
|
2884
3031
|
function canonicalOf(parsed) {
|
|
2885
3032
|
if (!parsed) return null;
|
|
2886
3033
|
if (parsed.ambiguousParse) {
|
|
@@ -2890,11 +3037,16 @@ function canonicalOf(parsed) {
|
|
|
2890
3037
|
};
|
|
2891
3038
|
}
|
|
2892
3039
|
if (parsed.node) {
|
|
2893
|
-
|
|
3040
|
+
const gloss = parsed.node === "boolean" ? complementGloss(parsed) : null;
|
|
3041
|
+
return {
|
|
3042
|
+
english: gloss || `a compositional query (${parsed.node})`,
|
|
3043
|
+
machine: `composite(${parsed.node})`,
|
|
3044
|
+
};
|
|
2894
3045
|
}
|
|
2895
3046
|
const q = (s) => JSON.stringify(String(s ?? ""));
|
|
2896
3047
|
const args = [];
|
|
2897
3048
|
if (parsed.kind) args.push(parsed.kind);
|
|
3049
|
+
if (parsed.negated) args.push("negated");
|
|
2898
3050
|
if (parsed.entityType) args.push(`entityType=${parsed.entityType}`);
|
|
2899
3051
|
if (parsed.modifier && parsed.modifier !== "direct") args.push(`modifier=${parsed.modifier}`);
|
|
2900
3052
|
if (parsed.subject != null) args.push(`subject=${q(parsed.subject)}`);
|
|
@@ -2902,7 +3054,7 @@ function canonicalOf(parsed) {
|
|
|
2902
3054
|
const machine = `${parsed.shape}(${args.join(", ")})`;
|
|
2903
3055
|
let english;
|
|
2904
3056
|
if (parsed.shape === "ask") {
|
|
2905
|
-
english = `does "${parsed.subject}" ${verbFor(parsed.kind)} "${parsed.object}"?`;
|
|
3057
|
+
english = `does "${parsed.subject}" ${parsed.negated ? "not " : ""}${verbFor(parsed.kind)} "${parsed.object}"?`;
|
|
2906
3058
|
} else if (parsed.shape === "meta") {
|
|
2907
3059
|
english = `what does "${parsed.object}" mean, in this graph's own vocabulary?`;
|
|
2908
3060
|
} else if (parsed.shape === "mentions") {
|
|
@@ -2921,19 +3073,19 @@ function canonicalOf(parsed) {
|
|
|
2921
3073
|
/** Render a compiled query result into {content, miss, ambiguous, matches?,
|
|
2922
3074
|
* candidates?}. A tier-5 fuzzy object resolution is announced, not silent:
|
|
2923
3075
|
* prefixed "assuming you meant <label>:" so the correction is on the record. */
|
|
2924
|
-
export function render(parsed, result) {
|
|
2925
|
-
const r = renderCore(parsed, result);
|
|
3076
|
+
export function render(parsed, result, graph = null) {
|
|
3077
|
+
const r = renderCore(parsed, result, graph);
|
|
2926
3078
|
if (result && result.matchedVia === "fuzzy" && result.objMatch && !r.ambiguous) {
|
|
2927
3079
|
r.content = `assuming you meant ${result.objMatch.label}: ${r.content}`;
|
|
2928
3080
|
}
|
|
2929
3081
|
return r;
|
|
2930
3082
|
}
|
|
2931
3083
|
|
|
2932
|
-
function renderCore(parsed, result) {
|
|
3084
|
+
function renderCore(parsed, result, graph) {
|
|
2933
3085
|
if (!parsed) {
|
|
2934
3086
|
return { content: `couldn't parse this as a graph question. Try: ${rephraseHint()}`, miss: true, ambiguous: false };
|
|
2935
3087
|
}
|
|
2936
|
-
if (parsed.node) return renderComposite(parsed, result);
|
|
3088
|
+
if (parsed.node) return renderComposite(parsed, result, graph);
|
|
2937
3089
|
if (parsed.ambiguousParse) {
|
|
2938
3090
|
// Each branch was actually resolved in traverse() above (never guessed at) —
|
|
2939
3091
|
// show every reading's real, effective answer, not just its one-line label, so
|
|
@@ -3060,7 +3212,7 @@ function renderCore(parsed, result) {
|
|
|
3060
3212
|
const what = /^(?:commit[:\s])?[0-9a-f]{7,40}$/i.test(objText) ? "commit"
|
|
3061
3213
|
: (!objText.includes("/") && /^[\w$]+(\.[\w$]+)+$/.test(objText) ? "symbol" : fallback);
|
|
3062
3214
|
return {
|
|
3063
|
-
content: `no ${what} matching "${parsed.object}" found in the index. ${touchesRephraseHint()}`,
|
|
3215
|
+
content: `no ${what} matching "${parsed.object}" found in the index. ${touchesRephraseHint(graph)}`,
|
|
3064
3216
|
miss: true, ambiguous: false, candidates: [],
|
|
3065
3217
|
};
|
|
3066
3218
|
}
|
|
@@ -3106,7 +3258,7 @@ function renderCore(parsed, result) {
|
|
|
3106
3258
|
if (result.whenShape) {
|
|
3107
3259
|
const subject = result.objMatch.label;
|
|
3108
3260
|
if (!result.matches.length) {
|
|
3109
|
-
return { content: `no recorded commit touches ${subject} in this index. ${touchesRephraseHint()}`, miss: true, ambiguous: false };
|
|
3261
|
+
return { content: `no recorded commit touches ${subject} in this index. ${touchesRephraseHint(graph)}`, miss: true, ambiguous: false };
|
|
3110
3262
|
}
|
|
3111
3263
|
const newest = result.matches[0];
|
|
3112
3264
|
const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
|
|
@@ -3133,7 +3285,7 @@ function renderCore(parsed, result) {
|
|
|
3133
3285
|
if (result.whoLastShape) {
|
|
3134
3286
|
const subject = result.objMatch.label;
|
|
3135
3287
|
if (!result.matches.length) {
|
|
3136
|
-
return { content: `no recorded commit touches ${subject} in this index. ${touchesRephraseHint()}`, miss: true, ambiguous: false };
|
|
3288
|
+
return { content: `no recorded commit touches ${subject} in this index. ${touchesRephraseHint(graph)}`, miss: true, ambiguous: false };
|
|
3137
3289
|
}
|
|
3138
3290
|
const newest = result.matches[0];
|
|
3139
3291
|
const author = (newest.attributes || []).find((a) => a.key === "author")?.value;
|
|
@@ -3174,9 +3326,17 @@ function renderCore(parsed, result) {
|
|
|
3174
3326
|
if (!result.objMatch || !result.subjMatch) {
|
|
3175
3327
|
return { content: `couldn't resolve one of the terms in this question.`, miss: true, ambiguous: false };
|
|
3176
3328
|
}
|
|
3329
|
+
// The edge sentence and the no-edge sentence each state what the graph
|
|
3330
|
+
// holds; the question's polarity only decides which one carries the Yes.
|
|
3331
|
+
const edgeFound = `${result.traversal}.`;
|
|
3332
|
+
const noEdge = `no ${parsed.kind} edge found from ${result.subjMatch.label} to ${result.objMatch.label}.`;
|
|
3333
|
+
const holds = parsed.negated ? noEdge : edgeFound;
|
|
3334
|
+
const fails = parsed.negated ? edgeFound : noEdge;
|
|
3177
3335
|
return {
|
|
3178
|
-
content: result.answer ?
|
|
3179
|
-
miss
|
|
3336
|
+
content: `${result.answer ? "Yes" : "No"} — ${result.answer ? holds : fails}`,
|
|
3337
|
+
// A miss is the absence of a citation, which the polarity never changes:
|
|
3338
|
+
// "no edge found" cites nothing whether it arrives as a Yes or a No.
|
|
3339
|
+
miss: !result.matches.length, ambiguous: false,
|
|
3180
3340
|
};
|
|
3181
3341
|
}
|
|
3182
3342
|
if (!result.matches.length) {
|
|
@@ -3201,7 +3361,7 @@ function renderCore(parsed, result) {
|
|
|
3201
3361
|
}
|
|
3202
3362
|
const entityWord = nounFor(parsed.entityType || "Module", 2);
|
|
3203
3363
|
return {
|
|
3204
|
-
content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}. ${touchesRephraseHint()}`,
|
|
3364
|
+
content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}. ${touchesRephraseHint(graph)}`,
|
|
3205
3365
|
miss: true, ambiguous: false,
|
|
3206
3366
|
};
|
|
3207
3367
|
}
|
|
@@ -3399,7 +3559,7 @@ export function relaxParse(graph, query, { nlp = undefined, contextId = null, pr
|
|
|
3399
3559
|
if (p.object != null && !hasRealTerm(p.object)) return null;
|
|
3400
3560
|
if (p.shape === "ask" && p.subject != null && !hasRealTerm(p.subject)) return null;
|
|
3401
3561
|
}
|
|
3402
|
-
const rendered = render(p, traverse(graph, p, { contextId, prev }));
|
|
3562
|
+
const rendered = render(p, traverse(graph, p, { contextId, prev }), graph);
|
|
3403
3563
|
return rendered.miss ? null : { parsed: p, text };
|
|
3404
3564
|
};
|
|
3405
3565
|
const done = (hit) => ({ parsed: hit.parsed, from, to: hit.text, dropped: [...dropped], steps });
|
|
@@ -3604,14 +3764,14 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
3604
3764
|
if (r) { parsed = r.parsed; relaxed = { from: r.from, to: r.to, dropped: r.dropped, steps: r.steps }; }
|
|
3605
3765
|
}
|
|
3606
3766
|
let result = traverse(graph, parsed, { contextId, prev });
|
|
3607
|
-
let rendered = render(parsed, result);
|
|
3767
|
+
let rendered = render(parsed, result, graph);
|
|
3608
3768
|
// Dynamic memory-graph class count/list fallback, only once everything
|
|
3609
3769
|
// above already produced an honest miss.
|
|
3610
3770
|
if (rendered.miss && !rendered.ambiguous) {
|
|
3611
3771
|
const dyn = dynamicClassQuery(graph, query);
|
|
3612
3772
|
if (dyn) {
|
|
3613
3773
|
const dynResult = traverse(graph, dyn, { contextId, prev });
|
|
3614
|
-
const dynRendered = render(dyn, dynResult);
|
|
3774
|
+
const dynRendered = render(dyn, dynResult, graph);
|
|
3615
3775
|
if (!dynRendered.miss) { parsed = dyn; result = dynResult; rendered = dynRendered; relaxed = null; }
|
|
3616
3776
|
}
|
|
3617
3777
|
}
|
|
@@ -3620,13 +3780,17 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
3620
3780
|
// `!envelope.parsed`, so populating it here would steal turns from lanes
|
|
3621
3781
|
// like moduleOrientLane ("what is X for"). Fires only once nothing else parsed.
|
|
3622
3782
|
if (parsed === null && rendered.miss && !rendered.ambiguous) {
|
|
3623
|
-
|
|
3783
|
+
// The contraction is written out first, or "what's X" reads as a different
|
|
3784
|
+
// question from "what is X" on the apostrophe alone. Only the contraction
|
|
3785
|
+
// pass, never normalizeQuery's fuller one — the filler strip would let
|
|
3786
|
+
// "please describe X"-shaped turns in through a lead they didn't type.
|
|
3787
|
+
const bareM = expandContractions(String(query || "")).trim().match(BARE_META_WHATIS_RE);
|
|
3624
3788
|
const bareTerm = bareM?.[1]?.trim();
|
|
3625
3789
|
if (bareTerm && !/\s+(?:for|about)$/i.test(bareTerm)) {
|
|
3626
3790
|
const bareParsed = parseQuery(`what is a ${bareTerm}`, { nlp });
|
|
3627
3791
|
if (bareParsed?.shape === "meta") {
|
|
3628
3792
|
result = traverse(graph, bareParsed, { contextId, prev });
|
|
3629
|
-
rendered = render(bareParsed, result);
|
|
3793
|
+
rendered = render(bareParsed, result, graph);
|
|
3630
3794
|
}
|
|
3631
3795
|
}
|
|
3632
3796
|
}
|
|
@@ -3645,7 +3809,7 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
3645
3809
|
const forParsed = parseQuery(`what is a ${forTerm}`, { nlp });
|
|
3646
3810
|
if (forParsed?.shape === "meta") {
|
|
3647
3811
|
const forResult = traverse(graph, forParsed, { contextId, prev });
|
|
3648
|
-
const forRendered = render(forParsed, forResult);
|
|
3812
|
+
const forRendered = render(forParsed, forResult, graph);
|
|
3649
3813
|
if (!forRendered.miss && !forRendered.ambiguous) {
|
|
3650
3814
|
result = forResult;
|
|
3651
3815
|
rendered = forRendered;
|
|
@@ -3669,7 +3833,7 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
3669
3833
|
const answered = directFull.alternates
|
|
3670
3834
|
.map((a) => {
|
|
3671
3835
|
const altResult = traverse(graph, a.parsed, { contextId, prev });
|
|
3672
|
-
const altRendered = render(a.parsed, altResult);
|
|
3836
|
+
const altRendered = render(a.parsed, altResult, graph);
|
|
3673
3837
|
return altRendered.miss ? null : { a, text: altRendered.content };
|
|
3674
3838
|
})
|
|
3675
3839
|
.filter(Boolean);
|