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.
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 whyCart.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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follow-on from #74, which cleared the security-hotspot condition and left
new_coverageas 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
new_coveragenew_security_hotspots_reviewednew_reliability_ratingnew_security_ratingnew_maintainability_ratingnew_duplicated_lines_densityOne 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/showreportsPREVIOUS_VERSION, inherited from the global default. Butsonar.projectVersionis never set — it appears nowhere insonar-project.properties, nowhere in.gitea/workflows/sonarqube.yml, andapi/project_analyses/searchshows every analysis back to 2026-08-14 recordingversion = not provided.With no previous version to diff against, SonarQube treats everything as new code. The measures confirm it directly:
nclocnew_linescoverage(overall)new_coveragenew_linesexceeds totalnclocbecause it counts test files too, andnew_coveragesits 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:
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:
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:
frontend/src/cart/Cart.tsxbackend/src/routes/cartCheckout.tsfrontend/src/admin/Categories.tsxbackend/src/routes/shippingAddresses.tsfrontend/src/admin/Admin.tsxbackend/src/usps.tsfrontend/src/admin/Tags.tsxbackend/src/server.tsfrontend/src/components/ItemCard.tsxbackend/src/routes/adminCategories.tsfrontend/src/cart/cartApi.tsThe 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.tsxat 3.5%,cartCheckout.tsat 39.9%,cartApi.tsat 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.tsxat 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.tsat 0.0% is a different case and probably should not be chased — it is process bootstrap, and testing it has little value. Considersonar.coverage.exclusionsrather than a token test that games the number.Two directions, and they are not alternatives
1. Set
sonar.projectVersionso 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 insonar-project.propertiesor 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 thanPREVIOUS_VERSIONgiven 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
Cart.tsxquestion above is the one place they touch.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.
Direction 1 done on
feature/79-sonar-project-version, and it corrects a claim I made abovesonar.projectVersion=1.0.0now declared insonar-project.properties, with a comment explaining what PREVIOUS_VERSION baselines against and when to bump it. Left at 1.0.0 to match bothpackage.jsonfiles; 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.shagainst the scratch key. Taking this project's own rule thatEXECUTION SUCCESSis weak evidence, I checked the measures rather than the exit code:1.0.0— previous two analyses still readnot providednclocnew_linesThe 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_coverageon 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.jsonfiles 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 viaapi/new_code_periods/setrather 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.tsxat 3.5%,cartCheckout.tsat 39.9%,cartApi.tsat 14.3%), including first establishing whyCart.tsxreports 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.