@kungfu-tech/buildchain 4.0.1-alpha.8 → 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.
Files changed (98) hide show
  1. package/README.md +9 -6
  2. package/actions/v4-release-candidate-promote/README.md +3 -0
  3. package/architecture/agent-change-map.md +1015 -0
  4. package/architecture/ci-lane-change-budget.json +70 -1
  5. package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
  6. package/architecture/internal-capabilities.json +156 -11
  7. package/architecture/maintainability-debt.json +3229 -0
  8. package/architecture/maintainability-policy.json +480 -129
  9. package/architecture/v3-core-mechanism-inventory.json +4 -3
  10. package/architecture/v3-v4-capability-closure.md +59 -0
  11. package/architecture/v3-v4-live-capability-inventory.json +102404 -0
  12. package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
  13. package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
  14. package/architecture/v4-runtime-semantic-closure.json +710 -0
  15. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  16. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  17. package/contracts/v4-publication-qualification-v1.schema.json +51 -0
  18. package/dist/site/agent-index.json +1 -0
  19. package/dist/site/artifact-schemas.json +2 -0
  20. package/dist/site/buildchain-contract.json +123 -32
  21. package/dist/site/buildchain-site.json +96 -28
  22. package/dist/site/capability-registry.json +6 -6
  23. package/dist/site/cli-registry.json +5 -2
  24. package/dist/site/controller-registry.json +41 -5
  25. package/dist/site/kfd-claims.json +170 -21
  26. package/dist/site/kfd-upstream-aggregate.json +1 -1
  27. package/dist/site/manual-registry.json +6 -6
  28. package/dist/site/node-api-registry.json +1474 -215
  29. package/dist/site/page-registry.json +85 -17
  30. package/dist/site/public-surface-audit.json +85 -20
  31. package/dist/site/publication-authority-registry.json +28 -1
  32. package/dist/site/publication-registry.json +4 -4
  33. package/dist/site/release-provenance.json +5 -0
  34. package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
  35. package/dist/site/site-manifest.json +10 -10
  36. package/dist/site/workflow-registry.json +94 -18
  37. package/docs/MAP.md +1 -1
  38. package/docs/cli-reference.md +4 -4
  39. package/docs/dev-delivery-warrant.md +12 -9
  40. package/docs/node-api-reference.md +291 -206
  41. package/docs/publish-transaction.md +11 -4
  42. package/docs/release-candidate.md +7 -0
  43. package/docs/v4-adopter-delivery.md +23 -4
  44. package/docs/v4-declarative-release-tail-migration.md +43 -0
  45. package/package.json +8 -3
  46. package/packages/core/adopter-delivery-passport.js +36 -0
  47. package/packages/core/buildchain-contract.js +31 -1
  48. package/packages/core/buildchain-publication-authority.js +1 -0
  49. package/packages/core/dev-delivery-warrant-settlement.js +239 -3
  50. package/packages/core/dev-delivery-warrant-state.js +15 -2
  51. package/packages/core/dev-delivery-warrant.js +21 -4
  52. package/packages/core/github-governance-authority.js +4 -4
  53. package/packages/core/index.js +1 -0
  54. package/packages/core/paper-scaffold-content.js +401 -0
  55. package/packages/core/paper.js +12 -378
  56. package/packages/core/publication-control-plane-audit.js +135 -111
  57. package/packages/core/publication-rehearsal-projection.js +104 -0
  58. package/packages/core/publication-rehearsal-runtime.js +190 -0
  59. package/packages/core/release-candidate-recovery.js +6 -5
  60. package/packages/core/release-passport.js +82 -184
  61. package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
  62. package/packages/core/v4-floating-consumer-policy.js +3 -2
  63. package/packages/core/v4-floating-consumer-release-passport.js +134 -15
  64. package/packages/core/v4-publication-qualification.js +222 -0
  65. package/scripts/artifact-signing-controller-core.mjs +1 -1
  66. package/scripts/audit-publication-control-plane.mjs +17 -17
  67. package/scripts/buildchain-cli-help.mjs +3 -0
  68. package/scripts/check-internal-architecture.mjs +23 -3
  69. package/scripts/check-inventory.mjs +23 -5
  70. package/scripts/check-maintainability.mjs +145 -4
  71. package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
  72. package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
  73. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
  74. package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
  75. package/scripts/dev-delivery-warrant-options.mjs +27 -11
  76. package/scripts/dev-delivery-warrant.mjs +45 -5
  77. package/scripts/dev-pr-auto-merge.mjs +25 -11
  78. package/scripts/dev-pr-delivery-warrant.mjs +44 -0
  79. package/scripts/dev-pr-prequeue-guard.mjs +0 -18
  80. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  81. package/scripts/generate-agent-change-map.mjs +118 -0
  82. package/scripts/generate-release-candidate-passport.mjs +26 -1
  83. package/scripts/generate-site-bundle.mjs +6 -0
  84. package/scripts/maintainability-domain-metrics.mjs +237 -0
  85. package/scripts/maintainability-governance.mjs +473 -0
  86. package/scripts/maintainability-metrics.mjs +28 -6
  87. package/scripts/project-cut-merge-queue-admission.mjs +394 -0
  88. package/scripts/promotion-routing-evidence.mjs +35 -6
  89. package/scripts/release-candidate-resolver.mjs +44 -0
  90. package/scripts/release-line-policy.mjs +25 -15
  91. package/scripts/release-tail.mjs +19 -2
  92. package/scripts/resume-from-candidate-run.mjs +106 -103
  93. package/scripts/run-lifecycle-core.mjs +341 -376
  94. package/scripts/site-capability-metadata.mjs +4 -0
  95. package/scripts/stable-candidate-qualification.mjs +9 -10
  96. package/scripts/v3-v4-capability-catalog.mjs +439 -0
  97. package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
  98. 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 qualificationRoot = passport.controllerReceipts?.[0]?.receiptDigest;
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 };