@polycode-projects/the-mechanical-code-talker 6.0.19 → 6.0.21

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.
Files changed (72) hide show
  1. package/README.md +20 -23
  2. package/bin/tmct.mjs +16 -33
  3. package/corpus/LICENSES.json +0 -21
  4. package/corpus/README.md +10 -13
  5. package/corpus/reference/manifest.json +19 -19
  6. package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
  7. package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
  8. package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
  9. package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
  10. package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
  11. package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
  12. package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
  13. package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
  14. package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
  15. package/corpus/tier2/generate.mjs +6 -142
  16. package/corpus/tier2/manifest.json +0 -42
  17. package/package.json +4 -4
  18. package/src/adapters/corpus/child-seed.mjs +74 -0
  19. package/src/adapters/corpus/conceptnet.mjs +45 -26
  20. package/src/adapters/memory/blocks.mjs +7 -1
  21. package/src/adapters/memory/core.mjs +453 -103
  22. package/src/adapters/memory/corpus-bands.mjs +33 -10
  23. package/src/adapters/memory/inspect.mjs +24 -5
  24. package/src/adapters/memory/rows.mjs +106 -9
  25. package/src/adapters/memory/shacl.mjs +10 -3
  26. package/src/domain/ask.mjs +27 -10
  27. package/src/domain/cli-verbs.mjs +3 -4
  28. package/src/domain/completions/group.mjs +8 -3
  29. package/src/domain/completions/infer.mjs +7 -2
  30. package/src/domain/completions/prune.mjs +5 -1
  31. package/src/domain/completions/rank.mjs +7 -2
  32. package/src/domain/digest/compose.mjs +5 -1
  33. package/src/domain/digest/select.mjs +12 -6
  34. package/src/domain/domain.mjs +15 -8
  35. package/src/domain/el-classify.mjs +11 -2
  36. package/src/domain/fact-phrase.mjs +86 -4
  37. package/src/domain/hash.mjs +9 -0
  38. package/src/domain/memory/bias.mjs +8 -4
  39. package/src/domain/memory/capability.mjs +12 -6
  40. package/src/domain/memory/fact-order.mjs +29 -0
  41. package/src/domain/memory/resolution.mjs +3 -0
  42. package/src/domain/news-feed.mjs +422 -56
  43. package/src/domain/reference-pack.mjs +5 -0
  44. package/src/domain/sense-scope.mjs +116 -0
  45. package/src/domain/sense-split.mjs +1 -1
  46. package/src/domain/syllogise.mjs +21 -13
  47. package/src/domain/tableau.mjs +23 -14
  48. package/src/domain/worlds-pack.mjs +5 -1
  49. package/src/services/adventure-autoplay.mjs +6 -1
  50. package/src/services/adventure-editor.mjs +43 -21
  51. package/src/services/adventure-viz.mjs +26 -9
  52. package/src/services/adventure.mjs +40 -10
  53. package/src/services/chat.mjs +253 -113
  54. package/src/services/extensions.mjs +51 -58
  55. package/src/services/extract-facts.mjs +670 -95
  56. package/src/services/init.mjs +4 -4
  57. package/src/services/ledger-viz.mjs +9 -4
  58. package/src/services/memory-panel-viz.mjs +4 -5
  59. package/src/services/mud-editor.mjs +40 -16
  60. package/src/services/mud-viz.mjs +8 -2
  61. package/src/services/mudiii-turn.mjs +5 -3
  62. package/src/services/mudiii-viz.mjs +8 -2
  63. package/src/services/news.mjs +277 -11
  64. package/src/services/research-viz.mjs +1 -1
  65. package/src/services/sprite-catalog-viz.mjs +10 -5
  66. package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
  67. package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
  68. package/src/surfaces/web/mud-browser-entry.mjs +7 -11
  69. package/src/surfaces/web/research-browser-entry.mjs +5 -2
  70. package/corpus/tier2/aws.jsonl +0 -39
  71. package/corpus/tier2/java.jsonl +0 -31
  72. package/corpus/tier2/python.jsonl +0 -30
@@ -27,30 +27,28 @@ import { assertValidRow } from "./row-backend.mjs";
27
27
  export const BAND_PARTITION_PREFIX = "corpus:";
28
28
  export const MANIFEST_SORT_KEY = "manifest";
29
29
 
30
- /** The three bands this plan ships a build pipeline and a loader for. */
30
+ /** The bands this repo ships a build pipeline and a loader for. */
31
31
  export const FIRST_CLASS_BANDS = Object.freeze([
32
- "wikidata-slice",
32
+ "child",
33
+ "conceptnet",
33
34
  "wordnet-complete",
34
- "conceptnet-full",
35
35
  ]);
36
36
 
37
- // Reserved so the partition name is spoken for; no pipeline for it ships
38
- // here. Its own design doc covers the band when it lands.
39
- export const RESERVED_BAND_NAMES = Object.freeze(["simplewiki-derived"]);
40
-
41
37
  /** The licence and attribution notice each first-class band's content
42
38
  * carries — a property of the band's identity, not of any one load, so the
43
39
  * loader reads it from here rather than taking it as a per-invocation flag.
44
40
  * `notice` is a repo-relative path to the human-readable attribution file;
45
41
  * null when the licence carries no attribution burden. */
46
42
  export const BAND_LICENSES = Object.freeze({
47
- "wikidata-slice": Object.freeze({ license: "CC0-1.0", notice: null }),
43
+ // The child pack is ConceptNet-derived, so it carries ConceptNet's own
44
+ // share-alike terms rather than the maintainer-owned seed script's.
45
+ child: Object.freeze({ license: "CC-BY-SA-4.0", notice: "corpus/child/LICENSE-NOTICE" }),
46
+ conceptnet: Object.freeze({ license: "CC-BY-SA-4.0", notice: "corpus/conceptnet/LICENSE-NOTICE" }),
48
47
  "wordnet-complete": Object.freeze({ license: "CC-BY-4.0", notice: "corpus/wordnet/LICENSE-NOTICE" }),
49
- "conceptnet-full": Object.freeze({ license: "CC-BY-SA-4.0", notice: "corpus/conceptnet/conceptnet-full.NOTICE" }),
50
48
  });
51
49
 
52
50
  /** `{license, notice}` for `band`, or both null when the band carries no
53
- * entry here (a consumer's own band, outside the three first-class ones). */
51
+ * entry here (a consumer's own band, outside the first-class ones). */
54
52
  export function bandLicenseInfo(band) {
55
53
  return BAND_LICENSES[band] ?? { license: null, notice: null };
56
54
  }
@@ -153,3 +151,28 @@ export function bandFactRow({ subject, predicate, object, provenance = "", band,
153
151
  };
154
152
  return assertValidRow(row, { provenance });
155
153
  }
154
+
155
+ /** The inverse of `bandFactRow`: the `{subject, predicate, object,
156
+ * provenance}` triple a band's own wire row carries, read back off its
157
+ * `json` field. A caller that grounds a term straight from a band Query (no
158
+ * article, no extraction) hands this triple to `appendFacts` unchanged, so
159
+ * the fact lands under the SAME content-addressed id and the SAME
160
+ * `corpus:<band> ...` provenance the band shipped it with — trust reads it
161
+ * as a corpus source, not a live research one.
162
+ *
163
+ * Null when `json` fails to parse or the individual carries no
164
+ * rdf:subject/predicate/object triple — a malformed row degrades to
165
+ * "nothing to fold in" rather than a throw, since a caller reads a whole
166
+ * page of rows and one bad one should not cost the rest. */
167
+ export function factFromBandRow(row) {
168
+ let parsed;
169
+ try { parsed = JSON.parse(row?.json ?? ""); } catch { return null; }
170
+ const attributes = parsed?.individual?.attributes;
171
+ if (!Array.isArray(attributes)) return null;
172
+ const valueOf = (prop) => attributes.find((a) => a?.prop === prop)?.value;
173
+ const subject = valueOf("rdf:subject");
174
+ const predicate = valueOf("rdf:predicate");
175
+ const object = valueOf("rdf:object");
176
+ if (!subject || !predicate || !object) return null;
177
+ return { subject, predicate, object, provenance: valueOf("mgx:factProvenance") || "" };
178
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { loadMemory, UTTERANCE_CLASS, IN_REPLY_TO_PROP, readFactRows, findContradictions } from "./core.mjs";
7
7
  import { loadBlockIndex } from "./blocks.mjs";
8
+ import { compareFactsByContent } from "../../domain/memory/fact-order.mjs";
8
9
  import { buildTableauKb, findTableauViolations } from "../../domain/tableau.mjs";
9
10
  import { elUnsatisfiableClasses } from "../../domain/el-classify.mjs";
10
11
 
@@ -33,6 +34,23 @@ export function balancedSample(items, k) {
33
34
  }
34
35
 
35
36
  const attrOf = (ind, key) => (ind?.attributes || []).find((a) => a.key === key)?.value || "";
37
+
38
+ /** Codepoint order, never localeCompare — this text is read on whatever machine
39
+ * holds the store, and two locales have to land on the same lines. */
40
+ const byCodepoint = (a, b) => {
41
+ const ka = String(a ?? "");
42
+ const kb = String(b ?? "");
43
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
44
+ };
45
+
46
+ /** Order two individuals by their own id, in codepoint order. Every id here is
47
+ * derived from content — a Fact hashes its triple, an Utterance reads
48
+ * `utt:<session>#<ts>#<role>`, a Source keys on its provenance tag — so this
49
+ * is a pure function of the stored set, and the utterance case still comes out
50
+ * in the order the conversation happened. Without it the class samples below
51
+ * span the payload in the order the individuals were written, which is arrival
52
+ * order: two peers holding one store would sample different rows. */
53
+ const byIndividualId = (a, b) => byCodepoint(a?.id, b?.id);
36
54
  const truncate = (s, cap) => {
37
55
  const t = String(s ?? "").replace(/\s+/g, " ").trim();
38
56
  return t.length > cap ? `${t.slice(0, cap - 1)}…` : t;
@@ -55,7 +73,8 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
55
73
  if (!byClass.has(cls)) byClass.set(cls, []);
56
74
  byClass.get(cls).push(ind);
57
75
  }
58
- const classes = [...byClass.entries()].sort((a, b) => b[1].length - a[1].length);
76
+ for (const members of byClass.values()) members.sort(byIndividualId);
77
+ const classes = [...byClass.entries()].sort((a, b) => b[1].length - a[1].length || byCodepoint(a[0], b[0]));
59
78
  lines.push(`memory — ${individuals.length} individuals: ${classes.map(([c, of]) => `${of.length} ${c}`).join(", ")}.`);
60
79
  for (const [cls, of] of classes) {
61
80
  const k = sampleSize(of.length, { verbose });
@@ -68,7 +87,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
68
87
  .filter((r) => r.sourceIds.length || r.provenance)
69
88
  .sort((a, b) => b.trust - a.trust
70
89
  || b.sourceIds.length - a.sourceIds.length
71
- || `${a.subject} ${a.predicate} ${a.object}`.localeCompare(`${b.subject} ${b.predicate} ${b.object}`));
90
+ || compareFactsByContent(a, b));
72
91
  if (ranked.length) {
73
92
  lines.push("", "top facts by trust:");
74
93
  for (const r of ranked.slice(0, verbose ? 8 : 3)) {
@@ -137,7 +156,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
137
156
  const pairs = (replyGroup?.examples || [])
138
157
  .map((e) => ({ a: byId.get(e.subject), q: byId.get(e.object) }))
139
158
  .filter((p) => p.a && p.q && p.a.class === UTTERANCE_CLASS)
140
- .sort((x, y) => attrOf(y.a, "ts").localeCompare(attrOf(x.a, "ts")));
159
+ .sort((x, y) => byCodepoint(attrOf(y.a, "ts"), attrOf(x.a, "ts")) || byIndividualId(x.a, y.a));
141
160
  if (pairs.length) {
142
161
  lines.push("", `recent Q→A pairs (${pairs.length} recorded):`);
143
162
  for (const p of pairs.slice(0, verbose ? 8 : 3)) {
@@ -153,7 +172,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
153
172
  const tokens = entries.reduce((n, [, b]) => n + (b.tokens?.length || 0), 0);
154
173
  const top = entries
155
174
  .slice()
156
- .sort((a, b) => (b[1].rank ?? 0) - (a[1].rank ?? 0) || a[0].localeCompare(b[0]))
175
+ .sort((a, b) => (b[1].rank ?? 0) - (a[1].rank ?? 0) || byCodepoint(a[0], b[0]))
157
176
  .slice(0, verbose ? 8 : 3);
158
177
  lines.push("", `blocks — ${entries.length} folded session block${entries.length === 1 ? "" : "s"}, ${tokens} indexed tokens.`);
159
178
  lines.push(` top by rank: ${top.map(([id, b]) => `${String(id).slice(0, 8)} (${(b.rank ?? 0).toFixed(3)})`).join(", ")}`);
@@ -169,7 +188,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
169
188
  const freq = new Map();
170
189
  for (const f of facts) if (clean(f.object)) freq.set(f.object, (freq.get(f.object) || 0) + 1);
171
190
  const terms = [...freq.entries()]
172
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
191
+ .sort((a, b) => b[1] - a[1] || byCodepoint(a[0], b[0]))
173
192
  .map(([t]) => t)
174
193
  .slice(0, 3);
175
194
  const sample = facts.find((f) => clean(f.subject) && terms.includes(f.object));
@@ -89,8 +89,13 @@ function storedIndividualForm(ind) {
89
89
  /** The ord each row key carries. An existing key keeps the ord it already had
90
90
  * and a new one takes the next free number, matching how a store keeps a row's
91
91
  * sort position across an update. Without prior rows the ords are the payload's
92
- * own array order. */
93
- function ordAssigner(priorRows) {
92
+ * own array order.
93
+ *
94
+ * `baseOrds` is a read-only layer's key -> ord map, consulted after the prior
95
+ * rows so a key both layers hold keeps the row's own ord. It is read in place:
96
+ * a caller holding one for the life of its handle hands it over rather than
97
+ * projecting a row per key and parsing each back. */
98
+ function ordAssigner(priorRows, baseOrds) {
94
99
  const priorOrds = new Map();
95
100
  let next = 0;
96
101
  for (const row of priorRows || []) {
@@ -100,8 +105,9 @@ function ordAssigner(priorRows) {
100
105
  priorOrds.set(row.rowKey, ord);
101
106
  if (ord >= next) next = ord + 1;
102
107
  }
108
+ for (const ord of baseOrds?.values() || []) if (Number.isFinite(ord) && ord >= next) next = ord + 1;
103
109
  return (rowKey) => {
104
- const prior = priorOrds.get(rowKey);
110
+ const prior = priorOrds.get(rowKey) ?? baseOrds?.get(rowKey);
105
111
  if (prior !== undefined) return prior;
106
112
  const ord = next;
107
113
  next += 1;
@@ -139,14 +145,16 @@ const OVERSIZED_ROW_POSTURES = new Set(["throw", "drop", "keep"]);
139
145
  *
140
146
  * `priorRows` are the rows this payload was last projected as; pass them and
141
147
  * every unchanged row comes back byte-identical, so `diffRows` writes only
142
- * what actually moved. `onOversizedRow` is "throw" (default), "drop", or
143
- * "keep" (rows that never reach the wire see `admitRow`), and `log` takes
144
- * the drop notices. */
145
- export function payloadToRows(payload, { priorRows = null, onOversizedRow = "throw", log = warnToConsole } = {}) {
148
+ * what actually moved. `priorOrds` is the same information for a read-only
149
+ * layer that already holds it as a key -> ord map (a sqlite seed's own key
150
+ * columns), read under the prior rows. `onOversizedRow` is "throw" (default),
151
+ * "drop", or "keep" (rows that never reach the wire see `admitRow`), and
152
+ * `log` takes the drop notices. */
153
+ export function payloadToRows(payload, { priorRows = null, priorOrds = null, onOversizedRow = "throw", log = warnToConsole } = {}) {
146
154
  if (!OVERSIZED_ROW_POSTURES.has(onOversizedRow)) {
147
155
  throw new TypeError(`onOversizedRow must be "throw", "drop", or "keep", got ${JSON.stringify(onOversizedRow)}`);
148
156
  }
149
- const ordFor = ordAssigner(priorRows);
157
+ const ordFor = ordAssigner(priorRows, priorOrds);
150
158
  const posture = { onOversizedRow, log };
151
159
  const rows = [];
152
160
 
@@ -239,6 +247,87 @@ function carryForward(payload, carried) {
239
247
  });
240
248
  }
241
249
 
250
+ // The ids of the assembled individuals in ROW order, kept beside the assembled
251
+ // array. `sortFactIndividualsById` lifts the Facts out of the slots row order
252
+ // put them in, so the array itself no longer says which slot each Fact came
253
+ // from — and a removal needs exactly that, because dropping a Fact drops one of
254
+ // those slots, not the position the sort moved it to. Non-enumerable and
255
+ // symbol-keyed for the same reason as the derivations above; the `individuals`
256
+ // reference is the guard, so a copy or a rebuilt array is never reconciled
257
+ // against an order that describes a different one.
258
+ const ASSEMBLED_ROW_ORDER = Symbol("tmct.assembledRowOrder");
259
+
260
+ function carryRowOrder(payload, ids) {
261
+ Object.defineProperty(payload, ASSEMBLED_ROW_ORDER, {
262
+ value: { individuals: payload.individuals, ids },
263
+ writable: true, configurable: true, enumerable: false,
264
+ });
265
+ }
266
+
267
+ /** The row order this payload was assembled in, or null when it carries none
268
+ * or the one it carries describes a different individuals array. */
269
+ function carriedRowOrder(payload) {
270
+ const carried = payload?.[ASSEMBLED_ROW_ORDER];
271
+ if (!carried || carried.individuals !== payload.individuals) return null;
272
+ return carried;
273
+ }
274
+
275
+ /** True when `removedIds` can be dropped from this payload's individuals
276
+ * incrementally — every id is one the assembly actually holds, and the payload
277
+ * still carries the row order that says which slot each one occupies. Asked
278
+ * before anything is mutated, so a caller that gets `false` can rebuild from a
279
+ * payload nothing has touched. */
280
+ export function canDropAssembledIndividuals(payload, removedIds) {
281
+ const carried = carriedRowOrder(payload);
282
+ if (!carried) return false;
283
+ const held = new Set(carried.ids);
284
+ for (const id of removedIds) if (!held.has(String(id))) return false;
285
+ return true;
286
+ }
287
+
288
+ /** Drop `removedIds` from an assembled payload's individuals, in place, leaving
289
+ * the array a rebuild from the remaining rows would have produced. Reads the
290
+ * row order back, filters the dropped ids out of it, and refills the array in
291
+ * that order — so the fact slots that survive are the ones the surviving ROWS
292
+ * own, which is what `sortFactIndividualsById` then sorts into. Returns false
293
+ * when the payload has no usable row order, having changed nothing. */
294
+ export function dropAssembledIndividuals(payload, removedIds) {
295
+ const carried = carriedRowOrder(payload);
296
+ if (!carried) return false;
297
+ const dropped = new Set([...removedIds].map((id) => String(id)));
298
+ const byId = new Map();
299
+ for (const ind of payload.individuals || []) if (ind?.id) byId.set(String(ind.id), ind);
300
+ const keptIds = [];
301
+ const kept = [];
302
+ for (const id of carried.ids) {
303
+ if (dropped.has(id)) continue;
304
+ const ind = byId.get(id);
305
+ if (!ind) return false;
306
+ keptIds.push(id);
307
+ kept.push(ind);
308
+ }
309
+ payload.individuals = kept;
310
+ carried.individuals = kept;
311
+ carried.ids = keptIds;
312
+ return true;
313
+ }
314
+
315
+ /** Forget the row order this payload carries, for a caller that rewrote the ids
316
+ * the order names (the load-time legacy-fact-id heal). The next removal
317
+ * rebuilds from rows instead of patching. */
318
+ export function dropAssembledRowOrder(payload) {
319
+ if (payload && payload[ASSEMBLED_ROW_ORDER]) payload[ASSEMBLED_ROW_ORDER] = null;
320
+ }
321
+
322
+ /** Record one newly-assembled individual's id at the tail of the row order —
323
+ * where a row keyed for the first time lands, since `payloadToRows` gives it an
324
+ * ord past every ord already assembled. A payload carrying no row order stays
325
+ * that way, and the next removal rebuilds instead of patching. */
326
+ export function appendAssembledRowOrder(payload, id) {
327
+ const carried = carriedRowOrder(payload);
328
+ if (carried) carried.ids.push(String(id));
329
+ }
330
+
242
331
  /** What this payload can reconcile against, or null when it must derive
243
332
  * instead. `index` identity is the guard: state that describes some other
244
333
  * prose index cannot be applied to this one. `needSupersessions` narrows to
@@ -469,8 +558,16 @@ export function rowsToPayload(rows, { meta = null } = {}) {
469
558
  individualEntries.sort(byOrdThenRowKey);
470
559
  groupEntries.sort(byOrdThenRowKey);
471
560
 
472
- payload.individuals = individualEntries.map((e) => e.individual).filter(Boolean);
561
+ const rowOrderIds = [];
562
+ const individuals = [];
563
+ for (const entry of individualEntries) {
564
+ if (!entry.individual) continue;
565
+ individuals.push(entry.individual);
566
+ rowOrderIds.push(String(entry.individual.id ?? ""));
567
+ }
568
+ payload.individuals = individuals;
473
569
  payload.objectProperties = groupEntries.map((e) => e.group).filter(Boolean);
570
+ carryRowOrder(payload, rowOrderIds);
474
571
  return renormalizeAssembledPayload(payload);
475
572
  }
476
573
 
@@ -18,12 +18,19 @@ export const EXTRACTION_FINDINGS = Object.freeze([
18
18
  "clause-fallback", // the row grounded from a clause fragment after the whole sentence declined
19
19
  "pronoun-carry", // the subject was substituted from the paragraph's pronoun carry
20
20
  "definitional-frame", // the row came from a definitional copula frame ("X is the name for Y")
21
+ "reported-speech", // the row was read from a reported-speech clause, its speaker attributed separately
21
22
  ]);
22
23
  const EXTRACTION_FINDING_SET = new Set(EXTRACTION_FINDINGS);
23
- const RULE_KINDS = new Set([
24
+
25
+ // The closed vocabulary of a taught Rule's SHAPE tag — three structural
26
+ // kinds plus the action-* family shared by one taught sentence's sibling
27
+ // Rules. Exported (like EXTRACTION_FINDINGS above) so an estate guard can
28
+ // cross-check it against its own prose mirrors in the ontology files.
29
+ export const RULE_KINDS = Object.freeze([
24
30
  "compose2", "filter", "recursive",
25
31
  "action-signature", "action-precond", "action-effect", "action-constraint",
26
32
  ]);
33
+ const RULE_KIND_SET = new Set(RULE_KINDS);
27
34
 
28
35
  // Mirrors the REQUIRED subset of core.mjs's own (unexported) RULE_SLOT_SPEC —
29
36
  // kept in sync by hand. Two kinds there also carry optional slots
@@ -140,8 +147,8 @@ function checkFact(ind, violations) {
140
147
  function checkRule(ind, violations) {
141
148
  if (!nonEmpty(attrValue(ind, "mgx:ruleName"))) violations.push("a Rule needs a non-empty mgx:ruleName");
142
149
  const kind = attrValue(ind, "mgx:ruleKind");
143
- if (!kind || !RULE_KINDS.has(kind)) {
144
- violations.push(`a Rule's mgx:ruleKind must be one of ${[...RULE_KINDS].join(" | ")} (got ${JSON.stringify(kind)})`);
150
+ if (!kind || !RULE_KIND_SET.has(kind)) {
151
+ violations.push(`a Rule's mgx:ruleKind must be one of ${RULE_KINDS.join(" | ")} (got ${JSON.stringify(kind)})`);
145
152
  return; // no declared kind to check slots against
146
153
  }
147
154
  for (const prop of RULE_SLOT_PROPS[kind]) {
@@ -43,6 +43,23 @@ import { pickPhrase } from "./answer-variants.mjs";
43
43
  export { normalizeQuery, applyNegationFrames };
44
44
  import { defaultNlp } from "./interpret/nlp-registry.mjs";
45
45
 
46
+ /** Codepoint order, never localeCompare. Every string sorted through this is a
47
+ * label, id or attribute value read off the graph. Several of these sorts get
48
+ * read at `[0]` (the newest commit a query substitutes in, the entry-point
49
+ * module a where-defined answer names) or cut to a top-N, so the comparator
50
+ * picks WHICH individual the answer is about. A locale-sensitive compare would
51
+ * let two readers ask one graph the same question and be told about different
52
+ * commits. */
53
+ const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
54
+
55
+ /** Commits newest first, ties broken on the commit's own id. The date is
56
+ * ISO-8601, so a codepoint compare IS the date compare, and undated commits
57
+ * sort last. The id tiebreak is what makes `[0]` a pure function of the
58
+ * graph: two commits sharing a date would otherwise be separated by whichever
59
+ * edge happened to be walked first. */
60
+ const byNewestCommit = (dateOf) => (a, b) =>
61
+ byCodepoint(dateOf(b), dateOf(a)) || byCodepoint(String(a.id), String(b.id));
62
+
46
63
  // Per-graph, per-kind memo; a local copy of codegraph.mjs's private
47
64
  // edgesOfKind. Named differently from codegraph.mjs's own cache: the inlined
48
65
  // viewer bundle concatenates a stripped codegraph.mjs + this file into one
@@ -2045,7 +2062,7 @@ function computeFind(graph, entityType, term) {
2045
2062
  * the architecture map prints, so the two surfaces agree. */
2046
2063
  function packageIndividuals(graph) {
2047
2064
  return [...packageCounts(modulesOf(graph)).entries()]
2048
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
2065
+ .sort((a, b) => b[1] - a[1] || byCodepoint(a[0], b[0]))
2049
2066
  .map(([dir]) => ({ id: `pkg:${dir}`, label: dir, class: "Package" }));
2050
2067
  }
2051
2068
 
@@ -2364,7 +2381,7 @@ export function degreeMetric(graph, ind, metric) {
2364
2381
  function evalRecentCommits(graph) {
2365
2382
  const commits = graph.individuals.filter((i) => i.class === "Commit");
2366
2383
  const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
2367
- commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
2384
+ commits.sort(byNewestCommit(dateOf));
2368
2385
  return { compositeKind: "recentCommits", matches: commits };
2369
2386
  }
2370
2387
 
@@ -2407,7 +2424,7 @@ function evalCommitFilter(graph, ast) {
2407
2424
  if (op === "after") return d > pivotDate;
2408
2425
  return d === pivotDate; // "on"
2409
2426
  })
2410
- .sort((a, b) => dateOf(b).localeCompare(dateOf(a)) || String(a.id).localeCompare(String(b.id)));
2427
+ .sort(byNewestCommit(dateOf));
2411
2428
  // A kind-headed window reads what the qualifying commits touched at that
2412
2429
  // grain; the bare shape answers with the commits themselves.
2413
2430
  const touched = entityType && entityType !== "Commit" && entityType !== "Change"
@@ -2450,7 +2467,7 @@ function evalTemporal(graph, ast, opts) {
2450
2467
  // reverseOverSet(touches) collects touching commits across both grains.
2451
2468
  const commits = reverseOverSet(graph, "touches", "Commit", ids);
2452
2469
  const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
2453
- commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
2470
+ commits.sort(byNewestCommit(dateOf));
2454
2471
  return { compositeKind: "temporal", matches: commits, entityType: ast.entityType, innerCount: inner.length };
2455
2472
  }
2456
2473
 
@@ -3899,7 +3916,7 @@ function rankEntryPointModules(graph, term) {
3899
3916
  fixture: isTestFixturePath(ind.label) ? 1 : 0,
3900
3917
  }))
3901
3918
  .sort((a, b) => (b.named - a.named) || (a.depth - b.depth) || (a.fixture - b.fixture)
3902
- || String(a.ind.label).localeCompare(String(b.ind.label)))
3919
+ || byCodepoint(String(a.ind.label), String(b.ind.label)))
3903
3920
  .map((x) => x.ind);
3904
3921
  }
3905
3922
 
@@ -3939,7 +3956,7 @@ function subclassClosure(graph, ind, kind) {
3939
3956
  found.set(next.id, next);
3940
3957
  queue.push(...childrenOf(next));
3941
3958
  }
3942
- return [...found.values()].sort((a, b) => String(a.label).localeCompare(String(b.label)));
3959
+ return [...found.values()].sort((a, b) => byCodepoint(String(a.label), String(b.label)));
3943
3960
  }
3944
3961
 
3945
3962
  /** A graph's own vocabulary nodes carry their definition text under one of
@@ -4171,7 +4188,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
4171
4188
  const c = graph.byId.get(e.subject);
4172
4189
  if (c && c.class === "Commit") commits.push(c);
4173
4190
  }
4174
- commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
4191
+ commits.sort(byNewestCommit(dateOf));
4175
4192
  }
4176
4193
  // The dated commit this answer named is a discourse `event` referent, so a
4177
4194
  // later "was that before X was touched" binds it (see evalCommitFilter).
@@ -4203,7 +4220,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
4203
4220
  const c = graph.byId.get(e.subject);
4204
4221
  if (c && c.class === "Commit") commits.push(c);
4205
4222
  }
4206
- commits.sort((a, b) => dateOf(b).localeCompare(dateOf(a)));
4223
+ commits.sort(byNewestCommit(dateOf));
4207
4224
  // The dated commit behind the "who last touched X" answer is the same
4208
4225
  // `event` referent the when-shape registers, so either phrasing feeds a
4209
4226
  // later temporal comparison. Registered only when the commit carries a date.
@@ -5371,7 +5388,7 @@ function substituteLastCommitPhrase(graph, query) {
5371
5388
  const commits = graph.individuals.filter((i) => i.class === "Commit");
5372
5389
  if (!commits.length) return q;
5373
5390
  const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
5374
- const newest = [...commits].sort((a, b) => dateOf(b).localeCompare(dateOf(a)))[0];
5391
+ const newest = [...commits].sort(byNewestCommit(dateOf))[0];
5375
5392
  if (!newest) return q;
5376
5393
  const out = q.replace(LAST_COMMIT_PHRASE_RE, `commit ${newest.label}`);
5377
5394
  const bareTrimmed = out.trim().replace(/[?.!]+$/, "");
@@ -5559,7 +5576,7 @@ function evalWorldRelation(graph, ast) {
5559
5576
  // never placed outright.
5560
5577
  const pairs = [...stated.values(), ...[...taught.values()].filter((p) => !stated.has(p.subject.id))]
5561
5578
  .sort((a, b) => (
5562
- String(a.subject.id).localeCompare(String(b.subject.id)) || String(a.object).localeCompare(String(b.object))
5579
+ byCodepoint(String(a.subject.id), String(b.subject.id)) || byCodepoint(String(a.object), String(b.object))
5563
5580
  ));
5564
5581
  return {
5565
5582
  compositeKind: "worldRelation",
@@ -58,12 +58,11 @@ export const CLI_VERBS = [
58
58
  prose: ["initialize a repo for tmct (default: cwd): .tmct/,"],
59
59
  flags: [
60
60
  { flag: "[--force]", prose: ["tmct.toml, .tmct/TOOLS.md (the cold-tool catalog),", "tier-1 corpus seed, provenance record"] },
61
- { flag: "[--corpus <id|path>]", prose: ["also seed a corpus — a tier-2 manifest id (aws|python|java|", "general) or a jsonl file path — opt-in, offline, $0"] },
61
+ { flag: "[--corpus <id|path>]", prose: ["also seed a corpus — a bundle name (code|conceptnet|child|", "namenet|general) or a jsonl file path — opt-in, offline, $0"] },
62
62
  { flag: "[--ontology <name|path>]", prose: ["activate+seed an ontology bundle (a recognized name or a path)"] },
63
63
  { flag: "[--lexicon <name|path>]", prose: ["activate a lexicon bundle (recognized name or a path;", "merged read-time, never seeded — see mergedLexiconExtra)"] },
64
64
  { flag: "[--graph <path>]", prose: ["set graph_file/graph_files in tmct.toml (repeatable)"] },
65
65
  { flag: "[--config <path>]", prose: ["write to an alternate tmct.toml location"] },
66
- { flag: "[--detect]", prose: ["suggest a tier-2 corpus from the repo's manifests", "(pyproject.toml → python, pom.xml → java); never seeds unasked"] },
67
66
  { flag: "[--with-persona <name>]", prose: ["write an explicit [extensions]/[bias] preset into tmct.toml", "(\"code\" — today's implicit default, made explicit)"] },
68
67
  { flag: "[--persona-size <medium|large>]", prose: ["grow the default \"human\" persona's fact count", "beyond Small (the default): \"medium\" activates", "human-medium.jsonl (~1,608 facts total), \"large\" also", "activates human-large.jsonl (~13,600 facts total,", "with genuine multi-hop hypernym chains) — additive", "size tiers of the SAME bundle, not separate personas"] },
69
68
  { flag: "[--memory-backend <default|memory|sqlite>]", prose: ["write tmct.toml's [memory] backend", "(same flag name as `tmct chat`) — a later `tmct chat`", "in this repo picks it up with no flag needed"] },
@@ -216,9 +215,9 @@ export const CLI_VERBS = [
216
215
  mode: "corpus",
217
216
  errorLabel: "corpus load",
218
217
  usage: "tmct corpus load <band> [--table <name>] [--source <path>] [--dry-run]",
219
- prose: ["load a shared, read-only corpus band (wikidata-slice, wordnet-complete,"],
218
+ prose: ["load a shared, read-only corpus band (wordnet-complete, or a"],
220
219
  flags: [
221
- { flag: "[--table <name>]", prose: ["conceptnet-full) into a DynamoDB row-backend table from a jsonl of", "wire-row-shaped facts (default table from TMCT_DYNAMO_TABLE); a", "source whose digest already matches the band's manifest is a no-op"] },
220
+ { flag: "[--table <name>]", prose: ["consumer's own) into a DynamoDB row-backend table from a jsonl of", "wire-row-shaped facts (default table from TMCT_DYNAMO_TABLE); a", "source whose digest already matches the band's manifest is a no-op"] },
222
221
  { flag: "[--source <path>]", prose: ["the band's jsonl (a scripts/corpus-bands/ build output, or any jsonl", "in the same wire-row shape)"] },
223
222
  { flag: "[--dry-run]", prose: ["report the row count and source digest without writing anything"] },
224
223
  ],
@@ -9,6 +9,11 @@ import { requireInjected } from "./injected.mjs";
9
9
 
10
10
  const LABEL_TOKEN_COUNT = 5;
11
11
 
12
+ // Codepoint order, never localeCompare — hit ids and content tokens trace
13
+ // back to the memory store, and two readers must land on the same order
14
+ // regardless of locale.
15
+ const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
16
+
12
17
  /** Plain union-find (path halving, union-by-index) — small N here (a single broad search's
13
18
  * hit count). */
14
19
  function unionFind(n) {
@@ -83,7 +88,7 @@ export function groupHits(hits, { overlapMin, store } = {}) {
83
88
  for (const memberIdx of componentIdx.values()) {
84
89
  const members = memberIdx
85
90
  .map((i) => byId.get(ids[i]))
86
- .sort((a, b) => a.id.localeCompare(b.id));
91
+ .sort((a, b) => byCodepoint(a.id, b.id));
87
92
  const memberIds = members.map((m) => m.id);
88
93
 
89
94
  // label tokens: rank by member coverage, then IDF, then token text (deterministic).
@@ -92,7 +97,7 @@ export function groupHits(hits, { overlapMin, store } = {}) {
92
97
  for (const t of new Set(tokensById[ids[i]])) coverage.set(t, (coverage.get(t) || 0) + 1);
93
98
  }
94
99
  const tokens = [...coverage.keys()]
95
- .sort((a, b) => (coverage.get(b) - coverage.get(a)) || (idf(b) - idf(a)) || a.localeCompare(b))
100
+ .sort((a, b) => (coverage.get(b) - coverage.get(a)) || (idf(b) - idf(a)) || byCodepoint(a, b))
96
101
  .slice(0, LABEL_TOKEN_COUNT);
97
102
 
98
103
  groups.push({
@@ -104,6 +109,6 @@ export function groupHits(hits, { overlapMin, store } = {}) {
104
109
  });
105
110
  }
106
111
 
107
- groups.sort((a, b) => a.memberIds[0].localeCompare(b.memberIds[0]));
112
+ groups.sort((a, b) => byCodepoint(a.memberIds[0], b.memberIds[0]));
108
113
  return groups;
109
114
  }
@@ -16,6 +16,11 @@ import { requireInjected } from "./injected.mjs";
16
16
  // supply their own copy in the `helpers` bag resolveRelationChase expects.
17
17
  const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
18
18
 
19
+ // Codepoint order, never localeCompare — group ids trace back to memory-store
20
+ // block ids, and two readers must land on the same relation order regardless
21
+ // of locale.
22
+ const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
23
+
19
24
  // The taught ISA-family predicates (stored edges only, not their transitive closure).
20
25
  const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
21
26
 
@@ -281,7 +286,7 @@ export async function inferRelations(groups, memory, { store } = {}) {
281
286
  relationNames: relationNameCandidates(rows),
282
287
  };
283
288
 
284
- const sorted = list.slice().sort((x, y) => x.id.localeCompare(y.id));
289
+ const sorted = list.slice().sort((x, y) => byCodepoint(x.id, y.id));
285
290
  const out = [];
286
291
 
287
292
  for (let i = 0; i < sorted.length; i += 1) {
@@ -311,6 +316,6 @@ export async function inferRelations(groups, memory, { store } = {}) {
311
316
  }
312
317
  }
313
318
 
314
- out.sort((x, y) => x.from.localeCompare(y.from) || x.to.localeCompare(y.to) || x.relation.localeCompare(y.relation));
319
+ out.sort((x, y) => byCodepoint(x.from, y.from) || byCodepoint(x.to, y.to) || byCodepoint(x.relation, y.relation));
315
320
  return out;
316
321
  }
@@ -8,6 +8,10 @@
8
8
 
9
9
  const DEFAULT_MAX_SENTENCES_PER_GROUP = 3;
10
10
 
11
+ // Codepoint order, never localeCompare — group ids trace back to memory-store
12
+ // block ids, and two readers must land on the same order regardless of locale.
13
+ const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
14
+
11
15
  /** Every group id referenced as either side of an asserted relation. */
12
16
  function relatedGroupIdsOf(relations) {
13
17
  const set = new Set();
@@ -56,7 +60,7 @@ export function pruneCompletion(state = {}, { maxSentencesPerGroup = DEFAULT_MAX
56
60
 
57
61
  const relatedGroupIds = relatedGroupIdsOf(relations);
58
62
 
59
- const sortedGroups = groups.slice().sort((a, b) => a.id.localeCompare(b.id));
63
+ const sortedGroups = groups.slice().sort((a, b) => byCodepoint(a.id, b.id));
60
64
  for (const g of sortedGroups) {
61
65
  const ranked = Array.isArray(rankedByGroup[g.id]) ? rankedByGroup[g.id] : [];
62
66
  const groupFeedsInference = relatedGroupIds.has(g.id);
@@ -12,6 +12,11 @@ import { requireInjected } from "./injected.mjs";
12
12
  // (no abbreviation dictionary).
13
13
  const SENTENCE_SPLIT_RE = /(?<=[.!?])\s+(?=[A-Z0-9])/;
14
14
 
15
+ // Codepoint order, never localeCompare — sourceBlockId traces back to the
16
+ // memory store's block ids, and two readers must land on the same rank order
17
+ // regardless of locale.
18
+ const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
19
+
15
20
  /**
16
21
  * Split raw block text into trimmed, non-empty sentences (order-preserving, no dedup).
17
22
  *
@@ -89,7 +94,7 @@ export function rankSentences(group, { overlapMin, query = null, store } = {}) {
89
94
  });
90
95
 
91
96
  scored.sort((a, b) => b.score - a.score
92
- || a.sourceBlockId.localeCompare(b.sourceBlockId)
93
- || a.sentence.localeCompare(b.sentence));
97
+ || byCodepoint(a.sourceBlockId, b.sourceBlockId)
98
+ || byCodepoint(a.sentence, b.sentence));
94
99
  return scored;
95
100
  }
@@ -12,6 +12,10 @@ import DEFAULT_CONFIG from "./config.json" with { type: "json" };
12
12
 
13
13
  const DESCRIPTION_FAMILIES = FAMILY_PRIORITY.filter((f) => f !== "isa" && f !== "other");
14
14
 
15
+ /** Codepoint order, never localeCompare — an ontology root is a class name
16
+ * read off stored facts, so two locales have to name the same roots. */
17
+ const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
18
+
15
19
  /** Group the selector's `selected` items by family, preserving each item's
16
20
  * ranked order, and return `{ family -> rows[] }` over the fact rows. */
17
21
  function rowsByFamily(selected) {
@@ -82,7 +86,7 @@ export function closerRootsFor(chains, usedRows, count) {
82
86
  rootRows.get(root).push(row);
83
87
  }
84
88
  const roots = [...rootCount.keys()]
85
- .sort((a, b) => (rootCount.get(b) - rootCount.get(a)) || a.localeCompare(b))
89
+ .sort((a, b) => (rootCount.get(b) - rootCount.get(a)) || byCodepoint(a, b))
86
90
  .slice(0, Math.max(0, count));
87
91
  const rows = [];
88
92
  const seen = new Set();