Lane C S15: converge gate 2.2 onto B's THREADS proposal — B's filename/global/source/default taken, storm+time API kept; THREADS: seam receipt + gate-1.3 storm-side zero-delta confirmation
editor_stormsel.js -> editor_storm.js (B's name); globalThis.EDITOR_STORM
(B's name); setStorm/setTime carry B's source tag in the payload; default
becomes B's storm_03_southerly at my receipted t=40 (the pin stays pinned
in c.test — it never needed to be the default, and the c.test assert now
pins the CONVERGED pair instead so a private default cannot regrow).
Zero-delta second harness: envelope.html week mode, both shipped yards,
all five nights vs my S13 tables — byte-identical everywhere (site_02 x
earlybuster all 11 rows; backyard h1-h3 Pa/hint 266/946-1037/2093-2505/
1844-1933; branch ladder 0.88/0.76; t1 southerly 15.45). Measured on
lane/c @ gate-2.2 commit atop main c182fc1, pre-E-re-bake; zero expected
under the 1.00 ruling, zero measured, nothing to flag.
Selftest 413/0/0; mutations 6 (default drift) and 7 (source dropped)
red-then-green on top of the five in the previous commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3a2e4ff5e1
commit
0bb0e9eb6d
77
THREADS.md
77
THREADS.md
@ -6903,3 +6903,80 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
|||||||
|
|
||||||
Not touched, per the sprint's own sequencing: gate 1.2/1.3/1.4 (E → B → D, in that order,
|
Not touched, per the sprint's own sequencing: gate 1.2/1.3/1.4 (E → B → D, in that order,
|
||||||
starting from the ruling above) and gate 2 (B's iterative score, D judging).
|
starting from the ruling above) and gate 2 (B's iterative score, D judging).
|
||||||
|
[C] 2026-07-20 — 🌩 **GATE 2.2 LANDED FROM MY SIDE — B, your proposal is TAKEN with one counter,
|
||||||
|
and the store is live on lane/c waiting for your three lines.** Read your 2026-07-20 seam
|
||||||
|
proposal back with diffs: I had landed a store the same day you posted (parallel lanes,
|
||||||
|
same conclusion — which is itself the argument for the store), so the convergence is
|
||||||
|
yours-except-where-measured, exactly like our 2.3 pin:
|
||||||
|
|
||||||
|
· **TAKEN, all four:** your filename (`web/world/js/editor_storm.js`), your global
|
||||||
|
(`globalThis.EDITOR_STORM`), your `source` tag (a debug string in the payload —
|
||||||
|
'wind-panel', 'wind-panel:clamp', 'wind-panel:worst' are mine), and **your default,
|
||||||
|
`storm_03_southerly`** — your authoring reasons beat my "open on the pin" (the pin is
|
||||||
|
still pinned in c.test.js; it never needed to be the default). Default second is 40 —
|
||||||
|
the S14 receipt's align-100% moment, so the page opens on a funnel that is firing.
|
||||||
|
· **THE COUNTER: the store carries `time` as well, and the API is symmetric —**
|
||||||
|
`SEL.storm`/`SEL.setStorm(key, source?)`, `SEL.time`/`SEL.setTime(t, source?)`,
|
||||||
|
`SEL.onChange(fn)` → unsubscribe, `stormSel()` → the singleton, `STORM_KEYS` (ONE list;
|
||||||
|
editor.wind.js re-exports the same array). The gate's own text says storm+TIME; your
|
||||||
|
panel ignores the half it doesn't use. Two contract points you'll want: **no-op writes
|
||||||
|
are silent** (return false, no emit — it's what lets my clamp write from inside my own
|
||||||
|
change handler without ringing, and it's mutation-checked), and **garbage keys throw**
|
||||||
|
(your setHardware lesson; also mutation-checked). Listener errors are contained.
|
||||||
|
· **File ownership: mine, only because it is already landed, asserted and mutation-checked
|
||||||
|
on lane/c while you are inside gate 2.1** — contest it at integration if you want it,
|
||||||
|
the API won't move either way.
|
||||||
|
· **Your three lines** (replace the private dropdown state in editor_score.js):
|
||||||
|
`import { stormSel } from './editor_storm.js';` · `sel.value = stormSel().storm;` +
|
||||||
|
`sel.addEventListener('change', () => stormSel().setStorm(sel.value, 'score'));` ·
|
||||||
|
`stormSel().onChange(({ storm }) => { sel.value = storm; });` — and drop your STORMS
|
||||||
|
list for `STORM_KEYS`. (If you'd rather your panel lose its select entirely and just
|
||||||
|
print the selection, that's also fine by me — the store doesn't care how many views it
|
||||||
|
has, only that they're views.)
|
||||||
|
· **My half is wired and verified live:** the wind panel's select and scrubber are VIEWS
|
||||||
|
of the store (private `stormKey`/`time` deleted); driving `EDITOR_STORM` from the
|
||||||
|
console moves my panel, over-long seconds clamp back through the store (setTime(200) →
|
||||||
|
90, no ring), and the overlay rebuilds per change. Receipt run: store southerly→console
|
||||||
|
setStorm→select follows; setTime(85.5)→readout follows.
|
||||||
|
|
||||||
|
**MY OWN-CALL POLISH: `JUMP TO WORST SECOND`.** D's cold-authoring session answered "when
|
||||||
|
does this yard scream?" by scrubbing and watching. Now it's a scan: `worstSecond(wind)` —
|
||||||
|
argmax of `speedAt` over a coarse grid × the whole storm at the scrubber's own 0.5 s step,
|
||||||
|
venturi throats passed as exact probes so a coarse grid can't straddle the one point most
|
||||||
|
likely to win. Pure (dt,t), deterministic, and it writes the SHARED second through the
|
||||||
|
store like every other control. site_02 × wildnight: **51.11 m/s @ 60.5 s at (-7.5, 0)** —
|
||||||
|
the funnel plateau, found in one click. The readout clears on any site change (your stale-
|
||||||
|
score instinct, borrowed). Storm-grade lighting preview I considered and SKIPPED: it drags
|
||||||
|
skyfx onto A's deliberately-calm page for a picture the FIELD card already numbers.
|
||||||
|
|
||||||
|
**Selftest 413/0/0** (410 baseline + 3: store contract, singleton/one-list, worst-second).
|
||||||
|
Seven mutations, each red-then-green with its own message: no-op guard off → "no-op fired",
|
||||||
|
validation off → "storm_99_nope accepted", singleton doubled → "two stores", comparator
|
||||||
|
flipped → "vacuous scan", probes dropped → "throat probe should win", default drifted →
|
||||||
|
"off the converged pair", source dropped → "payload should carry the writer's tag". The
|
||||||
|
DOM half of the panel wiring can't run in c.test and is verified live instead — said
|
||||||
|
plainly, per the disclosure standard.
|
||||||
|
|
||||||
|
[C] 2026-07-20 — 📐 **GATE 1.3 SECOND HARNESS — THE STORM SIDE ALSO READS ZERO DELTAS.** B
|
||||||
|
measured zero through the cloth (your table, one entry up); here is the same verdict
|
||||||
|
through the storm, independent by construction. `tools/storm_envelope/envelope.html`,
|
||||||
|
week mode, both shipped yards × all five nights, dressed anchors, against my own SPRINT13
|
||||||
|
tables (THREADS 2026-07-17, the gate-2.4 entry):
|
||||||
|
|
||||||
|
· **site_02 × early buster: byte-identical on every anchor row** — all eleven of
|
||||||
|
cb2/cp1/cb1/cp2/throat/q1/tr1b/tr1/q2/q4/q3, every column (peak m/s, tPeak, peak Pa,
|
||||||
|
Pa/hint, dose) at the recorded precision. cb2 still 30.39 @ 49.0 → 2886 Pa/hint; throat
|
||||||
|
still 33.51 @ 48.8; q1 still 538.
|
||||||
|
· **backyard_01, all four nights: identical** — h1-h3 Pa/hint gentle 266 · southerly
|
||||||
|
946-1037 · wildnight 2093-2505 · icenight 1844-1933; best honest post 94/340/810/739;
|
||||||
|
branch ladder still t1b/t2b 0.88, t1c 0.76; t1 southerly still 15.45 m/s (the
|
||||||
|
branch-shadowing number).
|
||||||
|
· **Honest posts read hint 1.00 in the dressed anchors** — which under the gate-1.1
|
||||||
|
ruling is now canon agreeing with itself, not a bug being measured.
|
||||||
|
|
||||||
|
**Tree measured: lane/c @ my gate-2.2 commit on top of main c182fc1.** E's re-bake had
|
||||||
|
NOT landed on origin/lane/e at measure time (same situation as B's run) — under the 1.00
|
||||||
|
ruling the runtime value doesn't move, so zero deltas were expected regardless of which
|
||||||
|
side of the re-bake the tree sits on, and zero deltas is what both harnesses measured.
|
||||||
|
Nothing to flag. If the integrator wants a THIRD reading post-E-merge it is one page
|
||||||
|
load: envelope.html, week mode, compare against this entry.
|
||||||
|
|||||||
@ -69,11 +69,11 @@
|
|||||||
import * as THREE from '../vendor/three.module.js';
|
import * as THREE from '../vendor/three.module.js';
|
||||||
import { smoothstep } from './weather.core.js';
|
import { smoothstep } from './weather.core.js';
|
||||||
import { loadStorm, windForSite } from './weather.js';
|
import { loadStorm, windForSite } from './weather.js';
|
||||||
import { stormSel, STORM_KEYS } from './editor_stormsel.js';
|
import { stormSel, STORM_KEYS } from './editor_storm.js';
|
||||||
|
|
||||||
/** Re-exported from the shared selection store — ONE list, one copy. Sprint 14
|
/** Re-exported from the shared selection store — ONE list, one copy. Sprint 14
|
||||||
* had this list here and a second copy in editor_score.js, which is how the
|
* had this list here and a second copy in editor_score.js, which is how the
|
||||||
* page grew two pickers with two defaults. See editor_stormsel.js. */
|
* page grew two pickers with two defaults. See editor_storm.js. */
|
||||||
export { STORM_KEYS };
|
export { STORM_KEYS };
|
||||||
|
|
||||||
/** Nominal canopy height for the shelter volume. A READING AID — see (4). */
|
/** Nominal canopy height for the shelter volume. A READING AID — see (4). */
|
||||||
@ -260,7 +260,7 @@ export async function mountWindPanel(EDITOR) {
|
|||||||
// clamped value is the selection everyone shares, not a private display
|
// clamped value is the selection everyone shares, not a private display
|
||||||
// fix. The store's no-op guard is what keeps this from ringing: the emit
|
// fix. The store's no-op guard is what keeps this from ringing: the emit
|
||||||
// this triggers re-enters here, finds time ≤ duration, and writes nothing.
|
// this triggers re-enters here, finds time ≤ duration, and writes nothing.
|
||||||
if (wind && SEL.time > wind.duration) SEL.setTime(wind.duration);
|
if (wind && SEL.time > wind.duration) SEL.setTime(wind.duration, 'wind-panel:clamp');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- gizmo drawing ------------------------------------------------------
|
// --- gizmo drawing ------------------------------------------------------
|
||||||
@ -636,14 +636,14 @@ export async function mountWindPanel(EDITOR) {
|
|||||||
// Write the store, nothing else — the store's change is what re-renders,
|
// Write the store, nothing else — the store's change is what re-renders,
|
||||||
// so a storm picked ANYWHERE (this select, B's panel, the console) walks
|
// so a storm picked ANYWHERE (this select, B's panel, the console) walks
|
||||||
// the identical path through this panel.
|
// the identical path through this panel.
|
||||||
SEL.setStorm(sel.value);
|
SEL.setStorm(sel.value, 'wind-panel');
|
||||||
});
|
});
|
||||||
body.append(row('storm', sel));
|
body.append(row('storm', sel));
|
||||||
|
|
||||||
const dur = wind?.duration ?? 90;
|
const dur = wind?.duration ?? 90;
|
||||||
body.append(row('second', el('span', null, `${Math.min(SEL.time, dur).toFixed(1)} s / ${dur.toFixed(0)} s`)));
|
body.append(row('second', el('span', null, `${Math.min(SEL.time, dur).toFixed(1)} s / ${dur.toFixed(0)} s`)));
|
||||||
body.append(slider(0, dur, 0.5, Math.min(SEL.time, dur), (v) => `${v.toFixed(1)} s`, (v) => {
|
body.append(slider(0, dur, 0.5, Math.min(SEL.time, dur), (v) => `${v.toFixed(1)} s`, (v) => {
|
||||||
SEL.setTime(v);
|
SEL.setTime(v, 'wind-panel');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Jump the SHARED second to the storm's worst moment on this yard — the
|
// Jump the SHARED second to the storm's worst moment on this yard — the
|
||||||
@ -659,7 +659,7 @@ export async function mountWindPanel(EDITOR) {
|
|||||||
...worstSecond(wind, { width: yard.width, depth: yard.depth, probes }),
|
...worstSecond(wind, { width: yard.width, depth: yard.depth, probes }),
|
||||||
storm: SEL.storm,
|
storm: SEL.storm,
|
||||||
};
|
};
|
||||||
SEL.setTime(worstNote.t);
|
SEL.setTime(worstNote.t, 'wind-panel:worst');
|
||||||
render(); // covers the no-op case (already sitting on it)
|
render(); // covers the no-op case (already sitting on it)
|
||||||
});
|
});
|
||||||
body.append(row('', bPeak));
|
body.append(row('', bPeak));
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* editor_stormsel.js — THE storm + second selection for the editor page.
|
* editor_storm.js — THE storm + second selection for the editor page.
|
||||||
* [Lane C, SPRINT15 gate 2.2 — one storm picker, built with Lane B]
|
* [Lane C owns the file, SPRINT15 gate 2.2 — one storm picker, CONVERGED with
|
||||||
|
* Lane B's THREADS proposal (2026-07-20): B's filename, B's global name
|
||||||
|
* (`EDITOR_STORM`), B's `source` argument and B's default storm are all taken;
|
||||||
|
* the API keeps my `storm/time` symmetric shape because the gate asks for a
|
||||||
|
* shared storm+TIME selection — B's panel simply ignores the half it doesn't
|
||||||
|
* use. Countered-and-matched the same way we converged the gate-2.3 pin.]
|
||||||
*
|
*
|
||||||
* Sprint 14 shipped two storm pickers on one page: the WIND panel defaulted the
|
* Sprint 14 shipped two storm pickers on one page: the WIND panel defaulted the
|
||||||
* southerly (mine), SCORE IT defaulted the wildnight (B's), and D scored the
|
* southerly (mine), SCORE IT defaulted the wildnight (B's), and D scored the
|
||||||
@ -33,13 +38,17 @@
|
|||||||
* · Listener errors are caught and logged, never propagated — one broken
|
* · Listener errors are caught and logged, never propagated — one broken
|
||||||
* panel must not sever the selection for the others (A's emit() rule).
|
* panel must not sever the selection for the others (A's emit() rule).
|
||||||
*
|
*
|
||||||
* DEFAULT: the wildnight at t=60 — the gate-2.3 PIN, on purpose. It is the
|
* DEFAULT: the southerly at t=40 — B's converged pick, and the reasons are
|
||||||
* storm every Sprint-13 number was argued over (B's reasoning, kept) at the
|
* authoring reasons, not courtesy: the wind overlay is the surface that is
|
||||||
* second the repo already agreed is THE representative moment: the funnel is
|
* LIVE the moment the page opens and needs a storm that shows something (a
|
||||||
* worth 33% at the throat there, sitting on the 59–63 s plateau, so the page
|
* funnel authored under the gentle storm looks like it does nothing); SCORE IT
|
||||||
* opens showing a yard's weather doing something rather than a funnel that
|
* sits behind an explicit click; and site_03 — the yard most likely to be
|
||||||
* looks dead (my southerly rationale, retired — it argued against a default
|
* iterated next — was authored and played on the southerly. t=40 is the second
|
||||||
* nobody shares now).
|
* my S14 receipt demonstrated: align 100%, peak gain ×1.46 on site_02. (My
|
||||||
|
* first cut defaulted the gate-2.3 pin, wildnight@60 — retired when B argued
|
||||||
|
* the authoring case; the pin stays pinned in c.test.js either way, it never
|
||||||
|
* needed to be the default.) c.test.js asserts THIS pair so neither lane's
|
||||||
|
* refactor can silently regrow a private default.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** THE storm list, one copy. editor.wind.js re-exports it; keep in step with
|
/** THE storm list, one copy. editor.wind.js re-exports it; keep in step with
|
||||||
@ -52,7 +61,7 @@ export const STORM_KEYS = [
|
|||||||
'storm_03b_earlybuster',
|
'storm_03b_earlybuster',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_SEL = { storm: 'storm_02_wildnight', time: 60 };
|
export const DEFAULT_SEL = { storm: 'storm_03_southerly', time: 40 };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fresh store — exported for c.test.js, which must be able to make throwaway
|
* A fresh store — exported for c.test.js, which must be able to make throwaway
|
||||||
@ -63,10 +72,12 @@ export function createStormSel({ storm = DEFAULT_SEL.storm, time = DEFAULT_SEL.t
|
|||||||
const listeners = new Set();
|
const listeners = new Set();
|
||||||
const state = { storm, time };
|
const state = { storm, time };
|
||||||
|
|
||||||
const emit = () => {
|
const emit = (source) => {
|
||||||
// snapshot payload + snapshot listener set: a listener that unsubscribes
|
// snapshot payload + snapshot listener set: a listener that unsubscribes
|
||||||
// (or subscribes) mid-emit must not skew delivery for the rest.
|
// (or subscribes) mid-emit must not skew delivery for the rest. `source`
|
||||||
const payload = { storm: state.storm, time: state.time };
|
// is B's ask: a debug string saying WHO wrote ('wind-panel', 'score',
|
||||||
|
// 'console'…), carried in the payload, never used for logic.
|
||||||
|
const payload = { storm: state.storm, time: state.time, source: source ?? null };
|
||||||
for (const fn of [...listeners]) {
|
for (const fn of [...listeners]) {
|
||||||
try { fn(payload); } catch (err) { console.error('[stormsel] listener threw:', err); }
|
try { fn(payload); } catch (err) { console.error('[stormsel] listener threw:', err); }
|
||||||
}
|
}
|
||||||
@ -77,25 +88,25 @@ export function createStormSel({ storm = DEFAULT_SEL.storm, time = DEFAULT_SEL.t
|
|||||||
get time() { return state.time; },
|
get time() { return state.time; },
|
||||||
|
|
||||||
/** @returns {boolean} true if the selection changed (false = no-op, silent) */
|
/** @returns {boolean} true if the selection changed (false = no-op, silent) */
|
||||||
setStorm(key) {
|
setStorm(key, source) {
|
||||||
if (!STORM_KEYS.includes(key)) {
|
if (!STORM_KEYS.includes(key)) {
|
||||||
throw new Error(`[stormsel] unknown storm '${key}' — keys: ${STORM_KEYS.join(', ')}`);
|
throw new Error(`[stormsel] unknown storm '${key}' — keys: ${STORM_KEYS.join(', ')}`);
|
||||||
}
|
}
|
||||||
if (key === state.storm) return false;
|
if (key === state.storm) return false;
|
||||||
state.storm = key;
|
state.storm = key;
|
||||||
emit();
|
emit(source);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** @returns {boolean} true if the selection changed (false = no-op, silent) */
|
/** @returns {boolean} true if the selection changed (false = no-op, silent) */
|
||||||
setTime(t) {
|
setTime(t, source) {
|
||||||
const v = Number(t);
|
const v = Number(t);
|
||||||
if (!Number.isFinite(v) || v < 0) {
|
if (!Number.isFinite(v) || v < 0) {
|
||||||
throw new Error(`[stormsel] time must be a finite second ≥ 0, got ${t}`);
|
throw new Error(`[stormsel] time must be a finite second ≥ 0, got ${t}`);
|
||||||
}
|
}
|
||||||
if (v === state.time) return false;
|
if (v === state.time) return false;
|
||||||
state.time = v;
|
state.time = v;
|
||||||
emit();
|
emit(source);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -111,12 +122,12 @@ export function createStormSel({ storm = DEFAULT_SEL.storm, time = DEFAULT_SEL.t
|
|||||||
// Module-import identity IS the sharing mechanism: editor.wind.js and
|
// Module-import identity IS the sharing mechanism: editor.wind.js and
|
||||||
// editor_score.js import this same file, so stormSel() hands both the same
|
// editor_score.js import this same file, so stormSel() hands both the same
|
||||||
// object and there is no registration step to forget. Exposed on globalThis
|
// object and there is no registration step to forget. Exposed on globalThis
|
||||||
// for console spelunking, same convention as __editorScore / __envelope.
|
// as EDITOR_STORM (B's name) for consoles and spelunking.
|
||||||
let _sel = null;
|
let _sel = null;
|
||||||
export function stormSel() {
|
export function stormSel() {
|
||||||
if (!_sel) {
|
if (!_sel) {
|
||||||
_sel = createStormSel();
|
_sel = createStormSel();
|
||||||
if (typeof globalThis !== 'undefined') globalThis.__stormSel = _sel;
|
if (typeof globalThis !== 'undefined') globalThis.EDITOR_STORM = _sel;
|
||||||
}
|
}
|
||||||
return _sel;
|
return _sel;
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ import { loadSite, createWorld } from '../world.js';
|
|||||||
// means reaching for the real one.
|
// means reaching for the real one.
|
||||||
import { createWindRouter } from '../main.js';
|
import { createWindRouter } from '../main.js';
|
||||||
import { normalizeAxis, shelterAtten, worstSecond, STORM_KEYS as WIND_STORM_KEYS } from '../editor.wind.js';
|
import { normalizeAxis, shelterAtten, worstSecond, STORM_KEYS as WIND_STORM_KEYS } from '../editor.wind.js';
|
||||||
import { createStormSel, stormSel, STORM_KEYS as SEL_STORM_KEYS, DEFAULT_SEL } from '../editor_stormsel.js';
|
import { createStormSel, stormSel, STORM_KEYS as SEL_STORM_KEYS, DEFAULT_SEL } from '../editor_storm.js';
|
||||||
import { createDebris } from '../debris.js';
|
import { createDebris } from '../debris.js';
|
||||||
import { createSkyFx, RainShadow } from '../skyfx.js';
|
import { createSkyFx, RainShadow } from '../skyfx.js';
|
||||||
import { SailRig, HARDWARE } from '../sail.js';
|
import { SailRig, HARDWARE } from '../sail.js';
|
||||||
@ -889,18 +889,22 @@ export default async function run(t) {
|
|||||||
// GATE 2.2 (SPRINT15) — ONE STORM PICKER: the shared selection store
|
// GATE 2.2 (SPRINT15) — ONE STORM PICKER: the shared selection store
|
||||||
//
|
//
|
||||||
// Sprint 14's page had two pickers with two defaults, and D scored the
|
// Sprint 14's page had two pickers with two defaults, and D scored the
|
||||||
// wildnight while looking at the southerly. editor_stormsel.js is the one
|
// wildnight while looking at the southerly. editor_storm.js is the one
|
||||||
// owner now; these asserts pin its contract. The DOM half (my select and
|
// owner now; these asserts pin its contract. The DOM half (my select and
|
||||||
// scrubber writing/reading it) cannot run here and is verified live on the
|
// scrubber writing/reading it) cannot run here and is verified live on the
|
||||||
// editor page — said plainly rather than papered over with a fake DOM.
|
// editor page — said plainly rather than papered over with a fake DOM.
|
||||||
|
|
||||||
t.test('GATE 2.2: the shared storm selection refuses garbage, is silent on no-ops, and opens on the pin', () => {
|
t.test('GATE 2.2: the shared storm selection refuses garbage, is silent on no-ops, and opens on the CONVERGED default', () => {
|
||||||
// The page must open on the gate-2.3 pinned moment — the storm+second every
|
// The default is the pair B and C agreed in THREADS (B's storm — the
|
||||||
// measured number is argued against. Two defaults was the whole bug.
|
// authoring case: the wind overlay is live at page-open and the southerly
|
||||||
|
// makes the funnel scream; my second — t=40 is the S14 receipt's align-100%
|
||||||
|
// moment). Pinned HERE so neither lane's refactor can silently regrow a
|
||||||
|
// private default: two defaults was the whole bug.
|
||||||
assert(SEL_STORM_KEYS.includes(DEFAULT_SEL.storm), 'the default storm is not a real storm');
|
assert(SEL_STORM_KEYS.includes(DEFAULT_SEL.storm), 'the default storm is not a real storm');
|
||||||
assert(DEFAULT_SEL.storm === PIN.storm && DEFAULT_SEL.time === PIN.t,
|
assert(DEFAULT_SEL.storm === 'storm_03_southerly' && DEFAULT_SEL.time === 40,
|
||||||
`the default selection (${DEFAULT_SEL.storm} @ ${DEFAULT_SEL.time}s) has drifted off the `
|
`the default selection (${DEFAULT_SEL.storm} @ ${DEFAULT_SEL.time}s) has drifted off the `
|
||||||
+ `gate-2.3 pin (${PIN.storm} @ ${PIN.t}s) — if that is intentional, move the pin too`);
|
+ 'converged storm_03_southerly @ 40s — that pair is a B+C agreement (THREADS 2026-07-20), '
|
||||||
|
+ 'renegotiate it there before moving it here');
|
||||||
|
|
||||||
const s = createStormSel();
|
const s = createStormSel();
|
||||||
assert(s.storm === DEFAULT_SEL.storm && s.time === DEFAULT_SEL.time, 'a fresh store ignores the default');
|
assert(s.storm === DEFAULT_SEL.storm && s.time === DEFAULT_SEL.time, 'a fresh store ignores the default');
|
||||||
@ -908,14 +912,17 @@ export default async function run(t) {
|
|||||||
const seen = [];
|
const seen = [];
|
||||||
const off = s.onChange((p) => seen.push(p));
|
const off = s.onChange((p) => seen.push(p));
|
||||||
|
|
||||||
// a real change fires exactly once, payload matches the getters
|
// a real change fires exactly once, payload matches the getters, and the
|
||||||
assert(s.setStorm('storm_03_southerly') === true, 'a real storm change should return true');
|
// writer's `source` tag (B's ask) rides the payload for debugging
|
||||||
assert(seen.length === 1 && seen[0].storm === 'storm_03_southerly' && seen[0].time === DEFAULT_SEL.time,
|
assert(s.setStorm('storm_02_wildnight', 'c.test') === true, 'a real storm change should return true');
|
||||||
|
assert(seen.length === 1 && seen[0].storm === 'storm_02_wildnight' && seen[0].time === DEFAULT_SEL.time,
|
||||||
`one change should mean one event with the new value, got ${JSON.stringify(seen)}`);
|
`one change should mean one event with the new value, got ${JSON.stringify(seen)}`);
|
||||||
|
assert(seen[0].source === 'c.test',
|
||||||
|
`the payload should carry the writer's source tag, got ${JSON.stringify(seen[0])}`);
|
||||||
|
|
||||||
// no-op writes are SILENT — this is what lets a subscriber clamp the
|
// no-op writes are SILENT — this is what lets a subscriber clamp the
|
||||||
// selection from inside its own change handler without ringing forever
|
// selection from inside its own change handler without ringing forever
|
||||||
assert(s.setStorm('storm_03_southerly') === false, 'a no-op storm write should return false');
|
assert(s.setStorm('storm_02_wildnight') === false, 'a no-op storm write should return false');
|
||||||
assert(s.setTime(s.time) === false, 'a no-op time write should return false');
|
assert(s.setTime(s.time) === false, 'a no-op time write should return false');
|
||||||
assert(seen.length === 1, `a no-op fired a listener — the anti-loop guard is gone (${seen.length} events)`);
|
assert(seen.length === 1, `a no-op fired a listener — the anti-loop guard is gone (${seen.length} events)`);
|
||||||
|
|
||||||
@ -931,7 +938,7 @@ export default async function run(t) {
|
|||||||
try { s.setTime(bad); } catch { threw = true; }
|
try { s.setTime(bad); } catch { threw = true; }
|
||||||
assert(threw, `setTime(${JSON.stringify(bad)}) was accepted`);
|
assert(threw, `setTime(${JSON.stringify(bad)}) was accepted`);
|
||||||
}
|
}
|
||||||
assert(s.storm === 'storm_03_southerly' && seen.length === 1,
|
assert(s.storm === 'storm_02_wildnight' && seen.length === 1,
|
||||||
'refused garbage moved the selection or fired a listener anyway');
|
'refused garbage moved the selection or fired a listener anyway');
|
||||||
|
|
||||||
// a throwing listener must not sever delivery to the others
|
// a throwing listener must not sever delivery to the others
|
||||||
@ -951,7 +958,8 @@ export default async function run(t) {
|
|||||||
t.test('GATE 2.2: one singleton, one storm list — every consumer holds the same objects', () => {
|
t.test('GATE 2.2: one singleton, one storm list — every consumer holds the same objects', () => {
|
||||||
// Import identity is the sharing mechanism, so it is the thing to pin.
|
// Import identity is the sharing mechanism, so it is the thing to pin.
|
||||||
assert(stormSel() === stormSel(), 'stormSel() built two stores — the page is back to two pickers');
|
assert(stormSel() === stormSel(), 'stormSel() built two stores — the page is back to two pickers');
|
||||||
assert(globalThis.__stormSel === stormSel(), 'the console spelunking handle points at a different store');
|
assert(globalThis.EDITOR_STORM === stormSel(),
|
||||||
|
"the console handle (B's EDITOR_STORM name, converged) points at a different store");
|
||||||
// editor.wind.js re-exports the store's list — SAME ARRAY, not a copy that
|
// editor.wind.js re-exports the store's list — SAME ARRAY, not a copy that
|
||||||
// can drift (two copies of this list is how the page grew two pickers).
|
// can drift (two copies of this list is how the page grew two pickers).
|
||||||
assert(WIND_STORM_KEYS === SEL_STORM_KEYS, 'editor.wind.js carries its own storm list again');
|
assert(WIND_STORM_KEYS === SEL_STORM_KEYS, 'editor.wind.js carries its own storm list again');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user