@polycode-projects/the-mechanical-code-talker 0.7.0 → 0.8.0
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 +113 -11
- package/bin/tmct.mjs +45 -0
- package/package.json +3 -1
- package/src/ask-vocab.mjs +1 -1
- package/src/ask.mjs +94 -2
- package/src/chat.mjs +86 -19
- 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 +28 -1
- 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/guardrail.mjs +120 -0
- package/src/router/planner.mjs +168 -0
- package/src/router/registry.mjs +271 -0
- package/src/router/resolver.mjs +293 -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,37 @@ 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.0 — Phase 11 landed (Phases 0–10 shipped in 0.7.1).** All five Phase-11 tracks are
|
|
18
|
+
built, merged, and measured; `npm test` green (**869**). Push/publish held for the operator. Highlights:
|
|
19
|
+
|
|
20
|
+
- **Track 1 — chat levers (measured).** Pronoun/focus binding, discourse-count anaphora, and C1
|
|
21
|
+
temporal-over-relative composition all landed. `CHATBENCH_0.8.0`: tier-1 spine **331→333**, all three
|
|
22
|
+
lever families moved their cells (the B1-temporal control did not spill); judged fixed tag
|
|
23
|
+
`multi-turn-focus` **1.433→1.9** (the pronoun lever's echo). The judged pooled mean (1.44) is **not
|
|
24
|
+
case-comparable** to 0.7.1's 1.488 — the graded draw re-samples 10% of cells each run, so §1 compares
|
|
25
|
+
cell-level, not the scalar; the deterministic tier-1 spine is the load-bearing PASS.
|
|
26
|
+
- **Track 2 — the router (designed → DEMONSTRATED).** The Anthropic-compatible `/v1/messages` shim
|
|
27
|
+
(`serve` mode), the Stage-0 capability registry (STRIPS/PDDL operators, closed-world default-deny),
|
|
28
|
+
and the resolver (S1) + guardrail (S4) + planner (S3, pure-JS POP/HTN + Steel & Ho monitor). Measured:
|
|
29
|
+
`AGENTBENCH_0.8.0` (shim-transport floor, 46% completion) → `AGENTBENCH_0.8.0_001` (the real router):
|
|
30
|
+
**96% completion at 0% hallucination on every rung**, clearing the closed-world ladder to **C1**. The
|
|
31
|
+
honest gate the phase was staked on is **met on its hallucination axis (0%)**. **Caveat (stated in the
|
|
32
|
+
artifact):** AGENTBENCH grades the correct **call-plan + proof**, *not* the executed composed result,
|
|
33
|
+
and B1/B2/C1 are thin rungs (2–3 cases); the C2 case is honestly **refused** (Stage 5, unbuilt).
|
|
34
|
+
- **Track 3 — bedrock-meter surface.** `../bedrock-meter` gained a cost-ascending router ladder with the
|
|
35
|
+
**tmct `$0` rung at rank 0 below nova-micro**; an end-to-end test meters an in-envelope request at
|
|
36
|
+
**£0** and escalates out-of-envelope to nova-micro (that repo: 73 green, unpushed).
|
|
37
|
+
- **Track 4 — playtest.** Three frozen dialogue-flow transcripts (`test/chatflow-{coverage,history,
|
|
38
|
+
architecture}.test.mjs`) from routing dead-ends off the grammar wall.
|
|
39
|
+
- **Track 5 — research.** `docs/references/planning/` gained Stage-2 intent-frames + Stage-5
|
|
40
|
+
goal-reasoner design notes + a BDI/Goal-Driven-Autonomy reference entry.
|
|
41
|
+
|
|
42
|
+
**Next:** the C2 goal-reasoner (Stage 5) and Stage-2 imperative intent frames are the designed-but-
|
|
43
|
+
unbuilt edge; the tier-4/tier-5 items in Phase LATER remain the long tail. See Phase 11 below and
|
|
44
|
+
`HANDOVER.md` for the open follow-ups.
|
|
45
|
+
|
|
15
46
|
## The umbrella product definition (item 1)
|
|
16
47
|
|
|
17
48
|
**A tolerant, ELIZA/PARRY-style chat, obsessed with software.** A best-efforts
|
|
@@ -279,11 +310,13 @@ and the block-index summary (blocks, tokens, top PageRank blocks). Same renderer
|
|
|
279
310
|
|
|
280
311
|
> **STATUS: in progress — the two headline B1 levers shipped.** Negation as a bounded SET
|
|
281
312
|
> 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
|
|
313
|
+
> are live; the harness meta-fixes and the rest of the ranked lever board (levers 3–6 + the C2
|
|
314
|
+
> ceiling, below) continue. **This section is the durable home for the tuning arc** — the detailed
|
|
315
|
+
> cycle-4 plan was archived to `archive/PLAN_CYCLE_4.md` once its substance lived here.
|
|
283
316
|
|
|
284
|
-
>
|
|
285
|
-
> (
|
|
286
|
-
>
|
|
317
|
+
> The dependency audit that also fed this phase is archived at `archive/PLAN_DEPENDENCY_STRATEGY.md`
|
|
318
|
+
> (verdict: no dep changes now; a standing adoption register + avoid-list for phases 6-9; two
|
|
319
|
+
> near-term actions, both shipped — see below).
|
|
287
320
|
|
|
288
321
|
The immediate work: drive the graded benchmark up the CEFR ladder, one lever per cycle, per
|
|
289
322
|
`SKILL_TUNING_CYCLE.md`. Cycle 3 (post-wiring-wave, CHATBENCH_003) gave the first full-spectrum
|
|
@@ -312,7 +345,7 @@ from meta-1's harness artifact), **C2 ceiling LAST**. Operator decision 2026-07-
|
|
|
312
345
|
them**. Exit criterion in the plan (roughly: B1 grade mean ≥ ~1.5 with all cells dual-draw
|
|
313
346
|
agreeing unlocks C-grade judging).
|
|
314
347
|
|
|
315
|
-
## Near-term actions (from the dependency audit, PLAN_DEPENDENCY_STRATEGY.md)
|
|
348
|
+
## Near-term actions (from the dependency audit, archive/PLAN_DEPENDENCY_STRATEGY.md)
|
|
316
349
|
|
|
317
350
|
> **STATUS: both shipped.** The wink browser-loader seam is added (shared model loader with a
|
|
318
351
|
> browser registration path) and `fnv1a` is single-sourced into `src/hash.mjs`.
|
|
@@ -322,7 +355,7 @@ Two concrete, low-risk actions the audit surfaced — not features, not dep chan
|
|
|
322
355
|
1. **Fix wink's `createRequire` browser-loader gap** — the wink model IS the browser build, but
|
|
323
356
|
our adapters load it via `createRequire(import.meta.url)` (`ask-nlp.mjs:29`, `prose-nlp.mjs:31`),
|
|
324
357
|
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
|
|
358
|
+
blocker** — budget it into `archive/PLAN_REPOSITORY_INTERFACE.md`; it is a wiring fix, not a dependency
|
|
326
359
|
change (the model is already browser-capable).
|
|
327
360
|
2. **Single-source `fnv1a`** — extract the content-address hash to one `src/hash.mjs` so the
|
|
328
361
|
cross-version-stable fact-id contract has a single definition. Trivial refactor, do any time;
|
|
@@ -337,7 +370,7 @@ Two concrete, low-risk actions the audit surfaced — not features, not dep chan
|
|
|
337
370
|
> `/memory` inspector surfaces contradictions with provenance. Legacy `mgx:factProvenance` kept
|
|
338
371
|
> as a compat shim.
|
|
339
372
|
|
|
340
|
-
> Detailed plan: **PLAN_PROVENANCE_TRUST.md**.
|
|
373
|
+
> Detailed plan: **archive/PLAN_PROVENANCE_TRUST.md**.
|
|
341
374
|
|
|
342
375
|
*(Operator-specified 2026-07-05, from the observation that Phase-6 canonicalise-and-link,
|
|
343
376
|
tier-4 learn-on-miss, and the ConceptNet slice all share one shape: raw source preserved,
|
|
@@ -370,7 +403,7 @@ fact enters memory:
|
|
|
370
403
|
> **STATUS: shipped.** A technical (C1) register of templates and productive/performance
|
|
371
404
|
> dual-banding (computed from the `via` provenance) are live in the benchmark.
|
|
372
405
|
|
|
373
|
-
> Detailed plan: **`PLAN_FORMULAIC_COMPETENCE.md`**.
|
|
406
|
+
> Detailed plan: **`archive/PLAN_FORMULAIC_COMPETENCE.md`**.
|
|
374
407
|
|
|
375
408
|
The operator's insight upgraded to the strategy: a consistently-failed C1/C2 graded cell whose
|
|
376
409
|
answer EXISTS as a stable phrasing in technical prose is not a ceiling — it is a
|
|
@@ -403,7 +436,7 @@ answer EXISTS as a stable phrasing in technical prose is not a ceiling — it is
|
|
|
403
436
|
> pass runs on prose spans only under a protected-span invariance guard. The a/an article fix is
|
|
404
437
|
> active; broader voice/agreement rules are implemented-but-parked.
|
|
405
438
|
|
|
406
|
-
> Detailed plan: **`PLAN_RESPONSE_FINISHING.md`**.
|
|
439
|
+
> Detailed plan: **`archive/PLAN_RESPONSE_FINISHING.md`**.
|
|
407
440
|
|
|
408
441
|
*(Refined 2026-07-05; decisions settled with the operator. Fact invariance is achieved by
|
|
409
442
|
CONSTRUCTION, not by hope. Finishing operates over a SEGMENTED answer, never a raw string.
|
|
@@ -440,7 +473,7 @@ ever emerge.)*
|
|
|
440
473
|
> runnable conformance/compatibility suite; and `tmct init` (scaffold `.tmct/`, `tmct.toml`,
|
|
441
474
|
> tier-1 seed, provenance).
|
|
442
475
|
|
|
443
|
-
> Detailed plan: **`PLAN_REPOSITORY_INTERFACE.md`**.
|
|
476
|
+
> Detailed plan: **`archive/PLAN_REPOSITORY_INTERFACE.md`**.
|
|
444
477
|
|
|
445
478
|
*(Operator-specified 2026-07-05; upgraded from research item to a build phase. tmct was spun OUT
|
|
446
479
|
of seonix; this inverts the relationship: seonix reorients as a USER that imports the tmct
|
|
@@ -512,7 +545,7 @@ a passive payload loader into the product's primary integration surface.)*
|
|
|
512
545
|
before it — PROACTIVELY extending memory with inferences that will be useful later, forward and
|
|
513
546
|
backward chaining over the OWL base during idle/fold time rather than at query time.)*
|
|
514
547
|
|
|
515
|
-
> Detailed plan: **`PLAN_SPECULATIVE_INFERENCE.md`**.
|
|
548
|
+
> Detailed plan: **`archive/PLAN_SPECULATIVE_INFERENCE.md`**.
|
|
516
549
|
|
|
517
550
|
**A maintenance job, not a query-time cost.** Speculative inference runs as an explicit
|
|
518
551
|
batch — `npx tmct syllogise --depth <N>` (default depth bounded, e.g. 32) — and **once
|
|
@@ -595,6 +628,75 @@ you toward precision" promise on the conversational surface.
|
|
|
595
628
|
with new graded cells for the miss / empty-graph / concept-touch surfaces so these become
|
|
596
629
|
regression-protected levers, not one-off polish.
|
|
597
630
|
|
|
631
|
+
## Phase 11 — The capability router & the agentic bench (0.8.0 — LANDED)
|
|
632
|
+
|
|
633
|
+
*(Operator-directed 2026-07-06; built the same day across five concurrent tracks.)* tmct as a **deterministic, no-LLM
|
|
634
|
+
tool router** behind an Anthropic-compatible API — the workstream specified in
|
|
635
|
+
`PLAN_CAPABILITY_ROUTER.md`, grounded in `docs/references/planning/`. This is a **new capability on a
|
|
636
|
+
new axis** (driving a tool loop, not answering a chat turn), so it gets its **own benchmark**:
|
|
637
|
+
**`AGENTBENCH`**, a sibling to CHATBENCH — same versioned-naming + grading discipline
|
|
638
|
+
(`AGENTBENCH_<version>.md`, `_00N` for re-runs), but the levels are the **A0→C2 agentic rungs** and a
|
|
639
|
+
**hallucinated tool call is an automatic fail**.
|
|
640
|
+
|
|
641
|
+
**Status: DEMONSTRATED (with a stated scope caveat).** The router is built and measured:
|
|
642
|
+
`AGENTBENCH_0.8.0_001` = **96% completion at 0% hallucination on every rung**, closed-world ladder
|
|
643
|
+
cleared to **C1**. The gate the phase was staked on — a **0% hallucination rate on a real domain** (the
|
|
644
|
+
graph-query toolset over the fixture) — **is met.** The honest scope line, held from the start:
|
|
645
|
+
AGENTBENCH grades the correct **call-plan + causal-link proof, not the executed composed result**; the
|
|
646
|
+
B1/B2/C1 rungs are **thin (2–3 cases)**; and the one C2 case is **refused** (the Stage-5 goal-reasoner
|
|
647
|
+
is designed, not built). So "closed-world C1" means *the router provably selects and binds the right
|
|
648
|
+
tool sequence*, not *end-to-end multi-step reasoning* — the demonstrated-vs-designed boundary is the
|
|
649
|
+
real deliverable. The five tracks below are all built; the two research-agent stages (Track 4 below)
|
|
650
|
+
remain designed-not-built by intent.
|
|
651
|
+
|
|
652
|
+
### Track 1 — chat-surface levers (next CHATBENCH; all three)
|
|
653
|
+
The three levers `CHATBENCH_0.7.1` measured + ranked — which **double as router prerequisites** (they
|
|
654
|
+
gate the A2→B1→C1 rungs, per Phase B of the router plan):
|
|
655
|
+
1. **Pronoun / focus binding** — the "it → Commit" mis-bind (`B1 pron 1.24`); biggest movable mass.
|
|
656
|
+
2. **Discourse-count anaphora** — "count them / how many of those" over a prior listing (clears the 2
|
|
657
|
+
`CHATBENCH_0.7.1` tier-1 misses).
|
|
658
|
+
3. **C1 temporal-over-relative composition** — the two-hop ceiling (`C1 temp 0.31`).
|
|
659
|
+
Land all three (not just #1); they raise the chat floor *and* the router's floor at once.
|
|
660
|
+
|
|
661
|
+
### Track 2 — the router build (the within-horizon slice, in order)
|
|
662
|
+
Buildable now with a frontier model as co-author (see PLAN §"solved vs unsolved"):
|
|
663
|
+
- **Phase A — the shim.** An Anthropic Messages API endpoint (`/v1/messages`, `tool_use`/`tool_result`
|
|
664
|
+
blocks). **Extended:** also present as a **`bedrock-meter`-compatible routing target** (see below).
|
|
665
|
+
- **Phase B — measure today → `AGENTBENCH_0.7.2.md`.** Shim + a small graph-query toolset up the
|
|
666
|
+
A0→C2 ladder; the honest baseline (expected A0 solid, A1–A2 partial, per the CHATBENCH_0.7.1
|
|
667
|
+
inherited assets).
|
|
668
|
+
- **Phase C — the grading ladder.** The AGENTBENCH benchmark itself (rungs as levels, comparable
|
|
669
|
+
local/hosted models as reference bands, zero-hallucination gate).
|
|
670
|
+
- **Stage 0 — capability registry** (`Capability`/`Parameter`/`Precondition`/`Effect` = STRIPS/PDDL
|
|
671
|
+
operators as facts).
|
|
672
|
+
- **Stage 1 — the resolver** (unification + backward chaining / a mini Datalog).
|
|
673
|
+
- **Stage 4 — the guardrail** (validate an LLM's proposed `tool_use` against declared preconditions —
|
|
674
|
+
the hybrid fast-path; cheap once 0–1 exist).
|
|
675
|
+
- **Stage 3 — the planner** (POP/HTN over operators + Steel & Ho monitor-and-replan → **closed-world
|
|
676
|
+
C1**; optionally defer search to an external PDDL solver).
|
|
677
|
+
|
|
678
|
+
### Phase A extension — the `bedrock-meter` deployment surface
|
|
679
|
+
`../bedrock-meter` is pre-flight Bedrock cost metering + capping, with a **roadmap optimiser** that
|
|
680
|
+
"cheaply assesses a task's complexity … and routes to the lowest-cost capable model" (it already
|
|
681
|
+
meters Nova Lite + Nova Micro). tmct — **benchmarked against agent capabilities by AGENTBENCH** — slots
|
|
682
|
+
in as the **$0 floor *below* Nova-micro** in that routing ladder: for a request class AGENTBENCH proves
|
|
683
|
+
in-envelope, the optimiser routes to tmct (deterministic, ~$0, ms latency) instead of any metered
|
|
684
|
+
model. So Phase A's shim is built **bedrock-meter-pluggable**, and AGENTBENCH is what defines the
|
|
685
|
+
envelope the optimiser is allowed to trust. This is the concrete "near-free alternative" deployment.
|
|
686
|
+
|
|
687
|
+
### Track 3 — playtest alongside the build (`git worktree`)
|
|
688
|
+
Run `SKILL_CHAT_PLAYTEST.md` **in a parallel `git worktree`** while the router is built — the
|
|
689
|
+
dialogue-flow dead-end hunt keeps running without blocking the build, and its fixes **merge back**.
|
|
690
|
+
(The worktree is auto-cleaned if unchanged; merge the frozen `test/chatflow-*` transcripts in.)
|
|
691
|
+
|
|
692
|
+
### Track 4 — research agents (the "at the edge" stages)
|
|
693
|
+
Two stages need design judgment + exploration, so they run as **background research agents**, off the
|
|
694
|
+
critical build path:
|
|
695
|
+
- **Stage 2 — intent frames, controlled fragment** — imperative NL → structured intent for the
|
|
696
|
+
controlled command language (the front-end; the general case stays out-of-scope / escalate).
|
|
697
|
+
- **Stage 5 — goal-reasoner, closed-world C2** — BDI + Goal-Driven Autonomy: deduce-goals (long-chain
|
|
698
|
+
deduction) → plan-each (C1) → threat-aware, *persistent* first-step arbitration.
|
|
699
|
+
|
|
598
700
|
## Phase LATER — recognized, deferred, not now
|
|
599
701
|
|
|
600
702
|
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.0",
|
|
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: [] };
|
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}`;
|
|
@@ -1264,25 +1296,33 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
|
|
|
1264
1296
|
* the graph parser reads as a count. Null when the line isn't such a touch. The
|
|
1265
1297
|
* concept force is gated further downstream (a KNOWN, instance-bearing concept), so
|
|
1266
1298
|
* this only has to recognize the SHAPE, not vet the term. */
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1299
|
+
/** The VAGUE-TOUCH shapes ("tell me about X", "[and] what about X") — a concept
|
|
1300
|
+
* touch that is NOT the "what is a X" / "what does X mean" META shape. The meta shape
|
|
1301
|
+
* has its own established handling (a noun definition, a predicate definition, or the
|
|
1302
|
+
* honest ambiguity surround for a term that is BOTH a noun and a predicate — e.g.
|
|
1303
|
+
* "imports"), which the RELATION force must never preempt (frozen case
|
|
1304
|
+
* am-meta-imports). Gated downstream by CONCEPT_CLASS / RELATION_TERM, so a real
|
|
1305
|
+
* entity name declines here. */
|
|
1306
|
+
function vagueTouchTermOf(query) {
|
|
1270
1307
|
const q = String(query).trim();
|
|
1271
1308
|
const m = q.match(/^tell me about\s+(?:an?\s+)?(.+?)[?.!\s]*$/i)
|
|
1272
|
-
// "[and/so/…] what about X" with no good discourse continuation — the concept
|
|
1273
|
-
// KIND word is a concept touch, not a bare module lookup (DEAD-END 4). Gated
|
|
1274
|
-
// downstream by CONCEPT_CLASS/RELATION_TERM, so a real entity name declines here.
|
|
1275
1309
|
|| q.match(/^(?:(?:and|so|but|ok|okay|now|then)\s+)*what about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i);
|
|
1276
1310
|
return m ? m[1].trim() : null;
|
|
1277
1311
|
}
|
|
1278
1312
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1313
|
+
function conceptTermOf(query, envelope) {
|
|
1314
|
+
return metaTermOf(query, envelope) || vagueTouchTermOf(query);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** The RELATION term a vague touch names — the VAGUE-touch shapes only ("tell me
|
|
1318
|
+
* about X", "what about X"), NOT the "what is a X"/"what does X mean" meta shape (a
|
|
1319
|
+
* relation term that is also a vocabulary word, like "imports", keeps its established
|
|
1320
|
+
* ambiguity/predicate-definition answer — frozen case am-meta-imports). Plus the
|
|
1321
|
+
* relation-only openers the graph parser reads as something else: "what are the
|
|
1322
|
+
* imports", "what calls are there", "what is calling". Null when the line isn't such a
|
|
1323
|
+
* touch. Gated downstream by RELATION_TERM, so this only has to recognize the SHAPE. */
|
|
1284
1324
|
function relationTermOf(query, envelope) {
|
|
1285
|
-
const base =
|
|
1325
|
+
const base = vagueTouchTermOf(query);
|
|
1286
1326
|
if (base) return base;
|
|
1287
1327
|
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
1288
1328
|
let m;
|
|
@@ -1374,7 +1414,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
1374
1414
|
const pending = composed.remainder && composed.remainder.length
|
|
1375
1415
|
? { items: composed.remainder, noun: composed.noun }
|
|
1376
1416
|
: null;
|
|
1377
|
-
return { text, instances: composed.instances, pending };
|
|
1417
|
+
return { text, instances: composed.instances, allIds: composed.allInstanceIds, pending };
|
|
1378
1418
|
}
|
|
1379
1419
|
|
|
1380
1420
|
/** A bare question → tmct_ask. When a focus is set AND the graph is in hand we
|
|
@@ -1390,7 +1430,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1390
1430
|
// answer's entity set. That set is the ids the last dispatched turn cited — carried
|
|
1391
1431
|
// on `last.detail.matches`. Threading it as ask()'s `prev` is what lets the anaphora
|
|
1392
1432
|
// node resolve instead of the "needs a previous answer" honest miss.
|
|
1393
|
-
|
|
1433
|
+
// Prefer the FULL id set (`allIds`) when the last turn carried one — a concept-force
|
|
1434
|
+
// listing caps its shown `matches` at MAX_EXAMPLES, so counting `matches` alone would
|
|
1435
|
+
// undercount "count them" over a truncated listing (CHATBENCH_0.7.1 discourse-count).
|
|
1436
|
+
const prev = (last?.detail?.allIds && last.detail.allIds.length)
|
|
1437
|
+
? last.detail.allIds.filter(Boolean)
|
|
1438
|
+
: (last?.detail?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
1394
1439
|
// The query the ENGINE parses: a "what about X" continuation is rewritten to the
|
|
1395
1440
|
// prior shape with X swapped in; everything else parses verbatim. The record and
|
|
1396
1441
|
// transcript keep the user's ACTUAL words (`query`), only the parse target changes.
|
|
@@ -1430,8 +1475,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1430
1475
|
let resolvedIds = [];
|
|
1431
1476
|
let newFocus = focus;
|
|
1432
1477
|
if (graph && envelope?.parsed?.object) {
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1478
|
+
const obj = envelope.parsed.object;
|
|
1479
|
+
// A PRONOUN object ("it"/"this") was already resolved against the focus via
|
|
1480
|
+
// contextId — the resolved antecedent IS the focus. Re-resolving the literal
|
|
1481
|
+
// pronoun string is the CHATBENCH_0.7.1 B1-pron bug: "it" substring-matches the
|
|
1482
|
+
// "Commit" schema node (label contains "it"), so the focus jumped off the module
|
|
1483
|
+
// to a Commit and the NEXT "it" bound wrong. Reuse the focus directly instead.
|
|
1484
|
+
const ent = (isPronoun(obj) && focus?.id) ? focus : await resolveEntity(graph, obj);
|
|
1485
|
+
if (ent) {
|
|
1486
|
+
resolvedIds = [ent.id];
|
|
1487
|
+
// Class-gate the focus update: a Commit/Session/schema object never displaces a
|
|
1488
|
+
// standing code-entity focus (see nextFocus).
|
|
1489
|
+
newFocus = nextFocus(graph, focus, ent);
|
|
1490
|
+
}
|
|
1435
1491
|
}
|
|
1436
1492
|
const answeredIds = (envelope?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
1437
1493
|
const miss = envelope ? !!envelope.miss : true;
|
|
@@ -1503,12 +1559,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1503
1559
|
// Runs after the corpus-fact/curated branches (via composed|corpus/seon) but not
|
|
1504
1560
|
// over a "you told me" fact, a meta/self summary, or the conversational lanes.
|
|
1505
1561
|
let conceptInstances = null;
|
|
1562
|
+
let conceptAllIds = null;
|
|
1506
1563
|
let conceptPending = null;
|
|
1507
1564
|
if (via === "composed" || via === "corpus/seon") {
|
|
1508
1565
|
const concept = await conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates });
|
|
1509
1566
|
if (concept) {
|
|
1510
1567
|
answer = concept.text; via = "corpus/seon"; recordMiss = false;
|
|
1511
1568
|
conceptInstances = concept.instances;
|
|
1569
|
+
conceptAllIds = concept.allIds;
|
|
1512
1570
|
conceptPending = concept.pending;
|
|
1513
1571
|
} else {
|
|
1514
1572
|
// THE RELATION CONCEPT FORCE — the noun force declined, so try the edge-kind
|
|
@@ -1566,8 +1624,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1566
1624
|
// holds it on conceptPending (the relation force resolves no instance ids, so it can
|
|
1567
1625
|
// still page even with an empty matches set); a fact listing holds it on factPending.
|
|
1568
1626
|
const pending = conceptPending ?? factPending;
|
|
1627
|
+
// `allIds` carries the FULL result-set ids (uncapped) so discourse-count anaphora
|
|
1628
|
+
// ("count them" / "how many of those") survives a truncated render: the concept
|
|
1629
|
+
// force shows only MAX_EXAMPLES instances on `matches`, but a follow-up count must
|
|
1630
|
+
// see them all. The ordinary ask-engine listing already carries its whole set on
|
|
1631
|
+
// `matches`, so only the concept force needs the extra field (a full id array).
|
|
1632
|
+
const allIds = conceptAllIds && conceptAllIds.length ? conceptAllIds : null;
|
|
1569
1633
|
const detail = conceptInstances
|
|
1570
|
-
? { traversal: envelope?.traversal || null, matches: conceptInstances, ...(pending ? { pending } : {}) }
|
|
1634
|
+
? { traversal: envelope?.traversal || null, matches: conceptInstances, ...(allIds ? { allIds } : {}), ...(pending ? { pending } : {}) }
|
|
1571
1635
|
: (envelope
|
|
1572
1636
|
? { traversal: envelope.traversal || null, matches: envelope.matches || [], ...(pending ? { pending } : {}) }
|
|
1573
1637
|
: (pending ? { traversal: null, matches: [], pending } : null));
|
|
@@ -1645,7 +1709,10 @@ async function runCommand(line, { config, source, graph, focus, memoryDir }) {
|
|
|
1645
1709
|
// follow-up ("what calls it", a no-arg /context) reuses it.
|
|
1646
1710
|
if (entityArg) {
|
|
1647
1711
|
const ent = await resolveEntity(graph, value);
|
|
1648
|
-
|
|
1712
|
+
// Same class-gate as the ask path (nextFocus): a command whose arg resolves to a
|
|
1713
|
+
// Commit/Session/schema node records the resolution but does not displace a
|
|
1714
|
+
// standing code-entity focus that "it" is meant to keep binding to.
|
|
1715
|
+
if (ent) return mk(answer, { resolvedIds: [ent.id], newFocus: nextFocus(graph, focus, ent) });
|
|
1649
1716
|
}
|
|
1650
1717
|
return mk(answer);
|
|
1651
1718
|
}
|
package/src/concept.mjs
CHANGED
|
@@ -213,6 +213,11 @@ export function composeConcept(graph, term, { definition = null, factRows = [] }
|
|
|
213
213
|
instances: graphInstances.slice(0, MAX_EXAMPLES).map((i) => ({
|
|
214
214
|
id: i.id, label: i.label, type: i.class,
|
|
215
215
|
})),
|
|
216
|
+
// EVERY instance id (uncapped), for discourse-count anaphora ("count them" /
|
|
217
|
+
// "how many of those") over a truncated listing: `instances` is capped at
|
|
218
|
+
// MAX_EXAMPLES and the rest lives in `remainder` as prose labels, invisible to
|
|
219
|
+
// the counter — so a follow-up count would undercount without the full id set.
|
|
220
|
+
allInstanceIds: graphInstances.map((i) => i.id),
|
|
216
221
|
// the un-shown instance labels + their plural noun, for the shell's "more"
|
|
217
222
|
// pagination — empty when nothing was truncated.
|
|
218
223
|
remainder: remainderLabels,
|
package/src/conformance.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// conformance.mjs — the Repository-Interface CONTRACT TEST SUITE as a reusable kit.
|
|
2
2
|
//
|
|
3
|
-
// PLAN_REPOSITORY_INTERFACE.md deliverable 3: an implementation is CONFORMANT iff it
|
|
3
|
+
// archive/PLAN_REPOSITORY_INTERFACE.md deliverable 3: an implementation is CONFORMANT iff it
|
|
4
4
|
// passes `runConformance(name, makeProvider)`. tmct's own fixture + bootstrap providers
|
|
5
5
|
// pass it in `npm test`; an EXTERNAL producer (seonix) imports this kit from the
|
|
6
6
|
// published package and runs the SAME suite against its native provider to claim
|