Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .claude/skills/contributing-to-gittensory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: >-
JSONbored/gittensory repo — picking/validating an issue, implementing a change, writing tests
that pass Codecov, running the local CI gate, predicting the gittensory gate, and formatting the
commit + PR. gittensory reviews PRs ONE-SHOT via the gittensory gate (a GitHub App / CI) plus a
strict CI suite with Codecov (97% patch coverage, hard); there is no review back-and-forth, so a
strict CI suite with Codecov (99% patch coverage, hard); there is no review back-and-forth, so a
PR must be correct, fully tested, house-style-compliant, and green before it is pushed. Invoke
for any "contribute to / open a PR against / fix a bug in / add a feature to gittensory" task.
---
Expand Down Expand Up @@ -49,7 +49,7 @@ opening a **fresh** PR. This is the entire reason to get it right before you pus

## The non-negotiables (read once, hold throughout)

1. **97%+ patch coverage is a HARD wall.** Codecov `codecov/patch` has `target: 97%, threshold: 0%`
1. **99% patch coverage is a HARD wall.** Codecov `codecov/patch` has `target: 99%, threshold: 0%`
— *zero slack* — and it counts **BRANCH** coverage, not just lines. **Aim for 100% on every line
you change, including invariants and a regression test for any bug you fix.** Only `src/**` counts;
`apps/**`, `test/**`, `scripts/**`, and `src/env.d.ts` are ignored by Codecov. (So a UI-only change
Expand Down Expand Up @@ -183,8 +183,8 @@ This is where most PRs fail Codecov. The bar is **every changed line AND every c
so a single shard under-reports).
- **Find the uncovered branch.** In the v8 text report, read the **% Branch** column and the
**Uncovered Line #s** for your changed file — a line at 100% lines but <100% branch has an un-taken
`??`/ternary/`&&` side; add that case. Aim for **≥98% branch on your diff locally** so normal CI
variance never drops you under the 97% wall.
`??`/ternary/`&&` side; add that case. Aim for **100% branch on your diff locally** so normal CI
variance never drops you under the 99% wall.

---

Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/contributing-to-gittensory/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ workflow in this repo. There is **no** root-level Prettier gate — Prettier is

## 2. Codecov — the real coverage gate (`codecov.yml`)

- **`codecov/patch`: `target: 97%`, `threshold: 0%`, `if_ci_failed: error`.** Every line your PR
changes must be ≥97% covered. With 0% threshold and small diffs, **one uncovered branch can fail it.**
- **`codecov/patch`: `target: 99%`, `threshold: 0%`, `if_ci_failed: error`, `only_pulls: true`.**
Every line your PR changes must be ≥99% covered. With 0% threshold and small diffs, **one uncovered branch can fail it.**
- **It counts BRANCH coverage** (v8 → lcov `BRDA`). A changed line whose branches are only partially
exercised counts against you. "100% lines" ≠ "100% branches."
- **`codecov/project`: `informational: true`** — a trend, never blocks.
Expand Down Expand Up @@ -168,7 +168,7 @@ Date("2026-05-28T00:00:00Z"))` then `vi.useRealTimers()`.
or the `test:unit` / `test:integration` scripts. Verify before pushing with the full unsharded
`npm run test:coverage`. **Find a partial branch:** read the v8 text report's **% Branch** column and
the **Uncovered Line #s** for your changed file (or open `coverage/lcov-report/…` / `coverage/lcov.info`)
— a line at 100% lines but <100% branch has an un-taken side. Aim ≥98% branch on the diff locally.
— a line at 100% lines but <100% branch has an un-taken side. Aim for 100% branch on the diff locally.

**Branch coverage — the rule that fails most PRs.** Each of `if/else`, `? :`, `&&`, `||`, and `??`
is two branches; exercise **both**.
Expand Down
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- [ ] `git diff --check`
- [ ] `npm run actionlint`
- [ ] `npm run typecheck`
- [ ] `npm run test:coverage` locally; **`codecov/patch` requires ≥97% coverage of the lines AND branches you changed** (aim for **98%+** on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
- [ ] `npm run test:coverage` locally; **`codecov/patch` requires ≥99% coverage of the lines AND branches you changed** (aim for **100%** on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
- [ ] `npm run test:workers`
- [ ] `npm run build:mcp`
- [ ] `npm run test:mcp-pack`
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ jobs:
echo "::error title=Tests::The backend test coverage suite failed."
echo "Coverage itself is gated by Codecov on changed lines (codecov/patch), computed from the complete lcov generated by this job."
echo "Reproduce locally with: 'npm run test:coverage'."
- name: Verify coverage report exists
if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') }}
run: |
if [ ! -s coverage/lcov.info ]; then
echo "::error title=Coverage::coverage/lcov.info is missing or empty"
exit 1
fi
- name: Upload coverage to Codecov
if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
Expand All @@ -139,7 +146,12 @@ jobs:
override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}
override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }}
fail_ci_if_error: false
# Coverage upload is part of the hard gate: upload or service errors
# should fail CI instead of allowing a PR to merge without patch data.
fail_ci_if_error: true
# Test results are useful Codecov annotations, not the coverage gate. Keep
# their upload non-blocking so a JUnit ingestion hiccup does not fail CI
# after the tests and hard coverage upload have already passed.
- name: Upload Vitest results to Codecov
if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ process evolves — edits to those files improve both Claude Code and Codex.
(including `codecov/patch`), a gate failure, a base conflict, or an ineligible linked issue is
**auto-CLOSED, one-shot** (recovery = open a fresh PR). Green CI + passing gate + mergeable-clean +
a valid linked issue is **auto-MERGED**. So make it perfect before you push.
2. **97% patch coverage, branch-counted.** Aim for **100% of every changed line *and branch*** — test
2. **99% patch coverage, branch-counted.** Aim for **100% of every changed line *and branch*** — test
both sides of every `??` / ternary / `&&` (a `SUM()` can return `NULL`, so the nullish arm is real),
plus invariant tests and a regression test for every fix. Only `src/**` is measured by Codecov.
3. **The whole local gate must be green:** `npm run test:ci` (+ `npm audit --audit-level=moderate`).
Expand Down
14 changes: 8 additions & 6 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Codecov configuration.
#
# The real coverage gate is `patch`: every line a PR changes must be >=97%
# covered. Patch coverage depends only on the PR's own diff, so merging one PR
# can never push another below the bar -- this is what kills the cross-PR churn
# the old global vitest threshold caused.
# The real coverage gate is `patch`: changed lines and branches in a PR must be
# >=99% covered. Patch coverage depends only on the PR's own diff, so merging
# one PR can never push another below the bar -- this is what kills the cross-PR
# churn the old global vitest threshold caused.
#
# `project` (whole-repo total) is informational only: it is reported as a trend
# but never blocks a merge. vitest keeps a loose 90% local backstop separately.
Expand All @@ -15,10 +15,12 @@ coverage:
status:
patch:
default:
target: 97%
target: 99%
threshold: 0%
# Only fail if the PR actually changes coverable lines.
# Keep the patch status in error if the upstream CI run failed.
if_ci_failed: error
# Suppress codecov/patch on main pushes; uploads still maintain history.
only_pulls: true
project:
default:
informational: true
Expand Down
69 changes: 69 additions & 0 deletions test/unit/codecov-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { readFileSync } from "node:fs";
import { parse } from "yaml";

function readYaml(path: string): Record<string, unknown> {
return record(parse(readFileSync(path, "utf8")), path);
}

function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${label} must be an object`);
}
return value as Record<string, unknown>;
}

function recordArray(value: unknown, label: string): Array<Record<string, unknown>> {
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
return value.map((entry, index) => record(entry, `${label}[${index}]`));
}

function nestedRecord(source: Record<string, unknown>, path: string[]): Record<string, unknown> {
return path.reduce((current, key) => record(current[key], path.join(".")), source);
}

describe("Codecov policy", () => {
it("keeps patch coverage strict and PR-scoped", () => {
const config = readYaml("codecov.yml");
const patch = nestedRecord(config, ["coverage", "status", "patch", "default"]);
const project = nestedRecord(config, ["coverage", "status", "project", "default"]);

expect(patch.target).toBe("99%");
expect(patch.threshold).toBe("0%");
expect(patch.if_ci_failed).toBe("error");
expect(patch.only_pulls).toBe(true);
expect(project.informational).toBe(true);
});

it("fails closed when the backend coverage report is missing or cannot upload", () => {
const workflow = readYaml(".github/workflows/ci.yml");
const validateCode = nestedRecord(workflow, ["jobs", "validate-code"]);
const steps = recordArray(validateCode.steps, "jobs.validate-code.steps");

const stepNames = steps.map((step) => step.name);
const verifyIndex = stepNames.indexOf("Verify coverage report exists");
const coverageUploadIndex = stepNames.indexOf("Upload coverage to Codecov");
const testResultsUploadIndex = stepNames.indexOf("Upload Vitest results to Codecov");

expect(verifyIndex).toBeGreaterThan(-1);
expect(coverageUploadIndex).toBeGreaterThan(verifyIndex);
expect(testResultsUploadIndex).toBeGreaterThan(coverageUploadIndex);

const verifyStep = steps[verifyIndex]!;
const coverageUpload = steps[coverageUploadIndex]!;
const testResultsUpload = steps[testResultsUploadIndex]!;

expect(verifyStep.if).toBe(coverageUpload.if);
expect(String(verifyStep.run)).toContain("coverage/lcov.info is missing or empty");
expect(String(verifyStep.run)).toContain("exit 1");

const coverageUploadWith = record(coverageUpload.with, "coverage upload with");
expect(coverageUploadWith.files).toBe("./coverage/lcov.info");
expect(coverageUploadWith.disable_search).toBe(true);
expect(coverageUploadWith.fail_ci_if_error).toBe(true);

const testResultsUploadWith = record(testResultsUpload.with, "test results upload with");
expect(testResultsUploadWith.report_type).toBe("test_results");
expect(testResultsUploadWith.disable_search).toBe(true);
expect(testResultsUploadWith.fail_ci_if_error).toBe(false);
});
});
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default defineConfig({
exclude: ["src/env.d.ts", "apps/**"],
// Emit lcov for Codecov to compute patch (changed-lines) coverage.
reporter: ["text", "lcov"],
// The 97% requirement now lives in codecov.yml as a *patch* gate (changed
// The 99% requirement now lives in codecov.yml as a *patch* gate (changed
// lines only), which is compositional: merging one PR can't drop another
// below the bar. These global thresholds are only a loose catastrophe net
// (e.g. a deleted test file), set well below actual coverage so routine
Expand Down