@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -1
- package/actions/promote-buildchain-ref/README.md +90 -19
- package/actions/report-buildchain-issue/README.md +20 -2
- package/actions/run-lifecycle/README.md +5 -0
- package/actions/validate-config/README.md +19 -1
- package/bin/buildchain.mjs +56 -2
- package/bin/internal/command-registry.mjs +82 -0
- package/bin/internal/trust-release-cli.mjs +1 -0
- package/contracts/auditable-demo-media-profiles-v1.json +116 -1
- package/contracts/buildchain-v2-residuals-v1.json +146 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +188 -46
- package/dist/site/buildchain-site.json +181 -90
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +860 -98
- package/dist/site/controller-registry.json +118 -5
- package/dist/site/kfd-claims.json +233 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -20
- package/dist/site/node-api-registry.json +374 -59
- package/dist/site/page-registry.json +156 -65
- package/dist/site/public-surface-audit.json +231 -22
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +24 -24
- package/dist/site/workflow-registry.json +575 -79
- package/docs/MAP.md +22 -4
- package/docs/auditable-demo.md +54 -13
- package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
- package/docs/cli.md +119 -12
- package/docs/consumer-issue-reporting.md +20 -2
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +24 -31
- package/docs/lifecycle-protocol.md +20 -2
- package/docs/migration-inventory.md +24 -5
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/ownership.md +19 -1
- package/docs/publication-artifacts.md +158 -18
- package/docs/publication-authority.md +4 -4
- package/docs/publish-transaction.md +78 -31
- package/docs/release-candidate.md +24 -0
- package/docs/release-flow.md +50 -32
- package/docs/release-governance.md +39 -32
- package/docs/release-passport.md +4 -0
- package/docs/release-propagation.md +57 -12
- package/docs/reusable-build-surface.md +218 -90
- package/docs/runtime-train-validation.md +20 -2
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/stable-candidate-patrol.md +6 -6
- package/docs/toolkit-observability.md +22 -0
- package/docs/versioning.md +46 -24
- package/docs/web-surface-deployments.md +82 -1
- package/package.json +7 -5
- package/packages/core/README.md +60 -4
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +9 -4
- package/packages/core/cache-evidence.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/diagnostics.js +105 -30
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/index.js +40 -0
- package/packages/core/paper.js +3226 -0
- package/packages/core/public-surface-audit.js +3 -0
- package/packages/core/publication-artifact.js +1 -6
- package/packages/core/publication-authority.js +1 -0
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/publication-reproducibility.js +947 -0
- package/packages/core/publication-sealed-bundle.js +190 -0
- package/packages/core/publish-transaction.js +147 -17
- package/packages/core/release-candidate.js +234 -0
- package/packages/core/release-passport.js +519 -228
- package/packages/core/release-propagation.js +175 -1
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +266 -7
- package/scripts/aws-codebuild-toolchain.mjs +285 -0
- package/scripts/aws-macos-jit-core.mjs +378 -0
- package/scripts/aws-macos-jit.mjs +123 -0
- package/scripts/aws-runner-burst-core.mjs +248 -0
- package/scripts/aws-runner-burst.mjs +79 -0
- package/scripts/aws-windows-jit-core.mjs +374 -0
- package/scripts/aws-windows-jit.mjs +121 -0
- package/scripts/build-contract-core.mjs +69 -6
- package/scripts/build-standalone-binary.mjs +23 -0
- package/scripts/buildchain-contract-lock.mjs +1 -1
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-internal-architecture.mjs +69 -2
- package/scripts/check-inventory.mjs +70 -2
- package/scripts/check-javascript-syntax.mjs +31 -0
- package/scripts/check-maintainability.mjs +371 -0
- package/scripts/compiler-cache-evidence.mjs +221 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/generate-channel-promotion-workflow.mjs +3 -12
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +84 -18
- package/scripts/import-artifact-signing-results.mjs +64 -5
- package/scripts/inspect-artifact-signing-requests.mjs +52 -14
- package/scripts/maintainability-metrics.mjs +339 -0
- package/scripts/npm-publish-transaction.mjs +57 -6
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/paper.mjs +645 -0
- package/scripts/promotion-channel-router.mjs +16 -4
- package/scripts/publication-reproducibility.mjs +62 -0
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/release-propagation.mjs +39 -0
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/resolve-build-contract.mjs +6 -0
- package/scripts/route-offline-runners.mjs +236 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/run-lifecycle-core.mjs +62 -1
- package/scripts/runtime-ref-core.mjs +36 -4
- package/scripts/seal-artifact-signing-requests.mjs +64 -12
- package/scripts/stable-candidate-qualification.mjs +50 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-core.mjs +272 -86
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -42,6 +42,52 @@ function resolveImportTarget(root, sourcePath, specifier) {
|
|
|
42
42
|
return normalizeRelative(root, absolute);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function resolveGraphTarget(root, sourcePath, specifier, implementationPaths) {
|
|
46
|
+
const target = resolveImportTarget(root, sourcePath, specifier);
|
|
47
|
+
return [
|
|
48
|
+
target,
|
|
49
|
+
`${target}.js`,
|
|
50
|
+
`${target}.mjs`,
|
|
51
|
+
`${target}.cjs`,
|
|
52
|
+
`${target}/index.js`,
|
|
53
|
+
`${target}/index.mjs`,
|
|
54
|
+
].find((candidate) => implementationPaths.has(candidate));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dependencyCycles(graph) {
|
|
58
|
+
const cycles = [];
|
|
59
|
+
const visited = new Set();
|
|
60
|
+
const active = new Set();
|
|
61
|
+
const stack = [];
|
|
62
|
+
const canonical = new Set();
|
|
63
|
+
const visit = (node) => {
|
|
64
|
+
if (active.has(node)) {
|
|
65
|
+
const start = stack.indexOf(node);
|
|
66
|
+
const cycle = [...stack.slice(start), node];
|
|
67
|
+
const members = cycle.slice(0, -1);
|
|
68
|
+
const rotations = members.map((_, index) => [
|
|
69
|
+
...members.slice(index),
|
|
70
|
+
...members.slice(0, index),
|
|
71
|
+
].join(" -> "));
|
|
72
|
+
const key = rotations.sort()[0];
|
|
73
|
+
if (!canonical.has(key)) {
|
|
74
|
+
canonical.add(key);
|
|
75
|
+
cycles.push(cycle);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (visited.has(node)) return;
|
|
80
|
+
visited.add(node);
|
|
81
|
+
active.add(node);
|
|
82
|
+
stack.push(node);
|
|
83
|
+
for (const target of graph.get(node) || []) visit(target);
|
|
84
|
+
stack.pop();
|
|
85
|
+
active.delete(node);
|
|
86
|
+
};
|
|
87
|
+
for (const node of [...graph.keys()].sort()) visit(node);
|
|
88
|
+
return cycles;
|
|
89
|
+
}
|
|
90
|
+
|
|
45
91
|
function assertIndexShape(index) {
|
|
46
92
|
if (index?.schemaVersion !== 1) {
|
|
47
93
|
throw new Error("internal architecture index schemaVersion must be 1");
|
|
@@ -74,6 +120,9 @@ function checkInternalArchitecture({
|
|
|
74
120
|
continue;
|
|
75
121
|
}
|
|
76
122
|
capabilityIds.add(capability.id);
|
|
123
|
+
if (!capability.owner || typeof capability.owner !== "string") {
|
|
124
|
+
issues.push(`${capability.id}: owner is empty`);
|
|
125
|
+
}
|
|
77
126
|
if (!Array.isArray(capability.implementation) || capability.implementation.length === 0) {
|
|
78
127
|
issues.push(`${capability.id}: implementation mapping is empty`);
|
|
79
128
|
}
|
|
@@ -139,6 +188,22 @@ function checkInternalArchitecture({
|
|
|
139
188
|
}
|
|
140
189
|
}
|
|
141
190
|
|
|
191
|
+
const graph = new Map([...expectedImplementation].map((file) => [file, new Set()]));
|
|
192
|
+
for (const sourcePath of expectedImplementation) {
|
|
193
|
+
if (![".js", ".mjs", ".cjs"].includes(path.extname(sourcePath))) continue;
|
|
194
|
+
const source = sourceOverrides.has(sourcePath)
|
|
195
|
+
? sourceOverrides.get(sourcePath)
|
|
196
|
+
: fs.readFileSync(path.resolve(root, sourcePath), "utf8");
|
|
197
|
+
for (const specifier of relativeImports(source)) {
|
|
198
|
+
const target = resolveGraphTarget(root, sourcePath, specifier, expectedImplementation);
|
|
199
|
+
if (target) graph.get(sourcePath).add(target);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const cycles = dependencyCycles(graph);
|
|
203
|
+
for (const cycle of cycles) {
|
|
204
|
+
issues.push(`internal dependency cycle: ${cycle.join(" -> ")}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
142
207
|
if (issues.length > 0) {
|
|
143
208
|
throw new Error(
|
|
144
209
|
`internal architecture check failed:\n- ${issues.join("\n- ")}`,
|
|
@@ -149,6 +214,7 @@ function checkInternalArchitecture({
|
|
|
149
214
|
capabilities: index.capabilities.length,
|
|
150
215
|
implementations: expectedImplementation.size,
|
|
151
216
|
dependencyRules: index.dependencyRules.length,
|
|
217
|
+
dependencyCycles: cycles.length,
|
|
152
218
|
};
|
|
153
219
|
}
|
|
154
220
|
|
|
@@ -160,7 +226,8 @@ if (
|
|
|
160
226
|
const report = checkInternalArchitecture();
|
|
161
227
|
console.log(
|
|
162
228
|
`internal architecture check passed: ${report.capabilities} capabilities, ` +
|
|
163
|
-
`${report.implementations} implementations, ${report.dependencyRules} dependency rules
|
|
229
|
+
`${report.implementations} implementations, ${report.dependencyRules} dependency rules, ` +
|
|
230
|
+
`${report.dependencyCycles} cycles`,
|
|
164
231
|
);
|
|
165
232
|
} catch (error) {
|
|
166
233
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -168,4 +235,4 @@ if (
|
|
|
168
235
|
}
|
|
169
236
|
}
|
|
170
237
|
|
|
171
|
-
export { checkInternalArchitecture, relativeImports };
|
|
238
|
+
export { checkInternalArchitecture, dependencyCycles, relativeImports };
|
|
@@ -56,6 +56,9 @@ const requiredPaths = [
|
|
|
56
56
|
"packages/core/anchored-version-material.js",
|
|
57
57
|
"packages/core/build-facts.js",
|
|
58
58
|
"packages/core/publication-package.js",
|
|
59
|
+
"packages/core/publication-reproducibility.js",
|
|
60
|
+
"packages/core/publication-sealed-bundle.js",
|
|
61
|
+
"packages/core/paper.js",
|
|
59
62
|
"packages/core/release-line-bootstrap.js",
|
|
60
63
|
"packages/core/public-surface-audit.js",
|
|
61
64
|
"docs/MAP.md",
|
|
@@ -73,9 +76,14 @@ const requiredPaths = [
|
|
|
73
76
|
"docs/auditable-demo.md",
|
|
74
77
|
"contracts/auditable-demo-media-profiles-v1.json",
|
|
75
78
|
"contracts/evidence/auditable-demo-web-delivery-v1.json",
|
|
79
|
+
"contracts/evidence/auditable-demo-responsive-web-delivery-v1.json",
|
|
80
|
+
"contracts/buildchain-v2-residuals-v1.json",
|
|
76
81
|
"contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt",
|
|
77
82
|
"contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json",
|
|
78
83
|
"contracts/fixtures/auditable-demo-web-delivery-v1/scene.json",
|
|
84
|
+
"contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt",
|
|
85
|
+
"contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json",
|
|
86
|
+
"contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json",
|
|
79
87
|
"docs/release-propagation.md",
|
|
80
88
|
"docs/site-bundle-contract.md",
|
|
81
89
|
"docs/toolkit-observability.md",
|
|
@@ -101,8 +109,10 @@ const requiredPaths = [
|
|
|
101
109
|
"scripts/anchored-version-material.mjs",
|
|
102
110
|
"scripts/npm-publish-dry-run.mjs",
|
|
103
111
|
"scripts/npm-publish-transaction.mjs",
|
|
112
|
+
"scripts/paper.mjs",
|
|
104
113
|
"scripts/publication-package.mjs",
|
|
105
114
|
"scripts/publication-commit-evidence.mjs",
|
|
115
|
+
"scripts/publication-reproducibility.mjs",
|
|
106
116
|
"scripts/release-candidate-resolver.mjs",
|
|
107
117
|
"scripts/buildchain-patrol.mjs",
|
|
108
118
|
"scripts/observed-evidence.mjs",
|
|
@@ -113,6 +123,7 @@ const requiredPaths = [
|
|
|
113
123
|
"docs/observed-evidence-patrol.md",
|
|
114
124
|
"docs/ownership.md",
|
|
115
125
|
"tests/buildchain-inventory.json",
|
|
126
|
+
"tests/paper.test.mjs",
|
|
116
127
|
".buildchain/buildchain.toml",
|
|
117
128
|
".buildchain/contract-lock.json",
|
|
118
129
|
".buildchain/alpha-contract-lock.json",
|
|
@@ -260,12 +271,19 @@ for (const requiredSnippet of [
|
|
|
260
271
|
`/${promotionShellRouting.stable.workflowPath}@${promotionShellRouting.stable.callRef}`,
|
|
261
272
|
`STABLE_SHELL_REF: v${selfDogfoodMajor}`,
|
|
262
273
|
"promotion-contract-lock-digest:",
|
|
263
|
-
"promotion
|
|
274
|
+
"authorize-promotion-runtime-override.cjs",
|
|
264
275
|
]) {
|
|
265
276
|
if (!channelPromotionWorkflow.includes(requiredSnippet)) {
|
|
266
277
|
throw new Error(`channel promotion workflow missing routing contract: ${requiredSnippet}`);
|
|
267
278
|
}
|
|
268
279
|
}
|
|
280
|
+
const promotionOverrideAuthorization = fs.readFileSync(
|
|
281
|
+
path.join(root, "scripts/authorize-promotion-runtime-override.cjs"),
|
|
282
|
+
"utf8",
|
|
283
|
+
);
|
|
284
|
+
if (!promotionOverrideAuthorization.includes("promotion runtime override is only allowed for trusted workflow_dispatch runs")) {
|
|
285
|
+
throw new Error("promotion runtime override authorization must remain fail closed");
|
|
286
|
+
}
|
|
269
287
|
for (const requiredSnippet of [
|
|
270
288
|
"buildchain-channel:",
|
|
271
289
|
"uses: ./.github/workflows/.build.yml",
|
|
@@ -382,6 +400,9 @@ if (rootPackage.exports?.["./buildchain-kfd-claims"] !== "./packages/core/buildc
|
|
|
382
400
|
if (rootPackage.exports?.["./public-surface-audit"] !== "./packages/core/public-surface-audit.js") {
|
|
383
401
|
throw new Error("root package must export @kungfu-tech/buildchain/public-surface-audit");
|
|
384
402
|
}
|
|
403
|
+
if (rootPackage.exports?.["./paper"] !== "./packages/core/paper.js") {
|
|
404
|
+
throw new Error("root package must export @kungfu-tech/buildchain/paper");
|
|
405
|
+
}
|
|
385
406
|
if (rootPackage.exports?.["./site/buildchain-site.json"] !== "./dist/site/buildchain-site.json") {
|
|
386
407
|
throw new Error("root package must export @kungfu-tech/buildchain/site/buildchain-site.json");
|
|
387
408
|
}
|
|
@@ -646,16 +667,60 @@ for (const manual of manualRegistry.manuals || []) {
|
|
|
646
667
|
}
|
|
647
668
|
}
|
|
648
669
|
const cliRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/cli-registry.json"), "utf8"));
|
|
670
|
+
const requiredPublicLifecycleFields = [
|
|
671
|
+
"owner",
|
|
672
|
+
"maturity",
|
|
673
|
+
"introducedVersion",
|
|
674
|
+
"compatibilityPromise",
|
|
675
|
+
"deprecationReplacement",
|
|
676
|
+
"sunsetCondition",
|
|
677
|
+
"capabilityGroup",
|
|
678
|
+
"nonDuplicationRationale",
|
|
679
|
+
];
|
|
680
|
+
function assertPublicLifecycle(entry, label) {
|
|
681
|
+
for (const field of requiredPublicLifecycleFields) {
|
|
682
|
+
if (!Object.prototype.hasOwnProperty.call(entry, field) || typeof entry[field] !== "string") {
|
|
683
|
+
throw new Error(`${label} missing public lifecycle field: ${field}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (!entry.owner || !entry.maturity || !entry.introducedVersion || !entry.compatibilityPromise || !entry.sunsetCondition || !entry.nonDuplicationRationale) {
|
|
687
|
+
throw new Error(`${label} has incomplete public lifecycle metadata`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
649
690
|
for (const command of cliRegistry.commands || []) {
|
|
650
691
|
if (!command.capabilityGroup || !capabilityGroupIds.has(command.capabilityGroup) || !Array.isArray(command.audience) || !command.maturity || !command.purpose || command.purpose.includes("Add a specific purpose")) {
|
|
651
692
|
throw new Error(`cli-registry.json command missing capability metadata: ${command.id || command.usage}`);
|
|
652
693
|
}
|
|
694
|
+
assertPublicLifecycle(command, `cli-registry.json command ${command.id || command.usage}`);
|
|
653
695
|
}
|
|
654
696
|
const nodeApiRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/node-api-registry.json"), "utf8"));
|
|
655
697
|
for (const exported of nodeApiRegistry.exports || []) {
|
|
656
698
|
if (!exported.capabilityGroup || !capabilityGroupIds.has(exported.capabilityGroup) || !Array.isArray(exported.audience) || !exported.maturity || !exported.summary) {
|
|
657
699
|
throw new Error(`node-api-registry.json export missing capability metadata: ${exported.export || exported.specifier}`);
|
|
658
700
|
}
|
|
701
|
+
assertPublicLifecycle(exported, `node-api-registry.json export ${exported.export || exported.specifier}`);
|
|
702
|
+
}
|
|
703
|
+
const workflowRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/workflow-registry.json"), "utf8"));
|
|
704
|
+
for (const workflow of workflowRegistry.workflows || []) {
|
|
705
|
+
assertPublicLifecycle(workflow, `workflow-registry.json workflow ${workflow.id || workflow.path}`);
|
|
706
|
+
}
|
|
707
|
+
for (const action of workflowRegistry.actions || []) {
|
|
708
|
+
assertPublicLifecycle(action, `workflow-registry.json action ${action.id || action.path}`);
|
|
709
|
+
}
|
|
710
|
+
const registeredActionIds = (workflowRegistry.actions || []).map((entry) => entry.id).sort();
|
|
711
|
+
const readmeActionIndex = fs.readFileSync(path.join(root, "README.md"), "utf8");
|
|
712
|
+
const mapActionIndex = fs.readFileSync(path.join(root, "docs/MAP.md"), "utf8");
|
|
713
|
+
const retrospectiveActionIndex = fs.readFileSync(path.join(root, ".github/retrospectives/2026-07-10-buildchain-consolidation.md"), "utf8");
|
|
714
|
+
if (registeredActionIds.length !== 6) {
|
|
715
|
+
throw new Error(`workflow-registry.json must expose the six current action entries, got ${registeredActionIds.length}`);
|
|
716
|
+
}
|
|
717
|
+
for (const actionId of registeredActionIds) {
|
|
718
|
+
if (!readmeActionIndex.includes(`actions/${actionId}`) || !mapActionIndex.includes(`actions/${actionId}`)) {
|
|
719
|
+
throw new Error(`README and docs/MAP.md must index registered action: ${actionId}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (!retrospectiveActionIndex.includes("snapshot of the four consumer-facing actions")) {
|
|
723
|
+
throw new Error("the v2 four-action retrospective must remain explicitly classified as historical");
|
|
659
724
|
}
|
|
660
725
|
if (!pageRegistry.pages?.some((page) => page.sourcePath === "actions/promote-buildchain-ref/README.md")) {
|
|
661
726
|
throw new Error("page-registry.json must include action manuals");
|
|
@@ -1068,7 +1133,10 @@ if (commonJsSourcePattern.test(npmDryRunScript)) {
|
|
|
1068
1133
|
}
|
|
1069
1134
|
for (const requiredSnippet of [
|
|
1070
1135
|
"BUILDCHAIN_PUBLISH_EVIDENCE",
|
|
1071
|
-
"
|
|
1136
|
+
"BUILDCHAIN_SEALED_NPM_TARBALL",
|
|
1137
|
+
"...(pack.tarballPath ? [pack.tarballPath] : [])",
|
|
1138
|
+
"\"--access\"",
|
|
1139
|
+
"\"--tag\"",
|
|
1072
1140
|
"artifact digest mismatch",
|
|
1073
1141
|
]) {
|
|
1074
1142
|
if (!npmPublishTransactionScript.includes(requiredSnippet)) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const files = execFileSync(
|
|
8
|
+
"git",
|
|
9
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
10
|
+
{ cwd: root, encoding: "utf8" },
|
|
11
|
+
)
|
|
12
|
+
.split("\n")
|
|
13
|
+
.filter((file) => [".js", ".mjs", ".cjs"].includes(path.extname(file)))
|
|
14
|
+
.filter((file) => !/^actions\/[^/]+\/dist\//u.test(file))
|
|
15
|
+
.sort();
|
|
16
|
+
|
|
17
|
+
const failures = [];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const result = spawnSync(process.execPath, ["--check", file], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
23
|
+
if (result.status !== 0) {
|
|
24
|
+
failures.push(`${file}: ${(result.stderr || result.stdout).trim()}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (failures.length > 0) {
|
|
29
|
+
throw new Error(`JavaScript syntax check failed:\n${failures.join("\n")}`);
|
|
30
|
+
}
|
|
31
|
+
console.log(`JavaScript syntax check passed: ${files.length} files`);
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
analyzeJavaScript,
|
|
9
|
+
collectMaintainabilityMetrics,
|
|
10
|
+
isHandMaintainedSource,
|
|
11
|
+
} from "./maintainability-metrics.mjs";
|
|
12
|
+
|
|
13
|
+
function readJson(root, file) {
|
|
14
|
+
return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function gitOutput(root, args) {
|
|
18
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function revisionAvailable(root, revision) {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync("git", ["cat-file", "-e", `${revision}^{commit}`], {
|
|
24
|
+
cwd: root,
|
|
25
|
+
stdio: "ignore",
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureRevisionAvailable(root, revision) {
|
|
34
|
+
if (revisionAvailable(root, revision)) return false;
|
|
35
|
+
try {
|
|
36
|
+
gitOutput(root, ["fetch", "--no-tags", "--depth=1", "origin", revision]);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const detail = String(error?.stderr || error?.message || error).trim();
|
|
39
|
+
throw new Error(
|
|
40
|
+
`maintainability revision ${revision} is unavailable and could not be fetched from origin${detail ? `: ${detail}` : ""}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!revisionAvailable(root, revision)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`maintainability revision ${revision} is unavailable after a successful origin fetch`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureMaintainabilityRevisionsAvailable(
|
|
52
|
+
root,
|
|
53
|
+
{ baselineRevision, enforcementRevision },
|
|
54
|
+
) {
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
[...new Set([baselineRevision, enforcementRevision])].map((revision) => [
|
|
57
|
+
revision,
|
|
58
|
+
ensureRevisionAvailable(root, revision),
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sourceMetricsAtRevision(root, revision) {
|
|
64
|
+
const files = gitOutput(root, ["ls-tree", "-r", "--name-only", revision])
|
|
65
|
+
.split("\n")
|
|
66
|
+
.filter(isHandMaintainedSource)
|
|
67
|
+
.sort();
|
|
68
|
+
return Object.fromEntries(
|
|
69
|
+
files.map((file) => [
|
|
70
|
+
file,
|
|
71
|
+
analyzeJavaScript(file, gitOutput(root, ["show", `${revision}:${file}`])),
|
|
72
|
+
]),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function maximumFunction(metrics, field) {
|
|
77
|
+
return Math.max(
|
|
78
|
+
0,
|
|
79
|
+
...(metrics?.functions || []).map((entry) => entry[field]),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function selectedFunction(metrics, file, name) {
|
|
84
|
+
return metrics.files[file]?.functions.find((entry) => entry.name === name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readJsonAtRevision(root, revision, file) {
|
|
88
|
+
return JSON.parse(gitOutput(root, ["show", `${revision}:${file}`]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function publicSurfaceContract(entry, kind) {
|
|
92
|
+
if (kind === "cli") return { id: entry.id, usage: entry.usage };
|
|
93
|
+
if (kind === "node")
|
|
94
|
+
return {
|
|
95
|
+
export: entry.export,
|
|
96
|
+
specifier: entry.specifier,
|
|
97
|
+
target: entry.target,
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
id: entry.id,
|
|
101
|
+
path: entry.path,
|
|
102
|
+
reusable: entry.reusable,
|
|
103
|
+
inputs: entry.inputs || [],
|
|
104
|
+
secrets: entry.secrets || [],
|
|
105
|
+
outputs: entry.outputs || [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function evaluatePublicSurface({ root, revision, policy }) {
|
|
110
|
+
const issues = [];
|
|
111
|
+
const lifecycleFields = policy.publicSurfacePolicy.requiredLifecycleFields;
|
|
112
|
+
const capabilityGroups = new Set(
|
|
113
|
+
readJson(root, "dist/site/capability-registry.json").groups.map(
|
|
114
|
+
(entry) => entry.id,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
const definitions = [
|
|
118
|
+
{
|
|
119
|
+
file: "dist/site/cli-registry.json",
|
|
120
|
+
collection: "commands",
|
|
121
|
+
kind: "cli",
|
|
122
|
+
key: "id",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
file: "dist/site/node-api-registry.json",
|
|
126
|
+
collection: "exports",
|
|
127
|
+
kind: "node",
|
|
128
|
+
key: "export",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
file: "dist/site/workflow-registry.json",
|
|
132
|
+
collection: "workflows",
|
|
133
|
+
kind: "workflow",
|
|
134
|
+
key: "id",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
file: "dist/site/workflow-registry.json",
|
|
138
|
+
collection: "actions",
|
|
139
|
+
kind: "action",
|
|
140
|
+
key: "id",
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
for (const definition of definitions) {
|
|
144
|
+
const current =
|
|
145
|
+
readJson(root, definition.file)[definition.collection] || [];
|
|
146
|
+
const baseline =
|
|
147
|
+
readJsonAtRevision(root, revision, definition.file)[
|
|
148
|
+
definition.collection
|
|
149
|
+
] || [];
|
|
150
|
+
const baselineByKey = new Map(
|
|
151
|
+
baseline.map((entry) => [entry[definition.key], entry]),
|
|
152
|
+
);
|
|
153
|
+
for (const entry of current) {
|
|
154
|
+
const label = `${definition.kind}:${entry[definition.key]}`;
|
|
155
|
+
for (const field of lifecycleFields) {
|
|
156
|
+
if (
|
|
157
|
+
!Object.prototype.hasOwnProperty.call(entry, field) ||
|
|
158
|
+
typeof entry[field] !== "string"
|
|
159
|
+
) {
|
|
160
|
+
issues.push(`${label}: lifecycle field ${field} is missing`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!capabilityGroups.has(entry.capabilityGroup)) {
|
|
164
|
+
issues.push(
|
|
165
|
+
`${label}: capability group ${entry.capabilityGroup || "<empty>"} is not registered`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const previous = baselineByKey.get(entry[definition.key]);
|
|
169
|
+
if (
|
|
170
|
+
previous &&
|
|
171
|
+
JSON.stringify(publicSurfaceContract(entry, definition.kind)) !==
|
|
172
|
+
JSON.stringify(publicSurfaceContract(previous, definition.kind))
|
|
173
|
+
) {
|
|
174
|
+
issues.push(
|
|
175
|
+
`${label}: existing public contract drifted from ${revision}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!previous && !entry.nonDuplicationRationale) {
|
|
179
|
+
issues.push(
|
|
180
|
+
`${label}: new public surface requires a non-duplication rationale`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return issues;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evaluateAddedFunctionBudgets({
|
|
189
|
+
file,
|
|
190
|
+
metrics,
|
|
191
|
+
baseline,
|
|
192
|
+
policy,
|
|
193
|
+
budgets,
|
|
194
|
+
}) {
|
|
195
|
+
const issues = [];
|
|
196
|
+
const baselineFunctionNames = new Set(
|
|
197
|
+
(baseline.functions || [])
|
|
198
|
+
.map((entry) => entry.name)
|
|
199
|
+
.filter((name) => !name.startsWith("<anonymous@")),
|
|
200
|
+
);
|
|
201
|
+
for (const entry of metrics.functions) {
|
|
202
|
+
if (
|
|
203
|
+
entry.name.startsWith("<anonymous@") ||
|
|
204
|
+
baselineFunctionNames.has(entry.name)
|
|
205
|
+
) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const key = `${file}#${entry.name}`;
|
|
209
|
+
const approval = policy.approvedExtractedDebt?.[key];
|
|
210
|
+
if (approval && !String(approval.rationale || "").trim()) {
|
|
211
|
+
issues.push(`${key}: approved extracted debt requires a rationale`);
|
|
212
|
+
}
|
|
213
|
+
const allowedLines = approval?.maxLines ?? budgets.newFunctionLines;
|
|
214
|
+
const allowedComplexity =
|
|
215
|
+
approval?.maxComplexity ?? budgets.newFunctionComplexity;
|
|
216
|
+
if (entry.lines > allowedLines) {
|
|
217
|
+
issues.push(
|
|
218
|
+
`${file}:${entry.start} ${entry.name} has ${entry.lines} lines; new-function budget is ${allowedLines}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (entry.complexity > allowedComplexity) {
|
|
222
|
+
issues.push(
|
|
223
|
+
`${file}:${entry.start} ${entry.name} has complexity ${entry.complexity}; new-function budget is ${allowedComplexity}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return issues;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function evaluateMaintainability({ current, baselineFiles, policy }) {
|
|
231
|
+
const issues = [];
|
|
232
|
+
const budgets = policy.sourceBudgets;
|
|
233
|
+
for (const [file, metrics] of Object.entries(current.files)) {
|
|
234
|
+
const baseline = baselineFiles[file];
|
|
235
|
+
if (!baseline) {
|
|
236
|
+
if (metrics.lines > budgets.newFileLines) {
|
|
237
|
+
issues.push(
|
|
238
|
+
`${file}: new file has ${metrics.lines} lines; budget is ${budgets.newFileLines}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
for (const entry of metrics.functions) {
|
|
242
|
+
if (entry.lines > budgets.newFunctionLines) {
|
|
243
|
+
issues.push(
|
|
244
|
+
`${file}:${entry.start} ${entry.name} has ${entry.lines} lines; new-function budget is ${budgets.newFunctionLines}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (entry.complexity > budgets.newFunctionComplexity) {
|
|
248
|
+
issues.push(
|
|
249
|
+
`${file}:${entry.start} ${entry.name} has complexity ${entry.complexity}; new-function budget is ${budgets.newFunctionComplexity}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const transition = policy.approvedExistingDebtTransitions?.[file];
|
|
256
|
+
if (transition && !String(transition.rationale || "").trim()) {
|
|
257
|
+
issues.push(`${file}: approved debt transition requires a rationale`);
|
|
258
|
+
}
|
|
259
|
+
const allowedLines =
|
|
260
|
+
transition?.maxLines ?? Math.max(baseline.lines, budgets.newFileLines);
|
|
261
|
+
const allowedFunctionLines =
|
|
262
|
+
transition?.maxFunctionLines ??
|
|
263
|
+
Math.max(maximumFunction(baseline, "lines"), budgets.newFunctionLines);
|
|
264
|
+
const allowedFunctionComplexity =
|
|
265
|
+
transition?.maxFunctionComplexity ??
|
|
266
|
+
Math.max(
|
|
267
|
+
maximumFunction(baseline, "complexity"),
|
|
268
|
+
budgets.newFunctionComplexity,
|
|
269
|
+
);
|
|
270
|
+
if (metrics.lines > allowedLines) {
|
|
271
|
+
issues.push(
|
|
272
|
+
`${file}: file debt widened from ${baseline.lines} to ${metrics.lines} lines`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const maxLines = maximumFunction(metrics, "lines");
|
|
276
|
+
if (maxLines > allowedFunctionLines) {
|
|
277
|
+
issues.push(
|
|
278
|
+
`${file}: maximum function length widened beyond ${allowedFunctionLines} to ${maxLines}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const maxComplexity = maximumFunction(metrics, "complexity");
|
|
282
|
+
if (maxComplexity > allowedFunctionComplexity) {
|
|
283
|
+
issues.push(
|
|
284
|
+
`${file}: maximum function complexity widened beyond ${allowedFunctionComplexity} to ${maxComplexity}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
issues.push(
|
|
288
|
+
...evaluateAddedFunctionBudgets({
|
|
289
|
+
file,
|
|
290
|
+
metrics,
|
|
291
|
+
baseline,
|
|
292
|
+
policy,
|
|
293
|
+
budgets,
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const [name, budget] of Object.entries(policy.selectedFunctionBudgets)) {
|
|
299
|
+
const entry = selectedFunction(current, budget.file, name);
|
|
300
|
+
if (!entry) {
|
|
301
|
+
issues.push(`${budget.file}: selected function ${name} is missing`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (entry.lines > budget.lines) {
|
|
305
|
+
issues.push(
|
|
306
|
+
`${budget.file}: ${name} has ${entry.lines} lines; terminal budget is ${budget.lines}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (entry.complexity > budget.complexity) {
|
|
310
|
+
issues.push(
|
|
311
|
+
`${budget.file}: ${name} has complexity ${entry.complexity}; terminal budget is ${budget.complexity}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return issues;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function checkMaintainability({ root = process.cwd() } = {}) {
|
|
319
|
+
const policy = readJson(root, "architecture/maintainability-policy.json");
|
|
320
|
+
const baseline = readJson(root, policy.baseline);
|
|
321
|
+
const enforcementRevision = policy.enforcementRevision || baseline.revision;
|
|
322
|
+
const hydratedRevisions = ensureMaintainabilityRevisionsAvailable(root, {
|
|
323
|
+
baselineRevision: baseline.revision,
|
|
324
|
+
enforcementRevision,
|
|
325
|
+
});
|
|
326
|
+
const current = collectMaintainabilityMetrics({ root });
|
|
327
|
+
const baselineFiles = sourceMetricsAtRevision(root, enforcementRevision);
|
|
328
|
+
const issues = evaluateMaintainability({ current, baselineFiles, policy });
|
|
329
|
+
issues.push(
|
|
330
|
+
...evaluatePublicSurface({ root, revision: enforcementRevision, policy }),
|
|
331
|
+
);
|
|
332
|
+
if (issues.length > 0) {
|
|
333
|
+
throw new Error(`maintainability check failed:\n- ${issues.join("\n- ")}`);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
schemaVersion: 1,
|
|
337
|
+
baselineRevision: baseline.revision,
|
|
338
|
+
enforcementRevision,
|
|
339
|
+
hydratedBaselineRevision: hydratedRevisions[baseline.revision],
|
|
340
|
+
hydratedEnforcementRevision: hydratedRevisions[enforcementRevision],
|
|
341
|
+
trackedFiles: current.repository.trackedFiles,
|
|
342
|
+
sourceFiles: current.repository.handMaintainedSourceFiles,
|
|
343
|
+
publicSurface: current.publicSurface,
|
|
344
|
+
hotspots: current.hotspots,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (
|
|
349
|
+
process.argv[1] &&
|
|
350
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
351
|
+
) {
|
|
352
|
+
try {
|
|
353
|
+
const report = checkMaintainability();
|
|
354
|
+
console.log(
|
|
355
|
+
`maintainability check passed: ${report.sourceFiles} source files against ${report.enforcementRevision} ` +
|
|
356
|
+
`(audit baseline ${report.baselineRevision})`,
|
|
357
|
+
);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export {
|
|
365
|
+
checkMaintainability,
|
|
366
|
+
ensureMaintainabilityRevisionsAvailable,
|
|
367
|
+
ensureRevisionAvailable,
|
|
368
|
+
evaluateMaintainability,
|
|
369
|
+
evaluatePublicSurface,
|
|
370
|
+
sourceMetricsAtRevision,
|
|
371
|
+
};
|