Internal interfaces and model contract
This is the MVP software design contract. Usability amendments are tracked in Draft usability problem resolution. Executed implementation evidence and remaining verification gaps are linked from Project maturity. Use Implementation concept and handover for lifecycle/module behavior and Technology stack and dependency decisions for selected dependencies. Related decisions: STV_ADR_001: Static build integration and browser runtime, STV_ADR_002: Portable graph and rendered-detail bundle and STV_ADR_005: Optional seed sets and paged project entry.
Implementation ownership follows Software elements: every software element maps to its matching component directory, source filenames and colocated unit tests. STV_ADR_007: Architecture-owned component directories defines the organization; the interfaces below retain their behavior across the source move.
Build-to-browser contract
A versioned immutable payload contains:
schema version and build content identity;
host Sphinx project name and sorted top-level stakeholder IDs;
normalized levels, type placement/color, accepted statuses and relation labels;
objects keyed by Need ID: title, type, status, document/anchor and detail key;
canonical outgoing edges and indexed incoming/outgoing target groups;
unresolved target records preserving the referenced ID;
per-object local detail-script locations and a lazily populated registry of final rendered fragments keyed by resolved Need ID.
Per-view initialization is page-local: {mode: "seeds", seed_ids: [...]}
or {mode: "project", seed_ids: []}, plus the instance identity. The manifest
contains the project name from app.config.project as escaped text, not HTML.
The synthetic root is a presentation record, not an entry in the Need registry.
Use tagged identities ["need", id] and ["project", viewId] so host IDs
cannot collide with the artificial root. Synthetic entry edges and controls
are separate from canonical authored relationship triples and relation indexes.
Per-view seeds and identities remain page-local. Emit one shared project graph asset per build rather than embedding the full graph in every view. Emit details as local per-object scripts and load them on first inspection, caching by build identity and ID. Classic script assignment to a private namespaced registry permits loading from disk; do not rely on file-origin fetch or ES-module requests. Include only project content already published by the host, not hidden Need internals. Serialize strings safely for their HTML/JavaScript context, including closing-script sequences, and avoid evaluating Need text as code.
Detail fragments are derived from the host’s final HTML, not reconstructed from raw Need dictionaries. Rebase relative links and media to work on the embedding page, namespace duplicated HTML IDs and update fragment references, preserve original documentation anchors for navigation, and exclude executable scripts from copied fragments. Third-party widgets preserve their static rendered content in MVP details; widget-specific JavaScript remains on the original page by owner agreement (STV_SWR_049).
State interface
Input: immutable graph/model, current view state and a command. Output: either new state plus visible graph, or unchanged state plus a diagnostic. Commands include expand/collapse group, replace seed, project overview, open/close details, zoom and pan; the later selector adds mode projection. Graph state is separate from viewport state. A rejected cap transition cannot partially update graph, mode, page or focus. Seed mode retains every distinct supplied seed as a visibility root until an explicit runtime seed or overview replacement succeeds. Project mode retains its synthetic root and follows only the current stakeholder entry page when expanded. Each page owns an independent expansion-key set; inactive pages contribute neither visible nodes nor traversal roots. Page size is five, with IDs sorted lexicographically. Previous/Next selects an adjacent valid page, restores its saved valid exploration state, then uses normal cap/layout checks before atomic commit. Project mode initially opens page zero and at most five entries. Root collapse retains the page index and page state but hides every descendant. Empty lists have no root expansion or page controls.
Every view includes a labelled picker over its immutable published object catalog. Case-insensitive substring matching searches IDs and titles; show at most ten suggestions with the total and a refine-search hint. Confirmation of a suggestion or case-sensitive exact trimmed ID proposes fresh seed mode. Unconfirmed typing and invalid/hidden IDs cannot change graph state. Keyboard Up/Down selects a suggestion; Enter confirms; Escape dismisses suggestions. Project overview proposes fresh project mode with page zero open. Both commands clear previous expansions/page memory only on successful capacity/layout commit, reset that view’s viewport and close transient hover details and retain persistent boxes. Preserve picker focus and other instances. Failed/stale transitions preserve the committed scene/state. No runtime graph fetching, source edit or rebuild is involved.
Capacity and performance
Build adjacency in O(N + E); runtime expansion uses indexed groups and considers only candidate visible nodes for rendering. The entire project graph/detail payload can still be large: this design does not claim bounded memory or load time independent of N. Measure a proposed 30,000-object/90,000-edge fixture and 200 visible nodes in the prototype, recording payload size, startup and interaction timings. These are proposed measurements, not accepted performance thresholds; budgets remain open.
Reviewed publication identity: content-addressed graph and detail scripts with build-keyed registries prevent stale browser cache entries crossing revisions.
Remote embedded resource contract
Uncached remote embedded media uses an unavailable-offline indication and an explicit original-resource link. Preserve local bundled media. Do not download remote media automatically at build time or request it when a detail panel opens. During extraction, replace remote image/srcset, audio/video/source/poster, iframe/object/embed and SVG resource references with a static placeholder and explicit link. Remove remote CSS imports and url() dependencies from copied styles; retain the affected resource URLs as explicit links. Resolve relative references against the original page before classifying them, including protocol-relative URLs. Local resources and permitted data images remain usable; ordinary external navigation links remain explicit reader actions. Do not copy remote stylesheets or active embedded documents into details. This policy refines STV_SWR_015/017 and the explicitly amended STV_SHR_015; it does not require a media cache.
Layout and inspection contract
Relationship controls use configured target levels to order wholly upward groups first, neutral groups next and wholly downward groups last. Mixed, same-level, test-role and unresolved groups are neutral. Stable ties preserve host order. The ordering helper preserves group identity and expansion state.
Direct unobstructed routes use facing box edges; blocked routes retain outer corridors and canonical arrow direction. The viewport is 975 CSS pixels high. Inspection owns one pending 1000 ms hover timer per view; node departure, Escape, scene commit and viewport movement cancel it. Explicit Details activation bypasses it. Moving onto the Details button retains the node timer. Escape is observed even when keyboard focus is outside the hovered view.
Detail asset path contract
Each transformed detail fragment uses its full SHA-256 content digest as the
filename, _static/stv/details/<content-hash>.js. Objects with identical
fragments share one immutable asset. The manifest retains per-object identity
and references its digest and URL; fragment registration remains keyed by that
same full digest. No digest is truncated. Windows file-URL consumers still need
an output root short enough that the complete asset path fits their browser’s
path support; arbitrary-depth output roots are not promised.
Detail transport marker ownership
data-stv-* attributes are reserved for the detail extractor. Remove all
such attributes from authored HTML before generating transport markers from
validated ordinary attributes. The inspector independently restores only
output-local relative paths or permitted raster data images; it rejects active
schemes, remote/absolute paths, traversal and escaped separators. Ordinary
external navigation links remain explicit reader actions. CSS transport accepts
canonical quoted local url() values and rejects imports and escaped syntax.
This closes Authored detail transport a... (STV_PR_007) within Publish full rendered details (STV_SWR_015), Avoid runtime network loading (STV_SWR_017)
and Use portable output URLs (STV_SWR_018). Graph state, events and timers are unchanged.
Local documentation publication
build-docs.ps1 delegates to scripts/build_docs.py after dependency checks.
The helper builds into a new sibling staging directory with a separate Sphinx
doctree cache. Only a successful warnings-as-errors build is promoted to
docs/_build/html. Existing output is retained in a sibling previous-output
directory; failed promotion attempts restore it. No recursive deletion occurs.
Resolved rename targets must be direct children of the source’s build directory;
redirected HTML output paths are rejected before moving existing output.
_stv-publication.json inventories every staged publication file by relative
path, byte count and SHA-256. It excludes its own entry and contains no local
absolute paths. Only html is the publication tree; retained prior/failed
builds and doctrees outside it are local diagnostics. This prevents carryover
of obsolete graph/detail scripts, pages and downloads. It does not revoke
already copied output or add automatic deployment. Direct incremental Sphinx
commands remain suitable for development but are not the publication workflow.
This maintenance boundary refines Incremental output retains ... (STV_PR_011); extension graph state,
timers and runtime behavior are unchanged, so no state diagram changes apply.
Classified test evidence placement
STV_ADR_009: Resolve evidence rows before exploration adds two mutually exclusive type placements to the existing
fixed level and legacy test role. levels_by_tag maps nonempty host tag names
to configured levels. level_from contains relation, direction
(incoming/outgoing) and a levels owner-level-to-target-level map. Its owners
must have fixed/tag placement; chained owner-derived data links are not inferred
and use fallback (or a Need-located strict error if fallback is absent).
fallback_level is optional on either classifier and names a configured row.
Malformed selectors, unknown levels/relations and invalid chaining fail model
validation with a field path. No host type/tag/relation names are hard-coded.
GRAPH first excludes hidden records and resolves all fixed/tag placements, then uses indexed published relations to resolve one-hop owner placements. Exactly one resulting level is required: no selected tags, conflicting tag levels, missing owners, unresolved owners or mixed owner levels choose the configured fallback, or fail with Need ID if no fallback exists. Same-class multiple owners are unambiguous. A fallback owner propagates fallback, rather than upgrading an execution to a known class based on only one of its other owners. Hidden owner identities are never published or included in diagnostics. Parts follow their published metadata and the same rules. No derived backlink is required.
Schema 1 gains additive object.level and object.placement_fallback fields
for classified records only. Fixed types and legacy-role graphs remain readable
without them. Layout and directional controls use resolved level before fixed
type level. Classified rows use ordinary same-row/top alignment and stable slots;
only role: test retains the legacy lane. RENDER labels fallback nodes
Unclassified test evidence without disabling links/details/navigation.
The synthetic root, cap, request tokens, world-coordinate SVG, focus and view
independence contracts remain unchanged. Empty configured rows retain spacing.
The project’s model declares the nine CR_013 levels followed by unclassified and problem rows. Test cases use test_system/test_software tags; executions use incoming executed_by owners. Generic authors can select different names, fixed levels or legacy role; the default five-type preset is unchanged.
Viewport clipping marker
Mark the viewport clipping ... (STV_SWR_062) places a 1 px solid grey top border on .stv-viewport,
not on .stv-scene. The viewport owns overflow clipping and stays in normal
flow after toolbar/status. Explicit content-box sizing retains 975 px client
height under host themes. The border is outside the content clip area; no
pseudo-element, overlay, handler, timer or extra focus target is introduced.
Persistent detail collection contract
traceability_view_details_limit = 10 in conf.py is a positive safe integer;
MODEL validates it, DIRECTIVE publishes optional graph.details_limit before
BUNDLE emission. Missing fields in older graphs use 10. This presentation
setting is separate from the host type/relationship mapping; authors need not
replace that model to configure capacity.
INSPECT separates hover from persistent entries. content.js owns cached
classic-script transport and safe fragment/URL/IDREF restoration. inspect.js
keeps Floating UI hover timers and dismissal. collection.js is an ordered
per-view Need-ID map; persistent.js owns labelled entry DOM, loading/error,
current-entry completion guards, reuse/FIFO/close/all and trigger associations.
RENDER owns the dock and right-aligned Close all, refreshes live trigger links
after node-control replacement and computes Reset against current viewport size.
A new ID is created once, oldest live entry is evicted if full, then the new entry is appended. Reuse leaves order intact. Closed/reopened IDs get a new entry identity; old completions cannot insert. Transport cache survives entry removal. Entry errors remain readable; close/reopen retries transport failures. Graph/seed/overview/page commits and Escape affect hover only, not the collection. Document pagehide clears entries without focus restoration, including cache suspension; navigation/reload drops page-local state. No storage or graph mutation.
The dock is 720 px beside graph containers wider than 1120 px. Below that threshold it follows the graph, aligned right and bounded to the available width and 720 px, retaining usable graph space. Stack height is bounded and scrollable; boxes have bounded own scrolling and unique namespaced body IDs. Open/reuse focuses/reveals its Close control; async content never focuses. Close resolves a fresh visible node trigger, else the view’s Close all. The empty Close all remains focusable. Native layout reserves graph space; it does not overlay nodes. Graph coordinates/D3 transform are not changed merely by opening/closing a box; explicit Reset uses the new dimensions.
Copied Need metadata controls
inspect/content.js initializes only copied .needs_collapse controls.
Decode their host row-class list before replacing the control ID; resolve rows
within the same containing Need table. Generate unique control/row IDs, keep
button role, tabindex, aria-expanded and aria-controls in sync, and start folded.
Click, Enter and Space toggle those rows and the host right/down icons together.
Each new fragment owns its handlers; reuse preserves current state. No host
jQuery/document-ready callback or arbitrary widget script is executed. Unknown
controls without matching rows are hidden, rather than left as dead arrows.
Artifact-ID header anchors under .needs-id retain their restored canonical
source URL and open with noopener/noreferrer in a new tab. They are excluded
from copied-body fragment rebasing. Other same-document anchors keep namespaced
local destinations; other restored links/resources retain the safe URL policy.
Detail pane boundary
RENDER gives the visible stack a 1 px solid #999 border inside its 720 px border-box width. Its side-by-side outer height is 976 px, matching the graph’s 975 px client area plus top border. Keep 8 px inner padding and no persistent card shadow so the pane top/separator remain crisp. The narrow container’s existing 60vh maximum still bounds the below-graph stack. Hidden stacks do not render the frame. No exploration or inspection state transition changes.
CR015-018 command and presentation contracts
RENDER dispatches {type: 'collapse-all'} to STATE. STATE computes
initial(current.mode === 'seeds' ? current.seeds : []), sets revision to
current revision plus one, and returns the ordinary validated visibility result.
This clears expanded groups and all page restoration caches, sets project page
zero/rootOpen true, and never changes the persistent collection. RENDER cancels
old layout generations and closes transient hover using its normal graph-commit
policy. A successful fresh layout fits the current viewport. Pending layout
failure retains the committed view and permits retry; no partial reset is shown.
The existing occurrence-mode deferral remains; no unimplemented mode is enabled.
traceability_view_tooltip_font_scale and
traceability_view_details_font_scale are independent finite positive numbers,
not booleans, with new-build default 0.7. MODEL validates; DIRECTIVE publishes
schema-1 additive graph.presentation = {tooltip_font_scale, details_font_scale}.
Missing fields in old bundles default to 1.0. Both values participate in bundle
identity. No user data model replacement or reader settings panel is required.
INSPECT measures each newly restored fragment in its actual presentation style context at scale 1.0 before changing any font styles. Capture computed CSS-pixel sizes for the complete descendant tree, then apply each captured size times the presentation scale in a second pass. This avoids nested multiplication and preserves existing explicit heading/table/code sizes. The visible panel wrapper’s own heading/link text uses the same baseline rule. Measure synchronously in the actual restored presentation before the next paint, never creating a second accessible live card. Restore retained baselines before measuring newly inserted descendants, then apply all sizes in a second pass; initial loading/error wrappers use the same rule. Delayed fonts must finish before measurement, or re-measure from the retained unscaled baseline after font readiness. Reuse never rescales already scaled values. Fragment disposal releases its measurement state. This is presentation-local; no source-page or graph label CSS is changed.
render/page-metadata.js initializes at DOMContentLoaded (or immediately when
already ready), selecting original table.need elements outside .stv-view
and all .stv-detail/persistent-copy subtrees. DIRECTIVE includes the existing
local runtime on every HTML page with rendered Needs, even with zero STV roots;
initialization is idempotent and does not require a graph transport. DIRECTIVE
finalization scans completed host HTML for rendered Need tables and injects one
STV-owned local runtime/CSS reference using output-relative URLs when absent.
It does not inject a graph script merely for page controls. RENDER index.js
initializes the page controller before looking for a data-stv-root script; if
no view roots exist it returns without dereferencing a missing script. Place one
labelled control group before the first host Need, with real button elements.
No Needs means no group; Needs without disclosures show disabled buttons with
an explanation. Discover host disclosure controls only in their closest owning
Need table and derive target rows using the host’s encoded class list. The pinned Sphinx-Needs 8.2.0 handler is attached to the nested
span.needs.collapsed and span.needs.visible icons, not their
.needs_collapse wrapper. For uniformly folded/open rows whose state differs
from the requested target, activate the currently displayed nested icon once,
then reconcile actual rows/icons/ARIA. For already matching rows do nothing.
For mixed rows within one disclosure normalize the identified rows and its two
icons directly to the target instead of inverting their mixed states.
A shared synchronization routine derives aria-expanded, aria-controls and label
from row visibility. A post-host delegated icon-click listener invokes only that
synchronization routine, never a second toggle; this keeps ARIA correct after
subsequent individual native clicks. Give the wrapper role/button tabindex and
map Enter/Space to the currently displayed host icon once, with preventDefault.
Bulk buttons remain real native buttons. Do not attach a competing individual
visibility-toggle handler to the host wrapper.
Enable page controls only after window load and a registered jQuery ready callback, including already-complete document cases; host collapse handlers are then installed. Missing pinned host prerequisites or unsupported encodings leave a readable disabled group, not a partial/global toggle. Custom markup without collapsible metadata retains the defined no-attributes explanation. Copied INSPECT controls retain their separate existing initialization and lifecycle.
Visible-area capture and resource contract
EXPORT.capture(workspace, {base, resources, regions})
returns an immutable snapshot descriptor or a typed capture error. A crop with
zero width/height produces “No visible view area to export” and no download;
never fabricate a minimum-size canvas or scroll the page automatically. Capture
requires completed local resource registration and document.fonts readiness.
Pending resources/fonts expose a retryable preparation state; failed required
resources/fonts expose an error while graph interaction remains available.
Capture does not asynchronously wait for fonts and then read newer live DOM.
RENDER exposes
only its last committed presentation. Busy/failed layout blocks export with a
retry instruction rather than mixing pending geometry and committed DOM.
Capture reads geometry, computed styles, all scroll offsets and DOM synchronously in one task. The crop is the intersection of the workspace rectangle with the browser visual viewport (layout viewport fallback), in CSS pixels. Toolbar, status, other views and content outside this crop are excluded. Narrow layouts include only the screen-visible part of the below-graph dock. The outer SVG uses this crop as width/height and viewBox; workspace/tooltip positions are translated relative to its top-left. Each overflow ancestor becomes an explicit clipping wrapper. Normalize scroll containers into fixed-size overflow-hidden wrappers with inner content translated by captured negative scroll offsets. Preserve the D3 transform and existing nested SVG viewBox/origins; never refit for export.
A pointer activation snapshots on primary pointerdown before toolbar focus can dismiss a visible tooltip; click completes serialization/save. Cancelled pointer activation discards the descriptor. Keyboard activation captures the state at activation; it does not recreate a tooltip already dismissed by normal focus. An INSPECT region is included only if it belongs to this view and intersects the crop. Frozen descriptors contain no live nodes or callbacks. Future graph, font, viewport or card changes cannot alter an in-progress snapshot. Read-only snapshotting does not close cards, update their age or move focus.
EXPORT.serialize(snapshot) constructs namespaced SVG with
XHTML foreignObject, computed inline style and nested graph SVG. It materializes
visible ::before/::after textual decoration, replacing control icons with their
captured inert appearance. Freeze animation/transition state. Discard handlers,
scripts, forms, embedded executable widgets, graph/transport attributes and
nonvisual accessibility-control markup. Already safe detail content remains the
source; export adds a second inert-output boundary. Do not serialize document
head, style sheets or arbitrary host scripts. Rewrite fragment IDs/references
within the snapshot. The effective workspace ancestor background is captured
as an SVG backdrop. Original style elements are omitted after computed-style
capture; only selected embedded font-face rules are emitted. Ordinary external navigation links may remain explicit
links; automatic resource loading is restricted to embedded validated data.
Non-intersecting positioned/SVG clipping or leaf boxes are omitted. Containers
with visible overflow are traversed even when their own box lies outside the
crop, because positioned descendants can overflow back into it. Off-screen flow boxes
become empty, nonvisual geometry placeholders, preserving scroll positions and
flex/grid gaps without their text, resources or identities. Clipping remains
authoritative for partial text/media intersections. SVG is a visual crop, not a promise to redact every
character beyond the visible portion of a partly clipped text node.
DETAIL_BUILD finalization walks safe local media/CSS dependencies used by detail fragments and STV styles. Resolve and symlink-check every file beneath the output root before reading. Validate MIME by content, retain approved raster data, sanitize local SVG to inert namespaced markup (no scripts, event handlers, foreign executable content or unresolved external dependencies), and collect local font bytes needed for @font-face. Reject cycles/unresolvable references with an explicit resource error. Never traverse outside output or download HTTP resources. Existing unavailable remote-media placeholders remain visible. Build records map canonical output-relative paths to MIME/base64/digest or a readable failure record. Keep large resource maps in separate content-addressed classic scripts, registered as immutable data using the existing escaped-JSON transport; the graph points to their identity/URL. Load the local record script before enabling Save; keep graph exploration usable if it fails. Include only resources selected by the visible capture (including currentSrc for srcset), not the entire resource map, in the saved SVG. Font fallback with no bundled webfont uses the captured system-font family and carries no external request.
The resulting XML must parse before a download is offered. Required visible resource/style failures abort with a readable message; do not silently replace previously available local content or report success with broken images. No runtime fetch/XHR, arbitrary file access or original-page dependency is permitted. No export change relaxes the existing detail URL/sanitization policies.
EXPORT.saveSVG(svgText, viewId) uses Blob type
image/svg+xml;charset=utf-8, URL.createObjectURL and a temporary HTML anchor
with download. Default name is stv-<safe-view-id>.svg; allow only safe filename
characters and bound the generated component to 80 characters. There is one
in-flight operation per view. Remove the anchor after activation; revoke its URL
after 60 seconds or pagehide, whichever is first. A pending URL registry supports
several successive completed downloads without early revocation. The 60-second
lease is a cleanup policy to verify with the target browser, not proof of disk
save completion. Generation errors release immediately and allow retry.
Location/name choice is browser/OS owned. User guidance explains enabling “Always ask you where to save files” in Firefox (or the equivalent browser setting). STV cannot force the native dialog or observe a cancelled/completed download. Its status says “SVG download requested”, never “file saved”. Automatic folder mode remains usable but does not claim a location prompt. The accepted owner clarification permits this documented browser-managed behavior.
Local SVG validation uses the standard-library XML parser with DTD/entity constructs rejected before parsing. Accept only the SVG namespace and static svg/g/defs/path/rect/circle/ellipse/line/polyline/polygon/text/tspan/use/image, clipPath/mask/marker/pattern/linearGradient/radialGradient/stop/title/desc/style nodes. Reject animation, scripts, event attributes, foreignObject and unknown namespaces in imported resources; EXPORT’s own controlled foreignObject wrapper is a different boundary. SVG use/href and paint/filter references must resolve to a local fragment or an embedded validated raster dependency; unresolved or external references produce a typed resource failure. Reject CSS imports and external URL references; embed accepted local font-face records explicitly. Validate all references again after rewriting. Unsupported local SVG remains viewable in the source documentation but export reports its unsupported resource rather than claiming a complete standalone snapshot. Extending the allowlist is a reviewed compatibility change with corresponding security/fidelity fixtures.
Host bulk actions reject disclosures targeting title/head or content/body rows. Missing host handlers or unsupported row encodings disable the page-wide pair with a readable explanation rather than applying a partial change.
Remembered view protocol
STATE owns frontend/src/state/persistence.js: snapshot encoding, validation,
key scope and guarded storage access. RENDER wires it to committed exploration,
LAYOUT positions, INSPECT ordered IDs and D3 transform. No new component/service.
Version-1 snapshot: {version:1, state:{mode,seeds,expanded,pages,page,rootOpen},
cards:[Need ID], transform:{x,y,k}, geometry:{signature,nodes,bandTops}}.
Sets/maps are encoded as arrays. The key is stv:view:1: plus a JSON tuple of
documentation base URL, project name, page pathname, root ID and authored seeds.
URL fragments identify document anchors and do not override remembered graph
state; explicit Start at Need/Project overview actions replace it and are saved.
Validate at the browser storage boundary. Unknown snapshot versions, malformed fields, invalid starting context or over-capacity rooted graphs select ordinary startup and announce why. Remove unavailable expansion groups and cards by checking current published objects/adjacency; never restore copied HTML. A reduced card capacity keeps the newest. Missing storage is a recoverable UI error, not a mount failure. Last completed save wins; storage events do not replace live state.
Saved geometry includes node key, finite coordinates/dimensions and semantic rank; band tops retain stable-navigation offsets after reload; its signature covers visible edge tuples, project entries and layout model. Only matching keys, sizes, ranks and signature permit geometry reuse before current edge routing/validation. Otherwise fresh layout applies the saved pan/zoom. A corrupt geometry record is ignored without trusting stored DOM or edge paths.
The mount gate suppresses saves until first layout, card reopening and transform
restoration finish. Checkbox changes are unavailable during that gate. Save only
committed scenes; layout completion uses existing request-ticket protection.
Card open/close and D3 zoom-end save synchronously; failed candidates never save.
Pagehide does not empty a remembered card collection; no unload writes overwrite other tabs.
Persistent reopening accepts {focus:false}; ordinary explicit opens retain
existing focus/reveal and FIFO semantics. A separate live status reports storage
failure without overwriting layout diagnostics. Read/remove/write are guarded.