@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/AGENTS.md
CHANGED
|
@@ -27,6 +27,9 @@ process: an application binds its ports and composes it.
|
|
|
27
27
|
| `./risk` | kernel | candidate |
|
|
28
28
|
| `./shipping` | kernel | candidate |
|
|
29
29
|
| `./subscription` | kernel | candidate |
|
|
30
|
+
| `./outbox` | kernel | experimental |
|
|
31
|
+
| `./readiness` | kernel | experimental |
|
|
32
|
+
| `./standard-schema` | kernel | experimental |
|
|
30
33
|
| `./testing` | testing | testing |
|
|
31
34
|
|
|
32
35
|
Any version may change an export, and each change is recorded in
|
|
@@ -80,4 +83,21 @@ those before guessing at a contract.
|
|
|
80
83
|
|
|
81
84
|
## Readiness codes
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
The checker reports `OPENLUP_E_PORT_MISSING`, `OPENLUP_E_EVENT_UNHANDLED`,
|
|
87
|
+
`OPENLUP_E_EVENT_DUPLICATE`, `OPENLUP_E_SCHEDULE_UNBOUND`, `OPENLUP_E_SCHEMA_BEHIND`,
|
|
88
|
+
`OPENLUP_E_SET_MISMATCH` and `OPENLUP_E_ENV_MISSING`. Only a `ready` report admits
|
|
89
|
+
its exact candidate. A host must provide complete installed metadata and selected-adapter
|
|
90
|
+
observations; a report does not prove ACL, function bodies or recovery.
|
|
91
|
+
|
|
92
|
+
## Using this package in an application
|
|
93
|
+
|
|
94
|
+
- **Do not edit the installed package.** Do not change files under `node_modules/@openlup/`, and
|
|
95
|
+
do not patch it with `patch-package`, `overrides` or a fork.
|
|
96
|
+
- **Change behaviour through seams.** Use this package's ports and options and your application's
|
|
97
|
+
own composition. If a seam is missing, raise an upstream issue.
|
|
98
|
+
- **Copying source is ejection.** Copying this package's source into your application makes the
|
|
99
|
+
copy yours, including its upgrades.
|
|
100
|
+
- **Upgrade every `@openlup/*` package together,** after reading each changelog's `Migration:`
|
|
101
|
+
blocks.
|
|
102
|
+
- **Inside the OpenLup monorepo,** where this package is a workspace, the repository's root
|
|
103
|
+
`AGENTS.md` governs instead.
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,30 @@ SemVer promise.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.13.1]
|
|
12
|
+
|
|
13
|
+
- Publishable on the npm `latest` dist-tag as `0.13.1`, from tag
|
|
14
|
+
`openlup-core-v0.13.1`.
|
|
15
|
+
|
|
16
|
+
- Republish the unchanged core API and implementation with the repaired outbox publication as the complete `0.13.1` package set.
|
|
17
|
+
|
|
18
|
+
## [0.13.0]
|
|
19
|
+
|
|
20
|
+
- Publishable on the npm `latest` dist-tag as `0.13.0`, from tag
|
|
21
|
+
`openlup-core-v0.13.0`.
|
|
22
|
+
|
|
23
|
+
- Add experimental outbox envelope/handler descriptors, structural validation, job lease and candidate readiness contracts.
|
|
24
|
+
- Check package inventory, event ownership/coverage, actual bindings and qualified schema observations before admitting a new candidate.
|
|
25
|
+
|
|
26
|
+
Migration: existing exports remain; compose the selected rail and bind its schema probe before activation. Unknown observations refuse the new candidate and do not stop a serving version.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `AGENTS.md` gains a required "Using this package in an application" section: do not edit or
|
|
32
|
+
patch the installed package, change behaviour through its seams, and upgrade every
|
|
33
|
+
`@openlup/*` package together. `docs:check` refuses a guide without it.
|
|
34
|
+
|
|
11
35
|
## [0.12.0]
|
|
12
36
|
|
|
13
37
|
### Removed
|
package/README.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
`@openlup/core` is the kernel of the `@openlup/*` package family: the
|
|
4
4
|
framework-independent TypeScript contracts and logic for subscription and
|
|
5
5
|
bundle commerce that the other packages and an application's composition read.
|
|
6
|
-
Agents start with [AGENTS.md](AGENTS.md).
|
|
6
|
+
Agents start with [AGENTS.md](AGENTS.md). Its "Using this package in an
|
|
7
|
+
application" section, which `docs:check` requires, tells an application's agent
|
|
8
|
+
not to edit or patch the installed package and to upgrade every `@openlup/*`
|
|
9
|
+
package together.
|
|
7
10
|
|
|
8
11
|
## Channel and stability
|
|
9
12
|
|
|
@@ -18,6 +21,8 @@ Agents start with [AGENTS.md](AGENTS.md).
|
|
|
18
21
|
each change is recorded in [the changelog](CHANGELOG.md). Pin an exact
|
|
19
22
|
version and read the changelog when you upgrade.
|
|
20
23
|
|
|
24
|
+
The experimental `./outbox`, `./standard-schema` and `./readiness` subpaths provide shared infrastructure contracts. `checkReadiness` evaluates inert contribution descriptors, the complete host-observed package set, actual host bindings (schedule/route function identity must match the contribution) and a bounded schema probe. It never migrates, leases or sends. Only `ready` admits a new candidate; `unknown` and `unsatisfied` leave the serving candidate in place. The host owns inventory completeness, immutable artifact/configuration/schema identity and admission at every work entrance. Catalog existence does not prove SQL bodies, permissions or provider recovery.
|
|
25
|
+
|
|
21
26
|
## Package Surface Maturity
|
|
22
27
|
|
|
23
28
|
Every current export is an internal candidate, experimental kernel, or testing
|
|
@@ -40,6 +45,9 @@ promise.
|
|
|
40
45
|
| `./risk` | kernel | candidate | `smoke/riskStandalone.test.ts` |
|
|
41
46
|
| `./shipping` | kernel | candidate | `smoke/shippingStandalone.test.ts` |
|
|
42
47
|
| `./subscription` | kernel | candidate | `smoke/subscriptionBundleStandalone.test.ts` |
|
|
48
|
+
| `./outbox` | kernel | experimental | `smoke/outboxStandalone.test.ts` |
|
|
49
|
+
| `./readiness` | kernel | experimental | `smoke/readinessStandalone.test.ts` |
|
|
50
|
+
| `./standard-schema` | kernel | experimental | `smoke/standardSchemaStandalone.test.ts` |
|
|
43
51
|
| `./testing` | testing | testing | `smoke/testingStandalone.test.ts` |
|
|
44
52
|
|
|
45
53
|
`release-gates.json` records four distinct evidence classes:
|
|
@@ -52,9 +60,26 @@ promise.
|
|
|
52
60
|
- `externalConsumerEvidence`: not yet evaluated; a first-party packed consumer
|
|
53
61
|
remains package smoke, not external adoption.
|
|
54
62
|
|
|
55
|
-
##
|
|
63
|
+
## Use in an application
|
|
56
64
|
|
|
57
|
-
From
|
|
65
|
+
From your application's root, with Node 24 and npm 11.19.0:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm install --save-exact @openlup/core@0.12.0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Read the installed `AGENTS.md`, then compose the [typed wiring example](AGENTS.md#wiring-example)
|
|
72
|
+
with your application's own ports and options. Inspect the shipped sources and
|
|
73
|
+
declarations for the exact version you installed. A missing seam needs an
|
|
74
|
+
upstream proposal under your application's submission authority; the dependency
|
|
75
|
+
guide grants no permission to publish an issue or change your application.
|
|
76
|
+
Keep every installed `@openlup/*` package on the same exact set version.
|
|
77
|
+
|
|
78
|
+
## Local verification from source
|
|
79
|
+
|
|
80
|
+
The following developer commands require a source checkout of this package;
|
|
81
|
+
the npm tarball omits its development scripts and tests. From the source package
|
|
82
|
+
directory:
|
|
58
83
|
|
|
59
84
|
```sh
|
|
60
85
|
npm ci
|
|
@@ -63,10 +88,17 @@ npm run ci
|
|
|
63
88
|
|
|
64
89
|
Focused proofs are `npm run api:check`, `npm run release:check`, and
|
|
65
90
|
`npm run test:consumer`. The root test script skips this suite.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
`npm run docs:check` requires relative links in shipped Markdown to resolve to
|
|
92
|
+
files actually packed in the tarball; a target present only in source refuses.
|
|
93
|
+
Ambiguous destinations refuse explicitly: use literal punctuation or
|
|
94
|
+
percent-encoded filenames, with ASCII whitespace before an optional title.
|
|
95
|
+
Raw ampersands, angle characters and Unicode whitespace in local destinations
|
|
96
|
+
require a canonical rewrite rather than passing an incomplete link check.
|
|
97
|
+
|
|
98
|
+
The [repository neutrality checks](https://github.com/openlup/openlup/blob/openlup-core-v0.12.0/CONTRIBUTING.md#development-preview-checks)
|
|
99
|
+
reuse the source scanner through `./scripts/neutrality-tree-counts.ts` relative
|
|
100
|
+
to this source package. This process interface reads JSON containing source paths,
|
|
101
|
+
contents and a policy
|
|
70
102
|
from standard input, then emits finding counts for each path. Shell sources
|
|
71
103
|
retain the scanner's existing shell handling. The interface adds no package
|
|
72
104
|
export and changes no matcher or kernel behavior.
|
|
@@ -74,7 +106,7 @@ export and changes no matcher or kernel behavior.
|
|
|
74
106
|
Repository lint keeps this package's production source independent of configured
|
|
75
107
|
provider SDKs and explicit industry contract names. Adopter adapters compose
|
|
76
108
|
providers; generic ports can forward opaque extension data. See the
|
|
77
|
-
[syntax scope and exceptions](https://github.com/openlup/openlup/blob/
|
|
109
|
+
[syntax scope and exceptions](https://github.com/openlup/openlup/blob/openlup-core-v0.12.0/CONTRIBUTING.md#development-preview-checks).
|
|
78
110
|
|
|
79
111
|
The repository checker also calls the UI neutrality counting process with
|
|
80
112
|
`--counts-json` to count its existing patterns over the supplied sources. Running that checker without
|
|
@@ -90,6 +122,8 @@ neutrality evidence, not package activation or consumer compatibility evidence.
|
|
|
90
122
|
|
|
91
123
|
## Kernel Documentation
|
|
92
124
|
|
|
125
|
+
<!-- openlup-doc-impact {"unit":"core","digest":"sha256-4cbbfc0be291fa6ce550be48f3dafb7c9db6f474d8679ea1f66f8fd2a9947a9a","reason":"The core manifest and own lockfile move from 0.13.0 to 0.13.1 as part of the complete recovery set. API snapshots, exports, dependency resolutions, runtime behavior and package gates are unchanged; no application composition or verification step changes."} -->
|
|
126
|
+
|
|
93
127
|
- [`docs/SUBSCRIPTION_ENGINE.md`](docs/SUBSCRIPTION_ENGINE.md) — what the
|
|
94
128
|
`./subscription` kernel owns, its deterministic-clock contract, the
|
|
95
129
|
late-payment cycle-shift rule and its monotonic clamp, and how a host
|
|
@@ -0,0 +1,55 @@
|
|
|
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";
|
|
20
|
+
detail?: Record<string, unknown>;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "retry";
|
|
23
|
+
reason: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "discard";
|
|
26
|
+
reason: string;
|
|
27
|
+
benign?: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "snooze";
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/** @beta Phase observations are transient; never put payloads or credentials here. */
|
|
33
|
+
export interface OutboxHandlerExecutionContext {
|
|
34
|
+
setPhase(phase: string): void;
|
|
35
|
+
}
|
|
36
|
+
/** @beta Inert registration shared by preflight and the run-bound builder. */
|
|
37
|
+
export interface OutboxHandlerDescriptor {
|
|
38
|
+
readonly eventType: string;
|
|
39
|
+
readonly timeoutMs: number;
|
|
40
|
+
}
|
|
41
|
+
/** @beta Structural abort signal, shared by browser and server implementations. */
|
|
42
|
+
export interface OutboxAbortSignal {
|
|
43
|
+
readonly aborted: boolean;
|
|
44
|
+
readonly reason: unknown;
|
|
45
|
+
addEventListener(type: "abort", listener: () => void, options?: {
|
|
46
|
+
once?: boolean;
|
|
47
|
+
}): void;
|
|
48
|
+
removeEventListener(type: "abort", listener: () => void): void;
|
|
49
|
+
}
|
|
50
|
+
/** @beta Effects must be idempotent and honor the abort signal. */
|
|
51
|
+
export interface OutboxHandler<Signal extends OutboxAbortSignal = OutboxAbortSignal> extends OutboxHandlerDescriptor {
|
|
52
|
+
handle(row: OutboxEventRow, signal: Signal, execution?: OutboxHandlerExecutionContext): Promise<OutboxHandlerOutcome>;
|
|
53
|
+
}
|
|
54
|
+
export { PLATFORM_EVENT_VOCABULARY } from "./vocabulary.js";
|
|
55
|
+
export type { EventVocabularyEntry } from "./vocabulary.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PLATFORM_EVENT_VOCABULARY } from "./vocabulary.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @beta Vocabulary ownership is separate from consumer registration. */
|
|
2
|
+
export interface EventVocabularyEntry {
|
|
3
|
+
eventType: string;
|
|
4
|
+
owner: string;
|
|
5
|
+
}
|
|
6
|
+
/** @beta Applications supply their complete ordered persisted vocabulary separately. */
|
|
7
|
+
export declare const PLATFORM_EVENT_VOCABULARY: readonly EventVocabularyEntry[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** @beta Applications supply their complete ordered persisted vocabulary separately. */
|
|
2
|
+
export const PLATFORM_EVENT_VOCABULARY = [
|
|
3
|
+
{ eventType: "commerce.order_draft.created", owner: "@openlup/core" },
|
|
4
|
+
{ eventType: "commerce.order.paid", owner: "@openlup/core" },
|
|
5
|
+
{ eventType: "commerce.order.paid.email", owner: "@openlup/core" },
|
|
6
|
+
{ eventType: "commerce.payment.failed", owner: "@openlup/core" },
|
|
7
|
+
{ eventType: "commerce.checkout.expired", owner: "@openlup/core" },
|
|
8
|
+
{ eventType: "commerce.order.canceled", owner: "@openlup/core" },
|
|
9
|
+
{ eventType: "commerce.order.refunded", owner: "@openlup/core" },
|
|
10
|
+
{ eventType: "commerce.shipment.dispatched", owner: "@openlup/core" },
|
|
11
|
+
{ eventType: "commerce.shipment.delivered", owner: "@openlup/core" },
|
|
12
|
+
{ eventType: "commerce.shipment.exception", owner: "@openlup/core" },
|
|
13
|
+
{ eventType: "commerce.return.approved", owner: "@openlup/core" },
|
|
14
|
+
{ eventType: "commerce.return.rejected", owner: "@openlup/core" },
|
|
15
|
+
{ eventType: "commerce.order.review_request", owner: "@openlup/core" },
|
|
16
|
+
{ eventType: "commerce.order.review_effects", owner: "@openlup/core" },
|
|
17
|
+
{ eventType: "commerce.product.back_in_stock", owner: "@openlup/core" },
|
|
18
|
+
{ eventType: "commerce.order_draft.abandoned.1h", owner: "@openlup/core" },
|
|
19
|
+
{ eventType: "commerce.order_draft.abandoned.24h", owner: "@openlup/core" },
|
|
20
|
+
{ eventType: "commerce.order_draft.abandoned.72h", owner: "@openlup/core" },
|
|
21
|
+
{ eventType: "commerce.order.reorder_reminder", owner: "@openlup/core" },
|
|
22
|
+
{ eventType: "commerce.checkout_recovery", owner: "@openlup/core" },
|
|
23
|
+
{ eventType: "subscription.cancelled", owner: "@openlup/core" },
|
|
24
|
+
{ eventType: "subscription.created", owner: "@openlup/core" },
|
|
25
|
+
{ eventType: "subscription.activation_action_required", owner: "@openlup/core" },
|
|
26
|
+
{ eventType: "subscription.pause_reminder_due", owner: "@openlup/core" },
|
|
27
|
+
{ eventType: "subscription.paused", owner: "@openlup/core" },
|
|
28
|
+
{ eventType: "subscription.resumed", owner: "@openlup/core" },
|
|
29
|
+
{ eventType: "subscription.delivery_rescheduled", owner: "@openlup/core" },
|
|
30
|
+
{ eventType: "subscription.cycle_skipped", owner: "@openlup/core" },
|
|
31
|
+
{ eventType: "subscription.address_changed", owner: "@openlup/core" },
|
|
32
|
+
{ eventType: "subscription.package_changed", owner: "@openlup/core" },
|
|
33
|
+
{ eventType: "subscription.renewal_upcoming", owner: "@openlup/core" },
|
|
34
|
+
{ eventType: "accounting.order.reversal_requested", owner: "@openlup/core" },
|
|
35
|
+
{ eventType: "commerce.fulfillment.handed_over", owner: "@openlup/core" },
|
|
36
|
+
];
|
|
@@ -8,3 +8,4 @@ export { createPlatformBundleRegistry, platformEnvSchema, } from "./platformKern
|
|
|
8
8
|
export type { CreatePlatformBundleRegistryInput, PlatformBundleDescriptor, PlatformBundleRegistry, PlatformBundleReadiness, PlatformEnv, PlatformEnvInput, } from "./platformKernel.js";
|
|
9
9
|
/** @beta */
|
|
10
10
|
export type { AnalyticsPort, BlobStoragePort, DataGatewayPort, HttpRuntimePort, MigrationRunnerPort, PlatformHttpRequest, PlatformHttpResponse, SchedulerPort, TransactionalRuntimePort, } from "./ports.js";
|
|
11
|
+
export type { JobLeasePort, PlatformJobTriggerKind, PlatformJobInvocation, PlatformJobClaim, PlatformJobFinishStatus, PlatformJobFinishSummary } from "./ports.js";
|
|
@@ -98,3 +98,27 @@ export interface TransactionalRuntimePort {
|
|
|
98
98
|
status: number;
|
|
99
99
|
}>;
|
|
100
100
|
}
|
|
101
|
+
/** @beta Invocation evidence and claim/finish contract shared by infrastructure rails. */
|
|
102
|
+
export type PlatformJobTriggerKind = "worker" | "scheduler" | "operator";
|
|
103
|
+
export type PlatformJobInvocation = {
|
|
104
|
+
triggerKind: PlatformJobTriggerKind;
|
|
105
|
+
/** Observational adapter evidence; it never grants execution authority. */
|
|
106
|
+
invocationSource: string;
|
|
107
|
+
};
|
|
108
|
+
export type PlatformJobClaim = {
|
|
109
|
+
acquired: boolean;
|
|
110
|
+
runId: string | null;
|
|
111
|
+
reason: string;
|
|
112
|
+
};
|
|
113
|
+
export type PlatformJobFinishStatus = "success" | "failed";
|
|
114
|
+
export type PlatformJobFinishSummary = {
|
|
115
|
+
checked: number;
|
|
116
|
+
updated: number;
|
|
117
|
+
failures: number;
|
|
118
|
+
skipped: boolean;
|
|
119
|
+
reason?: string;
|
|
120
|
+
};
|
|
121
|
+
export interface JobLeasePort {
|
|
122
|
+
claimJobRun(jobName: string, invocation: PlatformJobInvocation, leaseSeconds?: number): Promise<PlatformJobClaim>;
|
|
123
|
+
finishJobRun(jobName: string, runId: string, invocation: PlatformJobInvocation, status: PlatformJobFinishStatus, result: PlatformJobFinishSummary, extraMetadata?: Record<string, unknown>): Promise<boolean>;
|
|
124
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { OutboxHandlerDescriptor } from "../outbox/index.js";
|
|
2
|
+
import type { EventVocabularyEntry } from "../outbox/vocabulary.js";
|
|
3
|
+
/** @beta */
|
|
4
|
+
export declare const READINESS_CODES: readonly ["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"];
|
|
5
|
+
/** @beta */
|
|
6
|
+
export type ReadinessCode = typeof READINESS_CODES[number];
|
|
7
|
+
/** @beta */
|
|
8
|
+
export interface ReadinessIssue {
|
|
9
|
+
code: ReadinessCode;
|
|
10
|
+
package: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
fix: string;
|
|
13
|
+
observation?: string;
|
|
14
|
+
}
|
|
15
|
+
/** @beta */
|
|
16
|
+
export interface SchemaObject {
|
|
17
|
+
kind: "table" | "column" | "function";
|
|
18
|
+
name: string;
|
|
19
|
+
signature?: string;
|
|
20
|
+
}
|
|
21
|
+
/** @beta Version is an adapter-specific migration identity, not inferred from object existence. */
|
|
22
|
+
export interface RequiredSchema {
|
|
23
|
+
version: string;
|
|
24
|
+
migration: string;
|
|
25
|
+
objects: readonly SchemaObject[];
|
|
26
|
+
}
|
|
27
|
+
/** @beta */
|
|
28
|
+
export interface PackageManifest {
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
kind: "kernel" | "rail" | "capability" | "implementation";
|
|
32
|
+
emits: readonly string[];
|
|
33
|
+
handles: readonly string[];
|
|
34
|
+
schedules: readonly {
|
|
35
|
+
id: string;
|
|
36
|
+
cadenceSeconds: number;
|
|
37
|
+
}[];
|
|
38
|
+
routes: readonly string[];
|
|
39
|
+
requiredPorts: readonly string[];
|
|
40
|
+
env: readonly string[];
|
|
41
|
+
requiredSchema?: RequiredSchema;
|
|
42
|
+
}
|
|
43
|
+
/** @beta Run-bound handlers are represented by their shared inert descriptors. */
|
|
44
|
+
export interface Contribution {
|
|
45
|
+
manifest: PackageManifest;
|
|
46
|
+
handlers: readonly OutboxHandlerDescriptor[];
|
|
47
|
+
schedules: readonly {
|
|
48
|
+
id: string;
|
|
49
|
+
cadenceSeconds: number;
|
|
50
|
+
run: (...args: never[]) => unknown;
|
|
51
|
+
}[];
|
|
52
|
+
routes: readonly {
|
|
53
|
+
id: string;
|
|
54
|
+
handle: (...args: never[]) => unknown;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
/** @beta Host-observed inventory, including core. The loader owns completeness. */
|
|
58
|
+
export interface LoadedPackage {
|
|
59
|
+
name: string;
|
|
60
|
+
version: string;
|
|
61
|
+
kind: PackageManifest["kind"];
|
|
62
|
+
}
|
|
63
|
+
/** @beta Candidate identity must change when configuration, schema or artifact changes. */
|
|
64
|
+
export interface CandidateIdentity {
|
|
65
|
+
artifact: string;
|
|
66
|
+
configuration: string;
|
|
67
|
+
environment: string;
|
|
68
|
+
database: string;
|
|
69
|
+
schema: string;
|
|
70
|
+
}
|
|
71
|
+
/** @beta */
|
|
72
|
+
export interface EventExemption {
|
|
73
|
+
pattern: string;
|
|
74
|
+
prefix: boolean;
|
|
75
|
+
owner: string;
|
|
76
|
+
reason: string;
|
|
77
|
+
state: "ignored" | "dormant";
|
|
78
|
+
}
|
|
79
|
+
/** @beta */
|
|
80
|
+
export interface ReadinessInput {
|
|
81
|
+
candidate: CandidateIdentity;
|
|
82
|
+
inventoryComplete: boolean;
|
|
83
|
+
loadedPackages: readonly LoadedPackage[];
|
|
84
|
+
contributions: readonly Contribution[];
|
|
85
|
+
vocabulary: readonly EventVocabularyEntry[];
|
|
86
|
+
exemptions: readonly EventExemption[];
|
|
87
|
+
bindings: {
|
|
88
|
+
ports: Readonly<Record<string, unknown>>;
|
|
89
|
+
triggers: Readonly<Record<string, unknown>>;
|
|
90
|
+
routes: Readonly<Record<string, unknown>>;
|
|
91
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
92
|
+
};
|
|
93
|
+
schemaProbe: SchemaProbePort;
|
|
94
|
+
probeTimeoutMs?: number;
|
|
95
|
+
}
|
|
96
|
+
/** @beta A bounded catalog observation, not proof of SQL bodies, ACL or recovery. */
|
|
97
|
+
export interface SchemaObservation {
|
|
98
|
+
database: string;
|
|
99
|
+
version: string | null;
|
|
100
|
+
objects: readonly {
|
|
101
|
+
object: SchemaObject;
|
|
102
|
+
present: boolean | null;
|
|
103
|
+
}[];
|
|
104
|
+
}
|
|
105
|
+
/** @beta */
|
|
106
|
+
export interface SchemaProbePort {
|
|
107
|
+
observe(requirement: RequiredSchema): Promise<SchemaObservation>;
|
|
108
|
+
}
|
|
109
|
+
/** @beta Each operation commits before resolving; never lend a producer transaction to dispatch. */
|
|
110
|
+
export interface SqlExecutor {
|
|
111
|
+
query<Row extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: readonly unknown[]): Promise<{
|
|
112
|
+
rows: Row[];
|
|
113
|
+
}>;
|
|
114
|
+
}
|
|
115
|
+
/** @beta */
|
|
116
|
+
export interface ReadinessReport {
|
|
117
|
+
state: "ready" | "unsatisfied" | "unknown";
|
|
118
|
+
candidate: CandidateIdentity;
|
|
119
|
+
loadedPackages: readonly LoadedPackage[];
|
|
120
|
+
issues: readonly ReadinessIssue[];
|
|
121
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ReadinessInput, ReadinessIssue } from "./contracts.js";
|
|
3
|
+
export declare const schemaObjectShape: z.ZodObject<{
|
|
4
|
+
kind: z.ZodEnum<{
|
|
5
|
+
function: "function";
|
|
6
|
+
table: "table";
|
|
7
|
+
column: "column";
|
|
8
|
+
}>;
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
signature: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export declare const manifestShape: z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
version: z.ZodString;
|
|
15
|
+
kind: z.ZodEnum<{
|
|
16
|
+
kernel: "kernel";
|
|
17
|
+
rail: "rail";
|
|
18
|
+
capability: "capability";
|
|
19
|
+
implementation: "implementation";
|
|
20
|
+
}>;
|
|
21
|
+
emits: z.ZodArray<z.ZodString>;
|
|
22
|
+
handles: z.ZodArray<z.ZodString>;
|
|
23
|
+
schedules: z.ZodArray<z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
cadenceSeconds: z.ZodNumber;
|
|
26
|
+
}, z.core.$strict>>;
|
|
27
|
+
routes: z.ZodArray<z.ZodString>;
|
|
28
|
+
requiredPorts: z.ZodArray<z.ZodString>;
|
|
29
|
+
env: z.ZodArray<z.ZodString>;
|
|
30
|
+
requiredSchema: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
version: z.ZodString;
|
|
32
|
+
migration: z.ZodString;
|
|
33
|
+
objects: z.ZodArray<z.ZodObject<{
|
|
34
|
+
kind: z.ZodEnum<{
|
|
35
|
+
function: "function";
|
|
36
|
+
table: "table";
|
|
37
|
+
column: "column";
|
|
38
|
+
}>;
|
|
39
|
+
name: z.ZodString;
|
|
40
|
+
signature: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, z.core.$strict>>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
/** Validate declarations without invoking a handler, schedule, route or probe. */
|
|
45
|
+
export declare function declarationIssues(input: ReadinessInput): ReadinessIssue[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const name = z.string().min(1).max(240).regex(/^[a-zA-Z0-9@_./:-]+$/);
|
|
3
|
+
const version = z.string().regex(/^0\.\d+\.\d+$/);
|
|
4
|
+
const list = z.array(name).refine((items) => new Set(items).size === items.length);
|
|
5
|
+
const kind = z.enum(["kernel", "rail", "capability", "implementation"]);
|
|
6
|
+
export const schemaObjectShape = z.object({ kind: z.enum(["table", "column", "function"]), name,
|
|
7
|
+
signature: z.string().max(1000).optional() }).strict().refine((o) => o.name.split(".").length === (o.kind === "column" ? 3 : 2) && (o.kind !== "function" || o.signature !== undefined));
|
|
8
|
+
export const manifestShape = z.object({ name: name.regex(/^@openlup\/[a-z0-9-]+$/), version, kind,
|
|
9
|
+
emits: list, handles: list, schedules: z.array(z.object({ id: name, cadenceSeconds: z.number().finite().positive() }).strict()),
|
|
10
|
+
routes: list, requiredPorts: list, env: list,
|
|
11
|
+
requiredSchema: z.object({ version: name, migration: name, objects: z.array(schemaObjectShape) }).strict().optional(),
|
|
12
|
+
}).strict();
|
|
13
|
+
/** Validate declarations without invoking a handler, schedule, route or probe. */
|
|
14
|
+
export function declarationIssues(input) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
const issue = (subject, fix, code = "OPENLUP_E_SET_MISMATCH") => issues.push({ code, package: "@openlup/core", subject, fix });
|
|
17
|
+
if (!input || typeof input !== "object") {
|
|
18
|
+
issue("input", "Supply complete candidate observations.");
|
|
19
|
+
return issues;
|
|
20
|
+
}
|
|
21
|
+
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)))
|
|
22
|
+
issue("candidate", "Supply sanitized immutable artifact, configuration, environment and database identities.");
|
|
23
|
+
if (input.inventoryComplete !== true)
|
|
24
|
+
issue("inventory", "Observe the complete installed package inventory, including core.");
|
|
25
|
+
if (!Array.isArray(input.loadedPackages) || !Array.isArray(input.contributions)) {
|
|
26
|
+
issue("inventory", "Supply loaded packages and contributions as arrays.");
|
|
27
|
+
return issues;
|
|
28
|
+
}
|
|
29
|
+
for (const pkg of input.loadedPackages)
|
|
30
|
+
if (!z.object({ name, version, kind }).strict().safeParse(pkg).success)
|
|
31
|
+
issue("loaded-package", "Supply a valid observed package name, version and kind.");
|
|
32
|
+
for (const c of input.contributions) {
|
|
33
|
+
if (!manifestShape.safeParse(c?.manifest).success) {
|
|
34
|
+
issue("manifest", "Supply a valid complete manifest with unique declarations.");
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(c.handlers) || c.handlers.some((h) => !name.safeParse(h?.eventType).success || !Number.isFinite(h?.timeoutMs) || h?.timeoutMs <= 0))
|
|
38
|
+
issue(c.manifest.name, "Supply valid shared handler descriptors.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
39
|
+
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"))
|
|
40
|
+
issue(c.manifest.name, "Supply actual runnable schedules.", "OPENLUP_E_SCHEDULE_UNBOUND");
|
|
41
|
+
if (!Array.isArray(c.routes) || c.routes.some((r) => !name.safeParse(r?.id).success || typeof r?.handle !== "function"))
|
|
42
|
+
issue(c.manifest.name, "Supply actual route registrations.", "OPENLUP_E_PORT_MISSING");
|
|
43
|
+
const objects = c.manifest.requiredSchema?.objects ?? [];
|
|
44
|
+
if (new Set(objects.map((o) => `${o.kind}:${o.name}:${o.signature ?? ""}`)).size !== objects.length)
|
|
45
|
+
issue(c.manifest.name, "Remove duplicate schema requirements.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(input.vocabulary) || input.vocabulary.some((v) => !name.safeParse(v?.eventType).success || !name.safeParse(v?.owner).success))
|
|
48
|
+
issue("vocabulary", "Supply explicit vocabulary entries with owners.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
49
|
+
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)))
|
|
50
|
+
issue("exemptions", "Supply explicit owned exemptions with reasons.", "OPENLUP_E_EVENT_UNHANDLED");
|
|
51
|
+
if (!input.bindings || [input.bindings.ports, input.bindings.routes, input.bindings.triggers, input.bindings.env].some((v) => !v || typeof v !== "object" || Array.isArray(v)))
|
|
52
|
+
issue("bindings", "Supply actual host bindings.", "OPENLUP_E_PORT_MISSING");
|
|
53
|
+
if (!input.schemaProbe || typeof input.schemaProbe.observe !== "function")
|
|
54
|
+
issue("schemaProbe", "Bind a schema observation port.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
55
|
+
if (input.probeTimeoutMs !== undefined && (!Number.isFinite(input.probeTimeoutMs) || input.probeTimeoutMs < 1 || input.probeTimeoutMs > 30000))
|
|
56
|
+
issue("probeTimeoutMs", "Use a bounded probe timeout from 1 to 30000 ms.", "OPENLUP_E_SCHEMA_BEHIND");
|
|
57
|
+
return issues;
|
|
58
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReadinessInput, ReadinessReport } from "./contracts.js";
|
|
2
|
+
export * from "./contracts.js";
|
|
3
|
+
export { schemaObjectKey, buildSchemaObjectProbe, createSchemaProbe } from "./schemaProbe.js";
|
|
4
|
+
/** @beta Evaluates supplied facts; never performs migration or effect work. */
|
|
5
|
+
export declare function checkReadiness(input: ReadinessInput): Promise<ReadinessReport>;
|