@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +42 -27
- package/dist/site/buildchain-site.json +384 -33
- package/dist/site/capability-registry.json +15 -12
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +359 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +5882 -3605
- package/dist/site/page-registry.json +348 -21
- package/dist/site/public-surface-audit.json +396 -16
- package/dist/site/publication-authority-registry.json +106 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +35 -11
- package/dist/site/workflow-registry.json +204 -10
- package/docs/MAP.md +6 -2
- package/docs/auditable-demo.md +2 -2
- package/docs/cli-reference.md +136 -0
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +495 -281
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +11 -3
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +5 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +42 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +921 -0
- package/packages/core/release-passport.js +130 -20
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +2 -2
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +10 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +47 -4
- package/scripts/init-repo.mjs +26 -2
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +137 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/release-tail.mjs +159 -0
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +13 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -61,6 +61,10 @@ const SUPPORTED_SIGNING_PROFILES = new Set([
|
|
|
61
61
|
"windows-authenticode",
|
|
62
62
|
"detached-signature-v1",
|
|
63
63
|
]);
|
|
64
|
+
const SUPPORTED_SIGNING_ENTITLEMENTS_PROFILES = new Set([
|
|
65
|
+
"none",
|
|
66
|
+
"jit-executable-v1",
|
|
67
|
+
]);
|
|
64
68
|
const SUPPORTED_SIGNING_ARTIFACT_KINDS = new Set([
|
|
65
69
|
"auto", "binary", "archive", "blob", "directory", "mach-o", "app-bundle",
|
|
66
70
|
"framework-bundle", "plugin-bundle", "xpc-bundle", "dylib", "pkg", "dmg",
|
|
@@ -99,6 +103,27 @@ function normalizeStringArray(value, label) {
|
|
|
99
103
|
return value.map((entry, index) => assertString(entry, `${label}[${index}]`));
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
function normalizeSigningEntitlementsPaths(value, label) {
|
|
107
|
+
const paths = normalizeStringArray(value, label);
|
|
108
|
+
for (const [index, entry] of paths.entries()) {
|
|
109
|
+
const parts = entry.split("/");
|
|
110
|
+
if (
|
|
111
|
+
entry.startsWith("/") ||
|
|
112
|
+
entry.includes("\\") ||
|
|
113
|
+
entry.includes(",") ||
|
|
114
|
+
parts.some((part) => part === "" || part === "." || part === "..")
|
|
115
|
+
) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`${label}[${index}] must be a safe archive-relative path`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (new Set(paths).size !== paths.length) {
|
|
122
|
+
throw new Error(`${label} must not contain duplicate paths`);
|
|
123
|
+
}
|
|
124
|
+
return paths;
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
function getByDottedKey(target, key) {
|
|
103
128
|
return String(key)
|
|
104
129
|
.split(".")
|
|
@@ -362,7 +387,16 @@ function normalizeSigningSection(signing) {
|
|
|
362
387
|
const artifacts = signing.artifacts.map((artifact, index) => {
|
|
363
388
|
const label = `signing.artifacts[${index}]`;
|
|
364
389
|
assertPlainObject(artifact, label);
|
|
365
|
-
const allowedKeys = new Set([
|
|
390
|
+
const allowedKeys = new Set([
|
|
391
|
+
"id",
|
|
392
|
+
"path",
|
|
393
|
+
"profile",
|
|
394
|
+
"entitlements_profile",
|
|
395
|
+
"entitlements_paths",
|
|
396
|
+
"kind",
|
|
397
|
+
"platforms",
|
|
398
|
+
"required",
|
|
399
|
+
]);
|
|
366
400
|
for (const key of Object.keys(artifact)) {
|
|
367
401
|
if (!allowedKeys.has(key)) {
|
|
368
402
|
throw new Error(`${label}.${key} is not supported; declare desired signature state only`);
|
|
@@ -377,10 +411,41 @@ function normalizeSigningSection(signing) {
|
|
|
377
411
|
if (!SUPPORTED_SIGNING_ARTIFACT_KINDS.has(kind)) {
|
|
378
412
|
throw new Error(`${label}.kind is not a supported signing artifact kind`);
|
|
379
413
|
}
|
|
414
|
+
const entitlementsProfile =
|
|
415
|
+
artifact.entitlements_profile === undefined
|
|
416
|
+
? "none"
|
|
417
|
+
: assertString(
|
|
418
|
+
artifact.entitlements_profile,
|
|
419
|
+
`${label}.entitlements_profile`,
|
|
420
|
+
);
|
|
421
|
+
if (!SUPPORTED_SIGNING_ENTITLEMENTS_PROFILES.has(entitlementsProfile)) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`${label}.entitlements_profile must be one of none or jit-executable-v1`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (entitlementsProfile !== "none" && kind !== "archive") {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`${label}.entitlements_profile ${entitlementsProfile} requires kind = "archive"`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const entitlementsPaths = normalizeSigningEntitlementsPaths(
|
|
432
|
+
artifact.entitlements_paths,
|
|
433
|
+
`${label}.entitlements_paths`,
|
|
434
|
+
);
|
|
435
|
+
if (
|
|
436
|
+
(entitlementsProfile === "none" && entitlementsPaths.length !== 0) ||
|
|
437
|
+
(entitlementsProfile !== "none" && entitlementsPaths.length === 0)
|
|
438
|
+
) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`${label}.entitlements_paths must be non-empty exactly when entitlements_profile is enabled`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
380
443
|
return {
|
|
381
444
|
id: artifact.id === undefined ? artifactPath : assertString(artifact.id, `${label}.id`),
|
|
382
445
|
path: artifactPath,
|
|
383
446
|
profile,
|
|
447
|
+
entitlementsProfile,
|
|
448
|
+
entitlementsPaths,
|
|
384
449
|
kind,
|
|
385
450
|
platforms: normalizeStringArray(artifact.platforms, `${label}.platforms`),
|
|
386
451
|
required: optionalBoolean(artifact.required, true),
|
|
@@ -1193,11 +1258,6 @@ function validateAnchoredDerivedVersionMaterialConfig(config) {
|
|
|
1193
1258
|
if (derivedFiles.length === 0) {
|
|
1194
1259
|
return;
|
|
1195
1260
|
}
|
|
1196
|
-
if (config.version.strategy !== "anchored" || config.version.next !== "manual") {
|
|
1197
|
-
throw new Error(
|
|
1198
|
-
"version.derived_files requires version.strategy = \"anchored\" and version.next = \"manual\"",
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
1261
|
const versionState =
|
|
1202
1262
|
config.lifecycle?.["version-state"] ||
|
|
1203
1263
|
config.lifecycle?.version_state;
|
|
@@ -138,7 +138,7 @@ function immediateReadmes(root, dir) {
|
|
|
138
138
|
|
|
139
139
|
function publicDocumentationFiles(root) {
|
|
140
140
|
return uniquePaths([
|
|
141
|
-
...BUILDCHAIN_AGENT_MANUALS.map((entry) => entry.path),
|
|
141
|
+
...(readJson(root, "dist/site/manual-registry.json").manuals || BUILDCHAIN_AGENT_MANUALS).map((entry) => entry.path),
|
|
142
142
|
"README.md",
|
|
143
143
|
"packages/core/README.md",
|
|
144
144
|
...immediateReadmes(root, "actions"),
|
|
@@ -37,6 +37,10 @@ const DESCRIPTORS = Object.freeze([
|
|
|
37
37
|
[".github/workflows/dev-delivery-warrant-cancel.yml", "governance-write"],
|
|
38
38
|
[".github/workflows/dev-merge-queue-governance.yml", "governance-write"],
|
|
39
39
|
[".github/workflows/dev-pr-auto-merge.yml", "governance-write"],
|
|
40
|
+
[".github/workflows/engineering-housekeeper-daily.yml", "governance-write"],
|
|
41
|
+
[".github/workflows/engineering-housekeeper-monthly.yml", "governance-write"],
|
|
42
|
+
[".github/workflows/engineering-housekeeper-weekly.yml", "governance-write"],
|
|
43
|
+
[".github/workflows/engineering-housekeeper.yml", "governance-write"],
|
|
40
44
|
[".github/workflows/github-governance-audit.yml", "governance-write"],
|
|
41
45
|
[".github/workflows/github-artifact-attestation.yml", "evidence-publication", true, ["github-artifact-attestation"], "oidc", "consumer-defined", "caller-bound", "caller-bound"],
|
|
42
46
|
[".github/workflows/npm-publish.yml", "dry-run-only"],
|
|
@@ -50,6 +54,7 @@ const DESCRIPTORS = Object.freeze([
|
|
|
50
54
|
[".github/workflows/release-candidate-promote.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "caller-bound"],
|
|
51
55
|
[".github/workflows/release-line-bootstrap.yml", "governance-write"],
|
|
52
56
|
[".github/workflows/release-propagation.yml", "governance-write"],
|
|
57
|
+
[".github/workflows/release-tail.yml", "product-publication", true, ["artifact.publish", "signed-channel.commit", "release.activate", "released-evidence.synthesize"], "caller-secrets", "caller-bound", "caller-bound", "fixed"],
|
|
53
58
|
[".github/workflows/release-verify.yml", "governance-write"],
|
|
54
59
|
[".github/workflows/stable-candidate-patrol.yml", "governance-write"],
|
|
55
60
|
]);
|
|
@@ -34,13 +34,14 @@ const CONTROLLER_SPECS = [
|
|
|
34
34
|
"resolve-runtime",
|
|
35
35
|
"resolve-source",
|
|
36
36
|
"anchored-release-preflight",
|
|
37
|
+
"pre-build-contracts",
|
|
37
38
|
"build",
|
|
38
39
|
"signing-finalization",
|
|
39
40
|
"credential-island",
|
|
40
41
|
"verify",
|
|
41
42
|
"aggregate",
|
|
42
43
|
],
|
|
43
|
-
optionalStages: ["anchored-release-preflight", "credential-island"],
|
|
44
|
+
optionalStages: ["anchored-release-preflight", "pre-build-contracts", "credential-island"],
|
|
44
45
|
evidence: [
|
|
45
46
|
"platform-manifests",
|
|
46
47
|
"build-summary",
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import {
|
|
2
|
+
devDeliveryClone as clone,
|
|
3
|
+
devDeliveryContentRoot,
|
|
4
|
+
devDeliveryExactRoot as exactRoot,
|
|
5
|
+
devDeliveryExactSha as exactSha,
|
|
6
|
+
devDeliveryPositiveInteger as positiveInteger,
|
|
7
|
+
devDeliveryText as text,
|
|
8
|
+
devDeliveryTimestamp as timestamp,
|
|
9
|
+
} from "./dev-delivery-common.js";
|
|
10
|
+
import {
|
|
11
|
+
normalizeDevDeliveryQueue,
|
|
12
|
+
rankDevDeliveryCandidates,
|
|
13
|
+
} from "./dev-delivery-warrant.js";
|
|
14
|
+
|
|
15
|
+
export const DEV_DELIVERY_WARRANT_SHADOW_OBSERVATION_SCHEMA =
|
|
16
|
+
"kungfu.buildchain.dev-delivery-warrant-shadow-observation/v1";
|
|
17
|
+
export const DEV_DELIVERY_WARRANT_SHADOW_PLAN_SCHEMA =
|
|
18
|
+
"kungfu.buildchain.dev-delivery-warrant-shadow-plan/v1";
|
|
19
|
+
export const DEV_DELIVERY_WARRANT_SHADOW_QUALIFICATION_SCHEMA =
|
|
20
|
+
"kungfu.buildchain.dev-delivery-warrant-shadow-qualification/v1";
|
|
21
|
+
|
|
22
|
+
const EVIDENCE_STATES = Object.freeze({
|
|
23
|
+
projectCut: "qualified",
|
|
24
|
+
approval: "approved",
|
|
25
|
+
requiredChecks: "passed",
|
|
26
|
+
status: "ready",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function nonNegativeInteger(value, label, fallback = 0) {
|
|
30
|
+
const parsed = Number(value ?? fallback);
|
|
31
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
32
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function boolean(value, label) {
|
|
38
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be boolean`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function evidenceBinding(input, label) {
|
|
43
|
+
const binding = input || {};
|
|
44
|
+
return {
|
|
45
|
+
candidateId: exactRoot(binding.candidateId, `${label}.candidateId`),
|
|
46
|
+
sourceHead: exactSha(binding.sourceHead, `${label}.sourceHead`),
|
|
47
|
+
baseHead: exactSha(binding.baseHead, `${label}.baseHead`),
|
|
48
|
+
root: exactRoot(binding.root, `${label}.root`),
|
|
49
|
+
state: text(binding.state),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function laneBinding(input) {
|
|
54
|
+
const binding = input || {};
|
|
55
|
+
return {
|
|
56
|
+
candidateId: exactRoot(binding.candidateId, "binding.candidateId"),
|
|
57
|
+
sourceHead: exactSha(binding.sourceHead, "binding.sourceHead"),
|
|
58
|
+
baseHead: exactSha(binding.baseHead, "binding.baseHead"),
|
|
59
|
+
queueGeneration: nonNegativeInteger(
|
|
60
|
+
binding.queueGeneration,
|
|
61
|
+
"binding.queueGeneration",
|
|
62
|
+
),
|
|
63
|
+
queueStateRoot: exactRoot(binding.queueStateRoot, "binding.queueStateRoot"),
|
|
64
|
+
projectedBaseRoot: exactRoot(
|
|
65
|
+
binding.projectedBaseRoot,
|
|
66
|
+
"binding.projectedBaseRoot",
|
|
67
|
+
),
|
|
68
|
+
warrantFencingToken: binding.warrantFencingToken
|
|
69
|
+
? exactRoot(binding.warrantFencingToken, "binding.warrantFencingToken")
|
|
70
|
+
: null,
|
|
71
|
+
warrantGeneration:
|
|
72
|
+
binding.warrantGeneration == null
|
|
73
|
+
? null
|
|
74
|
+
: positiveInteger(
|
|
75
|
+
binding.warrantGeneration,
|
|
76
|
+
"binding.warrantGeneration",
|
|
77
|
+
),
|
|
78
|
+
projectCut: evidenceBinding(binding.projectCut, "binding.projectCut"),
|
|
79
|
+
approval: evidenceBinding(binding.approval, "binding.approval"),
|
|
80
|
+
requiredChecks: evidenceBinding(
|
|
81
|
+
binding.requiredChecks,
|
|
82
|
+
"binding.requiredChecks",
|
|
83
|
+
),
|
|
84
|
+
status: evidenceBinding(binding.status, "binding.status"),
|
|
85
|
+
lease: {
|
|
86
|
+
...evidenceBinding(binding.lease, "binding.lease"),
|
|
87
|
+
state: text(binding.lease?.state),
|
|
88
|
+
},
|
|
89
|
+
conflictKeys: [
|
|
90
|
+
...new Set((binding.conflictKeys || []).map((entry) => text(entry))),
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.sort(),
|
|
94
|
+
expectedEligible:
|
|
95
|
+
binding.expectedEligible == null
|
|
96
|
+
? null
|
|
97
|
+
: boolean(binding.expectedEligible, "binding.expectedEligible"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeObservation(input) {
|
|
102
|
+
const observation = clone(input || {});
|
|
103
|
+
if (observation.schema !== DEV_DELIVERY_WARRANT_SHADOW_OBSERVATION_SCHEMA) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`shadow observation must use ${DEV_DELIVERY_WARRANT_SHADOW_OBSERVATION_SCHEMA}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const queue = normalizeDevDeliveryQueue(observation.queue);
|
|
109
|
+
const normalized = {
|
|
110
|
+
schema: observation.schema,
|
|
111
|
+
observationId: text(observation.observationId),
|
|
112
|
+
kind: text(observation.kind || "replay"),
|
|
113
|
+
observedAt: timestamp(observation.observedAt, "observation.observedAt"),
|
|
114
|
+
protectedBaseHead: exactSha(
|
|
115
|
+
observation.protectedBaseHead,
|
|
116
|
+
"observation.protectedBaseHead",
|
|
117
|
+
),
|
|
118
|
+
projectedBaseRoot: exactRoot(
|
|
119
|
+
observation.projectedBaseRoot,
|
|
120
|
+
"observation.projectedBaseRoot",
|
|
121
|
+
),
|
|
122
|
+
queue,
|
|
123
|
+
nativeQueue: {
|
|
124
|
+
occupied: boolean(
|
|
125
|
+
observation.nativeQueue?.occupied,
|
|
126
|
+
"observation.nativeQueue.occupied",
|
|
127
|
+
),
|
|
128
|
+
entryCount: nonNegativeInteger(
|
|
129
|
+
observation.nativeQueue?.entryCount,
|
|
130
|
+
"observation.nativeQueue.entryCount",
|
|
131
|
+
),
|
|
132
|
+
root: exactRoot(
|
|
133
|
+
observation.nativeQueue?.root,
|
|
134
|
+
"observation.nativeQueue.root",
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
candidateBindings: (observation.candidateBindings || []).map(laneBinding),
|
|
138
|
+
metrics: {
|
|
139
|
+
baselineQueueWaitSeconds: nonNegativeInteger(
|
|
140
|
+
observation.metrics?.baselineQueueWaitSeconds,
|
|
141
|
+
"observation.metrics.baselineQueueWaitSeconds",
|
|
142
|
+
),
|
|
143
|
+
shadowQueueWaitSeconds: nonNegativeInteger(
|
|
144
|
+
observation.metrics?.shadowQueueWaitSeconds,
|
|
145
|
+
"observation.metrics.shadowQueueWaitSeconds",
|
|
146
|
+
),
|
|
147
|
+
additionalCheckSeconds: nonNegativeInteger(
|
|
148
|
+
observation.metrics?.additionalCheckSeconds,
|
|
149
|
+
"observation.metrics.additionalCheckSeconds",
|
|
150
|
+
),
|
|
151
|
+
additionalRunnerSeconds: nonNegativeInteger(
|
|
152
|
+
observation.metrics?.additionalRunnerSeconds,
|
|
153
|
+
"observation.metrics.additionalRunnerSeconds",
|
|
154
|
+
),
|
|
155
|
+
ambiguous: boolean(
|
|
156
|
+
observation.metrics?.ambiguous ?? false,
|
|
157
|
+
"observation.metrics.ambiguous",
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
if (!normalized.observationId) {
|
|
162
|
+
throw new Error("observation.observationId is required");
|
|
163
|
+
}
|
|
164
|
+
const bindingIds = normalized.candidateBindings.map(
|
|
165
|
+
(binding) => binding.candidateId,
|
|
166
|
+
);
|
|
167
|
+
if (new Set(bindingIds).size !== bindingIds.length) {
|
|
168
|
+
throw new Error("shadow observation contains duplicate candidate bindings");
|
|
169
|
+
}
|
|
170
|
+
return normalized;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function productionOrder(queue, now) {
|
|
174
|
+
const active = queue.activeWarrant
|
|
175
|
+
? queue.candidates.find(
|
|
176
|
+
(candidate) =>
|
|
177
|
+
candidate.candidateId === queue.activeWarrant.candidateId,
|
|
178
|
+
)
|
|
179
|
+
: null;
|
|
180
|
+
const queued = rankDevDeliveryCandidates(queue, { now }).map(
|
|
181
|
+
(entry) => entry.candidate,
|
|
182
|
+
);
|
|
183
|
+
return active ? [active, ...queued] : queued;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function bindingReasons({ binding, candidate, observation }) {
|
|
187
|
+
const reasons = [];
|
|
188
|
+
const expect = (condition, reason) => {
|
|
189
|
+
if (!condition) reasons.push(reason);
|
|
190
|
+
};
|
|
191
|
+
expect(
|
|
192
|
+
binding.candidateId === candidate.candidateId,
|
|
193
|
+
"candidate-id-mismatch",
|
|
194
|
+
);
|
|
195
|
+
expect(binding.sourceHead === candidate.sourceHead, "stale-source-head");
|
|
196
|
+
expect(binding.baseHead === observation.protectedBaseHead, "stale-base-head");
|
|
197
|
+
expect(
|
|
198
|
+
binding.queueGeneration === observation.queue.generation,
|
|
199
|
+
"stale-queue-generation",
|
|
200
|
+
);
|
|
201
|
+
expect(
|
|
202
|
+
binding.queueStateRoot === observation.queue.stateRoot,
|
|
203
|
+
"stale-queue-state-root",
|
|
204
|
+
);
|
|
205
|
+
expect(
|
|
206
|
+
binding.projectedBaseRoot === observation.projectedBaseRoot,
|
|
207
|
+
"incompatible-projected-base",
|
|
208
|
+
);
|
|
209
|
+
for (const [name, acceptedState] of Object.entries(EVIDENCE_STATES)) {
|
|
210
|
+
const evidence = binding[name];
|
|
211
|
+
expect(
|
|
212
|
+
evidence.candidateId === candidate.candidateId,
|
|
213
|
+
`${name}-candidate-mismatch`,
|
|
214
|
+
);
|
|
215
|
+
expect(
|
|
216
|
+
evidence.sourceHead === candidate.sourceHead,
|
|
217
|
+
`${name}-head-mismatch`,
|
|
218
|
+
);
|
|
219
|
+
expect(
|
|
220
|
+
evidence.baseHead === observation.protectedBaseHead,
|
|
221
|
+
`${name}-base-mismatch`,
|
|
222
|
+
);
|
|
223
|
+
expect(evidence.state === acceptedState, `${name}-not-${acceptedState}`);
|
|
224
|
+
}
|
|
225
|
+
expect(
|
|
226
|
+
binding.lease.candidateId === candidate.candidateId,
|
|
227
|
+
"lease-candidate-mismatch",
|
|
228
|
+
);
|
|
229
|
+
expect(
|
|
230
|
+
binding.lease.sourceHead === candidate.sourceHead,
|
|
231
|
+
"lease-head-mismatch",
|
|
232
|
+
);
|
|
233
|
+
expect(
|
|
234
|
+
binding.lease.baseHead === observation.protectedBaseHead,
|
|
235
|
+
"lease-base-mismatch",
|
|
236
|
+
);
|
|
237
|
+
const active = observation.queue.activeWarrant;
|
|
238
|
+
if (active?.candidateId === candidate.candidateId) {
|
|
239
|
+
expect(binding.lease.state === "active", "active-lease-not-bound");
|
|
240
|
+
expect(
|
|
241
|
+
binding.warrantFencingToken === active.fencingToken,
|
|
242
|
+
"stale-warrant-fence",
|
|
243
|
+
);
|
|
244
|
+
expect(
|
|
245
|
+
binding.warrantGeneration === active.generation,
|
|
246
|
+
"stale-warrant-generation",
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
expect(
|
|
250
|
+
binding.lease.state === "available",
|
|
251
|
+
"queued-lane-lease-unavailable",
|
|
252
|
+
);
|
|
253
|
+
expect(binding.warrantFencingToken === null, "queued-lane-has-live-fence");
|
|
254
|
+
expect(
|
|
255
|
+
binding.warrantGeneration === null,
|
|
256
|
+
"queued-lane-has-live-generation",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return reasons;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function aliasedLaneIndexes(lanes) {
|
|
263
|
+
const indexes = new Set();
|
|
264
|
+
const roots = new Map();
|
|
265
|
+
const fields = [
|
|
266
|
+
"projectCut",
|
|
267
|
+
"approval",
|
|
268
|
+
"requiredChecks",
|
|
269
|
+
"status",
|
|
270
|
+
"lease",
|
|
271
|
+
];
|
|
272
|
+
lanes.forEach((lane, index) => {
|
|
273
|
+
if (!lane.binding) return;
|
|
274
|
+
for (const field of fields) {
|
|
275
|
+
const key = `${field}:${lane.binding[field].root}`;
|
|
276
|
+
const prior = roots.get(key);
|
|
277
|
+
if (prior != null) {
|
|
278
|
+
indexes.add(prior);
|
|
279
|
+
indexes.add(index);
|
|
280
|
+
} else roots.set(key, index);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return indexes;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function conflictingLaneIndexes(lanes) {
|
|
287
|
+
const indexes = new Set();
|
|
288
|
+
for (let left = 0; left < lanes.length; left += 1) {
|
|
289
|
+
for (let right = left + 1; right < lanes.length; right += 1) {
|
|
290
|
+
if (!lanes[left].binding || !lanes[right].binding) continue;
|
|
291
|
+
const rightKeys = new Set(lanes[right].binding.conflictKeys);
|
|
292
|
+
if (lanes[left].binding.conflictKeys.some((key) => rightKeys.has(key))) {
|
|
293
|
+
indexes.add(left);
|
|
294
|
+
indexes.add(right);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return indexes;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function planDevDeliveryWarrantShadow(
|
|
302
|
+
input,
|
|
303
|
+
{ maxConcurrency = 2 } = {},
|
|
304
|
+
) {
|
|
305
|
+
const requested = positiveInteger(maxConcurrency, "maxConcurrency");
|
|
306
|
+
if (requested > 2) throw new Error("shadow maxConcurrency cannot exceed 2");
|
|
307
|
+
const observation = normalizeObservation(input);
|
|
308
|
+
const order = productionOrder(observation.queue, observation.observedAt);
|
|
309
|
+
const candidates = order.slice(0, requested);
|
|
310
|
+
const bindings = new Map(
|
|
311
|
+
observation.candidateBindings.map((binding) => [
|
|
312
|
+
binding.candidateId,
|
|
313
|
+
binding,
|
|
314
|
+
]),
|
|
315
|
+
);
|
|
316
|
+
const lanes = candidates.map((candidate) => {
|
|
317
|
+
const binding = bindings.get(candidate.candidateId);
|
|
318
|
+
const reasonCodes = binding
|
|
319
|
+
? bindingReasons({ binding, candidate, observation })
|
|
320
|
+
: ["candidate-binding-missing"];
|
|
321
|
+
return { candidate, binding, reasonCodes };
|
|
322
|
+
});
|
|
323
|
+
if (observation.nativeQueue.occupied) {
|
|
324
|
+
for (const lane of lanes) lane.reasonCodes.push("native-queue-occupied");
|
|
325
|
+
}
|
|
326
|
+
for (const index of aliasedLaneIndexes(lanes)) {
|
|
327
|
+
lanes[index].reasonCodes.push("cross-lane-binding-alias");
|
|
328
|
+
}
|
|
329
|
+
for (const index of conflictingLaneIndexes(lanes)) {
|
|
330
|
+
lanes[index].reasonCodes.push("cross-lane-conflict");
|
|
331
|
+
}
|
|
332
|
+
const plannedLanes = lanes.map((lane) => {
|
|
333
|
+
const reasonCodes = [...new Set(lane.reasonCodes)].sort();
|
|
334
|
+
const laneBody = {
|
|
335
|
+
schema: "kungfu.buildchain.dev-delivery-warrant-shadow-lane/v1",
|
|
336
|
+
candidateId: lane.candidate.candidateId,
|
|
337
|
+
pullRequestNumber: lane.candidate.pullRequestNumber,
|
|
338
|
+
sourceHead: lane.candidate.sourceHead,
|
|
339
|
+
baseHead: observation.protectedBaseHead,
|
|
340
|
+
queueStateRoot: observation.queue.stateRoot,
|
|
341
|
+
bindingRoot: lane.binding ? devDeliveryContentRoot(lane.binding) : null,
|
|
342
|
+
accepted: reasonCodes.length === 0,
|
|
343
|
+
reasonCodes,
|
|
344
|
+
effects: [],
|
|
345
|
+
productionAuthority: false,
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
...laneBody,
|
|
349
|
+
laneId: devDeliveryContentRoot({
|
|
350
|
+
schema: laneBody.schema,
|
|
351
|
+
candidateId: laneBody.candidateId,
|
|
352
|
+
sourceHead: laneBody.sourceHead,
|
|
353
|
+
baseHead: laneBody.baseHead,
|
|
354
|
+
queueStateRoot: laneBody.queueStateRoot,
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
const productionCandidateId = order[0]?.candidateId || null;
|
|
359
|
+
const body = {
|
|
360
|
+
schema: DEV_DELIVERY_WARRANT_SHADOW_PLAN_SCHEMA,
|
|
361
|
+
observationId: observation.observationId,
|
|
362
|
+
observationRoot: devDeliveryContentRoot(observation),
|
|
363
|
+
observedAt: observation.observedAt,
|
|
364
|
+
maxConcurrency: requested,
|
|
365
|
+
productionCandidateId,
|
|
366
|
+
singleFlightParity: {
|
|
367
|
+
evaluated: requested === 1,
|
|
368
|
+
candidateIdentityMatches:
|
|
369
|
+
requested === 1 &&
|
|
370
|
+
(plannedLanes[0]?.candidateId || null) === productionCandidateId,
|
|
371
|
+
},
|
|
372
|
+
lanes: plannedLanes,
|
|
373
|
+
acceptedLaneCount: plannedLanes.filter((lane) => lane.accepted).length,
|
|
374
|
+
rejectedLaneCount: plannedLanes.filter((lane) => !lane.accepted).length,
|
|
375
|
+
deferredCandidateIds: order
|
|
376
|
+
.slice(requested)
|
|
377
|
+
.map((candidate) => candidate.candidateId),
|
|
378
|
+
metrics: observation.metrics,
|
|
379
|
+
decision:
|
|
380
|
+
plannedLanes.some((lane) => lane.accepted) &&
|
|
381
|
+
!observation.nativeQueue.occupied
|
|
382
|
+
? "qualified-shadow-only"
|
|
383
|
+
: "hold",
|
|
384
|
+
effects: [],
|
|
385
|
+
mutationAllowed: false,
|
|
386
|
+
productionAuthority: "unchanged-single-flight",
|
|
387
|
+
rolloutAuthorized: false,
|
|
388
|
+
};
|
|
389
|
+
return { ...body, planRoot: devDeliveryContentRoot(body) };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function thresholds(input = {}) {
|
|
393
|
+
return {
|
|
394
|
+
minObservationCount: positiveInteger(
|
|
395
|
+
input.minObservationCount,
|
|
396
|
+
"thresholds.minObservationCount",
|
|
397
|
+
8,
|
|
398
|
+
),
|
|
399
|
+
minEligibleOverlapCount: positiveInteger(
|
|
400
|
+
input.minEligibleOverlapCount,
|
|
401
|
+
"thresholds.minEligibleOverlapCount",
|
|
402
|
+
2,
|
|
403
|
+
),
|
|
404
|
+
minProjectedQueueWaitBenefitSeconds: nonNegativeInteger(
|
|
405
|
+
input.minProjectedQueueWaitBenefitSeconds,
|
|
406
|
+
"thresholds.minProjectedQueueWaitBenefitSeconds",
|
|
407
|
+
300,
|
|
408
|
+
),
|
|
409
|
+
maxAdditionalRunnerSeconds: nonNegativeInteger(
|
|
410
|
+
input.maxAdditionalRunnerSeconds,
|
|
411
|
+
"thresholds.maxAdditionalRunnerSeconds",
|
|
412
|
+
7200,
|
|
413
|
+
),
|
|
414
|
+
maxAmbiguityCount: nonNegativeInteger(
|
|
415
|
+
input.maxAmbiguityCount,
|
|
416
|
+
"thresholds.maxAmbiguityCount",
|
|
417
|
+
0,
|
|
418
|
+
),
|
|
419
|
+
maxFalsePositiveCount: nonNegativeInteger(
|
|
420
|
+
input.maxFalsePositiveCount,
|
|
421
|
+
"thresholds.maxFalsePositiveCount",
|
|
422
|
+
0,
|
|
423
|
+
),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function qualifyDevDeliveryWarrantShadow(input = {}) {
|
|
428
|
+
const observations = Array.isArray(input.observations)
|
|
429
|
+
? input.observations
|
|
430
|
+
: [];
|
|
431
|
+
const policy = thresholds(input.thresholds);
|
|
432
|
+
const plans = observations.map((observation) =>
|
|
433
|
+
planDevDeliveryWarrantShadow(observation, { maxConcurrency: 2 }),
|
|
434
|
+
);
|
|
435
|
+
const metrics = plans.reduce(
|
|
436
|
+
(summary, plan) => {
|
|
437
|
+
if (plan.lanes.filter((lane) => lane.accepted).length === 2) {
|
|
438
|
+
summary.eligibleOverlapCount += 1;
|
|
439
|
+
}
|
|
440
|
+
summary.projectedQueueWaitBenefitSeconds += Math.max(
|
|
441
|
+
0,
|
|
442
|
+
plan.metrics.baselineQueueWaitSeconds -
|
|
443
|
+
plan.metrics.shadowQueueWaitSeconds,
|
|
444
|
+
);
|
|
445
|
+
summary.additionalCheckSeconds += plan.metrics.additionalCheckSeconds;
|
|
446
|
+
summary.additionalRunnerSeconds += plan.metrics.additionalRunnerSeconds;
|
|
447
|
+
if (plan.metrics.ambiguous) summary.ambiguityCount += 1;
|
|
448
|
+
const observation = normalizeObservation(
|
|
449
|
+
observations[summary.observationCount],
|
|
450
|
+
);
|
|
451
|
+
const expected = new Map(
|
|
452
|
+
observation.candidateBindings.map((binding) => [
|
|
453
|
+
binding.candidateId,
|
|
454
|
+
binding.expectedEligible,
|
|
455
|
+
]),
|
|
456
|
+
);
|
|
457
|
+
summary.falsePositiveCount += plan.lanes.filter(
|
|
458
|
+
(lane) => lane.accepted && expected.get(lane.candidateId) === false,
|
|
459
|
+
).length;
|
|
460
|
+
summary.observationCount += 1;
|
|
461
|
+
return summary;
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
observationCount: 0,
|
|
465
|
+
eligibleOverlapCount: 0,
|
|
466
|
+
projectedQueueWaitBenefitSeconds: 0,
|
|
467
|
+
additionalCheckSeconds: 0,
|
|
468
|
+
additionalRunnerSeconds: 0,
|
|
469
|
+
ambiguityCount: 0,
|
|
470
|
+
falsePositiveCount: 0,
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
const checks = {
|
|
474
|
+
observationCount: metrics.observationCount >= policy.minObservationCount,
|
|
475
|
+
eligibleOverlap:
|
|
476
|
+
metrics.eligibleOverlapCount >= policy.minEligibleOverlapCount,
|
|
477
|
+
projectedQueueWaitBenefit:
|
|
478
|
+
metrics.projectedQueueWaitBenefitSeconds >=
|
|
479
|
+
policy.minProjectedQueueWaitBenefitSeconds,
|
|
480
|
+
runnerCost:
|
|
481
|
+
metrics.additionalRunnerSeconds <= policy.maxAdditionalRunnerSeconds,
|
|
482
|
+
ambiguity: metrics.ambiguityCount <= policy.maxAmbiguityCount,
|
|
483
|
+
falsePositives: metrics.falsePositiveCount <= policy.maxFalsePositiveCount,
|
|
484
|
+
};
|
|
485
|
+
const decision = Object.values(checks).every(Boolean) ? "proceed" : "hold";
|
|
486
|
+
const body = {
|
|
487
|
+
schema: DEV_DELIVERY_WARRANT_SHADOW_QUALIFICATION_SCHEMA,
|
|
488
|
+
decision,
|
|
489
|
+
reasonCodes: Object.entries(checks)
|
|
490
|
+
.filter(([, passed]) => !passed)
|
|
491
|
+
.map(([name]) => `threshold-${name}-not-met`),
|
|
492
|
+
thresholds: policy,
|
|
493
|
+
metrics,
|
|
494
|
+
checks,
|
|
495
|
+
planRoots: plans.map((plan) => plan.planRoot),
|
|
496
|
+
effects: [],
|
|
497
|
+
mutationAllowed: false,
|
|
498
|
+
productionAuthority: "unchanged-single-flight",
|
|
499
|
+
rolloutAuthorized: false,
|
|
500
|
+
};
|
|
501
|
+
return { ...body, qualificationRoot: devDeliveryContentRoot(body), plans };
|
|
502
|
+
}
|