@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7
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 +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ derive by rule from both. Every answer is either grounded or an honest miss.
|
|
|
27
27
|
| `data/` | seed data assets: sprites, phrasebook, games, response templates |
|
|
28
28
|
| `ontology/` | the software ontology (`tmct-core.ttl`) and memory shapes, in Turtle |
|
|
29
29
|
| `scripts/` | build, check, and maintenance scripts (`npm run` targets live here) |
|
|
30
|
+
| `infra/` | the AWS CDK app that provisions the deployed site and its OIDC/deploy roles |
|
|
30
31
|
| `examples/` | runnable example scripts and fixture repos used by the README's own examples and the test suite |
|
|
31
32
|
| `demo/` | standalone demo scripts (e.g. the agentic-loop demo) |
|
|
32
33
|
| `electron/` | the Electron desktop app wrapper |
|
|
@@ -37,7 +38,7 @@ derive by rule from both. Every answer is either grounded or an honest miss.
|
|
|
37
38
|
| `reports/` | benchmark write-ups (`BENCHMARK_*.md`) and `PAGE_WEIGHTS.md` — see the root `STATUS.md` for the one-page summary these feed |
|
|
38
39
|
| `playtests/` | numbered playtest session logs, one edge found and fixed per entry |
|
|
39
40
|
| `archive/` | delivered `PLAN_*.md`/`BENCHMARK_*.md` docs, kept for history |
|
|
40
|
-
| `public/` | the
|
|
41
|
+
| `public/` | the demo site: the hand-written home page, the eleven about pages, the shared stylesheet and the model/screenshot assets. The demo pages and browser bundles beside them are gitignored build outputs of `npm run demo:build` |
|
|
41
42
|
|
|
42
43
|
`node_modules/` (dependencies) and dotfiles/hidden tooling directories are omitted above.
|
|
43
44
|
|
|
@@ -84,11 +85,13 @@ tmct> ahab is the father of john
|
|
|
84
85
|
noted — remembered: ahab fathers john
|
|
85
86
|
|
|
86
87
|
Goal (inferred): Teach/remember a new fact.
|
|
88
|
+
(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, or on a stored backend, to keep it.)
|
|
87
89
|
|
|
88
90
|
tmct> john is the father of ishmael
|
|
89
91
|
noted — remembered: john fathers ishmael
|
|
90
92
|
|
|
91
93
|
Goal (inferred): Teach/remember a new fact.
|
|
94
|
+
(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, or on a stored backend, to keep it.)
|
|
92
95
|
|
|
93
96
|
tmct> a father is a kind of parent
|
|
94
97
|
noted — remembered 1 fact: father rdfs:subClassOf parent (father is a type of parent)
|
|
@@ -96,21 +99,25 @@ noted — remembered 1 fact: father rdfs:subClassOf parent (father is a type of
|
|
|
96
99
|
Goal (inferred): Teach/remember a new fact.
|
|
97
100
|
|
|
98
101
|
Canonical: does "father" inherits "parent"? — ask(inherits, subject="father", "parent")
|
|
102
|
+
(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, or on a stored backend, to keep it.)
|
|
99
103
|
|
|
100
104
|
tmct> remember that ahab is male
|
|
101
105
|
noted — remembered: ahab is male
|
|
102
106
|
|
|
103
107
|
Goal (inferred): Teach/remember a new fact.
|
|
108
|
+
(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, or on a stored backend, to keep it.)
|
|
104
109
|
|
|
105
110
|
tmct> a grandparent is a parent of a parent
|
|
106
111
|
noted — remembered: a grandparent is a parent of a parent
|
|
107
112
|
|
|
108
113
|
Goal (inferred): Teach/remember a new fact.
|
|
114
|
+
(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, or on a stored backend, to keep it.)
|
|
109
115
|
|
|
110
116
|
tmct> a grandfather is a grandparent who is male
|
|
111
117
|
noted — remembered: a grandfather is a grandparent who is male
|
|
112
118
|
|
|
113
119
|
Goal (inferred): Teach/remember a new fact.
|
|
120
|
+
(this session keeps nothing — the fact is gone when it ends. Run without --ephemeral, or on a stored backend, to keep it.)
|
|
114
121
|
|
|
115
122
|
tmct> is ahab the grandfather of ishmael
|
|
116
123
|
yes — you told me: ahab fathers john (source: teach:chat:<session-id>@<timestamp>); father is a kind of parent (source: ace:chat:<session-id>@<timestamp>); you told me: john fathers ishmael (source: teach:chat:<session-id>@<timestamp>); you told me: ahab is male (source: teach:chat:<session-id>@<timestamp>)
|
|
@@ -319,16 +326,19 @@ tmct> /exit
|
|
|
319
326
|
|
|
320
327
|
**[Try it live in your browser →](https://tmct.polycode.co.uk/)**
|
|
321
328
|
runs the actual query engine client-side. No server, no install. The landing
|
|
322
|
-
page answers codebase questions live, and
|
|
329
|
+
page answers codebase questions live, and eleven more pages each ground their
|
|
323
330
|
own domain: a full chat seeded with 63,470 facts (the same nine bands as
|
|
324
331
|
`npm run init:xl`), the
|
|
325
332
|
**memory ledger** (every fact as a readable sentence; drill by clicking the
|
|
326
333
|
terms inside), and the **code explorer** (the same ledger UI refocused on a
|
|
327
334
|
code graph, with a hint rail of suggested next questions). An **ingest
|
|
328
335
|
page** turns pasted or dropped text into grounded facts and downloads them
|
|
329
|
-
as canonical JSONL
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
as canonical JSONL, and a **research page** walks a stub wiki graph link by
|
|
337
|
+
link. The rest are a Towers-of-Hanoi plan replayed move by move, the
|
|
338
|
+
spider-and-fly and text-adventure games, a sprite gallery whose chat dock
|
|
339
|
+
answers from 1,480 generated sprite facts, and two multi-agent worlds: a
|
|
340
|
+
burrow that shares one world across browsers over WebRTC, and a Three.js
|
|
341
|
+
town square where a wolf hunts goblins. The chat page and
|
|
332
342
|
the ledger take the same paste-or-drop text in place; every page that holds
|
|
333
343
|
a fact store exports it as JSONL.
|
|
334
344
|
The site hosts its own copy of wink-nlp, ships its assets precompressed,
|
|
@@ -373,8 +383,8 @@ functions". Every suggestion resolves to a real answer.
|
|
|
373
383
|
The identical page also runs as a plain hosted page over the demo code
|
|
374
384
|
graph, with nothing to install:
|
|
375
385
|
**[try it live →](https://tmct.polycode.co.uk/code.html)**.
|
|
376
|
-
The desktop build below is for exploring your own repo or graph
|
|
377
|
-
hosted page
|
|
386
|
+
The desktop build below is for exploring your own repo or graph: a browser page
|
|
387
|
+
gets no filesystem access, so the hosted page reads the demo graph it ships with.
|
|
378
388
|
|
|
379
389
|
Electron is a dev-only dependency and never ships in the npm package. Because
|
|
380
390
|
`.npmrc` sets `ignore-scripts=true`, installing it does not fetch the runtime
|
|
@@ -659,7 +669,7 @@ now consults two shipped, lazily-loaded packs before giving up:
|
|
|
659
669
|
triples into memory (provenance `child:conceptnet:kettle`, ranked below
|
|
660
670
|
anything you teach) and answers from them; the next ask answers from
|
|
661
671
|
memory directly.
|
|
662
|
-
- `corpus/reference/`: 3,
|
|
672
|
+
- `corpus/reference/`: 3,888 Simple English Wikipedia summaries. When the
|
|
663
673
|
triples cannot answer, a matching article answers as a cited read-out
|
|
664
674
|
(`source: reference article "Otter"…, CC BY-SA 4.0`).
|
|
665
675
|
|
|
@@ -712,7 +722,7 @@ the shipped `npm run example:*` demos) reads a graph but writes nothing back.
|
|
|
712
722
|
|
|
713
723
|
The default persona also comes in three sizes: Small (~664 facts, the
|
|
714
724
|
default), Medium (~1,608, `tmct init --persona-size medium`) and Large
|
|
715
|
-
(~13,
|
|
725
|
+
(~13,600, `--persona-size large`, deep enough to chain real multi-hop
|
|
716
726
|
reasoning).
|
|
717
727
|
|
|
718
728
|
### Memory backends
|
|
@@ -935,6 +945,17 @@ provenance record. Most of its flags choose what gets seeded and where config is
|
|
|
935
945
|
in this repo picks it up with no flag needed
|
|
936
946
|
```
|
|
937
947
|
|
|
948
|
+
`tmct index` is the producer side of the graph seam. It walks a repo's own source
|
|
949
|
+
and writes the `.tmct/graph.json` that chat, serve and the CLI then read.
|
|
950
|
+
|
|
951
|
+
```output:help:index
|
|
952
|
+
tmct index [--repo <abs>] produce a code graph from a repo's OWN source (default: cwd):
|
|
953
|
+
[--no-history] walk the tree, parse JS/TS with the TypeScript compiler
|
|
954
|
+
API, read git history, and write <repo>/.tmct/graph.json —
|
|
955
|
+
the artifact chat/serve/cli then read. --no-history skips
|
|
956
|
+
the git passes (no commit/touches/cochange edges)
|
|
957
|
+
```
|
|
958
|
+
|
|
938
959
|
`tmct import` does the same activation as `tmct init`, but against a repo that is
|
|
939
960
|
already set up. Its `--graph` flag works differently from the others: it appends to
|
|
940
961
|
`tmct.toml`'s `graph_files` array instead of activating a bundle.
|
|
@@ -1055,6 +1076,15 @@ kill $SERVE_PID
|
|
|
1055
1076
|
[--graph <path>] the same thing. --graph names the graph file outright
|
|
1056
1077
|
[--config <path>] (repeatable), --config an alternate tmct.toml
|
|
1057
1078
|
tmct cli digest '{…}' architecture map + per-module context bundles
|
|
1079
|
+
tmct --help show this help
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
The help closes with two notes on where a chat session runs and what it leaves
|
|
1083
|
+
behind:
|
|
1084
|
+
|
|
1085
|
+
```output:help:notes
|
|
1086
|
+
On a terminal, chat opens the full-screen TUI; piped input gets the plain shell.
|
|
1087
|
+
In chat: /help lists slash-commands; /exit leaves. Session log → <repo>/.tmct/session-<id>.md.
|
|
1058
1088
|
```
|
|
1059
1089
|
|
|
1060
1090
|
Two precedence chains apply across every command above, in this order:
|
|
@@ -1116,6 +1146,12 @@ graph_file = ".tmct/graph.json"
|
|
|
1116
1146
|
# Extra graphs, merged alongside graph_file (ids that collide are auto-prefixed).
|
|
1117
1147
|
graph_files = [".tmct/graph.json", ".tmct/legacy-graph.json"]
|
|
1118
1148
|
|
|
1149
|
+
[graph]
|
|
1150
|
+
# A chat session reads this repo's graph and writes nothing back into its
|
|
1151
|
+
# .tmct/: no session upsert, no transcript, no memory. The committed example
|
|
1152
|
+
# fixtures set it so `tmct chat --repo examples/<x>` can't rewrite them.
|
|
1153
|
+
read_only = false
|
|
1154
|
+
|
|
1119
1155
|
[corpus]
|
|
1120
1156
|
# "tier1" (committed slice only, $0/offline, the default), "tier2" (also fetch
|
|
1121
1157
|
# growable corpora at seed time), or "tier3" (also consult live sources per query).
|
|
@@ -1124,6 +1160,8 @@ tier = "tier1"
|
|
|
1124
1160
|
[seed]
|
|
1125
1161
|
enabled = true # seed the committed corpus into .tmct/memory during init
|
|
1126
1162
|
limit = 500 # cap the seeded fact count (definitional band first); unset = no cap
|
|
1163
|
+
capture_unknown_context = true # keep the sentence around an unrecognized term
|
|
1164
|
+
unknown_context_limit = 200 # cap how many of those contexts are kept
|
|
1127
1165
|
|
|
1128
1166
|
# One [extensions.<name>] table per bundle. A recognized name (human, seon,
|
|
1129
1167
|
# conceptnet, human-medium, human-large, tier2-aws, tier2-python, tier2-java,
|
|
@@ -1177,7 +1215,6 @@ call_adjacency = true # boost callers/callees of a matched symbol
|
|
|
1177
1215
|
impl_of_interface = true # boost an interface's implementations
|
|
1178
1216
|
beam_search = true # use beam search over the graph walk
|
|
1179
1217
|
beam_width = 8
|
|
1180
|
-
embed_rank = false # rerank by embedding similarity (off by default)
|
|
1181
1218
|
prose_layers = 2 # how many prose-generation passes to run
|
|
1182
1219
|
|
|
1183
1220
|
[tune.expansion]
|
|
@@ -1186,6 +1223,17 @@ nodes = 50 # node budget for the walk
|
|
|
1186
1223
|
q = 0.5 # expansion breadth parameter
|
|
1187
1224
|
depth = 3 # max hops
|
|
1188
1225
|
|
|
1226
|
+
# Research-lane knobs (src/services/research.mjs).
|
|
1227
|
+
[research]
|
|
1228
|
+
fanout_limit = 8 # links followed per page
|
|
1229
|
+
max_depth = 3 # hops from the starting topic
|
|
1230
|
+
max_topics = 40 # topics visited in one crawl
|
|
1231
|
+
min_interval_ms = 1000 # politeness delay between fetches
|
|
1232
|
+
|
|
1233
|
+
# Discourse record (src/domain/discourse.mjs).
|
|
1234
|
+
[discourse]
|
|
1235
|
+
max_referents = 12 # how many referents stay resolvable across turns
|
|
1236
|
+
|
|
1189
1237
|
[telemetry]
|
|
1190
1238
|
enabled = false # local-only counters; never phones home
|
|
1191
1239
|
|
|
@@ -1202,10 +1250,13 @@ spider_initial_mass = 15
|
|
|
1202
1250
|
spider_mass_decrement_per_turn = 0.5 # lower = slower to starve
|
|
1203
1251
|
fly_initial_mass = 10
|
|
1204
1252
|
fly_mass_decrement_per_turn = 1
|
|
1205
|
-
|
|
1253
|
+
spider_vision_radius = 4 # Chebyshev radius a spider sees other agents within
|
|
1254
|
+
fly_vision_radius = 4 # the same radius for a fly
|
|
1206
1255
|
egg_hatch_delay_turns = 3 # turns between a lay and its hatch
|
|
1207
1256
|
fly_spawn_interval_turns = 3 # a new fly arrives every Nth turn
|
|
1208
|
-
|
|
1257
|
+
egg_lay_mass_threshold = 25 # mass a spider must reach before it lays
|
|
1258
|
+
egg_hatch_count = 2 # hatchlings per egg
|
|
1259
|
+
min_hatchling_mass = 3 # mass a hatchling starts with
|
|
1209
1260
|
web_duration_turns = 10 # turns a spider-built web stays active
|
|
1210
1261
|
|
|
1211
1262
|
[games.guess-number]
|
|
@@ -1219,8 +1270,9 @@ max_depth = 300 # the "solve it" plan lane's search-depth cap (hanoi, river-cro
|
|
|
1219
1270
|
|
|
1220
1271
|
### Try it on an example graph
|
|
1221
1272
|
|
|
1222
|
-
tmct
|
|
1223
|
-
|
|
1273
|
+
tmct reads a code graph at `<repo>/.tmct/graph.json`. `tmct index` builds one
|
|
1274
|
+
from a repo's own source, and any other producer can write the same file. Two
|
|
1275
|
+
ready-made example graphs live in `examples/` in this repo (clone the
|
|
1224
1276
|
repo to use them; they are not in the published npm package), so you can see
|
|
1225
1277
|
it answer real questions with no setup:
|
|
1226
1278
|
|
|
@@ -1402,7 +1454,7 @@ and transcripts are in the linked write-ups.
|
|
|
1402
1454
|
| Determinism | Byte-identical on rerun: a 379-case `--replay` clean across 2 runs, no LLM, no network, $0 per turn | A property of the no-model pipeline. | `archive/BENCHMARK_INFERENCE_2.7.12.md` |
|
|
1403
1455
|
| Dialogue robustness (persona sweep) | A 6-persona sweep (textbook logician, casual newcomer, new developer, adversarial sceptic, returning user, planning user) fixed 25 of the prior cycle's 29 routed findings (21 clean, 4 with a residual noted); 4 remain broken, 2 in a shape distinct from the original complaint | Free exploration across all six personas surfaced roughly 60 fresh findings beyond the ratchet check. The single highest-signal pattern: tmct's own suggested repair text was itself frequently broken when followed verbatim (since fixed, see `NEXT.md`). | `archive/BENCHMARK_CONVERSATION_2.7.11.md` |
|
|
1404
1456
|
|
|
1405
|
-
|
|
1457
|
+
Seven offline benchmark rigs live in a clone (they are not in the npm
|
|
1406
1458
|
package). Each replays a committed case set through the real product and
|
|
1407
1459
|
writes graded rows you can diff between runs:
|
|
1408
1460
|
|
|
@@ -1412,7 +1464,14 @@ writes graded rows you can diff between runs:
|
|
|
1412
1464
|
- `npm run infbench` generates inference cases, then runs each through both
|
|
1413
1465
|
drive points, the reasoning kernel and the chat surface;
|
|
1414
1466
|
- `npm run agentbench:run` measures the tool-loop behaviour, and every
|
|
1415
|
-
verdict carries a hallucination axis
|
|
1467
|
+
verdict carries a hallucination axis;
|
|
1468
|
+
- `npm run idxbench:run` grades how faithfully the code-index producer
|
|
1469
|
+
restates source as a graph;
|
|
1470
|
+
- `npm run ingestbench:run` (with `ingestbench:judge`) grades text-to-facts
|
|
1471
|
+
extraction;
|
|
1472
|
+
- `npm run researchbench:run` grades the research lane's link traversal
|
|
1473
|
+
against a frozen stub wiki graph;
|
|
1474
|
+
- `npm run synthbench:code` grades deterministic code synthesis.
|
|
1416
1475
|
|
|
1417
1476
|
The smallest real slice of each, the same invocations the test suite's
|
|
1418
1477
|
bench-smoke lane replays:
|
|
@@ -1457,7 +1516,7 @@ tmct's vocabulary is grounded in published standards where they exist, and says
|
|
|
1457
1516
|
Each alignment below is a triple in `ontology/tmct-core.ttl` and a test in
|
|
1458
1517
|
`test/adapters/grammar-ontology.test.mjs`. `docs/references/` holds an entry per source: the
|
|
1459
1518
|
edition, the retrieval date, the terms tmct uses, and what could not be verified.
|
|
1460
|
-
`PLAN_NORMATIVE.md` holds the reconciliation, one verdict per term.
|
|
1519
|
+
`archive/PLAN_NORMATIVE.md` holds the reconciliation, one verdict per term.
|
|
1461
1520
|
|
|
1462
1521
|
### The data model
|
|
1463
1522
|
|
|
@@ -1476,7 +1535,7 @@ edition, the retrieval date, the terms tmct uses, and what could not be verified
|
|
|
1476
1535
|
| [Attempto Controlled English](http://attempto.ifi.uzh.ch/site/docs/) | ACE 6.7, 2013 | The controlled-English fragment. tmct implements 9 of ACE's declarative sentence patterns. |
|
|
1477
1536
|
| Kuhn, "A Survey and Classification of Controlled Natural Languages" | *Computational Linguistics* 40(1), 2014 | Where ACE sits among controlled languages. |
|
|
1478
1537
|
| [ConceptNet](https://github.com/commonsense/conceptnet5/wiki/Relations) | slice pins 5.7.0 | The commonsense corpus. 25 relations are mirrored into `mgx:` and each cites its `/r/` origin. |
|
|
1479
|
-
| Damerau, *CACM* 7(3), 1964 · Levenshtein, *Soviet Physics Doklady* 10(8), 1966 | — | Fuzzy matching. `fuzzy.mjs` implements **Optimal String Alignment** — restricted Damerau-Levenshtein, which allows adjacent transposition but edits no substring twice. |
|
|
1538
|
+
| Damerau, *CACM* 7(3), 1964 · Levenshtein, *Soviet Physics Doklady* 10(8), 1966 | — | Fuzzy matching. `src/domain/interpret/fuzzy.mjs` implements **Optimal String Alignment** — restricted Damerau-Levenshtein, which allows adjacent transposition but edits no substring twice. |
|
|
1480
1539
|
|
|
1481
1540
|
### Reasoning and planning
|
|
1482
1541
|
|
|
@@ -1492,7 +1551,7 @@ edition, the retrieval date, the terms tmct uses, and what could not be verified
|
|
|
1492
1551
|
|
|
1493
1552
|
| source | edition | what tmct uses it for |
|
|
1494
1553
|
|---|---|---|
|
|
1495
|
-
| Jensen et al., "A Consensus Glossary of Temporal Database Concepts" | *SIGMOD Record* 23(1), 1994 | The time vocabulary. `mgx:utteranceTs` is valid time; `mgx:createdAt` is a transaction-time start. tmct is **not** bitemporal: `mgx:updatedAt` is an audit stamp, so
|
|
1554
|
+
| Jensen et al., "A Consensus Glossary of Temporal Database Concepts" | *SIGMOD Record* 23(1), 1994 | The time vocabulary. `mgx:utteranceTs` is valid time; `mgx:createdAt` is a transaction-time start. tmct is **not** bitemporal: `mgx:updatedAt` is an audit stamp, so "what did it believe last Tuesday" lands on the honest miss wall. |
|
|
1496
1555
|
| RFC 9923, "The FNV Non-Cryptographic Hash Algorithm" | Informational, 2026 | FNV hashes the narrow non-fact-id pools (paraphrase keys, per-URL source ids, corpus dedupe). Fact ids are content-addressed with a **64-bit truncation of SHA-256**, so a fact id is collision-resistant at tmct's corpus sizes; tmct is still **not** a Merkle tree and offers no tamper-evidence. |
|
|
1497
1556
|
| Green, Karvounarakis, Tannen, "Provenance Semirings" | PODS 2007 | The distinction tmct's docs keep: it records source annotation and PROV-style attribution, not how-provenance. |
|
|
1498
1557
|
|
package/bin/tmct.mjs
CHANGED
|
@@ -183,6 +183,34 @@ async function runDigest(args, { dispatchTool, buildContextBundle, source, confi
|
|
|
183
183
|
process.stdout.write([header, ...body].join("\n") + "\n");
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/** The conversational memory store's vocabulary reader, bound to whatever store
|
|
187
|
+
* the target repo already has. `cli tmct_ask` and chat answer over the same
|
|
188
|
+
* `--repo`, so a term chat knows used to come back from the cold route as a
|
|
189
|
+
* miss purely because the cold route reads the code graph and nothing else.
|
|
190
|
+
*
|
|
191
|
+
* Read-only by construction: the reader is handed a throwaway in-memory copy
|
|
192
|
+
* of the store, so a repo with no memory yet stays that way (opening a backend
|
|
193
|
+
* would create one) and nothing a reader writes reaches disk. `{ factLookup:
|
|
194
|
+
* null }` when the repo has no store, which leaves the graph-only answer. */
|
|
195
|
+
async function openColdMemoryReader(config) {
|
|
196
|
+
const empty = { factLookup: null, close: async () => {} };
|
|
197
|
+
if (!config?.graphFile) return empty;
|
|
198
|
+
const { dirname: dirnameOf } = await import("node:path");
|
|
199
|
+
const repoRoot = dirnameOf(dirnameOf(config.graphFile));
|
|
200
|
+
const { openExistingMemoryBackend, readOnlyMemorySnapshot } = await import("../src/adapters/memory/core.mjs");
|
|
201
|
+
const store = await openExistingMemoryBackend(repoRoot);
|
|
202
|
+
if (!store) return empty;
|
|
203
|
+
let snapshot = null;
|
|
204
|
+
try { snapshot = await readOnlyMemorySnapshot(store.dir); }
|
|
205
|
+
finally { await store.close(); }
|
|
206
|
+
if (!snapshot) return empty;
|
|
207
|
+
const { factAnswer } = await import("../src/services/chat.mjs");
|
|
208
|
+
return {
|
|
209
|
+
factLookup: (query, envelope) => factAnswer(snapshot, query, envelope, true),
|
|
210
|
+
close: async () => {},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
186
214
|
/** The carried `cli` dispatcher (digest / tmct_locate / any-tool fallback).
|
|
187
215
|
* Imports are lazy so `tmct --help` and chat startup never pay for the tool
|
|
188
216
|
* stack. */
|
|
@@ -266,15 +294,18 @@ async function runCliMode() {
|
|
|
266
294
|
process.exit(2);
|
|
267
295
|
}
|
|
268
296
|
const config = await configFor(args.repo_path);
|
|
297
|
+
const memory = await openColdMemoryReader(config);
|
|
269
298
|
try {
|
|
270
299
|
// The recognizer seam a tool like tmct_ingest needs: injected here (bin
|
|
271
300
|
// sits above the service layer) rather than imported by the tool layer.
|
|
272
301
|
const { ingestText } = await import("../src/services/extract-facts.mjs");
|
|
273
|
-
const text = await dispatchTool(sub, args, { config, ingest: ingestText });
|
|
302
|
+
const text = await dispatchTool(sub, args, { config, ingest: ingestText, factLookup: memory.factLookup });
|
|
274
303
|
process.stdout.write(text + "\n");
|
|
275
304
|
} catch (e) {
|
|
276
305
|
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
277
306
|
process.exit(1);
|
|
307
|
+
} finally {
|
|
308
|
+
await memory.close();
|
|
278
309
|
}
|
|
279
310
|
return;
|
|
280
311
|
}
|
|
@@ -540,7 +571,12 @@ async function buildEngineBundleJs(builderFile) {
|
|
|
540
571
|
}
|
|
541
572
|
const dir = await mkdtemp(join(tmpdir(), "tmct-render-"));
|
|
542
573
|
try {
|
|
543
|
-
|
|
574
|
+
// Quiet: this build is an implementation detail of writing one page. Its
|
|
575
|
+
// esbuild log is eleven "import.meta is not available with the iife output
|
|
576
|
+
// format" warnings about Node-only paths the browser entry never reaches,
|
|
577
|
+
// and they arrived ahead of the single line the user asked for. A build
|
|
578
|
+
// script still prints them — its reader is a developer.
|
|
579
|
+
const { outPath } = await build(dir, { quiet: true });
|
|
544
580
|
return await readFile(outPath, "utf8");
|
|
545
581
|
} finally {
|
|
546
582
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -603,8 +639,33 @@ async function writeStandaloneViewPage(archetype, rest) {
|
|
|
603
639
|
process.stdout.write(`wrote ${outPath} (${(Buffer.byteLength(html, "utf8") / 1024).toFixed(0)} KB, self-contained)\n`);
|
|
604
640
|
}
|
|
605
641
|
|
|
642
|
+
/** Refuse a `--repo` that names nothing on disk, before any verb gets to create
|
|
643
|
+
* it. Eight verbs open a memory store under the path they are handed, and the
|
|
644
|
+
* store's own mkdir is recursive, so a mistyped path used to be scaffolded in
|
|
645
|
+
* silence — a fresh tmct.toml, a .tmct/ tree, 688 seeded facts — and the
|
|
646
|
+
* answer came back as if the repo were the one meant. `init` is the one verb
|
|
647
|
+
* whose whole job is to create a repo from nothing, so it never reaches here. */
|
|
648
|
+
async function refuseMissingRepoPath(argv) {
|
|
649
|
+
const i = argv.indexOf("--repo");
|
|
650
|
+
const given = i !== -1 ? argv[i + 1] : null;
|
|
651
|
+
if (!given || given.startsWith("--")) return;
|
|
652
|
+
const { resolve } = await import("node:path");
|
|
653
|
+
const { stat } = await import("node:fs/promises");
|
|
654
|
+
const abs = resolve(process.cwd(), given);
|
|
655
|
+
let entry = null;
|
|
656
|
+
try { entry = await stat(abs); } catch { entry = null; }
|
|
657
|
+
if (entry?.isDirectory()) return;
|
|
658
|
+
const where = abs === given ? given : `${given} (${abs})`;
|
|
659
|
+
process.stderr.write(entry
|
|
660
|
+
? `tmct: --repo ${where} is not a directory.\n`
|
|
661
|
+
: `tmct: --repo ${where} does not exist. Nothing was created. `
|
|
662
|
+
+ `Check the path, or run \`tmct init --repo ${given}\` to make a repo there.\n`);
|
|
663
|
+
process.exit(2);
|
|
664
|
+
}
|
|
665
|
+
|
|
606
666
|
async function main() {
|
|
607
667
|
const mode = process.argv[2];
|
|
668
|
+
if (mode !== "init") await refuseMissingRepoPath(process.argv.slice(3));
|
|
608
669
|
|
|
609
670
|
if (mode === "chat") {
|
|
610
671
|
const rest = process.argv.slice(3);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.7",
|
|
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.",
|
|
@@ -698,6 +698,29 @@ export async function openMemoryBackend(repoRoot, backendChoice) {
|
|
|
698
698
|
return { dir: handle, close: async () => closeSqliteMemoryStore(handle) };
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
/** openMemoryBackend for a caller that only wants to READ what a repo already
|
|
702
|
+
* holds: null when the repo has no store yet, because opening one creates it.
|
|
703
|
+
* The cold surfaces (the `cli` tool route) answer over a repo they were merely
|
|
704
|
+
* pointed at, and must leave a repo with no memory exactly as they found it. */
|
|
705
|
+
export async function openExistingMemoryBackend(repoRoot, backendChoice = "") {
|
|
706
|
+
if (!repoRoot || typeof repoRoot !== "string") return null;
|
|
707
|
+
const dbPath = join(repoRoot, ".tmct", "memory", "graph.sqlite");
|
|
708
|
+
try { await access(dbPath); } catch { return null; }
|
|
709
|
+
return openMemoryBackend(repoRoot, backendChoice);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** A throwaway in-memory COPY of a store's facts: readers answer from the real
|
|
713
|
+
* data, and anything a reader writes lands in the copy rather than on disk.
|
|
714
|
+
* What a surface with no session behind it (the `cli` tool route, the HTTP
|
|
715
|
+
* messages endpoint) hands a reader that expects a memory handle, so a cold
|
|
716
|
+
* answer matches a chat answer without the cold call gaining a write. */
|
|
717
|
+
export async function readOnlyMemorySnapshot(memoryDir) {
|
|
718
|
+
if (!memoryDir) return null;
|
|
719
|
+
const snapshot = createInMemoryStore();
|
|
720
|
+
applySeedPayload(snapshot, cloneMemoryPayload(await loadMemory(memoryDir)));
|
|
721
|
+
return snapshot;
|
|
722
|
+
}
|
|
723
|
+
|
|
701
724
|
/** openMemoryBackend for an entry point that holds only a repo path and has no
|
|
702
725
|
* CLI-flag tier (the fold's idle pass, `tmct import --file`): resolve the
|
|
703
726
|
* backend token the way the chat path does minus the flag —
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -396,6 +396,25 @@ export const VERB_TO_KIND = Object.freeze(
|
|
|
396
396
|
* builds never collide on the same top-level identifier. */
|
|
397
397
|
export const HAS_FAMILY_VERBS = Object.freeze(new Set(["has", "have", "holds", "hold"]));
|
|
398
398
|
|
|
399
|
+
/** Every action-flavored word in RELATIONS.calls.verbs ("run(s)", "execute(s)",
|
|
400
|
+
* "invoke(s)", "trigger(s)", "fire(s)", "hit(s)", "kick(s) off") also opens an
|
|
401
|
+
* imperative request for a NAMED REPORT ("run the impact of X", "trigger the
|
|
402
|
+
* untested scan") — the router's own capability labels (registry.mjs), not a
|
|
403
|
+
* code-graph relation. "run"/"execute" earn the calls kind because "which
|
|
404
|
+
* functions run X" is a genuine reverse-calls question, so the collision can't
|
|
405
|
+
* be fixed by dropping them from RELATIONS.calls; instead grammar.mjs and
|
|
406
|
+
* keywords.mjs check this set before reading the calls kind's object, so
|
|
407
|
+
* "impact"/"untested"/… stay the report the sentence names rather than
|
|
408
|
+
* becoming the leading word of a term nothing will ever resolve. Curated
|
|
409
|
+
* against registry.mjs's own capability labels — a new capability's label
|
|
410
|
+
* needs adding here too, the same discipline CASCADE_FUZZY_REAL_WORDS
|
|
411
|
+
* (ask.mjs) already uses for "impact" on the fuzzy-correction side. */
|
|
412
|
+
export const CALLS_VERB_REPORT_NOUNS = Object.freeze(new Set([
|
|
413
|
+
"search", "describe", "signature", "impact", "members", "subclasses",
|
|
414
|
+
"exports", "callers", "callees", "calls", "tests", "untested", "history",
|
|
415
|
+
"cochanges", "architecture", "arch", "related", "sprite",
|
|
416
|
+
]));
|
|
417
|
+
|
|
399
418
|
/** "what is a kind of X" / "what is a subclass of X" collision fix: some
|
|
400
419
|
* inherits verbs are themselves phrased "is a <continuation>", which would
|
|
401
420
|
* otherwise collide with grammar.mjs's literal meta-whatis reading and
|
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, packageCounts, modulesOf, HISTORY_CAP } from "./codegraph.mjs";
|
|
18
|
+
import { relationKind, impactClosure, moduleCountOf, normPath, packageCounts, modulesOf, HISTORY_CAP, NO_CODE_INDEX_NOTE } from "./codegraph.mjs";
|
|
19
19
|
import { isTestPath } from "./module-paths.mjs";
|
|
20
20
|
import {
|
|
21
21
|
RELATIONS,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
AGGREGATE_TRIGGERS, LIST_TRIGGERS, SUPERLATIVE_EXTREMES, EDGE_NOUN_TO_METRIC, METRIC_IMPLIES_ENTITY, ANAPHORA_TRIGGERS,
|
|
28
28
|
MEMBERSHIP_KINDS, CASCADE_NOISE, CASCADE_SYNONYMS, HELP_TRIGGERS,
|
|
29
29
|
WORLD_RELATIONS, WORLD_NOUN_TO_RELATION, WORLD_PREDICATES, locativePreposition,
|
|
30
|
-
stripTrailingScopeFiller, stripTrailingTemporalAdverb,
|
|
30
|
+
stripTrailingScopeFiller, stripTrailingTemporalAdverb, CALLS_VERB_REPORT_NOUNS,
|
|
31
31
|
} from "./ask-vocab.mjs";
|
|
32
32
|
import { expandContractions, normalizeQuery, applyNegationFrames, applyPhrasingFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf, escapeRegex } from "./interpret/normalize.mjs";
|
|
33
33
|
import { editDistance, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
@@ -2491,9 +2491,7 @@ export function rephraseHint() {
|
|
|
2491
2491
|
* graph is UNKNOWN, not empty (see chat.mjs's noCodeGraph), so it keeps the
|
|
2492
2492
|
* index-shaped advice. */
|
|
2493
2493
|
function touchesRephraseHint(graph = null) {
|
|
2494
|
-
if (graph && moduleCountOf(graph) === 0)
|
|
2495
|
-
return "This store holds no code index, so it records no modules or commits to look through.";
|
|
2496
|
-
}
|
|
2494
|
+
if (graph && moduleCountOf(graph) === 0) return NO_CODE_INDEX_NOTE;
|
|
2497
2495
|
return 'Try "who touched <a module that actually has commits>" or "/describe <module>" to see what\'s in the index.';
|
|
2498
2496
|
}
|
|
2499
2497
|
|
|
@@ -4127,6 +4125,13 @@ const CONTENT_VOCAB = new Set([
|
|
|
4127
4125
|
...wordsOf(PLACEHOLDER_NOUNS), ...wordsOf(ANAPHORA_TRIGGERS), ...wordsOf(META_MEANING_VERBS),
|
|
4128
4126
|
...wordsOf(WHERE_MARKERS), ...wordsOf(MENTION_MARKERS), ...wordsOf(RELATIVE_PRONOUNS),
|
|
4129
4127
|
...wordsOf(Object.keys(CASCADE_SYNONYMS)),
|
|
4128
|
+
// "impact"/"untested"/… (CALLS_VERB_REPORT_NOUNS) name the report an imperative
|
|
4129
|
+
// "run"/"execute"/"trigger" sentence is asking for. Without this, the drop-
|
|
4130
|
+
// unmatched pass below reads a report noun as packaging around a "calls"
|
|
4131
|
+
// reverse-question object it never was, drops it, and turns "run the impact
|
|
4132
|
+
// of X" into the confidently WRONG answer "what calls X" instead of the
|
|
4133
|
+
// honest miss keywords.mjs's own guard already declines the direct parse to.
|
|
4134
|
+
...wordsOf([...CALLS_VERB_REPORT_NOUNS]),
|
|
4130
4135
|
]);
|
|
4131
4136
|
|
|
4132
4137
|
/** Structural scaffolding words — question words, frame verbs, context
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -57,6 +57,10 @@ export function moduleCountOf(graph) {
|
|
|
57
57
|
return graph.individuals.filter((i) => (i.class || "") === "Module").length;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** What a store with zero modules actually holds, said once so every lane that
|
|
61
|
+
* has to decline for that reason declines in the same words. */
|
|
62
|
+
export const NO_CODE_INDEX_NOTE = "This store holds no code index, so it records no modules or commits to look through.";
|
|
63
|
+
|
|
60
64
|
// ---- relation-kind classifier (for impact + tests-coverage) -------------------
|
|
61
65
|
|
|
62
66
|
const KINDS = ["imports", "calls", "defines", "tests", "touches", "contains", "inherits", "callsSymbol", "touchesSymbol"];
|
|
@@ -1360,7 +1364,13 @@ export function renderTestsFor(graph, ind) {
|
|
|
1360
1364
|
}
|
|
1361
1365
|
|
|
1362
1366
|
/** Source modules with no covering test module — a coverage gap view. Test
|
|
1363
|
-
* modules (subjects of test edges, or test-named paths) are excluded.
|
|
1367
|
+
* modules (subjects of test edges, or test-named paths) are excluded.
|
|
1368
|
+
*
|
|
1369
|
+
* "Nothing is uncovered" and "I hold nothing to look at" are opposite
|
|
1370
|
+
* statements, so the empty survey has to say which one it means: with no
|
|
1371
|
+
* source module recorded there is no coverage claim to make at all, and the
|
|
1372
|
+
* full-coverage sentence would be an assertion about a repo this store has
|
|
1373
|
+
* never seen. */
|
|
1364
1374
|
export function renderUntested(graph) {
|
|
1365
1375
|
const covered = new Set();
|
|
1366
1376
|
const testModules = new Set();
|
|
@@ -1368,14 +1378,18 @@ export function renderUntested(graph) {
|
|
|
1368
1378
|
covered.add(e.object);
|
|
1369
1379
|
testModules.add(e.subject);
|
|
1370
1380
|
}
|
|
1371
|
-
const
|
|
1372
|
-
.
|
|
1373
|
-
(i)
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1381
|
+
const sourceModules = graph.individuals.filter(
|
|
1382
|
+
(i) => (i.class || "") === "Module"
|
|
1383
|
+
&& !testModules.has(i.id)
|
|
1384
|
+
&& !isTestPath(String(i.label).toLowerCase()),
|
|
1385
|
+
);
|
|
1386
|
+
if (!sourceModules.length) {
|
|
1387
|
+
return moduleCountOf(graph) === 0
|
|
1388
|
+
? `no modules to check for test coverage in this index. ${NO_CODE_INDEX_NOTE}`
|
|
1389
|
+
: "no source modules to check for test coverage in this index — only test modules are recorded.";
|
|
1390
|
+
}
|
|
1391
|
+
const untested = sourceModules
|
|
1392
|
+
.filter((i) => !covered.has(i.id))
|
|
1379
1393
|
.map((i) => i.label)
|
|
1380
1394
|
.sort();
|
|
1381
1395
|
if (!untested.length) return "every source module has at least one covering test module.";
|
|
@@ -90,6 +90,16 @@ export const DEFAULT_GAME_CONFIG = Object.freeze({
|
|
|
90
90
|
// this is the one switch that turns that off (visionRadius: Infinity for
|
|
91
91
|
// the food-only belief call) without any new belief machinery.
|
|
92
92
|
foodVisionGated: true,
|
|
93
|
+
// Whether prey weigh the predator and the crumb in one score instead of
|
|
94
|
+
// running the evade rung ahead of the forage one. Off by default: strict
|
|
95
|
+
// priority is what ships, and the blend is the alternative a comparison
|
|
96
|
+
// run measures against it (scripts/compare-prey-decision.mjs).
|
|
97
|
+
blendPreyDecision: false,
|
|
98
|
+
// How much of that score belongs to keeping away from the predator, with
|
|
99
|
+
// the rest going to closing on food. 1 evades exactly as the priority
|
|
100
|
+
// chain does, 0 ignores the predator, and the middle takes a crumb that
|
|
101
|
+
// costs little distance. Read only when blendPreyDecision is on.
|
|
102
|
+
preyThreatWeight: 0.5,
|
|
93
103
|
// Whether the town-square lane accepts teaching. Its own knob rather than
|
|
94
104
|
// a share of the adventure one below: the board's sentence table is a
|
|
95
105
|
// different vocabulary, and a page checkbox on the town square should not
|
|
@@ -166,6 +176,8 @@ const MUDIII_KEY_MAP = Object.freeze({
|
|
|
166
176
|
max_prey_population: "maxPreyPopulation",
|
|
167
177
|
max_food_items: "maxFoodItems",
|
|
168
178
|
food_vision_gated: "foodVisionGated",
|
|
179
|
+
blend_prey_decision: "blendPreyDecision",
|
|
180
|
+
prey_threat_weight: "preyThreatWeight",
|
|
169
181
|
teach: "teach",
|
|
170
182
|
});
|
|
171
183
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
|
|
10
10
|
WHERE_MARKERS, MENTION_MARKERS, PLACEHOLDER_NOUNS, PASSIVE_PARTICIPLE_TO_KIND,
|
|
11
|
-
INHERITS_REVERSE_VERBS, HAS_FAMILY_VERBS,
|
|
11
|
+
INHERITS_REVERSE_VERBS, HAS_FAMILY_VERBS, CALLS_VERB_REPORT_NOUNS,
|
|
12
12
|
} from "../../ask-vocab.mjs";
|
|
13
13
|
import { STOPWORDS } from "../normalize.mjs";
|
|
14
14
|
import { VOCAB_WORDS, eligibleForCanon, fuzzyVocabWord } from "../fuzzy.mjs";
|
|
@@ -29,6 +29,33 @@ const TEMPORAL_TRAILING_ADVERBS = new Set(["recently", "lately", "yet", "already
|
|
|
29
29
|
// below (the forward/reverse branches keep their own tested grain-check
|
|
30
30
|
// decline, per that constant's own docblock).
|
|
31
31
|
|
|
32
|
+
/** "run the impact of X" / "if I run impact on X": a calls-kind object phrase
|
|
33
|
+
* led by one of ask-vocab.mjs's CALLS_VERB_REPORT_NOUNS, with a further word
|
|
34
|
+
* after it, is the report the sentence is naming, not the start of a code-graph
|
|
35
|
+
* term — reading "impact app/lib/a.mjs" as one term answers a question nobody
|
|
36
|
+
* asked and can never resolve. A BARE single-word object ("what calls impact")
|
|
37
|
+
* is left alone: that's the genuine, structurally identical question about a
|
|
38
|
+
* symbol that happens to be named "impact", and there's no local signal to
|
|
39
|
+
* tell the two apart, so this only declines the shape a real report request
|
|
40
|
+
* actually takes. */
|
|
41
|
+
function leadsWithCallsReportNoun(objectText) {
|
|
42
|
+
const words = objectText.trim().split(/\s+/);
|
|
43
|
+
return words.length > 1 && CALLS_VERB_REPORT_NOUNS.has(words[0].toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "run the calls of X": CALLS_VERB_REPORT_NOUNS includes "calls" itself (the
|
|
47
|
+
* tmct_calls edge-dump label), and findPhrase (above) matches phrases in
|
|
48
|
+
* table-order rather than leftmost-in-sentence — for kind "calls" that order
|
|
49
|
+
* puts the literal word "calls" ahead of "run", so verbHit lands on "calls"
|
|
50
|
+
* (mid-sentence) instead of "run" (sentence-initial), and "run" survives as
|
|
51
|
+
* the ask-shape's SUBJECT instead of being read as the imperative lead. A
|
|
52
|
+
* subject that is itself nothing but one of the calls kind's own verb words
|
|
53
|
+
* is that same misread, one hop further along — decline it here rather than
|
|
54
|
+
* reordering findPhrase's table, which every other kind's lookup shares. */
|
|
55
|
+
function isBareCallsVerbWord(text) {
|
|
56
|
+
return VERB_TO_KIND[text.trim().toLowerCase()] === "calls";
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
/** Find the longest phrase from `table`'s keys that appears as a contiguous
|
|
33
60
|
* run of `words` (case already lowercased by the caller). Longest-match-first
|
|
34
61
|
* (multi-word phrases before single words) so "co-changes with" isn't
|
|
@@ -289,6 +316,7 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
289
316
|
// the entityType-driven forward/reverse branches below, which keep their
|
|
290
317
|
// own tested grain-check decline.
|
|
291
318
|
if (kind === "defines" && HAS_FAMILY_VERBS.has(verbPhrase)) return null;
|
|
319
|
+
if (kind === "calls" && (leadsWithCallsReportNoun(afterText) || isBareCallsVerbWord(beforeText))) return null;
|
|
292
320
|
// A semantically-reverse verb ("superclass of") swaps subject/object, same as
|
|
293
321
|
// grammar.mjs's T1.
|
|
294
322
|
let subject = beforeText;
|
|
@@ -297,6 +325,7 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
297
325
|
return stamp({ shape: "ask", entityType: null, modifier: "direct", kind, subject, object });
|
|
298
326
|
}
|
|
299
327
|
if (afterText) {
|
|
328
|
+
if (kind === "calls" && leadsWithCallsReportNoun(afterText)) return null;
|
|
300
329
|
// A type word riding beside the named object ("what uses the Store
|
|
301
330
|
// CLASS") describes the OBJECT's grain — it says which "Store" is meant,
|
|
302
331
|
// not what class of thing may answer. Reading it as the result filter
|