@camstack/types 1.2.264 → 1.2.265

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.
@@ -0,0 +1,362 @@
1
+ /**
2
+ * A COMPOSITION: a device assembled from other devices' state (D659).
3
+ *
4
+ * The data half of the composed-devices spec §1. A composition is stored inside
5
+ * a core block (`CoreBlock.source.kind === 'composition'`) and run by the shared
6
+ * `composer` runner. It names a TARGET and, for each capability of the target, a
7
+ * FIELD MAP: every state field has exactly one source.
8
+ *
9
+ * Sources are keyed by `(addonId, stableId)`, NEVER by numeric device id. A
10
+ * resync reissues numeric ids, and a composition that stored them would be
11
+ * orphaned by the first re-adoption (the linked-devices lesson). A `stableId` is
12
+ * unique only within its owning addon, so the owner travels with it.
13
+ *
14
+ * Later slices EXTEND these unions and do not reshape them. The variants a later
15
+ * slice implements are already in the wire shape, and `validateComposition`
16
+ * refuses them BY NAME until then:
17
+ * target `existing` → slice 2 · feature `passthrough` → slice 3 ·
18
+ * `commands` (`forward`) → slice 3 · source `code` → slice 4.
19
+ */
20
+ import { z } from 'zod';
21
+ import { DeviceRole, DeviceType } from '../device/device-type.js';
22
+ /** The owner of every composed device: the `composer` builtin. */
23
+ export declare const COMPOSER_ADDON_ID = "composer";
24
+ /** A composed device's stableId is this prefix plus the block id. */
25
+ export declare const COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
26
+ /** A device with more capabilities than this is two devices. */
27
+ export declare const MAX_COMPOSITION_FEATURES = 16;
28
+ export declare const MAX_COMPOSITION_FIELDS_PER_FEATURE = 32;
29
+ /** Longest snippet a `code` source may carry (slice 4). */
30
+ export declare const MAX_COMPOSITION_CODE_LENGTH = 20000;
31
+ export declare const CompositionSourceRefSchema: z.ZodObject<{
32
+ addonId: z.ZodString;
33
+ stableId: z.ZodString;
34
+ }, z.core.$strip>;
35
+ export type CompositionSourceRef = z.infer<typeof CompositionSourceRefSchema>;
36
+ /** One field of one capability of one source device. */
37
+ export declare const CompositionFieldReadSchema: z.ZodObject<{
38
+ source: z.ZodObject<{
39
+ addonId: z.ZodString;
40
+ stableId: z.ZodString;
41
+ }, z.core.$strip>;
42
+ cap: z.ZodString;
43
+ fieldPath: z.ZodString;
44
+ }, z.core.$strip>;
45
+ export type CompositionFieldRead = z.infer<typeof CompositionFieldReadSchema>;
46
+ /** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
47
+ export declare const CompositionFromSourceSchema: z.ZodObject<{
48
+ source: z.ZodObject<{
49
+ addonId: z.ZodString;
50
+ stableId: z.ZodString;
51
+ }, z.core.$strip>;
52
+ cap: z.ZodString;
53
+ fieldPath: z.ZodString;
54
+ kind: z.ZodLiteral<"from">;
55
+ }, z.core.$strip>;
56
+ export type CompositionFromSource = z.infer<typeof CompositionFromSourceSchema>;
57
+ /** A constant binding. No device is read. */
58
+ export declare const CompositionLiteralBindingSchema: z.ZodObject<{
59
+ kind: z.ZodLiteral<"literal">;
60
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
61
+ }, z.core.$strip>;
62
+ export type CompositionLiteralBinding = z.infer<typeof CompositionLiteralBindingSchema>;
63
+ export declare const CompositionBindingSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
64
+ source: z.ZodObject<{
65
+ addonId: z.ZodString;
66
+ stableId: z.ZodString;
67
+ }, z.core.$strip>;
68
+ cap: z.ZodString;
69
+ fieldPath: z.ZodString;
70
+ kind: z.ZodLiteral<"from">;
71
+ }, z.core.$strip>, z.ZodObject<{
72
+ kind: z.ZodLiteral<"literal">;
73
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
74
+ }, z.core.$strip>], "kind">;
75
+ export type CompositionBinding = z.infer<typeof CompositionBindingSchema>;
76
+ /**
77
+ * `expression`: a formula over named bindings, in the salvaged expression
78
+ * engine. Validated at parse by the SAME function every other consumer runs,
79
+ * so the editor and the store cannot disagree.
80
+ */
81
+ export declare const CompositionExpressionSourceSchema: z.ZodObject<{
82
+ kind: z.ZodLiteral<"expression">;
83
+ expr: z.ZodString;
84
+ bindings: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
85
+ source: z.ZodObject<{
86
+ addonId: z.ZodString;
87
+ stableId: z.ZodString;
88
+ }, z.core.$strip>;
89
+ cap: z.ZodString;
90
+ fieldPath: z.ZodString;
91
+ kind: z.ZodLiteral<"from">;
92
+ }, z.core.$strip>, z.ZodObject<{
93
+ kind: z.ZodLiteral<"literal">;
94
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
95
+ }, z.core.$strip>], "kind">>;
96
+ }, z.core.$strip>;
97
+ export type CompositionExpressionSource = z.infer<typeof CompositionExpressionSourceSchema>;
98
+ /** `code`: RESERVED for slice 4 (own runner, one-way eject). */
99
+ export declare const CompositionCodeSourceSchema: z.ZodObject<{
100
+ kind: z.ZodLiteral<"code">;
101
+ code: z.ZodString;
102
+ }, z.core.$strip>;
103
+ export type CompositionCodeSource = z.infer<typeof CompositionCodeSourceSchema>;
104
+ export declare const CompositionFieldSourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
105
+ source: z.ZodObject<{
106
+ addonId: z.ZodString;
107
+ stableId: z.ZodString;
108
+ }, z.core.$strip>;
109
+ cap: z.ZodString;
110
+ fieldPath: z.ZodString;
111
+ kind: z.ZodLiteral<"from">;
112
+ }, z.core.$strip>, z.ZodObject<{
113
+ kind: z.ZodLiteral<"expression">;
114
+ expr: z.ZodString;
115
+ bindings: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
116
+ source: z.ZodObject<{
117
+ addonId: z.ZodString;
118
+ stableId: z.ZodString;
119
+ }, z.core.$strip>;
120
+ cap: z.ZodString;
121
+ fieldPath: z.ZodString;
122
+ kind: z.ZodLiteral<"from">;
123
+ }, z.core.$strip>, z.ZodObject<{
124
+ kind: z.ZodLiteral<"literal">;
125
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
126
+ }, z.core.$strip>], "kind">>;
127
+ }, z.core.$strip>, z.ZodObject<{
128
+ kind: z.ZodLiteral<"code">;
129
+ code: z.ZodString;
130
+ }, z.core.$strip>], "kind">;
131
+ export type CompositionFieldSource = z.infer<typeof CompositionFieldSourceSchema>;
132
+ /** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
133
+ export declare const CompositionForwardCommandSchema: z.ZodObject<{
134
+ kind: z.ZodLiteral<"forward">;
135
+ source: z.ZodObject<{
136
+ addonId: z.ZodString;
137
+ stableId: z.ZodString;
138
+ }, z.core.$strip>;
139
+ cap: z.ZodString;
140
+ method: z.ZodString;
141
+ }, z.core.$strip>;
142
+ export type CompositionForwardCommand = z.infer<typeof CompositionForwardCommandSchema>;
143
+ export declare const CompositionCommandTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
144
+ kind: z.ZodLiteral<"forward">;
145
+ source: z.ZodObject<{
146
+ addonId: z.ZodString;
147
+ stableId: z.ZodString;
148
+ }, z.core.$strip>;
149
+ cap: z.ZodString;
150
+ method: z.ZodString;
151
+ }, z.core.$strip>, z.ZodObject<{
152
+ kind: z.ZodLiteral<"code">;
153
+ code: z.ZodString;
154
+ }, z.core.$strip>], "kind">;
155
+ export type CompositionCommandTarget = z.infer<typeof CompositionCommandTargetSchema>;
156
+ export declare const CompositionFieldsFeatureSchema: z.ZodObject<{
157
+ kind: z.ZodLiteral<"fields">;
158
+ cap: z.ZodString;
159
+ fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
160
+ source: z.ZodObject<{
161
+ addonId: z.ZodString;
162
+ stableId: z.ZodString;
163
+ }, z.core.$strip>;
164
+ cap: z.ZodString;
165
+ fieldPath: z.ZodString;
166
+ kind: z.ZodLiteral<"from">;
167
+ }, z.core.$strip>, z.ZodObject<{
168
+ kind: z.ZodLiteral<"expression">;
169
+ expr: z.ZodString;
170
+ bindings: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
171
+ source: z.ZodObject<{
172
+ addonId: z.ZodString;
173
+ stableId: z.ZodString;
174
+ }, z.core.$strip>;
175
+ cap: z.ZodString;
176
+ fieldPath: z.ZodString;
177
+ kind: z.ZodLiteral<"from">;
178
+ }, z.core.$strip>, z.ZodObject<{
179
+ kind: z.ZodLiteral<"literal">;
180
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
181
+ }, z.core.$strip>], "kind">>;
182
+ }, z.core.$strip>, z.ZodObject<{
183
+ kind: z.ZodLiteral<"code">;
184
+ code: z.ZodString;
185
+ }, z.core.$strip>], "kind">>;
186
+ commands: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
187
+ kind: z.ZodLiteral<"forward">;
188
+ source: z.ZodObject<{
189
+ addonId: z.ZodString;
190
+ stableId: z.ZodString;
191
+ }, z.core.$strip>;
192
+ cap: z.ZodString;
193
+ method: z.ZodString;
194
+ }, z.core.$strip>, z.ZodObject<{
195
+ kind: z.ZodLiteral<"code">;
196
+ code: z.ZodString;
197
+ }, z.core.$strip>], "kind">>>;
198
+ }, z.core.$strip>;
199
+ export type CompositionFieldsFeature = z.infer<typeof CompositionFieldsFeatureSchema>;
200
+ /** RESERVED for slice 3: a media / data-plane cap, whole, from one source. */
201
+ export declare const CompositionPassthroughFeatureSchema: z.ZodObject<{
202
+ kind: z.ZodLiteral<"passthrough">;
203
+ cap: z.ZodString;
204
+ source: z.ZodObject<{
205
+ addonId: z.ZodString;
206
+ stableId: z.ZodString;
207
+ }, z.core.$strip>;
208
+ }, z.core.$strip>;
209
+ export type CompositionPassthroughFeature = z.infer<typeof CompositionPassthroughFeatureSchema>;
210
+ export declare const CompositionFeatureSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
211
+ kind: z.ZodLiteral<"fields">;
212
+ cap: z.ZodString;
213
+ fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
214
+ source: z.ZodObject<{
215
+ addonId: z.ZodString;
216
+ stableId: z.ZodString;
217
+ }, z.core.$strip>;
218
+ cap: z.ZodString;
219
+ fieldPath: z.ZodString;
220
+ kind: z.ZodLiteral<"from">;
221
+ }, z.core.$strip>, z.ZodObject<{
222
+ kind: z.ZodLiteral<"expression">;
223
+ expr: z.ZodString;
224
+ bindings: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
225
+ source: z.ZodObject<{
226
+ addonId: z.ZodString;
227
+ stableId: z.ZodString;
228
+ }, z.core.$strip>;
229
+ cap: z.ZodString;
230
+ fieldPath: z.ZodString;
231
+ kind: z.ZodLiteral<"from">;
232
+ }, z.core.$strip>, z.ZodObject<{
233
+ kind: z.ZodLiteral<"literal">;
234
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
235
+ }, z.core.$strip>], "kind">>;
236
+ }, z.core.$strip>, z.ZodObject<{
237
+ kind: z.ZodLiteral<"code">;
238
+ code: z.ZodString;
239
+ }, z.core.$strip>], "kind">>;
240
+ commands: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
241
+ kind: z.ZodLiteral<"forward">;
242
+ source: z.ZodObject<{
243
+ addonId: z.ZodString;
244
+ stableId: z.ZodString;
245
+ }, z.core.$strip>;
246
+ cap: z.ZodString;
247
+ method: z.ZodString;
248
+ }, z.core.$strip>, z.ZodObject<{
249
+ kind: z.ZodLiteral<"code">;
250
+ code: z.ZodString;
251
+ }, z.core.$strip>], "kind">>>;
252
+ }, z.core.$strip>, z.ZodObject<{
253
+ kind: z.ZodLiteral<"passthrough">;
254
+ cap: z.ZodString;
255
+ source: z.ZodObject<{
256
+ addonId: z.ZodString;
257
+ stableId: z.ZodString;
258
+ }, z.core.$strip>;
259
+ }, z.core.$strip>], "kind">;
260
+ export type CompositionFeature = z.infer<typeof CompositionFeatureSchema>;
261
+ /**
262
+ * A NEW device. It has no `name` of its own: the block's name IS the device's
263
+ * name (2026-08-08). A second copy here would be a second writer of one string (D62).
264
+ */
265
+ export declare const CompositionNewTargetSchema: z.ZodObject<{
266
+ kind: z.ZodLiteral<"new">;
267
+ type: z.ZodEnum<typeof DeviceType>;
268
+ role: z.ZodOptional<z.ZodEnum<typeof DeviceRole>>;
269
+ }, z.core.$strip>;
270
+ export type CompositionNewTarget = z.infer<typeof CompositionNewTargetSchema>;
271
+ /** RESERVED for slice 2: customize an existing device. */
272
+ export declare const CompositionExistingTargetSchema: z.ZodObject<{
273
+ kind: z.ZodLiteral<"existing">;
274
+ device: z.ZodObject<{
275
+ addonId: z.ZodString;
276
+ stableId: z.ZodString;
277
+ }, z.core.$strip>;
278
+ }, z.core.$strip>;
279
+ export type CompositionExistingTarget = z.infer<typeof CompositionExistingTargetSchema>;
280
+ export declare const CompositionTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
281
+ kind: z.ZodLiteral<"new">;
282
+ type: z.ZodEnum<typeof DeviceType>;
283
+ role: z.ZodOptional<z.ZodEnum<typeof DeviceRole>>;
284
+ }, z.core.$strip>, z.ZodObject<{
285
+ kind: z.ZodLiteral<"existing">;
286
+ device: z.ZodObject<{
287
+ addonId: z.ZodString;
288
+ stableId: z.ZodString;
289
+ }, z.core.$strip>;
290
+ }, z.core.$strip>], "kind">;
291
+ export type CompositionTarget = z.infer<typeof CompositionTargetSchema>;
292
+ export declare const CompositionSchema: z.ZodObject<{
293
+ target: z.ZodDiscriminatedUnion<[z.ZodObject<{
294
+ kind: z.ZodLiteral<"new">;
295
+ type: z.ZodEnum<typeof DeviceType>;
296
+ role: z.ZodOptional<z.ZodEnum<typeof DeviceRole>>;
297
+ }, z.core.$strip>, z.ZodObject<{
298
+ kind: z.ZodLiteral<"existing">;
299
+ device: z.ZodObject<{
300
+ addonId: z.ZodString;
301
+ stableId: z.ZodString;
302
+ }, z.core.$strip>;
303
+ }, z.core.$strip>], "kind">;
304
+ features: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
305
+ kind: z.ZodLiteral<"fields">;
306
+ cap: z.ZodString;
307
+ fields: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
308
+ source: z.ZodObject<{
309
+ addonId: z.ZodString;
310
+ stableId: z.ZodString;
311
+ }, z.core.$strip>;
312
+ cap: z.ZodString;
313
+ fieldPath: z.ZodString;
314
+ kind: z.ZodLiteral<"from">;
315
+ }, z.core.$strip>, z.ZodObject<{
316
+ kind: z.ZodLiteral<"expression">;
317
+ expr: z.ZodString;
318
+ bindings: z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
319
+ source: z.ZodObject<{
320
+ addonId: z.ZodString;
321
+ stableId: z.ZodString;
322
+ }, z.core.$strip>;
323
+ cap: z.ZodString;
324
+ fieldPath: z.ZodString;
325
+ kind: z.ZodLiteral<"from">;
326
+ }, z.core.$strip>, z.ZodObject<{
327
+ kind: z.ZodLiteral<"literal">;
328
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
329
+ }, z.core.$strip>], "kind">>;
330
+ }, z.core.$strip>, z.ZodObject<{
331
+ kind: z.ZodLiteral<"code">;
332
+ code: z.ZodString;
333
+ }, z.core.$strip>], "kind">>;
334
+ commands: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
335
+ kind: z.ZodLiteral<"forward">;
336
+ source: z.ZodObject<{
337
+ addonId: z.ZodString;
338
+ stableId: z.ZodString;
339
+ }, z.core.$strip>;
340
+ cap: z.ZodString;
341
+ method: z.ZodString;
342
+ }, z.core.$strip>, z.ZodObject<{
343
+ kind: z.ZodLiteral<"code">;
344
+ code: z.ZodString;
345
+ }, z.core.$strip>], "kind">>>;
346
+ }, z.core.$strip>, z.ZodObject<{
347
+ kind: z.ZodLiteral<"passthrough">;
348
+ cap: z.ZodString;
349
+ source: z.ZodObject<{
350
+ addonId: z.ZodString;
351
+ stableId: z.ZodString;
352
+ }, z.core.$strip>;
353
+ }, z.core.$strip>], "kind">>;
354
+ }, z.core.$strip>;
355
+ export type Composition = z.infer<typeof CompositionSchema>;
356
+ export declare function compositionSourceKey(ref: CompositionSourceRef): string;
357
+ export declare function compositionSliceKey(ref: CompositionSourceRef, cap: string): string;
358
+ export declare function compositionReadKey(read: CompositionFieldRead): string;
359
+ export declare function composedDeviceStableId(blockId: string): string;
360
+ export declare function composedDeviceRef(blockId: string): CompositionSourceRef;
361
+ /** Every device field a source reads: `from` is one read, an expression is its `from` bindings. */
362
+ export declare function fieldReads(source: CompositionFieldSource): readonly CompositionFieldRead[];
@@ -0,0 +1,63 @@
1
+ /**
2
+ * One composed field, evaluated from what its sources say NOW (D659).
3
+ *
4
+ * Pure. The composer calls it on every source change, and the slice-5 editor
5
+ * will call it for its live preview, so both see the same answer.
6
+ *
7
+ * The rules (spec §2):
8
+ * - a source that is offline or unreadable keeps its last value and is marked
9
+ * STALE; the field is stale and keeps computing (D49);
10
+ * - a source that is GONE makes the field UNAVAILABLE, `fatal`, and never a
11
+ * plausible number (D393);
12
+ * - a source that is unavailable but NOT gone (e.g. a cap unregistered at
13
+ * runtime, R7b) makes the field UNAVAILABLE, non-fatal, `value: null`: it
14
+ * may come back on its own, no block failure is raised for it;
15
+ * - an expression that fails on a value makes the field unavailable for that
16
+ * value, and NOT fatal: the next value may compute.
17
+ *
18
+ * When an expression has more than one unavailable binding, a FATAL one always
19
+ * wins the outcome, regardless of position: every binding is scanned before
20
+ * the field is judged, so an earlier non-fatal binding (e.g. a cap that is
21
+ * momentarily unregistered) can never hide a later fatal one (a deleted
22
+ * source) behind an early return.
23
+ */
24
+ import type { ExpressionValue } from '../expression/ast.js';
25
+ import { type CompositionFieldRead, type CompositionFieldSource } from './composition.js';
26
+ export interface SourceFreshReading {
27
+ readonly state: 'fresh';
28
+ readonly value: ExpressionValue;
29
+ }
30
+ export interface SourceStaleReading {
31
+ readonly state: 'stale';
32
+ readonly value: ExpressionValue;
33
+ readonly reason: string;
34
+ }
35
+ export interface SourceUnavailableReading {
36
+ readonly state: 'unavailable';
37
+ readonly reason: string;
38
+ /** True when the source is GONE: device or cap missing, two reads agreed. */
39
+ readonly fatal: boolean;
40
+ }
41
+ export type SourceReading = SourceFreshReading | SourceStaleReading | SourceUnavailableReading;
42
+ export type SourceReader = (read: CompositionFieldRead) => SourceReading;
43
+ export interface FieldValueOutcome {
44
+ readonly state: 'fresh' | 'stale';
45
+ readonly value: ExpressionValue;
46
+ readonly reason: string | null;
47
+ /** When a windowed builtin's answer flips on its own; null when nothing will. */
48
+ readonly deadline: number | null;
49
+ }
50
+ export interface FieldUnavailableOutcome {
51
+ readonly state: 'unavailable';
52
+ /** Always `null` — an unavailable field is never a stand-in value (D393). */
53
+ readonly value: null;
54
+ readonly reason: string;
55
+ readonly fatal: boolean;
56
+ }
57
+ export type FieldOutcome = FieldValueOutcome | FieldUnavailableOutcome;
58
+ export interface EvaluateCompositionFieldInput {
59
+ readonly source: CompositionFieldSource;
60
+ readonly read: SourceReader;
61
+ readonly now: number;
62
+ }
63
+ export declare function evaluateCompositionField(input: EvaluateCompositionFieldInput): FieldOutcome;
@@ -0,0 +1,10 @@
1
+ import { type Composition, type CompositionSourceRef } from './composition.js';
2
+ export declare const ROOM_OCCUPANCY_WINDOW_MS = 300000;
3
+ export declare const ROOM_EXAMPLE_TEMPERATURE_SOURCES: readonly CompositionSourceRef[];
4
+ export declare const ROOM_EXAMPLE_MOTION_SOURCES: readonly CompositionSourceRef[];
5
+ export interface RoomExampleInput {
6
+ readonly temperatureSources: readonly CompositionSourceRef[];
7
+ readonly motionSources: readonly CompositionSourceRef[];
8
+ readonly occupancyWindowMs: number;
9
+ }
10
+ export declare function roomCompositionExample(input: RoomExampleInput): Composition;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What an expression can PRODUCE: the static half of type validation at save
3
+ * (D659). The language is untyped at runtime; this walk gives the set of value
4
+ * kinds the result may take, so a save that would write a string into
5
+ * `celsius`, or a possible null into a non-nullable field, is refused by name
6
+ * before it is stored.
7
+ */
8
+ import type { ExpressionValue } from '../expression/ast.js';
9
+ export type ExpressionValueKind = 'number' | 'string' | 'boolean' | 'null';
10
+ export type ExpressionKindSet = ReadonlySet<ExpressionValueKind>;
11
+ export interface FixedKindRule {
12
+ readonly kind: 'fixed';
13
+ readonly result: readonly ExpressionValueKind[];
14
+ }
15
+ /** `coalesce`: the first non-null argument, null only when every argument may be null. */
16
+ export interface CoalesceKindRule {
17
+ readonly kind: 'coalesce';
18
+ }
19
+ export type BuiltinKindRule = FixedKindRule | CoalesceKindRule;
20
+ /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
21
+ export declare const BUILTIN_RESULT_KINDS: Readonly<Record<string, BuiltinKindRule>>;
22
+ export interface InferExpressionKindsInput {
23
+ readonly expr: string;
24
+ readonly bindingKinds: ReadonlyMap<string, ExpressionKindSet>;
25
+ }
26
+ export interface InferredKinds {
27
+ readonly ok: true;
28
+ readonly kinds: ExpressionKindSet;
29
+ }
30
+ export interface InferenceFailure {
31
+ readonly ok: false;
32
+ readonly error: string;
33
+ }
34
+ export type InferExpressionKindsResult = InferredKinds | InferenceFailure;
35
+ export declare function kindOfValue(value: ExpressionValue): ExpressionValueKind;
36
+ export declare function inferExpressionKinds(input: InferExpressionKindsInput): InferExpressionKindsResult;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Compositions: composed devices (D659). This is the module barrel; leaf files
3
+ * inside @camstack/types import the deep modules, never this file.
4
+ */
5
+ export * from './composition.js';
6
+ export * from './composition-graph.js';
7
+ export * from './composition-report.js';
8
+ export * from './evaluate-field.js';
9
+ export * from './examples.js';
10
+ export * from './expression-kinds.js';
11
+ export * from './state-fields.js';
12
+ export * from './validate-composition.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * What a capability's state field IS: kind, nullability, whether required. The
3
+ * validator needs nullability, which `enumerateSchemaFields` does not report,
4
+ * and that decides a field's unavailable mode (D393).
5
+ */
6
+ import { z } from 'zod';
7
+ import { type WireableField } from '../device/schema-fields.js';
8
+ export interface StateFieldDescription {
9
+ readonly path: string;
10
+ readonly kind: WireableField['kind'];
11
+ readonly nullable: boolean;
12
+ readonly required: boolean;
13
+ /** Present only when `kind === 'enum'` — the closed set of strings the field accepts. */
14
+ readonly enumValues?: readonly string[];
15
+ }
16
+ export declare function describeStateField(schema: z.ZodType, path: string): StateFieldDescription | null;
17
+ export declare function describeTopLevelStateFields(schema: z.ZodType): ReadonlyMap<string, StateFieldDescription>;
18
+ /** Top-level keys a value MUST carry: those whose schema refuses `undefined`. */
19
+ export declare function requiredTopLevelKeys(schema: z.ZodType): readonly string[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Save-time validation of a composition (D659): shape, capability fit, field
3
+ * kinds, nullability, sources, cycles. Every refusal names its reason (D391),
4
+ * and every variant a later slice implements is refused BY NAME with that slice.
5
+ *
6
+ * Pure: the caller DESCRIBES the sources (one read each) and passes the other
7
+ * stored compositions. `planComposition` is the static half the composer
8
+ * re-runs at apply time, where the sources are the tracker's business.
9
+ */
10
+ import type { CapabilityDefinition } from '../capabilities/capability-definition.js';
11
+ import { type Composition, type CompositionSourceRef } from './composition.js';
12
+ import { type CompositionPeer } from './composition-graph.js';
13
+ import type { CompositionFieldPlan, CompositionProblem, CompositionValidation } from './composition-report.js';
14
+ export type CapabilityLookup = (capName: string) => CapabilityDefinition | null;
15
+ export declare function buildCapabilityLookup(defs: readonly CapabilityDefinition[]): CapabilityLookup;
16
+ /** Caps every `BaseDevice` registers on itself; a second registration throws. */
17
+ export declare const BASE_DEVICE_CAPS: ReadonlySet<string>;
18
+ export interface CompositionSourceFound {
19
+ readonly kind: 'found';
20
+ readonly deviceId: number;
21
+ readonly caps: ReadonlySet<string>;
22
+ }
23
+ export interface CompositionSourceAbsent {
24
+ readonly kind: 'absent';
25
+ }
26
+ export interface CompositionSourceUnreadable {
27
+ readonly kind: 'unreadable';
28
+ readonly error: string;
29
+ }
30
+ export type CompositionSourceDescription = CompositionSourceFound | CompositionSourceAbsent | CompositionSourceUnreadable;
31
+ export interface ValidateCompositionContext {
32
+ readonly lookupCap: CapabilityLookup;
33
+ /** Keyed by `compositionSourceKey`. A ref with no entry was not read, and is refused as unreadable. */
34
+ readonly sources: ReadonlyMap<string, CompositionSourceDescription>;
35
+ /** The device this composition writes; null on create, when the block has no id yet. */
36
+ readonly self: CompositionSourceRef | null;
37
+ /** Every OTHER stored composition; cycles run through them. */
38
+ readonly peers: readonly CompositionPeer[];
39
+ }
40
+ export interface CompositionPlanResult {
41
+ readonly problems: readonly CompositionProblem[];
42
+ readonly fields: readonly CompositionFieldPlan[];
43
+ }
44
+ export declare function planComposition(composition: Composition, lookupCap: CapabilityLookup): CompositionPlanResult;
45
+ export declare function compositionSourceRefs(composition: Composition): readonly CompositionSourceRef[];
46
+ export declare function validateComposition(composition: Composition, ctx: ValidateCompositionContext): CompositionValidation;
@@ -8,6 +8,9 @@ export interface WireableField {
8
8
  * `target.itemKey`. Absent/false for plain status fields. */
9
9
  readonly item?: boolean;
10
10
  }
11
+ /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
12
+ * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
13
+ export declare function unwrapSchema(schema: z.ZodType): z.ZodType;
11
14
  /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
12
15
  * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
13
16
  * so fields with null live values are still offered. Arrays / unknown shapes
@@ -1,10 +1,38 @@
1
+ /**
2
+ * Frozen, null-prototype builtin function table for the expression engine
3
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
4
+ * parser rejects any callee not in it, and the evaluator gates each call on an
5
+ * own-property check against it.
6
+ *
7
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
8
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
9
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
10
+ * (there is no `Object.prototype` in the chain), so those names are not
11
+ * callable — they are simply "unknown function" at parse time.
12
+ *
13
+ * Every numeric argument is validated as a finite number and every numeric
14
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
15
+ * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
16
+ * closed rather than emitting a garbage value.
17
+ */
1
18
  import type { ExpressionValue } from './ast.js';
2
- /** Optional runtime hooks the evaluator threads through to builtins. Stage U
3
- * installs the real unit-conversion table here; absent → the `convert` stub. */
19
+ /** Optional runtime hooks the evaluator threads through to builtins. */
4
20
  export interface ExpressionEvalHooks {
5
21
  /** Convert `value` from unit `from` to unit `to`; `null` when the pair is
6
22
  * not convertible. Absent hook → `convert` uses its identity-only stub. */
7
23
  readonly convert?: (value: number, from: string, to: string) => number | null;
24
+ /**
25
+ * Epoch ms of THIS evaluation. `evaluateExpressionSource` sets it to the same
26
+ * `now` the scope carries. A windowed builtin with no clock fails closed:
27
+ * reading the wall clock here would make an evaluation unrepeatable.
28
+ */
29
+ readonly now?: number;
30
+ /**
31
+ * Called by a windowed builtin with the instant its answer will change ON ITS
32
+ * OWN, with no source event. The composer arms ONE timer at the earliest such
33
+ * instant. That is a deadline, never a poll (D659).
34
+ */
35
+ readonly noteDeadline?: (at: number) => void;
8
36
  }
9
37
  export interface ExpressionBuiltin {
10
38
  readonly minArgs: number;