editor: one storm picker + a progress line that paints (S15 gates 2.1/2.2, B's halves)
editor_storm.js owns THE storm selection and THE storm list; SCORE IT's dropdown is a view of it (default = southerly, C's old default, kept on purpose). The score panel renders scoreSite's progress ticks — flight x/y, elapsed — and audit.html gets the same line. C's half: subscribe editor.wind.js per the THREADS proposal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b1d2d2a8e6
commit
d6f04d5e43
@ -91,7 +91,17 @@ async function run() {
|
||||
? (sepKey === stormName ? stormDef : await loadJSON(`../../web/world/data/storms/${sepKey}.json`))
|
||||
: null;
|
||||
|
||||
const score = await scoreSite({ site, stormDef, stormName, sepStormDef });
|
||||
// scoreSite yields between flights now (SPRINT15 gate 2.1), so this page can
|
||||
// say where it is instead of sitting on "loading…" for a minute.
|
||||
const t0 = performance.now();
|
||||
const score = await scoreSite({ site, stormDef, stormName, sepStormDef, onProgress: (p) => {
|
||||
const secs = ((performance.now() - t0) / 1000).toFixed(1);
|
||||
el('sub').textContent = p.phase === 'sweep'
|
||||
? `${siteName} / ${stormName} — sweeping: flight ${p.done}/${p.total} quads · ${secs} s`
|
||||
: p.phase === 'fly'
|
||||
? `${siteName} / ${stormName} — flying the garden: line ${p.done}/${p.total}${p.label ? ` (${p.label})` : ''} · ${secs} s`
|
||||
: `${siteName} / ${stormName} — judging the separation target (${p.label}) · ${secs} s`;
|
||||
} });
|
||||
const {
|
||||
dressed, cands, rows, verdict, winners, marginalWinners,
|
||||
flown, skipped, bare, separation: sep, sepStormName,
|
||||
|
||||
@ -40,17 +40,11 @@
|
||||
import { loadStorm } from './weather.js';
|
||||
import { START_BUDGET } from './contracts.js';
|
||||
import { scoreSite, buildScoringWorld, cheapestHonest, marginFlags, collateralExposure } from '../../../tools/site_audit/scorecard.js';
|
||||
|
||||
/** The storms a yard gets judged against. `storm_02_wildnight` leads because
|
||||
* it is the storm every Sprint-13 number was argued over — score against the
|
||||
* one people remember. */
|
||||
const STORMS = [
|
||||
'storm_02_wildnight',
|
||||
'storm_01_gentle',
|
||||
'storm_02b_icenight',
|
||||
'storm_03_southerly',
|
||||
'storm_03b_earlybuster',
|
||||
];
|
||||
// SPRINT15 gate 2.2: the storm list and the SELECTION live in editor_storm.js
|
||||
// now — one picker's state, shared with C's WIND panel. D scored the wildnight
|
||||
// while viewing the southerly because this file and editor.wind.js each
|
||||
// defaulted a private stormKey; neither carries one any more.
|
||||
import { stormSel } from './editor_storm.js';
|
||||
|
||||
const el = (tag, cls, text) => {
|
||||
const n = document.createElement(tag);
|
||||
@ -93,7 +87,14 @@ function boot() {
|
||||
const rowStorm = el('div', 'ed-row');
|
||||
rowStorm.append(el('span', 'ed-label', 'storm'));
|
||||
const sel = el('select', 'ed-sel');
|
||||
for (const s of STORMS) sel.append(new Option(s.replace(/^storm_/, ''), s));
|
||||
for (const s of stormSel.STORM_KEYS) sel.append(new Option(s.replace(/^storm_/, ''), s));
|
||||
// One storm selection for the whole page (gate 2.2): this select is a VIEW of
|
||||
// stormSel, not an owner — change it here and C's wind panel follows, change
|
||||
// it there and this follows. Scoring one storm while reading another's wind
|
||||
// field is how D got quietly wrong numbers.
|
||||
sel.value = stormSel.key;
|
||||
sel.addEventListener('change', () => stormSel.set(sel.value, 'score'));
|
||||
stormSel.onChange((k) => { sel.value = k; });
|
||||
rowStorm.append(sel);
|
||||
body.append(rowStorm);
|
||||
|
||||
@ -108,7 +109,8 @@ function boot() {
|
||||
const note = el('div', 'ed-note',
|
||||
'Scores a fresh dressed world built from the EXPORT clone — not the editor\'s '
|
||||
+ 'view, whose wind is a calm stub. Every number flies windForSite() and the real '
|
||||
+ 'commit→attach chain. Takes a few seconds; that is the point.');
|
||||
+ 'commit→attach chain. Slow is honest — every candidate flies the full storm — '
|
||||
+ 'but the page stays live and the progress line says where it is.');
|
||||
body.append(note);
|
||||
|
||||
// A score describes the yard as it was WHEN SCORED. The moment anything
|
||||
@ -129,14 +131,30 @@ function boot() {
|
||||
stale = false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'scoring…';
|
||||
out.replaceChildren(el('div', 'ed-note', 'building a dressed world, sweeping every quad, flying the winners…'));
|
||||
// yield so the button repaints before we block the thread for seconds
|
||||
// The progress line (gate 2.1). D's verdict on the old blocking run: the
|
||||
// worst part "is not the wait, it's not knowing whether it died". scoreSite
|
||||
// yields between flights now, so every update here actually PAINTS.
|
||||
const prog = el('div', 'ed-note', 'building a dressed world…');
|
||||
out.replaceChildren(prog);
|
||||
// yield so the button repaints before the first heavy chunk
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
const t0 = performance.now();
|
||||
const secs = () => `${((performance.now() - t0) / 1000).toFixed(1)} s`;
|
||||
const onProgress = (p) => {
|
||||
if (p.phase === 'sweep') {
|
||||
prog.textContent = `sweeping — flight ${p.done}/${p.total} quads · ${secs()}`;
|
||||
} else if (p.phase === 'fly') {
|
||||
prog.textContent = `flying the garden — line ${p.done}/${p.total}${p.label ? ` · ${p.label}` : ''} · ${secs()}`;
|
||||
} else if (p.phase === 'separation') {
|
||||
prog.textContent = p.done
|
||||
? `separation target judged · ${secs()}`
|
||||
: `judging the pinned separation target (${p.label}) · ${secs()}`;
|
||||
}
|
||||
};
|
||||
try {
|
||||
const site = EDITOR.siteClone();
|
||||
const stormName = sel.value;
|
||||
const stormName = stormSel.key;
|
||||
const stormDef = await loadStorm(stormName);
|
||||
|
||||
// The separation block names its OWN storm — judging A's pinned target on
|
||||
@ -146,7 +164,7 @@ function boot() {
|
||||
const sepStormDef = sepKey ? (sepKey === stormName ? stormDef : await loadStorm(sepKey)) : null;
|
||||
|
||||
const built = await buildScoringWorld(site);
|
||||
const score = await scoreSite({ site, stormDef, stormName, sepStormDef, prebuilt: built });
|
||||
const score = await scoreSite({ site, stormDef, stormName, sepStormDef, prebuilt: built, onProgress });
|
||||
render(out, score, performance.now() - t0);
|
||||
globalThis.__editorScore = score; // for the selftest + console spelunking
|
||||
} catch (err) {
|
||||
|
||||
66
web/world/js/editor_storm.js
Normal file
66
web/world/js/editor_storm.js
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* editor_storm.js — the editor's ONE storm selection. [Lane B, SPRINT15 gate 2.2]
|
||||
*
|
||||
* D scored the wildnight while looking at the southerly: B's SCORE IT and C's
|
||||
* WIND panel each carried a private `stormKey`, independently defaulted — the
|
||||
* exact two-harnesses-disagreeing failure this repo keeps paying for, arrived
|
||||
* inside a single page. This module is the fix: one storm selection, one storm
|
||||
* LIST, owned here; every panel that shows or uses a storm reads and writes
|
||||
* THIS and never a private copy.
|
||||
*
|
||||
* Deliberately not on A's `EDITOR.on` — emit is private to editor.js, and
|
||||
* widening A's seam for one dropdown is more contract than the problem needs.
|
||||
* B owns this file; C's panel subscribes (THREADS 2026-07-20, gate 2.2
|
||||
* proposal). The default is the southerly — C's old default, kept on purpose:
|
||||
* the WIND panel is live the moment the page opens and needs a storm that
|
||||
* shows something, while SCORE IT is an explicit click behind a visible
|
||||
* dropdown. (A funnel authored under the gentle storm looks like it does
|
||||
* nothing — C's own comment.)
|
||||
*
|
||||
* No I/O here: this holds the NAME, not the storm def. Fetching defs stays
|
||||
* with the panels (loadStorm), which already cache.
|
||||
*/
|
||||
|
||||
/** Keep in step with data/storms/ — THE list; panels must not carry their own. */
|
||||
export const STORM_KEYS = [
|
||||
'storm_01_gentle',
|
||||
'storm_02_wildnight',
|
||||
'storm_02b_icenight',
|
||||
'storm_03_southerly',
|
||||
'storm_03b_earlybuster',
|
||||
];
|
||||
|
||||
export const DEFAULT_STORM = 'storm_03_southerly';
|
||||
|
||||
let key = DEFAULT_STORM;
|
||||
const subs = new Set();
|
||||
|
||||
export const stormSel = {
|
||||
/** The currently selected storm key. */
|
||||
get key() { return key; },
|
||||
|
||||
/**
|
||||
* Change the selection. No-op on the same key; throws on a stranger — a
|
||||
* typo'd storm silently becoming the default is setHardware's lesson again.
|
||||
* `source` is a free label ('score', 'wind', …) so a subscriber can skip
|
||||
* reacting to its own write.
|
||||
*/
|
||||
set(next, source = null) {
|
||||
if (next === key) return;
|
||||
if (!STORM_KEYS.includes(next)) {
|
||||
throw new Error(`unknown storm "${next}" — the editor knows: ${STORM_KEYS.join(', ')}`);
|
||||
}
|
||||
key = next;
|
||||
for (const fn of [...subs]) {
|
||||
try { fn(key, source); } catch (err) { console.error('[storm] subscriber threw:', err); }
|
||||
}
|
||||
},
|
||||
|
||||
/** Subscribe to changes; returns an unsubscribe function. */
|
||||
onChange(fn) { subs.add(fn); return () => subs.delete(fn); },
|
||||
|
||||
STORM_KEYS,
|
||||
};
|
||||
|
||||
// For consoles and selftests — same object, not a copy.
|
||||
globalThis.EDITOR_STORM = stormSel;
|
||||
Loading…
Reference in New Issue
Block a user