Skip to content

Include workspace packages in changed-line coverage #1684

Description

@thymikee

Plan 001: Include workspace packages in changed-line coverage

Executor instructions: Follow this plan step by step. Run every
verification command and confirm the expected result before moving to the
next step. If anything in the "STOP conditions" section occurs, stop and
report — do not improvise. A reviewer maintains the plan index for this work;
do not create or edit plans/README.md.

Drift check (run first):
git diff --stat 13bc70f24..HEAD -- scripts/coverage-changed/model.ts scripts/coverage-changed/model.test.ts scripts/coverage-changed/run.test.ts
If any in-scope file changed since this plan was written, compare the
"Current state" excerpts against the live code before proceeding; on a
mismatch, treat it as a STOP condition.

Status

  • Priority: P1
  • Effort: S
  • Risk: LOW
  • Depends on: none
  • Category: tests
  • Planned at: commit 13bc70f24, 2026-08-08

Why this matters

The full Vitest coverage run instruments both root src/ code and workspace
package source, but the changed-line gate rejects every packages/*/src/**
path before consulting LCOV. A pull request can therefore add uncovered lines
to contracts, selectors, kernel, or provider packages while the 70% changed-line
gate reports no package denominator. This plan makes the package source universe
match the coverage configuration and pins the behavior at both the pure model
and executable gate entrypoint.

Current state

  • vitest.config.ts owns the full coverage include universe. It is an
    authoritative reference, but it is out of scope for this fix:

    // vitest.config.ts:119-127
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov', 'json-summary'],
      thresholds: {
        statements: 78,
        lines: 80,
      },
      include: ['src/**/*.ts', 'packages/*/src/**/*.ts'],
  • scripts/coverage-changed/model.ts describes an LCOV-driven universe, but
    its prefilter only recognizes root source:

    // scripts/coverage-changed/model.ts:6-10
    // The coverable universe is the lcov report itself, not a second copy of
    // vitest's exclude globs: coverage runs with `all` on, so every includable
    // `src/**/*.ts` file appears in lcov even when untested. A changed includable
    // file ABSENT from lcov was dropped by an exclude glob — reported (non-gating)
    // so exclusions cannot silently absorb new logic.
    
    // scripts/coverage-changed/model.ts:152-160
    // Vitest coverage `include` is `src/**/*.ts`. Test files are excluded there and
    // carry no product logic, so they never count toward the gate or the excluded
    // tally (which exists to surface hidden logic, not test code).
    export function isTestFile(path: string): boolean {
      return /\.test\.ts$/.test(path) || /(^|\/)__tests__\//.test(path);
    }
    
    export function isIncludableSource(path: string): boolean {
      return /^src\/.*\.ts$/.test(path) && !isTestFile(path);
    }
  • computeChangedCoverage drops a diff before looking it up in LCOV:

    // scripts/coverage-changed/model.ts:289-299
    for (const diff of input.diffs) {
      if (diff.deleted || diff.added.length === 0 || !isIncludableSource(diff.path)) continue;
    
      const cov = input.coverage.get(diff.path);
      if (!cov) {
        // Includable source absent from the all-files lcov report: an exclude glob
        // dropped it. Count its code-like added lines so the exclusion is visible.
  • The classification test claims parity with the coverage glob without a
    package case:

    // scripts/coverage-changed/model.test.ts:86-94
    test('source/test classification matches the coverage include glob', () => {
      assert.equal(isIncludableSource('src/core/x.ts'), true);
      assert.equal(isIncludableSource('src/core/x.test.ts'), false);
      assert.equal(isIncludableSource('src/core/__tests__/x.ts'), false);
      assert.equal(isIncludableSource('scripts/y.ts'), false);
      assert.equal(isIncludableSource('src/core/x.tsx'), false);
      assert.equal(isTestFile('src/a.test.ts'), true);
      assert.equal(isTestFile('src/__tests__/a.ts'), true);
      assert.equal(isTestFile('src/a.ts'), false);
    });
  • scripts/coverage-changed/run.test.ts:75-84 is the entrypoint regression
    pattern to copy: create a temporary git change, provide LCOV, call run, and
    assert a failing code plus the uncovered path/line.

  • Workspace package ownership is already a first-class concept in
    scripts/check-affected/model.ts:229-255; match its path shape
    packages/<package>/src/** rather than introducing another package layout.

  • Tests in this directory use node:test plus node:assert/strict. Keep that
    style; do not convert them to Vitest APIs.

Commands you will need

Purpose Command Expected on success
Focused gate tests pnpm check:coverage-changed:test exit 0; all changed-coverage model and entrypoint tests pass
Static checks pnpm check:quick exit 0; lint and TypeScript report no errors
Affected gates pnpm check:affected --run exit 0; selected local checks pass, with CI-owned checks only reported
Working-tree scope git status --short only the three in-scope files are changed

Dependencies must already be installed. Do not run a different package manager
or create/update package-lock.json.

Scope

In scope (the only files you should modify):

  • scripts/coverage-changed/model.ts
  • scripts/coverage-changed/model.test.ts
  • scripts/coverage-changed/run.test.ts

Out of scope (do NOT touch, even though they look related):

  • vitest.config.ts — its include list is already correct.
  • .github/workflows/ci.yml — CI already invokes the model test, coverage
    producer, and changed-line gate in the right order.
  • scripts/check-affected/** — workspace packages are already classified there.
  • Coverage thresholds, waiver behavior, branch reporting, and output wording.
  • Product source under src/** or packages/**.
  • Documentation and skills — this is an internal gate-correctness fix with no
    user-facing command behavior.
  • plans/README.md and every other plan file.

Git workflow

  • Branch: advisor/001-workspace-package-changed-coverage
  • Use one logical commit after the red/green proof is complete:
    fix(test): include workspace packages in changed-line coverage
  • Do NOT commit the intentionally red intermediate state.
  • Do NOT push or open a pull request unless the operator explicitly instructs it.

Steps

Step 1: Add regressions and prove them red on the current model

In scripts/coverage-changed/model.test.ts:

  1. Extend source/test classification matches the coverage include glob with:
    • packages/contracts/src/x.tstrue;
    • packages/contracts/src/x.test.tsfalse;
    • packages/contracts/src/__tests__/x.tsfalse;
    • a non-source package path such as packages/contracts/test/x.tsfalse.
  2. Add a model-level regression whose LCOV contains
    SF:packages/contracts/src/feature.ts, one covered DA line, and one
    uncovered DA line. Pass a matching package diff to
    computeChangedCoverage and assert:
    • totalLines === 2;
    • coveredLines === 1;
    • passed === false;
    • the offender path is packages/contracts/src/feature.ts and its uncovered
      line is reported.

In scripts/coverage-changed/run.test.ts, copy the structure of
fails when a changed source line is uncovered and names that line at lines
75-84, but write and commit
packages/contracts/src/feature.ts in the temporary repo. Supply matching LCOV
with one uncovered executable line and assert run(['--base', 'main'], repo)
returns 1, prints FAIL, and names the package path and line.

Do not weaken existing root-source, docs-only, exclusion, waiver, or branch
assertions.

Verify red: pnpm check:coverage-changed:test → exit nonzero. The new
classification assertion and/or package-entrypoint regression must fail because
the current model reports the package change as non-includable or 0/0. Record
the failing test names in the implementation handoff. If the new tests pass
before production changes, STOP: the regression is not exercising the reported
gap.

Step 2: Admit workspace package source without admitting package tests

Update isIncludableSource in scripts/coverage-changed/model.ts so it accepts
exactly these TypeScript source roots:

  • src/**/*.ts
  • packages/<one-package-segment>/src/**/*.ts

Continue to exclude .test.ts, every __tests__/ directory, .tsx, scripts,
package-level test/, and unrelated paths. Prefer one named source-root matcher
plus the existing isTestFile predicate; do not copy Vitest's full exclusion
list or add package names one by one.

Update the comments at lines 6-10 and 152-154 so they truthfully name both
coverage roots. Preserve all scoring, waiver, exclusion-tally, branch, and
reporting behavior.

Verify green: pnpm check:coverage-changed:test → exit 0. The new package
model and entrypoint tests pass, and all existing tests remain green.

Step 3: Run the repository gates selected for tooling changes

Run the focused suite once more, then the static and affected gates. Do not
paper over a gate by adding an allowlist or coverage waiver.

Verify:

pnpm check:coverage-changed:test && pnpm check:quick && pnpm check:affected --run

Expected: exit 0. The affected selector may fail open because scripts/** is
workflow/tooling; all checks it elects to run must pass, and CI-only/native
checks may be reported rather than executed.

Step 4: Tighten comments and scope before handoff

Confirm the old root-only coverage statements are gone and no unrelated files
changed.

Verify:

rg -n 'include` is `src/\*\*/\*\.ts|every includable `src/\*\*/\*\.ts' scripts/coverage-changed/model.ts
git status --short

Expected: the rg command prints no stale root-only claim; git status --short
lists only the three in-scope files.

Test plan

  • Extend the existing classification matrix in
    scripts/coverage-changed/model.test.ts with package source and package-test
    cases.
  • Add one pure-model scoring test proving a package source diff contributes to
    the denominator and can fail below 70%.
  • Add one run.test.ts temporary-repository test proving the executable gate
    returns failure and renders the uncovered package path/line.
  • Keep the existing root-source regression as a control.
  • Prove the new tests red before changing model.ts; quote the failing test
    names in the handoff.
  • Verification: pnpm check:coverage-changed:test → all tests pass after the
    fix.

Done criteria

  • The planned-at excerpts still matched before editing.
  • The new package regressions failed on the pre-fix model and that failure
    was recorded.
  • isIncludableSource('packages/contracts/src/x.ts') is true.
  • Package .test.ts, __tests__, .tsx, and package-level test/ paths
    remain excluded.
  • An uncovered package source line makes both the pure model and run
    entrypoint report failure.
  • Root src/ scoring, waivers, excluded-line reporting, and branch reporting
    are unchanged.
  • pnpm check:coverage-changed:test exits 0.
  • pnpm check:quick exits 0.
  • pnpm check:affected --run exits 0 for local gates.
  • git status --short lists only the three in-scope files.
  • No docs or skills were changed because the public command surface and
    workflow guidance did not change.

STOP conditions

Stop and report back; do not improvise if:

  • Any in-scope current-state excerpt has drifted since 13bc70f24.
  • vitest.config.ts no longer includes packages/*/src/**/*.ts.
  • The coverage producer does not emit normalized
    packages/<name>/src/<file>.ts LCOV paths; report the observed shape instead
    of changing normalization speculatively.
  • The red test passes before model.ts changes, or fails for missing fixtures
    rather than the root-only classifier.
  • Supporting package coverage requires changing thresholds, CI workflow order,
    waiver semantics, or Vitest configuration.
  • A verification command fails twice after one reasonable correction.
  • Any fix requires touching a file outside the in-scope list.

Maintenance notes

  • Reviewers should check that the source-root predicate is package-generic and
    still excludes tests; an enumerated package list will drift as workspaces are
    added.
  • Future coverage-root changes must update the classifier matrix and executable
    package-style regression in the same change.
  • The full global thresholds remain GitHub CI's authority. This plan only closes
    the changed-line prefilter gap and must not change the 70% threshold or waiver
    policy.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions