@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +441 -202
  2. package/bin/tmct.mjs +126 -1
  3. package/package.json +4 -2
  4. package/src/answer-variants.mjs +8 -36
  5. package/src/ask-browser-entry.mjs +5 -23
  6. package/src/ask-browser.bundle.js +1 -2
  7. package/src/ask-nlp.mjs +9 -23
  8. package/src/ask-vocab.mjs +139 -589
  9. package/src/ask.mjs +627 -1729
  10. package/src/chat.mjs +1684 -2872
  11. package/src/cli-args.mjs +14 -28
  12. package/src/codegraph.mjs +236 -644
  13. package/src/completions/complete.mjs +18 -62
  14. package/src/completions/graph-adapter.mjs +14 -60
  15. package/src/completions/group.mjs +12 -68
  16. package/src/completions/infer.mjs +38 -126
  17. package/src/completions/prune.mjs +17 -70
  18. package/src/completions/rank.mjs +16 -69
  19. package/src/completions/search.mjs +8 -31
  20. package/src/concept.mjs +32 -88
  21. package/src/conformance.mjs +11 -15
  22. package/src/corpus/conceptnet.mjs +31 -89
  23. package/src/corpus/templates.mjs +19 -45
  24. package/src/corpus/unknown-ingest.mjs +31 -92
  25. package/src/embed.mjs +10 -22
  26. package/src/extensions.mjs +50 -154
  27. package/src/finish.mjs +35 -91
  28. package/src/grammar/ace.mjs +16 -40
  29. package/src/grammar/assert.mjs +1 -1
  30. package/src/grammar/lexicon-core.json +1 -1
  31. package/src/grammar/lexicon.mjs +9 -27
  32. package/src/graph-merge.mjs +2 -3
  33. package/src/hash.mjs +6 -14
  34. package/src/index.mjs +6 -10
  35. package/src/init.mjs +38 -125
  36. package/src/interpret/fuzzy.mjs +10 -29
  37. package/src/interpret/merge.mjs +9 -27
  38. package/src/interpret/normalize.mjs +137 -585
  39. package/src/interpret/pipeline.mjs +23 -71
  40. package/src/interpret/strategies/ace.mjs +7 -31
  41. package/src/interpret/strategies/constructions.mjs +14 -41
  42. package/src/interpret/strategies/grammar.mjs +21 -60
  43. package/src/interpret/strategies/keywords.mjs +42 -131
  44. package/src/interpret/strategies/noise-strip.mjs +18 -89
  45. package/src/memory/bias.mjs +11 -54
  46. package/src/memory/blocks.mjs +18 -69
  47. package/src/memory/core.mjs +171 -591
  48. package/src/memory/fold.mjs +0 -0
  49. package/src/memory/inspect.mjs +7 -25
  50. package/src/memory/shacl.mjs +10 -39
  51. package/src/memory/trust.mjs +26 -127
  52. package/src/memory-ask-browser-entry.mjs +7 -30
  53. package/src/memory-ask-browser.bundle.js +1 -1
  54. package/src/paraphrase.mjs +20 -53
  55. package/src/planning.mjs +15 -157
  56. package/src/prose-nlp.mjs +4 -17
  57. package/src/prose.mjs +19 -67
  58. package/src/providers/bootstrap.mjs +1 -2
  59. package/src/providers/fixture.mjs +1 -2
  60. package/src/providers/graph-service.mjs +28 -59
  61. package/src/repository-interface.mjs +6 -8
  62. package/src/router/drive.mjs +183 -0
  63. package/src/router/goal-reasoner.mjs +66 -231
  64. package/src/router/guardrail.mjs +20 -58
  65. package/src/router/planner.mjs +15 -46
  66. package/src/router/registry.mjs +13 -43
  67. package/src/router/resolver.mjs +46 -131
  68. package/src/router/results.mjs +231 -0
  69. package/src/schema-docs.mjs +10 -27
  70. package/src/server-http.mjs +10 -19
  71. package/src/server.mjs +22 -28
  72. package/src/sessions.mjs +15 -30
  73. package/src/source-slice.mjs +5 -7
  74. package/src/source.mjs +10 -20
  75. package/src/syllogise.mjs +187 -575
  76. package/src/telemetry.mjs +3 -3
  77. package/src/toml-config.mjs +4 -4
  78. package/src/tui/app.mjs +9 -19
  79. package/src/viz.mjs +66 -123
  80. package/src/wink-model.mjs +10 -24
package/bin/tmct.mjs CHANGED
@@ -103,6 +103,14 @@ Usage:
103
103
  [--host <h>] [--port <n>] (POST /v1/messages) over the graph — a deterministic,
104
104
  [--graph <path>] no-LLM "model" a tool-loop client can call; $0 usage.
105
105
  [--config <path>] Defaults: host 127.0.0.1, port 8787. Ctrl+C to stop.
106
+ tmct plan "<request>" the capability router: compose/execute read-only graph-
107
+ [--repo <abs>] query tool calls for a compound or maintenance-goal
108
+ [--graph <path>] request ("of the modules impacted by X, which are
109
+ [--config <path>] untested", "what most needs a test") — a real STRIPS/
110
+ [--tools <a,b,...>] PDDL planner (src/router/*), never a guessed call.
111
+ [--json] Prints the grounded step sequence + composed answer,
112
+ or an honest "no plan found". --tools restricts the
113
+ declared toolset; --json prints the full loop result.
106
114
  tmct cli <tool> '{…}' invoke a graph tool directly (carry-over, de-emphasized)
107
115
  tmct cli digest '{…}' architecture map + per-module context bundles
108
116
  tmct --help show this help
@@ -658,6 +666,22 @@ async function main() {
658
666
  process.stderr.write(`tmct init: ${e?.message || e}\n`);
659
667
  process.exit(2);
660
668
  }
669
+ // `init` takes no positional argument at all, so a bare "sqlite"/"memory"/
670
+ // "default" here almost always means `npm run init --memory-backend X`
671
+ // was typed without the `--` separator npm needs — npm silently drops its
672
+ // own unrecognized flag and forwards only the bare word.
673
+ if (!memoryBackendVal) {
674
+ const strayBackendWord = rest.find((a) => ["default", "memory", "sqlite"].includes(a));
675
+ if (strayBackendWord) {
676
+ process.stderr.write(
677
+ `tmct init: read a bare "${strayBackendWord}" as --memory-backend ${strayBackendWord} — ` +
678
+ `npm likely dropped your --memory-backend flag because "npm run init --memory-backend ${strayBackendWord}" ` +
679
+ `needs a "--" before it: npm run init -- --memory-backend ${strayBackendWord} (or npm run init:sqlite, ` +
680
+ `or npx tmct init --memory-backend ${strayBackendWord} directly).\n`,
681
+ );
682
+ memoryBackendVal = strayBackendWord;
683
+ }
684
+ }
661
685
 
662
686
  // Resolve + validate EVERY pluggable input BEFORE touching disk — mirrors
663
687
  // `--with-persona`'s own "validate before scaffolding" discipline, and the
@@ -1079,6 +1103,107 @@ async function main() {
1079
1103
  return; // the listening server keeps the event loop alive
1080
1104
  }
1081
1105
 
1106
+ if (mode === "plan") {
1107
+ // `tmct plan "<request>"` — the capability router (src/router/*), surfaced
1108
+ // for real: a STRIPS/PDDL-style planner over the SAME read-only graph-query
1109
+ // tools chat/serve dispatch. A single-shot request ("who calls X") resolves
1110
+ // directly; a compound request ("of the modules impacted by X, which are
1111
+ // untested", "assess Y and then check Z") decomposes into an ordered call
1112
+ // sequence with a causal-link proof, then folds the executed results into one
1113
+ // composed answer; a request neither stage grounds escalates to the
1114
+ // closed-world goal-reasoner (a maintenance-invariant deduction — coverage
1115
+ // gaps, change-coupling risk — never a keyword guess). Anything none of the
1116
+ // three grounds is an HONEST "no plan found", never a guessed call — same
1117
+ // "grounded or an honest miss" contract as every other tmct answer path.
1118
+ const rest = process.argv.slice(3);
1119
+ if (rest.includes("--help") || rest.includes("-h")) {
1120
+ process.stdout.write(
1121
+ "tmct plan \"<request>\" — the capability router: compose/execute read-only\n" +
1122
+ "graph-query tool calls for a compound or maintenance-goal request.\n\n" +
1123
+ "Usage:\n" +
1124
+ " tmct plan \"<request>\" [--repo <abs>] [--graph <path>] [--config <path>]\n" +
1125
+ " [--tools <name,name,...>] [--json]\n\n" +
1126
+ " --repo <abs> target a repo's graph (<abs>/.tmct/graph.json); default: git root/cwd\n" +
1127
+ " --graph <path> explicit graph file (repeatable — multiple graphs merge)\n" +
1128
+ " --config <path> an alternate tmct.toml location (a file or a directory)\n" +
1129
+ " --tools <list> restrict the declared toolset (comma-separated capability\n" +
1130
+ " names, e.g. tmct_impact,tmct_untested); default: every\n" +
1131
+ " registered capability\n" +
1132
+ " --json print the full machine-readable loop result instead of\n" +
1133
+ " the human-readable report (a non-zero exit still means\n" +
1134
+ " an honest refusal, not a crash)\n\n" +
1135
+ "Examples:\n" +
1136
+ " tmct plan \"of the modules impacted by src/lib/http.mjs, which are untested\"\n" +
1137
+ " tmct plan \"what most needs a test in this codebase\"\n" +
1138
+ " tmct plan \"who calls createApp\"\n",
1139
+ );
1140
+ return;
1141
+ }
1142
+ const { strFlag, resolveRuntimeConfig } = await import("../src/cli-args.mjs");
1143
+ const toolsFlag = strFlag(rest, ["--tools"]);
1144
+ const jsonFlag = rest.includes("--json");
1145
+ // The request is every argv token that isn't a recognized flag or its value,
1146
+ // rejoined with single spaces — so both `tmct plan "quoted request"` and a
1147
+ // bare `tmct plan unquoted request words` work.
1148
+ const FLAG_WITH_VALUE = new Set(["--repo", "--graph", "--config", "--tools"]);
1149
+ const requestParts = [];
1150
+ for (let i = 0; i < rest.length; i += 1) {
1151
+ const a = rest[i];
1152
+ if (FLAG_WITH_VALUE.has(a)) { i += 1; continue; }
1153
+ if (a === "--json") continue;
1154
+ requestParts.push(a);
1155
+ }
1156
+ const request = requestParts.join(" ").trim();
1157
+ if (!request) {
1158
+ process.stderr.write("tmct plan: needs a request, e.g. `tmct plan \"of the modules impacted by X, which are untested\"`\n");
1159
+ process.exit(2);
1160
+ }
1161
+ const { config } = await resolveRuntimeConfig({ argv: rest });
1162
+ const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("../src/router/drive.mjs");
1163
+ const declared = declaredCapabilityNames();
1164
+ let tools = declared;
1165
+ if (toolsFlag) {
1166
+ tools = toolsFlag.split(",").map((s) => s.trim()).filter(Boolean);
1167
+ const unknown = tools.filter((t) => !declared.includes(t));
1168
+ if (unknown.length) {
1169
+ process.stderr.write(`tmct plan: unknown --tools name(s): ${unknown.join(", ")}. Registered capabilities: ${declared.join(", ")}.\n`);
1170
+ process.exit(2);
1171
+ }
1172
+ }
1173
+ let ctx;
1174
+ try {
1175
+ ctx = await buildCapabilityPlanCtx({ config });
1176
+ } catch (e) {
1177
+ process.stderr.write(`tmct plan: could not load the graph — ${e?.message || e}\n`);
1178
+ process.exit(1);
1179
+ }
1180
+ const result = await runCapabilityPlan(request, tools, ctx);
1181
+ if (jsonFlag) {
1182
+ process.stdout.write(JSON.stringify({ request, ...result }, null, 2) + "\n");
1183
+ process.exit(result.refused ? 1 : 0);
1184
+ }
1185
+ process.stdout.write(`tmct plan: "${request}"\n`);
1186
+ if (result.refused) {
1187
+ process.stdout.write(`no plan found — ${Array.isArray(result.why) ? result.why.join("; ") : result.why}\n`);
1188
+ if (result.c1Why) {
1189
+ process.stdout.write(`(the direct router also declined: ${Array.isArray(result.c1Why) ? result.c1Why.join("; ") : result.c1Why})\n`);
1190
+ }
1191
+ process.exit(1);
1192
+ }
1193
+ process.stdout.write(`driver: ${result.driver}\n\nsteps:\n`);
1194
+ for (let i = 0; i < result.calls.length; i += 1) {
1195
+ const c = result.calls[i];
1196
+ let text = "";
1197
+ try { text = await ctx.dispatch(c.name, c.input || {}).then((r) => (r.ok ? r.text : `(unresolved: ${r.error})`)); }
1198
+ catch (e) { text = `(error: ${e?.message || e})`; }
1199
+ process.stdout.write(` ${i + 1}. ${c.name} ${JSON.stringify(c.input || {})}\n ${String(text).split("\n").join("\n ")}\n`);
1200
+ }
1201
+ if (result.composed !== undefined && result.composed !== null) {
1202
+ process.stdout.write(`\ncomposed answer (${result.composed.length}): ${result.composed.length ? result.composed.join(", ") : "(empty set)"}\n`);
1203
+ }
1204
+ return;
1205
+ }
1206
+
1082
1207
  if (mode === "cli") {
1083
1208
  await runCliMode();
1084
1209
  return;
@@ -1087,7 +1212,7 @@ async function main() {
1087
1212
  // An unknown mode gets the instructive usage line and exit 2. (A bare invocation
1088
1213
  // never lands here — the argv splice above rewrote it to `chat`.)
1089
1214
  process.stderr.write(`tmct: unknown invocation "${process.argv.slice(2).join(" ")}". ` +
1090
- "Use `chat`, `memory`, `init`, `import`, `extend --validate`, `syllogise`, `serve`, `cli digest …`, or `cli <tool> …`.\n");
1215
+ "Use `chat`, `memory`, `init`, `import`, `extend --validate`, `syllogise`, `serve`, `plan`, `cli digest …`, or `cli <tool> …`.\n");
1091
1216
  process.exit(2);
1092
1217
  }
1093
1218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.9.2",
3
+ "version": "1.10.1",
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.",
@@ -58,7 +58,8 @@
58
58
  "./extensions": "./src/extensions.mjs",
59
59
  "./toml-config": "./src/toml-config.mjs",
60
60
  "./generateCompletion": "./src/completions/complete.mjs",
61
- "./createCompletionsGraphAdapter": "./src/completions/graph-adapter.mjs"
61
+ "./createCompletionsGraphAdapter": "./src/completions/graph-adapter.mjs",
62
+ "./plan": "./src/router/drive.mjs"
62
63
  },
63
64
  "files": [
64
65
  "bin/",
@@ -85,6 +86,7 @@
85
86
  "chat": "node bin/tmct.mjs",
86
87
  "chat:repo": "node bin/tmct.mjs chat --repo",
87
88
  "init": "node bin/tmct.mjs init",
89
+ "init:sqlite": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --memory-backend sqlite",
88
90
  "init:persona:human": "node bin/tmct.mjs init --with-persona human",
89
91
  "init:persona:empty": "node bin/tmct.mjs init --with-persona empty",
90
92
  "init:large": "node bin/tmct.mjs init && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java",
@@ -1,33 +1,10 @@
1
- // answer-variants.mjs — deterministic, committed answer-phrasing variety
2
- // ("wiring the generated corpus into the live answer path", ROADMAP.md's
3
- // What's next item / archive/PLAN_TEMPLATE_COVERAGE.md's stated follow-on).
1
+ // answer-variants.mjs — deterministic, committed answer-phrasing variety for
2
+ // a curated set of answer templates (answer-variants.json).
4
3
  //
5
- // A SMALL, curated, committed table (answer-variants.json) of safe cosmetic/
6
- // locational/connector-word rephrasings for a deliberately narrow set of
7
- // answer templates in ask.mjs/chat.mjs -- never a relation verb (imports/
8
- // calls/tests/inherits/touches), never an entity id/label/path, never a miss/
9
- // rephrase-hint template. Each pool was hand-reviewed for meaning-safety in
10
- // its SPECIFIC rendered sentence (see answer-variants.json's own "curation"
11
- // field) -- WordNet was consulted but its sense-1 lookups mostly resolved to
12
- // the wrong sense for this domain-specific connector vocabulary, so nothing
13
- // here was auto-accepted from a raw synset match.
14
- //
15
- // Selection is a pure hash, never Math.random/Date.now: the SAME (poolId, key)
16
- // always picks the SAME form, so the same query against the same entity
17
- // renders byte-identical text every time (pinnable in tests), while different
18
- // entities/answers spread across the pool's forms. This is the ONE place that
19
- // hash-and-pick logic lives -- every call site below imports pickPhrase
20
- // rather than re-deriving its own index arithmetic.
21
- //
22
- // Lazy + failure-tolerant load (chat.mjs's own "a turn never crashes" ethos,
23
- // see chatTemplates()): a missing/corrupt data file, or `node:fs` simply
24
- // being unavailable, degrades to always returning `base` -- never a throw.
25
- // That second case is load-bearing, not theoretical: ask.mjs is bundled
26
- // whole into the browser "Ask the graph" panel (scripts/build-ask-bundle.mjs
27
- // stubs node:fs's readFileSync as a function that THROWS when called, so the
28
- // browser bundle still boots; this module's readFileSync call only ever
29
- // happens lazily inside pickPhrase, wrapped in try/catch, exactly so that
30
- // throw is caught here and never reaches the caller.
4
+ // Selection is a pure hash of (poolId, key), never Math.random/Date.now, so
5
+ // output is byte-identical across runs (pinnable in tests). Load is lazy and
6
+ // failure-tolerant (missing file, or a stubbed node:fs in the browser bundle,
7
+ // both degrade to returning `base`) rather than throwing.
31
8
 
32
9
  import { readFileSync } from "node:fs";
33
10
  import { fileURLToPath } from "node:url";
@@ -48,13 +25,8 @@ function loadData() {
48
25
  }
49
26
 
50
27
  /** Deterministically choose `base` or one of `poolId`'s committed variants,
51
- * keyed on `key` -- pass something stable that identifies THIS specific
52
- * answer (an entity's own `id` for entity-anchored templates; any other
53
- * stable string, e.g. a graph-summary key, for templates with no single
54
- * entity). Same (poolId, key) always returns the same string; an unknown
55
- * pool, a missing/unreadable data file, or a falsy `key` all fall back to
56
- * `base` unconditionally -- variety is strictly additive, never a way to
57
- * lose the original wording. */
28
+ * keyed on `key`. Any failure (unknown pool, unreadable data, falsy `key`)
29
+ * falls back to `base`. */
58
30
  export function pickPhrase(poolId, key, base) {
59
31
  if (!key) return base;
60
32
  const data = loadData();
@@ -1,18 +1,7 @@
1
1
  // ask-browser-entry.mjs — the esbuild entry for `tmct viz`'s embedded "Ask the
2
- // graph" chat panel (PLAN_BREADTH_FIRST_NLU.md §5 follow-on, operator directive
3
- // 2026-07-11: a real NL chat running client-side against the embedded graph,
4
- // via tmct's OWN JS engine not reimplemented, not a stub).
5
- //
6
- // Precedent: seonix's own `src/ask-browser-entry.mjs` (PLAN_CHAT_EXTRACTION.md
7
- // Stage 5) proved this exact approach in production — esbuild + a Node-builtin
8
- // stub plugin bundles tmct's real ask() into a single browser IIFE. This entry
9
- // mirrors that pattern, adapted for tmct bundling its OWN ask.mjs directly
10
- // (no external package import needed — we're already inside the source tree)
11
- // and extended with the graph-traversal exports `tmct viz`'s own client-side
12
- // re-focus/re-walk needs (spiralExpand etc. — seonix's code-graph viewer
13
- // recomputes depth with its own hand-rolled client-side BFS; tmct's viewer
14
- // reuses the real spiralExpand instead, so the browser walk is byte-identical
15
- // to the CLI's).
2
+ // graph" chat panel: a real NL chat running client-side against the embedded
3
+ // graph, via tmct's own JS engine (esbuild + a Node-builtin stub plugin
4
+ // bundles tmct's real ask() into a single browser IIFE).
16
5
  //
17
6
  import { ask, parseQuery } from "./ask.mjs";
18
7
  import {
@@ -21,15 +10,8 @@ import {
21
10
  edgeKindsFor, collapseToTopN,
22
11
  } from "./codegraph.mjs";
23
12
 
24
- // PLAN_VIZ_MEMORY.md Bug 2 fix + Controls port: deriveFactTermGraph/
25
- // pickLegendDimension/legendValueFor/MEMORY_FACT_LINK_KINDS/edgeKindsFor/
26
- // collapseToTopN are exported here (alongside the pre-existing traversal
27
- // exports) so the viewer page's client-side recentre/edge-kind-toggle/
28
- // dimension-switcher (a user double-clicking a node, changing which edge
29
- // kinds the walk follows, or flipping the legend from "split by predicate"
30
- // to "split by trust source") re-derives the SAME term graph, kind set, and
31
- // legend the CLI's own generation-time computeVizGraph used — never a second
32
- // hand-rolled copy that could drift.
13
+ // Exported so the viewer page's client-side recentre/edge-kind-toggle/
14
+ // dimension-switcher reuses the same computation as the CLI, not a second copy.
33
15
  globalThis.tmctViz = {
34
16
  ask, parseQuery, parseEntities, spiralExpand, mostRecentIndividual, derivedUpdatedAt,
35
17
  MEMORY_SPIRAL_EXPAND_KINDS, MEMORY_FACT_LINK_KINDS, buildVizNodesAndEdges,
@@ -1837,8 +1837,7 @@
1837
1837
  // (neither recognizes the bare "is in" idiom once "else" sits in front of it).
1838
1838
  // The only rescue was the relaxation cascade's drop-unmatched layer — but that
1839
1839
  // layer refuses to accept a relaxed reading that still renders an honest EMPTY
1840
- // (by design: relaxation must turn a miss into a real answer, never into
1841
- // another kind of miss), so a genuinely empty class ("what else is in
1840
+ // , so a genuinely empty class ("what else is in
1842
1841
  // Task.complete" — a method, no members) bottomed out at the bare grammar
1843
1842
  // wall instead of the specific "no contains edges" receipt. Routing this
1844
1843
  // frame onto the SAME direct "what does X contain" path the plain "what is
package/src/ask-nlp.mjs CHANGED
@@ -1,18 +1,11 @@
1
- // ask-nlp.mjs — the OPTIONAL wink-nlp adapter behind ask.mjs's lemma/POS tier.
2
- //
3
- // wink-nlp and wink-eng-lite-web-model are loaded through the shared leaf loader
4
- // src/wink-model.mjs (Node `createRequire` fallback + a browser registration seam),
5
- // so this file no longer carries its own Node-only load block. The load happens
6
- // lazily on first use and failure is cached as null: a checkout without the optional
7
- // deps installed answers exactly like the browser bundle, it never throws.
1
+ // ask-nlp.mjs — the optional wink-nlp adapter behind ask.mjs's lemma/POS tier.
2
+ // Loaded lazily via src/wink-model.mjs; failure is cached as null, never a throw.
8
3
 
9
4
  import { winkInstance } from "./wink-model.mjs";
10
5
 
11
- let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
6
+ let cached; // undefined = not tried yet; null = unavailable
12
7
 
13
- /** Lazily build the {lemma, posTags} adapter, or null when wink isn't loadable.
14
- * Deterministic: wink-nlp's tagger/lemmatiser is a fixed model with no sampling,
15
- * so the same input always yields the same tags/lemmas across processes. */
8
+ /** Lazily build the {lemma, posTags} adapter, or null when wink isn't loadable. */
16
9
  export function nlpAdapter() {
17
10
  if (cached !== undefined) return cached;
18
11
  try {
@@ -20,8 +13,7 @@ export function nlpAdapter() {
20
13
  if (!nlp) { cached = null; return cached; }
21
14
  const its = nlp.its;
22
15
  cached = {
23
- /** Lowercase lemma of a single token ("imported" -> "import"); the word
24
- * itself when wink has nothing better (unknown words come back as-is). */
16
+ /** Lowercase lemma of a single token ("imported" -> "import"). */
25
17
  lemma(word) {
26
18
  const w = String(word || "");
27
19
  try {
@@ -31,12 +23,7 @@ export function nlpAdapter() {
31
23
  return w.toLowerCase();
32
24
  }
33
25
  },
34
- /** True when wink's lexicon flags the word as an English stop word
35
- * ("anyway", "well", "also", …). Consulted by the noise-strip strategy
36
- * (interpret/strategies/noise-strip.mjs) as its wink tier — the strategy's
37
- * own KEEP set screens out the grammar's load-bearing words (which/what/
38
- * does/…) BEFORE this is asked, so wink flagging a question word is
39
- * harmless by construction. False on any surprise, never a throw. */
26
+ /** True when wink's lexicon flags the word as an English stop word. */
40
27
  isStopWord(word) {
41
28
  try {
42
29
  const out = nlp.readDoc(String(word || "")).tokens().out(its.stopWordFlag);
@@ -45,10 +32,9 @@ export function nlpAdapter() {
45
32
  return false;
46
33
  }
47
34
  },
48
- /** UPOS tags aligned to the CALLER's word array. wink re-tokenizes (it
49
- * splits "walk.mjs" into three tokens), so each input word is greedily
50
- * matched to the run of wink tokens that spell it and takes its FIRST
51
- * sub-token's tag; null per word on any surprise, never a throw. */
35
+ /** UPOS tags aligned to the caller's word array. wink re-tokenizes, so
36
+ * each input word is greedily matched to the run of wink tokens that
37
+ * spell it and takes its first sub-token's tag. */
52
38
  posTags(words) {
53
39
  try {
54
40
  const toks = nlp.readDoc(words.join(" ")).tokens();