@openlup/core 0.12.0 → 0.13.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/AGENTS.md +21 -1
- package/CHANGELOG.md +24 -0
- package/README.md +42 -8
- package/dist/outbox/index.d.ts +55 -0
- package/dist/outbox/index.js +1 -0
- package/dist/outbox/vocabulary.d.ts +7 -0
- package/dist/outbox/vocabulary.js +36 -0
- package/dist/platform-runtime/index.d.ts +1 -0
- package/dist/platform-runtime/ports.d.ts +24 -0
- package/dist/readiness/contracts.d.ts +121 -0
- package/dist/readiness/contracts.js +2 -0
- package/dist/readiness/declarations.d.ts +45 -0
- package/dist/readiness/declarations.js +58 -0
- package/dist/readiness/index.d.ts +5 -0
- package/dist/readiness/index.js +147 -0
- package/dist/readiness/schemaProbe.d.ts +13 -0
- package/dist/readiness/schemaProbe.js +25 -0
- package/dist/standard-schema/index.d.ts +26 -0
- package/dist/standard-schema/index.js +1 -0
- package/docs/SUBSCRIPTION_ENGINE.md +235 -0
- package/package.json +32 -1
- package/release-gates.json +81 -21
- package/src/outbox/index.ts +44 -0
- package/src/outbox/vocabulary.ts +38 -0
- package/src/platform-runtime/index.ts +2 -0
- package/src/platform-runtime/ports.ts +43 -0
- package/src/readiness/contracts.ts +57 -0
- package/src/readiness/declarations.ts +38 -0
- package/src/readiness/index.ts +97 -0
- package/src/readiness/schemaProbe.ts +24 -0
- package/src/standard-schema/index.ts +17 -0
package/release-gates.json
CHANGED
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"role": "kernel",
|
|
35
35
|
"maturity": "candidate",
|
|
36
36
|
"compatibilityPolicy": "internal-candidate",
|
|
37
|
-
"packageSmokeEvidence": [
|
|
37
|
+
"packageSmokeEvidence": [
|
|
38
|
+
"smoke/subscriptionBundleStandalone.test.ts"
|
|
39
|
+
],
|
|
38
40
|
"entrypoint": "dist/subscription/index.d.ts",
|
|
39
41
|
"snapshot": "api/subscription.api.md"
|
|
40
42
|
},
|
|
@@ -42,7 +44,9 @@
|
|
|
42
44
|
"role": "kernel",
|
|
43
45
|
"maturity": "candidate",
|
|
44
46
|
"compatibilityPolicy": "internal-candidate",
|
|
45
|
-
"packageSmokeEvidence": [
|
|
47
|
+
"packageSmokeEvidence": [
|
|
48
|
+
"smoke/subscriptionBundleStandalone.test.ts"
|
|
49
|
+
],
|
|
46
50
|
"entrypoint": "dist/bundle/contracts.d.ts",
|
|
47
51
|
"snapshot": "api/bundle.api.md"
|
|
48
52
|
},
|
|
@@ -50,7 +54,9 @@
|
|
|
50
54
|
"role": "kernel",
|
|
51
55
|
"maturity": "candidate",
|
|
52
56
|
"compatibilityPolicy": "internal-candidate",
|
|
53
|
-
"packageSmokeEvidence": [
|
|
57
|
+
"packageSmokeEvidence": [
|
|
58
|
+
"smoke/pricingStandalone.test.ts"
|
|
59
|
+
],
|
|
54
60
|
"entrypoint": "dist/pricing/index.d.ts",
|
|
55
61
|
"snapshot": "api/pricing.api.md"
|
|
56
62
|
},
|
|
@@ -58,7 +64,9 @@
|
|
|
58
64
|
"role": "kernel",
|
|
59
65
|
"maturity": "experimental",
|
|
60
66
|
"compatibilityPolicy": "internal-candidate",
|
|
61
|
-
"packageSmokeEvidence": [
|
|
67
|
+
"packageSmokeEvidence": [
|
|
68
|
+
"smoke/companyIdentityStandalone.test.ts"
|
|
69
|
+
],
|
|
62
70
|
"entrypoint": "dist/company-identity/index.d.ts",
|
|
63
71
|
"snapshot": "api/company-identity.api.md"
|
|
64
72
|
},
|
|
@@ -66,7 +74,9 @@
|
|
|
66
74
|
"role": "kernel",
|
|
67
75
|
"maturity": "experimental",
|
|
68
76
|
"compatibilityPolicy": "internal-candidate",
|
|
69
|
-
"packageSmokeEvidence": [
|
|
77
|
+
"packageSmokeEvidence": [
|
|
78
|
+
"smoke/fulfillmentStandalone.test.ts"
|
|
79
|
+
],
|
|
70
80
|
"entrypoint": "dist/fulfillment/index.d.ts",
|
|
71
81
|
"snapshot": "api/fulfillment.api.md"
|
|
72
82
|
},
|
|
@@ -74,7 +84,9 @@
|
|
|
74
84
|
"role": "kernel",
|
|
75
85
|
"maturity": "candidate",
|
|
76
86
|
"compatibilityPolicy": "internal-candidate",
|
|
77
|
-
"packageSmokeEvidence": [
|
|
87
|
+
"packageSmokeEvidence": [
|
|
88
|
+
"smoke/inventoryStandalone.test.ts"
|
|
89
|
+
],
|
|
78
90
|
"entrypoint": "dist/inventory/index.d.ts",
|
|
79
91
|
"snapshot": "api/inventory.api.md"
|
|
80
92
|
},
|
|
@@ -82,7 +94,9 @@
|
|
|
82
94
|
"role": "kernel",
|
|
83
95
|
"maturity": "candidate",
|
|
84
96
|
"compatibilityPolicy": "internal-candidate",
|
|
85
|
-
"packageSmokeEvidence": [
|
|
97
|
+
"packageSmokeEvidence": [
|
|
98
|
+
"smoke/shippingStandalone.test.ts"
|
|
99
|
+
],
|
|
86
100
|
"entrypoint": "dist/shipping/index.d.ts",
|
|
87
101
|
"snapshot": "api/shipping.api.md"
|
|
88
102
|
},
|
|
@@ -90,7 +104,9 @@
|
|
|
90
104
|
"role": "kernel",
|
|
91
105
|
"maturity": "candidate",
|
|
92
106
|
"compatibilityPolicy": "internal-candidate",
|
|
93
|
-
"packageSmokeEvidence": [
|
|
107
|
+
"packageSmokeEvidence": [
|
|
108
|
+
"smoke/catalogStandalone.test.ts"
|
|
109
|
+
],
|
|
94
110
|
"entrypoint": "dist/catalog/index.d.ts",
|
|
95
111
|
"snapshot": "api/catalog.api.md"
|
|
96
112
|
},
|
|
@@ -98,7 +114,9 @@
|
|
|
98
114
|
"role": "kernel",
|
|
99
115
|
"maturity": "candidate",
|
|
100
116
|
"compatibilityPolicy": "internal-candidate",
|
|
101
|
-
"packageSmokeEvidence": [
|
|
117
|
+
"packageSmokeEvidence": [
|
|
118
|
+
"smoke/promoStandalone.test.ts"
|
|
119
|
+
],
|
|
102
120
|
"entrypoint": "dist/promo/index.d.ts",
|
|
103
121
|
"snapshot": "api/promo.api.md"
|
|
104
122
|
},
|
|
@@ -106,7 +124,9 @@
|
|
|
106
124
|
"role": "kernel",
|
|
107
125
|
"maturity": "candidate",
|
|
108
126
|
"compatibilityPolicy": "internal-candidate",
|
|
109
|
-
"packageSmokeEvidence": [
|
|
127
|
+
"packageSmokeEvidence": [
|
|
128
|
+
"smoke/riskStandalone.test.ts"
|
|
129
|
+
],
|
|
110
130
|
"entrypoint": "dist/risk/index.d.ts",
|
|
111
131
|
"snapshot": "api/risk.api.md"
|
|
112
132
|
},
|
|
@@ -114,7 +134,9 @@
|
|
|
114
134
|
"role": "kernel",
|
|
115
135
|
"maturity": "experimental",
|
|
116
136
|
"compatibilityPolicy": "internal-candidate",
|
|
117
|
-
"packageSmokeEvidence": [
|
|
137
|
+
"packageSmokeEvidence": [
|
|
138
|
+
"smoke/marketingResearchStandalone.test.ts"
|
|
139
|
+
],
|
|
118
140
|
"entrypoint": "dist/marketing/research/index.d.ts",
|
|
119
141
|
"snapshot": "api/marketing-research.api.md"
|
|
120
142
|
},
|
|
@@ -122,7 +144,9 @@
|
|
|
122
144
|
"role": "kernel",
|
|
123
145
|
"maturity": "experimental",
|
|
124
146
|
"compatibilityPolicy": "internal-candidate",
|
|
125
|
-
"packageSmokeEvidence": [
|
|
147
|
+
"packageSmokeEvidence": [
|
|
148
|
+
"smoke/partnersStandalone.test.ts"
|
|
149
|
+
],
|
|
126
150
|
"entrypoint": "dist/partners/index.d.ts",
|
|
127
151
|
"snapshot": "api/partners.api.md"
|
|
128
152
|
},
|
|
@@ -130,7 +154,9 @@
|
|
|
130
154
|
"role": "kernel",
|
|
131
155
|
"maturity": "experimental",
|
|
132
156
|
"compatibilityPolicy": "internal-candidate",
|
|
133
|
-
"packageSmokeEvidence": [
|
|
157
|
+
"packageSmokeEvidence": [
|
|
158
|
+
"smoke/paymentStandalone.test.ts"
|
|
159
|
+
],
|
|
134
160
|
"entrypoint": "dist/payment/index.d.ts",
|
|
135
161
|
"snapshot": "api/payment.api.md"
|
|
136
162
|
},
|
|
@@ -138,15 +164,49 @@
|
|
|
138
164
|
"role": "kernel",
|
|
139
165
|
"maturity": "experimental",
|
|
140
166
|
"compatibilityPolicy": "internal-candidate",
|
|
141
|
-
"packageSmokeEvidence": [
|
|
167
|
+
"packageSmokeEvidence": [
|
|
168
|
+
"smoke/platformRuntimeStandalone.test.ts"
|
|
169
|
+
],
|
|
142
170
|
"entrypoint": "dist/platform-runtime/index.d.ts",
|
|
143
171
|
"snapshot": "api/platform-runtime.api.md"
|
|
144
172
|
},
|
|
173
|
+
"./outbox": {
|
|
174
|
+
"role": "kernel",
|
|
175
|
+
"maturity": "experimental",
|
|
176
|
+
"compatibilityPolicy": "internal-candidate",
|
|
177
|
+
"packageSmokeEvidence": [
|
|
178
|
+
"smoke/outboxStandalone.test.ts"
|
|
179
|
+
],
|
|
180
|
+
"entrypoint": "dist/outbox/index.d.ts",
|
|
181
|
+
"snapshot": "api/outbox.api.md"
|
|
182
|
+
},
|
|
183
|
+
"./readiness": {
|
|
184
|
+
"role": "kernel",
|
|
185
|
+
"maturity": "experimental",
|
|
186
|
+
"compatibilityPolicy": "internal-candidate",
|
|
187
|
+
"packageSmokeEvidence": [
|
|
188
|
+
"smoke/readinessStandalone.test.ts"
|
|
189
|
+
],
|
|
190
|
+
"entrypoint": "dist/readiness/index.d.ts",
|
|
191
|
+
"snapshot": "api/readiness.api.md"
|
|
192
|
+
},
|
|
193
|
+
"./standard-schema": {
|
|
194
|
+
"role": "kernel",
|
|
195
|
+
"maturity": "experimental",
|
|
196
|
+
"compatibilityPolicy": "internal-candidate",
|
|
197
|
+
"packageSmokeEvidence": [
|
|
198
|
+
"smoke/standardSchemaStandalone.test.ts"
|
|
199
|
+
],
|
|
200
|
+
"entrypoint": "dist/standard-schema/index.d.ts",
|
|
201
|
+
"snapshot": "api/standard-schema.api.md"
|
|
202
|
+
},
|
|
145
203
|
"./testing": {
|
|
146
204
|
"role": "testing",
|
|
147
205
|
"maturity": "testing",
|
|
148
206
|
"compatibilityPolicy": "testing-internal",
|
|
149
|
-
"packageSmokeEvidence": [
|
|
207
|
+
"packageSmokeEvidence": [
|
|
208
|
+
"smoke/testingStandalone.test.ts"
|
|
209
|
+
],
|
|
150
210
|
"entrypoint": "dist/testing.d.ts",
|
|
151
211
|
"snapshot": "api/testing.api.md"
|
|
152
212
|
}
|
|
@@ -202,18 +262,18 @@
|
|
|
202
262
|
}
|
|
203
263
|
},
|
|
204
264
|
"pack": {
|
|
205
|
-
"$budgetReason": "2026-10-
|
|
265
|
+
"$budgetReason": "2026-10-05, owner-approved shared-contract growth: seven new source files add 21 source/runtime/declaration artifacts. The measured contribution has 198 code files, 15 above the prior cap, and 127202 code gzip bytes. The code budget is 131072 bytes/198 files; documentation stays at 24576 bytes/12 files. The total budget is 155648 packed bytes/655360 unpacked bytes/211 files, including the shipped documentation outside the root-file class. These bounded increases cover the outbox envelope, candidate-readiness and Standard Schema contracts; later growth still requires a measured budget decision.",
|
|
206
266
|
"code": {
|
|
207
|
-
"maxPackedBytes":
|
|
208
|
-
"maxFiles":
|
|
267
|
+
"maxPackedBytes": 131072,
|
|
268
|
+
"maxFiles": 198
|
|
209
269
|
},
|
|
210
270
|
"docs": {
|
|
211
271
|
"maxPackedBytes": 24576,
|
|
212
272
|
"maxFiles": 12
|
|
213
273
|
},
|
|
214
|
-
"maxPackedBytes":
|
|
215
|
-
"maxUnpackedBytes":
|
|
216
|
-
"maxFiles":
|
|
274
|
+
"maxPackedBytes": 155648,
|
|
275
|
+
"maxUnpackedBytes": 655360,
|
|
276
|
+
"maxFiles": 211,
|
|
217
277
|
"requiredFiles": [
|
|
218
278
|
"AGENTS.md",
|
|
219
279
|
"CHANGELOG.md",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** @beta Shared event envelope. Storage and execution policy belong to the rail. */
|
|
2
|
+
export interface OutboxEventRow {
|
|
3
|
+
id: string;
|
|
4
|
+
created_at: string;
|
|
5
|
+
available_at: string;
|
|
6
|
+
processed_at: string | null;
|
|
7
|
+
aggregate_type: string;
|
|
8
|
+
aggregate_id: string;
|
|
9
|
+
event_type: string;
|
|
10
|
+
idempotency_key: string;
|
|
11
|
+
status: string;
|
|
12
|
+
attempts: number;
|
|
13
|
+
payload: Record<string, unknown>;
|
|
14
|
+
error: string | null;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** @beta */
|
|
18
|
+
export type OutboxHandlerOutcome =
|
|
19
|
+
| { kind: "processed"; detail?: Record<string, unknown> }
|
|
20
|
+
| { kind: "retry"; reason: string }
|
|
21
|
+
| { kind: "discard"; reason: string; benign?: boolean }
|
|
22
|
+
| { kind: "snooze"; reason: string };
|
|
23
|
+
/** @beta Phase observations are transient; never put payloads or credentials here. */
|
|
24
|
+
export interface OutboxHandlerExecutionContext {
|
|
25
|
+
setPhase(phase: string): void;
|
|
26
|
+
}
|
|
27
|
+
/** @beta Inert registration shared by preflight and the run-bound builder. */
|
|
28
|
+
export interface OutboxHandlerDescriptor {
|
|
29
|
+
readonly eventType: string;
|
|
30
|
+
readonly timeoutMs: number;
|
|
31
|
+
}
|
|
32
|
+
/** @beta Structural abort signal, shared by browser and server implementations. */
|
|
33
|
+
export interface OutboxAbortSignal {
|
|
34
|
+
readonly aborted: boolean;
|
|
35
|
+
readonly reason: unknown;
|
|
36
|
+
addEventListener(type: "abort", listener: () => void, options?: { once?: boolean }): void;
|
|
37
|
+
removeEventListener(type: "abort", listener: () => void): void;
|
|
38
|
+
}
|
|
39
|
+
/** @beta Effects must be idempotent and honor the abort signal. */
|
|
40
|
+
export interface OutboxHandler<Signal extends OutboxAbortSignal = OutboxAbortSignal> extends OutboxHandlerDescriptor {
|
|
41
|
+
handle(row: OutboxEventRow, signal: Signal, execution?: OutboxHandlerExecutionContext): Promise<OutboxHandlerOutcome>;
|
|
42
|
+
}
|
|
43
|
+
export { PLATFORM_EVENT_VOCABULARY } from "./vocabulary.js";
|
|
44
|
+
export type { EventVocabularyEntry } from "./vocabulary.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @beta Vocabulary ownership is separate from consumer registration. */
|
|
2
|
+
export interface EventVocabularyEntry { eventType: string; owner: string }
|
|
3
|
+
/** @beta Applications supply their complete ordered persisted vocabulary separately. */
|
|
4
|
+
export const PLATFORM_EVENT_VOCABULARY: readonly EventVocabularyEntry[] = [
|
|
5
|
+
{ eventType: "commerce.order_draft.created", owner: "@openlup/core" },
|
|
6
|
+
{ eventType: "commerce.order.paid", owner: "@openlup/core" },
|
|
7
|
+
{ eventType: "commerce.order.paid.email", owner: "@openlup/core" },
|
|
8
|
+
{ eventType: "commerce.payment.failed", owner: "@openlup/core" },
|
|
9
|
+
{ eventType: "commerce.checkout.expired", owner: "@openlup/core" },
|
|
10
|
+
{ eventType: "commerce.order.canceled", owner: "@openlup/core" },
|
|
11
|
+
{ eventType: "commerce.order.refunded", owner: "@openlup/core" },
|
|
12
|
+
{ eventType: "commerce.shipment.dispatched", owner: "@openlup/core" },
|
|
13
|
+
{ eventType: "commerce.shipment.delivered", owner: "@openlup/core" },
|
|
14
|
+
{ eventType: "commerce.shipment.exception", owner: "@openlup/core" },
|
|
15
|
+
{ eventType: "commerce.return.approved", owner: "@openlup/core" },
|
|
16
|
+
{ eventType: "commerce.return.rejected", owner: "@openlup/core" },
|
|
17
|
+
{ eventType: "commerce.order.review_request", owner: "@openlup/core" },
|
|
18
|
+
{ eventType: "commerce.order.review_effects", owner: "@openlup/core" },
|
|
19
|
+
{ eventType: "commerce.product.back_in_stock", owner: "@openlup/core" },
|
|
20
|
+
{ eventType: "commerce.order_draft.abandoned.1h", owner: "@openlup/core" },
|
|
21
|
+
{ eventType: "commerce.order_draft.abandoned.24h", owner: "@openlup/core" },
|
|
22
|
+
{ eventType: "commerce.order_draft.abandoned.72h", owner: "@openlup/core" },
|
|
23
|
+
{ eventType: "commerce.order.reorder_reminder", owner: "@openlup/core" },
|
|
24
|
+
{ eventType: "commerce.checkout_recovery", owner: "@openlup/core" },
|
|
25
|
+
{ eventType: "subscription.cancelled", owner: "@openlup/core" },
|
|
26
|
+
{ eventType: "subscription.created", owner: "@openlup/core" },
|
|
27
|
+
{ eventType: "subscription.activation_action_required", owner: "@openlup/core" },
|
|
28
|
+
{ eventType: "subscription.pause_reminder_due", owner: "@openlup/core" },
|
|
29
|
+
{ eventType: "subscription.paused", owner: "@openlup/core" },
|
|
30
|
+
{ eventType: "subscription.resumed", owner: "@openlup/core" },
|
|
31
|
+
{ eventType: "subscription.delivery_rescheduled", owner: "@openlup/core" },
|
|
32
|
+
{ eventType: "subscription.cycle_skipped", owner: "@openlup/core" },
|
|
33
|
+
{ eventType: "subscription.address_changed", owner: "@openlup/core" },
|
|
34
|
+
{ eventType: "subscription.package_changed", owner: "@openlup/core" },
|
|
35
|
+
{ eventType: "subscription.renewal_upcoming", owner: "@openlup/core" },
|
|
36
|
+
{ eventType: "accounting.order.reversal_requested", owner: "@openlup/core" },
|
|
37
|
+
{ eventType: "commerce.fulfillment.handed_over", owner: "@openlup/core" },
|
|
38
|
+
];
|
|
@@ -86,3 +86,46 @@ export interface AnalyticsPort {
|
|
|
86
86
|
export interface TransactionalRuntimePort {
|
|
87
87
|
invoke(functionName: string, payload: unknown): Promise<{ ok: boolean; status: number }>;
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/** @beta Invocation evidence and claim/finish contract shared by infrastructure rails. */
|
|
91
|
+
export type PlatformJobTriggerKind = "worker" | "scheduler" | "operator";
|
|
92
|
+
|
|
93
|
+
export type PlatformJobInvocation = {
|
|
94
|
+
triggerKind: PlatformJobTriggerKind;
|
|
95
|
+
/** Observational adapter evidence; it never grants execution authority. */
|
|
96
|
+
invocationSource: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type PlatformJobClaim = {
|
|
100
|
+
acquired: boolean;
|
|
101
|
+
runId: string | null;
|
|
102
|
+
reason: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type PlatformJobFinishStatus = "success" | "failed";
|
|
106
|
+
|
|
107
|
+
export type PlatformJobFinishSummary = {
|
|
108
|
+
checked: number;
|
|
109
|
+
updated: number;
|
|
110
|
+
failures: number;
|
|
111
|
+
skipped: boolean;
|
|
112
|
+
reason?: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export interface JobLeasePort {
|
|
116
|
+
claimJobRun(
|
|
117
|
+
jobName: string,
|
|
118
|
+
invocation: PlatformJobInvocation,
|
|
119
|
+
leaseSeconds?: number,
|
|
120
|
+
): Promise<PlatformJobClaim>;
|
|
121
|
+
|
|
122
|
+
finishJobRun(
|
|
123
|
+
jobName: string,
|
|
124
|
+
runId: string,
|
|
125
|
+
invocation: PlatformJobInvocation,
|
|
126
|
+
status: PlatformJobFinishStatus,
|
|
127
|
+
result: PlatformJobFinishSummary,
|
|
128
|
+
extraMetadata?: Record<string, unknown>,
|
|
129
|
+
): Promise<boolean>;
|
|
130
|
+
|
|
131
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { OutboxHandlerDescriptor } from "../outbox/index.js";
|
|
2
|
+
import type { EventVocabularyEntry } from "../outbox/vocabulary.js";
|
|
3
|
+
/** @beta */
|
|
4
|
+
export const READINESS_CODES = ["OPENLUP_E_PORT_MISSING", "OPENLUP_E_EVENT_UNHANDLED", "OPENLUP_E_EVENT_DUPLICATE", "OPENLUP_E_SCHEDULE_UNBOUND", "OPENLUP_E_SCHEMA_BEHIND", "OPENLUP_E_SET_MISMATCH", "OPENLUP_E_ENV_MISSING"] as const;
|
|
5
|
+
/** @beta */
|
|
6
|
+
export type ReadinessCode = typeof READINESS_CODES[number];
|
|
7
|
+
/** @beta */
|
|
8
|
+
export interface ReadinessIssue { code: ReadinessCode; package: string; subject: string; fix: string; observation?: string }
|
|
9
|
+
/** @beta */
|
|
10
|
+
export interface SchemaObject { kind: "table" | "column" | "function"; name: string; signature?: string }
|
|
11
|
+
/** @beta Version is an adapter-specific migration identity, not inferred from object existence. */
|
|
12
|
+
export interface RequiredSchema { version: string; migration: string; objects: readonly SchemaObject[] }
|
|
13
|
+
/** @beta */
|
|
14
|
+
export interface PackageManifest {
|
|
15
|
+
name: string; version: string; kind: "kernel" | "rail" | "capability" | "implementation";
|
|
16
|
+
emits: readonly string[]; handles: readonly string[];
|
|
17
|
+
schedules: readonly { id: string; cadenceSeconds: number }[];
|
|
18
|
+
routes: readonly string[]; requiredPorts: readonly string[]; env: readonly string[];
|
|
19
|
+
requiredSchema?: RequiredSchema;
|
|
20
|
+
}
|
|
21
|
+
/** @beta Run-bound handlers are represented by their shared inert descriptors. */
|
|
22
|
+
export interface Contribution {
|
|
23
|
+
manifest: PackageManifest;
|
|
24
|
+
handlers: readonly OutboxHandlerDescriptor[];
|
|
25
|
+
schedules: readonly { id: string; cadenceSeconds: number; run: (...args: never[]) => unknown }[];
|
|
26
|
+
routes: readonly { id: string; handle: (...args: never[]) => unknown }[];
|
|
27
|
+
}
|
|
28
|
+
/** @beta Host-observed inventory, including core. The loader owns completeness. */
|
|
29
|
+
export interface LoadedPackage { name: string; version: string; kind: PackageManifest["kind"] }
|
|
30
|
+
/** @beta Candidate identity must change when configuration, schema or artifact changes. */
|
|
31
|
+
export interface CandidateIdentity { artifact: string; configuration: string; environment: string; database: string; schema: string }
|
|
32
|
+
/** @beta */
|
|
33
|
+
export interface EventExemption { pattern: string; prefix: boolean; owner: string; reason: string; state: "ignored" | "dormant" }
|
|
34
|
+
/** @beta */
|
|
35
|
+
export interface ReadinessInput {
|
|
36
|
+
candidate: CandidateIdentity; inventoryComplete: boolean;
|
|
37
|
+
loadedPackages: readonly LoadedPackage[]; contributions: readonly Contribution[];
|
|
38
|
+
vocabulary: readonly EventVocabularyEntry[]; exemptions: readonly EventExemption[];
|
|
39
|
+
bindings: { ports: Readonly<Record<string, unknown>>; triggers: Readonly<Record<string, unknown>>; routes: Readonly<Record<string, unknown>>; env: Readonly<Record<string, string | undefined>> };
|
|
40
|
+
schemaProbe: SchemaProbePort; probeTimeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
/** @beta A bounded catalog observation, not proof of SQL bodies, ACL or recovery. */
|
|
43
|
+
export interface SchemaObservation {
|
|
44
|
+
database: string; version: string | null;
|
|
45
|
+
objects: readonly { object: SchemaObject; present: boolean | null }[];
|
|
46
|
+
}
|
|
47
|
+
/** @beta */
|
|
48
|
+
export interface SchemaProbePort { observe(requirement: RequiredSchema): Promise<SchemaObservation> }
|
|
49
|
+
/** @beta Each operation commits before resolving; never lend a producer transaction to dispatch. */
|
|
50
|
+
export interface SqlExecutor {
|
|
51
|
+
query<Row extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: readonly unknown[]): Promise<{ rows: Row[] }>;
|
|
52
|
+
}
|
|
53
|
+
/** @beta */
|
|
54
|
+
export interface ReadinessReport {
|
|
55
|
+
state: "ready" | "unsatisfied" | "unknown";
|
|
56
|
+
candidate: CandidateIdentity; loadedPackages: readonly LoadedPackage[]; issues: readonly ReadinessIssue[];
|
|
57
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ReadinessInput, ReadinessIssue, Contribution } from "./contracts.js";
|
|
3
|
+
const name = z.string().min(1).max(240).regex(/^[a-zA-Z0-9@_./:-]+$/);
|
|
4
|
+
const version = z.string().regex(/^0\.\d+\.\d+$/);
|
|
5
|
+
const list = z.array(name).refine((items) => new Set(items).size === items.length);
|
|
6
|
+
const kind = z.enum(["kernel", "rail", "capability", "implementation"]);
|
|
7
|
+
export const schemaObjectShape = z.object({ kind: z.enum(["table", "column", "function"]), name,
|
|
8
|
+
signature: z.string().max(1000).optional() }).strict().refine((o) =>
|
|
9
|
+
o.name.split(".").length === (o.kind === "column" ? 3 : 2) && (o.kind !== "function" || o.signature !== undefined));
|
|
10
|
+
export const manifestShape = z.object({ name: name.regex(/^@openlup\/[a-z0-9-]+$/), version, kind,
|
|
11
|
+
emits: list, handles: list, schedules: z.array(z.object({ id: name, cadenceSeconds: z.number().finite().positive() }).strict()),
|
|
12
|
+
routes: list, requiredPorts: list, env: list,
|
|
13
|
+
requiredSchema: z.object({ version: name, migration: name, objects: z.array(schemaObjectShape) }).strict().optional(),
|
|
14
|
+
}).strict();
|
|
15
|
+
/** Validate declarations without invoking a handler, schedule, route or probe. */
|
|
16
|
+
export function declarationIssues(input: ReadinessInput): ReadinessIssue[] {
|
|
17
|
+
const issues: ReadinessIssue[] = [];
|
|
18
|
+
const issue = (subject: string, fix: string, code: ReadinessIssue["code"] = "OPENLUP_E_SET_MISMATCH") => issues.push({ code, package: "@openlup/core", subject, fix });
|
|
19
|
+
if (!input || typeof input !== "object") { issue("input", "Supply complete candidate observations."); return issues; }
|
|
20
|
+
if (!input.candidate || ![input.candidate.artifact, input.candidate.configuration, input.candidate.environment, input.candidate.database, input.candidate.schema].every((v) => typeof v === "string" && /^[a-zA-Z0-9_.:/-]{1,240}$/.test(v))) issue("candidate", "Supply sanitized immutable artifact, configuration, environment and database identities.");
|
|
21
|
+
if (input.inventoryComplete !== true) issue("inventory", "Observe the complete installed package inventory, including core.");
|
|
22
|
+
if (!Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions)) { issue("inventory", "Supply loaded packages and contributions as arrays."); return issues; }
|
|
23
|
+
for (const pkg of input.loadedPackages) if (!z.object({ name, version, kind }).strict().safeParse(pkg).success) issue("loaded-package", "Supply a valid observed package name, version and kind.");
|
|
24
|
+
for (const c of input.contributions as readonly Contribution[]) {
|
|
25
|
+
if (!manifestShape.safeParse(c?.manifest).success) { issue("manifest", "Supply a valid complete manifest with unique declarations."); continue; }
|
|
26
|
+
if (!Array.isArray(c.handlers) || c.handlers.some((h) => !name.safeParse(h?.eventType).success || !Number.isFinite(h?.timeoutMs) || h?.timeoutMs <= 0)) issue(c.manifest.name, "Supply valid shared handler descriptors.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
27
|
+
if (!Array.isArray(c.schedules) || c.schedules.some((s) => !name.safeParse(s?.id).success || !Number.isFinite(s?.cadenceSeconds) || s?.cadenceSeconds <= 0 || typeof s?.run !== "function")) issue(c.manifest.name, "Supply actual runnable schedules.", "OPENLUP_E_SCHEDULE_UNBOUND");
|
|
28
|
+
if (!Array.isArray(c.routes) || c.routes.some((r) => !name.safeParse(r?.id).success || typeof r?.handle !== "function")) issue(c.manifest.name, "Supply actual route registrations.", "OPENLUP_E_PORT_MISSING");
|
|
29
|
+
const objects = c.manifest.requiredSchema?.objects ?? [];
|
|
30
|
+
if (new Set(objects.map((o) => `${o.kind}:${o.name}:${o.signature ?? ""}`)).size !== objects.length) issue(c.manifest.name, "Remove duplicate schema requirements.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(input.vocabulary) || input.vocabulary.some((v) => !name.safeParse(v?.eventType).success || !name.safeParse(v?.owner).success)) issue("vocabulary", "Supply explicit vocabulary entries with owners.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
33
|
+
if (!Array.isArray(input.exemptions) || input.exemptions.some((e) => !name.safeParse(e?.pattern).success || typeof e?.prefix !== "boolean" || !name.safeParse(e?.owner).success || typeof e?.reason !== "string" || !e?.reason.trim() || !["ignored", "dormant"].includes(e?.state))) issue("exemptions", "Supply explicit owned exemptions with reasons.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
34
|
+
if (!input.bindings || [input.bindings.ports, input.bindings.routes, input.bindings.triggers, input.bindings.env].some((v) => !v || typeof v !== "object" || Array.isArray(v))) issue("bindings", "Supply actual host bindings.", "OPENLUP_E_PORT_MISSING");
|
|
35
|
+
if (!input.schemaProbe || typeof input.schemaProbe.observe !== "function") issue("schemaProbe", "Bind a schema observation port.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
36
|
+
if (input.probeTimeoutMs !== undefined && (!Number.isFinite(input.probeTimeoutMs) || input.probeTimeoutMs < 1 || input.probeTimeoutMs > 30000)) issue("probeTimeoutMs", "Use a bounded probe timeout from 1 to 30000 ms.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { declarationIssues, manifestShape, schemaObjectShape } from "./declarations.js";
|
|
2
|
+
import { schemaObjectKey } from "./schemaProbe.js";
|
|
3
|
+
import type { ReadinessInput, ReadinessIssue, ReadinessReport, RequiredSchema, SchemaObservation, Contribution } from "./contracts.js";
|
|
4
|
+
export * from "./contracts.js";
|
|
5
|
+
export { schemaObjectKey, buildSchemaObjectProbe, createSchemaProbe } from "./schemaProbe.js";
|
|
6
|
+
/** @beta Evaluates supplied facts; never performs migration or effect work. */
|
|
7
|
+
export async function checkReadiness(input: ReadinessInput): Promise<ReadinessReport> {
|
|
8
|
+
const issues = declarationIssues(input);
|
|
9
|
+
let unknown = issues.some((i) => ["input", "candidate", "inventory", "loaded-package", "schemaProbe", "probeTimeoutMs"].includes(i.subject));
|
|
10
|
+
const add = (code: ReadinessIssue["code"], pkg: string, subject: string, fix: string, observation?: string) => {
|
|
11
|
+
issues.push({ code, package: pkg, subject, fix, ...(observation ? { observation } : {}) });
|
|
12
|
+
};
|
|
13
|
+
const report = (): ReadinessReport => ({ state: unknown ? "unknown" : issues.length ? "unsatisfied" : "ready", candidate: input?.candidate,
|
|
14
|
+
loadedPackages: input?.loadedPackages ?? [], issues: issues.sort((a,b) => [a.code,a.package,a.subject,a.observation ?? ""].join("|").localeCompare([b.code,b.package,b.subject,b.observation ?? ""].join("|"))) });
|
|
15
|
+
if (!input || !Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions)) return report();
|
|
16
|
+
const contributions: readonly Contribution[] = input.contributions.filter((c: Contribution) => manifestShape.safeParse(c?.manifest).success && Array.isArray(c.handlers) && c.handlers.every((h) => h && typeof h.eventType === "string" && Number.isFinite(h.timeoutMs) && h.timeoutMs > 0) && Array.isArray(c.schedules) && c.schedules.every((s) => s && typeof s.id === "string" && typeof s.run === "function") && Array.isArray(c.routes) && c.routes.every((r) => r && typeof r.id === "string" && typeof r.handle === "function"));
|
|
17
|
+
const loaded = input.loadedPackages.filter((p) => p && typeof p.name === "string" && typeof p.version === "string");
|
|
18
|
+
const packages = new Map<string, number>();
|
|
19
|
+
for (const p of loaded) packages.set(p.name, (packages.get(p.name) ?? 0) + 1);
|
|
20
|
+
if (packages.get("@openlup/core") !== 1 || loaded.find((p) => p.name === "@openlup/core")?.kind !== "kernel") add("OPENLUP_E_SET_MISMATCH", "@openlup/core", "kernel", "Observe exactly one core kernel.");
|
|
21
|
+
const set = loaded.find((p) => p.name === "@openlup/core")?.version;
|
|
22
|
+
const install = `Align the complete set: npm install ${[...new Set(loaded.map((p) => p.name))].sort().map((name) => `${name}@${set ?? "<set-version>"}`).join(" ")}`;
|
|
23
|
+
for (const [name,count] of packages) if (count > 1) add("OPENLUP_E_SET_MISMATCH", name, "inventory", "Observe each loaded package exactly once.");
|
|
24
|
+
for (const p of loaded) {
|
|
25
|
+
if (p.version !== set || (p.kind === "kernel" && p.name !== "@openlup/core")) add("OPENLUP_E_SET_MISMATCH", p.name, "version", install);
|
|
26
|
+
if (p.kind !== "kernel" && contributions.filter((c) => c.manifest.name === p.name).length !== 1) add("OPENLUP_E_SET_MISMATCH", p.name, "contribution", "Compose exactly one contribution for each loaded non-kernel package.");
|
|
27
|
+
}
|
|
28
|
+
const handlers = new Map<string,string[]>(); const emitters = new Map<string,string[]>();
|
|
29
|
+
const events = new Set<string>();
|
|
30
|
+
for (const v of (Array.isArray(input.vocabulary) ? input.vocabulary : [])) {
|
|
31
|
+
if (v && typeof v.eventType === "string") {
|
|
32
|
+
if (events.has(v.eventType)) add("OPENLUP_E_EVENT_DUPLICATE", v.owner, v.eventType, "Declare each vocabulary identity once.");
|
|
33
|
+
events.add(v.eventType);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const c of contributions) {
|
|
37
|
+
const m = c.manifest;
|
|
38
|
+
const p = loaded.filter((p) => p.name === m.name);
|
|
39
|
+
if (p.length !== 1 || p[0]?.version !== m.version || p[0]?.kind !== m.kind) add("OPENLUP_E_SET_MISMATCH", m.name, "manifest", "Match the contribution to its observed installed package.");
|
|
40
|
+
for (const type of m.emits) {
|
|
41
|
+
events.add(type);
|
|
42
|
+
if (m.kind !== "kernel") emitters.set(type, [...(emitters.get(type) ?? []), m.name]);
|
|
43
|
+
}
|
|
44
|
+
const actual = c.handlers.map((h) => h?.eventType);
|
|
45
|
+
for (const type of new Set([...m.handles,...actual])) {
|
|
46
|
+
if (m.handles.filter((x) => x === type).length !== 1 || actual.filter((x) => x === type).length !== 1) add("OPENLUP_E_EVENT_DUPLICATE", m.name, type, "Match each declared consumer to exactly one composed handler descriptor.");
|
|
47
|
+
else handlers.set(type, [...(handlers.get(type) ?? []), m.name]);
|
|
48
|
+
events.add(type);
|
|
49
|
+
}
|
|
50
|
+
for (const port of m.requiredPorts) if (!isPort(input.bindings?.ports?.[`${m.name}:${port}`])) add("OPENLUP_E_PORT_MISSING", m.name, port, "Bind the selected adapter port in composition.");
|
|
51
|
+
for (const env of m.env) if (typeof input.bindings?.env?.[env] !== "string" || !input.bindings.env[env]?.trim()) add("OPENLUP_E_ENV_MISSING", m.name, env, "Supply the named environment variable without exposing its value.");
|
|
52
|
+
for (const id of new Set([...m.schedules.map((s) => s.id),...c.schedules.map((s) => s?.id)])) {
|
|
53
|
+
const declarations = m.schedules.filter((s) => s.id === id), actual = c.schedules.filter((s) => s?.id === id);
|
|
54
|
+
if (declarations.length !== 1 || actual.length !== 1 || declarations[0]?.cadenceSeconds !== actual[0]?.cadenceSeconds || typeof actual[0]?.run !== "function" || input.bindings?.triggers?.[`${m.name}:${id}`] !== actual[0]?.run) add("OPENLUP_E_SCHEDULE_UNBOUND", m.name, id, "Bind the actual schedule with its declared cadence to one host trigger.");
|
|
55
|
+
}
|
|
56
|
+
for (const id of new Set([...m.routes,...c.routes.map((r) => r?.id)])) if (m.routes.filter((r) => r === id).length !== 1 || c.routes.filter((r) => r?.id === id && typeof r.handle === "function").length !== 1 || input.bindings?.routes?.[`${m.name}:${id}`] !== c.routes.find((r) => r.id === id)?.handle) add("OPENLUP_E_PORT_MISSING", m.name, id, "Bind the declared route to its actual host entry.");
|
|
57
|
+
}
|
|
58
|
+
for (const [type,owners] of [...emitters,...handlers]) if (owners.length > 1) add("OPENLUP_E_EVENT_DUPLICATE", owners.sort().join(","), type, "Use one non-kernel producer owner and one composed consumer per event.");
|
|
59
|
+
for (const type of events) if (!handlers.has(type) && !(Array.isArray(input.exemptions) ? input.exemptions : []).some((e) => e && typeof e.reason === "string" && e.reason.trim() && typeof e.owner === "string" && typeof e.pattern === "string" && ["ignored", "dormant"].includes(e.state) && typeof e.prefix === "boolean" && (e.prefix ? type.startsWith(e.pattern) : type === e.pattern))) add("OPENLUP_E_EVENT_UNHANDLED", "@openlup/core", type, "Register a handler or an explicit owned ignored/dormant declaration.");
|
|
60
|
+
for (const c of contributions) {
|
|
61
|
+
const req = c.manifest.requiredSchema;
|
|
62
|
+
if (!req) continue;
|
|
63
|
+
if (issues.some((i) => ["schemaProbe", "probeTimeoutMs"].includes(i.subject))) { unknown = true; continue; }
|
|
64
|
+
let observation: unknown;
|
|
65
|
+
try { observation = await boundedObservation(input, req); }
|
|
66
|
+
catch { unknown = true; add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, `Observe the selected adapter and apply ${req.migration} through your migration chain.`, "probe_unavailable"); continue; }
|
|
67
|
+
if (!validObservation(observation, req)) { unknown = true; add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, "Return one valid observation for every requested schema object.", "probe_malformed_or_incomplete"); continue; }
|
|
68
|
+
if (!observation.database || observation.database !== input.candidate?.database || observation.version === null) { unknown = true; add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, "Observe the exact candidate database and applied migration identity.", "identity_or_version_unknown"); }
|
|
69
|
+
else if (observation.version !== req.version) add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, req.version, `Apply ${req.migration} and observe its version.`, "version_mismatch");
|
|
70
|
+
for (const {object,present} of observation.objects) if (present !== true) {
|
|
71
|
+
unknown ||= present === null;
|
|
72
|
+
add("OPENLUP_E_SCHEMA_BEHIND", c.manifest.name, schemaObjectKey(object), `Apply ${req.migration} for the selected adapter.`, present === null ? "object_unknown" : "object_absent");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return report();
|
|
76
|
+
}
|
|
77
|
+
function validObservation(value: unknown, requirement: RequiredSchema): value is SchemaObservation {
|
|
78
|
+
if (!value || typeof value !== "object") return false;
|
|
79
|
+
const v = value as SchemaObservation;
|
|
80
|
+
if (typeof v.database !== "string" || !(v.version === null || typeof v.version === "string") || !Array.isArray(v.objects)) return false;
|
|
81
|
+
const requested = new Set(requirement.objects.map(schemaObjectKey)), seen = new Set<string>();
|
|
82
|
+
for (const item of v.objects) {
|
|
83
|
+
if (!item || !schemaObjectShape.safeParse(item.object).success || ![true,false,null].includes(item.present)) return false;
|
|
84
|
+
const key = schemaObjectKey(item.object);
|
|
85
|
+
if (!requested.has(key) || seen.has(key)) return false;
|
|
86
|
+
seen.add(key);
|
|
87
|
+
}
|
|
88
|
+
return seen.size === requested.size;
|
|
89
|
+
}
|
|
90
|
+
async function boundedObservation(input: ReadinessInput, requirement: RequiredSchema): Promise<unknown> {
|
|
91
|
+
const timers = globalThis as unknown as { setTimeout(callback: () => void, ms: number): unknown; clearTimeout(handle: unknown): void };
|
|
92
|
+
let timer: unknown;
|
|
93
|
+
try { return await Promise.race([input.schemaProbe.observe(requirement), new Promise((_,reject) => { timer = timers.setTimeout(() => reject(new Error("probe_timeout")), input.probeTimeoutMs ?? 3000); })]); }
|
|
94
|
+
finally { if (timer !== undefined) timers.clearTimeout(timer); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isPort(value: unknown): boolean { return typeof value === "function" || (value !== null && typeof value === "object" && !Array.isArray(value)); }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SchemaObject, SqlExecutor, SchemaProbePort, RequiredSchema } from "./contracts.js";
|
|
2
|
+
/** @beta Identity distinguishes overloads and qualified columns. */
|
|
3
|
+
export function schemaObjectKey(object: SchemaObject): string {
|
|
4
|
+
return `${object.kind}:${object.name}${object.kind === "function" ? `(${object.signature ?? ""})` : ""}`;
|
|
5
|
+
}
|
|
6
|
+
/** @beta Parameterized catalog queries; this builder performs no I/O. */
|
|
7
|
+
export function buildSchemaObjectProbe(object: SchemaObject): { text: string; values: readonly unknown[] } {
|
|
8
|
+
if (object.kind === "function") return { text: "select to_regprocedure($1)::oid is not null as present", values: [`${object.name}(${object.signature ?? ""})`] };
|
|
9
|
+
if (object.kind === "table") return { text: "select exists (select 1 from pg_catalog.pg_class c where c.oid = to_regclass($1) and c.relkind in ('r','p')) as present", values: [object.name] };
|
|
10
|
+
const parts = object.name.split(".");
|
|
11
|
+
return { text: "select exists (select 1 from pg_catalog.pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as present", values: [parts.slice(0, -1).join("."), parts.at(-1)] };
|
|
12
|
+
}
|
|
13
|
+
/** @beta Explicit adapter. Host supplies independently observed database and migration identity. */
|
|
14
|
+
export function createSchemaProbe(executor: SqlExecutor, identity: (requirement: RequiredSchema) => Promise<{ database: string; version: string | null }>): SchemaProbePort {
|
|
15
|
+
return { async observe(requirement) {
|
|
16
|
+
const observed = await identity(requirement);
|
|
17
|
+
const objects = await Promise.all(requirement.objects.map(async (object) => {
|
|
18
|
+
const query = buildSchemaObjectProbe(object);
|
|
19
|
+
const { rows } = await executor.query(query.text, query.values);
|
|
20
|
+
return { object, present: rows.length === 1 && typeof rows[0]?.present === "boolean" ? rows[0].present : null };
|
|
21
|
+
}));
|
|
22
|
+
return { ...observed, objects };
|
|
23
|
+
} };
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** @beta Structural Standard Schema v1 interface; no validator dependency required. */
|
|
2
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
3
|
+
readonly "~standard": {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly vendor: string;
|
|
6
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
7
|
+
readonly types?: { readonly input: Input; readonly output: Output };
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** @beta */
|
|
11
|
+
export type Result<Output> = { readonly value: Output; readonly issues?: undefined }
|
|
12
|
+
| { readonly issues: readonly Issue[] };
|
|
13
|
+
/** @beta */
|
|
14
|
+
export interface Issue {
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly path?: readonly (PropertyKey | { readonly key: PropertyKey })[];
|
|
17
|
+
}
|