@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/package.json +6 -6
- package/src/anchorMount.test.ts +124 -0
- package/src/anchorMount.ts +153 -0
- package/src/fillComposition.ts +1 -1
- package/src/index.ts +56 -3
- package/src/mountedRegistry.test.ts +1 -0
- package/src/mounting.test.ts +23 -0
- package/src/mounting.ts +10 -1
- package/src/placementChecks.test.ts +180 -4
- package/src/placementChecks.ts +132 -7
- package/src/registry.test.ts +289 -0
- package/src/registry.ts +158 -0
- package/src/registryFromManifest.test.ts +126 -0
- package/src/registryFromManifest.ts +92 -0
- package/src/renderLayout.test.ts +263 -0
- package/src/renderLayout.ts +345 -0
- package/src/rules.test.ts +85 -0
- package/src/rules.ts +9 -0
- package/src/slotLayout.test.ts +127 -0
- package/src/slotLayout.ts +113 -0
- package/src/slots.test.ts +37 -3
- package/src/slots.ts +10 -1
- package/src/wire.test.ts +80 -0
- package/src/wire.ts +60 -7
- package/src/wireEngine.test.ts +1 -1
|
@@ -37,6 +37,7 @@ const placed = (
|
|
|
37
37
|
values: {},
|
|
38
38
|
span: Option.none(),
|
|
39
39
|
order: Option.none(),
|
|
40
|
+
selector: Option.none(),
|
|
40
41
|
state: 'active',
|
|
41
42
|
overriddenBy: [],
|
|
42
43
|
...overrides,
|
|
@@ -207,6 +208,77 @@ describe('checkPlacements — check 2: slot not in the theme', () => {
|
|
|
207
208
|
},
|
|
208
209
|
])
|
|
209
210
|
})
|
|
211
|
+
|
|
212
|
+
test('does not fire for an anchor — @anchor is the escape hatch, not a missing declaration', () => {
|
|
213
|
+
const report = checkPlacements(
|
|
214
|
+
[
|
|
215
|
+
placed('p1', 'promo-banner', '@anchor', {
|
|
216
|
+
selector: Option.some('#hero .cta-row'),
|
|
217
|
+
}),
|
|
218
|
+
],
|
|
219
|
+
contextOf([manifestOf('promo', ['promo-banner'])]),
|
|
220
|
+
)
|
|
221
|
+
expect(issuesFor(report, 'slot-not-in-theme')).toEqual([])
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// The failure mode: no crawl exists, so an anchor's resolution CANNOT be
|
|
226
|
+
// checked — and a screen that says nothing about it reads as green. Check 5
|
|
227
|
+
// ships as a reporter only: one unverifiable entry per distinct selector,
|
|
228
|
+
// never an issue, never a stored guess (the write-mode plan's D5).
|
|
229
|
+
describe('checkPlacements — check 5: anchor selectors are unverifiable', () => {
|
|
230
|
+
test('reports one unverifiable entry per distinct rendered selector', () => {
|
|
231
|
+
const report = checkPlacements(
|
|
232
|
+
[
|
|
233
|
+
placed('p1', 'promo-banner', '@anchor', {
|
|
234
|
+
selector: Option.some('#hero'),
|
|
235
|
+
}),
|
|
236
|
+
placed('p2', 'contact-form', '@anchor', {
|
|
237
|
+
selector: Option.some('#hero'),
|
|
238
|
+
}),
|
|
239
|
+
placed('p3', 'price-ticker', '@anchor', {
|
|
240
|
+
selector: Option.some('footer'),
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
contextOf([
|
|
244
|
+
manifestOf('promo', ['promo-banner']),
|
|
245
|
+
manifestOf('contact', ['contact-form']),
|
|
246
|
+
manifestOf('price', ['price-ticker']),
|
|
247
|
+
]),
|
|
248
|
+
)
|
|
249
|
+
expect(
|
|
250
|
+
report.unverifiable.filter(
|
|
251
|
+
entry => entry.check === 'anchor-selector-unresolved',
|
|
252
|
+
),
|
|
253
|
+
).toEqual([
|
|
254
|
+
{
|
|
255
|
+
check: 'anchor-selector-unresolved',
|
|
256
|
+
reason: expect.stringContaining('"#hero"'),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
check: 'anchor-selector-unresolved',
|
|
260
|
+
reason: expect.stringContaining('"footer"'),
|
|
261
|
+
},
|
|
262
|
+
])
|
|
263
|
+
expect(issuesFor(report, 'anchor-selector-unresolved')).toEqual([])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('an entirely overridden anchor reports nothing — it renders for nobody', () => {
|
|
267
|
+
const report = checkPlacements(
|
|
268
|
+
[
|
|
269
|
+
placed('p1', 'promo-banner', '@anchor', {
|
|
270
|
+
selector: Option.some('#hero'),
|
|
271
|
+
state: 'overridden',
|
|
272
|
+
}),
|
|
273
|
+
],
|
|
274
|
+
contextOf([manifestOf('promo', ['promo-banner'])]),
|
|
275
|
+
)
|
|
276
|
+
expect(
|
|
277
|
+
report.unverifiable.filter(
|
|
278
|
+
entry => entry.check === 'anchor-selector-unresolved',
|
|
279
|
+
),
|
|
280
|
+
).toEqual([])
|
|
281
|
+
})
|
|
210
282
|
})
|
|
211
283
|
|
|
212
284
|
// The failure mode: a manifest-required attribute nobody sets means the
|
|
@@ -367,12 +439,12 @@ describe('checkPlacements — check 6: over the page-weight budget', () => {
|
|
|
367
439
|
// kill.
|
|
368
440
|
const report = checkPlacements(
|
|
369
441
|
[
|
|
370
|
-
placed('p1', 'workbench-
|
|
442
|
+
placed('p1', 'workbench-board', 'hero-slot'),
|
|
371
443
|
placed('p2', 'workbench-brief', 'side-slot'),
|
|
372
444
|
placed('p3', 'wiring-canvas', 'side-slot'),
|
|
373
445
|
],
|
|
374
446
|
contextOf([
|
|
375
|
-
manifestOf('workbench', ['workbench-
|
|
447
|
+
manifestOf('workbench', ['workbench-board', 'workbench-brief'], {
|
|
376
448
|
gzipBytes: 100 * KB,
|
|
377
449
|
}),
|
|
378
450
|
manifestOf('wiring', ['wiring-canvas'], { gzipBytes: 70 * KB }),
|
|
@@ -384,12 +456,12 @@ describe('checkPlacements — check 6: over the page-weight budget', () => {
|
|
|
384
456
|
test('warns once over budget, naming the total and the largest bundle', () => {
|
|
385
457
|
const report = checkPlacements(
|
|
386
458
|
[
|
|
387
|
-
placed('p1', 'workbench-
|
|
459
|
+
placed('p1', 'workbench-board', 'hero-slot'),
|
|
388
460
|
placed('p2', 'workbench-brief', 'side-slot'),
|
|
389
461
|
placed('p3', 'wiring-canvas', 'side-slot'),
|
|
390
462
|
],
|
|
391
463
|
contextOf([
|
|
392
|
-
manifestOf('workbench', ['workbench-
|
|
464
|
+
manifestOf('workbench', ['workbench-board', 'workbench-brief'], {
|
|
393
465
|
gzipBytes: 100 * KB,
|
|
394
466
|
}),
|
|
395
467
|
manifestOf('wiring', ['wiring-canvas'], { gzipBytes: 90 * KB }),
|
|
@@ -591,3 +663,107 @@ describe('checkPlacements — the reading-B override qualifier', () => {
|
|
|
591
663
|
).toEqual(['loser'])
|
|
592
664
|
})
|
|
593
665
|
})
|
|
666
|
+
|
|
667
|
+
describe('checkPlacements — check 8: over slot capacity', () => {
|
|
668
|
+
// Diagnostics, not enforcement: nothing at runtime refuses the third
|
|
669
|
+
// placement in a capacity-2 slot — the generated host stacks it — so the
|
|
670
|
+
// claim is DISAGREEMENT between the declaration and the placements, which
|
|
671
|
+
// stays true regardless of renderer behavior.
|
|
672
|
+
const capacityManifest: HostSlotManifest = {
|
|
673
|
+
theme: { name: '@bespokeagentics/microdots-theme', version: '0.1.1' },
|
|
674
|
+
slots: [
|
|
675
|
+
{ id: 'hero-slot', kind: 'band', row: 0, capacity: 2 },
|
|
676
|
+
{ id: 'main-slot', kind: 'grid', row: 1 },
|
|
677
|
+
],
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
test('warns once per overflowing slot, on the first placement past capacity in stacking order', () => {
|
|
681
|
+
const report = checkPlacements(
|
|
682
|
+
[
|
|
683
|
+
placed('p1', 'a-view', 'hero-slot', { order: Option.some(1) }),
|
|
684
|
+
placed('p2', 'b-view', 'hero-slot', { order: Option.some(2) }),
|
|
685
|
+
placed('p3', 'c-view', 'hero-slot', { order: Option.some(3) }),
|
|
686
|
+
placed('p4', 'd-view', 'hero-slot', { order: Option.some(4) }),
|
|
687
|
+
],
|
|
688
|
+
contextOf([], { slotManifest: capacityManifest }),
|
|
689
|
+
)
|
|
690
|
+
const issues = issuesFor(report, 'over-slot-capacity')
|
|
691
|
+
expect(issues.length).toBe(1)
|
|
692
|
+
expect(issues[0]?.placementId).toBe('p3')
|
|
693
|
+
expect(issues[0]?.severity).toBe('warning')
|
|
694
|
+
expect(issues[0]?.consequence).toContain('capacity 2')
|
|
695
|
+
expect(issues[0]?.consequence).toContain('4 placements')
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
test('does not fire at or under capacity, or for a capacity-less slot', () => {
|
|
699
|
+
const report = checkPlacements(
|
|
700
|
+
[
|
|
701
|
+
placed('p1', 'a-view', 'hero-slot'),
|
|
702
|
+
placed('p2', 'b-view', 'hero-slot'),
|
|
703
|
+
placed('p3', 'c-view', 'main-slot'),
|
|
704
|
+
placed('p4', 'd-view', 'main-slot'),
|
|
705
|
+
placed('p5', 'e-view', 'main-slot'),
|
|
706
|
+
],
|
|
707
|
+
contextOf([], { slotManifest: capacityManifest }),
|
|
708
|
+
)
|
|
709
|
+
expect(issuesFor(report, 'over-slot-capacity')).toEqual([])
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test('stays silent with no manifest — the check-2 unverifiable entry already covers it', () => {
|
|
713
|
+
const report = checkPlacements(
|
|
714
|
+
[
|
|
715
|
+
placed('p1', 'a-view', 'hero-slot'),
|
|
716
|
+
placed('p2', 'b-view', 'hero-slot'),
|
|
717
|
+
placed('p3', 'c-view', 'hero-slot'),
|
|
718
|
+
],
|
|
719
|
+
{ manifests: [], env: 'dev' },
|
|
720
|
+
)
|
|
721
|
+
expect(issuesFor(report, 'over-slot-capacity')).toEqual([])
|
|
722
|
+
expect(
|
|
723
|
+
report.unverifiable.filter(entry => entry.check === 'over-slot-capacity'),
|
|
724
|
+
).toEqual([])
|
|
725
|
+
})
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
describe('checkPlacements — check 9: a span in a non-grid slot', () => {
|
|
729
|
+
const kindsManifest: HostSlotManifest = {
|
|
730
|
+
theme: { name: '@bespokeagentics/microdots-theme', version: '0.1.1' },
|
|
731
|
+
slots: [
|
|
732
|
+
{ id: 'hero-slot', kind: 'band', row: 0 },
|
|
733
|
+
{ id: 'main-slot', kind: 'grid', row: 1 },
|
|
734
|
+
],
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
test('notes a dormant span in a band slot — the legal residue of a kind edit', () => {
|
|
738
|
+
const report = checkPlacements(
|
|
739
|
+
[
|
|
740
|
+
placed('p1', 'a-view', 'hero-slot', { span: Option.some(6) }),
|
|
741
|
+
placed('p2', 'b-view', 'main-slot', { span: Option.some(6) }),
|
|
742
|
+
],
|
|
743
|
+
contextOf([], { slotManifest: kindsManifest }),
|
|
744
|
+
)
|
|
745
|
+
const issues = issuesFor(report, 'span-in-non-grid')
|
|
746
|
+
expect(issues.length).toBe(1)
|
|
747
|
+
expect(issues[0]?.placementId).toBe('p1')
|
|
748
|
+
expect(issues[0]?.severity).toBe('note')
|
|
749
|
+
expect(issues[0]?.consequence).toContain('dormant')
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
test('does not fire without a span, in a grid, in an undeclared slot, or with no manifest', () => {
|
|
753
|
+
const noSpan = checkPlacements(
|
|
754
|
+
[placed('p1', 'a-view', 'hero-slot')],
|
|
755
|
+
contextOf([], { slotManifest: kindsManifest }),
|
|
756
|
+
)
|
|
757
|
+
expect(issuesFor(noSpan, 'span-in-non-grid')).toEqual([])
|
|
758
|
+
const undeclared = checkPlacements(
|
|
759
|
+
[placed('p2', 'b-view', 'phantom-slot', { span: Option.some(4) })],
|
|
760
|
+
contextOf([], { slotManifest: kindsManifest }),
|
|
761
|
+
)
|
|
762
|
+
expect(issuesFor(undeclared, 'span-in-non-grid')).toEqual([])
|
|
763
|
+
const bare = checkPlacements(
|
|
764
|
+
[placed('p3', 'c-view', 'hero-slot', { span: Option.some(4) })],
|
|
765
|
+
{ manifests: [], env: 'dev' },
|
|
766
|
+
)
|
|
767
|
+
expect(issuesFor(bare, 'span-in-non-grid')).toEqual([])
|
|
768
|
+
})
|
|
769
|
+
})
|
package/src/placementChecks.ts
CHANGED
|
@@ -9,14 +9,17 @@ import {
|
|
|
9
9
|
} from './rules.ts'
|
|
10
10
|
import type { HostSlotManifest } from './slots.ts'
|
|
11
11
|
import type { WireEnv } from './wire.ts'
|
|
12
|
+
import { ANCHOR_SLOT_ID } from './wire.ts'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* The placement CHECKS —
|
|
15
|
+
* The placement CHECKS — the spec's seven, run as a pure lint engine
|
|
15
16
|
* over `resolvePlacements`' output. Phase 5 work item 4 of
|
|
16
17
|
* `wiki/plans/shipped/microdots-platform-phase-5-pages-design.md`;
|
|
17
18
|
* the seven checks' severities and sentences are the handoff spec's, lifted
|
|
18
|
-
* verbatim where the fixture wrote them. Check 5 (anchor selector
|
|
19
|
-
*
|
|
19
|
+
* verbatim where the fixture wrote them. Check 5 (anchor selector resolution)
|
|
20
|
+
* has an id and a reporter but NO verifier: no crawl exists, so every rendered
|
|
21
|
+
* anchor is reported unverifiable — a check that cannot run is never silently
|
|
22
|
+
* green (the Pages write-mode plan's D5).
|
|
20
23
|
*
|
|
21
24
|
* Reading B's qualifier lands here exactly as
|
|
22
25
|
* `question-does-the-override-contest-consider-condition.md` predicted: a
|
|
@@ -32,8 +35,11 @@ export type PlacementCheckId =
|
|
|
32
35
|
| 'slot-not-in-theme'
|
|
33
36
|
| 'required-attribute-unset'
|
|
34
37
|
| 'mounts-twice-overlapping'
|
|
38
|
+
| 'anchor-selector-unresolved'
|
|
35
39
|
| 'over-weight-budget'
|
|
36
40
|
| 'placed-twice-disjoint'
|
|
41
|
+
| 'over-slot-capacity'
|
|
42
|
+
| 'span-in-non-grid'
|
|
37
43
|
|
|
38
44
|
export type PlacementCheckSeverity = 'error' | 'warning' | 'note'
|
|
39
45
|
|
|
@@ -188,6 +194,9 @@ const checkSlotInTheme = (
|
|
|
188
194
|
const theme = `${slotManifest.theme.name}@${slotManifest.theme.version}`
|
|
189
195
|
const issues = Array.getSomes(
|
|
190
196
|
Array.map(rendered, (placement): Option.Option<PlacementIssue> =>
|
|
197
|
+
// An anchor is deliberately OUTSIDE the theme — `@anchor` is the
|
|
198
|
+
// escape hatch, not a missing declaration. Check 5 owns anchors.
|
|
199
|
+
placement.slotId === ANCHOR_SLOT_ID ||
|
|
191
200
|
Array.some(slotManifest.slots, slot => slot.id === placement.slotId)
|
|
192
201
|
? Option.none()
|
|
193
202
|
: Option.some({
|
|
@@ -253,6 +262,39 @@ const checkRequiredAttributes = (
|
|
|
253
262
|
return { issues, unverifiable }
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
/* ============================================================
|
|
266
|
+
Check 5 — anchor selector resolution. UNVERIFIABLE, deliberately.
|
|
267
|
+
|
|
268
|
+
No crawl exists, so whether a selector matches anything in the host's
|
|
269
|
+
painted DOM cannot be checked from here. The honest report is one
|
|
270
|
+
unverifiable entry per distinct selector among the rendered anchors —
|
|
271
|
+
never a green row, never a stored guess (the write-mode plan's D5).
|
|
272
|
+
The verifier lands with the crawl, under this same check id.
|
|
273
|
+
============================================================ */
|
|
274
|
+
|
|
275
|
+
const checkAnchors = (
|
|
276
|
+
rendered: ReadonlyArray<ResolvedPlacement>,
|
|
277
|
+
): PlacementCheckReport => {
|
|
278
|
+
const selectors = Array.dedupe(
|
|
279
|
+
Array.getSomes(
|
|
280
|
+
Array.map(rendered, placement =>
|
|
281
|
+
placement.slotId === ANCHOR_SLOT_ID
|
|
282
|
+
? placement.selector
|
|
283
|
+
: Option.none<string>(),
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
return {
|
|
288
|
+
issues: [],
|
|
289
|
+
unverifiable: selectors.map(
|
|
290
|
+
(selector): UnverifiableCheck => ({
|
|
291
|
+
check: 'anchor-selector-unresolved',
|
|
292
|
+
reason: `no crawl has run — whether "${selector}" matches anything in the host cannot be verified`,
|
|
293
|
+
}),
|
|
294
|
+
),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
256
298
|
/* ============================================================
|
|
257
299
|
Checks 4 and 7 — the same dot twice on the route. Warning / note.
|
|
258
300
|
|
|
@@ -396,15 +438,95 @@ const checkWeightBudget = (
|
|
|
396
438
|
})
|
|
397
439
|
}
|
|
398
440
|
|
|
441
|
+
/* ============================================================
|
|
442
|
+
Checks 8 and 9 — the generated-layout widening
|
|
443
|
+
(`wiki/plans/active/microdots-platform-pages-generated-layout.md` §E).
|
|
444
|
+
DIAGNOSTICS, not runtime enforcement: no renderer refuses the state
|
|
445
|
+
either check reports, and both claim only what a manifest can make
|
|
446
|
+
verifiable — with no manifest they are silent (the existing check-2
|
|
447
|
+
unverifiable entry already covers that state; a second entry would
|
|
448
|
+
repeat it).
|
|
449
|
+
============================================================ */
|
|
450
|
+
|
|
451
|
+
/* Check 8 — over slot capacity. Warning.
|
|
452
|
+
|
|
453
|
+
Fires ONCE per overflowing slot, on the first placement past the declared
|
|
454
|
+
capacity in stacking order. The claim is DISAGREEMENT between the
|
|
455
|
+
declaration and the placements — deliberately not "will not render", so
|
|
456
|
+
the sentence stays true whether or not any renderer ever enforces
|
|
457
|
+
capacity (today none does; the generated host stacks everything). */
|
|
458
|
+
|
|
459
|
+
const checkSlotCapacity = (
|
|
460
|
+
rendered: ReadonlyArray<ResolvedPlacement>,
|
|
461
|
+
slotManifest: HostSlotManifest | undefined,
|
|
462
|
+
): PlacementCheckReport => {
|
|
463
|
+
if (slotManifest === undefined) return { issues: [], unverifiable: [] }
|
|
464
|
+
const issues = Array.getSomes(
|
|
465
|
+
slotManifest.slots.map((slot): Option.Option<PlacementIssue> => {
|
|
466
|
+
if (slot.capacity === undefined) return Option.none()
|
|
467
|
+
const capacity = slot.capacity
|
|
468
|
+
const occupants = Array.sort(
|
|
469
|
+
Array.filter(rendered, placement => placement.slotId === slot.id),
|
|
470
|
+
Order.mapInput(Order.Number, (placement: ResolvedPlacement) =>
|
|
471
|
+
Option.getOrElse(placement.order, () => 0),
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
if (occupants.length <= capacity) return Option.none()
|
|
475
|
+
const first = occupants[capacity]
|
|
476
|
+
if (first === undefined) return Option.none()
|
|
477
|
+
return Option.some({
|
|
478
|
+
check: 'over-slot-capacity',
|
|
479
|
+
severity: 'warning',
|
|
480
|
+
placementId: first.id,
|
|
481
|
+
consequence: `Slot ${slot.id} declares capacity ${String(capacity)} and ${String(occupants.length)} placements resolve into it. The declaration and the placements disagree; nothing enforces capacity at runtime, so all ${String(occupants.length)} render.`,
|
|
482
|
+
action: 'Move a placement to another slot, or raise the capacity',
|
|
483
|
+
})
|
|
484
|
+
}),
|
|
485
|
+
)
|
|
486
|
+
return { issues, unverifiable: [] }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Check 9 — a span on a placement in a non-grid slot. Note.
|
|
490
|
+
|
|
491
|
+
The legal residue of a kind edit: spans go dormant on grid→band and
|
|
492
|
+
return if the slot becomes a grid again, so this is stated as dormancy,
|
|
493
|
+
never as an error demanding cleanup. */
|
|
494
|
+
|
|
495
|
+
const checkSpanInNonGrid = (
|
|
496
|
+
rendered: ReadonlyArray<ResolvedPlacement>,
|
|
497
|
+
slotManifest: HostSlotManifest | undefined,
|
|
498
|
+
): PlacementCheckReport => {
|
|
499
|
+
if (slotManifest === undefined) return { issues: [], unverifiable: [] }
|
|
500
|
+
const kinds = new Map(
|
|
501
|
+
slotManifest.slots.map(slot => [slot.id, slot.kind] as const),
|
|
502
|
+
)
|
|
503
|
+
const issues = Array.getSomes(
|
|
504
|
+
Array.map(rendered, (placement): Option.Option<PlacementIssue> => {
|
|
505
|
+
if (Option.isNone(placement.span)) return Option.none()
|
|
506
|
+
const kind = kinds.get(placement.slotId)
|
|
507
|
+
if (kind === undefined || kind === 'grid') return Option.none()
|
|
508
|
+
return Option.some({
|
|
509
|
+
check: 'span-in-non-grid',
|
|
510
|
+
severity: 'note',
|
|
511
|
+
placementId: placement.id,
|
|
512
|
+
consequence: `A ${String(placement.span.value)}/12 span is set but ${placement.slotId} is a ${kind} slot, so the span is dormant. It returns the moment the slot becomes a grid again.`,
|
|
513
|
+
action: 'No action needed',
|
|
514
|
+
})
|
|
515
|
+
}),
|
|
516
|
+
)
|
|
517
|
+
return { issues, unverifiable: [] }
|
|
518
|
+
}
|
|
519
|
+
|
|
399
520
|
/* ============================================================
|
|
400
521
|
The engine.
|
|
401
522
|
============================================================ */
|
|
402
523
|
|
|
403
524
|
/**
|
|
404
|
-
* Runs
|
|
405
|
-
* inputs, same report. Placements
|
|
406
|
-
* any check runs — they render for
|
|
407
|
-
* are checked like any other, the
|
|
525
|
+
* Runs all seven checks over a resolution's output — check 5 as its
|
|
526
|
+
* unverifiable reporter only. Pure: same inputs, same report. Placements
|
|
527
|
+
* overridden ENTIRELY are excluded before any check runs — they render for
|
|
528
|
+
* nobody — while partially-overridden ones are checked like any other, the
|
|
529
|
+
* reading-B qualifier.
|
|
408
530
|
*/
|
|
409
531
|
export const checkPlacements = (
|
|
410
532
|
resolved: ReadonlyArray<ResolvedPlacement>,
|
|
@@ -419,7 +541,10 @@ export const checkPlacements = (
|
|
|
419
541
|
checkSlotInTheme(rendered, context.slotManifest),
|
|
420
542
|
checkRequiredAttributes(rendered, context.manifests),
|
|
421
543
|
checkTwins(rendered),
|
|
544
|
+
checkAnchors(rendered),
|
|
422
545
|
checkWeightBudget(rendered, context),
|
|
546
|
+
checkSlotCapacity(rendered, context.slotManifest),
|
|
547
|
+
checkSpanInNonGrid(rendered, context.slotManifest),
|
|
423
548
|
]
|
|
424
549
|
return {
|
|
425
550
|
issues: Array.flatMap(reports, report => report.issues),
|