@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.55
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 +9 -6
- package/actions/release-tail/README.md +8 -0
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1015 -0
- package/architecture/ci-lane-change-budget.json +256 -0
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +220 -15
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +489 -132
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v3-core-mechanism-inventory.json +4 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +4 -0
- package/dist/site/artifact-schemas.json +8 -0
- package/dist/site/buildchain-contract.json +125 -34
- package/dist/site/buildchain-site.json +308 -33
- package/dist/site/capability-registry.json +13 -12
- package/dist/site/cli-registry.json +57 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +438 -24
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +22 -7
- package/dist/site/node-api-registry.json +12843 -9314
- package/dist/site/page-registry.json +288 -21
- package/dist/site/public-surface-audit.json +281 -21
- package/dist/site/publication-authority-registry.json +66 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +20 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +19 -11
- package/dist/site/workflow-registry.json +195 -17
- package/docs/MAP.md +4 -2
- package/docs/cli-reference.md +70 -2
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +34 -10
- package/docs/node-api-reference.md +524 -279
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +152 -0
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +24 -4
- package/packages/core/adopter-delivery-passport.js +295 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/buildchain-publication-authority.js +2 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-delivery-native-proof.js +43 -27
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +46 -10
- package/packages/core/dev-delivery-warrant.js +29 -7
- package/packages/core/github-governance-authority.js +5 -4
- package/packages/core/index.js +6 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +71 -15
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +95 -170
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +139 -35
- package/packages/core/v4-floating-consumer-release-passport.js +154 -8
- package/packages/core/v4-publication-qualification.js +222 -0
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +24 -6
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +30 -4
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +50 -6
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +45 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-release-candidate-passport.mjs +177 -0
- package/scripts/generate-site-bundle.mjs +36 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +68 -3
- package/scripts/resume-from-candidate-run.mjs +595 -65
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +21 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { evaluateCiLaneChangeBudget } from "../packages/core/ci-lane-change-budget.js";
|
|
8
|
+
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const workflowRoot = path.join(root, ".github", "workflows");
|
|
11
|
+
const policyPath = path.join(
|
|
12
|
+
root,
|
|
13
|
+
"architecture",
|
|
14
|
+
"ci-lane-change-budget.json",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const workflows = fs
|
|
18
|
+
.readdirSync(workflowRoot, { withFileTypes: true })
|
|
19
|
+
.filter((entry) => entry.isFile() && /\.(?:yaml|yml)$/u.test(entry.name))
|
|
20
|
+
.map((entry) => {
|
|
21
|
+
const workflowPath = path.posix.join(".github/workflows", entry.name);
|
|
22
|
+
return {
|
|
23
|
+
path: workflowPath,
|
|
24
|
+
text: fs.readFileSync(path.join(workflowRoot, entry.name), "utf8"),
|
|
25
|
+
};
|
|
26
|
+
})
|
|
27
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
28
|
+
|
|
29
|
+
const policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
|
30
|
+
const baselineRevision = String(policy.baseline?.gitRevision || "").trim();
|
|
31
|
+
if (!/^[0-9a-f]{40}$/u.test(baselineRevision)) {
|
|
32
|
+
throw new Error("baseline.gitRevision must be an exact Git commit");
|
|
33
|
+
}
|
|
34
|
+
const embeddedBaselineLanes = policy.baseline?.lanes;
|
|
35
|
+
if (
|
|
36
|
+
!Array.isArray(embeddedBaselineLanes) ||
|
|
37
|
+
embeddedBaselineLanes.length === 0
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("baseline.lanes must contain the exact offline lane set");
|
|
40
|
+
}
|
|
41
|
+
const embeddedLaneSetRoot = `sha256:${crypto
|
|
42
|
+
.createHash("sha256")
|
|
43
|
+
.update(`${JSON.stringify(embeddedBaselineLanes)}\n`)
|
|
44
|
+
.digest("hex")}`;
|
|
45
|
+
if (embeddedLaneSetRoot !== policy.baseline?.laneSetRoot) {
|
|
46
|
+
throw new Error("baseline laneSetRoot does not match baseline.lanes");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let baselineWorkflows = null;
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("git", ["cat-file", "-e", `${baselineRevision}^{commit}`], {
|
|
52
|
+
cwd: root,
|
|
53
|
+
stdio: "ignore",
|
|
54
|
+
});
|
|
55
|
+
const baselinePaths = execFileSync(
|
|
56
|
+
"git",
|
|
57
|
+
[
|
|
58
|
+
"ls-tree",
|
|
59
|
+
"-r",
|
|
60
|
+
"--name-only",
|
|
61
|
+
baselineRevision,
|
|
62
|
+
"--",
|
|
63
|
+
".github/workflows",
|
|
64
|
+
],
|
|
65
|
+
{ cwd: root, encoding: "utf8" },
|
|
66
|
+
)
|
|
67
|
+
.split(/\r?\n/u)
|
|
68
|
+
.filter((value) => /\.(?:yaml|yml)$/u.test(value))
|
|
69
|
+
.sort();
|
|
70
|
+
baselineWorkflows = baselinePaths.map((workflowPath) => ({
|
|
71
|
+
path: workflowPath,
|
|
72
|
+
text: execFileSync("git", ["show", `${baselineRevision}:${workflowPath}`], {
|
|
73
|
+
cwd: root,
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
} catch {
|
|
78
|
+
// Shallow and exported source cuts may not contain the pinned commit object.
|
|
79
|
+
// The content-rooted embedded lane set remains the exact offline authority.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const embeddedEvaluation = evaluateCiLaneChangeBudget({
|
|
83
|
+
policy,
|
|
84
|
+
workflows,
|
|
85
|
+
});
|
|
86
|
+
const evaluation = baselineWorkflows
|
|
87
|
+
? evaluateCiLaneChangeBudget({ policy, workflows, baselineWorkflows })
|
|
88
|
+
: embeddedEvaluation;
|
|
89
|
+
if (
|
|
90
|
+
baselineWorkflows &&
|
|
91
|
+
(evaluation.baselineLaneCount !== embeddedEvaluation.baselineLaneCount ||
|
|
92
|
+
JSON.stringify(evaluation.newLanes) !==
|
|
93
|
+
JSON.stringify(embeddedEvaluation.newLanes))
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"embedded baseline lanes do not match the pinned baseline revision",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(`${JSON.stringify(evaluation, null, 2)}\n`);
|
|
100
|
+
if (!evaluation.ok) process.exitCode = 1;
|
|
@@ -100,8 +100,8 @@ function dependencyCycles(graph) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function assertIndexShape(index) {
|
|
103
|
-
if (index?.schemaVersion !==
|
|
104
|
-
throw new Error("internal architecture index schemaVersion must be
|
|
103
|
+
if (index?.schemaVersion !== 2) {
|
|
104
|
+
throw new Error("internal architecture index schemaVersion must be 2");
|
|
105
105
|
}
|
|
106
106
|
for (const field of [
|
|
107
107
|
"coverageRoots",
|
|
@@ -209,7 +209,27 @@ function evaluateCapabilities(root, index) {
|
|
|
209
209
|
if (!Array.isArray(capability.tests) || capability.tests.length === 0) {
|
|
210
210
|
issues.push(`${capability.id}: test mapping is empty`);
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
if (!Array.isArray(capability.generatedOutputs)) {
|
|
213
|
+
issues.push(`${capability.id}: generated output mapping is missing`);
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
!Array.isArray(capability.validationCommands) ||
|
|
217
|
+
capability.validationCommands.length === 0
|
|
218
|
+
) {
|
|
219
|
+
issues.push(`${capability.id}: validation command mapping is empty`);
|
|
220
|
+
} else if (
|
|
221
|
+
capability.validationCommands.some(
|
|
222
|
+
(command) => !String(command || "").trim(),
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
issues.push(`${capability.id}: validation command is empty`);
|
|
226
|
+
}
|
|
227
|
+
for (const field of [
|
|
228
|
+
"implementation",
|
|
229
|
+
"tests",
|
|
230
|
+
"contracts",
|
|
231
|
+
"generatedOutputs",
|
|
232
|
+
]) {
|
|
213
233
|
for (const relativePath of capability[field] || []) {
|
|
214
234
|
if (!fs.existsSync(path.resolve(root, relativePath))) {
|
|
215
235
|
issues.push(
|
|
@@ -285,6 +285,10 @@ const channelPromotionWorkflow = fs.readFileSync(
|
|
|
285
285
|
path.join(root, ".github/workflows/release-candidate-promote.yml"),
|
|
286
286
|
"utf8",
|
|
287
287
|
);
|
|
288
|
+
const boundedAlphaRecoveryWorkflow = fs.readFileSync(
|
|
289
|
+
path.join(root, ".github/workflows/buildchain-ref-promotion-recovery.yml"),
|
|
290
|
+
"utf8",
|
|
291
|
+
);
|
|
288
292
|
const promotionShellRouting = parsePromotionShellRouting(
|
|
289
293
|
fs.readFileSync(path.join(root, ".buildchain/promotion-shell-routing.json"), "utf8"),
|
|
290
294
|
{ major: Number(selfDogfoodMajor) },
|
|
@@ -295,6 +299,20 @@ if (channelPromotionWorkflow !== generateChannelPromotionWorkflow(advancedPromot
|
|
|
295
299
|
})) {
|
|
296
300
|
throw new Error("generated channel promotion workflow is stale");
|
|
297
301
|
}
|
|
302
|
+
for (const requiredSnippet of [
|
|
303
|
+
"uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@alpha/v4/v4.0",
|
|
304
|
+
"promotion-shell-ref: ${{ needs.consumer-admission.outputs.shell-call-ref }}",
|
|
305
|
+
"declarative-release-tail: true",
|
|
306
|
+
]) {
|
|
307
|
+
if (!boundedAlphaRecoveryWorkflow.includes(requiredSnippet)) {
|
|
308
|
+
throw new Error(`bounded alpha recovery missing bootstrap contract: ${requiredSnippet}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const workflowSource of [boundedAlphaRecoveryWorkflow, channelPromotionWorkflow]) {
|
|
312
|
+
if (workflowSource.includes("channel-finalization-recovery")) {
|
|
313
|
+
throw new Error("Buildchain-only alpha bootstrap must not add a public or recovery workflow input");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
298
316
|
for (const requiredSnippet of [
|
|
299
317
|
"buildchain-channel:",
|
|
300
318
|
`/${promotionShellRouting.alpha.workflowPath}@${promotionShellRouting.alpha.callRef}`,
|
|
@@ -759,8 +777,8 @@ const registeredActionIds = (workflowRegistry.actions || []).map((entry) => entr
|
|
|
759
777
|
const readmeActionIndex = fs.readFileSync(path.join(root, "README.md"), "utf8");
|
|
760
778
|
const mapActionIndex = fs.readFileSync(path.join(root, "docs/MAP.md"), "utf8");
|
|
761
779
|
const retrospectiveActionIndex = fs.readFileSync(path.join(root, ".github/retrospectives/2026-07-10-buildchain-consolidation.md"), "utf8");
|
|
762
|
-
if (registeredActionIds.length !==
|
|
763
|
-
throw new Error(`workflow-registry.json must expose the
|
|
780
|
+
if (registeredActionIds.length !== 8) {
|
|
781
|
+
throw new Error(`workflow-registry.json must expose the eight current action entries, got ${registeredActionIds.length}`);
|
|
764
782
|
}
|
|
765
783
|
for (const actionId of registeredActionIds) {
|
|
766
784
|
if (!readmeActionIndex.includes(`actions/${actionId}`) || !mapActionIndex.includes(`actions/${actionId}`)) {
|
|
@@ -1021,16 +1039,16 @@ for (const forbiddenSnippet of [
|
|
|
1021
1039
|
for (const requiredSnippet of [
|
|
1022
1040
|
"id-token: write",
|
|
1023
1041
|
"actions: write",
|
|
1024
|
-
"uses:
|
|
1042
|
+
"uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@v4-alpha",
|
|
1025
1043
|
"github.event.workflow_run.event == 'push'",
|
|
1026
1044
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
|
|
1027
1045
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
|
|
1028
1046
|
"resume-candidate-run-id:",
|
|
1029
1047
|
"resume-expected-source-tree:",
|
|
1030
|
-
"resume-buildchain-runtime-
|
|
1031
|
-
"github-
|
|
1048
|
+
"resume-buildchain-runtime-ref:",
|
|
1049
|
+
"uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
|
|
1032
1050
|
"release-passport-buildchain-self-kfd: true",
|
|
1033
|
-
"
|
|
1051
|
+
"declarative-release-tail: true",
|
|
1034
1052
|
"release-passport-impact-json: .buildchain/release-impact.json",
|
|
1035
1053
|
]) {
|
|
1036
1054
|
if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
|
|
@@ -5,10 +5,52 @@ import path from "node:path";
|
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
analyzeSource,
|
|
9
|
+
analyzeWorkflow,
|
|
9
10
|
collectMaintainabilityMetrics,
|
|
10
11
|
isHandMaintainedSource,
|
|
11
12
|
} from "./maintainability-metrics.mjs";
|
|
13
|
+
import {
|
|
14
|
+
calculateDebtMeasuredExcess,
|
|
15
|
+
evaluateDebtAuthority,
|
|
16
|
+
evaluateDebtBudget,
|
|
17
|
+
evaluateExceptionBudget,
|
|
18
|
+
evaluateExceptionGovernance,
|
|
19
|
+
evaluateTestBudgets,
|
|
20
|
+
evaluateWorkflowBudgets,
|
|
21
|
+
} from "./maintainability-governance.mjs";
|
|
22
|
+
|
|
23
|
+
function collectHotspots(root, current, limit = 20, shallowFallback = []) {
|
|
24
|
+
const maintained = new Set([
|
|
25
|
+
...Object.keys(current.files || {}),
|
|
26
|
+
...Object.keys(current.tests || {}),
|
|
27
|
+
...Object.keys(current.workflows || {}),
|
|
28
|
+
]);
|
|
29
|
+
// Shallow consumers cannot reproduce repository-wide churn ranking.
|
|
30
|
+
// Reuse the audited routes; full clones still re-rank them below.
|
|
31
|
+
// This keeps consumer verification deterministic without inventing history.
|
|
32
|
+
if (
|
|
33
|
+
gitOutput(root, ["rev-parse", "--is-shallow-repository"]).trim() === "true"
|
|
34
|
+
) {
|
|
35
|
+
return shallowFallback.slice(0, limit);
|
|
36
|
+
}
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
const record = (file) =>
|
|
39
|
+
maintained.has(file) && counts.set(file, (counts.get(file) || 0) + 1);
|
|
40
|
+
for (const args of [
|
|
41
|
+
["log", "-n", "500", "--format=", "--name-only"],
|
|
42
|
+
["diff", "--name-only"],
|
|
43
|
+
])
|
|
44
|
+
for (const file of gitOutput(root, args).split("\n").filter(Boolean))
|
|
45
|
+
record(file);
|
|
46
|
+
return [...counts]
|
|
47
|
+
.sort(
|
|
48
|
+
([leftPath, left], [rightPath, right]) =>
|
|
49
|
+
right - left || leftPath.localeCompare(rightPath),
|
|
50
|
+
)
|
|
51
|
+
.slice(0, limit)
|
|
52
|
+
.map(([file]) => file);
|
|
53
|
+
}
|
|
12
54
|
|
|
13
55
|
function readJson(root, file) {
|
|
14
56
|
return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
|
|
@@ -68,7 +110,35 @@ function sourceMetricsAtRevision(root, revision) {
|
|
|
68
110
|
return Object.fromEntries(
|
|
69
111
|
files.map((file) => [
|
|
70
112
|
file,
|
|
71
|
-
|
|
113
|
+
analyzeSource(file, gitOutput(root, ["show", `${revision}:${file}`])),
|
|
114
|
+
]),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function testMetricsAtRevision(root, revision) {
|
|
119
|
+
const files = gitOutput(root, ["ls-tree", "-r", "--name-only", revision])
|
|
120
|
+
.split("\n")
|
|
121
|
+
.filter((file) =>
|
|
122
|
+
/(?:^tests\/|(?:^|\/)tests?\/).+\.(?:c|m)?js$/u.test(file),
|
|
123
|
+
)
|
|
124
|
+
.sort();
|
|
125
|
+
return Object.fromEntries(
|
|
126
|
+
files.map((file) => [
|
|
127
|
+
file,
|
|
128
|
+
analyzeSource(file, gitOutput(root, ["show", `${revision}:${file}`])),
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function workflowMetricsAtRevision(root, revision) {
|
|
134
|
+
const files = gitOutput(root, ["ls-tree", "-r", "--name-only", revision])
|
|
135
|
+
.split("\n")
|
|
136
|
+
.filter((file) => /^\.github\/workflows\/.*\.ya?ml$/u.test(file))
|
|
137
|
+
.sort();
|
|
138
|
+
return Object.fromEntries(
|
|
139
|
+
files.map((file) => [
|
|
140
|
+
file,
|
|
141
|
+
analyzeWorkflow(file, gitOutput(root, ["show", `${revision}:${file}`])),
|
|
72
142
|
]),
|
|
73
143
|
);
|
|
74
144
|
}
|
|
@@ -276,10 +346,14 @@ function evaluateRepositoryBudgets({ current, policy }) {
|
|
|
276
346
|
return issues;
|
|
277
347
|
}
|
|
278
348
|
|
|
349
|
+
function budgetsForFile(policy, file) {
|
|
350
|
+
return file.endsWith(".rs") ? policy.rustBudgets : policy.sourceBudgets;
|
|
351
|
+
}
|
|
352
|
+
|
|
279
353
|
function evaluateMaintainability({ current, baselineFiles, policy }) {
|
|
280
354
|
const issues = [];
|
|
281
|
-
const budgets = policy.sourceBudgets;
|
|
282
355
|
for (const [file, metrics] of Object.entries(current.files)) {
|
|
356
|
+
const budgets = budgetsForFile(policy, file);
|
|
283
357
|
const baseline = baselineFiles[file];
|
|
284
358
|
if (!baseline) {
|
|
285
359
|
const transition = policy.approvedNewFileTransitions?.[file];
|
|
@@ -381,15 +455,66 @@ function evaluateMaintainability({ current, baselineFiles, policy }) {
|
|
|
381
455
|
|
|
382
456
|
function checkMaintainability({ root = process.cwd() } = {}) {
|
|
383
457
|
const policy = readJson(root, "architecture/maintainability-policy.json");
|
|
458
|
+
if (policy.schemaVersion !== 2) {
|
|
459
|
+
throw new Error("maintainability policy schemaVersion must be 2");
|
|
460
|
+
}
|
|
384
461
|
const baseline = readJson(root, policy.baseline);
|
|
385
462
|
const enforcementRevision = policy.enforcementRevision || baseline.revision;
|
|
463
|
+
const extendedCoverageRevision =
|
|
464
|
+
policy.extendedCoverageRevision || enforcementRevision;
|
|
386
465
|
const hydratedRevisions = ensureMaintainabilityRevisionsAvailable(root, {
|
|
387
466
|
baselineRevision: baseline.revision,
|
|
388
467
|
enforcementRevision,
|
|
389
468
|
});
|
|
469
|
+
const hydratedExtendedCoverageRevision = ensureRevisionAvailable(
|
|
470
|
+
root,
|
|
471
|
+
extendedCoverageRevision,
|
|
472
|
+
);
|
|
390
473
|
const current = collectMaintainabilityMetrics({ root });
|
|
474
|
+
const debt = readJson(root, "architecture/maintainability-debt.json");
|
|
475
|
+
const architecture = readJson(
|
|
476
|
+
root,
|
|
477
|
+
"architecture/internal-capabilities.json",
|
|
478
|
+
);
|
|
391
479
|
const baselineFiles = sourceMetricsAtRevision(root, enforcementRevision);
|
|
392
|
-
const
|
|
480
|
+
const extendedSourceFiles = sourceMetricsAtRevision(
|
|
481
|
+
root,
|
|
482
|
+
extendedCoverageRevision,
|
|
483
|
+
);
|
|
484
|
+
for (const [file, metrics] of Object.entries(extendedSourceFiles)) {
|
|
485
|
+
if (file.endsWith(".rs")) baselineFiles[file] = metrics;
|
|
486
|
+
}
|
|
487
|
+
const baselineTests = testMetricsAtRevision(root, extendedCoverageRevision);
|
|
488
|
+
const baselineWorkflows = workflowMetricsAtRevision(
|
|
489
|
+
root,
|
|
490
|
+
extendedCoverageRevision,
|
|
491
|
+
);
|
|
492
|
+
const issues = evaluateExceptionGovernance({ policy });
|
|
493
|
+
issues.push(...evaluateExceptionBudget({ policy }));
|
|
494
|
+
const hotspots = collectHotspots(root, current, 20, debt.hotspots || []);
|
|
495
|
+
issues.push(
|
|
496
|
+
...evaluateDebtAuthority({
|
|
497
|
+
current,
|
|
498
|
+
policy,
|
|
499
|
+
debt,
|
|
500
|
+
capabilityIds: new Set(
|
|
501
|
+
architecture.capabilities.map((entry) => entry.id),
|
|
502
|
+
),
|
|
503
|
+
hotspots,
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
issues.push(...evaluateDebtBudget({ debt, policy }));
|
|
507
|
+
issues.push(...evaluateMaintainability({ current, baselineFiles, policy }));
|
|
508
|
+
issues.push(
|
|
509
|
+
...evaluateTestBudgets({ current, baselineFiles: baselineTests, policy }),
|
|
510
|
+
);
|
|
511
|
+
issues.push(
|
|
512
|
+
...evaluateWorkflowBudgets({
|
|
513
|
+
current,
|
|
514
|
+
baselineFiles: baselineWorkflows,
|
|
515
|
+
policy,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
393
518
|
issues.push(...evaluateRepositoryBudgets({ current, policy }));
|
|
394
519
|
issues.push(
|
|
395
520
|
...evaluatePublicSurface({ root, revision: enforcementRevision, policy }),
|
|
@@ -401,12 +526,18 @@ function checkMaintainability({ root = process.cwd() } = {}) {
|
|
|
401
526
|
schemaVersion: 1,
|
|
402
527
|
baselineRevision: baseline.revision,
|
|
403
528
|
enforcementRevision,
|
|
529
|
+
extendedCoverageRevision,
|
|
404
530
|
hydratedBaselineRevision: hydratedRevisions[baseline.revision],
|
|
405
531
|
hydratedEnforcementRevision: hydratedRevisions[enforcementRevision],
|
|
532
|
+
hydratedExtendedCoverageRevision,
|
|
406
533
|
trackedFiles: current.repository.trackedFiles,
|
|
407
534
|
sourceFiles: current.repository.handMaintainedSourceFiles,
|
|
408
535
|
publicSurface: current.publicSurface,
|
|
409
536
|
hotspots: current.hotspots,
|
|
537
|
+
governedDebtSurfaces: Object.values(debt.surfaces).reduce(
|
|
538
|
+
(total, entries) => total + Object.keys(entries).length,
|
|
539
|
+
0,
|
|
540
|
+
),
|
|
410
541
|
};
|
|
411
542
|
}
|
|
412
543
|
|
|
@@ -427,11 +558,21 @@ if (
|
|
|
427
558
|
}
|
|
428
559
|
|
|
429
560
|
export {
|
|
561
|
+
calculateDebtMeasuredExcess,
|
|
430
562
|
checkMaintainability,
|
|
563
|
+
collectHotspots,
|
|
431
564
|
ensureMaintainabilityRevisionsAvailable,
|
|
432
565
|
ensureRevisionAvailable,
|
|
433
566
|
evaluateMaintainability,
|
|
567
|
+
evaluateDebtAuthority,
|
|
568
|
+
evaluateDebtBudget,
|
|
569
|
+
evaluateExceptionBudget,
|
|
570
|
+
evaluateExceptionGovernance,
|
|
434
571
|
evaluateRepositoryBudgets,
|
|
572
|
+
evaluateTestBudgets,
|
|
573
|
+
evaluateWorkflowBudgets,
|
|
435
574
|
evaluatePublicSurface,
|
|
436
575
|
sourceMetricsAtRevision,
|
|
576
|
+
testMetricsAtRevision,
|
|
577
|
+
workflowMetricsAtRevision,
|
|
437
578
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
ProjectCutAdmissionError,
|
|
5
|
+
qualifyProjectCut,
|
|
6
|
+
releaseFamilyQueueLease,
|
|
7
|
+
} from "./project-cut-merge-queue-admission.mjs";
|
|
8
|
+
|
|
9
|
+
function parseArguments(argv) {
|
|
10
|
+
const options = {};
|
|
11
|
+
const names = new Map([
|
|
12
|
+
["--base", "base"],
|
|
13
|
+
["--head", "head"],
|
|
14
|
+
["--initiative-id", "initiativeId"],
|
|
15
|
+
["--assignment-id", "assignmentId"],
|
|
16
|
+
["--delivery-class", "deliveryClass"],
|
|
17
|
+
["--queue-attempt", "queueAttempt"],
|
|
18
|
+
["--admission-proof-root", "admissionProofRoot"],
|
|
19
|
+
["--release-family-marker", "releaseFamilyMarker"],
|
|
20
|
+
["--expected-pr-head", "expectedPullRequestHead"],
|
|
21
|
+
["--terminal-reason", "terminalReason"],
|
|
22
|
+
["--evidence-root", "evidenceRoot"],
|
|
23
|
+
]);
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const name = argv[index];
|
|
26
|
+
const key = names.get(name);
|
|
27
|
+
if (!key) throw new Error(`unknown argument: ${name}`);
|
|
28
|
+
const value = argv[index + 1];
|
|
29
|
+
if (value === undefined || value.startsWith("--"))
|
|
30
|
+
throw new Error(`${name} requires a value`);
|
|
31
|
+
if (Object.hasOwn(options, key))
|
|
32
|
+
throw new Error(`${name} was provided more than once`);
|
|
33
|
+
options[key] = value;
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markerText(path) {
|
|
40
|
+
return path === "-"
|
|
41
|
+
? fs.readFileSync(0, "utf8")
|
|
42
|
+
: fs.readFileSync(path, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function repairReceipt(error) {
|
|
46
|
+
return {
|
|
47
|
+
schema: "project.cut.merge-queue-admission/v1",
|
|
48
|
+
ok: false,
|
|
49
|
+
decision: "repair-required",
|
|
50
|
+
retryable: false,
|
|
51
|
+
compositionChanged: false,
|
|
52
|
+
reasonCodes: [error.reasonCode],
|
|
53
|
+
error: error.message,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function main(argv = process.argv.slice(2)) {
|
|
58
|
+
try {
|
|
59
|
+
const options = parseArguments(argv);
|
|
60
|
+
if (options.releaseFamilyMarker) {
|
|
61
|
+
console.log(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
releaseFamilyQueueLease(
|
|
64
|
+
markerText(options.releaseFamilyMarker),
|
|
65
|
+
options,
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (!options.base || !options.head)
|
|
72
|
+
throw new Error("--base and --head are required");
|
|
73
|
+
console.log(
|
|
74
|
+
JSON.stringify(qualifyProjectCut({ cwd: process.cwd(), ...options })),
|
|
75
|
+
);
|
|
76
|
+
return 0;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof ProjectCutAdmissionError) {
|
|
79
|
+
console.log(JSON.stringify(repairReceipt(error)));
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
console.error(`project-cut admission error: ${error.message}`);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (import.meta.url === `file://${process.argv[1]}`) process.exitCode = main();
|