macp-sdk-typescript 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/agent/transports.js +10 -1
- package/dist/agent/types.d.ts +12 -0
- package/dist/projections/base.d.ts +149 -0
- package/dist/projections/base.js +126 -0
- package/dist/projections/decision.d.ts +43 -0
- package/dist/projections/decision.js +75 -0
- package/dist/projections/handoff.d.ts +42 -0
- package/dist/projections/handoff.js +55 -0
- package/dist/projections/proposal.d.ts +42 -0
- package/dist/projections/proposal.js +55 -0
- package/dist/projections/quorum.d.ts +43 -0
- package/dist/projections/quorum.js +91 -5
- package/dist/projections/task.d.ts +42 -0
- package/dist/projections/task.js +55 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project uses [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.8.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.7.1...v0.8.0) (2026-08-31)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* **projections:** DecisionProjection and QuorumProjection now keep the FIRST accepted Vote/ballot per sender (per proposal_id / request_id) and discard later ones, where they previously kept the last. For a transcript containing a genuine duplicate, derived state changes — most consequentially QuorumProjection.hasQuorum(), which could previously be fabricated by a single sender voting twice under requiredApprovals: 1. Conforming transcripts are unaffected. The discarded message is recorded on the new `anomalies` surface.
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **projections:** the first accepted vote or ballot stands ([#61](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/61)) ([ac407c3](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/ac407c3767e785b2a1cf82e1ae735a9ebf1138be))
|
|
17
|
+
|
|
7
18
|
## [0.7.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.7.0...v0.7.1) (2026-08-31)
|
|
8
19
|
|
|
9
20
|
|
package/dist/agent/transports.js
CHANGED
|
@@ -88,6 +88,15 @@ class HttpTransportAdapter {
|
|
|
88
88
|
if (itemSeq > this.lastSeq) {
|
|
89
89
|
this.lastSeq = itemSeq;
|
|
90
90
|
}
|
|
91
|
+
// Normalize message_id -> messageId before yielding: without
|
|
92
|
+
// this, `raw` is the raw snake_case JSON straight off the wire,
|
|
93
|
+
// so `raw.messageId` is `undefined` and the projection's
|
|
94
|
+
// redelivery guard (`if (envelope.messageId)`,
|
|
95
|
+
// `src/projections/base.ts`) short-circuits — every polled
|
|
96
|
+
// envelope looks id-less and skips dedup entirely, leaving HTTP
|
|
97
|
+
// polling silently unprotected against RFC-MACP-0006 §3.2
|
|
98
|
+
// redelivery.
|
|
99
|
+
const messageId = item.message_id ?? item.messageId ?? '';
|
|
91
100
|
yield {
|
|
92
101
|
messageType: item.message_type ?? item.messageType ?? '',
|
|
93
102
|
sender: item.sender ?? '',
|
|
@@ -95,7 +104,7 @@ class HttpTransportAdapter {
|
|
|
95
104
|
? item.payload
|
|
96
105
|
: this.tryParsePayload(item.payload),
|
|
97
106
|
proposalId: item.proposal_id ?? item.proposalId,
|
|
98
|
-
raw: item,
|
|
107
|
+
raw: { ...item, messageId },
|
|
99
108
|
seq: this.seq++,
|
|
100
109
|
};
|
|
101
110
|
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectionAnomaly } from '../projections/base';
|
|
1
2
|
import type { Envelope } from '../types';
|
|
2
3
|
export interface IncomingMessage {
|
|
3
4
|
messageType: string;
|
|
@@ -62,6 +63,17 @@ export interface ParticipantLike {
|
|
|
62
63
|
export interface ProjectionLike {
|
|
63
64
|
readonly phase: string;
|
|
64
65
|
readonly transcript: Envelope[];
|
|
66
|
+
/**
|
|
67
|
+
* Cardinality anomalies observed and discarded by the projection, in transcript order.
|
|
68
|
+
*
|
|
69
|
+
* OPTIONAL, and it must STAY optional. `ProjectionLike` is structurally
|
|
70
|
+
* implementable by consumers — test fakes, custom transports, alternative
|
|
71
|
+
* projection implementations — none of which this SDK controls. Making this
|
|
72
|
+
* member required would break every such implementer at compile time for a
|
|
73
|
+
* field most of them have no reason to provide. The guard below turns a
|
|
74
|
+
* later "tightening" into a build failure rather than a silent break.
|
|
75
|
+
*/
|
|
76
|
+
readonly anomalies?: readonly ProjectionAnomaly[];
|
|
65
77
|
}
|
|
66
78
|
export type MessageHandler = (event: IncomingMessage, ctx: HandlerContext) => void | Promise<void>;
|
|
67
79
|
export type TerminalHandler = (result: TerminalResult) => void | Promise<void>;
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import type { ProtoRegistry } from '../proto-registry';
|
|
2
2
|
import type { Envelope } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* The kind of cardinality anomaly a projection recorded while replaying an
|
|
5
|
+
* accepted transcript. Cross-SDK frozen contract (`macp-sdk-python` adopted
|
|
6
|
+
* the identical field set, snake_case there) — do not add a kind without
|
|
7
|
+
* cross-SDK agreement.
|
|
8
|
+
*/
|
|
9
|
+
export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot';
|
|
10
|
+
/**
|
|
11
|
+
* A recorded observation that a second distinct `Vote` from this sender for
|
|
12
|
+
* this proposal (RFC-MACP-0007 §5.3), or a second distinct ballot across
|
|
13
|
+
* `Approve`/`Reject`/`Abstain` from this sender for this request
|
|
14
|
+
* (RFC-MACP-0011 §5 rule 3), was observed and discarded — the first stands.
|
|
15
|
+
* (RFC-MACP-0011 §5 rule 3 caps *how many* ballots and is silent on *which of
|
|
16
|
+
* two* stands; first-ballot-wins here is parity with RFC-MACP-0007 §5.3 plus
|
|
17
|
+
* runtime-enforced behaviour.)
|
|
18
|
+
*
|
|
19
|
+
* **Deliberately narrow claim — do not overclaim "this transcript violates
|
|
20
|
+
* the spec".** A projection cannot tell a genuinely non-conforming source
|
|
21
|
+
* from a conforming source fed through an unfiltered loader, because
|
|
22
|
+
* acceptance is not a wire property (`Envelope` carries no acceptance
|
|
23
|
+
* marker — see `applyEnvelope`'s "Input contract: accepted-only" docblock).
|
|
24
|
+
* An anomaly records only what was observed and what this projection did
|
|
25
|
+
* about it (discarded the second one, kept the first); it does not, and
|
|
26
|
+
* structurally cannot, assert that a conforming runtime was actually
|
|
27
|
+
* bypassed. Both `macp-sdk-python` and this SDK state it in these terms —
|
|
28
|
+
* agreed cross-SDK wording, not a style preference.
|
|
29
|
+
*
|
|
30
|
+
* Cross-SDK frozen contract, seven fields (camelCase here, snake_case in
|
|
31
|
+
* `macp-sdk-python`). Do not add, rename, or remove a field without
|
|
32
|
+
* cross-SDK agreement.
|
|
33
|
+
*
|
|
34
|
+
* Only `DecisionProjection` and `QuorumProjection` populate this today.
|
|
35
|
+
*/
|
|
36
|
+
export interface ProjectionAnomaly {
|
|
37
|
+
kind: ProjectionAnomalyKind;
|
|
38
|
+
mode: string;
|
|
39
|
+
messageType: string;
|
|
40
|
+
messageId: string;
|
|
41
|
+
sender: string;
|
|
42
|
+
/** `proposal_id` (Decision) or `request_id` (Quorum) the duplicate targeted. */
|
|
43
|
+
subjectId: string;
|
|
44
|
+
/**
|
|
45
|
+
* Human-readable detail. For a cross-type Quorum duplicate, `messageType`
|
|
46
|
+
* is what distinguishes the discarded ballot's type (Approve/Reject/Abstain)
|
|
47
|
+
* from the kept one — `detail` should name both.
|
|
48
|
+
*/
|
|
49
|
+
detail: string;
|
|
50
|
+
}
|
|
3
51
|
/**
|
|
4
52
|
* Abstract base for in-process mode-state tracking — parity with python-sdk's
|
|
5
53
|
* `macp_sdk.base_projection.BaseProjection`. Maintains a shared transcript,
|
|
@@ -8,13 +56,114 @@ import type { Envelope } from '../types';
|
|
|
8
56
|
* handled here so custom modes get the common lifecycle for free.
|
|
9
57
|
*/
|
|
10
58
|
export declare abstract class BaseProjection {
|
|
59
|
+
/**
|
|
60
|
+
* The session's accepted history as a conforming runtime holds it: one
|
|
61
|
+
* envelope per unique `message_id`, in application order. Redelivered
|
|
62
|
+
* envelopes (same `message_id` — expected under MACP's at-least-once
|
|
63
|
+
* delivery, RFC-MACP-0001 §8, and normatively idempotent per RFC-MACP-0006
|
|
64
|
+
* §3.2 Redelivery) and envelopes for other modes are not appended; a
|
|
65
|
+
* redelivery is logged at debug level and has no effect on projection
|
|
66
|
+
* state.
|
|
67
|
+
*/
|
|
11
68
|
readonly transcript: Envelope[];
|
|
12
69
|
phase: string;
|
|
13
70
|
commitment?: Record<string, unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* `message_id`s already applied to this projection, used to make
|
|
73
|
+
* `applyEnvelope` idempotent under redelivery (RFC-MACP-0006 §3.2:
|
|
74
|
+
* "A consumer that accumulates state per envelope ... MUST be idempotent
|
|
75
|
+
* with respect to `message_id`", `:136`). Deliberately unbounded: this
|
|
76
|
+
* class already retains every full envelope (payload bytes included) in
|
|
77
|
+
* `transcript`, so a set of id strings is strictly dominated by that; the
|
|
78
|
+
* runtime keeps an unbounded per-message dedup set itself
|
|
79
|
+
* (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
|
|
80
|
+
* `crates/macp-core/src/session.rs:69`), and sessions are TTL-bounded by
|
|
81
|
+
* protocol.
|
|
82
|
+
*/
|
|
83
|
+
private readonly seenMessageIds;
|
|
84
|
+
/**
|
|
85
|
+
* Cardinality anomalies recorded while replaying this projection's
|
|
86
|
+
* accepted transcript (e.g. a duplicate vote or ballot from the same
|
|
87
|
+
* sender). See `ProjectionAnomaly`. Empty unless a subclass calls
|
|
88
|
+
* `recordAnomaly`. NOTE: as of this SDK's built-in modes, nothing calls
|
|
89
|
+
* `recordAnomaly` — `DecisionProjection` and `QuorumProjection` do not
|
|
90
|
+
* extend `BaseProjection` (see the class docblock) and inline their own
|
|
91
|
+
* two lines instead. This field and `recordAnomaly` exist for ext-mode
|
|
92
|
+
* `BaseProjection` subclasses outside this repo.
|
|
93
|
+
*/
|
|
94
|
+
readonly anomalies: ProjectionAnomaly[];
|
|
14
95
|
protected abstract readonly mode: string;
|
|
96
|
+
/**
|
|
97
|
+
* True once at least one `ProjectionAnomaly` has been recorded — i.e. this
|
|
98
|
+
* projection observed and discarded a duplicate vote or ballot from the
|
|
99
|
+
* same sender (see `anomalies` above). This getter is exactly
|
|
100
|
+
* `anomalies.length > 0`; it never asserts "this transcript violates the
|
|
101
|
+
* spec" (see `ProjectionAnomaly`'s docblock, and `applyEnvelope`'s "Input
|
|
102
|
+
* contract: accepted-only" section above, for why a projection cannot make
|
|
103
|
+
* that claim).
|
|
104
|
+
*/
|
|
105
|
+
get hasAnomalies(): boolean;
|
|
15
106
|
get isCommitted(): boolean;
|
|
16
107
|
get isPositiveOutcome(): boolean | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Apply one envelope to this projection's in-process state.
|
|
110
|
+
*
|
|
111
|
+
* ## Input contract: accepted-only
|
|
112
|
+
*
|
|
113
|
+
* `applyEnvelope` assumes every envelope it is given was **accepted** by a
|
|
114
|
+
* conforming MACP runtime — i.e. it is part of the session's authoritative
|
|
115
|
+
* accepted history, never a submission the runtime rejected. This is a
|
|
116
|
+
* **caller-maintained invariant**, not something this method can check:
|
|
117
|
+
* `Envelope` (`src/types.ts`) carries no acceptance marker on the wire, so a
|
|
118
|
+
* projection has no field to inspect to tell an accepted envelope from a
|
|
119
|
+
* rejected one. Canonical source, verbatim: `schemas/conformance/README.md`
|
|
120
|
+
* "Notes:" — "SDKs replay only `accept` messages through their projections
|
|
121
|
+
* (reject-path fixtures replay their accepted *prefix*)." The rule this
|
|
122
|
+
* supports is spelled out in RFC-MACP-0007 §5 rule 3 (this repo's shorthand:
|
|
123
|
+
* "§5.3" — the RFC has no literal `### 5.3` heading) — "the first accepted
|
|
124
|
+
* `Vote` stands" — and RFC-MACP-0011 §5 rule 3 — at most one ballot per
|
|
125
|
+
* participant. Both describe what a conforming runtime enforces on ACCEPTED
|
|
126
|
+
* history; a projection reconstructing that history is only correct when fed
|
|
127
|
+
* the same set the runtime held.
|
|
128
|
+
*
|
|
129
|
+
* This SDK's own send paths uphold the invariant already: each of the five
|
|
130
|
+
* built-in mode sessions has its own private `sendAndTrack` that calls
|
|
131
|
+
* `this.projection.applyEnvelope(...)` only `if (ack.ok)`. `BaseSession`'s
|
|
132
|
+
* own `sendAndTrack` (`src/base-session.ts`) — the equivalent path for
|
|
133
|
+
* custom modes built on the ext-mode extension point — does the same. The
|
|
134
|
+
* conformance harness upholds it too: its `acceptedMessages` filter in
|
|
135
|
+
* `tests/conformance/conformance.test.ts` keeps only messages with
|
|
136
|
+
* `expect === 'accept'` before replay.
|
|
137
|
+
*
|
|
138
|
+
* **Failure mode if the contract is violated:** wiring this method to raw
|
|
139
|
+
* captured or hand-built traffic — including envelopes a runtime rejected —
|
|
140
|
+
* replays a timeline that no conforming runtime ever held, fabricating
|
|
141
|
+
* projection state. Concretely: a session whose only `Commitment` was
|
|
142
|
+
* rejected can still end up with `isCommitted === true` and
|
|
143
|
+
* `phase === 'Committed'` if that rejected envelope is replayed anyway. See
|
|
144
|
+
* `tests/unit/projections/accepted-only-contract.test.ts` for an executable
|
|
145
|
+
* demonstration, both with the corruption present and with it avoided.
|
|
146
|
+
*
|
|
147
|
+
* @param envelope - MUST be an envelope the runtime accepted; the caller is
|
|
148
|
+
* responsible for filtering out rejected submissions before calling this
|
|
149
|
+
* method. Envelopes for a different `mode` are ignored (the `if
|
|
150
|
+
* (envelope.mode !== this.mode) return;` guard below, in this method's own
|
|
151
|
+
* body), but that check is unrelated to this contract and does not
|
|
152
|
+
* substitute for it.
|
|
153
|
+
*/
|
|
17
154
|
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
18
155
|
/** Handle a mode-specific (non-Commitment) envelope. */
|
|
19
156
|
protected abstract applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
157
|
+
/**
|
|
158
|
+
* Record a cardinality anomaly: push it onto `anomalies` and emit it via
|
|
159
|
+
* `logger.warn('projection anomaly', anomaly)`. The `anomalies` array is
|
|
160
|
+
* the canonical, cross-SDK-agreed semantic; the log call is explicitly
|
|
161
|
+
* NON-contractual observability and may differ per SDK. `logger.warn` is
|
|
162
|
+
* visible by default (this SDK's default log level is `warn`,
|
|
163
|
+
* `src/logging.ts`) — observing and discarding a duplicate is deliberately
|
|
164
|
+
* not silent. To quiet it without losing the `anomalies` array, configure a
|
|
165
|
+
* higher level: `configureLogging({ level: 'error' })` or the
|
|
166
|
+
* `MACP_LOG_LEVEL` environment variable.
|
|
167
|
+
*/
|
|
168
|
+
protected recordAnomaly(anomaly: ProjectionAnomaly): void;
|
|
20
169
|
}
|
package/dist/projections/base.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BaseProjection = void 0;
|
|
4
|
+
const logging_1 = require("../logging");
|
|
4
5
|
/**
|
|
5
6
|
* Abstract base for in-process mode-state tracking — parity with python-sdk's
|
|
6
7
|
* `macp_sdk.base_projection.BaseProjection`. Maintains a shared transcript,
|
|
@@ -9,9 +10,54 @@ exports.BaseProjection = void 0;
|
|
|
9
10
|
* handled here so custom modes get the common lifecycle for free.
|
|
10
11
|
*/
|
|
11
12
|
class BaseProjection {
|
|
13
|
+
/**
|
|
14
|
+
* The session's accepted history as a conforming runtime holds it: one
|
|
15
|
+
* envelope per unique `message_id`, in application order. Redelivered
|
|
16
|
+
* envelopes (same `message_id` — expected under MACP's at-least-once
|
|
17
|
+
* delivery, RFC-MACP-0001 §8, and normatively idempotent per RFC-MACP-0006
|
|
18
|
+
* §3.2 Redelivery) and envelopes for other modes are not appended; a
|
|
19
|
+
* redelivery is logged at debug level and has no effect on projection
|
|
20
|
+
* state.
|
|
21
|
+
*/
|
|
12
22
|
transcript = [];
|
|
13
23
|
phase = '';
|
|
14
24
|
commitment;
|
|
25
|
+
/**
|
|
26
|
+
* `message_id`s already applied to this projection, used to make
|
|
27
|
+
* `applyEnvelope` idempotent under redelivery (RFC-MACP-0006 §3.2:
|
|
28
|
+
* "A consumer that accumulates state per envelope ... MUST be idempotent
|
|
29
|
+
* with respect to `message_id`", `:136`). Deliberately unbounded: this
|
|
30
|
+
* class already retains every full envelope (payload bytes included) in
|
|
31
|
+
* `transcript`, so a set of id strings is strictly dominated by that; the
|
|
32
|
+
* runtime keeps an unbounded per-message dedup set itself
|
|
33
|
+
* (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
|
|
34
|
+
* `crates/macp-core/src/session.rs:69`), and sessions are TTL-bounded by
|
|
35
|
+
* protocol.
|
|
36
|
+
*/
|
|
37
|
+
seenMessageIds = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Cardinality anomalies recorded while replaying this projection's
|
|
40
|
+
* accepted transcript (e.g. a duplicate vote or ballot from the same
|
|
41
|
+
* sender). See `ProjectionAnomaly`. Empty unless a subclass calls
|
|
42
|
+
* `recordAnomaly`. NOTE: as of this SDK's built-in modes, nothing calls
|
|
43
|
+
* `recordAnomaly` — `DecisionProjection` and `QuorumProjection` do not
|
|
44
|
+
* extend `BaseProjection` (see the class docblock) and inline their own
|
|
45
|
+
* two lines instead. This field and `recordAnomaly` exist for ext-mode
|
|
46
|
+
* `BaseProjection` subclasses outside this repo.
|
|
47
|
+
*/
|
|
48
|
+
anomalies = [];
|
|
49
|
+
/**
|
|
50
|
+
* True once at least one `ProjectionAnomaly` has been recorded — i.e. this
|
|
51
|
+
* projection observed and discarded a duplicate vote or ballot from the
|
|
52
|
+
* same sender (see `anomalies` above). This getter is exactly
|
|
53
|
+
* `anomalies.length > 0`; it never asserts "this transcript violates the
|
|
54
|
+
* spec" (see `ProjectionAnomaly`'s docblock, and `applyEnvelope`'s "Input
|
|
55
|
+
* contract: accepted-only" section above, for why a projection cannot make
|
|
56
|
+
* that claim).
|
|
57
|
+
*/
|
|
58
|
+
get hasAnomalies() {
|
|
59
|
+
return this.anomalies.length > 0;
|
|
60
|
+
}
|
|
15
61
|
get isCommitted() {
|
|
16
62
|
return this.commitment !== undefined;
|
|
17
63
|
}
|
|
@@ -22,9 +68,74 @@ class BaseProjection {
|
|
|
22
68
|
this.commitment.outcome_positive;
|
|
23
69
|
return val !== undefined ? Boolean(val) : true;
|
|
24
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Apply one envelope to this projection's in-process state.
|
|
73
|
+
*
|
|
74
|
+
* ## Input contract: accepted-only
|
|
75
|
+
*
|
|
76
|
+
* `applyEnvelope` assumes every envelope it is given was **accepted** by a
|
|
77
|
+
* conforming MACP runtime — i.e. it is part of the session's authoritative
|
|
78
|
+
* accepted history, never a submission the runtime rejected. This is a
|
|
79
|
+
* **caller-maintained invariant**, not something this method can check:
|
|
80
|
+
* `Envelope` (`src/types.ts`) carries no acceptance marker on the wire, so a
|
|
81
|
+
* projection has no field to inspect to tell an accepted envelope from a
|
|
82
|
+
* rejected one. Canonical source, verbatim: `schemas/conformance/README.md`
|
|
83
|
+
* "Notes:" — "SDKs replay only `accept` messages through their projections
|
|
84
|
+
* (reject-path fixtures replay their accepted *prefix*)." The rule this
|
|
85
|
+
* supports is spelled out in RFC-MACP-0007 §5 rule 3 (this repo's shorthand:
|
|
86
|
+
* "§5.3" — the RFC has no literal `### 5.3` heading) — "the first accepted
|
|
87
|
+
* `Vote` stands" — and RFC-MACP-0011 §5 rule 3 — at most one ballot per
|
|
88
|
+
* participant. Both describe what a conforming runtime enforces on ACCEPTED
|
|
89
|
+
* history; a projection reconstructing that history is only correct when fed
|
|
90
|
+
* the same set the runtime held.
|
|
91
|
+
*
|
|
92
|
+
* This SDK's own send paths uphold the invariant already: each of the five
|
|
93
|
+
* built-in mode sessions has its own private `sendAndTrack` that calls
|
|
94
|
+
* `this.projection.applyEnvelope(...)` only `if (ack.ok)`. `BaseSession`'s
|
|
95
|
+
* own `sendAndTrack` (`src/base-session.ts`) — the equivalent path for
|
|
96
|
+
* custom modes built on the ext-mode extension point — does the same. The
|
|
97
|
+
* conformance harness upholds it too: its `acceptedMessages` filter in
|
|
98
|
+
* `tests/conformance/conformance.test.ts` keeps only messages with
|
|
99
|
+
* `expect === 'accept'` before replay.
|
|
100
|
+
*
|
|
101
|
+
* **Failure mode if the contract is violated:** wiring this method to raw
|
|
102
|
+
* captured or hand-built traffic — including envelopes a runtime rejected —
|
|
103
|
+
* replays a timeline that no conforming runtime ever held, fabricating
|
|
104
|
+
* projection state. Concretely: a session whose only `Commitment` was
|
|
105
|
+
* rejected can still end up with `isCommitted === true` and
|
|
106
|
+
* `phase === 'Committed'` if that rejected envelope is replayed anyway. See
|
|
107
|
+
* `tests/unit/projections/accepted-only-contract.test.ts` for an executable
|
|
108
|
+
* demonstration, both with the corruption present and with it avoided.
|
|
109
|
+
*
|
|
110
|
+
* @param envelope - MUST be an envelope the runtime accepted; the caller is
|
|
111
|
+
* responsible for filtering out rejected submissions before calling this
|
|
112
|
+
* method. Envelopes for a different `mode` are ignored (the `if
|
|
113
|
+
* (envelope.mode !== this.mode) return;` guard below, in this method's own
|
|
114
|
+
* body), but that check is unrelated to this contract and does not
|
|
115
|
+
* substitute for it.
|
|
116
|
+
*/
|
|
25
117
|
applyEnvelope(envelope, protoRegistry) {
|
|
26
118
|
if (envelope.mode !== this.mode)
|
|
27
119
|
return;
|
|
120
|
+
// RFC-MACP-0006 §3.2 Redelivery (`:94`, `:134`-`:136`): a runtime MAY echo
|
|
121
|
+
// back accepted envelopes, a redelivery MUST NOT advance sequence position
|
|
122
|
+
// or count a second time against cardinality, and a consumer that
|
|
123
|
+
// accumulates state per envelope MUST be idempotent w.r.t. `message_id`.
|
|
124
|
+
// The `if (envelope.messageId)` guard is mandatory: an empty/absent id
|
|
125
|
+
// must apply WITHOUT dedup, or a feed of empty-id envelopes collapses to
|
|
126
|
+
// one. This is architecturally expected (RFC-MACP-0001:306 at-least-once
|
|
127
|
+
// delivery), not an anomaly — hence `debug`, never `warn`.
|
|
128
|
+
if (envelope.messageId) {
|
|
129
|
+
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
130
|
+
logging_1.logger.debug('projection redelivery ignored', {
|
|
131
|
+
messageId: envelope.messageId,
|
|
132
|
+
mode: envelope.mode,
|
|
133
|
+
messageType: envelope.messageType,
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.seenMessageIds.add(envelope.messageId);
|
|
138
|
+
}
|
|
28
139
|
this.transcript.push(envelope);
|
|
29
140
|
if (envelope.messageType === 'Commitment') {
|
|
30
141
|
this.commitment = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
@@ -33,5 +144,20 @@ class BaseProjection {
|
|
|
33
144
|
}
|
|
34
145
|
this.applyMode(envelope, protoRegistry);
|
|
35
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Record a cardinality anomaly: push it onto `anomalies` and emit it via
|
|
149
|
+
* `logger.warn('projection anomaly', anomaly)`. The `anomalies` array is
|
|
150
|
+
* the canonical, cross-SDK-agreed semantic; the log call is explicitly
|
|
151
|
+
* NON-contractual observability and may differ per SDK. `logger.warn` is
|
|
152
|
+
* visible by default (this SDK's default log level is `warn`,
|
|
153
|
+
* `src/logging.ts`) — observing and discarding a duplicate is deliberately
|
|
154
|
+
* not silent. To quiet it without losing the `anomalies` array, configure a
|
|
155
|
+
* higher level: `configureLogging({ level: 'error' })` or the
|
|
156
|
+
* `MACP_LOG_LEVEL` environment variable.
|
|
157
|
+
*/
|
|
158
|
+
recordAnomaly(anomaly) {
|
|
159
|
+
this.anomalies.push(anomaly);
|
|
160
|
+
logging_1.logger.warn('projection anomaly', anomaly);
|
|
161
|
+
}
|
|
36
162
|
}
|
|
37
163
|
exports.BaseProjection = BaseProjection;
|
|
@@ -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 DecisionProposalRecord {
|
|
@@ -30,10 +31,52 @@ export declare class DecisionProjection {
|
|
|
30
31
|
readonly evaluations: DecisionEvaluationRecord[];
|
|
31
32
|
readonly objections: DecisionObjectionRecord[];
|
|
32
33
|
readonly votes: Map<string, Map<string, DecisionVoteRecord>>;
|
|
34
|
+
/**
|
|
35
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
36
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
37
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
38
|
+
* only as a one-line pointer.
|
|
39
|
+
*/
|
|
33
40
|
readonly transcript: Envelope[];
|
|
41
|
+
/**
|
|
42
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
43
|
+
* transcript (e.g. a duplicate `Vote` from the same sender for the same
|
|
44
|
+
* `proposal_id`). See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
45
|
+
* for the canonical description; duplicated here only as a one-line
|
|
46
|
+
* pointer.
|
|
47
|
+
*/
|
|
48
|
+
readonly anomalies: ProjectionAnomaly[];
|
|
34
49
|
phase: 'Proposal' | 'Evaluation' | 'Voting' | 'Committed';
|
|
35
50
|
commitment?: Record<string, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* `message_id`s already applied to this projection. See
|
|
53
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
54
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
55
|
+
* only as a one-line pointer.
|
|
56
|
+
*/
|
|
57
|
+
private readonly seenMessageIds;
|
|
58
|
+
/**
|
|
59
|
+
* Apply one envelope to this projection's in-process state.
|
|
60
|
+
*
|
|
61
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
62
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
63
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
64
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
65
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
66
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
67
|
+
*
|
|
68
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
69
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
70
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
71
|
+
* citation; duplicated here only as a one-line pointer.
|
|
72
|
+
*/
|
|
36
73
|
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
74
|
+
/**
|
|
75
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
76
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
77
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
78
|
+
*/
|
|
79
|
+
get hasAnomalies(): boolean;
|
|
37
80
|
get isCommitted(): boolean;
|
|
38
81
|
get isPositiveOutcome(): boolean | undefined;
|
|
39
82
|
voteTotals(): Record<string, number>;
|
|
@@ -2,17 +2,65 @@
|
|
|
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");
|
|
5
6
|
class DecisionProjection {
|
|
6
7
|
proposals = new Map();
|
|
7
8
|
evaluations = [];
|
|
8
9
|
objections = [];
|
|
9
10
|
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
|
+
*/
|
|
10
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 = [];
|
|
11
26
|
phase = 'Proposal';
|
|
12
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
|
+
*/
|
|
13
50
|
applyEnvelope(envelope, protoRegistry) {
|
|
14
51
|
if (envelope.mode !== constants_1.MODE_DECISION)
|
|
15
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
|
+
}
|
|
16
64
|
this.transcript.push(envelope);
|
|
17
65
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
18
66
|
switch (envelope.messageType) {
|
|
@@ -40,6 +88,25 @@ class DecisionProjection {
|
|
|
40
88
|
case 'Vote': {
|
|
41
89
|
const record = payload;
|
|
42
90
|
const bySender = this.votes.get(record.proposalId) ?? new Map();
|
|
91
|
+
const kept = bySender.get(envelope.sender);
|
|
92
|
+
if (kept !== undefined) {
|
|
93
|
+
// RFC-MACP-0007 §5 item 3: the first accepted Vote stands. A conforming
|
|
94
|
+
// runtime NACKs the duplicate (macp-runtime decision.rs), so reaching
|
|
95
|
+
// here means the transcript was not filtered to a conforming
|
|
96
|
+
// runtime's accepted history.
|
|
97
|
+
const anomaly = {
|
|
98
|
+
kind: 'duplicate_vote',
|
|
99
|
+
mode: envelope.mode,
|
|
100
|
+
messageType: envelope.messageType,
|
|
101
|
+
messageId: envelope.messageId,
|
|
102
|
+
sender: envelope.sender,
|
|
103
|
+
subjectId: record.proposalId,
|
|
104
|
+
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);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
43
110
|
bySender.set(envelope.sender, { ...record, sender: envelope.sender });
|
|
44
111
|
this.votes.set(record.proposalId, bySender);
|
|
45
112
|
this.phase = 'Voting';
|
|
@@ -54,6 +121,14 @@ class DecisionProjection {
|
|
|
54
121
|
break;
|
|
55
122
|
}
|
|
56
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
126
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
127
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
128
|
+
*/
|
|
129
|
+
get hasAnomalies() {
|
|
130
|
+
return this.anomalies.length > 0;
|
|
131
|
+
}
|
|
57
132
|
get isCommitted() {
|
|
58
133
|
return this.commitment !== undefined;
|
|
59
134
|
}
|
|
@@ -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 HandoffRecord {
|
|
@@ -19,10 +20,51 @@ export interface HandoffRecord {
|
|
|
19
20
|
}
|
|
20
21
|
export declare class HandoffProjection {
|
|
21
22
|
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
|
+
*/
|
|
22
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[];
|
|
23
37
|
phase: 'Pending' | 'OfferPending' | 'ContextSharing' | 'Accepted' | 'Declined' | 'Committed';
|
|
24
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
|
+
*/
|
|
25
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;
|
|
26
68
|
get isCommitted(): boolean;
|
|
27
69
|
get isPositiveOutcome(): boolean | undefined;
|
|
28
70
|
getHandoff(handoffId: string): HandoffRecord | undefined;
|
|
@@ -2,14 +2,61 @@
|
|
|
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");
|
|
5
6
|
class HandoffProjection {
|
|
6
7
|
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
|
+
*/
|
|
7
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 = [];
|
|
8
22
|
phase = 'Pending';
|
|
9
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
|
+
*/
|
|
10
46
|
applyEnvelope(envelope, protoRegistry) {
|
|
11
47
|
if (envelope.mode !== constants_1.MODE_HANDOFF)
|
|
12
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
|
+
}
|
|
13
60
|
this.transcript.push(envelope);
|
|
14
61
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
15
62
|
switch (envelope.messageType) {
|
|
@@ -74,6 +121,14 @@ class HandoffProjection {
|
|
|
74
121
|
break;
|
|
75
122
|
}
|
|
76
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
126
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
127
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
128
|
+
*/
|
|
129
|
+
get hasAnomalies() {
|
|
130
|
+
return this.anomalies.length > 0;
|
|
131
|
+
}
|
|
77
132
|
get isCommitted() {
|
|
78
133
|
return this.commitment !== undefined;
|
|
79
134
|
}
|
|
@@ -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 ProposalRecord {
|
|
@@ -24,10 +25,51 @@ export declare class ProposalProjection {
|
|
|
24
25
|
readonly proposals: Map<string, ProposalRecord>;
|
|
25
26
|
readonly accepts: ProposalAcceptRecord[];
|
|
26
27
|
readonly rejections: ProposalRejectRecord[];
|
|
28
|
+
/**
|
|
29
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
30
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
31
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
32
|
+
* only as a one-line pointer.
|
|
33
|
+
*/
|
|
27
34
|
readonly transcript: Envelope[];
|
|
35
|
+
/**
|
|
36
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
37
|
+
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
38
|
+
* for the canonical description; duplicated here only as a one-line
|
|
39
|
+
* pointer. No built-in detection populates this for Proposal mode.
|
|
40
|
+
*/
|
|
41
|
+
readonly anomalies: ProjectionAnomaly[];
|
|
28
42
|
phase: 'Negotiating' | 'TerminalRejected' | 'Committed';
|
|
29
43
|
commitment?: Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* `message_id`s already applied to this projection. See
|
|
46
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
47
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
48
|
+
* only as a one-line pointer.
|
|
49
|
+
*/
|
|
50
|
+
private readonly seenMessageIds;
|
|
51
|
+
/**
|
|
52
|
+
* Apply one envelope to this projection's in-process state.
|
|
53
|
+
*
|
|
54
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
55
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
56
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
57
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
58
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
59
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
60
|
+
*
|
|
61
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
62
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
63
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
64
|
+
* citation; duplicated here only as a one-line pointer.
|
|
65
|
+
*/
|
|
30
66
|
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
67
|
+
/**
|
|
68
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
69
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
70
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
71
|
+
*/
|
|
72
|
+
get hasAnomalies(): boolean;
|
|
31
73
|
get isCommitted(): boolean;
|
|
32
74
|
get isPositiveOutcome(): boolean | undefined;
|
|
33
75
|
activeProposals(): ProposalRecord[];
|
|
@@ -2,16 +2,63 @@
|
|
|
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
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
12
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
13
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
14
|
+
* only as a one-line pointer.
|
|
15
|
+
*/
|
|
9
16
|
transcript = [];
|
|
17
|
+
/**
|
|
18
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
19
|
+
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
20
|
+
* for the canonical description; duplicated here only as a one-line
|
|
21
|
+
* pointer. No built-in detection populates this for Proposal mode.
|
|
22
|
+
*/
|
|
23
|
+
anomalies = [];
|
|
10
24
|
phase = 'Negotiating';
|
|
11
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
|
+
*/
|
|
12
48
|
applyEnvelope(envelope, protoRegistry) {
|
|
13
49
|
if (envelope.mode !== constants_1.MODE_PROPOSAL)
|
|
14
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
|
+
}
|
|
15
62
|
this.transcript.push(envelope);
|
|
16
63
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
17
64
|
switch (envelope.messageType) {
|
|
@@ -77,6 +124,14 @@ class ProposalProjection {
|
|
|
77
124
|
break;
|
|
78
125
|
}
|
|
79
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
129
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
130
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
131
|
+
*/
|
|
132
|
+
get hasAnomalies() {
|
|
133
|
+
return this.anomalies.length > 0;
|
|
134
|
+
}
|
|
80
135
|
get isCommitted() {
|
|
81
136
|
return this.commitment !== undefined;
|
|
82
137
|
}
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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(
|
|
113
|
+
setBallot(envelope, requestId, vote, reason) {
|
|
58
114
|
const senderMap = this.ballots.get(requestId) ?? new Map();
|
|
59
|
-
senderMap.
|
|
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;
|
package/dist/projections/task.js
CHANGED
|
@@ -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) {
|
|
@@ -87,6 +134,14 @@ class TaskProjection {
|
|
|
87
134
|
break;
|
|
88
135
|
}
|
|
89
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
139
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
140
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
141
|
+
*/
|
|
142
|
+
get hasAnomalies() {
|
|
143
|
+
return this.anomalies.length > 0;
|
|
144
|
+
}
|
|
90
145
|
get isCommitted() {
|
|
91
146
|
return this.commitment !== undefined;
|
|
92
147
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.
|
|
58
|
+
"@multiagentcoordinationprotocol/proto": "^0.1.9",
|
|
59
59
|
"protobufjs": "^8.6.5"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|