Skip to content

Commit 8a92844

Browse files
JohnCCarterclaude
andcommitted
feat(research): explain the 4H<->1D snapping sign flip (net-vs-path channel, descriptive)
Narrow investigate follow-up: why does the snapping<->cleanliness relationship flip sign between 4H (deflate) and 1D (inflate)? Same frozen data / locked detection; DESCRIPTIVE-ONLY -- no verdict, no claim, no lock change, no matched-null, no new universe, no new bins. Crux stays OPEN. cleanliness = net/path, so a snap changes cleanliness via a NET channel and a PATH channel. On the moved-snap domain (snaps that actually changed the span; unmoved snaps are dclean=0 by construction): - 4H: path grows faster than net (median rel_path 0.231 > rel_net 0.181; 63% path-dominated) -> cleanliness DOWN (matches snapping_deflates). - 1D: net grows faster than path (median rel_net 0.063 > rel_path 0.025; 60% net-dominated) -> cleanliness UP (matches snapping_inflates). The dominant channel FLIPS between TFs -- mechanically consistent with candle granularity (fine 4H bars add intermediate retracement = path; coarse 1D bars put the detector pivot at a more extreme price = net). Spearman(dclean, rel_path) itself flips -0.18 -> +0.20. The dclean~(rel_net-rel_path) Spearman is 0.99 BY CONSTRUCTION (arithmetic identity, flagged, not the finding). Kept apart from any selection-claim: this is the measurement geometry of snapping, NOT evidence about cleanliness-as-human-signal. Extended the mechanics module (+net/path decomposition, 2 descriptive ArtifactRow fields snap_a_idx/snap_b_idx, +2 tests). Checkpoint + handoff updated. Gates green: ruff + format + 588 pytest (cov 74.38%) + bounds + wiki-lint. No Genesis/1H/ETH/refresh/label-mutation/edge claim. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5f58099 commit 8a92844

6 files changed

Lines changed: 230 additions & 3 deletions

docs/research_wiki/handoff.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ legs/ranges* (labels = facit; **no edge/behaviour/backtest/PnL/Genesis/auto-fib
131131
Population guard: M1 ≠ Stage-2 lead (different population). No matched-null/new universe/Genesis/1H/
132132
ETH/refresh. [Mechanics note](reviews/btc-fib-selection-learning-artifact-mechanics-20260624.md);
133133
mechanics_summary.json gitignored/regenerable.
134+
- **2026-06-24 Fib SELECTION-LEARNING snapping FLIP mechanics — BUILT + RUN (descriptive, no verdict).**
135+
Narrow follow-up: *why does snapping↔cleanliness flip sign 4H↔1D*. Extended the mechanics module
136+
(+net/path decomposition, 2 more `ArtifactRow` fields, +2 tests). On the moved-snap domain it is a
137+
**net-vs-path channel reversal** consistent with **candle granularity**: 4H grows **path** > net
138+
(rel_path 0.231 > rel_net 0.181, 63% path-dominated → clean down); 1D grows **net** > path (0.063 >
139+
0.025, 60% net-dominated → clean up). Explains the **measurement geometry**, kept apart from any
140+
selection-claim; **no verdict, no lock change, crux OPEN**. No matched-null/universe/Genesis/1H/ETH/
141+
refresh. [Flip note](reviews/btc-fib-selection-learning-artifact-mechanics-flip-20260624.md).
134142

135143
**Next work requires a separate explicit GO. No W/gap, no Stage 1, no new sensitivity, and no Genesis
136144
may be started automatically.** Parked (test-only, separate GO): lock the facit-discipline refusal
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# BTC Fib Selection-Learning — snapping sign-FLIP mechanics (4H↔1D) (2026-06-24)
2+
3+
**Lean Fib Research. Research-only. DESCRIPTIVE-ONLY — NO verdict, NO claim, no edge/behaviour/PnL/
4+
backtest/Genesis/auto-fib/1H/ETH/label-mutation.** A narrow follow-up to the
5+
[mechanics note](btc-fib-selection-learning-artifact-mechanics-20260624.md): *why does the relationship
6+
between snapping and cleanliness flip sign between 4H and 1D* (artifact-probe: 4H snapping **deflates**
7+
cleanliness, 1D **inflates** it). Same **frozen data / locked detection** (no refresh, no new universe,
8+
no matched-null, no new bins, no lock change). It explains the **measurement geometry of snapping** and
9+
says **nothing** about human selection — the crux stays OPEN.
10+
11+
> **STATUS — the flip is a net-vs-path channel reversal driven by candle granularity (descriptive).**
12+
> `cleanliness = net/path`, so a snap changes cleanliness via two channels: it moves the **net**
13+
> (endpoint-to-endpoint move) and the **path** (summed intermediate move). Restricting to snaps that
14+
> **actually moved the span** (the question's support; unmoved snaps have Δclean=0 by construction):
15+
> on **4H** snapping grows **path faster than net** (median rel_path **0.231** > rel_net **0.181**;
16+
> 63% path-dominated) → cleanliness **down**; on **1D** it grows **net faster than path** (median
17+
> rel_net **0.063** > rel_path **0.025**; 60% net-dominated) → cleanliness **up**. The dominant channel
18+
> **flips** between the two TFs — mechanically consistent with **candle granularity** (fine 4H bars add
19+
> mostly intermediate retracement = path; coarse 1D bars put the detector pivot at a genuinely more
20+
> extreme price = net). **No verdict, no claim, crux unchanged.**
21+
22+
## Method (descriptive decomposition; no new universe / bins / lock)
23+
24+
For each reached non-degenerate leg the probe already records the exact human span `[pos_a, pos_b]` and
25+
the snapped detector-pivot span `[snap_a_idx, snap_b_idx]` (two new descriptive `ArtifactRow` fields,
26+
used by no contrast/verdict). On the same frozen closes: `net = |close[hi] − close[lo]|`,
27+
`path = Σ|Δclose|`; `rel_net = Δnet/net_exact`, `rel_path = Δpath/path_exact`. Since
28+
`cleanliness = net/path`, `Δln(cleanliness) ≈ rel_net − rel_path` — an **arithmetic identity** (the
29+
`d_clean ~ (rel_net−rel_path)` Spearman is **0.99 by construction**, flagged, **not** the finding). The
30+
**non-trivial empirical content** is *which channel dominates per TF*. Domain = snaps that **moved the
31+
span** (unmoved snaps are Δclean=0 — the question's support, not a post-hoc bin); the moved fraction is
32+
reported.
33+
34+
## Results — channel medians on the moved-snap domain
35+
36+
| TF | moved / total | median `rel_net` | median `rel_path` | dominant channel | frac net-dom | Spearman(Δclean, rel_net) | Spearman(Δclean, rel_path) |
37+
|----|--------------:|-----------------:|------------------:|------------------|-------------:|--------------------------:|---------------------------:|
38+
| **4H** | 107 / 314 | 0.181 | **0.231** | **PATH** (clean ↓) | 0.374 | +0.476 | **−0.177** |
39+
| **1D** | 20 / 60 | **0.063** | 0.025 | **NET** (clean ↑) | 0.600 | +0.864 | +0.201 |
40+
| 1w (ctx) | 5 / 19 | 0.024 | 0.048 | path | 0.40 | +0.90 | +0.10 |
41+
| 1M (ctx) | 6 / 9 | 0.244 | 0.253 | ~tie | 0.67 | +0.83 | +0.26 |
42+
43+
(`d_clean ~ (rel_net−rel_path)` identity Spearman = 0.99 on 4H/1D — validation, by construction.)
44+
45+
- **The flip is the channel reversal.** 4H: `rel_path > rel_net`, 63% of moved snaps path-dominated →
46+
net deflation of cleanliness (matches the locked `snapping_deflates`). 1D: `rel_net > rel_path`, 60%
47+
net-dominated → inflation (matches `snapping_inflates_cleanliness`). The `Spearman(Δclean, rel_path)`
48+
even changes sign (4H **−0.18** vs 1D **+0.20**): on 4H the path channel pulls cleanliness down, on 1D
49+
it does not.
50+
- **Magnitude context:** 4H snaps move the span proportionally **more** (rel ~0.18–0.23) than 1D
51+
(~0.03–0.06) — but on 4H that movement is disproportionately **path**.
52+
- **Caveat (honest):** the per-leg `median(rel_net − rel_path)` is ≈0 on 4H (the per-leg difference
53+
straddles 0); the sign is carried by the **channel medians + the net-dominated fraction**, not the
54+
per-leg median — reported as such, not overstated.
55+
56+
## Mechanical reading (descriptive — kept apart from any selection-claim)
57+
58+
The dominant-channel flip is **mechanically consistent with candle granularity**: on **fine 4H bars**,
59+
a detector pivot a few bars off the human anchor adds **fine intermediate candles that are mostly
60+
retracement** → path grows faster than net → cleanliness falls. On **coarse 1D bars**, the detector
61+
pivot being a different bar means a **genuinely more extreme price reached comparatively directly**
62+
net grows faster than path → cleanliness rises. This is a statement about **how the detector's bar
63+
granularity interacts with the cleanliness measurement when anchors are snapped** — it is **not** a
64+
statement about human selection, the `cleanliness` lead, or the crux.
65+
66+
## Separation of mechanics from selection-claim (binding)
67+
68+
- This explains the **measurement geometry of snapping** (a net-vs-path channel reversal by TF). It is
69+
**not** evidence that the Stage-2 `cleanliness` lead is or is not a genuine human signal.
70+
- The artifact-probe reading is **unchanged** (4H `snapping_deflates`, 1D `snapping_inflates`,
71+
combined `meta:` status); **no lock is touched**; **no new verdict / claim**.
72+
- **The crux stays OPEN.** Explaining the snapping flip mechanically does not resolve "is `cleanliness`
73+
human intuition or artifact" either way.
74+
75+
## Observed / Inferred / Unverified
76+
77+
- **Observed (verified):** the table; 4H moved-snap median rel_path 0.231 > rel_net 0.181 (63%
78+
path-dominated); 1D rel_net 0.063 > rel_path 0.025 (60% net-dominated); `Spearman(Δclean, rel_path)`
79+
flips −0.18→+0.20; identity Spearman 0.99; deterministic; new tests green.
80+
- **Inferred (descriptive, scoped):** the 4H↔1D snapping sign flip is a **net-vs-path channel reversal**
81+
mechanically consistent with **candle granularity** (fine bars add path, coarse bars add net).
82+
- **Unverified / scope limits:** no verdict, no claim, no lock change; the granularity reading is a
83+
*mechanical interpretation*, not a tested hypothesis (no new universe was built to isolate it); 1M/1w
84+
are context (moved n = 6 / 5); the crux is unchanged and OPEN.
85+
86+
## Non-claims (binding)
87+
88+
Descriptive consolidation only. **No verdict, no positive claim, no lock change, no reproduction, no
89+
edge/behaviour/PnL/backtest/strategy claim.** Explaining the snapping geometry is **not** evidence about
90+
`cleanliness`-as-human-signal. No matched-null, no new candidate universe, no new model feature, no
91+
Genesis, no auto-fib-as-truth, no 1H, no ETH, no label/corpus mutation, no `data.fetch --refresh`.
92+
93+
> The snapping sign flip is a **net-vs-path channel reversal**: 4H snaps add **path** (fine-bar
94+
> retracement) → cleanliness down; 1D snaps add **net** (coarse-bar extreme) → cleanliness up —
95+
> mechanically a **candle-granularity** effect. Descriptive only — measurement geometry, not human
96+
> selection; no verdict, no claim, crux unchanged.

docs/research_wiki/reviews/btc-fib-selection-learning-checkpoint-20260624.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ steeply with span (Spearman −0.69), reached legs are longer (median 5 vs 3 bar
8888
**vanishes when conditioning on span** (short-span +0.003, long-span −0.017): a composition effect, not
8989
a detector cleanliness-preference. 4H snapping deflates because detector pivots sit **outside** the
9090
human anchors (span extends, 33% vs 1.6%); the 1D flip is a **TF-dependent geometry** (1D shrinks more,
91-
10%, **and** the span↔cleanliness relationship flips sign, −0.19→+0.18) — *why* the local relationship
92-
reverses by TF stays an **open investigate-target**. None of this resolves the crux either way.
91+
10%, **and** the span↔cleanliness relationship flips sign, −0.19→+0.18). A follow-up
92+
[flip note](btc-fib-selection-learning-artifact-mechanics-flip-20260624.md) now **mechanically
93+
attributes** that reversal (descriptive) to a **net-vs-path channel flip** consistent with **candle
94+
granularity**: on the moved-snap domain, 4H grows path > net (rel_path 0.231 > rel_net 0.181) →
95+
cleanliness down; 1D grows net > path (0.063 > 0.025) → up. None of this resolves the crux either way.
9396

9497
**Secondary loose end:** set-level **`exclusivity`** (`k*=3`) was specced in the
9598
[§12 addendum](btc-fib-selection-learning-addendum-20260618.md) but the Stage-2 live whitelist actually

src/fibengine/research/selection_learning_artifact.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ class ArtifactRow:
120120
span_bars: int = 0 # |pos_b - pos_a| (duration proxy)
121121
magnitude_atr: float | None = None # |close[b] - close[a]| / causal ATR at anchor_b
122122
snap_span_delta: int | None = None # |snapped span| - |exact span| (reached, non-degenerate)
123+
snap_a_idx: int | None = (
124+
None # snapped (detector-pivot) bar index at anchor_a (reached non-deg)
125+
)
126+
snap_b_idx: int | None = (
127+
None # snapped (detector-pivot) bar index at anchor_b (reached non-deg)
128+
)
123129

124130

125131
def _quarter_of(ts: pd.Timestamp) -> str:
@@ -158,6 +164,7 @@ def build_artifact_rows(
158164
else None
159165
)
160166
reached, snapped, drop, snap_span_delta = False, None, None, None
167+
snap_a_idx, snap_b_idx = None, None
161168
if atr_at_b > 0 and np.isfinite(atr_at_b): # fail-closed on degenerate ATR
162169
price_tol = cfg.eps_price_atr * atr_at_b
163170
pivots = detect_pivots(df_t, pivot_cfg)
@@ -174,6 +181,7 @@ def build_artifact_rows(
174181
else:
175182
snapped = _cleanliness_idx(closes, piv_a.index, piv_b.index)
176183
snap_span_delta = abs(piv_b.index - piv_a.index) - span_bars # descriptive (P3)
184+
snap_a_idx, snap_b_idx = int(piv_a.index), int(piv_b.index)
177185
rows.append(
178186
ArtifactRow(
179187
quarter=_quarter_of(leg.anchor_b_ts),
@@ -186,6 +194,8 @@ def build_artifact_rows(
186194
span_bars=span_bars,
187195
magnitude_atr=magnitude_atr,
188196
snap_span_delta=snap_span_delta,
197+
snap_a_idx=snap_a_idx,
198+
snap_b_idx=snap_b_idx,
189199
)
190200
)
191201
return rows

src/fibengine/research/selection_learning_artifact_mechanics.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,61 @@ def _median_iqr(vals: list[float]) -> dict[str, Any] | None:
8989
}
9090

9191

92+
# --- the 4H↔1D snapping FLIP: net-vs-path channel decomposition (DESCRIPTIVE) -----------------
93+
94+
95+
def _net_path_idx(closes: np.ndarray, i: int, j: int) -> tuple[float | None, float | None]:
96+
"""(net, path) over the bar-index span [lo,hi] — the two ingredients of cleanliness=net/path."""
97+
lo, hi = sorted((int(i), int(j)))
98+
seg = closes[lo : hi + 1]
99+
if len(seg) < 2:
100+
return None, None
101+
return float(abs(seg[-1] - seg[0])), float(np.abs(np.diff(seg)).sum())
102+
103+
104+
def _flip_decomposition(rows: list, closes: np.ndarray) -> dict[str, Any]:
105+
"""Why snapping changes cleanliness, and why the sign flips 4H↔1D. cleanliness=net/path, so
106+
d_clean is governed by whether snapping changes NET or PATH more (rel_net − rel_path is an
107+
arithmetic identity — flagged). Non-trivial content = WHICH channel dominates per TF (median
108+
rel_net vs rel_path). DESCRIPTIVE — no verdict/claim."""
109+
rel_net, rel_path, npmp, dclean = [], [], [], []
110+
n_total = 0
111+
for r in rows:
112+
if not (r.reached and r.snap_a_idx is not None and r.snapped_clean is not None):
113+
continue
114+
n_total += 1
115+
# MOVED domain only: legs where snapping actually changed the span. Unmoved snaps have
116+
# d_clean=0 by construction (the majority here) and are irrelevant to "why snapping changes
117+
# cleanliness" — excluding them is the question's support, NOT a post-hoc bin.
118+
if sorted((r.snap_a_idx, r.snap_b_idx)) == sorted((r.pos_a, r.pos_b)):
119+
continue
120+
en, ep = _net_path_idx(closes, r.pos_a, r.pos_b)
121+
sn, sp = _net_path_idx(closes, r.snap_a_idx, r.snap_b_idx)
122+
if None in (en, ep, sn, sp) or en <= 0 or ep <= 0:
123+
continue
124+
rn, rp = (sn - en) / en, (sp - ep) / ep
125+
rel_net.append(rn)
126+
rel_path.append(rp)
127+
npmp.append(rn - rp)
128+
dclean.append(r.snapped_clean - r.exact_clean)
129+
n = len(dclean)
130+
if n == 0:
131+
return {"n_total": n_total, "n_moved": 0}
132+
return {
133+
"n_total": n_total, # all reached non-degenerate snaps
134+
"n_moved": n, # snaps that actually changed the span (the analysis domain)
135+
"median_rel_net": float(np.median(rel_net)), # relative net change from snapping
136+
"median_rel_path": float(np.median(rel_path)), # relative path change from snapping
137+
"median_net_minus_path": float(np.median(npmp)), # >0 → net-dominated (clean up); <0 → path
138+
"frac_net_dominates": float(np.mean(np.asarray(npmp) > 0)),
139+
"spearman_dclean_vs_net_minus_path_IDENTITY": _spearman(
140+
npmp, dclean
141+
), # ≈+1 by construction
142+
"spearman_dclean_vs_rel_net": _spearman(rel_net, dclean),
143+
"spearman_dclean_vs_rel_path": _spearman(rel_path, dclean),
144+
}
145+
146+
92147
# --- per-cell descriptive mechanics (PLAN P2/P3; NO verdict) -----------------------------------
93148

94149

@@ -162,6 +217,9 @@ def run_mechanics_cell(timeframe: str, cfg: SelectionConfig, settings: Any) -> d
162217
_spearman([float(p[0]) for p in pairs], [float(p[1]) for p in pairs]) if pairs else None
163218
)
164219

220+
# FLIP — net-vs-path channel decomposition explaining the 4H↔1D snapping sign flip
221+
flip = _flip_decomposition(rows, df["close"].to_numpy())
222+
165223
return {
166224
"timeframe": timeframe,
167225
"k": cfg.k,
@@ -171,6 +229,7 @@ def run_mechanics_cell(timeframe: str, cfg: SelectionConfig, settings: Any) -> d
171229
"M1_size_length_confound": m1,
172230
"M3_snap_span_delta_asymmetry": m3, # headline descriptive object (PLAN P3)
173231
"M2_span_vs_cleanliness_spearman_PARTLY_ARITHMETIC": m2_spearman,
232+
"FLIP_net_path_decomposition": flip, # why snapping->cleanliness flips sign 4H<->1D
174233
"note": "DESCRIPTIVE ONLY — no verdict/claim; artifact-probe reading unchanged (PLAN P4)",
175234
}
176235

@@ -226,6 +285,12 @@ def print_mechanics(report: dict, path: Any) -> None:
226285
print(
227286
f" M2(arithmetic) spearman={r['M2_span_vs_cleanliness_spearman_PARTLY_ARITHMETIC']}"
228287
)
288+
fl = r["FLIP_net_path_decomposition"]
289+
print(
290+
f" FLIP moved={fl.get('n_moved')}/{fl.get('n_total')} "
291+
f"rel_net={fl.get('median_rel_net')} rel_path={fl.get('median_rel_path')} "
292+
f"frac_net_dom={fl.get('frac_net_dominates')}"
293+
)
229294
print(f"DESCRIPTIVE-ONLY (no verdict) summary={path}")
230295

231296

tests/research/test_selection_learning_artifact_mechanics.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from fibengine.research import selection_learning_artifact_mechanics as mech
1515

1616

17-
def _row(reached, exact, span, mag=None, snap_delta=None, snapped=None, q="2020Q1"):
17+
def _row(
18+
reached, exact, span, mag=None, snap_delta=None, snapped=None, q="2020Q1", sa=None, sb=None
19+
):
1820
return art.ArtifactRow(
1921
quarter=q,
2022
pos_a=0,
@@ -25,6 +27,8 @@ def _row(reached, exact, span, mag=None, snap_delta=None, snapped=None, q="2020Q
2527
span_bars=span,
2628
magnitude_atr=mag,
2729
snap_span_delta=snap_delta,
30+
snap_a_idx=sa,
31+
snap_b_idx=sb,
2832
)
2933

3034

@@ -50,12 +54,53 @@ def test_median_iqr_and_empty():
5054
assert mech._median_iqr([None, None]) is None
5155

5256

57+
def test_net_path_idx():
58+
closes = np.array([100.0, 110.0, 105.0, 115.0])
59+
net, path = mech._net_path_idx(closes, 0, 3)
60+
assert net == pytest.approx(15.0) and path == pytest.approx(25.0) # |15|, 10+5+10
61+
assert mech._net_path_idx(closes, 2, 2) == (None, None) # <2 bars
62+
63+
64+
def test_flip_decomposition_net_vs_path_channel():
65+
# closes laid out in 3 regions so two snaps are PATH-dominated (clean down) and one is
66+
# NET-dominated (clean up) — the decomposition must recover that split.
67+
closes = np.array([100.0, 110.0, 108.0, 100.0, 130.0, 128.0, 130.0, 100.0, 110.0, 107.0])
68+
rows = [
69+
# A: extend [0,1]->[0,2] adds path -> clean down (path-dominated)
70+
_row(True, 1.0, span=1, snapped=8.0 / 12.0, sa=0, sb=2),
71+
# B: shrink [3,6]->[3,4] removes a wiggle -> clean up (net-dominated)
72+
_row(True, 30.0 / 34.0, span=3, snapped=1.0, sa=3, sb=4, q="2020Q2"),
73+
# C: extend [7,8]->[7,9] adds path -> clean down (path-dominated)
74+
_row(True, 1.0, span=1, snapped=7.0 / 13.0, sa=7, sb=9, q="2020Q3"),
75+
]
76+
# row B's pos_a/pos_b must point at region B (3,6); _row uses pos_a=0,pos_b=span, so fix span=3
77+
rows[1].pos_a, rows[1].pos_b = 3, 6
78+
rows[2].pos_a, rows[2].pos_b = 7, 8
79+
f = mech._flip_decomposition(rows, closes)
80+
assert f["n_moved"] == 3 and f["n_total"] == 3
81+
# the d_clean <-> (rel_net - rel_path) link is the arithmetic identity → Spearman ≈ +1
82+
assert f["spearman_dclean_vs_net_minus_path_IDENTITY"] == pytest.approx(1.0)
83+
# two of three snaps are path-dominated (net_minus_path < 0) → overall path-dominated, frac=1/3
84+
assert f["median_net_minus_path"] < 0
85+
assert f["frac_net_dominates"] == pytest.approx(1.0 / 3.0)
86+
assert f["median_rel_path"] > f["median_rel_net"]
87+
assert mech._flip_decomposition([], closes) == {"n_total": 0, "n_moved": 0}
88+
89+
5390
# --- per-cell descriptive mechanics (no verdict) ----------------------------------------------
5491

5592

5693
class _NonEmpty:
5794
empty = False
5895

96+
def __getitem__(self, key): # df["close"] → object with .to_numpy() (flip needs closes)
97+
return _Col()
98+
99+
100+
class _Col:
101+
def to_numpy(self):
102+
return np.zeros(0)
103+
59104

60105
def _patch_cell(monkeypatch, rows):
61106
class _D:

0 commit comments

Comments
 (0)