@veil-runtime/ui 0.1.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,44 @@
1
+ # 0.3.0 — stable release preparation
2
+
3
+ - Promote the merged rc.2 implementation and all 13 review remediations, including notification-loop termination, to stable metadata with the `latest` dist-tag.
4
+ - Preserve all existing public APIs; no additional functionality.
5
+ - Publication awaits maintainer approval of the exact qualified artifact.
6
+
7
+ # 0.3.0-rc.2 — review remediation candidate
8
+
9
+ - Reject inherited composition field names and stage bounded state changes before acceptance.
10
+ - Apply independent resource limits to interface definition, data, and state snapshot copies.
11
+ - Preserve conditional layout visibility, numeric edit spelling, focus and selection, current accessibility labels, and the previous screen when replacement mounting fails.
12
+ - Route form fallback submissions locally, keep field/error IDs unique, and exercise only visible actions in the model qualification harness.
13
+ - Add controller, browser, harness, and adversarial boundary regressions for all review findings.
14
+
15
+ # 0.3.0-rc.1 — Developer Preview release candidate
16
+
17
+ - Prepare the universal interface engine for independent qualification and external consumption.
18
+ - Add a provider-neutral six-scenario model harness with deterministic fixtures; live model reliability remains unverified until provider access is available.
19
+ - Preserve the v0.1.0 and v0.2.0 APIs. Publication remains subject to maintainer approval of the exact candidate artifact.
20
+
21
+ # 0.3.0-dev.0 — dynamic universal interface work
22
+
23
+ - Add a single-call DOM mount helper with one teardown, a JSON-safe built-in capability manifest, and a strict TypeScript type for built-in screens.
24
+ - Add Row, Heading, List, and Status primitives; reject unsupported properties on built-ins and validate bound collections before mounting.
25
+ - Exercise six workflows from an independently installed tarball in TypeScript and JavaScript; update the three browser demonstrations to use the public helper and manifest.
26
+ - Make controller and mounted DOM acceptance transactional for generated definition, data, and local state updates; failed preparation preserves the accepted revision and view.
27
+ - Prepare detached custom replacements before commit and dispose rejected candidates; restore rejected input edits in the DOM.
28
+ - Retain compatible input state and DOM identity across definition updates, including keyed reordering and focus/selection.
29
+ - Add optional custom DOM component update/dispose hooks and an abort signal for owned resources.
30
+ - Stage new DOM subtrees before structural changes; reject invalid definitions without changing the current controller.
31
+ - Reject dispatch to hidden actions and expand the simulated agent and browser lifecycle regressions.
32
+ - Keep the development version unpublished; the 0.2.0-rc.1 checkpoint remains at `c65e752`.
33
+
34
+ # 0.2.0-rc.1 — application composition foundation (release candidate)
35
+
36
+ - Added a bounded one-screen composition schema, reactive controller, typed action result, and native DOM renderer.
37
+ - Added `/composition` and `/dom` subpaths and an independently installable example without Core.
38
+ - Marked Core as an optional peer for the new API; preserved the existing capability graph APIs and `/inspection` export.
39
+ - Hardened field metadata and child validation; added controller disposal, late-result suppression, DOM listener cleanup on remount, external field synchronization, and focus on invalid submission.
40
+ - This is a release candidate for maintainer review. No package has been published from this branch.
41
+
1
42
  # Proposed v0.1.0 release notes — release candidate
2
43
 
3
44
  Veil UI — Deterministic Composition Engine (Developer Preview).
package/README.md CHANGED
@@ -1,10 +1,14 @@
1
- # Veil UI — Deterministic Composition Engine (Developer Preview)
1
+ # Veil UI — Deterministic Composition Engine
2
+
3
+ The v0.3.0 stable release's framework-neutral interface engine is documented in [UNIVERSAL-INTERFACE.md](docs/UNIVERSAL-INTERFACE.md). It supports bounded JSON definitions, keyed dynamic updates, local state retention, trusted component lifecycles, and host-controlled actions without Veil Core. It uses the same public package alongside the existing composition and capability description APIs. See [RELEASE-0.3.0.md](docs/RELEASE-0.3.0.md) for release scope and limits.
4
+
5
+ **v0.2.0-rc.1 candidate:** a one-screen declarative composition foundation and DOM renderer are documented in [COMPOSITION.md](docs/COMPOSITION.md), with a working [stock treatment example](examples/composed). These additions are pending maintainer approval and are not a release announcement. The text below documents the v0.1.0 Developer Preview baseline.
2
6
 
3
7
  `@veil-runtime/ui@0.1.0` derives and validates interface descriptions from governed capability contracts. It is framework-neutral: the package returns graphs, fields, surfaces, and interaction descriptors. Your host renders those descriptions and executes authorized operations through Core.
4
8
 
5
9
  **Developer Preview: publication requires explicit maintainer approval of the exact qualified tarball and release action.**
6
10
 
7
- ## Install and run the candidate
11
+ ## Install and run 0.3.0
8
12
 
9
13
  Requires Node.js 24 or newer and npm. The supported TypeScript baseline is 6.0.2, ESM with NodeNext resolution. Strict consumer checks use `ES2022` and `DOM` libraries plus Node types because public Core/MCP declarations reference web transport types. UI itself makes no DOM or Core runtime calls. Direct CommonJS `require()` is unsupported; use ESM imports or dynamic `import()` from a CommonJS host.
10
14
 
@@ -17,11 +21,11 @@ npm run qualify:ui
17
21
 
18
22
  Qualification prints the candidate artifact and evidence directory. It builds and tests a source-only snapshot, installs the tarball into a new consumer with **published Core 0.4.0**, runs the standalone example, checks declarations and browser bundling, compares repeated tarballs, audits production dependencies, and inspects legal files. It never publishes.
19
23
 
20
- In a separate application, use the printed absolute tarball path:
24
+ After maintainer-approved publication, install with `npm install @veil-runtime/ui@0.3.0`. Before publication, use the qualified absolute tarball path:
21
25
 
22
26
  ```sh
23
27
  npm init -y
24
- npm install /absolute/path/veil-runtime-ui-0.1.0.tgz @veil-runtime/core@0.4.0
28
+ npm install /absolute/path/veil-runtime-ui-0.3.0.tgz
25
29
  ```
26
30
 
27
31
  Then copy [examples/standalone/index.mjs](examples/standalone/index.mjs) into that application and run:
@@ -30,7 +34,7 @@ Then copy [examples/standalone/index.mjs](examples/standalone/index.mjs) into th
30
34
  node index.mjs
31
35
  ```
32
36
 
33
- The example prints derived input controls, rejects invalid input, demonstrates a policy-denied execution without calling the service, and displays an approved result. It uses a tiny host-owned terminal renderer, a local identity fixture, an existing-service-style operation, and authoritative Core authorization. No model, Emergence, React, or Sandbox is involved. The included example `package.json` intentionally omits the unpublished UI dependency: install the candidate tarball explicitly using the command above.
37
+ For the v0.3.0 interface quick start, use [UNIVERSAL-INTERFACE.md](docs/UNIVERSAL-INTERFACE.md) and [examples/universal](examples/universal). The v0.1.0 standalone example below also needs `@veil-runtime/core@0.4.0`; install that peer separately when running it. The example prints derived input controls, rejects invalid input, demonstrates a policy-denied execution without calling the service, and displays an approved result. It uses a tiny host-owned terminal renderer, a local identity fixture, an existing-service-style operation, and authoritative Core authorization. No model, Emergence, React, or Sandbox is involved. The included example `package.json` intentionally omits the unpublished UI dependency: install the candidate tarball explicitly using the command above.
34
38
 
35
39
  ## Minimal derivation
36
40
 
@@ -1,27 +1,32 @@
1
1
  {
2
2
  "inputs": {
3
- "package.json": "c157a9da53acaef17d77f2c26c31b66c0709ff389eb660ac83889de64f4a39bb",
3
+ "package.json": "b2bfc55dd16c0ffb130a5f86823791cf68eea2115f94be87eb947e55683eb8d5",
4
4
  "scripts/artifact.mjs": "92d2ffd8836d0e14742528da9ddc96e0834ef873ffa81097a3ebae58d825aace",
5
- "scripts/build.mjs": "e0a8a0dd359c09a76b70d93865247f8d4404064442e76ff4b8d4d3e3390cb9aa",
6
- "scripts/check-boundary.mjs": "f5c0efe0b0f720b6e78dfa6bb5d22a1ab784e173e4d4db97adcb08d3ce889e8b",
5
+ "scripts/build.mjs": "54ca91e728925b1efefa61e5aeeb9e5bf549d25c2faa8ceb618009c214a7e03c",
6
+ "scripts/check-boundary.mjs": "5c09c118f7340f3a111e3947c2d98579f73903728ded9d604ac13b93077a5b29",
7
7
  "scripts/publication-hold.mjs": "2c7a00f8f23ac34d888813e76c4ab8f5bb112526002fd6be15993825dff118ca",
8
8
  "scripts/verify-dist.mjs": "668d5b105b7762cc2fd812091dba24f6985597da4519f72d572d5b7e63b880c0",
9
+ "scripts/verify-universal-consumer.mjs": "09c77f374c32ce6ec3b8887f6fa5bf1026800c1890adeace5ab226606c78b5be",
9
10
  "src/application.ts": "18cee45849d0ef9858d673b35aa42b77c4469b637669a0ab8a1ffc1f2c980d5d",
10
11
  "src/capability-view-derivation.ts": "07367b08e115c4022c873d2d01888d0d2c26ab58ac7518500fd95331301bbaa4",
11
12
  "src/capability-view.ts": "fda1db85ffcb26b45b60890b3a20b875ee4aff76bd3c35ce5d41018b22a64de7",
12
13
  "src/composed-surface.ts": "b2c2fc4cfbc8c5a07048f5482f9a2807ff936f62a99cd93c5a794133d6a31175",
13
14
  "src/composition-inspection.ts": "a9265c68bab785ba9e38ea83899997daec9322de10b98fdafaa44bc523ca0c3a",
15
+ "src/composition.ts": "4d06b8fe4ed61b2e0a4daf07770be708b2f0167a35ae41140b4047b7cd8fbd78",
14
16
  "src/derive-input-fields.ts": "cf8c7ee9f98ed0838dbf299fb4653814c3e70004fc121d8c99752aa137331efd",
15
17
  "src/derive-output-view.ts": "81d622d5430f7b9333a41c33794d8d22e3e539e861e04830ce089d83a6790a8e",
18
+ "src/dom.ts": "27724ba587addc050fdb0abd2ee1c6226971b6e7ce5dd5564fb67780e7ede486",
16
19
  "src/execution-request.ts": "775c3beba47aae482acbc2a48461ab8f947ca7b773c71a69077af387557ae344",
17
20
  "src/experience-orchestration.ts": "f156497d99828d15da2b42f3155e4ec0b77fc72ad6e6d846bb9bbea4d6791fa4",
18
21
  "src/format-semantic-value.ts": "2192cd723c5a9d64164287ddae99c0aa105024e1d0778a597d0b71a2ea8ab9dd",
19
- "src/index.ts": "44b876d7a7f0aaf333a6a03c72bcc48911f6d15e8717fad6fb6b77ea0f47db21",
20
- "src/input-schema.ts": "8c03637abeb80958881475ab049494a8905d19deb76df61c23c76d9b19eb692f",
22
+ "src/index.ts": "403cb5e536fe1df95c4911de7fe221dcf5d118f097cbd56bbdf7d19f6da1b7c7",
23
+ "src/input-schema.ts": "3f40e707882efc81f5e537c8ad8a2cdc4fd279e6362d32ee6fb55a09ac298ee7",
21
24
  "src/interaction-resolution.ts": "e9a75d501ded82fe773f03d2dcb1e2fa8b396a98d9c2902888a3f6b3cbf8f55e",
25
+ "src/interface-transaction.ts": "fe415c5318d0f0df78760dc374968dcfee42b0327e8796f3195aef07ccd19e91",
26
+ "src/interface.ts": "80843613765e7adc18b7f1e3b3c8ed46a3b8de41859ce06d8e2cb4750e2173c4",
22
27
  "src/output-presentation-suggestion-schema.ts": "2421d9f152cb4ce5e4d560f8ebe1d4029147dcab01a187c67008f36a3dfd6bf4",
23
28
  "src/output-schema.ts": "68f7f49fed5badafb5ca509365802dec6526a56c23d2bf2837b350852823688b",
24
- "src/presentation-composition.ts": "b591c0b041f3518fc0287934a063e75c217355987e9da9c4ed604d10e3acc940",
29
+ "src/presentation-composition.ts": "0116ae011d577396b5eff5b11a4c6c58b196b7610e71f0f3a0898e63de6457f0",
25
30
  "src/presentation-context.ts": "34c2796cb23c6086e6ee9474b756a03fb69d713aca28c0cdb7d6b0bd60f5c248",
26
31
  "src/presentation-suggestion-schema.ts": "f618db5e8caf439e0ae758a74a38edf8db57485662f1ddad29614e68425b1d41",
27
32
  "src/registry.ts": "de906003b8098202c905532cfd497ab0e2f9092226be2d9f469c5cf883891a8c",
@@ -29,7 +34,7 @@
29
34
  "src/validate-application.ts": "f76874d49955d7ade1d0f8c5f995b63a6026102173b3e50b710517ccbc1442f4",
30
35
  "src/validation-safety.ts": "e0bcc379269e3f51a0021dd4797c422f813b41141274125705b425d2b1b1eb2b",
31
36
  "src/workflow.ts": "6c5b31242474c0a37b1a9f85717eebf3b1d4b373c17c8d47861804e28d3302e5",
32
- "tsconfig.json": "eb3e08c27d8ac0302b339a13814c0f4416372f7b5f10fbafe3f4f16751978e9e"
37
+ "tsconfig.json": "d4ec334afc2daeccd982d4c3410b53f39df080f3cd1b761557f8e46dc96ab8aa"
33
38
  },
34
39
  "outputs": {
35
40
  "application.d.ts": "2c3091787abcd5a8005bfe951dc012056219b5ba6d70e5d8ad275281cf481d86",
@@ -42,28 +47,36 @@
42
47
  "composed-surface.js": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881",
43
48
  "composition-inspection.d.ts": "dca099a4a9776626a3b0fffe16f65aeecf4de100eab84287906c03f94c059941",
44
49
  "composition-inspection.js": "29527de641d75b0ea6710aa5407f9d0362ef5c61595694ced3745dd2ace92b72",
50
+ "composition.d.ts": "b76e23e686ca428378274bf74d55986ccb0bc91b95edc32beb6f82427e7a9c8b",
51
+ "composition.js": "6840719dac8b9239a67c647961fe32552b6fa98d30422bf3438edca993267147",
45
52
  "derive-input-fields.d.ts": "417aa8a884416e1a679a69d938592908488611fb453cb4e84b3666d351ca1045",
46
53
  "derive-input-fields.js": "de940148e3c029b786fec95a520a3d8a8d1eef1d361436fa607b2990b63f2adc",
47
54
  "derive-output-view.d.ts": "9ca3c32ac65215bb85ae74b75121c55c2ac82d04d7754d38ebc0de94d3fa898a",
48
55
  "derive-output-view.js": "e54de4b2ece567a518d346cfd8d96fe2179f5a1fcf76cc6306c5222184e19b73",
56
+ "dom.d.ts": "ae12ba88b9bf43bdc99a9c2ca8923764cf28e77d55b132a05dc5965cba34a6a5",
57
+ "dom.js": "cfc237bc80931652b54726efc65585970c386d851471d68e603e14c999053e84",
49
58
  "execution-request.d.ts": "f8b68cf456bfbeb63a968378e5ccaef5f55a908022e323901668ca99ebe75a3a",
50
59
  "execution-request.js": "f32bd2e9de70dd9f243d08d0fce4898e847fa3d34d77d3376a1b3c5839edb611",
51
60
  "experience-orchestration.d.ts": "45fa9770a37acccaeb6e0f267e0a566c6d6cda6133d6e1077589859478effd77",
52
61
  "experience-orchestration.js": "3ffccbcdc94bb2083f8e2227755651825d279f93b48f0c71d32d595c297b3fc2",
53
62
  "format-semantic-value.d.ts": "8ecb9dff99d819af71251a909e307e11e07f5c76bec1e8796a8cefcd0692a921",
54
63
  "format-semantic-value.js": "dd36b084312a57949a0a6e104e1c84a92ba334488e5e4dc7555f5d16a7eea7aa",
55
- "index.d.ts": "44b876d7a7f0aaf333a6a03c72bcc48911f6d15e8717fad6fb6b77ea0f47db21",
56
- "index.js": "44b876d7a7f0aaf333a6a03c72bcc48911f6d15e8717fad6fb6b77ea0f47db21",
57
- "input-schema.d.ts": "9f93c761ca0936ca18dd6a9df250fb16ba43f0da48a74897918765851edfacc2",
64
+ "index.d.ts": "403cb5e536fe1df95c4911de7fe221dcf5d118f097cbd56bbdf7d19f6da1b7c7",
65
+ "index.js": "403cb5e536fe1df95c4911de7fe221dcf5d118f097cbd56bbdf7d19f6da1b7c7",
66
+ "input-schema.d.ts": "e3cf583d925bc65d54250cfa3c1c3230b5df15dcb7be300a5de5fd9f0f0ab81d",
58
67
  "input-schema.js": "f5747d9b6acdced514c1c8c80d7a8a36e972d703fc234e1ac266f02dfd19c045",
59
68
  "interaction-resolution.d.ts": "fc3af3c0405e78e85bacf67262d7566f1b9f9ea6263bc4c200d0318795b805cf",
60
69
  "interaction-resolution.js": "498ea435bbb9ba0c814728664652491ba6c4be5c3407233984df4cf4f16103d7",
70
+ "interface-transaction.d.ts": "e93b9188ebfd497a4ef180e31fb93b48052a25c5905bf94a6b018a41538f33ac",
71
+ "interface-transaction.js": "9bf1a2bf576576ccb25d993581980e06280fa35107c96e49f962c47a72c1dbb3",
72
+ "interface.d.ts": "ec4973c9c99f2def7e6e2946b5652bff2f9877b256f181d0364ac85f35aca86b",
73
+ "interface.js": "7a29e93f17b32f5d5a3d978603a2cd235617b6474e5690d7f2c7e13ef61da19b",
61
74
  "output-presentation-suggestion-schema.d.ts": "88072dc37afeaab8ac05182b5d49fdf52099bbb18518ce2485feea476ea85895",
62
75
  "output-presentation-suggestion-schema.js": "708d2f821132978ba44a2e01812e41e3eec70266e5dbeb7053f55f88142ad634",
63
76
  "output-schema.d.ts": "8dbf090a79ab489e068e4a290dccd57e222c377f111d38e990308db8a3debca8",
64
77
  "output-schema.js": "29345dd93ccc27fafbdc29c66fbd35e38ab9d382392ca40fcdf643dbedefc662",
65
- "presentation-composition.d.ts": "42dcb9fd0a7478907e30bac5288f4a6508b24ffc78dbacb0eaa51f0ac99f5378",
66
- "presentation-composition.js": "35a310e39a660ba2f2ba322272db369dfdb27bc6d8608f77a75651f2ff0baad6",
78
+ "presentation-composition.d.ts": "e2b80f670230104e6b29e6447d842d0962c2ebcbde6dd064620d5194c2cd4689",
79
+ "presentation-composition.js": "c3d1666f55173f4e9f839e2b6e69acd2172a086f7e002e8332fde0b7edc8aec4",
67
80
  "presentation-context.d.ts": "f3565bd1666882a0ddd6041cb379270fd3fcb2c172a8e011b9c07ff189a404a9",
68
81
  "presentation-context.js": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881",
69
82
  "presentation-suggestion-schema.d.ts": "9fdd12079f822a608c9a3f2f3330b953fc247c65c5b6181a595ddc0b4440f37c",
@@ -0,0 +1,43 @@
1
+ import type { UIField } from "./capability-view.js";
2
+ import { type RichCapabilityInputSchema } from "./input-schema.js";
3
+ export interface ComponentNode {
4
+ component: string;
5
+ field?: string;
6
+ action?: string;
7
+ text?: string;
8
+ children?: ComponentNode[];
9
+ }
10
+ export interface ComposedApplication {
11
+ id: string;
12
+ title: string;
13
+ fields: RichCapabilityInputSchema;
14
+ initialState?: Record<string, unknown>;
15
+ screen: ComponentNode;
16
+ actions: readonly string[];
17
+ }
18
+ export type SubmissionStatus = "idle" | "submitting" | "success" | "error";
19
+ export interface CompositionSnapshot {
20
+ values: Record<string, unknown>;
21
+ errors: Record<string, string>;
22
+ status: SubmissionStatus;
23
+ message: string;
24
+ }
25
+ export type ActionResult = {
26
+ ok: true;
27
+ } | {
28
+ ok: false;
29
+ };
30
+ export type ActionHandler = (input: Readonly<Record<string, unknown>>) => Promise<ActionResult> | ActionResult;
31
+ export type ActionHandlers = Readonly<Record<string, ActionHandler>>;
32
+ export declare function defineComposedApplication<const T extends ComposedApplication>(definition: T): T;
33
+ export declare function validateComposedApplication(definition: ComposedApplication, customComponents?: readonly string[]): void;
34
+ export interface CompositionController<Action extends string = string, Field extends string = string> {
35
+ readonly definition: ComposedApplication;
36
+ snapshot(): CompositionSnapshot;
37
+ field(name: Field): UIField;
38
+ setField(name: Field, value: unknown): void;
39
+ submit(action: Action): Promise<boolean>;
40
+ subscribe(listener: (snapshot: CompositionSnapshot) => void): () => void;
41
+ dispose(): void;
42
+ }
43
+ export declare function createCompositionController<const T extends ComposedApplication>(definition: T, handlers: Readonly<Record<T["actions"][number], ActionHandler>>, customComponents?: readonly string[]): CompositionController<T["actions"][number], keyof T["fields"] & string>;
@@ -0,0 +1,227 @@
1
+ import { deriveUIField } from "./derive-input-fields.js";
2
+ import { validateRichCapabilityInput, validateRichInputSchema, validateRichInputValue } from "./input-schema.js";
3
+ import { assertBoundedData, assertSafeKey, copyData } from "./validation-safety.js";
4
+ const builtins = new Set(["Stack", "Form", "Field", "Submit", "Text", "Status"]);
5
+ const identifier = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;
6
+ export function defineComposedApplication(definition) {
7
+ return definition;
8
+ }
9
+ export function validateComposedApplication(definition, customComponents = []) {
10
+ assertBoundedData(definition);
11
+ if (typeof definition.id !== "string" || !identifier.test(definition.id) || typeof definition.title !== "string" || !definition.title.trim())
12
+ throw new Error("Invalid application identity");
13
+ if (!definition.fields || typeof definition.fields !== "object" || Array.isArray(definition.fields))
14
+ throw new Error("Invalid field schema");
15
+ validateRichInputSchema(definition.fields);
16
+ for (const [name, field] of Object.entries(definition.fields)) {
17
+ if (!identifier.test(name) || typeof field.required !== "boolean" || typeof field.description !== "string")
18
+ throw new Error(`Invalid field contract: ${name}`);
19
+ if (field.enum !== undefined && (!Array.isArray(field.enum) || field.enum.length === 0 || new Set(field.enum).size !== field.enum.length))
20
+ throw new Error(`Invalid enum: ${name}`);
21
+ if (field.format !== undefined && !["date", "date-time", "multiline"].includes(field.format))
22
+ throw new Error(`Invalid format: ${name}`);
23
+ if (field.semantic !== undefined && field.semantic !== "currency")
24
+ throw new Error(`Invalid semantic: ${name}`);
25
+ if (field.currency !== undefined && (typeof field.currency !== "string" || !/^[A-Z]{3}$/.test(field.currency)))
26
+ throw new Error(`Invalid currency: ${name}`);
27
+ for (const key of ["minLength", "maxLength", "minItems", "maxItems"]) {
28
+ const value = field[key];
29
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
30
+ throw new Error(`Invalid ${key}: ${name}`);
31
+ }
32
+ for (const key of ["minimum", "maximum"]) {
33
+ const value = field[key];
34
+ if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value)))
35
+ throw new Error(`Invalid ${key}: ${name}`);
36
+ }
37
+ }
38
+ if (!Array.isArray(definition.actions) || new Set(definition.actions).size !== definition.actions.length || definition.actions.some((id) => typeof id !== "string" || !identifier.test(id)))
39
+ throw new Error("Invalid action list");
40
+ for (const name of customComponents) {
41
+ if (!identifier.test(name))
42
+ throw new Error(`Invalid component name: ${name}`);
43
+ assertSafeKey(name);
44
+ }
45
+ const known = new Set([...builtins, ...customComponents]);
46
+ if (known.size !== builtins.size + customComponents.length)
47
+ throw new Error("Component names must be unique and cannot replace built-ins");
48
+ const fields = new Set();
49
+ const actions = new Set();
50
+ function visit(node, insideForm) {
51
+ if (!node || typeof node !== "object" || !known.has(node.component))
52
+ throw new Error(`Unknown component: ${String(node?.component)}`);
53
+ if (node.children !== undefined && !Array.isArray(node.children))
54
+ throw new Error("Component children must be an array");
55
+ if (node.component === "Form" && insideForm)
56
+ throw new Error("Nested forms are unsupported");
57
+ if (node.component === "Field") {
58
+ if (!insideForm || !node.field || !Object.hasOwn(definition.fields, node.field) || fields.has(node.field))
59
+ throw new Error(`Invalid or duplicate field: ${node.field}`);
60
+ const type = definition.fields[node.field]?.type;
61
+ if (type === "object" || type === "array")
62
+ throw new Error(`Structured field requires a custom component: ${node.field}`);
63
+ fields.add(node.field);
64
+ }
65
+ else if (node.field !== undefined)
66
+ throw new Error("Only Field nodes accept a field binding");
67
+ if (node.component === "Submit") {
68
+ if (!insideForm || !node.action || !definition.actions.includes(node.action) || actions.has(node.action))
69
+ throw new Error(`Invalid or duplicate action: ${node.action}`);
70
+ actions.add(node.action);
71
+ }
72
+ else if (node.action !== undefined)
73
+ throw new Error("Only Submit nodes accept an action");
74
+ if (node.text !== undefined && (typeof node.text !== "string" || node.text.length > 4096))
75
+ throw new Error("Invalid component text");
76
+ if (node.component === "Field" || node.component === "Submit" || node.component === "Text" || node.component === "Status") {
77
+ if (node.children?.length)
78
+ throw new Error(`${node.component} cannot contain children`);
79
+ }
80
+ for (const child of node.children ?? [])
81
+ visit(child, insideForm || node.component === "Form");
82
+ }
83
+ visit(definition.screen, false);
84
+ if (fields.size !== Object.keys(definition.fields).length || actions.size !== definition.actions.length)
85
+ throw new Error("All fields and actions must appear in the screen");
86
+ if (definition.initialState !== undefined) {
87
+ if (!definition.initialState || typeof definition.initialState !== "object" || Array.isArray(definition.initialState))
88
+ throw new Error("Invalid initial state");
89
+ for (const [key, value] of Object.entries(definition.initialState)) {
90
+ const field = Object.hasOwn(definition.fields, key) ? definition.fields[key] : undefined;
91
+ if (!field)
92
+ throw new Error(`Unknown initial field: ${key}`);
93
+ validateRichInputValue(value, { ...field, required: false }, key);
94
+ }
95
+ }
96
+ }
97
+ export function createCompositionController(definition, handlers, customComponents = []) {
98
+ validateComposedApplication(definition, customComponents);
99
+ const source = copyData(definition);
100
+ for (const action of source.actions)
101
+ if (!Object.hasOwn(handlers, action) || typeof handlers[action] !== "function")
102
+ throw new Error(`Missing action handler: ${action}`);
103
+ const listeners = new Set();
104
+ const values = Object.fromEntries(Object.keys(source.fields).map((name) => [name, source.initialState && Object.hasOwn(source.initialState, name) ? source.initialState[name] : (source.fields[name]?.type === "boolean" ? false : "")]));
105
+ let errors = {};
106
+ let status = "idle";
107
+ let message = "";
108
+ let disposed = false;
109
+ const snapshot = () => copyData({ values, errors, status, message });
110
+ const assertSnapshot = (nextValues, nextErrors, nextStatus, nextMessage) => {
111
+ assertBoundedData({ values: nextValues, errors: nextErrors, status: nextStatus, message: nextMessage });
112
+ };
113
+ const errorBudget = Object.fromEntries(Object.entries(source.fields).map(([name, field]) => [name, `Check ${deriveUIField(name, field).label}`]));
114
+ const assertValuesResource = (nextValues) => {
115
+ assertSnapshot(nextValues, errorBudget, "error", "Unable to save. Please try again.");
116
+ };
117
+ assertValuesResource(values);
118
+ const emit = () => { for (const listener of listeners) {
119
+ try {
120
+ listener(snapshot());
121
+ }
122
+ catch { /* Host listener failures cannot change the transaction. */ }
123
+ } };
124
+ return {
125
+ get definition() { return copyData(source); },
126
+ snapshot,
127
+ field(name) {
128
+ const field = Object.hasOwn(source.fields, name) ? source.fields[name] : undefined;
129
+ if (!field)
130
+ throw new Error(`Unknown field: ${name}`);
131
+ return deriveUIField(name, field);
132
+ },
133
+ setField(name, value) {
134
+ if (disposed)
135
+ return;
136
+ const field = Object.hasOwn(source.fields, name) ? source.fields[name] : undefined;
137
+ if (!field)
138
+ throw new Error(`Unknown field: ${name}`);
139
+ if (status === "submitting")
140
+ return;
141
+ assertBoundedData(value);
142
+ const nextValues = { ...values, [name]: copyData(value) };
143
+ const next = { ...errors };
144
+ try {
145
+ validateRichInputValue(value, field, name);
146
+ delete next[name];
147
+ }
148
+ catch {
149
+ next[name] = `Check ${deriveUIField(name, field).label}`;
150
+ }
151
+ assertValuesResource(nextValues);
152
+ Object.assign(values, nextValues);
153
+ errors = next;
154
+ status = "idle";
155
+ message = "";
156
+ emit();
157
+ },
158
+ async submit(action) {
159
+ if (disposed)
160
+ return false;
161
+ if (!source.actions.includes(action))
162
+ throw new Error(`Unknown action: ${action}`);
163
+ if (status === "submitting")
164
+ return false;
165
+ const next = {};
166
+ for (const [name, field] of Object.entries(source.fields)) {
167
+ try {
168
+ validateRichInputValue(values[name], field, name);
169
+ }
170
+ catch {
171
+ next[name] = `Check ${deriveUIField(name, field).label}`;
172
+ }
173
+ }
174
+ assertSnapshot(values, next, Object.keys(next).length ? "error" : "submitting", Object.keys(next).length ? "Review the highlighted fields." : "Submitting…");
175
+ errors = next;
176
+ if (Object.keys(next).length) {
177
+ status = "error";
178
+ message = "Review the highlighted fields.";
179
+ emit();
180
+ return false;
181
+ }
182
+ const input = copyData(values);
183
+ validateRichCapabilityInput(source.fields, input);
184
+ status = "submitting";
185
+ message = "Submitting…";
186
+ emit();
187
+ try {
188
+ const result = await handlers[action](input);
189
+ if (disposed)
190
+ return false;
191
+ assertBoundedData(result);
192
+ if (!result || typeof result !== "object" || Object.keys(result).length !== 1 || (result.ok !== true && result.ok !== false))
193
+ throw new Error("Invalid action result");
194
+ if (!result.ok)
195
+ throw new Error("Action failed");
196
+ assertSnapshot(values, errors, "success", "Saved successfully.");
197
+ status = "success";
198
+ message = "Saved successfully.";
199
+ emit();
200
+ return true;
201
+ }
202
+ catch {
203
+ if (disposed)
204
+ return false;
205
+ assertSnapshot(values, errors, "error", "Unable to save. Please try again.");
206
+ status = "error";
207
+ message = "Unable to save. Please try again.";
208
+ emit();
209
+ return false;
210
+ }
211
+ },
212
+ subscribe(listener) {
213
+ if (disposed)
214
+ throw new Error("Composition controller is disposed");
215
+ listeners.add(listener);
216
+ try {
217
+ listener(snapshot());
218
+ }
219
+ catch (error) {
220
+ listeners.delete(listener);
221
+ throw error;
222
+ }
223
+ return () => { listeners.delete(listener); };
224
+ },
225
+ dispose() { disposed = true; listeners.clear(); },
226
+ };
227
+ }
package/dist/dom.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { ComponentNode, CompositionController } from "./composition.js";
2
+ import { type InterfaceController, type InterfaceDefinition, type InterfaceEvent, type InterfaceNode, type InterfaceSnapshot } from "./interface.js";
3
+ export type DOMComponent = (node: Readonly<ComponentNode>, context: {
4
+ controller: CompositionController;
5
+ renderChildren: () => HTMLElement[];
6
+ }) => HTMLElement;
7
+ export type DOMComponentRegistry = Readonly<Record<string, DOMComponent>>;
8
+ /** Mount one composed screen. Caller owns the controller, mount point, and disposal. */
9
+ export declare function mountComposedApplication(target: HTMLElement, controller: CompositionController, components?: DOMComponentRegistry): () => void;
10
+ export interface InterfaceDOMComponentInstance {
11
+ element: HTMLElement;
12
+ update?: (node: Readonly<InterfaceNode>, snapshot: InterfaceSnapshot) => void;
13
+ dispose?: () => void;
14
+ }
15
+ export type InterfaceDOMComponent = (node: Readonly<InterfaceNode>, context: {
16
+ renderChildren: () => HTMLElement[];
17
+ snapshot: InterfaceSnapshot;
18
+ signal: AbortSignal;
19
+ }) => HTMLElement | InterfaceDOMComponentInstance;
20
+ export type InterfaceDOMRegistry = Readonly<Record<string, InterfaceDOMComponent>>;
21
+ /** Own a controller and one DOM mount with a single teardown. Component factories remain trusted host code. */
22
+ export declare function createMountedInterface(target: HTMLElement, definition: InterfaceDefinition, options?: {
23
+ components?: InterfaceDOMRegistry;
24
+ allowedActions?: readonly string[];
25
+ onEvent?: (event: InterfaceEvent) => void | Promise<void>;
26
+ onError?: (error: unknown) => void;
27
+ }): InterfaceController & {
28
+ unmount(): void;
29
+ };
30
+ /** Mount a framework-neutral interface. The caller owns and disposes the controller separately. */
31
+ export declare function mountInterface(target: HTMLElement, controller: InterfaceController, components?: InterfaceDOMRegistry, onError?: (error: unknown) => void): () => void;