@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,104 @@
1
+ /**
2
+ * ADR-129 — atomic preview-snapshot assembly (pure).
3
+ *
4
+ * The atomic snapshot the Studio overlay layer consumes carries every
5
+ * overlay item for one render — slices + explicitly bound fields.
6
+ * Fields are discovered through `[data-ss-bind="<fieldId>"]` attributes
7
+ * on slice subtrees.
8
+ *
9
+ * Multi-SliceZone support — chantier S4 (2026-05-27).
10
+ *
11
+ * Per-render `renderId` is fresh per `<SliceZone>` call, so a page with
12
+ * multiple SliceZones (e.g. chrome zone with header slices + body zone
13
+ * with page slices ; or recursive zone-container sub-SliceZones) emits
14
+ * slices with N distinct renderIds. Pre-S4 this triggered a global
15
+ * `return null` degrade. Post-S4 :
16
+ *
17
+ * - DOM-scan entries are grouped by `renderId` ; each group is one
18
+ * zone.
19
+ * - Per-zone emit (slice + explicit-binding fields) runs in DOM order.
20
+ * - `pageDocId` heuristic — picks the most-frequent `model.doc` among
21
+ * top-level slices (tiebreak first-in-DOM-order). This is a single-
22
+ * string best-effort signal ; the wire shape stays a single `string`
23
+ * (the brief carves "single is safer ; array touches the gate
24
+ * contract" — wire-shape changes here would require owner
25
+ * arbitration via the ADR-137 trigger path).
26
+ *
27
+ * The Studio gate's strict `pageDocId === commandedDocId` resolves
28
+ * correctly for :
29
+ * (i) single-zone pages (one zone = one doc),
30
+ * (ii) single-doc multi-zone (every zone shares the commanded
31
+ * doc — recursive zone-containers' inner zones are skipped,
32
+ * so only top-level zones count),
33
+ * (iii) multi-doc multi-zone with the body zone dominant (e.g.
34
+ * 1 chrome slice + N body slices, N ≥ 1 — body wins on
35
+ * count).
36
+ *
37
+ * Two residual edges that the heuristic CAN'T disambiguate on a
38
+ * single string without a Studio-provided hint :
39
+ * (a) 1-1 tiebreak — exactly 1 chrome slice + 1 body slice with
40
+ * chrome-first DOM order. Heuristic picks chrome ; strict
41
+ * gate drops the snapshot ; the page paints no overlays.
42
+ * Product-rare (asymmetric counts dominate).
43
+ * (b) Embedded-other-doc-larger-than-host — an OLD page has 1
44
+ * own slice + a top-level SliceZone surfacing 2+ slices from
45
+ * the (newly commanded) doc B. Heuristic picks doc B ; gate
46
+ * passes ; a stale pre-nav snapshot might paint briefly
47
+ * until the iframe completes navigation. Product-bizarre
48
+ * (customers rarely render another page's SliceZone at the
49
+ * same level as their own).
50
+ *
51
+ * Both edges are documented as known V1 limitations tied to a
52
+ * future ADR for `pageDocId` cardinality (Studio-provided commanded
53
+ * -doc hint via render-mode, OR `pageDocId: string[]` ; both are
54
+ * wire-shape changes deferred to a separate chantier). Strict
55
+ * equality at the gate is non-negotiable per codex S4 R2 P1 — an
56
+ * epoch-fresh pre-nav snapshot can surface the commanded doc via
57
+ * a chrome/linked SliceZone, so softer "any commanded-doc item"
58
+ * checks at the gate admit stale snapshots that the strict check
59
+ * correctly drops.
60
+ * - Aggregate `renderId` is the joined `|`-separated list of every
61
+ * zone's renderId (sorted DOM-order). Any zone re-render produces
62
+ * a different aggregate, so downstream renderId-keyed equality
63
+ * checks still see a fresh tag on any render.
64
+ *
65
+ * C4 anchored path support: new SDK models carry the rendered zone
66
+ * field plus ordered parent anchors, so nested zone-container slices
67
+ * emit visual paths such as `sections[0].sidebar[1]` while Studio
68
+ * navigation gets the mounted path (`sidebar[1]`). Legacy/defaulted
69
+ * models keep the older single-zone `sections` fallback and same-doc
70
+ * guard, so older SDKs degrade instead of fabricating nested paths.
71
+ */
72
+ import type { SnapshotItem } from '../types.js';
73
+ import { type PreviewOverlayRegistryDraft } from './preview-overlay-sidecar.js';
74
+ export interface PlainRect {
75
+ x: number;
76
+ y: number;
77
+ width: number;
78
+ height: number;
79
+ }
80
+ /** Injected geometry source — `el.getBoundingClientRect()` in the
81
+ * live reporter, a deterministic stub in unit tests. */
82
+ export type Measure = (el: Element) => PlainRect;
83
+ export interface AssembledSnapshot {
84
+ renderId: string;
85
+ pageDocId: string;
86
+ /**
87
+ * ADR-139 §2.1 + §5 — renamed from `visibleDocumentIds`. Iframe-side
88
+ * "what model islands declared this render" — debug hint only. See
89
+ * `MsgPreviewSnapshot.iframeDeclaredDocumentIdsDebugHint` doc-comment
90
+ * for the trust-boundary rationale ; the field is NEVER consulted by
91
+ * the Studio anti-bleed gate.
92
+ */
93
+ iframeDeclaredDocumentIdsDebugHint: string[];
94
+ items: SnapshotItem[];
95
+ }
96
+ export interface SnapshotAssemblyOptions {
97
+ overlayRegistry?: PreviewOverlayRegistryDraft;
98
+ }
99
+ /**
100
+ * Synchronous assemble path — Tier-1 explicit only. Pre-C2 behaviour
101
+ * preserved verbatim. Returns null when there is no preview seam OR
102
+ * when the multi-SliceZone guard fires.
103
+ */
104
+ export declare function assembleSnapshot(doc: Document, measure?: Measure, scrollX?: number, scrollY?: number, options?: SnapshotAssemblyOptions): AssembledSnapshot | null;
@@ -0,0 +1,553 @@
1
+ /**
2
+ * ADR-129 — atomic preview-snapshot assembly (pure).
3
+ *
4
+ * The atomic snapshot the Studio overlay layer consumes carries every
5
+ * overlay item for one render — slices + explicitly bound fields.
6
+ * Fields are discovered through `[data-ss-bind="<fieldId>"]` attributes
7
+ * on slice subtrees.
8
+ *
9
+ * Multi-SliceZone support — chantier S4 (2026-05-27).
10
+ *
11
+ * Per-render `renderId` is fresh per `<SliceZone>` call, so a page with
12
+ * multiple SliceZones (e.g. chrome zone with header slices + body zone
13
+ * with page slices ; or recursive zone-container sub-SliceZones) emits
14
+ * slices with N distinct renderIds. Pre-S4 this triggered a global
15
+ * `return null` degrade. Post-S4 :
16
+ *
17
+ * - DOM-scan entries are grouped by `renderId` ; each group is one
18
+ * zone.
19
+ * - Per-zone emit (slice + explicit-binding fields) runs in DOM order.
20
+ * - `pageDocId` heuristic — picks the most-frequent `model.doc` among
21
+ * top-level slices (tiebreak first-in-DOM-order). This is a single-
22
+ * string best-effort signal ; the wire shape stays a single `string`
23
+ * (the brief carves "single is safer ; array touches the gate
24
+ * contract" — wire-shape changes here would require owner
25
+ * arbitration via the ADR-137 trigger path).
26
+ *
27
+ * The Studio gate's strict `pageDocId === commandedDocId` resolves
28
+ * correctly for :
29
+ * (i) single-zone pages (one zone = one doc),
30
+ * (ii) single-doc multi-zone (every zone shares the commanded
31
+ * doc — recursive zone-containers' inner zones are skipped,
32
+ * so only top-level zones count),
33
+ * (iii) multi-doc multi-zone with the body zone dominant (e.g.
34
+ * 1 chrome slice + N body slices, N ≥ 1 — body wins on
35
+ * count).
36
+ *
37
+ * Two residual edges that the heuristic CAN'T disambiguate on a
38
+ * single string without a Studio-provided hint :
39
+ * (a) 1-1 tiebreak — exactly 1 chrome slice + 1 body slice with
40
+ * chrome-first DOM order. Heuristic picks chrome ; strict
41
+ * gate drops the snapshot ; the page paints no overlays.
42
+ * Product-rare (asymmetric counts dominate).
43
+ * (b) Embedded-other-doc-larger-than-host — an OLD page has 1
44
+ * own slice + a top-level SliceZone surfacing 2+ slices from
45
+ * the (newly commanded) doc B. Heuristic picks doc B ; gate
46
+ * passes ; a stale pre-nav snapshot might paint briefly
47
+ * until the iframe completes navigation. Product-bizarre
48
+ * (customers rarely render another page's SliceZone at the
49
+ * same level as their own).
50
+ *
51
+ * Both edges are documented as known V1 limitations tied to a
52
+ * future ADR for `pageDocId` cardinality (Studio-provided commanded
53
+ * -doc hint via render-mode, OR `pageDocId: string[]` ; both are
54
+ * wire-shape changes deferred to a separate chantier). Strict
55
+ * equality at the gate is non-negotiable per codex S4 R2 P1 — an
56
+ * epoch-fresh pre-nav snapshot can surface the commanded doc via
57
+ * a chrome/linked SliceZone, so softer "any commanded-doc item"
58
+ * checks at the gate admit stale snapshots that the strict check
59
+ * correctly drops.
60
+ * - Aggregate `renderId` is the joined `|`-separated list of every
61
+ * zone's renderId (sorted DOM-order). Any zone re-render produces
62
+ * a different aggregate, so downstream renderId-keyed equality
63
+ * checks still see a fresh tag on any render.
64
+ *
65
+ * C4 anchored path support: new SDK models carry the rendered zone
66
+ * field plus ordered parent anchors, so nested zone-container slices
67
+ * emit visual paths such as `sections[0].sidebar[1]` while Studio
68
+ * navigation gets the mounted path (`sidebar[1]`). Legacy/defaulted
69
+ * models keep the older single-zone `sections` fallback and same-doc
70
+ * guard, so older SDKs degrade instead of fabricating nested paths.
71
+ */
72
+ import { attachPreviewOverlayRegistryTarget, } from './preview-overlay-sidecar.js';
73
+ import { readWindowedMinimapGeometry } from './windowed-minimap-projection.js';
74
+ // ─── Wire contract (mirrors @storystick/react) ───────────────
75
+ /** Per-slice model attribute — the serialized `SliceModel` JSON, on the
76
+ * slice's own root element (ADR-129 D9 — no `<script>` node). */
77
+ const SS_MODEL_ATTR = 'data-ss-model';
78
+ /** Slice-root binding attribute — carries the slice `_key`. */
79
+ const SS_SLICE_ATTR = 'data-ss-slice';
80
+ /** Field binding attribute — carries the schema-relative field path. */
81
+ const SS_BIND_ATTR = 'data-ss-bind';
82
+ /** Legacy/default SliceZone field used only when the producer did not
83
+ * prove a concrete zone field. */
84
+ const LEGACY_DEFAULT_ZONE_FIELD = 'sections';
85
+ /**
86
+ * Read + validate ONE slice element's `data-ss-model` attribute.
87
+ */
88
+ function readSliceModel(el) {
89
+ const raw = el.getAttribute(SS_MODEL_ATTR);
90
+ if (raw === null || raw.length === 0)
91
+ return null;
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(raw);
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ if (parsed === null || typeof parsed !== 'object')
100
+ return null;
101
+ const m = parsed;
102
+ if (m.v !== 129 ||
103
+ typeof m.renderId !== 'string' ||
104
+ m.renderId.length === 0 ||
105
+ typeof m.doc !== 'string' ||
106
+ typeof m.i !== 'number' ||
107
+ typeof m.type !== 'string' ||
108
+ m.fields === null ||
109
+ typeof m.fields !== 'object' ||
110
+ Array.isArray(m.fields)) {
111
+ return null;
112
+ }
113
+ if (m.zoneField !== undefined && typeof m.zoneField !== 'string')
114
+ return null;
115
+ if (m.zoneFieldSource !== undefined &&
116
+ m.zoneFieldSource !== 'explicit' &&
117
+ m.zoneFieldSource !== 'default') {
118
+ return null;
119
+ }
120
+ if (m.parentAnchors !== undefined && !isPathAnchorArray(m.parentAnchors)) {
121
+ return null;
122
+ }
123
+ if (m.source !== undefined) {
124
+ if (m.source === null || typeof m.source !== 'object')
125
+ return null;
126
+ const source = m.source;
127
+ if (typeof source.path !== 'string' || source.path.length === 0 ||
128
+ !isPathAnchorArray(source.anchors) || source.anchors.length === 0 ||
129
+ source.anchors.at(-1)?.key !== el.getAttribute(SS_SLICE_ATTR) ||
130
+ m.zoneFieldSource !== 'explicit')
131
+ return null;
132
+ }
133
+ return parsed;
134
+ }
135
+ function isPathAnchorArray(value) {
136
+ return (Array.isArray(value) &&
137
+ value.every((anchor) => anchor !== null &&
138
+ typeof anchor === 'object' &&
139
+ typeof anchor.pathPrefix === 'string' &&
140
+ typeof anchor.key === 'string'));
141
+ }
142
+ function pathAnchorSignature(documentId, anchors) {
143
+ return `${documentId}\x01${anchors
144
+ .map((anchor) => `${anchor.pathPrefix}\x02${anchor.key}`)
145
+ .join('\x03')}`;
146
+ }
147
+ function anchorsEqual(left, right) {
148
+ if (left.length !== right.length)
149
+ return false;
150
+ for (let index = 0; index < left.length; index += 1) {
151
+ const a = left[index];
152
+ const b = right[index];
153
+ if (a.pathPrefix !== b.pathPrefix || a.key !== b.key)
154
+ return false;
155
+ }
156
+ return true;
157
+ }
158
+ function classifyAnchorState(model) {
159
+ const hasZoneField = model.zoneField !== undefined;
160
+ const hasSource = model.zoneFieldSource !== undefined;
161
+ const hasParentAnchors = model.parentAnchors !== undefined;
162
+ if (!hasZoneField && !hasSource && !hasParentAnchors)
163
+ return 'legacy';
164
+ if (hasZoneField && hasSource && hasParentAnchors) {
165
+ return model.zoneFieldSource === 'explicit' ? 'explicit' : 'default';
166
+ }
167
+ return 'partial';
168
+ }
169
+ function nearestSliceAncestor(el) {
170
+ const parent = el.parentElement;
171
+ if (parent === null)
172
+ return null;
173
+ try {
174
+ return parent.closest(`[${SS_SLICE_ATTR}]`);
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
180
+ /**
181
+ * Internal explicit-binding DOM scan.
182
+ *
183
+ * Deliberate invariant: this is the single Tier-1 emission path for
184
+ * authored `data-ss-bind` annotations. Do not add a second resolver or
185
+ * direct emission path beside it.
186
+ *
187
+ * Multi-SliceZone (S4) : entries are grouped by `renderId` (one zone =
188
+ * one renderId) ; each zone contributes its slice/field items in DOM
189
+ * order. `pageDocId` uses a most-frequent-doc heuristic with
190
+ * first-in-DOM-order tiebreak ; the aggregate `renderId` is the joined
191
+ * list (DOM-order, deduped, `|`-separated) so any zone's fresh re-render
192
+ * produces a distinct snapshot identity.
193
+ */
194
+ function assembleTier1(doc, measure, scrollX, scrollY, options = {}) {
195
+ let sliceEls;
196
+ try {
197
+ sliceEls = doc.querySelectorAll(`[${SS_SLICE_ATTR}]`);
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ if (sliceEls.length === 0)
203
+ return null; // no preview seam on this page
204
+ // DOM-order pass — read every entry, grouping by renderId while
205
+ // refusing legacy/incomplete nested models before they can fabricate
206
+ // a visible path.
207
+ const zones = new Map();
208
+ let entryCount = 0;
209
+ let nestedSkipped = false;
210
+ for (let i = 0; i < sliceEls.length; i++) {
211
+ const el = sliceEls[i];
212
+ const key = el.getAttribute(SS_SLICE_ATTR);
213
+ if (key === null || key.length === 0)
214
+ continue;
215
+ const model = readSliceModel(el);
216
+ if (model === null)
217
+ continue;
218
+ const anchorState = classifyAnchorState(model);
219
+ const nested = nearestSliceAncestor(el) !== null;
220
+ if (anchorState === 'partial') {
221
+ nestedSkipped ||= nested;
222
+ continue;
223
+ }
224
+ if (nested && anchorState !== 'explicit') {
225
+ nestedSkipped = true;
226
+ continue;
227
+ }
228
+ const zoneField = anchorState === 'legacy' ? LEGACY_DEFAULT_ZONE_FIELD : model.zoneField;
229
+ const parentAnchors = anchorState === 'legacy' ? [] : model.parentAnchors;
230
+ entryCount++;
231
+ const bucket = zones.get(model.renderId);
232
+ const entry = {
233
+ el,
234
+ key,
235
+ model,
236
+ anchorState,
237
+ zoneField,
238
+ parentAnchors,
239
+ nested,
240
+ };
241
+ if (bucket === undefined)
242
+ zones.set(model.renderId, [entry]);
243
+ else
244
+ bucket.push(entry);
245
+ }
246
+ if (nestedSkipped)
247
+ warnNestedZone();
248
+ if (entryCount === 0)
249
+ return null;
250
+ const items = [];
251
+ const docIds = new Set();
252
+ // Count `model.doc` occurrences for the pageDocId heuristic + remember
253
+ // first-in-DOM-order per doc for the tiebreak. `firstOrder` indexes
254
+ // distinct docs by their FIRST appearance across all zones — a stable
255
+ // tiebreak that doesn't depend on Map iteration semantics for ties.
256
+ const docCounts = new Map();
257
+ const docFirstOrder = new Map();
258
+ let firstSeenIdx = 0;
259
+ // Source identity and visual occurrence are separate. Equal, explicitly
260
+ // proven sources may have several DOM roots; contradictory paths/keys still
261
+ // fail closed. The overlay registry assigns each root its own snapshot key.
262
+ const emittedPathsByDoc = new Map();
263
+ const emittedAnchorSignatures = new Map();
264
+ // Legacy/defaulted top-level namespaces keep the old same-doc guard:
265
+ // a second renderId on the same doc can fabricate a non-colliding
266
+ // `sections[i]` path unless the producer proved the real field.
267
+ const firstDefaultTopLevelRenderIdByDoc = new Map();
268
+ const emittedInfoByElement = new Map();
269
+ let pathCollisionSkipped = false;
270
+ let duplicateKeySkipped = false;
271
+ for (const [renderId, entries] of zones) {
272
+ for (const entry of entries) {
273
+ const { el, key, model, zoneField } = entry;
274
+ let slicePath;
275
+ let anchors;
276
+ let navigationPath;
277
+ let navigationAnchors;
278
+ if (model.source) {
279
+ slicePath = model.source.path;
280
+ anchors = model.source.anchors;
281
+ if (entry.parentAnchors.length) {
282
+ navigationPath = `${zoneField}[${model.i}]`;
283
+ navigationAnchors = [{ pathPrefix: zoneField, key }];
284
+ }
285
+ }
286
+ else if (entry.nested) {
287
+ const parentEl = nearestSliceAncestor(el);
288
+ const parentInfo = parentEl === null ? undefined : emittedInfoByElement.get(parentEl);
289
+ if (parentInfo === undefined ||
290
+ !anchorsEqual(entry.parentAnchors, parentInfo.fromAnchors)) {
291
+ pathCollisionSkipped = true;
292
+ continue;
293
+ }
294
+ const currentPrefix = `${parentInfo.path}.${zoneField}`;
295
+ slicePath = `${currentPrefix}[${model.i}]`;
296
+ anchors = [...parentInfo.fromAnchors, { pathPrefix: currentPrefix, key }];
297
+ navigationPath = `${zoneField}[${model.i}]`;
298
+ navigationAnchors = [{ pathPrefix: zoneField, key }];
299
+ }
300
+ else {
301
+ if (entry.anchorState !== 'explicit') {
302
+ const firstRenderId = firstDefaultTopLevelRenderIdByDoc.get(model.doc);
303
+ if (firstRenderId !== undefined && firstRenderId !== renderId) {
304
+ pathCollisionSkipped = true;
305
+ continue;
306
+ }
307
+ }
308
+ const currentPrefix = zoneField;
309
+ slicePath = `${currentPrefix}[${model.i}]`;
310
+ anchors = [{ pathPrefix: currentPrefix, key }];
311
+ }
312
+ const anchorSignature = pathAnchorSignature(model.doc, anchors);
313
+ if (emittedAnchorSignatures.has(anchorSignature) && (!model.source || emittedAnchorSignatures.get(anchorSignature) !== slicePath)) {
314
+ duplicateKeySkipped = true;
315
+ continue;
316
+ }
317
+ let emittedPaths = emittedPathsByDoc.get(model.doc);
318
+ if (emittedPaths === undefined) {
319
+ emittedPaths = new Map();
320
+ emittedPathsByDoc.set(model.doc, emittedPaths);
321
+ }
322
+ if (emittedPaths.has(slicePath) && (!model.source || emittedPaths.get(slicePath) !== anchorSignature)) {
323
+ pathCollisionSkipped = true;
324
+ continue;
325
+ }
326
+ emittedPaths.set(slicePath, anchorSignature);
327
+ emittedAnchorSignatures.set(anchorSignature, slicePath);
328
+ if (!entry.nested && entry.anchorState !== 'explicit') {
329
+ firstDefaultTopLevelRenderIdByDoc.set(model.doc, renderId);
330
+ }
331
+ docIds.add(model.doc);
332
+ docCounts.set(model.doc, (docCounts.get(model.doc) ?? 0) + 1);
333
+ if (!docFirstOrder.has(model.doc)) {
334
+ docFirstOrder.set(model.doc, firstSeenIdx++);
335
+ }
336
+ items.push(attachPreviewOverlayRegistryTarget({
337
+ kind: 'slice',
338
+ method: 'explicit',
339
+ documentId: model.doc,
340
+ sliceKey: key,
341
+ path: slicePath,
342
+ fromAnchors: anchors,
343
+ ...(navigationPath !== undefined ? { navigationPath } : {}),
344
+ ...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
345
+ field: '',
346
+ documentRect: toDocumentRect(measure(el), scrollX, scrollY),
347
+ tier: { kind: 'explicit' },
348
+ }, el, options.overlayRegistry));
349
+ let bindEls;
350
+ try {
351
+ bindEls = Array.from(el.querySelectorAll(`[${SS_BIND_ATTR}]`));
352
+ }
353
+ catch {
354
+ bindEls = [];
355
+ }
356
+ if (el.hasAttribute(SS_BIND_ATTR))
357
+ bindEls.unshift(el);
358
+ for (let j = 0; j < bindEls.length; j++) {
359
+ const fieldEl = bindEls[j];
360
+ // Multi-SliceZone (codex S4 R1 P1#3) — `querySelectorAll` walks
361
+ // the FULL subtree, so a nested slice's `[data-ss-bind]` would
362
+ // pollute the parent scope and emit under the parent's slice path.
363
+ // The DOM ownership rule is "nearest `[data-ss-slice]` ancestor"
364
+ // — keep the binding ONLY when this slice element IS that
365
+ // ancestor. Skip silently otherwise (the binding belongs to the
366
+ // nested slice, which is itself currently skipped). The
367
+ // slice-root self-binding (added via `bindEls.unshift(el)`) is
368
+ // exempt because `el.closest('[data-ss-slice]')` matches `el`
369
+ // itself.
370
+ const owner = fieldEl.closest(`[${SS_SLICE_ATTR}]`);
371
+ if (owner !== el)
372
+ continue;
373
+ const field = fieldEl.getAttribute(SS_BIND_ATTR);
374
+ if (field === null || field.length === 0)
375
+ continue;
376
+ if (field.includes('[')) {
377
+ warnNestedBind(field);
378
+ continue;
379
+ }
380
+ items.push(attachPreviewOverlayRegistryTarget({
381
+ kind: 'field',
382
+ method: 'explicit',
383
+ documentId: model.doc,
384
+ sliceKey: key,
385
+ path: `${slicePath}.${field}`,
386
+ fromAnchors: anchors,
387
+ ...(navigationPath !== undefined
388
+ ? { navigationPath: `${navigationPath}.${field}` }
389
+ : {}),
390
+ ...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
391
+ field,
392
+ documentRect: toDocumentRect(measure(fieldEl), scrollX, scrollY),
393
+ tier: { kind: 'explicit' },
394
+ }, fieldEl, options.overlayRegistry));
395
+ }
396
+ const emittedInfo = {
397
+ path: slicePath,
398
+ fromAnchors: anchors,
399
+ ...(navigationPath !== undefined ? { navigationPath } : {}),
400
+ ...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
401
+ };
402
+ emittedInfoByElement.set(el, emittedInfo);
403
+ }
404
+ }
405
+ if (pathCollisionSkipped)
406
+ warnPathCollision();
407
+ if (duplicateKeySkipped)
408
+ warnDuplicateSliceKey();
409
+ // pageDocId — most-frequent `model.doc` ; ties broken by first
410
+ // appearance in DOM order. Single-zone collapses to `firstEntry.doc`
411
+ // (identical to pre-S4). Multi-zone same-doc collapses identically.
412
+ let pageDocId = '';
413
+ let pageDocCount = -1;
414
+ let pageDocOrder = Number.POSITIVE_INFINITY;
415
+ for (const [doc, count] of docCounts) {
416
+ if (count > pageDocCount) {
417
+ pageDocId = doc;
418
+ pageDocCount = count;
419
+ pageDocOrder = docFirstOrder.get(doc) ?? 0;
420
+ continue;
421
+ }
422
+ if (count === pageDocCount) {
423
+ const order = docFirstOrder.get(doc) ?? 0;
424
+ if (order < pageDocOrder) {
425
+ pageDocId = doc;
426
+ pageDocOrder = order;
427
+ }
428
+ }
429
+ }
430
+ // Aggregate renderId — Map preserves insertion order so the zones
431
+ // iterate DOM-first-slice. Join with `|` ; single-zone collapses to
432
+ // the lone renderId (no separator), exactly preserving pre-S4 shape.
433
+ const renderId = [...zones.keys()].join('|');
434
+ return {
435
+ renderId,
436
+ pageDocId,
437
+ iframeDeclaredDocumentIdsDebugHint: [...docIds],
438
+ items,
439
+ };
440
+ }
441
+ /**
442
+ * Synchronous assemble path — Tier-1 explicit only. Pre-C2 behaviour
443
+ * preserved verbatim. Returns null when there is no preview seam OR
444
+ * when the multi-SliceZone guard fires.
445
+ */
446
+ export function assembleSnapshot(doc, measure = liveMeasure, scrollX = 0, scrollY = 0, options = {}) {
447
+ const projection = readWindowedMinimapGeometry(doc);
448
+ if (!projection) {
449
+ return assembleTier1(doc, measure, scrollX, scrollY, options);
450
+ }
451
+ const inverse = 1 / projection.compression;
452
+ const unprojectedMeasure = (element) => {
453
+ const rect = measure(element);
454
+ return {
455
+ x: rect.x * inverse,
456
+ y: rect.y * inverse,
457
+ width: rect.width * inverse,
458
+ height: rect.height * inverse,
459
+ };
460
+ };
461
+ return assembleTier1(doc, unprojectedMeasure, scrollX * inverse, scrollY * inverse, options);
462
+ }
463
+ // ─── Helpers ─────────────────────────────────────────────────
464
+ let warnedNestedZone = false;
465
+ /**
466
+ * Warn (once) that the previewed page contains a nested SliceZone
467
+ * emitted by a legacy/defaulted producer.
468
+ *
469
+ * C4 accepts nested zone-container slices only when the producer proves
470
+ * the zone field and parent anchor chain. Older payloads are degraded
471
+ * because their numeric index cannot be placed safely in the mounted
472
+ * Studio path.
473
+ */
474
+ function warnNestedZone() {
475
+ if (warnedNestedZone)
476
+ return;
477
+ warnedNestedZone = true;
478
+ try {
479
+ console.warn(`[storystick] ADR-129 v1 supports TOP-LEVEL <SliceZone> instances ; ` +
480
+ `legacy/defaulted nested zone-container <SliceZone>s are degraded ` +
481
+ `(no overlay). Upgrade @storystick/react to emit anchored zone metadata.`);
482
+ }
483
+ catch {
484
+ // `console` absent — the warning is best-effort.
485
+ }
486
+ }
487
+ let warnedPathCollision = false;
488
+ /**
489
+ * Warn (once) that two SliceZones produced the same visual slice path
490
+ * on the SAME doc. The first safe path wins; the duplicate degrades
491
+ * rather than overwriting snapshot identity.
492
+ */
493
+ function warnPathCollision() {
494
+ if (warnedPathCollision)
495
+ return;
496
+ warnedPathCollision = true;
497
+ try {
498
+ console.warn(`[storystick] Two <SliceZone>s referencing the SAME document collided ` +
499
+ `on at least one slice path — the first path wins and the duplicate ` +
500
+ `is degraded (no overlay).`);
501
+ }
502
+ catch {
503
+ // best-effort
504
+ }
505
+ }
506
+ let warnedDuplicateSliceKey = false;
507
+ /**
508
+ * Warn (once) that two SliceZone slices share the same full anchor
509
+ * signature for one document.
510
+ *
511
+ * `_key`s are sibling-unique per ADR-051. C4's route lookup uses the
512
+ * full keyed-hop chain when it is present; an exact duplicate chain is
513
+ * still ambiguous and degrades.
514
+ */
515
+ function warnDuplicateSliceKey() {
516
+ if (warnedDuplicateSliceKey)
517
+ return;
518
+ warnedDuplicateSliceKey = true;
519
+ try {
520
+ console.warn(`[storystick] Two <SliceZone> slices share the same full anchor ` +
521
+ `signature for one document — the duplicate is degraded (no overlay).`);
522
+ }
523
+ catch {
524
+ // best-effort
525
+ }
526
+ }
527
+ let warnedNestedBind = false;
528
+ function warnNestedBind(field) {
529
+ if (warnedNestedBind)
530
+ return;
531
+ warnedNestedBind = true;
532
+ try {
533
+ console.warn(`[storystick] ADR-129 v1 binds simple field paths; the binding ` +
534
+ `"${field}" uses array syntax. Repeater / nested-array bindings ` +
535
+ `need the \`fieldPath\` helper + per-row \`_key\` anchors — a ` +
536
+ `tracked follow-up. No overlay is shown for it.`);
537
+ }
538
+ catch {
539
+ // best-effort
540
+ }
541
+ }
542
+ function liveMeasure(el) {
543
+ const r = el.getBoundingClientRect();
544
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
545
+ }
546
+ function toDocumentRect(r, scrollX, scrollY) {
547
+ return {
548
+ x: r.x + scrollX,
549
+ y: r.y + scrollY,
550
+ width: r.width,
551
+ height: r.height,
552
+ };
553
+ }