Skip to content

Commit 2da2901

Browse files
claude[bot]claude
andauthored
feat(cli): os lint --strict — warning-severity findings fail the run (#15967)
* feat(cli): `os lint --strict` fails the run on warning-severity findings 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 * test(cli): pin `os lint --strict` as a pair on one warning-only stack 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f50c394 commit 2da2901

4 files changed

Lines changed: 375 additions & 11 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`os lint --strict` makes warning-severity findings fail the run, so an app can rely on the platform's warning-level rules as its gate instead of re-implementing them locally (#15935)
6+
7+
Only an `error` failed `os lint` before. `packages/lint` ships ≈250 authoring rules, 119 of them at `warning`, and a run with any number of warnings and no errors exited 0 — so an app that wanted one of those rules to gate its CI had to re-implement it locally at error level, or bolt a script onto the JSON output to promote a family by hand.
8+
9+
New public flag: **`os lint --strict`**. With it, a run with one or more `warning`-severity findings exits 1 exactly as an `error` does, and the console says why, naming the count and the flag:
10+
11+
```
12+
✗ 1 warning(s) fail this run under --strict (a warning is advisory without the flag)
13+
```
14+
15+
`suggestion`s stay advisory under both. ⛔ The default is unchanged: without the flag the same stack still exits 0, and no existing `os lint` expectation moves.
16+
17+
The `--json` face carries the verdict so a gate can read it without re-deriving it from the counts. Two keys, unconditionally present on every project-lint payload, flag or no flag:
18+
19+
```json
20+
{ "passed": false, "errors": 0, "warnings": 1, "suggestions": 0, "strict": true, "failing": 1 }
21+
```
22+
23+
`strict` says whether the flag was in effect; `failing` is the count the exit code was read from — `errors`, or `errors + warnings` under `--strict`; and `passed` is `failing === 0`, the same statement the exit code makes — so `--strict --json` on a warning-only stack reads `passed: false` beside exit 1, never `passed: true` next to a failing exit.
24+
25+
Not in this change: per-rule severity configuration, any change to a rule's severity, and `--eval` mode, which keeps its own pass bar (`--eval-min`).

content/docs/deployment/cli.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,7 @@ data-model conventions, translation coverage, with a 0-100 quality score.
13511351
os lint # Author-time rules + style / convention checks
13521352
os lint --score # Append a 0-100 metadata quality score (letter-graded)
13531353
os lint --fix # Show what would be fixed (dry-run)
1354+
os lint --strict # Warnings fail the run too (exit 1); suggestions stay advisory
13541355
os lint --json # JSON output for CI
13551356
```
13561357

@@ -1362,6 +1363,18 @@ before #4409: `os lint` ran one gating rule neither other command ran and missed
13621363
six that both of them ran, so it disagreed with the build in **both**
13631364
directions.
13641365

1366+
**What fails the run.** By default only an `error`-severity finding fails
1367+
`os lint` (exit 1); warnings and suggestions are printed and the exit code stays
1368+
0. `--strict` makes a run with one or more `warning`-severity findings exit 1
1369+
exactly as an error does, and says why — `N warning(s) fail this run under
1370+
--strict` — so an app can rely on the platform's warning-level rules as its gate
1371+
instead of re-implementing them locally; `suggestion`s stay advisory either way,
1372+
and the default is unchanged by the flag's existence. On the `--json` face the
1373+
verdict is readable without re-deriving it from the counts: `strict` (whether
1374+
the flag was in effect), `failing` (the count the exit code was read from —
1375+
`errors`, or `errors + warnings` under `--strict`) and `passed` (`failing` is
1376+
`0` — the same statement the exit code makes).
1377+
13651378
#### `os test`
13661379

13671380
Runs Quality Protocol test scenarios (JSON-based BDD) against a running ObjectStack server.

packages/cli/src/commands/lint.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ export default class Lint extends Command {
478478
static override flags = {
479479
json: Flags.boolean({ description: 'Output as JSON' }),
480480
fix: Flags.boolean({ description: 'Show what would be fixed (dry-run)' }),
481+
strict: Flags.boolean({
482+
description:
483+
'Fail the run (exit 1) on warning-severity findings too, exactly as an error does; suggestions stay advisory. Without it only errors fail',
484+
}),
481485
score: Flags.boolean({
482486
description: 'Print a 0–100 metadata-quality score (the lint rubric) for this project',
483487
}),
@@ -612,17 +616,38 @@ export default class Lint extends Command {
612616
// Metadata-quality score (the lint rubric expressed as 0–100).
613617
const score = flags.score ? scoreMetadata(normalized) : null;
614618

619+
// ── Verdict ──
620+
// Only an `error` fails a run by default. `--strict` (#15935) makes a
621+
// `warning` fail it too — so an app can rely on the warning-level rules
622+
// this registry ships as its gate instead of re-implementing them
623+
// locally at error level — while a `suggestion` stays advisory under
624+
// both. `failing` is the ONE count the exit code is read from, computed
625+
// here, above the two faces, so `--json` and the console cannot disagree
626+
// about it. ⛔ The default is deliberately unchanged: promoting warnings
627+
// for every app is a separate decision, not this flag's.
628+
const strict = flags.strict ?? false;
629+
const errors = issues.filter((i) => i.severity === 'error');
630+
const warnings = issues.filter((i) => i.severity === 'warning');
631+
const suggestions = issues.filter((i) => i.severity === 'suggestion');
632+
const failing = errors.length + (strict ? warnings.length : 0);
633+
615634
// ── JSON output ──
616635
if (flags.json) {
617-
const errors = issues.filter((i) => i.severity === 'error');
618-
const warnings = issues.filter((i) => i.severity === 'warning');
619-
const suggestions = issues.filter((i) => i.severity === 'suggestion');
620636
await emitJson({
621-
passed: errors.length === 0,
637+
passed: failing === 0,
622638
total: issues.length,
623639
errors: errors.length,
624640
warnings: warnings.length,
625641
suggestions: suggestions.length,
642+
// [#15935] The verdict, readable without re-deriving it from the
643+
// counts: `strict` says whether the flag was in effect, `failing`
644+
// is the count the exit code was read from — `errors`, or
645+
// `errors + warnings` under `--strict` — and `passed` is
646+
// `failing === 0`, the same statement the exit code makes. Both
647+
// keys are unconditionally present so a gate keying off them never
648+
// has to distinguish "not strict" from "this build does not say".
649+
strict,
650+
failing,
626651
...(hiddenPlatform > 0 ? { hiddenPlatform } : {}),
627652
...(score ? { score: score.score, grade: score.grade } : {}),
628653
issues,
@@ -635,7 +660,7 @@ export default class Lint extends Command {
635660
// distinguish "did not convert" from "this command does not tell me".
636661
conversions: conversionNotices,
637662
duration: timer.elapsed(),
638-
}, errors.length > 0 ? 1 : 0);
663+
}, failing > 0 ? 1 : 0);
639664
return;
640665
}
641666

@@ -659,11 +684,6 @@ export default class Lint extends Command {
659684
return;
660685
}
661686

662-
// Group by severity
663-
const errors = issues.filter((i) => i.severity === 'error');
664-
const warnings = issues.filter((i) => i.severity === 'warning');
665-
const suggestions = issues.filter((i) => i.severity === 'suggestion');
666-
667687
const printIssue = (issue: LintIssue) => {
668688
const color =
669689
issue.severity === 'error' ? chalk.red :
@@ -714,9 +734,20 @@ export default class Lint extends Command {
714734
printInfo('Dry-run mode: no files were modified.');
715735
}
716736

737+
// A run that fails ONLY because of `--strict` says so, naming the count
738+
// and the flag: the summary line above reads identically with and
739+
// without the flag, and exit 1 under a heading that says "Warnings" is
740+
// otherwise a verdict with no stated reason.
741+
if (strict && warnings.length > 0) {
742+
console.log('');
743+
printError(
744+
`${warnings.length} warning(s) fail this run under --strict (a warning is advisory without the flag)`,
745+
);
746+
}
747+
717748
console.log('');
718749

719-
if (errors.length > 0) process.exit(1);
750+
if (failing > 0) process.exit(1);
720751

721752
} catch (error: any) {
722753
if (isExitSignal(error)) throw error;

0 commit comments

Comments
 (0)