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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Entrypoint: `index.html` (+ static pages in `pages/`)
- Source composed of small, focused modules in `src/` (`components/`, `utils/`, ..) with colocated tests
- Frequently during development and before each commit: run `npm test`
- Run `npm outdated` at the start of each significant task and weekly at minimum; keep dependencies healthy. Prefer bumping to the `Wanted` version unless blocked by incompatibilities (document any exceptions). Also run `npm audit` to catch security issues even when versions are current
- `README.md` typically contains big picture dev. spec and context. It should be kept up to date whenever the code is ready for a PR
- Static app => serve `index.html` with simple static server (e.g., VS Code Live Server)
- Only change code directly related to the current task; keep diffs small
Expand Down
288 changes: 288 additions & 0 deletions docs/codeCoverageIgnoreGuidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# Coverage Ignore Directives (Istanbul/nyc) — Policy & Guidelines

This document defines how to use coverage-ignore comments in this repository. It targets both human contributors and code-generation tools (LLMs).

## TL;DR

- `/* istanbul ignore next */` excludes the **next AST node** from coverage.
- Use ignores **sparingly** and **only** for code that is *truly* untestable or irrelevant to product behavior.
- Every ignore **must include a reason** right next to it.
- Prefer tests, refactors, or config-level excludes over in-source ignores.

---

## What do these directives do?

Istanbul (used by nyc/Jest) reads special comments that mark code as not counted in coverage:

- `/* istanbul ignore next */` — exclude the next node (a statement, declaration, or control structure).
- `/* istanbul ignore if */` — exclude only the `if` branch.
- `/* istanbul ignore else */` — exclude only the `else` branch.
- `/* istanbul ignore file */` — exclude the entire file (discouraged for source files; see policy).

> Note: With V8/c8 coverage (e.g., Jest `coverageProvider: "v8"`), the equivalent is `/* c8 ignore next */`. Do **not** mix styles within one project.

---

## When it **is** acceptable

Use an ignore only when exercising the code in automated tests is impractical or meaningless, and only for the smallest possible node.

1. **Unreachable defensive code**
Exhaustive switch fallthroughs, invariant guards, or “should never happen” paths that exist purely as safety nets.
```ts
type Kind = "A" | "B"
function assertNever(x: never): never { throw new Error("unreachable") }

switch (kind) {
case "A": handleA(); break
case "B": handleB(); break
/* istanbul ignore next -- defensive, unreachable by construction */
default: assertNever(kind as never)
}

2. **Platform-/environment-specific branches**
Behavior that cannot be exercised in CI or across all supported OSes without unrealistic setups.

```ts
if (process.platform === "win32") {
/* istanbul ignore next -- requires native Windows console; not in CI image */
enableWindowsConsoleMode()
}
```

3. **Generated or vendored code**
Autogenerated files where coverage is not actionable. Prefer **config excludes** for whole folders rather than sprinkling ignores inline.

4. **Pure logging/telemetry in a dead-fallback**
Logging in a path that is otherwise an assert/throw and providing no distinct functional behavior.

---

## When it is **not** acceptable

* To boost coverage percentages or hide missing tests.
* On **business logic** or any behavior affecting users.
* Broadly before `if`/`switch`/function declarations that mask multiple branches or large regions.
* As a substitute for a **small refactor** that would make testing feasible (e.g., splitting out side effects, injecting dependencies).
* For convenience when a test is mildly inconvenient to write (e.g., mocking a timer or a rejected promise).

---

## Required hygiene

1. **Reason required**
Each ignore must be paired with a short reason:

```ts
/* istanbul ignore next -- OS-specific console mode not available in CI */
```

A matching explanation comment on the same or previous line is mandatory.

2. **Minimize scope**
Target the **smallest possible node** (e.g., the one call or the single `default`), not an entire block.

3. **No `ignore file` in source**
`/* istanbul ignore file */` is only allowed in generated/vendor files, not hand-written source.

4. **Issue linkage (recommended)**
Add a ticket reference where appropriate:

```ts
/* istanbul ignore next -- unreachable by type construction; see ENG-1234 */
```

---

## Preferred alternatives to ignores

* **Write a focused test**: Use dependency injection, seam extraction, or a small adapter to isolate side effects.
* **Refactor for testability**: Split logic from I/O; return values instead of printing; pass a clock/random source.
* **Use config excludes for generated code**: Keep production logic fully measured.
* **Switch directive, not scope**: Prefer `ignore if/else` over `ignore next` when only one branch is untestable.

---

## Project configuration

### Coverage thresholds (per-file)

```json
// package.json
{
"nyc": {
"check-coverage": true,
"per-file": true,
"lines": 90,
"functions": 90,
"branches": 85,
"statements": 90,
"exclude": [
"dist/**",
"build/**",
"**/*.gen.*",
"**/__fixtures__/**",
"**/*.d.ts"
]
}
}
```

Jest example (if using V8 coverage):

```js
// jest.config.js
module.exports = {
collectCoverage: true,
coverageProvider: "v8",
coveragePathIgnorePatterns: [
"/node_modules/",
"/dist/",
"/build/",
"\\.gen\\."
]
}
```

> Align comment style with the active provider: `istanbul` for Babel/nyc instrumentation; `c8` for V8.

---

## CI guardrails

**Block unexplained ignores** (simple Node check). Add a script:

```js
// scripts/check-coverage-ignores.mjs
import { readFileSync } from "node:fs";
import { globby } from "globby";

const files = await globby(["src/**/*.{ts,tsx,js,jsx}"], { gitignore: true });

const offenders = [];
const re = /(istanbul|c8)\s+ignore\s+(next|if|else|file)/;

for (const f of files) {
const lines = readFileSync(f, "utf8").split("\n");
for (let i = 0; i < lines.length; i++) {
if (re.test(lines[i])) {
const hasReason =
/--\s*[A-Za-z0-9]/.test(lines[i]) || (i > 0 && /--\s*[A-Za-z0-9]/.test(lines[i - 1]));
if (!hasReason) offenders.push(`${f}:${i + 1}: missing reason after ignore comment`);
}
}
}

if (offenders.length) {
console.error("Coverage ignore comments require an inline reason (use `-- reason`).");
console.error(offenders.join("\n"));
process.exit(1);
}
```

`package.json`:

```json
{
"scripts": {
"lint:coverage-ignores": "node scripts/check-coverage-ignores.mjs"
}
}
```

Optional ESLint guard (warn on any usage):

```json
// .eslintrc.json
{
"rules": {
"no-restricted-comments": [
"warn",
{
"terms": ["istanbul ignore next", "istanbul ignore if", "istanbul ignore else", "istanbul ignore file", "c8 ignore next"],
"location": "anywhere",
"message": "Coverage ignore detected: add `-- reason` and ensure policy compliance."
}
]
}
}
```

---

## Examples

**Ignore only the `else` branch, not the whole statement:**

```ts
if (cacheEnabled) {
warmCache()
}
/* istanbul ignore else -- cold path is a telemetry-only fallback */
else {
coldStartWithTelemetry()
}
```

**Smallest-node ignore for a single call:**

```ts
// Calls a native API that only exists on macOS ≥ 13:
if (isDarwin13Plus()) {
/* istanbul ignore next -- native API unavailable in CI runners */
enableFancyTerminal()
}
```

**Prefer config exclude for generated code:**

```txt
nyc.exclude += ["src/generated/**"] // in package.json nyc config
```

---

## Migration & auditing

1. **Find all ignores**

```bash
git grep -nE "istanbul ignore|c8 ignore"
```

2. **Classify**

* ✅ Legitimate (add/verify reason, minimize scope)
* 🟡 Replaceable (write a test or refactor)
* 🔴 Remove/ban (business logic, overly broad)

3. **Refactor & test**

* Extract logic from side effects; inject collaborators; mock clocks/randomness.

4. **Guard**

* Add CI script and ESLint rule to prevent regressions.

---

## FAQ

**Q: Does `ignore next` skip execution?**
A: No. Code still executes. It is excluded only from coverage metrics.

**Q: Why not ignore whole files?**
A: It hides real gaps and makes coverage numbers meaningless. Use config excludes for generated/vendor folders instead.

**Q: Babel/nyc vs V8/c8?**
A: Use one approach consistently. If switching to V8 coverage, update directives (`istanbul` → `c8`) and configs together.

---

## Checklist for new code

* [ ] Coverage added for changed behavior.
* [ ] No new `istanbul`/`c8` ignores **unless** justified and minimal.
* [ ] Each ignore has `-- reason` and (optionally) a ticket reference.
* [ ] Generated/vendor code excluded via config, not inline comments.
3 changes: 2 additions & 1 deletion mutation-testing/mutation-report-to-md.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ function calculateOverallStats(report) {
});
});

const mutationScore = totalMutants > 0 ? ((killedMutants / (totalMutants - noCoverageMutants)) * 100) : 0;
const coveredMutants = totalMutants - noCoverageMutants;
const mutationScore = coveredMutants > 0 ? ((killedMutants / coveredMutants) * 100) : 0;
const coverageScore = totalMutants > 0 ? (((totalMutants - noCoverageMutants) / totalMutants) * 100) : 0;

return {
Expand Down
Loading