One hundred and forty-three comments across the research dashboard, the research
map, Big Picture and the two composites attributed a decision to a person: by
name, by quotation, by date, or as "per Driver". Shipped code should carry the
reason a thing is the way it is, not who said so. A reader of the giveaway repo
has no way to ask that person anything, and the name tells them nothing the
reason does not.
So a quotation becomes the requirement it was quoting, a ruling becomes the
ruling, "the Driver reported" becomes what was reported, and a decision still
waiting on an answer now reads as provisional and pending a product decision
rather than as somebody's pending word. Where a comment already stated the
reason -- most of them did -- only the attribution went.
Two things were removed rather than rewritten. A personal medical fact appeared
twice ("the Driver is photosensitive"), once in a comment and once in a gate's
console output where it printed on every run; the motion-safety requirement
stands on its own and now reads that readers may be photosensitive. Internal card
ids that named nobody stayed, since they point at this repo's own history.
The bulk went in three batched passes of meaning-preserving substitutions, and
that is where the risk was: a blind replacement produces sentences that are
grammatically wrong but still look plausible in a diff. Re-reading every changed
line found thirteen -- "the the report describes", "including the two that were
had just complained about", "because was asked for WHITE rings" -- each repaired
individually. The count of remaining references caught the tail three separate
times, when a file type, an escaped apostrophe, and a scripts/ path each fell
outside a filter I had assumed was total.
Comment bodies and two console strings only. All twenty-one files parse, both
meta.json files are still valid JSON, and no declaration moved.
85 lines
5 KiB
JavaScript
85 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-hover-channel.mjs — hover points at a record; it must never commit to one.
|
|
*
|
|
* SPEC-DASHBOARD-KIT D3. The bus carries three coordinated channels: filterSet NARROWS scope,
|
|
* selectionSet MARKS individuals, and hoverKey POINTS at the one record under the pointer.
|
|
* kepler.gl treats hover as a configured interaction alongside filters for the same reason — it is
|
|
* how a reader sweeps a table and watches the map answer without choosing anything.
|
|
*
|
|
* WHY THIS IS GATED RATHER THAN REVIEWED. Hover looks like a weaker selection, and the cheap way to
|
|
* implement it is to put the key in selectionSet "just while hovering". Every consequence of that
|
|
* is bad and none of them are obvious at the call site:
|
|
* - selectionSet is a plain Set with a version counter, and useSelectionCamera watches that
|
|
* counter. A hover that bumped it would fly the camera at every mousemove — the resize-strobe
|
|
* bug wearing a new hat, on a reader who is photosensitive.
|
|
* - the CSV exporter reads selection > filter > all, so a hover would silently change what an
|
|
* export contains.
|
|
* - the table's selection pill and the shell's live region both report selection counts, and
|
|
* both would narrate the pointer.
|
|
* So the contract is asserted: hover is its own ref, it does not touch the selection, and a repeat
|
|
* is a no-op (hover fires on every mousemove across 2,107 rows; a channel that woke every panel on
|
|
* an unchanged value would need a debounce it should not need).
|
|
*
|
|
* NOT CHECKED: which panels light up on hover. D3 says panels opt in, and the first opt-in belongs
|
|
* with a consumer that has a reason — wiring one into Big Picture speculatively would be a visual
|
|
* change to the flagship with no reader asking for it. This gate guards the CHANNEL so the first
|
|
* panel to use it inherits the rules rather than inventing them.
|
|
*
|
|
* NEGATIVE-TESTED: routing setHover through selectionSet fails A; dropping the equality guard
|
|
* fails B; removing hoverKey from the returned surface fails the import check.
|
|
*/
|
|
import { existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const TAG = 'HOVER-CHANNEL'
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
|
const BUS = join(ROOT, 'library/composables/useSelectionBus.js')
|
|
if (!existsSync(BUS)) { console.log(`${TAG}: SKIP — no ${BUS.replace(ROOT + '/', '')} (kit landing 1 not applied).`); process.exit(0) }
|
|
const { createSelectionBus } = await import(BUS)
|
|
|
|
const fails = []
|
|
const notes = []
|
|
const eq = (what, got, want) => { if (JSON.stringify(got) !== JSON.stringify(want)) fails.push(`${what} — got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`) }
|
|
|
|
const bus = createSelectionBus()
|
|
if (!bus.hoverKey || typeof bus.setHover !== 'function') {
|
|
fails.push('the bus exposes no hoverKey/setHover — D3\'s channel is missing, and a panel wanting hover would have to invent its own, which is how selectionSet gets abused for it.')
|
|
} else {
|
|
// ── A. hover is not selection ──────────────────────────────────────────────────────────────
|
|
bus.selectMany(['keep-me'])
|
|
const versionBefore = bus.selectionVersion.value
|
|
bus.setHover('abkhaz')
|
|
eq('A hoverKey holds the record', bus.hoverKey.value, 'abkhaz')
|
|
eq('A hover does NOT enter the selection', [...bus.selectionSet], ['keep-me'])
|
|
eq('A hover does NOT bump selectionVersion (the camera watches it)', bus.selectionVersion.value, versionBefore)
|
|
eq('A hover does NOT touch the filters', Object.keys(bus.filterSet), [])
|
|
|
|
// ── B. a repeat is a no-op, and clearing is explicit ───────────────────────────────────────
|
|
let emitted = 0
|
|
bus.on('hover', () => { emitted++ })
|
|
bus.setHover('same'); bus.setHover('same'); bus.setHover('same')
|
|
eq('B three identical hovers emit once', emitted, 1)
|
|
bus.setHover(null)
|
|
eq('B null clears', bus.hoverKey.value, null)
|
|
eq('B clearing emits', emitted, 2)
|
|
bus.setHover()
|
|
eq('B a bare call means nothing hovered', bus.hoverKey.value, null)
|
|
eq('B clearing an already-clear hover is a no-op', emitted, 2)
|
|
|
|
// ── C. clearing the selection leaves hover alone, and vice versa ────────────────────────────
|
|
bus.setHover('x'); bus.clearSelection()
|
|
eq('C clearing the selection does not clear hover', bus.hoverKey.value, 'x')
|
|
bus.selectMany(['y']); bus.setHover(null)
|
|
eq('C clearing hover does not clear the selection', [...bus.selectionSet], ['y'])
|
|
if (!fails.length) notes.push('hover: its own ref, never the selection, no-op on repeat, independent of both other channels')
|
|
}
|
|
|
|
for (const n of notes) console.log(' · ' + n)
|
|
if (fails.length) {
|
|
console.error(`\n${TAG}: FAIL`)
|
|
for (const f of fails) console.error(' ✗ ' + f)
|
|
process.exit(1)
|
|
}
|
|
console.log(`${TAG}: PASS — hover points without committing.`)
|