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:
- Extend
source/test classification matches the coverage include glob with:
packages/contracts/src/x.ts → true;
packages/contracts/src/x.test.ts → false;
packages/contracts/src/__tests__/x.ts → false;
- a non-source package path such as
packages/contracts/test/x.ts → false.
- 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
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.
Plan 001: Include workspace packages in changed-line coverage
Status
13bc70f24, 2026-08-08Why this matters
The full Vitest coverage run instruments both root
src/code and workspacepackage 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.tsowns the full coverage include universe. It is anauthoritative reference, but it is out of scope for this fix:
scripts/coverage-changed/model.tsdescribes an LCOV-driven universe, butits prefilter only recognizes root source:
computeChangedCoveragedrops a diff before looking it up in LCOV:The classification test claims parity with the coverage glob without a
package case:
scripts/coverage-changed/run.test.ts:75-84is the entrypoint regressionpattern to copy: create a temporary git change, provide LCOV, call
run, andassert 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 shapepackages/<package>/src/**rather than introducing another package layout.Tests in this directory use
node:testplusnode:assert/strict. Keep thatstyle; do not convert them to Vitest APIs.
Commands you will need
pnpm check:coverage-changed:testpnpm check:quickpnpm check:affected --rungit status --shortDependencies 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.tsscripts/coverage-changed/model.test.tsscripts/coverage-changed/run.test.tsOut 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, coverageproducer, and changed-line gate in the right order.
scripts/check-affected/**— workspace packages are already classified there.src/**orpackages/**.user-facing command behavior.
plans/README.mdand every other plan file.Git workflow
advisor/001-workspace-package-changed-coveragefix(test): include workspace packages in changed-line coverageSteps
Step 1: Add regressions and prove them red on the current model
In
scripts/coverage-changed/model.test.ts:source/test classification matches the coverage include globwith:packages/contracts/src/x.ts→true;packages/contracts/src/x.test.ts→false;packages/contracts/src/__tests__/x.ts→false;packages/contracts/test/x.ts→false.SF:packages/contracts/src/feature.ts, one coveredDAline, and oneuncovered
DAline. Pass a matching package diff tocomputeChangedCoverageand assert:totalLines === 2;coveredLines === 1;passed === false;packages/contracts/src/feature.tsand its uncoveredline is reported.
In
scripts/coverage-changed/run.test.ts, copy the structure offails when a changed source line is uncovered and names that lineat lines75-84, but write and commit
packages/contracts/src/feature.tsin the temporary repo. Supply matching LCOVwith one uncovered executable line and assert
run(['--base', 'main'], repo)returns
1, printsFAIL, 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 newclassification assertion and/or package-entrypoint regression must fail because
the current model reports the package change as non-includable or
0/0. Recordthe 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
isIncludableSourceinscripts/coverage-changed/model.tsso it acceptsexactly these TypeScript source roots:
src/**/*.tspackages/<one-package-segment>/src/**/*.tsContinue to exclude
.test.ts, every__tests__/directory,.tsx, scripts,package-level
test/, and unrelated paths. Prefer one named source-root matcherplus the existing
isTestFilepredicate; do not copy Vitest's full exclusionlist 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 packagemodel 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:
Expected: exit 0. The affected selector may fail open because
scripts/**isworkflow/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 --shortExpected: the
rgcommand prints no stale root-only claim;git status --shortlists only the three in-scope files.
Test plan
scripts/coverage-changed/model.test.tswith package source and package-testcases.
the denominator and can fail below 70%.
run.test.tstemporary-repository test proving the executable gatereturns failure and renders the uncovered package path/line.
model.ts; quote the failing testnames in the handoff.
pnpm check:coverage-changed:test→ all tests pass after thefix.
Done criteria
was recorded.
isIncludableSource('packages/contracts/src/x.ts')is true..test.ts,__tests__,.tsx, and package-leveltest/pathsremain excluded.
runentrypoint report failure.
src/scoring, waivers, excluded-line reporting, and branch reportingare unchanged.
pnpm check:coverage-changed:testexits 0.pnpm check:quickexits 0.pnpm check:affected --runexits 0 for local gates.git status --shortlists only the three in-scope files.workflow guidance did not change.
STOP conditions
Stop and report back; do not improvise if:
13bc70f24.vitest.config.tsno longer includespackages/*/src/**/*.ts.packages/<name>/src/<file>.tsLCOV paths; report the observed shape insteadof changing normalization speculatively.
model.tschanges, or fails for missing fixturesrather than the root-only classifier.
waiver semantics, or Vitest configuration.
Maintenance notes
still excludes tests; an enumerated package list will drift as workspaces are
added.
package-style regression in the same change.
the changed-line prefilter gap and must not change the 70% threshold or waiver
policy.