ADR-0093: One Scope Context — the Toolbar Switcher Is the Only Scope Selector¶
- Status: accepted (Nicolas 2026-07-22: "only to have one of these selectors in the app and apply its filter everywhere… keep the filter at the top including the option to see all projects… if a project is selected there, this filter should be preselected in every other view… if 'all' is active and the selection is required, offer a dropdown with a neutral selection where the user has to pick a project first"; neutral-state UX delegated)
- Date: 2026-07-22
- Deciders: Nicolas Burri, UX detail by Claude. Origin: the obligations-oversight page's own scope selector (ADR-0089 §4) sitting beside the toolbar context switcher — two selectors, two states, confusing.
- Relates to: the toolbar context switcher (Amendment B,
core/context.ts), ADR-0080 (traceability workspace — the pages that grew local selectors), ADR-0002 (scope separation — filtering is display-side; the server enforces per-scope authorization regardless).
Citation note (2026-09-04). "Amendment B" above is the codebase's shorthand, not a document: no file under
docs/defines the label (grep-verified by the 2026-09-04 ADR-coherence review, which flagged it as an unresolvable reference). It names the toolbar context switcher introduced in the 2026-07-03 live-demo UX batch — his own design, "all projects + All, persisted" (worklog, 2026-07-03) — and its text lives in the code that implements it:core/context.ts, withdocument-list.ts,search-results.ts,inbox.ts,my-access.tsandinbox-count.tsall citing the same label for the filtered/deliberately-unfiltered rule. Read every "Amendment B" in this ADR as "the toolbar context switcher and its filtering rule".
Decision¶
- One state.
ContextService.context(a scope id or ALL, localStorage-persisted) is the app's single scope-filter state. The toolbar switcher is its one standing control; per-view scope selectors are removed. - Views that can honor ALL (documents list, search — the existing behavior) keep filtering client-side and showing everything on ALL.
- Views that require one scope (the per-scope traceability pages: coverage, chains, risk, training compliance, obligations oversight, report download — their APIs are per-scope by design) read the context:
- context = a scope → that scope is used directly, no view-level selector rendered;
- context = ALL → the view renders a neutral picker state: the page frame with a "Select a project" dropdown (placeholder, no preselection) and an explanatory empty state instead of data; nothing is fetched until a scope is picked.
- Picking writes the one state. Choosing a scope in a neutral picker calls
ContextService.select(...)— the toolbar switcher follows, and the filter now applies everywhere, exactly as if picked at the top. No local-only overrides: the switcher never shows something a view silently deviates from. - Deep links win on load. A URL that names a scope (route param/query) applies that scope to the context on navigation, keeping traceability links shareable; thereafter rule 4 governs.
- Authorization is unaffected. The context is a display filter; every per-scope API call
remains server-authorized (ADR-0002/0007). A stored context the caller can no longer see
falls back to ALL (existing
setScopesrule).
Consequences¶
- Frontend-only; no wire or backend change. The traceability pages' local selectors are removed; their e2e specs move to driving the toolbar switcher (or deep links) instead.
- The scope-config surface and admin pages are out of scope: they operate ON a chosen scope as an object of administration (their own navigation), not as a data filter.
- Inbox stays deliberately unfiltered (personal workload spans contexts — the recorded Amendment B exception).
Amendment (2026-07-22, Nicolas) — scope-config joins the one context¶
The scope-config exclusion above did not survive first contact: "If I change the project in the scope dropdown, the information does not get propagated to the global project selector (and vice versa)." The administer-vs-filter distinction is a designer's distinction, not a user's — revoked for scope-config. The scope-config picker becomes a view of the ONE context like every other (rules 3–5 apply): context = scope → administer it; changing the selection writes the context; context = ALL → the neutral picker. A context scope where the caller lacks the config permissions keeps the existing no-access state (the permitted-subset pattern the training/obligations pages already use). STILL excluded: admin pages and action-target pickers (draft-create, instantiate, link dialogs, my-training) — those choose the target of a single act, not a view's filter; and the inbox (unchanged).
Second amendment (2026-07-25, Nicolas) — action-target pickers READ the context as a default (read-yes, write-no)¶
The action-target-picker exclusion above ("those choose the target of a single act, not a view's filter") stands for the WRITE direction but was too strong for the READ direction. Play-test finding: a picker that ignores the context makes the user re-pick the project they already have in context for every new act. The decided rule — a target picker reads the context as its default preselection, and never writes it:
- When the toolbar context names a scope the caller may act in there, that scope is preselected in the picker (running the picker's normal on-scope cascade, exactly as a manual pick would), so the common case needs no re-pick.
- Changing the picker's selection stays LOCAL — it never calls
ContextService.select, so a one-off act in another project does not move the global filter (the write-exclusion of the first amendment is unchanged; this is the asymmetry: read the context, never write it). - ALL / "All projects" leaves the picker in its prior neutral/blank state — there is no scope to preselect, and nothing is invented.
- The rule applies only where a picker would otherwise start EMPTY. A picker that already defaults to a MORE specific, correct scope keeps that default — notably the trace-link dialog's target scope, which defaults to the document's own scope (the right target for an in-scope link); the amendment does not override a considered non-empty default.
Applied to: draft-create (context scope when authorable, else the tree's folder-selection scope, else blank), instantiate-dialog (context scope when authorable, else blank), and my-training's external-entry form (context scope when visible, else the first scope — its prior default). Left as-is: the trace-link dialog's target scope (already the document's own scope — a correct non-empty default). Authorization is unaffected — every act stays server-authorized regardless of the preselected scope (rule 6).