@polycode-projects/the-mechanical-code-talker 2.8.0 → 2.8.3
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 +30 -16
- package/corpus/tier2/generate.mjs +1 -0
- package/corpus/tier2/human.jsonl +1 -0
- package/corpus/tier2/manifest.json +3 -3
- package/package.json +2 -1
- package/src/adapters/corpus/sprite-large-template-files.mjs +6 -2
- package/src/domain/game-config.mjs +10 -4
- package/src/domain/hanoi-lesson.mjs +53 -0
- package/src/domain/spider-fly-world.mjs +16 -0
- package/src/domain/sprite-expressions.mjs +120 -0
- package/src/domain/sprite-templates.mjs +31 -9
- package/src/services/adventure-editor.mjs +361 -0
- package/src/services/adventure-viz.mjs +422 -51
- package/src/services/chat-page-viz.mjs +398 -0
- package/src/services/plan-pddl.mjs +245 -0
- package/src/services/plan-viz.mjs +324 -67
- package/src/services/spider-fly-turn.mjs +120 -3
- package/src/services/spider-fly-viz.mjs +341 -22
- package/src/services/spider-fly.mjs +337 -143
- package/src/services/sprite-catalog-viz.mjs +395 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +34 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +10 -4
- package/src/surfaces/web/plan-browser-entry.mjs +114 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +33 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// chat-page-viz.mjs — "Talk to it"'s full-screen destination
|
|
2
|
+
// (PLAN_GAMES_UPLIFT_V3.md Part C.4 item 1): a self-contained document shaped
|
|
3
|
+
// exactly like spider-fly-viz.mjs/adventure-viz.mjs's own page-builders — one
|
|
4
|
+
// inlined <style> importing viz-theme.mjs's shared tokens, behaviour as an
|
|
5
|
+
// inlined IIFE — reusing the SAME chat engine the home page's embedded
|
|
6
|
+
// #tmct-chat widget runs (chat-browser.bundle.js's globalThis.tmctChat,
|
|
7
|
+
// chat-seed.json, public/reference-pack/), referenced by the same same-origin
|
|
8
|
+
// relative paths chat-ui.mjs already uses. This module does not import or
|
|
9
|
+
// re-render chat-ui.mjs — it is a second, independent consumer of the same
|
|
10
|
+
// engine/bundle, the same relationship spider-fly-viz.mjs's own inlined chat
|
|
11
|
+
// dock already has with createSpiderFlySession (both call the shared
|
|
12
|
+
// session.turn(line), neither reimplements it).
|
|
13
|
+
//
|
|
14
|
+
// What's actually new here, versus the embedded widget: full-screen
|
|
15
|
+
// post-ChatGPT-style chrome (centered message column, bottom-fixed composer,
|
|
16
|
+
// message bubbles) and this page's own signature element — a quiet
|
|
17
|
+
// per-message PROVENANCE CHIP (taught / corpus / entailed) next to every
|
|
18
|
+
// grounded answer, tmct's actual differentiator versus an LLM chatbot. The
|
|
19
|
+
// chip is read straight off the SAME "(source: ...)" citation chat.mjs's own
|
|
20
|
+
// factPhrase/renderFactLine convention already appends to most answers (see
|
|
21
|
+
// e.g. `dog is a kind of animal (source: corpus:conceptnet ...)` — already
|
|
22
|
+
// asserted by e2e/pages-chat.test.mjs against the embedded widget), never a
|
|
23
|
+
// second provenance computation against memory internals: `provBucketFor`
|
|
24
|
+
// (ledger-viz.mjs) is spliced in unmodified and applied to whatever citation
|
|
25
|
+
// text the answer already carries, so this page's chip and the ledger's own
|
|
26
|
+
// per-row color always agree by construction. A miss carries no citation and
|
|
27
|
+
// gets no chip — the absence IS the signal, matching the product's own
|
|
28
|
+
// honest-miss posture rather than inventing a fourth "trust tier" to badge it.
|
|
29
|
+
//
|
|
30
|
+
// renderChatHtml() is pure: no I/O, deterministic output for identical
|
|
31
|
+
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
32
|
+
// to public/chat.html, after chat-browser.bundle.js/chat-seed.json already
|
|
33
|
+
// exist (both built earlier in that same script, for the embedded widget).
|
|
34
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
35
|
+
import { provBucketFor } from "./ledger-viz.mjs";
|
|
36
|
+
|
|
37
|
+
const DEFAULT_TITLE = "tmct — talk to it";
|
|
38
|
+
|
|
39
|
+
// Reads left-to-right as the reader meets each tier: what you taught it
|
|
40
|
+
// directly, what its bundled corpus already knew, what it worked out itself.
|
|
41
|
+
// Mirrors ledger-viz.mjs's own PROVS legend labels verbatim, so the same
|
|
42
|
+
// three words mean the same thing on both pages.
|
|
43
|
+
const PROV_LEGEND = [
|
|
44
|
+
["taught", "you taught"],
|
|
45
|
+
["corpus", "corpus"],
|
|
46
|
+
["entailed", "entailed"],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** This page's own tiny CSS-class fold — ledger-viz.mjs's `--entail` CSS
|
|
50
|
+
* custom property (viz-theme.mjs's own token name) is one letter short of
|
|
51
|
+
* the "entailed" bucket provBucketFor returns; every other tier's token name
|
|
52
|
+
* already matches its bucket name exactly. */
|
|
53
|
+
function provKey(tier) {
|
|
54
|
+
return tier === "entailed" ? "entail" : tier;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The chip tier for one chat turn — "taught" | "corpus" | "entailed" | null
|
|
59
|
+
* (no chip: a miss, or an answer that grounds in nothing citable, e.g. /help
|
|
60
|
+
* output or a focus-set confirmation).
|
|
61
|
+
*
|
|
62
|
+
* Reads the SAME "(source: ...)" citation(s) the visible answer text already
|
|
63
|
+
* carries, via `bucketFor` (the page's own spliced copy of ledger-viz.mjs's
|
|
64
|
+
* `provBucketFor`, injected rather than imported so this function stays
|
|
65
|
+
* `.toString()`-splice safe — the same discipline spider-fly-viz.mjs's own
|
|
66
|
+
* `threadCellsForSpiderPlan` holds its own injected `geometry` to).
|
|
67
|
+
* `provBucketFor`'s fallback branch (empty sourceTypes) classifies a raw
|
|
68
|
+
* legacy provenance TAG string directly — exactly the string chat.mjs embeds
|
|
69
|
+
* after "(source: " (memory/trust.mjs's `provenanceTagToSource` parses the
|
|
70
|
+
* identical shape) — so no Fact/individuals lookup is needed here at all.
|
|
71
|
+
*
|
|
72
|
+
* A teach-lane confirmation ("noted — remembered: ...") cites no fact yet to
|
|
73
|
+
* read back — nothing has been asked of it — so its own `record.via ===
|
|
74
|
+
* "assert"` stands in for a citation: the user just taught this, "taught" is
|
|
75
|
+
* the whole point of the reply.
|
|
76
|
+
*
|
|
77
|
+
* Multiple citations (a multi-step proof chain) resolve by the same
|
|
78
|
+
* taught-over-entailed-over-corpus precedence provBucketFor's own header
|
|
79
|
+
* documents for one fact's sourceTypes, applied across citations instead:
|
|
80
|
+
* a chain resting on any taught premise reads as "taught" overall.
|
|
81
|
+
*
|
|
82
|
+
* Self-contained (no outer refs beyond the injected `bucketFor`),
|
|
83
|
+
* `.toString()`-splice safe.
|
|
84
|
+
*/
|
|
85
|
+
export function provenanceChipFor(answer, record, bucketFor) {
|
|
86
|
+
if (!record || record.miss) return null;
|
|
87
|
+
const cites = [...String(answer || "").matchAll(/\(source: ([^)]+)\)/g)].map((m) => m[1]);
|
|
88
|
+
if (!cites.length) return record.via === "assert" ? "taught" : null;
|
|
89
|
+
const buckets = cites.map((c) => bucketFor([], c));
|
|
90
|
+
if (buckets.includes("taught")) return "taught";
|
|
91
|
+
if (buckets.includes("entailed")) return "entailed";
|
|
92
|
+
return "corpus";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The self-contained "talk to it" full-screen page. Pure — the same output
|
|
96
|
+
* for the same `title` every time; every other piece of state (the session,
|
|
97
|
+
* every message, every chip) is computed live in the browser once the
|
|
98
|
+
* sibling chat bundle loads, exactly as the embedded widget already works. */
|
|
99
|
+
export function renderChatHtml({ title = DEFAULT_TITLE } = {}) {
|
|
100
|
+
const legendHtml = PROV_LEGEND.map(
|
|
101
|
+
([key, label]) => `<span class="legend-item"><i class="dot dot-${provKey(key)}"></i>${escapeHtml(label)}</span>`,
|
|
102
|
+
).join("");
|
|
103
|
+
|
|
104
|
+
return `<!doctype html>
|
|
105
|
+
<html lang="en">
|
|
106
|
+
<head>
|
|
107
|
+
<meta charset="utf-8">
|
|
108
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
109
|
+
<title>${escapeHtml(title)}</title>
|
|
110
|
+
<style>
|
|
111
|
+
${THEME_TOKENS_CSS}
|
|
112
|
+
html, body { height: 100%; }
|
|
113
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; display: flex; flex-direction: column; overflow: hidden; }
|
|
114
|
+
.mono { font-family: ${MONO_STACK}; }
|
|
115
|
+
button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
|
|
116
|
+
button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
117
|
+
a { color: var(--corpus); }
|
|
118
|
+
|
|
119
|
+
.topbar { flex: 0 0 auto; display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: .7rem 1.1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
120
|
+
.brand { display: flex; align-items: baseline; gap: .55rem; }
|
|
121
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
122
|
+
.topbar h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
|
|
123
|
+
.legend { display: flex; gap: .8rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
124
|
+
.legend-item { display: inline-flex; align-items: center; gap: .32rem; white-space: nowrap; }
|
|
125
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
|
|
126
|
+
.dot-taught { background: var(--taught); } .dot-corpus { background: var(--corpus); } .dot-entail { background: var(--entail); }
|
|
127
|
+
|
|
128
|
+
main.chatMain { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; }
|
|
129
|
+
.messages { max-width: 720px; margin: 0 auto; padding: 1.2rem 1rem 1.6rem; display: flex; flex-direction: column; gap: .15rem; min-height: 100%; }
|
|
130
|
+
|
|
131
|
+
.msg-row { display: flex; flex-direction: column; margin: .35rem 0; max-width: 100%; }
|
|
132
|
+
.msg-row.user { align-items: flex-end; }
|
|
133
|
+
.msg-row.assistant { align-items: flex-start; }
|
|
134
|
+
.msg-row.system { align-items: center; margin: .6rem 0; }
|
|
135
|
+
|
|
136
|
+
.bubble { max-width: 80%; padding: .55rem .85rem; border-radius: 16px; white-space: pre-wrap; word-break: break-word; }
|
|
137
|
+
.bubble.user { background: var(--ink); color: var(--bg); border-bottom-right-radius: 4px; }
|
|
138
|
+
.bubble.user .prompt { opacity: .6; font-family: ${MONO_STACK}; font-size: .82em; }
|
|
139
|
+
.bubble.assistant { background: var(--card); border: 1px solid var(--line); border-bottom-left-radius: 4px; }
|
|
140
|
+
.bubble.assistant.pending { color: var(--muted); font-style: italic; }
|
|
141
|
+
.bubble.assistant.miss { color: var(--muted); border-style: dashed; }
|
|
142
|
+
.bubble.system { max-width: 100%; background: none; border: none; color: var(--muted); font-family: ${MONO_STACK}; font-size: .76rem; text-align: center; padding: .2rem .5rem; }
|
|
143
|
+
|
|
144
|
+
.provchip { align-self: flex-start; margin: .28rem 0 0 .15rem; font-family: ${MONO_STACK}; font-size: .64rem; letter-spacing: .05em; text-transform: uppercase; padding: .12rem .55rem; border-radius: 99px; cursor: default; }
|
|
145
|
+
.pc-taught { color: var(--taught); background: var(--taught-soft); }
|
|
146
|
+
.pc-corpus { color: var(--corpus); background: var(--corpus-soft); }
|
|
147
|
+
.pc-entail { color: var(--entail); background: var(--entail-soft); }
|
|
148
|
+
|
|
149
|
+
form.composer { flex: 0 0 auto; border-top: 1px solid var(--line); background: var(--bg); }
|
|
150
|
+
.composer-inner { max-width: 720px; margin: 0 auto; padding: .7rem 1rem; display: flex; gap: .5rem; align-items: center; }
|
|
151
|
+
.composer-inner input { flex: 1; min-width: 0; font-family: ${SERIF_STACK}; font-size: .95rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 20px; padding: .55rem 1rem; }
|
|
152
|
+
.composer-inner input::placeholder { color: var(--muted); }
|
|
153
|
+
.composer-inner input:disabled { opacity: .55; }
|
|
154
|
+
.composer-inner button[type="submit"] { width: 2.3rem; height: 2.3rem; border-radius: 50%; background: var(--ink); color: var(--bg); display: flex; align-items: center; justify-content: center; font-size: 1rem; flex: 0 0 auto; }
|
|
155
|
+
.composer-inner button[type="submit"]:disabled { opacity: .4; cursor: default; }
|
|
156
|
+
.statusline { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .6rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
|
|
157
|
+
|
|
158
|
+
@media (max-width: 560px) {
|
|
159
|
+
.legend { display: none; }
|
|
160
|
+
.bubble { max-width: 92%; }
|
|
161
|
+
}
|
|
162
|
+
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
* { scroll-behavior: auto !important; }
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
166
|
+
</head>
|
|
167
|
+
<body>
|
|
168
|
+
<header class="topbar">
|
|
169
|
+
<div class="brand">
|
|
170
|
+
<span class="eyebrow">tmct</span>
|
|
171
|
+
<h1>Talk to it</h1>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="legend" aria-hidden="true">${legendHtml}</div>
|
|
174
|
+
</header>
|
|
175
|
+
<main class="chatMain">
|
|
176
|
+
<div class="messages" id="messages" role="log" aria-live="polite" aria-label="Conversation"></div>
|
|
177
|
+
</main>
|
|
178
|
+
<form class="composer" id="composer">
|
|
179
|
+
<div class="composer-inner">
|
|
180
|
+
<input id="composerInput" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
|
|
181
|
+
placeholder="loading the engine…" aria-label="Ask tmct something" disabled>
|
|
182
|
+
<button type="submit" id="composerSend" aria-label="Send" disabled>→</button>
|
|
183
|
+
</div>
|
|
184
|
+
</form>
|
|
185
|
+
<div class="statusline" id="status">loading the engine…</div>
|
|
186
|
+
<script src="./chat-browser.bundle.js"></script>
|
|
187
|
+
<script>
|
|
188
|
+
(function () {
|
|
189
|
+
"use strict";
|
|
190
|
+
const provBucketFor = ${provBucketFor.toString()};
|
|
191
|
+
const provenanceChipFor = ${provenanceChipFor.toString()};
|
|
192
|
+
const el = (id) => document.getElementById(id);
|
|
193
|
+
|
|
194
|
+
const messagesEl = el("messages");
|
|
195
|
+
const composerForm = el("composer");
|
|
196
|
+
const inputEl = el("composerInput");
|
|
197
|
+
const sendBtn = el("composerSend");
|
|
198
|
+
const statusEl = el("status");
|
|
199
|
+
|
|
200
|
+
function scrollToEnd() {
|
|
201
|
+
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function addSystemLine(text) {
|
|
205
|
+
const row = document.createElement("div");
|
|
206
|
+
row.className = "msg-row system";
|
|
207
|
+
const bubble = document.createElement("div");
|
|
208
|
+
bubble.className = "bubble system";
|
|
209
|
+
bubble.textContent = text;
|
|
210
|
+
row.appendChild(bubble);
|
|
211
|
+
messagesEl.appendChild(row);
|
|
212
|
+
scrollToEnd();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function addUserBubble(text) {
|
|
216
|
+
const row = document.createElement("div");
|
|
217
|
+
row.className = "msg-row user";
|
|
218
|
+
const bubble = document.createElement("div");
|
|
219
|
+
bubble.className = "bubble user";
|
|
220
|
+
const prompt = document.createElement("span");
|
|
221
|
+
prompt.className = "prompt";
|
|
222
|
+
prompt.textContent = "tmct> ";
|
|
223
|
+
bubble.appendChild(prompt);
|
|
224
|
+
bubble.appendChild(document.createTextNode(text));
|
|
225
|
+
row.appendChild(bubble);
|
|
226
|
+
messagesEl.appendChild(row);
|
|
227
|
+
scrollToEnd();
|
|
228
|
+
return row;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addPendingAssistantBubble() {
|
|
232
|
+
const row = document.createElement("div");
|
|
233
|
+
row.className = "msg-row assistant";
|
|
234
|
+
const bubble = document.createElement("div");
|
|
235
|
+
bubble.className = "bubble assistant pending";
|
|
236
|
+
bubble.textContent = "thinking\\u2026";
|
|
237
|
+
row.appendChild(bubble);
|
|
238
|
+
messagesEl.appendChild(row);
|
|
239
|
+
scrollToEnd();
|
|
240
|
+
return row;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const CHIP_TITLE = {
|
|
244
|
+
taught: "you taught tmct this fact directly",
|
|
245
|
+
corpus: "grounded in tmct's bundled corpus",
|
|
246
|
+
entailed: "tmct derived this from taught facts, not read back verbatim",
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
function settleAssistantBubble(row, answer, record) {
|
|
250
|
+
const bubble = row.querySelector(".bubble");
|
|
251
|
+
bubble.classList.remove("pending");
|
|
252
|
+
const missed = !record || Boolean(record.miss);
|
|
253
|
+
bubble.classList.toggle("miss", missed);
|
|
254
|
+
bubble.textContent = answer;
|
|
255
|
+
const tier = provenanceChipFor(answer, record, provBucketFor);
|
|
256
|
+
if (tier) {
|
|
257
|
+
const key = tier === "entailed" ? "entail" : tier;
|
|
258
|
+
const chip = document.createElement("span");
|
|
259
|
+
chip.className = "provchip pc-" + key;
|
|
260
|
+
chip.title = CHIP_TITLE[tier] || "";
|
|
261
|
+
chip.textContent = tier;
|
|
262
|
+
row.appendChild(chip);
|
|
263
|
+
}
|
|
264
|
+
scrollToEnd();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---- engine boot -------------------------------------------------------
|
|
268
|
+
// Mirrors chat-ui.mjs's own boot sequence against the SAME bundle/seed/pack
|
|
269
|
+
// — a second consumer of the shared engine, not a fork of it.
|
|
270
|
+
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
271
|
+
const timeoutAfter = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
|
|
272
|
+
|
|
273
|
+
let winkStatus = "pending";
|
|
274
|
+
async function tryLoadWink() {
|
|
275
|
+
try {
|
|
276
|
+
const [{ default: winkNLP }, { default: model }] = await Promise.race([
|
|
277
|
+
Promise.all([import("wink-nlp"), import("wink-eng-lite-web-model")]),
|
|
278
|
+
timeoutAfter(WINK_LOAD_TIMEOUT_MS, "wink-nlp CDN load timed out"),
|
|
279
|
+
]);
|
|
280
|
+
window.tmctChat.registerWinkModel(() => ({ winkNLP, model }));
|
|
281
|
+
winkStatus = "loaded";
|
|
282
|
+
} catch (err) {
|
|
283
|
+
winkStatus = "unavailable";
|
|
284
|
+
console.warn("tmct chat: wink-nlp CDN load failed, continuing without the lemma/POS tier", err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let seedPayload = null;
|
|
289
|
+
let seedFacts = 0;
|
|
290
|
+
async function fetchSeed() {
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch("./chat-seed.json");
|
|
293
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
294
|
+
seedPayload = await res.json();
|
|
295
|
+
seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
296
|
+
} catch (err) {
|
|
297
|
+
seedPayload = null;
|
|
298
|
+
console.warn("tmct chat: chat-seed.json unavailable — starting unseeded", err);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const cloneSeed = () => {
|
|
302
|
+
if (!seedPayload) return null;
|
|
303
|
+
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
304
|
+
};
|
|
305
|
+
function newSession() {
|
|
306
|
+
return window.tmctChat.createChatSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload) });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let packIndexPromise = null;
|
|
310
|
+
function fetchPackIndex() {
|
|
311
|
+
if (!packIndexPromise) {
|
|
312
|
+
packIndexPromise = fetch("./reference-pack/index.json")
|
|
313
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
314
|
+
.catch(() => null);
|
|
315
|
+
}
|
|
316
|
+
return packIndexPromise;
|
|
317
|
+
}
|
|
318
|
+
const fetchPackProvider = {
|
|
319
|
+
async lookup(normTerm) {
|
|
320
|
+
const index = await fetchPackIndex();
|
|
321
|
+
const id = index && index.terms ? index.terms[String(normTerm || "")] : null;
|
|
322
|
+
if (!id) return null;
|
|
323
|
+
try {
|
|
324
|
+
const res = await fetch("./reference-pack/articles/" + id + ".json");
|
|
325
|
+
return res.ok ? await res.json() : null;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
function renderStatus() {
|
|
333
|
+
const seedPart = seedPayload
|
|
334
|
+
? "starter memory: " + seedFacts + " facts"
|
|
335
|
+
: "starter memory unavailable — starting empty";
|
|
336
|
+
const winkPart = winkStatus === "loaded"
|
|
337
|
+
? "wink-nlp lemma/POS tier: loaded"
|
|
338
|
+
: winkStatus === "unavailable"
|
|
339
|
+
? "wink-nlp unavailable — curated + fuzzy tiers only (still zero guesses, zero LLM)"
|
|
340
|
+
: "wink-nlp: loading\\u2026";
|
|
341
|
+
statusEl.textContent = seedPart + " \\u00b7 " + winkPart;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let busy = true;
|
|
345
|
+
function setBusy(v) {
|
|
346
|
+
busy = v;
|
|
347
|
+
const ready = Boolean(window.tmctChatSession);
|
|
348
|
+
inputEl.disabled = v || !ready;
|
|
349
|
+
sendBtn.disabled = v || !ready;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
composerForm.addEventListener("submit", (e) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
const q = inputEl.value.trim();
|
|
355
|
+
if (!q || busy || !window.tmctChatSession) return;
|
|
356
|
+
inputEl.value = "";
|
|
357
|
+
addUserBubble(q);
|
|
358
|
+
const pendingRow = addPendingAssistantBubble();
|
|
359
|
+
setBusy(true);
|
|
360
|
+
window.tmctChatSession.turn(q)
|
|
361
|
+
.then((result) => settleAssistantBubble(pendingRow, result.answer, result.record))
|
|
362
|
+
.catch((err) => settleAssistantBubble(pendingRow,
|
|
363
|
+
"something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
|
|
364
|
+
{ miss: true }))
|
|
365
|
+
.finally(() => {
|
|
366
|
+
setBusy(false);
|
|
367
|
+
inputEl.focus();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
async function boot() {
|
|
372
|
+
if (!window.tmctChat) {
|
|
373
|
+
statusEl.textContent = "the chat engine didn't load \\u2014 this page needs its build step (npm run demo:build)";
|
|
374
|
+
inputEl.placeholder = "chat engine unavailable";
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await Promise.all([fetchSeed(), tryLoadWink()]);
|
|
378
|
+
window.tmctChat.registerReferencePackProvider(fetchPackProvider);
|
|
379
|
+
window.tmctChatSession = newSession();
|
|
380
|
+
addSystemLine(seedPayload
|
|
381
|
+
? "tmct \\u2014 the real engine, running in this page \\u2014 " + seedFacts + " starter facts loaded"
|
|
382
|
+
: "tmct \\u2014 the real engine, running in this page \\u2014 no starter memory; starting empty");
|
|
383
|
+
inputEl.placeholder = seedPayload ? 'try "what is a dog"' : window.tmctChat.vocabExampleHint(false);
|
|
384
|
+
renderStatus();
|
|
385
|
+
setBusy(false);
|
|
386
|
+
inputEl.focus();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
window.tmctChatReady = boot().catch((err) => {
|
|
390
|
+
console.error("tmct chat failed to boot", err);
|
|
391
|
+
statusEl.textContent = "the chat failed to start (" + (err && err.message ? err.message : err) + ")";
|
|
392
|
+
});
|
|
393
|
+
})();
|
|
394
|
+
</script>
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// plan-pddl.mjs — a PDDL-style + OWL/RDF text rendering of a solved plan
|
|
2
|
+
// (the plan-lane contract chat.mjs's planLaneAnswer returns, PLAN_GAMES_
|
|
3
|
+
// UPLIFT_V3.md Part C.4 item 3): tmct's own richest textual account of what
|
|
4
|
+
// findActionPath actually consulted, for the "it plans, and shows the work"
|
|
5
|
+
// page's new text panel.
|
|
6
|
+
//
|
|
7
|
+
// Pure formatting over already-structured data (plan.actions/.states/.goal/
|
|
8
|
+
// .domain) — no I/O, no new tracking. Every fact line keeps its REAL
|
|
9
|
+
// predicate tag verbatim (mgx:rest-on, rdf:type, rdfs:subClassOf —
|
|
10
|
+
// plan-viz.mjs's own displayPredicate strips the "mgx:" prefix for the
|
|
11
|
+
// visual board; this renderer never does, on purpose: the point is showing
|
|
12
|
+
// the actual reasoning surface, not decorative syntax), and every
|
|
13
|
+
// :precondition/:effect block is a mechanical diff between two consecutive
|
|
14
|
+
// plan.states snapshots — nothing here infers a taught rule's own guard
|
|
15
|
+
// conditions that never surface as a fact-row change (e.g. "nothing may
|
|
16
|
+
// rest on the target" never toggles a row when it already holds, so it
|
|
17
|
+
// leaves no diff to show).
|
|
18
|
+
//
|
|
19
|
+
// The `rdf:type`/`rdfs:subClassOf` split for the :ontology block is read
|
|
20
|
+
// straight off domain.classMembers' own one-hop shape (compileDomain, see
|
|
21
|
+
// domain.mjs): a class-membership edge lands under `classMembers[object] =
|
|
22
|
+
// [...subjects]` for BOTH "X is a Y" (rdf:type) and "X is a kind of Y"
|
|
23
|
+
// (rdfs:subClassOf) teach frames alike, with no record of which frame taught
|
|
24
|
+
// it — so the split here is a real, testable structural fact (a member that
|
|
25
|
+
// is itself a declared class name is a class-to-class edge; anything else is
|
|
26
|
+
// an individual-to-class edge), not a guess.
|
|
27
|
+
|
|
28
|
+
const attachPrefix = (predicate) => {
|
|
29
|
+
const p = String(predicate ?? "").trim();
|
|
30
|
+
return p.includes(":") ? p : `mgx:${p}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const slug = (s) => String(s ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34
|
+
|
|
35
|
+
const factAtom = (r) => `(${r.predicate} ${r.subject} ${r.object})`;
|
|
36
|
+
const factKeyOf = (r) => `${r.subject} ${r.predicate} ${r.object}`;
|
|
37
|
+
|
|
38
|
+
/** Every class name domain.classMembers declares — a member that is also a
|
|
39
|
+
* key names a class-to-class edge; anything else is a plain individual. */
|
|
40
|
+
function classNamesOf(classMembers) {
|
|
41
|
+
return new Set(Object.keys(classMembers || {}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** {rdf:type, rdfs:subClassOf}-tagged edges, one per (member, class) pair,
|
|
45
|
+
* sorted for deterministic output. */
|
|
46
|
+
function ontologyEdges(classMembers) {
|
|
47
|
+
const classNames = classNamesOf(classMembers);
|
|
48
|
+
const edges = [];
|
|
49
|
+
for (const cls of Object.keys(classMembers || {}).sort()) {
|
|
50
|
+
for (const member of [...(classMembers[cls] || [])].sort()) {
|
|
51
|
+
edges.push({
|
|
52
|
+
predicate: classNames.has(member) ? "rdfs:subClassOf" : "rdf:type",
|
|
53
|
+
subject: member,
|
|
54
|
+
object: cls,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return edges;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `member`'s direct class (first match in sorted class-name order), or null
|
|
62
|
+
* when `member` is itself a class name (not an individual) or untyped. */
|
|
63
|
+
function directClassOf(classMembers, member) {
|
|
64
|
+
if (classNamesOf(classMembers).has(member)) return null;
|
|
65
|
+
for (const cls of Object.keys(classMembers || {}).sort()) {
|
|
66
|
+
if ((classMembers[cls] || []).includes(member)) return cls;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every plain individual (a member that is never itself a class name),
|
|
72
|
+
* sorted, deduplicated across every class it happens to appear under. */
|
|
73
|
+
function individualsOf(classMembers) {
|
|
74
|
+
const classNames = classNamesOf(classMembers);
|
|
75
|
+
const out = new Set();
|
|
76
|
+
for (const members of Object.values(classMembers || {})) {
|
|
77
|
+
for (const m of members || []) if (!classNames.has(m)) out.add(m);
|
|
78
|
+
}
|
|
79
|
+
return [...out].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The :objects block's lines — individuals grouped by their direct class,
|
|
83
|
+
* PDDL's own `a b c - type` typed-list shorthand. Untyped individuals (no
|
|
84
|
+
* direct class found) list on their own trailing line rather than being
|
|
85
|
+
* silently dropped. */
|
|
86
|
+
function objectsLines(classMembers) {
|
|
87
|
+
const byClass = new Map();
|
|
88
|
+
const untyped = [];
|
|
89
|
+
for (const member of individualsOf(classMembers)) {
|
|
90
|
+
const cls = directClassOf(classMembers, member);
|
|
91
|
+
if (!cls) { untyped.push(member); continue; }
|
|
92
|
+
if (!byClass.has(cls)) byClass.set(cls, []);
|
|
93
|
+
byClass.get(cls).push(member);
|
|
94
|
+
}
|
|
95
|
+
const lines = [...byClass.keys()].sort().map((cls) => ` ${byClass.get(cls).join(" ")} - ${cls}`);
|
|
96
|
+
if (untyped.length) lines.push(` ${untyped.join(" ")}`);
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A goal spec ({universal, term, predicate, object}) expanded into concrete
|
|
101
|
+
* ground atoms — a universal spec over every member domain.classMembers
|
|
102
|
+
* names for `term` (compileGoal's own expansion, domain.mjs), a non-
|
|
103
|
+
* universal spec as the single named atom. A universal term with no known
|
|
104
|
+
* members expands to nothing (never a placeholder atom naming an unknown
|
|
105
|
+
* member). */
|
|
106
|
+
function goalAtoms(specs, classMembers) {
|
|
107
|
+
const atoms = [];
|
|
108
|
+
for (const spec of specs || []) {
|
|
109
|
+
const predicate = attachPrefix(spec.predicate);
|
|
110
|
+
const members = spec.universal ? [...(classMembers?.[spec.term] || [])].sort() : [spec.term];
|
|
111
|
+
for (const member of members) atoms.push({ subject: member, predicate, object: spec.object });
|
|
112
|
+
}
|
|
113
|
+
return atoms;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One action's :precondition/:effect block as a mechanical diff between the
|
|
117
|
+
* before (`before`) and after (`after`) state snapshots — every fact row
|
|
118
|
+
* present before and absent after is a precondition that stopped holding
|
|
119
|
+
* (rendered as `(not …)` in the effect); every row absent before and
|
|
120
|
+
* present after is newly asserted. No inference beyond the two snapshots
|
|
121
|
+
* themselves — a rule's own guard conditions that never toggle a row (e.g.
|
|
122
|
+
* "nothing may rest on the target") leave no diff and so render nothing
|
|
123
|
+
* here; the taught rule's `becauseText` names them in prose instead. */
|
|
124
|
+
function diffAction(before, after) {
|
|
125
|
+
const beforeByKey = new Map((before || []).map((r) => [factKeyOf(r), r]));
|
|
126
|
+
const afterByKey = new Map((after || []).map((r) => [factKeyOf(r), r]));
|
|
127
|
+
const removed = [...beforeByKey.entries()].filter(([k]) => !afterByKey.has(k)).map(([, r]) => r);
|
|
128
|
+
const added = [...afterByKey.entries()].filter(([k]) => !beforeByKey.has(k)).map(([, r]) => r);
|
|
129
|
+
const bySubjObj = (a, b) => (a.subject === b.subject ? (a.object < b.object ? -1 : 1) : a.subject < b.subject ? -1 : 1);
|
|
130
|
+
return { removed: removed.sort(bySubjObj), added: added.sort(bySubjObj) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A PDDL-style `(define (problem …) …)` block plus one `(:action …)` per
|
|
135
|
+
* plan step, each carrying tmct's own real ontology tags — the richest
|
|
136
|
+
* textual form of a solved plan the engine can express, for a visitor to
|
|
137
|
+
* read alongside the visual block/circle render, not instead of it.
|
|
138
|
+
*
|
|
139
|
+
* `plan`: the plan-lane contract ({ actions, states, stepGoals, goal,
|
|
140
|
+
* domain: { classMembers, ordering } }) — see chat.mjs's planLaneAnswer.
|
|
141
|
+
* Returns a string; `""` for a plan with no actions and no init facts (an
|
|
142
|
+
* already-satisfied goal has nothing to narrate).
|
|
143
|
+
*/
|
|
144
|
+
export function planToPddl(plan, { problemName = "tmct-plan", domainName = "tmct-taught-domain" } = {}) {
|
|
145
|
+
const actions = plan?.actions || [];
|
|
146
|
+
const states = plan?.states || [];
|
|
147
|
+
const classMembers = plan?.domain?.classMembers || {};
|
|
148
|
+
const ordering = plan?.domain?.ordering || [];
|
|
149
|
+
const goalText = plan?.goal?.text || "";
|
|
150
|
+
const goal = goalAtoms(plan?.goal?.specs, classMembers);
|
|
151
|
+
const init = (states[0] || []);
|
|
152
|
+
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`;; tmct plan artifact — PDDL-style action sequence + OWL/RDF ontology tags`);
|
|
155
|
+
lines.push(`;; goal: ${goalText || "(none stated)"}`);
|
|
156
|
+
if (plan?.becauseText) lines.push(`;; because: ${plan.becauseText}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(`(define (problem ${slug(problemName) || "tmct-plan"})`);
|
|
159
|
+
lines.push(` (:domain ${slug(domainName) || "tmct-taught-domain"})`);
|
|
160
|
+
lines.push("");
|
|
161
|
+
|
|
162
|
+
const objLines = objectsLines(classMembers);
|
|
163
|
+
if (objLines.length) {
|
|
164
|
+
lines.push(" ;; :objects — individuals grounded through the taught class hierarchy");
|
|
165
|
+
lines.push(" (:objects");
|
|
166
|
+
lines.push(...objLines);
|
|
167
|
+
lines.push(" )");
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const edges = ontologyEdges(classMembers);
|
|
172
|
+
if (edges.length) {
|
|
173
|
+
lines.push(" ;; :ontology — the real rdf:type/rdfs:subClassOf rows compileDomain folded");
|
|
174
|
+
lines.push(" ;; into domain.classMembers (rdf:type = individual->class, rdfs:subClassOf =");
|
|
175
|
+
lines.push(" ;; class->superclass)");
|
|
176
|
+
lines.push(" (:ontology");
|
|
177
|
+
for (const e of edges) lines.push(` (${e.predicate} ${e.subject} ${e.object})`);
|
|
178
|
+
lines.push(" )");
|
|
179
|
+
lines.push("");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const orderingRows = [...ordering].sort((a, b) => (a.subject === b.subject ? (a.object < b.object ? -1 : 1) : a.subject < b.subject ? -1 : 1));
|
|
183
|
+
if (orderingRows.length) {
|
|
184
|
+
lines.push(" ;; :ordering — the real mgx:*-than facts the taught precondition consulted");
|
|
185
|
+
lines.push(" (:ordering");
|
|
186
|
+
for (const r of orderingRows) lines.push(` ${factAtom(r)}`);
|
|
187
|
+
lines.push(" )");
|
|
188
|
+
lines.push("");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (init.length) {
|
|
192
|
+
lines.push(" ;; :init — state@0, the taught starting board (real mgx:* predicate tags)");
|
|
193
|
+
lines.push(" (:init");
|
|
194
|
+
for (const r of [...init].sort((a, b) => (a.subject === b.subject ? (a.object < b.object ? -1 : 1) : a.subject < b.subject ? -1 : 1))) {
|
|
195
|
+
lines.push(` ${factAtom(r)}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push(" )");
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (goal.length) {
|
|
202
|
+
lines.push(" ;; :goal");
|
|
203
|
+
lines.push(" (:goal (and");
|
|
204
|
+
for (const a of goal) lines.push(` ${factAtom(a)}`);
|
|
205
|
+
lines.push(" ))");
|
|
206
|
+
}
|
|
207
|
+
lines.push(")");
|
|
208
|
+
|
|
209
|
+
if (actions.length) {
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push(`;; action sequence — findActionPath's own shortest path (${actions.length} move${actions.length === 1 ? "" : "s"})`);
|
|
212
|
+
actions.forEach((action, i) => {
|
|
213
|
+
const before = states[i] || [];
|
|
214
|
+
const after = states[i + 1] || [];
|
|
215
|
+
const { removed, added } = diffAction(before, after);
|
|
216
|
+
const name = `${slug(action.name) || "move"}-step${i + 1}`;
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(`(:action ${name}`);
|
|
219
|
+
lines.push(` :label "${action.label || `${action.name} ${action.subject} ${action.target}`}"`);
|
|
220
|
+
lines.push(` :subject ${action.subject}`);
|
|
221
|
+
lines.push(` :target ${action.target}`);
|
|
222
|
+
if (removed.length) {
|
|
223
|
+
lines.push(" :precondition (and");
|
|
224
|
+
for (const r of removed) lines.push(` ${factAtom(r)}`);
|
|
225
|
+
lines.push(" )");
|
|
226
|
+
} else {
|
|
227
|
+
lines.push(" :precondition (and)");
|
|
228
|
+
}
|
|
229
|
+
const effectLines = [
|
|
230
|
+
...removed.map((r) => ` (not ${factAtom(r)})`),
|
|
231
|
+
...added.map((r) => ` ${factAtom(r)}`),
|
|
232
|
+
];
|
|
233
|
+
if (effectLines.length) {
|
|
234
|
+
lines.push(" :effect (and");
|
|
235
|
+
lines.push(...effectLines);
|
|
236
|
+
lines.push(" )");
|
|
237
|
+
} else {
|
|
238
|
+
lines.push(" :effect (and)");
|
|
239
|
+
}
|
|
240
|
+
lines.push(")");
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return `${lines.join("\n")}\n`;
|
|
245
|
+
}
|