@formbar/arbiter 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ var core = require('@arbitre/core');
4
+ var kuery = require('kuery');
5
+
6
+ // src/arbiter-plugin.ts
7
+
8
+ // src/internal-paths.ts
9
+ function isArbiterInternalPath(path) {
10
+ return path.startsWith("$") && !path.startsWith("$ui.");
11
+ }
12
+
13
+ // src/arbiter-plugin.ts
14
+ function createArbiterPlugin(options) {
15
+ const { rules, session: externalSession } = options;
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
+ }
27
+ return {
28
+ 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
+ },
64
+ onDispose() {
65
+ if (ownsSession) {
66
+ session.dispose();
67
+ }
68
+ }
69
+ };
70
+ }
71
+ function evaluateExpression(expr, context) {
72
+ return kuery.evaluate(expr, context);
73
+ }
74
+
75
+ Object.defineProperty(exports, "assertSafeSegment", {
76
+ enumerable: true,
77
+ get: function () { return kuery.assertSafeSegment; }
78
+ });
79
+ exports.createArbiterPlugin = createArbiterPlugin;
80
+ exports.evaluateExpression = evaluateExpression;
81
+ exports.isArbiterInternalPath = isArbiterInternalPath;
82
+ //# sourceMappingURL=index.cjs.map
83
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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 | void {\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"]}
@@ -0,0 +1,31 @@
1
+ import { ProductionRule, RuleSession } from '@arbitre/core';
2
+ import { FormPlugin } from '@formbar/core';
3
+ import { ExprNode } from 'kuery';
4
+ export { ExprNode, assertSafeSegment } from 'kuery';
5
+
6
+ interface ArbiterPluginOptions {
7
+ /** Provide raw rules — a session will be created internally. */
8
+ readonly rules?: readonly ProductionRule[];
9
+ /** Provide a pre-configured session instead of raw rules. */
10
+ readonly session?: RuleSession;
11
+ }
12
+ /**
13
+ * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.
14
+ * Syncs form data into the rule session, fires rules, and converts results
15
+ * into PluginWrite[] and PluginFieldMeta records.
16
+ */
17
+ declare function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin;
18
+
19
+ /**
20
+ * Evaluate a kuery expression against a data context.
21
+ * This is a convenience re-export for consumers using expressions with arbiter rules.
22
+ */
23
+ declare function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown;
24
+
25
+ /**
26
+ * Arbiter-internal paths start with $ and are not user-facing form data.
27
+ * Filter them out of write intents.
28
+ */
29
+ declare function isArbiterInternalPath(path: string): boolean;
30
+
31
+ export { type ArbiterPluginOptions, createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
@@ -0,0 +1,31 @@
1
+ import { ProductionRule, RuleSession } from '@arbitre/core';
2
+ import { FormPlugin } from '@formbar/core';
3
+ import { ExprNode } from 'kuery';
4
+ export { ExprNode, assertSafeSegment } from 'kuery';
5
+
6
+ interface ArbiterPluginOptions {
7
+ /** Provide raw rules — a session will be created internally. */
8
+ readonly rules?: readonly ProductionRule[];
9
+ /** Provide a pre-configured session instead of raw rules. */
10
+ readonly session?: RuleSession;
11
+ }
12
+ /**
13
+ * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.
14
+ * Syncs form data into the rule session, fires rules, and converts results
15
+ * into PluginWrite[] and PluginFieldMeta records.
16
+ */
17
+ declare function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin;
18
+
19
+ /**
20
+ * Evaluate a kuery expression against a data context.
21
+ * This is a convenience re-export for consumers using expressions with arbiter rules.
22
+ */
23
+ declare function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown;
24
+
25
+ /**
26
+ * Arbiter-internal paths start with $ and are not user-facing form data.
27
+ * Filter them out of write intents.
28
+ */
29
+ declare function isArbiterInternalPath(path: string): boolean;
30
+
31
+ export { type ArbiterPluginOptions, createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import { createSession } from '@arbitre/core';
2
+ import { evaluate } from 'kuery';
3
+ export { assertSafeSegment } from 'kuery';
4
+
5
+ // src/arbiter-plugin.ts
6
+
7
+ // src/internal-paths.ts
8
+ function isArbiterInternalPath(path) {
9
+ return path.startsWith("$") && !path.startsWith("$ui.");
10
+ }
11
+
12
+ // src/arbiter-plugin.ts
13
+ function createArbiterPlugin(options) {
14
+ const { rules, session: externalSession } = options;
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
+ }
26
+ return {
27
+ 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
+ },
63
+ onDispose() {
64
+ if (ownsSession) {
65
+ session.dispose();
66
+ }
67
+ }
68
+ };
69
+ }
70
+ function evaluateExpression(expr, context) {
71
+ return evaluate(expr, context);
72
+ }
73
+
74
+ export { createArbiterPlugin, evaluateExpression, isArbiterInternalPath };
75
+ //# sourceMappingURL=index.js.map
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 | void {\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"]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@formbar/arbiter",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Arbiter rule engine bridge for @formbar/core — declarative UI governance via production rules",
6
+ "license": "MIT",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsc --noEmit",
24
+ "build:dist": "tsup",
25
+ "test": "vitest run"
26
+ },
27
+ "dependencies": {
28
+ "@formbar/core": "workspace:*"
29
+ },
30
+ "peerDependencies": {
31
+ "@arbitre/core": ">=0.1.0",
32
+ "kuery": ">=2.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@arbitre/core": "link:@arbitre/core",
36
+ "kuery": "^2.0.0"
37
+ }
38
+ }
@@ -0,0 +1,162 @@
1
+ import type { ProductionRule } from "@arbitre/core";
2
+ import { createForm } from "@formbar/core";
3
+ import { describe, expect, test } from "vitest";
4
+ import { createArbiterPlugin } from "../arbiter-plugin.js";
5
+
6
+ /**
7
+ * Reproduction test for demo 18 (arbiter visibility).
8
+ * Uses the exact same rules and initial state as the demo component
9
+ * to verify core logic works end-to-end through createForm + arbiter plugin.
10
+ */
11
+
12
+ interface FormData {
13
+ readonly country: string;
14
+ readonly state: string;
15
+ readonly province: string;
16
+ readonly region: string;
17
+ }
18
+
19
+ interface UiState {
20
+ readonly showState: boolean;
21
+ readonly showProvince: boolean;
22
+ }
23
+
24
+ const arbiterRules: readonly ProductionRule[] = [
25
+ {
26
+ name: "showUSState",
27
+ when: { country: "US" },
28
+ then: [{ $set: { "$ui.showState": true, "$ui.showProvince": false } }],
29
+ },
30
+ {
31
+ name: "showCAProvince",
32
+ when: { country: "CA" },
33
+ then: [{ $set: { "$ui.showState": false, "$ui.showProvince": true } }],
34
+ },
35
+ {
36
+ name: "hideRegional",
37
+ when: { country: { $nin: ["US", "CA"] } },
38
+ then: [{ $set: { "$ui.showState": false, "$ui.showProvince": false } }],
39
+ },
40
+ ];
41
+
42
+ function makeForm() {
43
+ return createForm<FormData, UiState>({
44
+ initialData: { country: "", state: "", province: "", region: "" },
45
+ initialUiState: { showState: false, showProvince: false },
46
+ plugins: [createArbiterPlugin({ rules: arbiterRules })],
47
+ });
48
+ }
49
+
50
+ describe("demo 18 arbiter visibility — direct state", () => {
51
+ test("initial state: both hidden", () => {
52
+ const form = makeForm();
53
+ const { uiState } = form.getState();
54
+ expect(uiState.showState).toBe(false);
55
+ expect(uiState.showProvince).toBe(false);
56
+ form.dispose();
57
+ });
58
+
59
+ test("select US: showState=true, showProvince=false", () => {
60
+ const form = makeForm();
61
+ form.setValue("country", "US");
62
+ const { uiState } = form.getState();
63
+ expect(uiState.showState).toBe(true);
64
+ expect(uiState.showProvince).toBe(false);
65
+ form.dispose();
66
+ });
67
+
68
+ test("switch US → CA: showState=false, showProvince=true", () => {
69
+ const form = makeForm();
70
+ form.setValue("country", "US");
71
+ form.setValue("country", "CA");
72
+ const { uiState } = form.getState();
73
+ expect(uiState.showState).toBe(false);
74
+ expect(uiState.showProvince).toBe(true);
75
+ form.dispose();
76
+ });
77
+
78
+ test("switch CA → UK: both hidden", () => {
79
+ const form = makeForm();
80
+ form.setValue("country", "US");
81
+ form.setValue("country", "CA");
82
+ form.setValue("country", "UK");
83
+ const { uiState } = form.getState();
84
+ expect(uiState.showState).toBe(false);
85
+ expect(uiState.showProvince).toBe(false);
86
+ form.dispose();
87
+ });
88
+
89
+ test("switch UK → US: showState=true again", () => {
90
+ const form = makeForm();
91
+ form.setValue("country", "US");
92
+ form.setValue("country", "CA");
93
+ form.setValue("country", "UK");
94
+ form.setValue("country", "US");
95
+ const { uiState } = form.getState();
96
+ expect(uiState.showState).toBe(true);
97
+ expect(uiState.showProvince).toBe(false);
98
+ form.dispose();
99
+ });
100
+
101
+ test("select empty string: both hidden", () => {
102
+ const form = makeForm();
103
+ form.setValue("country", "US");
104
+ form.setValue("country", "");
105
+ const { uiState } = form.getState();
106
+ expect(uiState.showState).toBe(false);
107
+ expect(uiState.showProvince).toBe(false);
108
+ form.dispose();
109
+ });
110
+ });
111
+
112
+ describe("demo 18 arbiter visibility — subscription", () => {
113
+ test("subscriber sees correct uiState after each setValue", () => {
114
+ const form = makeForm();
115
+ const snapshots: UiState[] = [];
116
+ form.subscribe(() => {
117
+ snapshots.push({ ...form.getState().uiState });
118
+ });
119
+
120
+ form.setValue("country", "US");
121
+ const afterUS = snapshots[snapshots.length - 1];
122
+ expect(afterUS.showState).toBe(true);
123
+ expect(afterUS.showProvince).toBe(false);
124
+
125
+ form.setValue("country", "CA");
126
+ const afterCA = snapshots[snapshots.length - 1];
127
+ expect(afterCA.showState).toBe(false);
128
+ expect(afterCA.showProvince).toBe(true);
129
+
130
+ form.setValue("country", "UK");
131
+ const afterUK = snapshots[snapshots.length - 1];
132
+ expect(afterUK.showState).toBe(false);
133
+ expect(afterUK.showProvince).toBe(false);
134
+
135
+ form.setValue("country", "US");
136
+ const afterUS2 = snapshots[snapshots.length - 1];
137
+ expect(afterUS2.showState).toBe(true);
138
+ expect(afterUS2.showProvince).toBe(false);
139
+
140
+ form.setValue("country", "");
141
+ const afterEmpty = snapshots[snapshots.length - 1];
142
+ expect(afterEmpty.showState).toBe(false);
143
+ expect(afterEmpty.showProvince).toBe(false);
144
+
145
+ form.dispose();
146
+ });
147
+
148
+ test("subscriber receives notifications for every transition", () => {
149
+ const form = makeForm();
150
+ let callCount = 0;
151
+ form.subscribe(() => {
152
+ callCount++;
153
+ });
154
+
155
+ form.setValue("country", "US");
156
+ form.setValue("country", "CA");
157
+ form.setValue("country", "UK");
158
+
159
+ expect(callCount).toBeGreaterThanOrEqual(3);
160
+ form.dispose();
161
+ });
162
+ });
@@ -0,0 +1,205 @@
1
+ import type { ProductionRule } from "@arbitre/core";
2
+ import { createSession } from "@arbitre/core";
3
+ import { createForm } from "@formbar/core";
4
+ import { describe, expect, test } from "vitest";
5
+ import { createArbiterPlugin } from "../arbiter-plugin.js";
6
+
7
+ describe("createArbiterPlugin", () => {
8
+ test("creates plugin from rules", () => {
9
+ const rules: readonly ProductionRule[] = [
10
+ {
11
+ name: "setTotal",
12
+ when: { qty: { $gt: 0 } },
13
+ then: [{ $set: { "$ui.showTotal": true } }],
14
+ },
15
+ ];
16
+ const plugin = createArbiterPlugin({ rules });
17
+ expect(plugin.id).toBe("arbiter");
18
+ expect(plugin.evaluate).toBeInstanceOf(Function);
19
+ });
20
+
21
+ test("plugin writes when rules fire via form", () => {
22
+ const rules: readonly ProductionRule[] = [
23
+ {
24
+ name: "showDiscount",
25
+ when: { qty: { $gte: 10 } },
26
+ then: [{ $set: { "$ui.showDiscount": true } }],
27
+ },
28
+ ];
29
+ const form = createForm({
30
+ initialData: { qty: 0 },
31
+ plugins: [createArbiterPlugin({ rules })],
32
+ });
33
+ form.setValue("qty", 15);
34
+ expect((form.getState().uiState as Record<string, unknown>).showDiscount).toBe(true);
35
+ form.dispose();
36
+ });
37
+
38
+ test("no writes when rules do not fire", () => {
39
+ const rules: readonly ProductionRule[] = [
40
+ {
41
+ name: "showDiscount",
42
+ when: { qty: { $gte: 10 } },
43
+ then: [{ $set: { "$ui.showDiscount": true } }],
44
+ },
45
+ ];
46
+ const form = createForm({
47
+ initialData: { qty: 3 },
48
+ plugins: [createArbiterPlugin({ rules })],
49
+ });
50
+ form.setValue("qty", 5);
51
+ expect((form.getState().uiState as Record<string, unknown>).showDiscount).toBeUndefined();
52
+ form.dispose();
53
+ });
54
+ });
55
+
56
+ describe("createArbiterPlugin with pre-configured session", () => {
57
+ test("wraps a pre-configured session", () => {
58
+ const session = createSession({
59
+ rules: [{ name: "r1", when: { x: 1 }, then: [{ $set: { "$ui.y": 2 } }] }],
60
+ });
61
+ const plugin = createArbiterPlugin({ session });
62
+ expect(plugin.id).toBe("arbiter");
63
+ });
64
+
65
+ test("filters out arbiter-internal namespace changes", () => {
66
+ const rules: readonly ProductionRule[] = [
67
+ {
68
+ name: "internalWrite",
69
+ when: {},
70
+ then: [
71
+ { $set: { "$state.counter": 1 } },
72
+ { $set: { "$ui.visible": true } },
73
+ { $set: { "$meta.timestamp": 999 } },
74
+ { $set: { "$contributions.source": "test" } },
75
+ { $set: { name: "kept" } },
76
+ ],
77
+ },
78
+ ];
79
+ const form = createForm({
80
+ initialData: { trigger: true, name: "" },
81
+ plugins: [createArbiterPlugin({ rules })],
82
+ });
83
+ form.setValue("trigger", false);
84
+ const state = form.getState();
85
+ // $ui writes should pass through
86
+ expect((state.uiState as Record<string, unknown>).visible).toBe(true);
87
+ // data writes should pass through
88
+ expect((state.data as Record<string, unknown>).name).toBe("kept");
89
+ form.dispose();
90
+ });
91
+ });
92
+
93
+ describe("createForm with arbiter plugin", () => {
94
+ test("form with arbiter plugin evaluates on setValue", () => {
95
+ const rules: readonly ProductionRule[] = [
96
+ {
97
+ name: "calcTotal",
98
+ when: {},
99
+ then: [{ $set: { "$ui.evaluated": true } }],
100
+ },
101
+ ];
102
+ const form = createForm({
103
+ initialData: { qty: 0 },
104
+ plugins: [createArbiterPlugin({ rules })],
105
+ });
106
+ form.setValue("qty", 5);
107
+ const state = form.getState();
108
+ expect((state.uiState as Record<string, unknown>).evaluated).toBe(true);
109
+ form.dispose();
110
+ });
111
+
112
+ test("form without plugins skips expression step", () => {
113
+ const form = createForm({ initialData: { x: 1 } });
114
+ const result = form.setValue("x", 2);
115
+ expect(result.ok).toBe(true);
116
+ expect(form.getState().data).toEqual({ x: 2 });
117
+ form.dispose();
118
+ });
119
+
120
+ test("arbiter plugin can write to data namespace", () => {
121
+ const rules: readonly ProductionRule[] = [
122
+ {
123
+ name: "setLabel",
124
+ when: {},
125
+ then: [{ $set: { label: "computed" } }],
126
+ },
127
+ ];
128
+ const form = createForm({
129
+ initialData: { name: "test", label: "" },
130
+ plugins: [createArbiterPlugin({ rules })],
131
+ });
132
+ form.setValue("name", "hello");
133
+ expect((form.getState().data as Record<string, unknown>).label).toBe("computed");
134
+ form.dispose();
135
+ });
136
+
137
+ test("form with arbiterPlugin from session", () => {
138
+ const session = createSession({
139
+ rules: [{ name: "r1", when: {}, then: [{ $set: { "$ui.ready": true } }] }],
140
+ });
141
+ const form = createForm({
142
+ initialData: { x: 0 },
143
+ plugins: [createArbiterPlugin({ session })],
144
+ });
145
+ form.setValue("x", 1);
146
+ expect((form.getState().uiState as Record<string, unknown>).ready).toBe(true);
147
+ form.dispose();
148
+ });
149
+
150
+ test("data writes apply to all fields including user-edited field", () => {
151
+ const rules: readonly ProductionRule[] = [
152
+ {
153
+ name: "resetQty",
154
+ when: {},
155
+ then: [{ $set: { qty: 0 } }],
156
+ },
157
+ ];
158
+ const form = createForm({
159
+ initialData: { qty: 0, label: "" },
160
+ plugins: [createArbiterPlugin({ rules })],
161
+ });
162
+ form.setValue("qty", 5);
163
+ // Plugin writes apply unconditionally — filtering is plugin responsibility
164
+ expect((form.getState().data as Record<string, unknown>).qty).toBe(0);
165
+ form.dispose();
166
+ });
167
+
168
+ test("arbiter writes to OTHER fields still apply when user edits a field", () => {
169
+ const rules: readonly ProductionRule[] = [
170
+ {
171
+ name: "computeLabel",
172
+ when: {},
173
+ then: [{ $set: { label: "computed" } }],
174
+ },
175
+ ];
176
+ const form = createForm({
177
+ initialData: { qty: 0, label: "" },
178
+ plugins: [createArbiterPlugin({ rules })],
179
+ });
180
+ form.setValue("qty", 5);
181
+ expect((form.getState().data as Record<string, unknown>).label).toBe("computed");
182
+ form.dispose();
183
+ });
184
+
185
+ test("uiState retracts when rule condition becomes false", () => {
186
+ const rules: readonly ProductionRule[] = [
187
+ {
188
+ name: "showDiscount",
189
+ when: { qty: { $gte: 10 } },
190
+ then: [{ $set: { "$ui.showDiscount": true } }],
191
+ },
192
+ ];
193
+ const form = createForm({
194
+ initialData: { qty: 15 },
195
+ plugins: [createArbiterPlugin({ rules })],
196
+ });
197
+
198
+ form.setValue("qty", 15);
199
+ expect((form.getState().uiState as Record<string, unknown>).showDiscount).toBe(true);
200
+
201
+ form.setValue("qty", 3);
202
+ expect((form.getState().uiState as Record<string, unknown>).showDiscount).toBeUndefined();
203
+ form.dispose();
204
+ });
205
+ });
@@ -0,0 +1,100 @@
1
+ import { createSession } from "@arbitre/core";
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";
10
+ import { isArbiterInternalPath } from "./internal-paths.js";
11
+
12
+ export interface ArbiterPluginOptions {
13
+ /** Provide raw rules — a session will be created internally. */
14
+ readonly rules?: readonly ProductionRule[];
15
+ /** Provide a pre-configured session instead of raw rules. */
16
+ readonly session?: RuleSession;
17
+ }
18
+
19
+ /**
20
+ * Creates a FormPlugin that bridges @arbitre/core into the formbar pipeline.
21
+ * Syncs form data into the rule session, fires rules, and converts results
22
+ * into PluginWrite[] and PluginFieldMeta records.
23
+ */
24
+ export function createArbiterPlugin(options: ArbiterPluginOptions): FormPlugin {
25
+ const { rules, session: externalSession } = options;
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
+
40
+ return {
41
+ id: "arbiter",
42
+
43
+ evaluate(ctx: PluginEvaluateContext): PluginEvaluateResult | void {
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
+
94
+ onDispose() {
95
+ if (ownsSession) {
96
+ session.dispose();
97
+ }
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,13 @@
1
+ import { assertSafeSegment, evaluate } from "kuery";
2
+ import type { ExprNode } from "kuery";
3
+
4
+ /**
5
+ * Evaluate a kuery expression against a data context.
6
+ * This is a convenience re-export for consumers using expressions with arbiter rules.
7
+ */
8
+ export function evaluateExpression(expr: ExprNode, context: Record<string, unknown>): unknown {
9
+ return evaluate(expr, context);
10
+ }
11
+
12
+ export { assertSafeSegment };
13
+ export type { ExprNode };
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { createArbiterPlugin } from "./arbiter-plugin.js";
2
+ export type { ArbiterPluginOptions } from "./arbiter-plugin.js";
3
+ export { evaluateExpression, assertSafeSegment } from "./expression-utils.js";
4
+ export type { ExprNode } from "./expression-utils.js";
5
+ export { isArbiterInternalPath } from "./internal-paths.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Arbiter-internal paths start with $ and are not user-facing form data.
3
+ * Filter them out of write intents.
4
+ */
5
+ export function isArbiterInternalPath(path: string): boolean {
6
+ return path.startsWith("$") && !path.startsWith("$ui.");
7
+ }