@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.
@@ -1023,7 +1023,7 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
1023
1023
  const p = normText(predicate);
1024
1024
  const o = normFactTerm(object);
1025
1025
  if (!s || !p || !o) throw new Error("a fact needs subject, predicate and object");
1026
- const id = `fact:${fnv1aHex(`${s}${p}${o}`)}`;
1026
+ const id = `fact:${fnv1aHex(`${s}\0${p}\0${o}`)}`;
1027
1027
  const text = `${s} ${p} ${o}`;
1028
1028
  const tokens = proseTokensFor({ doc: text });
1029
1029
  const q = normText(quantifier);
@@ -1166,31 +1166,56 @@ export async function appendFacts(dir, facts) {
1166
1166
  export const RULE_KIND_COMPOSE2 = "compose2";
1167
1167
  export const RULE_KIND_FILTER = "filter";
1168
1168
  export const RULE_KIND_RECURSIVE = "recursive";
1169
- export const RULE_KINDS = Object.freeze([RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE]);
1169
+ // The action family: one taught sentence = one Rule individual, the family
1170
+ // sharing one mgx:ruleName. A single flat kind can't hold several
1171
+ // preconditions under content addressing (one value per slot), so signature,
1172
+ // precondition and effect are sibling kinds collected by name at plan time.
1173
+ export const RULE_KIND_ACTION_SIGNATURE = "action-signature";
1174
+ export const RULE_KIND_ACTION_PRECOND = "action-precond";
1175
+ export const RULE_KIND_ACTION_EFFECT = "action-effect";
1176
+ export const RULE_KINDS = Object.freeze([
1177
+ RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE,
1178
+ RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT,
1179
+ ]);
1170
1180
 
1171
1181
  export const RULE_NAME_PROP = "mgx:ruleName";
1172
1182
  export const RULE_KIND_PROP = "mgx:ruleKind";
1173
1183
 
1174
1184
  // Per-kind slot contract: JS slot key -> the mgx: attribute it's written under.
1175
- // filter's "base" slot deliberately reuses ruleBase1 (not a fresh "ruleBase")
1176
- // — the same attribute name compose2's first hop already uses, since both
1177
- // play the identical "base relation this rule builds on" role. Order within
1178
- // each array is the (slot1, slot2) order the content-address hash below
1179
- // uses fixed and load-bearing.
1185
+ // filter's "base" slot reuses ruleBase1 the same attribute name compose2's
1186
+ // first hop already uses, since both play the identical "base relation this
1187
+ // rule builds on" role. Order within each array is the content-address hash
1188
+ // order below fixed and load-bearing. Predicate slots store BARE values
1189
+ // ("rest-on", "smaller-than"): normFactTerm strips a leading CURIE prefix, so
1190
+ // an mgx:-prefixed value could never round-trip; readers re-attach mgx:.
1180
1191
  const RULE_SLOT_SPEC = {
1181
1192
  [RULE_KIND_COMPOSE2]: [["base1", "mgx:ruleBase1"], ["base2", "mgx:ruleBase2"]],
1182
1193
  [RULE_KIND_FILTER]: [["base", "mgx:ruleBase1"], ["property", "mgx:ruleFilterProperty"]],
1183
1194
  [RULE_KIND_RECURSIVE]: [["baseCase", "mgx:ruleBaseCase"], ["recStep", "mgx:ruleRecStep"]],
1195
+ [RULE_KIND_ACTION_SIGNATURE]: [
1196
+ ["subjectClass", "mgx:ruleActionSubjectClass"], ["targetClass", "mgx:ruleActionTargetClass"],
1197
+ ],
1198
+ [RULE_KIND_ACTION_PRECOND]: [
1199
+ ["shape", "mgx:ruleActionPrecondShape"], ["predicate", "mgx:ruleActionPrecondPredicate"],
1200
+ ["role", "mgx:ruleActionPrecondRole"], ["scope", "mgx:ruleActionPrecondScope"],
1201
+ ],
1202
+ [RULE_KIND_ACTION_EFFECT]: [
1203
+ ["predicate", "mgx:ruleActionEffectPredicate"], ["subjectRole", "mgx:ruleActionEffectSubject"],
1204
+ ["objectRole", "mgx:ruleActionEffectObject"],
1205
+ ],
1184
1206
  };
1185
1207
 
1186
- // Content-addressed over (kind, name, slot1, slot2), mirroring factIdFor's
1187
- // NUL-delimited discipline: identical rules upsert, different ones coexist.
1188
- const ruleIdFor = (kind, name, slot1, slot2) => `rule:${fnv1aHex(`${kind}\0${name}\0${slot1}\0${slot2}`)}`;
1189
-
1190
- /** Append one taught RULE (compose2 | filter | recursive) — a sibling of
1191
- * appendFact storing a `Rule` individual, same upsert/provenance/trust/SHACL
1192
- * discipline (neither pipeline ever checks `individual.class`). `slots` is
1193
- * the matching per-kind object (RULE_SLOT_SPEC above). Returns { id }. */
1208
+ // Content-addressed over (kind, name, ...slots in RULE_SLOT_SPEC order),
1209
+ // mirroring factIdFor's NUL-delimited discipline: identical rules upsert,
1210
+ // different ones coexist. For 2-slot kinds the joined string is byte-identical
1211
+ // to the historical (kind, name, slot1, slot2) template, so pre-existing rule
1212
+ // ids never change (pinned by test/memory-rules-action.test.mjs).
1213
+ const ruleIdFor = (kind, name, slotValues) => `rule:${fnv1aHex([kind, name, ...slotValues].join("\0"))}`;
1214
+
1215
+ /** Append one taught RULE a sibling of appendFact storing a `Rule`
1216
+ * individual, same upsert/provenance/trust/SHACL discipline (neither
1217
+ * pipeline ever checks `individual.class`). `slots` is the matching
1218
+ * per-kind object (RULE_SLOT_SPEC above). Returns { id }. */
1194
1219
  export async function appendRule(dir, { name, kind, slots, provenance = "", createdAt = "" } = {}) {
1195
1220
  const spec = RULE_SLOT_SPEC[kind];
1196
1221
  if (!spec) throw new Error(`a rule kind must be one of ${RULE_KINDS.join(", ")}, got ${JSON.stringify(kind)}`);
@@ -1200,7 +1225,7 @@ export async function appendRule(dir, { name, kind, slots, provenance = "", crea
1200
1225
  if (slotValues.some((v) => !v)) {
1201
1226
  throw new Error(`a ${kind} rule needs ${spec.map(([slotKey]) => slotKey).join(" + ")}`);
1202
1227
  }
1203
- const id = ruleIdFor(kind, n, slotValues[0], slotValues[1]);
1228
+ const id = ruleIdFor(kind, n, slotValues);
1204
1229
  const label = labelOf(`${n} = ${kind}(${slotValues.join(", ")})`);
1205
1230
  await mutateMemory(dir, async (payload) => {
1206
1231
  const prior = payload.individuals.find((x) => x?.id === id);
@@ -1244,6 +1269,45 @@ export function findRuleByName(memory, name) {
1244
1269
  );
1245
1270
  }
1246
1271
 
1272
+ /** Every Rule individual sharing `name` — an action family's members live as
1273
+ * sibling individuals (one per taught sentence), so consumers collect them
1274
+ * all. Sorted by kind then id for deterministic iteration. */
1275
+ export function findRulesByName(memory, name) {
1276
+ const n = normFactTerm(name);
1277
+ const kindOf = (i) => (i.attributes || []).find((a) => a?.prop === RULE_KIND_PROP)?.value || "";
1278
+ return (memory?.individuals || [])
1279
+ .filter((i) => i?.class === RULE_CLASS
1280
+ && (i.attributes || []).find((a) => a?.prop === RULE_NAME_PROP)?.value === n)
1281
+ .sort((a, b) => kindOf(a).localeCompare(kindOf(b)) || String(a.id).localeCompare(String(b.id)));
1282
+ }
1283
+
1284
+ /** Every taught Rule as a plain row {id, name, kind, slots, provenance} —
1285
+ * the sibling of readFactRows, so consumers (src/domain.mjs) never touch
1286
+ * raw individuals. Rules whose kind has no RULE_SLOT_SPEC entry are
1287
+ * skipped (unreadable without a slot contract). Sorted by name, kind, id. */
1288
+ export function readRuleRows(memory) {
1289
+ const rows = [];
1290
+ for (const ind of memory?.individuals || []) {
1291
+ if (ind?.class !== RULE_CLASS) continue;
1292
+ const attr = (prop) => (ind.attributes || []).find((a) => a?.prop === prop)?.value;
1293
+ const kind = attr(RULE_KIND_PROP);
1294
+ const spec = RULE_SLOT_SPEC[kind];
1295
+ if (!spec) continue;
1296
+ const slots = {};
1297
+ for (const [slotKey, prop] of spec) slots[slotKey] = attr(prop) ?? "";
1298
+ rows.push({
1299
+ id: ind.id,
1300
+ name: attr(RULE_NAME_PROP) || "",
1301
+ kind,
1302
+ slots,
1303
+ provenance: attr("mgx:factProvenance") || "",
1304
+ });
1305
+ }
1306
+ rows.sort((a, b) => a.name.localeCompare(b.name)
1307
+ || a.kind.localeCompare(b.kind) || String(a.id).localeCompare(String(b.id)));
1308
+ return rows;
1309
+ }
1310
+
1247
1311
  // ---- Relation chase ---------------------------------------------------------
1248
1312
  //
1249
1313
  // `resolveRelationChase` and `resolveRelationChaseReverse` are standalone,
@@ -6,15 +6,26 @@
6
6
  // only fires on genuine malformation, never a sparse-but-valid write.
7
7
 
8
8
  const MEMORY_CLASSES = new Set(["Utterance", "Fact", "Session", "Source", "Rule"]);
9
- const RULE_KINDS = new Set(["compose2", "filter", "recursive"]);
9
+ const RULE_KINDS = new Set([
10
+ "compose2", "filter", "recursive",
11
+ "action-signature", "action-precond", "action-effect",
12
+ ]);
10
13
 
11
14
  // Mirrors core.mjs's own (unexported) RULE_SLOT_SPEC exactly — the single
12
- // source of truth for the closed compose2/filter/recursive shapes; kept in
13
- // sync by hand (both describe the SAME three rule kinds' slot pairs).
15
+ // source of truth for the closed rule-kind shapes; kept in sync by hand
16
+ // (both describe the same kinds' slots).
14
17
  const RULE_SLOT_PROPS = {
15
18
  compose2: ["mgx:ruleBase1", "mgx:ruleBase2"],
16
19
  filter: ["mgx:ruleBase1", "mgx:ruleFilterProperty"],
17
20
  recursive: ["mgx:ruleBaseCase", "mgx:ruleRecStep"],
21
+ "action-signature": ["mgx:ruleActionSubjectClass", "mgx:ruleActionTargetClass"],
22
+ "action-precond": [
23
+ "mgx:ruleActionPrecondShape", "mgx:ruleActionPrecondPredicate",
24
+ "mgx:ruleActionPrecondRole", "mgx:ruleActionPrecondScope",
25
+ ],
26
+ "action-effect": [
27
+ "mgx:ruleActionEffectPredicate", "mgx:ruleActionEffectSubject", "mgx:ruleActionEffectObject",
28
+ ],
18
29
  };
19
30
 
20
31
  function attrValue(ind, prop) {
@@ -45,14 +56,13 @@ function checkFact(ind, violations) {
45
56
  }
46
57
 
47
58
  /** RuleShape (mgx:RuleShape): a non-empty name; a kind from the closed
48
- * compose2 | filter | recursive vocabulary; and the matching pair of slots
49
- * for that declared kind, each present and non-empty (RULE_SLOT_PROPS
50
- * above). */
59
+ * vocabulary; and the matching slots for that declared kind, each present
60
+ * and non-empty (RULE_SLOT_PROPS above). */
51
61
  function checkRule(ind, violations) {
52
62
  if (!nonEmpty(attrValue(ind, "mgx:ruleName"))) violations.push("a Rule needs a non-empty mgx:ruleName");
53
63
  const kind = attrValue(ind, "mgx:ruleKind");
54
64
  if (!kind || !RULE_KINDS.has(kind)) {
55
- violations.push(`a Rule's mgx:ruleKind must be one of compose2 | filter | recursive (got ${JSON.stringify(kind)})`);
65
+ violations.push(`a Rule's mgx:ruleKind must be one of ${[...RULE_KINDS].join(" | ")} (got ${JSON.stringify(kind)})`);
56
66
  return; // no declared kind to check slots against
57
67
  }
58
68
  for (const prop of RULE_SLOT_PROPS[kind]) {
@@ -6,8 +6,10 @@
6
6
  // already-embedded payload with zero fs I/O. Called with `envelope: null,
7
7
  // miss: true` to arm factAnswer's bare-question regex fallbacks directly,
8
8
  // bypassing the structural-graph parse pipeline this panel has no use for.
9
- import { factAnswer } from "./chat.mjs";
9
+ import { factAnswer, factReadBack } from "./chat.mjs";
10
10
  import { createInMemoryStore, normFactTerm } from "./memory/core.mjs";
11
11
 
12
12
  // normFactTerm is re-exported too, for viz.mjs's client-side term normalization.
13
- globalThis.tmctMemoryAsk = { factAnswer, createInMemoryStore, normFactTerm };
13
+ // factReadBack carries the taught-relation chases (grandfather-style questions)
14
+ // that factAnswer's own lanes don't reach.
15
+ globalThis.tmctMemoryAsk = { factAnswer, factReadBack, createInMemoryStore, normFactTerm };