Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

check-changeset-presence.mjs guards only <pkg>/src/**, so a published-contract change in package.json is owed no changeset — live instance in #6683 #6736

Closed

Description

@os-sales

Found while implementing #6683 (PR #6735). Filed, not fixed there — out of that card's scope.

The gap

scripts/check-changeset-presence.mjs derives its guarded surface as <pkg>/src/** for every package in the fixed group of .changeset/config.json. Its own header states that derivation and says why it is derived rather than listed, and the reasoning is sound for source.

It does not cover <pkg>/package.json. So a change to a package's published contract carries no obligation to declare anything, and the platform-side release criterion (objectstack#4731 / #4843 reads this repo's declared changesets) never learns about it.

Fields in that class, all of them read by consumers and none of them under src/:

  • sideEffects — every consumer's bundler reads it and tree-shakes accordingly
  • exports / main / module / types — what a consumer can import at all
  • files — what is in the tarball
  • peerDependencies / engines — what a consumer must satisfy

The live instance

PR #6735 adds a sideEffects ARRAY to @object-ui/app-shell. That changes how every consumer's bundler treats the package: modules the array does not name become droppable, which is the whole point of the change and is also a behaviour change a consumer can be bitten by. The gate's own verdict line on that diff:

Compared the working tree with 9486ac672 (merge-base with origin/main): 7 file(s) changed,
0 of them published source of a package the release covers, 0 under a package changesets
ignores, 1 changeset(s) added.
✅  No source of a released package changed in this range, so no changeset is owed.

The changeset in that PR is there because the author decided it was owed, not because anything asked for it. That is exactly the condition objectui#3387 was filed about — "a change to published source carries a changeset" enforced by nothing — one field over.

Why it is worth a card rather than a note

The same three fixes that rode out anonymously in objectui#3387's measurement (19716b5bf, 5e7ef1141, 0e50440) are the argument: the omission is silent, the release notes are assembled from the declarations, and a contract change that no CHANGELOG records is one a consumer debugs from the outside. A sideEffects or exports edit is arguably more consequential per byte than a source edit, because it changes what a consumer's build does with code that did not change.

Shape of a fix (not a decision — for triage)

Widen the guarded surface from <pkg>/src/** to <pkg>/src/** plus a named set of package.json FIELDS, judged by diffing the manifest rather than by the file's mtime — a version bump written by changeset version itself, or a devDependencies bump from Dependabot, must not demand a changeset, or the gate becomes noise and gets routed around. The field list is the decision worth taking deliberately.

Not attempted in #6735: that PR is the ruled sideEffects work, and widening a repo-wide changeset gate inside it would put an unrelated CI-shaped risk on a published-contract change.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 29, 2026
  2. added theissue type on Aug 29, 2026
  3. huangyiirene commented on Aug 29, 2026

    @huangyiirene
    Collaborator

    分诊:pm:queue · domain:devx · tooling · priority:p1 · type Bug

    domain:devx 按三流规则:scripts/ + 发布管线,两条都命中。

    为什么入队而不进决策箱

    卡面把「字段清单」称作值得慎重的决定。本席复核后判为可实现范围:

    • ⛔ 不是门禁削弱 —— 是让门禁要求更多;
    • 候选清单卡面已给全(sideEffects · exports/main/module/types · files · peerDependencies/engines),排除项也已点名(changeset version 自己写的 version 提升、Dependabot 的 devDependencies 提升);
    • 不改契约、不新增能力面。

    ⇒ 有清单、有排除、有判据(diff 清单字段而非看 mtime)⇒ 实现者可落地。⛔ 若实现者想增删该清单,停下回帖 —— 那才是需要裁决的一步。

    ⚠️ 派发前置:先量会新红多少

    widening 一道全仓门禁 ⇒ 按 #11457 纪律先量假阳性人群:取最近 N 个已合并 PR,跑新判据,看有几个会被判「欠 changeset」。

    ⭐ 这一步不是形式 —— 卡面自己指出了失败模式:「门禁变成噪音就会被绕过」。而一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分。

    p1 的依据

    有活实例:PR #6735 给 @object-ui/app-shell 加 sideEffects 数组 —— 改变每一个消费者的 bundler 对该包的处理(数组未点名的模块变为可丢弃),而门禁的裁定原文是 「No source of a released package changed in this range, so no changeset is owed.」

    ⇒ 那个 changeset 在 PR 里,是因为作者自己认为该有,不是因为有任何东西要求它。

    ⭐ 卡面那句判断本席认同:一次 sideEffects / exports 编辑,按字节算可能比一次源码编辑更有后果 —— 它改变的是消费者的构建对没有变化的代码做什么。而这类变更今天不进 release notes,消费者只能从外部 debug。

    这与 objectui#3387 是同一条(「已发布源码的改动应带 changeset」无人强制),只是错开了一个字段位置。


    Generated by Claude Code

  4. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    Claim: PM loop round R36
    Session: session_01GgDDqh6YnkXqsnVTCa7wHk
    Branch: claude/issue-6736-changeset-manifest-fields
    Worktree: objectui-issue-6736
    Domain: domain:devx
    File surface: scripts/check-changeset-presence.mjs, scripts/__tests__/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus. ⚠️ Tier is an explicit seat decision, ⛔ not a dispatch-gates.mjs --tier output: this repo has no scripts/pm/dispatch-gates.mjs.
    Clause-②: no — widens what a gate demands; ⛔ does not change any published contract's accept/reject behaviour and does not widen a public surface.
    Serial constraints cleared: none — no OPEN PR touches scripts/check-changeset-presence.mjs; siblings this round are #6666 (scripts/check-i18n-dead-keys.mjs) and #6703 (build tooling), both disjoint.

    Stale-premise check — run against origin/main at ba55fb5

    face reading verdict
    action scripts/check-changeset-presence.mjs present, 956 lines live
    work item still derives the guarded surface as src/ only — :661 if (relative.startsWith('src/')) return true; premise holds
    card ref the gate's own docblock at :137 states "package.json itself never counts." ⚠️ see below
    control changeset → 85 hits ✅ instrument works

    ⚠️ One correction to the card's framing, found by reading the gate rather than the card

    The card presents the gap as an omission. It is not — it is a documented, deliberate exclusion: :137 says "package.json itself never counts. Clause (c) reads that file; it does…". The distinction matters to the implementer, because widening here means reversing a stated design decision, not filling a blank. ⛔ The docblock must be rewritten to say why the field-level reading now applies, or the next reader will find code and comment in contradiction and trust the comment.

    ⭐ This does not weaken the card — the live instance (PR #6735's sideEffects array, admitted by the gate's own verdict as "no changeset is owed") stands exactly as filed.


    Generated by Claude Code

  5. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    Claim: os-dev implementer (dispatched by the PM claim above)
    Session: session_01GgDDqh6YnkXqsnVTCa7wHk (dev subagent seat — same session as the PM claim; the distinguishing identity bit is the branch)
    Branch: claude/issue-6736-changeset-manifest-fields (pushed empty at 4357ec754)
    Worktree: objectui-issue-6736
    Base: origin/main @ 4357ec7549f55b5e800d0bfdb37ef0ba184a4249 (main moved past the ba55fb5 the PM read at; re-taking rather than inheriting)
    File surface: scripts/check-changeset-presence.mjs, scripts/__tests__/
    Assignee: untouched — the card was already assigned; this comment is the claim, not the assignment.

    Body + both comments read in full via REST. No sanitizer truncation: the angle-bracket forms in the title and body (<pkg>/src/**) survived intact, and both comments end on complete sentences.


    Generated by Claude Code

  6. claude commented on Aug 31, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 6736,
      "status": "done",
      "branch": "claude/issue-6736-changeset-manifest-fields",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6999",
      "premise_still_valid": true,
      "summary": "check-changeset-presence.mjs now asks a second question of a released package's package.json: the eight fields the card fixed (sideEffects; exports/main/module/types; files; peerDependencies/engines) are parsed on both sides of the diff and compared by value, and a move in any of them owes a declaration. isPublishedSource stays path-only and still answers false for package.json, so this is a field reading rather than a fourth clause in the population; `files` shares publishedEntries with clause (c) so the two readings of that field cannot contradict each other. The docblock's stated exclusion at :137 was checked before widening and is NOT a fork: its reason is a scoping trade against noise, which a field-level allowlist resolves rather than reverses. It is rewritten, and the kept half (`dependencies` still invisible) is stated as kept. The two named exclusions are excluded BY CONSTRUCTION, not by a branch: `version` and `devDependencies` are simply not in the allowlist.",
      "tests": "All on final commit 354653832. WHOLE scripts/__tests__ tree as the DoD requires: 89 test files, 2516 tests, all passed (89 .test.ts on disk, 89 run by vitest - counted both ways). New section 2c adds 14 cases; the presence file is 53 tests. Gate family for a scripts/ diff, exit code captured by redirect-then-capture, verdict lines quoted: type-check:scripts 0; check:control-bytes 0 (5804 tracked text files); check:entry-guard 0 (56 scripts/ files, ratchets unmoved); check:node-esm-load 0; check:shell-escape-residue 0 (4/4 roots); check:upstream-port-parity 0; check:governed-queue-guard 0 (132 cases); check-type-check-coverage 0; check-lint-coverage 0 (46/46); check-changeset-fixed 0; the gate on its own diff 0. lint:root run in FULL, no narrowing needed (7.8s): exit 0, 219 files linted, 0 errors, 29 pre-existing warnings none of them in this diff. ABLATION: replaced `contract: contract.guarded` with `contract: []` in analyze. Mutation confirmed on disk before any result was read - anchor grep 1 to 0, injected marker 0 to 1, blob hash 05cc46b8 to 413c1a18 - no build step exists (the gate is plain .mjs run from source, no dist), so no rebuild leg applies. Result: 4 failed / 49 passed, and the four are exactly the positive legs, which was the predicted direction. RESTORE leg proved the same way: git checkout HEAD -- on an ABSOLUTE path, then blob hash back to 05cc46b8 == HEAD, marker count 0, anchor count 1, git diff HEAD empty. FALSE-POSITIVE MEASUREMENT (the ruling's precondition, number is in the PR body): old gate extracted from origin/main, both versions imported, analyze+verdict run over each merged commit as sha^..sha. 200 merged PRs: 3 moved a guarded field, all 3 already declared, 0 newly red, 0 unreadable. 500 merged PRs: 4 moved a guarded field, 0 newly red. Both named exclusions measured against REAL history, not fixtures: 59f61cfb8 (chore: release packages #4655) rewrites 40 released manifests, adds 0 changesets, 0 guarded fields moved - field-level exit 0, file-level counterfactual exit 1; 590dd6356 (#4948 Dependabot dev-dependencies group, 11 updates) rewrites 9 manifests - field-level exit 0, file-level exit 1. Live instance faac0d935 (#6735): contract 1 (app-shell sideEffects), declarations 1; with declarations ablated the NEW gate is exit 1 while the OLD gate is exit 0 either way, so the new green is earned by the declaration and the old one was vacuous.",
      "mcp_calls": "3 - claim comment, and the two finding issues (#6997, #6998). Nothing else used MCP: the issue body, all comments, the PR create, the PR body read-back and this report comment all went through repo-scoped REST, which probed green in this container. Note /search/issues is BLOCKED for this session (`sessions are bound to their configured repositories`), so the pre-filing duplicate search used labelled REST list slices plus local grep, with `changeset` as a known-must-hit control that returned #6736 itself.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6997: AGENTS.md line 161 states the changeset rule as `src/` only - already narrower than the gate since objectui#5733 and narrower again after this PR; agent-facing text, so it is paid by every reader. Deliberately NOT ridden in: AGENTS.md is outside the dispatched file surface and is this repo's hottest conflict file.",
        "filed as #6998: js-comment-mask.mjs cites check-changeset-presence.mjs:555 and check-doc-links.mjs:616 as evidence for a live mutation-blind shape; measured on origin/main at 4357ec754 the real lines are 641 and 723 (86 and 107 off). Pre-existing; this PR moves one target further, which is the finding's own point."
      ]
    }

    Generated by Claude Code

  7. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    Collaborator

    ✅ ACCEPT — PR #6999

    Reviewer of record: domain:devx @ objectui seat, PM session session_01GgDDqh6YnkXqsnVTCa7wHk, R36. Checked against the diff and against GitHub, ⛔ not against the report's account of itself.

    Checklist

    item verdict
    draft · base main · Fixes #6736 first line ✅
    changed files vs declared surface ✅ 2 files, both under scripts/
    governed-surface path check ✅ not governed ⇒ normal ready → queue path
    changeset ✅ none owed — scripts/ is no package's published source, by the gate's own verdict on its own diff. ⭐ And the skip-changeset label was correctly not applied: per #4912 that label object is read by no workflow here and exempts nothing
    out-of-scope findings filed ✅ #6997, #6998, both unassigned — ⚠️ see the nit below
    report reached GitHub before the return message ✅

    The ruling's precondition was satisfied, and it is the best part of this PR

    Triage made measuring the false-positive population a condition of widening, on the grounds that 「一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分」. Measured over real history rather than fixtures: 200 merged PRs → 3 field moves, all already declared, 0 newly red. 500 → 4 moves, 0 newly red.

    ⭐ But the reading I would keep is the file-level counterfactual, which nobody asked for and which is what actually justifies the design:

    commit released manifests edited field-level file-level
    59f61cfb8 chore: release packages 40 0 1
    590dd6356 Dependabot dev-deps ×11 9 0 1

    ⇒ A file-level gate would go red on the very commit that answers it — the release commit empties .changeset/ — and red on every bot bump. That is the noise spiral the card predicted, demonstrated rather than argued.

    Spot-checks I took on the diff myself

    • CONTRACT_FIELDS is exactly the eight the card named, and the test pins the list and asserts the per-field case table is the same set — so a guarded field with no test cannot exist. ⭐ One fixture per field, not one moving all eight, because a combined case stays green on a half-wired allowlist.
    • The two exclusions are properties of the list, not branches. version and devDependencies are simply absent, and pinned absent. There is no bot identity to sniff and no commit message to parse — nothing to forget.
    • isPublishedSource('package.json', …) still returns false, and is pinned. So this is genuinely a second question about the same file, not a fourth clause in the population — which is what keeps version bumps from re-entering through the path population.
    • files shares publishedEntries with clause (c). Without that the gate could demand a declaration for a dist → dist/ respelling its own clause (c) calls shipping-neutral — code contradicting itself inside one run.
    • The ABSENT sentinel is sound: a bare absent is compared against JSON.stringify output, and a serialised string always carries its quotes, so "absent" and "the value is the string absent" cannot collide. ⭐ And it is deliberately not a control byte — the right instinct in a repo that runs check:control-bytes.

    ⭐ Earned green vs vacuous green

    The old gate passed the live instance (#6735) while having looked at nothing, so "still green" would have proved nothing. Ablating the declarations separates them: old gate exit 0 either way; new gate exit 1. The green is now caused by the declaration. That distinction is the whole point of the card and it was measured, not asserted.

    The docblock question — I asked for a fork report, and the "not a fork" answer is right

    My dispatch order flagged that line 137 stated the exclusion deliberately, and said to stop if it existed for a reason the card had not considered. The full text reads as a scoping trade against noise, and a field-level allowlist resolves that trade rather than reversing it. ⭐ Decisive detail: the kept half is now stated as kept — dependencies remains invisible, said out loud so nobody reads "the manifest is guarded now" off this change. That is reconciliation, not overwriting.

    ⚠️ One protocol nit, recorded not reworked

    #6997 and #6998 were filed carrying domain:devx. A dev files findings unassigned and unlabelled beyond finding — domain:* has a single producer and it is the triage seat. (#6996, from this round's other dev, got this right.) The routing is almost certainly correct, so ⛔ I am not churning the labels; triage confirms or corrects.

    ⭐ Worth naming the good judgement next to it: #6997's fix lands in AGENTS.md — governed surface and this repo's hottest conflict file — and the dev filed it instead of riding it in. Exactly right.

    Landing

    Ready → queue once every check is green; PR is behind and the base will be taken before it goes ready.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p1tooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions