@bespokeagentics/microdots-host 0.1.0 → 0.1.1

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/index.ts CHANGED
@@ -74,12 +74,102 @@ export {
74
74
  WireValueType,
75
75
  } from './wire.ts'
76
76
 
77
+ /**
78
+ * The composition IR a spec-filler emits and `compileComposition` consumes.
79
+ * Surface-derived JSON Schema is the smaller prompt: legal tags/events/attrs
80
+ * are unrepresentable to invent. See
81
+ * `wiki/plans/active/microdots-specialist-spec-filler.md`.
82
+ */
83
+ export {
84
+ type CompositionIssue,
85
+ type JsonSchema,
86
+ CompositionSpec,
87
+ CompositionSpecV1,
88
+ CompositionSpecV2,
89
+ CompositionSpecVersioned,
90
+ GenerateDotSpec,
91
+ GenerateDotSpecV1,
92
+ catalogLocksEqual,
93
+ compositionJsonSchema,
94
+ decodeCompositionSpec,
95
+ decodeCompositionSpecSync,
96
+ decodeCompositionSpecVersioned,
97
+ decodeCompositionSpecVersionedSync,
98
+ encodeCompositionSpecV2Sync,
99
+ encodeTopologyJson,
100
+ migrateCompositionSpecV1,
101
+ validateComposition,
102
+ validateExistingSurfaceComposition,
103
+ } from './compositionSpec.ts'
104
+
105
+ /**
106
+ * Constrained-decode a CompositionSpec from a brief. The JSON Schema is the
107
+ * prompt; `complete` is the model seam. See
108
+ * `wiki/plans/active/microdots-specialist-spec-filler.md`.
109
+ */
110
+ export {
111
+ type CompleteCompositionJson,
112
+ type FillCompositionFailure,
113
+ type FillCompositionOptions,
114
+ FillCompositionError,
115
+ compositionPrompt,
116
+ compositionVocabulary,
117
+ extractCompositionJson,
118
+ fillComposition,
119
+ liveFillPrompt,
120
+ } from './fillComposition.ts'
121
+
122
+ /**
123
+ * Catalog-picker → CompositionSpec. `generate[]` is always empty; unknown
124
+ * tags cannot be represented. See
125
+ * `wiki/plans/active/microdots-specialist-spec-filler.md`.
126
+ */
127
+ export {
128
+ type CompositionLayout,
129
+ type CompositionWirePick,
130
+ type ComposeFromSelectionInput,
131
+ composeExistingSurfacesFromSelection,
132
+ composeFromSelection,
133
+ eventFieldsOf,
134
+ wireableAttributesOf,
135
+ } from './composeFromSelection.ts'
136
+
137
+ export {
138
+ TopologyRegressionCaseV1,
139
+ TopologyRegressionCorpusV1,
140
+ TopologyRegressionObservationV1,
141
+ decodeTopologyRegressionCorpusSync,
142
+ observeTopologyRegressionCase,
143
+ topologyRegressionSurfaces,
144
+ type TopologyRegressionCaseV1 as TopologyRegressionCase,
145
+ type TopologyRegressionCorpusV1 as TopologyRegressionCorpus,
146
+ type TopologyRegressionObservationV1 as TopologyRegressionObservation,
147
+ } from './topologyRegression.ts'
148
+
77
149
  /**
78
150
  * The host slot manifest — Phase 5 work item 1: a host declaring, as data and
79
151
  * versioned by theme, where MicroDots may go. Lives inside
80
152
  * `host-topology.json` as `HostTopology.slotManifest`.
81
153
  */
82
- export { HostSlotManifest, SlotKind, SlotSpec } from './slots.ts'
154
+ export {
155
+ HostSlotManifest,
156
+ SlotKind,
157
+ SlotLayoutPreset,
158
+ SlotResizeAxis,
159
+ SlotSpec,
160
+ } from './slots.ts'
161
+
162
+ /**
163
+ * Acquiring a topology at RUNTIME rather than importing it at build — Phase A
164
+ * of `wiki/plans/active/wiring-live-placement-on-a-remote-host.md`. The decode
165
+ * is unchanged; only WHEN it runs moves, which is why the failure had to become
166
+ * a typed error a shell can render rather than a throw at import.
167
+ */
168
+ export {
169
+ type TopologyFailureReason,
170
+ TopologyUnavailable,
171
+ acquireTopology,
172
+ } from './topologySource.ts'
83
173
 
84
174
  /**
85
175
  * Rules, THE matcher, and the reading-B resolution — Phase 5 work item 3.
@@ -107,7 +197,42 @@ export {
107
197
  * tag as the pending list is built, never after) and `mountOverridesFor`
108
198
  * (resume overrides dropped when empty BEFORE the merge).
109
199
  */
110
- export { mountOverridesFor, pendingMounts } from './mounting.ts'
200
+ export {
201
+ type MountedRegistry,
202
+ makeMountedRegistry,
203
+ mountOverridesFor,
204
+ pendingMounts,
205
+ } from './mounting.ts'
206
+
207
+ /**
208
+ * Containers for placements the build never knew about — Phase B of
209
+ * `wiki/plans/active/wiring-live-placement-on-a-remote-host.md`. Markup wins;
210
+ * these are the fallback for a slot a FETCHED topology added after the page was
211
+ * built.
212
+ */
213
+ export {
214
+ ensureSection,
215
+ ensureSlot,
216
+ pruneGeneratedContainers,
217
+ } from './slotDom.ts'
218
+
219
+ /**
220
+ * Drag-resize for slots that declare a `resize` axis — the remaining half of
221
+ * the slot-manifest generalisation in
222
+ * `wiki/plans/active/wiring-visual-parity-with-the-prototype.md`. Handles are
223
+ * host chrome; sizes are CSS variables on the section, never a topology write.
224
+ */
225
+ export {
226
+ attachSlotResize,
227
+ chromeOfSlot,
228
+ clearSlotResize,
229
+ applySlotSize,
230
+ restoreSlotResize,
231
+ SLOT_RESIZE_HANDLE_CLASS,
232
+ type SlotResizeStorage,
233
+ slotResizeStorageKey,
234
+ slotSizeVar,
235
+ } from './slotResize.ts'
111
236
 
112
237
  /**
113
238
  * The placement checks engine — Phase 5 work item 4: six of the spec's seven
@@ -138,7 +263,20 @@ export {
138
263
  type WireSkipReason,
139
264
  type WireTrace,
140
265
  type WireWrite,
266
+ WIRE_TRACE_EVENT,
141
267
  applyTransform,
142
268
  attachWireEngine,
143
269
  planWires,
144
270
  } from './wireEngine.ts'
271
+
272
+ /**
273
+ * Host-document POST of engine traces — the seam Watch/Trace poll when the
274
+ * selected host is not this page. Empty url/key is a no-op (fail closed).
275
+ */
276
+ export {
277
+ type PostedWireTrace,
278
+ type TracePublisher,
279
+ type TracePublisherOptions,
280
+ postWireTraces,
281
+ startTracePublisher,
282
+ } from './tracePublisher.ts'
@@ -0,0 +1,196 @@
1
+ import { Option } from 'effect'
2
+ import { beforeEach, describe, expect, test } from 'vitest'
3
+
4
+ import { makeMountedRegistry, pendingMounts } from './mounting.ts'
5
+ import type { ResolvedPlacement } from './rules.ts'
6
+
7
+ /**
8
+ * The unmount half of `activate`, and specifically the window that did not
9
+ * exist before it.
10
+ *
11
+ * `mounting.ts` exists because `filter`-then-`forEach` double-mounted: the
12
+ * whole predicate pass ran before anything latched, so one tag resolved twice
13
+ * survived twice. Adding an unmount path creates the MIRROR of that defect — a
14
+ * tag released while its mount is still awaiting, whose element then appends
15
+ * after the removal — and a MicroDot that comes back after the user deleted it
16
+ * is worse than one that never left. Every test below either pins the old
17
+ * behaviour that must survive or pins that window shut.
18
+ */
19
+
20
+ /**
21
+ * A resolved placement, spelled out rather than produced by `resolvePlacements`
22
+ * — `pendingMounts` reads only `tag` and `state`, and a literal keeps this
23
+ * suite about the registry instead of about the resolution.
24
+ */
25
+ const placement = (tag: string): ResolvedPlacement => ({
26
+ id: `route:${tag}@${tag}-slot#0`,
27
+ tag,
28
+ slotId: `${tag}-slot`,
29
+ source: { _tag: 'route' },
30
+ condition: { who: 'any', device: 'any', locale: 'any' },
31
+ envs: ['dev', 'preview', 'prod'],
32
+ values: {},
33
+ span: Option.none(),
34
+ order: Option.none(),
35
+ state: 'active',
36
+ overriddenBy: [],
37
+ })
38
+
39
+ /**
40
+ * A mounted element, in a slot, IN THE DOCUMENT.
41
+ *
42
+ * `isConnected` is the assertion that matters — "was this actually removed from
43
+ * the page" — and it is false for any detached tree, so a helper that builds
44
+ * one off-document would make every removal assertion pass for the wrong
45
+ * reason.
46
+ */
47
+ const element = (): Element => {
48
+ const slot = document.createElement('div')
49
+ document.body.appendChild(slot)
50
+ const child = document.createElement('span')
51
+ slot.appendChild(child)
52
+ return child
53
+ }
54
+
55
+ beforeEach(() => {
56
+ document.body.replaceChildren()
57
+ })
58
+
59
+ describe('makeMountedRegistry', () => {
60
+ test('a latched tag is no longer pending — the claim survives the await', () => {
61
+ const mounted = makeMountedRegistry()
62
+ const resolved = [placement('a'), placement('b')]
63
+
64
+ expect(
65
+ pendingMounts(resolved, mounted.mountedTags()).map(p => p.tag),
66
+ ).toEqual(['a', 'b'])
67
+
68
+ mounted.latch('a')
69
+
70
+ expect(
71
+ pendingMounts(resolved, mounted.mountedTags()).map(p => p.tag),
72
+ ).toEqual(['b'])
73
+ })
74
+
75
+ test('retainOnly removes the element and reports what went', () => {
76
+ const mounted = makeMountedRegistry()
77
+ const a = element()
78
+ mounted.latch('a')
79
+ mounted.adopt('a', a, 'a-slot')
80
+
81
+ expect(a.isConnected).toBe(true)
82
+
83
+ const gone = mounted.retainOnly(new Map([['b', 'b-slot']]))
84
+
85
+ expect(gone).toEqual(['a'])
86
+ expect(a.isConnected).toBe(false)
87
+ expect(mounted.mountedTags().has('a')).toBe(false)
88
+ })
89
+
90
+ test('a tag still in `keep` is untouched — navigation must not unmount', () => {
91
+ const mounted = makeMountedRegistry()
92
+ const a = element()
93
+ mounted.latch('a')
94
+ mounted.adopt('a', a, 'a-slot')
95
+
96
+ expect(mounted.retainOnly(new Map([['a', 'a-slot']]))).toEqual([])
97
+ expect(a.isConnected).toBe(true)
98
+ })
99
+
100
+ test('THE MIRROR BUG: a tag released mid-mount does not append when its mount lands', () => {
101
+ const mounted = makeMountedRegistry()
102
+ mounted.latch('a')
103
+
104
+ // The topology changed while `mountMicroDot('a')` was still awaiting.
105
+ expect(mounted.retainOnly(new Map())).toEqual(['a'])
106
+
107
+ // ...and now the in-flight mount completes and hands over its element.
108
+ const late = element()
109
+ const adopted = mounted.adopt('a', late, 'a-slot')
110
+
111
+ expect(adopted).toBe(false)
112
+ expect(late.isConnected).toBe(false)
113
+ expect(mounted.mountedTags().has('a')).toBe(false)
114
+ })
115
+
116
+ test('re-appearing after an unmount mounts cleanly — no stale element, no double latch', () => {
117
+ const mounted = makeMountedRegistry()
118
+ const first = element()
119
+ mounted.latch('a')
120
+ mounted.adopt('a', first, 'a-slot')
121
+ mounted.retainOnly(new Map())
122
+
123
+ // The tag comes back: it is pending again, and adopting a second element
124
+ // must not leave the first one anywhere.
125
+ expect(
126
+ pendingMounts([placement('a')], mounted.mountedTags()).map(p => p.tag),
127
+ ).toEqual(['a'])
128
+
129
+ const second = element()
130
+ mounted.latch('a')
131
+
132
+ expect(mounted.adopt('a', second, 'a-slot')).toBe(true)
133
+ expect(first.isConnected).toBe(false)
134
+ expect(second.isConnected).toBe(true)
135
+ expect([...mounted.mountedTags()]).toEqual(['a'])
136
+ })
137
+
138
+ test('a released tag is pending again, so a FAILED mount retries', () => {
139
+ const mounted = makeMountedRegistry()
140
+ mounted.latch('a')
141
+ mounted.release('a')
142
+
143
+ expect(
144
+ pendingMounts([placement('a')], mounted.mountedTags()).map(p => p.tag),
145
+ ).toEqual(['a'])
146
+ })
147
+
148
+ test('mountedTags is a copy — a caller cannot mutate the registry through it', () => {
149
+ const mounted = makeMountedRegistry()
150
+ mounted.latch('a')
151
+
152
+ const snapshot = mounted.mountedTags()
153
+ mounted.latch('b')
154
+
155
+ expect(snapshot.has('b')).toBe(false)
156
+ expect(mounted.mountedTags().has('b')).toBe(true)
157
+ })
158
+
159
+ test('a placement that MOVES SLOTS is unmounted, so the next pass re-mounts it', () => {
160
+ const mounted = makeMountedRegistry()
161
+ const first = element()
162
+ mounted.latch('a')
163
+ mounted.adopt('a', first, 'a-slot')
164
+
165
+ // Same tag, still placed — but the topology now puts it somewhere else.
166
+ // A registry keyed on tag alone would see nothing to do here, and the dot
167
+ // would never move, which reads to a user as the edit being ignored.
168
+ const moved = mounted.retainOnly(new Map([['a', 'somewhere-else-slot']]))
169
+
170
+ expect(moved).toEqual(['a'])
171
+ expect(first.isConnected).toBe(false)
172
+ expect(
173
+ pendingMounts([placement('a')], mounted.mountedTags()).map(p => p.tag),
174
+ ).toEqual(['a'])
175
+ })
176
+
177
+ test('a tag still in the SAME slot is not disturbed by a topology change', () => {
178
+ const mounted = makeMountedRegistry()
179
+ const a = element()
180
+ mounted.latch('a')
181
+ mounted.adopt('a', a, 'a-slot')
182
+
183
+ expect(mounted.retainOnly(new Map([['a', 'a-slot']]))).toEqual([])
184
+ expect(a.isConnected).toBe(true)
185
+ })
186
+
187
+ test('a tag still MID-MOUNT is judged on presence alone — it has no slot yet', () => {
188
+ const mounted = makeMountedRegistry()
189
+ mounted.latch('a')
190
+
191
+ // Present in the new topology, slot unknown: keep the claim rather than
192
+ // tearing down a mount that has not landed.
193
+ expect(mounted.retainOnly(new Map([['a', 'anywhere-slot']]))).toEqual([])
194
+ expect(mounted.mountedTags().has('a')).toBe(true)
195
+ })
196
+ })
@@ -69,8 +69,8 @@ describe('pendingMounts', () => {
69
69
  test('a tag resolved twice in one list is pending ONCE', () => {
70
70
  const topology = topologyOf([
71
71
  route('/price', [
72
- { tag: 'price-ticker', slotId: 'price-slot' },
73
- { tag: 'price-ticker', slotId: 'compare-slot' },
72
+ { tag: 'readout-view', slotId: 'price-slot' },
73
+ { tag: 'readout-view', slotId: 'compare-slot' },
74
74
  ]),
75
75
  ])
76
76
  const resolved = resolvePlacements(topology, '/price', 'dev')
@@ -95,7 +95,7 @@ describe('pendingMounts', () => {
95
95
  ],
96
96
  [
97
97
  patternRule('r1', '/*', [
98
- { tag: 'price-ticker', slotId: 'price-slot' },
98
+ { tag: 'readout-view', slotId: 'price-slot' },
99
99
  ]),
100
100
  ],
101
101
  )
@@ -103,7 +103,7 @@ describe('pendingMounts', () => {
103
103
 
104
104
  expect(resolved.length).toBe(6)
105
105
  expect(pendingMounts(resolved, new Set()).map(item => item.tag)).toEqual([
106
- 'price-ticker',
106
+ 'readout-view',
107
107
  ])
108
108
  })
109
109
 
@@ -111,8 +111,8 @@ describe('pendingMounts', () => {
111
111
  // Live today without any rules: the union is over routes, so one tag on
112
112
  // two of them is in it twice.
113
113
  const topology = topologyOf([
114
- route('/price', [{ tag: 'price-ticker', slotId: 'price-slot' }]),
115
- route('/all', [{ tag: 'price-ticker', slotId: 'price-slot' }]),
114
+ route('/price', [{ tag: 'readout-view', slotId: 'price-slot' }]),
115
+ route('/all', [{ tag: 'readout-view', slotId: 'price-slot' }]),
116
116
  ])
117
117
  const resolved = overviewUnion(topology)
118
118
 
@@ -141,14 +141,14 @@ describe('pendingMounts', () => {
141
141
  test('skips a tag already latched by an earlier activation', () => {
142
142
  const topology = topologyOf([
143
143
  route('/price', [
144
- { tag: 'price-ticker', slotId: 'price-slot' },
144
+ { tag: 'readout-view', slotId: 'price-slot' },
145
145
  { tag: 'fleet-health', slotId: 'fleet-slot' },
146
146
  ]),
147
147
  ])
148
148
  const resolved = resolvePlacements(topology, '/price', 'dev')
149
149
 
150
150
  expect(
151
- pendingMounts(resolved, new Set(['price-ticker'])).map(item => item.tag),
151
+ pendingMounts(resolved, new Set(['readout-view'])).map(item => item.tag),
152
152
  ).toEqual(['fleet-health'])
153
153
  })
154
154
 
package/src/mounting.ts CHANGED
@@ -88,3 +88,119 @@ export const mountOverridesFor = (
88
88
  Object.entries(resume).filter(([, value]) => value !== ''),
89
89
  ),
90
90
  })
91
+
92
+ /**
93
+ * Which tags this shell has mounted, and the element each one produced.
94
+ *
95
+ * Until Phase B of `wiki/plans/active/wiring-live-placement-on-a-remote-host.md`
96
+ * both shells tracked this as a bare `Set<string>` that only ever grew, and
97
+ * that was deliberate and documented: "a MicroDot is mounted the FIRST time a
98
+ * route asks for it and never again, so an explicit *Unmount ticker* survives
99
+ * navigating away and back." **That stays true for ROUTE changes.** What a
100
+ * grow-only set cannot express is a TOPOLOGY change — a placement that has
101
+ * left the record entirely — and "appear, disappear, re-appear" is the whole
102
+ * proof.
103
+ *
104
+ * Two structures on purpose:
105
+ *
106
+ * - `latched` is every tag CLAIMED, whether its mount is in flight or done. It
107
+ * is what `pendingMounts` must see, so a claim made before an await cannot be
108
+ * claimed twice.
109
+ * - `elements` is the subset whose element exists and can be removed.
110
+ *
111
+ * **The in-flight window is the whole reason this is a type and not a Map.**
112
+ * `mounting.ts` already exists because `filter`-then-`forEach` double-mounted
113
+ * (2026-08-18 review, finding 1); an unmount path introduces the mirror of that
114
+ * defect — a tag released while its mount is still awaiting would append its
115
+ * element AFTER the removal, and a dot the user just deleted would silently
116
+ * come back. `adopt` refuses that append and cleans up, which is why a mount
117
+ * must hand its element here rather than appending and forgetting.
118
+ */
119
+ export type MountedRegistry = {
120
+ /**
121
+ * The claimed tags, for `pendingMounts`. Derived per call rather than kept
122
+ * as a third structure, because a cached copy is one more thing that can
123
+ * disagree with the other two.
124
+ */
125
+ readonly mountedTags: () => ReadonlySet<string>
126
+ /** Claim a tag BEFORE its mount is awaited. */
127
+ readonly latch: (tag: string) => void
128
+ /**
129
+ * Hand over the element a completed mount produced, and the slot it went
130
+ * into.
131
+ *
132
+ * The slot is recorded because a placement can MOVE: a topology edit that
133
+ * puts an already-mounted tag in a different slot leaves the tag placed, so
134
+ * a registry keyed on tag alone sees nothing to do and the dot never moves.
135
+ * That reads to a user as the edit being ignored.
136
+ *
137
+ * Returns `false` — and removes the element — when the tag was released
138
+ * while the mount was in flight. A caller may ignore the result; the
139
+ * cleanup has already happened.
140
+ */
141
+ readonly adopt: (tag: string, element: Element, slotId: string) => boolean
142
+ /** Give up a claim whose mount FAILED, so the next activation retries it. */
143
+ readonly release: (tag: string) => void
144
+ /**
145
+ * Unmount every claimed tag that the topology no longer places THERE, and
146
+ * report which went.
147
+ *
148
+ * `keep` maps tag to the slot it is currently placed in, and is "placed
149
+ * ANYWHERE in the current topology", never "placed on the current route" —
150
+ * passing the latter would unmount on navigation and undo the documented
151
+ * behaviour above.
152
+ *
153
+ * A tag goes when it is absent from `keep` (deleted) OR when its slot has
154
+ * changed (moved). A moved tag is unmounted here and re-mounted by the very
155
+ * next `pendingMounts` pass, which is what makes a move look like a move
156
+ * rather than like nothing happening. A tag still mid-mount has no recorded
157
+ * slot yet and is judged on presence alone.
158
+ */
159
+ readonly retainOnly: (
160
+ keep: ReadonlyMap<string, string>,
161
+ ) => ReadonlyArray<string>
162
+ }
163
+
164
+ export const makeMountedRegistry = (): MountedRegistry => {
165
+ const latched = new Set<string>()
166
+ const elements = new Map<string, Element>()
167
+ const slots = new Map<string, string>()
168
+
169
+ return {
170
+ mountedTags: () => new Set(latched),
171
+ latch: tag => {
172
+ latched.add(tag)
173
+ },
174
+ adopt: (tag, element, slotId) => {
175
+ if (!latched.has(tag)) {
176
+ element.remove()
177
+ return false
178
+ }
179
+ elements.set(tag, element)
180
+ slots.set(tag, slotId)
181
+ return true
182
+ },
183
+ release: tag => {
184
+ latched.delete(tag)
185
+ elements.delete(tag)
186
+ slots.delete(tag)
187
+ },
188
+ retainOnly: keep => {
189
+ const removed = Array.filter(Array.fromIterable(latched), tag => {
190
+ const wanted = keep.get(tag)
191
+ if (wanted === undefined) return true
192
+ // Mid-mount: no slot recorded yet, so presence is all there is to go
193
+ // on. The in-flight guard in `adopt` covers the rest.
194
+ const where = slots.get(tag)
195
+ return where !== undefined && where !== wanted
196
+ })
197
+ removed.forEach(tag => {
198
+ elements.get(tag)?.remove()
199
+ elements.delete(tag)
200
+ slots.delete(tag)
201
+ latched.delete(tag)
202
+ })
203
+ return removed
204
+ },
205
+ }
206
+ }
@@ -215,9 +215,9 @@ describe('checkPlacements — check 2: slot not in the theme', () => {
215
215
  describe('checkPlacements — check 3: required attribute unset', () => {
216
216
  test('warns when a required dot-owned attribute has no supplied value', () => {
217
217
  const report = checkPlacements(
218
- [placed('p1', 'price-ticker', 'hero-slot')],
218
+ [placed('p1', 'readout-view', 'hero-slot')],
219
219
  contextOf([
220
- manifestOf('price', ['price-ticker'], {
220
+ manifestOf('price', ['readout-view'], {
221
221
  attributes: [requiredAttribute('symbol', 'dot')],
222
222
  }),
223
223
  ]),
@@ -236,12 +236,12 @@ describe('checkPlacements — check 3: required attribute unset', () => {
236
236
  test('does not warn when the resolved placement supplies the value', () => {
237
237
  const report = checkPlacements(
238
238
  [
239
- placed('p1', 'price-ticker', 'hero-slot', {
239
+ placed('p1', 'readout-view', 'hero-slot', {
240
240
  values: { symbol: 'FOLD' },
241
241
  }),
242
242
  ],
243
243
  contextOf([
244
- manifestOf('price', ['price-ticker'], {
244
+ manifestOf('price', ['readout-view'], {
245
245
  attributes: [requiredAttribute('symbol', 'dot')],
246
246
  }),
247
247
  ]),
@@ -251,9 +251,9 @@ describe('checkPlacements — check 3: required attribute unset', () => {
251
251
 
252
252
  test('exempts environment- and host-owned attributes — the loader and the host supply those', () => {
253
253
  const report = checkPlacements(
254
- [placed('p1', 'price-ticker', 'hero-slot')],
254
+ [placed('p1', 'readout-view', 'hero-slot')],
255
255
  contextOf([
256
- manifestOf('price', ['price-ticker'], {
256
+ manifestOf('price', ['readout-view'], {
257
257
  attributes: [
258
258
  requiredAttribute('api-url', 'environment'),
259
259
  requiredAttribute('interview-id', 'host-input'),
@@ -542,7 +542,7 @@ describe('checkPlacements — the reading-B override qualifier', () => {
542
542
  // two firing conditions, zero issues, because it renders for nobody.
543
543
  const report = checkPlacements(
544
544
  [
545
- placed('loser', 'price-ticker', 'ghost-slot', {
545
+ placed('loser', 'readout-view', 'ghost-slot', {
546
546
  state: 'overridden',
547
547
  overriddenBy: [
548
548
  {
@@ -553,12 +553,12 @@ describe('checkPlacements — the reading-B override qualifier', () => {
553
553
  },
554
554
  ],
555
555
  }),
556
- placed('winner', 'price-ticker', 'hero-slot', {
556
+ placed('winner', 'readout-view', 'hero-slot', {
557
557
  values: { symbol: 'FOLD' },
558
558
  }),
559
559
  ],
560
560
  contextOf([
561
- manifestOf('price', ['price-ticker'], {
561
+ manifestOf('price', ['readout-view'], {
562
562
  attributes: [requiredAttribute('symbol', 'dot')],
563
563
  }),
564
564
  ]),
@@ -569,7 +569,7 @@ describe('checkPlacements — the reading-B override qualifier', () => {
569
569
  test('a partially overridden placement is still checked — it still renders for somebody', () => {
570
570
  const report = checkPlacements(
571
571
  [
572
- placed('loser', 'price-ticker', 'ghost-slot', {
572
+ placed('loser', 'readout-view', 'ghost-slot', {
573
573
  state: 'partially-overridden',
574
574
  overriddenBy: [
575
575
  {
@@ -580,11 +580,11 @@ describe('checkPlacements — the reading-B override qualifier', () => {
580
580
  },
581
581
  ],
582
582
  }),
583
- placed('winner', 'price-ticker', 'hero-slot', {
583
+ placed('winner', 'readout-view', 'hero-slot', {
584
584
  condition: { who: 'any', device: 'any', locale: 'fr' },
585
585
  }),
586
586
  ],
587
- contextOf([manifestOf('price', ['price-ticker'])]),
587
+ contextOf([manifestOf('price', ['readout-view'])]),
588
588
  )
589
589
  expect(
590
590
  issuesFor(report, 'slot-not-in-theme').map(issue => issue.placementId),
package/src/rules.test.ts CHANGED
@@ -122,7 +122,7 @@ describe('ruleMatches', () => {
122
122
  describe('resolvePlacements', () => {
123
123
  test('a no-rules topology resolves a single mount to the fully-defaulted identity', () => {
124
124
  const topology = topologyOf([
125
- route('/price', [{ tag: 'price-ticker', slotId: 'price-slot' }]),
125
+ route('/price', [{ tag: 'readout-view', slotId: 'price-slot' }]),
126
126
  ])
127
127
  // The whole record, pinned: defaults made explicit (condition axes to
128
128
  // 'any', envs to all three, values to {}), route provenance, and the
@@ -130,8 +130,8 @@ describe('resolvePlacements', () => {
130
130
  // issues depend on that id never changing shape.
131
131
  expect(resolvePlacements(topology, '/price', 'dev')).toEqual([
132
132
  {
133
- id: '/price|0|route:price-ticker@price-slot#0',
134
- tag: 'price-ticker',
133
+ id: '/price|0|route:readout-view@price-slot#0',
134
+ tag: 'readout-view',
135
135
  slotId: 'price-slot',
136
136
  source: { _tag: 'route' },
137
137
  condition: { who: 'any', device: 'any', locale: 'any' },
@@ -147,7 +147,7 @@ describe('resolvePlacements', () => {
147
147
 
148
148
  test('a no-rules topology resolves to exactly the route’s own mounts — the Phase-4 identity', () => {
149
149
  const mounts: ReadonlyArray<TopologyPlacement> = [
150
- { tag: 'price-ticker', slotId: 'price-slot' },
150
+ { tag: 'readout-view', slotId: 'price-slot' },
151
151
  { tag: 'fleet-health', slotId: 'fleet-slot' },
152
152
  ]
153
153
  const topology = topologyOf([route('/all', mounts)])
package/src/rules.ts CHANGED
@@ -214,7 +214,7 @@ export type ResolvedState = 'active' | 'partially-overridden' | 'overridden'
214
214
  *
215
215
  * It is the declared id when the record has one AND that id is declared
216
216
  * exactly once in this resolution; otherwise it is (or is qualified by) a
217
- * synthesized fallback — `/price|0|route:price-ticker@main-slot#0`: route
217
+ * synthesized fallback — `/price|0|route:readout-view@main-slot#0`: route
218
218
  * path, layer, source key, tag, slot, index within its layer. See
219
219
  * `resolvedIdOf` for the collision rule.
220
220
  */