@polycode-projects/the-mechanical-code-talker 2.9.6 → 2.10.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/README.md +42 -4
- package/bin/tmct.mjs +16 -1
- package/package.json +6 -1
- package/src/adapters/memory/export-jsonl.mjs +38 -0
- package/src/domain/ask.mjs +1 -1
- package/src/domain/cli-verbs.mjs +2 -1
- package/src/domain/code-explorer-hints.mjs +176 -0
- package/src/services/adventure-viz.mjs +29 -21
- package/src/services/chat-page-viz.mjs +39 -1
- package/src/services/chat.mjs +217 -8
- package/src/services/code-explorer-viz.mjs +343 -0
- package/src/services/import-file.mjs +69 -7
- package/src/services/ledger-viz.mjs +6 -1
- package/src/services/spider-fly-viz.mjs +59 -3
- package/src/services/spider-fly.mjs +5 -2
- package/src/surfaces/web/chat-browser-entry.mjs +12 -1
- package/src/surfaces/web/code-explorer-browser-entry.mjs +79 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +94 -92
- package/src/tools/definitions.mjs +7 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-export.mjs +26 -0
package/README.md
CHANGED
|
@@ -157,6 +157,32 @@ npx tmct chat --prompt 'disk-1 rests on disk-2. disk-2 rests on disk-3.
|
|
|
157
157
|
|
|
158
158
|
More on the game file and the planner under "Teach it a game" below.
|
|
159
159
|
|
|
160
|
+
### The code explorer (desktop)
|
|
161
|
+
|
|
162
|
+
The same ledger UI, refocused on a code graph, also runs as a desktop app. It
|
|
163
|
+
reads a `graph.json` (or a repo's `.tmct/` folder), shows every import, call and
|
|
164
|
+
`contains` edge as a plain sentence around a focus symbol, and docks a live chat
|
|
165
|
+
over the same graph. A hint rail suggests the next question from what the graph
|
|
166
|
+
actually holds — "what does X import", "which functions call Y", "list
|
|
167
|
+
functions" — so every suggestion resolves to a real answer.
|
|
168
|
+
|
|
169
|
+
Electron is a dev-only dependency and never ships in the npm package. Because
|
|
170
|
+
`.npmrc` sets `ignore-scripts=true`, installing it does not fetch the runtime
|
|
171
|
+
binary; fetch it once, then build and launch:
|
|
172
|
+
|
|
173
|
+
```bash skip=network
|
|
174
|
+
npm i -D electron
|
|
175
|
+
node node_modules/electron/install.js # fetch the Electron binary (ignore-scripts skips this)
|
|
176
|
+
npm run build:electron # render electron/renderer/ from the demo graph
|
|
177
|
+
npm run electron # open the code explorer on the demo graph
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Open a graph or a repo from the window's title bar to explore your own code.
|
|
181
|
+
The UI is channel-agnostic — only the Electron shell (`electron/main.mjs` +
|
|
182
|
+
`electron/preload.cjs`) is desktop-specific; the same page stays servable as a
|
|
183
|
+
plain web page. `npm run test:electron` runs the shell smoke via Playwright and
|
|
184
|
+
skips cleanly when the binary is absent.
|
|
185
|
+
|
|
160
186
|
## How it interprets you
|
|
161
187
|
|
|
162
188
|
Every message runs through **multiple concurrent interpretation strategies**:
|
|
@@ -498,6 +524,13 @@ nudges you to ground one side first. Quantified teaching stores the
|
|
|
498
524
|
quantifier ("some functions are risky" … "how many functions are risky" →
|
|
499
525
|
"A few."), and "how many facts are there" counts the store back.
|
|
500
526
|
|
|
527
|
+
The store answers about its own contents directly. "list facts" and "list
|
|
528
|
+
utterances" enumerate what it holds; "how many sessions are there", "how many
|
|
529
|
+
sources", and "how many rules" count the store's own book-keeping classes
|
|
530
|
+
rather than the code graph. A taught class answers both shapes too: after
|
|
531
|
+
"dog is a kind of animal", "how many animals are there" counts its members and
|
|
532
|
+
"list all animals" reads them back, each cited to where it came from.
|
|
533
|
+
|
|
501
534
|
Teaching doesn't have to be typed, either. `tmct extract` runs a plain text
|
|
502
535
|
file through the same recognizer the chat's teach lane uses. Sentences the
|
|
503
536
|
recognizer grounds become fact rows; everything else is skipped and counted,
|
|
@@ -616,6 +649,9 @@ Usage:
|
|
|
616
649
|
tmct memory [--repo <abs>] what tmct remembers: facts, utterances, sessions,
|
|
617
650
|
[--config <path>] folded blocks (the /memory chat command, from the shell)
|
|
618
651
|
[--verbose]
|
|
652
|
+
[--export <file.jsonl>] write every stored fact as JSONL (subject/predicate/object/
|
|
653
|
+
provenance) to a file and exit — the shape `tmct extract`
|
|
654
|
+
emits, for audit or backup
|
|
619
655
|
```
|
|
620
656
|
|
|
621
657
|
`tmct init` sets up a repo for the first time: `.tmct/`, `tmct.toml`, a seed, and a
|
|
@@ -657,9 +693,10 @@ already set up. Its `--graph` flag works differently from the others: it appends
|
|
|
657
693
|
[--ontology <name|path>] DIFFERENT operation from the others: it APPENDS to
|
|
658
694
|
[--lexicon <name|path>] tmct.toml's graph_files array (multi-graph growth),
|
|
659
695
|
[--graph <path>] never an extensions-bundle activation.
|
|
660
|
-
[--file <
|
|
661
|
-
sentence (# lines are comments
|
|
662
|
-
|
|
696
|
+
[--file <defs.txt|facts.jsonl>] teach a definition file: a .txt taught sentence by
|
|
697
|
+
sentence (# lines are comments; a declined sentence exits
|
|
698
|
+
non-zero, named), or a .jsonl triple dump loaded fact by
|
|
699
|
+
fact, keeping each line's own provenance
|
|
663
700
|
[--memory-backend <default|memory|sqlite>] same knob as `tmct init`
|
|
664
701
|
[--config <path>]
|
|
665
702
|
```
|
|
@@ -972,7 +1009,7 @@ loads a repo's graph into a `{ dispatch, resolve, graph }` context,
|
|
|
972
1009
|
|
|
973
1010
|
## The tool surface
|
|
974
1011
|
|
|
975
|
-
Everything above runs on the same
|
|
1012
|
+
Everything above runs on the same 24 tools. Each one is read-only, answers one question in a single call, and returns bounded output. None of them calls a model. A tool that cannot ground an answer says so — the same honest miss you get everywhere else in tmct.
|
|
976
1013
|
|
|
977
1014
|
Three of them are **hot**: their schemas stay resident, so an agent driving tmct sees them every turn and reaches for one call instead of a Read/Grep loop.
|
|
978
1015
|
|
|
@@ -1044,6 +1081,7 @@ The remaining tools are **cold**: still served, but not billed to an agent every
|
|
|
1044
1081
|
| `tmct_calls` | The in-repo symbols a function calls (fn→fn), each with file:line. | `symbol` (required) |
|
|
1045
1082
|
| `tmct_cochanges` | Modules that historically change in the same commit as a symbol's module (git co-change). | `symbol` (required) |
|
|
1046
1083
|
| `tmct_context_more` | The bundle sections a lean tmct_context omitted (siblings / tests / cochange / class members / re-exports). | `symbol` (required) |
|
|
1084
|
+
| `tmct_export` | Every stored memory fact as JSONL (subject/predicate/object/provenance) — the shape `tmct extract` emits, for backup or audit. | none |
|
|
1047
1085
|
|
|
1048
1086
|
Add `repo_path` to any of them to point at a repository other than the working directory. `tmct init` also writes this catalog, with a worked invocation per tool, to `.tmct/TOOLS.md` inside the repo it indexed.
|
|
1049
1087
|
|
package/bin/tmct.mjs
CHANGED
|
@@ -729,10 +729,11 @@ async function main() {
|
|
|
729
729
|
// with every other subcommand. No `--graph`: memory reads no code graph.
|
|
730
730
|
const rest = process.argv.slice(3);
|
|
731
731
|
const verbose = rest.includes("--verbose") || rest.includes("-v");
|
|
732
|
-
const { resolveRuntimeConfig } = await import("../src/services/cli-args.mjs");
|
|
732
|
+
const { resolveRuntimeConfig, strFlag } = await import("../src/services/cli-args.mjs");
|
|
733
733
|
const { renderMemory } = await import("../src/adapters/memory/inspect.mjs");
|
|
734
734
|
const { loadMemory, openMemoryBackend } = await import("../src/adapters/memory/core.mjs");
|
|
735
735
|
const { loadBlockIndex } = await import("../src/adapters/memory/blocks.mjs");
|
|
736
|
+
const exportPath = strFlag(rest, ["--export"]);
|
|
736
737
|
const { repo, toml } = await resolveRuntimeConfig({ argv: rest });
|
|
737
738
|
// Same backend resolution as chat's createSession, minus the (nonexistent
|
|
738
739
|
// here) CLI-flag tier: env > tmct.toml > the sqlite default — so this verb
|
|
@@ -741,6 +742,20 @@ async function main() {
|
|
|
741
742
|
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
|
|
742
743
|
try {
|
|
743
744
|
const memory = await loadMemory(memoryDir);
|
|
745
|
+
// `--export <file.jsonl>` dumps every stored fact in the extract shape and
|
|
746
|
+
// exits — a backup/audit trail of the same store the tool layer and the
|
|
747
|
+
// browser pages serialize, one serializer for all three.
|
|
748
|
+
if (exportPath) {
|
|
749
|
+
const { serializeFactsJsonl } = await import("../src/adapters/memory/export-jsonl.mjs");
|
|
750
|
+
const { writeFile } = await import("node:fs/promises");
|
|
751
|
+
const { resolve } = await import("node:path");
|
|
752
|
+
const jsonl = serializeFactsJsonl(memory);
|
|
753
|
+
const out = resolve(process.cwd(), exportPath);
|
|
754
|
+
await writeFile(out, jsonl, "utf8");
|
|
755
|
+
const count = jsonl ? jsonl.trimEnd().split("\n").length : 0;
|
|
756
|
+
process.stderr.write(`wrote ${count} fact${count === 1 ? "" : "s"} to ${exportPath}\n`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
744
759
|
// The folded-block index is file-backed beside the session logs, not part
|
|
745
760
|
// of the memory store — read it off the repo path directly.
|
|
746
761
|
const blocks = await loadBlockIndex(repo);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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.",
|
|
@@ -139,6 +139,10 @@
|
|
|
139
139
|
"build:spider-fly-bundle": "node scripts/build-spider-fly-bundle.mjs",
|
|
140
140
|
"build:plan-bundle": "node scripts/build-plan-bundle.mjs",
|
|
141
141
|
"build:ledger-bundle": "node scripts/build-ledger-bundle.mjs",
|
|
142
|
+
"build:code-explorer-bundle": "node scripts/build-code-explorer-bundle.mjs",
|
|
143
|
+
"build:electron": "node scripts/build-electron-app.mjs",
|
|
144
|
+
"electron": "electron electron/main.mjs",
|
|
145
|
+
"test:electron": "node --test electron/smoke.test.mjs",
|
|
142
146
|
"build:sprites-bundle": "node scripts/build-sprites-bundle.mjs",
|
|
143
147
|
"build:wink-vendor": "node scripts/build-wink-vendor.mjs",
|
|
144
148
|
"build:chat-seed": "node scripts/build-chat-seed.mjs",
|
|
@@ -159,6 +163,7 @@
|
|
|
159
163
|
"extract:facts": "node --disable-warning=ExperimentalWarning bin/tmct.mjs extract"
|
|
160
164
|
},
|
|
161
165
|
"devDependencies": {
|
|
166
|
+
"electron": "^43.2.0",
|
|
162
167
|
"esbuild": "0.28.1",
|
|
163
168
|
"ink-testing-library": "4.0.0",
|
|
164
169
|
"playwright": "1.61.1",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// export-jsonl.mjs — serialize a memory store's Facts to JSONL in the same
|
|
2
|
+
// shape `tmct extract` emits: one JSON object per line, each carrying
|
|
3
|
+
// { subject, predicate, object, provenance } (and `quantifier` when a plural
|
|
4
|
+
// class-membership teach set one). Provenance is the store's own legacy compat
|
|
5
|
+
// string, verbatim — so an exported line names where the fact came from, and a
|
|
6
|
+
// re-import through the recognizer or a fact loader lands it back.
|
|
7
|
+
//
|
|
8
|
+
// Pure: no I/O. The CLI (`tmct memory --export`), the tool layer
|
|
9
|
+
// (`tmct_export`) and the browser pages all read a loaded memory (or its fact
|
|
10
|
+
// rows) and call one of these — one serializer, one shape, everywhere.
|
|
11
|
+
|
|
12
|
+
import { readFactRows } from "./core.mjs";
|
|
13
|
+
|
|
14
|
+
/** One export record from a readFactRows row — the fields a re-import needs,
|
|
15
|
+
* and nothing the store computed internally (id, trust, justification). */
|
|
16
|
+
export function factRowToExportRecord(row) {
|
|
17
|
+
const record = {
|
|
18
|
+
subject: row.subject,
|
|
19
|
+
predicate: row.predicate,
|
|
20
|
+
object: row.object,
|
|
21
|
+
provenance: row.provenance || "",
|
|
22
|
+
};
|
|
23
|
+
if (row.quantifier) record.quantifier = row.quantifier;
|
|
24
|
+
return record;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** JSONL for an array of readFactRows rows — one object per line, a trailing
|
|
28
|
+
* newline when there is at least one row, the empty string for none. */
|
|
29
|
+
export function factRowsToJsonl(rows) {
|
|
30
|
+
const records = (rows || []).map(factRowToExportRecord);
|
|
31
|
+
return records.map((r) => JSON.stringify(r)).join("\n") + (records.length ? "\n" : "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** JSONL for a loaded memory (loadMemory's result, or a Backend-B payload) —
|
|
35
|
+
* every stored Fact, in the extract shape. */
|
|
36
|
+
export function serializeFactsJsonl(memory) {
|
|
37
|
+
return factRowsToJsonl(readFactRows(memory));
|
|
38
|
+
}
|
package/src/domain/ask.mjs
CHANGED
|
@@ -4074,7 +4074,7 @@ const DYNAMIC_LIST_TRIGGER_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([
|
|
|
4074
4074
|
const DYNAMIC_COUNT_TRIGGER_RE = /^(?:how\s+many|number\s+of|count(?:\s+the)?)\s+([a-z][a-z'-]*)\s*(.*)$/i;
|
|
4075
4075
|
// A closed set of harmless trailing fillers; anything else (a real
|
|
4076
4076
|
// restrictor like "that mention X") is left alone.
|
|
4077
|
-
const DYNAMIC_TAIL_OK_RE = /^(?:are there(?:\s+in\s+total)?|is there|do you know(?:\s+about)?|do you have|exist(?:s)?|are known|in (?:the |a )?(?:graph|memory)|you know(?:\s+about)?)?[?.!\s]*$/i;
|
|
4077
|
+
export const DYNAMIC_TAIL_OK_RE = /^(?:are there(?:\s+in\s+total)?|is there|do you know(?:\s+about)?|do you have|exist(?:s)?|are known|in (?:the |a )?(?:graph|memory)|you know(?:\s+about)?)?[?.!\s]*$/i;
|
|
4078
4078
|
|
|
4079
4079
|
/** Compile "list/how many <memory-class-noun>" into the same count/list AST
|
|
4080
4080
|
* every code-graph count/list query already builds, or null when this isn't
|
package/src/domain/cli-verbs.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export const CLI_VERBS = [
|
|
|
47
47
|
flags: [
|
|
48
48
|
{ flag: "[--config <path>]", prose: ["folded blocks (the /memory chat command, from the shell)"] },
|
|
49
49
|
{ flag: "[--verbose]", prose: [] },
|
|
50
|
+
{ flag: "[--export <file.jsonl>]", prose: ["write every stored fact as JSONL (subject/predicate/object/", "provenance) to a file and exit — the shape `tmct extract`", "emits, for audit or backup"] },
|
|
50
51
|
],
|
|
51
52
|
},
|
|
52
53
|
{
|
|
@@ -77,7 +78,7 @@ export const CLI_VERBS = [
|
|
|
77
78
|
{ flag: "[--ontology <name|path>]", prose: ["DIFFERENT operation from the others: it APPENDS to"] },
|
|
78
79
|
{ flag: "[--lexicon <name|path>]", prose: ["tmct.toml's graph_files array (multi-graph growth),"] },
|
|
79
80
|
{ flag: "[--graph <path>]", prose: ["never an extensions-bundle activation."] },
|
|
80
|
-
{ flag: "[--file <
|
|
81
|
+
{ flag: "[--file <defs.txt|facts.jsonl>]", prose: ["teach a definition file: a .txt taught sentence by", "sentence (# lines are comments; a declined sentence exits", "non-zero, named), or a .jsonl triple dump loaded fact by", "fact, keeping each line's own provenance"] },
|
|
81
82
|
{ flag: "[--memory-backend <default|memory|sqlite>]", prose: ["same knob as `tmct init`"] },
|
|
82
83
|
{ flag: "[--config <path>]", prose: [] },
|
|
83
84
|
],
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// code-explorer-hints.mjs — suggested next queries drawn from what a loaded
|
|
2
|
+
// CODE graph actually holds. Pure over the entities payload (individuals +
|
|
3
|
+
// objectProperties), so the desktop shell and any future plain page share one
|
|
4
|
+
// tested generator and neither hand-writes prompt strings.
|
|
5
|
+
//
|
|
6
|
+
// The shapes are exactly the ones ask.mjs already answers, phrased from its own
|
|
7
|
+
// closed vocabulary (ask-vocab.mjs's RELATIONS/EDGE_NOUN_TO_METRIC): a relation
|
|
8
|
+
// only appears in a hint when the graph carries an edge of that kind, and a
|
|
9
|
+
// focus symbol only fills a slot when it actually sits at that end of an edge —
|
|
10
|
+
// so every suggestion resolves to a real answer, never a guess.
|
|
11
|
+
|
|
12
|
+
import { RELATIONS, EDGE_NOUN_TO_METRIC } from "./ask-vocab.mjs";
|
|
13
|
+
|
|
14
|
+
// Node classes this explorer reads as code, with the singular/plural nouns
|
|
15
|
+
// ask.mjs's own list and count lanes accept (ENTITY_TO_TYPE's inverse). Schema
|
|
16
|
+
// documentation individuals (SchemaClass/SchemaPredicate) and anything else are
|
|
17
|
+
// left out — they are not what a reader explores.
|
|
18
|
+
const CODE_CLASS_NOUN = Object.freeze({
|
|
19
|
+
Module: ["module", "modules"],
|
|
20
|
+
Class: ["class", "classes"],
|
|
21
|
+
Function: ["function", "functions"],
|
|
22
|
+
Method: ["method", "methods"],
|
|
23
|
+
Attribute: ["attribute", "attributes"],
|
|
24
|
+
GlobalVariable: ["variable", "variables"],
|
|
25
|
+
Commit: ["commit", "commits"],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// The classes a focus symbol is drawn from — the things a reader centres on.
|
|
29
|
+
const FOCUSABLE_CLASSES = new Set(["Module", "Class", "Function", "Method"]);
|
|
30
|
+
|
|
31
|
+
// Fine, symbol-grain predicates fold onto their coarse sibling for phrasing:
|
|
32
|
+
// "what does X call" covers callsSymbol too, exactly as ask.mjs's
|
|
33
|
+
// SYMBOL_GRAIN_SIBLING pairs them.
|
|
34
|
+
const GRAIN_BASE = Object.freeze({ callsSymbol: "calls", touchesSymbol: "touches" });
|
|
35
|
+
const baseKind = (predicate) => GRAIN_BASE[predicate] || predicate;
|
|
36
|
+
|
|
37
|
+
// Forward reading (focus is the subject): "what does <focus> <bare>". The bare
|
|
38
|
+
// verb form is ask-vocab's own, so the phrasing tracks the vocabulary.
|
|
39
|
+
const FORWARD_TEMPLATE = Object.freeze({
|
|
40
|
+
imports: (f) => `what does ${f} import`,
|
|
41
|
+
calls: (f) => `what does ${f} call`,
|
|
42
|
+
contains: (f) => `what is in ${f}`,
|
|
43
|
+
defines: (f) => `what does ${f} define`,
|
|
44
|
+
inherits: (f) => `what does ${f} inherit from`,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Reverse reading (focus is the object): "what <kind> <focus>".
|
|
48
|
+
const REVERSE_TEMPLATE = Object.freeze({
|
|
49
|
+
imports: (f) => `what imports ${f}`,
|
|
50
|
+
calls: (f) => `what calls ${f}`,
|
|
51
|
+
tests: (f) => `what tests ${f}`,
|
|
52
|
+
inherits: (f) => `what inherits from ${f}`,
|
|
53
|
+
contains: (f) => `what contains ${f}`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Index a payload into the counts and adjacency the hints read: class →
|
|
57
|
+
* count, base relation kind → { count, subjects, objects }, and a label →
|
|
58
|
+
* { class, degree } term table over every example edge. */
|
|
59
|
+
function indexGraph(payload) {
|
|
60
|
+
const individuals = Array.isArray(payload?.individuals) ? payload.individuals : [];
|
|
61
|
+
const classOfLabel = new Map();
|
|
62
|
+
const classCounts = new Map();
|
|
63
|
+
for (const ind of individuals) {
|
|
64
|
+
if (!ind || !ind.label) continue;
|
|
65
|
+
if (CODE_CLASS_NOUN[ind.class]) classCounts.set(ind.class, (classCounts.get(ind.class) || 0) + 1);
|
|
66
|
+
if (!classOfLabel.has(ind.label)) classOfLabel.set(ind.label, ind.class);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const groups = Array.isArray(payload?.objectProperties) ? payload.objectProperties : [];
|
|
70
|
+
const kinds = new Map(); // base kind -> { count, subjects:Set, objects:Set }
|
|
71
|
+
const degree = new Map(); // label -> degree over example edges
|
|
72
|
+
const bumpKind = (k) => kinds.get(k) || kinds.set(k, { count: 0, subjects: new Set(), objects: new Set() }).get(k);
|
|
73
|
+
for (const g of groups) {
|
|
74
|
+
if (!g || !g.predicate) continue;
|
|
75
|
+
const k = baseKind(String(g.predicate));
|
|
76
|
+
const entry = bumpKind(k);
|
|
77
|
+
entry.count += Number(g.count) || (Array.isArray(g.examples) ? g.examples.length : 0);
|
|
78
|
+
for (const e of Array.isArray(g.examples) ? g.examples : []) {
|
|
79
|
+
const s = e?.subjectLabel || e?.subject;
|
|
80
|
+
const o = e?.objectLabel || e?.object;
|
|
81
|
+
if (s) { entry.subjects.add(s); degree.set(s, (degree.get(s) || 0) + 1); }
|
|
82
|
+
if (o) { entry.objects.add(o); degree.set(o, (degree.get(o) || 0) + 1); }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { classCounts, classOfLabel, kinds, degree };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Pick the focus symbol: the caller's choice when it is a real term, else the
|
|
89
|
+
* highest-degree focusable-class label, else the highest-degree label at all,
|
|
90
|
+
* else null. Ties break on label for determinism. */
|
|
91
|
+
function pickFocus(index, requested) {
|
|
92
|
+
if (requested && index.degree.has(requested)) return requested;
|
|
93
|
+
const ranked = [...index.degree.entries()]
|
|
94
|
+
.sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0])));
|
|
95
|
+
const focusable = ranked.find(([label]) => FOCUSABLE_CLASSES.has(index.classOfLabel.get(label)));
|
|
96
|
+
if (focusable) return focusable[0];
|
|
97
|
+
return ranked.length ? ranked[0][0] : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Suggested queries for a loaded code graph.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} payload the entities payload (individuals + objectProperties).
|
|
104
|
+
* @param {object} [opts]
|
|
105
|
+
* @param {string} [opts.focus] centre the neighbourhood on this symbol.
|
|
106
|
+
* @param {number} [opts.limit] cap the returned hints (default 12).
|
|
107
|
+
* @returns {{ focus: (string|null), hints: Array<{text,group,rationale}> }}
|
|
108
|
+
* `hints` is ordered neighbourhood → compositional → explore; empty
|
|
109
|
+
* when the graph holds nothing to explore.
|
|
110
|
+
*/
|
|
111
|
+
export function generateCodeHints(payload, { focus: requestedFocus = null, limit = 12 } = {}) {
|
|
112
|
+
const index = indexGraph(payload);
|
|
113
|
+
const focus = pickFocus(index, requestedFocus);
|
|
114
|
+
const hints = [];
|
|
115
|
+
const add = (text, group, rationale) => hints.push({ text, group, rationale });
|
|
116
|
+
const kindPresent = (k) => (index.kinds.get(k)?.count || 0) > 0;
|
|
117
|
+
|
|
118
|
+
if (focus) {
|
|
119
|
+
for (const [kind, template] of Object.entries(FORWARD_TEMPLATE)) {
|
|
120
|
+
if (index.kinds.get(kind)?.subjects.has(focus)) {
|
|
121
|
+
add(template(focus), "neighbourhood", `${focus} ${verbBare(kind)} other symbols in this graph`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const [kind, template] of Object.entries(REVERSE_TEMPLATE)) {
|
|
125
|
+
if (index.kinds.get(kind)?.objects.has(focus)) {
|
|
126
|
+
add(template(focus), "neighbourhood", `other symbols ${kind} ${focus}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compositional shapes over the graph's own edges and classes.
|
|
132
|
+
const calls = index.kinds.get("calls");
|
|
133
|
+
if (calls && index.classCounts.has("Function") && calls.objects.size) {
|
|
134
|
+
const target = [...calls.objects].sort()[0];
|
|
135
|
+
add(`which functions call ${target}`, "compositional", `${target} is called somewhere in this graph`);
|
|
136
|
+
}
|
|
137
|
+
const contains = index.kinds.get("contains");
|
|
138
|
+
if (contains && index.classCounts.has("Method")) {
|
|
139
|
+
const owner = [...contains.subjects].sort().find((s) => index.classOfLabel.get(s) === "Class");
|
|
140
|
+
if (owner) add(`public methods of ${owner}`, "compositional", `${owner} contains members`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Explore-the-whole-graph shapes. The rankings and the coverage filter come
|
|
144
|
+
// first — they are the more useful whole-graph questions — then a list and a
|
|
145
|
+
// count per present class. EDGE_NOUN_TO_METRIC names the nouns ask.mjs's
|
|
146
|
+
// superlative lane accepts, so a hint only offers a ranking the graph can
|
|
147
|
+
// actually compute.
|
|
148
|
+
const superlatives = [
|
|
149
|
+
{ noun: "imports", entity: "module", filter: "Module" },
|
|
150
|
+
{ noun: "callers", entity: "function", filter: "Function" },
|
|
151
|
+
];
|
|
152
|
+
for (const s of superlatives) {
|
|
153
|
+
const metric = EDGE_NOUN_TO_METRIC[s.noun];
|
|
154
|
+
if (metric && kindPresent(metric.kind) && index.classCounts.has(s.filter)) {
|
|
155
|
+
add(`which ${s.entity} has the most ${s.noun}`, "explore", `rank ${s.entity}s by ${s.noun}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (kindPresent("tests")) {
|
|
159
|
+
add("which of those are tested", "explore", "filter a listing by test coverage");
|
|
160
|
+
}
|
|
161
|
+
const classesByCount = [...index.classCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
162
|
+
for (const [cls, count] of classesByCount) {
|
|
163
|
+
const [, plural] = CODE_CLASS_NOUN[cls];
|
|
164
|
+
add(`list ${plural}`, "explore", `${count} ${plural} in this graph`);
|
|
165
|
+
add(`how many ${plural}`, "explore", `count the ${plural}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { focus, hints: hints.slice(0, limit) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The infinitive ask-vocab curates for each relation, used in rationales.
|
|
172
|
+
function verbBare(kind) {
|
|
173
|
+
return RELATIONS[kind]?.bare || kind;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { CODE_CLASS_NOUN, indexGraph };
|
|
@@ -430,13 +430,19 @@ ${THEME_TOKENS_CSS}
|
|
|
430
430
|
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 1rem; align-items: start; }
|
|
431
431
|
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
432
432
|
|
|
433
|
+
/* the left column: quest and satchel as full-width strips above the room,
|
|
434
|
+
the room itself, then the reset/play/step/turn strip pinned directly
|
|
435
|
+
below it — everything in this column shares the room's own width, so
|
|
436
|
+
the column's total height tracks the side column instead of leaving a
|
|
437
|
+
gap under a lone, short room panel. */
|
|
438
|
+
.stage-left { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
439
|
+
|
|
433
440
|
/* the room view — a 90s-RPG interior cutaway, all CSS gradients, no
|
|
434
441
|
external asset: a striped papered wall over a gilt dado rail over a
|
|
435
442
|
diagonal-plank floor, framed by the same ornate double rule as before.
|
|
436
443
|
The sprite tiles sit on the floor band (align-items flex-end), so
|
|
437
444
|
furniture and people read as standing IN the room, against its wall,
|
|
438
445
|
rather than floating in a chip strip. */
|
|
439
|
-
#playStage .room-frame { align-self: start; position: sticky; top: 1rem; }
|
|
440
446
|
.room-frame {
|
|
441
447
|
position: relative; min-height: 250px;
|
|
442
448
|
background:
|
|
@@ -557,7 +563,7 @@ ${THEME_TOKENS_CSS}
|
|
|
557
563
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
558
564
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
|
|
559
565
|
.chatask input:disabled { opacity: .5; }
|
|
560
|
-
.controls-row { display: flex; align-items: center; gap: .6rem;
|
|
566
|
+
.controls-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
|
561
567
|
.controls-row button { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; padding: .38rem .85rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); }
|
|
562
568
|
.controls-row button:hover:not(:disabled) { background: var(--parchment-strong); }
|
|
563
569
|
.controls-row button:disabled { opacity: .4; cursor: default; }
|
|
@@ -567,7 +573,7 @@ ${THEME_TOKENS_CSS}
|
|
|
567
573
|
|
|
568
574
|
/* edit mode */
|
|
569
575
|
body:not(.editing) #editStage { display: none; }
|
|
570
|
-
body.editing #playStage
|
|
576
|
+
body.editing #playStage { display: none; }
|
|
571
577
|
/* .stage's inherited align-items: start (kept for #playStage's own fixed-
|
|
572
578
|
size room panel) would otherwise top-align .edittext against the
|
|
573
579
|
manor-map/room-detail/legend column and leave a dead gap below the
|
|
@@ -583,7 +589,7 @@ ${THEME_TOKENS_CSS}
|
|
|
583
589
|
.roomdetail .sprite-row { min-height: 3.2rem; }
|
|
584
590
|
#legendList { display: flex; flex-wrap: wrap; gap: .5rem .3rem; }
|
|
585
591
|
|
|
586
|
-
body.preview .side, body.preview .controls-row, body.preview .status { display: none; }
|
|
592
|
+
body.preview .side, body.preview .stage-left > .panel, body.preview .controls-row, body.preview .status { display: none; }
|
|
587
593
|
body.preview main { padding: 0; max-width: none; }
|
|
588
594
|
body.preview .stage { display: block; }
|
|
589
595
|
body.preview .eyebrow, body.preview h1, body.preview .mode-toggle, body.preview #editStage { display: none; }
|
|
@@ -597,9 +603,25 @@ ${THEME_TOKENS_CSS}
|
|
|
597
603
|
<button id="editModeBtn" type="button" class="mode-toggle" disabled>edit the world</button>
|
|
598
604
|
</div>
|
|
599
605
|
<div class="stage" id="playStage">
|
|
600
|
-
<div class="
|
|
601
|
-
<div class="
|
|
602
|
-
|
|
606
|
+
<div class="stage-left">
|
|
607
|
+
<div class="panel goals">
|
|
608
|
+
<h2>quest</h2>
|
|
609
|
+
<div id="goalList"></div>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="panel carrying">
|
|
612
|
+
<h2>satchel</h2>
|
|
613
|
+
<div class="chips" id="carryList"></div>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="room-frame" id="roomFrame">
|
|
616
|
+
<div class="room-plaque mono" id="roomName"></div>
|
|
617
|
+
<div class="sprite-row" id="spriteRow"></div>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="controls-row" id="playControls">
|
|
620
|
+
<button id="resetBtn" type="button" disabled>reset</button>
|
|
621
|
+
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
622
|
+
<button id="stepBtn" type="button" disabled>step</button>
|
|
623
|
+
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
624
|
+
</div>
|
|
603
625
|
</div>
|
|
604
626
|
<aside class="side" aria-label="The adventure's log and chat">
|
|
605
627
|
<div class="chat">
|
|
@@ -612,18 +634,10 @@ ${THEME_TOKENS_CSS}
|
|
|
612
634
|
<input id="chatq" type="text" placeholder="go north" aria-label="Type a command, or ask a question" disabled>
|
|
613
635
|
</form>
|
|
614
636
|
</div>
|
|
615
|
-
<div class="panel carrying">
|
|
616
|
-
<h2>satchel</h2>
|
|
617
|
-
<div class="chips" id="carryList"></div>
|
|
618
|
-
</div>
|
|
619
637
|
<div class="panel roommap">
|
|
620
638
|
<h2>the manor, so far</h2>
|
|
621
639
|
<div class="map-viewport"><div id="mapWrap"></div></div>
|
|
622
640
|
</div>
|
|
623
|
-
<div class="panel goals">
|
|
624
|
-
<h2>quest</h2>
|
|
625
|
-
<div id="goalList"></div>
|
|
626
|
-
</div>
|
|
627
641
|
</aside>
|
|
628
642
|
</div>
|
|
629
643
|
|
|
@@ -652,12 +666,6 @@ ${THEME_TOKENS_CSS}
|
|
|
652
666
|
</div>
|
|
653
667
|
|
|
654
668
|
<div class="goal-line" id="goalLine"></div>
|
|
655
|
-
<div class="controls-row" id="playControls">
|
|
656
|
-
<button id="resetBtn" type="button" disabled>reset</button>
|
|
657
|
-
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
658
|
-
<button id="stepBtn" type="button" disabled>step</button>
|
|
659
|
-
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
660
|
-
</div>
|
|
661
669
|
<div class="status" id="status">loading the engine…</div>
|
|
662
670
|
</main>
|
|
663
671
|
<script>
|
|
@@ -311,7 +311,9 @@ ${THEME_TOKENS_CSS}
|
|
|
311
311
|
</label>
|
|
312
312
|
<span class="tool-cluster">
|
|
313
313
|
<button type="button" id="exportMd" class="tool-btn" title="download this conversation as Markdown">export .md</button>
|
|
314
|
+
<button type="button" id="exportFacts" class="tool-btn" title="download this session's facts as JSONL (the tmct extract shape, provenance included)">export facts</button>
|
|
314
315
|
<button type="button" id="printChat" class="tool-btn" title="print the whole conversation">print</button>
|
|
316
|
+
<button type="button" id="reinitStore" class="tool-btn" title="drop everything saved on this device and reload from the shipped seed">reset to seed</button>
|
|
315
317
|
</span>
|
|
316
318
|
</div>
|
|
317
319
|
</form>
|
|
@@ -802,6 +804,42 @@ ${THEME_TOKENS_CSS}
|
|
|
802
804
|
});
|
|
803
805
|
el("printChat").addEventListener("click", () => window.print());
|
|
804
806
|
|
|
807
|
+
// ---- store controls: export the triple store, or reset it whole ----------
|
|
808
|
+
// "export facts" downloads the session's whole memory as JSONL (the same
|
|
809
|
+
// subject/predicate/object/provenance shape the extract and memory-export
|
|
810
|
+
// CLI paths emit), so what you taught leaves in the standard shape.
|
|
811
|
+
el("exportFacts").addEventListener("click", async () => {
|
|
812
|
+
const session = window.tmctChatSession;
|
|
813
|
+
if (!session || !window.tmctChat.exportFactsJsonl) return;
|
|
814
|
+
let jsonl;
|
|
815
|
+
try {
|
|
816
|
+
jsonl = await window.tmctChat.exportFactsJsonl(session.memoryDir);
|
|
817
|
+
} catch (err) {
|
|
818
|
+
statusEl.textContent = "couldn't export the facts (" + (err && err.message ? err.message : err) + ")";
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const blob = new Blob([jsonl], { type: "application/x-ndjson" });
|
|
822
|
+
const url = URL.createObjectURL(blob);
|
|
823
|
+
const link = document.createElement("a");
|
|
824
|
+
link.href = url;
|
|
825
|
+
link.download = "tmct-facts.jsonl";
|
|
826
|
+
document.body.appendChild(link);
|
|
827
|
+
link.click();
|
|
828
|
+
link.remove();
|
|
829
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// "reset to seed" is the full re-initialisation: drop the persisted payload
|
|
833
|
+
// outright and reload, so boot re-seeds from the page's shipped seed as if on
|
|
834
|
+
// a first visit. Harder than "forget everything", which only swaps the live
|
|
835
|
+
// session — this trusts nothing in memory and re-fetches the seed asset.
|
|
836
|
+
el("reinitStore").addEventListener("click", async () => {
|
|
837
|
+
clearTimeout(saveTimer);
|
|
838
|
+
saveTimer = null;
|
|
839
|
+
if (persist) await persist.clear();
|
|
840
|
+
window.location.reload();
|
|
841
|
+
});
|
|
842
|
+
|
|
805
843
|
async function boot() {
|
|
806
844
|
if (!window.tmctChat) {
|
|
807
845
|
statusEl.textContent = "the chat engine didn't load \\u2014 this page needs its build step (npm run demo:build)";
|
|
@@ -836,7 +874,7 @@ ${THEME_TOKENS_CSS}
|
|
|
836
874
|
addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats)
|
|
837
875
|
+ "." + restoredNote + " Ask it something, or teach it a fact of your own.");
|
|
838
876
|
await renderStatsPanel(stats);
|
|
839
|
-
inputEl.placeholder = seedPayload ? 'try "what is a dog"' : window.tmctChat.vocabExampleHint(false);
|
|
877
|
+
inputEl.placeholder = seedPayload ? 'try "what is a dog" or "list facts"' : window.tmctChat.vocabExampleHint(false);
|
|
840
878
|
renderStatus();
|
|
841
879
|
setBusy(false);
|
|
842
880
|
inputEl.focus();
|