@storystick/preview 0.0.0-stage → 0.2.0-rc.1

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 (247) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +56 -0
  3. package/README.md +42 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/authoring/runtime-contract.d.ts +92 -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/page-presentation.d.ts +24 -0
  72. package/dist/page-presentation.js +32 -0
  73. package/dist/provenance/anchor-map.d.ts +95 -0
  74. package/dist/provenance/anchor-map.js +177 -0
  75. package/dist/provenance/carrier-plan.d.ts +51 -0
  76. package/dist/provenance/carrier-plan.js +90 -0
  77. package/dist/provenance/coded-text.d.ts +20 -0
  78. package/dist/provenance/coded-text.js +302 -0
  79. package/dist/provenance/direct-edit-locator.d.ts +29 -0
  80. package/dist/provenance/direct-edit-locator.js +218 -0
  81. package/dist/provenance/editable-leaf.d.ts +134 -0
  82. package/dist/provenance/editable-leaf.js +483 -0
  83. package/dist/provenance/editor-target.d.ts +29 -0
  84. package/dist/provenance/editor-target.js +46 -0
  85. package/dist/provenance/field-leaf-walker.d.ts +21 -0
  86. package/dist/provenance/field-leaf-walker.js +98 -0
  87. package/dist/provenance/frame-dependencies.d.ts +212 -0
  88. package/dist/provenance/frame-dependencies.js +550 -0
  89. package/dist/provenance/index.d.ts +26 -0
  90. package/dist/provenance/index.js +26 -0
  91. package/dist/provenance/local-events.d.ts +1 -0
  92. package/dist/provenance/local-events.js +1 -0
  93. package/dist/provenance/markers.d.ts +21 -0
  94. package/dist/provenance/markers.js +70 -0
  95. package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
  96. package/dist/provenance/multi-frame-anchor-map.js +109 -0
  97. package/dist/provenance/normalize.d.ts +2 -0
  98. package/dist/provenance/normalize.js +19 -0
  99. package/dist/provenance/overlay-focus-echo.d.ts +17 -0
  100. package/dist/provenance/overlay-focus-echo.js +16 -0
  101. package/dist/provenance/overlay-projection.d.ts +65 -0
  102. package/dist/provenance/overlay-projection.js +201 -0
  103. package/dist/provenance/owner-map.d.ts +55 -0
  104. package/dist/provenance/owner-map.js +239 -0
  105. package/dist/provenance/read-carrier.d.ts +22 -0
  106. package/dist/provenance/read-carrier.js +92 -0
  107. package/dist/provenance/recorder.d.ts +38 -0
  108. package/dist/provenance/recorder.js +82 -0
  109. package/dist/provenance/render-context.d.ts +106 -0
  110. package/dist/provenance/render-context.js +338 -0
  111. package/dist/provenance/reveal-contract.d.ts +44 -0
  112. package/dist/provenance/reveal-contract.js +1 -0
  113. package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
  114. package/dist/provenance/richtext-slice-embed-address.js +35 -0
  115. package/dist/provenance/runtime-script.d.ts +1 -0
  116. package/dist/provenance/runtime-script.js +2217 -0
  117. package/dist/provenance/sidecar-store.d.ts +63 -0
  118. package/dist/provenance/sidecar-store.js +151 -0
  119. package/dist/provenance/sidecar.d.ts +34 -0
  120. package/dist/provenance/sidecar.js +227 -0
  121. package/dist/provenance/slice-model.d.ts +28 -0
  122. package/dist/provenance/slice-model.js +86 -0
  123. package/dist/provenance/source-identity.d.ts +3 -0
  124. package/dist/provenance/source-identity.js +7 -0
  125. package/dist/provenance/source-map-materializer.d.ts +76 -0
  126. package/dist/provenance/source-map-materializer.js +286 -0
  127. package/dist/provenance/source-registry.d.ts +103 -0
  128. package/dist/provenance/source-registry.js +562 -0
  129. package/dist/provenance/transformed-output-gate.d.ts +161 -0
  130. package/dist/provenance/transformed-output-gate.js +171 -0
  131. package/dist/provenance/types.d.ts +102 -0
  132. package/dist/provenance/types.js +1 -0
  133. package/dist/refresh-policy.d.ts +11 -0
  134. package/dist/refresh-policy.js +14 -0
  135. package/dist/reveal-target.d.ts +8 -0
  136. package/dist/reveal-target.js +47 -0
  137. package/dist/route-paths.d.ts +11 -0
  138. package/dist/route-paths.js +14 -0
  139. package/dist/router-subscriber.d.ts +92 -0
  140. package/dist/router-subscriber.js +44 -0
  141. package/dist/runtime/anchor-types.d.ts +5 -0
  142. package/dist/runtime/anchor-types.js +1 -0
  143. package/dist/runtime/authority-store.d.ts +31 -0
  144. package/dist/runtime/authority-store.js +56 -0
  145. package/dist/runtime/boot.d.ts +52 -0
  146. package/dist/runtime/boot.js +379 -0
  147. package/dist/runtime/content-refresh.d.ts +30 -0
  148. package/dist/runtime/content-refresh.js +68 -0
  149. package/dist/runtime/emit-runtime-error.d.ts +51 -0
  150. package/dist/runtime/emit-runtime-error.js +64 -0
  151. package/dist/runtime/field-proxy.d.ts +112 -0
  152. package/dist/runtime/field-proxy.js +471 -0
  153. package/dist/runtime/form-contract-reporter.d.ts +99 -0
  154. package/dist/runtime/form-contract-reporter.js +391 -0
  155. package/dist/runtime/invalidation-adapter.d.ts +17 -0
  156. package/dist/runtime/invalidation-adapter.js +42 -0
  157. package/dist/runtime/live-query-overlay.d.ts +39 -0
  158. package/dist/runtime/live-query-overlay.js +345 -0
  159. package/dist/runtime/momentary-navigate.d.ts +16 -0
  160. package/dist/runtime/momentary-navigate.js +106 -0
  161. package/dist/runtime/observation-scheduler.d.ts +15 -0
  162. package/dist/runtime/observation-scheduler.js +42 -0
  163. package/dist/runtime/perf-metrics.d.ts +28 -0
  164. package/dist/runtime/perf-metrics.js +90 -0
  165. package/dist/runtime/preview-interaction-policy.d.ts +1 -0
  166. package/dist/runtime/preview-interaction-policy.js +103 -0
  167. package/dist/runtime/preview-overlay-painter.d.ts +27 -0
  168. package/dist/runtime/preview-overlay-painter.js +1732 -0
  169. package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
  170. package/dist/runtime/preview-overlay-sidecar.js +45 -0
  171. package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
  172. package/dist/runtime/preview-perspective-alignment.js +40 -0
  173. package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
  174. package/dist/runtime/preview-perspective-intent-state.js +5 -0
  175. package/dist/runtime/preview-transition-state.d.ts +142 -0
  176. package/dist/runtime/preview-transition-state.js +257 -0
  177. package/dist/runtime/render-mode-state.d.ts +51 -0
  178. package/dist/runtime/render-mode-state.js +76 -0
  179. package/dist/runtime/request-id.d.ts +4 -0
  180. package/dist/runtime/request-id.js +8 -0
  181. package/dist/runtime/resolve-schema-field.d.ts +99 -0
  182. package/dist/runtime/resolve-schema-field.js +356 -0
  183. package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
  184. package/dist/runtime/runtime-error-dispatch.js +75 -0
  185. package/dist/runtime/schema-store.d.ts +121 -0
  186. package/dist/runtime/schema-store.js +667 -0
  187. package/dist/runtime/snapshot-assembly.d.ts +104 -0
  188. package/dist/runtime/snapshot-assembly.js +553 -0
  189. package/dist/runtime/snapshot-reporter.d.ts +55 -0
  190. package/dist/runtime/snapshot-reporter.js +375 -0
  191. package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
  192. package/dist/runtime/standalone-overlay-controller.js +281 -0
  193. package/dist/runtime/store.d.ts +307 -0
  194. package/dist/runtime/store.js +1026 -0
  195. package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
  196. package/dist/runtime/windowed-minimap-projection.js +189 -0
  197. package/dist/runtime/workspace-shortcuts.d.ts +30 -0
  198. package/dist/runtime/workspace-shortcuts.js +188 -0
  199. package/dist/sdk-error-copy.d.ts +48 -0
  200. package/dist/sdk-error-copy.js +182 -0
  201. package/dist/select-cursor.d.ts +2 -0
  202. package/dist/select-cursor.js +23 -0
  203. package/dist/server/admission-cookie.d.ts +38 -0
  204. package/dist/server/admission-cookie.js +122 -0
  205. package/dist/server/control-client.d.ts +29 -0
  206. package/dist/server/control-client.js +22 -0
  207. package/dist/server/control-errors.d.ts +22 -0
  208. package/dist/server/control-errors.js +182 -0
  209. package/dist/server/draft-route.d.ts +12 -0
  210. package/dist/server/draft-route.js +45 -0
  211. package/dist/server/extract-token.d.ts +30 -0
  212. package/dist/server/extract-token.js +98 -0
  213. package/dist/server/grant.d.ts +128 -0
  214. package/dist/server/grant.js +395 -0
  215. package/dist/server/ordinary-read-provenance.d.ts +11 -0
  216. package/dist/server/ordinary-read-provenance.js +124 -0
  217. package/dist/server/preview-read-metadata.d.ts +10 -0
  218. package/dist/server/preview-read-metadata.js +27 -0
  219. package/dist/server/provenance-frame-builder.d.ts +53 -0
  220. package/dist/server/provenance-frame-builder.js +102 -0
  221. package/dist/server/provenance-runtime.d.ts +70 -0
  222. package/dist/server/provenance-runtime.js +46 -0
  223. package/dist/server/read-context-command.d.ts +11 -0
  224. package/dist/server/read-context-command.js +53 -0
  225. package/dist/server/read-identity.d.ts +3 -0
  226. package/dist/server/read-identity.js +63 -0
  227. package/dist/server/read-lifecycle.d.ts +17 -0
  228. package/dist/server/read-lifecycle.js +52 -0
  229. package/dist/server/render-frame-provenance.d.ts +133 -0
  230. package/dist/server/render-frame-provenance.js +676 -0
  231. package/dist/server/render-id.d.ts +15 -0
  232. package/dist/server/render-id.js +22 -0
  233. package/dist/server/route-document-provenance.d.ts +77 -0
  234. package/dist/server/route-document-provenance.js +1327 -0
  235. package/dist/server/route-session-cookie.d.ts +33 -0
  236. package/dist/server/route-session-cookie.js +54 -0
  237. package/dist/session-projections.d.ts +33 -0
  238. package/dist/session-projections.js +43 -0
  239. package/dist/session-subscriber.d.ts +124 -0
  240. package/dist/session-subscriber.js +79 -0
  241. package/dist/sha256-fallback.d.ts +30 -0
  242. package/dist/sha256-fallback.js +126 -0
  243. package/dist/types.d.ts +446 -0
  244. package/dist/types.js +85 -0
  245. package/dist/validators.d.ts +61 -0
  246. package/dist/validators.js +110 -0
  247. package/package.json +210 -3
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Module-level mutable state for the current render mode inside the
3
+ * developer's iframe. Read by framework SDKs to decide whether to
4
+ * debounce Convex subscriptions (Navigation mode = 100ms debounce
5
+ * per ADR-059 §Technical commitments + spec/preview-system.md
6
+ * §"Convex subscription debounced 100ms in Navigation mode").
7
+ *
8
+ * The snapshot reporter writes here when a `render-mode` postMessage
9
+ * arrives. Framework SDKs read here in their query hook. Subscribers
10
+ * can register a callback to be notified on changes — useful for
11
+ * triggering re-renders without polling.
12
+ *
13
+ * This intentionally lives at module scope (not React Context) for
14
+ * three reasons:
15
+ *
16
+ * 1. The state is iframe-global (one site rendered in one iframe).
17
+ * Two providers in the same iframe with different render modes
18
+ * would be a bug, not a feature.
19
+ *
20
+ * 2. The state is set by the postMessage runtime (vanilla JS,
21
+ * no React tree). A Context API requires a Provider mounted
22
+ * above every consumer; module state is observed regardless.
23
+ *
24
+ * 3. Tests can reset it deterministically via `_resetForTests()`.
25
+ * Cross-test pollution is the only real risk and it's explicit.
26
+ */
27
+ let current = 'device';
28
+ const subscribers = new Set();
29
+ export function getRenderMode() {
30
+ return current;
31
+ }
32
+ export function setRenderMode(mode) {
33
+ if (mode === current)
34
+ return;
35
+ current = mode;
36
+ subscribers.forEach((sub) => {
37
+ try {
38
+ sub(mode);
39
+ }
40
+ catch {
41
+ // Subscriber errors must not break the dispatch chain.
42
+ }
43
+ });
44
+ }
45
+ /**
46
+ * Subscribe to render-mode changes. Returns an unsubscribe function.
47
+ * Subscribers are called synchronously after `setRenderMode`. They
48
+ * MUST not call `setRenderMode` from within their handler — that
49
+ * would re-enter the dispatch.
50
+ */
51
+ export function subscribeRenderMode(callback) {
52
+ subscribers.add(callback);
53
+ return () => {
54
+ subscribers.delete(callback);
55
+ };
56
+ }
57
+ /**
58
+ * Returns the debounce window (ms) appropriate for the current
59
+ * render mode. Navigation mode = 100ms; Device mode = 0 (no debounce).
60
+ *
61
+ * Per ADR-059 §Technical commitments: "the editor reads structure
62
+ * (section order, slice type, slice count), not typography. Typo-
63
+ * graphic updates do not need to propagate to the Navigation
64
+ * miniature within one frame."
65
+ */
66
+ export function debounceForRenderMode(mode = current) {
67
+ return mode === 'navigation' ? 100 : 0;
68
+ }
69
+ /**
70
+ * Test-only reset. Not exported from index.ts to keep the public
71
+ * API small.
72
+ */
73
+ export function _resetForTests() {
74
+ current = 'device';
75
+ subscribers.clear();
76
+ }
@@ -0,0 +1,4 @@
1
+ /** Opaque 128-bit correlation id for Preview commands, handoffs and envelopes.
2
+ * getRandomValues also works on HTTP customer origins, unlike randomUUID.
3
+ */
4
+ export declare function createPreviewRequestId(source?: Pick<Crypto, 'getRandomValues'>): string;
@@ -0,0 +1,8 @@
1
+ /** Opaque 128-bit correlation id for Preview commands, handoffs and envelopes.
2
+ * getRandomValues also works on HTTP customer origins, unlike randomUUID.
3
+ */
4
+ export function createPreviewRequestId(source = globalThis.crypto) {
5
+ const bytes = new Uint8Array(16);
6
+ source.getRandomValues(bytes);
7
+ return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
8
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Conserved deliberately despite having zero importers today: this walker
3
+ * is the source of the coverage denominator (schema fields that should be
4
+ * annotated). Do not remove it as dead resolver machinery.
5
+ *
6
+ * ADR-131 §D6.1 — walk a serialised schema row to a leaf field shape
7
+ * the resolver uses.
8
+ *
9
+ * The schema returned by `/api/q/schemas/current` has the shape
10
+ * documented in `@storystick/types` `SerializedSchema` :
11
+ * - `stories: Record<storyId, SerializedStory>`
12
+ * - Each `SerializedStory` has `fields: Record<fieldName, SerializedField>`
13
+ *
14
+ * The resolver's `fieldId` argument is a dotted/indexed path emitted
15
+ * by the snapshot-assembly traversal (e.g. `"headline"`,
16
+ * `"cards[0].title"`, `"sections[1].cta.label"`). This helper walks
17
+ * the path against the right story (resolved from `storyTypeId`),
18
+ * descending into sub-stories on each hop.
19
+ *
20
+ * Returns `null` when :
21
+ * - the schema row is missing/malformed ;
22
+ * - the storyTypeId doesn't exist ;
23
+ * - any path segment doesn't resolve (renamed field, missing array
24
+ * hop, etc.).
25
+ */
26
+ import type { RawSchemaRow } from '@storystick/client/adapter';
27
+ export interface ResolvedSchemaField {
28
+ /** Final leaf field shape (subset of `SerializedField`). */
29
+ type: string;
30
+ /** Whether the field is an array (repeater). */
31
+ isArray: boolean;
32
+ /** Optional enum values — useful for downstream value→DOM matchers. */
33
+ enumValues?: readonly string[];
34
+ }
35
+ export interface SchemaFieldDeclaration {
36
+ /** Stable inside one serialized schema. Inherited fields point to their source declaration. */
37
+ readonly id: string;
38
+ readonly storyTypeId: string;
39
+ readonly field: string;
40
+ readonly type: string;
41
+ readonly isArray: boolean;
42
+ }
43
+ export interface SchemaCoverageDocument {
44
+ readonly documentId: string;
45
+ readonly storyType: string;
46
+ readonly fields: Readonly<Record<string, unknown>>;
47
+ }
48
+ export interface SchemaStoriesInput {
49
+ readonly stories: unknown;
50
+ }
51
+ export interface SchemaCoverageOccurrence extends SchemaFieldDeclaration {
52
+ /** Concrete identity used only inside Studio; never crosses the iframe boundary. */
53
+ readonly documentId: string;
54
+ readonly path: string;
55
+ }
56
+ /**
57
+ * Walk the schema for `storyTypeId` against the dotted/indexed
58
+ * `fieldPath`. Returns the leaf field shape or null on any failure.
59
+ *
60
+ * Path grammar :
61
+ * - dotted segments descend into sub-stories : `foo.bar`
62
+ * - bracketed indices descend into array items : `cards[0]` (the
63
+ * index is structurally meaningful in the path, not a value —
64
+ * `cards[0]` and `cards[3]` share the same leaf schema).
65
+ */
66
+ export declare function resolveSchemaField(schema: RawSchemaRow, storyTypeId: string, fieldPath: string): ResolvedSchemaField | null;
67
+ /**
68
+ * Enumerate the declaration catalog carried by a serialized schema.
69
+ *
70
+ * `variations` and `computed` are deliberately absent because this function
71
+ * reads only each story's materialized `fields`. Fragment/extends fields are
72
+ * materialized into every consumer by the serializer, so their `origin` is
73
+ * used as the declaration owner to avoid multiplying one source declaration.
74
+ */
75
+ export declare function enumerateSchemaFieldDeclarations(schema: SchemaStoriesInput): SchemaFieldDeclaration[];
76
+ /**
77
+ * Enumerate schema-classified values that are actually present in the
78
+ * authoritative documents for one route. Missing optional fields are not a
79
+ * demand; falsy values and empty arrays are. Nested custom field stories and
80
+ * concrete `_type` slice instances are walked without expanding SDK primitive
81
+ * objects such as image/link/richtext.
82
+ */
83
+ export declare function enumerateSchemaCoverageOccurrences(schema: SchemaStoriesInput, documents: readonly SchemaCoverageDocument[]): SchemaCoverageOccurrence[];
84
+ type PathSegment = {
85
+ kind: 'name';
86
+ name: string;
87
+ } | {
88
+ kind: 'index';
89
+ idx: number;
90
+ };
91
+ /**
92
+ * Split `cards[0].cta.label` into
93
+ * `[{name:'cards'},{index:0},{name:'cta'},{name:'label'}]`.
94
+ *
95
+ * Returns null on malformed input (e.g. `cards[]`, `cards[abc]`,
96
+ * stray `[`).
97
+ */
98
+ export declare function splitFieldPath(path: string): PathSegment[] | null;
99
+ export {};
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Conserved deliberately despite having zero importers today: this walker
3
+ * is the source of the coverage denominator (schema fields that should be
4
+ * annotated). Do not remove it as dead resolver machinery.
5
+ *
6
+ * ADR-131 §D6.1 — walk a serialised schema row to a leaf field shape
7
+ * the resolver uses.
8
+ *
9
+ * The schema returned by `/api/q/schemas/current` has the shape
10
+ * documented in `@storystick/types` `SerializedSchema` :
11
+ * - `stories: Record<storyId, SerializedStory>`
12
+ * - Each `SerializedStory` has `fields: Record<fieldName, SerializedField>`
13
+ *
14
+ * The resolver's `fieldId` argument is a dotted/indexed path emitted
15
+ * by the snapshot-assembly traversal (e.g. `"headline"`,
16
+ * `"cards[0].title"`, `"sections[1].cta.label"`). This helper walks
17
+ * the path against the right story (resolved from `storyTypeId`),
18
+ * descending into sub-stories on each hop.
19
+ *
20
+ * Returns `null` when :
21
+ * - the schema row is missing/malformed ;
22
+ * - the storyTypeId doesn't exist ;
23
+ * - any path segment doesn't resolve (renamed field, missing array
24
+ * hop, etc.).
25
+ */
26
+ import { effectiveSchemaFieldType } from '@storystick/types';
27
+ const BUILTIN_SCHEMA_FIELD_TYPES = new Set([
28
+ 'boolean',
29
+ 'color',
30
+ 'date',
31
+ 'datetime',
32
+ 'enum',
33
+ 'file',
34
+ 'geopoint',
35
+ 'image',
36
+ 'link',
37
+ 'markdown',
38
+ 'number',
39
+ 'pathname',
40
+ 'portableText',
41
+ 'reference',
42
+ 'relation',
43
+ 'richtext',
44
+ 'sliceZone',
45
+ 'slug',
46
+ 'text',
47
+ 'video',
48
+ ]);
49
+ /**
50
+ * Walk the schema for `storyTypeId` against the dotted/indexed
51
+ * `fieldPath`. Returns the leaf field shape or null on any failure.
52
+ *
53
+ * Path grammar :
54
+ * - dotted segments descend into sub-stories : `foo.bar`
55
+ * - bracketed indices descend into array items : `cards[0]` (the
56
+ * index is structurally meaningful in the path, not a value —
57
+ * `cards[0]` and `cards[3]` share the same leaf schema).
58
+ */
59
+ export function resolveSchemaField(schema, storyTypeId, fieldPath) {
60
+ const stories = readStories(schema);
61
+ if (stories === null)
62
+ return null;
63
+ const story = stories[storyTypeId];
64
+ if (story === undefined || story.fields === undefined)
65
+ return null;
66
+ const segments = splitFieldPath(fieldPath);
67
+ if (segments === null || segments.length === 0)
68
+ return null;
69
+ let currentFields = story.fields;
70
+ let currentField = null;
71
+ for (let i = 0; i < segments.length; i++) {
72
+ const segment = segments[i];
73
+ if (segment.kind === 'index') {
74
+ // Array index — current field must be an array. Descend into
75
+ // the resolved type's fields.
76
+ if (currentField === null || currentField.array !== true)
77
+ return null;
78
+ const typeId = currentField.resolvedTypeId;
79
+ if (typeof typeId !== 'string')
80
+ return null;
81
+ const subStory = stories[typeId];
82
+ if (subStory === undefined || subStory.fields === undefined)
83
+ return null;
84
+ currentFields = subStory.fields;
85
+ currentField = null;
86
+ continue;
87
+ }
88
+ // Named segment — look up the field on the current story.
89
+ const field = currentFields[segment.name];
90
+ if (field === undefined)
91
+ return null;
92
+ currentField = field;
93
+ // If there's another segment after this one (dotted descent), we
94
+ // need to walk into the sub-story. Skip when the next segment is
95
+ // an array index — that path is handled by the index case above.
96
+ const next = segments[i + 1];
97
+ if (next !== undefined && next.kind === 'name') {
98
+ // Dotted descent into a sub-object/sub-story.
99
+ if (field.primitiveBase !== undefined)
100
+ return null;
101
+ const typeId = field.resolvedTypeId;
102
+ if (typeof typeId !== 'string')
103
+ return null;
104
+ const subStory = stories[typeId];
105
+ if (subStory === undefined || subStory.fields === undefined)
106
+ return null;
107
+ currentFields = subStory.fields;
108
+ currentField = null;
109
+ }
110
+ }
111
+ if (currentField === null)
112
+ return null;
113
+ return {
114
+ type: effectiveSchemaFieldType(currentField) ??
115
+ pickFieldType(currentField.type),
116
+ isArray: currentField.array === true,
117
+ enumValues: currentField.enumValues,
118
+ };
119
+ }
120
+ /**
121
+ * Enumerate the declaration catalog carried by a serialized schema.
122
+ *
123
+ * `variations` and `computed` are deliberately absent because this function
124
+ * reads only each story's materialized `fields`. Fragment/extends fields are
125
+ * materialized into every consumer by the serializer, so their `origin` is
126
+ * used as the declaration owner to avoid multiplying one source declaration.
127
+ */
128
+ export function enumerateSchemaFieldDeclarations(schema) {
129
+ const stories = readStories(schema);
130
+ if (stories === null)
131
+ return [];
132
+ const byId = new Map();
133
+ for (const [storyTypeId, story] of Object.entries(stories)) {
134
+ for (const [field, shape] of Object.entries(story.fields ?? {})) {
135
+ const declaration = schemaFieldDeclaration(storyTypeId, field, shape);
136
+ if (declaration.type.length === 0 || byId.has(declaration.id))
137
+ continue;
138
+ byId.set(declaration.id, declaration);
139
+ }
140
+ }
141
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
142
+ }
143
+ /**
144
+ * Enumerate schema-classified values that are actually present in the
145
+ * authoritative documents for one route. Missing optional fields are not a
146
+ * demand; falsy values and empty arrays are. Nested custom field stories and
147
+ * concrete `_type` slice instances are walked without expanding SDK primitive
148
+ * objects such as image/link/richtext.
149
+ */
150
+ export function enumerateSchemaCoverageOccurrences(schema, documents) {
151
+ const stories = readStories(schema);
152
+ if (stories === null)
153
+ return [];
154
+ const occurrences = [];
155
+ for (const document of documents) {
156
+ if (document.documentId.trim().length === 0 ||
157
+ document.storyType.trim().length === 0 ||
158
+ !isRecord(document.fields)) {
159
+ continue;
160
+ }
161
+ visitSchemaStoryValue({
162
+ stories,
163
+ storyTypeId: document.storyType,
164
+ value: document.fields,
165
+ documentId: document.documentId,
166
+ pathPrefix: '',
167
+ occurrences,
168
+ depth: 0,
169
+ });
170
+ }
171
+ return occurrences.sort((left, right) => `${left.documentId}:${left.path}:${left.id}`.localeCompare(`${right.documentId}:${right.path}:${right.id}`));
172
+ }
173
+ function visitSchemaStoryValue(input) {
174
+ // JSON-shaped content cannot be cyclic, but a depth cap keeps malformed
175
+ // inputs fail-closed instead of turning coverage into unbounded work.
176
+ if (input.depth > 64)
177
+ return;
178
+ const story = input.stories[input.storyTypeId];
179
+ if (story?.fields === undefined)
180
+ return;
181
+ for (const [field, shape] of Object.entries(story.fields)) {
182
+ if (!Object.prototype.hasOwnProperty.call(input.value, field))
183
+ continue;
184
+ const fieldValue = input.value[field];
185
+ const fieldPath = joinCoveragePath(input.pathPrefix, field);
186
+ const declaration = schemaFieldDeclaration(input.storyTypeId, field, shape);
187
+ if (declaration.type.length === 0)
188
+ continue;
189
+ if (shape.array === true && Array.isArray(fieldValue)) {
190
+ if (fieldValue.length === 0) {
191
+ input.occurrences.push({
192
+ ...declaration,
193
+ documentId: input.documentId,
194
+ path: fieldPath,
195
+ });
196
+ continue;
197
+ }
198
+ for (let index = 0; index < fieldValue.length; index++) {
199
+ const item = fieldValue[index];
200
+ const itemPath = `${fieldPath}[${index}]`;
201
+ input.occurrences.push({
202
+ ...declaration,
203
+ documentId: input.documentId,
204
+ path: itemPath,
205
+ });
206
+ visitNestedSchemaValue({
207
+ ...input,
208
+ fieldShape: shape,
209
+ value: item,
210
+ pathPrefix: itemPath,
211
+ depth: input.depth + 1,
212
+ });
213
+ }
214
+ continue;
215
+ }
216
+ input.occurrences.push({
217
+ ...declaration,
218
+ documentId: input.documentId,
219
+ path: fieldPath,
220
+ });
221
+ visitNestedSchemaValue({
222
+ ...input,
223
+ fieldShape: shape,
224
+ value: fieldValue,
225
+ pathPrefix: fieldPath,
226
+ depth: input.depth + 1,
227
+ });
228
+ }
229
+ }
230
+ function visitNestedSchemaValue(input) {
231
+ if (!isRecord(input.value))
232
+ return;
233
+ const nestedStoryTypeId = nestedStoryTypeForValue(input.stories, input.fieldShape, input.value);
234
+ if (nestedStoryTypeId === null)
235
+ return;
236
+ visitSchemaStoryValue({
237
+ stories: input.stories,
238
+ storyTypeId: nestedStoryTypeId,
239
+ value: input.value,
240
+ documentId: input.documentId,
241
+ pathPrefix: input.pathPrefix,
242
+ occurrences: input.occurrences,
243
+ depth: input.depth,
244
+ });
245
+ }
246
+ function nestedStoryTypeForValue(stories, field, value) {
247
+ const fieldType = effectiveSchemaFieldType(field) ?? pickFieldType(field.type);
248
+ if (fieldType === 'sliceZone') {
249
+ const concrete = typeof value._type === 'string' ? value._type : null;
250
+ if (concrete === null || stories[concrete] === undefined)
251
+ return null;
252
+ if (!Array.isArray(field.allowed))
253
+ return null;
254
+ return field.allowed.some((entry) => entry.slice === concrete) ? concrete : null;
255
+ }
256
+ if (BUILTIN_SCHEMA_FIELD_TYPES.has(fieldType))
257
+ return null;
258
+ const candidates = [
259
+ ...(typeof field.resolvedTypeId === 'string' ? [field.resolvedTypeId] : []),
260
+ ...(field.resolvedTypeIds ?? []),
261
+ ...(stories[fieldType] !== undefined ? [fieldType] : []),
262
+ ].filter((candidate, index, all) => all.indexOf(candidate) === index);
263
+ if (candidates.length === 1)
264
+ return candidates[0];
265
+ const concrete = typeof value._type === 'string' ? value._type : null;
266
+ if (concrete !== null && candidates.includes(concrete))
267
+ return concrete;
268
+ return null;
269
+ }
270
+ function schemaFieldDeclaration(storyTypeId, field, shape) {
271
+ const type = effectiveSchemaFieldType(shape) ?? pickFieldType(shape.type);
272
+ const declarationStoryTypeId = declarationOwnerStoryType(storyTypeId, shape.origin);
273
+ return {
274
+ id: `${declarationStoryTypeId}:${field}:${type}:${shape.array === true ? 'array' : 'one'}`,
275
+ storyTypeId: declarationStoryTypeId,
276
+ field,
277
+ type,
278
+ isArray: shape.array === true,
279
+ };
280
+ }
281
+ function declarationOwnerStoryType(storyTypeId, origin) {
282
+ if (origin?.startsWith('fragment:') || origin?.startsWith('inherited:')) {
283
+ const owner = origin.slice(origin.indexOf(':') + 1).trim();
284
+ if (owner.length > 0)
285
+ return owner;
286
+ }
287
+ return storyTypeId;
288
+ }
289
+ function joinCoveragePath(prefix, field) {
290
+ return prefix.length === 0 ? field : `${prefix}.${field}`;
291
+ }
292
+ function isRecord(value) {
293
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
294
+ }
295
+ // ─────────────────────────────────────────────
296
+ // Internals
297
+ // ─────────────────────────────────────────────
298
+ function readStories(schema) {
299
+ const raw = schema.stories;
300
+ if (raw === null || raw === undefined || typeof raw !== 'object')
301
+ return null;
302
+ return raw;
303
+ }
304
+ /**
305
+ * Split `cards[0].cta.label` into
306
+ * `[{name:'cards'},{index:0},{name:'cta'},{name:'label'}]`.
307
+ *
308
+ * Returns null on malformed input (e.g. `cards[]`, `cards[abc]`,
309
+ * stray `[`).
310
+ */
311
+ export function splitFieldPath(path) {
312
+ if (path.length === 0)
313
+ return [];
314
+ const out = [];
315
+ const len = path.length;
316
+ let i = 0;
317
+ while (i < len) {
318
+ if (path[i] === '.') {
319
+ i++;
320
+ continue;
321
+ }
322
+ if (path[i] === '[') {
323
+ // Index segment.
324
+ const close = path.indexOf(']', i + 1);
325
+ if (close === -1)
326
+ return null;
327
+ const inner = path.slice(i + 1, close);
328
+ if (inner.length === 0)
329
+ return null;
330
+ const idx = Number(inner);
331
+ if (!Number.isInteger(idx) || idx < 0)
332
+ return null;
333
+ out.push({ kind: 'index', idx });
334
+ i = close + 1;
335
+ continue;
336
+ }
337
+ // Name segment — read until `.` or `[`.
338
+ let j = i;
339
+ while (j < len && path[j] !== '.' && path[j] !== '[')
340
+ j++;
341
+ const name = path.slice(i, j);
342
+ if (name.length === 0)
343
+ return null;
344
+ out.push({ kind: 'name', name });
345
+ i = j;
346
+ }
347
+ return out;
348
+ }
349
+ function pickFieldType(type) {
350
+ if (typeof type === 'string')
351
+ return type;
352
+ if (Array.isArray(type) && type.length > 0 && typeof type[0] === 'string') {
353
+ return type[0];
354
+ }
355
+ return '';
356
+ }
@@ -0,0 +1,23 @@
1
+ import { type RuntimeErrorPayload } from '@storystick/transport-allowlist';
2
+ export interface RuntimeErrorDispatchOptions {
3
+ readonly code: string;
4
+ readonly message: string;
5
+ readonly severity: RuntimeErrorPayload['severity'];
6
+ readonly retryable: boolean;
7
+ readonly retryNotBefore?: number;
8
+ readonly persistence: RuntimeErrorPayload['persistence'];
9
+ }
10
+ export interface RuntimeErrorDispatchContext {
11
+ readonly parentOrigin: string | null;
12
+ readonly sessionId: string | null;
13
+ readonly projectId: string | null;
14
+ }
15
+ /**
16
+ * Dispatch a runtime diagnostic from an already-resolved preview identity.
17
+ *
18
+ * Keeping the context explicit lets the runtime store report a recoverable
19
+ * subscription failure without importing the public emitter back through the
20
+ * store singleton (which would create a module cycle).
21
+ */
22
+ export declare function dispatchRuntimeError(options: RuntimeErrorDispatchOptions, context: RuntimeErrorDispatchContext): void;
23
+ export declare function dispatchRuntimeRecovered(code: string, context: RuntimeErrorDispatchContext): void;
@@ -0,0 +1,75 @@
1
+ import { PREVIEW_NAMESPACE, PREVIEW_PROTOCOL_VERSION, RUNTIME_ERROR_CODE_PREFIX, } from '@storystick/transport-allowlist';
2
+ import { createPreviewRequestId } from './request-id.js';
3
+ /**
4
+ * Dispatch a runtime diagnostic from an already-resolved preview identity.
5
+ *
6
+ * Keeping the context explicit lets the runtime store report a recoverable
7
+ * subscription failure without importing the public emitter back through the
8
+ * store singleton (which would create a module cycle).
9
+ */
10
+ export function dispatchRuntimeError(options, context) {
11
+ if (!options.code.startsWith(RUNTIME_ERROR_CODE_PREFIX)) {
12
+ throw new Error(`emitRuntimeError: code must start with '${RUNTIME_ERROR_CODE_PREFIX}' — got '${options.code}'`);
13
+ }
14
+ if (context.parentOrigin === null ||
15
+ context.sessionId === null ||
16
+ context.projectId === null) {
17
+ return;
18
+ }
19
+ if (typeof window === 'undefined' || window.parent === window) {
20
+ return;
21
+ }
22
+ try {
23
+ const envelope = {
24
+ namespace: PREVIEW_NAMESPACE,
25
+ version: PREVIEW_PROTOCOL_VERSION,
26
+ type: 'storystick.preview.runtime-error',
27
+ sessionId: context.sessionId,
28
+ projectId: context.projectId,
29
+ requestId: createPreviewRequestId(),
30
+ nonce: createPreviewRequestId(),
31
+ sentAt: Date.now(),
32
+ payload: {
33
+ code: options.code,
34
+ message: options.message,
35
+ severity: options.severity,
36
+ retryable: options.retryable,
37
+ ...(options.retryNotBefore !== undefined ? { retryNotBefore: options.retryNotBefore } : {}),
38
+ persistence: options.persistence,
39
+ },
40
+ };
41
+ window.parent.postMessage(envelope, context.parentOrigin);
42
+ }
43
+ catch {
44
+ // Best-effort diagnostics must never compound the runtime failure.
45
+ }
46
+ }
47
+ export function dispatchRuntimeRecovered(code, context) {
48
+ if (!code.startsWith(RUNTIME_ERROR_CODE_PREFIX)) {
49
+ throw new Error(`emitRuntimeRecovered: code must start with '${RUNTIME_ERROR_CODE_PREFIX}' — got '${code}'`);
50
+ }
51
+ if (context.parentOrigin === null ||
52
+ context.sessionId === null ||
53
+ context.projectId === null) {
54
+ return;
55
+ }
56
+ if (typeof window === 'undefined' || window.parent === window)
57
+ return;
58
+ try {
59
+ const envelope = {
60
+ namespace: PREVIEW_NAMESPACE,
61
+ version: PREVIEW_PROTOCOL_VERSION,
62
+ type: 'storystick.preview.runtime-recovered',
63
+ sessionId: context.sessionId,
64
+ projectId: context.projectId,
65
+ requestId: createPreviewRequestId(),
66
+ nonce: createPreviewRequestId(),
67
+ sentAt: Date.now(),
68
+ payload: { code },
69
+ };
70
+ window.parent.postMessage(envelope, context.parentOrigin);
71
+ }
72
+ catch {
73
+ // Best-effort diagnostics must never compound runtime recovery.
74
+ }
75
+ }