@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.5

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/src/ask.mjs CHANGED
@@ -31,7 +31,7 @@ import { parseAnchored } from "./interpret/strategies/grammar.mjs";
31
31
  import { parseKeywordSpot, findPhrase } from "./interpret/strategies/keywords.mjs";
32
32
  import { runStrategiesSync } from "./interpret/pipeline.mjs";
33
33
  import { mergeStrategyResults, alternateLines } from "./interpret/merge.mjs";
34
- import { lookupByProseTokens } from "./prose.mjs";
34
+ import { lookupByProseTokens, splitIdentifierWords } from "./prose.mjs";
35
35
  import { pickPhrase } from "./answer-variants.mjs";
36
36
 
37
37
  // Normalization stays importable from its original site (tests + chat surface).
@@ -88,6 +88,20 @@ function nounFor(entityType, n) {
88
88
  return n === 1 ? s : p;
89
89
  }
90
90
 
91
+ /** A class enum rendered as prose words ("GlobalVariable" -> "global
92
+ * variable"), lowercase to match nounFor's own convention. For the render
93
+ * sites that must name the enum itself rather than a curated PLURAL_FORMS
94
+ * noun. The typeof guard is the same viewer-bundle boundary the tier-4 prose
95
+ * fallback documents: viz.mjs's askSource strips the prose.mjs import. */
96
+ export function classDisplayName(cls) {
97
+ const s = String(cls || "");
98
+ if (typeof splitIdentifierWords === "function") {
99
+ const words = splitIdentifierWords(s).join(" ");
100
+ if (words) return words;
101
+ }
102
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
103
+ }
104
+
91
105
  // Every relation kind is already the correct 3rd-person-singular verb form
92
106
  // except "cochange" ("X cochanges Y") and "reexports" (human word "export").
93
107
  const REVERSE_MISS_VERB = { cochange: "cochanges", reexports: "export" };
@@ -2342,6 +2356,43 @@ function commitTouches(graph, commit, entityType, extra = {}) {
2342
2356
  };
2343
2357
  }
2344
2358
 
2359
+ // ---- entry-point survey for the where-defined shape: "where is the (main)
2360
+ // entry point defined" names a ROLE, not a label, so resolveObject can only
2361
+ // miss it (or accidentally substring-match something unrelated). A closed
2362
+ // basename vocabulary stands in for the role; every candidate is ranked
2363
+ // deterministically and the ranking is disclosed in the answer, never a
2364
+ // silently picked single winner. ----
2365
+
2366
+ /** The where-defined object phrasings that ask for the entry-point role. */
2367
+ const ENTRY_POINT_QUERY_RE = /^(?:the\s+)?(?:main\s+|primary\s+)?entry[\s-]?points?(?:\s+(?:of|to|for)\s+(?:this|the)\s+(?:codebase|code|repo|repository|project|app))?$/i;
2368
+ /** Module basenames conventionally used as a program's entry point. */
2369
+ const ENTRY_POINT_BASENAMES = new Set(["index", "main", "app", "server", "cli", "__main__"]);
2370
+ /** Directory segments marking test/fixture territory — ranked below real code. */
2371
+ const TEST_FIXTURE_PATH_SEGMENTS = new Set(["test", "tests", "__tests__", "fixture", "fixtures", "spec", "specs", "testdata"]);
2372
+
2373
+ const moduleStemOf = (label) => String(label).toLowerCase().split("/").pop().replace(/\.[a-z0-9]+$/, "");
2374
+ const isTestFixturePath = (label) => String(label).toLowerCase().split("/").slice(0, -1)
2375
+ .some((seg) => TEST_FIXTURE_PATH_SEGMENTS.has(seg));
2376
+
2377
+ /** All entry-point-basename Modules, best first: a basename the query itself
2378
+ * names ("MAIN entry point" -> main.mjs) beats root proximity (fewer path
2379
+ * segments), which beats a non-test path over a test/fixture one; a full tie
2380
+ * falls back to label order so the ranking is stable. */
2381
+ function rankEntryPointModules(graph, term) {
2382
+ const queryWords = new Set(String(term || "").toLowerCase().split(/[\s-]+/).filter(Boolean));
2383
+ return (graph.individuals || [])
2384
+ .filter((i) => i.class === "Module" && ENTRY_POINT_BASENAMES.has(moduleStemOf(i.label)))
2385
+ .map((ind) => ({
2386
+ ind,
2387
+ named: queryWords.has(moduleStemOf(ind.label)) ? 1 : 0,
2388
+ depth: String(ind.label).split("/").length,
2389
+ fixture: isTestFixturePath(ind.label) ? 1 : 0,
2390
+ }))
2391
+ .sort((a, b) => (b.named - a.named) || (a.depth - b.depth) || (a.fixture - b.fixture)
2392
+ || String(a.ind.label).localeCompare(String(b.ind.label)))
2393
+ .map((x) => x.ind);
2394
+ }
2395
+
2345
2396
  /** Safety net: a {shape, kind, entityType} combination must be explicitly
2346
2397
  * listed here to receive real non-"direct" modifier behavior; anything else
2347
2398
  * gets an honest "not supported yet" response, never a silent fallback to
@@ -2414,6 +2465,18 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2414
2465
  };
2415
2466
  }
2416
2467
 
2468
+ // where + an entry-point ROLE phrasing: checked before object resolution,
2469
+ // which could only miss the role term or accidentally land on an unrelated
2470
+ // partial match.
2471
+ if (shape === "where" && ENTRY_POINT_QUERY_RE.test(String(parsed.object || "").trim())) {
2472
+ const ranked = rankEntryPointModules(graph, parsed.object);
2473
+ return {
2474
+ matches: ranked, objMatch: ranked[0] || null, candidates: ranked.slice(1, 5),
2475
+ ambiguous: false, entryPointShape: true,
2476
+ traversal: `Module individuals with an entry-point basename (${[...ENTRY_POINT_BASENAMES].join("/")}), ranked query-named basename first, then shallower path, then non-test path`,
2477
+ };
2478
+ }
2479
+
2417
2480
  // Checked before object resolution, so an unsupported modifier+kind
2418
2481
  // combination gets its own honest capability-gap message.
2419
2482
  if (parsed.modifier && parsed.modifier !== "direct" && !modifierIsWired(shape, kind, entityType)) {
@@ -2598,7 +2661,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2598
2661
  return {
2599
2662
  matches: [], objMatch, candidates, ambiguous, matchedVia,
2600
2663
  forwardGrainMiss: true, wantClasses: [...wantClasses],
2601
- traversal: `${fwdKinds.join("+")} edges where subject = ${objMatch.label} (grain mismatch: this "${kind}" relation never targets a ${entityType})`,
2664
+ traversal: `${fwdKinds.join("+")} edges where subject = ${objMatch.label} (grain mismatch: this "${kind}" relation never targets a ${classDisplayName(entityType)})`,
2602
2665
  };
2603
2666
  }
2604
2667
  }
@@ -2705,7 +2768,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2705
2768
  return {
2706
2769
  matches: [], objMatch: gObjMatch, candidates: gCandidates, ambiguous: gAmbiguous, matchedVia: gMatchedVia,
2707
2770
  wrongGrainMiss: true, wantClass,
2708
- 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)`,
2771
+ traversal: `"${parsed.object}" resolved to ${classDisplayName(gObjMatch.class)} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${classDisplayName(wantClass)}, and no containing module could be found to refine to)`,
2709
2772
  };
2710
2773
  }
2711
2774
  } else {
@@ -2714,7 +2777,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2714
2777
  return {
2715
2778
  matches: [], objMatch: gObjMatch, candidates: gCandidates, ambiguous: gAmbiguous, matchedVia: gMatchedVia,
2716
2779
  wrongGrainMiss: true, wantClass,
2717
- traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass})`,
2780
+ traversal: `"${parsed.object}" resolved to ${classDisplayName(gObjMatch.class)} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${classDisplayName(wantClass)})`,
2718
2781
  };
2719
2782
  }
2720
2783
  }
@@ -2764,7 +2827,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2764
2827
  } else if (entityType !== "Module" && subjects.some((s) => s.class === "Module")) {
2765
2828
  const moduleIds = new Set(subjects.filter((s) => s.class === "Module").map((s) => s.id));
2766
2829
  matches = refineToEntities(graph, moduleIds, entityType);
2767
- grainNote = `, then ${entityType} defined in the matched module(s)`;
2830
+ grainNote = `, then ${classDisplayName(entityType)} defined in the matched module(s)`;
2768
2831
  } else {
2769
2832
  matches = [];
2770
2833
  }
@@ -2883,6 +2946,7 @@ function renderCore(parsed, result) {
2883
2946
  return {
2884
2947
  content: `this could mean more than one thing:\n${options}\n(ask one of these directly, or try rephrasing more specifically, to get just that reading)`,
2885
2948
  miss: false, ambiguous: true, candidates: parsed.candidates.map(describeParse),
2949
+ candidateParses: parsed.candidates,
2886
2950
  };
2887
2951
  }
2888
2952
  // Fallback (no `result.branches` — e.g. a caller invoking render() directly
@@ -2891,6 +2955,7 @@ function renderCore(parsed, result) {
2891
2955
  return {
2892
2956
  content: `this could mean more than one thing: ${options} — try rephrasing more specifically.`,
2893
2957
  miss: false, ambiguous: true, candidates: parsed.candidates.map(describeParse),
2958
+ candidateParses: parsed.candidates,
2894
2959
  };
2895
2960
  }
2896
2961
  if (result.unresolvedPronoun) {
@@ -2967,6 +3032,25 @@ function renderCore(parsed, result) {
2967
3032
  miss: false, ambiguous: false, matches: result.matches,
2968
3033
  };
2969
3034
  }
3035
+ // entry-point survey: every candidate is disclosed with the rank order,
3036
+ // never a silently picked single winner; zero candidates is an honest miss
3037
+ // naming the closed basename vocabulary that was searched.
3038
+ if (result.entryPointShape) {
3039
+ if (!result.matches.length) {
3040
+ return {
3041
+ content: `no entry-point module found in the index — no module basename matches ${listJoin([...ENTRY_POINT_BASENAMES])}.`,
3042
+ miss: true, ambiguous: false, candidates: [],
3043
+ };
3044
+ }
3045
+ const [top, ...rest] = result.matches;
3046
+ const shownRest = rest.slice(0, OVERFLOW_CAP).map((i) => i.label);
3047
+ const extra = rest.length > OVERFLOW_CAP ? `, …and ${rest.length - OVERFLOW_CAP} more` : "";
3048
+ const also = rest.length ? ` — also matched: ${listJoin(shownRest)}${extra}` : "";
3049
+ return {
3050
+ content: `ranked ${result.matches.length} entry-point match${result.matches.length === 1 ? "" : "es"}; top: ${top.label}${also}.`,
3051
+ miss: false, ambiguous: false, matches: result.matches,
3052
+ };
3053
+ }
2970
3054
  if (!result.objMatch && (!result.candidates || result.candidates.length === 0) && parsed.shape !== "ask") {
2971
3055
  // Name what kind of thing was looked for: a sha-shaped term reads as
2972
3056
  // "commit", a dotted slash-free term as "symbol" (both keep priority over
@@ -3491,6 +3575,12 @@ function dynamicClassQuery(graph, query) {
3491
3575
  // extract the term for the article-insertion fallback rather than duplicating it.
3492
3576
  const BARE_META_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
3493
3577
 
3578
+ // "what is X for" / "what is X used for" with a bare entity term — the lead
3579
+ // refuses a/an articles and pronouns so the vocabulary phrasings ("what is a
3580
+ // horse for", "what is it for") never read as an entity term; a leading "the"
3581
+ // is entity-term noise (resolveObject's own article strip) and is dropped.
3582
+ const WHATIS_FOR_FALLBACK_RE = /^what\s+is\s+(?:the\s+)?(?!(?:an?|it|this|that|these|those)\s)(.+?)\s+(?:used\s+)?for[?.!\s]*$/i;
3583
+
3494
3584
  export function ask(graph, query, { contextId = null, nlp = undefined, prev = null } = {}) {
3495
3585
  if (isHelpRequest(query)) {
3496
3586
  return {
@@ -3540,6 +3630,29 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
3540
3630
  }
3541
3631
  }
3542
3632
  }
3633
+ // "what is X for" — the purpose paraphrase of the same bare-whatis intent,
3634
+ // excluded from the block above (and from the phrasing frames) because
3635
+ // chat.mjs's module-overview lane owns this phrasing and gates on an ask()
3636
+ // miss. The meta reading is adopted ONLY when it actually answers (a unique
3637
+ // metaFallback entity); on a miss, every byte — parsed, canonical, the miss
3638
+ // text — stays exactly as the lane cascade expects. Article/pronoun-led
3639
+ // terms ("what is a horse for") belong to the memory-facts readers and are
3640
+ // never claimed.
3641
+ if (parsed === null && rendered.miss && !rendered.ambiguous) {
3642
+ const forM = normalizeQuery(String(query || "")).match(WHATIS_FOR_FALLBACK_RE);
3643
+ const forTerm = forM?.[1]?.trim();
3644
+ if (forTerm) {
3645
+ const forParsed = parseQuery(`what is a ${forTerm}`, { nlp });
3646
+ if (forParsed?.shape === "meta") {
3647
+ const forResult = traverse(graph, forParsed, { contextId, prev });
3648
+ const forRendered = render(forParsed, forResult);
3649
+ if (!forRendered.miss && !forRendered.ambiguous) {
3650
+ result = forResult;
3651
+ rendered = forRendered;
3652
+ }
3653
+ }
3654
+ }
3655
+ }
3543
3656
  // If relaxation materially rewrote the query and produced a real answer,
3544
3657
  // note it lightly so the reader knows how the question was read.
3545
3658
  let content = (relaxed && !rendered.miss && relaxed.to !== relaxed.from)
@@ -3586,7 +3699,7 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
3586
3699
  // edit-distance, announced in the content as "assuming you meant …");
3587
3700
  // null for every literal-identifier tier.
3588
3701
  matchedVia: result.matchedVia || null,
3589
- ...(rendered.ambiguous ? { candidates: rendered.candidates } : {}),
3702
+ ...(rendered.ambiguous ? { candidates: rendered.candidates, candidateParses: rendered.candidateParses } : {}),
3590
3703
  },
3591
3704
  };
3592
3705
  }