@polycode-projects/the-mechanical-code-talker 2.7.10 → 2.7.12

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.
@@ -4,7 +4,7 @@
4
4
  "counts": {
5
5
  "worlds": 2,
6
6
  "facts": 527,
7
- "rules": 14
7
+ "rules": 19
8
8
  },
9
9
  "budgets": {
10
10
  "sourceBytesPerWorld": 131072,
@@ -14,8 +14,8 @@
14
14
  "sources": [
15
15
  {
16
16
  "file": "src/ashcombe-hall.jsonl",
17
- "bytes": 7481,
18
- "sha256": "776634e8f7db704f2895ad1557cdcb79c28e14fe8fd4202ef1d0db9b4358ca91"
17
+ "bytes": 8352,
18
+ "sha256": "f141da008edb6abfe025ddf4753fc374ccf44bdf23e3a414741f33f7246a3c9a"
19
19
  },
20
20
  {
21
21
  "file": "src/spider-fly.jsonl",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  {
33
33
  "file": "shards/ashcombe-hall.jsonl.gz",
34
- "bytes": 864,
35
- "sha256": "468ba9ae859fc63094f5030648f07885e1e0785fb4e18e309cc4e6729c878ebf"
34
+ "bytes": 948,
35
+ "sha256": "49bbdf7bd0008c916145e866cd174d334c97ec449bd8ec850d10985468653c25"
36
36
  },
37
37
  {
38
38
  "file": "shards/spider-fly.jsonl.gz",
@@ -60,5 +60,10 @@
60
60
  {"world":"ashcombe-hall","kind":"rule","name":"give","ruleKind":"action-signature","slots":{"subjectClass":"portable","targetClass":"person"}}
61
61
  {"world":"ashcombe-hall","kind":"rule","name":"give","ruleKind":"action-effect","slots":{"predicate":"located-in","subjectRole":"subject","objectRole":"target"}}
62
62
  {"world":"ashcombe-hall","kind":"rule","name":"open","ruleKind":"action-signature","slots":{"subjectClass":"adventurer","targetClass":"furniture"}}
63
+ {"world":"ashcombe-hall","kind":"rule","name":"open","ruleKind":"action-precond","slots":{"shape":"fact-value","predicate":"stands-locked-in","role":"target","scope":"any","negate":"true"}}
64
+ {"world":"ashcombe-hall","kind":"rule","name":"open","ruleKind":"action-precond","slots":{"shape":"fact-value","predicate":"is-open","role":"target","scope":"any","value":"true","negate":"true"}}
65
+ {"world":"ashcombe-hall","kind":"rule","name":"open","ruleKind":"action-effect","slots":{"predicate":"is-open","subjectRole":"target","value":"true"}}
63
66
  {"world":"ashcombe-hall","kind":"rule","name":"unlock","ruleKind":"action-signature","slots":{"subjectClass":"adventurer","targetClass":"furniture"}}
64
67
  {"world":"ashcombe-hall","kind":"rule","name":"close","ruleKind":"action-signature","slots":{"subjectClass":"adventurer","targetClass":"furniture"}}
68
+ {"world":"ashcombe-hall","kind":"rule","name":"close","ruleKind":"action-precond","slots":{"shape":"fact-value","predicate":"is-open","role":"target","scope":"any","value":"true"}}
69
+ {"world":"ashcombe-hall","kind":"rule","name":"close","ruleKind":"action-effect","slots":{"predicate":"is-open","subjectRole":"target","value":"false"}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.7.10",
3
+ "version": "2.7.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -1287,10 +1287,18 @@ const RULE_SLOT_SPEC = {
1287
1287
  [RULE_KIND_ACTION_PRECOND]: [
1288
1288
  ["shape", "mgx:ruleActionPrecondShape"], ["predicate", "mgx:ruleActionPrecondPredicate"],
1289
1289
  ["role", "mgx:ruleActionPrecondRole"], ["scope", "mgx:ruleActionPrecondScope"],
1290
+ // value/negate: the "fact-value" shape's literal-match and negation
1291
+ // slots. Optional (RULE_SLOT_OPTIONAL below) — no-incoming/comparator
1292
+ // preconds never set them.
1293
+ ["value", "mgx:ruleActionPrecondValue"], ["negate", "mgx:ruleActionPrecondNegate"],
1290
1294
  ],
1291
1295
  [RULE_KIND_ACTION_EFFECT]: [
1292
1296
  ["predicate", "mgx:ruleActionEffectPredicate"], ["subjectRole", "mgx:ruleActionEffectSubject"],
1293
- ["objectRole", "mgx:ruleActionEffectObject"],
1297
+ // objectRole/value: exactly one of these two must be set (enforced
1298
+ // below, not by this per-slot spec) — a role-bound effect (Hanoi's
1299
+ // "rest-on") supplies objectRole, a literal datatype effect (Ashcombe's
1300
+ // is-open = "true") supplies value instead.
1301
+ ["objectRole", "mgx:ruleActionEffectObject"], ["value", "mgx:ruleActionEffectValue"],
1294
1302
  ],
1295
1303
  // "the <left> may not be with the <right> without the <guard>" — each slot
1296
1304
  // names a class whose sole member src/domain/domain.mjs resolves at compile time.
@@ -1300,25 +1308,47 @@ const RULE_SLOT_SPEC = {
1300
1308
  ],
1301
1309
  };
1302
1310
 
1311
+ // Slots a kind's RULE_SLOT_SPEC lists but does NOT require non-empty — the
1312
+ // literal-effect/fact-value extension's slots, added after the original
1313
+ // four-kind design. A pre-existing rule never sets them, so it reads back
1314
+ // with these keys simply absent (readRuleRows defaults an absent slot to
1315
+ // ""), the same "not supplied" signal domain.mjs's compileDomain gives an
1316
+ // explicit "" — see optionalTerm there.
1317
+ const RULE_SLOT_OPTIONAL = {
1318
+ [RULE_KIND_ACTION_PRECOND]: new Set(["value", "negate"]),
1319
+ [RULE_KIND_ACTION_EFFECT]: new Set(["objectRole", "value"]),
1320
+ };
1321
+
1303
1322
  // Content-addressed over (kind, name, ...slots in RULE_SLOT_SPEC order),
1304
1323
  // mirroring factIdFor's NUL-delimited discipline: identical rules upsert,
1305
1324
  // different ones coexist. For 2-slot kinds the joined string is byte-identical
1306
1325
  // to the historical (kind, name, slot1, slot2) template, so pre-existing rule
1307
- // ids never change (pinned by test/adapters/memory-rules-action.test.mjs).
1326
+ // ids never change (pinned by test/adapters/memory-rules-action.test.mjs);
1327
+ // action-precond/action-effect ids shifted when their optional slots joined
1328
+ // the spec above (their own round-trip test asserts dedup behavior, never a
1329
+ // specific hash, so this was never a promise for those two kinds).
1308
1330
  const ruleIdFor = (kind, name, slotValues) => `rule:${fnv1aHex([kind, name, ...slotValues].join("\0"))}`;
1309
1331
 
1310
1332
  /** Append one taught RULE — a sibling of appendFact storing a `Rule`
1311
1333
  * individual, same upsert/provenance/trust/SHACL discipline (neither
1312
1334
  * pipeline ever checks `individual.class`). `slots` is the matching
1313
- * per-kind object (RULE_SLOT_SPEC above). Returns { id }. */
1335
+ * per-kind object (RULE_SLOT_SPEC above); a slot named in RULE_SLOT_OPTIONAL
1336
+ * may be omitted. Returns { id }. */
1314
1337
  export async function appendRule(dir, { name, kind, slots, provenance = "", createdAt = "" } = {}) {
1315
1338
  const spec = RULE_SLOT_SPEC[kind];
1316
1339
  if (!spec) throw new Error(`a rule kind must be one of ${RULE_KINDS.join(", ")}, got ${JSON.stringify(kind)}`);
1317
1340
  const n = normFactTerm(name);
1318
1341
  if (!n) throw new Error("a rule needs a name");
1342
+ const optional = RULE_SLOT_OPTIONAL[kind] || new Set();
1319
1343
  const slotValues = spec.map(([slotKey]) => normFactTerm(slots?.[slotKey]));
1320
- if (slotValues.some((v) => !v)) {
1321
- throw new Error(`a ${kind} rule needs ${spec.map(([slotKey]) => slotKey).join(" + ")}`);
1344
+ const missing = spec.filter(([slotKey], i) => !optional.has(slotKey) && !slotValues[i]);
1345
+ if (missing.length) {
1346
+ throw new Error(`a ${kind} rule needs ${missing.map(([slotKey]) => slotKey).join(" + ")}`);
1347
+ }
1348
+ if (kind === RULE_KIND_ACTION_EFFECT) {
1349
+ const objectRole = slotValues[spec.findIndex(([k]) => k === "objectRole")];
1350
+ const value = slotValues[spec.findIndex(([k]) => k === "value")];
1351
+ if (!objectRole && !value) throw new Error(`a ${kind} rule needs an objectRole or a value`);
1322
1352
  }
1323
1353
  const id = ruleIdFor(kind, n, slotValues);
1324
1354
  const label = labelOf(`${n} = ${kind}(${slotValues.join(", ")})`);
@@ -1336,7 +1366,13 @@ export async function appendRule(dir, { name, kind, slots, provenance = "", crea
1336
1366
  { prop: "rdf:type", key: "type", value: "owl:NamedIndividual" },
1337
1367
  { prop: RULE_NAME_PROP, key: "ruleName", value: n },
1338
1368
  { prop: RULE_KIND_PROP, key: "ruleKind", value: kind },
1339
- ...spec.map(([slotKey, prop], i) => ({ prop, key: slotKey, value: slotValues[i] })),
1369
+ // An optional slot left empty stores no attribute at all — the same
1370
+ // "never supplied" shape a pre-extension Rule individual already has,
1371
+ // rather than a wasted always-"" one.
1372
+ ...spec
1373
+ .map(([slotKey, prop], i) => ({ prop, key: slotKey, value: slotValues[i], skip: !slotValues[i] && optional.has(slotKey) }))
1374
+ .filter((attr) => !attr.skip)
1375
+ .map(({ skip, ...attr }) => attr),
1340
1376
  { prop: CREATED_AT_PROP, key: "createdAt", value: createdAtVal },
1341
1377
  ...(provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []),
1342
1378
  ],
@@ -11,9 +11,13 @@ const RULE_KINDS = new Set([
11
11
  "action-signature", "action-precond", "action-effect", "action-constraint",
12
12
  ]);
13
13
 
14
- // Mirrors core.mjs's own (unexported) RULE_SLOT_SPEC exactly the single
15
- // source of truth for the closed rule-kind shapes; kept in sync by hand
16
- // (both describe the same kinds' slots).
14
+ // Mirrors the REQUIRED subset of core.mjs's own (unexported) RULE_SLOT_SPEC —
15
+ // kept in sync by hand. Two kinds there also carry optional slots
16
+ // (RULE_SLOT_OPTIONAL: action-precond's value/negate, action-effect's
17
+ // objectRole/value) that deliberately do NOT appear here: this gate only
18
+ // checks genuine malformation (the comment above), and "at least one of
19
+ // objectRole/value" is a joint constraint core.mjs's own appendRule already
20
+ // enforces at write time, not a per-slot presence check this shape can express.
17
21
  const RULE_SLOT_PROPS = {
18
22
  compose2: ["mgx:ruleBase1", "mgx:ruleBase2"],
19
23
  filter: ["mgx:ruleBase1", "mgx:ruleFilterProperty"],
@@ -23,9 +27,7 @@ const RULE_SLOT_PROPS = {
23
27
  "mgx:ruleActionPrecondShape", "mgx:ruleActionPrecondPredicate",
24
28
  "mgx:ruleActionPrecondRole", "mgx:ruleActionPrecondScope",
25
29
  ],
26
- "action-effect": [
27
- "mgx:ruleActionEffectPredicate", "mgx:ruleActionEffectSubject", "mgx:ruleActionEffectObject",
28
- ],
30
+ "action-effect": ["mgx:ruleActionEffectPredicate", "mgx:ruleActionEffectSubject"],
29
31
  "action-constraint": [
30
32
  "mgx:ruleActionConstraintLeft", "mgx:ruleActionConstraintRight", "mgx:ruleActionConstraintGuard",
31
33
  ],
@@ -19,6 +19,15 @@ const attachPrefix = (predicate) => {
19
19
  return p.includes(":") ? p : `mgx:${p}`;
20
20
  };
21
21
 
22
+ /** A genuinely-supplied optional slot (a literal precond/effect value): `""`,
23
+ * `null` and `undefined` all read as "not supplied", the same signal a
24
+ * never-taught slot and a readRuleRows-defaulted `""` both give. */
25
+ const optionalTerm = (value) => {
26
+ if (value == null) return undefined;
27
+ const t = normTerm(value);
28
+ return t === "" ? undefined : t;
29
+ };
30
+
22
31
  const rowSort = (a, b) =>
23
32
  a.subject.localeCompare(b.subject) ||
24
33
  a.predicate.localeCompare(b.predicate) ||
@@ -55,17 +64,32 @@ export function compileDomain(factRows, ruleRows) {
55
64
  targetClass: normTerm(slots.targetClass),
56
65
  });
57
66
  } else if (rule.kind === "action-precond") {
67
+ // value/negate are optional: absent in every no-incoming/comparator row
68
+ // taught so far, so their key is omitted rather than written as "" —
69
+ // an omitted key keeps a pre-existing precond's JSON.stringify sort
70
+ // form (and its precondHolds behavior) byte-identical to before this
71
+ // shape existed. normFactTerm/readRuleRows read a genuinely absent
72
+ // slot back as "", the same signal an explicit "" would give.
73
+ const value = optionalTerm(slots.value);
74
+ const negate = String(slots.negate) === "true" ? true : undefined;
58
75
  family.preconds.push({
59
76
  shape: normTerm(slots.shape),
60
77
  predicate: attachPrefix(slots.predicate),
61
78
  role: normTerm(slots.role),
62
79
  scope: normTerm(slots.scope),
80
+ ...(value !== undefined ? { value } : {}),
81
+ ...(negate !== undefined ? { negate } : {}),
63
82
  });
64
83
  } else if (rule.kind === "action-effect") {
84
+ // value is the literal-effect alternative to objectRole (a datatype
85
+ // write, e.g. mgx:is-open = "true", rather than a role binding) — same
86
+ // omit-when-absent discipline as the precond fields above.
87
+ const value = optionalTerm(slots.value);
65
88
  family.effects.push({
66
89
  predicate: attachPrefix(slots.predicate),
67
90
  subjectRole: normTerm(slots.subjectRole),
68
91
  objectRole: normTerm(slots.objectRole),
92
+ ...(value !== undefined ? { value } : {}),
69
93
  });
70
94
  } else if (rule.kind === "action-constraint") {
71
95
  family.constraints.push({
@@ -111,7 +135,11 @@ export function compileDomain(factRows, ruleRows) {
111
135
  };
112
136
  for (const action of actions) {
113
137
  for (const effect of action.effects) {
114
- for (const role of [effect.subjectRole, effect.objectRole]) {
138
+ // A literal-valued effect (effect.value set) has no objectRole to bind
139
+ // — the literal IS the object, so objectRole (unused, "" when the
140
+ // teaching row never set it) never enters the class-membership check.
141
+ const roles = effect.value !== undefined ? [effect.subjectRole] : [effect.subjectRole, effect.objectRole];
142
+ for (const role of roles) {
115
143
  if (role !== "subject" && role !== "target") requireSoleMember(role, `an effect role of "${action.name}"`);
116
144
  }
117
145
  }
@@ -187,7 +215,13 @@ export function stateKeyFor(state) {
187
215
  const precondApplies = (precond, target, domain) =>
188
216
  precond.scope === "any" || (domain.classMembers[precond.scope] || []).includes(target);
189
217
 
190
- function precondHolds(precond, subject, target, state, domain) {
218
+ /** A precondition on a fact ABOUT the role term itself (roleTerm as subject),
219
+ * as opposed to "no-incoming"'s fact pointing AT the role term (roleTerm as
220
+ * object). Datatype/state checks — a lock, an open/closed flag — read this
221
+ * way: "does (roleTerm, predicate, value) exist", with `value` an existence
222
+ * wildcard when omitted and `negate` flipping the sense ("must NOT exist"
223
+ * covers "must not be locked", "must not already be open"). */
224
+ export function precondHolds(precond, subject, target, state, domain) {
191
225
  const roleTerm = precond.role === "target" ? target : subject;
192
226
  if (precond.shape === "no-incoming") {
193
227
  return !state.some((r) => r.predicate === precond.predicate && r.object === roleTerm);
@@ -198,13 +232,21 @@ function precondHolds(precond, subject, target, state, domain) {
198
232
  return domain.ordering.some((r) =>
199
233
  r.subject === left && r.predicate === precond.predicate && r.object === right);
200
234
  }
235
+ if (precond.shape === "fact-value") {
236
+ const exists = state.some((r) => r.subject === roleTerm && r.predicate === precond.predicate
237
+ && (precond.value == null || r.object === precond.value));
238
+ return precond.negate ? !exists : exists;
239
+ }
201
240
  return false;
202
241
  }
203
242
 
204
243
  /** Ground an effect/constraint role word: "subject"/"target" bind the
205
244
  * grounding pair; any other word is class-bound and binds the class's sole
206
- * member — its companion semantics (compileDomain guarantees exactly one). */
207
- const roleBinding = (role, subject, target, domain) => {
245
+ * member — its companion semantics (compileDomain guarantees exactly one).
246
+ * Exported so a caller consulting one grounded precond/effect directly,
247
+ * rather than running a full movesFromRules search, can still resolve an
248
+ * effect's subject/object the same way the planner does. */
249
+ export const roleBinding = (role, subject, target, domain) => {
208
250
  if (role === "subject") return subject;
209
251
  if (role === "target") return target;
210
252
  return (domain.classMembers[role] || [])[0];
@@ -213,12 +255,15 @@ const roleBinding = (role, subject, target, domain) => {
213
255
  const positionIn = (rows, term, predicate) =>
214
256
  rows.find((r) => r.subject === term && r.predicate === predicate)?.object;
215
257
 
216
- function applyEffects(effects, subject, target, state, domain) {
258
+ export function applyEffects(effects, subject, target, state, domain) {
217
259
  let rows = state;
218
260
  let changed = false;
219
261
  for (const effect of effects) {
220
262
  const effSubject = roleBinding(effect.subjectRole, subject, target, domain);
221
- const effObject = roleBinding(effect.objectRole, subject, target, domain);
263
+ // A literal-valued effect (a taught datatype flag) writes its value
264
+ // directly; a role-valued effect resolves objectRole through the
265
+ // grounding pair / class-bound member instead.
266
+ const effObject = effect.value !== undefined ? effect.value : roleBinding(effect.objectRole, subject, target, domain);
222
267
  const already = rows.some((r) =>
223
268
  r.subject === effSubject && r.predicate === effect.predicate && r.object === effObject);
224
269
  if (already) continue;
@@ -502,7 +502,15 @@ const VERB_SYNONYMS = new Map([
502
502
  // consumed whole before any shorter prefix inside it gets a chance to match.
503
503
  const VERB_SYNONYM_MAX_PREFIX = Math.max(...[...VERB_SYNONYMS.keys()].map((k) => k.split(" ").length));
504
504
 
505
- const VERB_FUZZY_CANDIDATES = [...new Set([...IMPERATIVE_VERBS, ...VERB_SYNONYMS.values()])];
505
+ // Every leading SURFACE word the exact tiers below recognise as the start of
506
+ // a command: a bare IMPERATIVE_VERBS word, or the first token of any
507
+ // VERB_SYNONYMS key (single- or multi-word alike — "pick", "have", "chat" are
508
+ // each only ever a key's own first word, never a key on their own, so they'd
509
+ // otherwise have nothing to fuzzy-repair a typo onto). Corrects the SURFACE
510
+ // word, not the canonical verb, so the existing exact-match tiers below (in
511
+ // particular the multi-word prefix loop) still resolve the rest of the phrase
512
+ // after repair — "hav a look at" -> "have a look at" -> examine, not just "look".
513
+ const VERB_SURFACE_WORDS = [...new Set([...IMPERATIVE_VERBS, ...[...VERB_SYNONYMS.keys()].map((k) => k.split(" ")[0])])];
506
514
  const IMPERATIVE_FUZZY_BOUND = 1; // this project's existing distance-1 tolerance elsewhere (interpret/fuzzy.mjs)
507
515
 
508
516
  // A small, hand-checked exemption (same idiom as this file's own
@@ -513,31 +521,48 @@ const IMPERATIVE_FUZZY_BOUND = 1; // this project's existing distance-1 toleranc
513
521
  // Checked against this repo's own WordNet corpus, not guessed.
514
522
  const NEVER_FUZZY_VERB = new Set(["walk", "wake", "make"]);
515
523
 
516
- /** Resolve the leading verb token(s) to a canonical, recognised verb: the
517
- * 2-token VERB_SYNONYMS prefix FIRST (so "talk to"/"look at" resolve as
518
- * themselves rather than as the bare exact verbs "talk"/"look" they'd
519
- * otherwise short-circuit on, leaving their "to"/"at" stranded in the object
520
- * phrase), then an exact IMPERATIVE_VERBS match, then the 1-token
521
- * VERB_SYNONYMS form, then a bounded fuzzy typo repair against the same
522
- * vocabulary. `consumed` is how many leading tokens the match ate (1, or 2
523
- * for a two-word synonym phrase); `corrected` is non-null only for the
524
- * fuzzy tier. Null when nothing at all recognises the leading token(s). */
525
- function resolveImperativeVerb(toks) {
524
+ /** The three deterministic (non-fuzzy) tiers: a 2+-token VERB_SYNONYMS prefix
525
+ * FIRST (so "talk to"/"look at" resolve as themselves rather than as the
526
+ * bare exact verbs "talk"/"look" they'd otherwise short-circuit on, leaving
527
+ * their "to"/"at" stranded in the object phrase), then an exact
528
+ * IMPERATIVE_VERBS match, then the 1-token VERB_SYNONYMS form. `consumed` is
529
+ * how many leading tokens the match ate. Null when none of the three match. */
530
+ function exactImperativeVerb(toks) {
526
531
  const first = toks[0].toLowerCase();
527
532
  const lower = toks.map((t) => t.toLowerCase());
528
533
  for (let n = Math.min(VERB_SYNONYM_MAX_PREFIX, toks.length); n >= 2; n -= 1) {
529
534
  const hit = VERB_SYNONYMS.get(lower.slice(0, n).join(" "));
530
- if (hit) return { verb: hit, consumed: n, corrected: null };
535
+ if (hit) return { verb: hit, consumed: n };
531
536
  }
532
- if (IMPERATIVE_VERBS.has(first)) return { verb: first, consumed: 1, corrected: null };
537
+ if (IMPERATIVE_VERBS.has(first)) return { verb: first, consumed: 1 };
533
538
  const oneHit = VERB_SYNONYMS.get(first);
534
- if (oneHit) return { verb: oneHit, consumed: 1, corrected: null };
535
- if (NEVER_FUZZY_VERB.has(first)) return null;
536
- const fuzzyHit = fuzzyMatchInSet(first, VERB_FUZZY_CANDIDATES, IMPERATIVE_FUZZY_BOUND);
537
- if (fuzzyHit) return { verb: fuzzyHit, consumed: 1, corrected: { from: first, to: fuzzyHit } };
539
+ if (oneHit) return { verb: oneHit, consumed: 1 };
538
540
  return null;
539
541
  }
540
542
 
543
+ /** Resolve the leading verb token(s) to a canonical, recognised verb: the
544
+ * three exact tiers above, failing that a bounded fuzzy TYPO repair
545
+ * (distance <= 1, via the existing interpret/fuzzy.mjs matcher) of the
546
+ * leading token against every recognised surface word, single- or
547
+ * multi-word idiom alike, then the exact tiers retried against the
548
+ * corrected token — so a typo'd idiom lead word ("hav a look at") is
549
+ * repaired to its real word first ("have a look at") and still resolves the
550
+ * whole idiom, not just a bare-verb guess. Only the fuzzy tier is a
551
+ * correction of an ERROR rather than a recognised alternate wording, so only
552
+ * it is reported back on the command (`corrected`) for the caller to name in
553
+ * its response — a synonym executes silently, a typo fix does not. */
554
+ function resolveImperativeVerb(toks) {
555
+ const exact = exactImperativeVerb(toks);
556
+ if (exact) return { ...exact, corrected: null };
557
+ const first = toks[0].toLowerCase();
558
+ if (NEVER_FUZZY_VERB.has(first)) return null;
559
+ const fixedFirst = fuzzyMatchInSet(first, VERB_SURFACE_WORDS, IMPERATIVE_FUZZY_BOUND);
560
+ if (!fixedFirst) return null;
561
+ const retried = exactImperativeVerb([fixedFirst, ...toks.slice(1)]);
562
+ if (!retried) return null;
563
+ return { ...retried, corrected: { from: first, to: fixedFirst } };
564
+ }
565
+
541
566
  /** Resolve one imperative object phrase to its bare lexicon term. */
542
567
  function imperativeNP(lexicon, tokens) {
543
568
  const np = resolveNP(lexicon, tokens);
@@ -12,6 +12,7 @@ import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
12
12
  import { parseImperative } from "../domain/grammar/ace.mjs";
13
13
  import { createCompletionsGraphAdapter } from "../domain/completions/graph-adapter.mjs";
14
14
  import { actionFamilies } from "../domain/router/taught.mjs";
15
+ import { compileDomain, precondHolds, roleBinding } from "../domain/domain.mjs";
15
16
  import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
16
17
  import { appendFacts, appendRule, loadMemory, normFactTerm, readFactRows, readRuleRows } from "../adapters/memory/core.mjs";
17
18
  import { COMPLETIONS_STORE, generateCompletion } from "./completions.mjs";
@@ -78,7 +79,12 @@ async function openAdventure(opening, { planHolder, memoryDir, sessionId, env, c
78
79
  world = names[0];
79
80
  } else if (!names.includes(world)) {
80
81
  // An unknown "play X" is not necessarily an adventure ask at all ("play
81
- // chess") fall through to the ordinary lanes rather than claim it.
82
+ // spider" is spider-fly's own opener, not a broken adventure request)
83
+ // fall through so a sibling game's opener keeps first refusal.
84
+ // unclaimedAdventureOpening below is chat's LAST-RESORT check: once
85
+ // every "play X" lane (this one included) has had its turn and none
86
+ // claimed the line, THAT'S when an unrecognized name gets named and
87
+ // declined honestly, never silently.
82
88
  return null;
83
89
  }
84
90
 
@@ -125,6 +131,31 @@ async function openAdventure(opening, { planHolder, memoryDir, sessionId, env, c
125
131
  };
126
132
  }
127
133
 
134
+ /** chat's LAST-RESORT check for a named opener no lane claimed — "play
135
+ * atlantis" when the pack has worlds but none is called that. Called AFTER
136
+ * every other "play X"-shaped lane (spider-fly's own opener among them) has
137
+ * already had its chance, so this never steals a name a sibling game
138
+ * recognizes as its own (openAdventure's own fallthrough above stays
139
+ * silent for exactly that reason). Only once nothing else wanted the line
140
+ * does it get named and declined, rather than answered with an unrelated
141
+ * generic non-answer. Null when the line isn't a named opener at all, or
142
+ * the pack has no worlds (openAdventure's own first pass already gave that
143
+ * case its honest missingPackAnswer, before any lane got a turn), or the
144
+ * name IS one of the pack's — never re-decides a real hit. */
145
+ export async function unclaimedAdventureOpening(line, { env }) {
146
+ const opening = matchAdventureOpening(line);
147
+ if (!opening?.world) return null;
148
+ const provider = getWorldsPackProvider(env);
149
+ let names = null;
150
+ try { names = await provider.list(); } catch { names = null; }
151
+ if (!names || !names.length || names.includes(opening.world)) return null;
152
+ return {
153
+ text: `I don't know a world called "${spokenNameOf(opening.world)}" — the pack has: ${names.map(spokenNameOf).join(", ")}.`,
154
+ lane: "game-inform",
155
+ note: `ADVENTURE — last-resort opening decline: "${opening.world}" names no world in the pack (has: ${names.join(", ")}), and no other lane claimed the line either`,
156
+ };
157
+ }
158
+
128
159
  /** A resumed game's current room: non-null only when earlier @turnN
129
160
  * snapshots exist (the world was already played in this store), folded the
130
161
  * same way every other reader folds them. A fresh world returns null and
@@ -264,8 +295,10 @@ const affordanceSuffix = (actions) => (actions.length ? ` You can: ${actions.joi
264
295
 
265
296
  /** The effect predicate a family writes (its action-effect row's slot, with
266
297
  * the mgx: prefix rule readers re-attach). Null when the family carries no
267
- * effect row — the open/unlock/close families, whose datatype state writes
268
- * have no shipped effect shape yet and ride the container logic below. */
298
+ * effect row — unlock's family stays signature-only (its instrument match
299
+ * needs a third, externally-supplied binding no shipped rule shape covers)
300
+ * and rides the hand-written logic below; go/take/drop/give/open/close all
301
+ * carry real effect rows and never hit this null. */
269
302
  function familyEffectPredicate(family) {
270
303
  const effect = (family || []).find((r) => r.kind === "action-effect");
271
304
  if (!effect?.slots?.predicate) return null;
@@ -449,6 +482,24 @@ function containerStatusPhrase(object, { state }) {
449
482
  : `the ${object} is open. It's empty.`;
450
483
  }
451
484
 
485
+ /** `object`'s own datatype facts (its placement predicate, its open/closed
486
+ * flag), as the tiny {subject,predicate,object} row set a "fact-value"
487
+ * precond needs — read from the already-folded CURRENT truth (state.
488
+ * placements/state.openness), never raw @turnN rows, so a superseded
489
+ * snapshot can never look current. domain.mjs's own stateFromFacts can't
490
+ * serve this: it keys "current" off an @stepN suffix (this world writes
491
+ * @turnN) and restricts rows to individuals typed into some action's
492
+ * SUBJECT class, which "furniture" (the container's own class) never is —
493
+ * a container's own facts about itself would silently vanish through it. */
494
+ function containerDatatypeState(state, object) {
495
+ const rows = [];
496
+ const place = state.placements.get(object);
497
+ if (place) rows.push({ subject: object, predicate: place.predicate, object: place.object });
498
+ const openness = state.openness.get(object);
499
+ if (openness) rows.push({ subject: object, predicate: OPEN_PREDICATE, object: openness.open ? "true" : "false" });
500
+ return rows;
501
+ }
502
+
452
503
  async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
453
504
  const memory = await loadMemory(memoryDir);
454
505
  const rows = readFactRows(memory);
@@ -512,7 +563,8 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
512
563
  );
513
564
  }
514
565
 
515
- const families = actionFamilies(readRuleRows(memory));
566
+ const ruleRows = readRuleRows(memory);
567
+ const families = actionFamilies(ruleRows);
516
568
  const family = families.get(cmd.verb);
517
569
  if (!family) {
518
570
  return answer(
@@ -619,9 +671,15 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
619
671
  );
620
672
  }
621
673
 
622
- // open / unlock / close — the container verbs. Their families are
623
- // signature-only (no shipped rule shape for a datatype effect yet), so the
624
- // state writes are the closed container vocabulary below.
674
+ // open / unlock / close — the container verbs. presence and container-ness
675
+ // stay hand-checked here (visibility gating, not a state precondition);
676
+ // unlock's instrument match stays fully hand-written below it too — it
677
+ // needs a third, externally-supplied binding beyond subject/target, which
678
+ // this retrofit does not attempt. open/close's lock-state and open/closed
679
+ // checks, and their mgx:is-open write, are now taught "fact-value"
680
+ // precond/effect rows consulted through domain.mjs below; only the
681
+ // hidden-contents reveal (a variable-arity effect over a discovered set)
682
+ // stays hand-written JS, since no shipped rule shape covers that either.
625
683
  const presentHere = place && place.predicate !== "mgx:hidden-in" && place.predicate !== "mgx:currently-in" && place.object === here;
626
684
  if (!presentHere) {
627
685
  return answer(`I don't see a ${object} here.`, noteFor(`${cmd.verb} — ${object} isn't in the ${here}; declined`), { miss: true });
@@ -629,40 +687,50 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
629
687
  if (!isContainer(rows, object)) {
630
688
  return answer(`the ${object} doesn't open.`, noteFor(`${cmd.verb} — no mgx:is-container fact on ${object}; declined by name`), { miss: true });
631
689
  }
632
- const open = !!state.openness.get(object)?.open;
633
690
 
634
- if (cmd.verb === "open") {
635
- if (place.predicate === "mgx:stands-locked-in") {
636
- return answer(`the ${object} is locked.`, noteFor(`open ${object} stands locked; precondition declined by name`), { miss: true });
691
+ if (cmd.verb === "open" || cmd.verb === "close") {
692
+ const domain = compileDomain(rows, ruleRows);
693
+ const taughtAction = domain.actions.find((a) => a.name === cmd.verb);
694
+ const effect = taughtAction?.effects.find((e) => e.predicate === OPEN_PREDICATE);
695
+ if (!effect) {
696
+ return answer(
697
+ `this world doesn't teach how ${cmd.verb === "open" ? "opening" : "closing"} changes the ${object}.`,
698
+ noteFor(`${cmd.verb} — the taught "${cmd.verb}" family carries no ${OPEN_PREDICATE} effect; honest decline`),
699
+ { miss: true },
700
+ );
637
701
  }
638
- if (open) {
639
- return answer(`the ${object} is already open.`, noteFor("open — already open; declined"), { miss: true });
702
+ const factState = containerDatatypeState(state, object);
703
+ const failed = taughtAction.preconds.find((p) => !precondHolds(p, "player", object, factState, domain));
704
+ if (failed) {
705
+ const text = failed.predicate === "mgx:stands-locked-in"
706
+ ? `the ${object} is locked.`
707
+ : cmd.verb === "open" ? `the ${object} is already open.` : `the ${object} isn't open.`;
708
+ return answer(text, noteFor(`${cmd.verb} — the taught "${cmd.verb}" family's ${failed.predicate} precondition declined by name`), { miss: true });
640
709
  }
641
- const revealed = [...state.placements]
642
- .filter(([, p]) => p.predicate === "mgx:hidden-in" && p.object === object)
643
- .map(([thing]) => thing)
644
- .sort();
645
- return commit(
646
- [
647
- { subject: `${object}@turn${k}`, predicate: "mgx:is-open", object: "true" },
648
- ...revealed.map((thing) => ({ subject: `${thing}@turn${k}`, predicate: "mgx:located-in", object })),
649
- ],
650
- revealed.length
651
- ? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
652
- : `you open the ${object}. It's empty.`,
653
- `open — ${object} opens${revealed.length ? `, revealing ${revealed.join(", ")}` : ""}`,
654
- `open the ${object}`,
655
- );
656
- }
657
-
658
- if (cmd.verb === "close") {
659
- if (!open) {
660
- return answer(`the ${object} isn't open.`, noteFor("close — not open; declined"), { miss: true });
710
+ const effSubject = roleBinding(effect.subjectRole, "player", object, domain);
711
+ const writeIsOpen = { subject: `${effSubject}@turn${k}`, predicate: effect.predicate, object: effect.value };
712
+
713
+ if (cmd.verb === "open") {
714
+ const revealed = [...state.placements]
715
+ .filter(([, p]) => p.predicate === "mgx:hidden-in" && p.object === object)
716
+ .map(([thing]) => thing)
717
+ .sort();
718
+ return commit(
719
+ [
720
+ writeIsOpen,
721
+ ...revealed.map((thing) => ({ subject: `${thing}@turn${k}`, predicate: "mgx:located-in", object })),
722
+ ],
723
+ revealed.length
724
+ ? `you open the ${object} — inside: the ${revealed.join(", the ")}.`
725
+ : `you open the ${object}. It's empty.`,
726
+ `open — ${object} opens${revealed.length ? `, revealing ${revealed.join(", ")}` : ""} via the taught "open" family's effect`,
727
+ `open the ${object}`,
728
+ );
661
729
  }
662
730
  return commit(
663
- [{ subject: `${object}@turn${k}`, predicate: "mgx:is-open", object: "false" }],
731
+ [writeIsOpen],
664
732
  `you close the ${object}.`,
665
- `close — ${object} closes`,
733
+ `close — ${object} closes via the taught "close" family's effect`,
666
734
  `close the ${object}`,
667
735
  );
668
736
  }
@@ -52,7 +52,7 @@ import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
52
52
  import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
53
53
  import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
54
54
  import { relatedForTerm } from "../domain/skos-view.mjs";
55
- import { adventureTurn } from "./adventure.mjs";
55
+ import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
56
56
  import { spiderFlyTurn } from "./spider-fly-turn.mjs";
57
57
 
58
58
  // Composition: the chat surface supplies the domain parser's default lemma/POS
@@ -12646,6 +12646,24 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
12646
12646
  }
12647
12647
  }
12648
12648
 
12649
+ // A "play X" naming no world EITHER game lane above claimed — last resort,
12650
+ // checked only once the adventure lane's own fallthrough and spider-fly's
12651
+ // own opener have both passed on the line, so this never outguesses a
12652
+ // sibling game's own recognized phrasing. Honest by name ("I don't know a
12653
+ // world called…") rather than the generic non-answer an unclaimed opener
12654
+ // fell into before.
12655
+ {
12656
+ const unclaimed = await unclaimedAdventureOpening(workingLine, { env });
12657
+ if (unclaimed) {
12658
+ note(trace, `lane: ${unclaimed.note}`);
12659
+ const result = plainTurn(workingLine, unclaimed.text, { via: "game", miss: true, focus });
12660
+ result.lane = unclaimed.lane;
12661
+ const rec = withLast(result, "play the adventure");
12662
+ rec.planState = planHolder.state;
12663
+ return rec;
12664
+ }
12665
+ }
12666
+
12649
12667
  // Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
12650
12668
  // resolve no entity and carry their own preserved `last`. Bypasses withLast (a
12651
12669
  // conversational turn is never finish()'d / never becomes a new `last`), so the
@@ -9131,11 +9131,7 @@ ${bodyText}` : graphText, tier });
9131
9131
  "mgx:ruleActionPrecondRole",
9132
9132
  "mgx:ruleActionPrecondScope"
9133
9133
  ],
9134
- "action-effect": [
9135
- "mgx:ruleActionEffectPredicate",
9136
- "mgx:ruleActionEffectSubject",
9137
- "mgx:ruleActionEffectObject"
9138
- ],
9134
+ "action-effect": ["mgx:ruleActionEffectPredicate", "mgx:ruleActionEffectSubject"],
9139
9135
  "action-constraint": [
9140
9136
  "mgx:ruleActionConstraintLeft",
9141
9137
  "mgx:ruleActionConstraintRight",
@@ -10088,9 +10084,16 @@ ${bodyText}` : graphText, tier });
10088
10084
  if (!spec) throw new Error(`a rule kind must be one of ${RULE_KINDS2.join(", ")}, got ${JSON.stringify(kind)}`);
10089
10085
  const n = normFactTerm(name);
10090
10086
  if (!n) throw new Error("a rule needs a name");
10087
+ const optional = RULE_SLOT_OPTIONAL[kind] || /* @__PURE__ */ new Set();
10091
10088
  const slotValues = spec.map(([slotKey]) => normFactTerm(slots?.[slotKey]));
10092
- if (slotValues.some((v) => !v)) {
10093
- throw new Error(`a ${kind} rule needs ${spec.map(([slotKey]) => slotKey).join(" + ")}`);
10089
+ const missing = spec.filter(([slotKey], i) => !optional.has(slotKey) && !slotValues[i]);
10090
+ if (missing.length) {
10091
+ throw new Error(`a ${kind} rule needs ${missing.map(([slotKey]) => slotKey).join(" + ")}`);
10092
+ }
10093
+ if (kind === RULE_KIND_ACTION_EFFECT) {
10094
+ const objectRole = slotValues[spec.findIndex(([k]) => k === "objectRole")];
10095
+ const value = slotValues[spec.findIndex(([k]) => k === "value")];
10096
+ if (!objectRole && !value) throw new Error(`a ${kind} rule needs an objectRole or a value`);
10094
10097
  }
10095
10098
  const id = ruleIdFor(kind, n, slotValues);
10096
10099
  const label = labelOf(`${n} = ${kind}(${slotValues.join(", ")})`);
@@ -10109,7 +10112,10 @@ ${bodyText}` : graphText, tier });
10109
10112
  { prop: "rdf:type", key: "type", value: "owl:NamedIndividual" },
10110
10113
  { prop: RULE_NAME_PROP, key: "ruleName", value: n },
10111
10114
  { prop: RULE_KIND_PROP, key: "ruleKind", value: kind },
10112
- ...spec.map(([slotKey, prop], i) => ({ prop, key: slotKey, value: slotValues[i] })),
10115
+ // An optional slot left empty stores no attribute at all — the same
10116
+ // "never supplied" shape a pre-extension Rule individual already has,
10117
+ // rather than a wasted always-"" one.
10118
+ ...spec.map(([slotKey, prop], i) => ({ prop, key: slotKey, value: slotValues[i], skip: !slotValues[i] && optional.has(slotKey) })).filter((attr) => !attr.skip).map(({ skip, ...attr }) => attr),
10113
10119
  { prop: CREATED_AT_PROP, key: "createdAt", value: createdAtVal },
10114
10120
  ...provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []
10115
10121
  ]
@@ -10376,7 +10382,7 @@ ${bodyText}` : graphText, tier });
10376
10382
  }
10377
10383
  return out.sort((a, b) => `${a[0].subject} ${a[0].predicate}`.localeCompare(`${b[0].subject} ${b[0].predicate}`));
10378
10384
  }
10379
- var MEMORY_DIR_REL, MEMORY_GRAPH_REL, UTTERANCE_CLASS, FACT_CLASS, MEMORY_SESSION_CLASS, SOURCE_CLASS, RULE_CLASS, SAID_IN_SESSION_PROP, IN_REPLY_TO_PROP, DERIVED_FROM_PROP, STATED_BY_PROP, CANONICALISED_FROM_PROP, SOURCE_RELIABILITY_PROP, OPERATOR_SOURCE_ID, TEACH_SOURCE_ID, ROLES, LABEL_CAP, MEMORY_VOCABULARY, memoryGraphFile, BACKEND_MEMORY, BACKEND_SQLITE, SQLITE_DDL, STD_EDGE_KEYS, cloneJson, MEMORY_MANIFEST_REL, DEFAULT_RETENTION, resolveManifestFile, LEGACY_FACT_ID_RE, SYLLOGISE_STATE_REL, SQLITE_SYLLOGISE_STATE_KEY, MEMORY_INDEX, memoryIndexOf, labelOf, nowIso, sourceLabel, PROV_CLASS_BY_SOURCE_TYPE, isSessionScopedSourceId, RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE, RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT, RULE_KIND_ACTION_CONSTRAINT, RULE_KINDS2, RULE_NAME_PROP, RULE_KIND_PROP, RULE_SLOT_SPEC, ruleIdFor, CONTRADICTION_TRUST_FLOOR, HAS_A_PREDICATE, CAPABLE_OF_PREDICATE2, MULTI_VALUED_PREDICATES;
10385
+ var MEMORY_DIR_REL, MEMORY_GRAPH_REL, UTTERANCE_CLASS, FACT_CLASS, MEMORY_SESSION_CLASS, SOURCE_CLASS, RULE_CLASS, SAID_IN_SESSION_PROP, IN_REPLY_TO_PROP, DERIVED_FROM_PROP, STATED_BY_PROP, CANONICALISED_FROM_PROP, SOURCE_RELIABILITY_PROP, OPERATOR_SOURCE_ID, TEACH_SOURCE_ID, ROLES, LABEL_CAP, MEMORY_VOCABULARY, memoryGraphFile, BACKEND_MEMORY, BACKEND_SQLITE, SQLITE_DDL, STD_EDGE_KEYS, cloneJson, MEMORY_MANIFEST_REL, DEFAULT_RETENTION, resolveManifestFile, LEGACY_FACT_ID_RE, SYLLOGISE_STATE_REL, SQLITE_SYLLOGISE_STATE_KEY, MEMORY_INDEX, memoryIndexOf, labelOf, nowIso, sourceLabel, PROV_CLASS_BY_SOURCE_TYPE, isSessionScopedSourceId, RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE, RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT, RULE_KIND_ACTION_CONSTRAINT, RULE_KINDS2, RULE_NAME_PROP, RULE_KIND_PROP, RULE_SLOT_SPEC, RULE_SLOT_OPTIONAL, ruleIdFor, CONTRADICTION_TRUST_FLOOR, HAS_A_PREDICATE, CAPABLE_OF_PREDICATE2, MULTI_VALUED_PREDICATES;
10380
10386
  var init_core = __esm({
10381
10387
  "src/adapters/memory/core.mjs"() {
10382
10388
  init_promises();
@@ -10506,12 +10512,22 @@ CREATE INDEX IF NOT EXISTS edges_by_prop ON edges(prop);
10506
10512
  ["shape", "mgx:ruleActionPrecondShape"],
10507
10513
  ["predicate", "mgx:ruleActionPrecondPredicate"],
10508
10514
  ["role", "mgx:ruleActionPrecondRole"],
10509
- ["scope", "mgx:ruleActionPrecondScope"]
10515
+ ["scope", "mgx:ruleActionPrecondScope"],
10516
+ // value/negate: the "fact-value" shape's literal-match and negation
10517
+ // slots. Optional (RULE_SLOT_OPTIONAL below) — no-incoming/comparator
10518
+ // preconds never set them.
10519
+ ["value", "mgx:ruleActionPrecondValue"],
10520
+ ["negate", "mgx:ruleActionPrecondNegate"]
10510
10521
  ],
10511
10522
  [RULE_KIND_ACTION_EFFECT]: [
10512
10523
  ["predicate", "mgx:ruleActionEffectPredicate"],
10513
10524
  ["subjectRole", "mgx:ruleActionEffectSubject"],
10514
- ["objectRole", "mgx:ruleActionEffectObject"]
10525
+ // objectRole/value: exactly one of these two must be set (enforced
10526
+ // below, not by this per-slot spec) — a role-bound effect (Hanoi's
10527
+ // "rest-on") supplies objectRole, a literal datatype effect (Ashcombe's
10528
+ // is-open = "true") supplies value instead.
10529
+ ["objectRole", "mgx:ruleActionEffectObject"],
10530
+ ["value", "mgx:ruleActionEffectValue"]
10515
10531
  ],
10516
10532
  // "the <left> may not be with the <right> without the <guard>" — each slot
10517
10533
  // names a class whose sole member src/domain/domain.mjs resolves at compile time.
@@ -10521,6 +10537,10 @@ CREATE INDEX IF NOT EXISTS edges_by_prop ON edges(prop);
10521
10537
  ["guard", "mgx:ruleActionConstraintGuard"]
10522
10538
  ]
10523
10539
  };
10540
+ RULE_SLOT_OPTIONAL = {
10541
+ [RULE_KIND_ACTION_PRECOND]: /* @__PURE__ */ new Set(["value", "negate"]),
10542
+ [RULE_KIND_ACTION_EFFECT]: /* @__PURE__ */ new Set(["objectRole", "value"])
10543
+ };
10524
10544
  ruleIdFor = (kind, name, slotValues) => `rule:${fnv1aHex([kind, name, ...slotValues].join("\0"))}`;
10525
10545
  CONTRADICTION_TRUST_FLOOR = 0.5;
10526
10546
  HAS_A_PREDICATE = "mgx:hasA";
@@ -21709,7 +21729,7 @@ ${codeblock}`, options);
21709
21729
  });
21710
21730
 
21711
21731
  // src/domain/grammar/ace.mjs
21712
- var PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM, PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE, PATTERN_CAPABILITY, PATTERNS, IMPERATIVE_VERBS, VERB_SYNONYMS, VERB_SYNONYM_MAX_PREFIX, VERB_FUZZY_CANDIDATES;
21732
+ var PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM, PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE, PATTERN_CAPABILITY, PATTERNS, IMPERATIVE_VERBS, VERB_SYNONYMS, VERB_SYNONYM_MAX_PREFIX, VERB_SURFACE_WORDS;
21713
21733
  var init_ace2 = __esm({
21714
21734
  "src/domain/grammar/ace.mjs"() {
21715
21735
  init_lexicon();
@@ -21758,7 +21778,7 @@ ${codeblock}`, options);
21758
21778
  ["shut", "close"]
21759
21779
  ]);
21760
21780
  VERB_SYNONYM_MAX_PREFIX = Math.max(...[...VERB_SYNONYMS.keys()].map((k) => k.split(" ").length));
21761
- VERB_FUZZY_CANDIDATES = [.../* @__PURE__ */ new Set([...IMPERATIVE_VERBS, ...VERB_SYNONYMS.values()])];
21781
+ VERB_SURFACE_WORDS = [.../* @__PURE__ */ new Set([...IMPERATIVE_VERBS, ...[...VERB_SYNONYMS.keys()].map((k) => k.split(" ")[0])])];
21762
21782
  }
21763
21783
  });
21764
21784
 
@@ -22011,6 +22031,14 @@ ${codeblock}`, options);
22011
22031
  }
22012
22032
  });
22013
22033
 
22034
+ // src/domain/domain.mjs
22035
+ var SEP2;
22036
+ var init_domain = __esm({
22037
+ "src/domain/domain.mjs"() {
22038
+ SEP2 = String.fromCharCode(0);
22039
+ }
22040
+ });
22041
+
22014
22042
  // src/adapters/memory/blocks.mjs
22015
22043
  var BLOCKS_DIR_REL;
22016
22044
  var init_blocks = __esm({
@@ -23449,6 +23477,7 @@ ${JSON.stringify(envelope, null, 2)}`;
23449
23477
  init_ace2();
23450
23478
  init_graph_adapter();
23451
23479
  init_taught();
23480
+ init_domain();
23452
23481
 
23453
23482
  // src/adapters/corpus/worlds-pack.mjs
23454
23483
  init_node_fs();