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
#8184 gave AMS a real backtest for ONE knob: calibration backtest-threshold replays a candidate min-rank skip threshold against the node's own history (backtestMinRankCandidate in packages/loopover-miner/lib/ams-calibration.ts, pure core in packages/loopover-engine/src/calibration/ams-rank-corpus.ts). The eligibility-exclusion rules — AMS's other deterministic gate — have no equivalent: a proposed ContributionProfile change ships on a read of the diff, exactly the backtest-before-ship gap #8082 closed for ORB. With bounded raw context captured on exclusion events (blocked-by, see callout), a candidate profile can be replayed over the recorded exclusion history and Pareto-compared before it ships.
Requirements
⚠️BLOCKED — do not open a PR until the "capture bounded candidate context in eligibility-exclusion fired events" issue is CLOSED (it is linked as blocked-by on this issue). A PR opened while that issue is open will be closed. Fired events recorded before that capture landed have no metadata and MUST be skipped (counted, not guessed at) — see below.
New miner lib module packages/loopover-miner/lib/ams-eligibility-backtest.ts exporting a pure backtestEligibilityCandidate(events, currentProfiles, candidateProfiles):
Reads signal_rule_fired / signal_human_override events for the four eligibility ruleIds (exclusion_label, missing_eligibility_label, conflicting_signals, excluded_assignee) via the same event-reading conventions ams-calibration.ts uses.
Reconstructs each fired event's FilterCandidate from its metadata (labels/assignees/owner + targetKey's repoFullName); events with no metadata are excluded from the corpus and counted in a skippedNoContext stat the CLI must print.
Classifier: re-run filterCandidatesByProfiles (imported from contribution-profile-filter.ts — do not reimplement its logic) under the candidate profiles; a candidate profile excluding the reconstructed candidate (any reason) classifies as the fired outcome, kept classifies as not-fired.
Ground truth via buildBacktestCorpus; split via splitBacktestCorpus reusing ams-rank-corpus.ts's held-out fraction constant and a new pinned seed constant; score via scoreBacktest; compare current-vs-candidate via compareBacktestScores on visible AND held-out splits.
Sample floors: reuse the exact floor constants backtestMinRankCandidate applies (read them from ams-calibration.ts — do not invent new numbers); under-floored corpus returns null exactly like backtestMinRankCandidate does.
CLI subcommand calibration backtest-eligibility --profile <path> in packages/loopover-miner/lib/calibration-cli.ts, mirroring runBacktestThreshold's structure verbatim: --json support; missing/invalid --profile file (JSON map of repoFullName → ContributionProfile) → reportCliFailure; under-floored corpus prints an explicit line and exits 0; success prints current→candidate summary, skippedNoContext, and renderBacktestComparison for both splits; exit is nonzero only on operational error, never on verdict (the Scaffold the advisory backtest CI workflow + threshold-only comparison, and persist each run's result #8138 advisory guarantee).
Tests, all Codecov-visible (root-vitest mirror per test(miner): mirror shouldReenter under root vitest for Codecov (#8347) #8479 if needed): corpus reconstruction (with/without metadata → skippedNoContext both arms), classifier agreement with filterCandidatesByProfiles on a fixture, under-floor null arm and success arm, CLI --json and text outputs, persistence event shape, track-record inclusion. 100% of changed lines and branches — both arms of every ??/ternary/&&.
Deliverables
ams-eligibility-backtest.ts pure core per the exact contract above
Advisory only — nothing under this issue changes live discovery behavior or applies a profile. No new profile-mutation paths; no reimplementation of filter logic; overrides may be sparse or absent (the scorer's decided ≤ fired discipline handles that — never fabricate verdicts).
Problem
#8184 gave AMS a real backtest for ONE knob:
calibration backtest-thresholdreplays a candidate min-rank skip threshold against the node's own history (backtestMinRankCandidateinpackages/loopover-miner/lib/ams-calibration.ts, pure core inpackages/loopover-engine/src/calibration/ams-rank-corpus.ts). The eligibility-exclusion rules — AMS's other deterministic gate — have no equivalent: a proposedContributionProfilechange ships on a read of the diff, exactly the backtest-before-ship gap #8082 closed for ORB. With bounded raw context captured on exclusion events (blocked-by, see callout), a candidate profile can be replayed over the recorded exclusion history and Pareto-compared before it ships.Requirements
packages/loopover-miner/lib/ams-eligibility-backtest.tsexporting a purebacktestEligibilityCandidate(events, currentProfiles, candidateProfiles):signal_rule_fired/signal_human_overrideevents for the four eligibility ruleIds (exclusion_label,missing_eligibility_label,conflicting_signals,excluded_assignee) via the same event-reading conventionsams-calibration.tsuses.FilterCandidatefrom itsmetadata(labels/assignees/owner+targetKey'srepoFullName); events with no metadata are excluded from the corpus and counted in askippedNoContextstat the CLI must print.filterCandidatesByProfiles(imported fromcontribution-profile-filter.ts— do not reimplement its logic) under the candidate profiles; a candidate profile excluding the reconstructed candidate (any reason) classifies as the fired outcome, kept classifies as not-fired.buildBacktestCorpus; split viasplitBacktestCorpusreusingams-rank-corpus.ts's held-out fraction constant and a new pinned seed constant; score viascoreBacktest; compare current-vs-candidate viacompareBacktestScoreson visible AND held-out splits.backtestMinRankCandidateapplies (read them fromams-calibration.ts— do not invent new numbers); under-floored corpus returnsnullexactly likebacktestMinRankCandidatedoes.calibration backtest-eligibility --profile <path>inpackages/loopover-miner/lib/calibration-cli.ts, mirroringrunBacktestThreshold's structure verbatim:--jsonsupport; missing/invalid--profilefile (JSON map ofrepoFullName→ContributionProfile) →reportCliFailure; under-floored corpus prints an explicit line and exits 0; success prints current→candidate summary,skippedNoContext, andrenderBacktestComparisonfor both splits; exit is nonzero only on operational error, never on verdict (the Scaffold the advisory backtest CI workflow + threshold-only comparison, and persist each run's result #8138 advisory guarantee).MINER_AMS_ELIGIBILITY_BACKTEST_EVENT = "ams_eligibility_backtest_run"ledger event mirroringrecordAmsThresholdBacktestRun's shape, and include the new event type incomputeAmsBacktestTrackRecord's aggregation (parity with ams-calibration: aggregate backtest track record into the miner calibration report + snapshot event #8185).skippedNoContextboth arms), classifier agreement withfilterCandidatesByProfileson a fixture, under-floornullarm and success arm, CLI--jsonand text outputs, persistence event shape, track-record inclusion. 100% of changed lines and branches — both arms of every??/ternary/&&.Deliverables
ams-eligibility-backtest.tspure core per the exact contract abovecalibration backtest-eligibilitysubcommand mirroringrunBacktestThresholdLinks & Resources
packages/loopover-miner/lib/ams-calibration.ts,packages/loopover-miner/lib/calibration-cli.ts,packages/loopover-miner/lib/contribution-profile-filter.ts,packages/loopover-engine/src/calibration/(backtest-corpus,backtest-score,backtest-compare,backtest-split,backtest-report)Boundaries
Advisory only — nothing under this issue changes live discovery behavior or applies a profile. No new profile-mutation paths; no reimplementation of filter logic; overrides may be sparse or absent (the scorer's
decided ≤ fireddiscipline handles that — never fabricate verdicts).