@polycode-projects/the-mechanical-code-talker 3.3.0 → 4.0.1
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 +176 -8
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +52 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +53 -13
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-garden.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-garden.jsonl +34 -1
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- package/src/adapters/p2p/webrtc-transport.mjs +146 -0
- package/src/domain/game-config.mjs +26 -5
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +31 -0
- package/src/domain/memory/trust.mjs +6 -4
- package/src/domain/p2p/facts.mjs +81 -0
- package/src/domain/p2p/peer-id.mjs +32 -0
- package/src/domain/p2p/provenance-relabel.mjs +26 -0
- package/src/domain/p2p/sync-filter.mjs +31 -0
- package/src/domain/p2p/wire.mjs +123 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/spider-fly-world.mjs +1 -1
- package/src/domain/sprite-map.mjs +10 -2
- package/src/domain/sprite-templates.mjs +8 -6
- package/src/services/adventure-editor.mjs +10 -2
- package/src/services/adventure-viz.mjs +309 -73
- package/src/services/adventure.mjs +1034 -330
- package/src/services/chat-page-viz.mjs +1097 -16
- package/src/services/chat.mjs +99 -18
- package/src/services/code-explorer-viz.mjs +52 -14
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +54 -26
- package/src/services/ledger-viz.mjs +105 -56
- package/src/services/memory-panel-viz.mjs +24 -0
- package/src/services/mud-editor.mjs +313 -0
- package/src/services/mud-turn.mjs +264 -87
- package/src/services/mud-viz.mjs +2606 -384
- package/src/services/p2p-room.mjs +559 -0
- package/src/services/plan-viz.mjs +119 -61
- package/src/services/research-viz.mjs +151 -57
- package/src/services/spider-fly-turn.mjs +1 -1
- package/src/services/spider-fly-viz.mjs +67 -43
- package/src/services/sprite-catalog-viz.mjs +175 -51
- package/src/services/viz-theme.mjs +15 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +109 -109
- package/src/surfaces/web/mud-browser-entry.mjs +226 -18
- package/src/surfaces/web/p2p-browser-entry.mjs +39 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// mud-editor.mjs — the burrow's text<->fact bridge, the mud world's own
|
|
2
|
+
// counterpart to adventure-editor.mjs. Same discipline, different vocabulary:
|
|
3
|
+
// a small closed sentence table, invertible by construction, where every phrase
|
|
4
|
+
// the renderer writes has exactly one parser rule that reads it back to the same
|
|
5
|
+
// triple.
|
|
6
|
+
//
|
|
7
|
+
// It is a separate table rather than an extension of adventure-editor.mjs's
|
|
8
|
+
// because the two worlds are built out of different facts. Ashcombe Hall is a
|
|
9
|
+
// puzzle — locks, hidden things, an objective, an NPC schedule. mud-garden is an
|
|
10
|
+
// ecology — a class hierarchy, masses, a predator, and the dig knobs that say how
|
|
11
|
+
// far a burrow reaches and what digging one out turns up. An editor whose whole
|
|
12
|
+
// point is "change what this world is made of" has to speak the vocabulary the
|
|
13
|
+
// world is actually written in, and neither table is a superset of the other.
|
|
14
|
+
//
|
|
15
|
+
// No imports: every export here is `.toString()`-splice-safe, the same discipline
|
|
16
|
+
// adventure-editor.mjs and the *-viz render-glue functions hold — mud-viz.mjs
|
|
17
|
+
// splices these straight into mud.html's inline script, which captures a
|
|
18
|
+
// function's own source text and nothing it closes over. A splice-safe function
|
|
19
|
+
// carries its whole dependency graph inside its own body.
|
|
20
|
+
//
|
|
21
|
+
// Two predicate families sync differently, for the reason adventure-editor.mjs's
|
|
22
|
+
// own header sets out:
|
|
23
|
+
// - PLACEMENT (currently-in/located-in/fixed-in/hidden-in), OPENNESS and MASS
|
|
24
|
+
// are fold-versioned — foldWorldState already reads the newest write as the
|
|
25
|
+
// current truth, which is how every turn's own move and mass drain lands — so
|
|
26
|
+
// an edit to one is a plain new write superseding the old, never a
|
|
27
|
+
// retraction. Mass belongs here and not below precisely because it MOVES: a
|
|
28
|
+
// playing animal's weight is a fold over turn snapshots, so an editor
|
|
29
|
+
// diffing raw mass rows would show the seed and fight the drain.
|
|
30
|
+
// - Everything else (typing, the class hierarchy, exits, the predator flag,
|
|
31
|
+
// the dig knobs) is read raw by the engine, so changing one genuinely needs
|
|
32
|
+
// the old row retracted. planMudEditorSync computes that diff, and the caller
|
|
33
|
+
// only applies its removals when the whole document parsed cleanly: a line
|
|
34
|
+
// that fails to parse this keystroke must never be read as "this fact is
|
|
35
|
+
// gone".
|
|
36
|
+
|
|
37
|
+
const PLACEMENT_KIND = "placement";
|
|
38
|
+
const OPENNESS_KIND = "openness";
|
|
39
|
+
const MASS_KIND = "mass";
|
|
40
|
+
const OTHER_KIND = "other";
|
|
41
|
+
|
|
42
|
+
const LOW = (s) => String(s || "").trim().toLowerCase();
|
|
43
|
+
|
|
44
|
+
const EXIT_PREDICATE_RE = /^mgx:has-exit-([a-z]+)$/;
|
|
45
|
+
const SNAPSHOT_RE = /^(.+)@turn(\d+)$/;
|
|
46
|
+
|
|
47
|
+
// Every predicate this editor renders and parses outside the placement/openness
|
|
48
|
+
// families — the closed set planMudEditorSync diffs against, and the only rows a
|
|
49
|
+
// removal can ever target. A predicate the engine reads but this table cannot
|
|
50
|
+
// say is deliberately absent: unrenderable and undiffable together, so an edit
|
|
51
|
+
// elsewhere in the document can never silently retract it.
|
|
52
|
+
const EDITABLE_OTHER_PREDICATES = [
|
|
53
|
+
"rdf:type", "rdfs:subClassOf", "mgx:display-name",
|
|
54
|
+
"mgx:is-container", "mgx:is-predator", "mgx:is-origin",
|
|
55
|
+
"mgx:dig-spawns", "mgx:den-spawns", "mgx:den-resident",
|
|
56
|
+
"mgx:dig-reach", "mgx:dig-spawn-max", "mgx:den-chance-in", "mgx:den-resident-chance-in",
|
|
57
|
+
"mgx:mass-drain-per-turn",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// The two flags that only ever have a sentence in the "true" direction. A row
|
|
61
|
+
// saying "false" is unrenderable here, so it must not be diffable either.
|
|
62
|
+
const TRUE_ONLY_FLAG_PREDICATES = ["mgx:is-container", "mgx:is-predator", "mgx:is-origin"];
|
|
63
|
+
|
|
64
|
+
// ---- rendering ---------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/** The whole burrow's editable facts as plain sentences, one per line, sorted by
|
|
67
|
+
* (subject, predicate, object) so two edits apart produce a reviewable diff
|
|
68
|
+
* rather than a reshuffle. Placement and openness come from the FOLDED state, so
|
|
69
|
+
* what shows is where things actually are now; everything else comes from the
|
|
70
|
+
* raw rows, skipping @turnN snapshot subjects — those carry only the placement
|
|
71
|
+
* overrides the fold has already read.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately self-contained (its own local casing/phrase helpers, duplicating
|
|
74
|
+
* this module's top-level ones) rather than calling private siblings: this
|
|
75
|
+
* function is spliced into mud.html's inline script by `.toString()`, which
|
|
76
|
+
* captures the body and nothing else. Pure. */
|
|
77
|
+
export function renderMudEditorText(rows, state) {
|
|
78
|
+
const cap = (s) => (s ? String(s).charAt(0).toUpperCase() + String(s).slice(1) : s);
|
|
79
|
+
const low = (s) => String(s || "").trim().toLowerCase();
|
|
80
|
+
const typePhraseFor = (object) => (/^[aeiou]/i.test(object) ? "is an" : "is a");
|
|
81
|
+
const exitRe = /^mgx:has-exit-([a-z]+)$/;
|
|
82
|
+
const snapshotRe = /^(.+)@turn(\d+)$/;
|
|
83
|
+
|
|
84
|
+
// A character is placed with currently-in; that is the whole test the engine
|
|
85
|
+
// itself applies, so "carries" versus "lies in" is read off the holder's own
|
|
86
|
+
// placement predicate rather than off a class list.
|
|
87
|
+
const isCharacter = (subject) => state.placements?.get(subject)?.predicate === "mgx:currently-in";
|
|
88
|
+
|
|
89
|
+
const lines = [];
|
|
90
|
+
const push = (subject, predicate, object, text) =>
|
|
91
|
+
lines.push({ key: [subject, predicate, object].join(" "), text });
|
|
92
|
+
|
|
93
|
+
const placementLine = (subject, place) => {
|
|
94
|
+
if (place.predicate === "mgx:currently-in") return `${cap(subject)} stands in the ${place.object}.`;
|
|
95
|
+
if (place.predicate === "mgx:fixed-in") return `${cap(subject)} is fixed in the ${place.object}.`;
|
|
96
|
+
if (place.predicate === "mgx:hidden-in") return `${cap(subject)} is hidden in the ${place.object}.`;
|
|
97
|
+
if (place.predicate !== "mgx:located-in") return null;
|
|
98
|
+
return isCharacter(place.object)
|
|
99
|
+
? `${cap(place.object)} carries the ${subject}.`
|
|
100
|
+
: `${cap(subject)} lies in the ${place.object}.`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const [subject, place] of state.placements || new Map()) {
|
|
104
|
+
const text = placementLine(subject, place);
|
|
105
|
+
if (text) push(subject, place.predicate, place.object, text);
|
|
106
|
+
}
|
|
107
|
+
for (const [subject, openness] of state.openness || new Map()) {
|
|
108
|
+
push(subject, "mgx:is-open", String(openness.open),
|
|
109
|
+
openness.open ? `${cap(subject)} is open.` : `${cap(subject)} is closed.`);
|
|
110
|
+
}
|
|
111
|
+
for (const [subject, mass] of state.masses || new Map()) {
|
|
112
|
+
push(subject, "mgx:hasMass", String(mass.value), `${cap(subject)} weighs ${mass.value}.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const row of rows || []) {
|
|
116
|
+
if (snapshotRe.test(row.subject)) continue;
|
|
117
|
+
const s = row.subject, p = row.predicate, o = row.object;
|
|
118
|
+
const exit = exitRe.exec(p);
|
|
119
|
+
if (exit) { push(s, p, o, `${cap(s)} has an exit ${exit[1]} to the ${o}.`); continue; }
|
|
120
|
+
if (p === "rdf:type") { push(s, p, o, `${cap(s)} ${typePhraseFor(o)} ${o}.`); continue; }
|
|
121
|
+
if (p === "rdfs:subClassOf") { push(s, p, o, `${cap(s)} is a kind of ${o}.`); continue; }
|
|
122
|
+
if (p === "mgx:display-name") { push(s, p, o, `${cap(s)} is shown as ${o}.`); continue; }
|
|
123
|
+
if (p === "mgx:is-container" && o === "true") { push(s, p, o, `${cap(s)} is a container.`); continue; }
|
|
124
|
+
if (p === "mgx:is-predator" && o === "true") { push(s, p, o, `${cap(s)} hunts the other animals.`); continue; }
|
|
125
|
+
if (p === "mgx:is-origin" && o === "true") { push(s, p, o, `${cap(s)} is where the burrow starts.`); continue; }
|
|
126
|
+
if (p === "mgx:dig-spawns") { push(s, p, o, `Digging in ${s} turns up ${o}.`); continue; }
|
|
127
|
+
if (p === "mgx:den-spawns") { push(s, p, o, `A den in ${s} stores ${o}.`); continue; }
|
|
128
|
+
if (p === "mgx:den-resident") { push(s, p, o, `A den in ${s} is lived in by ${o}.`); continue; }
|
|
129
|
+
if (p === "mgx:dig-reach") { push(s, p, o, `The burrow digs ${o} rooms out from the ${s}.`); continue; }
|
|
130
|
+
if (p === "mgx:dig-spawn-max") { push(s, p, o, `Digging in ${s} turns up at most ${o} things.`); continue; }
|
|
131
|
+
if (p === "mgx:den-chance-in") { push(s, p, o, `One dig in ${o} in ${s} opens a den.`); continue; }
|
|
132
|
+
if (p === "mgx:den-resident-chance-in") { push(s, p, o, `One den in ${o} in ${s} is lived in.`); continue; }
|
|
133
|
+
if (p === "mgx:mass-drain-per-turn") { push(s, p, o, `${cap(s)} loses ${o} mass a turn.`); continue; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const deduped = lines.filter((l) => (seen.has(l.key) ? false : (seen.add(l.key), true)));
|
|
138
|
+
deduped.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
139
|
+
return deduped.map((l) => l.text).join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- parsing -----------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
// Checked in this order, first match wins. Every phrase above the two generic
|
|
145
|
+
// fallbacks is a literal-text special case one of those two would also match
|
|
146
|
+
// ("is a container." is a valid "is a <class>." sentence), so the specific
|
|
147
|
+
// readings have to be tried first and the bare ones last.
|
|
148
|
+
const LINE_PATTERNS = [
|
|
149
|
+
{ re: /^(.+?)\s+carries\s+the\s+(.+?)\.?$/i,
|
|
150
|
+
build: (m) => ({ subject: LOW(m[2]), predicate: "mgx:located-in", object: LOW(m[1]) }) },
|
|
151
|
+
{ re: /^(.+?)\s+stands\s+in\s+the\s+(.+?)\.?$/i,
|
|
152
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:currently-in", object: LOW(m[2]) }) },
|
|
153
|
+
{ re: /^(.+?)\s+lies\s+in\s+the\s+(.+?)\.?$/i,
|
|
154
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:located-in", object: LOW(m[2]) }) },
|
|
155
|
+
{ re: /^(.+?)\s+is\s+fixed\s+in\s+the\s+(.+?)\.?$/i,
|
|
156
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:fixed-in", object: LOW(m[2]) }) },
|
|
157
|
+
{ re: /^(.+?)\s+is\s+hidden\s+in\s+the\s+(.+?)\.?$/i,
|
|
158
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:hidden-in", object: LOW(m[2]) }) },
|
|
159
|
+
{ re: /^(.+?)\s+has\s+an\s+exit\s+(\w+)\s+to\s+the\s+(.+?)\.?$/i,
|
|
160
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: `mgx:has-exit-${LOW(m[2])}`, object: LOW(m[3]) }) },
|
|
161
|
+
{ re: /^(.+?)\s+is\s+open\.?$/i,
|
|
162
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:is-open", object: "true" }) },
|
|
163
|
+
{ re: /^(.+?)\s+is\s+closed\.?$/i,
|
|
164
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:is-open", object: "false" }) },
|
|
165
|
+
{ re: /^(.+?)\s+is\s+a\s+container\.?$/i,
|
|
166
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:is-container", object: "true" }) },
|
|
167
|
+
{ re: /^(.+?)\s+hunts\s+the\s+other\s+animals\.?$/i,
|
|
168
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:is-predator", object: "true" }) },
|
|
169
|
+
{ re: /^(.+?)\s+is\s+where\s+the\s+burrow\s+starts\.?$/i,
|
|
170
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:is-origin", object: "true" }) },
|
|
171
|
+
{ re: /^(.+?)\s+is\s+a\s+kind\s+of\s+(.+?)\.?$/i,
|
|
172
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "rdfs:subClassOf", object: LOW(m[2]) }) },
|
|
173
|
+
{ re: /^(.+?)\s+weighs\s+([\d.]+)\.?$/i,
|
|
174
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:hasMass", object: m[2] }) },
|
|
175
|
+
{ re: /^(.+?)\s+is\s+shown\s+as\s+(.+?)\.?$/i,
|
|
176
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:display-name", object: LOW(m[2]) }) },
|
|
177
|
+
{ re: /^(.+?)\s+loses\s+([\d.]+)\s+mass\s+a\s+turn\.?$/i,
|
|
178
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:mass-drain-per-turn", object: m[2] }) },
|
|
179
|
+
// The dig knobs. "at most N things" is tried before the plain "turns up X" so
|
|
180
|
+
// the count reading wins over reading "at most 2 things" as a spawned kind.
|
|
181
|
+
{ re: /^digging\s+in\s+(.+?)\s+turns\s+up\s+at\s+most\s+(\d+)\s+things?\.?$/i,
|
|
182
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:dig-spawn-max", object: m[2] }) },
|
|
183
|
+
{ re: /^digging\s+in\s+(.+?)\s+turns\s+up\s+(.+?)\.?$/i,
|
|
184
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:dig-spawns", object: LOW(m[2]) }) },
|
|
185
|
+
{ re: /^a\s+den\s+in\s+(.+?)\s+stores\s+(.+?)\.?$/i,
|
|
186
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:den-spawns", object: LOW(m[2]) }) },
|
|
187
|
+
{ re: /^a\s+den\s+in\s+(.+?)\s+is\s+lived\s+in\s+by\s+(.+?)\.?$/i,
|
|
188
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "mgx:den-resident", object: LOW(m[2]) }) },
|
|
189
|
+
{ re: /^the\s+burrow\s+digs\s+(\d+)\s+rooms?\s+out\s+from\s+the\s+(.+?)\.?$/i,
|
|
190
|
+
build: (m) => ({ subject: LOW(m[2]), predicate: "mgx:dig-reach", object: m[1] }) },
|
|
191
|
+
{ re: /^one\s+dig\s+in\s+(\d+)\s+in\s+(.+?)\s+opens\s+a\s+den\.?$/i,
|
|
192
|
+
build: (m) => ({ subject: LOW(m[2]), predicate: "mgx:den-chance-in", object: m[1] }) },
|
|
193
|
+
{ re: /^one\s+den\s+in\s+(\d+)\s+in\s+(.+?)\s+is\s+lived\s+in\.?$/i,
|
|
194
|
+
build: (m) => ({ subject: LOW(m[2]), predicate: "mgx:den-resident-chance-in", object: m[1] }) },
|
|
195
|
+
// The generic type fallback, tried last on purpose.
|
|
196
|
+
{ re: /^(.+?)\s+is\s+an?\s+(.+?)\.?$/i,
|
|
197
|
+
build: (m) => ({ subject: LOW(m[1]), predicate: "rdf:type", object: LOW(m[2]) }) },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const PLACEMENT_PREDICATES = new Set([
|
|
201
|
+
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:hidden-in",
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
/** One line -> `{ subject, predicate, object, kind }`, or null when nothing in
|
|
205
|
+
* the table recognizes it — an honest miss, never a guessed shape. Pure. */
|
|
206
|
+
export function parseMudEditorLine(line) {
|
|
207
|
+
const trimmed = String(line || "").trim();
|
|
208
|
+
if (!trimmed) return null;
|
|
209
|
+
for (const pattern of LINE_PATTERNS) {
|
|
210
|
+
const m = trimmed.match(pattern.re);
|
|
211
|
+
if (!m) continue;
|
|
212
|
+
const triple = pattern.build(m);
|
|
213
|
+
if (!triple.subject || !triple.object) continue;
|
|
214
|
+
const kind = triple.predicate === "mgx:is-open" ? OPENNESS_KIND
|
|
215
|
+
: triple.predicate === "mgx:hasMass" ? MASS_KIND
|
|
216
|
+
: PLACEMENT_PREDICATES.has(triple.predicate) ? PLACEMENT_KIND : OTHER_KIND;
|
|
217
|
+
return { ...triple, kind };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Parse the whole editor textarea: every non-blank line either becomes a triple
|
|
223
|
+
* or lands in `unrecognized` (1-based line numbers, and the original text —
|
|
224
|
+
* never silently dropped). Pure. */
|
|
225
|
+
export function parseMudEditorText(text) {
|
|
226
|
+
const triples = [];
|
|
227
|
+
const unrecognized = [];
|
|
228
|
+
String(text || "").split("\n").forEach((raw, i) => {
|
|
229
|
+
const trimmed = raw.trim();
|
|
230
|
+
if (!trimmed) return;
|
|
231
|
+
const parsed = parseMudEditorLine(trimmed);
|
|
232
|
+
if (parsed) triples.push(parsed);
|
|
233
|
+
else unrecognized.push({ line: i + 1, text: raw });
|
|
234
|
+
});
|
|
235
|
+
return { triples, unrecognized };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---- sync planning -----------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
const tripleKey = (t) => `${t.subject} ${t.predicate} ${t.object}`;
|
|
241
|
+
|
|
242
|
+
/** Every raw fact row this editor's "other" family is allowed to touch. Skips
|
|
243
|
+
* @turnN snapshot subjects, mirroring the renderer, and skips a "false" row of
|
|
244
|
+
* a flag the renderer only writes a sentence for in the "true" direction. Pure. */
|
|
245
|
+
export function editableMudOtherRows(rows) {
|
|
246
|
+
return (rows || []).filter((r) => {
|
|
247
|
+
if (SNAPSHOT_RE.test(r.subject)) return false;
|
|
248
|
+
if (EXIT_PREDICATE_RE.test(r.predicate)) return true;
|
|
249
|
+
if (TRUE_ONLY_FLAG_PREDICATES.includes(r.predicate)) return r.object === "true";
|
|
250
|
+
return EDITABLE_OTHER_PREDICATES.includes(r.predicate);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Plan the fact-store writes one parsed edit implies, from already-parsed
|
|
255
|
+
* `triples` against the world's current `rows`/`state`. Returns
|
|
256
|
+
* `{ toAppend, toRemoveIds }` — pure, no I/O.
|
|
257
|
+
*
|
|
258
|
+
* Placement and openness triples are never retracted: one joins `toAppend` only
|
|
259
|
+
* when it actually differs from the subject's current folded value, so
|
|
260
|
+
* re-asserting an unchanged line doesn't append a no-op duplicate per keystroke.
|
|
261
|
+
* Everything else gets a real add/remove diff — but the CALLER decides whether
|
|
262
|
+
* `toRemoveIds` is safe to apply, and must skip it whenever the parse reported
|
|
263
|
+
* any unrecognized line. */
|
|
264
|
+
export function planMudEditorSync(rows, state, triples) {
|
|
265
|
+
const toAppend = [];
|
|
266
|
+
// One "already said this" set per fold-versioned family, never one shared set:
|
|
267
|
+
// a subject can carry a placement, an openness and a mass at once, and a
|
|
268
|
+
// single set would let whichever line came last silence the other two.
|
|
269
|
+
const seenByKind = { [PLACEMENT_KIND]: new Set(), [OPENNESS_KIND]: new Set(), [MASS_KIND]: new Set() };
|
|
270
|
+
const otherTriples = [];
|
|
271
|
+
// Last occurrence per subject wins, mirroring how a person reads a document
|
|
272
|
+
// top to bottom and treats a later line as the correction of an earlier one.
|
|
273
|
+
for (const t of [...(triples || [])].reverse()) {
|
|
274
|
+
if (t.kind === OTHER_KIND) { otherTriples.push(t); continue; }
|
|
275
|
+
const seen = seenByKind[t.kind];
|
|
276
|
+
if (seen.has(t.subject)) continue;
|
|
277
|
+
seen.add(t.subject);
|
|
278
|
+
if (t.kind === PLACEMENT_KIND) {
|
|
279
|
+
const current = state.placements?.get(t.subject);
|
|
280
|
+
if (!current || current.predicate !== t.predicate || current.object !== t.object) toAppend.push(t);
|
|
281
|
+
} else if (t.kind === OPENNESS_KIND) {
|
|
282
|
+
const current = state.openness?.get(t.subject);
|
|
283
|
+
if (!current || current.open !== (t.object === "true")) toAppend.push(t);
|
|
284
|
+
} else {
|
|
285
|
+
const current = state.masses?.get(t.subject);
|
|
286
|
+
if (!current || Number(current.value) !== Number(t.object)) toAppend.push(t);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const currentKeys = new Map(editableMudOtherRows(rows).map((r) => [tripleKey(r), r.id]));
|
|
291
|
+
const newOtherKeys = new Set();
|
|
292
|
+
for (const t of otherTriples) {
|
|
293
|
+
const key = tripleKey(t);
|
|
294
|
+
if (newOtherKeys.has(key)) continue;
|
|
295
|
+
newOtherKeys.add(key);
|
|
296
|
+
if (!currentKeys.has(key)) toAppend.push(t);
|
|
297
|
+
}
|
|
298
|
+
const toRemoveIds = [];
|
|
299
|
+
for (const [key, id] of currentKeys) {
|
|
300
|
+
if (!newOtherKeys.has(key)) toRemoveIds.push(id);
|
|
301
|
+
}
|
|
302
|
+
return { toAppend, toRemoveIds };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The word immediately before `cursorPos` in `text` — a run of letters, digits
|
|
306
|
+
* and hyphens, the shape this vocabulary's own terms take ("underground-space",
|
|
307
|
+
* "mole-1"). Empty when the cursor sits after whitespace or punctuation with no
|
|
308
|
+
* word directly behind it. Pure. */
|
|
309
|
+
export function wordBeforeCursor(text, cursorPos) {
|
|
310
|
+
const head = String(text || "").slice(0, cursorPos);
|
|
311
|
+
const m = head.match(/[A-Za-z][A-Za-z0-9-]*$/);
|
|
312
|
+
return m ? m[0].toLowerCase() : "";
|
|
313
|
+
}
|