@bespokeagentics/microdots-host 0.1.2 → 0.2.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 CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  ManifestTag,
7
7
  } from '@bespokeagentics/microdots-element'
8
8
 
9
+ import { TopologyRegistryEntry } from './registry.ts'
9
10
  import { type RouteTable, makeRouteTable } from './routes.ts'
10
11
  // Runtime import of the matcher only — `rules.ts` imports this module
11
12
  // type-only, so the emitted module graph stays acyclic.
@@ -126,11 +127,21 @@ export type PlacementCondition = typeof PlacementCondition.Type
126
127
  export const PlacementSpan = S.Literals([12, 8, 6, 4, 3])
127
128
  export type PlacementSpan = typeof PlacementSpan.Type
128
129
 
130
+ /**
131
+ * The sentinel `slotId` of a placement pinned to a CSS selector in host-owned
132
+ * DOM instead of a declared slot. An anchor placement carries `selector`; the
133
+ * refinement below makes a half-anchor (either half without the other)
134
+ * unrepresentable, so a hand-edited file fails decode loudly and a mutator's
135
+ * `redecode()` refuses it as `undecodable` — never a silent half-record.
136
+ */
137
+ export const ANCHOR_SLOT_ID = '@anchor'
138
+
129
139
  /**
130
140
  * Where one MicroDot element goes — widened in Phase 5 from `{tag, slotId}`
131
141
  * 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
142
+ * phase design's table (work item 2), and again by the Pages write-mode plan
143
+ * (anchors: `'@anchor'` + `selector`). Every new field is `S.optionalKey`,
144
+ * because the existing `host-topology.json` files must decode unchanged —
134
145
  * backward compatibility is a hard requirement, pinned by the Phase-4-shaped
135
146
  * decode test in `./wire.test.ts`.
136
147
  *
@@ -145,11 +156,14 @@ export type PlacementSpan = typeof PlacementSpan.Type
145
156
  * three.
146
157
  * - `span` — grid columns (grid slots only).
147
158
  * - `order` — stacking order within the slot.
159
+ * - `selector` — the CSS selector an anchor placement pins to. Present
160
+ * exactly when `slotId === ANCHOR_SLOT_ID`.
148
161
  *
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).
162
+ * Deliberately NOT here, with their reasons: `resolved`/`resolvedAt` (check 5
163
+ * needs a crawl that does not exist an anchor's resolution is reported
164
+ * UNVERIFIABLE, never stored as a guess) and `loading` (no check needs it and
165
+ * the runtime has one strategy — a stored `loading` the loader ignores would
166
+ * be a lie).
153
167
  */
154
168
  export const TopologyPlacement = S.Struct({
155
169
  id: S.optionalKey(S.String),
@@ -160,7 +174,17 @@ export const TopologyPlacement = S.Struct({
160
174
  envs: S.optionalKey(S.Array(WireEnv)),
161
175
  span: S.optionalKey(PlacementSpan),
162
176
  order: S.optionalKey(S.Number),
163
- })
177
+ selector: S.optionalKey(S.String),
178
+ }).pipe(
179
+ S.check(
180
+ S.makeFilter(
181
+ placement =>
182
+ (placement.slotId === ANCHOR_SLOT_ID) === ('selector' in placement) ||
183
+ `an anchor placement carries both slotId "${ANCHOR_SLOT_ID}" and a selector — never one half`,
184
+ { expected: 'selector present exactly when slotId is "@anchor"' },
185
+ ),
186
+ ),
187
+ )
164
188
  export type TopologyPlacement = typeof TopologyPlacement.Type
165
189
 
166
190
  export const TopologyRoute = S.Struct({
@@ -244,9 +268,27 @@ export const TopologyHost = S.Struct({
244
268
  id: S.String,
245
269
  label: S.String,
246
270
  ownedInputs: S.Array(S.Struct({ name: S.String, type: S.String })),
271
+ /**
272
+ * Who owns this host's layout DOM. Absent means `authored` — hand-written
273
+ * markup wins and `slotDom.ts` only backfills missing containers. Only an
274
+ * explicitly `generated` host renders its slot manifest as its page
275
+ * structure (`renderLayout.ts`) and accepts layout writes from Pages. The
276
+ * marker is HOST-level, not manifest-level, because the gate must be
277
+ * readable when NO manifest exists: `generateLayout` targets exactly that
278
+ * state, and `removeLayout` returns to it (plan ruling D4,
279
+ * `wiki/plans/active/microdots-platform-pages-generated-layout.md`).
280
+ */
281
+ layout: S.optionalKey(S.Literals(['authored', 'generated'])),
247
282
  })
248
283
  export type TopologyHost = typeof TopologyHost.Type
249
284
 
285
+ /** The layout-ownership seam: absent means `authored`. Every gate — the
286
+ * proof host's renderer, the Pages service's layout mutators — reads the
287
+ * mode through here, never `topology.host.layout` directly. */
288
+ export const layoutModeOf = (topology: {
289
+ readonly host: TopologyHost
290
+ }): 'authored' | 'generated' => topology.host.layout ?? 'authored'
291
+
250
292
  /**
251
293
  * A whole host, as data: who it is, where its elements go, what is wired to
252
294
  * what, and which events it merely watches (log-only taps — the demo host's
@@ -261,6 +303,16 @@ export type TopologyHost = typeof TopologyHost.Type
261
303
  * - `rules` — route rules resolved by `./rules.ts`. Absent means no rules,
262
304
  * and `resolvePlacements` degrades to the identity: exactly the route's own
263
305
  * mounts, byte-identical to Phase 4 behaviour.
306
+ *
307
+ * 2026-09-03 adds a third, on the same terms:
308
+ *
309
+ * - `registry` — where to FETCH the bundle for a tag (`./registry.ts`).
310
+ * Absent means the host mounts from its compiled registry alone, exactly as
311
+ * before. Present, it wins per tag, which is what lets a placement for a dot
312
+ * the host was never built with actually mount: until this key existed, a
313
+ * fetched placement naming an unregistered tag reached `findEntry`, got
314
+ * `undefined`, and threw — so placing a MicroDot on a DEPLOYED host meant a
315
+ * human editing `registry.ts` and shipping a host build.
264
316
  */
265
317
  export const HostTopology = S.Struct({
266
318
  host: TopologyHost,
@@ -268,6 +320,7 @@ export const HostTopology = S.Struct({
268
320
  overview: S.optionalKey(TopologyOverview),
269
321
  slotManifest: S.optionalKey(HostSlotManifest),
270
322
  rules: S.optionalKey(S.Array(RouteRule)),
323
+ registry: S.optionalKey(S.Array(TopologyRegistryEntry)),
271
324
  wires: S.Array(Wire),
272
325
  watch: S.Array(S.Struct({ event: S.String })),
273
326
  })
@@ -614,7 +614,7 @@ describe('attachWireEngine', () => {
614
614
  * click sent paths from host A into panes rendering host B. All four wires
615
615
  * derive `live`, so the Wiring screen showed nothing wrong.
616
616
  */
617
- test('two dots emitting the same event name drive only the wire whose `from` matches', () => {
617
+ test('two MicroDots emitting the same event name drive only the wire whose `from` matches', () => {
618
618
  const table = mount('wiring-table')
619
619
  const canvas = mount('pages-canvas')
620
620
  const inspector = mount('pages-inspector')