@veil-runtime/ui 0.3.0 → 0.4.1

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,9 +1,3 @@
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
1
  # 0.3.0-rc.2 — review remediation candidate
8
2
 
9
3
  - Reject inherited composition field names and stage bounded state changes before acceptance.
@@ -54,3 +48,16 @@ Veil UI — Deterministic Composition Engine (Developer Preview).
54
48
  Behavior changes from the private lab extraction: older Core peer ranges are removed; unknown outputs are errors rather than accepted extras; registry updates require a new registry; presentation callbacks cannot execute the supplied capability; bounded JSON-like data and real calendars are enforced. Callers must adapt service results to public DTOs. Optional presentation APIs remain experimental.
55
49
 
56
50
  This package supplies descriptions, not a React renderer, execution host, authentication service, workflow engine, or resumable-operation protocol. Historical lab UI/Emergence limitations remain outside the supported preview. Publication requires explicit maintainer approval of the exact qualified tarball and release action; these are proposed notes, not an announcement of availability.
51
+
52
+ ## 0.4.1 — Developer Preview patch
53
+
54
+ - Document and test a runnable backend integration example in the source repository; the server, application, and browser fixture are excluded from the npm package.
55
+ - Preserve the 0.4.0 runtime API and optional Core peer contract. This version resolves the already published 0.4.0 package identity.
56
+
57
+ ## 0.4.0 — Developer Preview
58
+
59
+ - Add bounded compound conditions and runtime-enforced conditional Button disabling.
60
+ - Add declarative field validation, accessible feedback, and opt-in validated submission.
61
+ - Add Select and Textarea with strict descriptors and retained state.
62
+ - Add a validated conditional request example and isolated Core compatibility probes.
63
+ - Correct development metadata following the v0.3.0 tag. See docs/v0.4.0.md for semantics and migration.
package/README.md CHANGED
@@ -1,16 +1,18 @@
1
- # Veil UI — Deterministic Composition Engine
1
+ # Veil UI — Deterministic Composition Engine (Developer Preview)
2
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.
3
+ The v0.4.1 Developer Preview's framework-neutral interface engine is documented in [UNIVERSAL-INTERFACE.md](docs/UNIVERSAL-INTERFACE.md) and [v0.4.0.md](docs/v0.4.0.md). It supports bounded JSON definitions, validated multi-input forms, conditional controls, keyed dynamic updates, local state retention, trusted component lifecycles, and host-controlled actions without Veil Core. See [RELEASE-0.4.1.md](docs/RELEASE-0.4.1.md) for release scope and limits.
4
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.
5
+ The one-screen composition API and DOM renderer are documented in [COMPOSITION.md](docs/COMPOSITION.md), with a working [stock treatment example](examples/composed). The root capability-description API remains available alongside the newer interface APIs.
6
6
 
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.
7
+ The runnable [backend integration example](https://github.com/veil-runtime/veil-ui-lab/tree/main/packages/ui/examples/backend) lives in the source repository only. Its server, application, and browser fixture are not in the npm tarball. The tarball includes the public runtime, declarations, documentation, and the standalone, composed, and universal examples listed in `package.json`.
8
+
9
+ The root API derives and validates interface descriptions from governed capability contracts. It is framework-neutral: it returns graphs, fields, surfaces, and interaction descriptors. Your host renders those descriptions and executes authorized operations through Core.
8
10
 
9
11
  **Developer Preview: publication requires explicit maintainer approval of the exact qualified tarball and release action.**
10
12
 
11
- ## Install and run 0.3.0
13
+ ## Install and run
12
14
 
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.
15
+ Requires Node.js 24 or newer for server-side use and npm. The supported TypeScript baseline is 6.0.2, ESM with NodeNext resolution. Strict Core consumer checks use `ES2022` and `DOM` libraries plus Node types because public Core/MCP declarations reference web transport types. The `/dom` entry point requires a browser DOM and should be bundled for browser use; UI has no compiled Core runtime import. Direct CommonJS `require()` is unsupported; use ESM imports or dynamic `import()` from a CommonJS host.
14
16
 
15
17
  From this repository:
16
18
 
@@ -19,13 +21,13 @@ npm ci
19
21
  npm run qualify:ui
20
22
  ```
21
23
 
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.
24
+ Qualification prints the 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.
23
25
 
24
- After maintainer-approved publication, install with `npm install @veil-runtime/ui@0.3.0`. Before publication, use the qualified absolute tarball path:
26
+ In a separate application, use the printed absolute tarball path:
25
27
 
26
28
  ```sh
27
29
  npm init -y
28
- npm install /absolute/path/veil-runtime-ui-0.3.0.tgz
30
+ npm install /absolute/path/veil-runtime-ui-0.4.1.tgz
29
31
  ```
30
32
 
31
33
  Then copy [examples/standalone/index.mjs](examples/standalone/index.mjs) into that application and run:
@@ -34,7 +36,7 @@ Then copy [examples/standalone/index.mjs](examples/standalone/index.mjs) into th
34
36
  node index.mjs
35
37
  ```
36
38
 
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.
39
+ For the current interface quick start, use [UNIVERSAL-INTERFACE.md](docs/UNIVERSAL-INTERFACE.md) and [examples/universal](examples/universal). The legacy capability 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 UI dependency: install the qualified tarball explicitly using the command above.
38
40
 
39
41
  ## Minimal derivation
40
42
 
@@ -67,9 +69,9 @@ validateRichCapabilityInput(inputSchema, { name: 'Amina' });
67
69
 
68
70
  ## Public contract
69
71
 
70
- Supported exports are the root `@veil-runtime/ui` and `@veil-runtime/ui/inspection`. Internal dist subpaths are not API. See [API.md](docs/API.md) for the inventory and [DECISIONS.md](docs/DECISIONS.md) for compatibility decisions.
72
+ Supported exports are the root `@veil-runtime/ui`, plus `/inspection`, `/composition`, `/interface`, and `/dom`. Internal dist subpaths are not API. See [API.md](docs/API.md) for the inventory and [DECISIONS.md](docs/DECISIONS.md) for compatibility decisions.
71
73
 
72
- The deterministic preview supports injected registry/graph validation; recursive inputs; form, table, detail, aggregate, time-series, and category-series descriptions; relationship bindings/conditions; interaction and human-input payload resolution; descriptive workflow/success-transition metadata; semantic value formatting; and composition inspection. It does not mount controls or enforce domain transitions.
74
+ The root capability API supports injected registry/graph validation; recursive inputs; form, table, detail, aggregate, time-series, and category-series descriptions; relationship bindings/conditions; interaction and human-input payload resolution; descriptive workflow/success-transition metadata; semantic value formatting; and composition inspection. It does not mount controls or enforce domain transitions. The separate `/dom` API mounts controls.
73
75
 
74
76
  `createUIRegistry()` snapshots definitions. Returned metadata can be edited without changing subsequent derivations. To change descriptions or capability versions, create a new registry and refresh the host's view/context lifecycle explicitly. There is no subscription or live-update protocol.
75
77
 
@@ -92,10 +94,14 @@ npm --workspace @veil-runtime/ui run check
92
94
  npm test
93
95
  ```
94
96
 
95
- The build targets ES2022, emits ESM/declarations without dangling source maps, and fingerprints inputs and emitted files. `verify:dist` rejects missing, stale, altered, or extra files; `npm pack` rebuilds and verifies first. A failing `prepublishOnly` hook blocks publication from a source directory. Only the exact qualified tarball may be published after maintainer approval of its SHA-256 and the release action. npm tarball publication does not run directory lifecycle hooks; no script bypass is needed. Publication defaults to public access on `https://registry.npmjs.org/` under the `preview` tag. Qualification verifies source-directory refusal and an exact-tarball publication dry-run; it never publishes. Neither qualification nor CI authorizes a release.
97
+ The build targets ES2022, emits ESM/declarations without dangling source maps, and fingerprints inputs and emitted files. `verify:dist` rejects missing, stale, altered, or extra files; `npm pack` rebuilds and verifies first. A failing `prepublishOnly` hook blocks publication from a source directory. Only the exact qualified tarball may be published after maintainer approval of its SHA-256 and the release action. npm tarball publication does not run directory lifecycle hooks; no script bypass is needed. Publication defaults to public access on `https://registry.npmjs.org/` under the `latest` tag. Qualification verifies source-directory refusal and an exact-tarball publication dry-run; it never publishes. Neither qualification nor CI authorizes a release.
96
98
 
97
99
  The laboratory React UI and Emergence remain historical experiments outside this release contract. The capability lab is at `/lab`; `/` opens the separate Emergence experiment. Known lab-only issues include mobile overflow, decimal input stepping, fixed/permissive demo identity/policy, process-local stores, constructor/singleton integration friction, and unavailable Core Emergence imports in its broad client/server build. These do not become supported package features through this preview. See the release qualification report for separate laboratory dependency findings.
98
100
 
99
101
  ## License
100
102
 
101
- Apache License 2.0, approved by the maintainer for this package. [LICENSE](LICENSE) and [NOTICE](NOTICE) are included in the candidate tarball. External dependencies retain their own licenses; see [THIRD_PARTY.md](docs/THIRD_PARTY.md). Root laboratory metadata remains separate.
103
+ Apache License 2.0, approved by the maintainer for this package. [LICENSE](LICENSE) and [NOTICE](NOTICE) are included in the qualified tarball. External dependencies retain their own licenses; see [THIRD_PARTY.md](docs/THIRD_PARTY.md). Root laboratory metadata remains separate.
104
+
105
+ ### v0.4.1
106
+
107
+ This patch release adds a [source-only backend integration example](https://github.com/veil-runtime/veil-ui-lab/tree/main/packages/ui/examples/backend). See [validation, conditions, and new inputs](docs/v0.4.0.md) and the [validated request example](examples/universal/validated.mjs). Submission validation is opt-in with `validate: true` on a Button; hosts retain execution and authorization responsibility.
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "inputs": {
3
- "package.json": "b2bfc55dd16c0ffb130a5f86823791cf68eea2115f94be87eb947e55683eb8d5",
3
+ "package.json": "bcc8309a63cde3b3b38f9017d22915203b813d75229a7347c873ce3074ba0a25",
4
4
  "scripts/artifact.mjs": "92d2ffd8836d0e14742528da9ddc96e0834ef873ffa81097a3ebae58d825aace",
5
5
  "scripts/build.mjs": "54ca91e728925b1efefa61e5aeeb9e5bf549d25c2faa8ceb618009c214a7e03c",
6
6
  "scripts/check-boundary.mjs": "5c09c118f7340f3a111e3947c2d98579f73903728ded9d604ac13b93077a5b29",
7
7
  "scripts/publication-hold.mjs": "2c7a00f8f23ac34d888813e76c4ab8f5bb112526002fd6be15993825dff118ca",
8
+ "scripts/verify-core-matrix.mjs": "69614723a540ddc6167d9d87a25718d1f1ce36f2a51f8fcd767a7ac0f1c6259c",
8
9
  "scripts/verify-dist.mjs": "668d5b105b7762cc2fd812091dba24f6985597da4519f72d572d5b7e63b880c0",
9
- "scripts/verify-universal-consumer.mjs": "09c77f374c32ce6ec3b8887f6fa5bf1026800c1890adeace5ab226606c78b5be",
10
+ "scripts/verify-universal-consumer.mjs": "f637b99540831089f05c71d113ac958bf5dfb2a316358b75de3e0c1f29e34bd5",
10
11
  "src/application.ts": "18cee45849d0ef9858d673b35aa42b77c4469b637669a0ab8a1ffc1f2c980d5d",
11
12
  "src/capability-view-derivation.ts": "07367b08e115c4022c873d2d01888d0d2c26ab58ac7518500fd95331301bbaa4",
12
13
  "src/capability-view.ts": "fda1db85ffcb26b45b60890b3a20b875ee4aff76bd3c35ce5d41018b22a64de7",
@@ -15,7 +16,7 @@
15
16
  "src/composition.ts": "4d06b8fe4ed61b2e0a4daf07770be708b2f0167a35ae41140b4047b7cd8fbd78",
16
17
  "src/derive-input-fields.ts": "cf8c7ee9f98ed0838dbf299fb4653814c3e70004fc121d8c99752aa137331efd",
17
18
  "src/derive-output-view.ts": "81d622d5430f7b9333a41c33794d8d22e3e539e861e04830ce089d83a6790a8e",
18
- "src/dom.ts": "27724ba587addc050fdb0abd2ee1c6226971b6e7ce5dd5564fb67780e7ede486",
19
+ "src/dom.ts": "a7580ecc8599de9df2554390beaf430ac5217b0adb7e7e9145f0caaedc39e3f1",
19
20
  "src/execution-request.ts": "775c3beba47aae482acbc2a48461ab8f947ca7b773c71a69077af387557ae344",
20
21
  "src/experience-orchestration.ts": "f156497d99828d15da2b42f3155e4ec0b77fc72ad6e6d846bb9bbea4d6791fa4",
21
22
  "src/format-semantic-value.ts": "2192cd723c5a9d64164287ddae99c0aa105024e1d0778a597d0b71a2ea8ab9dd",
@@ -23,7 +24,7 @@
23
24
  "src/input-schema.ts": "3f40e707882efc81f5e537c8ad8a2cdc4fd279e6362d32ee6fb55a09ac298ee7",
24
25
  "src/interaction-resolution.ts": "e9a75d501ded82fe773f03d2dcb1e2fa8b396a98d9c2902888a3f6b3cbf8f55e",
25
26
  "src/interface-transaction.ts": "fe415c5318d0f0df78760dc374968dcfee42b0327e8796f3195aef07ccd19e91",
26
- "src/interface.ts": "80843613765e7adc18b7f1e3b3c8ed46a3b8de41859ce06d8e2cb4750e2173c4",
27
+ "src/interface.ts": "ea35aa01aac62478cf218b1980823200063c79451bf44542b1a1e5541b0fef4f",
27
28
  "src/output-presentation-suggestion-schema.ts": "2421d9f152cb4ce5e4d560f8ebe1d4029147dcab01a187c67008f36a3dfd6bf4",
28
29
  "src/output-schema.ts": "68f7f49fed5badafb5ca509365802dec6526a56c23d2bf2837b350852823688b",
29
30
  "src/presentation-composition.ts": "0116ae011d577396b5eff5b11a4c6c58b196b7610e71f0f3a0898e63de6457f0",
@@ -54,7 +55,7 @@
54
55
  "derive-output-view.d.ts": "9ca3c32ac65215bb85ae74b75121c55c2ac82d04d7754d38ebc0de94d3fa898a",
55
56
  "derive-output-view.js": "e54de4b2ece567a518d346cfd8d96fe2179f5a1fcf76cc6306c5222184e19b73",
56
57
  "dom.d.ts": "ae12ba88b9bf43bdc99a9c2ca8923764cf28e77d55b132a05dc5965cba34a6a5",
57
- "dom.js": "cfc237bc80931652b54726efc65585970c386d851471d68e603e14c999053e84",
58
+ "dom.js": "decdde492af176b865643e0c11992724e33c4146b155f9b9f9ca10f889769b6f",
58
59
  "execution-request.d.ts": "f8b68cf456bfbeb63a968378e5ccaef5f55a908022e323901668ca99ebe75a3a",
59
60
  "execution-request.js": "f32bd2e9de70dd9f243d08d0fce4898e847fa3d34d77d3376a1b3c5839edb611",
60
61
  "experience-orchestration.d.ts": "45fa9770a37acccaeb6e0f267e0a566c6d6cda6133d6e1077589859478effd77",
@@ -69,8 +70,8 @@
69
70
  "interaction-resolution.js": "498ea435bbb9ba0c814728664652491ba6c4be5c3407233984df4cf4f16103d7",
70
71
  "interface-transaction.d.ts": "e93b9188ebfd497a4ef180e31fb93b48052a25c5905bf94a6b018a41538f33ac",
71
72
  "interface-transaction.js": "9bf1a2bf576576ccb25d993581980e06280fa35107c96e49f962c47a72c1dbb3",
72
- "interface.d.ts": "ec4973c9c99f2def7e6e2946b5652bff2f9877b256f181d0364ac85f35aca86b",
73
- "interface.js": "7a29e93f17b32f5d5a3d978603a2cd235617b6474e5690d7f2c7e13ef61da19b",
73
+ "interface.d.ts": "f7e780ea478aff594de1fcbc34bed3f2239fd3f5bb8f8b6da289b86463f5cee2",
74
+ "interface.js": "02885e7c3293fd2c9924472844bc5e5b88391ecf747c53ee756ce8be834bf24e",
74
75
  "output-presentation-suggestion-schema.d.ts": "88072dc37afeaab8ac05182b5d49fdf52099bbb18518ce2485feea476ea85895",
75
76
  "output-presentation-suggestion-schema.js": "708d2f821132978ba44a2e01812e41e3eec70266e5dbeb7053f55f88142ad634",
76
77
  "output-schema.d.ts": "8dbf090a79ab489e068e4a290dccd57e222c377f111d38e990308db8a3debca8",
package/dist/dom.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createInterfaceController, resolveInterfaceBinding } from "./interface.js";
1
+ import { createInterfaceController, evaluateInterfaceCondition, getInterfaceValidation, isInterfaceField, resolveInterfaceBinding } from "./interface.js";
2
2
  import { registerInterfaceRenderer } from "./interface-transaction.js";
3
3
  let instance = 0;
4
4
  const mounted = new WeakMap();
@@ -230,7 +230,7 @@ export function mountInterface(target, controller, components = {}, onError) {
230
230
  const make = (tag, className) => { const el = document.createElement(tag); el.className = className; return el; };
231
231
  const display = (value) => value === undefined || value === null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
232
232
  const resolveText = (node, snapshot) => typeof node.text === "string" ? node.text : node.text ? display(resolveInterfaceBinding(snapshot, node.text)) : "";
233
- const visible = (node, snapshot) => !node.visible || resolveInterfaceBinding(snapshot, node.visible.binding) === node.visible.equals;
233
+ const visible = (node, snapshot) => !node.visible || evaluateInterfaceCondition(node.visible, snapshot);
234
234
  function create(node, snapshot, candidate = false) {
235
235
  const children = [];
236
236
  let element;
@@ -265,26 +265,27 @@ export function mountInterface(target, controller, components = {}, onError) {
265
265
  dispose = () => button.removeEventListener("click", click);
266
266
  element = button;
267
267
  }
268
- else if (node.component === "Input") {
268
+ else if (isInterfaceField(node)) {
269
269
  const wrapper = make("div", "veil-interface-input");
270
270
  const label = document.createElement("label");
271
- const input = document.createElement("input");
272
- input.id = `${domId}-${node.key}`;
271
+ const input = document.createElement(node.component === "Textarea" ? "textarea" : node.component === "Select" ? "select" : "input");
272
+ input.id = `${domId}-input-${node.key}`;
273
273
  label.htmlFor = input.id;
274
- input.type = node.inputType ?? "text";
274
+ if (input instanceof HTMLInputElement)
275
+ input.type = node.inputType ?? "text";
275
276
  input.style.boxSizing = "border-box";
276
277
  input.style.maxWidth = "100%";
277
278
  input.style.minWidth = "0";
278
279
  const change = () => {
279
280
  if (!active || !live)
280
281
  return;
281
- const value = input.type === "checkbox" ? input.checked : input.type === "number" && input.value !== "" ? Number(input.value) : input.value;
282
+ const value = input instanceof HTMLInputElement && input.type === "checkbox" ? input.checked : input.type === "number" && input.value !== "" ? Number(input.value) : input.value;
282
283
  try {
283
284
  controller.setState(node.binding.path, value);
284
285
  }
285
286
  catch (error) {
286
287
  const accepted = resolveInterfaceBinding(controller.snapshot(), node.binding);
287
- if (input.type === "checkbox")
288
+ if (input instanceof HTMLInputElement && input.type === "checkbox")
288
289
  input.checked = accepted === true;
289
290
  else
290
291
  input.value = display(accepted);
@@ -294,7 +295,11 @@ export function mountInterface(target, controller, components = {}, onError) {
294
295
  input.addEventListener("input", change);
295
296
  input.addEventListener("change", change);
296
297
  dispose = () => { input.removeEventListener("input", change); input.removeEventListener("change", change); };
297
- wrapper.append(label, input);
298
+ const feedback = document.createElement("span");
299
+ feedback.id = `${domId}-feedback-${node.key}`;
300
+ feedback.setAttribute("aria-live", "polite");
301
+ input.setAttribute("aria-describedby", feedback.id);
302
+ wrapper.append(label, input, feedback);
298
303
  element = wrapper;
299
304
  }
300
305
  else if (node.component === "Table") {
@@ -381,6 +386,8 @@ export function mountInterface(target, controller, components = {}, onError) {
381
386
  const { node, element } = rendered;
382
387
  const isVisible = visible(node, snapshot);
383
388
  element.hidden = !isVisible;
389
+ if (node.component === "Button")
390
+ element.disabled = !!node.disabled && evaluateInterfaceCondition(node.disabled, snapshot);
384
391
  element.style.display = isVisible ? rendered.visibleDisplay : "none";
385
392
  if (node.ariaLabel)
386
393
  element.setAttribute("aria-label", node.ariaLabel);
@@ -395,12 +402,20 @@ export function mountInterface(target, controller, components = {}, onError) {
395
402
  const entries = (items ?? []).map((item) => { const li = document.createElement("li"); li.textContent = display(item); return li; });
396
403
  element.replaceChildren(...entries);
397
404
  }
398
- if (node.component === "Input") {
405
+ if (isInterfaceField(node)) {
399
406
  const label = element.querySelector("label");
400
- const input = element.querySelector("input");
407
+ const input = element.querySelector("input,select,textarea");
408
+ if (input instanceof HTMLSelectElement) {
409
+ input.replaceChildren(...[{ value: "", label: "Choose…" }, ...node.options.filter(option => option.value !== "")].map(option => { const item = document.createElement("option"); item.value = option.value; item.textContent = option.label; return item; }));
410
+ }
411
+ const errors = getInterfaceValidation(snapshot);
412
+ const error = Object.hasOwn(errors, node.key) ? errors[node.key] : "";
413
+ element.querySelector("span").textContent = error;
414
+ input.setAttribute("aria-invalid", String(!!error));
415
+ input.required = node.validation?.required === true;
401
416
  label.textContent = node.label ?? "";
402
417
  const value = resolveInterfaceBinding(snapshot, node.binding);
403
- if (input.type === "checkbox")
418
+ if (input instanceof HTMLInputElement && input.type === "checkbox")
404
419
  input.checked = value === true;
405
420
  else {
406
421
  const shown = display(value);
@@ -458,8 +473,8 @@ export function mountInterface(target, controller, components = {}, onError) {
458
473
  function prepare(previous, node, snapshot, staged) {
459
474
  const same = previous.node.key === node.key && previous.node.component === node.component &&
460
475
  (node.component !== "Button" || previous.node.action === node.action) &&
461
- (node.component !== "Input" || (previous.node.binding?.path === node.binding?.path && previous.node.inputType === node.inputType)) &&
462
- ["Stack", "Row", "Text", "Heading", "Status", "Input", "Button", "Table", "List"].includes(node.component);
476
+ (!isInterfaceField(node) || (previous.node.binding?.path === node.binding?.path && previous.node.inputType === node.inputType)) &&
477
+ ["Stack", "Row", "Text", "Heading", "Status", "Input", "Textarea", "Select", "Button", "Table", "List"].includes(node.component);
463
478
  if (!same) {
464
479
  const next = create(node, snapshot, true);
465
480
  staged.push(next);
@@ -7,6 +7,28 @@ export interface InterfaceCondition {
7
7
  binding: InterfaceBinding;
8
8
  equals: InterfaceValue;
9
9
  }
10
+ export type InterfaceConditionExpression = InterfaceCondition | {
11
+ binding: InterfaceBinding;
12
+ operator: "ne" | "lt" | "lte" | "gt" | "gte";
13
+ value: InterfaceValue;
14
+ } | {
15
+ all: InterfaceConditionExpression[];
16
+ } | {
17
+ any: InterfaceConditionExpression[];
18
+ } | {
19
+ not: InterfaceConditionExpression;
20
+ };
21
+ export interface InterfaceValidation {
22
+ required?: boolean;
23
+ minLength?: number;
24
+ maxLength?: number;
25
+ min?: number;
26
+ max?: number;
27
+ }
28
+ export interface InterfaceOption {
29
+ value: string;
30
+ label: string;
31
+ }
10
32
  export interface InterfaceColumn {
11
33
  key: string;
12
34
  label: string;
@@ -17,7 +39,11 @@ export interface InterfaceNode {
17
39
  children?: InterfaceNode[];
18
40
  text?: string | InterfaceBinding;
19
41
  binding?: InterfaceBinding;
20
- visible?: InterfaceCondition;
42
+ visible?: InterfaceConditionExpression;
43
+ disabled?: InterfaceConditionExpression;
44
+ validation?: InterfaceValidation;
45
+ options?: InterfaceOption[];
46
+ validate?: boolean;
21
47
  action?: string;
22
48
  label?: string;
23
49
  inputType?: "text" | "number" | "checkbox";
@@ -49,10 +75,28 @@ export type BuiltinInterfaceNode = (BuiltinBase & {
49
75
  source: "state";
50
76
  };
51
77
  inputType?: "text" | "number" | "checkbox";
78
+ validation?: InterfaceValidation;
79
+ }) | (BuiltinBase & {
80
+ component: "Textarea";
81
+ label: string;
82
+ binding: InterfaceBinding & {
83
+ source: "state";
84
+ };
85
+ validation?: InterfaceValidation;
86
+ }) | (BuiltinBase & {
87
+ component: "Select";
88
+ label: string;
89
+ binding: InterfaceBinding & {
90
+ source: "state";
91
+ };
92
+ options: InterfaceOption[];
93
+ validation?: InterfaceValidation;
52
94
  }) | (BuiltinBase & {
53
95
  component: "Button";
54
96
  text: string | InterfaceBinding;
55
97
  action: string;
98
+ disabled?: InterfaceConditionExpression;
99
+ validate?: boolean;
56
100
  }) | (BuiltinBase & {
57
101
  component: "Table";
58
102
  binding: InterfaceBinding;
@@ -120,13 +164,25 @@ export declare function getInterfaceCapabilities(): {
120
164
  readonly event: null;
121
165
  };
122
166
  readonly Input: {
123
- readonly properties: readonly ["key", "component", "visible", "ariaLabel", "label", "binding", "inputType"];
167
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "label", "binding", "inputType", "validation"];
124
168
  readonly required: readonly ["label", "binding"];
125
169
  readonly children: false;
126
170
  readonly event: "state change";
127
171
  };
172
+ readonly Textarea: {
173
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "label", "binding", "validation"];
174
+ readonly required: readonly ["label", "binding"];
175
+ readonly children: false;
176
+ readonly event: "state change";
177
+ };
178
+ readonly Select: {
179
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "label", "binding", "validation", "options"];
180
+ readonly required: readonly ["label", "binding", "options"];
181
+ readonly children: false;
182
+ readonly event: "state change";
183
+ };
128
184
  readonly Button: {
129
- readonly properties: readonly ["key", "component", "visible", "ariaLabel", "text", "action"];
185
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "text", "action", "disabled", "validate"];
130
186
  readonly required: readonly ["text", "action"];
131
187
  readonly children: false;
132
188
  readonly event: "action";
@@ -157,6 +213,10 @@ export declare function getInterfaceCapabilities(): {
157
213
  text: string;
158
214
  binding: string;
159
215
  visible: string;
216
+ disabled: string;
217
+ validation: string;
218
+ validate: string;
219
+ options: string;
160
220
  action: string;
161
221
  label: string;
162
222
  inputType: string[];
@@ -193,4 +253,9 @@ export declare function createInterfaceController(definition: InterfaceDefinitio
193
253
  onError?: (error: unknown) => void;
194
254
  }): InterfaceController;
195
255
  export declare function resolveInterfaceBinding(snapshot: InterfaceSnapshot, bindingValue: InterfaceBinding): unknown;
256
+ /** Evaluate a validated condition without coercion or side effects. Missing numeric values never match. */
257
+ export declare function evaluateInterfaceCondition(condition: InterfaceConditionExpression, snapshot: Pick<InterfaceSnapshot, "data" | "state">): boolean;
258
+ export declare function isInterfaceField(node: Pick<InterfaceNode, "component">): boolean;
259
+ /** Errors are recomputed from accepted state; hidden fields are excluded, including hidden ancestors. */
260
+ export declare function getInterfaceValidation(snapshot: Pick<InterfaceSnapshot, "definition" | "data" | "state">): Record<string, string>;
196
261
  export {};
package/dist/interface.js CHANGED
@@ -8,17 +8,19 @@ const vocabulary = {
8
8
  Text: { properties: [...commonProperties, "text"], required: [], children: false, event: null },
9
9
  Heading: { properties: [...commonProperties, "text"], required: ["text"], children: false, event: null },
10
10
  Status: { properties: [...commonProperties, "text"], required: [], children: false, event: null },
11
- Input: { properties: [...commonProperties, "label", "binding", "inputType"], required: ["label", "binding"], children: false, event: "state change" },
12
- Button: { properties: [...commonProperties, "text", "action"], required: ["text", "action"], children: false, event: "action" },
11
+ Input: { properties: [...commonProperties, "label", "binding", "inputType", "validation"], required: ["label", "binding"], children: false, event: "state change" },
12
+ Textarea: { properties: [...commonProperties, "label", "binding", "validation"], required: ["label", "binding"], children: false, event: "state change" },
13
+ Select: { properties: [...commonProperties, "label", "binding", "validation", "options"], required: ["label", "binding", "options"], children: false, event: "state change" },
14
+ Button: { properties: [...commonProperties, "text", "action", "disabled", "validate"], required: ["text", "action"], children: false, event: "action" },
13
15
  Table: { properties: [...commonProperties, "binding", "columns"], required: ["binding", "columns"], children: false, event: null },
14
16
  List: { properties: [...commonProperties, "binding"], required: ["binding"], children: false, event: null },
15
17
  };
16
18
  const builtins = new Set(Object.keys(vocabulary));
17
- const nodeKeys = new Set(["key", "component", "children", "text", "binding", "visible", "action", "label", "inputType", "columns", "ariaLabel"]);
19
+ const nodeKeys = new Set(["key", "component", "children", "text", "binding", "visible", "action", "label", "inputType", "columns", "ariaLabel", "disabled", "validation", "options", "validate"]);
18
20
  /** JSON-safe description of the built-in vocabulary. Host extensions are trusted code and are never advertised here. */
19
21
  export function getInterfaceCapabilities() {
20
- return copyData({ version: 1, components: vocabulary, bindings: { sources: ["data", "state"], path: "dot-separated own-property path", missing: "empty display", visibility: "strict scalar equality" },
21
- propertyTypes: { key: "identifier", component: "component name", children: "node array", text: "string or binding", binding: "binding", visible: "{binding,equals:scalar}", action: "declared action identifier", label: "string", inputType: ["text", "number", "checkbox"], columns: "array of {key:identifier,label:string}", ariaLabel: "string" },
22
+ return copyData({ version: 1, components: vocabulary, bindings: { sources: ["data", "state"], path: "dot-separated own-property path", missing: "empty display", visibility: "strict scalar equality; ne, lt, lte, gt, gte; all, any, not" },
23
+ propertyTypes: { key: "identifier", component: "component name", children: "node array", text: "string or binding", binding: "binding", visible: "condition", disabled: "Button condition", validation: "{required,minLength,maxLength,min,max}", validate: "Button boolean", options: "Select array of {value:string,label:string}", action: "declared action identifier", label: "string", inputType: ["text", "number", "checkbox"], columns: "array of {key:identifier,label:string}", ariaLabel: "string" },
22
24
  events: { action: { fields: ["action", "source", "state", "revision"], declaration: "definition.actions and host allowedActions" }, state: { type: "scalar", source: "state binding" } },
23
25
  limits: { treeDepth: 16, nodes: 512, textLength: 4096, tableRows: 100, tableColumns: 20, listItems: 100, transportBytes: 1048576 } });
24
26
  }
@@ -68,7 +70,7 @@ function validateBindings(definition, data, state) {
68
70
  if (items !== undefined && (!Array.isArray(items) || items.length > 100 || items.some((item) => !scalar(item))))
69
71
  throw new Error("Invalid or oversized list items");
70
72
  }
71
- if (node.component === "Input") {
73
+ if (isInterfaceField(node)) {
72
74
  const current = valueAt(state, node.binding.path);
73
75
  if (current !== undefined && !scalar(current))
74
76
  throw new Error("Invalid input state");
@@ -81,13 +83,13 @@ function retainedState(previous, next, oldState) {
81
83
  if (previous.id !== next.id)
82
84
  return state;
83
85
  const inputs = new Map();
84
- visitNodes(previous.screen, (node) => { if (node.component === "Input")
86
+ visitNodes(previous.screen, (node) => { if (isInterfaceField(node))
85
87
  inputs.set(node.key, node); });
86
88
  visitNodes(next.screen, (node) => {
87
- if (node.component !== "Input")
89
+ if (!isInterfaceField(node))
88
90
  return;
89
91
  const old = inputs.get(node.key);
90
- if (!old || old.inputType !== node.inputType || old.binding?.path !== node.binding?.path)
92
+ if (!old || old.component !== node.component || old.inputType !== node.inputType || old.binding?.path !== node.binding?.path)
91
93
  return;
92
94
  const path = node.binding.path;
93
95
  const oldValue = valueAt(oldState, path);
@@ -153,13 +155,12 @@ export function validateInterfaceDefinition(value, components = []) {
153
155
  }
154
156
  if (node.binding !== undefined)
155
157
  binding(node.binding);
156
- if (node.visible !== undefined) {
157
- if (!plain(node.visible))
158
- throw new Error("Invalid visibility condition");
159
- exact(node.visible, new Set(["binding", "equals"]));
160
- binding(node.visible.binding);
161
- if (!scalar(node.visible.equals))
162
- throw new Error("Invalid visibility condition");
158
+ if (node.visible !== undefined)
159
+ validateCondition(node.visible);
160
+ if (node.disabled !== undefined) {
161
+ if (node.component !== "Button")
162
+ throw new Error("Only Button accepts disabled");
163
+ validateCondition(node.disabled);
163
164
  }
164
165
  for (const field of ["label", "ariaLabel"])
165
166
  if (node[field] !== undefined && (typeof node[field] !== "string" || node[field].length > 256))
@@ -168,11 +169,44 @@ export function validateInterfaceDefinition(value, components = []) {
168
169
  throw new Error("Action is not declared by the host");
169
170
  if (node.inputType !== undefined && !["text", "number", "checkbox"].includes(node.inputType))
170
171
  throw new Error("Invalid input type");
172
+ if (node.validate !== undefined && (node.component !== "Button" || typeof node.validate !== "boolean"))
173
+ throw new Error("Invalid validate flag");
174
+ if (node.options !== undefined) {
175
+ if (node.component !== "Select" || !Array.isArray(node.options) || node.options.length < 1 || node.options.length > 100)
176
+ throw new Error("Invalid select options");
177
+ const values = new Set();
178
+ for (const option of node.options) {
179
+ if (!plain(option))
180
+ throw new Error("Invalid option");
181
+ exact(option, new Set(["value", "label"]));
182
+ if (typeof option.value !== "string" || option.value.length > 256 || typeof option.label !== "string" || option.label.length > 256 || values.has(option.value))
183
+ throw new Error("Invalid option");
184
+ values.add(option.value);
185
+ }
186
+ }
187
+ if (node.validation !== undefined) {
188
+ if (!isInterfaceField(node) || !plain(node.validation))
189
+ throw new Error("Invalid validation");
190
+ exact(node.validation, new Set(["required", "minLength", "maxLength", "min", "max"]));
191
+ const rules = node.validation;
192
+ if (rules.required !== undefined && typeof rules.required !== "boolean")
193
+ throw new Error("Invalid required rule");
194
+ for (const key of ["minLength", "maxLength", "min", "max"])
195
+ if (rules[key] !== undefined && (typeof rules[key] !== "number" || !Number.isFinite(rules[key])))
196
+ throw new Error("Invalid validation bound");
197
+ for (const key of ["minLength", "maxLength"])
198
+ if (rules[key] !== undefined && (!Number.isInteger(rules[key]) || rules[key] < 0 || rules[key] > 4096 || node.inputType === "number" || node.inputType === "checkbox"))
199
+ throw new Error("Invalid length rule");
200
+ if ((rules.min !== undefined || rules.max !== undefined) && (node.component !== "Input" || node.inputType !== "number"))
201
+ throw new Error("Numeric rules require number input");
202
+ if (rules.minLength > rules.maxLength || rules.min > rules.max)
203
+ throw new Error("Reversed validation bounds");
204
+ }
171
205
  if (node.columns !== undefined) {
172
206
  if (!Array.isArray(node.columns) || node.columns.length < 1 || node.columns.length > 20 || node.columns.some((col) => !plain(col) || Object.keys(col).some((key) => key !== "key" && key !== "label") || !name(col.key) || typeof col.label !== "string" || col.label.length > 256))
173
207
  throw new Error("Invalid table columns");
174
208
  }
175
- if (node.component === "Input" && (!node.binding || node.binding.source !== "state" || !node.label))
209
+ if (isInterfaceField(node) && (!node.binding || node.binding.source !== "state" || !node.label))
176
210
  throw new Error("Input needs a state binding and label");
177
211
  if (node.component === "Button" && (!node.action || !node.text))
178
212
  throw new Error("Button needs an action and text");
@@ -262,8 +296,11 @@ export function createInterfaceController(definition, options = {}) {
262
296
  prepared.push(renderer(copySnapshot(candidate)));
263
297
  if (disposed)
264
298
  throw new Error("Interface controller was disposed during update");
265
- for (const renderer of prepared)
299
+ for (const renderer of prepared) {
266
300
  renderer.commit();
301
+ if (disposed)
302
+ throw new Error("Interface controller was disposed during update");
303
+ }
267
304
  current = nextDefinition;
268
305
  data = nextData;
269
306
  state = nextState;
@@ -339,6 +376,11 @@ export function createInterfaceController(definition, options = {}) {
339
376
  throw new Error("Interface update already in progress");
340
377
  if (!current.actions?.includes(action) || !findAction(current.screen, source, action, data, state))
341
378
  throw new Error("Action is not available");
379
+ let submit = false;
380
+ visitNodes(current.screen, node => { if (node.key === source)
381
+ submit = node.validate === true; });
382
+ if (submit && Object.keys(getInterfaceValidation({ definition: current, data, state })).length)
383
+ throw new Error("Interface validation failed");
342
384
  dispatching = true;
343
385
  try {
344
386
  const result = options.onEvent?.({ action, source, state: copyData(state), revision });
@@ -366,10 +408,117 @@ export function createInterfaceController(definition, options = {}) {
366
408
  return controller;
367
409
  }
368
410
  function findAction(node, key, action, data, state) {
369
- if (node.visible && valueAt(node.visible.binding.source === "data" ? data : state, node.visible.binding.path) !== node.visible.equals)
411
+ if (node.visible && !evaluateInterfaceCondition(node.visible, { data, state }))
412
+ return false;
413
+ if (node.disabled && evaluateInterfaceCondition(node.disabled, { data, state }))
370
414
  return false;
371
415
  return (node.key === key && node.action === action) || (node.children?.some((child) => findAction(child, key, action, data, state)) ?? false);
372
416
  }
373
417
  export function resolveInterfaceBinding(snapshot, bindingValue) {
374
418
  return valueAt(bindingValue.source === "data" ? snapshot.data : snapshot.state, bindingValue.path);
375
419
  }
420
+ function validateCondition(value) {
421
+ let count = 0;
422
+ function visit(value, depth) {
423
+ if (!plain(value) || depth > 8 || ++count > 64)
424
+ throw new Error("Invalid condition or condition limit exceeded");
425
+ if (Object.hasOwn(value, "all") || Object.hasOwn(value, "any")) {
426
+ const key = Object.hasOwn(value, "all") ? "all" : "any";
427
+ exact(value, new Set([key]));
428
+ const terms = value[key];
429
+ if (!Array.isArray(terms) || terms.length < 1 || terms.length > 16)
430
+ throw new Error("Invalid condition terms");
431
+ for (const term of terms)
432
+ visit(term, depth + 1);
433
+ }
434
+ else if (Object.hasOwn(value, "not")) {
435
+ exact(value, new Set(["not"]));
436
+ visit(value.not, depth + 1);
437
+ }
438
+ else {
439
+ binding(value.binding);
440
+ if (Object.hasOwn(value, "equals")) {
441
+ exact(value, new Set(["binding", "equals"]));
442
+ if (!scalar(value.equals))
443
+ throw new Error("Invalid condition value");
444
+ }
445
+ else {
446
+ exact(value, new Set(["binding", "operator", "value"]));
447
+ if (!["ne", "lt", "lte", "gt", "gte"].includes(value.operator) || !scalar(value.value))
448
+ throw new Error("Invalid condition operator or value");
449
+ if (value.operator !== "ne" && typeof value.value !== "number")
450
+ throw new Error("Numeric condition needs a number");
451
+ }
452
+ }
453
+ }
454
+ visit(value, 0);
455
+ }
456
+ /** Evaluate a validated condition without coercion or side effects. Missing numeric values never match. */
457
+ export function evaluateInterfaceCondition(condition, snapshot) {
458
+ if ("all" in condition)
459
+ return condition.all.every(term => evaluateInterfaceCondition(term, snapshot));
460
+ if ("any" in condition)
461
+ return condition.any.some(term => evaluateInterfaceCondition(term, snapshot));
462
+ if ("not" in condition)
463
+ return !evaluateInterfaceCondition(condition.not, snapshot);
464
+ const value = valueAt(snapshot[condition.binding.source], condition.binding.path);
465
+ if ("equals" in condition)
466
+ return value === condition.equals;
467
+ if (condition.operator === "ne")
468
+ return value !== condition.value;
469
+ if (typeof value !== "number" || !Number.isFinite(value) || typeof condition.value !== "number")
470
+ return false;
471
+ switch (condition.operator) {
472
+ case "lt": return value < condition.value;
473
+ case "lte": return value <= condition.value;
474
+ case "gt": return value > condition.value;
475
+ case "gte": return value >= condition.value;
476
+ }
477
+ }
478
+ export function isInterfaceField(node) {
479
+ return ["Input", "Textarea", "Select"].includes(node.component);
480
+ }
481
+ /** Errors are recomputed from accepted state; hidden fields are excluded, including hidden ancestors. */
482
+ export function getInterfaceValidation(snapshot) {
483
+ const errors = {};
484
+ function visit(node) {
485
+ if (node.visible && !evaluateInterfaceCondition(node.visible, snapshot))
486
+ return;
487
+ if (isInterfaceField(node)) {
488
+ const value = valueAt(snapshot.state, node.binding.path);
489
+ const rules = node.validation ?? {};
490
+ const empty = value === undefined || value === null || value === "";
491
+ let error = "";
492
+ if (rules.required && (empty || (node.inputType === "checkbox" && value !== true)))
493
+ error = "This field is required.";
494
+ else if (!empty) {
495
+ if (node.inputType === "number") {
496
+ if (typeof value !== "number" || !Number.isFinite(value))
497
+ error = "Enter a number.";
498
+ else if (rules.min !== undefined && value < rules.min)
499
+ error = `Minimum value is ${rules.min}.`;
500
+ else if (rules.max !== undefined && value > rules.max)
501
+ error = `Maximum value is ${rules.max}.`;
502
+ }
503
+ else if (node.inputType === "checkbox") {
504
+ if (typeof value !== "boolean")
505
+ error = "Enter a boolean.";
506
+ }
507
+ else if (typeof value !== "string")
508
+ error = "Enter text.";
509
+ else if (rules.minLength !== undefined && value.length < rules.minLength)
510
+ error = `Minimum length is ${rules.minLength}.`;
511
+ else if (rules.maxLength !== undefined && value.length > rules.maxLength)
512
+ error = `Maximum length is ${rules.maxLength}.`;
513
+ else if (node.component === "Select" && !node.options.some(option => option.value === value))
514
+ error = "Choose an available option.";
515
+ }
516
+ if (error)
517
+ errors[node.key] = error;
518
+ }
519
+ for (const child of node.children ?? [])
520
+ visit(child);
521
+ }
522
+ visit(snapshot.definition.screen);
523
+ return errors;
524
+ }
package/docs/DECISIONS.md CHANGED
@@ -9,7 +9,7 @@ Baseline: commit `4c073681e69ef5e323e5ba1a0ff11b4e93898eac`, Astra audit dated 2
9
9
  5. Real calendar dates/UTC instants are required across backend and field validators. Format-only acceptance of impossible dates is intentionally removed. Existing schema vocabulary and signatures remain intact.
10
10
  6. Compiler output has no source/declaration maps, avoiding references to omitted source files. Build manifests fingerprint the source, build scripts, package metadata, compiler configuration and outputs. Packing builds and verifies; artifacts and legal files are inspected in a clean consumer. Repeated tarball bytes must match.
11
11
  7. LICENSE/NOTICE apply to this independent package. No third-party dependency is bundled in dist; dependency licenses remain in their own packages. Production licenses are inventoried separately from laboratory tooling, with an explicit allowlist that fails on an unreviewed license.
12
- 8. Publication enablement removes `private: true` while retaining the failing `prepublishOnly` guard against source-directory publication. Only an exact qualified tarball is eligible for separately authorized publication; npm does not run directory lifecycle hooks for a tarball. Public npm access and the `preview` tag are explicit defaults. Qualification requires the public metadata, verifies directory refusal and performs a tarball-only dry-run with matching integrity. Neither qualification nor CI publishes or supplies publication credentials. The maintainer must approve the final SHA-256 and release action explicitly.
12
+ 8. Publication enablement removes `private: true` while retaining the failing `prepublishOnly` guard against source-directory publication. Only an exact qualified tarball is eligible for separately authorized publication; npm does not run directory lifecycle hooks for a tarball. Public npm access was the initial default, with the `preview` tag for release candidates. The stable 0.4.0 metadata sets `latest`. Qualification requires the public metadata, verifies directory refusal and performs a tarball-only dry-run with matching integrity. Neither qualification nor CI publishes or supplies publication credentials. The maintainer must approve the final SHA-256 and release action explicitly.
13
13
 
14
14
  Strict output checks exposed previously undeclared lab results. Required compatibility-only fixes now return explicit public DTOs for Orders, Tickets, and Tasks; history remains available through order.history instead of the undeclared order.get.history field. No validator exception was added. A targeted output-contract regression covers the adapters.
15
15
 
@@ -0,0 +1,9 @@
1
+ # Veil UI 0.4.0 — Developer Preview
2
+
3
+ Veil UI 0.4.0 adds bounded compound conditions, conditional visibility and Button disabling, validated multi-input submissions, Select, and Textarea to the universal interface API. Existing root, `/inspection`, `/composition`, `/interface`, and `/dom` entry points remain available. The [validated request example](../examples/universal/validated.mjs) demonstrates the new controls. [v0.4.0.md](v0.4.0.md) describes the contract and migration details.
4
+
5
+ The package is ESM. Node.js 24 or newer is the qualified server baseline; the DOM entry point is qualified in Chrome at mobile and desktop sizes. TypeScript 6.0.2 with strict NodeNext resolution is the qualified type baseline. UI has one runtime dependency, Zod 4.x. Core remains an optional peer with range `^0.4.0`: the interface and DOM APIs do not require it, while capability graph consumers can install Core. Published Core 0.4.0 is the supported and qualified integration baseline. Isolated 0.5.0 and 0.6.0 probes do not expand the declared peer range.
6
+
7
+ Hosts must bound raw bytes before parsing, allowlist actions, validate inputs again at the service boundary, and authorize effects. Trusted custom components own their external resources. Candidate preparation failures preserve controller state, revision, and mounted view. A commit-phase exception preserves controller state and revision, but cannot generally undo already applied DOM changes or external effects; renderer commits must be nonthrowing. Live-model reliability, Firefox, WebKit, and actual screen-reader behavior are outside the current qualification evidence.
8
+
9
+ Install the exact qualified tarball for review with `npm install /absolute/path/veil-runtime-ui-0.4.0.tgz`. Publication and the proposed `v0.4.0` Git tag require separate maintainer approval of the final source commit and artifact SHA-256. This document is release text, not a publication announcement.
@@ -0,0 +1,9 @@
1
+ # Veil UI 0.4.1 — Developer Preview patch
2
+
3
+ Veil UI 0.4.1 preserves the published 0.4.0 runtime API. The patch documents and tests a runnable [backend integration example in the source repository](https://github.com/veil-runtime/veil-ui-lab/tree/main/packages/ui/examples/backend). Its server, application, and browser fixture are excluded from the npm package. Existing root, `/inspection`, `/composition`, `/interface`, and `/dom` entry points remain available. [v0.4.0.md](v0.4.0.md) describes the interface contract and migration details.
4
+
5
+ The package is ESM. Node.js 24 or newer is the qualified server baseline; the DOM entry point is qualified in Chrome at mobile and desktop sizes. TypeScript 6.0.2 with strict NodeNext resolution is the qualified type baseline. UI has one runtime dependency, Zod 4.x. Core remains an optional peer with range `^0.4.0`: the interface and DOM APIs do not require it, while capability graph consumers can install Core. Published Core 0.4.0 is the supported and qualified integration baseline. Isolated 0.5.0 and 0.6.0 probes do not expand the declared peer range.
6
+
7
+ Hosts must bound raw bytes before parsing, allowlist actions, validate inputs again at the service boundary, and authorize effects. Trusted custom components own their external resources. Candidate preparation failures preserve controller state, revision, and mounted view. A commit-phase exception preserves controller state and revision, but cannot generally undo already applied DOM changes or external effects; renderer commits must be nonthrowing. Live-model reliability, Firefox, WebKit, and actual screen-reader behavior are outside the current qualification evidence.
8
+
9
+ Install the exact qualified tarball for review with `npm install /absolute/path/veil-runtime-ui-0.4.1.tgz`. Publication and any `v0.4.1` Git tag require separate maintainer approval of the final source commit and artifact SHA-256. The published 0.4.0 package and existing `v0.4.0` tag remain unchanged. This document is release text, not a publication announcement.
@@ -13,4 +13,4 @@ The npm tarball includes this library's compiled JavaScript/declarations, docume
13
13
 
14
14
  MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause, and 0BSD are the reviewed permissive license categories accepted by the qualification inventory. The [Apache Software Foundation third-party policy, Category A](https://www.apache.org/legal/resolved.html#category-a) lists Apache, MIT/ISC, BSD, and 0BSD as acceptable for inclusion subject to their notice requirements. Here those packages remain external dependencies; their own grants/notices are retained. No compatibility exception or relicensing of dependencies is claimed.
15
15
 
16
- The release gate checks the actual downloaded production graph, including the nonoptional Core peer, against this explicit license list and writes a versioned inventory. It also extracts the exact tarball and checks LICENSE and NOTICE byte-for-byte against the reviewed source files. Neither a package.json license label alone nor root lab dependency metadata is treated as proof of distribution contents.
16
+ The release gate checks the actual downloaded production graph in its Core consumer, as well as UI's runtime dependency independently, against this explicit license list and writes a versioned inventory. Core is an optional peer for UI, but is installed explicitly in that consumer. It also extracts the exact tarball and checks LICENSE and NOTICE byte-for-byte against the reviewed source files. Neither a package.json license label alone nor root lab dependency metadata is treated as proof of distribution contents.
package/docs/v0.4.0.md ADDED
@@ -0,0 +1,51 @@
1
+ # v0.4.0 Developer Preview
2
+
3
+ Version 0.4.0 extends the existing universal interface descriptor. Existing descriptors and controller APIs remain supported. No historical tag is changed.
4
+
5
+ ## Fields and submission
6
+
7
+ Input, Select, and Textarea accept `validation: { required, minLength, maxLength, min, max }`. Every property is optional. Required is boolean; lengths are integers from 0 to 4096; numeric bounds are finite numbers. Reversed bounds are rejected. Length rules apply to text fields; numeric bounds require Input with inputType number. Checkbox required means true. String lengths count UTF-16 code units. Whitespace is text, not empty.
8
+
9
+ Missing, null, and empty string are empty. Optional empty fields pass. Nonempty values must match the field type; Select values must be declared options. Validation does not prevent editing invalid intermediate state. `getInterfaceValidation(snapshot)` returns field-keyed errors for visible fields, excluding hidden ancestors. Errors are recomputed after state, data, and definition changes without an extra revision or event.
10
+
11
+ A Button with `validate: true` validates all visible fields before dispatch. Invalid submission throws `Interface validation failed`, delivers no host event, and does not change state or revision. The DOM click handler isolates the error through onError. Buttons without this flag retain existing behavior, supporting cancel/navigation actions. Hosts must use validated buttons for submission and still authorize and validate requests at their service boundary.
12
+
13
+ Fields have native labels, required state, aria-invalid, and aria-describedby linked to live field feedback. Errors appear immediately, including initial required errors. Hidden field state is retained but not validated for submission. There is no asynchronous validation or executable rule language.
14
+
15
+ ## Components
16
+
17
+ ```json
18
+ {"key":"kind","component":"Select","label":"Kind","binding":{"source":"state","path":"kind"},"options":[{"value":"supplies","label":"Supplies"}],"validation":{"required":true}}
19
+ ```
20
+
21
+ Select accepts 1–100 unique string values and labels, each at most 256 characters. Empty string denotes no selection. A native empty prompt precedes nonempty options.
22
+
23
+ ```json
24
+ {"key":"notes","component":"Textarea","label":"Details","binding":{"source":"state","path":"notes"},"validation":{"maxLength":1000}}
25
+ ```
26
+
27
+ Both components use scalar state bindings and the existing lifecycle/transaction machinery. State retention requires unchanged definition ID, field key, component, input type, binding path, and a default of the same scalar type. Group related fields with Stack and Heading; no competing form container is introduced.
28
+
29
+ ## Conditions
30
+
31
+ Legacy `{ "binding": { "source":"state", "path":"name" }, "equals":"Ada" }` remains valid. Comparisons use `{binding, operator, value}` with ne, lt, lte, gt, gte. Numeric comparisons require finite numeric operands and never coerce strings. Missing values fail numeric comparisons; strict inequality against a scalar is true for missing values.
32
+
33
+ Compose conditions with `{all:[condition,...]}`, `{any:[condition,...]}`, or `{not:condition}`. Each condition tree permits depth 8, 64 nodes, and 1–16 operands per logical array. Unknown properties/operators are rejected. Conditions read own-property data/state bindings only.
34
+
35
+ All nodes accept visible; Buttons also accept disabled conditions. Hidden ancestors and disabled Buttons prevent direct controller dispatch as well as DOM interaction. Host allowedActions remains independent of conditions.
36
+
37
+ ## Integration and boundaries
38
+
39
+ Import universal APIs from `@veil-runtime/ui/interface` and DOM APIs from `@veil-runtime/ui/dom`; no Core or AI provider is required. Capability APIs retain legitimate Core types. The optional peer range remains ^0.4.0; isolated 0.5.0/0.6.0 probes are evidence, not a claim that all integration contracts have been qualified.
40
+
41
+ The existing component allowlist, safe bindings, bounded data/tree limits, text-only rendering, revisions, and atomic candidate rejection remain in force. New fields do not execute actions or network calls. See `examples/universal/validated.mjs` for a validated form, conditional fields, multiple input types, and host-controlled submission. The repository-local universal playground includes it; no external website is changed.
42
+
43
+ ## Compatibility and transaction qualification
44
+
45
+ The exported `InterfaceCondition` retains the v0.3.0 equality interface with required `binding` and `equals`. `InterfaceConditionExpression` is the expanded union used by visibility, disabling, and the evaluator. Existing interface inheritance and direct equality-binding access remain supported.
46
+
47
+ Candidate preparation failures leave controller state, revision, and the mounted DOM unchanged. Disposal during preparation or commit rejects controller state advancement and stops subsequent commits. A commit-phase exception preserves controller state/revision and discards prepared candidates in reverse order, but discard is resource cleanup, not a general rollback of already-applied DOM or external side effects. Renderer commits must therefore be nonthrowing; arbitrary commit failures cannot promise cross-renderer visual rollback. This limitation is explicitly tested rather than claimed as full transaction atomicity.
48
+
49
+ ## Connecting an existing backend
50
+
51
+ Application code handles `onEvent` and owns HTTP, credentials, authorization, and teardown. The [source repository's backend example](https://github.com/veil-runtime/veil-ui-lab/tree/main/packages/ui/examples/backend) demonstrates a local HTTP service and a mounted interface using request identities, abort signals, revision checks, safe error messages and user-initiated retry. From a source checkout, run the package build, then `node packages/ui/examples/backend/server.mjs`; see the example's README for the complete pattern. The server, application, and browser fixture are not included in the npm package.
@@ -5,3 +5,7 @@ const human = mountHuman(document.getElementById('human'));
5
5
  const agent = mountAgent(document.getElementById('agent'));
6
6
  const application = mountApplication(document.getElementById('application'));
7
7
  globalThis.veilExamples = { human, agent, application };
8
+
9
+ import { mountValidated } from './validated.mjs';
10
+ const validatedTarget = document.createElement('section'); validatedTarget.id = 'validated'; document.body.append(validatedTarget);
11
+ globalThis.veilValidated = mountValidated(validatedTarget);
@@ -0,0 +1,21 @@
1
+ import { createMountedInterface } from '../../dist/dom.js';
2
+ export const validatedDefinition = {
3
+ id: 'request', actions: ['submit'], state: { name: '', kind: '', notes: '', quantity: 1 },
4
+ screen: { key: 'request', component: 'Stack', children: [
5
+ { key: 'title', component: 'Heading', text: 'Request supplies' },
6
+ { key: 'name', component: 'Input', label: 'Your name', binding: { source: 'state', path: 'name' }, validation: { required: true, minLength: 2, maxLength: 80 } },
7
+ { key: 'kind', component: 'Select', label: 'Request type', binding: { source: 'state', path: 'kind' }, options: [{ value: 'supplies', label: 'Supplies' }, { value: 'other', label: 'Other' }], validation: { required: true } },
8
+ { key: 'quantity', component: 'Input', inputType: 'number', label: 'Quantity', binding: { source: 'state', path: 'quantity' }, validation: { required: true, min: 1, max: 20 }, visible: { binding: { source: 'state', path: 'kind' }, equals: 'supplies' } },
9
+ { key: 'notes', component: 'Textarea', label: 'Details', binding: { source: 'state', path: 'notes' }, validation: { required: true, maxLength: 1000 }, visible: { binding: { source: 'state', path: 'kind' }, equals: 'other' } },
10
+ { key: 'submit', component: 'Button', text: 'Submit request', action: 'submit', validate: true },
11
+ { key: 'feedback', component: 'Status', text: { source: 'data', path: 'message' } },
12
+ ] },
13
+ };
14
+ export function mountValidated(target) {
15
+ const controller = createMountedInterface(target, validatedDefinition, {
16
+ allowedActions: ['submit'],
17
+ onEvent(event) { controller.updateData({ message: `Received request from ${event.state.name}.` }); },
18
+ onError() { /* Field feedback explains invalid submissions; hosts may also log errors. */ },
19
+ });
20
+ return controller;
21
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@veil-runtime/ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/",
7
7
  "tag": "latest"
8
8
  },
9
9
  "type": "module",
10
- "description": "Veil UI universal interface foundation and deterministic capability descriptions.",
10
+ "description": "Veil UI universal interface foundation and deterministic capability descriptions (Developer Preview).",
11
11
  "files": [
12
12
  "dist",
13
13
  "README.md",
@@ -1,7 +0,0 @@
1
- # Veil UI 0.3.0
2
-
3
- Stable release prepared for maintainer approval; publication has not occurred. Intended npm dist-tag: `latest`. After publication: `npm install @veil-runtime/ui@0.3.0`. Before publication, install the exact qualified tarball.
4
-
5
- Includes the universal interface engine, DOM mounting, composition, and capability-description APIs from the merged candidate. All 13 review remediations are retained, including bounded notification feedback when onError updates controller state. No public API changes were made during stable preparation. Node.js 24+, ESM; TypeScript baseline 6.0.2. Core 0.4.x is optional for interface/composition and required for capability execution.
6
-
7
- See [UNIVERSAL-INTERFACE.md](UNIVERSAL-INTERFACE.md) and [API.md](API.md). Live-model reliability, Firefox, WebKit, and actual screen-reader behavior remain unverified. Custom components are trusted host code.