@kungfu-tech/buildchain 4.0.1-alpha.9 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- 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 +70 -1
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +156 -11
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +480 -129
- 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-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +123 -32
- package/dist/site/buildchain-site.json +96 -28
- package/dist/site/capability-registry.json +6 -6
- package/dist/site/cli-registry.json +5 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +170 -21
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +1474 -215
- package/dist/site/page-registry.json +85 -17
- package/dist/site/public-surface-audit.json +85 -20
- package/dist/site/publication-authority-registry.json +28 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +5 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +94 -18
- package/docs/MAP.md +1 -1
- package/docs/cli-reference.md +4 -4
- package/docs/dev-delivery-warrant.md +12 -9
- package/docs/node-api-reference.md +291 -206
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/v4-adopter-delivery.md +23 -4
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/package.json +8 -3
- package/packages/core/adopter-delivery-passport.js +36 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +15 -2
- package/packages/core/dev-delivery-warrant.js +21 -4
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/index.js +1 -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/release-candidate-recovery.js +6 -5
- package/packages/core/release-passport.js +82 -184
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-floating-consumer-policy.js +3 -2
- package/packages/core/v4-floating-consumer-release-passport.js +134 -15
- package/packages/core/v4-publication-qualification.js +222 -0
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +23 -5
- 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 +10 -3
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant.mjs +45 -5
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +44 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-release-candidate-passport.mjs +26 -1
- package/scripts/generate-site-bundle.mjs +6 -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-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +19 -2
- package/scripts/resume-from-candidate-run.mjs +106 -103
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +4 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const indexPath = "architecture/internal-capabilities.json";
|
|
8
|
+
const debtPath = "architecture/maintainability-debt.json";
|
|
9
|
+
const outputPath = "architecture/agent-change-map.md";
|
|
10
|
+
|
|
11
|
+
function list(values) {
|
|
12
|
+
if (!values?.length) return "- None.";
|
|
13
|
+
return values.map((value) => `- \`${value}\``).join("\n");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function debtRouteSection(debt) {
|
|
17
|
+
if (!debt?.surfaces) return "";
|
|
18
|
+
const defaults = debt.defaults;
|
|
19
|
+
const governedPaths = Object.entries(debt.surfaces).flatMap(
|
|
20
|
+
([domain, entries]) =>
|
|
21
|
+
Object.keys(entries).map((entry) => `${domain}: \`${entry}\``),
|
|
22
|
+
);
|
|
23
|
+
return `## Governed complexity debt routes
|
|
24
|
+
|
|
25
|
+
Owner: ${defaults.owner}
|
|
26
|
+
|
|
27
|
+
Capability: \`${defaults.capability}\`
|
|
28
|
+
|
|
29
|
+
Invariant: ${defaults.invariant}
|
|
30
|
+
|
|
31
|
+
Tests: ${defaults.tests.map((entry) => `\`${entry}\``).join(", ")}
|
|
32
|
+
|
|
33
|
+
Safe change route: ${defaults.safeChangeRoute}
|
|
34
|
+
|
|
35
|
+
### Oversized surfaces
|
|
36
|
+
|
|
37
|
+
${governedPaths.map((entry) => `- ${entry}`).join("\n")}
|
|
38
|
+
|
|
39
|
+
### Current top twenty maintenance hotspots
|
|
40
|
+
|
|
41
|
+
${debt.hotspots.map((entry) => `- \`${entry}\``).join("\n")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateAgentChangeMap(index, debt = null) {
|
|
45
|
+
const capabilities = [...index.capabilities].sort((left, right) =>
|
|
46
|
+
left.id.localeCompare(right.id),
|
|
47
|
+
);
|
|
48
|
+
const sections = capabilities.map(
|
|
49
|
+
(capability) => `## ${capability.id}
|
|
50
|
+
|
|
51
|
+
Owner: ${capability.owner}
|
|
52
|
+
|
|
53
|
+
### Implementation
|
|
54
|
+
|
|
55
|
+
${list(capability.implementation)}
|
|
56
|
+
|
|
57
|
+
### Contracts
|
|
58
|
+
|
|
59
|
+
${list(capability.contracts)}
|
|
60
|
+
|
|
61
|
+
### Tests
|
|
62
|
+
|
|
63
|
+
${list(capability.tests)}
|
|
64
|
+
|
|
65
|
+
### Generated outputs
|
|
66
|
+
|
|
67
|
+
${list(capability.generatedOutputs)}
|
|
68
|
+
|
|
69
|
+
### Minimal validation
|
|
70
|
+
|
|
71
|
+
${list(capability.validationCommands)}`,
|
|
72
|
+
);
|
|
73
|
+
return `<!-- Generated by scripts/generate-agent-change-map.mjs. Do not edit directly. -->
|
|
74
|
+
# Agent Change Map
|
|
75
|
+
|
|
76
|
+
Use this index to identify the smallest implementation, contract, test, generated-output, and validation surface for a capability change. The machine-readable source of truth is \`${indexPath}\`.
|
|
77
|
+
|
|
78
|
+
${debtRouteSection(debt)}
|
|
79
|
+
|
|
80
|
+
${sections.join("\n\n")}
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function run({ root = process.cwd(), mode = "check" } = {}) {
|
|
85
|
+
const index = JSON.parse(fs.readFileSync(path.join(root, indexPath), "utf8"));
|
|
86
|
+
const debt = JSON.parse(fs.readFileSync(path.join(root, debtPath), "utf8"));
|
|
87
|
+
const generated = generateAgentChangeMap(index, debt);
|
|
88
|
+
const target = path.join(root, outputPath);
|
|
89
|
+
if (mode === "write") {
|
|
90
|
+
fs.writeFileSync(target, generated);
|
|
91
|
+
return { outputPath, updated: true };
|
|
92
|
+
}
|
|
93
|
+
const current = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
|
94
|
+
if (current !== generated) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${outputPath} is stale; run node scripts/generate-agent-change-map.mjs --write`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return { outputPath, updated: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
process.argv[1] &&
|
|
104
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
105
|
+
) {
|
|
106
|
+
try {
|
|
107
|
+
const mode = process.argv.includes("--write") ? "write" : "check";
|
|
108
|
+
const report = run({ mode });
|
|
109
|
+
console.log(
|
|
110
|
+
`${report.outputPath} ${report.updated ? "generated" : "is current"}`,
|
|
111
|
+
);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { generateAgentChangeMap, run };
|
|
@@ -8,6 +8,7 @@ import { scanV4FloatingConsumerPolicy, v4ConsumerPolicyScannerRoot } from "../pa
|
|
|
8
8
|
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
9
9
|
import { V4_STAGE_CAPSULE_CONTRACT, V4_STAGE_CAPSULE_IDENTITY_CONTRACT, v4StageCapsuleIdentityRoot, v4StageCapsuleRoot, validateV4StageCapsule } from "../packages/core/v4-stage-capsule.js";
|
|
10
10
|
import { v4RuntimeResumeDocumentRoot } from "../packages/core/v4-runtime-ref-resume-authority.js";
|
|
11
|
+
import { createV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
|
|
11
12
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
12
13
|
|
|
13
14
|
const env = (name, fallback = "") => process.env[name] || fallback;
|
|
@@ -32,7 +33,20 @@ export function createReleaseCandidateStageCapsules({
|
|
|
32
33
|
const coordinateByPlatform = new Map(
|
|
33
34
|
coordinates.artifacts.map((entry) => [entry.platformId, entry]),
|
|
34
35
|
);
|
|
35
|
-
const
|
|
36
|
+
const qualificationArtifacts = passport.platformMatrix.map((platform) => {
|
|
37
|
+
const coordinate = coordinateByPlatform.get(platform.platformId);
|
|
38
|
+
if (!coordinate || coordinate.name !== platform.artifactName) throw new Error(`Stage Capsule coordinate missing for ${platform.platformId}`);
|
|
39
|
+
const manifest = readJsonFile(platform.manifestPath);
|
|
40
|
+
return { role: platform.artifactName, platform: platform.platformId, artifactRoot: coordinate.digest, manifestRoot: v4ContentRoot("stage-capsule-artifact-manifest", manifest) };
|
|
41
|
+
});
|
|
42
|
+
const qualificationExpiresAt = coordinates.artifacts.map((entry) => entry.expiresAt).sort((left, right) => Date.parse(left) - Date.parse(right))[0];
|
|
43
|
+
const qualificationReceipt = createV4PublicationQualificationReceipt({
|
|
44
|
+
repository: passport.repository, candidateRoot: `sha256:${passport.candidateHash}`,
|
|
45
|
+
sourceSha: passport.source.headSha, sourceRoot: v4ContentRoot("candidate-identity", passport.source),
|
|
46
|
+
policyDigest: passport.consumerPolicy.receiptRoot, artifacts: qualificationArtifacts,
|
|
47
|
+
issuedAt: passport.createdAt || new Date(Date.parse(qualificationExpiresAt) - 86_400_000).toISOString(), expiresAt: qualificationExpiresAt,
|
|
48
|
+
});
|
|
49
|
+
const qualificationRoot = qualificationReceipt.receiptRoot;
|
|
36
50
|
const entries = passport.platformMatrix
|
|
37
51
|
.map((platform) => {
|
|
38
52
|
const coordinate = coordinateByPlatform.get(platform.platformId);
|
|
@@ -93,6 +107,9 @@ export function createReleaseCandidateStageCapsules({
|
|
|
93
107
|
platform: platform.platformId,
|
|
94
108
|
artifactName: coordinate.name,
|
|
95
109
|
artifactDigest: coordinate.digest,
|
|
110
|
+
publicationArtifact: qualificationArtifacts.find(
|
|
111
|
+
({ platform: platformId }) => platformId === platform.platformId,
|
|
112
|
+
),
|
|
96
113
|
artifact: coordinate,
|
|
97
114
|
capsule,
|
|
98
115
|
};
|
|
@@ -112,6 +129,8 @@ export function createReleaseCandidateStageCapsules({
|
|
|
112
129
|
treeSha: passport.source.treeHash,
|
|
113
130
|
},
|
|
114
131
|
consumerPolicyReceiptRoot: passport.consumerPolicy.receiptRoot,
|
|
132
|
+
publicationQualificationRoot: qualificationRoot,
|
|
133
|
+
publicationQualificationReceipt: qualificationReceipt,
|
|
115
134
|
capsules: entries,
|
|
116
135
|
};
|
|
117
136
|
return { ...body, root: v4RuntimeResumeDocumentRoot(body) };
|
|
@@ -150,12 +169,16 @@ function writeReleaseCandidateStageCapsules({
|
|
|
150
169
|
stageCapsulesPath,
|
|
151
170
|
`${JSON.stringify(stageCapsules, null, 2)}\n`,
|
|
152
171
|
);
|
|
172
|
+
const qualificationPath = path.join(path.dirname(outputPath), "release-candidate-publication-qualification.json");
|
|
173
|
+
fs.writeFileSync(qualificationPath, `${JSON.stringify(stageCapsules.publicationQualificationReceipt, null, 2)}\n`);
|
|
153
174
|
return {
|
|
154
175
|
path: path
|
|
155
176
|
.relative(process.cwd(), stageCapsulesPath)
|
|
156
177
|
.split(path.sep)
|
|
157
178
|
.join("/"),
|
|
158
179
|
root: stageCapsules.root,
|
|
180
|
+
qualificationPath: path.relative(process.cwd(), qualificationPath).split(path.sep).join("/"),
|
|
181
|
+
qualificationRoot: stageCapsules.publicationQualificationRoot,
|
|
159
182
|
};
|
|
160
183
|
}
|
|
161
184
|
|
|
@@ -246,6 +269,8 @@ export function generateReleaseCandidatePassportCli() {
|
|
|
246
269
|
}),
|
|
247
270
|
"release-candidate-stage-capsules-path": stageCapsules.path,
|
|
248
271
|
"release-candidate-stage-capsules-root": stageCapsules.root,
|
|
272
|
+
"release-candidate-publication-qualification-path": stageCapsules.qualificationPath,
|
|
273
|
+
"release-candidate-publication-qualification-root": stageCapsules.qualificationRoot,
|
|
249
274
|
});
|
|
250
275
|
return passport;
|
|
251
276
|
}
|
|
@@ -66,10 +66,15 @@ const RELEASE_TAIL_CAPABILITIES_SCHEMA =
|
|
|
66
66
|
"schemas/release-tail-capabilities-v1.schema.json";
|
|
67
67
|
const RELEASE_TAIL_CAPABILITIES_SCHEMA_SOURCE =
|
|
68
68
|
"contracts/release-tail-capabilities-v1.schema.json";
|
|
69
|
+
const V4_PUBLICATION_QUALIFICATION_SCHEMA =
|
|
70
|
+
"schemas/v4-publication-qualification-v1.schema.json";
|
|
71
|
+
const V4_PUBLICATION_QUALIFICATION_SCHEMA_SOURCE =
|
|
72
|
+
"contracts/v4-publication-qualification-v1.schema.json";
|
|
69
73
|
const PUBLICATION_REHEARSAL_SITE_SCHEMAS = [
|
|
70
74
|
V4_PUBLICATION_REHEARSAL_SCHEMA,
|
|
71
75
|
V4_ADOPTER_DELIVERY_SCHEMA,
|
|
72
76
|
RELEASE_TAIL_CAPABILITIES_SCHEMA,
|
|
77
|
+
V4_PUBLICATION_QUALIFICATION_SCHEMA,
|
|
73
78
|
];
|
|
74
79
|
const root = path.resolve(import.meta.dirname, "..");
|
|
75
80
|
const outputDir = path.join(root, "dist", "site");
|
|
@@ -102,6 +107,7 @@ function siteFileBytes(name, value) {
|
|
|
102
107
|
if (name === DEV_DELIVERY_AUTHORITY_SCHEMA) return readText(DEV_DELIVERY_AUTHORITY_SCHEMA_SOURCE);
|
|
103
108
|
if (name === V4_COMPATIBILITY_FACTS_SCHEMA) return readText(V4_COMPATIBILITY_FACTS_SCHEMA_SOURCE);
|
|
104
109
|
if (name === V4_PUBLICATION_REHEARSAL_SCHEMA) return readText(V4_PUBLICATION_REHEARSAL_SCHEMA_SOURCE);
|
|
110
|
+
if (name === V4_PUBLICATION_QUALIFICATION_SCHEMA) return readText(V4_PUBLICATION_QUALIFICATION_SCHEMA_SOURCE);
|
|
105
111
|
if (name === V4_ADOPTER_DELIVERY_SCHEMA) return readText(V4_ADOPTER_DELIVERY_SCHEMA_SOURCE);
|
|
106
112
|
if (name === RELEASE_TAIL_CAPABILITIES_SCHEMA) return readText(RELEASE_TAIL_CAPABILITIES_SCHEMA_SOURCE);
|
|
107
113
|
return stableJson(value);
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { parseWorkflowDocument } from "../packages/core/workflow-yaml-contract.js";
|
|
2
|
+
|
|
3
|
+
function lineCount(source) {
|
|
4
|
+
const text = String(source || "");
|
|
5
|
+
if (!text) return 0;
|
|
6
|
+
const lines = text.split(/\r?\n/u);
|
|
7
|
+
return lines.at(-1) === "" ? lines.length - 1 : lines.length;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function rustTokens(source) {
|
|
11
|
+
const tokens = [];
|
|
12
|
+
let index = 0;
|
|
13
|
+
let line = 1;
|
|
14
|
+
const advance = () => {
|
|
15
|
+
if (source[index] === "\n") line += 1;
|
|
16
|
+
index += 1;
|
|
17
|
+
};
|
|
18
|
+
while (index < source.length) {
|
|
19
|
+
if (/\s/u.test(source[index])) {
|
|
20
|
+
advance();
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (source.startsWith("//", index)) {
|
|
24
|
+
while (index < source.length && source[index] !== "\n") advance();
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (source.startsWith("/*", index)) {
|
|
28
|
+
let depth = 1;
|
|
29
|
+
advance();
|
|
30
|
+
advance();
|
|
31
|
+
while (index < source.length && depth > 0) {
|
|
32
|
+
if (source.startsWith("/*", index)) {
|
|
33
|
+
depth += 1;
|
|
34
|
+
advance();
|
|
35
|
+
advance();
|
|
36
|
+
} else if (source.startsWith("*/", index)) {
|
|
37
|
+
depth -= 1;
|
|
38
|
+
advance();
|
|
39
|
+
advance();
|
|
40
|
+
} else {
|
|
41
|
+
advance();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const raw = source.slice(index).match(/^(?:br|r)(#+)?"/u);
|
|
47
|
+
if (raw) {
|
|
48
|
+
const suffix = `"${raw[1] || ""}`;
|
|
49
|
+
for (let count = 0; count < raw[0].length; count += 1) advance();
|
|
50
|
+
while (index < source.length && !source.startsWith(suffix, index)) {
|
|
51
|
+
advance();
|
|
52
|
+
}
|
|
53
|
+
for (let count = 0; count < suffix.length; count += 1) advance();
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (source[index] === '"' || source.startsWith('b"', index)) {
|
|
57
|
+
if (source[index] === "b") advance();
|
|
58
|
+
advance();
|
|
59
|
+
while (index < source.length) {
|
|
60
|
+
if (source[index] === "\\") {
|
|
61
|
+
advance();
|
|
62
|
+
if (index < source.length) advance();
|
|
63
|
+
} else if (source[index] === '"') {
|
|
64
|
+
advance();
|
|
65
|
+
break;
|
|
66
|
+
} else {
|
|
67
|
+
advance();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const character = source.slice(index).match(/^'(?:\\.|[^'\\\n])'/u);
|
|
73
|
+
if (character) {
|
|
74
|
+
for (let count = 0; count < character[0].length; count += 1) advance();
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const identifier = source.slice(index).match(/^[A-Za-z_][A-Za-z0-9_]*/u);
|
|
78
|
+
if (identifier) {
|
|
79
|
+
tokens.push({ value: identifier[0], line });
|
|
80
|
+
index += identifier[0].length;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const operator = [
|
|
84
|
+
"=>",
|
|
85
|
+
"&&",
|
|
86
|
+
"||",
|
|
87
|
+
"::",
|
|
88
|
+
"->",
|
|
89
|
+
"<=",
|
|
90
|
+
">=",
|
|
91
|
+
"==",
|
|
92
|
+
"!=",
|
|
93
|
+
].find((candidate) => source.startsWith(candidate, index));
|
|
94
|
+
tokens.push({ value: operator || source[index], line });
|
|
95
|
+
index += (operator || source[index]).length;
|
|
96
|
+
}
|
|
97
|
+
return tokens;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function rustFunctions(tokens) {
|
|
101
|
+
const functions = [];
|
|
102
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
103
|
+
if (tokens[index].value !== "fn") continue;
|
|
104
|
+
const name =
|
|
105
|
+
tokens[index + 1]?.value || `<anonymous@${tokens[index].line}>`;
|
|
106
|
+
let body = index + 2;
|
|
107
|
+
while (body < tokens.length && !["{", ";"].includes(tokens[body].value)) {
|
|
108
|
+
body += 1;
|
|
109
|
+
}
|
|
110
|
+
if (tokens[body]?.value !== "{") continue;
|
|
111
|
+
let depth = 1;
|
|
112
|
+
let end = body + 1;
|
|
113
|
+
while (end < tokens.length && depth > 0) {
|
|
114
|
+
if (tokens[end].value === "{") depth += 1;
|
|
115
|
+
if (tokens[end].value === "}") depth -= 1;
|
|
116
|
+
end += 1;
|
|
117
|
+
}
|
|
118
|
+
if (depth !== 0) continue;
|
|
119
|
+
functions.push({ name, token: index, body, end: end - 1 });
|
|
120
|
+
}
|
|
121
|
+
return functions;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isRustDecision(tokens, index) {
|
|
125
|
+
const value = tokens[index].value;
|
|
126
|
+
if (["if", "for", "while", "loop", "?", "=>"].includes(value)) return true;
|
|
127
|
+
if (!["&&", "||"].includes(value)) return false;
|
|
128
|
+
return !["=", "(", "{", "[", ",", ";", "return", "move"].includes(
|
|
129
|
+
tokens[index - 1]?.value,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function analyzeRust(_file, source) {
|
|
134
|
+
const tokens = rustTokens(source);
|
|
135
|
+
const parsed = rustFunctions(tokens);
|
|
136
|
+
const byStart = new Map(parsed.map((entry) => [entry.token, entry]));
|
|
137
|
+
const functions = parsed.map((entry) => {
|
|
138
|
+
let complexity = 1;
|
|
139
|
+
for (let index = entry.body + 1; index < entry.end; index += 1) {
|
|
140
|
+
const nested = byStart.get(index);
|
|
141
|
+
if (nested && nested.end <= entry.end) {
|
|
142
|
+
index = nested.end;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (isRustDecision(tokens, index)) complexity += 1;
|
|
146
|
+
}
|
|
147
|
+
const start = tokens[entry.token].line;
|
|
148
|
+
const end = tokens[entry.end].line;
|
|
149
|
+
return {
|
|
150
|
+
name: entry.name,
|
|
151
|
+
start,
|
|
152
|
+
end,
|
|
153
|
+
lines: end - start + 1,
|
|
154
|
+
complexity,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
lines: lineCount(source),
|
|
159
|
+
complexity: functions.reduce((total, entry) => total + entry.complexity, 0),
|
|
160
|
+
functions,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function semanticYamlLines(source) {
|
|
165
|
+
const result = [];
|
|
166
|
+
let blockIndent = null;
|
|
167
|
+
for (const [line, raw] of String(source || "")
|
|
168
|
+
.split(/\r?\n/u)
|
|
169
|
+
.entries()) {
|
|
170
|
+
const indent = raw.match(/^(\s*)/u)?.[1].length || 0;
|
|
171
|
+
if (blockIndent !== null) {
|
|
172
|
+
if (!raw.trim() || indent > blockIndent) continue;
|
|
173
|
+
blockIndent = null;
|
|
174
|
+
}
|
|
175
|
+
const text = raw.trim();
|
|
176
|
+
if (!text || text.startsWith("#")) continue;
|
|
177
|
+
result.push({ line: line + 1, indent, text });
|
|
178
|
+
if (/:\s*[>|][+-]?\s*(?:#.*)?$/u.test(raw)) blockIndent = indent;
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function analyzeWorkflow(_file, source) {
|
|
184
|
+
const lines = semanticYamlLines(source);
|
|
185
|
+
const document = parseWorkflowDocument(source);
|
|
186
|
+
const jobsEntry = lines.find(
|
|
187
|
+
(entry) => entry.indent === 0 && entry.text === "jobs:",
|
|
188
|
+
);
|
|
189
|
+
const jobs = [];
|
|
190
|
+
if (jobsEntry) {
|
|
191
|
+
const candidates = lines.filter(
|
|
192
|
+
(entry) => entry.line > jobsEntry.line && entry.indent > jobsEntry.indent,
|
|
193
|
+
);
|
|
194
|
+
const jobIndent = Math.min(...candidates.map((entry) => entry.indent));
|
|
195
|
+
for (const entry of candidates) {
|
|
196
|
+
if (
|
|
197
|
+
entry.indent === jobIndent &&
|
|
198
|
+
/^[A-Za-z0-9_.-]+:\s*(?:#.*)?$/u.test(entry.text)
|
|
199
|
+
) {
|
|
200
|
+
jobs.push(entry);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const stepsByJob = jobs.map((job, jobIndex) => {
|
|
205
|
+
const endLine = jobs[jobIndex + 1]?.line || Number.POSITIVE_INFINITY;
|
|
206
|
+
const stepRoot = lines.find(
|
|
207
|
+
(entry) =>
|
|
208
|
+
entry.line > job.line &&
|
|
209
|
+
entry.line < endLine &&
|
|
210
|
+
entry.indent > job.indent &&
|
|
211
|
+
entry.text === "steps:",
|
|
212
|
+
);
|
|
213
|
+
if (!stepRoot) return 0;
|
|
214
|
+
const stepLines = lines.filter(
|
|
215
|
+
(entry) =>
|
|
216
|
+
entry.line > stepRoot.line &&
|
|
217
|
+
entry.line < endLine &&
|
|
218
|
+
entry.indent > stepRoot.indent,
|
|
219
|
+
);
|
|
220
|
+
const stepIndent = Math.min(...stepLines.map((entry) => entry.indent));
|
|
221
|
+
return stepLines.filter(
|
|
222
|
+
(entry) => entry.indent === stepIndent && entry.text.startsWith("- "),
|
|
223
|
+
).length;
|
|
224
|
+
});
|
|
225
|
+
const decisions = lines.filter((entry) =>
|
|
226
|
+
/^(?:-\s+)?(?:if|matrix):/u.test(entry.text),
|
|
227
|
+
).length;
|
|
228
|
+
return {
|
|
229
|
+
lines: lineCount(source),
|
|
230
|
+
jobs: document.jobs.length,
|
|
231
|
+
steps: stepsByJob.reduce((total, count) => total + count, 0),
|
|
232
|
+
maxStepsPerJob: Math.max(0, ...stepsByJob),
|
|
233
|
+
decisions,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export { analyzeRust, analyzeWorkflow };
|