@kungfu-tech/buildchain 4.0.1-alpha.6 → 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.
Files changed (50) hide show
  1. package/architecture/ci-lane-change-budget.json +187 -0
  2. package/architecture/maintainability-policy.json +13 -13
  3. package/architecture/v4-floating-consumer-policy.json +6 -0
  4. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  5. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  6. package/dist/site/buildchain-contract.json +9 -9
  7. package/dist/site/buildchain-site.json +19 -9
  8. package/dist/site/capability-registry.json +2 -2
  9. package/dist/site/kfd-claims.json +31 -5
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +1 -1
  12. package/dist/site/node-api-registry.json +154 -73
  13. package/dist/site/page-registry.json +14 -4
  14. package/dist/site/public-surface-audit.json +14 -3
  15. package/dist/site/publication-authority-registry.json +23 -5
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +1 -0
  18. package/dist/site/site-manifest.json +5 -5
  19. package/dist/site/workflow-registry.json +25 -4
  20. package/docs/dev-delivery-warrant.md +22 -1
  21. package/docs/node-api-reference.md +161 -148
  22. package/package.json +3 -2
  23. package/packages/core/buildchain-publication-authority.js +1 -0
  24. package/packages/core/ci-lane-change-budget.js +247 -0
  25. package/packages/core/dev-delivery-native-proof.js +43 -27
  26. package/packages/core/dev-delivery-warrant-state.js +31 -8
  27. package/packages/core/dev-delivery-warrant.js +8 -3
  28. package/packages/core/github-governance-authority.js +1 -0
  29. package/packages/core/index.js +5 -0
  30. package/packages/core/release-candidate-recovery.js +66 -11
  31. package/packages/core/release-candidate.js +2 -2
  32. package/packages/core/release-passport.js +10 -1
  33. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  34. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  35. package/packages/core/v4-floating-consumer-policy.js +138 -35
  36. package/packages/core/v4-floating-consumer-release-passport.js +30 -3
  37. package/packages/core/workflow-yaml-contract.js +24 -2
  38. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  39. package/scripts/check-inventory.mjs +2 -2
  40. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  41. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  42. package/scripts/dev-delivery-warrant.mjs +5 -1
  43. package/scripts/dev-pr-delivery-warrant.mjs +1 -0
  44. package/scripts/dev-qualification-patrol.mjs +9 -9
  45. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  46. package/scripts/promotion-channel-router.mjs +4 -4
  47. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  48. package/scripts/resume-from-candidate-run.mjs +11 -14
  49. package/scripts/site-capability-metadata.mjs +1 -0
  50. package/scripts/v4-consumer-policy.mjs +4 -0
@@ -308,6 +308,42 @@ function validateProductPayloads({ passport, artifacts, productPayloadManifests,
308
308
  }
309
309
  }
310
310
 
311
+ function assertAnchorEqual(actual, expected, label) {
312
+ assertEqual(actual, expected, "anchored-provenance-invalid", label, "Preserve both candidate runs and repair the explicit anchor provenance before retrying.");
313
+ }
314
+
315
+ function validateAnchorProvenance({ run, passport, anchorProvenance, expectedTransactionId, workflowFile, expectedWorkflowName, repository, targetRef }) {
316
+ const request = anchorProvenance.request;
317
+ const provenanceRun = anchorProvenance.run;
318
+ const provenanceWorkflow = anchorProvenance.workflow;
319
+ const provenancePassport = anchorProvenance.passport;
320
+ assertAnchorEqual(run?.status, "completed", "anchored candidate run status");
321
+ assertAnchorEqual(run?.conclusion, "success", "anchored candidate run conclusion");
322
+ assertAnchorEqual(run?.event, "workflow_dispatch", "anchored candidate run event");
323
+ assertAnchorEqual(passport?.target?.channel, "anchor", "anchored candidate channel");
324
+ assertAnchorEqual(passport?.target?.ref, "publish-gate/anchor", "anchored candidate ref");
325
+ assertAnchorEqual(run?.headSha, passport?.buildchain?.sha, "anchored candidate runtime");
326
+ assertAnchorEqual(request?.schema, 1, "anchor request schema");
327
+ assertAnchorEqual(request?.contract, "kungfu-buildchain-explicit-publish-anchor-request/v1", "anchor request contract");
328
+ assertAnchorEqual(Boolean(request?.assignmentId), true, "anchor request Assignment");
329
+ assertAnchorEqual(/^native:[0-9a-f-]+$/u.test(String(request?.attemptId || "")), true, "anchor request Attempt");
330
+ assertAnchorEqual(request?.transactionId, expectedTransactionId, "anchor request transaction");
331
+ assertAnchorEqual(String(provenanceRun?.id || ""), String(request?.supersededCandidate?.workflowRunId || ""), "superseded candidate run");
332
+ assertAnchorEqual(provenancePassport?.workflow?.runId, String(request?.supersededCandidate?.workflowRunId || ""), "superseded candidate Passport run");
333
+ assertAnchorEqual(request?.source?.sha, passport?.source?.headSha, "anchor request source SHA");
334
+ assertAnchorEqual(request?.source?.tree, passport?.source?.treeHash, "anchor request source tree");
335
+ assertAnchorEqual(request?.supersededCandidate?.sha, provenancePassport?.source?.headSha, "superseded candidate source SHA");
336
+ assertAnchorEqual(request?.supersededCandidate?.root, `sha256:${provenancePassport?.candidateHash || ""}`, "superseded candidate root");
337
+ assertAnchorEqual(request?.runtime?.sha, passport?.buildchain?.sha, "anchor request runtime");
338
+ const provenanceWorkflowPath = String(provenanceWorkflow?.path || provenanceRun?.path || "").split("@")[0].replace(/^\.github\/workflows\//, "");
339
+ assertEqual(provenanceWorkflowPath, workflowFile, "workflow-mismatch", "superseded candidate workflow file", "Use the exact trusted original candidate workflow.");
340
+ assertEqual(String(provenanceWorkflow?.name || provenanceRun?.name || ""), expectedWorkflowName, "workflow-mismatch", "superseded candidate workflow name", "Use the exact trusted original candidate workflow.");
341
+ if (provenanceWorkflow?.state && provenanceWorkflow.state !== "active") fail("untrusted-build-run", `superseded candidate workflow is not active: ${provenanceWorkflow.state}`, "Restore or select an allowed active Build workflow.");
342
+ const validation = validateReleaseCandidatePassport({ passport: provenancePassport, repository, targetChannel: normalizedRef(targetRef).split("/")[0], buildSummary: anchorProvenance.buildSummary });
343
+ if (!validation.ok) fail("anchored-provenance-invalid", `superseded candidate Passport validation failed: ${validation.errors.join("; ")}`, "Preserve both candidate runs and repair the explicit anchor provenance before retrying.");
344
+ return { provenanceRun, provenanceWorkflow, provenancePassport };
345
+ }
346
+
311
347
  function validateCandidateProvenance({
312
348
  candidateRepository,
313
349
  targetRepository,
@@ -319,26 +355,33 @@ function validateCandidateProvenance({
319
355
  workflow,
320
356
  pullRequest,
321
357
  ancestry,
358
+ passport,
359
+ anchorProvenance,
360
+ expectedTransactionId,
322
361
  }) {
323
362
  const repository = required(candidateRepository, "candidateRepository");
324
363
  assertEqual(repository, required(targetRepository, "targetRepository"), "repository-mismatch", "candidate repository", "Dispatch recovery in the repository that created the candidate.");
325
364
  assertEqual(String(run?.id || ""), required(expectedRunId, "expectedRunId"), "run-mismatch", "candidate run ID", "Select the exact successful candidate run.");
326
365
  assertEqual(run?.repository, repository, "repository-mismatch", "run repository", "Use a candidate run from the target repository.");
327
366
  assertEqual(run?.headRepository, repository, "provenance-insufficient", "run head repository", "Fork candidates are not recoverable; create a same-repository candidate.");
328
- if (run?.status !== "completed" || run?.conclusion !== "success" || run?.event !== "pull_request") {
329
- fail("untrusted-build-run", "candidate run must be a successful completed pull_request build", "Choose a successful allowed Build workflow run.");
330
- }
331
367
  const workflowFile = required(expectedWorkflowFile, "expectedWorkflowFile").replace(/^\.github\/workflows\//, "");
332
368
  const actualWorkflowPath = String(workflow?.path || run?.path || "").split("@")[0].replace(/^\.github\/workflows\//, "");
333
369
  assertEqual(actualWorkflowPath, workflowFile, "workflow-mismatch", "candidate workflow file", "Select a run from the documented trusted Build workflow.");
334
370
  assertEqual(String(workflow?.name || run?.name || ""), required(expectedWorkflowName, "expectedWorkflowName"), "workflow-mismatch", "candidate workflow name", "Select a run from the documented trusted Build workflow.");
335
371
  if (workflow?.state && workflow.state !== "active") fail("untrusted-build-run", `candidate workflow is not active: ${workflow.state}`, "Restore or select an allowed active Build workflow.");
372
+ const anchor = anchorProvenance ? validateAnchorProvenance({ run, passport, anchorProvenance, expectedTransactionId, workflowFile, expectedWorkflowName: required(expectedWorkflowName, "expectedWorkflowName"), repository, targetRef }) : undefined;
373
+ const provenanceRun = anchor?.provenanceRun || run;
374
+ if (provenanceRun?.status !== "completed" || provenanceRun?.conclusion !== "success" || provenanceRun?.event !== "pull_request") {
375
+ fail("untrusted-build-run", "candidate provenance run must be a successful completed pull_request build", "Choose a successful allowed Build workflow run.");
376
+ }
377
+ assertEqual(provenanceRun?.repository, repository, "repository-mismatch", "provenance run repository", "Use a candidate run from the target repository.");
378
+ assertEqual(provenanceRun?.headRepository, repository, "provenance-insufficient", "provenance run head repository", "Fork candidates are not recoverable; create a same-repository candidate.");
336
379
  if (!TRUSTED_ASSOCIATIONS.has(String(pullRequest?.authorAssociation || ""))) fail("permission-evidence-insufficient", "candidate PR author lacks trusted repository association evidence", "Have a repository member create or approve a same-repository candidate PR.");
337
380
  assertEqual(pullRequest?.headRepository, repository, "provenance-insufficient", "candidate PR head repository", "Fork candidates are outside the recovery permission boundary.");
338
381
  if (!pullRequest?.merged) fail("pr-identity-invalid", "candidate PR is not merged", "Use a merged channel candidate PR.");
339
382
  assertEqual(normalizedRef(pullRequest?.baseRef), normalizedRef(targetRef), "target-mismatch", "candidate PR target ref", "Use the original release channel target.");
340
- const boundByNumber = Array.isArray(run?.pullRequestNumbers) && run.pullRequestNumbers.includes(Number(pullRequest?.number));
341
- const boundByHead = run?.headSha === pullRequest?.headSha && normalizedRef(run?.headBranch) === normalizedRef(pullRequest?.headRef);
383
+ const boundByNumber = Array.isArray(provenanceRun?.pullRequestNumbers) && provenanceRun.pullRequestNumbers.includes(Number(pullRequest?.number));
384
+ const boundByHead = provenanceRun?.headSha === pullRequest?.headSha && normalizedRef(provenanceRun?.headBranch) === normalizedRef(pullRequest?.headRef);
342
385
  if (!boundByNumber && !boundByHead) fail("pr-identity-invalid", "candidate run is not bound to the merged candidate PR", "Select the exact PR-stage Build run.");
343
386
  if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical") fail("ancestry-invalid", "candidate merge is not an ancestor of the promotion SHA", "Promote a descendant of the verified merged candidate identity.");
344
387
  return { repository, workflowFile };
@@ -354,12 +397,13 @@ function validateCandidateIdentity({
354
397
  currentToolingSha,
355
398
  passport,
356
399
  buildSummary,
400
+ anchorProvenance,
357
401
  }) {
358
402
  const sha = exactSha(targetSha, "targetSha");
359
403
  const tree = exactSha(targetTree, "targetTree");
360
404
  const runtimeSha = exactSha(expectedRuntimeSha, "expectedRuntimeSha");
361
405
  const toolingSha = exactSha(currentToolingSha, "currentToolingSha");
362
- const validation = validateReleaseCandidatePassport({ passport, repository, targetChannel: channel, buildSummary });
406
+ const validation = validateReleaseCandidatePassport({ passport, repository, targetChannel: anchorProvenance ? "" : channel, buildSummary });
363
407
  if (!validation.ok) fail("passport-invalid", `Release Candidate Passport validation failed: ${validation.errors.join("; ")}`, "Preserve the candidate evidence and explicitly create a new candidate after fixing the producer.");
364
408
  const expectedPassportHash = sha256Json({
365
409
  repository: passport.repository,
@@ -369,6 +413,7 @@ function validateCandidateIdentity({
369
413
  buildchain: passport.buildchain,
370
414
  ...(passport.gateProfileEvidence ? { gateProfileEvidence: passport.gateProfileEvidence } : {}),
371
415
  ...(passport.familyEvidence ? { familyEvidence: passport.familyEvidence } : {}),
416
+ ...(passport.consumerPolicy ? { consumerPolicy: passport.consumerPolicy } : {}),
372
417
  ...(passport.controllerReceipts ? { controllerReceipts: passport.controllerReceipts } : {}),
373
418
  });
374
419
  if (passport.candidateHash !== expectedPassportHash) fail("candidate-root-mismatch", "Release Candidate Passport candidate hash does not match its content", "Preserve the run evidence and explicitly create a new candidate after fixing the producer.");
@@ -378,7 +423,7 @@ function validateCandidateIdentity({
378
423
  return { sha, tree, runtimeSha, toolingSha };
379
424
  }
380
425
 
381
- function validateRecoveryTransaction({ existingTransaction, expectedTransactionId, repository, version, passport, sha, targetRef, candidateRoot }) {
426
+ function validateRecoveryTransaction({ existingTransaction, expectedTransactionId, repository, version, passport, channel, sha, targetRef, candidateRoot }) {
382
427
  const actualTransactionId = String(existingTransaction?.id || "");
383
428
  if (expectedTransactionId && !actualTransactionId) fail("transaction-identity-conflict", `expected transaction ${expectedTransactionId} does not exist`, "Remove the stale transaction identity only if no durable transaction was ever sealed; otherwise preserve evidence and enter repair_required.");
384
429
  if (expectedTransactionId && expectedTransactionId !== actualTransactionId) fail("transaction-identity-conflict", `existing transaction ${actualTransactionId} conflicts with expected ${expectedTransactionId}`, "Enter repair_required and inspect the durable transaction before any retry.");
@@ -389,7 +434,7 @@ function validateRecoveryTransaction({ existingTransaction, expectedTransactionI
389
434
  assertEqual(normalizedRef(existingTransaction.target_ref), normalizedRef(targetRef), "transaction-identity-conflict", "durable transaction target ref", "Enter repair_required; never retarget a sealed transaction.");
390
435
  assertEqual(existingTransaction.source_sha, sha, "transaction-identity-conflict", "durable transaction source SHA", "Resume with the transaction's exact promotion SHA or enter repair_required.");
391
436
  assertEqual(existingTransaction.version, version, "transaction-identity-conflict", "durable transaction version", "Enter repair_required; never change a sealed publication version.");
392
- assertEqual(existingTransaction.channel, passport.target.channel, "transaction-identity-conflict", "durable transaction channel", "Enter repair_required; never move a transaction between channels.");
437
+ assertEqual(existingTransaction.channel, channel || passport.target.channel, "transaction-identity-conflict", "durable transaction channel", "Enter repair_required; never move a transaction between channels.");
393
438
  }
394
439
  if (existingTransaction?.candidateRoot && existingTransaction.candidateRoot !== candidateRoot) fail("transaction-identity-conflict", "existing transaction is sealed to a different candidate root", "Enter repair_required; never reuse the conflicting transaction.");
395
440
  return actualTransactionId;
@@ -415,6 +460,7 @@ export function verifyReleaseCandidateRecovery({
415
460
  workflow,
416
461
  pullRequest,
417
462
  ancestry,
463
+ anchorProvenance = undefined,
418
464
  passport,
419
465
  buildSummary,
420
466
  controllerReceipts = [],
@@ -430,10 +476,11 @@ export function verifyReleaseCandidateRecovery({
430
476
  const { repository, workflowFile } = validateCandidateProvenance({
431
477
  candidateRepository, targetRepository, expectedRunId, expectedWorkflowFile,
432
478
  expectedWorkflowName, targetRef, run, workflow, pullRequest, ancestry,
479
+ passport, anchorProvenance, expectedTransactionId,
433
480
  });
434
481
  const { sha, tree, runtimeSha, toolingSha } = validateCandidateIdentity({
435
482
  repository, channel, targetSha, targetTree, expectedSourceTree,
436
- expectedRuntimeSha, currentToolingSha, passport, buildSummary,
483
+ expectedRuntimeSha, currentToolingSha, passport, buildSummary, anchorProvenance,
437
484
  });
438
485
 
439
486
  const recoveredArtifacts = artifacts.map(normalizeArtifact).sort((left, right) => left.name.localeCompare(right.name));
@@ -478,7 +525,7 @@ export function verifyReleaseCandidateRecovery({
478
525
 
479
526
  const version = required(publicationVersion || passport.target?.version, "publicationVersion");
480
527
  const actualTransactionId = validateRecoveryTransaction({
481
- existingTransaction, expectedTransactionId, repository, version, passport, sha, targetRef, candidateRoot,
528
+ existingTransaction, expectedTransactionId, repository, version, passport, channel, sha, targetRef, candidateRoot,
482
529
  });
483
530
 
484
531
  const receipt = {
@@ -496,9 +543,17 @@ export function verifyReleaseCandidateRecovery({
496
543
  sourceSha: passport.source.headSha,
497
544
  mergeSha: String(pullRequest.mergeSha || ""),
498
545
  tree: passport.source.treeHash,
546
+ ...(anchorProvenance ? {
547
+ provenance: "anchored-rematerialization",
548
+ supersededCandidate: {
549
+ runId: String(anchorProvenance.run.id),
550
+ candidateRoot: String(anchorProvenance.request.supersededCandidate.root),
551
+ sourceSha: String(anchorProvenance.request.supersededCandidate.sha),
552
+ },
553
+ } : {}),
499
554
  },
500
555
  target: {
501
- channel: passport.target.channel,
556
+ channel,
502
557
  ref: normalizedRef(targetRef),
503
558
  sha,
504
559
  observedRefSha: exactSha(targetRefSha, "targetRefSha"),
@@ -206,7 +206,7 @@ function validateConsumerPolicyEvidence(passport, check) {
206
206
  receipt: passport.consumerPolicy.receipt,
207
207
  receiptRoot: passport.consumerPolicy.receiptRoot,
208
208
  repository: passport.repository,
209
- sourceSha: passport.source?.headSha,
209
+ sourceSha: passport.target?.ref === "publish-gate/anchor" ? passport.consumerPolicy.receipt?.caller?.sourceSha : passport.source?.headSha,
210
210
  resolvedRuntimeSha: passport.buildchain?.sha,
211
211
  });
212
212
  for (const failure of verification.failures) {
@@ -403,7 +403,7 @@ export function createReleaseCandidatePassport({
403
403
  };
404
404
  const normalizedConsumerPolicy = normalizeConsumerPolicyEvidence(consumerPolicyReceipt, {
405
405
  repository: repository || normalizedSummary.git?.repository || "",
406
- sourceSha,
406
+ sourceSha: normalizedSummary.publishSource?.ref === "publish-gate/anchor" ? (consumerPolicyReceipt?.receipt || consumerPolicyReceipt)?.caller?.sourceSha : sourceSha,
407
407
  runtimeSha: resolvedBuildchain.sha,
408
408
  });
409
409
  if (isV4BuildchainRuntime(resolvedBuildchain) && !normalizedConsumerPolicy) {
@@ -38,6 +38,7 @@ import {
38
38
  isV4PromotionRouting,
39
39
  releasePassportCertificationVerificationOptions,
40
40
  requireV4ConsumerPolicyCertification,
41
+ resolveV4ConsumerPolicyCertificationIdentity,
41
42
  } from "./v4-floating-consumer-release-passport.js";
42
43
  import {
43
44
  verifyV4RuntimeAuthorizationReceipt,
@@ -1112,17 +1113,25 @@ function normalizePromotionEvidence({
1112
1113
  release = {},
1113
1114
  v4ConsumerPolicyCertification: certification,
1114
1115
  v4ConsumerPolicyCertificationRoot: certificationRoot,
1116
+ v4RuntimeResumeEvidence,
1115
1117
  repository,
1116
1118
  sourceSha,
1117
1119
  cwd,
1118
1120
  }) {
1119
1121
  const routing = normalizePromotionRouting(release.promotionRouting);
1122
+ const identity = resolveV4ConsumerPolicyCertificationIdentity({
1123
+ release,
1124
+ routing,
1125
+ runtimeResume: v4RuntimeResumeEvidence,
1126
+ sourceSha,
1127
+ });
1120
1128
  return {
1121
1129
  routing,
1122
1130
  consumerPolicy: requireV4ConsumerPolicyCertification({
1123
1131
  value: certification,
1124
1132
  repository,
1125
- sourceSha,
1133
+ sourceSha: identity.sourceSha,
1134
+ runtimeSha: identity.runtimeSha,
1126
1135
  routing,
1127
1136
  cwd,
1128
1137
  certificationRoot,
@@ -100,7 +100,9 @@ export function validateV4DeliveryWarrantReadQualification(
100
100
  }
101
101
 
102
102
  export function projectV3QueueToV4ReadState(queueInput) {
103
- const queue = normalizeDevDeliveryQueue(queueInput);
103
+ const queue = normalizeDevDeliveryQueue(queueInput, {
104
+ allowLegacyV3Readback: true,
105
+ });
104
106
  return {
105
107
  schema: STATE_CONTRACT,
106
108
  generation: queue.generation,
@@ -219,7 +221,9 @@ export async function runV4DeliveryWarrantReadCandidate(
219
221
  });
220
222
  if (typeof retain !== "function")
221
223
  fail("retention-required", "v4 read evidence retention is required");
222
- const queue = normalizeDevDeliveryQueue(queueInput);
224
+ const queue = normalizeDevDeliveryQueue(queueInput, {
225
+ allowLegacyV3Readback: true,
226
+ });
223
227
  const state = projectV3QueueToV4ReadState(queue);
224
228
  const request = createV4DeliveryWarrantReadRequest(state, {
225
229
  requestId,
@@ -235,7 +239,10 @@ export async function runV4DeliveryWarrantReadCandidate(
235
239
  );
236
240
  }
237
241
  const projection = validateProjection(response, request, state);
238
- const observation = observeDevDeliveryQueue(queue, { now: observedAt });
242
+ const observation = observeDevDeliveryQueue(queue, {
243
+ now: observedAt,
244
+ allowLegacyV3Readback: true,
245
+ });
239
246
  const evidence = {
240
247
  schema: V4_DELIVERY_WARRANT_READ_EVIDENCE_CONTRACT,
241
248
  authority: "typescript-v3",
@@ -8,6 +8,12 @@ export const V4_FLOATING_CONSUMER_CERTIFICATION =
8
8
  "kungfu-buildchain-v4-floating-consumer-certification/v1";
9
9
 
10
10
  const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
11
+ const ALPHA_RECOVERY_BOOTSTRAP = Object.freeze({
12
+ repository: "kungfu-systems/buildchain",
13
+ sourcePath: ".github/workflows/buildchain-ref-promotion-recovery.yml",
14
+ workflow: ".github/workflows/.release-candidate-promote.yml",
15
+ selector: "alpha/v4/v4.0",
16
+ });
11
17
 
12
18
  function stableJson(value) {
13
19
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -109,15 +115,23 @@ function verifyAuthorityFields(value, expected, check, label) {
109
115
  }
110
116
 
111
117
  function verifyFloatingSelector(value, check, label) {
118
+ const bootstrap =
119
+ value?.caller?.repository === ALPHA_RECOVERY_BOOTSTRAP.repository &&
120
+ value?.invocation?.sourcePath === ALPHA_RECOVERY_BOOTSTRAP.sourcePath &&
121
+ value?.invocation?.workflow === ALPHA_RECOVERY_BOOTSTRAP.workflow &&
122
+ value?.invocation?.visibleSelector === ALPHA_RECOVERY_BOOTSTRAP.selector &&
123
+ value?.invocation?.selectorClass === "protected-bootstrap" &&
124
+ value?.invocation?.channel === "alpha";
112
125
  check(
113
- value?.invocation?.selectorClass === "floating",
126
+ value?.invocation?.selectorClass === "floating" || bootstrap,
114
127
  "selector-class-invalid",
115
- `${label} must bind a floating selector`,
128
+ `${label} must bind a floating selector or the bounded protected alpha recovery bootstrap`,
116
129
  );
117
130
  check(
118
- ["v4", "v4-alpha"].includes(value?.invocation?.visibleSelector),
131
+ ["v4", "v4-alpha"].includes(value?.invocation?.visibleSelector) ||
132
+ bootstrap,
119
133
  "selector-invalid",
120
- `${label} selector must be v4 or v4-alpha`,
134
+ `${label} selector must be v4, v4-alpha, or the bounded protected alpha recovery bootstrap`,
121
135
  );
122
136
  }
123
137
 
@@ -26,6 +26,11 @@ const EXACT_SHA = /^[0-9a-f]{40}$/u;
26
26
  const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
27
27
  const BUILDCHAIN_REPOSITORY = "kungfu-systems/buildchain";
28
28
  const CHANNELS = Object.freeze({ v4: "stable", "v4-alpha": "alpha" });
29
+ const ALPHA_RECOVERY_BOOTSTRAP = Object.freeze({
30
+ selector: "alpha/v4/v4.0",
31
+ sourcePath: ".github/workflows/buildchain-ref-promotion-recovery.yml",
32
+ workflow: ".github/workflows/.release-candidate-promote.yml",
33
+ });
29
34
  const DEFAULT_STABLE_LOCK_PATH = ".buildchain/contract-lock.json";
30
35
  const DEFAULT_ALPHA_LOCK_PATH = ".buildchain/alpha-contract-lock.json";
31
36
  const TRANSIENT_ACTION =
@@ -277,7 +282,7 @@ function readContractLock(root, relative, expectedRef, label, failures) {
277
282
  }
278
283
  }
279
284
 
280
- function classifyBuildchainUses(records, failures) {
285
+ function classifyBuildchainUses(records, failures, { repository } = {}) {
281
286
  const uses = [];
282
287
  for (const record of records) {
283
288
  const parsed = coordinate(record.uses);
@@ -286,15 +291,27 @@ function classifyBuildchainUses(records, failures) {
286
291
  parsed.selector === "v4" ||
287
292
  parsed.selector === "v4-alpha" ||
288
293
  /^v4(?:[./-]|$)/u.test(parsed.selector) ||
294
+ parsed.selector === ALPHA_RECOVERY_BOOTSTRAP.selector ||
289
295
  EXACT_SHA.test(parsed.selector.toLowerCase()) ||
290
296
  parsed.selector.includes("${{");
291
297
  if (!v4Candidate) continue;
292
- const channel = CHANNELS[parsed.selector] || "";
298
+ const protectedBootstrap =
299
+ repository === BUILDCHAIN_REPOSITORY &&
300
+ normalizeWorkflowPath(parsed.path) ===
301
+ ALPHA_RECOVERY_BOOTSTRAP.workflow &&
302
+ record.sourcePath === ALPHA_RECOVERY_BOOTSTRAP.sourcePath &&
303
+ parsed.selector === ALPHA_RECOVERY_BOOTSTRAP.selector;
304
+ const channel =
305
+ CHANNELS[parsed.selector] || (protectedBootstrap ? "alpha" : "");
293
306
  uses.push({
294
307
  ...record,
295
308
  ...parsed,
296
309
  channel,
297
- selectorClass: channel ? "floating" : "rejected",
310
+ selectorClass: protectedBootstrap
311
+ ? "protected-bootstrap"
312
+ : channel
313
+ ? "floating"
314
+ : "rejected",
298
315
  });
299
316
  if (!channel) failures.push(selectorFailure(record, parsed.selector));
300
317
  }
@@ -320,8 +337,97 @@ function selectInvocation(uses, workflow, failures, source = "", channel = "") {
320
337
  return { expectedWorkflow, selected: invocations[0] };
321
338
  }
322
339
 
340
+ function scanInvocationSource({
341
+ sourceRoot,
342
+ invocationRoot,
343
+ sourceScan,
344
+ sourceUses,
345
+ failures,
346
+ repository,
347
+ }) {
348
+ if (invocationRoot === sourceRoot)
349
+ return { invocationScan: sourceScan, invocationUses: sourceUses };
350
+ try {
351
+ const invocationScan = enumerateUses(invocationRoot);
352
+ return {
353
+ invocationScan,
354
+ invocationUses: classifyBuildchainUses(invocationScan.records, failures, {
355
+ repository,
356
+ }),
357
+ };
358
+ } catch (error) {
359
+ failures.push({
360
+ code: "invocation-source-scan-failed",
361
+ message: error.message,
362
+ });
363
+ return {
364
+ invocationScan: { files: [], records: [] },
365
+ invocationUses: [],
366
+ };
367
+ }
368
+ }
369
+
370
+ function sourceScanRoot({
371
+ sourceRoot,
372
+ invocationRoot,
373
+ sourceScan,
374
+ invocationScan,
375
+ invocationUses,
376
+ }) {
377
+ const material =
378
+ invocationRoot === sourceRoot
379
+ ? sourceScan
380
+ : {
381
+ source: sourceScan,
382
+ invocation: {
383
+ files: invocationScan.files,
384
+ invocations: invocationUses,
385
+ },
386
+ };
387
+ return sha256(stableJson(material));
388
+ }
389
+
390
+ function verifySelectedLock({
391
+ selected,
392
+ stable,
393
+ alpha,
394
+ workflowSha,
395
+ failures,
396
+ }) {
397
+ const selectedLock = selected?.channel === "alpha" ? alpha : stable;
398
+ if (!selected?.channel || !selectedLock) return;
399
+ const authorityRef =
400
+ selected.selectorClass === "protected-bootstrap"
401
+ ? "v4-alpha"
402
+ : selected.selector;
403
+ try {
404
+ const compatibility = evaluateBuildchainContractLock({
405
+ lock: selectedLock,
406
+ current: createBuildchainContractWorld({ root: defaultRuntimeRoot() }),
407
+ runtimeRef: authorityRef,
408
+ runtimeSha: workflowSha,
409
+ runtimeClass: selected.channel,
410
+ workflowShellRef: authorityRef,
411
+ expectedChannel: selected.channel,
412
+ expectedMajor: "v4",
413
+ });
414
+ if (!compatibility.ok) {
415
+ failures.push({
416
+ code: "selected-lock-runtime-incompatible",
417
+ message: `${selected.channel} contract lock rejects resolved workflow shell ${workflowSha}: ${compatibility.reasons?.join("; ") || compatibility.status}`,
418
+ });
419
+ }
420
+ } catch (error) {
421
+ failures.push({
422
+ code: "selected-lock-contract-world-invalid",
423
+ message: `cannot evaluate ${selected.channel} contract lock against the resolved workflow shell: ${error.message}`,
424
+ });
425
+ }
426
+ }
427
+
323
428
  export function scanV4FloatingConsumerPolicy({
324
429
  root = process.cwd(),
430
+ invocationRoot = root,
325
431
  repository = "",
326
432
  sourceSha = "",
327
433
  invokedWorkflow = "",
@@ -335,6 +441,7 @@ export function scanV4FloatingConsumerPolicy({
335
441
  scannerRoot = "",
336
442
  } = {}) {
337
443
  const resolvedRoot = path.resolve(root);
444
+ const resolvedInvocationRoot = path.resolve(invocationRoot);
338
445
  if (!policy || policy.contract !== V4_FLOATING_CONSUMER_POLICY) {
339
446
  throw new Error(`policy must use ${V4_FLOATING_CONSUMER_POLICY}`);
340
447
  }
@@ -374,43 +481,34 @@ export function scanV4FloatingConsumerPolicy({
374
481
  } catch (error) {
375
482
  failures.push({ code: "source-scan-failed", message: error.message });
376
483
  }
377
- const buildchainUses = classifyBuildchainUses(scanned.records, failures);
484
+ const buildchainUses = classifyBuildchainUses(scanned.records, failures, {
485
+ repository,
486
+ });
487
+ const { invocationScan, invocationUses } = scanInvocationSource({
488
+ sourceRoot: resolvedRoot,
489
+ invocationRoot: resolvedInvocationRoot,
490
+ sourceScan: scanned,
491
+ sourceUses: buildchainUses,
492
+ failures,
493
+ repository,
494
+ });
378
495
  const sourcePath = String(invocationSourcePath || "")
379
496
  .split("@")[0]
380
497
  .replace(`${repository}/`, "");
381
498
  const { expectedWorkflow, selected } = selectInvocation(
382
- buildchainUses,
499
+ invocationUses,
383
500
  invokedWorkflow,
384
501
  failures,
385
502
  sourcePath,
386
503
  expectedInvocationChannel,
387
504
  );
388
- const selectedLock = selected?.channel === "alpha" ? alpha : stable;
389
- if (selected?.channel && selectedLock) {
390
- try {
391
- const compatibility = evaluateBuildchainContractLock({
392
- lock: selectedLock,
393
- current: createBuildchainContractWorld({ root: defaultRuntimeRoot() }),
394
- runtimeRef: selected.selector,
395
- runtimeSha: normalizedWorkflowSha,
396
- runtimeClass: selected.channel,
397
- workflowShellRef: selected.selector,
398
- expectedChannel: selected.channel,
399
- expectedMajor: "v4",
400
- });
401
- if (!compatibility.ok) {
402
- failures.push({
403
- code: "selected-lock-runtime-incompatible",
404
- message: `${selected.channel} contract lock rejects resolved workflow shell ${normalizedWorkflowSha}: ${compatibility.reasons?.join("; ") || compatibility.status}`,
405
- });
406
- }
407
- } catch (error) {
408
- failures.push({
409
- code: "selected-lock-contract-world-invalid",
410
- message: `cannot evaluate ${selected.channel} contract lock against the resolved workflow shell: ${error.message}`,
411
- });
412
- }
413
- }
505
+ verifySelectedLock({
506
+ selected,
507
+ stable,
508
+ alpha,
509
+ workflowSha: normalizedWorkflowSha,
510
+ failures,
511
+ });
414
512
 
415
513
  const policyRoot = sha256(stableJson(policy));
416
514
  const resolvedScannerRoot = scannerRoot || policyRoot;
@@ -424,9 +522,14 @@ export function scanV4FloatingConsumerPolicy({
424
522
  stable: stable ? sha256(stableJson(stable)) : "",
425
523
  alpha: alpha ? sha256(stableJson(alpha)) : "",
426
524
  };
427
- const scanRoot = sha256(
428
- stableJson({ files: scanned.files, invocations: buildchainUses }),
429
- );
525
+ const sourceScan = { files: scanned.files, invocations: buildchainUses };
526
+ const scanRoot = sourceScanRoot({
527
+ sourceRoot: resolvedRoot,
528
+ invocationRoot: resolvedInvocationRoot,
529
+ sourceScan,
530
+ invocationScan,
531
+ invocationUses,
532
+ });
430
533
  const receipt = {
431
534
  schema: V4_FLOATING_CONSUMER_RECEIPT,
432
535
  status: failures.length ? "failed" : "passed",
@@ -459,7 +562,7 @@ export function scanV4FloatingConsumerPolicy({
459
562
  ok: failures.length === 0,
460
563
  failures,
461
564
  scannedFiles: scanned.files,
462
- invocations: buildchainUses,
565
+ invocations: invocationUses,
463
566
  receipt,
464
567
  receiptRoot,
465
568
  };
@@ -77,10 +77,31 @@ function normalizeCertification(value, expected) {
77
77
  return { certificationRoot: verification.certificationRoot, certification };
78
78
  }
79
79
 
80
+ export function resolveV4ConsumerPolicyCertificationIdentity({
81
+ release = {},
82
+ routing = {},
83
+ runtimeResume = undefined,
84
+ sourceSha = "",
85
+ } = {}) {
86
+ const builtSourceSha =
87
+ release?.builtSourceSha || release?.built_source_sha || "";
88
+ return {
89
+ sourceSha:
90
+ release?.treeEquivalent === true && builtSourceSha
91
+ ? builtSourceSha
92
+ : sourceSha,
93
+ runtimeSha:
94
+ runtimeResume?.lineage?.attempts?.build?.runtimeSha ||
95
+ routing?.runtime?.resolvedSha ||
96
+ "",
97
+ };
98
+ }
99
+
80
100
  export function requireV4ConsumerPolicyCertification({
81
101
  value,
82
102
  repository,
83
103
  sourceSha,
104
+ runtimeSha = "",
84
105
  routing,
85
106
  cwd,
86
107
  certificationRoot,
@@ -100,7 +121,7 @@ export function requireV4ConsumerPolicyCertification({
100
121
  return normalizeCertification(value, {
101
122
  repository,
102
123
  sourceSha,
103
- runtimeSha: routing?.runtime?.resolvedSha || "",
124
+ runtimeSha: runtimeSha || routing?.runtime?.resolvedSha || "",
104
125
  certificationRoot,
105
126
  ...(v4Routing ? resolveV4FloatingConsumerCallerLocks(value, cwd) : {}),
106
127
  });
@@ -112,12 +133,18 @@ export function releasePassportCertificationVerificationOptions({
112
133
  routing,
113
134
  }) {
114
135
  const authority = evidence?.certification?.authority || {};
136
+ const identity = resolveV4ConsumerPolicyCertificationIdentity({
137
+ release: passport?.release,
138
+ routing,
139
+ runtimeResume: passport?.v4RuntimeResume,
140
+ sourceSha: passport?.release?.sourceSha || "",
141
+ });
115
142
  return {
116
143
  certification: evidence?.certification,
117
144
  certificationRoot: evidence?.certificationRoot,
118
145
  repository: passport?.product?.repository || "",
119
- sourceSha: passport?.release?.sourceSha || "",
120
- resolvedRuntimeSha: routing?.runtime?.resolvedSha || "",
146
+ sourceSha: identity.sourceSha,
147
+ resolvedRuntimeSha: identity.runtimeSha,
121
148
  stableLockRoot: authority.contractLocks?.stable?.root || "",
122
149
  alphaLockRoot: authority.contractLocks?.alpha?.root || "",
123
150
  stableLockPath: authority.contractLocks?.stable?.path || "",
@@ -67,8 +67,7 @@ function directEntries(lines, parentLine) {
67
67
  const trimmed = line.trim();
68
68
  const indent = indentation(line);
69
69
  if (trimmed && indent <= parentIndent) break;
70
- if (!trimmed || trimmed.startsWith("#") || indent !== childIndent)
71
- continue;
70
+ if (!trimmed || trimmed.startsWith("#") || indent !== childIndent) continue;
72
71
  const match = line.match(/^\s*([A-Za-z0-9_.-]+):(?:\s*(.*))?$/);
73
72
  if (match) {
74
73
  entries.push({
@@ -263,6 +262,28 @@ export function parseWorkflowCallJobs(text) {
263
262
  .filter((job) => job.uses);
264
263
  }
265
264
 
265
+ export function parseWorkflowJobs(text) {
266
+ const lines = String(text || "").split(/\r?\n/);
267
+ const jobs = topLevelEntry(lines, "jobs");
268
+ if (!jobs) return [];
269
+ return directEntries(lines, jobs.line)
270
+ .map((job) => {
271
+ const properties = directEntries(lines, job.line);
272
+ const property = (name) =>
273
+ properties.find((entry) => entry.key === name) || null;
274
+ return {
275
+ id: job.key,
276
+ name: unquote(property("name")?.value || job.key),
277
+ uses: unquote(property("uses")?.value || ""),
278
+ runsOn: unquote(property("runs-on")?.value || ""),
279
+ timeoutMinutes: property("timeout-minutes")
280
+ ? scalar(property("timeout-minutes").value)
281
+ : null,
282
+ };
283
+ })
284
+ .sort((left, right) => compareCodeUnits(left.id, right.id));
285
+ }
286
+
266
287
  function blockScalarIndent(lines, lineIndex, parentIndent) {
267
288
  for (let index = lineIndex + 1; index < lines.length; index += 1) {
268
289
  if (!lines[index].trim()) continue;
@@ -315,6 +336,7 @@ export function parseWorkflowDocument(text) {
315
336
  return {
316
337
  triggers: parseTriggers(String(text || "").split(/\r?\n/)),
317
338
  interface: parseReusableWorkflowInterface(text),
339
+ jobs: parseWorkflowJobs(text),
318
340
  callJobs: parseWorkflowCallJobs(text),
319
341
  };
320
342
  }