@kungfu-tech/buildchain 2.3.1-alpha.0 → 2.3.1-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.
@@ -0,0 +1,1392 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ discoverConfiguredVersionStateFiles,
8
+ getNativeDiagnosticsProfile,
9
+ getLifecycleStage,
10
+ getPublishContract,
11
+ getVersionStrategy,
12
+ loadBuildchainConfig,
13
+ loadConfiguredAnchorManifest,
14
+ validateBuildchainConfig,
15
+ } from "./buildchain-config.js";
16
+ import { detectPackageManager, getWorkspaceInfo } from "./package-manager.js";
17
+ import {
18
+ BUILDCHAIN_LOG_EVENT_CONTRACT,
19
+ readBuildchainLogEvents,
20
+ summarizeBuildchainLogEvents,
21
+ } from "./logging.js";
22
+
23
+ export const BUILDCHAIN_DIAGNOSTICS_CONTRACT = "kungfu-buildchain-diagnostics";
24
+ export const BUILDCHAIN_LIFECYCLE_OBSERVABILITY_CONTRACT =
25
+ "kungfu-buildchain-lifecycle-observability";
26
+ export const BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT =
27
+ "kungfu-buildchain-anchored-package-release-validation";
28
+ export const BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT =
29
+ "kungfu-buildchain-diagnostics-manifest";
30
+ export const BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT =
31
+ "kungfu-buildchain-diagnostics-summary";
32
+ export const BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT =
33
+ "kungfu-buildchain-process-sample-report";
34
+ export const BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT =
35
+ "kungfu-buildchain-process-sample-summary";
36
+
37
+ const DEFAULT_SECRET_KEY_PATTERN =
38
+ /(authorization|cookie|credential|password|passwd|private[_-]?key|secret|token|api[_-]?key)/i;
39
+
40
+ function posixPath(value) {
41
+ return String(value || "").split(path.sep).join("/");
42
+ }
43
+
44
+ function safeReadJson(filePath) {
45
+ try {
46
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ function readJsonResult(filePath) {
53
+ try {
54
+ return { value: JSON.parse(fs.readFileSync(filePath, "utf8")) };
55
+ } catch (error) {
56
+ return { error };
57
+ }
58
+ }
59
+
60
+ function sha256File(filePath) {
61
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
62
+ }
63
+
64
+ function fileStats(cwd, entries = []) {
65
+ return entries.map((entry) => {
66
+ const filePath = path.resolve(cwd, entry);
67
+ if (!fs.existsSync(filePath)) {
68
+ return { path: posixPath(entry), exists: false };
69
+ }
70
+ const stat = fs.statSync(filePath);
71
+ return {
72
+ path: posixPath(entry),
73
+ exists: true,
74
+ type: stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "other",
75
+ bytes: stat.size,
76
+ };
77
+ });
78
+ }
79
+
80
+ function commandVersion(command, args = ["--version"], cwd = process.cwd()) {
81
+ try {
82
+ return execFileSync(command, args, {
83
+ cwd,
84
+ encoding: "utf8",
85
+ stdio: ["ignore", "pipe", "ignore"],
86
+ timeout: 5000,
87
+ }).trim().split(/\r?\n/)[0] || "";
88
+ } catch {
89
+ return "";
90
+ }
91
+ }
92
+
93
+ function defaultDiagnosticCommandRunner(command, args = [], { cwd = process.cwd(), timeoutMs = 5000 } = {}) {
94
+ return execFileSync(command, args, {
95
+ cwd,
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ timeout: timeoutMs,
99
+ });
100
+ }
101
+
102
+ function gitField(cwd, args) {
103
+ try {
104
+ return execFileSync("git", args, {
105
+ cwd,
106
+ encoding: "utf8",
107
+ stdio: ["ignore", "pipe", "ignore"],
108
+ timeout: 5000,
109
+ }).trim();
110
+ } catch {
111
+ return "";
112
+ }
113
+ }
114
+
115
+ function safeAnchorManifest(cwd, loadedConfig) {
116
+ try {
117
+ return loadConfiguredAnchorManifest(cwd, loadedConfig);
118
+ } catch (error) {
119
+ return { error: error.message };
120
+ }
121
+ }
122
+
123
+ function check(ok, id, message, details = {}) {
124
+ return { id, status: ok ? "pass" : "fail", message, details };
125
+ }
126
+
127
+ function normalizePublishSourceRef(value = "") {
128
+ return String(value || "")
129
+ .trim()
130
+ .replace(/^refs\/heads\//, "")
131
+ .replace(/^refs\/tags\//, "");
132
+ }
133
+
134
+ function parsePublishGateSourceRef(value = "") {
135
+ const sourceRef = normalizePublishSourceRef(value);
136
+ if (sourceRef === "publish-gate/major") {
137
+ return { sourceRef, channel: "major", line: "", consumerVersion: "" };
138
+ }
139
+ if (sourceRef === "publish-gate/anchor") {
140
+ return { sourceRef, channel: "anchor", line: "", consumerVersion: "" };
141
+ }
142
+ const match = sourceRef.match(/^publish-gate\/(alpha|release)\/(.+)\/([^/]+)$/);
143
+ if (!match) {
144
+ return { sourceRef, channel: "", line: "", consumerVersion: "" };
145
+ }
146
+ return {
147
+ sourceRef,
148
+ channel: match[1],
149
+ line: match[2],
150
+ consumerVersion: match[3],
151
+ };
152
+ }
153
+
154
+ function versionFileValue(file) {
155
+ if (file.type === "json" || file.type === "toml") {
156
+ return String(file.key || "")
157
+ .split(".")
158
+ .reduce((current, segment) => current?.[segment], file.content);
159
+ }
160
+ return file.source.match(file.pattern)?.groups?.version;
161
+ }
162
+
163
+ function defaultPublishSourceFromEnv(env = process.env) {
164
+ return {
165
+ sourceRef: env.BUILDCHAIN_PUBLISH_SOURCE_REF || "",
166
+ sourceSha: env.BUILDCHAIN_PUBLISH_SOURCE_SHA || "",
167
+ sourceLocked: env.BUILDCHAIN_PUBLISH_SOURCE_LOCKED || "",
168
+ channel: env.BUILDCHAIN_PUBLISH_SOURCE_CHANNEL || "",
169
+ consumerVersion: env.BUILDCHAIN_PUBLISH_SOURCE_CONSUMER_VERSION || "",
170
+ };
171
+ }
172
+
173
+ function lifecycleTimingFromEvents(events = []) {
174
+ const stages = {};
175
+ const spans = [];
176
+ let warningCount = 0;
177
+ let errorCount = 0;
178
+ for (const event of events) {
179
+ if (event.level === "warn") {
180
+ warningCount += 1;
181
+ }
182
+ if (event.level === "error") {
183
+ errorCount += 1;
184
+ }
185
+ const stage = event.attributes?.stage || event.phase || "unknown";
186
+ const durationMs = Number(event.durationMs || 0);
187
+ if (durationMs > 0) {
188
+ stages[stage] = stages[stage] || { durationMs: 0, eventCount: 0 };
189
+ stages[stage].durationMs += durationMs;
190
+ spans.push({
191
+ event: event.event,
192
+ source: event.source || "",
193
+ component: event.component || "",
194
+ stage,
195
+ phase: event.phase || "",
196
+ durationMs,
197
+ });
198
+ }
199
+ if (stage) {
200
+ stages[stage] = stages[stage] || { durationMs: 0, eventCount: 0 };
201
+ stages[stage].eventCount += 1;
202
+ }
203
+ }
204
+ return {
205
+ stages: Object.fromEntries(Object.entries(stages).sort(([left], [right]) => left.localeCompare(right))),
206
+ topSlowSpans: spans.sort((left, right) => right.durationMs - left.durationMs).slice(0, 10),
207
+ warningCount,
208
+ errorCount,
209
+ };
210
+ }
211
+
212
+ export function summarizeLifecycleObservability({
213
+ events = [],
214
+ logPath = "",
215
+ artifactScanDurationMs = 0,
216
+ artifactUploadDurationMs = 0,
217
+ totalBytes = 0,
218
+ fileCount = 0,
219
+ } = {}) {
220
+ const resolvedEvents = events.length ? events : readBuildchainLogEvents(logPath);
221
+ const timing = lifecycleTimingFromEvents(resolvedEvents);
222
+ return {
223
+ schemaVersion: 1,
224
+ contract: BUILDCHAIN_LIFECYCLE_OBSERVABILITY_CONTRACT,
225
+ log: {
226
+ contract: BUILDCHAIN_LOG_EVENT_CONTRACT,
227
+ path: logPath ? posixPath(logPath) : "",
228
+ summary: summarizeBuildchainLogEvents(resolvedEvents),
229
+ },
230
+ stages: timing.stages,
231
+ artifactScan: { durationMs: Math.round(Number(artifactScanDurationMs || 0)) },
232
+ artifactUpload: { durationMs: Math.round(Number(artifactUploadDurationMs || 0)) },
233
+ totalBytes: Number(totalBytes || 0),
234
+ fileCount: Number(fileCount || 0),
235
+ topSlowSpans: timing.topSlowSpans,
236
+ warningCount: timing.warningCount,
237
+ errorCount: timing.errorCount,
238
+ };
239
+ }
240
+
241
+ export function collectBuildchainDiagnostics({ cwd = process.cwd(), artifactPaths = [] } = {}) {
242
+ const resolvedCwd = path.resolve(cwd);
243
+ const loadedConfig = loadBuildchainConfig(resolvedCwd);
244
+ let configSummary = {};
245
+ try {
246
+ configSummary = validateBuildchainConfig(resolvedCwd);
247
+ } catch (error) {
248
+ configSummary = { ok: false, error: error.message };
249
+ }
250
+ return {
251
+ config: loadedConfig
252
+ ? {
253
+ path: loadedConfig.path,
254
+ project: loadedConfig.config.project || {},
255
+ versionStrategy: getVersionStrategy(loadedConfig),
256
+ publishContract: getPublishContract(loadedConfig),
257
+ lifecycleStages: Object.keys(loadedConfig.config.lifecycle || {}),
258
+ diagnostics: {
259
+ native: getNativeDiagnosticsProfile(loadedConfig),
260
+ },
261
+ anchorManifest: safeAnchorManifest(resolvedCwd, loadedConfig),
262
+ validation: configSummary,
263
+ }
264
+ : { path: "", validation: configSummary },
265
+ lifecycle: Object.fromEntries(
266
+ ["install", "build", "verify", "publish"]
267
+ .map((stage) => [stage, getLifecycleStage(loadedConfig, stage)])
268
+ .filter(([, value]) => Boolean(value)),
269
+ ),
270
+ artifactPaths: fileStats(resolvedCwd, artifactPaths),
271
+ };
272
+ }
273
+
274
+ function selectedCompilerCacheDiagnostics({ cwd, compilerCache, runCommand }) {
275
+ if (compilerCache === "none") {
276
+ return {};
277
+ }
278
+ const diagnostics = collectCompilerCacheDiagnostics({ cwd, runCommand });
279
+ if (compilerCache === "ccache") {
280
+ return { ccache: diagnostics.ccache };
281
+ }
282
+ if (compilerCache === "sccache") {
283
+ return { sccache: diagnostics.sccache };
284
+ }
285
+ return diagnostics;
286
+ }
287
+
288
+ export function collectNativeDiagnostics({
289
+ cwd = process.cwd(),
290
+ profile = undefined,
291
+ runCommand = defaultDiagnosticCommandRunner,
292
+ } = {}) {
293
+ const resolvedCwd = path.resolve(cwd);
294
+ const nativeProfile = profile || getNativeDiagnosticsProfile(loadBuildchainConfig(resolvedCwd));
295
+ if (!nativeProfile.enabled) {
296
+ return {
297
+ enabled: false,
298
+ profile: nativeProfile,
299
+ };
300
+ }
301
+ const expectedTools = nativeProfile.expectedTools.length
302
+ ? nativeProfile.expectedTools
303
+ : ["node", "pnpm", "npm", "git", "cmake", "ninja", "ccache", "sccache"];
304
+ return {
305
+ enabled: true,
306
+ profile: nativeProfile,
307
+ tools: collectToolDiagnostics({ cwd: resolvedCwd, tools: expectedTools }),
308
+ compilerCaches: selectedCompilerCacheDiagnostics({
309
+ cwd: resolvedCwd,
310
+ compilerCache: nativeProfile.compilerCache,
311
+ runCommand,
312
+ }),
313
+ artifactDirs: fileStats(resolvedCwd, nativeProfile.artifactDirs),
314
+ cacheDirs: fileStats(resolvedCwd, nativeProfile.cacheDirs),
315
+ };
316
+ }
317
+
318
+ export function validateAnchoredPackageRelease({
319
+ cwd = process.cwd(),
320
+ requireManifest = true,
321
+ requirePackageSetOrder = "platforms-first-main-last",
322
+ requireTrustedPublishing = true,
323
+ requireLifecycleStages = ["install", "build", "verify", "publish"],
324
+ requirePublishGateSourceLock = false,
325
+ publishSource = undefined,
326
+ env = process.env,
327
+ } = {}) {
328
+ const resolvedCwd = path.resolve(cwd);
329
+ const checks = [];
330
+ let versionFiles = [];
331
+ let loadedConfig;
332
+ try {
333
+ loadedConfig = loadBuildchainConfig(resolvedCwd);
334
+ checks.push(check(Boolean(loadedConfig), "config.load", "buildchain.toml is readable"));
335
+ } catch (error) {
336
+ checks.push(check(false, "config.load", error.message));
337
+ }
338
+ const versionStrategy = getVersionStrategy(loadedConfig);
339
+ checks.push(check(
340
+ versionStrategy.strategy === "anchored",
341
+ "version.strategy",
342
+ "version strategy is anchored",
343
+ versionStrategy,
344
+ ));
345
+ checks.push(check(
346
+ versionStrategy.next === "manual",
347
+ "version.next",
348
+ "next version strategy is manual",
349
+ versionStrategy,
350
+ ));
351
+ const anchorManifest = safeAnchorManifest(resolvedCwd, loadedConfig);
352
+ checks.push(check(
353
+ !requireManifest || (anchorManifest && !anchorManifest.error),
354
+ "version.manifest",
355
+ anchorManifest?.error || "anchor manifest is readable",
356
+ { path: anchorManifest?.path || versionStrategy.manifest || "" },
357
+ ));
358
+ try {
359
+ versionFiles = discoverConfiguredVersionStateFiles(resolvedCwd, loadedConfig);
360
+ checks.push(check(versionFiles.length > 0, "version.files", "configured version files are readable", {
361
+ files: versionFiles.map((file) => file.path),
362
+ }));
363
+ } catch (error) {
364
+ checks.push(check(false, "version.files", error.message));
365
+ }
366
+ const publish = getPublishContract(loadedConfig) || {};
367
+ checks.push(check(
368
+ publish.mode === "publish-final-version",
369
+ "publish.mode",
370
+ "publish mode is compatible with anchored final-version release",
371
+ { mode: publish.mode || "" },
372
+ ));
373
+ if (requireTrustedPublishing) {
374
+ checks.push(check(
375
+ publish.auth === "trusted-publishing",
376
+ "publish.auth",
377
+ "trusted publishing is enabled",
378
+ { auth: publish.auth || "" },
379
+ ));
380
+ }
381
+ if (requirePackageSetOrder) {
382
+ checks.push(check(
383
+ publish.packageSetOrder === requirePackageSetOrder,
384
+ "publish.package_set_order",
385
+ `package set order is ${requirePackageSetOrder}`,
386
+ { packageSetOrder: publish.packageSetOrder || "" },
387
+ ));
388
+ }
389
+ for (const stage of requireLifecycleStages || []) {
390
+ checks.push(check(
391
+ Boolean(getLifecycleStage(loadedConfig, stage)),
392
+ `lifecycle.${stage}`,
393
+ `lifecycle stage is configured: ${stage}`,
394
+ ));
395
+ }
396
+ const source = publishSource || defaultPublishSourceFromEnv(env);
397
+ const parsedSource = parsePublishGateSourceRef(source.sourceRef);
398
+ if (requirePublishGateSourceLock) {
399
+ const normalizedLocked = String(source.sourceLocked || "").toLowerCase();
400
+ const lockEnabled = normalizedLocked === "true" || normalizedLocked === "1" || source.sourceLocked === true;
401
+ checks.push(check(
402
+ lockEnabled,
403
+ "publish.source_locked",
404
+ "publish source lock is required before package publication",
405
+ { sourceLocked: source.sourceLocked || "" },
406
+ ));
407
+ checks.push(check(
408
+ Boolean(parsedSource.channel) && parsedSource.channel !== "anchor",
409
+ "publish.source_ref",
410
+ "publish source ref uses publish-gate/{alpha,release,major}",
411
+ { sourceRef: parsedSource.sourceRef || source.sourceRef || "", channel: parsedSource.channel || "" },
412
+ ));
413
+ checks.push(check(
414
+ /^[0-9a-f]{40}$/i.test(String(source.sourceSha || "")),
415
+ "publish.source_sha",
416
+ "publish source SHA is a 40-character Git SHA",
417
+ { sourceSha: source.sourceSha || "" },
418
+ ));
419
+ const consumerVersion = parsedSource.consumerVersion || source.consumerVersion || "";
420
+ if (consumerVersion) {
421
+ const mismatches = versionFiles
422
+ .map((file) => ({ path: file.path, version: versionFileValue(file) || "" }))
423
+ .filter((file) => file.version !== consumerVersion);
424
+ checks.push(check(
425
+ mismatches.length === 0,
426
+ "publish.source_version",
427
+ `publish source consumer version matches configured version files: ${consumerVersion}`,
428
+ { consumerVersion, mismatches },
429
+ ));
430
+ checks.push(check(
431
+ !anchorManifest?.fields?.npmVersion || anchorManifest.fields.npmVersion === consumerVersion,
432
+ "publish.anchor_version",
433
+ `anchor manifest npmVersion matches publish source version: ${consumerVersion}`,
434
+ { consumerVersion, npmVersion: anchorManifest?.fields?.npmVersion || "" },
435
+ ));
436
+ }
437
+ }
438
+ return {
439
+ schemaVersion: 1,
440
+ contract: BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT,
441
+ cwd: resolvedCwd,
442
+ ok: checks.every((entry) => entry.status === "pass"),
443
+ checks,
444
+ summary: {
445
+ versionStrategy,
446
+ anchorManifest: anchorManifest?.path || "",
447
+ publish,
448
+ publishSource: {
449
+ sourceRef: parsedSource.sourceRef || source.sourceRef || "",
450
+ sourceSha: source.sourceSha || "",
451
+ sourceLocked: source.sourceLocked || "",
452
+ channel: parsedSource.channel || source.channel || "",
453
+ line: parsedSource.line || "",
454
+ consumerVersion: parsedSource.consumerVersion || source.consumerVersion || "",
455
+ },
456
+ requiredLifecycleStages: requireLifecycleStages,
457
+ },
458
+ };
459
+ }
460
+
461
+ export function collectRunnerDiagnostics() {
462
+ return {
463
+ os: {
464
+ platform: os.platform(),
465
+ release: os.release(),
466
+ arch: os.arch(),
467
+ type: os.type(),
468
+ },
469
+ cpu: {
470
+ logicalCount: os.cpus().length,
471
+ model: os.cpus()[0]?.model || "",
472
+ loadAverage: os.loadavg(),
473
+ },
474
+ memory: {
475
+ totalBytes: os.totalmem(),
476
+ freeBytes: os.freemem(),
477
+ },
478
+ uptimeSeconds: Math.round(os.uptime()),
479
+ github: {
480
+ actions: process.env.GITHUB_ACTIONS === "true",
481
+ runnerOs: process.env.RUNNER_OS || "",
482
+ runnerArch: process.env.RUNNER_ARCH || "",
483
+ runnerName: process.env.RUNNER_NAME || "",
484
+ },
485
+ };
486
+ }
487
+
488
+ export function collectToolDiagnostics({ cwd = process.cwd(), tools = ["node", "pnpm", "npm", "git", "cmake", "ninja", "ccache", "sccache"] } = {}) {
489
+ return Object.fromEntries(tools.map((tool) => [tool, { version: commandVersion(tool, ["--version"], cwd) }]));
490
+ }
491
+
492
+ function parseJsonDiagnostics(value = "") {
493
+ try {
494
+ return JSON.parse(String(value || "").trim());
495
+ } catch {
496
+ return undefined;
497
+ }
498
+ }
499
+
500
+ function collectCompilerCacheTool({ command, args, cwd, runCommand }) {
501
+ try {
502
+ const output = runCommand(command, args, { cwd, timeoutMs: 5000 });
503
+ const text = String(output || "").trim();
504
+ const stats = parseJsonDiagnostics(text);
505
+ return {
506
+ available: true,
507
+ command,
508
+ format: stats ? "json" : "text",
509
+ stats: stats || {},
510
+ rawBytes: Buffer.byteLength(text, "utf8"),
511
+ parseError: stats ? "" : "stats output was not JSON",
512
+ };
513
+ } catch (error) {
514
+ return {
515
+ available: false,
516
+ command,
517
+ error: error?.code || error?.message || "stats command failed",
518
+ };
519
+ }
520
+ }
521
+
522
+ export function collectCompilerCacheDiagnostics({
523
+ cwd = process.cwd(),
524
+ runCommand = defaultDiagnosticCommandRunner,
525
+ } = {}) {
526
+ const resolvedCwd = path.resolve(cwd);
527
+ return {
528
+ ccache: collectCompilerCacheTool({
529
+ command: "ccache",
530
+ args: ["--show-stats", "--json"],
531
+ cwd: resolvedCwd,
532
+ runCommand,
533
+ }),
534
+ sccache: collectCompilerCacheTool({
535
+ command: "sccache",
536
+ args: ["--show-stats", "--stats-format", "json"],
537
+ cwd: resolvedCwd,
538
+ runCommand,
539
+ }),
540
+ };
541
+ }
542
+
543
+ export function collectCacheDiagnostics({ cwd = process.cwd(), cacheDirs = [], runCommand = defaultDiagnosticCommandRunner } = {}) {
544
+ const resolvedCwd = path.resolve(cwd);
545
+ return {
546
+ packageManager: (() => {
547
+ try {
548
+ return detectPackageManager(resolvedCwd);
549
+ } catch (error) {
550
+ return { name: "unknown", error: error.message };
551
+ }
552
+ })(),
553
+ workspace: (() => {
554
+ try {
555
+ return getWorkspaceInfo(resolvedCwd);
556
+ } catch (error) {
557
+ return { error: error.message };
558
+ }
559
+ })(),
560
+ dirs: fileStats(resolvedCwd, cacheDirs),
561
+ compilerCaches: collectCompilerCacheDiagnostics({ cwd: resolvedCwd, runCommand }),
562
+ };
563
+ }
564
+
565
+ export function collectGitDiagnostics({ cwd = process.cwd() } = {}) {
566
+ return {
567
+ repository: gitField(cwd, ["config", "--get", "remote.origin.url"]),
568
+ head: gitField(cwd, ["rev-parse", "HEAD"]),
569
+ ref: gitField(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]),
570
+ dirty: gitField(cwd, ["status", "--short"]) !== "",
571
+ };
572
+ }
573
+
574
+ export function redactDiagnosticsValue(key, value, pattern = DEFAULT_SECRET_KEY_PATTERN) {
575
+ if (pattern.test(String(key || ""))) {
576
+ return "[REDACTED]";
577
+ }
578
+ return value;
579
+ }
580
+
581
+ export function collectProcessTreeSnapshot({ rootPid = process.pid, cwd = process.cwd() } = {}) {
582
+ const pid = String(rootPid || process.pid);
583
+ if (process.platform === "win32") {
584
+ return { rootPid: pid, platform: process.platform, processes: [] };
585
+ }
586
+ let lines = [];
587
+ try {
588
+ lines = execFileSync("ps", ["-axo", "pid=,ppid=,pcpu=,comm=,args="], {
589
+ cwd,
590
+ encoding: "utf8",
591
+ stdio: ["ignore", "pipe", "ignore"],
592
+ timeout: 5000,
593
+ }).split(/\r?\n/).filter(Boolean);
594
+ } catch {
595
+ return { rootPid: pid, platform: process.platform, processes: [] };
596
+ }
597
+ const rows = lines.map((line) => {
598
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+([0-9.]+)\s+(\S+)(?:\s+(.*))?$/);
599
+ const args = match?.[5] || "";
600
+ return match
601
+ ? {
602
+ pid: match[1],
603
+ ppid: match[2],
604
+ cpu: Number(match[3]),
605
+ command: path.basename(match[4]),
606
+ commandLine: redactCommandLine(args),
607
+ }
608
+ : undefined;
609
+ }).filter(Boolean);
610
+ const byParent = new Map();
611
+ for (const row of rows) {
612
+ byParent.set(row.ppid, [...(byParent.get(row.ppid) || []), row]);
613
+ }
614
+ const seen = new Set();
615
+ const stack = [pid];
616
+ const processes = [];
617
+ while (stack.length) {
618
+ const current = stack.pop();
619
+ for (const child of byParent.get(current) || []) {
620
+ if (seen.has(child.pid)) {
621
+ continue;
622
+ }
623
+ seen.add(child.pid);
624
+ processes.push(child);
625
+ stack.push(child.pid);
626
+ }
627
+ }
628
+ return { rootPid: pid, platform: process.platform, processes };
629
+ }
630
+
631
+ export function classifyProcessCommand(command = "") {
632
+ const name = path.basename(String(command || "")).toLowerCase();
633
+ if (/^(ccache|sccache)$/.test(name)) {
634
+ return "cache";
635
+ }
636
+ if (/^(clang|clang\+\+|gcc|g\+\+|cc|c\+\+|cl)(\.exe)?$/.test(name)) {
637
+ return "compiler";
638
+ }
639
+ if (/^(ld|lld|lld-link|link)(\.exe)?$/.test(name)) {
640
+ return "linker";
641
+ }
642
+ if (/^(ar|llvm-ar|libtool|ranlib)(\.exe)?$/.test(name)) {
643
+ return "archive";
644
+ }
645
+ if (/^(make|ninja|cmake|msbuild|vcbuild|gyp-mac-tool)(\.exe)?$/.test(name)) {
646
+ return "build-tool";
647
+ }
648
+ if (/^(node|python|python3|bash|sh|pwsh|powershell)(\.exe)?$/.test(name)) {
649
+ return "script";
650
+ }
651
+ return "other";
652
+ }
653
+
654
+ function firstPositiveInteger(value) {
655
+ const number = Number(value);
656
+ return Number.isInteger(number) && number > 0 ? number : 0;
657
+ }
658
+
659
+ function commandName(value = "") {
660
+ return path.basename(String(value || "").replace(/[()]/g, "").replace(/\.exe$/i, "")).toLowerCase();
661
+ }
662
+
663
+ function detectParallelismFromTokens(tokens = []) {
664
+ for (let index = 0; index < tokens.length; index += 1) {
665
+ const token = String(tokens[index] || "");
666
+ const inlineMatch = token.match(/^(?:-j|--jobs=|--parallel=|-jobs|\/m:|\/maxcpucount:|-m:)(\d+)$/i);
667
+ if (inlineMatch) {
668
+ return { value: Number(inlineMatch[1]), source: "command", token };
669
+ }
670
+ if (/^(?:-j|--jobs|--parallel|-jobs)$/i.test(token)) {
671
+ const value = firstPositiveInteger(tokens[index + 1]);
672
+ if (value) {
673
+ return { value, source: "command", token: `${token} ${tokens[index + 1]}` };
674
+ }
675
+ }
676
+ }
677
+ return { value: 0, source: "", token: "" };
678
+ }
679
+
680
+ function shellishTokens(command = "") {
681
+ return String(command || "").match(/"[^"]+"|'[^']+'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) || [];
682
+ }
683
+
684
+ function redactCommandLine(commandLine = "", pattern = DEFAULT_SECRET_KEY_PATTERN) {
685
+ const tokens = shellishTokens(commandLine);
686
+ let redactNext = false;
687
+ return tokens.map((token) => {
688
+ const withUrlUserInfo = token.replace(/:\/\/[^/\s:@]+:[^/\s@]+@/g, "://[REDACTED]@");
689
+ if (redactNext) {
690
+ redactNext = false;
691
+ return "[REDACTED]";
692
+ }
693
+ const assignment = withUrlUserInfo.match(/^([^=\s]+)=(.*)$/);
694
+ if (assignment && pattern.test(assignment[1])) {
695
+ return `${assignment[1]}=[REDACTED]`;
696
+ }
697
+ const inlineOption = withUrlUserInfo.match(/^(--?[^=\s]+)=(.*)$/);
698
+ if (inlineOption && pattern.test(inlineOption[1])) {
699
+ return `${inlineOption[1]}=[REDACTED]`;
700
+ }
701
+ if (/^--?/.test(withUrlUserInfo) && pattern.test(withUrlUserInfo)) {
702
+ redactNext = true;
703
+ }
704
+ return withUrlUserInfo;
705
+ }).join(" ");
706
+ }
707
+
708
+ function commandLineFromProcess(entry = {}) {
709
+ return [
710
+ entry.commandLine,
711
+ Array.isArray(entry.args) ? entry.args.join(" ") : entry.args,
712
+ Array.isArray(entry.argv) ? entry.argv.join(" ") : "",
713
+ entry.command,
714
+ ].find((value) => String(value || "").trim()) || "";
715
+ }
716
+
717
+ function parallelismCandidate({ value, source, token, entry, sample }) {
718
+ return {
719
+ value,
720
+ source,
721
+ token,
722
+ pid: entry?.pid ? String(entry.pid) : "",
723
+ command: commandName(entry?.command || shellishTokens(commandLineFromProcess(entry))[0] || ""),
724
+ commandLine: commandLineFromProcess(entry),
725
+ timestamp: sample?.timestamp || "",
726
+ };
727
+ }
728
+
729
+ export function detectRequestedParallelism({
730
+ command = "",
731
+ args = [],
732
+ env = process.env,
733
+ } = {}) {
734
+ const commandTokens = [
735
+ ...shellishTokens(command),
736
+ ...(Array.isArray(args) ? args.map((entry) => String(entry)) : shellishTokens(args)),
737
+ ];
738
+ const commandParallelism = detectParallelismFromTokens(commandTokens);
739
+ if (commandParallelism.value) {
740
+ return commandParallelism;
741
+ }
742
+
743
+ const explicitEnvKeys = [
744
+ "BUILDCHAIN_REQUESTED_PARALLELISM",
745
+ "CMAKE_BUILD_PARALLEL_LEVEL",
746
+ "NINJA_STATUS_JOBS",
747
+ "npm_config_jobs",
748
+ ];
749
+ for (const key of explicitEnvKeys) {
750
+ const value = firstPositiveInteger(env?.[key]);
751
+ if (value) {
752
+ return { value, source: `env:${key}`, token: key };
753
+ }
754
+ }
755
+
756
+ const makeflags = detectParallelismFromTokens(shellishTokens(env?.MAKEFLAGS || ""));
757
+ if (makeflags.value) {
758
+ return { ...makeflags, source: "env:MAKEFLAGS" };
759
+ }
760
+
761
+ return { value: 0, source: "", token: "" };
762
+ }
763
+
764
+ export function detectRequestedParallelismFromProcessSamples(samples = []) {
765
+ const candidates = [];
766
+ const buildToolNames = new Set(["make", "ninja", "cmake", "msbuild", "xcodebuild"]);
767
+ for (const sample of Array.isArray(samples) ? samples : []) {
768
+ for (const entry of Array.isArray(sample?.processes) ? sample.processes : []) {
769
+ const command = commandName(entry.command || shellishTokens(commandLineFromProcess(entry))[0] || "");
770
+ if (!buildToolNames.has(command)) {
771
+ continue;
772
+ }
773
+ const tokens = shellishTokens(commandLineFromProcess(entry));
774
+ const detected = detectParallelismFromTokens(tokens);
775
+ if (detected.value) {
776
+ candidates.push(parallelismCandidate({
777
+ ...detected,
778
+ source: "process-tree",
779
+ entry,
780
+ sample,
781
+ }));
782
+ }
783
+ }
784
+ }
785
+ candidates.sort((left, right) => right.value - left.value || left.command.localeCompare(right.command));
786
+ return {
787
+ value: candidates[0]?.value || 0,
788
+ source: candidates[0] ? "process-tree" : "",
789
+ token: candidates[0]?.token || "",
790
+ evidence: candidates[0] || undefined,
791
+ candidates,
792
+ };
793
+ }
794
+
795
+ export function summarizeProcessSamples({
796
+ samples = [],
797
+ requestedParallelism = 0,
798
+ command = "",
799
+ args = [],
800
+ env = process.env,
801
+ activeCpuThreshold = 0.1,
802
+ } = {}) {
803
+ const normalizedSamples = Array.isArray(samples) ? samples : [];
804
+ const activeCounts = [];
805
+ const cpuTotals = [];
806
+ const categoryMax = {};
807
+ const commandStats = new Map();
808
+ const threshold = Number(activeCpuThreshold || 0);
809
+
810
+ for (const sample of normalizedSamples) {
811
+ const processes = Array.isArray(sample?.processes) ? sample.processes : [];
812
+ const active = processes.filter((entry) => Number(entry.cpu || 0) >= threshold);
813
+ activeCounts.push(active.length);
814
+ cpuTotals.push(processes.reduce((sum, entry) => sum + Number(entry.cpu || 0), 0));
815
+
816
+ const categoryCounts = {};
817
+ for (const entry of active) {
818
+ const command = path.basename(String(entry.command || "unknown"));
819
+ const category = classifyProcessCommand(command);
820
+ categoryCounts[category] = (categoryCounts[category] || 0) + 1;
821
+ const stats = commandStats.get(command) || {
822
+ command,
823
+ category,
824
+ samples: 0,
825
+ maxConcurrent: 0,
826
+ maxCpu: 0,
827
+ };
828
+ stats.samples += 1;
829
+ stats.maxCpu = Math.max(stats.maxCpu, Number(entry.cpu || 0));
830
+ commandStats.set(command, stats);
831
+ }
832
+
833
+ const commandCounts = {};
834
+ for (const entry of active) {
835
+ const command = path.basename(String(entry.command || "unknown"));
836
+ commandCounts[command] = (commandCounts[command] || 0) + 1;
837
+ }
838
+ for (const [command, count] of Object.entries(commandCounts)) {
839
+ const stats = commandStats.get(command);
840
+ if (stats) {
841
+ stats.maxConcurrent = Math.max(stats.maxConcurrent, count);
842
+ }
843
+ }
844
+ for (const [category, count] of Object.entries(categoryCounts)) {
845
+ categoryMax[category] = Math.max(categoryMax[category] || 0, count);
846
+ }
847
+ }
848
+
849
+ const observedMax = activeCounts.length ? Math.max(...activeCounts) : 0;
850
+ const observedAverage = activeCounts.length
851
+ ? activeCounts.reduce((sum, value) => sum + value, 0) / activeCounts.length
852
+ : 0;
853
+ const totalCpuMax = cpuTotals.length ? Math.max(...cpuTotals) : 0;
854
+ const totalCpuAverage = cpuTotals.length
855
+ ? cpuTotals.reduce((sum, value) => sum + value, 0) / cpuTotals.length
856
+ : 0;
857
+ const explicitRequested = firstPositiveInteger(requestedParallelism);
858
+ const commandParallelism = detectRequestedParallelism({ command, args, env });
859
+ const sampledParallelism = detectRequestedParallelismFromProcessSamples(normalizedSamples);
860
+ const requested = explicitRequested || sampledParallelism.value || commandParallelism.value;
861
+ const requestedSource = explicitRequested
862
+ ? "explicit"
863
+ : sampledParallelism.source || commandParallelism.source;
864
+ const requestedEvidence = explicitRequested
865
+ ? { value: explicitRequested, source: "explicit" }
866
+ : sampledParallelism.evidence || (commandParallelism.value ? commandParallelism : undefined);
867
+
868
+ return {
869
+ schemaVersion: 1,
870
+ contract: BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT,
871
+ sampleCount: normalizedSamples.length,
872
+ activeCpuThreshold: threshold,
873
+ requestedParallelism: requested,
874
+ requestedParallelismSource: requestedSource,
875
+ requestedParallelismEvidence: requestedEvidence || {},
876
+ requestedParallelismCandidates: sampledParallelism.candidates.slice(0, 10),
877
+ observedConcurrency: {
878
+ max: observedMax,
879
+ average: Number(observedAverage.toFixed(2)),
880
+ ratioToRequestedMax: requested > 0 ? Number((observedMax / requested).toFixed(3)) : 0,
881
+ },
882
+ cpu: {
883
+ totalMax: Number(totalCpuMax.toFixed(2)),
884
+ totalAverage: Number(totalCpuAverage.toFixed(2)),
885
+ },
886
+ categories: Object.fromEntries(Object.entries(categoryMax).sort(([left], [right]) => left.localeCompare(right))),
887
+ topCommands: [...commandStats.values()]
888
+ .sort((left, right) => right.maxConcurrent - left.maxConcurrent || right.maxCpu - left.maxCpu)
889
+ .slice(0, 10),
890
+ };
891
+ }
892
+
893
+ export function startProcessSampler({
894
+ rootPid = process.pid,
895
+ intervalMs = 15000,
896
+ label = "",
897
+ command = "",
898
+ args = [],
899
+ env = process.env,
900
+ requestedParallelism = 0,
901
+ onSample = () => undefined,
902
+ cwd = process.cwd(),
903
+ } = {}) {
904
+ const samples = [];
905
+ const startedAt = Date.now();
906
+ const explicitRequested = firstPositiveInteger(requestedParallelism);
907
+ const detectedParallelism = detectRequestedParallelism({ command, args, env });
908
+ const requested = explicitRequested || detectedParallelism.value;
909
+ const sample = () => {
910
+ const snapshot = {
911
+ timestamp: new Date().toISOString(),
912
+ label,
913
+ elapsedMs: Date.now() - startedAt,
914
+ requestedParallelism: requested,
915
+ requestedParallelismSource: explicitRequested ? "explicit" : detectedParallelism.source,
916
+ ...collectProcessTreeSnapshot({ rootPid, cwd }),
917
+ };
918
+ samples.push(snapshot);
919
+ onSample(snapshot);
920
+ return snapshot;
921
+ };
922
+ sample();
923
+ const timer = setInterval(sample, Math.max(1000, Number(intervalMs || 15000)));
924
+ return {
925
+ samples,
926
+ stop() {
927
+ clearInterval(timer);
928
+ return samples;
929
+ },
930
+ };
931
+ }
932
+
933
+ export function createDiagnosticsArtifact({
934
+ cwd = process.cwd(),
935
+ logPath = "",
936
+ artifactPaths = [],
937
+ cacheDirs = [],
938
+ lifecycleObservability = undefined,
939
+ processSamples = [],
940
+ processSummary = undefined,
941
+ requestedParallelism = 0,
942
+ links = {},
943
+ } = {}) {
944
+ const resolvedCwd = path.resolve(cwd);
945
+ const events = logPath ? readBuildchainLogEvents(logPath) : [];
946
+ const loadedConfig = loadBuildchainConfig(resolvedCwd);
947
+ const nativeProfile = getNativeDiagnosticsProfile(loadedConfig);
948
+ return {
949
+ schemaVersion: 1,
950
+ contract: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
951
+ generatedAt: new Date().toISOString(),
952
+ cwd: resolvedCwd,
953
+ buildchain: collectBuildchainDiagnostics({ cwd: resolvedCwd, artifactPaths }),
954
+ runner: collectRunnerDiagnostics(),
955
+ tools: collectToolDiagnostics({ cwd: resolvedCwd }),
956
+ cache: collectCacheDiagnostics({ cwd: resolvedCwd, cacheDirs }),
957
+ native: collectNativeDiagnostics({ cwd: resolvedCwd, profile: nativeProfile }),
958
+ git: collectGitDiagnostics({ cwd: resolvedCwd }),
959
+ lifecycleObservability: lifecycleObservability || summarizeLifecycleObservability({ events, logPath }),
960
+ process: processSummary || summarizeProcessSamples({ samples: processSamples, requestedParallelism }),
961
+ links,
962
+ };
963
+ }
964
+
965
+ export function writeDiagnosticsArtifact(filePath, diagnostics) {
966
+ if (!filePath) {
967
+ return "";
968
+ }
969
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
970
+ fs.writeFileSync(filePath, `${JSON.stringify(diagnostics, null, 2)}\n`);
971
+ return filePath;
972
+ }
973
+
974
+ export function readDiagnosticsArtifact(filePath) {
975
+ return safeReadJson(filePath);
976
+ }
977
+
978
+ function sumLifecycleDurationMs(stages = {}) {
979
+ return Object.values(stages).reduce((sum, entry) => sum + Number(entry?.durationMs || 0), 0);
980
+ }
981
+
982
+ function stageDurationMs(stages = {}, stage) {
983
+ return Math.round(Number(stages?.[stage]?.durationMs || 0));
984
+ }
985
+
986
+ function formatDiagnosticsDurationMs(value) {
987
+ const durationMs = Math.round(Number(value || 0));
988
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
989
+ return "-";
990
+ }
991
+ if (durationMs < 1000) {
992
+ return `${durationMs}ms`;
993
+ }
994
+ if (durationMs < 60000) {
995
+ const seconds = durationMs / 1000;
996
+ return `${seconds < 10 ? seconds.toFixed(1).replace(/\.0$/, "") : Math.round(seconds)}s`;
997
+ }
998
+ const minutes = Math.floor(durationMs / 60000);
999
+ const seconds = Math.round((durationMs % 60000) / 1000);
1000
+ return seconds ? `${minutes}m${seconds}s` : `${minutes}m`;
1001
+ }
1002
+
1003
+ function compactProcessSummary(process = {}) {
1004
+ const observedConcurrency = process?.observedConcurrency || {};
1005
+ const topCommands = Array.isArray(process?.topCommands)
1006
+ ? process.topCommands.slice(0, 3).map((entry) => ({
1007
+ command: entry.command || "",
1008
+ category: entry.category || "",
1009
+ maxConcurrent: Number(entry.maxConcurrent || 0),
1010
+ maxCpu: Number(entry.maxCpu || 0),
1011
+ }))
1012
+ : [];
1013
+ return {
1014
+ requestedParallelism: Number(process?.requestedParallelism || 0),
1015
+ requestedParallelismSource: process?.requestedParallelismSource || "",
1016
+ observedConcurrencyMax: Number(observedConcurrency.max || 0),
1017
+ ratioToRequestedMax: Number(observedConcurrency.ratioToRequestedMax || 0),
1018
+ sampleCount: Number(process?.sampleCount || 0),
1019
+ categories: process?.categories || {},
1020
+ topCommands,
1021
+ };
1022
+ }
1023
+
1024
+ function formatDiagnosticsProcessValue(value) {
1025
+ const number = Number(value || 0);
1026
+ return number > 0 ? String(number) : "-";
1027
+ }
1028
+
1029
+ function compactRunnerDetails(runner = {}) {
1030
+ return {
1031
+ os: {
1032
+ platform: runner.os?.platform || "",
1033
+ release: runner.os?.release || "",
1034
+ arch: runner.os?.arch || "",
1035
+ type: runner.os?.type || "",
1036
+ },
1037
+ github: {
1038
+ actions: Boolean(runner.github?.actions),
1039
+ runnerOs: runner.github?.runnerOs || "",
1040
+ runnerArch: runner.github?.runnerArch || "",
1041
+ runnerName: runner.github?.runnerName || "",
1042
+ },
1043
+ cpu: {
1044
+ logicalCount: Number(runner.cpu?.logicalCount || 0),
1045
+ model: runner.cpu?.model || "",
1046
+ loadAverage: Array.isArray(runner.cpu?.loadAverage)
1047
+ ? runner.cpu.loadAverage.map((entry) => Number(entry || 0))
1048
+ : [],
1049
+ },
1050
+ memory: {
1051
+ totalBytes: Number(runner.memory?.totalBytes || 0),
1052
+ freeBytes: Number(runner.memory?.freeBytes || 0),
1053
+ },
1054
+ uptimeSeconds: Number(runner.uptimeSeconds || 0),
1055
+ };
1056
+ }
1057
+
1058
+ function compactToolSummary(...toolSets) {
1059
+ const merged = {};
1060
+ for (const toolSet of toolSets) {
1061
+ for (const [name, details] of Object.entries(toolSet || {})) {
1062
+ merged[name] = details || {};
1063
+ }
1064
+ }
1065
+ const versions = {};
1066
+ const missing = [];
1067
+ for (const [name, details] of Object.entries(merged).sort(([left], [right]) => left.localeCompare(right))) {
1068
+ const version = String(details?.version || "").trim();
1069
+ if (version) {
1070
+ versions[name] = version;
1071
+ } else {
1072
+ missing.push(name);
1073
+ }
1074
+ }
1075
+ return {
1076
+ checked: Object.keys(merged).length,
1077
+ available: Object.keys(versions).length,
1078
+ missing,
1079
+ versions,
1080
+ };
1081
+ }
1082
+
1083
+ function compactStatsObject(stats = {}, limit = 12) {
1084
+ const entries = [];
1085
+ const visit = (prefix, value) => {
1086
+ if (entries.length >= limit || value === undefined || value === null) {
1087
+ return;
1088
+ }
1089
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
1090
+ entries.push([prefix, value]);
1091
+ return;
1092
+ }
1093
+ if (Array.isArray(value) || typeof value !== "object") {
1094
+ return;
1095
+ }
1096
+ for (const [key, nested] of Object.entries(value).sort(([left], [right]) => left.localeCompare(right))) {
1097
+ visit(prefix ? `${prefix}.${key}` : key, nested);
1098
+ if (entries.length >= limit) {
1099
+ break;
1100
+ }
1101
+ }
1102
+ };
1103
+ visit("", stats);
1104
+ return Object.fromEntries(entries);
1105
+ }
1106
+
1107
+ function compactCompilerCacheSummary(compilerCaches = {}) {
1108
+ return Object.fromEntries(
1109
+ Object.entries(compilerCaches || {})
1110
+ .sort(([left], [right]) => left.localeCompare(right))
1111
+ .map(([name, details]) => [name, {
1112
+ available: Boolean(details?.available),
1113
+ format: details?.format || "",
1114
+ rawBytes: Number(details?.rawBytes || 0),
1115
+ error: details?.error || "",
1116
+ stats: compactStatsObject(details?.stats || {}),
1117
+ }]),
1118
+ );
1119
+ }
1120
+
1121
+ function compactCacheSummary(cache = {}, native = {}) {
1122
+ const workspace = cache.workspace || {};
1123
+ const workspacePackages = workspace && !workspace.error && !Array.isArray(workspace) && typeof workspace === "object"
1124
+ ? Object.keys(workspace).length
1125
+ : 0;
1126
+ return {
1127
+ packageManager: cache.packageManager || {},
1128
+ workspacePackages,
1129
+ workspaceError: workspace.error || "",
1130
+ dirs: Array.isArray(cache.dirs)
1131
+ ? cache.dirs.map((entry) => ({
1132
+ path: entry.path || "",
1133
+ exists: Boolean(entry.exists),
1134
+ type: entry.type || "",
1135
+ bytes: Number(entry.bytes || 0),
1136
+ }))
1137
+ : [],
1138
+ compilerCaches: compactCompilerCacheSummary({
1139
+ ...(cache.compilerCaches || {}),
1140
+ ...(native.compilerCaches || {}),
1141
+ }),
1142
+ nativeCacheDirs: Array.isArray(native.cacheDirs)
1143
+ ? native.cacheDirs.map((entry) => ({
1144
+ path: entry.path || "",
1145
+ exists: Boolean(entry.exists),
1146
+ type: entry.type || "",
1147
+ bytes: Number(entry.bytes || 0),
1148
+ }))
1149
+ : [],
1150
+ };
1151
+ }
1152
+
1153
+ function resolveDiagnosticsInput(entry) {
1154
+ if (typeof entry !== "string") {
1155
+ return { diagnostics: entry, sourcePath: "" };
1156
+ }
1157
+ return {
1158
+ diagnostics: readDiagnosticsArtifact(entry),
1159
+ sourcePath: path.resolve(entry),
1160
+ };
1161
+ }
1162
+
1163
+ function buildchainRootForPath(filePath) {
1164
+ const parts = path.resolve(filePath).split(path.sep);
1165
+ const index = parts.lastIndexOf(".buildchain");
1166
+ if (index <= 0) {
1167
+ return "";
1168
+ }
1169
+ const rootParts = parts.slice(0, index);
1170
+ return rootParts.length ? rootParts.join(path.sep) || path.sep : path.sep;
1171
+ }
1172
+
1173
+ function diagnosticsManifestCandidates(diagnostics, sourcePath) {
1174
+ if (!sourcePath) {
1175
+ return [];
1176
+ }
1177
+ const candidates = [path.join(path.dirname(sourcePath), "diagnostics-manifest.json")];
1178
+ const linkedPath = diagnostics?.links?.diagnosticsManifest || "";
1179
+ if (linkedPath) {
1180
+ if (path.isAbsolute(linkedPath)) {
1181
+ candidates.push(linkedPath);
1182
+ } else {
1183
+ const buildchainRoot = buildchainRootForPath(sourcePath);
1184
+ if (buildchainRoot) {
1185
+ candidates.push(path.resolve(buildchainRoot, linkedPath));
1186
+ }
1187
+ candidates.push(path.resolve(process.cwd(), linkedPath));
1188
+ candidates.push(path.resolve(path.dirname(sourcePath), linkedPath));
1189
+ }
1190
+ }
1191
+ return [...new Set(candidates)];
1192
+ }
1193
+
1194
+ function compactDiagnosticsManifestFile(entry = {}) {
1195
+ return {
1196
+ kind: entry.kind || "",
1197
+ path: entry.path || "",
1198
+ bytes: Number(entry.bytes || 0),
1199
+ sha256: entry.sha256 || "",
1200
+ required: Boolean(entry.required),
1201
+ };
1202
+ }
1203
+
1204
+ function summarizeDiagnosticsContract(diagnostics = {}) {
1205
+ const actual = diagnostics?.contract || "";
1206
+ const warnings = [];
1207
+ if (actual !== BUILDCHAIN_DIAGNOSTICS_CONTRACT) {
1208
+ warnings.push(`unexpected diagnostics artifact contract: ${actual || "unknown"}`);
1209
+ }
1210
+ return {
1211
+ status: warnings.length ? "warning" : "verified",
1212
+ expected: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
1213
+ actual,
1214
+ warningCount: warnings.length,
1215
+ warnings,
1216
+ };
1217
+ }
1218
+
1219
+ function summarizeDiagnosticsManifest(diagnostics, sourcePath) {
1220
+ if (!sourcePath) {
1221
+ return undefined;
1222
+ }
1223
+ const candidates = diagnosticsManifestCandidates(diagnostics, sourcePath);
1224
+ const manifestPath = candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0] || "";
1225
+ const relativePath = manifestPath
1226
+ ? posixPath(path.relative(path.dirname(sourcePath), manifestPath))
1227
+ : "";
1228
+ if (!manifestPath || !fs.existsSync(manifestPath)) {
1229
+ return {
1230
+ status: "missing",
1231
+ path: relativePath || "diagnostics-manifest.json",
1232
+ warningCount: 1,
1233
+ warnings: ["diagnostics sidecar manifest is missing"],
1234
+ };
1235
+ }
1236
+
1237
+ const { value: manifest, error } = readJsonResult(manifestPath);
1238
+ if (error) {
1239
+ return {
1240
+ status: "invalid",
1241
+ path: relativePath,
1242
+ warningCount: 1,
1243
+ warnings: [`failed to read diagnostics sidecar manifest: ${error.message}`],
1244
+ };
1245
+ }
1246
+
1247
+ const warnings = [];
1248
+ if (manifest?.contract !== BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT) {
1249
+ warnings.push(`unexpected diagnostics sidecar manifest contract: ${manifest?.contract || "unknown"}`);
1250
+ }
1251
+ const files = Array.isArray(manifest?.files) ? manifest.files.map(compactDiagnosticsManifestFile) : [];
1252
+ if (!Array.isArray(manifest?.files)) {
1253
+ warnings.push("diagnostics sidecar manifest files must be an array");
1254
+ }
1255
+ const diagnosticsEntry = files.find((entry) => entry.kind === "diagnostics");
1256
+ if (!diagnosticsEntry) {
1257
+ warnings.push("diagnostics sidecar manifest does not list diagnostics.json");
1258
+ } else {
1259
+ const stat = fs.statSync(sourcePath);
1260
+ const digest = sha256File(sourcePath);
1261
+ if (diagnosticsEntry.bytes !== stat.size) {
1262
+ warnings.push("diagnostics sidecar manifest bytes do not match diagnostics.json");
1263
+ }
1264
+ if (diagnosticsEntry.sha256 && diagnosticsEntry.sha256 !== digest) {
1265
+ warnings.push("diagnostics sidecar manifest sha256 does not match diagnostics.json");
1266
+ }
1267
+ }
1268
+
1269
+ return {
1270
+ status: warnings.length ? "warning" : "verified",
1271
+ path: relativePath,
1272
+ artifactName: manifest?.artifactName || "",
1273
+ diagnosticsArtifactName: manifest?.diagnosticsArtifactName || "",
1274
+ platformId: manifest?.platformId || "",
1275
+ fileCount: Number(manifest?.fileCount || files.length),
1276
+ totalBytes: Number(manifest?.totalBytes || files.reduce((sum, entry) => sum + entry.bytes, 0)),
1277
+ files,
1278
+ warningCount: warnings.length,
1279
+ warnings,
1280
+ };
1281
+ }
1282
+
1283
+ function formatDiagnosticsSummaryRows(rows = []) {
1284
+ const widths = rows[0].map((_, columnIndex) => (
1285
+ Math.max(...rows.map((row) => String(row[columnIndex] ?? "").length))
1286
+ ));
1287
+ return rows
1288
+ .map((row) => row.map((cell, index) => String(cell ?? "").padEnd(widths[index], " ")).join(" ").trimEnd())
1289
+ .join("\n");
1290
+ }
1291
+
1292
+ export function formatDiagnosticsSummaryTable(summary = {}) {
1293
+ const rows = [[
1294
+ "platform",
1295
+ "head",
1296
+ "install",
1297
+ "build",
1298
+ "verify",
1299
+ "publish",
1300
+ "scan",
1301
+ "upload",
1302
+ "total",
1303
+ "jobs",
1304
+ "active",
1305
+ "warn",
1306
+ "err",
1307
+ ]];
1308
+ for (const platform of summary.platforms || []) {
1309
+ const label = `${platform.runner || "unknown"}/${platform.arch || "unknown"}`;
1310
+ const processSummary = compactProcessSummary(platform.process);
1311
+ rows.push([
1312
+ label,
1313
+ platform.gitHead ? platform.gitHead.slice(0, 12) : "-",
1314
+ formatDiagnosticsDurationMs(stageDurationMs(platform.lifecycle, "install")),
1315
+ formatDiagnosticsDurationMs(stageDurationMs(platform.lifecycle, "build")),
1316
+ formatDiagnosticsDurationMs(stageDurationMs(platform.lifecycle, "verify")),
1317
+ formatDiagnosticsDurationMs(stageDurationMs(platform.lifecycle, "publish")),
1318
+ formatDiagnosticsDurationMs(platform.artifactScanDurationMs),
1319
+ formatDiagnosticsDurationMs(platform.artifactUploadDurationMs),
1320
+ formatDiagnosticsDurationMs(platform.totalDurationMs),
1321
+ formatDiagnosticsProcessValue(processSummary.requestedParallelism),
1322
+ formatDiagnosticsProcessValue(processSummary.observedConcurrencyMax),
1323
+ platform.warningCount || 0,
1324
+ platform.errorCount || 0,
1325
+ ]);
1326
+ }
1327
+ return formatDiagnosticsSummaryRows(rows);
1328
+ }
1329
+
1330
+ export function summarizeDiagnosticsArtifacts(inputs = []) {
1331
+ const diagnostics = inputs
1332
+ .map(resolveDiagnosticsInput)
1333
+ .filter((entry) => entry.diagnostics);
1334
+ const platforms = diagnostics.map(({ diagnostics: entry, sourcePath }) => {
1335
+ const lifecycle = entry.lifecycleObservability?.stages || {};
1336
+ const lifecycleTotalDurationMs = sumLifecycleDurationMs(lifecycle);
1337
+ const artifactScanDurationMs = Math.round(Number(entry.lifecycleObservability?.artifactScan?.durationMs || 0));
1338
+ const artifactUploadDurationMs = Math.round(Number(entry.lifecycleObservability?.artifactUpload?.durationMs || 0));
1339
+ const diagnosticsContract = summarizeDiagnosticsContract(entry);
1340
+ const diagnosticsManifest = summarizeDiagnosticsManifest(entry, sourcePath);
1341
+ return {
1342
+ runner: entry.runner?.github?.runnerOs || entry.runner?.os?.platform || "unknown",
1343
+ arch: entry.runner?.github?.runnerArch || entry.runner?.os?.arch || "unknown",
1344
+ gitHead: entry.git?.head || "",
1345
+ lifecycle,
1346
+ lifecycleTotalDurationMs,
1347
+ artifactScanDurationMs,
1348
+ artifactUploadDurationMs,
1349
+ totalDurationMs: lifecycleTotalDurationMs + artifactScanDurationMs + artifactUploadDurationMs,
1350
+ totalBytes: Number(entry.lifecycleObservability?.totalBytes || 0),
1351
+ fileCount: Number(entry.lifecycleObservability?.fileCount || 0),
1352
+ topSlowSpans: (entry.lifecycleObservability?.topSlowSpans || []).slice(0, 5),
1353
+ runnerDetails: compactRunnerDetails(entry.runner || {}),
1354
+ tools: compactToolSummary(entry.tools || {}, entry.native?.tools || {}),
1355
+ cache: compactCacheSummary(entry.cache || {}, entry.native || {}),
1356
+ process: entry.process || {},
1357
+ diagnosticsContract,
1358
+ ...(diagnosticsManifest ? { diagnosticsManifest } : {}),
1359
+ links: entry.links || {},
1360
+ warningCount: entry.lifecycleObservability?.warningCount || 0,
1361
+ errorCount: entry.lifecycleObservability?.errorCount || 0,
1362
+ };
1363
+ });
1364
+ return {
1365
+ schemaVersion: 1,
1366
+ contract: BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT,
1367
+ generatedAt: new Date().toISOString(),
1368
+ count: diagnostics.length,
1369
+ totalWarningCount: platforms.reduce((sum, entry) => sum + entry.warningCount, 0),
1370
+ totalErrorCount: platforms.reduce((sum, entry) => sum + entry.errorCount, 0),
1371
+ diagnosticsManifestWarningCount: platforms.reduce(
1372
+ (sum, entry) => sum + Number(entry.diagnosticsManifest?.warningCount || 0),
1373
+ 0,
1374
+ ),
1375
+ diagnosticsContractWarningCount: platforms.reduce(
1376
+ (sum, entry) => sum + Number(entry.diagnosticsContract?.warningCount || 0),
1377
+ 0,
1378
+ ),
1379
+ slowestPlatforms: platforms
1380
+ .map(({ runner, arch, gitHead, lifecycleTotalDurationMs, totalDurationMs, process }) => ({
1381
+ runner,
1382
+ arch,
1383
+ gitHead,
1384
+ lifecycleTotalDurationMs,
1385
+ totalDurationMs,
1386
+ process: compactProcessSummary(process),
1387
+ }))
1388
+ .sort((left, right) => right.totalDurationMs - left.totalDurationMs)
1389
+ .slice(0, 10),
1390
+ platforms,
1391
+ };
1392
+ }