@formbar/arbiter 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,63 @@
2
2
 
3
3
  Formbar plugin bridge for Arbitre production rules. It syncs form data and `$ui` state into an Arbitre session, fires rules during Formbar evaluation, and applies resulting writes back to form state.
4
4
 
5
- ## Install
5
+ ## Opt-in shared pure expressions (#90)
6
+
7
+ ```ts
8
+ import { createSession } from "@arbitre/core";
9
+ import { registerExpressionThenOperator } from "@formbar/arbiter";
10
+
11
+ const handlers = new Map();
12
+ const thenOperators = {
13
+ register: (name, handler) => handlers.set(name, handler),
14
+ get: name => handlers.get(name),
15
+ has: name => handlers.has(name),
16
+ };
17
+ const bridge = registerExpressionThenOperator(thenOperators, {
18
+ programs: new Map([["adjusted", { kind: "op", op: "add", args: [
19
+ { kind: "ref", ref: { namespace: "data", segments: ["nativeTotal"] } },
20
+ { kind: "literal", value: 1 },
21
+ ] }]]),
22
+ });
23
+ const session = createSession({
24
+ thenOperators,
25
+ rules: [{ name: "calculate", when: { ready: true }, then: [
26
+ { $set: { nativeTotal: { $multiply: ["$quantity", "$unitPrice"] } } },
27
+ { $formbarValue: { adjusted: "adjusted" } },
28
+ ] }],
29
+ });
30
+ // Optional form integration: createArbiterPlugin({ session }).
31
+ session.assert("quantity", 2);
32
+ session.assert("unitPrice", 12);
33
+ session.assert("ready", true);
34
+ session.fire(); // nativeTotal 24; adjusted 25 from the actual current RHS scope
35
+ session.dispose();
36
+ bridge.dispose();
37
+ ```
38
+
39
+ The bridge compiles all host-registered IDs before mutating Arbitre's public
40
+ `ThenOperatorRegistry`. A host may select one immutable Kuery `ExpressionProfile`. Data maps to
41
+ non-reserved current scope keys, UI to `$ui`, and explicit external roots can be selected with
42
+ `namespaces: scope => ({ pricing: scope.$pricing })`; configure the corresponding
43
+ native session namespace too. Optional authorization applies at every read.
44
+
45
+ Failures throw fixed-code `ExpressionError`s, not scope values. Native session
46
+ strict/lenient error behavior remains authoritative. Registration is opt-in and
47
+ not globally installed. Every entry evaluates against the same incoming stage scope
48
+ before any tracked write occurs, so failed stages write nothing. Prior stages are
49
+ visible, same-stage writes are not inputs, and separate stages can chain. Writes
50
+ remain visible to Arbitre changes and TMS. The host owns bridge/session disposal;
51
+ disposed retained handlers fail because Arbitre 0.3 has no unregister operation.
52
+
53
+ This does **not** replace the native `when` compiler, its dependency indexes,
54
+ refiring/TMS semantics or stored-computation scheduling. A continuously true
55
+ native condition is not promised to rerun after every RHS dependency edit. The
56
+ shared arithmetic profile is strictly finite/no-coercion, not a claim of matching
57
+ native Arbitre null/coercion behavior. Broader policy normalization remains #70;
58
+ stored computations/effects remain #68. See the
59
+ [ADR](../expressions/docs/adr/0001-expression-service-and-reactive-props.md).
60
+
61
+ ## Package installation
6
62
 
7
63
  ```bash
8
64
  bun add @formbar/arbiter @formbar/core @arbitre/core kuery
@@ -36,6 +92,9 @@ console.log(form.getState().uiState); // { showDiscount: true }
36
92
  form.dispose();
37
93
  ```
38
94
 
95
+ Pass rules through `createArbiterPlugin` in the `plugins` option. The former top-level `arbiterRules` form option is
96
+ not supported; non-production core builds warn with this migration path when they encounter it.
97
+
39
98
  ## When to use this package
40
99
 
41
100
  - Use `@formbar/arbiter` when visibility, requiredness, computed values, or other form behavior should be governed by Arbitre production rules.
@@ -45,4 +104,4 @@ form.dispose();
45
104
  ## Dependencies
46
105
 
47
106
  - Depends on `@formbar/core`.
48
- - Peer dependencies: `@arbitre/core >=0.1.0` and `kuery >=2.0.0`.
107
+ - Peer dependencies: `@arbitre/core ^0.3.0` and `kuery ^2.1.0`.
package/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var core = require('@arbitre/core');
4
4
  var kuery = require('kuery');
5
+ var expressions = require('@formbar/expressions');
5
6
 
6
7
  // src/arbiter-plugin.ts
7
8
 
@@ -71,13 +72,100 @@ function createArbiterPlugin(options) {
71
72
  function evaluateExpression(expr, context) {
72
73
  return kuery.evaluate(expr, context);
73
74
  }
75
+ var FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
76
+ function providers(roots) {
77
+ return Object.fromEntries(
78
+ Object.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {
79
+ } }])
80
+ );
81
+ }
82
+ function plainRootMap(value) {
83
+ if (value === null || typeof value !== "object") return false;
84
+ const prototype = Object.getPrototypeOf(value);
85
+ if (prototype !== Object.prototype && prototype !== null) return false;
86
+ return Reflect.ownKeys(value).every((key) => {
87
+ if (typeof key !== "string") return false;
88
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
89
+ return descriptor !== void 0 && "value" in descriptor && descriptor.enumerable;
90
+ });
91
+ }
92
+ function scopeProviders(options, scope) {
93
+ try {
94
+ const selected = options.namespaces ? expressions.synchronousValue(options.namespaces(scope)) : {};
95
+ if (!plainRootMap(selected)) throw new expressions.ExpressionError("adapter");
96
+ const data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith("$")));
97
+ return providers({ ...selected, data, ui: scope.$ui });
98
+ } catch {
99
+ throw new expressions.ExpressionError("adapter");
100
+ }
101
+ }
102
+ function compilePrograms(options) {
103
+ const service = expressions.createExpressionService({
104
+ ...options.profile ? { profile: options.profile } : {},
105
+ ...options.authorize ? { authorize: options.authorize } : {}
106
+ });
107
+ const programs = /* @__PURE__ */ new Map();
108
+ try {
109
+ for (const [id, expression] of options.programs) {
110
+ if (typeof id !== "string") throw new expressions.ExpressionError("invalid-input");
111
+ const compiled = service.compile(expression);
112
+ if (!compiled.ok) throw new expressions.ExpressionError(compiled.diagnostics[0].code);
113
+ programs.set(id, compiled.value);
114
+ }
115
+ return { service, programs };
116
+ } catch (error) {
117
+ service.dispose();
118
+ programs.clear();
119
+ throw error;
120
+ }
121
+ }
122
+ function registerExpressionThenOperator(registry, options) {
123
+ if (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new expressions.ExpressionError("invalid-input");
124
+ const { service, programs } = compilePrograms(options);
125
+ let disposed = false;
126
+ const handler = (entries, scope, write) => {
127
+ if (disposed) throw new expressions.ExpressionError("disposed");
128
+ const selected = [];
129
+ for (const [path, id] of entries) {
130
+ if (typeof id !== "string") throw new expressions.ExpressionError("invalid-input");
131
+ const program = programs.get(id);
132
+ if (!program) throw new expressions.ExpressionError("unknown-program");
133
+ selected.push([path, program]);
134
+ }
135
+ const context = scopeProviders(options, scope);
136
+ const values = selected.map(([, program]) => {
137
+ const result = service.evaluate(program, context);
138
+ if (!result.ok) throw new expressions.ExpressionError(result.diagnostics[0].code);
139
+ return result.value;
140
+ });
141
+ for (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);
142
+ };
143
+ try {
144
+ registry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);
145
+ } catch {
146
+ service.dispose();
147
+ programs.clear();
148
+ throw new expressions.ExpressionError("adapter");
149
+ }
150
+ return {
151
+ handler,
152
+ dispose: () => {
153
+ if (disposed) return;
154
+ disposed = true;
155
+ service.dispose();
156
+ programs.clear();
157
+ }
158
+ };
159
+ }
74
160
 
75
161
  Object.defineProperty(exports, "assertSafeSegment", {
76
162
  enumerable: true,
77
163
  get: function () { return kuery.assertSafeSegment; }
78
164
  });
165
+ exports.FORMBAR_VALUE_THEN_OPERATOR = FORMBAR_VALUE_THEN_OPERATOR;
79
166
  exports.createArbiterPlugin = createArbiterPlugin;
80
167
  exports.evaluateExpression = evaluateExpression;
81
168
  exports.isArbiterInternalPath = isArbiterInternalPath;
169
+ exports.registerExpressionThenOperator = registerExpressionThenOperator;
82
170
  //# sourceMappingURL=index.cjs.map
83
171
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal-paths.ts","../src/arbiter-plugin.ts","../src/expression-utils.ts"],"names":["createSession","evaluate"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Arbiter-internal paths start with $ and are not user-facing form data.\n * Filter them out of write intents.\n */\nexport function isArbiterInternalPath(path: string): boolean {\n\treturn path.startsWith(\"$\") && !path.startsWith(\"$ui.\");\n}\n","import { createSession } from \"@arbitre/core\";\nimport type { FiringResult, ProductionRule, RuleSession } from \"@arbitre/core\";\nimport type {\n\tFormPlugin,\n\tPluginEvaluateContext,\n\tPluginEvaluateResult,\n\tPluginFieldMeta,\n\tPluginWrite,\n} from \"@formbar/core\";\nimport { isArbiterInternalPath } from \"./internal-paths.js\";\n\nexport interface ArbiterPluginOptions {\n\t/** Provide raw rules — a session will be created internally. */\n\treadonly rules?: readonly ProductionRule[];\n\t/** Provide a pre-configured session instead of raw rules. */\n\treadonly session?: RuleSession;\n}\n\n/**\n * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.\n * Syncs form data into the rule session, fires rules, and converts results\n * into PluginWrite[] and PluginFieldMeta records.\n */\nexport function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {\n\tconst { rules, session: externalSession } = options;\n\n\tlet session: RuleSession;\n\tlet ownsSession: boolean;\n\n\tif (externalSession) {\n\t\tsession = externalSession;\n\t\townsSession = false;\n\t} else if (rules) {\n\t\tsession = createSession({ rules: rules as ProductionRule[] });\n\t\townsSession = true;\n\t} else {\n\t\tthrow new Error(\"createArbiterPlugin requires either `rules` or `session`\");\n\t}\n\n\treturn {\n\t\tid: \"arbiter\",\n\n\t\tevaluate(ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {\n\t\t\t// Prevent re-entry from own writes\n\t\t\tif (ctx.origin.startsWith(\"plugin:arbiter\")) return;\n\n\t\t\t// Short-circuit when nothing relevant changed\n\t\t\tif (!ctx.change.dataChanged && !ctx.change.uiChanged) return;\n\n\t\t\t// Sync form data fields into the session\n\t\t\tconst data = ctx.data as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(data)) {\n\t\t\t\tsession.assert(key, data[key]);\n\t\t\t}\n\n\t\t\t// Sync $ui.* state\n\t\t\tconst uiState = ctx.uiState as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(uiState)) {\n\t\t\t\tsession.assert(`$ui.${key}`, uiState[key]);\n\t\t\t}\n\n\t\t\t// Fire rules\n\t\t\tconst result: FiringResult = session.fire();\n\n\t\t\t// Convert changes to PluginWrite[], filtering internal paths\n\t\t\tconst writes: PluginWrite[] = [];\n\t\t\tfor (const change of result.changes) {\n\t\t\t\tif (isArbiterInternalPath(change.path)) continue;\n\t\t\t\twrites.push({\n\t\t\t\t\tpath: change.path,\n\t\t\t\t\tvalue: change.newValue,\n\t\t\t\t\tmode: \"set\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Derive field meta from session state\n\t\t\tconst fieldMeta: Record<string, PluginFieldMeta> = {};\n\t\t\tconst state = session.getState();\n\t\t\tfor (const [path, value] of Object.entries(state)) {\n\t\t\t\tif (!path.startsWith(\"$meta.\")) continue;\n\t\t\t\t// Convention: $meta.<fieldPath> holds { visible, disabled, required, readOnly, label }\n\t\t\t\tconst fieldPath = path.slice(\"$meta.\".length);\n\t\t\t\tif (typeof value === \"object\" && value !== null) {\n\t\t\t\t\tfieldMeta[fieldPath] = value as PluginFieldMeta;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\twrites: writes.length > 0 ? writes : undefined,\n\t\t\t\tfieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : undefined,\n\t\t\t};\n\t\t},\n\n\t\tonDispose() {\n\t\t\tif (ownsSession) {\n\t\t\t\tsession.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n","import { assertSafeSegment, evaluate } from \"kuery\";\nimport type { ExprNode } from \"kuery\";\n\n/**\n * Evaluate a kuery expression against a data context.\n * This is a convenience re-export for consumers using expressions with arbiter rules.\n */\nexport function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown {\n\treturn evaluate(expr, context);\n}\n\nexport { assertSafeSegment };\nexport type { ExprNode };\n"]}
1
+ {"version":3,"sources":["../src/internal-paths.ts","../src/arbiter-plugin.ts","../src/expression-utils.ts","../src/expression-then-operator.ts"],"names":["createSession","evaluate","synchronousValue","ExpressionError","createExpressionService"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Arbiter-internal paths start with $ and are not user-facing form data.\n * Filter them out of write intents.\n */\nexport function isArbiterInternalPath(path: string): boolean {\n\treturn path.startsWith(\"$\") && !path.startsWith(\"$ui.\");\n}\n","import { createSession } from \"@arbitre/core\";\nimport type { FiringResult, ProductionRule, RuleSession } from \"@arbitre/core\";\nimport type {\n\tFormPlugin,\n\tPluginEvaluateContext,\n\tPluginEvaluateResult,\n\tPluginFieldMeta,\n\tPluginWrite,\n} from \"@formbar/core\";\nimport { isArbiterInternalPath } from \"./internal-paths.js\";\n\nexport interface ArbiterPluginOptions {\n\t/** Provide raw rules — a session will be created internally. */\n\treadonly rules?: readonly ProductionRule[];\n\t/** Provide a pre-configured session instead of raw rules. */\n\treadonly session?: RuleSession;\n}\n\n/**\n * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.\n * Syncs form data into the rule session, fires rules, and converts results\n * into PluginWrite[] and PluginFieldMeta records.\n */\nexport function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {\n\tconst { rules, session: externalSession } = options;\n\n\tlet session: RuleSession;\n\tlet ownsSession: boolean;\n\n\tif (externalSession) {\n\t\tsession = externalSession;\n\t\townsSession = false;\n\t} else if (rules) {\n\t\tsession = createSession({ rules: rules as ProductionRule[] });\n\t\townsSession = true;\n\t} else {\n\t\tthrow new Error(\"createArbiterPlugin requires either `rules` or `session`\");\n\t}\n\n\treturn {\n\t\tid: \"arbiter\",\n\n\t\tevaluate(ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {\n\t\t\t// Prevent re-entry from own writes\n\t\t\tif (ctx.origin.startsWith(\"plugin:arbiter\")) return;\n\n\t\t\t// Short-circuit when nothing relevant changed\n\t\t\tif (!ctx.change.dataChanged && !ctx.change.uiChanged) return;\n\n\t\t\t// Sync form data fields into the session\n\t\t\tconst data = ctx.data as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(data)) {\n\t\t\t\tsession.assert(key, data[key]);\n\t\t\t}\n\n\t\t\t// Sync $ui.* state\n\t\t\tconst uiState = ctx.uiState as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(uiState)) {\n\t\t\t\tsession.assert(`$ui.${key}`, uiState[key]);\n\t\t\t}\n\n\t\t\t// Fire rules\n\t\t\tconst result: FiringResult = session.fire();\n\n\t\t\t// Convert changes to PluginWrite[], filtering internal paths\n\t\t\tconst writes: PluginWrite[] = [];\n\t\t\tfor (const change of result.changes) {\n\t\t\t\tif (isArbiterInternalPath(change.path)) continue;\n\t\t\t\twrites.push({\n\t\t\t\t\tpath: change.path,\n\t\t\t\t\tvalue: change.newValue,\n\t\t\t\t\tmode: \"set\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Derive field meta from session state\n\t\t\tconst fieldMeta: Record<string, PluginFieldMeta> = {};\n\t\t\tconst state = session.getState();\n\t\t\tfor (const [path, value] of Object.entries(state)) {\n\t\t\t\tif (!path.startsWith(\"$meta.\")) continue;\n\t\t\t\t// Convention: $meta.<fieldPath> holds { visible, disabled, required, readOnly, label }\n\t\t\t\tconst fieldPath = path.slice(\"$meta.\".length);\n\t\t\t\tif (typeof value === \"object\" && value !== null) {\n\t\t\t\t\tfieldMeta[fieldPath] = value as PluginFieldMeta;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\twrites: writes.length > 0 ? writes : undefined,\n\t\t\t\tfieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : undefined,\n\t\t\t};\n\t\t},\n\n\t\tonDispose() {\n\t\t\tif (ownsSession) {\n\t\t\t\tsession.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n","import { assertSafeSegment, evaluate } from \"kuery\";\nimport type { ExprNode } from \"kuery\";\n\n/**\n * Evaluate a kuery expression against a data context.\n * This is a convenience re-export for consumers using expressions with arbiter rules.\n */\nexport function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown {\n\treturn evaluate(expr, context);\n}\n\nexport { assertSafeSegment };\nexport type { ExprNode };\n","import type { ThenOperatorHandler, ThenOperatorRegistry } from \"@arbitre/core\";\nimport { ExpressionError, createExpressionService, synchronousValue } from \"@formbar/expressions\";\nimport type { Authorization, Expression, ExpressionProfile, NamespaceProvider, Program } from \"@formbar/expressions\";\n\nexport const FORMBAR_VALUE_THEN_OPERATOR = \"$formbarValue\";\n\nexport interface ExpressionThenOperatorOptions {\n\treadonly programs: ReadonlyMap<string, Expression>;\n\treadonly profile?: ExpressionProfile;\n\treadonly authorize?: Authorization;\n\t/** Select trusted external roots from the current incoming stage scope. */\n\treadonly namespaces?: (scope: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;\n}\n\nexport interface RegisteredExpressionThenOperator {\n\treadonly handler: ThenOperatorHandler;\n\tdispose(): void;\n}\n\nfunction providers(roots: Readonly<Record<string, unknown>>): Record<string, NamespaceProvider> {\n\treturn Object.fromEntries(\n\t\tObject.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {} }]),\n\t);\n}\n\nfunction plainRootMap(value: unknown): value is Readonly<Record<string, unknown>> {\n\tif (value === null || typeof value !== \"object\") return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\tif (prototype !== Object.prototype && prototype !== null) return false;\n\treturn Reflect.ownKeys(value).every((key) => {\n\t\tif (typeof key !== \"string\") return false;\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, key);\n\t\treturn descriptor !== undefined && \"value\" in descriptor && descriptor.enumerable;\n\t});\n}\n\nfunction scopeProviders(\n\toptions: ExpressionThenOperatorOptions,\n\tscope: Readonly<Record<string, unknown>>,\n): Record<string, NamespaceProvider> {\n\ttry {\n\t\tconst selected = options.namespaces ? synchronousValue(options.namespaces(scope)) : {};\n\t\tif (!plainRootMap(selected)) throw new ExpressionError(\"adapter\");\n\t\tconst data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith(\"$\")));\n\t\treturn providers({ ...selected, data, ui: scope.$ui });\n\t} catch {\n\t\tthrow new ExpressionError(\"adapter\");\n\t}\n}\n\nfunction compilePrograms(options: ExpressionThenOperatorOptions) {\n\tconst service = createExpressionService({\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t\t...(options.authorize ? { authorize: options.authorize } : {}),\n\t});\n\tconst programs = new Map<string, Program>();\n\ttry {\n\t\tfor (const [id, expression] of options.programs) {\n\t\t\tif (typeof id !== \"string\") throw new ExpressionError(\"invalid-input\");\n\t\t\tconst compiled = service.compile(expression);\n\t\t\tif (!compiled.ok) throw new ExpressionError(compiled.diagnostics[0].code);\n\t\t\tprograms.set(id, compiled.value);\n\t\t}\n\t\treturn { service, programs };\n\t} catch (error) {\n\t\tservice.dispose();\n\t\tprograms.clear();\n\t\tthrow error;\n\t}\n}\n\n/** Registers Formbar's atomic expression stage in an Arbitre 0.3 public registry. */\nexport function registerExpressionThenOperator(\n\tregistry: ThenOperatorRegistry,\n\toptions: ExpressionThenOperatorOptions,\n): RegisteredExpressionThenOperator {\n\tif (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new ExpressionError(\"invalid-input\");\n\tconst { service, programs } = compilePrograms(options);\n\tlet disposed = false;\n\tconst handler: ThenOperatorHandler = (entries, scope, write) => {\n\t\tif (disposed) throw new ExpressionError(\"disposed\");\n\t\tconst selected: Array<readonly [string, Program]> = [];\n\t\tfor (const [path, id] of entries) {\n\t\t\tif (typeof id !== \"string\") throw new ExpressionError(\"invalid-input\");\n\t\t\tconst program = programs.get(id);\n\t\t\tif (!program) throw new ExpressionError(\"unknown-program\");\n\t\t\tselected.push([path, program]);\n\t\t}\n\t\tconst context = scopeProviders(options, scope);\n\t\tconst values = selected.map(([, program]) => {\n\t\t\tconst result = service.evaluate(program, context);\n\t\t\tif (!result.ok) throw new ExpressionError(result.diagnostics[0].code);\n\t\t\treturn result.value;\n\t\t});\n\t\tfor (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);\n\t};\n\ttry {\n\t\tregistry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);\n\t} catch {\n\t\tservice.dispose();\n\t\tprograms.clear();\n\t\tthrow new ExpressionError(\"adapter\");\n\t}\n\treturn {\n\t\thandler,\n\t\tdispose: () => {\n\t\t\tif (disposed) return;\n\t\t\tdisposed = true;\n\t\t\tservice.dispose();\n\t\t\tprograms.clear();\n\t\t},\n\t};\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
- import { ProductionRule, RuleSession } from '@arbitre/core';
1
+ import { ProductionRule, RuleSession, ThenOperatorHandler, ThenOperatorRegistry } from '@arbitre/core';
2
2
  import { FormPlugin } from '@formbar/core';
3
3
  import { ExprNode } from 'kuery';
4
4
  export { ExprNode, assertSafeSegment } from 'kuery';
5
+ import { Expression, ExpressionProfile, Authorization } from '@formbar/expressions';
5
6
 
6
7
  interface ArbiterPluginOptions {
7
8
  /** Provide raw rules — a session will be created internally. */
@@ -28,4 +29,19 @@ declare function evaluateExpression(expr: ExprNode, context: Record<string, unkn
28
29
  */
29
30
  declare function isArbiterInternalPath(path: string): boolean;
30
31
 
31
- export { type ArbiterPluginOptions, createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
32
+ declare const FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
33
+ interface ExpressionThenOperatorOptions {
34
+ readonly programs: ReadonlyMap<string, Expression>;
35
+ readonly profile?: ExpressionProfile;
36
+ readonly authorize?: Authorization;
37
+ /** Select trusted external roots from the current incoming stage scope. */
38
+ readonly namespaces?: (scope: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
39
+ }
40
+ interface RegisteredExpressionThenOperator {
41
+ readonly handler: ThenOperatorHandler;
42
+ dispose(): void;
43
+ }
44
+ /** Registers Formbar's atomic expression stage in an Arbitre 0.3 public registry. */
45
+ declare function registerExpressionThenOperator(registry: ThenOperatorRegistry, options: ExpressionThenOperatorOptions): RegisteredExpressionThenOperator;
46
+
47
+ export { type ArbiterPluginOptions, type ExpressionThenOperatorOptions, FORMBAR_VALUE_THEN_OPERATOR, type RegisteredExpressionThenOperator, createArbiterPlugin, evaluateExpression, isArbiterInternalPath, registerExpressionThenOperator };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { ProductionRule, RuleSession } from '@arbitre/core';
1
+ import { ProductionRule, RuleSession, ThenOperatorHandler, ThenOperatorRegistry } from '@arbitre/core';
2
2
  import { FormPlugin } from '@formbar/core';
3
3
  import { ExprNode } from 'kuery';
4
4
  export { ExprNode, assertSafeSegment } from 'kuery';
5
+ import { Expression, ExpressionProfile, Authorization } from '@formbar/expressions';
5
6
 
6
7
  interface ArbiterPluginOptions {
7
8
  /** Provide raw rules — a session will be created internally. */
@@ -28,4 +29,19 @@ declare function evaluateExpression(expr: ExprNode, context: Record<string, unkn
28
29
  */
29
30
  declare function isArbiterInternalPath(path: string): boolean;
30
31
 
31
- export { type ArbiterPluginOptions, createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
32
+ declare const FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
33
+ interface ExpressionThenOperatorOptions {
34
+ readonly programs: ReadonlyMap<string, Expression>;
35
+ readonly profile?: ExpressionProfile;
36
+ readonly authorize?: Authorization;
37
+ /** Select trusted external roots from the current incoming stage scope. */
38
+ readonly namespaces?: (scope: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
39
+ }
40
+ interface RegisteredExpressionThenOperator {
41
+ readonly handler: ThenOperatorHandler;
42
+ dispose(): void;
43
+ }
44
+ /** Registers Formbar's atomic expression stage in an Arbitre 0.3 public registry. */
45
+ declare function registerExpressionThenOperator(registry: ThenOperatorRegistry, options: ExpressionThenOperatorOptions): RegisteredExpressionThenOperator;
46
+
47
+ export { type ArbiterPluginOptions, type ExpressionThenOperatorOptions, FORMBAR_VALUE_THEN_OPERATOR, type RegisteredExpressionThenOperator, createArbiterPlugin, evaluateExpression, isArbiterInternalPath, registerExpressionThenOperator };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createSession } from '@arbitre/core';
2
2
  import { evaluate } from 'kuery';
3
3
  export { assertSafeSegment } from 'kuery';
4
+ import { ExpressionError, createExpressionService, synchronousValue } from '@formbar/expressions';
4
5
 
5
6
  // src/arbiter-plugin.ts
6
7
 
@@ -70,7 +71,92 @@ function createArbiterPlugin(options) {
70
71
  function evaluateExpression(expr, context) {
71
72
  return evaluate(expr, context);
72
73
  }
74
+ var FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
75
+ function providers(roots) {
76
+ return Object.fromEntries(
77
+ Object.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {
78
+ } }])
79
+ );
80
+ }
81
+ function plainRootMap(value) {
82
+ if (value === null || typeof value !== "object") return false;
83
+ const prototype = Object.getPrototypeOf(value);
84
+ if (prototype !== Object.prototype && prototype !== null) return false;
85
+ return Reflect.ownKeys(value).every((key) => {
86
+ if (typeof key !== "string") return false;
87
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
88
+ return descriptor !== void 0 && "value" in descriptor && descriptor.enumerable;
89
+ });
90
+ }
91
+ function scopeProviders(options, scope) {
92
+ try {
93
+ const selected = options.namespaces ? synchronousValue(options.namespaces(scope)) : {};
94
+ if (!plainRootMap(selected)) throw new ExpressionError("adapter");
95
+ const data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith("$")));
96
+ return providers({ ...selected, data, ui: scope.$ui });
97
+ } catch {
98
+ throw new ExpressionError("adapter");
99
+ }
100
+ }
101
+ function compilePrograms(options) {
102
+ const service = createExpressionService({
103
+ ...options.profile ? { profile: options.profile } : {},
104
+ ...options.authorize ? { authorize: options.authorize } : {}
105
+ });
106
+ const programs = /* @__PURE__ */ new Map();
107
+ try {
108
+ for (const [id, expression] of options.programs) {
109
+ if (typeof id !== "string") throw new ExpressionError("invalid-input");
110
+ const compiled = service.compile(expression);
111
+ if (!compiled.ok) throw new ExpressionError(compiled.diagnostics[0].code);
112
+ programs.set(id, compiled.value);
113
+ }
114
+ return { service, programs };
115
+ } catch (error) {
116
+ service.dispose();
117
+ programs.clear();
118
+ throw error;
119
+ }
120
+ }
121
+ function registerExpressionThenOperator(registry, options) {
122
+ if (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new ExpressionError("invalid-input");
123
+ const { service, programs } = compilePrograms(options);
124
+ let disposed = false;
125
+ const handler = (entries, scope, write) => {
126
+ if (disposed) throw new ExpressionError("disposed");
127
+ const selected = [];
128
+ for (const [path, id] of entries) {
129
+ if (typeof id !== "string") throw new ExpressionError("invalid-input");
130
+ const program = programs.get(id);
131
+ if (!program) throw new ExpressionError("unknown-program");
132
+ selected.push([path, program]);
133
+ }
134
+ const context = scopeProviders(options, scope);
135
+ const values = selected.map(([, program]) => {
136
+ const result = service.evaluate(program, context);
137
+ if (!result.ok) throw new ExpressionError(result.diagnostics[0].code);
138
+ return result.value;
139
+ });
140
+ for (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);
141
+ };
142
+ try {
143
+ registry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);
144
+ } catch {
145
+ service.dispose();
146
+ programs.clear();
147
+ throw new ExpressionError("adapter");
148
+ }
149
+ return {
150
+ handler,
151
+ dispose: () => {
152
+ if (disposed) return;
153
+ disposed = true;
154
+ service.dispose();
155
+ programs.clear();
156
+ }
157
+ };
158
+ }
73
159
 
74
- export { createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
160
+ export { FORMBAR_VALUE_THEN_OPERATOR, createArbiterPlugin, evaluateExpression, isArbiterInternalPath, registerExpressionThenOperator };
75
161
  //# sourceMappingURL=index.js.map
76
162
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal-paths.ts","../src/arbiter-plugin.ts","../src/expression-utils.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Arbiter-internal paths start with $ and are not user-facing form data.\n * Filter them out of write intents.\n */\nexport function isArbiterInternalPath(path: string): boolean {\n\treturn path.startsWith(\"$\") && !path.startsWith(\"$ui.\");\n}\n","import { createSession } from \"@arbitre/core\";\nimport type { FiringResult, ProductionRule, RuleSession } from \"@arbitre/core\";\nimport type {\n\tFormPlugin,\n\tPluginEvaluateContext,\n\tPluginEvaluateResult,\n\tPluginFieldMeta,\n\tPluginWrite,\n} from \"@formbar/core\";\nimport { isArbiterInternalPath } from \"./internal-paths.js\";\n\nexport interface ArbiterPluginOptions {\n\t/** Provide raw rules — a session will be created internally. */\n\treadonly rules?: readonly ProductionRule[];\n\t/** Provide a pre-configured session instead of raw rules. */\n\treadonly session?: RuleSession;\n}\n\n/**\n * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.\n * Syncs form data into the rule session, fires rules, and converts results\n * into PluginWrite[] and PluginFieldMeta records.\n */\nexport function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {\n\tconst { rules, session: externalSession } = options;\n\n\tlet session: RuleSession;\n\tlet ownsSession: boolean;\n\n\tif (externalSession) {\n\t\tsession = externalSession;\n\t\townsSession = false;\n\t} else if (rules) {\n\t\tsession = createSession({ rules: rules as ProductionRule[] });\n\t\townsSession = true;\n\t} else {\n\t\tthrow new Error(\"createArbiterPlugin requires either `rules` or `session`\");\n\t}\n\n\treturn {\n\t\tid: \"arbiter\",\n\n\t\tevaluate(ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {\n\t\t\t// Prevent re-entry from own writes\n\t\t\tif (ctx.origin.startsWith(\"plugin:arbiter\")) return;\n\n\t\t\t// Short-circuit when nothing relevant changed\n\t\t\tif (!ctx.change.dataChanged && !ctx.change.uiChanged) return;\n\n\t\t\t// Sync form data fields into the session\n\t\t\tconst data = ctx.data as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(data)) {\n\t\t\t\tsession.assert(key, data[key]);\n\t\t\t}\n\n\t\t\t// Sync $ui.* state\n\t\t\tconst uiState = ctx.uiState as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(uiState)) {\n\t\t\t\tsession.assert(`$ui.${key}`, uiState[key]);\n\t\t\t}\n\n\t\t\t// Fire rules\n\t\t\tconst result: FiringResult = session.fire();\n\n\t\t\t// Convert changes to PluginWrite[], filtering internal paths\n\t\t\tconst writes: PluginWrite[] = [];\n\t\t\tfor (const change of result.changes) {\n\t\t\t\tif (isArbiterInternalPath(change.path)) continue;\n\t\t\t\twrites.push({\n\t\t\t\t\tpath: change.path,\n\t\t\t\t\tvalue: change.newValue,\n\t\t\t\t\tmode: \"set\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Derive field meta from session state\n\t\t\tconst fieldMeta: Record<string, PluginFieldMeta> = {};\n\t\t\tconst state = session.getState();\n\t\t\tfor (const [path, value] of Object.entries(state)) {\n\t\t\t\tif (!path.startsWith(\"$meta.\")) continue;\n\t\t\t\t// Convention: $meta.<fieldPath> holds { visible, disabled, required, readOnly, label }\n\t\t\t\tconst fieldPath = path.slice(\"$meta.\".length);\n\t\t\t\tif (typeof value === \"object\" && value !== null) {\n\t\t\t\t\tfieldMeta[fieldPath] = value as PluginFieldMeta;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\twrites: writes.length > 0 ? writes : undefined,\n\t\t\t\tfieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : undefined,\n\t\t\t};\n\t\t},\n\n\t\tonDispose() {\n\t\t\tif (ownsSession) {\n\t\t\t\tsession.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n","import { assertSafeSegment, evaluate } from \"kuery\";\nimport type { ExprNode } from \"kuery\";\n\n/**\n * Evaluate a kuery expression against a data context.\n * This is a convenience re-export for consumers using expressions with arbiter rules.\n */\nexport function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown {\n\treturn evaluate(expr, context);\n}\n\nexport { assertSafeSegment };\nexport type { ExprNode };\n"]}
1
+ {"version":3,"sources":["../src/internal-paths.ts","../src/arbiter-plugin.ts","../src/expression-utils.ts","../src/expression-then-operator.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Arbiter-internal paths start with $ and are not user-facing form data.\n * Filter them out of write intents.\n */\nexport function isArbiterInternalPath(path: string): boolean {\n\treturn path.startsWith(\"$\") && !path.startsWith(\"$ui.\");\n}\n","import { createSession } from \"@arbitre/core\";\nimport type { FiringResult, ProductionRule, RuleSession } from \"@arbitre/core\";\nimport type {\n\tFormPlugin,\n\tPluginEvaluateContext,\n\tPluginEvaluateResult,\n\tPluginFieldMeta,\n\tPluginWrite,\n} from \"@formbar/core\";\nimport { isArbiterInternalPath } from \"./internal-paths.js\";\n\nexport interface ArbiterPluginOptions {\n\t/** Provide raw rules — a session will be created internally. */\n\treadonly rules?: readonly ProductionRule[];\n\t/** Provide a pre-configured session instead of raw rules. */\n\treadonly session?: RuleSession;\n}\n\n/**\n * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.\n * Syncs form data into the rule session, fires rules, and converts results\n * into PluginWrite[] and PluginFieldMeta records.\n */\nexport function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {\n\tconst { rules, session: externalSession } = options;\n\n\tlet session: RuleSession;\n\tlet ownsSession: boolean;\n\n\tif (externalSession) {\n\t\tsession = externalSession;\n\t\townsSession = false;\n\t} else if (rules) {\n\t\tsession = createSession({ rules: rules as ProductionRule[] });\n\t\townsSession = true;\n\t} else {\n\t\tthrow new Error(\"createArbiterPlugin requires either `rules` or `session`\");\n\t}\n\n\treturn {\n\t\tid: \"arbiter\",\n\n\t\tevaluate(ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {\n\t\t\t// Prevent re-entry from own writes\n\t\t\tif (ctx.origin.startsWith(\"plugin:arbiter\")) return;\n\n\t\t\t// Short-circuit when nothing relevant changed\n\t\t\tif (!ctx.change.dataChanged && !ctx.change.uiChanged) return;\n\n\t\t\t// Sync form data fields into the session\n\t\t\tconst data = ctx.data as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(data)) {\n\t\t\t\tsession.assert(key, data[key]);\n\t\t\t}\n\n\t\t\t// Sync $ui.* state\n\t\t\tconst uiState = ctx.uiState as Record<string, unknown>;\n\t\t\tfor (const key of Object.keys(uiState)) {\n\t\t\t\tsession.assert(`$ui.${key}`, uiState[key]);\n\t\t\t}\n\n\t\t\t// Fire rules\n\t\t\tconst result: FiringResult = session.fire();\n\n\t\t\t// Convert changes to PluginWrite[], filtering internal paths\n\t\t\tconst writes: PluginWrite[] = [];\n\t\t\tfor (const change of result.changes) {\n\t\t\t\tif (isArbiterInternalPath(change.path)) continue;\n\t\t\t\twrites.push({\n\t\t\t\t\tpath: change.path,\n\t\t\t\t\tvalue: change.newValue,\n\t\t\t\t\tmode: \"set\",\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Derive field meta from session state\n\t\t\tconst fieldMeta: Record<string, PluginFieldMeta> = {};\n\t\t\tconst state = session.getState();\n\t\t\tfor (const [path, value] of Object.entries(state)) {\n\t\t\t\tif (!path.startsWith(\"$meta.\")) continue;\n\t\t\t\t// Convention: $meta.<fieldPath> holds { visible, disabled, required, readOnly, label }\n\t\t\t\tconst fieldPath = path.slice(\"$meta.\".length);\n\t\t\t\tif (typeof value === \"object\" && value !== null) {\n\t\t\t\t\tfieldMeta[fieldPath] = value as PluginFieldMeta;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\twrites: writes.length > 0 ? writes : undefined,\n\t\t\t\tfieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : undefined,\n\t\t\t};\n\t\t},\n\n\t\tonDispose() {\n\t\t\tif (ownsSession) {\n\t\t\t\tsession.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n","import { assertSafeSegment, evaluate } from \"kuery\";\nimport type { ExprNode } from \"kuery\";\n\n/**\n * Evaluate a kuery expression against a data context.\n * This is a convenience re-export for consumers using expressions with arbiter rules.\n */\nexport function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown {\n\treturn evaluate(expr, context);\n}\n\nexport { assertSafeSegment };\nexport type { ExprNode };\n","import type { ThenOperatorHandler, ThenOperatorRegistry } from \"@arbitre/core\";\nimport { ExpressionError, createExpressionService, synchronousValue } from \"@formbar/expressions\";\nimport type { Authorization, Expression, ExpressionProfile, NamespaceProvider, Program } from \"@formbar/expressions\";\n\nexport const FORMBAR_VALUE_THEN_OPERATOR = \"$formbarValue\";\n\nexport interface ExpressionThenOperatorOptions {\n\treadonly programs: ReadonlyMap<string, Expression>;\n\treadonly profile?: ExpressionProfile;\n\treadonly authorize?: Authorization;\n\t/** Select trusted external roots from the current incoming stage scope. */\n\treadonly namespaces?: (scope: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;\n}\n\nexport interface RegisteredExpressionThenOperator {\n\treadonly handler: ThenOperatorHandler;\n\tdispose(): void;\n}\n\nfunction providers(roots: Readonly<Record<string, unknown>>): Record<string, NamespaceProvider> {\n\treturn Object.fromEntries(\n\t\tObject.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {} }]),\n\t);\n}\n\nfunction plainRootMap(value: unknown): value is Readonly<Record<string, unknown>> {\n\tif (value === null || typeof value !== \"object\") return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\tif (prototype !== Object.prototype && prototype !== null) return false;\n\treturn Reflect.ownKeys(value).every((key) => {\n\t\tif (typeof key !== \"string\") return false;\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, key);\n\t\treturn descriptor !== undefined && \"value\" in descriptor && descriptor.enumerable;\n\t});\n}\n\nfunction scopeProviders(\n\toptions: ExpressionThenOperatorOptions,\n\tscope: Readonly<Record<string, unknown>>,\n): Record<string, NamespaceProvider> {\n\ttry {\n\t\tconst selected = options.namespaces ? synchronousValue(options.namespaces(scope)) : {};\n\t\tif (!plainRootMap(selected)) throw new ExpressionError(\"adapter\");\n\t\tconst data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith(\"$\")));\n\t\treturn providers({ ...selected, data, ui: scope.$ui });\n\t} catch {\n\t\tthrow new ExpressionError(\"adapter\");\n\t}\n}\n\nfunction compilePrograms(options: ExpressionThenOperatorOptions) {\n\tconst service = createExpressionService({\n\t\t...(options.profile ? { profile: options.profile } : {}),\n\t\t...(options.authorize ? { authorize: options.authorize } : {}),\n\t});\n\tconst programs = new Map<string, Program>();\n\ttry {\n\t\tfor (const [id, expression] of options.programs) {\n\t\t\tif (typeof id !== \"string\") throw new ExpressionError(\"invalid-input\");\n\t\t\tconst compiled = service.compile(expression);\n\t\t\tif (!compiled.ok) throw new ExpressionError(compiled.diagnostics[0].code);\n\t\t\tprograms.set(id, compiled.value);\n\t\t}\n\t\treturn { service, programs };\n\t} catch (error) {\n\t\tservice.dispose();\n\t\tprograms.clear();\n\t\tthrow error;\n\t}\n}\n\n/** Registers Formbar's atomic expression stage in an Arbitre 0.3 public registry. */\nexport function registerExpressionThenOperator(\n\tregistry: ThenOperatorRegistry,\n\toptions: ExpressionThenOperatorOptions,\n): RegisteredExpressionThenOperator {\n\tif (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new ExpressionError(\"invalid-input\");\n\tconst { service, programs } = compilePrograms(options);\n\tlet disposed = false;\n\tconst handler: ThenOperatorHandler = (entries, scope, write) => {\n\t\tif (disposed) throw new ExpressionError(\"disposed\");\n\t\tconst selected: Array<readonly [string, Program]> = [];\n\t\tfor (const [path, id] of entries) {\n\t\t\tif (typeof id !== \"string\") throw new ExpressionError(\"invalid-input\");\n\t\t\tconst program = programs.get(id);\n\t\t\tif (!program) throw new ExpressionError(\"unknown-program\");\n\t\t\tselected.push([path, program]);\n\t\t}\n\t\tconst context = scopeProviders(options, scope);\n\t\tconst values = selected.map(([, program]) => {\n\t\t\tconst result = service.evaluate(program, context);\n\t\t\tif (!result.ok) throw new ExpressionError(result.diagnostics[0].code);\n\t\t\treturn result.value;\n\t\t});\n\t\tfor (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);\n\t};\n\ttry {\n\t\tregistry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);\n\t} catch {\n\t\tservice.dispose();\n\t\tprograms.clear();\n\t\tthrow new ExpressionError(\"adapter\");\n\t}\n\treturn {\n\t\thandler,\n\t\tdispose: () => {\n\t\t\tif (disposed) return;\n\t\t\tdisposed = true;\n\t\t\tservice.dispose();\n\t\t\tprograms.clear();\n\t\t},\n\t};\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formbar/arbiter",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Arbiter rule engine bridge for @formbar/core — declarative UI governance via production rules",
6
6
  "license": "MIT",
@@ -24,17 +24,18 @@
24
24
  "scripts": {
25
25
  "build": "tsc --noEmit",
26
26
  "build:dist": "tsup",
27
- "test": "vitest run"
27
+ "test": "vitest run --root ../.. packages/arbiter/src/__tests__"
28
28
  },
29
29
  "dependencies": {
30
- "@formbar/core": "^0.3.0"
30
+ "@formbar/expressions": "^0.4.0",
31
+ "@formbar/core": "^0.4.0"
31
32
  },
32
33
  "peerDependencies": {
33
- "@arbitre/core": ">=0.1.0",
34
- "kuery": ">=2.0.0"
34
+ "@arbitre/core": "^0.3.0",
35
+ "kuery": "^2.1.0"
35
36
  },
36
37
  "devDependencies": {
37
- "@arbitre/core": "^0.2.0",
38
- "kuery": "^2.0.0"
38
+ "@arbitre/core": "^0.3.0",
39
+ "kuery": "^2.1.0"
39
40
  }
40
41
  }
@@ -0,0 +1,150 @@
1
+ import { createSession } from "@arbitre/core";
2
+ import type { ThenOperatorHandler, ThenOperatorRegistry } from "@arbitre/core";
3
+ import { ExpressionError } from "@formbar/expressions";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { literal, op, ref } from "../../../../test/expression-fixtures.js";
6
+ import { FORMBAR_VALUE_THEN_OPERATOR, registerExpressionThenOperator } from "../index.js";
7
+
8
+ function createRegistry(): ThenOperatorRegistry {
9
+ const handlers = new Map<string, ThenOperatorHandler>();
10
+ return {
11
+ register(name, handler) {
12
+ if (handlers.has(name)) throw new Error("duplicate");
13
+ handlers.set(name, handler);
14
+ },
15
+ get: (name) => handlers.get(name),
16
+ has: (name) => handlers.has(name),
17
+ };
18
+ }
19
+
20
+ function programMap(entries: Record<string, ReturnType<typeof literal> | ReturnType<typeof ref>>) {
21
+ return new Map(Object.entries(entries));
22
+ }
23
+
24
+ describe("Arbitre expression then operator", () => {
25
+ it("registers the fixed public name and rejects duplicate registration", () => {
26
+ const registry = createRegistry();
27
+ const registered = registerExpressionThenOperator(registry, { programs: programMap({ value: literal(1) }) });
28
+ expect(registry.get(FORMBAR_VALUE_THEN_OPERATOR)).toBe(registered.handler);
29
+ expect(() => registerExpressionThenOperator(registry, { programs: new Map() })).toThrow(/^invalid-input$/);
30
+ registered.dispose();
31
+ });
32
+
33
+ it("compiles every program before mutating the registry and cleans up registration failures", () => {
34
+ const registry = createRegistry();
35
+ const register = vi.spyOn(registry, "register");
36
+ expect(() =>
37
+ registerExpressionThenOperator(registry, {
38
+ programs: new Map([
39
+ ["ok", literal(1)],
40
+ ["bad", { kind: "op", op: "missing", args: [] }],
41
+ ]),
42
+ }),
43
+ ).toThrow(/^unsupported-operator$/);
44
+ expect(register).not.toHaveBeenCalled();
45
+ const failing = {
46
+ ...createRegistry(),
47
+ register: () => {
48
+ throw new Error("secret");
49
+ },
50
+ };
51
+ expect(() => registerExpressionThenOperator(failing, { programs: new Map() })).toThrow(/^adapter$/);
52
+ });
53
+
54
+ it("uses one incoming scope for atomic ordered writes while separate stages chain", () => {
55
+ const registry = createRegistry();
56
+ registerExpressionThenOperator(registry, {
57
+ programs: new Map([
58
+ ["source", ref("source")],
59
+ ["first", ref("first")],
60
+ ]),
61
+ });
62
+ const session = createSession({
63
+ initialState: { trigger: true, source: 2, first: 10 },
64
+ thenOperators: registry,
65
+ rules: [
66
+ {
67
+ name: "pipeline",
68
+ when: { trigger: true },
69
+ then: [
70
+ { $set: { source: 3 } },
71
+ { $formbarValue: { first: "source", sameStage: "first" } },
72
+ { $formbarValue: { nextStage: "first" } },
73
+ ],
74
+ },
75
+ ],
76
+ });
77
+ const result = session.fire();
78
+ expect(session.getState()).toMatchObject({ source: 3, first: 3, sameStage: 10, nextStage: 3 });
79
+ expect(result.changes.map(({ path }) => path)).toEqual(["source", "first", "sameStage", "nextStage"]);
80
+ expect(session.introspect.getRuleDependencies("pipeline")).toMatchObject({
81
+ actionWrites: ["source"],
82
+ actionWritesUnknown: true,
83
+ rhsReads: [],
84
+ });
85
+ });
86
+
87
+ it("evaluates all entries before writing and records no partial changes on failure", () => {
88
+ const registry = createRegistry();
89
+ registerExpressionThenOperator(registry, {
90
+ programs: programMap({ ok: literal(1), denied: ref("secret") }),
91
+ authorize: (reference) => reference.segments[0] !== "secret",
92
+ });
93
+ const session = createSession({
94
+ initialState: { trigger: true, secret: 9 },
95
+ thenOperators: registry,
96
+ rules: [
97
+ { name: "atomic", when: { trigger: true }, then: [{ $formbarValue: { first: "ok", second: "denied" } }] },
98
+ ],
99
+ });
100
+ expect(() => session.fire()).toThrow(/^denied$/);
101
+ expect(session.getPath("first")).toBeUndefined();
102
+ });
103
+
104
+ it("selects plain synchronous namespaces and maps selector failures to adapter", async () => {
105
+ const registry = createRegistry();
106
+ let selection: unknown = { pricing: { rate: 5 } };
107
+ const registered = registerExpressionThenOperator(registry, {
108
+ programs: new Map([["total", op("add", ref("source"), ref("rate", "pricing"))]]),
109
+ namespaces: () => selection as Record<string, unknown>,
110
+ });
111
+ const run = () => registered.handler(new Map([["total", "total"]]), { source: 2 }, () => {});
112
+ expect(run).not.toThrow();
113
+ selection = null;
114
+ expect(run).toThrow(/^adapter$/);
115
+ selection = Promise.reject(new Error("secret"));
116
+ expect(run).toThrow(/^adapter$/);
117
+ await new Promise((resolve) => setTimeout(resolve, 0));
118
+ selection = Object.create({ inherited: true });
119
+ expect(run).toThrow(/^adapter$/);
120
+ });
121
+
122
+ it("rejects malformed IDs, unknown programs, and use after idempotent disposal", () => {
123
+ const registry = createRegistry();
124
+ const registered = registerExpressionThenOperator(registry, { programs: programMap({ value: literal(1) }) });
125
+ expect(() => registered.handler(new Map([["x", 1]]), {}, () => {})).toThrow(/^invalid-input$/);
126
+ expect(() => registered.handler(new Map([["x", "unknown"]]), {}, () => {})).toThrow(/^unknown-program$/);
127
+ registered.dispose();
128
+ registered.dispose();
129
+ expect(() => registered.handler(new Map([["x", "value"]]), {}, () => {})).toThrow(/^disposed$/);
130
+ expect(() => {
131
+ throw new ExpressionError("adapter");
132
+ }).toThrow(/^adapter$/);
133
+ });
134
+
135
+ it("participates in Arbitre truth maintenance through tracked writes", () => {
136
+ const registry = createRegistry();
137
+ registerExpressionThenOperator(registry, { programs: programMap({ value: literal(7) }) });
138
+ const session = createSession({
139
+ initialState: { active: false },
140
+ thenOperators: registry,
141
+ rules: [{ name: "derived", when: { active: true }, then: [{ $formbarValue: { derived: "value" } }] }],
142
+ });
143
+ session.assert("active", true);
144
+ expect(session.fire().changes.some(({ path }) => path === "derived")).toBe(true);
145
+ session.assert("active", false);
146
+ expect(session.fire().changes.some(({ path, newValue }) => path === "derived" && newValue === undefined)).toBe(
147
+ true,
148
+ );
149
+ });
150
+ });
@@ -0,0 +1,113 @@
1
+ import type { ThenOperatorHandler, ThenOperatorRegistry } from "@arbitre/core";
2
+ import { ExpressionError, createExpressionService, synchronousValue } from "@formbar/expressions";
3
+ import type { Authorization, Expression, ExpressionProfile, NamespaceProvider, Program } from "@formbar/expressions";
4
+
5
+ export const FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
6
+
7
+ export interface ExpressionThenOperatorOptions {
8
+ readonly programs: ReadonlyMap<string, Expression>;
9
+ readonly profile?: ExpressionProfile;
10
+ readonly authorize?: Authorization;
11
+ /** Select trusted external roots from the current incoming stage scope. */
12
+ readonly namespaces?: (scope: Readonly<Record<string, unknown>>) => Readonly<Record<string, unknown>>;
13
+ }
14
+
15
+ export interface RegisteredExpressionThenOperator {
16
+ readonly handler: ThenOperatorHandler;
17
+ dispose(): void;
18
+ }
19
+
20
+ function providers(roots: Readonly<Record<string, unknown>>): Record<string, NamespaceProvider> {
21
+ return Object.fromEntries(
22
+ Object.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {} }]),
23
+ );
24
+ }
25
+
26
+ function plainRootMap(value: unknown): value is Readonly<Record<string, unknown>> {
27
+ if (value === null || typeof value !== "object") return false;
28
+ const prototype = Object.getPrototypeOf(value);
29
+ if (prototype !== Object.prototype && prototype !== null) return false;
30
+ return Reflect.ownKeys(value).every((key) => {
31
+ if (typeof key !== "string") return false;
32
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
33
+ return descriptor !== undefined && "value" in descriptor && descriptor.enumerable;
34
+ });
35
+ }
36
+
37
+ function scopeProviders(
38
+ options: ExpressionThenOperatorOptions,
39
+ scope: Readonly<Record<string, unknown>>,
40
+ ): Record<string, NamespaceProvider> {
41
+ try {
42
+ const selected = options.namespaces ? synchronousValue(options.namespaces(scope)) : {};
43
+ if (!plainRootMap(selected)) throw new ExpressionError("adapter");
44
+ const data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith("$")));
45
+ return providers({ ...selected, data, ui: scope.$ui });
46
+ } catch {
47
+ throw new ExpressionError("adapter");
48
+ }
49
+ }
50
+
51
+ function compilePrograms(options: ExpressionThenOperatorOptions) {
52
+ const service = createExpressionService({
53
+ ...(options.profile ? { profile: options.profile } : {}),
54
+ ...(options.authorize ? { authorize: options.authorize } : {}),
55
+ });
56
+ const programs = new Map<string, Program>();
57
+ try {
58
+ for (const [id, expression] of options.programs) {
59
+ if (typeof id !== "string") throw new ExpressionError("invalid-input");
60
+ const compiled = service.compile(expression);
61
+ if (!compiled.ok) throw new ExpressionError(compiled.diagnostics[0].code);
62
+ programs.set(id, compiled.value);
63
+ }
64
+ return { service, programs };
65
+ } catch (error) {
66
+ service.dispose();
67
+ programs.clear();
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ /** Registers Formbar's atomic expression stage in an Arbitre 0.3 public registry. */
73
+ export function registerExpressionThenOperator(
74
+ registry: ThenOperatorRegistry,
75
+ options: ExpressionThenOperatorOptions,
76
+ ): RegisteredExpressionThenOperator {
77
+ if (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new ExpressionError("invalid-input");
78
+ const { service, programs } = compilePrograms(options);
79
+ let disposed = false;
80
+ const handler: ThenOperatorHandler = (entries, scope, write) => {
81
+ if (disposed) throw new ExpressionError("disposed");
82
+ const selected: Array<readonly [string, Program]> = [];
83
+ for (const [path, id] of entries) {
84
+ if (typeof id !== "string") throw new ExpressionError("invalid-input");
85
+ const program = programs.get(id);
86
+ if (!program) throw new ExpressionError("unknown-program");
87
+ selected.push([path, program]);
88
+ }
89
+ const context = scopeProviders(options, scope);
90
+ const values = selected.map(([, program]) => {
91
+ const result = service.evaluate(program, context);
92
+ if (!result.ok) throw new ExpressionError(result.diagnostics[0].code);
93
+ return result.value;
94
+ });
95
+ for (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);
96
+ };
97
+ try {
98
+ registry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);
99
+ } catch {
100
+ service.dispose();
101
+ programs.clear();
102
+ throw new ExpressionError("adapter");
103
+ }
104
+ return {
105
+ handler,
106
+ dispose: () => {
107
+ if (disposed) return;
108
+ disposed = true;
109
+ service.dispose();
110
+ programs.clear();
111
+ },
112
+ };
113
+ }
package/src/index.ts CHANGED
@@ -3,3 +3,9 @@ export type { ArbiterPluginOptions } from "./arbiter-plugin.js";
3
3
  export { evaluateExpression, assertSafeSegment } from "./expression-utils.js";
4
4
  export type { ExprNode } from "./expression-utils.js";
5
5
  export { isArbiterInternalPath } from "./internal-paths.js";
6
+ export {
7
+ FORMBAR_VALUE_THEN_OPERATOR,
8
+ registerExpressionThenOperator,
9
+ type ExpressionThenOperatorOptions,
10
+ type RegisteredExpressionThenOperator,
11
+ } from "./expression-then-operator.js";