macp-sdk-typescript 0.7.1 → 0.9.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.
@@ -2,16 +2,77 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ProposalProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
+ const logging_1 = require("../logging");
5
6
  class ProposalProjection {
6
7
  proposals = new Map();
7
8
  accepts = [];
8
9
  rejections = [];
10
+ /**
11
+ * Each sender's current (unsuperseded) `Accept`, keyed by sender.
12
+ * RFC-MACP-0008 §5 rule 5 (`:70`): "A participant MAY change its
13
+ * acceptance target by sending a later `Accept` for a different live
14
+ * proposal. The latest accepted `Accept` from a participant supersedes
15
+ * earlier accepts from the same participant." `accepts` above retains the
16
+ * full append-only history (including superseded accepts) for audit
17
+ * purposes; `isAccepted` and `acceptedProposal` read from this map instead
18
+ * so they reflect the live acceptance set — required for determinism by
19
+ * §7 (`:89`): "Given the same accepted history and the same version-bound
20
+ * rules, implementations MUST derive the same live proposal set, the same
21
+ * acceptance set, and the same commitment eligibility."
22
+ */
23
+ latestAcceptBySender = new Map();
24
+ /**
25
+ * The session's accepted history, one envelope per unique `message_id`. See
26
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
27
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
28
+ * only as a one-line pointer.
29
+ */
9
30
  transcript = [];
31
+ /**
32
+ * Cardinality anomalies recorded while replaying this projection's accepted
33
+ * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
34
+ * for the canonical description; duplicated here only as a one-line
35
+ * pointer. No built-in detection populates this for Proposal mode.
36
+ */
37
+ anomalies = [];
10
38
  phase = 'Negotiating';
11
39
  commitment;
40
+ /**
41
+ * `message_id`s already applied to this projection. See
42
+ * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
43
+ * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
44
+ * only as a one-line pointer.
45
+ */
46
+ seenMessageIds = new Set();
47
+ /**
48
+ * Apply one envelope to this projection's in-process state.
49
+ *
50
+ * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
51
+ * no acceptance marker). Canonical source: `schemas/conformance/README.md`
52
+ * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
53
+ * mode are documented once, on `BaseProjection.applyEnvelope`
54
+ * (`src/projections/base.ts`), and duplicated here only as a one-line
55
+ * pointer so six independent copies of the same prose cannot drift.
56
+ *
57
+ * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
58
+ * non-event — not appended to `transcript`, not passed to the `switch`.
59
+ * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
60
+ * citation; duplicated here only as a one-line pointer.
61
+ */
12
62
  applyEnvelope(envelope, protoRegistry) {
13
63
  if (envelope.mode !== constants_1.MODE_PROPOSAL)
14
64
  return;
65
+ if (envelope.messageId) {
66
+ if (this.seenMessageIds.has(envelope.messageId)) {
67
+ logging_1.logger.debug('projection redelivery ignored', {
68
+ messageId: envelope.messageId,
69
+ mode: envelope.mode,
70
+ messageType: envelope.messageType,
71
+ });
72
+ return;
73
+ }
74
+ this.seenMessageIds.add(envelope.messageId);
75
+ }
15
76
  this.transcript.push(envelope);
16
77
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
17
78
  switch (envelope.messageType) {
@@ -41,7 +102,11 @@ class ProposalProjection {
41
102
  }
42
103
  case 'Accept': {
43
104
  const record = payload;
44
- this.accepts.push({ ...record, sender: envelope.sender });
105
+ const accept = { ...record, sender: envelope.sender };
106
+ this.accepts.push(accept);
107
+ // RFC-MACP-0008 §5 rule 5 (`:70`): this Accept supersedes any earlier
108
+ // one from the same sender for the live acceptance set.
109
+ this.latestAcceptBySender.set(envelope.sender, accept);
45
110
  break;
46
111
  }
47
112
  case 'Reject': {
@@ -77,6 +142,14 @@ class ProposalProjection {
77
142
  break;
78
143
  }
79
144
  }
145
+ /**
146
+ * True once at least one `ProjectionAnomaly` has been recorded. See
147
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
148
+ * canonical description; duplicated here only as a one-line pointer.
149
+ */
150
+ get hasAnomalies() {
151
+ return this.anomalies.length > 0;
152
+ }
80
153
  get isCommitted() {
81
154
  return this.commitment !== undefined;
82
155
  }
@@ -94,8 +167,19 @@ class ProposalProjection {
94
167
  const all = [...this.proposals.values()];
95
168
  return all[all.length - 1];
96
169
  }
170
+ /**
171
+ * True if some participant's *current* (unsuperseded) `Accept` targets
172
+ * `proposalId` — RFC-MACP-0008 §5 rule 5 (`:70`). Note this reads from the
173
+ * live acceptance set, not from `accepts`' full history: a participant
174
+ * whose only accept for `proposalId` was later superseded by a re-accept
175
+ * of a different proposal is not counted.
176
+ */
97
177
  isAccepted(proposalId) {
98
- return this.accepts.some((a) => a.proposalId === proposalId);
178
+ for (const accept of this.latestAcceptBySender.values()) {
179
+ if (accept.proposalId === proposalId)
180
+ return true;
181
+ }
182
+ return false;
99
183
  }
100
184
  isTerminallyRejected(proposalId) {
101
185
  return this.rejections.some((r) => r.proposalId === proposalId && r.terminal);
@@ -108,10 +192,19 @@ class ProposalProjection {
108
192
  }
109
193
  return result;
110
194
  }
195
+ /**
196
+ * The single proposal every participant's *current* accept targets, or
197
+ * `undefined` if no one has an outstanding accept or outstanding accepts
198
+ * are split across more than one proposal. Computed from the live
199
+ * acceptance set (see `isAccepted`), not from `accepts`' full history — a
200
+ * participant who accepts `p1` and later re-accepts `p2` (RFC-MACP-0008 §5
201
+ * rule 5, `:70`) contributes only `p2`, so this correctly returns `p2`
202
+ * rather than `undefined`.
203
+ */
111
204
  acceptedProposal() {
112
- if (this.accepts.length === 0)
205
+ if (this.latestAcceptBySender.size === 0)
113
206
  return undefined;
114
- const ids = new Set(this.accepts.map((a) => a.proposalId));
207
+ const ids = new Set([...this.latestAcceptBySender.values()].map((a) => a.proposalId));
115
208
  if (ids.size === 1)
116
209
  return ids.values().next().value;
117
210
  return undefined;
@@ -1,3 +1,4 @@
1
+ import type { ProjectionAnomaly } from './base';
1
2
  import type { Envelope } from '../types';
2
3
  import type { ProtoRegistry } from '../proto-registry';
3
4
  export interface ApprovalRequestRecord {
@@ -16,10 +17,52 @@ export interface BallotRecord {
16
17
  export declare class QuorumProjection {
17
18
  readonly requests: Map<string, ApprovalRequestRecord>;
18
19
  readonly ballots: Map<string, Map<string, BallotRecord>>;
20
+ /**
21
+ * The session's accepted history, one envelope per unique `message_id`. See
22
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
23
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
24
+ * only as a one-line pointer.
25
+ */
19
26
  readonly transcript: Envelope[];
27
+ /**
28
+ * Cardinality anomalies recorded while replaying this projection's accepted
29
+ * transcript (e.g. a duplicate ballot across `Approve`/`Reject`/`Abstain`
30
+ * from the same sender for the same `request_id`). See
31
+ * `BaseProjection.anomalies` (`src/projections/base.ts`) for the canonical
32
+ * description; duplicated here only as a one-line pointer.
33
+ */
34
+ readonly anomalies: ProjectionAnomaly[];
20
35
  phase: 'Pending' | 'Voting' | 'Committed';
21
36
  commitment?: Record<string, unknown>;
37
+ /**
38
+ * `message_id`s already applied to this projection. See
39
+ * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
40
+ * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
41
+ * only as a one-line pointer.
42
+ */
43
+ private readonly seenMessageIds;
44
+ /**
45
+ * Apply one envelope to this projection's in-process state.
46
+ *
47
+ * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
48
+ * no acceptance marker). Canonical source: `schemas/conformance/README.md`
49
+ * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
50
+ * mode are documented once, on `BaseProjection.applyEnvelope`
51
+ * (`src/projections/base.ts`), and duplicated here only as a one-line
52
+ * pointer so six independent copies of the same prose cannot drift.
53
+ *
54
+ * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
55
+ * non-event — not appended to `transcript`, not passed to the `switch`.
56
+ * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
57
+ * citation; duplicated here only as a one-line pointer.
58
+ */
22
59
  applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
60
+ /**
61
+ * True once at least one `ProjectionAnomaly` has been recorded. See
62
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
63
+ * canonical description; duplicated here only as a one-line pointer.
64
+ */
65
+ get hasAnomalies(): boolean;
23
66
  get isCommitted(): boolean;
24
67
  get isPositiveOutcome(): boolean | undefined;
25
68
  private setBallot;
@@ -2,15 +2,63 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QuorumProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
+ const logging_1 = require("../logging");
5
6
  class QuorumProjection {
6
7
  requests = new Map();
7
8
  ballots = new Map();
9
+ /**
10
+ * The session's accepted history, one envelope per unique `message_id`. See
11
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
12
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
13
+ * only as a one-line pointer.
14
+ */
8
15
  transcript = [];
16
+ /**
17
+ * Cardinality anomalies recorded while replaying this projection's accepted
18
+ * transcript (e.g. a duplicate ballot across `Approve`/`Reject`/`Abstain`
19
+ * from the same sender for the same `request_id`). See
20
+ * `BaseProjection.anomalies` (`src/projections/base.ts`) for the canonical
21
+ * description; duplicated here only as a one-line pointer.
22
+ */
23
+ anomalies = [];
9
24
  phase = 'Pending';
10
25
  commitment;
26
+ /**
27
+ * `message_id`s already applied to this projection. See
28
+ * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
29
+ * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
30
+ * only as a one-line pointer.
31
+ */
32
+ seenMessageIds = new Set();
33
+ /**
34
+ * Apply one envelope to this projection's in-process state.
35
+ *
36
+ * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
37
+ * no acceptance marker). Canonical source: `schemas/conformance/README.md`
38
+ * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
39
+ * mode are documented once, on `BaseProjection.applyEnvelope`
40
+ * (`src/projections/base.ts`), and duplicated here only as a one-line
41
+ * pointer so six independent copies of the same prose cannot drift.
42
+ *
43
+ * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
44
+ * non-event — not appended to `transcript`, not passed to the `switch`.
45
+ * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
46
+ * citation; duplicated here only as a one-line pointer.
47
+ */
11
48
  applyEnvelope(envelope, protoRegistry) {
12
49
  if (envelope.mode !== constants_1.MODE_QUORUM)
13
50
  return;
51
+ if (envelope.messageId) {
52
+ if (this.seenMessageIds.has(envelope.messageId)) {
53
+ logging_1.logger.debug('projection redelivery ignored', {
54
+ messageId: envelope.messageId,
55
+ mode: envelope.mode,
56
+ messageType: envelope.messageType,
57
+ });
58
+ return;
59
+ }
60
+ this.seenMessageIds.add(envelope.messageId);
61
+ }
14
62
  this.transcript.push(envelope);
15
63
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
16
64
  switch (envelope.messageType) {
@@ -22,17 +70,17 @@ class QuorumProjection {
22
70
  }
23
71
  case 'Approve': {
24
72
  const record = payload;
25
- this.setBallot(record.requestId, envelope.sender, 'approve', record.reason);
73
+ this.setBallot(envelope, record.requestId, 'approve', record.reason);
26
74
  break;
27
75
  }
28
76
  case 'Reject': {
29
77
  const record = payload;
30
- this.setBallot(record.requestId, envelope.sender, 'reject', record.reason);
78
+ this.setBallot(envelope, record.requestId, 'reject', record.reason);
31
79
  break;
32
80
  }
33
81
  case 'Abstain': {
34
82
  const record = payload;
35
- this.setBallot(record.requestId, envelope.sender, 'abstain', record.reason);
83
+ this.setBallot(envelope, record.requestId, 'abstain', record.reason);
36
84
  break;
37
85
  }
38
86
  case 'Commitment': {
@@ -44,6 +92,14 @@ class QuorumProjection {
44
92
  break;
45
93
  }
46
94
  }
95
+ /**
96
+ * True once at least one `ProjectionAnomaly` has been recorded. See
97
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
98
+ * canonical description; duplicated here only as a one-line pointer.
99
+ */
100
+ get hasAnomalies() {
101
+ return this.anomalies.length > 0;
102
+ }
47
103
  get isCommitted() {
48
104
  return this.commitment !== undefined;
49
105
  }
@@ -54,9 +110,39 @@ class QuorumProjection {
54
110
  this.commitment.outcome_positive;
55
111
  return val !== undefined ? Boolean(val) : true;
56
112
  }
57
- setBallot(requestId, sender, vote, reason) {
113
+ setBallot(envelope, requestId, vote, reason) {
58
114
  const senderMap = this.ballots.get(requestId) ?? new Map();
59
- senderMap.set(sender, { requestId, vote, reason, sender });
115
+ const kept = senderMap.get(envelope.sender);
116
+ if (kept !== undefined) {
117
+ // RFC-MACP-0011 §5 rule 3 (`:67`): participation is MAY (a participant need
118
+ // not cast a ballot), but the cap of one ballot per participant ACROSS
119
+ // Approve, Reject, and Abstain is enforced under §5's opening sentence,
120
+ // "Implementations MUST enforce the following:" (`:63`). Do NOT phrase this
121
+ // as "a participant MUST cast at most one ballot" — RFC-0011 puts the MUST
122
+ // on the implementation, not the participant (contrast RFC-MACP-0007 §5.3,
123
+ // which puts it directly on the participant; same obligation, different
124
+ // addressee). A later ballot of a *different* type is still a duplicate,
125
+ // not a change of vote, so this guard is keyed on the sender ALONE within
126
+ // the request — never on sender + vote. macp-runtime enforces this
127
+ // identically in all three arms (quorum.rs:164/184/204).
128
+ // Keeping the sender's FIRST ballot (rather than the last) is not stated
129
+ // by RFC-0011 either; it is parity with RFC-MACP-0007 §5 item 3's
130
+ // explicit first-stands rule for `Vote`, plus macp-runtime's enforced
131
+ // first-wins behaviour.
132
+ const anomaly = {
133
+ kind: 'duplicate_ballot',
134
+ mode: envelope.mode,
135
+ messageType: envelope.messageType,
136
+ messageId: envelope.messageId,
137
+ sender: envelope.sender,
138
+ subjectId: requestId,
139
+ detail: `sender ${envelope.sender} already cast '${kept.vote}' on request ${requestId}; discarded '${vote}'`,
140
+ };
141
+ this.anomalies.push(anomaly);
142
+ logging_1.logger.warn('projection anomaly', anomaly);
143
+ return;
144
+ }
145
+ senderMap.set(envelope.sender, { requestId, vote, reason, sender: envelope.sender });
60
146
  this.ballots.set(requestId, senderMap);
61
147
  }
62
148
  approvalCount(requestId) {
@@ -1,3 +1,4 @@
1
+ import type { ProjectionAnomaly } from './base';
1
2
  import type { Envelope } from '../types';
2
3
  import type { ProtoRegistry } from '../proto-registry';
3
4
  export interface TaskRecord {
@@ -37,10 +38,51 @@ export declare class TaskProjection {
37
38
  readonly updates: TaskUpdateRecord[];
38
39
  readonly completions: TaskCompletionRecord[];
39
40
  readonly failures: TaskFailureRecord[];
41
+ /**
42
+ * The session's accepted history, one envelope per unique `message_id`. See
43
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
44
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
45
+ * only as a one-line pointer.
46
+ */
40
47
  readonly transcript: Envelope[];
48
+ /**
49
+ * Cardinality anomalies recorded while replaying this projection's accepted
50
+ * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
51
+ * for the canonical description; duplicated here only as a one-line
52
+ * pointer. No built-in detection populates this for Task mode.
53
+ */
54
+ readonly anomalies: ProjectionAnomaly[];
41
55
  phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
42
56
  commitment?: Record<string, unknown>;
57
+ /**
58
+ * `message_id`s already applied to this projection. See
59
+ * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
60
+ * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
61
+ * only as a one-line pointer.
62
+ */
63
+ private readonly seenMessageIds;
64
+ /**
65
+ * Apply one envelope to this projection's in-process state.
66
+ *
67
+ * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
68
+ * no acceptance marker). Canonical source: `schemas/conformance/README.md`
69
+ * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
70
+ * mode are documented once, on `BaseProjection.applyEnvelope`
71
+ * (`src/projections/base.ts`), and duplicated here only as a one-line
72
+ * pointer so six independent copies of the same prose cannot drift.
73
+ *
74
+ * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
75
+ * non-event — not appended to `transcript`, not passed to the `switch`.
76
+ * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
77
+ * citation; duplicated here only as a one-line pointer.
78
+ */
43
79
  applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
80
+ /**
81
+ * True once at least one `ProjectionAnomaly` has been recorded. See
82
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
83
+ * canonical description; duplicated here only as a one-line pointer.
84
+ */
85
+ get hasAnomalies(): boolean;
44
86
  get isCommitted(): boolean;
45
87
  get isPositiveOutcome(): boolean | undefined;
46
88
  getTask(taskId: string): TaskRecord | undefined;
@@ -2,17 +2,64 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
+ const logging_1 = require("../logging");
5
6
  class TaskProjection {
6
7
  tasks = new Map();
7
8
  updates = [];
8
9
  completions = [];
9
10
  failures = [];
11
+ /**
12
+ * The session's accepted history, one envelope per unique `message_id`. See
13
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
14
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
15
+ * only as a one-line pointer.
16
+ */
10
17
  transcript = [];
18
+ /**
19
+ * Cardinality anomalies recorded while replaying this projection's accepted
20
+ * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
21
+ * for the canonical description; duplicated here only as a one-line
22
+ * pointer. No built-in detection populates this for Task mode.
23
+ */
24
+ anomalies = [];
11
25
  phase = 'Pending';
12
26
  commitment;
27
+ /**
28
+ * `message_id`s already applied to this projection. See
29
+ * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
30
+ * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
31
+ * only as a one-line pointer.
32
+ */
33
+ seenMessageIds = new Set();
34
+ /**
35
+ * Apply one envelope to this projection's in-process state.
36
+ *
37
+ * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
38
+ * no acceptance marker). Canonical source: `schemas/conformance/README.md`
39
+ * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
40
+ * mode are documented once, on `BaseProjection.applyEnvelope`
41
+ * (`src/projections/base.ts`), and duplicated here only as a one-line
42
+ * pointer so six independent copies of the same prose cannot drift.
43
+ *
44
+ * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
45
+ * non-event — not appended to `transcript`, not passed to the `switch`.
46
+ * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
47
+ * citation; duplicated here only as a one-line pointer.
48
+ */
13
49
  applyEnvelope(envelope, protoRegistry) {
14
50
  if (envelope.mode !== constants_1.MODE_TASK)
15
51
  return;
52
+ if (envelope.messageId) {
53
+ if (this.seenMessageIds.has(envelope.messageId)) {
54
+ logging_1.logger.debug('projection redelivery ignored', {
55
+ messageId: envelope.messageId,
56
+ mode: envelope.mode,
57
+ messageType: envelope.messageType,
58
+ });
59
+ return;
60
+ }
61
+ this.seenMessageIds.add(envelope.messageId);
62
+ }
16
63
  this.transcript.push(envelope);
17
64
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
18
65
  switch (envelope.messageType) {
@@ -34,7 +81,30 @@ class TaskProjection {
34
81
  case 'TaskAccept': {
35
82
  const record = payload;
36
83
  const task = this.tasks.get(record.taskId);
37
- if (task) {
84
+ // RFC-MACP-0009 §5 rules 3/3a (`:69-71`): "Only one assignee may
85
+ // become active for the Session in base v1. The first accepted
86
+ // `TaskAccept` from any eligible participant designates that
87
+ // participant as the active assignee. Subsequent `TaskAccept`
88
+ // messages for the same session MUST be rejected if an active
89
+ // assignee is already designated." A conforming runtime rejects the
90
+ // second `TaskAccept` before it reaches accepted history; this guard
91
+ // makes the projection first-accept-wins too, so a rogue second one
92
+ // (e.g. an unfiltered transcript) cannot silently reassign the task.
93
+ //
94
+ // Rule 3c's policy-gated reassignment path (`allow_reassignment_on_reject`,
95
+ // RFC-MACP-0012 `:135`) is deliberately NOT modelled here:
96
+ // `TaskProjection` has no session-policy input today, and rule 3b
97
+ // makes an unconditional reassignment wrong by default ("`TaskAccept`
98
+ // is irrevocable unless policy explicitly permits reassignment").
99
+ // Plumbing policy into this projection needs its own decision (see
100
+ // plans/cross-repo/macp-sdk-typescript-conformance-and-transport.md,
101
+ // Phase 3 open question 4) before that path can be implemented.
102
+ //
103
+ // An anomaly would be recorded when this guard discards a second
104
+ // `TaskAccept`, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
105
+ // deliberately frozen pending cross-SDK agreement with
106
+ // macp-sdk-python.
107
+ if (task && task.assignee === undefined) {
38
108
  task.assignee = record.assignee;
39
109
  task.status = 'accepted';
40
110
  }
@@ -44,6 +114,11 @@ class TaskProjection {
44
114
  case 'TaskReject': {
45
115
  const record = payload;
46
116
  const task = this.tasks.get(record.taskId);
117
+ // Deliberately does not clear `task.assignee`: rule 3c's
118
+ // policy-gated reassignment path is not modelled here (see the
119
+ // `TaskAccept` case above), so `assignee` is treated as sticky once
120
+ // set, per rule 3b's default ("irrevocable unless policy explicitly
121
+ // permits reassignment").
47
122
  if (task)
48
123
  task.status = 'rejected';
49
124
  break;
@@ -87,6 +162,14 @@ class TaskProjection {
87
162
  break;
88
163
  }
89
164
  }
165
+ /**
166
+ * True once at least one `ProjectionAnomaly` has been recorded. See
167
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
168
+ * canonical description; duplicated here only as a one-line pointer.
169
+ */
170
+ get hasAnomalies() {
171
+ return this.anomalies.length > 0;
172
+ }
90
173
  get isCommitted() {
91
174
  return this.commitment !== undefined;
92
175
  }
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * Kept in sync with package.json by release-please (see the
5
5
  * `x-release-please-version` annotation).
6
6
  */
7
- export declare const VERSION = "0.7.1";
7
+ export declare const VERSION = "0.9.0";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@ exports.VERSION = void 0;
7
7
  * Kept in sync with package.json by release-please (see the
8
8
  * `x-release-please-version` annotation).
9
9
  */
10
- exports.VERSION = '0.7.1'; // x-release-please-version
10
+ exports.VERSION = '0.9.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macp-sdk-typescript",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "TypeScript SDK for the MACP Rust runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@grpc/grpc-js": "^1.14.3",
57
57
  "@grpc/proto-loader": "^0.8.0",
58
- "@multiagentcoordinationprotocol/proto": "^0.1.8",
58
+ "@multiagentcoordinationprotocol/proto": "^0.1.9",
59
59
  "protobufjs": "^8.6.5"
60
60
  },
61
61
  "devDependencies": {