@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.8
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/internal-capabilities.json +65 -5
- package/architecture/maintainability-policy.json +27 -21
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -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-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 +1 -1
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -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/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +14 -14
- package/dist/site/buildchain-site.json +231 -24
- package/dist/site/capability-registry.json +11 -10
- package/dist/site/cli-registry.json +53 -1
- package/dist/site/kfd-claims.json +279 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -5
- package/dist/site/node-api-registry.json +11569 -9299
- package/dist/site/page-registry.json +214 -15
- package/dist/site/public-surface-audit.json +206 -11
- package/dist/site/publication-authority-registry.json +39 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +15 -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-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +17 -9
- package/dist/site/workflow-registry.json +110 -8
- package/docs/MAP.md +3 -1
- package/docs/cli-reference.md +68 -0
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +22 -1
- package/docs/node-api-reference.md +332 -172
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +18 -3
- 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 +1 -0
- package/packages/core/buildchain-config.js +84 -0
- 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/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-delivery-native-proof.js +43 -27
- package/packages/core/dev-delivery-warrant-state.js +31 -8
- package/packages/core/dev-delivery-warrant.js +8 -3
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +66 -11
- 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-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 +138 -35
- package/packages/core/v4-floating-consumer-release-passport.js +30 -3
- 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/workflow-yaml-contract.js +24 -2
- package/scripts/buildchain-cli-help.mjs +10 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +3 -3
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +5 -1
- package/scripts/dev-pr-delivery-warrant.mjs +1 -0
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +30 -0
- 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/resume-from-candidate-run.mjs +556 -29
- package/scripts/site-capability-metadata.mjs +17 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { parseWorkflowDocument } from "./workflow-yaml-contract.js";
|
|
3
|
+
|
|
4
|
+
export const CI_LANE_CHANGE_BUDGET_CONTRACT =
|
|
5
|
+
"buildchain.ci-lane-change-budget/v1";
|
|
6
|
+
|
|
7
|
+
const AUTHORITY_CLASSES = new Set([
|
|
8
|
+
"merge-critical-required",
|
|
9
|
+
"merge-critical-admission",
|
|
10
|
+
"post-merge-advisory",
|
|
11
|
+
"diagnostic",
|
|
12
|
+
"evidence",
|
|
13
|
+
"release",
|
|
14
|
+
]);
|
|
15
|
+
const TRIGGER_CLASSES = new Set([
|
|
16
|
+
"merge-group",
|
|
17
|
+
"pull-request",
|
|
18
|
+
"push",
|
|
19
|
+
"scheduled",
|
|
20
|
+
"manual",
|
|
21
|
+
"reusable",
|
|
22
|
+
"release",
|
|
23
|
+
"mixed",
|
|
24
|
+
]);
|
|
25
|
+
const CONCURRENCY_MODES = new Set([
|
|
26
|
+
"none",
|
|
27
|
+
"ref-scoped",
|
|
28
|
+
"workflow-scoped",
|
|
29
|
+
"serialized",
|
|
30
|
+
]);
|
|
31
|
+
const CANCELLATION_BEHAVIORS = new Set([
|
|
32
|
+
"cancel-stale",
|
|
33
|
+
"finish-started",
|
|
34
|
+
"provider-default",
|
|
35
|
+
"not-applicable",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function canonical(value) {
|
|
39
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
Object.keys(value)
|
|
43
|
+
.sort()
|
|
44
|
+
.map((key) => [key, canonical(value[key])]),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function root(value) {
|
|
51
|
+
return `sha256:${crypto
|
|
52
|
+
.createHash("sha256")
|
|
53
|
+
.update(JSON.stringify(canonical(value)))
|
|
54
|
+
.digest("hex")}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function triggerClass(triggers) {
|
|
58
|
+
const classes = new Set();
|
|
59
|
+
for (const trigger of triggers) {
|
|
60
|
+
if (trigger === "merge_group") classes.add("merge-group");
|
|
61
|
+
else if (trigger.startsWith("pull_request")) classes.add("pull-request");
|
|
62
|
+
else if (trigger === "push") classes.add("push");
|
|
63
|
+
else if (trigger === "schedule") classes.add("scheduled");
|
|
64
|
+
else if (trigger === "workflow_dispatch") classes.add("manual");
|
|
65
|
+
else if (trigger === "workflow_call") classes.add("reusable");
|
|
66
|
+
else if (trigger === "release") classes.add("release");
|
|
67
|
+
else classes.add("mixed");
|
|
68
|
+
}
|
|
69
|
+
return classes.size === 1 ? [...classes][0] : "mixed";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function diagnostic(code, laneId, message) {
|
|
73
|
+
return { code, laneId, message };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateDeclaration(declaration, observed) {
|
|
77
|
+
const laneId = String(declaration?.laneId || "");
|
|
78
|
+
const diagnostics = [];
|
|
79
|
+
if (!AUTHORITY_CLASSES.has(declaration?.authorityClass)) {
|
|
80
|
+
diagnostics.push(
|
|
81
|
+
diagnostic(
|
|
82
|
+
"invalid-authority-class",
|
|
83
|
+
laneId,
|
|
84
|
+
"authorityClass must identify the lane's merge or non-merge authority",
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!TRIGGER_CLASSES.has(declaration?.triggerClass)) {
|
|
89
|
+
diagnostics.push(
|
|
90
|
+
diagnostic(
|
|
91
|
+
"invalid-trigger-class",
|
|
92
|
+
laneId,
|
|
93
|
+
"triggerClass must identify the workflow trigger class",
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
} else if (observed && declaration.triggerClass !== observed.triggerClass) {
|
|
97
|
+
diagnostics.push(
|
|
98
|
+
diagnostic(
|
|
99
|
+
"trigger-class-mismatch",
|
|
100
|
+
laneId,
|
|
101
|
+
`declared ${declaration.triggerClass}, observed ${observed.triggerClass}`,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const concurrency = declaration?.concurrencyPolicy;
|
|
106
|
+
if (
|
|
107
|
+
!concurrency ||
|
|
108
|
+
!CONCURRENCY_MODES.has(concurrency.mode) ||
|
|
109
|
+
typeof concurrency.cancelInProgress !== "boolean"
|
|
110
|
+
) {
|
|
111
|
+
diagnostics.push(
|
|
112
|
+
diagnostic(
|
|
113
|
+
"invalid-concurrency-policy",
|
|
114
|
+
laneId,
|
|
115
|
+
"concurrencyPolicy requires a known mode and boolean cancelInProgress",
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
!Number.isFinite(declaration?.expectedRunnerMinutes) ||
|
|
121
|
+
declaration.expectedRunnerMinutes <= 0
|
|
122
|
+
) {
|
|
123
|
+
diagnostics.push(
|
|
124
|
+
diagnostic(
|
|
125
|
+
"invalid-runner-budget",
|
|
126
|
+
laneId,
|
|
127
|
+
"expectedRunnerMinutes must be greater than zero",
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!CANCELLATION_BEHAVIORS.has(declaration?.cancellationBehavior)) {
|
|
132
|
+
diagnostics.push(
|
|
133
|
+
diagnostic(
|
|
134
|
+
"invalid-cancellation-behavior",
|
|
135
|
+
laneId,
|
|
136
|
+
"cancellationBehavior must describe how stale or started work settles",
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const impact = declaration?.sloImpact;
|
|
141
|
+
if (
|
|
142
|
+
!impact ||
|
|
143
|
+
typeof impact.mergeCritical !== "boolean" ||
|
|
144
|
+
!String(impact.metric || "").trim() ||
|
|
145
|
+
!Number.isFinite(impact.expectedContributionSeconds) ||
|
|
146
|
+
impact.expectedContributionSeconds < 0 ||
|
|
147
|
+
!String(impact.rationale || "").trim()
|
|
148
|
+
) {
|
|
149
|
+
diagnostics.push(
|
|
150
|
+
diagnostic(
|
|
151
|
+
"invalid-slo-impact",
|
|
152
|
+
laneId,
|
|
153
|
+
"sloImpact requires mergeCritical, metric, expectedContributionSeconds, and rationale",
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return diagnostics;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function collectLanes(workflows) {
|
|
161
|
+
const lanes = new Map();
|
|
162
|
+
for (const workflow of workflows) {
|
|
163
|
+
const workflowPath = String(workflow?.path || "").trim();
|
|
164
|
+
if (!workflowPath) throw new Error("workflow path is required");
|
|
165
|
+
const document = parseWorkflowDocument(workflow.text);
|
|
166
|
+
for (const job of document.jobs) {
|
|
167
|
+
const laneId = `${workflowPath}#${job.id}`;
|
|
168
|
+
if (lanes.has(laneId)) throw new Error(`duplicate CI lane: ${laneId}`);
|
|
169
|
+
lanes.set(laneId, {
|
|
170
|
+
laneId,
|
|
171
|
+
workflowPath,
|
|
172
|
+
jobId: job.id,
|
|
173
|
+
triggerClass: triggerClass(document.triggers),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return lanes;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function evaluateCiLaneChangeBudget({
|
|
181
|
+
policy,
|
|
182
|
+
workflows,
|
|
183
|
+
baselineWorkflows = null,
|
|
184
|
+
} = {}) {
|
|
185
|
+
if (policy?.schema !== CI_LANE_CHANGE_BUDGET_CONTRACT) {
|
|
186
|
+
throw new Error(`expected ${CI_LANE_CHANGE_BUDGET_CONTRACT}`);
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(workflows)) throw new Error("workflows must be an array");
|
|
189
|
+
const observed = collectLanes(workflows);
|
|
190
|
+
const baselineLaneIds = baselineWorkflows
|
|
191
|
+
? [...collectLanes(baselineWorkflows).keys()]
|
|
192
|
+
: policy.baseline?.lanes || [];
|
|
193
|
+
const baseline = new Set(baselineLaneIds);
|
|
194
|
+
if (baseline.size !== baselineLaneIds.length) {
|
|
195
|
+
throw new Error("baseline CI lanes must be unique");
|
|
196
|
+
}
|
|
197
|
+
const declarations = new Map();
|
|
198
|
+
const diagnostics = [];
|
|
199
|
+
for (const declaration of policy.declarations || []) {
|
|
200
|
+
const laneId = String(declaration?.laneId || "").trim();
|
|
201
|
+
if (!laneId) throw new Error("CI lane declaration laneId is required");
|
|
202
|
+
if (declarations.has(laneId))
|
|
203
|
+
throw new Error(`duplicate CI lane declaration: ${laneId}`);
|
|
204
|
+
declarations.set(laneId, declaration);
|
|
205
|
+
if (!observed.has(laneId)) {
|
|
206
|
+
diagnostics.push(
|
|
207
|
+
diagnostic(
|
|
208
|
+
"stale-declaration",
|
|
209
|
+
laneId,
|
|
210
|
+
"declared CI lane is absent from the current workflow cut",
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
diagnostics.push(
|
|
215
|
+
...validateDeclaration(declaration, observed.get(laneId) || null),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const newLanes = [...observed.keys()]
|
|
219
|
+
.filter((laneId) => !baseline.has(laneId))
|
|
220
|
+
.sort();
|
|
221
|
+
for (const laneId of newLanes) {
|
|
222
|
+
if (!declarations.has(laneId)) {
|
|
223
|
+
diagnostics.push(
|
|
224
|
+
diagnostic(
|
|
225
|
+
"undeclared-new-lane",
|
|
226
|
+
laneId,
|
|
227
|
+
"new CI lane must declare authority, trigger, concurrency, runner, cancellation, and SLO budgets",
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const result = {
|
|
233
|
+
schema: "buildchain.ci-lane-change-budget.evaluation/v1",
|
|
234
|
+
ok: diagnostics.length === 0,
|
|
235
|
+
observedLaneCount: observed.size,
|
|
236
|
+
baselineLaneCount: baseline.size,
|
|
237
|
+
newLaneCount: newLanes.length,
|
|
238
|
+
declaredLaneCount: declarations.size,
|
|
239
|
+
newLanes,
|
|
240
|
+
diagnostics: diagnostics.sort((left, right) =>
|
|
241
|
+
`${left.laneId}:${left.code}`.localeCompare(
|
|
242
|
+
`${right.laneId}:${right.code}`,
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
};
|
|
246
|
+
return { ...result, evaluationRoot: root(result) };
|
|
247
|
+
}
|
|
@@ -43,7 +43,39 @@ export const DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA =
|
|
|
43
43
|
export const NATIVE_PROOF_BASE_DELTA_SCHEMA =
|
|
44
44
|
"kungfu.buildchain.native-proof-base-delta/v1";
|
|
45
45
|
const NATIVE_QUALIFICATION_ROOT_SEMANTICS = "semantic-native-identity-v1";
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
function normalizeQualifiedWarrant(warrant, candidate, allowLegacyV3Readback) {
|
|
48
|
+
const qualifiedV3Readback =
|
|
49
|
+
allowLegacyV3Readback &&
|
|
50
|
+
!warrant.nativeCommandContract &&
|
|
51
|
+
!candidate.nativeCommandContract &&
|
|
52
|
+
!warrant.nativeExecutionReceiptRoot &&
|
|
53
|
+
!warrant.qualificationReceiptRoot;
|
|
54
|
+
warrant.nativeProofRoot = exactRoot(
|
|
55
|
+
warrant.nativeProofRoot,
|
|
56
|
+
"Warrant nativeProofRoot",
|
|
57
|
+
);
|
|
58
|
+
warrant.nativeProofReuseRoot = exactRoot(
|
|
59
|
+
warrant.nativeProofReuseRoot,
|
|
60
|
+
"Warrant nativeProofReuseRoot",
|
|
61
|
+
);
|
|
62
|
+
if (!qualifiedV3Readback) {
|
|
63
|
+
warrant.nativeExecutionReceiptRoot = exactRoot(
|
|
64
|
+
warrant.nativeExecutionReceiptRoot,
|
|
65
|
+
"Warrant nativeExecutionReceiptRoot",
|
|
66
|
+
);
|
|
67
|
+
warrant.qualificationReceiptRoot = exactRoot(
|
|
68
|
+
warrant.qualificationReceiptRoot,
|
|
69
|
+
"Warrant qualificationReceiptRoot",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
warrant.qualifiedAt = timestamp(warrant.qualifiedAt, "Warrant qualifiedAt");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function validateActiveDevDeliveryWarrant(
|
|
76
|
+
queue,
|
|
77
|
+
{ allowLegacyV3Readback = false } = {},
|
|
78
|
+
) {
|
|
47
79
|
const warrant = queue.activeWarrant;
|
|
48
80
|
if (warrant.schema !== "kungfu.buildchain.dev-delivery-warrant/v1")
|
|
49
81
|
throw new Error("active Warrant schema is unsupported");
|
|
@@ -68,24 +100,17 @@ export function validateActiveDevDeliveryWarrant(queue) {
|
|
|
68
100
|
const phase = historicalPhaseLess ? "legacy-active" : warrant.phase;
|
|
69
101
|
if (!["legacy-active", "provisional", "qualified"].includes(phase))
|
|
70
102
|
throw new Error("active Warrant phase is unsupported");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"Warrant
|
|
79
|
-
);
|
|
80
|
-
warrant.nativeExecutionReceiptRoot = exactRoot(
|
|
81
|
-
warrant.nativeExecutionReceiptRoot,
|
|
82
|
-
"Warrant nativeExecutionReceiptRoot",
|
|
83
|
-
);
|
|
84
|
-
warrant.qualificationReceiptRoot = exactRoot(
|
|
85
|
-
warrant.qualificationReceiptRoot,
|
|
86
|
-
"Warrant qualificationReceiptRoot",
|
|
103
|
+
const active = queue.candidates.filter((candidate) =>
|
|
104
|
+
["selected", "proving", "waiting", "blocked", "qualified"].includes(
|
|
105
|
+
candidate.status,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
if (active.length !== 1 || active[0].candidateId !== warrant.candidateId)
|
|
109
|
+
throw new Error(
|
|
110
|
+
"exactly one active candidate must match the active Warrant",
|
|
87
111
|
);
|
|
88
|
-
|
|
112
|
+
if (warrant.phase === "qualified") {
|
|
113
|
+
normalizeQualifiedWarrant(warrant, active[0], allowLegacyV3Readback);
|
|
89
114
|
} else if (
|
|
90
115
|
warrant.phase === "provisional" &&
|
|
91
116
|
(warrant.nativeProofRoot ||
|
|
@@ -98,15 +123,6 @@ export function validateActiveDevDeliveryWarrant(queue) {
|
|
|
98
123
|
"provisional Warrant cannot carry qualified native evidence",
|
|
99
124
|
);
|
|
100
125
|
}
|
|
101
|
-
const active = queue.candidates.filter((candidate) =>
|
|
102
|
-
["selected", "proving", "waiting", "blocked", "qualified"].includes(
|
|
103
|
-
candidate.status,
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
if (active.length !== 1 || active[0].candidateId !== warrant.candidateId)
|
|
107
|
-
throw new Error(
|
|
108
|
-
"exactly one active candidate must match the active Warrant",
|
|
109
|
-
);
|
|
110
126
|
if (
|
|
111
127
|
historicalPhaseLess &&
|
|
112
128
|
(active[0].environmentRoot ||
|
|
@@ -51,8 +51,8 @@ export const TERMINAL_STATES = new Set([
|
|
|
51
51
|
"terminal-failure",
|
|
52
52
|
"dequeued",
|
|
53
53
|
"cancelled",
|
|
54
|
+
"superseded",
|
|
54
55
|
]);
|
|
55
|
-
|
|
56
56
|
export function issueDevDeliveryWarrant(
|
|
57
57
|
queue,
|
|
58
58
|
before,
|
|
@@ -158,6 +158,21 @@ function normalizePolicy(policy = {}) {
|
|
|
158
158
|
emergencyPolicy: text(policy.emergencyPolicy || "reviewed-explicit-only"),
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
function lacksLiveNativeProof(candidate, status, allowLegacyV3Readback) {
|
|
163
|
+
const { environmentRoot, nativeCommandContract } = candidate;
|
|
164
|
+
const native =
|
|
165
|
+
candidate.deliveryClass !== "non-native-fast" ||
|
|
166
|
+
environmentRoot ||
|
|
167
|
+
nativeCommandContract;
|
|
168
|
+
return (
|
|
169
|
+
native &&
|
|
170
|
+
(!environmentRoot ||
|
|
171
|
+
(!nativeCommandContract && allowLegacyV3Readback !== true)) &&
|
|
172
|
+
!TERMINAL_STATES.has(status)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
161
176
|
function normalizeCandidate(input, expected) {
|
|
162
177
|
const identity = createDevDeliveryCandidateIdentity(
|
|
163
178
|
input,
|
|
@@ -246,9 +261,7 @@ function normalizeCandidate(input, expected) {
|
|
|
246
261
|
"candidate shardEvidenceRoots",
|
|
247
262
|
);
|
|
248
263
|
}
|
|
249
|
-
|
|
250
|
-
const native = candidate.deliveryClass !== "non-native-fast" || env || cmd;
|
|
251
|
-
if (native && (!env || !cmd) && !TERMINAL_STATES.has(status))
|
|
264
|
+
if (lacksLiveNativeProof(candidate, status, expected.allowLegacyV3Readback))
|
|
252
265
|
throw new Error("live native candidate requires exact native proof");
|
|
253
266
|
if (Object.hasOwn(input, "releaseBlockerPriority"))
|
|
254
267
|
candidate.releaseBlockerPriority = normalizeReleaseBlockerPriorityClaim(
|
|
@@ -318,13 +331,21 @@ export function normalizeDevDeliveryQueue(input, expected = {}) {
|
|
|
318
331
|
"queue fencingCounter",
|
|
319
332
|
);
|
|
320
333
|
queue.policy = normalizePolicy(queue.policy);
|
|
334
|
+
const allowLegacyV3Readback =
|
|
335
|
+
expected.allowLegacyV3Readback === true &&
|
|
336
|
+
!(queue.candidates || []).some(
|
|
337
|
+
(candidate) => candidate?.nativeCommandContract,
|
|
338
|
+
) &&
|
|
339
|
+
!queue.activeWarrant?.nativeCommandContract &&
|
|
340
|
+
!queue.activeWarrant?.nativeExecutionReceiptRoot &&
|
|
341
|
+
!queue.activeWarrant?.qualificationReceiptRoot;
|
|
321
342
|
queue.candidates = (queue.candidates || []).map((candidate) =>
|
|
322
|
-
normalizeCandidate(candidate, queue),
|
|
343
|
+
normalizeCandidate(candidate, { ...queue, allowLegacyV3Readback }),
|
|
323
344
|
);
|
|
324
345
|
validateDevDeliveryCandidateChain(queue.candidates, TERMINAL_STATES);
|
|
325
346
|
queue.updatedAt = timestamp(queue.updatedAt, "queue updatedAt");
|
|
326
347
|
if (queue.activeWarrant) {
|
|
327
|
-
validateActiveDevDeliveryWarrant(queue);
|
|
348
|
+
validateActiveDevDeliveryWarrant(queue, { allowLegacyV3Readback });
|
|
328
349
|
} else if (
|
|
329
350
|
queue.candidates.some((candidate) =>
|
|
330
351
|
["selected", "proving", "waiting", "blocked", "qualified"].includes(
|
|
@@ -544,9 +565,11 @@ function effectivePriority(candidate, policy, now) {
|
|
|
544
565
|
}
|
|
545
566
|
export function rankDevDeliveryCandidates(
|
|
546
567
|
queueInput,
|
|
547
|
-
{ now = new Date().toISOString() } = {},
|
|
568
|
+
{ now = new Date().toISOString(), allowLegacyV3Readback = false } = {},
|
|
548
569
|
) {
|
|
549
|
-
const queue = normalizeDevDeliveryQueue(queueInput
|
|
570
|
+
const queue = normalizeDevDeliveryQueue(queueInput, {
|
|
571
|
+
allowLegacyV3Readback,
|
|
572
|
+
});
|
|
550
573
|
const currentTime = timestamp(now, "now");
|
|
551
574
|
return queue.candidates
|
|
552
575
|
.filter((candidate) => candidate.status === "queued")
|
|
@@ -516,14 +516,19 @@ export const settleDevDeliveryTerminalEvent = createDevDeliveryTerminalSettler({
|
|
|
516
516
|
});
|
|
517
517
|
export function observeDevDeliveryQueue(
|
|
518
518
|
queueInput,
|
|
519
|
-
{ now = new Date().toISOString() } = {},
|
|
519
|
+
{ now = new Date().toISOString(), allowLegacyV3Readback = false } = {},
|
|
520
520
|
) {
|
|
521
|
-
const queue = normalizeDevDeliveryQueue(queueInput
|
|
521
|
+
const queue = normalizeDevDeliveryQueue(queueInput, {
|
|
522
|
+
allowLegacyV3Readback,
|
|
523
|
+
});
|
|
522
524
|
const currentTime = timestamp(now, "now");
|
|
523
525
|
const states = {};
|
|
524
526
|
for (const candidate of queue.candidates)
|
|
525
527
|
states[candidate.status] = (states[candidate.status] || 0) + 1;
|
|
526
|
-
const queued = rankDevDeliveryCandidates(queue, {
|
|
528
|
+
const queued = rankDevDeliveryCandidates(queue, {
|
|
529
|
+
now: currentTime,
|
|
530
|
+
allowLegacyV3Readback,
|
|
531
|
+
});
|
|
527
532
|
return {
|
|
528
533
|
schema: "kungfu.buildchain.dev-delivery-queue-observation/v1",
|
|
529
534
|
repository: queue.repository,
|
|
@@ -116,6 +116,7 @@ const PROTECTED_AUTHORITY_PATHS = Object.freeze([
|
|
|
116
116
|
".github/CODEOWNERS",
|
|
117
117
|
".github/workflows/.publication-authority.yml",
|
|
118
118
|
".github/workflows/.release-candidate-promote.yml",
|
|
119
|
+
".github/workflows/buildchain-ref-promotion-recovery.yml",
|
|
119
120
|
".github/workflows/buildchain-ref-promotion.yml",
|
|
120
121
|
".github/workflows/github-governance-audit.yml",
|
|
121
122
|
".github/workflows/paper-release-sealed.yml",
|
package/packages/core/index.js
CHANGED
|
@@ -139,6 +139,11 @@ export {
|
|
|
139
139
|
normalizeCandidateTimelineEvent,
|
|
140
140
|
} from "./candidate-timeline.js";
|
|
141
141
|
|
|
142
|
+
export {
|
|
143
|
+
CI_LANE_CHANGE_BUDGET_CONTRACT,
|
|
144
|
+
evaluateCiLaneChangeBudget,
|
|
145
|
+
} from "./ci-lane-change-budget.js";
|
|
146
|
+
|
|
142
147
|
export {
|
|
143
148
|
CHANNEL_CANDIDATE_DECISION_SCHEMA,
|
|
144
149
|
channelCandidateSourceLockRef,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ADOPTER_PROTOCOL_DRIVER_INTERFACE,
|
|
3
|
+
adopterDeliveryGateDigest,
|
|
4
|
+
defineAdopterProtocolDriver,
|
|
5
|
+
} from "./adopter-delivery-gate.js";
|
|
6
|
+
import {
|
|
7
|
+
KFD_LEGACY_SUPPORT_MATRIX_CONTRACT,
|
|
8
|
+
validateKfdAdopterManifestGate,
|
|
9
|
+
validateKfdLegacySupportMatrixProjection,
|
|
10
|
+
} from "./kfd-adopter-manifest.js";
|
|
11
|
+
|
|
12
|
+
export const LEGACY_KFD_ADOPTER_PROTOCOL_ID =
|
|
13
|
+
"buildchain.kfd-adopter/legacy-support-matrix";
|
|
14
|
+
export const LEGACY_KFD_ADOPTER_PROTOCOL_VERSION = "1.0.0";
|
|
15
|
+
export const LEGACY_KFD_ADOPTER_DRIVER_REPORT_CONTRACT =
|
|
16
|
+
"kungfu-buildchain-legacy-kfd-adopter-driver-report";
|
|
17
|
+
|
|
18
|
+
function issue(code, path, message) {
|
|
19
|
+
return { code, path, message };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizedIssues(entries) {
|
|
23
|
+
return (entries ?? []).map(({ code, path, message }) => ({
|
|
24
|
+
code,
|
|
25
|
+
path: path ? `/${path.replaceAll(".", "/")}` : "/declaration",
|
|
26
|
+
message,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sameJson(left, right) {
|
|
31
|
+
try {
|
|
32
|
+
return adopterDeliveryGateDigest(left) === adopterDeliveryGateDigest(right);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function bindingIssues(request, manifest) {
|
|
39
|
+
const issues = [];
|
|
40
|
+
if (
|
|
41
|
+
request.project.instanceId !== manifest?.manifestId ||
|
|
42
|
+
request.project.adopterId !== manifest?.adopter?.id
|
|
43
|
+
) {
|
|
44
|
+
issues.push(
|
|
45
|
+
issue(
|
|
46
|
+
"delivery-legacy-project-binding-mismatch",
|
|
47
|
+
"/project",
|
|
48
|
+
"Delivery project identity must match the standard adopter manifest.",
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (!sameJson(request.artifact, manifest?.adopter?.artifact)) {
|
|
53
|
+
issues.push(
|
|
54
|
+
issue(
|
|
55
|
+
"delivery-legacy-artifact-binding-mismatch",
|
|
56
|
+
"/artifact",
|
|
57
|
+
"Delivery artifact must match the standard adopter manifest artifact.",
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return issues;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function invalidContextResult(issues) {
|
|
65
|
+
const report = {
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
contract: LEGACY_KFD_ADOPTER_DRIVER_REPORT_CONTRACT,
|
|
68
|
+
valid: false,
|
|
69
|
+
qualifying: false,
|
|
70
|
+
selfCertified: false,
|
|
71
|
+
independentlyCertified: false,
|
|
72
|
+
manifestRoot: null,
|
|
73
|
+
manifestGateRoot: null,
|
|
74
|
+
legacyProjectionRoot: null,
|
|
75
|
+
legacyProjection: null,
|
|
76
|
+
adapterIssues: issues,
|
|
77
|
+
nonClaims: [
|
|
78
|
+
"The legacy support matrix is a one-way projection, not declaration authority.",
|
|
79
|
+
"Buildchain carries the existing standard manifest decision and does not certify KFD adoption.",
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
report,
|
|
85
|
+
reportRoot: adopterDeliveryGateDigest(report),
|
|
86
|
+
issues,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createLegacyKfdAdopterProtocolDriver() {
|
|
91
|
+
return defineAdopterProtocolDriver({
|
|
92
|
+
interface: ADOPTER_PROTOCOL_DRIVER_INTERFACE,
|
|
93
|
+
id: LEGACY_KFD_ADOPTER_PROTOCOL_ID,
|
|
94
|
+
version: LEGACY_KFD_ADOPTER_PROTOCOL_VERSION,
|
|
95
|
+
verify({ request, context }) {
|
|
96
|
+
const manifest = context?.adopterManifest;
|
|
97
|
+
const manifestGate = context?.adopterManifestGate;
|
|
98
|
+
if (!manifest || !manifestGate) {
|
|
99
|
+
return invalidContextResult([
|
|
100
|
+
issue(
|
|
101
|
+
"delivery-legacy-verification-context-invalid",
|
|
102
|
+
"/protocol",
|
|
103
|
+
"The exact standard adopter manifest and manifest gate are required.",
|
|
104
|
+
),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const source = manifest?.adopter?.artifact?.coordinate?.match(
|
|
109
|
+
/^([^@\s]+)@([0-9a-f]{40})$/,
|
|
110
|
+
);
|
|
111
|
+
const gateValidation = validateKfdAdopterManifestGate(manifestGate, {
|
|
112
|
+
expectedAdopterId: manifest?.adopter?.id,
|
|
113
|
+
expectedSourceRepository: source?.[1] ?? "",
|
|
114
|
+
expectedSourceSha: source?.[2] ?? "",
|
|
115
|
+
checkedAt: manifestGate?.checkedAt,
|
|
116
|
+
});
|
|
117
|
+
const projectionValidation = validateKfdLegacySupportMatrixProjection(
|
|
118
|
+
request.declaration,
|
|
119
|
+
{ manifest, manifestGate },
|
|
120
|
+
);
|
|
121
|
+
const adapterIssues = [
|
|
122
|
+
...bindingIssues(request, manifest),
|
|
123
|
+
...normalizedIssues(gateValidation.issues),
|
|
124
|
+
...normalizedIssues(projectionValidation.issues),
|
|
125
|
+
];
|
|
126
|
+
if (
|
|
127
|
+
request.declaration?.contract !== KFD_LEGACY_SUPPORT_MATRIX_CONTRACT
|
|
128
|
+
) {
|
|
129
|
+
adapterIssues.push(
|
|
130
|
+
issue(
|
|
131
|
+
"delivery-legacy-contract-invalid",
|
|
132
|
+
"/declaration/contract",
|
|
133
|
+
`Legacy declaration must use ${KFD_LEGACY_SUPPORT_MATRIX_CONTRACT}.`,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const valid =
|
|
139
|
+
gateValidation.valid &&
|
|
140
|
+
projectionValidation.valid &&
|
|
141
|
+
adapterIssues.length === 0;
|
|
142
|
+
const report = {
|
|
143
|
+
schemaVersion: 1,
|
|
144
|
+
contract: LEGACY_KFD_ADOPTER_DRIVER_REPORT_CONTRACT,
|
|
145
|
+
valid,
|
|
146
|
+
qualifying: false,
|
|
147
|
+
selfCertified: false,
|
|
148
|
+
independentlyCertified: false,
|
|
149
|
+
manifestRoot: manifestGate?.authority?.manifestRoot ?? null,
|
|
150
|
+
manifestGateRoot: manifestGate?.gateRoot ?? null,
|
|
151
|
+
legacyProjectionRoot: adopterDeliveryGateDigest(request.declaration),
|
|
152
|
+
legacyProjection: structuredClone(request.declaration),
|
|
153
|
+
adapterIssues,
|
|
154
|
+
nonClaims: [
|
|
155
|
+
"The legacy support matrix is a one-way projection, not declaration authority.",
|
|
156
|
+
"Buildchain carries the existing standard manifest decision and does not certify KFD adoption.",
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
valid,
|
|
161
|
+
report,
|
|
162
|
+
reportRoot: adopterDeliveryGateDigest(report),
|
|
163
|
+
issues: adapterIssues,
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|