@sekiban/dcb-domain 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,92 @@
1
+ Elastic License 2.0
2
+
3
+ Acceptance
4
+
5
+ By using the software, you agree to all of the terms and conditions below.
6
+
7
+ Copyright License
8
+
9
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
10
+ non-sublicensable, non-transferable license to use, copy, distribute, make
11
+ available, and prepare derivative works of the software, in each case subject
12
+ to the limitations and conditions below.
13
+
14
+ Limitations
15
+
16
+ You may not provide the software to third parties as a hosted or managed
17
+ service, where the service provides users with access to any substantial set
18
+ of the features or functionality of the software.
19
+
20
+ You may not move, change, disable, or circumvent the license key functionality
21
+ in the software, and you may not remove or obscure any functionality in the
22
+ software that is protected by the license key.
23
+
24
+ You may not alter, remove, or obscure any licensing, copyright, or other
25
+ notices of the licensor in the software. Any use of the licensor's trademarks
26
+ is subject to applicable law.
27
+
28
+ Patents
29
+
30
+ The licensor grants you a license, under any patent claims the licensor can
31
+ license, or becomes able to license, to make, have made, use, sell, offer for
32
+ sale, import and have imported the software, in each case subject to the
33
+ limitations and conditions in this license. This license does not cover any
34
+ patent claims that you cause to be infringed by modifications or additions to
35
+ the software.
36
+
37
+ If you or your company make any written claim that the software infringes or
38
+ contributes to infringement of any patent, your patent license for the software
39
+ granted under these terms ends immediately. If your company makes such a
40
+ claim, your patent license ends immediately for work on behalf of your company.
41
+
42
+ Notices
43
+
44
+ You must ensure that anyone who gets a copy of any part of the software from
45
+ you also gets a copy of these terms.
46
+
47
+ If you modify the software, you must include in any modified copies of the
48
+ software prominent notices stating that you have modified the software.
49
+
50
+ No Other Rights
51
+
52
+ These terms do not imply any licenses other than those expressly granted in
53
+ these terms.
54
+
55
+ Termination
56
+
57
+ If you use the software in violation of these terms, such use is not licensed,
58
+ and your licenses will automatically terminate. If the licensor provides you
59
+ with a notice of your violation, and you cease all violation of this license no
60
+ later than 30 days after you receive that notice, your licenses will be
61
+ reinstated retroactively. However, if you violate these terms after such
62
+ reinstatement, any additional violation of these terms will cause your licenses
63
+ to terminate automatically and permanently.
64
+
65
+ No Liability
66
+
67
+ As far as the law allows, the software comes as is, without any warranty or
68
+ condition, and the licensor will not be liable to you for any damages arising
69
+ out of these terms or the use or nature of the software, under any kind of
70
+ legal claim.
71
+
72
+ Definitions
73
+
74
+ The licensor is the entity offering these terms, and the software is the
75
+ software the licensor makes available under these terms, including any portion
76
+ of it.
77
+
78
+ you refers to the individual or entity agreeing to these terms.
79
+
80
+ your company is any legal entity, sole proprietorship, or other kind of
81
+ organization that you work for, plus all organizations that have control over,
82
+ are under the control of, or are under common control with that organization.
83
+ control means ownership of substantially all the assets of an entity, or the
84
+ power to direct its management and policies by vote, contract, or otherwise.
85
+ Control can be direct or indirect.
86
+
87
+ your licenses are all the licenses granted to you for the software under these
88
+ terms.
89
+
90
+ use means anything you do with the software requiring one of your licenses.
91
+
92
+ trademark means trademarks, service marks, and similar rights.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @sekiban/dcb-domain
2
+
3
+ `@sekiban/dcb-domain` is the runtime-free, schema-first authoring layer for
4
+ Sekiban DCB domains. It depends on `zod` only and does not import the runtime,
5
+ storage, transport, or host APIs.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @sekiban/dcb-domain zod
11
+ ```
12
+
13
+ The package is ESM-only and supports Node.js 20 or newer. The `zod` runtime
14
+ dependency is pinned to `4.4.3` for the 0.1.x line.
15
+
16
+ ```ts
17
+ import { z } from "zod";
18
+ import {
19
+ command,
20
+ done,
21
+ domain,
22
+ event,
23
+ projector,
24
+ read,
25
+ stateUnion,
26
+ tagFamily,
27
+ toRuntimeDomain,
28
+ } from "@sekiban/dcb-domain";
29
+
30
+ const order = tagFamily("order");
31
+ const placed = event("OrderPlaced", z.object({ orderId: z.string() }), {
32
+ tags: (value) => [order.of(value.orderId)],
33
+ });
34
+
35
+ const orderProjector = projector({
36
+ id: "orders",
37
+ tag: order,
38
+ events: [placed],
39
+ state: stateUnion(z.object({ count: z.number() }), { initial: { count: 0 } }),
40
+ initialState: { count: 0 },
41
+ handlers: {
42
+ OrderPlaced: (state) => ({ count: state.count + 1 }),
43
+ },
44
+ });
45
+
46
+ const place = command({
47
+ id: "place-order",
48
+ input: z.object({ orderId: z.string() }),
49
+ reads: (input) => read(orderProjector, order.of(input.orderId)),
50
+ handle: async (input, ctx) => {
51
+ ctx.append(placed, placed.make(input));
52
+ return done();
53
+ },
54
+ });
55
+
56
+ const authoringDomain = domain({
57
+ events: [placed],
58
+ projectors: [orderProjector],
59
+ commands: [place],
60
+ });
61
+
62
+ // The bridge is the only adapter needed by the existing runtime.
63
+ const runtimeDomain = toRuntimeDomain(authoringDomain);
64
+ ```
65
+
66
+ Events require both a Zod schema and an explicit tag derivation. `make()`
67
+ validates and brands the payload; `append()` accepts that branded payload and
68
+ derives tags from the event definition, so caller-supplied routing cannot drift.
69
+ Projectors bind their tag family at compile time. Commands declare their pure,
70
+ bounded read set and receive only `state`, `exists`, `now`, and `append`.
71
+
72
+ Sessions use one executor-captured `now`, per-(projector, tag) snapshots, a
73
+ fresh session on consistency-conflict retry, and an atomic `done` envelope.
74
+ `none`, `reject`, thrown errors, and cancellation discard all tentative work.
75
+ Portable snapshots, decision logs, and the five ingress/query parse boundaries
76
+ are exported from the main entrypoint. Pure command/evolve exercises are
77
+ available from `@sekiban/dcb-domain/testing`.
78
+
79
+ The package boundary is enforced in CI by the dedicated compile-fail project,
80
+ source import/global checks, negative fixtures, and an `npm pack --dry-run`
81
+ inspection. Its only runtime dependency is the pinned `zod` package.
82
+
83
+ ## Versioning
84
+
85
+ The package follows semver while the major version is `0`: minor releases may
86
+ add public authoring capabilities, and patch releases are limited to fixes and
87
+ documentation. The public surface is frozen to the following entrypoints and
88
+ helpers for the `0.1.x` line:
89
+
90
+ - Main entrypoint: `domain`, `event`, `eventUnion`, `projector`, `stateUnion`,
91
+ `command`, `done`, `none`, `reject`, `read`, `readExists`, `Session`,
92
+ portable-snapshot serialization, the five boundary parsers, and the
93
+ runtime-domain bridge (`toRuntimeDomain`).
94
+ - Testing entrypoint: `given`, `evolveTable`, and `evolve` from
95
+ `@sekiban/dcb-domain/testing`.
96
+ - No deep imports beyond `.` and `./testing` are supported.
97
+
98
+ The earlier `@sekiban/core` and related packages belong to the older
99
+ sekiban-ts line; they are not dependencies or aliases for this package.
100
+
101
+ The first public release is `0.1.0`. The repository tag
102
+ `dcb-domain-v0.1.0` drives the provenance-enabled release workflow. The final
103
+ `npm publish --provenance --access public` is an operator action performed
104
+ only after the trusted publisher or `NPM_TOKEN` fallback has been configured.
@@ -0,0 +1,162 @@
1
+ import { type CandidateEnvelope, type CommitCandidateEvent, type FixedNow, type JsonValue, type SnapshotReader } from "./types.js";
2
+ import type { CommandDefinition } from "./command.js";
3
+ import type { EventDefinition, RuntimeEventValue } from "./event.js";
4
+ import type { AuthoringDomain, DomainViewDefinition } from "./domain.js";
5
+ export interface RuntimeProjectionEvent {
6
+ readonly eventId?: string;
7
+ readonly suid?: string;
8
+ readonly eventType: string;
9
+ readonly eventPayloadName?: string;
10
+ readonly payload: unknown;
11
+ readonly eventTags?: readonly string[];
12
+ readonly provenance?: "g32";
13
+ }
14
+ export interface RuntimeEventDefinition {
15
+ readonly name: string;
16
+ readonly eventName: string;
17
+ readonly eventPayloadName: string;
18
+ readonly eventType: string;
19
+ readonly create: (payload: unknown) => RuntimeEventValue;
20
+ readonly construct: (payload: unknown) => RuntimeEventValue;
21
+ readonly parse: (payload: unknown) => JsonValue;
22
+ }
23
+ export interface RuntimeProjectorDefinition {
24
+ readonly id: string;
25
+ readonly projectorId: string;
26
+ readonly version: number;
27
+ readonly projectorVersion: number;
28
+ readonly subscribedEventNames: readonly string[];
29
+ readonly subscribedEventTypes: readonly string[];
30
+ readonly initialState: JsonValue;
31
+ readonly apply: (state: JsonValue, event: RuntimeProjectionEvent) => JsonValue;
32
+ readonly reduce: (state: JsonValue, event: RuntimeProjectionEvent) => JsonValue;
33
+ readonly serializeState: (state: JsonValue) => string;
34
+ readonly deserializeState: (serialized: string) => JsonValue;
35
+ }
36
+ export interface RuntimeCommandDefinition {
37
+ readonly id: string;
38
+ readonly name: string;
39
+ readonly parseInput: (value: unknown) => unknown;
40
+ readonly execute: (value: unknown, options?: RuntimeCommandExecutionOptions) => RuntimeCommandOutcome | Promise<RuntimeCommandOutcome>;
41
+ readonly handle: (value: unknown, options?: RuntimeCommandExecutionOptions) => RuntimeCommandOutcome | Promise<RuntimeCommandOutcome>;
42
+ }
43
+ export interface RuntimeCommandCandidateEvent extends CommitCandidateEvent {
44
+ readonly provenance: "g32";
45
+ }
46
+ export interface RuntimeCommandCandidateEnvelope {
47
+ readonly kind: "candidate-envelope";
48
+ readonly now: FixedNow;
49
+ readonly events: readonly RuntimeCommandCandidateEvent[];
50
+ readonly tags: CandidateEnvelope["tags"];
51
+ readonly readClaims: CandidateEnvelope["readClaims"];
52
+ readonly decision: CandidateEnvelope["decision"];
53
+ }
54
+ export interface RuntimeAllocationVector {
55
+ readonly candidates: readonly {
56
+ readonly ordinal: string;
57
+ readonly suid: string;
58
+ }[];
59
+ readonly allocatorLineageId?: string;
60
+ /** Durable attempt identity returned by the runtime allocator/admission path. */
61
+ readonly attemptId?: string;
62
+ }
63
+ /**
64
+ * Immutable response-loss context. Reconciliation must use this exact
65
+ * candidate and allocation rather than constructing a fresh submission.
66
+ */
67
+ export interface RuntimeCommandAttemptContext {
68
+ readonly candidateKey: string;
69
+ readonly attemptId: string;
70
+ readonly candidate: RuntimeCommandCandidateEnvelope;
71
+ readonly allocation?: RuntimeAllocationVector;
72
+ }
73
+ export type RuntimeCommandPortResult = {
74
+ readonly kind: "accepted";
75
+ readonly attemptId?: string;
76
+ } | {
77
+ readonly kind: "consistency-conflict";
78
+ readonly error?: unknown;
79
+ } | {
80
+ readonly kind: "unknown";
81
+ readonly error?: unknown;
82
+ readonly attemptId?: string;
83
+ } | {
84
+ readonly kind: "rejected";
85
+ readonly error?: unknown;
86
+ readonly reason?: string;
87
+ readonly code?: string;
88
+ };
89
+ export interface RuntimeCommandPort {
90
+ /** Read-only conflict barrier. A conflict here must not enter any write port. */
91
+ readonly conflictBarrier?: (candidate: RuntimeCommandCandidateEnvelope) => Promise<RuntimeCommandPortResult> | RuntimeCommandPortResult;
92
+ /** Admission write gate runs after the read-only conflict barrier and before allocation. */
93
+ readonly admit?: (candidate: RuntimeCommandCandidateEnvelope) => Promise<RuntimeCommandPortResult> | RuntimeCommandPortResult;
94
+ /** Allocation is deliberately after admission and receives no durable id from the authoring log. */
95
+ readonly allocate?: (candidate: RuntimeCommandCandidateEnvelope) => Promise<RuntimeAllocationVector> | RuntimeAllocationVector;
96
+ /** Commit receives the one allocated vector and the same canonical G27 event identity. */
97
+ readonly commit?: (candidate: RuntimeCommandCandidateEnvelope, allocation?: RuntimeAllocationVector) => Promise<RuntimeCommandPortResult> | RuntimeCommandPortResult;
98
+ /** An unknown outcome is reconciled against the same immutable candidate/attempt/vector, never resubmitted as new work. */
99
+ readonly reconcile?: (context: RuntimeCommandAttemptContext, outcome: RuntimeCommandPortResult) => Promise<RuntimeCommandPortResult> | RuntimeCommandPortResult;
100
+ }
101
+ export interface RuntimeCommandExecutionOptions {
102
+ readonly state?: Readonly<Record<string, JsonValue>>;
103
+ readonly now?: FixedNow;
104
+ readonly snapshots?: SnapshotReader;
105
+ readonly runtimePort?: RuntimeCommandPort;
106
+ }
107
+ export interface RuntimeCommandCommitted {
108
+ readonly kind: "committed";
109
+ readonly value?: JsonValue;
110
+ readonly events: readonly RuntimeCommandCandidateEvent[];
111
+ }
112
+ export interface RuntimeCommandNoop {
113
+ readonly kind: "noop";
114
+ readonly reason?: string;
115
+ readonly events: readonly [];
116
+ }
117
+ export interface RuntimeCommandRejected {
118
+ readonly kind: "rejected";
119
+ readonly reason: string;
120
+ readonly code: string;
121
+ readonly events: readonly [];
122
+ }
123
+ export type RuntimeCommandOutcome = RuntimeCommandCommitted | RuntimeCommandNoop | RuntimeCommandRejected;
124
+ export interface RuntimeDomainDefinition {
125
+ readonly events: readonly RuntimeEventDefinition[];
126
+ readonly commands: readonly RuntimeCommandDefinition[];
127
+ readonly projectors: readonly RuntimeProjectorDefinition[];
128
+ readonly views: readonly DomainViewDefinition[];
129
+ readonly queries: readonly [];
130
+ readonly materializedViews: readonly [];
131
+ readonly eventByName: ReadonlyMap<string, RuntimeEventDefinition>;
132
+ readonly eventByType: ReadonlyMap<string, RuntimeEventDefinition>;
133
+ readonly __sekibanAuthoringBridge: true;
134
+ }
135
+ interface LegacyEventDefinition {
136
+ readonly name?: string;
137
+ readonly eventName?: string;
138
+ readonly eventPayloadName?: string;
139
+ readonly eventType?: string;
140
+ readonly parse?: (payload: unknown) => unknown;
141
+ readonly create?: (payload: unknown) => {
142
+ readonly payload?: unknown;
143
+ readonly eventName?: string;
144
+ readonly eventPayloadName?: string;
145
+ };
146
+ readonly construct?: (payload: unknown) => {
147
+ readonly payload?: unknown;
148
+ readonly eventName?: string;
149
+ readonly eventPayloadName?: string;
150
+ };
151
+ }
152
+ interface LegacyDomainDefinition {
153
+ readonly events?: readonly LegacyEventDefinition[];
154
+ readonly commands?: readonly RuntimeCommandDefinition[];
155
+ readonly projectors?: readonly RuntimeProjectorDefinition[];
156
+ readonly views?: readonly DomainViewDefinition[];
157
+ }
158
+ /** Adapt an authoring command into the existing runtime's three-outcome contract. */
159
+ export declare function adaptRuntimeCommand(command: CommandDefinition): RuntimeCommandDefinition;
160
+ export declare function toRuntimeDomain(domain: AuthoringDomain<readonly EventDefinition[], readonly unknown[], readonly unknown[]> | LegacyDomainDefinition): RuntimeDomainDefinition;
161
+ export declare const normalizeRuntimeDomain: typeof toRuntimeDomain;
162
+ export {};
package/dist/bridge.js ADDED
@@ -0,0 +1,250 @@
1
+ import { assertJsonValue, DomainAuthoringError, } from "./types";
2
+ import { executeCommand } from "./session";
3
+ function runtimeEventFrom(definition) {
4
+ const name = definition.name ?? definition.eventPayloadName;
5
+ if (name === undefined || name.length === 0 || name.includes(":"))
6
+ throw new DomainAuthoringError("EVENT_NAME_INVALID", "Runtime event name is invalid");
7
+ const eventPayloadName = definition.eventPayloadName ?? name;
8
+ if (eventPayloadName.length === 0 || eventPayloadName.includes(":"))
9
+ throw new DomainAuthoringError("EVENT_NAME_INVALID", "Runtime event payload name is invalid");
10
+ const expectedEventType = eventPayloadName;
11
+ const eventType = definition.eventType ?? expectedEventType;
12
+ if (eventType !== expectedEventType)
13
+ throw new DomainAuthoringError("CANONICAL_EVENT_IDENTITY_INVALID", `Runtime event identity must be ${expectedEventType}`);
14
+ const parse = (payload) => {
15
+ const parsed = definition.parse === undefined
16
+ ? definition.create?.(payload)?.payload ?? definition.construct?.(payload)?.payload ?? payload
17
+ : definition.parse(payload);
18
+ return assertJsonValue(parsed, "event-construction");
19
+ };
20
+ const create = (payload) => Object.freeze({
21
+ eventName: name,
22
+ eventPayloadName,
23
+ eventType,
24
+ payload: parse(payload),
25
+ tags: Object.freeze([]),
26
+ });
27
+ return Object.freeze({
28
+ name,
29
+ eventName: name,
30
+ eventPayloadName: definition.eventPayloadName ?? name,
31
+ eventType,
32
+ create,
33
+ construct: create,
34
+ parse,
35
+ });
36
+ }
37
+ function runtimeProjectorFrom(definition, events) {
38
+ const eventTypes = Object.freeze([...definition.eventTypes]);
39
+ const eventNames = Object.freeze(eventTypes.map((eventType) => events.get(eventType)?.name ?? eventType));
40
+ const initial = typeof definition.initialState === "function"
41
+ ? definition.initialState()
42
+ : definition.initialState;
43
+ const apply = (state, event) => {
44
+ const registered = events.get(event.eventType);
45
+ if (registered === undefined || !eventTypes.includes(event.eventType)) {
46
+ throw new DomainAuthoringError("EVENT_TYPE_UNREGISTERED", `Runtime projector received ${event.eventType}`);
47
+ }
48
+ const syntheticTag = definition.tag.of("__runtime__");
49
+ const next = definition.apply(state, {
50
+ eventType: event.eventType,
51
+ eventName: registered.name,
52
+ payload: registered.parse(event.payload),
53
+ tags: [syntheticTag],
54
+ ordinal: "runtime",
55
+ });
56
+ return assertJsonValue(next, "state-persistence");
57
+ };
58
+ const serializeState = (state) => definition.serializeState(state);
59
+ const deserializeState = (serialized) => assertJsonValue(definition.deserializeState(serialized), "state-persistence");
60
+ return Object.freeze({
61
+ id: definition.id,
62
+ projectorId: definition.id,
63
+ version: definition.version,
64
+ projectorVersion: definition.version,
65
+ subscribedEventNames: eventNames,
66
+ subscribedEventTypes: eventTypes,
67
+ initialState: assertJsonValue(initial, "state-persistence"),
68
+ apply,
69
+ reduce: apply,
70
+ serializeState,
71
+ deserializeState,
72
+ });
73
+ }
74
+ function runtimeCandidateFrom(envelope) {
75
+ return Object.freeze({
76
+ kind: "candidate-envelope",
77
+ now: envelope.now,
78
+ events: Object.freeze(envelope.events.map((event) => Object.freeze({
79
+ ...event,
80
+ provenance: "g32",
81
+ }))),
82
+ tags: envelope.tags,
83
+ readClaims: envelope.readClaims,
84
+ decision: envelope.decision,
85
+ });
86
+ }
87
+ function candidateKey(candidate) {
88
+ return JSON.stringify({
89
+ now: candidate.now.toString(),
90
+ events: candidate.events.map((event) => ({
91
+ eventType: event.eventType,
92
+ eventName: event.eventName,
93
+ payload: event.payload,
94
+ tags: event.tags.map((tag) => tag.id),
95
+ ordinal: event.ordinal,
96
+ })),
97
+ tags: candidate.tags.map((tag) => tag.id),
98
+ readClaims: candidate.readClaims.map((claim) => ({ kind: claim.kind, projectorId: claim.projectorId, tag: claim.tag.id, head: claim.head })),
99
+ });
100
+ }
101
+ function freezeAllocation(allocation) {
102
+ if (allocation === undefined)
103
+ return undefined;
104
+ return Object.freeze({
105
+ ...allocation,
106
+ candidates: Object.freeze(allocation.candidates.map((candidate) => Object.freeze({ ...candidate }))),
107
+ });
108
+ }
109
+ function attemptContext(candidate, allocation, attemptId) {
110
+ const key = candidateKey(candidate);
111
+ return Object.freeze({
112
+ candidateKey: key,
113
+ attemptId: attemptId ?? allocation?.attemptId ?? key,
114
+ candidate,
115
+ ...(allocation === undefined ? {} : { allocation }),
116
+ });
117
+ }
118
+ function defaultRuntimeSnapshots(options) {
119
+ if (options.snapshots !== undefined)
120
+ return options.snapshots;
121
+ const states = options.state ?? {};
122
+ return {
123
+ read: (projector, tag) => ({
124
+ projectorId: projector.id,
125
+ tag,
126
+ head: null,
127
+ state: states[tag.id] ?? (typeof projector.initialState === "function" ? projector.initialState() : projector.initialState),
128
+ exists: states[tag.id] !== undefined,
129
+ }),
130
+ };
131
+ }
132
+ async function commitThroughRuntimePort(envelope, port) {
133
+ if (port === undefined)
134
+ return { kind: "accepted" };
135
+ const candidate = runtimeCandidateFrom(envelope);
136
+ const barrier = port.conflictBarrier === undefined ? { kind: "accepted" } : await port.conflictBarrier(candidate);
137
+ if (barrier.kind !== "accepted") {
138
+ return barrier.kind === "unknown" && port.reconcile !== undefined
139
+ ? await port.reconcile(attemptContext(candidate, undefined, barrier.attemptId), barrier)
140
+ : barrier;
141
+ }
142
+ const admitted = port.admit === undefined ? { kind: "accepted" } : await port.admit(candidate);
143
+ if (admitted.kind !== "accepted") {
144
+ return admitted.kind === "unknown" && port.reconcile !== undefined
145
+ ? await port.reconcile(attemptContext(candidate, undefined, admitted.attemptId), admitted)
146
+ : admitted;
147
+ }
148
+ const allocation = freezeAllocation(port.allocate === undefined ? undefined : await port.allocate(candidate));
149
+ const committed = port.commit === undefined
150
+ ? { kind: "accepted" }
151
+ : await port.commit(candidate, allocation);
152
+ return committed.kind === "unknown" && port.reconcile !== undefined
153
+ ? await port.reconcile(attemptContext(candidate, allocation, committed.attemptId ?? allocation?.attemptId ?? admitted.attemptId), committed)
154
+ : committed;
155
+ }
156
+ function runtimeEventsFrom(result) {
157
+ return result.envelope === undefined
158
+ ? Object.freeze([])
159
+ : Object.freeze(result.envelope.events.map((event) => Object.freeze({
160
+ ...event,
161
+ provenance: "g32",
162
+ })));
163
+ }
164
+ function runtimeOutcomeFrom(result) {
165
+ if (result.status === "accepted" && result.decision.kind === "done") {
166
+ return Object.freeze({
167
+ kind: "committed",
168
+ ...(result.decision.value === undefined ? {} : { value: result.decision.value }),
169
+ events: runtimeEventsFrom(result),
170
+ });
171
+ }
172
+ if (result.status === "discarded") {
173
+ const reason = result.decision.kind === "none" || result.decision.kind === "reject" ? result.decision.reason : undefined;
174
+ return Object.freeze({
175
+ kind: "noop",
176
+ ...(reason === undefined ? {} : { reason }),
177
+ events: Object.freeze([]),
178
+ });
179
+ }
180
+ if (result.status === "rejected") {
181
+ return Object.freeze({
182
+ kind: "rejected",
183
+ reason: result.decision.kind === "reject" ? result.decision.reason : "Command was rejected",
184
+ code: result.decision.kind === "reject" ? result.decision.code : "command_rejected",
185
+ events: Object.freeze([]),
186
+ });
187
+ }
188
+ return Object.freeze({
189
+ kind: "rejected",
190
+ reason: "Command outcome is unknown and requires durable reconciliation",
191
+ code: "unknown_outcome",
192
+ events: Object.freeze([]),
193
+ });
194
+ }
195
+ /** Adapt an authoring command into the existing runtime's three-outcome contract. */
196
+ export function adaptRuntimeCommand(command) {
197
+ const execute = async (value, options = {}) => {
198
+ const result = await executeCommand(command, value, {
199
+ timeProvider: { now: () => options.now ?? 0 },
200
+ snapshots: defaultRuntimeSnapshots(options),
201
+ commit: (envelope) => commitThroughRuntimePort(envelope, options.runtimePort),
202
+ });
203
+ return runtimeOutcomeFrom(result);
204
+ };
205
+ return Object.freeze({
206
+ id: command.id,
207
+ name: command.id,
208
+ parseInput: command.parseInput,
209
+ execute,
210
+ handle: execute,
211
+ });
212
+ }
213
+ export function toRuntimeDomain(domain) {
214
+ const rawEvents = domain.events ?? [];
215
+ const events = rawEvents.map(runtimeEventFrom);
216
+ const byType = new Map();
217
+ for (const event of events) {
218
+ if (byType.has(event.eventType))
219
+ throw new DomainAuthoringError("DUPLICATE_CANONICAL_EVENT_IDENTITY", `Duplicate event identity ${event.eventType}`);
220
+ byType.set(event.eventType, event);
221
+ }
222
+ const byName = new Map();
223
+ for (const event of events)
224
+ if (!byName.has(event.name))
225
+ byName.set(event.name, event);
226
+ const projectors = [];
227
+ for (const projectorValue of domain.projectors ?? []) {
228
+ if (typeof projectorValue === "object" && projectorValue !== null && "tag" in projectorValue) {
229
+ projectors.push(runtimeProjectorFrom(projectorValue, byType));
230
+ }
231
+ else {
232
+ projectors.push(projectorValue);
233
+ }
234
+ }
235
+ const commands = (domain.commands ?? []).map((command) => typeof command === "object" && command !== null && "reads" in command
236
+ ? adaptRuntimeCommand(command)
237
+ : command);
238
+ return Object.freeze({
239
+ events: Object.freeze(events),
240
+ commands: Object.freeze(commands),
241
+ projectors: Object.freeze(projectors),
242
+ views: Object.freeze([...(domain.views ?? [])]),
243
+ queries: Object.freeze([]),
244
+ materializedViews: Object.freeze([]),
245
+ eventByName: byName,
246
+ eventByType: byType,
247
+ __sekibanAuthoringBridge: true,
248
+ });
249
+ }
250
+ export const normalizeRuntimeDomain = toRuntimeDomain;
@@ -0,0 +1,53 @@
1
+ import type { z } from "zod";
2
+ import { type EventOf, type FixedNow, type JsonValue, type ReadClaimDeclaration, type ReadSet, type Reject, type RejectKind, type Tag, type TerminalDecision } from "./types.js";
3
+ import type { EventDefinition } from "./event.js";
4
+ import type { ProjectorDefinition, ProjectorState } from "./state.js";
5
+ export interface CommandContext {
6
+ readonly state: <State, Family extends string, Events extends readonly EventDefinition[]>(projector: ProjectorDefinition<State, Family, Events>, tag: Tag<Family>) => Promise<State>;
7
+ readonly exists: <Family extends string>(tag: Tag<Family>) => Promise<boolean>;
8
+ readonly now: () => FixedNow;
9
+ readonly append: <Event extends EventDefinition>(event: Event, payload: EventOf<Event>) => void;
10
+ }
11
+ export interface StagedEvent<Event extends EventDefinition = EventDefinition> {
12
+ readonly event: Event;
13
+ readonly eventType: string;
14
+ readonly payload: EventOf<Event>;
15
+ readonly tags: readonly Tag[];
16
+ readonly ordinal: string;
17
+ }
18
+ export declare function read<State, Family extends string, Events extends readonly EventDefinition[]>(projector: ProjectorDefinition<State, Family, Events>, tag: Tag<Family>): ReadSet;
19
+ export declare function readExists<Family extends string>(tag: Tag<Family>): ReadSet;
20
+ export declare const readTag: typeof readExists;
21
+ export declare function readSet(...declarations: readonly (ReadClaimDeclaration | ReadSet)[]): ReadSet;
22
+ export interface CommandDefinition<Id extends string = string, InputSchema extends z.ZodTypeAny = z.ZodTypeAny> {
23
+ readonly id: Id;
24
+ readonly input: InputSchema;
25
+ readonly parseInput: (value: unknown) => z.infer<InputSchema>;
26
+ readonly reads: (input: z.infer<InputSchema>) => ReadSet;
27
+ readonly handle: (input: z.infer<InputSchema>, context: CommandContext) => TerminalDecision | Promise<TerminalDecision>;
28
+ readonly execute: (input: unknown, context: CommandContext) => TerminalDecision | Promise<TerminalDecision>;
29
+ }
30
+ export interface CommandOptions<InputSchema extends z.ZodTypeAny> {
31
+ readonly id: string;
32
+ readonly input: InputSchema;
33
+ readonly reads: (input: z.infer<InputSchema>) => ReadSet;
34
+ readonly handle: (input: z.infer<InputSchema>, context: CommandContext) => TerminalDecision | Promise<TerminalDecision>;
35
+ }
36
+ export declare function command<const Id extends string, InputSchema extends z.ZodTypeAny>(options: CommandOptions<InputSchema> & {
37
+ readonly id: Id;
38
+ }): CommandDefinition<Id, InputSchema>;
39
+ export declare function done<Value extends JsonValue = JsonValue>(value?: Value): Extract<TerminalDecision, {
40
+ readonly kind: "done";
41
+ }>;
42
+ export declare function none(reason?: string): Extract<TerminalDecision, {
43
+ readonly kind: "none";
44
+ }>;
45
+ export declare function reject<Kind extends RejectKind, Details = unknown>(rejectKind: Kind, reason: string, details?: Details): Reject<Kind, Details>;
46
+ export declare const terminal: {
47
+ readonly done: typeof done;
48
+ readonly none: typeof none;
49
+ readonly reject: typeof reject;
50
+ };
51
+ export type CommandInput<Definition extends CommandDefinition> = Definition extends CommandDefinition<string, infer Schema> ? z.infer<Schema> : never;
52
+ export type CommandState<Definition extends CommandDefinition> = Definition extends CommandDefinition<string, z.ZodTypeAny> ? ProjectorState<ProjectorDefinition> : never;
53
+ export type CommandDecision = TerminalDecision;