Releases: clerk/break-check
Release list
v0.6.0
Minor Changes
- #115
757f609Thanks @jacekradko! - Deterministically downgrade suggestion-only union changes (issue #114). A union carrying an absorbing arm,string & {}orstring & Record<never, never>(or thenumberequivalents, theAutocomplete/LiteralUnionidiom), accepts every value of that primitive; its literal and template-literal arms only drive editor autocomplete. When a type-alias change keeps that arm identical on both sides and every changed arm is provably a subtype of the primitive, the assignable set is unchanged, so the change is reported non-breaking and tagged with the absorbing arm and the swapped arms. Alias definitions are resolved from each side's stored API report (the referenced aliases are usually unexported and absent from the doc model), and the check is fail-closed: anything it cannot prove keeps the change breaking, and the AI reviewer can record but not apply an escalation. A new system-prompt rule also teaches the reviewer the idiom for spellings the deterministic pass bails on. Opt out withdowngradeAbsorbingArmUnions: false.
Patch Changes
- #116
68efe14Thanks @jacekradko! - Fix quote-blind whitespace normalization in the differ. The compare-time normalizer collapsed whitespace and stripped spacing around punctuation inside string and template literals too, so two types differing only in a literal's internal spacing ('a | b'vs'a|b') compared equal and the change went unreported. Spacing is now normalized only outside quoted regions, in both the rule-based differ and the AI reviewer's surface lines. Compare-time only and symmetric across both reads, so committed baselines need no regeneration.
v0.5.0
Minor Changes
- #105
3f376e1Thanks @jacekradko! - Support package-scopedignoreSubpathsentries. A bare entry still applies to every configured package; an entry prefixed with a package name and#(the same separatoracknowledgedChangesuses) ignores the subpath only in matching packages, e.g."@clerk/astro#./env". Globs are accepted on both sides ("@clerk/*#./internal"), which goes beyondacknowledgedChanges, whose package part is matched exactly. Skip-reason warnings for unsnapshotable subpaths now name the exact scoped entry to copy. Entries starting with.keep their historical meaning byte-for-byte; the only reinterpreted shape is a non-dot entry containing#(e.g.**#./env), which previously matched a literal#inside an exports key and is now read as a scoped entry.
v0.4.0
Minor Changes
-
#101
1860598Thanks @jacekradko! - Skip reasons for subpaths API Extractor cannot snapshot are now classified
into actionable messages instead of echoing AE's raw InternalError text. An
ambient entry.d.ts(no top-level import or export) is reported as a surface
AE can never analyze, and an unresolvable type name in the shipped
declarations is reported as likely-broken published types; both point at
ignoreSubpathsas the acknowledgment path and keep the original AE first
line for traceability. Unrecognized messages pass through with only the
"software defect" boilerplate stripped. -
#99
4c81537Thanks @jacekradko! - A reference repair is now reported non-breaking (issue #98). When a breaking
modification's only diff is swapping module specifiers consumers could not
resolve (export-blocked, e.g.@clerk/shared/_chunks/index-Cr_OtBLqunder
"./_chunks/*": null, or chunk-shaped with the dependency unlocatable) for
specifiers that provably resolve against the dependency'sexports, the change
is deterministically downgraded and tagged as a repaired reference in the
report. The old reference errored (TS2307) or degraded toanydownstream, so
fixing it cannot break anyone. The check is fail-closed: any difference beyond
the specifier/alias swap, or an introduced specifier that does not provably
resolve, keeps the change breaking. Opt out with
downgradeRepairedReferences: false.The AI reviewer now also receives deterministic exports-map verdicts
(referenceResolutions) for every specifier a signature drops or introduces,
instead of guessing resolvability from path shapes, and cannot escalate a
deterministically repaired change back to breaking (recorded as
ai-suggested-escalation, mirroring the downgrade refusal for unresolvable
references).
v0.3.0
Minor Changes
-
#93
d3297f3Thanks @jacekradko! - Version-bump validation now follows semver range semantics instead of raw
labels. A breaking change in a0.xpackage is satisfied by a minor bump
(^0.2.3ranges stop at the next minor, so that IS the breaking boundary);
previously every 0.x breaking change was flagged "insufficient" unless it went
to 1.0.0. An advance within one prerelease train (1.0.0-beta.1 -> 1.0.0-beta.2, or finalizing to1.0.0) is no longer reported as "version was
not bumped".VersionAnalyzer.compareVersionsnow honors prerelease
precedence per the semver spec (1.0.0-beta.1ranks below1.0.0), and a new
VersionAnalyzer.isPrereleaseAdvanceis exported.BREAKING (programmatic API only):
VersionAnalyzer.isPreReleaseand
isValidPreReleaseBumpare renamed toisZeroMajorand
isValidZeroMajorBump. The old names conflated the 0.x convention with semver
prerelease tags, which this release starts handling as a distinct concept.
Per the 0.x rule above (which break-check now applies to itself), this rename
ships in a minor. -
#94
a6c4eb9Thanks @jacekradko! - The report's projected bump target now follows the same conventions bump
validation applies. A 0.x package with breaking changes projects the next
minor (0.2.0 -> 0.3.0) instead of suggesting a jump to1.0.0, with a note
explaining the 0.x convention (shown after the bump lands too, so
"Recommended: MAJOR / Actual: MINOR ✅" doesn't read as a contradiction). A
package on a prerelease tag projects the next tag in its train
(1.0.0-beta.1 -> 1.0.0-beta.2) instead of a whole-major jump. Adds
VersionAnalyzer.nextPrereleaseVersionto the programmatic API. The JSON
output is unchanged:recommendedVersionBumpremains the severity label.
Patch Changes
- #96
fd0f124Thanks @jacekradko! -findConfigFileno longer hangs when called with a relative start directory.
The walk-up loop compared againstpath.parse(startDir).root, which is empty
for a relative path whilepath.dirnamebottoms out at".", so the loop
never terminated. The start directory is now resolved against cwd before
walking. The CLI always passed absolute paths, so this only affected
programmatic callers of the exported function.
v0.2.0
Minor Changes
- #89
7c59947Thanks @jacekradko! - Stop under-reporting surface break-check never actually checked. (Minor, per
break-check's own verdict on this diff:PackageInfogains an optional
unresolvedSubpathsfield in the programmatic API.) Entry-point
discovery now resolves types declared under nested export conditions
({ "node": { "import": { "types": ... } } }) and.d.ctsfiles, both
previously skipped without a trace. A subpath that declares types break-check
still cannot resolve (a missing file, a target escaping the package, an
unsupported wildcard pattern) is now recorded as a skipped entry, so it shows
up in the report and--fail-on-skippedcatches it; JS-only and asset subpaths
stay silent. Anddetectnow refuses a--baselinethat resolves to the
configuredsnapshotDir, which previously overwrote the baseline with the
current snapshots and reported "no changes" for any break.
Patch Changes
- #88
123300fThanks @jacekradko! - Fix a family of false negatives in the rule-based differ. Overload signatures
now carry theiroverloadIndexin the comparison key, so removing or editing a
function or method overload (or one of several call/construct/index signatures)
is reported instead of silently collapsing onto the last overload. An
optionality flip no longer short-circuits the member compare, so a change like
a: string->a?: numberreports the breaking type change instead of a
non-breaking "became optional"; property and variable types are now compared
via their kind-specific token ranges (propertyTypeTokenRange,
variableTypeTokenRange) instead of the full declaration text. And
static/protected/abstractmodifier flips are compared explicitly, which
also catches them on methods and classes where the signature compare never saw
them. All fixes are compare-time only; committed baselines need no
regeneration.
v0.1.2
Patch Changes
- #86
b38f1fcThanks @jacekradko! - Stop reporting pure union/intersection member reorders as breaking changes
(#85). TS emits inferred union members in an unstable order, so an unrelated
edit could rotate them and trip a phantomReturn type changed; the differ now
sorts members before comparing. Compare-time only, so committed baselines need
no regeneration.
v0.1.1
Patch Changes
- #81
a4b6ff9Thanks @jacekradko! - The GitHub Action gains ananthropic-api-keyinput to enable the AI reviewer, anddetectgains--json-outputso the Action runs detect once instead of twice. Oversized PR comments are truncated with the full report attached as thebreak-check-reportartifact. Requiresbreak-check-version>= 0.1.1.
v0.1.0
First published release. Break Check is a CLI that snapshots a TypeScript package's public .d.ts surface with Microsoft API Extractor, diffs a branch against a baseline, and classifies every change as breaking, non-breaking, or an addition. It is built for PR gates: fail CI when a breaking API change is not matched by an adequate version bump.
npm install -D @clerk/break-check
npx break-check init # write break-check.config.json
npx break-check snapshot # record a baseline
npx break-check detect --fail-on-breakingCore workflow
init, snapshot, and detect commands driven by a zod-validated break-check.config.json, with optional version-bump enforcement (checkVersionBump). Reports render as Markdown or JSON; in JSON mode stdout stays clean (every log goes to stderr) so it pipes. detect exits non-zero on a breaking change under --fail-on-breaking, and exits 3 when it refuses an incompatible baseline so CI can tell the two cases apart. (#3, #5, #8)
Structural API diff
The differ compares parsed declaration structure, not joined token strings, so parameter renames and whitespace no longer read as breaking. It classifies optional/required parameter flips, added optional parameters, rest-parameter changes, return-type changes, and property-type changes individually, and normalizes equivalent import spellings (import("pkg").Foo vs a namespace alias) so build-strategy differences don't surface as phantom breaks. Type variance is intentionally pessimistic: any type change is flagged breaking unless something downgrades it. (#5, #45, #70)
Full export-map coverage
Break Check walks a package's entire exports map, producing one snapshot per subpath (./react, ./errors, ...), and expands single-segment wildcard exports ("./*") into concrete subpaths instead of skipping them. Content-hashed bundler chunks pulled in by a wildcard (index-Dq-_K2VH) are filtered out (ignoreHashedChunks, on by default) so their per-build hash churn doesn't masquerade as removed/added subpaths. ignoreSubpaths is glob-aware as the manual escape hatch. (#19, #26/#37, #49/#50)
Optional AI reviewer
When BREAK_CHECK_ANTHROPIC_API_KEY is set, Break Check sends each change a focused context (only the type definitions its signature references, plus usage sites) to Claude, which can confirm, escalate, or downgrade the rule-based verdict and scan for breaks the rules missed. It is fail-soft: any AI failure falls back to the rule-based result, so the exit code never depends on AI availability, and an incomplete review is flagged (partial) in the report rather than silently trusted. Two independent opt-ins, both off by default: --ai-scan runs the deeper missed-breaks audit, and --ai-apply-downgrades lets a breaking → non-breaking verdict actually clear a break (otherwise it is recorded only as a suggestion). Model resolution: --ai-model > BREAK_CHECK_AI_MODEL > ai.model > claude-sonnet-4-6. (#18, #22, #51, #57, #59)
GitHub Action
uses: clerk/break-check@v1 drops the snapshot → detect → comment flow into a PR. It builds both sides in isolated git worktrees pinned to the PR's base.sha and head.sha (so a moving base branch can't fabricate or invert changes) and posts a single, self-updating PR comment. Inputs cover config path, base/head ref, setup command, version, comment toggle, and fail-on-breaking; baseline-artifact-name plus baseline-max-age let large monorepos reuse a baseline uploaded from a push-to-main workflow; policy-mode enforces the base ref's config so a PR can't suppress its own break. Outputs expose has-breaking-changes and the report path. (#9, #25, #30, #43, #64)
Safety and correctness gates
- Version-stamped baselines. Snapshots record
breakCheckVersion,apiExtractorVersion, anddiscoveryVersion(schema v4);detectrefuses a baseline whose API Extractor major or discovery semantics differ from the running build rather than producing nonsense diffs. (#27, #41) - Unresolvable-reference guard. A signature that newly references an export-blocked dependency subpath (e.g.
@clerk/shared/_chunks/...under"./_chunks/*": null) is pinned breaking, since it errors or degrades toanyfor consumers, and the AI may not relax it. (#60, #61) - Phantom-addition and report-size limits. A newly-baselined subpath collapses to one "new subpath" entry instead of one per member, and the Markdown report enforces a size budget so it stays under GitHub's 65 KB comment limit. (#30, #41)
- Resilient extraction. A subpath API Extractor can't process (ambient globals,
@clerk/astro/env, Cypress/Playwright patterns) is skipped with a warning instead of crashing the run;--fail-on-skippedturns skips back into a hard failure for baseline production. (#36) - Path-traversal guard. Entry discovery refuses a
types/exportspath that resolves outside the package root (apackage.jsonis attacker-controlled when the Action builds a PR). (#64)
Configuration
Beyond the basics, break-check.config.json accepts acknowledgedChanges (downgrade a named breaking change unconditionally; a maintainer override that always wins), resolvableSpecifiers (exempt a specifier from the unresolvable-reference guard), ignoreSubpaths / ignoreHashedChunks, and an ai block (enabled, model, applyDowngrades, scanForMissed). (#57, #61)