@formbar/arbiter 0.3.0 → 0.7.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 +61 -2
- package/dist/index.cjs +112 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +111 -51
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/arbiter-integration.test.ts +2 -0
- package/src/__tests__/expression-then-operator.test.ts +150 -0
- package/src/arbiter-plugin.ts +32 -78
- package/src/expression-then-operator.ts +113 -0
- package/src/index.ts +6 -0
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
|
-
##
|
|
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
|
|
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
|
|
|
@@ -11,73 +12,134 @@ function isArbiterInternalPath(path) {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// src/arbiter-plugin.ts
|
|
15
|
+
function resolveSession(options) {
|
|
16
|
+
if (options.session) return { session: options.session, owned: false };
|
|
17
|
+
if (options.rules) return { session: core.createSession({ rules: options.rules }), owned: true };
|
|
18
|
+
throw new Error("createArbiterPlugin requires either `rules` or `session`");
|
|
19
|
+
}
|
|
20
|
+
function syncSession(session, ctx) {
|
|
21
|
+
const data = ctx.data;
|
|
22
|
+
for (const key of Object.keys(data)) session.assert(key, data[key]);
|
|
23
|
+
const uiState = ctx.uiState;
|
|
24
|
+
for (const key of Object.keys(uiState)) session.assert(`$ui.${key}`, uiState[key]);
|
|
25
|
+
}
|
|
26
|
+
function toWrites(result) {
|
|
27
|
+
return result.changes.filter((change) => !isArbiterInternalPath(change.path)).map((change) => ({ path: change.path, value: change.newValue, mode: "set" }));
|
|
28
|
+
}
|
|
29
|
+
function evaluateSession(session, ctx) {
|
|
30
|
+
if (ctx.origin.startsWith("plugin:arbiter")) return;
|
|
31
|
+
if (!ctx.change.dataChanged && !ctx.change.uiChanged) return;
|
|
32
|
+
syncSession(session, ctx);
|
|
33
|
+
const writes = toWrites(session.fire());
|
|
34
|
+
return { writes: writes.length > 0 ? writes : void 0 };
|
|
35
|
+
}
|
|
14
36
|
function createArbiterPlugin(options) {
|
|
15
|
-
const {
|
|
16
|
-
let session;
|
|
17
|
-
let ownsSession;
|
|
18
|
-
if (externalSession) {
|
|
19
|
-
session = externalSession;
|
|
20
|
-
ownsSession = false;
|
|
21
|
-
} else if (rules) {
|
|
22
|
-
session = core.createSession({ rules });
|
|
23
|
-
ownsSession = true;
|
|
24
|
-
} else {
|
|
25
|
-
throw new Error("createArbiterPlugin requires either `rules` or `session`");
|
|
26
|
-
}
|
|
37
|
+
const { session, owned } = resolveSession(options);
|
|
27
38
|
return {
|
|
28
39
|
id: "arbiter",
|
|
29
|
-
evaluate(ctx)
|
|
30
|
-
if (ctx.origin.startsWith("plugin:arbiter")) return;
|
|
31
|
-
if (!ctx.change.dataChanged && !ctx.change.uiChanged) return;
|
|
32
|
-
const data = ctx.data;
|
|
33
|
-
for (const key of Object.keys(data)) {
|
|
34
|
-
session.assert(key, data[key]);
|
|
35
|
-
}
|
|
36
|
-
const uiState = ctx.uiState;
|
|
37
|
-
for (const key of Object.keys(uiState)) {
|
|
38
|
-
session.assert(`$ui.${key}`, uiState[key]);
|
|
39
|
-
}
|
|
40
|
-
const result = session.fire();
|
|
41
|
-
const writes = [];
|
|
42
|
-
for (const change of result.changes) {
|
|
43
|
-
if (isArbiterInternalPath(change.path)) continue;
|
|
44
|
-
writes.push({
|
|
45
|
-
path: change.path,
|
|
46
|
-
value: change.newValue,
|
|
47
|
-
mode: "set"
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
const fieldMeta = {};
|
|
51
|
-
const state = session.getState();
|
|
52
|
-
for (const [path, value] of Object.entries(state)) {
|
|
53
|
-
if (!path.startsWith("$meta.")) continue;
|
|
54
|
-
const fieldPath = path.slice("$meta.".length);
|
|
55
|
-
if (typeof value === "object" && value !== null) {
|
|
56
|
-
fieldMeta[fieldPath] = value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
writes: writes.length > 0 ? writes : void 0,
|
|
61
|
-
fieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : void 0
|
|
62
|
-
};
|
|
63
|
-
},
|
|
40
|
+
evaluate: (ctx) => evaluateSession(session, ctx),
|
|
64
41
|
onDispose() {
|
|
65
|
-
if (
|
|
66
|
-
session.dispose();
|
|
67
|
-
}
|
|
42
|
+
if (owned) session.dispose();
|
|
68
43
|
}
|
|
69
44
|
};
|
|
70
45
|
}
|
|
71
46
|
function evaluateExpression(expr, context) {
|
|
72
47
|
return kuery.evaluate(expr, context);
|
|
73
48
|
}
|
|
49
|
+
var FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
|
|
50
|
+
function providers(roots) {
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
Object.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {
|
|
53
|
+
} }])
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function plainRootMap(value) {
|
|
57
|
+
if (value === null || typeof value !== "object") return false;
|
|
58
|
+
const prototype = Object.getPrototypeOf(value);
|
|
59
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
60
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
61
|
+
if (typeof key !== "string") return false;
|
|
62
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
63
|
+
return descriptor !== void 0 && "value" in descriptor && descriptor.enumerable;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function scopeProviders(options, scope) {
|
|
67
|
+
try {
|
|
68
|
+
const selected = options.namespaces ? expressions.synchronousValue(options.namespaces(scope)) : {};
|
|
69
|
+
if (!plainRootMap(selected)) throw new expressions.ExpressionError("adapter");
|
|
70
|
+
const data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith("$")));
|
|
71
|
+
return providers({ ...selected, data, ui: scope.$ui });
|
|
72
|
+
} catch {
|
|
73
|
+
throw new expressions.ExpressionError("adapter");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function compilePrograms(options) {
|
|
77
|
+
const service = expressions.createExpressionService({
|
|
78
|
+
...options.profile ? { profile: options.profile } : {},
|
|
79
|
+
...options.authorize ? { authorize: options.authorize } : {}
|
|
80
|
+
});
|
|
81
|
+
const programs = /* @__PURE__ */ new Map();
|
|
82
|
+
try {
|
|
83
|
+
for (const [id, expression] of options.programs) {
|
|
84
|
+
if (typeof id !== "string") throw new expressions.ExpressionError("invalid-input");
|
|
85
|
+
const compiled = service.compile(expression);
|
|
86
|
+
if (!compiled.ok) throw new expressions.ExpressionError(compiled.diagnostics[0].code);
|
|
87
|
+
programs.set(id, compiled.value);
|
|
88
|
+
}
|
|
89
|
+
return { service, programs };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
service.dispose();
|
|
92
|
+
programs.clear();
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function registerExpressionThenOperator(registry, options) {
|
|
97
|
+
if (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new expressions.ExpressionError("invalid-input");
|
|
98
|
+
const { service, programs } = compilePrograms(options);
|
|
99
|
+
let disposed = false;
|
|
100
|
+
const handler = (entries, scope, write) => {
|
|
101
|
+
if (disposed) throw new expressions.ExpressionError("disposed");
|
|
102
|
+
const selected = [];
|
|
103
|
+
for (const [path, id] of entries) {
|
|
104
|
+
if (typeof id !== "string") throw new expressions.ExpressionError("invalid-input");
|
|
105
|
+
const program = programs.get(id);
|
|
106
|
+
if (!program) throw new expressions.ExpressionError("unknown-program");
|
|
107
|
+
selected.push([path, program]);
|
|
108
|
+
}
|
|
109
|
+
const context = scopeProviders(options, scope);
|
|
110
|
+
const values = selected.map(([, program]) => {
|
|
111
|
+
const result = service.evaluate(program, context);
|
|
112
|
+
if (!result.ok) throw new expressions.ExpressionError(result.diagnostics[0].code);
|
|
113
|
+
return result.value;
|
|
114
|
+
});
|
|
115
|
+
for (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
registry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);
|
|
119
|
+
} catch {
|
|
120
|
+
service.dispose();
|
|
121
|
+
programs.clear();
|
|
122
|
+
throw new expressions.ExpressionError("adapter");
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
handler,
|
|
126
|
+
dispose: () => {
|
|
127
|
+
if (disposed) return;
|
|
128
|
+
disposed = true;
|
|
129
|
+
service.dispose();
|
|
130
|
+
programs.clear();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
74
134
|
|
|
75
135
|
Object.defineProperty(exports, "assertSafeSegment", {
|
|
76
136
|
enumerable: true,
|
|
77
137
|
get: function () { return kuery.assertSafeSegment; }
|
|
78
138
|
});
|
|
139
|
+
exports.FORMBAR_VALUE_THEN_OPERATOR = FORMBAR_VALUE_THEN_OPERATOR;
|
|
79
140
|
exports.createArbiterPlugin = createArbiterPlugin;
|
|
80
141
|
exports.evaluateExpression = evaluateExpression;
|
|
81
142
|
exports.isArbiterInternalPath = isArbiterInternalPath;
|
|
143
|
+
exports.registerExpressionThenOperator = registerExpressionThenOperator;
|
|
82
144
|
//# sourceMappingURL=index.cjs.map
|
|
83
145
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 { FormPlugin, PluginEvaluateContext, PluginEvaluateResult, PluginWrite } 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\nfunction resolveSession(options: ArbiterPluginOptions): { session: RuleSession; owned: boolean } {\n\tif (options.session) return { session: options.session, owned: false };\n\tif (options.rules) return { session: createSession({ rules: options.rules as ProductionRule[] }), owned: true };\n\tthrow new Error(\"createArbiterPlugin requires either `rules` or `session`\");\n}\n\nfunction syncSession(session: RuleSession, ctx: PluginEvaluateContext): void {\n\tconst data = ctx.data as Record<string, unknown>;\n\tfor (const key of Object.keys(data)) session.assert(key, data[key]);\n\tconst uiState = ctx.uiState as Record<string, unknown>;\n\tfor (const key of Object.keys(uiState)) session.assert(`$ui.${key}`, uiState[key]);\n}\n\nfunction toWrites(result: FiringResult): readonly PluginWrite[] {\n\treturn result.changes\n\t\t.filter((change) => !isArbiterInternalPath(change.path))\n\t\t.map((change) => ({ path: change.path, value: change.newValue, mode: \"set\" as const }));\n}\n\nfunction evaluateSession(session: RuleSession, ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {\n\tif (ctx.origin.startsWith(\"plugin:arbiter\")) return;\n\tif (!ctx.change.dataChanged && !ctx.change.uiChanged) return;\n\tsyncSession(session, ctx);\n\tconst writes = toWrites(session.fire());\n\treturn { writes: writes.length > 0 ? writes : undefined };\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[] records.\n */\nexport function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {\n\tconst { session, owned } = resolveSession(options);\n\treturn {\n\t\tid: \"arbiter\",\n\t\tevaluate: (ctx) => evaluateSession(session, ctx),\n\t\tonDispose() {\n\t\t\tif (owned) session.dispose();\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. */
|
|
@@ -12,7 +13,7 @@ interface ArbiterPluginOptions {
|
|
|
12
13
|
/**
|
|
13
14
|
* Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.
|
|
14
15
|
* Syncs form data into the rule session, fires rules, and converts results
|
|
15
|
-
* into PluginWrite[]
|
|
16
|
+
* into PluginWrite[] records.
|
|
16
17
|
*/
|
|
17
18
|
declare function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin;
|
|
18
19
|
|
|
@@ -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
|
-
|
|
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. */
|
|
@@ -12,7 +13,7 @@ interface ArbiterPluginOptions {
|
|
|
12
13
|
/**
|
|
13
14
|
* Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.
|
|
14
15
|
* Syncs form data into the rule session, fires rules, and converts results
|
|
15
|
-
* into PluginWrite[]
|
|
16
|
+
* into PluginWrite[] records.
|
|
16
17
|
*/
|
|
17
18
|
declare function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin;
|
|
18
19
|
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -10,67 +11,126 @@ function isArbiterInternalPath(path) {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// src/arbiter-plugin.ts
|
|
14
|
+
function resolveSession(options) {
|
|
15
|
+
if (options.session) return { session: options.session, owned: false };
|
|
16
|
+
if (options.rules) return { session: createSession({ rules: options.rules }), owned: true };
|
|
17
|
+
throw new Error("createArbiterPlugin requires either `rules` or `session`");
|
|
18
|
+
}
|
|
19
|
+
function syncSession(session, ctx) {
|
|
20
|
+
const data = ctx.data;
|
|
21
|
+
for (const key of Object.keys(data)) session.assert(key, data[key]);
|
|
22
|
+
const uiState = ctx.uiState;
|
|
23
|
+
for (const key of Object.keys(uiState)) session.assert(`$ui.${key}`, uiState[key]);
|
|
24
|
+
}
|
|
25
|
+
function toWrites(result) {
|
|
26
|
+
return result.changes.filter((change) => !isArbiterInternalPath(change.path)).map((change) => ({ path: change.path, value: change.newValue, mode: "set" }));
|
|
27
|
+
}
|
|
28
|
+
function evaluateSession(session, ctx) {
|
|
29
|
+
if (ctx.origin.startsWith("plugin:arbiter")) return;
|
|
30
|
+
if (!ctx.change.dataChanged && !ctx.change.uiChanged) return;
|
|
31
|
+
syncSession(session, ctx);
|
|
32
|
+
const writes = toWrites(session.fire());
|
|
33
|
+
return { writes: writes.length > 0 ? writes : void 0 };
|
|
34
|
+
}
|
|
13
35
|
function createArbiterPlugin(options) {
|
|
14
|
-
const {
|
|
15
|
-
let session;
|
|
16
|
-
let ownsSession;
|
|
17
|
-
if (externalSession) {
|
|
18
|
-
session = externalSession;
|
|
19
|
-
ownsSession = false;
|
|
20
|
-
} else if (rules) {
|
|
21
|
-
session = createSession({ rules });
|
|
22
|
-
ownsSession = true;
|
|
23
|
-
} else {
|
|
24
|
-
throw new Error("createArbiterPlugin requires either `rules` or `session`");
|
|
25
|
-
}
|
|
36
|
+
const { session, owned } = resolveSession(options);
|
|
26
37
|
return {
|
|
27
38
|
id: "arbiter",
|
|
28
|
-
evaluate(ctx)
|
|
29
|
-
if (ctx.origin.startsWith("plugin:arbiter")) return;
|
|
30
|
-
if (!ctx.change.dataChanged && !ctx.change.uiChanged) return;
|
|
31
|
-
const data = ctx.data;
|
|
32
|
-
for (const key of Object.keys(data)) {
|
|
33
|
-
session.assert(key, data[key]);
|
|
34
|
-
}
|
|
35
|
-
const uiState = ctx.uiState;
|
|
36
|
-
for (const key of Object.keys(uiState)) {
|
|
37
|
-
session.assert(`$ui.${key}`, uiState[key]);
|
|
38
|
-
}
|
|
39
|
-
const result = session.fire();
|
|
40
|
-
const writes = [];
|
|
41
|
-
for (const change of result.changes) {
|
|
42
|
-
if (isArbiterInternalPath(change.path)) continue;
|
|
43
|
-
writes.push({
|
|
44
|
-
path: change.path,
|
|
45
|
-
value: change.newValue,
|
|
46
|
-
mode: "set"
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
const fieldMeta = {};
|
|
50
|
-
const state = session.getState();
|
|
51
|
-
for (const [path, value] of Object.entries(state)) {
|
|
52
|
-
if (!path.startsWith("$meta.")) continue;
|
|
53
|
-
const fieldPath = path.slice("$meta.".length);
|
|
54
|
-
if (typeof value === "object" && value !== null) {
|
|
55
|
-
fieldMeta[fieldPath] = value;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
writes: writes.length > 0 ? writes : void 0,
|
|
60
|
-
fieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : void 0
|
|
61
|
-
};
|
|
62
|
-
},
|
|
39
|
+
evaluate: (ctx) => evaluateSession(session, ctx),
|
|
63
40
|
onDispose() {
|
|
64
|
-
if (
|
|
65
|
-
session.dispose();
|
|
66
|
-
}
|
|
41
|
+
if (owned) session.dispose();
|
|
67
42
|
}
|
|
68
43
|
};
|
|
69
44
|
}
|
|
70
45
|
function evaluateExpression(expr, context) {
|
|
71
46
|
return evaluate(expr, context);
|
|
72
47
|
}
|
|
48
|
+
var FORMBAR_VALUE_THEN_OPERATOR = "$formbarValue";
|
|
49
|
+
function providers(roots) {
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
Object.entries(roots).map(([name, root]) => [name, { getSnapshot: () => root, subscribe: () => () => {
|
|
52
|
+
} }])
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function plainRootMap(value) {
|
|
56
|
+
if (value === null || typeof value !== "object") return false;
|
|
57
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
59
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
60
|
+
if (typeof key !== "string") return false;
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
62
|
+
return descriptor !== void 0 && "value" in descriptor && descriptor.enumerable;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function scopeProviders(options, scope) {
|
|
66
|
+
try {
|
|
67
|
+
const selected = options.namespaces ? synchronousValue(options.namespaces(scope)) : {};
|
|
68
|
+
if (!plainRootMap(selected)) throw new ExpressionError("adapter");
|
|
69
|
+
const data = Object.fromEntries(Object.entries(scope).filter(([key]) => !key.startsWith("$")));
|
|
70
|
+
return providers({ ...selected, data, ui: scope.$ui });
|
|
71
|
+
} catch {
|
|
72
|
+
throw new ExpressionError("adapter");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function compilePrograms(options) {
|
|
76
|
+
const service = createExpressionService({
|
|
77
|
+
...options.profile ? { profile: options.profile } : {},
|
|
78
|
+
...options.authorize ? { authorize: options.authorize } : {}
|
|
79
|
+
});
|
|
80
|
+
const programs = /* @__PURE__ */ new Map();
|
|
81
|
+
try {
|
|
82
|
+
for (const [id, expression] of options.programs) {
|
|
83
|
+
if (typeof id !== "string") throw new ExpressionError("invalid-input");
|
|
84
|
+
const compiled = service.compile(expression);
|
|
85
|
+
if (!compiled.ok) throw new ExpressionError(compiled.diagnostics[0].code);
|
|
86
|
+
programs.set(id, compiled.value);
|
|
87
|
+
}
|
|
88
|
+
return { service, programs };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
service.dispose();
|
|
91
|
+
programs.clear();
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function registerExpressionThenOperator(registry, options) {
|
|
96
|
+
if (registry.has(FORMBAR_VALUE_THEN_OPERATOR)) throw new ExpressionError("invalid-input");
|
|
97
|
+
const { service, programs } = compilePrograms(options);
|
|
98
|
+
let disposed = false;
|
|
99
|
+
const handler = (entries, scope, write) => {
|
|
100
|
+
if (disposed) throw new ExpressionError("disposed");
|
|
101
|
+
const selected = [];
|
|
102
|
+
for (const [path, id] of entries) {
|
|
103
|
+
if (typeof id !== "string") throw new ExpressionError("invalid-input");
|
|
104
|
+
const program = programs.get(id);
|
|
105
|
+
if (!program) throw new ExpressionError("unknown-program");
|
|
106
|
+
selected.push([path, program]);
|
|
107
|
+
}
|
|
108
|
+
const context = scopeProviders(options, scope);
|
|
109
|
+
const values = selected.map(([, program]) => {
|
|
110
|
+
const result = service.evaluate(program, context);
|
|
111
|
+
if (!result.ok) throw new ExpressionError(result.diagnostics[0].code);
|
|
112
|
+
return result.value;
|
|
113
|
+
});
|
|
114
|
+
for (let index = 0; index < selected.length; index++) write(selected[index][0], values[index]);
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
registry.register(FORMBAR_VALUE_THEN_OPERATOR, handler);
|
|
118
|
+
} catch {
|
|
119
|
+
service.dispose();
|
|
120
|
+
programs.clear();
|
|
121
|
+
throw new ExpressionError("adapter");
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
handler,
|
|
125
|
+
dispose: () => {
|
|
126
|
+
if (disposed) return;
|
|
127
|
+
disposed = true;
|
|
128
|
+
service.dispose();
|
|
129
|
+
programs.clear();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
73
133
|
|
|
74
|
-
export { createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
|
|
134
|
+
export { FORMBAR_VALUE_THEN_OPERATOR, createArbiterPlugin, evaluateExpression, isArbiterInternalPath, registerExpressionThenOperator };
|
|
75
135
|
//# sourceMappingURL=index.js.map
|
|
76
136
|
//# 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 { FormPlugin, PluginEvaluateContext, PluginEvaluateResult, PluginWrite } 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\nfunction resolveSession(options: ArbiterPluginOptions): { session: RuleSession; owned: boolean } {\n\tif (options.session) return { session: options.session, owned: false };\n\tif (options.rules) return { session: createSession({ rules: options.rules as ProductionRule[] }), owned: true };\n\tthrow new Error(\"createArbiterPlugin requires either `rules` or `session`\");\n}\n\nfunction syncSession(session: RuleSession, ctx: PluginEvaluateContext): void {\n\tconst data = ctx.data as Record<string, unknown>;\n\tfor (const key of Object.keys(data)) session.assert(key, data[key]);\n\tconst uiState = ctx.uiState as Record<string, unknown>;\n\tfor (const key of Object.keys(uiState)) session.assert(`$ui.${key}`, uiState[key]);\n}\n\nfunction toWrites(result: FiringResult): readonly PluginWrite[] {\n\treturn result.changes\n\t\t.filter((change) => !isArbiterInternalPath(change.path))\n\t\t.map((change) => ({ path: change.path, value: change.newValue, mode: \"set\" as const }));\n}\n\nfunction evaluateSession(session: RuleSession, ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {\n\tif (ctx.origin.startsWith(\"plugin:arbiter\")) return;\n\tif (!ctx.change.dataChanged && !ctx.change.uiChanged) return;\n\tsyncSession(session, ctx);\n\tconst writes = toWrites(session.fire());\n\treturn { writes: writes.length > 0 ? writes : undefined };\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[] records.\n */\nexport function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {\n\tconst { session, owned } = resolveSession(options);\n\treturn {\n\t\tid: \"arbiter\",\n\t\tevaluate: (ctx) => evaluateSession(session, ctx),\n\t\tonDispose() {\n\t\t\tif (owned) session.dispose();\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
|
+
"version": "0.7.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/
|
|
30
|
+
"@formbar/expressions": "^0.4.0",
|
|
31
|
+
"@formbar/core": "^0.7.0"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
|
-
"@arbitre/core": "
|
|
34
|
-
"kuery": "
|
|
34
|
+
"@arbitre/core": "^0.3.0",
|
|
35
|
+
"kuery": "^2.1.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@arbitre/core": "^0.
|
|
38
|
-
"kuery": "^2.
|
|
38
|
+
"@arbitre/core": "^0.3.0",
|
|
39
|
+
"kuery": "^2.1.0"
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -86,6 +86,8 @@ describe("createArbiterPlugin with pre-configured session", () => {
|
|
|
86
86
|
expect((state.uiState as Record<string, unknown>).visible).toBe(true);
|
|
87
87
|
// data writes should pass through
|
|
88
88
|
expect((state.data as Record<string, unknown>).name).toBe("kept");
|
|
89
|
+
// $meta remains internal; Arbiter policy production is intentionally deferred.
|
|
90
|
+
expect(state.fieldPolicy).toEqual([]);
|
|
89
91
|
form.dispose();
|
|
90
92
|
});
|
|
91
93
|
});
|
|
@@ -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
|
+
});
|
package/src/arbiter-plugin.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { createSession } from "@arbitre/core";
|
|
2
2
|
import type { FiringResult, ProductionRule, RuleSession } from "@arbitre/core";
|
|
3
|
-
import type {
|
|
4
|
-
FormPlugin,
|
|
5
|
-
PluginEvaluateContext,
|
|
6
|
-
PluginEvaluateResult,
|
|
7
|
-
PluginFieldMeta,
|
|
8
|
-
PluginWrite,
|
|
9
|
-
} from "@formbar/core";
|
|
3
|
+
import type { FormPlugin, PluginEvaluateContext, PluginEvaluateResult, PluginWrite } from "@formbar/core";
|
|
10
4
|
import { isArbiterInternalPath } from "./internal-paths.js";
|
|
11
5
|
|
|
12
6
|
export interface ArbiterPluginOptions {
|
|
@@ -16,85 +10,45 @@ export interface ArbiterPluginOptions {
|
|
|
16
10
|
readonly session?: RuleSession;
|
|
17
11
|
}
|
|
18
12
|
|
|
13
|
+
function resolveSession(options: ArbiterPluginOptions): { session: RuleSession; owned: boolean } {
|
|
14
|
+
if (options.session) return { session: options.session, owned: false };
|
|
15
|
+
if (options.rules) return { session: createSession({ rules: options.rules as ProductionRule[] }), owned: true };
|
|
16
|
+
throw new Error("createArbiterPlugin requires either `rules` or `session`");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function syncSession(session: RuleSession, ctx: PluginEvaluateContext): void {
|
|
20
|
+
const data = ctx.data as Record<string, unknown>;
|
|
21
|
+
for (const key of Object.keys(data)) session.assert(key, data[key]);
|
|
22
|
+
const uiState = ctx.uiState as Record<string, unknown>;
|
|
23
|
+
for (const key of Object.keys(uiState)) session.assert(`$ui.${key}`, uiState[key]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toWrites(result: FiringResult): readonly PluginWrite[] {
|
|
27
|
+
return result.changes
|
|
28
|
+
.filter((change) => !isArbiterInternalPath(change.path))
|
|
29
|
+
.map((change) => ({ path: change.path, value: change.newValue, mode: "set" as const }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function evaluateSession(session: RuleSession, ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {
|
|
33
|
+
if (ctx.origin.startsWith("plugin:arbiter")) return;
|
|
34
|
+
if (!ctx.change.dataChanged && !ctx.change.uiChanged) return;
|
|
35
|
+
syncSession(session, ctx);
|
|
36
|
+
const writes = toWrites(session.fire());
|
|
37
|
+
return { writes: writes.length > 0 ? writes : undefined };
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/**
|
|
20
41
|
* Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.
|
|
21
42
|
* Syncs form data into the rule session, fires rules, and converts results
|
|
22
|
-
* into PluginWrite[]
|
|
43
|
+
* into PluginWrite[] records.
|
|
23
44
|
*/
|
|
24
45
|
export function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {
|
|
25
|
-
const {
|
|
26
|
-
|
|
27
|
-
let session: RuleSession;
|
|
28
|
-
let ownsSession: boolean;
|
|
29
|
-
|
|
30
|
-
if (externalSession) {
|
|
31
|
-
session = externalSession;
|
|
32
|
-
ownsSession = false;
|
|
33
|
-
} else if (rules) {
|
|
34
|
-
session = createSession({ rules: rules as ProductionRule[] });
|
|
35
|
-
ownsSession = true;
|
|
36
|
-
} else {
|
|
37
|
-
throw new Error("createArbiterPlugin requires either `rules` or `session`");
|
|
38
|
-
}
|
|
39
|
-
|
|
46
|
+
const { session, owned } = resolveSession(options);
|
|
40
47
|
return {
|
|
41
48
|
id: "arbiter",
|
|
42
|
-
|
|
43
|
-
evaluate(ctx: PluginEvaluateContext): PluginEvaluateResult | undefined {
|
|
44
|
-
// Prevent re-entry from own writes
|
|
45
|
-
if (ctx.origin.startsWith("plugin:arbiter")) return;
|
|
46
|
-
|
|
47
|
-
// Short-circuit when nothing relevant changed
|
|
48
|
-
if (!ctx.change.dataChanged && !ctx.change.uiChanged) return;
|
|
49
|
-
|
|
50
|
-
// Sync form data fields into the session
|
|
51
|
-
const data = ctx.data as Record<string, unknown>;
|
|
52
|
-
for (const key of Object.keys(data)) {
|
|
53
|
-
session.assert(key, data[key]);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Sync $ui.* state
|
|
57
|
-
const uiState = ctx.uiState as Record<string, unknown>;
|
|
58
|
-
for (const key of Object.keys(uiState)) {
|
|
59
|
-
session.assert(`$ui.${key}`, uiState[key]);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Fire rules
|
|
63
|
-
const result: FiringResult = session.fire();
|
|
64
|
-
|
|
65
|
-
// Convert changes to PluginWrite[], filtering internal paths
|
|
66
|
-
const writes: PluginWrite[] = [];
|
|
67
|
-
for (const change of result.changes) {
|
|
68
|
-
if (isArbiterInternalPath(change.path)) continue;
|
|
69
|
-
writes.push({
|
|
70
|
-
path: change.path,
|
|
71
|
-
value: change.newValue,
|
|
72
|
-
mode: "set",
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Derive field meta from session state
|
|
77
|
-
const fieldMeta: Record<string, PluginFieldMeta> = {};
|
|
78
|
-
const state = session.getState();
|
|
79
|
-
for (const [path, value] of Object.entries(state)) {
|
|
80
|
-
if (!path.startsWith("$meta.")) continue;
|
|
81
|
-
// Convention: $meta.<fieldPath> holds { visible, disabled, required, readOnly, label }
|
|
82
|
-
const fieldPath = path.slice("$meta.".length);
|
|
83
|
-
if (typeof value === "object" && value !== null) {
|
|
84
|
-
fieldMeta[fieldPath] = value as PluginFieldMeta;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
writes: writes.length > 0 ? writes : undefined,
|
|
90
|
-
fieldMeta: Object.keys(fieldMeta).length > 0 ? fieldMeta : undefined,
|
|
91
|
-
};
|
|
92
|
-
},
|
|
93
|
-
|
|
49
|
+
evaluate: (ctx) => evaluateSession(session, ctx),
|
|
94
50
|
onDispose() {
|
|
95
|
-
if (
|
|
96
|
-
session.dispose();
|
|
97
|
-
}
|
|
51
|
+
if (owned) session.dispose();
|
|
98
52
|
},
|
|
99
53
|
};
|
|
100
54
|
}
|
|
@@ -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";
|