@polycode-projects/the-mechanical-code-talker 0.7.1 → 0.8.1
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/ROADMAP.md +120 -11
- package/bin/tmct.mjs +45 -0
- package/package.json +3 -1
- package/src/ask-vocab.mjs +1 -1
- package/src/ask.mjs +123 -3
- package/src/chat.mjs +79 -7
- package/src/concept.mjs +5 -0
- package/src/conformance.mjs +1 -1
- package/src/corpus/templates.mjs +1 -1
- package/src/finish.mjs +1 -1
- package/src/hash.mjs +1 -1
- package/src/interpret/normalize.mjs +65 -1
- package/src/interpret/pipeline.mjs +18 -3
- package/src/interpret/strategies/ace.mjs +49 -0
- package/src/interpret/strategies/keywords.mjs +2 -2
- package/src/providers/bootstrap.mjs +1 -1
- package/src/providers/fixture.mjs +1 -1
- package/src/providers/graph-service.mjs +1 -1
- package/src/repository-interface.mjs +1 -1
- package/src/router/goal-reasoner.mjs +266 -0
- package/src/router/guardrail.mjs +120 -0
- package/src/router/planner.mjs +168 -0
- package/src/router/registry.mjs +271 -0
- package/src/router/resolver.mjs +331 -0
- package/src/server-http.mjs +296 -0
- package/src/syllogise.mjs +0 -0
- package/src/tui/app.mjs +63 -14
package/ROADMAP.md
CHANGED
|
@@ -12,6 +12,44 @@ mapped into them (item numbers retained for traceability); the seven sketches
|
|
|
12
12
|
formerly held in `code-talker-ideas.txt` are folded into items 8–11 below and
|
|
13
13
|
the file has been deleted.
|
|
14
14
|
|
|
15
|
+
## Where we are now (2026-07-06)
|
|
16
|
+
|
|
17
|
+
**Built: v0.8.1 — Phase 11 deepened (0.8.0 shipped + published; 0.8.1 push held for the operator).**
|
|
18
|
+
`npm test` green (**916**). 0.8.1 hardened the router with real *reasoning* measurement and closed the
|
|
19
|
+
frontier stages, all under the same demonstrated-vs-designed honesty discipline:
|
|
20
|
+
|
|
21
|
+
- **AGENTBENCH now grades the executed composed RESULT, not just the call-plan** (retires the headline
|
|
22
|
+
0.8.0 caveat). Under result-grading the resolver is **97% plan / 91% result / 0% hallucination**; the
|
|
23
|
+
honest gap lands exactly where reasoning is hard (C1, C2).
|
|
24
|
+
- **Stage 5 — the C2 goal-reasoner** (BDI + Goal-Driven Autonomy) genuinely lifts result-completion.
|
|
25
|
+
The honest, **like-for-like** figure is the *driver swap on the identical 39-case ladder*
|
|
26
|
+
(`AGENTBENCH_0.8.1_001`): resolver **85% → goal-reasoner 95% result** (+10pp, purely the driver, 0%
|
|
27
|
+
hallucination). C2 is **cleared for one declared coverage-invariant goal-rule** — real and
|
|
28
|
+
phrasing-robust (validated by held-out phrasings graded blind, no request-string literals), but
|
|
29
|
+
**thinly sampled, not rule-general**; the open-world case is honestly **refused**. *(NB: the
|
|
30
|
+
cross-release "0→83% C2" reads on a grown 6-case C2 ladder whose 4 new cases exercise the
|
|
31
|
+
goal-reasoner — not a like-for-like rate; the same-basket win is one case fixed, 0→50% original C2.)*
|
|
32
|
+
- **Stage 2 — imperative intent frames + ACE reach** (`AGENTBENCH_0.8.1_002`, goal driver): **100% plan
|
|
33
|
+
/ 95% result / 0% hallucination**. `tmct_calls` is now **genuinely NL-reachable** via a distinct
|
|
34
|
+
edge-dump frame (not by un-tagging — bidirectional conformance enforces it; `NOT_NL_REACHABLE` is now
|
|
35
|
+
`{}`). ACE is wired **async** into the interpret pipeline so the synchronous CHATBENCH parse spine is
|
|
36
|
+
**byte-identical** — ACE is inert in the shipped product path (`interpret()` is called nowhere in
|
|
37
|
+
chat/ask/server/bin), reach without regression.
|
|
38
|
+
- **Chat surface (CHATBENCH_0.8.1):** quick wins (singular "what is a test", friendly commit-author
|
|
39
|
+
refs, "No tests cover X" honest-empty, the chatbench discourse-count flake root-caused + scrubbed) +
|
|
40
|
+
two playtests (coverage-survey + authorship routing; **6 frozen `chatflow-*` transcripts** total).
|
|
41
|
+
Joint deterministic CHATBENCH (both new frame tables together): **no tier-1 regression vs 0.7.1.**
|
|
42
|
+
Judged tags touched by the text changes were re-judged (not blanket-reused) — see `CHATBENCH_0.8.1`.
|
|
43
|
+
|
|
44
|
+
**Shipped earlier in v0.8.0 (published):** all five Phase-11 tracks — the `/v1/messages` shim + Stage-0
|
|
45
|
+
registry + resolver/guardrail/planner (`AGENTBENCH_0.8.0_001`: 96% *plan* completion, 0% hallucination,
|
|
46
|
+
closed-world C1); the three chat levers (`CHATBENCH_0.8.0`: tier-1 331→333); the `../bedrock-meter` `$0`
|
|
47
|
+
rung (rank 0 below nova-micro, £0 metered e2e); the playtest; and the Stage-2/Stage-5 research notes.
|
|
48
|
+
|
|
49
|
+
**Next:** grow the thin C2/coverage sampling to a second goal-rule; author→commit querying (a dead-end
|
|
50
|
+
the friendly-commit-ref quick win opened); AGENTBENCH ladder depth; the tier-4/tier-5 Phase LATER long
|
|
51
|
+
tail. See Phase 11 below and `HANDOVER.md` for the full open-follow-up list.
|
|
52
|
+
|
|
15
53
|
## The umbrella product definition (item 1)
|
|
16
54
|
|
|
17
55
|
**A tolerant, ELIZA/PARRY-style chat, obsessed with software.** A best-efforts
|
|
@@ -279,11 +317,13 @@ and the block-index summary (blocks, tokens, top PageRank blocks). Same renderer
|
|
|
279
317
|
|
|
280
318
|
> **STATUS: in progress — the two headline B1 levers shipped.** Negation as a bounded SET
|
|
281
319
|
> COMPLEMENT ("which X do not <verb> Y") and reversible-passive traversal ("X is imported by Y")
|
|
282
|
-
> are live; the harness meta-fixes and the rest of the ranked lever board
|
|
320
|
+
> are live; the harness meta-fixes and the rest of the ranked lever board (levers 3–6 + the C2
|
|
321
|
+
> ceiling, below) continue. **This section is the durable home for the tuning arc** — the detailed
|
|
322
|
+
> cycle-4 plan was archived to `archive/PLAN_CYCLE_4.md` once its substance lived here.
|
|
283
323
|
|
|
284
|
-
>
|
|
285
|
-
> (
|
|
286
|
-
>
|
|
324
|
+
> The dependency audit that also fed this phase is archived at `archive/PLAN_DEPENDENCY_STRATEGY.md`
|
|
325
|
+
> (verdict: no dep changes now; a standing adoption register + avoid-list for phases 6-9; two
|
|
326
|
+
> near-term actions, both shipped — see below).
|
|
287
327
|
|
|
288
328
|
The immediate work: drive the graded benchmark up the CEFR ladder, one lever per cycle, per
|
|
289
329
|
`SKILL_TUNING_CYCLE.md`. Cycle 3 (post-wiring-wave, CHATBENCH_003) gave the first full-spectrum
|
|
@@ -312,7 +352,7 @@ from meta-1's harness artifact), **C2 ceiling LAST**. Operator decision 2026-07-
|
|
|
312
352
|
them**. Exit criterion in the plan (roughly: B1 grade mean ≥ ~1.5 with all cells dual-draw
|
|
313
353
|
agreeing unlocks C-grade judging).
|
|
314
354
|
|
|
315
|
-
## Near-term actions (from the dependency audit, PLAN_DEPENDENCY_STRATEGY.md)
|
|
355
|
+
## Near-term actions (from the dependency audit, archive/PLAN_DEPENDENCY_STRATEGY.md)
|
|
316
356
|
|
|
317
357
|
> **STATUS: both shipped.** The wink browser-loader seam is added (shared model loader with a
|
|
318
358
|
> browser registration path) and `fnv1a` is single-sourced into `src/hash.mjs`.
|
|
@@ -322,7 +362,7 @@ Two concrete, low-risk actions the audit surfaced — not features, not dep chan
|
|
|
322
362
|
1. **Fix wink's `createRequire` browser-loader gap** — the wink model IS the browser build, but
|
|
323
363
|
our adapters load it via `createRequire(import.meta.url)` (`ask-nlp.mjs:29`, `prose-nlp.mjs:31`),
|
|
324
364
|
which is Node-only. Browser mode needs a bundler `import` path. **A Phase 8 (browser-mode)
|
|
325
|
-
blocker** — budget it into `PLAN_REPOSITORY_INTERFACE.md`; it is a wiring fix, not a dependency
|
|
365
|
+
blocker** — budget it into `archive/PLAN_REPOSITORY_INTERFACE.md`; it is a wiring fix, not a dependency
|
|
326
366
|
change (the model is already browser-capable).
|
|
327
367
|
2. **Single-source `fnv1a`** — extract the content-address hash to one `src/hash.mjs` so the
|
|
328
368
|
cross-version-stable fact-id contract has a single definition. Trivial refactor, do any time;
|
|
@@ -337,7 +377,7 @@ Two concrete, low-risk actions the audit surfaced — not features, not dep chan
|
|
|
337
377
|
> `/memory` inspector surfaces contradictions with provenance. Legacy `mgx:factProvenance` kept
|
|
338
378
|
> as a compat shim.
|
|
339
379
|
|
|
340
|
-
> Detailed plan: **PLAN_PROVENANCE_TRUST.md**.
|
|
380
|
+
> Detailed plan: **archive/PLAN_PROVENANCE_TRUST.md**.
|
|
341
381
|
|
|
342
382
|
*(Operator-specified 2026-07-05, from the observation that Phase-6 canonicalise-and-link,
|
|
343
383
|
tier-4 learn-on-miss, and the ConceptNet slice all share one shape: raw source preserved,
|
|
@@ -370,7 +410,7 @@ fact enters memory:
|
|
|
370
410
|
> **STATUS: shipped.** A technical (C1) register of templates and productive/performance
|
|
371
411
|
> dual-banding (computed from the `via` provenance) are live in the benchmark.
|
|
372
412
|
|
|
373
|
-
> Detailed plan: **`PLAN_FORMULAIC_COMPETENCE.md`**.
|
|
413
|
+
> Detailed plan: **`archive/PLAN_FORMULAIC_COMPETENCE.md`**.
|
|
374
414
|
|
|
375
415
|
The operator's insight upgraded to the strategy: a consistently-failed C1/C2 graded cell whose
|
|
376
416
|
answer EXISTS as a stable phrasing in technical prose is not a ceiling — it is a
|
|
@@ -403,7 +443,7 @@ answer EXISTS as a stable phrasing in technical prose is not a ceiling — it is
|
|
|
403
443
|
> pass runs on prose spans only under a protected-span invariance guard. The a/an article fix is
|
|
404
444
|
> active; broader voice/agreement rules are implemented-but-parked.
|
|
405
445
|
|
|
406
|
-
> Detailed plan: **`PLAN_RESPONSE_FINISHING.md`**.
|
|
446
|
+
> Detailed plan: **`archive/PLAN_RESPONSE_FINISHING.md`**.
|
|
407
447
|
|
|
408
448
|
*(Refined 2026-07-05; decisions settled with the operator. Fact invariance is achieved by
|
|
409
449
|
CONSTRUCTION, not by hope. Finishing operates over a SEGMENTED answer, never a raw string.
|
|
@@ -440,7 +480,7 @@ ever emerge.)*
|
|
|
440
480
|
> runnable conformance/compatibility suite; and `tmct init` (scaffold `.tmct/`, `tmct.toml`,
|
|
441
481
|
> tier-1 seed, provenance).
|
|
442
482
|
|
|
443
|
-
> Detailed plan: **`PLAN_REPOSITORY_INTERFACE.md`**.
|
|
483
|
+
> Detailed plan: **`archive/PLAN_REPOSITORY_INTERFACE.md`**.
|
|
444
484
|
|
|
445
485
|
*(Operator-specified 2026-07-05; upgraded from research item to a build phase. tmct was spun OUT
|
|
446
486
|
of seonix; this inverts the relationship: seonix reorients as a USER that imports the tmct
|
|
@@ -512,7 +552,7 @@ a passive payload loader into the product's primary integration surface.)*
|
|
|
512
552
|
before it — PROACTIVELY extending memory with inferences that will be useful later, forward and
|
|
513
553
|
backward chaining over the OWL base during idle/fold time rather than at query time.)*
|
|
514
554
|
|
|
515
|
-
> Detailed plan: **`PLAN_SPECULATIVE_INFERENCE.md`**.
|
|
555
|
+
> Detailed plan: **`archive/PLAN_SPECULATIVE_INFERENCE.md`**.
|
|
516
556
|
|
|
517
557
|
**A maintenance job, not a query-time cost.** Speculative inference runs as an explicit
|
|
518
558
|
batch — `npx tmct syllogise --depth <N>` (default depth bounded, e.g. 32) — and **once
|
|
@@ -595,6 +635,75 @@ you toward precision" promise on the conversational surface.
|
|
|
595
635
|
with new graded cells for the miss / empty-graph / concept-touch surfaces so these become
|
|
596
636
|
regression-protected levers, not one-off polish.
|
|
597
637
|
|
|
638
|
+
## Phase 11 — The capability router & the agentic bench (0.8.0 shipped · 0.8.1 deepened)
|
|
639
|
+
|
|
640
|
+
*(Operator-directed 2026-07-06; built the same day across five concurrent tracks.)* tmct as a **deterministic, no-LLM
|
|
641
|
+
tool router** behind an Anthropic-compatible API — the workstream specified in
|
|
642
|
+
`PLAN_CAPABILITY_ROUTER.md`, grounded in `docs/references/planning/`. This is a **new capability on a
|
|
643
|
+
new axis** (driving a tool loop, not answering a chat turn), so it gets its **own benchmark**:
|
|
644
|
+
**`AGENTBENCH`**, a sibling to CHATBENCH — same versioned-naming + grading discipline
|
|
645
|
+
(`AGENTBENCH_<version>.md`, `_00N` for re-runs), but the levels are the **A0→C2 agentic rungs** and a
|
|
646
|
+
**hallucinated tool call is an automatic fail**.
|
|
647
|
+
|
|
648
|
+
**Status: DEMONSTRATED (with a stated scope caveat).** The router is built and measured:
|
|
649
|
+
`AGENTBENCH_0.8.0_001` = **96% completion at 0% hallucination on every rung**, closed-world ladder
|
|
650
|
+
cleared to **C1**. The gate the phase was staked on — a **0% hallucination rate on a real domain** (the
|
|
651
|
+
graph-query toolset over the fixture) — **is met.** The honest scope line, held from the start:
|
|
652
|
+
AGENTBENCH grades the correct **call-plan + causal-link proof, not the executed composed result**; the
|
|
653
|
+
B1/B2/C1 rungs are **thin (2–3 cases)**; and the one C2 case is **refused** (the Stage-5 goal-reasoner
|
|
654
|
+
is designed, not built). So "closed-world C1" means *the router provably selects and binds the right
|
|
655
|
+
tool sequence*, not *end-to-end multi-step reasoning* — the demonstrated-vs-designed boundary is the
|
|
656
|
+
real deliverable. The five tracks below are all built; the two research-agent stages (Track 4 below)
|
|
657
|
+
remain designed-not-built by intent.
|
|
658
|
+
|
|
659
|
+
### Track 1 — chat-surface levers (next CHATBENCH; all three)
|
|
660
|
+
The three levers `CHATBENCH_0.7.1` measured + ranked — which **double as router prerequisites** (they
|
|
661
|
+
gate the A2→B1→C1 rungs, per Phase B of the router plan):
|
|
662
|
+
1. **Pronoun / focus binding** — the "it → Commit" mis-bind (`B1 pron 1.24`); biggest movable mass.
|
|
663
|
+
2. **Discourse-count anaphora** — "count them / how many of those" over a prior listing (clears the 2
|
|
664
|
+
`CHATBENCH_0.7.1` tier-1 misses).
|
|
665
|
+
3. **C1 temporal-over-relative composition** — the two-hop ceiling (`C1 temp 0.31`).
|
|
666
|
+
Land all three (not just #1); they raise the chat floor *and* the router's floor at once.
|
|
667
|
+
|
|
668
|
+
### Track 2 — the router build (the within-horizon slice, in order)
|
|
669
|
+
Buildable now with a frontier model as co-author (see PLAN §"solved vs unsolved"):
|
|
670
|
+
- **Phase A — the shim.** An Anthropic Messages API endpoint (`/v1/messages`, `tool_use`/`tool_result`
|
|
671
|
+
blocks). **Extended:** also present as a **`bedrock-meter`-compatible routing target** (see below).
|
|
672
|
+
- **Phase B — measure today → `AGENTBENCH_0.7.2.md`.** Shim + a small graph-query toolset up the
|
|
673
|
+
A0→C2 ladder; the honest baseline (expected A0 solid, A1–A2 partial, per the CHATBENCH_0.7.1
|
|
674
|
+
inherited assets).
|
|
675
|
+
- **Phase C — the grading ladder.** The AGENTBENCH benchmark itself (rungs as levels, comparable
|
|
676
|
+
local/hosted models as reference bands, zero-hallucination gate).
|
|
677
|
+
- **Stage 0 — capability registry** (`Capability`/`Parameter`/`Precondition`/`Effect` = STRIPS/PDDL
|
|
678
|
+
operators as facts).
|
|
679
|
+
- **Stage 1 — the resolver** (unification + backward chaining / a mini Datalog).
|
|
680
|
+
- **Stage 4 — the guardrail** (validate an LLM's proposed `tool_use` against declared preconditions —
|
|
681
|
+
the hybrid fast-path; cheap once 0–1 exist).
|
|
682
|
+
- **Stage 3 — the planner** (POP/HTN over operators + Steel & Ho monitor-and-replan → **closed-world
|
|
683
|
+
C1**; optionally defer search to an external PDDL solver).
|
|
684
|
+
|
|
685
|
+
### Phase A extension — the `bedrock-meter` deployment surface
|
|
686
|
+
`../bedrock-meter` is pre-flight Bedrock cost metering + capping, with a **roadmap optimiser** that
|
|
687
|
+
"cheaply assesses a task's complexity … and routes to the lowest-cost capable model" (it already
|
|
688
|
+
meters Nova Lite + Nova Micro). tmct — **benchmarked against agent capabilities by AGENTBENCH** — slots
|
|
689
|
+
in as the **$0 floor *below* Nova-micro** in that routing ladder: for a request class AGENTBENCH proves
|
|
690
|
+
in-envelope, the optimiser routes to tmct (deterministic, ~$0, ms latency) instead of any metered
|
|
691
|
+
model. So Phase A's shim is built **bedrock-meter-pluggable**, and AGENTBENCH is what defines the
|
|
692
|
+
envelope the optimiser is allowed to trust. This is the concrete "near-free alternative" deployment.
|
|
693
|
+
|
|
694
|
+
### Track 3 — playtest alongside the build (`git worktree`)
|
|
695
|
+
Run `SKILL_CHAT_PLAYTEST.md` **in a parallel `git worktree`** while the router is built — the
|
|
696
|
+
dialogue-flow dead-end hunt keeps running without blocking the build, and its fixes **merge back**.
|
|
697
|
+
(The worktree is auto-cleaned if unchanged; merge the frozen `test/chatflow-*` transcripts in.)
|
|
698
|
+
|
|
699
|
+
### Track 4 — research agents (the "at the edge" stages)
|
|
700
|
+
Two stages need design judgment + exploration, so they run as **background research agents**, off the
|
|
701
|
+
critical build path:
|
|
702
|
+
- **Stage 2 — intent frames, controlled fragment** — imperative NL → structured intent for the
|
|
703
|
+
controlled command language (the front-end; the general case stays out-of-scope / escalate).
|
|
704
|
+
- **Stage 5 — goal-reasoner, closed-world C2** — BDI + Goal-Driven Autonomy: deduce-goals (long-chain
|
|
705
|
+
deduction) → plan-each (C1) → threat-aware, *persistent* first-step arbitration.
|
|
706
|
+
|
|
598
707
|
## Phase LATER — recognized, deferred, not now
|
|
599
708
|
|
|
600
709
|
Features we have deliberately shaped seams for but will not build until the phases above have
|
package/bin/tmct.mjs
CHANGED
|
@@ -48,6 +48,10 @@ Usage:
|
|
|
48
48
|
tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
|
|
49
49
|
[--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
|
|
50
50
|
bounded, low-trust, retractable entailed facts (never on the chat path)
|
|
51
|
+
tmct serve [--repo <abs>] run the Anthropic Messages API-compatible endpoint
|
|
52
|
+
[--host <h>] [--port <n>] (POST /v1/messages) over the graph — a deterministic,
|
|
53
|
+
no-LLM "model" a tool-loop client can call; $0 usage.
|
|
54
|
+
Defaults: host 127.0.0.1, port 8787. Ctrl+C to stop.
|
|
51
55
|
tmct cli <tool> '{…}' invoke a graph tool directly (carry-over, de-emphasized)
|
|
52
56
|
tmct cli digest '{…}' architecture map + per-module context bundles
|
|
53
57
|
tmct --help show this help
|
|
@@ -387,6 +391,47 @@ async function main() {
|
|
|
387
391
|
return;
|
|
388
392
|
}
|
|
389
393
|
|
|
394
|
+
if (mode === "serve") {
|
|
395
|
+
// `tmct serve` — the Phase-A capability-router interface: an Anthropic
|
|
396
|
+
// Messages API-compatible HTTP endpoint (POST /v1/messages) over the graph.
|
|
397
|
+
// A deterministic, no-LLM "model" a tool-loop client (Claude Code) can point
|
|
398
|
+
// at; every response reports $0 usage. Read-only: no session artifacts, no
|
|
399
|
+
// writes back to the graph. See src/server-http.mjs.
|
|
400
|
+
const rest = process.argv.slice(3);
|
|
401
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
402
|
+
process.stdout.write(
|
|
403
|
+
"tmct serve — Anthropic Messages API-compatible endpoint (POST /v1/messages)\n\n" +
|
|
404
|
+
"Usage:\n" +
|
|
405
|
+
" tmct serve [--repo <abs>] [--host <h>] [--port <n>]\n\n" +
|
|
406
|
+
" --repo <abs> target a repo's graph (<abs>/.tmct/graph.json); default: cwd/TMCT_GRAPH_FILE\n" +
|
|
407
|
+
" --host <h> bind address (default 127.0.0.1)\n" +
|
|
408
|
+
" --port <n> TCP port (default 8787; 0 picks an ephemeral port)\n\n" +
|
|
409
|
+
"Request: { model, messages:[...], tools:[...], max_tokens, system? }\n" +
|
|
410
|
+
"Response: { id, type:\"message\", role:\"assistant\", content:[...blocks], stop_reason, usage }\n" +
|
|
411
|
+
" usage is always { input_tokens: 0, output_tokens: 0 } — tmct is the $0 floor.\n",
|
|
412
|
+
);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const strFlag = (name, dflt) => { const j = rest.indexOf(name); return j !== -1 ? rest[j + 1] : dflt; };
|
|
416
|
+
const repoPath = strFlag("--repo", undefined);
|
|
417
|
+
const host = strFlag("--host", "127.0.0.1");
|
|
418
|
+
const portRaw = strFlag("--port", undefined);
|
|
419
|
+
const port = portRaw !== undefined && Number.isFinite(Number(portRaw)) ? Number(portRaw) : 8787;
|
|
420
|
+
const { join } = await import("node:path");
|
|
421
|
+
const { startServer } = await import("../src/server-http.mjs");
|
|
422
|
+
const { loadConfig, DEFAULT_GRAPH_REL } = await import("../src/config.mjs");
|
|
423
|
+
const configFor = (rp) => rp ? { graphFile: join(rp, DEFAULT_GRAPH_REL) } : loadConfig();
|
|
424
|
+
const srv = await startServer({ config: configFor(repoPath), host, port });
|
|
425
|
+
process.stdout.write(
|
|
426
|
+
`tmct serve — Anthropic Messages API at ${srv.url}/v1/messages (POST) — ` +
|
|
427
|
+
`graph ${srv.config.graphFile} — usage billed $0 — Ctrl+C to stop\n`,
|
|
428
|
+
);
|
|
429
|
+
const shutdown = async () => { await srv.close(); process.exit(0); };
|
|
430
|
+
process.on("SIGINT", shutdown);
|
|
431
|
+
process.on("SIGTERM", shutdown);
|
|
432
|
+
return; // the listening server keeps the event loop alive
|
|
433
|
+
}
|
|
434
|
+
|
|
390
435
|
if (mode === "cli") {
|
|
391
436
|
await runCliMode();
|
|
392
437
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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.",
|
|
@@ -85,6 +85,8 @@
|
|
|
85
85
|
"example:polyglot": "node bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
|
|
86
86
|
"chatbench:run": "node chatbench/run.mjs",
|
|
87
87
|
"chatbench:judge": "node chatbench/judge.mjs",
|
|
88
|
+
"serve": "node bin/tmct.mjs serve",
|
|
89
|
+
"agentbench:run": "node agentbench/run.mjs",
|
|
88
90
|
"audit": "npm audit --audit-level=high",
|
|
89
91
|
"audit:fix": "npm audit fix"
|
|
90
92
|
},
|
package/src/ask-vocab.mjs
CHANGED
|
@@ -220,7 +220,7 @@ export const MODIFIER_TO_KIND = Object.freeze({
|
|
|
220
220
|
transitively: "transitive", indirectly: "transitive",
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
// ---- reversible-passive participles (Cycle 6, PLAN_CYCLE_4.md) — past participles ->
|
|
223
|
+
// ---- reversible-passive participles (Cycle 6, archive/PLAN_CYCLE_4.md) — past participles ->
|
|
224
224
|
// relation kind, for the agent-marked passive "X is <participle> by Y". Kept SEPARATE
|
|
225
225
|
// from VERB_TO_KIND on purpose: these forms are NOT standalone active verbs in this
|
|
226
226
|
// grammar ("defined" belongs to the multi-word "is defined in" and to the WHERE_MARKERS
|
package/src/ask.mjs
CHANGED
|
@@ -246,6 +246,7 @@ function parseComposite(text, nlp) {
|
|
|
246
246
|
const lc = w.map((x) => x.toLowerCase());
|
|
247
247
|
return parseNegation(text, nlp, 0)
|
|
248
248
|
|| parseForwardNegation(w, lc, nlp)
|
|
249
|
+
|| parseTemporal(w, lc, nlp, 0)
|
|
249
250
|
|| parseAnaphora(w, lc, nlp)
|
|
250
251
|
|| parseAggregate(w, lc, nlp)
|
|
251
252
|
|| parseSuperlative(w, lc, nlp)
|
|
@@ -254,7 +255,7 @@ function parseComposite(text, nlp) {
|
|
|
254
255
|
|| parseRelationalOrQualified(w, lc, nlp, 0);
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
// B1 NEGATION (Cycle 5, PLAN_CYCLE_4.md) — the SET COMPLEMENT. "which X do not <verb>
|
|
258
|
+
// B1 NEGATION (Cycle 5, archive/PLAN_CYCLE_4.md) — the SET COMPLEMENT. "which X do not <verb>
|
|
258
259
|
// Y" / "X that don't <verb> Y" / "modules not importing Y" / "which X are not
|
|
259
260
|
// <qualifier>" compiles to allOfClass(kind) DIFFERENCE (the positive result set),
|
|
260
261
|
// reusing the EXISTING machinery: evalBoolean already folds a "difference" atom, and
|
|
@@ -416,6 +417,44 @@ function parseNested(w, lc, nlp, depth) {
|
|
|
416
417
|
return null;
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
// TEMPORAL-OVER-RELATIVE (Phase 11 Track 1, lever 3) — "when did <relative set> [last]
|
|
421
|
+
// change". The flat when-shape (traverse) dates the commits touching ONE resolved term;
|
|
422
|
+
// this composes that same touches→commit→date-sort machinery as an OUTER operator over a
|
|
423
|
+
// NESTED inner set ("when did the modules that import X last change", "when were the
|
|
424
|
+
// functions that call Y last touched"). Fires only for a RELATIVE subject (a "that/which"
|
|
425
|
+
// marker) so the single-entity "when did X change" stays on the flat path untouched; a
|
|
426
|
+
// marker present but uncompilable inner is an honest miss, never a guess.
|
|
427
|
+
const TEMPORAL_AUX = new Set(["did", "was", "were", "do", "does", "has", "have", "had"]);
|
|
428
|
+
const TEMPORAL_TAIL = new Set([
|
|
429
|
+
"change", "changed", "changes", "update", "updated", "updates",
|
|
430
|
+
"modify", "modified", "modifies", "touch", "touched", "touches", "edit", "edited", "revise", "revised",
|
|
431
|
+
]);
|
|
432
|
+
const TEMPORAL_TRAIL_FILLER = new Set(["last", "recently", "ever", "get", "got", "been", "then", "now", "already"]);
|
|
433
|
+
const TEMPORAL_DET = new Set(["the", "a", "an", "all", "those", "these", "any"]);
|
|
434
|
+
|
|
435
|
+
function parseTemporal(w, lc, nlp, depth = 0) {
|
|
436
|
+
if (lc[0] !== "when") return null; // temporal questions lead with "when"
|
|
437
|
+
let i = 1;
|
|
438
|
+
if (!TEMPORAL_AUX.has(lc[i])) return null; // need an auxiliary ("when did …")
|
|
439
|
+
i += 1;
|
|
440
|
+
// the change-verb tail — take the LAST occurrence so "…that import X last change" works.
|
|
441
|
+
let t = -1;
|
|
442
|
+
for (let k = lc.length - 1; k >= i; k -= 1) { if (TEMPORAL_TAIL.has(lc[k])) { t = k; break; } }
|
|
443
|
+
if (t < 0) return null; // no change verb → not a temporal question
|
|
444
|
+
let subjWords = w.slice(i, t);
|
|
445
|
+
let subjLc = lc.slice(i, t);
|
|
446
|
+
while (subjLc.length && TEMPORAL_TRAIL_FILLER.has(subjLc[subjLc.length - 1])) { subjWords = subjWords.slice(0, -1); subjLc = subjLc.slice(0, -1); }
|
|
447
|
+
while (subjLc.length && TEMPORAL_DET.has(subjLc[0])) { subjWords = subjWords.slice(1); subjLc = subjLc.slice(1); }
|
|
448
|
+
if (!subjWords.length) return null;
|
|
449
|
+
// ONLY a relative/nested subject composes here; a bare named entity is the flat path's.
|
|
450
|
+
if (!subjLc.some((x) => RELATIVE_PRONOUNS.includes(x))) return null;
|
|
451
|
+
const framed = FRAME_WORDS.has(subjLc[0]) ? subjWords.join(" ") : `which ${subjWords.join(" ")}`;
|
|
452
|
+
const inner = parseSetPhrase(framed, nlp, depth + 1);
|
|
453
|
+
if (!inner || inner.node === "miss") return inner ? { node: "miss", reason: inner.reason || "the inner set of the temporal query didn't parse" } : { node: "miss", reason: "the inner set of the temporal query didn't parse" };
|
|
454
|
+
const noun = entityNoun(subjLc[0]);
|
|
455
|
+
return { node: "temporal", inner, entityType: (noun && noun.entityType) || null };
|
|
456
|
+
}
|
|
457
|
+
|
|
419
458
|
/** ANAPHORA over the previous result set: "which of those/them <filter>", "how many
|
|
420
459
|
* of those <filter>". Requires "of <pronoun>" (so a bare "those" in a term never
|
|
421
460
|
* fires). Returns a {node:"anaphora"} (mode count|list), a miss (filter present but
|
|
@@ -745,7 +784,15 @@ function reverseOverSet(graph, kind, entityType, objectIds) {
|
|
|
745
784
|
const edges = edgesOfKind(graph, symbolKind).filter((e) => objectIds.has(e.object));
|
|
746
785
|
return uniqueById(edges.map((e) => graph.byId.get(e.subject)).filter((s) => s && s.class === entityType));
|
|
747
786
|
}
|
|
748
|
-
|
|
787
|
+
// GRAIN-AWARE OBJECT SET (Phase 11 Track 1, lever 3): when the inner set resolved to
|
|
788
|
+
// FINE symbols (functions/methods/…), also scan the symbol-grain sibling — the coarse
|
|
789
|
+
// edge (touches Commit→Module, calls Module→Module) can never point AT a symbol, so a
|
|
790
|
+
// two-hop whose inner clause produced symbols ("which commits touched the functions
|
|
791
|
+
// that call X") would falsely miss without it. Mirrors traverse()'s objIsFineSymbol
|
|
792
|
+
// branch for the flat path; the module-coarse case is byte-unchanged (no fine ids).
|
|
793
|
+
const objHasFine = !!symbolKind && [...objectIds].some((id) => FINE_ENTITY_TYPES.has(graph.byId.get(id)?.class));
|
|
794
|
+
const scanKinds = objHasFine ? [...kindsFor(kind), symbolKind] : kindsFor(kind);
|
|
795
|
+
const edges = scanKinds.flatMap((k) => edgesOfKind(graph, k)).filter((e) => objectIds.has(e.object));
|
|
749
796
|
const subjects = uniqueById(edges.map((e) => graph.byId.get(e.subject)).filter(Boolean));
|
|
750
797
|
if (!entityType || entityType === "Change") return subjects;
|
|
751
798
|
const direct = subjects.filter((s) => s.class === entityType);
|
|
@@ -933,6 +980,24 @@ function degreeMetric(graph, ind, metric) {
|
|
|
933
980
|
}
|
|
934
981
|
return n;
|
|
935
982
|
}
|
|
983
|
+
/** TEMPORAL over a nested set (lever 3) — the commits that touched ANY member of the
|
|
984
|
+
* inner set, newest commit date first. Reuses the SAME touches→commit→date-sort the
|
|
985
|
+
* flat when-shape runs (mgx:commitDate is ISO-8601, so a lexical sort IS a date sort;
|
|
986
|
+
* undated commits sort last and render says so). `entityType` is the inner noun, only
|
|
987
|
+
* for phrasing. An empty inner set (nothing resolved) or no touching commit is an
|
|
988
|
+
* honest empty — never a guess. */
|
|
989
|
+
function evalTemporal(graph, ast, opts) {
|
|
990
|
+
const inner = evalSet(graph, ast.inner, opts);
|
|
991
|
+
const ids = new Set(inner.map((i) => i.id));
|
|
992
|
+
if (!ids.size) return { compositeKind: "temporal", matches: [], entityType: ast.entityType, innerCount: 0 };
|
|
993
|
+
// reverseOverSet(touches) collects the touching commits across BOTH grains (its
|
|
994
|
+
// grain-aware object-set branch reads touchesSymbol when the inner set is symbols).
|
|
995
|
+
const commits = reverseOverSet(graph, "touches", "Commit", ids);
|
|
996
|
+
const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
|
|
997
|
+
commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
|
|
998
|
+
return { compositeKind: "temporal", matches: commits, entityType: ast.entityType, innerCount: inner.length };
|
|
999
|
+
}
|
|
1000
|
+
|
|
936
1001
|
function evalSuperlative(graph, ast) {
|
|
937
1002
|
const pool = graph.individuals.filter((i) => i.class === ast.entityType);
|
|
938
1003
|
const scored = pool.map((ind) => ({ ind, score: degreeMetric(graph, ind, ast.metric) }))
|
|
@@ -950,6 +1015,7 @@ export function evalComposite(graph, ast, opts = {}) {
|
|
|
950
1015
|
if (ast.node === "count") return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
|
|
951
1016
|
if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
|
|
952
1017
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
1018
|
+
if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
|
|
953
1019
|
if (ast.node === "anaphora") return evalAnaphora(graph, ast, opts);
|
|
954
1020
|
return { compositeKind: "set", matches: evalSet(graph, ast, opts), entityType: ast.entityType || null };
|
|
955
1021
|
}
|
|
@@ -1000,6 +1066,32 @@ function renderComposite(parsed, result) {
|
|
|
1000
1066
|
miss: false, ambiguous: false, matches: result.matches,
|
|
1001
1067
|
};
|
|
1002
1068
|
}
|
|
1069
|
+
// temporal (lever 3): the newest touching commit + its date over the inner set;
|
|
1070
|
+
// honest empty when nothing in the set was touched, undated commits said out loud —
|
|
1071
|
+
// the same discipline as the flat when-shape, now over a composed set.
|
|
1072
|
+
if (result.compositeKind === "temporal") {
|
|
1073
|
+
const n = result.innerCount || 0;
|
|
1074
|
+
const setNoun = result.entityType ? nounFor(result.entityType, n || 2) : (n === 1 ? "entity" : "entities");
|
|
1075
|
+
const wasWere = n === 1 ? "was" : "were";
|
|
1076
|
+
if (!n) {
|
|
1077
|
+
return { content: `nothing in the index matches the inner set, so there is no change history to date.`, miss: true, ambiguous: false, matches: [] };
|
|
1078
|
+
}
|
|
1079
|
+
if (!result.matches.length) {
|
|
1080
|
+
return { content: `no recorded commit touched the ${n} ${setNoun} in that set in this index.`, miss: true, ambiguous: false, matches: [] };
|
|
1081
|
+
}
|
|
1082
|
+
const newest = result.matches[0];
|
|
1083
|
+
const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
|
|
1084
|
+
if (!date) {
|
|
1085
|
+
return { content: `the ${setNoun} in that set ${wasWere} last touched by commit ${newest.label}, but this index records no commit dates — regenerate the graph to attach mgx:commitDate.`, miss: true, ambiguous: false, matches: result.matches };
|
|
1086
|
+
}
|
|
1087
|
+
const msg = (newest.attributes || []).find((a) => a.key === "message")?.value || "";
|
|
1088
|
+
const day = String(date).slice(0, 10);
|
|
1089
|
+
const more = result.matches.length - 1;
|
|
1090
|
+
return {
|
|
1091
|
+
content: `the ${setNoun} in that set ${wasWere} last touched by commit ${newest.label} on ${day}${msg ? ` ("${msg}")` : ""}${more ? `; ${more} earlier commit${more === 1 ? "" : "s"} recorded` : ""}.`,
|
|
1092
|
+
miss: false, ambiguous: false, matches: result.matches,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1003
1095
|
// set-producing
|
|
1004
1096
|
if (!result.matches.length) {
|
|
1005
1097
|
return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}.`, miss: true, ambiguous: false, matches: [] };
|
|
@@ -1554,6 +1646,17 @@ function symbolLabelOf(ind) {
|
|
|
1554
1646
|
return ["Function", "Method"].includes(ind.class) ? `function ${label}()` : label;
|
|
1555
1647
|
}
|
|
1556
1648
|
|
|
1649
|
+
/** A FRIENDLY commit reference for a "who touched X" list — the raw sha alone reads as
|
|
1650
|
+
* noise, so when the Commit individual carries an author (mgx:commitAuthor → key
|
|
1651
|
+
* "author") name them beside it. The label is already the graph's short ref (the
|
|
1652
|
+
* builder stores sha.slice(0,12)), so it is used verbatim. Degrades gracefully: a
|
|
1653
|
+
* commit with no recorded author renders the sha alone, exactly as before. */
|
|
1654
|
+
function commitRefOf(ind) {
|
|
1655
|
+
const sha = String(ind.label || ind.id || "");
|
|
1656
|
+
const author = (ind.attributes || []).find((a) => a.key === "author")?.value;
|
|
1657
|
+
return author ? `${sha} (${author})` : sha;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1557
1660
|
function listJoin(syms) {
|
|
1558
1661
|
return syms.length > 1 ? `${syms.slice(0, -1).join(", ")} and ${syms[syms.length - 1]}` : syms[0];
|
|
1559
1662
|
}
|
|
@@ -1742,6 +1845,20 @@ function renderCore(parsed, result) {
|
|
|
1742
1845
|
miss: true, ambiguous: false,
|
|
1743
1846
|
};
|
|
1744
1847
|
}
|
|
1848
|
+
// "what tests cover X" / "what tests X" — the tests themselves are the search
|
|
1849
|
+
// target (no explicit entity keyword → entityType null), and "tests" reads as a
|
|
1850
|
+
// verb phrase, so the generic "No <modules> found whose module directly tests <obj>"
|
|
1851
|
+
// template garbles: it mislabels the searched kind as "modules" and lets the leaked
|
|
1852
|
+
// "cover " verb ride into the object ("…directly tests cover X"). Render the honest
|
|
1853
|
+
// empty as the natural "No tests cover X." The frozen entity-keyword form ("which
|
|
1854
|
+
// modules test X", entityType="Module") keeps its pinned wording below.
|
|
1855
|
+
if (parsed.kind === "tests" && !parsed.entityType) {
|
|
1856
|
+
const obj = String(parsed.object || "").replace(/^cover(?:s|ing)?\s+/i, "").trim();
|
|
1857
|
+
return {
|
|
1858
|
+
content: `No tests cover ${obj}. (traversal: ${result.traversal || "no traversal resolved"})`,
|
|
1859
|
+
miss: true, ambiguous: false,
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1745
1862
|
// NOTE (Cycle 5): a voice-nit rephrasing ("that directly <verb>") was reverted —
|
|
1746
1863
|
// the frozen v1 cases.jsonl pins the "whose module directly <verb>s X" wording
|
|
1747
1864
|
// (hm-empty-result-calls / tf-wat-calls / ns-wondering), and the case set is
|
|
@@ -1761,7 +1878,10 @@ function renderCore(parsed, result) {
|
|
|
1761
1878
|
// Commit list ("which commits touched X") has no containing module to group by, so
|
|
1762
1879
|
// anything that is not a fine entity takes the flat join.
|
|
1763
1880
|
if (parsed.shape === "forward" || parsed.entityType === "Module" || result.matches.every((m) => !FINE_ENTITY_TYPES.has(m.class))) {
|
|
1764
|
-
|
|
1881
|
+
// A reverse "who touched X" resolves to Commit individuals — render friendly refs
|
|
1882
|
+
// (short sha + author) instead of the raw stored sha; every other flat list (module
|
|
1883
|
+
// labels, etc.) keeps its own label verbatim.
|
|
1884
|
+
const shown = result.matches.slice(0, OVERFLOW_CAP).map((m) => m.class === "Commit" ? commitRefOf(m) : m.label);
|
|
1765
1885
|
const extra = result.matches.length > OVERFLOW_CAP ? `, …and ${result.matches.length - OVERFLOW_CAP} more` : "";
|
|
1766
1886
|
return { content: shown.join(" and ") + extra + ".", miss: false, ambiguous: false, matches: result.matches };
|
|
1767
1887
|
}
|
package/src/chat.mjs
CHANGED
|
@@ -105,6 +105,29 @@ export const COMMANDS = {
|
|
|
105
105
|
* back to the focus for, and that update the focus on a successful resolve. */
|
|
106
106
|
const ENTITY_ARGS = new Set(["symbol", "module", "class"]);
|
|
107
107
|
|
|
108
|
+
/** The individual classes that are FOCUS-WORTHY — the code entities the ENTITY_ARGS
|
|
109
|
+
* commands (symbol/module/class) name and that "it"/"this" should bind to. A
|
|
110
|
+
* `Commit`/`Session` (history/provenance) or a `SchemaClass`/`SchemaPredicate`
|
|
111
|
+
* (vocabulary meta-node) is a real graph individual but NOT a standing antecedent:
|
|
112
|
+
* it must not silently DISPLACE a code-entity focus, or the next turn's pronoun
|
|
113
|
+
* binds to the wrong thing (CHATBENCH_0.7.1 B1-pron: "it" → Commit). */
|
|
114
|
+
const FOCUS_WORTHY_CLASSES = new Set(["Module", "Function", "Class", "Method", "Attribute", "GlobalVariable"]);
|
|
115
|
+
|
|
116
|
+
/** Is the individual with this id a focus-worthy code entity? Looks the class up on
|
|
117
|
+
* the loaded graph (focus objects stay `{id,label}` — the class is never stored on
|
|
118
|
+
* them, so a caller that deepEquals the focus shape is unaffected). An unknown id
|
|
119
|
+
* (ext: endpoint, missing) is treated as not-worthy: conservative, so it never
|
|
120
|
+
* displaces a standing code focus but is freely adopted when there is none. */
|
|
121
|
+
const isFocusWorthy = (graph, id) => FOCUS_WORTHY_CLASSES.has(graph?.byId?.get(id)?.class);
|
|
122
|
+
|
|
123
|
+
/** The focus to carry forward after a turn resolved `ent`. A newly-resolved entity
|
|
124
|
+
* becomes the focus UNLESS it is not focus-worthy (a Commit/Session/schema node)
|
|
125
|
+
* AND there is already a standing focus-worthy (code) focus — in which case the
|
|
126
|
+
* standing code focus holds. So a query whose object resolves to a Commit never
|
|
127
|
+
* hijacks the "it" antecedent from the module/function the user was working on. */
|
|
128
|
+
const nextFocus = (graph, focus, ent) =>
|
|
129
|
+
(!focus?.id || isFocusWorthy(graph, ent.id) || !isFocusWorthy(graph, focus.id)) ? ent : focus;
|
|
130
|
+
|
|
108
131
|
/** System-command words that a forgiving shell accepts WITHOUT the leading "/":
|
|
109
132
|
* `stats`, `memory`, `describe X`, `members X`, … all work bare. "help" is left
|
|
110
133
|
* out on purpose — bare "help" stays the friendly orientation; "/help" is the
|
|
@@ -127,9 +150,18 @@ export function asBareCommand(line) {
|
|
|
127
150
|
if (!trimmed || trimmed.startsWith("/")) return null;
|
|
128
151
|
const [first, ...restTok] = trimmed.split(/\s+/);
|
|
129
152
|
if (!COMMAND_WORDS.has(first.toLowerCase())) return null;
|
|
153
|
+
const fl = first.toLowerCase();
|
|
130
154
|
const rest = restTok.join(" ");
|
|
131
155
|
// Zero-arg system commands are always the command; a bare command word is too.
|
|
132
|
-
if (!rest ||
|
|
156
|
+
if (!rest || fl === "stats" || fl === "memory") return `/${trimmed}`;
|
|
157
|
+
// A NO-ARGUMENT command word ("untested") with trailing words is NOT a command
|
|
158
|
+
// call — the /untested tool takes no argument and would silently drop the qualifier,
|
|
159
|
+
// listing MODULES for "untested classes". "untested classes" / "untested modules"
|
|
160
|
+
// is a kind-FILTERED query the ask engine answers correctly (Base, Button for
|
|
161
|
+
// classes) AND, as a listing, seeds discourse-count anaphora ("count them",
|
|
162
|
+
// "how many of those are tested") with its match set — the CHATBENCH_0.7.1
|
|
163
|
+
// discourse-count tier-1 misses (g-b1-disc-count-22/-3). Fall through to the engine.
|
|
164
|
+
if (COMMANDS[fl]?.arg == null && rest) return null;
|
|
133
165
|
// Arg commands: route only a short, name-like argument (no query connectives),
|
|
134
166
|
// so "describe Widget" / "members my class" route but a compositional query does not.
|
|
135
167
|
if (restTok.length <= 3 && !QUERY_CONNECTIVES.test(rest)) return `/${trimmed}`;
|
|
@@ -1300,6 +1332,19 @@ function relationTermOf(query, envelope) {
|
|
|
1300
1332
|
if ((m = q.match(/^what\s+([a-z][a-z-]*?)\s+are\s+there$/))) return m[1];
|
|
1301
1333
|
// "what is calling", "what is importing" (bare gerund, no object)
|
|
1302
1334
|
if ((m = q.match(/^what\s+(?:is|are)\s+([a-z][a-z-]*ing)$/))) return m[1];
|
|
1335
|
+
// THE SINGULAR META FORM — "what is a test" / "what is an import". The whole meta
|
|
1336
|
+
// shape used to be excluded here to keep the frozen am-meta-imports ambiguity case
|
|
1337
|
+
// ("what does imports mean") out; but that case is a DIFFERENT shape (ambiguousParse
|
|
1338
|
+
// → envelope.parsed is null), and a relation word whose SINGULAR reads as a real
|
|
1339
|
+
// graph-schema class/predicate ("what is a contains"/"cochange") answers non-miss
|
|
1340
|
+
// from the ordinary meta path. So admit the article meta form ONLY when the ordinary
|
|
1341
|
+
// path MISSED on a plain definitional parse (envelope.miss on a shape:"meta" object):
|
|
1342
|
+
// an unambiguous relation word like "test" — no schema reading, no ambiguity — then
|
|
1343
|
+
// reaches the relation-concept force exactly as its plural "what are the tests" does.
|
|
1344
|
+
// RELATION_TERM still gates the term downstream, so a non-relation miss is untouched.
|
|
1345
|
+
if (envelope?.miss === true && envelope?.parsed?.shape === "meta" && envelope.parsed.object) {
|
|
1346
|
+
return envelope.parsed.object;
|
|
1347
|
+
}
|
|
1303
1348
|
return null;
|
|
1304
1349
|
}
|
|
1305
1350
|
|
|
@@ -1382,7 +1427,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
1382
1427
|
const pending = composed.remainder && composed.remainder.length
|
|
1383
1428
|
? { items: composed.remainder, noun: composed.noun }
|
|
1384
1429
|
: null;
|
|
1385
|
-
return { text, instances: composed.instances, pending };
|
|
1430
|
+
return { text, instances: composed.instances, allIds: composed.allInstanceIds, pending };
|
|
1386
1431
|
}
|
|
1387
1432
|
|
|
1388
1433
|
/** A bare question → tmct_ask. When a focus is set AND the graph is in hand we
|
|
@@ -1398,7 +1443,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1398
1443
|
// answer's entity set. That set is the ids the last dispatched turn cited — carried
|
|
1399
1444
|
// on `last.detail.matches`. Threading it as ask()'s `prev` is what lets the anaphora
|
|
1400
1445
|
// node resolve instead of the "needs a previous answer" honest miss.
|
|
1401
|
-
|
|
1446
|
+
// Prefer the FULL id set (`allIds`) when the last turn carried one — a concept-force
|
|
1447
|
+
// listing caps its shown `matches` at MAX_EXAMPLES, so counting `matches` alone would
|
|
1448
|
+
// undercount "count them" over a truncated listing (CHATBENCH_0.7.1 discourse-count).
|
|
1449
|
+
const prev = (last?.detail?.allIds && last.detail.allIds.length)
|
|
1450
|
+
? last.detail.allIds.filter(Boolean)
|
|
1451
|
+
: (last?.detail?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
1402
1452
|
// The query the ENGINE parses: a "what about X" continuation is rewritten to the
|
|
1403
1453
|
// prior shape with X swapped in; everything else parses verbatim. The record and
|
|
1404
1454
|
// transcript keep the user's ACTUAL words (`query`), only the parse target changes.
|
|
@@ -1438,8 +1488,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1438
1488
|
let resolvedIds = [];
|
|
1439
1489
|
let newFocus = focus;
|
|
1440
1490
|
if (graph && envelope?.parsed?.object) {
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1491
|
+
const obj = envelope.parsed.object;
|
|
1492
|
+
// A PRONOUN object ("it"/"this") was already resolved against the focus via
|
|
1493
|
+
// contextId — the resolved antecedent IS the focus. Re-resolving the literal
|
|
1494
|
+
// pronoun string is the CHATBENCH_0.7.1 B1-pron bug: "it" substring-matches the
|
|
1495
|
+
// "Commit" schema node (label contains "it"), so the focus jumped off the module
|
|
1496
|
+
// to a Commit and the NEXT "it" bound wrong. Reuse the focus directly instead.
|
|
1497
|
+
const ent = (isPronoun(obj) && focus?.id) ? focus : await resolveEntity(graph, obj);
|
|
1498
|
+
if (ent) {
|
|
1499
|
+
resolvedIds = [ent.id];
|
|
1500
|
+
// Class-gate the focus update: a Commit/Session/schema object never displaces a
|
|
1501
|
+
// standing code-entity focus (see nextFocus).
|
|
1502
|
+
newFocus = nextFocus(graph, focus, ent);
|
|
1503
|
+
}
|
|
1443
1504
|
}
|
|
1444
1505
|
const answeredIds = (envelope?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
1445
1506
|
const miss = envelope ? !!envelope.miss : true;
|
|
@@ -1511,12 +1572,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1511
1572
|
// Runs after the corpus-fact/curated branches (via composed|corpus/seon) but not
|
|
1512
1573
|
// over a "you told me" fact, a meta/self summary, or the conversational lanes.
|
|
1513
1574
|
let conceptInstances = null;
|
|
1575
|
+
let conceptAllIds = null;
|
|
1514
1576
|
let conceptPending = null;
|
|
1515
1577
|
if (via === "composed" || via === "corpus/seon") {
|
|
1516
1578
|
const concept = await conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates });
|
|
1517
1579
|
if (concept) {
|
|
1518
1580
|
answer = concept.text; via = "corpus/seon"; recordMiss = false;
|
|
1519
1581
|
conceptInstances = concept.instances;
|
|
1582
|
+
conceptAllIds = concept.allIds;
|
|
1520
1583
|
conceptPending = concept.pending;
|
|
1521
1584
|
} else {
|
|
1522
1585
|
// THE RELATION CONCEPT FORCE — the noun force declined, so try the edge-kind
|
|
@@ -1574,8 +1637,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1574
1637
|
// holds it on conceptPending (the relation force resolves no instance ids, so it can
|
|
1575
1638
|
// still page even with an empty matches set); a fact listing holds it on factPending.
|
|
1576
1639
|
const pending = conceptPending ?? factPending;
|
|
1640
|
+
// `allIds` carries the FULL result-set ids (uncapped) so discourse-count anaphora
|
|
1641
|
+
// ("count them" / "how many of those") survives a truncated render: the concept
|
|
1642
|
+
// force shows only MAX_EXAMPLES instances on `matches`, but a follow-up count must
|
|
1643
|
+
// see them all. The ordinary ask-engine listing already carries its whole set on
|
|
1644
|
+
// `matches`, so only the concept force needs the extra field (a full id array).
|
|
1645
|
+
const allIds = conceptAllIds && conceptAllIds.length ? conceptAllIds : null;
|
|
1577
1646
|
const detail = conceptInstances
|
|
1578
|
-
? { traversal: envelope?.traversal || null, matches: conceptInstances, ...(pending ? { pending } : {}) }
|
|
1647
|
+
? { traversal: envelope?.traversal || null, matches: conceptInstances, ...(allIds ? { allIds } : {}), ...(pending ? { pending } : {}) }
|
|
1579
1648
|
: (envelope
|
|
1580
1649
|
? { traversal: envelope.traversal || null, matches: envelope.matches || [], ...(pending ? { pending } : {}) }
|
|
1581
1650
|
: (pending ? { traversal: null, matches: [], pending } : null));
|
|
@@ -1653,7 +1722,10 @@ async function runCommand(line, { config, source, graph, focus, memoryDir }) {
|
|
|
1653
1722
|
// follow-up ("what calls it", a no-arg /context) reuses it.
|
|
1654
1723
|
if (entityArg) {
|
|
1655
1724
|
const ent = await resolveEntity(graph, value);
|
|
1656
|
-
|
|
1725
|
+
// Same class-gate as the ask path (nextFocus): a command whose arg resolves to a
|
|
1726
|
+
// Commit/Session/schema node records the resolution but does not displace a
|
|
1727
|
+
// standing code-entity focus that "it" is meant to keep binding to.
|
|
1728
|
+
if (ent) return mk(answer, { resolvedIds: [ent.id], newFocus: nextFocus(graph, focus, ent) });
|
|
1657
1729
|
}
|
|
1658
1730
|
return mk(answer);
|
|
1659
1731
|
}
|