macp-sdk-typescript 0.2.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 +73 -0
- package/LICENSE +201 -0
- package/README.md +307 -0
- package/dist/agent/dispatcher.d.ts +16 -0
- package/dist/agent/dispatcher.js +61 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.js +25 -0
- package/dist/agent/participant.d.ts +38 -0
- package/dist/agent/participant.js +221 -0
- package/dist/agent/runner.d.ts +18 -0
- package/dist/agent/runner.js +84 -0
- package/dist/agent/strategies.d.ts +40 -0
- package/dist/agent/strategies.js +106 -0
- package/dist/agent/transports.d.ts +35 -0
- package/dist/agent/transports.js +125 -0
- package/dist/agent/types.d.ts +72 -0
- package/dist/agent/types.js +2 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.js +112 -0
- package/dist/client.d.ts +171 -0
- package/dist/client.js +384 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +29 -0
- package/dist/decision.d.ts +65 -0
- package/dist/decision.js +148 -0
- package/dist/envelope.d.ts +50 -0
- package/dist/envelope.js +97 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +114 -0
- package/dist/handoff.d.ts +73 -0
- package/dist/handoff.js +160 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +57 -0
- package/dist/policy.d.ts +76 -0
- package/dist/policy.js +118 -0
- package/dist/projections/decision.d.ts +49 -0
- package/dist/projections/decision.js +132 -0
- package/dist/projections/handoff.d.ts +28 -0
- package/dist/projections/handoff.js +111 -0
- package/dist/projections/proposal.d.ts +40 -0
- package/dist/projections/proposal.js +123 -0
- package/dist/projections/quorum.d.ts +36 -0
- package/dist/projections/quorum.js +107 -0
- package/dist/projections/task.d.ts +54 -0
- package/dist/projections/task.js +127 -0
- package/dist/projections.d.ts +5 -0
- package/dist/projections.js +21 -0
- package/dist/proposal.d.ts +69 -0
- package/dist/proposal.js +155 -0
- package/dist/proto-registry.d.ts +12 -0
- package/dist/proto-registry.js +165 -0
- package/dist/quorum.d.ts +65 -0
- package/dist/quorum.js +144 -0
- package/dist/retry.d.ts +14 -0
- package/dist/retry.js +48 -0
- package/dist/task.d.ts +73 -0
- package/dist/task.js +166 -0
- package/dist/types.d.ts +263 -0
- package/dist/types.js +2 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.js +92 -0
- package/dist/watchers.d.ts +47 -0
- package/dist/watchers.js +176 -0
- package/package.json +72 -0
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { PolicyDescriptor } from './types';
|
|
2
|
+
export interface CommitmentRulesInput {
|
|
3
|
+
authority?: 'initiator_only' | 'any_participant' | 'designated_role';
|
|
4
|
+
designatedRoles?: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface DecisionVotingRules {
|
|
7
|
+
algorithm?: 'none' | 'majority' | 'supermajority' | 'unanimous' | 'weighted' | 'plurality';
|
|
8
|
+
threshold?: number;
|
|
9
|
+
quorum?: {
|
|
10
|
+
type: 'count' | 'percentage';
|
|
11
|
+
value: number;
|
|
12
|
+
};
|
|
13
|
+
weights?: Record<string, number>;
|
|
14
|
+
}
|
|
15
|
+
export interface DecisionObjectionHandling {
|
|
16
|
+
criticalSeverityVetoes?: boolean;
|
|
17
|
+
vetoThreshold?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface DecisionEvaluationRules {
|
|
20
|
+
minimumConfidence?: number;
|
|
21
|
+
requiredBeforeVoting?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface DecisionCommitmentRules {
|
|
24
|
+
authority?: 'initiator_only' | 'any_participant' | 'designated_role';
|
|
25
|
+
designatedRoles?: string[];
|
|
26
|
+
requireVoteQuorum?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface DecisionPolicyRulesInput {
|
|
29
|
+
voting?: DecisionVotingRules;
|
|
30
|
+
objectionHandling?: DecisionObjectionHandling;
|
|
31
|
+
evaluation?: DecisionEvaluationRules;
|
|
32
|
+
commitment?: DecisionCommitmentRules;
|
|
33
|
+
}
|
|
34
|
+
export interface QuorumPolicyRulesInput {
|
|
35
|
+
threshold?: {
|
|
36
|
+
type: 'n_of_m' | 'percentage' | 'weighted';
|
|
37
|
+
value: number;
|
|
38
|
+
};
|
|
39
|
+
abstention?: {
|
|
40
|
+
countsTowardQuorum?: boolean;
|
|
41
|
+
interpretation?: 'neutral' | 'implicit_reject' | 'ignored';
|
|
42
|
+
};
|
|
43
|
+
commitment?: CommitmentRulesInput;
|
|
44
|
+
}
|
|
45
|
+
export interface ProposalPolicyRulesInput {
|
|
46
|
+
acceptance?: {
|
|
47
|
+
criterion?: 'all_parties' | 'counterparty' | 'initiator';
|
|
48
|
+
};
|
|
49
|
+
counterProposal?: {
|
|
50
|
+
maxRounds?: number;
|
|
51
|
+
};
|
|
52
|
+
rejection?: {
|
|
53
|
+
terminalOnAnyReject?: boolean;
|
|
54
|
+
};
|
|
55
|
+
commitment?: CommitmentRulesInput;
|
|
56
|
+
}
|
|
57
|
+
export interface TaskPolicyRulesInput {
|
|
58
|
+
assignment?: {
|
|
59
|
+
allowReassignmentOnReject?: boolean;
|
|
60
|
+
};
|
|
61
|
+
completion?: {
|
|
62
|
+
requireOutput?: boolean;
|
|
63
|
+
};
|
|
64
|
+
commitment?: CommitmentRulesInput;
|
|
65
|
+
}
|
|
66
|
+
export interface HandoffPolicyRulesInput {
|
|
67
|
+
acceptance?: {
|
|
68
|
+
implicitAcceptTimeoutMs?: number;
|
|
69
|
+
};
|
|
70
|
+
commitment?: CommitmentRulesInput;
|
|
71
|
+
}
|
|
72
|
+
export declare function buildDecisionPolicy(policyId: string, description: string, rules: DecisionPolicyRulesInput): PolicyDescriptor;
|
|
73
|
+
export declare function buildQuorumPolicy(policyId: string, description: string, rules: QuorumPolicyRulesInput): PolicyDescriptor;
|
|
74
|
+
export declare function buildProposalPolicy(policyId: string, description: string, rules: ProposalPolicyRulesInput): PolicyDescriptor;
|
|
75
|
+
export declare function buildTaskPolicy(policyId: string, description: string, rules: TaskPolicyRulesInput): PolicyDescriptor;
|
|
76
|
+
export declare function buildHandoffPolicy(policyId: string, description: string, rules: HandoffPolicyRulesInput): PolicyDescriptor;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildDecisionPolicy = buildDecisionPolicy;
|
|
4
|
+
exports.buildQuorumPolicy = buildQuorumPolicy;
|
|
5
|
+
exports.buildProposalPolicy = buildProposalPolicy;
|
|
6
|
+
exports.buildTaskPolicy = buildTaskPolicy;
|
|
7
|
+
exports.buildHandoffPolicy = buildHandoffPolicy;
|
|
8
|
+
function serializeCommitment(commitment) {
|
|
9
|
+
return {
|
|
10
|
+
authority: commitment?.authority ?? 'initiator_only',
|
|
11
|
+
designated_roles: commitment?.designatedRoles ?? [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
// ── Builder helpers ─────────────────────────────────────────────────
|
|
15
|
+
function buildDecisionPolicy(policyId, description, rules) {
|
|
16
|
+
const rulesJson = {
|
|
17
|
+
voting: {
|
|
18
|
+
algorithm: rules.voting?.algorithm ?? 'none',
|
|
19
|
+
threshold: rules.voting?.threshold ?? 0.5,
|
|
20
|
+
quorum: rules.voting?.quorum ? { type: rules.voting.quorum.type, value: rules.voting.quorum.value } : undefined,
|
|
21
|
+
weights: rules.voting?.weights ?? undefined,
|
|
22
|
+
},
|
|
23
|
+
objection_handling: {
|
|
24
|
+
critical_severity_vetoes: rules.objectionHandling?.criticalSeverityVetoes ?? false,
|
|
25
|
+
veto_threshold: rules.objectionHandling?.vetoThreshold ?? 1,
|
|
26
|
+
},
|
|
27
|
+
evaluation: {
|
|
28
|
+
minimum_confidence: rules.evaluation?.minimumConfidence ?? 0,
|
|
29
|
+
required_before_voting: rules.evaluation?.requiredBeforeVoting ?? false,
|
|
30
|
+
},
|
|
31
|
+
commitment: {
|
|
32
|
+
authority: rules.commitment?.authority ?? 'initiator_only',
|
|
33
|
+
designated_roles: rules.commitment?.designatedRoles ?? [],
|
|
34
|
+
require_vote_quorum: rules.commitment?.requireVoteQuorum ?? false,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
policyId,
|
|
39
|
+
mode: 'macp.mode.decision.v1',
|
|
40
|
+
description,
|
|
41
|
+
rules: Buffer.from(JSON.stringify(rulesJson)),
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function buildQuorumPolicy(policyId, description, rules) {
|
|
46
|
+
const rulesJson = {
|
|
47
|
+
threshold: {
|
|
48
|
+
type: rules.threshold?.type ?? 'n_of_m',
|
|
49
|
+
value: rules.threshold?.value ?? 0,
|
|
50
|
+
},
|
|
51
|
+
abstention: {
|
|
52
|
+
counts_toward_quorum: rules.abstention?.countsTowardQuorum ?? false,
|
|
53
|
+
interpretation: rules.abstention?.interpretation ?? 'neutral',
|
|
54
|
+
},
|
|
55
|
+
commitment: serializeCommitment(rules.commitment),
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
policyId,
|
|
59
|
+
mode: 'macp.mode.quorum.v1',
|
|
60
|
+
description,
|
|
61
|
+
rules: Buffer.from(JSON.stringify(rulesJson)),
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function buildProposalPolicy(policyId, description, rules) {
|
|
66
|
+
const rulesJson = {
|
|
67
|
+
acceptance: {
|
|
68
|
+
criterion: rules.acceptance?.criterion ?? 'all_parties',
|
|
69
|
+
},
|
|
70
|
+
counter_proposal: {
|
|
71
|
+
max_rounds: rules.counterProposal?.maxRounds ?? 0,
|
|
72
|
+
},
|
|
73
|
+
rejection: {
|
|
74
|
+
terminal_on_any_reject: rules.rejection?.terminalOnAnyReject ?? false,
|
|
75
|
+
},
|
|
76
|
+
commitment: serializeCommitment(rules.commitment),
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
policyId,
|
|
80
|
+
mode: 'macp.mode.proposal.v1',
|
|
81
|
+
description,
|
|
82
|
+
rules: Buffer.from(JSON.stringify(rulesJson)),
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function buildTaskPolicy(policyId, description, rules) {
|
|
87
|
+
const rulesJson = {
|
|
88
|
+
assignment: {
|
|
89
|
+
allow_reassignment_on_reject: rules.assignment?.allowReassignmentOnReject ?? false,
|
|
90
|
+
},
|
|
91
|
+
completion: {
|
|
92
|
+
require_output: rules.completion?.requireOutput ?? false,
|
|
93
|
+
},
|
|
94
|
+
commitment: serializeCommitment(rules.commitment),
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
policyId,
|
|
98
|
+
mode: 'macp.mode.task.v1',
|
|
99
|
+
description,
|
|
100
|
+
rules: Buffer.from(JSON.stringify(rulesJson)),
|
|
101
|
+
schemaVersion: 1,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function buildHandoffPolicy(policyId, description, rules) {
|
|
105
|
+
const rulesJson = {
|
|
106
|
+
acceptance: {
|
|
107
|
+
implicit_accept_timeout_ms: rules.acceptance?.implicitAcceptTimeoutMs ?? 0,
|
|
108
|
+
},
|
|
109
|
+
commitment: serializeCommitment(rules.commitment),
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
policyId,
|
|
113
|
+
mode: 'macp.mode.handoff.v1',
|
|
114
|
+
description,
|
|
115
|
+
rules: Buffer.from(JSON.stringify(rulesJson)),
|
|
116
|
+
schemaVersion: 1,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Envelope } from '../types';
|
|
2
|
+
import type { ProtoRegistry } from '../proto-registry';
|
|
3
|
+
export interface DecisionProposalRecord {
|
|
4
|
+
proposalId: string;
|
|
5
|
+
option: string;
|
|
6
|
+
rationale?: string;
|
|
7
|
+
sender: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DecisionEvaluationRecord {
|
|
10
|
+
proposalId: string;
|
|
11
|
+
recommendation: string;
|
|
12
|
+
confidence: number;
|
|
13
|
+
reason?: string;
|
|
14
|
+
sender: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DecisionObjectionRecord {
|
|
17
|
+
proposalId: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
severity: string;
|
|
20
|
+
sender: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DecisionVoteRecord {
|
|
23
|
+
proposalId: string;
|
|
24
|
+
vote: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
sender: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class DecisionProjection {
|
|
29
|
+
readonly proposals: Map<string, DecisionProposalRecord>;
|
|
30
|
+
readonly evaluations: DecisionEvaluationRecord[];
|
|
31
|
+
readonly objections: DecisionObjectionRecord[];
|
|
32
|
+
readonly votes: Map<string, Map<string, DecisionVoteRecord>>;
|
|
33
|
+
readonly transcript: Envelope[];
|
|
34
|
+
phase: 'Proposal' | 'Evaluation' | 'Voting' | 'Committed';
|
|
35
|
+
commitment?: Record<string, unknown>;
|
|
36
|
+
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
37
|
+
get isCommitted(): boolean;
|
|
38
|
+
get isPositiveOutcome(): boolean | undefined;
|
|
39
|
+
voteTotals(): Record<string, number>;
|
|
40
|
+
majorityWinner(): string | undefined;
|
|
41
|
+
/** Returns the APPROVE vote ratio excluding ABSTAIN votes from the denominator. */
|
|
42
|
+
voteRatio(proposalId: string): number;
|
|
43
|
+
/** Only critical-severity objections are blocking per RFC-MACP-0004. */
|
|
44
|
+
hasBlockingObjection(proposalId?: string): boolean;
|
|
45
|
+
/** Evaluations with REVIEW recommendation (informational only). */
|
|
46
|
+
reviewEvaluations(): DecisionEvaluationRecord[];
|
|
47
|
+
/** Evaluations excluding REVIEW (qualifying evaluations). */
|
|
48
|
+
qualifyingEvaluations(): DecisionEvaluationRecord[];
|
|
49
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DecisionProjection = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
class DecisionProjection {
|
|
6
|
+
proposals = new Map();
|
|
7
|
+
evaluations = [];
|
|
8
|
+
objections = [];
|
|
9
|
+
votes = new Map();
|
|
10
|
+
transcript = [];
|
|
11
|
+
phase = 'Proposal';
|
|
12
|
+
commitment;
|
|
13
|
+
applyEnvelope(envelope, protoRegistry) {
|
|
14
|
+
if (envelope.mode !== constants_1.MODE_DECISION)
|
|
15
|
+
return;
|
|
16
|
+
this.transcript.push(envelope);
|
|
17
|
+
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
18
|
+
switch (envelope.messageType) {
|
|
19
|
+
case 'Proposal': {
|
|
20
|
+
const record = payload;
|
|
21
|
+
this.proposals.set(record.proposalId, {
|
|
22
|
+
proposalId: record.proposalId,
|
|
23
|
+
option: record.option,
|
|
24
|
+
rationale: record.rationale,
|
|
25
|
+
sender: envelope.sender,
|
|
26
|
+
});
|
|
27
|
+
this.phase = 'Evaluation';
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'Evaluation': {
|
|
31
|
+
const record = payload;
|
|
32
|
+
this.evaluations.push({ ...record, sender: envelope.sender });
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'Objection': {
|
|
36
|
+
const record = payload;
|
|
37
|
+
this.objections.push({ ...record, severity: record.severity ?? 'medium', sender: envelope.sender });
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case 'Vote': {
|
|
41
|
+
const record = payload;
|
|
42
|
+
const bySender = this.votes.get(record.proposalId) ?? new Map();
|
|
43
|
+
bySender.set(envelope.sender, { ...record, sender: envelope.sender });
|
|
44
|
+
this.votes.set(record.proposalId, bySender);
|
|
45
|
+
this.phase = 'Voting';
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'Commitment': {
|
|
49
|
+
this.commitment = payload;
|
|
50
|
+
this.phase = 'Committed';
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
get isCommitted() {
|
|
58
|
+
return this.commitment !== undefined;
|
|
59
|
+
}
|
|
60
|
+
get isPositiveOutcome() {
|
|
61
|
+
if (!this.commitment)
|
|
62
|
+
return undefined;
|
|
63
|
+
const val = this.commitment.outcomePositive ??
|
|
64
|
+
this.commitment.outcome_positive;
|
|
65
|
+
return val !== undefined ? Boolean(val) : true;
|
|
66
|
+
}
|
|
67
|
+
voteTotals() {
|
|
68
|
+
const totals = {};
|
|
69
|
+
for (const [proposalId, senderVotes] of this.votes.entries()) {
|
|
70
|
+
totals[proposalId] = [...senderVotes.values()].filter((item) => isPositiveVote(item.vote)).length;
|
|
71
|
+
}
|
|
72
|
+
return totals;
|
|
73
|
+
}
|
|
74
|
+
majorityWinner() {
|
|
75
|
+
const totals = this.voteTotals();
|
|
76
|
+
const entries = Object.entries(totals);
|
|
77
|
+
if (!entries.length)
|
|
78
|
+
return undefined;
|
|
79
|
+
// Count total non-abstain votes across all proposals
|
|
80
|
+
let nonAbstain = 0;
|
|
81
|
+
for (const senderVotes of this.votes.values()) {
|
|
82
|
+
for (const vote of senderVotes.values()) {
|
|
83
|
+
if (vote.vote.toUpperCase() !== 'ABSTAIN') {
|
|
84
|
+
nonAbstain++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (nonAbstain === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
for (const [proposalId, count] of entries) {
|
|
91
|
+
if (count / nonAbstain > 0.5)
|
|
92
|
+
return proposalId;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/** Returns the APPROVE vote ratio excluding ABSTAIN votes from the denominator. */
|
|
97
|
+
voteRatio(proposalId) {
|
|
98
|
+
const senderVotes = this.votes.get(proposalId);
|
|
99
|
+
if (!senderVotes)
|
|
100
|
+
return 0;
|
|
101
|
+
const votes = [...senderVotes.values()];
|
|
102
|
+
const nonAbstain = votes.filter((v) => v.vote.toUpperCase() !== 'ABSTAIN');
|
|
103
|
+
if (nonAbstain.length === 0)
|
|
104
|
+
return 0;
|
|
105
|
+
const approvals = nonAbstain.filter((v) => isPositiveVote(v.vote)).length;
|
|
106
|
+
return approvals / nonAbstain.length;
|
|
107
|
+
}
|
|
108
|
+
/** Only critical-severity objections are blocking per RFC-MACP-0004. */
|
|
109
|
+
hasBlockingObjection(proposalId) {
|
|
110
|
+
if (proposalId !== undefined) {
|
|
111
|
+
return this.objections.some((item) => item.proposalId === proposalId && item.severity.toLowerCase() === 'critical');
|
|
112
|
+
}
|
|
113
|
+
return this.objections.some((item) => item.severity.toLowerCase() === 'critical');
|
|
114
|
+
}
|
|
115
|
+
/** Evaluations with REVIEW recommendation (informational only). */
|
|
116
|
+
reviewEvaluations() {
|
|
117
|
+
return this.evaluations.filter((e) => e.recommendation.toUpperCase() === 'REVIEW');
|
|
118
|
+
}
|
|
119
|
+
/** Evaluations excluding REVIEW (qualifying evaluations). */
|
|
120
|
+
qualifyingEvaluations() {
|
|
121
|
+
return this.evaluations.filter((e) => e.recommendation.toUpperCase() !== 'REVIEW');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.DecisionProjection = DecisionProjection;
|
|
125
|
+
function isPositiveVote(vote) {
|
|
126
|
+
const normalized = vote.trim().toUpperCase();
|
|
127
|
+
return (normalized === 'APPROVE' ||
|
|
128
|
+
normalized === 'APPROVED' ||
|
|
129
|
+
normalized === 'YES' ||
|
|
130
|
+
normalized === 'ACCEPT' ||
|
|
131
|
+
normalized === 'ACCEPTED');
|
|
132
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Envelope } from '../types';
|
|
2
|
+
import type { ProtoRegistry } from '../proto-registry';
|
|
3
|
+
export interface HandoffRecord {
|
|
4
|
+
handoffId: string;
|
|
5
|
+
targetParticipant: string;
|
|
6
|
+
scope: string;
|
|
7
|
+
reason?: string;
|
|
8
|
+
sender: string;
|
|
9
|
+
status: 'offered' | 'context_sent' | 'accepted' | 'declined';
|
|
10
|
+
contextContentType?: string;
|
|
11
|
+
acceptedBy?: string;
|
|
12
|
+
declinedBy?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class HandoffProjection {
|
|
15
|
+
readonly handoffs: Map<string, HandoffRecord>;
|
|
16
|
+
readonly transcript: Envelope[];
|
|
17
|
+
phase: 'Pending' | 'OfferPending' | 'ContextSharing' | 'Accepted' | 'Declined' | 'Committed';
|
|
18
|
+
commitment?: Record<string, unknown>;
|
|
19
|
+
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
20
|
+
get isCommitted(): boolean;
|
|
21
|
+
get isPositiveOutcome(): boolean | undefined;
|
|
22
|
+
getHandoff(handoffId: string): HandoffRecord | undefined;
|
|
23
|
+
isAccepted(handoffId: string): boolean;
|
|
24
|
+
isDeclined(handoffId: string): boolean;
|
|
25
|
+
pendingHandoffs(): HandoffRecord[];
|
|
26
|
+
hasAcceptedOffer(handoffId?: string): boolean;
|
|
27
|
+
activeOffer(): HandoffRecord | undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HandoffProjection = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
class HandoffProjection {
|
|
6
|
+
handoffs = new Map();
|
|
7
|
+
transcript = [];
|
|
8
|
+
phase = 'Pending';
|
|
9
|
+
commitment;
|
|
10
|
+
applyEnvelope(envelope, protoRegistry) {
|
|
11
|
+
if (envelope.mode !== constants_1.MODE_HANDOFF)
|
|
12
|
+
return;
|
|
13
|
+
this.transcript.push(envelope);
|
|
14
|
+
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
15
|
+
switch (envelope.messageType) {
|
|
16
|
+
case 'HandoffOffer': {
|
|
17
|
+
const record = payload;
|
|
18
|
+
this.handoffs.set(record.handoffId, {
|
|
19
|
+
handoffId: record.handoffId,
|
|
20
|
+
targetParticipant: record.targetParticipant,
|
|
21
|
+
scope: record.scope,
|
|
22
|
+
reason: record.reason,
|
|
23
|
+
sender: envelope.sender,
|
|
24
|
+
status: 'offered',
|
|
25
|
+
});
|
|
26
|
+
this.phase = 'OfferPending';
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'HandoffContext': {
|
|
30
|
+
const record = payload;
|
|
31
|
+
const handoff = this.handoffs.get(record.handoffId);
|
|
32
|
+
if (handoff) {
|
|
33
|
+
// Per RFC-MACP-0010 §2.1: context after accept is permitted as supplementary docs.
|
|
34
|
+
// Only update status if not already accepted/declined.
|
|
35
|
+
if (handoff.status === 'offered') {
|
|
36
|
+
handoff.status = 'context_sent';
|
|
37
|
+
}
|
|
38
|
+
handoff.contextContentType = record.contentType;
|
|
39
|
+
}
|
|
40
|
+
if (this.phase === 'OfferPending')
|
|
41
|
+
this.phase = 'ContextSharing';
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'HandoffAccept': {
|
|
45
|
+
const record = payload;
|
|
46
|
+
const handoff = this.handoffs.get(record.handoffId);
|
|
47
|
+
if (handoff) {
|
|
48
|
+
handoff.status = 'accepted';
|
|
49
|
+
handoff.acceptedBy = record.acceptedBy;
|
|
50
|
+
}
|
|
51
|
+
this.phase = 'Accepted';
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'HandoffDecline': {
|
|
55
|
+
const record = payload;
|
|
56
|
+
const handoff = this.handoffs.get(record.handoffId);
|
|
57
|
+
if (handoff) {
|
|
58
|
+
handoff.status = 'declined';
|
|
59
|
+
handoff.declinedBy = record.declinedBy;
|
|
60
|
+
}
|
|
61
|
+
this.phase = 'Declined';
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'Commitment': {
|
|
65
|
+
this.commitment = payload;
|
|
66
|
+
this.phase = 'Committed';
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
get isCommitted() {
|
|
74
|
+
return this.commitment !== undefined;
|
|
75
|
+
}
|
|
76
|
+
get isPositiveOutcome() {
|
|
77
|
+
if (!this.commitment)
|
|
78
|
+
return undefined;
|
|
79
|
+
const val = this.commitment.outcomePositive ??
|
|
80
|
+
this.commitment.outcome_positive;
|
|
81
|
+
return val !== undefined ? Boolean(val) : true;
|
|
82
|
+
}
|
|
83
|
+
getHandoff(handoffId) {
|
|
84
|
+
return this.handoffs.get(handoffId);
|
|
85
|
+
}
|
|
86
|
+
isAccepted(handoffId) {
|
|
87
|
+
return this.handoffs.get(handoffId)?.status === 'accepted';
|
|
88
|
+
}
|
|
89
|
+
isDeclined(handoffId) {
|
|
90
|
+
return this.handoffs.get(handoffId)?.status === 'declined';
|
|
91
|
+
}
|
|
92
|
+
pendingHandoffs() {
|
|
93
|
+
return [...this.handoffs.values()].filter((h) => h.status === 'offered' || h.status === 'context_sent');
|
|
94
|
+
}
|
|
95
|
+
hasAcceptedOffer(handoffId) {
|
|
96
|
+
if (handoffId)
|
|
97
|
+
return this.handoffs.get(handoffId)?.status === 'accepted';
|
|
98
|
+
return [...this.handoffs.values()].some((h) => h.status === 'accepted');
|
|
99
|
+
}
|
|
100
|
+
activeOffer() {
|
|
101
|
+
const all = [...this.handoffs.values()];
|
|
102
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
103
|
+
const record = all[i];
|
|
104
|
+
if (record && (record.status === 'offered' || record.status === 'context_sent')) {
|
|
105
|
+
return record;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.HandoffProjection = HandoffProjection;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Envelope } from '../types';
|
|
2
|
+
import type { ProtoRegistry } from '../proto-registry';
|
|
3
|
+
export interface ProposalRecord {
|
|
4
|
+
proposalId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
sender: string;
|
|
9
|
+
supersedes?: string;
|
|
10
|
+
status: 'open' | 'accepted' | 'rejected' | 'withdrawn';
|
|
11
|
+
}
|
|
12
|
+
export interface ProposalAcceptRecord {
|
|
13
|
+
proposalId: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
sender: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ProposalRejectRecord {
|
|
18
|
+
proposalId: string;
|
|
19
|
+
terminal: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
sender: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class ProposalProjection {
|
|
24
|
+
readonly proposals: Map<string, ProposalRecord>;
|
|
25
|
+
readonly accepts: ProposalAcceptRecord[];
|
|
26
|
+
readonly rejections: ProposalRejectRecord[];
|
|
27
|
+
readonly transcript: Envelope[];
|
|
28
|
+
phase: 'Negotiating' | 'TerminalRejected' | 'Committed';
|
|
29
|
+
commitment?: Record<string, unknown>;
|
|
30
|
+
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
31
|
+
get isCommitted(): boolean;
|
|
32
|
+
get isPositiveOutcome(): boolean | undefined;
|
|
33
|
+
activeProposals(): ProposalRecord[];
|
|
34
|
+
latestProposal(): ProposalRecord | undefined;
|
|
35
|
+
isAccepted(proposalId: string): boolean;
|
|
36
|
+
isTerminallyRejected(proposalId: string): boolean;
|
|
37
|
+
liveProposals(): Map<string, ProposalRecord>;
|
|
38
|
+
acceptedProposal(): string | undefined;
|
|
39
|
+
hasTerminalRejection(): boolean;
|
|
40
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProposalProjection = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
class ProposalProjection {
|
|
6
|
+
proposals = new Map();
|
|
7
|
+
accepts = [];
|
|
8
|
+
rejections = [];
|
|
9
|
+
transcript = [];
|
|
10
|
+
phase = 'Negotiating';
|
|
11
|
+
commitment;
|
|
12
|
+
applyEnvelope(envelope, protoRegistry) {
|
|
13
|
+
if (envelope.mode !== constants_1.MODE_PROPOSAL)
|
|
14
|
+
return;
|
|
15
|
+
this.transcript.push(envelope);
|
|
16
|
+
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
17
|
+
switch (envelope.messageType) {
|
|
18
|
+
case 'Proposal': {
|
|
19
|
+
const record = payload;
|
|
20
|
+
this.proposals.set(record.proposalId, {
|
|
21
|
+
proposalId: record.proposalId,
|
|
22
|
+
title: record.title,
|
|
23
|
+
summary: record.summary,
|
|
24
|
+
tags: record.tags,
|
|
25
|
+
sender: envelope.sender,
|
|
26
|
+
status: 'open',
|
|
27
|
+
});
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'CounterProposal': {
|
|
31
|
+
const record = payload;
|
|
32
|
+
this.proposals.set(record.proposalId, {
|
|
33
|
+
proposalId: record.proposalId,
|
|
34
|
+
title: record.title,
|
|
35
|
+
summary: record.summary,
|
|
36
|
+
sender: envelope.sender,
|
|
37
|
+
supersedes: record.supersedesProposalId,
|
|
38
|
+
status: 'open',
|
|
39
|
+
});
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'Accept': {
|
|
43
|
+
const record = payload;
|
|
44
|
+
this.accepts.push({ ...record, sender: envelope.sender });
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'Reject': {
|
|
48
|
+
const record = payload;
|
|
49
|
+
const terminal = record.terminal ?? false;
|
|
50
|
+
this.rejections.push({
|
|
51
|
+
proposalId: record.proposalId,
|
|
52
|
+
terminal,
|
|
53
|
+
reason: record.reason,
|
|
54
|
+
sender: envelope.sender,
|
|
55
|
+
});
|
|
56
|
+
if (terminal) {
|
|
57
|
+
const proposal = this.proposals.get(record.proposalId);
|
|
58
|
+
if (proposal)
|
|
59
|
+
proposal.status = 'rejected';
|
|
60
|
+
this.phase = 'TerminalRejected';
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case 'Withdraw': {
|
|
65
|
+
const record = payload;
|
|
66
|
+
const proposal = this.proposals.get(record.proposalId);
|
|
67
|
+
if (proposal)
|
|
68
|
+
proposal.status = 'withdrawn';
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'Commitment': {
|
|
72
|
+
this.commitment = payload;
|
|
73
|
+
this.phase = 'Committed';
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
get isCommitted() {
|
|
81
|
+
return this.commitment !== undefined;
|
|
82
|
+
}
|
|
83
|
+
get isPositiveOutcome() {
|
|
84
|
+
if (!this.commitment)
|
|
85
|
+
return undefined;
|
|
86
|
+
const val = this.commitment.outcomePositive ??
|
|
87
|
+
this.commitment.outcome_positive;
|
|
88
|
+
return val !== undefined ? Boolean(val) : true;
|
|
89
|
+
}
|
|
90
|
+
activeProposals() {
|
|
91
|
+
return [...this.proposals.values()].filter((p) => p.status === 'open');
|
|
92
|
+
}
|
|
93
|
+
latestProposal() {
|
|
94
|
+
const all = [...this.proposals.values()];
|
|
95
|
+
return all[all.length - 1];
|
|
96
|
+
}
|
|
97
|
+
isAccepted(proposalId) {
|
|
98
|
+
return this.accepts.some((a) => a.proposalId === proposalId);
|
|
99
|
+
}
|
|
100
|
+
isTerminallyRejected(proposalId) {
|
|
101
|
+
return this.rejections.some((r) => r.proposalId === proposalId && r.terminal);
|
|
102
|
+
}
|
|
103
|
+
liveProposals() {
|
|
104
|
+
const result = new Map();
|
|
105
|
+
for (const [id, p] of this.proposals) {
|
|
106
|
+
if (p.status !== 'withdrawn')
|
|
107
|
+
result.set(id, p);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
acceptedProposal() {
|
|
112
|
+
if (this.accepts.length === 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
const ids = new Set(this.accepts.map((a) => a.proposalId));
|
|
115
|
+
if (ids.size === 1)
|
|
116
|
+
return ids.values().next().value;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
hasTerminalRejection() {
|
|
120
|
+
return this.rejections.some((r) => r.terminal);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.ProposalProjection = ProposalProjection;
|