@kungfu-tech/buildchain 4.0.1-alpha.2 → 4.0.1-alpha.21
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/actions/release-tail/README.md +8 -0
- package/architecture/ci-lane-change-budget.json +187 -0
- package/architecture/decisions/0002-next-development-transition.md +101 -0
- package/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
- package/architecture/internal-capabilities.json +211 -8
- package/architecture/maintainability-policy.json +113 -28
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v3-core-mechanism-inventory.json +91 -0
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-capability-state-machine-manifest.json +73 -0
- package/architecture/v4-compatibility-facts-parity.json +77 -0
- package/architecture/v4-delivery-authority-parity.json +250 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-next-development-parity.json +192 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/architecture/v4-release-train-parity.json +1 -0
- package/architecture/v4-tail-reseal-parity.json +150 -0
- package/bin/buildchain.mjs +32 -3
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +3 -0
- package/bin/internal/compatibility-facts-cli.mjs +101 -0
- package/contracts/dev-delivery-authority-v2.schema.json +662 -0
- package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
- package/contracts/next-development-request-v1.schema.json +66 -0
- package/contracts/next-development-transition-v1.schema.json +292 -0
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/contracts/v4-tail-reseal-v1.schema.json +276 -0
- package/dist/site/agent-index.json +5 -0
- package/dist/site/artifact-schemas.json +10 -0
- package/dist/site/buildchain-contract.json +1475 -27
- package/dist/site/buildchain-site.json +627 -31
- package/dist/site/capability-registry.json +17 -14
- package/dist/site/cli-registry.json +205 -3
- package/dist/site/kfd-claims.json +490 -16
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +52 -6
- package/dist/site/node-api-registry.json +14736 -8851
- package/dist/site/page-registry.json +593 -21
- package/dist/site/public-surface-audit.json +837 -48
- package/dist/site/publication-authority-registry.json +44 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +24 -0
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +34 -10
- package/dist/site/workflow-registry.json +160 -10
- package/docs/MAP.md +8 -2
- package/docs/cli-reference.md +404 -13
- package/docs/cli.md +8 -0
- package/docs/dev-alpha-candidate-patrol.md +8 -0
- package/docs/dev-delivery-qualification-landing-adr.md +251 -0
- package/docs/dev-delivery-warrant.md +351 -38
- package/docs/next-development-transition.md +118 -0
- package/docs/node-api-reference.md +646 -293
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-compatibility-facts.md +91 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/docs/v4-tail-reseal.md +73 -0
- package/docs/versioning.md +4 -3
- package/package.json +34 -6
- package/packages/core/adopter-delivery-passport.js +259 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-compatibility-authority.js +336 -0
- package/packages/core/buildchain-compatibility-fact.js +394 -0
- package/packages/core/buildchain-compatibility-facts.json +217 -0
- package/packages/core/buildchain-compatibility-proof.js +566 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-contract.js +96 -143
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +55 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-alpha-candidate-selection.js +10 -2
- package/packages/core/dev-delivery-authority-candidate.js +270 -0
- package/packages/core/dev-delivery-authority-evidence.js +146 -0
- package/packages/core/dev-delivery-authority-landing.js +461 -0
- package/packages/core/dev-delivery-authority-observation.js +48 -0
- package/packages/core/dev-delivery-authority-qualification.js +591 -0
- package/packages/core/dev-delivery-authority-settlement.js +213 -0
- package/packages/core/dev-delivery-authority-state.js +583 -0
- package/packages/core/dev-delivery-candidate-identity.js +13 -0
- package/packages/core/dev-delivery-contract-surface.js +76 -0
- package/packages/core/dev-delivery-execution-failure.js +133 -0
- package/packages/core/dev-delivery-execution-transfer.js +592 -0
- package/packages/core/dev-delivery-landing-admission-core.js +119 -0
- package/packages/core/dev-delivery-landing-readback.js +598 -0
- package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
- package/packages/core/dev-delivery-landing-testing-port.js +6 -0
- package/packages/core/dev-delivery-native-execution.js +110 -0
- package/packages/core/dev-delivery-native-proof.js +562 -0
- package/packages/core/dev-delivery-process-boundary.js +563 -0
- package/packages/core/dev-delivery-provider-attempt.js +127 -0
- package/packages/core/dev-delivery-provider-heartbeat.js +382 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-qualification.js +145 -0
- package/packages/core/dev-delivery-warrant-settlement.js +237 -36
- package/packages/core/dev-delivery-warrant-state.js +587 -0
- package/packages/core/dev-delivery-warrant.js +328 -370
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- package/packages/core/kungfu-temporal-fact.js +557 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/next-development-candidate-reservation.js +186 -0
- package/packages/core/next-development-controller.js +728 -0
- package/packages/core/next-development-projection.js +287 -0
- package/packages/core/next-development-transition.js +738 -0
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper.js +16 -5
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +70 -15
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +36 -9
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-canonical-contracts.js +5 -0
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +143 -20
- package/packages/core/v4-floating-consumer-release-passport.js +32 -5
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/v4-tail-reseal-contract.js +26 -0
- package/packages/core/v4-tail-reseal-github.js +178 -0
- package/packages/core/v4-tail-reseal-receipt.js +225 -0
- package/packages/core/v4-tail-reseal.js +594 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/buildchain-cli-help.mjs +41 -2
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +14 -3
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
- package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
- package/scripts/dev-delivery-authority-provider.mjs +28 -0
- package/scripts/dev-delivery-authority.mjs +490 -0
- package/scripts/dev-delivery-native-run.mjs +177 -0
- package/scripts/dev-delivery-process-boundary.mjs +260 -0
- package/scripts/dev-delivery-proof.mjs +67 -2
- package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
- package/scripts/dev-delivery-two-phase.mjs +573 -0
- package/scripts/dev-delivery-warrant-options.mjs +266 -0
- package/scripts/dev-delivery-warrant-store.mjs +231 -0
- package/scripts/dev-delivery-warrant.mjs +232 -194
- package/scripts/dev-pr-auto-merge.mjs +1 -1
- package/scripts/dev-pr-delivery-warrant.mjs +11 -0
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-next-development-guidance.mjs +49 -0
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +69 -4
- package/scripts/init-repo.mjs +22 -4
- package/scripts/next-development-self-dogfood-harness.mjs +460 -0
- package/scripts/next-development-self-dogfood.mjs +597 -0
- package/scripts/next-development-transition.mjs +47 -0
- package/scripts/npm-publish-transaction.mjs +2 -2
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/release-train-self-dogfood.mjs +94 -92
- package/scripts/resume-from-candidate-run.mjs +557 -30
- package/scripts/site-capability-metadata.mjs +41 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v4-consumer-policy.mjs +5 -5
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
- package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
- package/scripts/v4-tail-reseal.mjs +416 -0
- package/templates/native-dev-delivery.yml +143 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
discoverConfiguredDerivedVersionMaterial,
|
|
9
|
+
discoverConfiguredVersionStateFiles,
|
|
10
|
+
getVersionStrategy,
|
|
11
|
+
loadBuildchainConfig,
|
|
12
|
+
loadConfiguredAnchorManifest,
|
|
13
|
+
updateConfiguredVersionStateContents,
|
|
14
|
+
} from "./buildchain-config.js";
|
|
15
|
+
|
|
16
|
+
export const NEXT_DEVELOPMENT_TRANSITION_CONTRACT =
|
|
17
|
+
"kungfu-buildchain-next-development-transition/v1";
|
|
18
|
+
export const NEXT_DEVELOPMENT_REQUEST_CONTRACT =
|
|
19
|
+
"kungfu-buildchain-next-development-request/v1";
|
|
20
|
+
export const NEXT_DEVELOPMENT_ADR =
|
|
21
|
+
"architecture/decisions/0002-next-development-transition.md";
|
|
22
|
+
export const NEXT_DEVELOPMENT_INVARIANT =
|
|
23
|
+
"A completed Alpha remains successful and its refs remain immutable while the next-development transition is incomplete.";
|
|
24
|
+
export const NEXT_DEVELOPMENT_STATES = Object.freeze([
|
|
25
|
+
"planned",
|
|
26
|
+
"waiting-anchor",
|
|
27
|
+
"materialized",
|
|
28
|
+
"pr-pending",
|
|
29
|
+
"merged",
|
|
30
|
+
"verified",
|
|
31
|
+
]);
|
|
32
|
+
export const NEXT_DEVELOPMENT_VERSION_MODELS = Object.freeze([
|
|
33
|
+
Object.freeze({ strategy: "semver", next: "auto" }),
|
|
34
|
+
Object.freeze({ strategy: "anchored", next: "manual" }),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
38
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
39
|
+
const SEMVER =
|
|
40
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
41
|
+
const ALPHA = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-alpha\.(0|[1-9]\d*)$/u;
|
|
42
|
+
const TRANSITIONS = new Map([
|
|
43
|
+
["planned", new Set(["waiting-anchor", "materialized"])],
|
|
44
|
+
["waiting-anchor", new Set(["materialized"])],
|
|
45
|
+
["materialized", new Set(["pr-pending", "merged", "verified"])],
|
|
46
|
+
["pr-pending", new Set(["merged"])],
|
|
47
|
+
["merged", new Set(["verified"])],
|
|
48
|
+
["verified", new Set()],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function canonical(value) {
|
|
52
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
53
|
+
if (value && typeof value === "object") {
|
|
54
|
+
return Object.fromEntries(
|
|
55
|
+
Object.entries(value)
|
|
56
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
57
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function nextDevelopmentRoot(value) {
|
|
64
|
+
return `sha256:${crypto
|
|
65
|
+
.createHash("sha256")
|
|
66
|
+
.update(JSON.stringify(canonical(value)))
|
|
67
|
+
.digest("hex")}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function contentRoot(value, label) {
|
|
71
|
+
const normalized = String(value || "")
|
|
72
|
+
.trim()
|
|
73
|
+
.toLowerCase();
|
|
74
|
+
if (!ROOT.test(normalized))
|
|
75
|
+
throw new Error(`${label} must be a sha256 content root`);
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function exactSha(value, label) {
|
|
80
|
+
const normalized = String(value || "")
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase();
|
|
83
|
+
if (!SHA.test(normalized))
|
|
84
|
+
throw new Error(`${label} must be an exact 40-character Git SHA`);
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function timestamp(value, label) {
|
|
89
|
+
const normalized = String(value || "").trim();
|
|
90
|
+
const milliseconds = Date.parse(normalized);
|
|
91
|
+
if (
|
|
92
|
+
!Number.isFinite(milliseconds) ||
|
|
93
|
+
new Date(milliseconds).toISOString() !== normalized
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(`${label} must be a canonical ISO-8601 timestamp`);
|
|
96
|
+
}
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function repository(value) {
|
|
101
|
+
const normalized = String(value || "").trim();
|
|
102
|
+
if (!/^[^/\s]+\/[^/\s]+$/u.test(normalized))
|
|
103
|
+
throw new Error("repository must be owner/repo");
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function relativePath(value, label) {
|
|
108
|
+
const normalized = String(value || "")
|
|
109
|
+
.trim()
|
|
110
|
+
.replaceAll("\\", "/");
|
|
111
|
+
if (
|
|
112
|
+
!normalized ||
|
|
113
|
+
path.posix.isAbsolute(normalized) ||
|
|
114
|
+
normalized === "." ||
|
|
115
|
+
normalized.split("/").some((part) => !part || part === "." || part === "..")
|
|
116
|
+
) {
|
|
117
|
+
throw new Error(`${label} must be an unambiguous repository-relative path`);
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sortedPaths(values, label) {
|
|
123
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
124
|
+
throw new Error(`${label} must be a non-empty array`);
|
|
125
|
+
const normalized = values.map((value, index) =>
|
|
126
|
+
relativePath(value, `${label}[${index}]`),
|
|
127
|
+
);
|
|
128
|
+
const expected = [...new Set(normalized)].sort();
|
|
129
|
+
if (
|
|
130
|
+
expected.length !== normalized.length ||
|
|
131
|
+
expected.some((value, index) => value !== normalized[index])
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(`${label} must be sorted and duplicate-free`);
|
|
134
|
+
}
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function optionalSortedPaths(values, label) {
|
|
139
|
+
if (values === undefined) return [];
|
|
140
|
+
if (!Array.isArray(values)) throw new Error(`${label} must be an array`);
|
|
141
|
+
if (values.length === 0) return [];
|
|
142
|
+
return sortedPaths(values, label);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeModel(value = {}) {
|
|
146
|
+
const model = {
|
|
147
|
+
strategy: String(value.strategy || "").trim(),
|
|
148
|
+
next: String(value.next || "").trim(),
|
|
149
|
+
};
|
|
150
|
+
const legal = NEXT_DEVELOPMENT_VERSION_MODELS.some(
|
|
151
|
+
(candidate) =>
|
|
152
|
+
candidate.strategy === model.strategy && candidate.next === model.next,
|
|
153
|
+
);
|
|
154
|
+
if (!legal) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"next-development version model must be semver/auto or anchored/manual",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return model;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeCompletedAlpha(value = {}) {
|
|
163
|
+
const version = String(value.version || "").trim();
|
|
164
|
+
if (!ALPHA.test(version))
|
|
165
|
+
throw new Error("completedAlpha.version must be an alpha semantic version");
|
|
166
|
+
const exactTag = String(value.exactTag || "").trim();
|
|
167
|
+
if (exactTag !== `v${version}`)
|
|
168
|
+
throw new Error(
|
|
169
|
+
"completedAlpha.exactTag must match completedAlpha.version",
|
|
170
|
+
);
|
|
171
|
+
const normalized = {
|
|
172
|
+
outcome: String(value.outcome || "").trim(),
|
|
173
|
+
version,
|
|
174
|
+
exactTag,
|
|
175
|
+
releaseSha: exactSha(value.releaseSha, "completedAlpha.releaseSha"),
|
|
176
|
+
treeSha: exactSha(value.treeSha, "completedAlpha.treeSha"),
|
|
177
|
+
publicationRoot: contentRoot(
|
|
178
|
+
value.publicationRoot,
|
|
179
|
+
"completedAlpha.publicationRoot",
|
|
180
|
+
),
|
|
181
|
+
completedAt: timestamp(value.completedAt, "completedAlpha.completedAt"),
|
|
182
|
+
};
|
|
183
|
+
if (normalized.outcome !== "succeeded") {
|
|
184
|
+
throw new Error('completedAlpha.outcome must be "succeeded"');
|
|
185
|
+
}
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function deriveAutoVersion(alphaVersion) {
|
|
190
|
+
const match = alphaVersion.match(ALPHA);
|
|
191
|
+
return `${match[1]}.${match[2]}.${match[3]}-alpha.${Number(match[4]) + 1}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeAdapter(input = {}) {
|
|
195
|
+
const sourcePaths = sortedPaths(
|
|
196
|
+
input.sourcePaths || input.declaredPaths,
|
|
197
|
+
"sourcePaths",
|
|
198
|
+
);
|
|
199
|
+
const derivedPaths = optionalSortedPaths(input.derivedPaths, "derivedPaths");
|
|
200
|
+
const readOnlyPaths = optionalSortedPaths(
|
|
201
|
+
input.readOnlyPaths,
|
|
202
|
+
"readOnlyPaths",
|
|
203
|
+
);
|
|
204
|
+
const roles = [...sourcePaths, ...derivedPaths, ...readOnlyPaths];
|
|
205
|
+
if (new Set(roles).size !== roles.length) {
|
|
206
|
+
throw new Error("adapter path roles must be disjoint");
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
environmentVariable: "BUILDCHAIN_VERSION",
|
|
210
|
+
sourcePaths,
|
|
211
|
+
derivedPaths,
|
|
212
|
+
allowedChangePaths: [...sourcePaths, ...derivedPaths].sort(),
|
|
213
|
+
readOnlyPaths,
|
|
214
|
+
derivationStage: derivedPaths.length > 0 ? "lifecycle.version-state" : null,
|
|
215
|
+
verificationStage: "lifecycle.verify",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeTarget({ model, completedAlpha, targetVersion, anchor }) {
|
|
220
|
+
const requestedVersion = String(targetVersion || "").trim();
|
|
221
|
+
if (model.strategy === "semver") {
|
|
222
|
+
const derived = deriveAutoVersion(completedAlpha.version);
|
|
223
|
+
if (requestedVersion && requestedVersion !== derived) {
|
|
224
|
+
throw new Error(`semver/auto targetVersion must be ${derived}`);
|
|
225
|
+
}
|
|
226
|
+
if (anchor !== undefined && anchor !== null) {
|
|
227
|
+
throw new Error("semver/auto must not declare an anchor");
|
|
228
|
+
}
|
|
229
|
+
return { version: derived, anchor: null };
|
|
230
|
+
}
|
|
231
|
+
if (!requestedVersion && (anchor === undefined || anchor === null)) {
|
|
232
|
+
return { version: null, anchor: null };
|
|
233
|
+
}
|
|
234
|
+
if (!SEMVER.test(requestedVersion)) {
|
|
235
|
+
throw new Error("anchored/manual targetVersion must be a semantic version");
|
|
236
|
+
}
|
|
237
|
+
if (!anchor || typeof anchor !== "object" || Array.isArray(anchor)) {
|
|
238
|
+
throw new Error("anchored/manual targetVersion requires an anchor");
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
version: requestedVersion,
|
|
242
|
+
anchor: {
|
|
243
|
+
manifestPath: relativePath(anchor.manifestPath, "anchor.manifestPath"),
|
|
244
|
+
manifestRoot: contentRoot(anchor.manifestRoot, "anchor.manifestRoot"),
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function initialStateRoot(status, idempotencyKey) {
|
|
250
|
+
return nextDevelopmentRoot({ status, generation: 1, idempotencyKey });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function createNextDevelopmentTransition(input = {}) {
|
|
254
|
+
const completedAlpha = normalizeCompletedAlpha(input.completedAlpha);
|
|
255
|
+
const model = normalizeModel(input.model);
|
|
256
|
+
const adapter = normalizeAdapter(input);
|
|
257
|
+
const declaredPaths = [
|
|
258
|
+
...adapter.sourcePaths,
|
|
259
|
+
...adapter.derivedPaths,
|
|
260
|
+
...adapter.readOnlyPaths,
|
|
261
|
+
].sort();
|
|
262
|
+
const target = normalizeTarget({
|
|
263
|
+
model,
|
|
264
|
+
completedAlpha,
|
|
265
|
+
targetVersion: input.targetVersion,
|
|
266
|
+
anchor: input.anchor,
|
|
267
|
+
});
|
|
268
|
+
const completedAlphaRoot = nextDevelopmentRoot(completedAlpha);
|
|
269
|
+
const identity = {
|
|
270
|
+
contract: NEXT_DEVELOPMENT_TRANSITION_CONTRACT,
|
|
271
|
+
repository: repository(input.repository),
|
|
272
|
+
completedAlphaRoot,
|
|
273
|
+
model,
|
|
274
|
+
adapter,
|
|
275
|
+
};
|
|
276
|
+
const idempotencyKey = nextDevelopmentRoot(identity);
|
|
277
|
+
const status = model.strategy === "anchored" ? "waiting-anchor" : "planned";
|
|
278
|
+
return {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
contract: NEXT_DEVELOPMENT_TRANSITION_CONTRACT,
|
|
281
|
+
idempotencyKey,
|
|
282
|
+
repository: identity.repository,
|
|
283
|
+
completedAlpha,
|
|
284
|
+
completedAlphaRoot,
|
|
285
|
+
alphaOutcome: "preserved-success",
|
|
286
|
+
model,
|
|
287
|
+
target,
|
|
288
|
+
declaredPaths,
|
|
289
|
+
adapter,
|
|
290
|
+
effectBounds: {
|
|
291
|
+
allowedPaths: adapter.allowedChangePaths,
|
|
292
|
+
refUpdates: [],
|
|
293
|
+
forbiddenRefNamespaces: ["refs/heads/alpha/", "refs/tags/"],
|
|
294
|
+
},
|
|
295
|
+
state: {
|
|
296
|
+
status,
|
|
297
|
+
generation: 1,
|
|
298
|
+
stateRoot: initialStateRoot(status, idempotencyKey),
|
|
299
|
+
},
|
|
300
|
+
materialization: null,
|
|
301
|
+
transitions: [],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function bindNextDevelopmentAnchor(record, input = {}) {
|
|
306
|
+
const current = validateNextDevelopmentTransition(record);
|
|
307
|
+
if (
|
|
308
|
+
current.model.strategy !== "anchored" ||
|
|
309
|
+
current.state.status !== "waiting-anchor"
|
|
310
|
+
) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
"next-development anchor input requires anchored/manual waiting-anchor state",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const target = normalizeTarget({
|
|
316
|
+
model: current.model,
|
|
317
|
+
completedAlpha: current.completedAlpha,
|
|
318
|
+
targetVersion: input.targetVersion,
|
|
319
|
+
anchor: input.anchor,
|
|
320
|
+
});
|
|
321
|
+
if (!target.version || !target.anchor) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
"next-development reviewed anchor input requires targetVersion and anchor",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const next = structuredClone(current);
|
|
327
|
+
next.target = target;
|
|
328
|
+
return validateNextDevelopmentTransition(next);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function transitionRequestRoot(record, request) {
|
|
332
|
+
return nextDevelopmentRoot({
|
|
333
|
+
contract: NEXT_DEVELOPMENT_TRANSITION_CONTRACT,
|
|
334
|
+
idempotencyKey: record.idempotencyKey,
|
|
335
|
+
expectedStateRoot: request.expectedStateRoot,
|
|
336
|
+
from: request.from || record.state.status,
|
|
337
|
+
to: request.to,
|
|
338
|
+
event: request.event,
|
|
339
|
+
evidenceRoot: request.evidenceRoot,
|
|
340
|
+
recordedAt: request.recordedAt,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function advanceNextDevelopmentTransition(record, input = {}) {
|
|
345
|
+
const current = validateNextDevelopmentTransition(record);
|
|
346
|
+
const request = {
|
|
347
|
+
to: String(input.to || "").trim(),
|
|
348
|
+
event: String(input.event || "").trim(),
|
|
349
|
+
expectedStateRoot: contentRoot(
|
|
350
|
+
input.expectedStateRoot,
|
|
351
|
+
"expectedStateRoot",
|
|
352
|
+
),
|
|
353
|
+
evidenceRoot: contentRoot(input.evidenceRoot, "evidenceRoot"),
|
|
354
|
+
recordedAt: timestamp(input.recordedAt, "recordedAt"),
|
|
355
|
+
};
|
|
356
|
+
if (!request.event) throw new Error("event is required");
|
|
357
|
+
const replay = current.transitions.find(
|
|
358
|
+
(entry) =>
|
|
359
|
+
entry.expectedStateRoot === request.expectedStateRoot &&
|
|
360
|
+
entry.to === request.to &&
|
|
361
|
+
entry.event === request.event &&
|
|
362
|
+
entry.evidenceRoot === request.evidenceRoot &&
|
|
363
|
+
entry.recordedAt === request.recordedAt,
|
|
364
|
+
);
|
|
365
|
+
if (replay) return structuredClone(current);
|
|
366
|
+
if (request.expectedStateRoot !== current.state.stateRoot) {
|
|
367
|
+
throw new Error("next-development transition compare-and-swap failed");
|
|
368
|
+
}
|
|
369
|
+
if (!TRANSITIONS.get(current.state.status)?.has(request.to)) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`invalid next-development transition: ${current.state.status} -> ${request.to}`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (
|
|
375
|
+
request.to === "waiting-anchor" &&
|
|
376
|
+
current.model.strategy !== "anchored"
|
|
377
|
+
) {
|
|
378
|
+
throw new Error("only anchored/manual may enter waiting-anchor");
|
|
379
|
+
}
|
|
380
|
+
if (
|
|
381
|
+
["materialized", "pr-pending", "merged", "verified"].includes(request.to) &&
|
|
382
|
+
!current.materialization
|
|
383
|
+
) {
|
|
384
|
+
throw new Error(`${request.to} requires materialized declared paths`);
|
|
385
|
+
}
|
|
386
|
+
const requestRoot = transitionRequestRoot(current, request);
|
|
387
|
+
const transitionBody = {
|
|
388
|
+
expectedStateRoot: request.expectedStateRoot,
|
|
389
|
+
from: current.state.status,
|
|
390
|
+
to: request.to,
|
|
391
|
+
event: request.event,
|
|
392
|
+
evidenceRoot: request.evidenceRoot,
|
|
393
|
+
recordedAt: request.recordedAt,
|
|
394
|
+
requestRoot,
|
|
395
|
+
};
|
|
396
|
+
const transition = {
|
|
397
|
+
...transitionBody,
|
|
398
|
+
transitionRoot: nextDevelopmentRoot(transitionBody),
|
|
399
|
+
};
|
|
400
|
+
const next = structuredClone(current);
|
|
401
|
+
next.transitions.push(transition);
|
|
402
|
+
next.state = {
|
|
403
|
+
status: request.to,
|
|
404
|
+
generation: current.state.generation + 1,
|
|
405
|
+
stateRoot: nextDevelopmentRoot({
|
|
406
|
+
status: request.to,
|
|
407
|
+
generation: current.state.generation + 1,
|
|
408
|
+
priorStateRoot: current.state.stateRoot,
|
|
409
|
+
transitionRoot: transition.transitionRoot,
|
|
410
|
+
idempotencyKey: current.idempotencyKey,
|
|
411
|
+
}),
|
|
412
|
+
};
|
|
413
|
+
return next;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function recordNextDevelopmentMaterialization(
|
|
417
|
+
record,
|
|
418
|
+
{ materialization, evidenceRoot, recordedAt } = {},
|
|
419
|
+
) {
|
|
420
|
+
const current = validateNextDevelopmentTransition(record);
|
|
421
|
+
if (!current.target.version) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
"next-development materialization requires an authorized target version",
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (!materialization || typeof materialization !== "object") {
|
|
427
|
+
throw new Error("next-development materialization is required");
|
|
428
|
+
}
|
|
429
|
+
const next = structuredClone(current);
|
|
430
|
+
next.materialization = structuredClone(materialization);
|
|
431
|
+
assertMaterialization(next);
|
|
432
|
+
return advanceNextDevelopmentTransition(next, {
|
|
433
|
+
to: "materialized",
|
|
434
|
+
event: "declared-version-state-materialized",
|
|
435
|
+
expectedStateRoot: current.state.stateRoot,
|
|
436
|
+
evidenceRoot: evidenceRoot || next.materialization.materializationRoot,
|
|
437
|
+
recordedAt,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function assertLocalDeclaredFile(cwd, relative) {
|
|
442
|
+
const root = fs.realpathSync(cwd);
|
|
443
|
+
const target = path.resolve(root, relative);
|
|
444
|
+
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
|
445
|
+
throw new Error(`declared path escapes checkout: ${relative}`);
|
|
446
|
+
}
|
|
447
|
+
const stat = fs.lstatSync(target);
|
|
448
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`declared path must be a regular non-symlink file: ${relative}`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return target;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function fileRoot(content) {
|
|
457
|
+
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function getDotted(value, dottedKey) {
|
|
461
|
+
return String(dottedKey)
|
|
462
|
+
.split(".")
|
|
463
|
+
.reduce((current, key) => current?.[key], value);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function observedVersion(file) {
|
|
467
|
+
if (file.type === "json" || file.type === "toml")
|
|
468
|
+
return getDotted(file.content, file.key);
|
|
469
|
+
return file.source.match(file.pattern)?.groups?.version;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function materializeNextDevelopmentTransition({
|
|
473
|
+
cwd = process.cwd(),
|
|
474
|
+
request,
|
|
475
|
+
write = false,
|
|
476
|
+
} = {}) {
|
|
477
|
+
if (!request || request.contract !== NEXT_DEVELOPMENT_REQUEST_CONTRACT) {
|
|
478
|
+
throw new Error(`request must use ${NEXT_DEVELOPMENT_REQUEST_CONTRACT}`);
|
|
479
|
+
}
|
|
480
|
+
const recordedAt = timestamp(request.recordedAt, "recordedAt");
|
|
481
|
+
const resolvedCwd = fs.realpathSync(path.resolve(cwd));
|
|
482
|
+
const loadedConfig = loadBuildchainConfig(resolvedCwd);
|
|
483
|
+
if (!loadedConfig)
|
|
484
|
+
throw new Error(
|
|
485
|
+
"next-development materialization requires buildchain.toml",
|
|
486
|
+
);
|
|
487
|
+
const model = getVersionStrategy(loadedConfig);
|
|
488
|
+
const versionFiles = discoverConfiguredVersionStateFiles(
|
|
489
|
+
resolvedCwd,
|
|
490
|
+
loadedConfig,
|
|
491
|
+
);
|
|
492
|
+
if (versionFiles.length === 0)
|
|
493
|
+
throw new Error(
|
|
494
|
+
"next-development materialization requires declared version.files",
|
|
495
|
+
);
|
|
496
|
+
const derivedFiles = discoverConfiguredDerivedVersionMaterial(
|
|
497
|
+
resolvedCwd,
|
|
498
|
+
loadedConfig,
|
|
499
|
+
);
|
|
500
|
+
const manifest = loadConfiguredAnchorManifest(resolvedCwd, loadedConfig);
|
|
501
|
+
const sourcePaths = versionFiles.map((file) => file.path).sort();
|
|
502
|
+
const derivedPaths = derivedFiles.map((file) => file.path).sort();
|
|
503
|
+
const readOnlyPaths = manifest ? [manifest.path] : [];
|
|
504
|
+
const plan = createNextDevelopmentTransition({
|
|
505
|
+
repository: request.repository,
|
|
506
|
+
completedAlpha: request.completedAlpha,
|
|
507
|
+
model,
|
|
508
|
+
sourcePaths,
|
|
509
|
+
derivedPaths,
|
|
510
|
+
readOnlyPaths,
|
|
511
|
+
targetVersion: request.targetVersion,
|
|
512
|
+
anchor: request.anchor,
|
|
513
|
+
});
|
|
514
|
+
if (plan.state.status === "waiting-anchor" && !plan.target.version) {
|
|
515
|
+
return plan;
|
|
516
|
+
}
|
|
517
|
+
if (write && derivedPaths.length > 0) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
"reference adapter cannot write derived files; the transaction adapter must run lifecycle.version-state and lifecycle.verify with BUILDCHAIN_VERSION",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (model.strategy === "anchored") {
|
|
523
|
+
if (!manifest || plan.target.anchor.manifestPath !== manifest.path) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
"anchored/manual anchor must name the configured version.manifest",
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const manifestPath = assertLocalDeclaredFile(resolvedCwd, manifest.path);
|
|
529
|
+
const observedRoot = fileRoot(fs.readFileSync(manifestPath));
|
|
530
|
+
if (observedRoot !== plan.target.anchor.manifestRoot) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"anchored/manual manifest root does not match the local checkout",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const file of versionFiles) {
|
|
537
|
+
assertLocalDeclaredFile(resolvedCwd, file.path);
|
|
538
|
+
const current = observedVersion(file);
|
|
539
|
+
if (![plan.completedAlpha.version, plan.target.version].includes(current)) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`${file.path} is neither the completed Alpha nor the planned next version`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const file of derivedFiles) {
|
|
546
|
+
assertLocalDeclaredFile(resolvedCwd, file.path);
|
|
547
|
+
}
|
|
548
|
+
const updates = updateConfiguredVersionStateContents(
|
|
549
|
+
versionFiles,
|
|
550
|
+
plan.target.version,
|
|
551
|
+
);
|
|
552
|
+
const updateByPath = new Map(updates.map((entry) => [entry.path, entry]));
|
|
553
|
+
const paths = versionFiles.map((file) => {
|
|
554
|
+
const update = updateByPath.get(file.path);
|
|
555
|
+
return {
|
|
556
|
+
path: file.path,
|
|
557
|
+
beforeRoot: fileRoot(file.source),
|
|
558
|
+
afterRoot: fileRoot(update ? update.content : file.source),
|
|
559
|
+
changed: Boolean(update),
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
const materializationBody = {
|
|
563
|
+
targetVersion: plan.target.version,
|
|
564
|
+
anchorRoot: plan.target.anchor?.manifestRoot || null,
|
|
565
|
+
paths,
|
|
566
|
+
};
|
|
567
|
+
const next = structuredClone(plan);
|
|
568
|
+
next.materialization = {
|
|
569
|
+
...materializationBody,
|
|
570
|
+
materializationRoot: nextDevelopmentRoot(materializationBody),
|
|
571
|
+
};
|
|
572
|
+
if (!write) return next;
|
|
573
|
+
for (const update of updates) {
|
|
574
|
+
const target = assertLocalDeclaredFile(resolvedCwd, update.path);
|
|
575
|
+
fs.writeFileSync(target, update.content);
|
|
576
|
+
}
|
|
577
|
+
const evidenceRoot = next.materialization.materializationRoot;
|
|
578
|
+
return advanceNextDevelopmentTransition(next, {
|
|
579
|
+
to: "materialized",
|
|
580
|
+
event: "declared-version-state-written",
|
|
581
|
+
expectedStateRoot: next.state.stateRoot,
|
|
582
|
+
evidenceRoot,
|
|
583
|
+
recordedAt,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function recreateTransition(record) {
|
|
588
|
+
return createNextDevelopmentTransition({
|
|
589
|
+
repository: record.repository,
|
|
590
|
+
completedAlpha: record.completedAlpha,
|
|
591
|
+
model: record.model,
|
|
592
|
+
sourcePaths: record.adapter?.sourcePaths,
|
|
593
|
+
derivedPaths: record.adapter?.derivedPaths,
|
|
594
|
+
readOnlyPaths: record.adapter?.readOnlyPaths,
|
|
595
|
+
targetVersion: record.target?.version || undefined,
|
|
596
|
+
anchor: record.target?.anchor || undefined,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function assertStaticTransition(record, recreated) {
|
|
601
|
+
for (const field of [
|
|
602
|
+
"idempotencyKey",
|
|
603
|
+
"completedAlphaRoot",
|
|
604
|
+
"alphaOutcome",
|
|
605
|
+
]) {
|
|
606
|
+
if (record[field] !== recreated[field])
|
|
607
|
+
throw new Error(`next-development ${field} drifted`);
|
|
608
|
+
}
|
|
609
|
+
for (const field of ["adapter", "declaredPaths", "effectBounds", "target"]) {
|
|
610
|
+
if (JSON.stringify(record[field]) !== JSON.stringify(recreated[field])) {
|
|
611
|
+
const label = field === "effectBounds" ? "effect bounds" : field;
|
|
612
|
+
throw new Error(`next-development ${label} drifted`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (!NEXT_DEVELOPMENT_STATES.includes(record.state?.status)) {
|
|
616
|
+
throw new Error("next-development state is invalid");
|
|
617
|
+
}
|
|
618
|
+
if (
|
|
619
|
+
record.alphaOutcome !== "preserved-success" ||
|
|
620
|
+
record.effectBounds.refUpdates.length !== 0
|
|
621
|
+
) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
"next-development must preserve completed Alpha success and refs",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (!Array.isArray(record.transitions)) {
|
|
627
|
+
throw new Error("next-development transitions must be an array");
|
|
628
|
+
}
|
|
629
|
+
if (
|
|
630
|
+
["materialized", "pr-pending", "merged", "verified"].includes(
|
|
631
|
+
record.state.status,
|
|
632
|
+
) &&
|
|
633
|
+
record.materialization === null
|
|
634
|
+
) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`next-development ${record.state.status} requires materialization`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function assertMaterialization(record) {
|
|
642
|
+
if (record.materialization !== null) {
|
|
643
|
+
const { materializationRoot, ...body } = record.materialization;
|
|
644
|
+
if (materializationRoot !== nextDevelopmentRoot(body)) {
|
|
645
|
+
throw new Error("next-development materialization root drifted");
|
|
646
|
+
}
|
|
647
|
+
const materializedPaths = (record.materialization.paths || []).map(
|
|
648
|
+
(entry) => entry.path,
|
|
649
|
+
);
|
|
650
|
+
if (
|
|
651
|
+
JSON.stringify(materializedPaths) !==
|
|
652
|
+
JSON.stringify(record.adapter.sourcePaths)
|
|
653
|
+
) {
|
|
654
|
+
throw new Error("next-development materialization escaped source paths");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function replayTransitionChain(record, recreated) {
|
|
660
|
+
let status = recreated.state.status;
|
|
661
|
+
let generation = 1;
|
|
662
|
+
let stateRoot = recreated.state.stateRoot;
|
|
663
|
+
for (const [index, transition] of record.transitions.entries()) {
|
|
664
|
+
if (
|
|
665
|
+
transition.expectedStateRoot !== stateRoot ||
|
|
666
|
+
transition.from !== status
|
|
667
|
+
) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`next-development transition ${index} compare-and-swap chain drifted`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
if (!TRANSITIONS.get(status)?.has(transition.to)) {
|
|
673
|
+
throw new Error(
|
|
674
|
+
`next-development transition ${index} has an invalid state edge`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const normalized = {
|
|
678
|
+
to: String(transition.to || "").trim(),
|
|
679
|
+
event: String(transition.event || "").trim(),
|
|
680
|
+
expectedStateRoot: contentRoot(
|
|
681
|
+
transition.expectedStateRoot,
|
|
682
|
+
`transitions[${index}].expectedStateRoot`,
|
|
683
|
+
),
|
|
684
|
+
evidenceRoot: contentRoot(
|
|
685
|
+
transition.evidenceRoot,
|
|
686
|
+
`transitions[${index}].evidenceRoot`,
|
|
687
|
+
),
|
|
688
|
+
recordedAt: timestamp(
|
|
689
|
+
transition.recordedAt,
|
|
690
|
+
`transitions[${index}].recordedAt`,
|
|
691
|
+
),
|
|
692
|
+
from: status,
|
|
693
|
+
};
|
|
694
|
+
if (!normalized.event) {
|
|
695
|
+
throw new Error(`next-development transition ${index} event is required`);
|
|
696
|
+
}
|
|
697
|
+
const expectedRequestRoot = transitionRequestRoot(recreated, normalized);
|
|
698
|
+
if (transition.requestRoot !== expectedRequestRoot) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
`next-development transition ${index} request root drifted`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
const { transitionRoot, ...transitionBody } = transition;
|
|
704
|
+
if (transitionRoot !== nextDevelopmentRoot(transitionBody)) {
|
|
705
|
+
throw new Error(`next-development transition ${index} root drifted`);
|
|
706
|
+
}
|
|
707
|
+
generation += 1;
|
|
708
|
+
const priorStateRoot = stateRoot;
|
|
709
|
+
status = transition.to;
|
|
710
|
+
stateRoot = nextDevelopmentRoot({
|
|
711
|
+
status,
|
|
712
|
+
generation,
|
|
713
|
+
priorStateRoot,
|
|
714
|
+
transitionRoot,
|
|
715
|
+
idempotencyKey: record.idempotencyKey,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
record.state.status !== status ||
|
|
720
|
+
record.state.generation !== generation ||
|
|
721
|
+
record.state.stateRoot !== stateRoot
|
|
722
|
+
) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
"next-development state does not match its transition chain",
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function validateNextDevelopmentTransition(record) {
|
|
730
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
731
|
+
throw new Error("next-development transition must be an object");
|
|
732
|
+
}
|
|
733
|
+
const recreated = recreateTransition(record);
|
|
734
|
+
assertStaticTransition(record, recreated);
|
|
735
|
+
assertMaterialization(record);
|
|
736
|
+
replayTransitionChain(record, recreated);
|
|
737
|
+
return structuredClone(record);
|
|
738
|
+
}
|