@polycode-projects/the-mechanical-code-talker 2.8.1 → 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.
@@ -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
+ }