My coalescing broke the echo guard it ran past
rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event,
identically on both engines. The grid stayed narrowed only because the
composite's 1.2 second push window happened to swallow the second one — and a
pass that depends on a swallow is not a pass. rega said so, and was right to
refuse to score it green.
THE CAUSE IS MINE AND IT IS NOT A TIMING QUIRK. emitFilterState has always opened
with `if (applyingExternal) return`: when the HOST pushes state DOWN through
externalFilters the store changes, and reporting that back up is an echo of the
host's own instruction. That guard worked because the emit ran SYNCHRONOUSLY
inside the watcher, while the flag was still set. My coalescing moved the emit
60ms later, and applyingExternal is released on nextTick — so by the time the
emit ran the flag was false and the echo went out.
I fixed a burst and broke a guard that depended on the burst's timing. Changing
WHEN something runs is a change to everything that depended on when, and I did
not re-read what else was standing on that synchrony.
So the decision moves to schedule time, where the flag still means what it says:
a change that arrived from the host is not a change the reader made, and is not
reported back. The check inside emitFilterState stays as a second line of
defence.
This is the same shape as the dialect case on the branch below, and it is why
that one is a guard rather than a longer timeout: both are "the emit is no longer
where it thought it was".
★ AND THE GATE CAUGHT THE SAME HOLE IN ITSELF FOR THE THIRD TIME. The assertion
searched the scheduler for `if (applyingExternal) return`, and the comment
explaining the fix QUOTES that line — so deleting the code left the gate green.
It strips comments before asserting now. A check its own documentation can
satisfy is not a check, and I have now written that mistake three different ways:
a probe that yields zero when its assumption is wrong, a mutation aimed at one of
three identical bindings, and this.
Source only, stacked on sr/dialect-empty-cascade. STUDIO: rebuild
doxa-research-map (the same rebuild that branch already needs).
This commit is contained in:
parent
dffae0502c
commit
8ec207c662
2 changed files with 37 additions and 0 deletions
|
|
@ -2321,6 +2321,23 @@ let _emptyRetries = 0
|
|||
// first load, or after a clear that has already been reported, empty is simply the truth.
|
||||
let _lastEmitWasFilled = false
|
||||
function emitFilterStateSettled() {
|
||||
// ★ THE ECHO GUARD MUST BE READ WHEN THE CHANGE HAPPENS, NOT WHEN THE TIMER FIRES.
|
||||
// rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event, on both
|
||||
// engines. It passed only because the composite's 1.2s push window happened to swallow the
|
||||
// second one — a pass that depends on a swallow is not a pass.
|
||||
//
|
||||
// THE CAUSE IS MINE, AND IT IS THE COALESCING I ADDED. emitFilterState has always opened with
|
||||
// `if (applyingExternal) return`: when the HOST pushes state down through externalFilters, the
|
||||
// store changes, and reporting that back up is an echo of the host's own instruction. That guard
|
||||
// worked because the emit ran SYNCHRONOUSLY inside the watcher, while the flag was still set.
|
||||
// Debouncing moved the emit 60ms later — and applyingExternal is released on nextTick, long
|
||||
// before that — so by the time the emit ran the flag was false and the echo went out. I fixed a
|
||||
// burst and broke a guard that depended on the burst's timing, which is the cost of changing WHEN
|
||||
// something runs without re-reading what else depended on when.
|
||||
//
|
||||
// So the decision is taken here, at schedule time, where the flag still means what it says: a
|
||||
// change that arrived from the host is not a change the reader made, and is not reported back.
|
||||
if (applyingExternal) return
|
||||
_emptyRetries = 0 // a fresh user action restarts the budget
|
||||
clearTimeout(_emitTimer)
|
||||
_emitTimer = setTimeout(emitFilterState, EMIT_SETTLE_MS)
|
||||
|
|
|
|||
|
|
@ -278,6 +278,26 @@ if (matchesLanguage) {
|
|||
} else notes.push('empty guard: a mid-cascade read is re-taken, a real clear still gets through')
|
||||
}
|
||||
|
||||
// ── H. the echo guard survives the coalescing ────────────────────────────────────────────────
|
||||
// rega, row 50: a region click emitted the selection and then an EMPTY event, passing only because
|
||||
// the composite's 1.2s push window swallowed the second. emitFilterState has always opened with
|
||||
// `if (applyingExternal) return` — a change pushed DOWN by the host is not a change to report back
|
||||
// up — and that worked while the emit ran synchronously inside the watcher. Debouncing moved the
|
||||
// emit 60ms later while applyingExternal is released on nextTick, so the flag was already false
|
||||
// when the emit ran and the echo went out. The guard has to be read where the change happens.
|
||||
{
|
||||
const src = emitterSrc
|
||||
const sched = /function emitFilterStateSettled\(\) \{([\s\S]*?)\n\}/.exec(src)
|
||||
if (!sched) fails.push('H research-map.vue: emitFilterStateSettled() not found — this gate cannot see whether the echo guard survives the debounce.')
|
||||
// ★ COMMENTS STRIPPED FIRST, and this is the THIRD time the same hole has appeared in my own
|
||||
// checks. The body below explains the fix in prose that QUOTES the line it is asserting, so a
|
||||
// naive search matched the comment and stayed green after the code was deleted. A check that its
|
||||
// own documentation can satisfy is not a check; only executable lines count.
|
||||
else if (!/if \(applyingExternal\) return/.test(sched[1].split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n'))) {
|
||||
fails.push('H the settle scheduler does not check applyingExternal. The check inside emitFilterState runs 60ms later, and applyingExternal is released on nextTick — so a change the HOST pushed down is reported back up as if the reader had made it. That is an echo, and it reaches the composite as an empty report.')
|
||||
} else notes.push('echo guard: read at schedule time, where applyingExternal still means what it says')
|
||||
}
|
||||
|
||||
for (const n of notes) console.log(' · ' + n)
|
||||
if (fails.length) {
|
||||
console.error(`\n${TAG}: FAIL`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue