feat(cli): os lint --strict — warning-severity findings fail the run - #15967
Conversation
Only an error failed the run; with the flag a warning does too, suggestions stay advisory and the default is unchanged. The --json face publishes the verdict as `strict` + `failing` beside `passed`, and the console names the count and the flag when the flag alone decided the exit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
Eight e2e pins over four fixtures: the stack REALLY warns (2 warnings, 0 errors) before either half is read; without the flag exit 0 / passed:true, with it exit 1 / passed:false / failing === warnings; the console names the count and the flag; suggestions stay advisory; errors still fail and `failing` sums under the flag; a clean stack stays 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b6f1d3246153e7b005f31d13f4720f1c43fd4e9b && git checkout b6f1d3246153e7b005f31d13f4720f1c43fd4e9b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ceb48776534dc6ac5923cdabb717f6de6b135ae0 b239599c619ce2b430a8cd039142af910f02f4af && git checkout -B drift-repro ceb48776534dc6ac5923cdabb717f6de6b135ae0 && git merge --no-ff b239599c619ce2b430a8cd039142af910f02f4af
node scripts/docs-audit/affected-docs.mjs --json ceb48776534dc6ac5923cdabb717f6de6b135ae0
|
Standing down on the merge-queue red — it is not this PR's, and it is blocking the whole queueEpic PM for hotcrm#1579, The failureQueue branch Why it is not this PR's — measured, not assertedThe same check is red on
And on this PR's own head Root cause already diagnosed by other seats — ⛔ no third card filed, this is a dedupe hit with a working control:
⇒ An environment move (the pnpm version Corepack resolves), surfacing through an unpinned invocation in a gate fixture. Nothing in this PR touches No fix ported, because none exists yetSearched open PRs for a repair (regen-pending / self-test / pnpm / corepack / merge-driver): zero. ⛔ A zero is not a zero without a control — the control query ( ⛔ Deliberately not repairing it here. The fix belongs in shared tooling that two seats already hold cards on, it is outside this PR's declared file surface, and adding an unreviewed tooling change to a PR that has just passed contract review would put a second, unreviewed change into a contract-surface PR. The queue is blocked for everyone, so this lands centrally once, not per-PR. ⛔ The one permitted re-run is not spent, and will not be spent on this. Re-submitting into a queue whose base is red reproduces the same failure by construction — it is not a flake, and "flake" is not a root cause. The re-run stays unspent for a case that could actually be one. What happens nextThis PR holds. When the queue is unblocked — #15992 / #15994 landing on Generated by Claude Code |
Fixes #15935
Epic hotcrm#1579, step 1 (dispatched by the epic PM). Adds the public flag
os lint --strict: with it, a run with one or morewarning-severity findings exits 1 exactly as anerrordoes;suggestionstays advisory; the default is unchanged.What changed
packages/cli/src/commands/lint.ts— the flag, and one verdict computed above the two faces:failing = errors + (strict ? warnings : 0). The exit code (both faces) readsfailing, so--jsonand the console cannot disagree. The console names the count and the flag when the flag alone decided the exit:N warning(s) fail this run under --strict (a warning is advisory without the flag). The pre-existing--i18n-strict(which re-grades i18n coverage findings) is untouched and composes with this one.--jsonface, two keys unconditionally present on every project-lint payload, flag or no flag:strict(was the flag in effect) andfailing(the count the exit was read from).passedis nowfailing === 0— the same statement the exit code makes. Without the flagfailing === errors, sopassedis byte-identical to before; with the flag a warning-only stack readspassed: falsebeside exit 1 rather thanpassed: truenext to a failing exit.content/docs/deployment/cli.mdx— the flag in theos lintblock plus a "What fails the run" paragraph (the flag, the default, the three JSON keys)..changeset/cli-lint-strict-warnings-fail.md—@objectstack/climinor.packages/cli/test/lint-strict-warnings.e2e.test.ts— the pins below.Re-measured on this branch's base
a2051fa(the card measured6c439f2):packages/lintstill ships 161 error / 119 warning / 5 info / 6 suggestion (git grep -h -oE "severity: '(error|warning|info|suggestion)'" -- packages/lint/src | sort | uniq -c), andlint.ts:719was stillif (errors.length > 0) process.exit(1);— the card's premise holds.Contract spelling, for the downstream card that pins it: the flag is exactly
--strict; the JSON keys arestrict(boolean) andfailing(integer).What each pin establishes
Eight e2e tests spawn the SOURCE CLI (
bin/run-dev.jsvia tsx) over four generated fixtures —warns(2 warnings, 0 errors, 0 suggestions),suggests(2 suggestions only),errs(1 error + 1 warning),clean(nothing):warnsreally warns —errors === 0,warnings > 0, andwarnings === 2— read off the--jsonface of the run under test BEFORE either half of the pair, so the pair cannot pass over an empty set.warnswithout the flag: exit 0,passed: true,strict: false,failing: 0— a regression that promoted warnings by default reds here.warnswith the flag: exit 1,passed: false,strict: true,failing === warnings— a regression that made the flag a no-op reds here; the count is the number of warnings, not "1 if any".--jsonrun measured.suggestswith the flag: exit 0,failing: 0— the severity boundary (suggestions stay advisory).errs: exit 1 with and without the flag;failing === errorswithout,failing === errors + warningswith.cleanwith the flag: exit 0 in both faces — the flag does not invent a failure.Which rule produces which finding is deliberately not pinned (a rule's severity is
packages/lint's decision); the count preconditions go red loudly if one moves.Reverse verification (ablation), both directions, restore proven by blob hash
Committed first (
b239599), then mutated on disk, run, restored withgit checkout HEAD -- packages/cli/src/commands/lint.ts, and the restore proven bygit hash-objectequal to the HEAD blob (a0bb8e20…) plusgit diff HEADempty — never by exit code. Anchor and marker counts were read before and after each mutation (anchor 1→0, marker 0→1; after restore marker 0). Nodist/sits on the measured path (the tests runlint.tsfrom source), so no rebuild leg applies.failing = errors.length): 3 failed / 5 passed — exactly pins 3, 4 and the strict half of 7. Predicted direction: red on the strict half; observed as predicted.strict = true): 3 failed / 5 passed — exactly pins 2, 5 and the default half of 7. Predicted: red on the default half; observed as predicted.Verification, all at
b239599c(tree content identical to the gate runs)Every build/test below went through
scripts/pm/os-verify-lock.sh; every result is the wrapper'sVERDICT command-exitline.pnpm exec turbo run build --filter='@objectstack/cli^...': 56/56, exit 0.pnpm --filter @objectstack/cli build: exit 0.pnpm --filter @objectstack/cli exec vitest run --project integration test/lint-strict-warnings.e2e.test.ts: 1 file, 8/8 passed, exit 0.pnpm --filter @objectstack/cli exec vitest run --project unit: 177 files — 176 passed, 1 refused as a prerequisite (published-subpath-hook-body.pin.test.ts: "packages/cli is not built"); afterpnpm --filter @objectstack/cli buildthat file reruns 13/13, exit 0. Net: 177/177.pnpm --filter @objectstack/cli typecheck(tsc +check:test-typecheckovertsconfig.test.json, which compiles the new test file): exit 0.TURBO_SCM_BASE=a2051fa pnpm exec turbo ls --affected: cli, dogfood, downstream-contract, example-crm, example-multi-package (no test script), example-showcase, example-todo. Run: downstream-contract 3 files/31 tests, example-todo 4/106, example-crm 5/45, example-showcase 27/375 — all exit 0.grep -l 'build-shaped-artifact|@objectstack/cli|run-dev.js|lint' packages/qa/dogfood/test) ran: 5 files / 45 tests, exit 0. The rest is CI's (Dogfood Regression Gate).packages/cli: only the new file and the pin rerun ran locally (the diff touches no spawn entry, no driver, no kernel boot path); the tier is declared to CI.node scripts/pm/dispatch-gates.mjs --commandsderived 85 families from the real changed set (untracked included) at this tree; all 85 ran;--ranreconciles "85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN". Three needed a real rerun to reach a verdict, none for a reason connected to this diff:check:skill-examples(neededclient-react/clientdist; rerun: 257 examples type-check, exit 0),check:dual-build-cjs-loadsandcheck:i18n-coverage(needed packages outside cli's closure built; afterturbo run build --filter='!@objectstack/docs'— 71/72 from the shared cache — both exit 0: 13 configs, none new; dual-build population 103/66/619/1 at floors), andcheck-plugin-teardown-shape --self-test(refused on the shallow clone; aftergit fetch --depth=1 origin 621a4876…it passes 47 cases). Everything else exit 0 on first run,check:nul-bytesincluded.eslint.config.mjs(thepackages/cli/src/**/*.{ts,tsx,mts,cts}block;packages/cli/testnot ignored);eslint --no-inline-config --format jsonover the two TS files changed: 2 files, 0 errors / 0 warnings; invariance: the config runs no typed linting (line 328: noparserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's.Not in this change
--evalkeeps its own pass bar (--eval-min), and--stricthas no effect in that mode, matching how--scorebehaves there.skills/**is untouched: no generator emits theos lintflag list (the--score/--evallines inskills/objectstack-data/SKILL.mdare hand-written prose on a governed surface), so the card's "follows through the generator" has nothing to run — flagged in the report for the PM.Generated by Claude Code