Skip to content

docs(changeset): scope the os lint --json conversions guarantee to the project-lint exits - #14973

Merged
os-trump merged 1 commit into
mainfrom
claude/issue-14015-lint-json-conversions-changeset
Sep 3, 2026
Merged

docs(changeset): scope the os lint --json conversions guarantee to the project-lint exits#14973
os-trump merged 1 commit into
mainfrom
claude/issue-14015-lint-json-conversions-changeset

Conversation

@os-trump

@os-trump os-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14015

Wording only, in one unreleased changeset. No contract moves, no code change.

File face

M  .changeset/cli-lint-conversion-notices.md     (+21 / -8)

That is the whole diff. packages/cli/src/commands/lint.ts was read for the
census and never edited; content/docs/releases/ is untouched; packages/spec is
untouched.

The census — every JSON exit in lint.ts, not the ones I expected

The --eval omission was inherited from PR #13741's at-tier contract review, so it
was re-derived on the head this branch was cut from (5bc2f2727a). The enumeration
is deliberately not keyed on emitJson, so an exit written another way still
shows up. Three independent spellings were run and they agree:

# probe command result
A every --json flag guard grep -n "flags\.json" packages/cli/src/commands/lint.ts 7 sites: 3 negative (human face), 4 positive
B every machine-payload writer, any spelling grep -nE "emitJson|emitText|process\.stdout\.write|process\.stderr\.write|JSON\.stringify|this\.logJson|console\.dir" packages/cli/src/commands/lint.ts 4 sites, all emitJson; zero JSON.stringify, zero direct stdout writes
C what emitters are even in scope sed -n '18,29p' packages/cli/src/commands/lint.ts emitJson is the only emitter imported; emitText is not, so no other machine face can exist in this module

A and B are a bijection: each of the 4 positive flags.json guards owns exactly one
emitJson call, and no emitJson call sits outside a guard.

The four exits

# line mode payload conversions
1 621 project lint, report full lint report presentconversions: conversionNotices (L637)
2 730 project lint, caught throw error + errorCodeFields present (L731)
3 780 --eval, --generator load failure { error: msg } absent
4 792 --eval, eval report { ...report, duration } absent

Exit 4 was checked at the source rather than assumed: runMetadataEval returns a
MetadataEvalReport, whose interface (packages/cli/src/lint/metadata-eval.ts
L49-59) declares exactly results / total / passed / failed / meanScore /
ok / mode, and the returned object literal is that same 7-key shape. emitJson
adds nothing — it is a bare JSON.stringify plus a drained write.

Census: 4 JSON exits, conversions on 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-object on lint.ts matched its HEAD blob throughout) carrying
two injections:

  • a fifth JSON exit written without emitJson, as
    process.stdout.write(JSON.stringify(...)) — a spelling probe B covers but that
    the real file does not use;
  • a conversions: key added to one of the two --eval exits.

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: 5 and
exits_carrying_conversions: 3, against the baseline's 4 and 2.

A second, non-synthetic control: the same script over the sibling commands returns
validate.ts 6 exits / 6 carrying, and compile.ts 13 / 12 — so the tool
discriminates 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 report
exit and the caught-error exit and no --eval case at all — the guard covers
the subset the changeset then described as the whole.

One extra reading, recorded but not acted on

runEval is dispatched at L516, above the try at L561, and the command
hand-rolls its json boolean rather than using oclif's enableJsonFlag. So an
uncaught throw inside --eval produces no JSON document at all — not an empty
conversions, no payload. That is a separate shape from the two exits above and is
not touched here.

Premise held

The changeset sentence overclaims on today's main. Had the key been present on the
--eval exits, 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 or
failure, so it can be read unconditionally". A consumer who believed it writes
payload.conversions.length and gets a TypeError on the --eval path.

The replacement scopes the guarantee to the project-lint payloads and names the
exception by mode and by count: lint.ts has four --json exits, the two
project-lint ones carry the key and the two --eval ones do not, the latter being
the eval report and the --generator load failure. A reader can verify every clause
by running os lint --json and os lint --eval --json, or by counting the exits in
the file — rather than by trusting a hedge like "may be present". The --eval row
was 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 this
changeset declares is unchanged; only what it says about it is corrected.

Branch 2 — deliberately not taken

Making the --eval exits emit conversions too is a further widening of a
published 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 is
not in this PR and not as a "while I'm here" addition. My reading is recorded on the
card: it is not obviously right — --eval scores a generation corpus and never
loads 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 sibling
changeset 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/cli minor
was already declared on main by PR #13741, and this PR neither adds nor alters
that declaration. Adding a second changeset would inject a spurious release entry
describing a wording correction to an unreleased file. Check Changeset counts with
--diff-filter=A (pr-automation.yml L498), so a modify-only changeset PR scores
added == 0 by construction and the label is the intended route.

Precedent, same class and byte-identical file face: 8d1f22f7e3
docs(changeset): correct the share-link refusal log-volume sentence (PR #14764) —
one M on 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:

  • 13 green, including 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.
  • 1 NOT MEASURED: node scripts/pm/check-half-states.mjs exited 3, its own
    verdict line reading PREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused. That is the container's egress, not this diff; the
    same 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/, and
no package's src or test is touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza


Generated by Claude Code

…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
@github-actions github-actions Bot added the size/s label Sep 3, 2026
@os-trump os-trump added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 3, 2026 — with Claude
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 3, 2026
@os-trump
os-trump marked this pull request as ready for review September 3, 2026 16:11
@os-trump
os-trump enabled auto-merge September 3, 2026 16:12
@os-trump
os-trump added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit cfe6fb6 Sep 3, 2026
31 checks passed
@os-trump
os-trump deleted the claude/issue-14015-lint-json-conversions-changeset branch September 3, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

2 participants