@texuli/spec 0.3.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.
- package/README.md +80 -14
- package/dist/build.d.ts +5 -0
- package/dist/build.js +1 -0
- package/dist/canonical.js +20 -13
- package/dist/catalog.d.ts +9 -3
- package/dist/catalog.js +7 -1
- package/dist/codegen/index.d.ts +2 -2
- package/dist/codegen/index.js +2 -2
- package/dist/codegen/kotlin.d.ts +6 -0
- package/dist/codegen/kotlin.js +80 -8
- package/dist/conformance.d.ts +11 -2
- package/dist/conformance.js +93 -2
- package/dist/contract.d.ts +112 -0
- package/dist/contract.js +196 -0
- package/dist/dialect.d.ts +10 -1
- package/dist/dialect.js +6 -1
- package/dist/generated/typescript/blueprint.d.ts +103 -5
- package/dist/generated/typescript/blueprint.js +1 -1
- package/dist/generated/typescript/catalog-schema.d.ts +35 -0
- package/dist/generated/typescript/catalog-schema.js +1 -1
- package/dist/generated/typescript/diagnostics.d.ts +10 -1
- package/dist/generated/typescript/diagnostics.js +1 -1
- package/dist/generated/typescript/manifest.js +1 -1
- package/dist/generated/typescript/vocabulary.d.ts +152 -4
- package/dist/generated/typescript/vocabulary.js +27 -2
- package/dist/generated/typescript/workflow.d.ts +84 -0
- package/dist/generated/typescript/workflow.js +3 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.js +9 -3
- package/dist/migrations/0.2.0-0.3.0.d.ts +7 -0
- package/dist/migrations/0.2.0-0.3.0.js +10 -0
- package/dist/migrations/0.3.0-0.4.0.d.ts +9 -0
- package/dist/migrations/0.3.0-0.4.0.js +12 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +53 -0
- package/dist/migrations/types.d.ts +14 -0
- package/dist/migrations/types.js +1 -0
- package/dist/surface.d.ts +60 -0
- package/dist/surface.js +162 -0
- package/dist/validate.d.ts +89 -14
- package/dist/validate.js +725 -64
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow.d.ts +15 -0
- package/dist/workflow.js +256 -0
- package/package.json +13 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 0.3.0 interaction contract in code (ADR-008): the closed action set,
|
|
3
|
+
* the closed event vocabulary, the state types, the limits and the name
|
|
4
|
+
* patterns, mirrored from the schemas (`tests/spec.test.ts` asserts they
|
|
5
|
+
* stay equal), plus the helpers that turn a catalog property schema into a
|
|
6
|
+
* dialect schema and build the `$ref` root property schemas resolve against.
|
|
7
|
+
*/
|
|
8
|
+
import type { SpecPropSchema } from './generated/typescript/catalog-schema.js';
|
|
9
|
+
import { type Schema } from './dialect.js';
|
|
10
|
+
/** The binding key that reads local state: `{ "$state": name }`. */
|
|
11
|
+
export declare const STATE_BINDING_KEY = "$state";
|
|
12
|
+
/** The binding key that reads host data (read-only): `{ "$data": name }`. */
|
|
13
|
+
export declare const DATA_BINDING_KEY = "$data";
|
|
14
|
+
/** Every state variable type (`$defs.stateDeclaration.type`). */
|
|
15
|
+
export declare const SPEC_STATE_TYPES: readonly ["boolean", "number", "enum", "string", "list", "workflow"];
|
|
16
|
+
export type SpecStateType = (typeof SPEC_STATE_TYPES)[number];
|
|
17
|
+
/** The closed action set (`$defs.action.op`). Never open to code. */
|
|
18
|
+
export declare const SPEC_ACTION_OPS: readonly ["setState", "toggleState", "moveItem", "flowGo", "emit", "submit"];
|
|
19
|
+
export type SpecActionOp = (typeof SPEC_ACTION_OPS)[number];
|
|
20
|
+
/** The closed event vocabulary (catalog `$defs.eventName`). */
|
|
21
|
+
export declare const SPEC_EVENT_NAMES: readonly ["press", "change", "select", "submit", "dismiss", "move"];
|
|
22
|
+
export type SpecEventName = (typeof SPEC_EVENT_NAMES)[number];
|
|
23
|
+
/** The `$ref` targets a catalog property schema may name. */
|
|
24
|
+
export declare const SPEC_PROP_REFS: readonly ["binding", "reorder", "action", "eventMap", "condition", "workflowGraph"];
|
|
25
|
+
/** The fields each action op takes (besides `op`) and the ones it needs. */
|
|
26
|
+
export declare const ACTION_FIELDS: Readonly<Record<SpecActionOp, {
|
|
27
|
+
readonly allowed: readonly string[];
|
|
28
|
+
readonly required: readonly string[];
|
|
29
|
+
}>>;
|
|
30
|
+
/** The fields each state type takes (besides `type`). */
|
|
31
|
+
export declare const STATE_FIELDS: Readonly<Record<SpecStateType, readonly string[]>>;
|
|
32
|
+
/** Every bound of the contract. Strings are counted in UTF-16 units, like the player. */
|
|
33
|
+
export declare const SPEC_LIMITS: Readonly<{
|
|
34
|
+
stateVars: 64;
|
|
35
|
+
enumValues: 32;
|
|
36
|
+
enumValueLength: 64;
|
|
37
|
+
stringLength: 1000;
|
|
38
|
+
defaultStringLength: 200;
|
|
39
|
+
flowViews: 16;
|
|
40
|
+
flowTransitions: 64;
|
|
41
|
+
actionsPerEvent: 8;
|
|
42
|
+
emitValues: 32;
|
|
43
|
+
emitValueLength: 2048;
|
|
44
|
+
listItems: 500;
|
|
45
|
+
/** Fields one list item may carry besides its `id`. */
|
|
46
|
+
listItemFields: 16;
|
|
47
|
+
workflowNodes: 500;
|
|
48
|
+
workflowEdges: 2000;
|
|
49
|
+
workflowNodeTypes: 64;
|
|
50
|
+
workflowGroups: 64;
|
|
51
|
+
workflowPorts: 16;
|
|
52
|
+
workflowFields: 32;
|
|
53
|
+
workflowLabelLength: 200;
|
|
54
|
+
workflowStringLength: 2000;
|
|
55
|
+
/** Deepest prop value walked for nested bindings. */
|
|
56
|
+
propDepth: 32;
|
|
57
|
+
}>;
|
|
58
|
+
/** The name patterns, as the schemas write them. */
|
|
59
|
+
export declare const SPEC_PATTERNS: Readonly<{
|
|
60
|
+
stateName: "^[a-z][A-Za-z0-9_]{0,31}$";
|
|
61
|
+
dataName: "^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z0-9_]+)*$";
|
|
62
|
+
flowName: "^[a-z][a-z0-9-]{0,31}$";
|
|
63
|
+
intentName: "^[A-Za-z][A-Za-z0-9_-]{0,63}$";
|
|
64
|
+
itemKey: "^[A-Za-z0-9._:-]{1,64}$";
|
|
65
|
+
groupName: "^[a-z][a-z0-9-]{0,31}$";
|
|
66
|
+
workflowId: "^[A-Za-z0-9._:-]{1,64}$";
|
|
67
|
+
workflowFieldName: "^[a-z][A-Za-z0-9_]{0,31}$";
|
|
68
|
+
/** Multi-line display text (workflow string field values): tab and newline allowed. */
|
|
69
|
+
workflowText: "^[^\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]*$";
|
|
70
|
+
workflowLabel: "^[^\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]*$";
|
|
71
|
+
/**
|
|
72
|
+
* List item text (list state fields): the workflowLabel rule (no C0 or C1
|
|
73
|
+
* controls, bidirectional overrides or isolates) plus the invisible
|
|
74
|
+
* formatting characters that hide or reorder text: the Arabic letter mark,
|
|
75
|
+
* the Mongolian vowel separator, zero-width space, the left-to-right and
|
|
76
|
+
* right-to-left marks, word joiner and invisible operators, and the byte
|
|
77
|
+
* order mark. ZWNJ and ZWJ (U+200C, U+200D) stay allowed: scripts such as
|
|
78
|
+
* Persian and Devanagari, and emoji sequences, need them.
|
|
79
|
+
*/
|
|
80
|
+
listText: "^[^\\u0000-\\u001f\\u007f-\\u009f\\u061c\\u180e\\u200b\\u200e\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u2069\\ufeff]*$";
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* The patterns the blueprint validator tests, compiled once; `u` so classes
|
|
84
|
+
* count code points. The workflow validator compiles its own.
|
|
85
|
+
*/
|
|
86
|
+
export declare const SPEC_REGEX: Readonly<{
|
|
87
|
+
stateName: RegExp;
|
|
88
|
+
dataName: RegExp;
|
|
89
|
+
intentName: RegExp;
|
|
90
|
+
itemKey: RegExp;
|
|
91
|
+
listText: RegExp;
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* A catalog property schema as a dialect schema: the same keywords, with
|
|
95
|
+
* every object closed unless `additionalProperties` gives a map.
|
|
96
|
+
*/
|
|
97
|
+
export declare function toDialectSchema(schema: SpecPropSchema): Schema;
|
|
98
|
+
/**
|
|
99
|
+
* Problems with one catalog property schema: it must stay in the dialect,
|
|
100
|
+
* name only {@link SPEC_PROP_REFS} in `$ref`, and give a `type` wherever it
|
|
101
|
+
* has no `$ref`. Empty means usable.
|
|
102
|
+
*/
|
|
103
|
+
export declare function checkPropSchema(schema: SpecPropSchema): string[];
|
|
104
|
+
/** Def names both documents declare; the contract root needs them disjoint. */
|
|
105
|
+
export declare function defCollisions(blueprintSchema: Schema, workflowSchema?: Schema): string[];
|
|
106
|
+
/**
|
|
107
|
+
* The root property schemas resolve `$ref`s against: the blueprint `$defs`,
|
|
108
|
+
* the workflow `$defs` and a synthetic `workflowGraph` (the workflow root).
|
|
109
|
+
* Without a workflow schema there is no `workflowGraph`, so a property that
|
|
110
|
+
* names it fails closed.
|
|
111
|
+
*/
|
|
112
|
+
export declare function contractRoot(blueprintSchema: Schema, workflowSchema?: Schema): Schema;
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { LIST_TEXT_PATTERN } from "./surface.js";
|
|
2
|
+
import { checkSchemaDocument, DIALECT_URI, isRecord } from "./dialect.js";
|
|
3
|
+
/** The binding key that reads local state: `{ "$state": name }`. */
|
|
4
|
+
export const STATE_BINDING_KEY = '$state';
|
|
5
|
+
/** The binding key that reads host data (read-only): `{ "$data": name }`. */
|
|
6
|
+
export const DATA_BINDING_KEY = '$data';
|
|
7
|
+
/** Every state variable type (`$defs.stateDeclaration.type`). */
|
|
8
|
+
export const SPEC_STATE_TYPES = [
|
|
9
|
+
'boolean',
|
|
10
|
+
'number',
|
|
11
|
+
'enum',
|
|
12
|
+
'string',
|
|
13
|
+
'list',
|
|
14
|
+
'workflow',
|
|
15
|
+
];
|
|
16
|
+
/** The closed action set (`$defs.action.op`). Never open to code. */
|
|
17
|
+
export const SPEC_ACTION_OPS = [
|
|
18
|
+
'setState',
|
|
19
|
+
'toggleState',
|
|
20
|
+
'moveItem',
|
|
21
|
+
'flowGo',
|
|
22
|
+
'emit',
|
|
23
|
+
'submit',
|
|
24
|
+
];
|
|
25
|
+
/** The closed event vocabulary (catalog `$defs.eventName`). */
|
|
26
|
+
export const SPEC_EVENT_NAMES = ['press', 'change', 'select', 'submit', 'dismiss', 'move'];
|
|
27
|
+
/** The `$ref` targets a catalog property schema may name. */
|
|
28
|
+
export const SPEC_PROP_REFS = [
|
|
29
|
+
'binding',
|
|
30
|
+
'reorder',
|
|
31
|
+
'action',
|
|
32
|
+
'eventMap',
|
|
33
|
+
'condition',
|
|
34
|
+
'workflowGraph',
|
|
35
|
+
];
|
|
36
|
+
/** The fields each action op takes (besides `op`) and the ones it needs. */
|
|
37
|
+
export const ACTION_FIELDS = Object.freeze({
|
|
38
|
+
setState: { allowed: ['state', 'value', 'fromEvent'], required: ['state'] },
|
|
39
|
+
toggleState: { allowed: ['state'], required: ['state'] },
|
|
40
|
+
moveItem: { allowed: ['from', 'item', 'to', 'index'], required: ['from', 'item'] },
|
|
41
|
+
flowGo: { allowed: ['event'], required: ['event'] },
|
|
42
|
+
emit: { allowed: ['intent', 'values'], required: ['intent'] },
|
|
43
|
+
submit: { allowed: [], required: [] },
|
|
44
|
+
});
|
|
45
|
+
/** The fields each state type takes (besides `type`). */
|
|
46
|
+
export const STATE_FIELDS = Object.freeze({
|
|
47
|
+
boolean: ['initial', 'source'],
|
|
48
|
+
number: ['min', 'max', 'initial', 'source'],
|
|
49
|
+
enum: ['values', 'initial', 'source'],
|
|
50
|
+
string: ['maxLength', 'initial', 'source'],
|
|
51
|
+
list: ['maxItems', 'initial'],
|
|
52
|
+
workflow: ['initial'],
|
|
53
|
+
});
|
|
54
|
+
/** Every bound of the contract. Strings are counted in UTF-16 units, like the player. */
|
|
55
|
+
export const SPEC_LIMITS = Object.freeze({
|
|
56
|
+
stateVars: 64,
|
|
57
|
+
enumValues: 32,
|
|
58
|
+
enumValueLength: 64,
|
|
59
|
+
stringLength: 1000,
|
|
60
|
+
defaultStringLength: 200,
|
|
61
|
+
flowViews: 16,
|
|
62
|
+
flowTransitions: 64,
|
|
63
|
+
actionsPerEvent: 8,
|
|
64
|
+
emitValues: 32,
|
|
65
|
+
emitValueLength: 2048,
|
|
66
|
+
listItems: 500,
|
|
67
|
+
/** Fields one list item may carry besides its `id`. */
|
|
68
|
+
listItemFields: 16,
|
|
69
|
+
workflowNodes: 500,
|
|
70
|
+
workflowEdges: 2000,
|
|
71
|
+
workflowNodeTypes: 64,
|
|
72
|
+
workflowGroups: 64,
|
|
73
|
+
workflowPorts: 16,
|
|
74
|
+
workflowFields: 32,
|
|
75
|
+
workflowLabelLength: 200,
|
|
76
|
+
workflowStringLength: 2000,
|
|
77
|
+
/** Deepest prop value walked for nested bindings. */
|
|
78
|
+
propDepth: 32,
|
|
79
|
+
});
|
|
80
|
+
/** The name patterns, as the schemas write them. */
|
|
81
|
+
export const SPEC_PATTERNS = Object.freeze({
|
|
82
|
+
stateName: '^[a-z][A-Za-z0-9_]{0,31}$',
|
|
83
|
+
dataName: '^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z0-9_]+)*$',
|
|
84
|
+
flowName: '^[a-z][a-z0-9-]{0,31}$',
|
|
85
|
+
intentName: '^[A-Za-z][A-Za-z0-9_-]{0,63}$',
|
|
86
|
+
itemKey: '^[A-Za-z0-9._:-]{1,64}$',
|
|
87
|
+
groupName: '^[a-z][a-z0-9-]{0,31}$',
|
|
88
|
+
workflowId: '^[A-Za-z0-9._:-]{1,64}$',
|
|
89
|
+
workflowFieldName: '^[a-z][A-Za-z0-9_]{0,31}$',
|
|
90
|
+
/** Multi-line display text (workflow string field values): tab and newline allowed. */
|
|
91
|
+
workflowText: '^[^\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]*$',
|
|
92
|
+
workflowLabel: '^[^\\u0000-\\u001f\\u007f-\\u009f\\u202a-\\u202e\\u2066-\\u2069]*$',
|
|
93
|
+
/**
|
|
94
|
+
* List item text (list state fields): the workflowLabel rule (no C0 or C1
|
|
95
|
+
* controls, bidirectional overrides or isolates) plus the invisible
|
|
96
|
+
* formatting characters that hide or reorder text: the Arabic letter mark,
|
|
97
|
+
* the Mongolian vowel separator, zero-width space, the left-to-right and
|
|
98
|
+
* right-to-left marks, word joiner and invisible operators, and the byte
|
|
99
|
+
* order mark. ZWNJ and ZWJ (U+200C, U+200D) stay allowed: scripts such as
|
|
100
|
+
* Persian and Devanagari, and emoji sequences, need them.
|
|
101
|
+
*/
|
|
102
|
+
listText: LIST_TEXT_PATTERN,
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* The patterns the blueprint validator tests, compiled once; `u` so classes
|
|
106
|
+
* count code points. The workflow validator compiles its own.
|
|
107
|
+
*/
|
|
108
|
+
export const SPEC_REGEX = Object.freeze({
|
|
109
|
+
stateName: new RegExp(SPEC_PATTERNS.stateName, 'u'),
|
|
110
|
+
dataName: new RegExp(SPEC_PATTERNS.dataName, 'u'),
|
|
111
|
+
intentName: new RegExp(SPEC_PATTERNS.intentName, 'u'),
|
|
112
|
+
itemKey: new RegExp(SPEC_PATTERNS.itemKey, 'u'),
|
|
113
|
+
listText: new RegExp(SPEC_PATTERNS.listText, 'u'),
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* A catalog property schema as a dialect schema: the same keywords, with
|
|
117
|
+
* every object closed unless `additionalProperties` gives a map.
|
|
118
|
+
*/
|
|
119
|
+
export function toDialectSchema(schema) {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
122
|
+
if (key === 'properties' && isRecord(value)) {
|
|
123
|
+
out.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [
|
|
124
|
+
name,
|
|
125
|
+
toDialectSchema(child),
|
|
126
|
+
]));
|
|
127
|
+
}
|
|
128
|
+
else if ((key === 'items' || key === 'additionalProperties') && isRecord(value)) {
|
|
129
|
+
out[key] = toDialectSchema(value);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
out[key] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (out.type === 'object' && out.additionalProperties === undefined) {
|
|
136
|
+
out.additionalProperties = false;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Problems with one catalog property schema: it must stay in the dialect,
|
|
142
|
+
* name only {@link SPEC_PROP_REFS} in `$ref`, and give a `type` wherever it
|
|
143
|
+
* has no `$ref`. Empty means usable.
|
|
144
|
+
*/
|
|
145
|
+
export function checkPropSchema(schema) {
|
|
146
|
+
const issues = [];
|
|
147
|
+
const dialect = { $schema: DIALECT_URI, ...toDialectSchema(schema) };
|
|
148
|
+
for (const issue of checkSchemaDocument(dialect)) {
|
|
149
|
+
if (issue.code !== 'bad-ref')
|
|
150
|
+
issues.push(`${issue.path} ${issue.code}`);
|
|
151
|
+
}
|
|
152
|
+
const visit = (node, path) => {
|
|
153
|
+
if (node.$ref !== undefined) {
|
|
154
|
+
const name = /^#\/\$defs\/([A-Za-z]+)$/.exec(node.$ref)?.[1];
|
|
155
|
+
if (name === undefined || !SPEC_PROP_REFS.includes(name)) {
|
|
156
|
+
issues.push(`${path}.$ref names no contract $defs entry`);
|
|
157
|
+
}
|
|
158
|
+
if (Object.keys(node).some((key) => key !== '$ref' && key !== 'description')) {
|
|
159
|
+
issues.push(`${path} mixes $ref with other keywords`);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (node.type === undefined)
|
|
164
|
+
issues.push(`${path} needs a type`);
|
|
165
|
+
for (const [name, child] of Object.entries(node.properties ?? {})) {
|
|
166
|
+
visit(child, `${path}.properties.${name}`);
|
|
167
|
+
}
|
|
168
|
+
if (node.items)
|
|
169
|
+
visit(node.items, `${path}.items`);
|
|
170
|
+
if (node.additionalProperties)
|
|
171
|
+
visit(node.additionalProperties, `${path}.additionalProperties`);
|
|
172
|
+
};
|
|
173
|
+
visit(schema, '$');
|
|
174
|
+
return issues;
|
|
175
|
+
}
|
|
176
|
+
/** Def names both documents declare; the contract root needs them disjoint. */
|
|
177
|
+
export function defCollisions(blueprintSchema, workflowSchema) {
|
|
178
|
+
const own = Object.keys(blueprintSchema.$defs ?? {});
|
|
179
|
+
const other = new Set([...Object.keys(workflowSchema?.$defs ?? {}), 'workflowGraph']);
|
|
180
|
+
return own.filter((name) => other.has(name));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The root property schemas resolve `$ref`s against: the blueprint `$defs`,
|
|
184
|
+
* the workflow `$defs` and a synthetic `workflowGraph` (the workflow root).
|
|
185
|
+
* Without a workflow schema there is no `workflowGraph`, so a property that
|
|
186
|
+
* names it fails closed.
|
|
187
|
+
*/
|
|
188
|
+
export function contractRoot(blueprintSchema, workflowSchema) {
|
|
189
|
+
const defs = { ...(blueprintSchema.$defs ?? {}) };
|
|
190
|
+
if (workflowSchema) {
|
|
191
|
+
Object.assign(defs, workflowSchema.$defs ?? {});
|
|
192
|
+
const { $schema: _s, $id: _i, $defs: _d, title: _t, ...graph } = workflowSchema;
|
|
193
|
+
defs.workflowGraph = graph;
|
|
194
|
+
}
|
|
195
|
+
return { $defs: defs };
|
|
196
|
+
}
|
package/dist/dialect.d.ts
CHANGED
|
@@ -48,9 +48,18 @@ export declare function refName(ref: string): string | undefined;
|
|
|
48
48
|
* (`additionalProperties: false`) or an explicit map. Fails closed.
|
|
49
49
|
*/
|
|
50
50
|
export declare function checkSchemaDocument(document: unknown): SchemaIssue[];
|
|
51
|
+
/**
|
|
52
|
+
* Optional callbacks for {@link validateInstance}. `value` runs at every
|
|
53
|
+
* schema node a value meets (after `$ref` resolution, so a caller can match
|
|
54
|
+
* a `$defs` entry by identity). Returning `undefined` lets the dialect check
|
|
55
|
+
* the value as usual; returning issues (possibly none) replaces that check.
|
|
56
|
+
*/
|
|
57
|
+
export interface DialectHooks {
|
|
58
|
+
value?(node: Schema, value: unknown, path: string): readonly SchemaIssue[] | undefined;
|
|
59
|
+
}
|
|
51
60
|
/**
|
|
52
61
|
* Validate a JSON value against a dialect schema. `root` resolves `$ref`s
|
|
53
62
|
* (defaults to `schema`). Returns every issue, with JSON paths from `$`.
|
|
54
63
|
*/
|
|
55
|
-
export declare function validateInstance(schema: Schema, value: unknown, root?: Schema): SchemaIssue[];
|
|
64
|
+
export declare function validateInstance(schema: Schema, value: unknown, root?: Schema, hooks?: DialectHooks): SchemaIssue[];
|
|
56
65
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
package/dist/dialect.js
CHANGED
|
@@ -153,7 +153,7 @@ export function checkSchemaDocument(document) {
|
|
|
153
153
|
* Validate a JSON value against a dialect schema. `root` resolves `$ref`s
|
|
154
154
|
* (defaults to `schema`). Returns every issue, with JSON paths from `$`.
|
|
155
155
|
*/
|
|
156
|
-
export function validateInstance(schema, value, root = schema) {
|
|
156
|
+
export function validateInstance(schema, value, root = schema, hooks = {}) {
|
|
157
157
|
const issues = [];
|
|
158
158
|
const fail = (code, path, message) => {
|
|
159
159
|
issues.push({ code, path, message });
|
|
@@ -172,6 +172,11 @@ export function validateInstance(schema, value, root = schema) {
|
|
|
172
172
|
visit(target, current, path, depth + 1);
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
|
+
const handled = hooks.value?.(node, current, path);
|
|
176
|
+
if (handled !== undefined) {
|
|
177
|
+
issues.push(...handled);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
175
180
|
if (node.const !== undefined && current !== node.const) {
|
|
176
181
|
fail('const', path, `Must be ${JSON.stringify(node.const)}.`);
|
|
177
182
|
return;
|
|
@@ -47,7 +47,7 @@ export interface BlueprintEditOp {
|
|
|
47
47
|
tree?: BlueprintNode;
|
|
48
48
|
}
|
|
49
49
|
export type BlueprintNodePath = readonly number[];
|
|
50
|
-
/** One catalog node: id, props, children
|
|
50
|
+
/** One catalog node: id, props, children (each with an optional named slot), when, bind and on. */
|
|
51
51
|
export interface BlueprintNode {
|
|
52
52
|
/** A catalog component id. */
|
|
53
53
|
id: string;
|
|
@@ -60,6 +60,9 @@ export interface BlueprintNode {
|
|
|
60
60
|
*/
|
|
61
61
|
slot?: string;
|
|
62
62
|
when?: BlueprintCondition;
|
|
63
|
+
/** Two-way bindings: a prop the component lists as bindable → a $state binding it writes back. */
|
|
64
|
+
bind?: Readonly<Record<string, BlueprintBinding>>;
|
|
65
|
+
on?: BlueprintEventMap;
|
|
63
66
|
}
|
|
64
67
|
/**
|
|
65
68
|
* Shows the node when true (audit G07): the closed Condition AST of the blocks formula grammar.
|
|
@@ -97,11 +100,96 @@ export interface BlueprintNumberFormula {
|
|
|
97
100
|
ifTrue?: BlueprintNumberFormula;
|
|
98
101
|
ifFalse?: BlueprintNumberFormula;
|
|
99
102
|
}
|
|
103
|
+
/** A local state variable name. */
|
|
104
|
+
export type BlueprintStateName = string;
|
|
105
|
+
/** A host data source name, such as tickets.open. */
|
|
106
|
+
export type BlueprintDataName = string;
|
|
107
|
+
/** A flow view or event name. */
|
|
108
|
+
export type BlueprintFlowName = string;
|
|
109
|
+
/** A host intent (action) name. */
|
|
110
|
+
export type BlueprintIntentName = string;
|
|
111
|
+
/** The id of one item in a list variable. */
|
|
112
|
+
export type BlueprintItemKey = string;
|
|
113
|
+
/** A reorder group name. */
|
|
114
|
+
export type BlueprintGroupName = string;
|
|
100
115
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
* Reads one value: {"$state": name} (local state) or {"$data": name} (host data, read-only).
|
|
117
|
+
* Exactly one key; checked by the validator.
|
|
118
|
+
*/
|
|
119
|
+
export interface BlueprintBinding {
|
|
120
|
+
$state?: BlueprintStateName;
|
|
121
|
+
$data?: BlueprintDataName;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One typed, bounded variable (mirrors @texuli/player parseStateDeclarations; list and workflow are
|
|
125
|
+
* new). Per-type fields and initial values are checked by the validator.
|
|
126
|
+
*/
|
|
127
|
+
export interface BlueprintStateDeclaration {
|
|
128
|
+
type: 'boolean' | 'number' | 'enum' | 'string' | 'list' | 'workflow';
|
|
129
|
+
/** A value of the declared type. */
|
|
130
|
+
initial?: unknown;
|
|
131
|
+
min?: number;
|
|
132
|
+
max?: number;
|
|
133
|
+
values?: readonly string[];
|
|
134
|
+
maxLength?: number;
|
|
135
|
+
maxItems?: number;
|
|
136
|
+
source?: BlueprintDataName;
|
|
137
|
+
}
|
|
138
|
+
/** Named views and a closed transition table (mirrors @texuli/player parseFlowDeclaration). */
|
|
139
|
+
export interface BlueprintFlow {
|
|
140
|
+
views: readonly BlueprintFlowName[];
|
|
141
|
+
initial?: BlueprintFlowName;
|
|
142
|
+
transitions: readonly BlueprintFlowTransition[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* One transition: from a view (or * for any view) to a view on an event, with an optional guard.
|
|
146
|
+
*/
|
|
147
|
+
export interface BlueprintFlowTransition {
|
|
148
|
+
from: string;
|
|
149
|
+
to: BlueprintFlowName;
|
|
150
|
+
on: BlueprintFlowName;
|
|
151
|
+
when?: BlueprintCondition;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* One closed action. Per-op fields are checked by the validator (setState: state + value|fromEvent;
|
|
155
|
+
* toggleState: state; moveItem: from, item, to?, index?; flowGo: event; emit: intent, values?;
|
|
156
|
+
* submit: none).
|
|
157
|
+
*/
|
|
158
|
+
export interface BlueprintAction {
|
|
159
|
+
op: 'setState' | 'toggleState' | 'moveItem' | 'flowGo' | 'emit' | 'submit';
|
|
160
|
+
state?: BlueprintStateName;
|
|
161
|
+
/** setState: a JSON literal of the variable's type. */
|
|
162
|
+
value?: unknown;
|
|
163
|
+
fromEvent?: 'value';
|
|
164
|
+
from?: BlueprintStateName;
|
|
165
|
+
to?: BlueprintStateName;
|
|
166
|
+
item?: BlueprintItemKey;
|
|
167
|
+
index?: number;
|
|
168
|
+
event?: BlueprintFlowName;
|
|
169
|
+
intent?: BlueprintIntentName;
|
|
170
|
+
/** emit: name → scalar or $state binding, at most 32. */
|
|
171
|
+
values?: Readonly<Record<string, unknown>>;
|
|
172
|
+
}
|
|
173
|
+
/** Event → actions run in order. Keys must be events the component declares (catalog events). */
|
|
174
|
+
export type BlueprintEventMap = Readonly<Record<string, readonly BlueprintAction[]>>;
|
|
175
|
+
/**
|
|
176
|
+
* Drag as data: the container's items are the bound list; a drop or keyboard move runs the closed
|
|
177
|
+
* moveItem reducer. Containers sharing a group exchange items when accepts allows.
|
|
178
|
+
*/
|
|
179
|
+
export interface BlueprintReorder {
|
|
180
|
+
bind: BlueprintBinding;
|
|
181
|
+
axis?: 'vertical' | 'horizontal' | 'grid';
|
|
182
|
+
group?: BlueprintGroupName;
|
|
183
|
+
accepts?: readonly BlueprintGroupName[];
|
|
184
|
+
locked?: readonly BlueprintItemKey[];
|
|
185
|
+
pull?: 'move' | 'clone' | 'none';
|
|
186
|
+
disabled?: boolean;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Envelope version 1 around one catalog tree (ADR-004, audit G13). Mirrors what
|
|
190
|
+
* @texuli/catalog/envelope reads; a declared status, activation or tier is display data, never
|
|
191
|
+
* authority. Spec 0.4.0 (ADR-008 + PLAN_008 deck encoding) adds an optional spec version, local
|
|
192
|
+
* state and a flow. Planned minor additions: the native profile and several roots.
|
|
105
193
|
*/
|
|
106
194
|
export interface BlueprintEnvelope {
|
|
107
195
|
format: 'texuli-blueprint';
|
|
@@ -122,4 +210,14 @@ export interface BlueprintEnvelope {
|
|
|
122
210
|
activatedBy?: BlueprintActivation;
|
|
123
211
|
provenance?: readonly BlueprintProvenanceEntry[];
|
|
124
212
|
tree: BlueprintNode;
|
|
213
|
+
/**
|
|
214
|
+
* The spec version the envelope was written for. Omitted means 0.2.0; migrateBlueprint stamps it.
|
|
215
|
+
*/
|
|
216
|
+
spec?: '0.3.0' | '0.4.0';
|
|
217
|
+
/**
|
|
218
|
+
* Local state declarations (ADR-008), at most 64. Names, counts and per-type rules are checked by
|
|
219
|
+
* the validator.
|
|
220
|
+
*/
|
|
221
|
+
state?: Readonly<Record<string, BlueprintStateDeclaration>>;
|
|
222
|
+
flow?: BlueprintFlow;
|
|
125
223
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/blueprint.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.4.0, digest fac29f542b16ddb78ab401c508e5b8346f655991c56e25aa811f4931661a8cb3
|
|
3
3
|
export {};
|
|
@@ -10,6 +10,11 @@ export interface SpecComponent {
|
|
|
10
10
|
/** ADR-003 portability. unknown until audited. */
|
|
11
11
|
capability: 'portable-core' | 'web-only' | 'native-only' | 'unknown';
|
|
12
12
|
props: readonly SpecProp[];
|
|
13
|
+
/** The closed events the component fires; a node's on keys must be listed here. */
|
|
14
|
+
events?: readonly SpecEventName[];
|
|
15
|
+
/** Props a node may two-way bind (node bind). */
|
|
16
|
+
bindable?: readonly string[];
|
|
17
|
+
deprecated?: SpecDeprecation;
|
|
13
18
|
}
|
|
14
19
|
export interface SpecProp {
|
|
15
20
|
name: string;
|
|
@@ -21,6 +26,36 @@ export interface SpecProp {
|
|
|
21
26
|
* unknown kinds).
|
|
22
27
|
*/
|
|
23
28
|
extension?: 'web';
|
|
29
|
+
schema?: SpecPropSchema;
|
|
30
|
+
deprecated?: SpecDeprecation;
|
|
31
|
+
}
|
|
32
|
+
/** The closed event vocabulary. */
|
|
33
|
+
export type SpecEventName = 'press' | 'change' | 'select' | 'submit' | 'dismiss' | 'move';
|
|
34
|
+
/** Deprecated since a spec version; warns for one minor before removal. */
|
|
35
|
+
export interface SpecDeprecation {
|
|
36
|
+
since: string;
|
|
37
|
+
replacement?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A dialect subset an array or object prop's value must match. Objects are closed unless
|
|
41
|
+
* additionalProperties gives a map. No pattern (cross-target regex parity). $ref names one of
|
|
42
|
+
* binding, reorder, action, eventMap, condition (blueprint $defs) or workflowGraph.
|
|
43
|
+
*/
|
|
44
|
+
export interface SpecPropSchema {
|
|
45
|
+
type?: 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean';
|
|
46
|
+
description?: string;
|
|
47
|
+
properties?: Readonly<Record<string, SpecPropSchema>>;
|
|
48
|
+
required?: readonly string[];
|
|
49
|
+
additionalProperties?: SpecPropSchema;
|
|
50
|
+
items?: SpecPropSchema;
|
|
51
|
+
enum?: readonly string[];
|
|
52
|
+
minLength?: number;
|
|
53
|
+
maxLength?: number;
|
|
54
|
+
minimum?: number;
|
|
55
|
+
maximum?: number;
|
|
56
|
+
minItems?: number;
|
|
57
|
+
maxItems?: number;
|
|
58
|
+
$ref?: string;
|
|
24
59
|
}
|
|
25
60
|
/**
|
|
26
61
|
* The platform-neutral catalog vocabulary, bootstrapped from the built catalog. Placement, emission
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/catalog.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.4.0, digest fac29f542b16ddb78ab401c508e5b8346f655991c56e25aa811f4931661a8cb3
|
|
3
3
|
export {};
|
|
@@ -4,9 +4,18 @@ export interface SpecDiagnostic {
|
|
|
4
4
|
message: string;
|
|
5
5
|
id?: string;
|
|
6
6
|
}
|
|
7
|
-
export type SpecDiagnosticCode = 'schema' | 'unknown-id' | 'unknown-prop' | 'prop-type' | 'enum-value' | 'missing-required' | 'extension-prop' | 'catalog-mismatch' | 'unsupported-profile' | 'unknown-slot';
|
|
7
|
+
export type SpecDiagnosticCode = 'schema' | 'unknown-id' | 'unknown-prop' | 'prop-type' | 'enum-value' | 'missing-required' | 'extension-prop' | 'catalog-mismatch' | 'unsupported-profile' | 'unknown-slot' | 'state' | 'unknown-state' | 'binding' | 'action' | 'unknown-event' | 'workflow';
|
|
8
|
+
export interface SpecWarning {
|
|
9
|
+
code: SpecWarningCode;
|
|
10
|
+
path: string;
|
|
11
|
+
message: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
}
|
|
14
|
+
export type SpecWarningCode = 'deprecated';
|
|
8
15
|
/** The stable result shape of validating a blueprint against the spec. */
|
|
9
16
|
export interface SpecValidation {
|
|
10
17
|
ok: boolean;
|
|
11
18
|
diagnostics: readonly SpecDiagnostic[];
|
|
19
|
+
/** Advisory findings; ok still means no diagnostics. */
|
|
20
|
+
warnings?: readonly SpecWarning[];
|
|
12
21
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/diagnostics.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.4.0, digest fac29f542b16ddb78ab401c508e5b8346f655991c56e25aa811f4931661a8cb3
|
|
3
3
|
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by @texuli/spec from schemas/manifest.schema.json. Do not edit: run the spec generator.
|
|
2
|
-
// Spec 0.
|
|
2
|
+
// Spec 0.4.0, digest fac29f542b16ddb78ab401c508e5b8346f655991c56e25aa811f4931661a8cb3
|
|
3
3
|
export {};
|