@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.
Files changed (47) hide show
  1. package/LICENSE +157 -0
  2. package/dist/command-decoder.d.ts +5 -0
  3. package/dist/command-decoder.js +329 -0
  4. package/dist/constants.d.ts +15 -0
  5. package/dist/constants.js +15 -0
  6. package/dist/decoder-helpers.d.ts +58 -0
  7. package/dist/decoder-helpers.js +1757 -0
  8. package/dist/errors.d.ts +14 -0
  9. package/dist/errors.js +28 -0
  10. package/dist/event-decoder.d.ts +5 -0
  11. package/dist/event-decoder.js +232 -0
  12. package/dist/index.d.ts +10 -0
  13. package/dist/index.js +10 -0
  14. package/dist/lifecycle.d.ts +67 -0
  15. package/dist/lifecycle.js +271 -0
  16. package/dist/model.d.ts +1058 -0
  17. package/dist/model.js +1 -0
  18. package/dist/primitives.d.ts +59 -0
  19. package/dist/primitives.js +135 -0
  20. package/dist/projection.d.ts +8 -0
  21. package/dist/projection.js +875 -0
  22. package/dist/prompt-template.d.ts +12 -0
  23. package/dist/prompt-template.js +83 -0
  24. package/dist/protocol-validation.d.ts +36 -0
  25. package/dist/protocol-validation.js +1139 -0
  26. package/dist/result.d.ts +20 -0
  27. package/dist/result.js +21 -0
  28. package/dist/workflow-helpers.d.ts +31 -0
  29. package/dist/workflow-helpers.js +193 -0
  30. package/dist/workflow.d.ts +10 -0
  31. package/dist/workflow.js +594 -0
  32. package/package.json +30 -0
  33. package/src/command-decoder.ts +292 -0
  34. package/src/constants.ts +27 -0
  35. package/src/decoder-helpers.ts +1672 -0
  36. package/src/errors.ts +38 -0
  37. package/src/event-decoder.ts +225 -0
  38. package/src/index.ts +43 -0
  39. package/src/lifecycle.ts +403 -0
  40. package/src/model.ts +1233 -0
  41. package/src/primitives.ts +196 -0
  42. package/src/projection.ts +1081 -0
  43. package/src/prompt-template.ts +97 -0
  44. package/src/protocol-validation.ts +1252 -0
  45. package/src/result.ts +35 -0
  46. package/src/workflow-helpers.ts +239 -0
  47. package/src/workflow.ts +753 -0
@@ -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
+ }