@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.5
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 +121 -114
- package/ROADMAP.md +14 -4
- package/bin/tmct.mjs +167 -77
- package/data/games/crates.txt +24 -0
- package/data/games/hanoi-3.txt +30 -0
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +1515 -52
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +350 -0
- package/src/import-file.mjs +86 -0
- package/src/init.mjs +46 -1
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +635 -0
- package/src/memory/core.mjs +100 -17
- package/src/memory/shacl.mjs +20 -7
- package/src/memory-ask-browser-entry.mjs +9 -7
- package/src/memory-ask-browser.bundle.js +5648 -1177
- package/src/plan-viz.mjs +409 -0
- package/src/router/drive.mjs +122 -13
- package/src/router/guardrail.mjs +5 -0
- package/src/router/registry.mjs +55 -11
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +84 -0
- package/src/sentences.mjs +19 -0
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +66 -0
- package/src/wink-model.mjs +12 -6
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5411
- package/src/viz.mjs +0 -959
package/src/plan-viz.mjs
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// plan-viz.mjs — renders a computed plan (result.plan from the chat plan lane)
|
|
2
|
+
// as a self-contained, animated HTML page: the "blocks" archetype.
|
|
3
|
+
//
|
|
4
|
+
// A pure layout step (computeBlocksLayout) and a pure string builder
|
|
5
|
+
// (renderPlanHtml). No I/O here — callers pass the plan, the class→archetype
|
|
6
|
+
// map (rendersAs), and the size-order pairs; both derive from fact rows at
|
|
7
|
+
// wiring time.
|
|
8
|
+
|
|
9
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
10
|
+
|
|
11
|
+
const BOARD_W = 640;
|
|
12
|
+
const BOARD_H = 260;
|
|
13
|
+
const BASE_Y = BOARD_H - 28;
|
|
14
|
+
const BLOCK_H = 26;
|
|
15
|
+
const BLOCK_GAP = 3;
|
|
16
|
+
const BASE_W = 64;
|
|
17
|
+
const STEP_W = 33;
|
|
18
|
+
const LIFT_Y = 36;
|
|
19
|
+
|
|
20
|
+
// One stable hue per block class (viz's one-hue-per-class rule); shades within
|
|
21
|
+
// a class darken with size rank.
|
|
22
|
+
const CLASS_HUES = ["#5A80AC", "#8A6E4E", "#5E8A4E", "#8A4E6E", "#4E8A86"];
|
|
23
|
+
|
|
24
|
+
function darken(hex, fraction) {
|
|
25
|
+
const n = parseInt(hex.slice(1), 16);
|
|
26
|
+
const ch = (v) => Math.max(0, Math.round(v * (1 - fraction)));
|
|
27
|
+
const r = ch((n >> 16) & 255);
|
|
28
|
+
const g = ch((n >> 8) & 255);
|
|
29
|
+
const b = ch(n & 255);
|
|
30
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Topological rank over [smaller, larger] pairs, label tiebreak; members
|
|
34
|
+
* absent from every pair are appended in label order. */
|
|
35
|
+
function rankBySize(sizeOrder, members) {
|
|
36
|
+
const pairs = Array.isArray(sizeOrder) ? sizeOrder : [];
|
|
37
|
+
const inPairs = new Set();
|
|
38
|
+
const after = new Map(); // smaller -> Set(larger)
|
|
39
|
+
const indegree = new Map();
|
|
40
|
+
for (const [small, large] of pairs) {
|
|
41
|
+
if (!small || !large) continue;
|
|
42
|
+
inPairs.add(small); inPairs.add(large);
|
|
43
|
+
if (!after.has(small)) after.set(small, new Set());
|
|
44
|
+
if (!after.get(small).has(large)) {
|
|
45
|
+
after.get(small).add(large);
|
|
46
|
+
indegree.set(large, (indegree.get(large) || 0) + 1);
|
|
47
|
+
}
|
|
48
|
+
if (!indegree.has(small)) indegree.set(small, indegree.get(small) || 0);
|
|
49
|
+
}
|
|
50
|
+
const ranks = {};
|
|
51
|
+
let next = 0;
|
|
52
|
+
let ready = [...inPairs].filter((t) => (indegree.get(t) || 0) === 0).sort();
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
while (ready.length) {
|
|
55
|
+
const term = ready.shift();
|
|
56
|
+
if (seen.has(term)) continue;
|
|
57
|
+
seen.add(term);
|
|
58
|
+
ranks[term] = next++;
|
|
59
|
+
for (const larger of [...(after.get(term) || [])].sort()) {
|
|
60
|
+
indegree.set(larger, indegree.get(larger) - 1);
|
|
61
|
+
if (indegree.get(larger) === 0) ready.push(larger);
|
|
62
|
+
}
|
|
63
|
+
ready.sort();
|
|
64
|
+
}
|
|
65
|
+
for (const m of [...members].sort()) {
|
|
66
|
+
if (!(m in ranks)) ranks[m] = next++;
|
|
67
|
+
}
|
|
68
|
+
return ranks;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pure geometry for the blocks archetype.
|
|
73
|
+
*
|
|
74
|
+
* plan: { states: [[{subject, predicate, object}], …], domain: { classMembers } }
|
|
75
|
+
* rendersAs: { className: "block" | "slot" } — classes absent from the map
|
|
76
|
+
* fall back to labeled circles.
|
|
77
|
+
* sizeOrder: [[smallerLabel, largerLabel], …]
|
|
78
|
+
*
|
|
79
|
+
* Returns { board, ranks, anchors, snapshots: [{ items, stacks }] } —
|
|
80
|
+
* deterministic for identical inputs.
|
|
81
|
+
*/
|
|
82
|
+
export function computeBlocksLayout({ plan, rendersAs = {}, sizeOrder = [] }) {
|
|
83
|
+
const classMembers = plan?.domain?.classMembers || {};
|
|
84
|
+
const classes = Object.keys(classMembers).sort();
|
|
85
|
+
const blockSet = new Set();
|
|
86
|
+
const anchorDefs = [];
|
|
87
|
+
const classHue = {};
|
|
88
|
+
let hueIndex = 0;
|
|
89
|
+
for (const cls of classes) {
|
|
90
|
+
const archetype = rendersAs[cls];
|
|
91
|
+
const members = [...(classMembers[cls] || [])].sort();
|
|
92
|
+
if (archetype === "block") {
|
|
93
|
+
classHue[cls] = CLASS_HUES[hueIndex++ % CLASS_HUES.length];
|
|
94
|
+
for (const m of members) blockSet.add(m);
|
|
95
|
+
} else {
|
|
96
|
+
const kind = archetype === "slot" ? "slot" : "circle";
|
|
97
|
+
for (const m of members) anchorDefs.push({ id: m, kind });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
anchorDefs.sort((a, b) => (a.kind === b.kind ? (a.id < b.id ? -1 : 1) : a.kind === "slot" ? -1 : 1));
|
|
101
|
+
const anchors = anchorDefs.map((a, i) => ({
|
|
102
|
+
...a,
|
|
103
|
+
x: Math.round((BOARD_W * (2 * i + 1)) / (2 * anchorDefs.length)),
|
|
104
|
+
y: BASE_Y,
|
|
105
|
+
}));
|
|
106
|
+
const anchorX = new Map(anchors.map((a) => [a.id, a.x]));
|
|
107
|
+
|
|
108
|
+
const blocks = [...blockSet].sort();
|
|
109
|
+
const ranks = rankBySize(sizeOrder, blocks);
|
|
110
|
+
const maxRank = blocks.reduce((m, b) => Math.max(m, ranks[b] ?? 0), 0);
|
|
111
|
+
const blockClassOf = (label) =>
|
|
112
|
+
classes.find((cls) => rendersAs[cls] === "block" && (classMembers[cls] || []).includes(label));
|
|
113
|
+
const widthOf = (label) => BASE_W + (ranks[label] ?? 0) * STEP_W;
|
|
114
|
+
const fillOf = (label) => {
|
|
115
|
+
const hue = classHue[blockClassOf(label)] || CLASS_HUES[0];
|
|
116
|
+
const f = maxRank > 0 ? ((ranks[label] ?? 0) / (maxRank + 1)) * 0.45 : 0;
|
|
117
|
+
return darken(hue, f);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const snapshots = (plan?.states || []).map((rows) => {
|
|
121
|
+
const supporterOf = new Map(); // object -> subject resting on it
|
|
122
|
+
for (const r of [...rows].sort((a, b) => (a.subject < b.subject ? -1 : 1))) {
|
|
123
|
+
if (blockSet.has(r.subject) && !supporterOf.has(r.object)) {
|
|
124
|
+
supporterOf.set(r.object, r.subject);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const stacks = {};
|
|
128
|
+
const items = anchors.map((a) => ({ ...a }));
|
|
129
|
+
for (const a of anchors) {
|
|
130
|
+
const stack = [];
|
|
131
|
+
let top = a.id;
|
|
132
|
+
while (supporterOf.has(top)) {
|
|
133
|
+
top = supporterOf.get(top);
|
|
134
|
+
stack.push(top);
|
|
135
|
+
}
|
|
136
|
+
stacks[a.id] = stack;
|
|
137
|
+
stack.forEach((label, i) => {
|
|
138
|
+
const w = widthOf(label);
|
|
139
|
+
items.push({
|
|
140
|
+
id: label,
|
|
141
|
+
kind: "block",
|
|
142
|
+
x: a.x - Math.round(w / 2),
|
|
143
|
+
y: BASE_Y - (i + 1) * (BLOCK_H + BLOCK_GAP),
|
|
144
|
+
w,
|
|
145
|
+
h: BLOCK_H,
|
|
146
|
+
rank: ranks[label] ?? 0,
|
|
147
|
+
fill: fillOf(label),
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
items.sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
152
|
+
return { items, stacks };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return { board: { w: BOARD_W, h: BOARD_H }, ranks, anchors, snapshots };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Phase brackets from the largest block's single move: everything before it
|
|
159
|
+
* frees the piece, the move itself is the pivot, the rest rebuilds. */
|
|
160
|
+
function phasesFor(actions, ranks) {
|
|
161
|
+
const ranked = Object.keys(ranks);
|
|
162
|
+
if (!ranked.length || !actions.length) return [];
|
|
163
|
+
const pivot = ranked.reduce((a, b) => (ranks[a] >= ranks[b] ? a : b));
|
|
164
|
+
const k = actions.findIndex((a) => a.subject === pivot);
|
|
165
|
+
if (k <= 0 || k >= actions.length - 1) return [];
|
|
166
|
+
return [
|
|
167
|
+
{ label: `free ${pivot}`, from: 0, to: k },
|
|
168
|
+
{ label: "the pivot", from: k, to: k + 1 },
|
|
169
|
+
{ label: `rebuild on ${pivot}`, from: k + 1, to: actions.length },
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const displayPredicate = (p) => String(p).replace(/^mgx:/, "").replace(/-/g, " ");
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The self-contained plan page. Consumes the plan-lane contract
|
|
177
|
+
* ({actions, states, stepGoals, goal, domain}) plus the render inputs.
|
|
178
|
+
*/
|
|
179
|
+
export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } = {}) {
|
|
180
|
+
const actions = plan?.actions || [];
|
|
181
|
+
const layout = computeBlocksLayout({ plan, rendersAs, sizeOrder });
|
|
182
|
+
const stepGoals =
|
|
183
|
+
Array.isArray(plan?.stepGoals) && plan.stepGoals.length === actions.length
|
|
184
|
+
? plan.stepGoals
|
|
185
|
+
: null;
|
|
186
|
+
const labels = actions.map((a, i) => a.label || `move ${a.subject} onto ${a.target}`);
|
|
187
|
+
const factsPerStep = (plan?.states || []).map((rows) =>
|
|
188
|
+
[...rows]
|
|
189
|
+
.map((r) => `${r.subject} ${displayPredicate(r.predicate)} ${r.object}`)
|
|
190
|
+
.sort(),
|
|
191
|
+
);
|
|
192
|
+
const embedded = embedJson({
|
|
193
|
+
actions,
|
|
194
|
+
labels,
|
|
195
|
+
stepGoals,
|
|
196
|
+
goalText: plan?.goal?.text || "",
|
|
197
|
+
layouts: layout.snapshots,
|
|
198
|
+
anchors: layout.anchors,
|
|
199
|
+
board: layout.board,
|
|
200
|
+
facts: factsPerStep,
|
|
201
|
+
phases: phasesFor(actions, layout.ranks),
|
|
202
|
+
});
|
|
203
|
+
const pageTitle = title || `tmct plan — ${actions.length} move${actions.length === 1 ? "" : "s"}`;
|
|
204
|
+
|
|
205
|
+
return `<!doctype html>
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<head>
|
|
208
|
+
<meta charset="utf-8">
|
|
209
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
210
|
+
<title>${escapeHtml(pageTitle)}</title>
|
|
211
|
+
<style>
|
|
212
|
+
${THEME_TOKENS_CSS}
|
|
213
|
+
html { background: var(--bg); }
|
|
214
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; }
|
|
215
|
+
main { max-width: 880px; margin: 0 auto; padding: 1.4rem 1rem 3rem; }
|
|
216
|
+
.head { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: .4rem; }
|
|
217
|
+
h1 { font-size: 1.15rem; margin: 0 0 .8rem; }
|
|
218
|
+
.chip { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; padding: .12rem .55rem; }
|
|
219
|
+
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: 1rem; }
|
|
220
|
+
@media (max-width: 660px) { .stage { grid-template-columns: 1fr; } }
|
|
221
|
+
.boardwrap { overflow-x: auto; }
|
|
222
|
+
.board { position: relative; width: ${BOARD_W}px; height: ${BOARD_H}px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; }
|
|
223
|
+
.base { position: absolute; left: 4%; right: 4%; bottom: 22px; height: 6px; background: var(--line); border-radius: 3px; }
|
|
224
|
+
.post { position: absolute; bottom: 28px; width: 6px; height: 150px; background: var(--line); border-radius: 3px 3px 0 0; }
|
|
225
|
+
.anchorlabel { position: absolute; bottom: 2px; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); }
|
|
226
|
+
.block { position: absolute; height: ${BLOCK_H}px; border-radius: 6px; color: #fff; display: flex; align-items: center; justify-content: center; font-family: ${MONO_STACK}; font-size: .66rem; box-shadow: 0 1px 2px rgba(0,0,0,.25); }
|
|
227
|
+
.block.moving { z-index: 3; box-shadow: 0 4px 10px rgba(0,0,0,.3); }
|
|
228
|
+
.circle { position: absolute; width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--muted); transform: translate(-50%, -100%); }
|
|
229
|
+
.circlelabel { position: absolute; transform: translateX(-50%); font-family: ${MONO_STACK}; font-size: .6rem; color: var(--muted); }
|
|
230
|
+
.controls { display: flex; gap: .45rem; align-items: center; margin-top: .7rem; flex-wrap: wrap; }
|
|
231
|
+
.controls button { font-family: ${MONO_STACK}; font-size: .78rem; padding: .3rem .7rem; border-radius: 6px; border: 1px solid var(--line); background: var(--card); color: var(--ink); cursor: pointer; }
|
|
232
|
+
.controls button:hover { border-color: var(--taught); }
|
|
233
|
+
.controls button:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
234
|
+
.controls button[disabled] { opacity: .4; cursor: default; }
|
|
235
|
+
.controls .step { margin-left: auto; font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
236
|
+
.goalline { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .76rem; color: var(--taught); background: var(--taught-soft); border-radius: 6px; padding: .45rem .65rem; }
|
|
237
|
+
.facts { margin-top: .7rem; font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); border-top: 1px dashed var(--line); padding-top: .55rem; line-height: 1.7; }
|
|
238
|
+
.facts b { color: var(--ink); }
|
|
239
|
+
.movelist { list-style: none; margin: 0; padding: 0; font-family: ${MONO_STACK}; font-size: .74rem; align-self: start; }
|
|
240
|
+
.movelist li { padding: .28rem .55rem; border-left: 3px solid transparent; color: var(--muted); cursor: pointer; border-radius: 0 5px 5px 0; font-variant-numeric: tabular-nums; }
|
|
241
|
+
.movelist li:hover { color: var(--ink); }
|
|
242
|
+
.movelist li.done { color: var(--ink); }
|
|
243
|
+
.movelist li.current { border-left-color: var(--taught); color: var(--ink); background: var(--taught-soft); font-weight: 700; }
|
|
244
|
+
.movelist .phasehead { font-size: .64rem; letter-spacing: .06em; text-transform: uppercase; border-left: 3px solid var(--taught); margin-top: .5rem; }
|
|
245
|
+
.movelist .phasehead:first-child { margin-top: 0; }
|
|
246
|
+
.movelist .phasehead:hover { color: var(--taught); background: var(--taught-soft); }
|
|
247
|
+
.movelist .phasehead:focus-visible { outline: 2px solid var(--taught); outline-offset: 2px; }
|
|
248
|
+
@media (prefers-reduced-motion: reduce) { .block { transition: none !important; } }
|
|
249
|
+
</style>
|
|
250
|
+
</head>
|
|
251
|
+
<body>
|
|
252
|
+
<main>
|
|
253
|
+
<div class="head">
|
|
254
|
+
<h1>${escapeHtml(pageTitle)}</h1>
|
|
255
|
+
<span class="chip">blocks archetype · ${layout.snapshots.length} snapshots · plan: findActionPath</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="stage">
|
|
258
|
+
<div>
|
|
259
|
+
<div class="boardwrap"><div class="board" id="board" aria-label="plan board"></div></div>
|
|
260
|
+
<div class="controls">
|
|
261
|
+
<button id="reset" aria-label="Reset to start">⏮ reset</button>
|
|
262
|
+
<button id="back" aria-label="Step back">◀ back</button>
|
|
263
|
+
<button id="play" aria-label="Play">▶ play</button>
|
|
264
|
+
<button id="next" aria-label="Step forward">step ▶</button>
|
|
265
|
+
<span class="step" id="stepLabel"></span>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="goalline" id="goalline" hidden></div>
|
|
268
|
+
<div class="facts" id="facts"></div>
|
|
269
|
+
</div>
|
|
270
|
+
<ol class="movelist" id="movelist"></ol>
|
|
271
|
+
</div>
|
|
272
|
+
</main>
|
|
273
|
+
<script>
|
|
274
|
+
const PLAN = ${embedded};
|
|
275
|
+
(function () {
|
|
276
|
+
"use strict";
|
|
277
|
+
const N = PLAN.actions.length;
|
|
278
|
+
const board = document.getElementById("board");
|
|
279
|
+
const stepLabel = document.getElementById("stepLabel");
|
|
280
|
+
const goalline = document.getElementById("goalline");
|
|
281
|
+
const factsEl = document.getElementById("facts");
|
|
282
|
+
const movelist = document.getElementById("movelist");
|
|
283
|
+
const btn = {
|
|
284
|
+
reset: document.getElementById("reset"), back: document.getElementById("back"),
|
|
285
|
+
play: document.getElementById("play"), next: document.getElementById("next"),
|
|
286
|
+
};
|
|
287
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
288
|
+
let step = 0, playing = false, animating = false;
|
|
289
|
+
|
|
290
|
+
const base = document.createElement("div"); base.className = "base"; board.appendChild(base);
|
|
291
|
+
for (const a of PLAN.anchors) {
|
|
292
|
+
if (a.kind === "slot") {
|
|
293
|
+
const post = document.createElement("div"); post.className = "post";
|
|
294
|
+
post.style.left = (a.x - 3) + "px"; board.appendChild(post);
|
|
295
|
+
} else {
|
|
296
|
+
const c = document.createElement("div"); c.className = "circle";
|
|
297
|
+
c.style.left = a.x + "px"; c.style.top = a.y + "px"; board.appendChild(c);
|
|
298
|
+
}
|
|
299
|
+
const lab = document.createElement("div");
|
|
300
|
+
lab.className = a.kind === "slot" ? "anchorlabel" : "circlelabel";
|
|
301
|
+
lab.textContent = a.id; lab.style.left = a.x + "px";
|
|
302
|
+
if (a.kind !== "slot") lab.style.top = (a.y + 4) + "px";
|
|
303
|
+
board.appendChild(lab);
|
|
304
|
+
}
|
|
305
|
+
const blockEls = {};
|
|
306
|
+
for (const item of PLAN.layouts[0].items) {
|
|
307
|
+
if (item.kind !== "block") continue;
|
|
308
|
+
const el = document.createElement("div");
|
|
309
|
+
el.className = "block"; el.textContent = item.id;
|
|
310
|
+
el.style.width = item.w + "px"; el.style.background = item.fill;
|
|
311
|
+
board.appendChild(el); blockEls[item.id] = el;
|
|
312
|
+
}
|
|
313
|
+
const posIn = (snap, id) => snap.items.find((i) => i.id === id && i.kind === "block");
|
|
314
|
+
function drawState(i) {
|
|
315
|
+
for (const id of Object.keys(blockEls)) {
|
|
316
|
+
const p = posIn(PLAN.layouts[i], id);
|
|
317
|
+
if (!p) continue;
|
|
318
|
+
blockEls[id].style.transition = "none";
|
|
319
|
+
blockEls[id].style.left = p.x + "px";
|
|
320
|
+
blockEls[id].style.top = p.y + "px";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
324
|
+
async function animateMove(i) {
|
|
325
|
+
const before = PLAN.layouts[i], after = PLAN.layouts[i + 1];
|
|
326
|
+
const movers = Object.keys(blockEls).filter((id) => {
|
|
327
|
+
const a = posIn(before, id), b = posIn(after, id);
|
|
328
|
+
return a && b && (a.x !== b.x || a.y !== b.y);
|
|
329
|
+
});
|
|
330
|
+
if (reduced || movers.length !== 1) { drawState(i + 1); return; }
|
|
331
|
+
const id = movers[0], el = blockEls[id], to = posIn(after, id);
|
|
332
|
+
animating = true; el.classList.add("moving");
|
|
333
|
+
el.style.transition = "top .18s ease-in"; el.style.top = "${LIFT_Y}px"; await wait(190);
|
|
334
|
+
el.style.transition = "left .26s ease-in-out"; el.style.left = to.x + "px"; await wait(270);
|
|
335
|
+
el.style.transition = "top .18s ease-out"; el.style.top = to.y + "px"; await wait(200);
|
|
336
|
+
el.classList.remove("moving"); animating = false;
|
|
337
|
+
}
|
|
338
|
+
const phaseFor = (i) => {
|
|
339
|
+
if (i >= N) return "done";
|
|
340
|
+
const ph = PLAN.phases.find((p) => i >= p.from && i < p.to);
|
|
341
|
+
return ph ? ph.label : "";
|
|
342
|
+
};
|
|
343
|
+
function render() {
|
|
344
|
+
const phase = phaseFor(step);
|
|
345
|
+
stepLabel.textContent = "step " + step + " / " + N + (phase ? " · " + phase : "");
|
|
346
|
+
if (PLAN.stepGoals) {
|
|
347
|
+
goalline.hidden = false;
|
|
348
|
+
goalline.textContent = step < N
|
|
349
|
+
? "Goal (inferred): " + PLAN.stepGoals[step]
|
|
350
|
+
: "Goal (inferred): Goal reached" + (PLAN.goalText ? " — " + PLAN.goalText : "") + " (" + N + " of " + N + " steps).";
|
|
351
|
+
}
|
|
352
|
+
factsEl.innerHTML = "<b>board@step" + step + "</b> — " +
|
|
353
|
+
PLAN.facts[step].map((f) => f.replace(/&/g, "&").replace(/</g, "<")).join(" · ") +
|
|
354
|
+
' <span style="opacity:.7">(plan: findActionPath)</span>';
|
|
355
|
+
[...movelist.querySelectorAll("li:not(.phasehead)")].forEach((li, i) => {
|
|
356
|
+
li.classList.toggle("done", i < step);
|
|
357
|
+
li.classList.toggle("current", i === step && step < N);
|
|
358
|
+
});
|
|
359
|
+
btn.back.disabled = step === 0 || animating;
|
|
360
|
+
btn.next.disabled = step === N || animating;
|
|
361
|
+
btn.play.textContent = playing ? "⏸ pause" : (step === N ? "▶ replay" : "▶ play");
|
|
362
|
+
}
|
|
363
|
+
async function forward() {
|
|
364
|
+
if (animating || step >= N) return;
|
|
365
|
+
render(); await animateMove(step); step += 1; render();
|
|
366
|
+
}
|
|
367
|
+
async function playRange(from, to) {
|
|
368
|
+
if (animating) return;
|
|
369
|
+
playing = false; step = from; drawState(step); render();
|
|
370
|
+
playing = true; render();
|
|
371
|
+
while (playing && step < to) { await forward(); if (step < to) await wait(300); }
|
|
372
|
+
playing = false; render();
|
|
373
|
+
}
|
|
374
|
+
PLAN.labels.forEach((label, i) => {
|
|
375
|
+
const ph = PLAN.phases.find((p) => p.from === i);
|
|
376
|
+
if (ph) {
|
|
377
|
+
const head = document.createElement("li");
|
|
378
|
+
head.className = "phasehead"; head.tabIndex = 0;
|
|
379
|
+
head.setAttribute("role", "button");
|
|
380
|
+
head.setAttribute("aria-label", "Play phase: " + ph.label);
|
|
381
|
+
head.textContent = ph.label + " · " + (ph.from + 1) + (ph.to - ph.from > 1 ? "–" + ph.to : "");
|
|
382
|
+
const go = () => playRange(ph.from, ph.to);
|
|
383
|
+
head.addEventListener("click", go);
|
|
384
|
+
head.addEventListener("keydown", (e) => {
|
|
385
|
+
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); go(); }
|
|
386
|
+
});
|
|
387
|
+
movelist.appendChild(head);
|
|
388
|
+
}
|
|
389
|
+
const li = document.createElement("li");
|
|
390
|
+
li.textContent = (i + 1) + ". " + label;
|
|
391
|
+
li.addEventListener("click", () => { if (animating) return; playing = false; step = i + 1; drawState(step); render(); });
|
|
392
|
+
movelist.appendChild(li);
|
|
393
|
+
});
|
|
394
|
+
btn.next.addEventListener("click", () => { playing = false; forward(); });
|
|
395
|
+
btn.back.addEventListener("click", () => { if (animating) return; playing = false; step = Math.max(0, step - 1); drawState(step); render(); });
|
|
396
|
+
btn.reset.addEventListener("click", () => { if (animating) return; playing = false; step = 0; drawState(0); render(); });
|
|
397
|
+
btn.play.addEventListener("click", async () => {
|
|
398
|
+
if (animating) return;
|
|
399
|
+
if (playing) { playing = false; render(); return; }
|
|
400
|
+
if (step === N) { step = 0; drawState(0); }
|
|
401
|
+
await playRange(step, N);
|
|
402
|
+
});
|
|
403
|
+
drawState(0); render();
|
|
404
|
+
})();
|
|
405
|
+
</script>
|
|
406
|
+
</body>
|
|
407
|
+
</html>
|
|
408
|
+
`;
|
|
409
|
+
}
|
package/src/router/drive.mjs
CHANGED
|
@@ -10,25 +10,35 @@
|
|
|
10
10
|
// which are ..." request -> the planner, HTN-decomposed into an ordered call
|
|
11
11
|
// sequence with a POP causal-link proof chain, then folded into ONE composed
|
|
12
12
|
// answer via the same set-algebra the HTN method names (relative-filter ->
|
|
13
|
-
// intersect; conditional -> fallback/guard). A
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
13
|
+
// intersect; conditional -> fallback/guard). A refused WORLD goal ("make every
|
|
14
|
+
// disk rest on peg-c") is tried against the taught capability records next
|
|
15
|
+
// (runTaughtPlan — selected by backward chaining, grounded by pure simulation,
|
|
16
|
+
// never dispatched). A request none of those ground escalates to the
|
|
17
|
+
// closed-world goal-reasoner — a maintenance-invariant deduction
|
|
18
|
+
// (coverage-gap / cochange-risk), never a keyword guess. Anything no stage
|
|
19
|
+
// grounds is an honest refuse, the same "grounded or an honest miss" contract
|
|
20
|
+
// as every other tmct answer path.
|
|
18
21
|
|
|
19
|
-
import { resolveOne } from "./resolver.mjs";
|
|
22
|
+
import { resolveOne, backwardChainWorld } from "./resolver.mjs";
|
|
20
23
|
import { plan, isMultiStep, decompose, MAX_STEPS } from "./planner.mjs";
|
|
21
24
|
import { goalReason } from "./goal-reasoner.mjs";
|
|
22
25
|
import { capabilities } from "./registry.mjs";
|
|
26
|
+
import { registerTaughtActions } from "./taught.mjs";
|
|
23
27
|
import { intersect, fallbackIfEmpty, guardIfEmpty, memberIndividuals, membersReaching, resultSetOf } from "./results.mjs";
|
|
24
28
|
import { resolveObject } from "../ask.mjs";
|
|
25
29
|
import { parseEntities } from "../codegraph.mjs";
|
|
26
30
|
import { dispatchTool } from "../server.mjs";
|
|
27
31
|
import { ToolError } from "../config.mjs";
|
|
32
|
+
import { loadMemory, readFactRows, readRuleRows } from "../memory/core.mjs";
|
|
33
|
+
import {
|
|
34
|
+
compileDomain, stateFromFacts, stateKeyFor, movesFromRules, compileGoal, PlanBudgetError,
|
|
35
|
+
} from "../domain.mjs";
|
|
36
|
+
import { findActionPath } from "../planning.mjs";
|
|
28
37
|
import * as defaultSource from "../source.mjs";
|
|
29
38
|
|
|
30
39
|
export const ROUTER_DRIVER = "resolver-0.8.0";
|
|
31
40
|
export const GOAL_DRIVER = "goal-0.8.1";
|
|
41
|
+
export const TAUGHT_DRIVER = "taught-0.1.0";
|
|
32
42
|
|
|
33
43
|
/** Every registered capability's name — the default declared toolset for a
|
|
34
44
|
* caller that doesn't want to hand-pick a subset. */
|
|
@@ -138,14 +148,102 @@ export async function runResolverPlan(request, tools, ctx) {
|
|
|
138
148
|
};
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
// ---- the taught world-goal lane -----------------------------------------------
|
|
152
|
+
|
|
153
|
+
// The one closed world-goal recognizer: "make/get (every|each|all)? <term>
|
|
154
|
+
// <verb>s? <prep> <object>". The preposition set mirrors chat.mjs's PREP_SRC
|
|
155
|
+
// but stays LOCAL and closed — this lane must never widen because a chat
|
|
156
|
+
// frame did, or the two surfaces drift apart silently instead of loudly.
|
|
157
|
+
const WORLD_PREP_SRC = "on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside";
|
|
158
|
+
const WORLD_GOAL_RE = new RegExp(
|
|
159
|
+
`^(?:make|get)\\s+(?:(every|each|all)\\s+)?([\\w-]+)\\s+([a-z]+?)s?\\s+(${WORLD_PREP_SRC})\\s+([\\w-]+)[.!?\\s]*$`,
|
|
160
|
+
"i",
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/** The taught world-goal lane: recognize "make every disk rest on peg-c",
|
|
164
|
+
* backward-chain the goal predicate to a REGISTERED taught capability record
|
|
165
|
+
* (the record src/router/taught.mjs bridged in is the thing consumed), then
|
|
166
|
+
* ground the move sequence by pure simulation over the taught rules
|
|
167
|
+
* (compileDomain + stateFromFacts + compileGoal + findActionPath — all
|
|
168
|
+
* read-only). Returned calls are NEVER dispatched: taught records carry
|
|
169
|
+
* readOnly:false / dispatchable:false, so the plan is simulated and chat's
|
|
170
|
+
* "next" executes move 1. Returns a loopResult, or null when the request is
|
|
171
|
+
* not a world-goal shape (the caller falls through to the goal-reasoner). */
|
|
172
|
+
export async function runTaughtPlan(request, tools, ctx) {
|
|
173
|
+
const m = WORLD_GOAL_RE.exec(String(request || "").trim());
|
|
174
|
+
if (!m) return null;
|
|
175
|
+
const universal = Boolean(m[1]);
|
|
176
|
+
const term = m[2].toLowerCase();
|
|
177
|
+
const predicate = `${m[3].toLowerCase()}-${m[4].toLowerCase()}`;
|
|
178
|
+
const object = m[5].toLowerCase();
|
|
179
|
+
|
|
180
|
+
const cap = backwardChainWorld(predicate);
|
|
181
|
+
if (!cap) {
|
|
182
|
+
return refuse(`the world goal needs a taught action whose effect achieves "${predicate}", and no taught: record with that world-effect is registered — teach the action rules first (honest miss)`, TAUGHT_DRIVER);
|
|
183
|
+
}
|
|
184
|
+
if (!tools.includes(cap.name)) {
|
|
185
|
+
return refuse(`selected ${cap.name} but it is not in the declared toolset`, TAUGHT_DRIVER);
|
|
186
|
+
}
|
|
187
|
+
if (!ctx.memoryDir) {
|
|
188
|
+
return refuse(`${cap.name} plans over a taught memory store, and this context carries none`, TAUGHT_DRIVER);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let domain;
|
|
192
|
+
let state;
|
|
193
|
+
let isGoal;
|
|
194
|
+
try {
|
|
195
|
+
const memory = await loadMemory(ctx.memoryDir);
|
|
196
|
+
const factRows = readFactRows(memory);
|
|
197
|
+
domain = compileDomain(factRows, readRuleRows(memory));
|
|
198
|
+
state = stateFromFacts(factRows, domain);
|
|
199
|
+
isGoal = compileGoal([{ universal, term, predicate, object }], domain);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return refuse(`the taught domain does not ground this goal: ${e?.message ?? e}`, TAUGHT_DRIVER);
|
|
202
|
+
}
|
|
203
|
+
if (!state.length) {
|
|
204
|
+
return refuse(`no current world state is taught yet — state the board first (e.g. "disk-1 rests on peg-a"), then re-ask`, TAUGHT_DRIVER);
|
|
205
|
+
}
|
|
206
|
+
let found;
|
|
207
|
+
try {
|
|
208
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth: 300, stateKey: stateKeyFor });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e instanceof PlanBudgetError) return refuse(`the taught search space is too large (${e.message}) — narrow the classes involved`, TAUGHT_DRIVER);
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
if (!found) {
|
|
214
|
+
return refuse(`no move sequence within 300 steps reaches the goal from the taught state (honest miss)`, TAUGHT_DRIVER);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const calls = found.actions.map((a) => ({ name: `taught:${a.name}`, input: { subject: a.subject, target: a.target } }));
|
|
218
|
+
for (const c of calls) {
|
|
219
|
+
if (!tools.includes(c.name)) return refuse(`the plan needs ${c.name}, which is not in the declared toolset`, TAUGHT_DRIVER);
|
|
220
|
+
}
|
|
221
|
+
const proof = [
|
|
222
|
+
{ step: "backward-chain", pred: "taught:world-effect", predicate, capability: cap.name, ok: true },
|
|
223
|
+
...calls.map((c, i) => ({ step: "effect", pred: "taught:world-effect", predicate, consumer: `step-${i + 1}:${c.name}`, ok: true })),
|
|
224
|
+
];
|
|
225
|
+
const why = [
|
|
226
|
+
`world goal (${universal ? "every " : ""}${term} ${predicate} ${object}) => backward-chain over taught:world-effect => ${cap.name}`,
|
|
227
|
+
`grounded by simulation: compileDomain + findActionPath over the taught rules (${calls.length} move${calls.length === 1 ? "" : "s"}, shortest)`,
|
|
228
|
+
];
|
|
229
|
+
return {
|
|
230
|
+
calls, refused: false, terminated: true, proof, driver: TAUGHT_DRIVER, why,
|
|
231
|
+
observed: `plan(taught): ${calls.length} move${calls.length === 1 ? "" : "s"} simulated over the taught rules — taught: calls are never dispatched; in chat, "next" executes move 1`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The full drive: resolver/planner first; a refusal there falls through to
|
|
236
|
+
* the taught world-goal lane (runTaughtPlan, above), and only a request that
|
|
237
|
+
* is not a world goal escalates to the closed-world goal-reasoner. Mirrors
|
|
238
|
+
* agentbench's driver-resolver.mjs + driver-goal.mjs composition, with no
|
|
239
|
+
* agentbench/ dependency (agentbench/ is dev-only, never shipped). Returns a
|
|
240
|
+
* loopResult:
|
|
145
241
|
* `{ calls, refused, terminated, proof, why, driver, composed?, observed? }`. */
|
|
146
242
|
export async function runCapabilityPlan(request, tools, ctx) {
|
|
147
243
|
const c1 = await runResolverPlan(request, tools, ctx);
|
|
148
244
|
if (!c1.refused) return c1;
|
|
245
|
+
const taught = await runTaughtPlan(request, tools, ctx);
|
|
246
|
+
if (taught) return taught.refused ? { ...taught, c1Why: c1.why } : taught;
|
|
149
247
|
const c2 = await goalReason(request, tools, ctx, { driver: GOAL_DRIVER });
|
|
150
248
|
// Both stages refused: carry the resolver/planner's own reason alongside the
|
|
151
249
|
// goal-reasoner's so a caller can show why the direct route AND the
|
|
@@ -163,8 +261,14 @@ export async function runCapabilityPlan(request, tools, ctx) {
|
|
|
163
261
|
* Pass an already-parsed `graph` (e.g. a chat session's own) to skip reloading
|
|
164
262
|
* it — mirrors the config -> source.fetchEntities -> parseEntities chain
|
|
165
263
|
* dispatchTool runs internally, so a passed-in graph must come from that same
|
|
166
|
-
* chain to stay consistent.
|
|
167
|
-
|
|
264
|
+
* chain to stay consistent.
|
|
265
|
+
*
|
|
266
|
+
* Pass a `memoryDir` to open the taught world-goal lane: the memory store's
|
|
267
|
+
* action families are registered as taught: capability records (idempotent —
|
|
268
|
+
* an already-registered name is skipped) and runTaughtPlan simulates over the
|
|
269
|
+
* same store. The new registrations' unregister disposers ride the ctx as
|
|
270
|
+
* `ctx.disposers`; the caller runs them when the ctx is done. */
|
|
271
|
+
export async function buildCapabilityPlanCtx({ config, source = defaultSource, tel = null, graph = null, memoryDir = null } = {}) {
|
|
168
272
|
const g = graph || parseEntities(await source.fetchEntities(config));
|
|
169
273
|
const resolve = (term) => resolveObject(g, term);
|
|
170
274
|
const dispatch = async (name, input) => {
|
|
@@ -179,5 +283,10 @@ export async function buildCapabilityPlanCtx({ config, source = defaultSource, t
|
|
|
179
283
|
throw e;
|
|
180
284
|
}
|
|
181
285
|
};
|
|
182
|
-
|
|
286
|
+
const ctx = { dispatch, resolve, graph: g, config };
|
|
287
|
+
if (memoryDir) {
|
|
288
|
+
ctx.memoryDir = memoryDir;
|
|
289
|
+
ctx.disposers = registerTaughtActions(await loadMemory(memoryDir));
|
|
290
|
+
}
|
|
291
|
+
return ctx;
|
|
183
292
|
}
|
package/src/router/guardrail.mjs
CHANGED
|
@@ -18,6 +18,11 @@ import { hallucinationsIn } from "./call-validator.mjs";
|
|
|
18
18
|
* undefined when there is no dispatcher to run it with. */
|
|
19
19
|
async function dispatchEachCandidate(pool, capName, arg, ctx) {
|
|
20
20
|
if (!ctx.dispatch) return undefined;
|
|
21
|
+
// Only readOnly capabilities may be dispatched here: the enrichment runs the
|
|
22
|
+
// SAME tool once per tied candidate, which is only safe when dispatch
|
|
23
|
+
// performs no writes. A registered world-mutating capability is planned
|
|
24
|
+
// over, never dispatched.
|
|
25
|
+
if (capabilityByName(capName)?.readOnly !== true) return undefined;
|
|
21
26
|
const results = [];
|
|
22
27
|
for (const c of pool) {
|
|
23
28
|
const res = await ctx.dispatch(capName, { [arg]: c.label });
|