Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(scripts): guard a package's published contract fields, not only its source by claude[bot] · Pull Request #6999 · objectstack-ai/objectui · GitHub
Skip to content

feat(scripts): guard a package's published contract fields, not only its source - #6999

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields
Aug 31, 2026
Merged

feat(scripts): guard a package's published contract fields, not only its source#6999
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-6736-changeset-manifest-fields

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#6736

scripts/check-changeset-presence.mjs derived its guarded surface from paths alone, so a change to a released package's published contractsideEffects, exports/main/module/types, files, peerDependencies/engines — was owed no declaration. The live instance is PR #6735, which gave @object-ui/app-shell a sideEffects ARRAY and got the gate's own No source of a released package changed in this range, so no changeset is owed. The changeset in that PR was there because its author decided it was owed.

The false-positive measurement — the ruling's precondition

Triage made measuring the false-positive population a condition of the change, on #11457 discipline: "一个被绕过的门禁比没有门禁更糟,因为它还占着「已覆盖」的名分". Method: extract the pre-change gate from origin/main, import both versions, and run analyze + verdict over each merged commit on origin/main as sha^..sha (these are squash merges, so that range is the PR's diff).

last 200 merged PRslast 500 merged PRs
moved a guarded contract field at all34
NEWLY RED under the widened criterion00
flipped red to green (must be 0)00
unreadable00

Zero false positives. All three field moves in the 200-PR window already carried a changeset:

green f99932a42 feat(app-shell): render `global:search` and `global:notifications` … (#6838)
@object-ui/app-shell: sideEffects (changesets added: 1)
green faac0d935 feat(app-shell): the precise `sideEffects` array … (#6735)
@object-ui/app-shell: sideEffects (changesets added: 1)
green 3e028c8d8 refactor(types,app-shell,plugin-designer): one tombstone registry … (#6627)
@object-ui/types: exports (changesets added: 1)

The two named exclusions, measured against real history rather than fixtures

The card names two changes that must NOT demand a declaration. Both were run through the old gate, the new gate, and a file-level counterfactual ("any edit to a released package's package.json"):

commitreleased package.json editedguarded fields movedchangesets addedoldnew (field-level)counterfactual (file-level)
59f61cfb8chore: release packages (#4655) — what changeset version writes4000001
590dd6356chore(deps-dev): bump the dev-dependencies group … 11 updates (#4948) — Dependabot900001

This is the whole argument for reading FIELDS rather than the file. A release commit rewrites 40 manifests and adds no changeset — it empties.changeset/. A file-level gate would go red on the very commit that answers it, and red on every Dependabot bump. Neither exclusion is a branch in the code: they hold because the reading is an allowlist of eight fields and version/devDependencies are not in it. There is no bot identity to sniff and no commit message to parse.

Is the new green on the live instance earned, or vacuous?

The old gate passed #6735 while having looked at nothing, so "still green" is not evidence. Ablating the declarations from the analysis separates the two:

OLD, as merged : exit 0 (guarded source files: 0)
OLD, declarations ablated : exit 0 <== green either way; the old gate never looked
NEW, as merged : exit 0 (guarded source: 0, contract: 1, declarations: 1)
NEW, declarations ablated : exit 1 <== RED; the green is earned by the declaration

What changed

A second question asked of the manifest, not a fourth clause in the population.isPublishedSource stays path-only and still answers false for package.json — pinned. A new contractChanges reads both sides of the diff out of git, parses them, and compares the eight guarded fields by value.

  • Values, not bytes, not mtime. Re-indenting a manifest or moving version above name is green. Comparison is order-preserving serialisation, which is correct rather than convenient: exports condition order is resolution order in Node, so a reorder that moves no key and no value is a behaviour change and goes red.
  • One normalisation for files, shared with clause (c).publishedEntries is what clause (c) compares changed paths against, and it is now what the field diff compares too. Without that the gate could demand a declaration for a files respelling (dist to dist/) that its own clause (c) reports as changing nothing shipped — code contradicting itself inside one run.
  • Failures name the FIELD. "your package.json changed" is also true of a version bump this gate passes, so a red without the field name sends the author looking for a change the gate did not object to.
  • Unclassified and ignored packages route the same way as source, so the two classifications compose into one verdict.
  • Loud on every missing input, in the established direction: an unparseable manifest on either side is a red build, never a quiet pass.

The docblock said this exclusion was deliberate — reconciled, not overwritten

The card frames the gap as an omission. It is not: line 137 stated it as a boundary — "package.json itself never counts. Clause (c) reads that file; it does not match it. A dependency bump can be just as user-visible, and this gate still does not see it. That was the first draft's trade and it is kept."

Read against the code, that is a scoping trade against noise, not a mechanism-level obstacle — the same population discipline the header states two paragraphs earlier ("too wide and every incidental file in a package directory demands a declaration … which is how a gate stops being read at all"). A field-level reading is the resolution of exactly that trade rather than its reversal, so this is not a fork.

The bullet is rewritten, and the part of the trade that is kept is now stated as kept: dependencies stays out, and a runtime dependency bump is still invisible to this gate. Nobody should read "the manifest is guarded now" off this change.

Tests

scripts/__tests__/check-changeset-presence.test.ts gains section 2c — 14 cases, both directions:

  • POSITIVE: a sideEffects array with no changeset goes red naming the file and the field; the same change with a changeset is green because of the declaration.
  • Each of the eight fields goes red on its own (one fixture per field — a single combined case passes as long as any one field is read, which is how a half-wired allowlist stays green), with an assertion that the case table and CONTRACT_FIELDS are the same set, so a field with no case cannot exist.
  • Both named exclusions: a version-only bump and a devDependencies-only bump are green.
  • Unguarded fields (scripts, dependencies, publishConfig, description) moving together: green. Reformatting: green. Root manifest: green. files respelling: green. exports condition reorder: red.
  • Ignored package: skipped, not demanded. Unparseable manifest: loud.
  • CONTRACT_FIELDS is pinned as a list, which is what makes the ruling mechanical: adding or dropping a field cannot happen without a deliberate edit here.

Verification

All on the final commit 354653832.

whole scripts/__tests__ tree 89 test files, 2516 tests, all passed (89 on disk, 89 run)
lint:root (full population) exit 0 — 219 files, 0 errors, 29 pre-existing warnings, none in this diff
type-check:scripts exit 0
check:control-bytes exit 0 — 5804 tracked text files
check:entry-guard exit 0 — 56 scripts/ files, ratchets unmoved
check:node-esm-load exit 0
check:shell-escape-residue exit 0 — 4/4 roots resolved
check:upstream-port-parity exit 0
check:governed-queue-guard exit 0 — 132 cases
check-type-check-coverage exit 0
check-lint-coverage exit 0 — 46/46 packages
check-changeset-fixed exit 0
the gate on its own diff exit 0 — no changeset owed (scripts/ is no package's source)

Ablation — replacing contract: contract.guarded with contract: [] in analyze, with the mutation confirmed on disk (anchor 1 to 0, marker 0 to 1, blob hash moved) before reading any result: 4 failed / 49 passed, and the four are the positive legs. The restore leg was confirmed the same way — git checkout HEAD -- on an absolute path, then blob hash equal to HEAD, marker gone, anchor back, git diff HEAD empty.

No changeset: this diff touches scripts/ only, which is no package's published source. The skip-changeset label is deliberately not applied — in this repository that label object exists from a historical mis-attachment, is read by no workflow, and exempts nothing.

Filed, not fixed here

Two pre-existing findings, both measured on origin/main at 4357ec754 and both outside this card's declared file surface:

Generated by Claude Code


Generated by Claude Code

…its source
`check-changeset-presence.mjs` derived its guarded surface from paths alone, so
a change to `sideEffects`, `exports`, `files`, `peerDependencies` or `engines`
in a released package's `package.json` was owed no declaration — measured on PR
#6735, whose `sideEffects` array got the gate's own `no changeset is owed`.
The reading is by FIELD, over both sides of the diff, and that is what makes the
two named exclusions expressible: a `version` bump written by `changeset
version` and a Dependabot `devDependencies` bump are excluded BY CONSTRUCTION,
because they are not in the allowlist — no branch to forget and no bot identity
to sniff. `files` shares one normalisation with clause (c), so the two readings
of that field cannot contradict each other inside a single run.
The header's stated exclusion is rewritten rather than left standing: what is
kept from that trade (`dependencies`, `scripts`, `version`) is now stated as
kept, so the manifest is not read as guarded.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@zhuangjianguo