Filed unassigned, recording only — found while censusing the JSON exits of packages/cli/src/commands/lint.ts for #14015 (PR #14973). Not in that card's scope, not touched by it.
The reading
packages/cli/src/commands/lint.ts:
run() dispatches eval mode at L516 — if (flags.eval) { await this.runEval(flags, timer); return; }
- the project-lint
try opens at L561 and its catch-all JSON exit is at L722-730
So the eval branch runs entirely above the try. Nothing thrown out of runEval can reach that catch-all. runEval has one internal try of its own (L778), covering bundleRequire of --generator only; a throw from runMetadataEval or from anything else below it is uncaught inside the command.
The command also hand-rolls its json flag as a plain Flags.boolean rather than using oclif's enableJsonFlag, so oclif wraps nothing: there is no framework-level JSON error envelope to fall back on.
⇒ Net effect: a caller that ran os lint --eval --json and hits an uncaught throw gets oclif's human error text, not a JSON document. A machine consumer parsing stdout gets a parse failure rather than an error payload — and the two project-lint exits it may have been written against both do produce JSON on a throw (L730), so the shape is inconsistent across the same command's modes.
Why it is worth recording
This is the same family the conversions and warnings cards kept landing in — "the machine face is missing on some exit of a command that advertises --json" — one mode over. #12125 and #13741 both settled that every failure exit of an authoring command should carry the same shape; the --eval mode was never brought under that rule, and this is a stronger version of the gap than a missing key: there is no payload at all.
⚠️ What is NOT claimed
I did not construct a reproducer. This is a structural reading of the control flow at the line numbers above, not a measured crash. Whoever picks it up should first establish that a throw is actually reachable there — runMetadataEval captures per-case generationError internally (packages/cli/src/lint/metadata-eval.ts), so the reachable throw sites may be narrow, and "narrow enough not to be worth a contract move" is a legitimate outcome. The bug, if any, is the missing face, not a specific crash.
Related, and deliberately separate
⛔ This is not the "make the --eval exits emit conversions" branch that #14015 fenced off as needing its own card and an at-tier contract review. That one is about a key on two exits that already produce JSON. This one is about a path that produces none. They touch the same mode and the same file, so they may well want reviewing together, but they are different defects and neither implies the other.
Refs
Filed unassigned, recording only — found while censusing the JSON exits of
packages/cli/src/commands/lint.tsfor #14015 (PR #14973). Not in that card's scope, not touched by it.The reading
packages/cli/src/commands/lint.ts:run()dispatches eval mode at L516 —if (flags.eval) { await this.runEval(flags, timer); return; }tryopens at L561 and its catch-all JSON exit is at L722-730So the eval branch runs entirely above the
try. Nothing thrown out ofrunEvalcan reach that catch-all.runEvalhas one internaltryof its own (L778), coveringbundleRequireof--generatoronly; a throw fromrunMetadataEvalor from anything else below it is uncaught inside the command.The command also hand-rolls its
jsonflag as a plainFlags.booleanrather than using oclif'senableJsonFlag, so oclif wraps nothing: there is no framework-level JSON error envelope to fall back on.⇒ Net effect: a caller that ran
os lint --eval --jsonand hits an uncaught throw gets oclif's human error text, not a JSON document. A machine consumer parsing stdout gets a parse failure rather than an error payload — and the two project-lint exits it may have been written against both do produce JSON on a throw (L730), so the shape is inconsistent across the same command's modes.Why it is worth recording
This is the same family the
conversionsandwarningscards kept landing in — "the machine face is missing on some exit of a command that advertises--json" — one mode over. #12125 and #13741 both settled that every failure exit of an authoring command should carry the same shape; the--evalmode was never brought under that rule, and this is a stronger version of the gap than a missing key: there is no payload at all.I did not construct a reproducer. This is a structural reading of the control flow at the line numbers above, not a measured crash. Whoever picks it up should first establish that a throw is actually reachable there —
runMetadataEvalcaptures per-casegenerationErrorinternally (packages/cli/src/lint/metadata-eval.ts), so the reachable throw sites may be narrow, and "narrow enough not to be worth a contract move" is a legitimate outcome. The bug, if any, is the missing face, not a specific crash.Related, and deliberately separate
⛔ This is not the "make the
--evalexits emitconversions" branch that #14015 fenced off as needing its own card and an at-tier contract review. That one is about a key on two exits that already produce JSON. This one is about a path that produces none. They touch the same mode and the same file, so they may well want reviewing together, but they are different defects and neither implies the other.Refs
os lint --json'sconversionskey can be "read unconditionally", but the two--evalJSON exits omit it — and release notes compile from that changeset #14015 — the census that surfaced this; PR docs(changeset): scope theos lint --jsonconversionsguarantee to the project-lint exits #14973 is its wording-only fixpackages/cli/src/commands/lint.tsL516 (dispatch), L561 (try), L722-730 (catch-all JSON exit), L778 (the eval branch's own narrowtry)