Skip to content

chore(build-cli): reshape bundleSize library to pure-function primitives#27321

Open
ChumpChief wants to merge 22 commits into
microsoft:mainfrom
ChumpChief:bundle-size-library-reshape
Open

chore(build-cli): reshape bundleSize library to pure-function primitives#27321
ChumpChief wants to merge 22 commits into
microsoft:mainfrom
ChumpChief:bundle-size-library-reshape

Conversation

@ChumpChief
Copy link
Copy Markdown
Contributor

@ChumpChief ChumpChief commented May 15, 2026

Implements AB#73449 — followup to PR #27298 (AB#73299), continuing the bundle-size rework. Replaces the class-based ADOSizeComparator with pure-function primitives, redesigns the comparison types, and pulls generic helpers out of the bundleSize library.

Highlights

  • bundleSize/ADO/ and ADOSizeComparator go away. New primitives (compareJsonReportsByPackage, extractAnalyzerJsonsFromArtifact, readAnalyzerJsonsFromFileSystem, sourcePackageFromAnalyzerPath) compose inside check bundleSize.
  • Type redesign. BundleData / BundlesComparison / PackageComparison. Field presence on each bundle entry encodes added vs removed vs both-sides — previously silently dropped asymmetric assets.
  • Generic helpers promoted out of bundleSize/. library/azureDevops/getArtifactForCommit.ts (find a build for a commit, download its artifact). library/git/pickFreshestRemote.ts (PR feat(build-cli): rework bundle size check for inner-dev-loop use #27298's canonical-remote auto-detect, now parameterized via a URL-matcher callback).
  • Drop the "command never throws" contract. With the diff moving to a follow-up GH Actions workflow rather than a CI pipeline, the kind: "error" result variant was over-engineered. Use oclif's this.error() for fatals — matches the rest of the codebase (bump.ts, list.ts, etc.).
  • downloadArtifact HTTP-status check. Missing artifacts now surface "Failed to download artifact X from build Y (HTTP 404)" instead of fflate's cryptic "invalid zip data."

Followup

Converge getArtifactForCommit / getBaselineBuildMetrics / getBuildArtifactForSpecificBuild around a shared "find build by selector + download artifact" pattern (AB#73595).

🤖 Generated with Claude Code

ChumpChief and others added 16 commits May 15, 2026 12:20
Lays the new library shape down alongside the existing AdoSizeComparator
path so nothing breaks yet. The migration off the class-based path lands
in follow-up commits.

New files:
- library/bundleSize/types.ts gains BundleData / BundlesComparison /
  PackageComparison / AnalyzerJsonByPackage. Old BundleMetric /
  BundleSummaries / BundleComparison remain temporarily.
- library/bundleSize/sourcePackageFromAnalyzerPath.ts — classifier for the
  nested `<sourcePackage>/analyzer.json` artifact layout. (No `__`
  encoding; the flatten was tried and reverted under AB#73299.)
- library/bundleSize/compareJsonReports.ts and
  compareJsonReportsByPackage.ts — pure-function comparison primitives.
  Field-presence on each entry encodes added/removed (vs the old
  "log and skip" behavior in compareBundles.ts).
- library/bundleSize/extractAnalyzerJsonsFromArtifact.ts and
  readAnalyzerJsonsFromFileSystem.ts — turn an artifact's ArtifactContents
  or a local reports dir into `AnalyzerJsonByPackage`.
- library/azureDevops/getArtifactForCommit.ts — generic "find a build for
  a commit on this pipeline and download an artifact from it." Replaces
  the inline scan inside AdoSizeComparator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The command no longer goes through ADOSizeComparator. Instead it threads:

  pickFreshestCanonicalRemote → getMergeBaseWithHead → getArtifactForCommit
                              → extractAnalyzerJsonsFromArtifact
                              → readAnalyzerJsonsFromFileSystem
                              → compareJsonReportsByPackage

The user-facing behavior is preserved: same `--target` flag, same
auto-detect logging, same per-asset parsed+gzip output for the common
case. The PackageComparison shape additionally distinguishes added /
removed bundles via field presence — bundles present on only one side
are now reported explicitly instead of being silently dropped.

ADOSizeComparator (and the rest of bundleSize/ADO/) is now unreferenced
and gets deleted in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`check bundleSize` no longer references any of these as of the previous
commit, and nothing else does. Drop:

- library/bundleSize/ADO/ — the whole subtree (AdoArtifactFileProvider,
  AdoSizeComparator, Constants, FileSystemBundleFileProvider,
  getBundleFilePathsFromFolder, getBundleSummaries, index).
- library/bundleSize/compareBundles.ts (replaced by compareJsonReports
  and compareJsonReportsByPackage).
- library/bundleSize/utilities/getBuilds.ts (replaced by
  azureDevops/getArtifactForCommit.ts).
- library/bundleSize/utilities/getAllFilesInDirectory.ts (only consumer
  was the deleted ADO/ subtree; readAnalyzerJsonsFromFileSystem has its
  own local walker).
- Legacy types from library/bundleSize/types.ts: BundleMetric,
  BundleMetricSet, BundleSummaries, BundleComparison.

Library barrel and utilities/index.ts trimmed to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The contents of `library/bundleSize/utilities/gitCommands.ts` had no
bundle-size concepts in them. Two functions were also FF-specific
(hardcoded `microsoft/FluidFramework` URL regex + log strings) but the
specificity belonged at the call site, not in a "git utilities" file.

Split it:
- Generic git helpers (`getMergeBaseWithHead`, plus the previously-
  pickFreshestCanonicalRemote logic renamed and reshaped to
  `pickFreshestRemote(branch, isEligible)`) move to
  `library/git/gitCommands.ts`. `isEligible: (url: string) => boolean`
  decides which remotes are candidates; the function knows nothing
  about FF. Log strings are neutral ("Eligible remotes:" / "No eligible
  remote has […] fetched locally") — callers provide context above.
- `check bundleSize` defines the FF-canonical URL regex inline and
  passes it as the `isEligible` predicate.

The now-empty `library/bundleSize/utilities/` directory is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…thHead

The file's name described its implementation ("git commands") rather than
its purpose. After this commit it holds exactly one exported function
(`pickFreshestRemote`) and the file name says so.

`getMergeBaseWithHead` had a single call site and was a one-liner around
`execFileSync('git', ['merge-base', targetRef, 'HEAD'])`. Inline it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `extractAnalyzerJsonsFromArtifact` is synchronous, so wrapping it in
  `Promise.resolve()` to feed `Promise.all` just adds noise — `Promise.all`
  accepts non-Promise values, and there's no real overlap to gain anyway
  (only the filesystem read is async). Replace with two statements.
- The empty-inputs guard fires only when *both* sides are empty (`&&`),
  but the warning text said "baseline artifact or local bundle reports
  are empty," reading as either. Reword to match the condition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous PR's `ADOSizeComparator.getSizeComparison()` wrapped its
body in a try/catch that converted any throw into the `error` variant
of its discriminated return. Deleting that class moved several throw-
prone calls (the merge-base shell-out, `pickFreshestRemote` on old git,
`readAnalyzerJsonsFromFileSystem` on bad paths, …) into the command's
`run()` with no equivalent guard, regressing the "always returns a
`CheckBundleSizeResult`" contract that `--json` consumers depend on.

Single top-level try/catch in `run()` restores the property. The catch
synthesizes the same error variant the per-step error paths produce.
Doesn't try to carry `baselineCommit` forward — the catch path returns
`baselineCommit: undefined` regardless of where in the flow the throw
landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the change-line builder out of `run()` into a `formatComparison`
helper that turns a `PackageComparison` into a flat list of
human-readable lines. The command keeps the orchestration; the helper
handles presentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "always returns a result variant, never throws" contract was carried
over from when CI pipelines consumed the JSON output. With the diff
moving to a followup GH-Actions workflow and this command now positioned
for inner-dev-loop use, the contract isn't doing useful work — oclif's
default error handling (clean message on stderr, non-zero exit) is the
codebase's convention for fatal cases (see `bump.ts`, `list.ts`, etc.,
which use `this.error()` even with `enableJsonFlag = true`).

Changes:
- Drop the `kind: "error"` variant from `CheckBundleSizeResult`. The
  result is now just `no-changes | changes`.
- Drop the top-level try/catch from `run()`. Unexpected throws
  (filesystem errors, unresolvable refs, …) propagate to oclif.
- Convert the per-step error returns to `this.error(...)`: artifact
  lookup failures and the "both sides empty" guard.
- Drop the silent `origin` fallback when no canonical remote is found.
  That fallback was usually wrong anyway (origin is typically the
  developer's fork) and silently produced bad merge-bases. Error with
  an actionable message pointing at both resolutions: add a
  microsoft/FluidFramework remote, or pass --target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scriminated union

The `kind: "found" | "error"` shape was an artifact of the now-removed
"command never throws" contract. With the command using `this.error()`
for fatal cases, the sibling `getBaselineBuildMetrics.ts` pattern of
throwing on "couldn't produce the result" applies cleanly here too.
Callsite drops the `if (kind === "error")` branch.

The artifact-download catch is kept (it adds useful context — which
artifact, which commit) but reworded neutrally so it doesn't lie about
network/auth failures, and chained via `{ cause: e }` so the underlying
error is preserved instead of stringified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`buildApi.getArtifactContentZip` returns a `ReadableStream` per its
declared type, but the underlying object is a Node `http.IncomingMessage`
and the SDK doesn't throw on non-2xx for this endpoint. A missing
artifact comes back as a 404 with a non-zip body, which fflate then
greets with the unhelpful "invalid zip data" error.

Peek at `statusCode` via a `Partial<IncomingMessage>` assertion and
throw a meaningful error before unzip. The assertion is defensive — if
a future SDK update returns something without a `statusCode`, the
existing unzip-time error remains the fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ffix"

The split/check/pop/length/join chain hid a simple intent: if the path
ends with `/analyzer.json`, return everything before it; else undefined.
Replace with `endsWith` + `slice`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two functions are tightly coupled — `compareJsonReportsByPackage`
is just a per-package fanout of `compareJsonReports`, with no
external caller for the per-package function. Move both into the
same file, unexport `compareJsonReports` (now a private helper),
delete the empty `compareJsonReportsByPackage.ts`, and update the
barrel to re-export only the public `compareJsonReportsByPackage`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kind flag was redundant with the data. A JSON consumer can compute
"did anything change?" themselves (and may want their own threshold);
the human display logic pivots on `changeLines.length === 0` locally
and doesn't need it threaded through the return type.

Result type collapses to `{ baselineCommit, comparison }`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The constants block was only consumed in one place. Inlining surfaces
the values where they're used and lets each one carry a targeted
inline comment. Also drops the speculative "automated consumers
authenticate at the library layer" clause that didn't refer to any
actual code path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"PR" presumed the inner-dev-loop user is on a feature branch about to
open a PR. They might just be checking local impact. `compareJsons`
matches the `BundlesComparison.compare` field and
`compareJsonReportsByPackage`'s `compare` parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (1217 lines, 21 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

🔭 PR Review Fleet Report

Note

This report is generated by an experimental AI review fleet and is provided as a beta feature. Findings are a starting point for discussion, not a gate. Use your own judgement.

Verdict: ⚠️ Approve with Suggestions

0 Exterminate, 2 Squash, 4 Investigate

Findings

Sev # Area File What Fix
🦟 Squash H1 Testing build-tools/packages/build-cli/src/library/bundleSize/compareJsonReports.ts:57 compareJsonReports and compareJsonReportsByPackage have zero test coverage. The new field-presence encoding for added/removed bundles (...(baseBundle && { base: baseBundle })) is the entire basis of the new comparison API — a bug here (e.g. wrong set-union logic, inverted undefined-check) would silently produce an empty or wrong PackageComparison and the command would either suppress real regressions or falsely report clean results. No test file for this module exists in the repo. Add unit tests: (1) base and compare both have the same bundle → both base and compare present; (2) bundle only in base (package removed) → only base field present; (3) bundle only in compare (package added) → only compare field present; (4) package only in base map → emits its bundles under the package key with no compare side; (5) package only in compare map → emits its bundles with no base side; (6) both maps empty → empty PackageComparison.
🦟 Squash H2 Testing build-tools/packages/build-cli/src/library/bundleSize/sourcePackageFromAnalyzerPath.ts:16 sourcePackageFromAnalyzerPath is the sole key-extraction function for both sides of the comparison — used in extractAnalyzerJsonsFromArtifact (baseline) and readAnalyzerJsonsFromFileSystem (local). It has no tests. If it returns wrong keys (e.g., a scoped package like @scope/pkg/analyzer.json is keyed differently on each side, or Windows backslash normalization is broken), the two AnalyzerJsonByPackage maps will never share keys — causing every package to appear as simultaneously added AND removed with no actual size delta visible to the user. Add unit tests: (1) @scope/pkg/analyzer.json'@scope/pkg'; (2) Windows path @scope\\pkg\\analyzer.json'@scope/pkg'; (3) top-level analyzer.json (no directory prefix) → undefined; (4) path with no analyzer.json suffix → undefined; (5) deeply nested path a/b/c/analyzer.json'a/b/c' (verifying the full prefix is returned, not just the immediate parent).
🐜 Investigate M1 Correctness build-tools/packages/build-cli/src/library/azureDevops/getArtifactForCommit.ts:88 BuildStatus.Cancelling is not included in the isActivelyRunning guard (which only covers NotStarted, InProgress, and Postponed). A build in Cancelling state slips through to the next check: candidates.every((b) => b.result !== BuildResult.Succeeded). Because a cancelling build has not yet set its result, b.result is undefined, which satisfies !== BuildResult.Succeeded, so the check passes and the code throws "All builds for commit X have completed but none succeeded." The word "completed" is factually wrong — the build is still in the process of being cancelled — and the message implies a permanent failure when the user may just need to wait a moment for the cancellation to finish. Add BuildStatus.Cancelling to the isActivelyRunning predicate (or rename it to something like isNotYetSettled to cover all pre-terminal states). The check would then correctly report "Found an in-progress build ... none have succeeded yet" for cancelling builds.
🐜 Investigate M2 Correctness build-tools/packages/build-cli/src/library/bundleSize/extractAnalyzerJsonsFromArtifact.ts:24 JSON.parse(text) is called without any error handling. If the downloaded ADO artifact contains a corrupt or truncated analyzer.json entry, a raw SyntaxError: Unexpected token … propagates up through getArtifactForCommit (outside its try/catch wrapper) and crashes the command with no context about which package or commit was problematic. The error message says nothing that helps the user diagnose whether the artifact was partially written, incompatible, or tampered with. Wrap the parse in a try/catch and rethrow with relativePath (and ideally the commit SHA) in the message, e.g. throw new Error(\Failed to parse analyzer.json for "${relativePath}"`, { cause: e })`.
🐜 Investigate M3 Correctness build-tools/packages/build-cli/src/library/bundleSize/readAnalyzerJsonsFromFileSystem.ts:45 JSON.parse(text) is called without error handling inside a Promise.all callback. If any locally-generated analyzer.json is malformed (e.g. a partial write from an interrupted pnpm bundle-analysis:collect), the promise rejects with a raw SyntaxError that carries no indication of which file failed. The outer await Promise.all(...) re-throws this directly, so the command crashes with an unhelpful error. Wrap the parse in a try/catch and rethrow with join(rootPath, relativePath) in the message so the user can find and delete or regenerate the offending file.
🐜 Investigate M4 Testing build-tools/packages/build-cli/src/library/azureDevops/getArtifactForCommit.ts:332 findBuildIdForCommit has a documented multi-build scenario (manual re-runs, partial-success retries) with no test coverage. The isActivelyRunning predicate gates which error message fires; if it were accidentally inverted or the some vs every call swapped, a user with an in-progress build would see 'all builds failed' instead of 'wait for the running build', causing wasted retries or incorrect manual intervention. The anomaly branch (succeeded build but id === undefined) is also entirely untested. Add unit tests: (1) two builds for the same commit — one failed, one in-progress → throws 'in-progress' error, NOT 'all failed'; (2) two builds — one succeeded with id: undefined, one failed → throws 'no usable build id'; (3) two succeeded builds → returns the id of the first succeeded one; (4) no builds for the commit → throws 'No build found'.

View workflow run

ChumpChief and others added 2 commits May 15, 2026 16:28
- `readAnalyzerJsonsFromFileSystem` catches `ENOENT` on the directory
  walk and rethrows with a pointer to `pnpm bundle-analysis:collect`,
  so first-time users don't see a raw `ENOENT: no such file or
  directory` with no guidance.
- The `git merge-base` shell-out wraps `execFileSync` failures with
  `this.error()`, pointing at "ensure it is fetched locally, or pass
  --target <ref> to override." Captures git's stderr too instead of
  letting it leak as a parallel line above the wrapper.

Also updates a doc comment from `npm run bundle-analysis:collect` to
`pnpm bundle-analysis:collect`, the convention in this repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous guard only fired when *both* sides had no analyzer.json
files. The asymmetric case (the local directory exists but is empty —
e.g., bundle-analysis:collect partly failed, or the user manually
emptied it) silently fed `compareJsonReportsByPackage` an empty
`compare` map, producing a comparison that reported every baseline
bundle as "removed". The user saw "everything was deleted" with no
hint that the local data was actually missing.

Check each side individually now: an empty baseline (rare — implies
the artifact had no analyzer.json files) and an empty local dir get
distinct, actionable messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ChumpChief ChumpChief marked this pull request as ready for review May 15, 2026 23:55
Copilot AI review requested due to automatic review settings May 15, 2026 23:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the bundleSize library from class-based ADOSizeComparator into composable pure functions, introduces new comparison types that distinguish added/removed/changed bundles via field presence, and promotes generic helpers (ADO build/artifact lookup, freshest-remote selection) out of the bundleSize folder. The check bundleSize command is rewired to compose these primitives directly and uses this.error() for fatal cases instead of a kind: "error" result variant.

Changes:

  • Removed bundleSize/ADO/, compareBundles.ts, and bundleSize/utilities/; added new primitives: compareJsonReportsByPackage, extractAnalyzerJsonsFromArtifact, readAnalyzerJsonsFromFileSystem, sourcePackageFromAnalyzerPath.
  • Added library/azureDevops/getArtifactForCommit.ts and HTTP-status check in downloadArtifact.ts; generalized pickFreshestCanonicalRemote to pickFreshestRemote(branch, filter) in library/git/.
  • Rewrote commands/check/bundleSize.ts to use the new primitives, drop the SizeComparison discriminated union, and emit fatal failures via this.error().

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.

Show a summary per file
File Description
build-tools/packages/build-cli/src/library/git/pickFreshestRemote.ts Generalized canonical-remote helper to accept a URL filter callback.
build-tools/packages/build-cli/src/library/bundleSize/utilities/index.ts Removed (helpers relocated/inlined).
build-tools/packages/build-cli/src/library/bundleSize/utilities/getBuilds.ts Removed; getBuilds wrapper inlined into getArtifactForCommit.
build-tools/packages/build-cli/src/library/bundleSize/utilities/getAllFilesInDirectory.ts Removed; inlined into readAnalyzerJsonsFromFileSystem.
build-tools/packages/build-cli/src/library/bundleSize/types.ts Replaced BundleSummaries/BundleMetric* with BundleData/BundlesComparison/PackageComparison.
build-tools/packages/build-cli/src/library/bundleSize/sourcePackageFromAnalyzerPath.ts New helper to derive source-package name from analyzer.json path.
build-tools/packages/build-cli/src/library/bundleSize/readAnalyzerJsonsFromFileSystem.ts New: reads analyzer.json files from a local directory tree.
build-tools/packages/build-cli/src/library/bundleSize/index.ts Re-exports the new primitives and types; drops removed exports.
build-tools/packages/build-cli/src/library/bundleSize/extractAnalyzerJsonsFromArtifact.ts New: extracts analyzer.json files from a downloaded artifact.
build-tools/packages/build-cli/src/library/bundleSize/compareJsonReports.ts New: compares per-package JsonReports producing a PackageComparison.
build-tools/packages/build-cli/src/library/bundleSize/compareBundles.ts Removed (replaced by compareJsonReports).
build-tools/packages/build-cli/src/library/bundleSize/ADO/* Removed; functionality replaced by new primitives.
build-tools/packages/build-cli/src/library/azureDevops/getArtifactForCommit.ts New: encapsulates "find ADO build by commit and download artifact".
build-tools/packages/build-cli/src/library/azureDevops/downloadArtifact.ts Surfaces non-2xx HTTP status as a clear error before fflate parsing.
build-tools/packages/build-cli/src/commands/check/bundleSize.ts Rewired to compose new primitives; uses this.error() for fatals; new formatComparison renderer.

ChumpChief and others added 4 commits May 15, 2026 18:15
Replace the previous "is local empty?" guard that only fired when both
sides were empty: a local directory that exists but is empty (or holds
files but no analyzer.json) silently produced an "everything removed"
diff. Catch it before the walk.

- New `checkLocalBundleAnalysisExists(rootPath)` returns
  `"ok" | "missing" | "noAnalyzerJson"`. The library function does no
  message-building; the command decides what to print and appends the
  `pnpm bundle-analysis:collect` hint only on the default path (overrides
  are populated from some source we don't know about).
- Internals use `statSync({ throwIfNoEntry: false })` and `globSync` —
  single-shot calls where async buys nothing. Function is sync.
- `readAnalyzerJsonsFromFileSystem` switched to `globSync(...).map(read)
  + Promise.all` (cleaner than the for-await-with-explicit-reads-array).
- Direct `readFile` import from `node:fs/promises` instead of aliasing
  the whole promises namespace for one function.
- Baseline-empty check moves to immediately after baseline extraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A commit can have multiple ADO builds (manual re-run, partial-success
retry); `Array.find` could lock onto an older failed one even when a
newer succeeded build exists. Filter all matches, prefer a usable one,
and report the most actionable failure state across candidates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `some` form fires whenever any candidate failed, including the
mixed case where one succeeded (but had no `id`) alongside a failure —
producing the misleading "none succeeded" message. `every` correctly
limits that throw to the all-failed case; the mixed-success-no-id case
falls through to the final "no usable build id" throw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants