new_coverage is 71.2% against an 80% gate — and "new code" is silently the entire codebase #79

Closed
opened 2026-08-20 11:25:14 -05:00 by bermudalamb · 1 comment
Owner

Follow-on from #74, which cleared the security-hotspot condition and left new_coverage as the only failing condition on the quality gate. #61 made the coverage number real; this issue is about the number not being high enough, and about a second thing I found while measuring the gap that changes what "not high enough" even means here.

Where the gate actually stands

Condition Threshold Actual Status
new_coverage ≥ 80 71.2 ERROR
new_security_hotspots_reviewed 100 100.0 OK
new_reliability_rating 1 1 OK
new_security_rating 1 1 OK
new_maintainability_rating 1 1 OK
new_duplicated_lines_density ≤ 3 1.10 OK

One condition from green.

The part worth reading first: the new-code period is not doing what it looks like it's doing

api/new_code_periods/show reports PREVIOUS_VERSION, inherited from the global default. But sonar.projectVersion is never set — it appears nowhere in sonar-project.properties, nowhere in .gitea/workflows/sonarqube.yml, and api/project_analyses/search shows every analysis back to 2026-08-14 recording version = not provided.

With no previous version to diff against, SonarQube treats everything as new code. The measures confirm it directly:

Metric Value
ncloc 5,496
new_lines 9,460
coverage (overall) 69.6
new_coverage 71.2

new_lines exceeds total ncloc because it counts test files too, and new_coverage sits within two points of overall coverage because they are measuring nearly the same thing.

Three consequences, none of which are visible from the SonarQube UI without going looking:

  • The gate has no ratchet. The usual value of a new-code gate is that a pull request is judged on the lines it touched, so coverage climbs from wherever it is without anyone having to pay off the backlog first. That is not happening — every analysis is graded against the whole codebase.
  • Every pull request inherits the entire deficit. A perfectly-tested three-line change still fails the gate, because the gate is not looking at those three lines. This is going to read as "the gate is broken" to anyone who hits it, and they will be very nearly right.
  • It interacts badly with Community edition. With no branch analysis, the only analysis is main, so there is no per-branch new-code baseline to fall back on either.

This is the same failure shape the project has now met four times — #67 (frontend silently skipped, EXECUTION SUCCESS), #60 (ESLint matcher silently matched nothing), #61 (coverage collected but never imported), and now a new-code period that silently means "all code". A tool reporting a plausible number for something other than what you asked it.

What closing the gap actually costs

The gate blends line and condition coverage:

To cover Uncovered Covered
Lines 1,767 471 1,296
Conditions 692 238 454
Total 2,459 709 1,750

1750 / 2459 = 71.2%, which matches. Reaching 80% needs 1,968 covered, so 218 more covered units — uncovered has to fall from 709 to 491.

Where those 709 live, ranked, with a running total:

Uncovered (L + C) To cover Cov % Cumulative File
139 (89 + 50) 144 3.5 139 frontend/src/cart/Cart.tsx
92 (71 + 21) 153 39.9 231 backend/src/routes/cartCheckout.ts
72 (46 + 26) 140 48.6 303 frontend/src/admin/Categories.tsx
42 (34 + 8) 81 48.1 345 backend/src/routes/shippingAddresses.ts
32 (23 + 9) 83 61.4 377 frontend/src/admin/Admin.tsx
31 (19 + 12) 41 24.4 408 backend/src/usps.ts
25 (19 + 6) 61 59.0 433 frontend/src/admin/Tags.tsx
24 (21 + 3) 24 0.0 457 backend/src/server.ts
24 (19 + 5) 92 73.9 481 frontend/src/components/ItemCard.tsx
22 (10 + 12) 112 80.4 503 backend/src/routes/adminCategories.ts
18 (15 + 3) 21 14.3 541 frontend/src/cart/cartApi.ts

The top two files alone are 231 units — more than the 218 needed. The gate is two files away from green, not a codebase-wide effort.

And those two files are the ones you'd least want uncovered

Cart.tsx at 3.5%, cartCheckout.ts at 39.9%, cartApi.ts at 14.3%. The cart and checkout vertical — the path that takes customers' money — is simultaneously the least-covered code in the project and, on its own, the entire gate gap.

Cart.tsx at 3.5% is worth a second look before writing anything, because it is surprising on its face. Frontend coverage comes from istanbul instrumenting a dev server that Playwright drives, and several e2e specs mention the cart — yet the cart page component reports as essentially never executed. Either the specs exercise cart state through the API without ever rendering that component, or something in collection is missing it. Those call for completely different work, so establish which before estimating. Note this is exactly the flattering-coverage weakness #72 documents, seen from the other side: e2e coverage overstates what is tested, so a file it reports at 3.5% is being generous.

server.ts at 0.0% is a different case and probably should not be chased — it is process bootstrap, and testing it has little value. Consider sonar.coverage.exclusions rather than a token test that games the number.

Two directions, and they are not alternatives

1. Set sonar.projectVersion so the new-code period means something. Without this the gate stays structurally wrong however many tests get written, and any future contributor hits a gate that judges them for code they never touched. This is small — a version in sonar-project.properties or fed from CI, plus deciding what a "version" means for a project with no releases. A date-based or reference-branch new-code period may fit better than PREVIOUS_VERSION given Community edition; worth deciding deliberately rather than inheriting the global default.

2. Cover the cart and checkout vertical. Worth doing on its own merits regardless of the gate, and it happens to close the gap.

Doing (1) alone turns the gate green immediately by narrowing what it measures — which is the right behaviour but should not be mistaken for the risk going away. Doing (2) alone leaves the structural problem in place for the next person. Recommend (1) first because it is cheap and makes the gate honest, then (2) as real work with its own issue if it turns out to be a project rather than a task.

Relationship to the neighbouring issues

  • #61 — imported coverage in the first place. Note it is still open even though its PR (#73) merged; may just need closing.
  • #72 — covers whether the coverage number is honest (frontend measures execution rather than testing, no guard against partial collapse, integration suite hangs). This issue is about the number being below the bar and about the period it is measured over. Related but not overlapping; the Cart.tsx question above is the one place they touch.
  • #74 — cleared the other failing condition. Closed.

Severity

Medium-high. Nothing is broken in production, but the gate is red, and the half of it that is a real signal is pointing at the checkout path.

Follow-on from #74, which cleared the security-hotspot condition and left `new_coverage` as the **only** failing condition on the quality gate. #61 made the coverage number real; this issue is about the number not being high enough, and about a second thing I found while measuring the gap that changes what "not high enough" even means here. ## Where the gate actually stands | Condition | Threshold | Actual | Status | | --- | --- | --- | --- | | `new_coverage` | ≥ 80 | **71.2** | **ERROR** | | `new_security_hotspots_reviewed` | 100 | 100.0 | OK | | `new_reliability_rating` | 1 | 1 | OK | | `new_security_rating` | 1 | 1 | OK | | `new_maintainability_rating` | 1 | 1 | OK | | `new_duplicated_lines_density` | ≤ 3 | 1.10 | OK | One condition from green. ## The part worth reading first: the new-code period is not doing what it looks like it's doing `api/new_code_periods/show` reports `PREVIOUS_VERSION`, inherited from the global default. But `sonar.projectVersion` is **never set** — it appears nowhere in `sonar-project.properties`, nowhere in `.gitea/workflows/sonarqube.yml`, and `api/project_analyses/search` shows every analysis back to 2026-08-14 recording `version = not provided`. With no previous version to diff against, SonarQube treats **everything** as new code. The measures confirm it directly: | Metric | Value | | --- | --- | | `ncloc` | 5,496 | | `new_lines` | **9,460** | | `coverage` (overall) | 69.6 | | `new_coverage` | 71.2 | `new_lines` exceeds total `ncloc` because it counts test files too, and `new_coverage` sits within two points of overall coverage because they are measuring nearly the same thing. Three consequences, none of which are visible from the SonarQube UI without going looking: - **The gate has no ratchet.** The usual value of a new-code gate is that a pull request is judged on the lines it touched, so coverage climbs from wherever it is without anyone having to pay off the backlog first. That is not happening — every analysis is graded against the whole codebase. - **Every pull request inherits the entire deficit.** A perfectly-tested three-line change still fails the gate, because the gate is not looking at those three lines. This is going to read as "the gate is broken" to anyone who hits it, and they will be very nearly right. - **It interacts badly with Community edition.** With no branch analysis, the only analysis is `main`, so there is no per-branch new-code baseline to fall back on either. This is the same failure shape the project has now met four times — #67 (frontend silently skipped, `EXECUTION SUCCESS`), #60 (ESLint matcher silently matched nothing), #61 (coverage collected but never imported), and now a new-code period that silently means "all code". A tool reporting a plausible number for something other than what you asked it. ## What closing the gap actually costs The gate blends line and condition coverage: | | To cover | Uncovered | Covered | | --- | --- | --- | --- | | Lines | 1,767 | 471 | 1,296 | | Conditions | 692 | 238 | 454 | | **Total** | **2,459** | **709** | **1,750** | 1750 / 2459 = 71.2%, which matches. Reaching 80% needs 1,968 covered, so **218 more covered units** — uncovered has to fall from 709 to 491. Where those 709 live, ranked, with a running total: | Uncovered (L + C) | To cover | Cov % | Cumulative | File | | --- | --- | --- | --- | --- | | 139 (89 + 50) | 144 | **3.5** | 139 | `frontend/src/cart/Cart.tsx` | | 92 (71 + 21) | 153 | 39.9 | 231 | `backend/src/routes/cartCheckout.ts` | | 72 (46 + 26) | 140 | 48.6 | 303 | `frontend/src/admin/Categories.tsx` | | 42 (34 + 8) | 81 | 48.1 | 345 | `backend/src/routes/shippingAddresses.ts` | | 32 (23 + 9) | 83 | 61.4 | 377 | `frontend/src/admin/Admin.tsx` | | 31 (19 + 12) | 41 | 24.4 | 408 | `backend/src/usps.ts` | | 25 (19 + 6) | 61 | 59.0 | 433 | `frontend/src/admin/Tags.tsx` | | 24 (21 + 3) | 24 | **0.0** | 457 | `backend/src/server.ts` | | 24 (19 + 5) | 92 | 73.9 | 481 | `frontend/src/components/ItemCard.tsx` | | 22 (10 + 12) | 112 | 80.4 | 503 | `backend/src/routes/adminCategories.ts` | | 18 (15 + 3) | 21 | 14.3 | 541 | `frontend/src/cart/cartApi.ts` | **The top two files alone are 231 units** — more than the 218 needed. The gate is two files away from green, not a codebase-wide effort. ## And those two files are the ones you'd least want uncovered `Cart.tsx` at **3.5%**, `cartCheckout.ts` at 39.9%, `cartApi.ts` at 14.3%. The cart and checkout vertical — the path that takes customers' money — is simultaneously the least-covered code in the project and, on its own, the entire gate gap. `Cart.tsx` at 3.5% is worth a second look before writing anything, because it is surprising on its face. Frontend coverage comes from istanbul instrumenting a dev server that Playwright drives, and several e2e specs mention the cart — yet the cart page component reports as essentially never executed. Either the specs exercise cart state through the API without ever rendering that component, or something in collection is missing it. Those call for completely different work, so establish which before estimating. Note this is exactly the flattering-coverage weakness #72 documents, seen from the other side: e2e coverage overstates what is tested, so a file it reports at 3.5% is being generous. `server.ts` at 0.0% is a different case and probably should not be chased — it is process bootstrap, and testing it has little value. Consider `sonar.coverage.exclusions` rather than a token test that games the number. ## Two directions, and they are not alternatives **1. Set `sonar.projectVersion` so the new-code period means something.** Without this the gate stays structurally wrong however many tests get written, and any future contributor hits a gate that judges them for code they never touched. This is small — a version in `sonar-project.properties` or fed from CI, plus deciding what a "version" means for a project with no releases. A date-based or reference-branch new-code period may fit better than `PREVIOUS_VERSION` given Community edition; worth deciding deliberately rather than inheriting the global default. **2. Cover the cart and checkout vertical.** Worth doing on its own merits regardless of the gate, and it happens to close the gap. Doing (1) alone turns the gate green immediately by narrowing what it measures — which is the right behaviour but should not be mistaken for the risk going away. Doing (2) alone leaves the structural problem in place for the next person. Recommend (1) first because it is cheap and makes the gate honest, then (2) as real work with its own issue if it turns out to be a project rather than a task. ## Relationship to the neighbouring issues - **#61** — imported coverage in the first place. Note it is **still open** even though its PR (#73) merged; may just need closing. - **#72** — covers whether the coverage number is *honest* (frontend measures execution rather than testing, no guard against partial collapse, integration suite hangs). This issue is about the number being *below the bar* and about the period it is measured over. Related but not overlapping; the `Cart.tsx` question above is the one place they touch. - **#74** — cleared the other failing condition. Closed. ## Severity Medium-high. Nothing is broken in production, but the gate is red, and the half of it that is a real signal is pointing at the checkout path.
Author
Owner

Direction 1 done on feature/79-sonar-project-version, and it corrects a claim I made above

sonar.projectVersion=1.0.0 now declared in sonar-project.properties, with a comment explaining what PREVIOUS_VERSION baselines against and when to bump it. Left at 1.0.0 to match both package.json files; nothing enforces that they stay in step, so the comment says to move all three together.

Verified by scanning, not by reasoning about the config

Ran scripts/scan-local.sh against the scratch key. Taking this project's own rule that EXECUTION SUCCESS is weak evidence, I checked the measures rather than the exit code:

Check Result
Version recorded 1.0.0 — previous two analyses still read not provided
ncloc 5,557 — held, so nothing was silently dropped
Coverage imported 69.5%, matching the real project's 69.6% — the lcov files were read
new_lines 151, down from the whole codebase

The window is now the diff. That part works.

The correction

Above I wrote that doing this alone "turns the gate green immediately by narrowing what it measures". That is wrong, and the verification scan is what showed it.

With the window narrowed, new_coverage on the scratch scan came back 0.0% — against just 2 new lines to cover, neither covered. Two uncovered lines in a small diff is all it takes to read as zero and fail an 80% threshold outright.

So narrowing the window does not make the gate pass. It makes the gate volatile: on a small diff the percentage is a tiny sample and swings between 0 and 100 on a line or two. The number settles as commits accumulate and the window widens, but expect the gate to be jumpy across the first few merges after this lands, and expect some genuinely-fine pull requests to fail it. That is a better failure than the one being replaced — it fails on code you actually touched — but it is not the clean win I described.

Which raises a question this issue should probably answer

PREVIOUS_VERSION only works as a rolling window if somebody bumps the version. This project has no git tags at all, and both package.json files have sat at 1.0.0. If 1.0.0 is never bumped, the baseline stays pinned at today's analysis and the window widens indefinitely — which eventually drifts back toward the original problem of grading most of the codebase, just slowly enough not to notice.

NUMBER_OF_DAYS (say 30) would be self-maintaining: always a real window, never dependent on a release ritual this project does not have. It is a server-side setting via api/new_code_periods/set rather than a repo file, so it is a separate one-line change I can apply whenever — flagging it rather than doing it, since it changes gate behaviour for everyone and was not part of what was agreed.

Still open here

Direction 2 — covering the cart and checkout vertical (Cart.tsx at 3.5%, cartCheckout.ts at 39.9%, cartApi.ts at 14.3%), including first establishing why Cart.tsx reports as essentially never executed when e2e specs touch the cart. Unchanged by this commit, and the more valuable half.

Not pushed — left local per the usual arrangement.

## Direction 1 done on `feature/79-sonar-project-version`, and it corrects a claim I made above `sonar.projectVersion=1.0.0` now declared in `sonar-project.properties`, with a comment explaining what PREVIOUS_VERSION baselines against and when to bump it. Left at 1.0.0 to match both `package.json` files; nothing enforces that they stay in step, so the comment says to move all three together. ## Verified by scanning, not by reasoning about the config Ran `scripts/scan-local.sh` against the scratch key. Taking this project's own rule that `EXECUTION SUCCESS` is weak evidence, I checked the measures rather than the exit code: | Check | Result | | --- | --- | | Version recorded | `1.0.0` — previous two analyses still read `not provided` | | `ncloc` | 5,557 — held, so nothing was silently dropped | | Coverage imported | 69.5%, matching the real project's 69.6% — the lcov files were read | | **`new_lines`** | **151**, down from the whole codebase | The window is now the diff. That part works. ## The correction Above I wrote that doing this alone "turns the gate green immediately by narrowing what it measures". **That is wrong, and the verification scan is what showed it.** With the window narrowed, `new_coverage` on the scratch scan came back **0.0%** — against just **2** new lines to cover, neither covered. Two uncovered lines in a small diff is all it takes to read as zero and fail an 80% threshold outright. So narrowing the window does not make the gate pass. It makes the gate **volatile**: on a small diff the percentage is a tiny sample and swings between 0 and 100 on a line or two. The number settles as commits accumulate and the window widens, but expect the gate to be jumpy across the first few merges after this lands, and expect some genuinely-fine pull requests to fail it. That is a better failure than the one being replaced — it fails on code you actually touched — but it is not the clean win I described. ## Which raises a question this issue should probably answer PREVIOUS_VERSION only works as a rolling window if somebody bumps the version. This project has **no git tags at all**, and both `package.json` files have sat at 1.0.0. If 1.0.0 is never bumped, the baseline stays pinned at today's analysis and the window widens indefinitely — which eventually drifts back toward the original problem of grading most of the codebase, just slowly enough not to notice. `NUMBER_OF_DAYS` (say 30) would be self-maintaining: always a real window, never dependent on a release ritual this project does not have. It is a server-side setting via `api/new_code_periods/set` rather than a repo file, so it is a separate one-line change I can apply whenever — flagging it rather than doing it, since it changes gate behaviour for everyone and was not part of what was agreed. ## Still open here Direction 2 — covering the cart and checkout vertical (`Cart.tsx` at 3.5%, `cartCheckout.ts` at 39.9%, `cartApi.ts` at 14.3%), including first establishing *why* `Cart.tsx` reports as essentially never executed when e2e specs touch the cart. Unchanged by this commit, and the more valuable half. Not pushed — left local per the usual arrangement.
bermudalamb self-assigned this 2026-08-20 15:21:44 -05:00
bermudalamb added this to the Code Quality and Hardening project 2026-08-20 15:21:51 -05:00
bermudalamb moved this to Ready for Release in Code Quality and Hardening on 2026-08-20 15:21:55 -05:00
bermudalamb moved this to Released in Code Quality and Hardening on 2026-08-21 12:12:11 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#79