@hunsu/protocol 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 +157 -0
- package/dist/command-decoder.d.ts +5 -0
- package/dist/command-decoder.js +329 -0
- package/dist/constants.d.ts +15 -0
- package/dist/constants.js +15 -0
- package/dist/decoder-helpers.d.ts +58 -0
- package/dist/decoder-helpers.js +1757 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +28 -0
- package/dist/event-decoder.d.ts +5 -0
- package/dist/event-decoder.js +232 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lifecycle.d.ts +67 -0
- package/dist/lifecycle.js +271 -0
- package/dist/model.d.ts +1058 -0
- package/dist/model.js +1 -0
- package/dist/primitives.d.ts +59 -0
- package/dist/primitives.js +135 -0
- package/dist/projection.d.ts +8 -0
- package/dist/projection.js +875 -0
- package/dist/prompt-template.d.ts +12 -0
- package/dist/prompt-template.js +83 -0
- package/dist/protocol-validation.d.ts +36 -0
- package/dist/protocol-validation.js +1139 -0
- package/dist/result.d.ts +20 -0
- package/dist/result.js +21 -0
- package/dist/workflow-helpers.d.ts +31 -0
- package/dist/workflow-helpers.js +193 -0
- package/dist/workflow.d.ts +10 -0
- package/dist/workflow.js +594 -0
- package/package.json +30 -0
- package/src/command-decoder.ts +292 -0
- package/src/constants.ts +27 -0
- package/src/decoder-helpers.ts +1672 -0
- package/src/errors.ts +38 -0
- package/src/event-decoder.ts +225 -0
- package/src/index.ts +43 -0
- package/src/lifecycle.ts +403 -0
- package/src/model.ts +1233 -0
- package/src/primitives.ts +196 -0
- package/src/projection.ts +1081 -0
- package/src/prompt-template.ts +97 -0
- package/src/protocol-validation.ts +1252 -0
- package/src/result.ts +35 -0
- package/src/workflow-helpers.ts +239 -0
- package/src/workflow.ts +753 -0
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccidentMoveRecord,
|
|
3
|
+
AbandonedLine,
|
|
4
|
+
ActiveLine,
|
|
5
|
+
ArrivedMoveRecord,
|
|
6
|
+
BlockedDestination,
|
|
7
|
+
CanceledDestination,
|
|
8
|
+
ClaimedDestination,
|
|
9
|
+
CompleteLine,
|
|
10
|
+
ConfirmedHunsuDraft,
|
|
11
|
+
Destination,
|
|
12
|
+
DestinationBase,
|
|
13
|
+
DestinationId,
|
|
14
|
+
DestinationPatch,
|
|
15
|
+
DestinationSeed,
|
|
16
|
+
DestinationSource,
|
|
17
|
+
DiscardedHunsuDraft,
|
|
18
|
+
DomainRole,
|
|
19
|
+
DraftHunsuDraft,
|
|
20
|
+
DraftSkillDraft,
|
|
21
|
+
FailedLine,
|
|
22
|
+
HunsuDraftBase,
|
|
23
|
+
HunsuDraftRecord,
|
|
24
|
+
HunsuId,
|
|
25
|
+
InProgressDestination,
|
|
26
|
+
LineId,
|
|
27
|
+
LineRecord,
|
|
28
|
+
MoveId,
|
|
29
|
+
MoveRecord,
|
|
30
|
+
MoveRecordBase,
|
|
31
|
+
PausedLine,
|
|
32
|
+
PlayableLine,
|
|
33
|
+
PendingDestination,
|
|
34
|
+
ReadyHunsuDraft,
|
|
35
|
+
ReachedDestination,
|
|
36
|
+
RequestId,
|
|
37
|
+
SkillBinding,
|
|
38
|
+
SkillDraftBase,
|
|
39
|
+
SkillDraftRecord,
|
|
40
|
+
SupersededDestination
|
|
41
|
+
} from "./model.ts";
|
|
42
|
+
import { makeFailureReason, makeNonEmptyArray, makeSingleItemArray } from "./primitives.ts";
|
|
43
|
+
import { err, ok, type Result } from "./result.ts";
|
|
44
|
+
|
|
45
|
+
export type DomainModelError = {
|
|
46
|
+
type: "DomainModelError";
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type DestinationTransitionMeta = {
|
|
51
|
+
updatedBy: DomainRole;
|
|
52
|
+
updatedAt?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ClaimDestinationInput = DestinationTransitionMeta & {
|
|
56
|
+
claimedBy: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type StartDestinationWorkInput = DestinationTransitionMeta & {
|
|
60
|
+
claimedBy: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type BlockDestinationInput = DestinationTransitionMeta & {
|
|
64
|
+
blockedReason: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ReachDestinationInput = DestinationTransitionMeta & {
|
|
68
|
+
reachedByMoveId: MoveId;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type CancelDestinationInput = DestinationTransitionMeta & {
|
|
72
|
+
canceledReason: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type SupersedeDestinationInput = DestinationTransitionMeta & {
|
|
76
|
+
supersededByDestinationId: DestinationId;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type UpdateDestinationDetailsInput = DestinationTransitionMeta & DestinationPatch;
|
|
80
|
+
|
|
81
|
+
export function createPendingDestination(
|
|
82
|
+
requestId: RequestId,
|
|
83
|
+
seed: DestinationSeed,
|
|
84
|
+
source: DestinationSource,
|
|
85
|
+
at?: string
|
|
86
|
+
): PendingDestination {
|
|
87
|
+
const actor: DomainRole = source === "initial-execute-team" || source === "initial-request" ? "SYSTEM" : "DIRECTOR";
|
|
88
|
+
return {
|
|
89
|
+
...seed,
|
|
90
|
+
requestId,
|
|
91
|
+
status: "pending",
|
|
92
|
+
source,
|
|
93
|
+
createdBy: actor,
|
|
94
|
+
updatedBy: actor,
|
|
95
|
+
createdAt: at,
|
|
96
|
+
updatedAt: at
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function makePendingDestination(input: Omit<PendingDestination, "status">): Result<PendingDestination, DomainModelError> {
|
|
101
|
+
return ok({ ...input, status: "pending" });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function claimDestination(destination: Destination, input: ClaimDestinationInput): Result<ClaimedDestination, DomainModelError> {
|
|
105
|
+
if (!hasText(input.claimedBy)) {
|
|
106
|
+
return invalid("Claim Destination requires claimedBy");
|
|
107
|
+
}
|
|
108
|
+
if (destination.status !== "pending" && destination.status !== "blocked") {
|
|
109
|
+
return invalid(`Destination ${destination.id} cannot be claimed from status ${destination.status}`);
|
|
110
|
+
}
|
|
111
|
+
return ok({
|
|
112
|
+
...baseDestinationForTransition(destination, input),
|
|
113
|
+
status: "claimed",
|
|
114
|
+
claimedBy: input.claimedBy
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function startDestinationWork(destination: Destination, input: StartDestinationWorkInput): Result<InProgressDestination, DomainModelError> {
|
|
119
|
+
if (!hasText(input.claimedBy)) {
|
|
120
|
+
return invalid("Start Destination work requires claimedBy");
|
|
121
|
+
}
|
|
122
|
+
if (destination.status === "blocked") {
|
|
123
|
+
return invalid(`Destination ${destination.id} cannot start work from status blocked`);
|
|
124
|
+
}
|
|
125
|
+
if (isClosedDestination(destination)) {
|
|
126
|
+
return invalid(`Destination ${destination.id} cannot start work from status ${destination.status}`);
|
|
127
|
+
}
|
|
128
|
+
return ok({
|
|
129
|
+
...baseDestinationForTransition(destination, input),
|
|
130
|
+
status: "in_progress",
|
|
131
|
+
claimedBy: input.claimedBy
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function blockDestination(destination: Destination, input: BlockDestinationInput): Result<BlockedDestination, DomainModelError> {
|
|
136
|
+
if (!hasText(input.blockedReason)) {
|
|
137
|
+
return invalid("Block Destination requires blockedReason");
|
|
138
|
+
}
|
|
139
|
+
if (isClosedDestination(destination)) {
|
|
140
|
+
return invalid(`Destination ${destination.id} cannot be blocked from status ${destination.status}`);
|
|
141
|
+
}
|
|
142
|
+
return ok({
|
|
143
|
+
...baseDestinationForTransition(destination, input),
|
|
144
|
+
status: "blocked",
|
|
145
|
+
claimedBy: "claimedBy" in destination ? destination.claimedBy : undefined,
|
|
146
|
+
blockedReason: input.blockedReason
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function unblockDestination(destination: Destination, input: DestinationTransitionMeta): Result<PendingDestination, DomainModelError> {
|
|
151
|
+
if (destination.status !== "blocked") {
|
|
152
|
+
return invalid(`Destination ${destination.id} cannot be unblocked from status ${destination.status}`);
|
|
153
|
+
}
|
|
154
|
+
return ok({
|
|
155
|
+
...baseDestinationForTransition(destination, input),
|
|
156
|
+
status: "pending"
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function reachDestination(destination: Destination, input: ReachDestinationInput): Result<ReachedDestination, DomainModelError> {
|
|
161
|
+
if (!hasText(input.reachedByMoveId)) {
|
|
162
|
+
return invalid("Reach Destination requires reachedByMoveId");
|
|
163
|
+
}
|
|
164
|
+
if (isClosedDestination(destination)) {
|
|
165
|
+
return invalid(`Destination ${destination.id} cannot be reached from status ${destination.status}`);
|
|
166
|
+
}
|
|
167
|
+
return ok({
|
|
168
|
+
...baseDestinationForTransition(destination, input),
|
|
169
|
+
status: "reached",
|
|
170
|
+
claimedBy: "claimedBy" in destination ? destination.claimedBy : undefined,
|
|
171
|
+
reachedByMoveId: input.reachedByMoveId
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function cancelDestination(destination: Destination, input: CancelDestinationInput): Result<CanceledDestination, DomainModelError> {
|
|
176
|
+
if (!hasText(input.canceledReason)) {
|
|
177
|
+
return invalid("Cancel Destination requires canceledReason");
|
|
178
|
+
}
|
|
179
|
+
if (isClosedDestination(destination)) {
|
|
180
|
+
return invalid(`Destination ${destination.id} cannot be canceled from status ${destination.status}`);
|
|
181
|
+
}
|
|
182
|
+
return ok({
|
|
183
|
+
...baseDestinationForTransition(destination, input),
|
|
184
|
+
status: "canceled",
|
|
185
|
+
claimedBy: "claimedBy" in destination ? destination.claimedBy : undefined,
|
|
186
|
+
canceledReason: input.canceledReason
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function supersedeDestination(destination: Destination, input: SupersedeDestinationInput): Result<SupersededDestination, DomainModelError> {
|
|
191
|
+
if (!hasText(input.supersededByDestinationId)) {
|
|
192
|
+
return invalid("Supersede Destination requires supersededByDestinationId");
|
|
193
|
+
}
|
|
194
|
+
if (isClosedDestination(destination)) {
|
|
195
|
+
return invalid(`Destination ${destination.id} cannot be superseded from status ${destination.status}`);
|
|
196
|
+
}
|
|
197
|
+
return ok({
|
|
198
|
+
...baseDestinationForTransition(destination, input),
|
|
199
|
+
status: "superseded",
|
|
200
|
+
claimedBy: "claimedBy" in destination ? destination.claimedBy : undefined,
|
|
201
|
+
supersededByDestinationId: input.supersededByDestinationId
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function updateDestinationDetails(destination: Destination, input: UpdateDestinationDetailsInput): Result<Destination, DomainModelError> {
|
|
206
|
+
const { updatedBy, updatedAt, ...patch } = input;
|
|
207
|
+
return ok({
|
|
208
|
+
...destination,
|
|
209
|
+
...patch,
|
|
210
|
+
updatedBy,
|
|
211
|
+
updatedAt
|
|
212
|
+
} as Destination);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function pauseLine(line: LineRecord): Result<PausedLine, DomainModelError> {
|
|
216
|
+
return line.status === "active"
|
|
217
|
+
? ok({ ...line, status: "paused" })
|
|
218
|
+
: invalid(`Line ${line.id} cannot be paused from status ${line.status}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resumeLine(line: LineRecord): Result<ActiveLine, DomainModelError> {
|
|
222
|
+
return line.status === "paused"
|
|
223
|
+
? ok({ ...line, status: "active" })
|
|
224
|
+
: invalid(`Line ${line.id} cannot be resumed from status ${line.status}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function completeLine(line: LineRecord): Result<CompleteLine, DomainModelError> {
|
|
228
|
+
return line.status === "active" || line.status === "paused"
|
|
229
|
+
? ok({ ...line, status: "complete" })
|
|
230
|
+
: invalid(`Line ${line.id} cannot be completed from status ${line.status}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function abandonLine(line: LineRecord): Result<AbandonedLine, DomainModelError> {
|
|
234
|
+
return line.status === "active" || line.status === "paused"
|
|
235
|
+
? ok({ ...line, status: "abandoned" })
|
|
236
|
+
: invalid(`Line ${line.id} cannot be abandoned from status ${line.status}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function failLine(line: LineRecord): Result<FailedLine, DomainModelError> {
|
|
240
|
+
return line.status === "active"
|
|
241
|
+
? ok({ ...line, status: "failed" })
|
|
242
|
+
: invalid(`Line ${line.id} cannot fail from status ${line.status}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function makePlayableLine(line: LineRecord): Result<PlayableLine, DomainModelError> {
|
|
246
|
+
return line.status === "active"
|
|
247
|
+
? ok(line)
|
|
248
|
+
: invalid(`TEAM line ${line.id} cannot continue from status ${line.status}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function makeArrivedMoveRecord(input: MoveRecordBase & { reachedDestinationIds: DestinationId[] }): Result<ArrivedMoveRecord, DomainModelError> {
|
|
252
|
+
const reached = makeSingleItemArray(input.reachedDestinationIds, "reachedDestinationIds");
|
|
253
|
+
if (!reached.ok) {
|
|
254
|
+
return invalid("Arrived MOVE must reach exactly one Destination");
|
|
255
|
+
}
|
|
256
|
+
const evidence = makeNonEmptyArray(input.evidence, "evidence");
|
|
257
|
+
if (!evidence.ok) {
|
|
258
|
+
return invalid("Arrived MOVE requires evidence");
|
|
259
|
+
}
|
|
260
|
+
return ok({
|
|
261
|
+
...input,
|
|
262
|
+
outcome: "arrived",
|
|
263
|
+
reachedDestinationIds: reached.value,
|
|
264
|
+
evidence: evidence.value
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function makeAccidentMoveRecord(input: MoveRecordBase & { failureReason: string }): Result<AccidentMoveRecord, DomainModelError> {
|
|
269
|
+
const failureReason = makeFailureReason(input.failureReason, "failureReason");
|
|
270
|
+
if (!failureReason.ok) {
|
|
271
|
+
return invalid("Accident MOVE requires failureReason");
|
|
272
|
+
}
|
|
273
|
+
const evidence = makeNonEmptyArray(input.evidence, "evidence");
|
|
274
|
+
if (!evidence.ok) {
|
|
275
|
+
return invalid("Accident MOVE requires evidence");
|
|
276
|
+
}
|
|
277
|
+
return ok({
|
|
278
|
+
...input,
|
|
279
|
+
outcome: "accident",
|
|
280
|
+
reachedDestinationIds: [],
|
|
281
|
+
failureReason: failureReason.value,
|
|
282
|
+
evidence: evidence.value
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export type MoveRecordBasePatch = Partial<Pick<MoveRecordBase, "fromNodeId" | "toNodeId" | "teamName" | "ordinal" | "snapshot" | "commit">>;
|
|
287
|
+
|
|
288
|
+
export function patchMoveRecordBase(move: MoveRecord, patch: MoveRecordBasePatch): MoveRecord {
|
|
289
|
+
if (move.outcome === "accident") {
|
|
290
|
+
return {
|
|
291
|
+
...move,
|
|
292
|
+
...patch,
|
|
293
|
+
outcome: "accident",
|
|
294
|
+
reachedDestinationIds: [],
|
|
295
|
+
failureReason: move.failureReason
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
...move,
|
|
300
|
+
...patch,
|
|
301
|
+
outcome: "arrived",
|
|
302
|
+
reachedDestinationIds: move.reachedDestinationIds
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function normalizeMoveRecord(move: MoveRecord): Result<MoveRecord, DomainModelError> {
|
|
307
|
+
if (move.outcome === "accident") {
|
|
308
|
+
return makeAccidentMoveRecord(move);
|
|
309
|
+
}
|
|
310
|
+
return makeArrivedMoveRecord({
|
|
311
|
+
...move,
|
|
312
|
+
reachedDestinationIds: move.reachedDestinationIds
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function makeDraftSkillDraft(input: Omit<SkillDraftBase, "status">): DraftSkillDraft {
|
|
317
|
+
return { ...input, status: "draft" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function makeDraftHunsuDraft(input: Omit<HunsuDraftBase, "status">): DraftHunsuDraft {
|
|
321
|
+
return { ...input, status: "draft" };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function makeReadyHunsuDraft(input: Omit<ReadyHunsuDraft, "status">): ReadyHunsuDraft {
|
|
325
|
+
return { ...input, status: "ready" };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function confirmHunsuDraftRecord(
|
|
329
|
+
draft: HunsuDraftRecord,
|
|
330
|
+
input: { hunsuId?: HunsuId; newLineId?: LineId; updatedAt?: string } = {}
|
|
331
|
+
): Result<ConfirmedHunsuDraft, DomainModelError> {
|
|
332
|
+
if (draft.status !== "ready") {
|
|
333
|
+
return invalid(`HUNSU Draft ${draft.id} cannot be confirmed from status ${draft.status}`);
|
|
334
|
+
}
|
|
335
|
+
return ok({
|
|
336
|
+
...draft,
|
|
337
|
+
status: "confirmed",
|
|
338
|
+
hunsuId: input.hunsuId ?? draft.hunsuId,
|
|
339
|
+
newLineId: input.newLineId ?? draft.newLineId,
|
|
340
|
+
updatedAt: input.updatedAt ?? draft.updatedAt
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function discardHunsuDraftRecord(
|
|
345
|
+
draft: HunsuDraftRecord,
|
|
346
|
+
input: { updatedAt?: string } = {}
|
|
347
|
+
): Result<DiscardedHunsuDraft, DomainModelError> {
|
|
348
|
+
if (draft.status !== "draft" && draft.status !== "ready") {
|
|
349
|
+
return invalid(`HUNSU Draft ${draft.id} cannot be discarded from status ${draft.status}`);
|
|
350
|
+
}
|
|
351
|
+
const {
|
|
352
|
+
hunsuId: _hunsuId,
|
|
353
|
+
newLineId: _newLineId,
|
|
354
|
+
...base
|
|
355
|
+
} = draft;
|
|
356
|
+
return ok({
|
|
357
|
+
...base,
|
|
358
|
+
status: "discarded",
|
|
359
|
+
updatedAt: input.updatedAt ?? draft.updatedAt
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function acceptSkillDraft(draft: SkillDraftRecord, acceptedSnapshot: SkillBinding): Result<SkillDraftRecord, DomainModelError> {
|
|
364
|
+
if (draft.status !== "draft") {
|
|
365
|
+
return invalid(`Skill Draft ${draft.id} cannot be accepted from status ${draft.status}`);
|
|
366
|
+
}
|
|
367
|
+
return ok({ ...draft, status: "accepted", acceptedSnapshot });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function discardSkillDraft(draft: SkillDraftRecord): Result<SkillDraftRecord, DomainModelError> {
|
|
371
|
+
if (draft.status !== "draft") {
|
|
372
|
+
return invalid(`Skill Draft ${draft.id} cannot be discarded from status ${draft.status}`);
|
|
373
|
+
}
|
|
374
|
+
return ok({ ...draft, status: "discarded" });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function baseDestinationForTransition(destination: Destination, input: DestinationTransitionMeta): DestinationBase {
|
|
378
|
+
const {
|
|
379
|
+
claimedBy: _claimedBy,
|
|
380
|
+
reachedByMoveId: _reachedByMoveId,
|
|
381
|
+
blockedReason: _blockedReason,
|
|
382
|
+
canceledReason: _canceledReason,
|
|
383
|
+
supersededByDestinationId: _supersededByDestinationId,
|
|
384
|
+
...base
|
|
385
|
+
} = destination;
|
|
386
|
+
return {
|
|
387
|
+
...base,
|
|
388
|
+
updatedBy: input.updatedBy,
|
|
389
|
+
updatedAt: input.updatedAt
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isClosedDestination(destination: Destination): boolean {
|
|
394
|
+
return destination.status === "reached" || destination.status === "canceled" || destination.status === "superseded";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function hasText(value: unknown): value is string {
|
|
398
|
+
return typeof value === "string" && value.trim() !== "";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function invalid(message: string): Result<never, DomainModelError> {
|
|
402
|
+
return err({ type: "DomainModelError", message });
|
|
403
|
+
}
|