@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/architecture/agent-change-map.md +4 -4
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/maintainability-debt.json +41 -115
  4. package/architecture/maintainability-policy.json +12 -7
  5. package/architecture/release-tail-contract-inventory.json +4 -4
  6. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  7. package/architecture/v4-release-invocation-fixtures.json +119 -0
  8. package/architecture/v4-release-topology.json +238 -0
  9. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  10. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  11. package/contracts/v4-release-invocation-v1.schema.json +86 -0
  12. package/dist/site/buildchain-contract.json +9 -9
  13. package/dist/site/buildchain-site.json +7 -7
  14. package/dist/site/kfd-claims.json +7 -5
  15. package/dist/site/kfd-upstream-aggregate.json +1 -1
  16. package/dist/site/manual-registry.json +1 -1
  17. package/dist/site/node-api-registry.json +3 -3
  18. package/dist/site/page-registry.json +2 -2
  19. package/dist/site/public-surface-audit.json +17 -9
  20. package/dist/site/publication-authority-registry.json +2 -3
  21. package/dist/site/publication-registry.json +4 -4
  22. package/dist/site/site-manifest.json +5 -5
  23. package/dist/site/workflow-registry.json +19 -11
  24. package/docs/node-api-reference.md +3 -3
  25. package/package.json +2 -2
  26. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  27. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  28. package/packages/core/dev-delivery-warrant-state.js +12 -28
  29. package/packages/core/v4-canonical-contracts.js +8 -0
  30. package/packages/core/v4-floating-consumer-policy.js +4 -1
  31. package/packages/core/v4-release-invocation.js +356 -0
  32. package/scripts/audit-publication-control-plane.mjs +0 -1
  33. package/scripts/check-inventory.mjs +27 -59
  34. package/scripts/check-maintainability.mjs +13 -5
  35. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  36. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  37. package/scripts/check-v4-release-topology.mjs +237 -0
  38. package/scripts/dev-delivery-warrant.mjs +13 -4
  39. package/scripts/generate-channel-promotion-workflow.mjs +12 -54
  40. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  41. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  42. package/scripts/capture-package-release-propagation.mjs +0 -263
  43. package/scripts/publication-commit-evidence.mjs +0 -444
  44. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  45. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import {
9
+ parseWorkflowDocument,
10
+ parseYamlUses,
11
+ } from "../packages/core/workflow-yaml-contract.js";
12
+
13
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const ledgerPath = path.join(root, "architecture/v4-release-topology.json");
15
+ const releaseMarker =
16
+ /(?:release-candidate-promote\.yml|promote-buildchain-ref|v4-release-candidate-promote|release-tail-runtime)/u;
17
+
18
+ function read(relative) {
19
+ return fs.readFileSync(path.join(root, relative), "utf8");
20
+ }
21
+
22
+ function jobBlock(source, jobId) {
23
+ const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
24
+ return (
25
+ source.match(
26
+ new RegExp(
27
+ `^ ${escaped}:\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_.-]+:|(?![\\s\\S]))`,
28
+ "mu",
29
+ ),
30
+ )?.[1] || ""
31
+ );
32
+ }
33
+
34
+ function permission(block, name) {
35
+ return (
36
+ new RegExp(`^ ${name}:\\s*([^#\\n]+)`, "mu")
37
+ .exec(block)?.[1]
38
+ ?.trim() || null
39
+ );
40
+ }
41
+
42
+ function workflowSnapshot(relative) {
43
+ const source = read(relative);
44
+ const document = parseWorkflowDocument(source);
45
+ const jobs = document.jobs.map((job) => {
46
+ const block = jobBlock(source, job.id);
47
+ const uses = job.uses || null;
48
+ return {
49
+ id: job.id,
50
+ kind: uses ? "reusable-call" : "runner",
51
+ uses,
52
+ permissions: {
53
+ contents: permission(block, "contents"),
54
+ idToken: permission(block, "id-token"),
55
+ },
56
+ carriers: {
57
+ artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
58
+ artifactUpload: /uses:\s+actions\/upload-artifact@/u.test(block),
59
+ jobOutput: /GITHUB_OUTPUT/u.test(block),
60
+ },
61
+ mutationSignals: [
62
+ /contents:\s*write/u.test(block) && "contents-write",
63
+ /id-token:\s*write/u.test(block) && "oidc-write",
64
+ /(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
65
+ block,
66
+ ) && "promotion-runtime",
67
+ /(?:git push|npm publish|gh release (?:create|upload))/u.test(block) &&
68
+ "direct-publication-command",
69
+ ].filter(Boolean),
70
+ };
71
+ });
72
+ return {
73
+ path: relative,
74
+ triggers: document.triggers,
75
+ jobs,
76
+ reusableEdges: parseYamlUses(source)
77
+ .map(({ value }) => value)
78
+ .filter((value) => /\.github\/workflows\//u.test(value))
79
+ .sort(),
80
+ };
81
+ }
82
+
83
+ export function discoverV4ReleaseTopology(
84
+ workflowPaths,
85
+ semanticWorkflowPaths = workflowPaths,
86
+ ) {
87
+ const workflows = workflowPaths.map(workflowSnapshot);
88
+ const jobs = workflows.flatMap((workflow) => workflow.jobs);
89
+ const mutationJobs = jobs.filter((job) => job.mutationSignals.length > 0);
90
+ const semanticPaths = new Set(semanticWorkflowPaths);
91
+ const semanticJobs = workflows
92
+ .filter((workflow) => semanticPaths.has(workflow.path))
93
+ .flatMap((workflow) => workflow.jobs);
94
+ const semanticMutationJobs = semanticJobs.filter(
95
+ (job) => job.kind === "runner" && job.mutationSignals.length > 0,
96
+ );
97
+ return {
98
+ jobRecordFields: [
99
+ "id",
100
+ "kind",
101
+ "uses",
102
+ "contentsPermission",
103
+ "oidcPermission",
104
+ "carriers",
105
+ "mutationSignals",
106
+ ],
107
+ workflows: workflows.map((workflow) => ({
108
+ path: workflow.path,
109
+ triggers: workflow.triggers,
110
+ jobs: workflow.jobs.map((job) =>
111
+ [
112
+ job.id,
113
+ job.kind,
114
+ job.uses || "-",
115
+ job.permissions.contents || "-",
116
+ job.permissions.idToken || "-",
117
+ Object.entries(job.carriers)
118
+ .filter(([, present]) => present)
119
+ .map(([name]) => name)
120
+ .join(",") || "-",
121
+ job.mutationSignals.join(",") || "-",
122
+ ].join("|"),
123
+ ),
124
+ reusableEdges: workflow.reusableEdges,
125
+ })),
126
+ metrics: {
127
+ workflowCount: workflows.length,
128
+ jobCount: jobs.length,
129
+ reusableEdgeCount: workflows.reduce(
130
+ (count, workflow) => count + workflow.reusableEdges.length,
131
+ 0,
132
+ ),
133
+ mutationRelevantNodeCount: jobs.filter(
134
+ (job) =>
135
+ job.kind === "reusable-call" ||
136
+ job.mutationSignals.length > 0 ||
137
+ job.carriers.artifactDownload ||
138
+ job.carriers.artifactUpload,
139
+ ).length,
140
+ contentsWriteJobCount: mutationJobs.filter((job) =>
141
+ job.mutationSignals.includes("contents-write"),
142
+ ).length,
143
+ oidcWriteJobCount: mutationJobs.filter((job) =>
144
+ job.mutationSignals.includes("oidc-write"),
145
+ ).length,
146
+ },
147
+ semanticMetrics: {
148
+ workflowCount: semanticPaths.size,
149
+ mutationRelevantNodeCount: semanticJobs.filter(
150
+ (job) =>
151
+ job.kind === "reusable-call" ||
152
+ job.mutationSignals.length > 0 ||
153
+ job.carriers.artifactDownload ||
154
+ job.carriers.artifactUpload,
155
+ ).length,
156
+ contentsWriteJobCount: semanticMutationJobs.filter((job) =>
157
+ job.mutationSignals.includes("contents-write"),
158
+ ).length,
159
+ oidcWriteJobCount: semanticMutationJobs.filter((job) =>
160
+ job.mutationSignals.includes("oidc-write"),
161
+ ).length,
162
+ },
163
+ };
164
+ }
165
+
166
+ export function findUnknownV4ReleaseTopology(
167
+ workflowPaths,
168
+ allWorkflowPaths = fs
169
+ .readdirSync(path.join(root, ".github/workflows"))
170
+ .filter((name) => /\.ya?ml$/u.test(name))
171
+ .map((name) => `.github/workflows/${name}`),
172
+ readWorkflow = read,
173
+ ) {
174
+ const declared = new Set(workflowPaths);
175
+ return allWorkflowPaths
176
+ .filter((relative) => !declared.has(relative))
177
+ .filter((relative) => releaseMarker.test(readWorkflow(relative)))
178
+ .sort();
179
+ }
180
+
181
+ function assertClosedWorld(workflowPaths) {
182
+ const unknown = findUnknownV4ReleaseTopology(workflowPaths);
183
+ assert.deepEqual(
184
+ unknown,
185
+ [],
186
+ `unknown v4 release topology workflows: ${unknown.join(", ")}`,
187
+ );
188
+ }
189
+
190
+ export function checkV4ReleaseTopology() {
191
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
192
+ assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
193
+ assertClosedWorld(ledger.closedWorld.workflowPaths);
194
+ const actual = discoverV4ReleaseTopology(
195
+ ledger.closedWorld.workflowPaths,
196
+ ledger.semanticScope.workflowPaths,
197
+ );
198
+ assert.deepEqual(actual, ledger.observedTopology);
199
+ assert.equal(
200
+ ledger.targetBudgets.maximumMutationRelevantNodeCount,
201
+ Math.floor(ledger.baselineMetrics.semanticMutationRelevantNodeCount / 2),
202
+ );
203
+ if (ledger.enforcement === "converged") {
204
+ assert.ok(
205
+ actual.semanticMetrics.mutationRelevantNodeCount <=
206
+ ledger.targetBudgets.maximumMutationRelevantNodeCount,
207
+ );
208
+ assert.equal(actual.semanticMetrics.contentsWriteJobCount, 1);
209
+ assert.equal(actual.semanticMetrics.oidcWriteJobCount, 1);
210
+ const publisher = actual.workflows.find(
211
+ ({ path: workflowPath }) =>
212
+ workflowPath === ".github/workflows/.release-candidate-promote.yml",
213
+ );
214
+ assert.deepEqual(
215
+ publisher.jobs.map((job) => job.split("|", 1)[0]),
216
+ ["apply", "qualify", "settle"],
217
+ );
218
+ }
219
+ return actual;
220
+ }
221
+
222
+ if (process.argv.includes("--print")) {
223
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
224
+ process.stdout.write(
225
+ `${JSON.stringify(
226
+ discoverV4ReleaseTopology(
227
+ ledger.closedWorld.workflowPaths,
228
+ ledger.semanticScope.workflowPaths,
229
+ ),
230
+ null,
231
+ 2,
232
+ )}\n`,
233
+ );
234
+ } else {
235
+ checkV4ReleaseTopology();
236
+ process.stdout.write("v4 release topology: ok\n");
237
+ }
@@ -430,20 +430,29 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
430
430
  options.command,
431
431
  ),
432
432
  });
433
+ let concurrencyRecovery = null;
433
434
  if (
434
435
  options.expectedOldStateRoot &&
435
436
  loaded.queue.stateRoot !== options.expectedOldStateRoot
436
437
  ) {
437
- throw new Error(
438
- `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
439
- );
438
+ if (options.command !== "heartbeat") {
439
+ throw new Error(
440
+ `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
441
+ );
442
+ }
443
+ concurrencyRecovery = {
444
+ schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
445
+ action: "heartbeat-state-root-rebased",
446
+ requestedStateRoot: options.expectedOldStateRoot,
447
+ observedStateRoot: loaded.queue.stateRoot,
448
+ observedCommitSha: loaded.commitSha,
449
+ };
440
450
  }
441
451
  if (options.command === "observe") return observeQueue(loaded, options);
442
452
  const initialLoaded = loaded;
443
453
  let changed = transitionFor(options.command, loaded.queue, options);
444
454
  let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
445
455
  let write = null;
446
- let concurrencyRecovery = null;
447
456
  if (options.execute && mutates) {
448
457
  if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
449
458
  throw new Error(
@@ -74,7 +74,7 @@ function publicInputs(inputBlock) {
74
74
  function publicOutputs(outputBlock) {
75
75
  const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
76
76
  /^ value:.*$/m,
77
- ` value: \${{ jobs.alpha.outputs.${entry.name} || jobs.stable.outputs.${entry.name} }}`,
77
+ ` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
78
78
  ));
79
79
  const routed = [
80
80
  ["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
@@ -292,7 +292,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
292
292
  },
293
293
  };
294
294
  const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
295
- const stableRoute = validateWorkflowRoute("stable", routes.stable, `v${major}`);
295
+ validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
296
  const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
297
297
  const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
298
298
  const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
@@ -300,8 +300,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
300
300
  for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
301
301
  if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
302
302
  }
303
- const alphaForwarded = routedInputs(inputNames, alphaRoute, major);
304
- const stableForwarded = routedInputs(inputNames, stableRoute, major);
303
+ const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
305
304
  return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
306
305
  name: Release Candidate Promote
307
306
 
@@ -336,7 +335,7 @@ jobs:
336
335
  target-ref: \${{ steps.route.outputs.target-ref }}
337
336
  router-ref: \${{ steps.router.outputs.ref }}
338
337
  router-sha: \${{ steps.router.outputs.sha }}
339
- shell-ref: \${{ steps.route.outputs.shell-ref }}
338
+ shell-ref: \${{ steps.identities.outputs.shell-ref }}
340
339
  shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
341
340
  shell-sha: \${{ steps.identities.outputs.shell-sha }}
342
341
  runtime-ref: \${{ steps.route.outputs.runtime-ref }}
@@ -425,32 +424,17 @@ jobs:
425
424
  shell: bash
426
425
  env:
427
426
  GITHUB_TOKEN: \${{ github.token }}
428
- CHANNEL: \${{ steps.route.outputs.channel }}
429
- SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
430
- ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
431
- ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
432
- STABLE_SHELL_REF: ${stableRoute.logicalRef}
433
- STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
427
+ SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
434
428
  run: |
435
429
  set -euo pipefail
436
- if [[ "\${CHANNEL}" = "alpha" ]]; then
437
- expected_ref="\${ALPHA_SHELL_REF}"
438
- call_ref="\${ALPHA_SHELL_CALL_REF}"
439
- else
440
- expected_ref="\${STABLE_SHELL_REF}"
441
- call_ref="\${STABLE_SHELL_CALL_REF}"
442
- fi
443
- if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
444
- echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
445
- exit 1
446
- fi
447
430
  node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
448
431
  --repository "\${{ steps.router.outputs.repository }}" \\
449
432
  --router-ref "\${{ steps.router.outputs.ref }}" \\
450
433
  --router-sha "\${{ steps.router.outputs.sha }}" \\
451
434
  --shell-ref "\${SELECTED_SHELL_REF}" \\
452
- --shell-call-ref "\${call_ref}" \\
435
+ --shell-call-ref "\${SELECTED_SHELL_REF}" \\
453
436
  --runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
437
+ echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
454
438
 
455
439
  - name: Checkout selected promotion workflow shell
456
440
  uses: actions/checkout@v7.0.0
@@ -500,29 +484,21 @@ jobs:
500
484
  - name: Verify immutable promotion checkouts
501
485
  shell: bash
502
486
  env:
503
- CHANNEL: \${{ steps.route.outputs.channel }}
504
- ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
505
- STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
487
+ SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
506
488
  ROUTER_SHA: \${{ steps.router.outputs.sha }}
507
489
  SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
508
490
  RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
509
491
  run: |
510
492
  set -euo pipefail
511
- if [[ "\${CHANNEL}" = "alpha" ]]; then
512
- workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
513
- else
514
- workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
515
- fi
516
- test -f ".buildchain/shell/\${workflow_path}"
493
+ test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
517
494
  [[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
518
495
  [[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
519
496
  [[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
520
497
 
521
498
  ${consumerAdmissionJob()}
522
- alpha:
523
- name: Promote with alpha workflow shell
499
+ invoke:
500
+ name: Invoke the single v4 publisher adapter
524
501
  needs: [resolve-promotion, consumer-admission]
525
- if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
526
502
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
527
503
  permissions:
528
504
  actions: write
@@ -534,25 +510,7 @@ ${consumerAdmissionJob()}
534
510
  issues: write
535
511
  pull-requests: write
536
512
  with:
537
- ${alphaForwarded}
538
- secrets: inherit
539
-
540
- stable:
541
- name: Promote with stable workflow shell
542
- needs: [resolve-promotion, consumer-admission]
543
- if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
544
- uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
545
- permissions:
546
- actions: write
547
- artifact-metadata: write
548
- attestations: write
549
- checks: write
550
- contents: write
551
- id-token: write
552
- issues: write
553
- pull-requests: write
554
- with:
555
- ${stableForwarded}
513
+ ${invokeForwarded}
556
514
  secrets: inherit
557
515
  `;
558
516
  }
@@ -23,6 +23,9 @@ const PLATFORM_MARKERS = Object.freeze({
23
23
  "self-hosted": /\bself-hosted\b/iu,
24
24
  windows: /\bwindows\b/iu,
25
25
  });
26
+ const LIVE_V4_PROTECTED_BRANCH = "refs/heads/dev/v4/v4.0";
27
+ const LIVE_V4_PROTECTED_LINEAGE_REF = "refs/remotes/origin/dev/v4/v4.0";
28
+ const PROTECTED_LINEAGE_DEPTH = 256;
26
29
 
27
30
  export function sha256(value) {
28
31
  return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
@@ -47,6 +50,110 @@ export function git(root, args, { trim = true } = {}) {
47
50
  return trim ? output.trim() : output;
48
51
  }
49
52
 
53
+ export function assertCapabilityCutAncestor({
54
+ root = process.cwd(),
55
+ revision,
56
+ descendant = "HEAD",
57
+ label = "capability cut",
58
+ } = {}) {
59
+ try {
60
+ execFileSync("git", ["merge-base", "--is-ancestor", revision, descendant], {
61
+ cwd: root,
62
+ stdio: "ignore",
63
+ });
64
+ } catch {
65
+ throw new Error(
66
+ `${label} ${revision} must be an ancestor of ${descendant}; regenerate the cut after rebasing instead of relying on a retained local object`,
67
+ );
68
+ }
69
+ }
70
+
71
+ export function ensureCapabilityCutAncestor({
72
+ root = process.cwd(),
73
+ revision,
74
+ descendant = "HEAD",
75
+ label = "capability cut",
76
+ } = {}) {
77
+ try {
78
+ assertCapabilityCutAncestor({ root, revision, descendant, label });
79
+ return;
80
+ } catch (error) {
81
+ if (git(root, ["rev-parse", "--is-shallow-repository"]) !== "true")
82
+ throw error;
83
+ }
84
+ const descendantCommit = git(root, ["rev-parse", `${descendant}^{commit}`]);
85
+ try {
86
+ execFileSync(
87
+ "git",
88
+ ["fetch", "--no-tags", "--depth=128", "origin", descendantCommit],
89
+ { cwd: root, stdio: "ignore" },
90
+ );
91
+ } catch {
92
+ throw new Error(
93
+ `${label} ${revision} ancestry could not be hydrated from ${descendantCommit} through a bounded origin fetch`,
94
+ );
95
+ }
96
+ assertCapabilityCutAncestor({ root, revision, descendant, label });
97
+ }
98
+
99
+ function protectedV4TreeWitness(root, descendant) {
100
+ try {
101
+ execFileSync(
102
+ "git",
103
+ [
104
+ "fetch",
105
+ "--no-tags",
106
+ `--depth=${PROTECTED_LINEAGE_DEPTH}`,
107
+ "origin",
108
+ `${LIVE_V4_PROTECTED_BRANCH}:${LIVE_V4_PROTECTED_LINEAGE_REF}`,
109
+ ],
110
+ { cwd: root, stdio: "ignore" },
111
+ );
112
+ } catch {
113
+ throw new Error(
114
+ `protected v4 lineage ${LIVE_V4_PROTECTED_BRANCH} could not be hydrated through a bounded origin fetch`,
115
+ );
116
+ }
117
+ const tree = git(root, ["rev-parse", `${descendant}^{tree}`]);
118
+ return (
119
+ git(root, [
120
+ "log",
121
+ `--max-count=${PROTECTED_LINEAGE_DEPTH}`,
122
+ "--format=%H %T",
123
+ LIVE_V4_PROTECTED_LINEAGE_REF,
124
+ ])
125
+ .split("\n")
126
+ .map((row) => row.split(" "))
127
+ .find(([, candidateTree]) => candidateTree === tree)?.[0] || ""
128
+ );
129
+ }
130
+
131
+ export function ensureCapabilityCutLineage({
132
+ root = process.cwd(),
133
+ revision,
134
+ descendant = "HEAD",
135
+ label = "capability cut",
136
+ } = {}) {
137
+ try {
138
+ ensureCapabilityCutAncestor({ root, revision, descendant, label });
139
+ return { mode: "direct-ancestry", witness: descendant };
140
+ } catch (directError) {
141
+ const witness = protectedV4TreeWitness(root, descendant);
142
+ if (!witness)
143
+ throw new Error(
144
+ `${label} ${revision} is not an ancestor of ${descendant}, and ${descendant} has no tree-equivalent commit in the bounded protected v4 lineage`,
145
+ { cause: directError },
146
+ );
147
+ ensureCapabilityCutAncestor({
148
+ root,
149
+ revision,
150
+ descendant: witness,
151
+ label: `${label} protected-lineage witness`,
152
+ });
153
+ return { mode: "protected-tree-equivalent", witness };
154
+ }
155
+ }
156
+
50
157
  function gitJson(root, revision, relPath) {
51
158
  return JSON.parse(
52
159
  git(root, ["show", `${revision}:${relPath}`], { trim: false }),
@@ -8,7 +8,10 @@ export function admitV4DeclarativePromotion({
8
8
  runtimeRef,
9
9
  declarative,
10
10
  }) {
11
- if (!/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))) {
11
+ if (
12
+ declarative !== true &&
13
+ !/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))
14
+ ) {
12
15
  return Object.freeze({ mode: "legacy", admitted: true });
13
16
  }
14
17
  if (declarative !== true) {