@kungfu-tech/buildchain 2.14.17-alpha.0 → 2.14.17-alpha.2

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.
@@ -104,6 +104,10 @@ import {
104
104
  } from "../packages/core/kfd3-surface-register.js";
105
105
  import { createBuildchainLayoutDiscovery } from "../packages/core/buildchain-layout.js";
106
106
  import { createPortableDevCachePlan, createPortableDevCacheReceipt } from "../packages/core/portable-dev-cache.js";
107
+ import {
108
+ createCandidateTimeline,
109
+ formatCandidateTimelineReport,
110
+ } from "../packages/core/candidate-timeline.js";
107
111
 
108
112
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
109
113
  const embeddedPackageVersion = process.env.BUILDCHAIN_EMBEDDED_PACKAGE_VERSION || "";
@@ -121,6 +125,7 @@ function usage() {
121
125
  [--validation-reason <text>]
122
126
  [--cold-fallback-status not-run|passed|failed]
123
127
  [--output <file>] [--json]
128
+ buildchain candidate timeline --input <file-or-json> [--output <file>] [--json]
124
129
  buildchain init [--cwd <dir>] [--type package|native|web-surface|infra-contract|publication-artifact|anchored-package] [--force]
125
130
  [--package-manager pnpm|npm|yarn] [--runner-preset <preset>]
126
131
  [--artifact-name <template>]
@@ -1451,6 +1456,28 @@ async function main(argv = process.argv.slice(2)) {
1451
1456
  throw new Error("usage: buildchain portable-cache <plan|receipt> ...");
1452
1457
  }
1453
1458
 
1459
+ if (command === "candidate") {
1460
+ const [subcommand = "", ...candidateArgs] = args;
1461
+ if (subcommand !== "timeline") {
1462
+ throw new Error("usage: buildchain candidate timeline --input <file-or-json>");
1463
+ }
1464
+ const inputValue = readFlag(candidateArgs, "input", "");
1465
+ if (!inputValue) {
1466
+ throw new Error("buildchain candidate timeline requires --input <file-or-json>");
1467
+ }
1468
+ const input = readJsonInput(inputValue, { label: "candidate timeline input" });
1469
+ const timeline = createCandidateTimeline(input);
1470
+ const output = readFlag(candidateArgs, "output", "");
1471
+ if (output) writeJsonFile(path.resolve(output), timeline);
1472
+ if (readBooleanFlag(candidateArgs, "json") || !output) {
1473
+ printJson(timeline);
1474
+ } else {
1475
+ process.stdout.write(`${formatCandidateTimelineReport(timeline)}\n`);
1476
+ process.stdout.write(`wrote: ${output}\n`);
1477
+ }
1478
+ return;
1479
+ }
1480
+
1454
1481
  if (command === "init") {
1455
1482
  const result = initBuildchainRepo({
1456
1483
  cwd: readFlag(args, "cwd", process.cwd()),
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "name": "Buildchain",
6
6
  "package": "@kungfu-tech/buildchain",
7
- "version": "2.14.17-alpha.0",
7
+ "version": "2.14.17-alpha.2",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "majorLine": "v2",
@@ -256,7 +256,7 @@
256
256
  "channel-aware artifact host facts are checked against the deploy plan before adapter side effects"
257
257
  ],
258
258
  "breakingDigest": "sha256:da569f90374e878948e3d5160ed9929338ce0572473ca8ee54867b0fe1aaeace",
259
- "auditDigest": "sha256:cd620b10103c27137ae17f23583753adfe2f3ef501e511f7380431aa95f08b18"
259
+ "auditDigest": "sha256:4a7c696ec399c14aa3170fdd24adac0d624bd33db6a2639c489e2b58726c190b"
260
260
  },
261
261
  {
262
262
  "contractVersion": 1,
@@ -323,6 +323,9 @@
323
323
  "publish transactions can require a resolved publish-gate source lock to prevent floating-ref drift",
324
324
  "semver GitHub Releases are created or updated only after transaction completion, with prerelease/latest metadata bound to the authoritative publication channel and tag syntax retained as the fallback for ordinary callers"
325
325
  ],
326
+ "compatibleBreakingDigests": [
327
+ "sha256:a59f0910e6df842e7699139472e5dd69ac2fdd7f7213bf2cb346d1d622556874"
328
+ ],
326
329
  "breakingDigest": "sha256:6147c0a8d5c36bfaa6021871574e1fc28192a3f1b8cfdae4d24ef3059ac1ebda",
327
330
  "auditDigest": "sha256:ff220542fd1cb21fc4780d0bb31eb9f78d44d70746b6c4364483e104b48ae083"
328
331
  },
@@ -492,7 +495,7 @@
492
495
  "README badge block checks and writes are generated from machine-readable repository facts"
493
496
  ],
494
497
  "breakingDigest": "sha256:90a73892b2d6c214048448d5f45df75639bdf7b7e8fefd9c596e04b087407bcc",
495
- "auditDigest": "sha256:0d6ab717fa934bb7d50a166e8cf6e3cea5c57b3be460864d083a91edb013f868"
498
+ "auditDigest": "sha256:0e33a9c24a2a23e5657d06ee7013454ada97b141a3dbae0c10490554bb74bf1f"
496
499
  },
497
500
  {
498
501
  "contractVersion": 1,
@@ -582,7 +585,7 @@
582
585
  "manual entries carry source file digests so downstream sites and agents can detect stale hand-written documentation"
583
586
  ],
584
587
  "breakingDigest": "sha256:7d0d2819e3a3e72989d9c57b5efe9d0bc0a79bc0f2c82a0c7b9d6c5a211a91f2",
585
- "auditDigest": "sha256:b012b8c27fda3a7b5816b14fbd30c7761b899ba65c83079014d8d1f891b60608"
588
+ "auditDigest": "sha256:a877a3ac90cabaa5f30a735a4ebd7b5be6254492572723a940f7df0fecd43ee7"
586
589
  },
587
590
  {
588
591
  "contractVersion": 1,
@@ -604,7 +607,7 @@
604
607
  "agents can discover supported Node APIs without importing internal file paths"
605
608
  ],
606
609
  "breakingDigest": "sha256:48f925608d3e2131d90936b07dc2a30341204cae3e6e785c0f77d61ad755c945",
607
- "auditDigest": "sha256:8005d01fa6c771f466c77629f0589b361fc43a3a4f04b7d88bc1645d33895a51"
610
+ "auditDigest": "sha256:99097e8474a8dcdccb1a3ab62c19e1b960a010fbc8f00f21df3aac1f04ba2c09"
608
611
  },
609
612
  {
610
613
  "contractVersion": 1,
@@ -1850,7 +1853,7 @@
1850
1853
  "missing receipts are non-qualifying and must not be represented as a successful controller run"
1851
1854
  ],
1852
1855
  "breakingDigest": "sha256:f14825324a16b51c2e9ed708ebe64e1932e05b6d5e0866f83aea8439decb4a27",
1853
- "auditDigest": "sha256:cd620b10103c27137ae17f23583753adfe2f3ef501e511f7380431aa95f08b18"
1856
+ "auditDigest": "sha256:4a7c696ec399c14aa3170fdd24adac0d624bd33db6a2639c489e2b58726c190b"
1854
1857
  },
1855
1858
  {
1856
1859
  "contractVersion": 1,
@@ -2913,5 +2916,5 @@
2913
2916
  }
2914
2917
  ],
2915
2918
  "compatibilityDigest": "sha256:cdf588422ec6b460bd6f631400efa6e361a045e052c8967d6c50fadd7aa4dd90",
2916
- "contractDigest": "sha256:fe7adf9ed1c1a0b14b29fef1eb72205ae29c9558806c9b35bfcc258f07d422a8"
2919
+ "contractDigest": "sha256:95680b4596a6e848d6b8cff4b7216b3c560e085298d99587baa4419d4fdc6aef"
2917
2920
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-bundle",
4
- "generatedAt": "2026-07-22T15:29:55.552Z",
5
- "publishedAt": "2026-07-22T15:29:55.552Z",
4
+ "generatedAt": "2026-07-23T00:58:45.650Z",
5
+ "publishedAt": "2026-07-23T00:58:45.650Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "a013078fdf88b5b850e48e93faf58de582130440",
22
+ "sourceRevision": "d67ebd6fcb075c00ef286cee9f2bc1f03633af13",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "2.14.17-alpha.0",
40
+ "version": "2.14.17-alpha.2",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "source": {
@@ -1996,7 +1996,7 @@
1996
1996
  ],
1997
1997
  "maturity": "stable",
1998
1998
  "sourcePath": "docs/toolkit-observability.md",
1999
- "digest": "sha256:239949a75ca81b6002a2e148f7028402dbd98c00e246acd8d0955855781206a1",
1999
+ "digest": "sha256:ed6c2d0a7e76a5e4fc3fbee635966e462947f748a52d88e515fe4f95a630ac91",
2000
2000
  "headings": [
2001
2001
  {
2002
2002
  "level": 1,
@@ -2013,6 +2013,11 @@
2013
2013
  "title": "Library API",
2014
2014
  "anchor": "library-api"
2015
2015
  },
2016
+ {
2017
+ "level": 2,
2018
+ "title": "Candidate timelines and critical-path-safe timing",
2019
+ "anchor": "candidate-timelines-and-critical-path-safe-timing"
2020
+ },
2016
2021
  {
2017
2022
  "level": 2,
2018
2023
  "title": "Diagnostics API",
@@ -2029,7 +2034,7 @@
2029
2034
  "anchor": "release-gate"
2030
2035
  }
2031
2036
  ],
2032
- "markdown": "# Toolkit Observability\n\nBuildchain ships a small logging toolkit for repository workflows and project\nscripts. The goal is to separate time spent in Buildchain's framework from time\nspent in the consumer's own build, test, packaging, and publish steps.\n\n## Choose API or CLI\n\n`@kungfu-tech/buildchain` is not only a CLI package. It exports ESM toolkit APIs\nthat project scripts can import directly:\n\n```js\nimport { createBuildchainLogger } from \"@kungfu-tech/buildchain/logging\";\n```\n\nUse the API inside JavaScript or TypeScript build code. Do not spawn\n`buildchain`, download the standalone binary, or shell out through `npx` from\ncode that can import the package. The CLI is for GitHub Actions steps, shell\nscripts, and non-JavaScript tools.\n\nWhen a script runs inside `buildchain lifecycle run`, the lifecycle runner sets\n`BUILDCHAIN_LOG_PATH` and `BUILDCHAIN_LOG_RUN_ID`. Imported loggers pick up those\nenvironment variables automatically, so events emitted deep inside the build are\ngrouped into the same lifecycle summary.\n\nOutside a Buildchain lifecycle or GitHub Actions run, the logger defaults to\nconsole output unless a path is provided. Pass `path` when local scripts should\nwrite a reusable JSONL log:\n\n```js\nconst logger = createBuildchainLogger({\n path: \".buildchain/logs/native-build.jsonl\",\n source: \"user\",\n component: \"native-build\",\n});\n```\n\n## Library API\n\n```js\nimport {\n createBuildchainLogger,\n verifyBuildchainLogEvents,\n} from \"@kungfu-tech/buildchain/logging\";\n\nconst logger = createBuildchainLogger({\n source: \"user\",\n component: \"native-build\",\n});\n\nlogger.mark(\"configure.ready\", {\n phase: \"configure\",\n attributes: { preset: \"release\" },\n});\n\nawait logger.span(\"native.compile\", {\n phase: \"build\",\n attributes: { target: \"release\" },\n}, async () => {\n await compile();\n});\n\nlogger.spanSync(\"native.archive\", {\n phase: \"build\",\n attributes: { tool: \"libtool\" },\n}, () => {\n archiveStaticLibraries();\n});\n\nlogger.spawnSync(\"native.build\", \"make\", [\"-j20\"], {\n stdio: \"inherit\",\n}, {\n phase: \"build\",\n attributes: { requestedJobs: 20 },\n});\n\nconst report = verifyBuildchainLogEvents({\n path: logger.path,\n minEvents: 3,\n requirePhases: [\"configure\", \"build\"],\n requireEvents: [\n \"configure.ready\",\n \"native.compile.start\",\n \"native.compile.end\",\n ],\n});\n\nif (!report.ok) {\n throw new Error(\"Buildchain observability verification failed\");\n}\n```\n\nUse the API when a build script has internal stages that are invisible to the\nouter workflow. Keep secret values out of attributes; known sensitive keys are\nredacted, but callers should still avoid logging private material.\n\nCommonJS scripts should import Buildchain's ESM surfaces dynamically:\n\n```js\nconst { createBuildchainLogger } = await import(\"@kungfu-tech/buildchain/logging\");\nconst { collectRunnerDiagnostics } = await import(\"@kungfu-tech/buildchain/diagnostics\");\n```\n\n## Diagnostics API\n\nThe diagnostics surface collects local, non-telemetry build facts that are\nuseful when a native build is slow or flaky:\n\n```js\nimport {\n collectBuildchainDiagnostics,\n collectCacheDiagnostics,\n collectCompilerCacheDiagnostics,\n collectRunnerDiagnostics,\n collectToolDiagnostics,\n detectRequestedParallelism,\n startProcessSampler,\n summarizeDiagnosticsArtifacts,\n summarizeLifecycleObservability,\n summarizeProcessSamples,\n validateAnchoredPackageRelease,\n writeDiagnosticsArtifact,\n} from \"@kungfu-tech/buildchain/diagnostics\";\n\nconst lifecycleObservability = summarizeLifecycleObservability({\n logPath: \".buildchain/logs/events.jsonl\",\n});\nconst buildCommand = \"make\";\nconst buildArgs = [\"-j20\"];\nconst requestedParallelism = detectRequestedParallelism({\n command: buildCommand,\n args: buildArgs,\n});\nconst processSampler = startProcessSampler({\n intervalMs: 15000,\n label: \"native-build\",\n command: buildCommand,\n args: buildArgs,\n});\n// Run the long native build while the sampler is active.\nconst processSummary = summarizeProcessSamples({\n requestedParallelism: requestedParallelism.value,\n samples: processSampler.stop(),\n});\nconst cacheDiagnostics = collectCacheDiagnostics({ cwd: process.cwd() });\n\nwriteDiagnosticsArtifact(\".buildchain/artifacts/diagnostics.json\", {\n contract: \"consumer-build-diagnostics\",\n buildchain: collectBuildchainDiagnostics({ cwd: process.cwd() }),\n runner: collectRunnerDiagnostics(),\n tools: collectToolDiagnostics({ cwd: process.cwd() }),\n cache: cacheDiagnostics,\n lifecycleObservability,\n process: processSummary,\n});\n```\n\n`collectCacheDiagnostics()` includes package-manager/workspace context, selected\ncache directory stats, and compiler-cache stats from `ccache --show-stats\n--json` plus `sccache --show-stats --stats-format json` when those tools are\npresent. If a ccache build does not support JSON stats, Buildchain falls back to\nplain `ccache --show-stats` and parses the text counters. Missing cache tools\nare recorded as unavailable instead of failing the diagnostics artifact. Call\n`collectCompilerCacheDiagnostics()` directly when a consumer script only needs\ncompiler cache data. Native diagnostics also expose `compilerCaches` and\n`nativeCacheDirs` as top-level fields in each diagnostics artifact and aggregate\nsummary, so reviewers do not have to dig through nested cache sections first.\n\nProcess samples are intentionally summarized before they become long-lived\nartifacts. The summary records requested parallelism, the source of that value\n(`command`, `process-tree`, `env:MAKEFLAGS`,\n`env:CMAKE_BUILD_PARALLEL_LEVEL`, or `explicit`), observed active process\nconcurrency, elapsed sample time, total sampled CPU, and conservative command\ncategories such as `compiler`, `archive`, `linker`, `build-tool`, and `cache`.\nThe sampler detects common `make -j N`, `ninja -j N`, CMake/MSBuild/Xcode job\nflags, and MAKEFLAGS. This lets native projects distinguish \"we asked for\n`make -j20`\" from \"the build graph only kept two active compiler or archive\nchildren busy during the sampled window\" without storing environment dumps.\n\nNative repositories can opt into a reusable diagnostics profile in\n`buildchain.toml`:\n\n```toml\n[diagnostics.native]\nenabled = true\nsample_process_tree = true\ncompiler_cache = \"auto\"\nexpected_tools = [\"ccache\", \"sccache\", \"clang\", \"cl\", \"cmake\", \"ninja\"]\nartifact_dirs = [\"build\", \"dist\", \"build/Release\"]\ncache_dirs = [\".ccache\", \".sccache\"]\n```\n\nWhen enabled, diagnostics artifacts include the normalized profile, selected\ntool versions, compiler-cache stats, and configured artifact/cache directory\nstats. The profile is data-driven: Buildchain does not assume a specific\nproject such as libnode. The reusable build workflow also exposes\n`sample-process-tree`, which wraps the build lifecycle with the process sampler\nand carries the generated summary into the final diagnostics artifact. Custom\nworkflows can call `buildchain sample process-tree` directly when they need a\ndifferent command boundary.\n\nAnchored/manual package projects can also run one higher-level release-shape\ncheck instead of assembling lower-level config calls:\n\n```js\nconst anchoredReport = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requireManifest: true,\n requirePackageSetOrder: \"platforms-first-main-last\",\n requireTrustedPublishing: true,\n});\n\nif (!anchoredReport.ok) {\n throw new Error(\"Anchored package release contract failed\");\n}\n```\n\nIn an actual publish job, make that check source-lock aware:\n\n```js\nconst publishReady = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requirePublishGateSourceLock: true,\n});\n```\n\nThe source-lock inputs are read from `BUILDCHAIN_PUBLISH_SOURCE_REF`,\n`BUILDCHAIN_PUBLISH_SOURCE_SHA`, and `BUILDCHAIN_PUBLISH_SOURCE_LOCKED`, which\nthe reusable build workflow emits after resolving `publish-source-ref`. That\nturns direct channel-branch publication from `alpha/*` or `release/*` into a\nhard failure, while `publish-gate/alpha/<line>/<version>` and\n`publish-gate/release/<line>/<version>` also validate the requested consumer\nversion against configured version files and the anchor manifest.\n\n`buildchain lifecycle run` writes this small diagnostics artifact next to the\nplatform manifest by default. The per-platform diagnostics upload includes the\ncompact `diagnostics.json`, `diagnostics-manifest.json`, lifecycle\n`events.jsonl`, and, when process sampling is enabled, copied\n`process-summary.json` and `process-samples.jsonl` sidecars. The sidecar\nmanifest records each uploaded diagnostics file with its relative path, byte\ncount, and sha256 hash. It is intended to stay small enough to download without\nfetching large binary packages, and it should not include full environment dumps\nor secret-looking values.\n\nUse `summarizeDiagnosticsArtifacts()` when a matrix build uploads one\ndiagnostics artifact per platform. The summary keeps each platform's lifecycle\nstage table, adds a lifecycle total duration, carries the top slow spans, and\naggregates warning/error counts plus the slowest platforms. Per-platform rows\nalso include compact runner facts, checked tool versions/missing tools, package\nmanager/cache directory details, compiler cache availability, and a compact\nprocess sampler view with requested parallelism, observed max active processes,\nthe ratio between them, sample count, process categories, and top sampled\ncommand basenames. That gives release reviewers a small cross-platform timing,\nrunner, tool, cache, and concurrency view without downloading full build outputs\nor process sidecars.\n\nThe reusable build workflow writes that rollup as `diagnostics-summary.json` and\nuploads it in a separate aggregate diagnostics summary artifact. Consumers can\nread the `build-diagnostics-summary-artifact` output when they need only timing,\nwarning/error, runner, cache, and process-sampler context instead of the build\nsummary or binary artifacts. Per-platform rows keep the diagnostics `links`\nobject, including the binary artifact name, manifest artifact name, diagnostics\nartifact name, platform id, diagnostics sidecar manifest path, manifest path,\nsummary path, and process sidecar paths when present. They also keep compact\nrunner/tool/cache summaries so reviewers can tell whether a slow row ran on the\nexpected runner, missed an expected tool, or lacked useful compiler-cache stats.\nWhen the downloaded platform diagnostics include a sibling\n`diagnostics-manifest.json`, `summarizeDiagnosticsArtifacts()` carries a compact\n`diagnosticsManifest` section for that platform and verifies the manifest's\n`diagnostics.json` byte count and sha256. Missing or mismatched sidecar manifests\nincrement `diagnosticsManifestWarningCount`, so reviewers can distinguish\ndiagnostics sidecar drift from lifecycle warnings or build failures.\nThe same summary carries `diagnosticsContract` per platform and aggregates\n`diagnosticsContractWarningCount` when a downloaded `diagnostics.json` does not\nmatch `BUILDCHAIN_DIAGNOSTICS_CONTRACT`.\n\nThe diagnostics SDK also exports stable contract constants such as\n`BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT`,\n`BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT`, and\n`BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT` from\n`@kungfu-tech/buildchain/diagnostics`; consumers should compare against those\nconstants instead of hardcoding contract strings.\n\nThe CLI exposes the same aggregation for shell and workflow steps:\n\n```bash\nbuildchain diagnostics summary \\\n .buildchain/artifacts/linux-x64/diagnostics.json \\\n .buildchain/artifacts/macos-arm64/diagnostics.json \\\n --output .buildchain/artifacts/diagnostics-summary.json \\\n --json\n```\n\nOmit `--json` when the workflow log should show a compact platform table with\nlifecycle stages, artifact scan/upload time, total time, requested jobs,\nobserved active processes, warnings, and errors.\n\nFor long native build commands, the CLI can also sample the child process tree\nwhile preserving the wrapped command's exit code:\n\n```bash\nbuildchain sample process-tree \\\n --label native-build \\\n --interval-ms 15000 \\\n --output .buildchain/diagnostics/process-samples.jsonl \\\n --summary-output .buildchain/diagnostics/process-summary.json \\\n -- \\\n make -j20\n```\n\nThe JSONL sample file stores timestamped process-tree snapshots with full\nredacted command lines, command basenames, CPU percentages when available,\nelapsed time, and requested parallelism context. Unix and macOS snapshots read\nthe full `ps args` field instead of truncated `comm` names. Windows snapshots\nread `Win32_Process` command lines through PowerShell so the sampler no longer\nreturns an empty process set on Windows runners. The summary file records\nobserved concurrency, total sampled CPU, command categories, and top command\nbasenames. This is intended for diagnosing low-utilization tails such as\narchive/link phases without logging full environment dumps.\n\nThe lifecycle observability summary is stage-wide, not just final-step timing:\nwhen install and build write to the same Buildchain log, the final platform\nmanifest can show both stages and the slowest spans.\n\n## CLI Logging\n\n```bash\nbuildchain mark \\\n --event native.configure \\\n --phase configure \\\n --component cmake \\\n --attribute preset=release\n\nbuildchain span \\\n --event native.build \\\n --phase build \\\n --component cmake \\\n -- cmake --build build --config Release\n\nbuildchain log summary --json\nbuildchain verify observability-log .buildchain/logs/events.jsonl --min-events 4\n```\n\nEvery event records a timestamp. `span` records duration and preserves the\nwrapped command's exit code.\n\n## Release Gate\n\nBuildchain's own binary distribution lane verifies required log events before\nuploading release assets. Consumers can apply the same pattern:\n\n```bash\nbuildchain verify observability-log .buildchain/logs/events.jsonl \\\n --require-phase build \\\n --require-phase package \\\n --require-component workflow \\\n --require-event native.build.start \\\n --require-event native.build.end\n```\n\nThis makes missing instrumentation a release failure instead of a dashboard\nafterthought."
2037
+ "markdown": "# Toolkit Observability\n\nBuildchain ships a small logging toolkit for repository workflows and project\nscripts. The goal is to separate time spent in Buildchain's framework from time\nspent in the consumer's own build, test, packaging, and publish steps.\n\n## Choose API or CLI\n\n`@kungfu-tech/buildchain` is not only a CLI package. It exports ESM toolkit APIs\nthat project scripts can import directly:\n\n```js\nimport { createBuildchainLogger } from \"@kungfu-tech/buildchain/logging\";\n```\n\nUse the API inside JavaScript or TypeScript build code. Do not spawn\n`buildchain`, download the standalone binary, or shell out through `npx` from\ncode that can import the package. The CLI is for GitHub Actions steps, shell\nscripts, and non-JavaScript tools.\n\nWhen a script runs inside `buildchain lifecycle run`, the lifecycle runner sets\n`BUILDCHAIN_LOG_PATH` and `BUILDCHAIN_LOG_RUN_ID`. Imported loggers pick up those\nenvironment variables automatically, so events emitted deep inside the build are\ngrouped into the same lifecycle summary.\n\nOutside a Buildchain lifecycle or GitHub Actions run, the logger defaults to\nconsole output unless a path is provided. Pass `path` when local scripts should\nwrite a reusable JSONL log:\n\n```js\nconst logger = createBuildchainLogger({\n path: \".buildchain/logs/native-build.jsonl\",\n source: \"user\",\n component: \"native-build\",\n});\n```\n\n## Library API\n\n```js\nimport {\n createBuildchainLogger,\n verifyBuildchainLogEvents,\n} from \"@kungfu-tech/buildchain/logging\";\n\nconst logger = createBuildchainLogger({\n source: \"user\",\n component: \"native-build\",\n});\n\nlogger.mark(\"configure.ready\", {\n phase: \"configure\",\n attributes: { preset: \"release\" },\n});\n\nawait logger.span(\"native.compile\", {\n phase: \"build\",\n attributes: { target: \"release\" },\n}, async () => {\n await compile();\n});\n\nlogger.spanSync(\"native.archive\", {\n phase: \"build\",\n attributes: { tool: \"libtool\" },\n}, () => {\n archiveStaticLibraries();\n});\n\nlogger.spawnSync(\"native.build\", \"make\", [\"-j20\"], {\n stdio: \"inherit\",\n}, {\n phase: \"build\",\n attributes: { requestedJobs: 20 },\n});\n\nconst report = verifyBuildchainLogEvents({\n path: logger.path,\n minEvents: 3,\n requirePhases: [\"configure\", \"build\"],\n requireEvents: [\n \"configure.ready\",\n \"native.compile.start\",\n \"native.compile.end\",\n ],\n});\n\nif (!report.ok) {\n throw new Error(\"Buildchain observability verification failed\");\n}\n```\n\nUse the API when a build script has internal stages that are invisible to the\nouter workflow. Keep secret values out of attributes; known sensitive keys are\nredacted, but callers should still avoid logging private material.\n\n## Candidate timelines and critical-path-safe timing\n\nLifecycle diagnostics retain their existing summed-duration tables for\ncompatibility. Do not interpret those tables as elapsed time when spans are\nnested or jobs run in parallel. Use the candidate timeline contract when a\nchange must be followed from pull-request admission through Merge Queue attempts\nto its final merge:\n\n```js\nimport {\n createCandidateTimeline,\n formatCandidateTimelineReport,\n} from \"@kungfu-tech/buildchain/candidate-timeline\";\n\nconst timeline = createCandidateTimeline({\n candidate: {\n repository: \"owner/repository\",\n baseBranch: \"dev/v4/v4.0\",\n sourceSha,\n pullRequest: 123,\n },\n events,\n});\n\nconsole.log(formatCandidateTimelineReport(timeline));\n```\n\nEach event binds a stable event id to one attempt id and may also bind a Gate\nid, merge-group SHA, workflow run, platform, partition, cache outcome, execution\nboundary, and parent span. Terminal execution states (`success`, `failure`, and\n`cancelled`) require start and completion timestamps. Non-executed states use\n`skipped`, `dependency-blocked`, or `not-required` without invented timings.\nRecord the clock and timestamp precision explicitly: GitHub Actions timestamps\nare normally provider wall-clock observations with one-second precision, while\nan in-process span may also carry a monotonic duration with millisecond or\nbetter precision.\n\nThe `buildchain.candidate-timeline/v1` artifact reports an independent critical\npath for every attempt. Its critical-path duration is the attempt's observed\nwall-clock envelope; its active duration and per-phase durations are interval\nunions. This prevents nested or parallel spans from being added twice, and it\ndoes not combine a failed or dequeued attempt with a later retry. Missing\nrequired measurements make that attempt `incomplete` instead of producing fake\nprecision.\n\nShell and workflow consumers can generate the same machine artifact plus a\ncompact report:\n\n```sh\nbuildchain candidate timeline \\\n --input .buildchain/candidate-timeline-input.json \\\n --output .buildchain/candidate-timeline.json\n```\n\nThe input and output should contain only bounded correlation facts and timing\nreceipts. Do not include tokens, environment dumps, full commands, raw process\ndumps, or private absolute paths.\n\nCommonJS scripts should import Buildchain's ESM surfaces dynamically:\n\n```js\nconst { createBuildchainLogger } = await import(\"@kungfu-tech/buildchain/logging\");\nconst { collectRunnerDiagnostics } = await import(\"@kungfu-tech/buildchain/diagnostics\");\n```\n\n## Diagnostics API\n\nThe diagnostics surface collects local, non-telemetry build facts that are\nuseful when a native build is slow or flaky:\n\n```js\nimport {\n collectBuildchainDiagnostics,\n collectCacheDiagnostics,\n collectCompilerCacheDiagnostics,\n collectRunnerDiagnostics,\n collectToolDiagnostics,\n detectRequestedParallelism,\n startProcessSampler,\n summarizeDiagnosticsArtifacts,\n summarizeLifecycleObservability,\n summarizeProcessSamples,\n validateAnchoredPackageRelease,\n writeDiagnosticsArtifact,\n} from \"@kungfu-tech/buildchain/diagnostics\";\n\nconst lifecycleObservability = summarizeLifecycleObservability({\n logPath: \".buildchain/logs/events.jsonl\",\n});\nconst buildCommand = \"make\";\nconst buildArgs = [\"-j20\"];\nconst requestedParallelism = detectRequestedParallelism({\n command: buildCommand,\n args: buildArgs,\n});\nconst processSampler = startProcessSampler({\n intervalMs: 15000,\n label: \"native-build\",\n command: buildCommand,\n args: buildArgs,\n});\n// Run the long native build while the sampler is active.\nconst processSummary = summarizeProcessSamples({\n requestedParallelism: requestedParallelism.value,\n samples: processSampler.stop(),\n});\nconst cacheDiagnostics = collectCacheDiagnostics({ cwd: process.cwd() });\n\nwriteDiagnosticsArtifact(\".buildchain/artifacts/diagnostics.json\", {\n contract: \"consumer-build-diagnostics\",\n buildchain: collectBuildchainDiagnostics({ cwd: process.cwd() }),\n runner: collectRunnerDiagnostics(),\n tools: collectToolDiagnostics({ cwd: process.cwd() }),\n cache: cacheDiagnostics,\n lifecycleObservability,\n process: processSummary,\n});\n```\n\n`collectCacheDiagnostics()` includes package-manager/workspace context, selected\ncache directory stats, and compiler-cache stats from `ccache --show-stats\n--json` plus `sccache --show-stats --stats-format json` when those tools are\npresent. If a ccache build does not support JSON stats, Buildchain falls back to\nplain `ccache --show-stats` and parses the text counters. Missing cache tools\nare recorded as unavailable instead of failing the diagnostics artifact. Call\n`collectCompilerCacheDiagnostics()` directly when a consumer script only needs\ncompiler cache data. Native diagnostics also expose `compilerCaches` and\n`nativeCacheDirs` as top-level fields in each diagnostics artifact and aggregate\nsummary, so reviewers do not have to dig through nested cache sections first.\n\nProcess samples are intentionally summarized before they become long-lived\nartifacts. The summary records requested parallelism, the source of that value\n(`command`, `process-tree`, `env:MAKEFLAGS`,\n`env:CMAKE_BUILD_PARALLEL_LEVEL`, or `explicit`), observed active process\nconcurrency, elapsed sample time, total sampled CPU, and conservative command\ncategories such as `compiler`, `archive`, `linker`, `build-tool`, and `cache`.\nThe sampler detects common `make -j N`, `ninja -j N`, CMake/MSBuild/Xcode job\nflags, and MAKEFLAGS. This lets native projects distinguish \"we asked for\n`make -j20`\" from \"the build graph only kept two active compiler or archive\nchildren busy during the sampled window\" without storing environment dumps.\n\nNative repositories can opt into a reusable diagnostics profile in\n`buildchain.toml`:\n\n```toml\n[diagnostics.native]\nenabled = true\nsample_process_tree = true\ncompiler_cache = \"auto\"\nexpected_tools = [\"ccache\", \"sccache\", \"clang\", \"cl\", \"cmake\", \"ninja\"]\nartifact_dirs = [\"build\", \"dist\", \"build/Release\"]\ncache_dirs = [\".ccache\", \".sccache\"]\n```\n\nWhen enabled, diagnostics artifacts include the normalized profile, selected\ntool versions, compiler-cache stats, and configured artifact/cache directory\nstats. The profile is data-driven: Buildchain does not assume a specific\nproject such as libnode. The reusable build workflow also exposes\n`sample-process-tree`, which wraps the build lifecycle with the process sampler\nand carries the generated summary into the final diagnostics artifact. Custom\nworkflows can call `buildchain sample process-tree` directly when they need a\ndifferent command boundary.\n\nAnchored/manual package projects can also run one higher-level release-shape\ncheck instead of assembling lower-level config calls:\n\n```js\nconst anchoredReport = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requireManifest: true,\n requirePackageSetOrder: \"platforms-first-main-last\",\n requireTrustedPublishing: true,\n});\n\nif (!anchoredReport.ok) {\n throw new Error(\"Anchored package release contract failed\");\n}\n```\n\nIn an actual publish job, make that check source-lock aware:\n\n```js\nconst publishReady = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requirePublishGateSourceLock: true,\n});\n```\n\nThe source-lock inputs are read from `BUILDCHAIN_PUBLISH_SOURCE_REF`,\n`BUILDCHAIN_PUBLISH_SOURCE_SHA`, and `BUILDCHAIN_PUBLISH_SOURCE_LOCKED`, which\nthe reusable build workflow emits after resolving `publish-source-ref`. That\nturns direct channel-branch publication from `alpha/*` or `release/*` into a\nhard failure, while `publish-gate/alpha/<line>/<version>` and\n`publish-gate/release/<line>/<version>` also validate the requested consumer\nversion against configured version files and the anchor manifest.\n\n`buildchain lifecycle run` writes this small diagnostics artifact next to the\nplatform manifest by default. The per-platform diagnostics upload includes the\ncompact `diagnostics.json`, `diagnostics-manifest.json`, lifecycle\n`events.jsonl`, and, when process sampling is enabled, copied\n`process-summary.json` and `process-samples.jsonl` sidecars. The sidecar\nmanifest records each uploaded diagnostics file with its relative path, byte\ncount, and sha256 hash. It is intended to stay small enough to download without\nfetching large binary packages, and it should not include full environment dumps\nor secret-looking values.\n\nUse `summarizeDiagnosticsArtifacts()` when a matrix build uploads one\ndiagnostics artifact per platform. The summary keeps each platform's lifecycle\nstage table, adds a lifecycle total duration, carries the top slow spans, and\naggregates warning/error counts plus the slowest platforms. Per-platform rows\nalso include compact runner facts, checked tool versions/missing tools, package\nmanager/cache directory details, compiler cache availability, and a compact\nprocess sampler view with requested parallelism, observed max active processes,\nthe ratio between them, sample count, process categories, and top sampled\ncommand basenames. That gives release reviewers a small cross-platform timing,\nrunner, tool, cache, and concurrency view without downloading full build outputs\nor process sidecars.\n\nThe reusable build workflow writes that rollup as `diagnostics-summary.json` and\nuploads it in a separate aggregate diagnostics summary artifact. Consumers can\nread the `build-diagnostics-summary-artifact` output when they need only timing,\nwarning/error, runner, cache, and process-sampler context instead of the build\nsummary or binary artifacts. Per-platform rows keep the diagnostics `links`\nobject, including the binary artifact name, manifest artifact name, diagnostics\nartifact name, platform id, diagnostics sidecar manifest path, manifest path,\nsummary path, and process sidecar paths when present. They also keep compact\nrunner/tool/cache summaries so reviewers can tell whether a slow row ran on the\nexpected runner, missed an expected tool, or lacked useful compiler-cache stats.\nWhen the downloaded platform diagnostics include a sibling\n`diagnostics-manifest.json`, `summarizeDiagnosticsArtifacts()` carries a compact\n`diagnosticsManifest` section for that platform and verifies the manifest's\n`diagnostics.json` byte count and sha256. Missing or mismatched sidecar manifests\nincrement `diagnosticsManifestWarningCount`, so reviewers can distinguish\ndiagnostics sidecar drift from lifecycle warnings or build failures.\nThe same summary carries `diagnosticsContract` per platform and aggregates\n`diagnosticsContractWarningCount` when a downloaded `diagnostics.json` does not\nmatch `BUILDCHAIN_DIAGNOSTICS_CONTRACT`.\n\nThe diagnostics SDK also exports stable contract constants such as\n`BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT`,\n`BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT`, and\n`BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT` from\n`@kungfu-tech/buildchain/diagnostics`; consumers should compare against those\nconstants instead of hardcoding contract strings.\n\nThe CLI exposes the same aggregation for shell and workflow steps:\n\n```bash\nbuildchain diagnostics summary \\\n .buildchain/artifacts/linux-x64/diagnostics.json \\\n .buildchain/artifacts/macos-arm64/diagnostics.json \\\n --output .buildchain/artifacts/diagnostics-summary.json \\\n --json\n```\n\nOmit `--json` when the workflow log should show a compact platform table with\nlifecycle stages, artifact scan/upload time, total time, requested jobs,\nobserved active processes, warnings, and errors.\n\nFor long native build commands, the CLI can also sample the child process tree\nwhile preserving the wrapped command's exit code:\n\n```bash\nbuildchain sample process-tree \\\n --label native-build \\\n --interval-ms 15000 \\\n --output .buildchain/diagnostics/process-samples.jsonl \\\n --summary-output .buildchain/diagnostics/process-summary.json \\\n -- \\\n make -j20\n```\n\nThe JSONL sample file stores timestamped process-tree snapshots with full\nredacted command lines, command basenames, CPU percentages when available,\nelapsed time, and requested parallelism context. Unix and macOS snapshots read\nthe full `ps args` field instead of truncated `comm` names. Windows snapshots\nread `Win32_Process` command lines through PowerShell so the sampler no longer\nreturns an empty process set on Windows runners. The summary file records\nobserved concurrency, total sampled CPU, command categories, and top command\nbasenames. This is intended for diagnosing low-utilization tails such as\narchive/link phases without logging full environment dumps.\n\nThe lifecycle observability summary is stage-wide, not just final-step timing:\nwhen install and build write to the same Buildchain log, the final platform\nmanifest can show both stages and the slowest spans.\n\n## CLI Logging\n\n```bash\nbuildchain mark \\\n --event native.configure \\\n --phase configure \\\n --component cmake \\\n --attribute preset=release\n\nbuildchain span \\\n --event native.build \\\n --phase build \\\n --component cmake \\\n -- cmake --build build --config Release\n\nbuildchain log summary --json\nbuildchain verify observability-log .buildchain/logs/events.jsonl --min-events 4\n```\n\nEvery event records a timestamp. `span` records duration and preserves the\nwrapped command's exit code.\n\n## Release Gate\n\nBuildchain's own binary distribution lane verifies required log events before\nuploading release assets. Consumers can apply the same pattern:\n\n```bash\nbuildchain verify observability-log .buildchain/logs/events.jsonl \\\n --require-phase build \\\n --require-phase package \\\n --require-component workflow \\\n --require-event native.build.start \\\n --require-event native.build.end\n```\n\nThis makes missing instrumentation a release failure instead of a dashboard\nafterthought."
2033
2038
  },
2034
2039
  {
2035
2040
  "id": "manual:versioning",
@@ -2677,7 +2682,7 @@
2677
2682
  "path": "docs/toolkit-observability.md",
2678
2683
  "plane": "use",
2679
2684
  "exists": true,
2680
- "digest": "sha256:239949a75ca81b6002a2e148f7028402dbd98c00e246acd8d0955855781206a1"
2685
+ "digest": "sha256:ed6c2d0a7e76a5e4fc3fbee635966e462947f748a52d88e515fe4f95a630ac91"
2681
2686
  },
2682
2687
  {
2683
2688
  "id": "site-bundle-contract",
@@ -216,8 +216,8 @@
216
216
  "counts": {
217
217
  "manualCount": 3,
218
218
  "pageCount": 3,
219
- "cliCommandCount": 12,
220
- "nodeApiCount": 5,
219
+ "cliCommandCount": 13,
220
+ "nodeApiCount": 6,
221
221
  "workflowCount": 0,
222
222
  "actionCount": 1
223
223
  },
@@ -77,6 +77,18 @@
77
77
  ],
78
78
  "maturity": "stable"
79
79
  },
80
+ {
81
+ "id": "candidate",
82
+ "source": "bin/buildchain.mjs",
83
+ "usage": "buildchain candidate timeline --input <file-or-json> [--output <file>] [--json]",
84
+ "purpose": "Inspect source-bound candidate evidence command families.",
85
+ "capabilityGroup": "observability-diagnostics",
86
+ "audience": [
87
+ "agent",
88
+ "operator"
89
+ ],
90
+ "maturity": "stable"
91
+ },
80
92
  {
81
93
  "id": "collect",
82
94
  "source": "bin/buildchain.mjs",
@@ -21,13 +21,13 @@
21
21
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
22
22
  "path": "dist/site/public-surface-audit.json",
23
23
  "status": "passed",
24
- "sha256": "85d9b19de156773aab9a45cc7f8c201c0cebfc07a3f8d4ae902ca2d0e9fbfd86",
24
+ "sha256": "82b7e4e4827b8ab01397192a969e427bceaa344026b758719f8e1f59aa9b97fe",
25
25
  "summary": {
26
- "cliCommandCount": 85,
26
+ "cliCommandCount": 86,
27
27
  "workflowCount": 51,
28
28
  "actionCount": 4,
29
29
  "sitePageCount": 52,
30
- "docCommandRefCount": 264,
30
+ "docCommandRefCount": 265,
31
31
  "failureCount": 0
32
32
  },
33
33
  "auditBoundary": {
@@ -228,13 +228,13 @@
228
228
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
229
229
  "path": "dist/site/public-surface-audit.json",
230
230
  "status": "passed",
231
- "sha256": "85d9b19de156773aab9a45cc7f8c201c0cebfc07a3f8d4ae902ca2d0e9fbfd86",
231
+ "sha256": "82b7e4e4827b8ab01397192a969e427bceaa344026b758719f8e1f59aa9b97fe",
232
232
  "summary": {
233
- "cliCommandCount": 85,
233
+ "cliCommandCount": 86,
234
234
  "workflowCount": 51,
235
235
  "actionCount": 4,
236
236
  "sitePageCount": 52,
237
- "docCommandRefCount": 264,
237
+ "docCommandRefCount": 265,
238
238
  "failureCount": 0
239
239
  },
240
240
  "auditBoundary": {
@@ -1090,6 +1090,18 @@
1090
1090
  "public": true,
1091
1091
  "packageExport": "./buildchain-contract"
1092
1092
  },
1093
+ {
1094
+ "id": "export:./candidate-timeline",
1095
+ "name": "@kungfu-tech/buildchain/candidate-timeline",
1096
+ "kind": "package-export",
1097
+ "sourcePath": "packages/core/candidate-timeline.js",
1098
+ "evidencePath": "packages/core/candidate-timeline.js",
1099
+ "availability": "shipped",
1100
+ "visibility": "public",
1101
+ "participantFacing": true,
1102
+ "public": true,
1103
+ "packageExport": "./candidate-timeline"
1104
+ },
1093
1105
  {
1094
1106
  "id": "export:./controller-evidence",
1095
1107
  "name": "@kungfu-tech/buildchain/controller-evidence",
@@ -1686,6 +1698,18 @@
1686
1698
  "public": true,
1687
1699
  "reverseAuditSource": "bin/buildchain.mjs"
1688
1700
  },
1701
+ {
1702
+ "id": "cli:candidate",
1703
+ "name": "buildchain candidate timeline --input <file-or-json> [--output <file>] [--json]",
1704
+ "kind": "cli-command",
1705
+ "sourcePath": "bin/buildchain.mjs",
1706
+ "evidencePath": "bin/buildchain.mjs",
1707
+ "availability": "shipped",
1708
+ "visibility": "public",
1709
+ "participantFacing": true,
1710
+ "public": true,
1711
+ "reverseAuditSource": "bin/buildchain.mjs"
1712
+ },
1689
1713
  {
1690
1714
  "id": "cli:collect",
1691
1715
  "name": "buildchain collect",
@@ -4965,6 +4989,6 @@
4965
4989
  }
4966
4990
  }
4967
4991
  ],
4968
- "publicSurfaceCount": 316
4992
+ "publicSurfaceCount": 318
4969
4993
  }
4970
4994
  }
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "id": "kungfu-tech-buildchain",
6
6
  "name": "@kungfu-tech/buildchain",
7
- "version": "2.14.17-alpha.0",
7
+ "version": "2.14.17-alpha.2",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "source": {
@@ -177,7 +177,7 @@
177
177
  "title": "Toolkit observability",
178
178
  "path": "docs/toolkit-observability.md",
179
179
  "plane": "use",
180
- "digest": "sha256:239949a75ca81b6002a2e148f7028402dbd98c00e246acd8d0955855781206a1",
180
+ "digest": "sha256:ed6c2d0a7e76a5e4fc3fbee635966e462947f748a52d88e515fe4f95a630ac91",
181
181
  "capabilityGroup": "observability-diagnostics",
182
182
  "audience": [
183
183
  "developer",
@@ -8,7 +8,7 @@
8
8
  "specifier": "@kungfu-tech/buildchain",
9
9
  "export": ".",
10
10
  "target": "./packages/core/index.js",
11
- "digest": "sha256:78c4f1885d1f619242f31e27b6cd4d1e21e0fd9c67fbe92f8b613abe9acdd3c1",
11
+ "digest": "sha256:1e02f99f2424f2850c0f264ad6a0bb75973144bc1099dba82d06ba0434c2b406",
12
12
  "summary": "Root toolkit export for Buildchain's public Node API.",
13
13
  "capabilityGroup": "api-cli-reference",
14
14
  "audience": [
@@ -47,7 +47,7 @@
47
47
  "specifier": "@kungfu-tech/buildchain/core",
48
48
  "export": "./core",
49
49
  "target": "./packages/core/index.js",
50
- "digest": "sha256:78c4f1885d1f619242f31e27b6cd4d1e21e0fd9c67fbe92f8b613abe9acdd3c1",
50
+ "digest": "sha256:1e02f99f2424f2850c0f264ad6a0bb75973144bc1099dba82d06ba0434c2b406",
51
51
  "summary": "Alias for the root public toolkit export.",
52
52
  "capabilityGroup": "api-cli-reference",
53
53
  "audience": [
@@ -99,7 +99,7 @@
99
99
  "specifier": "@kungfu-tech/buildchain/buildchain-contract",
100
100
  "export": "./buildchain-contract",
101
101
  "target": "./packages/core/buildchain-contract.js",
102
- "digest": "sha256:da92e920f6cc00dd5e21405f06580c2fa943eead3b755dc36b6c3a2381e68423",
102
+ "digest": "sha256:1e5f8a1741a0a98116e754ef0874337db4c1821bb7a153ab398009145509681c",
103
103
  "summary": "Runtime contract world and compatibility digest APIs for floating-ref drift checks.",
104
104
  "capabilityGroup": "governance-versioning",
105
105
  "audience": [
@@ -108,6 +108,19 @@
108
108
  ],
109
109
  "maturity": "stable"
110
110
  },
111
+ {
112
+ "specifier": "@kungfu-tech/buildchain/candidate-timeline",
113
+ "export": "./candidate-timeline",
114
+ "target": "./packages/core/candidate-timeline.js",
115
+ "digest": "sha256:d394d97d2165d2294ade84921e81e272c7d9e2a0a0673124531760f0fe428a1a",
116
+ "summary": "Source-bound candidate event normalization, per-attempt critical-path-safe aggregation, and compact reporting APIs.",
117
+ "capabilityGroup": "observability-diagnostics",
118
+ "audience": [
119
+ "developer",
120
+ "agent"
121
+ ],
122
+ "maturity": "stable"
123
+ },
111
124
  {
112
125
  "specifier": "@kungfu-tech/buildchain/controller-evidence",
113
126
  "export": "./controller-evidence",
@@ -1930,7 +1930,7 @@
1930
1930
  ],
1931
1931
  "maturity": "stable",
1932
1932
  "sourcePath": "docs/toolkit-observability.md",
1933
- "digest": "sha256:239949a75ca81b6002a2e148f7028402dbd98c00e246acd8d0955855781206a1",
1933
+ "digest": "sha256:ed6c2d0a7e76a5e4fc3fbee635966e462947f748a52d88e515fe4f95a630ac91",
1934
1934
  "headings": [
1935
1935
  {
1936
1936
  "level": 1,
@@ -1947,6 +1947,11 @@
1947
1947
  "title": "Library API",
1948
1948
  "anchor": "library-api"
1949
1949
  },
1950
+ {
1951
+ "level": 2,
1952
+ "title": "Candidate timelines and critical-path-safe timing",
1953
+ "anchor": "candidate-timelines-and-critical-path-safe-timing"
1954
+ },
1950
1955
  {
1951
1956
  "level": 2,
1952
1957
  "title": "Diagnostics API",
@@ -1963,7 +1968,7 @@
1963
1968
  "anchor": "release-gate"
1964
1969
  }
1965
1970
  ],
1966
- "markdown": "# Toolkit Observability\n\nBuildchain ships a small logging toolkit for repository workflows and project\nscripts. The goal is to separate time spent in Buildchain's framework from time\nspent in the consumer's own build, test, packaging, and publish steps.\n\n## Choose API or CLI\n\n`@kungfu-tech/buildchain` is not only a CLI package. It exports ESM toolkit APIs\nthat project scripts can import directly:\n\n```js\nimport { createBuildchainLogger } from \"@kungfu-tech/buildchain/logging\";\n```\n\nUse the API inside JavaScript or TypeScript build code. Do not spawn\n`buildchain`, download the standalone binary, or shell out through `npx` from\ncode that can import the package. The CLI is for GitHub Actions steps, shell\nscripts, and non-JavaScript tools.\n\nWhen a script runs inside `buildchain lifecycle run`, the lifecycle runner sets\n`BUILDCHAIN_LOG_PATH` and `BUILDCHAIN_LOG_RUN_ID`. Imported loggers pick up those\nenvironment variables automatically, so events emitted deep inside the build are\ngrouped into the same lifecycle summary.\n\nOutside a Buildchain lifecycle or GitHub Actions run, the logger defaults to\nconsole output unless a path is provided. Pass `path` when local scripts should\nwrite a reusable JSONL log:\n\n```js\nconst logger = createBuildchainLogger({\n path: \".buildchain/logs/native-build.jsonl\",\n source: \"user\",\n component: \"native-build\",\n});\n```\n\n## Library API\n\n```js\nimport {\n createBuildchainLogger,\n verifyBuildchainLogEvents,\n} from \"@kungfu-tech/buildchain/logging\";\n\nconst logger = createBuildchainLogger({\n source: \"user\",\n component: \"native-build\",\n});\n\nlogger.mark(\"configure.ready\", {\n phase: \"configure\",\n attributes: { preset: \"release\" },\n});\n\nawait logger.span(\"native.compile\", {\n phase: \"build\",\n attributes: { target: \"release\" },\n}, async () => {\n await compile();\n});\n\nlogger.spanSync(\"native.archive\", {\n phase: \"build\",\n attributes: { tool: \"libtool\" },\n}, () => {\n archiveStaticLibraries();\n});\n\nlogger.spawnSync(\"native.build\", \"make\", [\"-j20\"], {\n stdio: \"inherit\",\n}, {\n phase: \"build\",\n attributes: { requestedJobs: 20 },\n});\n\nconst report = verifyBuildchainLogEvents({\n path: logger.path,\n minEvents: 3,\n requirePhases: [\"configure\", \"build\"],\n requireEvents: [\n \"configure.ready\",\n \"native.compile.start\",\n \"native.compile.end\",\n ],\n});\n\nif (!report.ok) {\n throw new Error(\"Buildchain observability verification failed\");\n}\n```\n\nUse the API when a build script has internal stages that are invisible to the\nouter workflow. Keep secret values out of attributes; known sensitive keys are\nredacted, but callers should still avoid logging private material.\n\nCommonJS scripts should import Buildchain's ESM surfaces dynamically:\n\n```js\nconst { createBuildchainLogger } = await import(\"@kungfu-tech/buildchain/logging\");\nconst { collectRunnerDiagnostics } = await import(\"@kungfu-tech/buildchain/diagnostics\");\n```\n\n## Diagnostics API\n\nThe diagnostics surface collects local, non-telemetry build facts that are\nuseful when a native build is slow or flaky:\n\n```js\nimport {\n collectBuildchainDiagnostics,\n collectCacheDiagnostics,\n collectCompilerCacheDiagnostics,\n collectRunnerDiagnostics,\n collectToolDiagnostics,\n detectRequestedParallelism,\n startProcessSampler,\n summarizeDiagnosticsArtifacts,\n summarizeLifecycleObservability,\n summarizeProcessSamples,\n validateAnchoredPackageRelease,\n writeDiagnosticsArtifact,\n} from \"@kungfu-tech/buildchain/diagnostics\";\n\nconst lifecycleObservability = summarizeLifecycleObservability({\n logPath: \".buildchain/logs/events.jsonl\",\n});\nconst buildCommand = \"make\";\nconst buildArgs = [\"-j20\"];\nconst requestedParallelism = detectRequestedParallelism({\n command: buildCommand,\n args: buildArgs,\n});\nconst processSampler = startProcessSampler({\n intervalMs: 15000,\n label: \"native-build\",\n command: buildCommand,\n args: buildArgs,\n});\n// Run the long native build while the sampler is active.\nconst processSummary = summarizeProcessSamples({\n requestedParallelism: requestedParallelism.value,\n samples: processSampler.stop(),\n});\nconst cacheDiagnostics = collectCacheDiagnostics({ cwd: process.cwd() });\n\nwriteDiagnosticsArtifact(\".buildchain/artifacts/diagnostics.json\", {\n contract: \"consumer-build-diagnostics\",\n buildchain: collectBuildchainDiagnostics({ cwd: process.cwd() }),\n runner: collectRunnerDiagnostics(),\n tools: collectToolDiagnostics({ cwd: process.cwd() }),\n cache: cacheDiagnostics,\n lifecycleObservability,\n process: processSummary,\n});\n```\n\n`collectCacheDiagnostics()` includes package-manager/workspace context, selected\ncache directory stats, and compiler-cache stats from `ccache --show-stats\n--json` plus `sccache --show-stats --stats-format json` when those tools are\npresent. If a ccache build does not support JSON stats, Buildchain falls back to\nplain `ccache --show-stats` and parses the text counters. Missing cache tools\nare recorded as unavailable instead of failing the diagnostics artifact. Call\n`collectCompilerCacheDiagnostics()` directly when a consumer script only needs\ncompiler cache data. Native diagnostics also expose `compilerCaches` and\n`nativeCacheDirs` as top-level fields in each diagnostics artifact and aggregate\nsummary, so reviewers do not have to dig through nested cache sections first.\n\nProcess samples are intentionally summarized before they become long-lived\nartifacts. The summary records requested parallelism, the source of that value\n(`command`, `process-tree`, `env:MAKEFLAGS`,\n`env:CMAKE_BUILD_PARALLEL_LEVEL`, or `explicit`), observed active process\nconcurrency, elapsed sample time, total sampled CPU, and conservative command\ncategories such as `compiler`, `archive`, `linker`, `build-tool`, and `cache`.\nThe sampler detects common `make -j N`, `ninja -j N`, CMake/MSBuild/Xcode job\nflags, and MAKEFLAGS. This lets native projects distinguish \"we asked for\n`make -j20`\" from \"the build graph only kept two active compiler or archive\nchildren busy during the sampled window\" without storing environment dumps.\n\nNative repositories can opt into a reusable diagnostics profile in\n`buildchain.toml`:\n\n```toml\n[diagnostics.native]\nenabled = true\nsample_process_tree = true\ncompiler_cache = \"auto\"\nexpected_tools = [\"ccache\", \"sccache\", \"clang\", \"cl\", \"cmake\", \"ninja\"]\nartifact_dirs = [\"build\", \"dist\", \"build/Release\"]\ncache_dirs = [\".ccache\", \".sccache\"]\n```\n\nWhen enabled, diagnostics artifacts include the normalized profile, selected\ntool versions, compiler-cache stats, and configured artifact/cache directory\nstats. The profile is data-driven: Buildchain does not assume a specific\nproject such as libnode. The reusable build workflow also exposes\n`sample-process-tree`, which wraps the build lifecycle with the process sampler\nand carries the generated summary into the final diagnostics artifact. Custom\nworkflows can call `buildchain sample process-tree` directly when they need a\ndifferent command boundary.\n\nAnchored/manual package projects can also run one higher-level release-shape\ncheck instead of assembling lower-level config calls:\n\n```js\nconst anchoredReport = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requireManifest: true,\n requirePackageSetOrder: \"platforms-first-main-last\",\n requireTrustedPublishing: true,\n});\n\nif (!anchoredReport.ok) {\n throw new Error(\"Anchored package release contract failed\");\n}\n```\n\nIn an actual publish job, make that check source-lock aware:\n\n```js\nconst publishReady = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requirePublishGateSourceLock: true,\n});\n```\n\nThe source-lock inputs are read from `BUILDCHAIN_PUBLISH_SOURCE_REF`,\n`BUILDCHAIN_PUBLISH_SOURCE_SHA`, and `BUILDCHAIN_PUBLISH_SOURCE_LOCKED`, which\nthe reusable build workflow emits after resolving `publish-source-ref`. That\nturns direct channel-branch publication from `alpha/*` or `release/*` into a\nhard failure, while `publish-gate/alpha/<line>/<version>` and\n`publish-gate/release/<line>/<version>` also validate the requested consumer\nversion against configured version files and the anchor manifest.\n\n`buildchain lifecycle run` writes this small diagnostics artifact next to the\nplatform manifest by default. The per-platform diagnostics upload includes the\ncompact `diagnostics.json`, `diagnostics-manifest.json`, lifecycle\n`events.jsonl`, and, when process sampling is enabled, copied\n`process-summary.json` and `process-samples.jsonl` sidecars. The sidecar\nmanifest records each uploaded diagnostics file with its relative path, byte\ncount, and sha256 hash. It is intended to stay small enough to download without\nfetching large binary packages, and it should not include full environment dumps\nor secret-looking values.\n\nUse `summarizeDiagnosticsArtifacts()` when a matrix build uploads one\ndiagnostics artifact per platform. The summary keeps each platform's lifecycle\nstage table, adds a lifecycle total duration, carries the top slow spans, and\naggregates warning/error counts plus the slowest platforms. Per-platform rows\nalso include compact runner facts, checked tool versions/missing tools, package\nmanager/cache directory details, compiler cache availability, and a compact\nprocess sampler view with requested parallelism, observed max active processes,\nthe ratio between them, sample count, process categories, and top sampled\ncommand basenames. That gives release reviewers a small cross-platform timing,\nrunner, tool, cache, and concurrency view without downloading full build outputs\nor process sidecars.\n\nThe reusable build workflow writes that rollup as `diagnostics-summary.json` and\nuploads it in a separate aggregate diagnostics summary artifact. Consumers can\nread the `build-diagnostics-summary-artifact` output when they need only timing,\nwarning/error, runner, cache, and process-sampler context instead of the build\nsummary or binary artifacts. Per-platform rows keep the diagnostics `links`\nobject, including the binary artifact name, manifest artifact name, diagnostics\nartifact name, platform id, diagnostics sidecar manifest path, manifest path,\nsummary path, and process sidecar paths when present. They also keep compact\nrunner/tool/cache summaries so reviewers can tell whether a slow row ran on the\nexpected runner, missed an expected tool, or lacked useful compiler-cache stats.\nWhen the downloaded platform diagnostics include a sibling\n`diagnostics-manifest.json`, `summarizeDiagnosticsArtifacts()` carries a compact\n`diagnosticsManifest` section for that platform and verifies the manifest's\n`diagnostics.json` byte count and sha256. Missing or mismatched sidecar manifests\nincrement `diagnosticsManifestWarningCount`, so reviewers can distinguish\ndiagnostics sidecar drift from lifecycle warnings or build failures.\nThe same summary carries `diagnosticsContract` per platform and aggregates\n`diagnosticsContractWarningCount` when a downloaded `diagnostics.json` does not\nmatch `BUILDCHAIN_DIAGNOSTICS_CONTRACT`.\n\nThe diagnostics SDK also exports stable contract constants such as\n`BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT`,\n`BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT`, and\n`BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT` from\n`@kungfu-tech/buildchain/diagnostics`; consumers should compare against those\nconstants instead of hardcoding contract strings.\n\nThe CLI exposes the same aggregation for shell and workflow steps:\n\n```bash\nbuildchain diagnostics summary \\\n .buildchain/artifacts/linux-x64/diagnostics.json \\\n .buildchain/artifacts/macos-arm64/diagnostics.json \\\n --output .buildchain/artifacts/diagnostics-summary.json \\\n --json\n```\n\nOmit `--json` when the workflow log should show a compact platform table with\nlifecycle stages, artifact scan/upload time, total time, requested jobs,\nobserved active processes, warnings, and errors.\n\nFor long native build commands, the CLI can also sample the child process tree\nwhile preserving the wrapped command's exit code:\n\n```bash\nbuildchain sample process-tree \\\n --label native-build \\\n --interval-ms 15000 \\\n --output .buildchain/diagnostics/process-samples.jsonl \\\n --summary-output .buildchain/diagnostics/process-summary.json \\\n -- \\\n make -j20\n```\n\nThe JSONL sample file stores timestamped process-tree snapshots with full\nredacted command lines, command basenames, CPU percentages when available,\nelapsed time, and requested parallelism context. Unix and macOS snapshots read\nthe full `ps args` field instead of truncated `comm` names. Windows snapshots\nread `Win32_Process` command lines through PowerShell so the sampler no longer\nreturns an empty process set on Windows runners. The summary file records\nobserved concurrency, total sampled CPU, command categories, and top command\nbasenames. This is intended for diagnosing low-utilization tails such as\narchive/link phases without logging full environment dumps.\n\nThe lifecycle observability summary is stage-wide, not just final-step timing:\nwhen install and build write to the same Buildchain log, the final platform\nmanifest can show both stages and the slowest spans.\n\n## CLI Logging\n\n```bash\nbuildchain mark \\\n --event native.configure \\\n --phase configure \\\n --component cmake \\\n --attribute preset=release\n\nbuildchain span \\\n --event native.build \\\n --phase build \\\n --component cmake \\\n -- cmake --build build --config Release\n\nbuildchain log summary --json\nbuildchain verify observability-log .buildchain/logs/events.jsonl --min-events 4\n```\n\nEvery event records a timestamp. `span` records duration and preserves the\nwrapped command's exit code.\n\n## Release Gate\n\nBuildchain's own binary distribution lane verifies required log events before\nuploading release assets. Consumers can apply the same pattern:\n\n```bash\nbuildchain verify observability-log .buildchain/logs/events.jsonl \\\n --require-phase build \\\n --require-phase package \\\n --require-component workflow \\\n --require-event native.build.start \\\n --require-event native.build.end\n```\n\nThis makes missing instrumentation a release failure instead of a dashboard\nafterthought."
1971
+ "markdown": "# Toolkit Observability\n\nBuildchain ships a small logging toolkit for repository workflows and project\nscripts. The goal is to separate time spent in Buildchain's framework from time\nspent in the consumer's own build, test, packaging, and publish steps.\n\n## Choose API or CLI\n\n`@kungfu-tech/buildchain` is not only a CLI package. It exports ESM toolkit APIs\nthat project scripts can import directly:\n\n```js\nimport { createBuildchainLogger } from \"@kungfu-tech/buildchain/logging\";\n```\n\nUse the API inside JavaScript or TypeScript build code. Do not spawn\n`buildchain`, download the standalone binary, or shell out through `npx` from\ncode that can import the package. The CLI is for GitHub Actions steps, shell\nscripts, and non-JavaScript tools.\n\nWhen a script runs inside `buildchain lifecycle run`, the lifecycle runner sets\n`BUILDCHAIN_LOG_PATH` and `BUILDCHAIN_LOG_RUN_ID`. Imported loggers pick up those\nenvironment variables automatically, so events emitted deep inside the build are\ngrouped into the same lifecycle summary.\n\nOutside a Buildchain lifecycle or GitHub Actions run, the logger defaults to\nconsole output unless a path is provided. Pass `path` when local scripts should\nwrite a reusable JSONL log:\n\n```js\nconst logger = createBuildchainLogger({\n path: \".buildchain/logs/native-build.jsonl\",\n source: \"user\",\n component: \"native-build\",\n});\n```\n\n## Library API\n\n```js\nimport {\n createBuildchainLogger,\n verifyBuildchainLogEvents,\n} from \"@kungfu-tech/buildchain/logging\";\n\nconst logger = createBuildchainLogger({\n source: \"user\",\n component: \"native-build\",\n});\n\nlogger.mark(\"configure.ready\", {\n phase: \"configure\",\n attributes: { preset: \"release\" },\n});\n\nawait logger.span(\"native.compile\", {\n phase: \"build\",\n attributes: { target: \"release\" },\n}, async () => {\n await compile();\n});\n\nlogger.spanSync(\"native.archive\", {\n phase: \"build\",\n attributes: { tool: \"libtool\" },\n}, () => {\n archiveStaticLibraries();\n});\n\nlogger.spawnSync(\"native.build\", \"make\", [\"-j20\"], {\n stdio: \"inherit\",\n}, {\n phase: \"build\",\n attributes: { requestedJobs: 20 },\n});\n\nconst report = verifyBuildchainLogEvents({\n path: logger.path,\n minEvents: 3,\n requirePhases: [\"configure\", \"build\"],\n requireEvents: [\n \"configure.ready\",\n \"native.compile.start\",\n \"native.compile.end\",\n ],\n});\n\nif (!report.ok) {\n throw new Error(\"Buildchain observability verification failed\");\n}\n```\n\nUse the API when a build script has internal stages that are invisible to the\nouter workflow. Keep secret values out of attributes; known sensitive keys are\nredacted, but callers should still avoid logging private material.\n\n## Candidate timelines and critical-path-safe timing\n\nLifecycle diagnostics retain their existing summed-duration tables for\ncompatibility. Do not interpret those tables as elapsed time when spans are\nnested or jobs run in parallel. Use the candidate timeline contract when a\nchange must be followed from pull-request admission through Merge Queue attempts\nto its final merge:\n\n```js\nimport {\n createCandidateTimeline,\n formatCandidateTimelineReport,\n} from \"@kungfu-tech/buildchain/candidate-timeline\";\n\nconst timeline = createCandidateTimeline({\n candidate: {\n repository: \"owner/repository\",\n baseBranch: \"dev/v4/v4.0\",\n sourceSha,\n pullRequest: 123,\n },\n events,\n});\n\nconsole.log(formatCandidateTimelineReport(timeline));\n```\n\nEach event binds a stable event id to one attempt id and may also bind a Gate\nid, merge-group SHA, workflow run, platform, partition, cache outcome, execution\nboundary, and parent span. Terminal execution states (`success`, `failure`, and\n`cancelled`) require start and completion timestamps. Non-executed states use\n`skipped`, `dependency-blocked`, or `not-required` without invented timings.\nRecord the clock and timestamp precision explicitly: GitHub Actions timestamps\nare normally provider wall-clock observations with one-second precision, while\nan in-process span may also carry a monotonic duration with millisecond or\nbetter precision.\n\nThe `buildchain.candidate-timeline/v1` artifact reports an independent critical\npath for every attempt. Its critical-path duration is the attempt's observed\nwall-clock envelope; its active duration and per-phase durations are interval\nunions. This prevents nested or parallel spans from being added twice, and it\ndoes not combine a failed or dequeued attempt with a later retry. Missing\nrequired measurements make that attempt `incomplete` instead of producing fake\nprecision.\n\nShell and workflow consumers can generate the same machine artifact plus a\ncompact report:\n\n```sh\nbuildchain candidate timeline \\\n --input .buildchain/candidate-timeline-input.json \\\n --output .buildchain/candidate-timeline.json\n```\n\nThe input and output should contain only bounded correlation facts and timing\nreceipts. Do not include tokens, environment dumps, full commands, raw process\ndumps, or private absolute paths.\n\nCommonJS scripts should import Buildchain's ESM surfaces dynamically:\n\n```js\nconst { createBuildchainLogger } = await import(\"@kungfu-tech/buildchain/logging\");\nconst { collectRunnerDiagnostics } = await import(\"@kungfu-tech/buildchain/diagnostics\");\n```\n\n## Diagnostics API\n\nThe diagnostics surface collects local, non-telemetry build facts that are\nuseful when a native build is slow or flaky:\n\n```js\nimport {\n collectBuildchainDiagnostics,\n collectCacheDiagnostics,\n collectCompilerCacheDiagnostics,\n collectRunnerDiagnostics,\n collectToolDiagnostics,\n detectRequestedParallelism,\n startProcessSampler,\n summarizeDiagnosticsArtifacts,\n summarizeLifecycleObservability,\n summarizeProcessSamples,\n validateAnchoredPackageRelease,\n writeDiagnosticsArtifact,\n} from \"@kungfu-tech/buildchain/diagnostics\";\n\nconst lifecycleObservability = summarizeLifecycleObservability({\n logPath: \".buildchain/logs/events.jsonl\",\n});\nconst buildCommand = \"make\";\nconst buildArgs = [\"-j20\"];\nconst requestedParallelism = detectRequestedParallelism({\n command: buildCommand,\n args: buildArgs,\n});\nconst processSampler = startProcessSampler({\n intervalMs: 15000,\n label: \"native-build\",\n command: buildCommand,\n args: buildArgs,\n});\n// Run the long native build while the sampler is active.\nconst processSummary = summarizeProcessSamples({\n requestedParallelism: requestedParallelism.value,\n samples: processSampler.stop(),\n});\nconst cacheDiagnostics = collectCacheDiagnostics({ cwd: process.cwd() });\n\nwriteDiagnosticsArtifact(\".buildchain/artifacts/diagnostics.json\", {\n contract: \"consumer-build-diagnostics\",\n buildchain: collectBuildchainDiagnostics({ cwd: process.cwd() }),\n runner: collectRunnerDiagnostics(),\n tools: collectToolDiagnostics({ cwd: process.cwd() }),\n cache: cacheDiagnostics,\n lifecycleObservability,\n process: processSummary,\n});\n```\n\n`collectCacheDiagnostics()` includes package-manager/workspace context, selected\ncache directory stats, and compiler-cache stats from `ccache --show-stats\n--json` plus `sccache --show-stats --stats-format json` when those tools are\npresent. If a ccache build does not support JSON stats, Buildchain falls back to\nplain `ccache --show-stats` and parses the text counters. Missing cache tools\nare recorded as unavailable instead of failing the diagnostics artifact. Call\n`collectCompilerCacheDiagnostics()` directly when a consumer script only needs\ncompiler cache data. Native diagnostics also expose `compilerCaches` and\n`nativeCacheDirs` as top-level fields in each diagnostics artifact and aggregate\nsummary, so reviewers do not have to dig through nested cache sections first.\n\nProcess samples are intentionally summarized before they become long-lived\nartifacts. The summary records requested parallelism, the source of that value\n(`command`, `process-tree`, `env:MAKEFLAGS`,\n`env:CMAKE_BUILD_PARALLEL_LEVEL`, or `explicit`), observed active process\nconcurrency, elapsed sample time, total sampled CPU, and conservative command\ncategories such as `compiler`, `archive`, `linker`, `build-tool`, and `cache`.\nThe sampler detects common `make -j N`, `ninja -j N`, CMake/MSBuild/Xcode job\nflags, and MAKEFLAGS. This lets native projects distinguish \"we asked for\n`make -j20`\" from \"the build graph only kept two active compiler or archive\nchildren busy during the sampled window\" without storing environment dumps.\n\nNative repositories can opt into a reusable diagnostics profile in\n`buildchain.toml`:\n\n```toml\n[diagnostics.native]\nenabled = true\nsample_process_tree = true\ncompiler_cache = \"auto\"\nexpected_tools = [\"ccache\", \"sccache\", \"clang\", \"cl\", \"cmake\", \"ninja\"]\nartifact_dirs = [\"build\", \"dist\", \"build/Release\"]\ncache_dirs = [\".ccache\", \".sccache\"]\n```\n\nWhen enabled, diagnostics artifacts include the normalized profile, selected\ntool versions, compiler-cache stats, and configured artifact/cache directory\nstats. The profile is data-driven: Buildchain does not assume a specific\nproject such as libnode. The reusable build workflow also exposes\n`sample-process-tree`, which wraps the build lifecycle with the process sampler\nand carries the generated summary into the final diagnostics artifact. Custom\nworkflows can call `buildchain sample process-tree` directly when they need a\ndifferent command boundary.\n\nAnchored/manual package projects can also run one higher-level release-shape\ncheck instead of assembling lower-level config calls:\n\n```js\nconst anchoredReport = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requireManifest: true,\n requirePackageSetOrder: \"platforms-first-main-last\",\n requireTrustedPublishing: true,\n});\n\nif (!anchoredReport.ok) {\n throw new Error(\"Anchored package release contract failed\");\n}\n```\n\nIn an actual publish job, make that check source-lock aware:\n\n```js\nconst publishReady = validateAnchoredPackageRelease({\n cwd: process.cwd(),\n requirePublishGateSourceLock: true,\n});\n```\n\nThe source-lock inputs are read from `BUILDCHAIN_PUBLISH_SOURCE_REF`,\n`BUILDCHAIN_PUBLISH_SOURCE_SHA`, and `BUILDCHAIN_PUBLISH_SOURCE_LOCKED`, which\nthe reusable build workflow emits after resolving `publish-source-ref`. That\nturns direct channel-branch publication from `alpha/*` or `release/*` into a\nhard failure, while `publish-gate/alpha/<line>/<version>` and\n`publish-gate/release/<line>/<version>` also validate the requested consumer\nversion against configured version files and the anchor manifest.\n\n`buildchain lifecycle run` writes this small diagnostics artifact next to the\nplatform manifest by default. The per-platform diagnostics upload includes the\ncompact `diagnostics.json`, `diagnostics-manifest.json`, lifecycle\n`events.jsonl`, and, when process sampling is enabled, copied\n`process-summary.json` and `process-samples.jsonl` sidecars. The sidecar\nmanifest records each uploaded diagnostics file with its relative path, byte\ncount, and sha256 hash. It is intended to stay small enough to download without\nfetching large binary packages, and it should not include full environment dumps\nor secret-looking values.\n\nUse `summarizeDiagnosticsArtifacts()` when a matrix build uploads one\ndiagnostics artifact per platform. The summary keeps each platform's lifecycle\nstage table, adds a lifecycle total duration, carries the top slow spans, and\naggregates warning/error counts plus the slowest platforms. Per-platform rows\nalso include compact runner facts, checked tool versions/missing tools, package\nmanager/cache directory details, compiler cache availability, and a compact\nprocess sampler view with requested parallelism, observed max active processes,\nthe ratio between them, sample count, process categories, and top sampled\ncommand basenames. That gives release reviewers a small cross-platform timing,\nrunner, tool, cache, and concurrency view without downloading full build outputs\nor process sidecars.\n\nThe reusable build workflow writes that rollup as `diagnostics-summary.json` and\nuploads it in a separate aggregate diagnostics summary artifact. Consumers can\nread the `build-diagnostics-summary-artifact` output when they need only timing,\nwarning/error, runner, cache, and process-sampler context instead of the build\nsummary or binary artifacts. Per-platform rows keep the diagnostics `links`\nobject, including the binary artifact name, manifest artifact name, diagnostics\nartifact name, platform id, diagnostics sidecar manifest path, manifest path,\nsummary path, and process sidecar paths when present. They also keep compact\nrunner/tool/cache summaries so reviewers can tell whether a slow row ran on the\nexpected runner, missed an expected tool, or lacked useful compiler-cache stats.\nWhen the downloaded platform diagnostics include a sibling\n`diagnostics-manifest.json`, `summarizeDiagnosticsArtifacts()` carries a compact\n`diagnosticsManifest` section for that platform and verifies the manifest's\n`diagnostics.json` byte count and sha256. Missing or mismatched sidecar manifests\nincrement `diagnosticsManifestWarningCount`, so reviewers can distinguish\ndiagnostics sidecar drift from lifecycle warnings or build failures.\nThe same summary carries `diagnosticsContract` per platform and aggregates\n`diagnosticsContractWarningCount` when a downloaded `diagnostics.json` does not\nmatch `BUILDCHAIN_DIAGNOSTICS_CONTRACT`.\n\nThe diagnostics SDK also exports stable contract constants such as\n`BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT`,\n`BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT`,\n`BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT`, and\n`BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT` from\n`@kungfu-tech/buildchain/diagnostics`; consumers should compare against those\nconstants instead of hardcoding contract strings.\n\nThe CLI exposes the same aggregation for shell and workflow steps:\n\n```bash\nbuildchain diagnostics summary \\\n .buildchain/artifacts/linux-x64/diagnostics.json \\\n .buildchain/artifacts/macos-arm64/diagnostics.json \\\n --output .buildchain/artifacts/diagnostics-summary.json \\\n --json\n```\n\nOmit `--json` when the workflow log should show a compact platform table with\nlifecycle stages, artifact scan/upload time, total time, requested jobs,\nobserved active processes, warnings, and errors.\n\nFor long native build commands, the CLI can also sample the child process tree\nwhile preserving the wrapped command's exit code:\n\n```bash\nbuildchain sample process-tree \\\n --label native-build \\\n --interval-ms 15000 \\\n --output .buildchain/diagnostics/process-samples.jsonl \\\n --summary-output .buildchain/diagnostics/process-summary.json \\\n -- \\\n make -j20\n```\n\nThe JSONL sample file stores timestamped process-tree snapshots with full\nredacted command lines, command basenames, CPU percentages when available,\nelapsed time, and requested parallelism context. Unix and macOS snapshots read\nthe full `ps args` field instead of truncated `comm` names. Windows snapshots\nread `Win32_Process` command lines through PowerShell so the sampler no longer\nreturns an empty process set on Windows runners. The summary file records\nobserved concurrency, total sampled CPU, command categories, and top command\nbasenames. This is intended for diagnosing low-utilization tails such as\narchive/link phases without logging full environment dumps.\n\nThe lifecycle observability summary is stage-wide, not just final-step timing:\nwhen install and build write to the same Buildchain log, the final platform\nmanifest can show both stages and the slowest spans.\n\n## CLI Logging\n\n```bash\nbuildchain mark \\\n --event native.configure \\\n --phase configure \\\n --component cmake \\\n --attribute preset=release\n\nbuildchain span \\\n --event native.build \\\n --phase build \\\n --component cmake \\\n -- cmake --build build --config Release\n\nbuildchain log summary --json\nbuildchain verify observability-log .buildchain/logs/events.jsonl --min-events 4\n```\n\nEvery event records a timestamp. `span` records duration and preserves the\nwrapped command's exit code.\n\n## Release Gate\n\nBuildchain's own binary distribution lane verifies required log events before\nuploading release assets. Consumers can apply the same pattern:\n\n```bash\nbuildchain verify observability-log .buildchain/logs/events.jsonl \\\n --require-phase build \\\n --require-phase package \\\n --require-component workflow \\\n --require-event native.build.start \\\n --require-event native.build.end\n```\n\nThis makes missing instrumentation a release failure instead of a dashboard\nafterthought."
1967
1972
  },
1968
1973
  {
1969
1974
  "id": "manual:versioning",
@@ -3,11 +3,11 @@
3
3
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
4
4
  "status": "passed",
5
5
  "summary": {
6
- "cliCommandCount": 85,
6
+ "cliCommandCount": 86,
7
7
  "workflowCount": 51,
8
8
  "actionCount": 4,
9
9
  "sitePageCount": 52,
10
- "docCommandRefCount": 264,
10
+ "docCommandRefCount": 265,
11
11
  "failureCount": 0
12
12
  },
13
13
  "enumerated": {
@@ -42,6 +42,11 @@
42
42
  "source": "bin/buildchain.mjs",
43
43
  "usage": "buildchain facts module [--cwd <dir>] [--module <id>] [--module-root <path>]"
44
44
  },
45
+ {
46
+ "id": "candidate",
47
+ "source": "bin/buildchain.mjs",
48
+ "usage": "buildchain candidate timeline --input <file-or-json> [--output <file>] [--json]"
49
+ },
45
50
  {
46
51
  "id": "collect",
47
52
  "source": "bin/buildchain.mjs",
@@ -2595,6 +2600,11 @@
2595
2600
  "path": "docs/cli.md",
2596
2601
  "command": "buildchain facts verify --fact"
2597
2602
  },
2603
+ {
2604
+ "id": "candidate",
2605
+ "path": "docs/toolkit-observability.md",
2606
+ "command": "buildchain candidate timeline"
2607
+ },
2598
2608
  {
2599
2609
  "id": "collect-github-release",
2600
2610
  "path": "docs/binary-distribution.md",
@@ -3736,9 +3746,9 @@
3736
3746
  "cliRegistryPath": "dist/site/cli-registry.json",
3737
3747
  "workflowRegistryPath": "dist/site/workflow-registry.json",
3738
3748
  "pageRegistryPath": "dist/site/page-registry.json",
3739
- "cliRegistryDigest": "07802d410c2111838c8c5f3d93392d7e8205ba64ab4c76ce594f09864ab5baaf",
3749
+ "cliRegistryDigest": "cc76cd0f061103d8ab78ee97dd0b7f0d28d64489f79b7ed06c94d8fef28bc200",
3740
3750
  "workflowRegistryDigest": "a954f67672292e16be1f4f663a98d0c69285f7359a43d5d6333b3ffcf6463c35",
3741
- "pageRegistryDigest": "75384a857698af54b7a739b40b6eefe0bb641e90bae584c47dd1e2c0b7aa8b2b"
3751
+ "pageRegistryDigest": "273a1baa5db4ae0b719c76b7f42dc055941f616b20947a38eb5eb48f5af1c431"
3742
3752
  },
3743
3753
  "comparison": {
3744
3754
  "missingCliRegistry": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-07-22T15:29:55.552Z",
5
- "publishedAt": "2026-07-22T15:29:55.552Z",
4
+ "generatedAt": "2026-07-23T00:58:45.650Z",
5
+ "publishedAt": "2026-07-23T00:58:45.650Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "a013078fdf88b5b850e48e93faf58de582130440",
22
+ "sourceRevision": "d67ebd6fcb075c00ef286cee9f2bc1f03633af13",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "2.14.17-alpha.0",
35
+ "version": "2.14.17-alpha.2",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -16,6 +16,7 @@
16
16
  "./artifact-verification-envelope": "./packages/core/artifact-verification-envelope.js",
17
17
  "./anchored-version-material": "./packages/core/anchored-version-material.js",
18
18
  "./buildchain-contract": "./packages/core/buildchain-contract.js",
19
+ "./candidate-timeline": "./packages/core/candidate-timeline.js",
19
20
  "./controller-evidence": "./packages/core/controller-evidence.js",
20
21
  "./diagnostics": "./packages/core/diagnostics.js",
21
22
  "./homebrew": "./packages/core/homebrew.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-07-22T15:29:55.552Z",
5
- "publishedAt": "2026-07-22T15:29:55.552Z",
4
+ "generatedAt": "2026-07-23T00:58:45.650Z",
5
+ "publishedAt": "2026-07-23T00:58:45.650Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "a013078fdf88b5b850e48e93faf58de582130440",
22
+ "sourceRevision": "d67ebd6fcb075c00ef286cee9f2bc1f03633af13",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "2.14.17-alpha.0",
40
+ "version": "2.14.17-alpha.2",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -149,7 +149,7 @@
149
149
  "path": "docs/toolkit-observability.md",
150
150
  "plane": "use",
151
151
  "exists": true,
152
- "digest": "sha256:239949a75ca81b6002a2e148f7028402dbd98c00e246acd8d0955855781206a1"
152
+ "digest": "sha256:ed6c2d0a7e76a5e4fc3fbee635966e462947f748a52d88e515fe4f95a630ac91"
153
153
  },
154
154
  {
155
155
  "id": "site-bundle-contract",
@@ -94,6 +94,64 @@ Use the API when a build script has internal stages that are invisible to the
94
94
  outer workflow. Keep secret values out of attributes; known sensitive keys are
95
95
  redacted, but callers should still avoid logging private material.
96
96
 
97
+ ## Candidate timelines and critical-path-safe timing
98
+
99
+ Lifecycle diagnostics retain their existing summed-duration tables for
100
+ compatibility. Do not interpret those tables as elapsed time when spans are
101
+ nested or jobs run in parallel. Use the candidate timeline contract when a
102
+ change must be followed from pull-request admission through Merge Queue attempts
103
+ to its final merge:
104
+
105
+ ```js
106
+ import {
107
+ createCandidateTimeline,
108
+ formatCandidateTimelineReport,
109
+ } from "@kungfu-tech/buildchain/candidate-timeline";
110
+
111
+ const timeline = createCandidateTimeline({
112
+ candidate: {
113
+ repository: "owner/repository",
114
+ baseBranch: "dev/v4/v4.0",
115
+ sourceSha,
116
+ pullRequest: 123,
117
+ },
118
+ events,
119
+ });
120
+
121
+ console.log(formatCandidateTimelineReport(timeline));
122
+ ```
123
+
124
+ Each event binds a stable event id to one attempt id and may also bind a Gate
125
+ id, merge-group SHA, workflow run, platform, partition, cache outcome, execution
126
+ boundary, and parent span. Terminal execution states (`success`, `failure`, and
127
+ `cancelled`) require start and completion timestamps. Non-executed states use
128
+ `skipped`, `dependency-blocked`, or `not-required` without invented timings.
129
+ Record the clock and timestamp precision explicitly: GitHub Actions timestamps
130
+ are normally provider wall-clock observations with one-second precision, while
131
+ an in-process span may also carry a monotonic duration with millisecond or
132
+ better precision.
133
+
134
+ The `buildchain.candidate-timeline/v1` artifact reports an independent critical
135
+ path for every attempt. Its critical-path duration is the attempt's observed
136
+ wall-clock envelope; its active duration and per-phase durations are interval
137
+ unions. This prevents nested or parallel spans from being added twice, and it
138
+ does not combine a failed or dequeued attempt with a later retry. Missing
139
+ required measurements make that attempt `incomplete` instead of producing fake
140
+ precision.
141
+
142
+ Shell and workflow consumers can generate the same machine artifact plus a
143
+ compact report:
144
+
145
+ ```sh
146
+ buildchain candidate timeline \
147
+ --input .buildchain/candidate-timeline-input.json \
148
+ --output .buildchain/candidate-timeline.json
149
+ ```
150
+
151
+ The input and output should contain only bounded correlation facts and timing
152
+ receipts. Do not include tokens, environment dumps, full commands, raw process
153
+ dumps, or private absolute paths.
154
+
97
155
  CommonJS scripts should import Buildchain's ESM surfaces dynamically:
98
156
 
99
157
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.14.17-alpha.0",
3
+ "version": "2.14.17-alpha.2",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -19,6 +19,7 @@
19
19
  "./artifact-verification-envelope": "./packages/core/artifact-verification-envelope.js",
20
20
  "./anchored-version-material": "./packages/core/anchored-version-material.js",
21
21
  "./buildchain-contract": "./packages/core/buildchain-contract.js",
22
+ "./candidate-timeline": "./packages/core/candidate-timeline.js",
22
23
  "./controller-evidence": "./packages/core/controller-evidence.js",
23
24
  "./diagnostics": "./packages/core/diagnostics.js",
24
25
  "./homebrew": "./packages/core/homebrew.js",
@@ -351,6 +351,9 @@ export function createBuildchainContractWorld({
351
351
  "publish transactions can require a resolved publish-gate source lock to prevent floating-ref drift",
352
352
  "semver GitHub Releases are created or updated only after transaction completion, with prerelease/latest metadata bound to the authoritative publication channel and tag syntax retained as the fallback for ordinary callers",
353
353
  ],
354
+ compatibleBreakingDigests: [
355
+ "sha256:a59f0910e6df842e7699139472e5dd69ac2fdd7f7213bf2cb346d1d622556874",
356
+ ],
354
357
  }),
355
358
  surface(root, {
356
359
  id: "report-buildchain-issue-action",
@@ -758,7 +761,11 @@ export function evaluateBuildchainContractLock({
758
761
  reasons.push(`surface removed: ${oldSurface.id}`);
759
762
  continue;
760
763
  }
761
- if (nextSurface.breakingDigest !== oldSurface.breakingDigest) {
764
+ const compatibleBreakingDigests = new Set(nextSurface.compatibleBreakingDigests || []);
765
+ if (
766
+ nextSurface.breakingDigest !== oldSurface.breakingDigest
767
+ && !compatibleBreakingDigests.has(oldSurface.breakingDigest)
768
+ ) {
762
769
  reasons.push(`surface breaking digest changed: ${oldSurface.id}`);
763
770
  }
764
771
  }
@@ -0,0 +1,349 @@
1
+ export const BUILDCHAIN_CANDIDATE_TIMELINE_CONTRACT =
2
+ "buildchain.candidate-timeline/v1";
3
+ export const BUILDCHAIN_CANDIDATE_TIMELINE_EVENT_CONTRACT =
4
+ "buildchain.candidate-timeline-event/v1";
5
+
6
+ const EVENT_STATUSES = new Set([
7
+ "success",
8
+ "failure",
9
+ "cancelled",
10
+ "skipped",
11
+ "dependency-blocked",
12
+ "not-required",
13
+ "unknown",
14
+ ]);
15
+
16
+ function compactObject(value) {
17
+ return Object.fromEntries(
18
+ Object.entries(value).filter(
19
+ ([, entry]) => entry !== undefined && entry !== "",
20
+ ),
21
+ );
22
+ }
23
+
24
+ function finiteNumber(value, fallback = undefined) {
25
+ const parsed = Number(value);
26
+ return Number.isFinite(parsed) ? parsed : fallback;
27
+ }
28
+
29
+ function timestampMs(value) {
30
+ const parsed = Date.parse(value || "");
31
+ return Number.isFinite(parsed) ? parsed : null;
32
+ }
33
+
34
+ function normalizeTiming(input = {}) {
35
+ const startedAtMs = timestampMs(input.startedAt);
36
+ const completedAtMs = timestampMs(input.completedAt);
37
+ const durationMs = finiteNumber(input.durationMs);
38
+ const measured =
39
+ startedAtMs !== null &&
40
+ completedAtMs !== null &&
41
+ completedAtMs >= startedAtMs;
42
+ return compactObject({
43
+ startedAt: measured ? new Date(startedAtMs).toISOString() : undefined,
44
+ completedAt: measured ? new Date(completedAtMs).toISOString() : undefined,
45
+ durationMs:
46
+ durationMs !== undefined && durationMs >= 0
47
+ ? Math.round(durationMs * 1000) / 1000
48
+ : measured
49
+ ? completedAtMs - startedAtMs
50
+ : undefined,
51
+ clock: input.clock || (measured ? "wall" : "unknown"),
52
+ precisionMs: finiteNumber(input.precisionMs, measured ? 1000 : undefined),
53
+ authority: input.authority,
54
+ measured,
55
+ });
56
+ }
57
+
58
+ function normalizeAttempt(input = {}) {
59
+ if (!input.id) {
60
+ throw new Error("candidate timeline event attempt.id is required");
61
+ }
62
+ return compactObject({
63
+ id: String(input.id),
64
+ index: Number.isInteger(input.index) ? input.index : undefined,
65
+ kind: input.kind || "unknown",
66
+ mergeGroupSha: input.mergeGroupSha,
67
+ workflowRunId:
68
+ input.workflowRunId === undefined
69
+ ? undefined
70
+ : String(input.workflowRunId),
71
+ });
72
+ }
73
+
74
+ export function normalizeCandidateTimelineEvent(input = {}) {
75
+ if (!input.id) {
76
+ throw new Error("candidate timeline event id is required");
77
+ }
78
+ if (!input.phase) {
79
+ throw new Error(`candidate timeline event ${input.id} phase is required`);
80
+ }
81
+ const status = input.status || "unknown";
82
+ if (!EVENT_STATUSES.has(status)) {
83
+ throw new Error(
84
+ `candidate timeline event ${input.id} has invalid status: ${status}`,
85
+ );
86
+ }
87
+ const timing = normalizeTiming(input.timing);
88
+ if (
89
+ ["success", "failure", "cancelled"].includes(status) &&
90
+ !timing.measured
91
+ ) {
92
+ throw new Error(
93
+ `candidate timeline event ${input.id} status ${status} requires a measured interval`,
94
+ );
95
+ }
96
+ return compactObject({
97
+ contract: BUILDCHAIN_CANDIDATE_TIMELINE_EVENT_CONTRACT,
98
+ id: String(input.id),
99
+ attempt: normalizeAttempt(input.attempt),
100
+ phase: String(input.phase),
101
+ category: input.category || "gate",
102
+ status,
103
+ gate: input.gate
104
+ ? compactObject({
105
+ id: input.gate.id,
106
+ platform: input.gate.platform,
107
+ partition:
108
+ input.gate.partition === undefined
109
+ ? undefined
110
+ : String(input.gate.partition),
111
+ })
112
+ : undefined,
113
+ span: input.span
114
+ ? compactObject({
115
+ id: input.span.id,
116
+ parentId: input.span.parentId,
117
+ })
118
+ : undefined,
119
+ execution: input.execution
120
+ ? compactObject({
121
+ boundary: input.execution.boundary,
122
+ runner: input.execution.runner,
123
+ })
124
+ : undefined,
125
+ cache: input.cache
126
+ ? compactObject({
127
+ layer: input.cache.layer,
128
+ outcome: input.cache.outcome,
129
+ })
130
+ : undefined,
131
+ timing,
132
+ criticalPathEligible: input.criticalPathEligible !== false,
133
+ attributes: input.attributes || {},
134
+ });
135
+ }
136
+
137
+ function interval(event) {
138
+ if (!event.timing.measured || !event.criticalPathEligible) return null;
139
+ return [
140
+ Date.parse(event.timing.startedAt),
141
+ Date.parse(event.timing.completedAt),
142
+ ];
143
+ }
144
+
145
+ function intervalUnionMs(intervals) {
146
+ const ordered = intervals
147
+ .filter(Boolean)
148
+ .map(([start, end]) => [start, end])
149
+ .sort((left, right) => left[0] - right[0] || left[1] - right[1]);
150
+ if (!ordered.length) return 0;
151
+ let total = 0;
152
+ let [currentStart, currentEnd] = ordered[0];
153
+ for (const [start, end] of ordered.slice(1)) {
154
+ if (start <= currentEnd) {
155
+ currentEnd = Math.max(currentEnd, end);
156
+ } else {
157
+ total += currentEnd - currentStart;
158
+ currentStart = start;
159
+ currentEnd = end;
160
+ }
161
+ }
162
+ return total + currentEnd - currentStart;
163
+ }
164
+
165
+ function groupIntervalSummary(events, field) {
166
+ const values = [
167
+ ...new Set(events.map((event) => event[field] || "unknown")),
168
+ ].sort();
169
+ return Object.fromEntries(
170
+ values.map((value) => {
171
+ const selected = events.filter(
172
+ (event) => (event[field] || "unknown") === value,
173
+ );
174
+ return [
175
+ value,
176
+ {
177
+ eventCount: selected.length,
178
+ measuredEventCount: selected.filter((event) => event.timing.measured)
179
+ .length,
180
+ intervalUnionMs: intervalUnionMs(selected.map(interval)),
181
+ },
182
+ ];
183
+ }),
184
+ );
185
+ }
186
+
187
+ function attemptOrder(left, right) {
188
+ const leftIndex = left.attempt.index;
189
+ const rightIndex = right.attempt.index;
190
+ if (Number.isInteger(leftIndex) && Number.isInteger(rightIndex)) {
191
+ return leftIndex - rightIndex;
192
+ }
193
+ return left.attempt.id.localeCompare(right.attempt.id);
194
+ }
195
+
196
+ function summarizeAttempt(events) {
197
+ const ordered = [...events].sort(attemptOrder);
198
+ const measured = ordered.filter(
199
+ (event) => event.timing.measured && event.criticalPathEligible,
200
+ );
201
+ const intervals = measured.map(interval);
202
+ const starts = intervals.map(([start]) => start);
203
+ const ends = intervals.map(([, end]) => end);
204
+ const startedAtMs = starts.length ? Math.min(...starts) : null;
205
+ const completedAtMs = ends.length ? Math.max(...ends) : null;
206
+ const activeUnionMs = intervalUnionMs(intervals);
207
+ const elapsedMs =
208
+ startedAtMs === null || completedAtMs === null
209
+ ? null
210
+ : completedAtMs - startedAtMs;
211
+ const unmeasured = ordered.filter(
212
+ (event) =>
213
+ event.criticalPathEligible &&
214
+ !event.timing.measured &&
215
+ !["skipped", "dependency-blocked", "not-required"].includes(event.status),
216
+ );
217
+ return {
218
+ attempt: ordered[0].attempt,
219
+ status: ordered.some((event) => event.status === "failure")
220
+ ? "failure"
221
+ : ordered.some((event) => event.status === "cancelled")
222
+ ? "cancelled"
223
+ : unmeasured.length
224
+ ? "incomplete"
225
+ : "observed",
226
+ eventCount: ordered.length,
227
+ measuredEventCount: ordered.filter((event) => event.timing.measured).length,
228
+ phases: groupIntervalSummary(ordered, "phase"),
229
+ categories: groupIntervalSummary(ordered, "category"),
230
+ criticalPath: {
231
+ method: "attempt-wall-clock-envelope",
232
+ status: unmeasured.length
233
+ ? "incomplete"
234
+ : measured.length
235
+ ? "observed"
236
+ : "not-observed",
237
+ startedAt:
238
+ startedAtMs === null ? null : new Date(startedAtMs).toISOString(),
239
+ completedAt:
240
+ completedAtMs === null ? null : new Date(completedAtMs).toISOString(),
241
+ durationMs: elapsedMs,
242
+ activeIntervalUnionMs: activeUnionMs,
243
+ idleOrUnobservedMs:
244
+ elapsedMs === null ? null : Math.max(0, elapsedMs - activeUnionMs),
245
+ unmeasuredEventIds: unmeasured.map((event) => event.id),
246
+ note: "The attempt envelope is wall-clock elapsed time. Interval unions avoid summing nested or parallel spans; attempts are never combined into one critical path.",
247
+ },
248
+ };
249
+ }
250
+
251
+ function normalizeCandidate(input = {}) {
252
+ if (!input.sourceSha) {
253
+ throw new Error("candidate timeline candidate.sourceSha is required");
254
+ }
255
+ return compactObject({
256
+ repository: input.repository,
257
+ baseBranch: input.baseBranch,
258
+ sourceSha: String(input.sourceSha),
259
+ pullRequest:
260
+ input.pullRequest === undefined ? undefined : Number(input.pullRequest),
261
+ mergedAt:
262
+ timestampMs(input.mergedAt) === null
263
+ ? undefined
264
+ : new Date(timestampMs(input.mergedAt)).toISOString(),
265
+ });
266
+ }
267
+
268
+ export function createCandidateTimeline({
269
+ candidate = {},
270
+ events = [],
271
+ generatedAt,
272
+ } = {}) {
273
+ const normalizedCandidate = normalizeCandidate(candidate);
274
+ const normalizedEvents = events.map(normalizeCandidateTimelineEvent);
275
+ const sourceDrift = normalizedEvents.filter(
276
+ (event) =>
277
+ event.attributes?.sourceSha &&
278
+ event.attributes.sourceSha !== normalizedCandidate.sourceSha,
279
+ );
280
+ if (sourceDrift.length) {
281
+ throw new Error(
282
+ `candidate timeline source drift in events: ${sourceDrift.map((event) => event.id).join(", ")}`,
283
+ );
284
+ }
285
+ const attemptIds = [
286
+ ...new Set(normalizedEvents.map((event) => event.attempt.id)),
287
+ ];
288
+ const attempts = attemptIds
289
+ .map((attemptId) =>
290
+ summarizeAttempt(
291
+ normalizedEvents.filter((event) => event.attempt.id === attemptId),
292
+ ),
293
+ )
294
+ .sort((left, right) => {
295
+ const leftIndex = left.attempt.index;
296
+ const rightIndex = right.attempt.index;
297
+ if (Number.isInteger(leftIndex) && Number.isInteger(rightIndex)) {
298
+ return leftIndex - rightIndex;
299
+ }
300
+ return left.attempt.id.localeCompare(right.attempt.id);
301
+ });
302
+ return {
303
+ schemaVersion: 1,
304
+ contract: BUILDCHAIN_CANDIDATE_TIMELINE_CONTRACT,
305
+ generatedAt: generatedAt || new Date().toISOString(),
306
+ candidate: normalizedCandidate,
307
+ summary: {
308
+ attemptCount: attempts.length,
309
+ eventCount: normalizedEvents.length,
310
+ measuredEventCount: normalizedEvents.filter(
311
+ (event) => event.timing.measured,
312
+ ).length,
313
+ status: attempts.some((attempt) => attempt.status === "failure")
314
+ ? "failure"
315
+ : attempts.some((attempt) => attempt.status === "incomplete")
316
+ ? "incomplete"
317
+ : "observed",
318
+ criticalPathScope: "per-attempt-only",
319
+ },
320
+ attempts,
321
+ events: normalizedEvents,
322
+ };
323
+ }
324
+
325
+ export function formatCandidateTimelineReport(timeline) {
326
+ const lines = [
327
+ `Candidate ${timeline.candidate.repository || "repository"}#${timeline.candidate.pullRequest || "?"} ${timeline.candidate.sourceSha}`,
328
+ `status=${timeline.summary.status} attempts=${timeline.summary.attemptCount} events=${timeline.summary.eventCount} measured=${timeline.summary.measuredEventCount}`,
329
+ ];
330
+ for (const attempt of timeline.attempts) {
331
+ const duration = attempt.criticalPath.durationMs;
332
+ lines.push(
333
+ [
334
+ `attempt=${attempt.attempt.id}`,
335
+ `kind=${attempt.attempt.kind}`,
336
+ `status=${attempt.status}`,
337
+ `elapsed=${duration === null ? "unknown" : `${duration}ms`}`,
338
+ `active=${attempt.criticalPath.activeIntervalUnionMs}ms`,
339
+ `unmeasured=${attempt.criticalPath.unmeasuredEventIds.length}`,
340
+ ].join(" "),
341
+ );
342
+ for (const [phase, summary] of Object.entries(attempt.phases)) {
343
+ lines.push(
344
+ ` phase=${phase} union=${summary.intervalUnionMs}ms measured=${summary.measuredEventCount}/${summary.eventCount}`,
345
+ );
346
+ }
347
+ }
348
+ return lines.join("\n");
349
+ }
@@ -82,6 +82,14 @@ export {
82
82
  verifyBuildchainLogEvents,
83
83
  } from "./logging.js";
84
84
 
85
+ export {
86
+ BUILDCHAIN_CANDIDATE_TIMELINE_CONTRACT,
87
+ BUILDCHAIN_CANDIDATE_TIMELINE_EVENT_CONTRACT,
88
+ createCandidateTimeline,
89
+ formatCandidateTimelineReport,
90
+ normalizeCandidateTimelineEvent,
91
+ } from "./candidate-timeline.js";
92
+
85
93
  export {
86
94
  BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT,
87
95
  BUILDCHAIN_DIAGNOSTICS_CONTRACT,
@@ -390,6 +390,8 @@ function cliCommandMeta(id) {
390
390
  "badges-readme": { group: "distribution-indexes", purpose: "Generate or verify managed README badge blocks." },
391
391
  "build-contract": { group: "governance-versioning", purpose: "Resolve Buildchain runtime contract metadata for floating-ref drift checks." },
392
392
  "build-facts": { group: "observability-diagnostics", purpose: "Collect and verify Git source, version, module output, product artifact, and legacy Kungfu buildinfo facts." },
393
+ candidate: { group: "observability-diagnostics", purpose: "Inspect source-bound candidate evidence command families." },
394
+ "candidate-timeline": { group: "observability-diagnostics", purpose: "Create a critical-path-safe timeline across pull-request and Merge Queue attempts." },
393
395
  collect: { group: "release-passport-trust", purpose: "Inspect release evidence collection command families." },
394
396
  "collect-github-release": { group: "release-passport-trust", purpose: "Collect GitHub Release assets into a release passport." },
395
397
  create: { group: "release-passport-trust", purpose: "Create canonical sealed publication evidence documents." },
@@ -495,6 +497,7 @@ function nodeApiMeta(exportName) {
495
497
  "./readme-badges": { group: "distribution-indexes", summary: "Managed README badge block facts, rendering, drift checks, and updates." },
496
498
  "./homebrew": { group: "distribution-indexes", summary: "Homebrew tap fact collection, Formula rendering, update, and check APIs." },
497
499
  "./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
500
+ "./candidate-timeline": { group: "observability-diagnostics", summary: "Source-bound candidate event normalization, per-attempt critical-path-safe aggregation, and compact reporting APIs." },
498
501
  "./diagnostics": { group: "observability-diagnostics", summary: "Native diagnostics collection, summarization, cache, compiler, and process-sampler APIs." },
499
502
  "./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
500
503
  "./portable-dev-cache": { group: "observability-diagnostics", summary: "Portable dependency/compiler cache plan, exact-root verification, and provider receipt APIs." },
@@ -8,6 +8,7 @@ const TRUSTED_PERMISSIONS = new Set(["write", "maintain", "admin"]);
8
8
 
9
9
  export function resolveWebSurfaceProductionDecision({
10
10
  eventName,
11
+ eventAction = "",
11
12
  refName,
12
13
  repository,
13
14
  sourceSha,
@@ -36,9 +37,11 @@ export function resolveWebSurfaceProductionDecision({
36
37
  reason: trusted ? "trusted-manual-dispatch" : "manual-actor-permission-insufficient",
37
38
  });
38
39
  }
40
+ const reviewedReleaseEvent =
41
+ (eventName === "push" && refName === "main") ||
42
+ (eventName === "pull_request" && eventAction === "closed");
39
43
  if (
40
- eventName === "push" &&
41
- refName === "main" &&
44
+ reviewedReleaseEvent &&
42
45
  productionApply === true &&
43
46
  productionReleaseOnMain === true &&
44
47
  releaseApproved === true
@@ -103,9 +106,10 @@ async function main() {
103
106
  }
104
107
  const decision = resolveWebSurfaceProductionDecision({
105
108
  eventName,
109
+ eventAction: process.env.GITHUB_EVENT_ACTION,
106
110
  refName: process.env.GITHUB_REF_NAME,
107
111
  repository,
108
- sourceSha: process.env.GITHUB_SHA,
112
+ sourceSha: process.env.BUILDCHAIN_PRODUCTION_SOURCE_SHA || process.env.GITHUB_SHA,
109
113
  actor,
110
114
  productionApply: bool("BUILDCHAIN_PRODUCTION_APPLY"),
111
115
  productionApproved: bool("BUILDCHAIN_PRODUCTION_APPROVED"),