@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 +21 -0
- package/README.md +42 -0
- package/dist/index.cjs +162 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +90 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
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":"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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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|