@braidlabs/contract 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,95 @@
1
+ # `@braidlabs/contract`
2
+
3
+ Data-driven schema migrations: the API that owns a contract **publishes its
4
+ history as a document**, and consumers — including consumers built before the
5
+ newest version existed — fetch it, interpret it, and migrate in either
6
+ direction. No client redeploy required.
7
+
8
+ ```jsonc
9
+ // GET /.well-known/skew/contracts/portfolio-fund
10
+ {
11
+ "skewContract": "1",
12
+ "name": "portfolio-fund",
13
+ "current": 2,
14
+ "steps": [
15
+ {
16
+ "from": 1, "to": 2,
17
+ "description": "promote scalars to structure; add liquidity fields",
18
+ "ops": [
19
+ { "rename": { "from": "currency", "to": "baseCurrency" } },
20
+ { "wrap": { "path": "nav", "key": "amount", "also": { "asOf": { "$now": true } } } },
21
+ { "move": { "from": "cashPct", "to": "liquidity.cashPct" } },
22
+ { "default": { "path": "liquidity.hqlaPct", "value": 0 } }
23
+ ]
24
+ }
25
+ ],
26
+ "schemas": { "1": { /* JSON Schema */ }, "2": { /* JSON Schema */ } }
27
+ }
28
+ ```
29
+
30
+ Nothing in a document is executable. Ops are interpreted against a closed
31
+ whitelist (`rename`, `move`, `wrap`, `hoist`, `map`, `default`, `drop`,
32
+ `convert`, `const`), and each op knows its inverse — so declaring the up
33
+ migration buys the down migration, with the guessed fields (`derivedPaths`)
34
+ and the discarded ones (`lossyPaths`) computed rather than hand-annotated.
35
+
36
+ ## Why this exists
37
+
38
+ The `ahead` failure — data written by a newer build than the reader — is the
39
+ one failure a code-shipped migration chain can never fix: the reader was built
40
+ before the knowledge existed. But **an API is always at least as new as the
41
+ newest data it serves.** So the origin that produced the too-new data can also
42
+ serve the document that explains it:
43
+
44
+ ```ts
45
+ import { createContractResolver, wellKnownContractUrl } from '@braidlabs/contract';
46
+
47
+ const resolver = createContractResolver();
48
+ const url = wellKnownContractUrl(API_BASE, 'portfolio-fund');
49
+
50
+ // Reads exactly like schema.read(); the difference only appears on `ahead`:
51
+ // fetch the contract, learn the newer steps, read a downgraded projection.
52
+ const result = await resolver.readResolving(FundSchemaV1, body, url);
53
+ if (result.ok && result.downgradedFrom) {
54
+ // honest, lossy, and labeled: result.lossyPaths names what v1 cannot carry
55
+ }
56
+ ```
57
+
58
+ ## Building schemas from a document
59
+
60
+ ```ts
61
+ import { versionedFromContract } from '@braidlabs/contract';
62
+
63
+ // Drop-in replacement for a hand-maintained versioned().next() chain:
64
+ const FundSchema = versionedFromContract<FundV2>(fundContract);
65
+
66
+ // A build pinned below current still learns the newer steps — they feed the
67
+ // shared registry, which is what read() consults to downgrade newer data:
68
+ const FundSchemaV1 = versionedFromContract<FundV1>(fundContract, { at: 1 });
69
+ ```
70
+
71
+ Steps a document cannot express (semantic transforms) are declared as
72
+ `"code": "derive-summary"` — the *name* of an implementation the consuming
73
+ bundle ships via `codeSteps`. A consumer without the implementation degrades
74
+ loudly with `gap`, never with a guess. A contract accumulating `code` steps is
75
+ a signal the change wanted a new resource, not a new version.
76
+
77
+ ## Frozen types, generated
78
+
79
+ `skew-contract gen` (in `@braidlabs/build`) emits one frozen interface per
80
+ documented version plus the document as a typed const, which retires the
81
+ hardest rule in schema versioning — "never edit a past version's interface" —
82
+ by making the document the only source:
83
+
84
+ ```sh
85
+ skew-contract gen --in contracts/portfolio-fund.json \
86
+ --out src/generated/portfolio-fund.contract.ts
87
+ ```
88
+
89
+ ## Trust model
90
+
91
+ A contract document is fetched from the same origin whose data you already
92
+ trust, carries no executable code, is cached with ETag revalidation (a stale
93
+ copy still beats none when the origin is unreachable), and can be pinned by
94
+ content fingerprint (`pinnedFingerprints`) when the deployment pipeline knows
95
+ exactly which contract it was built against.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@braidlabs/contract",
3
+ "version": "0.1.1",
4
+ "description": "Data-driven schema migrations: publish a contract document from your API, resolve it at runtime, and migrate in both directions without shipping new client code.",
5
+ "keywords": [
6
+ "version-skew",
7
+ "migration",
8
+ "schema-versioning",
9
+ "api-contract",
10
+ "lens"
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
+ "peerDependencies": {
25
+ "@braidlabs/skew": "^0.1.1"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/braidjs/braid.git",
33
+ "directory": "libs/contract"
34
+ },
35
+ "homepage": "https://github.com/braidjs/braid#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/braidjs/braid/issues"
38
+ },
39
+ "module": "./src/index.js"
40
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './lib/document.js';
2
+ export * from './lib/schema-from-contract.js';
3
+ export * from './lib/resolver.js';
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './lib/document.js';
2
+ export * from './lib/schema-from-contract.js';
3
+ export * from './lib/resolver.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/contract/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { LensOp } from '@braidlabs/skew';
2
+ /**
3
+ * The contract document — a schema's history as *data*.
4
+ *
5
+ * A closure-based migration chain can only travel inside the bundle that
6
+ * compiled it, which is why every consumer of a contract ends up hand-copying
7
+ * frozen interfaces and hoping they stay in agreement. A contract document is
8
+ * the same knowledge made portable: published by the API that owns the
9
+ * contract, fetched (or codegen'd) by consumers, interpreted — never
10
+ * executed — on arrival.
11
+ *
12
+ * The property that makes runtime resolution worth having: **an API is always
13
+ * at least as new as the newest data it serves.** A client that hits `ahead`
14
+ * can fetch the contract from the very origin that produced the too-new data,
15
+ * obtain the steps it was born too early to know, and migrate down. The
16
+ * `ahead` dead end resolves itself without redeploying the client.
17
+ *
18
+ * Each step is either:
19
+ *
20
+ * - `ops` — declarative, invertible-by-construction where the ops allow, the
21
+ * default for structural evolution; or
22
+ * - `code` — the name of a migration this document *cannot* express (a
23
+ * semantic transform). Consumers that carry an implementation under that
24
+ * name run it; consumers that don't degrade loudly with `gap`. A contract
25
+ * accumulating `code` steps is a signal the change wanted a new resource,
26
+ * not a new version.
27
+ */
28
+ export interface SkewContractStep {
29
+ readonly from: number;
30
+ readonly to: number;
31
+ readonly description: string;
32
+ readonly ops?: readonly LensOp[];
33
+ readonly code?: string;
34
+ }
35
+ export interface SkewContractDocument {
36
+ /** Format version of the document itself. */
37
+ readonly skewContract: '1';
38
+ /** The contract's name — the same string consumers pass to `versioned()`. */
39
+ readonly name: string;
40
+ /** The newest version the publisher serves. */
41
+ readonly current: number;
42
+ /** Every transition, oldest first, covering v1 → `current` contiguously. */
43
+ readonly steps: readonly SkewContractStep[];
44
+ /** Optional JSON Schema per version, keyed by version number as a string. */
45
+ readonly schemas?: Readonly<Record<string, unknown>>;
46
+ }
47
+ /** The well-known path a Skew-aware origin publishes contracts under. */
48
+ export declare function wellKnownContractPath(name: string): string;
49
+ /** Absolute or origin-relative URL for a contract on a given base. */
50
+ export declare function wellKnownContractUrl(base: string, name: string): string;
51
+ /**
52
+ * Content fingerprint of a document — order-insensitive for object keys, so
53
+ * logically identical documents always agree. Used for pinning and as an
54
+ * ETag.
55
+ */
56
+ export declare function contractFingerprint(doc: SkewContractDocument): string;
57
+ /**
58
+ * Validates an untrusted value as a contract document. Throws `TypeError`
59
+ * with a message naming the first problem — a malformed contract must fail at
60
+ * the boundary where it arrived, not later inside a migration.
61
+ */
62
+ export declare function parseContractDocument(raw: unknown): SkewContractDocument;
@@ -0,0 +1,76 @@
1
+ import { canonicalJson, fnv1a } from '@braidlabs/skew';
2
+ /** The well-known path a Skew-aware origin publishes contracts under. */
3
+ export function wellKnownContractPath(name) {
4
+ return `/.well-known/skew/contracts/${encodeURIComponent(name)}`;
5
+ }
6
+ /** Absolute or origin-relative URL for a contract on a given base. */
7
+ export function wellKnownContractUrl(base, name) {
8
+ return `${base.replace(/\/$/, '')}${wellKnownContractPath(name)}`;
9
+ }
10
+ /**
11
+ * Content fingerprint of a document — order-insensitive for object keys, so
12
+ * logically identical documents always agree. Used for pinning and as an
13
+ * ETag.
14
+ */
15
+ export function contractFingerprint(doc) {
16
+ return fnv1a(canonicalJson(doc));
17
+ }
18
+ /**
19
+ * Validates an untrusted value as a contract document. Throws `TypeError`
20
+ * with a message naming the first problem — a malformed contract must fail at
21
+ * the boundary where it arrived, not later inside a migration.
22
+ */
23
+ export function parseContractDocument(raw) {
24
+ const fail = (message) => {
25
+ throw new TypeError(`skew contract: ${message}`);
26
+ };
27
+ if (typeof raw !== 'object' || raw === null)
28
+ fail('document must be an object');
29
+ const doc = raw;
30
+ if (doc['skewContract'] !== '1') {
31
+ fail(`unsupported document format "${String(doc['skewContract'])}" — this reader understands "1"`);
32
+ }
33
+ if (typeof doc['name'] !== 'string' || doc['name'].length === 0) {
34
+ fail('name must be a non-empty string');
35
+ }
36
+ const current = doc['current'];
37
+ if (!Number.isInteger(current) || current < 1) {
38
+ fail(`current must be a positive integer, got ${String(current)}`);
39
+ }
40
+ if (!Array.isArray(doc['steps']))
41
+ fail('steps must be an array');
42
+ const steps = doc['steps'];
43
+ if (steps.length !== current - 1) {
44
+ fail(`steps must cover v1 → v${String(current)} contiguously: expected ${current - 1} steps, got ${steps.length}`);
45
+ }
46
+ steps.forEach((entry, index) => {
47
+ if (typeof entry !== 'object' || entry === null)
48
+ fail(`step ${index} must be an object`);
49
+ const step = entry;
50
+ const expectedTo = index + 2;
51
+ if (step['to'] !== expectedTo) {
52
+ fail(`step ${index} must target v${expectedTo}, got ${String(step['to'])}`);
53
+ }
54
+ if (step['from'] !== expectedTo - 1) {
55
+ fail(`step ${index} must come from v${expectedTo - 1}, got ${String(step['from'])}`);
56
+ }
57
+ if (typeof step['description'] !== 'string' || step['description'].length === 0) {
58
+ fail(`step ${index} needs a description`);
59
+ }
60
+ const hasOps = step['ops'] !== undefined;
61
+ const hasCode = step['code'] !== undefined;
62
+ if (hasOps === hasCode) {
63
+ fail(`step ${index} must declare exactly one of "ops" or "code"`);
64
+ }
65
+ if (hasOps && !Array.isArray(step['ops']))
66
+ fail(`step ${index} ops must be an array`);
67
+ if (hasCode && (typeof step['code'] !== 'string' || step['code'].length === 0)) {
68
+ fail(`step ${index} code must be a non-empty migration name`);
69
+ }
70
+ });
71
+ if (doc['schemas'] !== undefined && (typeof doc['schemas'] !== 'object' || doc['schemas'] === null)) {
72
+ fail('schemas, when present, must be an object keyed by version');
73
+ }
74
+ return raw;
75
+ }
76
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../../../../../libs/contract/src/lib/document.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { ReadOptions, SkewResult, VersionedSchema } from '@braidlabs/skew';
2
+ import { SkewContractDocument } from './document.js';
3
+ import { ContractCodeStep } from './schema-from-contract.js';
4
+ /**
5
+ * Runtime contract resolution — the async layer over `@braidlabs/skew`'s
6
+ * synchronous reads.
7
+ *
8
+ * Trust model: a contract document is fetched from the same origin whose
9
+ * *data* you already trust, and nothing in it is executable — ops are
10
+ * interpreted against a closed whitelist, and `code` steps only name
11
+ * implementations the consuming bundle itself ships. For belt-and-braces
12
+ * deployments, `pinnedFingerprints` refuses a document whose content hash
13
+ * moved.
14
+ */
15
+ export interface ContractResolverOptions {
16
+ /** Defaults to the global `fetch`. Injected for tests and odd runtimes. */
17
+ readonly fetchImpl?: typeof fetch;
18
+ /** Implementations for named `code` steps, passed through to every load. */
19
+ readonly codeSteps?: Readonly<Record<string, ContractCodeStep<any, any>>>;
20
+ /**
21
+ * Expected content fingerprint per contract URL. A resolved document whose
22
+ * fingerprint differs is refused — use when the deployment pipeline knows
23
+ * exactly which contract it was built against.
24
+ */
25
+ readonly pinnedFingerprints?: Readonly<Record<string, string>>;
26
+ /**
27
+ * List-schema names to derive per contract name. An API publishes the
28
+ * *item* contract (`portfolio-fund`); a client often reads the collection
29
+ * through its own list schema (`portfolio-funds`). Declaring
30
+ * `{ 'portfolio-fund': 'portfolio-funds' }` here makes every resolve also
31
+ * register the item steps lifted element-wise under the list name.
32
+ */
33
+ readonly lists?: Readonly<Record<string, string>>;
34
+ }
35
+ export interface ContractResolver {
36
+ /**
37
+ * Fetches, validates, and caches a contract document. Subsequent calls for
38
+ * the same URL revalidate with `If-None-Match` when the origin sent an
39
+ * ETag, and fall back to the cached copy when the origin is unreachable —
40
+ * a stale contract is still a better guide than none.
41
+ */
42
+ resolve(url: string): Promise<SkewContractDocument>;
43
+ /**
44
+ * Resolves a document and registers every runnable step with the shared
45
+ * registry — after which any schema of the same contract name, in any
46
+ * bundle of the page, can read across versions it never shipped.
47
+ */
48
+ resolveAndRegister(url: string): Promise<SkewContractDocument>;
49
+ /**
50
+ * The `ahead` cure, packaged: read; if the result is `ahead`, resolve the
51
+ * contract (which the origin serves, and the origin is always at least as
52
+ * new as the data it produced), then read again with the newly registered
53
+ * down-steps. Any other outcome — success or failure — passes through
54
+ * untouched.
55
+ */
56
+ readResolving<T>(schema: VersionedSchema<T>, raw: unknown, contractUrl: string, options?: ReadOptions): Promise<SkewResult<T>>;
57
+ /** Drops the cache — the next resolve refetches unconditionally. */
58
+ invalidate(url?: string): void;
59
+ }
60
+ export declare function createContractResolver(options?: ContractResolverOptions): ContractResolver;
@@ -0,0 +1,97 @@
1
+ import { __awaiter } from "tslib";
2
+ import { registerSchema, versionedList, } from '@braidlabs/skew';
3
+ import { contractFingerprint, parseContractDocument, } from './document.js';
4
+ import { versionedFromContract } from './schema-from-contract.js';
5
+ export function createContractResolver(options = {}) {
6
+ var _a;
7
+ const cache = new Map();
8
+ const inFlight = new Map();
9
+ // Wrapped rather than referenced: a bare `fetch` alias loses its `window`
10
+ // receiver in browsers and throws "Illegal invocation".
11
+ const doFetch = (_a = options.fetchImpl) !== null && _a !== void 0 ? _a : ((input, init) => fetch(input, init));
12
+ function fetchDocument(url) {
13
+ return __awaiter(this, void 0, void 0, function* () {
14
+ var _a;
15
+ const cached = cache.get(url);
16
+ const headers = { accept: 'application/json' };
17
+ if (cached === null || cached === void 0 ? void 0 : cached.etag)
18
+ headers['if-none-match'] = cached.etag;
19
+ let response;
20
+ try {
21
+ response = yield doFetch(url, { headers });
22
+ }
23
+ catch (cause) {
24
+ if (cached)
25
+ return cached.doc;
26
+ const unreachable = new Error(`skew contract: could not reach ${url}`);
27
+ unreachable.cause = cause;
28
+ throw unreachable;
29
+ }
30
+ if (response.status === 304 && cached)
31
+ return cached.doc;
32
+ if (!response.ok) {
33
+ if (cached)
34
+ return cached.doc;
35
+ throw new Error(`skew contract: ${url} answered HTTP ${response.status}`);
36
+ }
37
+ const doc = parseContractDocument(yield response.json());
38
+ const pinned = (_a = options.pinnedFingerprints) === null || _a === void 0 ? void 0 : _a[url];
39
+ if (pinned !== undefined) {
40
+ const actual = contractFingerprint(doc);
41
+ if (actual !== pinned) {
42
+ throw new Error(`skew contract: ${url} resolved to fingerprint ${actual}, but this build pins ${pinned} — refusing the document`);
43
+ }
44
+ }
45
+ cache.set(url, { doc, etag: response.headers.get('etag') });
46
+ return doc;
47
+ });
48
+ }
49
+ function resolve(url) {
50
+ const pending = inFlight.get(url);
51
+ if (pending)
52
+ return pending;
53
+ const request = fetchDocument(url).finally(() => inFlight.delete(url));
54
+ inFlight.set(url, request);
55
+ return request;
56
+ }
57
+ return {
58
+ resolve,
59
+ resolveAndRegister(url) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ var _a;
62
+ const doc = yield resolve(url);
63
+ // Building at `current` materializes and registers every runnable step;
64
+ // the schema itself is only kept long enough to lift it to a list.
65
+ const schema = versionedFromContract(doc, { codeSteps: options.codeSteps });
66
+ const listName = (_a = options.lists) === null || _a === void 0 ? void 0 : _a[doc.name];
67
+ if (listName !== undefined) {
68
+ registerSchema(versionedList(schema, listName));
69
+ }
70
+ return doc;
71
+ });
72
+ },
73
+ readResolving(schema, raw, contractUrl, readOptions) {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ const first = schema.read(raw, readOptions);
76
+ if (first.ok || first.reason !== 'ahead')
77
+ return first;
78
+ try {
79
+ yield this.resolveAndRegister(contractUrl);
80
+ }
81
+ catch (_a) {
82
+ // The refusal stands, with its original, accurate message — the cure
83
+ // failed, the diagnosis didn't.
84
+ return first;
85
+ }
86
+ return schema.read(raw, readOptions);
87
+ });
88
+ },
89
+ invalidate(url) {
90
+ if (url === undefined)
91
+ cache.clear();
92
+ else
93
+ cache.delete(url);
94
+ },
95
+ };
96
+ }
97
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../../../../libs/contract/src/lib/resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { MigrationContext, VersionedSchema } from '@braidlabs/skew';
2
+ import { SkewContractDocument } from './document.js';
3
+ /**
4
+ * A named implementation for a contract step declared as `code` — the escape
5
+ * hatch for semantic migrations the op set cannot express. Shipped inside the
6
+ * consuming bundle, referenced by name from the document.
7
+ */
8
+ export interface ContractCodeStep<TFrom = unknown, TTo = unknown> {
9
+ readonly up: (previous: TFrom, ctx: MigrationContext) => TTo;
10
+ readonly down?: (next: TTo, ctx: MigrationContext) => TFrom;
11
+ readonly derives?: readonly string[];
12
+ readonly downDerives?: readonly string[];
13
+ readonly lossy?: readonly string[];
14
+ }
15
+ export interface ContractSchemaOptions<TCurrent> {
16
+ /**
17
+ * Version this build is pinned to. Defaults to the document's `current`.
18
+ * A build pinned below `current` still learns the newer steps: they are
19
+ * contributed to the shared registry (unless `register` is false), which is
20
+ * what lets `read()` downgrade data from the future.
21
+ */
22
+ readonly at?: number;
23
+ /** Implementations for the document's named `code` steps. */
24
+ readonly codeSteps?: Readonly<Record<string, ContractCodeStep<any, any>>>;
25
+ /**
26
+ * Contribute every runnable step to the shared registry. Defaults to
27
+ * `true` — adopting a published contract *is* the explicit act of sharing
28
+ * that plain `versioned()` declarations require `registerSchema` for.
29
+ */
30
+ readonly register?: boolean;
31
+ readonly validate?: (value: unknown) => value is TCurrent;
32
+ readonly assumeLegacyVersion?: number;
33
+ }
34
+ /**
35
+ * Builds a {@link VersionedSchema} from a contract document — the drop-in,
36
+ * data-driven replacement for a hand-maintained `versioned().next()` chain.
37
+ *
38
+ * - `ops` steps compile to up *and* (where invertible) down migrations, with
39
+ * derived and lossy paths computed rather than hand-annotated.
40
+ * - `code` steps use the implementation supplied in `codeSteps`; a missing
41
+ * implementation leaves a hole that reads as `gap` — loud, not guessed.
42
+ * - Steps beyond the pinned version feed the shared registry, so this build
43
+ * can downgrade newer data it could never have shipped knowledge of.
44
+ */
45
+ export declare function versionedFromContract<TCurrent>(doc: SkewContractDocument, options?: ContractSchemaOptions<TCurrent>): VersionedSchema<TCurrent>;
@@ -0,0 +1,71 @@
1
+ import { compileLens, registerSteps, versionedFromSteps, } from '@braidlabs/skew';
2
+ /**
3
+ * Builds a {@link VersionedSchema} from a contract document — the drop-in,
4
+ * data-driven replacement for a hand-maintained `versioned().next()` chain.
5
+ *
6
+ * - `ops` steps compile to up *and* (where invertible) down migrations, with
7
+ * derived and lossy paths computed rather than hand-annotated.
8
+ * - `code` steps use the implementation supplied in `codeSteps`; a missing
9
+ * implementation leaves a hole that reads as `gap` — loud, not guessed.
10
+ * - Steps beyond the pinned version feed the shared registry, so this build
11
+ * can downgrade newer data it could never have shipped knowledge of.
12
+ */
13
+ export function versionedFromContract(doc, options = {}) {
14
+ var _a;
15
+ const at = (_a = options.at) !== null && _a !== void 0 ? _a : doc.current;
16
+ if (!Number.isInteger(at) || at < 1 || at > doc.current) {
17
+ throw new TypeError(`skew contract: cannot pin "${doc.name}" at v${at} — the document covers v1 through v${doc.current}`);
18
+ }
19
+ const localSteps = [];
20
+ const futureSteps = [];
21
+ for (const contractStep of doc.steps) {
22
+ const step = materialize(doc.name, contractStep, options.codeSteps);
23
+ if (contractStep.to <= at) {
24
+ localSteps[contractStep.to - 2] = step;
25
+ }
26
+ else if (step) {
27
+ futureSteps.push(step);
28
+ }
29
+ }
30
+ // Preserve chain length even when the last local step is a hole.
31
+ localSteps.length = at - 1;
32
+ const schema = versionedFromSteps(doc.name, localSteps, {
33
+ validate: options.validate,
34
+ assumeLegacyVersion: options.assumeLegacyVersion,
35
+ });
36
+ if (options.register !== false) {
37
+ registerSteps(doc.name, localSteps);
38
+ registerSteps(doc.name, futureSteps);
39
+ }
40
+ return schema;
41
+ }
42
+ /** Compiles one contract step, or returns `undefined` for an unrunnable one. */
43
+ function materialize(name, contractStep, codeSteps) {
44
+ var _a;
45
+ if (contractStep.ops) {
46
+ const lens = compileLens(contractStep.ops);
47
+ return {
48
+ to: contractStep.to,
49
+ description: contractStep.description,
50
+ up: lens.up,
51
+ down: (_a = lens.down) !== null && _a !== void 0 ? _a : undefined,
52
+ derives: lens.derivedUp,
53
+ downDerives: lens.derivedDown,
54
+ lossy: lens.lossyDown,
55
+ ops: contractStep.ops,
56
+ };
57
+ }
58
+ const implementation = contractStep.code ? codeSteps === null || codeSteps === void 0 ? void 0 : codeSteps[contractStep.code] : undefined;
59
+ if (!implementation)
60
+ return undefined;
61
+ return {
62
+ to: contractStep.to,
63
+ description: contractStep.description,
64
+ up: implementation.up,
65
+ down: implementation.down,
66
+ derives: implementation.derives,
67
+ downDerives: implementation.downDerives,
68
+ lossy: implementation.lossy,
69
+ };
70
+ }
71
+ //# sourceMappingURL=schema-from-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-from-contract.js","sourceRoot":"","sources":["../../../../../libs/contract/src/lib/schema-from-contract.ts"],"names":[],"mappings":"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"}