@zenithbuild/runtime 0.2.0 → 0.5.0-beta.2.3

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.
@@ -0,0 +1,98 @@
1
+ # Zenith Runtime V0 Hydration Contract
2
+
3
+ Status: FROZEN (V0)
4
+
5
+ This contract locks hydration and reactivity boundaries for Zenith V0.
6
+
7
+ ## 1. No Runtime Discovery
8
+
9
+ Runtime must not discover bindings by scanning unknown DOM structure.
10
+
11
+ Allowed:
12
+ - Resolve selectors from bundler-provided marker/event tables.
13
+
14
+ Forbidden:
15
+ - Global `querySelectorAll('*')` discovery passes.
16
+ - Runtime inference of binding intent from HTML shape.
17
+
18
+ ## 2. No Implicit Reactivity
19
+
20
+ Runtime primitives are explicit:
21
+ - `signal(initial)`
22
+ - `state(initialObject)`
23
+ - `zeneffect(dependencies, fn)`
24
+
25
+ Forbidden:
26
+ - Proxy-based tracking.
27
+ - Implicit dependency capture contexts.
28
+ - Scheduler/queue abstractions.
29
+
30
+ ## 3. Deterministic Ordering
31
+
32
+ Compiler expression ordering is canonical.
33
+ Bundler and runtime must preserve index semantics exactly.
34
+
35
+ Required:
36
+ - Marker indices are sequential `0..N-1`.
37
+ - Marker count equals expression table length.
38
+ - Runtime never remaps indices.
39
+
40
+ ## 4. Explicit Bootstrap
41
+
42
+ Hydration is explicit and called by bundler bootstrap only:
43
+
44
+ ```js
45
+ hydrate({
46
+ ir_version: 1,
47
+ root: document,
48
+ expressions: __zenith_expr,
49
+ markers: __zenith_markers,
50
+ events: __zenith_events,
51
+ state_values: __zenith_state_values,
52
+ signals: __zenith_signals,
53
+ components: __zenith_components
54
+ });
55
+ ```
56
+
57
+ Runtime must export functions only. Runtime must not auto-run.
58
+
59
+ ## 5. Component Factory Payload
60
+
61
+ Component scripts are compile-time hoisted and emitted as factory modules.
62
+ Runtime receives only explicit component references in `payload.components`.
63
+
64
+ Required:
65
+ - Component host selectors are provided by bundler.
66
+ - Runtime instantiates factories only from payload.
67
+ - Runtime merges returned `bindings` into hydration state by instance key.
68
+
69
+ Forbidden:
70
+ - Runtime component discovery by scanning unknown DOM shape.
71
+ - Runtime-generated component wrappers or lifecycle registries.
72
+
73
+ ## 6. Hard Fail Semantics
74
+
75
+ Runtime must throw for contract drift:
76
+ - Missing or unsupported `ir_version`.
77
+ - Duplicate marker indices.
78
+ - Missing index in sequence.
79
+ - Out-of-order expression table entries.
80
+ - Out-of-order marker table entries.
81
+ - Marker index out of bounds.
82
+ - Marker/expression count mismatch.
83
+ - Event index out of bounds.
84
+
85
+ No fallback behavior is allowed for broken payload contracts.
86
+
87
+ ## 7. Determinism and Purity
88
+
89
+ Forbidden in runtime/bundler output:
90
+ - `eval(`
91
+ - `new Function(`
92
+ - `require(`
93
+ - `Date(`
94
+ - `Math.random(`
95
+ - `crypto.randomUUID(`
96
+ - `process.env`
97
+
98
+ Compile-time guarantees override runtime flexibility.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @zenith/runtime
2
2
 
3
+ > **⚠️ Internal API:** This package is an internal implementation detail of the Zenith framework. It is not intended for public use and its API may break without warning. Please use `@zenithbuild/core` instead.
4
+
5
+
3
6
  The core runtime library for the Zenith framework.
4
7
 
5
8
  ## Overview
@@ -0,0 +1,183 @@
1
+ # RUNTIME_CONTRACT.md — Sealed Runtime Interface
2
+
3
+ > **This document is a legal boundary.**
4
+ > The runtime is a consumer of bundler output.
5
+ > It does not reinterpret, normalize, or extend.
6
+
7
+ ## Status: FROZEN (V0)
8
+
9
+ ---
10
+
11
+ ## 1. Input Surface
12
+
13
+ The runtime receives exactly one module per page:
14
+
15
+ | Export | Type | Contract |
16
+ |---|---|---|
17
+ | `__zenith_html` | `string` | Template literal, immutable |
18
+ | `__zenith_expr` | `(() => any)[]` | Pre-bound expression functions, immutable, ordered |
19
+ | `__zenith_page()` | `function` | Returns `{ html, expressions }` |
20
+
21
+ ### Expression Functions
22
+
23
+ Each entry in `__zenith_expr` is a **pre-bound zero-argument function** emitted by the bundler. The runtime never resolves expression strings, never performs key lookups, and never interprets JavaScript.
24
+
25
+ ```js
26
+ // Bundler emits:
27
+ export const __zenith_expr = [
28
+ () => count(),
29
+ () => count() + 1,
30
+ () => increment()
31
+ ];
32
+
33
+ export default function __zenith_page() {
34
+ return {
35
+ html: __zenith_html,
36
+ expressions: __zenith_expr
37
+ };
38
+ }
39
+ ```
40
+
41
+ The runtime simply executes: `expressions[index]()`.
42
+
43
+ ---
44
+
45
+ ## 2. Runtime Responsibilities (Allowed)
46
+
47
+ | Action | Method |
48
+ |---|---|
49
+ | Insert HTML into container | `container.innerHTML = html` |
50
+ | Locate expression bindings | `querySelectorAll('[data-zx-e]')` |
51
+ | Locate event bindings | `querySelectorAll('[data-zx-on-*]')` |
52
+ | Bind expressions to DOM | `effect(() => node.textContent = expressions[index]())` |
53
+ | Bind event listeners | `node.addEventListener(event, expressions[index])` |
54
+ | Update DOM on signal change | Via effect re-execution |
55
+ | Clean up on unmount | Remove effects + listeners |
56
+
57
+ ---
58
+
59
+ ## 3. Runtime Prohibitions (Forbidden)
60
+
61
+ The runtime **must never**:
62
+
63
+ - Parse JavaScript expressions
64
+ - Resolve expression strings against a scope object
65
+ - Normalize expression strings
66
+ - Modify expression content
67
+ - Introduce component abstractions
68
+ - Perform virtual DOM diffing
69
+ - Re-render full subtrees
70
+ - Implement lifecycle hooks beyond mount/unmount
71
+ - Access or mutate `window` globals
72
+ - Reorder binding indices
73
+ - Interpret import semantics
74
+ - Add framework-level abstractions (routing, stores, context)
75
+
76
+ ---
77
+
78
+ ## 4. Data Attribute Contract
79
+
80
+ Inherited from `BUNDLER_CONTRACT.md`:
81
+
82
+ | Attribute | Format | Runtime Action |
83
+ |---|---|---|
84
+ | `data-zx-e` | `"<index>"` or `"<i0> <i1> ..."` | Execute `expressions[index]()`, bind result to node |
85
+ | `data-zx-on-<event>` | `"<index>"` | Call `addEventListener(event, expressions[index])` |
86
+
87
+ Index values are 0-based integers matching `__zenith_expr` array positions.
88
+
89
+ ---
90
+
91
+ ## 5. Reactivity Model
92
+
93
+ ### Signal Primitive
94
+
95
+ ```js
96
+ const count = signal(0); // Create
97
+ count(); // Read (tracks dependency)
98
+ count.set(1); // Write (notifies subscribers)
99
+ ```
100
+
101
+ Internals:
102
+ - Each signal maintains a `Set<Effect>` of subscribers
103
+ - Reading a signal during effect execution registers the effect as a subscriber
104
+ - Writing a signal notifies all subscribers synchronously
105
+
106
+ ### Effect Primitive
107
+
108
+ ```js
109
+ effect(() => {
110
+ node.textContent = count();
111
+ });
112
+ ```
113
+
114
+ Internals:
115
+ - On execution, sets itself as the "current tracking context"
116
+ - Any signal read during execution adds this effect to its subscriber set
117
+ - When a dependency signal changes, the effect re-runs
118
+
119
+ ### Constraints
120
+
121
+ - No batching
122
+ - No scheduler / microtask queue
123
+ - No async effects
124
+ - No suspense / lazy loading
125
+ - No lifecycle hooks beyond `cleanup()`
126
+
127
+ ---
128
+
129
+ ## 6. Hydration Algorithm
130
+
131
+ Single-pass, deterministic:
132
+
133
+ 1. Call `__zenith_page()` → receive `{ html, expressions }`
134
+ 2. Set `container.innerHTML = html`
135
+ 3. Walk DOM once: `querySelectorAll('[data-zx-e], [data-zx-on-click], ...')`
136
+ 4. For each node with `data-zx-e`:
137
+ - Parse space-separated indices
138
+ - For each index: create `effect(() => node.textContent = expressions[index]())`
139
+ 5. For each node with `data-zx-on-*`:
140
+ - Extract event name from attribute suffix
141
+ - `node.addEventListener(eventName, expressions[index])`
142
+ 6. Store all effects and listeners in a cleanup registry
143
+
144
+ No recursive diffing. No re-render cycle. No component tree.
145
+
146
+ ---
147
+
148
+ ## 7. Cleanup Contract
149
+
150
+ The runtime exposes `cleanup()`:
151
+
152
+ - Disposes all active effects (clears subscriber sets)
153
+ - Removes all event listeners
154
+ - Clears the binding registry
155
+ - Leaves the DOM intact (caller decides whether to clear container)
156
+
157
+ Cleanup is deterministic — calling it twice is a no-op.
158
+
159
+ ---
160
+
161
+ ## 8. Public API Surface
162
+
163
+ Total exports (exhaustive):
164
+
165
+ ```js
166
+ export { signal } // Create reactive signal
167
+ export { effect } // Create reactive effect
168
+ export { mount } // Mount page module into container
169
+ export { cleanup } // Tear down all bindings
170
+ ```
171
+
172
+ Four functions. No more.
173
+
174
+ ---
175
+
176
+ ## 9. Alignment Verification
177
+
178
+ This contract is valid if and only if:
179
+
180
+ - [ ] `data-zx-e` indices match `__zenith_expr` array positions (from `BUNDLER_CONTRACT.md` §2)
181
+ - [ ] Expression functions are pre-bound at bundle time — runtime never resolves strings
182
+ - [ ] HMR footer is ignored by runtime (from `BUNDLER_CONTRACT.md` §7)
183
+ - [ ] No symbol beyond `__zenith_html`, `__zenith_expr`, `__zenith_page` is consumed (from `BUNDLER_CONTRACT.md` §1)
@@ -0,0 +1,88 @@
1
+ // ---------------------------------------------------------------------------
2
+ // cleanup.js — Zenith Runtime V0
3
+ // ---------------------------------------------------------------------------
4
+ // Deterministic teardown system.
5
+ //
6
+ // Tracks:
7
+ // - Active effect disposers
8
+ // - Active event listeners
9
+ //
10
+ // cleanup() removes everything.
11
+ // Calling cleanup() twice is a no-op.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ import { resetGlobalSideEffects } from './zeneffect.js';
15
+
16
+ /** @type {function[]} */
17
+ const _disposers = [];
18
+
19
+ /** @type {{ element: Element, event: string, handler: function }[]} */
20
+ const _listeners = [];
21
+
22
+ let _cleaned = false;
23
+
24
+ /**
25
+ * Register an effect disposer for later cleanup.
26
+ *
27
+ * @param {function} dispose
28
+ */
29
+ export function _registerDisposer(dispose) {
30
+ _disposers.push(dispose);
31
+ _cleaned = false;
32
+ }
33
+
34
+ /**
35
+ * Register an event listener for later cleanup.
36
+ *
37
+ * @param {Element} element
38
+ * @param {string} event
39
+ * @param {function} handler
40
+ */
41
+ export function _registerListener(element, event, handler) {
42
+ _listeners.push({ element, event, handler });
43
+ _cleaned = false;
44
+ }
45
+
46
+ /**
47
+ * Tear down all active effects and event listeners.
48
+ *
49
+ * - Disposes all effects (clears subscriber sets)
50
+ * - Removes all event listeners
51
+ * - Clears registries
52
+ * - Idempotent: calling twice is a no-op
53
+ */
54
+ export function cleanup() {
55
+ // Global zenMount/zenEffect registrations can be created outside hydrate's
56
+ // disposer table (e.g. page-level component bootstraps). Even when cleanup
57
+ // has already run once, we still need to reset that scope deterministically.
58
+ if (_cleaned) {
59
+ resetGlobalSideEffects();
60
+ return;
61
+ }
62
+
63
+ // 1. Dispose all effects
64
+ for (let i = 0; i < _disposers.length; i++) {
65
+ _disposers[i]();
66
+ }
67
+ _disposers.length = 0;
68
+
69
+ // 2. Remove all event listeners
70
+ for (let i = 0; i < _listeners.length; i++) {
71
+ const { element, event, handler } = _listeners[i];
72
+ element.removeEventListener(event, handler);
73
+ }
74
+ _listeners.length = 0;
75
+
76
+ _cleaned = true;
77
+ }
78
+
79
+ /**
80
+ * Get counts for testing/debugging.
81
+ * @returns {{ effects: number, listeners: number }}
82
+ */
83
+ export function _getCounts() {
84
+ return {
85
+ effects: _disposers.length,
86
+ listeners: _listeners.length
87
+ };
88
+ }
@@ -0,0 +1,309 @@
1
+ const OVERLAY_ID = '__zenith_runtime_error_overlay';
2
+ const MAX_MESSAGE_LENGTH = 120;
3
+ const MAX_HINT_LENGTH = 140;
4
+ const MAX_PATH_LENGTH = 120;
5
+
6
+ const VALID_PHASES = new Set(['hydrate', 'bind', 'render', 'event']);
7
+ const VALID_CODES = new Set([
8
+ 'UNRESOLVED_EXPRESSION',
9
+ 'NON_RENDERABLE_VALUE',
10
+ 'MARKER_MISSING',
11
+ 'FRAGMENT_MOUNT_FAILED',
12
+ 'BINDING_APPLY_FAILED',
13
+ 'EVENT_HANDLER_FAILED'
14
+ ]);
15
+
16
+ function _truncate(input, maxLength) {
17
+ const text = String(input ?? '');
18
+ if (text.length <= maxLength) return text;
19
+ return `${text.slice(0, maxLength - 3)}...`;
20
+ }
21
+
22
+ function _sanitizeAbsolutePaths(value) {
23
+ return String(value ?? '')
24
+ .replace(/[A-Za-z]:\\[^\s"'`]+/g, '<path>')
25
+ .replace(/\/Users\/[^\s"'`]+/g, '<path>')
26
+ .replace(/\/home\/[^\s"'`]+/g, '<path>')
27
+ .replace(/\/private\/[^\s"'`]+/g, '<path>')
28
+ .replace(/\/tmp\/[^\s"'`]+/g, '<path>')
29
+ .replace(/\/var\/folders\/[^\s"'`]+/g, '<path>');
30
+ }
31
+
32
+ function _sanitizeMessage(value) {
33
+ const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
34
+ return _truncate(compact || 'Runtime failure', MAX_MESSAGE_LENGTH);
35
+ }
36
+
37
+ function _sanitizeHint(value) {
38
+ if (value === null || value === undefined || value === false) {
39
+ return undefined;
40
+ }
41
+ const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
42
+ if (!compact) return undefined;
43
+ return _truncate(compact, MAX_HINT_LENGTH);
44
+ }
45
+
46
+ function _sanitizePath(value) {
47
+ if (value === null || value === undefined || value === false) {
48
+ return undefined;
49
+ }
50
+ const compact = _sanitizeAbsolutePaths(value).replace(/\s+/g, ' ').trim();
51
+ if (!compact) return undefined;
52
+ return _truncate(compact, MAX_PATH_LENGTH);
53
+ }
54
+
55
+ function _normalizeMarker(marker) {
56
+ if (!marker || typeof marker !== 'object') return undefined;
57
+ const markerType = _truncate(_sanitizeAbsolutePaths(marker.type || 'data-zx'), 48);
58
+ const markerId = marker.id;
59
+ if (markerId === null || markerId === undefined || markerId === '') return undefined;
60
+ if (typeof markerId === 'number') {
61
+ return { type: markerType, id: markerId };
62
+ }
63
+ return { type: markerType, id: _truncate(_sanitizeAbsolutePaths(markerId), 48) };
64
+ }
65
+
66
+ function _extractErrorMessage(error) {
67
+ if (!error) return '';
68
+ if (typeof error === 'string') return error;
69
+ if (error instanceof Error && typeof error.message === 'string') return error.message;
70
+ if (typeof error.message === 'string') return error.message;
71
+ return String(error);
72
+ }
73
+
74
+ function _safeJson(payload) {
75
+ try {
76
+ return JSON.stringify(payload, null, 2);
77
+ } catch {
78
+ return '{"kind":"ZENITH_RUNTIME_ERROR","message":"Unable to serialize runtime error payload"}';
79
+ }
80
+ }
81
+
82
+ function _isDevDiagnosticsMode() {
83
+ const runtime = typeof globalThis !== 'undefined' ? globalThis : {};
84
+ if (runtime.__ZENITH_RUNTIME_DEV__ === true || runtime.__ZENITH_DEV__ === true) {
85
+ return true;
86
+ }
87
+ if (runtime.__ZENITH_RUNTIME_DEV__ === false || runtime.__ZENITH_DEV__ === false) {
88
+ return false;
89
+ }
90
+ if (runtime.__ZENITH_RUNTIME_PROD__ === true) {
91
+ return false;
92
+ }
93
+ if (typeof location !== 'undefined' && location && typeof location.hostname === 'string') {
94
+ const host = String(location.hostname).toLowerCase();
95
+ if (host === 'localhost' || host === '127.0.0.1' || host === '[::1]') {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+
102
+ function _renderOverlay(payload) {
103
+ if (!_isDevDiagnosticsMode()) return;
104
+ if (typeof document === 'undefined' || !document.body) return;
105
+
106
+ let overlay = document.getElementById(OVERLAY_ID);
107
+ if (!overlay) {
108
+ overlay = document.createElement('aside');
109
+ overlay.id = OVERLAY_ID;
110
+ overlay.setAttribute('role', 'alert');
111
+ overlay.setAttribute('aria-live', 'assertive');
112
+ overlay.style.position = 'fixed';
113
+ overlay.style.left = '12px';
114
+ overlay.style.right = '12px';
115
+ overlay.style.bottom = '12px';
116
+ overlay.style.maxHeight = '45vh';
117
+ overlay.style.overflow = 'auto';
118
+ overlay.style.zIndex = '2147483647';
119
+ overlay.style.padding = '12px';
120
+ overlay.style.border = '1px solid #ff6b6b';
121
+ overlay.style.borderRadius = '8px';
122
+ overlay.style.background = '#111';
123
+ overlay.style.color = '#ffe5e5';
124
+ overlay.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, monospace';
125
+ overlay.style.fontSize = '12px';
126
+ overlay.style.lineHeight = '1.45';
127
+ overlay.style.boxShadow = '0 12px 40px rgba(0,0,0,0.45)';
128
+
129
+ const copyButton = document.createElement('button');
130
+ copyButton.type = 'button';
131
+ copyButton.setAttribute('data-zx-runtime-copy', 'true');
132
+ copyButton.style.marginTop = '8px';
133
+ copyButton.style.padding = '4px 8px';
134
+ copyButton.style.border = '1px solid #ff9d9d';
135
+ copyButton.style.borderRadius = '4px';
136
+ copyButton.style.background = '#2a2a2a';
137
+ copyButton.style.color = '#ffe5e5';
138
+ copyButton.style.cursor = 'pointer';
139
+ copyButton.textContent = 'Copy JSON';
140
+ overlay.appendChild(copyButton);
141
+
142
+ document.body.appendChild(overlay);
143
+ }
144
+
145
+ const textLines = [
146
+ 'Zenith Runtime Error',
147
+ `phase: ${payload.phase}`,
148
+ `code: ${payload.code}`,
149
+ `message: ${payload.message}`
150
+ ];
151
+
152
+ if (payload.marker) {
153
+ textLines.push(`marker: ${payload.marker.type}#${payload.marker.id}`);
154
+ }
155
+ if (payload.path) {
156
+ textLines.push(`path: ${payload.path}`);
157
+ }
158
+ if (payload.hint) {
159
+ textLines.push(`hint: ${payload.hint}`);
160
+ }
161
+
162
+ const jsonText = _safeJson(payload);
163
+ const panelText = textLines.join('\n');
164
+
165
+ let pre = overlay.querySelector('pre[data-zx-runtime-error]');
166
+ if (!pre) {
167
+ pre = document.createElement('pre');
168
+ pre.setAttribute('data-zx-runtime-error', 'true');
169
+ pre.style.margin = '0';
170
+ pre.style.whiteSpace = 'pre-wrap';
171
+ pre.style.wordBreak = 'break-word';
172
+ overlay.insertBefore(pre, overlay.firstChild);
173
+ }
174
+ pre.textContent = panelText;
175
+
176
+ const copyButton = overlay.querySelector('button[data-zx-runtime-copy="true"]');
177
+ if (copyButton) {
178
+ copyButton.onclick = () => {
179
+ const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : null;
180
+ if (clipboard && typeof clipboard.writeText === 'function') {
181
+ void clipboard.writeText(jsonText);
182
+ }
183
+ };
184
+ }
185
+ }
186
+
187
+ function _mapLegacyError(error, fallback) {
188
+ const rawMessage = _extractErrorMessage(error);
189
+ const safeMessage = _sanitizeMessage(rawMessage);
190
+
191
+ const details = {
192
+ phase: VALID_PHASES.has(fallback.phase) ? fallback.phase : 'hydrate',
193
+ code: VALID_CODES.has(fallback.code) ? fallback.code : 'BINDING_APPLY_FAILED',
194
+ message: _sanitizeMessage(fallback.message || safeMessage),
195
+ marker: _normalizeMarker(fallback.marker),
196
+ path: _sanitizePath(fallback.path),
197
+ hint: _sanitizeHint(fallback.hint)
198
+ };
199
+
200
+ if (/failed to resolve expression literal/i.test(rawMessage)) {
201
+ details.phase = 'bind';
202
+ details.code = 'UNRESOLVED_EXPRESSION';
203
+ details.hint = details.hint || 'Verify expression scope keys and signal aliases.';
204
+ } else if (/non-renderable (object|function)/i.test(rawMessage)) {
205
+ details.phase = 'render';
206
+ details.code = 'NON_RENDERABLE_VALUE';
207
+ const match = rawMessage.match(/at\s+([A-Za-z0-9_\[\].-]+)/);
208
+ if (match && !details.path) {
209
+ details.path = _sanitizePath(match[1]);
210
+ }
211
+ details.hint = details.hint || 'Use map() to render object fields into nodes.';
212
+ } else if (/unresolved .* marker index/i.test(rawMessage)) {
213
+ details.phase = 'bind';
214
+ details.code = 'MARKER_MISSING';
215
+ const markerMatch = rawMessage.match(/unresolved\s+(\w+)\s+marker index\s+(\d+)/i);
216
+ if (markerMatch && !details.marker) {
217
+ details.marker = {
218
+ type: `data-zx-${markerMatch[1]}`,
219
+ id: Number(markerMatch[2])
220
+ };
221
+ }
222
+ details.hint = details.hint || 'Confirm SSR markers and client selector tables match.';
223
+ }
224
+
225
+ return details;
226
+ }
227
+
228
+ export function isZenithRuntimeError(error) {
229
+ return !!(
230
+ error &&
231
+ typeof error === 'object' &&
232
+ error.zenithRuntimeError &&
233
+ error.zenithRuntimeError.kind === 'ZENITH_RUNTIME_ERROR'
234
+ );
235
+ }
236
+
237
+ export function createZenithRuntimeError(details, cause) {
238
+ const phase = VALID_PHASES.has(details?.phase) ? details.phase : 'hydrate';
239
+ const code = VALID_CODES.has(details?.code) ? details.code : 'BINDING_APPLY_FAILED';
240
+ const message = _sanitizeMessage(details?.message || 'Runtime failure');
241
+
242
+ const payload = {
243
+ kind: 'ZENITH_RUNTIME_ERROR',
244
+ phase,
245
+ code,
246
+ message
247
+ };
248
+
249
+ const marker = _normalizeMarker(details?.marker);
250
+ if (marker) payload.marker = marker;
251
+
252
+ const path = _sanitizePath(details?.path);
253
+ if (path) payload.path = path;
254
+
255
+ const hint = _sanitizeHint(details?.hint);
256
+ if (hint) payload.hint = hint;
257
+
258
+ const error = new Error(`[Zenith Runtime] ${code}: ${message}`);
259
+ error.name = 'ZenithRuntimeError';
260
+ error.zenithRuntimeError = payload;
261
+ if (cause !== undefined) {
262
+ error.cause = cause;
263
+ }
264
+ error.toJSON = () => payload;
265
+ return error;
266
+ }
267
+
268
+ function _reportRuntimeError(error) {
269
+ if (!error || error.__zenithRuntimeErrorReported === true) return;
270
+ error.__zenithRuntimeErrorReported = true;
271
+ const payload = error.zenithRuntimeError;
272
+ if (payload && typeof console !== 'undefined' && typeof console.error === 'function') {
273
+ console.error('[Zenith Runtime]', payload);
274
+ }
275
+ _renderOverlay(payload);
276
+ }
277
+
278
+ export function throwZenithRuntimeError(details, cause) {
279
+ const error = createZenithRuntimeError(details, cause);
280
+ _reportRuntimeError(error);
281
+ throw error;
282
+ }
283
+
284
+ export function rethrowZenithRuntimeError(error, fallback = {}) {
285
+ if (isZenithRuntimeError(error)) {
286
+ const payload = error.zenithRuntimeError || {};
287
+ let updatedPayload = payload;
288
+ const marker = !payload.marker ? _normalizeMarker(fallback.marker) : payload.marker;
289
+ const path = !payload.path ? _sanitizePath(fallback.path) : payload.path;
290
+ const hint = !payload.hint ? _sanitizeHint(fallback.hint) : payload.hint;
291
+
292
+ if (marker || path || hint) {
293
+ updatedPayload = {
294
+ ...payload,
295
+ ...(marker ? { marker } : null),
296
+ ...(path ? { path } : null),
297
+ ...(hint ? { hint } : null)
298
+ };
299
+ error.zenithRuntimeError = updatedPayload;
300
+ error.toJSON = () => updatedPayload;
301
+ }
302
+ _reportRuntimeError(error);
303
+ throw error;
304
+ }
305
+ const mapped = _mapLegacyError(error, fallback || {});
306
+ const wrapped = createZenithRuntimeError(mapped, error);
307
+ _reportRuntimeError(wrapped);
308
+ throw wrapped;
309
+ }
package/dist/effect.js ADDED
@@ -0,0 +1,3 @@
1
+ // Deprecated compatibility shim.
2
+ // Runtime V0 contract uses explicit `zeneffect(deps, fn)`.
3
+ export { zeneffect as effect } from './zeneffect.js';