Skip to content

Commit cc42f8b

Browse files
committed
把两条筛选轴钉成一块,并把下方每层的偏移改成实测
滚动时看不到异常筛选:类型轴自己 claim 过 position: sticky,异常轴没有,于是 筛选过的会话滚下去之后,页面上没有任何一行还在说明「现在被筛过什么」。 - `.filter-sticky` 把类型轴和异常轴包成一个钉在 top: 0 的不透明块。它们回答 同一个问题(账本显示哪些记录),分开钉就会出现「类型轴还在、异常轴滚走了」 的半截状态,比完全不钉更糟。 - 下方三层的偏移不再写死。原来的 0–32 / 32–83 只在「总览条上方只有一行筛选轴」 的布局里成立,而异常轴会随窄屏换行、提示行的出现和消失改变高度,任何常量都会 悄悄变成重叠。`syncStickyOffsets()` 在 `render()` 末尾量三个块,写进 `--sticky-band-h` / `--sticky-overview-h` / `--sticky-window-h`,样式表只读变量。 - 轮头从 `top: 0; z-index: 2` 移到实测偏移下方。它原先压在总览刻度和类型轴按钮 上,两处都读不出来;z-index 6 的筛选带盖住它只是把重叠藏起来,不是修好。 守卫:`verify-sticky.mjs`(20 条断言)把 syncStickyOffsets() 拿 137 / 65 / 57 的 stub 高度跑一遍——文件里没有任何常量等于这三个数,所以「写数字而不是量」的版本 过不去,把 band 量成 strip 的版本也过不去。`mutate-sticky.py` 18 条变异逐条要求 它以正确的名字失败,其中三条是补了守卫才有的:样式表读到的变量集合必须等于测量 写出的集合(单向都不够)、被测量的块必须真的绑定到 markup 里存在的 id(stub 永远 喂得起函数,抓不到这种静默回退)、以及任何钉在账本之上的层都必须有不透明背景 (账本行是 `background: transparent`)。 测试 22 → 24 个守卫,全绿。
1 parent 7a92cef commit cc42f8b

5 files changed

Lines changed: 592 additions & 13 deletions

File tree

‎plugins/avatasia/mmc-trajectory/miniapp/client/index.html‎

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -831,16 +831,32 @@
831831
on. The reference styles the glyph rather than the button, so the button stays
832832
flat when pressed and only the mark changes. */
833833

834-
.traj-controls {
834+
/* One sticky band for both filter axes.
835+
836+
The type row and the 异常 row decide the same question — which records the ledger shows —
837+
so they are pinned as a single block rather than one of them following the other out of
838+
view. Only the type row claimed `position: sticky` before, and it did not survive: with
839+
the ledger below it and both rows scrolled away, a session filtered down to a few hundred
840+
rows gave no sign that anything was filtered at all. The band carries its own background
841+
because it now sits over the ledger, which is opaque underneath it.
842+
843+
Everything pinned below this measures its offset from it rather than assuming it — see
844+
syncStickyOffsets(). The anomaly row wraps at narrow widths and the note under it comes
845+
and goes, so its height is not a number anyone can write down once. */
846+
.filter-sticky {
835847
position: sticky;
836848
top: 0;
837-
z-index: 5;
849+
z-index: 6;
850+
background: var(--mcode-bg);
851+
border-bottom: 1px solid var(--mcode-border);
852+
}
853+
854+
.traj-controls {
838855
display: flex;
839856
flex: none;
840857
align-items: center;
841858
padding: 6px 16px;
842859
background: var(--mcode-bg);
843-
border-bottom: 1px solid var(--mcode-border);
844860
}
845861

846862
.traj-controls-actions {
@@ -941,7 +957,7 @@
941957

942958
.overview {
943959
position: sticky;
944-
top: 32px;
960+
top: var(--sticky-band-h, 32px);
945961
z-index: 4;
946962
border-bottom: 1px solid var(--mcode-border);
947963
user-select: none;
@@ -1207,7 +1223,11 @@
12071223

12081224
.turn-head {
12091225
position: sticky;
1210-
top: 0;
1226+
/* Below every band above it, measured. It used to pin at 0 with z-index 2 and simply got
1227+
drawn over — its turn title and its kind summary landed on the type row's buttons and on
1228+
the overview's tick labels, and neither could be read. */
1229+
top: calc(var(--sticky-band-h, 32px) + var(--sticky-overview-h, 51px)
1230+
+ var(--sticky-window-h, 45px));
12111231
z-index: 2;
12121232
display: flex;
12131233
align-items: center;
@@ -1586,11 +1606,12 @@
15861606

15871607
.window-bar {
15881608
position: sticky;
1589-
/* Sticky offsets stack here: the type row owns 0–32 and the overview strip owns
1590-
32–83 (32 + the 50px plot + its 1px border), so a bar pinned at 0 would simply
1591-
hide behind both of them. z-index 3 keeps it under the overview (4) and over the
1592-
turn head (2). */
1593-
top: 83px;
1609+
/* Sticky offsets stack here, and every one of them below the filter band is measured
1610+
rather than written down: the band is the two filter rows (whose height changes when
1611+
异常 wraps or when its note appears) plus the overview strip. The old numbers — 0–32 and
1612+
32–83 — were correct for a layout where the 异常 row was not in the band, and they are
1613+
exactly the kind of constant that goes stale silently. syncStickyOffsets() owns them. */
1614+
top: calc(var(--sticky-band-h, 32px) + var(--sticky-overview-h, 51px));
15941615
z-index: 3;
15951616
padding: 8px 16px;
15961617
background: var(--mcode-bg);
@@ -2058,7 +2079,8 @@ <h2 class="section-title" id="kpi-heading">关键指标</h2>
20582079
<div class="kpi-grid" id="kpi-grid"></div>
20592080
</section>
20602081

2061-
<div class="traj-controls" id="kind-controls" role="toolbar" aria-label="按类型筛选轨迹" aria-controls="overview" data-filter-scope>
2082+
<div class="filter-sticky" id="filter-sticky">
2083+
<div class="traj-controls" id="kind-controls" role="toolbar" aria-label="按类型筛选轨迹" aria-controls="overview" data-filter-scope>
20622084
<nav class="traj-controls-actions" aria-label="按类型筛选轨迹">
20632085
<button type="button" class="tbtn tbtn-action tbtn-all" id="kind-all" aria-pressed="true"
20642086
aria-label="当前显示全部类型 · 点击只看空白" title="当前显示全部类型 · 点击只看空白">
@@ -2136,6 +2158,7 @@ <h2 class="section-title" id="kpi-heading">关键指标</h2>
21362158
<div class="anomaly-quick" id="anomaly-controls" role="group" aria-labelledby="anomaly-label"></div>
21372159
<span class="anomaly-bar-note" id="anomaly-mode-note" hidden></span>
21382160
</div>
2161+
</div>
21392162

21402163
<section class="overview" id="overview" aria-labelledby="overview-heading">
21412164
<h2 class="sr-only" id="overview-heading">轨迹总览</h2>
@@ -3124,6 +3147,7 @@ <h2 class="section-title" id="inspector-heading">检视</h2>
31243147
dom.staleBar = document.getElementById('stale-bar');
31253148
dom.toolbar = document.getElementById('toolbar');
31263149
dom.kindControls = document.getElementById('kind-controls');
3150+
dom.filterSticky = document.getElementById('filter-sticky');
31273151
dom.sessionSelect = document.getElementById('session-select');
31283152
dom.generationField = document.getElementById('generation-field');
31293153
dom.generationSelect = document.getElementById('generation-select');
@@ -3139,6 +3163,7 @@ <h2 class="section-title" id="inspector-heading">检视</h2>
31393163
dom.anomalyNote = document.getElementById('anomaly-mode-note');
31403164
dom.anomalyMulti = document.getElementById('anomaly-multi');
31413165
dom.windowBar = document.getElementById('window-bar');
3166+
dom.overview = document.getElementById('overview');
31423167
dom.headerToggle = document.getElementById('header-toggle');
31433168
dom.kpiToggle = document.getElementById('kpi-toggle');
31443169
dom.kpiGrid = document.getElementById('kpi-grid');
@@ -6091,6 +6116,28 @@ <h2 class="section-title" id="inspector-heading">检视</h2>
60916116

60926117
/* ---------------- top-level render ---------------- */
60936118

6119+
/**
6120+
* Publishes the heights of the pinned bands so everything below them can be positioned from
6121+
* a measurement instead of a number written down once.
6122+
*
6123+
* The alternative was three constants — 32 for the type row, 51 for the overview strip, 45
6124+
* for the window bar — and they were already wrong the moment the 异常 row joined the band:
6125+
* that row wraps at narrow widths and grows a line when its note appears, so no constant can
6126+
* stay right. This runs at the end of `render()`, after the note has been written and the
6127+
* buttons re-counted, which is the only moment the band's height is final. `offsetHeight`
6128+
* rather than `getBoundingClientRect().height`, so a fractional device-pixel ratio cannot
6129+
* make the bands overlap by a third of a pixel per poll.
6130+
*/
6131+
function syncStickyOffsets() {
6132+
if (!dom.filterSticky || !dom.overview || !dom.windowBar) return;
6133+
var root = document.documentElement.style;
6134+
var band = dom.filterSticky.offsetHeight;
6135+
var overview = dom.overview.offsetHeight;
6136+
root.setProperty('--sticky-band-h', band + 'px');
6137+
root.setProperty('--sticky-overview-h', overview + 'px');
6138+
root.setProperty('--sticky-window-h', dom.windowBar.offsetHeight + 'px');
6139+
}
6140+
60946141
function render() {
60956142
renderBars();
60966143
renderGeneration();
@@ -6123,6 +6170,10 @@ <h2 class="section-title" id="inspector-heading">检视</h2>
61236170
renderInspector();
61246171
renderFooter();
61256172
updateHandoffButton();
6173+
// Last: it reads the bands the lines above just changed. The 异常 note appears and
6174+
// disappears here, so measuring before this point would pin the bands to yesterday's
6175+
// height and leave the window bar overlapping the strip above it.
6176+
syncStickyOffsets();
61266177
}
61276178

61286179
function renderGeneration() {

‎plugins/avatasia/mmc-trajectory/tests/METHODOLOGY.md‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,57 @@ Two guards hold this: the exact call sequence, and that `render()` contains exac
219219
220220
---
221221
222-
## 8. Say what you did not manage to check
222+
## 8. Measure the layout; never write it down
223+
224+
A sticky offset is a measurement, not a constant. This file carried `top: 32px` and `top: 83px`
225+
with a comment explaining them, and both were correct only while the layout had exactly one filter
226+
row above the strip. Adding a second filter row — one that wraps at narrow widths and grows a line
227+
when its note appears — invalidated them silently: nothing overlaps at the width the comment was
228+
written at, and at 735px the window bar sits on top of the strip.
229+
230+
```css
231+
/* wrong: correct for one layout, and there is no way to tell from reading it */
232+
.overview { top: 32px; }
233+
.window-bar { top: 83px; }
234+
```
235+
236+
```js
237+
function syncStickyOffsets() {
238+
if (!dom.filterSticky || !dom.overview || !dom.windowBar) return;
239+
var root = document.documentElement.style;
240+
var band = dom.filterSticky.offsetHeight;
241+
var overview = dom.overview.offsetHeight;
242+
root.setProperty('--sticky-band-h', band + 'px');
243+
root.setProperty('--sticky-overview-h', overview + 'px');
244+
root.setProperty('--sticky-window-h', dom.windowBar.offsetHeight + 'px');
245+
}
246+
```
247+
248+
`offsetHeight`, not `getBoundingClientRect().height`: this runs on every 2.5s poll, and a
249+
fractional device-pixel ratio would otherwise accumulate a third of a pixel per poll into an
250+
overlap nobody can see and everybody inherits.
251+
252+
Three rules make that hold, and each of them needed a mutation before it was believed:
253+
254+
- **Pin what answers one question as one block.** The two filter rows were pinned separately, which
255+
allowed the state where the type row is still on screen and the 异常 row is not. Half a filter
256+
visible is worse than none: the reader is told nothing is filtered.
257+
- **The set of variables the stylesheet reads must equal the set the measurement writes.** One
258+
direction lets a fallback govern a band that nothing measures; the other lets the measurement
259+
keep writing a variable nobody reads, which is what "the offsets went back to being constants"
260+
looks like from the outside.
261+
- **Everything pinned over the ledger paints a background.** The rows underneath are
262+
`background: transparent`, so a pinned band that stops covering them shows them through.
263+
264+
And the general form of the third one: **an element named in markup and an element named in a
265+
selector are two names, and nothing checks that they agree.** A band looked up by an id the markup
266+
does not ship measures nothing, all three offsets fall back to their constants, and no error is
267+
raised anywhere. A guard that hands the function a stub cannot catch this — the stub supplies
268+
exactly what the function asked for — so the binding is checked against the markup instead.
269+
270+
---
271+
272+
## 9. Say what you did not manage to check
223273
224274
Some things cannot be driven through the browser harness. The honest form is the useful one:
225275
@@ -245,6 +295,9 @@ A harness limitation reported as a limitation is useful. The same limitation sil
245295
[ ] prose assertions are produced by running the function, numbers parsed back out of the string
246296
[ ] no assertion name contains a colon
247297
[ ] everything one reader can compare came from one snapshot
298+
[ ] no layout offset is written down; anything pinned is measured, and measured at the end of render
299+
[ ] every variable the stylesheet reads is written by the measurement, and one written is one read
300+
[ ] every element the measurement reads is bound to an id the markup actually ships
248301
[ ] anything not verified is written down as not verified
249302
```
250303

‎plugins/avatasia/mmc-trajectory/tests/README.md‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ none of them, and the guards then fail for want of something to find.
9494
| `verify-prompt-fix.mjs` | injected-block splitting via `canonicalTextRange` |
9595
| `verify-window-widen.mjs` | `向上加载更早的 N 条` survives the next poll (self-contained: the shipped rule is transcribed) |
9696
| `verify-anomaly.mjs` | the four 异常 rules, their units, their position on the page, and their exclusivity with the other axis |
97+
| `verify-sticky.mjs` | both filter rows pin as one band, and every band below it is measured rather than written down |
9798
| `verify-block-order.mjs` | assistant blocks are ordered per message: thinking → text → toolCall |
9899
| `verify-response-rail.mjs` | every row carries the rail of the response it came from |
99100
| `verify-call-fold-coverage.mjs` | folding every tool call really removes them from the rendered build |
@@ -105,9 +106,10 @@ none of them, and the guards then fail for want of something to find.
105106
| `mutate-turn-breakdown.py` | the turn breakdown parts sum to the headline |
106107
| `mutate-compaction-turn.py` | the compaction-turn attribution is not a free-text label |
107108
| `mutate-pr15-fixes.py` | each of the six PR #15 fixes has a guard that can fail |
108-
| `mutate-anomaly.py` | each of the 50 异常 mutations, in the page, has a guard that can fail |
109+
| `mutate-anomaly.py` | each of the 70 异常 mutations, in the page, has a guard that can fail |
109110
| `mutate-steer.py` | the steer split and the turn classification hold |
110111
| `mutate-window-default.py` | the window defaults, labels, resets and load-more hold |
112+
| `mutate-sticky.py` | each of the 18 pinned-band rules has a guard that can fail |
111113

112114
## The 异常 axis, and what it promises
113115

@@ -150,6 +152,41 @@ be a field of the search form any more, and the KPI cards must not come between
150152
to be the last field of the form, four cards above the axis it reads as part of — a layout defect
151153
that no functional assertion in the suite could see.
152154

155+
## The pinned bands, and why none of them carries a number
156+
157+
Four things pin themselves while the ledger scrolls under them: the filter band (both axes), the
158+
overview strip, the window bar, and the turn head. Three properties are asserted, and each one was
159+
false in this file's own history.
160+
161+
1. **The two filter rows pin as one block.** They answer the same question — which records the
162+
ledger shows — so a state where the type row is still pinned and the 异常 row has scrolled away
163+
is the worst of both: the reader is told nothing is filtered while half the filter is off
164+
screen. Only the type row ever claimed `position: sticky` and it did not survive; the guard
165+
now asserts that neither row pins itself and that both are inside the band, because a band that
166+
does not contain both is the same defect wearing a different hat.
167+
2. **No pinned offset is written down.** The band is two rows whose height changes when the 异常 row
168+
wraps at narrow widths and when its note appears or disappears, so `0–32` and `32–83` were
169+
correct only for a layout without the 异常 row. `syncStickyOffsets()` measures the three bands at
170+
the end of `render()` and writes them into `--sticky-band-h`, `--sticky-overview-h` and
171+
`--sticky-window-h`; the stylesheet only ever reads those. The guard runs that function against
172+
stub heights of 137 / 65 / 57 — no constant in the file is any of them — so a version that
173+
writes a number instead of a measurement fails, and so does one that measures the band for the
174+
strip.
175+
3. **Anything pinned over the ledger has to paint a background.** The rows underneath are
176+
`background: transparent`, so a pinned band that stops covering them shows them through. The
177+
strip paints through `.overview-plot` rather than through the sticky section, which is why the
178+
guard names that rule and not `.overview`.
179+
180+
Two more assertions exist because the mutation control found them missing: the set of variables the
181+
stylesheet reads has to be **exactly** the set the measurement writes (one side alone lets a stale
182+
fallback or a dead measurement through), and every band the function reads has to be bound to an
183+
element the markup actually ships. A band that is never bound measures nothing, all three offsets
184+
fall back to their constants, and no error is reported anywhere — the stub the guard measures with
185+
cannot catch it, so the binding is checked against the markup instead.
186+
187+
The order of `render()` is asserted too: the measurement has to be the last statement, because the
188+
异常 note is written by `syncAnomalyMode()` and that note is what changes the band's height.
189+
153190
## Deliberately not in the default run
154191

155192
Shipping a suite that is red teaches reviewers to ignore it, so these are present but skipped,

0 commit comments

Comments
 (0)