@polycode-projects/seonix 0.2.1 → 0.4.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.
@@ -0,0 +1,73 @@
1
+ // ask-nlp.mjs — the OPTIONAL wink-nlp adapter behind ask.mjs's lemma/POS tier.
2
+ //
3
+ // BOUNDARY (hard, do not move): this file is Node-only and is NEVER inlined into
4
+ // the viewer bundle. viz.mjs's askSource() inlines codegraph.mjs + ask-vocab.mjs +
5
+ // ask.mjs ONLY and strips their import lines, so the portable single-file HTML has
6
+ // no wink, no model, and no `nlpAdapter` binding at all — ask.mjs reaches this
7
+ // module exclusively through a `typeof nlpAdapter === "function"` guard and
8
+ // degrades to adapter-less parsing (lemma/POS tiers off; the curated tables and
9
+ // the bounded edit-distance tier still work, browser and Node alike). Keeping the
10
+ // ~1MB CJS model out of the page is the point of the split.
11
+ //
12
+ // wink-nlp and wink-eng-lite-web-model are CJS — loaded via createRequire, the
13
+ // same Node-module-resolution approach viz.mjs uses to locate cytoscape (resolve
14
+ // through the module system, never a guessed path). The require happens lazily on
15
+ // first use and failure is cached as null: a checkout without the optional deps
16
+ // installed answers exactly like the browser bundle, it never throws.
17
+
18
+ import { createRequire } from "node:module";
19
+
20
+ let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
21
+
22
+ /** Lazily build the {lemma, posTags} adapter, or null when wink isn't loadable.
23
+ * Deterministic: wink-nlp's tagger/lemmatiser is a fixed model with no sampling,
24
+ * so the same input always yields the same tags/lemmas across processes. */
25
+ export function nlpAdapter() {
26
+ if (cached !== undefined) return cached;
27
+ try {
28
+ const require = createRequire(import.meta.url);
29
+ const winkNLP = require("wink-nlp");
30
+ const model = require("wink-eng-lite-web-model");
31
+ const nlp = winkNLP(model);
32
+ const its = nlp.its;
33
+ cached = {
34
+ /** Lowercase lemma of a single token ("imported" -> "import"); the word
35
+ * itself when wink has nothing better (unknown words come back as-is). */
36
+ lemma(word) {
37
+ const w = String(word || "");
38
+ try {
39
+ const out = nlp.readDoc(w).tokens().out(its.lemma);
40
+ return String(out[0] || w).toLowerCase();
41
+ } catch {
42
+ return w.toLowerCase();
43
+ }
44
+ },
45
+ /** UPOS tags aligned to the CALLER's word array. wink re-tokenizes (it
46
+ * splits "walk.mjs" into three tokens), so each input word is greedily
47
+ * matched to the run of wink tokens that spell it and takes its FIRST
48
+ * sub-token's tag; null per word on any surprise, never a throw. */
49
+ posTags(words) {
50
+ try {
51
+ const toks = nlp.readDoc(words.join(" ")).tokens();
52
+ const texts = toks.out();
53
+ const tags = toks.out(its.pos);
54
+ const out = [];
55
+ let k = 0;
56
+ for (const w of words) {
57
+ if (k >= texts.length) { out.push(null); continue; }
58
+ out.push(tags[k]);
59
+ let acc = texts[k];
60
+ k += 1;
61
+ while (acc.length < w.length && k < texts.length) { acc += texts[k]; k += 1; }
62
+ }
63
+ return out;
64
+ } catch {
65
+ return words.map(() => null);
66
+ }
67
+ },
68
+ };
69
+ } catch {
70
+ cached = null;
71
+ }
72
+ return cached;
73
+ }
package/src/ask-vocab.mjs CHANGED
@@ -35,8 +35,9 @@ export const RELATIONS = {
35
35
  imports: {
36
36
  comment: "Module -> Module: subject's import graph references object (usesComplexType).",
37
37
  verbs: [
38
- // formal/neutral
39
- "couples to", "couple to", "uses", "use", "depends on", "imports", "import",
38
+ // formal/neutral ("uses"/"use" moved to the `uses` union family, 2026-07-02 —
39
+ // "uses code from" stays here: its phrasing is specifically import-flavored)
40
+ "couples to", "couple to", "depends on", "imports", "import",
40
41
  "relies on", "rely on", "requires", "require", "references", "reference",
41
42
  "pulls in", "pull in", "built on", "builds on", "build on", "uses code from",
42
43
  // casual/colloquial
@@ -46,6 +47,21 @@ export const RELATIONS = {
46
47
  "importing",
47
48
  ],
48
49
  },
50
+ // "uses" is a QUERY-side union family, not a stored predicate (2026-07-02 query
51
+ // families): "what uses X" honestly means BOTH the import graph and the call
52
+ // graph, so ask.mjs traverses it as imports + calls + callsSymbol together
53
+ // (KIND_UNIONS there). The verbs moved here FROM imports — "which modules use X"
54
+ // still answers with the importing modules (the asked Module grain filters the
55
+ // union down to module-grain subjects), and "what uses <function>" now also
56
+ // reaches the symbol-grain callers instead of silently ignoring them.
57
+ uses: {
58
+ comment: "query-side union: imports (Module->Module) + calls (Module->Module) + callsSymbol (fn->fn).",
59
+ verbs: [
60
+ "uses", "use", "used by", "makes use of", "make use of",
61
+ // gerund (g-drop normalization — §3.5)
62
+ "using",
63
+ ],
64
+ },
49
65
  calls: {
50
66
  comment: "Function/Method -> Function/Class (symbol-grain) or Module -> Module (coarse): subject invokes object.",
51
67
  verbs: [
@@ -93,8 +109,13 @@ export const RELATIONS = {
93
109
  "derives from", "derive from", "is a subclass of", "are a subclass of",
94
110
  "is a kind of", "are a kind of", "is built off", "are built off",
95
111
  "is built on top of", "are built on top of",
96
- // gerund (g-drop normalization — §3.5)
97
- "extending",
112
+ // gerund (g-drop normalization — §3.5, and the compositional grammar's
113
+ // gerund-led boolean gate: "classes inheriting from Base but not tested").
114
+ // Both the two-word "inheriting from" (so "from" is consumed into the verb
115
+ // phrase, not the object term) and the bare "inheriting" (the single token
116
+ // the gerund-lead check reads) are listed; longest-match-first prefers the
117
+ // two-word form when "from" follows.
118
+ "extending", "inheriting from", "inheriting", "subclassing", "extends from",
98
119
  ],
99
120
  },
100
121
  touches: {
@@ -104,6 +125,27 @@ export const RELATIONS = {
104
125
  "was changed in", "were changed in", "was edited in", "were edited in",
105
126
  "was modified by", "were modified by", "was tweaked in", "were tweaked in",
106
127
  "got changed in", "got edited in",
128
+ // commit-question forms (2026-07-02, viewer commit-chat fix): the same touch
129
+ // relation asked from the commit's side — "which changes touch commit <sha>",
130
+ // "what did commit <sha> touch", "which functions changed in <sha>", "which
131
+ // changes landed in commit <sha>", "what was touched by commit <sha>". Bare
132
+ // "touch" completes the touched/touches pair for the "did … touch" auxiliary
133
+ // form; the "by"/"in" phrases are the passive/locative counterparts of forms
134
+ // already above (curated per register spread, not a thesaurus dump).
135
+ "touch", "touched by", "modified by", "changed by", "changed in",
136
+ "landed in", "land in",
137
+ // contents-of-a-commit forms (2026-07-02, operator screenshot: "what was in
138
+ // commit <sha>" missed on the live site). "was in"/"went into" only read as
139
+ // touch questions when the object is a commit — the sha-shaped object keeps
140
+ // them from firing on structural questions ("is X in the graph" has no verb
141
+ // match anyway). Judgment call: "is in" omitted — too generic without the
142
+ // past-tense anchor and risks matching containment phrasings.
143
+ "was in", "were in", "went into", "included in",
144
+ // when-question forms (2026-07-02 query families): "when was X last
145
+ // updated/edited" — bare past participles that only read naturally in the
146
+ // temporal shape; the when template routes them, but they are ordinary
147
+ // touches verbs so "which modules were updated ..." keeps working too.
148
+ "updated", "edited",
107
149
  // gerund (g-drop normalization — §3.5)
108
150
  "touching",
109
151
  ],
@@ -118,15 +160,31 @@ export const RELATIONS = {
118
160
  ],
119
161
  },
120
162
  reexports: {
121
- comment: "Module -> Module: subject re-exports a name it imported from object.",
163
+ comment: "Module -> exported symbol: subject's public API surface (__all__/export list).",
122
164
  verbs: [
123
165
  "exports", "export", "re-exports", "re-export", "passes through", "pass through",
166
+ // API-surface phrasing (2026-07-02 query families): "what does <module>
167
+ // expose". Bare "exposed" is NOT listed — the lemma tier maps it here when
168
+ // an adapter is present, and "how does the API get exposed" has no
169
+ // traversal either way (pinned as an honest miss in the tests).
170
+ "exposes", "expose",
124
171
  // gerund (g-drop normalization — §3.5)
125
172
  "exporting",
126
173
  ],
127
174
  },
128
175
  };
129
176
 
177
+ // ---- where/when/mentions markers (2026-07-02 query families) — the location and
178
+ // prose-mention questions carry NO relation verb ("where is X defined", "where is
179
+ // X mentioned"), so ask.mjs routes them by these marker words instead of
180
+ // VERB_TO_KIND. Closed lists, same curation discipline as everything above. ----
181
+
182
+ /** Definition-location markers: "where is X <marker>" (or bare "where is X"). */
183
+ export const WHERE_MARKERS = Object.freeze(["defined", "declared", "located", "implemented"]);
184
+
185
+ /** Prose-mention markers: "where is X <marker>" -> the prose/mentions surface. */
186
+ export const MENTION_MARKERS = Object.freeze(["mentioned", "referenced"]);
187
+
130
188
  /** relation token -> flat verb-phrase list, the shape ask.mjs's VERB_TO_KIND
131
189
  * table needs (phrase -> kind), derived once from RELATIONS. */
132
190
  export const VERB_TO_KIND = Object.freeze(
@@ -142,6 +200,18 @@ export const ENTITY_TO_TYPE = Object.freeze({
142
200
  module: "Module", modules: "Module", file: "Module", files: "Module",
143
201
  attribute: "Attribute", attributes: "Attribute", field: "Attribute", fields: "Attribute",
144
202
  variable: "GlobalVariable", variables: "GlobalVariable", global: "GlobalVariable", globals: "GlobalVariable",
203
+ // "changes" in a touch question ("which changes touch commit <sha>") means the
204
+ // code entities on the other end of the touch edges, at WHATEVER grain the graph
205
+ // recorded — module (touches) and symbol (touchesSymbol) together when the commit
206
+ // is the given side, and the touching commits themselves when a module/symbol is
207
+ // the given side. Mapped to the pseudo-type "Change" (not a node class): ask.mjs's
208
+ // traverse() reads it as a wildcard over the touch traversal's results rather than
209
+ // aliasing it to ONE real class and silently dropping the other grain of the
210
+ // answer. Listed BEFORE commit/commits: findPhrase (ask.mjs) takes the first
211
+ // same-length phrase in table order, so in "which changes touch commit <sha>" the
212
+ // entity slot must consume "changes" and leave "commit <sha>" intact as the
213
+ // object term.
214
+ change: "Change", changes: "Change",
145
215
  commit: "Commit", commits: "Commit",
146
216
  });
147
217
 
@@ -173,6 +243,83 @@ export const CONTRACTIONS = Object.freeze({
173
243
  "gimme": "give me",
174
244
  });
175
245
 
246
+ // ---- misspellings and wrong words (2026-07-02, two-level fuzzy work) — these are
247
+ // CORRECTIONS, not synonyms: the asker typed a broken or incorrect surface form of
248
+ // a word this grammar already owns, and we restore the canonical form BEFORE
249
+ // parsing (normalizeQuery applies both tables like CONTRACTIONS: word-boundary,
250
+ // longest key first, case-insensitive). Synonyms belong in RELATIONS/ENTITY_TO_TYPE;
251
+ // these tables exist so a typo'd or misused word maps to canonical language
252
+ // deterministically, ahead of (and more precisely than) ask.mjs's bounded
253
+ // edit-distance fallback. ask.mjs's correction regex refuses to rewrite a word
254
+ // glued to a dotted extension ("revision.mjs" stays a module name), since
255
+ // WRONG_WORDS entries are real English words that plausibly name modules; a
256
+ // bare standalone token that happens to be a real identifier remains the
257
+ // accepted residual exposure, same as CONTRACTIONS. ----
258
+
259
+ /** Curated common typos of THIS vocabulary's own keywords — entity nouns, relation
260
+ * verbs, and the grammar's anchor words (which/what/does/the): a typo'd anchor
261
+ * kills the anchored templates outright, so anchors earn entries too. Values are
262
+ * always the canonical word. Judgment calls logged: "calss" maps to "class" (the
263
+ * doubled-s slip of "class"), NOT "calls", though both are edit-distance 1 — the
264
+ * exact tie the generic fuzzy tier must refuse to break; a curated table is where
265
+ * that call is made deliberately. "dose" is a real English word, but at a word
266
+ * boundary in this closed question grammar ("dose X import Y") the intent is
267
+ * unambiguous. */
268
+ export const MISSPELLINGS = Object.freeze({
269
+ // entity nouns
270
+ "funtion": "function", "funtions": "functions",
271
+ "fucntion": "function", "fucntions": "functions",
272
+ "functoin": "function", "functoins": "functions",
273
+ "calss": "class", "calsses": "classes", "classs": "class", "clases": "classes",
274
+ "modul": "module", "moduls": "modules",
275
+ "moduel": "module", "moduels": "modules",
276
+ "moudle": "module", "moudles": "modules",
277
+ "methdo": "method", "methdos": "methods",
278
+ "mehtod": "method", "mehtods": "methods",
279
+ "comit": "commit", "comits": "commits",
280
+ "commmit": "commit", "commmits": "commits",
281
+ "varaible": "variable", "varaibles": "variables",
282
+ "varibale": "variable", "varibales": "variables",
283
+ "atribute": "attribute", "atributes": "attributes",
284
+ // relation verbs
285
+ "improt": "import", "improts": "imports",
286
+ "imoprt": "import", "imoprts": "imports",
287
+ "inherts": "inherits", "inheirts": "inherits",
288
+ "extands": "extends", "extneds": "extends",
289
+ "depnds": "depends",
290
+ "touchs": "touches", "tuoches": "touches", "touhced": "touched",
291
+ "chagned": "changed", "chnaged": "changed",
292
+ "chagnes": "changes", "chnages": "changes",
293
+ "calles": "calls",
294
+ "exprot": "export", "exprots": "exports",
295
+ "tets": "tests",
296
+ // grammar anchor words
297
+ "whcih": "which", "wich": "which", "whihc": "which",
298
+ "waht": "what",
299
+ "dose": "does", "doess": "does",
300
+ "teh": "the",
301
+ });
302
+
303
+ /** Words used INCORRECTLY but with clear intent, mapped to the canonical schema
304
+ * term — used like synonyms by the asker, but they are corrections of usage, not
305
+ * alternative names (which is why they live here and not in ENTITY_TO_TYPE).
306
+ * Only mapped where intent is unambiguous in a code-graph question; words
307
+ * deliberately NOT mapped are logged in the omitted-on-purpose block at the end
308
+ * of this file. */
309
+ export const WRONG_WORDS = Object.freeze({
310
+ // neither a folder nor a directory grain exists in this graph — in "which
311
+ // folders import X" the only honest referent is the module/file grain.
312
+ "folder": "module", "folders": "modules",
313
+ "directory": "module", "directories": "modules",
314
+ // unambiguous abbreviation of an entity noun this grammar owns
315
+ "func": "function", "funcs": "functions",
316
+ // VCS terms whose canonical schema class here is Commit
317
+ "changeset": "commit", "changesets": "commits",
318
+ "revision": "commit", "revisions": "commits",
319
+ // code-graph "property" is the Attribute grain in this schema
320
+ "property": "attribute", "properties": "attributes",
321
+ });
322
+
176
323
  /** Trailing g-drop ("callin'", "hittin'") — a plain suffix restore, not a
177
324
  * lemmatiser: -in' -> -ing, applied per-word during normalization. The
178
325
  * apostrophe is REQUIRED (not optional): bare "-in" endings with no
@@ -223,6 +370,27 @@ export const NEGATION_FRAMES = Object.freeze([
223
370
  { re: /\b(?:nobody|nothing|no one)\s+(\S.*?)(?:,?\s+does\s+it\??|,?\s+do\s+they\??)?$/i, to: (m) => `what ${m[1]}` },
224
371
  ]);
225
372
 
373
+ // ---- commit-content frames (2026-07-02, operator screenshot: "what was in commit
374
+ // ef74e44e25c8" missed on the live site) — "what was/is in commit <sha>", "what's
375
+ // in <sha>" (the contraction table has already expanded "what's" by the time
376
+ // frames run), "what went into <sha>", "what made it into commit <sha>" all mean
377
+ // the canonical commit-subject question "what did <sha> touch", so they are
378
+ // REWRITTEN to it before either parse strategy runs: the same mechanism and scope
379
+ // discipline as NEGATION_FRAMES (a small closed pattern set, first match wins),
380
+ // which also makes both strategies parse the family for free. The sha-shaped tail
381
+ // is REQUIRED so these frames only ever fire on an actual commit reference: over a
382
+ // non-sha object ("what was in walk.mjs") the frame does not match and the text is
383
+ // left for the ordinary grammar to handle as it already does, rather than being
384
+ // bent into a commit-subject touches query that would then blank. ----
385
+ export const COMMIT_CONTENT_FRAMES = Object.freeze([
386
+ // "what was in commit ef74e44e25c8" / "what is in ef74e44e" / "what's in commit <sha>"
387
+ { re: /^what\s+(?:is|was|were|are)\s+in\s+((?:commit\s+)?[0-9a-f]{7,40})\??$/i, to: (m) => `what did ${m[1]} touch` },
388
+ // "what went into commit ef74e44e25c8" / casual "what got into <sha>"
389
+ { re: /^what\s+(?:went|got)\s+into\s+((?:commit\s+)?[0-9a-f]{7,40})\??$/i, to: (m) => `what did ${m[1]} touch` },
390
+ // "what made it into commit ef74e44e25c8"
391
+ { re: /^what\s+made\s+it\s+into\s+((?:commit\s+)?[0-9a-f]{7,40})\??$/i, to: (m) => `what did ${m[1]} touch` },
392
+ ]);
393
+
226
394
  // ---- §7 meta-vocabulary — trigger phrases for the "meta" query shape (asking what a
227
395
  // graph vocabulary term itself means: "what does cochange mean", "what does mgx:
228
396
  // callsSymbol mean"). Deliberately NOT folded into RELATIONS/VERB_TO_KIND: these
@@ -238,6 +406,197 @@ export const META_MEANING_VERBS = Object.freeze([
238
406
  "represent", "represents", "refer to", "refers to",
239
407
  ]);
240
408
 
409
+ // ---- §compositional grammar vocabulary (PLAN §5.16 P3 — the ask engine's step up
410
+ // from ELIZA keyword-spotting to a real recursive-descent grammar over CLAUSES).
411
+ // ask.mjs's parseComposite reads these tables to recognize the compositional
412
+ // MARKERS — relative clauses, boolean connectives, subject qualifiers, aggregates,
413
+ // superlatives, and anaphora — that compose the SAME closed clause vocabulary
414
+ // (RELATIONS/ENTITY_TO_TYPE above) into multi-hop / set-algebra queries. The
415
+ // grammar COMPOSES the closed vocabulary; it never opens it. Every table here is
416
+ // curated + closed, same discipline as everything above: a marker the tables don't
417
+ // carry is an honest miss, never a guess. ----
418
+
419
+ /** Relative-clause introducers: "<noun> that/which/who <predicate>". Only ever
420
+ * read as a relative pronoun when it sits AFTER a noun and BEFORE a predicate
421
+ * (ask.mjs enforces both) — "that" is also a CONTEXT_PRONOUN ("what calls that"),
422
+ * so position, not mere presence, decides. This gates the nested/relative and
423
+ * same-subject boolean shapes: their absence is exactly what keeps the bare
424
+ * reverse-template query "which classes extends Base and couples to logging" out
425
+ * of the compositional path (it stays the existing two-strategy ambiguous parse). */
426
+ export const RELATIVE_PRONOUNS = Object.freeze(["that", "which", "who"]);
427
+
428
+ /** Placeholder object nouns — the indefinite "something" in "what calls something
429
+ * that imports X": they name NO entity, they stand in for the inner clause's
430
+ * result set. Mapped to entityType null (any grain); a real entity noun in the
431
+ * same slot ("what calls modules that import X") narrows the grain instead. */
432
+ export const PLACEHOLDER_NOUNS = Object.freeze([
433
+ "something", "anything", "everything", "somethings", "things", "thing",
434
+ "entities", "entity", "nodes", "node", "stuff", "code",
435
+ // "symbol(s)" is a grain-agnostic stand-in for any code entity — "exported
436
+ // symbols of X" means whatever X defines, at any grain, filtered by the qualifier.
437
+ "symbol", "symbols",
438
+ ]);
439
+
440
+ /** Boolean connectives over same-subject clauses -> a set operation on result ids.
441
+ * "and" = intersection, "or" = union, "but not"/"and not"/"without"/"except" =
442
+ * difference. Multi-word keys are matched longest-first by ask.mjs so "but not"
443
+ * wins over a bare "not". Left-associative in ask.mjs's fold. */
444
+ export const BOOLEAN_CONNECTIVES = Object.freeze({
445
+ "but not": "difference", "and not": "difference", "except": "difference",
446
+ "without": "difference",
447
+ "and": "intersection", "plus": "intersection",
448
+ "or": "union",
449
+ });
450
+
451
+ /** Subject QUALIFIERS (adjectives) -> a post-filter over the result set, read off
452
+ * attributes/edges the graph already carries (codegraph.mjs): `visibility`
453
+ * (private/protected present; public is the default/absent), `isStatic`/
454
+ * `isConstant`/`isAbstract` boolean attrs, the reexports edge set (exported), and
455
+ * the tests edge set (tested/untested). `isAbstract` is never populated by any
456
+ * extractor today (codegraph.mjs says so) — "abstract methods" therefore honestly
457
+ * returns an empty set, not an error, exactly like any other zero-hit answer. An
458
+ * UNKNOWN qualifier is an honest miss naming these supported ones (ask.mjs). */
459
+ export const QUALIFIERS = Object.freeze({
460
+ public: { via: "visibility", value: "public" },
461
+ private: { via: "visibility", value: "private" },
462
+ protected: { via: "visibility", value: "protected" },
463
+ static: { via: "attr", attr: "isStatic" },
464
+ abstract: { via: "attr", attr: "isAbstract" },
465
+ constant: { via: "attr", attr: "isConstant" },
466
+ exported: { via: "exported" },
467
+ "re-exported": { via: "exported" },
468
+ tested: { via: "tested", value: true },
469
+ covered: { via: "tested", value: true },
470
+ untested: { via: "tested", value: false },
471
+ uncovered: { via: "tested", value: false },
472
+ });
473
+
474
+ /** Aggregate/count triggers: "how many <kind> …", "count <kind>s", "number of
475
+ * <kind>". Answered by counting a class of individuals or a clause's result set —
476
+ * no header magic, a straight count over the graph (ask.mjs). */
477
+ export const AGGREGATE_TRIGGERS = Object.freeze(["how many", "how much", "count", "number of", "count of"]);
478
+
479
+ /** Superlative extremes -> ranking direction. "most/greatest/highest/biggest/
480
+ * largest/most-connected" rank descending; "fewest/least/smallest/lowest" rank
481
+ * ascending. Read by ask.mjs's parseSuperlative alongside EDGE_NOUN_TO_METRIC. */
482
+ export const SUPERLATIVE_EXTREMES = Object.freeze({
483
+ most: "most", greatest: "most", highest: "most", biggest: "most",
484
+ largest: "most", "most-connected": "most", "most connected": "most",
485
+ fewest: "fewest", least: "fewest", smallest: "fewest", lowest: "fewest",
486
+ });
487
+
488
+ /** Degree-metric nouns for superlatives ("which module has the most <noun>") ->
489
+ * {kind, dir} over the SAME classified edge groups. dir "out" counts edges where
490
+ * the ranked entity is the subject (its own imports/calls); "in" counts edges
491
+ * where it is the object (its importers/callers/tests). "connections"/"edges"/
492
+ * "connected" is the total (both directions, all structural kinds). A `sibling`
493
+ * fine-grained kind is added to the tally when present (callers include the
494
+ * symbol-grain callsSymbol edges, not just module-coarse calls); a `filter`
495
+ * restricts the counted objects to one class ("methods"). */
496
+ export const EDGE_NOUN_TO_METRIC = Object.freeze({
497
+ imports: { kind: "imports", dir: "out" },
498
+ dependencies: { kind: "imports", dir: "out" },
499
+ importers: { kind: "imports", dir: "in" },
500
+ dependents: { kind: "imports", dir: "in" },
501
+ callers: { kind: "calls", dir: "in", sibling: "callsSymbol" },
502
+ callees: { kind: "calls", dir: "out", sibling: "callsSymbol" },
503
+ calls: { kind: "calls", dir: "out", sibling: "callsSymbol" },
504
+ methods: { kind: "contains", dir: "out", filter: "Method" },
505
+ members: { kind: "contains", dir: "out" },
506
+ tests: { kind: "tests", dir: "in" },
507
+ subclasses: { kind: "inherits", dir: "in" },
508
+ connections: { kind: "*", dir: "both" },
509
+ edges: { kind: "*", dir: "both" },
510
+ connected: { kind: "*", dir: "both" },
511
+ });
512
+
513
+ /** Anaphora triggers over the PREVIOUS result set (ask()'s `prev` id array):
514
+ * "which of those/them/these …", "how many of those …". The pronoun refers to the
515
+ * last answer's ids, not a graph term — with no prev supplied it is an honest miss
516
+ * (ask.mjs), never a guess, exactly like an unresolved context pronoun. */
517
+ export const ANAPHORA_TRIGGERS = Object.freeze(["those", "them", "these"]);
518
+
519
+ /** Membership relations for "<entity> of/in <term>" (qualifier/relative inner
520
+ * clauses like "public methods of Widget", "untested functions in walk.mjs") ->
521
+ * the forward edge kinds whose subject is <term> and whose objects are the
522
+ * members. contains (Class->member) and defines (Module->symbol) are both tried;
523
+ * the asked entity type narrows the result. */
524
+ export const MEMBERSHIP_KINDS = Object.freeze(["contains", "defines"]);
525
+
526
+ // ---- §progressive-relaxation cascade vocabulary (SHRDLU-in-a-code-graph, with a
527
+ // Zork parser's forgiveness) — the three closed, curated tables ask.mjs's relaxParse
528
+ // reads when the DIRECT parse of a query would MISS. The cascade only ever DROPS
529
+ // noise/unmatched words or NORMALISES a near-canonical word to the closed vocabulary;
530
+ // it never invents a term or guesses an entity, and it bottoms out in the same honest
531
+ // rephrase hint. Every entry is hand-curated with inline provenance, same "closed is
532
+ // deliberate" ethos as everything above: a word these tables don't carry is left for
533
+ // the honest miss, never a general-English stoplist that would silently eat a real
534
+ // code term. ----
535
+
536
+ /** Politeness / filler / vocative / presentation-frame tokens the cascade may strip
537
+ * ONE AT A TIME (leftmost first) when a query misses — but only ever a token that is
538
+ * NOT itself a content vocabulary word (a relation verb, entity noun, modifier,
539
+ * qualifier, aggregate/superlative trigger, …) and does NOT resolve to a graph entity
540
+ * (ask.mjs guards both), so a module literally named "show" or "the" is never eaten.
541
+ * This is a SUPERSET of the multi-word politeness FILLER_WORDS strips up-front during
542
+ * normalization: those handle "could you"/"tell me"/"please" before either parse runs;
543
+ * these single tokens catch what a spoken-style question keeps AFTER that pass — the
544
+ * bare vocative ("matey"), the article ("the"/"a"), and the presentation frame words
545
+ * ("show"/"me"/"list") that the compositional grammar already skips as FRAME_WORDS but
546
+ * the keyword-spotting strategy does not, so an un-stripped "show me" otherwise
547
+ * decomposes to a bogus ask{subject:"show me"}. Curated, not a general stoplist:
548
+ * question words (what/which/…), connectives (and/or), and pronouns (this/it/that)
549
+ * are deliberately ABSENT — they carry grammatical weight and must survive. */
550
+ export const CASCADE_NOISE = Object.freeze([
551
+ // articles / vague determiners (kept OUT of content vocab so they're strippable;
552
+ // the aggregate/where parsers already tolerate a stray "the"/"a", so stripping is
553
+ // belt-and-braces, not load-bearing)
554
+ "the", "a", "an", "some",
555
+ // politeness / hedges (single-token; multi-word "could you"/"please" etc. are
556
+ // FILLER_WORDS, stripped earlier during normalization)
557
+ "please", "pls", "plz", "kindly", "just", "simply", "maybe", "perhaps",
558
+ "thanks", "thank", "ta", "cheers",
559
+ // greetings a question sometimes opens with (chat.mjs owns standalone greetings;
560
+ // here they're only stripped when embedded in an otherwise-real question)
561
+ "hi", "hello", "hey", "yo", "hiya", "howdy", "ok", "okay",
562
+ // vocatives / terms of address (the "matey" of the worked example, and its kin)
563
+ "matey", "mate", "buddy", "pal", "dude", "man", "bro", "bru", "fam",
564
+ "friend", "sir", "maam", "folks", "guys", "everyone", "dear",
565
+ // presentation frames — the keyword-spotting strategy's blind spot: the
566
+ // compositional grammar skips these as FRAME_WORDS, but "show me what imports X"
567
+ // otherwise decomposes (via keyword-spot) to ask{subject:"show me"}. Stripping
568
+ // them on a miss recovers the underlying reverse/forward question. ("count" is NOT
569
+ // here — it is an aggregate trigger; "find"/"search" are here as presentation
570
+ // verbs, not the seonix_search tool, which ask.mjs never dispatches.)
571
+ "show", "tell", "give", "list", "find", "me", "us", "lemme",
572
+ ]);
573
+
574
+ /** Near-canonical words the cascade REWRITES to the closed vocabulary once noise and
575
+ * unmatched tokens are gone (SYNONYM-NORMALISE, the cascade's third layer). Kept
576
+ * deliberately TINY and aggregate-flavoured: the relation/entity synonyms a developer
577
+ * actually types already live in RELATIONS/ENTITY_TO_TYPE (and "uses"/"depends on" are
578
+ * mapped there); this table only closes the gap for the count family, whose triggers
579
+ * (AGGREGATE_TRIGGERS) don't include the "tally the classes"/"total number of classes"
580
+ * register. Each key is also kept OUT of the drop pass (ask.mjs treats a synonym key as
581
+ * meaningful) so it survives to be normalised rather than dropped as unmatched. The
582
+ * rewrite is guarded by ask.mjs (never applied to a token that resolves to a graph
583
+ * entity), so a symbol named "total" is never bent into "count". */
584
+ export const CASCADE_SYNONYMS = Object.freeze({
585
+ tally: "count", tallies: "count", sum: "count", total: "count", totals: "count",
586
+ });
587
+
588
+ /** Explicit help / orientation requests — when the WHOLE query is one of these, ask.mjs
589
+ * shows the rephrase hint DIRECTLY (the honest bottom of the cascade, reached on
590
+ * demand) rather than pretending to answer or running the relaxation loop. A closed
591
+ * set matched against the whole normalized query only, so "which functions call help"
592
+ * (a real question about a symbol named "help") is untouched. Standalone greetings and
593
+ * the chat "/help" command are chat.mjs's own surface; this is the bare CLI/MCP ask()
594
+ * entry point's equivalent. */
595
+ export const HELP_TRIGGERS = Object.freeze([
596
+ "help", "help me", "how do i ask", "how do i use this", "what can i ask",
597
+ "what can you ask", "usage", "commands", "examples", "syntax", "options",
598
+ ]);
599
+
241
600
  // ---- omitted-on-purpose (judgment calls, not oversights) -------------------
242
601
  // "runs"/"executes" (calls) are common English words with many non-code
243
602
  // senses — accepted anyway, formal-template AND keyword-spotting alike,
@@ -256,3 +615,9 @@ export const META_MEANING_VERBS = Object.freeze([
256
615
  // alongside"), so g-drop normalization has no bare stem to dialectally
257
616
  // contract in the first place; only relations with a genuinely bare-verb
258
617
  // casual form ("call", "touch") needed one.
618
+ // WRONG_WORDS not mapped (judgment calls): "script(s)" (routinely names a real
619
+ // module/identifier — rewriting it could corrupt an object term, and file/files
620
+ // already covers the honest synonym); "package(s)" (a genuinely coarser grain
621
+ // this graph doesn't model — mapping it to module would silently answer a
622
+ // different question than the one asked); "fn" (too short and too often a real
623
+ // identifier fragment to rewrite at a word boundary).