Messaging policy — Notices, inline lines, and the log¶
Where a message goes is decided by where you are looking, not by how the code happens to be structured (#546 C). This is the rule; the audit that produced it counted 145 Notices across 72 files with no policy behind them.
The three rules¶
-
Off-surface or asynchronous → a
Notice. A toast is the right tool for something that happened somewhere you are not looking: a background job finished, a vault event fired a hook, a command wrote a file you cannot see, an async reload completed. The toast is the only channel the user has for these, so it earns its interruption. -
On-surface → an inline line. If the message is about the thing in front of you — the modal you are in, the note the view is showing, the button you just pressed — it belongs on that surface, as a line near what it refers to, not as a toast that floats over the whole app. An error you can read where it happened is better than one you have to catch before it fades.
-
log.erroralways, for the detail. Every failure logs, regardless of what the user sees. TheLoggerwireserror()from construction so it surfaces even before settings load — an error is never silently swallowed. The user-facing message is the headline; the console is the detail.
The fourth rule, which is the point: no silent failure¶
The worked example is #544: a save failed, the code did if (!x) return;, and the thought vanished with nothing said. A user-triggered write that can bail out or throw must tell the user something — an inline line if you are on the surface, a Notice if the surface is closing or off-screen — and always log.error. A catch on a path the user explicitly triggered may never contain only a log.*.
This is guarded. test/architecture/components/noticesPolicy.test.ts:
- caps the toast budget — a ratcheting ceiling on the total count of
new Notice(. The number only goes down as on-surface toasts become inline lines; a new toast fails the build until the ceiling is raised deliberately, which is the moment to ask "is this really off-surface?"; - pins the #544-class fixes — the write paths that used to fail silently now surface a message, and the test asserts they still do.
Which is which — the buckets from the audit¶
| Bucket | Count at the audit | Where it goes |
|---|---|---|
| Error, off-surface (a service, a command, a hook, a canvas patch) | 50 | Notice — correct, keep |
| Error, on-surface (a modal / view / renderer you are in) | 35 | should become an inline line — migrated per surface, held by the ceiling meanwhile |
| Success, off-surface (an effect you cannot see: a file written elsewhere, the clipboard) | 17 | Notice — keep; the effect is off-screen |
| Success, on-surface (something you just watched happen) | 22 | remove — a confirmation for a visible effect is noise (C2) |
| Info / async (a background reload, an undo offer, a computed summary) | 21 | Notice — correct, keep |
The clipboard is off-surface: you cannot see that a copy happened, so "copied" is a legitimate Notice, not noise.
When you add a message¶
- Ask where the user is looking when it fires. On the surface → inline. Elsewhere →
Notice. - A success toast has to justify itself: what did it confirm that the user could not already see? If the answer is "nothing", do not add it.
- A failure on a path the user triggered is never optional. Surface it, and
log.errorthe detail. - Adding a
new Notice(raises the ceiling innoticesPolicy.test.ts. Raise it in the same change, with a one-line note saying why the message is off-surface — that note is the review gate.
Not a wrapper¶
There is deliberately no notify() choke point. A wrapper would let a toast be added without the one question that matters — where is the user looking? — being asked at the call site. The policy is a habit and a guardrail, not an abstraction. (See design by subtraction.)