@texuli/spec 0.2.0 → 0.4.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 (46) hide show
  1. package/README.md +80 -14
  2. package/dist/build.d.ts +5 -0
  3. package/dist/build.js +1 -0
  4. package/dist/canonical.js +20 -13
  5. package/dist/catalog.d.ts +9 -3
  6. package/dist/catalog.js +7 -1
  7. package/dist/codegen/index.d.ts +2 -2
  8. package/dist/codegen/index.js +2 -2
  9. package/dist/codegen/kotlin.d.ts +6 -0
  10. package/dist/codegen/kotlin.js +80 -8
  11. package/dist/conformance.d.ts +11 -2
  12. package/dist/conformance.js +93 -2
  13. package/dist/contract.d.ts +112 -0
  14. package/dist/contract.js +196 -0
  15. package/dist/dialect.d.ts +10 -1
  16. package/dist/dialect.js +6 -1
  17. package/dist/generated/typescript/blueprint.d.ts +103 -5
  18. package/dist/generated/typescript/blueprint.js +1 -1
  19. package/dist/generated/typescript/catalog-schema.d.ts +35 -0
  20. package/dist/generated/typescript/catalog-schema.js +1 -1
  21. package/dist/generated/typescript/diagnostics.d.ts +10 -1
  22. package/dist/generated/typescript/diagnostics.js +1 -1
  23. package/dist/generated/typescript/manifest.js +1 -1
  24. package/dist/generated/typescript/vocabulary.d.ts +152 -4
  25. package/dist/generated/typescript/vocabulary.js +27 -2
  26. package/dist/generated/typescript/workflow.d.ts +84 -0
  27. package/dist/generated/typescript/workflow.js +3 -0
  28. package/dist/index.d.ts +13 -5
  29. package/dist/index.js +9 -3
  30. package/dist/migrations/0.2.0-0.3.0.d.ts +7 -0
  31. package/dist/migrations/0.2.0-0.3.0.js +10 -0
  32. package/dist/migrations/0.3.0-0.4.0.d.ts +9 -0
  33. package/dist/migrations/0.3.0-0.4.0.js +12 -0
  34. package/dist/migrations/index.d.ts +15 -0
  35. package/dist/migrations/index.js +53 -0
  36. package/dist/migrations/types.d.ts +14 -0
  37. package/dist/migrations/types.js +1 -0
  38. package/dist/surface.d.ts +60 -0
  39. package/dist/surface.js +162 -0
  40. package/dist/validate.d.ts +89 -14
  41. package/dist/validate.js +725 -64
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/dist/workflow.d.ts +15 -0
  45. package/dist/workflow.js +256 -0
  46. package/package.json +13 -1
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The state surface's node rules (spec 0.3.0): where the state-only nodes
3
+ * may sit and what they must name, exactly as `@texuli/player` refuses them
4
+ * at paint time, so a blueprint that validates always paints.
5
+ *
6
+ * - Forms (code `action`): `form-field` and `form-submit` sit inside an
7
+ * `action-form`; forms never nest; a form names an intent-like action; a
8
+ * field has an intent-like name, unique in its form, and writes a declared
9
+ * variable of its type (`number` fields a number, the others a string).
10
+ * - Controls (code `binding`): each control names a declared variable of
11
+ * the type it writes.
12
+ * - Host-bound variables (code `binding`): a variable with a `source` is the
13
+ * host's; no control or field writes it.
14
+ * - Flow nodes (code `state`): `flow-view` names a declared view and
15
+ * `flow-action` an event some transition handles; neither appears
16
+ * without a flow.
17
+ *
18
+ * Inputs are read from the raw envelope (`surfaceInputs`) so every
19
+ * validator applies the same rules to the same data. Framework-free; never
20
+ * evaluates anything.
21
+ */
22
+ /** The spec's `intentName` pattern (pinned to `SPEC_PATTERNS` by the spec tests); no imports keep this module lean. */
23
+ export declare const SURFACE_INTENT_NAME: RegExp;
24
+ /** The spec's `listText` pattern (the contract's `SPEC_PATTERNS.listText` is this string). */
25
+ export declare const LIST_TEXT_PATTERN = "^[^\\u0000-\\u001f\\u007f-\\u009f\\u061c\\u180e\\u200b\\u200e\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u2069\\ufeff]*$";
26
+ /** The state type each control writes (mirrors the catalog's `CONTROL_STATE_TYPES`). */
27
+ export declare const SPEC_CONTROL_STATE_TYPES: Readonly<Record<string, string>>;
28
+ /** The player's issue code each spec code stands for (`action-binding` → `action`, …). */
29
+ export declare const SURFACE_PLAYER_CODES: Readonly<{
30
+ readonly 'action-binding': "action";
31
+ readonly 'control-binding': "binding";
32
+ readonly 'host-binding': "binding";
33
+ readonly 'flow-binding': "state";
34
+ }>;
35
+ export type SurfaceCode = (typeof SURFACE_PLAYER_CODES)[keyof typeof SURFACE_PLAYER_CODES];
36
+ export interface SurfaceIssue {
37
+ code: SurfaceCode;
38
+ /** Child indices from the root: `[]` is the root, `[0, 2]` its first child's third child. */
39
+ at: readonly number[];
40
+ message: string;
41
+ }
42
+ export interface SurfaceInputs {
43
+ /** Declared variables (type, and whether a node may write it); `null` without `state` or `flow`. */
44
+ declarations: ReadonlyMap<string, {
45
+ type: string;
46
+ writable: boolean;
47
+ }> | null;
48
+ /** The flow's views and handled events; `null` without a flow. */
49
+ flow: {
50
+ views: readonly string[];
51
+ events: readonly string[];
52
+ } | null;
53
+ }
54
+ /**
55
+ * Read the surface inputs from a raw envelope, as the player does: an
56
+ * envelope with `state` or `flow` has declarations (empty with only a flow).
57
+ */
58
+ export declare function surfaceInputs(envelope: unknown): SurfaceInputs;
59
+ /** Check one tree root against the surface rules; one issue per refused node and rule. */
60
+ export declare function checkStateSurface(root: unknown, inputs: SurfaceInputs): SurfaceIssue[];
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The state surface's node rules (spec 0.3.0): where the state-only nodes
3
+ * may sit and what they must name, exactly as `@texuli/player` refuses them
4
+ * at paint time, so a blueprint that validates always paints.
5
+ *
6
+ * - Forms (code `action`): `form-field` and `form-submit` sit inside an
7
+ * `action-form`; forms never nest; a form names an intent-like action; a
8
+ * field has an intent-like name, unique in its form, and writes a declared
9
+ * variable of its type (`number` fields a number, the others a string).
10
+ * - Controls (code `binding`): each control names a declared variable of
11
+ * the type it writes.
12
+ * - Host-bound variables (code `binding`): a variable with a `source` is the
13
+ * host's; no control or field writes it.
14
+ * - Flow nodes (code `state`): `flow-view` names a declared view and
15
+ * `flow-action` an event some transition handles; neither appears
16
+ * without a flow.
17
+ *
18
+ * Inputs are read from the raw envelope (`surfaceInputs`) so every
19
+ * validator applies the same rules to the same data. Framework-free; never
20
+ * evaluates anything.
21
+ */
22
+ /** The spec's `intentName` pattern (pinned to `SPEC_PATTERNS` by the spec tests); no imports keep this module lean. */
23
+ export const SURFACE_INTENT_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u;
24
+ /** The spec's `listText` pattern (the contract's `SPEC_PATTERNS.listText` is this string). */
25
+ export const LIST_TEXT_PATTERN = '^[^\\u0000-\\u001f\\u007f-\\u009f\\u061c\\u180e\\u200b\\u200e\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u2069\\ufeff]*$';
26
+ /** The state type each control writes (mirrors the catalog's `CONTROL_STATE_TYPES`). */
27
+ export const SPEC_CONTROL_STATE_TYPES =
28
+ /* @__PURE__ */ Object.freeze({
29
+ 'control-toggle': 'boolean',
30
+ 'control-choice': 'enum',
31
+ 'control-slider': 'number',
32
+ 'control-filter': 'string',
33
+ });
34
+ /** The player's issue code each spec code stands for (`action-binding` → `action`, …). */
35
+ export const SURFACE_PLAYER_CODES = /* @__PURE__ */ Object.freeze({
36
+ 'action-binding': 'action',
37
+ 'control-binding': 'binding',
38
+ 'host-binding': 'binding',
39
+ 'flow-binding': 'state',
40
+ });
41
+ const isBag = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
43
+ /**
44
+ * Read the surface inputs from a raw envelope, as the player does: an
45
+ * envelope with `state` or `flow` has declarations (empty with only a flow).
46
+ */
47
+ export function surfaceInputs(envelope) {
48
+ if (!isBag(envelope))
49
+ return { declarations: null, flow: null };
50
+ const hasState = Object.hasOwn(envelope, 'state');
51
+ const hasFlow = Object.hasOwn(envelope, 'flow');
52
+ let declarations = null;
53
+ if (hasState || hasFlow) {
54
+ declarations = new Map();
55
+ const state = isBag(envelope.state) ? envelope.state : {};
56
+ for (const [name, raw] of Object.entries(state)) {
57
+ if (isBag(raw) && typeof raw.type === 'string') {
58
+ declarations.set(name, { type: raw.type, writable: !Object.hasOwn(raw, 'source') });
59
+ }
60
+ }
61
+ }
62
+ const raw = envelope.flow;
63
+ const flow = isBag(raw)
64
+ ? {
65
+ views: strings(raw.views),
66
+ events: Array.isArray(raw.transitions)
67
+ ? raw.transitions.flatMap((t) => (isBag(t) && typeof t.on === 'string' ? [t.on] : []))
68
+ : [],
69
+ }
70
+ : null;
71
+ return { declarations, flow };
72
+ }
73
+ /** Check one tree root against the surface rules; one issue per refused node and rule. */
74
+ export function checkStateSurface(root, inputs) {
75
+ const issues = [];
76
+ const { declarations, flow } = inputs;
77
+ const events = new Set(flow?.events ?? []);
78
+ function visit(node, at, form) {
79
+ if (!isBag(node) || at.length > 64)
80
+ return;
81
+ const fail = (code, message) => issues.push({ code, at, message });
82
+ const id = typeof node.id === 'string' ? node.id : '';
83
+ const props = isBag(node.props) ? node.props : {};
84
+ let scope = form;
85
+ // Controls.
86
+ const wanted = Object.hasOwn(SPEC_CONTROL_STATE_TYPES, id)
87
+ ? SPEC_CONTROL_STATE_TYPES[id]
88
+ : undefined;
89
+ if (wanted !== undefined) {
90
+ const name = props.state;
91
+ const declaration = typeof name === 'string' ? declarations?.get(name) : undefined;
92
+ if (!declarations) {
93
+ fail('binding', `"${id}" needs declared local state; this blueprint declares none.`);
94
+ }
95
+ else if (typeof name !== 'string' || !declaration) {
96
+ fail('binding', `"${id}" names no declared state variable.`);
97
+ }
98
+ else if (declaration.type !== wanted) {
99
+ fail('binding', `"${id}" writes a ${wanted}, but "${name}" is a ${declaration.type}.`);
100
+ }
101
+ }
102
+ // Flow nodes.
103
+ if (id === 'flow-view' || id === 'flow-action') {
104
+ if (!flow) {
105
+ fail('state', `"${id}" needs a declared flow; this blueprint declares none.`);
106
+ }
107
+ else if (id === 'flow-view' &&
108
+ (typeof props.name !== 'string' || !flow.views.includes(props.name))) {
109
+ fail('state', '"flow-view" names no declared view.');
110
+ }
111
+ else if (id === 'flow-action' &&
112
+ (typeof props.event !== 'string' || !events.has(props.event))) {
113
+ fail('state', '"flow-action" fires an event no transition handles.');
114
+ }
115
+ }
116
+ // Forms.
117
+ if (id === 'action-form') {
118
+ if (form)
119
+ fail('action', '"action-form" cannot nest inside another form.');
120
+ if (typeof props.action !== 'string' || !SURFACE_INTENT_NAME.test(props.action)) {
121
+ fail('action', '"action-form" needs an action name like "request-access".');
122
+ }
123
+ scope = new Set();
124
+ }
125
+ else if (id === 'form-field' || id === 'form-submit') {
126
+ if (!form)
127
+ fail('action', `"${id}" must be inside an "action-form".`);
128
+ else if (id === 'form-field') {
129
+ const { name, state, kind } = props;
130
+ const declaration = typeof state === 'string' ? declarations?.get(state) : undefined;
131
+ const type = kind === 'number' ? 'number' : 'string';
132
+ if (typeof name !== 'string' || !SURFACE_INTENT_NAME.test(name))
133
+ fail('action', '"form-field" needs a field name.');
134
+ else if (form.has(name))
135
+ fail('action', `Field name "${name}" is used twice in one form.`);
136
+ else
137
+ form.add(name);
138
+ if (!declarations)
139
+ fail('action', '"form-field" needs declared local state; this blueprint declares none.');
140
+ else if (!declaration)
141
+ fail('action', '"form-field" names no declared state variable.');
142
+ else if (declaration.type !== type) {
143
+ fail('action', `A ${String(kind ?? 'text')} field writes a ${type}, but "${String(state)}" is a ${declaration.type}.`);
144
+ }
145
+ }
146
+ }
147
+ // Host-bound variables are the host's to write.
148
+ const writes = wanted !== undefined || id === 'form-field';
149
+ if (writes && typeof props.state === 'string') {
150
+ const declaration = declarations?.get(props.state);
151
+ if (declaration && !declaration.writable) {
152
+ fail('binding', `"${id}" cannot write "${props.state}": its value comes from the host.`);
153
+ }
154
+ }
155
+ if (Array.isArray(node.children)) {
156
+ for (const [index, child] of node.children.entries())
157
+ visit(child, [...at, index], scope);
158
+ }
159
+ }
160
+ visit(root, [], null);
161
+ return issues;
162
+ }
@@ -1,27 +1,102 @@
1
- import type { SpecDiagnostic, SpecDiagnosticCode, SpecValidation } from './generated/typescript/diagnostics.js';
1
+ import type { SpecDiagnostic, SpecDiagnosticCode, SpecValidation, SpecWarning } from './generated/typescript/diagnostics.js';
2
2
  import type { SpecCatalog } from './catalog.js';
3
+ import { type SpecStateType } from './contract.js';
3
4
  import { type Schema } from './dialect.js';
4
- export type { SpecDiagnostic, SpecDiagnosticCode, SpecValidation };
5
+ export type { SpecDiagnostic, SpecDiagnosticCode, SpecValidation, SpecWarning };
6
+ /**
7
+ * Checks one workflow graph against the workflow schema:
8
+ * `validateWorkflowGraph` from `@texuli/spec/workflow`. The root never
9
+ * imports it, so blueprints without workflows do not pay for it.
10
+ */
11
+ export type WorkflowGraphValidator = (graph: unknown, workflowSchema: Schema) => SpecValidation;
5
12
  export interface SpecDocuments {
6
13
  blueprintSchema: Schema;
7
14
  catalog: SpecCatalog;
15
+ /**
16
+ * `schemas/workflow.schema.json`. Workflow state and `workflowGraph` props
17
+ * need it and {@link SpecDocuments.validateWorkflowGraph}; without both
18
+ * they fail closed (`state`, `prop-type`).
19
+ */
20
+ workflowSchema?: Schema;
21
+ /** `validateWorkflowGraph` from `@texuli/spec/workflow`, for hosts that render workflows. */
22
+ validateWorkflowGraph?: WorkflowGraphValidator;
23
+ }
24
+ /** The workflow schema and its validator, when the host supplied both. */
25
+ export interface WorkflowSupport {
26
+ schema: Schema;
27
+ validate: WorkflowGraphValidator;
28
+ }
29
+ /** One declared variable with its defaults applied; `valid: false` when its declaration was refused. */
30
+ export interface SpecStateVariable {
31
+ type: SpecStateType;
32
+ valid: boolean;
33
+ /** The first-paint value (the declared `initial` or the type's default). */
34
+ initial: unknown;
35
+ /** No `source`: the blueprint may write it. */
36
+ writable: boolean;
37
+ values?: readonly string[];
38
+ min?: number;
39
+ max?: number;
40
+ maxLength?: number;
41
+ maxItems?: number;
42
+ }
43
+ /** The declared flow, as the validator reads it. */
44
+ export interface SpecFlowInfo {
45
+ views: readonly string[];
46
+ events: ReadonlySet<string>;
47
+ }
48
+ export interface TreeOptions {
49
+ portable?: boolean;
50
+ path?: string;
51
+ /** Declared variables by name (from {@link checkStateDeclarations}). Without it, every `$state` is unknown. */
52
+ state?: ReadonlyMap<string, SpecStateVariable>;
53
+ /** The declared flow, or `null`/omitted when none. */
54
+ flow?: SpecFlowInfo | null;
55
+ /** The root property schemas resolve against ({@link contractRoot}). */
56
+ root?: Schema;
57
+ /** Receives advisory findings (`deprecated`). */
58
+ warnings?: SpecWarning[];
59
+ /** Checks `workflowGraph` positions; without it they fail closed. */
60
+ workflow?: WorkflowSupport;
8
61
  }
9
62
  /**
10
63
  * Validate a blueprint envelope against the spec alone: the envelope schema,
11
- * then every node against the catalog vocabulary (closed ids, closed prop
12
- * names, prop kinds and enum values, required props, no platform-extension
13
- * props from data, a `slot` that names a slot prop of the parent) and the
14
- * declared profile. A `when` condition is checked for shape only; the
15
- * runtime formula parser checks its operands. With `catalogHash`, an envelope
16
- * bound to another catalog is refused. This is the shared, target-neutral
17
- * contract; URL policy, limits, grammar and rendering stay in the runtime
18
- * validators (`@texuli/catalog/validate`).
64
+ * the declared state and flow, then every node against the catalog
65
+ * vocabulary (closed ids, closed prop names, prop kinds and enum values,
66
+ * property schemas, required props, no platform-extension props from data,
67
+ * a `slot` that names a slot prop of the parent, bindings, two-way `bind`,
68
+ * `on` events and their closed actions) and the declared profile. A `when`
69
+ * condition is checked for shape only; the runtime formula parser checks
70
+ * its operands. With `catalogHash`, an envelope bound to another catalog is
71
+ * refused. Deprecated components and props add `warnings` (never failing).
72
+ * This is the shared, target-neutral contract; URL policy, limits, grammar
73
+ * and rendering stay in the runtime validators (`@texuli/catalog/validate`).
19
74
  */
20
75
  export declare function validateBlueprint(input: unknown, documents: SpecDocuments, options?: {
21
76
  catalogHash?: string;
22
77
  }): SpecValidation;
78
+ /**
79
+ * Read the envelope's `state` (code `state`): at most 64 variables, safe
80
+ * names, the fields each type takes, and an initial value of the declared
81
+ * type. One diagnostic per refused declaration; a refused declaration stays
82
+ * declared (so bindings to it are not also `unknown-state`). Shape problems
83
+ * the schema reports are not repeated.
84
+ */
85
+ export declare function checkStateDeclarations(input: unknown, out: SpecDiagnostic[], workflow?: WorkflowSupport): Map<string, SpecStateVariable>;
86
+ /**
87
+ * A list value: at most `maxItems` items, each `{ id, ...fields }` with a
88
+ * unique item key and at most 16 scalar fields (string ≤ 1000, finite
89
+ * number, boolean) named like state variables. Strings follow the
90
+ * `listText` rule: a draggable item's text never hides or reorders itself.
91
+ */
92
+ export declare function listProblem(value: unknown, maxItems: number): {
93
+ at: string;
94
+ message: string;
95
+ } | null;
96
+ /**
97
+ * Read the envelope's `flow` (code `state`): distinct views, an initial and
98
+ * transitions that name declared views (or `*` as a source).
99
+ */
100
+ export declare function checkFlow(input: unknown, out: SpecDiagnostic[]): SpecFlowInfo | null;
23
101
  /** Check one catalog tree against the vocabulary. See {@link validateBlueprint}. */
24
- export declare function validateTree(tree: unknown, catalog: SpecCatalog, options?: {
25
- portable?: boolean;
26
- path?: string;
27
- }): SpecDiagnostic[];
102
+ export declare function validateTree(tree: unknown, catalog: SpecCatalog, options?: TreeOptions): SpecDiagnostic[];