Filed unassigned, no live blocker. Recording it because it is cheap to settle once and it cost two separate adversarial contract reviews real effort on the same morning, in the same program (#14122), and they came out opposite.
The two readings
Both reviews were given the same instruction — establish the answer from the repository's own precedent, not from general semver intuition — and both did.
Review of PR #15261 (adds export * from './artifact-collections.js' to packages/core/src/index.ts; subject feat(runtime)) concluded minor, citing the adjacent precedent:
commit 655b106 (#14643) added the neighbouring line — export * from './artifact-packages.js' — to the same file … "@objectstack/core": minor. Same ADR-0130 program, structurally identical act, consuming packages at patch and core at minor.
Review of PR #15282 (adds two named exports to packages/plugins/plugin-dev/src/index.ts; subject fix(plugin-dev)) concluded patch, from a corpus scan:
last 600 commits touching any packages/*/src/index.ts, purely additive export changes (added export … lines, zero removed) where the changeset declares a bump for that package:
{minor: 158, patch: 64, major: 4} n=226
of which subject starts with fix(: {patch: 49, minor: 36, major: 2} n=87
The reading that reconciles them — and why it should be written down rather than inferred
The two data sets are consistent under one rule: the commit TYPE decides — feat( → minor, fix( → patch — which is what the split at n=226 vs the fix(-only n=87 shows.
That rule is defensible for this repo (the whole monorepo is one Changesets fixed group per .changeset/config.json, and scripts/check-changeset-no-major.mjs records that during the launch window the bump level is deliberately not the carrier of breaking-ness — the BREAKING banner is). But it is worth stating plainly, because on its face it is surprising: it makes the subject line, not the act, decide the published bump. Two changes that widen the same index by the same amount take different bumps depending on how the author opened the sentence.
Note the second review also cited "#15261 declares @objectstack/core: patch while genuinely widening core's index" as in-program precedent for patch — but #15261's subject is feat(, so under the reconciling rule that PR's own reviewer was right to call it minor and the citation cuts the other way. Neither review was wrong about its own case; there was simply nothing written down for either to check against.
What would settle it
One paragraph, wherever the changeset rules already live (the Check Changeset step's prose in .github/workflows/pr-automation.yml is the surface every author already reads, and scripts/check-changeset-no-major.mjs already carries the launch-window reasoning):
⚠️ Do not turn this into a new gate. The existing Check Changeset already catches the absent case, and check-changeset-no-major.mjs the major case; what is missing is a written rule for humans and agents choosing between the two remaining levels.
Status of the two PRs that raised it
Neither is blocked on this. #15282 stands at patch (its review passed that question). #15261's semver item went moot when the module moved out of @objectstack/core and that PR stopped publishing anything.
Filed unassigned, no live blocker. Recording it because it is cheap to settle once and it cost two separate adversarial contract reviews real effort on the same morning, in the same program (#14122), and they came out opposite.
The two readings
Both reviews were given the same instruction — establish the answer from the repository's own precedent, not from general semver intuition — and both did.
Review of PR #15261 (adds
export * from './artifact-collections.js'topackages/core/src/index.ts; subjectfeat(runtime)) concludedminor, citing the adjacent precedent:Review of PR #15282 (adds two named exports to
packages/plugins/plugin-dev/src/index.ts; subjectfix(plugin-dev)) concludedpatch, from a corpus scan:The reading that reconciles them — and why it should be written down rather than inferred
The two data sets are consistent under one rule: the commit TYPE decides —
feat(→minor,fix(→patch— which is what the split at n=226 vs thefix(-only n=87 shows.That rule is defensible for this repo (the whole monorepo is one Changesets
fixedgroup per.changeset/config.json, andscripts/check-changeset-no-major.mjsrecords that during the launch window the bump level is deliberately not the carrier of breaking-ness — the BREAKING banner is). But it is worth stating plainly, because on its face it is surprising: it makes the subject line, not the act, decide the published bump. Two changes that widen the same index by the same amount take different bumps depending on how the author opened the sentence.Note the second review also cited "#15261 declares
@objectstack/core: patchwhile genuinely widening core's index" as in-program precedent forpatch— but #15261's subject isfeat(, so under the reconciling rule that PR's own reviewer was right to call itminorand the citation cuts the other way. Neither review was wrong about its own case; there was simply nothing written down for either to check against.What would settle it
One paragraph, wherever the changeset rules already live (the
Check Changesetstep's prose in.github/workflows/pr-automation.ymlis the surface every author already reads, andscripts/check-changeset-no-major.mjsalready carries the launch-window reasoning):minorcan carry a BREAKING banner (feat(spec): refuse unknown keys insidemanifest:—ManifestSchemagoes strict (the #8687 door one level down) #14714, feat(spec): composeStacks refuses two stacks whose actions resolve to one scope-qualified runtime key #14854).Check Changesetalready catches the absent case, andcheck-changeset-no-major.mjsthe major case; what is missing is a written rule for humans and agents choosing between the two remaining levels.Status of the two PRs that raised it
Neither is blocked on this. #15282 stands at
patch(its review passed that question). #15261's semver item went moot when the module moved out of@objectstack/coreand that PR stopped publishing anything.