@12ui/design 0.2.48 → 0.2.49

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.
Files changed (119) hide show
  1. package/SKILL.md +1 -1
  2. package/dist/branch-page-conversion.d.ts +4 -1
  3. package/dist/branch-page-conversion.d.ts.map +1 -1
  4. package/dist/branch-page-conversion.js +27 -17
  5. package/dist/branch-page-conversion.js.map +1 -1
  6. package/dist/branch-run-record.d.ts +2 -0
  7. package/dist/branch-run-record.d.ts.map +1 -1
  8. package/dist/branch-run-record.js.map +1 -1
  9. package/dist/cli-draft-command.d.ts +11 -5
  10. package/dist/cli-draft-command.d.ts.map +1 -1
  11. package/dist/cli-draft-command.js +37 -35
  12. package/dist/cli-draft-command.js.map +1 -1
  13. package/dist/cli-help/design.d.ts +10 -0
  14. package/dist/cli-help/design.d.ts.map +1 -1
  15. package/dist/cli-help/design.js +29 -0
  16. package/dist/cli-help/design.js.map +1 -1
  17. package/dist/cli-help/index.d.ts +11 -0
  18. package/dist/cli-help/index.d.ts.map +1 -1
  19. package/dist/cli-help/index.js +18 -2
  20. package/dist/cli-help/index.js.map +1 -1
  21. package/dist/cli-next-command.d.ts.map +1 -1
  22. package/dist/cli-next-command.js +26 -14
  23. package/dist/cli-next-command.js.map +1 -1
  24. package/dist/cli-progress.d.ts +2 -0
  25. package/dist/cli-progress.d.ts.map +1 -1
  26. package/dist/cli-progress.js +2 -0
  27. package/dist/cli-progress.js.map +1 -1
  28. package/dist/cli-redesign-command.d.ts +42 -0
  29. package/dist/cli-redesign-command.d.ts.map +1 -0
  30. package/dist/cli-redesign-command.js +520 -0
  31. package/dist/cli-redesign-command.js.map +1 -0
  32. package/dist/cli-run.d.ts.map +1 -1
  33. package/dist/cli-run.js +8 -0
  34. package/dist/cli-run.js.map +1 -1
  35. package/dist/cli-usage.d.ts.map +1 -1
  36. package/dist/cli-usage.js +8 -1
  37. package/dist/cli-usage.js.map +1 -1
  38. package/dist/draft-dispatch-attempt.d.ts +0 -7
  39. package/dist/draft-dispatch-attempt.d.ts.map +1 -1
  40. package/dist/draft-dispatch-attempt.js +1 -25
  41. package/dist/draft-dispatch-attempt.js.map +1 -1
  42. package/dist/draft-identity.d.ts +2 -0
  43. package/dist/draft-identity.d.ts.map +1 -1
  44. package/dist/draft-identity.js +1 -0
  45. package/dist/draft-identity.js.map +1 -1
  46. package/dist/draft-run.d.ts +2 -0
  47. package/dist/draft-run.d.ts.map +1 -1
  48. package/dist/draft-run.js +4 -3
  49. package/dist/draft-run.js.map +1 -1
  50. package/dist/package-client.d.ts +11 -0
  51. package/dist/package-client.d.ts.map +1 -1
  52. package/dist/package-client.js.map +1 -1
  53. package/dist/package-submission.d.ts +12 -0
  54. package/dist/package-submission.d.ts.map +1 -1
  55. package/dist/package-submission.js +5 -0
  56. package/dist/package-submission.js.map +1 -1
  57. package/dist/redesign-brand-asset-inventory.d.ts +71 -0
  58. package/dist/redesign-brand-asset-inventory.d.ts.map +1 -0
  59. package/dist/redesign-brand-asset-inventory.js +318 -0
  60. package/dist/redesign-brand-asset-inventory.js.map +1 -0
  61. package/dist/redesign-capture.d.ts +57 -0
  62. package/dist/redesign-capture.d.ts.map +1 -0
  63. package/dist/redesign-capture.js +181 -0
  64. package/dist/redesign-capture.js.map +1 -0
  65. package/dist/redesign-pinned-asset.d.ts +20 -0
  66. package/dist/redesign-pinned-asset.d.ts.map +1 -0
  67. package/dist/redesign-pinned-asset.js +35 -0
  68. package/dist/redesign-pinned-asset.js.map +1 -0
  69. package/dist/redesign-plan-client.d.ts +49 -0
  70. package/dist/redesign-plan-client.d.ts.map +1 -0
  71. package/dist/redesign-plan-client.js +90 -0
  72. package/dist/redesign-plan-client.js.map +1 -0
  73. package/dist/redesign-plan-identity.d.ts +8 -0
  74. package/dist/redesign-plan-identity.d.ts.map +1 -0
  75. package/dist/redesign-plan-identity.js +16 -0
  76. package/dist/redesign-plan-identity.js.map +1 -0
  77. package/dist/redesign-plan.d.ts +30 -0
  78. package/dist/redesign-plan.d.ts.map +1 -0
  79. package/dist/redesign-plan.js +99 -0
  80. package/dist/redesign-plan.js.map +1 -0
  81. package/dist/redesign-workspace.d.ts +142 -0
  82. package/dist/redesign-workspace.d.ts.map +1 -0
  83. package/dist/redesign-workspace.js +82 -0
  84. package/dist/redesign-workspace.js.map +1 -0
  85. package/dist/run-journal-commands.d.ts.map +1 -1
  86. package/dist/run-journal-commands.js +10 -0
  87. package/dist/run-journal-commands.js.map +1 -1
  88. package/open-design.json +2 -2
  89. package/package.json +2 -2
  90. package/prototype-kit/build.mjs +9 -3
  91. package/prototype-kit/lib/action-target.mjs +26 -0
  92. package/prototype-kit/lib/local-document.mjs +16 -0
  93. package/prototype-kit/lib/runtime-check.mjs +83 -19
  94. package/prototype-kit/lib/shell-runtime-equivalence.mjs +41 -0
  95. package/prototype-kit/polish/harm.mjs +27 -5
  96. package/prototype-kit/polish/measure.mjs +101 -5
  97. package/prototype-kit/polish/overrides.mjs +32 -3
  98. package/prototype-kit/polish/polish.mjs +23 -11
  99. package/prototype-kit/polish/rules/align.mjs +73 -3
  100. package/prototype-kit/polish/rules/left-rail.mjs +2 -3
  101. package/prototype-kit/polish/rules/section-breathing.mjs +0 -1
  102. package/prototype-kit/polish/tests/fixtures/align/expect.json +1 -1
  103. package/prototype-kit/polish/tests/fixtures/align/fire.html +4 -1
  104. package/prototype-kit/polish/tests/fixtures/align/silent.html +8 -0
  105. package/prototype-kit/polish/tests/fixtures/align/zoom-constrained.html +17 -0
  106. package/prototype-kit/polish/tests/fixtures/harm/left-rail-contained.html +16 -0
  107. package/prototype-kit/polish/tests/fixtures/harm/left-rail-overflow.html +16 -0
  108. package/prototype-kit/polish/tests/fixtures/harm/nested-actions.html +6 -0
  109. package/prototype-kit/polish/tests/fixtures/harm/unidentified-parent.html +3 -0
  110. package/prototype-kit/polish/tests/fixtures/shell-seam/fire.html +1 -1
  111. package/prototype-kit/polish/tests/harm-check.mjs +131 -2
  112. package/prototype-kit/polish/tests/rules-check.mjs +7 -2
  113. package/prototype-kit/runtime/proto.js +4 -1
  114. package/prototype-kit/tests/adversarial-runtime.mjs +3 -0
  115. package/prototype-kit/tests/align-rule-check.mjs +105 -0
  116. package/prototype-kit/tests/fixtures/interaction-runtime/review-bottom-action.html +8 -0
  117. package/prototype-kit/tests/offline-check.mjs +17 -1
  118. package/prototype-kit/tests/rules-check.mjs +67 -1
  119. package/prototype-kit/tests/shell-stitching-runtime.mjs +15 -8
@@ -0,0 +1,105 @@
1
+ import assert from 'node:assert/strict';
2
+ import * as align from '../polish/rules/align.mjs';
3
+
4
+ const rect = (left, top, width, height) => ({ left, top, width, height, right: left + width, bottom: top + height });
5
+
6
+ export function checkAlignRule() {
7
+ const previousDocument = globalThis.document;
8
+ const previousGetComputedStyle = globalThis.getComputedStyle;
9
+ const elements = new Map();
10
+ const section = { getBoundingClientRect: () => rect(0, 0, 1536, 1024) };
11
+ const defaultStyle = {
12
+ position: 'static', left: 'auto', right: 'auto', top: 'auto', bottom: 'auto', width: 'auto', height: 'auto',
13
+ backgroundColor: 'rgba(0, 0, 0, 0)', backgroundImage: 'none',
14
+ borderTopWidth: '0px', borderRightWidth: '0px', borderBottomWidth: '0px', borderLeftWidth: '0px',
15
+ };
16
+ const element = (id, box, style = {}, { tagName = 'DIV', classes = [] } = {}) => {
17
+ const classSet = new Set(classes);
18
+ const el = {
19
+ id, tagName, parentElement: null, children: [], box, style: { ...defaultStyle, ...style },
20
+ classList: { contains: (name) => classSet.has(name) },
21
+ getBoundingClientRect: () => el.box,
22
+ closest: (selector) => selector.startsWith('section') ? section : null,
23
+ querySelectorAll: () => [],
24
+ contains: (candidate) => candidate === el,
25
+ matches: (selector) => (tagName === 'BUTTON' && selector.includes('button'))
26
+ || (classSet.has('button') && selector.includes('.button'))
27
+ || (classSet.has('badge') && selector.includes('.badge'))
28
+ || (classSet.has('chip') && selector.includes('.chip'))
29
+ || (classSet.has('circle') && selector.includes('.circle')),
30
+ };
31
+ elements.set(id, el);
32
+ return el;
33
+ };
34
+ const parent = (id, box, children) => {
35
+ const el = element(id, box);
36
+ el.children = children;
37
+ for (const child of children) child.parentElement = el;
38
+ return el;
39
+ };
40
+ const run = (alignment) => align.browserFn()({ metrics: { alignment, columns: [] } });
41
+
42
+ globalThis.document = { getElementById: (id) => elements.get(id) || null, body: section };
43
+ globalThis.getComputedStyle = (el) => el.style;
44
+ try {
45
+ const card1 = element('card-1', rect(0, 0, 100, 80), { position: 'relative', top: '0px', height: '80px' });
46
+ const card2 = element('card-2', rect(120, 1.02, 100, 80), { position: 'relative', top: '1.02px', height: '80px' });
47
+ const card3 = element('card-3', rect(240, 2.05, 100, 80), { position: 'relative', top: '2.05px', height: '80px' });
48
+ parent('stray-row', rect(0, 0, 400, 180), [card1, card2, card3]);
49
+ const fire = run([
50
+ { axis: 'top', a: 'card-1', b: 'card-2', delta: 1.02 },
51
+ { axis: 'top', a: 'card-1', b: 'card-3', delta: 2.05 },
52
+ ]);
53
+ assert.deepEqual(fire.map((override) => override.id), ['card-2', 'card-3'], 'must fire: genuine strays in an edge row still snap');
54
+
55
+ elements.clear();
56
+ const button1 = element('centered-button-1', rect(0, 0, 40, 40), { backgroundColor: 'rgb(220, 230, 210)' }, { tagName: 'BUTTON', classes: ['button'] });
57
+ const icon1 = element('centered-icon-1', rect(12, 12, 16, 16), { position: 'absolute', top: '12px', height: '16px' }, { classes: ['icon'] });
58
+ const button2 = element('centered-circle-2', rect(72, 0, 44, 44), { backgroundColor: 'rgb(220, 230, 210)' }, { classes: ['circle'] });
59
+ const icon2 = element('centered-icon-2', rect(86, 14, 16, 16), { position: 'absolute', top: '14px', height: '16px' }, { classes: ['icon'] });
60
+ parent('centered-controls', rect(0, 0, 116, 44), [button1, icon1, button2, icon2]);
61
+ const silent = run([{ axis: 'top', a: 'centered-icon-1', b: 'centered-icon-2', delta: 2 }]);
62
+ assert.deepEqual(silent, [], 'must stay silent: a centered icon keeps its button/circle relationship');
63
+
64
+ elements.clear();
65
+ const anchorButton = element('anchor-button', rect(0, 0, 40, 40), {}, { tagName: 'BUTTON', classes: ['button'] });
66
+ const centeredAnchor = element('centered-anchor-icon', rect(12, 12, 16, 16), { position: 'absolute', top: '12px', height: '16px' }, { classes: ['icon'] });
67
+ const ordinaryStray = element('ordinary-stray-icon', rect(80, 14, 16, 16), { position: 'absolute', top: '14px', height: '16px' }, { classes: ['icon'] });
68
+ parent('mixed-controls', rect(0, 0, 116, 44), [anchorButton, centeredAnchor, ordinaryStray]);
69
+ assert.deepEqual(
70
+ run([{ axis: 'top', a: 'centered-anchor-icon', b: 'ordinary-stray-icon', delta: 2 }]).map((override) => override.id),
71
+ ['ordinary-stray-icon'],
72
+ 'must fire: an uncontained stray still snaps to a centered anchor',
73
+ );
74
+
75
+ elements.clear();
76
+ const boundaryAnchor = element('boundary-anchor-icon', rect(12, 12, 16, 16), { position: 'absolute', top: '12px', height: '16px' }, { classes: ['icon'] });
77
+ const boundaryTarget = element('boundary-target-icon', rect(86, 14, 16, 16), { position: 'absolute', top: '14px', height: '16px' }, { classes: ['icon'] });
78
+ const boundaryButton = element('boundary-button', rect(72, 0, 44, 45.024), {}, { tagName: 'BUTTON', classes: ['button'] });
79
+ parent('boundary-controls', rect(0, 0, 116, 46), [boundaryAnchor, boundaryButton, boundaryTarget]);
80
+ assert.deepEqual(
81
+ run([{ axis: 'top', a: 'boundary-anchor-icon', b: 'boundary-target-icon', delta: 2 }]),
82
+ [],
83
+ 'must stay silent: 1.024px inset imbalance is inside the measured centered cluster',
84
+ );
85
+
86
+ boundaryButton.box = rect(72, 0, 44, 46.048);
87
+ const outsideBoundary = run([{ axis: 'top', a: 'boundary-anchor-icon', b: 'boundary-target-icon', delta: 2 }]);
88
+ assert.deepEqual(
89
+ outsideBoundary.map((override) => override.id),
90
+ ['boundary-target-icon'],
91
+ 'must fire: 2.048px inset imbalance is outside the measured centered cluster',
92
+ );
93
+
94
+ elements.clear();
95
+ const box1 = element('constrained-box-1', rect(80, 20, 100, 80), { position: 'absolute', top: '20px', bottom: '20px', width: '100px', height: '80px' });
96
+ const box2 = element('constrained-box-2', rect(220, 22, 100, 80), { position: 'absolute', top: '22px', bottom: '18px', width: '100px', height: '80px' });
97
+ parent('constrained-row', rect(0, 0, 600, 120), [box1, box2]);
98
+ const constrained = run([{ axis: 'top', a: 'constrained-box-1', b: 'constrained-box-2', delta: 2 }]);
99
+ assert.equal(constrained.length, 1, 'must fire: a genuine constrained vertical stray still snaps');
100
+ assert.deepEqual(constrained[0].css, { top: '20px', bottom: '20px' }, 'must stay silent: align shifts paired insets and does not freeze height');
101
+ } finally {
102
+ globalThis.document = previousDocument;
103
+ globalThis.getComputedStyle = previousGetComputedStyle;
104
+ }
105
+ }
@@ -0,0 +1,8 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><link rel="stylesheet" href="../../../runtime/proto.css"><style>
2
+ html,body{margin:0;width:1536px;height:1024px}.page{position:relative;width:1536px;height:1024px}.proto-review-framing{position:fixed;right:18px;bottom:18px;z-index:2147483646;width:430px;height:170px;background:#14161a}.control{position:absolute;left:1246px;top:921px;width:215px;height:59px}
3
+ </style></head><body>
4
+ <aside id="proto-review-framing" class="proto-review-framing">Prototype review</aside>
5
+ <main class="page"><button id="review-bottom-modal" class="control" data-proto-action="modal" data-proto-modal='{"title":"Analyze my site?","body":"Confirm analysis.","confirm":"Analyze my site"}'>Analyze my site</button></main>
6
+ <script id="proto-state" type="application/json">{"unit":"review-bottom-action","tokens":{},"contentFile":null}</script>
7
+ <script src="../../../runtime/proto.js"></script>
8
+ </body></html>
@@ -4,13 +4,29 @@ import { createHash } from 'node:crypto';
4
4
  import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
+ import { parseHTML } from 'linkedom';
7
8
  import { reviewFramingMarkup } from '../lib/review-framing.mjs';
9
+ import { removeRemoteHeadLinks } from '../lib/local-document.mjs';
8
10
  import { buildPrototype } from './lib/run-process.mjs';
9
11
 
10
12
  const testsDir = path.dirname(new URL(import.meta.url).pathname);
11
13
  const kitDir = path.resolve(testsDir, '..');
12
14
  const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'proto-kit-offline-'));
13
15
 
16
+ const localDocument = parseHTML([
17
+ '<html><head>',
18
+ '<link rel="preconnect" href="https://fonts.example">',
19
+ '<link rel="stylesheet" href="//cdn.example/font.css">',
20
+ '<link rel="stylesheet" href="proto.css">',
21
+ '</head></html>',
22
+ ].join('')).document;
23
+ assert.equal(removeRemoteHeadLinks(localDocument), 2, 'must fire: direct-file prototypes remove remote head links');
24
+ assert.deepEqual(
25
+ [...localDocument.querySelectorAll('head link')].map((link) => link.getAttribute('href')),
26
+ ['proto.css'],
27
+ 'must stay silent: prototype-local stylesheets survive offline sanitization',
28
+ );
29
+
14
30
  function buildFixture(name) {
15
31
  const runDir = path.join(testsDir, 'fixtures', name);
16
32
  return buildPrototype({ kitDir, run: { runDir }, runWorkDir: path.join(scratch, name) });
@@ -44,7 +60,7 @@ const outputDigest = (directory) => {
44
60
  };
45
61
  assert.equal(
46
62
  outputDigest(threeShell.protoDir),
47
- '0c2147df5bc6552016cc278faec7abaabecbc7f5945cad7e1ac9b1df6e554403',
63
+ '0a24a0c220106a1592598f1ad11685081337cb60d3044953e56e11e239dbc8bd',
48
64
  'must stay silent: review support leaves the accepted shell/navigation complete output byte-identical',
49
65
  );
50
66
  const threeShellModel = JSON.parse(fs.readFileSync(threeShell.modelFile, 'utf8'));
@@ -2,11 +2,13 @@
2
2
  import assert from 'node:assert/strict';
3
3
  import { parseHTML } from 'linkedom';
4
4
  import { resolveDropdownAction, suppressDeterministicAction, usableDropdownOptions } from '../lib/actions.mjs';
5
- import { restoreActionPointerReachability } from '../lib/action-target.mjs';
5
+ import { restoreActionPointerReachability, selectUniqueActionOwners } from '../lib/action-target.mjs';
6
+ import { checkAlignRule } from './align-rule-check.mjs';
6
7
  import { declaredActiveMarkers, linkedNavForUnit, markerOverlapsNav } from '../lib/nav-active-state.mjs';
7
8
  import { matchPlannedPage } from '../lib/navigation.mjs';
8
9
  import { needsCanonicalShell, selectCanonicalShellPage } from '../lib/shell-consistency.mjs';
9
10
  import { isolateTransplantedShell, shellElementsForSourceId } from '../lib/shell-identity.mjs';
11
+ import { compareCanonicalShellSamples, SHELL_RUNTIME_GEOMETRY_EPSILON_PX } from '../lib/shell-runtime-equivalence.mjs';
10
12
  import { stitchUnitShells } from '../lib/shell-stitching.mjs';
11
13
  import { compareCanonicalShells, isWithinShell, qualifiesShellCandidate, selectOneShellPerRegion, shouldSuppressDroppedShell, shouldTransplantShell } from '../lib/shells.mjs';
12
14
  import { slugText } from '../lib/text.mjs';
@@ -69,6 +71,68 @@ const reachableBefore = reachableDropdown.outerHTML;
69
71
  assert.equal(restoreActionPointerReachability(reachableDropdown), false, 'must stay silent: an already reachable dropdown target is unchanged');
70
72
  assert.equal(reachableDropdown.outerHTML, reachableBefore);
71
73
 
74
+ const repeatedDropdown = { action: 'dropdown', options: ['All opportunities', 'High intent', 'Low intent'] };
75
+ const distinctDropdown = { action: 'dropdown', options: ['All countries', 'Australia', 'Canada'] };
76
+ const landsFilterDocument = parseHTML(`<div id="filters"><button id="filter-1"></button><button id="filter-2"></button><button id="filter-3"></button><button id="filter-4"></button><button id="filter-5"></button><button id="filter-6"></button></div>`).document;
77
+ const landsFilterActions = new Map([
78
+ ['filters', repeatedDropdown],
79
+ ['filter-1', repeatedDropdown],
80
+ ['filter-2', distinctDropdown],
81
+ ['filter-3', distinctDropdown],
82
+ ['filter-4', distinctDropdown],
83
+ ['filter-5', distinctDropdown],
84
+ ['filter-6', distinctDropdown],
85
+ ]);
86
+ const uniqueLandsFilters = selectUniqueActionOwners(landsFilterDocument, landsFilterActions);
87
+ assert.deepEqual(uniqueLandsFilters.removed, [{ wrapperId: 'filters', ownerIds: ['filter-1', 'filter-2', 'filter-3', 'filter-4', 'filter-5', 'filter-6'], action: 'dropdown' }], 'must fire: the six-child Ghost Sitemap filter shape gives the wrapper action to its terminal controls');
88
+ assert.deepEqual([...uniqueLandsFilters.actions.keys()], ['filter-1', 'filter-2', 'filter-3', 'filter-4', 'filter-5', 'filter-6'], 'must stay silent: every native filter control remains independently actionable');
89
+ const landsModalDocument = parseHTML('<div id="submit"><button id="submit-button"></button></div>').document;
90
+ const landsModalActions = new Map([
91
+ ['submit', { action: 'modal', modal: { title: 'Analyze my site?', confirm: 'Analyze my site' } }],
92
+ ['submit-button', { action: 'modal', modal: { title: 'Analyze my site?', confirm: 'Analyze my site' } }],
93
+ ]);
94
+ const uniqueLandsModal = selectUniqueActionOwners(landsModalDocument, landsModalActions);
95
+ assert.deepEqual(uniqueLandsModal.removed, [{ wrapperId: 'submit', ownerIds: ['submit-button'], action: 'modal' }], 'must fire: the One Question modal wrapper yields to its terminal button');
96
+ assert.deepEqual([...uniqueLandsModal.actions.keys()], ['submit-button']);
97
+ const territoryFilterDocument = parseHTML('<aside id="filter-bar"><button id="filter-a"></button><button id="filter-b"></button><button id="filter-c"></button><button id="filter-d"></button></aside>').document;
98
+ const territoryFilterActions = new Map([
99
+ ['filter-bar', { action: 'dropdown', options: ['All', 'Open', 'Closed'] }],
100
+ ['filter-a', { action: 'dropdown', options: ['All territories', 'East', 'West'] }],
101
+ ['filter-b', { action: 'dropdown', options: ['All visits', 'High', 'Low'] }],
102
+ ['filter-c', { action: 'dropdown', options: ['All claims', 'Claimed', 'Open'] }],
103
+ ['filter-d', { action: 'dropdown', options: ['Sort', 'Visits', 'Fit'] }],
104
+ ]);
105
+ const uniqueTerritoryFilters = selectUniqueActionOwners(territoryFilterDocument, territoryFilterActions);
106
+ assert.deepEqual(uniqueTerritoryFilters.removed, [{ wrapperId: 'filter-bar', ownerIds: ['filter-a', 'filter-b', 'filter-c', 'filter-d'], action: 'dropdown' }], 'must fire: the Territory Map four-button aside yields to every terminal dropdown even when their option payloads differ');
107
+ assert.deepEqual([...uniqueTerritoryFilters.actions.keys()], ['filter-a', 'filter-b', 'filter-c', 'filter-d']);
108
+ const distinctNestedDocument = parseHTML('<div id="parent"><button id="child"></button></div>').document;
109
+ const distinctNestedActions = new Map([
110
+ ['parent', { action: 'dropdown', options: ['One', 'Two', 'Three'] }],
111
+ ['child', { action: 'modal', modal: { title: 'Child' } }],
112
+ ]);
113
+ const uniqueDistinctNested = selectUniqueActionOwners(distinctNestedDocument, distinctNestedActions);
114
+ assert.deepEqual([...uniqueDistinctNested.actions.keys()], ['parent', 'child'], 'must stay silent: distinct nested actions retain the runtime child-wins contract');
115
+
116
+ const shellSample = ({ visualOffset = 0, shellOffset = 0, appearanceSignature = 'same' } = {}) => ({
117
+ appearanceSignature,
118
+ visualGeometry: [[0, 0, 390, 72], [20 + visualOffset, 20, 100, 28]],
119
+ shellGeometry: [shellOffset, 0, 390, 72],
120
+ });
121
+ assert.equal(SHELL_RUNTIME_GEOMETRY_EPSILON_PX, 0.5, 'fixture contract: canonical shell geometry tolerance is half a CSS pixel');
122
+ const subpixelShellComparison = compareCanonicalShellSamples(shellSample(), shellSample({ visualOffset: 0.4 }));
123
+ assert.equal(subpixelShellComparison.equivalent, true, 'must stay silent: a sub-half-pixel glyph measurement drift is visually equivalent');
124
+ assert.ok(Math.abs(subpixelShellComparison.maxGeometryDelta - 0.4) < 1e-9, 'fixture contract: the comparator reports the measured subpixel drift');
125
+ assert.deepEqual(compareCanonicalShellSamples(shellSample(), shellSample({ visualOffset: 24 })), {
126
+ equivalent: false,
127
+ reason: 'geometry',
128
+ maxGeometryDelta: 24,
129
+ }, 'must fire: the b13-class displaced header descendant remains a canonical-shell mismatch');
130
+ assert.deepEqual(compareCanonicalShellSamples(shellSample(), shellSample({ appearanceSignature: 'changed' })), {
131
+ equivalent: false,
132
+ reason: 'appearance',
133
+ maxGeometryDelta: null,
134
+ }, 'must fire: geometry tolerance never conceals structural or style differences');
135
+
72
136
  const headerNav = { containerId: 'nav', region: 'header', semantic: true, items: 8, share: 0.8, box: [0, 40, 1536, 76] };
73
137
  const announcement = { containerId: 'announcement', region: 'header', semantic: false, items: 3, share: 1, box: [0, 0, 1536, 40] };
74
138
  const footer = { containerId: 'footer', region: 'footer', semantic: true, items: 5, share: 0.8, box: [0, 900, 1536, 124] };
@@ -205,4 +269,6 @@ assert.equal(fireResult.bands[0].regions[0].active.navText, 'Menu');
205
269
  assert.equal(fireResult.bands[0].regions[0].shellId, isolatedHeader.id);
206
270
  assert.equal(fireResult.bands[0].regions[0].active.markerId, isolatedMarker.id);
207
271
 
272
+ checkAlignRule();
273
+
208
274
  process.stdout.write('rules: ok\n');
@@ -8,9 +8,9 @@ import { checkRuntime } from '../lib/runtime-check.mjs';
8
8
  import { startStaticServer } from '../lib/static-server.mjs';
9
9
  import { buildPrototype } from './lib/run-process.mjs';
10
10
 
11
- const expectFailure = process.argv.includes('--expect-failure');
12
- const unexpectedArgs = process.argv.slice(2).filter((arg) => arg !== '--expect-failure');
13
- if (unexpectedArgs.length > 0) throw new Error(`usage: shell-stitching-runtime.mjs [--expect-failure]`);
11
+ const reintroduceDefect = process.argv.includes('--reintroduce-defect');
12
+ const unexpectedArgs = process.argv.slice(2).filter((arg) => arg !== '--reintroduce-defect');
13
+ if (unexpectedArgs.length > 0) throw new Error(`usage: shell-stitching-runtime.mjs [--reintroduce-defect]`);
14
14
 
15
15
  const testsDir = path.dirname(new URL(import.meta.url).pathname);
16
16
  const kitDir = path.resolve(testsDir, '..');
@@ -49,6 +49,13 @@ try {
49
49
  bandHeight(inventory, 'p2-canvas-control', 'mobile-390'),
50
50
  ], [900, 1400], 'fixture contract: the portrait control has a substantially taller canvas');
51
51
 
52
+ if (reintroduceDefect) {
53
+ const collisionFile = path.join(built.protoDir, 'id-collision.html');
54
+ const collisionHtml = fs.readFileSync(collisionFile, 'utf8');
55
+ const displaced = '<style id="reintroduced-b13-defect">@media(max-width:600px){[data-proto-shell-source-id="mobile-390-header-brand"]{transform:translateX(24px)!important}}</style>';
56
+ fs.writeFileSync(collisionFile, collisionHtml.replace('</head>', `${displaced}</head>`));
57
+ }
58
+
52
59
  server = await startStaticServer(runWorkDir);
53
60
  browser = await chromium.launch();
54
61
  const audit = await checkRuntime({
@@ -77,11 +84,11 @@ try {
77
84
  assert.equal(collision1536.actualSignature, donor1536.actualSignature, 'must stay silent: distinct native identities render the same canonical header');
78
85
 
79
86
  const groups = audit.shellConsistencyFailures.map((failure) => failure.group);
80
- if (expectFailure) {
81
- assert.deepEqual(groups, ['390:mobile-390:header'], 'pre-fix evidence must isolate one canonical collision to the mobile header');
82
- assert.notEqual(collision390.actualSignature, donor390.actualSignature, 'must fire before the fix: recipient offsets alter transplanted donor descendants');
87
+ if (reintroduceDefect) {
88
+ assert.deepEqual(groups, ['390:mobile-390:header'], 'reintroduced b13 evidence must isolate one canonical collision to the mobile header');
89
+ assert.notEqual(collision390.actualSignature, donor390.actualSignature, 'must fire with the defect present: a 24px recipient displacement alters transplanted donor descendants');
83
90
  const failure = audit.shellConsistencyFailures[0];
84
- assert.equal(failure.reason, 'prototype-shell-signature-mismatch', 'pre-fix evidence is the rendered canonical-shell mismatch');
91
+ assert.equal(failure.reason, 'prototype-shell-signature-mismatch', 'reintroduced evidence is the rendered canonical-shell mismatch');
85
92
  assert.deepEqual(failure.pages, [
86
93
  { file: 'donor.html', signature: donor390.actualSignature, geometry: donor390.actualGeometry },
87
94
  { file: 'id-collision.html', signature: collision390.actualSignature, geometry: collision390.actualGeometry },
@@ -92,7 +99,7 @@ try {
92
99
  }
93
100
 
94
101
  process.stdout.write(JSON.stringify({
95
- expectation: expectFailure ? 'pre-fix-failure' : 'post-fix-pass',
102
+ expectation: reintroduceDefect ? 'reintroduced-defect-failure' : 'post-fix-pass',
96
103
  groups,
97
104
  mobile: {
98
105
  donor: donor390.actualSignature,