@polycode-projects/the-mechanical-code-talker 0.8.2 → 0.9.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/README.md +14 -0
- package/ROADMAP.md +90 -57
- package/corpus/seon/concepts.jsonl +42 -0
- package/package.json +2 -1
- package/src/ask.mjs +460 -13
- package/src/chat.mjs +516 -31
- package/src/grammar/lexicon-core.json +7 -0
- package/src/interpret/normalize.mjs +139 -3
package/src/ask.mjs
CHANGED
|
@@ -130,8 +130,12 @@ function nounFor(entityType, n) {
|
|
|
130
130
|
// name is a bare noun/verb stem ("X cochange Y" is wrong; "X cochanges Y" is right) —
|
|
131
131
|
// so the reverse-shape zero-hit template below reads off this table instead of
|
|
132
132
|
// unconditionally appending "s" (which used to double-pluralize every other kind:
|
|
133
|
-
// "callss", "importss", "touchess").
|
|
134
|
-
|
|
133
|
+
// "callss", "importss", "touchess"). "reexports" -> "export" (Bug B3, HANDOVER
|
|
134
|
+
// follow-up #2): the raw internal kind identifier "reexports" leaked straight into
|
|
135
|
+
// the forward-miss prose ("X has no reexports edges in the index") — the human
|
|
136
|
+
// word for this relation is "export" ("X has no export edges in the index"),
|
|
137
|
+
// matching every other kind's already-natural phrasing.
|
|
138
|
+
const REVERSE_MISS_VERB = { cochange: "cochanges", reexports: "export" };
|
|
135
139
|
function verbFor(kind) {
|
|
136
140
|
return REVERSE_MISS_VERB[kind] || kind;
|
|
137
141
|
}
|
|
@@ -279,6 +283,7 @@ function parseComposite(text, nlp) {
|
|
|
279
283
|
|| parseAnaphora(w, lc, nlp)
|
|
280
284
|
|| parseAggregate(w, lc, nlp)
|
|
281
285
|
|| parseSuperlative(w, lc, nlp)
|
|
286
|
+
|| parseFind(w, lc, nlp, 0)
|
|
282
287
|
|| parseList(w, lc, nlp, 0)
|
|
283
288
|
|| parseNested(w, lc, nlp, 0)
|
|
284
289
|
|| parseRelationalOrQualified(w, lc, nlp, 0);
|
|
@@ -673,6 +678,62 @@ function parseSuperlative(w, lc, nlp) {
|
|
|
673
678
|
return { node: "superlative", entityType, metric, metricNoun, extreme: ext };
|
|
674
679
|
}
|
|
675
680
|
|
|
681
|
+
// PREDICATE-FIND (Workstream 2 — new product feature): "find [me/us] [the/a] <term>
|
|
682
|
+
// <entityType>" (trailing-type — "find me the payment class") or "find [me/us]
|
|
683
|
+
// [the/a] <entityType> <linker> <term>" (leading-type-with-linker — "find the class
|
|
684
|
+
// named Foo"). A TYPE FILTER ∧ FUZZY PROPERTY-SURFACE MATCH, not a literal name
|
|
685
|
+
// lookup (contrast parseList's plain class enumeration) — reuses the same closed
|
|
686
|
+
// compositional grammar (evalSet's new "find" case, renderComposite's new branch)
|
|
687
|
+
// so it composes for free with qualifiers/booleans later (§6 generalization below).
|
|
688
|
+
const FIND_LINKERS = new Set(["called", "named", "about", "like", "containing", "matching", "with"]);
|
|
689
|
+
|
|
690
|
+
/** PREDICATE-FIND: see the file comment above. Triggered ONLY by a leading "find"
|
|
691
|
+
* (parseNegation, earlier in parseComposite's chain, already claims "find" as an
|
|
692
|
+
* optional lead before an EXPLICIT set-negation marker — "find modules that don't
|
|
693
|
+
* import X" reaches that production first and never reaches here). Reuses
|
|
694
|
+
* LIST_SKIP and entityNoun/ENTITY_TO_TYPE exactly as parseList does. A clear
|
|
695
|
+
* imperative "find <one unknown plain word>" (mirroring parseList's own single-
|
|
696
|
+
* trailing-word discipline) is an honest miss naming LISTABLE_KINDS — a
|
|
697
|
+
* PARSE-TIME miss, structurally distinct from evalSet("find")'s zero-hit SEARCH
|
|
698
|
+
* miss; anything less certain — a longer uncertain remainder, or ANY relative-
|
|
699
|
+
* clause marker present anywhere (that/which/who) — defers (null) to the existing
|
|
700
|
+
* parser/cascade, so "find the file that imports store" keeps parsing via the
|
|
701
|
+
* established parseNested/parseRelationalOrQualified relative-clause path (the
|
|
702
|
+
* §6 generalization below is the ONE place a term-bearing find-with-predicate
|
|
703
|
+
* shape is recognized, and it is a structurally separate production). */
|
|
704
|
+
function parseFind(w, lc, nlp, depth) {
|
|
705
|
+
if (lc[0] !== "find") return null;
|
|
706
|
+
let i = 1;
|
|
707
|
+
while (i < lc.length && LIST_SKIP.has(lc[i])) i += 1;
|
|
708
|
+
if (i >= lc.length) return null;
|
|
709
|
+
|
|
710
|
+
// leading-type-with-linker: "find [me] [the] <entityType> <linker> <term…>"
|
|
711
|
+
const leadNoun = entityNoun(lc[i]);
|
|
712
|
+
if (leadNoun && !leadNoun.placeholder && leadNoun.entityType !== "Change"
|
|
713
|
+
&& i + 1 < lc.length && FIND_LINKERS.has(lc[i + 1])) {
|
|
714
|
+
const term = w.slice(i + 2).join(" ").trim();
|
|
715
|
+
if (term) return { node: "find", entityType: leadNoun.entityType, term };
|
|
716
|
+
// a linker with nothing after it is too uncertain to claim — fall through.
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// trailing-type: "find [me] [the] <term…> <entityType>"
|
|
720
|
+
const lastNoun = entityNoun(lc[lc.length - 1]);
|
|
721
|
+
if (lastNoun && !lastNoun.placeholder && lastNoun.entityType !== "Change") {
|
|
722
|
+
const term = w.slice(i, lc.length - 1).join(" ").trim();
|
|
723
|
+
if (term) return { node: "find", entityType: lastNoun.entityType, term };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// A relative-clause marker anywhere means a DIFFERENT production owns this text
|
|
727
|
+
// (either the plain relative-clause path, or the §6 find-with-predicate
|
|
728
|
+
// generalization inside parseRelationalOrQualified) — never claimed here.
|
|
729
|
+
if (lc.some((t) => RELATIVE_PRONOUNS.includes(t))) return null;
|
|
730
|
+
// A clear imperative single unknown trailing word (mirrors parseList's own rule).
|
|
731
|
+
if (i === lc.length - 1 && /^[a-z]+$/.test(lc[i]) && !VERB_TO_KIND[lc[i]] && !PLACEHOLDER_NOUNS.includes(lc[i])) {
|
|
732
|
+
return { node: "miss", reason: `"${lc[i]}" isn't a listable kind — try ${LISTABLE_KINDS}` };
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
676
737
|
/** RELATIONAL / BOOLEAN / QUALIFIER (subject-first): "[which] [<qualifier>…] <entity>
|
|
677
738
|
* [that] <predicate>", where <predicate> is one or more relation clauses joined by
|
|
678
739
|
* and/or/but-not over the SAME subject, a "<of|in> <term>" membership, or empty (a
|
|
@@ -681,7 +742,78 @@ function parseSuperlative(w, lc, nlp) {
|
|
|
681
742
|
* reverse query ("which functions call helper") and the bare-template ambiguous case
|
|
682
743
|
* ("which classes extends Base and couples to logging", no marker) both fall through
|
|
683
744
|
* to the existing strategies untouched. Returns an AST node, a miss, or null. */
|
|
745
|
+
/** §6 generalization (predicate-find, Workstream 2 follow-up) — detect the HEAD of
|
|
746
|
+
* "find [me/us] [the/a] <term…> <entityType> that|which|who <predicate>": mirrors
|
|
747
|
+
* parseFind's own trailing-type recognition (LIST_SKIP, entityNoun), but requires
|
|
748
|
+
* a relative-clause marker directly after the entity noun and a NON-EMPTY term
|
|
749
|
+
* before it. An EMPTY term ("find classes that…") is deliberately NOT this shape
|
|
750
|
+
* — it already reaches parseRelationalOrQualified's normal head-parsing below
|
|
751
|
+
* once "find" is skipped as a FRAME_WORD, with no find-seed needed. Returns
|
|
752
|
+
* {entityType, term, relIdx} (relIdx = the relative pronoun's token index) or
|
|
753
|
+
* null — null on anything less than an exact match (never a guess). */
|
|
754
|
+
function parseFindPredicateHead(w, lc) {
|
|
755
|
+
if (lc[0] !== "find") return null;
|
|
756
|
+
let i = 1;
|
|
757
|
+
while (i < lc.length && LIST_SKIP.has(lc[i])) i += 1;
|
|
758
|
+
let r = -1;
|
|
759
|
+
for (let k = i + 1; k < lc.length; k += 1) { if (RELATIVE_PRONOUNS.includes(lc[k])) { r = k; break; } }
|
|
760
|
+
if (r < 0) return null;
|
|
761
|
+
const noun = entityNoun(lc[r - 1]);
|
|
762
|
+
if (!noun || noun.placeholder || noun.entityType === "Change") return null;
|
|
763
|
+
const term = w.slice(i, r - 1).join(" ").trim();
|
|
764
|
+
if (!term) return null;
|
|
765
|
+
return { entityType: noun.entityType, term, relIdx: r };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Build boolean/qualifier atoms for a predicate whose FIRST (seed) atom the
|
|
769
|
+
* caller already determined externally (the §6 generalization's find-seed,
|
|
770
|
+
* above) — `predLc`/`predWords` are the tokens AFTER the leading relative
|
|
771
|
+
* pronoun has already been consumed. Every atom's op defaults to
|
|
772
|
+
* "intersection" (a relative clause always RESTRICTS the seed) except where an
|
|
773
|
+
* explicit and/or/but-not connective says otherwise. Mirrors
|
|
774
|
+
* parseRelationalOrQualified's own branch-classification (qualifier-only /
|
|
775
|
+
* membership / verb-phrase clause) in miniature, duplicated rather than
|
|
776
|
+
* shared, so neither path risks regressing the other. */
|
|
777
|
+
function buildPredicateAtoms(entityType, subjPrefix, predLc, predWords, nlp, depth) {
|
|
778
|
+
const { branches, ops } = splitBoolean(predLc, predWords);
|
|
779
|
+
let prevVerb = null;
|
|
780
|
+
const atoms = [];
|
|
781
|
+
for (let b = 0; b < branches.length; b += 1) {
|
|
782
|
+
const bw = branches[b];
|
|
783
|
+
const blc = bw.map((x) => x.toLowerCase());
|
|
784
|
+
const op = b === 0 ? "intersection" : ops[b - 1];
|
|
785
|
+
if (bw.length && blc.every((x) => QUALIFIERS[x])) { atoms.push({ op, kind: "qual", filters: blc }); continue; }
|
|
786
|
+
if (blc[0] === "of" || blc[0] === "in") {
|
|
787
|
+
atoms.push({ op, kind: "set", ast: { node: "membership", entityType, term: bw.slice(1).join(" ") } });
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
let phrase = bw;
|
|
791
|
+
const vh = findPhrase(blc, VERB_TO_KIND);
|
|
792
|
+
if (vh) prevVerb = bw.slice(vh.start, vh.end);
|
|
793
|
+
else if (prevVerb) phrase = [...prevVerb, ...bw];
|
|
794
|
+
const ast = parseBranchAst(`${subjPrefix} ${phrase.join(" ")}`, nlp, depth);
|
|
795
|
+
if (!ast || ast.node === "miss") return { miss: (ast && ast.reason) || "a clause in the combination didn't parse" };
|
|
796
|
+
atoms.push({ op, kind: "set", ast });
|
|
797
|
+
}
|
|
798
|
+
return { atoms };
|
|
799
|
+
}
|
|
800
|
+
|
|
684
801
|
function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
802
|
+
// §6 generalization (predicate-find): seeds the SAME boolean/qualifier fold
|
|
803
|
+
// below with a {node:"find",…} atom instead of the plain {node:"allOfClass"}
|
|
804
|
+
// a bare qualified class gets — see parseFindPredicateHead's own doc above.
|
|
805
|
+
const findHead = parseFindPredicateHead(w, lc);
|
|
806
|
+
if (findHead) {
|
|
807
|
+
const { entityType, term, relIdx } = findHead;
|
|
808
|
+
const predLc = lc.slice(relIdx + 1);
|
|
809
|
+
const predWords = w.slice(relIdx + 1);
|
|
810
|
+
if (!predLc.length) return { node: "miss", reason: `a relative clause needs a predicate after "${lc[relIdx]}"` };
|
|
811
|
+
const built = buildPredicateAtoms(entityType, `which ${lc[relIdx - 1]}`, predLc, predWords, nlp, depth + 1);
|
|
812
|
+
if (built.miss) return { node: "miss", reason: built.miss };
|
|
813
|
+
const atoms = [{ op: "seed", kind: "set", ast: { node: "find", entityType, term } }, ...built.atoms];
|
|
814
|
+
return atoms.length === 1 ? atoms[0].ast : { node: "boolean", entityType, atoms };
|
|
815
|
+
}
|
|
816
|
+
|
|
685
817
|
let i = 0;
|
|
686
818
|
while (i < lc.length && FRAME_WORDS.has(lc[i])) i += 1;
|
|
687
819
|
const framed = i > 0;
|
|
@@ -864,6 +996,13 @@ function qualSets(graph) {
|
|
|
864
996
|
qualCache.set(graph, c);
|
|
865
997
|
return c;
|
|
866
998
|
}
|
|
999
|
+
// KNOWN DIVERGENCE (not a bug, do not merge): this moduleIdOf is DEFINES-EDGE-keyed
|
|
1000
|
+
// (walks the `defines` edge Module->symbol, built once in qualSets above), while
|
|
1001
|
+
// codegraph.mjs's own moduleIdOf (codegraph.mjs:1198) is SITE-ATTRIBUTE-keyed (reads
|
|
1002
|
+
// the individual's `site` attribute / a `fn:<path>#name` id shape) — the two can
|
|
1003
|
+
// disagree for a symbol whose site attribute and defines edge point at different
|
|
1004
|
+
// modules (a genuine, currently-untested cross-file edge case), so this file
|
|
1005
|
+
// deliberately keeps its own copy rather than importing codegraph.mjs's.
|
|
867
1006
|
function moduleIdOf(graph, ind) {
|
|
868
1007
|
if (!ind) return null;
|
|
869
1008
|
if (ind.class === "Module") return ind.id;
|
|
@@ -894,11 +1033,185 @@ function qualHolds(graph, ind, spec) {
|
|
|
894
1033
|
}
|
|
895
1034
|
}
|
|
896
1035
|
|
|
1036
|
+
// ---- predicate-find (Workstream 2) — the narrow-then-broaden inheritance cascade
|
|
1037
|
+
// over `inherits` edges (Class->Class today; ANY entityType that later gains such
|
|
1038
|
+
// edges between individuals of the SAME class extends automatically — detected
|
|
1039
|
+
// dynamically via inheritsApplicable, never hardcoded to "Class"). ----
|
|
1040
|
+
|
|
1041
|
+
/** All `inherits` edges (subject inherits FROM object — the derived class is the
|
|
1042
|
+
* subject, the base class is the object; RELATIONS.inherits' own comment). */
|
|
1043
|
+
function inheritsEdges(graph) {
|
|
1044
|
+
return edgesOfKind(graph, "inherits");
|
|
1045
|
+
}
|
|
1046
|
+
/** Direct subclasses of `id` (edges whose OBJECT is id). */
|
|
1047
|
+
function directChildrenOf(graph, id) {
|
|
1048
|
+
return inheritsEdges(graph).filter((e) => e.object === id).map((e) => e.subject);
|
|
1049
|
+
}
|
|
1050
|
+
/** Direct superclasses of `id` (edges whose SUBJECT is id). */
|
|
1051
|
+
function directParentsOf(graph, id) {
|
|
1052
|
+
return inheritsEdges(graph).filter((e) => e.subject === id).map((e) => e.object);
|
|
1053
|
+
}
|
|
1054
|
+
/** Every descendant (subclass, transitively) of `id` — cycle-safe BFS. */
|
|
1055
|
+
function descendantsOf(graph, id) {
|
|
1056
|
+
const out = new Set();
|
|
1057
|
+
const queue = [...directChildrenOf(graph, id)];
|
|
1058
|
+
while (queue.length) {
|
|
1059
|
+
const next = queue.shift();
|
|
1060
|
+
if (out.has(next)) continue;
|
|
1061
|
+
out.add(next);
|
|
1062
|
+
for (const c of directChildrenOf(graph, next)) if (!out.has(c)) queue.push(c);
|
|
1063
|
+
}
|
|
1064
|
+
return out;
|
|
1065
|
+
}
|
|
1066
|
+
/** Every ancestor (superclass, transitively) of `id` — cycle-safe BFS. */
|
|
1067
|
+
function ancestorsOf(graph, id) {
|
|
1068
|
+
const out = new Set();
|
|
1069
|
+
const queue = [...directParentsOf(graph, id)];
|
|
1070
|
+
while (queue.length) {
|
|
1071
|
+
const next = queue.shift();
|
|
1072
|
+
if (out.has(next)) continue;
|
|
1073
|
+
out.add(next);
|
|
1074
|
+
for (const p of directParentsOf(graph, next)) if (!out.has(p)) queue.push(p);
|
|
1075
|
+
}
|
|
1076
|
+
return out;
|
|
1077
|
+
}
|
|
1078
|
+
/** Does `entityType` participate in an `inherits`-style subsumption relation TODAY —
|
|
1079
|
+
* at least one inherits edge whose subject AND object are both individuals of this
|
|
1080
|
+
* class? Detected dynamically (never hardcoded to "Class") so the cascade below
|
|
1081
|
+
* extends automatically to any future type that gains such edges; when false, the
|
|
1082
|
+
* broad (ancestor/sibling) pass is simply a no-op and predicate-find degrades to a
|
|
1083
|
+
* flat own-label+attributes match — the common case for Module/Function today.
|
|
1084
|
+
* Memoized per graph (a WeakMap so it never leaks/needs manual invalidation). */
|
|
1085
|
+
const inheritsApplicableCache = new WeakMap();
|
|
1086
|
+
function inheritsApplicable(graph, entityType) {
|
|
1087
|
+
let byType = inheritsApplicableCache.get(graph);
|
|
1088
|
+
if (!byType) { byType = new Map(); inheritsApplicableCache.set(graph, byType); }
|
|
1089
|
+
if (byType.has(entityType)) return byType.get(entityType);
|
|
1090
|
+
const ok = inheritsEdges(graph).some((e) => {
|
|
1091
|
+
const s = graph.byId.get(e.subject); const o = graph.byId.get(e.object);
|
|
1092
|
+
return !!s && !!o && s.class === entityType && o.class === entityType;
|
|
1093
|
+
});
|
|
1094
|
+
byType.set(entityType, ok);
|
|
1095
|
+
return ok;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** Does `ind`'s OWN property surface (label, or an attribute value) contain EVERY
|
|
1099
|
+
* token of the fuzzy term (AND across tokens, same tokenizer resolveObject's own
|
|
1100
|
+
* tier-3 uses)? Returns "label" | "attr" | null — the provenance tag findSortHits
|
|
1101
|
+
* scores label hits above attribute-only hits (per the match-scope design). */
|
|
1102
|
+
function ownSurfaceHit(ind, termTokens) {
|
|
1103
|
+
const labelLc = String(ind.label || "").toLowerCase();
|
|
1104
|
+
if (termTokens.every((tok) => labelLc.includes(tok))) return "label";
|
|
1105
|
+
const attrs = (ind.attributes || []).map((a) => String(a.value ?? "").toLowerCase());
|
|
1106
|
+
if (termTokens.every((tok) => attrs.some((v) => v.includes(tok)))) return "attr";
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
// own-label hits rank above inheritance-chain hits above attribute-only hits (the
|
|
1110
|
+
// match-scope design's stated scoring); tie-break by shorter label (the same
|
|
1111
|
+
// convention resolveObject's own tiers use for a scored tie).
|
|
1112
|
+
const FIND_TIER = { label: 3, chain: 2, attr: 1 };
|
|
1113
|
+
function sortFindHits(hits) {
|
|
1114
|
+
return hits.slice()
|
|
1115
|
+
.sort((a, b) => (FIND_TIER[b.via] - FIND_TIER[a.via]) || (String(a.ind.label).length - String(b.ind.label).length))
|
|
1116
|
+
.map((h) => h.ind);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** A BOUNDED-FUZZY (Damerau-Levenshtein, same budget resolveObject's own tier-5
|
|
1120
|
+
* uses) near-match of the WHOLE term against `ind`'s label or any of its
|
|
1121
|
+
* components. Used ONLY by the broad pass below — never the narrow pass, whose
|
|
1122
|
+
* exact-substring `ownSurfaceHit` test already runs against EVERY individual of
|
|
1123
|
+
* the type (ancestors and siblings included, being ordinary pool members too),
|
|
1124
|
+
* so an exact-substring re-test in the broad pass would be logically vacuous: if
|
|
1125
|
+
* narrow found nothing, no individual's own surface can contain the term as a
|
|
1126
|
+
* substring, full stop. Fuzzy near-matching is what makes the broad pass find
|
|
1127
|
+
* something narrow genuinely couldn't (a typo'd or partial name on a relative),
|
|
1128
|
+
* which is also why a broad-pass hit is always rendered "related, not exact" —
|
|
1129
|
+
* it is a near-miss by construction, not a confident equal. */
|
|
1130
|
+
function fuzzyFindHit(ind, term) {
|
|
1131
|
+
const tLc = String(term || "").trim().toLowerCase();
|
|
1132
|
+
if (tLc.length < 4) return false; // same floor resolveObject's tier-5 uses
|
|
1133
|
+
const bound = fuzzyBound(tLc);
|
|
1134
|
+
if (editDistance(String(ind.label || "").toLowerCase(), tLc, bound) <= bound) return true;
|
|
1135
|
+
for (const comp of componentSet(ind.label)) {
|
|
1136
|
+
if (editDistance(comp, tLc, bound) <= bound) return true;
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** The narrow-then-broaden search behind evalSet's "find" case and evalComposite's
|
|
1142
|
+
* dedicated "find" handling (predicate-find, Workstream 2):
|
|
1143
|
+
* 1. NARROW — for each `entityType` individual, a hit if its OWN surface matches
|
|
1144
|
+
* every term token, OR (when the type participates in `inherits` today) any of
|
|
1145
|
+
* its DESCENDANTS' own surface does — a subclass genuinely IS a kind of its
|
|
1146
|
+
* superclass, so a hit anywhere in the subtree counts as the candidate itself
|
|
1147
|
+
* matching. If this pass finds ≥1 hit anywhere in the pool, it is the WHOLE
|
|
1148
|
+
* answer — never silently widened when a specific answer exists (the same
|
|
1149
|
+
* discipline Bug C's grain-aware resolution establishes). Every individual of
|
|
1150
|
+
* the type is tested here, ancestors and siblings included (they are ordinary
|
|
1151
|
+
* pool members too) — so an EMPTY narrow pass means no individual's own
|
|
1152
|
+
* surface anywhere in the pool contains the term as a substring.
|
|
1153
|
+
* 2. BROAD — only when the narrow pass is EMPTY across the WHOLE pool AND the
|
|
1154
|
+
* type participates in `inherits`: for each candidate, walk UP to its
|
|
1155
|
+
* superclass(es); a bounded-FUZZY near-match (fuzzyFindHit, above — never a
|
|
1156
|
+
* repeat of narrow's exact test, which the previous point shows would find
|
|
1157
|
+
* nothing new) on a superclass's own surface counts, and so does one on that
|
|
1158
|
+
* superclass's OTHER direct children (siblings) — always rendered as
|
|
1159
|
+
* "related, not exact" (renderComposite), never an unqualified match.
|
|
1160
|
+
* Returns {narrow, broad} — `broad` is only ever non-empty when `narrow` is empty.
|
|
1161
|
+
* When the type has no inherits edges at all, the broad pass is a no-op and this
|
|
1162
|
+
* degrades to a flat own-label+attributes match (Module/Function today). */
|
|
1163
|
+
function computeFind(graph, entityType, term) {
|
|
1164
|
+
const pool = graph.individuals.filter((i) => i.class === entityType);
|
|
1165
|
+
const termTokens = [...componentSet(term)];
|
|
1166
|
+
if (!termTokens.length || !pool.length) return { narrow: [], broad: [] };
|
|
1167
|
+
const cascade = inheritsApplicable(graph, entityType);
|
|
1168
|
+
|
|
1169
|
+
const narrowHits = [];
|
|
1170
|
+
for (const ind of pool) {
|
|
1171
|
+
const own = ownSurfaceHit(ind, termTokens);
|
|
1172
|
+
if (own) { narrowHits.push({ ind, via: own }); continue; }
|
|
1173
|
+
if (!cascade) continue;
|
|
1174
|
+
const viaChain = [...descendantsOf(graph, ind.id)].some((did) => {
|
|
1175
|
+
const d = graph.byId.get(did);
|
|
1176
|
+
return !!d && !!ownSurfaceHit(d, termTokens);
|
|
1177
|
+
});
|
|
1178
|
+
if (viaChain) narrowHits.push({ ind, via: "chain" });
|
|
1179
|
+
}
|
|
1180
|
+
if (narrowHits.length || !cascade) return { narrow: sortFindHits(narrowHits), broad: [] };
|
|
1181
|
+
|
|
1182
|
+
const broadHits = new Map(); // id -> {ind, via}
|
|
1183
|
+
for (const ind of pool) {
|
|
1184
|
+
for (const ancId of ancestorsOf(graph, ind.id)) {
|
|
1185
|
+
if (!broadHits.has(ancId)) {
|
|
1186
|
+
const anc = graph.byId.get(ancId);
|
|
1187
|
+
if (anc && anc.class === entityType && fuzzyFindHit(anc, term)) {
|
|
1188
|
+
broadHits.set(ancId, { ind: anc, via: "chain" });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
for (const sibId of directChildrenOf(graph, ancId)) {
|
|
1192
|
+
if (sibId === ind.id || broadHits.has(sibId)) continue;
|
|
1193
|
+
const sib = graph.byId.get(sibId);
|
|
1194
|
+
if (sib && sib.class === entityType && fuzzyFindHit(sib, term)) broadHits.set(sibId, { ind: sib, via: "chain" });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return { narrow: [], broad: sortFindHits([...broadHits.values()]) };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
897
1201
|
/** Compile a set-producing AST into an array of individuals. */
|
|
898
1202
|
function evalSet(graph, ast, opts) {
|
|
899
1203
|
switch (ast.node) {
|
|
900
1204
|
case "clause": return traverse(graph, ast.clause, opts).matches || [];
|
|
901
1205
|
case "allOfClass": return graph.individuals.filter((i) => i.class === ast.entityType);
|
|
1206
|
+
// predicate-find (Workstream 2), embedded as a set atom (§6 generalization —
|
|
1207
|
+
// a find-seed inside a boolean/qualifier fold): the narrow-then-broaden
|
|
1208
|
+
// cascade's result, transparently flattened (the "related, not exact" framing
|
|
1209
|
+
// is a top-level RENDER concern — evalComposite's dedicated "find" handling
|
|
1210
|
+
// below, not this generic embedding).
|
|
1211
|
+
case "find": {
|
|
1212
|
+
const { narrow, broad } = computeFind(graph, ast.entityType, ast.term);
|
|
1213
|
+
return narrow.length ? narrow : broad;
|
|
1214
|
+
}
|
|
902
1215
|
// the SUBJECTS that have ANY edge of a kind (the existential "modules that import
|
|
903
1216
|
// anything") — the positive set an existential negation ("do not import anything")
|
|
904
1217
|
// differences off allOfClass to yield "modules that import nothing".
|
|
@@ -1046,6 +1359,17 @@ export function evalComposite(graph, ast, opts = {}) {
|
|
|
1046
1359
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
1047
1360
|
if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
|
|
1048
1361
|
if (ast.node === "anaphora") return evalAnaphora(graph, ast, opts);
|
|
1362
|
+
// predicate-find (Workstream 2), TOP-LEVEL: unlike evalSet's "find" case (used
|
|
1363
|
+
// when a find-seed is embedded inside a boolean/qualifier fold, §6), this keeps
|
|
1364
|
+
// the broad-pass provenance so renderComposite can label a "related, not exact"
|
|
1365
|
+
// hit distinctly rather than presenting it as an unqualified match.
|
|
1366
|
+
if (ast.node === "find") {
|
|
1367
|
+
const { narrow, broad } = computeFind(graph, ast.entityType, ast.term);
|
|
1368
|
+
return {
|
|
1369
|
+
compositeKind: "find", entityType: ast.entityType, term: ast.term,
|
|
1370
|
+
matches: narrow.length ? narrow : broad, broad: !narrow.length && broad.length > 0,
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1049
1373
|
return { compositeKind: "set", matches: evalSet(graph, ast, opts), entityType: ast.entityType || null };
|
|
1050
1374
|
}
|
|
1051
1375
|
|
|
@@ -1059,7 +1383,17 @@ const compositeList = (matches) => listJoin(matches.slice(0, OVERFLOW_CAP)
|
|
|
1059
1383
|
/** A compositional worked example for the rephrase hint (§honest miss now shows a
|
|
1060
1384
|
* compositional phrasing too). */
|
|
1061
1385
|
export function compositionalHint() {
|
|
1062
|
-
return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", or (after a listing) "which of those are tested"';
|
|
1386
|
+
return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", "find me the payment class", or (after a listing) "which of those are tested"';
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** A short citation line for a SINGLE predicate-find hit — the module it lives in,
|
|
1390
|
+
* when known (mirrors the plain reverse-shape render's grouping convention, just
|
|
1391
|
+
* condensed to one line since there is exactly one hit to cite). */
|
|
1392
|
+
function describeFindHit(ind) {
|
|
1393
|
+
const label = ["Function", "Method"].includes(ind.class) ? `${ind.label}()` : ind.label;
|
|
1394
|
+
if (ind.class === "Module") return label;
|
|
1395
|
+
const mod = moduleLabelOf(ind);
|
|
1396
|
+
return mod && mod !== "(unknown module)" ? `${label} in ${mod}` : label;
|
|
1063
1397
|
}
|
|
1064
1398
|
|
|
1065
1399
|
function renderComposite(parsed, result) {
|
|
@@ -1086,6 +1420,25 @@ function renderComposite(parsed, result) {
|
|
|
1086
1420
|
: "";
|
|
1087
1421
|
return { content: `${compositeList(result.matches)}${hint}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1088
1422
|
}
|
|
1423
|
+
// predicate-find (Workstream 2): zero hits -> an honest miss naming BOTH the type
|
|
1424
|
+
// and the term; the broad ("related, not exact") pass is ALWAYS clearly labeled,
|
|
1425
|
+
// never presented as an unqualified match — the confident-wrong discipline Bug
|
|
1426
|
+
// C's grain-aware resolution established; one hit -> a short citation; many hits
|
|
1427
|
+
// -> the standard compositeList/OVERFLOW_CAP convention, reused verbatim.
|
|
1428
|
+
if (result.compositeKind === "find") {
|
|
1429
|
+
const typeNoun = nounFor(result.entityType, 1);
|
|
1430
|
+
if (!result.matches.length) {
|
|
1431
|
+
return { content: `no ${nounFor(result.entityType, 2)} found matching "${result.term}".`, miss: true, ambiguous: false, matches: [] };
|
|
1432
|
+
}
|
|
1433
|
+
const cited = result.matches.length === 1 ? describeFindHit(result.matches[0]) : compositeList(result.matches);
|
|
1434
|
+
if (result.broad) {
|
|
1435
|
+
return {
|
|
1436
|
+
content: `no exact ${typeNoun} named "${result.term}", but found a related ${result.matches.length === 1 ? typeNoun : nounFor(result.entityType, 2)}: ${cited}.`,
|
|
1437
|
+
miss: false, ambiguous: false, matches: result.matches, relatedNotExact: true,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
return { content: `${cited}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1441
|
+
}
|
|
1089
1442
|
if (result.compositeKind === "superlative") {
|
|
1090
1443
|
if (!result.matches.length) return { content: `no ${nounFor(result.entityType, 2)} to rank in this index.`, miss: true, ambiguous: false };
|
|
1091
1444
|
const lead = result.extreme === "most" ? "the most" : "the fewest";
|
|
@@ -1170,12 +1523,25 @@ function componentSet(s) {
|
|
|
1170
1523
|
* only) nor to terms under 4 chars (the bound would cover half of everything).
|
|
1171
1524
|
* (6) no match at all — an honest miss. Returns {match, candidates, tier, ambiguous
|
|
1172
1525
|
* [, matchedVia]} — ambiguous on a true tier-3 score tie, a tier-4 overlap-count
|
|
1173
|
-
* tie, or a tier-5 distance tie.
|
|
1174
|
-
|
|
1526
|
+
* tie, or a tier-5 distance tie.
|
|
1527
|
+
*
|
|
1528
|
+
* `opts.expectedClass` (grain-aware resolution, Bug C+D fix): when set, narrows
|
|
1529
|
+
* the candidate POOL to `i.class === expectedClass` before every pool-driven tier
|
|
1530
|
+
* (exact/tier-3/tier-5) — the ranking code within each tier is untouched, only the
|
|
1531
|
+
* universe it ranks over shrinks. The ext: tier (synthetic matches with
|
|
1532
|
+
* `class: null`, never a real individual) is skipped outright when a class is
|
|
1533
|
+
* expected — it can never BE that class. The prose tier (tier 4) filters its hits
|
|
1534
|
+
* to the expected class before picking a winner. Every existing call site passes
|
|
1535
|
+
* no 3rd argument, so `expectedClass` defaults to null and behavior is
|
|
1536
|
+
* byte-identical to before this option existed — this is purely opt-in narrowing
|
|
1537
|
+
* for a caller (traverse()'s reverse case) that already knows what class the
|
|
1538
|
+
* relation's object slot expects ("which modules import logger" must never
|
|
1539
|
+
* resolve "logger" to a same-stem Class). */
|
|
1540
|
+
export function resolveObject(graph, term, { expectedClass = null } = {}) {
|
|
1175
1541
|
const t = String(term || "").trim();
|
|
1176
1542
|
if (!t) return { match: null, candidates: [], tier: null, ambiguous: false };
|
|
1177
1543
|
const tLc = t.toLowerCase();
|
|
1178
|
-
const pool = graph.individuals;
|
|
1544
|
+
const pool = expectedClass ? graph.individuals.filter((i) => i.class === expectedClass) : graph.individuals;
|
|
1179
1545
|
|
|
1180
1546
|
// commit-sha tier (checked first, only for sha-shaped terms): "ef74e44e25c8",
|
|
1181
1547
|
// "commit ef74e44e25c8", "commit:ef74e44", or a full 40-char sha resolve against
|
|
@@ -1212,7 +1578,10 @@ export function resolveObject(graph, term) {
|
|
|
1212
1578
|
if (String(e.object).toLowerCase() === extLc) { extId = e.object; break outer; }
|
|
1213
1579
|
}
|
|
1214
1580
|
}
|
|
1215
|
-
|
|
1581
|
+
// ext: matches are synthetic (class: null, no real individual) — with a class
|
|
1582
|
+
// expected, they can never satisfy it, so skip this tier entirely rather than
|
|
1583
|
+
// returning a match whose class silently doesn't match what the caller asked for.
|
|
1584
|
+
if (extId && !expectedClass) return { match: { id: extId, label: t, class: null }, candidates: [], tier: 2, ambiguous: false };
|
|
1216
1585
|
|
|
1217
1586
|
// tier 3 — two disjoint regimes (dotted-symbol fix, 2026-07-02, advisor-verified
|
|
1218
1587
|
// bug): a DOTTED term with no slash ("res.json", "Widget.render", "walk.mjs") is
|
|
@@ -1274,7 +1643,9 @@ export function resolveObject(graph, term) {
|
|
|
1274
1643
|
// side door — a dotted term names an identifier, and identifiers resolve by
|
|
1275
1644
|
// label (tiers above) or the bounded fuzzy pass below, or they honestly miss.
|
|
1276
1645
|
let proseResult = null;
|
|
1277
|
-
const proseHits = !dotted && typeof lookupByProseTokens === "function"
|
|
1646
|
+
const proseHits = !dotted && typeof lookupByProseTokens === "function"
|
|
1647
|
+
? lookupByProseTokens(graph.proseIndex, t).filter((h) => !expectedClass || graph.byId.get(h.id)?.class === expectedClass)
|
|
1648
|
+
: [];
|
|
1278
1649
|
if (proseHits.length) {
|
|
1279
1650
|
const [best, ...rest] = proseHits;
|
|
1280
1651
|
const bestInd = graph.byId.get(best.id);
|
|
@@ -1660,22 +2031,81 @@ export function traverse(graph, parsed, { contextId = null, prev = null } = {})
|
|
|
1660
2031
|
return { matches, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
|
|
1661
2032
|
}
|
|
1662
2033
|
|
|
2034
|
+
// §grain-aware object resolution (Bug C+D, HANDOVER follow-up #2, checked BEFORE
|
|
2035
|
+
// the edge filter below): a predicate's OBJECT slot carries one particular class
|
|
2036
|
+
// (kindObjectClass) — resolveObject itself is blind to that, so a same-stem term
|
|
2037
|
+
// ("logger") can resolve to the WRONG grain (a Class named Logger) instead of the
|
|
2038
|
+
// Module the "imports"/"calls"/… edge actually points at, and the edge filter
|
|
2039
|
+
// below then legitimately returns [] for the wrong-grain id — a confident-wrong
|
|
2040
|
+
// empty, not an honest miss. `wantClass` is null for a kind whose edges span more
|
|
2041
|
+
// than one object class (e.g. "contains") — no grain check applies there, byte-
|
|
2042
|
+
// identical to before. objMatch.class === null (an ext: synthetic match, no real
|
|
2043
|
+
// individual — see resolveObject's tier 2) is likewise never grain-checked: it has
|
|
2044
|
+
// no better class to compare against, and is already the most specific resolution
|
|
2045
|
+
// available.
|
|
2046
|
+
let gObjMatch = objMatch;
|
|
2047
|
+
let gCandidates = candidates;
|
|
2048
|
+
let gAmbiguous = ambiguous;
|
|
2049
|
+
let gMatchedVia = matchedVia;
|
|
2050
|
+
let grainRefinedNote = "";
|
|
2051
|
+
const wantClass = kindObjectClass(graph, kind);
|
|
2052
|
+
if (wantClass && gObjMatch.class && gObjMatch.class !== wantClass) {
|
|
2053
|
+
// (1) retry resolution SCOPED to the expected class — "logger" now only
|
|
2054
|
+
// considers Module individuals, so it lands on src/lib/logger.mjs instead of
|
|
2055
|
+
// the same-stem Class (fixes Bug C).
|
|
2056
|
+
const retry = resolveObject(graph, parsed.object, { expectedClass: wantClass });
|
|
2057
|
+
if (retry.match && !retry.ambiguous) {
|
|
2058
|
+
gObjMatch = retry.match;
|
|
2059
|
+
gCandidates = retry.candidates;
|
|
2060
|
+
gAmbiguous = retry.ambiguous;
|
|
2061
|
+
gMatchedVia = retry.matchedVia;
|
|
2062
|
+
} else if ((kind === "tests" || kind === "cochange") && gObjMatch.class !== "Module") {
|
|
2063
|
+
// (2) tests/cochange are always Module->Module — no same-grain alternative
|
|
2064
|
+
// exists (the retry above genuinely found nothing), but the resolved
|
|
2065
|
+
// fine-grain entity (a Function, say) DOES live in a module, and that
|
|
2066
|
+
// module is the real, honest subject of a tests/cochange question ("does
|
|
2067
|
+
// createTask have tests" — fixes Bug D). Up-refine via the same moduleIdOf
|
|
2068
|
+
// qualHolds's "tested" case already uses (see its divergence comment above).
|
|
2069
|
+
const mid = moduleIdOf(graph, gObjMatch);
|
|
2070
|
+
const mod = mid && graph.byId.get(mid);
|
|
2071
|
+
if (mod) {
|
|
2072
|
+
grainRefinedNote = `, refined from ${gObjMatch.label} to its containing module`;
|
|
2073
|
+
gObjMatch = mod;
|
|
2074
|
+
} else {
|
|
2075
|
+
return {
|
|
2076
|
+
matches: [], objMatch: gObjMatch, candidates: gCandidates, ambiguous: gAmbiguous, matchedVia: gMatchedVia,
|
|
2077
|
+
wrongGrainMiss: true, wantClass,
|
|
2078
|
+
traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass}, and no containing module could be found to refine to)`,
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
} else {
|
|
2082
|
+
// (3) neither a same-grain resolution nor an up-refinement applies — an
|
|
2083
|
+
// honest wrong-grain miss, distinct from both "unresolved" (the existing
|
|
2084
|
+
// objMatch-null branch below, untouched) and "resolved + genuinely empty".
|
|
2085
|
+
return {
|
|
2086
|
+
matches: [], objMatch: gObjMatch, candidates: gCandidates, ambiguous: gAmbiguous, matchedVia: gMatchedVia,
|
|
2087
|
+
wrongGrainMiss: true, wantClass,
|
|
2088
|
+
traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass})`,
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1663
2093
|
// General case: some predicates are already fine-grained (inherits: Class->Class, contains:
|
|
1664
2094
|
// Class->Member) and some are module-coarse (imports/calls/tests/cochange: Module->Module).
|
|
1665
2095
|
// Rather than assume one or the other, check what the edge's actual subjects ARE: if they
|
|
1666
2096
|
// already match the requested entityType, use them directly (inherits); only when they're
|
|
1667
2097
|
// Module individuals and a FINER entityType was asked for do we refine via `defines`
|
|
1668
2098
|
// (imports) — never blindly treat an edge's subject id as if it were always a module id.
|
|
1669
|
-
let edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.object ===
|
|
2099
|
+
let edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.object === gObjMatch.id);
|
|
1670
2100
|
let extNote = "";
|
|
1671
|
-
if (!edges.length &&
|
|
2101
|
+
if (!edges.length && gObjMatch.class) {
|
|
1672
2102
|
// Unresolved ext:<Name> endpoints with the SAME name as the resolved entity:
|
|
1673
2103
|
// the extractor declined to assert identity (e.g. commander's every "class X
|
|
1674
2104
|
// extends Command" edge points at ext:Command, never the Class node), so a
|
|
1675
2105
|
// strict id match renders a FALSE blank. Count them by NAME instead and say
|
|
1676
2106
|
// so in the receipt — name-grade evidence, labeled as such, same standard as
|
|
1677
2107
|
// resolveObject's own ext: tier.
|
|
1678
|
-
const extId = `ext:${String(
|
|
2108
|
+
const extId = `ext:${String(gObjMatch.label).toLowerCase()}`;
|
|
1679
2109
|
edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => String(e.object).toLowerCase() === extId);
|
|
1680
2110
|
if (edges.length) extNote = ` (by name, via unresolved ${extId} references)`;
|
|
1681
2111
|
}
|
|
@@ -1706,7 +2136,11 @@ export function traverse(graph, parsed, { contextId = null, prev = null } = {})
|
|
|
1706
2136
|
matches = [];
|
|
1707
2137
|
}
|
|
1708
2138
|
}
|
|
1709
|
-
return {
|
|
2139
|
+
return {
|
|
2140
|
+
matches, objMatch: gObjMatch, candidates: gCandidates,
|
|
2141
|
+
traversal: `${kindsFor(kind).join("+")} edges where object = ${gObjMatch.label}${extNote}${grainNote}${grainRefinedNote}`,
|
|
2142
|
+
ambiguous: gAmbiguous, matchedVia: gMatchedVia,
|
|
2143
|
+
};
|
|
1710
2144
|
}
|
|
1711
2145
|
|
|
1712
2146
|
// ---- §5 templated renderer — string interpolation + grouping/pluralization/overflow rules,
|
|
@@ -1786,6 +2220,19 @@ function renderCore(parsed, result) {
|
|
|
1786
2220
|
miss: true, ambiguous: false,
|
|
1787
2221
|
};
|
|
1788
2222
|
}
|
|
2223
|
+
// wrong-grain honest miss (Bug C+D, traverse()'s general reverse case): the term
|
|
2224
|
+
// resolved to a REAL entity, just not the class this predicate's object slot
|
|
2225
|
+
// needs, and no same-grain alternative (nor an up-refinement to a containing
|
|
2226
|
+
// module) exists — distinct from both the objMatch-null "unresolved" miss below
|
|
2227
|
+
// and a resolved-but-genuinely-empty answer.
|
|
2228
|
+
if (result.wrongGrainMiss) {
|
|
2229
|
+
const gotNoun = result.objMatch.class ? nounFor(result.objMatch.class, 1) : "term";
|
|
2230
|
+
const wantNoun = nounFor(result.wantClass, 1);
|
|
2231
|
+
return {
|
|
2232
|
+
content: `"${parsed.object}" resolved to the ${gotNoun} ${result.objMatch.label}, but this question needs a ${wantNoun} — no ${wantNoun} named "${parsed.object}" was found in the index.`,
|
|
2233
|
+
miss: true, ambiguous: false,
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
1789
2236
|
if (parsed.shape === "meta") {
|
|
1790
2237
|
if (!result.objMatch) {
|
|
1791
2238
|
return {
|
|
@@ -1934,7 +2381,7 @@ function renderCore(parsed, result) {
|
|
|
1934
2381
|
// subject-first phrasing rather than reusing reverse's "found ... that OBJECT" template.
|
|
1935
2382
|
if (parsed.shape === "forward") {
|
|
1936
2383
|
return {
|
|
1937
|
-
content: `${result.objMatch.label} has no ${parsed.kind} edges in the index.`,
|
|
2384
|
+
content: `${result.objMatch.label} has no ${verbFor(parsed.kind)} edges in the index.`,
|
|
1938
2385
|
miss: true, ambiguous: false,
|
|
1939
2386
|
};
|
|
1940
2387
|
}
|