@texuli/spec 0.5.0 → 0.6.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 CHANGED
@@ -14,9 +14,14 @@ by Texuli's Kotlin Multiplatform twin. The package is framework-free: no
14
14
  DOM, no Svelte, no Node APIs and no runtime dependencies.
15
15
 
16
16
  > **Status: 0.x release.** Texuli is in development. The packages are
17
- > published to npm as `0.5.0`, a pre-1.0 release (1.0.0 will be the first
18
- > stable release). Under 0.x, a new minor (`0.6.0`) may contain breaking
19
- > changes, so pin the exact version or use `^0.5.0`, which accepts only 0.5.x.
17
+ > published to npm as `0.6.0`, a pre-1.0 release (1.0.0 will be the first
18
+ > stable release). Under 0.x, a new minor (`0.7.0`) may contain breaking
19
+ > changes, so pin the exact version or use `^0.6.0`, which accepts only 0.6.x.
20
+
21
+ > **New in `0.6.0`.** `@texuli/spec@0.6.0` carries **spec 0.6.0**: the
22
+ > closed motion vocabulary (`@texuli/spec/motion`, below), and the new
23
+ > `@texuli/spec/stream` subpath reads streamed blueprints. Every `0.5.0`
24
+ > envelope migrates by stamping `spec`.
20
25
 
21
26
  > **New in `0.4.0`.** `@texuli/spec@0.4.0` carries **spec 0.4.0**: spec
22
27
  > 0.3.0 (state, bindings, closed actions, `reorder`, workflow graphs), spec
@@ -56,7 +61,7 @@ import {
56
61
  validateInstance,
57
62
  } from '@texuli/spec';
58
63
 
59
- SPEC_VERSION; // the spec version: '0.5.0' in @texuli/spec@0.5.0 ('0.4.0' in @texuli/spec@0.4.0)
64
+ SPEC_VERSION; // the spec version: '0.6.0' in @texuli/spec@0.6.0
60
65
  SPEC_COMPONENT_IDS.includes('alert'); // true: the closed component vocabulary
61
66
 
62
67
  // The dialect validator: a closed subset of JSON Schema 2020-12.
@@ -88,7 +93,7 @@ import { type Schema, type SpecCatalog, validateBlueprint } from '@texuli/spec';
88
93
  import { migrateBlueprint } from '@texuli/spec/migrate';
89
94
  import { validateWorkflowGraph } from '@texuli/spec/workflow';
90
95
 
91
- const migrated = migrateBlueprint(blueprint); // 0.2.0 → 0.3.0 → 0.4.0 → 0.5.0; never throws
96
+ const migrated = migrateBlueprint(blueprint); // 0.2.0 → … → 0.5.0 → 0.6.0; never throws
92
97
  const result = validateBlueprint(migrated.blueprint, {
93
98
  blueprintSchema: blueprintSchemaJson as Schema,
94
99
  catalog: componentsJson as SpecCatalog,
@@ -120,6 +125,8 @@ warning without failing.
120
125
  | `@texuli/spec/workflow` | Runtime, opt-in: workflow graph validation | `validateWorkflowGraph` (pass it in `SpecDocuments.validateWorkflowGraph`) |
121
126
  | `@texuli/spec/migrate` | Runtime, opt-in: envelope migrations | `migrateBlueprint`, `SPEC_MIGRATIONS`, `UNDECLARED_SPEC_VERSION` |
122
127
  | `@texuli/spec/surface` | Runtime: the state surface rules every validator shares (no imports) | `checkStateSurface`, `surfaceInputs`, `SPEC_CONTROL_STATE_TYPES`, `SURFACE_PLAYER_CODES`, `LIST_TEXT_PATTERN` |
128
+ | `@texuli/spec/motion` | Runtime (new in `0.6.0`, spec `0.6.0`): the closed motion vocabulary and its check, shared by the catalog and the player ([motion.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/motion.md)) | `SPEC_MOTION_PRESETS`, `SPEC_MOTION_DURATIONS`, `SPEC_MOTION_STAGGERS`, `SPEC_MOTION_LEVELS`, `checkNodeMotion`, `checkEnvelopeMotion` |
129
+ | `@texuli/spec/stream` | Runtime, opt-in (new in `0.6.0`): streamed blueprints ([stream.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/stream.md)) | `createJsonTokenizer`, `createBlueprintAssembler`, `createBlueprintStream`, `STREAM_NODE_ORDER` |
123
130
  | `@texuli/spec/codegen` | Build time: generation and conformance | `buildSpec`, `projectCatalog`, `checkConformance`, `checkCatalogContract`, `formatJson`, `nextSpecVersion`, `checkVersionHistory` |
124
131
 
125
132
  ## Key guarantees
@@ -148,6 +155,19 @@ warning without failing.
148
155
  component slides. The change is additive: every 0.3.0 envelope is a valid
149
156
  0.4.0 envelope, and the `0.3.0 → 0.4.0` migration only stamps `spec`. The
150
157
  deck model itself is validated by `@texuli/deck`, not by this package.
158
+ - **Motion as data (spec 0.6.0, new in `0.6.0`).** A node may declare `motion`
159
+ (`enter` and `exit` presets `fade`, `rise`, `scale`, `slide-start` and
160
+ `slide-end`; `layout`; `stagger` and `duration` in token steps) and the
161
+ envelope a `motion.level` (`full`, `subtle`, `none`). Names map to the
162
+ motion tokens: a blueprint never carries CSS, keyframes, milliseconds or
163
+ code, and anything else is refused. The `0.5.0 → 0.6.0` migration only
164
+ stamps `spec`.
165
+ - **Streams parse, never guess.** `@texuli/spec/stream` (new in `0.6.0`)
166
+ reads a blueprint split anywhere in linear time under a byte budget and a
167
+ depth cap, reports a node only once its shell (everything but
168
+ `children`) has closed, and refuses duplicate and prototype keys. It
169
+ judges no rule: the catalog checks each node, and the complete draft
170
+ goes through `validateDraft`.
151
171
  - **Structure only.** This validator checks structure. URL policy, tree
152
172
  limits and rendering live in the runtime validator of
153
173
  [`@texuli/catalog`](https://www.npmjs.com/package/@texuli/catalog). The
@@ -190,11 +210,14 @@ catalog hash, and from 0.3.0 may declare the exact spec version it was
190
210
  written for (`spec`; omitted means 0.2.0). `migrateBlueprint`
191
211
  (`@texuli/spec/migrate`) brings an
192
212
  envelope up to the current spec with pure, per-version steps
193
- (`src/migrations/`: `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0` and `0.4.0 → 0.5.0`,
194
- each of which only stamps `spec`); a compatible version range is planned,
195
- not built. `@texuli/spec@0.5.0` carries spec `0.5.0`, which adds the
196
- `data-cohort-table`, `data-notification-inbox`, `data-filter-builder` and
197
- `time-field` catalog ids; `@texuli/spec@0.4.0` carried spec `0.4.0` (digest
213
+ (`src/migrations/`: `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0`, `0.4.0 → 0.5.0` and
214
+ `0.5.0 → 0.6.0`, each of which only stamps `spec`); a compatible version range is planned,
215
+ not built. `@texuli/spec@0.6.0` carries spec `0.6.0` (digest
216
+ `37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc`), which
217
+ adds the closed motion vocabulary (ADR-012); `@texuli/spec@0.5.0` carried
218
+ spec `0.5.0`, which added the `data-cohort-table`,
219
+ `data-notification-inbox`, `data-filter-builder` and `time-field` catalog
220
+ ids, and `@texuli/spec@0.4.0` spec `0.4.0` (digest
198
221
  `fac29f542b16ddb78ab401c508e5b8346f655991c56e25aa811f4931661a8cb3`). Every
199
222
  pin is in `versions.json`.
200
223
 
@@ -211,6 +234,10 @@ conformance harness) are in
211
234
  [packages/spec](https://github.com/DailybotHQ/texuli/tree/main/packages/spec)
212
235
  - The contract in depth (the JSON Schema dialect, projections, conformance
213
236
  and versioning): [contract.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/contract.md)
237
+ - The motion vocabulary (spec 0.6.0):
238
+ [motion.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/motion.md)
239
+ - Streamed blueprints (the incremental tokenizer, the canonical order and
240
+ node shells; new in `0.6.0`): [stream.md](https://github.com/DailybotHQ/texuli/blob/main/packages/spec/docs/stream.md)
214
241
  - The Kotlin Multiplatform twin that consumes the generated models:
215
242
  [native/README.md](https://github.com/DailybotHQ/texuli/blob/main/native/README.md)
216
243
  - Architecture: [ARCHITECTURE.md](https://github.com/DailybotHQ/texuli/blob/main/docs/ARCHITECTURE.md)
@@ -47,7 +47,10 @@ export interface BlueprintEditOp {
47
47
  tree?: BlueprintNode;
48
48
  }
49
49
  export type BlueprintNodePath = readonly number[];
50
- /** One catalog node: id, props, children (each with an optional named slot), when, bind and on. */
50
+ /**
51
+ * One catalog node: id, props, children (each with an optional named slot), when, bind, on and
52
+ * motion.
53
+ */
51
54
  export interface BlueprintNode {
52
55
  /** A catalog component id. */
53
56
  id: string;
@@ -63,6 +66,7 @@ export interface BlueprintNode {
63
66
  /** Two-way bindings: a prop the component lists as bindable → a $state binding it writes back. */
64
67
  bind?: Readonly<Record<string, BlueprintBinding>>;
65
68
  on?: BlueprintEventMap;
69
+ motion?: BlueprintNodeMotion;
66
70
  }
67
71
  /**
68
72
  * Shows the node when true (audit G07): the closed Condition AST of the blocks formula grammar.
@@ -185,11 +189,47 @@ export interface BlueprintReorder {
185
189
  pull?: 'move' | 'clone' | 'none';
186
190
  disabled?: boolean;
187
191
  }
192
+ /**
193
+ * A closed motion preset (ADR-013): fade, rise (fade in from the block end), scale (fade and grow
194
+ * from 96%), slide-start or slide-end (fade in from the inline start or end, mirrored right to
195
+ * left). The runtime maps each to token keyframes; a blueprint never carries keyframes.
196
+ */
197
+ export type BlueprintMotionPreset = 'fade' | 'rise' | 'scale' | 'slide-start' | 'slide-end';
198
+ /** A duration token step (@texuli/tokens motion durations), never milliseconds. */
199
+ export type BlueprintMotionDuration = 'fast' | 'base' | 'slow';
200
+ /**
201
+ * The offset between the entering children of this node, in token steps: none, fast or base. The
202
+ * runtime caps the whole spread.
203
+ */
204
+ export type BlueprintMotionStagger = 'none' | 'fast' | 'base';
205
+ /**
206
+ * A motion ceiling (ADR-013): full, subtle (shorter, no travel or scale) or none (instant). The
207
+ * host and the reader's reduced-motion preference can only lower it.
208
+ */
209
+ export type BlueprintMotionLevel = 'full' | 'subtle' | 'none';
210
+ /**
211
+ * How a node moves (spec 0.6.0, ADR-012): named presets and token steps only. Nothing here is CSS,
212
+ * a duration in milliseconds or code. The server paints the end state, and nothing plays on
213
+ * hydration or under reduced motion.
214
+ */
215
+ export interface BlueprintNodeMotion {
216
+ enter?: BlueprintMotionPreset;
217
+ exit?: BlueprintMotionPreset;
218
+ /** Animate a change of this node's position or size among its siblings (FLIP). */
219
+ layout?: boolean;
220
+ stagger?: BlueprintMotionStagger;
221
+ duration?: BlueprintMotionDuration;
222
+ }
223
+ /** The blueprint's motion settings (spec 0.6.0): the level every node motion plays at. */
224
+ export interface BlueprintEnvelopeMotion {
225
+ level: BlueprintMotionLevel;
226
+ }
188
227
  /**
189
228
  * Envelope version 1 around one catalog tree (ADR-004, audit G13). Mirrors what
190
229
  * @texuli/catalog/envelope reads; a declared status, activation or tier is display data, never
191
230
  * 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.
231
+ * state and a flow; spec 0.6.0 (ADR-012, ADR-013) adds the closed motion vocabulary: a node motion
232
+ * and the envelope motion level. Planned minor additions: the native profile and several roots.
193
233
  */
194
234
  export interface BlueprintEnvelope {
195
235
  format: 'texuli-blueprint';
@@ -213,11 +253,12 @@ export interface BlueprintEnvelope {
213
253
  /**
214
254
  * The spec version the envelope was written for. Omitted means 0.2.0; migrateBlueprint stamps it.
215
255
  */
216
- spec?: '0.3.0' | '0.4.0' | '0.5.0';
256
+ spec?: '0.3.0' | '0.4.0' | '0.5.0' | '0.6.0';
217
257
  /**
218
258
  * Local state declarations (ADR-008), at most 64. Names, counts and per-type rules are checked by
219
259
  * the validator.
220
260
  */
221
261
  state?: Readonly<Record<string, BlueprintStateDeclaration>>;
222
262
  flow?: BlueprintFlow;
263
+ motion?: BlueprintEnvelopeMotion;
223
264
  }
@@ -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.5.0, digest 1ba15e8b082f68140c4b2768b8fd69c9f760e655dd117fac756f356a71525b2d
2
+ // Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
3
3
  export {};
@@ -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.5.0, digest 1ba15e8b082f68140c4b2768b8fd69c9f760e655dd117fac756f356a71525b2d
2
+ // Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
3
3
  export {};
@@ -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.5.0, digest 1ba15e8b082f68140c4b2768b8fd69c9f760e655dd117fac756f356a71525b2d
2
+ // Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
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.5.0, digest 1ba15e8b082f68140c4b2768b8fd69c9f760e655dd117fac756f356a71525b2d
2
+ // Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
3
3
  export {};
@@ -1,5 +1,5 @@
1
1
  // Generated by @texuli/spec from catalog/components.json. Do not edit: run the spec generator.
2
- // Spec 0.5.0, digest 1ba15e8b082f68140c4b2768b8fd69c9f760e655dd117fac756f356a71525b2d
2
+ // Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
3
3
  /** The built catalog this vocabulary was projected from. */
4
4
  export const SPEC_CATALOG_HASH = '4ac7ecedd1c59d1999a9f7eea2f523c7653fececb97258a478f09253f6b6057f';
5
5
  /** Every closed component id, sorted. */
@@ -1,3 +1,3 @@
1
1
  // Generated by @texuli/spec from schemas/workflow.schema.json. Do not edit: run the spec generator.
2
- // Spec 0.5.0, digest 1ba15e8b082f68140c4b2768b8fd69c9f760e655dd117fac756f356a71525b2d
2
+ // Spec 0.6.0, digest 37141b85b9ab9f02af849a62a9c97ba729d6c5c2705130290bcdf45b308ea4cc
3
3
  export {};
@@ -0,0 +1,9 @@
1
+ import type { SpecMigration } from './types.js';
2
+ /**
3
+ * 0.5.0 → 0.6.0 is additive (PLAN_010 T-motion-spec, ADR-012 and ADR-013):
4
+ * nodes gain an optional closed `motion` (enter and exit presets, `layout`,
5
+ * `stagger` and `duration` in token steps) and the envelope an optional
6
+ * `motion.level`. Every 0.5.0 envelope is a valid 0.6.0 envelope, so the
7
+ * step only stamps `spec`.
8
+ */
9
+ export declare const migration050to060: SpecMigration;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 0.5.0 → 0.6.0 is additive (PLAN_010 T-motion-spec, ADR-012 and ADR-013):
3
+ * nodes gain an optional closed `motion` (enter and exit presets, `layout`,
4
+ * `stagger` and `duration` in token steps) and the envelope an optional
5
+ * `motion.level`. Every 0.5.0 envelope is a valid 0.6.0 envelope, so the
6
+ * step only stamps `spec`.
7
+ */
8
+ export const migration050to060 = Object.freeze({
9
+ from: '0.5.0',
10
+ to: '0.6.0',
11
+ migrate: (envelope) => ({ ...envelope, spec: '0.6.0' }),
12
+ });
@@ -3,6 +3,7 @@ import { SPEC_VERSION } from "../version.js";
3
3
  import { migration020to030 } from "./0.2.0-0.3.0.js";
4
4
  import { migration030to040 } from "./0.3.0-0.4.0.js";
5
5
  import { migration040to050 } from "./0.4.0-0.5.0.js";
6
+ import { migration050to060 } from "./0.5.0-0.6.0.js";
6
7
  /** The version an envelope without `spec` was written for. */
7
8
  export const UNDECLARED_SPEC_VERSION = '0.2.0';
8
9
  /** Every step, in order; each `to` is the next step's `from`. */
@@ -10,6 +11,7 @@ export const SPEC_MIGRATIONS = Object.freeze([
10
11
  migration020to030,
11
12
  migration030to040,
12
13
  migration040to050,
14
+ migration050to060,
13
15
  ]);
14
16
  /**
15
17
  * Bring a blueprint envelope up to `target` (the current spec by default)
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `@texuli/spec/motion` — the closed motion vocabulary of spec 0.6.0
3
+ * (ADR-012, ADR-013) in code, mirrored from `schemas/blueprint.schema.json`
4
+ * (`tests/spec.test.ts` asserts they stay equal), and the one structural
5
+ * check every runtime shares: `@texuli/catalog` refuses with it, and
6
+ * `@texuli/player` paints only what it accepted.
7
+ *
8
+ * Agents name presets and token steps. Nothing here is CSS, keyframes, a
9
+ * duration in milliseconds or code. No imports at runtime.
10
+ */
11
+ import type { BlueprintEnvelopeMotion, BlueprintMotionDuration, BlueprintMotionLevel, BlueprintMotionPreset, BlueprintMotionStagger, BlueprintNodeMotion } from './generated/typescript/blueprint.js';
12
+ export type { BlueprintEnvelopeMotion, BlueprintMotionDuration, BlueprintMotionLevel, BlueprintMotionPreset, BlueprintMotionStagger, BlueprintNodeMotion, };
13
+ /** Enter and exit presets (`$defs.motionPreset`). */
14
+ export declare const SPEC_MOTION_PRESETS: readonly BlueprintMotionPreset[];
15
+ /** Duration token steps (`$defs.motionDuration`); `base` when omitted. */
16
+ export declare const SPEC_MOTION_DURATIONS: readonly BlueprintMotionDuration[];
17
+ /** Stagger token steps between entering children (`$defs.motionStagger`). */
18
+ export declare const SPEC_MOTION_STAGGERS: readonly BlueprintMotionStagger[];
19
+ /** Motion ceilings (`$defs.motionLevel`), highest first. */
20
+ export declare const SPEC_MOTION_LEVELS: readonly BlueprintMotionLevel[];
21
+ /** The fields a node `motion` takes (`$defs.nodeMotion`). */
22
+ export declare const SPEC_NODE_MOTION_FIELDS: readonly string[];
23
+ export type SpecMotionCheck<T> = {
24
+ ok: true;
25
+ motion: Readonly<T>;
26
+ }
27
+ /** `path` is relative to the motion value (`""`, `.enter` …). */
28
+ | {
29
+ ok: false;
30
+ path: string;
31
+ message: string;
32
+ };
33
+ /**
34
+ * Check a node `motion` against the closed vocabulary. Returns a frozen
35
+ * copy with only the known fields, or the first refusal. Linear in the
36
+ * number of keys, and it stops at the first unknown one.
37
+ */
38
+ export declare function checkNodeMotion(value: unknown): SpecMotionCheck<BlueprintNodeMotion>;
39
+ /** Check an envelope `motion` (`{ level }`). Returns a frozen copy, or the first refusal. */
40
+ export declare function checkEnvelopeMotion(value: unknown): SpecMotionCheck<BlueprintEnvelopeMotion>;
package/dist/motion.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `@texuli/spec/motion` — the closed motion vocabulary of spec 0.6.0
3
+ * (ADR-012, ADR-013) in code, mirrored from `schemas/blueprint.schema.json`
4
+ * (`tests/spec.test.ts` asserts they stay equal), and the one structural
5
+ * check every runtime shares: `@texuli/catalog` refuses with it, and
6
+ * `@texuli/player` paints only what it accepted.
7
+ *
8
+ * Agents name presets and token steps. Nothing here is CSS, keyframes, a
9
+ * duration in milliseconds or code. No imports at runtime.
10
+ */
11
+ /** Enter and exit presets (`$defs.motionPreset`). */
12
+ export const SPEC_MOTION_PRESETS = Object.freeze([
13
+ 'fade',
14
+ 'rise',
15
+ 'scale',
16
+ 'slide-start',
17
+ 'slide-end',
18
+ ]);
19
+ /** Duration token steps (`$defs.motionDuration`); `base` when omitted. */
20
+ export const SPEC_MOTION_DURATIONS = Object.freeze([
21
+ 'fast',
22
+ 'base',
23
+ 'slow',
24
+ ]);
25
+ /** Stagger token steps between entering children (`$defs.motionStagger`). */
26
+ export const SPEC_MOTION_STAGGERS = Object.freeze([
27
+ 'none',
28
+ 'fast',
29
+ 'base',
30
+ ]);
31
+ /** Motion ceilings (`$defs.motionLevel`), highest first. */
32
+ export const SPEC_MOTION_LEVELS = Object.freeze([
33
+ 'full',
34
+ 'subtle',
35
+ 'none',
36
+ ]);
37
+ /** The fields a node `motion` takes (`$defs.nodeMotion`). */
38
+ export const SPEC_NODE_MOTION_FIELDS = Object.freeze([
39
+ 'enter',
40
+ 'exit',
41
+ 'layout',
42
+ 'stagger',
43
+ 'duration',
44
+ ]);
45
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
46
+ /** A short, single-line label for a refused value: never the value itself in full. */
47
+ function describe(value) {
48
+ if (typeof value === 'string')
49
+ return JSON.stringify(value.slice(0, 24));
50
+ if (Array.isArray(value))
51
+ return 'an array';
52
+ return value === null ? 'null' : typeof value;
53
+ }
54
+ function oneOf(value, allowed, field) {
55
+ if (typeof value === 'string' && allowed.includes(value)) {
56
+ return { ok: true, value: value };
57
+ }
58
+ return {
59
+ ok: false,
60
+ path: `.${field}`,
61
+ message: `"${field}" must be one of ${allowed.join(', ')} (got ${describe(value)}).`,
62
+ };
63
+ }
64
+ /**
65
+ * Check a node `motion` against the closed vocabulary. Returns a frozen
66
+ * copy with only the known fields, or the first refusal. Linear in the
67
+ * number of keys, and it stops at the first unknown one.
68
+ */
69
+ export function checkNodeMotion(value) {
70
+ if (!isRecord(value)) {
71
+ return { ok: false, path: '', message: `"motion" must be an object (got ${describe(value)}).` };
72
+ }
73
+ const out = {};
74
+ for (const key of Object.keys(value)) {
75
+ if (!SPEC_NODE_MOTION_FIELDS.includes(key)) {
76
+ return {
77
+ ok: false,
78
+ path: `.${key.slice(0, 32)}`,
79
+ message: `"motion" takes only ${SPEC_NODE_MOTION_FIELDS.join(', ')}; no CSS, keyframes or code.`,
80
+ };
81
+ }
82
+ const entry = value[key];
83
+ if (key === 'layout') {
84
+ if (typeof entry !== 'boolean') {
85
+ return {
86
+ ok: false,
87
+ path: '.layout',
88
+ message: `"layout" must be a boolean (got ${describe(entry)}).`,
89
+ };
90
+ }
91
+ out.layout = entry;
92
+ continue;
93
+ }
94
+ if (key === 'enter' || key === 'exit') {
95
+ const checked = oneOf(entry, SPEC_MOTION_PRESETS, key);
96
+ if (!checked.ok)
97
+ return checked;
98
+ out[key] = checked.value;
99
+ }
100
+ else if (key === 'stagger') {
101
+ const checked = oneOf(entry, SPEC_MOTION_STAGGERS, key);
102
+ if (!checked.ok)
103
+ return checked;
104
+ out.stagger = checked.value;
105
+ }
106
+ else {
107
+ const checked = oneOf(entry, SPEC_MOTION_DURATIONS, key);
108
+ if (!checked.ok)
109
+ return checked;
110
+ out.duration = checked.value;
111
+ }
112
+ }
113
+ return { ok: true, motion: Object.freeze(out) };
114
+ }
115
+ /** Check an envelope `motion` (`{ level }`). Returns a frozen copy, or the first refusal. */
116
+ export function checkEnvelopeMotion(value) {
117
+ if (!isRecord(value)) {
118
+ return { ok: false, path: '', message: `"motion" must be an object (got ${describe(value)}).` };
119
+ }
120
+ for (const key of Object.keys(value)) {
121
+ if (key !== 'level') {
122
+ return {
123
+ ok: false,
124
+ path: `.${key.slice(0, 32)}`,
125
+ message: 'The envelope "motion" takes only "level".',
126
+ };
127
+ }
128
+ }
129
+ const level = oneOf(value.level, SPEC_MOTION_LEVELS, 'level');
130
+ if (!level.ok)
131
+ return level;
132
+ return { ok: true, motion: Object.freeze({ level: level.value }) };
133
+ }
@@ -0,0 +1,101 @@
1
+ import { type JsonTokenizerOptions, type StreamError } from './tokenizer.js';
2
+ /**
3
+ * The partial-blueprint assembler (ADR-014): builds the value the tokenizer
4
+ * reads and reports, for every catalog node, when its **shell** is final.
5
+ * Streamed nodes put `children` last (`key`, `id`, `slot`, `props`, `when`,
6
+ * `bind`, `on`, `children`), so a node's shell (every member but `children`)
7
+ * is final once `children` starts, or once the node closes without it.
8
+ * Envelopes put `state` and `flow` before the tree, so the envelope header
9
+ * is final once `tree` starts. A member after `children` (or after `tree`)
10
+ * is a `stream-order` issue: the node it lands in is held until the final
11
+ * verdict. The assembler judges no rule itself; the catalog checks each
12
+ * shell and `validateDraft` judges the whole value.
13
+ */
14
+ /** Members a streamed node may have, in the canonical order (`children` last). */
15
+ export declare const STREAM_NODE_ORDER: readonly string[];
16
+ /** A node's members except `children`. Values are the assembled ones (not copies). */
17
+ export type StreamNodeShell = Readonly<Record<string, unknown>>;
18
+ export type BlueprintStreamEvent =
19
+ /** An envelope's members before its tree (`final` once the tree starts; again for a late member). */
20
+ {
21
+ type: 'header';
22
+ header: Readonly<Record<string, unknown>>;
23
+ offset: number;
24
+ }
25
+ /** A node object opened. Its shell is not final yet. */
26
+ | {
27
+ type: 'node-start';
28
+ path: string;
29
+ parent: string | null;
30
+ index: number;
31
+ /** `true` for an element of an array (children, or an array of roots). */
32
+ inArray: boolean;
33
+ offset: number;
34
+ }
35
+ /** A node's `id` member closed (for a skeleton of the right family). */
36
+ | {
37
+ type: 'node-id';
38
+ path: string;
39
+ id: unknown;
40
+ offset: number;
41
+ }
42
+ /** A node's shell is final: every member but `children` arrived. */
43
+ | {
44
+ type: 'node';
45
+ path: string;
46
+ /** The node's stable stream id: its `key` when it has a string one, else its path. */
47
+ streamId: string;
48
+ parent: string | null;
49
+ index: number;
50
+ shell: StreamNodeShell;
51
+ /** `children` started (more nodes follow under this one). */
52
+ hasChildren: boolean;
53
+ /** `true` for an element of an array (children, or an array of roots). */
54
+ inArray: boolean;
55
+ offset: number;
56
+ }
57
+ /** A node object closed: all its children arrived. */
58
+ | {
59
+ type: 'node-end';
60
+ path: string;
61
+ offset: number;
62
+ }
63
+ /** A member after `children` (node) or after the tree (envelope, path `$`). */
64
+ | {
65
+ type: 'stream-order';
66
+ path: string;
67
+ key: string;
68
+ offset: number;
69
+ }
70
+ /** The whole value arrived and the stream ended cleanly. */
71
+ | {
72
+ type: 'complete';
73
+ value: unknown;
74
+ offset: number;
75
+ }
76
+ /** The stream stopped: nothing more is reported. */
77
+ | {
78
+ type: 'error';
79
+ error: StreamError;
80
+ };
81
+ export interface BlueprintAssemblerOptions extends JsonTokenizerOptions {
82
+ /**
83
+ * Root members that hold the tree. Default `['tree']` (the blueprint
84
+ * envelope). A host that also streams another wrapper (the player's
85
+ * local-state wrapper uses `blueprint`) adds its member here.
86
+ */
87
+ treeMembers?: readonly string[];
88
+ }
89
+ export interface BlueprintAssembler {
90
+ /** Feed a chunk; returns what it completed. */
91
+ write(chunk: string): BlueprintStreamEvent[];
92
+ /** No more input. Returns `complete` or the `truncated` error. */
93
+ end(): BlueprintStreamEvent[];
94
+ /** Stop with an `aborted` or `connection` error (a dropped stream). */
95
+ abort(code?: 'aborted' | 'connection', message?: string): BlueprintStreamEvent[];
96
+ readonly bytes: number;
97
+ readonly error: StreamError | null;
98
+ /** The value assembled so far (containers still open are partial). */
99
+ readonly value: unknown;
100
+ }
101
+ export declare function createBlueprintAssembler(options?: BlueprintAssemblerOptions): BlueprintAssembler;