@wildwinter/scoperegistry 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,27 @@ another engine.
27
27
  settable per scope (a resolver with no `set`) and per property
28
28
  (`writable: false`), enforced by `set`.
29
29
 
30
+ Since 0.2.0 this package is also the **state kernel** the Patter and
31
+ Storylet Engine runtime families share (their "one properties implementer"):
32
+
33
+ - **`PropertyBag`** is a first-class citizen: typed declarations + defaults,
34
+ the firing rule (engine writes notify `subscribe`rs; host writes pass
35
+ `{ silent: true, reason }` and reach only the always-on `onAudit` hook),
36
+ examiner `rows()`, one sanctioned `clone()` door, in-place `reseed`, and
37
+ bare-value `save()`/`load()`. Name normalisation is a policy: lowercase by
38
+ default, or pass `{ normalise }` (a case-significant product passes
39
+ identity).
40
+ - Owned registry scopes are bags: `ownedBag(token)` exposes them, and
41
+ `mountOwned(token, bag)` attaches a bag another holder owns (the shared
42
+ state container for a mixed two-engine game).
43
+ - **`listProperties()`** returns examiner rows across owned scopes and
44
+ declared foreign scopes, for the shared in-engine property panels.
45
+ - **`saveFragment()`/`loadFragment()`** speak a versioned
46
+ `OwnedStateFragment` (`{ version, scopes }`), the one serialisation shape
47
+ both products' save envelopes embed when they adopt the kernel.
48
+ `save()`/`load()` keep the bare 0.1.x shape, so existing consumers' save
49
+ formats are untouched.
50
+
30
51
  ```ts
31
52
  import { ScopeRegistry } from "@wildwinter/scoperegistry";
32
53
 
package/dist/index.cjs CHANGED
@@ -20,6 +20,106 @@ function readScopeRegistrySpec(source) {
20
20
  }
21
21
  return spec;
22
22
  }
23
+ var PropertyBag = class _PropertyBag {
24
+ /** The live values record (stable identity across reseed, so an
25
+ * EvalContext built over it stays valid). Read-path for evaluation;
26
+ * writes go through `set` so the firing rule applies. */
27
+ values = {};
28
+ decls = /* @__PURE__ */ new Map();
29
+ subscribers = /* @__PURE__ */ new Set();
30
+ auditors = /* @__PURE__ */ new Set();
31
+ /** Name normalisation policy: lowercase by default (the registry's
32
+ * long-standing contract); a product whose names are case-significant
33
+ * passes identity. */
34
+ norm;
35
+ constructor(declarations = [], opts) {
36
+ this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
37
+ this.seed(declarations);
38
+ }
39
+ seed(declarations) {
40
+ for (const d of declarations) {
41
+ const name = this.norm(d.name);
42
+ this.decls.set(name, d);
43
+ this.values[name] = structuredClone(d.default ?? defaultFor(d));
44
+ }
45
+ }
46
+ get(name) {
47
+ return this.values[this.norm(name)];
48
+ }
49
+ /** Write a property. Engine writes (the default) notify subscribers;
50
+ * pass `silent: true` for a host write, which reaches only the audit
51
+ * hook. Throws on a read-only property. Returns the change. */
52
+ set(name, value, opts) {
53
+ const n = this.norm(name);
54
+ if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
55
+ const change = {
56
+ name: n,
57
+ prev: this.values[n],
58
+ next: value,
59
+ silent: opts?.silent ?? false,
60
+ reason: opts?.reason
61
+ };
62
+ this.values[n] = value;
63
+ for (const audit of this.auditors) audit(change);
64
+ if (!change.silent) for (const fn of this.subscribers) fn(change);
65
+ return change;
66
+ }
67
+ /** Notified of engine (non-silent) writes. Returns the unsubscribe. */
68
+ subscribe(fn) {
69
+ this.subscribers.add(fn);
70
+ return () => this.subscribers.delete(fn);
71
+ }
72
+ /** Notified of EVERY write, silent or not. Returns the unsubscribe. */
73
+ onAudit(fn) {
74
+ this.auditors.add(fn);
75
+ return () => this.auditors.delete(fn);
76
+ }
77
+ /** Examiner rows: the declared surface only (stray values are storage,
78
+ * not surface). */
79
+ rows() {
80
+ return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name));
81
+ }
82
+ declarations() {
83
+ return [...this.decls.values()];
84
+ }
85
+ /** The one sanctioned copy door: values deep-copied, declarations
86
+ * duplicated, the normalisation policy carried, subscriptions NOT
87
+ * carried. */
88
+ clone() {
89
+ const c = new _PropertyBag([], { normalise: this.norm });
90
+ c.decls = new Map(this.decls);
91
+ Object.assign(c.values, structuredClone(this.values));
92
+ return c;
93
+ }
94
+ /** Clear and re-seed from new declarations, in place (the values record
95
+ * keeps its identity, so contexts built over it stay valid). */
96
+ reseed(declarations) {
97
+ for (const k of Object.keys(this.values)) delete this.values[k];
98
+ this.decls.clear();
99
+ this.seed(declarations);
100
+ }
101
+ /** Bare values, ready to embed in a product's save. */
102
+ save() {
103
+ return structuredClone(this.values);
104
+ }
105
+ /** Lay saved values over the current ones (call after a fresh seed:
106
+ * orphans land as strays, new declarations keep their defaults; the
107
+ * product decides whether to prune). Does not fire events. */
108
+ load(values) {
109
+ for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
110
+ }
111
+ };
112
+ function rowFor(d, value, writable, name) {
113
+ return {
114
+ name: name ?? d.name.toLowerCase(),
115
+ type: d.type,
116
+ value,
117
+ default: d.default ?? defaultFor(d),
118
+ ...d.values !== void 0 ? { values: d.values } : {},
119
+ writable: writable ?? d.writable ?? true
120
+ };
121
+ }
122
+ var SAVE_FRAGMENT_VERSION = 1;
23
123
  var ScopeRegistry = class {
24
124
  scopes = /* @__PURE__ */ new Map();
25
125
  /**
@@ -28,17 +128,24 @@ var ScopeRegistry = class {
28
128
  * type-checked (declarations) and serialized by `save`/`load`.
29
129
  */
30
130
  defineOwned(token, declarations) {
131
+ return this.mountOwned(token, new PropertyBag(declarations));
132
+ }
133
+ /**
134
+ * Attach an EXISTING bag as an owned scope - the shared-container move: a
135
+ * host (or the other product) holds the bag; this registry reads, writes
136
+ * and lists it like its own, but the holder saves it.
137
+ */
138
+ mountOwned(token, bag) {
31
139
  this.assertFree(token);
32
- const bag = {};
33
- const decls = /* @__PURE__ */ new Map();
34
- for (const d of declarations) {
35
- const name = d.name.toLowerCase();
36
- decls.set(name, d);
37
- bag[name] = d.default ?? defaultFor(d);
38
- }
39
- this.scopes.set(token, { kind: "owned", bag, decls });
140
+ this.scopes.set(token, { kind: "owned", bag });
40
141
  return this;
41
142
  }
143
+ /** An owned scope's bag (subscribe, audit, rows live there). */
144
+ ownedBag(token) {
145
+ const e = this.scopes.get(token);
146
+ if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
147
+ return e.bag;
148
+ }
42
149
  /**
43
150
  * Re-initialise an existing **owned** scope's bag from new declarations,
44
151
  * clearing its current values. For scope-local state that resets on a context
@@ -47,15 +154,7 @@ var ScopeRegistry = class {
47
154
  * registry stays valid.
48
155
  */
49
156
  reseedOwned(token, declarations) {
50
- const e = this.scopes.get(token);
51
- if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
52
- for (const k of Object.keys(e.bag)) delete e.bag[k];
53
- e.decls.clear();
54
- for (const d of declarations) {
55
- const name = d.name.toLowerCase();
56
- e.decls.set(name, d);
57
- e.bag[name] = d.default ?? defaultFor(d);
58
- }
157
+ this.ownedBag(token).reseed(declarations);
59
158
  return this;
60
159
  }
61
160
  /**
@@ -78,23 +177,49 @@ var ScopeRegistry = class {
78
177
  get(scope, name) {
79
178
  const e = this.scopes.get(scope);
80
179
  if (!e) return void 0;
81
- const n = name.toLowerCase();
82
- return e.kind === "owned" ? e.bag[n] : e.resolver.get(n);
180
+ return e.kind === "owned" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());
83
181
  }
84
- /** Write a property. Throws on an unknown or read-only scope/property. */
182
+ /** Write a property (an ENGINE write: the bag's subscribers fire; use
183
+ * the bag directly for silent host writes). Throws on an unknown or
184
+ * read-only scope/property. */
85
185
  set(scope, name, value) {
86
186
  const e = this.scopes.get(scope);
87
187
  if (!e) throw new Error(`unknown scope '@${scope}'`);
188
+ if (e.kind === "owned") {
189
+ try {
190
+ e.bag.set(name, value);
191
+ } catch {
192
+ throw new Error(`'@${scope}.${name}' is read-only`);
193
+ }
194
+ return;
195
+ }
88
196
  const n = name.toLowerCase();
89
- if (!this.writable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
90
- if (e.kind === "owned") e.bag[n] = value;
91
- else e.resolver.set(n, value);
197
+ if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
198
+ e.resolver.set(n, value);
92
199
  }
93
- writable(e, name) {
94
- if (e.kind === "owned") return e.decls.get(name)?.writable ?? true;
200
+ foreignWritable(e, name) {
95
201
  if (!e.resolver.set) return false;
96
202
  return e.decls.get(name)?.writable ?? e.scopeWritable;
97
203
  }
204
+ /** Examiner rows across every scope with a declared surface: owned bags
205
+ * first, then declared foreign scopes (values read through, writability
206
+ * reflecting the resolver). Opaque foreign scopes are not listed. */
207
+ listProperties() {
208
+ const out = [];
209
+ for (const [token, e] of this.scopes) {
210
+ if (e.kind === "owned") {
211
+ for (const row of e.bag.rows()) out.push({ scope: token, ...row });
212
+ } else {
213
+ for (const d of e.decls.values()) {
214
+ out.push({
215
+ scope: token,
216
+ ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()))
217
+ });
218
+ }
219
+ }
220
+ }
221
+ return out;
222
+ }
98
223
  /**
99
224
  * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
100
225
  * bags, foreign scopes as their resolvers. `host` carries dialect-function
@@ -103,9 +228,31 @@ var ScopeRegistry = class {
103
228
  toEvalContext(host) {
104
229
  const scopes = {};
105
230
  for (const [token, e] of this.scopes) {
106
- scopes[token] = e.kind === "owned" ? e.bag : e.resolver;
231
+ scopes[token] = e.kind === "owned" ? e.bag.values : e.resolver;
232
+ }
233
+ const qualities = this.qualityLadders();
234
+ return qualities.size === 0 ? { scopes, host } : {
235
+ scopes,
236
+ host,
237
+ qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase())
238
+ };
239
+ }
240
+ /** Every quality declaration's ladder, keyed scope token then name. */
241
+ qualityLadders() {
242
+ const out = /* @__PURE__ */ new Map();
243
+ for (const [token, e] of this.scopes) {
244
+ const decls = e.kind === "owned" ? e.bag.declarations() : [...e.decls.values()];
245
+ for (const d of decls) {
246
+ if (d.type !== "quality" || d.stages === void 0) continue;
247
+ let m = out.get(token);
248
+ if (!m) {
249
+ m = /* @__PURE__ */ new Map();
250
+ out.set(token, m);
251
+ }
252
+ m.set(d.name.toLowerCase(), d.stages);
253
+ }
107
254
  }
108
- return { scopes, host };
255
+ return out;
109
256
  }
110
257
  /**
111
258
  * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
@@ -115,25 +262,47 @@ var ScopeRegistry = class {
115
262
  toSchema() {
116
263
  const properties = /* @__PURE__ */ new Map();
117
264
  for (const [token, e] of this.scopes) {
118
- if (e.decls.size === 0) continue;
265
+ const decls = e.kind === "owned" ? e.bag.declarations() : [...e.decls.values()];
266
+ if (decls.length === 0) continue;
119
267
  const m = /* @__PURE__ */ new Map();
120
- for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });
268
+ for (const d of decls) m.set(d.name.toLowerCase(), {
269
+ type: d.type,
270
+ enumValues: d.values,
271
+ ...d.stages !== void 0 ? { stages: d.stages } : {}
272
+ });
121
273
  properties.set(token, m);
122
274
  }
123
275
  return { properties };
124
276
  }
125
- /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
277
+ /** Serialize **owned** scopes only (foreign scopes are host-owned,
278
+ * host-saved), as bare bags - the 0.1.x shape, kept stable so existing
279
+ * consumers' save formats are untouched. A product embedding the
280
+ * versioned cross-product shape uses `saveFragment`. */
126
281
  save() {
127
282
  const out = {};
128
- for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = { ...e.bag };
283
+ for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = e.bag.save();
129
284
  return out;
130
285
  }
131
- /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
286
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
287
+ * are ignored. */
132
288
  load(blob) {
133
289
  for (const [token, vals] of Object.entries(blob)) {
134
290
  const e = this.scopes.get(token);
135
- if (e?.kind === "owned") Object.assign(e.bag, vals);
291
+ if (e?.kind === "owned") e.bag.load(vals);
292
+ }
293
+ }
294
+ /** The versioned owned-state fragment (the one serialisation shape both
295
+ * product families' save envelopes embed when they adopt the kernel;
296
+ * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
297
+ saveFragment() {
298
+ return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };
299
+ }
300
+ /** Restore from a versioned fragment; an unsupported version throws. */
301
+ loadFragment(fragment) {
302
+ if (fragment.version !== SAVE_FRAGMENT_VERSION) {
303
+ throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);
136
304
  }
305
+ this.load(fragment.scopes);
137
306
  }
138
307
  assertFree(token) {
139
308
  if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);
@@ -152,9 +321,14 @@ function defaultFor(d) {
152
321
  return d.values?.[0] ?? "";
153
322
  case "flags":
154
323
  return [];
324
+ // A quality starts at the first rung of its ladder.
325
+ case "quality":
326
+ return d.stages?.[0] ?? "";
155
327
  }
156
328
  }
157
329
 
330
+ exports.PropertyBag = PropertyBag;
331
+ exports.SAVE_FRAGMENT_VERSION = SAVE_FRAGMENT_VERSION;
158
332
  exports.SUPPORTED_SPEC_VERSIONS = SUPPORTED_SPEC_VERSIONS;
159
333
  exports.ScopeRegistry = ScopeRegistry;
160
334
  exports.readScopeRegistrySpec = readScopeRegistrySpec;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: Record<string, ScalarValue>;\n decls: Map<string, ScopeDeclaration>;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.assertFree(token);\n const bag: Record<string, ScalarValue> = {};\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n decls.set(name, d);\n bag[name] = d.default ?? defaultFor(d);\n }\n this.scopes.set(token, { kind: \"owned\", bag, decls });\n return this;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n for (const k of Object.keys(e.bag)) delete e.bag[k];\n e.decls.clear();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n e.decls.set(name, d);\n e.bag[name] = d.default ?? defaultFor(d);\n }\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n const n = name.toLowerCase();\n return e.kind === \"owned\" ? e.bag[n] : e.resolver.get(n);\n }\n\n /** Write a property. Throws on an unknown or read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n const n = name.toLowerCase();\n if (!this.writable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n if (e.kind === \"owned\") e.bag[n] = value;\n else e.resolver.set!(n, value);\n }\n\n private writable(e: Entry, name: string): boolean {\n if (e.kind === \"owned\") return e.decls.get(name)?.writable ?? true;\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag : e.resolver;\n }\n return { scopes, host };\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[] }>>();\n for (const [token, e] of this.scopes) {\n if (e.decls.size === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[] }>();\n for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = { ...e.bag };\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") Object.assign(e.bag, vals);\n }\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n /** A quality's ordered ladder of stage names (quality.md). */\n stages?: string[];\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n constructor(declarations: ScopeDeclaration[] = [], opts?: { normalise?: (name: string) => string }) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(d: ScopeDeclaration, value: ScalarValue | undefined, writable?: boolean, name?: string): PropertyRow {\n return {\n name: name ?? d.name.toLowerCase(),\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n return this.mountOwned(token, new PropertyBag(declarations));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase())),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n // The quality channel (quality.md): declared here once, so a host that\n // registers a quality gets ordering comparisons and advance() with no\n // further wiring. Only added when a quality exists, so contexts stay\n // byte-identical for products that declare none.\n const qualities = this.qualityLadders();\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase()),\n };\n }\n\n /** Every quality declaration's ladder, keyed scope token then name. */\n private qualityLadders(): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n for (const d of decls) {\n if (d.type !== \"quality\" || d.stages === undefined) continue;\n let m = out.get(token);\n if (!m) { m = new Map(); out.set(token, m); }\n m.set(d.name.toLowerCase(), d.stages);\n }\n }\n return out;\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), {\n type: d.type, enumValues: d.values,\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n // A quality starts at the first rung of its ladder.\n case \"quality\": return d.stages?.[0] ?? \"\";\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { PropertyType, ScalarValue, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
1
+ import { ScalarValue, PropertyType, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
2
2
  export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
3
3
 
4
4
  /**
@@ -10,6 +10,8 @@ interface ScopeDeclaration {
10
10
  name: string;
11
11
  type: PropertyType;
12
12
  values?: string[];
13
+ /** A quality's ordered ladder of stage names (quality.md). */
14
+ stages?: string[];
13
15
  default?: ScalarValue;
14
16
  writable?: boolean;
15
17
  }
@@ -39,6 +41,79 @@ declare const SUPPORTED_SPEC_VERSIONS: readonly [1];
39
41
  * throws on a malformed or unsupported-version spec.
40
42
  */
41
43
  declare function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null;
44
+ /** One property change. `silent` marks a host write (the firing rule: it
45
+ * reaches the audit hook but not subscribers); `reason` is the host's own
46
+ * note for its log. */
47
+ interface BagChange {
48
+ name: string;
49
+ prev?: ScalarValue;
50
+ next: ScalarValue;
51
+ silent: boolean;
52
+ reason?: string;
53
+ }
54
+ /** One examiner row: what a property examiner/editor needs to render and
55
+ * edit a declared property. */
56
+ interface PropertyRow {
57
+ name: string;
58
+ type: PropertyType;
59
+ value: ScalarValue | undefined;
60
+ default: ScalarValue;
61
+ values?: string[];
62
+ writable: boolean;
63
+ }
64
+ declare class PropertyBag {
65
+ /** The live values record (stable identity across reseed, so an
66
+ * EvalContext built over it stays valid). Read-path for evaluation;
67
+ * writes go through `set` so the firing rule applies. */
68
+ readonly values: Record<string, ScalarValue>;
69
+ private decls;
70
+ private readonly subscribers;
71
+ private readonly auditors;
72
+ /** Name normalisation policy: lowercase by default (the registry's
73
+ * long-standing contract); a product whose names are case-significant
74
+ * passes identity. */
75
+ private readonly norm;
76
+ constructor(declarations?: ScopeDeclaration[], opts?: {
77
+ normalise?: (name: string) => string;
78
+ });
79
+ private seed;
80
+ get(name: string): ScalarValue | undefined;
81
+ /** Write a property. Engine writes (the default) notify subscribers;
82
+ * pass `silent: true` for a host write, which reaches only the audit
83
+ * hook. Throws on a read-only property. Returns the change. */
84
+ set(name: string, value: ScalarValue, opts?: {
85
+ silent?: boolean;
86
+ reason?: string;
87
+ }): BagChange;
88
+ /** Notified of engine (non-silent) writes. Returns the unsubscribe. */
89
+ subscribe(fn: (change: BagChange) => void): () => void;
90
+ /** Notified of EVERY write, silent or not. Returns the unsubscribe. */
91
+ onAudit(fn: (change: BagChange) => void): () => void;
92
+ /** Examiner rows: the declared surface only (stray values are storage,
93
+ * not surface). */
94
+ rows(): PropertyRow[];
95
+ declarations(): ScopeDeclaration[];
96
+ /** The one sanctioned copy door: values deep-copied, declarations
97
+ * duplicated, the normalisation policy carried, subscriptions NOT
98
+ * carried. */
99
+ clone(): PropertyBag;
100
+ /** Clear and re-seed from new declarations, in place (the values record
101
+ * keeps its identity, so contexts built over it stay valid). */
102
+ reseed(declarations: ScopeDeclaration[]): void;
103
+ /** Bare values, ready to embed in a product's save. */
104
+ save(): Record<string, ScalarValue>;
105
+ /** Lay saved values over the current ones (call after a fresh seed:
106
+ * orphans land as strays, new declarations keep their defaults; the
107
+ * product decides whether to prune). Does not fire events. */
108
+ load(values: Record<string, ScalarValue>): void;
109
+ }
110
+ /** The versioned owned-state fragment both product save envelopes embed
111
+ * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */
112
+ interface OwnedStateFragment {
113
+ version: number;
114
+ scopes: Record<string, Record<string, ScalarValue>>;
115
+ }
116
+ declare const SAVE_FRAGMENT_VERSION = 1;
42
117
  declare class ScopeRegistry {
43
118
  private readonly scopes;
44
119
  /**
@@ -47,6 +122,14 @@ declare class ScopeRegistry {
47
122
  * type-checked (declarations) and serialized by `save`/`load`.
48
123
  */
49
124
  defineOwned(token: string, declarations: ScopeDeclaration[]): this;
125
+ /**
126
+ * Attach an EXISTING bag as an owned scope - the shared-container move: a
127
+ * host (or the other product) holds the bag; this registry reads, writes
128
+ * and lists it like its own, but the holder saves it.
129
+ */
130
+ mountOwned(token: string, bag: PropertyBag): this;
131
+ /** An owned scope's bag (subscribe, audit, rows live there). */
132
+ ownedBag(token: string): PropertyBag;
50
133
  /**
51
134
  * Re-initialise an existing **owned** scope's bag from new declarations,
52
135
  * clearing its current values. For scope-local state that resets on a context
@@ -65,26 +148,46 @@ declare class ScopeRegistry {
65
148
  has(token: string): boolean;
66
149
  /** Read a property; undefined if the scope or property is not present. */
67
150
  get(scope: string, name: string): ScalarValue | undefined;
68
- /** Write a property. Throws on an unknown or read-only scope/property. */
151
+ /** Write a property (an ENGINE write: the bag's subscribers fire; use
152
+ * the bag directly for silent host writes). Throws on an unknown or
153
+ * read-only scope/property. */
69
154
  set(scope: string, name: string, value: ScalarValue): void;
70
- private writable;
155
+ private foreignWritable;
156
+ /** Examiner rows across every scope with a declared surface: owned bags
157
+ * first, then declared foreign scopes (values read through, writability
158
+ * reflecting the resolver). Opaque foreign scopes are not listed. */
159
+ listProperties(): ({
160
+ scope: string;
161
+ } & PropertyRow)[];
71
162
  /**
72
163
  * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
73
164
  * bags, foreign scopes as their resolvers. `host` carries dialect-function
74
165
  * callbacks (PRNG, tag lookups) and is passed through untouched.
75
166
  */
76
167
  toEvalContext(host?: Record<string, unknown>): EvalContext;
168
+ /** Every quality declaration's ladder, keyed scope token then name. */
169
+ private qualityLadders;
77
170
  /**
78
171
  * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
79
172
  * declarations are **omitted** (opaque - references into them are not flagged);
80
173
  * declared scopes contribute their property types for validation.
81
174
  */
82
175
  toSchema(): ExpressionSchema;
83
- /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
176
+ /** Serialize **owned** scopes only (foreign scopes are host-owned,
177
+ * host-saved), as bare bags - the 0.1.x shape, kept stable so existing
178
+ * consumers' save formats are untouched. A product embedding the
179
+ * versioned cross-product shape uses `saveFragment`. */
84
180
  save(): Record<string, Record<string, ScalarValue>>;
85
- /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
181
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
182
+ * are ignored. */
86
183
  load(blob: Record<string, Record<string, ScalarValue>>): void;
184
+ /** The versioned owned-state fragment (the one serialisation shape both
185
+ * product families' save envelopes embed when they adopt the kernel;
186
+ * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
187
+ saveFragment(): OwnedStateFragment;
188
+ /** Restore from a versioned fragment; an unsupported version throws. */
189
+ loadFragment(fragment: OwnedStateFragment): void;
87
190
  private assertFree;
88
191
  }
89
192
 
90
- export { SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
193
+ export { type BagChange, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PropertyType, ScalarValue, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
1
+ import { ScalarValue, PropertyType, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
2
2
  export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
3
3
 
4
4
  /**
@@ -10,6 +10,8 @@ interface ScopeDeclaration {
10
10
  name: string;
11
11
  type: PropertyType;
12
12
  values?: string[];
13
+ /** A quality's ordered ladder of stage names (quality.md). */
14
+ stages?: string[];
13
15
  default?: ScalarValue;
14
16
  writable?: boolean;
15
17
  }
@@ -39,6 +41,79 @@ declare const SUPPORTED_SPEC_VERSIONS: readonly [1];
39
41
  * throws on a malformed or unsupported-version spec.
40
42
  */
41
43
  declare function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null;
44
+ /** One property change. `silent` marks a host write (the firing rule: it
45
+ * reaches the audit hook but not subscribers); `reason` is the host's own
46
+ * note for its log. */
47
+ interface BagChange {
48
+ name: string;
49
+ prev?: ScalarValue;
50
+ next: ScalarValue;
51
+ silent: boolean;
52
+ reason?: string;
53
+ }
54
+ /** One examiner row: what a property examiner/editor needs to render and
55
+ * edit a declared property. */
56
+ interface PropertyRow {
57
+ name: string;
58
+ type: PropertyType;
59
+ value: ScalarValue | undefined;
60
+ default: ScalarValue;
61
+ values?: string[];
62
+ writable: boolean;
63
+ }
64
+ declare class PropertyBag {
65
+ /** The live values record (stable identity across reseed, so an
66
+ * EvalContext built over it stays valid). Read-path for evaluation;
67
+ * writes go through `set` so the firing rule applies. */
68
+ readonly values: Record<string, ScalarValue>;
69
+ private decls;
70
+ private readonly subscribers;
71
+ private readonly auditors;
72
+ /** Name normalisation policy: lowercase by default (the registry's
73
+ * long-standing contract); a product whose names are case-significant
74
+ * passes identity. */
75
+ private readonly norm;
76
+ constructor(declarations?: ScopeDeclaration[], opts?: {
77
+ normalise?: (name: string) => string;
78
+ });
79
+ private seed;
80
+ get(name: string): ScalarValue | undefined;
81
+ /** Write a property. Engine writes (the default) notify subscribers;
82
+ * pass `silent: true` for a host write, which reaches only the audit
83
+ * hook. Throws on a read-only property. Returns the change. */
84
+ set(name: string, value: ScalarValue, opts?: {
85
+ silent?: boolean;
86
+ reason?: string;
87
+ }): BagChange;
88
+ /** Notified of engine (non-silent) writes. Returns the unsubscribe. */
89
+ subscribe(fn: (change: BagChange) => void): () => void;
90
+ /** Notified of EVERY write, silent or not. Returns the unsubscribe. */
91
+ onAudit(fn: (change: BagChange) => void): () => void;
92
+ /** Examiner rows: the declared surface only (stray values are storage,
93
+ * not surface). */
94
+ rows(): PropertyRow[];
95
+ declarations(): ScopeDeclaration[];
96
+ /** The one sanctioned copy door: values deep-copied, declarations
97
+ * duplicated, the normalisation policy carried, subscriptions NOT
98
+ * carried. */
99
+ clone(): PropertyBag;
100
+ /** Clear and re-seed from new declarations, in place (the values record
101
+ * keeps its identity, so contexts built over it stay valid). */
102
+ reseed(declarations: ScopeDeclaration[]): void;
103
+ /** Bare values, ready to embed in a product's save. */
104
+ save(): Record<string, ScalarValue>;
105
+ /** Lay saved values over the current ones (call after a fresh seed:
106
+ * orphans land as strays, new declarations keep their defaults; the
107
+ * product decides whether to prune). Does not fire events. */
108
+ load(values: Record<string, ScalarValue>): void;
109
+ }
110
+ /** The versioned owned-state fragment both product save envelopes embed
111
+ * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */
112
+ interface OwnedStateFragment {
113
+ version: number;
114
+ scopes: Record<string, Record<string, ScalarValue>>;
115
+ }
116
+ declare const SAVE_FRAGMENT_VERSION = 1;
42
117
  declare class ScopeRegistry {
43
118
  private readonly scopes;
44
119
  /**
@@ -47,6 +122,14 @@ declare class ScopeRegistry {
47
122
  * type-checked (declarations) and serialized by `save`/`load`.
48
123
  */
49
124
  defineOwned(token: string, declarations: ScopeDeclaration[]): this;
125
+ /**
126
+ * Attach an EXISTING bag as an owned scope - the shared-container move: a
127
+ * host (or the other product) holds the bag; this registry reads, writes
128
+ * and lists it like its own, but the holder saves it.
129
+ */
130
+ mountOwned(token: string, bag: PropertyBag): this;
131
+ /** An owned scope's bag (subscribe, audit, rows live there). */
132
+ ownedBag(token: string): PropertyBag;
50
133
  /**
51
134
  * Re-initialise an existing **owned** scope's bag from new declarations,
52
135
  * clearing its current values. For scope-local state that resets on a context
@@ -65,26 +148,46 @@ declare class ScopeRegistry {
65
148
  has(token: string): boolean;
66
149
  /** Read a property; undefined if the scope or property is not present. */
67
150
  get(scope: string, name: string): ScalarValue | undefined;
68
- /** Write a property. Throws on an unknown or read-only scope/property. */
151
+ /** Write a property (an ENGINE write: the bag's subscribers fire; use
152
+ * the bag directly for silent host writes). Throws on an unknown or
153
+ * read-only scope/property. */
69
154
  set(scope: string, name: string, value: ScalarValue): void;
70
- private writable;
155
+ private foreignWritable;
156
+ /** Examiner rows across every scope with a declared surface: owned bags
157
+ * first, then declared foreign scopes (values read through, writability
158
+ * reflecting the resolver). Opaque foreign scopes are not listed. */
159
+ listProperties(): ({
160
+ scope: string;
161
+ } & PropertyRow)[];
71
162
  /**
72
163
  * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
73
164
  * bags, foreign scopes as their resolvers. `host` carries dialect-function
74
165
  * callbacks (PRNG, tag lookups) and is passed through untouched.
75
166
  */
76
167
  toEvalContext(host?: Record<string, unknown>): EvalContext;
168
+ /** Every quality declaration's ladder, keyed scope token then name. */
169
+ private qualityLadders;
77
170
  /**
78
171
  * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
79
172
  * declarations are **omitted** (opaque - references into them are not flagged);
80
173
  * declared scopes contribute their property types for validation.
81
174
  */
82
175
  toSchema(): ExpressionSchema;
83
- /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
176
+ /** Serialize **owned** scopes only (foreign scopes are host-owned,
177
+ * host-saved), as bare bags - the 0.1.x shape, kept stable so existing
178
+ * consumers' save formats are untouched. A product embedding the
179
+ * versioned cross-product shape uses `saveFragment`. */
84
180
  save(): Record<string, Record<string, ScalarValue>>;
85
- /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
181
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
182
+ * are ignored. */
86
183
  load(blob: Record<string, Record<string, ScalarValue>>): void;
184
+ /** The versioned owned-state fragment (the one serialisation shape both
185
+ * product families' save envelopes embed when they adopt the kernel;
186
+ * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
187
+ saveFragment(): OwnedStateFragment;
188
+ /** Restore from a versioned fragment; an unsupported version throws. */
189
+ loadFragment(fragment: OwnedStateFragment): void;
87
190
  private assertFree;
88
191
  }
89
192
 
90
- export { SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
193
+ export { type BagChange, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
package/dist/index.js CHANGED
@@ -18,6 +18,106 @@ function readScopeRegistrySpec(source) {
18
18
  }
19
19
  return spec;
20
20
  }
21
+ var PropertyBag = class _PropertyBag {
22
+ /** The live values record (stable identity across reseed, so an
23
+ * EvalContext built over it stays valid). Read-path for evaluation;
24
+ * writes go through `set` so the firing rule applies. */
25
+ values = {};
26
+ decls = /* @__PURE__ */ new Map();
27
+ subscribers = /* @__PURE__ */ new Set();
28
+ auditors = /* @__PURE__ */ new Set();
29
+ /** Name normalisation policy: lowercase by default (the registry's
30
+ * long-standing contract); a product whose names are case-significant
31
+ * passes identity. */
32
+ norm;
33
+ constructor(declarations = [], opts) {
34
+ this.norm = opts?.normalise ?? ((n) => n.toLowerCase());
35
+ this.seed(declarations);
36
+ }
37
+ seed(declarations) {
38
+ for (const d of declarations) {
39
+ const name = this.norm(d.name);
40
+ this.decls.set(name, d);
41
+ this.values[name] = structuredClone(d.default ?? defaultFor(d));
42
+ }
43
+ }
44
+ get(name) {
45
+ return this.values[this.norm(name)];
46
+ }
47
+ /** Write a property. Engine writes (the default) notify subscribers;
48
+ * pass `silent: true` for a host write, which reaches only the audit
49
+ * hook. Throws on a read-only property. Returns the change. */
50
+ set(name, value, opts) {
51
+ const n = this.norm(name);
52
+ if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
53
+ const change = {
54
+ name: n,
55
+ prev: this.values[n],
56
+ next: value,
57
+ silent: opts?.silent ?? false,
58
+ reason: opts?.reason
59
+ };
60
+ this.values[n] = value;
61
+ for (const audit of this.auditors) audit(change);
62
+ if (!change.silent) for (const fn of this.subscribers) fn(change);
63
+ return change;
64
+ }
65
+ /** Notified of engine (non-silent) writes. Returns the unsubscribe. */
66
+ subscribe(fn) {
67
+ this.subscribers.add(fn);
68
+ return () => this.subscribers.delete(fn);
69
+ }
70
+ /** Notified of EVERY write, silent or not. Returns the unsubscribe. */
71
+ onAudit(fn) {
72
+ this.auditors.add(fn);
73
+ return () => this.auditors.delete(fn);
74
+ }
75
+ /** Examiner rows: the declared surface only (stray values are storage,
76
+ * not surface). */
77
+ rows() {
78
+ return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), void 0, name));
79
+ }
80
+ declarations() {
81
+ return [...this.decls.values()];
82
+ }
83
+ /** The one sanctioned copy door: values deep-copied, declarations
84
+ * duplicated, the normalisation policy carried, subscriptions NOT
85
+ * carried. */
86
+ clone() {
87
+ const c = new _PropertyBag([], { normalise: this.norm });
88
+ c.decls = new Map(this.decls);
89
+ Object.assign(c.values, structuredClone(this.values));
90
+ return c;
91
+ }
92
+ /** Clear and re-seed from new declarations, in place (the values record
93
+ * keeps its identity, so contexts built over it stay valid). */
94
+ reseed(declarations) {
95
+ for (const k of Object.keys(this.values)) delete this.values[k];
96
+ this.decls.clear();
97
+ this.seed(declarations);
98
+ }
99
+ /** Bare values, ready to embed in a product's save. */
100
+ save() {
101
+ return structuredClone(this.values);
102
+ }
103
+ /** Lay saved values over the current ones (call after a fresh seed:
104
+ * orphans land as strays, new declarations keep their defaults; the
105
+ * product decides whether to prune). Does not fire events. */
106
+ load(values) {
107
+ for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;
108
+ }
109
+ };
110
+ function rowFor(d, value, writable, name) {
111
+ return {
112
+ name: name ?? d.name.toLowerCase(),
113
+ type: d.type,
114
+ value,
115
+ default: d.default ?? defaultFor(d),
116
+ ...d.values !== void 0 ? { values: d.values } : {},
117
+ writable: writable ?? d.writable ?? true
118
+ };
119
+ }
120
+ var SAVE_FRAGMENT_VERSION = 1;
21
121
  var ScopeRegistry = class {
22
122
  scopes = /* @__PURE__ */ new Map();
23
123
  /**
@@ -26,17 +126,24 @@ var ScopeRegistry = class {
26
126
  * type-checked (declarations) and serialized by `save`/`load`.
27
127
  */
28
128
  defineOwned(token, declarations) {
129
+ return this.mountOwned(token, new PropertyBag(declarations));
130
+ }
131
+ /**
132
+ * Attach an EXISTING bag as an owned scope - the shared-container move: a
133
+ * host (or the other product) holds the bag; this registry reads, writes
134
+ * and lists it like its own, but the holder saves it.
135
+ */
136
+ mountOwned(token, bag) {
29
137
  this.assertFree(token);
30
- const bag = {};
31
- const decls = /* @__PURE__ */ new Map();
32
- for (const d of declarations) {
33
- const name = d.name.toLowerCase();
34
- decls.set(name, d);
35
- bag[name] = d.default ?? defaultFor(d);
36
- }
37
- this.scopes.set(token, { kind: "owned", bag, decls });
138
+ this.scopes.set(token, { kind: "owned", bag });
38
139
  return this;
39
140
  }
141
+ /** An owned scope's bag (subscribe, audit, rows live there). */
142
+ ownedBag(token) {
143
+ const e = this.scopes.get(token);
144
+ if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
145
+ return e.bag;
146
+ }
40
147
  /**
41
148
  * Re-initialise an existing **owned** scope's bag from new declarations,
42
149
  * clearing its current values. For scope-local state that resets on a context
@@ -45,15 +152,7 @@ var ScopeRegistry = class {
45
152
  * registry stays valid.
46
153
  */
47
154
  reseedOwned(token, declarations) {
48
- const e = this.scopes.get(token);
49
- if (!e || e.kind !== "owned") throw new Error(`'@${token}' is not an owned scope`);
50
- for (const k of Object.keys(e.bag)) delete e.bag[k];
51
- e.decls.clear();
52
- for (const d of declarations) {
53
- const name = d.name.toLowerCase();
54
- e.decls.set(name, d);
55
- e.bag[name] = d.default ?? defaultFor(d);
56
- }
155
+ this.ownedBag(token).reseed(declarations);
57
156
  return this;
58
157
  }
59
158
  /**
@@ -76,23 +175,49 @@ var ScopeRegistry = class {
76
175
  get(scope, name) {
77
176
  const e = this.scopes.get(scope);
78
177
  if (!e) return void 0;
79
- const n = name.toLowerCase();
80
- return e.kind === "owned" ? e.bag[n] : e.resolver.get(n);
178
+ return e.kind === "owned" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());
81
179
  }
82
- /** Write a property. Throws on an unknown or read-only scope/property. */
180
+ /** Write a property (an ENGINE write: the bag's subscribers fire; use
181
+ * the bag directly for silent host writes). Throws on an unknown or
182
+ * read-only scope/property. */
83
183
  set(scope, name, value) {
84
184
  const e = this.scopes.get(scope);
85
185
  if (!e) throw new Error(`unknown scope '@${scope}'`);
186
+ if (e.kind === "owned") {
187
+ try {
188
+ e.bag.set(name, value);
189
+ } catch {
190
+ throw new Error(`'@${scope}.${name}' is read-only`);
191
+ }
192
+ return;
193
+ }
86
194
  const n = name.toLowerCase();
87
- if (!this.writable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
88
- if (e.kind === "owned") e.bag[n] = value;
89
- else e.resolver.set(n, value);
195
+ if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);
196
+ e.resolver.set(n, value);
90
197
  }
91
- writable(e, name) {
92
- if (e.kind === "owned") return e.decls.get(name)?.writable ?? true;
198
+ foreignWritable(e, name) {
93
199
  if (!e.resolver.set) return false;
94
200
  return e.decls.get(name)?.writable ?? e.scopeWritable;
95
201
  }
202
+ /** Examiner rows across every scope with a declared surface: owned bags
203
+ * first, then declared foreign scopes (values read through, writability
204
+ * reflecting the resolver). Opaque foreign scopes are not listed. */
205
+ listProperties() {
206
+ const out = [];
207
+ for (const [token, e] of this.scopes) {
208
+ if (e.kind === "owned") {
209
+ for (const row of e.bag.rows()) out.push({ scope: token, ...row });
210
+ } else {
211
+ for (const d of e.decls.values()) {
212
+ out.push({
213
+ scope: token,
214
+ ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()))
215
+ });
216
+ }
217
+ }
218
+ }
219
+ return out;
220
+ }
96
221
  /**
97
222
  * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
98
223
  * bags, foreign scopes as their resolvers. `host` carries dialect-function
@@ -101,9 +226,31 @@ var ScopeRegistry = class {
101
226
  toEvalContext(host) {
102
227
  const scopes = {};
103
228
  for (const [token, e] of this.scopes) {
104
- scopes[token] = e.kind === "owned" ? e.bag : e.resolver;
229
+ scopes[token] = e.kind === "owned" ? e.bag.values : e.resolver;
230
+ }
231
+ const qualities = this.qualityLadders();
232
+ return qualities.size === 0 ? { scopes, host } : {
233
+ scopes,
234
+ host,
235
+ qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase())
236
+ };
237
+ }
238
+ /** Every quality declaration's ladder, keyed scope token then name. */
239
+ qualityLadders() {
240
+ const out = /* @__PURE__ */ new Map();
241
+ for (const [token, e] of this.scopes) {
242
+ const decls = e.kind === "owned" ? e.bag.declarations() : [...e.decls.values()];
243
+ for (const d of decls) {
244
+ if (d.type !== "quality" || d.stages === void 0) continue;
245
+ let m = out.get(token);
246
+ if (!m) {
247
+ m = /* @__PURE__ */ new Map();
248
+ out.set(token, m);
249
+ }
250
+ m.set(d.name.toLowerCase(), d.stages);
251
+ }
105
252
  }
106
- return { scopes, host };
253
+ return out;
107
254
  }
108
255
  /**
109
256
  * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
@@ -113,25 +260,47 @@ var ScopeRegistry = class {
113
260
  toSchema() {
114
261
  const properties = /* @__PURE__ */ new Map();
115
262
  for (const [token, e] of this.scopes) {
116
- if (e.decls.size === 0) continue;
263
+ const decls = e.kind === "owned" ? e.bag.declarations() : [...e.decls.values()];
264
+ if (decls.length === 0) continue;
117
265
  const m = /* @__PURE__ */ new Map();
118
- for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });
266
+ for (const d of decls) m.set(d.name.toLowerCase(), {
267
+ type: d.type,
268
+ enumValues: d.values,
269
+ ...d.stages !== void 0 ? { stages: d.stages } : {}
270
+ });
119
271
  properties.set(token, m);
120
272
  }
121
273
  return { properties };
122
274
  }
123
- /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
275
+ /** Serialize **owned** scopes only (foreign scopes are host-owned,
276
+ * host-saved), as bare bags - the 0.1.x shape, kept stable so existing
277
+ * consumers' save formats are untouched. A product embedding the
278
+ * versioned cross-product shape uses `saveFragment`. */
124
279
  save() {
125
280
  const out = {};
126
- for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = { ...e.bag };
281
+ for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = e.bag.save();
127
282
  return out;
128
283
  }
129
- /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
284
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes
285
+ * are ignored. */
130
286
  load(blob) {
131
287
  for (const [token, vals] of Object.entries(blob)) {
132
288
  const e = this.scopes.get(token);
133
- if (e?.kind === "owned") Object.assign(e.bag, vals);
289
+ if (e?.kind === "owned") e.bag.load(vals);
290
+ }
291
+ }
292
+ /** The versioned owned-state fragment (the one serialisation shape both
293
+ * product families' save envelopes embed when they adopt the kernel;
294
+ * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */
295
+ saveFragment() {
296
+ return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };
297
+ }
298
+ /** Restore from a versioned fragment; an unsupported version throws. */
299
+ loadFragment(fragment) {
300
+ if (fragment.version !== SAVE_FRAGMENT_VERSION) {
301
+ throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);
134
302
  }
303
+ this.load(fragment.scopes);
135
304
  }
136
305
  assertFree(token) {
137
306
  if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);
@@ -150,9 +319,12 @@ function defaultFor(d) {
150
319
  return d.values?.[0] ?? "";
151
320
  case "flags":
152
321
  return [];
322
+ // A quality starts at the first rung of its ladder.
323
+ case "quality":
324
+ return d.stages?.[0] ?? "";
153
325
  }
154
326
  }
155
327
 
156
- export { SUPPORTED_SPEC_VERSIONS, ScopeRegistry, readScopeRegistrySpec };
328
+ export { PropertyBag, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, ScopeRegistry, readScopeRegistrySpec };
157
329
  //# sourceMappingURL=index.js.map
158
330
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: Record<string, ScalarValue>;\n decls: Map<string, ScopeDeclaration>;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.assertFree(token);\n const bag: Record<string, ScalarValue> = {};\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n decls.set(name, d);\n bag[name] = d.default ?? defaultFor(d);\n }\n this.scopes.set(token, { kind: \"owned\", bag, decls });\n return this;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n for (const k of Object.keys(e.bag)) delete e.bag[k];\n e.decls.clear();\n for (const d of declarations) {\n const name = d.name.toLowerCase();\n e.decls.set(name, d);\n e.bag[name] = d.default ?? defaultFor(d);\n }\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n const n = name.toLowerCase();\n return e.kind === \"owned\" ? e.bag[n] : e.resolver.get(n);\n }\n\n /** Write a property. Throws on an unknown or read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n const n = name.toLowerCase();\n if (!this.writable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n if (e.kind === \"owned\") e.bag[n] = value;\n else e.resolver.set!(n, value);\n }\n\n private writable(e: Entry, name: string): boolean {\n if (e.kind === \"owned\") return e.decls.get(name)?.writable ?? true;\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag : e.resolver;\n }\n return { scopes, host };\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[] }>>();\n for (const [token, e] of this.scopes) {\n if (e.decls.size === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[] }>();\n for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = { ...e.bag };\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") Object.assign(e.bag, vals);\n }\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n /** A quality's ordered ladder of stage names (quality.md). */\n stages?: string[];\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n constructor(declarations: ScopeDeclaration[] = [], opts?: { normalise?: (name: string) => string }) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(d: ScopeDeclaration, value: ScalarValue | undefined, writable?: boolean, name?: string): PropertyRow {\n return {\n name: name ?? d.name.toLowerCase(),\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[]): this {\n return this.mountOwned(token, new PropertyBag(declarations));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase())),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n // The quality channel (quality.md): declared here once, so a host that\n // registers a quality gets ordering comparisons and advance() with no\n // further wiring. Only added when a quality exists, so contexts stay\n // byte-identical for products that declare none.\n const qualities = this.qualityLadders();\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase()),\n };\n }\n\n /** Every quality declaration's ladder, keyed scope token then name. */\n private qualityLadders(): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n for (const d of decls) {\n if (d.type !== \"quality\" || d.stages === undefined) continue;\n let m = out.get(token);\n if (!m) { m = new Map(); out.set(token, m); }\n m.set(d.name.toLowerCase(), d.stages);\n }\n }\n return out;\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), {\n type: d.type, enumValues: d.values,\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\nfunction defaultFor(d: ScopeDeclaration): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n // A quality starts at the first rung of its ladder.\n case \"quality\": return d.stages?.[0] ?? \"\";\n }\n}\n"]}
package/package.json CHANGED
@@ -1,12 +1,22 @@
1
1
  {
2
2
  "name": "@wildwinter/scoperegistry",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scope registry / runtime state container for @wildwinter/expr: owned property scopes + foreign (host-resolved) scopes, save/load, and the scopeRegistrySpec interop format. Produces the EvalContext and ExpressionSchema that expr consumes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Ian Thomas",
8
- "repository": { "type": "git", "url": "git+https://github.com/wildwinter/expr.git", "directory": "packages/scoperegistry" },
9
- "keywords": ["expression", "scope", "registry", "state", "wildwinter"],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/wildwinter/expr.git",
11
+ "directory": "packages/scoperegistry"
12
+ },
13
+ "keywords": [
14
+ "expression",
15
+ "scope",
16
+ "registry",
17
+ "state",
18
+ "wildwinter"
19
+ ],
10
20
  "main": "./dist/index.cjs",
11
21
  "module": "./dist/index.js",
12
22
  "types": "./dist/index.d.ts",
@@ -17,7 +27,11 @@
17
27
  "require": "./dist/index.cjs"
18
28
  }
19
29
  },
20
- "files": ["dist", "README.md", "LICENSE"],
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
21
35
  "sideEffects": false,
22
36
  "publishConfig": {
23
37
  "registry": "https://registry.npmjs.org",
@@ -31,6 +45,6 @@
31
45
  "prepublishOnly": "npm run build && npm test"
32
46
  },
33
47
  "dependencies": {
34
- "@wildwinter/expr": "^0.3.0"
48
+ "@wildwinter/expr": "^0.4.0"
35
49
  }
36
50
  }