chore(engine): one maintainer-association predicate, with a guard against the ninth copy - #9932
Conversation
…inst the ninth copy #9877 consolidated five spellings of ["OWNER", "MEMBER", "COLLABORATOR"] into packages/loopover-engine/src/settings/author-association.ts. Verifying that merge turned up three more, so the real count was eight: - engine/scoring/pending-pr-scenarios.ts, whose own comment said "duplicated here (rather than imported) because this package cannot reach into src/; keep the two in sync by hand". That reason expired the moment the definition moved INTO the engine. - services/contributor-evidence-graph.ts, spelled with .toUpperCase(). - services/outcome-calibration.ts -- which I did not find by reading, but which the new guard caught on its first run. Two had already drifted: one compared case-sensitively, one case-insensitively. That is how a copy stops being a copy, and why this is worth a checker rather than a sweep. engine/signals/engine.ts's strongestAssociation is the one consumer that depends on the list's ORDER (OWNER > MEMBER > COLLABORATOR) rather than membership, so it now iterates the shared constant and says so. The guard computes the fact instead of remembering it, per #9860's own bar: it greps for the three-value maintainer subset and fails with the import to use. It deliberately does NOT flag z.enum declarations of GitHub's full eight-value vocabulary -- those are a wire schema, not this predicate.
|
Important 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏳ LoopOver is waiting…LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 7f9b7f0 | Commit Preview URL Branch Preview URL |
Jul 30 2026, 05:39 PM |
Bundle ReportBundle size has no change ✅ |
105da06 to
badf9fb
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9932 +/- ##
=======================================
Coverage 91.88% 91.88%
=======================================
Files 923 923
Lines 113407 113391 -16
Branches 27332 27325 -7
=======================================
- Hits 104205 104191 -14
+ Misses 7915 7913 -2
Partials 1287 1287
Flags with carried forward coverage won't be shown. Click here to find out more.
|
…al exception Review was right, and the miss was in the guard itself. MAINTAINER_TRIPLE only matched comma-separated array literals, so it could never have found `value === "OWNER" || value === "MEMBER" || value === "COLLABORATOR"` -- which was sitting in signals/engine.ts with fourteen call sites, in the same file whose strongestAssociation I did migrate. A guard that recognises one spelling of the thing it guards is a hand-maintained list with extra steps. Detection is now "all three named within a 4-line window", which is form-agnostic and survives a formatter wrapping a chain. Widening it immediately surfaced four more genuine copies nothing had flagged: signals/issue-quality-report.ts, signals/predicted-gate-engine.ts, services/control-panel-roles.ts (identical chains) and services/recommendation-outcomes.ts (the same predicate inside isMaintainerLane). It also surfaced one site that must NOT be migrated: settings/command-authorization.ts maps OWNER/MEMBER to `maintainer` and COLLABORATOR to a SEPARATE `collaborator` role. Collapsing those would grant collaborators maintainer-only commands. That is an exception with a stated reason, not a known-copy list -- the distinction #9860 draws -- and it follows check-regate-sort-key.ts's ALLOWED_OMISSIONS idiom. Also fixes the guard's own reporting: it printed the window's first line, which is often a brace or blank, instead of the line that names an association.
|
The blocker is correct, and the miss was in the guard rather than only in the migration. Fixed in the latest push — thank you, this was the right catch. What was wrong
function isMaintainerAssociation(value: string | null | undefined): boolean {
return value === "OWNER" || value === "MEMBER" || value === "COLLABORATOR";
}which was sitting in What changedDetection is now "all three named within a 4-line window" — form-agnostic, and it survives a formatter wrapping a chain across lines. Widening it immediately surfaced four more genuine copies that nothing had flagged, none of which I had found by reading:
So the real count was thirteen, not eight. All are migrated. One site deliberately not migrated
if (args.commenterAssociation === "OWNER" || args.commenterAssociation === "MEMBER") roles.push("maintainer");
if (args.commenterAssociation === "COLLABORATOR") roles.push("collaborator");
Also fixedThe guard printed the window's first line, which is often a closing brace or blank, instead of the line that actually names an association. Its output was unreadable in exactly the case it was built for. Validation
|
Toward #9860 (candidate class: hand-maintained lists). Follow-up to #9877.
What verifying #9877's merge turned up
#9877 consolidated five spellings of
["OWNER", "MEMBER", "COLLABORATOR"]. Checking that the merge landed intact, I grepped for the literal again and found three more — so the real count was eight:packages/loopover-engine/src/scoring/pending-pr-scenarios.tssrc/; keep the two in sync by hand." That reason expired the moment #9877 moved the definition into the engine.src/services/contributor-evidence-graph.ts.toUpperCase()src/services/outcome-calibration.tsTwo of the eight had already drifted: one compared case-sensitively, one case-insensitively. That is how a copy stops being a copy, and it is the argument for a checker rather than another sweep.
The one that isn't membership
packages/loopover-engine/src/signals/engine.ts'sstrongestAssociationdepends on the list's order (OWNER > MEMBER > COLLABORATOR), not on membership. It iterates the shared constant now and says so in a comment, so the ordering dependency is explicit rather than implied by a re-typed literal.The guard
scripts/check-maintainer-association-copies.ts, wired intotest:ci. Per #9860's own bar — compute the fact, don't remember it — it greps for the three-value maintainer subset rather than listing known offenders, and fails with the import to use:It deliberately does not flag
z.enumdeclarations of GitHub's full eight-value vocabulary (CONTRIBUTOR,FIRST_TIMER,MANNEQUIN,NONE, …). Those are a wire schema, not this predicate, and the contract legitimately declares that enum four times — conflating the two would be its own mistake. That exclusion is tested.Behaviour note
pending-pr-scenarios.tsandoutcome-calibration.tswere case-sensitive; the shared predicate is case-insensitive and trims. GitHub always sends upper-case, so this is a widening with no practical change — and it removes a latent mismatch where a lower-cased stored value would have been missed by two of the eight and caught by the other six.Validation
test:ci.maintainer-associations:check,engine-parity:drift-check,dead-exports:check,dead-source-files:check,import-specifiers:check,docs:drift-check,db:migrations:check,tsc --noEmit,git diff --checkall clean.