docs(changeset): scope the os lint --json conversions guarantee to the project-lint exits - #14973
Merged
Merged
Conversation
…o the project-lint exits (#14015) The merged #13741 changeset told consumers `conversions` is "always an array on `os lint --json` … so it can be read unconditionally". Re-censused on `main`: `lint.ts` has four `--json` exits and only two of them carry the key. The two `--eval` exits — the eval report and the `--generator` load failure — publish no `conversions`, so a consumer who believed the sentence and wrote `payload.conversions.length` gets a `TypeError` on that path. Release notes compile from `.changeset/*.md` centrally at release time, so this sentence was scheduled to become a published statement about a machine-readable contract. Narrow it now, in the unreleased file, to say which exits carry the key and which do not. Wording only — no contract moves. Making the `--eval` exits emit `conversions` would widen a published payload on paths that never carried the key; that needs its own card and an at-tier contract review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
os-trump
marked this pull request as ready for review
September 3, 2026 16:11
os-trump
enabled auto-merge
September 3, 2026 16:12
os-trump
deleted the
claude/issue-14015-lint-json-conversions-changeset
branch
September 3, 2026 17:05
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14015
Wording only, in one unreleased changeset. No contract moves, no code change.
File face
That is the whole diff.
packages/cli/src/commands/lint.tswas read for thecensus and never edited;
content/docs/releases/is untouched;packages/specisuntouched.
The census — every JSON exit in
lint.ts, not the ones I expectedThe
--evalomission was inherited from PR #13741's at-tier contract review, so itwas re-derived on the head this branch was cut from (
5bc2f2727a). The enumerationis deliberately not keyed on
emitJson, so an exit written another way stillshows up. Three independent spellings were run and they agree:
--jsonflag guardgrep -n "flags\.json" packages/cli/src/commands/lint.tsgrep -nE "emitJson|emitText|process\.stdout\.write|process\.stderr\.write|JSON\.stringify|this\.logJson|console\.dir" packages/cli/src/commands/lint.tsemitJson; zeroJSON.stringify, zero direct stdout writessed -n '18,29p' packages/cli/src/commands/lint.tsemitJsonis the only emitter imported;emitTextis not, so no other machine face can exist in this moduleA and B are a bijection: each of the 4 positive
flags.jsonguards owns exactly oneemitJsoncall, and noemitJsoncall sits outside a guard.The four exits
conversionsconversions: conversionNotices(L637)error+errorCodeFields--eval,--generatorload failure{ error: msg }--eval, eval report{ ...report, duration }Exit 4 was checked at the source rather than assumed:
runMetadataEvalreturns aMetadataEvalReport, whose interface (packages/cli/src/lint/metadata-eval.tsL49-59) declares exactly
results/total/passed/failed/meanScore/ok/mode, and the returned object literal is that same 7-key shape.emitJsonadds nothing — it is a bare
JSON.stringifyplus a drained write.Census: 4 JSON exits,
conversionson exactly 2.The positive control — this bound could have returned N+1
A bound with no way of having been exceeded is not a bound. The census was re-run
against a scratch copy in the session scratchpad (the tracked file was never
mutated —
git hash-objectonlint.tsmatched its HEAD blob throughout) carryingtwo injections:
emitJson, asprocess.stdout.write(JSON.stringify(...))— a spelling probe B covers but thatthe real file does not use;
conversions:key added to one of the two--evalexits.Both injections were confirmed on disk by counting the injected text (1 and 1)
before reading any result. The census then returned
json_exits_total: 5andexits_carrying_conversions: 3, against the baseline's 4 and 2.A second, non-synthetic control: the same script over the sibling commands returns
validate.ts6 exits / 6 carrying, andcompile.ts13 / 12 — so the tooldiscriminates across real files and counts well past 4.
Corroboration from the other direction: PR #13741's own pin test,
packages/cli/test/lint-conversion-notices.e2e.test.ts, has cases for the reportexit and the caught-error exit and no
--evalcase at all — the guard coversthe subset the changeset then described as the whole.
One extra reading, recorded but not acted on
runEvalis dispatched at L516, above thetryat L561, and the commandhand-rolls its
jsonboolean rather than using oclif'senableJsonFlag. So anuncaught throw inside
--evalproduces no JSON document at all — not an emptyconversions, no payload. That is a separate shape from the two exits above and isnot touched here.
Premise held
The changeset sentence overclaims on today's
main. Had the key been present on the--evalexits, the right deliverable would have been that reading and no diff.The new wording, and why it is checkable
The old text said the key is "always an array on
os lint --json, success orfailure, so it can be read unconditionally". A consumer who believed it writes
payload.conversions.lengthand gets aTypeErroron the--evalpath.The replacement scopes the guarantee to the project-lint payloads and names the
exception by mode and by count:
lint.tshas four--jsonexits, the twoproject-lint ones carry the key and the two
--evalones do not, the latter beingthe eval report and the
--generatorload failure. A reader can verify every clauseby running
os lint --jsonandos lint --eval --json, or by counting the exits inthe file — rather than by trusting a hedge like "may be present". The
--evalrowwas also added to the "what changed" table, and the exact-key-set warning was scoped
to the project-lint payload.
The frontmatter is untouched: still
"@objectstack/cli": minor. The release thischangeset declares is unchanged; only what it says about it is corrected.
Branch 2 — deliberately not taken
Making the
--evalexits emitconversionstoo is a further widening of apublished payload on exit paths that never carried the key. Per the card that needs
its own issue and an at-tier contract review under
CONTRACT_REVIEW_TIER, so it isnot in this PR and not as a "while I'm here" addition. My reading is recorded on the
card: it is not obviously right —
--evalscores a generation corpus and neverloads the project, so it never reaches the conversion layer and the honest value
there would be a constant
[], which is the "phantom field" shape the siblingchangeset warns about. That judgment is for the review, not for this PR.
Changeset fork
skip-changeset— applied to this PR, no changeset of its own.Per AGENTS.md the label "is for a diff that publishes nothing from any released
package", and this diff publishes nothing of its own: the
@objectstack/climinorwas already declared on
mainby PR #13741, and this PR neither adds nor altersthat declaration. Adding a second changeset would inject a spurious release entry
describing a wording correction to an unreleased file.
Check Changesetcounts with--diff-filter=A(pr-automation.ymlL498), so a modify-only changeset PR scoresadded == 0by construction and the label is the intended route.Precedent, same class and byte-identical file face:
8d1f22f7e3docs(changeset): correct the share-link refusal log-volume sentence (PR #14764) —
one
Mon one.changeset/*.md, adding none.Gates
Derived from the real diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-written path list), then all 14 runnable families were run at
d991627575, exit codes captured before any pipe:pnpm check:nul-bytes,pnpm check:changeset-gate-self-tests,node scripts/check-empty-changeset.mjs,node scripts/check-changeset-no-major.mjs,node scripts/check-adr-0087-registration.mjs,node scripts/check-keyed-text-bounds.mjs,node scripts/pm/release-rehearsal-clone.mjs --self-test.node scripts/pm/check-half-states.mjsexited 3, its ownverdict line reading
PREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused. That is the container's egress, not this diff; thesame family's self-test (
pnpm check:pm-half-states) passes 2062 cases.No build or test run was needed: the diff is one markdown file in
.changeset/, andno package's
srcortestis touched.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code