@braidlabs/skew 0.1.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.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @braidlabs/skew
2
+
3
+ Framework-agnostic primitives for surviving version skew.
4
+
5
+ No dependencies. No framework. Works in browsers, Node, workers, and Deno.
6
+
7
+ ---
8
+
9
+ ## The problem
10
+
11
+ Four failures that look unrelated are the same failure:
12
+
13
+ | Boundary | What crosses it | Symptom |
14
+ |---|---|---|
15
+ | Client ↔ origin | a lazy chunk request | `ChunkLoadError` after a deploy |
16
+ | Client ↔ API | a queued mutation, flushed later | 400s, or silent data corruption |
17
+ | Host ↔ fragment | props and events | mismatched contracts at runtime |
18
+ | **Past self ↔ present self** | a persisted draft or cache | `undefined` deep in a renderer |
19
+
20
+ In every case, two independently-versioned parties met at a boundary and had no way to discover that they disagreed.
21
+
22
+ This package is that missing primitive: **stamp what crosses a boundary with the version it was authored under, detect disagreement, then migrate forward or fail loudly.**
23
+
24
+ The fourth row is the one most teams miss. A draft written by build 41 and resumed by build 57 is the same problem as a client on 41 calling a server on 57 — the counterparty is just your own past deployment.
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ npm install @braidlabs/skew
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Versioned schemas
37
+
38
+ Declare a type's current version, its history, and the functions that move data between them in one place.
39
+
40
+ ```ts
41
+ import { versioned } from '@braidlabs/skew';
42
+
43
+ // Snapshot shapes — frozen copies, never your live application types.
44
+ interface V1 { id: string; themeQuote?: { text: string } }
45
+ interface V2 { id: string; scriptureOfWeek?: { text: string } }
46
+ type V3 = V2 & { orderOfWorship: { setting: string; hymns: string[] } };
47
+
48
+ export const WeeklyContent = versioned<V1>('weekly-content')
49
+ .next<V2>('rename themeQuote to scriptureOfWeek', (p) => ({
50
+ id: p.id,
51
+ scriptureOfWeek: p.themeQuote,
52
+ }))
53
+ .next<V3>('introduce orderOfWorship', (p) => ({
54
+ ...p,
55
+ orderOfWorship: { setting: '', hymns: [] },
56
+ }));
57
+ ```
58
+
59
+ Reading migrates forward automatically:
60
+
61
+ ```ts
62
+ const result = WeeklyContent.read(rawFromFirestore);
63
+
64
+ if (result.ok) {
65
+ render(result.value); // always the current shape
66
+ if (result.migratedFrom !== null) {
67
+ console.info(`upgraded from v${result.migratedFrom}`);
68
+ }
69
+ }
70
+ ```
71
+
72
+ Each `next()` is typed against the previous version, so a migration that does not actually produce the next shape is a compile error. There is no terminal `build()` call — the chain *is* the schema.
73
+
74
+ ### The one rule
75
+
76
+ **A migration must never import your current application types or services.** Close each step over its own snapshot type (`V1`, `V2`, …). The moment a migration references a live interface, it silently changes meaning the next time that interface is edited, and your old migrations start lying about what they produce.
77
+
78
+ ---
79
+
80
+ ## Results, not exceptions
81
+
82
+ `read()` returns a discriminated result, because the failure modes need *different* remedies:
83
+
84
+ ```ts
85
+ const result = WeeklyContent.read(raw);
86
+
87
+ if (!result.ok) {
88
+ switch (result.reason) {
89
+ case 'ahead': return refetchFromServer(); // written by a NEWER build
90
+ case 'gap': return reportBug(result); // missing migration step
91
+ case 'invalid': return discardAndRefetch();
92
+ case 'threw': return reportBug(result); // a migration failed
93
+ case 'retired': return discardAndRefetch(); // below the declared floor — policy, not a bug
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Why `ahead` matters
99
+
100
+ Data written by a newer build than the one reading it **cannot be migrated downward** — the information genuinely is not there. This is not hypothetical: a colleague saves from the new deploy while your tab is stale, or a user's phone updates before their laptop.
101
+
102
+ Collapsing this into `null` means every caller guesses, and the guess is almost always "discard it" — which destroys perfectly good data that merely came from the future.
103
+
104
+ ---
105
+
106
+ ## Versioned storage
107
+
108
+ The failure this prevents is the quiet one:
109
+
110
+ ```ts
111
+ // Before: an assertion, not a check.
112
+ return JSON.parse(raw) as WeeklyContent;
113
+ ```
114
+
115
+ The moment the model changes, every cached record on every user's machine has the old shape while being *typed* as the new one. You get `undefined` deep inside a renderer instead of a clean failure at the boundary.
116
+
117
+ ```ts
118
+ import { createVersionedStore, webStorageDriver } from '@braidlabs/skew';
119
+
120
+ const drafts = createVersionedStore(WeeklyContent, {
121
+ driver: webStorageDriver('local'),
122
+ buildId: BUILD_ID,
123
+ onReadFailure: (key, failure) => telemetry.warn('stale draft', { key, ...failure }),
124
+ rewriteOnRead: true, // read-repair: persist migrated records at the current version
125
+ });
126
+
127
+ await drafts.set('2026-12-06', content);
128
+ const result = await drafts.get('2026-12-06'); // migrated on the way out
129
+
130
+ // Sync read for signal/hook initialisers — no flash of empty state.
131
+ const immediate = drafts.peek('2026-12-06'); // null on async drivers
132
+ ```
133
+
134
+ Drivers: `memoryDriver()`, `webStorageDriver('local' | 'session')`, or implement `StorageDriver` for IndexedDB or anything else. Web Storage degrades to memory automatically under Safari private mode, disabled cookies, and SSR, and swallows quota errors on write — a failing cache should never break a save the user asked for.
135
+
136
+ ### Adopting on existing data
137
+
138
+ You do not need a backfill. Data with no envelope is treated as **v1**, so declare your *current* shape as the base and records upgrade themselves as users touch them.
139
+
140
+ ```ts
141
+ export const Parish = versioned<CurrentShape>('parish'); // v1, adopts everything
142
+ ```
143
+
144
+ ### Retiring old versions (cleanup)
145
+
146
+ Chains are **append-only at the top and trim-only at the bottom**. A step
147
+ `n → n+1` is deletable only when no data enveloped at ≤ n can still reach a
148
+ reader — so cleanup is a sequence, not an edit:
149
+
150
+ 1. **Instrument**: watch `result.migratedFrom` in telemetry. You can only
151
+ delete steps you can prove are idle.
152
+ 2. **Shrink the tail**: enable `rewriteOnRead` on stores (below), so each old
153
+ record pays its migration once, is re-persisted at the current version,
154
+ and drops out of the telemetry.
155
+ 3. **Trim**: re-declare the schema with the oldest surviving shape as its
156
+ base and delete the retired steps. Never renumber — v4 stays v4.
157
+
158
+ ```ts
159
+ // before: versioned<V1>('draft').next<V2>(…).next<V3>(…).next<V4>(…)
160
+ export const Draft = versioned<V3>('draft', { base: 3 }).next<V4>(…);
161
+ ```
162
+
163
+ Reads below the floor fail with `reason: 'retired'` (plus `floor`) — a
164
+ *policy* outcome whose remedy is discard/refetch/reset — never `gap`, which
165
+ still means "a step is missing and that's a bug". If another bundle on the
166
+ page or a resolved contract still supplies the retired steps via the shared
167
+ registry, the read simply succeeds. Bare (un-enveloped) data is still assumed
168
+ to be v1, so after a trim it surfaces as `retired`; set
169
+ `assumeLegacyVersion: base` only if bare data is known to carry the base
170
+ shape. `write({ as })` below the floor throws.
171
+
172
+ Retire conservatively for data you cannot refetch (drafts, queued outboxes —
173
+ drain queues first and give users a "too old to open" path), aggressively for
174
+ refetchable caches. Steps are cheap; delete with evidence, not tidiness.
175
+
176
+ ---
177
+
178
+ ## Build identity and skew detection
179
+
180
+ ```ts
181
+ import { createVersionProbe } from '@braidlabs/skew';
182
+
183
+ const probe = createVersionProbe({
184
+ identity: { buildId: BUILD_ID, builtAt: BUILT_AT },
185
+ manifestUrl: '/skew-manifest.json', // serve with Cache-Control: no-store
186
+ });
187
+
188
+ const status = await probe.check();
189
+ ```
190
+
191
+ | Status | Meaning | Correct response |
192
+ |---|---|---|
193
+ | `current` | in sync | — |
194
+ | `staleClient` | a newer deploy exists | offer a reload |
195
+ | `staleOrigin` | **origin is older than us** | **do not reload — it will loop** |
196
+ | `differs` | cannot be ordered (no timestamps) | treat conservatively |
197
+ | `unreachable` | offline or blocked | do not reload; you would land on an error page |
198
+
199
+ `staleOrigin` is the case naïve implementations miss. If a CDN is serving a cached entry document or a region is lagging, reloading fetches the same stale bundle and fails again — forever. Detecting it requires comparing build *timestamps*, which is why `builtAt` is worth stamping.
200
+
201
+ The probe collapses concurrent callers onto a single request and caches for `minIntervalMs` (default 10s), so a page that fails three chunks at once still makes one network call.
202
+
203
+ ### The manifest
204
+
205
+ Emit it at build time and serve it uncached:
206
+
207
+ ```json
208
+ {
209
+ "buildId": "a1b2c3d",
210
+ "builtAt": "2026-08-07T10:14:00Z",
211
+ "modules": { "admin.routes": { "file": "chunk-XYZ789.js" } }
212
+ }
213
+ ```
214
+
215
+ `modules` is optional. With it, `moduleWasRemoved(manifest, id)` distinguishes "this route moved" from "this route was deleted" — which is the difference between reloading and redirecting to a fallback.
216
+
217
+ ---
218
+
219
+ ## API
220
+
221
+ | Export | Purpose |
222
+ |---|---|
223
+ | `versioned<T>(name, options?)` | Begin a schema declaration (`base` retires older versions) |
224
+ | `VersionedSchema.next<TNext>(desc?, fn)` | Add a version |
225
+ | `emitSkewTrace` / `SKEW_DEVTOOLS_HOOK` | Devtools trace hook — reads/writes emit events when a hook is installed |
226
+ | `.read(raw)` / `.write(value, buildId?)` | Migrate in / envelope out |
227
+ | `isEnvelope(v)` / `peekVersion(v)` | Inspect without migrating |
228
+ | `createVersionedStore(schema, opts)` | Persistence with migration |
229
+ | `memoryDriver()` / `webStorageDriver()` | Built-in drivers |
230
+ | `createVersionProbe(opts)` | Build comparison against an origin |
231
+ | `compareBuilds(identity, manifest)` | Pure classification |
232
+ | `moduleWasRemoved(manifest, id)` | Route-deleted detection |
233
+ | `isOk` / `isErr` / `valueOr` / `mapResult` | Result helpers |
234
+
235
+ ---
236
+
237
+ ## Related packages
238
+
239
+ `@braidlabs/skew` is consumed by, but never requires, the framework bindings:
240
+
241
+ - `@braidlabs/angular-router` — chunk-load recovery for the Angular router
242
+ - `@braidlabs/angular-data` — versioned cache and durable mutation outbox
243
+ - `@braidlabs/angular-workflow` — durable multi-step flows
244
+ - `@braidlabs/react-*` — the same, for React
245
+ - `@braidlabs/node` / `@braidlabs/nest` — server-side negotiation and manifest serving
246
+
247
+ Each is independently installable. None requires the others.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@braidlabs/skew",
3
+ "version": "0.1.1",
4
+ "description": "Framework-agnostic primitives for surviving version skew: versioned envelopes, migration chains, build identity and negotiation.",
5
+ "keywords": [
6
+ "version-skew",
7
+ "migration",
8
+ "schema-versioning",
9
+ "deployment",
10
+ "offline"
11
+ ],
12
+ "license": "MIT",
13
+ "sideEffects": false,
14
+ "type": "module",
15
+ "main": "./src/index.js",
16
+ "types": "./src/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.d.ts",
20
+ "default": "./src/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/braidjs/braid.git",
30
+ "directory": "libs/skew"
31
+ },
32
+ "homepage": "https://github.com/braidjs/braid#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/braidjs/braid/issues"
35
+ },
36
+ "module": "./src/index.js"
37
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from './lib/result.js';
2
+ export * from './lib/context.js';
3
+ export * from './lib/versioned.js';
4
+ export * from './lib/lens.js';
5
+ export * from './lib/list.js';
6
+ export * from './lib/registry.js';
7
+ export * from './lib/fingerprint.js';
8
+ export * from './lib/http.js';
9
+ export * from './lib/identity.js';
10
+ export * from './lib/storage.js';
11
+ export * from './lib/devtools.js';
12
+ export * from './lib/disabled.js';
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export * from './lib/result.js';
2
+ export * from './lib/context.js';
3
+ export * from './lib/versioned.js';
4
+ export * from './lib/lens.js';
5
+ export * from './lib/list.js';
6
+ export * from './lib/registry.js';
7
+ export * from './lib/fingerprint.js';
8
+ export * from './lib/http.js';
9
+ export * from './lib/identity.js';
10
+ export * from './lib/storage.js';
11
+ export * from './lib/devtools.js';
12
+ // Not public API. Undocumented on purpose — read the module comment first.
13
+ export * from './lib/disabled.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/skew/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAElC,2EAA2E;AAC3E,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The context handed to every migration function.
3
+ *
4
+ * Migrations must be deterministic: the same input must produce the same
5
+ * output, or replays, tests, memoization, and fingerprinting all quietly
6
+ * break. Anything a migration needs from the outside world therefore arrives
7
+ * through this context — never through `new Date()` or `Math.random()` inside
8
+ * the migration body — so a test (or a replay) can pin it.
9
+ */
10
+ export interface MigrationContext {
11
+ /**
12
+ * The clock. A migration that needs "now" (an `asOf` stamp, a derived
13
+ * timestamp) must take it from here so two runs over the same input can be
14
+ * made to agree.
15
+ */
16
+ now(): Date;
17
+ /**
18
+ * A stable seed for anything that must be unique-but-reproducible — a
19
+ * derived idempotency key, a placeholder reference. Callers that retry a
20
+ * migration (an outbox, a replay) pass the same seed each attempt so the
21
+ * derived value stays identical across attempts.
22
+ */
23
+ readonly seed?: string;
24
+ }
25
+ /** The context used when a caller supplies none: the real clock, no seed. */
26
+ export declare const defaultMigrationContext: MigrationContext;
@@ -0,0 +1,5 @@
1
+ /** The context used when a caller supplies none: the real clock, no seed. */
2
+ export const defaultMigrationContext = {
3
+ now: () => new Date(),
4
+ };
5
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../../../libs/skew/src/lib/context.ts"],"names":[],"mappings":"AAyBA,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAqB;IACvD,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;CACtB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The devtools trace hook.
3
+ *
4
+ * Follows the React-devtools pattern: a tool that wants to observe skew
5
+ * activity installs a global hook *before* the app boots, and the library
6
+ * emits one event per `read()` / `write()` into it. When no hook is
7
+ * installed (production, tests, every app that never opens the tools) the
8
+ * cost is one property lookup per call and nothing is retained.
9
+ *
10
+ * Events deliberately carry versions, reasons, and paths — never payloads.
11
+ * Payload capture is a tool-side, opt-in concern (drafts contain user data);
12
+ * the library's default must be safe to leave enabled everywhere.
13
+ */
14
+ /** One observed schema operation. */
15
+ export interface SkewTraceEvent {
16
+ readonly kind: 'read' | 'write';
17
+ /** Schema / contract name. */
18
+ readonly schema: string;
19
+ /**
20
+ * Version the data claimed (envelope `v`, or the assumed version for bare
21
+ * data). For writes: the version of the value being written (the chain's
22
+ * current version).
23
+ */
24
+ readonly from: number;
25
+ /**
26
+ * The reader's current version (reads), or the target version being
27
+ * written at (writes — differs from `from` on a `write({ as })`).
28
+ */
29
+ readonly to: number;
30
+ /** Read outcome. Always `true` for writes — a failed write throws. */
31
+ readonly ok: boolean;
32
+ /** Failure reason, when `ok` is false. */
33
+ readonly reason?: string;
34
+ readonly migratedFrom?: number | null;
35
+ readonly downgradedFrom?: number | null;
36
+ readonly derivedPaths?: readonly string[];
37
+ readonly lossyPaths?: readonly string[];
38
+ /** Epoch milliseconds. */
39
+ readonly ts: number;
40
+ }
41
+ /** What a devtools implementation installs at `globalThis.__SKEW_DEVTOOLS_HOOK__`. */
42
+ export interface SkewDevtoolsHook {
43
+ emit(event: SkewTraceEvent): void;
44
+ }
45
+ /** The global property devtools implementations install themselves under. */
46
+ export declare const SKEW_DEVTOOLS_HOOK = "__SKEW_DEVTOOLS_HOOK__";
47
+ /**
48
+ * Emits a trace event into the installed hook, if any. A throwing hook is
49
+ * swallowed — observability must never break the app it observes.
50
+ */
51
+ export declare function emitSkewTrace(event: SkewTraceEvent): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The devtools trace hook.
3
+ *
4
+ * Follows the React-devtools pattern: a tool that wants to observe skew
5
+ * activity installs a global hook *before* the app boots, and the library
6
+ * emits one event per `read()` / `write()` into it. When no hook is
7
+ * installed (production, tests, every app that never opens the tools) the
8
+ * cost is one property lookup per call and nothing is retained.
9
+ *
10
+ * Events deliberately carry versions, reasons, and paths — never payloads.
11
+ * Payload capture is a tool-side, opt-in concern (drafts contain user data);
12
+ * the library's default must be safe to leave enabled everywhere.
13
+ */
14
+ /** The global property devtools implementations install themselves under. */
15
+ export const SKEW_DEVTOOLS_HOOK = '__SKEW_DEVTOOLS_HOOK__';
16
+ /**
17
+ * Emits a trace event into the installed hook, if any. A throwing hook is
18
+ * swallowed — observability must never break the app it observes.
19
+ */
20
+ export function emitSkewTrace(event) {
21
+ const hook = globalThis[SKEW_DEVTOOLS_HOOK];
22
+ if (!hook || typeof hook.emit !== 'function')
23
+ return;
24
+ try {
25
+ hook.emit(event);
26
+ }
27
+ catch (_a) {
28
+ // The hook is diagnostic machinery; its failures are its own.
29
+ }
30
+ }
31
+ //# sourceMappingURL=devtools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools.js","sourceRoot":"","sources":["../../../../../libs/skew/src/lib/devtools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmCH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAE3D;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAqB;IACjD,MAAM,IAAI,GAAI,UAAsC,CAAC,kBAAkB,CAE1D,CAAC;IACd,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO;IACrD,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAAC,WAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;AACH,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ⚠️ NOT PUBLIC API. Undocumented on purpose. Do not use in an application.
3
+ *
4
+ * A process-wide switch that makes every `@braidlabs` package behave as though it
5
+ * were not installed:
6
+ *
7
+ * - stores write bare payloads and read them back with no envelope check,
8
+ * which is `JSON.parse(raw) as T` — an assertion, not a check
9
+ * - `versioned().read()` returns whatever it was handed, unmigrated
10
+ * - `lazy()` stops retrying
11
+ * - recovery stops classifying and takes no action
12
+ *
13
+ * ## Why this exists
14
+ *
15
+ * The demos need to show the failure modes these packages prevent, and the only
16
+ * trustworthy way to show a failure is to actually produce one. A screenshot of
17
+ * a hand-written "before" snippet proves nothing; running the same scenario
18
+ * with the protections removed proves it exactly.
19
+ *
20
+ * ## Why it is not documented
21
+ *
22
+ * There is no legitimate production use. Every reason to reach for it — "just
23
+ * while I debug this", "only in staging" — ends with a flag nobody remembers
24
+ * set on a deployment nobody re-checks, and the failures it re-enables are
25
+ * silent ones: a draft read at the wrong shape, a mutation replayed against a
26
+ * contract that moved. Those surface as `undefined` in a renderer weeks later,
27
+ * far from the switch that caused them.
28
+ *
29
+ * So it ships, because the demos are more valuable than the purity of the
30
+ * export list, but it stays out of the README, out of the package docs, and out
31
+ * of every example. If you found this by reading the source: that was the deal.
32
+ * Turning it on logs a warning for the same reason.
33
+ *
34
+ * ## Why it is global mutable state
35
+ *
36
+ * `@braidlabs/skew` has no framework and no dependencies, so there is no injector to
37
+ * read from. A module-level flag is the only mechanism available to it. The
38
+ * Angular packages wrap this in `provideSkewDisabled()` so an application at
39
+ * least *looks* like it is configuring something, but the state is here.
40
+ *
41
+ * One consequence worth knowing: under Module Federation this is per *bundle
42
+ * instance*, so it only behaves as one switch when `@braidlabs/skew` is genuinely
43
+ * shared as a singleton. Two copies of core means two flags.
44
+ */
45
+ /** @internal Not public API — see the module comment. */
46
+ export declare function setSkewDisabled(next: boolean): void;
47
+ /** @internal Not public API — see the module comment. */
48
+ export declare function isSkewDisabled(): boolean;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ⚠️ NOT PUBLIC API. Undocumented on purpose. Do not use in an application.
3
+ *
4
+ * A process-wide switch that makes every `@braidlabs` package behave as though it
5
+ * were not installed:
6
+ *
7
+ * - stores write bare payloads and read them back with no envelope check,
8
+ * which is `JSON.parse(raw) as T` — an assertion, not a check
9
+ * - `versioned().read()` returns whatever it was handed, unmigrated
10
+ * - `lazy()` stops retrying
11
+ * - recovery stops classifying and takes no action
12
+ *
13
+ * ## Why this exists
14
+ *
15
+ * The demos need to show the failure modes these packages prevent, and the only
16
+ * trustworthy way to show a failure is to actually produce one. A screenshot of
17
+ * a hand-written "before" snippet proves nothing; running the same scenario
18
+ * with the protections removed proves it exactly.
19
+ *
20
+ * ## Why it is not documented
21
+ *
22
+ * There is no legitimate production use. Every reason to reach for it — "just
23
+ * while I debug this", "only in staging" — ends with a flag nobody remembers
24
+ * set on a deployment nobody re-checks, and the failures it re-enables are
25
+ * silent ones: a draft read at the wrong shape, a mutation replayed against a
26
+ * contract that moved. Those surface as `undefined` in a renderer weeks later,
27
+ * far from the switch that caused them.
28
+ *
29
+ * So it ships, because the demos are more valuable than the purity of the
30
+ * export list, but it stays out of the README, out of the package docs, and out
31
+ * of every example. If you found this by reading the source: that was the deal.
32
+ * Turning it on logs a warning for the same reason.
33
+ *
34
+ * ## Why it is global mutable state
35
+ *
36
+ * `@braidlabs/skew` has no framework and no dependencies, so there is no injector to
37
+ * read from. A module-level flag is the only mechanism available to it. The
38
+ * Angular packages wrap this in `provideSkewDisabled()` so an application at
39
+ * least *looks* like it is configuring something, but the state is here.
40
+ *
41
+ * One consequence worth knowing: under Module Federation this is per *bundle
42
+ * instance*, so it only behaves as one switch when `@braidlabs/skew` is genuinely
43
+ * shared as a singleton. Two copies of core means two flags.
44
+ */
45
+ let disabled = false;
46
+ /** @internal Not public API — see the module comment. */
47
+ export function setSkewDisabled(next) {
48
+ if (next && !disabled) {
49
+ console.warn('[skew] PROTECTIONS DISABLED. Envelopes, migrations, retries and ' +
50
+ 'recovery are all inert. This is a demo/debugging switch and is not ' +
51
+ 'supported in production.');
52
+ }
53
+ disabled = next;
54
+ }
55
+ /** @internal Not public API — see the module comment. */
56
+ export function isSkewDisabled() {
57
+ return disabled;
58
+ }
59
+ //# sourceMappingURL=disabled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disabled.js","sourceRoot":"","sources":["../../../../../libs/skew/src/lib/disabled.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,IAAI,QAAQ,GAAG,KAAK,CAAC;AAErB,yDAAyD;AACzD,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CACV,kEAAkE;YAChE,qEAAqE;YACrE,0BAA0B,CAC7B,CAAC;IACJ,CAAC;IACD,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,cAAc;IAC5B,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Content fingerprints for schemas and their steps.
3
+ *
4
+ * Two independently built bundles can declare a chain under the same contract
5
+ * name. When their declarations *agree*, that duplication is harmless; when
6
+ * they disagree — same name, same version numbers, different meaning — the
7
+ * result is the silent kind of corruption nothing else in the system can see.
8
+ * A fingerprint makes "do these two declarations agree?" a cheap, answerable
9
+ * question.
10
+ *
11
+ * What goes into a step's fingerprint is deliberate:
12
+ *
13
+ * - For an ops step, the canonical JSON of the ops — the ops *are* the
14
+ * migration, so structural equality is real equality.
15
+ * - For a code step, the target version and the (required) description — and
16
+ * **not** `Function.prototype.toString()`. Two builds minify the same source
17
+ * differently, so function text equality would report false conflicts
18
+ * between builds that genuinely agree. The description is the human-owned
19
+ * identity of a code step; that is one of the reasons it is required.
20
+ */
21
+ import type { MigrationStep } from './versioned.js';
22
+ /** FNV-1a, 32-bit, hex output. Small, stable, dependency-free. */
23
+ export declare function fnv1a(text: string): string;
24
+ /**
25
+ * JSON with object keys sorted at every level, so logically identical
26
+ * documents always serialize identically.
27
+ */
28
+ export declare function canonicalJson(value: unknown): string;
29
+ /** Fingerprint of a single migration step. */
30
+ export declare function stepFingerprint(step: MigrationStep): string;
31
+ /** Fingerprint of a whole chain: name, current version, and every step. */
32
+ export declare function chainFingerprint(name: string, version: number, steps: readonly (MigrationStep | undefined)[]): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Content fingerprints for schemas and their steps.
3
+ *
4
+ * Two independently built bundles can declare a chain under the same contract
5
+ * name. When their declarations *agree*, that duplication is harmless; when
6
+ * they disagree — same name, same version numbers, different meaning — the
7
+ * result is the silent kind of corruption nothing else in the system can see.
8
+ * A fingerprint makes "do these two declarations agree?" a cheap, answerable
9
+ * question.
10
+ *
11
+ * What goes into a step's fingerprint is deliberate:
12
+ *
13
+ * - For an ops step, the canonical JSON of the ops — the ops *are* the
14
+ * migration, so structural equality is real equality.
15
+ * - For a code step, the target version and the (required) description — and
16
+ * **not** `Function.prototype.toString()`. Two builds minify the same source
17
+ * differently, so function text equality would report false conflicts
18
+ * between builds that genuinely agree. The description is the human-owned
19
+ * identity of a code step; that is one of the reasons it is required.
20
+ */
21
+ /** FNV-1a, 32-bit, hex output. Small, stable, dependency-free. */
22
+ export function fnv1a(text) {
23
+ let hash = 0x811c9dc5;
24
+ for (let i = 0; i < text.length; i++) {
25
+ hash ^= text.charCodeAt(i);
26
+ hash = Math.imul(hash, 0x01000193);
27
+ }
28
+ return (hash >>> 0).toString(16).padStart(8, '0');
29
+ }
30
+ /**
31
+ * JSON with object keys sorted at every level, so logically identical
32
+ * documents always serialize identically.
33
+ */
34
+ export function canonicalJson(value) {
35
+ return JSON.stringify(sortKeys(value));
36
+ }
37
+ function sortKeys(value) {
38
+ if (Array.isArray(value))
39
+ return value.map(sortKeys);
40
+ if (typeof value === 'object' && value !== null) {
41
+ const source = value;
42
+ const sorted = {};
43
+ for (const key of Object.keys(source).sort()) {
44
+ sorted[key] = sortKeys(source[key]);
45
+ }
46
+ return sorted;
47
+ }
48
+ return value;
49
+ }
50
+ /** Fingerprint of a single migration step. */
51
+ export function stepFingerprint(step) {
52
+ const body = step.ops ? canonicalJson(step.ops) : 'code';
53
+ return fnv1a(`${step.to}:${step.description}:${body}`);
54
+ }
55
+ /** Fingerprint of a whole chain: name, current version, and every step. */
56
+ export function chainFingerprint(name, version, steps) {
57
+ // A hole (a known-of but not-runnable step) fingerprints as such — two
58
+ // builds missing the same implementation still agree about the chain.
59
+ const parts = steps.map((s) => (s ? stepFingerprint(s) : 'hole')).join('.');
60
+ return fnv1a(`${name}@${version}:${parts}`);
61
+ }
62
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../../../libs/skew/src/lib/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,kEAAkE;AAClE,MAAM,UAAU,KAAK,CAAC,IAAY;IAChC,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,KAAgC,CAAC;QAChD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACzD,OAAO,KAAK,CAAC,GAAG,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,OAAe,EACf,KAA6C;IAE7C,uEAAuE;IACvE,sEAAsE;IACtE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5E,OAAO,KAAK,CAAC,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC;AAC9C,CAAC"}