@veil-runtime/ui 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +100 -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 +95 -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/composition.d.ts +43 -0
  18. package/dist/composition.js +227 -0
  19. package/dist/derive-input-fields.d.ts +4 -0
  20. package/dist/derive-input-fields.js +63 -0
  21. package/dist/derive-output-view.d.ts +26 -0
  22. package/dist/derive-output-view.js +97 -0
  23. package/dist/dom.d.ts +31 -0
  24. package/dist/dom.js +554 -0
  25. package/dist/execution-request.d.ts +13 -0
  26. package/dist/execution-request.js +58 -0
  27. package/dist/experience-orchestration.d.ts +13 -0
  28. package/dist/experience-orchestration.js +31 -0
  29. package/dist/format-semantic-value.d.ts +4 -0
  30. package/dist/format-semantic-value.js +15 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.js +20 -0
  33. package/dist/input-schema.d.ts +25 -0
  34. package/dist/input-schema.js +131 -0
  35. package/dist/interaction-resolution.d.ts +27 -0
  36. package/dist/interaction-resolution.js +179 -0
  37. package/dist/interface-transaction.d.ts +9 -0
  38. package/dist/interface-transaction.js +14 -0
  39. package/dist/interface.d.ts +196 -0
  40. package/dist/interface.js +375 -0
  41. package/dist/output-presentation-suggestion-schema.d.ts +12 -0
  42. package/dist/output-presentation-suggestion-schema.js +8 -0
  43. package/dist/output-schema.d.ts +26 -0
  44. package/dist/output-schema.js +213 -0
  45. package/dist/presentation-composition.d.ts +29 -0
  46. package/dist/presentation-composition.js +25 -0
  47. package/dist/presentation-context.d.ts +6 -0
  48. package/dist/presentation-context.js +1 -0
  49. package/dist/presentation-suggestion-schema.d.ts +15 -0
  50. package/dist/presentation-suggestion-schema.js +10 -0
  51. package/dist/registry.d.ts +21 -0
  52. package/dist/registry.js +71 -0
  53. package/dist/relationship-interpretation.d.ts +9 -0
  54. package/dist/relationship-interpretation.js +16 -0
  55. package/dist/validate-application.d.ts +24 -0
  56. package/dist/validate-application.js +253 -0
  57. package/dist/validation-safety.d.ts +6 -0
  58. package/dist/validation-safety.js +90 -0
  59. package/dist/workflow.d.ts +18 -0
  60. package/dist/workflow.js +3 -0
  61. package/docs/API.md +19 -0
  62. package/docs/COMPOSITION.md +67 -0
  63. package/docs/DECISIONS.md +16 -0
  64. package/docs/RELEASE-0.3.0-rc.1.md +25 -0
  65. package/docs/RELEASE-0.3.0-rc.2.md +9 -0
  66. package/docs/RELEASE-0.3.0.md +7 -0
  67. package/docs/THIRD_PARTY.md +16 -0
  68. package/docs/UNIVERSAL-INTERFACE.md +105 -0
  69. package/docs/third-party/ZOD-LICENSE.txt +21 -0
  70. package/examples/composed/app.mjs +44 -0
  71. package/examples/composed/build.mjs +7 -0
  72. package/examples/composed/index.html +34 -0
  73. package/examples/standalone/index.mjs +78 -0
  74. package/examples/standalone/package.json +8 -0
  75. package/examples/universal/agent.mjs +52 -0
  76. package/examples/universal/application.mjs +16 -0
  77. package/examples/universal/build.mjs +6 -0
  78. package/examples/universal/human.mjs +16 -0
  79. package/examples/universal/index.html +4 -0
  80. package/examples/universal/main.mjs +7 -0
  81. package/package.json +90 -4
@@ -0,0 +1,25 @@
1
+ import { assertBoundedData, copyData } from "./validation-safety.js";
2
+ import { z } from "zod";
3
+ const emphasisSchema = z.enum(["normal", "prominent", "subtle"]);
4
+ const densitySchema = z.enum(["comfortable", "compact"]);
5
+ const groupSchema = z.object({ id: z.string().regex(/^[a-zA-Z0-9_-]+$/).max(48), title: z.string().min(1).max(80).optional(), description: z.string().min(1).max(240).optional(), emphasis: emphasisSchema, density: densitySchema, sections: z.array(z.string().min(1).max(120)).min(1).max(256) }).strict();
6
+ export const presentationPlanSchema = z.object({ title: z.string().min(1).max(100).optional(), description: z.string().min(1).max(240).optional(), groups: z.array(groupSchema).max(12) }).strict();
7
+ export function sectionId(kind, capability) { return kind + ":" + capability; }
8
+ export function buildPresentationEnvelope(primary, related) { return { applicationSurface: primary.capability, sections: [{ id: sectionId("primary", primary.capability), capability: primary.capability, surface: primary.view.surface, label: primary.view.title, required: true, movable: false, groupable: false, defaultOrder: 0 }, ...related.map((section, index) => ({ id: sectionId("related", section.capability), capability: section.capability, surface: section.view?.surface ?? "error", label: section.label, required: true, movable: true, groupable: true, defaultOrder: index + 1 }))], immutable: ["capabilities", "fields", "metrics", "bindings", "workflow", "risk", "actions", "surface semantics", "execution"] }; }
9
+ export function deterministicPresentation(envelope) { return { groups: envelope.sections.filter((section) => section.movable).map((section, index) => ({ id: "section_" + (index + 1), title: section.label, emphasis: "normal", density: "comfortable", sections: [section.id] })) }; }
10
+ export function validatePresentationPlan(envelope, value) { assertBoundedData(envelope); assertBoundedData(value); const parsed = presentationPlanSchema.safeParse(value); if (!parsed.success)
11
+ throw new Error("Invalid generated composition: " + parsed.error.message); const allowed = envelope.sections.filter((section) => section.movable).map((section) => section.id), received = parsed.data.groups.flatMap((group) => group.sections); if (new Set(received).size !== received.length)
12
+ throw new Error("Generated composition duplicated a section"); if (received.some((id) => !allowed.includes(id)))
13
+ throw new Error("Generated composition referenced an unknown or fixed section"); if (allowed.length !== received.length || allowed.some((id) => !received.includes(id)))
14
+ throw new Error("Generated composition omitted a required section"); const groupIds = parsed.data.groups.map((group) => group.id); if (new Set(groupIds).size !== groupIds.length)
15
+ throw new Error("Generated composition duplicated a group id"); return parsed.data; }
16
+ export async function composePresentation(envelope, strategy, generate) { const baseline = deterministicPresentation(envelope); if (strategy === "deterministic")
17
+ return { plan: baseline, provenance: { strategy: "deterministic", requestedStrategy: "deterministic", fallback: false, validated: true } }; try {
18
+ if (!generate)
19
+ throw new Error("No presentation provider configured");
20
+ const proposed = await generate(copyData(envelope));
21
+ return { plan: validatePresentationPlan(envelope, proposed), provenance: { strategy: "generative", requestedStrategy: "generative", fallback: false, validated: true } };
22
+ }
23
+ catch (error) {
24
+ return { plan: baseline, provenance: { strategy: "deterministic", requestedStrategy: "generative", fallback: true, validated: true, reason: error instanceof Error ? error.message : "generation-failed" } };
25
+ } }
@@ -0,0 +1,6 @@
1
+ export type DeviceContext = "desktop" | "mobile";
2
+ export type InteractionMode = "full" | "compact";
3
+ export interface ViewContext {
4
+ device: DeviceContext;
5
+ mode: InteractionMode;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const generatedFieldPresentationSchema: z.ZodObject<{
3
+ name: z.ZodString;
4
+ label: z.ZodString;
5
+ }, z.core.$strict>;
6
+ export declare const generatedPresentationSchema: z.ZodObject<{
7
+ title: z.ZodString;
8
+ submitLabel: z.ZodString;
9
+ fields: z.ZodArray<z.ZodObject<{
10
+ name: z.ZodString;
11
+ label: z.ZodString;
12
+ }, z.core.$strict>>;
13
+ }, z.core.$strict>;
14
+ export type GeneratedPresentation = z.infer<typeof generatedPresentationSchema>;
15
+ export type GeneratedFieldPresentation = z.infer<typeof generatedFieldPresentationSchema>;
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export const generatedFieldPresentationSchema = z.object({
3
+ name: z.string().min(1).max(160),
4
+ label: z.string().min(1).max(160),
5
+ }).strict();
6
+ export const generatedPresentationSchema = z.object({
7
+ title: z.string().min(1).max(160),
8
+ submitLabel: z.string().min(1).max(160),
9
+ fields: z.array(generatedFieldPresentationSchema).max(256),
10
+ }).strict();
@@ -0,0 +1,21 @@
1
+ import type { Capability } from "@veil-runtime/core";
2
+ import type { ApplicationManifest, CapabilityRelationship, CapabilityTransition, DerivedApplication } from "./application.js";
3
+ import { type CapabilityOutputSchema } from "./output-schema.js";
4
+ import { type ApplicationValidationResult } from "./validate-application.js";
5
+ export interface UIRegistryDefinitions {
6
+ capabilities: readonly Capability[];
7
+ applications: readonly ApplicationManifest[];
8
+ outputSchemas?: Readonly<Record<string, CapabilityOutputSchema>>;
9
+ relationships?: readonly CapabilityRelationship[];
10
+ transitions?: readonly CapabilityTransition[];
11
+ }
12
+ export interface UIRegistry {
13
+ listApplications(): ApplicationManifest[];
14
+ getApplication(applicationId: string): ApplicationManifest | undefined;
15
+ getOutputSchema(capabilityName: string): CapabilityOutputSchema | undefined;
16
+ getRelationships(capabilityName: string): CapabilityRelationship[];
17
+ getTransition(capabilityName: string): CapabilityTransition | undefined;
18
+ deriveApplication(applicationId: string): DerivedApplication | undefined;
19
+ validateApplication(application: DerivedApplication): ApplicationValidationResult;
20
+ }
21
+ export declare function createUIRegistry(definitions: UIRegistryDefinitions): UIRegistry;
@@ -0,0 +1,71 @@
1
+ import { copyData } from "./validation-safety.js";
2
+ import { validateRichInputSchema, } from "./input-schema.js";
3
+ import { validateOutputSchema, } from "./output-schema.js";
4
+ import { validateApplication, } from "./validate-application.js";
5
+ export function createUIRegistry(definitions) {
6
+ // Snapshot caller metadata; executable functions stay host-owned and are never exposed.
7
+ definitions = {
8
+ capabilities: definitions.capabilities.map((capability) => ({
9
+ ...capability,
10
+ ...(capability.inputSchema ? { inputSchema: copyData(capability.inputSchema) } : {}),
11
+ })),
12
+ applications: copyData(definitions.applications),
13
+ ...(definitions.outputSchemas ? { outputSchemas: copyData(definitions.outputSchemas) } : {}),
14
+ ...(definitions.relationships ? { relationships: copyData(definitions.relationships) } : {}),
15
+ ...(definitions.transitions ? { transitions: copyData(definitions.transitions) } : {}),
16
+ };
17
+ const capabilities = new Map(definitions.capabilities.map((capability) => [capability.name, capability]));
18
+ const applications = new Map(definitions.applications.map((application) => [application.id, application]));
19
+ const outputSchemas = definitions.outputSchemas ?? {};
20
+ const relationships = definitions.relationships ?? [];
21
+ const transitions = definitions.transitions ?? [];
22
+ const getCapability = (name) => capabilities.get(name);
23
+ function deriveApplication(applicationId) {
24
+ const application = applications.get(applicationId);
25
+ if (!application)
26
+ return undefined;
27
+ const derivedCapabilities = application.capabilities.map((capabilityName) => {
28
+ const capability = getCapability(capabilityName);
29
+ if (!capability) {
30
+ throw new Error("Application \"" + applicationId + "\" references unknown capability: " + capabilityName);
31
+ }
32
+ const inputSchema = (capability.inputSchema ?? {});
33
+ validateRichInputSchema(inputSchema);
34
+ const outputSchema = Object.hasOwn(outputSchemas, capability.name)
35
+ ? outputSchemas[capability.name]
36
+ : undefined;
37
+ if (outputSchema)
38
+ validateOutputSchema(outputSchema);
39
+ const transition = transitions.find((candidate) => candidate.capability === capability.name);
40
+ return {
41
+ name: capability.name,
42
+ version: capability.version,
43
+ description: capability.description,
44
+ risk: capability.risk,
45
+ inputSchema,
46
+ ...(outputSchema ? { outputSchema } : {}),
47
+ relationships: relationships.filter((relationship) => relationship.sourceCapability === capability.name),
48
+ ...(transition ? { transition } : {}),
49
+ };
50
+ });
51
+ return copyData({
52
+ id: application.id,
53
+ title: application.title,
54
+ ...(application.description
55
+ ? { description: application.description }
56
+ : {}),
57
+ entryCapability: application.entryCapability,
58
+ capabilities: derivedCapabilities,
59
+ ...(application.workflow ? { workflow: application.workflow } : {}),
60
+ });
61
+ }
62
+ return {
63
+ listApplications: () => copyData([...applications.values()]),
64
+ getApplication: (applicationId) => copyData(applications.get(applicationId)),
65
+ getOutputSchema: (capabilityName) => copyData(Object.hasOwn(outputSchemas, capabilityName) ? outputSchemas[capabilityName] : undefined),
66
+ getRelationships: (capabilityName) => copyData(relationships.filter((relationship) => relationship.sourceCapability === capabilityName)),
67
+ getTransition: (capabilityName) => copyData(transitions.find((transition) => transition.capability === capabilityName)),
68
+ deriveApplication,
69
+ validateApplication: (application) => validateApplication(application, getCapability),
70
+ };
71
+ }
@@ -0,0 +1,9 @@
1
+ export type RelationshipRole = "action" | "navigate" | "related";
2
+ export type RelationshipInteraction = "application-action" | "contextual-action" | "row-action" | "navigation" | "related-read";
3
+ export interface InterpretableRelationship {
4
+ role?: RelationshipRole;
5
+ bindings: Record<string, string>;
6
+ conditions?: readonly unknown[];
7
+ }
8
+ export declare function interpretRelationship(relationship: InterpretableRelationship, source: "application-entry" | "table-row" | "detail"): RelationshipInteraction;
9
+ export declare function isExplicitAction(interaction: RelationshipInteraction): boolean;
@@ -0,0 +1,16 @@
1
+ export function interpretRelationship(relationship, source) {
2
+ if (relationship.role === "related")
3
+ return "related-read";
4
+ if (relationship.role === "navigate")
5
+ return "navigation";
6
+ const contextual = Object.keys(relationship.bindings).length > 0
7
+ || (relationship.conditions?.length ?? 0) > 0;
8
+ if (source === "table-row" && contextual)
9
+ return "row-action";
10
+ if (contextual)
11
+ return "contextual-action";
12
+ return "application-action";
13
+ }
14
+ export function isExplicitAction(interaction) {
15
+ return interaction !== "related-read";
16
+ }
@@ -0,0 +1,24 @@
1
+ import type { Capability } from "@veil-runtime/core";
2
+ import type { DerivedApplication } from "./application.js";
3
+ export type ApplicationValidationSeverity = "error" | "warning";
4
+ export type ApplicationValidationIssueCode = "ENTRY_CAPABILITY_NOT_FOUND" | "DUPLICATE_CAPABILITY" | "RELATIONSHIP_TARGET_NOT_FOUND" | "TRANSITION_TARGET_NOT_FOUND" | "RELATIONSHIP_SOURCE_FIELD_NOT_FOUND" | "RELATIONSHIP_TARGET_FIELD_NOT_FOUND" | "RELATIONSHIP_REQUIRED_INPUT_UNBOUND" | "CAPABILITY_RISK_MISMATCH" | "WORKFLOW_STATE_FIELD_NOT_FOUND" | "WORKFLOW_ENTRY_STATE_NOT_FOUND" | "DUPLICATE_WORKFLOW_STATE" | "WORKFLOW_STATE_NOT_FOUND" | "WORKFLOW_CAPABILITY_NOT_FOUND" | "CONDITION_FIELD_NOT_FOUND" | "RESULT_BINDING_SOURCE_FIELD_NOT_FOUND" | "RESULT_BINDING_TARGET_FIELD_NOT_FOUND" | "RESULT_BINDING_REQUIRED_INPUT_UNBOUND" | "RELATED_TARGET_NOT_READ" | "RELATED_SELF_REFERENCE" | "RELATED_DIRECT_CYCLE" | "DUPLICATE_RELATED_CAPABILITY" | "RELATED_BINDING_TYPE_MISMATCH" | "RELATED_REQUIRED_INPUT_UNBOUND" | "INVALID_RELATIONSHIP_ROLE" | "UNSUPPORTED_NESTED_RELATED_COMPOSITION";
5
+ export interface ApplicationValidationIssue {
6
+ code: ApplicationValidationIssueCode;
7
+ severity: ApplicationValidationSeverity;
8
+ message: string;
9
+ applicationId: string;
10
+ capability?: string;
11
+ targetCapability?: string;
12
+ field?: string;
13
+ }
14
+ export interface ApplicationValidationResult {
15
+ valid: boolean;
16
+ issues: ApplicationValidationIssue[];
17
+ }
18
+ export interface AuthoritativeCapabilityMetadata {
19
+ name: Capability["name"];
20
+ risk: Capability["risk"];
21
+ inputSchema?: Capability["inputSchema"];
22
+ }
23
+ export type CapabilityMetadataResolver = (capabilityName: string) => AuthoritativeCapabilityMetadata | undefined;
24
+ export declare function validateApplication(application: DerivedApplication, resolveCapability: CapabilityMetadataResolver): ApplicationValidationResult;
@@ -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,19 @@
1
+ # Public API inventory
2
+
3
+ The v0.3.0-rc.2 candidate adds `@veil-runtime/ui/interface` (`defineInterface`, `validateInterfaceDefinition`, `createInterfaceController`, `getInterfaceCapabilities`, `resolveInterfaceBinding`, and types including `BuiltinInterfaceDefinition`) and `mountInterface`, `createMountedInterface`, plus `InterfaceDOMComponentInstance` in `/dom`. `InterfaceDOMComponent` may still return an `HTMLElement`; it may now return `{ element, update?, dispose? }`. Updates prepare detached custom instances before controller acceptance. See [UNIVERSAL-INTERFACE.md](UNIVERSAL-INTERFACE.md). The v0.2.0 composition API remains available through `/composition` and `/dom`. The inventory below describes preserved v0.1.0 APIs.
4
+
5
+ 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 documented `/inspection`, `/composition`, `/interface`, and `/dom` subpaths, not paths inside dist.
6
+
7
+ | Area | Root modules / representative API |
8
+ |---|---|
9
+ | Input contracts | `input-schema`, `defineInputSchema`, `validateRichInputSchema`, `validateRichInputField`, `validateRichCapabilityInput`, `validateRichInputValue` |
10
+ | Output contracts | `output-schema`, `defineOutputSchema`, `validateOutputSchema`, `validateCapabilityOutput`, `getOutputSchemaFields`, `isAggregateOutputSchema` |
11
+ | Registry and graphs | `application`, `registry`, `createUIRegistry`, `validate-application`, `validateApplication` |
12
+ | Descriptive views | `capability-view`, `derive-input-fields`, `deriveUIField`, `deriveUIFields`, `derive-output-view`, `deriveOutputView`, `deriveDeterministicCapabilityView` |
13
+ | Interaction | `interaction-resolution`, `resolveCapabilityInteraction`, `validateFieldValue`, `serializeDateTimeLocal`, `buildCapabilityExecutionPayload`, `execution-request`, `parseApplicationExecutionRequest` |
14
+ | Composition semantics | `relationship-interpretation`, `interpretRelationship`, `workflow`, `experience-orchestration`, `deriveApplicationExperience`, `deriveApplicationActions` |
15
+ | Presentation types and formatting | `presentation-context`, `composed-surface`, `format-semantic-value`, `formatSemanticValue` |
16
+ | Experimental optional presentation | `capability-view-derivation` provider interfaces and `deriveCapabilityView`; `presentation-composition`, `buildPresentationEnvelope`, `deterministicPresentation`, `validatePresentationPlan`, `composePresentation`, `presentationPlanSchema`, `sectionId` |
17
+ | Inspection subpath | `@veil-runtime/ui/inspection`: `buildCompositionInspection` and inspection model types |
18
+
19
+ Qualification exercises the legacy Core capability API and the independent universal interface subpaths, strict public declarations, and browser bundling. Published export conditions intentionally support ESM import, not direct CommonJS require. Core is an optional peer and is unnecessary for `/interface` and `/dom` consumers. UI has no compiled Core runtime import. A host may use UI's descriptors in a browser and keep Core execution on its server.