address-routing¶
A single dashboard address vocabulary for clickable references — graph nodes, sessions, issues, and evals — projected to canonical hash URLs and executed through one navigation helper.
Clickable references in the dashboard name a product object first and a route second. A search row, an IssueCard, or any future node/session/review reference should all produce the same small app address shape, then let the shared address layer project it to the canonical destination.
The vocabulary is intentionally closed and mirrors the top-level pages side-nav already owns:
graph-nodefocuses a node on#/graph/<node-id>; the node id is one independently encoded path segment, so an address copied from any node reference can be opened, reloaded, and history-walked without relying on the tab's remembered focus. Bare#/graphremains the graph home and keeps its tab-local remembered/root fallback; an id no longer present in the current board also falls back safely rather than blanking the graph. Desktop selects and expands the node's drill-down; phone restores its ancestor breadcrumb and opens that node's screen. The incoming node parameter is applied when the graph route changes, not enforced on every render: after a direct open, mouse/keyboard/programmatic focus moves remain usable as local, transient graph navigation and leave the address unchanged. A new explicit address navigation or a copy action names the target node; high-frequency board movement never makes the address bar flicker.sessionopens#/sessions/<id>.session-evalopens the scoped default list#/evals?q=is:eval scope:<id>— or, with a node + scenario,#/evals/<node>/<scenario>?q=scope:<id>— the session-SCOPED Evals pages (session-eval / evals-view). This is the address an MR/CI note pastes so a reviewer one-clicks into the live, remarkable, worktree-rooted reading of an un-merged branch — and the address every session DOOR wears: the console tab bar's and the phone session header's eval entries are REAL anchors whose href is this projection, and the scoped Evals pages mint every scoped href (rows, queue neighbors, the detail's way back to the scoped list) through it too. Only that scoped list exposes the separate real anchor back to#/sessions/<id>; details first return to their canonical scoped list, so the scope grammar lives here and nowhere else. The old#/sessions/<id>/eval[/<node>/<scenario>]shape is LEGACY: the route layer normalizes it to this form on arrival (side-nav) and nothing mints it anymore.issueopens#/issues/<issue-id>— the issue's own DETAIL page (issues-view).evalopens#/evals/<node>/<scenario>— the eval's own DETAIL page, TRUNK-rooted (evals-view), path only (the detail hash carries no list filters); a not-yet-merged session reading's address issession-eval, not this. Scenario-less,eval(nodeId)is the node's AGGREGATE entry: the Evals LIST filtered to that node —#/evals?q=is:eval node:<id>, review-query's canonical token text (the default view + thenodequalifier, minted vianodeEvalQuery) — the address every aggregate score/count affordance (eval-score-badge) mints. The list-filter grammar lives in this one projection and nowhere else.
addressHash(address) is the href side: real anchors and copyable links get the canonical hash without
hand-rolled string assembly in components. navigateAddress(address, callbacks) is the SPA side: it follows
the same projection; graph-node focus is applied by the graph route itself, while the warm session page may
take its immediate selection callback. This makes a direct open, a review-node reference, and a palette pick
the same transaction instead of giving graph focus a second state channel.
addressUrl(address) is the clipboard side: it resolves that canonical hash against the browser's current
document URL, preserving its origin and project pathname (/p/<id>/) without a public-host setting. A copy
action therefore hands over a URL a recipient can open in the same deployment, not a bare hash or a local-only
address.
detailBackHash(page, scopeId) is the review details' return gate — the compact back anchor's href
(review-chrome's DetailShell), derived ONLY from the detail's own canonical address: #/issues from
an issue detail, the bare #/evals from a TRUNK eval detail, and the scoped DEFAULT list (the same
session-eval projection the doors mint, scope: token kept) from a SCOPED eval detail — "back" always
means the list on the detail's own data-source axis. The scope never diverts the back arrow to the
session console: a worktree-rooted reading reaches the terminal only through the scoped LIST's icon-only
door (evals-view). The helper takes no history, referrer, or session-presence input
at all, so a pushed visit and a direct open share one destination by construction.
Consumers may choose button or anchor chrome, but they do not decide the route vocabulary. That keeps review
objects first-class: issue and scenario references land on their owning review pages, never by accident on
the bound spec node or a node-popup tab.
Review list addresses also carry the ONE pagination grammar. Page follows q when one exists. Pagination
anchors preserve q and change only page, including minting explicit page=1 when returning to the first
page. Any query builder removes page before it PUSHES, so a new filtered population begins at the omitted
page-1 form. Direct open, refresh, Back, and Forward retain an explicit page=1; the two page-1 forms are
action/history state, not a canonical-address error. Automatic legacy normalization and invalid/non-positive
page repair REPLACE; human pagination/filter actions PUSH.