@agenticprimitives/coordination 0.0.0-alpha.2

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/core/commands.d.ts +179 -0
  4. package/dist/core/commands.d.ts.map +1 -0
  5. package/dist/core/commands.js +26 -0
  6. package/dist/core/commands.js.map +1 -0
  7. package/dist/core/events.d.ts +163 -0
  8. package/dist/core/events.d.ts.map +1 -0
  9. package/dist/core/events.js +2 -0
  10. package/dist/core/events.js.map +1 -0
  11. package/dist/core/ids.d.ts +31 -0
  12. package/dist/core/ids.d.ts.map +1 -0
  13. package/dist/core/ids.js +36 -0
  14. package/dist/core/ids.js.map +1 -0
  15. package/dist/core/index.d.ts +12 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +6 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/core/provenance.d.ts +29 -0
  20. package/dist/core/provenance.d.ts.map +1 -0
  21. package/dist/core/provenance.js +175 -0
  22. package/dist/core/provenance.js.map +1 -0
  23. package/dist/core/reducer.d.ts +50 -0
  24. package/dist/core/reducer.d.ts.map +1 -0
  25. package/dist/core/reducer.js +1164 -0
  26. package/dist/core/reducer.js.map +1 -0
  27. package/dist/core/types.d.ts +106 -0
  28. package/dist/core/types.d.ts.map +1 -0
  29. package/dist/core/types.js +22 -0
  30. package/dist/core/types.js.map +1 -0
  31. package/dist/decisions/index.d.ts +17 -0
  32. package/dist/decisions/index.d.ts.map +1 -0
  33. package/dist/decisions/index.js +6 -0
  34. package/dist/decisions/index.js.map +1 -0
  35. package/dist/decisions/triage.d.ts +30 -0
  36. package/dist/decisions/triage.d.ts.map +1 -0
  37. package/dist/decisions/triage.js +66 -0
  38. package/dist/decisions/triage.js.map +1 -0
  39. package/dist/decisions/types.d.ts +39 -0
  40. package/dist/decisions/types.d.ts.map +1 -0
  41. package/dist/decisions/types.js +2 -0
  42. package/dist/decisions/types.js.map +1 -0
  43. package/dist/execution/compile.d.ts +60 -0
  44. package/dist/execution/compile.d.ts.map +1 -0
  45. package/dist/execution/compile.js +83 -0
  46. package/dist/execution/compile.js.map +1 -0
  47. package/dist/execution/index.d.ts +3 -0
  48. package/dist/execution/index.d.ts.map +1 -0
  49. package/dist/execution/index.js +2 -0
  50. package/dist/execution/index.js.map +1 -0
  51. package/dist/index.d.ts +13 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +10 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/participation/index.d.ts +2 -0
  56. package/dist/participation/index.d.ts.map +1 -0
  57. package/dist/participation/index.js +2 -0
  58. package/dist/participation/index.js.map +1 -0
  59. package/dist/participation/types.d.ts +66 -0
  60. package/dist/participation/types.d.ts.map +1 -0
  61. package/dist/participation/types.js +2 -0
  62. package/dist/participation/types.js.map +1 -0
  63. package/dist/planning/hash.d.ts +8 -0
  64. package/dist/planning/hash.d.ts.map +1 -0
  65. package/dist/planning/hash.js +20 -0
  66. package/dist/planning/hash.js.map +1 -0
  67. package/dist/planning/index.d.ts +5 -0
  68. package/dist/planning/index.d.ts.map +1 -0
  69. package/dist/planning/index.js +3 -0
  70. package/dist/planning/index.js.map +1 -0
  71. package/dist/planning/types.d.ts +75 -0
  72. package/dist/planning/types.d.ts.map +1 -0
  73. package/dist/planning/types.js +2 -0
  74. package/dist/planning/types.js.map +1 -0
  75. package/dist/planning/validate.d.ts +13 -0
  76. package/dist/planning/validate.d.ts.map +1 -0
  77. package/dist/planning/validate.js +55 -0
  78. package/dist/planning/validate.js.map +1 -0
  79. package/dist/profiles/index.d.ts +11 -0
  80. package/dist/profiles/index.d.ts.map +1 -0
  81. package/dist/profiles/index.js +3 -0
  82. package/dist/profiles/index.js.map +1 -0
  83. package/dist/projections/index.d.ts +8 -0
  84. package/dist/projections/index.d.ts.map +1 -0
  85. package/dist/projections/index.js +3 -0
  86. package/dist/projections/index.js.map +1 -0
  87. package/package.json +78 -0
  88. package/spec.md +1 -0
@@ -0,0 +1,1164 @@
1
+ // Event-sourced coordination machine (spec 332 §6). The reducer is the ONLY state-transition
2
+ // path (ADR-0013); command validators reject before append; no event encodes authority (§9).
3
+ // Deterministic: ids and instants come from commands/events, never from this module.
4
+ import { COORDINATION_COMMAND_KINDS } from './commands.js';
5
+ import { planContentHash } from '../planning/hash.js';
6
+ import { validatePlanContent } from '../planning/validate.js';
7
+ export function initialCoordinationState() {
8
+ return {
9
+ revision: 0,
10
+ plans: {},
11
+ participations: {},
12
+ proposals: {},
13
+ allocations: {},
14
+ commitments: {},
15
+ milestones: {},
16
+ satisfiedSteps: {},
17
+ recommendations: {},
18
+ };
19
+ }
20
+ export class CoordinationEventError extends Error {
21
+ constructor(message) {
22
+ super(`[coordination] ${message}`);
23
+ }
24
+ }
25
+ export const planKey = (planId, revision) => `${planId}@${revision}`;
26
+ const TERMINAL_LIFECYCLES = new Set(['satisfied', 'abandoned']);
27
+ const eventId = (revision, kind) => `evt_${String(revision).padStart(4, '0')}_${kind}`;
28
+ const reject = (reason) => ({ ok: false, reason });
29
+ function activeRoles(state, address) {
30
+ const roles = new Set();
31
+ for (const p of Object.values(state.participations)) {
32
+ if (p.participant === address && p.status === 'active')
33
+ roles.add(p.role);
34
+ }
35
+ return roles;
36
+ }
37
+ function participationStatus(state, address) {
38
+ let invited = false;
39
+ for (const p of Object.values(state.participations)) {
40
+ if (p.participant !== address)
41
+ continue;
42
+ if (p.status === 'active')
43
+ return 'active';
44
+ if (p.status === 'invited')
45
+ invited = true;
46
+ }
47
+ return invited ? 'invited' : 'none';
48
+ }
49
+ function isSteward(state, address) {
50
+ const roles = activeRoles(state, address);
51
+ return roles.has('coordinator') || roles.has('sponsor');
52
+ }
53
+ function endeavorContext(state, endeavorId) {
54
+ if (!state.endeavor)
55
+ return { reason: 'no endeavor adopted' };
56
+ if (state.endeavor.endeavorId !== endeavorId)
57
+ return { reason: `unknown endeavor ${endeavorId}` };
58
+ if (TERMINAL_LIFECYCLES.has(state.endeavor.lifecycle)) {
59
+ return { reason: `endeavor is terminal (${state.endeavor.lifecycle})` };
60
+ }
61
+ return { endeavor: state.endeavor };
62
+ }
63
+ function adoptedPlan(state) {
64
+ const ref = state.endeavor?.adoptedPlanRef;
65
+ if (!ref)
66
+ return undefined;
67
+ return state.plans[planKey(ref.planId, ref.revision)];
68
+ }
69
+ function samePlanRef(a, b) {
70
+ return a.planId === b.planId && a.revision === b.revision && a.hash === b.hash;
71
+ }
72
+ export function validateCoordinationCommand(state, command) {
73
+ const nextRevision = state.revision + 1;
74
+ const at = command.issuedAt;
75
+ switch (command.kind) {
76
+ case 'SubmitEndeavorRequest': {
77
+ if (state.request)
78
+ return reject('a request was already submitted');
79
+ if (state.endeavor)
80
+ return reject('an endeavor already exists');
81
+ if (command.goal.trim().length === 0)
82
+ return reject('goal must be non-empty');
83
+ const request = {
84
+ schema: 'apcoord/EndeavorRequestV1',
85
+ requestId: command.requestId,
86
+ requester: command.actor,
87
+ targetPrincipal: command.targetPrincipal,
88
+ goal: command.goal,
89
+ entryPoint: command.entryPoint,
90
+ submittedAt: at,
91
+ ...(command.intentRef ? { intentRef: command.intentRef } : {}),
92
+ ...(command.intakeContext ? { intakeContext: command.intakeContext } : {}),
93
+ ...(command.profileRef ? { profileRef: command.profileRef } : {}),
94
+ };
95
+ return {
96
+ ok: true,
97
+ events: [
98
+ {
99
+ kind: 'EndeavorRequestSubmitted',
100
+ eventId: eventId(nextRevision, 'EndeavorRequestSubmitted'),
101
+ requestId: command.requestId,
102
+ request,
103
+ occurredAt: at,
104
+ revision: nextRevision,
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ case 'AdoptEndeavor': {
110
+ if (!state.request)
111
+ return reject('no request to adopt');
112
+ if (state.request.record.requestId !== command.requestId)
113
+ return reject('request id mismatch');
114
+ if (state.request.status !== 'submitted')
115
+ return reject(`request is ${state.request.status}, not submitted`);
116
+ if (command.actor !== state.request.record.targetPrincipal) {
117
+ return reject('only the target principal may adopt');
118
+ }
119
+ if (command.initialParticipations.length === 0) {
120
+ return reject('adoption must assert at least one sponsor/coordinator participation');
121
+ }
122
+ const seen = new Set();
123
+ for (const p of command.initialParticipations) {
124
+ if (p.role !== 'sponsor' && p.role !== 'coordinator') {
125
+ return reject(`initial participation role must be sponsor or coordinator, got ${p.role}`);
126
+ }
127
+ if (seen.has(p.participationId))
128
+ return reject(`duplicate participation id ${p.participationId}`);
129
+ seen.add(p.participationId);
130
+ }
131
+ if (command.outcome.criteria.length === 0)
132
+ return reject('outcome must declare at least one criterion');
133
+ const outcome = {
134
+ schema: 'apcoord/OutcomeSpecificationV1',
135
+ outcomeId: command.outcome.outcomeId,
136
+ endeavorId: command.endeavorId,
137
+ revision: 1,
138
+ criteria: command.outcome.criteria,
139
+ };
140
+ const events = [
141
+ {
142
+ kind: 'EndeavorAdopted',
143
+ eventId: eventId(nextRevision, 'EndeavorAdopted'),
144
+ endeavorId: command.endeavorId,
145
+ requestId: command.requestId,
146
+ managingPrincipal: command.actor,
147
+ situationId: command.situationId,
148
+ title: command.title,
149
+ outcome,
150
+ occurredAt: at,
151
+ revision: nextRevision,
152
+ },
153
+ ];
154
+ let revision = nextRevision;
155
+ for (const p of command.initialParticipations) {
156
+ revision += 1;
157
+ events.push({
158
+ kind: 'ParticipationAsserted',
159
+ eventId: eventId(revision, 'ParticipationAsserted'),
160
+ endeavorId: command.endeavorId,
161
+ participationId: p.participationId,
162
+ participant: p.participant,
163
+ role: p.role,
164
+ assertedBy: 'self-sponsor',
165
+ occurredAt: at,
166
+ revision,
167
+ });
168
+ }
169
+ return { ok: true, events };
170
+ }
171
+ case 'DeclineEndeavorRequest': {
172
+ if (!state.request)
173
+ return reject('no request to decline');
174
+ if (state.request.record.requestId !== command.requestId)
175
+ return reject('request id mismatch');
176
+ if (state.request.status !== 'submitted')
177
+ return reject(`request is ${state.request.status}, not submitted`);
178
+ if (command.actor !== state.request.record.targetPrincipal) {
179
+ return reject('only the target principal may decline');
180
+ }
181
+ return {
182
+ ok: true,
183
+ events: [
184
+ {
185
+ kind: 'EndeavorRequestDeclined',
186
+ eventId: eventId(nextRevision, 'EndeavorRequestDeclined'),
187
+ requestId: command.requestId,
188
+ declinedBy: command.actor,
189
+ reason: command.reason,
190
+ occurredAt: at,
191
+ revision: nextRevision,
192
+ },
193
+ ],
194
+ };
195
+ }
196
+ case 'ReviseOutcome': {
197
+ const ctx = endeavorContext(state, command.endeavorId);
198
+ if ('reason' in ctx)
199
+ return reject(ctx.reason);
200
+ if (command.actor !== ctx.endeavor.managingPrincipal && !isSteward(state, command.actor)) {
201
+ return reject('only the managing principal or a sponsor/coordinator may revise the outcome');
202
+ }
203
+ if (command.outcome.criteria.length === 0)
204
+ return reject('outcome must declare at least one criterion');
205
+ const outcome = {
206
+ schema: 'apcoord/OutcomeSpecificationV1',
207
+ outcomeId: command.outcome.outcomeId,
208
+ endeavorId: command.endeavorId,
209
+ revision: (state.outcome?.revision ?? 0) + 1,
210
+ criteria: command.outcome.criteria,
211
+ };
212
+ return {
213
+ ok: true,
214
+ events: [
215
+ {
216
+ kind: 'OutcomeRevised',
217
+ eventId: eventId(nextRevision, 'OutcomeRevised'),
218
+ endeavorId: command.endeavorId,
219
+ outcome,
220
+ occurredAt: at,
221
+ revision: nextRevision,
222
+ },
223
+ ],
224
+ };
225
+ }
226
+ case 'ProposePlan': {
227
+ const ctx = endeavorContext(state, command.endeavorId);
228
+ if ('reason' in ctx)
229
+ return reject(ctx.reason);
230
+ if (command.actor !== ctx.endeavor.managingPrincipal &&
231
+ participationStatus(state, command.actor) !== 'active') {
232
+ return reject('plan proposer must be the managing principal or an active participant');
233
+ }
234
+ const content = { steps: command.steps, edges: command.edges, milestones: command.milestones };
235
+ const structural = validatePlanContent(content);
236
+ if (!structural.ok)
237
+ return reject(structural.reason);
238
+ if (state.plans[planKey(command.planId, command.revision)]) {
239
+ return reject(`plan revision ${planKey(command.planId, command.revision)} already exists`);
240
+ }
241
+ let maxRevision = 0;
242
+ for (const p of Object.values(state.plans)) {
243
+ if (p.planId === command.planId && p.revision > maxRevision)
244
+ maxRevision = p.revision;
245
+ }
246
+ if (command.revision !== maxRevision + 1) {
247
+ return reject(`plan revision must be ${maxRevision + 1}, got ${command.revision}`);
248
+ }
249
+ return {
250
+ ok: true,
251
+ events: [
252
+ {
253
+ kind: 'PlanProposed',
254
+ eventId: eventId(nextRevision, 'PlanProposed'),
255
+ endeavorId: command.endeavorId,
256
+ planId: command.planId,
257
+ planRevision: command.revision,
258
+ contentHash: planContentHash(content),
259
+ steps: command.steps,
260
+ edges: command.edges,
261
+ milestones: command.milestones,
262
+ proposedBy: command.actor,
263
+ occurredAt: at,
264
+ revision: nextRevision,
265
+ },
266
+ ],
267
+ };
268
+ }
269
+ case 'AdoptPlan': {
270
+ const ctx = endeavorContext(state, command.endeavorId);
271
+ if ('reason' in ctx)
272
+ return reject(ctx.reason);
273
+ if (!isSteward(state, command.actor))
274
+ return reject('only a sponsor/coordinator may adopt a plan');
275
+ const plan = state.plans[planKey(command.planRef.planId, command.planRef.revision)];
276
+ if (!plan)
277
+ return reject(`unknown plan revision ${planKey(command.planRef.planId, command.planRef.revision)}`);
278
+ if (plan.status !== 'proposed')
279
+ return reject(`plan revision is ${plan.status}, not proposed`);
280
+ if (command.planRef.hash !== plan.contentHash) {
281
+ return reject('plan content hash mismatch — adoption binds an exact revision hash');
282
+ }
283
+ const prior = ctx.endeavor.adoptedPlanRef;
284
+ return {
285
+ ok: true,
286
+ events: [
287
+ {
288
+ kind: 'PlanAdopted',
289
+ eventId: eventId(nextRevision, 'PlanAdopted'),
290
+ endeavorId: command.endeavorId,
291
+ planRef: command.planRef,
292
+ adoptedBy: command.actor,
293
+ ...(prior ? { supersededPlanRef: prior } : {}),
294
+ occurredAt: at,
295
+ revision: nextRevision,
296
+ },
297
+ ],
298
+ };
299
+ }
300
+ case 'RejectPlan': {
301
+ const ctx = endeavorContext(state, command.endeavorId);
302
+ if ('reason' in ctx)
303
+ return reject(ctx.reason);
304
+ if (!isSteward(state, command.actor))
305
+ return reject('only a sponsor/coordinator may reject a plan');
306
+ const plan = state.plans[planKey(command.planId, command.revision)];
307
+ if (!plan)
308
+ return reject(`unknown plan revision ${planKey(command.planId, command.revision)}`);
309
+ if (plan.status !== 'proposed')
310
+ return reject(`plan revision is ${plan.status}, not proposed`);
311
+ return {
312
+ ok: true,
313
+ events: [
314
+ {
315
+ kind: 'PlanRejected',
316
+ eventId: eventId(nextRevision, 'PlanRejected'),
317
+ endeavorId: command.endeavorId,
318
+ planId: command.planId,
319
+ planRevision: command.revision,
320
+ rejectedBy: command.actor,
321
+ ...(command.reason ? { reason: command.reason } : {}),
322
+ occurredAt: at,
323
+ revision: nextRevision,
324
+ },
325
+ ],
326
+ };
327
+ }
328
+ case 'InviteParticipant': {
329
+ const ctx = endeavorContext(state, command.endeavorId);
330
+ if ('reason' in ctx)
331
+ return reject(ctx.reason);
332
+ if (!isSteward(state, command.actor))
333
+ return reject('only a sponsor/coordinator may invite');
334
+ if (state.participations[command.participationId]) {
335
+ return reject(`participation ${command.participationId} already exists`);
336
+ }
337
+ return {
338
+ ok: true,
339
+ events: [
340
+ {
341
+ kind: 'ParticipationInvited',
342
+ eventId: eventId(nextRevision, 'ParticipationInvited'),
343
+ endeavorId: command.endeavorId,
344
+ participationId: command.participationId,
345
+ participant: command.participant,
346
+ role: command.role,
347
+ invitedBy: command.actor,
348
+ ...(command.scope ? { scope: command.scope } : {}),
349
+ occurredAt: at,
350
+ revision: nextRevision,
351
+ },
352
+ ],
353
+ };
354
+ }
355
+ case 'AcceptParticipation': {
356
+ const ctx = endeavorContext(state, command.endeavorId);
357
+ if ('reason' in ctx)
358
+ return reject(ctx.reason);
359
+ const participation = state.participations[command.participationId];
360
+ if (!participation)
361
+ return reject(`unknown participation ${command.participationId}`);
362
+ if (participation.status !== 'invited')
363
+ return reject(`participation is ${participation.status}, not invited`);
364
+ if (command.actor !== participation.participant)
365
+ return reject('only the invitee may accept');
366
+ return {
367
+ ok: true,
368
+ events: [
369
+ {
370
+ kind: 'ParticipationAsserted',
371
+ eventId: eventId(nextRevision, 'ParticipationAsserted'),
372
+ endeavorId: command.endeavorId,
373
+ participationId: command.participationId,
374
+ participant: participation.participant,
375
+ role: participation.role,
376
+ assertedBy: 'invitation-acceptance',
377
+ ...(participation.scope ? { scope: participation.scope } : {}),
378
+ occurredAt: at,
379
+ revision: nextRevision,
380
+ },
381
+ ],
382
+ };
383
+ }
384
+ case 'WithdrawParticipation': {
385
+ const ctx = endeavorContext(state, command.endeavorId);
386
+ if ('reason' in ctx)
387
+ return reject(ctx.reason);
388
+ const participation = state.participations[command.participationId];
389
+ if (!participation)
390
+ return reject(`unknown participation ${command.participationId}`);
391
+ if (participation.status !== 'active')
392
+ return reject(`participation is ${participation.status}, not active`);
393
+ if (command.actor !== participation.participant)
394
+ return reject('only the participant may withdraw');
395
+ return {
396
+ ok: true,
397
+ events: [
398
+ {
399
+ kind: 'ParticipationWithdrawn',
400
+ eventId: eventId(nextRevision, 'ParticipationWithdrawn'),
401
+ endeavorId: command.endeavorId,
402
+ participationId: command.participationId,
403
+ occurredAt: at,
404
+ revision: nextRevision,
405
+ },
406
+ ],
407
+ };
408
+ }
409
+ case 'ProposeContribution': {
410
+ const ctx = endeavorContext(state, command.endeavorId);
411
+ if ('reason' in ctx)
412
+ return reject(ctx.reason);
413
+ if (participationStatus(state, command.actor) === 'none') {
414
+ return reject('contribution proposer must be a participant or invitee');
415
+ }
416
+ if (state.proposals[command.proposalId])
417
+ return reject(`proposal ${command.proposalId} already exists`);
418
+ const plan = state.plans[planKey(command.planRef.planId, command.planRef.revision)];
419
+ if (!plan)
420
+ return reject(`unknown plan revision ${planKey(command.planRef.planId, command.planRef.revision)}`);
421
+ if (command.planRef.hash !== plan.contentHash)
422
+ return reject('plan content hash mismatch');
423
+ if (command.steps.length === 0)
424
+ return reject('proposal must name at least one step');
425
+ const stepIds = new Set(plan.steps.map((s) => s.stepId));
426
+ for (const stepId of command.steps) {
427
+ if (!stepIds.has(stepId))
428
+ return reject(`step ${stepId} is not in plan revision ${plan.revision}`);
429
+ }
430
+ return {
431
+ ok: true,
432
+ events: [
433
+ {
434
+ kind: 'ContributionProposed',
435
+ eventId: eventId(nextRevision, 'ContributionProposed'),
436
+ endeavorId: command.endeavorId,
437
+ proposalId: command.proposalId,
438
+ proposer: command.actor,
439
+ planRef: command.planRef,
440
+ steps: command.steps,
441
+ ...(command.note ? { note: command.note } : {}),
442
+ occurredAt: at,
443
+ revision: nextRevision,
444
+ },
445
+ ],
446
+ };
447
+ }
448
+ case 'AllocateContribution': {
449
+ const ctx = endeavorContext(state, command.endeavorId);
450
+ if ('reason' in ctx)
451
+ return reject(ctx.reason);
452
+ if (!isSteward(state, command.actor))
453
+ return reject('only a sponsor/coordinator may allocate');
454
+ const proposal = state.proposals[command.proposalRef];
455
+ if (!proposal)
456
+ return reject(`unknown proposal ${command.proposalRef}`);
457
+ if (proposal.status !== 'open')
458
+ return reject(`proposal is ${proposal.status}, not open`);
459
+ if (state.allocations[command.allocationId])
460
+ return reject(`allocation ${command.allocationId} already exists`);
461
+ if (participationStatus(state, command.participant) === 'none') {
462
+ return reject('allocated participant must be a participant or invitee');
463
+ }
464
+ const proposed = new Set(proposal.steps);
465
+ for (const stepId of command.steps) {
466
+ if (!proposed.has(stepId))
467
+ return reject(`step ${stepId} was not proposed in ${command.proposalRef}`);
468
+ }
469
+ return {
470
+ ok: true,
471
+ events: [
472
+ {
473
+ kind: 'ContributionAllocated',
474
+ eventId: eventId(nextRevision, 'ContributionAllocated'),
475
+ endeavorId: command.endeavorId,
476
+ allocationId: command.allocationId,
477
+ proposalRef: command.proposalRef,
478
+ participant: command.participant,
479
+ steps: command.steps,
480
+ decidedBy: command.actor,
481
+ occurredAt: at,
482
+ revision: nextRevision,
483
+ },
484
+ ],
485
+ };
486
+ }
487
+ case 'CommitContribution': {
488
+ const ctx = endeavorContext(state, command.endeavorId);
489
+ if ('reason' in ctx)
490
+ return reject(ctx.reason);
491
+ const allocation = state.allocations[command.allocationRef];
492
+ if (!allocation)
493
+ return reject(`unknown allocation ${command.allocationRef}`);
494
+ if (allocation.status !== 'allocated')
495
+ return reject(`allocation is ${allocation.status}, not allocated`);
496
+ if (command.actor !== allocation.participant)
497
+ return reject('only the allocated participant may commit');
498
+ if (participationStatus(state, command.actor) === 'none') {
499
+ return reject('commitment requires an existing participation or invitation');
500
+ }
501
+ if (state.commitments[command.commitmentId])
502
+ return reject(`commitment ${command.commitmentId} already exists`);
503
+ const adopted = ctx.endeavor.adoptedPlanRef;
504
+ if (!adopted)
505
+ return reject('no adopted plan to commit against');
506
+ if (!samePlanRef(command.planRef, adopted)) {
507
+ return reject('commitment plan revision hash is stale — commitments bind the adopted revision hash');
508
+ }
509
+ const allocated = new Set(allocation.steps);
510
+ for (const stepId of command.steps) {
511
+ if (!allocated.has(stepId))
512
+ return reject(`step ${stepId} was not allocated in ${command.allocationRef}`);
513
+ }
514
+ if (command.signature.signer !== command.actor)
515
+ return reject('commitment signature signer must be the actor');
516
+ return {
517
+ ok: true,
518
+ events: [
519
+ {
520
+ kind: 'ContributionCommitted',
521
+ eventId: eventId(nextRevision, 'ContributionCommitted'),
522
+ endeavorId: command.endeavorId,
523
+ commitmentId: command.commitmentId,
524
+ allocationRef: command.allocationRef,
525
+ participant: command.actor,
526
+ planRef: command.planRef,
527
+ steps: command.steps,
528
+ signature: command.signature,
529
+ ...(command.bounds ? { bounds: command.bounds } : {}),
530
+ occurredAt: at,
531
+ revision: nextRevision,
532
+ },
533
+ ],
534
+ };
535
+ }
536
+ case 'WithdrawCommitment': {
537
+ const ctx = endeavorContext(state, command.endeavorId);
538
+ if ('reason' in ctx)
539
+ return reject(ctx.reason);
540
+ const commitment = state.commitments[command.commitmentId];
541
+ if (!commitment)
542
+ return reject(`unknown commitment ${command.commitmentId}`);
543
+ if (commitment.status !== 'active')
544
+ return reject(`commitment is ${commitment.status}, not active`);
545
+ if (command.actor !== commitment.participant)
546
+ return reject('only the committed participant may withdraw');
547
+ return {
548
+ ok: true,
549
+ events: [
550
+ {
551
+ kind: 'CommitmentWithdrawn',
552
+ eventId: eventId(nextRevision, 'CommitmentWithdrawn'),
553
+ endeavorId: command.endeavorId,
554
+ commitmentId: command.commitmentId,
555
+ ...(command.reason ? { reason: command.reason } : {}),
556
+ occurredAt: at,
557
+ revision: nextRevision,
558
+ },
559
+ ],
560
+ };
561
+ }
562
+ case 'ReallocateContribution': {
563
+ const ctx = endeavorContext(state, command.endeavorId);
564
+ if ('reason' in ctx)
565
+ return reject(ctx.reason);
566
+ if (!isSteward(state, command.actor))
567
+ return reject('only a sponsor/coordinator may reallocate');
568
+ const commitment = state.commitments[command.commitmentId];
569
+ if (!commitment)
570
+ return reject(`unknown commitment ${command.commitmentId}`);
571
+ if (commitment.status !== 'active' && commitment.status !== 'withdrawn') {
572
+ return reject(`commitment is ${commitment.status}; only active/withdrawn commitments reallocate`);
573
+ }
574
+ if (state.allocations[command.allocationId])
575
+ return reject(`allocation ${command.allocationId} already exists`);
576
+ if (participationStatus(state, command.participant) === 'none') {
577
+ return reject('reallocated participant must be a participant or invitee');
578
+ }
579
+ return {
580
+ ok: true,
581
+ events: [
582
+ {
583
+ kind: 'ContributionReallocated',
584
+ eventId: eventId(nextRevision, 'ContributionReallocated'),
585
+ endeavorId: command.endeavorId,
586
+ commitmentId: command.commitmentId,
587
+ allocationId: command.allocationId,
588
+ participant: command.participant,
589
+ decidedBy: command.actor,
590
+ occurredAt: at,
591
+ revision: nextRevision,
592
+ },
593
+ ],
594
+ };
595
+ }
596
+ case 'RecordStepSatisfied': {
597
+ const ctx = endeavorContext(state, command.endeavorId);
598
+ if ('reason' in ctx)
599
+ return reject(ctx.reason);
600
+ const plan = adoptedPlan(state);
601
+ const adopted = ctx.endeavor.adoptedPlanRef;
602
+ if (!plan || !adopted)
603
+ return reject('no adopted plan');
604
+ if (!plan.steps.some((s) => s.stepId === command.stepId)) {
605
+ return reject(`step ${command.stepId} is not in the adopted plan`);
606
+ }
607
+ if (state.satisfiedSteps[command.stepId])
608
+ return reject(`step ${command.stepId} already satisfied`);
609
+ if (command.evidenceRefs.length === 0)
610
+ return reject('step satisfaction requires completion evidence');
611
+ if (command.actor !== ctx.endeavor.managingPrincipal &&
612
+ participationStatus(state, command.actor) !== 'active') {
613
+ return reject('step satisfaction must be recorded by the managing principal or an active participant');
614
+ }
615
+ return {
616
+ ok: true,
617
+ events: [
618
+ {
619
+ kind: 'PlanStepSatisfied',
620
+ eventId: eventId(nextRevision, 'PlanStepSatisfied'),
621
+ endeavorId: command.endeavorId,
622
+ stepId: command.stepId,
623
+ planRef: adopted,
624
+ evidenceRefs: command.evidenceRefs,
625
+ recordedBy: command.actor,
626
+ occurredAt: at,
627
+ revision: nextRevision,
628
+ },
629
+ ],
630
+ };
631
+ }
632
+ case 'RecordMilestoneAchieved': {
633
+ const ctx = endeavorContext(state, command.endeavorId);
634
+ if ('reason' in ctx)
635
+ return reject(ctx.reason);
636
+ const plan = adoptedPlan(state);
637
+ if (!plan)
638
+ return reject('no adopted plan');
639
+ if (!plan.milestones.some((m) => m.milestoneId === command.milestoneId)) {
640
+ return reject(`milestone ${command.milestoneId} is not in the adopted plan`);
641
+ }
642
+ if (state.milestones[command.milestoneId])
643
+ return reject(`milestone ${command.milestoneId} already achieved`);
644
+ if (command.evidenceRefs.length === 0)
645
+ return reject('milestone achievement requires criteria evidence');
646
+ return {
647
+ ok: true,
648
+ events: [
649
+ {
650
+ kind: 'MilestoneAchieved',
651
+ eventId: eventId(nextRevision, 'MilestoneAchieved'),
652
+ endeavorId: command.endeavorId,
653
+ milestoneId: command.milestoneId,
654
+ evidenceRefs: command.evidenceRefs,
655
+ recordedBy: command.actor,
656
+ occurredAt: at,
657
+ revision: nextRevision,
658
+ },
659
+ ],
660
+ };
661
+ }
662
+ case 'SatisfyEndeavor': {
663
+ const ctx = endeavorContext(state, command.endeavorId);
664
+ if ('reason' in ctx)
665
+ return reject(ctx.reason);
666
+ if (command.actor !== ctx.endeavor.managingPrincipal && !isSteward(state, command.actor)) {
667
+ return reject('only the managing principal or a sponsor/coordinator may satisfy');
668
+ }
669
+ if (!command.outcomeValidationRef)
670
+ return reject('EndeavorSatisfied requires an outcome validation ref');
671
+ return {
672
+ ok: true,
673
+ events: [
674
+ {
675
+ kind: 'EndeavorSatisfied',
676
+ eventId: eventId(nextRevision, 'EndeavorSatisfied'),
677
+ endeavorId: command.endeavorId,
678
+ outcomeValidationRef: command.outcomeValidationRef,
679
+ occurredAt: at,
680
+ revision: nextRevision,
681
+ },
682
+ ],
683
+ };
684
+ }
685
+ case 'AbandonEndeavor': {
686
+ const ctx = endeavorContext(state, command.endeavorId);
687
+ if ('reason' in ctx)
688
+ return reject(ctx.reason);
689
+ if (command.actor !== ctx.endeavor.managingPrincipal && !isSteward(state, command.actor)) {
690
+ return reject('only the managing principal or a sponsor/coordinator may abandon');
691
+ }
692
+ return {
693
+ ok: true,
694
+ events: [
695
+ {
696
+ kind: 'EndeavorAbandoned',
697
+ eventId: eventId(nextRevision, 'EndeavorAbandoned'),
698
+ endeavorId: command.endeavorId,
699
+ ...(command.reason ? { reason: command.reason } : {}),
700
+ occurredAt: at,
701
+ revision: nextRevision,
702
+ },
703
+ ],
704
+ };
705
+ }
706
+ case 'ProposeRecommendation': {
707
+ if (state.recommendations[command.recommendationId]) {
708
+ return reject(`recommendation ${command.recommendationId} already exists`);
709
+ }
710
+ if (command.recommendationKind.trim().length === 0)
711
+ return reject('recommendation kind must be non-empty');
712
+ if (command.explanation.trim().length === 0)
713
+ return reject('recommendation requires an explanation');
714
+ if (command.proposedCommands.length === 0)
715
+ return reject('recommendation must propose at least one typed command');
716
+ for (const t of command.proposedCommands) {
717
+ if (!COORDINATION_COMMAND_KINDS.has(t.kind)) {
718
+ return reject(`proposed command kind ${t.kind} is not a coordination command — templates are typed, never free-form`);
719
+ }
720
+ }
721
+ if (command.confidence !== undefined && (command.confidence < 0 || command.confidence > 1)) {
722
+ return reject('confidence must be in [0, 1]');
723
+ }
724
+ // Producer gate: a request-subject recommendation (pre-adoption triage, spec 334 §10) is
725
+ // produced by the MANAGING PRINCIPAL's agent; an endeavor-subject one (spec 333 §3) by the
726
+ // managing principal or an active participant.
727
+ if (command.subject.kind === 'request') {
728
+ if (!state.request)
729
+ return reject('no request to recommend on');
730
+ if (state.request.record.requestId !== command.subject.requestId)
731
+ return reject('request id mismatch');
732
+ if (state.request.status !== 'submitted')
733
+ return reject(`request is ${state.request.status}, not submitted`);
734
+ if (command.actor !== state.request.record.targetPrincipal) {
735
+ return reject('only the target principal (its agent) may recommend on a pending request');
736
+ }
737
+ }
738
+ else {
739
+ const ctx = endeavorContext(state, command.subject.endeavorId);
740
+ if ('reason' in ctx)
741
+ return reject(ctx.reason);
742
+ if (command.actor !== ctx.endeavor.managingPrincipal && participationStatus(state, command.actor) !== 'active') {
743
+ return reject('only the managing principal or an active participant may recommend on an endeavor');
744
+ }
745
+ }
746
+ const recommendation = {
747
+ schema: 'apcoord/RecommendationV1',
748
+ recommendationId: command.recommendationId,
749
+ subject: command.subject,
750
+ producedBy: command.actor,
751
+ kind: command.recommendationKind,
752
+ proposedCommands: command.proposedCommands,
753
+ ...(command.confidence !== undefined ? { confidence: command.confidence } : {}),
754
+ explanation: command.explanation,
755
+ evidenceRefs: command.evidenceRefs ?? [],
756
+ status: 'proposed',
757
+ proposedAt: at,
758
+ };
759
+ return {
760
+ ok: true,
761
+ events: [
762
+ {
763
+ kind: 'RecommendationProposed',
764
+ eventId: eventId(nextRevision, 'RecommendationProposed'),
765
+ recommendationId: command.recommendationId,
766
+ recommendation,
767
+ occurredAt: at,
768
+ revision: nextRevision,
769
+ },
770
+ ],
771
+ };
772
+ }
773
+ case 'ResolveRecommendation': {
774
+ const rec = state.recommendations[command.recommendationId];
775
+ if (!rec)
776
+ return reject(`unknown recommendation ${command.recommendationId}`);
777
+ if (rec.status !== 'proposed')
778
+ return reject(`recommendation is ${rec.status}, not proposed`);
779
+ // Decider gate (spec 334 §10): request-subject → the target principal; endeavor-subject →
780
+ // the managing principal or a sponsor/coordinator. NOTE: acceptance records the decision
781
+ // ONLY — the proposed commands replay through ordinary validation at the serving plane;
782
+ // the reducer never applies templates (spec 333 §3 — acceptance is not a bypass).
783
+ if (rec.subject.kind === 'request') {
784
+ if (!state.request || state.request.record.requestId !== rec.subject.requestId) {
785
+ return reject('recommendation subject request is missing from state');
786
+ }
787
+ if (command.actor !== state.request.record.targetPrincipal) {
788
+ return reject('only the target principal may resolve a request recommendation');
789
+ }
790
+ }
791
+ else {
792
+ if (!state.endeavor || state.endeavor.endeavorId !== rec.subject.endeavorId) {
793
+ return reject('recommendation subject endeavor is missing from state');
794
+ }
795
+ if (command.actor !== state.endeavor.managingPrincipal && !isSteward(state, command.actor)) {
796
+ return reject('only the managing principal or a sponsor/coordinator may resolve an endeavor recommendation');
797
+ }
798
+ }
799
+ const kind = command.resolution === 'accepted' ? 'RecommendationAccepted' : 'RecommendationRejected';
800
+ return {
801
+ ok: true,
802
+ events: [
803
+ {
804
+ kind,
805
+ eventId: eventId(nextRevision, kind),
806
+ recommendationId: command.recommendationId,
807
+ resolvedBy: command.actor,
808
+ ...(command.reason ? { reason: command.reason } : {}),
809
+ occurredAt: at,
810
+ revision: nextRevision,
811
+ },
812
+ ],
813
+ };
814
+ }
815
+ default:
816
+ return reject(`unknown command kind ${command.kind ?? '<none>'}`);
817
+ }
818
+ }
819
+ export function applyCoordinationEvent(state, event) {
820
+ if (event.revision !== state.revision + 1) {
821
+ throw new CoordinationEventError(`non-monotonic revision: state at ${state.revision}, event ${event.kind} carries ${event.revision}`);
822
+ }
823
+ const endeavorAt = (revision) => {
824
+ if (!state.endeavor)
825
+ throw new CoordinationEventError(`${event.kind} applied before EndeavorAdopted`);
826
+ return { ...state.endeavor, revision };
827
+ };
828
+ switch (event.kind) {
829
+ case 'EndeavorRequestSubmitted':
830
+ return { ...state, revision: event.revision, request: { record: event.request, status: 'submitted' } };
831
+ case 'EndeavorRequestDeclined': {
832
+ if (!state.request)
833
+ throw new CoordinationEventError('EndeavorRequestDeclined without a request');
834
+ return { ...state, revision: event.revision, request: { ...state.request, status: 'declined' } };
835
+ }
836
+ case 'EndeavorAdopted': {
837
+ if (!state.request)
838
+ throw new CoordinationEventError('EndeavorAdopted without a request');
839
+ const endeavor = {
840
+ schema: 'apcoord/EndeavorV1',
841
+ endeavorId: event.endeavorId,
842
+ managingPrincipal: event.managingPrincipal,
843
+ situationId: event.situationId,
844
+ title: event.title,
845
+ outcomeRef: {
846
+ kind: 'resource',
847
+ iri: `urn:apcoord:outcome:${event.outcome.outcomeId}:${event.outcome.revision}`,
848
+ },
849
+ requestRef: event.requestId,
850
+ lifecycle: 'adopted',
851
+ relations: [],
852
+ revision: event.revision,
853
+ };
854
+ return {
855
+ ...state,
856
+ revision: event.revision,
857
+ request: { ...state.request, status: 'adopted' },
858
+ endeavor,
859
+ outcome: event.outcome,
860
+ };
861
+ }
862
+ case 'OutcomeRevised': {
863
+ const endeavor = endeavorAt(event.revision);
864
+ return {
865
+ ...state,
866
+ revision: event.revision,
867
+ endeavor: {
868
+ ...endeavor,
869
+ outcomeRef: {
870
+ kind: 'resource',
871
+ iri: `urn:apcoord:outcome:${event.outcome.outcomeId}:${event.outcome.revision}`,
872
+ },
873
+ },
874
+ outcome: event.outcome,
875
+ };
876
+ }
877
+ case 'PlanProposed': {
878
+ const plan = {
879
+ schema: 'apcoord/CoordinationPlanV1',
880
+ planId: event.planId,
881
+ endeavorId: event.endeavorId,
882
+ revision: event.planRevision,
883
+ contentHash: event.contentHash,
884
+ steps: event.steps,
885
+ edges: event.edges,
886
+ milestones: event.milestones,
887
+ proposedBy: event.proposedBy,
888
+ status: 'proposed',
889
+ };
890
+ return {
891
+ ...state,
892
+ revision: event.revision,
893
+ endeavor: endeavorAt(event.revision),
894
+ plans: { ...state.plans, [planKey(event.planId, event.planRevision)]: plan },
895
+ };
896
+ }
897
+ case 'PlanAdopted': {
898
+ const key = planKey(event.planRef.planId, event.planRef.revision);
899
+ const plan = state.plans[key];
900
+ if (!plan)
901
+ throw new CoordinationEventError(`PlanAdopted for unknown plan revision ${key}`);
902
+ const plans = { ...state.plans, [key]: { ...plan, status: 'adopted' } };
903
+ if (event.supersededPlanRef) {
904
+ const priorKey = planKey(event.supersededPlanRef.planId, event.supersededPlanRef.revision);
905
+ const prior = plans[priorKey];
906
+ if (prior)
907
+ plans[priorKey] = { ...prior, status: 'superseded' };
908
+ }
909
+ return {
910
+ ...state,
911
+ revision: event.revision,
912
+ endeavor: { ...endeavorAt(event.revision), adoptedPlanRef: event.planRef, lifecycle: 'active' },
913
+ plans,
914
+ };
915
+ }
916
+ case 'PlanRejected': {
917
+ const key = planKey(event.planId, event.planRevision);
918
+ const plan = state.plans[key];
919
+ if (!plan)
920
+ throw new CoordinationEventError(`PlanRejected for unknown plan revision ${key}`);
921
+ return {
922
+ ...state,
923
+ revision: event.revision,
924
+ endeavor: endeavorAt(event.revision),
925
+ plans: { ...state.plans, [key]: { ...plan, status: 'rejected' } },
926
+ };
927
+ }
928
+ case 'ParticipationInvited': {
929
+ const participation = {
930
+ schema: 'apcoord/EndeavorParticipationV1',
931
+ participationId: event.participationId,
932
+ endeavorId: event.endeavorId,
933
+ participant: event.participant,
934
+ role: event.role,
935
+ validity: { validFrom: event.occurredAt },
936
+ assertedBy: 'invitation-acceptance',
937
+ status: 'invited',
938
+ ...(event.scope ? { scope: event.scope } : {}),
939
+ };
940
+ return {
941
+ ...state,
942
+ revision: event.revision,
943
+ endeavor: endeavorAt(event.revision),
944
+ participations: { ...state.participations, [event.participationId]: participation },
945
+ };
946
+ }
947
+ case 'ParticipationAsserted': {
948
+ const participation = {
949
+ schema: 'apcoord/EndeavorParticipationV1',
950
+ participationId: event.participationId,
951
+ endeavorId: event.endeavorId,
952
+ participant: event.participant,
953
+ role: event.role,
954
+ validity: { validFrom: event.occurredAt },
955
+ assertedBy: event.assertedBy,
956
+ status: 'active',
957
+ ...(event.scope ? { scope: event.scope } : {}),
958
+ };
959
+ return {
960
+ ...state,
961
+ revision: event.revision,
962
+ endeavor: endeavorAt(event.revision),
963
+ participations: { ...state.participations, [event.participationId]: participation },
964
+ };
965
+ }
966
+ case 'ParticipationWithdrawn': {
967
+ const participation = state.participations[event.participationId];
968
+ if (!participation) {
969
+ throw new CoordinationEventError(`ParticipationWithdrawn for unknown participation ${event.participationId}`);
970
+ }
971
+ return {
972
+ ...state,
973
+ revision: event.revision,
974
+ endeavor: endeavorAt(event.revision),
975
+ participations: {
976
+ ...state.participations,
977
+ [event.participationId]: {
978
+ ...participation,
979
+ status: 'withdrawn',
980
+ validity: { ...participation.validity, validUntil: event.occurredAt },
981
+ },
982
+ },
983
+ };
984
+ }
985
+ case 'ContributionProposed': {
986
+ const proposal = {
987
+ schema: 'apcoord/ContributionProposalV1',
988
+ proposalId: event.proposalId,
989
+ endeavorId: event.endeavorId,
990
+ proposer: event.proposer,
991
+ planRef: event.planRef,
992
+ steps: event.steps,
993
+ proposedAt: event.occurredAt,
994
+ status: 'open',
995
+ ...(event.note ? { note: event.note } : {}),
996
+ };
997
+ return {
998
+ ...state,
999
+ revision: event.revision,
1000
+ endeavor: endeavorAt(event.revision),
1001
+ proposals: { ...state.proposals, [event.proposalId]: proposal },
1002
+ };
1003
+ }
1004
+ case 'ContributionAllocated': {
1005
+ const proposal = state.proposals[event.proposalRef];
1006
+ if (!proposal)
1007
+ throw new CoordinationEventError(`ContributionAllocated for unknown proposal ${event.proposalRef}`);
1008
+ const allocation = {
1009
+ schema: 'apcoord/AllocationDecisionV1',
1010
+ allocationId: event.allocationId,
1011
+ endeavorId: event.endeavorId,
1012
+ proposalRef: event.proposalRef,
1013
+ participant: event.participant,
1014
+ steps: event.steps,
1015
+ decidedBy: event.decidedBy,
1016
+ decidedAt: event.occurredAt,
1017
+ status: 'allocated',
1018
+ };
1019
+ return {
1020
+ ...state,
1021
+ revision: event.revision,
1022
+ endeavor: endeavorAt(event.revision),
1023
+ proposals: { ...state.proposals, [event.proposalRef]: { ...proposal, status: 'allocated' } },
1024
+ allocations: { ...state.allocations, [event.allocationId]: allocation },
1025
+ };
1026
+ }
1027
+ case 'ContributionCommitted': {
1028
+ const allocation = state.allocations[event.allocationRef];
1029
+ if (!allocation)
1030
+ throw new CoordinationEventError(`ContributionCommitted for unknown allocation ${event.allocationRef}`);
1031
+ const commitment = {
1032
+ schema: 'apcoord/ContributionCommitmentV1',
1033
+ commitmentId: event.commitmentId,
1034
+ endeavorId: event.endeavorId,
1035
+ allocationRef: event.allocationRef,
1036
+ participant: event.participant,
1037
+ planRef: event.planRef,
1038
+ steps: event.steps,
1039
+ signature: event.signature,
1040
+ status: 'active',
1041
+ ...(event.bounds ? { bounds: event.bounds } : {}),
1042
+ };
1043
+ return {
1044
+ ...state,
1045
+ revision: event.revision,
1046
+ endeavor: endeavorAt(event.revision),
1047
+ allocations: { ...state.allocations, [event.allocationRef]: { ...allocation, status: 'committed' } },
1048
+ commitments: { ...state.commitments, [event.commitmentId]: commitment },
1049
+ };
1050
+ }
1051
+ case 'CommitmentWithdrawn': {
1052
+ const commitment = state.commitments[event.commitmentId];
1053
+ if (!commitment)
1054
+ throw new CoordinationEventError(`CommitmentWithdrawn for unknown commitment ${event.commitmentId}`);
1055
+ return {
1056
+ ...state,
1057
+ revision: event.revision,
1058
+ endeavor: endeavorAt(event.revision),
1059
+ commitments: { ...state.commitments, [event.commitmentId]: { ...commitment, status: 'withdrawn' } },
1060
+ };
1061
+ }
1062
+ case 'ContributionReallocated': {
1063
+ const commitment = state.commitments[event.commitmentId];
1064
+ if (!commitment)
1065
+ throw new CoordinationEventError(`ContributionReallocated for unknown commitment ${event.commitmentId}`);
1066
+ const priorAllocation = state.allocations[commitment.allocationRef];
1067
+ if (!priorAllocation) {
1068
+ throw new CoordinationEventError(`ContributionReallocated: commitment ${event.commitmentId} references unknown allocation ${commitment.allocationRef}`);
1069
+ }
1070
+ const allocation = {
1071
+ schema: 'apcoord/AllocationDecisionV1',
1072
+ allocationId: event.allocationId,
1073
+ endeavorId: event.endeavorId,
1074
+ proposalRef: priorAllocation.proposalRef,
1075
+ participant: event.participant,
1076
+ steps: commitment.steps,
1077
+ decidedBy: event.decidedBy,
1078
+ decidedAt: event.occurredAt,
1079
+ status: 'allocated',
1080
+ };
1081
+ return {
1082
+ ...state,
1083
+ revision: event.revision,
1084
+ endeavor: endeavorAt(event.revision),
1085
+ commitments: { ...state.commitments, [event.commitmentId]: { ...commitment, status: 'reallocated' } },
1086
+ allocations: { ...state.allocations, [event.allocationId]: allocation },
1087
+ };
1088
+ }
1089
+ case 'PlanStepSatisfied':
1090
+ return {
1091
+ ...state,
1092
+ revision: event.revision,
1093
+ endeavor: endeavorAt(event.revision),
1094
+ satisfiedSteps: {
1095
+ ...state.satisfiedSteps,
1096
+ [event.stepId]: {
1097
+ stepId: event.stepId,
1098
+ planRef: event.planRef,
1099
+ evidenceRefs: event.evidenceRefs,
1100
+ recordedBy: event.recordedBy,
1101
+ recordedAt: event.occurredAt,
1102
+ },
1103
+ },
1104
+ };
1105
+ case 'MilestoneAchieved': {
1106
+ const achievement = {
1107
+ schema: 'apcoord/MilestoneAchievementV1',
1108
+ milestoneId: event.milestoneId,
1109
+ endeavorId: event.endeavorId,
1110
+ evidenceRefs: event.evidenceRefs,
1111
+ achievedAt: event.occurredAt,
1112
+ };
1113
+ return {
1114
+ ...state,
1115
+ revision: event.revision,
1116
+ endeavor: endeavorAt(event.revision),
1117
+ milestones: { ...state.milestones, [event.milestoneId]: achievement },
1118
+ };
1119
+ }
1120
+ case 'EndeavorSatisfied':
1121
+ return {
1122
+ ...state,
1123
+ revision: event.revision,
1124
+ endeavor: { ...endeavorAt(event.revision), lifecycle: 'satisfied' },
1125
+ };
1126
+ case 'EndeavorAbandoned':
1127
+ return {
1128
+ ...state,
1129
+ revision: event.revision,
1130
+ endeavor: { ...endeavorAt(event.revision), lifecycle: 'abandoned' },
1131
+ };
1132
+ case 'RecommendationProposed':
1133
+ return {
1134
+ ...state,
1135
+ revision: event.revision,
1136
+ recommendations: { ...state.recommendations, [event.recommendationId]: event.recommendation },
1137
+ };
1138
+ case 'RecommendationAccepted':
1139
+ case 'RecommendationRejected': {
1140
+ const rec = state.recommendations[event.recommendationId];
1141
+ if (!rec)
1142
+ throw new CoordinationEventError(`${event.kind} for unknown recommendation ${event.recommendationId}`);
1143
+ return {
1144
+ ...state,
1145
+ revision: event.revision,
1146
+ recommendations: {
1147
+ ...state.recommendations,
1148
+ [event.recommendationId]: {
1149
+ ...rec,
1150
+ status: event.kind === 'RecommendationAccepted' ? 'accepted' : 'rejected',
1151
+ resolvedBy: event.resolvedBy,
1152
+ resolvedAt: event.occurredAt,
1153
+ ...(event.reason ? { resolutionReason: event.reason } : {}),
1154
+ },
1155
+ },
1156
+ };
1157
+ }
1158
+ default: {
1159
+ const kind = event.kind ?? '<none>';
1160
+ throw new CoordinationEventError(`unknown event kind ${kind}`);
1161
+ }
1162
+ }
1163
+ }
1164
+ //# sourceMappingURL=reducer.js.map