@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.5

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,64 @@ 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_KIND_ACTION_CONSTRAINT = "action-constraint";
1177
+ export const RULE_KINDS = Object.freeze([
1178
+ RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE,
1179
+ RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT,
1180
+ RULE_KIND_ACTION_CONSTRAINT,
1181
+ ]);
1170
1182
 
1171
1183
  export const RULE_NAME_PROP = "mgx:ruleName";
1172
1184
  export const RULE_KIND_PROP = "mgx:ruleKind";
1173
1185
 
1174
1186
  // 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.
1187
+ // filter's "base" slot reuses ruleBase1 the same attribute name compose2's
1188
+ // first hop already uses, since both play the identical "base relation this
1189
+ // rule builds on" role. Order within each array is the content-address hash
1190
+ // order below fixed and load-bearing. Predicate slots store BARE values
1191
+ // ("rest-on", "smaller-than"): normFactTerm strips a leading CURIE prefix, so
1192
+ // an mgx:-prefixed value could never round-trip; readers re-attach mgx:.
1180
1193
  const RULE_SLOT_SPEC = {
1181
1194
  [RULE_KIND_COMPOSE2]: [["base1", "mgx:ruleBase1"], ["base2", "mgx:ruleBase2"]],
1182
1195
  [RULE_KIND_FILTER]: [["base", "mgx:ruleBase1"], ["property", "mgx:ruleFilterProperty"]],
1183
1196
  [RULE_KIND_RECURSIVE]: [["baseCase", "mgx:ruleBaseCase"], ["recStep", "mgx:ruleRecStep"]],
1197
+ [RULE_KIND_ACTION_SIGNATURE]: [
1198
+ ["subjectClass", "mgx:ruleActionSubjectClass"], ["targetClass", "mgx:ruleActionTargetClass"],
1199
+ ],
1200
+ [RULE_KIND_ACTION_PRECOND]: [
1201
+ ["shape", "mgx:ruleActionPrecondShape"], ["predicate", "mgx:ruleActionPrecondPredicate"],
1202
+ ["role", "mgx:ruleActionPrecondRole"], ["scope", "mgx:ruleActionPrecondScope"],
1203
+ ],
1204
+ [RULE_KIND_ACTION_EFFECT]: [
1205
+ ["predicate", "mgx:ruleActionEffectPredicate"], ["subjectRole", "mgx:ruleActionEffectSubject"],
1206
+ ["objectRole", "mgx:ruleActionEffectObject"],
1207
+ ],
1208
+ // "the <left> may not be with the <right> without the <guard>" — each slot
1209
+ // names a class whose sole member src/domain.mjs resolves at compile time.
1210
+ [RULE_KIND_ACTION_CONSTRAINT]: [
1211
+ ["left", "mgx:ruleActionConstraintLeft"], ["right", "mgx:ruleActionConstraintRight"],
1212
+ ["guard", "mgx:ruleActionConstraintGuard"],
1213
+ ],
1184
1214
  };
1185
1215
 
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 }. */
1216
+ // Content-addressed over (kind, name, ...slots in RULE_SLOT_SPEC order),
1217
+ // mirroring factIdFor's NUL-delimited discipline: identical rules upsert,
1218
+ // different ones coexist. For 2-slot kinds the joined string is byte-identical
1219
+ // to the historical (kind, name, slot1, slot2) template, so pre-existing rule
1220
+ // ids never change (pinned by test/memory-rules-action.test.mjs).
1221
+ const ruleIdFor = (kind, name, slotValues) => `rule:${fnv1aHex([kind, name, ...slotValues].join("\0"))}`;
1222
+
1223
+ /** Append one taught RULE a sibling of appendFact storing a `Rule`
1224
+ * individual, same upsert/provenance/trust/SHACL discipline (neither
1225
+ * pipeline ever checks `individual.class`). `slots` is the matching
1226
+ * per-kind object (RULE_SLOT_SPEC above). Returns { id }. */
1194
1227
  export async function appendRule(dir, { name, kind, slots, provenance = "", createdAt = "" } = {}) {
1195
1228
  const spec = RULE_SLOT_SPEC[kind];
1196
1229
  if (!spec) throw new Error(`a rule kind must be one of ${RULE_KINDS.join(", ")}, got ${JSON.stringify(kind)}`);
@@ -1200,7 +1233,7 @@ export async function appendRule(dir, { name, kind, slots, provenance = "", crea
1200
1233
  if (slotValues.some((v) => !v)) {
1201
1234
  throw new Error(`a ${kind} rule needs ${spec.map(([slotKey]) => slotKey).join(" + ")}`);
1202
1235
  }
1203
- const id = ruleIdFor(kind, n, slotValues[0], slotValues[1]);
1236
+ const id = ruleIdFor(kind, n, slotValues);
1204
1237
  const label = labelOf(`${n} = ${kind}(${slotValues.join(", ")})`);
1205
1238
  await mutateMemory(dir, async (payload) => {
1206
1239
  const prior = payload.individuals.find((x) => x?.id === id);
@@ -1244,6 +1277,45 @@ export function findRuleByName(memory, name) {
1244
1277
  );
1245
1278
  }
1246
1279
 
1280
+ /** Every Rule individual sharing `name` — an action family's members live as
1281
+ * sibling individuals (one per taught sentence), so consumers collect them
1282
+ * all. Sorted by kind then id for deterministic iteration. */
1283
+ export function findRulesByName(memory, name) {
1284
+ const n = normFactTerm(name);
1285
+ const kindOf = (i) => (i.attributes || []).find((a) => a?.prop === RULE_KIND_PROP)?.value || "";
1286
+ return (memory?.individuals || [])
1287
+ .filter((i) => i?.class === RULE_CLASS
1288
+ && (i.attributes || []).find((a) => a?.prop === RULE_NAME_PROP)?.value === n)
1289
+ .sort((a, b) => kindOf(a).localeCompare(kindOf(b)) || String(a.id).localeCompare(String(b.id)));
1290
+ }
1291
+
1292
+ /** Every taught Rule as a plain row {id, name, kind, slots, provenance} —
1293
+ * the sibling of readFactRows, so consumers (src/domain.mjs) never touch
1294
+ * raw individuals. Rules whose kind has no RULE_SLOT_SPEC entry are
1295
+ * skipped (unreadable without a slot contract). Sorted by name, kind, id. */
1296
+ export function readRuleRows(memory) {
1297
+ const rows = [];
1298
+ for (const ind of memory?.individuals || []) {
1299
+ if (ind?.class !== RULE_CLASS) continue;
1300
+ const attr = (prop) => (ind.attributes || []).find((a) => a?.prop === prop)?.value;
1301
+ const kind = attr(RULE_KIND_PROP);
1302
+ const spec = RULE_SLOT_SPEC[kind];
1303
+ if (!spec) continue;
1304
+ const slots = {};
1305
+ for (const [slotKey, prop] of spec) slots[slotKey] = attr(prop) ?? "";
1306
+ rows.push({
1307
+ id: ind.id,
1308
+ name: attr(RULE_NAME_PROP) || "",
1309
+ kind,
1310
+ slots,
1311
+ provenance: attr("mgx:factProvenance") || "",
1312
+ });
1313
+ }
1314
+ rows.sort((a, b) => a.name.localeCompare(b.name)
1315
+ || a.kind.localeCompare(b.kind) || String(a.id).localeCompare(String(b.id)));
1316
+ return rows;
1317
+ }
1318
+
1247
1319
  // ---- Relation chase ---------------------------------------------------------
1248
1320
  //
1249
1321
  // `resolveRelationChase` and `resolveRelationChaseReverse` are standalone,
@@ -1504,13 +1576,24 @@ export async function removeFacts(dir, ids) {
1504
1576
  * contradiction (below it the fact is too weak to contradict anything). */
1505
1577
  export const CONTRADICTION_TRUST_FLOOR = 0.5;
1506
1578
 
1579
+ export const HAS_A_PREDICATE = "mgx:hasA";
1580
+ export const CAPABLE_OF_PREDICATE = "mgx:capableOf";
1581
+
1582
+ /** Predicates whose real-world semantics allow many objects at once ("a dog
1583
+ * has legs" AND "a dog has a tail"; "a bird can fly" AND "a bird can sing"),
1584
+ * so a second object is a second fact, never a disagreement. A closed list:
1585
+ * every predicate outside it keeps the full contradiction contract. */
1586
+ export const MULTI_VALUED_PREDICATES = new Set([HAS_A_PREDICATE, CAPABLE_OF_PREDICATE]);
1587
+
1507
1588
  /** Facts that CONTRADICT: same (subject, predicate), different object, each
1508
1589
  * above the trust floor. Returns groups (trust-desc) so callers surface both,
1509
- * never silently pick one. Same (s,p,o) is corroboration, not contradiction. */
1590
+ * never silently pick one. Same (s,p,o) is corroboration, not contradiction,
1591
+ * and a MULTI_VALUED_PREDICATES predicate never contradicts on object count. */
1510
1592
  export function findContradictions(memory, { floor = CONTRADICTION_TRUST_FLOOR } = {}) {
1511
1593
  const rows = readFactRows(memory).filter((r) => r.trust >= floor);
1512
1594
  const byKey = new Map();
1513
1595
  for (const r of rows) {
1596
+ if (MULTI_VALUED_PREDICATES.has(r.predicate)) continue;
1514
1597
  const key = `${r.subject} ${r.predicate}`;
1515
1598
  if (!byKey.has(key)) byKey.set(key, []);
1516
1599
  byKey.get(key).push(r);
@@ -6,15 +6,29 @@
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", "action-constraint",
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
+ ],
29
+ "action-constraint": [
30
+ "mgx:ruleActionConstraintLeft", "mgx:ruleActionConstraintRight", "mgx:ruleActionConstraintGuard",
31
+ ],
18
32
  };
19
33
 
20
34
  function attrValue(ind, prop) {
@@ -45,14 +59,13 @@ function checkFact(ind, violations) {
45
59
  }
46
60
 
47
61
  /** 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). */
62
+ * vocabulary; and the matching slots for that declared kind, each present
63
+ * and non-empty (RULE_SLOT_PROPS above). */
51
64
  function checkRule(ind, violations) {
52
65
  if (!nonEmpty(attrValue(ind, "mgx:ruleName"))) violations.push("a Rule needs a non-empty mgx:ruleName");
53
66
  const kind = attrValue(ind, "mgx:ruleKind");
54
67
  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)})`);
68
+ violations.push(`a Rule's mgx:ruleKind must be one of ${[...RULE_KINDS].join(" | ")} (got ${JSON.stringify(kind)})`);
56
69
  return; // no declared kind to check slots against
57
70
  }
58
71
  for (const prop of RULE_SLOT_PROPS[kind]) {
@@ -1,13 +1,15 @@
1
- // memory-ask-browser-entry.mjs — the esbuild entry for `tmct viz`'s embedded
2
- // "Ask the graph" panel's memory-graph engine.
1
+ // memory-ask-browser-entry.mjs — the esbuild entry for the ledger page's
2
+ // chat dock (`tmct viz`, src/ledger-viz.mjs).
3
3
  //
4
4
  // Re-exports `factAnswer` (src/chat.mjs) and `createInMemoryStore`
5
- // (src/memory/core.mjs), which lets the panel hand `factAnswer` the page's
5
+ // (src/memory/core.mjs), which lets the dock hand `factAnswer` the page's
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
- // bypassing the structural-graph parse pipeline this panel has no use for.
9
- import { factAnswer } from "./chat.mjs";
8
+ // bypassing the structural-graph parse pipeline this dock has no use for.
9
+ import { factAnswer, factReadBack } from "./chat.mjs";
10
10
  import { createInMemoryStore, normFactTerm } from "./memory/core.mjs";
11
11
 
12
- // normFactTerm is re-exported too, for viz.mjs's client-side term normalization.
13
- globalThis.tmctMemoryAsk = { factAnswer, createInMemoryStore, normFactTerm };
12
+ // normFactTerm is re-exported too, for the page's client-side term normalization.
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 };