@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 +35 -10
- package/bin/tmct.mjs +1 -1
- package/package.json +18 -15
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/ask-vocab.mjs +4 -0
- package/src/domain/ask.mjs +101 -12
- package/src/domain/codegraph.mjs +25 -8
- package/src/domain/interpret/normalize.mjs +2 -2
- package/src/domain/reference-pack.mjs +12 -3
- package/src/domain/router/call-validator.mjs +1 -1
- package/src/domain/router/drive.mjs +2 -2
- package/src/domain/router/set-algebra.mjs +1 -1
- package/src/services/adventure.mjs +34 -19
- package/src/services/chat-page-viz.mjs +1 -1
- package/src/services/chat.mjs +172 -39
- package/src/services/index.mjs +6 -3
- package/src/services/ledger-viz.mjs +1 -1
- package/src/surfaces/web/chat-browser-entry.mjs +1 -1
- package/src/surfaces/web/digest-client.mjs +8 -3
- package/src/surfaces/web/graph-ask-browser-entry.mjs +41 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +86 -86
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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.
|
|
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:
|
|
91
|
-
"test:
|
|
92
|
-
"
|
|
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": {
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -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
|
package/src/domain/ask.mjs
CHANGED
|
@@ -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":
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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;
|