@magicvr/schema-ui-renderer 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/index.js +11788 -0
- package/package.json +26 -0
- package/renderer/confirm.d.ts +17 -0
- package/renderer/custom-components.d.ts +14 -0
- package/renderer/form-controls.d.ts +45 -0
- package/renderer/form-controls.types.d.ts +135 -0
- package/renderer/index.d.ts +13 -0
- package/renderer/modal.d.ts +6 -0
- package/renderer/permissions.d.ts +53 -0
- package/renderer/reaction-engine.d.ts +92 -0
- package/renderer/reaction-expression.d.ts +74 -0
- package/renderer/reactions.d.ts +64 -0
- package/renderer/render.d.ts +194 -0
- package/renderer/render.types.d.ts +289 -0
- package/renderer/resource.d.ts +114 -0
- package/renderer/row-action.d.ts +28 -0
- package/renderer/schema-table.d.ts +90 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magicvr/schema-ui-renderer",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "schema-ui-core 渲染器(试点 v0.1.0 · GOAL-004 S3 · 粗粒度单包:RenderPage + I18nProvider + 扩展接缝;React 为 peer)",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "./renderer/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./renderer/index.d.ts",
|
|
11
|
+
"import": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"renderer/"
|
|
21
|
+
],
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm dialog for row actions that declare a `confirm` string (S4 · GOAL-007).
|
|
3
|
+
*
|
|
4
|
+
* One-time renderer completion allowed by I-007-003 §9.5 (`confirm*.tsx`). The
|
|
5
|
+
* message text comes from the schema row action entry (`table.props.actions[].
|
|
6
|
+
* confirm` / `confirmKey`), so the dialog stays fixture-driven; cancelling
|
|
7
|
+
* mirrors the frozen `executeAction` CONFIRM_CANCELLED path and never issues
|
|
8
|
+
* a request.
|
|
9
|
+
*
|
|
10
|
+
* W14 F-12 (GOAL-018): keyboard/accessibility — initial focus on Cancel, ESC
|
|
11
|
+
* cancels, and Tab is trapped inside the dialog.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ConfirmDialog({ message, onConfirm, onCancel, }: {
|
|
14
|
+
message: string;
|
|
15
|
+
onConfirm: () => void;
|
|
16
|
+
onCancel: () => void;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { RenderCustomNode, RenderNode } from "@/renderer/render.types";
|
|
3
|
+
export interface CustomComponentProps {
|
|
4
|
+
node: RenderCustomNode;
|
|
5
|
+
context: Record<string, unknown>;
|
|
6
|
+
/** Children declared under the custom node (rarely used). */
|
|
7
|
+
children?: RenderNode[];
|
|
8
|
+
}
|
|
9
|
+
/** Registers a custom node component (idempotent; later registrations win). */
|
|
10
|
+
export declare function registerCustomComponent(key: string, component: ComponentType<CustomComponentProps>): void;
|
|
11
|
+
/** Returns the registered component for a key, or null when unregistered. */
|
|
12
|
+
export declare function getCustomComponent(key: string): ComponentType<CustomComponentProps> | null;
|
|
13
|
+
/** Test-only: clears the registry. */
|
|
14
|
+
export declare function resetCustomComponentsForTests(): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type MessageParams } from "@/i18n/catalog";
|
|
3
|
+
import type { UploadableFile } from "@/protocol/conformance/upload-orchestration";
|
|
4
|
+
import { type FormControlField } from "@/renderer/form-controls.types";
|
|
5
|
+
export interface FormControlsProps {
|
|
6
|
+
fields: FormControlField[];
|
|
7
|
+
values: Record<string, unknown>;
|
|
8
|
+
onChange: (id: string, value: unknown) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Per-field disabled override (R5 renderer reaction state). */
|
|
11
|
+
fieldDisabled?: (id: string) => boolean;
|
|
12
|
+
idPrefix?: string;
|
|
13
|
+
/** Upload control transport (ADR-0012): validates + uploads + returns the field value. */
|
|
14
|
+
onUpload?: (field: FormControlField, files: UploadableFile[]) => Promise<unknown>;
|
|
15
|
+
/** GOAL-014 D-002 §4: inline field errors keyed by field id (submit-time
|
|
16
|
+
* validation + server fieldErrors echo). */
|
|
17
|
+
fieldErrors?: Record<string, string>;
|
|
18
|
+
/** GOAL-014 D-002 §4: column count (default 1 = single-column layout).
|
|
19
|
+
* >1 enables a responsive grid; the mobile layout stays single-column. */
|
|
20
|
+
columns?: number;
|
|
21
|
+
/** W11 · U-01/U-02: auth-aware transport for dynamic option sources
|
|
22
|
+
* (optionsSource); defaults to globalThis.fetch. */
|
|
23
|
+
fetcher?: typeof fetch;
|
|
24
|
+
/**
|
|
25
|
+
* A-003 (GOAL-013 audit response): search-mode presentation — compact
|
|
26
|
+
* responsive auto-grid (1..5 columns), keyword input with search prefix
|
|
27
|
+
* icon + clear affordance. Search schemas keep their exact JSON shape.
|
|
28
|
+
*/
|
|
29
|
+
searchMode?: boolean;
|
|
30
|
+
/** A-003: action cluster (Reset button) rendered inside the
|
|
31
|
+
* search-mode grid so it aligns with the field row. */
|
|
32
|
+
actionSlot?: ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* A-003 (user pairing rule): one search button rendered side-by-side with
|
|
35
|
+
* EVERY keyword input field in search mode — as many buttons as there are
|
|
36
|
+
* text inputs, each pair adjacent in the same grid cell.
|
|
37
|
+
*/
|
|
38
|
+
searchButtonSlot?: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
export type FieldTranslator = (key: string, params?: MessageParams, literalFallback?: string) => string;
|
|
41
|
+
export declare function optionList(field: FormControlField, t: FieldTranslator): Array<{
|
|
42
|
+
value: string;
|
|
43
|
+
label: string;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function FormControls({ fields, values, onChange, disabled, fieldDisabled, idPrefix, onUpload, fieldErrors, columns, fetcher, searchMode: searchModeProp, actionSlot, searchButtonSlot, }: FormControlsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-FORM form control surface (frozen §5 whitelist + I-PROTO-FULL-001 full
|
|
3
|
+
* registry surface).
|
|
4
|
+
*
|
|
5
|
+
* Wire rules from schema-ui-docs@2.7.0 (fixed commit ca9e5fe…):
|
|
6
|
+
* - base: input → string, select (single) → string, inputNumber → number,
|
|
7
|
+
* datePicker → ISO 8601 string, dateRangePicker → {start,end} pair bound
|
|
8
|
+
* to startField/endField (registry props; no single-field wire)
|
|
9
|
+
* - 2.6 (capability form.controls.extended): textarea → string, switch → boolean,
|
|
10
|
+
* checkbox → boolean, radio → single string, select.mode=multiple → string[]
|
|
11
|
+
* - 2.7 (capability form.controls.advanced): cascader → path string[],
|
|
12
|
+
* checkboxGroup → value string[], richText → markdown string, password → string
|
|
13
|
+
* - 2.7 props.defaultValue must match the field's wire type
|
|
14
|
+
*
|
|
15
|
+
* The schema-driven page gate (meta.protocolVersion + requiredCapabilities) is
|
|
16
|
+
* enforced by checkFormCapabilities; the Renderer-level page gate lands in 2c.
|
|
17
|
+
*/
|
|
18
|
+
export type FormControlType = "input" | "select" | "inputNumber" | "datePicker" | "dateRangePicker" | "textarea" | "switch" | "checkbox" | "radio" | "cascader" | "checkboxGroup" | "richText" | "password" | "upload";
|
|
19
|
+
export declare const FORM_CONTROLS_EXTENDED_CAPABILITY = "form.controls.extended";
|
|
20
|
+
export declare const FORM_CONTROLS_ADVANCED_CAPABILITY = "form.controls.advanced";
|
|
21
|
+
/** ADR-0021: `form.props.recordSource` prefill GET (registry since 2.1). */
|
|
22
|
+
export declare const FORM_RECORD_LOAD_CAPABILITY = "form.record.load";
|
|
23
|
+
/** ADR-0040 (since 2.9): `readOnly` field declaration (value still projects). */
|
|
24
|
+
export declare const FORM_CONTROLS_READONLY_CAPABILITY = "form.controls.readonly";
|
|
25
|
+
export type WireKind = "string" | "boolean" | "string-array" | "number" | "date-range";
|
|
26
|
+
export interface FormOption {
|
|
27
|
+
value: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
/** S2 (VP-007): i18n key resolved before `label` (upstream registry field). */
|
|
30
|
+
labelKey?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface DateRangeValue {
|
|
33
|
+
start: string;
|
|
34
|
+
end: string;
|
|
35
|
+
}
|
|
36
|
+
export interface FormControlField {
|
|
37
|
+
id: string;
|
|
38
|
+
label?: string;
|
|
39
|
+
/** S2 (VP-007): i18n key resolved before `label` (missing-key observable). */
|
|
40
|
+
labelKey?: string;
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
/** S2 (VP-007): i18n key resolved before `placeholder` (local doc convention). */
|
|
43
|
+
placeholderKey?: string;
|
|
44
|
+
type: FormControlType;
|
|
45
|
+
/** select only: single (default) or multiple. */
|
|
46
|
+
mode?: "single" | "multiple";
|
|
47
|
+
options?: FormOption[];
|
|
48
|
+
/**
|
|
49
|
+
* W11 · U-01/U-02 — dynamic option source, aligned with the upstream
|
|
50
|
+
* registry shape (component-registry.json, since 0.2): an object with a
|
|
51
|
+
* required single-slash same-origin url plus the response item fields used
|
|
52
|
+
* for value/label. The response is {items:[...]} (or a bare array); while
|
|
53
|
+
* the source loads, static options (if any) remain the fallback; an invalid
|
|
54
|
+
* source or failed fetch fails closed to an empty option set.
|
|
55
|
+
*/
|
|
56
|
+
optionsSource?: {
|
|
57
|
+
url: string;
|
|
58
|
+
/** Optional scalar query params appended to url (e.g. pageSize). */
|
|
59
|
+
params?: Record<string, string | number | boolean | null>;
|
|
60
|
+
labelField: string;
|
|
61
|
+
valueField: string;
|
|
62
|
+
};
|
|
63
|
+
defaultValue?: unknown;
|
|
64
|
+
/** dateRangePicker only: the two bound output fields (registry props). */
|
|
65
|
+
startField?: string;
|
|
66
|
+
endField?: string;
|
|
67
|
+
/** inputNumber constraints (registry props, since 0.2.1). */
|
|
68
|
+
min?: number;
|
|
69
|
+
max?: number;
|
|
70
|
+
step?: number;
|
|
71
|
+
precision?: number;
|
|
72
|
+
/** GOAL-014 D-002 §3: field-level validation constraints (optional). */
|
|
73
|
+
required?: boolean;
|
|
74
|
+
/** ADR-0040 (since 2.9): read-only field — user cannot edit, value still
|
|
75
|
+
* participates in values and the submit projection (bodyMapping);
|
|
76
|
+
* recordSource backfill and reactions keep writing. Requires protocol
|
|
77
|
+
* >= 2.9 and form.controls.readonly. */
|
|
78
|
+
readOnly?: boolean;
|
|
79
|
+
/** Regex pattern for string-typed fields (submit-time validation). */
|
|
80
|
+
pattern?: string;
|
|
81
|
+
/** String length bounds for input/textarea. */
|
|
82
|
+
minLength?: number;
|
|
83
|
+
maxLength?: number;
|
|
84
|
+
/** datePicker display format (display-only; data stays ISO 8601). */
|
|
85
|
+
format?: string;
|
|
86
|
+
/** upload only: direct-URL mode (registry oneOf with actionRef). */
|
|
87
|
+
action?: string;
|
|
88
|
+
/** upload only: references a top-level type=upload action (requires actions.upload). */
|
|
89
|
+
actionRef?: string;
|
|
90
|
+
/**
|
|
91
|
+
* W17: optional Host-local addon rendered under this field. The value is a
|
|
92
|
+
* registered custom-component key (e.g. cron-preview). Not a protocol
|
|
93
|
+
* control type.
|
|
94
|
+
*/
|
|
95
|
+
afterComponent?: string;
|
|
96
|
+
/** upload constraints (direct-URL mode only; actionRef mode reads the action). */
|
|
97
|
+
accept?: string;
|
|
98
|
+
maxSize?: number;
|
|
99
|
+
multiple?: boolean;
|
|
100
|
+
}
|
|
101
|
+
export interface FormControlMeta {
|
|
102
|
+
protocolVersion: string;
|
|
103
|
+
requiredCapabilities: string[];
|
|
104
|
+
}
|
|
105
|
+
export interface FormControlGateError {
|
|
106
|
+
code: string;
|
|
107
|
+
path: string;
|
|
108
|
+
message: string;
|
|
109
|
+
}
|
|
110
|
+
export declare function isWhitelistedFormControl(type: string): type is FormControlType;
|
|
111
|
+
export declare function wireKindOf(field: FormControlField): WireKind;
|
|
112
|
+
/** Coerces a raw control value to its wire kind; defaultValue applies when raw is empty. */
|
|
113
|
+
export declare function coerceFieldValue(field: FormControlField, raw: unknown): unknown;
|
|
114
|
+
/** Fails closed when defaultValue does not match the field's wire type. */
|
|
115
|
+
export declare function validateDefaultValue(field: FormControlField): FormControlGateError | null;
|
|
116
|
+
/** Gates a control set against page meta (P-005 / frozen capability rules). */
|
|
117
|
+
export declare function checkFormCapabilities(meta: FormControlMeta, fields: FormControlField[]): FormControlGateError[];
|
|
118
|
+
/** Validates an arbitrary page-meta value without assuming record types. */
|
|
119
|
+
export declare function checkFormCapabilitiesRaw(metaValue: unknown, fields: FormControlField[]): FormControlGateError[];
|
|
120
|
+
/** One submit-time field validation failure (GOAL-014 D-002 §3). */
|
|
121
|
+
export interface FieldValidationError {
|
|
122
|
+
field: string;
|
|
123
|
+
code: "REQUIRED" | "PATTERN" | "MIN_LENGTH" | "MAX_LENGTH" | "MIN_VALUE" | "MAX_VALUE";
|
|
124
|
+
/** Stable i18n key (form.validation.*); message is the en fallback. */
|
|
125
|
+
messageKey: string;
|
|
126
|
+
message: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Validates form values against each field's declared constraints (GOAL-014
|
|
130
|
+
* D-002 §3.1). Pure function: returns all failures (not just the first) so
|
|
131
|
+
* the host can inline every field error. Boolean fields (switch/checkbox)
|
|
132
|
+
* never fail REQUIRED — their wire kind is boolean and the toggle has an
|
|
133
|
+
* explicit state.
|
|
134
|
+
*/
|
|
135
|
+
export declare function validateFieldValues(fields: FormControlField[], values: Record<string, unknown>): FieldValidationError[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @schema-ui/renderer 聚合导出(试点 v0.1.0 · GOAL-004 S3 · 粗粒度单包)。
|
|
3
|
+
*
|
|
4
|
+
* 渲染闭环:RenderPage(schema 文档 → DOM)+ I18nProvider(i18n 运行时面)
|
|
5
|
+
* + 扩展接缝(registerCustomComponent)+ 核心类型。
|
|
6
|
+
* 内部 bundle 面(components/i18n/lib/protocol)不导出为包级 API。
|
|
7
|
+
*/
|
|
8
|
+
export { RenderPage, useSchemaCrud, SchemaCrudContext, type RendererComponentProps, type SchemaCrudFeedback, type SchemaCrudConfirm, type TableSelection, type ActionResult, type SchemaCrudValue, type RunRequestOptions, } from "./render.tsx";
|
|
9
|
+
export { registerCustomComponent } from "./custom-components";
|
|
10
|
+
export type * from "./render.types";
|
|
11
|
+
export { I18nProvider, useI18n, getActiveLocale, setActiveLocale, applyLocaleToDocument, type I18nProviderProps, type I18nState, } from "@/i18n/runtime";
|
|
12
|
+
export { resolveTextProp, type MessageParams } from "@/i18n/catalog";
|
|
13
|
+
export type { Locale, LocalePreference } from "@/i18n/locale";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type NavigationContext } from "@/protocol/app-manifest";
|
|
2
|
+
/**
|
|
3
|
+
* R4 D-PERM permission evaluation engine (ADR-0023, frozen by GOAL-006 D-004).
|
|
4
|
+
*
|
|
5
|
+
* Coverage scope: the minimal renderer subset exercised by the frozen
|
|
6
|
+
* permissions-inheritance fixtures — permissionCascade / permissionIntent
|
|
7
|
+
* validation (fail-closed error codes) and effectivePermission evaluation for
|
|
8
|
+
* the approved target kinds (formField / formSubmit / rowAction /
|
|
9
|
+
* toolbarTrigger / actionButton). Execution-time gating (visibleWhen →
|
|
10
|
+
* permission → disabled/requiresSelection → confirm → action) is modeled for
|
|
11
|
+
* rowAction / toolbarTrigger / actionButton / default form submit.
|
|
12
|
+
*/
|
|
13
|
+
export type PermissionKey = "view" | "edit" | "delete";
|
|
14
|
+
export type L2ErrorCode = "PROTOCOL_VERSION_TOO_LOW" | "CAPABILITY_REQUIRED" | "PERMISSION_CASCADE_TYPE_INVALID" | "PERMISSION_CASCADE_KEYS_INVALID" | "PERMISSION_CASCADE_SOURCE_MISSING" | "PERMISSION_INTENT_FORBIDDEN" | "PERMISSION_INTENT_INVALID";
|
|
15
|
+
export interface L2Error {
|
|
16
|
+
code: L2ErrorCode;
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PermissionTarget {
|
|
20
|
+
targetId: string;
|
|
21
|
+
kind: "formField" | "formSubmit" | "rowAction" | "toolbarTrigger" | "actionButton" | "column";
|
|
22
|
+
key: PermissionKey;
|
|
23
|
+
cascadeApplied: boolean;
|
|
24
|
+
cascadedBy: string[];
|
|
25
|
+
effectivePermission: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ExecutionRequest {
|
|
28
|
+
targetId: string;
|
|
29
|
+
visible: boolean;
|
|
30
|
+
confirm?: boolean;
|
|
31
|
+
confirmed?: boolean;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
requiresSelection?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface ExecutionResult {
|
|
36
|
+
outcome: "EXECUTED" | "BLOCKED" | "CONFIRM_CANCELLED";
|
|
37
|
+
reason?: "NOT_VISIBLE" | "PERMISSION_DENIED" | "DISABLED";
|
|
38
|
+
events: Array<{
|
|
39
|
+
type: "confirmShown" | "actionExecuted";
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
type JsonRecord = Record<string, unknown>;
|
|
43
|
+
/** Validates the permission fields of a page; returns all L2 errors. */
|
|
44
|
+
export declare function validatePermissions(page: JsonRecord): L2Error[];
|
|
45
|
+
/**
|
|
46
|
+
* Evaluates the effective permission of every approved target in the page
|
|
47
|
+
* against the frozen $context snapshot. Targets in modal content and
|
|
48
|
+
* navigated pages start new roots (ADR-0023 D2a).
|
|
49
|
+
*/
|
|
50
|
+
export declare function evaluatePermissionTargets(page: JsonRecord, context: NavigationContext): PermissionTarget[];
|
|
51
|
+
/** Gates an action: visible → permission → disabled/requiresSelection → confirm. */
|
|
52
|
+
export declare function executeAction(page: JsonRecord, request: ExecutionRequest, context: NavigationContext): ExecutionResult;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-round snapshot reaction engine (schema-ui-docs@2.7.0 · 02-reaction-expression.md §14).
|
|
3
|
+
*
|
|
4
|
+
* Implements the upstream conformance contract (`conformance/fixtures/reactions/cases.json`):
|
|
5
|
+
* Snapshot → Evaluate → Commit → Next tick, with:
|
|
6
|
+
* - per-field reactions [{ when, fulfill, otherwise }] (value/visible/required/disabled)
|
|
7
|
+
* - observers evaluated against every round snapshot
|
|
8
|
+
* - baselines + resetMissingOtherwise (condition false without otherwise → restore baseline;
|
|
9
|
+
* explicit writes win over implicit baseline restores)
|
|
10
|
+
* - externalUpdates applied between rounds
|
|
11
|
+
* - deep-equal change detection (no commit / no next round on no-op writes)
|
|
12
|
+
* - loop protection (REACTION_LOOP_LIMIT after maxRounds, default 10)
|
|
13
|
+
* - MULTIPLE_VALUE_WRITES warnings (last write wins, deterministic order)
|
|
14
|
+
*/
|
|
15
|
+
export interface ReactionRuleInput {
|
|
16
|
+
when: string;
|
|
17
|
+
fulfill?: Record<string, unknown>;
|
|
18
|
+
otherwise?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface FieldReactionInput {
|
|
21
|
+
field: string;
|
|
22
|
+
reactions: ReactionRuleInput[];
|
|
23
|
+
}
|
|
24
|
+
export interface ReactionObserverInput {
|
|
25
|
+
id: string;
|
|
26
|
+
when: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ExternalUpdateInput {
|
|
29
|
+
afterRound: number;
|
|
30
|
+
values: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
export interface ReactionEngineInput {
|
|
33
|
+
initialValues: Record<string, unknown>;
|
|
34
|
+
fields?: FieldReactionInput[];
|
|
35
|
+
observers?: ReactionObserverInput[];
|
|
36
|
+
baselines?: Record<string, unknown>;
|
|
37
|
+
resetMissingOtherwise?: boolean;
|
|
38
|
+
externalUpdates?: ExternalUpdateInput[];
|
|
39
|
+
maxRounds?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface ReactionRound {
|
|
42
|
+
round: number;
|
|
43
|
+
snapshot: Record<string, unknown>;
|
|
44
|
+
observations: Record<string, boolean>;
|
|
45
|
+
commits: Array<{
|
|
46
|
+
field: string;
|
|
47
|
+
value: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
export interface MultipleValueWritesWarning {
|
|
51
|
+
code: "MULTIPLE_VALUE_WRITES";
|
|
52
|
+
field: string;
|
|
53
|
+
count: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ReactionEngineOk {
|
|
56
|
+
ok: true;
|
|
57
|
+
values: Record<string, unknown>;
|
|
58
|
+
rounds: ReactionRound[];
|
|
59
|
+
warnings: MultipleValueWritesWarning[];
|
|
60
|
+
}
|
|
61
|
+
export interface ReactionEngineLoopError {
|
|
62
|
+
ok: false;
|
|
63
|
+
code: "REACTION_LOOP_LIMIT";
|
|
64
|
+
maxRounds: number;
|
|
65
|
+
values: Record<string, unknown>;
|
|
66
|
+
roundCount: number;
|
|
67
|
+
dependencyFields: string[];
|
|
68
|
+
}
|
|
69
|
+
export type ReactionEngineResult = ReactionEngineOk | ReactionEngineLoopError;
|
|
70
|
+
/** Per-field control state after convergence (renderer integration). */
|
|
71
|
+
export interface ReactionFieldState {
|
|
72
|
+
visible?: boolean;
|
|
73
|
+
required?: boolean;
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface ReactionEngineDetailed {
|
|
77
|
+
result: ReactionEngineResult;
|
|
78
|
+
fieldStates: Record<string, ReactionFieldState>;
|
|
79
|
+
}
|
|
80
|
+
export declare const DEFAULT_MAX_ROUNDS = 10;
|
|
81
|
+
/**
|
|
82
|
+
* Runs the multi-round engine.
|
|
83
|
+
*
|
|
84
|
+
* A field's committed `value` only lands in `commits` when it deep-differs
|
|
85
|
+
* from the snapshot value; a no-op write neither commits nor schedules the
|
|
86
|
+
* next round. Rounds stop when no committed field is a dependency of any
|
|
87
|
+
* expression (reaction when or observer when).
|
|
88
|
+
*/
|
|
89
|
+
export declare function runReactionEngine(input: ReactionEngineInput): ReactionEngineResult;
|
|
90
|
+
/** Engine + per-field control state (visible/required/disabled/value) from
|
|
91
|
+
* the final round — used by the Renderer's form integration. */
|
|
92
|
+
export declare function runReactionEngineDetailed(input: ReactionEngineInput): ReactionEngineDetailed;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full protocol expression engine (schema-ui-docs@2.7.0 · docs/02-reaction-expression.md).
|
|
3
|
+
*
|
|
4
|
+
* Whitelisted grammar (no eval / new Function):
|
|
5
|
+
* - namespaces: $deps.<field>, $self, $context.user.* / $context.features.*
|
|
6
|
+
* - operators: == != > >= < <= contains && || !
|
|
7
|
+
* - grouping: ( )
|
|
8
|
+
* - literals: 'str' | "str" | number (int/float/exponent) | true | false | null
|
|
9
|
+
*
|
|
10
|
+
* Semantics (ADR-0016 / §7-§14):
|
|
11
|
+
* - strict typing, no coercion: 1 == 1.0 true; 1 == '1' false; true == 1 false
|
|
12
|
+
* - contains: array left operand, strict element equality; non-array → false
|
|
13
|
+
* - string ordering by Unicode code points (not UTF-16 code units)
|
|
14
|
+
* - undefined values compare as false (missing deps never throw)
|
|
15
|
+
*/
|
|
16
|
+
export type ExprValue = string | number | boolean | null | ExprValue[] | {
|
|
17
|
+
[key: string]: ExprValue;
|
|
18
|
+
} | undefined;
|
|
19
|
+
export interface ReactionEnv {
|
|
20
|
+
/** $deps.<field> values (form field snapshot). */
|
|
21
|
+
deps?: Record<string, unknown>;
|
|
22
|
+
/** $self (current field value). */
|
|
23
|
+
self?: unknown;
|
|
24
|
+
/** $context.user.* / $context.features.* snapshots. */
|
|
25
|
+
context?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export type ParseError = {
|
|
28
|
+
ok: false;
|
|
29
|
+
code: "SYNTAX";
|
|
30
|
+
message: string;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
code: "FORBIDDEN_VARIABLE";
|
|
34
|
+
message: string;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
code: "UNSUPPORTED_OPERATOR";
|
|
38
|
+
message: string;
|
|
39
|
+
};
|
|
40
|
+
export type ParsedExpr = {
|
|
41
|
+
kind: "literal";
|
|
42
|
+
value: ExprValue;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "var";
|
|
45
|
+
path: string[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: "self";
|
|
48
|
+
} | {
|
|
49
|
+
kind: "not";
|
|
50
|
+
operand: ParsedExpr;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "and";
|
|
53
|
+
left: ParsedExpr;
|
|
54
|
+
right: ParsedExpr;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "or";
|
|
57
|
+
left: ParsedExpr;
|
|
58
|
+
right: ParsedExpr;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "compare";
|
|
61
|
+
op: "==" | "!=" | ">" | ">=" | "<" | "<=" | "contains";
|
|
62
|
+
left: ParsedExpr;
|
|
63
|
+
right: ParsedExpr;
|
|
64
|
+
};
|
|
65
|
+
/** Parses a whitelisted expression; returns the AST or a typed error. */
|
|
66
|
+
export declare function parseExpression(source: string): ParsedExpr | ParseError;
|
|
67
|
+
/** Deep equality (arrays/objects by structure; scalars by strict identity). */
|
|
68
|
+
export declare function deepEqual(left: unknown, right: unknown): boolean;
|
|
69
|
+
/** Evaluates a whitelisted expression to a boolean; invalid input → false. */
|
|
70
|
+
export declare function evaluateFullExpression(source: string, env: ReactionEnv): boolean;
|
|
71
|
+
/** True when `source` parses under the full grammar. */
|
|
72
|
+
export declare function isValidFullExpression(source: string): boolean;
|
|
73
|
+
/** Extracts the $deps.<root> field names referenced by an expression. */
|
|
74
|
+
export declare function expressionDependencyFields(source: string): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R5 D-EXPR reaction surface (frozen Q: reactions operate on the $context
|
|
3
|
+
* namespace only; no field-value triggers).
|
|
4
|
+
*
|
|
5
|
+
* A reaction is a `{ when, apply }` rule: `when` is a frozen $context
|
|
6
|
+
* expression (evaluateExpression grammar), and `apply` turns a form control
|
|
7
|
+
* state on/off when the expression holds. This is the renderer-level
|
|
8
|
+
* equivalent of the navigation visibleWhen gate, applied to form fields.
|
|
9
|
+
*
|
|
10
|
+
* Fails closed: an unparseable `when` expression or an unknown apply target
|
|
11
|
+
* keeps the field at its default state instead of mutating it silently.
|
|
12
|
+
*/
|
|
13
|
+
export interface ReactionApply {
|
|
14
|
+
fieldId: string;
|
|
15
|
+
/** visible/disabled toggles are explicit booleans (no implicit flip). */
|
|
16
|
+
visible?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ReactionRule {
|
|
20
|
+
id: string;
|
|
21
|
+
when: string;
|
|
22
|
+
apply: ReactionApply[];
|
|
23
|
+
}
|
|
24
|
+
export type ReactionErrorCode = "REACTION_EXPRESSION_INVALID" | "REACTION_APPLY_FIELD_UNKNOWN" | "REACTION_APPLY_INVALID";
|
|
25
|
+
export interface ReactionError {
|
|
26
|
+
code: ReactionErrorCode;
|
|
27
|
+
path: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
export interface FormControlState {
|
|
31
|
+
visible: boolean;
|
|
32
|
+
disabled: boolean;
|
|
33
|
+
}
|
|
34
|
+
export type FormControlStateMap = Record<string, FormControlState>;
|
|
35
|
+
export interface ReactionEvaluation {
|
|
36
|
+
state: FormControlStateMap;
|
|
37
|
+
errors: ReactionError[];
|
|
38
|
+
}
|
|
39
|
+
/** Parses a raw reaction rule, fail-closed on malformed shapes. */
|
|
40
|
+
export declare function parseReactionRule(value: unknown, path: string): ReactionRule | ReactionError;
|
|
41
|
+
/**
|
|
42
|
+
* Evaluates a reaction rule list against the frozen $context snapshot.
|
|
43
|
+
* Unknown apply fieldIds fail closed (keep default) and are reported.
|
|
44
|
+
*/
|
|
45
|
+
export declare function evaluateReactions(rules: ReactionRule[], context: Record<string, unknown>, fieldIds: string[]): ReactionEvaluation;
|
|
46
|
+
/** Validates raw rules and returns parsed rules + fail-closed errors. */
|
|
47
|
+
export declare function parseAndEvaluateReactions(rawRules: unknown, context: Record<string, unknown>, fieldIds: string[]): ReactionEvaluation;
|
|
48
|
+
export interface FullReactionResult {
|
|
49
|
+
/** True when the form declares upstream-shaped per-field reactions ($deps). */
|
|
50
|
+
usesFullEngine: boolean;
|
|
51
|
+
/** Per-field control state after convergence (visible/disabled). */
|
|
52
|
+
state: FormControlStateMap;
|
|
53
|
+
/** Value commits to merge into the form values (last-wins, convergent). */
|
|
54
|
+
values: Record<string, unknown>;
|
|
55
|
+
errors: ReactionError[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves the full multi-round $deps reaction engine over a form's fields.
|
|
59
|
+
*
|
|
60
|
+
* Upstream shape (02-reaction-expression.md): each field node carries
|
|
61
|
+
* `reactions: [{ when, fulfill, otherwise }]`. Returns the convergent control
|
|
62
|
+
* state + value commits; malformed rules fail closed (reported, not applied).
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveFullFormReactions(rawFields: unknown, values: Record<string, unknown>, baselines: Record<string, unknown>): FullReactionResult;
|