@storystick/preview 0.0.0-stage → 0.2.0-rc.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.
Files changed (245) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +55 -0
  3. package/README.md +42 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/authoring/runtime-contract.d.ts +89 -0
  6. package/dist/authoring/runtime-contract.js +28 -0
  7. package/dist/channel-taxonomy.d.ts +17 -0
  8. package/dist/channel-taxonomy.js +46 -0
  9. package/dist/context-command.d.ts +18 -0
  10. package/dist/context-command.js +38 -0
  11. package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
  12. package/dist/dev/suspicious-codeword-detector.js +220 -0
  13. package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
  14. package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
  15. package/dist/framework/browser-context.d.ts +2 -0
  16. package/dist/framework/browser-context.js +2 -0
  17. package/dist/framework/generation-refresh.d.ts +23 -0
  18. package/dist/framework/generation-refresh.js +82 -0
  19. package/dist/framework/generation-signal.d.ts +8 -0
  20. package/dist/framework/generation-signal.js +17 -0
  21. package/dist/framework/projection-generation-watch.d.ts +5 -0
  22. package/dist/framework/projection-generation-watch.js +53 -0
  23. package/dist/framework/render-receipt.d.ts +8 -0
  24. package/dist/framework/render-receipt.js +10 -0
  25. package/dist/framework/runtime-adapter-store.d.ts +30 -0
  26. package/dist/framework/runtime-adapter-store.js +81 -0
  27. package/dist/grant-coordinator.d.ts +53 -0
  28. package/dist/grant-coordinator.js +329 -0
  29. package/dist/grant-lifecycle.d.ts +52 -0
  30. package/dist/grant-lifecycle.js +74 -0
  31. package/dist/identity/__test-adapter.d.ts +34 -0
  32. package/dist/identity/__test-adapter.js +59 -0
  33. package/dist/identity/browser-context-gate.d.ts +4 -0
  34. package/dist/identity/browser-context-gate.js +22 -0
  35. package/dist/identity/browser-context.d.ts +13 -0
  36. package/dist/identity/browser-context.js +22 -0
  37. package/dist/identity/carriers/postmessage.d.ts +84 -0
  38. package/dist/identity/carriers/postmessage.js +177 -0
  39. package/dist/identity/carriers/storage-state.d.ts +41 -0
  40. package/dist/identity/carriers/storage-state.js +99 -0
  41. package/dist/identity/carriers/storage.d.ts +39 -0
  42. package/dist/identity/carriers/storage.js +182 -0
  43. package/dist/identity/carriers/url.d.ts +19 -0
  44. package/dist/identity/carriers/url.js +38 -0
  45. package/dist/identity/identity-state-machine.d.ts +70 -0
  46. package/dist/identity/identity-state-machine.js +244 -0
  47. package/dist/identity/identity-types.d.ts +192 -0
  48. package/dist/identity/identity-types.js +22 -0
  49. package/dist/identity/index.d.ts +18 -0
  50. package/dist/identity/index.js +16 -0
  51. package/dist/identity/preview-cookie.d.ts +9 -0
  52. package/dist/identity/preview-cookie.js +12 -0
  53. package/dist/identity/preview-draft.d.ts +6 -0
  54. package/dist/identity/preview-draft.js +25 -0
  55. package/dist/identity/preview-identity.d.ts +28 -0
  56. package/dist/identity/preview-identity.js +84 -0
  57. package/dist/identity/preview-scope.d.ts +28 -0
  58. package/dist/identity/preview-scope.js +47 -0
  59. package/dist/identity/resolver.d.ts +110 -0
  60. package/dist/identity/resolver.js +334 -0
  61. package/dist/identity/session-query.d.ts +4 -0
  62. package/dist/identity/session-query.js +19 -0
  63. package/dist/index.d.ts +90 -0
  64. package/dist/index.js +788 -0
  65. package/dist/interaction-mode.d.ts +58 -0
  66. package/dist/interaction-mode.js +93 -0
  67. package/dist/nav-handler.d.ts +95 -0
  68. package/dist/nav-handler.js +84 -0
  69. package/dist/overlay-label.d.ts +79 -0
  70. package/dist/overlay-label.js +73 -0
  71. package/dist/provenance/anchor-map.d.ts +95 -0
  72. package/dist/provenance/anchor-map.js +177 -0
  73. package/dist/provenance/carrier-plan.d.ts +51 -0
  74. package/dist/provenance/carrier-plan.js +90 -0
  75. package/dist/provenance/coded-text.d.ts +20 -0
  76. package/dist/provenance/coded-text.js +302 -0
  77. package/dist/provenance/direct-edit-locator.d.ts +29 -0
  78. package/dist/provenance/direct-edit-locator.js +218 -0
  79. package/dist/provenance/editable-leaf.d.ts +134 -0
  80. package/dist/provenance/editable-leaf.js +483 -0
  81. package/dist/provenance/editor-target.d.ts +29 -0
  82. package/dist/provenance/editor-target.js +46 -0
  83. package/dist/provenance/field-leaf-walker.d.ts +21 -0
  84. package/dist/provenance/field-leaf-walker.js +98 -0
  85. package/dist/provenance/frame-dependencies.d.ts +212 -0
  86. package/dist/provenance/frame-dependencies.js +550 -0
  87. package/dist/provenance/index.d.ts +26 -0
  88. package/dist/provenance/index.js +26 -0
  89. package/dist/provenance/local-events.d.ts +1 -0
  90. package/dist/provenance/local-events.js +1 -0
  91. package/dist/provenance/markers.d.ts +21 -0
  92. package/dist/provenance/markers.js +70 -0
  93. package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
  94. package/dist/provenance/multi-frame-anchor-map.js +109 -0
  95. package/dist/provenance/normalize.d.ts +2 -0
  96. package/dist/provenance/normalize.js +19 -0
  97. package/dist/provenance/overlay-focus-echo.d.ts +17 -0
  98. package/dist/provenance/overlay-focus-echo.js +16 -0
  99. package/dist/provenance/overlay-projection.d.ts +65 -0
  100. package/dist/provenance/overlay-projection.js +201 -0
  101. package/dist/provenance/owner-map.d.ts +55 -0
  102. package/dist/provenance/owner-map.js +239 -0
  103. package/dist/provenance/read-carrier.d.ts +22 -0
  104. package/dist/provenance/read-carrier.js +92 -0
  105. package/dist/provenance/recorder.d.ts +38 -0
  106. package/dist/provenance/recorder.js +82 -0
  107. package/dist/provenance/render-context.d.ts +106 -0
  108. package/dist/provenance/render-context.js +338 -0
  109. package/dist/provenance/reveal-contract.d.ts +44 -0
  110. package/dist/provenance/reveal-contract.js +1 -0
  111. package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
  112. package/dist/provenance/richtext-slice-embed-address.js +35 -0
  113. package/dist/provenance/runtime-script.d.ts +1 -0
  114. package/dist/provenance/runtime-script.js +2217 -0
  115. package/dist/provenance/sidecar-store.d.ts +63 -0
  116. package/dist/provenance/sidecar-store.js +151 -0
  117. package/dist/provenance/sidecar.d.ts +34 -0
  118. package/dist/provenance/sidecar.js +227 -0
  119. package/dist/provenance/slice-model.d.ts +28 -0
  120. package/dist/provenance/slice-model.js +86 -0
  121. package/dist/provenance/source-identity.d.ts +3 -0
  122. package/dist/provenance/source-identity.js +7 -0
  123. package/dist/provenance/source-map-materializer.d.ts +76 -0
  124. package/dist/provenance/source-map-materializer.js +286 -0
  125. package/dist/provenance/source-registry.d.ts +103 -0
  126. package/dist/provenance/source-registry.js +562 -0
  127. package/dist/provenance/transformed-output-gate.d.ts +161 -0
  128. package/dist/provenance/transformed-output-gate.js +171 -0
  129. package/dist/provenance/types.d.ts +102 -0
  130. package/dist/provenance/types.js +1 -0
  131. package/dist/refresh-policy.d.ts +11 -0
  132. package/dist/refresh-policy.js +14 -0
  133. package/dist/reveal-target.d.ts +8 -0
  134. package/dist/reveal-target.js +47 -0
  135. package/dist/route-paths.d.ts +11 -0
  136. package/dist/route-paths.js +14 -0
  137. package/dist/router-subscriber.d.ts +92 -0
  138. package/dist/router-subscriber.js +44 -0
  139. package/dist/runtime/anchor-types.d.ts +5 -0
  140. package/dist/runtime/anchor-types.js +1 -0
  141. package/dist/runtime/authority-store.d.ts +31 -0
  142. package/dist/runtime/authority-store.js +56 -0
  143. package/dist/runtime/boot.d.ts +52 -0
  144. package/dist/runtime/boot.js +379 -0
  145. package/dist/runtime/content-refresh.d.ts +30 -0
  146. package/dist/runtime/content-refresh.js +68 -0
  147. package/dist/runtime/emit-runtime-error.d.ts +51 -0
  148. package/dist/runtime/emit-runtime-error.js +64 -0
  149. package/dist/runtime/field-proxy.d.ts +112 -0
  150. package/dist/runtime/field-proxy.js +471 -0
  151. package/dist/runtime/form-contract-reporter.d.ts +99 -0
  152. package/dist/runtime/form-contract-reporter.js +391 -0
  153. package/dist/runtime/invalidation-adapter.d.ts +17 -0
  154. package/dist/runtime/invalidation-adapter.js +42 -0
  155. package/dist/runtime/live-query-overlay.d.ts +39 -0
  156. package/dist/runtime/live-query-overlay.js +345 -0
  157. package/dist/runtime/momentary-navigate.d.ts +16 -0
  158. package/dist/runtime/momentary-navigate.js +106 -0
  159. package/dist/runtime/observation-scheduler.d.ts +15 -0
  160. package/dist/runtime/observation-scheduler.js +42 -0
  161. package/dist/runtime/perf-metrics.d.ts +28 -0
  162. package/dist/runtime/perf-metrics.js +90 -0
  163. package/dist/runtime/preview-interaction-policy.d.ts +1 -0
  164. package/dist/runtime/preview-interaction-policy.js +103 -0
  165. package/dist/runtime/preview-overlay-painter.d.ts +27 -0
  166. package/dist/runtime/preview-overlay-painter.js +1732 -0
  167. package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
  168. package/dist/runtime/preview-overlay-sidecar.js +45 -0
  169. package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
  170. package/dist/runtime/preview-perspective-alignment.js +40 -0
  171. package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
  172. package/dist/runtime/preview-perspective-intent-state.js +5 -0
  173. package/dist/runtime/preview-transition-state.d.ts +142 -0
  174. package/dist/runtime/preview-transition-state.js +257 -0
  175. package/dist/runtime/render-mode-state.d.ts +51 -0
  176. package/dist/runtime/render-mode-state.js +76 -0
  177. package/dist/runtime/request-id.d.ts +4 -0
  178. package/dist/runtime/request-id.js +8 -0
  179. package/dist/runtime/resolve-schema-field.d.ts +99 -0
  180. package/dist/runtime/resolve-schema-field.js +356 -0
  181. package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
  182. package/dist/runtime/runtime-error-dispatch.js +75 -0
  183. package/dist/runtime/schema-store.d.ts +121 -0
  184. package/dist/runtime/schema-store.js +667 -0
  185. package/dist/runtime/snapshot-assembly.d.ts +104 -0
  186. package/dist/runtime/snapshot-assembly.js +553 -0
  187. package/dist/runtime/snapshot-reporter.d.ts +55 -0
  188. package/dist/runtime/snapshot-reporter.js +375 -0
  189. package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
  190. package/dist/runtime/standalone-overlay-controller.js +281 -0
  191. package/dist/runtime/store.d.ts +307 -0
  192. package/dist/runtime/store.js +1026 -0
  193. package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
  194. package/dist/runtime/windowed-minimap-projection.js +189 -0
  195. package/dist/runtime/workspace-shortcuts.d.ts +30 -0
  196. package/dist/runtime/workspace-shortcuts.js +188 -0
  197. package/dist/sdk-error-copy.d.ts +48 -0
  198. package/dist/sdk-error-copy.js +182 -0
  199. package/dist/select-cursor.d.ts +2 -0
  200. package/dist/select-cursor.js +23 -0
  201. package/dist/server/admission-cookie.d.ts +38 -0
  202. package/dist/server/admission-cookie.js +122 -0
  203. package/dist/server/control-client.d.ts +29 -0
  204. package/dist/server/control-client.js +22 -0
  205. package/dist/server/control-errors.d.ts +22 -0
  206. package/dist/server/control-errors.js +165 -0
  207. package/dist/server/draft-route.d.ts +12 -0
  208. package/dist/server/draft-route.js +45 -0
  209. package/dist/server/extract-token.d.ts +30 -0
  210. package/dist/server/extract-token.js +98 -0
  211. package/dist/server/grant.d.ts +128 -0
  212. package/dist/server/grant.js +395 -0
  213. package/dist/server/ordinary-read-provenance.d.ts +11 -0
  214. package/dist/server/ordinary-read-provenance.js +124 -0
  215. package/dist/server/preview-read-metadata.d.ts +10 -0
  216. package/dist/server/preview-read-metadata.js +27 -0
  217. package/dist/server/provenance-frame-builder.d.ts +53 -0
  218. package/dist/server/provenance-frame-builder.js +102 -0
  219. package/dist/server/provenance-runtime.d.ts +70 -0
  220. package/dist/server/provenance-runtime.js +46 -0
  221. package/dist/server/read-context-command.d.ts +11 -0
  222. package/dist/server/read-context-command.js +53 -0
  223. package/dist/server/read-identity.d.ts +3 -0
  224. package/dist/server/read-identity.js +63 -0
  225. package/dist/server/read-lifecycle.d.ts +17 -0
  226. package/dist/server/read-lifecycle.js +52 -0
  227. package/dist/server/render-frame-provenance.d.ts +133 -0
  228. package/dist/server/render-frame-provenance.js +676 -0
  229. package/dist/server/render-id.d.ts +15 -0
  230. package/dist/server/render-id.js +22 -0
  231. package/dist/server/route-document-provenance.d.ts +77 -0
  232. package/dist/server/route-document-provenance.js +1327 -0
  233. package/dist/server/route-session-cookie.d.ts +33 -0
  234. package/dist/server/route-session-cookie.js +54 -0
  235. package/dist/session-projections.d.ts +33 -0
  236. package/dist/session-projections.js +43 -0
  237. package/dist/session-subscriber.d.ts +124 -0
  238. package/dist/session-subscriber.js +79 -0
  239. package/dist/sha256-fallback.d.ts +30 -0
  240. package/dist/sha256-fallback.js +126 -0
  241. package/dist/types.d.ts +446 -0
  242. package/dist/types.js +85 -0
  243. package/dist/validators.d.ts +61 -0
  244. package/dist/validators.js +110 -0
  245. package/package.json +206 -3
@@ -0,0 +1,2217 @@
1
+ import { installPreviewWorkspaceShortcuts, previewWorkspaceCommandIds, } from '../runtime/workspace-shortcuts.js';
2
+ import { decodePreviewReadCarrier, selectPreviewPrimaryPage } from './read-carrier.js';
3
+ import { PREVIEW_PROVENANCE_ANCHOR_MAP_EVENT } from './local-events.js';
4
+ // Utility consumers of the provenance entry point must not retain the injected
5
+ // runtime. Constructing this string is pure; only its explicit consumer runs it.
6
+ export const previewProvenanceRuntimeScript = /* @__PURE__ */ (() => String.raw `
7
+ (() => {
8
+ const decodeReadCarrier = ${decodePreviewReadCarrier.toString()}
9
+ const selectPrimaryPage = ${selectPreviewPrimaryPage.toString()}
10
+ const LOCAL_ANCHOR_MAP_EVENT = ${JSON.stringify(PREVIEW_PROVENANCE_ANCHOR_MAP_EVENT)}
11
+ // This script is served by the SDK endpoint, including the framework basePath.
12
+ const runtimePath = document.currentScript?.src ? new URL(document.currentScript.src).pathname : ''
13
+ const runtimeBasePath = runtimePath.endsWith('/api/storystick/provenance-runtime')
14
+ ? runtimePath.slice(0, -'/api/storystick/provenance-runtime'.length) : ''
15
+ if (window.__ssPreviewProvenanceRuntimeInstalled) return
16
+ window.__ssPreviewProvenanceRuntimeInstalled = true
17
+ const installWorkspaceShortcuts = ${installPreviewWorkspaceShortcuts.toString()}
18
+ installWorkspaceShortcuts(window, () => {
19
+ const state = window.__storystick_preview_runtime__?.state
20
+ return state && !state.previewEnded ? state.identity : null
21
+ }, ${JSON.stringify(previewWorkspaceCommandIds)})
22
+ // Missing required anchors remain invalid. Poll slowly between real DOM
23
+ // invalidations, with a wall-clock deadline independent of display refresh.
24
+ const READY_DEADLINE_MS = 4000
25
+ const READY_RETRY_MS = 100
26
+ // Work counters belong to this Document, not to each provenance frame.
27
+ // The existing manual probe exposes totals; they never drive readiness.
28
+ const observationStats = { readCarrierScans: 0, anchorCollections: 0, codedTextScans: 0, readyPasses: 0, publications: 0 }
29
+ let readyFrame = null
30
+ let readyRetryTimer = null
31
+ let readyWork = null
32
+ let readyPassRunning = false
33
+ let documentActive = true
34
+ let cancelEpoch = 1
35
+ let iframeGeneration = 1
36
+ let currentRenderIdentity = { generation: 1, sourceOfTruth: 'iframe' }
37
+ let currentInteractionMode = 'edit'
38
+ let lastMomentaryNavigateKeyActive = null
39
+ let pendingNavigationEpoch = null
40
+ let lastPublisherRevision = 0
41
+ let activePublisherRoute = null
42
+ let nativeViewAnchor = null
43
+ let lastPublishedSnapshot = null
44
+ let lastRenderedContextKey = null
45
+ const publisherFramesById = new Map()
46
+ const readCarrierCache = new Map()
47
+ let readCarrierFrames = []
48
+ let primaryPageCandidates = []
49
+ let lastPrimaryPagesKey = null
50
+ let readCarrierSignature = ''
51
+ let readCarriersDirty = true
52
+ let publisherSnapshotDirty = false
53
+ const consumedPublisherElements = new WeakSet()
54
+ const pendingOccurrenceRemovals = new Map()
55
+ const PUBLISHER_CONSUMED_EVENT = 'storystick.preview.provenance-publisher-consumed'
56
+ const PUBLISHER_CONSUMED_REGISTRY = '__ssPreviewProvenanceConsumedPublishersV2'
57
+ const PAINT_ID_ATTRIBUTE = 'data-ss-preview-paint-id'
58
+ const PAINT_TARGET_REGISTRY = '__ssPreviewPaintTargetRegistryV1'
59
+ let nextPaintId = 1
60
+ const paintIdByElement = new WeakMap()
61
+ const paintTargetById = new Map()
62
+ let provenanceObserver = null
63
+ let viewportFrame = null
64
+ let hydrationSafeDomMaterializationEnabled = false
65
+ let hydrationSafeDomMaterializationScheduled = false
66
+ let hydrationTimer = null
67
+ let hydrationFrame = null
68
+ let hydrationLoadListener = null
69
+ const DOM_HYDRATION_GRACE_MS = 2000
70
+ const CODED_TEXT_START = encodeTagAscii('ss-p:')
71
+ const CODED_TEXT_SEPARATOR = encodeTagAscii('|')
72
+ const CODED_TEXT_END = encodeTagAscii('/ss-p')
73
+ const SYNTHETIC_OVERLAY_SELECTOR =
74
+ '[data-ss-preview-field-synthetic-overlay-root],[data-ss-preview-field-synthetic-overlay-box]'
75
+ const VISIBILITY_ATTRIBUTE_FILTER = [
76
+ 'data-ss-p',
77
+ 'data-ss-e',
78
+ 'data-ss-provenance-frame',
79
+ 'data-ss-provenance-publisher',
80
+ 'data-ss-route-frame-hash',
81
+ 'data-preview-message-session-id',
82
+ 'hidden',
83
+ 'style',
84
+ 'class',
85
+ 'aria-expanded',
86
+ 'aria-hidden',
87
+ 'data-state',
88
+ 'open',
89
+ 'popover',
90
+ 'inert',
91
+ ]
92
+ const PROVENANCE_OBSERVER_OPTIONS = {
93
+ childList: true,
94
+ subtree: true,
95
+ characterData: true,
96
+ attributes: true,
97
+ attributeFilter: VISIBILITY_ATTRIBUTE_FILTER,
98
+ }
99
+
100
+ function encodeTagAscii(value) {
101
+ let encoded = ''
102
+ for (const char of value) {
103
+ const code = char.codePointAt(0)
104
+ if (code === undefined || code < 0x20 || code > 0x7e) return ''
105
+ encoded += String.fromCodePoint(0xe0000 + code)
106
+ }
107
+ return encoded
108
+ }
109
+
110
+ function decodeTagAscii(value) {
111
+ let decoded = ''
112
+ for (const char of value) {
113
+ const code = char.codePointAt(0)
114
+ if (code === undefined || code < 0xe0020 || code > 0xe007e) return ''
115
+ decoded += String.fromCodePoint(code - 0xe0000)
116
+ }
117
+ return decoded
118
+ }
119
+
120
+ function decodeCodedText(value) {
121
+ const segments = []
122
+ let text = ''
123
+ let cursor = 0
124
+ while (cursor < value.length) {
125
+ const markerStart = value.indexOf(CODED_TEXT_START, cursor)
126
+ if (markerStart < 0) {
127
+ text += value.slice(cursor)
128
+ break
129
+ }
130
+ text += value.slice(cursor, markerStart)
131
+ const encodedMarkerStart = markerStart + CODED_TEXT_START.length
132
+ const separator = value.indexOf(CODED_TEXT_SEPARATOR, encodedMarkerStart)
133
+ if (separator < 0) {
134
+ text += value.slice(markerStart)
135
+ break
136
+ }
137
+ const textStart = separator + CODED_TEXT_SEPARATOR.length
138
+ const textEnd = value.indexOf(CODED_TEXT_END, textStart)
139
+ if (textEnd < 0) {
140
+ text += value.slice(markerStart)
141
+ break
142
+ }
143
+ const carrierId = decodeTagAscii(value.slice(encodedMarkerStart, separator))
144
+ const markerId = normalizedCarrierMarkerId(carrierId)
145
+ const segmentText = value.slice(textStart, textEnd)
146
+ if (markerId.length > 0) {
147
+ segments.push({
148
+ markerId,
149
+ carrierId,
150
+ text: segmentText,
151
+ markerStart,
152
+ markerEnd: textEnd + CODED_TEXT_END.length,
153
+ textStart,
154
+ textEnd,
155
+ })
156
+ }
157
+ text += segmentText
158
+ cursor = textEnd + CODED_TEXT_END.length
159
+ }
160
+ return { text, segments }
161
+ }
162
+
163
+ function studioTargetOrigin() {
164
+ const runtime = window.__storystick_preview_runtime__
165
+ const identity = runtime && runtime.state && runtime.state.identity
166
+ const parentOrigin = identity && identity.parentOrigin
167
+ if (typeof parentOrigin === 'string' && parentOrigin.length > 0) {
168
+ try {
169
+ const parsed = new URL(parentOrigin)
170
+ if (parsed.origin === parentOrigin) return parentOrigin
171
+ } catch {}
172
+ }
173
+ // The provenance script can run before the identity handshake, and older
174
+ // SDK runtimes may not expose identity on the shared cell. The initial
175
+ // message must remain discoverable; inbound validation still locks source,
176
+ // origin, session, schema, and monotonic identity before accepting an echo.
177
+ return '*'
178
+ }
179
+
180
+ function isSyntheticOverlayNode(node) {
181
+ if (node instanceof Element) {
182
+ return node.closest(SYNTHETIC_OVERLAY_SELECTOR) !== null
183
+ }
184
+ const parent = node.parentNode
185
+ return parent instanceof Element &&
186
+ parent.closest(SYNTHETIC_OVERLAY_SELECTOR) !== null
187
+ }
188
+
189
+ function isSyntheticOverlayOnlyMutation(record) {
190
+ if (isSyntheticOverlayNode(record.target)) return true
191
+ if (record.type !== 'childList') return false
192
+ const changedNodes = [...record.addedNodes, ...record.removedNodes]
193
+ return changedNodes.length > 0 &&
194
+ changedNodes.every((node) => isSyntheticOverlayNode(node))
195
+ }
196
+
197
+ function normalizeRenderIdentity(identity) {
198
+ const generation = Number.isFinite(identity && identity.generation) && identity.generation > 0
199
+ ? identity.generation
200
+ : 1
201
+ const sourceOfTruth =
202
+ identity && (identity.sourceOfTruth === 'studio' || identity.sourceOfTruth === 'iframe' || identity.sourceOfTruth === 'system')
203
+ ? identity.sourceOfTruth
204
+ : 'system'
205
+ return {
206
+ generation,
207
+ sourceOfTruth,
208
+ ...(identity && typeof identity.commandId === 'string' && identity.commandId.length > 0
209
+ ? { commandId: identity.commandId }
210
+ : {}),
211
+ }
212
+ }
213
+
214
+ function parseSidecar(text) {
215
+ if (typeof text !== 'string' || text.trim().length === 0) return emptySidecar()
216
+ try {
217
+ const parsed = JSON.parse(text)
218
+ const dependencySet = normalizeDependencySet(parsed.dependencySet)
219
+ return {
220
+ type: 'storystick.preview.public-sidecar',
221
+ version: 1,
222
+ routeHash: typeof parsed.routeHash === 'string' ? parsed.routeHash : '',
223
+ ...(normalizeVariantId(parsed.variantId) !== null
224
+ ? { variantId: normalizeVariantId(parsed.variantId) }
225
+ : {}),
226
+ ...(normalizePreviewScope(parsed.previewScope) !== null
227
+ ? { previewScope: normalizePreviewScope(parsed.previewScope) }
228
+ : {}),
229
+ codeVersion: typeof parsed.codeVersion === 'string' ? parsed.codeVersion : '',
230
+ renderId: typeof parsed.renderId === 'string' ? parsed.renderId : '',
231
+ generation: typeof parsed.generation === 'number' ? parsed.generation : 0,
232
+ renderNonce: typeof parsed.renderNonce === 'string' ? parsed.renderNonce : '',
233
+ routeRenderNonce: typeof parsed.routeRenderNonce === 'string'
234
+ ? parsed.routeRenderNonce
235
+ : '',
236
+ emissionRevision: typeof parsed.emissionRevision === 'number'
237
+ ? parsed.emissionRevision
238
+ : 0,
239
+ ...(dependencySet ? { dependencySet } : {}),
240
+ markers: Array.isArray(parsed.markers)
241
+ ? parsed.markers.flatMap(normalizeMarker)
242
+ : [],
243
+ }
244
+ } catch {
245
+ return emptySidecar()
246
+ }
247
+ }
248
+
249
+ function normalizeMarker(marker) {
250
+ if (typeof marker !== 'object' || marker === null) return []
251
+ if (typeof marker.markerId !== 'string' || marker.markerId.length === 0) return []
252
+ const editLocator = normalizeEditLocator(marker.editLocator)
253
+ return [{
254
+ markerId: marker.markerId,
255
+ ...(editLocator ? { editLocator } : {}),
256
+ modality: typeof marker.modality === 'string' ? marker.modality : 'text',
257
+ anchorKind: typeof marker.anchorKind === 'string' ? marker.anchorKind : 'element',
258
+ anchorMultiplicity: marker.anchorMultiplicity === 'multi' ? 'multi' : undefined,
259
+ presence: marker.presence === 'optional' || marker.presence === 'expected'
260
+ ? marker.presence
261
+ : undefined,
262
+ renderedTextFingerprint: typeof marker.renderedTextFingerprint === 'string'
263
+ ? marker.renderedTextFingerprint
264
+ : undefined,
265
+ }]
266
+ }
267
+
268
+ function normalizeEditLocator(value) {
269
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
270
+ if (value.version !== 1) return null
271
+ const sourceKinds = new Set(['stored', 'binding', 'model', 'computed', 'richtext', 'embed', 'emptyFallback', 'unknown'])
272
+ if (!sourceKinds.has(value.sourceKind)) return null
273
+ const bounded = (candidate, max, allowEmpty) => {
274
+ if (typeof candidate !== 'string' || candidate.length > max) return null
275
+ const normalized = candidate.trim()
276
+ return normalized.length > 0 || allowEmpty === true ? normalized : null
277
+ }
278
+ const sourceSetId = bounded(value.sourceSetId, 512, false)
279
+ const documentId = bounded(value.documentId, 256, false)
280
+ const canonicalPath = bounded(value.canonicalPath, 4096, false)
281
+ if (!sourceSetId || !documentId || !canonicalPath) return null
282
+ const pathAnchors = normalizeLocatorAnchors(value.pathAnchors)
283
+ if (pathAnchors === null) return null
284
+ const navigationAnchors = value.navigationAnchors === undefined
285
+ ? undefined
286
+ : normalizeLocatorAnchors(value.navigationAnchors)
287
+ if (navigationAnchors === null) return null
288
+ const navigationPath = value.navigationPath === undefined
289
+ ? undefined
290
+ : bounded(value.navigationPath, 4096, false)
291
+ if (value.navigationPath !== undefined && !navigationPath) return null
292
+ const variantId = normalizeVariantId(value.variantId)
293
+ if (value.variantId !== undefined && (typeof value.variantId !== 'string' || !variantId)) return null
294
+ const textNodeIndex = Number.isInteger(value.textNodeIndex) && value.textNodeIndex >= 0
295
+ ? value.textNodeIndex
296
+ : undefined
297
+ if (value.textNodeIndex !== undefined && textNodeIndex === undefined) return null
298
+ const readScope = value.readScope === undefined
299
+ ? undefined
300
+ : normalizePreviewScope(value.readScope)
301
+ if (value.readScope !== undefined && readScope === null) return null
302
+ return {
303
+ version: 1,
304
+ sourceSetId,
305
+ documentId,
306
+ ...(variantId ? { variantId } : {}),
307
+ canonicalPath,
308
+ pathAnchors,
309
+ ...(navigationPath !== undefined ? { navigationPath } : {}),
310
+ ...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
311
+ ...(textNodeIndex !== undefined ? { textNodeIndex } : {}),
312
+ sourceKind: value.sourceKind,
313
+ ...(readScope !== undefined ? { readScope } : {}),
314
+ }
315
+ }
316
+
317
+ function normalizeLocatorAnchors(value) {
318
+ if (!Array.isArray(value) || value.length > 64) return null
319
+ const anchors = []
320
+ for (const entry of value) {
321
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return null
322
+ if (typeof entry.path !== 'string' || entry.path.length > 4096) return null
323
+ if (typeof entry.key !== 'string' || entry.key.trim().length === 0 || entry.key.length > 512) return null
324
+ anchors.push({ path: entry.path.trim(), key: entry.key.trim() })
325
+ }
326
+ return anchors
327
+ }
328
+
329
+ function normalizePreviewScope(value) {
330
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
331
+ const boundedString = (candidate, max) =>
332
+ typeof candidate === 'string' && candidate.length > 0 && candidate.length <= max
333
+ const nullableString = (candidate, max) =>
334
+ candidate === null || boundedString(candidate, max)
335
+ const validStatus =
336
+ value.releaseScopeStatus === 'none' ||
337
+ value.releaseScopeStatus === 'active' ||
338
+ value.releaseScopeStatus === 'missing' ||
339
+ value.releaseScopeStatus === 'not-previewable' ||
340
+ value.releaseScopeStatus === 'unbound'
341
+ if (
342
+ !nullableString(value.requestedVariantId, 256) ||
343
+ !nullableString(value.requestedDocumentId, 256) ||
344
+ !(value.requestedLocale === undefined || nullableString(value.requestedLocale, 128)) ||
345
+ !(value.appliedLocale === undefined || nullableString(value.appliedLocale, 128)) ||
346
+ !(value.appliesToDocument === null || typeof value.appliesToDocument === 'boolean') ||
347
+ !nullableString(value.appliedVariantId, 256) ||
348
+ !nullableString(value.requestedReleaseId, 256) ||
349
+ !validStatus ||
350
+ !(value.releaseLifecycleStatus === undefined || boundedString(value.releaseLifecycleStatus, 64)) ||
351
+ !nullableString(value.appliedReleaseId, 256) ||
352
+ !(value.releaseMemberState === undefined || value.releaseMemberState === 'active' || value.releaseMemberState === 'archived' || value.releaseMemberState === 'missing' || value.releaseMemberState === 'locale-removed') ||
353
+ !(value.degradedReason === undefined || value.degradedReason === 'archived' || value.degradedReason === 'missing' || value.degradedReason === 'locale-removed')
354
+ ) return null
355
+ return {
356
+ requestedVariantId: value.requestedVariantId,
357
+ requestedDocumentId: value.requestedDocumentId,
358
+ ...(value.requestedLocale !== undefined
359
+ ? { requestedLocale: value.requestedLocale }
360
+ : {}),
361
+ ...(value.appliedLocale !== undefined
362
+ ? { appliedLocale: value.appliedLocale }
363
+ : {}),
364
+ appliesToDocument: value.appliesToDocument,
365
+ appliedVariantId: value.appliedVariantId,
366
+ requestedReleaseId: value.requestedReleaseId,
367
+ releaseScopeStatus: value.releaseScopeStatus,
368
+ ...(typeof value.releaseLifecycleStatus === 'string'
369
+ ? { releaseLifecycleStatus: value.releaseLifecycleStatus }
370
+ : {}),
371
+ appliedReleaseId: value.appliedReleaseId,
372
+ ...(typeof value.releaseMemberState === 'string'
373
+ ? { releaseMemberState: value.releaseMemberState }
374
+ : {}),
375
+ ...(typeof value.degradedReason === 'string'
376
+ ? { degradedReason: value.degradedReason }
377
+ : {}),
378
+ }
379
+ }
380
+
381
+ function normalizeReadContext(value) {
382
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
383
+ Object.keys(value).sort().join(',') !== 'contextId,perspective,revision' ||
384
+ typeof value.contextId !== 'string' || !value.contextId || value.contextId.length > 512 ||
385
+ !Number.isSafeInteger(value.revision) || value.revision < 1) return null
386
+ const p = value.perspective
387
+ if (!p || typeof p !== 'object' || Array.isArray(p)) return null
388
+ const keys = Object.keys(p).sort().join(',')
389
+ if ((p.kind === 'living' || p.kind === 'published') && keys === 'kind') return value
390
+ const field = p.kind === 'variant' ? 'variantId' : p.kind === 'release' ? 'releaseId' : null
391
+ return field && keys === ['kind', field].sort().join(',') &&
392
+ typeof p[field] === 'string' && p[field].length > 0 && p[field].length <= 512 ? value : null
393
+ }
394
+
395
+ function authenticatedReadContext() {
396
+ const state = window.__storystick_preview_runtime__?.state
397
+ return state?.state === 'authenticated' && state?.session?.readContextStatus === 'active'
398
+ ? normalizeReadContext(state.session.readContext) : null
399
+ }
400
+
401
+ function matchesCurrentReadContext(dependencies) {
402
+ if (!dependencies?.readContext) return true
403
+ const current = authenticatedReadContext()
404
+ return current !== null && current.contextId === dependencies.readContext.contextId &&
405
+ current.revision === dependencies.readContext.revision
406
+ }
407
+
408
+ function normalizeDependencySet(value) {
409
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
410
+ if (
411
+ value.type !== 'storystick.preview.frame-dependency-set' ||
412
+ value.version !== 1 ||
413
+ (!(typeof value.sessionId === 'string' && value.sessionId.length > 0) && !normalizeReadContext(value.readContext)) ||
414
+ typeof value.routeHash !== 'string' || value.routeHash.length === 0 ||
415
+ typeof value.codeVersion !== 'string' || value.codeVersion.length === 0 ||
416
+ typeof value.frameId !== 'string' || value.frameId.length === 0 ||
417
+ typeof value.renderId !== 'string' || value.renderId.length === 0 ||
418
+ !Number.isFinite(value.generation) || value.generation <= 0 ||
419
+ !Array.isArray(value.dependencies) || value.dependencies.length > 256 ||
420
+ (value.observationId !== undefined && typeof value.observationId !== 'string') ||
421
+ (value.renderWatermark !== undefined && !Number.isFinite(value.renderWatermark))
422
+ ) return null
423
+ if (JSON.stringify(value).length > 1_000_000) return null
424
+ const readContext = value.readContext === undefined ? undefined : normalizeReadContext(value.readContext)
425
+ if (readContext === null) return null
426
+ if (value.receiptIds !== undefined && (!Array.isArray(value.receiptIds) || value.receiptIds.length > 64 ||
427
+ !value.receiptIds.every((id) => typeof id === 'string' && /^pqr_[a-f0-9]{64}$/.test(id)))) return null
428
+ const dependencies = value.dependencies.flatMap(normalizeDependency)
429
+ if (dependencies.length !== value.dependencies.length) return null
430
+ const variantId = normalizeVariantId(value.variantId)
431
+ return {
432
+ type: 'storystick.preview.frame-dependency-set',
433
+ version: 1,
434
+ ...(value.sessionId ? { sessionId: value.sessionId } : {}),
435
+ ...(readContext ? { readContext } : {}),
436
+ ...(value.receiptIds ? { receiptIds: value.receiptIds } : {}),
437
+ routeHash: value.routeHash,
438
+ codeVersion: value.codeVersion,
439
+ ...(variantId ? { variantId } : {}),
440
+ frameId: value.frameId,
441
+ renderId: value.renderId,
442
+ generation: value.generation,
443
+ ...(value.observationId !== undefined
444
+ ? { observationId: value.observationId }
445
+ : {}),
446
+ ...(value.renderWatermark !== undefined
447
+ ? { renderWatermark: value.renderWatermark }
448
+ : {}),
449
+ dependencies,
450
+ }
451
+ }
452
+
453
+ function normalizeDependency(value) {
454
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return []
455
+ const required = (candidate) =>
456
+ typeof candidate === 'string' && candidate.trim().length > 0
457
+ ? candidate.trim()
458
+ : null
459
+ switch (value.kind) {
460
+ case 'document': {
461
+ const documentId = required(value.documentId)
462
+ if (!documentId) return []
463
+ return [{
464
+ kind: 'document',
465
+ documentId,
466
+ ...(required(value.storyType) ? { storyType: required(value.storyType) } : {}),
467
+ ...(required(value.reason) ? { reason: required(value.reason) } : {}),
468
+ }]
469
+ }
470
+ case 'asset': {
471
+ const assetId = required(value.assetId)
472
+ return assetId ? [{ kind: 'asset', assetId }] : []
473
+ }
474
+ case 'route-lookup': {
475
+ const pathname = required(value.pathname)
476
+ if (!pathname) return []
477
+ const storyTypes = Array.isArray(value.storyTypes)
478
+ ? [...new Set(value.storyTypes.filter((entry) => required(entry)).map((entry) => required(entry)))]
479
+ : []
480
+ return [{ kind: 'route-lookup', pathname, ...(storyTypes.length > 0 ? { storyTypes } : {}) }]
481
+ }
482
+ case 'list': {
483
+ const storyType = required(value.storyType)
484
+ if (!storyType) return []
485
+ const orderBy = value.orderBy &&
486
+ required(value.orderBy.field) &&
487
+ (value.orderBy.direction === 'asc' || value.orderBy.direction === 'desc')
488
+ ? { field: required(value.orderBy.field), direction: value.orderBy.direction }
489
+ : undefined
490
+ return [{
491
+ kind: 'list',
492
+ storyType,
493
+ ...(value.where !== undefined ? { where: value.where } : {}),
494
+ ...(orderBy ? { orderBy } : {}),
495
+ ...(typeof value.limit === 'number' && Number.isFinite(value.limit)
496
+ ? { limit: value.limit }
497
+ : {}),
498
+ }]
499
+ }
500
+ case 'get-by-slug':
501
+ case 'get-by-pathname': {
502
+ const storyType = required(value.storyType)
503
+ const field = value.kind === 'get-by-slug' ? 'slug' : 'pathname'
504
+ const target = required(value[field])
505
+ return storyType && target
506
+ ? [{ kind: value.kind, storyType, [field]: target }]
507
+ : []
508
+ }
509
+ case 'batch-get': {
510
+ if (!Array.isArray(value.documentIds)) return []
511
+ const documentIds = [...new Set(value.documentIds
512
+ .filter((entry) => required(entry))
513
+ .map((entry) => required(entry)))]
514
+ return documentIds.length > 0 ? [{ kind: 'batch-get', documentIds }] : []
515
+ }
516
+ case 'template': {
517
+ const templateId = required(value.templateId)
518
+ return templateId ? [{ kind: 'template', templateId }] : []
519
+ }
520
+ case 'custom-read': {
521
+ const key = required(value.key)
522
+ return key ? [{ kind: 'custom-read', key }] : []
523
+ }
524
+ default:
525
+ return []
526
+ }
527
+ }
528
+
529
+ function emptySidecar() {
530
+ return {
531
+ type: 'storystick.preview.public-sidecar',
532
+ version: 1,
533
+ routeHash: '',
534
+ codeVersion: '',
535
+ renderId: '',
536
+ generation: 0,
537
+ renderNonce: '',
538
+ routeRenderNonce: '',
539
+ emissionRevision: 0,
540
+ markers: [],
541
+ }
542
+ }
543
+
544
+ function normalizeText(value) {
545
+ return value
546
+ .normalize('NFKD')
547
+ .replace(/\p{Diacritic}/gu, '')
548
+ .replace(/[\u201c\u201d]/g, '"')
549
+ .replace(/[\u2018\u2019]/g, "'")
550
+ .replace(/\s+/g, ' ')
551
+ .trim()
552
+ .toLowerCase()
553
+ }
554
+
555
+ function fingerprintRenderedText(value) {
556
+ const normalized = normalizeText(value)
557
+ let hash = 0x811c9dc5
558
+ for (let index = 0; index < normalized.length; index++) {
559
+ hash ^= normalized.charCodeAt(index)
560
+ hash = Math.imul(hash, 0x01000193) >>> 0
561
+ }
562
+ return 'txt:' + normalized.length + ':' + hash.toString(36)
563
+ }
564
+
565
+ function compareSnapshotOrder(left, right) {
566
+ if (left.generation !== right.generation) {
567
+ return left.generation < right.generation ? -1 : 1
568
+ }
569
+ if (left.emissionRevision === right.emissionRevision) return 0
570
+ return left.emissionRevision < right.emissionRevision ? -1 : 1
571
+ }
572
+
573
+ function validSnapshot(snapshot) {
574
+ if (!snapshot || typeof snapshot !== 'object') return false
575
+ if (
576
+ snapshot.type !== 'storystick.preview.provenance-sidecar-snapshot' ||
577
+ snapshot.version !== 2 ||
578
+ typeof snapshot.sessionId !== 'string' || snapshot.sessionId.length === 0 ||
579
+ typeof snapshot.routeHash !== 'string' ||
580
+ typeof snapshot.codeVersion !== 'string' ||
581
+ typeof snapshot.renderId !== 'string' ||
582
+ typeof snapshot.renderNonce !== 'string' || snapshot.renderNonce.length === 0 ||
583
+ !Number.isFinite(snapshot.generation) || snapshot.generation <= 0 ||
584
+ !Number.isFinite(snapshot.emissionRevision) || snapshot.emissionRevision <= 0 ||
585
+ (snapshot.operation !== 'replace' && snapshot.operation !== 'tombstone') ||
586
+ snapshot.complete !== true ||
587
+ !Array.isArray(snapshot.expectedFrameIds) ||
588
+ !Array.isArray(snapshot.frames)
589
+ ) return false
590
+ if (snapshot.operation === 'tombstone') {
591
+ return snapshot.expectedFrameIds.length === 0 && snapshot.frames.length === 0
592
+ }
593
+ const expected = new Set(snapshot.expectedFrameIds)
594
+ if (expected.size !== snapshot.expectedFrameIds.length || !expected.has('route')) return false
595
+ const received = new Set()
596
+ for (const frame of snapshot.frames) {
597
+ if (!frame || typeof frame !== 'object' || typeof frame.frameId !== 'string') return false
598
+ if (received.has(frame.frameId)) return false
599
+ const sidecar = frame.sidecar
600
+ if (
601
+ !sidecar || typeof sidecar !== 'object' ||
602
+ sidecar.type !== 'storystick.preview.public-sidecar' ||
603
+ sidecar.version !== 1 ||
604
+ sidecar.codeVersion !== snapshot.codeVersion ||
605
+ !Array.isArray(sidecar.markers) ||
606
+ typeof sidecar.renderNonce !== 'string' || sidecar.renderNonce.length === 0 ||
607
+ !Number.isFinite(sidecar.emissionRevision) || sidecar.emissionRevision <= 0 ||
608
+ (frame.frameId === 'route' && (
609
+ sidecar.routeHash !== snapshot.routeHash ||
610
+ sidecar.renderId !== snapshot.renderId ||
611
+ sidecar.renderNonce !== snapshot.renderNonce
612
+ ))
613
+ ) return false
614
+ received.add(frame.frameId)
615
+ }
616
+ return received.size === expected.size && [...expected].every((id) => received.has(id))
617
+ }
618
+
619
+ function installSidecarStore() {
620
+ const existing = window.__ssPreviewProvenanceSidecarStoreV2
621
+ if (existing && existing.version === 2) return existing
622
+ let active = null
623
+ let latestAccepted = null
624
+ const store = {
625
+ version: 2,
626
+ accept(snapshot) {
627
+ if (!validSnapshot(snapshot)) return false
628
+ if (latestAccepted !== null) {
629
+ const order = compareSnapshotOrder(snapshot, latestAccepted)
630
+ if (order < 0) return false
631
+ if (order === 0) {
632
+ if (snapshot.renderNonce !== latestAccepted.renderNonce) return false
633
+ if (snapshot.operation === 'tombstone') return true
634
+ }
635
+ }
636
+ latestAccepted = snapshot
637
+ active = snapshot.operation === 'tombstone' ? null : snapshot
638
+ return true
639
+ },
640
+ readSnapshot: () => active,
641
+ readFrames: () => active ? active.frames : [],
642
+ }
643
+ window.__ssPreviewProvenanceSidecarStoreV2 = store
644
+ return store
645
+ }
646
+
647
+ const sidecarStore = installSidecarStore()
648
+
649
+ function readCarrier(value) {
650
+ if (typeof value !== 'string' || !value.startsWith('ssr1.')) return null
651
+ if (readCarrierCache.has(value)) return readCarrierCache.get(value)
652
+ const decoded = decodeReadCarrier(value)
653
+ const sidecar = decoded ? parseSidecar(JSON.stringify(decoded.sidecar)) : null
654
+ const frame = sidecar?.dependencySet && sidecar.markers.length === 1 &&
655
+ (!sidecar.dependencySet.readContext || sidecar.dependencySet.receiptIds?.length > 0)
656
+ ? { frameId: decoded.frameId, sidecar, primaryPage: decoded.primaryPage } : null
657
+ if (readCarrierCache.size >= 2048) readCarrierCache.delete(readCarrierCache.keys().next().value)
658
+ readCarrierCache.set(value, frame)
659
+ return frame
660
+ }
661
+
662
+ function normalizedCarrierMarkerId(value) {
663
+ return readCarrier(value)?.sidecar.markers[0].markerId || value
664
+ }
665
+
666
+ // Only explicitly encoded text/attributes participate. These frames describe
667
+ // what is in the DOM now, so removed branches need no React unmount publisher.
668
+ function collectReadCarrierFrames() {
669
+ observationStats.readCarrierScans += 1
670
+ const carriers = new Set()
671
+ const root = document.body || document.documentElement
672
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
673
+ let node = walker.nextNode()
674
+ while (node) {
675
+ if (node instanceof Text && node.data.includes(CODED_TEXT_START) &&
676
+ !node.parentElement?.closest('script,style,template,noscript,textarea,title,option')) {
677
+ // The closing delimiter can be in another text node (e.g. an <em>
678
+ // inside a heading). The complete prefix alone carries read identity.
679
+ let cursor = 0
680
+ while (cursor < node.data.length) {
681
+ const start = node.data.indexOf(CODED_TEXT_START, cursor)
682
+ if (start < 0) break
683
+ const separator = node.data.indexOf(CODED_TEXT_SEPARATOR, start + CODED_TEXT_START.length)
684
+ if (separator < 0) break
685
+ const id = decodeTagAscii(node.data.slice(start + CODED_TEXT_START.length, separator))
686
+ if (id.startsWith('ssr1.')) carriers.add(id)
687
+ cursor = separator + CODED_TEXT_SEPARATOR.length
688
+ }
689
+ }
690
+ node = walker.nextNode()
691
+ }
692
+ for (const element of document.querySelectorAll('[data-ss-e],[data-ss-p]')) {
693
+ for (const name of ['data-ss-e', 'data-ss-p']) {
694
+ const value = element.getAttribute(name)
695
+ if (value?.startsWith('ssr1.')) carriers.add(value)
696
+ }
697
+ }
698
+ const signature = [...carriers].sort().join('|')
699
+ if (signature === readCarrierSignature) return false
700
+ readCarrierSignature = signature
701
+ const frames = new Map()
702
+ primaryPageCandidates = []
703
+ for (const value of carriers) {
704
+ const frame = readCarrier(value)
705
+ if (!frame) continue
706
+ if (frame.primaryPage) primaryPageCandidates.push({ primaryPage: frame.primaryPage, dependencySet: frame.sidecar.dependencySet })
707
+ const existing = frames.get(frame.frameId)
708
+ const sidecar = !existing || frame.sidecar.emissionRevision > existing.sidecar.emissionRevision
709
+ ? frame.sidecar : existing.sidecar
710
+ const markers = new Map((existing?.sidecar.markers || []).map(marker => [marker.markerId, marker]))
711
+ for (const marker of frame.sidecar.markers) markers.set(marker.markerId, marker)
712
+ frames.set(frame.frameId, { frameId: frame.frameId, sidecar: { ...sidecar, markers: [...markers.values()] } })
713
+ }
714
+ readCarrierFrames = [...frames.values()]
715
+ return true
716
+ }
717
+
718
+ function readFrames() {
719
+ const stored = sidecarStore.readFrames().filter((frame) => matchesCurrentReadContext(frame.sidecar.dependencySet))
720
+ if (stored.length > 0) return stored
721
+ const framesById = new Map()
722
+ for (const script of document.querySelectorAll('script[data-ss-provenance-frame]')) {
723
+ const frameId = script.getAttribute('data-ss-provenance-frame') || ''
724
+ if (frameId.length === 0) continue
725
+ framesById.set(frameId, {
726
+ frameId,
727
+ sidecar: parseSidecar(script.textContent),
728
+ })
729
+ }
730
+ return Array.from(framesById.values())
731
+ }
732
+
733
+ function routeFrameHash() {
734
+ const storedRoute = sidecarStore.readFrames().find((frame) => frame.frameId === 'route')
735
+ if (storedRoute) return storedRoute.sidecar.routeHash
736
+ const route = document.querySelector('[data-ss-route-frame-hash]')
737
+ return route && route.getAttribute('data-ss-route-frame-hash') || ''
738
+ }
739
+
740
+ function parsePublisher(element) {
741
+ const raw = element.content && typeof element.content.textContent === 'string'
742
+ ? element.content.textContent
743
+ : element.textContent
744
+ if (typeof raw !== 'string' || raw.trim().length === 0) return null
745
+ try {
746
+ const publisher = JSON.parse(raw)
747
+ if (
748
+ !publisher || typeof publisher !== 'object' ||
749
+ publisher.type !== 'storystick.preview.provenance-publisher' ||
750
+ publisher.version !== 2 ||
751
+ !publisher.frame || typeof publisher.frame !== 'object' ||
752
+ typeof publisher.frame.frameId !== 'string' || publisher.frame.frameId.length === 0
753
+ ) return null
754
+ const sidecar = parseSidecar(JSON.stringify(publisher.frame.sidecar))
755
+ if (
756
+ sidecar.routeHash.length === 0 ||
757
+ sidecar.codeVersion.length === 0 ||
758
+ sidecar.renderId.length === 0 ||
759
+ sidecar.renderNonce.length === 0 ||
760
+ sidecar.generation <= 0 ||
761
+ sidecar.emissionRevision <= 0
762
+ ) return null
763
+ const dependencySet = sidecar.dependencySet
764
+ const boundSession = sessionId()
765
+ if (dependencySet?.readContext) {
766
+ if (!authenticatedReadContext() || !boundSession ||
767
+ !dependencySet.receiptIds?.length) return null
768
+ sidecar.dependencySet = { ...dependencySet, sessionId: boundSession }
769
+ } else if (typeof publisher.sessionId !== 'string' || !publisher.sessionId) return null
770
+ return {
771
+ sessionId: dependencySet?.readContext ? boundSession : publisher.sessionId,
772
+ frame: { frameId: publisher.frame.frameId, sidecar },
773
+ }
774
+ } catch {
775
+ return null
776
+ }
777
+ }
778
+
779
+ function publisherConsumptionKey(frame) {
780
+ return JSON.stringify([frame.frameId, frame.sidecar.renderNonce])
781
+ }
782
+
783
+ function acknowledgePublisherConsumption(frame) {
784
+ const publisherKey = publisherConsumptionKey(frame)
785
+ const current = window[PUBLISHER_CONSUMED_REGISTRY]
786
+ const registry = current && typeof current === 'object' ? current : {}
787
+ registry[publisherKey] = true
788
+ const keys = Object.keys(registry)
789
+ for (let index = 0; index < keys.length - 128; index++) {
790
+ delete registry[keys[index]]
791
+ }
792
+ window[PUBLISHER_CONSUMED_REGISTRY] = registry
793
+ try {
794
+ window.dispatchEvent(new CustomEvent(PUBLISHER_CONSUMED_EVENT, {
795
+ detail: { publisherKey },
796
+ }))
797
+ } catch {
798
+ // The registry is the durable acknowledgement if CustomEvent is absent.
799
+ }
800
+ }
801
+
802
+ function publisherFrameIsNewer(next, current) {
803
+ if (!current) return true
804
+ if (next.sidecar.generation !== current.sidecar.generation) {
805
+ return next.sidecar.generation > current.sidecar.generation
806
+ }
807
+ if (next.sidecar.emissionRevision !== current.sidecar.emissionRevision) {
808
+ return next.sidecar.emissionRevision > current.sidecar.emissionRevision
809
+ }
810
+ return false
811
+ }
812
+
813
+ function publishRenderedContext(readContext) {
814
+ const detail = { sessionId: sessionId(), readContext }
815
+ const key = JSON.stringify(detail)
816
+ if (key === lastRenderedContextKey) return
817
+ lastRenderedContextKey = key
818
+ window.dispatchEvent(new CustomEvent('storystick.preview.rendered-context', { detail }))
819
+ }
820
+
821
+ // Read navigation metadata independently of overlay readiness/convergence.
822
+ function currentPrimaryPages() {
823
+ return authenticatedReadContext() ? primaryPageCandidates.filter(candidate =>
824
+ candidate.dependencySet?.readContext && matchesCurrentReadContext(candidate.dependencySet))
825
+ .map(candidate => candidate.primaryPage) : []
826
+ }
827
+
828
+ function publishPrimaryPages() {
829
+ const context = authenticatedReadContext()
830
+ const pages = currentPrimaryPages()
831
+ const detail = { sessionId: sessionId(), readContext: context,
832
+ pathname: window.location.pathname, pages }
833
+ const key = JSON.stringify(detail)
834
+ if (key === lastPrimaryPagesKey) return
835
+ lastPrimaryPagesKey = key
836
+ window.__ssPreviewPrimaryPages = detail
837
+ window.dispatchEvent(new CustomEvent('storystick.preview.primary-pages', { detail }))
838
+ }
839
+
840
+ // Capsules are acknowledged synchronously so their React owner can retire
841
+ // them. A repeated reconciliation can reuse the last carrier observation
842
+ // until the document changes; it never caches geometry across passes.
843
+ function consumeProvenancePublishers() {
844
+ let changed = false
845
+ for (const element of document.querySelectorAll('template[data-ss-provenance-publisher]')) {
846
+ if (consumedPublisherElements.has(element)) continue
847
+ const publisher = parsePublisher(element)
848
+ // An unbound server capsule waits for authenticated browser identity.
849
+ if (publisher === null) continue
850
+ consumedPublisherElements.add(element)
851
+ if (element.getAttribute('data-ss-provenance-react-owned') !== 'true') {
852
+ element.remove()
853
+ }
854
+ if (publisher === null) continue
855
+ const frame = publisher.frame
856
+ acknowledgePublisherConsumption(frame)
857
+ if (
858
+ frame.frameId.startsWith('slice_') &&
859
+ (
860
+ activePublisherRoute === null ||
861
+ publisher.sessionId !== activePublisherRoute.sessionId ||
862
+ frame.sidecar.routeHash !== activePublisherRoute.routeHash ||
863
+ frame.sidecar.generation !== activePublisherRoute.generation ||
864
+ frame.sidecar.codeVersion !== activePublisherRoute.codeVersion ||
865
+ frame.sidecar.routeRenderNonce !== activePublisherRoute.renderNonce
866
+ )
867
+ ) {
868
+ continue
869
+ }
870
+ const current = publisherFramesById.get(frame.frameId)
871
+ if (!publisherFrameIsNewer(frame, current)) continue
872
+ if (frame.frameId === 'route') {
873
+ const routeChanged = activePublisherRoute === null ||
874
+ activePublisherRoute.renderNonce !== frame.sidecar.renderNonce
875
+ if (routeChanged) {
876
+ const oldRouteHash = activePublisherRoute && activePublisherRoute.routeHash
877
+ if (oldRouteHash) {
878
+ for (const [frameId, registered] of publisherFramesById) {
879
+ if (registered.sidecar.routeHash === oldRouteHash) {
880
+ publisherFramesById.delete(frameId)
881
+ }
882
+ }
883
+ }
884
+ const activationGeneration = Math.max(
885
+ currentRenderIdentity.generation,
886
+ frame.sidecar.generation,
887
+ )
888
+ const activationRevision = Math.max(
889
+ lastPublisherRevision + 1,
890
+ frame.sidecar.emissionRevision,
891
+ )
892
+ sidecarStore.accept({
893
+ type: 'storystick.preview.provenance-sidecar-snapshot',
894
+ version: 2,
895
+ sessionId: publisher.sessionId,
896
+ routeHash: frame.sidecar.routeHash,
897
+ codeVersion: frame.sidecar.codeVersion,
898
+ variantId: normalizeVariantId(frame.sidecar.variantId),
899
+ renderId: frame.sidecar.renderId,
900
+ renderNonce: frame.sidecar.renderNonce,
901
+ generation: activationGeneration,
902
+ emissionRevision: activationRevision,
903
+ operation: 'tombstone',
904
+ complete: true,
905
+ expectedFrameIds: [],
906
+ frames: [],
907
+ })
908
+ lastPublisherRevision = activationRevision
909
+ activePublisherRoute = {
910
+ sessionId: publisher.sessionId,
911
+ routeHash: frame.sidecar.routeHash,
912
+ renderNonce: frame.sidecar.renderNonce,
913
+ generation: frame.sidecar.generation,
914
+ codeVersion: frame.sidecar.codeVersion,
915
+ }
916
+ }
917
+ }
918
+ publisherFramesById.set(frame.frameId, frame)
919
+ changed = true
920
+ }
921
+ if (changed) publisherSnapshotDirty = true
922
+ }
923
+
924
+ function reconcileProvenancePublishers() {
925
+ // A context/probe event may arrive before MutationObserver delivery.
926
+ // Consume its pending facts before deciding whether the carrier sample is
927
+ // still current. The caller already owns scheduling the observation pass.
928
+ const pendingMutations = provenanceObserver ? provenanceObserver.takeRecords() : []
929
+ if (pendingMutations.some(invalidatesReadCarriers)) {
930
+ readCarriersDirty = true
931
+ }
932
+ if (readCarriersDirty) {
933
+ readCarriersDirty = false
934
+ if (collectReadCarrierFrames()) publisherSnapshotDirty = true
935
+ }
936
+ consumeProvenancePublishers()
937
+ // Navigation metadata remains independent of anchor/overlay readiness.
938
+ publishPrimaryPages()
939
+ if (!publisherSnapshotDirty) return
940
+ publisherSnapshotDirty = false
941
+ // A refresh may stream the page before its persistent layouts. Keep the
942
+ // whole view read-only until every mounted read has the accepted context;
943
+ // filtering old occurrences individually would make a mixed view editable.
944
+ const mounted = [...publisherFramesById.values(), ...readCarrierFrames]
945
+ const pendingReadContext = mounted.some((frame) =>
946
+ frame.sidecar.dependencySet?.readContext && !matchesCurrentReadContext(frame.sidecar.dependencySet))
947
+ const occurrences = (pendingReadContext ? [] : mounted)
948
+ .filter((frame) => matchesCurrentReadContext(frame.sidecar.dependencySet))
949
+ .map((frame) => frame.sidecar.dependencySet?.readContext ? {
950
+ ...frame, sidecar: { ...frame.sidecar, dependencySet: {
951
+ ...frame.sidecar.dependencySet, sessionId: sessionId(),
952
+ } },
953
+ } : frame)
954
+ let routeFrame = occurrences.find((frame) => frame.frameId === 'route')
955
+ if (routeFrame?.sidecar.dependencySet?.readContext && activePublisherRoute) {
956
+ activePublisherRoute = { ...activePublisherRoute, sessionId: sessionId() }
957
+ }
958
+ if (!routeFrame && occurrences.length > 0) {
959
+ // Native pages need no CMS document. This empty view anchor supplies
960
+ // navigation identity; read/edit authority stays with mounted occurrences.
961
+ const currentContext = authenticatedReadContext()
962
+ const candidates = occurrences.filter((frame) => frame.sidecar.dependencySet?.readContext
963
+ ? matchesCurrentReadContext(frame.sidecar.dependencySet)
964
+ : frame.sidecar.dependencySet?.sessionId === sessionId())
965
+ const first = candidates[0]
966
+ if (first && currentContext && sessionId()) {
967
+ const key = JSON.stringify([window.location.pathname, window.location.search,
968
+ sessionId(), currentContext.contextId, currentContext.revision, first.sidecar.codeVersion])
969
+ if (nativeViewAnchor?.key !== key) {
970
+ const bytes = crypto.getRandomValues(new Uint8Array(16))
971
+ const nonce = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
972
+ nativeViewAnchor = { key, nonce: 'view:' + nonce, revision: Date.now() * 1000 }
973
+ }
974
+ const routeHash = 'view:' + window.location.pathname
975
+ const renderId = nativeViewAnchor.nonce
976
+ const generation = Math.max(1, currentRenderIdentity.generation)
977
+ const receiptIds = [...new Set(candidates.flatMap((frame) => frame.sidecar.dependencySet.receiptIds || []))]
978
+ routeFrame = { frameId: 'route', sidecar: {
979
+ type: 'storystick.preview.public-sidecar', version: 1,
980
+ routeHash, codeVersion: first.sidecar.codeVersion, renderId,
981
+ renderNonce: nativeViewAnchor.nonce, generation, emissionRevision: nativeViewAnchor.revision,
982
+ ...(currentContext.perspective.kind === 'variant' ? { variantId: currentContext.perspective.variantId } : {}),
983
+ ...(first.sidecar.previewScope ? { previewScope: first.sidecar.previewScope } : {}),
984
+ markers: [],
985
+ dependencySet: { type: 'storystick.preview.frame-dependency-set', version: 1,
986
+ sessionId: sessionId(), readContext: currentContext, receiptIds,
987
+ routeHash, codeVersion: first.sidecar.codeVersion, frameId: 'route', renderId, generation,
988
+ ...(candidates.every((frame) => Number.isFinite(frame.sidecar.dependencySet.renderWatermark))
989
+ ? { renderWatermark: Math.min(...candidates.map((frame) => frame.sidecar.dependencySet.renderWatermark)) } : {}),
990
+ ...(currentContext.perspective.kind === 'variant' ? { variantId: currentContext.perspective.variantId } : {}),
991
+ dependencies: [],
992
+ },
993
+ } }
994
+ activePublisherRoute = { sessionId: sessionId(), routeHash,
995
+ renderNonce: nativeViewAnchor.nonce, generation, codeVersion: first.sidecar.codeVersion }
996
+ }
997
+ }
998
+ if (!routeFrame || !activePublisherRoute || routeFrame.sidecar.renderNonce !== activePublisherRoute.renderNonce) {
999
+ publishRenderedContext(null)
1000
+ const previous = lastPublishedSnapshot ?? sidecarStore.readSnapshot()
1001
+ if (previous) {
1002
+ const tombstone = { ...previous, operation: 'tombstone', expectedFrameIds: [], frames: [],
1003
+ generation: Math.max(previous.generation, currentRenderIdentity.generation),
1004
+ emissionRevision: Math.max(lastPublisherRevision + 1, previous.emissionRevision + 1) }
1005
+ lastPublishedSnapshot = tombstone
1006
+ lastPublisherRevision = tombstone.emissionRevision
1007
+ sidecarStore.accept(tombstone)
1008
+ if (window.parent && window.parent !== window) window.parent.postMessage(tombstone, studioTargetOrigin())
1009
+ }
1010
+ return
1011
+ }
1012
+ const frames = [routeFrame, ...occurrences.filter((frame) => frame.frameId !== 'route')]
1013
+ .filter((frame) =>
1014
+ frame.sidecar.codeVersion === routeFrame.sidecar.codeVersion &&
1015
+ (!frame.frameId.startsWith('slice_') ||
1016
+ (frame.sidecar.routeHash === routeFrame.sidecar.routeHash &&
1017
+ frame.sidecar.generation === routeFrame.sidecar.generation &&
1018
+ frame.sidecar.routeRenderNonce === routeFrame.sidecar.renderNonce))
1019
+ )
1020
+ .sort((left, right) => left.frameId.localeCompare(right.frameId))
1021
+ const emissionRevision = Math.max(
1022
+ lastPublisherRevision + 1,
1023
+ ...frames.map((frame) => frame.sidecar.emissionRevision),
1024
+ )
1025
+ lastPublisherRevision = emissionRevision
1026
+ const snapshot = {
1027
+ type: 'storystick.preview.provenance-sidecar-snapshot',
1028
+ version: 2,
1029
+ sessionId: activePublisherRoute.sessionId,
1030
+ routeHash: routeFrame.sidecar.routeHash,
1031
+ codeVersion: routeFrame.sidecar.codeVersion,
1032
+ variantId: normalizeVariantId(routeFrame.sidecar.variantId),
1033
+ renderId: routeFrame.sidecar.renderId,
1034
+ renderNonce: routeFrame.sidecar.renderNonce,
1035
+ generation: Math.max(currentRenderIdentity.generation, routeFrame.sidecar.generation),
1036
+ emissionRevision,
1037
+ operation: 'replace',
1038
+ complete: true,
1039
+ expectedFrameIds: frames.map((frame) => frame.frameId),
1040
+ frames,
1041
+ }
1042
+ if (!validSnapshot(snapshot)) return
1043
+ lastPublishedSnapshot = snapshot
1044
+ publishRenderedContext(authenticatedReadContext())
1045
+ // In Studio, the parent validates and echoes this snapshot before the
1046
+ // reporter consumes it. A top-level preview has no parent relay, so the
1047
+ // same validated server-backed snapshot must enter the same local store
1048
+ // directly. The anchor-map builder then reads one authority in both hosts.
1049
+ if (!window.parent || window.parent === window) {
1050
+ sidecarStore.accept(snapshot)
1051
+ return
1052
+ }
1053
+ window.parent.postMessage(snapshot, studioTargetOrigin())
1054
+ }
1055
+
1056
+ function sessionId() {
1057
+ const state = window.__storystick_preview_runtime__?.state
1058
+ if (state) {
1059
+ if (state.previewEnded) return ''
1060
+ return typeof state.identity?.sessionId === 'string' ? state.identity.sessionId : ''
1061
+ }
1062
+ return (document.body && document.body.getAttribute('data-preview-message-session-id')) ||
1063
+ document.documentElement.getAttribute('data-preview-message-session-id') ||
1064
+ (document.querySelector('[data-preview-message-session-id]') &&
1065
+ document.querySelector('[data-preview-message-session-id]').getAttribute('data-preview-message-session-id')) ||
1066
+ (activePublisherRoute && activePublisherRoute.sessionId) ||
1067
+ ''
1068
+ }
1069
+
1070
+ function normalizeVariantId(value) {
1071
+ if (typeof value !== 'string') return null
1072
+ const trimmed = value.trim()
1073
+ return trimmed.length > 0 ? trimmed : null
1074
+ }
1075
+
1076
+ function previewScopeFromRuntimeStore() {
1077
+ const store = window.__storystick_preview_runtime__
1078
+ if (!store || typeof store !== 'object') return null
1079
+ const state = store.state
1080
+ if (!state || typeof state !== 'object') return null
1081
+ const variantId = normalizeVariantId(state.variantId)
1082
+ const releaseId = normalizeVariantId(state.releaseId)
1083
+ if (variantId && !releaseId) return { kind: 'variant', id: variantId }
1084
+ if (releaseId && !variantId) return { kind: 'release', id: releaseId }
1085
+ return null
1086
+ }
1087
+
1088
+ function activePreviewScope() {
1089
+ return previewScopeFromRuntimeStore()
1090
+ }
1091
+
1092
+ function activeVariantId() {
1093
+ const scope = activePreviewScope()
1094
+ return scope && scope.kind === 'variant' ? scope.id : null
1095
+ }
1096
+
1097
+ function isMomentaryNavigateKeyDown(event) {
1098
+ if (event.key !== 'Alt') return false
1099
+ if (!event.altKey) return false
1100
+ if (event.ctrlKey || event.metaKey) return false
1101
+ if (typeof event.getModifierState === 'function' && event.getModifierState('AltGraph')) return false
1102
+ return true
1103
+ }
1104
+
1105
+ function emitMomentaryNavigateKey(active) {
1106
+ if (lastMomentaryNavigateKeyActive === active) return
1107
+ lastMomentaryNavigateKeyActive = active
1108
+ window.parent.postMessage({
1109
+ type: 'storystick.preview.momentary-navigate-key',
1110
+ key: 'Alt',
1111
+ active,
1112
+ }, studioTargetOrigin())
1113
+ }
1114
+
1115
+ function normalizeMomentaryNavigateLinkClick(event, target) {
1116
+ if (!shouldNormalizeMomentaryNavigateLinkClick(event, target)) return false
1117
+ event.preventDefault()
1118
+ event.stopImmediatePropagation()
1119
+ target.click()
1120
+ return true
1121
+ }
1122
+
1123
+ function shouldNormalizeMomentaryNavigateLinkClick(event, target) {
1124
+ if (event.defaultPrevented) return false
1125
+ if (event.button !== 0) return false
1126
+ if (!event.altKey) return false
1127
+ if (event.ctrlKey || event.metaKey || event.shiftKey) return false
1128
+ if (target.hasAttribute('download')) return false
1129
+ try {
1130
+ return new URL(target.href, window.location.href).origin === window.location.origin
1131
+ } catch {
1132
+ return false
1133
+ }
1134
+ }
1135
+
1136
+ function topLevelRouteHash(frames, domRouteHash) {
1137
+ return domRouteHash ||
1138
+ (frames.find((frame) => frame.frameId === 'route') || { sidecar: emptySidecar() }).sidecar.routeHash ||
1139
+ ''
1140
+ }
1141
+
1142
+ function topLevelCodeVersion(frames) {
1143
+ const routeFrame = frames.find((frame) => frame.frameId === 'route')
1144
+ if (routeFrame && routeFrame.sidecar.codeVersion.length > 0) return routeFrame.sidecar.codeVersion
1145
+ const frameWithCodeVersion = frames.find((frame) => frame.sidecar.codeVersion.length > 0)
1146
+ return frameWithCodeVersion ? frameWithCodeVersion.sidecar.codeVersion : ''
1147
+ }
1148
+
1149
+ function collectAnchors(frames = readFrames()) {
1150
+ observationStats.anchorCollections += 1
1151
+ prunePaintTargets()
1152
+ if (hydrationSafeDomMaterializationEnabled) {
1153
+ materializeCodedTextAttributeCarriers(frames)
1154
+ }
1155
+ const codedTextAnchors = collectCodedTextAnchors(frames)
1156
+ const attributeElements = [...document.querySelectorAll('[data-ss-e],[data-ss-p]')]
1157
+ return {
1158
+ anchors: [...codedTextAnchors, ...collectAttributeAnchors(attributeElements)],
1159
+ markerCount: codedTextAnchors.length + attributeElements.length,
1160
+ }
1161
+ }
1162
+
1163
+ function scheduleHydrationSafeDomMaterialization() {
1164
+ if (!documentActive || hydrationSafeDomMaterializationEnabled || hydrationSafeDomMaterializationScheduled) return
1165
+ hydrationSafeDomMaterializationScheduled = true
1166
+ const enableAfterHydrationTurn = () => {
1167
+ hydrationLoadListener = null
1168
+ hydrationTimer = setTimeout(() => {
1169
+ hydrationTimer = null
1170
+ if (!documentActive) return
1171
+ hydrationFrame = requestAnimationFrame(() => {
1172
+ hydrationFrame = null
1173
+ if (!documentActive) return
1174
+ hydrationFrame = requestAnimationFrame(() => {
1175
+ hydrationFrame = null
1176
+ if (!documentActive) return
1177
+ hydrationTimer = setTimeout(() => {
1178
+ hydrationTimer = null
1179
+ if (!documentActive) return
1180
+ hydrationSafeDomMaterializationEnabled = true
1181
+ scheduleReady('hydration-safe-dom:' + window.location.pathname)
1182
+ }, 0)
1183
+ })
1184
+ })
1185
+ }, domHydrationGraceMs())
1186
+ }
1187
+ if (document.readyState !== 'complete') {
1188
+ hydrationLoadListener = enableAfterHydrationTurn
1189
+ window.addEventListener('load', enableAfterHydrationTurn, { once: true })
1190
+ } else {
1191
+ enableAfterHydrationTurn()
1192
+ }
1193
+ }
1194
+
1195
+ function domHydrationGraceMs() {
1196
+ const value = window.__ssPreviewProvenanceHydrationGraceMs
1197
+ return Number.isFinite(value) && value >= 0
1198
+ ? value
1199
+ : DOM_HYDRATION_GRACE_MS
1200
+ }
1201
+
1202
+ // DOM-native provenance materialization: coded text is the identity carrier,
1203
+ // but overlays paint only real [data-ss-p] elements. Promote only when a
1204
+ // single sidecar-backed element marker owns the element's text surface.
1205
+ function materializeCodedTextAttributeCarriers(frames) {
1206
+ const root = document.body || document.documentElement
1207
+ if (!root) return false
1208
+ const eligibleMarkerIds = elementMarkerIdsBySidecar(frames)
1209
+ if (eligibleMarkerIds.size === 0) return false
1210
+ let materialized = false
1211
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
1212
+ let node = walker.nextNode()
1213
+ while (node) {
1214
+ if (node instanceof Text && node.data.includes(CODED_TEXT_START)) {
1215
+ materialized =
1216
+ materializeCodedTextNodeAttributeCarrier(node, eligibleMarkerIds) ||
1217
+ materialized
1218
+ }
1219
+ node = walker.nextNode()
1220
+ }
1221
+ return materialized
1222
+ }
1223
+
1224
+ function elementMarkerIdsBySidecar(frames) {
1225
+ const markerIds = new Set()
1226
+ for (const frame of frames) {
1227
+ for (const marker of frame.sidecar.markers) {
1228
+ if (marker.anchorKind === 'element') markerIds.add(marker.markerId)
1229
+ }
1230
+ }
1231
+ return markerIds
1232
+ }
1233
+
1234
+ function materializeCodedTextNodeAttributeCarrier(node, eligibleMarkerIds) {
1235
+ let element = node.parentElement
1236
+ while (element) {
1237
+ if (element.closest('script,style,template,noscript,textarea,title,option')) return false
1238
+ if (element === document.body || element === document.documentElement) return false
1239
+ const decoded = decodeCodedText(element.textContent || '')
1240
+ const markerId = singleEligibleDecodedMarkerId(decoded, eligibleMarkerIds)
1241
+ if (markerId !== null) {
1242
+ return writeCodedTextAttributeCarrier(element, markerId)
1243
+ }
1244
+ if (decoded.segments.length > 0) return false
1245
+ element = element.parentElement
1246
+ }
1247
+ return false
1248
+ }
1249
+
1250
+ function singleEligibleDecodedMarkerId(decoded, eligibleMarkerIds) {
1251
+ if (decoded.segments.length !== 1) return null
1252
+ const markerId = decoded.segments[0] && decoded.segments[0].markerId || ''
1253
+ return eligibleMarkerIds.has(markerId) ? markerId : null
1254
+ }
1255
+
1256
+ function writeCodedTextAttributeCarrier(element, markerId) {
1257
+ // Autonomous values use the memory paint registry. React owns this ordinary
1258
+ // element; leaving synthetic attrs behind would outlive the coded text.
1259
+ if (decodeCodedText(element.textContent || '').segments.some(segment => segment.carrierId.startsWith('ssr1.'))) return false
1260
+ if (element.hasAttribute('data-ss-e') || element.querySelector('[data-ss-e]') !== null) return false
1261
+ const existing = normalizedCarrierMarkerId(element.getAttribute('data-ss-p'))
1262
+ if (existing === markerId) return false
1263
+ if (element.querySelector('[data-ss-p]') !== null) return false
1264
+ if (existing !== null && !isStaleCodedTextAttributeCarrier(element, markerId)) return false
1265
+ element.setAttribute('data-ss-p', markerId)
1266
+ return true
1267
+ }
1268
+
1269
+ function isStaleCodedTextAttributeCarrier(element, markerId) {
1270
+ const decoded = decodeCodedText(element.textContent || '')
1271
+ return decoded.segments.length === 1 &&
1272
+ ((decoded.segments[0] && decoded.segments[0].markerId) || '') === markerId
1273
+ }
1274
+
1275
+ function collectAttributeAnchors(elements) {
1276
+ return elements.flatMap((element) => {
1277
+ const rect = element.getBoundingClientRect()
1278
+ const elementMarkerId = element.getAttribute('data-ss-e')
1279
+ const markerId = normalizedCarrierMarkerId(elementMarkerId !== null
1280
+ ? elementMarkerId
1281
+ : element.getAttribute('data-ss-p') || '')
1282
+ if (elementMarkerId === null && isSupersededCodedTextAttributeCarrier(element, markerId)) {
1283
+ return []
1284
+ }
1285
+ return [{
1286
+ markerId,
1287
+ paintId: ensurePaintId(element, markerId),
1288
+ tagName: element.tagName,
1289
+ text: elementMarkerId !== null
1290
+ ? ''
1291
+ : decodeCodedText(element.textContent || '').text,
1292
+ connected: element.isConnected,
1293
+ carrier: 'attribute',
1294
+ documentRect: {
1295
+ x: rect.left + window.scrollX,
1296
+ y: rect.top + window.scrollY,
1297
+ width: rect.width,
1298
+ height: rect.height,
1299
+ },
1300
+ }]
1301
+ })
1302
+ }
1303
+
1304
+ function isSupersededCodedTextAttributeCarrier(element, markerId) {
1305
+ const decoded = decodeCodedText(element.textContent || '')
1306
+ if (decoded.segments.length !== 1) return false
1307
+ const decodedMarkerId = (decoded.segments[0] && decoded.segments[0].markerId) || ''
1308
+ return decodedMarkerId.length > 0 && decodedMarkerId !== markerId
1309
+ }
1310
+
1311
+ function canStampPaintId(element) {
1312
+ return hydrationSafeDomMaterializationEnabled &&
1313
+ (element.hasAttribute('data-ss-e') || element.hasAttribute('data-ss-p'))
1314
+ }
1315
+
1316
+ function ensurePaintId(element, markerId) {
1317
+ if (typeof markerId !== 'string' || markerId.length === 0) return undefined
1318
+ const knownPaintId = paintIdByElement.get(element)
1319
+ if (typeof knownPaintId === 'string') {
1320
+ registerPaintTarget(knownPaintId, markerId, element)
1321
+ if (
1322
+ canStampPaintId(element) &&
1323
+ element.getAttribute(PAINT_ID_ATTRIBUTE) !== knownPaintId
1324
+ ) {
1325
+ element.setAttribute(PAINT_ID_ATTRIBUTE, knownPaintId)
1326
+ }
1327
+ return knownPaintId
1328
+ }
1329
+ const paintId = 'paint:' + nextPaintId
1330
+ nextPaintId += 1
1331
+ paintIdByElement.set(element, paintId)
1332
+ registerPaintTarget(paintId, markerId, element)
1333
+ if (canStampPaintId(element)) {
1334
+ element.setAttribute(PAINT_ID_ATTRIBUTE, paintId)
1335
+ }
1336
+ return paintId
1337
+ }
1338
+
1339
+ function registerPaintTarget(paintId, markerId, element) {
1340
+ paintTargetById.set(paintId, {
1341
+ markerId,
1342
+ elementRef: typeof WeakRef === 'function' ? new WeakRef(element) : element,
1343
+ })
1344
+ }
1345
+
1346
+ function prunePaintTargets() {
1347
+ for (const [paintId, entry] of paintTargetById) {
1348
+ const element = entry.elementRef && typeof entry.elementRef.deref === 'function'
1349
+ ? entry.elementRef.deref()
1350
+ : entry.elementRef
1351
+ if (!(element instanceof Element) || !element.isConnected) {
1352
+ paintTargetById.delete(paintId)
1353
+ }
1354
+ }
1355
+ }
1356
+
1357
+ function resolvePaintTarget(paintId) {
1358
+ const entry = paintTargetById.get(paintId)
1359
+ if (!entry) return null
1360
+ const element = entry.elementRef && typeof entry.elementRef.deref === 'function'
1361
+ ? entry.elementRef.deref()
1362
+ : entry.elementRef
1363
+ if (!(element instanceof Element) || !element.isConnected) {
1364
+ paintTargetById.delete(paintId)
1365
+ return null
1366
+ }
1367
+ return { element, markerId: entry.markerId }
1368
+ }
1369
+
1370
+ function teardownPaintIdentityStamps() {
1371
+ documentActive = false
1372
+ cancelEpoch += 1
1373
+ pendingNavigationEpoch = null
1374
+ cancelReadyObservation()
1375
+ if (viewportFrame !== null) cancelAnimationFrame(viewportFrame)
1376
+ viewportFrame = null
1377
+ if (hydrationFrame !== null) cancelAnimationFrame(hydrationFrame)
1378
+ hydrationFrame = null
1379
+ if (hydrationTimer !== null) clearTimeout(hydrationTimer)
1380
+ hydrationTimer = null
1381
+ if (hydrationLoadListener !== null) window.removeEventListener('load', hydrationLoadListener)
1382
+ hydrationLoadListener = null
1383
+ hydrationSafeDomMaterializationScheduled = false
1384
+ if (provenanceObserver) provenanceObserver.disconnect()
1385
+ for (const element of document.querySelectorAll('[' + PAINT_ID_ATTRIBUTE + ']')) {
1386
+ element.removeAttribute(PAINT_ID_ATTRIBUTE)
1387
+ }
1388
+ }
1389
+
1390
+ function restorePaintIdentityStampsFromBackForwardCache(event) {
1391
+ if (!event.persisted || !provenanceObserver) return
1392
+ documentActive = true
1393
+ readCarriersDirty = true
1394
+ publisherSnapshotDirty = true
1395
+ provenanceObserver.observe(document.documentElement, PROVENANCE_OBSERVER_OPTIONS)
1396
+ scheduleHydrationSafeDomMaterialization()
1397
+ scheduleReady('pageshow:' + window.location.pathname)
1398
+ }
1399
+
1400
+ function collectCodedTextAnchors(frames = readFrames()) {
1401
+ observationStats.codedTextScans += 1
1402
+ const root = document.body || document.documentElement
1403
+ if (!root) return []
1404
+ const anchors = []
1405
+ const seenElements = new WeakMap()
1406
+ const eligibleMarkerIds = elementMarkerIdsBySidecar(frames)
1407
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
1408
+ let node = walker.nextNode()
1409
+ while (node) {
1410
+ if (node instanceof Text && node.data.includes(CODED_TEXT_START)) {
1411
+ const directAnchors = anchorsForCodedTextNode(node, eligibleMarkerIds)
1412
+ if (directAnchors.length > 0) {
1413
+ for (const anchor of directAnchors) {
1414
+ const seen = seenElements.get(anchor.element) || new Set()
1415
+ if (seen.has(anchor.markerId)) continue
1416
+ seen.add(anchor.markerId)
1417
+ seenElements.set(anchor.element, seen)
1418
+ const { element, ...publicAnchor } = anchor
1419
+ anchors.push(publicAnchor)
1420
+ }
1421
+ } else {
1422
+ for (const anchor of anchorsForSpanningCodedTextNode(node, eligibleMarkerIds)) {
1423
+ const seen = seenElements.get(anchor.element) || new Set()
1424
+ if (seen.has(anchor.markerId)) continue
1425
+ seen.add(anchor.markerId)
1426
+ seenElements.set(anchor.element, seen)
1427
+ const { element, ...publicAnchor } = anchor
1428
+ anchors.push(publicAnchor)
1429
+ }
1430
+ }
1431
+ }
1432
+ node = walker.nextNode()
1433
+ }
1434
+ return anchors
1435
+ }
1436
+
1437
+ function currentDocumentViewport() {
1438
+ return {
1439
+ x: window.scrollX,
1440
+ y: window.scrollY,
1441
+ width: window.innerWidth,
1442
+ height: window.innerHeight,
1443
+ }
1444
+ }
1445
+
1446
+ function anchorsForCodedTextNode(node, eligibleMarkerIds) {
1447
+ const parent = node.parentElement
1448
+ if (!parent || parent.closest('script,style,template,noscript,textarea,title,option')) return []
1449
+ const decoded = decodeCodedText(node.data)
1450
+ const paintId = codedTextPaintId(parent, decoded, eligibleMarkerIds)
1451
+ const anchors = decoded.segments.map((segment) => {
1452
+ // A paintId already identifies this exact, unambiguous element. Publish
1453
+ // the same box the painter measures; invisible metadata can distort
1454
+ // browser Range geometry even when the element renders correctly.
1455
+ const rect = paintId
1456
+ ? parent.getBoundingClientRect()
1457
+ : rectForTextRange(node, segment.textStart, segment.textEnd) || parent.getBoundingClientRect()
1458
+ return {
1459
+ markerId: segment.markerId,
1460
+ element: parent,
1461
+ ...(decoded.segments.length === 1 && paintId ? { paintId } : {}),
1462
+ tagName: parent.tagName,
1463
+ text: segment.text,
1464
+ connected: node.isConnected && parent.isConnected,
1465
+ carrier: 'coded-text',
1466
+ documentRect: {
1467
+ x: rect.left + window.scrollX,
1468
+ y: rect.top + window.scrollY,
1469
+ width: rect.width,
1470
+ height: rect.height,
1471
+ },
1472
+ }
1473
+ })
1474
+ return anchors.filter((anchor) =>
1475
+ !hasMatchingAttributeCarrier(parent, anchor.markerId)
1476
+ )
1477
+ }
1478
+
1479
+ function anchorsForSpanningCodedTextNode(node, eligibleMarkerIds) {
1480
+ let element = node.parentElement
1481
+ while (element) {
1482
+ if (element.closest('script,style,template,noscript,textarea,title,option')) return []
1483
+ const decoded = decodeCodedText(element.textContent || '')
1484
+ if (decoded.segments.length > 0) {
1485
+ const segments = decoded.segments.filter((segment) =>
1486
+ !hasMatchingAttributeCarrier(element, segment.markerId)
1487
+ )
1488
+ if (segments.length === 0) return []
1489
+ const rect = element.getBoundingClientRect()
1490
+ const paintId = codedTextPaintId(element, decoded, eligibleMarkerIds)
1491
+ return segments.map((segment) => ({
1492
+ markerId: segment.markerId,
1493
+ element,
1494
+ ...(segments.length === 1 && paintId ? { paintId } : {}),
1495
+ tagName: element.tagName,
1496
+ text: segment.text,
1497
+ connected: node.isConnected && element.isConnected,
1498
+ carrier: 'coded-text',
1499
+ documentRect: {
1500
+ x: rect.left + window.scrollX,
1501
+ y: rect.top + window.scrollY,
1502
+ width: rect.width,
1503
+ height: rect.height,
1504
+ },
1505
+ }))
1506
+ }
1507
+ element = element.parentElement
1508
+ }
1509
+ return []
1510
+ }
1511
+
1512
+ function hasMatchingAttributeCarrier(element, markerId) {
1513
+ return normalizedCarrierMarkerId(element.getAttribute('data-ss-p')) === markerId
1514
+ }
1515
+
1516
+ function codedTextPaintId(element, decoded, eligibleMarkerIds) {
1517
+ if (decoded.segments.length !== 1) return undefined
1518
+ const markerId = (decoded.segments[0] && decoded.segments[0].markerId) || ''
1519
+ if (!eligibleMarkerIds.has(markerId)) return undefined
1520
+ // A text node is not an element boundary. Inline rich text can put
1521
+ // independently owned nodes on both sides of a link in the same <p>.
1522
+ // Painting that parent for both would reuse its paint ID, overwrite the
1523
+ // registry owner and prevent the entire overlay map from being confirmed.
1524
+ // Keep their text provenance, but only promote an unambiguous element.
1525
+ if (decodeCodedText(element.textContent || '').segments.some((segment) =>
1526
+ segment.markerId !== markerId
1527
+ )) return undefined
1528
+ if (element.hasAttribute('data-ss-e') || element.querySelector('[data-ss-e]') !== null) {
1529
+ return undefined
1530
+ }
1531
+ if (element.querySelector('[data-ss-p]') !== null) return undefined
1532
+ const existing = normalizedCarrierMarkerId(element.getAttribute('data-ss-p'))
1533
+ if (
1534
+ existing !== null &&
1535
+ existing !== markerId &&
1536
+ !isStaleCodedTextAttributeCarrier(element, markerId)
1537
+ ) {
1538
+ return undefined
1539
+ }
1540
+ return ensurePaintId(element, markerId)
1541
+ }
1542
+
1543
+ function rectForTextRange(node, start, end) {
1544
+ if (start >= end || typeof document.createRange !== 'function') return null
1545
+ const range = document.createRange()
1546
+ try {
1547
+ range.setStart(node, start)
1548
+ range.setEnd(node, end)
1549
+ return typeof range.getBoundingClientRect === 'function'
1550
+ ? range.getBoundingClientRect()
1551
+ : null
1552
+ } catch {
1553
+ return null
1554
+ } finally {
1555
+ range.detach()
1556
+ }
1557
+ }
1558
+
1559
+ function buildMap(pathname, domRouteHash, frame, anchors, invalidMarkerIds, duplicateSidecarMarkerIds, phase, pending, lifecycle) {
1560
+ const sidecarMarkers = new Map(frame.sidecar.markers.map((marker) => [marker.markerId, marker]))
1561
+ const sidecarMarkerIds = new Set(sidecarMarkers.keys())
1562
+ const rawAnchors = anchors.filter((anchor) => anchor.markerId.length > 0)
1563
+ const counts = new Map()
1564
+ for (const anchor of rawAnchors) counts.set(anchor.markerId, (counts.get(anchor.markerId) || 0) + 1)
1565
+
1566
+ const observedMarkerIds = new Set(rawAnchors.map((anchor) => anchor.markerId))
1567
+ const localInvalidMarkerIds = rawAnchors
1568
+ .filter((anchor) => !sidecarMarkerIds.has(anchor.markerId))
1569
+ .map((anchor) => anchor.markerId)
1570
+ const unpaintedMarkerIds = [...sidecarMarkerIds].filter((markerId) =>
1571
+ !observedMarkerIds.has(markerId)
1572
+ )
1573
+ const missingMarkerIds = [...sidecarMarkerIds].filter((markerId) =>
1574
+ !observedMarkerIds.has(markerId) && isRequiredMarker(markerId, sidecarMarkers)
1575
+ )
1576
+ const ambiguousMarkerIds = [...counts.entries()]
1577
+ .filter(([markerId, count]) => count > 1 && !allowsMultipleAnchors(markerId, sidecarMarkers))
1578
+ .map(([markerId]) => markerId)
1579
+ const contentMismatchMarkerIds = rawAnchors
1580
+ .filter((anchor) => {
1581
+ const marker = sidecarMarkers.get(anchor.markerId)
1582
+ const renderedTextFingerprint = fingerprintRenderedText(anchor.text)
1583
+ return marker &&
1584
+ marker.renderedTextFingerprint !== undefined &&
1585
+ marker.renderedTextFingerprint !== renderedTextFingerprint
1586
+ })
1587
+ .map((anchor) => anchor.markerId)
1588
+ const contentMismatchMarkerIdSet = new Set(contentMismatchMarkerIds)
1589
+ const fatalContentMismatchMarkerIds = [...contentMismatchMarkerIdSet]
1590
+ .filter((markerId) => isRequiredMarker(markerId, sidecarMarkers))
1591
+ const mergedInvalidMarkerIds = [...new Set([...localInvalidMarkerIds, ...invalidMarkerIds])]
1592
+ const routeReady = frame.frameId !== 'route' || domRouteHash === frame.sidecar.routeHash
1593
+ const sidecarVariantId = normalizeVariantId(frame.sidecar.variantId)
1594
+
1595
+ const resolvedAnchors = rawAnchors.filter((anchor) =>
1596
+ anchor.connected &&
1597
+ sidecarMarkerIds.has(anchor.markerId) &&
1598
+ ((counts.get(anchor.markerId) || 0) === 1 || allowsMultipleAnchors(anchor.markerId, sidecarMarkers)) &&
1599
+ !contentMismatchMarkerIdSet.has(anchor.markerId)
1600
+ )
1601
+ const codedTextMarkerIds = new Set(
1602
+ resolvedAnchors
1603
+ .filter((anchor) => anchor.carrier === 'coded-text')
1604
+ .map((anchor) => anchor.markerId)
1605
+ )
1606
+ const domMarkerIds = new Set(
1607
+ resolvedAnchors
1608
+ .filter((anchor) => typeof anchor.paintId === 'string' && anchor.paintId.length > 0)
1609
+ .map((anchor) => anchor.markerId)
1610
+ )
1611
+ const dependencySet = frame.sidecar.dependencySet
1612
+ const dependencySetMatches = !dependencySet || (
1613
+ matchesCurrentReadContext(dependencySet) &&
1614
+ dependencySet.sessionId === lifecycle.sessionId &&
1615
+ dependencySet.routeHash === frame.sidecar.routeHash &&
1616
+ dependencySet.codeVersion === frame.sidecar.codeVersion &&
1617
+ dependencySet.frameId === frame.frameId &&
1618
+ dependencySet.renderId === frame.sidecar.renderId &&
1619
+ dependencySet.generation === frame.sidecar.generation &&
1620
+ normalizeVariantId(dependencySet.variantId) === sidecarVariantId
1621
+ )
1622
+
1623
+ return {
1624
+ sessionId: lifecycle.sessionId,
1625
+ generation: lifecycle.generation,
1626
+ sourceOfTruth: lifecycle.sourceOfTruth,
1627
+ ...(lifecycle.commandId ? { commandId: lifecycle.commandId } : {}),
1628
+ pathname,
1629
+ routeHash: frame.sidecar.routeHash,
1630
+ variantId: sidecarVariantId,
1631
+ ...(frame.sidecar.previewScope ? { previewScope: frame.sidecar.previewScope } : {}),
1632
+ codeVersion: frame.sidecar.codeVersion,
1633
+ renderId: frame.sidecar.renderId,
1634
+ ...(frame.sidecar.renderNonce ? { renderNonce: frame.sidecar.renderNonce } : {}),
1635
+ ...(frame.sidecar.emissionRevision > 0
1636
+ ? { emissionRevision: frame.sidecar.emissionRevision }
1637
+ : {}),
1638
+ ...(dependencySet && dependencySetMatches ? { dependencySet } : {}),
1639
+ frameId: frame.frameId,
1640
+ phase,
1641
+ pending,
1642
+ anchors: resolvedAnchors,
1643
+ editLocators: frame.sidecar.markers.flatMap((marker) =>
1644
+ marker.editLocator
1645
+ ? [{
1646
+ markerId: marker.markerId,
1647
+ locator: marker.editLocator,
1648
+ modality: marker.modality,
1649
+ ...(marker.presence ? { presence: marker.presence } : {}),
1650
+ }]
1651
+ : []
1652
+ ),
1653
+ sidecarMarkerIds: [...sidecarMarkerIds],
1654
+ invalidMarkerIds: mergedInvalidMarkerIds,
1655
+ unpaintedMarkerIds,
1656
+ missingMarkerIds,
1657
+ ambiguousMarkerIds,
1658
+ contentMismatchMarkerIds: [...contentMismatchMarkerIdSet],
1659
+ fatalContentMismatchMarkerIds,
1660
+ paintDiagnostics: {
1661
+ codedTextOnlyNotPaintable: [...sidecarMarkerIds].filter((markerId) =>
1662
+ codedTextMarkerIds.has(markerId) && !domMarkerIds.has(markerId)
1663
+ ),
1664
+ domTargetPainted: [...sidecarMarkerIds].filter((markerId) =>
1665
+ domMarkerIds.has(markerId)
1666
+ ),
1667
+ },
1668
+ valid:
1669
+ !pending &&
1670
+ lifecycle.sessionId.length > 0 &&
1671
+ Number.isFinite(lifecycle.generation) &&
1672
+ lifecycle.generation > 0 &&
1673
+ lifecycle.codeVersion.length > 0 &&
1674
+ frame.sidecar.codeVersion === lifecycle.codeVersion &&
1675
+ (frame.frameId !== 'route' || sidecarVariantId === lifecycle.variantId) &&
1676
+ routeReady &&
1677
+ frame.sidecar.routeHash.length > 0 &&
1678
+ frame.sidecar.codeVersion.length > 0 &&
1679
+ frame.sidecar.renderId.length > 0 &&
1680
+ dependencySetMatches &&
1681
+ mergedInvalidMarkerIds.length === 0 &&
1682
+ missingMarkerIds.length === 0 &&
1683
+ fatalContentMismatchMarkerIds.length === 0 &&
1684
+ duplicateSidecarMarkerIds.length === 0,
1685
+ }
1686
+ }
1687
+
1688
+ function collectObservation() {
1689
+ reconcileProvenancePublishers()
1690
+ const frames = readFrames()
1691
+ return {
1692
+ frames,
1693
+ ...collectAnchors(frames),
1694
+ pathname: window.location.pathname,
1695
+ domRouteHash: routeFrameHash(),
1696
+ viewport: currentDocumentViewport(),
1697
+ }
1698
+ }
1699
+
1700
+ function buildMultiFrameMaps(phase, pending, identity, observation) {
1701
+ const { frames, anchors, pathname, domRouteHash, viewport } = observation
1702
+ const lifecycle = {
1703
+ sessionId: sessionId(),
1704
+ generation: identity.generation,
1705
+ sourceOfTruth: identity.sourceOfTruth,
1706
+ commandId: identity.commandId,
1707
+ routeHash: topLevelRouteHash(frames, domRouteHash),
1708
+ variantId: activeVariantId(),
1709
+ codeVersion: topLevelCodeVersion(frames),
1710
+ }
1711
+ const ownerByMarkerId = new Map()
1712
+ const duplicateSidecarMarkerIds = new Set()
1713
+ for (const frame of frames) {
1714
+ for (const marker of frame.sidecar.markers) {
1715
+ const existing = ownerByMarkerId.get(marker.markerId)
1716
+ if (existing !== undefined && existing !== frame.frameId) duplicateSidecarMarkerIds.add(marker.markerId)
1717
+ ownerByMarkerId.set(marker.markerId, frame.frameId)
1718
+ }
1719
+ }
1720
+
1721
+ const invalidMarkerIds = [...new Set(
1722
+ anchors
1723
+ .filter((anchor) => anchor.markerId.length > 0 && !ownerByMarkerId.has(anchor.markerId))
1724
+ .map((anchor) => anchor.markerId)
1725
+ )]
1726
+ const invalidMarkerIdSet = new Set(invalidMarkerIds)
1727
+ const anchorsByFrame = new Map(frames.map((frame) => [frame.frameId, []]))
1728
+ for (const anchor of anchors) {
1729
+ if (invalidMarkerIdSet.has(anchor.markerId)) {
1730
+ // Unknown anchors must still invalidate every frame. Preserve their
1731
+ // original relative order in diagnostics without rescanning all anchors.
1732
+ for (const frameAnchors of anchorsByFrame.values()) frameAnchors.push(anchor)
1733
+ } else {
1734
+ anchorsByFrame.get(ownerByMarkerId.get(anchor.markerId))?.push(anchor)
1735
+ }
1736
+ }
1737
+ const duplicateIds = [...duplicateSidecarMarkerIds]
1738
+ const maps = frames.map((frame) => buildMap(
1739
+ pathname,
1740
+ domRouteHash,
1741
+ frame,
1742
+ anchorsByFrame.get(frame.frameId),
1743
+ invalidMarkerIds,
1744
+ duplicateIds,
1745
+ phase,
1746
+ pending === true,
1747
+ lifecycle,
1748
+ ))
1749
+ const routeMap = maps.find((map) => map.frameId === 'route')
1750
+ const lifecycleValid =
1751
+ pending !== true &&
1752
+ lifecycle.sessionId.length > 0 &&
1753
+ Number.isFinite(lifecycle.generation) &&
1754
+ lifecycle.generation > 0 &&
1755
+ lifecycle.routeHash.length > 0 &&
1756
+ lifecycle.codeVersion.length > 0 &&
1757
+ routeMap !== undefined &&
1758
+ routeMap.routeHash === lifecycle.routeHash &&
1759
+ routeMap.variantId === lifecycle.variantId &&
1760
+ maps.every((map) => map.codeVersion === lifecycle.codeVersion)
1761
+
1762
+ return {
1763
+ type: 'storystick.preview.multi-frame-anchor-maps',
1764
+ version: 1,
1765
+ sessionId: lifecycle.sessionId,
1766
+ generation: lifecycle.generation,
1767
+ sourceOfTruth: lifecycle.sourceOfTruth,
1768
+ ...(lifecycle.commandId ? { commandId: lifecycle.commandId } : {}),
1769
+ phase,
1770
+ pending: pending === true,
1771
+ pathname,
1772
+ routeHash: lifecycle.routeHash,
1773
+ variantId: lifecycle.variantId,
1774
+ ...(routeMap && routeMap.previewScope ? { previewScope: routeMap.previewScope } : {}),
1775
+ primaryPage: selectPrimaryPage(currentPrimaryPages(), pathname, runtimeBasePath),
1776
+ codeVersion: lifecycle.codeVersion,
1777
+ viewport,
1778
+ maps,
1779
+ invalidMarkerIds,
1780
+ duplicateSidecarMarkerIds: duplicateIds,
1781
+ valid: lifecycleValid && duplicateIds.length === 0 && invalidMarkerIds.length === 0 && maps.length > 0 && maps.every((map) => map.valid),
1782
+ }
1783
+ }
1784
+
1785
+ function logEvent(name) {
1786
+ window.__ssPreviewProvenanceEventLog = [...(window.__ssPreviewProvenanceEventLog || []), name].slice(-40)
1787
+ }
1788
+
1789
+ function logLifecycle(result, observation) {
1790
+ window.__ssPreviewProvenanceLifecycleLog = [...(window.__ssPreviewProvenanceLifecycleLog || []), {
1791
+ phase: result.phase,
1792
+ pending: result.pending,
1793
+ valid: result.valid,
1794
+ sessionId: result.sessionId,
1795
+ generation: result.generation,
1796
+ sourceOfTruth: result.sourceOfTruth,
1797
+ commandId: result.commandId || null,
1798
+ pathname: result.pathname,
1799
+ routeHash: result.routeHash,
1800
+ variantId: result.variantId,
1801
+ previewScope: result.previewScope || null,
1802
+ codeVersion: result.codeVersion,
1803
+ domRouteHash: observation.domRouteHash,
1804
+ routeMapRouteHash: (result.maps.find((map) => map.frameId === 'route') || {}).routeHash || '',
1805
+ routeMapValid: (result.maps.find((map) => map.frameId === 'route') || {}).valid === true,
1806
+ anyFrameValid: result.maps.some((map) => map.valid === true),
1807
+ markerCount: observation.markerCount,
1808
+ sidecarMarkerCount: totalSidecarMarkers(observation.frames),
1809
+ }].slice(-80)
1810
+ }
1811
+
1812
+ function publish(phase, pending, identity = currentRenderIdentity, observation = null, epoch = cancelEpoch) {
1813
+ if (!documentActive || epoch !== cancelEpoch) return
1814
+ observation = observation || collectObservation()
1815
+ if (!documentActive || epoch !== cancelEpoch) return
1816
+ observationStats.publications += 1
1817
+ logEvent(phase)
1818
+ const result = buildMultiFrameMaps(phase, pending, identity, observation)
1819
+ logLifecycle(result, observation)
1820
+ window.__ssMultiFrameAnchorMaps = result
1821
+ window.__ssAnchorMap = result.maps.find((map) => map.frameId === 'route') || result.maps[0]
1822
+ window.dispatchEvent(new CustomEvent(LOCAL_ANCHOR_MAP_EVENT, { detail: result }))
1823
+ if (documentActive && epoch === cancelEpoch) publishToStudio(result)
1824
+ return result
1825
+ }
1826
+
1827
+ function publishToStudio(anchorMap) {
1828
+ if (!window.parent || window.parent === window) return
1829
+ if (typeof window.parent.postMessage !== 'function') return
1830
+ window.parent.postMessage({
1831
+ type: 'storystick.preview.provenance-anchor-map',
1832
+ timestamp: Date.now(),
1833
+ anchorMap,
1834
+ }, studioTargetOrigin())
1835
+ }
1836
+
1837
+ function publishViewportAfterScroll() {
1838
+ if (!documentActive || document.visibilityState === 'hidden' || viewportFrame !== null) return
1839
+ viewportFrame = requestAnimationFrame(() => {
1840
+ viewportFrame = null
1841
+ if (!documentActive || document.visibilityState === 'hidden') return
1842
+ const current = window.__ssMultiFrameAnchorMaps
1843
+ if (!current || typeof current !== 'object') return
1844
+ const next = {
1845
+ ...current,
1846
+ viewport: currentDocumentViewport(),
1847
+ }
1848
+ window.__ssMultiFrameAnchorMaps = next
1849
+ const routeFrame = current.maps.find((map) => map.frameId === 'route')
1850
+ if (!routeFrame) return
1851
+ const message = {
1852
+ type: 'storystick.preview.provenance-viewport',
1853
+ timestamp: Date.now(),
1854
+ sessionId: current.sessionId,
1855
+ generation: current.generation,
1856
+ sourceOfTruth: current.sourceOfTruth,
1857
+ ...(current.commandId ? { commandId: current.commandId } : {}),
1858
+ pathname: current.pathname,
1859
+ routeHash: current.routeHash,
1860
+ variantId: current.variantId || null,
1861
+ codeVersion: current.codeVersion,
1862
+ renderId: routeFrame.renderId,
1863
+ viewport: next.viewport,
1864
+ }
1865
+ const previousStats = window.__ssPreviewProvenanceViewportStats || {
1866
+ publications: 0,
1867
+ maxPayloadBytes: 0,
1868
+ }
1869
+ const payloadBytes = JSON.stringify(message).length
1870
+ window.__ssPreviewProvenanceViewportStats = {
1871
+ publications: previousStats.publications + 1,
1872
+ lastPayloadBytes: payloadBytes,
1873
+ maxPayloadBytes: Math.max(previousStats.maxPayloadBytes, payloadBytes),
1874
+ }
1875
+ if (!window.parent || window.parent === window) return
1876
+ if (typeof window.parent.postMessage !== 'function') return
1877
+ window.parent.postMessage(message, studioTargetOrigin())
1878
+ })
1879
+ }
1880
+
1881
+ function totalSidecarMarkers(frames) {
1882
+ return frames.reduce((sum, frame) => sum + frame.sidecar.markers.length, 0)
1883
+ }
1884
+
1885
+ function allSidecarMarkersRendered(frames, anchors) {
1886
+ const renderedMarkerIds = new Set(anchors.map((anchor) => anchor.markerId))
1887
+ return frames.every((frame) =>
1888
+ frame.sidecar.markers.every((marker) =>
1889
+ marker.presence === 'optional' ||
1890
+ marker.presence === 'expected' ||
1891
+ renderedMarkerIds.has(marker.markerId)
1892
+ )
1893
+ )
1894
+ }
1895
+
1896
+ function allowsMultipleAnchors(markerId, sidecarMarkers) {
1897
+ const marker = sidecarMarkers.get(markerId)
1898
+ return marker && marker.anchorMultiplicity === 'multi'
1899
+ }
1900
+
1901
+ function isRequiredMarker(markerId, sidecarMarkers) {
1902
+ const marker = sidecarMarkers.get(markerId)
1903
+ return marker && marker.presence !== 'optional' && marker.presence !== 'expected'
1904
+ }
1905
+
1906
+ function cancelReadyObservation() {
1907
+ if (readyFrame !== null) cancelAnimationFrame(readyFrame)
1908
+ if (readyRetryTimer !== null) clearTimeout(readyRetryTimer)
1909
+ readyFrame = null
1910
+ readyRetryTimer = null
1911
+ readyWork = null
1912
+ }
1913
+
1914
+ function queueReadyFrame() {
1915
+ if (!documentActive || document.visibilityState === 'hidden' || readyFrame !== null || readyRetryTimer !== null || readyPassRunning || readyWork === null) return
1916
+ readyFrame = requestAnimationFrame(runReadyPass)
1917
+ }
1918
+
1919
+ function runReadyPass() {
1920
+ readyFrame = null
1921
+ const work = readyWork
1922
+ if (!documentActive || document.visibilityState === 'hidden' || work === null || work.epoch !== cancelEpoch) return
1923
+ // Preserve the initial two-paint boundary; retries remain in this chain.
1924
+ if (!work.afterPaint) {
1925
+ work.afterPaint = true
1926
+ queueReadyFrame()
1927
+ return
1928
+ }
1929
+ readyPassRunning = true
1930
+ const revision = work.revision
1931
+ const phase = work.phase
1932
+ const identity = work.identity
1933
+ try {
1934
+ observationStats.readyPasses += 1
1935
+ const observation = collectObservation()
1936
+ if (!documentActive || work !== readyWork || work.epoch !== cancelEpoch || revision !== work.revision) return
1937
+ const routeFrame = observation.frames.find((frame) => frame.frameId === 'route')
1938
+ const routeReady = !routeFrame || observation.domRouteHash === routeFrame.sidecar.routeHash
1939
+ const anchorsReady = allSidecarMarkersRendered(observation.frames, observation.anchors)
1940
+ const remainingMs = work.deadline - performance.now()
1941
+ if ((!routeReady || !anchorsReady) && remainingMs > 0) {
1942
+ readyRetryTimer = setTimeout(() => {
1943
+ readyRetryTimer = null
1944
+ queueReadyFrame()
1945
+ }, Math.min(READY_RETRY_MS, remainingMs))
1946
+ return
1947
+ }
1948
+ publish(phase, false, identity, observation, work.epoch)
1949
+ if (!documentActive || work !== readyWork || work.epoch !== cancelEpoch) return
1950
+ if (work.epoch === pendingNavigationEpoch) pendingNavigationEpoch = null
1951
+ if (revision === work.revision) readyWork = null
1952
+ } catch (error) {
1953
+ // An unexpected collector failure must not become a perpetual rAF loop.
1954
+ if (readyWork === work) readyWork = null
1955
+ throw error
1956
+ } finally {
1957
+ readyPassRunning = false
1958
+ queueReadyFrame()
1959
+ }
1960
+ }
1961
+
1962
+ function scheduleReady(phase, identity = currentRenderIdentity, epoch = cancelEpoch) {
1963
+ if (!documentActive || epoch !== cancelEpoch) return
1964
+ if (readyWork === null || readyWork.epoch !== epoch) {
1965
+ readyWork = { phase, identity, epoch, deadline: performance.now() + READY_DEADLINE_MS, afterPaint: false, revision: 0 }
1966
+ } else {
1967
+ readyWork.phase = phase
1968
+ readyWork.identity = identity
1969
+ readyWork.revision += 1
1970
+ }
1971
+ // Actual invalidations can run promptly but never restart the deadline.
1972
+ if (readyRetryTimer !== null) clearTimeout(readyRetryTimer)
1973
+ readyRetryTimer = null
1974
+ queueReadyFrame()
1975
+ }
1976
+
1977
+ function beginNavigation(pathname) {
1978
+ if (!documentActive) return
1979
+ cancelReadyObservation()
1980
+ cancelEpoch += 1
1981
+ iframeGeneration = Math.max(iframeGeneration + 1, currentRenderIdentity.generation + 1)
1982
+ const iframeIdentity = { generation: iframeGeneration, sourceOfTruth: 'iframe' }
1983
+ currentRenderIdentity = iframeIdentity
1984
+ pendingNavigationEpoch = cancelEpoch
1985
+ publish('pending/navigation-start', true, iframeIdentity)
1986
+ scheduleReady('navigation:' + pathname, iframeIdentity, cancelEpoch)
1987
+ }
1988
+
1989
+ function ensureNavigationGeneration(pathname) {
1990
+ if (pendingNavigationEpoch !== null) {
1991
+ scheduleReady('navigation:' + pathname, currentRenderIdentity, pendingNavigationEpoch)
1992
+ return
1993
+ }
1994
+ beginNavigation(pathname)
1995
+ }
1996
+
1997
+ function applyRenderIdentity(identity) {
1998
+ const nextIdentity = identity === null || identity === undefined
1999
+ ? { generation: currentRenderIdentity.generation, sourceOfTruth: 'system' }
2000
+ : normalizeRenderIdentity(identity)
2001
+ if (identity !== null && identity !== undefined) {
2002
+ if (nextIdentity.generation < currentRenderIdentity.generation) return
2003
+ if (
2004
+ pendingNavigationEpoch !== null &&
2005
+ currentRenderIdentity.sourceOfTruth === 'iframe' &&
2006
+ nextIdentity.sourceOfTruth !== 'iframe' &&
2007
+ nextIdentity.generation <= currentRenderIdentity.generation
2008
+ ) {
2009
+ return
2010
+ }
2011
+ }
2012
+ if (
2013
+ nextIdentity.generation === currentRenderIdentity.generation &&
2014
+ nextIdentity.sourceOfTruth === currentRenderIdentity.sourceOfTruth &&
2015
+ nextIdentity.commandId === currentRenderIdentity.commandId
2016
+ ) return
2017
+ cancelReadyObservation()
2018
+ cancelEpoch += 1
2019
+ pendingNavigationEpoch = null
2020
+ currentRenderIdentity = nextIdentity
2021
+ iframeGeneration = Math.max(iframeGeneration, currentRenderIdentity.generation)
2022
+ }
2023
+
2024
+ function isPreviewLifecycleSourceOfTruth(value) {
2025
+ return value === 'studio' || value === 'iframe' || value === 'system'
2026
+ }
2027
+
2028
+ function renderIdentityFromMessage(message) {
2029
+ if (!message || typeof message !== 'object') return null
2030
+ if (message.type !== 'render-mode') return null
2031
+ const lifecycle = message.previewLifecycle
2032
+ if (
2033
+ lifecycle &&
2034
+ typeof lifecycle === 'object' &&
2035
+ Number.isFinite(lifecycle.generation) &&
2036
+ isPreviewLifecycleSourceOfTruth(lifecycle.sourceOfTruth)
2037
+ ) {
2038
+ return normalizeRenderIdentity({
2039
+ generation: lifecycle.generation,
2040
+ sourceOfTruth: lifecycle.sourceOfTruth,
2041
+ ...(typeof lifecycle.commandId === 'string' && lifecycle.commandId.length > 0
2042
+ ? { commandId: lifecycle.commandId }
2043
+ : {}),
2044
+ })
2045
+ }
2046
+ if (Number.isFinite(message.epoch)) {
2047
+ return normalizeRenderIdentity({
2048
+ generation: message.epoch,
2049
+ sourceOfTruth: 'studio',
2050
+ })
2051
+ }
2052
+ return null
2053
+ }
2054
+
2055
+ function interactionModeFromMessage(message) {
2056
+ if (!message || typeof message !== 'object') return null
2057
+ if (message.type !== 'storystick.preview.interaction-mode') return null
2058
+ const payload = message.payload
2059
+ if (!payload || typeof payload !== 'object') return null
2060
+ return payload.mode === 'edit' || payload.mode === 'browse'
2061
+ ? payload.mode
2062
+ : null
2063
+ }
2064
+
2065
+ window.__ssPreviewProvenanceSetRenderIdentity = function setRenderIdentity(identity) {
2066
+ if (!documentActive) return
2067
+ applyRenderIdentity(identity)
2068
+ }
2069
+
2070
+ window.addEventListener('message', (event) => {
2071
+ if (!documentActive) return
2072
+ if (event.source !== window.parent) return
2073
+ if (validSnapshot(event.data)) {
2074
+ if (event.data.sessionId !== sessionId()) return
2075
+ if (sidecarStore.accept(event.data)) {
2076
+ scheduleReady('provenance-sidecar:' + window.location.pathname)
2077
+ }
2078
+ return
2079
+ }
2080
+ const interactionMode = interactionModeFromMessage(event.data)
2081
+ if (interactionMode !== null) {
2082
+ currentInteractionMode = interactionMode
2083
+ return
2084
+ }
2085
+ const identity = renderIdentityFromMessage(event.data)
2086
+ if (identity === null) return
2087
+ applyRenderIdentity(identity)
2088
+ scheduleReady('render-mode:' + window.location.pathname)
2089
+ })
2090
+
2091
+ document.addEventListener('click', (event) => {
2092
+ const target = event.target instanceof Element ? event.target.closest('a[href][data-ss-e], a[href][data-ss-p], [data-ss-e] a[href], [data-ss-p] a[href]') : null
2093
+ if (!(target instanceof HTMLAnchorElement)) return
2094
+ if (target.origin !== window.location.origin) return
2095
+ if (currentInteractionMode === 'edit') return
2096
+ if (normalizeMomentaryNavigateLinkClick(event, target)) return
2097
+ beginNavigation(new URL(target.href).pathname)
2098
+ }, { capture: true })
2099
+
2100
+ document.addEventListener('keydown', (event) => {
2101
+ if (!isMomentaryNavigateKeyDown(event)) return
2102
+ emitMomentaryNavigateKey(true)
2103
+ }, { capture: true })
2104
+
2105
+ document.addEventListener('keyup', (event) => {
2106
+ if (event.key === 'Alt' || !event.altKey) emitMomentaryNavigateKey(false)
2107
+ }, { capture: true })
2108
+
2109
+ window.addEventListener('pagehide', () => emitMomentaryNavigateKey(false))
2110
+ window.addEventListener('pagehide', teardownPaintIdentityStamps)
2111
+ window.addEventListener('pageshow', restorePaintIdentityStampsFromBackForwardCache)
2112
+ document.addEventListener('visibilitychange', () => {
2113
+ emitMomentaryNavigateKey(false)
2114
+ if (!documentActive) return
2115
+ if (document.visibilityState === 'hidden') {
2116
+ if (readyFrame !== null) cancelAnimationFrame(readyFrame)
2117
+ if (readyRetryTimer !== null) clearTimeout(readyRetryTimer)
2118
+ if (viewportFrame !== null) cancelAnimationFrame(viewportFrame)
2119
+ readyFrame = null
2120
+ readyRetryTimer = null
2121
+ viewportFrame = null
2122
+ } else {
2123
+ scheduleReady('visible:' + window.location.pathname)
2124
+ }
2125
+ })
2126
+
2127
+ // Framework adapters report committed locations through the existing router API.
2128
+ // Never patch customer history methods or rewrite authored hrefs.
2129
+ window.addEventListener('storystick.preview.location-committed', () => {
2130
+ if (!documentActive || !sessionId()) return
2131
+ readCarriersDirty = true
2132
+ publisherSnapshotDirty = true
2133
+ ensureNavigationGeneration(window.location.pathname)
2134
+ reconcileProvenancePublishers()
2135
+ })
2136
+
2137
+ window.addEventListener('popstate', () => beginNavigation(window.location.pathname))
2138
+ window.addEventListener('scroll', publishViewportAfterScroll, { passive: true })
2139
+
2140
+ window.addEventListener('storystick.preview.read-context-changed', () => {
2141
+ if (!documentActive) return
2142
+ publisherSnapshotDirty = true
2143
+ reconcileProvenancePublishers()
2144
+ scheduleReady('read-context:' + window.location.pathname)
2145
+ })
2146
+ window.addEventListener('storystick.preview.provenance-occurrence', (event) => {
2147
+ if (!documentActive) return
2148
+ const occurrence = event.detail
2149
+ if (!occurrence || typeof occurrence.frameId !== 'string' || typeof occurrence.renderNonce !== 'string') return
2150
+ const key = JSON.stringify([occurrence.frameId, occurrence.renderNonce])
2151
+ if (occurrence.operation === 'mount') {
2152
+ pendingOccurrenceRemovals.delete(key)
2153
+ readCarriersDirty = true
2154
+ reconcileProvenancePublishers()
2155
+ scheduleReady('mount:' + window.location.pathname)
2156
+ } else if (occurrence.operation === 'unmount') {
2157
+ pendingOccurrenceRemovals.set(key, occurrence)
2158
+ queueMicrotask(() => {
2159
+ if (!pendingOccurrenceRemovals.delete(key)) return
2160
+ const frame = publisherFramesById.get(occurrence.frameId)
2161
+ if (frame?.sidecar.renderNonce !== occurrence.renderNonce) return
2162
+ publisherFramesById.delete(occurrence.frameId)
2163
+ publisherSnapshotDirty = true
2164
+ readCarriersDirty = true
2165
+ if (!documentActive) return
2166
+ reconcileProvenancePublishers()
2167
+ scheduleReady('unmount:' + window.location.pathname)
2168
+ })
2169
+ }
2170
+ })
2171
+
2172
+ function invalidatesReadCarriers(record) {
2173
+ if (isSyntheticOverlayOnlyMutation(record)) return false
2174
+ return record.type !== 'attributes' || (
2175
+ record.attributeName !== 'style' &&
2176
+ record.attributeName !== 'class' &&
2177
+ record.attributeName !== 'hidden' &&
2178
+ record.attributeName !== 'aria-expanded' &&
2179
+ record.attributeName !== 'aria-hidden' &&
2180
+ record.attributeName !== 'data-state' &&
2181
+ record.attributeName !== 'popover' &&
2182
+ record.attributeName !== 'inert' &&
2183
+ record.attributeName !== 'open'
2184
+ )
2185
+ }
2186
+
2187
+ provenanceObserver = new MutationObserver((records) => {
2188
+ if (!documentActive || records.every(isSyntheticOverlayOnlyMutation)) return
2189
+ if (records.some(invalidatesReadCarriers)) readCarriersDirty = true
2190
+ reconcileProvenancePublishers()
2191
+ scheduleReady('mutation:' + window.location.pathname)
2192
+ })
2193
+ provenanceObserver.observe(document.documentElement, PROVENANCE_OBSERVER_OPTIONS)
2194
+
2195
+ window.__ssPreviewProvenanceProbe = {
2196
+ rebuild: (phase = 'probe') => publish(phase, false, currentRenderIdentity),
2197
+ observationStats: () => ({ ...observationStats }),
2198
+ eventLog: () => [...(window.__ssPreviewProvenanceEventLog || [])],
2199
+ lifecycleLog: () => [...(window.__ssPreviewProvenanceLifecycleLog || [])],
2200
+ }
2201
+
2202
+ window[PAINT_TARGET_REGISTRY] = {
2203
+ resolve: resolvePaintTarget,
2204
+ isDomMutationSafe: () => hydrationSafeDomMaterializationEnabled,
2205
+ }
2206
+
2207
+ scheduleHydrationSafeDomMaterialization()
2208
+ // Preserve synchronous authority publication, including initial publisher
2209
+ // ACKs. The scheduled pass reuses its carrier sample if the DOM is unchanged.
2210
+ reconcileProvenancePublishers()
2211
+ scheduleReady('initial:' + window.location.pathname)
2212
+ // This script can load after the snapshot reporter's handshake. Ask Studio
2213
+ // to replay its current contract only after our message listener is installed;
2214
+ // a previously posted render-mode cannot establish this reporter's identity.
2215
+ if (window.parent !== window) window.parent.postMessage({ type: 'ready' }, studioTargetOrigin())
2216
+ })()
2217
+ `)();