@polycode-projects/the-mechanical-code-talker 2.11.6 → 2.11.10
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 +52 -51
- package/bin/tmct.mjs +20 -8
- package/package.json +2 -1
- package/src/adapters/toml-config.mjs +2 -0
- package/src/domain/ask-vocab.mjs +32 -0
- package/src/domain/ask.mjs +89 -4
- package/src/domain/domain.mjs +14 -0
- package/src/domain/interpret/strategies/keywords.mjs +18 -2
- package/src/domain/reference-pack.mjs +15 -3
- package/src/services/adventure-viz.mjs +77 -23
- package/src/services/chat-page-viz.mjs +149 -3
- package/src/services/chat.mjs +207 -47
- package/src/services/extensions.mjs +9 -2
- package/src/services/extract-facts.mjs +74 -7
- package/src/services/init.mjs +21 -2
- package/src/services/ledger-viz.mjs +1 -3
- package/src/services/research-viz.mjs +672 -0
- package/src/surfaces/http/server-http.mjs +172 -3
- package/src/surfaces/web/chat-browser-entry.mjs +21 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +119 -119
- package/src/surfaces/web/research-browser-entry.mjs +319 -0
package/README.md
CHANGED
|
@@ -124,14 +124,14 @@ page answers codebase questions live, and eight more pages each ground their
|
|
|
124
124
|
own domain: a full chat seeded with 32,646 facts (the same nine bands as
|
|
125
125
|
`npm run init:xl`, capped to a 38.7 MB download, 2.1 MB on the wire), the
|
|
126
126
|
**memory ledger** (every fact as a readable sentence; drill by clicking the
|
|
127
|
-
terms inside), the **code explorer** (the same ledger UI refocused on a
|
|
128
|
-
graph, with a hint rail of suggested next questions)
|
|
129
|
-
turns pasted or dropped text into grounded facts and downloads them
|
|
130
|
-
canonical JSONL
|
|
131
|
-
spider-and-fly and text-adventure games, and a sprite gallery
|
|
132
|
-
answers from 1,033 generated sprite facts. The chat page and
|
|
133
|
-
the same paste-or-drop text in place; every page that holds
|
|
134
|
-
exports it as JSONL.
|
|
127
|
+
terms inside), and the **code explorer** (the same ledger UI refocused on a
|
|
128
|
+
code graph, with a hint rail of suggested next questions). An **ingest
|
|
129
|
+
page** turns pasted or dropped text into grounded facts and downloads them
|
|
130
|
+
as canonical JSONL. The rest are a Towers-of-Hanoi plan replayed move by
|
|
131
|
+
move, the spider-and-fly and text-adventure games, and a sprite gallery
|
|
132
|
+
whose chat dock answers from 1,033 generated sprite facts. The chat page and
|
|
133
|
+
the ledger take the same paste-or-drop text in place; every page that holds
|
|
134
|
+
a fact store exports it as JSONL.
|
|
135
135
|
The site hosts its own copy of wink-nlp, ships its assets precompressed,
|
|
136
136
|
and a service worker precaches the big ones, so a second visit works
|
|
137
137
|
offline. `tmct chat --render spider-fly|adventure|sprites [--output <path>]`
|
|
@@ -168,13 +168,14 @@ The same ledger UI, refocused on a code graph, also runs as a desktop app. It
|
|
|
168
168
|
reads a `graph.json` (or a repo's `.tmct/` folder), shows every import, call and
|
|
169
169
|
`contains` edge as a plain sentence around a focus symbol, and docks a live chat
|
|
170
170
|
over the same graph. A hint rail suggests the next question from what the graph
|
|
171
|
-
actually holds
|
|
172
|
-
functions"
|
|
171
|
+
actually holds: "what does X import", "which functions call Y", "list
|
|
172
|
+
functions". Every suggestion resolves to a real answer.
|
|
173
173
|
|
|
174
|
-
The identical page also runs as a plain hosted page
|
|
175
|
-
graph
|
|
176
|
-
|
|
177
|
-
exploring your own repo or graph, which the
|
|
174
|
+
The identical page also runs as a plain hosted page over the demo code
|
|
175
|
+
graph, with nothing to install:
|
|
176
|
+
**[try it live →](https://polycode-projects.gitlab.io/the-mechanical-code-talker/code.html)**.
|
|
177
|
+
The desktop build below is for exploring your own repo or graph, which the
|
|
178
|
+
hosted page cannot reach.
|
|
178
179
|
|
|
179
180
|
Electron is a dev-only dependency and never ships in the npm package. Because
|
|
180
181
|
`.npmrc` sets `ignore-scripts=true`, installing it does not fetch the runtime
|
|
@@ -188,8 +189,8 @@ npm run electron # open the code explorer on the demo gra
|
|
|
188
189
|
```
|
|
189
190
|
|
|
190
191
|
Open a graph or a repo from the window's title bar to explore your own code.
|
|
191
|
-
The UI is channel-agnostic
|
|
192
|
-
`electron/preload.cjs`) is desktop-specific
|
|
192
|
+
The UI is channel-agnostic. Only the Electron shell (`electron/main.mjs` +
|
|
193
|
+
`electron/preload.cjs`) is desktop-specific. The same page stays servable as a
|
|
193
194
|
plain web page. `npm run test:electron` runs the shell smoke via Playwright and
|
|
194
195
|
skips cleanly when the binary is absent.
|
|
195
196
|
|
|
@@ -226,8 +227,8 @@ resolves to a real graph traversal or declines cleanly:
|
|
|
226
227
|
(*because/although/while*), conditionals, and false-premise flags ("why
|
|
227
228
|
does X still import Y" when it no longer does).
|
|
228
229
|
|
|
229
|
-
The full catalog with measured coverage lives in `
|
|
230
|
-
|
|
230
|
+
The full catalog with measured coverage lives in the `BENCHMARK_*.md`
|
|
231
|
+
reports.
|
|
231
232
|
|
|
232
233
|
**Response finishing.** Before an answer prints, it is segmented into typed
|
|
233
234
|
spans: prose versus *protected* entities, paths, numbers, code, provenance,
|
|
@@ -310,7 +311,7 @@ planner over the same read-only graph-query tools chat/serve use
|
|
|
310
311
|
step in order with a provable causal-link proof chain, and folds the results
|
|
311
312
|
into one answer. A request neither the planner nor a single lookup can ground
|
|
312
313
|
escalates to a closed-world goal-reasoner, which deduces maintenance goals
|
|
313
|
-
(coverage gaps, change-coupling risk) straight from the graph
|
|
314
|
+
(coverage gaps, change-coupling risk) straight from the graph, never from
|
|
314
315
|
keywords in your question. Anything none of that grounds is an honest "no plan
|
|
315
316
|
found", the same "grounded or an honest miss" rule as everywhere else in tmct.
|
|
316
317
|
|
|
@@ -344,7 +345,7 @@ composed answer (4): src/handlers/base.mjs, src/handlers/users.mjs, src/server/a
|
|
|
344
345
|
```
|
|
345
346
|
|
|
346
347
|
tmct planned two calls (`tmct_impact` then `tmct_untested`), ran both against the
|
|
347
|
-
real graph, and intersected the results itself
|
|
348
|
+
real graph, and intersected the results itself. You get the four modules that
|
|
348
349
|
are both downstream of the change AND missing coverage, not two separate lists
|
|
349
350
|
you'd have to cross-reference by hand.
|
|
350
351
|
|
|
@@ -361,7 +362,7 @@ composed answer (1): src/lib/http.mjs
|
|
|
361
362
|
|
|
362
363
|
It deduced the goal ("an impactful module must be tested"), gathered every
|
|
363
364
|
untested module, ranked each by blast radius, and named the one worth testing
|
|
364
|
-
first
|
|
365
|
+
first: `src/lib/http.mjs`, the module with the widest reach.
|
|
365
366
|
|
|
366
367
|
`--tools tmct_impact,tmct_untested` restricts which capabilities the planner is
|
|
367
368
|
allowed to use; `--json` prints the full machine-readable loop result (calls,
|
|
@@ -372,9 +373,9 @@ flag reference.
|
|
|
372
373
|
## Teach it a game, then ask it to plan
|
|
373
374
|
|
|
374
375
|
The planner above works over a fixed toolset. This one works over rules you
|
|
375
|
-
teach. A game definition is a plain-text file of controlled English
|
|
376
|
+
teach. A game definition is a plain-text file of controlled English: the
|
|
376
377
|
classes, the pieces, the ordering, and the legal moves as taught action
|
|
377
|
-
rules
|
|
378
|
+
rules, with `#` comment lines carrying example prompts. `tmct init`
|
|
378
379
|
scaffolds one at `.tmct/imports/games/hanoi-3.txt`, and
|
|
379
380
|
`tmct import --file` teaches it sentence by sentence, reporting every line
|
|
380
381
|
and refusing (exit 1) if any sentence declines.
|
|
@@ -402,8 +403,8 @@ Goal (inferred): Plan a move sequence from the current state to the goal (7 move
|
|
|
402
403
|
facts stamped with the step that produced them ("disk-1@step1 rests on peg-c",
|
|
403
404
|
sourced to the plan). The final step re-reads the store and confirms the goal
|
|
404
405
|
from those written facts, never assuming success. The stamp is what makes each
|
|
405
|
-
step a separate record
|
|
406
|
-
rest?", "is disk-1 clear?") reads the current board
|
|
406
|
+
step a separate record. A question about the piece itself ("where does disk-1
|
|
407
|
+
rest?", "is disk-1 clear?") reads the current board: the latest step's facts,
|
|
407
408
|
not every step at once. The search is
|
|
408
409
|
domain-general: the test
|
|
409
410
|
suite teaches Towers of Hanoi purely as sentences for 1 to 8 disks and
|
|
@@ -417,17 +418,17 @@ the plan as a self-contained animated page (see "Two more surfaces" above).
|
|
|
417
418
|
Three games run inside an ordinary chat session, no setup.
|
|
418
419
|
|
|
419
420
|
**Guess the number.** Say `I'm thinking of a number between 1 and 100` and
|
|
420
|
-
tmct guesses by narrowing an interval
|
|
421
|
+
tmct guesses by narrowing an interval: answer `higher`, `lower`, or
|
|
421
422
|
`correct`. It finds any number in at most 7 guesses, and if your answers
|
|
422
423
|
contradict each other it names the contradicting pair and stops rather than
|
|
423
424
|
guessing on. Say `think of a number` to swap seats: tmct commits to a secret
|
|
424
|
-
and answers your guesses
|
|
425
|
-
its own record if you claim it already said `correct`. The
|
|
426
|
-
pinned by `test/corpus/games/guess-number.jsonl`.
|
|
425
|
+
and sticks to it. It answers your guesses, reveals the number on request, and
|
|
426
|
+
corrects you from its own record if you claim it already said `correct`. The
|
|
427
|
+
behaviour is pinned by `test/corpus/games/guess-number.jsonl`.
|
|
427
428
|
|
|
428
429
|
**A text adventure.** Say `start the adventure` (or `play ashcombe hall`)
|
|
429
430
|
and tmct loads a small country-house mystery from a lazily-fetched worlds
|
|
430
|
-
pack (`corpus/worlds/`) into the session's ordinary memory graph
|
|
431
|
+
pack (`corpus/worlds/`) into the session's ordinary memory graph: rooms,
|
|
431
432
|
objects and people become graph facts, and the verbs (`go`, `take`, `open`,
|
|
432
433
|
`unlock`, `look`…) are taught action rules, not hard-wired code. Every move
|
|
433
434
|
writes per-turn snapshot facts, `look` is an extractive digest of the graph,
|
|
@@ -436,30 +437,30 @@ schedule whether you are there to see it or not. The full worked mystery is
|
|
|
436
437
|
pinned step by step in `test/corpus/games/adventure.jsonl`.
|
|
437
438
|
|
|
438
439
|
**Two agents, planning against each other.** Say `play spider and fly` (or
|
|
439
|
-
`watch the spider and the fly`) and tmct runs both sides itself
|
|
440
|
+
`watch the spider and the fly`) and tmct runs both sides itself. Neither is
|
|
440
441
|
player-controlled. A spider hunts a fly across a 10×10 web; each side only
|
|
441
442
|
believes what it can currently see (`vision_radius`, tunable), a fly wanders
|
|
442
443
|
when nothing threatens it and evades when something does, a spider avoids
|
|
443
444
|
other spiders, chases what it believes it sees, and builds a web when it
|
|
444
445
|
holds position. Mass is real: both sides waste away each turn they don't
|
|
445
446
|
eat, and a spider gains exactly the mass of what it catches. You can address
|
|
446
|
-
either side directly (`@spider the fly is east`) to feed it a belief
|
|
447
|
-
or false
|
|
447
|
+
either side directly (`@spider the fly is east`) to feed it a belief, true
|
|
448
|
+
or false, and watch a wrong assertion mislead it for as long as the real
|
|
448
449
|
target stays out of sight. `tmct.toml`'s `[games.spider-fly]` table tunes
|
|
449
450
|
every rate; the full mechanic is pinned in `test/corpus/games/spider-fly.jsonl`.
|
|
450
451
|
|
|
451
452
|
## Learning on a miss
|
|
452
453
|
|
|
453
|
-
A question tmct cannot ground is still an honest miss
|
|
454
|
-
kind of miss (a recognised word, a clean parse, simply no facts anywhere) it
|
|
454
|
+
A question tmct cannot ground is still an honest miss. But on the cleanest
|
|
455
|
+
kind of miss (a recognised word, a clean parse, simply no facts anywhere), it
|
|
455
456
|
now consults two shipped, lazily-loaded packs before giving up:
|
|
456
457
|
|
|
457
|
-
- `corpus/child
|
|
458
|
+
- `corpus/child/`: 93k everyday-world triples filtered from ConceptNet by a
|
|
458
459
|
child-concept seed. Asked `what is a kettle` cold, tmct loads the term's
|
|
459
460
|
triples into memory (provenance `child:conceptnet:kettle`, ranked below
|
|
460
461
|
anything you teach) and answers from them; the next ask answers from
|
|
461
462
|
memory directly.
|
|
462
|
-
- `corpus/reference
|
|
463
|
+
- `corpus/reference/`: 3,887 Simple English Wikipedia summaries. When the
|
|
463
464
|
triples cannot answer, a matching article answers as a cited read-out
|
|
464
465
|
(`source: reference article "Otter"…, CC BY-SA 4.0`).
|
|
465
466
|
|
|
@@ -495,7 +496,7 @@ by cleaned session logs:
|
|
|
495
496
|
relevance rather than loaded wholesale.
|
|
496
497
|
|
|
497
498
|
Every session also writes its own human-readable transcript,
|
|
498
|
-
`.tmct/session-<id>.md
|
|
499
|
+
`.tmct/session-<id>.md`, a glow-friendly Markdown file with one heading per
|
|
499
500
|
turn, the question as a blockquote, the reply in a fenced block. The
|
|
500
501
|
browser chat page's "export .md" button writes the same shape.
|
|
501
502
|
|
|
@@ -538,7 +539,7 @@ Teaching isn't limited to the ACE grammar's fixed shapes. Tell tmct an
|
|
|
538
539
|
arbitrary fact, like "margo really eats ribs", and it mints a fact you can
|
|
539
540
|
ask about directly: "what does margo eat". New vocabulary compounds as you
|
|
540
541
|
teach: "redis is a cache" mints "redis" even though it was never in the
|
|
541
|
-
built-in lexicon, as long as one side of the sentence is already grounded
|
|
542
|
+
built-in lexicon, as long as one side of the sentence is already grounded.
|
|
542
543
|
tmct never mints a fact between two totally ungrounded terms; it declines and
|
|
543
544
|
nudges you to ground one side first. Quantified teaching stores the
|
|
544
545
|
quantifier ("some functions are risky" … "how many functions are risky" →
|
|
@@ -553,13 +554,13 @@ rather than the code graph. A taught class answers both shapes too: after
|
|
|
553
554
|
|
|
554
555
|
When you ask about a term, the read-back shows each "is a kind of" object with
|
|
555
556
|
its own superclass chain: "what is rover" answers "rover is a kind of dog →
|
|
556
|
-
canine → mammal → animal". If one label carries two unrelated senses
|
|
557
|
-
taught "rover is a dog" and a corpus row says "rover is a scout"
|
|
557
|
+
canine → mammal → animal". If one label carries two unrelated senses (you
|
|
558
|
+
taught "rover is a dog" and a corpus row says "rover is a scout"), the answer
|
|
558
559
|
groups by concept ("rover, the dog:" / "rover, the scout:") instead of listing
|
|
559
560
|
two unrelated lines as if they were one thing. The split is deterministic over
|
|
560
561
|
the stored hierarchy: two senses part when a stored disjointness separates
|
|
561
562
|
their ancestors, when their chains never meet, or when they meet only at the
|
|
562
|
-
very top. When the evidence is thin the answer stays a flat list
|
|
563
|
+
very top. When the evidence is thin the answer stays a flat list. Grouping is
|
|
563
564
|
presentation, and it never retracts or reranks a fact.
|
|
564
565
|
|
|
565
566
|
Teaching doesn't have to be typed, either. `tmct extract` runs a plain text
|
|
@@ -586,14 +587,14 @@ carries an `extracted:<file>` provenance tag at its own trust tier.
|
|
|
586
587
|
recognizer skips: a copula or a known relation verb flanked by two nouns
|
|
587
588
|
becomes a candidate triple, stored under its own `optimistic-extract:<file>`
|
|
588
589
|
provenance (prior 0.35, below every curated pack) with no operator tag riding
|
|
589
|
-
alongside
|
|
590
|
+
alongside, so a fuzzy guess can never corroborate a curated fact. It is an
|
|
590
591
|
attempt, not full NLU: a sentence with no clean pair yields nothing.
|
|
591
592
|
`--canonical` prints each grounded fact as a triple, noting how each endpoint
|
|
592
593
|
already links into the store.
|
|
593
594
|
|
|
594
595
|
The same pipeline is one library seam, `ingestText(text, options)` (exported
|
|
595
596
|
as `@polycode-projects/the-mechanical-code-talker/ingest`), and one cold tool,
|
|
596
|
-
`tmct_ingest
|
|
597
|
+
`tmct_ingest`. A browser page, a script, or a tool-calling agent can ground
|
|
597
598
|
text without the CLI.
|
|
598
599
|
|
|
599
600
|
### Provenance and trust
|
|
@@ -754,7 +755,7 @@ already set up. Its `--graph` flag works differently from the others: it appends
|
|
|
754
755
|
```
|
|
755
756
|
|
|
756
757
|
`tmct extract` is the document route into memory described under "Teach it"
|
|
757
|
-
above
|
|
758
|
+
above: the same teach recognizer, reading a file instead of your typing:
|
|
758
759
|
|
|
759
760
|
```output:help:extract
|
|
760
761
|
tmct extract <text-file> read a plain text file's sentences through the chat's own
|
|
@@ -787,7 +788,7 @@ inference" above:
|
|
|
787
788
|
retractable entailed facts (never on the chat path)
|
|
788
789
|
```
|
|
789
790
|
|
|
790
|
-
`tmct viz` renders the memory graph as the ledger explorer
|
|
791
|
+
`tmct viz` renders the memory graph as the ledger explorer, a single,
|
|
791
792
|
self-contained HTML file you can open in a browser:
|
|
792
793
|
|
|
793
794
|
```output:help:viz
|
|
@@ -867,7 +868,7 @@ aws/python/java) into ~7,380 facts on the default sqlite backend, a working
|
|
|
867
868
|
example to copy from. `init:xl` starts from the large persona tier and adds
|
|
868
869
|
the wordnet-xl corpus (~72,000 facts); `init:xxl` swaps wordnet-xl for the
|
|
869
870
|
full WordNet slice plus namenet (~239,000 facts, the biggest committed
|
|
870
|
-
vocabulary
|
|
871
|
+
vocabulary, so expect its imports to take a minute). The xl chain, spelled out:
|
|
871
872
|
|
|
872
873
|
```bash e2e heavy
|
|
873
874
|
npx tmct init --persona-size large # npm run init:xl runs this whole chain from a clone
|
|
@@ -1171,12 +1172,12 @@ The full tables, judge scores, and transcripts are in the linked write-ups.
|
|
|
1171
1172
|
|
|
1172
1173
|
| What it does | Result (2.7.12) | Read the number with this | Method |
|
|
1173
1174
|
|---|---|---|---|
|
|
1174
|
-
| Multi-hop entailment | 379/379 chat cases and 100/100 kernel cases, 0% fabrication, all bands pass | The case set is unchanged from 2.6.0 (same templates, same counts)
|
|
1175
|
-
| Tool-call planning | 68/68 cases, 100% plan-completion, 100% result-completion, 0% hallucination, every rung A0→C2 | Goal driver. 2.6.0 gated at TOOL-7 (62/66, 94%)
|
|
1175
|
+
| Multi-hop entailment | 379/379 chat cases and 100/100 kernel cases, 0% fabrication, all bands pass | The case set is unchanged from 2.6.0 (same templates, same counts). The one real move this cycle is INF-4's ceiling-graded count dropping 35→30: five cases that now pass as genuine capability instead of against the declared honest-miss floor. | `BENCHMARK_INFERENCE_2.7.12.md` |
|
|
1176
|
+
| Tool-call planning | 68/68 cases, 100% plan-completion, 100% result-completion, 0% hallucination, every rung A0→C2 | Goal driver. 2.6.0 gated at TOOL-7 (62/66, 94%). This cycle's router uplift (a guarded RECOVER step, a tied-candidate composer) cleared it: a real capability move, not a ruler change. | `BENCHMARK_AGENT_2.7.12.md` |
|
|
1176
1177
|
| Groundedness | Every answer carries a source, and an empty graph reports itself empty. Judge-scored mean 1.809/2 over 138 cases, 5 hard fails, 136/138 tier-1. | Judged (`claude-haiku-4-5-20251001`, `judge-prompt-v2`) at N=1. The judge prompt moved v1→v2 since 2.6.0, so this is a measurement, not a clean lever comparison against the prior cycle. The judge runs in the offline eval harness, never in the product. | `BENCHMARK_CEFR_ENGLISH_2.7.12.md` |
|
|
1177
1178
|
| Abstention (the honest miss) | 0% fabrication across 479 inference rows (379 chat + 100 kernel) and 0% hallucination across 272 agent rows | Structural, not a tuned threshold. tmct abstains because nothing matched, so the rows test a property of a no-model design rather than a score. | `BENCHMARK_INFERENCE_2.7.12.md`, `BENCHMARK_AGENT_2.7.12.md` |
|
|
1178
|
-
| Determinism | Byte-identical on rerun
|
|
1179
|
-
| 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
|
|
1179
|
+
| 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. | `BENCHMARK_INFERENCE_2.7.12.md` |
|
|
1180
|
+
| 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`). | `BENCHMARK_CONVERSATION_2.7.11.md` |
|
|
1180
1181
|
|
|
1181
1182
|
Three offline benchmark rigs live in a clone (they are not in the npm
|
|
1182
1183
|
package). Each replays a committed case set through the real product and
|
package/bin/tmct.mjs
CHANGED
|
@@ -1261,7 +1261,8 @@ async function main() {
|
|
|
1261
1261
|
const rest = process.argv.slice(3);
|
|
1262
1262
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1263
1263
|
process.stdout.write(
|
|
1264
|
-
"tmct serve — Anthropic Messages API-compatible endpoint (POST /v1/messages)\n
|
|
1264
|
+
"tmct serve — Anthropic Messages API-compatible endpoint (POST /v1/messages)\n" +
|
|
1265
|
+
" plus a capability-router plan verb (POST /v1/plan)\n\n" +
|
|
1265
1266
|
"Usage:\n" +
|
|
1266
1267
|
" tmct serve [--repo <abs>] [--graph <path>] [--config <path>] [--host <h>] [--port <n>]\n\n" +
|
|
1267
1268
|
" --repo <abs> target a repo's graph (<abs>/.tmct/graph.json); default: git root/cwd\n" +
|
|
@@ -1270,9 +1271,14 @@ async function main() {
|
|
|
1270
1271
|
" --config <path> an alternate tmct.toml location (a file or a directory)\n" +
|
|
1271
1272
|
" --host <h> bind address (default 127.0.0.1)\n" +
|
|
1272
1273
|
" --port <n> TCP port (default 8787; 0 picks an ephemeral port)\n\n" +
|
|
1273
|
-
"
|
|
1274
|
-
"
|
|
1275
|
-
"
|
|
1274
|
+
"POST /v1/messages\n" +
|
|
1275
|
+
" Request: { model, messages:[...], tools:[...], max_tokens, system? }\n" +
|
|
1276
|
+
" Response: { id, type:\"message\", role:\"assistant\", content:[...blocks], stop_reason, usage }\n" +
|
|
1277
|
+
" usage is always { input_tokens: 0, output_tokens: 0 } — tmct is the $0 floor.\n\n" +
|
|
1278
|
+
"POST /v1/plan\n" +
|
|
1279
|
+
" Request: { request: \"<NL request>\", tools?: [\"tmct_impact\", ...] }\n" +
|
|
1280
|
+
" Response: the capability-router loop result — grounded { driver, calls, proof,\n" +
|
|
1281
|
+
" composed?, usage } or an in-band honest { refused: true, why }.\n",
|
|
1276
1282
|
);
|
|
1277
1283
|
return;
|
|
1278
1284
|
}
|
|
@@ -1287,13 +1293,19 @@ async function main() {
|
|
|
1287
1293
|
// REPLACES serve's old cwd-only default (loadConfig had no git-root
|
|
1288
1294
|
// fallback) with the same git-root-aware default every other subcommand
|
|
1289
1295
|
// now shares — a deliberate, documented unification, not a regression.
|
|
1290
|
-
const { config } = await resolveRuntimeConfig({ argv: rest });
|
|
1291
|
-
|
|
1296
|
+
const { repo, config, toml } = await resolveRuntimeConfig({ argv: rest });
|
|
1297
|
+
// Open the taught store the same env > tmct.toml > default way `tmct plan`
|
|
1298
|
+
// does, so /v1/plan reasons over the taught world/rule records chat wrote —
|
|
1299
|
+
// registered per request and unregistered after, never mutated by serve.
|
|
1300
|
+
const { openMemoryBackend } = await import("../src/adapters/memory/core.mjs");
|
|
1301
|
+
const backendChoice = String(process.env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
|
|
1302
|
+
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
|
|
1303
|
+
const srv = await startServer({ config, host, port, memoryDir });
|
|
1292
1304
|
process.stdout.write(
|
|
1293
|
-
`tmct serve — Anthropic Messages API at ${srv.url}/v1/messages (POST) — ` +
|
|
1305
|
+
`tmct serve — Anthropic Messages API at ${srv.url}/v1/messages (POST), plan at ${srv.url}/v1/plan (POST) — ` +
|
|
1294
1306
|
`graph ${srv.config.graphFile} — usage billed $0 — Ctrl+C to stop\n`,
|
|
1295
1307
|
);
|
|
1296
|
-
const shutdown = async () => { await srv.close(); process.exit(0); };
|
|
1308
|
+
const shutdown = async () => { await srv.close(); await closeMemoryStore(); process.exit(0); };
|
|
1297
1309
|
process.on("SIGINT", shutdown);
|
|
1298
1310
|
process.on("SIGTERM", shutdown);
|
|
1299
1311
|
return; // the listening server keeps the event loop alive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -144,6 +144,7 @@
|
|
|
144
144
|
"build:plan-bundle": "node scripts/build-plan-bundle.mjs",
|
|
145
145
|
"build:ledger-bundle": "node scripts/build-ledger-bundle.mjs",
|
|
146
146
|
"build:ingest-bundle": "node scripts/build-ingest-bundle.mjs",
|
|
147
|
+
"build:research-bundle": "node scripts/build-research-bundle.mjs",
|
|
147
148
|
"build:code-explorer-bundle": "node scripts/build-code-explorer-bundle.mjs",
|
|
148
149
|
"build:electron": "node scripts/build-electron-app.mjs",
|
|
149
150
|
"electron": "electron electron/main.mjs",
|
|
@@ -108,6 +108,8 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
108
108
|
const seedCfg = {};
|
|
109
109
|
if (seed.enabled !== undefined) seedCfg.enabled = seed.enabled;
|
|
110
110
|
if (seed.limit !== undefined) seedCfg.limit = seed.limit;
|
|
111
|
+
if (seed.capture_unknown_context !== undefined) seedCfg.captureUnknownContext = seed.capture_unknown_context;
|
|
112
|
+
if (seed.unknown_context_limit !== undefined) seedCfg.unknownContextLimit = seed.unknown_context_limit;
|
|
111
113
|
if (Object.keys(seedCfg).length) cfg.seed = seedCfg;
|
|
112
114
|
|
|
113
115
|
// Extension-pack seam (src/services/extensions.mjs): sparse PASS-THROUGH only — the
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -317,6 +317,38 @@ export const PASSIVE_PARTICIPLE_TO_KIND = Object.freeze({
|
|
|
317
317
|
touched: "touches", changed: "touches", modified: "touches", edited: "touches", updated: "touches",
|
|
318
318
|
});
|
|
319
319
|
|
|
320
|
+
// ---- stacked reduced-relative clauses: a "<participle> <preposition>" bigram
|
|
321
|
+
// that opens a reduced relative modifying a head noun ("classes INHERITED FROM
|
|
322
|
+
// Widget DEFINED IN c.mjs"). Each entry names the relation kind and which role
|
|
323
|
+
// the following term fills:
|
|
324
|
+
// role "object" — the surface is active-disguised, the preposition marks the
|
|
325
|
+
// relation's OBJECT, so the answer is the SUBJECTS pointing at the term (a
|
|
326
|
+
// reverse traversal): "inherited from Widget" -> the classes that inherit
|
|
327
|
+
// Widget.
|
|
328
|
+
// role "agent" — a genuine passive whose "by"/"in" marks the AGENT, so the
|
|
329
|
+
// answer is the term's own FORWARD targets: "defined in c.mjs" -> what
|
|
330
|
+
// c.mjs defines.
|
|
331
|
+
// Only consulted by parseStackedReducedRelative, which requires TWO such
|
|
332
|
+
// bigrams on one head noun; a single reduced relative keeps its existing route.
|
|
333
|
+
// Naming senses and directionally-ambiguous bigrams ("imported from", "used
|
|
334
|
+
// in/for", "called <name>") are deliberately absent so they stay honest misses.
|
|
335
|
+
export const REDUCED_RELATIVE_CLAUSES = Object.freeze({
|
|
336
|
+
"inherited from": { kind: "inherits", role: "object" },
|
|
337
|
+
"extended from": { kind: "inherits", role: "object" },
|
|
338
|
+
"subclassed from": { kind: "inherits", role: "object" },
|
|
339
|
+
"defined in": { kind: "defines", role: "agent" },
|
|
340
|
+
"declared in": { kind: "defines", role: "agent" },
|
|
341
|
+
"contained in": { kind: "contains", role: "agent" },
|
|
342
|
+
"imported by": { kind: "imports", role: "agent" },
|
|
343
|
+
"called by": { kind: "calls", role: "agent" },
|
|
344
|
+
"used by": { kind: "uses", role: "agent" },
|
|
345
|
+
"tested by": { kind: "tests", role: "agent" },
|
|
346
|
+
"covered by": { kind: "tests", role: "agent" },
|
|
347
|
+
"touched by": { kind: "touches", role: "agent" },
|
|
348
|
+
"changed by": { kind: "touches", role: "agent" },
|
|
349
|
+
"exported by": { kind: "reexports", role: "agent" },
|
|
350
|
+
});
|
|
351
|
+
|
|
320
352
|
// ---- normalization: contractions/informal spellings expanded before parsing,
|
|
321
353
|
// shared by both parse strategies. ----
|
|
322
354
|
export const CONTRACTIONS = Object.freeze({
|
package/src/domain/ask.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
CONTEXT_PRONOUNS, META_MEANING_VERBS,
|
|
24
24
|
WHERE_MARKERS, MENTION_MARKERS,
|
|
25
25
|
RELATIVE_PRONOUNS, PLACEHOLDER_NOUNS, BOOLEAN_CONNECTIVES, QUALIFIERS,
|
|
26
|
-
PASSIVE_PARTICIPLE_TO_KIND, GENERIC_AGENT_WORDS,
|
|
26
|
+
PASSIVE_PARTICIPLE_TO_KIND, GENERIC_AGENT_WORDS, REDUCED_RELATIVE_CLAUSES,
|
|
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
|
stripTrailingScopeFiller,
|
|
@@ -236,9 +236,57 @@ function parseComposite(text, nlp) {
|
|
|
236
236
|
|| parseList(w, lc, nlp, 0)
|
|
237
237
|
|| parseNested(w, lc, nlp, 0)
|
|
238
238
|
|| parsePluralAnaphoraObject(w, lc, nlp)
|
|
239
|
+
|| parseStackedReducedRelative(w, lc)
|
|
239
240
|
|| parseRelationalOrQualified(w, lc, nlp, 0);
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
// Two reduced relatives stacked on one head noun ("classes INHERITED FROM
|
|
244
|
+
// Widget DEFINED IN c.mjs") — a garden-path shape a naive incremental parser
|
|
245
|
+
// misattaches as a second main clause. Both clauses modify the head, so the
|
|
246
|
+
// reading is their intersection. Each clause's REDUCED_RELATIVE_CLAUSES entry
|
|
247
|
+
// says whether its term is the relation's object (reverse: the subjects that
|
|
248
|
+
// point at it) or its agent (forward: the term's own targets). The head noun's
|
|
249
|
+
// entityType rides the SEED clause, so a forward "defines" leg that would
|
|
250
|
+
// otherwise return every symbol is filtered to the asked kind. Anything that
|
|
251
|
+
// isn't exactly [lead] head bigram term bigram term returns null, leaving
|
|
252
|
+
// every other shape's behavior byte-identical.
|
|
253
|
+
const STACKED_RRC_LEAD = new Set(["which", "the", "all"]);
|
|
254
|
+
function parseStackedReducedRelative(w, lc) {
|
|
255
|
+
let i = 0;
|
|
256
|
+
if (STACKED_RRC_LEAD.has(lc[i])) i += 1;
|
|
257
|
+
const noun = entityNoun(lc[i]);
|
|
258
|
+
if (!noun || noun.placeholder || !noun.entityType) return null;
|
|
259
|
+
const entityType = noun.entityType;
|
|
260
|
+
i += 1;
|
|
261
|
+
const bigramAt = (k) => (k + 1 < lc.length ? REDUCED_RELATIVE_CLAUSES[`${lc[k]} ${lc[k + 1]}`] : undefined);
|
|
262
|
+
const rr1 = bigramAt(i);
|
|
263
|
+
if (!rr1) return null;
|
|
264
|
+
const term1Start = i + 2;
|
|
265
|
+
let split = -1;
|
|
266
|
+
let rr2;
|
|
267
|
+
for (let k = term1Start; k + 1 < lc.length; k += 1) {
|
|
268
|
+
const hit = bigramAt(k);
|
|
269
|
+
if (hit) { split = k; rr2 = hit; break; }
|
|
270
|
+
}
|
|
271
|
+
if (split < 0) return null;
|
|
272
|
+
const term1 = w.slice(term1Start, split).join(" ").trim();
|
|
273
|
+
const term2 = w.slice(split + 2).join(" ").trim();
|
|
274
|
+
if (!term1 || !term2) return null;
|
|
275
|
+
const clauseFor = (rr, term) => (rr.role === "object"
|
|
276
|
+
? { shape: "reverse", kind: rr.kind, entityType, modifier: "direct", object: term }
|
|
277
|
+
: { shape: "forward", kind: rr.kind, modifier: "direct", object: term });
|
|
278
|
+
const seed = clauseFor(rr1, term1);
|
|
279
|
+
seed.entityType = entityType; // head-noun class filter on the seed set
|
|
280
|
+
return {
|
|
281
|
+
node: "boolean",
|
|
282
|
+
entityType,
|
|
283
|
+
atoms: [
|
|
284
|
+
{ op: "seed", kind: "set", ast: { node: "clause", clause: seed } },
|
|
285
|
+
{ op: "intersection", kind: "set", ast: { node: "clause", clause: clauseFor(rr2, term2) } },
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
242
290
|
// Negation as set complement: "which X do not <verb> Y" compiles to
|
|
243
291
|
// allOfClass(kind) DIFFERENCE (the positive result set). The "Change"
|
|
244
292
|
// pseudo-type has no bounded enumerable universe, so a complement over
|
|
@@ -598,6 +646,12 @@ function parsePredicateFilter(words, nlp) {
|
|
|
598
646
|
const restLc = lc.slice(i);
|
|
599
647
|
if (!rest.length) return { type: "all" };
|
|
600
648
|
if (restLc.every((x) => QUALIFIERS[x])) return { type: "qual", filters: restLc };
|
|
649
|
+
// A bare concrete entity noun ("which of them are functions") narrows the
|
|
650
|
+
// prior set to one class rather than testing a relation.
|
|
651
|
+
if (rest.length === 1) {
|
|
652
|
+
const en = entityNoun(restLc[0]);
|
|
653
|
+
if (en && !en.placeholder && en.entityType) return { type: "entity", entityType: en.entityType };
|
|
654
|
+
}
|
|
601
655
|
const clause = parseSimpleClause(`what ${rest.join(" ")}`, nlp);
|
|
602
656
|
if (clause && (clause.shape === "reverse" || clause.shape === "forward") && clause.object) {
|
|
603
657
|
return { type: "clause", clause };
|
|
@@ -1729,6 +1783,8 @@ function evalAnaphora(graph, ast, opts) {
|
|
|
1729
1783
|
const f = ast.filter;
|
|
1730
1784
|
if (f && f.type === "qual") {
|
|
1731
1785
|
items = items.filter((ind) => f.filters.every((q) => qualHolds(graph, ind, QUALIFIERS[q])));
|
|
1786
|
+
} else if (f && f.type === "entity") {
|
|
1787
|
+
items = items.filter((ind) => ind.class === f.entityType);
|
|
1732
1788
|
} else if (f && f.type === "clause") {
|
|
1733
1789
|
const r = resolveObject(graph, f.clause.object);
|
|
1734
1790
|
if (!r.match) items = [];
|
|
@@ -1743,9 +1799,12 @@ function evalAnaphora(graph, ast, opts) {
|
|
|
1743
1799
|
}
|
|
1744
1800
|
// A count over a prior set names the entity kind when survivors share a
|
|
1745
1801
|
// class; fall back to the prior set's own class when the filter empties it,
|
|
1746
|
-
// so the honest-empty render still names what was checked.
|
|
1802
|
+
// so the honest-empty render still names what was checked. An entity-type
|
|
1803
|
+
// filter that empties the set names the FILTER's kind ("functions"), not the
|
|
1804
|
+
// base set's — the reader asked which of them were that kind.
|
|
1747
1805
|
const sameClass = (list) => (list.length && list.every((x) => x.class === list[0].class) ? list[0].class : null);
|
|
1748
|
-
const
|
|
1806
|
+
const emptyClass = f && f.type === "entity" ? f.entityType : sameClass(baseItems);
|
|
1807
|
+
const common = items.length ? sameClass(items) : emptyClass;
|
|
1749
1808
|
if (ast.mode === "count") return { compositeKind: "count", count: items.length, entityType: common, matches: [] };
|
|
1750
1809
|
return { compositeKind: "set", matches: items, entityType: common };
|
|
1751
1810
|
}
|
|
@@ -3296,6 +3355,15 @@ function bareVerbFor(kind) {
|
|
|
3296
3355
|
return RELATIONS[kind]?.bare || kind;
|
|
3297
3356
|
}
|
|
3298
3357
|
|
|
3358
|
+
/** The passive participle of a relation ("uses" -> "used", "imports" ->
|
|
3359
|
+
* "imported"), for the confirming "X is <participle> by Y" frame. The bare
|
|
3360
|
+
* forms in this vocabulary are all regular, so a single +d/+ed rule covers
|
|
3361
|
+
* them. */
|
|
3362
|
+
function passiveParticipleFor(kind) {
|
|
3363
|
+
const bare = bareVerbFor(kind);
|
|
3364
|
+
return bare.endsWith("e") ? `${bare}d` : `${bare}ed`;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3299
3367
|
/** English gloss of a SET-COMPLEMENT AST — the shape parseNegation compiles
|
|
3300
3368
|
* "which X do not <verb> Y" into (allOfClass DIFFERENCE the positive set).
|
|
3301
3369
|
* Restates the question in the same grammar the positive canonical uses
|
|
@@ -3747,11 +3815,28 @@ function renderCore(parsed, result, graph) {
|
|
|
3747
3815
|
};
|
|
3748
3816
|
}
|
|
3749
3817
|
const entityWord = nounFor(parsed.entityType || "Module", 2);
|
|
3818
|
+
// Name the resolved antecedent, not the raw pronoun: "who touched it" that
|
|
3819
|
+
// bound "it" to fnAlpha must say so, or the receipt reads as though nothing
|
|
3820
|
+
// was resolved at all. Scoped to a context pronoun so a typed term keeps the
|
|
3821
|
+
// wording the reader chose, never its normalized graph label.
|
|
3822
|
+
const object = (result.objMatch && CONTEXT_PRONOUNS.includes(String(parsed.object || "").toLowerCase()))
|
|
3823
|
+
? result.objMatch.label
|
|
3824
|
+
: parsed.object;
|
|
3750
3825
|
return {
|
|
3751
|
-
content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${
|
|
3826
|
+
content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${object}. ${touchesRephraseHint(graph)}`,
|
|
3752
3827
|
miss: true, ambiguous: false,
|
|
3753
3828
|
};
|
|
3754
3829
|
}
|
|
3830
|
+
// A polar reverse question ("is X used anywhere") with exactly one match
|
|
3831
|
+
// reads as a confirming yes that names the single subject, rather than a bare
|
|
3832
|
+
// one-item list. Scoped to one match: two or more keep the plain list.
|
|
3833
|
+
if (parsed.polar && result.matches.length === 1) {
|
|
3834
|
+
const objLabel = result.objMatch?.label || parsed.object;
|
|
3835
|
+
return {
|
|
3836
|
+
content: `Yes — ${objLabel} is ${passiveParticipleFor(parsed.kind)} by ${result.matches[0].label}.`,
|
|
3837
|
+
miss: false, ambiguous: false, matches: result.matches,
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3755
3840
|
// Route by the matched entities' actual class, not just the parsed hint —
|
|
3756
3841
|
// grouping module-level matches by-module would read as nonsense ("in
|
|
3757
3842
|
// a.mjs there is a.mjs"). Fine-grained grouping only applies to sub-module
|
package/src/domain/domain.mjs
CHANGED
|
@@ -228,6 +228,20 @@ export function stateFromFacts(factRows, domain) {
|
|
|
228
228
|
return state;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/** The highest @stepN snapshot index present for a domain individual, or 0
|
|
232
|
+
* when the board carries no snapshot layer yet. A freshly minted plan reads
|
|
233
|
+
* this as its stepBase, so its own @stepK writes stack ABOVE any standing
|
|
234
|
+
* snapshot instead of colliding with it and being read as the same layer. */
|
|
235
|
+
export function maxSnapshotStep(factRows, domain) {
|
|
236
|
+
const individuals = domainIndividuals(domain);
|
|
237
|
+
let max = 0;
|
|
238
|
+
for (const row of factRows || []) {
|
|
239
|
+
const m = SNAPSHOT_RE.exec(normTerm(row.subject));
|
|
240
|
+
if (m && individuals.has(m[1])) max = Math.max(max, Number(m[2]));
|
|
241
|
+
}
|
|
242
|
+
return max;
|
|
243
|
+
}
|
|
244
|
+
|
|
231
245
|
/** Canonical identity for a state (rows are kept sorted). NUL-joined so
|
|
232
246
|
* multi-word terms can never collide with the separator; spelled without an
|
|
233
247
|
* escape sequence because tooling has twice turned a source-level \\0 into a
|
|
@@ -19,6 +19,11 @@ import { VOCAB_WORDS, eligibleForCanon, fuzzyVocabWord } from "../fuzzy.mjs";
|
|
|
19
19
|
const PASSIVE_AUX = new Set(["is", "are", "was", "were", "be", "been", "being", "get", "gets", "got"]);
|
|
20
20
|
const WH_WORDS = new Set(["which", "what", "who", "whom", "whose"]);
|
|
21
21
|
const PLACEHOLDER_SET = new Set(PLACEHOLDER_NOUNS.map((w) => w.toLowerCase()));
|
|
22
|
+
// A trailing time adverb on a bare passive ("was X touched RECENTLY") is not the
|
|
23
|
+
// relation's object — it modifies the whole clause. Only consulted on the
|
|
24
|
+
// participle path with no agent "by", so it can never touch an active-verb
|
|
25
|
+
// object slot. Mirrors ask.mjs's TEMPORAL_TRAIL_FILLER for the when-shape.
|
|
26
|
+
const TEMPORAL_TRAILING_ADVERBS = new Set(["recently", "lately", "yet", "already", "ever", "again"]);
|
|
22
27
|
// See ask-vocab.mjs's own HAS_FAMILY_VERBS for why a bare have-family verb
|
|
23
28
|
// never resolves to `defines` in this strategy's two-named-role "ask" shape
|
|
24
29
|
// below (the forward/reverse branches keep their own tested grain-check
|
|
@@ -198,7 +203,14 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
198
203
|
.join(" ")
|
|
199
204
|
.trim();
|
|
200
205
|
const beforeText = sideText(0, verbHit.start);
|
|
201
|
-
|
|
206
|
+
let afterText = sideText(verbHit.end, words.length);
|
|
207
|
+
// A lone trailing time adverb after a bare participle ("was it touched
|
|
208
|
+
// recently") is clause-level, not the relation's object; drop it so the
|
|
209
|
+
// sentence reaches the bare-passive branch and answers over the patient
|
|
210
|
+
// instead of trying to resolve "recently" as a term.
|
|
211
|
+
if (verbFromParticiple && !lcWords.includes("by") && TEMPORAL_TRAILING_ADVERBS.has(afterText.toLowerCase())) {
|
|
212
|
+
afterText = "";
|
|
213
|
+
}
|
|
202
214
|
const kind = verbHit.kind;
|
|
203
215
|
// slices read canonWords, not lcWords: the entity/modifier spans were matched
|
|
204
216
|
// against the canonicalized array, whose word IS the table key.
|
|
@@ -316,7 +328,11 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
316
328
|
// nobody asked, so this declines and the sentence misses honestly.
|
|
317
329
|
if (beforeText.split(/\s+/).length > 1) return null;
|
|
318
330
|
if (kind === "touches") return stamp({ shape: "when", entityType: null, modifier: "direct", kind, object: beforeText });
|
|
319
|
-
|
|
331
|
+
// A sentence that LEADS with the passive auxiliary is an interrogative
|
|
332
|
+
// yes/no ("is X used anywhere"), not a declarative patient statement; mark
|
|
333
|
+
// it so a single reverse match can render a confirming Yes frame.
|
|
334
|
+
const polar = PASSIVE_AUX.has(lcWords[0]);
|
|
335
|
+
return stamp({ shape: "reverse", entityType, modifier, kind, object: beforeText, ...(polar ? { polar: true } : {}) });
|
|
320
336
|
}
|
|
321
337
|
// forward keeps the spotted entityType (traverse()'s commit-as-subject grain
|
|
322
338
|
// selection); modifier stays hardcoded since no forward closure traversal exists.
|