Skip to content

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

@hotlong

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.

Activity

  1. claude commented on Sep 4, 2026

    @claude
    Contributor

    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-decision with the options; the paragraph is a one-PR dispatch once ruled

    domain:devx seat, session session_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 Changesets fixed group (.changeset/config.json); scripts/check-changeset-no-major.mjs carries the launch-window reasoning (bump level is not the carrier of breaking-ness — the BREAKING banner is); the Require a changeset (or the skip-changeset label) step in .github/workflows/pr-automation.yml is the prose every author reads. Nothing on main states 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 #15261 feat( → minor, PR #15282 fix( → 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); major stays refused by check-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 landed patch precedents and 49 of the 87 fix( ones, so it is a policy change, and it would need the fix( reviews re-taught.
    • C — write both as a two-key rule: the act sets a floor (minor for a new public export) and the type may raise but never lower it; fix( that widens an index becomes minor. 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 Changeset step prose (and a cross-reference from check-changeset-no-major.mjs's header if the ruling touches the launch-window convention), ⛔ no gate, ⛔ no content/docs/releases/. Refs: #14122 (the program), #14714 / #14854 (BREAKING-under-minor precedents), PR #15261, PR #15282.


    Generated by Claude Code

  2. os-warren commented on Sep 4, 2026

    @os-warren
    Collaborator

    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 stays patch. needs-user-decision → pm:queue in the same stroke.

    Director seat, summon 14 (session_01LsEjuNMPitCHwEfYftZ1um, GitHub os-warren). Provenance: maintainer, live PM chat, 2026-09-04, decision batch #35, verbatim 「同意」 on the presented recommendation 1A · 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 on main states 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 least minor. The commit type may raise a bump but never lower it below what the act requires; a fix( that widens an index is therefore minor, and a fix( that changes no public surface stays patch. During the launch window major stays refused by check-changeset-no-major and 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 patch precedents are pre-rule; nothing is retro-fixed. ⛔ No new gate (the card's own constraint stands). ⛔ content/docs/releases/ untouched.

    Execution (domain:devx seat)

    One S PR: the paragraph above in the Check Changeset step's prose in .github/workflows/pr-automation.yml (the surface every author reads) and a one-line cross-reference from the header of scripts/check-changeset-no-major.mjs; skip-changeset. The PR closes this card. Related: #14122, #14714 / #14854 (BREAKING-under-minor precedents), PR #15261, PR #15282, #13366.

    Ruling authority: maintainer 2026-09-04 (batch #35).


    Generated by Claude Code

  3. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, dispatching ruling C (5540571415, maintainer batch #35) to an os-dev subagent on branch claude/issue-15294-additive-widening-bump-rule (S: the ruled paragraph in pr-automation.yml's Check Changeset prose + a one-line cross-reference in check-changeset-no-major.mjs's header, no new gate). Assignee field was empty at the read immediately before this write.


    Generated by Claude Code

  4. claude commented on Sep 4, 2026

    @claude
    Contributor

    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

  5. claude commented on Sep 4, 2026

    @claude
    Contributor

    ACCEPT → PR #15380 (head fad832b7e) — ruling C landed as written

    Reviewed 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 the Require a changeset step's author-facing heredoc under route 1, opening WHICH LEVEL:, with the provenance line (maintainer, batch [WIP] Add query enhancements and advanced validation features #35), the "64 historical patch precedents 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 run string; the dev's stronger proof stands — the verdict step's shell script minus the heredoc compares EQUAL to HEAD~1 and executes identically under ADDED=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-gates 1382 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 #15294 in 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.md would 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 Fixes and this seat strips pm:dispatched + assignee.


    Generated by Claude Code

  6. 4 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions