@kungfu-tech/buildchain 4.0.2-alpha.9 → 4.0.2
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/AGENTS.md +10 -1
- package/CONTRIBUTING.md +24 -0
- package/README.md +2 -2
- package/actions/promote-buildchain-ref/README.md +1 -1
- package/actions/v4-release-candidate-promote/README.md +28 -0
- package/architecture/agent-change-map.md +64 -7
- package/architecture/ci-lane-change-budget.json +366 -0
- package/architecture/internal-capabilities.json +53 -8
- package/architecture/maintainability-debt.json +147 -93
- package/architecture/maintainability-policy.json +32 -28
- package/architecture/release-tail-contract-inventory.json +6 -6
- package/architecture/v4-architecture-constitution.md +23 -20
- package/architecture/v4-cross-platform-adopter-qualification.json +1 -1
- package/architecture/v4-delivery-authority-parity.json +2 -2
- package/architecture/v4-delivery-warrant-shadow-bootstrap-plan.json +1 -1
- package/architecture/v4-floating-consumer-policy.json +2 -2
- package/architecture/v4-next-development-parity.json +5 -5
- package/architecture/v4-partial-mutation-recovery-qualification.json +4 -1
- package/architecture/v4-platform-stage-checkpoints.json +1 -1
- package/architecture/v4-provider-operation-journal-contract.json +5 -2
- package/architecture/v4-publication-facade-patches.json +64 -0
- package/architecture/v4-release-activation-shadow-domain.json +5 -2
- package/architecture/v4-release-topology.json +74 -39
- package/architecture/v4-rust-wasm-production-authority.json +58 -0
- package/architecture/v4-stable-publication-fence.json +5 -2
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/architecture/v4-tail-reseal-parity.json +8 -6
- package/architecture/v4-universal-workflow-bootstrap.json +171 -0
- package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
- package/architecture/v4-universal-workflow-train-admission.json +40 -0
- package/architecture/workflow-taxonomy.json +1451 -0
- package/bin/buildchain.mjs +7 -7
- package/contracts/buildchain-v2-residuals-v1.json +57 -48
- package/contracts/dev-delivery-authority-v2.schema.json +16 -2
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-canonical-contracts-v1.schema.json +1 -0
- package/dist/site/buildchain-contract.json +93 -48
- package/dist/site/buildchain-site.json +222 -69
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +50 -13
- package/dist/site/kfd-claims.json +2585 -1000
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +13 -13
- package/dist/site/node-api-registry.json +120 -115
- package/dist/site/page-registry.json +204 -51
- package/dist/site/public-surface-audit.json +3205 -1219
- package/dist/site/publication-authority-registry.json +1124 -362
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +16 -2
- package/dist/site/site-manifest.json +17 -17
- package/dist/site/workflow-registry.json +5061 -1659
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +1 -1
- package/docs/cli.md +21 -8
- package/docs/dev-delivery-warrant.md +1 -1
- package/docs/engineering-housekeeper.md +3 -3
- package/docs/kfd-support.md +26 -2
- package/docs/next-development-transition.md +2 -2
- package/docs/node-api-reference.md +190 -190
- package/docs/publication-authority.md +2 -2
- package/docs/release-candidate.md +3 -3
- package/docs/release-flow.md +67 -19
- package/docs/release-governance.md +8 -8
- package/docs/release-passport.md +27 -2
- package/docs/reusable-build-surface.md +16 -0
- package/docs/v4-adopter-delivery.md +2 -2
- package/docs/v4-production-release.md +71 -8
- package/docs/v4-rust-wasm-production-authority.md +58 -0
- package/docs/v4-stage-capsule.md +2 -2
- package/docs/v4-tail-reseal.md +1 -1
- package/docs/workflow-catalog.md +154 -0
- package/docs/workflow-path-migration.md +109 -0
- package/package.json +10 -6
- package/packages/core/artifact-verification-envelope.js +5 -5
- package/packages/core/buildchain-contract.js +1 -1
- package/packages/core/buildchain-kfd-claims.js +2 -2
- package/packages/core/buildchain-publication-authority.js +42 -26
- package/packages/core/buildchain-v4-domain.wasm +0 -0
- package/packages/core/ci-lane-change-budget.js +14 -1
- package/packages/core/controller-evidence.js +4 -1
- package/packages/core/dev-delivery-candidate-identity.js +40 -6
- package/packages/core/dev-delivery-warrant-state.js +3 -4
- package/packages/core/dev-delivery-warrant.js +12 -2
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/kfd-adopter-manifest.js +46 -47
- package/packages/core/next-development-projection.js +2 -2
- package/packages/core/paper-agent-entry.js +30 -4
- package/packages/core/paper-fleet.js +1 -1
- package/packages/core/paper-runtime-channels.js +348 -0
- package/packages/core/paper-scaffold-content.js +10 -0
- package/packages/core/paper.js +37 -71
- package/packages/core/release-line-dry-run.js +1 -1
- package/packages/core/release-passport-contract.js +5 -5
- package/packages/core/release-passport.js +5 -4
- package/packages/core/release-tail-provider-plane.js +68 -1048
- package/packages/core/v4-canonical-contracts.js +10 -75
- package/packages/core/v4-domain-wasm-artifact.js +7 -0
- package/packages/core/v4-domain-wasm.js +216 -0
- package/packages/core/v4-floating-consumer-evidence.js +1 -1
- package/packages/core/v4-floating-consumer-policy.js +1 -1
- package/packages/core/v4-partial-mutation-recovery-qualification.js +9 -386
- package/packages/core/v4-product-publication.js +19 -349
- package/packages/core/v4-protected-publication-source.js +19 -21
- package/packages/core/v4-provider-operation-journal.js +18 -487
- package/packages/core/v4-provider-readback-idempotency.js +12 -97
- package/packages/core/v4-release-activation-shadow.js +13 -500
- package/packages/core/v4-release-invocation.js +28 -389
- package/packages/core/v4-stable-publication-fence.js +13 -342
- package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
- package/scripts/binary-publication-evidence.mjs +135 -0
- package/scripts/build-contract-core.mjs +1 -14
- package/scripts/build-v4-domain-wasm.mjs +189 -0
- package/scripts/check-action-bundles.mjs +12 -2
- package/scripts/check-ci-lane-change-budget.mjs +4 -1
- package/scripts/check-core-mechanism-inventory.mjs +4 -3
- package/scripts/check-inventory.mjs +35 -44
- package/scripts/check-maintainability.mjs +16 -113
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +2 -2
- package/scripts/check-v4-public-dogfood-contract.mjs +22 -9
- package/scripts/check-v4-release-topology.mjs +90 -33
- package/scripts/check-v4-runtime-semantic-closure.mjs +4 -1
- package/scripts/check-v4-universal-workflow-bootstrap.mjs +266 -0
- package/scripts/check-workflow-taxonomy.mjs +6 -0
- package/scripts/copy-v4-domain-wasm.mjs +14 -0
- package/scripts/dev-delivery-authority-command-adapters.mjs +1 -1
- package/scripts/dev-delivery-authority.mjs +8 -8
- package/scripts/dev-delivery-source-proof-reuse.mjs +2 -1
- package/scripts/dev-delivery-warrant-options.mjs +14 -9
- package/scripts/dev-delivery-warrant-store.mjs +73 -7
- package/scripts/dev-delivery-warrant-transition.mjs +109 -0
- package/scripts/dev-delivery-warrant.mjs +76 -99
- package/scripts/dev-pr-auto-merge.mjs +15 -15
- package/scripts/generate-channel-build-workflow.mjs +7 -2
- package/scripts/generate-channel-promotion-workflow.mjs +7 -5
- package/scripts/generate-site-bundle.mjs +11 -11
- package/scripts/generate-v4-universal-workflow-facades.mjs +458 -0
- package/scripts/generate-workflow-taxonomy.mjs +25 -0
- package/scripts/github-output.mjs +31 -0
- package/scripts/maintainability-metrics.mjs +16 -5
- package/scripts/maintainability-public-surface.mjs +115 -0
- package/scripts/next-development-review.mjs +307 -0
- package/scripts/paper-work-fleet-cli.mjs +2 -0
- package/scripts/release-asset-client.mjs +91 -0
- package/scripts/release-candidate-resolver.mjs +9 -9
- package/scripts/source-verification-evidence.mjs +344 -0
- package/scripts/stable-candidate-qualification.mjs +1 -1
- package/scripts/universal-facade-maintainability.mjs +82 -0
- package/scripts/v4-product-publication-intent.mjs +13 -6
- package/scripts/v4-publication-settlement.mjs +211 -0
- package/scripts/v4-universal-workflow-backflow.mjs +212 -0
- package/scripts/v4-universal-workflow-engine.mjs +618 -0
- package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
- package/scripts/verify-version-state-delta.mjs +173 -0
- package/scripts/workflow-taxonomy.mjs +507 -0
- package/templates/native-dev-delivery.yml +4 -7
- package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
- package/templates/universal-buildchain-bootstrap.yml +28 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { invokeV4DomainWasm } from "./v4-domain-wasm.js";
|
|
4
5
|
import { RELEASE_TAIL_PRODUCT_CAPABILITIES } from "./release-tail-product-capabilities.js";
|
|
6
|
+
|
|
5
7
|
export const RELEASE_TAIL_DECLARATION_CONTRACT =
|
|
6
8
|
"kungfu-buildchain-release-tail-capabilities";
|
|
7
9
|
export const RELEASE_TAIL_TRANSACTION_POLICY = "buildchain.release-tail/v1";
|
|
@@ -27,6 +29,7 @@ export const RELEASE_TAIL_STATES = Object.freeze([
|
|
|
27
29
|
"repair-required",
|
|
28
30
|
"terminal-failure",
|
|
29
31
|
]);
|
|
32
|
+
|
|
30
33
|
export const RELEASE_TAIL_CAPABILITY_REGISTRY = Object.freeze([
|
|
31
34
|
...RELEASE_TAIL_PRODUCT_CAPABILITIES,
|
|
32
35
|
Object.freeze({
|
|
@@ -67,425 +70,13 @@ export const RELEASE_TAIL_CAPABILITY_REGISTRY = Object.freeze([
|
|
|
67
70
|
}),
|
|
68
71
|
]);
|
|
69
72
|
|
|
70
|
-
const CAPABILITY_BY_ID = new Map(
|
|
71
|
-
RELEASE_TAIL_CAPABILITY_REGISTRY.map((entry) => [entry.id, entry]),
|
|
72
|
-
);
|
|
73
|
-
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
74
|
-
const SHA = /^[0-9a-f]{40}$/u;
|
|
75
|
-
const FORBIDDEN_KEYS = new Set([
|
|
76
|
-
"cmd",
|
|
77
|
-
"command",
|
|
78
|
-
"eval",
|
|
79
|
-
"executable",
|
|
80
|
-
"javascript",
|
|
81
|
-
"run",
|
|
82
|
-
"script",
|
|
83
|
-
"shell",
|
|
84
|
-
]);
|
|
85
|
-
const TOP_LEVEL_FIELDS = [
|
|
86
|
-
"contract",
|
|
87
|
-
"schemaVersion",
|
|
88
|
-
"transactionPolicy",
|
|
89
|
-
"subject",
|
|
90
|
-
"capabilities",
|
|
91
|
-
];
|
|
92
|
-
const SUBJECT_FIELDS = ["repository", "sourceSha", "version", "tag", "channel"];
|
|
93
|
-
const CAPABILITY_FIELDS = [
|
|
94
|
-
"id",
|
|
95
|
-
"executor",
|
|
96
|
-
"adapter",
|
|
97
|
-
"artifactRoles",
|
|
98
|
-
"destination",
|
|
99
|
-
"channelPolicy",
|
|
100
|
-
"activationPolicy",
|
|
101
|
-
"readbackPredicates",
|
|
102
|
-
"effect",
|
|
103
|
-
"observation",
|
|
104
|
-
"receipt",
|
|
105
|
-
"operationIdentity",
|
|
106
|
-
"idempotency",
|
|
107
|
-
"retry",
|
|
108
|
-
"evidenceRequirements",
|
|
109
|
-
];
|
|
110
|
-
const EFFECT_PLAN_FIELDS = [
|
|
111
|
-
"schema",
|
|
112
|
-
"transactionPolicy",
|
|
113
|
-
"transactionRoot",
|
|
114
|
-
"declarationRoot",
|
|
115
|
-
"subject",
|
|
116
|
-
"operationOrder",
|
|
117
|
-
"effects",
|
|
118
|
-
"planRoot",
|
|
119
|
-
];
|
|
120
|
-
const EFFECT_FIELDS = [
|
|
121
|
-
"schema",
|
|
122
|
-
"kind",
|
|
123
|
-
"transactionRoot",
|
|
124
|
-
"operationId",
|
|
125
|
-
"capabilityId",
|
|
126
|
-
"executor",
|
|
127
|
-
"adapter",
|
|
128
|
-
"subjectRoot",
|
|
129
|
-
"targetRoot",
|
|
130
|
-
"attemptKey",
|
|
131
|
-
"subject",
|
|
132
|
-
"artifactRoles",
|
|
133
|
-
"destination",
|
|
134
|
-
"channelPolicy",
|
|
135
|
-
"activationPolicy",
|
|
136
|
-
"readbackPredicates",
|
|
137
|
-
"idempotency",
|
|
138
|
-
"retry",
|
|
139
|
-
"evidenceRequirements",
|
|
140
|
-
"effectRoot",
|
|
141
|
-
];
|
|
142
|
-
const TERMINAL_STATES = new Set([
|
|
143
|
-
"complete",
|
|
144
|
-
"blocked",
|
|
145
|
-
"repair-required",
|
|
146
|
-
"terminal-failure",
|
|
147
|
-
]);
|
|
148
|
-
|
|
149
73
|
export function releaseTailStableJson(value) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (value && typeof value === "object") {
|
|
154
|
-
return `{${Object.keys(value)
|
|
155
|
-
.sort()
|
|
156
|
-
.map(
|
|
157
|
-
(key) => `${JSON.stringify(key)}:${releaseTailStableJson(value[key])}`,
|
|
158
|
-
)
|
|
159
|
-
.join(",")}}`;
|
|
160
|
-
}
|
|
161
|
-
return JSON.stringify(value);
|
|
74
|
+
const canonical = invokeV4DomainWasm("canonical-json", value).canonicalUtf8;
|
|
75
|
+
return canonical.endsWith("\n") ? canonical.slice(0, -1) : canonical;
|
|
162
76
|
}
|
|
163
77
|
|
|
164
78
|
export function releaseTailRoot(value) {
|
|
165
|
-
return
|
|
166
|
-
.createHash("sha256")
|
|
167
|
-
.update(releaseTailStableJson(value))
|
|
168
|
-
.digest("hex")}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function fail(message) {
|
|
172
|
-
throw new Error(`invalid release-tail declaration: ${message}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function plainObject(value, label) {
|
|
176
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
177
|
-
fail(`${label} must be an object`);
|
|
178
|
-
}
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function exactFields(value, fields, label) {
|
|
183
|
-
plainObject(value, label);
|
|
184
|
-
const actual = Object.keys(value).sort();
|
|
185
|
-
const expected = [...fields].sort();
|
|
186
|
-
if (actual.join("\n") !== expected.join("\n")) {
|
|
187
|
-
fail(`${label} fields must be exactly: ${fields.join(", ")}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function nonEmpty(value, label) {
|
|
192
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
193
|
-
fail(`${label} must be a non-empty string`);
|
|
194
|
-
}
|
|
195
|
-
return value;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function exactRoot(value, label) {
|
|
199
|
-
const normalized = nonEmpty(value, label).toLowerCase();
|
|
200
|
-
if (!ROOT.test(normalized)) fail(`${label} must be a sha256 content root`);
|
|
201
|
-
return normalized;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function oneOf(value, choices, label) {
|
|
205
|
-
if (!choices.includes(value)) {
|
|
206
|
-
fail(`${label} must be one of ${choices.join(", ")}`);
|
|
207
|
-
}
|
|
208
|
-
return value;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function rejectExecutableKeys(value, pointer = "$") {
|
|
212
|
-
if (Array.isArray(value)) {
|
|
213
|
-
value.forEach((entry, index) =>
|
|
214
|
-
rejectExecutableKeys(entry, `${pointer}/${index}`),
|
|
215
|
-
);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (!value || typeof value !== "object") return;
|
|
219
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
220
|
-
if (FORBIDDEN_KEYS.has(key.toLowerCase())) {
|
|
221
|
-
fail(`executable key '${key}' is forbidden at ${pointer}`);
|
|
222
|
-
}
|
|
223
|
-
rejectExecutableKeys(entry, `${pointer}/${key}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function normalizeMessage(value, expectedSchema, expectedKind, label) {
|
|
228
|
-
exactFields(value, ["kind", "schema"], label);
|
|
229
|
-
if (value.schema !== expectedSchema) fail(`${label}.schema is not supported`);
|
|
230
|
-
if (value.kind !== expectedKind)
|
|
231
|
-
fail(`${label}.kind must be ${expectedKind}`);
|
|
232
|
-
return { kind: value.kind, schema: value.schema };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function normalizeCapability(value, subject) {
|
|
236
|
-
exactFields(
|
|
237
|
-
value,
|
|
238
|
-
CAPABILITY_FIELDS,
|
|
239
|
-
`capability ${value?.id || "<unknown>"}`,
|
|
240
|
-
);
|
|
241
|
-
const registry = CAPABILITY_BY_ID.get(value.id);
|
|
242
|
-
if (!registry) fail(`unsupported capability id: ${value.id || "<empty>"}`);
|
|
243
|
-
if (value.executor !== registry.executor) {
|
|
244
|
-
fail(`${value.id}.executor must be ${registry.executor}`);
|
|
245
|
-
}
|
|
246
|
-
if (value.adapter !== registry.adapter) {
|
|
247
|
-
fail(`${value.id}.adapter must be ${registry.adapter}`);
|
|
248
|
-
}
|
|
249
|
-
if (!Array.isArray(value.artifactRoles)) {
|
|
250
|
-
fail(`${value.id}.artifactRoles must be an array`);
|
|
251
|
-
}
|
|
252
|
-
const artifactRoles = value.artifactRoles.map((entry, index) => {
|
|
253
|
-
exactFields(entry, ["role", "root"], `${value.id}.artifactRoles[${index}]`);
|
|
254
|
-
return {
|
|
255
|
-
role: nonEmpty(entry.role, `${value.id}.artifactRoles[${index}].role`),
|
|
256
|
-
root: exactRoot(entry.root, `${value.id}.artifactRoles[${index}].root`),
|
|
257
|
-
};
|
|
258
|
-
});
|
|
259
|
-
if (
|
|
260
|
-
new Set(artifactRoles.map((entry) => entry.role)).size !==
|
|
261
|
-
artifactRoles.length
|
|
262
|
-
) {
|
|
263
|
-
fail(`${value.id}.artifactRoles contains duplicate roles`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
exactFields(
|
|
267
|
-
value.destination,
|
|
268
|
-
["kind", "locator"],
|
|
269
|
-
`${value.id}.destination`,
|
|
270
|
-
);
|
|
271
|
-
const destination = {
|
|
272
|
-
kind: nonEmpty(value.destination.kind, `${value.id}.destination.kind`),
|
|
273
|
-
locator: nonEmpty(
|
|
274
|
-
value.destination.locator,
|
|
275
|
-
`${value.id}.destination.locator`,
|
|
276
|
-
),
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
exactFields(
|
|
280
|
-
value.channelPolicy,
|
|
281
|
-
["channel", "tagPattern", "authorityMove"],
|
|
282
|
-
`${value.id}.channelPolicy`,
|
|
283
|
-
);
|
|
284
|
-
const channel = oneOf(
|
|
285
|
-
value.channelPolicy.channel,
|
|
286
|
-
["alpha", "release", "stable"],
|
|
287
|
-
`${value.id}.channelPolicy.channel`,
|
|
288
|
-
);
|
|
289
|
-
if (channel !== subject.channel)
|
|
290
|
-
fail(`${value.id} channel does not match subject`);
|
|
291
|
-
const tagPattern = nonEmpty(
|
|
292
|
-
value.channelPolicy.tagPattern,
|
|
293
|
-
`${value.id}.channelPolicy.tagPattern`,
|
|
294
|
-
);
|
|
295
|
-
let tagExpression;
|
|
296
|
-
try {
|
|
297
|
-
tagExpression = new RegExp(tagPattern, "u");
|
|
298
|
-
} catch {
|
|
299
|
-
fail(`${value.id}.channelPolicy.tagPattern is not a valid expression`);
|
|
300
|
-
}
|
|
301
|
-
if (!tagExpression.test(subject.tag)) {
|
|
302
|
-
fail(`${value.id}.channelPolicy.tagPattern does not match subject.tag`);
|
|
303
|
-
}
|
|
304
|
-
const channelPolicy = {
|
|
305
|
-
channel,
|
|
306
|
-
tagPattern,
|
|
307
|
-
authorityMove: oneOf(
|
|
308
|
-
value.channelPolicy.authorityMove,
|
|
309
|
-
["none", "signed-cas", "verified-ref"],
|
|
310
|
-
`${value.id}.channelPolicy.authorityMove`,
|
|
311
|
-
),
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
exactFields(
|
|
315
|
-
value.activationPolicy,
|
|
316
|
-
["mode", "environment"],
|
|
317
|
-
`${value.id}.activationPolicy`,
|
|
318
|
-
);
|
|
319
|
-
const activationPolicy = {
|
|
320
|
-
mode: oneOf(
|
|
321
|
-
value.activationPolicy.mode,
|
|
322
|
-
["none", "receipt-set", "receipt-only"],
|
|
323
|
-
`${value.id}.activationPolicy.mode`,
|
|
324
|
-
),
|
|
325
|
-
environment: oneOf(
|
|
326
|
-
value.activationPolicy.environment,
|
|
327
|
-
["none", "shadow", "production"],
|
|
328
|
-
`${value.id}.activationPolicy.environment`,
|
|
329
|
-
),
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
!Array.isArray(value.readbackPredicates) ||
|
|
334
|
-
value.readbackPredicates.length === 0
|
|
335
|
-
) {
|
|
336
|
-
fail(`${value.id} must declare at least one readback predicate`);
|
|
337
|
-
}
|
|
338
|
-
const readbackPredicates = value.readbackPredicates.map((entry, index) => {
|
|
339
|
-
exactFields(
|
|
340
|
-
entry,
|
|
341
|
-
["id", "kind", "expected"],
|
|
342
|
-
`${value.id}.readbackPredicates[${index}]`,
|
|
343
|
-
);
|
|
344
|
-
return {
|
|
345
|
-
id: nonEmpty(entry.id, `${value.id}.readbackPredicates[${index}].id`),
|
|
346
|
-
kind: nonEmpty(
|
|
347
|
-
entry.kind,
|
|
348
|
-
`${value.id}.readbackPredicates[${index}].kind`,
|
|
349
|
-
),
|
|
350
|
-
expected: nonEmpty(
|
|
351
|
-
entry.expected,
|
|
352
|
-
`${value.id}.readbackPredicates[${index}].expected`,
|
|
353
|
-
),
|
|
354
|
-
};
|
|
355
|
-
});
|
|
356
|
-
if (
|
|
357
|
-
new Set(readbackPredicates.map((entry) => entry.id)).size !==
|
|
358
|
-
readbackPredicates.length
|
|
359
|
-
) {
|
|
360
|
-
fail(`${value.id}.readbackPredicates contains duplicate ids`);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
exactFields(
|
|
364
|
-
value.operationIdentity,
|
|
365
|
-
[
|
|
366
|
-
"transactionRoot",
|
|
367
|
-
"capabilityId",
|
|
368
|
-
"subjectRoot",
|
|
369
|
-
"targetRoot",
|
|
370
|
-
"attemptKey",
|
|
371
|
-
],
|
|
372
|
-
`${value.id}.operationIdentity`,
|
|
373
|
-
);
|
|
374
|
-
if (value.operationIdentity.capabilityId !== value.id) {
|
|
375
|
-
fail(
|
|
376
|
-
`${value.id}.operationIdentity.capabilityId must match the capability id`,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
const operationIdentity = {
|
|
380
|
-
transactionRoot: exactRoot(
|
|
381
|
-
value.operationIdentity.transactionRoot,
|
|
382
|
-
`${value.id}.operationIdentity.transactionRoot`,
|
|
383
|
-
),
|
|
384
|
-
capabilityId: value.id,
|
|
385
|
-
subjectRoot: exactRoot(
|
|
386
|
-
value.operationIdentity.subjectRoot,
|
|
387
|
-
`${value.id}.operationIdentity.subjectRoot`,
|
|
388
|
-
),
|
|
389
|
-
targetRoot: exactRoot(
|
|
390
|
-
value.operationIdentity.targetRoot,
|
|
391
|
-
`${value.id}.operationIdentity.targetRoot`,
|
|
392
|
-
),
|
|
393
|
-
attemptKey: nonEmpty(
|
|
394
|
-
value.operationIdentity.attemptKey,
|
|
395
|
-
`${value.id}.operationIdentity.attemptKey`,
|
|
396
|
-
),
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
exactFields(
|
|
400
|
-
value.idempotency,
|
|
401
|
-
["scope", "duplicate"],
|
|
402
|
-
`${value.id}.idempotency`,
|
|
403
|
-
);
|
|
404
|
-
const idempotency = {
|
|
405
|
-
scope: oneOf(
|
|
406
|
-
value.idempotency.scope,
|
|
407
|
-
["operation", "subject-target"],
|
|
408
|
-
`${value.id}.idempotency.scope`,
|
|
409
|
-
),
|
|
410
|
-
duplicate: oneOf(
|
|
411
|
-
value.idempotency.duplicate,
|
|
412
|
-
["return-observed-receipt", "readback-before-retry"],
|
|
413
|
-
`${value.id}.idempotency.duplicate`,
|
|
414
|
-
),
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
exactFields(
|
|
418
|
-
value.retry,
|
|
419
|
-
["class", "localAttempts", "exhausted"],
|
|
420
|
-
`${value.id}.retry`,
|
|
421
|
-
);
|
|
422
|
-
const localAttempts = value.retry.localAttempts;
|
|
423
|
-
if (
|
|
424
|
-
!Number.isSafeInteger(localAttempts) ||
|
|
425
|
-
localAttempts < 0 ||
|
|
426
|
-
localAttempts > 3
|
|
427
|
-
) {
|
|
428
|
-
fail(`${value.id}.retry.localAttempts must be between zero and three`);
|
|
429
|
-
}
|
|
430
|
-
const retry = {
|
|
431
|
-
class: oneOf(
|
|
432
|
-
value.retry.class,
|
|
433
|
-
["never", "readback", "provider-transient"],
|
|
434
|
-
`${value.id}.retry.class`,
|
|
435
|
-
),
|
|
436
|
-
localAttempts,
|
|
437
|
-
exhausted: oneOf(
|
|
438
|
-
value.retry.exhausted,
|
|
439
|
-
["blocked", "repair-required", "terminal-failure"],
|
|
440
|
-
`${value.id}.retry.exhausted`,
|
|
441
|
-
),
|
|
442
|
-
};
|
|
443
|
-
if (retry.class === "never" && retry.localAttempts !== 0) {
|
|
444
|
-
fail(`${value.id}.retry.localAttempts must be zero for retry class never`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
!Array.isArray(value.evidenceRequirements) ||
|
|
449
|
-
value.evidenceRequirements.length === 0
|
|
450
|
-
) {
|
|
451
|
-
fail(`${value.id}.evidenceRequirements must not be empty`);
|
|
452
|
-
}
|
|
453
|
-
const evidenceRequirements = value.evidenceRequirements.map((entry, index) =>
|
|
454
|
-
nonEmpty(entry, `${value.id}.evidenceRequirements[${index}]`),
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
return {
|
|
458
|
-
id: value.id,
|
|
459
|
-
executor: value.executor,
|
|
460
|
-
adapter: value.adapter,
|
|
461
|
-
artifactRoles,
|
|
462
|
-
destination,
|
|
463
|
-
channelPolicy,
|
|
464
|
-
activationPolicy,
|
|
465
|
-
readbackPredicates,
|
|
466
|
-
effect: normalizeMessage(
|
|
467
|
-
value.effect,
|
|
468
|
-
RELEASE_TAIL_EFFECT_SCHEMA,
|
|
469
|
-
registry.effectKind,
|
|
470
|
-
`${value.id}.effect`,
|
|
471
|
-
),
|
|
472
|
-
observation: normalizeMessage(
|
|
473
|
-
value.observation,
|
|
474
|
-
RELEASE_TAIL_OBSERVATION_SCHEMA,
|
|
475
|
-
registry.observationKind,
|
|
476
|
-
`${value.id}.observation`,
|
|
477
|
-
),
|
|
478
|
-
receipt: normalizeMessage(
|
|
479
|
-
value.receipt,
|
|
480
|
-
RELEASE_TAIL_RECEIPT_SCHEMA,
|
|
481
|
-
registry.receiptKind,
|
|
482
|
-
`${value.id}.receipt`,
|
|
483
|
-
),
|
|
484
|
-
operationIdentity,
|
|
485
|
-
idempotency,
|
|
486
|
-
retry,
|
|
487
|
-
evidenceRequirements,
|
|
488
|
-
};
|
|
79
|
+
return invokeV4DomainWasm("release-tail-root", value).root;
|
|
489
80
|
}
|
|
490
81
|
|
|
491
82
|
export function parseReleaseTailDeclaration(input) {
|
|
@@ -494,337 +85,31 @@ export function parseReleaseTailDeclaration(input) {
|
|
|
494
85
|
try {
|
|
495
86
|
value = JSON.parse(input);
|
|
496
87
|
} catch (error) {
|
|
497
|
-
|
|
88
|
+
throw new Error(
|
|
89
|
+
`invalid release-tail declaration: input is not valid JSON: ${error.message}`,
|
|
90
|
+
);
|
|
498
91
|
}
|
|
499
92
|
}
|
|
500
|
-
|
|
501
|
-
rejectExecutableKeys(value);
|
|
502
|
-
exactFields(value, TOP_LEVEL_FIELDS, "declaration");
|
|
503
|
-
if (value.contract !== RELEASE_TAIL_DECLARATION_CONTRACT) {
|
|
504
|
-
fail(`contract must be ${RELEASE_TAIL_DECLARATION_CONTRACT}`);
|
|
505
|
-
}
|
|
506
|
-
if (value.schemaVersion !== 1) fail("schemaVersion must be 1");
|
|
507
|
-
if (value.transactionPolicy !== RELEASE_TAIL_TRANSACTION_POLICY) {
|
|
508
|
-
fail(`transactionPolicy must be ${RELEASE_TAIL_TRANSACTION_POLICY}`);
|
|
509
|
-
}
|
|
510
|
-
exactFields(value.subject, SUBJECT_FIELDS, "subject");
|
|
511
|
-
const subject = {
|
|
512
|
-
repository: nonEmpty(value.subject.repository, "subject.repository"),
|
|
513
|
-
sourceSha: nonEmpty(
|
|
514
|
-
value.subject.sourceSha,
|
|
515
|
-
"subject.sourceSha",
|
|
516
|
-
).toLowerCase(),
|
|
517
|
-
version: nonEmpty(value.subject.version, "subject.version"),
|
|
518
|
-
tag: nonEmpty(value.subject.tag, "subject.tag"),
|
|
519
|
-
channel: oneOf(
|
|
520
|
-
value.subject.channel,
|
|
521
|
-
["alpha", "release", "stable"],
|
|
522
|
-
"subject.channel",
|
|
523
|
-
),
|
|
524
|
-
};
|
|
525
|
-
if (!SHA.test(subject.sourceSha)) {
|
|
526
|
-
fail("subject.sourceSha must be an exact 40-character Git SHA");
|
|
527
|
-
}
|
|
528
|
-
if (!Array.isArray(value.capabilities) || value.capabilities.length === 0) {
|
|
529
|
-
fail("capabilities must not be empty");
|
|
530
|
-
}
|
|
531
|
-
const capabilities = value.capabilities.map((entry) =>
|
|
532
|
-
normalizeCapability(entry, subject),
|
|
533
|
-
);
|
|
534
|
-
if (
|
|
535
|
-
new Set(capabilities.map((entry) => entry.id)).size !== capabilities.length
|
|
536
|
-
) {
|
|
537
|
-
fail("capability ids must be unique");
|
|
538
|
-
}
|
|
539
|
-
const order = new Map(
|
|
540
|
-
RELEASE_TAIL_CAPABILITY_REGISTRY.map((entry, index) => [entry.id, index]),
|
|
541
|
-
);
|
|
542
|
-
capabilities.sort((left, right) => order.get(left.id) - order.get(right.id));
|
|
543
|
-
const transactionRoots = new Set(
|
|
544
|
-
capabilities.map((entry) => entry.operationIdentity.transactionRoot),
|
|
545
|
-
);
|
|
546
|
-
if (transactionRoots.size !== 1) {
|
|
547
|
-
fail("all operation identities must bind one transaction root");
|
|
548
|
-
}
|
|
549
|
-
return {
|
|
550
|
-
contract: RELEASE_TAIL_DECLARATION_CONTRACT,
|
|
551
|
-
schemaVersion: 1,
|
|
552
|
-
transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
|
|
553
|
-
subject,
|
|
554
|
-
capabilities,
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function createEffect(declaration, capability) {
|
|
559
|
-
const identity = capability.operationIdentity;
|
|
560
|
-
const envelope = {
|
|
561
|
-
schema: RELEASE_TAIL_EFFECT_SCHEMA,
|
|
562
|
-
kind: capability.effect.kind,
|
|
563
|
-
transactionRoot: identity.transactionRoot,
|
|
564
|
-
operationId: releaseTailRoot(identity),
|
|
565
|
-
capabilityId: capability.id,
|
|
566
|
-
executor: capability.executor,
|
|
567
|
-
adapter: capability.adapter,
|
|
568
|
-
subjectRoot: identity.subjectRoot,
|
|
569
|
-
targetRoot: identity.targetRoot,
|
|
570
|
-
attemptKey: identity.attemptKey,
|
|
571
|
-
subject: declaration.subject,
|
|
572
|
-
artifactRoles: capability.artifactRoles,
|
|
573
|
-
destination: capability.destination,
|
|
574
|
-
channelPolicy: capability.channelPolicy,
|
|
575
|
-
activationPolicy: capability.activationPolicy,
|
|
576
|
-
readbackPredicates: capability.readbackPredicates,
|
|
577
|
-
idempotency: capability.idempotency,
|
|
578
|
-
retry: capability.retry,
|
|
579
|
-
evidenceRequirements: capability.evidenceRequirements,
|
|
580
|
-
};
|
|
581
|
-
return { ...envelope, effectRoot: releaseTailRoot(envelope) };
|
|
93
|
+
return invokeV4DomainWasm("release-tail-parse", value);
|
|
582
94
|
}
|
|
583
95
|
|
|
584
96
|
export function compileReleaseTailDeclaration(input) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
97
|
+
return invokeV4DomainWasm(
|
|
98
|
+
"release-tail-compile",
|
|
99
|
+
typeof input === "string" ? JSON.parse(input) : input,
|
|
588
100
|
);
|
|
589
|
-
const plan = {
|
|
590
|
-
schema: "kungfu.buildchain.release-tail.effect-plan/v1",
|
|
591
|
-
transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
|
|
592
|
-
transactionRoot:
|
|
593
|
-
declaration.capabilities[0].operationIdentity.transactionRoot,
|
|
594
|
-
declarationRoot: releaseTailRoot(declaration),
|
|
595
|
-
subject: declaration.subject,
|
|
596
|
-
operationOrder: effects.map((effect) => effect.operationId),
|
|
597
|
-
effects,
|
|
598
|
-
};
|
|
599
|
-
return { ...plan, planRoot: releaseTailRoot(plan) };
|
|
600
101
|
}
|
|
601
102
|
|
|
602
103
|
export function validateReleaseTailEffectPlan(plan) {
|
|
603
|
-
|
|
604
|
-
try {
|
|
605
|
-
exactFields(plan, EFFECT_PLAN_FIELDS, "effect plan");
|
|
606
|
-
rejectExecutableKeys(plan);
|
|
607
|
-
exactFields(plan.subject, SUBJECT_FIELDS, "effect plan subject");
|
|
608
|
-
} catch (error) {
|
|
609
|
-
issues.push(error.message);
|
|
610
|
-
}
|
|
611
|
-
if (plan?.schema !== "kungfu.buildchain.release-tail.effect-plan/v1") {
|
|
612
|
-
issues.push("effect plan schema is invalid");
|
|
613
|
-
}
|
|
614
|
-
if (plan?.transactionPolicy !== RELEASE_TAIL_TRANSACTION_POLICY) {
|
|
615
|
-
issues.push("effect plan transactionPolicy is invalid");
|
|
616
|
-
}
|
|
617
|
-
for (const field of ["transactionRoot", "declarationRoot", "planRoot"]) {
|
|
618
|
-
if (!ROOT.test(plan?.[field] || "")) issues.push(`${field} is invalid`);
|
|
619
|
-
}
|
|
620
|
-
if (!Array.isArray(plan?.effects) || plan.effects.length === 0) {
|
|
621
|
-
issues.push("effects must be a non-empty array");
|
|
622
|
-
} else {
|
|
623
|
-
const operationIds = [];
|
|
624
|
-
for (const effect of plan.effects) {
|
|
625
|
-
try {
|
|
626
|
-
exactFields(effect, EFFECT_FIELDS, `effect ${effect?.capabilityId}`);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
issues.push(error.message);
|
|
629
|
-
}
|
|
630
|
-
const descriptor = CAPABILITY_BY_ID.get(effect?.capabilityId);
|
|
631
|
-
if (!descriptor) {
|
|
632
|
-
issues.push(
|
|
633
|
-
`unsupported effect capability: ${effect?.capabilityId || "<empty>"}`,
|
|
634
|
-
);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if (
|
|
638
|
-
effect.schema !== RELEASE_TAIL_EFFECT_SCHEMA ||
|
|
639
|
-
effect.kind !== descriptor.effectKind ||
|
|
640
|
-
effect.executor !== descriptor.executor ||
|
|
641
|
-
effect.adapter !== descriptor.adapter
|
|
642
|
-
) {
|
|
643
|
-
issues.push(
|
|
644
|
-
`effect ${effect.capabilityId} does not match the capability registry`,
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
if (effect.transactionRoot !== plan.transactionRoot) {
|
|
648
|
-
issues.push(`effect ${effect.capabilityId} transactionRoot mismatch`);
|
|
649
|
-
}
|
|
650
|
-
if (
|
|
651
|
-
releaseTailStableJson(effect.subject) !==
|
|
652
|
-
releaseTailStableJson(plan.subject)
|
|
653
|
-
) {
|
|
654
|
-
issues.push(`effect ${effect.capabilityId} subject mismatch`);
|
|
655
|
-
}
|
|
656
|
-
if (
|
|
657
|
-
!ROOT.test(effect.subjectRoot || "") ||
|
|
658
|
-
!ROOT.test(effect.targetRoot || "")
|
|
659
|
-
) {
|
|
660
|
-
issues.push(
|
|
661
|
-
`effect ${effect.capabilityId} has an invalid subject or target root`,
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
if (
|
|
665
|
-
effect.operationId !==
|
|
666
|
-
releaseTailRoot({
|
|
667
|
-
transactionRoot: effect.transactionRoot,
|
|
668
|
-
capabilityId: effect.capabilityId,
|
|
669
|
-
subjectRoot: effect.subjectRoot,
|
|
670
|
-
targetRoot: effect.targetRoot,
|
|
671
|
-
attemptKey: effect.attemptKey,
|
|
672
|
-
})
|
|
673
|
-
) {
|
|
674
|
-
issues.push(`effect ${effect.capabilityId} operationId mismatch`);
|
|
675
|
-
}
|
|
676
|
-
const effectBody = structuredClone(effect);
|
|
677
|
-
delete effectBody.effectRoot;
|
|
678
|
-
if (effect.effectRoot !== releaseTailRoot(effectBody)) {
|
|
679
|
-
issues.push(`effect ${effect.capabilityId} effectRoot mismatch`);
|
|
680
|
-
}
|
|
681
|
-
operationIds.push(effect.operationId);
|
|
682
|
-
}
|
|
683
|
-
if (new Set(operationIds).size !== operationIds.length) {
|
|
684
|
-
issues.push("effect plan operation ids must be unique");
|
|
685
|
-
}
|
|
686
|
-
if (operationIds.join("\n") !== (plan.operationOrder || []).join("\n")) {
|
|
687
|
-
issues.push("effect plan operationOrder mismatch");
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
const body = structuredClone(plan || {});
|
|
691
|
-
delete body.planRoot;
|
|
692
|
-
if (plan?.planRoot !== releaseTailRoot(body))
|
|
693
|
-
issues.push("planRoot mismatch");
|
|
694
|
-
return { valid: issues.length === 0, issues };
|
|
104
|
+
return invokeV4DomainWasm("release-tail-validate-plan", plan);
|
|
695
105
|
}
|
|
696
106
|
|
|
697
107
|
export function createReleaseTailTransaction(input) {
|
|
698
|
-
|
|
699
|
-
? structuredClone(input)
|
|
700
|
-
: compileReleaseTailDeclaration(input);
|
|
701
|
-
const planValidation = validateReleaseTailEffectPlan(plan);
|
|
702
|
-
if (!planValidation.valid) {
|
|
703
|
-
throw new Error(
|
|
704
|
-
`invalid release-tail effect plan: ${planValidation.issues.join("; ")}`,
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
const transaction = {
|
|
708
|
-
schema: RELEASE_TAIL_TRANSACTION_SCHEMA,
|
|
709
|
-
transactionPolicy: RELEASE_TAIL_TRANSACTION_POLICY,
|
|
710
|
-
transactionRoot: plan.transactionRoot,
|
|
711
|
-
declarationRoot: plan.declarationRoot,
|
|
712
|
-
planRoot: plan.planRoot,
|
|
713
|
-
subject: plan.subject,
|
|
714
|
-
state: "prepared",
|
|
715
|
-
operationOrder: plan.operationOrder,
|
|
716
|
-
operations: plan.effects.map((effect) => ({
|
|
717
|
-
operationId: effect.operationId,
|
|
718
|
-
capabilityId: effect.capabilityId,
|
|
719
|
-
effect,
|
|
720
|
-
status: "pending",
|
|
721
|
-
effectAttempts: 0,
|
|
722
|
-
readbackAttempts: 0,
|
|
723
|
-
observationRoots: [],
|
|
724
|
-
receipt: null,
|
|
725
|
-
})),
|
|
726
|
-
observations: [],
|
|
727
|
-
receipts: [],
|
|
728
|
-
failure: null,
|
|
729
|
-
};
|
|
730
|
-
return refreshTransaction(transaction);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function transactionBody(transaction) {
|
|
734
|
-
const copy = structuredClone(transaction);
|
|
735
|
-
delete copy.stateRoot;
|
|
736
|
-
return copy;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function refreshTransaction(transaction) {
|
|
740
|
-
transaction.stateRoot = releaseTailRoot(transactionBody(transaction));
|
|
741
|
-
return transaction;
|
|
108
|
+
return invokeV4DomainWasm("release-tail-create", input);
|
|
742
109
|
}
|
|
743
110
|
|
|
744
111
|
export function validateReleaseTailTransaction(transaction) {
|
|
745
|
-
|
|
746
|
-
if (transaction?.schema !== RELEASE_TAIL_TRANSACTION_SCHEMA) {
|
|
747
|
-
issues.push(`schema must be ${RELEASE_TAIL_TRANSACTION_SCHEMA}`);
|
|
748
|
-
}
|
|
749
|
-
if (!RELEASE_TAIL_STATES.includes(transaction?.state))
|
|
750
|
-
issues.push("state is invalid");
|
|
751
|
-
if (!ROOT.test(transaction?.transactionRoot || "")) {
|
|
752
|
-
issues.push("transactionRoot is invalid");
|
|
753
|
-
}
|
|
754
|
-
if (!ROOT.test(transaction?.declarationRoot || "")) {
|
|
755
|
-
issues.push("declarationRoot is invalid");
|
|
756
|
-
}
|
|
757
|
-
if (!ROOT.test(transaction?.planRoot || ""))
|
|
758
|
-
issues.push("planRoot is invalid");
|
|
759
|
-
if (!Array.isArray(transaction?.operations)) {
|
|
760
|
-
issues.push("operations must be an array");
|
|
761
|
-
} else {
|
|
762
|
-
const ids = transaction.operations.map((entry) => entry.operationId);
|
|
763
|
-
if (ids.join("\n") !== (transaction.operationOrder || []).join("\n")) {
|
|
764
|
-
issues.push("operations must preserve operationOrder");
|
|
765
|
-
}
|
|
766
|
-
for (const operation of transaction.operations) {
|
|
767
|
-
if (!["pending", "complete", "failed"].includes(operation.status)) {
|
|
768
|
-
issues.push(
|
|
769
|
-
`operation ${operation.operationId || "<unknown>"} status is invalid`,
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
if (
|
|
773
|
-
!Number.isSafeInteger(operation.effectAttempts) ||
|
|
774
|
-
operation.effectAttempts < 0
|
|
775
|
-
) {
|
|
776
|
-
issues.push(
|
|
777
|
-
`operation ${operation.operationId || "<unknown>"} effectAttempts is invalid`,
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
if (
|
|
781
|
-
!Number.isSafeInteger(operation.readbackAttempts) ||
|
|
782
|
-
operation.readbackAttempts < 0
|
|
783
|
-
) {
|
|
784
|
-
issues.push(
|
|
785
|
-
`operation ${operation.operationId || "<unknown>"} readbackAttempts is invalid`,
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
if (!Array.isArray(operation.observationRoots)) {
|
|
789
|
-
issues.push(
|
|
790
|
-
`operation ${operation.operationId || "<unknown>"} observationRoots is invalid`,
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
if (!Array.isArray(transaction?.observations)) {
|
|
796
|
-
issues.push("observations must be an array");
|
|
797
|
-
} else {
|
|
798
|
-
for (const observation of transaction.observations) {
|
|
799
|
-
const body = structuredClone(observation);
|
|
800
|
-
delete body.observationRoot;
|
|
801
|
-
if (observation.observationRoot !== releaseTailRoot(body)) {
|
|
802
|
-
issues.push(
|
|
803
|
-
`observation ${observation.operationId || "<unknown>"} root mismatch`,
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
if (!Array.isArray(transaction?.receipts)) {
|
|
809
|
-
issues.push("receipts must be an array");
|
|
810
|
-
} else {
|
|
811
|
-
for (const receipt of transaction.receipts) {
|
|
812
|
-
const body = structuredClone(receipt);
|
|
813
|
-
delete body.receiptRoot;
|
|
814
|
-
if (receipt.receiptRoot !== releaseTailRoot(body)) {
|
|
815
|
-
issues.push(
|
|
816
|
-
`receipt ${receipt.operationId || "<unknown>"} root mismatch`,
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (
|
|
822
|
-
transaction?.stateRoot !==
|
|
823
|
-
releaseTailRoot(transactionBody(transaction || {}))
|
|
824
|
-
) {
|
|
825
|
-
issues.push("stateRoot mismatch");
|
|
826
|
-
}
|
|
827
|
-
return { valid: issues.length === 0, issues };
|
|
112
|
+
return invokeV4DomainWasm("release-tail-validate-transaction", transaction);
|
|
828
113
|
}
|
|
829
114
|
|
|
830
115
|
export function readReleaseTailTransaction(filePath) {
|
|
@@ -855,353 +140,88 @@ export function writeReleaseTailTransaction(filePath, transaction) {
|
|
|
855
140
|
return resolved;
|
|
856
141
|
}
|
|
857
142
|
|
|
858
|
-
function
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
["observed", "absent", "transient", "conflict"],
|
|
863
|
-
`${effect.capabilityId} adapter observation outcome`,
|
|
864
|
-
);
|
|
865
|
-
const subjectRoot = value.subjectRoot
|
|
866
|
-
? exactRoot(
|
|
867
|
-
value.subjectRoot,
|
|
868
|
-
`${effect.capabilityId} observation subjectRoot`,
|
|
869
|
-
)
|
|
870
|
-
: "";
|
|
871
|
-
const targetRoot = value.targetRoot
|
|
872
|
-
? exactRoot(
|
|
873
|
-
value.targetRoot,
|
|
874
|
-
`${effect.capabilityId} observation targetRoot`,
|
|
875
|
-
)
|
|
876
|
-
: "";
|
|
877
|
-
let status = outcome;
|
|
878
|
-
if (outcome === "observed") {
|
|
879
|
-
status =
|
|
880
|
-
subjectRoot === effect.subjectRoot && targetRoot === effect.targetRoot
|
|
881
|
-
? "matched"
|
|
882
|
-
: "stale";
|
|
143
|
+
async function emitCheckpoints(callback, checkpoints) {
|
|
144
|
+
if (!callback) return;
|
|
145
|
+
for (const checkpoint of checkpoints) {
|
|
146
|
+
await callback(structuredClone(checkpoint));
|
|
883
147
|
}
|
|
884
|
-
const evidenceRoots = Array.isArray(value.evidenceRoots)
|
|
885
|
-
? [
|
|
886
|
-
...new Set(
|
|
887
|
-
value.evidenceRoots.map((root, index) =>
|
|
888
|
-
exactRoot(
|
|
889
|
-
root,
|
|
890
|
-
`${effect.capabilityId} observation evidenceRoots[${index}]`,
|
|
891
|
-
),
|
|
892
|
-
),
|
|
893
|
-
),
|
|
894
|
-
].sort()
|
|
895
|
-
: [];
|
|
896
|
-
const observation = {
|
|
897
|
-
schema: RELEASE_TAIL_OBSERVATION_SCHEMA,
|
|
898
|
-
kind: CAPABILITY_BY_ID.get(effect.capabilityId).observationKind,
|
|
899
|
-
transactionRoot: effect.transactionRoot,
|
|
900
|
-
operationId: effect.operationId,
|
|
901
|
-
capabilityId: effect.capabilityId,
|
|
902
|
-
phase,
|
|
903
|
-
status,
|
|
904
|
-
subjectRoot,
|
|
905
|
-
targetRoot,
|
|
906
|
-
providerCode: /^[a-z0-9][a-z0-9._-]{0,159}$/u.test(
|
|
907
|
-
String(value.providerCode || status),
|
|
908
|
-
)
|
|
909
|
-
? String(value.providerCode || status)
|
|
910
|
-
: "provider-observation",
|
|
911
|
-
evidenceRoots,
|
|
912
|
-
};
|
|
913
|
-
return { ...observation, observationRoot: releaseTailRoot(observation) };
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function createReceipt(operation, action, observation) {
|
|
917
|
-
const effect = operation.effect;
|
|
918
|
-
const body = {
|
|
919
|
-
schema: RELEASE_TAIL_RECEIPT_SCHEMA,
|
|
920
|
-
kind: CAPABILITY_BY_ID.get(effect.capabilityId).receiptKind,
|
|
921
|
-
transactionRoot: effect.transactionRoot,
|
|
922
|
-
operationId: effect.operationId,
|
|
923
|
-
capabilityId: effect.capabilityId,
|
|
924
|
-
subjectRoot: effect.subjectRoot,
|
|
925
|
-
targetRoot: effect.targetRoot,
|
|
926
|
-
effectRoot: effect.effectRoot,
|
|
927
|
-
action,
|
|
928
|
-
effectAttempts: operation.effectAttempts,
|
|
929
|
-
readbackAttempts: operation.readbackAttempts,
|
|
930
|
-
observationRoots: [...operation.observationRoots],
|
|
931
|
-
evidenceRoots: observation.evidenceRoots,
|
|
932
|
-
};
|
|
933
|
-
return { ...body, receiptRoot: releaseTailRoot(body) };
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function capabilityForOperation(operation) {
|
|
937
|
-
return CAPABILITY_BY_ID.get(operation.capabilityId);
|
|
938
148
|
}
|
|
939
149
|
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
transaction.operations.find((entry) => entry.status === "pending") || null
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function adapterFor(adapters, operation) {
|
|
947
|
-
const descriptor = capabilityForOperation(operation);
|
|
948
|
-
const adapter = adapters?.[descriptor.adapter];
|
|
150
|
+
function adapterFor(adapters, effect) {
|
|
151
|
+
const adapter = adapters?.[effect.adapter];
|
|
949
152
|
if (
|
|
950
153
|
!adapter ||
|
|
951
154
|
typeof adapter.readback !== "function" ||
|
|
952
155
|
typeof adapter.apply !== "function"
|
|
953
156
|
) {
|
|
954
157
|
throw new Error(
|
|
955
|
-
`release-tail adapter '${
|
|
158
|
+
`release-tail adapter '${effect.adapter}' must provide readback and apply`,
|
|
956
159
|
);
|
|
957
160
|
}
|
|
958
161
|
return adapter;
|
|
959
162
|
}
|
|
960
163
|
|
|
961
|
-
function
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (!operation)
|
|
967
|
-
throw new Error(`unknown release-tail operation: ${operationId}`);
|
|
968
|
-
operation.observationRoots.push(observation.observationRoot);
|
|
969
|
-
next.observations.push(observation);
|
|
970
|
-
return refreshTransaction(next);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function completeOperation(transaction, operationId, action, observation) {
|
|
974
|
-
const next = structuredClone(transaction);
|
|
975
|
-
const operation = next.operations.find(
|
|
976
|
-
(entry) => entry.operationId === operationId,
|
|
977
|
-
);
|
|
978
|
-
operation.status = "complete";
|
|
979
|
-
operation.receipt = createReceipt(operation, action, observation);
|
|
980
|
-
next.receipts = next.operations
|
|
981
|
-
.filter((entry) => entry.receipt)
|
|
982
|
-
.map((entry) => entry.receipt);
|
|
983
|
-
next.failure = null;
|
|
984
|
-
next.state = nextPendingOperation(next)
|
|
985
|
-
? capabilityForOperation(nextPendingOperation(next)).transactionState
|
|
986
|
-
: "complete";
|
|
987
|
-
return refreshTransaction(next);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
function failOperation(
|
|
991
|
-
transaction,
|
|
992
|
-
operationId,
|
|
993
|
-
state,
|
|
994
|
-
code,
|
|
995
|
-
observation = null,
|
|
996
|
-
) {
|
|
997
|
-
const next = structuredClone(transaction);
|
|
998
|
-
const operation = next.operations.find(
|
|
999
|
-
(entry) => entry.operationId === operationId,
|
|
1000
|
-
);
|
|
1001
|
-
operation.status = "failed";
|
|
1002
|
-
next.state = state;
|
|
1003
|
-
next.failure = {
|
|
1004
|
-
operationId,
|
|
1005
|
-
capabilityId: operation.capabilityId,
|
|
1006
|
-
code,
|
|
1007
|
-
observationRoot: observation?.observationRoot || "",
|
|
164
|
+
function providerFailure(error, fallback) {
|
|
165
|
+
return {
|
|
166
|
+
classification:
|
|
167
|
+
error?.releaseTailClass === "conflict" ? "conflict" : "transient",
|
|
168
|
+
code: error?.releaseTailCode || fallback,
|
|
1008
169
|
};
|
|
1009
|
-
return refreshTransaction(next);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
async function checkpoint(callback, transaction) {
|
|
1013
|
-
if (callback) await callback(structuredClone(transaction));
|
|
1014
170
|
}
|
|
1015
171
|
|
|
1016
172
|
export async function executeReleaseTailTransaction(
|
|
1017
173
|
transaction,
|
|
1018
|
-
{ adapters, checkpoint
|
|
174
|
+
{ adapters, checkpoint } = {},
|
|
1019
175
|
) {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const capability = CAPABILITY_BY_ID.get(pending.capabilityId);
|
|
1032
|
-
const adapter = adapterFor(adapters, pending);
|
|
1033
|
-
current.state = capability.transactionState;
|
|
1034
|
-
current = refreshTransaction(current);
|
|
1035
|
-
await checkpoint(checkpointCallback, current);
|
|
1036
|
-
|
|
1037
|
-
const policy = pending.effect.retry;
|
|
1038
|
-
const maxCycles = 1 + policy.localAttempts;
|
|
1039
|
-
let applied = pending.effectAttempts > 0;
|
|
1040
|
-
let completed = false;
|
|
1041
|
-
|
|
1042
|
-
for (let cycle = 0; cycle < maxCycles; cycle += 1) {
|
|
1043
|
-
current.state = "reading-back";
|
|
1044
|
-
current = refreshTransaction(current);
|
|
1045
|
-
await checkpoint(checkpointCallback, current);
|
|
1046
|
-
let rawReadback;
|
|
176
|
+
let exchange = invokeV4DomainWasm(
|
|
177
|
+
"release-tail-execution-start",
|
|
178
|
+
transaction,
|
|
179
|
+
);
|
|
180
|
+
await emitCheckpoints(checkpoint, exchange.checkpoints);
|
|
181
|
+
while (exchange.instruction.action !== "done") {
|
|
182
|
+
const { instruction } = exchange;
|
|
183
|
+
const adapter = adapterFor(adapters, instruction.effect);
|
|
184
|
+
let signal;
|
|
185
|
+
if (instruction.action === "readback") {
|
|
186
|
+
let raw;
|
|
1047
187
|
try {
|
|
1048
|
-
|
|
188
|
+
raw = await adapter.readback(structuredClone(instruction.effect));
|
|
1049
189
|
} catch (error) {
|
|
1050
|
-
|
|
190
|
+
const failure = providerFailure(error, "readback-error");
|
|
191
|
+
raw = {
|
|
1051
192
|
outcome:
|
|
1052
|
-
|
|
1053
|
-
providerCode:
|
|
193
|
+
failure.classification === "conflict" ? "conflict" : "transient",
|
|
194
|
+
providerCode: failure.code,
|
|
1054
195
|
};
|
|
1055
196
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
pending.effect,
|
|
1063
|
-
rawReadback,
|
|
1064
|
-
applied ? "retry-readback" : "pre-effect-readback",
|
|
1065
|
-
);
|
|
1066
|
-
current = recordObservation(current, pending.operationId, preObservation);
|
|
1067
|
-
await checkpoint(checkpointCallback, current);
|
|
1068
|
-
if (preObservation.status === "matched") {
|
|
1069
|
-
current = completeOperation(
|
|
1070
|
-
current,
|
|
1071
|
-
pending.operationId,
|
|
1072
|
-
applied ? "applied-and-observed" : "observed-existing",
|
|
1073
|
-
preObservation,
|
|
1074
|
-
);
|
|
1075
|
-
await checkpoint(checkpointCallback, current);
|
|
1076
|
-
completed = true;
|
|
1077
|
-
break;
|
|
1078
|
-
}
|
|
1079
|
-
if (preObservation.status === "conflict") {
|
|
1080
|
-
current = failOperation(
|
|
1081
|
-
current,
|
|
1082
|
-
pending.operationId,
|
|
1083
|
-
"terminal-failure",
|
|
1084
|
-
"provider-conflict",
|
|
1085
|
-
preObservation,
|
|
1086
|
-
);
|
|
1087
|
-
await checkpoint(checkpointCallback, current);
|
|
1088
|
-
return current;
|
|
1089
|
-
}
|
|
1090
|
-
if (preObservation.status === "stale") {
|
|
1091
|
-
current = failOperation(
|
|
1092
|
-
current,
|
|
1093
|
-
pending.operationId,
|
|
1094
|
-
"repair-required",
|
|
1095
|
-
"stale-readback",
|
|
1096
|
-
preObservation,
|
|
1097
|
-
);
|
|
1098
|
-
await checkpoint(checkpointCallback, current);
|
|
1099
|
-
return current;
|
|
1100
|
-
}
|
|
1101
|
-
if (preObservation.status === "transient") {
|
|
1102
|
-
if (cycle + 1 < maxCycles) continue;
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
if (applied && policy.class !== "provider-transient") continue;
|
|
1106
|
-
|
|
1107
|
-
operation = current.operations.find(
|
|
1108
|
-
(entry) => entry.operationId === pending.operationId,
|
|
1109
|
-
);
|
|
1110
|
-
operation.effectAttempts += 1;
|
|
1111
|
-
current = refreshTransaction(current);
|
|
1112
|
-
await checkpoint(checkpointCallback, current);
|
|
1113
|
-
try {
|
|
1114
|
-
await adapter.apply(structuredClone(pending.effect));
|
|
1115
|
-
} catch (error) {
|
|
1116
|
-
if (error?.releaseTailClass === "conflict") {
|
|
1117
|
-
current = failOperation(
|
|
1118
|
-
current,
|
|
1119
|
-
pending.operationId,
|
|
1120
|
-
"terminal-failure",
|
|
1121
|
-
error?.releaseTailCode || "provider-conflict",
|
|
1122
|
-
);
|
|
1123
|
-
await checkpoint(checkpointCallback, current);
|
|
1124
|
-
return current;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
applied = true;
|
|
1128
|
-
|
|
1129
|
-
current.state = "reading-back";
|
|
1130
|
-
current = refreshTransaction(current);
|
|
1131
|
-
await checkpoint(checkpointCallback, current);
|
|
1132
|
-
let postRaw;
|
|
197
|
+
signal = {
|
|
198
|
+
kind: "readback",
|
|
199
|
+
phase: instruction.phase,
|
|
200
|
+
raw,
|
|
201
|
+
};
|
|
202
|
+
} else if (instruction.action === "apply") {
|
|
1133
203
|
try {
|
|
1134
|
-
|
|
204
|
+
await adapter.apply(structuredClone(instruction.effect));
|
|
205
|
+
signal = { kind: "apply", classification: "success", code: "" };
|
|
1135
206
|
} catch (error) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
providerCode: error?.releaseTailCode || "post-effect-readback-error",
|
|
207
|
+
signal = {
|
|
208
|
+
kind: "apply",
|
|
209
|
+
...providerFailure(error, "provider-apply-error"),
|
|
1140
210
|
};
|
|
1141
211
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
212
|
+
} else {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`unsupported Rust/WASM release-tail instruction '${instruction.action}'`,
|
|
1144
215
|
);
|
|
1145
|
-
operation.readbackAttempts += 1;
|
|
1146
|
-
current = refreshTransaction(current);
|
|
1147
|
-
const postObservation = normalizeAdapterObservation(
|
|
1148
|
-
pending.effect,
|
|
1149
|
-
postRaw,
|
|
1150
|
-
"post-effect-readback",
|
|
1151
|
-
);
|
|
1152
|
-
current = recordObservation(
|
|
1153
|
-
current,
|
|
1154
|
-
pending.operationId,
|
|
1155
|
-
postObservation,
|
|
1156
|
-
);
|
|
1157
|
-
await checkpoint(checkpointCallback, current);
|
|
1158
|
-
if (postObservation.status === "matched") {
|
|
1159
|
-
current = completeOperation(
|
|
1160
|
-
current,
|
|
1161
|
-
pending.operationId,
|
|
1162
|
-
"applied-and-observed",
|
|
1163
|
-
postObservation,
|
|
1164
|
-
);
|
|
1165
|
-
await checkpoint(checkpointCallback, current);
|
|
1166
|
-
completed = true;
|
|
1167
|
-
break;
|
|
1168
|
-
}
|
|
1169
|
-
if (postObservation.status === "conflict") {
|
|
1170
|
-
current = failOperation(
|
|
1171
|
-
current,
|
|
1172
|
-
pending.operationId,
|
|
1173
|
-
"terminal-failure",
|
|
1174
|
-
"provider-conflict",
|
|
1175
|
-
postObservation,
|
|
1176
|
-
);
|
|
1177
|
-
await checkpoint(checkpointCallback, current);
|
|
1178
|
-
return current;
|
|
1179
|
-
}
|
|
1180
|
-
if (postObservation.status === "stale") {
|
|
1181
|
-
current = failOperation(
|
|
1182
|
-
current,
|
|
1183
|
-
pending.operationId,
|
|
1184
|
-
"repair-required",
|
|
1185
|
-
"stale-readback",
|
|
1186
|
-
postObservation,
|
|
1187
|
-
);
|
|
1188
|
-
await checkpoint(checkpointCallback, current);
|
|
1189
|
-
return current;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
if (!completed) {
|
|
1194
|
-
current = failOperation(
|
|
1195
|
-
current,
|
|
1196
|
-
pending.operationId,
|
|
1197
|
-
policy.exhausted,
|
|
1198
|
-
"local-retry-exhausted",
|
|
1199
|
-
);
|
|
1200
|
-
await checkpoint(checkpointCallback, current);
|
|
1201
|
-
return current;
|
|
1202
216
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
217
|
+
exchange = invokeV4DomainWasm("release-tail-execution-advance", {
|
|
218
|
+
transaction: exchange.transaction,
|
|
219
|
+
cursor: exchange.cursor,
|
|
220
|
+
signal,
|
|
221
|
+
});
|
|
222
|
+
await emitCheckpoints(checkpoint, exchange.checkpoints);
|
|
223
|
+
}
|
|
224
|
+
return exchange.transaction;
|
|
1205
225
|
}
|
|
1206
226
|
|
|
1207
227
|
export function releaseTailRetryPolicyFromDeclaration(input) {
|