@bespokeagentics/microdots-host 0.1.0 → 0.1.2

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,105 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "microdots.topology-regressions.p5",
4
+ "corpusVersion": "p5.1",
5
+ "toolVersion": "compose-from-selection@1",
6
+ "surfaces": [
7
+ {
8
+ "tag": "readout-view",
9
+ "attributes": [
10
+ { "name": "symbol", "ownership": "dot" },
11
+ { "name": "api-url", "ownership": "environment" }
12
+ ],
13
+ "events": [
14
+ {
15
+ "name": "quote-changed",
16
+ "fields": { "symbol": "string" }
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "tag": "roster-backend",
22
+ "attributes": [{ "name": "refresh-token", "ownership": "dot" }],
23
+ "events": []
24
+ }
25
+ ],
26
+ "cases": [
27
+ {
28
+ "schemaVersion": 1,
29
+ "id": "topology.together",
30
+ "title": "Place both catalogued surfaces on one route",
31
+ "input": {
32
+ "hostId": "lead-capture",
33
+ "hostLabel": "Lead capture",
34
+ "tags": ["readout-view", "roster-backend"],
35
+ "layout": "together",
36
+ "slotKind": "band",
37
+ "wires": []
38
+ },
39
+ "expected": {
40
+ "hostId": "lead-capture",
41
+ "hostLabel": "Lead capture",
42
+ "generateCount": 0,
43
+ "routePaths": ["/"],
44
+ "mountedTags": ["readout-view", "roster-backend"],
45
+ "slotKinds": ["band", "band"],
46
+ "wirePlain": []
47
+ }
48
+ },
49
+ {
50
+ "schemaVersion": 1,
51
+ "id": "topology.per-tag",
52
+ "title": "Place one catalogued surface on each route",
53
+ "input": {
54
+ "hostId": "demo",
55
+ "hostLabel": "Demo",
56
+ "tags": ["readout-view", "roster-backend"],
57
+ "layout": "per-tag",
58
+ "slotKind": "rail",
59
+ "wires": []
60
+ },
61
+ "expected": {
62
+ "hostId": "demo",
63
+ "hostLabel": "Demo",
64
+ "generateCount": 0,
65
+ "routePaths": ["/readout-view", "/roster-backend"],
66
+ "mountedTags": ["readout-view", "roster-backend"],
67
+ "slotKinds": ["rail", "rail"],
68
+ "wirePlain": []
69
+ }
70
+ },
71
+ {
72
+ "schemaVersion": 1,
73
+ "id": "topology.direct-wire",
74
+ "title": "Materialize one catalogued direct wire",
75
+ "input": {
76
+ "brief": "Wire the readout quote into the roster refresh token.",
77
+ "hostId": "demo",
78
+ "hostLabel": "Demo",
79
+ "tags": ["readout-view", "roster-backend"],
80
+ "layout": "together",
81
+ "slotKind": "band",
82
+ "wires": [
83
+ {
84
+ "from": "readout-view",
85
+ "event": "quote-changed",
86
+ "field": "symbol",
87
+ "to": "roster-backend",
88
+ "input": "refresh-token"
89
+ }
90
+ ]
91
+ },
92
+ "expected": {
93
+ "hostId": "demo",
94
+ "hostLabel": "Demo",
95
+ "generateCount": 0,
96
+ "routePaths": ["/"],
97
+ "mountedTags": ["readout-view", "roster-backend"],
98
+ "slotKinds": ["band", "band"],
99
+ "wirePlain": [
100
+ "When quote-changed on readout-view, set refresh-token on roster-backend."
101
+ ]
102
+ }
103
+ }
104
+ ]
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bespokeagentics/microdots-host",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MicroDots host shell — registry, loader, slot manifest and placement checks for mounting MicroDot custom elements. Raw TypeScript source; consume with Bun or Vite.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -10,14 +10,16 @@
10
10
  "directory": "packages/microdots-host"
11
11
  },
12
12
  "publishConfig": {
13
- "access": "public"
13
+ "access": "restricted"
14
14
  },
15
15
  "main": "./src/index.ts",
16
16
  "exports": {
17
- ".": "./src/index.ts"
17
+ ".": "./src/index.ts",
18
+ "./composition": "./src/compositionSpec.ts"
18
19
  },
19
20
  "dependencies": {
20
- "@bespokeagentics/microdots-element": "0.1.0"
21
+ "@bespokeagentics/microdots-authoring": "0.1.1",
22
+ "@bespokeagentics/microdots-element": "0.1.1"
21
23
  },
22
24
  "peerDependencies": {
23
25
  "effect": "4.0.0-rc.108"
@@ -0,0 +1,277 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { decodeCatalogLockSync } from '@bespokeagentics/microdots-authoring'
4
+ import type {
5
+ ManifestAttribute,
6
+ ManifestEvent,
7
+ ManifestTag,
8
+ } from '@bespokeagentics/microdots-element'
9
+
10
+ import {
11
+ composeExistingSurfacesFromSelection,
12
+ composeFromSelection,
13
+ eventFieldsOf,
14
+ wireableAttributesOf,
15
+ } from './composeFromSelection.ts'
16
+ import { FillCompositionError } from './fillComposition.ts'
17
+
18
+ const attribute = (
19
+ name: string,
20
+ ownership: ManifestAttribute['ownership'] = 'dot',
21
+ ): ManifestAttribute => ({
22
+ name,
23
+ type: 'string',
24
+ required: false,
25
+ live: true,
26
+ ownership,
27
+ })
28
+
29
+ const manifestEvent = (
30
+ name: string,
31
+ properties: Readonly<Record<string, unknown>>,
32
+ ): ManifestEvent => ({
33
+ name,
34
+ payload: {
35
+ ref: `@microdots/test/contract#${name}`,
36
+ jsonSchema: {
37
+ dialect: 'draft-2020-12',
38
+ schema: { type: 'object', properties },
39
+ definitions: {},
40
+ },
41
+ },
42
+ })
43
+
44
+ const PRICE: ManifestTag = {
45
+ tag: 'readout-view',
46
+ attributes: [attribute('symbol'), attribute('api-url', 'environment')],
47
+ events: [manifestEvent('quote-changed', { symbol: { type: 'string' } })],
48
+ }
49
+
50
+ const FLEET: ManifestTag = {
51
+ tag: 'fleet-health',
52
+ attributes: [attribute('region')],
53
+ events: [],
54
+ }
55
+
56
+ const SURFACES: ReadonlyArray<ManifestTag> = [PRICE, FLEET]
57
+
58
+ const CATALOG_LOCK = decodeCatalogLockSync({
59
+ schemaVersion: 1,
60
+ snapshotSha256: 'a'.repeat(64),
61
+ compilerVersion: '1.2.1-p6',
62
+ shapeDigests: {},
63
+ capabilityDigests: {},
64
+ backendCatalogDigest: 'b'.repeat(64),
65
+ targetMatrixDigest: 'c'.repeat(64),
66
+ allocationBaselineDigest: 'd'.repeat(64),
67
+ })
68
+
69
+ describe('eventFieldsOf', () => {
70
+ test('reads nested draft-2020-12 properties', () => {
71
+ const event = PRICE.events[0]
72
+ expect(event).toBeDefined()
73
+ if (event === undefined) {
74
+ return
75
+ }
76
+ expect(eventFieldsOf(event)).toEqual([{ name: 'symbol', type: 'string' }])
77
+ })
78
+ })
79
+
80
+ describe('wireableAttributesOf', () => {
81
+ test('drops environment-owned attributes', () => {
82
+ expect(wireableAttributesOf(PRICE).map(item => item.name)).toEqual([
83
+ 'symbol',
84
+ ])
85
+ })
86
+ })
87
+
88
+ describe('composeFromSelection', () => {
89
+ test('emits catalog-locked v2 for new existing-surface traffic', () => {
90
+ const spec = composeExistingSurfacesFromSelection(
91
+ {
92
+ hostId: 'lead-capture',
93
+ hostLabel: 'Lead capture',
94
+ tags: ['readout-view', 'fleet-health'],
95
+ layout: 'together',
96
+ slotKind: 'band',
97
+ wires: [],
98
+ },
99
+ SURFACES,
100
+ CATALOG_LOCK,
101
+ )
102
+
103
+ expect(spec.schemaVersion).toBe(2)
104
+ expect(spec.generate).toEqual([])
105
+ expect(spec.catalogLock).toEqual(CATALOG_LOCK)
106
+ })
107
+
108
+ test('places every selected tag on one route when layout is together', () => {
109
+ const spec = composeFromSelection(
110
+ {
111
+ hostId: 'lead-capture',
112
+ hostLabel: 'Lead capture',
113
+ tags: ['readout-view', 'fleet-health'],
114
+ layout: 'together',
115
+ slotKind: 'band',
116
+ wires: [],
117
+ },
118
+ SURFACES,
119
+ )
120
+ expect(spec.generate).toEqual([])
121
+ expect(spec.topology.host).toEqual({
122
+ id: 'lead-capture',
123
+ label: 'Lead capture',
124
+ ownedInputs: [],
125
+ })
126
+ expect(spec.topology.routes).toHaveLength(1)
127
+ expect(spec.topology.routes[0]?.path).toBe('/')
128
+ expect(spec.topology.routes[0]?.mounts.map(mount => mount.tag)).toEqual([
129
+ 'readout-view',
130
+ 'fleet-health',
131
+ ])
132
+ expect(spec.topology.slotManifest?.theme).toEqual({
133
+ name: '@bespokeagentics/microdots-theme',
134
+ version: '0.1.1',
135
+ })
136
+ })
137
+
138
+ test('one route per tag when layout is per-tag', () => {
139
+ const spec = composeFromSelection(
140
+ {
141
+ hostId: 'demo',
142
+ hostLabel: 'Demo',
143
+ tags: ['readout-view', 'fleet-health'],
144
+ layout: 'per-tag',
145
+ slotKind: 'rail',
146
+ wires: [],
147
+ },
148
+ SURFACES,
149
+ )
150
+ expect(spec.topology.routes.map(route => route.path)).toEqual([
151
+ '/readout-view',
152
+ '/fleet-health',
153
+ ])
154
+ expect(spec.topology.slotManifest?.slots.map(slot => slot.kind)).toEqual([
155
+ 'rail',
156
+ 'rail',
157
+ ])
158
+ })
159
+
160
+ test('fills a catalogued wire and never invents generate[]', () => {
161
+ const spec = composeFromSelection(
162
+ {
163
+ brief: 'Wire the ticker into the fleet region.',
164
+ hostId: 'demo',
165
+ hostLabel: 'Demo',
166
+ tags: ['readout-view', 'fleet-health'],
167
+ layout: 'together',
168
+ slotKind: 'band',
169
+ wires: [
170
+ {
171
+ from: 'readout-view',
172
+ event: 'quote-changed',
173
+ field: 'symbol',
174
+ to: 'fleet-health',
175
+ input: 'region',
176
+ },
177
+ ],
178
+ },
179
+ SURFACES,
180
+ )
181
+ expect(spec.generate).toEqual([])
182
+ expect(spec.brief).toBe('Wire the ticker into the fleet region.')
183
+ expect(spec.topology.wires).toEqual([
184
+ {
185
+ id: 'w1',
186
+ from: 'readout-view',
187
+ event: 'quote-changed',
188
+ field: 'symbol',
189
+ fieldType: 'string',
190
+ to: 'fleet-health',
191
+ input: 'region',
192
+ inputType: 'string',
193
+ transform: { _tag: 'direct' },
194
+ envs: ['dev', 'preview', 'prod'],
195
+ plain:
196
+ 'When quote-changed on readout-view, set region on fleet-health.',
197
+ },
198
+ ])
199
+ })
200
+
201
+ test('refuses a tag that is not in the catalog', () => {
202
+ try {
203
+ composeFromSelection(
204
+ {
205
+ hostId: 'demo',
206
+ hostLabel: 'Demo',
207
+ tags: ['weather-5day-forecast'],
208
+ layout: 'together',
209
+ slotKind: 'band',
210
+ wires: [],
211
+ },
212
+ SURFACES,
213
+ )
214
+ expect.unreachable('unknown tags must fail')
215
+ } catch (error) {
216
+ expect(error).toBeInstanceOf(FillCompositionError)
217
+ if (error instanceof FillCompositionError) {
218
+ expect(error.failure._tag).toBe('invalid')
219
+ if (error.failure._tag === 'invalid') {
220
+ expect(error.failure.issues[0]?.message).toContain(
221
+ 'weather-5day-forecast',
222
+ )
223
+ expect(error.failure.issues[0]?.message).toContain('catalog')
224
+ }
225
+ }
226
+ }
227
+ })
228
+
229
+ test('refuses an empty selection', () => {
230
+ expect(() =>
231
+ composeFromSelection(
232
+ {
233
+ hostId: 'demo',
234
+ hostLabel: 'Demo',
235
+ tags: [],
236
+ layout: 'together',
237
+ slotKind: 'band',
238
+ wires: [],
239
+ },
240
+ SURFACES,
241
+ ),
242
+ ).toThrow(FillCompositionError)
243
+ })
244
+
245
+ test('refuses a wire to an environment-owned attribute', () => {
246
+ try {
247
+ composeFromSelection(
248
+ {
249
+ hostId: 'demo',
250
+ hostLabel: 'Demo',
251
+ tags: ['readout-view', 'fleet-health'],
252
+ layout: 'together',
253
+ slotKind: 'band',
254
+ wires: [
255
+ {
256
+ from: 'readout-view',
257
+ event: 'quote-changed',
258
+ field: 'symbol',
259
+ to: 'readout-view',
260
+ input: 'api-url',
261
+ },
262
+ ],
263
+ },
264
+ SURFACES,
265
+ )
266
+ expect.unreachable('environment inputs must fail')
267
+ } catch (error) {
268
+ expect(error).toBeInstanceOf(FillCompositionError)
269
+ if (
270
+ error instanceof FillCompositionError &&
271
+ error.failure._tag === 'invalid'
272
+ ) {
273
+ expect(error.failure.issues[0]?.message).toContain('api-url')
274
+ }
275
+ }
276
+ })
277
+ })