@agentplat/mesh-sim 0.3.0-beta.2 → 0.3.0-beta.4
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/README.md +447 -2
- package/dist/collective-closed-loop-action.d.ts +80 -0
- package/dist/collective-closed-loop-action.d.ts.map +1 -0
- package/dist/collective-closed-loop-action.js +471 -0
- package/dist/collective-closed-loop-action.js.map +1 -0
- package/dist/collective-closed-loop-contracts.d.ts +75 -0
- package/dist/collective-closed-loop-contracts.d.ts.map +1 -0
- package/dist/collective-closed-loop-contracts.js +420 -0
- package/dist/collective-closed-loop-contracts.js.map +1 -0
- package/dist/collective-closed-loop-fault-matrix.d.ts +101 -0
- package/dist/collective-closed-loop-fault-matrix.d.ts.map +1 -0
- package/dist/collective-closed-loop-fault-matrix.js +253 -0
- package/dist/collective-closed-loop-fault-matrix.js.map +1 -0
- package/dist/collective-closed-loop-recovery.d.ts +70 -0
- package/dist/collective-closed-loop-recovery.d.ts.map +1 -0
- package/dist/collective-closed-loop-recovery.js +823 -0
- package/dist/collective-closed-loop-recovery.js.map +1 -0
- package/dist/collective-closed-loop-reference.d.ts +54 -0
- package/dist/collective-closed-loop-reference.d.ts.map +1 -0
- package/dist/collective-closed-loop-reference.js +1696 -0
- package/dist/collective-closed-loop-reference.js.map +1 -0
- package/dist/collective-closed-loop-replanning.d.ts +27 -0
- package/dist/collective-closed-loop-replanning.d.ts.map +1 -0
- package/dist/collective-closed-loop-replanning.js +151 -0
- package/dist/collective-closed-loop-replanning.js.map +1 -0
- package/dist/collective-closed-loop-resilience-campaign.d.ts +22 -0
- package/dist/collective-closed-loop-resilience-campaign.d.ts.map +1 -0
- package/dist/collective-closed-loop-resilience-campaign.js +196 -0
- package/dist/collective-closed-loop-resilience-campaign.js.map +1 -0
- package/dist/collective-closed-loop-resilience-contracts.d.ts +138 -0
- package/dist/collective-closed-loop-resilience-contracts.d.ts.map +1 -0
- package/dist/collective-closed-loop-resilience-contracts.js +677 -0
- package/dist/collective-closed-loop-resilience-contracts.js.map +1 -0
- package/dist/collective-closed-loop-runner.d.ts +133 -0
- package/dist/collective-closed-loop-runner.d.ts.map +1 -0
- package/dist/collective-closed-loop-runner.js +1429 -0
- package/dist/collective-closed-loop-runner.js.map +1 -0
- package/dist/collective-closed-loop-runtime.d.ts +95 -0
- package/dist/collective-closed-loop-runtime.d.ts.map +1 -0
- package/dist/collective-closed-loop-runtime.js +1192 -0
- package/dist/collective-closed-loop-runtime.js.map +1 -0
- package/dist/collective-effect-provenance.d.ts +5 -0
- package/dist/collective-effect-provenance.d.ts.map +1 -0
- package/dist/collective-effect-provenance.js +32 -0
- package/dist/collective-effect-provenance.js.map +1 -0
- package/dist/collective-environment.d.ts +49 -0
- package/dist/collective-environment.d.ts.map +1 -0
- package/dist/collective-environment.js +907 -0
- package/dist/collective-environment.js.map +1 -0
- package/dist/collective-progressive-scale-overlay.d.ts +39 -0
- package/dist/collective-progressive-scale-overlay.d.ts.map +1 -0
- package/dist/collective-progressive-scale-overlay.js +147 -0
- package/dist/collective-progressive-scale-overlay.js.map +1 -0
- package/dist/collective-progressive-scale.d.ts +202 -0
- package/dist/collective-progressive-scale.d.ts.map +1 -0
- package/dist/collective-progressive-scale.js +755 -0
- package/dist/collective-progressive-scale.js.map +1 -0
- package/dist/collective-statistical-campaign-aggregation.d.ts +97 -0
- package/dist/collective-statistical-campaign-aggregation.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-aggregation.js +446 -0
- package/dist/collective-statistical-campaign-aggregation.js.map +1 -0
- package/dist/collective-statistical-campaign-artifact-stream.d.ts +66 -0
- package/dist/collective-statistical-campaign-artifact-stream.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-artifact-stream.js +207 -0
- package/dist/collective-statistical-campaign-artifact-stream.js.map +1 -0
- package/dist/collective-statistical-campaign-bundle.d.ts +151 -0
- package/dist/collective-statistical-campaign-bundle.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-bundle.js +885 -0
- package/dist/collective-statistical-campaign-bundle.js.map +1 -0
- package/dist/collective-statistical-campaign-executor.d.ts +131 -0
- package/dist/collective-statistical-campaign-executor.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-executor.js +856 -0
- package/dist/collective-statistical-campaign-executor.js.map +1 -0
- package/dist/collective-statistical-campaign-normative-analysis.d.ts +85 -0
- package/dist/collective-statistical-campaign-normative-analysis.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-normative-analysis.js +483 -0
- package/dist/collective-statistical-campaign-normative-analysis.js.map +1 -0
- package/dist/collective-statistical-campaign-normative-operation.d.ts +89 -0
- package/dist/collective-statistical-campaign-normative-operation.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-normative-operation.js +266 -0
- package/dist/collective-statistical-campaign-normative-operation.js.map +1 -0
- package/dist/collective-statistical-campaign-normative-registry.d.ts +24 -0
- package/dist/collective-statistical-campaign-normative-registry.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-normative-registry.js +163 -0
- package/dist/collective-statistical-campaign-normative-registry.js.map +1 -0
- package/dist/collective-statistical-campaign-registered-adapter.d.ts +16 -0
- package/dist/collective-statistical-campaign-registered-adapter.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-registered-adapter.js +518 -0
- package/dist/collective-statistical-campaign-registered-adapter.js.map +1 -0
- package/dist/collective-statistical-campaign-scale.d.ts +85 -0
- package/dist/collective-statistical-campaign-scale.d.ts.map +1 -0
- package/dist/collective-statistical-campaign-scale.js +349 -0
- package/dist/collective-statistical-campaign-scale.js.map +1 -0
- package/dist/collective-trace-journal.d.ts +20 -0
- package/dist/collective-trace-journal.d.ts.map +1 -0
- package/dist/collective-trace-journal.js +105 -0
- package/dist/collective-trace-journal.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/multi-domain-environment-contracts.d.ts +122 -0
- package/dist/multi-domain-environment-contracts.d.ts.map +1 -0
- package/dist/multi-domain-environment-contracts.js +14 -0
- package/dist/multi-domain-environment-contracts.js.map +1 -0
- package/dist/multi-domain-environment-runtime.d.ts +25 -0
- package/dist/multi-domain-environment-runtime.d.ts.map +1 -0
- package/dist/multi-domain-environment-runtime.js +493 -0
- package/dist/multi-domain-environment-runtime.js.map +1 -0
- package/dist/multi-domain-environment-validation.d.ts +33 -0
- package/dist/multi-domain-environment-validation.d.ts.map +1 -0
- package/dist/multi-domain-environment-validation.js +415 -0
- package/dist/multi-domain-environment-validation.js.map +1 -0
- package/dist/multi-domain-environment.d.ts +4 -0
- package/dist/multi-domain-environment.d.ts.map +1 -0
- package/dist/multi-domain-environment.js +4 -0
- package/dist/multi-domain-environment.js.map +1 -0
- package/dist/reducer-scenario.d.ts.map +1 -1
- package/dist/reducer-scenario.js +67 -19
- package/dist/reducer-scenario.js.map +1 -1
- package/dist/scalable-evaluation-contracts.d.ts +835 -0
- package/dist/scalable-evaluation-contracts.d.ts.map +1 -0
- package/dist/scalable-evaluation-contracts.js +22 -0
- package/dist/scalable-evaluation-contracts.js.map +1 -0
- package/dist/scalable-evaluation-durability.d.ts +16 -0
- package/dist/scalable-evaluation-durability.d.ts.map +1 -0
- package/dist/scalable-evaluation-durability.js +339 -0
- package/dist/scalable-evaluation-durability.js.map +1 -0
- package/dist/scalable-evaluation-evidence.d.ts +93 -0
- package/dist/scalable-evaluation-evidence.d.ts.map +1 -0
- package/dist/scalable-evaluation-evidence.js +470 -0
- package/dist/scalable-evaluation-evidence.js.map +1 -0
- package/dist/scalable-evaluation-reference-integrated.d.ts +87 -0
- package/dist/scalable-evaluation-reference-integrated.d.ts.map +1 -0
- package/dist/scalable-evaluation-reference-integrated.js +1318 -0
- package/dist/scalable-evaluation-reference-integrated.js.map +1 -0
- package/dist/scalable-evaluation-runner.d.ts +8 -0
- package/dist/scalable-evaluation-runner.d.ts.map +1 -0
- package/dist/scalable-evaluation-runner.js +1810 -0
- package/dist/scalable-evaluation-runner.js.map +1 -0
- package/dist/scalable-evaluation-runtime.d.ts +86 -0
- package/dist/scalable-evaluation-runtime.d.ts.map +1 -0
- package/dist/scalable-evaluation-runtime.js +1310 -0
- package/dist/scalable-evaluation-runtime.js.map +1 -0
- package/dist/scalable-evaluation-validation.d.ts +59 -0
- package/dist/scalable-evaluation-validation.d.ts.map +1 -0
- package/dist/scalable-evaluation-validation.js +377 -0
- package/dist/scalable-evaluation-validation.js.map +1 -0
- package/dist/scalable-evaluation.d.ts +8 -0
- package/dist/scalable-evaluation.d.ts.map +1 -0
- package/dist/scalable-evaluation.js +8 -0
- package/dist/scalable-evaluation.js.map +1 -0
- package/dist/sharded-simulation-contracts.d.ts +240 -0
- package/dist/sharded-simulation-contracts.d.ts.map +1 -0
- package/dist/sharded-simulation-contracts.js +199 -0
- package/dist/sharded-simulation-contracts.js.map +1 -0
- package/dist/sharded-simulation-runtime.d.ts +64 -0
- package/dist/sharded-simulation-runtime.d.ts.map +1 -0
- package/dist/sharded-simulation-runtime.js +755 -0
- package/dist/sharded-simulation-runtime.js.map +1 -0
- package/dist/sharded-simulation-validation.d.ts +10 -0
- package/dist/sharded-simulation-validation.d.ts.map +1 -0
- package/dist/sharded-simulation-validation.js +250 -0
- package/dist/sharded-simulation-validation.js.map +1 -0
- package/dist/sharded-simulation.d.ts +46 -0
- package/dist/sharded-simulation.d.ts.map +1 -0
- package/dist/sharded-simulation.js +32 -0
- package/dist/sharded-simulation.js.map +1 -0
- package/dist/verified-benchmark-contracts.d.ts +132 -0
- package/dist/verified-benchmark-contracts.d.ts.map +1 -0
- package/dist/verified-benchmark-contracts.js +2 -0
- package/dist/verified-benchmark-contracts.js.map +1 -0
- package/dist/verified-benchmark-runtime.d.ts +20 -0
- package/dist/verified-benchmark-runtime.d.ts.map +1 -0
- package/dist/verified-benchmark-runtime.js +199 -0
- package/dist/verified-benchmark-runtime.js.map +1 -0
- package/dist/verified-benchmark-validation.d.ts +64 -0
- package/dist/verified-benchmark-validation.d.ts.map +1 -0
- package/dist/verified-benchmark-validation.js +481 -0
- package/dist/verified-benchmark-validation.js.map +1 -0
- package/dist/verified-benchmark.d.ts +4 -0
- package/dist/verified-benchmark.d.ts.map +1 -0
- package/dist/verified-benchmark.js +4 -0
- package/dist/verified-benchmark.js.map +1 -0
- package/package.json +18 -8
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { deepFreezePlanning, digestPlanningJsonV1, } from "@agentplat/collective-planning";
|
|
2
|
+
import { validateCollectiveClosedLoopDefinitionV1, validateCollectiveClosedLoopRunResultV1, } from "./collective-closed-loop-contracts.js";
|
|
3
|
+
/** The closed-loop reference surface deliberately bounds one compact fault plan. */
|
|
4
|
+
export const COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1 = Object.freeze({
|
|
5
|
+
maximumFaults: 8,
|
|
6
|
+
minimumEpochs: 2,
|
|
7
|
+
maximumEpochs: 3,
|
|
8
|
+
maximumLinksPerFault: 49,
|
|
9
|
+
maximumTargetsPerFault: 50,
|
|
10
|
+
maximumCausalPredecessorsPerFault: 7,
|
|
11
|
+
maximumStaleResultRejections: 8,
|
|
12
|
+
});
|
|
13
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
14
|
+
const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/u;
|
|
15
|
+
const faultFamilies = new Set([
|
|
16
|
+
"capability.withdraw",
|
|
17
|
+
"assignment.decline",
|
|
18
|
+
"peer.crash",
|
|
19
|
+
"peer.restart",
|
|
20
|
+
"network.partition",
|
|
21
|
+
"network.heal",
|
|
22
|
+
]);
|
|
23
|
+
const faultPlanBodyKeys = [
|
|
24
|
+
"schemaVersion",
|
|
25
|
+
"nominalDefinitionDigest",
|
|
26
|
+
"faults",
|
|
27
|
+
];
|
|
28
|
+
const faultPlanKeys = [...faultPlanBodyKeys, "faultPlanDigest"];
|
|
29
|
+
const resilienceDefinitionBodyKeys = [
|
|
30
|
+
"schemaVersion",
|
|
31
|
+
"nominalDefinition",
|
|
32
|
+
"faultPlan",
|
|
33
|
+
"maximumEpochs",
|
|
34
|
+
];
|
|
35
|
+
const resilienceDefinitionKeys = [
|
|
36
|
+
...resilienceDefinitionBodyKeys,
|
|
37
|
+
"resilienceDefinitionDigest",
|
|
38
|
+
];
|
|
39
|
+
const resilienceResultBodyKeys = [
|
|
40
|
+
"schemaVersion",
|
|
41
|
+
"resilienceDefinitionDigest",
|
|
42
|
+
"run",
|
|
43
|
+
"epochs",
|
|
44
|
+
"faultObservations",
|
|
45
|
+
"staleResultRejections",
|
|
46
|
+
];
|
|
47
|
+
const resilienceResultKeys = [
|
|
48
|
+
...resilienceResultBodyKeys,
|
|
49
|
+
"resilienceResultDigest",
|
|
50
|
+
];
|
|
51
|
+
const campaignEvidenceBodyKeys = [
|
|
52
|
+
"schemaVersion",
|
|
53
|
+
"resilienceDefinitionDigest",
|
|
54
|
+
"resilienceResultDigest",
|
|
55
|
+
"runner",
|
|
56
|
+
"seed",
|
|
57
|
+
"limits",
|
|
58
|
+
"scheduledFaultIds",
|
|
59
|
+
"injectedFaultIds",
|
|
60
|
+
"observedFaultIds",
|
|
61
|
+
"staleResultRejectionIds",
|
|
62
|
+
];
|
|
63
|
+
const campaignEvidenceKeys = [
|
|
64
|
+
...campaignEvidenceBodyKeys,
|
|
65
|
+
"campaignEvidenceDigest",
|
|
66
|
+
];
|
|
67
|
+
function exact(value, keys, label) {
|
|
68
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
69
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
70
|
+
const names = Object.getOwnPropertyNames(value).sort();
|
|
71
|
+
const expected = [...keys].sort();
|
|
72
|
+
const prototype = Object.getPrototypeOf(value);
|
|
73
|
+
if ((prototype !== Object.prototype && prototype !== null) ||
|
|
74
|
+
Object.getOwnPropertySymbols(value).length !== 0 ||
|
|
75
|
+
names.length !== expected.length ||
|
|
76
|
+
names.some((name, index) => name !== expected[index]))
|
|
77
|
+
throw new TypeError(`${label} has an invalid shape`);
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
80
|
+
if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
|
|
81
|
+
throw new TypeError(`${label} must contain enumerable data properties`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function denseArray(value, label) {
|
|
85
|
+
if (!Array.isArray(value) ||
|
|
86
|
+
Object.getPrototypeOf(value) !== Array.prototype ||
|
|
87
|
+
Object.getOwnPropertySymbols(value).length !== 0 ||
|
|
88
|
+
Object.getOwnPropertyNames(value).length !== value.length + 1)
|
|
89
|
+
throw new TypeError(`${label} must be a dense array`);
|
|
90
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
91
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
92
|
+
if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
|
|
93
|
+
throw new TypeError(`${label} must contain enumerable data properties`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function token(value, label) {
|
|
97
|
+
if (typeof value !== "string" ||
|
|
98
|
+
value.length === 0 ||
|
|
99
|
+
value.length > 128 ||
|
|
100
|
+
!tokenPattern.test(value))
|
|
101
|
+
throw new TypeError(`${label} must be a bounded token`);
|
|
102
|
+
}
|
|
103
|
+
function digest(value, label) {
|
|
104
|
+
if (typeof value !== "string" || !digestPattern.test(value))
|
|
105
|
+
throw new TypeError(`${label} must be a digest`);
|
|
106
|
+
}
|
|
107
|
+
function safeInteger(value, label, minimum = 0) {
|
|
108
|
+
if (!Number.isSafeInteger(value) ||
|
|
109
|
+
value < minimum ||
|
|
110
|
+
Object.is(value, -0))
|
|
111
|
+
throw new TypeError(`${label} must be a safe integer`);
|
|
112
|
+
}
|
|
113
|
+
function sortedTokens(value, label, maximum) {
|
|
114
|
+
denseArray(value, label);
|
|
115
|
+
if (value.length > maximum)
|
|
116
|
+
throw new TypeError(`${label} is too large`);
|
|
117
|
+
const result = [];
|
|
118
|
+
let previous = null;
|
|
119
|
+
for (const item of value) {
|
|
120
|
+
token(item, label);
|
|
121
|
+
if (previous !== null && previous >= item)
|
|
122
|
+
throw new TypeError(`${label} must be sorted and unique`);
|
|
123
|
+
previous = item;
|
|
124
|
+
result.push(item);
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze(result);
|
|
127
|
+
}
|
|
128
|
+
function digestBody(domain, value) {
|
|
129
|
+
return digestPlanningJsonV1("environment-state-v1", { domain, value }, {
|
|
130
|
+
maximumBytes: 1_048_576,
|
|
131
|
+
maximumDepth: 32,
|
|
132
|
+
maximumNodes: 32_768,
|
|
133
|
+
maximumKeysPerObject: 256,
|
|
134
|
+
maximumItemsPerArray: 4_096,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function validateTrigger(value) {
|
|
138
|
+
exact(value, ["schemaVersion", "kind", "logicalTimeMs", "causalEventDigest"], "closed-loop fault trigger");
|
|
139
|
+
if (value.schemaVersion !== 1)
|
|
140
|
+
throw new TypeError("closed-loop fault trigger schema is invalid");
|
|
141
|
+
safeInteger(value.logicalTimeMs, "fault trigger logicalTimeMs");
|
|
142
|
+
if (value.kind === "logical_time" && value.causalEventDigest === null)
|
|
143
|
+
return deepFreezePlanning({
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
kind: "logical_time",
|
|
146
|
+
logicalTimeMs: value.logicalTimeMs,
|
|
147
|
+
causalEventDigest: null,
|
|
148
|
+
});
|
|
149
|
+
if (value.kind === "trace_event") {
|
|
150
|
+
digest(value.causalEventDigest, "fault trigger causalEventDigest");
|
|
151
|
+
return deepFreezePlanning({
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
kind: "trace_event",
|
|
154
|
+
logicalTimeMs: value.logicalTimeMs,
|
|
155
|
+
causalEventDigest: value.causalEventDigest,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
throw new TypeError("closed-loop fault trigger is invalid");
|
|
159
|
+
}
|
|
160
|
+
function validateLink(value) {
|
|
161
|
+
exact(value, ["schemaVersion", "fromPeerId", "toPeerId"], "closed-loop fault link");
|
|
162
|
+
if (value.schemaVersion !== 1)
|
|
163
|
+
throw new TypeError("closed-loop fault link schema is invalid");
|
|
164
|
+
token(value.fromPeerId, "fault link fromPeerId");
|
|
165
|
+
token(value.toPeerId, "fault link toPeerId");
|
|
166
|
+
if (value.fromPeerId === value.toPeerId)
|
|
167
|
+
throw new TypeError("closed-loop fault link may not be reflexive");
|
|
168
|
+
return deepFreezePlanning({
|
|
169
|
+
schemaVersion: 1,
|
|
170
|
+
fromPeerId: value.fromPeerId,
|
|
171
|
+
toPeerId: value.toPeerId,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function validateTarget(value) {
|
|
175
|
+
exact(value, ["schemaVersion", "peerId"], "closed-loop fault target");
|
|
176
|
+
if (value.schemaVersion !== 1)
|
|
177
|
+
throw new TypeError("closed-loop fault target schema is invalid");
|
|
178
|
+
token(value.peerId, "fault target peerId");
|
|
179
|
+
return deepFreezePlanning({ schemaVersion: 1, peerId: value.peerId });
|
|
180
|
+
}
|
|
181
|
+
function compareLink(left, right) {
|
|
182
|
+
const leftKey = `${left.fromPeerId}\u0000${left.toPeerId}`;
|
|
183
|
+
const rightKey = `${right.fromPeerId}\u0000${right.toPeerId}`;
|
|
184
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
185
|
+
}
|
|
186
|
+
function sortedLinks(value) {
|
|
187
|
+
denseArray(value, "fault links");
|
|
188
|
+
if (value.length >
|
|
189
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumLinksPerFault)
|
|
190
|
+
throw new TypeError("fault links are too large");
|
|
191
|
+
const links = value.map(validateLink);
|
|
192
|
+
for (let index = 1; index < links.length; index += 1)
|
|
193
|
+
if (compareLink(links[index - 1], links[index]) >= 0)
|
|
194
|
+
throw new TypeError("fault links must be sorted and unique");
|
|
195
|
+
return Object.freeze(links);
|
|
196
|
+
}
|
|
197
|
+
function sortedTargets(value) {
|
|
198
|
+
denseArray(value, "fault targets");
|
|
199
|
+
if (value.length >
|
|
200
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumTargetsPerFault)
|
|
201
|
+
throw new TypeError("fault targets are too large");
|
|
202
|
+
const targets = value.map(validateTarget);
|
|
203
|
+
for (let index = 1; index < targets.length; index += 1)
|
|
204
|
+
if (targets[index - 1].peerId >= targets[index].peerId)
|
|
205
|
+
throw new TypeError("fault targets must be sorted and unique");
|
|
206
|
+
return Object.freeze(targets);
|
|
207
|
+
}
|
|
208
|
+
function validateFault(value) {
|
|
209
|
+
exact(value, [
|
|
210
|
+
"schemaVersion",
|
|
211
|
+
"faultId",
|
|
212
|
+
"family",
|
|
213
|
+
"trigger",
|
|
214
|
+
"causalPredecessorFaultIds",
|
|
215
|
+
"links",
|
|
216
|
+
"targets",
|
|
217
|
+
], "closed-loop fault");
|
|
218
|
+
if (value.schemaVersion !== 1 ||
|
|
219
|
+
!faultFamilies.has(value.family))
|
|
220
|
+
throw new TypeError("closed-loop fault family is invalid");
|
|
221
|
+
token(value.faultId, "faultId");
|
|
222
|
+
const trigger = validateTrigger(value.trigger);
|
|
223
|
+
const causalPredecessorFaultIds = sortedTokens(value.causalPredecessorFaultIds, "causalPredecessorFaultIds", COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumCausalPredecessorsPerFault);
|
|
224
|
+
if (causalPredecessorFaultIds.includes(value.faultId))
|
|
225
|
+
throw new TypeError("closed-loop fault may not causally depend on itself");
|
|
226
|
+
const links = sortedLinks(value.links);
|
|
227
|
+
const targets = sortedTargets(value.targets);
|
|
228
|
+
const networkFault = value.family === "network.partition" || value.family === "network.heal";
|
|
229
|
+
if ((networkFault && (links.length === 0 || targets.length !== 0)) ||
|
|
230
|
+
(!networkFault && (links.length !== 0 || targets.length === 0)))
|
|
231
|
+
throw new TypeError("closed-loop fault family has invalid links or targets");
|
|
232
|
+
return deepFreezePlanning({
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
faultId: value.faultId,
|
|
235
|
+
family: value.family,
|
|
236
|
+
trigger,
|
|
237
|
+
causalPredecessorFaultIds,
|
|
238
|
+
links,
|
|
239
|
+
targets,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function normalizedFaultPlanBody(value) {
|
|
243
|
+
if (value.schemaVersion !== 1)
|
|
244
|
+
throw new TypeError("closed-loop fault plan schema is invalid");
|
|
245
|
+
digest(value.nominalDefinitionDigest, "nominalDefinitionDigest");
|
|
246
|
+
denseArray(value.faults, "faults");
|
|
247
|
+
if (value.faults.length === 0 ||
|
|
248
|
+
value.faults.length >
|
|
249
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumFaults)
|
|
250
|
+
throw new TypeError("closed-loop fault count is invalid");
|
|
251
|
+
const faults = value.faults.map(validateFault);
|
|
252
|
+
const faultsById = new Map();
|
|
253
|
+
let previousId = null;
|
|
254
|
+
let previousLogicalTime = -1;
|
|
255
|
+
for (const fault of faults) {
|
|
256
|
+
if (previousId !== null && previousId >= fault.faultId)
|
|
257
|
+
throw new TypeError("closed-loop faults must be sorted and unique");
|
|
258
|
+
if (fault.trigger.logicalTimeMs < previousLogicalTime)
|
|
259
|
+
throw new TypeError("closed-loop faults must be ordered by trigger time");
|
|
260
|
+
previousId = fault.faultId;
|
|
261
|
+
previousLogicalTime = fault.trigger.logicalTimeMs;
|
|
262
|
+
faultsById.set(fault.faultId, fault);
|
|
263
|
+
}
|
|
264
|
+
for (const fault of faults) {
|
|
265
|
+
for (const predecessorId of fault.causalPredecessorFaultIds) {
|
|
266
|
+
const predecessor = faultsById.get(predecessorId);
|
|
267
|
+
if (!predecessor ||
|
|
268
|
+
predecessor.trigger.logicalTimeMs >= fault.trigger.logicalTimeMs)
|
|
269
|
+
throw new TypeError("closed-loop fault causal predecessor is invalid");
|
|
270
|
+
}
|
|
271
|
+
if (fault.family === "peer.restart" &&
|
|
272
|
+
!fault.causalPredecessorFaultIds.some((id) => {
|
|
273
|
+
const predecessor = faultsById.get(id);
|
|
274
|
+
return (predecessor?.family === "peer.crash" &&
|
|
275
|
+
predecessor.targets.some((target) => fault.targets.some((restart) => restart.peerId === target.peerId)));
|
|
276
|
+
}))
|
|
277
|
+
throw new TypeError("closed-loop peer restart requires a causal crash");
|
|
278
|
+
if (fault.family === "network.heal" &&
|
|
279
|
+
!fault.causalPredecessorFaultIds.some((id) => {
|
|
280
|
+
const predecessor = faultsById.get(id);
|
|
281
|
+
return (predecessor?.family === "network.partition" &&
|
|
282
|
+
predecessor.links.length === fault.links.length &&
|
|
283
|
+
predecessor.links.every((link, index) => compareLink(link, fault.links[index]) === 0));
|
|
284
|
+
}))
|
|
285
|
+
throw new TypeError("closed-loop network heal requires its causal partition");
|
|
286
|
+
}
|
|
287
|
+
return deepFreezePlanning({
|
|
288
|
+
schemaVersion: 1,
|
|
289
|
+
nominalDefinitionDigest: value.nominalDefinitionDigest,
|
|
290
|
+
faults,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
export function collectiveClosedLoopFaultPlanDigestV1(value) {
|
|
294
|
+
exact(value, faultPlanBodyKeys, "closed-loop fault plan body");
|
|
295
|
+
const body = normalizedFaultPlanBody(value);
|
|
296
|
+
return digestBody("collective-closed-loop-fault-plan-v1", body);
|
|
297
|
+
}
|
|
298
|
+
export function createCollectiveClosedLoopFaultPlanV1(input) {
|
|
299
|
+
exact(input, faultPlanBodyKeys, "closed-loop fault plan input");
|
|
300
|
+
const body = normalizedFaultPlanBody(input);
|
|
301
|
+
return deepFreezePlanning({
|
|
302
|
+
...body,
|
|
303
|
+
faultPlanDigest: collectiveClosedLoopFaultPlanDigestV1(body),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
export function validateCollectiveClosedLoopFaultPlanV1(value) {
|
|
307
|
+
exact(value, faultPlanKeys, "closed-loop fault plan");
|
|
308
|
+
digest(value.faultPlanDigest, "faultPlanDigest");
|
|
309
|
+
const body = normalizedFaultPlanBody(value);
|
|
310
|
+
const faultPlanDigest = collectiveClosedLoopFaultPlanDigestV1(body);
|
|
311
|
+
if (value.faultPlanDigest !== faultPlanDigest)
|
|
312
|
+
throw new TypeError("closed-loop fault plan digest is invalid");
|
|
313
|
+
return deepFreezePlanning({ ...body, faultPlanDigest });
|
|
314
|
+
}
|
|
315
|
+
function assertFaultPlanTargetsBound(plan, definition) {
|
|
316
|
+
const peers = new Map(definition.peers.map((peer) => [peer.peerId, peer]));
|
|
317
|
+
for (const fault of plan.faults) {
|
|
318
|
+
if (fault.trigger.logicalTimeMs > definition.maximumLogicalTimeMs)
|
|
319
|
+
throw new TypeError("closed-loop fault trigger exceeds the nominal logical-time bound");
|
|
320
|
+
for (const target of fault.targets)
|
|
321
|
+
if (!peers.has(target.peerId))
|
|
322
|
+
throw new TypeError("closed-loop fault target is outside the nominal definition");
|
|
323
|
+
for (const link of fault.links) {
|
|
324
|
+
const fromPeer = peers.get(link.fromPeerId);
|
|
325
|
+
if (!fromPeer || !peers.has(link.toPeerId))
|
|
326
|
+
throw new TypeError("closed-loop fault link is outside the nominal definition");
|
|
327
|
+
if (!fromPeer.neighborPeerIds.includes(link.toPeerId))
|
|
328
|
+
throw new TypeError("closed-loop fault link is outside the nominal topology");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function normalizedResilienceDefinitionBody(value) {
|
|
333
|
+
if (value.schemaVersion !== 1)
|
|
334
|
+
throw new TypeError("closed-loop resilience definition schema is invalid");
|
|
335
|
+
const nominalDefinition = validateCollectiveClosedLoopDefinitionV1(value.nominalDefinition);
|
|
336
|
+
const faultPlan = validateCollectiveClosedLoopFaultPlanV1(value.faultPlan);
|
|
337
|
+
safeInteger(value.maximumEpochs, "maximumEpochs", COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.minimumEpochs);
|
|
338
|
+
if (value.maximumEpochs >
|
|
339
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumEpochs)
|
|
340
|
+
throw new TypeError("maximumEpochs exceeds the resilience limit");
|
|
341
|
+
if (faultPlan.nominalDefinitionDigest !== nominalDefinition.definitionDigest)
|
|
342
|
+
throw new TypeError("closed-loop resilience fault plan is not bound to its nominal definition");
|
|
343
|
+
assertFaultPlanTargetsBound(faultPlan, nominalDefinition);
|
|
344
|
+
return deepFreezePlanning({
|
|
345
|
+
schemaVersion: 1,
|
|
346
|
+
nominalDefinition,
|
|
347
|
+
faultPlan,
|
|
348
|
+
maximumEpochs: value.maximumEpochs,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
export function collectiveClosedLoopResilienceDefinitionDigestV1(value) {
|
|
352
|
+
exact(value, resilienceDefinitionBodyKeys, "closed-loop resilience definition body");
|
|
353
|
+
const body = normalizedResilienceDefinitionBody(value);
|
|
354
|
+
return digestBody("collective-closed-loop-resilience-definition-v1", body);
|
|
355
|
+
}
|
|
356
|
+
export function createCollectiveClosedLoopResilienceDefinitionV1(input) {
|
|
357
|
+
exact(input, resilienceDefinitionBodyKeys, "closed-loop resilience definition input");
|
|
358
|
+
const body = normalizedResilienceDefinitionBody(input);
|
|
359
|
+
return deepFreezePlanning({
|
|
360
|
+
...body,
|
|
361
|
+
resilienceDefinitionDigest: collectiveClosedLoopResilienceDefinitionDigestV1(body),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
export function validateCollectiveClosedLoopResilienceDefinitionV1(value) {
|
|
365
|
+
exact(value, resilienceDefinitionKeys, "closed-loop resilience definition");
|
|
366
|
+
digest(value.resilienceDefinitionDigest, "resilienceDefinitionDigest");
|
|
367
|
+
const body = normalizedResilienceDefinitionBody(value);
|
|
368
|
+
const resilienceDefinitionDigest = collectiveClosedLoopResilienceDefinitionDigestV1(body);
|
|
369
|
+
if (value.resilienceDefinitionDigest !== resilienceDefinitionDigest)
|
|
370
|
+
throw new TypeError("closed-loop resilience definition digest is invalid");
|
|
371
|
+
return deepFreezePlanning({ ...body, resilienceDefinitionDigest });
|
|
372
|
+
}
|
|
373
|
+
function validateEpoch(value) {
|
|
374
|
+
exact(value, [
|
|
375
|
+
"schemaVersion",
|
|
376
|
+
"epoch",
|
|
377
|
+
"startedAtLogicalMs",
|
|
378
|
+
"endedAtLogicalMs",
|
|
379
|
+
"planningStateRoot",
|
|
380
|
+
"meshStateRoot",
|
|
381
|
+
"governanceStateRoot",
|
|
382
|
+
], "closed-loop resilience epoch");
|
|
383
|
+
if (value.schemaVersion !== 1)
|
|
384
|
+
throw new TypeError("closed-loop resilience epoch schema is invalid");
|
|
385
|
+
safeInteger(value.epoch, "epoch", 1);
|
|
386
|
+
safeInteger(value.startedAtLogicalMs, "startedAtLogicalMs");
|
|
387
|
+
safeInteger(value.endedAtLogicalMs, "endedAtLogicalMs");
|
|
388
|
+
if (value.endedAtLogicalMs < value.startedAtLogicalMs)
|
|
389
|
+
throw new TypeError("closed-loop resilience epoch ends before it starts");
|
|
390
|
+
digest(value.planningStateRoot, "planningStateRoot");
|
|
391
|
+
digest(value.meshStateRoot, "meshStateRoot");
|
|
392
|
+
digest(value.governanceStateRoot, "governanceStateRoot");
|
|
393
|
+
return deepFreezePlanning({
|
|
394
|
+
schemaVersion: 1,
|
|
395
|
+
epoch: value.epoch,
|
|
396
|
+
startedAtLogicalMs: value.startedAtLogicalMs,
|
|
397
|
+
endedAtLogicalMs: value.endedAtLogicalMs,
|
|
398
|
+
planningStateRoot: value.planningStateRoot,
|
|
399
|
+
meshStateRoot: value.meshStateRoot,
|
|
400
|
+
governanceStateRoot: value.governanceStateRoot,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function validateFaultObservation(value) {
|
|
404
|
+
exact(value, [
|
|
405
|
+
"schemaVersion",
|
|
406
|
+
"faultId",
|
|
407
|
+
"scheduledEventDigest",
|
|
408
|
+
"injectedEventDigest",
|
|
409
|
+
"observedEventDigest",
|
|
410
|
+
], "closed-loop fault observation");
|
|
411
|
+
if (value.schemaVersion !== 1)
|
|
412
|
+
throw new TypeError("closed-loop fault observation schema is invalid");
|
|
413
|
+
token(value.faultId, "fault observation faultId");
|
|
414
|
+
digest(value.scheduledEventDigest, "scheduledEventDigest");
|
|
415
|
+
digest(value.injectedEventDigest, "injectedEventDigest");
|
|
416
|
+
digest(value.observedEventDigest, "observedEventDigest");
|
|
417
|
+
return deepFreezePlanning({
|
|
418
|
+
schemaVersion: 1,
|
|
419
|
+
faultId: value.faultId,
|
|
420
|
+
scheduledEventDigest: value.scheduledEventDigest,
|
|
421
|
+
injectedEventDigest: value.injectedEventDigest,
|
|
422
|
+
observedEventDigest: value.observedEventDigest,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function validateStaleResultRejection(value) {
|
|
426
|
+
exact(value, [
|
|
427
|
+
"schemaVersion",
|
|
428
|
+
"rejectionId",
|
|
429
|
+
"faultId",
|
|
430
|
+
"rejectedAtLogicalMs",
|
|
431
|
+
"staleFenceDigest",
|
|
432
|
+
"currentFenceDigest",
|
|
433
|
+
"rejectionEventDigest",
|
|
434
|
+
], "closed-loop stale result rejection");
|
|
435
|
+
if (value.schemaVersion !== 1)
|
|
436
|
+
throw new TypeError("closed-loop stale result rejection schema is invalid");
|
|
437
|
+
token(value.rejectionId, "stale result rejectionId");
|
|
438
|
+
token(value.faultId, "stale result faultId");
|
|
439
|
+
safeInteger(value.rejectedAtLogicalMs, "stale result rejectedAtLogicalMs");
|
|
440
|
+
digest(value.staleFenceDigest, "staleFenceDigest");
|
|
441
|
+
digest(value.currentFenceDigest, "currentFenceDigest");
|
|
442
|
+
digest(value.rejectionEventDigest, "rejectionEventDigest");
|
|
443
|
+
if (value.staleFenceDigest === value.currentFenceDigest)
|
|
444
|
+
throw new TypeError("stale result rejection must bind distinct fences");
|
|
445
|
+
return deepFreezePlanning({
|
|
446
|
+
schemaVersion: 1,
|
|
447
|
+
rejectionId: value.rejectionId,
|
|
448
|
+
faultId: value.faultId,
|
|
449
|
+
rejectedAtLogicalMs: value.rejectedAtLogicalMs,
|
|
450
|
+
staleFenceDigest: value.staleFenceDigest,
|
|
451
|
+
currentFenceDigest: value.currentFenceDigest,
|
|
452
|
+
rejectionEventDigest: value.rejectionEventDigest,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function normalizedResilienceResultBody(value) {
|
|
456
|
+
if (value.schemaVersion !== 1)
|
|
457
|
+
throw new TypeError("closed-loop resilience result schema is invalid");
|
|
458
|
+
digest(value.resilienceDefinitionDigest, "resilienceDefinitionDigest");
|
|
459
|
+
const run = validateCollectiveClosedLoopRunResultV1(value.run);
|
|
460
|
+
denseArray(value.epochs, "epochs");
|
|
461
|
+
if (value.epochs.length <
|
|
462
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.minimumEpochs ||
|
|
463
|
+
value.epochs.length >
|
|
464
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumEpochs)
|
|
465
|
+
throw new TypeError("closed-loop resilience epoch count is invalid");
|
|
466
|
+
const epochs = value.epochs.map(validateEpoch);
|
|
467
|
+
for (let index = 0; index < epochs.length; index += 1) {
|
|
468
|
+
const epoch = epochs[index];
|
|
469
|
+
if (epoch.epoch !== index + 1 ||
|
|
470
|
+
(index === 0 && epoch.startedAtLogicalMs !== 0) ||
|
|
471
|
+
(index > 0 &&
|
|
472
|
+
epoch.startedAtLogicalMs !== epochs[index - 1].endedAtLogicalMs))
|
|
473
|
+
throw new TypeError("closed-loop resilience epochs are not ordered");
|
|
474
|
+
}
|
|
475
|
+
if (epochs[epochs.length - 1].endedAtLogicalMs !== run.finalLogicalTimeMs)
|
|
476
|
+
throw new TypeError("closed-loop resilience epochs do not close the run");
|
|
477
|
+
denseArray(value.faultObservations, "faultObservations");
|
|
478
|
+
const faultObservations = value.faultObservations.map(validateFaultObservation);
|
|
479
|
+
let previousFaultId = null;
|
|
480
|
+
const retainedEventDigests = new Set();
|
|
481
|
+
for (const observation of faultObservations) {
|
|
482
|
+
if (previousFaultId !== null && previousFaultId >= observation.faultId)
|
|
483
|
+
throw new TypeError("closed-loop fault observations must be sorted and unique");
|
|
484
|
+
for (const eventDigest of [
|
|
485
|
+
observation.scheduledEventDigest,
|
|
486
|
+
observation.injectedEventDigest,
|
|
487
|
+
observation.observedEventDigest,
|
|
488
|
+
]) {
|
|
489
|
+
if (retainedEventDigests.has(eventDigest))
|
|
490
|
+
throw new TypeError("closed-loop fault evidence digest is reused");
|
|
491
|
+
retainedEventDigests.add(eventDigest);
|
|
492
|
+
}
|
|
493
|
+
previousFaultId = observation.faultId;
|
|
494
|
+
}
|
|
495
|
+
denseArray(value.staleResultRejections, "staleResultRejections");
|
|
496
|
+
if (value.staleResultRejections.length >
|
|
497
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumStaleResultRejections)
|
|
498
|
+
throw new TypeError("closed-loop stale result rejections are too large");
|
|
499
|
+
const staleResultRejections = value.staleResultRejections.map(validateStaleResultRejection);
|
|
500
|
+
let previousRejectionId = null;
|
|
501
|
+
for (const rejection of staleResultRejections) {
|
|
502
|
+
if (previousRejectionId !== null &&
|
|
503
|
+
previousRejectionId >= rejection.rejectionId)
|
|
504
|
+
throw new TypeError("closed-loop stale result rejections must be sorted and unique");
|
|
505
|
+
if (rejection.rejectedAtLogicalMs > run.finalLogicalTimeMs)
|
|
506
|
+
throw new TypeError("closed-loop stale result rejection is outside the run");
|
|
507
|
+
if (rejection.rejectionEventDigest === rejection.staleFenceDigest ||
|
|
508
|
+
rejection.rejectionEventDigest === rejection.currentFenceDigest ||
|
|
509
|
+
retainedEventDigests.has(rejection.rejectionEventDigest))
|
|
510
|
+
throw new TypeError("closed-loop stale result evidence digest is reused");
|
|
511
|
+
retainedEventDigests.add(rejection.rejectionEventDigest);
|
|
512
|
+
previousRejectionId = rejection.rejectionId;
|
|
513
|
+
}
|
|
514
|
+
return deepFreezePlanning({
|
|
515
|
+
schemaVersion: 1,
|
|
516
|
+
resilienceDefinitionDigest: value.resilienceDefinitionDigest,
|
|
517
|
+
run,
|
|
518
|
+
epochs,
|
|
519
|
+
faultObservations,
|
|
520
|
+
staleResultRejections,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
export function collectiveClosedLoopResilienceResultDigestV1(value) {
|
|
524
|
+
exact(value, resilienceResultBodyKeys, "closed-loop resilience result body");
|
|
525
|
+
const body = normalizedResilienceResultBody(value);
|
|
526
|
+
return digestBody("collective-closed-loop-resilience-result-v1", body);
|
|
527
|
+
}
|
|
528
|
+
export function createCollectiveClosedLoopResilienceResultV1(input) {
|
|
529
|
+
exact(input, resilienceResultBodyKeys, "closed-loop resilience result input");
|
|
530
|
+
const body = normalizedResilienceResultBody(input);
|
|
531
|
+
return deepFreezePlanning({
|
|
532
|
+
...body,
|
|
533
|
+
resilienceResultDigest: collectiveClosedLoopResilienceResultDigestV1(body),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
export function validateCollectiveClosedLoopResilienceResultV1(value) {
|
|
537
|
+
exact(value, resilienceResultKeys, "closed-loop resilience result");
|
|
538
|
+
digest(value.resilienceResultDigest, "resilienceResultDigest");
|
|
539
|
+
const body = normalizedResilienceResultBody(value);
|
|
540
|
+
const resilienceResultDigest = collectiveClosedLoopResilienceResultDigestV1(body);
|
|
541
|
+
if (value.resilienceResultDigest !== resilienceResultDigest)
|
|
542
|
+
throw new TypeError("closed-loop resilience result digest is invalid");
|
|
543
|
+
return deepFreezePlanning({ ...body, resilienceResultDigest });
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Performs the cross-record checks deliberately kept out of the result digest
|
|
547
|
+
* constructor. Runners call this at finalization, after they hold the exact
|
|
548
|
+
* registered resilience definition.
|
|
549
|
+
*/
|
|
550
|
+
export function validateCollectiveClosedLoopResilienceResultForDefinitionV1(value, resilienceDefinitionInput) {
|
|
551
|
+
const result = validateCollectiveClosedLoopResilienceResultV1(value);
|
|
552
|
+
const definition = validateCollectiveClosedLoopResilienceDefinitionV1(resilienceDefinitionInput);
|
|
553
|
+
if (result.resilienceDefinitionDigest !==
|
|
554
|
+
definition.resilienceDefinitionDigest ||
|
|
555
|
+
result.run.registrationBindingDigest !==
|
|
556
|
+
definition.nominalDefinition.registration.bindingDigest ||
|
|
557
|
+
result.run.runner !== definition.nominalDefinition.registration.runner ||
|
|
558
|
+
result.run.finalLogicalTimeMs >
|
|
559
|
+
definition.nominalDefinition.maximumLogicalTimeMs ||
|
|
560
|
+
result.epochs.length > definition.maximumEpochs)
|
|
561
|
+
throw new TypeError("closed-loop resilience result is not bound to its definition");
|
|
562
|
+
const expectedFaultIds = definition.faultPlan.faults.map((fault) => fault.faultId);
|
|
563
|
+
const observedFaultIds = result.faultObservations.map((observation) => observation.faultId);
|
|
564
|
+
if (!sameStrings(expectedFaultIds, observedFaultIds))
|
|
565
|
+
throw new TypeError("closed-loop resilience result fault coverage is incomplete");
|
|
566
|
+
const faultsById = new Map(definition.faultPlan.faults.map((fault) => [fault.faultId, fault]));
|
|
567
|
+
for (const fault of definition.faultPlan.faults)
|
|
568
|
+
if (fault.trigger.logicalTimeMs > result.run.finalLogicalTimeMs)
|
|
569
|
+
throw new TypeError("closed-loop fault trigger is outside the run");
|
|
570
|
+
for (const rejection of result.staleResultRejections) {
|
|
571
|
+
const fault = faultsById.get(rejection.faultId);
|
|
572
|
+
if (fault === undefined ||
|
|
573
|
+
rejection.rejectedAtLogicalMs < fault.trigger.logicalTimeMs)
|
|
574
|
+
throw new TypeError("closed-loop stale result rejection is outside the fault plan");
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
function validateCampaignLimits(value) {
|
|
579
|
+
exact(value, ["schemaVersion", "maximumFaults", "maximumEpochs", "maximumInteractions"], "closed-loop resilience campaign limits");
|
|
580
|
+
if (value.schemaVersion !== 1)
|
|
581
|
+
throw new TypeError("closed-loop resilience campaign limits schema is invalid");
|
|
582
|
+
safeInteger(value.maximumFaults, "maximumFaults", 1);
|
|
583
|
+
safeInteger(value.maximumEpochs, "maximumEpochs", COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.minimumEpochs);
|
|
584
|
+
safeInteger(value.maximumInteractions, "maximumInteractions", 1);
|
|
585
|
+
if (value.maximumFaults >
|
|
586
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumFaults ||
|
|
587
|
+
value.maximumEpochs >
|
|
588
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumEpochs)
|
|
589
|
+
throw new TypeError("closed-loop resilience campaign limits exceed contract limits");
|
|
590
|
+
return deepFreezePlanning({
|
|
591
|
+
schemaVersion: 1,
|
|
592
|
+
maximumFaults: value.maximumFaults,
|
|
593
|
+
maximumEpochs: value.maximumEpochs,
|
|
594
|
+
maximumInteractions: value.maximumInteractions,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
function normalizedCampaignEvidenceBody(value) {
|
|
598
|
+
if (value.schemaVersion !== 1)
|
|
599
|
+
throw new TypeError("closed-loop resilience campaign evidence schema is invalid");
|
|
600
|
+
digest(value.resilienceDefinitionDigest, "resilienceDefinitionDigest");
|
|
601
|
+
digest(value.resilienceResultDigest, "resilienceResultDigest");
|
|
602
|
+
if (value.runner !== "adaptive_collective" &&
|
|
603
|
+
value.runner !== "centralized_planner")
|
|
604
|
+
throw new TypeError("closed-loop resilience campaign runner is invalid");
|
|
605
|
+
safeInteger(value.seed, "seed");
|
|
606
|
+
const limits = validateCampaignLimits(value.limits);
|
|
607
|
+
const scheduledFaultIds = sortedTokens(value.scheduledFaultIds, "scheduledFaultIds", limits.maximumFaults);
|
|
608
|
+
const injectedFaultIds = sortedTokens(value.injectedFaultIds, "injectedFaultIds", limits.maximumFaults);
|
|
609
|
+
const observedFaultIds = sortedTokens(value.observedFaultIds, "observedFaultIds", limits.maximumFaults);
|
|
610
|
+
const staleResultRejectionIds = sortedTokens(value.staleResultRejectionIds, "staleResultRejectionIds", COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumStaleResultRejections);
|
|
611
|
+
if (!sameStrings(scheduledFaultIds, injectedFaultIds) ||
|
|
612
|
+
!sameStrings(scheduledFaultIds, observedFaultIds))
|
|
613
|
+
throw new TypeError("closed-loop resilience campaign fault coverage is incomplete");
|
|
614
|
+
return deepFreezePlanning({
|
|
615
|
+
schemaVersion: 1,
|
|
616
|
+
resilienceDefinitionDigest: value.resilienceDefinitionDigest,
|
|
617
|
+
resilienceResultDigest: value.resilienceResultDigest,
|
|
618
|
+
runner: value.runner,
|
|
619
|
+
seed: value.seed,
|
|
620
|
+
limits,
|
|
621
|
+
scheduledFaultIds,
|
|
622
|
+
injectedFaultIds,
|
|
623
|
+
observedFaultIds,
|
|
624
|
+
staleResultRejectionIds,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function sameStrings(left, right) {
|
|
628
|
+
return (left.length === right.length &&
|
|
629
|
+
left.every((item, index) => item === right[index]));
|
|
630
|
+
}
|
|
631
|
+
export function collectiveClosedLoopResilienceCampaignEvidenceDigestV1(value) {
|
|
632
|
+
exact(value, campaignEvidenceBodyKeys, "closed-loop resilience campaign evidence body");
|
|
633
|
+
const body = normalizedCampaignEvidenceBody(value);
|
|
634
|
+
return digestBody("collective-closed-loop-resilience-campaign-evidence-v1", body);
|
|
635
|
+
}
|
|
636
|
+
export function createCollectiveClosedLoopResilienceCampaignEvidenceV1(input) {
|
|
637
|
+
exact(input, campaignEvidenceBodyKeys, "closed-loop resilience campaign evidence input");
|
|
638
|
+
const body = normalizedCampaignEvidenceBody(input);
|
|
639
|
+
return deepFreezePlanning({
|
|
640
|
+
...body,
|
|
641
|
+
campaignEvidenceDigest: collectiveClosedLoopResilienceCampaignEvidenceDigestV1(body),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
export function validateCollectiveClosedLoopResilienceCampaignEvidenceV1(value) {
|
|
645
|
+
exact(value, campaignEvidenceKeys, "closed-loop resilience campaign evidence");
|
|
646
|
+
digest(value.campaignEvidenceDigest, "campaignEvidenceDigest");
|
|
647
|
+
const body = normalizedCampaignEvidenceBody(value);
|
|
648
|
+
const campaignEvidenceDigest = collectiveClosedLoopResilienceCampaignEvidenceDigestV1(body);
|
|
649
|
+
if (value.campaignEvidenceDigest !== campaignEvidenceDigest)
|
|
650
|
+
throw new TypeError("closed-loop resilience campaign evidence digest is invalid");
|
|
651
|
+
return deepFreezePlanning({ ...body, campaignEvidenceDigest });
|
|
652
|
+
}
|
|
653
|
+
/** Binds campaign counters to one finalized result and its registered plan. */
|
|
654
|
+
export function validateCollectiveClosedLoopResilienceCampaignEvidenceForResultV1(value, resilienceDefinitionInput, resilienceResultInput) {
|
|
655
|
+
const evidence = validateCollectiveClosedLoopResilienceCampaignEvidenceV1(value);
|
|
656
|
+
const definition = validateCollectiveClosedLoopResilienceDefinitionV1(resilienceDefinitionInput);
|
|
657
|
+
const result = validateCollectiveClosedLoopResilienceResultForDefinitionV1(resilienceResultInput, definition);
|
|
658
|
+
const expectedFaultIds = definition.faultPlan.faults.map((fault) => fault.faultId);
|
|
659
|
+
const expectedRejectionIds = result.staleResultRejections.map((rejection) => rejection.rejectionId);
|
|
660
|
+
if (evidence.resilienceDefinitionDigest !==
|
|
661
|
+
definition.resilienceDefinitionDigest ||
|
|
662
|
+
evidence.resilienceResultDigest !== result.resilienceResultDigest ||
|
|
663
|
+
evidence.runner !== result.run.runner ||
|
|
664
|
+
evidence.seed !== definition.nominalDefinition.registration.seed ||
|
|
665
|
+
evidence.limits.maximumFaults !==
|
|
666
|
+
COLLECTIVE_CLOSED_LOOP_RESILIENCE_LIMITS_V1.maximumFaults ||
|
|
667
|
+
evidence.limits.maximumEpochs !== definition.maximumEpochs ||
|
|
668
|
+
evidence.limits.maximumInteractions !==
|
|
669
|
+
definition.nominalDefinition.registration.limits.maximumInteractions ||
|
|
670
|
+
!sameStrings(evidence.scheduledFaultIds, expectedFaultIds) ||
|
|
671
|
+
!sameStrings(evidence.injectedFaultIds, expectedFaultIds) ||
|
|
672
|
+
!sameStrings(evidence.observedFaultIds, expectedFaultIds) ||
|
|
673
|
+
!sameStrings(evidence.staleResultRejectionIds, expectedRejectionIds))
|
|
674
|
+
throw new TypeError("closed-loop resilience campaign evidence is not bound to its result");
|
|
675
|
+
return evidence;
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=collective-closed-loop-resilience-contracts.js.map
|