@polycode-projects/the-mechanical-code-talker 3.0.6 → 3.0.8

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/README.md CHANGED
@@ -17,6 +17,30 @@ Teach it a fact in plain English and it mints a node. Ask it a question and
17
17
  it answers from what it was seeded with, what you taught it, and what it can
18
18
  derive by rule from both. Every answer is either grounded or an honest miss.
19
19
 
20
+ ## Repository layout
21
+
22
+ | directory | contains |
23
+ |---|---|
24
+ | `bin/` | the CLI entrypoint (`tmct.mjs`) |
25
+ | `src/` | the shipped product: `domain/` (pure logic), `adapters/` (I/O, storage, providers), `services/` (chat, adventure, research, ledger, plan), `surfaces/` (CLI, HTTP, TUI, web), `index/` (repo indexing) |
26
+ | `corpus/` | committed corpus and template data (ConceptNet, WordNet, NameNet, the generated persona vocab) |
27
+ | `data/` | seed data assets: sprites, phrasebook, games, response templates |
28
+ | `ontology/` | the software ontology (`tmct-core.ttl`) and memory shapes, in Turtle |
29
+ | `scripts/` | build, check, and maintenance scripts (`npm run` targets live here) |
30
+ | `examples/` | runnable example scripts and fixture repos used by the README's own examples and the test suite |
31
+ | `demo/` | standalone demo scripts (e.g. the agentic-loop demo) |
32
+ | `electron/` | the Electron desktop app wrapper |
33
+ | `docs/` | reference docs: the adapter/repository-interface contracts, bibliography |
34
+ | `test/` | the unit, corpus, and estate-guard test suite (`npm test`) |
35
+ | `test-e2e/` | the end-to-end suite: real CLI/TUI spawns and Playwright browser journeys (`npm run test:e2e`) |
36
+ | `test-benchmarks/` | the benchmark harnesses (agentbench, chatbench, idxbench, infbench, ingestbench, researchbench, synthbench) and their shared `benchlib/` |
37
+ | `reports/` | benchmark write-ups (`BENCHMARK_*.md`) and `PAGE_WEIGHTS.md` — see the root `STATUS.md` for the one-page summary these feed |
38
+ | `playtests/` | numbered playtest session logs, one edge found and fixed per entry |
39
+ | `archive/` | delivered `PLAN_*.md`/`BENCHMARK_*.md` docs, kept for history |
40
+ | `public/` | the built demo site — a gitignored output of `npm run demo:build`, never hand-edited |
41
+
42
+ `node_modules/` (dependencies) and dotfiles/hidden tooling directories are omitted above.
43
+
20
44
  ## Teach it, then ask it to reason
21
45
 
22
46
  This is real, runnable output. No cherry-picking, no model anywhere in the
@@ -227,7 +251,7 @@ resolves to a real graph traversal or declines cleanly:
227
251
  (*because/although/while*), conditionals, and false-premise flags ("why
228
252
  does X still import Y" when it no longer does).
229
253
 
230
- The full catalog with measured coverage lives in the `BENCHMARK_*.md`
254
+ The full catalog with measured coverage lives in the `reports/BENCHMARK_*.md`
231
255
  reports.
232
256
 
233
257
  **Response finishing.** Before an answer prints, it is segmented into typed
@@ -1179,9 +1203,11 @@ service. The LLM agent stays outside tmct, as the no-LLM ethos requires.
1179
1203
 
1180
1204
  ## Measuring it
1181
1205
 
1182
- What the 2.7.11/2.7.12 cycle measured, on 2026-07-19. Each figure links to its
1183
- method and carries, in the same row, the caveat that changes what it means.
1184
- The full tables, judge scores, and transcripts are in the linked write-ups.
1206
+ **For the latest measured numbers, see `STATUS.md`** a one-page summary of the most
1207
+ recent full sweep, citing its source reports by name. What follows here is what the 2.7.11/2.7.12
1208
+ cycle measured, on 2026-07-19, kept for its own detail. Each figure links to its method and
1209
+ carries, in the same row, the caveat that changes what it means. The full tables, judge scores,
1210
+ and transcripts are in the linked write-ups.
1185
1211
 
1186
1212
  | What it does | Result (2.7.12) | Read the number with this | Method |
1187
1213
  |---|---|---|---|
@@ -1208,10 +1234,10 @@ The smallest real slice of each, the same invocations the test suite's
1208
1234
  bench-smoke lane replays:
1209
1235
 
1210
1236
  ```bash cwd=repo
1211
- node chatbench/run.mjs --stamp smoke --only g-a1-naming-1 --out /tmp/chatbench-smoke
1212
- node infbench/generate-cases.mjs --out /tmp/infbench-cases.jsonl
1213
- node infbench/run.mjs --cases /tmp/infbench-cases.jsonl --only inf-1-lookup-subClassOf-001 --stamp smoke --out /tmp/infbench-smoke
1214
- node agentbench/run.mjs --stamp smoke --driver stub --only ab-a0-describe-widget --out /tmp/agentbench-smoke
1237
+ node test-benchmarks/chatbench/run.mjs --stamp smoke --only g-a1-naming-1 --out /tmp/chatbench-smoke
1238
+ node test-benchmarks/infbench/generate-cases.mjs --out /tmp/infbench-cases.jsonl
1239
+ node test-benchmarks/infbench/run.mjs --cases /tmp/infbench-cases.jsonl --only inf-1-lookup-subClassOf-001 --stamp smoke --out /tmp/infbench-smoke
1240
+ node test-benchmarks/agentbench/run.mjs --stamp smoke --driver stub --only ab-a0-describe-widget --out /tmp/agentbench-smoke
1215
1241
  ```
1216
1242
 
1217
1243
  Grading beyond tier 1 uses an LLM as judge. The offline eval harness is the
@@ -1229,7 +1255,6 @@ hardened:
1229
1255
  - CI runs **SAST and secret detection**.
1230
1256
  - A nightly **`npm audit` + OSV-Scanner** job watches dependencies.
1231
1257
  - Releases are published with **npm provenance** (`--provenance`).
1232
- - A coordinated-disclosure `SECURITY.md` policy covers reports.
1233
1258
 
1234
1259
  The content-address hash is single-sourced in `src/domain/hash.mjs`, so the
1235
1260
  cross-version-stable fact-id contract has exactly one definition.
@@ -1291,7 +1316,7 @@ edition, the retrieval date, the terms tmct uses, and what could not be verified
1291
1316
 
1292
1317
  | source | edition | what tmct uses it for |
1293
1318
  |---|---|---|
1294
- | Council of Europe, CEFR — Companion volume | 2020, ISBN 978-92-871-8621-8 | The band labels A1–C2 the chat benchmark grades against. CEFR measures what a *person* can do communicatively; grading the difficulty of *prompts* by band is tmct's adaptation, not a CEFR-validated use. The band descriptions in `chatbench/GRADED.md` are tmct's own prose. |
1319
+ | Council of Europe, CEFR — Companion volume | 2020, ISBN 978-92-871-8621-8 | The band labels A1–C2 the chat benchmark grades against. CEFR measures what a *person* can do communicatively; grading the difficulty of *prompts* by band is tmct's adaptation, not a CEFR-validated use. The band descriptions in `test-benchmarks/chatbench/GRADED.md` are tmct's own prose. |
1295
1320
  | Reiter, "On Closed World Data Bases" | *Logic and Data Bases*, Plenum, 1978, pp. 55–76 | Both halves of the honest miss. The planner's operator model is **closed-world**, which is what makes a plan checkable. The chat layer is **open-world**: it will not read "no matching rule" as "the answer is no". |
1296
1321
  | Chow, "On optimum recognition error and reject tradeoff" | *IEEE Trans. Information Theory* 16(1), 1970 | Prior art for the goal. The literature calls a refusal **abstention**, or selective prediction, and Chow's reject option is its root. Those methods threshold a confidence score; tmct has none, and abstains because nothing matched — which is why the row above names the mechanism. |
1297
1322
  | Ji et al., "Survey of Hallucination in Natural Language Generation" | *ACM Computing Surveys* 55(12), 2023 | Groundedness, and what tmct is avoiding by having no model to hallucinate with. |
package/bin/tmct.mjs CHANGED
@@ -467,7 +467,7 @@ async function activatePluggableInput(repoRoot, resolved) {
467
467
  }
468
468
  if (resolved.manifestEntry) {
469
469
  // Preserve the ORIGINAL `--corpus <tier2-id>` wording byte-for-byte
470
- // (e2e/init-cli.test.mjs asserts on this exact shape).
470
+ // (test-e2e/init-cli.test.mjs asserts on this exact shape).
471
471
  return `seeded tier-2 corpus "${resolved.manifestEntry.id}" (${resolved.manifestEntry.kind}) — ${seeded.appended} fact(s) added`
472
472
  + `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${resolved.manifestEntry.file} (${resolved.manifestEntry.license}). `
473
473
  + `Activated in tmct.toml — future \`tmct init\`/chat sessions seed it automatically.\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
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; indexes a repo on request (tmct index) or reads any producer's graph.",
@@ -47,7 +47,8 @@
47
47
  "./createCompletionsGraphAdapter": "./src/services/completions.mjs",
48
48
  "./ingest": "./src/services/extract-facts.mjs",
49
49
  "./repository-interface": "./src/adapters/repository-interface.mjs",
50
- "./conformance": "./src/tools/conformance.mjs"
50
+ "./conformance": "./src/tools/conformance.mjs",
51
+ "./ask-browser": "./src/surfaces/web/graph-ask-browser-entry.mjs"
51
52
  },
52
53
  "files": [
53
54
  "bin/",
@@ -87,9 +88,11 @@
87
88
  "test": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 \"test/**/*.test.mjs\"",
88
89
  "test:smoke": "node --disable-warning=ExperimentalWarning --test test/smoke/*.test.mjs",
89
90
  "test:fast": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 test/smoke/*.test.mjs test/fast/*.test.mjs test/tools/ask.test.mjs test/tools/server.test.mjs test/estate/import-layers.test.mjs",
90
- "test:e2e": "node --disable-warning=ExperimentalWarning --test \"e2e/*.test.mjs\"",
91
- "test:e2e:heavy": "node --disable-warning=ExperimentalWarning --test \"e2e/heavy/*.test.mjs\"",
92
- "check:readme": "TMCT_CHECK_README=1 node --disable-warning=ExperimentalWarning --test e2e/readme-examples.test.mjs",
91
+ "test:unit": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 $(node scripts/list-unit-test-files.mjs)",
92
+ "test:slow": "node --disable-warning=ExperimentalWarning --test test/bench/synthbench-code.test.mjs test/estate/generated-artifacts.test.mjs test/readme/readme.test.mjs test/corpus/bench-smoke.test.mjs test/corpus/inference.test.mjs",
93
+ "test:e2e": "node --disable-warning=ExperimentalWarning --test \"test-e2e/*.test.mjs\"",
94
+ "test:e2e:heavy": "node --disable-warning=ExperimentalWarning --test \"test-e2e/heavy/*.test.mjs\"",
95
+ "check:readme": "TMCT_CHECK_README=1 node --disable-warning=ExperimentalWarning --test test-e2e/readme-examples.test.mjs",
93
96
  "e2e:browsers": "playwright install chromium",
94
97
  "gen:screenshots": "node scripts/gen-screenshots.mjs",
95
98
  "check:links": "node scripts/check-links.mjs",
@@ -128,16 +131,16 @@
128
131
  "serve:public": "node --disable-warning=ExperimentalWarning bin/tmct.mjs serve --host 0.0.0.0 --port 8787",
129
132
  "example:mini": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
130
133
  "example:polyglot": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
131
- "chatbench:run": "node chatbench/run.mjs",
132
- "chatbench:judge": "node chatbench/judge.mjs",
133
- "chatbench:judge:cached": "node chatbench/judge.mjs --cache chatbench/verdict-cache.json",
134
- "agentbench:run": "node agentbench/run.mjs",
135
- "infbench": "node infbench/generate-cases.mjs && node infbench/run.mjs",
136
- "idxbench:run": "node idxbench/run.mjs",
137
- "researchbench:run": "node researchbench/run.mjs",
138
- "synthbench:code": "node synthbench/code/run.mjs",
139
- "ingestbench:run": "node ingestbench/run.mjs",
140
- "ingestbench:judge": "node ingestbench/judge.mjs",
134
+ "chatbench:run": "node test-benchmarks/chatbench/run.mjs",
135
+ "chatbench:judge": "node test-benchmarks/chatbench/judge.mjs",
136
+ "chatbench:judge:cached": "node test-benchmarks/chatbench/judge.mjs --cache test-benchmarks/chatbench/verdict-cache.json",
137
+ "agentbench:run": "node test-benchmarks/agentbench/run.mjs",
138
+ "infbench": "node test-benchmarks/infbench/generate-cases.mjs && node test-benchmarks/infbench/run.mjs",
139
+ "idxbench:run": "node test-benchmarks/idxbench/run.mjs",
140
+ "researchbench:run": "node test-benchmarks/researchbench/run.mjs",
141
+ "synthbench:code": "node test-benchmarks/synthbench/code/run.mjs",
142
+ "ingestbench:run": "node test-benchmarks/ingestbench/run.mjs",
143
+ "ingestbench:judge": "node test-benchmarks/ingestbench/judge.mjs",
141
144
  "corpus:matrix": "node scripts/corpus-matrix.mjs",
142
145
  "corpus:matrix:gaps": "node scripts/corpus-matrix.mjs --gaps",
143
146
  "template:coverage": "node scripts/template-coverage.mjs",
@@ -10,7 +10,7 @@
10
10
  "ask.mjs metaFallbackEntityAnswer (\"X is a Y in this codebase, defined in Z ...\")"
11
11
  ],
12
12
  "deliberatelyNotUsedBy": [
13
- "ask.mjs renderCore whereShape (\"X is defined in Y at line(s) Z\" / \"X is defined in Y (no line span...)\") -- chatbench/graded-pool-max.jsonl pins this exact substring as ground truth for 11 'where is X defined' cases (2 in the always-run promoted subset), and that pool is append-only/never-edited-mid-arc per SKILL_BENCHMARK_CEFR_ENGLISH.md."
13
+ "ask.mjs renderCore whereShape (\"X is defined in Y at line(s) Z\" / \"X is defined in Y (no line span...)\") -- test-benchmarks/chatbench/graded-pool-max.jsonl pins this exact substring as ground truth for 11 'where is X defined' cases (2 in the always-run promoted subset), and that pool is append-only/never-edited-mid-arc per SKILL_BENCHMARK_CEFR_ENGLISH.md."
14
14
  ]
15
15
  },
16
16
  "is-dated": {
@@ -287,6 +287,10 @@ export const ENTITY_TO_TYPE = Object.freeze({
287
287
  method: "Method", methods: "Method",
288
288
  class: "Class", classes: "Class",
289
289
  module: "Module", modules: "Module", mod: "Module", mods: "Module", file: "Module", files: "Module",
290
+ // "Package" is a pseudo-type like "Change" below: no node is ever stored
291
+ // with that class. ask.mjs derives packages from module labels, the same
292
+ // derivation the architecture map uses.
293
+ package: "Package", packages: "Package",
290
294
  attribute: "Attribute", attributes: "Attribute", field: "Attribute", fields: "Attribute",
291
295
  variable: "GlobalVariable", variables: "GlobalVariable", global: "GlobalVariable", globals: "GlobalVariable",
292
296
  // "Change" is a pseudo-type, not a node class: ask.mjs's traverse() reads it
@@ -15,7 +15,7 @@
15
15
  // ask.mjs's own `touches`/`cochange` verbs answer one-hop structural edges
16
16
  // (mgx:touchedByCommit / mgx:changeCoupledWith).
17
17
 
18
- import { relationKind, impactClosure, moduleCountOf, normPath, HISTORY_CAP } from "./codegraph.mjs";
18
+ import { relationKind, impactClosure, moduleCountOf, normPath, packageCounts, modulesOf, HISTORY_CAP } from "./codegraph.mjs";
19
19
  import { isTestPath } from "./module-paths.mjs";
20
20
  import {
21
21
  RELATIONS,
@@ -84,6 +84,7 @@ const PLURAL_FORMS = {
84
84
  Attribute: ["attribute", "attributes"], GlobalVariable: ["variable", "variables"],
85
85
  Commit: ["commit", "commits"],
86
86
  Change: ["change", "changes"],
87
+ Package: ["package", "packages"],
87
88
  Fact: ["fact", "facts"], Utterance: ["utterance", "utterances"],
88
89
  Session: ["session", "sessions"], Source: ["source", "sources"], Rule: ["rule", "rules"],
89
90
  };
@@ -92,6 +93,23 @@ function nounFor(entityType, n) {
92
93
  return n === 1 ? s : p;
93
94
  }
94
95
 
96
+ /** A discourse `set` referent for a class-homogeneous result list — the typed
97
+ * content a listing/filter answer establishes, so a later "which of those …"
98
+ * binds it (see evalCommitFilter for the shape the session layer registers).
99
+ * Returns an empty array when nothing typed can be registered: no member
100
+ * class, or an empty result. `extra` carries a lane's own sibling flags (the
101
+ * anaphora lane's `bound: true`). */
102
+ function setReferentsFor(cls, matches, lane, extra = {}) {
103
+ if (!cls || !matches.length) return [];
104
+ return [{
105
+ kind: "set", class: cls,
106
+ label: `${matches.length} ${nounFor(cls, matches.length)}`,
107
+ ids: matches.map((m) => m.id),
108
+ attrs: { count: matches.length },
109
+ lane, ...extra,
110
+ }];
111
+ }
112
+
95
113
  /** A class enum rendered as prose words ("GlobalVariable" -> "global
96
114
  * variable"), lowercase to match nounFor's own convention. For the render
97
115
  * sites that must name the enum itself rather than a curated PLURAL_FORMS
@@ -834,7 +852,7 @@ function parseAggregate(w, lc, nlp) {
834
852
 
835
853
  const LIST_SKIP = new Set(["the", "a", "an", "all", "me", "us"]);
836
854
  const LIST_TRIGGERS_SORTED = [...LIST_TRIGGERS].sort((a, b) => b.split(" ").length - a.split(" ").length);
837
- const LISTABLE_KINDS = "functions, classes, methods, modules, attributes, variables, or commits";
855
+ const LISTABLE_KINDS = "functions, classes, methods, modules, packages, attributes, variables, or commits";
838
856
  // A leading "in"/"inside"/"under" right after the entity noun is an
839
857
  // unambiguous location-scope tail, never a reverse-clause predicate object.
840
858
  const SCOPE_PREPOSITIONS = new Set(["in", "inside", "under"]);
@@ -1621,11 +1639,25 @@ function computeFind(graph, entityType, term) {
1621
1639
  return { narrow: [], broad: sortFindHits([...broadHits.values()]) };
1622
1640
  }
1623
1641
 
1642
+ /** The graph's packages as list/count-shaped individuals. Packages are derived
1643
+ * from module labels rather than stored as nodes (see codegraph.mjs's
1644
+ * packageCounts), so these carry a `pkg:` id of their own and resolve to
1645
+ * nothing in graph.byId — a follow-up that tries to traverse one gets an
1646
+ * empty result, never a wrong one. Ordered by module count, the same order
1647
+ * the architecture map prints, so the two surfaces agree. */
1648
+ function packageIndividuals(graph) {
1649
+ return [...packageCounts(modulesOf(graph)).entries()]
1650
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
1651
+ .map(([dir]) => ({ id: `pkg:${dir}`, label: dir, class: "Package" }));
1652
+ }
1653
+
1624
1654
  /** Compile a set-producing AST into an array of individuals. */
1625
1655
  function evalSet(graph, ast, opts) {
1626
1656
  switch (ast.node) {
1627
1657
  case "clause": return traverse(graph, ast.clause, opts).matches || [];
1628
- case "allOfClass": return graph.individuals.filter((i) => i.class === ast.entityType);
1658
+ case "allOfClass":
1659
+ if (ast.entityType === "Package") return packageIndividuals(graph);
1660
+ return graph.individuals.filter((i) => i.class === ast.entityType);
1629
1661
  // Predicate-find as a set atom: the narrow-then-broaden cascade's result,
1630
1662
  // transparently flattened ("related, not exact" is a render concern).
1631
1663
  case "find": {
@@ -1806,7 +1838,13 @@ function evalAnaphora(graph, ast, opts) {
1806
1838
  const emptyClass = f && f.type === "entity" ? f.entityType : sameClass(baseItems);
1807
1839
  const common = items.length ? sameClass(items) : emptyClass;
1808
1840
  if (ast.mode === "count") return { compositeKind: "count", count: items.length, entityType: common, matches: [] };
1809
- return { compositeKind: "set", matches: items, entityType: common };
1841
+ // A narrowed, class-homogeneous result registers as a NEW set referent so
1842
+ // the narrowing survives a later count and binds a further "which of those".
1843
+ // It rides `bound: true` (a follow-up derived FROM the standing set), so the
1844
+ // session layer's register() does not evict the set on the class change a
1845
+ // narrowing produces.
1846
+ const referents = setReferentsFor(common, items, "anaphora", { bound: true });
1847
+ return { compositeKind: "set", matches: items, entityType: common, referents };
1810
1848
  }
1811
1849
 
1812
1850
  // Structural kinds counted for "most-connected" (total degree). Symbol-grain and
@@ -1923,7 +1961,17 @@ function evalSuperlative(graph, ast) {
1923
1961
  if (!scored.length) return { compositeKind: "superlative", entityType: ast.entityType, matches: [] };
1924
1962
  const best = scored[0].score;
1925
1963
  const winners = scored.filter((s) => s.score === best).map((s) => s.ind);
1926
- return { compositeKind: "superlative", entityType: ast.entityType, metricNoun: ast.metricNoun, extreme: ast.extreme, score: best, matches: winners };
1964
+ // What the ranking established, as discourse referents (see evalCommitFilter
1965
+ // for the shape the session layer registers). A lone winner is one entity a
1966
+ // later "it"/"this"/"that" can bind; a metric tie (two winners on the same
1967
+ // score) is a set instead, since no singular form should silently pick one.
1968
+ // The score itself is always a measure referent, so "is that bigger than X"
1969
+ // binds the number the ranking produced rather than re-deriving it.
1970
+ const referents = winners.length >= 2
1971
+ ? setReferentsFor(ast.entityType, winners, "superlative")
1972
+ : [{ kind: "entity", class: ast.entityType, label: winners[0].label, ids: [winners[0].id], attrs: {}, lane: "superlative" }];
1973
+ referents.push({ kind: "measure", label: `${best} ${ast.metricNoun}`, ids: [], attrs: { metric: best }, lane: "superlative" });
1974
+ return { compositeKind: "superlative", entityType: ast.entityType, metricNoun: ast.metricNoun, extreme: ast.extreme, score: best, matches: winners, referents };
1927
1975
  }
1928
1976
 
1929
1977
  /** Top-level "<kind> of/in <owner>" membership eval, covering both a bare
@@ -1938,6 +1986,9 @@ function evalMembershipComposite(graph, ast, opts) {
1938
1986
  const filterFn = qualNode
1939
1987
  ? (ind) => qualNode.filters.every((f) => qualHolds(graph, ind, QUALIFIERS[f]))
1940
1988
  : null;
1989
+ // A class-typed membership result registers as a discourse set referent (see
1990
+ // evalCommitFilter) so a later "which of those …" binds it.
1991
+ const membershipReferents = (list) => setReferentsFor(entityType, list, "membership");
1941
1992
  const owner = resolveMembershipOwner(graph, memNode.term, opts && opts.contextId);
1942
1993
  if (owner.kind === "dir") {
1943
1994
  let objs;
@@ -1947,14 +1998,16 @@ function evalMembershipComposite(graph, ast, opts) {
1947
1998
  objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids))).filter((o) => o.class === entityType);
1948
1999
  }
1949
2000
  if (filterFn) objs = objs.filter(filterFn);
1950
- return { compositeKind: "set", matches: objs, entityType };
2001
+ return { compositeKind: "set", matches: objs, entityType, referents: membershipReferents(objs) };
1951
2002
  }
1952
2003
  if (owner.kind === "miss") return { compositeKind: "set", matches: [], entityType };
1953
2004
  const { own, inherited, viaLabel } = computeMembership(graph, owner.id, owner.entityClass, entityType, filterFn);
1954
2005
  const inheritedNotOwn = !own.length && inherited.length > 0;
2006
+ const finalMatches = inheritedNotOwn ? inherited : own;
1955
2007
  return {
1956
- compositeKind: "membership", entityType, matches: inheritedNotOwn ? inherited : own,
2008
+ compositeKind: "membership", entityType, matches: finalMatches,
1957
2009
  inheritedNotOwn, viaLabel, ownerLabel: owner.label,
2010
+ referents: membershipReferents(finalMatches),
1958
2011
  };
1959
2012
  }
1960
2013
 
@@ -2019,7 +2072,11 @@ function evalQualCheck(graph, ast, opts) {
2019
2072
  }
2020
2073
  const rawHolds = qualHolds(graph, r.match, QUALIFIERS[qualifier]);
2021
2074
  const holds = negated ? !rawHolds : rawHolds;
2022
- return { compositeKind: "qualCheck", subject: r.match, qualifier, negated, holds, matches: [r.match] };
2075
+ // The subject the check resolved is a discourse referent either way — the
2076
+ // answer being "no" does not make the entity any less bindable by a
2077
+ // follow-up (see evalCommitFilter for the registration shape).
2078
+ const referents = [{ kind: "entity", class: r.match.class, label: r.match.label, ids: [r.match.id], attrs: {}, lane: "qualCheck" }];
2079
+ return { compositeKind: "qualCheck", subject: r.match, qualifier, negated, holds, matches: [r.match], referents };
2023
2080
  }
2024
2081
 
2025
2082
  /** Universal-over-a-set: the object is grounded first (an unknown one is an
@@ -2102,7 +2159,14 @@ function evalComposite(graph, ast, opts = {}) {
2102
2159
  && !(Array.isArray(opts.prev) && opts.prev.length)) {
2103
2160
  return { compositeMiss: true, reason: "no-prev", matches: [] };
2104
2161
  }
2105
- return { compositeKind: "set", matches: evalSet(graph, ast, opts), entityType: ast.entityType || null };
2162
+ const matches = evalSet(graph, ast, opts);
2163
+ const entityType = ast.entityType || null;
2164
+ // A resolved, class-typed listing/filter set registers as a discourse
2165
+ // referent (see evalCommitFilter) so a later "which of those …" binds it. A
2166
+ // qualifier listing ("which modules are tested") registers the same way, just
2167
+ // under its own lane name.
2168
+ const referents = setReferentsFor(entityType, matches, ast.node === "qualifier" ? "qualifierListing" : "compositeSet");
2169
+ return { compositeKind: "set", matches, entityType, referents };
2106
2170
  }
2107
2171
 
2108
2172
  // ---- compositional render: templated, same "honest miss vs cited hit"
@@ -2200,7 +2264,7 @@ function renderComposite(parsed, result, graph) {
2200
2264
  }
2201
2265
  // Kinds that don't live in a module (or have no module-scope parse at
2202
2266
  // all, like memory-graph classes) get no narrow-by-module hint.
2203
- const scopeable = !["Module", "Commit", "Fact", "Utterance", "Session", "Source", "Rule"].includes(result.entityType);
2267
+ const scopeable = !["Module", "Package", "Commit", "Fact", "Utterance", "Session", "Source", "Rule"].includes(result.entityType);
2204
2268
  const hint = (!result.scoped && scopeable && result.matches.length > OVERFLOW_CAP)
2205
2269
  ? ` — narrow with "${nounFor(result.entityType, 2)} in <module>"`
2206
2270
  : "";
@@ -3111,8 +3175,15 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
3111
3175
  }
3112
3176
  commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
3113
3177
  }
3178
+ // The dated commit this answer named is a discourse `event` referent, so a
3179
+ // later "was that before X was touched" binds it (see evalCommitFilter).
3180
+ // Only when a dated commit resolved — an undated one is the render's honest
3181
+ // miss, and a referent with no date could not feed the comparison lane.
3182
+ const referents = commits.length && dateOf(commits[0])
3183
+ ? [{ kind: "event", class: "Commit", label: commits[0].label, ids: [commits[0].id], attrs: { date: dateOf(commits[0]).slice(0, 10) }, lane: "when" }]
3184
+ : [];
3114
3185
  return {
3115
- matches: commits, objMatch, candidates, ambiguous, matchedVia, whenShape: true,
3186
+ matches: commits, objMatch, candidates, ambiguous, matchedVia, whenShape: true, referents,
3116
3187
  traversal: `touches+touchesSymbol edges where object = ${objMatch.label}, newest commit date first`,
3117
3188
  };
3118
3189
  }
@@ -3135,8 +3206,14 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
3135
3206
  if (c && c.class === "Commit") commits.push(c);
3136
3207
  }
3137
3208
  commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
3209
+ // The dated commit behind the "who last touched X" answer is the same
3210
+ // `event` referent the when-shape registers, so either phrasing feeds a
3211
+ // later temporal comparison. Registered only when the commit carries a date.
3212
+ const referents = commits.length && dateOf(commits[0])
3213
+ ? [{ kind: "event", class: "Commit", label: commits[0].label, ids: [commits[0].id], attrs: { date: dateOf(commits[0]).slice(0, 10) }, lane: "whoLast" }]
3214
+ : [];
3138
3215
  return {
3139
- matches: commits, objMatch, candidates, ambiguous, matchedVia, whoLastShape: true,
3216
+ matches: commits, objMatch, candidates, ambiguous, matchedVia, whoLastShape: true, referents,
3140
3217
  traversal: `touches+touchesSymbol edges where object = ${objMatch.label}, newest commit's author`,
3141
3218
  };
3142
3219
  }
@@ -4409,6 +4486,18 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
4409
4486
  content = `${content}\n(answering for ${result.objMatch.label} — ${others.length} other match${others.length === 1 ? "" : "es"}: ${list})`;
4410
4487
  }
4411
4488
  }
4489
+ // A plain relation LISTING ("which modules import X", "which functions call
4490
+ // X") resolves through the simple traverse path, not evalComposite, so it
4491
+ // registers its class-typed result here — the same set referent the
4492
+ // composite listing lanes emit, so a later "which of those …" binds it. Only
4493
+ // a real, non-empty, class-homogeneous answer no lane already registered
4494
+ // (the composite lanes set result.referents themselves).
4495
+ if (!Array.isArray(result.referents) && !rendered.miss && !rendered.ambiguous
4496
+ && (parsed?.shape === "reverse" || parsed?.shape === "forward")
4497
+ && parsed.entityType && Array.isArray(result.matches) && result.matches.length
4498
+ && result.matches.every((m) => m && m.class === parsed.entityType)) {
4499
+ result = { ...result, referents: setReferentsFor(parsed.entityType, result.matches, "relationListing") };
4500
+ }
4412
4501
  return {
4413
4502
  content,
4414
4503
  tmct_ask: {
@@ -1277,19 +1277,36 @@ export function renderSubclasses(graph, ind) {
1277
1277
  const ARCH_PKG_CAP = 25;
1278
1278
  const ARCH_HUB_CAP = 15;
1279
1279
 
1280
+ /** The directories that group a set of modules, each mapped to how many
1281
+ * modules it holds. A package is not a node class — no individual is ever
1282
+ * stored with class "Package" — so every surface that reports packages
1283
+ * (the architecture map, ask()'s package list and count) derives them from
1284
+ * module labels, and this is the one place that derivation lives. A module
1285
+ * at the repo root has no directory, so it groups under "(root)". */
1286
+ export function packageCounts(modules) {
1287
+ const counts = new Map();
1288
+ for (const m of modules) {
1289
+ const dir = m.label.includes("/") ? m.label.slice(0, m.label.lastIndexOf("/")) : "(root)";
1290
+ counts.set(dir, (counts.get(dir) || 0) + 1);
1291
+ }
1292
+ return counts;
1293
+ }
1294
+
1295
+ /** Every module in the graph, optionally scoped to a path prefix. */
1296
+ export function modulesOf(graph, prefix = "") {
1297
+ const norm = normPath(prefix);
1298
+ return graph.individuals.filter(
1299
+ (i) => (i.class || "") === "Module" && (!norm || normPath(i.label).startsWith(norm)),
1300
+ );
1301
+ }
1302
+
1280
1303
  /** Package/module tree + the most-imported (hub) modules — replaces reading the dir
1281
1304
  * tree and many files to learn the shape. Optional `pkg` prefix scopes it. */
1282
1305
  export function renderArchitecture(graph, { pkg = "" } = {}) {
1283
1306
  const norm = normPath(pkg);
1284
- const modules = graph.individuals.filter(
1285
- (i) => (i.class || "") === "Module" && (!norm || normPath(i.label).startsWith(norm)),
1286
- );
1307
+ const modules = modulesOf(graph, pkg);
1287
1308
  if (!modules.length) return norm ? `no modules under "${pkg}".` : "no modules in the graph.";
1288
- const pkgCount = new Map();
1289
- for (const m of modules) {
1290
- const dir = m.label.includes("/") ? m.label.slice(0, m.label.lastIndexOf("/")) : "(root)";
1291
- pkgCount.set(dir, (pkgCount.get(dir) || 0) + 1);
1292
- }
1309
+ const pkgCount = packageCounts(modules);
1293
1310
  const inDeg = new Map();
1294
1311
  for (const e of edgesOfKind(graph, "imports")) inDeg.set(e.object, (inDeg.get(e.object) || 0) + 1);
1295
1312
  const modSet = new Set(modules.map((m) => m.id));
@@ -110,10 +110,10 @@ const INTERROGATIVE_LEAD_RE = /^(?:which|what|who|whose|where|when|why|how)\b/i;
110
110
  * untouched so that working path keeps it. Two shapes: a plural kind noun in
111
111
  * tail position, or a bare (det +) singular kind noun and nothing else. */
112
112
  const LISTING_TAIL_KINDS = new Set([
113
- "modules", "files", "functions", "methods", "classes", "attributes", "fields",
113
+ "modules", "files", "packages", "functions", "methods", "classes", "attributes", "fields",
114
114
  "properties", "variables", "globals", "commits", "changes", "tests", "members",
115
115
  ]);
116
- const BARE_KIND_RE = /^(?:all\s+|the\s+)?(?:module|file|function|method|class|attribute|field|property|variable|global|commit|change|test|member)\??$/i;
116
+ const BARE_KIND_RE = /^(?:all\s+|the\s+)?(?:module|file|package|function|method|class|attribute|field|property|variable|global|commit|change|test|member)\??$/i;
117
117
  const isListingRemainder = (rest) => {
118
118
  if (BARE_KIND_RE.test(rest)) return true;
119
119
  const words = rest.replace(/\?+\s*$/, "").trim().split(/\s+/);
@@ -12,6 +12,10 @@ import { RELATION_TERM } from "./concept.mjs";
12
12
  export const REFERENCE_PACK_NAME = "simplewiki";
13
13
  export const REFERENCE_SHARD_COUNT = 64;
14
14
 
15
+ /** The grain cue every encyclopedia-grounded answer carries: this is a word's
16
+ * general meaning, not something read out of the graph the session is about. */
17
+ export const GENERAL_VOCABULARY_CUE = "General vocabulary, not from this codebase.";
18
+
15
19
  /** The shard a term's article row lives in: FNV-1a first byte mod 64, as the
16
20
  * file basename "ref-00" … "ref-3f". Part of the pack's on-disk contract —
17
21
  * the build script shards with THIS function, so the reader never scans. */
@@ -43,10 +47,14 @@ export function isReferenceArticleRow(row) {
43
47
  }
44
48
 
45
49
  /** The cited answer for a clean miss the pack could ground: the article's
46
- * summary with its title, licence and revision-pinned URL always visible. */
50
+ * summary with its title, licence and revision-pinned URL always visible,
51
+ * then the grain cue. Without the cue a reader has to recognize the source
52
+ * name to tell this apart from a fact read out of their own graph — both
53
+ * answer the same "what is X" question in the same voice. */
47
54
  export function renderReferenceAnswer(term, article) {
48
55
  return `${term} — ${article.summary} (source: reference article "${article.title}", `
49
- + `Simple English Wikipedia, CC BY-SA 4.0 — ${article.url}?oldid=${article.revid})`;
56
+ + `Simple English Wikipedia, CC BY-SA 4.0 — ${article.url}?oldid=${article.revid})`
57
+ + ` ${GENERAL_VOCABULARY_CUE}`;
50
58
  }
51
59
 
52
60
  /** The provenance tag a fact stored from a pack article carries —
@@ -88,7 +96,8 @@ export function liveProvenanceTag(article) {
88
96
  * the revision-pinned URL always visible. */
89
97
  export function renderLiveReferenceAnswer(term, article) {
90
98
  return `${term} — ${article.summary} (source: live Wikipedia article "${article.title}", `
91
- + `English Wikipedia, CC BY-SA 4.0 — ${article.url}?oldid=${article.revid})`;
99
+ + `English Wikipedia, CC BY-SA 4.0 — ${article.url}?oldid=${article.revid})`
100
+ + ` ${GENERAL_VOCABULARY_CUE}`;
92
101
  }
93
102
 
94
103
  /** The pure half of the LIVE clean-miss gate — same fold, shape check and
@@ -1,6 +1,6 @@
1
1
  // src/domain/router/call-validator.mjs — pure registry validators shared by the
2
2
  // product router (resolver / goal-reasoner) + the bench grader
3
- // (agentbench/grade.mjs re-exports these). Depends ONLY on registry.mjs — no
3
+ // (test-benchmarks/agentbench/grade.mjs re-exports these). Depends ONLY on registry.mjs — no
4
4
  // bench code — so the product←bench dependency stays inverted: the bench
5
5
  // imports the product, never the other way round. No I/O, no Date.now, no LLM.
6
6
 
@@ -243,8 +243,8 @@ export async function runTaughtPlan(request, tools, ctx) {
243
243
  /** The full drive: resolver/planner first; a refusal there falls through to
244
244
  * the taught world-goal lane (runTaughtPlan, above), and only a request that
245
245
  * is not a world goal escalates to the closed-world goal-reasoner. Mirrors
246
- * agentbench's driver-resolver.mjs + driver-goal.mjs composition, with no
247
- * agentbench/ dependency (agentbench/ is dev-only, never shipped). Returns a
246
+ * test-benchmarks/agentbench's driver-resolver.mjs + driver-goal.mjs composition, with no
247
+ * test-benchmarks/agentbench/ dependency (test-benchmarks/agentbench/ is dev-only, never shipped). Returns a
248
248
  * loopResult:
249
249
  * `{ calls, refused, terminated, proof, why, driver, composed?, observed?, candidateResults? }`.
250
250
  *
@@ -1,7 +1,7 @@
1
1
  // src/domain/router/set-algebra.mjs — the COMPOSITION OPERATORS: pure set-algebra a
2
2
  // multi-step plan needs to fold its threaded step result-sets into ONE composed
3
3
  // answer. Shared by the product router (goal-reasoner's relative-filter fold)
4
- // and the bench result-execution layer (agentbench/results.mjs re-exports
4
+ // and the bench result-execution layer (test-benchmarks/agentbench/results.mjs re-exports
5
5
  // these) — the bench imports the product, never the other way round. The
6
6
  // resolver DRIVER picks the operator from the router's OWN HTN method
7
7
  // (relative-filter -> intersect; conditional -> fallback/guard) and applies it;