@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,97 @@
1
+ import { getOutputSchemaFields, isAggregateOutputSchema, } from "./output-schema.js";
2
+ function humanise(value) {
3
+ return value
4
+ .replace(/[._-]+/g, " ")
5
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
6
+ .replace(/\b\w/g, (character) => character.toUpperCase());
7
+ }
8
+ function deriveTableColumns(item) {
9
+ if (item.type !== "object" ||
10
+ !item.fields) {
11
+ return [];
12
+ }
13
+ return Object.keys(item.fields).map((key) => ({
14
+ key,
15
+ label: humanise(key),
16
+ }));
17
+ }
18
+ function deriveDetailFields(schema) {
19
+ return Object.entries(getOutputSchemaFields(schema)).map(([key, definition]) => ({
20
+ key,
21
+ label: humanise(key),
22
+ ...(definition.semantic && definition.semantic !== "time-series" && definition.semantic !== "category-series" ? { semantic: definition.semantic } : {}),
23
+ ...(definition.currency ? { currency: definition.currency } : {}),
24
+ }));
25
+ }
26
+ export function deriveOutputView(schema) {
27
+ if (!schema) {
28
+ return {
29
+ surface: "form",
30
+ derivation: { source: "input-schema", reason: "no-output-schema" },
31
+ };
32
+ }
33
+ if (isAggregateOutputSchema(schema)) {
34
+ return {
35
+ surface: "aggregate",
36
+ derivation: { source: "output-schema", reason: "authoritative-aggregate-object" },
37
+ aggregate: {
38
+ metrics: Object.entries(schema.fields).map(([key, definition]) => ({
39
+ key,
40
+ label: humanise(key),
41
+ authoritativeType: definition.type,
42
+ semantic: definition.semantic,
43
+ ...(definition.currency ? { currency: definition.currency } : {}),
44
+ derivation: { source: "authoritative-output-schema", reason: "metric-semantics" },
45
+ })),
46
+ },
47
+ };
48
+ }
49
+ const entries = Object.entries(getOutputSchemaFields(schema));
50
+ const categorySeries = entries.filter(([, definition]) => definition.semantic === "category-series");
51
+ if (categorySeries.length === 1) {
52
+ const [dataPath, collection] = categorySeries[0], itemFields = Object.entries(collection.items.fields), [dimensionKey, dimension] = itemFields.find(([, field]) => field.role === "dimension"), [measureKey, measure] = itemFields.find(([, field]) => field.role === "measure");
53
+ return { surface: "category-series", derivation: { source: "output-schema", reason: "authoritative-category-series" }, categorySeries: { dataPath, visualization: "bar", ordering: "source-order", dimension: { key: dimensionKey, label: humanise(dimensionKey), type: "string", ...(dimension.enum ? { enum: dimension.enum } : {}), role: "dimension" }, measure: { key: measureKey, label: humanise(measureKey), type: measure.type, semantic: measure.semantic, ...(measure.currency ? { currency: measure.currency } : {}), role: "measure" }, derivation: { source: "authoritative-output-schema", reason: "categorical-dimension-with-numeric-measure" } } };
54
+ }
55
+ const timeSeries = entries.filter(([, definition]) => definition.semantic === "time-series");
56
+ if (timeSeries.length === 1) {
57
+ const [dataPath, collection] = timeSeries[0];
58
+ const itemFields = Object.entries(collection.items.fields);
59
+ const [dimensionKey, dimension] = itemFields.find(([, field]) => field.role === "dimension");
60
+ const [measureKey, measure] = itemFields.find(([, field]) => field.role === "measure");
61
+ return { surface: "time-series", derivation: { source: "output-schema", reason: "authoritative-time-series" }, timeSeries: { dataPath, visualization: "line", dimension: { key: dimensionKey, label: humanise(dimensionKey), type: "string", format: dimension.format, role: "dimension" }, measure: { key: measureKey, label: humanise(measureKey), type: measure.type, semantic: measure.semantic, ...(measure.currency ? { currency: measure.currency } : {}), role: "measure" }, derivation: { source: "authoritative-output-schema", reason: "temporal-dimension-with-numeric-measure" } } };
62
+ }
63
+ // A single top-level array of objects naturally maps to a table.
64
+ if (entries.length === 1) {
65
+ const [key, definition] = entries[0];
66
+ if (definition.type === "array" &&
67
+ definition.items?.type === "object") {
68
+ return {
69
+ surface: "table",
70
+ derivation: { source: "output-schema", reason: "single-top-level-array-of-objects" },
71
+ table: {
72
+ dataPath: key,
73
+ columns: deriveTableColumns(definition.items),
74
+ },
75
+ };
76
+ }
77
+ }
78
+ // A flat object naturally maps to a detail surface.
79
+ const isFlatObject = entries.every(([, definition]) => definition.type !== "array" &&
80
+ definition.type !== "object");
81
+ if (isFlatObject) {
82
+ return {
83
+ surface: "detail",
84
+ derivation: { source: "output-schema", reason: "flat-output-object" },
85
+ detail: {
86
+ fields: deriveDetailFields(schema),
87
+ },
88
+ };
89
+ }
90
+ return {
91
+ surface: "detail",
92
+ derivation: { source: "output-schema", reason: "structured-output-object" },
93
+ detail: {
94
+ fields: deriveDetailFields(schema),
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,13 @@
1
+ export type ApplicationInvocationContext = {
2
+ kind: "relationship";
3
+ sourceExecutionToken: string;
4
+ sourceValues: Record<string, unknown>;
5
+ } | {
6
+ kind: "success-transition";
7
+ sourceExecutionToken: string;
8
+ };
9
+ export interface ApplicationExecutionRequest {
10
+ humanInput: Record<string, unknown>;
11
+ context?: ApplicationInvocationContext;
12
+ }
13
+ export declare function parseApplicationExecutionRequest(value: unknown): ApplicationExecutionRequest;
@@ -0,0 +1,58 @@
1
+ import { assertBoundedData } from "./validation-safety.js";
2
+ export function parseApplicationExecutionRequest(value) {
3
+ assertBoundedData(value);
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5
+ throw new Error("Application execution request must be an object");
6
+ }
7
+ const record = value;
8
+ for (const key of Object.keys(record)) {
9
+ if (key !== "humanInput" && key !== "context") {
10
+ throw new Error(`Unexpected application execution property: ${key}`);
11
+ }
12
+ }
13
+ const humanInput = record.humanInput;
14
+ if (!humanInput || typeof humanInput !== "object" || Array.isArray(humanInput)) {
15
+ throw new Error("Application execution humanInput must be an object");
16
+ }
17
+ if (record.context === undefined) {
18
+ return { humanInput: humanInput };
19
+ }
20
+ if (!record.context || typeof record.context !== "object" || Array.isArray(record.context)) {
21
+ throw new Error("Application execution context must be an object");
22
+ }
23
+ const context = record.context;
24
+ if (context.kind !== "relationship" && context.kind !== "success-transition") {
25
+ throw new Error("Unknown application execution context kind");
26
+ }
27
+ const allowed = context.kind === "relationship"
28
+ ? new Set(["kind", "sourceExecutionToken", "sourceValues"])
29
+ : new Set(["kind", "sourceExecutionToken"]);
30
+ for (const key of Object.keys(context)) {
31
+ if (!allowed.has(key)) {
32
+ throw new Error(`Unexpected application execution context property: ${key}`);
33
+ }
34
+ }
35
+ if (typeof context.sourceExecutionToken !== "string" || !context.sourceExecutionToken) {
36
+ throw new Error("Application execution context requires a source token");
37
+ }
38
+ if (context.kind === "success-transition") {
39
+ return {
40
+ humanInput: humanInput,
41
+ context: {
42
+ kind: "success-transition",
43
+ sourceExecutionToken: context.sourceExecutionToken,
44
+ },
45
+ };
46
+ }
47
+ if (!context.sourceValues || typeof context.sourceValues !== "object" || Array.isArray(context.sourceValues)) {
48
+ throw new Error("Relationship execution context requires sourceValues");
49
+ }
50
+ return {
51
+ humanInput: humanInput,
52
+ context: {
53
+ kind: "relationship",
54
+ sourceExecutionToken: context.sourceExecutionToken,
55
+ sourceValues: context.sourceValues,
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,13 @@
1
+ import type { DerivedApplication, DerivedApplicationCapability } from "./application.js";
2
+ export type ApplicationExperienceRole = "entry-primary" | "supporting-related" | "navigation-target" | "contextual-action" | "transition-target" | "developer-only";
3
+ export interface ApplicationExperienceCapability {
4
+ capability: DerivedApplicationCapability;
5
+ roles: ApplicationExperienceRole[];
6
+ }
7
+ export interface ApplicationExperienceAction {
8
+ capability: string;
9
+ label: string;
10
+ bindings: Record<string, string>;
11
+ }
12
+ export declare function deriveApplicationExperience(application: DerivedApplication): ApplicationExperienceCapability[];
13
+ export declare function deriveApplicationActions(application: DerivedApplication): ApplicationExperienceAction[];
@@ -0,0 +1,31 @@
1
+ import { interpretRelationship } from "./relationship-interpretation.js";
2
+ function addRole(roles, capability, role) { roles.get(capability)?.add(role); }
3
+ export function deriveApplicationExperience(application) {
4
+ const roles = new Map(application.capabilities.map((capability) => [capability.name, new Set()]));
5
+ addRole(roles, application.entryCapability, "entry-primary");
6
+ for (const source of application.capabilities) {
7
+ for (const relationship of source.relationships) {
8
+ addRole(roles, relationship.targetCapability, interpretRelationship(relationship, source.name === application.entryCapability ? "application-entry" : "detail") === "related-read"
9
+ ? "supporting-related"
10
+ : interpretRelationship(relationship, source.name === application.entryCapability ? "application-entry" : "detail") === "navigation"
11
+ ? "navigation-target"
12
+ : "contextual-action");
13
+ }
14
+ const transition = source.transition?.onSuccess;
15
+ if (transition)
16
+ addRole(roles, transition.capability, "transition-target");
17
+ }
18
+ return application.capabilities.map((capability) => {
19
+ const capabilityRoles = roles.get(capability.name);
20
+ if (capabilityRoles.size === 0)
21
+ capabilityRoles.add("developer-only");
22
+ return { capability, roles: [...capabilityRoles] };
23
+ });
24
+ }
25
+ function isApplicationAction(relationship) {
26
+ return interpretRelationship(relationship, "application-entry") === "application-action";
27
+ }
28
+ export function deriveApplicationActions(application) {
29
+ const entry = application.capabilities.find((capability) => capability.name === application.entryCapability);
30
+ return (entry?.relationships ?? []).filter(isApplicationAction).map((relationship) => ({ capability: relationship.targetCapability, label: relationship.label, bindings: relationship.bindings }));
31
+ }
@@ -0,0 +1,4 @@
1
+ export declare function formatSemanticValue(value: unknown, field?: {
2
+ semantic?: "currency" | "count" | "ratio";
3
+ currency?: string;
4
+ }): unknown;
@@ -0,0 +1,15 @@
1
+ export function formatSemanticValue(value, field) {
2
+ if (field?.semantic === "currency" && typeof value === "number" && field.currency) {
3
+ return new Intl.NumberFormat(undefined, {
4
+ style: "currency",
5
+ currency: field.currency,
6
+ }).format(value);
7
+ }
8
+ if (field?.semantic === "count" && typeof value === "number") {
9
+ return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value);
10
+ }
11
+ if (field?.semantic === "ratio" && typeof value === "number") {
12
+ return new Intl.NumberFormat(undefined, { style: "percent", maximumFractionDigits: 1 }).format(value);
13
+ }
14
+ return value;
15
+ }
@@ -0,0 +1,18 @@
1
+ export * from "./input-schema.js";
2
+ export * from "./output-schema.js";
3
+ export * from "./application.js";
4
+ export * from "./capability-view.js";
5
+ export * from "./composed-surface.js";
6
+ export * from "./workflow.js";
7
+ export * from "./relationship-interpretation.js";
8
+ export * from "./validate-application.js";
9
+ export * from "./registry.js";
10
+ export * from "./derive-input-fields.js";
11
+ export * from "./derive-output-view.js";
12
+ export * from "./capability-view-derivation.js";
13
+ export * from "./interaction-resolution.js";
14
+ export * from "./experience-orchestration.js";
15
+ export * from "./presentation-context.js";
16
+ export * from "./presentation-composition.js";
17
+ export * from "./execution-request.js";
18
+ export * from "./format-semantic-value.js";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export * from "./input-schema.js";
2
+ export * from "./output-schema.js";
3
+ export * from "./application.js";
4
+ export * from "./capability-view.js";
5
+ export * from "./composed-surface.js";
6
+ export * from "./workflow.js";
7
+ export * from "./relationship-interpretation.js";
8
+ export * from "./validate-application.js";
9
+ export * from "./registry.js";
10
+ export * from "./derive-input-fields.js";
11
+ export * from "./derive-output-view.js";
12
+ export * from "./capability-view-derivation.js";
13
+ export * from "./interaction-resolution.js";
14
+ export * from "./experience-orchestration.js";
15
+ export * from "./presentation-context.js";
16
+ export * from "./presentation-composition.js";
17
+ export * from "./execution-request.js";
18
+ export * from "./format-semantic-value.js";
@@ -0,0 +1,23 @@
1
+ import type { CapabilityInputField } from "@veil-runtime/core";
2
+ export type RichStringFormat = "date" | "date-time" | "multiline";
3
+ export interface RichCapabilityInputField extends CapabilityInputField {
4
+ type: "string" | "number" | "integer" | "boolean" | "object" | "array";
5
+ enum?: Array<string | number | boolean>;
6
+ format?: RichStringFormat;
7
+ semantic?: "currency";
8
+ currency?: string;
9
+ fields?: RichCapabilityInputSchema;
10
+ items?: RichCapabilityInputField;
11
+ minLength?: number;
12
+ maxLength?: number;
13
+ minimum?: number;
14
+ maximum?: number;
15
+ minItems?: number;
16
+ maxItems?: number;
17
+ }
18
+ export type RichCapabilityInputSchema = Record<string, RichCapabilityInputField>;
19
+ export declare function defineInputSchema<T extends RichCapabilityInputSchema>(schema: T): T;
20
+ export declare function validateRichInputField(field: RichCapabilityInputField, path: string): void;
21
+ export declare function validateRichInputSchema(schema: RichCapabilityInputSchema): void;
22
+ export declare function validateRichInputValue(value: unknown, field: RichCapabilityInputField, path: string): void;
23
+ export declare function validateRichCapabilityInput(schema: RichCapabilityInputSchema, input: unknown): asserts input is Record<string, unknown>;
@@ -0,0 +1,131 @@
1
+ import { assertBoundedData, isCalendarDate, isUTCInstant } from "./validation-safety.js";
2
+ export function defineInputSchema(schema) {
3
+ return schema;
4
+ }
5
+ function assertConstraintPair(minimum, maximum, path) {
6
+ if (minimum !== undefined && maximum !== undefined && minimum > maximum) {
7
+ throw new Error(`Invalid input schema at "${path}": minimum exceeds maximum`);
8
+ }
9
+ }
10
+ export function validateRichInputField(field, path) {
11
+ assertBoundedData(field);
12
+ const supported = ["string", "number", "integer", "boolean", "object", "array"];
13
+ if (!supported.includes(field.type)) {
14
+ throw new Error(`Unsupported input type "${field.type}" at "${path}"`);
15
+ }
16
+ if (field.enum) {
17
+ if (field.enum.length === 0)
18
+ throw new Error(`Empty enum at "${path}"`);
19
+ const primitive = field.type === "integer" ? "number" : field.type;
20
+ if (field.enum.some((value) => typeof value !== primitive)) {
21
+ throw new Error(`Enum value is incompatible with "${field.type}" at "${path}"`);
22
+ }
23
+ }
24
+ if (field.format && field.type !== "string") {
25
+ throw new Error(`Format requires a string field at "${path}"`);
26
+ }
27
+ if (field.semantic === "currency") {
28
+ if (!["number", "integer"].includes(field.type) || !field.currency) {
29
+ throw new Error(`Currency semantics require a numeric type and currency code at "${path}"`);
30
+ }
31
+ }
32
+ assertConstraintPair(field.minLength, field.maxLength, path);
33
+ assertConstraintPair(field.minimum, field.maximum, path);
34
+ assertConstraintPair(field.minItems, field.maxItems, path);
35
+ if (field.type === "object") {
36
+ if (!field.fields)
37
+ throw new Error(`Object field lacks nested fields at "${path}"`);
38
+ for (const [name, nested] of Object.entries(field.fields)) {
39
+ validateRichInputField(nested, `${path}.${name}`);
40
+ }
41
+ }
42
+ if (field.type === "array") {
43
+ if (!field.items)
44
+ throw new Error(`Array field lacks an item schema at "${path}"`);
45
+ validateRichInputField(field.items, `${path}[]`);
46
+ }
47
+ }
48
+ export function validateRichInputSchema(schema) {
49
+ assertBoundedData(schema);
50
+ for (const [name, field] of Object.entries(schema)) {
51
+ validateRichInputField(field, name);
52
+ }
53
+ }
54
+ function missing(value) {
55
+ return value === undefined || value === null || value === "";
56
+ }
57
+ export function validateRichInputValue(value, field, path) {
58
+ assertBoundedData(value);
59
+ assertBoundedData(field);
60
+ if (missing(value)) {
61
+ if (field.required)
62
+ throw new Error(`Required input omitted: ${path}`);
63
+ return;
64
+ }
65
+ if (field.enum && !field.enum.includes(value))
66
+ throw new Error(`Input "${path}" is not an allowed enum value`);
67
+ if (field.type === "string") {
68
+ if (typeof value !== "string")
69
+ throw new Error(`Input "${path}" must be a string`);
70
+ if (field.minLength !== undefined && value.length < field.minLength)
71
+ throw new Error(`Input "${path}" is shorter than minLength`);
72
+ if (field.maxLength !== undefined && value.length > field.maxLength)
73
+ throw new Error(`Input "${path}" exceeds maxLength`);
74
+ if (field.format === "date" && !isCalendarDate(value))
75
+ throw new Error(`Input "${path}" must use YYYY-MM-DD`);
76
+ if (field.format === "date-time" && !isUTCInstant(value))
77
+ throw new Error(`Input "${path}" must be an ISO-8601 UTC instant`);
78
+ return;
79
+ }
80
+ if (field.type === "number" || field.type === "integer") {
81
+ if (typeof value !== "number" || !Number.isFinite(value))
82
+ throw new Error(`Input "${path}" must be numeric`);
83
+ if (field.type === "integer" && !Number.isInteger(value))
84
+ throw new Error(`Input "${path}" must be an integer`);
85
+ if (field.minimum !== undefined && value < field.minimum)
86
+ throw new Error(`Input "${path}" is below minimum`);
87
+ if (field.maximum !== undefined && value > field.maximum)
88
+ throw new Error(`Input "${path}" exceeds maximum`);
89
+ return;
90
+ }
91
+ if (field.type === "boolean") {
92
+ if (typeof value !== "boolean")
93
+ throw new Error(`Input "${path}" must be boolean`);
94
+ return;
95
+ }
96
+ if (field.type === "object") {
97
+ if (!value || typeof value !== "object" || Array.isArray(value))
98
+ throw new Error(`Input "${path}" must be an object`);
99
+ const record = value;
100
+ const fields = field.fields ?? {};
101
+ for (const key of Object.keys(record))
102
+ if (!Object.hasOwn(fields, key))
103
+ throw new Error(`Unexpected nested field: ${path}.${key}`);
104
+ for (const [key, child] of Object.entries(fields))
105
+ validateRichInputValue(Object.hasOwn(record, key) ? record[key] : undefined, child, `${path}.${key}`);
106
+ return;
107
+ }
108
+ if (field.type === "array") {
109
+ if (!Array.isArray(value))
110
+ throw new Error(`Input "${path}" must be an array`);
111
+ if (field.minItems !== undefined && value.length < field.minItems)
112
+ throw new Error(`Input "${path}" is below minItems`);
113
+ if (field.maxItems !== undefined && value.length > field.maxItems)
114
+ throw new Error(`Input "${path}" exceeds maxItems`);
115
+ if (!field.items)
116
+ throw new Error(`Input "${path}" lacks an item schema`);
117
+ value.forEach((item, index) => validateRichInputValue(item, field.items, `${path}[${index}]`));
118
+ }
119
+ }
120
+ export function validateRichCapabilityInput(schema, input) {
121
+ assertBoundedData(schema);
122
+ assertBoundedData(input);
123
+ if (!input || typeof input !== "object" || Array.isArray(input))
124
+ throw new Error("Capability input must be an object");
125
+ const record = input;
126
+ for (const key of Object.keys(record))
127
+ if (!Object.hasOwn(schema, key))
128
+ throw new Error(`Unexpected supplied input: ${key}`);
129
+ for (const [key, field] of Object.entries(schema))
130
+ validateRichInputValue(Object.hasOwn(record, key) ? record[key] : undefined, field, key);
131
+ }
@@ -0,0 +1,27 @@
1
+ import type { CapabilityView, UIField } from "./capability-view.js";
2
+ export interface AuthoritativeInputBinding {
3
+ sourceField: string;
4
+ value: unknown;
5
+ }
6
+ export type AuthoritativeInputBindings = Record<string, AuthoritativeInputBinding>;
7
+ export type CapabilityInteractionMode = "form" | "action" | "confirmation" | "read";
8
+ export interface ResolvedCapabilityInput {
9
+ name: string;
10
+ source: "human" | "binding";
11
+ value?: unknown;
12
+ sourceField?: string;
13
+ field: UIField;
14
+ }
15
+ export interface CapabilityInteractionResolution {
16
+ mode: CapabilityInteractionMode;
17
+ suppliedInputs: Record<string, unknown>;
18
+ boundInputNames: string[];
19
+ humanFields: UIField[];
20
+ inputs: ResolvedCapabilityInput[];
21
+ unresolvedRequiredInputs: string[];
22
+ reason: string;
23
+ }
24
+ export declare function serializeDateTimeLocal(value: string): string;
25
+ export declare function validateFieldValue(value: unknown, field: UIField, path: string): void;
26
+ export declare function resolveCapabilityInteraction(view: CapabilityView, bindings?: AuthoritativeInputBindings): CapabilityInteractionResolution;
27
+ export declare function buildCapabilityExecutionPayload(resolution: CapabilityInteractionResolution, humanValues: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,179 @@
1
+ import { assertBoundedData, isCalendarDate, isUTCInstant } from "./validation-safety.js";
2
+ function valueMatchesField(value, field) {
3
+ try {
4
+ validateFieldValue(value, field, field.name);
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export function serializeDateTimeLocal(value) {
12
+ return new Date(value).toISOString();
13
+ }
14
+ export function validateFieldValue(value, field, path) {
15
+ assertBoundedData(value);
16
+ assertBoundedData(field);
17
+ if (value === undefined || value === null || value === "") {
18
+ if (field.required)
19
+ throw new Error(`Required input omitted: ${path}`);
20
+ return;
21
+ }
22
+ if (field.options && !field.options.includes(value)) {
23
+ throw new Error(`Input "${path}" is not an allowed enum value`);
24
+ }
25
+ if (field.authoritativeType === "string") {
26
+ if (typeof value !== "string")
27
+ throw new Error(`Input "${path}" must be a string`);
28
+ if (field.minLength !== undefined && value.length < field.minLength)
29
+ throw new Error(`Input "${path}" is shorter than minLength`);
30
+ if (field.maxLength !== undefined && value.length > field.maxLength)
31
+ throw new Error(`Input "${path}" exceeds maxLength`);
32
+ if (field.format === "date" && !isCalendarDate(value))
33
+ throw new Error(`Input "${path}" must use YYYY-MM-DD`);
34
+ if (field.format === "date-time" && !isUTCInstant(value)) {
35
+ throw new Error(`Input "${path}" must be an ISO-8601 UTC instant`);
36
+ }
37
+ return;
38
+ }
39
+ if (field.authoritativeType === "number" || field.authoritativeType === "integer") {
40
+ if (typeof value !== "number" || !Number.isFinite(value))
41
+ throw new Error(`Input "${path}" must be numeric`);
42
+ if (field.authoritativeType === "integer" && !Number.isInteger(value))
43
+ throw new Error(`Input "${path}" must be an integer`);
44
+ if (field.minimum !== undefined && value < field.minimum)
45
+ throw new Error(`Input "${path}" is below minimum`);
46
+ if (field.maximum !== undefined && value > field.maximum)
47
+ throw new Error(`Input "${path}" exceeds maximum`);
48
+ return;
49
+ }
50
+ if (field.authoritativeType === "boolean") {
51
+ if (typeof value !== "boolean")
52
+ throw new Error(`Input "${path}" must be boolean`);
53
+ return;
54
+ }
55
+ if (field.authoritativeType === "object") {
56
+ if (!value || typeof value !== "object" || Array.isArray(value))
57
+ throw new Error(`Input "${path}" must be an object`);
58
+ const record = value;
59
+ const children = field.fields ?? [];
60
+ const names = new Set(children.map((child) => child.name));
61
+ for (const key of Object.keys(record))
62
+ if (!names.has(key))
63
+ throw new Error(`Unexpected nested field: ${path}.${key}`);
64
+ for (const child of children)
65
+ validateFieldValue(Object.hasOwn(record, child.name) ? record[child.name] : undefined, child, `${path}.${child.name}`);
66
+ return;
67
+ }
68
+ if (field.authoritativeType === "array") {
69
+ if (!Array.isArray(value))
70
+ throw new Error(`Input "${path}" must be an array`);
71
+ if (field.minItems !== undefined && value.length < field.minItems)
72
+ throw new Error(`Input "${path}" is below minItems`);
73
+ if (field.maxItems !== undefined && value.length > field.maxItems)
74
+ throw new Error(`Input "${path}" exceeds maxItems`);
75
+ if (!field.item)
76
+ throw new Error(`Input "${path}" lacks an item schema`);
77
+ value.forEach((item, index) => validateFieldValue(item, field.item, `${path}[${index}]`));
78
+ }
79
+ }
80
+ export function resolveCapabilityInteraction(view, bindings = {}) {
81
+ assertBoundedData(view);
82
+ assertBoundedData(bindings);
83
+ const fieldsByName = new Map(view.fields.map((field) => [field.name, field]));
84
+ const suppliedInputs = {};
85
+ const boundInputNames = Object.keys(bindings);
86
+ const inputs = [];
87
+ for (const [name, binding] of Object.entries(bindings)) {
88
+ const field = fieldsByName.get(name);
89
+ if (!field) {
90
+ throw new Error(`Authoritative binding references unknown input: ${name}`);
91
+ }
92
+ if (binding.value === undefined || binding.value === null || binding.value === "") {
93
+ if (field.required) {
94
+ throw new Error(`Required bound input "${name}" has no runtime value`);
95
+ }
96
+ inputs.push({
97
+ name, source: "binding", value: binding.value,
98
+ sourceField: binding.sourceField, field,
99
+ });
100
+ continue;
101
+ }
102
+ if (!valueMatchesField(binding.value, field)) {
103
+ throw new Error(`Bound input "${name}" does not match declared type "${field.type}"`);
104
+ }
105
+ suppliedInputs[name] = binding.value;
106
+ inputs.push({
107
+ name,
108
+ source: "binding",
109
+ value: binding.value,
110
+ sourceField: binding.sourceField,
111
+ field,
112
+ });
113
+ }
114
+ const boundNames = new Set(boundInputNames);
115
+ const humanFields = view.fields.filter((field) => !boundNames.has(field.name));
116
+ for (const field of humanFields) {
117
+ inputs.push({ name: field.name, source: "human", field });
118
+ }
119
+ const unresolvedRequiredInputs = humanFields
120
+ .filter((field) => field.required)
121
+ .map((field) => field.name);
122
+ const mode = humanFields.length > 0
123
+ ? "form"
124
+ : view.risk === "destructive"
125
+ ? "confirmation"
126
+ : view.risk === "read"
127
+ ? "read"
128
+ : "action";
129
+ if (mode === "confirmation" && !view.confirmation?.required) {
130
+ throw new Error("Destructive zero-input interaction is missing confirmation metadata");
131
+ }
132
+ const reason = humanFields.length > 0
133
+ ? `${humanFields.length} input${humanFields.length === 1 ? "" : "s"} remain for the human`
134
+ : mode === "confirmation"
135
+ ? 'all inputs are supplied and authoritative risk is "destructive"'
136
+ : mode === "read"
137
+ ? "all inputs are supplied and the capability is read-only"
138
+ : "all inputs are supplied and the capability is a write";
139
+ return {
140
+ mode,
141
+ suppliedInputs,
142
+ boundInputNames,
143
+ humanFields,
144
+ inputs,
145
+ unresolvedRequiredInputs,
146
+ reason,
147
+ };
148
+ }
149
+ export function buildCapabilityExecutionPayload(resolution, humanValues) {
150
+ assertBoundedData(resolution);
151
+ assertBoundedData(humanValues);
152
+ const humanNames = new Set(resolution.humanFields.map((field) => field.name));
153
+ const boundNames = new Set(resolution.boundInputNames);
154
+ for (const name of Object.keys(humanValues)) {
155
+ if (boundNames.has(name)) {
156
+ throw new Error(`Human input cannot overwrite authoritative bound input: ${name}`);
157
+ }
158
+ if (!humanNames.has(name)) {
159
+ throw new Error(`Unexpected supplied input: ${name}`);
160
+ }
161
+ }
162
+ const payload = {};
163
+ for (const field of resolution.humanFields) {
164
+ const value = Object.hasOwn(humanValues, field.name) ? humanValues[field.name] : undefined;
165
+ if (field.required && (value === undefined || value === null || value === "")) {
166
+ throw new Error(`Required human input omitted: ${field.name}`);
167
+ }
168
+ if (value !== undefined && value !== "") {
169
+ validateFieldValue(value, field, field.name);
170
+ payload[field.name] = value;
171
+ }
172
+ }
173
+ for (const name of resolution.unresolvedRequiredInputs) {
174
+ if (!Object.prototype.hasOwnProperty.call(payload, name)) {
175
+ throw new Error(`Resolved execution payload is missing required input: ${name}`);
176
+ }
177
+ }
178
+ return payload;
179
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ export declare const generatedOutputFieldSchema: z.ZodObject<{
3
+ key: z.ZodString;
4
+ label: z.ZodString;
5
+ }, z.core.$strict>;
6
+ export declare const generatedOutputPresentationSchema: z.ZodObject<{
7
+ fields: z.ZodArray<z.ZodObject<{
8
+ key: z.ZodString;
9
+ label: z.ZodString;
10
+ }, z.core.$strict>>;
11
+ }, z.core.$strict>;
12
+ export type GeneratedOutputPresentation = z.infer<typeof generatedOutputPresentationSchema>;
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const generatedOutputFieldSchema = z.object({
3
+ key: z.string().min(1).max(160),
4
+ label: z.string().min(1).max(160),
5
+ }).strict();
6
+ export const generatedOutputPresentationSchema = z.object({
7
+ fields: z.array(generatedOutputFieldSchema).max(256),
8
+ }).strict();