Repository navigation
finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294
Description
Activity
Dispatch-time read — this card asks the repo to WRITE A RULE, and which rule is the maintainer's call, not a dev's.
pm:queue→needs-user-decisionwith the options; the paragraph is a one-PR dispatch once ruleddomain:devxseat, sessionsession_012zGPuVVX3deAx9LdjK8jCk, R13, 2026-09-04T09:1xZ. Read to the last page: no triage comment, labels set at filing.Premise re-verified on
origin/main: the monorepo is one Changesetsfixedgroup (.changeset/config.json);scripts/check-changeset-no-major.mjscarries the launch-window reasoning (bump level is not the carrier of breaking-ness — the BREAKING banner is); theRequire a changeset (or the skip-changeset label)step in.github/workflows/pr-automation.ymlis the prose every author reads. Nothing onmainstates what a purely additive widening of a published index takes. The card's data: n=226 additive-export changesets →{minor 158, patch 64, major 4};fix(-only n=87 →{patch 49, minor 36}— the commit TYPE is the observed discriminator, and the two reviews (PR #15261feat(→ minor, PR #15282fix(→ patch) were each right about their own case.Why not dispatch as written: the deliverable is one paragraph of repo policy in the surface that gates every PR. Which sentence goes there — the subject line decides vs the act decides — changes what every author and every contract reviewer does from then on. That is maintainer voice. ⛔ The card itself forbids a new gate; none is proposed here.
Options (one line each):
- A — write the observed rule down as the rule: a purely additive widening of a published index takes the bump the commit type implies —
feat(→minor,fix(→patch— because in the launch window the bump level carries intent, not compatibility (the BREAKING banner does);majorstays refused bycheck-changeset-no-major. Cheapest, matches 158+49 of 226 precedents, makes the two reviews reconcile, and says plainly that the subject line is the carrier. Recommended by this seat: it records what the repo already does and names the surprising part instead of hiding it. - B — make the act decide: adding an exported symbol to a published index is always
minor(semver's own reading). Cleaner in principle; contradicts 64 landedpatchprecedents and 49 of the 87fix(ones, so it is a policy change, and it would need thefix(reviews re-taught. - C — write both as a two-key rule: the act sets a floor (
minorfor a new public export) and the type may raise but never lower it;fix(that widens an index becomesminor. Also a change, smaller than B. - D — close as recorded: leave it to per-review judgement; the two readings stay reconcilable by the data in this card. Costs the next pair of reviewers the same morning.
On a ruling: one S PR — the paragraph in the
Check Changesetstep prose (and a cross-reference fromcheck-changeset-no-major.mjs's header if the ruling touches the launch-window convention), ⛔ no gate, ⛔ nocontent/docs/releases/. Refs: #14122 (the program), #14714 / #14854 (BREAKING-under-minorprecedents), PR #15261, PR #15282.
Generated by Claude Code
- A — write the observed rule down as the rule: a purely additive widening of a published index takes the bump the commit type implies —
Maintainer ruling recorded — C: the ACT sets the floor. A new public export on a published index is at least
minor; the commit type may raise a bump, never lower it; a pure fix that widens no surface stayspatch.needs-user-decision→pm:queuein the same stroke.Director seat, summon 14 (
session_01LsEjuNMPitCHwEfYftZ1um, GitHubos-warren). Provenance: maintainer, live PM chat, 2026-09-04, decision batch #35, verbatim 「同意」 on the presented recommendation1A · 2(Q1 A · Q2 A · Q3 B→A) · 3C(this card is item 3, C). Premise carried from the card body and the devx seat's dispatch-time read (5538251321): n=226 additive-export changesets split{minor 158, patch 64}, the commit type explains the split, and nothing onmainstates the rule.The ruling — the sentence to write
A purely additive widening of a published package's public surface (a new exported symbol on an
index, a new accepted key or value) takes at leastminor. The commit type may raise a bump but never lower it below what the act requires; afix(that widens an index is thereforeminor, and afix(that changes no public surface stayspatch. During the launch windowmajorstays refused bycheck-changeset-no-majorand breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level.Why C over A (the devx seat's recommendation): the bump level has to carry compatibility again the day the window closes, so a 「subject line decides」 rule would have to be un-taught; C is true now and after. It is also what this seat already ruled on #13366 (
5536870980: additive surface =minor) and applied to PR #15304 today, so the rule is a codification, not a change. The detector — 「does the diff add an exported symbol?」 — is the same mechanical trigger clause-② already keys on, so reviewers compute it once for two consequences.⛔ The 64 historical
patchprecedents are pre-rule; nothing is retro-fixed. ⛔ No new gate (the card's own constraint stands). ⛔content/docs/releases/untouched.Execution (
domain:devxseat)One S PR: the paragraph above in the
Check Changesetstep's prose in.github/workflows/pr-automation.yml(the surface every author reads) and a one-line cross-reference from the header ofscripts/check-changeset-no-major.mjs;skip-changeset. The PR closes this card. Related: #14122, #14714 / #14854 (BREAKING-under-minorprecedents), PR #15261, PR #15282, #13366.Ruling authority: maintainer 2026-09-04 (batch #35).
Generated by Claude Code
Claim: PM seat
domain:devx @ objectstack(#6023), sessionsession_012zGPuVVX3deAx9LdjK8jCk, dispatching ruling C (5540571415, maintainer batch #35) to anos-devsubagent on branchclaude/issue-15294-additive-widening-bump-rule(S: the ruled paragraph inpr-automation.yml's Check Changeset prose + a one-line cross-reference incheck-changeset-no-major.mjs's header, no new gate). Assignee field was empty at the read immediately before this write.
Generated by Claude Code
os-dev-report
{ "issue": 15294, "status": "done", "branch": "claude/issue-15294-additive-widening-bump-rule", "pr": "https://github.com/objectstack-ai/objectstack/pull/15380", "premise_still_valid": true, "summary": "Ruling C (maintainer, 2026-09-04, decision batch #35, recorded as comment 5540571415) is now written down in the two surfaces the ruling named. (1) .github/workflows/pr-automation.yml: the ruled paragraph sits inside the 'Require a changeset (or the skip-changeset label)' step's author-facing heredoc, under route 1 ('It releases something'), opening 'WHICH LEVEL:' — the moment an author who is told the changeset is missing has to pick a level. It carries the ruled sentence word for word (only the markdown bold markers were dropped, since the destination is a plain-text job log), then the provenance line, then the two things the ruling shuts down: the 64 historical patch precedents are pre-rule and nothing is retro-fixed, and this is prose, not a gate. (2) scripts/check-changeset-no-major.mjs: a cross-reference in the file header, placed right after the paragraph explaining that the bump level is not the carrier of breaking-ness during the launch window — the sentence the rule's last clause leans on — naming the rule, the workflow prose and #15294. No new gate; content/docs/releases/ untouched; nothing retro-fixed. PR is DRAFT with the skip-changeset label applied and read back. NOTE for the PM: the ruled surface only prints when a PR adds NO changeset — an author who wrote a changeset at the wrong level never reaches this text. That is the surface the ruling named, so it is left as ruled and raised as an open question rather than widened here.", "tests": "All runs on branch head fad832b7e (git rev-parse --short HEAD), the final commit. Exit codes captured before any pipe (cmd redirected to a log, then EXIT=$?). (a) DERIVED FAMILY: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exits 0, stderr 'gate list derived from the tree of objectstack-ai/objectstack at commit fad832b7e', change set '2 path(s) vs merge base c4d1354e3', 42 commands. All 42 ran, all EXIT=0 — including check-changeset-no-major.mjs --self-test, check-self-test-workflow-commands.mjs (plus --self-test), check-self-test-wired.mjs (plus --self-test), check-ci-filter-parity.mjs, check-whole-set-label-write.mjs (plus --self-test), check-step-collectors.mjs (plus --self-test), pnpm check:declared-population-live, check:ratchet-remedy-authority, check:nul-bytes, check:watch-hint-literal, check:changeset-gate-self-tests, check:entry-guard, check:workflow-status-functions, check:required-contexts. (b) check-required-contexts.mjs --verify-required-set first returned EXIT=2 = NOT MEASURED ('required-set sweep: NOT VERIFIED ... HTTP 401', the script's own advice: HTTPS_PROXY set without --use-env-proxy); re-run with NODE_OPTIONS=--use-env-proxy it exits 0 — 'required-set sweep: 7 live required context(s) on main, 0 registered-but-not-required, 1 required-but-unpinned', report-only by design, and the one unpinned row ('Governed Surface Queue Guard — from main (active)') is pre-existing on main and untouched by this diff. (c) VERIFY-LOCK RUN, foreground: os-verify-lock.sh -c 'pnpm check:pm-dispatch-gates && pnpm lint' printed 'os-verify-lock: VERDICT command-exit 0 · held the lock 452s (7m32s) · waited 323s (5m23s)'; dispatch-gates self-test: 1382 cases pass; whole-repo pnpm lint = 'eslint . --no-inline-config' over the entire tree, zero findings, zero problem lines. The first acquisition returned 99 = NOT MEASURED (queued); that interval was spent on lock-free work and OS_VERIFY_LOCK_SLOT=issue-15294 was set before the first attempt, so the queue place was kept. (d) BEHAVIOUR UNCHANGED, proven not asserted: the verdict step's run script was parsed out of HEAD~1 and of HEAD with the yaml package; with the heredoc body excluded the two shell scripts compare EQUAL, and both were executed under ADDED=0, ADDED=2 and ADDED='n/a', giving identical exit codes 1, 0, 0. The workflow also parses: 3 jobs, 14 steps in changeset-check, and the ruled text is present in the parsed step's run string. (e) COMMENT-ONLY on the script: git diff -U0 -- scripts/check-changeset-no-major.mjs, minus lines that are block-comment continuations, is 0 lines — the floor, the roster and the fixtures are untouched. (f) ROSTER FAMILIES that dispatch-gates flagged as keeping their list under one of my two directories were all run: check-ci-filter-parity --self-test, check-published-list-mirrors (plus --self-test), pr-labels.mjs --self-test, check:console-injection, check:engine-double-contract, check:i18n-stale-fill, check:pm-governed-prose, check:partof-closing-keyword — all EXIT=0. Two of that group are NOT MEASURED here for environment reasons, stated rather than counted green: check-single-claim-paths.mjs EXIT=2, 'NOT WIRED — PR_NUMBER is not set ... This is a wiring or usage failure, NOT a verdict' (it wants the PR context CI supplies), and pnpm check:published-readme-exports EXIT=1 whose every finding is 'type entry .../dist/index.d.ts does not exist. Build first' in a fresh worktree with no build; it names only package READMEs, none of them a path in this diff. (g) Control bytes: grep -naP over both edited files finds none; pnpm check:nul-bytes green.", "mcp_calls": "3 — issue_read(get), issue_read(get_comments), create_pull_request. Everything else (label write, label read-back, PR body read-back, this comment) went over repo-scoped REST after a probe returned HTTP 200.", "open_questions": [ { "question": "The ruled paragraph only reaches an author on the path where the Check Changeset gate FAILS (no changeset added). An author who writes a changeset and picks the wrong level passes the gate and never sees the rule. Should the rule get a second, always-visible home?", "options": [ "A - leave it exactly as ruled: one paragraph in the Check Changeset prose plus the script cross-reference. Cost: zero; the rule is discoverable from a red gate or from the script header, nowhere else.", "B - add the same sentence to .changeset/README.md or its neighbourhood, which is what an author has open while running 'pnpm changeset'. Cost: a second copy to keep equal to the first, and this card forbids the new gate that would normally hold such a pair equal.", "C - also print it in the 'Reject an empty-frontmatter changeset added by this PR' step's failure text. Cost: near zero, but it reaches an even narrower audience than the current home." ], "recommendation": "A for this PR — it is the surface the ruling names, and the card explicitly forbids a new gate, so a second copy would be an ungated duplicate that drifts. If B is wanted it is a separate card and should carry its own answer to 'what keeps the two copies equal without a gate'." } ], "out_of_scope_findings": [] }
Generated by Claude Code
ACCEPT → PR #15380 (head
fad832b7e) — ruling C landed as writtenReviewed on the branch (
origin/main...claude/issue-15294-additive-widening-bump-rule), not the self-report:- Two files, +24/−0.
.github/workflows/pr-automation.yml: the ruled paragraph, word for word (bold markers dropped for a plain-text log), inside theRequire a changesetstep's author-facing heredoc under route 1, openingWHICH LEVEL:, with the provenance line (maintainer, batch [WIP] Add query enhancements and advanced validation features #35), the "64 historicalpatchprecedents are pre-rule" clause and "this is prose, not a gate".scripts/check-changeset-no-major.mjs: a header cross-reference — 0 non-comment lines in that hunk (checked by this seat), so its roster, floor and fixtures are untouched. - Behaviour unchanged, checked by this seat: the workflow parses (3 jobs, 19 steps) with the ruled sentence present in the step's
runstring; the dev's stronger proof stands — the verdict step's shell script minus the heredoc compares EQUAL toHEAD~1and executes identically underADDED=0 / 2 / n/a(exit 1 / 0 / 0). - Dev's family: 42 derived commands all green incl.
check-changeset-no-major --self-test,check-self-test-workflow-commands,check-ci-filter-parity,check:required-contexts(report-only; the one unpinned row is pre-existing),check:pm-dispatch-gates1382 cases pass, whole-repo lint clean. ⛔ No new gate;content/docs/releases/untouched; nothing retro-fixed. - CI on
fad832b7e: 18 success / 11 skipped / 3 in progress, 0 red;skip-changeset+size/s+ci/cd;Fixes #15294in the PR body..github/workflows/**is not a governed surface.
Open question ruled: A — the ruled surface is the one the maintainer named; a second, ungated copy in
.changeset/README.mdwould drift, and the card forbids the gate that would hold the pair equal. Recorded here for the maintainer: the paragraph reaches an author only when the changeset is MISSING; an author who picks the wrong level passes the gate silently — a second home is a separate card if wanted.Flipping ready + arming auto-merge over MCP now; on merge the card closes via
Fixesand this seat stripspm:dispatched+ assignee.
Generated by Claude Code
- Two files, +24/−0.
4 remaining items
- added a commit that references this issue
on Sep 6, 2026 - added a commit that references this issue
on Sep 7, 2026 - added a commit that references this issue
on Sep 7, 2026 - added a commit that references this issue
on Sep 9, 2026 - added 4 commits that reference this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 11, 2026 - added 6 commits that reference this issue
on Sep 17, 2026
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.