Repository navigation
In-situ muon efficiency method for the Z dilepton and W analyses - #709
davidwalter2 wants to merge 16 commits into
Conversation
Determine the muon ID/HLT/Iso efficiency scale factors in situ, as Chebyshev polynomials in (pT, uT) fitted as unconstrained nuisances, instead of taking them from external tag-and-probe measurements. Reco and tracking scale factors continue to come from tag-and-probe. Covers both the Z dilepton path and the single-leg W path. Squashed from the following commits: - WIP: in-situ muon efficiency + local changes before merging origin/260216 - In-situ Z muon efficiency: finalize dilepton-only path and fix effStat - In-situ muon efficiency: (pT,uT) extension, both-legs effMC, uT quantiles - In-situ muon efficiency: logit SF, theory-weight fix, pT/uT binning - Restore full tag-and-probe smooth SF helpers alongside reco+tracking - mw: single-leg in-situ muon efficiency at parity with mz_dilepton - In-situ effMC: T&P-aligned probe denominator and tag-unbiased Z effMC - Fix HLT-bit operator precedence in effMC trigger flags; clamp uT at fill - In-situ effMC: logit-Chebyshev smoothing, correlation-factor plots, pt 26-90 - In-situ helpers: separate tag-leg effMC (probe conditional, tag unbiased) - In-situ SF: linear f_pass = 1 + P (MC-independent passing leg) - In-situ effMC plots: per-cell eff(uT) cells and 2D eff(pT,uT) maps - plot_insitu_effSF: linear SF = 1 + P parameterization - In-situ efficiency: pt window to 65 GeV and basis orthogonalisation - Fix stale muon collection names in the dilepton CVH SF block Co-Authored-By: Claude Opus 5 <[email protected]>
8339e1b to
3dac3e2
Compare
The Chebyshev coefficients of the in-situ efficiency parameterisation are strongly correlated under the probe density, which is what made the fit slow, and this recombined them into a basis orthonormal under that density to fix the conditioning. Preconditioning the fit parameters belongs in the fitter, where the transform can be built from the model's own Hessian, refreshed as the fit moves, and changed without regenerating anything upstream; rabbit does that now. Keeping a second mechanism here would mean the histmaker output depends on a basis file that has to be kept identical across the W and Z runs by hand. Removes make_insitu_basis.py, --insituBasisFile from both histmakers and from plot_insitu_effSF.py, the transform plumbing in muon_efficiencies_insitu.py, and the per-block transform matrices from the C++ helper, which goes back to evaluating the raw product basis directly. Co-Authored-By: Claude Opus 5 <[email protected]>
Fitting W and Z together shares every nuisance of the same name, so the muon momentum scale and resolution uncertainties become common to both and the Z constrains the W's calibration, and vice versa. They should not: each measurement carries its own calibration uncertainty. --decorrelateMuonCalibration prefixes their baseName with 'dilepton_' or 'singlelepton_', giving each analysis its own copies. The prefix is keyed off the dilepton flag rather than the channel, so the Z's four category channels stay correlated with each other as they must. The systematic group names are left alone, so grouped impacts still report the calibration as one entry. Off by default; single-analysis fits are unchanged. On the combined W+Z input the 289 muon calibration nuisances become 578, each name appearing once per analysis and none left untagged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HrsrmERvJF2Vafn7wE1B3v
Feeding a fitted set of scale factors back as the linearisation point made the histmakers exit rc=0 having processed a few per-mille of the MC events. The Z signal kept 2954 of 394 million events and 620 of 51200 cells, with 398 effective entries where the previous iteration had 6e7. Nothing was logged, and the yields still looked right to within 10% because the xsec normalisation divides by the bookkeeping weight_sum, which does not know how many events were processed. The cause is that the fitted coefficients are unconstrained and leave the physical region where little data constrains them -- the corners of the pt/ut window. Where eMC*(1+P) >= 1 the fail factor's numerator turns negative, central_factor threw, and the exception was swallowed inside RunGraphs/IMT, ending that dataset's event loop silently. Single threaded it surfaces: unphysical fail factor for step trigger at pt=50.3 eta=-1.49 effMC=0.860014 P=0.223648 -> data efficiency effMC*(1+P) >= 1 1+P is now capped so eMC*(1+P) <= effData_max, projecting such a point back onto the boundary instead. Value and gradient are both taken at the capped point, so accumulate and central_factor still linearise around the same place. With effData_max < 1 the fail numerator is positive by construction and the fail-side throw is unreachable, so it goes; 1+P <= 0 still throws. report_capped_cells logs where this happens, per step. Capping silently would hide exactly the condition that caused the original bug -- a fit that has walked outside the physical region -- and the count is the signal that the scale factors should not be trusted there. On the iteration-1 file: 27 trigger and 172 iso cells for the Z effMC, 23 and 160 for the W. Verified on 3 files with the same coefficients that previously truncated the run: every process back to a 1.000 event ratio and effective entries within 0.3% of the unreweighted reference. The cap is a guard on the linearisation point, not a fit constraint, and it bites hard where it fires: at eMC = 0.86 it leaves a fail factor of 7e-4, turning a 2.4x overprediction into a 2.2x underprediction in the affected bins. Constraining the fit itself is the better answer. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HrsrmERvJF2Vafn7wE1B3v
The cap on the linearisation point is a guard applied after the fit has already left the physical region, and it bites hard: at eMC = 0.86 it leaves a fail factor of 7e-4, so bins holding thousands of data events lose their prediction almost entirely. Constraining the fit is the better answer, and rabbit's InSituEfficiencyBound does that -- but it needs the same MC efficiencies the histmaker used, and the Chebyshev basis evaluated on them. build_insitu_bound_aux emits one row per live effMC cell: the efficiency, the basis there, and which coefficients it multiplies. --insituEffMCFile stores it beside the datacard, so the grid cannot drift away from the coefficient definitions the nuisances stand for. It takes several files and merge_insitu_bound_aux stacks them, because a combined fit shares the coefficients while each analysis keeps its own effMC, and both evaluate fail factors -- the W through its non-isolated sideband (category_expr "passIso ? 0 : 1"). Bounding against only one of them would leave the other unconstrained. Every cell is kept, so the binding constraint per coefficient is whichever analysis has the larger eMC there. The bundle carries coeff_scale = insitu_delta: the helper folds the variation step into the stored response, so the fitted nuisance and the polynomial coefficient differ by that factor. The flat layout is now generated in one place, _insitu_cell_blocks, shared by the bundle and by report_capped_cells, so the penalty and the report cannot drift from each other or from the header. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HrsrmERvJF2Vafn7wE1B3v
eval_leg clamps pt and ut to the Chebyshev window before the transform, so the polynomial's value at an edge is its value everywhere beyond it. The bound grid sampled bin centres only, which left the whole extrapolation unconstrained: the effMC centres stop at pt = 57.5 while the window runs to 65, so the top 7.5 GeV -- and the ut tails beyond +70 -- were never seen by the penalty. That is where the fitted scale factor curves diverge, and it is invisible to a scan over bin centres: a fit that looked physical everywhere it was checked reached f_fail = -22.9 at the edges. Add the two window edges to the sample points along each axis, reusing the nearest bin's efficiency, which is what the helper reads there anyway.
Make the covariance optional. A fit run with --noHessian has none, and the comparison overlay is drawn as a dashed line with no band, so only the primary input actually needs one. Add --ylimStep step=lo,hi. The ID, isolation and trigger scale factors sit on quite different scales, and one shared --ylim either flattens the small ones or clips the large ones. Report the largest scale factor difference between the two fits, both absolutely and in units of the first fit's uncertainty. Two coefficient sets can differ substantially while describing the same scale factors, so a by-eye comparison of the curves is not enough to say whether two fits agree.
rabbit moves to the branch carrying both the in-situ efficiency bound and the bins-sharded multi-device fitter, which is what these fits run on. narf and wums pick up two independent fixes needed to run at all with the current container: boost-histogram's storage_type rename, and setAxisFlow rebuilding axes from plain Python edge values. wremnants-data picks up new resource files from upstream.
rabbit moves from 76b5f3d to 3e59c30, which adds the parameter snapshots' threaded signal path and, more importantly, evaluates the in-situ polynomial as a dense product instead of an atomic scatter. That last change is worth the pointer bump on its own: the scatter cost 7.4 s per Hessian-vector product against 0.29 s for the entire likelihood without it, so it was ~97% of the wall clock of every in-situ fit run so far. On one H200 the fit now reaches a lower loss in 7.6 hours than two 4x V100 fits reached in 9 days 20 hours. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VUPqWUC6BkC7qWM9SM86LH
The bound arrays described the scale factor the fit varies, 1 + delta*P(n), which is the whole scale factor only on iteration 0. From iteration 1 the histmaker has already reweighted the MC by 1 + P(theta_central), so the total is 1 + P(theta_central) + delta*P(n), and the penalty was guarding the wrong quantity -- by up to 0.52 in scale factor where the accumulated correction is largest. Write the per-cell P(theta_central) into the bundle as "offset" and thread the accumulated coefficient file through setupRabbit as --insituSFFile, the same pkl the histmaker was run with. Iteration 0 passes nothing and the offset is zero, so that path is unchanged. The offset is capped exactly as fpass_capped is in the histmaker helper, min(1+P, effMC_max/e) - 1. That is not cosmetic: where theta_central is unphysical the MC was reweighted by the *capped* factor, so storing the uncapped polynomial makes the bound and the histograms disagree about where theta = 0 is, and the fit starts in a region the model does not represent. With the T&P-derived central values that left 130 of 58368 cells at u >= 1 with a fail probability of -0.17, and the fit stalled 1700 above the capped one. Verified end to end: the offset the setup writes reproduces the previous iteration's fitted coefficients to 1.9e-16, and the reconstruction from the theta_central pkl is bit-identical to what the file contains. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_012sxpuabhZaYxUN3erGezFa
Gives the iteration a second, independent linearisation point. Instead of starting from our own fit, start from the scale factors the default mW analysis applies, and check the iteration reaches the same minimum. The output is the same label-keyed theta_central pkl make_insitu_effSF.py writes, so the histmakers and setupRabbit consume it through --insituSFFile unchanged. The projection turns out to be exact rather than approximate, to ~1e-16. SF = 1 + P(theta) is linear in theta, and the smoothed 3D scale factors were themselves produced by smoothing with a 3rd-order polynomial in pt and 2nd in uT -- the same function space our basis spans -- so the least-squares solve reproduces them rather than approximating them. Shrinking the basis to 3x2 breaks it immediately (residuals 3e-2), which is the control showing the agreement is real and not a degenerate fit. Sources match what muon_efficiencies_smooth actually applies: trigger and iso from smoothSF3D_*, 3D in (eta, pt, uT), whose uT axis spans exactly insitu_ut_range and whose 48 eta bins are ours; idip from allSmooth_*.root, 2D, since idip has no uT dependence and is absent from the 3D file. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_012sxpuabhZaYxUN3erGezFa
Compares the fitted scale factors against the default T&P ones at matched uT slices, dashed against solid, so the two are read off the same axes. Takes them from smoothSF3D_*.pkl.lz4 rather than the 2D allSmooth file: the default analysis applies the 3D (eta, pt, uT) scale factors for trigger and iso, so a 2D comparison would be against something the analysis does not use and would attribute their uT dependence to us. --sf3dIsoKey selects between smoothSF3D_iso (triggering muon) and smoothSF3D_isonotrig; our in-situ iso leg is measured on Z dilepton, so which one is the right comparison is a physics choice and left to the caller. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_012sxpuabhZaYxUN3erGezFa
Picks up the matching rabbit-side change: the penalty now reads the per-cell offset this repo writes into the bound bundle, so it guards 1 + P(theta_central) + delta*P(n) rather than only the part the fit varies. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_012sxpuabhZaYxUN3erGezFa
The in-situ efficiencies are measured in the failID/failHLT/failIso legs, so
any nonprompt background there is absorbed into the fitted efficiency rather
than subtracted. Those legs turn out to be strongly fake-enriched: processes
that cannot give two prompt muons (W, W->tau, QG->WQ) are 0.004% of the MC in
the signal region but 1.7% in failID, a factor ~430. QCD multijet, which would
be two fakes, is not in the dilepton model at all.
--dileptonSameSign {plus,minus} rebuilds the pair from same-charge muons, so
the same regions can be produced in a sample that is almost pure nonprompt:
prompt Z->mumu is opposite-sign, and in a 60-file test it gives a same-sign
yield of 3e-6 of the opposite-sign one (charge misassignment), rising to 4e-4
in failID where badly measured muons both flip charge and fail ID.
The two charges are separate runs rather than one axis, because the fake
composition differs between them and merging would hide that.
Implementation note: the opposite-sign selection separates the pair with
"Muon_correctedCharge == {name}_charge0", one collection per sign. That cannot
work when both muons have the same charge -- both collections resolve to the
same muons and select_z_candidate's "exactly one of each" filter is never
satisfied. The same-sign branch therefore builds the collections itself, by pt
rank via the new wrem::nth_by_pt, and returns before the shared charge-based
Define. Nothing downstream is affected by the ordering: the tag/probe
assignment keys off the _tag0 flags and the event parity, not the charge.
The opposite-sign path is untouched -- the diff removes no lines from it -- and
a 60-file A/B gives identical yields through the unchanged branch.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012sxpuabhZaYxUN3erGezFa
Picks up the rabbit-side change that lets --globalImpacts run from a covariance loaded by --externalPostfit instead of silently computing nothing. Not needed by the in-situ analysis itself, but the pointer should name the code that actually produced the impact results in this working tree. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_012sxpuabhZaYxUN3erGezFa
There was a problem hiding this comment.
Issues Found
Important (P2)
- [P2] Remove leftover personal PATH export from setup.sh (
/home/c/cmsmwbot/WRemnantsCI/actions-runner-5/_work/WRemnants/WRemnants/setup.sh:6)- setup.sh now appends the hardcoded user-specific directory /home/submit/david_w/.local/bin/ to PATH for every user sourcing this file. On the shared submit cluster that path exists and can shadow the centrally provided binaries as a fallback (e.g. pip --user entry points), and everywhere else it pollutes the environment of every interactive session and batch job. This looks like a development leftover from the author's own machine and should be dropped before merging (the new Singularity/Apptainer LD_LIBRARY_PATH workaround below it is self-contained and unaffected).
Minor (P3)
- [P3] Per-channel HistoBoost reuses the fixed name "weight" (
/home/c/cmsmwbot/WRemnantsCI/actions-runner-5/_work/WRemnants/WRemnants/scripts/histmakers/mz_dilepton.py:2019-2026)- In the new per-category channel loop, every MC channel books the nominal_weight control histogram with the fixed name "weight". In in-situ mode four channels (nominal, failIso, failHLT, failID) therefore produce four results with the same name for one dataset; narf's build_and_run keys the per-dataset output dict by histogram name (output[name] = H5PickleProxy(...)), so the first three are silently overwritten and only the failID variant reaches the pickle. T&P mode (single "nominal" channel) is unaffected. Name it f"{channel}_weight" to keep all four.
Summary
Total issues: 0 critical, 1 important, 1 minor.
Overall Verdict
Status: Patch has blocking issues
Explanation: The physics machinery (in-situ Chebyshev SF helpers in C++, effMC production/recombination, per-category histmaker split, and rabbit integration) is internally consistent, and the PR additionally fixes two genuine pre-existing bugs (HLT-string parenthesization and a wrong pixel-hit histogram key). Two issues were introduced: a hardcoded personal PATH export committed to setup.sh that pollutes every user's environment (P2), and the per-channel loop silently overwriting three of the four same-named "weight" debug histograms in in-situ mode (P3).
Review details
- Model:
cern/Kimi-K3-FLATIRON
Review metrics: 86 turns · 89 tool calls · 35m 22s
- Tokens:
10.30Minput ·8.5Koutput
Adopt the in-situ muon efficiency workflow from WMass/WRemnants PR WMass#709 (davidwalter2, head d313a80) as the basis for the 5 TeV efficiency implementation: ID/HLT/Iso floated in-situ as Chebyshev coefficients, with only reco+tracking taken from external tag-and-probe SFs. Conflict resolutions: - scripts/rabbit/setupRabbit.py: kept BOTH independent additions at the same spot -- main's SCETlib-NP response-matrix embedding and the PR's in-situ efficiency-bound auxiliary; each writes its own auxiliary. - wremnants-data: kept OUR pointer (94d2ce48). The PR's pointer (124a4aee) differs only in five 5 TeV files -- the 5020 GeV pdfas/pdfvars theory corrections and three scarekit calibration files -- all older uploads (Jun 16 / Jul 2) of files since remade locally (Jul 13 / Jul 22). Taking it would silently revert the CT18Z-denominator corrections and the puWeight=1 calibration. The PR needs nothing else from that bump. Submodules move to the PR's commits: rabbit 9a84645 (needed for rabbit.regularization.insitu), narf 57b6656, wums 3a55815. The local lnN scalar-broadcast fix in rabbit/tensorwriter.py was re-applied on top of the new rabbit (still not upstream). Verified regression-free: the 5 TeV production + tensor + blinded-Asimov fit on this tree reproduces the pre-merge result to 8.4e-14 across all 33 grouped impact rows (sigma(alpha_s) 1.4933 either way). Co-Authored-By: Claude Opus 5.5 <[email protected]>
mz_5TeV.py gets the PR WMass#709 in-situ mode, implied by its inputs as in mz_dilepton.py (--makeInsituEffMC, --insituEffMCFile, --insituSFFile): tag-and-probe selection from the shared muon_selections helpers, four fit channels (nominal = 2HLT_passIso, failIso, failHLT, failID) with every systematic booked per channel, the muonInsituEff Chebyshev variations, the pass-0 effMCprobe spectra and the iteration >= 1 central reweight. The default (no in-situ flag) path is bit-identical to before. setupRabbit_5TeV.py builds one channel per fit category (--muonInsituEfficiency), registers the 2112 coefficients as unconstrained mirrored nuisances with the 13 TeV names, and stores the InSituEfficiencyBound auxiliary (--insituEffMCFile/--insituSFFile). Default tensor unchanged. Era_2017G + HLT_HIMu17 trigger matching: the filter bit is a placeholder (0 = no muon passes) until the 5.02 TeV NanoAOD carries muon trigger objects. make_insitu_effMC.py accepts the z_lowpu mode. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Nano v15 (5.02 TeV data and MC reproduced with muon trigger objects and the
standalone-muon branches the in-situ efficiencies need):
- mz_5TeV.py --nanoVersion {v9,v15} selects ONE production for data and MC via
datasetDict_2017G NANO_PROD_TAGS / NANO_DATA_TAGS (getDatasets honours an
era's tag maps, so productions are never mixed per sample). Default v9 keeps
the old paths.
- nanoaod_compat.harmonize_nano_types: v12+ NanoAOD stores 14 integer branches
in narrower types (e.g. Muon_nTrackerLayers uint8); redefine them to the v9
types when they differ. No-op on v9.
- In-situ trigger matching uses wrem::goodMuonTriggerCandidateLowPU (muon
TrigObj with filter bit 0 = "Mu17 for Muon"), as mz_lowPU.py does, passed
through a new optional goodTrigObjs argument of apply_triggermatching_muon.
The Era_2017G placeholder in utils.hpp/defines.hpp is removed.
np_damping_wall.py: the rabbit bump that came with PR WMass#709 passes indata= to
regularizers, routes -r key=value tokens to the regularizer instead of the
mapping, and adds set_expectations(parms=). Accept all three and route
smallb/margin through, so margin=0 is not silently replaced by the 5e-3
default; the configuration is printed.
hayden_notes.txt: reorganized; full 13 TeV pipeline description; v15 status.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts resolved: - mz_dilepton: keep the branch's per-channel layout and first/second muon naming; port upstream's top pt reweighting (+ nominal_topPtNNLO syst as <channel>_topPtNNLO), the recoil call (first/secondMuons), jpsi_style_cols for the J/psi-style helpers and the v9-only pixel multiplicity uncertainty. - mw_with_mu_eta_pt, setupRabbit: keep both sides. - rabbit: rebuilt the in-situ branch as upstream main + the 4 in-situ commits (261110_insituEfficiencyBound); the rest of the old branch is upstream. - narf, wremnants-data: upstream (the narf commit of the branch is upstream). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GdFuNr1QskK393tsRzxHMU
|
CI run attempt 1 (42916136735) finished with a failure: linting failed and all downstream jobs were skipped. Diagnosis: |
Draft — opened for visibility while the scope is still being split. Not ready to merge as-is, see Scope caveats below.
What this adds
An in-situ determination of the muon ID / HLT / Iso data-MC scale factors, as an alternative to the external tag-and-probe SFs. The three efficiency steps are floated as Chebyshev polynomials in (pT, uT), decorrelated per probe-eta bin, as unconstrained nuisances (not NOIs), so the scale factors are determined by the fit itself rather than taken from an external measurement.
The Z dilepton analysis is split into four categories —
nominal,failIso,failHLT,failID— which become four fit channels sharing the same coefficient nuisances, and that cross-category constraint is what determines the efficiencies. Reco and tracking SFs stay external.The passing-leg scale factor is the polynomial directly,
f_pass = 1 + P, which makes it MC-independent; the MC efficiency enters only the failing-leg factorf_fail = (1 - eMC(1+P))/(1 - eMC). Each analysis therefore supplies its own effMC. The method is implemented for both the dilepton (two-leg tag-and-probe) and the W single-muon (single-leg) topologies through a shared C++ base. The coefficients are common to W and Z, so the two can be fitted simultaneously with the scale factors fully correlated between them.The exact likelihood is linearised around a central point and iterated:
make_insitu_effSF.pyaccumulatestheta_new = theta_prev + delta * n_hat, fed back via--insituSFFile.Workflow
mz_dilepton.py --makeInsituEffMC --makeUTQuantileHists— probe spectra and uT quantile inputsscripts/corrections/make_insitu_effMC.py— per-step MC efficienciesmz_dilepton.py --insituEffMCFile ...— templates and coefficient response tensorssetupRabbit.py --muonInsituEfficiencywith the four--baseNamesmake_insitu_effSF.pyto produce the next iteration's central coefficientsConditioning of the coefficients
Chebyshev polynomials are orthogonal under the weight
1/sqrt(1-x^2), which is nothing like the probe density, so the coefficients come out strongly correlated — correlation condition number 6.1e4 over the 2112-coefficient block in the 2D fit, 1.3e5 in the 4D one. That is what made the fit slow:trust-krylov's inner solve needs ~sqrt(kappa)Hessian-vector products, and steps get rejected. Measured on the full-statistics 2D fit: 145 minutes, with 20 of 78 iterations returning a bit-identical loss and the worst single iteration burning 27 minutes to make no progress.This branch previously carried an offline fix (
make_insitu_basis.pyplus--insituBasisFile) that recombined the coefficients into a basis orthonormal under the probe density. That has been removed in favour of preconditioning the fit parameters inside the fitter, WMass/rabbit#150, which builds the transform from the model's own Hessian, refreshes it as the fit moves, and needs no regenerated inputs upstream. The offline version also required the same basis file to be used across the W and Z histmakers by hand, since the nuisances are correlated between them — a consistency requirement nothing checked.For reference, on the same full-statistics 2D input: unpreconditioned 145.5 min, the offline basis 179 min, preconditioning in rabbit 14.3 min. All three converge to genuine minima; the preconditioned fits reach EDM ~1e-16 against ~1e-11 unpreconditioned.
Scope caveats
260216_totalPhaseSpacebranch (make_pixel_corrections.py,setup.sh,.gitignore). Those are not part of this feature and should be split out or land separately.--insituEffMCFilethe histmakers follow the existing external tag-and-probe path unchanged.🤖 Generated with Claude Code