@polycode-projects/the-mechanical-code-talker 2.2.0 → 2.3.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.
Files changed (49) hide show
  1. package/bin/tmct.mjs +4 -5
  2. package/corpus/wordnet/generate.mjs +6 -7
  3. package/package.json +30 -2
  4. package/src/adapters/corpus/conceptnet.mjs +1 -1
  5. package/src/adapters/graph-build.mjs +3 -3
  6. package/src/adapters/memory/blocks.mjs +2 -2
  7. package/src/adapters/memory/core.mjs +5 -5
  8. package/src/adapters/providers/bootstrap.mjs +1 -1
  9. package/src/adapters/providers/fixture.mjs +1 -1
  10. package/src/adapters/wink-model.mjs +1 -1
  11. package/src/adapters/wordnet-source.mjs +70 -0
  12. package/src/domain/answer-variants.json +1 -1
  13. package/src/domain/ask-vocab.mjs +2 -2
  14. package/src/domain/ask.mjs +4 -4
  15. package/src/domain/codegraph.mjs +3 -3
  16. package/src/domain/corpus-matrix.mjs +87 -0
  17. package/src/domain/grammar/ace.mjs +11 -11
  18. package/src/domain/grammar/lexicon.mjs +3 -3
  19. package/src/domain/inflect.mjs +67 -0
  20. package/src/domain/interpret/fuzzy.mjs +1 -1
  21. package/src/domain/interpret/merge.mjs +1 -1
  22. package/src/domain/interpret/normalize.mjs +1 -1
  23. package/src/domain/licences.mjs +68 -0
  24. package/src/domain/memory/capability.mjs +1 -1
  25. package/src/domain/memory/trust.mjs +2 -2
  26. package/src/domain/persona/codegen.mjs +123 -0
  27. package/src/domain/persona/examples.mjs +26 -0
  28. package/src/domain/persona/tiers.mjs +270 -0
  29. package/src/domain/publish-gate.mjs +41 -0
  30. package/src/domain/router/call-validator.mjs +1 -1
  31. package/src/domain/router/drive.mjs +3 -4
  32. package/src/domain/router/registry.mjs +12 -13
  33. package/src/domain/router/resolver.mjs +18 -5
  34. package/src/domain/router/results.mjs +3 -3
  35. package/src/domain/router/taught.mjs +4 -3
  36. package/src/domain/schemaorg/turtle.mjs +25 -0
  37. package/src/domain/semcor/parse.mjs +87 -0
  38. package/src/domain/syllogise.mjs +6 -6
  39. package/src/domain/version-stamp.mjs +36 -0
  40. package/src/domain/wordnet/yaml.mjs +133 -0
  41. package/src/services/chat-session.mjs +2 -2
  42. package/src/services/chat.mjs +2 -2
  43. package/src/services/cli-args.mjs +4 -4
  44. package/src/services/finish.mjs +1 -1
  45. package/src/services/ledger-viz.mjs +2 -3
  46. package/src/services/sessions.mjs +4 -4
  47. package/src/services/viz-theme.mjs +3 -4
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +1 -18
  49. package/src/domain/router/guardrail.mjs +0 -116
package/bin/tmct.mjs CHANGED
@@ -167,7 +167,7 @@ const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
167
167
  * into a caller's prompt.
168
168
  *
169
169
  * Two ways to say which modules: an explicit `modules` array (unchanged), or a `query` string —
170
- * auto-locate + score-gap-select (R1b, the shipped default as of 2026-07-02) in one call, so a
170
+ * auto-locate + score-gap-select (the shipped default) in one call, so a
171
171
  * real caller no longer has to run `tmct_locate` and hand-pick a module themselves. `modules`
172
172
  * wins if both are given. The header reports which modules were actually selected either way.
173
173
  *
@@ -741,8 +741,7 @@ async function main() {
741
741
  const lexiconVal = strFlag(rest, ["--lexicon"]);
742
742
  const graphFlags = repeatedFlag(rest, ["--graph"]);
743
743
 
744
- // `--memory-backend <default|memory|sqlite>` (PLAN_SEED.md §6's storage-backend
745
- // seam, now reachable from `tmct init`): validated BEFORE touching disk, same
744
+ // `--memory-backend <default|memory|sqlite>`: validated BEFORE touching disk, same
746
745
  // discipline as every other pluggable input below. Written into tmct.toml's
747
746
  // `[memory] backend` (src/services/init.mjs's renderTomlConfig); chat.mjs's
748
747
  // createSession reads it back at CLI-flag > TMCT_MEMORY_BACKEND env >
@@ -798,7 +797,7 @@ async function main() {
798
797
  personaPreset = PERSONA_PRESETS[personaName];
799
798
  }
800
799
 
801
- // `--persona-size <medium|large>` (PLAN_SEED.md §3): Small/Medium/Large are
800
+ // `--persona-size <medium|large>`: Small/Medium/Large are
802
801
  // SIZES of the one `human` bundle, not separate corpus ids — human.jsonl
803
802
  // (Small, the default) stays exactly as-is; human-medium.jsonl/
804
803
  // human-large.jsonl hold ONLY the facts each size adds beyond the previous
@@ -1121,7 +1120,7 @@ async function main() {
1121
1120
  if (mode === "viz") {
1122
1121
  // `tmct viz` — the ledger explorer: one self-contained HTML page rendering
1123
1122
  // the memory graph as readable fact-sentences around a focus term, with
1124
- // the in-browser chat dock (PLAN_VIZ_LEDGER.md). Same repo resolution as
1123
+ // the in-browser chat dock. Same repo resolution as
1125
1124
  // `memory`/`syllogise` — resolveRuntimeConfig: --repo > git root > cwd.
1126
1125
  // `--ledger` is accepted as a no-op: the ledger IS the viz surface now.
1127
1126
  const rest = process.argv.slice(3);
@@ -30,12 +30,11 @@
30
30
  // emitting one row per edge (see RELATION_MAP / synonymPairs below).
31
31
  //
32
32
  // The hand-rolled `parseYaml` this file reuses (imported, not duplicated) is
33
- // scripts/extract-persona-sources.mjs's own tiny YAML-subset reader — already
34
- // proven against this exact OEWN dump shape by scripts/build-persona-tiers.mjs
35
- // and scripts/build-persona-examples.mjs. Reusing it (rather than adding a
36
- // general YAML dependency, or re-deriving a second hand-rolled parser) keeps
37
- // this converter self-consistent with the rest of the persona-tier tooling
38
- // that already reads this same source.
33
+ // src/domain/wordnet/yaml.mjs's tiny YAML-subset reader — already proven
34
+ // against this exact OEWN dump shape by the persona-tier tooling. Reusing it
35
+ // (rather than adding a general YAML dependency, or re-deriving a second
36
+ // hand-rolled parser) keeps this converter self-consistent with the rest of
37
+ // the tooling that already reads this same source.
39
38
  //
40
39
  // Licence: Open English WordNet content is CC-BY-4.0 (Princeton WordNet +
41
40
  // Open English Wordnet team) — see LICENSE-NOTICE in this directory. The
@@ -47,7 +46,7 @@ import { homedir } from "node:os";
47
46
  import { createHash } from "node:crypto";
48
47
  import { fileURLToPath } from "node:url";
49
48
  import { dirname, join } from "node:path";
50
- import { parseYaml } from "../../scripts/extract-persona-sources.mjs";
49
+ import { parseYaml } from "../../src/domain/wordnet/yaml.mjs";
51
50
 
52
51
  const HERE = dirname(fileURLToPath(import.meta.url));
53
52
  export const WORDNET_OUT_DIR = HERE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
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.",
@@ -72,8 +72,19 @@
72
72
  "test": "node --test \"test/**/*.test.mjs\"",
73
73
  "test:e2e": "node --test \"e2e/**/*.test.mjs\"",
74
74
  "e2e:browsers": "playwright install chromium",
75
+ "check:links": "node scripts/check-links.mjs",
76
+ "check:pii": "node scripts/pii-lint.mjs",
77
+ "check:pack": "node scripts/check-pack-manifest.mjs",
78
+ "check:licences": "node scripts/check-licences.mjs",
79
+ "check:publint": "npx --no-install publint",
80
+ "check:tool-docs": "node scripts/generate-tool-docs.mjs --check",
81
+ "check:publish": "node scripts/check-publish.mjs",
82
+ "check:all": "npm run check:links && npm run check:pii && npm run check:licences && npm run check:publint && npm run check:tool-docs",
83
+ "smoke:deploy": "node scripts/post-deploy-smoke.mjs",
75
84
  "chat": "node bin/tmct.mjs",
76
85
  "chat:repo": "node bin/tmct.mjs chat --repo",
86
+ "chat:plain": "node bin/tmct.mjs chat --plain",
87
+ "chat:narrate": "node bin/tmct.mjs chat --narrate",
77
88
  "init": "node bin/tmct.mjs init",
78
89
  "init:sqlite": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --memory-backend sqlite",
79
90
  "init:persona:human": "node bin/tmct.mjs init --with-persona human",
@@ -82,19 +93,36 @@
82
93
  "init:xl": "node bin/tmct.mjs init --persona-size large && 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 && node bin/tmct.mjs import --corpus wordnet-xl",
83
94
  "init:xxl": "node bin/tmct.mjs init --persona-size large && 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 && node bin/tmct.mjs import --corpus wordnet-full && node bin/tmct.mjs import --corpus namenet",
84
95
  "memory": "node bin/tmct.mjs memory",
96
+ "memory:verbose": "node bin/tmct.mjs memory --verbose",
85
97
  "syllogise": "node bin/tmct.mjs syllogise",
86
98
  "viz": "node bin/tmct.mjs viz",
99
+ "viz:term": "node bin/tmct.mjs viz --term",
100
+ "plan": "node bin/tmct.mjs plan",
101
+ "plan:json": "node bin/tmct.mjs plan --json",
102
+ "cli": "node bin/tmct.mjs cli",
103
+ "cli:digest": "node bin/tmct.mjs cli digest",
104
+ "import": "node bin/tmct.mjs import",
105
+ "extend": "node bin/tmct.mjs extend --validate",
106
+ "serve": "node bin/tmct.mjs serve",
107
+ "serve:public": "node bin/tmct.mjs serve --host 0.0.0.0 --port 8787",
87
108
  "example:mini": "node bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
88
109
  "example:polyglot": "node bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
89
110
  "chatbench:run": "node chatbench/run.mjs",
90
111
  "chatbench:judge": "node chatbench/judge.mjs",
91
- "serve": "node bin/tmct.mjs serve",
92
112
  "agentbench:run": "node agentbench/run.mjs",
93
113
  "infbench": "node infbench/generate-cases.mjs && node infbench/run.mjs",
114
+ "corpus:matrix": "node scripts/corpus-matrix.mjs",
115
+ "corpus:matrix:gaps": "node scripts/corpus-matrix.mjs --gaps",
116
+ "template:coverage": "node scripts/template-coverage.mjs",
94
117
  "audit": "npm audit --audit-level=high",
95
118
  "audit:fix": "npm audit fix",
96
119
  "demo:build": "node scripts/build-demo-site.mjs",
97
120
  "build:ask-bundle": "node scripts/build-ask-bundle.mjs",
121
+ "build:demo-graph": "node scripts/build-demo-graph.mjs",
122
+ "build:demo-memory": "node scripts/build-demo-memory.mjs",
123
+ "gen:tool-docs": "node scripts/generate-tool-docs.mjs",
124
+ "gen:collisions": "node scripts/generate-real-word-collisions.mjs",
125
+ "gen:variants": "node scripts/generate-template-variants.mjs",
98
126
  "extract:facts": "node scripts/extract-facts-from-text.mjs"
99
127
  },
100
128
  "devDependencies": {
@@ -35,7 +35,7 @@ export const TIER2_MANIFEST_FILE = join(TIER2_DIR, "manifest.json");
35
35
  // conversion, same slice shape/loader path as tier-1/tier-2. "wordnet-xl"/"wordnet-full"
36
36
  // are wired as BUILTIN_EXTENSIONS corpus entries in src/extensions.mjs.
37
37
  export const WORDNET_DIR = join(PKG_ROOT, "corpus", "wordnet");
38
- export const WORDNET_MANIFEST_FILE = join(WORDNET_DIR, "manifest.json");
38
+ const WORDNET_MANIFEST_FILE = join(WORDNET_DIR, "manifest.json");
39
39
 
40
40
  const ACE_PATTERNS = new Set(["subClassOf", "type", "ObjectProperty", "someValuesFrom", "disjointWith", "property", "none"]);
41
41
 
@@ -352,7 +352,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
352
352
  const countClass = (c) => fnIndividuals.filter((i) => i.class === c).length;
353
353
  const sampleClass = (c) => fnIndividuals.filter((i) => i.class === c).slice(0, 3).map((i) => i.label);
354
354
 
355
- // Second pass (PLAN_PROSE_INDEX.md) — see the returned `proseIndex` field's comment below.
355
+ // Second pass — see the returned `proseIndex` field's comment below.
356
356
  const allIndividuals = attachProseTokens(
357
357
  [...moduleIndividuals, ...fnIndividuals, ...commitIndividuals], { enabled: prose },
358
358
  );
@@ -361,7 +361,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
361
361
  return {
362
362
  generated_at: generatedAt,
363
363
  // SEON (se-on.org, FAMIX-derived) vocabulary + our `mgx:` extension, documented
364
- // for readers; the graph is JSON-label-only (no RDF store — see PLAN_SEON_RDF.md).
364
+ // for readers; the graph is JSON-label-only (no RDF store).
365
365
  prefixes: {
366
366
  seon: "http://se-on.org/ontologies/seon.owl#",
367
367
  mgx: "urn:tmct:mgx#",
@@ -419,7 +419,7 @@ export function buildEntities(modules, commits, { generatedAt = "", symbolHistor
419
419
  rel("reexports", "mgx:reExports", reExportEdges),
420
420
  ],
421
421
  individuals: allIndividuals,
422
- // Second pass (PLAN_PROSE_INDEX.md): word -> [individual ids], inverted from the
422
+ // Second pass: word -> [individual ids], inverted from the
423
423
  // `prose_tokens` attribute attachProseTokens just attached. Disable via
424
424
  // TMCT_PROSE_INDEX=0 (indexRepository, below) — {} when off. The typed graph above
425
425
  // (individuals' core fields, all edges) is byte-identical either way.
@@ -18,8 +18,8 @@ const trustFactorOf = (trust) => 0.5 + (typeof trust === "number" ? trust : SOUR
18
18
  export const BLOCKS_DIR_REL = join(".tmct", "memory", "blocks");
19
19
  const INDEX_NAME = "index.json";
20
20
 
21
- export const PAGERANK_DAMPING = 0.85;
22
- export const PAGERANK_ITERATIONS = 20;
21
+ const PAGERANK_DAMPING = 0.85;
22
+ const PAGERANK_ITERATIONS = 20;
23
23
  export const OVERLAP_MIN = 2; // shared tokens for a similarity edge
24
24
  const MAX_TOKENS_PER_BLOCK = 800; // beyond tokenizeProse's per-doc cap: union over lines
25
25
 
@@ -43,7 +43,7 @@ export const IN_REPLY_TO_PROP = "mgx:inReplyTo";
43
43
  // The provenance-link predicate family: one umbrella object property with two
44
44
  // workhorse subproperties, minted in the owned mgx: namespace to match
45
45
  // tmct-core.ttl's object-property style.
46
- export const DERIVED_FROM_PROP = "mgx:derivedFrom"; // umbrella: Fact → Source|Fact
46
+ const DERIVED_FROM_PROP = "mgx:derivedFrom"; // umbrella: Fact → Source|Fact
47
47
  export const STATED_BY_PROP = "mgx:statedBy"; // a Source directly asserts a Fact
48
48
  export const CANONICALISED_FROM_PROP = "mgx:canonicalisedFrom"; // a canonical Fact ← its raw form
49
49
  export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (session-scoped) trust nudge on a Source, [0.5,1.5]
@@ -52,7 +52,7 @@ export const SOURCE_RELIABILITY_PROP = "mgx:sourceReliability"; // actor-level (
52
52
  // with no session-id segment. A tag that does carry one mints its own
53
53
  // per-session Source instead (`${ID}:<sessionId>`, sourceIdFor below).
54
54
  export const OPERATOR_SOURCE_ID = "src:operator-chat";
55
- export const TEACH_SOURCE_ID = "src:teach-chat";
55
+ const TEACH_SOURCE_ID = "src:teach-chat";
56
56
 
57
57
  const ROLES = new Set(["visitor", "tmct"]);
58
58
  const LABEL_CAP = 48; // utterance/fact labels stay skimmable in renders
@@ -1113,13 +1113,13 @@ export const RULE_KIND_ACTION_SIGNATURE = "action-signature";
1113
1113
  export const RULE_KIND_ACTION_PRECOND = "action-precond";
1114
1114
  export const RULE_KIND_ACTION_EFFECT = "action-effect";
1115
1115
  export const RULE_KIND_ACTION_CONSTRAINT = "action-constraint";
1116
- export const RULE_KINDS = Object.freeze([
1116
+ const RULE_KINDS = Object.freeze([
1117
1117
  RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE,
1118
1118
  RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT,
1119
1119
  RULE_KIND_ACTION_CONSTRAINT,
1120
1120
  ]);
1121
1121
 
1122
- export const RULE_NAME_PROP = "mgx:ruleName";
1122
+ const RULE_NAME_PROP = "mgx:ruleName";
1123
1123
  export const RULE_KIND_PROP = "mgx:ruleKind";
1124
1124
 
1125
1125
  // Per-kind slot contract: JS slot key -> the mgx: attribute it's written under.
@@ -1514,7 +1514,7 @@ export async function removeFacts(dir, ids) {
1514
1514
 
1515
1515
  /** The trust floor a fact must clear before a differing object counts as a real
1516
1516
  * contradiction (below it the fact is too weak to contradict anything). */
1517
- export const CONTRADICTION_TRUST_FLOOR = 0.5;
1517
+ const CONTRADICTION_TRUST_FLOOR = 0.5;
1518
1518
 
1519
1519
  export const HAS_A_PREDICATE = "mgx:hasA";
1520
1520
  export const CAPABLE_OF_PREDICATE = "mgx:capableOf";
@@ -13,7 +13,7 @@ import { emptyEntities } from "../source.mjs";
13
13
  import { createGraphService } from "./graph-service.mjs";
14
14
 
15
15
  /** The parsed empty bootstrap graph. */
16
- export function bootstrapGraph() {
16
+ function bootstrapGraph() {
17
17
  return parseEntities(emptyEntities());
18
18
  }
19
19
 
@@ -15,7 +15,7 @@ import { createGraphService } from "./graph-service.mjs";
15
15
  /** A compact but type-complete entities payload: modules, a class hierarchy
16
16
  * (Base ← Widget ← Button), a method with a full signature, an attribute, a
17
17
  * module global, and a commit — wired by one edge of every closed kind. */
18
- export const FIXTURE_ENTITIES = Object.freeze({
18
+ const FIXTURE_ENTITIES = Object.freeze({
19
19
  generated_at: "2026-07-05T00:00:00.000Z",
20
20
  bootstrap: false,
21
21
  prefixes: { seon: "http://se-on.org/ontologies/seon.owl#", mgx: "urn:tmct:mgx#" },
@@ -26,7 +26,7 @@ export function registerWinkModel(factory) {
26
26
 
27
27
  /** Load `{ winkNLP, model }` once, or null when wink isn't available. Prefers a
28
28
  * registered browser factory; otherwise falls back to Node module resolution. */
29
- export function loadWinkModel() {
29
+ function loadWinkModel() {
30
30
  if (cached !== undefined) return cached;
31
31
  try {
32
32
  const pair = injected ? injected() : nodeRequireWink();
@@ -0,0 +1,70 @@
1
+ // wordnet-source.mjs — reads a LOCAL Open English WordNet clone off disk and
2
+ // indexes it. The clone is never vendored, never committed, never part of the
3
+ // npm package: point TMCT_WORDNET_SRC at it, or keep it at the default path.
4
+ //
5
+ // This is the disk half of the WordNet reader. The parsing half is pure and
6
+ // lives in src/domain/wordnet/yaml.mjs, so it is testable with no clone
7
+ // present; everything here needs the real files.
8
+
9
+ import { readFile, readdir } from "node:fs/promises";
10
+ import { existsSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { parseYaml } from "../domain/wordnet/yaml.mjs";
14
+
15
+ export const WORDNET_SRC = process.env.TMCT_WORDNET_SRC || join(homedir(), "projects", "globalwordnet", "english-wordnet");
16
+ export const WORDNET_YAML_DIR = join(WORDNET_SRC, "src", "yaml");
17
+
18
+ /** True iff a WordNet clone is readable at `yamlDir`. Callers use this to fail
19
+ * with a one-line message rather than a stack trace: these are maintainer
20
+ * tools, never a build dependency. */
21
+ export function hasWordnetSource(yamlDir = WORDNET_YAML_DIR) {
22
+ return existsSync(yamlDir);
23
+ }
24
+
25
+ /** Load one or more noun.<x>/verb.<x>.yaml files into a flat synset-id -> record map. */
26
+ export async function loadSynsets(files, yamlDir = WORDNET_YAML_DIR) {
27
+ const map = new Map();
28
+ for (const f of files) {
29
+ const path = join(yamlDir, f);
30
+ if (!existsSync(path)) continue;
31
+ const parsed = parseYaml(await readFile(path, "utf8"));
32
+ for (const [id, rec] of Object.entries(parsed)) map.set(id, rec);
33
+ }
34
+ return map;
35
+ }
36
+
37
+ /** Load the entries-<letter>.yaml files that could contain any of `words`
38
+ * (only the letters actually needed — 28 files, ~1MB-3MB each, no reason to
39
+ * load all 28 when a clump only needs a handful of letters). Returns
40
+ * word -> { n: [{id, synset}], v: [...], a: [...] }. */
41
+ export async function loadEntriesFor(words, yamlDir = WORDNET_YAML_DIR) {
42
+ const letters = new Set();
43
+ for (const w of words) {
44
+ const c = w[0].toLowerCase();
45
+ letters.add(/[a-z]/.test(c) ? c : "0");
46
+ }
47
+ const index = new Map();
48
+ for (const letter of letters) {
49
+ const path = join(yamlDir, `entries-${letter}.yaml`);
50
+ if (!existsSync(path)) continue;
51
+ const parsed = parseYaml(await readFile(path, "utf8"));
52
+ for (const [word, byPos] of Object.entries(parsed)) {
53
+ if (!words.has(word)) continue;
54
+ const senses = {};
55
+ for (const [pos, rec] of Object.entries(byPos || {})) {
56
+ if (pos === "form") continue;
57
+ const list = Array.isArray(rec?.sense) ? rec.sense : [];
58
+ senses[pos] = list.map((s) => ({ id: s.id, synset: s.synset })).filter((s) => s.synset);
59
+ }
60
+ index.set(word, senses);
61
+ }
62
+ }
63
+ return index;
64
+ }
65
+
66
+ /** Every noun.*.yaml synset in the clone. */
67
+ export async function loadAllNounSynsets(yamlDir = WORDNET_YAML_DIR) {
68
+ const files = (await readdir(yamlDir)).filter((f) => f.startsWith("noun."));
69
+ return loadSynsets(files, yamlDir);
70
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "curation": "Hand-curated. scripts/generate-answer-variants.mjs cross-checked each pool against real Open English WordNet synsets (scripts/lib/wordnet-synonyms.mjs) where a sense fit; most of these are domain-specific code-graph connector phrases WordNet's own sense-1 convention resolves to the wrong sense for -- e.g. 'locate' -> 'turn up', 'record' -> 'enter, put down', 'module' -> 'mental faculty' -- so every entry below was accepted or rejected by hand against the exact rendered sentence it replaces, never auto-accepted from a raw synset match. See src/domain/answer-variants.mjs for how these are selected at render time.",
3
+ "curation": "Hand-curated, and it has to stay that way: each pool was cross-checked against real Open English WordNet synsets where a sense fit, but most of these are domain-specific code-graph connector phrases whose sense-1 convention resolves to the wrong sense -- e.g. 'locate' -> 'turn up', 'record' -> 'enter, put down', 'module' -> 'mental faculty'. So every entry below was accepted or rejected by hand against the exact rendered sentence it replaces, never auto-accepted from a raw synset match. That is why no generator reproduces this file and no drift guard can check it. See src/domain/answer-variants.mjs for how these are selected at render time.",
4
4
  "pools": {
5
5
  "defined-in": {
6
6
  "base": "defined in",
@@ -211,7 +211,7 @@ export function stripTrailingScopeFiller(text) {
211
211
 
212
212
  /** Trailing bare discourse tags ("how many of those then"). "too" can stack
213
213
  * ("is UserController a validator too then"), hence the double pass below. */
214
- export const TRAILING_DISCOURSE_TAG = Object.freeze(["then", "though", "too"]);
214
+ const TRAILING_DISCOURSE_TAG = Object.freeze(["then", "though", "too"]);
215
215
 
216
216
  const TRAILING_DISCOURSE_TAG_RE = new RegExp(
217
217
  `\\s+(?:${TRAILING_DISCOURSE_TAG.join("|")})\\s*[?.!]*$`, "i",
@@ -219,7 +219,7 @@ const TRAILING_DISCOURSE_TAG_RE = new RegExp(
219
219
 
220
220
  /** Trailing comma-delimited discourse clauses ("what is a class, please
221
221
  * explain"), anchored on a literal comma so this never fires mid-phrase. */
222
- export const TRAILING_DISCOURSE_CLAUSE = Object.freeze(["please explain", "explain"]);
222
+ const TRAILING_DISCOURSE_CLAUSE = Object.freeze(["please explain", "explain"]);
223
223
 
224
224
  const TRAILING_DISCOURSE_CLAUSE_RE = new RegExp(
225
225
  `,\\s*(?:${TRAILING_DISCOURSE_CLAUSE.join("|")})\\s*[?.!]*$`, "i",
@@ -1786,7 +1786,7 @@ function evalQualCheck(graph, ast, opts) {
1786
1786
 
1787
1787
  /** Compile any compositional AST to a result object traverse() returns for the
1788
1788
  * simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
1789
- export function evalComposite(graph, ast, opts = {}) {
1789
+ function evalComposite(graph, ast, opts = {}) {
1790
1790
  if (ast.node === "miss") return { compositeMiss: true, reason: ast.reason || null, matches: [] };
1791
1791
  if (ast.node === "exists") return evalExists(graph, ast);
1792
1792
  if (ast.node === "qualCheck") return evalQualCheck(graph, ast, opts);
@@ -1828,7 +1828,7 @@ const compositeList = (matches) => listJoin(matches.slice(0, OVERFLOW_CAP)
1828
1828
  + (matches.length > OVERFLOW_CAP ? `, …and ${matches.length - OVERFLOW_CAP} more` : "");
1829
1829
 
1830
1830
  /** A compositional worked example for the rephrase hint. */
1831
- export function compositionalHint() {
1831
+ function compositionalHint() {
1832
1832
  return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", "find me the payment class", or (after a listing) "which of those are tested"';
1833
1833
  }
1834
1834
 
@@ -2047,7 +2047,7 @@ export function rephraseHint() {
2047
2047
  * It gets a line that says what the store actually holds instead. A NULL
2048
2048
  * graph is UNKNOWN, not empty (see chat.mjs's noCodeGraph), so it keeps the
2049
2049
  * index-shaped advice. */
2050
- export function touchesRephraseHint(graph = null) {
2050
+ function touchesRephraseHint(graph = null) {
2051
2051
  if (graph && moduleCountOf(graph) === 0) {
2052
2052
  return "This store holds no code index, so it records no modules or commits to look through.";
2053
2053
  }
@@ -3516,7 +3516,7 @@ function isHelpRequest(query) {
3516
3516
  * they are content that may honestly fail to resolve.
3517
3517
  * 3. SYNONYM — rewrite surviving near-canonical words to the closed vocab.
3518
3518
  * Bounded (one token removed per noise iteration; hard guard) and deterministic. */
3519
- export function relaxParse(graph, query, { nlp = undefined, contextId = null, prev = null } = {}) {
3519
+ function relaxParse(graph, query, { nlp = undefined, contextId = null, prev = null } = {}) {
3520
3520
  const from = applyNegationFrames(normalizeQuery(String(query || "")));
3521
3521
  let tokens = splitWords(from);
3522
3522
  if (!tokens.length) return null;
@@ -128,11 +128,11 @@ function basename(p) {
128
128
 
129
129
  const isProvRef = (r) => /^(git|turn):/.test(String(r || ""));
130
130
 
131
- export function turnRefCount(ind) {
131
+ function turnRefCount(ind) {
132
132
  return (ind?.derived_from || []).filter(isProvRef).length;
133
133
  }
134
134
 
135
- export function mentionTotal(ind) {
135
+ function mentionTotal(ind) {
136
136
  const fromMentions = (ind?.mentions || []).reduce((n, m) => n + (Number(m?.count) || 0), 0);
137
137
  return fromMentions + turnRefCount(ind);
138
138
  }
@@ -1402,7 +1402,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
1402
1402
 
1403
1403
  /** Map of lowercased author name → that author's Commit individuals (payload order).
1404
1404
  * Tolerates both attribute-key conventions (author / commitAuthor), like commitLine. */
1405
- export function authorIndex(graph) {
1405
+ function authorIndex(graph) {
1406
1406
  const idx = new Map();
1407
1407
  for (const ind of graph?.individuals || []) {
1408
1408
  if ((ind.class || "") !== "Commit") continue;
@@ -0,0 +1,87 @@
1
+ // corpus-matrix.mjs — the fold and the two gap heuristics behind the
2
+ // capability-by-lane coverage matrix, plus the table renderer. Pure: rows in,
3
+ // counts and text out, so the heuristics can be tested against a handful of
4
+ // made-up rows instead of whatever test/corpus happens to hold today.
5
+ //
6
+ // scripts/corpus-matrix.mjs keeps the readdir, the readFile and the printing.
7
+
8
+ /** A row's capability group: the first two dot-segments of its key, so
9
+ * "ask.alias.two-hop" and "ask.alias.miss" are one capability. */
10
+ export const groupOfKey = (key) => key.split(".").slice(0, 2).join(".");
11
+
12
+ /** The key a row is counted under. A row with no key is still a row, and
13
+ * hiding it would understate the lane. */
14
+ export const keyOfRow = (row) => String(row.key ?? "(no key)");
15
+
16
+ /** A key segment naming a miss, a guard or a negation — the row that pins what
17
+ * a capability DECLINES to do, as opposed to its happy path. */
18
+ const NEGATIVE_RE = /(honest-miss|miss|guard|negation|negative|never|decline|refus|unsolvable|unknown|hedge|no-antecedent|untouched|empty)/;
19
+
20
+ export const isNegativeKey = (key) => NEGATIVE_RE.test(key);
21
+
22
+ /** Fold `{ lane, row }` pairs into the two indexes every view needs: the count
23
+ * per group per lane, and the full keys each group was built from. */
24
+ export function tallyRows(entries) {
25
+ const counts = new Map(); // group -> Map<lane, rowCount>
26
+ const fullKeys = new Map(); // group -> Set<full key>
27
+ for (const { lane, row } of entries) {
28
+ const key = keyOfRow(row);
29
+ const group = groupOfKey(key);
30
+ if (!counts.has(group)) counts.set(group, new Map());
31
+ const perLane = counts.get(group);
32
+ perLane.set(lane, (perLane.get(lane) ?? 0) + 1);
33
+ if (!fullKeys.has(group)) fullKeys.set(group, new Set());
34
+ fullKeys.get(group).add(key);
35
+ }
36
+ return { counts, fullKeys };
37
+ }
38
+
39
+ /** The groups the gap heuristics judge. bench.* rows assert a rig runs rather
40
+ * than pinning a capability, so "no negative row" says nothing there. */
41
+ export const behaviourGroups = ({ counts }) =>
42
+ [...counts.keys()].filter((g) => !g.startsWith("bench.")).sort();
43
+
44
+ const rowTotal = (counts, group) => [...counts.get(group).values()].reduce((a, b) => a + b, 0);
45
+
46
+ /** Groups a single row pins end to end. A review candidate, not a hole. */
47
+ export function thinGroups(tally) {
48
+ return behaviourGroups(tally).filter((g) => rowTotal(tally.counts, g) === 1);
49
+ }
50
+
51
+ /** Groups whose keys never name a miss, guard or negation — a happy path is
52
+ * pinned and the decline is not. A review candidate, not a hole. */
53
+ export function groupsWithNoNegativeRow(tally) {
54
+ return behaviourGroups(tally).filter((g) => ![...tally.fullKeys.get(g)].some(isNegativeKey));
55
+ }
56
+
57
+ /** The lanes a group has rows in, in the order given. */
58
+ export const lanesOfGroup = ({ counts }, group) => [...counts.get(group).keys()];
59
+
60
+ /** One row per group, one column per lane, an empty cell where a lane has no
61
+ * row for that group. The header row comes first. */
62
+ export function matrixRows({ counts }, lanes) {
63
+ const groups = [...counts.keys()].sort();
64
+ return [
65
+ ["key", ...lanes],
66
+ ...groups.map((group) => [
67
+ group,
68
+ ...lanes.map((lane) => {
69
+ const n = counts.get(group).get(lane);
70
+ return n ? String(n) : "";
71
+ }),
72
+ ]),
73
+ ];
74
+ }
75
+
76
+ /** `rows` (header first) as fixed-width text, with a rule under the header.
77
+ * Each column is as wide as its widest cell; trailing padding is trimmed. */
78
+ export function renderTable(rows) {
79
+ const [header, ...body] = rows;
80
+ const widths = header.map((h, col) => Math.max(h.length, ...body.map((r) => r[col].length)));
81
+ const renderLine = (cells) => cells.map((c, col) => c.padEnd(widths[col])).join(" ").trimEnd();
82
+ return [
83
+ renderLine(header),
84
+ renderLine(widths.map((w) => "-".repeat(w))),
85
+ ...body.map(renderLine),
86
+ ].join("\n");
87
+ }
@@ -8,7 +8,7 @@
8
8
  // punctuation, morphology is the lexicon's suffix fold.
9
9
  //
10
10
  // parseAce(sentence, lexicon) → { pattern, triples, residue } | null
11
- // pattern one of the PATTERNS below (also exported individually).
11
+ // pattern one of the PATTERNS below.
12
12
  // triples [{ subject, predicate, object, kind, n? }] — OWL-labelled string
13
13
  // triples shaped for src/adapters/memory/core.mjs's appendFact (which
14
14
  // normalizes subject/object via normFactTerm: "tmct:module" is
@@ -36,18 +36,18 @@ import {
36
36
  // singularOnly below, and lexicon.mjs's lookupNoun doc for what this prunes).
37
37
  const SINGULAR_ONLY_DET = new Set(["a", "an"]);
38
38
 
39
- export const PATTERN_SUB_CLASS_OF = "subClassOf";
40
- export const PATTERN_TYPE_ASSERTION = "typeAssertion";
41
- export const PATTERN_RELATION = "relation";
42
- export const PATTERN_SOME_VALUES_FROM = "someValuesFrom";
43
- export const PATTERN_CARDINALITY = "cardinality";
44
- export const PATTERN_DISJOINT_WITH = "disjointWith";
45
- export const PATTERN_POSSESSIVE = "possessive";
46
- export const PATTERN_ADJECTIVE = "adjective";
47
- export const PATTERN_CAPABILITY = "capability";
39
+ const PATTERN_SUB_CLASS_OF = "subClassOf";
40
+ const PATTERN_TYPE_ASSERTION = "typeAssertion";
41
+ const PATTERN_RELATION = "relation";
42
+ const PATTERN_SOME_VALUES_FROM = "someValuesFrom";
43
+ const PATTERN_CARDINALITY = "cardinality";
44
+ const PATTERN_DISJOINT_WITH = "disjointWith";
45
+ const PATTERN_POSSESSIVE = "possessive";
46
+ const PATTERN_ADJECTIVE = "adjective";
47
+ const PATTERN_CAPABILITY = "capability";
48
48
 
49
49
  /** The pattern field's full domain, in the README's table order. */
50
- export const PATTERNS = Object.freeze([
50
+ const PATTERNS = Object.freeze([
51
51
  PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM,
52
52
  PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE,
53
53
  PATTERN_CAPABILITY,
@@ -25,7 +25,7 @@
25
25
  import coreLexiconRaw from "./lexicon-core.json" with { type: "json" };
26
26
 
27
27
  /** The CURIE namespace every tmct lexicon mints terms under. */
28
- export const DEFAULT_NS = "tmct:";
28
+ const DEFAULT_NS = "tmct:";
29
29
 
30
30
  /** Determiner tokens the grammar consumes (pattern table's every/a/no…). */
31
31
  export const DETERMINERS = Object.freeze({
@@ -160,7 +160,7 @@ export function lookupNoun(lexicon, word, opts = {}) {
160
160
  /** Every lexicon entry `word` could plausibly resolve to, ranked the same as
161
161
  * lookupNoun's top choice but without discarding a genuine alternate (e.g.
162
162
  * die/dice returns both entries). */
163
- export function lookupNounCandidates(lexicon, word, opts = {}) {
163
+ function lookupNounCandidates(lexicon, word, opts = {}) {
164
164
  const w = String(word ?? "").toLowerCase();
165
165
  const standalone = lexicon.nouns.get(w);
166
166
  const irregular = lexicon.nounPlurals.get(w);
@@ -195,7 +195,7 @@ export function lookupVerb(lexicon, word) {
195
195
 
196
196
  /** Every verb entry `word` could plausibly resolve to via foldCandidates,
197
197
  * most-specific-fold-first — the verb sibling of lookupNounCandidates. */
198
- export function lookupVerbCandidates(lexicon, word) {
198
+ function lookupVerbCandidates(lexicon, word) {
199
199
  const w = String(word ?? "").toLowerCase();
200
200
  const out = [];
201
201
  const seen = new Set();
@@ -0,0 +1,67 @@
1
+ // inflect.mjs — the regular English -s/-ed/-ing rules, applied to a lemma.
2
+ //
3
+ // WordNet carries lemmas only ("rest" is present, "rests" is absent), and it is
4
+ // the inflected forms that collide with the fuzzy repair tier's targets —
5
+ // "rests" is one edit from "tests". So the real-word collision table expands
6
+ // every lemma through these rules before it looks for collisions.
7
+ //
8
+ // These are the REGULAR rules and nothing else. No irregular table, no stress
9
+ // model: pastOf("run") is "runned" and pastOf("make") is "maked". That is the
10
+ // intended shape. The table's job is to name words the repair tier must not
11
+ // rewrite, and inflectionsOf is generous on purpose (see below) — an extra form
12
+ // costs one repair we decline to make, and the sentence misses honestly, while
13
+ // a missing form costs a real word rewritten into a different question,
14
+ // answered with confidence. The first is the cheaper mistake.
15
+
16
+ import { STOPWORDS } from "./interpret/normalize.mjs";
17
+ import {
18
+ FUZZY_TARGET_WORDS, FUZZY_REPAIR_MIN_LENGTH, fuzzyMatchInSet, fuzzyBound,
19
+ } from "./interpret/fuzzy.mjs";
20
+
21
+ const VOWELS = new Set(["a", "e", "i", "o", "u"]);
22
+ const isVowel = (c) => VOWELS.has(c);
23
+
24
+ /** A single final consonant after a single vowel doubles before -ed/-ing
25
+ * ("run" -> "running"). w, x and y never double. Stress is not modelled, so a
26
+ * second syllable doubles too ("visit" -> "visitting"). */
27
+ export function doublesFinalConsonant(w) {
28
+ const [c3, c2, c1] = [w.at(-3), w.at(-2), w.at(-1)];
29
+ if (!c3 || isVowel(c1) || "wxy".includes(c1)) return false;
30
+ return isVowel(c2) && !isVowel(c3);
31
+ }
32
+
33
+ export function pluralOf(w) {
34
+ if (/(?:s|x|z|ch|sh)$/.test(w)) return `${w}es`;
35
+ if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ies`;
36
+ return `${w}s`;
37
+ }
38
+
39
+ export function pastOf(w) {
40
+ if (w.endsWith("e")) return `${w}d`;
41
+ if (/[^aeiou]y$/.test(w)) return `${w.slice(0, -1)}ied`;
42
+ if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ed`;
43
+ return `${w}ed`;
44
+ }
45
+
46
+ export function gerundOf(w) {
47
+ if (w.endsWith("ie")) return `${w.slice(0, -2)}ying`;
48
+ if (w.endsWith("e") && !/(?:ee|oe|ye)$/.test(w)) return `${w.slice(0, -1)}ing`;
49
+ if (doublesFinalConsonant(w)) return `${w}${w.at(-1)}ing`;
50
+ return `${w}ing`;
51
+ }
52
+
53
+ /** Every surface form of `w` the collision table counts as real English. */
54
+ export const inflectionsOf = (w) => [w, pluralOf(w), pastOf(w), gerundOf(w)];
55
+
56
+ /** The words in `realWords` that the repair tier would rewrite onto one of its
57
+ * targets: long enough to reach the tier, not a stopword, not a target itself,
58
+ * and within the fuzzy bound of some target. Sorted, so the table it feeds is
59
+ * reproducible. */
60
+ export function collisionsFrom(realWords) {
61
+ return [...realWords]
62
+ .filter((w) => w.length >= FUZZY_REPAIR_MIN_LENGTH)
63
+ .filter((w) => !STOPWORDS.has(w))
64
+ .filter((w) => !FUZZY_TARGET_WORDS.includes(w))
65
+ .filter((w) => fuzzyMatchInSet(w, FUZZY_TARGET_WORDS, fuzzyBound(w)) !== null)
66
+ .sort();
67
+ }