@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -101
- package/ROADMAP.md +17 -4
- package/bin/tmct.mjs +115 -9
- package/data/games/crates.txt +24 -0
- package/data/games/hanoi-3.txt +30 -0
- package/package.json +1 -1
- package/src/ask-browser.bundle.js +129 -398
- package/src/chat.mjs +564 -12
- package/src/domain.mjs +271 -0
- package/src/import-file.mjs +86 -0
- package/src/init.mjs +46 -1
- package/src/ledger-viz.mjs +613 -0
- package/src/memory/core.mjs +80 -16
- package/src/memory/shacl.mjs +17 -7
- package/src/memory-ask-browser-entry.mjs +4 -2
- package/src/memory-ask-browser.bundle.js +5157 -1165
- package/src/plan-viz.mjs +410 -0
- package/src/router/guardrail.mjs +5 -0
- package/src/router/registry.mjs +55 -11
- package/src/router/taught.mjs +73 -0
- package/src/sentences.mjs +19 -0
- package/src/viz-theme.mjs +50 -0
- package/src/viz.mjs +2 -2
- package/src/wink-model.mjs +12 -6
package/src/domain.mjs
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// domain.mjs — the generic taught-action interpreter.
|
|
2
|
+
//
|
|
3
|
+
// Pure functions from taught rows to planner inputs: no I/O, and no knowledge
|
|
4
|
+
// of any particular game — every class, individual, predicate, and action
|
|
5
|
+
// arrives as data from the memory store's fact/Rule rows. Plugs into
|
|
6
|
+
// planning.mjs's findActionPath as its applyActions.
|
|
7
|
+
|
|
8
|
+
const MEMBER_EDGE_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
9
|
+
const SNAPSHOT_RE = /^(.+)@step(\d+)$/;
|
|
10
|
+
|
|
11
|
+
/** Trim a taught term defensively: some teach frames keep a sentence's
|
|
12
|
+
* trailing punctuation in the captured object. */
|
|
13
|
+
const normTerm = (value) => String(value ?? "").trim().replace(/[.!?]+$/, "");
|
|
14
|
+
|
|
15
|
+
/** Predicates in Rule slots are stored bare (normFactTerm strips prefixes);
|
|
16
|
+
* fact rows carry the prefixed form. */
|
|
17
|
+
const attachPrefix = (predicate) => {
|
|
18
|
+
const p = normTerm(predicate);
|
|
19
|
+
return p.includes(":") ? p : `mgx:${p}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const rowSort = (a, b) =>
|
|
23
|
+
a.subject.localeCompare(b.subject) ||
|
|
24
|
+
a.predicate.localeCompare(b.predicate) ||
|
|
25
|
+
a.object.localeCompare(b.object);
|
|
26
|
+
|
|
27
|
+
const normRow = (row) => ({
|
|
28
|
+
subject: normTerm(row.subject),
|
|
29
|
+
predicate: normTerm(row.predicate),
|
|
30
|
+
object: normTerm(row.object),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export class PlanBudgetError extends Error {
|
|
34
|
+
constructor(groundings, budget) {
|
|
35
|
+
super(`action grounding count ${groundings} exceeds the budget of ${budget}`);
|
|
36
|
+
this.name = "PlanBudgetError";
|
|
37
|
+
this.groundings = groundings;
|
|
38
|
+
this.budget = budget;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Compile fact + Rule rows into a planning domain:
|
|
43
|
+
* { actions, classMembers, dynamicPredicates, ordering }. */
|
|
44
|
+
export function compileDomain(factRows, ruleRows) {
|
|
45
|
+
const byName = new Map();
|
|
46
|
+
for (const rule of ruleRows || []) {
|
|
47
|
+
if (!String(rule.kind || "").startsWith("action-")) continue;
|
|
48
|
+
const name = normTerm(rule.name);
|
|
49
|
+
if (!byName.has(name)) byName.set(name, { name, signatures: [], preconds: [], effects: [] });
|
|
50
|
+
const family = byName.get(name);
|
|
51
|
+
const slots = rule.slots || {};
|
|
52
|
+
if (rule.kind === "action-signature") {
|
|
53
|
+
family.signatures.push({
|
|
54
|
+
subjectClass: normTerm(slots.subjectClass),
|
|
55
|
+
targetClass: normTerm(slots.targetClass),
|
|
56
|
+
});
|
|
57
|
+
} else if (rule.kind === "action-precond") {
|
|
58
|
+
family.preconds.push({
|
|
59
|
+
shape: normTerm(slots.shape),
|
|
60
|
+
predicate: attachPrefix(slots.predicate),
|
|
61
|
+
role: normTerm(slots.role),
|
|
62
|
+
scope: normTerm(slots.scope),
|
|
63
|
+
});
|
|
64
|
+
} else if (rule.kind === "action-effect") {
|
|
65
|
+
family.effects.push({
|
|
66
|
+
predicate: attachPrefix(slots.predicate),
|
|
67
|
+
subjectRole: normTerm(slots.subjectRole),
|
|
68
|
+
objectRole: normTerm(slots.objectRole),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const actions = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
for (const action of actions) {
|
|
74
|
+
action.signatures.sort((a, b) =>
|
|
75
|
+
a.subjectClass.localeCompare(b.subjectClass) || a.targetClass.localeCompare(b.targetClass));
|
|
76
|
+
action.preconds.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
77
|
+
action.effects.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Class membership from typing edges. A member is a subject with a typing
|
|
81
|
+
// edge into the class and no typing edge pointing at itself (a leaf).
|
|
82
|
+
const edges = (factRows || []).map(normRow).filter((r) => MEMBER_EDGE_PREDICATES.has(r.predicate));
|
|
83
|
+
const hasIncoming = new Set(edges.map((r) => r.object));
|
|
84
|
+
const classMembers = {};
|
|
85
|
+
for (const edge of edges) {
|
|
86
|
+
if (hasIncoming.has(edge.subject)) continue;
|
|
87
|
+
(classMembers[edge.object] ??= []).push(edge.subject);
|
|
88
|
+
}
|
|
89
|
+
for (const members of Object.values(classMembers)) {
|
|
90
|
+
members.sort();
|
|
91
|
+
// de-dup while keeping order
|
|
92
|
+
for (let i = members.length - 1; i > 0; i -= 1) if (members[i] === members[i - 1]) members.splice(i, 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const dynamicPredicates = new Set();
|
|
96
|
+
for (const action of actions) for (const effect of action.effects) dynamicPredicates.add(effect.predicate);
|
|
97
|
+
|
|
98
|
+
const ordering = (factRows || [])
|
|
99
|
+
.map(normRow)
|
|
100
|
+
.filter((r) => !dynamicPredicates.has(r.predicate) && !MEMBER_EDGE_PREDICATES.has(r.predicate))
|
|
101
|
+
.sort(rowSort);
|
|
102
|
+
|
|
103
|
+
return { actions, classMembers, dynamicPredicates, ordering };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const domainIndividuals = (domain) => {
|
|
107
|
+
const out = new Set();
|
|
108
|
+
for (const action of domain.actions) {
|
|
109
|
+
for (const sig of action.signatures) {
|
|
110
|
+
for (const m of domain.classMembers[sig.subjectClass] || []) out.add(m);
|
|
111
|
+
for (const m of domain.classMembers[sig.targetClass] || []) out.add(m);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** The current state as canonical sorted rows over the domain's dynamic
|
|
118
|
+
* predicates. Prefers the newest @stepN snapshot when one exists, so a
|
|
119
|
+
* re-plan after per-step execution never reads the stale step-0 board. */
|
|
120
|
+
export function stateFromFacts(factRows, domain) {
|
|
121
|
+
const individuals = domainIndividuals(domain);
|
|
122
|
+
const subjectClasses = new Set();
|
|
123
|
+
for (const action of domain.actions) for (const sig of action.signatures) subjectClasses.add(sig.subjectClass);
|
|
124
|
+
const subjects = new Set();
|
|
125
|
+
for (const cls of subjectClasses) for (const m of domain.classMembers[cls] || []) subjects.add(m);
|
|
126
|
+
|
|
127
|
+
const rows = (factRows || []).map(normRow).filter((r) => domain.dynamicPredicates.has(r.predicate));
|
|
128
|
+
let maxStep = -1;
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
const m = SNAPSHOT_RE.exec(row.subject);
|
|
131
|
+
if (m && individuals.has(m[1])) maxStep = Math.max(maxStep, Number(m[2]));
|
|
132
|
+
}
|
|
133
|
+
const state = [];
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
const m = SNAPSHOT_RE.exec(row.subject);
|
|
136
|
+
if (maxStep >= 0) {
|
|
137
|
+
if (!m || Number(m[2]) !== maxStep) continue;
|
|
138
|
+
const base = m[1];
|
|
139
|
+
if (subjects.has(base)) state.push({ subject: base, predicate: row.predicate, object: normTerm(row.object.replace(SNAPSHOT_RE, "$1")) });
|
|
140
|
+
} else if (!m && subjects.has(row.subject)) {
|
|
141
|
+
state.push(row);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
state.sort(rowSort);
|
|
145
|
+
return state;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Canonical identity for a state (rows are kept sorted). NUL-joined so
|
|
149
|
+
* multi-word terms can never collide with the separator; spelled without an
|
|
150
|
+
* escape sequence because tooling has twice turned a source-level \\0 into a
|
|
151
|
+
* literal NUL byte in this repo. */
|
|
152
|
+
const SEP = String.fromCharCode(0);
|
|
153
|
+
export function stateKeyFor(state) {
|
|
154
|
+
return state.map((r) => [r.subject, r.predicate, r.object].join(SEP)).join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const precondApplies = (precond, target, domain) =>
|
|
158
|
+
precond.scope === "any" || (domain.classMembers[precond.scope] || []).includes(target);
|
|
159
|
+
|
|
160
|
+
function precondHolds(precond, subject, target, state, domain) {
|
|
161
|
+
const roleTerm = precond.role === "target" ? target : subject;
|
|
162
|
+
if (precond.shape === "no-incoming") {
|
|
163
|
+
return !state.some((r) => r.predicate === precond.predicate && r.object === roleTerm);
|
|
164
|
+
}
|
|
165
|
+
if (precond.shape === "comparator") {
|
|
166
|
+
const left = roleTerm;
|
|
167
|
+
const right = precond.role === "target" ? subject : target;
|
|
168
|
+
return domain.ordering.some((r) =>
|
|
169
|
+
r.subject === left && r.predicate === precond.predicate && r.object === right);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function applyEffects(effects, subject, target, state) {
|
|
175
|
+
const roleTerm = (role) => (role === "target" ? target : subject);
|
|
176
|
+
let rows = state;
|
|
177
|
+
let changed = false;
|
|
178
|
+
for (const effect of effects) {
|
|
179
|
+
const effSubject = roleTerm(effect.subjectRole);
|
|
180
|
+
const effObject = roleTerm(effect.objectRole);
|
|
181
|
+
const already = rows.some((r) =>
|
|
182
|
+
r.subject === effSubject && r.predicate === effect.predicate && r.object === effObject);
|
|
183
|
+
if (already) continue;
|
|
184
|
+
rows = rows.filter((r) => !(r.subject === effSubject && r.predicate === effect.predicate));
|
|
185
|
+
rows = [...rows, { subject: effSubject, predicate: effect.predicate, object: effObject }];
|
|
186
|
+
changed = true;
|
|
187
|
+
}
|
|
188
|
+
if (!changed) return null;
|
|
189
|
+
return [...rows].sort(rowSort);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Every legal grounded action from `state`, with its successor.
|
|
193
|
+
* Deterministic: actions, signatures, and members are walked sorted. */
|
|
194
|
+
export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
195
|
+
let groundings = 0;
|
|
196
|
+
for (const action of domain.actions) {
|
|
197
|
+
for (const sig of action.signatures) {
|
|
198
|
+
groundings += (domain.classMembers[sig.subjectClass] || []).length *
|
|
199
|
+
(domain.classMembers[sig.targetClass] || []).length;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (groundings > budget) throw new PlanBudgetError(groundings, budget);
|
|
203
|
+
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const action of domain.actions) {
|
|
206
|
+
const [verb, particle] = action.name.split(/\s+/);
|
|
207
|
+
for (const sig of action.signatures) {
|
|
208
|
+
for (const subject of domain.classMembers[sig.subjectClass] || []) {
|
|
209
|
+
for (const target of domain.classMembers[sig.targetClass] || []) {
|
|
210
|
+
if (subject === target) continue;
|
|
211
|
+
let ok = true;
|
|
212
|
+
for (const precond of action.preconds) {
|
|
213
|
+
if (!precondApplies(precond, target, domain)) continue;
|
|
214
|
+
if (!precondHolds(precond, subject, target, state, domain)) { ok = false; break; }
|
|
215
|
+
}
|
|
216
|
+
if (!ok) continue;
|
|
217
|
+
const nextState = applyEffects(action.effects, subject, target, state);
|
|
218
|
+
if (!nextState) continue;
|
|
219
|
+
out.push({
|
|
220
|
+
action: {
|
|
221
|
+
name: action.name,
|
|
222
|
+
subject,
|
|
223
|
+
target,
|
|
224
|
+
label: [verb, subject, particle, target].filter(Boolean).join(" "),
|
|
225
|
+
},
|
|
226
|
+
nextState,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Compile goal specs ({universal, term, predicate, object}) into a pure
|
|
236
|
+
* state predicate. Satisfaction is a transitive walk along the goal
|
|
237
|
+
* predicate: a stacked member reaches the goal object through its support
|
|
238
|
+
* chain, which a direct row lookup cannot see. */
|
|
239
|
+
export function compileGoal(goalSpecs, domain) {
|
|
240
|
+
const specs = (goalSpecs || []).map((g) => ({
|
|
241
|
+
universal: Boolean(g.universal),
|
|
242
|
+
term: normTerm(g.term),
|
|
243
|
+
predicate: attachPrefix(g.predicate),
|
|
244
|
+
object: normTerm(g.object),
|
|
245
|
+
}));
|
|
246
|
+
const checks = [];
|
|
247
|
+
for (const spec of specs) {
|
|
248
|
+
const members = spec.universal ? domain.classMembers[spec.term] || [] : [spec.term];
|
|
249
|
+
if (spec.universal && members.length === 0) {
|
|
250
|
+
throw new Error(`the goal names "${spec.term}" as a class, but it has no known members`);
|
|
251
|
+
}
|
|
252
|
+
for (const member of members) checks.push({ member, predicate: spec.predicate, object: spec.object });
|
|
253
|
+
}
|
|
254
|
+
return function isGoal(state) {
|
|
255
|
+
for (const check of checks) {
|
|
256
|
+
let current = check.member;
|
|
257
|
+
let reached = false;
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
for (let hop = 0; hop <= state.length; hop += 1) {
|
|
260
|
+
if (seen.has(current)) break;
|
|
261
|
+
seen.add(current);
|
|
262
|
+
const row = state.find((r) => r.subject === current && r.predicate === check.predicate);
|
|
263
|
+
if (!row) break;
|
|
264
|
+
if (row.object === check.object) { reached = true; break; }
|
|
265
|
+
current = row.object;
|
|
266
|
+
}
|
|
267
|
+
if (!reached) return false;
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// import-file.mjs — `tmct import --file <definition.txt>`: teach a plain-text
|
|
2
|
+
// definition file, one sentence at a time, through the SAME recognizers the
|
|
3
|
+
// live chat uses (runTurn) — no separate parser, no guessing.
|
|
4
|
+
//
|
|
5
|
+
// The report is loud on purpose: a definition file that half-teaches produces
|
|
6
|
+
// a planner that finds wrong plans or no plans with no visible cause, so every
|
|
7
|
+
// sentence's outcome is printed and any decline makes the caller exit non-zero.
|
|
8
|
+
//
|
|
9
|
+
// `#` lines are comments (skipped, counted, never "declined") — a definition
|
|
10
|
+
// file carries its own example prompts this way.
|
|
11
|
+
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { basename, resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { runTurn, uuidv7 } from "./chat.mjs";
|
|
16
|
+
import { loadMemory, readFactRows, appendFact, openMemoryBackend } from "./memory/core.mjs";
|
|
17
|
+
import { loadConfig } from "./config.mjs";
|
|
18
|
+
import { splitSentences } from "./sentences.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Teach every sentence of `filePath` into `repoRoot`'s memory store.
|
|
22
|
+
*
|
|
23
|
+
* @returns {Promise<{
|
|
24
|
+
* sentences: number, taught: string[], declined: {sentence: string, reason: string}[],
|
|
25
|
+
* comments: number, report: string
|
|
26
|
+
* }>}
|
|
27
|
+
*/
|
|
28
|
+
export async function importDefinitionFile(repoRoot, filePath, { env = process.env } = {}) {
|
|
29
|
+
const root = resolve(repoRoot);
|
|
30
|
+
const abs = resolve(root, filePath);
|
|
31
|
+
const sourceTag = `import:${basename(abs)}`;
|
|
32
|
+
const text = await readFile(abs, "utf8");
|
|
33
|
+
|
|
34
|
+
const lines = text.split("\n");
|
|
35
|
+
const commentLines = lines.filter((l) => l.trim().startsWith("#"));
|
|
36
|
+
const body = lines.filter((l) => !l.trim().startsWith("#")).join("\n");
|
|
37
|
+
const sentences = splitSentences(body).map((s) => s.trim()).filter(Boolean);
|
|
38
|
+
|
|
39
|
+
const { loadTomlConfig } = await import("./toml-config.mjs");
|
|
40
|
+
const raw = await loadTomlConfig(root).catch(() => null);
|
|
41
|
+
const backend = String(raw?.memory?.backend || "default").trim().toLowerCase();
|
|
42
|
+
const { dir: memoryDir, close } = await openMemoryBackend(root, backend);
|
|
43
|
+
const config = loadConfig(env, root);
|
|
44
|
+
|
|
45
|
+
const taught = [];
|
|
46
|
+
const declined = [];
|
|
47
|
+
const reportLines = [`${basename(abs)} — ${sentences.length} sentence(s), ${commentLines.length} comment line(s) skipped`, ""];
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
for (const sentence of sentences) {
|
|
51
|
+
const before = readFactRows(await loadMemory(memoryDir));
|
|
52
|
+
const beforeById = new Map(before.map((r) => [r.id, r.provenance]));
|
|
53
|
+
const { record } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
54
|
+
const ok = record?.via === "assert" && !record?.miss;
|
|
55
|
+
if (!ok) {
|
|
56
|
+
const reason = String(record?.answer || "").split("\n")[0] || "not a recognized declarative shape";
|
|
57
|
+
declined.push({ sentence, reason });
|
|
58
|
+
reportLines.push(` DECLINED — ${sentence} — ${reason}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
taught.push(sentence);
|
|
62
|
+
reportLines.push(` taught — ${sentence}`);
|
|
63
|
+
// Layer the additive audit tag onto the fact rows this sentence touched
|
|
64
|
+
// (appendFact unions provenance by id — re-import is idempotent). Rule
|
|
65
|
+
// teaches touch no fact rows; the Rule's own provenance already names
|
|
66
|
+
// the teach source.
|
|
67
|
+
const after = readFactRows(await loadMemory(memoryDir));
|
|
68
|
+
const touched = after.filter((r) => beforeById.get(r.id) !== r.provenance);
|
|
69
|
+
for (const row of touched) {
|
|
70
|
+
await appendFact(memoryDir, {
|
|
71
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
72
|
+
provenance: sourceTag, quantifier: row.quantifier || "",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
await close();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
reportLines.push("");
|
|
81
|
+
reportLines.push(
|
|
82
|
+
`${taught.length} taught, ${declined.length} declined, ${commentLines.length} comment line(s) skipped`
|
|
83
|
+
+ (declined.length ? " — a half-taught game plans wrongly or not at all; fix the declined sentence(s) and re-import" : ""),
|
|
84
|
+
);
|
|
85
|
+
return { sentences: sentences.length, taught, declined, comments: commentLines.length, report: reportLines.join("\n") };
|
|
86
|
+
}
|
package/src/init.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// write the same `.tmct/memory/corpus-seed.json`, so whichever runs first wins. Re-declared
|
|
14
14
|
// here rather than imported, to keep init off chat.mjs's heavy module graph.
|
|
15
15
|
|
|
16
|
-
import { mkdir, readFile, writeFile, stat } from "node:fs/promises";
|
|
16
|
+
import { copyFile, mkdir, readFile, readdir, writeFile, stat } from "node:fs/promises";
|
|
17
17
|
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { stringify as stringifyToml } from "smol-toml";
|
|
@@ -197,6 +197,51 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// ---- 1b. Importable starters (.tmct/imports) — game definition files a
|
|
201
|
+
// fresh repo can discover by listing a directory, plus a README naming the
|
|
202
|
+
// corpus bundle ids `tmct import` accepts. Game files are copied (import
|
|
203
|
+
// --file consumes them directly); corpus bundles are listed by id, not
|
|
204
|
+
// copied — the wordnet-scale ones are far too large to scaffold into every
|
|
205
|
+
// repo, and `tmct import --corpus <id>` resolves ids without a local copy.
|
|
206
|
+
{
|
|
207
|
+
const importsDir = join(paths.tmct, "imports");
|
|
208
|
+
const gamesDir = join(importsDir, "games");
|
|
209
|
+
const shippedGames = join(dirname(fileURLToPath(import.meta.url)), "..", "data", "games");
|
|
210
|
+
if (!(await exists(gamesDir))) {
|
|
211
|
+
await mkdir(gamesDir, { recursive: true });
|
|
212
|
+
created.push(gamesDir);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
for (const f of await readdir(shippedGames)) {
|
|
216
|
+
if (!f.endsWith(".txt")) continue;
|
|
217
|
+
const dest = join(gamesDir, f);
|
|
218
|
+
if (!(await exists(dest))) {
|
|
219
|
+
await copyFile(join(shippedGames, f), dest);
|
|
220
|
+
created.push(dest);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch { /* no shipped games directory — scaffold stays empty */ }
|
|
224
|
+
const readmePath = join(importsDir, "README.txt");
|
|
225
|
+
if (!(await exists(readmePath))) {
|
|
226
|
+
await writeFile(readmePath, [
|
|
227
|
+
"Importable starters for this repo.",
|
|
228
|
+
"",
|
|
229
|
+
"Game definitions (plain controlled-English sentences; # lines are comments):",
|
|
230
|
+
" tmct import --file .tmct/imports/games/hanoi-3.txt",
|
|
231
|
+
"",
|
|
232
|
+
"Corpus bundles (activate by id — no local copy needed):",
|
|
233
|
+
" tmct import --corpus human | human-medium | human-large",
|
|
234
|
+
" tmct import --corpus seon | conceptnet | aws | python | java | general",
|
|
235
|
+
" tmct import --corpus wordnet-xl | wordnet-full | namenet (large: tens of thousands of facts)",
|
|
236
|
+
"",
|
|
237
|
+
"Ontology / lexicon resources are file paths declared as extension entries:",
|
|
238
|
+
" tmct import --ontology <path> | --lexicon <path>",
|
|
239
|
+
"",
|
|
240
|
+
].join("\n"));
|
|
241
|
+
created.push(readmePath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
200
245
|
// ---- 2. The externalised config (preserve an existing file unless force) ----
|
|
201
246
|
let config = defaultConfig();
|
|
202
247
|
// Persona overrides apply only to a fresh write.
|