Skip to content

fix: four second-tier correctness defects (--json truncation, badge NaN, score buckets)#20

Open
PunGrumpy wants to merge 5 commits into
mainfrom
advisor/007-second-tier-correctness
Open

fix: four second-tier correctness defects (--json truncation, badge NaN, score buckets)#20
PunGrumpy wants to merge 5 commits into
mainfrom
advisor/007-second-tier-correctness

Conversation

@PunGrumpy

Copy link
Copy Markdown
Owner

What

Fixes four second-tier correctness defects, grouped because each is small and independently verifiable. Implements plan 007. Depends on 002 (CLI integration tests).

Defects

A — piped --json output was silently truncated ✅

The CLI wrote the report with console.log then immediately called process.exit(), which does not flush an async stdout pipe — so docker-doctor . --json > report.json (or piping to jq in CI) produced truncated, invalid JSON at exactly the sizes that matter. Fixed by switching the three scan output paths (--score, --json, default) to process.exitCode = n; return; so Node drains stdout before exiting. The six immediate-abort process.exit() calls (two prompt Ctrl-C handlers, SIGINT/SIGTERM, two stderr error paths) are untouched.

Verified live: a 200-Dockerfile fixture producing 2400 diagnostics / 1.04 MB of --json now parses cleanly. The fixture was confirmed to actually reproduce the truncation on the old process.exit() code (JSON.parse failed every run, bytes short of the payload) — a real regression test, not synthetic. Exit codes preserved exactly: --score exits 1 below 50; --json/default exit 1 on any error-severity diagnostic.

B — badge rendered NaN/100 and cached it for an hour ✅

?s=abcNumber("abc") = NaN survived all clamps and was served with Cache-Control: public, max-age=3600. Fixed with a shared parseScoreQuery helper in apps/web/lib/score.ts that falls back when the value isn't finite, applied to both the badge and OG routes. grep NaN apps/web/app/share/ is now empty. (Empty ?s= renders 0, a present finite value; absent/null falls back to 100 — deliberate.)

C — score thresholds diverged into three copies ⚠️ partially unified

Extracted SCORE_BUCKETS + getScoreBucket into packages/core/src/scoring.ts and exported them from packages/core/src/index.ts; calculateScore now derives its label from the table with the returned strings byte-identical (incl. the ⚠️ U+FE0F variation selector — verified against a hex dump and by a new "Critical 🚨" test).

Wiring apps/web to consume core hit a real blocker and STOPPED per the plan: apps/web/tsconfig.json targets ES2017, but core's raw-TS entry pulls in dockerfile-parser.ts / security.ts, which use ES2018 named-capture-group regex (TS1503). Rather than create a 4th copy, the web-side core dependency was reverted cleanly (package.json/bun.lock show no net diff) and the existing apps/web/lib/score.ts table was completed with the missing Critical bucket. Result: 3 copies → 2 (core is now the single source for core/CLI; the web app has one local table pending the tsconfig decision below).

D — ignore.files is a validated no-op → reported, NOT implemented

It validates but is read by nothing (filterDiagnostics only consumes categories). Two options are surfaced for the maintainer (implement with a glob dep, which grows the CLI and raises existing users' scores; or remove it so it fails loudly, with a doc fix in plan 009). Neither implemented — this is a product decision.

Two decisions for the maintainer

  1. Defect C-web unification — bump apps/web/tsconfig.json target to ES2018 (safe for Next 16/React 19) so the web app can import core's SCORE_BUCKETS, achieving a true single source; or keep the local web table.
  2. Defect D — implement vs. remove ignore.files.

Verification

  • core 34/0 · cli 14/0 · typecheck 0 · ultracite 0 · bun run build 0 (CLI + core + web).
  • .changeset/quiet-planets-listen.md@docker-doctor/cli patch (covers defect A exit-flush + defect C SCORE_BUCKETS export). Defect B is apps/web-only.

Stacking & merge note

Base is advisor/002-cli-verification-baseline (PR #14). Auto-retargets to main on #14's merge.

⚠️ Merge-order note: PR #19 (plan 008) also edits packages/core/src/scoring.ts and packages/core/test/scoring.test.ts — 008 changes the formula, this PR refactors the label buckets and adds a scoring test. Whichever merges second needs a small conflict resolution in both files (keep 008's monotonic formula + this PR's SCORE_BUCKETS, and both test sets).

🤖 Generated with Claude Code

Establishes a verification baseline for @docker-doctor/cli by spawning
the built dist/cli.mjs against small Dockerfile fixtures and asserting
on the three documented exit-code paths and the --json report contract.
process.exit() in the scan action's output paths aborted the process
before async stdout writes to a pipe/file finished flushing, silently
truncating --json/--score output when redirected. Replace the three
output-path exit calls with process.exitCode + return so Node exits
naturally once the event loop drains and stdout is fully flushed. The
six interrupt/signal/error-handler exit calls are untouched.

Adds a fixture-driven regression test (200-service fixture, ~475KB of
JSON) that reproduces the truncation against the old code and asserts
the fix produces byte-identical, fully parseable output whether piped
or redirected to a file.
parseQuery only fell back on null, so a non-numeric ?s= value (e.g.
"abc") produced Number("abc") = NaN, which survives Math.min/Math.max
clamping and gets served (and cached for an hour) as "NaN/100" on both
the badge SVG and the OG image.

Extract a single shared helper, parseScoreQuery, in apps/web/lib/score.ts
(fallback applies to null/absent and to any non-finite parse) and use it
from both apps/web/app/share/badge/route.ts and apps/web/app/share/og/route.tsx
instead of two separate ad hoc implementations.

Note: an empty ?s= is a *present* value (Number("") is 0), so it clamps
to 0 rather than falling back -- deliberate, not an oversight.
The score-to-label/color threshold table (90/75/50/0) was duplicated
three times: packages/core/src/scoring.ts (4 buckets, inline if/else),
apps/web/lib/score.ts (3 buckets, missing Critical), and
apps/web/app/share/badge/route.ts (a third, color-only set, also
missing Critical).

Extract SCORE_BUCKETS + getScoreBucket in packages/core/src/scoring.ts
as the single source of truth and export them from
packages/core/src/index.ts. calculateScore's returned label strings
are byte-identical to before (verified against the exact UTF-8 bytes,
including the "Needs Work ⚠️" variation selector) -- covered
by a new packages/core/test/scoring.test.ts, including the previously
untested "Critical \ud83d\ude a8" bucket.

apps/web cannot cleanly depend on @docker-doctor/core: core's raw-TS
sources use regex named capturing groups (dockerfile-parser.ts,
rules/security.ts) that require `target >= ES2018`, but apps/web's
tsconfig targets ES2017, so tsc --noEmit fails across the whole app
when core is imported. Per the plan, no fourth copy was created;
apps/web/lib/score.ts and the badge route instead gained a completed,
NaN-safe, single *local* bucket table (adding the missing Critical
bucket, mirroring core's thresholds) so the badge's color mapping and
label mapping share one source within apps/web, even though it can't
yet be unified with core. See the executor report for the exact error
and the maintainer decision this needs (bump apps/web's target, or
drop named capture groups from core).
… SCORE_BUCKETS export

Covers defect A (process.exitCode instead of process.exit on the
scan action's output paths) and defect C (SCORE_BUCKETS/getScoreBucket
now exported from @docker-doctor/core, bundled into the CLI).
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docker-doctor Ready Ready Preview, Comment Jul 23, 2026 3:05am

@github-actions

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 7caba0e.

Base automatically changed from advisor/002-cli-verification-baseline to main July 23, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant