@polycode-projects/the-mechanical-code-talker 1.0.5 → 1.0.6
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 +93 -4
- package/src/interpret/normalize.mjs +89 -0
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.6",
|
|
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
|
@@ -320,6 +320,7 @@ function parseComposite(text, nlp) {
|
|
|
320
320
|
const w = splitWords(text);
|
|
321
321
|
const lc = w.map((x) => x.toLowerCase());
|
|
322
322
|
return parseExistence(w, lc)
|
|
323
|
+
|| parseQualifierCheck(w, lc)
|
|
323
324
|
|| parseNegation(text, nlp, 0)
|
|
324
325
|
|| parseForwardNegation(w, lc, nlp)
|
|
325
326
|
|| parseTemporal(w, lc, nlp, 0)
|
|
@@ -655,6 +656,43 @@ function parseExistence(w, lc) {
|
|
|
655
656
|
// different (relationship) question; leave it for the parsers below.
|
|
656
657
|
}
|
|
657
658
|
|
|
659
|
+
/** QUALIFIER-CHECK: "is <term> [a/an] <qualifier> [<kind>]?", "is <term> not
|
|
660
|
+
* <qualifier> …" — a single-ENTITY Yes/No property check ("is Task.title
|
|
661
|
+
* public", "is it exported", "is that class abstract"), reusing the SAME
|
|
662
|
+
* closed QUALIFIERS vocabulary and qualHolds() evaluator the attributive/
|
|
663
|
+
* predicative-survey filters already fold over a SET ("public methods",
|
|
664
|
+
* "which methods are public") — this is the missing single-entity sibling
|
|
665
|
+
* (0.9.15 Tier-1 single-touch playtest: "is it a public attribute?", a
|
|
666
|
+
* natural follow-up to a concept-force touch, had no recognizer at all and
|
|
667
|
+
* hit the bare grammar wall — even "is Task.title public", a concretely
|
|
668
|
+
* NAMED entity with no anaphora involved, walled the same way). Scoped
|
|
669
|
+
* tight: a leading "is"/"are", then TERM tokens up to the FIRST recognized
|
|
670
|
+
* qualifier word — a leading "the" and a trailing "a"/"an" article around
|
|
671
|
+
* the boundary are dropped, and a trailing decorative kind noun ("… public
|
|
672
|
+
* ATTRIBUTE") is simply never consumed, never required to agree with the
|
|
673
|
+
* resolved entity's real class. Guarded off "is/are THERE …" (parseExistence
|
|
674
|
+
* above owns that shape) and off any text with no qualifier word at all, so
|
|
675
|
+
* it can never swallow a genuine relationship/existence question. The term
|
|
676
|
+
* is resolved at EVAL time (a pronoun binds through the standing contextId,
|
|
677
|
+
* exactly like every other object term), never here. */
|
|
678
|
+
function parseQualifierCheck(w, lc) {
|
|
679
|
+
if (lc[0] !== "is" && lc[0] !== "are") return null;
|
|
680
|
+
if (lc[1] === "there") return null; // parseExistence's own shape
|
|
681
|
+
let qualIdx = -1;
|
|
682
|
+
let negated = false;
|
|
683
|
+
for (let i = 1; i < lc.length; i += 1) {
|
|
684
|
+
if (QUALIFIERS[lc[i]]) { qualIdx = i; negated = lc[i - 1] === "not"; break; }
|
|
685
|
+
}
|
|
686
|
+
if (qualIdx < 0) return null; // no qualifier word at all → not this shape
|
|
687
|
+
let termStart = 1;
|
|
688
|
+
if (lc[termStart] === "the") termStart += 1;
|
|
689
|
+
let termEnd = negated ? qualIdx - 1 : qualIdx;
|
|
690
|
+
if (termEnd > termStart && (lc[termEnd - 1] === "a" || lc[termEnd - 1] === "an")) termEnd -= 1;
|
|
691
|
+
const term = termEnd > termStart ? w.slice(termStart, termEnd).join(" ").trim() : "";
|
|
692
|
+
if (!term) return { node: "miss", reason: `"is/are <qualifier>" needs a named thing to check first` };
|
|
693
|
+
return { node: "qualCheck", term, qualifier: lc[qualIdx], negated };
|
|
694
|
+
}
|
|
695
|
+
|
|
658
696
|
/** Trailing "and that's the whole question" filler an aggregate/list tail can carry
|
|
659
697
|
* ("how many classes are there", "list functions in total", "which classes exist in
|
|
660
698
|
* the index") — a count/list over a bare kind is frequently phrased with such a tail,
|
|
@@ -1598,11 +1636,29 @@ function evalExists(graph, ast) {
|
|
|
1598
1636
|
return { compositeKind: "exists", entityType, term: null, scopeModule, scopeMatch, matches: pool };
|
|
1599
1637
|
}
|
|
1600
1638
|
|
|
1639
|
+
/** QUALIFIER-CHECK eval — resolve the term (a context pronoun binds through
|
|
1640
|
+
* contextId, exactly like resolveTermOrContext's every other caller), then
|
|
1641
|
+
* read the SAME qualHolds() predicate the set-filter path already uses.
|
|
1642
|
+
* `holds` here means "the STATEMENT AS ASKED is true" (negation already
|
|
1643
|
+
* folded in), so the renderer can answer Yes/No directly off it without
|
|
1644
|
+
* re-deriving the negation. An unresolved term is an honest miss, never a
|
|
1645
|
+
* guess — no different from any other named-object lookup. */
|
|
1646
|
+
function evalQualCheck(graph, ast, opts) {
|
|
1647
|
+
const { term, qualifier, negated } = ast;
|
|
1648
|
+
const r = resolveTermOrContext(graph, term, opts.contextId);
|
|
1649
|
+
if (r.unresolvedPronoun) return { compositeKind: "qualCheck", qualCheckMiss: "pronoun", term, matches: [] };
|
|
1650
|
+
if (!r.match) return { compositeKind: "qualCheck", qualCheckMiss: "unresolved", term, matches: [] };
|
|
1651
|
+
const rawHolds = qualHolds(graph, r.match, QUALIFIERS[qualifier]);
|
|
1652
|
+
const holds = negated ? !rawHolds : rawHolds;
|
|
1653
|
+
return { compositeKind: "qualCheck", subject: r.match, qualifier, negated, holds, matches: [r.match] };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1601
1656
|
/** Compile any compositional AST to a result object traverse() returns for the
|
|
1602
1657
|
* simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
|
|
1603
1658
|
export function evalComposite(graph, ast, opts = {}) {
|
|
1604
1659
|
if (ast.node === "miss") return { compositeMiss: true, reason: ast.reason || null, matches: [] };
|
|
1605
1660
|
if (ast.node === "exists") return evalExists(graph, ast);
|
|
1661
|
+
if (ast.node === "qualCheck") return evalQualCheck(graph, ast, opts);
|
|
1606
1662
|
if (ast.node === "count") return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
|
|
1607
1663
|
if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
|
|
1608
1664
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
@@ -1676,6 +1732,27 @@ function renderComposite(parsed, result) {
|
|
|
1676
1732
|
}
|
|
1677
1733
|
return { content: `Yes — ${compositeList(result.matches)}${scopeSuffix}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1678
1734
|
}
|
|
1735
|
+
// qualCheck: "is <term> [a/an] <qualifier> […]" — the single-entity sibling of
|
|
1736
|
+
// "is there a/an <kind> …" above: a direct Yes/No over one already-named/-
|
|
1737
|
+
// focused individual, never a set listing. `result.holds` already has the
|
|
1738
|
+
// negation folded in (evalQualCheck), so the renderer states the actual truth
|
|
1739
|
+
// plainly — "No — X is tested." for "is X not tested" when X IS tested, never
|
|
1740
|
+
// an echo of the (now-false) question's own wording.
|
|
1741
|
+
if (result.compositeKind === "qualCheck") {
|
|
1742
|
+
if (result.qualCheckMiss === "pronoun") {
|
|
1743
|
+
return { content: `"${result.term}" needs a selected node to refer to — click a node first, or name it directly.`, miss: true, ambiguous: false };
|
|
1744
|
+
}
|
|
1745
|
+
if (result.qualCheckMiss === "unresolved") {
|
|
1746
|
+
return { content: `couldn't find "${result.term}" in the index to check.`, miss: true, ambiguous: false };
|
|
1747
|
+
}
|
|
1748
|
+
const label = result.subject.label;
|
|
1749
|
+
const truePhrase = result.negated ? `not ${result.qualifier}` : result.qualifier;
|
|
1750
|
+
const falsePhrase = result.negated ? result.qualifier : `not ${result.qualifier}`;
|
|
1751
|
+
return {
|
|
1752
|
+
content: `${result.holds ? "Yes" : "No"} — ${label} is ${result.holds ? truePhrase : falsePhrase}.`,
|
|
1753
|
+
miss: false, ambiguous: false, matches: result.matches,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1679
1756
|
if (result.compositeKind === "count") {
|
|
1680
1757
|
const noun = result.entityType ? nounFor(result.entityType, result.count) : (result.count === 1 ? "result" : "results");
|
|
1681
1758
|
return { content: `${result.count} ${noun}.`, miss: false, ambiguous: false, matches: [] };
|
|
@@ -2994,10 +3071,22 @@ export function relaxParse(graph, query, { nlp = undefined, contextId = null, pr
|
|
|
2994
3071
|
// own as vocabulary/scaffolding? Guards against a relaxation that drops the actual
|
|
2995
3072
|
// asked term and lets a bare marker slide into its place ("where is [X] defined" →
|
|
2996
3073
|
// "where is defined", "defined" is a WHERE_MARKER, never the thing being located).
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3074
|
+
// A bare CONTEXT PRONOUN ("it"/"this"/"that"/"here"/…) is the one exception: it IS
|
|
3075
|
+
// the real, deliberate object here — it resolves through contextId, not through
|
|
3076
|
+
// vocabulary the grammar "already owns" as scaffolding — so it must count as a real
|
|
3077
|
+
// term rather than being mistaken for a dropped-into-place marker. Without this, a
|
|
3078
|
+
// relaxed candidate whose object survived layer 2 as a lone pronoun ("what else is
|
|
3079
|
+
// in that class" → drop "class"/"else" → "what does that contain") was rejected as
|
|
3080
|
+
// if it named nothing at all, even though the pronoun resolves to a real, answerable
|
|
3081
|
+
// focus (0.9.15 Tier-1 single-touch playtest).
|
|
3082
|
+
const hasRealTerm = (s) => {
|
|
3083
|
+
const whole = String(s || "").trim().toLowerCase();
|
|
3084
|
+
if (CONTEXT_PRONOUNS.includes(whole)) return true;
|
|
3085
|
+
return splitWords(whole).some((w) => {
|
|
3086
|
+
const lc = w.toLowerCase();
|
|
3087
|
+
return !CONTENT_VOCAB.has(lc) && !STRUCTURAL_WORDS.has(lc);
|
|
3088
|
+
});
|
|
3089
|
+
};
|
|
3001
3090
|
// Accept a relaxed attempt ONLY if it is a genuinely answerable parse (terms resolve)
|
|
3002
3091
|
// AND it renders a REAL positive answer — never another empty/miss (relaxation earns a
|
|
3003
3092
|
// win only by turning a miss into an answer, never a differently-worded miss) — and
|
|
@@ -44,6 +44,33 @@ const correctionRe = (table) => new RegExp(
|
|
|
44
44
|
const MISSPELLING_RE = correctionRe(MISSPELLINGS);
|
|
45
45
|
const WRONG_WORD_RE = correctionRe(WRONG_WORDS);
|
|
46
46
|
|
|
47
|
+
/** "that class"/"this module"/"that function" (a context pronoun immediately
|
|
48
|
+
* followed by the SINGULAR kind noun it's already standing in for) -> the bare
|
|
49
|
+
* pronoun alone (0.9.15 Tier-1 single-touch playtest). "which class contains
|
|
50
|
+
* Task.complete" answers by NAMING the class ("… there is Task"); a curious
|
|
51
|
+
* user's very next turn naturally says "what is in that class", not the bare
|
|
52
|
+
* "what is in that" the grammar already understands. Left unstripped, the two
|
|
53
|
+
* parse strategies disagreed on the SPAN (grammar kept "that class" as one
|
|
54
|
+
* literal 2-word object term; keyword-spot split off "class" as an entityType
|
|
55
|
+
* keyword, leaving bare "that" as the object) — same PARSE, different shape,
|
|
56
|
+
* so merge.mjs's honest {ambiguousParse} tie fired even though a human reads
|
|
57
|
+
* this as one unambiguous sentence. Folding the kind noun away BEFORE either
|
|
58
|
+
* strategy runs leaves exactly one reading: CONTEXT_PRONOUNS' own existing
|
|
59
|
+
* focus-resolution (ask.mjs's resolveTermOrContext) then takes it from there,
|
|
60
|
+
* unchanged — this frame only removes the strategy disagreement, it does not
|
|
61
|
+
* touch how the pronoun itself resolves. Singular kind nouns only ("that
|
|
62
|
+
* classes" isn't grammatical, so plurals are never a real anaphora and are
|
|
63
|
+
* left alone); "one" is excluded ("that one" is already its own literal
|
|
64
|
+
* CONTEXT_PRONOUNS entry). */
|
|
65
|
+
const KIND_NOUN_ANAPHORA_RE = /\b(this|that)\s+(class|module|function|method|attribute|variable|file|commit)\b/gi;
|
|
66
|
+
|
|
67
|
+
// every relation verb phrase VERB_TO_KIND knows, as one alternation (longest-first
|
|
68
|
+
// so a multi-word verb like "inherit from" wins over its own leading word "inherit"
|
|
69
|
+
// appearing elsewhere) — feeds the DOES-X-VERB-ANYTHING-ELSE frame below, which needs
|
|
70
|
+
// to recognize "does <subject> <ANY closed relation verb> anything/something [else]"
|
|
71
|
+
// without hardcoding its own parallel verb list.
|
|
72
|
+
const VERB_ALTERNATION = Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|
|
73
|
+
|
|
47
74
|
/** Just the curated MISSPELLINGS correction (ask-vocab.mjs), standalone — for a
|
|
48
75
|
* caller that needs typo-tolerant ANCHOR-WORD matching (a closed regex shape
|
|
49
76
|
* keyed on a literal "what"/"which"/"where" etc.) without running the rest of
|
|
@@ -158,6 +185,25 @@ const EXPLAIN_WRAPPER_RE = /^explain\s+(?:to\s+me\s+|please\s+)*(.+?)\??$/i;
|
|
|
158
185
|
* module" -> "describe store module"). */
|
|
159
186
|
const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
|
|
160
187
|
|
|
188
|
+
/** Leading STACCATO connective before an ALREADY well-formed question ("and
|
|
189
|
+
* what imports it", "so does it import anything else", "also where is X
|
|
190
|
+
* defined") -> the question alone (0.9.15 Tier-1 single-touch playtest). A
|
|
191
|
+
* rapid-fire drill-down naturally opens its next turn with a bare discourse
|
|
192
|
+
* connective (chat.mjs's own STACCATO_LEAKED_CONNECTIVES set: and/also/so/
|
|
193
|
+
* then/now, here joined by "but" for the same family); when what follows is
|
|
194
|
+
* ALREADY an interrogative lead or an auxiliary-question opener, the
|
|
195
|
+
* connective carries no query content of its own — conversational
|
|
196
|
+
* scaffolding, same species as the greeting/subordination preambles. Gated
|
|
197
|
+
* on the REMAINDER starting a real question, so a genuine mid-clause boolean
|
|
198
|
+
* composition ("classes that inherit from Base and are tested") is never
|
|
199
|
+
* touched — that "and" never sits at position 0 to begin with. Without this,
|
|
200
|
+
* "and what imports it" parsed as an "ask"-shape question with "and" itself
|
|
201
|
+
* MISREAD as the subject term — a miss the relaxation cascade can't rescue,
|
|
202
|
+
* because "and" is protected CONTENT_VOCAB (a boolean connective elsewhere)
|
|
203
|
+
* and the noise-strip layer never drops content vocab. */
|
|
204
|
+
const LEADING_CONNECTIVE_RE = /^(?:and|also|so|then|now|but)\s+(.+)$/i;
|
|
205
|
+
const QUESTION_AUX_LEAD_RE = /^(?:does|do|did|is|are|was|were|has|have|had|can|could|will|would|should)\b/i;
|
|
206
|
+
|
|
161
207
|
/** Apply the closed preamble frames in order (greeting -> modal -> show/give-me),
|
|
162
208
|
* repeated to a small fixpoint so stacked wrappers ("hey, can you show me X
|
|
163
209
|
* please") peel fully. Pure and idempotent; unmatched text passes through. */
|
|
@@ -180,6 +226,11 @@ export function applyPreambleFrames(text) {
|
|
|
180
226
|
q = (RELATION_VERB_RE.test(rest) || INTERROGATIVE_LEAD_RE.test(rest)) ? rest : `describe ${rest}`;
|
|
181
227
|
}
|
|
182
228
|
}
|
|
229
|
+
m = q.match(LEADING_CONNECTIVE_RE);
|
|
230
|
+
if (m) {
|
|
231
|
+
const rest = m[1].trim();
|
|
232
|
+
if (INTERROGATIVE_LEAD_RE.test(rest) || QUESTION_AUX_LEAD_RE.test(rest)) q = rest;
|
|
233
|
+
}
|
|
183
234
|
if (q === before) break;
|
|
184
235
|
}
|
|
185
236
|
return q;
|
|
@@ -343,6 +394,7 @@ export function normalizeQuery(text) {
|
|
|
343
394
|
q = q.replace(CONTRACTION_RE, (m) => CONTRACTIONS[m.toLowerCase()]);
|
|
344
395
|
q = q.replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
345
396
|
q = q.replace(WRONG_WORD_RE, (m) => WRONG_WORDS[m.toLowerCase()]);
|
|
397
|
+
q = q.replace(KIND_NOUN_ANAPHORA_RE, (_, pron) => pron);
|
|
346
398
|
q = q.replace(G_DROP, "$1ing");
|
|
347
399
|
// closed preamble frames (greeting lead-in, modal wrapper, show/give-me
|
|
348
400
|
// bridge) — AFTER the correction tables (a repaired "give me"/"show me" still
|
|
@@ -414,6 +466,23 @@ export const PHRASING_FRAMES = Object.freeze([
|
|
|
414
466
|
{ re: /^(?:the\s+)?(?:members?|methods?|attributes?|contents)\s+of\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
415
467
|
// "what's in X" / "what is in X" (contraction already expanded; sha handled above)
|
|
416
468
|
{ re: /^what\s+is\s+(?:in|inside)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
469
|
+
// "what else is in X" (0.9.15 Tier-1 single-touch playtest) — the natural
|
|
470
|
+
// "besides what I already know" drill-down after a members-of-class answer.
|
|
471
|
+
// Distinct from the "what else does X <verb>" family (which the compositional
|
|
472
|
+
// grammar already tolerates, dropping "else" as noise on its own): the "is
|
|
473
|
+
// in" idiom is NOT a compositional marker, so parseComposite never sees it and
|
|
474
|
+
// "what else is in X" fell through to the strategies with NO candidate at all
|
|
475
|
+
// (neither recognizes the bare "is in" idiom once "else" sits in front of it).
|
|
476
|
+
// The only rescue was the relaxation cascade's drop-unmatched layer — but that
|
|
477
|
+
// layer refuses to accept a relaxed reading that still renders an honest EMPTY
|
|
478
|
+
// (by design: relaxation must turn a miss into a real answer, never into
|
|
479
|
+
// another kind of miss), so a genuinely empty class ("what else is in
|
|
480
|
+
// Task.complete" — a method, no members) bottomed out at the bare grammar
|
|
481
|
+
// wall instead of the specific "no contains edges" receipt. Routing this
|
|
482
|
+
// frame onto the SAME direct "what does X contain" path the plain "what is
|
|
483
|
+
// in X" frame above already uses sidesteps the cascade's conservative gate
|
|
484
|
+
// entirely, so a real empty is reported honestly instead of walled.
|
|
485
|
+
{ re: /^what\s+else\s+is\s+(?:in|inside)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
|
|
417
486
|
|
|
418
487
|
// WHERE-DEFINED → "where is X defined". PAST TENSE ONLY ("what defined X", "what
|
|
419
488
|
// declared X"): the PRESENT "what defines X" already parses as a reverse-defines
|
|
@@ -512,6 +581,26 @@ export const PHRASING_FRAMES = Object.freeze([
|
|
|
512
581
|
// survey the bare "what is untested" frame lands on. Closed to the tests/coverage
|
|
513
582
|
// object, so it can't swallow a general "what needs X".
|
|
514
583
|
{ re: /^what\s+needs\s+(?:to\s+be\s+)?(?:a\s+)?(?:tested|tests?|testing|coverage|covering)\??$/i, to: () => "untested modules" },
|
|
584
|
+
|
|
585
|
+
// DOES-X-VERB-ANYTHING-ELSE → the plain forward "what does X <verb>" listing
|
|
586
|
+
// (0.9.15 Tier-1 single-touch playtest). A very natural drill-down follow-up
|
|
587
|
+
// after a relation answer — "does listTasks call anything else", "does
|
|
588
|
+
// src/handlers/tasks.mjs import something else" — used to dead-end: "anything"/
|
|
589
|
+
// "something" [else] is a placeholder standing in for "the rest of the list",
|
|
590
|
+
// not a real object term, but the two parse strategies disagreed on the SPAN
|
|
591
|
+
// (grammar kept "anything else" whole as the object, keyword-spot dropped
|
|
592
|
+
// "anything" and kept only "else"), landing on the {ambiguousParse} surface —
|
|
593
|
+
// two nonsense readings offered as if one might be right. "what does X <verb>"
|
|
594
|
+
// is the exact working canonical shape (see the MEMBERS-of-class frames above),
|
|
595
|
+
// so rewriting the whole closed pattern onto it sidesteps the disagreement
|
|
596
|
+
// instead of teaching either strategy's tokenizer to special-case "else".
|
|
597
|
+
// Anchored to the closed VERB_TO_KIND vocabulary so it can never swallow a
|
|
598
|
+
// genuine named object that happens to start with "any"/"some" (only the bare
|
|
599
|
+
// placeholder nouns "anything"/"something", optionally trailed by "else", match).
|
|
600
|
+
{
|
|
601
|
+
re: new RegExp(`^(?:do|does)\\s+(.+?)\\s+(${VERB_ALTERNATION})\\s+(?:anything|something)(?:\\s+else)?\\??$`, "i"),
|
|
602
|
+
to: (m) => `what does ${m[1]} ${m[2]}`,
|
|
603
|
+
},
|
|
515
604
|
]);
|
|
516
605
|
|
|
517
606
|
/** Apply the phrasing frames (members-of-class + where-defined) — first match wins
|