macp-sdk-typescript 0.10.0 → 0.12.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 (50) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/README.md +2 -2
  3. package/dist/agent/index.d.ts +1 -1
  4. package/dist/agent/index.js +2 -1
  5. package/dist/agent/participant.d.ts +47 -4
  6. package/dist/agent/participant.js +88 -10
  7. package/dist/agent/strategies.js +23 -7
  8. package/dist/agent/transports.d.ts +19 -1
  9. package/dist/agent/transports.js +108 -66
  10. package/dist/auth.d.ts +15 -4
  11. package/dist/auth.js +23 -7
  12. package/dist/base-session.js +13 -3
  13. package/dist/client.js +58 -15
  14. package/dist/commitment-hash.d.ts +33 -3
  15. package/dist/commitment-hash.js +39 -5
  16. package/dist/decision.d.ts +1 -0
  17. package/dist/decision.js +11 -9
  18. package/dist/envelope.js +9 -1
  19. package/dist/handoff.d.ts +1 -0
  20. package/dist/handoff.js +19 -6
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +18 -7
  23. package/dist/policy.d.ts +54 -7
  24. package/dist/policy.js +143 -19
  25. package/dist/projections/base.d.ts +71 -14
  26. package/dist/projections/base.js +106 -16
  27. package/dist/projections/decision.d.ts +5 -49
  28. package/dist/projections/decision.js +30 -85
  29. package/dist/projections/handoff.d.ts +5 -48
  30. package/dist/projections/handoff.js +44 -93
  31. package/dist/projections/proposal.d.ts +5 -48
  32. package/dist/projections/proposal.js +21 -79
  33. package/dist/projections/quorum.d.ts +5 -49
  34. package/dist/projections/quorum.js +20 -82
  35. package/dist/projections/task.d.ts +31 -51
  36. package/dist/projections/task.js +49 -90
  37. package/dist/proposal.d.ts +1 -0
  38. package/dist/proposal.js +2 -2
  39. package/dist/proto-registry.d.ts +85 -5
  40. package/dist/proto-registry.js +101 -15
  41. package/dist/quorum.d.ts +1 -0
  42. package/dist/quorum.js +4 -4
  43. package/dist/task.d.ts +1 -0
  44. package/dist/task.js +24 -11
  45. package/dist/types.d.ts +14 -7
  46. package/dist/validation.d.ts +14 -0
  47. package/dist/validation.js +39 -5
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +6 -5
@@ -2,66 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DecisionProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
- const logging_1 = require("../logging");
6
- class DecisionProjection {
5
+ const base_1 = require("./base");
6
+ class DecisionProjection extends base_1.BaseProjection {
7
+ mode = constants_1.MODE_DECISION;
7
8
  proposals = new Map();
8
9
  evaluations = [];
9
10
  objections = [];
10
11
  votes = new Map();
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
- */
17
- transcript = [];
18
- /**
19
- * Cardinality anomalies recorded while replaying this projection's accepted
20
- * transcript (e.g. a duplicate `Vote` from the same sender for the same
21
- * `proposal_id`). See `BaseProjection.anomalies` (`src/projections/base.ts`)
22
- * for the canonical description; duplicated here only as a one-line
23
- * pointer.
24
- */
25
- anomalies = [];
26
12
  phase = 'Proposal';
27
- commitment;
28
- /**
29
- * `message_id`s already applied to this projection. See
30
- * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
31
- * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
32
- * only as a one-line pointer.
33
- */
34
- seenMessageIds = new Set();
35
- /**
36
- * Apply one envelope to this projection's in-process state.
37
- *
38
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
39
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
40
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
41
- * mode are documented once, on `BaseProjection.applyEnvelope`
42
- * (`src/projections/base.ts`), and duplicated here only as a one-line
43
- * pointer so six independent copies of the same prose cannot drift.
44
- *
45
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
46
- * non-event — not appended to `transcript`, not passed to the `switch`.
47
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
48
- * citation; duplicated here only as a one-line pointer.
49
- */
50
- applyEnvelope(envelope, protoRegistry) {
51
- if (envelope.mode !== constants_1.MODE_DECISION)
52
- return;
53
- if (envelope.messageId) {
54
- if (this.seenMessageIds.has(envelope.messageId)) {
55
- logging_1.logger.debug('projection redelivery ignored', {
56
- messageId: envelope.messageId,
57
- mode: envelope.mode,
58
- messageType: envelope.messageType,
59
- });
60
- return;
61
- }
62
- this.seenMessageIds.add(envelope.messageId);
63
- }
64
- this.transcript.push(envelope);
13
+ /** Handle a Decision-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
14
+ applyMode(envelope, protoRegistry) {
65
15
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
66
16
  switch (envelope.messageType) {
67
17
  case 'Proposal': {
@@ -87,6 +37,23 @@ class DecisionProjection {
87
37
  }
88
38
  case 'Vote': {
89
39
  const record = payload;
40
+ // RFC-MACP-0007 §5 rule 2: "Evaluation, Objection, and Vote MUST
41
+ // reference an existing proposal_id." A conforming runtime never
42
+ // admits a Vote for an unknown proposal_id into accepted history, but
43
+ // per the accepted-only input contract (`BaseProjection.applyEnvelope`)
44
+ // this guards the same class of unfiltered-transcript input that
45
+ // `HandoffAccept`/`HandoffDecline` already guard against (issue #119,
46
+ // mirrors issue #111's task.ts fix). Evaluation/Objection need no
47
+ // matching guard: they only append to audit arrays (see
48
+ // `docs/api/projections.md` "List accumulators and logical
49
+ // duplicates"), so an unknown-proposalId record there fabricates no
50
+ // Map state and moves no phase. Vote is different: without this
51
+ // guard, `this.votes.get(...) ?? new Map()` below would fabricate a
52
+ // new `votes` entry for an unknown proposalId, inflating
53
+ // `voteTotals()`/`majorityWinner()`'s denominators even though no
54
+ // known proposal exists.
55
+ if (!this.proposals.has(record.proposalId))
56
+ break;
90
57
  const bySender = this.votes.get(record.proposalId) ?? new Map();
91
58
  const kept = bySender.get(envelope.sender);
92
59
  if (kept !== undefined) {
@@ -94,7 +61,7 @@ class DecisionProjection {
94
61
  // runtime NACKs the duplicate (macp-runtime decision.rs), so reaching
95
62
  // here means the transcript was not filtered to a conforming
96
63
  // runtime's accepted history.
97
- const anomaly = {
64
+ this.recordAnomaly({
98
65
  kind: 'duplicate_vote',
99
66
  mode: envelope.mode,
100
67
  messageType: envelope.messageType,
@@ -102,9 +69,7 @@ class DecisionProjection {
102
69
  sender: envelope.sender,
103
70
  subjectId: record.proposalId,
104
71
  detail: `sender ${envelope.sender} already voted '${kept.vote}' on proposal ${record.proposalId}; discarded '${record.vote}'`,
105
- };
106
- this.anomalies.push(anomaly);
107
- logging_1.logger.warn('projection anomaly', anomaly);
72
+ });
108
73
  break;
109
74
  }
110
75
  bySender.set(envelope.sender, { ...record, sender: envelope.sender });
@@ -120,41 +85,21 @@ class DecisionProjection {
120
85
  // not a normative MACP term (it appears once, in passing, at
121
86
  // RFC-MACP-0012 `:211`); this guard is justified by session
122
87
  // terminality, not by a phase specification. An anomaly would be
123
- // recorded here too, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
124
- // deliberately frozen pending cross-SDK agreement with
125
- // macp-sdk-python.
88
+ // recorded here too, but unlike five of the six sites that were
89
+ // "frozen pending cross-SDK agreement" (issue #126/#128, now
90
+ // resolved for `duplicate_task_accept`/`settled_handoff` — see
91
+ // `base.ts`), this one is a session-terminality phase-regression
92
+ // guard, not a settlement discard, and remains genuinely undecided
93
+ // per #128's own explicit scoping. No behavior change here.
126
94
  if (this.phase !== 'Committed') {
127
95
  this.phase = 'Voting';
128
96
  }
129
97
  break;
130
98
  }
131
- case 'Commitment': {
132
- this.commitment = payload;
133
- this.phase = 'Committed';
134
- break;
135
- }
136
99
  default:
137
100
  break;
138
101
  }
139
102
  }
140
- /**
141
- * True once at least one `ProjectionAnomaly` has been recorded. See
142
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
143
- * canonical description; duplicated here only as a one-line pointer.
144
- */
145
- get hasAnomalies() {
146
- return this.anomalies.length > 0;
147
- }
148
- get isCommitted() {
149
- return this.commitment !== undefined;
150
- }
151
- get isPositiveOutcome() {
152
- if (!this.commitment)
153
- return undefined;
154
- const val = this.commitment.outcomePositive ??
155
- this.commitment.outcome_positive;
156
- return val !== undefined ? Boolean(val) : true;
157
- }
158
103
  voteTotals() {
159
104
  const totals = {};
160
105
  for (const [proposalId, senderVotes] of this.votes.entries()) {
@@ -1,4 +1,4 @@
1
- import type { ProjectionAnomaly } from './base';
1
+ import { BaseProjection } from './base';
2
2
  import type { Envelope } from '../types';
3
3
  import type { ProtoRegistry } from '../proto-registry';
4
4
  export interface HandoffRecord {
@@ -18,55 +18,12 @@ export interface HandoffRecord {
18
18
  */
19
19
  implicit?: boolean;
20
20
  }
21
- export declare class HandoffProjection {
21
+ export declare class HandoffProjection extends BaseProjection {
22
+ protected readonly mode = "macp.mode.handoff.v1";
22
23
  readonly handoffs: Map<string, HandoffRecord>;
23
- /**
24
- * The session's accepted history, one envelope per unique `message_id`. See
25
- * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
26
- * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
27
- * only as a one-line pointer.
28
- */
29
- readonly transcript: Envelope[];
30
- /**
31
- * Cardinality anomalies recorded while replaying this projection's accepted
32
- * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
33
- * for the canonical description; duplicated here only as a one-line
34
- * pointer. No built-in detection populates this for Handoff mode.
35
- */
36
- readonly anomalies: ProjectionAnomaly[];
37
24
  phase: 'Pending' | 'OfferPending' | 'ContextSharing' | 'Accepted' | 'Declined' | 'Committed';
38
- commitment?: Record<string, unknown>;
39
- /**
40
- * `message_id`s already applied to this projection. See
41
- * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
42
- * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
43
- * only as a one-line pointer.
44
- */
45
- private readonly seenMessageIds;
46
- /**
47
- * Apply one envelope to this projection's in-process state.
48
- *
49
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
50
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
51
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
52
- * mode are documented once, on `BaseProjection.applyEnvelope`
53
- * (`src/projections/base.ts`), and duplicated here only as a one-line
54
- * pointer so six independent copies of the same prose cannot drift.
55
- *
56
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
57
- * non-event — not appended to `transcript`, not passed to the `switch`.
58
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
59
- * citation; duplicated here only as a one-line pointer.
60
- */
61
- applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
62
- /**
63
- * True once at least one `ProjectionAnomaly` has been recorded. See
64
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
65
- * canonical description; duplicated here only as a one-line pointer.
66
- */
67
- get hasAnomalies(): boolean;
68
- get isCommitted(): boolean;
69
- get isPositiveOutcome(): boolean | undefined;
25
+ /** Handle a Handoff-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
26
+ protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
70
27
  getHandoff(handoffId: string): HandoffRecord | undefined;
71
28
  isAccepted(handoffId: string): boolean;
72
29
  isDeclined(handoffId: string): boolean;
@@ -2,62 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HandoffProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
- const logging_1 = require("../logging");
6
- class HandoffProjection {
5
+ const base_1 = require("./base");
6
+ class HandoffProjection extends base_1.BaseProjection {
7
+ mode = constants_1.MODE_HANDOFF;
7
8
  handoffs = new Map();
8
- /**
9
- * The session's accepted history, one envelope per unique `message_id`. See
10
- * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
11
- * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
12
- * only as a one-line pointer.
13
- */
14
- transcript = [];
15
- /**
16
- * Cardinality anomalies recorded while replaying this projection's accepted
17
- * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
18
- * for the canonical description; duplicated here only as a one-line
19
- * pointer. No built-in detection populates this for Handoff mode.
20
- */
21
- anomalies = [];
22
9
  phase = 'Pending';
23
- commitment;
24
- /**
25
- * `message_id`s already applied to this projection. See
26
- * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
27
- * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
28
- * only as a one-line pointer.
29
- */
30
- seenMessageIds = new Set();
31
- /**
32
- * Apply one envelope to this projection's in-process state.
33
- *
34
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
35
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
36
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
37
- * mode are documented once, on `BaseProjection.applyEnvelope`
38
- * (`src/projections/base.ts`), and duplicated here only as a one-line
39
- * pointer so six independent copies of the same prose cannot drift.
40
- *
41
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
42
- * non-event — not appended to `transcript`, not passed to the `switch`.
43
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
44
- * citation; duplicated here only as a one-line pointer.
45
- */
46
- applyEnvelope(envelope, protoRegistry) {
47
- if (envelope.mode !== constants_1.MODE_HANDOFF)
48
- return;
49
- if (envelope.messageId) {
50
- if (this.seenMessageIds.has(envelope.messageId)) {
51
- logging_1.logger.debug('projection redelivery ignored', {
52
- messageId: envelope.messageId,
53
- mode: envelope.mode,
54
- messageType: envelope.messageType,
55
- });
56
- return;
57
- }
58
- this.seenMessageIds.add(envelope.messageId);
59
- }
60
- this.transcript.push(envelope);
10
+ /** Handle a Handoff-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
11
+ applyMode(envelope, protoRegistry) {
61
12
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
62
13
  switch (envelope.messageType) {
63
14
  case 'HandoffOffer': {
@@ -76,6 +27,11 @@ class HandoffProjection {
76
27
  case 'HandoffContext': {
77
28
  const record = payload;
78
29
  const handoff = this.handoffs.get(record.handoffId);
30
+ // RFC-MACP-0010 §5 rule 2: HandoffContext MUST reference an existing
31
+ // handoff_id, so the phase transition belongs inside this guard too
32
+ // (issue #119, same shape as issue #111) — a HandoffContext for an
33
+ // unknown handoff_id must not advance `phase` past `'OfferPending'`
34
+ // while a different, real handoff is still pending.
79
35
  if (handoff) {
80
36
  // Per RFC-MACP-0010 §2.1: context after accept is permitted as supplementary docs.
81
37
  // Only update status if not already accepted/declined.
@@ -83,9 +39,9 @@ class HandoffProjection {
83
39
  handoff.status = 'context_sent';
84
40
  }
85
41
  handoff.contextContentType = record.contentType;
42
+ if (this.phase === 'OfferPending')
43
+ this.phase = 'ContextSharing';
86
44
  }
87
- if (this.phase === 'OfferPending')
88
- this.phase = 'ContextSharing';
89
45
  break;
90
46
  }
91
47
  case 'HandoffAccept': {
@@ -96,9 +52,10 @@ class HandoffProjection {
96
52
  // and HandoffDecline MUST reference an existing handoff_id." An
97
53
  // accept for an unknown/never-offered handoff_id is invalid input
98
54
  // (e.g. an unfiltered transcript) and MUST NOT mutate `phase` or
99
- // fabricate a handoff record. An anomaly would be recorded here,
100
- // but `ProjectionAnomalyKind` (`base.ts:8-9`) is deliberately
101
- // frozen pending cross-SDK agreement with macp-sdk-python.
55
+ // fabricate a handoff record. Confirmed (issue #126/#128): no
56
+ // anomaly is recorded for this case — an unknown `handoff_id` is
57
+ // not caller misuse (a projection that joined mid-session may never
58
+ // have observed the original offer).
102
59
  break;
103
60
  }
104
61
  // RFC-MACP-0010 §5 rule 4 (`:68`): "Once an offer has been
@@ -107,10 +64,7 @@ class HandoffProjection {
107
64
  // direction too: a `handoff_id` transitions
108
65
  // offered -> accepted | declined exactly once. Only settle if this
109
66
  // handoff hasn't already resolved — the same shape as the
110
- // HandoffContext guard just above. An anomaly would be recorded when
111
- // this guard discards a competing accept, but `ProjectionAnomalyKind`
112
- // (`base.ts:8-9`) is deliberately frozen pending cross-SDK agreement
113
- // with macp-sdk-python.
67
+ // HandoffContext guard just above.
114
68
  if (handoff.status === 'offered' || handoff.status === 'context_sent') {
115
69
  handoff.status = 'accepted';
116
70
  handoff.acceptedBy = record.acceptedBy;
@@ -120,6 +74,17 @@ class HandoffProjection {
120
74
  handoff.implicit = record.implicit ?? false;
121
75
  this.phase = 'Accepted';
122
76
  }
77
+ else {
78
+ this.recordAnomaly({
79
+ kind: 'settled_handoff',
80
+ mode: envelope.mode,
81
+ messageType: envelope.messageType,
82
+ messageId: envelope.messageId,
83
+ sender: envelope.sender,
84
+ subjectId: record.handoffId,
85
+ detail: `sender ${envelope.sender} attempted HandoffAccept for ${record.handoffId}, but it already settled as '${handoff.status}'`,
86
+ });
87
+ }
123
88
  break;
124
89
  }
125
90
  case 'HandoffDecline': {
@@ -130,51 +95,37 @@ class HandoffProjection {
130
95
  // and HandoffDecline MUST reference an existing handoff_id." A
131
96
  // decline for an unknown/never-offered handoff_id is invalid input
132
97
  // (e.g. an unfiltered transcript) and MUST NOT mutate `phase` or
133
- // fabricate a handoff record. An anomaly would be recorded here,
134
- // but `ProjectionAnomalyKind` (`base.ts:8-9`) is deliberately
135
- // frozen pending cross-SDK agreement with macp-sdk-python.
98
+ // fabricate a handoff record. Confirmed (issue #126/#128): no
99
+ // anomaly is recorded for this case — an unknown `handoff_id` is
100
+ // not caller misuse (a projection that joined mid-session may never
101
+ // have observed the original offer).
136
102
  break;
137
103
  }
138
104
  // RFC-MACP-0010 §5 rule 4 (`:68`) + §5.1(4) (`:113-116`): a
139
105
  // `handoff_id` settles once. A decline after the handoff already
140
- // settled (accepted or declined) is invalid and ignored. An anomaly
141
- // would be recorded here too, but `ProjectionAnomalyKind`
142
- // (`base.ts:8-9`) is deliberately frozen pending cross-SDK agreement
143
- // with macp-sdk-python.
106
+ // settled (accepted or declined) is invalid and ignored.
144
107
  if (handoff.status === 'offered' || handoff.status === 'context_sent') {
145
108
  handoff.status = 'declined';
146
109
  handoff.declinedBy = record.declinedBy;
147
110
  this.phase = 'Declined';
148
111
  }
149
- break;
150
- }
151
- case 'Commitment': {
152
- this.commitment = payload;
153
- this.phase = 'Committed';
112
+ else {
113
+ this.recordAnomaly({
114
+ kind: 'settled_handoff',
115
+ mode: envelope.mode,
116
+ messageType: envelope.messageType,
117
+ messageId: envelope.messageId,
118
+ sender: envelope.sender,
119
+ subjectId: record.handoffId,
120
+ detail: `sender ${envelope.sender} attempted HandoffDecline for ${record.handoffId}, but it already settled as '${handoff.status}'`,
121
+ });
122
+ }
154
123
  break;
155
124
  }
156
125
  default:
157
126
  break;
158
127
  }
159
128
  }
160
- /**
161
- * True once at least one `ProjectionAnomaly` has been recorded. See
162
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
163
- * canonical description; duplicated here only as a one-line pointer.
164
- */
165
- get hasAnomalies() {
166
- return this.anomalies.length > 0;
167
- }
168
- get isCommitted() {
169
- return this.commitment !== undefined;
170
- }
171
- get isPositiveOutcome() {
172
- if (!this.commitment)
173
- return undefined;
174
- const val = this.commitment.outcomePositive ??
175
- this.commitment.outcome_positive;
176
- return val !== undefined ? Boolean(val) : true;
177
- }
178
129
  getHandoff(handoffId) {
179
130
  return this.handoffs.get(handoffId);
180
131
  }
@@ -1,4 +1,4 @@
1
- import type { ProjectionAnomaly } from './base';
1
+ import { BaseProjection } from './base';
2
2
  import type { Envelope } from '../types';
3
3
  import type { ProtoRegistry } from '../proto-registry';
4
4
  export interface ProposalRecord {
@@ -21,7 +21,8 @@ export interface ProposalRejectRecord {
21
21
  reason?: string;
22
22
  sender: string;
23
23
  }
24
- export declare class ProposalProjection {
24
+ export declare class ProposalProjection extends BaseProjection {
25
+ protected readonly mode = "macp.mode.proposal.v1";
25
26
  readonly proposals: Map<string, ProposalRecord>;
26
27
  readonly accepts: ProposalAcceptRecord[];
27
28
  readonly rejections: ProposalRejectRecord[];
@@ -39,53 +40,9 @@ export declare class ProposalProjection {
39
40
  * acceptance set, and the same commitment eligibility."
40
41
  */
41
42
  private readonly latestAcceptBySender;
42
- /**
43
- * The session's accepted history, one envelope per unique `message_id`. See
44
- * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
45
- * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
46
- * only as a one-line pointer.
47
- */
48
- readonly transcript: Envelope[];
49
- /**
50
- * Cardinality anomalies recorded while replaying this projection's accepted
51
- * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
52
- * for the canonical description; duplicated here only as a one-line
53
- * pointer. No built-in detection populates this for Proposal mode.
54
- */
55
- readonly anomalies: ProjectionAnomaly[];
56
43
  phase: 'Negotiating' | 'TerminalRejected' | 'Committed';
57
- commitment?: Record<string, unknown>;
58
- /**
59
- * `message_id`s already applied to this projection. See
60
- * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
61
- * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
62
- * only as a one-line pointer.
63
- */
64
- private readonly seenMessageIds;
65
- /**
66
- * Apply one envelope to this projection's in-process state.
67
- *
68
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
69
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
70
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
71
- * mode are documented once, on `BaseProjection.applyEnvelope`
72
- * (`src/projections/base.ts`), and duplicated here only as a one-line
73
- * pointer so six independent copies of the same prose cannot drift.
74
- *
75
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
76
- * non-event — not appended to `transcript`, not passed to the `switch`.
77
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
78
- * citation; duplicated here only as a one-line pointer.
79
- */
80
- applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
81
- /**
82
- * True once at least one `ProjectionAnomaly` has been recorded. See
83
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
84
- * canonical description; duplicated here only as a one-line pointer.
85
- */
86
- get hasAnomalies(): boolean;
87
- get isCommitted(): boolean;
88
- get isPositiveOutcome(): boolean | undefined;
44
+ /** Handle a Proposal-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
45
+ protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
89
46
  activeProposals(): ProposalRecord[];
90
47
  latestProposal(): ProposalRecord | undefined;
91
48
  /**
@@ -2,8 +2,9 @@
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");
6
- class ProposalProjection {
5
+ const base_1 = require("./base");
6
+ class ProposalProjection extends base_1.BaseProjection {
7
+ mode = constants_1.MODE_PROPOSAL;
7
8
  proposals = new Map();
8
9
  accepts = [];
9
10
  rejections = [];
@@ -21,59 +22,9 @@ class ProposalProjection {
21
22
  * acceptance set, and the same commitment eligibility."
22
23
  */
23
24
  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
- */
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 = [];
38
25
  phase = 'Negotiating';
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
- */
62
- applyEnvelope(envelope, protoRegistry) {
63
- if (envelope.mode !== constants_1.MODE_PROPOSAL)
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
- }
76
- this.transcript.push(envelope);
26
+ /** Handle a Proposal-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
27
+ applyMode(envelope, protoRegistry) {
77
28
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
78
29
  switch (envelope.messageType) {
79
30
  case 'Proposal': {
@@ -118,11 +69,25 @@ class ProposalProjection {
118
69
  reason: record.reason,
119
70
  sender: envelope.sender,
120
71
  });
72
+ // RFC-MACP-0008 §5 rule 3 requires `Reject` to reference an existing
73
+ // proposal_id, so `this.phase` must only move to the terminal state
74
+ // when a proposal record for it actually exists here (issue #119,
75
+ // same shape as issue #111's TaskComplete/TaskFail fix) — otherwise
76
+ // an unfiltered transcript could flip `phase` to `'TerminalRejected'`
77
+ // (a TERMINAL_PHASES member, see `src/agent/participant.ts`) on a
78
+ // never-seen proposalId, ending a live Participant's run() loop for a
79
+ // session that never actually terminated. `this.rejections.push(...)`
80
+ // above stays unconditional — it's an append-only audit log (see
81
+ // `docs/api/projections.md` "List accumulators and logical
82
+ // duplicates"), so `isTerminallyRejected`/`hasTerminalRejection` can
83
+ // still report `true` for an unknown id even though `phase` does not
84
+ // move; that split is intentional, not a residual bug.
121
85
  if (terminal) {
122
86
  const proposal = this.proposals.get(record.proposalId);
123
- if (proposal)
87
+ if (proposal) {
124
88
  proposal.status = 'rejected';
125
- this.phase = 'TerminalRejected';
89
+ this.phase = 'TerminalRejected';
90
+ }
126
91
  }
127
92
  break;
128
93
  }
@@ -133,33 +98,10 @@ class ProposalProjection {
133
98
  proposal.status = 'withdrawn';
134
99
  break;
135
100
  }
136
- case 'Commitment': {
137
- this.commitment = payload;
138
- this.phase = 'Committed';
139
- break;
140
- }
141
101
  default:
142
102
  break;
143
103
  }
144
104
  }
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
- }
153
- get isCommitted() {
154
- return this.commitment !== undefined;
155
- }
156
- get isPositiveOutcome() {
157
- if (!this.commitment)
158
- return undefined;
159
- const val = this.commitment.outcomePositive ??
160
- this.commitment.outcome_positive;
161
- return val !== undefined ? Boolean(val) : true;
162
- }
163
105
  activeProposals() {
164
106
  return [...this.proposals.values()].filter((p) => p.status === 'open');
165
107
  }