@polycode-projects/the-mechanical-code-talker 6.0.18 → 6.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -23
- package/bin/tmct.mjs +16 -33
- package/corpus/LICENSES.json +0 -21
- package/corpus/README.md +10 -13
- package/corpus/reference/manifest.json +19 -19
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/tier2/generate.mjs +6 -142
- package/corpus/tier2/manifest.json +0 -42
- package/package.json +6 -4
- package/src/adapters/corpus/child-seed.mjs +74 -0
- package/src/adapters/corpus/conceptnet.mjs +45 -26
- package/src/adapters/corpus/research-source.mjs +6 -2
- package/src/adapters/corpus/wikidata-live.mjs +92 -51
- package/src/adapters/memory/blocks.mjs +7 -1
- package/src/adapters/memory/core.mjs +505 -107
- package/src/adapters/memory/corpus-bands.mjs +27 -10
- package/src/adapters/memory/inspect.mjs +24 -5
- package/src/adapters/memory/rows.mjs +359 -30
- package/src/adapters/memory/shacl.mjs +10 -3
- package/src/domain/ask.mjs +27 -10
- package/src/domain/cli-verbs.mjs +3 -4
- package/src/domain/completions/group.mjs +8 -3
- package/src/domain/completions/infer.mjs +7 -2
- package/src/domain/completions/prune.mjs +5 -1
- package/src/domain/completions/rank.mjs +7 -2
- package/src/domain/digest/compose.mjs +5 -1
- package/src/domain/digest/select.mjs +12 -6
- package/src/domain/domain.mjs +15 -8
- package/src/domain/el-classify.mjs +11 -2
- package/src/domain/fact-phrase.mjs +86 -4
- package/src/domain/hash.mjs +9 -0
- package/src/domain/memory/bias.mjs +8 -4
- package/src/domain/memory/capability.mjs +12 -6
- package/src/domain/memory/fact-order.mjs +29 -0
- package/src/domain/memory/resolution.mjs +3 -0
- package/src/domain/news-feed.mjs +862 -92
- package/src/domain/reference-pack.mjs +5 -0
- package/src/domain/sense-gate.mjs +220 -0
- package/src/domain/sense-scope.mjs +116 -0
- package/src/domain/sense-split.mjs +1 -1
- package/src/domain/syllogise.mjs +60 -21
- package/src/domain/tableau.mjs +23 -14
- package/src/domain/term-ledger.mjs +16 -1
- package/src/domain/worlds-pack.mjs +5 -1
- package/src/services/adventure-autoplay.mjs +6 -1
- package/src/services/adventure-editor.mjs +43 -21
- package/src/services/adventure-viz.mjs +26 -9
- package/src/services/adventure.mjs +40 -10
- package/src/services/chat.mjs +270 -125
- package/src/services/extensions.mjs +51 -58
- package/src/services/extract-facts.mjs +906 -66
- package/src/services/init.mjs +4 -4
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +4 -5
- package/src/services/mud-editor.mjs +40 -16
- package/src/services/mud-viz.mjs +8 -2
- package/src/services/mudiii-turn.mjs +5 -3
- package/src/services/mudiii-viz.mjs +8 -2
- package/src/services/news.mjs +306 -21
- package/src/services/research-viz.mjs +1 -1
- package/src/services/sprite-catalog-viz.mjs +10 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
- package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
- package/src/surfaces/web/mud-browser-entry.mjs +7 -11
- package/src/surfaces/web/research-browser-entry.mjs +5 -2
- package/corpus/tier2/aws.jsonl +0 -39
- package/corpus/tier2/java.jsonl +0 -31
- package/corpus/tier2/python.jsonl +0 -30
package/src/domain/tableau.mjs
CHANGED
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
|
|
41
41
|
import { normFactTerm } from "./hash.mjs";
|
|
42
42
|
|
|
43
|
+
/** Codepoint order, never localeCompare. Every string sorted in this file is
|
|
44
|
+
* built from stored fact terms, and the sorts decide the ORDER RULES FIRE:
|
|
45
|
+
* which disjunct the or-rule branches on first, which axioms internalize
|
|
46
|
+
* first, which role assertions survive MAX_ROLE_ASSERTIONS. Under the step,
|
|
47
|
+
* branch and node ceilings a different firing order is a different verdict,
|
|
48
|
+
* so a locale-sensitive compare would let two machines read one KB and
|
|
49
|
+
* disagree about whether it is satisfiable. */
|
|
50
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
51
|
+
|
|
43
52
|
// ---- concept-expression AST --------------------------------------------
|
|
44
53
|
|
|
45
54
|
const atom = (name) => ({ t: "atom", name });
|
|
@@ -86,7 +95,7 @@ function pushNegation(expr, negate) {
|
|
|
86
95
|
case "or": {
|
|
87
96
|
const flip = expr.t === "and" ? "or" : "and";
|
|
88
97
|
const cs = expr.cs.map((c) => pushNegation(c, negate));
|
|
89
|
-
cs.sort((a, b) => canonicalKey(a)
|
|
98
|
+
cs.sort((a, b) => byCodepoint(canonicalKey(a), canonicalKey(b)));
|
|
90
99
|
return { t: negate ? flip : expr.t, cs };
|
|
91
100
|
}
|
|
92
101
|
case "some":
|
|
@@ -428,7 +437,7 @@ function addLabel(node, expr, from) {
|
|
|
428
437
|
}
|
|
429
438
|
|
|
430
439
|
function sortedLabelEntries(node) {
|
|
431
|
-
return [...node.labels.entries()].sort((a, b) => a[0]
|
|
440
|
+
return [...node.labels.entries()].sort((a, b) => byCodepoint(a[0], b[0]));
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
/** A node clashes when its label set holds bottom, or holds both an
|
|
@@ -649,7 +658,7 @@ function applyOrRule(branch, kb) {
|
|
|
649
658
|
for (const [key, { expr, from }] of sortedLabelEntries(node)) {
|
|
650
659
|
if (expr.t !== "or") continue;
|
|
651
660
|
if (node.branchedOn.has(key)) continue;
|
|
652
|
-
const cs = [...expr.cs].sort((a, b) => canonicalKey(a)
|
|
661
|
+
const cs = [...expr.cs].sort((a, b) => byCodepoint(canonicalKey(a), canonicalKey(b)));
|
|
653
662
|
if (cs.some((c) => node.labels.has(canonicalKey(c)))) {
|
|
654
663
|
node.branchedOn.add(key);
|
|
655
664
|
continue;
|
|
@@ -840,7 +849,7 @@ function serializeBranch(branch) {
|
|
|
840
849
|
.map((node) => ({
|
|
841
850
|
id: node.id,
|
|
842
851
|
parent: node.parent,
|
|
843
|
-
labels: [...node.labels.entries()].sort((a, b) => a[0]
|
|
852
|
+
labels: [...node.labels.entries()].sort((a, b) => byCodepoint(a[0], b[0])).map(([, { expr, from }]) => ({ expr, from })),
|
|
844
853
|
}));
|
|
845
854
|
const edges = branch.edges.map(({ from, r, to, fromFacts }) => ({ from, r, to, fromFacts: fromFacts.slice() }));
|
|
846
855
|
return { nodes, edges };
|
|
@@ -1147,7 +1156,7 @@ export function buildTableauKb(rows) {
|
|
|
1147
1156
|
// truncation is deterministic rather than arrival-ordered.
|
|
1148
1157
|
const roleAssertions = roleAssertionCandidates
|
|
1149
1158
|
.filter((r) => isIndividualTerm(r.subject) && isIndividualTerm(r.object))
|
|
1150
|
-
.sort((a, b) => a.subject
|
|
1159
|
+
.sort((a, b) => byCodepoint(a.subject, b.subject) || byCodepoint(a.predicate, b.predicate) || byCodepoint(a.object, b.object))
|
|
1151
1160
|
.slice(0, MAX_ROLE_ASSERTIONS)
|
|
1152
1161
|
.map((r) => ({ a: r.subject, r: r.predicate, b: r.object, from: [r.id] }));
|
|
1153
1162
|
|
|
@@ -1203,7 +1212,7 @@ export function buildTableauKb(rows) {
|
|
|
1203
1212
|
for (const r of negTypeRows) assertions.push({ ind: r.subject, expr: toNNF(notE(atom(r.object))), from: [r.id] });
|
|
1204
1213
|
|
|
1205
1214
|
for (const [unionId, memberRows] of unionMembersOf) {
|
|
1206
|
-
const sorted = [...memberRows].sort((a, b) => String(a.object)
|
|
1215
|
+
const sorted = [...memberRows].sort((a, b) => byCodepoint(String(a.object), String(b.object)));
|
|
1207
1216
|
const cs = sorted.map((mr) => atom(mr.object));
|
|
1208
1217
|
const ids = sorted.map((mr) => mr.id);
|
|
1209
1218
|
const orExpr = toNNF(orE(cs));
|
|
@@ -1225,7 +1234,7 @@ export function buildTableauKb(rows) {
|
|
|
1225
1234
|
// merge rule has a real carrier of the nominal to merge an outsider into.
|
|
1226
1235
|
const nominalIndividuals = new Map(); // ind -> fact ids that declared it
|
|
1227
1236
|
for (const [classId, memberRows] of oneOfMembersOf) {
|
|
1228
|
-
const sorted = [...memberRows].sort((a, b) => String(a.object)
|
|
1237
|
+
const sorted = [...memberRows].sort((a, b) => byCodepoint(String(a.object), String(b.object)));
|
|
1229
1238
|
const nomExprs = sorted.map((mr) => ({ t: "nom", ind: mr.object }));
|
|
1230
1239
|
const ids = sorted.map((mr) => mr.id);
|
|
1231
1240
|
axioms.push(mkAxiom(atom(classId), toNNF(orE(nomExprs)), ids));
|
|
@@ -1238,7 +1247,7 @@ export function buildTableauKb(rows) {
|
|
|
1238
1247
|
|
|
1239
1248
|
const differentFrom = differentFromRows
|
|
1240
1249
|
.map((r) => ({ a: r.subject, b: r.object, from: [r.id] }))
|
|
1241
|
-
.sort((a, b) => a.a
|
|
1250
|
+
.sort((a, b) => byCodepoint(a.a, b.a) || byCodepoint(a.b, b.b));
|
|
1242
1251
|
|
|
1243
1252
|
// owl:inverseOf is stored symmetrically by the grammar (both directions
|
|
1244
1253
|
// minted), but this reads either direction alone too, defensively.
|
|
@@ -1254,15 +1263,15 @@ export function buildTableauKb(rows) {
|
|
|
1254
1263
|
|
|
1255
1264
|
const subPropertyEdges = subPropertyRows
|
|
1256
1265
|
.map((r) => [r.subject, r.object])
|
|
1257
|
-
.sort((a, b) => a[0]
|
|
1266
|
+
.sort((a, b) => byCodepoint(a[0], b[0]) || byCodepoint(a[1], b[1]));
|
|
1258
1267
|
const roleNames = new Set([...roles, ...subPropertyEdges.flat(), ...inverseOf.keys(), ...inverseOf.values()]);
|
|
1259
1268
|
const roleClosure = buildRoleClosure(subPropertyEdges, roleNames);
|
|
1260
1269
|
|
|
1261
1270
|
axioms.sort((a, b) =>
|
|
1262
|
-
canonicalKey(a.sub)
|
|
1263
|
-
canonicalKey(a.sup)
|
|
1264
|
-
a.from.join(",")
|
|
1265
|
-
assertions.sort((a, b) => a.ind
|
|
1271
|
+
byCodepoint(canonicalKey(a.sub), canonicalKey(b.sub)) ||
|
|
1272
|
+
byCodepoint(canonicalKey(a.sup), canonicalKey(b.sup)) ||
|
|
1273
|
+
byCodepoint(a.from.join(","), b.from.join(",")));
|
|
1274
|
+
assertions.sort((a, b) => byCodepoint(a.ind, b.ind) || byCodepoint(canonicalKey(a.expr), canonicalKey(b.expr)));
|
|
1266
1275
|
|
|
1267
1276
|
const individuals = [...new Set([
|
|
1268
1277
|
...assertions.map((a) => a.ind),
|
|
@@ -1318,7 +1327,7 @@ export function findTableauViolations(kb, subjects = null, opts = {}) {
|
|
|
1318
1327
|
const premises = sortedUnique(result.closedClashes.flatMap((c) => c?.premises || []));
|
|
1319
1328
|
violations.push({ subject, premises, kind: describeClashKind(result.closedClashes[0]) });
|
|
1320
1329
|
}
|
|
1321
|
-
violations.sort((a, b) => a.subject
|
|
1330
|
+
violations.sort((a, b) => byCodepoint(a.subject, b.subject));
|
|
1322
1331
|
return violations;
|
|
1323
1332
|
}
|
|
1324
1333
|
|
|
@@ -65,8 +65,23 @@ const NOISE_TERM_SETS = [
|
|
|
65
65
|
FOREIGN_PARTICLE_TERMS,
|
|
66
66
|
];
|
|
67
67
|
|
|
68
|
+
/** A version or model string shatters into pieces that are mostly digits and
|
|
69
|
+
* punctuation — "Qwen3.8-2.4T" leaves "8-2" and "4t" behind. A term with no
|
|
70
|
+
* letter at all, or one carrying a digit whose digits and punctuation match or
|
|
71
|
+
* outnumber its letters, states a quantity or a version; it never names a
|
|
72
|
+
* subject a reference lookup could define. A letter-only term with interior
|
|
73
|
+
* stops ("u.s.") carries no digit, so it stays. */
|
|
74
|
+
function isShapeNoiseTerm(term) {
|
|
75
|
+
const letters = (term.match(/[a-z]/g) || []).length;
|
|
76
|
+
if (!letters) return true;
|
|
77
|
+
const digits = (term.match(/\d/g) || []).length;
|
|
78
|
+
if (!digits) return false;
|
|
79
|
+
const punctuation = (term.match(/[^a-z0-9\s]/g) || []).length;
|
|
80
|
+
return digits + punctuation >= letters;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
function isNoiseTerm(term) {
|
|
69
|
-
return NOISE_TERM_SETS.some((set) => set.has(term));
|
|
84
|
+
return NOISE_TERM_SETS.some((set) => set.has(term)) || isShapeNoiseTerm(term);
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
/** Ledger entry field order fixed once here so `ledgerPayload` serializes
|
|
@@ -72,6 +72,10 @@ export function worldProvenanceTag(worldName) {
|
|
|
72
72
|
|
|
73
73
|
const DEFAULT_CONTAINS_PREDICATE = "mgx:default-contains";
|
|
74
74
|
|
|
75
|
+
// Codepoint order, never localeCompare — two readers loading the same world
|
|
76
|
+
// facts on different locales must mint the same instance ids.
|
|
77
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
78
|
+
|
|
75
79
|
/** Materialize a world's class-default contents: for every
|
|
76
80
|
* `<room> mgx:default-contains <class>` fact, mint one placed, portable
|
|
77
81
|
* instance of that class in that room unless the room already holds one. A
|
|
@@ -87,7 +91,7 @@ export function expandWorldDefaultContents(facts) {
|
|
|
87
91
|
const rows = facts || [];
|
|
88
92
|
const defaults = rows
|
|
89
93
|
.filter((r) => r.predicate === DEFAULT_CONTAINS_PREDICATE)
|
|
90
|
-
.sort((a, b) => `${a.subject}\0${a.object}
|
|
94
|
+
.sort((a, b) => byCodepoint(`${a.subject}\0${a.object}`, `${b.subject}\0${b.object}`));
|
|
91
95
|
if (!defaults.length) return rows;
|
|
92
96
|
|
|
93
97
|
const instanceIds = new Set(rows.filter((r) => r.predicate === "rdf:type").map((r) => r.subject));
|
|
@@ -33,6 +33,11 @@ import { foldWorldState, adventureTurn, worldActionRows } from "./adventure.mjs"
|
|
|
33
33
|
const isTypedRow = (rows, subject, type) =>
|
|
34
34
|
(rows || []).some((r) => r.subject === subject && r.predicate === "rdf:type" && r.object === type);
|
|
35
35
|
|
|
36
|
+
// Codepoint order, never localeCompare — an exit direction traces back to a
|
|
37
|
+
// world fact, and two readers must land on the same explore order regardless
|
|
38
|
+
// of locale.
|
|
39
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
40
|
+
|
|
36
41
|
/** The room a subject's CURRENT placement resolves into — a room resolves to
|
|
37
42
|
* itself; an object placed directly in a room resolves to that room; an
|
|
38
43
|
* object one containment hop inside an OPEN container resolves to the
|
|
@@ -294,7 +299,7 @@ export async function runAdventureAutoplayTick(memoryDir, opts = {}) {
|
|
|
294
299
|
// isn't known yet. Prefer an immediate unexposed exit from here (the
|
|
295
300
|
// lowest-sorted direction); otherwise path toward the nearest exposed room
|
|
296
301
|
// that still has one.
|
|
297
|
-
const unexposedHere = unexposedExitsOf(here, exposedState, exposed).sort(([a], [b]) => a
|
|
302
|
+
const unexposedHere = unexposedExitsOf(here, exposedState, exposed).sort(([a], [b]) => byCodepoint(a, b));
|
|
298
303
|
if (unexposedHere.length) {
|
|
299
304
|
const [direction, target] = unexposedHere[0];
|
|
300
305
|
await runCommand(`go ${direction}`);
|
|
@@ -14,24 +14,29 @@
|
|
|
14
14
|
// mgx:acts-toward, mgx:is-objective) — an editor has to show and change
|
|
15
15
|
// exactly the facts a player is never told.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
17
|
+
// The two exports the adventure page splices into its inline script through
|
|
18
|
+
// `.toString()` — renderWorldEditorText and wordBeforeCursor — are entirely
|
|
19
|
+
// self-contained, the same discipline adventure-viz.mjs's own render-glue
|
|
20
|
+
// functions hold (see that module's header): a splice captures a function's
|
|
21
|
+
// own source text and none of its closure over sibling module bindings.
|
|
22
|
+
// wordBeforeCursor re-exports viz-theme.mjs's own shared copy (byte-identical
|
|
22
23
|
// to what used to live here, and to mud-editor.mjs's own copy) rather than
|
|
23
24
|
// keep a third copy of the same regex.
|
|
24
25
|
//
|
|
25
26
|
// Two predicate families get different sync strategies, on purpose:
|
|
26
27
|
// - PLACEMENT/OPENNESS (mgx:currently-in/located-in/fixed-in/stands-
|
|
27
28
|
// locked-in/hidden-in, mgx:is-open) are fold-versioned: foldWorldState
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// is handled as a plain new write superseding the old one, never a
|
|
29
|
+
// ranks these rows by the (epoch, turn) pair stamped on the subject, and
|
|
30
|
+
// reads the highest-ranked one as the current truth. Editing one of these
|
|
31
|
+
// facts is handled as a plain new write superseding the old one, never a
|
|
32
32
|
// retraction, so planWorldEditorSync can never touch memory/core.mjs's
|
|
33
33
|
// own removeFacts for this family, and can never race the fold logic the
|
|
34
|
-
// rest of the engine depends on.
|
|
34
|
+
// rest of the engine depends on. The superseding write is stamped one
|
|
35
|
+
// turn past the world's own turn count, exactly the way every in-game
|
|
36
|
+
// action's commit() writes, so it OUTRANKS what it replaces instead of
|
|
37
|
+
// merely arriving after it — at equal turn the fold takes whichever row
|
|
38
|
+
// comes later in the array, and array order is nobody's to promise once
|
|
39
|
+
// a peer's facts have merged in. One consequence, stated plainly: this
|
|
35
40
|
// editor can move an object or reopen/close a container, but it cannot
|
|
36
41
|
// make a placed object vanish outright — the append-only truth model has
|
|
37
42
|
// no way to write "nowhere" — a scope choice, not a defect.
|
|
@@ -47,6 +52,8 @@
|
|
|
47
52
|
// document parses cleanly again. Additions are never gated this way —
|
|
48
53
|
// they are non-destructive by construction.
|
|
49
54
|
|
|
55
|
+
import { snapshotSubject } from "../domain/world-snapshot.mjs";
|
|
56
|
+
|
|
50
57
|
const PLACEMENT_KIND = "placement";
|
|
51
58
|
const OPENNESS_KIND = "openness";
|
|
52
59
|
const OTHER_KIND = "other";
|
|
@@ -308,20 +315,29 @@ export function editableOtherRows(rows) {
|
|
|
308
315
|
|
|
309
316
|
/** Plan the fact-store writes one parsed edit implies, from already-parsed
|
|
310
317
|
* `triples` (parseWorldEditorText's own output) against the world's current
|
|
311
|
-
* `rows`/`state`. Returns `{ toAppend, toRemoveIds }` — pure, no
|
|
318
|
+
* `rows`/`state`. Returns `{ toAppend, toRemoveIds, editTurn }` — pure, no
|
|
319
|
+
* I/O. `toAppend` rows are store-ready: a fold-versioned one already carries
|
|
320
|
+
* its `subject@turnN` stamp, and `editTurn` is the turn every one of them
|
|
321
|
+
* was stamped at, for the caller's own provenance tag.
|
|
312
322
|
*
|
|
313
323
|
* Placement/openness triples are NEVER retracted (see this module's own
|
|
314
324
|
* header): a triple only joins `toAppend` when it actually differs from the
|
|
315
325
|
* subject's current folded value (or the subject has none yet) — otherwise
|
|
316
326
|
* re-asserting an unchanged line would append a no-op duplicate on every
|
|
317
|
-
* keystroke.
|
|
327
|
+
* keystroke. Each one is stamped one turn past the world's own turn count,
|
|
328
|
+
* which is what makes it outrank the placement it supersedes rather than
|
|
329
|
+
* tie with it and win on array position.
|
|
318
330
|
*
|
|
319
|
-
* "Other"-family triples (type/exits/container/puzzle)
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
331
|
+
* "Other"-family triples (type/exits/container/puzzle) keep their bare
|
|
332
|
+
* subject — every reader takes those raw, and a stamped one names a subject
|
|
333
|
+
* no verb resolves. They get a real add/remove diff against
|
|
334
|
+
* `editableOtherRows(rows)` — but the CALLER decides whether `toRemoveIds`
|
|
335
|
+
* is safe to apply (skip it whenever parseWorldEditorText reported any
|
|
336
|
+
* unrecognized line — see this module's header for why). */
|
|
324
337
|
export function planWorldEditorSync(rows, state, triples) {
|
|
338
|
+
const editTurn = (state?.turnCount ?? 0) + 1;
|
|
339
|
+
const editEpoch = state?.epoch ?? 0;
|
|
340
|
+
const stampedForFold = (t) => ({ ...t, subject: snapshotSubject(t.subject, editTurn, editEpoch) });
|
|
325
341
|
const toAppend = [];
|
|
326
342
|
const seenPlacementSubjects = new Set();
|
|
327
343
|
const otherTriples = [];
|
|
@@ -334,11 +350,11 @@ export function planWorldEditorSync(rows, state, triples) {
|
|
|
334
350
|
seenPlacementSubjects.add(t.subject);
|
|
335
351
|
if (t.kind === PLACEMENT_KIND) {
|
|
336
352
|
const current = state.placements?.get(t.subject);
|
|
337
|
-
if (!current || current.predicate !== t.predicate || current.object !== t.object) toAppend.push(t);
|
|
353
|
+
if (!current || current.predicate !== t.predicate || current.object !== t.object) toAppend.push(stampedForFold(t));
|
|
338
354
|
} else if (t.kind === OPENNESS_KIND) {
|
|
339
355
|
const current = state.openness?.get(t.subject);
|
|
340
356
|
const wantOpen = t.object === "true";
|
|
341
|
-
if (!current || current.open !== wantOpen) toAppend.push(t);
|
|
357
|
+
if (!current || current.open !== wantOpen) toAppend.push(stampedForFold(t));
|
|
342
358
|
}
|
|
343
359
|
}
|
|
344
360
|
|
|
@@ -355,7 +371,7 @@ export function planWorldEditorSync(rows, state, triples) {
|
|
|
355
371
|
for (const [key, id] of currentKeys) {
|
|
356
372
|
if (!newOtherKeys.has(key)) toRemoveIds.push(id);
|
|
357
373
|
}
|
|
358
|
-
return { toAppend, toRemoveIds };
|
|
374
|
+
return { toAppend, toRemoveIds, editTurn };
|
|
359
375
|
}
|
|
360
376
|
|
|
361
377
|
/** The additive half of planWorldEditorSync, for ONE already-parsed triple:
|
|
@@ -364,7 +380,13 @@ export function planWorldEditorSync(rows, state, triples) {
|
|
|
364
380
|
* one sentence only ever says what it says, so nothing it leaves out is
|
|
365
381
|
* evidence of anything. Re-asserting a fact the world already holds appends
|
|
366
382
|
* nothing — `reason` says which of the two happened, in the caller's own
|
|
367
|
-
* words. Pure.
|
|
383
|
+
* words. Pure.
|
|
384
|
+
*
|
|
385
|
+
* The triple comes back with its bare subject, unlike planWorldEditorSync's
|
|
386
|
+
* rows: a taught sentence can MINT the thing it talks about, and the caller
|
|
387
|
+
* picks the fresh id, so only the caller knows which subject the fold stamp
|
|
388
|
+
* belongs on. world-teach.mjs stamps a fold-versioned one at
|
|
389
|
+
* `turnCount + 1` there, for the same reason planWorldEditorSync does. */
|
|
368
390
|
export function planTaughtTriple(rows, state, triple) {
|
|
369
391
|
if (!triple?.subject || !triple?.object) return { toAppend: [], reason: "nothing parsed" };
|
|
370
392
|
if (triple.kind === PLACEMENT_KIND) {
|
|
@@ -114,16 +114,26 @@ const TICK_WAIT_MS = 900;
|
|
|
114
114
|
* (a distinct icon from plain furniture, since Ashcombe's own cabinet and
|
|
115
115
|
* portrait are typed "furniture" but read more clearly as a container on
|
|
116
116
|
* screen), else its own rdf:type object, else the generic "portable"
|
|
117
|
-
* fallback for anything a world places with no type fact at all.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
117
|
+
* fallback for anything a world places with no type fact at all. A room
|
|
118
|
+
* always wins when it's one of several rdf:type facts on one subject: every
|
|
119
|
+
* shipped world's outdoor/underground rooms carry `rdf:type room` PLUS a
|
|
120
|
+
* second `rdf:type outdoor-space`/`underground-space` fact purely as a
|
|
121
|
+
* border-paint marker for roomKindForRoom below, and that marker has no
|
|
122
|
+
* sprite of its own, so picking it as the class would fall through to the
|
|
123
|
+
* generic "portable" icon for something that is actually a room. Preferring
|
|
124
|
+
* "room" keeps the answer the same regardless of which of the two facts a
|
|
125
|
+
* store happens to list first. Pure, self-contained — no reference to
|
|
126
|
+
* adventure.mjs's own private isContainer/isTyped, since those aren't
|
|
127
|
+
* exported. */
|
|
120
128
|
export function spriteClassForObject(rows, subject) {
|
|
121
129
|
const isContainer = (rows || []).some(
|
|
122
130
|
(r) => r.subject === subject && r.predicate === "mgx:is-container" && r.object === "true",
|
|
123
131
|
);
|
|
124
132
|
if (isContainer) return "container";
|
|
125
|
-
const
|
|
126
|
-
|
|
133
|
+
const typeRows = (rows || []).filter((r) => r.subject === subject && r.predicate === "rdf:type");
|
|
134
|
+
if (!typeRows.length) return "portable";
|
|
135
|
+
const room = typeRows.find((r) => r.object === "room");
|
|
136
|
+
return (room || typeRows[0]).object;
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
/** `rows` plus synthetic rdfs:subClassOf edges covering `subject`'s whole
|
|
@@ -351,9 +361,13 @@ export function roomSceneLayout(rows, state, here) {
|
|
|
351
361
|
bases.push(o.subject);
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
364
|
+
// Codepoint order, never localeCompare — inlined, not a module-level
|
|
365
|
+
// helper, because this function is spliced verbatim into the page's own
|
|
366
|
+
// inline script and can carry no outer-scope reference with it.
|
|
367
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
368
|
+
wall.sort((a, b) => byCodepoint(a.subject, b.subject));
|
|
369
|
+
bases.sort(byCodepoint);
|
|
370
|
+
for (const list of stackedOnBy.values()) list.sort(byCodepoint);
|
|
357
371
|
|
|
358
372
|
const visitedForStack = new Set();
|
|
359
373
|
function stackFrom(base) {
|
|
@@ -390,7 +404,10 @@ export function carriedItems(rows, state, holder = "player") {
|
|
|
390
404
|
return [...state.placements]
|
|
391
405
|
.filter(([, p]) => p.predicate === "mgx:located-in" && p.object === holder)
|
|
392
406
|
.map(([subject]) => ({ subject, spriteClass: spriteClassForObject(rows, subject) }))
|
|
393
|
-
|
|
407
|
+
// Codepoint order, never localeCompare — inlined, not a module-level
|
|
408
|
+
// helper, because this function is spliced verbatim into the page's own
|
|
409
|
+
// inline script and can carry no outer-scope reference with it.
|
|
410
|
+
.sort((a, b) => (a.subject < b.subject ? -1 : a.subject > b.subject ? 1 : 0));
|
|
394
411
|
}
|
|
395
412
|
|
|
396
413
|
/** A visited-rooms-only map: one node per room `visitedRoomIds` actually
|
|
@@ -761,18 +761,26 @@ function rulingTestimonyClaim(provenance, knower, currentEpoch = 0) {
|
|
|
761
761
|
}
|
|
762
762
|
|
|
763
763
|
/** What `person` knows about NOW: the object of every knows-about edge whose
|
|
764
|
-
* ruling claim still stands,
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
764
|
+
* ruling claim still stands, oldest learned first. Nothing is deleted — an edge
|
|
765
|
+
* whose newest claim says the thing is gone just stops reading back.
|
|
766
|
+
* `currentEpoch` is the run the reader is on, so a claim a recast has left
|
|
767
|
+
* behind cannot decide what a character knows today.
|
|
768
|
+
*
|
|
769
|
+
* The order comes off each edge's own ruling claim rather than off the row
|
|
770
|
+
* array, so it says what it means: a character lists what it learned in the
|
|
771
|
+
* order it learned it, and two peers holding one fact set read the same list.
|
|
772
|
+
* Two topics learned on one turn fall back to the topic word. */
|
|
768
773
|
function currentKnowsAboutTopics(rows, person, currentEpoch = 0) {
|
|
769
|
-
const
|
|
774
|
+
const known = [];
|
|
770
775
|
for (const row of rows || []) {
|
|
771
776
|
if (row.subject !== person || row.predicate !== KNOWS_ABOUT_PREDICATE) continue;
|
|
772
|
-
|
|
773
|
-
|
|
777
|
+
const claim = rulingTestimonyClaim(row.provenance, person, currentEpoch);
|
|
778
|
+
if (claim?.voided) continue;
|
|
779
|
+
known.push({ topic: row.object, epoch: claim?.epoch ?? 0, turn: claim?.turn ?? 0 });
|
|
774
780
|
}
|
|
775
|
-
|
|
781
|
+
known.sort((a, b) => a.epoch - b.epoch || a.turn - b.turn
|
|
782
|
+
|| (a.topic < b.topic ? -1 : a.topic > b.topic ? 1 : 0));
|
|
783
|
+
return known.map((entry) => entry.topic);
|
|
776
784
|
}
|
|
777
785
|
|
|
778
786
|
/**
|
|
@@ -1223,12 +1231,34 @@ export function freshObjectId(rows, kind, alsoTaken = new Set()) {
|
|
|
1223
1231
|
}
|
|
1224
1232
|
|
|
1225
1233
|
/** The kinds a room kind declares for one of the spawn pools, in the order the
|
|
1226
|
-
*
|
|
1234
|
+
* fold hands them over, or `fallback` when it declares none. A pool is a set of
|
|
1235
|
+
* rows, not a list, so the store keeps no rank between them and the order is
|
|
1236
|
+
* the fold's content order rather than the order the world file lists them in.
|
|
1237
|
+
* A caller that shows the whole pool can read it as it stands; one that takes
|
|
1238
|
+
* only part of it wants preferredDigKinds below. Pure. */
|
|
1227
1239
|
function declaredKindsOr(rows, roomClass, predicate, fallback) {
|
|
1228
1240
|
const declared = factObjects(rows, roomClass, predicate);
|
|
1229
1241
|
return declared.length ? declared : fallback;
|
|
1230
1242
|
}
|
|
1231
1243
|
|
|
1244
|
+
/** A dig pool ranked by what a dig should turn up first: the engine's own
|
|
1245
|
+
* DIG_SPAWN_KINDS order, then content order for a kind it does not name.
|
|
1246
|
+
*
|
|
1247
|
+
* A den shows everything it holds, so its pool needs no rank. A plain dig takes
|
|
1248
|
+
* the first few, so which few it takes is a real choice — and the fact store
|
|
1249
|
+
* holds the pool as a set, with no rank to read. Leaving it to the fold's own
|
|
1250
|
+
* order would decide it alphabetically, which puts a kind the world already
|
|
1251
|
+
* keeps as one hand-named prop ahead of one minted only by digging, and the
|
|
1252
|
+
* point of a minted id is that it names something a hand-authored prop does
|
|
1253
|
+
* not. Pure. */
|
|
1254
|
+
function preferredDigKinds(kinds) {
|
|
1255
|
+
const rank = (kind) => {
|
|
1256
|
+
const at = DIG_SPAWN_KINDS.indexOf(kind);
|
|
1257
|
+
return at < 0 ? DIG_SPAWN_KINDS.length : at;
|
|
1258
|
+
};
|
|
1259
|
+
return kinds.slice().sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1232
1262
|
/** The mass row a freshly minted instance needs, copied off its own class, or
|
|
1233
1263
|
* nothing when the class declares no mass. eat reads the instance's mass, so a
|
|
1234
1264
|
* dug carrot with none would be worth the flat default however the world
|
|
@@ -1499,7 +1529,7 @@ async function handleDigVerb(ctx) {
|
|
|
1499
1529
|
const spawnCount = DIG_SPAWN_MIN + stableIndex(dug, spawnMax - DIG_SPAWN_MIN + 1);
|
|
1500
1530
|
const spawnedKinds = isDen
|
|
1501
1531
|
? declaredKindsOr(rows, dugKind, DEN_SPAWN_PREDICATE, DIG_SPAWN_KINDS)
|
|
1502
|
-
: declaredKindsOr(rows, dugKind, DIG_SPAWN_PREDICATE, DIG_SPAWN_KINDS).slice(0, spawnCount);
|
|
1532
|
+
: preferredDigKinds(declaredKindsOr(rows, dugKind, DIG_SPAWN_PREDICATE, DIG_SPAWN_KINDS)).slice(0, spawnCount);
|
|
1503
1533
|
const minted = new Set();
|
|
1504
1534
|
const spawned = spawnedKinds.map((kind) => {
|
|
1505
1535
|
const id = freshObjectId(rows, kind, minted);
|