You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[quality] docs/contributing.md phantom-branch section still lists close.ts:37 and reopen.ts:34 (and 1815/1819), which #642 removed #679
docs/contributing.md ("Phantom "uncovered" branches after an await", lines 93-96) says the unit run lists four branches as uncovered — close.ts:37, reopen.ts:34, approve.ts:516 and auth.ts:237 — reporting 1815/1819 branches, with the e2e run repeating "the last two".
That was true when #641 (81da9cb) wrote it, but #642 (9c45581, merged the same day) inverted the guards in src/issueComment/close.ts and src/issueComment/reopen.ts from if (isAuthUser) { …awaited work… } into an early-throw if (!isAuthUser) { throw … }. With a short then-block the implicit-else count the v8 provider derives no longer goes negative there, so the artefact is gone from those two sites.
Evidence from the latest main build (run 42151611102, main@9aa12b8):
unit artifact coverage-9aa12b8… lcov.info: BRF 1913 / BRH 1911; the only negative records are approve.tsBRDA:516,83,1,-7 and auth.tsBRDA:237,9,1,-85. close.tsBRDA:37,3,0,5 / BRDA:37,3,1,4 and reopen.tsBRDA:34,3,0,4 / BRDA:34,3,1,4 are both positive.
e2e artifact coverage-e2e-9aa12b8…: approve.ts:516 (-37) and auth.ts:237 (-44) — so the e2e run repeats both remaining sites, not "the last two" of four.
The awk one-liner in the same section already finds the right answer; only the prose and the 1815/1819 figure are stale. A contributor following the doc would look for two phantom arms that no longer exist and might take a genuine new miss on close.ts/reopen.ts for a known artefact.
Recommendation
Docs-only change to docs/contributing.md: say two branches (approve.ts:516, auth.ts:237), 1911/1913, and that the e2e run repeats both; mention that close.ts:37/reopen.ts:34 showed the same artefact until #642 inverted their guards, so the count is a property of the current code and test mix rather than a fixed list. No test, ignore marker or threshold change — the section's own policy forbids those.
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 6 closed as completed in the last 14 days) already cite docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.
[quality] coverageIgnoreHints.test.ts fails inside a Stryker sandbox whenever the probed file is imported by tests/setup.ts
Finding
npm run test:mutation -- --mutate src/adapters/actions.ts (or any glob that includes src/adapters/actions.ts or src/platform/index.ts) stops at the dry run on main9aa12b8:
ERROR DryRunExecutor One or more tests failed in the initial test run:
v8 ignore hints in src/ > arm-level hints (ignore next) > src/labels/lgtm.ts:67 names the unit test that drives the ignored arm
-- bindLgtm rethrows every failure as an Error; the String(e) arm is unreachable
— no __tests__/**/<name>.test.ts named, or the named file does not exist: expected undefined to be defined
ERROR Stryker There were failed tests in the initial test run.
Two things combine:
Stryker's sandbox prepends // @ts-nocheck to every file it copies (disableTypeChecks, default on), so inside .stryker-tmp/sandbox-*/ the hint at src/labels/lgtm.ts:66 is on line 67, and the driversOutsideTheHint key 'src/labels/lgtm.ts:66' in __tests__/coverageIgnoreHints.test.ts no longer matches. Two cases fail (names the unit test that drives the ignored arm and records out-of-hint drivers only for hints that still exist).
Stryker's vitest runner runs only test files related to the mutated files (vitest.related). __tests__/setup.ts imports ../src/adapters/actions and ../src/platform, so mutating either makes every test file related, including this one. The nightly set (stryker.nightly.config.mjs: mergeGate, labelMatch, hold, duration, tide, mergeQueue) never reaches it, which is why the scheduled mutation job is green (run 42454617747, 1827 tests in the dry run) and why npx stryker run stryker.nightly.config.mjs --dryRunOnly passes locally while the same sandbox copy fails under plain vitest run __tests__/coverageIgnoreHints.test.ts.
Reproduced at 9aa12b8 under Node 24.22.2 and 26.12.0. docs/contributing.md ("Mutation checks") offers --mutate <glob> to probe any other file; for the two modules above, and for src/server/* passes that include src/platform/index.ts, that path is currently broken.
Recommendation
Make the guard number hints as the tree does regardless of which copy of the file it reads: strip a leading // @ts-nocheck line in coverageIgnoreHints.test.ts before counting lines (and in exportedNames, which slices the same text by those numbers), with a unit case that feeds a prefixed copy through the same path. Test-only; no src/, dist/ or config change.
Decision taken by hive: normalise the text rather than re-key driversOutsideTheHint by reason, so the existing file:line keys and their "drop or move the entry" check keep working unchanged.
Priority
Impact: medium (the documented ad-hoc mutation probe fails on two of the most-imported modules; CI unaffected)
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 6 closed as completed in the last 14 days) already cite docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.
[quality] Gate proposal: add src/plugins/approve.ts to the nightly Stryker set
Finding
src/plugins/approve.ts decides the approved half of the merge decision (which approvers cover which changed files, when the label is added or removed, and the mirrored approve.github_review), yet it is outside both Stryker sets: the local default (stryker.config.mjs) and the scheduled mutation job (stryker.nightly.config.mjs), which today adds only src/plugins/tide.ts and src/utils/mergeQueue.ts. A behavioural regression in approve.ts that line coverage still reaches would therefore not fail anything, while the same regression in tide.ts fails the nightly run.
This is a gate-scope finding rather than a coverage-gap finding: the repository already has the gate (mutation testing in CI) and a written policy for survivors (docs/contributing.md, "Mutation checks"), so no per-survivor issue is filed. A full read of the file's survivors is attached below so that adding it is mechanical.
Evidence
Unit coverage (npm run test:coverage, coverage/lcov.info, 9aa12b8, 2026-11-08): approve.ts is fully line-covered, as the 99 % floor requires.
End-to-end coverage (npm run test:coverage:e2e, coverage-e2e/lcov.info, 9aa12b8, 2026-11-08): the bundle harness drives approve.ts as well; these runs are reported separately by design and are not combined (see docs/contributing.md).
Mutation run of the file, local, npx stryker run --mutate src/plugins/approve.ts through stryker.config.mjs at 9aa12b8 (2026-11-08): 503 killed, 33 survived, 0 no coverage (93.84 %). A second targeted run of lines 338-347, 395-420 and 457-492 with --force (no incremental reuse) reproduced the 11 survivors of those ranges exactly.
Nightly baseline: scheduled run 42454617747 at 9aa12b8, mutation job 9 min 15 s, artifact mutation-9aa12b8…: 762 killed / 31 survived over the six files (96.09 %). With approve.ts added at 503/33 the set scores 95.18 %, above the current break: 95 by about two survivors.
Reviewed survivors of approve.ts (all 33)
Each was read against the rule in docs/contributing.md: a survivor is a finding only when it changes an observable effect. None of these does.
Mutant
Why it is left
approve.ts:99, :100 (×2), :116 — comment.body ?? '', body === '', (review.body ?? '') → a placeholder string
A body of "Stryker was here!" carries no command and no reviewMarker, so the comment or review yields no event either way
With require_self_approval off the author is already in approving, so any file they could be suggested for is already covered; '' is never an approver login
Files that share a deepest OWNERS file share its approver set, so the merged, overwritten or first-kept list is the same already-sorted list
:378, :379[] → placeholder arrays
The placeholder reviews are only reached with ignore_review_state on and no identity, where nothing reads them, or by approvalEvents, which skips a review without a user
:383 (×3), :384 info text; :491 debug text
Log output is not asserted
:398 (×2) present.length > 0 → true, >= 0
removeLabels with an empty list issues no request (the hold.ts:70 case)
:414body.trim() → body
The rendered notifier starts with [APPROVALNOTIFIER] and ends with the marker; it has no surrounding whitespace
:460, :486action === undefined → false
Set.has(undefined) is false, so the skip is taken anyway (the tide.ts:445 case)
:464, :490?? '' → placeholder
The placeholder is neither approved nor contains reviewMarker
:514pull_request?.base → pull_request.base
pullNumber is read from pull_request first and the missing-pull-request throw precedes this line
Recommendation
Add src/plugins/approve.ts to the nightly set and record the table above in docs/contributing.md next to the tide.ts / mergeQueue.ts table. No workflow change is needed: at ~9 min today, the job stays far inside its 60-minute timeout with ~536 more mutants (~6-7 min on the GitHub runner, by the tide.ts rate).
and the doc comment's "tide.ts decides and performs the merge and mergeQueue.ts enqueues it" sentence gains "approve.ts decides the approved label they read".
Decision taken by hive:break goes to 94, following the file's own rule ("just under what main reaches"): the set would score 95.18 %, and keeping 95 leaves a margin of two survivors, which a single new equivalent survivor plus a Timeout/Survived swing could consume and fail a scheduled run on a non-regression. If the maintainers prefer to keep 95, the only cost is that margin.
Accept or decline adding approve.ts to the nightly Stryker set (this is a change to the scheduled gate's scope and runtime, so it is parked for a maintainer's call rather than opened as a PR)
Once accepted, a PR can land the config line, the threshold and the docs table together; nothing in it touches .github/workflows/.
Priority
Impact: medium (the approval half of the merge decision is unguarded against behavioural regressions line coverage still reaches)
Effort: low (one config line, one threshold, one docs table)
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 5 closed as completed in the last 14 days) already cite docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.
[quality] Mutation gate does not cover src/utils/auth.ts; its two memo keys are never asserted
Finding
src/utils/auth.ts decides who may command the bot (org membership, collaborator, OWNERS, authorization.* policies). It is 100 % line- and function-covered by both the unit run and the bundle run, but it is outside both Stryker sets (stryker.config.mjs and stryker.nightly.config.mjs cover mergeGate, labelMatch, hold, duration, tide, mergeQueue), so the nightly mutation job never looks at it.
Running it by hand at 9aa12b8 (npx stryker run stryker.config.mjs --mutate src/utils/auth.ts): 284 mutants, 267 killed, 9 timed out, 8 survived (97.18 %). Two of the survivors change observable behaviour:
checkIssueComments for a second issue in the same run answers from the first issue's commenters
auth.ts:457 memo key `${owner}/${repo}` → ` `
retrieveOwnersFile for a second repository in the same run returns the first repository's root OWNERS
No test pins either key: every existing memoization test (reads the root OWNERS file once per run, reads the comment thread once) uses one issue in one repository, so a collision is indistinguishable from the cache hit it asserts.
The other six are equivalent (platform().log.debug text at :464, :475, :489; the contents === '' guard of rootOwnersIncludes at :439, which only saves a parse of an empty file).
Evidence
Unit: npm run test:coverage at 9aa12b8 — auth.ts 100 / 98.71 / 100 / 100, the single branch miss being the documented phantom at :237.
Mutation: the local run above; reports/mutation/report.json is reproducible with the command given.
Recommendation
Pin both memo keys in __tests__/utils/auth.test.ts: two issues in one run get separate comment reads and separate answers; two repositories in one run get separate OWNERS reads and separate answers.
Add src/utils/auth.ts to the mutate set of stryker.nightly.config.mjs so the gate that already exists covers the authorization code, and record the six equivalent survivors in the docs/contributing.md survivor tables as the other files do. At 97–99 % the file keeps the nightly total above the break: 95 floor.
Decision taken by hive: extend the existing nightly set rather than the local default, which the docs deliberately keep small (~150 mutants, ~12 minutes); auth.ts adds ~280 mutants, the same order as tide.ts.
Priority
Impact: medium (unit-covered; the collision needs a run that spans issues or repositories, which the Actions adapter does not and the App adapter isolates per event — but the key is the only thing preventing it)
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 5 closed as completed in the last 14 days) already cite docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.
[quality] Gate proposal: exact (100 %) line floor for the bundle coverage run once its four structural misses carry v8-ignore hints
Finding
The bundle coverage run (npm run test:coverage:e2e, job e2e-coverage of test.yml) gates at 99 % lines / statements / functions / branches. vitest.e2e-coverage.config.mjs says so itself: "A single lost branch is ~0.06 pp and will not cross the floor on its own; the floor bounds drift, it does not detect one-branch regressions." Every miss that run still reports on main is structural, so the floor could be exact for lines instead of approximate.
Evidence — CI artifact coverage-e2e-9aa12b8… of run 42768459328 (main@9aa12b8), reproduced locally with the same command at the same revision (2613/2617 lines, 1769/1779 branches, 587/591 functions):
Miss
Why the bundle cannot reach it
Unit evidence
src/platform/index.ts:61 — the "no platform installed" throw in platform()
src/main.ts installs the platform before any dispatch
__tests__/platform/platformGuards.test.ts
src/plugins/requireMatchingLabel.ts:126 — resumeRequiredLabels body
only called from src/deferred.ts, the App/Worker resume path (already excluded from this run)
__tests__/plugins/requireMatchingLabel.test.ts
src/plugins/requireMatchingLabel.ts:163-164 — the === 'deferred' arm
actionsPlatform().defer always returns 'continue'; only the App adapter defers
documented phantom await branches (docs/contributing.md); must not be hinted
—
actionsContext, sleep, platform reported FNDA:0 while their lines are hit
source-map/remap artefact of the function record, not a behaviour miss
—
This is the sixth-plus instance of the same class — a bundle-unreachable arm lowers the e2e figure and is then hinted one PR at a time: #483, #537, #544, #549, #587 (merged), #693 (held). Per hive policy, three of a class means the gate is missing, so this is one proposal instead of another instance.
Recommendation
Tool: the thresholds that already exist in vitest.e2e-coverage.config.mjs, run by the existing e2e-coverage job — no workflow change.
Add /* v8 ignore next -- … unreachable from dist/index.js; <test>.test.ts drives it */ hints to platform/index.ts:61 and requireMatchingLabel.ts:125-126 and :163-164, in the form __tests__/coverageIgnoreHints.test.ts already enforces (reason + the unit test that drives the arm). Three hints, one PR.
Once those land (and chore(coverage): v8-ignore sweep.ts's unreachable String(e) arm in the per-pull-request setup catch #693), raise thresholds.lines in vitest.e2e-coverage.config.mjs from 99 to 100 and update the comment. Statements follow the same lines; leave branches at 99 because the two phantom branches cannot be hinted (the docs forbid it), and leave functions at 99 until the three FNDA:0 records are understood.
Update the npm run test:coverage:e2e row in docs/contributing.md ("Fails below 99 % lines …").
Decision requested from the maintainers: accept an exact line floor for the bundle run (every new bundle-unreachable line must then be hinted or driven in the same PR that adds it), or decline and keep the drift-bounding 99. Decision taken by hive: propose lines only; branches and functions stay at 99 for the reasons above.
Risk: governance — this changes what every future PR must satisfy, not just a number.
Priority
Impact: medium (the gate's blind spot is self-documented; nothing is broken today)
Effort: low (three comment lines, one threshold, one doc row)
[quality] Gate-scope addendum: src/utils/auth.ts belongs in the same nightly Stryker set change as approve.ts
Same deliverable as the folded "add src/plugins/approve.ts to the nightly Stryker set" proposal above — one line in stryker.nightly.config.mjs — so this is a comment, not a second issue.
Why auth.ts. It decides who may run /lgtm, /approve, /hold, /close and the label commands (policyAllows, assertPolicy, closePolicyAllows, assertAuthorizedByOwnersOrMembership), and #606/#607/#639/#641/#642 rewrote it (+129 lines net of deletions) after its last mutation pass. Like approve.ts it sits in neither Stryker set, so a behavioural regression that line coverage still reaches fails nothing.
Evidence (main@9aa12b8). Local npx stryker run over src/utils/{auth,config,owners}.ts (perTest, concurrency 12, vitest.mutation.config.mjs minus the bundle harness and the src/-text-scanning tests; dry run 1656 tests in 2m39s):
file
mutants
killed
timeout
survived
score
auth.ts
267
251
8
8
97.18 %
config.ts
774
760
12
2
99.75 %
owners.ts
327
314
12
1
99.71 %
49m20s for all three. Every survivor, read at its exact column span, is equivalent or text-only:
auth.ts:146 (issueCommenters memo key → '') and :457 (rootOwners memo key → ''): RunState is built per delivery (appPlatform, src/adapters/app/platform.ts:92), and a delivery — cron included — carries one repository and one issue, so a collapsed key can never serve another issue's or repository's answer. The per-login authChecks keys (member:, collaborator:) were killed.
auth.ts:439 (contents === '' guard in rootOwnersIncludes): parseOwners('OWNERS', '') yields empty sets, so the guard is a short-circuit only — the same reading as the :422 survivor triaged earlier.
config.ts:312 (kind: 'repo' → ''): the discriminant is only ever compared with === 'url'.
config.ts:799 (toLowerCase → toUpperCase): a dedup Set key applied consistently to both sides; the stored logins are untouched.
So adding auth.ts to stryker.nightly.config.mjs would hold the job's break: 95 today (the set's combined score stays above it), at roughly +270 mutants per nightly run. config.ts and owners.ts are not proposed: they are parsing/data code, not the decision, and at 774 + 327 mutants they would more than double the job for no decision-side gain.
Unit/e2e line coverage for context (run 42573820881, artifacts coverage-9aa12b8… and coverage-e2e-9aa12b8…): all three files 100 % lines in both runs; auth.ts:237 is the documented await artefact. No per-survivor issue or PR is filed — nothing behavioural to pin.
Decision taken by hive: fold auth.ts into the existing approve.ts nightly-set proposal rather than open a parallel one, because both are the same one-line edit to the same file.
[quality] Reconciliation: the auth.ts memo-key survivors are equivalent today; nothing new to land
The folded finding above ("Mutation gate does not cover src/utils/auth.ts; its two memo keys are never asserted", 2026-11-10) was filed before reading the gate-scope addendum of 2026-11-09 on this issue. Both runs saw the same eight survivors at 9aa12b8 (97.18 %). The addendum's reading stands: RunState is built per delivery and a delivery carries one issue and one repository, so collapsing the auth.ts:146 / :457 memo keys cannot serve another target's answer today. The later finding's "behavioural" label is withdrawn; no test PR is opened for it, and the nightly-set change remains parked on the approve.ts proposal's accept/decline box above.
For the record, two tests that would kill those mutants were written and confirmed to fail against them (two issues in one run read separately; two repositories in one run read separately, with the org-membership answer shared per org). They are worth adding only if the sweep or another multi-target delivery ever starts calling checkIssueComments or retrieveOwnersFile across targets — that is the condition under which the keys become observable.
[quality] Gate proposal (folded here by the hive, component docs/contributing.md): exact 100 % line floor for the bundle coverage run once its structural misses carry v8-ignore hints
Filed 2026-11-11 as a separate needs-decision issue; the hive consolidated it into this one because the last deliverable edits the same docs/contributing.md row. It is otherwise independent of the phantom-branch doc fix held in #680.
Finding
The bundle coverage run (npm run test:coverage:e2e, job e2e-coverage of test.yml) gates at 99 % lines / statements / functions / branches. vitest.e2e-coverage.config.mjs says so itself: "A single lost branch is ~0.06 pp and will not cross the floor on its own; the floor bounds drift, it does not detect one-branch regressions." Every miss that run still reports on main is structural, so the floor could be exact for lines instead of approximate.
Evidence — CI artifact coverage-e2e-9aa12b8… of run 42768459328 (main@9aa12b8), reproduced locally with the same command at the same revision (2613/2617 lines, 1769/1779 branches, 587/591 functions):
Miss
Why the bundle cannot reach it
Unit evidence
src/platform/index.ts:61 — the "no platform installed" throw in platform()
src/main.ts installs the platform before any dispatch
__tests__/platform/platformGuards.test.ts
src/plugins/requireMatchingLabel.ts:126 — resumeRequiredLabels body
only called from src/deferred.ts, the App/Worker resume path (already excluded from this run)
__tests__/plugins/requireMatchingLabel.test.ts
src/plugins/requireMatchingLabel.ts:163-164 — the === 'deferred' arm
actionsPlatform().defer always returns 'continue'; only the App adapter defers
documented phantom await branches (docs/contributing.md); must not be hinted
—
actionsContext, sleep, platform reported FNDA:0 while their lines are hit
source-map/remap artefact of the function record, not a behaviour miss
—
This is the sixth-plus instance of the same class — a bundle-unreachable arm lowers the e2e figure and is then hinted one PR at a time: #483, #537, #544, #549, #587 (merged), #693 (held). Per hive policy, three of a class means the gate is missing, so this is one proposal instead of another instance.
Recommendation
Tool: the thresholds that already exist in vitest.e2e-coverage.config.mjs, run by the existing e2e-coverage job — no workflow change.
Add /* v8 ignore next -- … unreachable from dist/index.js; <test>.test.ts drives it */ hints to platform/index.ts:61 and requireMatchingLabel.ts:125-126 and :163-164, in the form __tests__/coverageIgnoreHints.test.ts already enforces (reason + the unit test that drives the arm). Three hints, one PR.
Once those land (and chore(coverage): v8-ignore sweep.ts's unreachable String(e) arm in the per-pull-request setup catch #693), raise thresholds.lines in vitest.e2e-coverage.config.mjs from 99 to 100 and update the comment. Statements follow the same lines; leave branches at 99 because the two phantom branches cannot be hinted (the docs forbid it), and leave functions at 99 until the three FNDA:0 records are understood.
Update the npm run test:coverage:e2e row in docs/contributing.md ("Fails below 99 % lines …").
Decision requested from the maintainers: accept an exact line floor for the bundle run (every new bundle-unreachable line must then be hinted or driven in the same PR that adds it), or decline and keep the drift-bounding 99. Decision taken by hive: propose lines only; branches and functions stay at 99 for the reasons above.
Risk: governance — this changes what every future PR must satisfy, not just a number.
Priority
Impact: medium (the gate's blind spot is self-documented; nothing is broken today)
Effort: low (three comment lines, one threshold, one doc row)
Activity
hivecommons-hive commentedon Nov 8, 2026
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 6 closed as completed in the last 14 days) already cite
docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.[quality] coverageIgnoreHints.test.ts fails inside a Stryker sandbox whenever the probed file is imported by tests/setup.ts
Finding
npm run test:mutation -- --mutate src/adapters/actions.ts(or any glob that includessrc/adapters/actions.tsorsrc/platform/index.ts) stops at the dry run onmain9aa12b8:Two things combine:
// @ts-nocheckto every file it copies (disableTypeChecks, default on), so inside.stryker-tmp/sandbox-*/the hint atsrc/labels/lgtm.ts:66is on line 67, and thedriversOutsideTheHintkey'src/labels/lgtm.ts:66'in__tests__/coverageIgnoreHints.test.tsno longer matches. Two cases fail (names the unit test that drives the ignored armandrecords out-of-hint drivers only for hints that still exist).vitest.related).__tests__/setup.tsimports../src/adapters/actionsand../src/platform, so mutating either makes every test file related, including this one. The nightly set (stryker.nightly.config.mjs: mergeGate, labelMatch, hold, duration, tide, mergeQueue) never reaches it, which is why the scheduledmutationjob is green (run 42454617747, 1827 tests in the dry run) and whynpx stryker run stryker.nightly.config.mjs --dryRunOnlypasses locally while the same sandbox copy fails under plainvitest run __tests__/coverageIgnoreHints.test.ts.Reproduced at 9aa12b8 under Node 24.22.2 and 26.12.0.
docs/contributing.md("Mutation checks") offers--mutate <glob>to probe any other file; for the two modules above, and forsrc/server/*passes that includesrc/platform/index.ts, that path is currently broken.Recommendation
Make the guard number hints as the tree does regardless of which copy of the file it reads: strip a leading
// @ts-nocheckline incoverageIgnoreHints.test.tsbefore counting lines (and inexportedNames, which slices the same text by those numbers), with a unit case that feeds a prefixed copy through the same path. Test-only; nosrc/,dist/or config change.Decision taken by hive: normalise the text rather than re-key
driversOutsideTheHintby reason, so the existingfile:linekeys and their "drop or move the entry" check keep working unchanged.Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:8817bbf— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 6 closed as completed in the last 14 days) already cite
docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.[quality] Gate proposal: add src/plugins/approve.ts to the nightly Stryker set
Finding
src/plugins/approve.tsdecides theapprovedhalf of the merge decision (which approvers cover which changed files, when the label is added or removed, and the mirroredapprove.github_review), yet it is outside both Stryker sets: the local default (stryker.config.mjs) and the scheduledmutationjob (stryker.nightly.config.mjs), which today adds onlysrc/plugins/tide.tsandsrc/utils/mergeQueue.ts. A behavioural regression inapprove.tsthat line coverage still reaches would therefore not fail anything, while the same regression intide.tsfails the nightly run.This is a gate-scope finding rather than a coverage-gap finding: the repository already has the gate (mutation testing in CI) and a written policy for survivors (
docs/contributing.md, "Mutation checks"), so no per-survivor issue is filed. A full read of the file's survivors is attached below so that adding it is mechanical.Evidence
npm run test:coverage,coverage/lcov.info, 9aa12b8, 2026-11-08):approve.tsis fully line-covered, as the 99 % floor requires.npm run test:coverage:e2e,coverage-e2e/lcov.info, 9aa12b8, 2026-11-08): the bundle harness drivesapprove.tsas well; these runs are reported separately by design and are not combined (seedocs/contributing.md).npx stryker run --mutate src/plugins/approve.tsthroughstryker.config.mjsat 9aa12b8 (2026-11-08): 503 killed, 33 survived, 0 no coverage (93.84 %). A second targeted run of lines 338-347, 395-420 and 457-492 with--force(no incremental reuse) reproduced the 11 survivors of those ranges exactly.mutationjob 9 min 15 s, artifactmutation-9aa12b8…: 762 killed / 31 survived over the six files (96.09 %). Withapprove.tsadded at 503/33 the set scores 95.18 %, above the currentbreak: 95by about two survivors.Reviewed survivors of
approve.ts(all 33)Each was read against the rule in
docs/contributing.md: a survivor is a finding only when it changes an observable effect. None of these does.approve.ts:99,:100(×2),:116—comment.body ?? '',body === '',(review.body ?? '')→ a placeholder string"Stryker was here!"carries no command and noreviewMarker, so the comment or review yields no event either way:138'add'→'';:230,:232'ignore'→'';:179default:commandKind's non-cancel result is only ever compared to'cancel';effectiveKind's non-add/remove result falls into the switch'sdefaulteither way:206(×4)settings.require_self_approval && author !== ''→true,||, placeholderrequire_self_approvaloff the author is already inapproving, so any file they could be suggested for is already covered;''is never an approver login:241remaining.size > 0→>= 0;:256excluded.add(best)removedremainingyields an empty coverage map andbreaks; every file the chosen approver covers leavesremaining, so they never score again:335approvers: []→ placeholder for a file without an OWNERS pathrenderNotifierprints onlyentry.filefor such an entry:341entry === undefined→true;:344else {};:345.sort()dropped:378,:379[]→ placeholder arraysignore_review_stateon and no identity, where nothing reads them, or byapprovalEvents, which skips a review without auser:383(×3),:384info text;:491debug text:398(×2)present.length > 0→true,>= 0removeLabelswith an empty list issues no request (thehold.ts:70case):414body.trim()→body[APPROVALNOTIFIER]and ends with the marker; it has no surrounding whitespace:460,:486action === undefined→falseSet.has(undefined)is false, so the skip is taken anyway (thetide.ts:445case):464,:490?? ''→ placeholderapprovednor containsreviewMarker:514pull_request?.base→pull_request.basepullNumberis read frompull_requestfirst and the missing-pull-request throw precedes this lineRecommendation
Add
src/plugins/approve.tsto the nightly set and record the table above indocs/contributing.mdnext to thetide.ts/mergeQueue.tstable. No workflow change is needed: at ~9 min today, the job stays far inside its 60-minute timeout with ~536 more mutants (~6-7 min on the GitHub runner, by thetide.tsrate).stryker.nightly.config.mjs, exact replacement:and the doc comment's "tide.ts decides and performs the merge and mergeQueue.ts enqueues it" sentence gains "approve.ts decides the
approvedlabel they read".Decision taken by hive:
breakgoes to 94, following the file's own rule ("just under whatmainreaches"): the set would score 95.18 %, and keeping 95 leaves a margin of two survivors, which a single new equivalent survivor plus a Timeout/Survived swing could consume and fail a scheduled run on a non-regression. If the maintainers prefer to keep 95, the only cost is that margin.approve.tsto the nightly Stryker set (this is a change to the scheduled gate's scope and runtime, so it is parked for a maintainer's call rather than opened as a PR)Once accepted, a PR can land the config line, the threshold and the docs table together; nothing in it touches
.github/workflows/.Priority
Risk: governance
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9aa12b8— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 5 closed as completed in the last 14 days) already cite
docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.[quality] Mutation gate does not cover src/utils/auth.ts; its two memo keys are never asserted
Finding
src/utils/auth.tsdecides who may command the bot (org membership, collaborator, OWNERS,authorization.*policies). It is 100 % line- and function-covered by both the unit run and the bundle run, but it is outside both Stryker sets (stryker.config.mjsandstryker.nightly.config.mjscovermergeGate,labelMatch,hold,duration,tide,mergeQueue), so the nightlymutationjob never looks at it.Running it by hand at
9aa12b8(npx stryker run stryker.config.mjs --mutate src/utils/auth.ts): 284 mutants, 267 killed, 9 timed out, 8 survived (97.18 %). Two of the survivors change observable behaviour:auth.ts:146memo key`${owner}/${repo}#${issueNum}`→` `checkIssueCommentsfor a second issue in the same run answers from the first issue's commentersauth.ts:457memo key`${owner}/${repo}`→` `retrieveOwnersFilefor a second repository in the same run returns the first repository's root OWNERSNo test pins either key: every existing memoization test (
reads the root OWNERS file once per run,reads the comment thread once) uses one issue in one repository, so a collision is indistinguishable from the cache hit it asserts.The other six are equivalent (
platform().log.debugtext at:464,:475,:489; thecontents === ''guard ofrootOwnersIncludesat:439, which only saves a parse of an empty file).Evidence
npm run test:coverageat9aa12b8—auth.ts100 / 98.71 / 100 / 100, the single branch miss being the documented phantom at:237.coverage-e2e-9aa12b89ea354112926584a27fb4dc06a7ac3ec2of run https://github.com/cncf/prow-github-actions/actions/runs/42768459328 — the same figures forauth.ts.reports/mutation/report.jsonis reproducible with the command given.Recommendation
__tests__/utils/auth.test.ts: two issues in one run get separate comment reads and separate answers; two repositories in one run get separate OWNERS reads and separate answers.src/utils/auth.tsto themutateset ofstryker.nightly.config.mjsso the gate that already exists covers the authorization code, and record the six equivalent survivors in thedocs/contributing.mdsurvivor tables as the other files do. At 97–99 % the file keeps the nightly total above thebreak: 95floor.Decision taken by hive: extend the existing nightly set rather than the local default, which the docs deliberately keep small (~150 mutants, ~12 minutes);
auth.tsadds ~280 mutants, the same order astide.ts.Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9aa12b8— hive: agent=quality backend=copilot model=claude-fable-5.1
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 5 closed as completed in the last 14 days) already cite
docs/contributing.md, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.[quality] Gate proposal: exact (100 %) line floor for the bundle coverage run once its four structural misses carry v8-ignore hints
Finding
The bundle coverage run (
npm run test:coverage:e2e, jobe2e-coverageoftest.yml) gates at 99 % lines / statements / functions / branches.vitest.e2e-coverage.config.mjssays so itself: "A single lost branch is ~0.06 pp and will not cross the floor on its own; the floor bounds drift, it does not detect one-branch regressions." Every miss that run still reports onmainis structural, so the floor could be exact for lines instead of approximate.Evidence — CI artifact
coverage-e2e-9aa12b8…of run 42768459328 (main@9aa12b8), reproduced locally with the same command at the same revision (2613/2617 lines, 1769/1779 branches, 587/591 functions):src/platform/index.ts:61— the "no platform installed" throw inplatform()src/main.tsinstalls the platform before any dispatch__tests__/platform/platformGuards.test.tssrc/plugins/requireMatchingLabel.ts:126—resumeRequiredLabelsbodysrc/deferred.ts, the App/Worker resume path (already excluded from this run)__tests__/plugins/requireMatchingLabel.test.tssrc/plugins/requireMatchingLabel.ts:163-164— the=== 'deferred'armactionsPlatform().deferalways returns'continue'; only the App adapter defers__tests__/plugins/requireMatchingLabel.test.ts(line 192 stub)src/cronJobs/sweep.ts:139branchString(e)arm, non-Error rejectionapprove.ts:516,auth.ts:237branchesdocs/contributing.md); must not be hintedactionsContext,sleep,platformreportedFNDA:0while their lines are hitThis is the sixth-plus instance of the same class — a bundle-unreachable arm lowers the e2e figure and is then hinted one PR at a time: #483, #537, #544, #549, #587 (merged), #693 (held). Per hive policy, three of a class means the gate is missing, so this is one proposal instead of another instance.
Recommendation
Tool: the thresholds that already exist in
vitest.e2e-coverage.config.mjs, run by the existinge2e-coveragejob — no workflow change./* v8 ignore next -- … unreachable from dist/index.js; <test>.test.ts drives it */hints toplatform/index.ts:61andrequireMatchingLabel.ts:125-126and:163-164, in the form__tests__/coverageIgnoreHints.test.tsalready enforces (reason + the unit test that drives the arm). Three hints, one PR.thresholds.linesinvitest.e2e-coverage.config.mjsfrom 99 to 100 and update the comment. Statements follow the same lines; leavebranchesat 99 because the two phantom branches cannot be hinted (the docs forbid it), and leavefunctionsat 99 until the threeFNDA:0records are understood.npm run test:coverage:e2erow indocs/contributing.md("Fails below 99 % lines …").Decision requested from the maintainers: accept an exact line floor for the bundle run (every new bundle-unreachable line must then be hinted or driven in the same PR that adds it), or decline and keep the drift-bounding 99. Decision taken by hive: propose lines only; branches and functions stay at 99 for the reasons above.
Risk: governance — this changes what every future PR must satisfy, not just a number.
Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9aa12b8— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88
hivecommons-hive commentedon Nov 9, 2026
[quality] Gate-scope addendum:
src/utils/auth.tsbelongs in the same nightly Stryker set change asapprove.tsSame deliverable as the folded "add
src/plugins/approve.tsto the nightly Stryker set" proposal above — one line instryker.nightly.config.mjs— so this is a comment, not a second issue.Why
auth.ts. It decides who may run/lgtm,/approve,/hold,/closeand the label commands (policyAllows,assertPolicy,closePolicyAllows,assertAuthorizedByOwnersOrMembership), and#606/#607/#639/#641/#642rewrote it (+129 lines net of deletions) after its last mutation pass. Likeapprove.tsit sits in neither Stryker set, so a behavioural regression that line coverage still reaches fails nothing.Evidence (main@9aa12b8). Local
npx stryker runoversrc/utils/{auth,config,owners}.ts(perTest, concurrency 12,vitest.mutation.config.mjsminus the bundle harness and thesrc/-text-scanning tests; dry run 1656 tests in 2m39s):auth.tsconfig.tsowners.ts49m20s for all three. Every survivor, read at its exact column span, is equivalent or text-only:
auth.ts:146(issueCommentersmemo key →'') and:457(rootOwnersmemo key →''):RunStateis built per delivery (appPlatform,src/adapters/app/platform.ts:92), and a delivery — cron included — carries one repository and one issue, so a collapsed key can never serve another issue's or repository's answer. The per-loginauthCheckskeys (member:,collaborator:) were killed.auth.ts:439(contents === ''guard inrootOwnersIncludes):parseOwners('OWNERS', '')yields empty sets, so the guard is a short-circuit only — the same reading as the:422survivor triaged earlier.auth.ts:464/475/489,owners.ts:313: debug strings.config.ts:312(kind: 'repo'→''): the discriminant is only ever compared with=== 'url'.config.ts:799(toLowerCase→toUpperCase): a dedupSetkey applied consistently to both sides; the stored logins are untouched.So adding
auth.tstostryker.nightly.config.mjswould hold the job'sbreak: 95today (the set's combined score stays above it), at roughly +270 mutants per nightly run.config.tsandowners.tsare not proposed: they are parsing/data code, not the decision, and at 774 + 327 mutants they would more than double the job for no decision-side gain.Unit/e2e line coverage for context (run 42573820881, artifacts
coverage-9aa12b8…andcoverage-e2e-9aa12b8…): all three files 100 % lines in both runs;auth.ts:237is the documented await artefact. No per-survivor issue or PR is filed — nothing behavioural to pin.Decision taken by hive: fold
auth.tsinto the existingapprove.tsnightly-set proposal rather than open a parallel one, because both are the same one-line edit to the same file.Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9aa12b8— hive: agent=quality backend=copilot model=claude-fable-5.1
hivecommons-hive commentedon Nov 10, 2026
[quality] Reconciliation: the
auth.tsmemo-key survivors are equivalent today; nothing new to landThe folded finding above ("Mutation gate does not cover src/utils/auth.ts; its two memo keys are never asserted", 2026-11-10) was filed before reading the gate-scope addendum of 2026-11-09 on this issue. Both runs saw the same eight survivors at
9aa12b8(97.18 %). The addendum's reading stands:RunStateis built per delivery and a delivery carries one issue and one repository, so collapsing theauth.ts:146/:457memo keys cannot serve another target's answer today. The later finding's "behavioural" label is withdrawn; no test PR is opened for it, and the nightly-set change remains parked on theapprove.tsproposal's accept/decline box above.For the record, two tests that would kill those mutants were written and confirmed to fail against them (two issues in one run read separately; two repositories in one run read separately, with the org-membership answer shared per org). They are worth adding only if the sweep or another multi-target delivery ever starts calling
checkIssueCommentsorretrieveOwnersFileacross targets — that is the condition under which the keys become observable.🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9aa12b8— hive: agent=quality backend=copilot model=claude-fable-5.1
hivecommons-hive commentedon Nov 11, 2026
[quality] Gate proposal (folded here by the hive, component docs/contributing.md): exact 100 % line floor for the bundle coverage run once its structural misses carry v8-ignore hints
Filed 2026-11-11 as a separate needs-decision issue; the hive consolidated it into this one because the last deliverable edits the same
docs/contributing.mdrow. It is otherwise independent of the phantom-branch doc fix held in #680.Finding
The bundle coverage run (
npm run test:coverage:e2e, jobe2e-coverageoftest.yml) gates at 99 % lines / statements / functions / branches.vitest.e2e-coverage.config.mjssays so itself: "A single lost branch is ~0.06 pp and will not cross the floor on its own; the floor bounds drift, it does not detect one-branch regressions." Every miss that run still reports onmainis structural, so the floor could be exact for lines instead of approximate.Evidence — CI artifact
coverage-e2e-9aa12b8…of run 42768459328 (main@9aa12b8), reproduced locally with the same command at the same revision (2613/2617 lines, 1769/1779 branches, 587/591 functions):src/platform/index.ts:61— the "no platform installed" throw inplatform()src/main.tsinstalls the platform before any dispatch__tests__/platform/platformGuards.test.tssrc/plugins/requireMatchingLabel.ts:126—resumeRequiredLabelsbodysrc/deferred.ts, the App/Worker resume path (already excluded from this run)__tests__/plugins/requireMatchingLabel.test.tssrc/plugins/requireMatchingLabel.ts:163-164— the=== 'deferred'armactionsPlatform().deferalways returns'continue'; only the App adapter defers__tests__/plugins/requireMatchingLabel.test.ts(line 192 stub)src/cronJobs/sweep.ts:139branchString(e)arm, non-Error rejectionapprove.ts:516,auth.ts:237branchesdocs/contributing.md); must not be hintedactionsContext,sleep,platformreportedFNDA:0while their lines are hitThis is the sixth-plus instance of the same class — a bundle-unreachable arm lowers the e2e figure and is then hinted one PR at a time: #483, #537, #544, #549, #587 (merged), #693 (held). Per hive policy, three of a class means the gate is missing, so this is one proposal instead of another instance.
Recommendation
Tool: the thresholds that already exist in
vitest.e2e-coverage.config.mjs, run by the existinge2e-coveragejob — no workflow change./* v8 ignore next -- … unreachable from dist/index.js; <test>.test.ts drives it */hints toplatform/index.ts:61andrequireMatchingLabel.ts:125-126and:163-164, in the form__tests__/coverageIgnoreHints.test.tsalready enforces (reason + the unit test that drives the arm). Three hints, one PR.thresholds.linesinvitest.e2e-coverage.config.mjsfrom 99 to 100 and update the comment. Statements follow the same lines; leavebranchesat 99 because the two phantom branches cannot be hinted (the docs forbid it), and leavefunctionsat 99 until the threeFNDA:0records are understood.npm run test:coverage:e2erow indocs/contributing.md("Fails below 99 % lines …").Decision requested from the maintainers: accept an exact line floor for the bundle run (every new bundle-unreachable line must then be hinted or driven in the same PR that adds it), or decline and keep the drift-bounding 99. Decision taken by hive: propose lines only; branches and functions stay at 99 for the reasons above.
Risk: governance — this changes what every future PR must satisfy, not just a number.
Priority
Filed by quality agent (hold-gated mode)
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5q9t| SHA:9aa12b8— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88