@wildwinter/scoperegistry 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ian Thomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @wildwinter/scoperegistry
2
+
3
+ The scope registry / runtime state container that sits on top of
4
+ [`@wildwinter/expr`](../expr). expr is a stateless calculator; this package is
5
+ the **state layer**.
6
+
7
+ It owns the world state as a set of named **scopes**, each either:
8
+
9
+ - **owned** — a property bag this registry stores, seeds from declaration
10
+ defaults, and serializes via `save`/`load`; or
11
+ - **foreign** — host- or other-engine-resolved at runtime through a
12
+ `{ get, set? }` resolver, never stored here (read-only if there's no setter).
13
+
14
+ It then produces the two things expr consumes:
15
+
16
+ - **`toEvalContext()`** → the `EvalContext` for `evaluate` (owned bags + foreign
17
+ resolvers).
18
+ - **`toSchema()`** → the `ExpressionSchema` for `validateExpr` (from declarations;
19
+ undeclared scopes are opaque and unflagged).
20
+
21
+ Plus **`readScopeRegistrySpec(json)`** — extract a `scopeRegistrySpec` (the
22
+ interop format) from any JSON value (a `.storyworld` bundle, or a standalone
23
+ manifest), so one owner's scope declarations can be imported for validation by
24
+ another engine.
25
+
26
+ `expr` never depends on this; this depends one-way on `expr`. Read-only is
27
+ settable per scope (a resolver with no `set`) and per property
28
+ (`writable: false`), enforced by `set`.
29
+
30
+ ```ts
31
+ import { ScopeRegistry } from "@wildwinter/scoperegistry";
32
+
33
+ const reg = new ScopeRegistry()
34
+ .defineOwned("patter", [{ name: "hp", type: "number", default: 10 }])
35
+ .defineForeign("game", { get: (n) => host.read(n), set: (n, v) => host.write(n, v) });
36
+
37
+ evaluate(ast, reg.toEvalContext(), dialect); // reads owned + foreign
38
+ validateExpr(ast, reg.toSchema(), dialect); // checks declared scopes
39
+ const blob = reg.save(); // owned scopes only
40
+ ```
41
+
42
+ Design rationale: `design/scope-registry.md` in the Patter repo.
package/dist/index.cjs ADDED
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var SUPPORTED_SPEC_VERSIONS = [1];
5
+ function readScopeRegistrySpec(source) {
6
+ if (!source || typeof source !== "object") return null;
7
+ const raw = source.scopeRegistrySpec;
8
+ if (raw === void 0) return null;
9
+ if (typeof raw !== "object" || raw === null) throw new Error("scopeRegistrySpec must be an object");
10
+ const spec = raw;
11
+ if (typeof spec.version !== "number") throw new Error("scopeRegistrySpec.version must be a number");
12
+ if (!SUPPORTED_SPEC_VERSIONS.includes(spec.version)) {
13
+ throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(", ")})`);
14
+ }
15
+ if (!Array.isArray(spec.scopes)) throw new Error("scopeRegistrySpec.scopes must be an array");
16
+ for (const s of spec.scopes) {
17
+ if (!s || typeof s !== "object" || typeof s.token !== "string") {
18
+ throw new Error("each scopeRegistrySpec scope needs a string token");
19
+ }
20
+ }
21
+ return spec;
22
+ }
23
+ var ScopeRegistry = class {
24
+ scopes = /* @__PURE__ */ new Map();
25
+ /**
26
+ * Register a scope this registry **owns and stores**. Its bag is seeded from
27
+ * each declaration's `default` (or a type default). Owned scopes are
28
+ * type-checked (declarations) and serialized by `save`/`load`.
29
+ */
30
+ defineOwned(token, declarations) {
31
+ 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 });
40
+ return this;
41
+ }
42
+ /**
43
+ * Re-initialise an existing **owned** scope's bag from new declarations,
44
+ * clearing its current values. For scope-local state that resets on a context
45
+ * change (e.g. entering a new scene / site / deck) without disturbing other
46
+ * scopes. Mutates the bag in place, so an `EvalContext` already built from this
47
+ * registry stays valid.
48
+ */
49
+ 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
+ }
59
+ return this;
60
+ }
61
+ /**
62
+ * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The
63
+ * values live in the host/other engine and are never stored or saved here.
64
+ * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used
65
+ * only for validation; omit them for an opaque scope.
66
+ */
67
+ defineForeign(token, resolver, declarations = [], scopeWritable = true) {
68
+ this.assertFree(token);
69
+ const decls = /* @__PURE__ */ new Map();
70
+ for (const d of declarations) decls.set(d.name.toLowerCase(), d);
71
+ this.scopes.set(token, { kind: "foreign", resolver, decls, scopeWritable });
72
+ return this;
73
+ }
74
+ has(token) {
75
+ return this.scopes.has(token);
76
+ }
77
+ /** Read a property; undefined if the scope or property is not present. */
78
+ get(scope, name) {
79
+ const e = this.scopes.get(scope);
80
+ if (!e) return void 0;
81
+ const n = name.toLowerCase();
82
+ return e.kind === "owned" ? e.bag[n] : e.resolver.get(n);
83
+ }
84
+ /** Write a property. Throws on an unknown or read-only scope/property. */
85
+ set(scope, name, value) {
86
+ const e = this.scopes.get(scope);
87
+ if (!e) throw new Error(`unknown scope '@${scope}'`);
88
+ 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);
92
+ }
93
+ writable(e, name) {
94
+ if (e.kind === "owned") return e.decls.get(name)?.writable ?? true;
95
+ if (!e.resolver.set) return false;
96
+ return e.decls.get(name)?.writable ?? e.scopeWritable;
97
+ }
98
+ /**
99
+ * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
100
+ * bags, foreign scopes as their resolvers. `host` carries dialect-function
101
+ * callbacks (PRNG, tag lookups) and is passed through untouched.
102
+ */
103
+ toEvalContext(host) {
104
+ const scopes = {};
105
+ for (const [token, e] of this.scopes) {
106
+ scopes[token] = e.kind === "owned" ? e.bag : e.resolver;
107
+ }
108
+ return { scopes, host };
109
+ }
110
+ /**
111
+ * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
112
+ * declarations are **omitted** (opaque - references into them are not flagged);
113
+ * declared scopes contribute their property types for validation.
114
+ */
115
+ toSchema() {
116
+ const properties = /* @__PURE__ */ new Map();
117
+ for (const [token, e] of this.scopes) {
118
+ if (e.decls.size === 0) continue;
119
+ const m = /* @__PURE__ */ new Map();
120
+ for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });
121
+ properties.set(token, m);
122
+ }
123
+ return { properties };
124
+ }
125
+ /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
126
+ save() {
127
+ const out = {};
128
+ for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = { ...e.bag };
129
+ return out;
130
+ }
131
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
132
+ load(blob) {
133
+ for (const [token, vals] of Object.entries(blob)) {
134
+ const e = this.scopes.get(token);
135
+ if (e?.kind === "owned") Object.assign(e.bag, vals);
136
+ }
137
+ }
138
+ assertFree(token) {
139
+ if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);
140
+ }
141
+ };
142
+ function defaultFor(d) {
143
+ if (d.default !== void 0) return d.default;
144
+ switch (d.type) {
145
+ case "boolean":
146
+ return false;
147
+ case "number":
148
+ return 0;
149
+ case "string":
150
+ return "";
151
+ case "enum":
152
+ return d.values?.[0] ?? "";
153
+ case "flags":
154
+ return [];
155
+ }
156
+ }
157
+
158
+ exports.SUPPORTED_SPEC_VERSIONS = SUPPORTED_SPEC_VERSIONS;
159
+ exports.ScopeRegistry = ScopeRegistry;
160
+ exports.readScopeRegistrySpec = readScopeRegistrySpec;
161
+ //# sourceMappingURL=index.cjs.map
162
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AA4DO,IAAM,uBAAA,GAA0B,CAAC,CAAC;AAQlC,SAAS,sBAAsB,MAAA,EAA2C;AAC/E,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,IAAA;AAClD,EAAA,MAAM,MAAO,MAAA,CAAmC,iBAAA;AAChD,EAAA,IAAI,GAAA,KAAQ,QAAW,OAAO,IAAA;AAC9B,EAAA,IAAI,OAAO,QAAQ,QAAA,IAAY,GAAA,KAAQ,MAAM,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAClG,EAAA,MAAM,IAAA,GAAO,GAAA;AACb,EAAA,IAAI,OAAO,IAAA,CAAK,OAAA,KAAY,UAAU,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAClG,EAAA,IAAI,CAAE,uBAAA,CAA8C,QAAA,CAAS,IAAA,CAAK,OAAO,CAAA,EAAG;AAC1E,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sCAAA,EAAyC,IAAA,CAAK,OAAO,gBAAgB,uBAAA,CAAwB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,EAC5H;AACA,EAAA,IAAI,CAAC,MAAM,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,2CAA2C,CAAA;AAC5F,EAAA,KAAA,MAAW,CAAA,IAAK,KAAK,MAAA,EAAQ;AAC3B,IAAA,IAAI,CAAC,KAAK,OAAO,CAAA,KAAM,YAAY,OAAQ,CAAA,CAAgB,UAAU,QAAA,EAAU;AAC7E,MAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,IACrE;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAmBO,IAAM,gBAAN,MAAoB;AAAA,EACR,MAAA,uBAAa,GAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjD,WAAA,CAAY,OAAe,YAAA,EAAwC;AACjE,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,MAAM,MAAmC,EAAC;AAC1C,IAAA,MAAM,KAAA,uBAAY,GAAA,EAA8B;AAChD,IAAA,KAAA,MAAW,KAAK,YAAA,EAAc;AAC5B,MAAA,MAAM,IAAA,GAAO,CAAA,CAAE,IAAA,CAAK,WAAA,EAAY;AAChC,MAAA,KAAA,CAAM,GAAA,CAAI,MAAM,CAAC,CAAA;AACjB,MAAA,GAAA,CAAI,IAAI,CAAA,GAAI,CAAA,CAAE,OAAA,IAAW,WAAW,CAAC,CAAA;AAAA,IACvC;AACA,IAAA,IAAA,CAAK,MAAA,CAAO,IAAI,KAAA,EAAO,EAAE,MAAM,OAAA,EAAS,GAAA,EAAK,OAAO,CAAA;AACpD,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAA,CAAY,OAAe,YAAA,EAAwC;AACjE,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,CAAA,IAAK,CAAA,CAAE,IAAA,KAAS,OAAA,QAAe,IAAI,KAAA,CAAM,CAAA,EAAA,EAAK,KAAK,CAAA,uBAAA,CAAyB,CAAA;AACjF,IAAA,KAAA,MAAW,CAAA,IAAK,OAAO,IAAA,CAAK,CAAA,CAAE,GAAG,CAAA,EAAG,OAAO,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA;AAClD,IAAA,CAAA,CAAE,MAAM,KAAA,EAAM;AACd,IAAA,KAAA,MAAW,KAAK,YAAA,EAAc;AAC5B,MAAA,MAAM,IAAA,GAAO,CAAA,CAAE,IAAA,CAAK,WAAA,EAAY;AAChC,MAAA,CAAA,CAAE,KAAA,CAAM,GAAA,CAAI,IAAA,EAAM,CAAC,CAAA;AACnB,MAAA,CAAA,CAAE,IAAI,IAAI,CAAA,GAAI,CAAA,CAAE,OAAA,IAAW,WAAW,CAAC,CAAA;AAAA,IACzC;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,cACE,KAAA,EACA,QAAA,EACA,eAAmC,EAAC,EACpC,gBAAgB,IAAA,EACV;AACN,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,MAAM,KAAA,uBAAY,GAAA,EAA8B;AAChD,IAAA,KAAA,MAAW,CAAA,IAAK,cAAc,KAAA,CAAM,GAAA,CAAI,EAAE,IAAA,CAAK,WAAA,IAAe,CAAC,CAAA;AAC/D,IAAA,IAAA,CAAK,MAAA,CAAO,IAAI,KAAA,EAAO,EAAE,MAAM,SAAA,EAAW,QAAA,EAAU,KAAA,EAAO,aAAA,EAAe,CAAA;AAC1E,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,IAAI,KAAA,EAAwB;AAC1B,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAAA,EAC9B;AAAA;AAAA,EAGA,GAAA,CAAI,OAAe,IAAA,EAAuC;AACxD,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAG,OAAO,MAAA;AACf,IAAA,MAAM,CAAA,GAAI,KAAK,WAAA,EAAY;AAC3B,IAAA,OAAO,CAAA,CAAE,IAAA,KAAS,OAAA,GAAU,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA;AAAA,EACzD;AAAA;AAAA,EAGA,GAAA,CAAI,KAAA,EAAe,IAAA,EAAc,KAAA,EAA0B;AACzD,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,KAAK,CAAA,CAAA,CAAG,CAAA;AACnD,IAAA,MAAM,CAAA,GAAI,KAAK,WAAA,EAAY;AAC3B,IAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,CAAA,EAAG,CAAC,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,EAAI,IAAI,CAAA,cAAA,CAAgB,CAAA;AAC5E,IAAA,IAAI,EAAE,IAAA,KAAS,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,GAAI,KAAA;AAAA,SAC9B,CAAA,CAAE,QAAA,CAAS,GAAA,CAAK,CAAA,EAAG,KAAK,CAAA;AAAA,EAC/B;AAAA,EAEQ,QAAA,CAAS,GAAU,IAAA,EAAuB;AAChD,IAAA,IAAI,CAAA,CAAE,SAAS,OAAA,EAAS,OAAO,EAAE,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAG,QAAA,IAAY,IAAA;AAC9D,IAAA,IAAI,CAAC,CAAA,CAAE,QAAA,CAAS,GAAA,EAAK,OAAO,KAAA;AAC5B,IAAA,OAAO,EAAE,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAG,YAAY,CAAA,CAAE,aAAA;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,IAAA,EAA6C;AACzD,IAAA,MAAM,SAAgC,EAAC;AACvC,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ;AACpC,MAAA,MAAA,CAAO,KAAK,CAAA,GAAI,CAAA,CAAE,SAAS,OAAA,GAAU,CAAA,CAAE,MAAM,CAAA,CAAE,QAAA;AAAA,IACjD;AACA,IAAA,OAAO,EAAE,QAAQ,IAAA,EAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,GAA6B;AAC3B,IAAA,MAAM,UAAA,uBAAiB,GAAA,EAAwE;AAC/F,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ;AACpC,MAAA,IAAI,CAAA,CAAE,KAAA,CAAM,IAAA,KAAS,CAAA,EAAG;AACxB,MAAA,MAAM,CAAA,uBAAQ,GAAA,EAA2D;AACzE,MAAA,KAAA,MAAW,CAAC,IAAA,EAAM,CAAC,CAAA,IAAK,CAAA,CAAE,OAAO,CAAA,CAAE,GAAA,CAAI,IAAA,EAAM,EAAE,MAAM,CAAA,CAAE,IAAA,EAAM,UAAA,EAAY,CAAA,CAAE,QAAQ,CAAA;AACnF,MAAA,UAAA,CAAW,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,IACzB;AACA,IAAA,OAAO,EAAE,UAAA,EAAW;AAAA,EACtB;AAAA;AAAA,EAGA,IAAA,GAAoD;AAClD,IAAA,MAAM,MAAmD,EAAC;AAC1D,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,IAAA,CAAK,QAAQ,IAAI,CAAA,CAAE,IAAA,KAAS,OAAA,MAAa,KAAK,CAAA,GAAI,EAAE,GAAG,EAAE,GAAA,EAAI;AACtF,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA,EAGA,KAAK,IAAA,EAAyD;AAC5D,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,IAAI,KAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,EAAG;AAChD,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,MAAA,IAAI,GAAG,IAAA,KAAS,OAAA,SAAgB,MAAA,CAAO,CAAA,CAAE,KAAK,IAAI,CAAA;AAAA,IACpD;AAAA,EACF;AAAA,EAEQ,WAAW,KAAA,EAAqB;AACtC,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA,QAAS,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,uBAAA,CAAyB,CAAA;AAAA,EACvF;AACF;AAEA,SAAS,WAAW,CAAA,EAAkC;AACpD,EAAA,IAAI,CAAA,CAAE,OAAA,KAAY,MAAA,EAAW,OAAO,CAAA,CAAE,OAAA;AACtC,EAAA,QAAQ,EAAE,IAAA;AAAM,IACd,KAAK,SAAA;AAAW,MAAA,OAAO,KAAA;AAAA,IACvB,KAAK,QAAA;AAAU,MAAA,OAAO,CAAA;AAAA,IACtB,KAAK,QAAA;AAAU,MAAA,OAAO,EAAA;AAAA,IACtB,KAAK,MAAA;AAAQ,MAAA,OAAO,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK,EAAA;AAAA,IACrC,KAAK,OAAA;AAAS,MAAA,OAAO,EAAC;AAAA;AAE1B","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"]}
@@ -0,0 +1,90 @@
1
+ import { PropertyType, ScalarValue, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
2
+ export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
3
+
4
+ /**
5
+ * A property declaration. `default` is used by an *owned* scope to seed its bag
6
+ * (foreign scopes ignore it - the host owns the value). `writable: false` makes
7
+ * a property read-only; default is read/write. (`type`/`values` feed validation.)
8
+ */
9
+ interface ScopeDeclaration {
10
+ name: string;
11
+ type: PropertyType;
12
+ values?: string[];
13
+ default?: ScalarValue;
14
+ writable?: boolean;
15
+ }
16
+ /** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */
17
+ interface ScopeSpec {
18
+ token: string;
19
+ /** Scope-level read/write default for its declarations (default true). */
20
+ writable?: boolean;
21
+ /** Property declarations; omit for an opaque scope (any name, unchecked). */
22
+ declarations?: ScopeDeclaration[];
23
+ }
24
+ /**
25
+ * The interop format an owner (Storylet Studio, a host game) exports so another
26
+ * engine can validate references into its scopes. Carried under the well-known
27
+ * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).
28
+ */
29
+ interface ScopeRegistrySpec {
30
+ version: number;
31
+ scopes: ScopeSpec[];
32
+ }
33
+ /** The spec versions this build understands. */
34
+ declare const SUPPORTED_SPEC_VERSIONS: readonly [1];
35
+ /**
36
+ * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed
37
+ * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).
38
+ * Returns null when the key is absent (so callers can probe arbitrary files);
39
+ * throws on a malformed or unsupported-version spec.
40
+ */
41
+ declare function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null;
42
+ declare class ScopeRegistry {
43
+ private readonly scopes;
44
+ /**
45
+ * Register a scope this registry **owns and stores**. Its bag is seeded from
46
+ * each declaration's `default` (or a type default). Owned scopes are
47
+ * type-checked (declarations) and serialized by `save`/`load`.
48
+ */
49
+ defineOwned(token: string, declarations: ScopeDeclaration[]): this;
50
+ /**
51
+ * Re-initialise an existing **owned** scope's bag from new declarations,
52
+ * clearing its current values. For scope-local state that resets on a context
53
+ * change (e.g. entering a new scene / site / deck) without disturbing other
54
+ * scopes. Mutates the bag in place, so an `EvalContext` already built from this
55
+ * registry stays valid.
56
+ */
57
+ reseedOwned(token: string, declarations: ScopeDeclaration[]): this;
58
+ /**
59
+ * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The
60
+ * values live in the host/other engine and are never stored or saved here.
61
+ * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used
62
+ * only for validation; omit them for an opaque scope.
63
+ */
64
+ defineForeign(token: string, resolver: ScopeResolver, declarations?: ScopeDeclaration[], scopeWritable?: boolean): this;
65
+ has(token: string): boolean;
66
+ /** Read a property; undefined if the scope or property is not present. */
67
+ get(scope: string, name: string): ScalarValue | undefined;
68
+ /** Write a property. Throws on an unknown or read-only scope/property. */
69
+ set(scope: string, name: string, value: ScalarValue): void;
70
+ private writable;
71
+ /**
72
+ * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
73
+ * bags, foreign scopes as their resolvers. `host` carries dialect-function
74
+ * callbacks (PRNG, tag lookups) and is passed through untouched.
75
+ */
76
+ toEvalContext(host?: Record<string, unknown>): EvalContext;
77
+ /**
78
+ * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
79
+ * declarations are **omitted** (opaque - references into them are not flagged);
80
+ * declared scopes contribute their property types for validation.
81
+ */
82
+ toSchema(): ExpressionSchema;
83
+ /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
84
+ save(): Record<string, Record<string, ScalarValue>>;
85
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
86
+ load(blob: Record<string, Record<string, ScalarValue>>): void;
87
+ private assertFree;
88
+ }
89
+
90
+ export { SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
@@ -0,0 +1,90 @@
1
+ import { PropertyType, ScalarValue, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
2
+ export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
3
+
4
+ /**
5
+ * A property declaration. `default` is used by an *owned* scope to seed its bag
6
+ * (foreign scopes ignore it - the host owns the value). `writable: false` makes
7
+ * a property read-only; default is read/write. (`type`/`values` feed validation.)
8
+ */
9
+ interface ScopeDeclaration {
10
+ name: string;
11
+ type: PropertyType;
12
+ values?: string[];
13
+ default?: ScalarValue;
14
+ writable?: boolean;
15
+ }
16
+ /** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */
17
+ interface ScopeSpec {
18
+ token: string;
19
+ /** Scope-level read/write default for its declarations (default true). */
20
+ writable?: boolean;
21
+ /** Property declarations; omit for an opaque scope (any name, unchecked). */
22
+ declarations?: ScopeDeclaration[];
23
+ }
24
+ /**
25
+ * The interop format an owner (Storylet Studio, a host game) exports so another
26
+ * engine can validate references into its scopes. Carried under the well-known
27
+ * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).
28
+ */
29
+ interface ScopeRegistrySpec {
30
+ version: number;
31
+ scopes: ScopeSpec[];
32
+ }
33
+ /** The spec versions this build understands. */
34
+ declare const SUPPORTED_SPEC_VERSIONS: readonly [1];
35
+ /**
36
+ * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed
37
+ * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).
38
+ * Returns null when the key is absent (so callers can probe arbitrary files);
39
+ * throws on a malformed or unsupported-version spec.
40
+ */
41
+ declare function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null;
42
+ declare class ScopeRegistry {
43
+ private readonly scopes;
44
+ /**
45
+ * Register a scope this registry **owns and stores**. Its bag is seeded from
46
+ * each declaration's `default` (or a type default). Owned scopes are
47
+ * type-checked (declarations) and serialized by `save`/`load`.
48
+ */
49
+ defineOwned(token: string, declarations: ScopeDeclaration[]): this;
50
+ /**
51
+ * Re-initialise an existing **owned** scope's bag from new declarations,
52
+ * clearing its current values. For scope-local state that resets on a context
53
+ * change (e.g. entering a new scene / site / deck) without disturbing other
54
+ * scopes. Mutates the bag in place, so an `EvalContext` already built from this
55
+ * registry stays valid.
56
+ */
57
+ reseedOwned(token: string, declarations: ScopeDeclaration[]): this;
58
+ /**
59
+ * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The
60
+ * values live in the host/other engine and are never stored or saved here.
61
+ * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used
62
+ * only for validation; omit them for an opaque scope.
63
+ */
64
+ defineForeign(token: string, resolver: ScopeResolver, declarations?: ScopeDeclaration[], scopeWritable?: boolean): this;
65
+ has(token: string): boolean;
66
+ /** Read a property; undefined if the scope or property is not present. */
67
+ get(scope: string, name: string): ScalarValue | undefined;
68
+ /** Write a property. Throws on an unknown or read-only scope/property. */
69
+ set(scope: string, name: string, value: ScalarValue): void;
70
+ private writable;
71
+ /**
72
+ * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
73
+ * bags, foreign scopes as their resolvers. `host` carries dialect-function
74
+ * callbacks (PRNG, tag lookups) and is passed through untouched.
75
+ */
76
+ toEvalContext(host?: Record<string, unknown>): EvalContext;
77
+ /**
78
+ * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
79
+ * declarations are **omitted** (opaque - references into them are not flagged);
80
+ * declared scopes contribute their property types for validation.
81
+ */
82
+ toSchema(): ExpressionSchema;
83
+ /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
84
+ save(): Record<string, Record<string, ScalarValue>>;
85
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
86
+ load(blob: Record<string, Record<string, ScalarValue>>): void;
87
+ private assertFree;
88
+ }
89
+
90
+ export { SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, readScopeRegistrySpec };
package/dist/index.js ADDED
@@ -0,0 +1,158 @@
1
+ // src/index.ts
2
+ var SUPPORTED_SPEC_VERSIONS = [1];
3
+ function readScopeRegistrySpec(source) {
4
+ if (!source || typeof source !== "object") return null;
5
+ const raw = source.scopeRegistrySpec;
6
+ if (raw === void 0) return null;
7
+ if (typeof raw !== "object" || raw === null) throw new Error("scopeRegistrySpec must be an object");
8
+ const spec = raw;
9
+ if (typeof spec.version !== "number") throw new Error("scopeRegistrySpec.version must be a number");
10
+ if (!SUPPORTED_SPEC_VERSIONS.includes(spec.version)) {
11
+ throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(", ")})`);
12
+ }
13
+ if (!Array.isArray(spec.scopes)) throw new Error("scopeRegistrySpec.scopes must be an array");
14
+ for (const s of spec.scopes) {
15
+ if (!s || typeof s !== "object" || typeof s.token !== "string") {
16
+ throw new Error("each scopeRegistrySpec scope needs a string token");
17
+ }
18
+ }
19
+ return spec;
20
+ }
21
+ var ScopeRegistry = class {
22
+ scopes = /* @__PURE__ */ new Map();
23
+ /**
24
+ * Register a scope this registry **owns and stores**. Its bag is seeded from
25
+ * each declaration's `default` (or a type default). Owned scopes are
26
+ * type-checked (declarations) and serialized by `save`/`load`.
27
+ */
28
+ defineOwned(token, declarations) {
29
+ 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 });
38
+ return this;
39
+ }
40
+ /**
41
+ * Re-initialise an existing **owned** scope's bag from new declarations,
42
+ * clearing its current values. For scope-local state that resets on a context
43
+ * change (e.g. entering a new scene / site / deck) without disturbing other
44
+ * scopes. Mutates the bag in place, so an `EvalContext` already built from this
45
+ * registry stays valid.
46
+ */
47
+ 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
+ }
57
+ return this;
58
+ }
59
+ /**
60
+ * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The
61
+ * values live in the host/other engine and are never stored or saved here.
62
+ * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used
63
+ * only for validation; omit them for an opaque scope.
64
+ */
65
+ defineForeign(token, resolver, declarations = [], scopeWritable = true) {
66
+ this.assertFree(token);
67
+ const decls = /* @__PURE__ */ new Map();
68
+ for (const d of declarations) decls.set(d.name.toLowerCase(), d);
69
+ this.scopes.set(token, { kind: "foreign", resolver, decls, scopeWritable });
70
+ return this;
71
+ }
72
+ has(token) {
73
+ return this.scopes.has(token);
74
+ }
75
+ /** Read a property; undefined if the scope or property is not present. */
76
+ get(scope, name) {
77
+ const e = this.scopes.get(scope);
78
+ if (!e) return void 0;
79
+ const n = name.toLowerCase();
80
+ return e.kind === "owned" ? e.bag[n] : e.resolver.get(n);
81
+ }
82
+ /** Write a property. Throws on an unknown or read-only scope/property. */
83
+ set(scope, name, value) {
84
+ const e = this.scopes.get(scope);
85
+ if (!e) throw new Error(`unknown scope '@${scope}'`);
86
+ 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);
90
+ }
91
+ writable(e, name) {
92
+ if (e.kind === "owned") return e.decls.get(name)?.writable ?? true;
93
+ if (!e.resolver.set) return false;
94
+ return e.decls.get(name)?.writable ?? e.scopeWritable;
95
+ }
96
+ /**
97
+ * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static
98
+ * bags, foreign scopes as their resolvers. `host` carries dialect-function
99
+ * callbacks (PRNG, tag lookups) and is passed through untouched.
100
+ */
101
+ toEvalContext(host) {
102
+ const scopes = {};
103
+ for (const [token, e] of this.scopes) {
104
+ scopes[token] = e.kind === "owned" ? e.bag : e.resolver;
105
+ }
106
+ return { scopes, host };
107
+ }
108
+ /**
109
+ * Build the `ExpressionSchema` expr's validator consumes. Scopes with no
110
+ * declarations are **omitted** (opaque - references into them are not flagged);
111
+ * declared scopes contribute their property types for validation.
112
+ */
113
+ toSchema() {
114
+ const properties = /* @__PURE__ */ new Map();
115
+ for (const [token, e] of this.scopes) {
116
+ if (e.decls.size === 0) continue;
117
+ const m = /* @__PURE__ */ new Map();
118
+ for (const [name, d] of e.decls) m.set(name, { type: d.type, enumValues: d.values });
119
+ properties.set(token, m);
120
+ }
121
+ return { properties };
122
+ }
123
+ /** Serialize **owned** scopes only (foreign scopes are host-owned, host-saved). */
124
+ save() {
125
+ const out = {};
126
+ for (const [token, e] of this.scopes) if (e.kind === "owned") out[token] = { ...e.bag };
127
+ return out;
128
+ }
129
+ /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes are ignored. */
130
+ load(blob) {
131
+ for (const [token, vals] of Object.entries(blob)) {
132
+ const e = this.scopes.get(token);
133
+ if (e?.kind === "owned") Object.assign(e.bag, vals);
134
+ }
135
+ }
136
+ assertFree(token) {
137
+ if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);
138
+ }
139
+ };
140
+ function defaultFor(d) {
141
+ if (d.default !== void 0) return d.default;
142
+ switch (d.type) {
143
+ case "boolean":
144
+ return false;
145
+ case "number":
146
+ return 0;
147
+ case "string":
148
+ return "";
149
+ case "enum":
150
+ return d.values?.[0] ?? "";
151
+ case "flags":
152
+ return [];
153
+ }
154
+ }
155
+
156
+ export { SUPPORTED_SPEC_VERSIONS, ScopeRegistry, readScopeRegistrySpec };
157
+ //# sourceMappingURL=index.js.map
158
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@wildwinter/scoperegistry",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "license": "MIT",
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"],
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "files": ["dist", "README.md", "LICENSE"],
21
+ "sideEffects": false,
22
+ "publishConfig": {
23
+ "registry": "https://registry.npmjs.org",
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "typecheck": "tsc --noEmit",
31
+ "prepublishOnly": "npm run build && npm test"
32
+ },
33
+ "dependencies": {
34
+ "@wildwinter/expr": "^0.3.0"
35
+ }
36
+ }