@veil-runtime/ui 0.0.0-stage → 0.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +96 -2
  5. package/SECURITY.md +11 -0
  6. package/dist/application.d.ts +56 -0
  7. package/dist/application.js +9 -0
  8. package/dist/build-manifest.json +82 -0
  9. package/dist/capability-view-derivation.d.ts +40 -0
  10. package/dist/capability-view-derivation.js +185 -0
  11. package/dist/capability-view.d.ts +170 -0
  12. package/dist/capability-view.js +1 -0
  13. package/dist/composed-surface.d.ts +67 -0
  14. package/dist/composed-surface.js +1 -0
  15. package/dist/composition-inspection.d.ts +51 -0
  16. package/dist/composition-inspection.js +102 -0
  17. package/dist/derive-input-fields.d.ts +4 -0
  18. package/dist/derive-input-fields.js +63 -0
  19. package/dist/derive-output-view.d.ts +26 -0
  20. package/dist/derive-output-view.js +97 -0
  21. package/dist/execution-request.d.ts +13 -0
  22. package/dist/execution-request.js +58 -0
  23. package/dist/experience-orchestration.d.ts +13 -0
  24. package/dist/experience-orchestration.js +31 -0
  25. package/dist/format-semantic-value.d.ts +4 -0
  26. package/dist/format-semantic-value.js +15 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.js +18 -0
  29. package/dist/input-schema.d.ts +23 -0
  30. package/dist/input-schema.js +131 -0
  31. package/dist/interaction-resolution.d.ts +27 -0
  32. package/dist/interaction-resolution.js +179 -0
  33. package/dist/output-presentation-suggestion-schema.d.ts +12 -0
  34. package/dist/output-presentation-suggestion-schema.js +8 -0
  35. package/dist/output-schema.d.ts +26 -0
  36. package/dist/output-schema.js +213 -0
  37. package/dist/presentation-composition.d.ts +29 -0
  38. package/dist/presentation-composition.js +23 -0
  39. package/dist/presentation-context.d.ts +6 -0
  40. package/dist/presentation-context.js +1 -0
  41. package/dist/presentation-suggestion-schema.d.ts +15 -0
  42. package/dist/presentation-suggestion-schema.js +10 -0
  43. package/dist/registry.d.ts +21 -0
  44. package/dist/registry.js +71 -0
  45. package/dist/relationship-interpretation.d.ts +9 -0
  46. package/dist/relationship-interpretation.js +16 -0
  47. package/dist/validate-application.d.ts +24 -0
  48. package/dist/validate-application.js +253 -0
  49. package/dist/validation-safety.d.ts +6 -0
  50. package/dist/validation-safety.js +90 -0
  51. package/dist/workflow.d.ts +18 -0
  52. package/dist/workflow.js +3 -0
  53. package/docs/API.md +17 -0
  54. package/docs/DECISIONS.md +16 -0
  55. package/docs/THIRD_PARTY.md +16 -0
  56. package/docs/third-party/ZOD-LICENSE.txt +21 -0
  57. package/examples/standalone/index.mjs +78 -0
  58. package/examples/standalone/package.json +8 -0
  59. package/package.json +69 -4
@@ -0,0 +1,253 @@
1
+ import { assertBoundedData } from "./validation-safety.js";
2
+ import { getOutputSchemaFields } from "./output-schema.js";
3
+ function hasTopLevelOutputField(schema, fieldName) {
4
+ return Boolean(schema && Object.hasOwn(getOutputSchemaFields(schema), fieldName));
5
+ }
6
+ function getRelationshipSourceField(schema, fieldName) {
7
+ if (!schema)
8
+ return undefined;
9
+ const fields = getOutputSchemaFields(schema);
10
+ const definitions = Object.values(fields);
11
+ if (definitions.length === 1 && definitions[0]?.type === "array" && definitions[0].items?.type === "object")
12
+ return definitions[0].items.fields?.[fieldName];
13
+ return fields[fieldName];
14
+ }
15
+ function hasRelationshipSourceField(schema, fieldName) {
16
+ if (!schema) {
17
+ return false;
18
+ }
19
+ const definitions = Object.values(schema);
20
+ if (definitions.length === 1) {
21
+ const definition = definitions[0];
22
+ if (definition.type === "array" && definition.items?.type === "object") {
23
+ return Boolean(definition.items.fields && Object.hasOwn(definition.items.fields, fieldName));
24
+ }
25
+ }
26
+ return hasTopLevelOutputField(schema, fieldName);
27
+ }
28
+ function validateCapabilityRisk(application, capability, resolveCapability, issues) {
29
+ const authoritative = resolveCapability(capability.name);
30
+ if (authoritative &&
31
+ authoritative.risk !== capability.risk) {
32
+ issues.push({
33
+ code: "CAPABILITY_RISK_MISMATCH",
34
+ severity: "error",
35
+ message: `Capability "${capability.name}" has derived risk "${capability.risk}" but authoritative risk "${authoritative.risk}".`,
36
+ applicationId: application.id,
37
+ capability: capability.name,
38
+ });
39
+ }
40
+ }
41
+ function validateRelationship(application, source, capabilityNames, resolveCapability, issues) {
42
+ for (const relationship of source.relationships) {
43
+ const targetName = relationship.targetCapability;
44
+ if (!capabilityNames.has(targetName)) {
45
+ issues.push({
46
+ code: "RELATIONSHIP_TARGET_NOT_FOUND",
47
+ severity: "error",
48
+ message: `Relationship from "${source.name}" targets capability "${targetName}", which is not in application "${application.id}".`,
49
+ applicationId: application.id,
50
+ capability: source.name,
51
+ targetCapability: targetName,
52
+ });
53
+ }
54
+ const targetCapability = resolveCapability(targetName);
55
+ for (const [targetField, sourceField] of Object.entries(relationship.bindings)) {
56
+ if (!hasRelationshipSourceField(source.outputSchema, sourceField)) {
57
+ issues.push({
58
+ code: "RELATIONSHIP_SOURCE_FIELD_NOT_FOUND",
59
+ severity: "error",
60
+ message: `Relationship from "${source.name}" binds unknown source output field "${sourceField}".`,
61
+ applicationId: application.id,
62
+ capability: source.name,
63
+ targetCapability: targetName,
64
+ field: sourceField,
65
+ });
66
+ }
67
+ if (targetCapability &&
68
+ !Object.hasOwn(targetCapability.inputSchema ?? {}, targetField)) {
69
+ issues.push({
70
+ code: "RELATIONSHIP_TARGET_FIELD_NOT_FOUND",
71
+ severity: "error",
72
+ message: `Relationship to "${targetName}" binds unknown target input field "${targetField}".`,
73
+ applicationId: application.id,
74
+ capability: source.name,
75
+ targetCapability: targetName,
76
+ field: targetField,
77
+ });
78
+ }
79
+ }
80
+ for (const condition of relationship.conditions ?? []) {
81
+ if (!hasRelationshipSourceField(source.outputSchema, condition.field)) {
82
+ issues.push({
83
+ code: "CONDITION_FIELD_NOT_FOUND",
84
+ severity: "error",
85
+ message: `Relationship from "${source.name}" conditions on unknown output field "${condition.field}".`,
86
+ applicationId: application.id,
87
+ capability: source.name,
88
+ targetCapability: targetName,
89
+ field: condition.field,
90
+ });
91
+ }
92
+ }
93
+ if (targetCapability) {
94
+ for (const [fieldName, field] of Object.entries(targetCapability.inputSchema ?? {})) {
95
+ if (field.required &&
96
+ !Object.prototype.hasOwnProperty.call(relationship.bindings, fieldName)) {
97
+ issues.push({
98
+ code: "RELATIONSHIP_REQUIRED_INPUT_UNBOUND",
99
+ severity: "warning",
100
+ message: `Relationship from "${source.name}" does not bind required input "${fieldName}" on "${targetName}".`,
101
+ applicationId: application.id,
102
+ capability: source.name,
103
+ targetCapability: targetName,
104
+ field: fieldName,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ /*
110
+ * Duplicate target binding keys cannot be represented by the
111
+ * authoritative Record<string, string> binding type.
112
+ */
113
+ }
114
+ }
115
+ export function validateApplication(application, resolveCapability) {
116
+ assertBoundedData(application);
117
+ const issues = [];
118
+ const capabilityNames = new Set();
119
+ const duplicateNames = new Set();
120
+ for (const capability of application.capabilities) {
121
+ if (capabilityNames.has(capability.name) &&
122
+ !duplicateNames.has(capability.name)) {
123
+ duplicateNames.add(capability.name);
124
+ issues.push({
125
+ code: "DUPLICATE_CAPABILITY",
126
+ severity: "error",
127
+ message: `Application "${application.id}" contains capability "${capability.name}" more than once.`,
128
+ applicationId: application.id,
129
+ capability: capability.name,
130
+ });
131
+ }
132
+ capabilityNames.add(capability.name);
133
+ }
134
+ if (!capabilityNames.has(application.entryCapability)) {
135
+ issues.push({
136
+ code: "ENTRY_CAPABILITY_NOT_FOUND",
137
+ severity: "error",
138
+ message: `Entry capability "${application.entryCapability}" is not in application "${application.id}".`,
139
+ applicationId: application.id,
140
+ capability: application.entryCapability,
141
+ });
142
+ }
143
+ for (const capability of application.capabilities) {
144
+ for (const relationship of capability.relationships)
145
+ if (relationship.role !== undefined && !["action", "navigate", "related"].includes(relationship.role))
146
+ issues.push({ code: "INVALID_RELATIONSHIP_ROLE", severity: "error", message: "Invalid relationship role.", applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability });
147
+ const related = capability.relationships.filter((relationship) => relationship.role === "related");
148
+ const seenRelated = new Set();
149
+ for (const relationship of related) {
150
+ const target = application.capabilities.find((candidate) => candidate.name === relationship.targetCapability);
151
+ if (seenRelated.has(relationship.targetCapability))
152
+ issues.push({ code: "DUPLICATE_RELATED_CAPABILITY", severity: "error", message: "Related capability is declared more than once: " + relationship.targetCapability, applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability });
153
+ seenRelated.add(relationship.targetCapability);
154
+ if (relationship.targetCapability === capability.name)
155
+ issues.push({ code: "RELATED_SELF_REFERENCE", severity: "error", message: "A capability cannot compose itself automatically.", applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability });
156
+ if (target?.risk !== "read")
157
+ issues.push({ code: "RELATED_TARGET_NOT_READ", severity: "error", message: "Automatic related capability must be read-only: " + relationship.targetCapability, applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability });
158
+ if (target?.relationships.some((candidate) => candidate.role === "related"))
159
+ issues.push({ code: "UNSUPPORTED_NESTED_RELATED_COMPOSITION", severity: "error", message: "Nested automatic related composition is not supported.", applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability });
160
+ if (target?.relationships.some((candidate) => candidate.role === "related" && candidate.targetCapability === capability.name))
161
+ issues.push({ code: "RELATED_DIRECT_CYCLE", severity: "error", message: "Direct automatic-related cycle detected.", applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability });
162
+ for (const [targetField, sourceField] of Object.entries(relationship.bindings)) {
163
+ const sourceDefinition = getRelationshipSourceField(capability.outputSchema, sourceField);
164
+ const targetDefinition = target?.inputSchema[targetField];
165
+ if (sourceDefinition && targetDefinition && sourceDefinition.type !== targetDefinition.type)
166
+ issues.push({ code: "RELATED_BINDING_TYPE_MISMATCH", severity: "error", message: "Related binding type mismatch for " + targetField, applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability, field: targetField });
167
+ }
168
+ for (const [field, definition] of Object.entries(target?.inputSchema ?? {}))
169
+ if (definition.required && !Object.prototype.hasOwnProperty.call(relationship.bindings, field))
170
+ issues.push({ code: "RELATED_REQUIRED_INPUT_UNBOUND", severity: "error", message: "Automatic related capability is missing required binding: " + field, applicationId: application.id, capability: capability.name, targetCapability: relationship.targetCapability, field });
171
+ }
172
+ validateCapabilityRisk(application, capability, resolveCapability, issues);
173
+ validateRelationship(application, capability, capabilityNames, resolveCapability, issues);
174
+ const transitionTarget = capability.transition?.onSuccess?.capability;
175
+ if (transitionTarget &&
176
+ !capabilityNames.has(transitionTarget)) {
177
+ issues.push({
178
+ code: "TRANSITION_TARGET_NOT_FOUND",
179
+ severity: "error",
180
+ message: `Transition from "${capability.name}" targets capability "${transitionTarget}", which is not in application "${application.id}".`,
181
+ applicationId: application.id,
182
+ capability: capability.name,
183
+ targetCapability: transitionTarget,
184
+ });
185
+ }
186
+ const resultBindings = capability.transition?.onSuccess?.resultBindings ?? {};
187
+ const targetCapability = transitionTarget ? resolveCapability(transitionTarget) : undefined;
188
+ for (const [targetField, sourceField] of Object.entries(resultBindings)) {
189
+ if (!hasTopLevelOutputField(capability.outputSchema, sourceField)) {
190
+ issues.push({
191
+ code: "RESULT_BINDING_SOURCE_FIELD_NOT_FOUND", severity: "error",
192
+ message: `Transition from "${capability.name}" binds unknown result field "${sourceField}".`,
193
+ applicationId: application.id, capability: capability.name, ...(transitionTarget ? { targetCapability: transitionTarget } : {}), field: sourceField,
194
+ });
195
+ }
196
+ if (targetCapability && !Object.hasOwn(targetCapability.inputSchema ?? {}, targetField)) {
197
+ issues.push({
198
+ code: "RESULT_BINDING_TARGET_FIELD_NOT_FOUND", severity: "error",
199
+ message: `Transition to "${transitionTarget}" binds unknown input "${targetField}".`,
200
+ applicationId: application.id, capability: capability.name, ...(transitionTarget ? { targetCapability: transitionTarget } : {}), field: targetField,
201
+ });
202
+ }
203
+ }
204
+ if (targetCapability && capability.transition?.onSuccess) {
205
+ for (const [field, definition] of Object.entries(targetCapability.inputSchema ?? {})) {
206
+ if (definition.required && !Object.prototype.hasOwnProperty.call(resultBindings, field)) {
207
+ issues.push({
208
+ code: "RESULT_BINDING_REQUIRED_INPUT_UNBOUND", severity: "error",
209
+ message: `Transition from "${capability.name}" cannot supply required input "${field}" on "${transitionTarget}".`,
210
+ applicationId: application.id, capability: capability.name, ...(transitionTarget ? { targetCapability: transitionTarget } : {}), field,
211
+ });
212
+ }
213
+ }
214
+ }
215
+ /*
216
+ * CapabilityTransition contains navigation metadata only.
217
+ * Its type has no risk field, so it cannot override risk.
218
+ */
219
+ }
220
+ const workflow = application.workflow;
221
+ if (workflow) {
222
+ const stateFields = application.capabilities
223
+ .map((capability) => getRelationshipSourceField(capability.outputSchema, workflow.stateField))
224
+ .filter((field) => Boolean(field));
225
+ if (stateFields.length === 0) {
226
+ issues.push({
227
+ code: "WORKFLOW_STATE_FIELD_NOT_FOUND",
228
+ severity: "error",
229
+ message: `Workflow state field "${workflow.stateField}" is absent from authoritative output metadata.`,
230
+ applicationId: application.id,
231
+ field: workflow.stateField,
232
+ });
233
+ }
234
+ const states = new Set();
235
+ for (const state of workflow.states) {
236
+ if (states.has(state.id))
237
+ issues.push({ code: "DUPLICATE_WORKFLOW_STATE", severity: "error", message: `Workflow state "${state.id}" is duplicated.`, applicationId: application.id });
238
+ states.add(state.id);
239
+ }
240
+ if (!states.has(workflow.entryState))
241
+ issues.push({ code: "WORKFLOW_ENTRY_STATE_NOT_FOUND", severity: "error", message: `Workflow entry state "${workflow.entryState}" is not declared.`, applicationId: application.id });
242
+ for (const transition of workflow.transitions) {
243
+ if (!states.has(transition.from) || !states.has(transition.to))
244
+ issues.push({ code: "WORKFLOW_STATE_NOT_FOUND", severity: "error", message: `Workflow transition "${transition.capability}" references an unknown state.`, applicationId: application.id, capability: transition.capability });
245
+ if (!capabilityNames.has(transition.capability))
246
+ issues.push({ code: "WORKFLOW_CAPABILITY_NOT_FOUND", severity: "error", message: `Workflow transition references capability "${transition.capability}" outside the application.`, applicationId: application.id, capability: transition.capability });
247
+ }
248
+ }
249
+ return {
250
+ valid: !issues.some((issue) => issue.severity === "error"),
251
+ issues,
252
+ };
253
+ }
@@ -0,0 +1,6 @@
1
+ export declare function assertSafeKey(key: string): void;
2
+ /** Inspect descriptors, never getters; bound work before recursive or Zod validation. */
3
+ export declare function assertBoundedData(value: unknown): void;
4
+ export declare function copyData<T>(value: T): T;
5
+ export declare function isCalendarDate(value: string): boolean;
6
+ export declare function isUTCInstant(value: string): boolean;
@@ -0,0 +1,90 @@
1
+ /** Internal transport limits. Hosts must also limit bytes before decoding JSON. */
2
+ const limits = { depth: 32, nodes: 10000, keys: 256, items: 1000, string: 65536, bytes: 1048576 };
3
+ const reserved = new Set(["__proto__", "prototype", "constructor"]);
4
+ export function assertSafeKey(key) {
5
+ if (reserved.has(key))
6
+ throw new Error(`Reserved data key: ${key}`);
7
+ }
8
+ /** Inspect descriptors, never getters; bound work before recursive or Zod validation. */
9
+ export function assertBoundedData(value) {
10
+ const ancestors = new Set();
11
+ let nodes = 0;
12
+ let bytes = 0;
13
+ function visit(item, depth) {
14
+ if (++nodes > limits.nodes || depth > limits.depth)
15
+ throw new Error("Data traversal limit exceeded");
16
+ bytes += 8;
17
+ if (typeof item === "number" && !Number.isFinite(item))
18
+ throw new Error("Data value must be numeric and finite");
19
+ if (typeof item === "string") {
20
+ if (item.length > limits.string)
21
+ throw new Error("Data string limit exceeded");
22
+ bytes += item.length * 3; // Conservative UTF-8 upper bound for UTF-16 input.
23
+ }
24
+ else if (item !== null && typeof item === "object") {
25
+ if (ancestors.has(item))
26
+ throw new Error("Cyclic data is not supported");
27
+ const array = Array.isArray(item);
28
+ const proto = Object.getPrototypeOf(item);
29
+ if (!array && proto !== Object.prototype && proto !== null)
30
+ throw new Error("Data must use plain own properties");
31
+ if (array && item.length > limits.items)
32
+ throw new Error("Data collection limit exceeded");
33
+ const keys = Reflect.ownKeys(item);
34
+ if (keys.length > (array ? limits.items + 1 : limits.keys))
35
+ throw new Error("Data property limit exceeded");
36
+ if (array && keys.length !== item.length + 1)
37
+ throw new Error("Data arrays must be dense without extra properties");
38
+ ancestors.add(item);
39
+ for (const key of keys) {
40
+ if (array && key === "length")
41
+ continue;
42
+ if (typeof key !== "string")
43
+ throw new Error("Symbol data keys are not supported");
44
+ if (array && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= item.length))
45
+ throw new Error("Data arrays cannot contain extra properties");
46
+ if (key.length > limits.string)
47
+ throw new Error("Data key limit exceeded");
48
+ assertSafeKey(key);
49
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
50
+ if (!descriptor.enumerable || !("value" in descriptor))
51
+ throw new Error("Non-enumerable or accessor data is not supported");
52
+ bytes += key.length * 3;
53
+ visit(descriptor.value, depth + 1);
54
+ }
55
+ ancestors.delete(item);
56
+ }
57
+ else if (!["undefined", "boolean", "number"].includes(typeof item) && item !== null) {
58
+ throw new Error("Unsupported data value");
59
+ }
60
+ if (bytes > limits.bytes)
61
+ throw new Error("Data byte limit exceeded");
62
+ }
63
+ visit(value, 0);
64
+ }
65
+ export function copyData(value) {
66
+ assertBoundedData(value);
67
+ function copy(item) {
68
+ if (Array.isArray(item))
69
+ return item.map(copy);
70
+ if (item !== null && typeof item === "object") {
71
+ return Object.fromEntries(Object.entries(item).map(([key, child]) => [key, copy(child)]));
72
+ }
73
+ return item;
74
+ }
75
+ return copy(value);
76
+ }
77
+ export function isCalendarDate(value) {
78
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
79
+ return false;
80
+ const date = new Date(`${value}T00:00:00.000Z`);
81
+ return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
82
+ }
83
+ export function isUTCInstant(value) {
84
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value))
85
+ return false;
86
+ if (!isCalendarDate(value.slice(0, 10)))
87
+ return false;
88
+ const date = new Date(value);
89
+ return Number.isFinite(date.getTime()) && date.toISOString() === (value.length === 20 ? value.replace("Z", ".000Z") : value);
90
+ }
@@ -0,0 +1,18 @@
1
+ /** Declarative application metadata. This module does not execute workflows. */
2
+ export interface ApplicationWorkflowState {
3
+ id: string;
4
+ label: string;
5
+ terminal?: boolean;
6
+ }
7
+ export interface ApplicationWorkflowTransition {
8
+ capability: string;
9
+ from: string;
10
+ to: string;
11
+ }
12
+ export interface ApplicationWorkflowDefinition {
13
+ stateField: string;
14
+ entryState: string;
15
+ states: ApplicationWorkflowState[];
16
+ transitions: ApplicationWorkflowTransition[];
17
+ }
18
+ export declare function defineWorkflow<T extends ApplicationWorkflowDefinition>(workflow: T): T;
@@ -0,0 +1,3 @@
1
+ export function defineWorkflow(workflow) {
2
+ return workflow;
3
+ }
package/docs/API.md ADDED
@@ -0,0 +1,17 @@
1
+ # Public API inventory
2
+
3
+ The preview keeps all existing root exports; no existing symbol or subpath was removed. The root index exports modules below. All interfaces/types exported by those modules remain available in the declarations. Package consumers import through the root or `/inspection`, not paths inside dist.
4
+
5
+ | Area | Root modules / representative API |
6
+ |---|---|
7
+ | Input contracts | `input-schema`, `defineInputSchema`, `validateRichInputSchema`, `validateRichInputField`, `validateRichCapabilityInput`, `validateRichInputValue` |
8
+ | Output contracts | `output-schema`, `defineOutputSchema`, `validateOutputSchema`, `validateCapabilityOutput`, `getOutputSchemaFields`, `isAggregateOutputSchema` |
9
+ | Registry and graphs | `application`, `registry`, `createUIRegistry`, `validate-application`, `validateApplication` |
10
+ | Descriptive views | `capability-view`, `derive-input-fields`, `deriveUIField`, `deriveUIFields`, `derive-output-view`, `deriveOutputView`, `deriveDeterministicCapabilityView` |
11
+ | Interaction | `interaction-resolution`, `resolveCapabilityInteraction`, `validateFieldValue`, `serializeDateTimeLocal`, `buildCapabilityExecutionPayload`, `execution-request`, `parseApplicationExecutionRequest` |
12
+ | Composition semantics | `relationship-interpretation`, `interpretRelationship`, `workflow`, `experience-orchestration`, `deriveApplicationExperience`, `deriveApplicationActions` |
13
+ | Presentation types and formatting | `presentation-context`, `composed-surface`, `format-semantic-value`, `formatSemanticValue` |
14
+ | Experimental optional presentation | `capability-view-derivation` provider interfaces and `deriveCapabilityView`; `presentation-composition`, `buildPresentationEnvelope`, `deterministicPresentation`, `validatePresentationPlan`, `composePresentation`, `presentationPlanSchema`, `sectionId` |
15
+ | Inspection subpath | `@veil-runtime/ui/inspection`: `buildCompositionInspection` and inspection model types |
16
+
17
+ The qualification consumer exercises both supported subpaths, actual Core capability creation, strict public declarations, and browser bundling. Published export conditions intentionally support ESM import, not direct CommonJS require. Core is a nonoptional peer for the TypeScript capability contract; UI has no compiled Core runtime import. A host may use UI's descriptors in a browser and keep Core execution on its server.
@@ -0,0 +1,16 @@
1
+ # v0.1.0 release-candidate decisions
2
+
3
+ Baseline: commit `4c073681e69ef5e323e5ba1a0ff11b4e93898eac`, Astra audit dated 25 October 2026. Maintainer scope: deterministic composition-engine developer preview. Maintainer explicitly approved Apache License 2.0 and inclusion/verification of LICENSE and NOTICE on 25 October 2026.
4
+
5
+ 1. Preserve root and inspection exports. No renderer, authentication platform, model dependency, workflow engine, Emergence extraction, or Core/Sandbox changes. Optional provider/composition APIs stay experimental. ESM and Node 24+, TypeScript 6.0.2 are the qualified baseline. Consumers checking Core's transitive declarations without skipLibCheck need ES2022+DOM and Node ambient types.
6
+ 2. Support Core `^0.4.0`, qualify specifically against published `0.4.0`, and pin development Core to that published version. Root release development no longer depends on a sibling checkout. Older Core 0.1/0.3 support is withdrawn rather than claimed untested.
7
+ 3. Output validation rejects unknown own keys recursively. No implicit stripping, mutation, partial rendering, or permissive compatibility switch. This deliberately tightens pre-release behavior: hosts returning larger service records must map them to declared public DTOs. Inputs, metadata, bindings, and request envelopes reject reserved keys and nonplain/accessor data and use own-property membership. Boundary limits are documented in README.
8
+ 4. Registry definitions are snapshots and every metadata getter/graph is isolated. Callers replace the registry to refresh authoritative contracts. Optional providers receive copied metadata/view/context and an inert executor; the private deterministic baseline cannot be mutated through the callback request. Callback code itself is privileged host code and is not sandboxed. Suggestions and composition plans are bounded before Zod parsing. Zod's minimum is raised to a fixed release.
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
+ 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
+ 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.
13
+
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
+
16
+ Compatibility changes must be mentioned in release notes. Do not bypass a failed install with peer overrides or mask production audit results. Broad experimental lab checks can remain known failures only when separated from the supported engine checks and reported explicitly.
@@ -0,0 +1,16 @@
1
+ # Third-party licensing and distribution
2
+
3
+ The maintainer explicitly selected Apache License 2.0 for `@veil-runtime/ui` on 25 October 2026, consistent with Core. UI's LICENSE is the unmodified standard Apache License 2.0 text; NOTICE identifies this project and retains the external dependency distinction. Root laboratory licensing metadata is not reassigned by this package decision.
4
+
5
+ The npm tarball includes this library's compiled JavaScript/declarations, documentation, and standalone example. **It does not bundle third-party executable code or node_modules.** Zod remains a separately installed runtime dependency; Core remains a separately installed peer. A downstream application that bundles dependencies must preserve the applicable copyright and license notices for the code it redistributes.
6
+
7
+ | Dependency | Qualified version / constraint | License | Distribution treatment |
8
+ |---|---|---|---|
9
+ | Zod | 4.6.7 resolved, minimum ^4.6.6 | MIT | External runtime dependency. Copy of its copyright/license text retained in `docs/third-party/ZOD-LICENSE.txt`. |
10
+ | @veil-runtime/core | 0.4.0 qualified, peer ^0.4.0 | Apache-2.0 | External peer. Its own package includes LICENSE and NOTICE; neither is relabeled as UI's code. |
11
+ | Core's transitive runtime dependencies | Exact installed versions in qualification `production-licenses.json` and consumer lockfile | Per-package grants | External, not bundled. Qualification fails if an unreviewed license is encountered. |
12
+ | TypeScript / tsx / esbuild / Node types | Development tools only | Apache-2.0 / MIT as recorded in installed manifests | Not shipped as UI runtime code. |
13
+
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
+
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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Colin McDonnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createCapability, OperatorRuntime } from '@veil-runtime/core';
3
+ import {
4
+ createUIRegistry, deriveDeterministicCapabilityView, resolveCapabilityInteraction,
5
+ buildCapabilityExecutionPayload, validateRichCapabilityInput, validateCapabilityOutput,
6
+ } from '@veil-runtime/ui';
7
+ import { buildCompositionInspection } from '@veil-runtime/ui/inspection';
8
+
9
+ // A tiny existing service. Its business rules and storage stay outside UI.
10
+ const ticket = { id: 'T-1', status: 'open' };
11
+ let calls = 0;
12
+ const schema = {
13
+ ticketId: { type: 'string', required: true, description: 'Ticket identifier', minLength: 1 },
14
+ reason: { type: 'string', required: true, description: 'Resolution reason', minLength: 3, maxLength: 200 },
15
+ };
16
+ const outputSchema = {
17
+ id: { type: 'string', description: 'Ticket identifier' },
18
+ status: { type: 'string', description: 'Current status', enum: ['resolved'] },
19
+ };
20
+ const resolveTicket = createCapability({
21
+ name: 'ticket.resolve', version: '1.0.0', description: 'Resolve a support ticket',
22
+ risk: 'write', inputSchema: schema,
23
+ async execute({ input }) {
24
+ if (input.ticketId !== ticket.id || ticket.status !== 'open') throw new Error('Ticket cannot be resolved');
25
+ calls += 1;
26
+ ticket.status = 'resolved';
27
+ // Explicit public DTO: do not return internal service records or credentials.
28
+ return { id: ticket.id, status: ticket.status };
29
+ },
30
+ });
31
+ const registry = createUIRegistry({
32
+ capabilities: [resolveTicket],
33
+ applications: [{ id: 'support', title: 'Support', entryCapability: resolveTicket.name, capabilities: [resolveTicket.name] }],
34
+ outputSchemas: { [resolveTicket.name]: outputSchema },
35
+ });
36
+ const graph = registry.deriveApplication('support');
37
+ assert.equal(registry.validateApplication(graph).valid, true);
38
+ const view = deriveDeterministicCapabilityView({ capability: resolveTicket, outputSchema });
39
+ const interaction = resolveCapabilityInteraction(view);
40
+ const inspection = buildCompositionInspection(graph, resolveTicket.name, view, interaction);
41
+ assert.equal(inspection.active.view.surface, 'detail');
42
+
43
+ // Host-owned terminal renderer: consumes field descriptors; never executes a capability.
44
+ console.log('Support — Resolve ticket');
45
+ for (const field of interaction.humanFields) console.log(`${field.label}: ${field.type}${field.required ? ' (required)' : ''}`);
46
+ assert.throws(() => validateRichCapabilityInput(schema, { ticketId: ticket.id, reason: 'x' }));
47
+ console.log('Invalid input rejected before execution');
48
+ const input = buildCapabilityExecutionPayload(interaction, { ticketId: ticket.id, reason: 'Issue resolved' });
49
+
50
+ // This local fixture supplies identities. In a network host, authenticate on the
51
+ // server; never accept a principal or scopes supplied by browser form input.
52
+ const runtime = new OperatorRuntime({ authorizer: {
53
+ async authorize(context) {
54
+ return context.caller?.subject && context.caller.scopes?.includes('tickets:resolve')
55
+ ? { decision: 'allow' }
56
+ : { decision: 'deny', reason: 'tickets:resolve permission required' };
57
+ },
58
+ } });
59
+ runtime.use({ manifest: { name: 'standalone-support', version: '1.0.0', capabilities: [resolveTicket.name] }, capabilities: [resolveTicket] });
60
+ async function execute(principal, id) {
61
+ validateRichCapabilityInput(schema, input); // Server-side, independently of rendering.
62
+ return runtime.executePlan({
63
+ version: '1.0', id, goal: 'Resolve a ticket',
64
+ steps: [{ id: `${id}-step`, capability: resolveTicket.name, capabilityVersion: resolveTicket.version, input, reason: 'Explicit user request' }],
65
+ }, { caller: principal });
66
+ }
67
+ const denied = await execute({ subject: 'viewer', scopes: [] }, 'denied');
68
+ assert.notEqual(denied.steps[0]?.status, 'completed');
69
+ assert.equal(calls, 0);
70
+ assert.equal(ticket.status, 'open');
71
+ console.log('Denied by authoritative Core policy; service was not called');
72
+
73
+ const approved = await execute({ subject: 'operator', scopes: ['tickets:resolve'] }, 'approved');
74
+ assert.equal(approved.steps[0]?.status, 'completed');
75
+ validateCapabilityOutput(outputSchema, approved.steps[0].result); // Before transport/rendering.
76
+ assert.equal(calls, 1);
77
+ console.table([approved.steps[0].result]);
78
+ console.log('Approved execution completed; standalone example passed');
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "veil-ui-standalone-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": { "start": "node index.mjs" },
6
+ "dependencies": { "@veil-runtime/core": "0.4.0" },
7
+ "engines": { "node": ">=24" }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@veil-runtime/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/",
7
+ "tag": "preview"
8
+ },
9
+ "type": "module",
10
+ "description": "Veil UI \u2014 Deterministic Composition Engine (Developer Preview). Framework-neutral interface descriptions from governed capabilities.",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "SECURITY.md",
16
+ "docs",
17
+ "examples/standalone",
18
+ "LICENSE",
19
+ "NOTICE"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./inspection": {
29
+ "types": "./dist/composition-inspection.d.ts",
30
+ "import": "./dist/composition-inspection.js"
31
+ }
32
+ },
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "node scripts/build.mjs",
36
+ "build:no-dom": "node scripts/build.mjs",
37
+ "test": "node --import tsx --test --test-isolation=none test/*.test.ts test/*.test.mjs",
38
+ "check:boundary": "node scripts/check-boundary.mjs",
39
+ "typecheck:test": "tsc -p tsconfig.test.json",
40
+ "verify:dist": "node scripts/verify-dist.mjs",
41
+ "prepack": "npm run build && npm run verify:dist && npm run check:boundary",
42
+ "prepublishOnly": "node scripts/publication-hold.mjs",
43
+ "check": "npm run build && npm run typecheck:test && npm test && npm run verify:dist && npm run check:boundary"
44
+ },
45
+ "peerDependencies": {
46
+ "@veil-runtime/core": "^0.4.0"
47
+ },
48
+ "dependencies": {
49
+ "zod": "^4.6.6"
50
+ },
51
+ "devDependencies": {
52
+ "@veil-runtime/core": "0.4.0",
53
+ "@types/node": "^24.13.3",
54
+ "tsx": "^4.23.12",
55
+ "typescript": "6.0.2",
56
+ "esbuild": "^0.28.2"
57
+ },
58
+ "engines": {
59
+ "node": ">=24"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/veil-runtime/veil-ui-lab.git",
64
+ "directory": "packages/ui"
65
+ },
66
+ "homepage": "https://github.com/veil-runtime/veil-ui-lab/tree/main/packages/ui#readme",
67
+ "bugs": {
68
+ "url": "https://github.com/veil-runtime/veil-ui-lab/issues"
69
+ },
70
+ "license": "Apache-2.0"
71
+ }