Skip to content

Annotation: HED short form with long-form hover - #268

Merged
neuromechanist merged 1 commit into
feature/issue-256-epic-viewer-workbenchfrom
fix/hed-short-form
Sep 1, 2026
Merged

Annotation: HED short form with long-form hover#268
neuromechanist merged 1 commit into
feature/issue-256-epic-viewer-workbenchfrom
fix/hed-short-form

Conversation

@neuromechanist

Copy link
Copy Markdown
Contributor

Live-QA round 4 from Yahya (part of epic #256):

  1. Short form is HED's annotation default and the sc: library prefix is the semantically load-bearing part, so exports now write short-form tags (sc:Epileptic-seizure, Eye-blink-artifact) — validators expand them via the dataset's HEDVersion, which the panel footer already tells users to set. Long-form paths remain what is STORED, so nothing is lossy and old saved annotations pick the new rendering up unchanged.
  2. The panel was showing raw long-form paths whenever annotations restored before the lazily-chunked vocabulary loaded; the display fallback is now a pure short-form derivation, and the long form appears on hover (list rows and chosen chips).
  3. With rows this narrow, the annotation list becomes a responsive grid — up to three columns row-major, collapsing to one — so hundred-annotation scoring sessions stay scannable inside the existing scroll cap.

Gates: typecheck 0 errors, lint clean, 1350 tests (serializer expectations moved to short form, new hedShortForm cases).

Yahya's QA round 4:
- New pure hedShortForm(): leaf tag with the sc: library prefix carried
  over. formatHed() now writes short form to events.tsv/channels TSV
  (HED's annotation default; validators expand via HEDVersion).
- Panel rows and chips show short form even before the lazy vocab loads
  (labelForPath falls back to hedShortForm, not the raw path — how
  long paths leaked into the UI); the long form moves to hover titles
  on rows and chosen chips.
- Annotation list becomes a responsive grid: 2-3 columns of short rows
  on a wide panel, collapsing to one, row-major.

Tested: typecheck, lint, 1350 vitest (serializer expectations updated
to short form; new hedShortForm cases).
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying nemar-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5eaf35d
Status: ✅  Deploy successful!
Preview URL: https://d918edb6.nemar-website.pages.dev
Branch Preview URL: https://fix-hed-short-form.nemar-website.pages.dev

View logs

@neuromechanist
neuromechanist merged commit 543a103 into feature/issue-256-epic-viewer-workbench Sep 1, 2026
5 checks passed
neuromechanist added a commit that referenced this pull request Sep 1, 2026
* Surface a degraded view pyramid in the viewer

GroupHandle.viewLevelsDegraded was computed but never read, so a
truncated overview pyramid looked identical to a short recording. The
status line now carries a suffix and a standing note sits under the
minimap; both re-check when viewLevelsReady settles after first paint.

* Push annotation persistence failures to the viewer UI

mutate() runs syncAll() synchronously before the debounced flush, so a
write that fails on the last annotation before a tab close never re-armed
the beforeunload guard nor repainted the "not being saved" banner. The
store now notifies subscribers from degrade(); the annotation layer
subscribes at open and re-runs syncBeforeUnload() + renderPanel().

* Stop the preloader retrying into an outage

PrefetchController.loop walked the whole recording re-failing forever with
only a static buffered bar as feedback. Four consecutive segment failures
(each already past store.ts's retry ladder) now stop the walk and fire
onStalled; the viewer shows a note beside the preload toggle, and any
restart -- toggle, cache cap, group switch -- re-arms it. Aborts do not
count as failures.

* Block recording swaps while an annotation draft is open

isPopoverOpen() had zero callers, so prev/next and the subject/task
dropdowns destroyed the mounted instance and its unsaved popover draft
silently. The viewer now hands the page a ViewerAnnotationHandle on the
same seam as onTransfer; navigateEegViewer, stepEegViewer and both select
handlers refuse and flash+focus the popover instead. No window.confirm --
the refusal plus a visible cue says the same thing without a blocking
modal.

* Drop a manual gain across a modality change

applyTransfer carried a manually-set gain onto any new recording. Gain
multiplies the modality's DEFAULT_SCALINGS, so an EEG gain on an MEG
recording is a different physical scale, not the same zoom. The transfer
record now carries the modality it was chosen against; a mismatch falls
back to auto-scale. Same-modality swaps are unchanged.

* Tear down a mid-flight viewer on dialog close

The close handler relied on eegLive.destroy, which navigateEegViewer nulls
for the duration of the new mount. Fall back to the host's own
_eegvCleanup so an instance that published it but has not returned its
disposer yet is released immediately instead of at the post-await
staleness check.

* Surface an annotation export that fails to download

The export is the escape hatch for annotations that live nowhere else, so
a throwing createObjectURL/click must not read as an inert button. Wrap it
and render the failure beside the export buttons; a later success clears
the message.

* Warn when the nav-order preference cannot be stored

writeNavOrder returns false for exactly this (privacy mode, blocked
storage) and the return value was discarded.

* Fix stale comments and docs from the release panel

- hed-vocab.ts / annotation-ui.ts: the bundle is ~341 KB and 1525 entries,
  not ~140 KB / 500, and it is no longer curated (website#269).
- HedPath / HedVocabEntry.path / extract-hed-vocab.mjs: the long form is the
  storage and lookup key; the SHORT form is what an export writes (#268).
- annotation-ui.ts labelForPath: it falls back to the derived short form,
  matching its own inline comment.
- render-dir-listing.ts: the missing half of the CHEVRON_SVG sync comment.
- ADR 0013: dated update note; search now covers both schemas in full,
  curation narrowed to quick picks, decision unchanged.
- AGENTS.md: real test count, and eeg-viewer/ now names what it holds.
- [id].astro: releaseEegViewer has two call sites, and
  openRecordingInDialog can be reached with an inline viewer live.
- extract-hed-vocab.mjs: drop the unused byPath map (regenerating the
  bundle after the change produces a byte-identical file).

* Let a dialog refuse its own corner X

DialogCloseButton calls dialog.close(), which fires only the
non-cancelable "close" event -- a decision already taken. So the X walked
straight past the annotation-draft guard that Escape respects via
"cancel", and the detached-close branch destroyed the draft with no
trace. The button now dispatches a cancelable pre-close event; the
dataset page vetoes it while a detached viewer holds an open popover.
Inert for every dialog that does not listen.

* Derive the preload stall note instead of latching it

The note advertised "turn it off and on again to retry", and that cure
provably failed: renderImpl's read-failure branch returns before
updatePrefetchTarget, and the toggle-on path early-returns while no view
level is known, so the latched flag survived its own recovery and the
note stuck for good. The outage that trips the breaker is the same one
that fails the interactive read, so this was the common case.

The note is now derived from PrefetchController.stalled, which stop()
clears alongside start(), and is re-evaluated on every render exactly as
the degraded-pyramid note already was -- that asymmetry was why one
self-healed and the other did not.

* Make the viewer's destroy() idempotent

Three handles point at the one disposer, and a dialog close during a
navigate mount fires two of them: the close handler takes
host._eegvCleanup, then navigateEegViewer's superseded branch calls the
same returned destroy. Running the cleanups twice double-disposes the GL
context and tears the annotation layer down mid-flush. Guard in the
closure, which is the only place that can see all three call sites.

* Fail open when the annotation draft guard throws

A throwing isPopoverOpen/focusPopover would otherwise wedge every
prev/next and both dropdowns for the rest of the session, with the
selects re-syncing away from the user's pick and no gesture that
recovers. Losing at most one unsaved draft is the smaller failure.

* Extract and test the modality gain-transfer gate

gainCarriesOver was an inline expression in applyTransfer's closure. Its
failure mode is a trace off by orders of magnitude -- a flat line or a
wall of clipping that reads as the recording, not as a bug -- so it does
not belong untested. Pure, exported, and covered for same/different
modality, case, whitespace, and the null-safety convention that an absent
modality counts as a match.

* Cover the load-first annotation persistence degrade

The realistic first contact is mount, subscribe, load -- with no prior
save to have caught an unusable connection. That is the visit where the
annotator would be told their marks are safe when they are not.

* Stop the degraded-overview note naming a cause

ViewLevelDiscoveryError covers a retry-exhausted 5xx, a 403 from an
expired token and a decode error alike, and the flag does not say which,
so "connection problem" was a guess presented as a diagnosis. The action
is the same either way: reload.

* Codify the ADR amendment convention

ADR 0013 grew an "## Update -- YYYY-MM-DD" section for a factual change
that left the Decision standing. Write the rule down: amend by appending,
never by editing Context/Decision/Alternatives, and supersede when the
Decision itself changes.

* Fix a stale caller claim and the test count

releaseEegViewer's inline comment still named openEegViewerInline as its
only caller, contradicting the header six lines above it that now names
both. AGENTS.md's test count follows the suite to 1402.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant