@bespokeagentics/microdots-host 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/wire.ts ADDED
@@ -0,0 +1,560 @@
1
+ import { Array, Option, Schema as S } from 'effect'
2
+
3
+ import type {
4
+ ManifestAttribute,
5
+ ManifestEvent,
6
+ ManifestTag,
7
+ } from '@bespokeagentics/microdots-element'
8
+
9
+ import { type RouteTable, makeRouteTable } from './routes.ts'
10
+ // Runtime import of the matcher only — `rules.ts` imports this module
11
+ // type-only, so the emitted module graph stays acyclic.
12
+ import { ruleMatches } from './rules.ts'
13
+ import { HostSlotManifest } from './slots.ts'
14
+
15
+ /**
16
+ * The wire RECORD — the declarative half of the broker.
17
+ *
18
+ * A wire says "when this element's event fires, write this field onto that
19
+ * element's attribute", which is exactly what the hand-written listener blocks
20
+ * in `apps/host/src/entry.ts` have been saying in prose. Phase 4 turns the
21
+ * prose into data this schema decodes, so the same record can drive the DOM
22
+ * adapter in `./wireEngine`, the Wiring screen's read mode, and any future
23
+ * transport — a record describes WHAT connects to WHAT, never how the message
24
+ * arrives. See `wiki/framework/composition/wire-execution-and-transforms.md`.
25
+ *
26
+ * Deliberately NOT here: `status` and `lastFired`. A stored status is stale the
27
+ * moment a placement or a manifest changes without the wire being touched;
28
+ * `deriveWireState` below computes it fresh from the manifests and the route
29
+ * table every time it is asked. Canvas coordinates are presentation, not
30
+ * record, and live with the Wiring dot.
31
+ */
32
+
33
+ /** The environments a wire may run in — the fixture's vocabulary, verbatim. */
34
+ export const WireEnv = S.Literals(['dev', 'preview', 'prod'])
35
+ export type WireEnv = typeof WireEnv.Type
36
+
37
+ /**
38
+ * What a wire claims a value's string encoding MEANS — the same vocabulary as
39
+ * the manifest's `AttributeType`, because the type-match check in
40
+ * `deriveWireState` compares a wire's declaration against a manifest's.
41
+ */
42
+ export const WireValueType = S.Literals([
43
+ 'string',
44
+ 'number',
45
+ 'boolean',
46
+ 'json',
47
+ 'enum',
48
+ ])
49
+ export type WireValueType = typeof WireValueType.Type
50
+
51
+ /**
52
+ * The three transforms, each a generalisation of code that already ran by
53
+ * hand in `apps/host/src/entry.ts`:
54
+ *
55
+ * - `direct` — straight across (wire 5's `id` → `refresh-token`)
56
+ * - `lookup` — map source values to target values with a fallback
57
+ * (wire 4's `REGION_BY_SYMBOL` + `DEFAULT_REGION`)
58
+ * - `condition` — fire only when a payload field `is` a value
59
+ * (wire 4's `if (linkToggle.checked)`)
60
+ *
61
+ * `op` is only ever `'is'` because the fixture shows only `'is'` — the
62
+ * vocabulary is a record of what exists, not an invitation to invent.
63
+ */
64
+ export const WireTransform = S.Union([
65
+ S.Struct({ _tag: S.tag('direct') }),
66
+ S.Struct({
67
+ _tag: S.tag('lookup'),
68
+ rows: S.Record(S.String, S.String),
69
+ fallback: S.String,
70
+ }),
71
+ S.Struct({
72
+ _tag: S.tag('condition'),
73
+ field: S.String,
74
+ op: S.Literal('is'),
75
+ value: S.String,
76
+ }),
77
+ ])
78
+ export type WireTransform = typeof WireTransform.Type
79
+
80
+ /**
81
+ * One wire, field-for-field with the prototype fixture's Wire entity minus the
82
+ * two derived fields (`status`, `lastFired`). `from` and `to` are element TAG
83
+ * names — the fixture used short dot ids; real records use real tags.
84
+ * `plain` carries the Plain-English toggle's sentence.
85
+ */
86
+ export const Wire = S.Struct({
87
+ id: S.String,
88
+ from: S.String,
89
+ event: S.String,
90
+ field: S.String,
91
+ fieldType: WireValueType,
92
+ to: S.String,
93
+ input: S.String,
94
+ inputType: WireValueType,
95
+ transform: WireTransform,
96
+ envs: S.Array(WireEnv),
97
+ plain: S.String,
98
+ })
99
+ export type Wire = typeof Wire.Type
100
+
101
+ /* ============================================================
102
+ HostTopology — the per-host JSON record (`host-topology.json`) that both a
103
+ host's entry and the Wiring service decode. The route structs are shaped
104
+ exactly like `./routes`' `Placement`/`RouteDefinition`, so a decoded
105
+ topology feeds `makeRouteTable` with no assertion in between.
106
+ ============================================================ */
107
+
108
+ /**
109
+ * A placement condition: WHO sees this placement, on what DEVICE, in what
110
+ * LOCALE. Three independent axes; an absent axis means `'any'` — the record
111
+ * stores only what narrows, and the resolution's `resolveCondition` makes the
112
+ * default explicit when it is asked (derive-fresh, never store). The literal
113
+ * string `'any'` is also admitted and means the same as absence, per the
114
+ * fixture's vocabulary. Two placements collide when ALL three axes overlap;
115
+ * see `./rules.ts` for the overlap rule.
116
+ */
117
+ export const PlacementCondition = S.Struct({
118
+ who: S.optionalKey(S.String),
119
+ device: S.optionalKey(S.String),
120
+ locale: S.optionalKey(S.String),
121
+ })
122
+ export type PlacementCondition = typeof PlacementCondition.Type
123
+
124
+ /** The spans a grid placement may take — 12/8/6/4/3 of twelve columns, the
125
+ * spec's exact set. Meaningful in `grid` slots only. */
126
+ export const PlacementSpan = S.Literals([12, 8, 6, 4, 3])
127
+ export type PlacementSpan = typeof PlacementSpan.Type
128
+
129
+ /**
130
+ * Where one MicroDot element goes — widened in Phase 5 from `{tag, slotId}`
131
+ * only as far as a shipping check or the reading-B resolution needs, per the
132
+ * phase design's table (work item 2). Every new field is `S.optionalKey`,
133
+ * because BOTH existing `host-topology.json` files must decode unchanged —
134
+ * backward compatibility is a hard requirement, pinned by the Phase-4-shaped
135
+ * decode test in `./wire.test.ts`.
136
+ *
137
+ * - `id` — override provenance and screen selection. Absent on the
138
+ * Phase-4 records; the resolution synthesizes a stable one.
139
+ * - `values` — attribute values this placement supplies (check 3). Absent
140
+ * means `{}`.
141
+ * - `condition` — who/device/locale (checks 4/7, the resolution). Absent
142
+ * means every axis `'any'`.
143
+ * - `envs` — the environments the placement exists in (check 1's
144
+ * vocabulary, the same `WireEnv` wires use). Absent means all
145
+ * three.
146
+ * - `span` — grid columns (grid slots only).
147
+ * - `order` — stacking order within the slot.
148
+ *
149
+ * Deliberately NOT here, with their checks: `'@anchor'` + `selector` +
150
+ * `resolved` (check 5 needs a crawl) and `loading` (no check needs it and the
151
+ * runtime has one strategy — a stored `loading` the loader ignores would be a
152
+ * lie).
153
+ */
154
+ export const TopologyPlacement = S.Struct({
155
+ id: S.optionalKey(S.String),
156
+ tag: S.String,
157
+ slotId: S.String,
158
+ values: S.optionalKey(S.Record(S.String, S.String)),
159
+ condition: S.optionalKey(PlacementCondition),
160
+ envs: S.optionalKey(S.Array(WireEnv)),
161
+ span: S.optionalKey(PlacementSpan),
162
+ order: S.optionalKey(S.Number),
163
+ })
164
+ export type TopologyPlacement = typeof TopologyPlacement.Type
165
+
166
+ export const TopologyRoute = S.Struct({
167
+ path: S.String,
168
+ label: S.String,
169
+ title: S.String,
170
+ sectionIds: S.Array(S.String),
171
+ mounts: S.Array(TopologyPlacement),
172
+ })
173
+ export type TopologyRoute = typeof TopologyRoute.Type
174
+
175
+ /** The fields every rule kind shares. Spread rather than nested so `kind`
176
+ * stays a flat discriminant, matching the fixture's records. */
177
+ const routeRuleFields = {
178
+ id: S.String,
179
+ label: S.String,
180
+ placements: S.Array(TopologyPlacement),
181
+ }
182
+
183
+ /**
184
+ * A ROUTE RULE places dots on many routes at once. Three kinds, the spec's
185
+ * exact vocabulary, each with its own match data:
186
+ *
187
+ * - `pattern` — a path glob (`/*`, `/blog/*`); only the `/*` suffix wildcard
188
+ * exists.
189
+ * - `group` — an explicit path list (the spec's "named collection").
190
+ * - `dynamic` — a `:param` template (`/docs/:slug`), matched segment-wise.
191
+ *
192
+ * The record is SCHEMA here beside the other topology records (a rule is
193
+ * topology data); the matcher and the resolution that consume it live in
194
+ * `./rules.ts` — defining the schema there instead would put `wire.ts` and
195
+ * `rules.ts` in a runtime import cycle, since `HostTopology` below needs this
196
+ * const at module evaluation.
197
+ *
198
+ * `ruleMatches` in `./rules.ts` is THE matcher (the phase's ruling 1): the
199
+ * runtime resolution and the Pages screen's match counts both call it, and
200
+ * nothing else may reimplement it.
201
+ */
202
+ export const RouteRule = S.Union([
203
+ S.Struct({
204
+ ...routeRuleFields,
205
+ kind: S.Literal('pattern'),
206
+ pattern: S.String,
207
+ }),
208
+ S.Struct({
209
+ ...routeRuleFields,
210
+ kind: S.Literal('group'),
211
+ paths: S.Array(S.String),
212
+ }),
213
+ S.Struct({
214
+ ...routeRuleFields,
215
+ kind: S.Literal('dynamic'),
216
+ template: S.String,
217
+ }),
218
+ ])
219
+ export type RouteRule = typeof RouteRule.Type
220
+
221
+ /**
222
+ * The descriptor `makeRouteTable` derives an aggregate landing route from.
223
+ * Stored as a DESCRIPTOR rather than as the derived route itself: a stored
224
+ * copy of the union of every component route rots the day a route changes.
225
+ * `optionalKey` (not `optional`) so the decoded Type is exact-optional and
226
+ * assignable to `makeRouteTable`'s overview parameter under
227
+ * `exactOptionalPropertyTypes`.
228
+ */
229
+ export const TopologyOverview = S.Struct({
230
+ path: S.String,
231
+ label: S.String,
232
+ title: S.String,
233
+ leadingSectionIds: S.optionalKey(S.Array(S.String)),
234
+ })
235
+ export type TopologyOverview = typeof TopologyOverview.Type
236
+
237
+ /**
238
+ * Inputs the HOST itself owns — the activity log, remembered interview ids.
239
+ * They render distinctly on the Wiring canvas because no manifest declares
240
+ * them; `type` is free-form (`'append'` in the prototype) rather than a
241
+ * `WireValueType`, for the same reason.
242
+ */
243
+ export const TopologyHost = S.Struct({
244
+ id: S.String,
245
+ label: S.String,
246
+ ownedInputs: S.Array(S.Struct({ name: S.String, type: S.String })),
247
+ })
248
+ export type TopologyHost = typeof TopologyHost.Type
249
+
250
+ /**
251
+ * A whole host, as data: who it is, where its elements go, what is wired to
252
+ * what, and which events it merely watches (log-only taps — the demo host's
253
+ * wire-7 events). `routes` is non-empty by schema so `topologyRouteTable`
254
+ * below always has a fallback without an assertion.
255
+ *
256
+ * Phase 5 adds two keys, both `optionalKey` because a Phase-4 topology file
257
+ * must decode unchanged:
258
+ *
259
+ * - `slotManifest` — the theme's declared slots (see `./slots.ts`). Absent
260
+ * means the topology predates the manifest; slot checks degrade honestly.
261
+ * - `rules` — route rules resolved by `./rules.ts`. Absent means no rules,
262
+ * and `resolvePlacements` degrades to the identity: exactly the route's own
263
+ * mounts, byte-identical to Phase 4 behaviour.
264
+ */
265
+ export const HostTopology = S.Struct({
266
+ host: TopologyHost,
267
+ routes: S.NonEmptyArray(TopologyRoute),
268
+ overview: S.optionalKey(TopologyOverview),
269
+ slotManifest: S.optionalKey(HostSlotManifest),
270
+ rules: S.optionalKey(S.Array(RouteRule)),
271
+ wires: S.Array(Wire),
272
+ watch: S.Array(S.Struct({ event: S.String })),
273
+ })
274
+ export type HostTopology = typeof HostTopology.Type
275
+
276
+ /**
277
+ * The ONE way a route table is built from a topology — both the demo host's
278
+ * entry and the Wiring dot's client-side derivation go through here, which is
279
+ * what keeps a wire whose two ends only share the DERIVED overview route
280
+ * deriving `live` identically everywhere.
281
+ *
282
+ * With an overview descriptor the fallback is derived by `makeRouteTable`;
283
+ * without one the first route is the fallback (`routes` is schema-guaranteed
284
+ * non-empty, and index 0 of a non-empty tuple type needs no `Option`).
285
+ *
286
+ * Phase 5: RULE-CARRIED placements fold into each matching route's mounts,
287
+ * through `ruleMatches` — THE matcher, never a reimplementation. Every
288
+ * table-derived join reads `route.mounts`: `unroutedTags`' startup throw,
289
+ * `findPlacement`, and `deriveWireState`'s mounts-both-ends check. Without
290
+ * the fold, a wire whose two ends ride a group rule would derive
291
+ * `never-fires` on the Wiring screen while the runtime mounts both ends —
292
+ * the screen lying about the runtime, the exact failure ruling 1 of the
293
+ * phase design exists to prevent. Rule placements come first (broadest
294
+ * layer, the resolution's own ordering); the table stays env-agnostic, so
295
+ * the fold is the plain union — the env- and condition-aware contest is
296
+ * `resolvePlacements`' job. A topology with no rules keeps the exact route
297
+ * objects it decoded: byte-identical to Phase 4.
298
+ */
299
+ export const topologyRouteTable = (topology: HostTopology): RouteTable => {
300
+ const rules = topology.rules ?? []
301
+ const widen = (route: TopologyRoute): TopologyRoute =>
302
+ rules.length === 0
303
+ ? route
304
+ : {
305
+ ...route,
306
+ mounts: [
307
+ ...rules
308
+ .filter(rule => ruleMatches(rule, route.path))
309
+ .flatMap(rule => rule.placements),
310
+ ...route.mounts,
311
+ ],
312
+ }
313
+ return topology.overview === undefined
314
+ ? {
315
+ routes: topology.routes.map(widen),
316
+ fallback: widen(topology.routes[0]),
317
+ }
318
+ : makeRouteTable(topology.routes.map(widen), topology.overview)
319
+ }
320
+
321
+ /* ============================================================
322
+ Derived wire state — computed, never stored.
323
+ ============================================================ */
324
+
325
+ /**
326
+ * - `live` — verified against manifest data, types reconciled, and some
327
+ * route mounts both ends.
328
+ * - `never-fires` — well-formed, but no single route mounts both ends, so the
329
+ * event and its target are never on screen together.
330
+ * - `draft` — a referenced tag/event/field/input is missing from the
331
+ * manifest data, the manifest for a referenced tag is absent
332
+ * (unverifiable ⇒ draft), or the type pairing is not
333
+ * reconciled by the transform.
334
+ */
335
+ export type WireState = 'live' | 'never-fires' | 'draft'
336
+
337
+ /**
338
+ * The slice of an emitted JSON Schema document this module reads: the
339
+ * top-level object schema's property map. Everything else in the document
340
+ * (`dialect`, `required`, `definitions`, …) is irrelevant to "does this field
341
+ * exist, and what primitive does it carry".
342
+ */
343
+ const JsonSchemaDocument = S.Struct({
344
+ schema: S.Struct({
345
+ properties: S.optionalKey(S.Record(S.String, S.Unknown)),
346
+ }),
347
+ })
348
+ const decodeJsonSchemaDocument = S.decodeUnknownOption(JsonSchemaDocument)
349
+
350
+ const JsonSchemaDirectType = S.Struct({ type: S.String })
351
+ const decodeJsonSchemaDirectType = S.decodeUnknownOption(JsonSchemaDirectType)
352
+
353
+ const JsonSchemaAnyOf = S.Struct({
354
+ anyOf: S.Array(
355
+ S.Struct({
356
+ type: S.optionalKey(S.String),
357
+ enum: S.optionalKey(S.Unknown),
358
+ }),
359
+ ),
360
+ })
361
+ const decodeJsonSchemaAnyOf = S.decodeUnknownOption(JsonSchemaAnyOf)
362
+
363
+ /**
364
+ * The JSON Schema `type`s a property may carry, best effort. The emitter
365
+ * renders `S.Number` as an `anyOf` whose EXTRA branches are literal string
366
+ * encodings — `{type: 'string', enum: ['NaN']}`, `'Infinity'`, `'-Infinity'`.
367
+ * Counting those branch types would let `'string'` corroborate every numeric
368
+ * field, deriving a mistyped wire `live` when it can never fire (the exact
369
+ * "draft shown as live ⇒ silently dead broker" failure), so a branch carrying
370
+ * an `enum` key is an encoding artifact and its `type` does not count. An
371
+ * empty result means "could not tell" — the caller trusts the wire's
372
+ * declaration rather than failing a wire the tooling cannot see into.
373
+ */
374
+ const jsonSchemaTypesOf = (property: unknown): ReadonlyArray<string> =>
375
+ Option.match(decodeJsonSchemaDirectType(property), {
376
+ onSome: direct => [direct.type],
377
+ onNone: () =>
378
+ Option.match(decodeJsonSchemaAnyOf(property), {
379
+ onSome: union =>
380
+ Array.getSomes(
381
+ union.anyOf.map(branch =>
382
+ branch.enum === undefined
383
+ ? Option.fromNullishOr(branch.type)
384
+ : Option.none(),
385
+ ),
386
+ ),
387
+ onNone: () => [],
388
+ }),
389
+ })
390
+
391
+ /** Which JSON Schema `type`s corroborate each declared wire value type. */
392
+ const JSON_TYPES_BY_VALUE_TYPE: Record<WireValueType, ReadonlyArray<string>> = {
393
+ string: ['string'],
394
+ enum: ['string'],
395
+ number: ['number', 'integer'],
396
+ boolean: ['boolean'],
397
+ // `json` encodes anything, so no JSON Schema type can contradict it.
398
+ json: [],
399
+ }
400
+
401
+ /**
402
+ * Verifies a payload field against the manifest event's `jsonSchema`.
403
+ *
404
+ * `jsonSchema` is OPTIONAL in manifests — the emitter omits it when a payload
405
+ * schema cannot be rendered. When it is absent (or carries no property map),
406
+ * there is nothing to verify against, so the wire's DECLARED `fieldType` is
407
+ * trusted rather than the wire being condemned to draft: an unverifiable
408
+ * declaration on the SOURCE side is the manifest's gap, not the wire's. A
409
+ * missing manifest or event, by contrast, means the referenced thing may not
410
+ * exist at all — that is draft, handled by the caller.
411
+ */
412
+ const fieldChecksOut = (
413
+ event: ManifestEvent,
414
+ field: string,
415
+ declared: Option.Option<WireValueType>,
416
+ ): boolean =>
417
+ Option.match(decodeJsonSchemaDocument(event.payload.jsonSchema), {
418
+ onNone: () => true,
419
+ onSome: document => {
420
+ const properties = document.schema.properties
421
+ if (properties === undefined) {
422
+ return true
423
+ }
424
+ if (!Object.hasOwn(properties, field)) {
425
+ return false
426
+ }
427
+ return Option.match(declared, {
428
+ onNone: () => true,
429
+ onSome: valueType => {
430
+ const jsonTypes = jsonSchemaTypesOf(properties[field])
431
+ const corroborating = JSON_TYPES_BY_VALUE_TYPE[valueType]
432
+ if (jsonTypes.length === 0 || corroborating.length === 0) {
433
+ return true
434
+ }
435
+ return Array.some(corroborating, jsonType =>
436
+ Array.contains(jsonTypes, jsonType),
437
+ )
438
+ },
439
+ })
440
+ },
441
+ })
442
+
443
+ const manifestFor = (
444
+ manifests: ReadonlyArray<ManifestTag>,
445
+ tag: string,
446
+ ): Option.Option<ManifestTag> =>
447
+ Array.findFirst(manifests, manifest => manifest.tag === tag)
448
+
449
+ const eventFor = (
450
+ manifest: ManifestTag,
451
+ name: string,
452
+ ): Option.Option<ManifestEvent> =>
453
+ Array.findFirst(manifest.events, event => event.name === name)
454
+
455
+ const attributeFor = (
456
+ manifest: ManifestTag,
457
+ name: string,
458
+ ): Option.Option<ManifestAttribute> =>
459
+ Array.findFirst(manifest.attributes, attribute => attribute.name === name)
460
+
461
+ /**
462
+ * Whether the transform reconciles the declared type pairing:
463
+ *
464
+ * - `direct` and `condition` copy the field straight across, so
465
+ * `fieldType === inputType`.
466
+ * - `lookup` re-maps the value entirely: valid into a `string` input
467
+ * regardless of the source type, or into an `enum` input iff every row
468
+ * value AND the fallback are members of the target attribute's declared
469
+ * values (an undeclared value set cannot be verified ⇒ not reconciled).
470
+ */
471
+ const transformReconciles = (
472
+ wire: Wire,
473
+ target: ManifestAttribute,
474
+ ): boolean => {
475
+ switch (wire.transform._tag) {
476
+ case 'direct':
477
+ case 'condition':
478
+ return wire.fieldType === wire.inputType
479
+ case 'lookup': {
480
+ if (wire.inputType === 'string') {
481
+ return true
482
+ }
483
+ if (wire.inputType !== 'enum') {
484
+ return false
485
+ }
486
+ const declaredValues = target.values
487
+ if (declaredValues === undefined) {
488
+ return false
489
+ }
490
+ const outputs = [
491
+ ...Object.values(wire.transform.rows),
492
+ wire.transform.fallback,
493
+ ]
494
+ return Array.every(outputs, value =>
495
+ Array.contains(declaredValues, value),
496
+ )
497
+ }
498
+ }
499
+ }
500
+
501
+ const routeMountsBothEnds = (table: RouteTable, wire: Wire): boolean =>
502
+ Array.some(
503
+ table.routes,
504
+ route =>
505
+ Array.some(route.mounts, mount => mount.tag === wire.from) &&
506
+ Array.some(route.mounts, mount => mount.tag === wire.to),
507
+ )
508
+
509
+ /**
510
+ * Derives a wire's state fresh from the manifests and the route table —
511
+ * see `WireState` for what each state means and why none of this is stored.
512
+ *
513
+ * Draft on ANY unverifiable reference, including a wire whose declared
514
+ * `inputType` disagrees with the target attribute's manifest type: a stale
515
+ * declaration is indistinguishable from a wrong one, and the whole point of
516
+ * draft is "this record cannot be trusted to fire correctly".
517
+ */
518
+ export const deriveWireState = (
519
+ wire: Wire,
520
+ context: {
521
+ readonly manifests: ReadonlyArray<ManifestTag>
522
+ readonly table: RouteTable
523
+ },
524
+ ): WireState => {
525
+ const verified = Option.match(manifestFor(context.manifests, wire.from), {
526
+ onNone: () => false,
527
+ onSome: fromManifest =>
528
+ Option.match(eventFor(fromManifest, wire.event), {
529
+ onNone: () => false,
530
+ onSome: event => {
531
+ if (!fieldChecksOut(event, wire.field, Option.some(wire.fieldType))) {
532
+ return false
533
+ }
534
+ // A condition's gate field is a payload reference too — existence
535
+ // only, because the wire declares no type for it.
536
+ if (
537
+ wire.transform._tag === 'condition' &&
538
+ !fieldChecksOut(event, wire.transform.field, Option.none())
539
+ ) {
540
+ return false
541
+ }
542
+ return Option.match(manifestFor(context.manifests, wire.to), {
543
+ onNone: () => false,
544
+ onSome: toManifest =>
545
+ Option.match(attributeFor(toManifest, wire.input), {
546
+ onNone: () => false,
547
+ onSome: target =>
548
+ target.type === wire.inputType &&
549
+ transformReconciles(wire, target),
550
+ }),
551
+ })
552
+ },
553
+ }),
554
+ })
555
+
556
+ if (!verified) {
557
+ return 'draft'
558
+ }
559
+ return routeMountsBothEnds(context.table, wire) ? 'live' : 'never-fires'
560
+ }