Sense 33: The Bell — When the System Notices, the Person Hears
Sense 33 · Steady · Last touched 2026-09-12
- last_verified: 2026-07-27
Synced from
docs/design/sense_33_the_bell.mdin the engine repo — that’s the source; this page is a build-time mirror.
The pipeline already notices many things. Today it whispers them into tables nobody reads. The Bell is where the noticing becomes a sound.
Not every noticing wants the same hearing. A wisdom that just confirmed wants a calm pointer in the workshop. A 100%-null column wants a louder note. A new finding may want no sound at all — just a counter. The Bell holds the shape that lets each one ring at the right volume.
Status: implementing (core landed; pretty much done — 2026-05-17) Date: 2026-05-12 Authors: the owner + Claude (morning conversation toward the RMC noon ship) Inherits:
- Sense 14 — signals fire findings; some findings are noisy
- Sense 18 — Identity & Passes gates who can dismiss / resolve / act
- Sense 29 — one of several consumers; the workshop can project rings anchored to artefacts it authors
- Sense 14 Subject Matter check verdicts — wisdoms with checks already produce confirmed/violated/no_data states Companion impl docs (to come):
notification_sources.md— how each of the five feeds gets wired innotification_surfaces.md— the Inbox, JinDesk bell, the Atelier projection, per-page panels
What pulled us in
Section titled “What pulled us in”The pipeline already notices a lot. Most of it is buried.
- Subject Matter wisdoms with checks fire
confirmed | violated | no_dataon every build. Today they’re stored insmebit_verdictsand surfaced one-at-a-time on Subject Matter pages — not aggregated into “things requiring your attention”. - Engine diagnostics (
W010,E***, etc.) are baked into_<tenant>.diagnostics. Not surfaced anywhere in the UI today. - Silver
is_valid = falserows already exist as data-quality flags. Not anchored to a user-facing surface. - Source-data gaps (a master export missing, a column 100% null, a signal that can’t fire) are noticed during build but only documented when a human writes a note about them.
- Notebook actions (Sense 21 / 23) already aggregate to
/actions— but only for explicit author-curated items, not for system-noticed concerns.
Five feeds. No common surface. The result: important findings stay buried until somebody happens to land on the right page. When the system notices “your supply-chains table is 100% null on its key column”, the person who could act on it currently has to discover it by browsing.
The recent supply-chains case made this concrete. The structural answer (“SAP IS-H is single-channel — see the wisdom”) is documented, but the human pointer — “hey, this is fishy, please look” — has nowhere to live. Resolutions happen in many places — a note here, a wisdom there, an action cell, a CLI command — but the awareness of “something wants you” needs a single home that every audience can reach, including audiences who never open an authoring tool.
The shape of the answer
Section titled “The shape of the answer”A Notification is a typed record that lets the system speak to the person without forcing the person to come looking.
Notification { id: <stable hash> source: wisdom | diagnostic | validity | data_gap | action | build_warning source_ref: <id of the originating thing — wisdom id, diag code, etc.> severity: info | heads_up | concern | alarm polarity: positive | neutral | negative message: { en, de, fr, ... } # short, human, action-oriented anchor: { surface, target_id } # where this should also appear inline suggested_action: { en, de, fr, ... }? # optional, structured for one-click conversion lifecycle: ephemeral | persistent created_at: timestamp resolved_at: timestamp? resolution: dismissed | snoozed_until | acted_via_<ref> | auto_cleared}The Bell is the single aggregator that produces these records and the singleton Inbox that lets them be heard — with secondary surfaces (JinDesk bell, Atelier projection, per-page banners, CLI, future TUI) consuming the same substrate.
The five sources
Section titled “The five sources”Each source publishes notifications during build (or, for some, at runtime). The contract: a source promises that a notification refers to a specific noticing, not a general state — so resolution is meaningful.
| Source | What it publishes | Example |
|---|---|---|
| wisdom | Smebit checks that flipped to confirmed or violated since last build, where the wisdom declares bell: true | ”SAP IS-H single-channel confirmed for this tenant — the I/O reconciliation family is N/A” |
| diagnostic | Engine warnings/errors with diagnostic codes (W010, E***); only those marked user-visible | ”Bake metadata: skipped exposure tile because gold_net_exposure has no leakage rows” |
| validity | Silver is_valid = false clusters above a threshold, grouped by invalid_reason | ”42 billing events failed validation (invalid_reason: missing case_id)“ |
| data_gap | Build-phase audits noticing structural absences — missing master export, 100%-null key columns, empty bronze tables that should populate | ”gold_supply_chains is empty because from_location is 100% null” |
| action | Notebook action cells whose status changed (pending → in_progress, proposed → completed) since last build | ”a viewer marked the MSEG data request ‘in_progress’” |
(A sixth source — runtime-reported errors from JinDesk itself — is plausible but out of scope here. The Bell speaks for build-time noticings; live-app errors belong to a different surface.)
Sources are plugin-shaped: each is a Python module that registers under notification_sources/ and exports produce(con, tenant) → list[Notification]. New sources don’t require Bell core changes — they register, they ring.
The Inbox — a singleton, with many consumers
Section titled “The Inbox — a singleton, with many consumers”The substrate is one table per tenant: _<tenant>.notifications. The Bell aggregator writes to it; everything else reads from it. The Inbox is the first-class surface over that table — a top-level destination at /[tenant]/inbox, a sibling of Findings / Theses / Observations, reachable without opening any authoring environment.
Three design commitments follow from making the Inbox a singleton:
- Audience independence. A Reader with only
view:bellshould reach the rings without needing the Atelier (which requires authoring capabilities). The Inbox is that route. - Surface diversity. The same substrate is consumed by JinDesk header bell, by the Atelier when projecting onto an authored artefact, by per-page banners, by the CLI, and eventually by a TUI / menu-bar app. No consumer owns the Inbox.
- Resolution is shared. Dismissing or snoozing a ring from any surface updates the same record. The history is one history, not one-per-tool.
The Inbox surface
Section titled “The Inbox surface”Lives at /[tenant]/inbox. The primary home for rings.
Properties:
- Two tabs: Active (unresolved rings — the default landing tab) and History (resolved rings, sorted resolved-most-recently first). History exists for retrospectives, audit (“what did we close last quarter?”), and pattern-finding (“we keep dismissing this — should it stop firing?”). Storage cost is near-zero.
- Grouped by source. wisdom-rings sit together, data_gap-rings sit together.
- Filterable by severity, polarity, lifecycle, anchor.
- Each notification carries its suggested action as a button that, when present, hands off to whatever surface owns the resolution: a new note in the Atelier, a new action cell, an
edit-this-wisdomroute, ajin <command>recipe. The Inbox launches; it does not author. - Resolution updates the notification record: dismissed (with reason), snoozed_until, acted_via (links to the resulting note / commit / action), or auto_cleared (when the next build no longer produces it).
- Pass-aware (Sense 18). The Inbox respects the role/pass model.
view:belllets a role read the Inbox in read-mode — see rings with explanations and anchors, but no resolution buttons and no suggested-action shortcuts that would open authoring forms. A read-mode user can leave comments on a ring (lightweight markdown thread, one column wider than the ring card) — authors see those comments when they next visit. This is a genuine collaboration primitive that, if it grows, could become its own Sense (the Thread). Resolution itself requiresact:bell(or higher).
The other consumers
Section titled “The other consumers”| Consumer | What it shows | How it differs from the Inbox |
|---|---|---|
| JinDesk status bell | Compact icon in the header, count of active concerns, dropdown of the top 3-5 rings with an Open Inbox footer | Awareness, not action. Placement: right of the locale switcher, left of the identity menu. Badge dot only for concern (amber) and alarm (red); heads_up is counted but un-painted. Persistent notifications only. |
| Atelier projection | A filtered panel inside Sense 29 showing rings whose anchors point at artefacts the current Atelier session can author (wisdoms, signals, notes, …) | A view, not a home. When the Atelier is closed the rings still exist; when a Reader visits the Inbox they see the same rings without the authoring shell. |
| Per-page banners | An inline panel on a specific entity / page driven by an anchor (e.g. the org-units list shows a “this is empty because…” banner driven by a data_gap notification anchored there) | Contextual. Renders where the noticing is about, not where rings live in general. |
| CLI | jin bell lists active rings; jin bell resolve <id> / --snooze / --dismiss mutate the same table | For headless / scripted workflows. Same substrate; no UI dependency. |
| TUI / menu-bar (future) | Persistent system tray awareness across tenants | Reads the substrate from every live tenant under the live root. Out of scope here. |
The Atelier doesn’t own the Inbox. The Inbox doesn’t depend on the Atelier. They share a substrate.
Cross-tenant aggregation
Section titled “Cross-tenant aggregation”Each tenant gets its own rings. A pack-level wisdom that fires confirmed on twelve tenants produces twelve rings — one per tenant Inbox — and each tenant’s authors resolve theirs independently. Per-tenant isolation is the default and matches the tenant-blindness invariant: no tenant’s runtime ever sees another tenant’s state.
But pack-level patterns benefit from a pack-level view. “This wisdom fires on these twelve tenants — do we want a bulk authoring action?” That question lives in the packhub clone, not in any tenant’s KLS. A pack-view surface (rendered by the Atelier when opened against the pack repo, or by any other pack-scoped tool) walks all known tenant KLSes and aggregates rings keyed by source-ref. Authors working at the pack level see “this wisdom fires confirmed on 12/15 tenants” and can propose a pack-wide change in one place. Tenant rings remain authoritative; the pack view is a read aggregator.
This pattern is generalisable beyond the Bell — it’s the same shape any pack-level dashboard would want — but the Bell is its first concrete use.
What the Bell is not
Section titled “What the Bell is not”- Not an alerting system. No emails. No pager. No 3am calls. The Bell is for noticings, not for incidents that require synchronous response. (If we ever want that, it’s a different Sense.)
- Not a logging system. Build logs continue to live where they live. The Bell only carries items that are meaningfully resolved by a human action.
- Not an exception catcher. Runtime errors (failed SQL, missing files at request time) belong to operational logs and to the Landscape’s structural-view observability (Sense 42 The Landscape — the Garden), not to the Bell.
- Not an authoring assistant. The Bell rings on existing noticings; it does not invent new wisdoms or signals. When a pattern recurs across multiple rings (same
pattern_id, same source-ref shape, different tenants), the Bell tags those rings so the Atelier’s authoring intelligence (Sense 29 layer 5) can ask “this looks systemic — author a wisdom?”. The Bell creates the hook; the suggestion engine acts on it.
The lifecycle
Section titled “The lifecycle” ┌─ build ─┐ ┌────────── human ──────────┐ │ │ │ │source.produce() notifications.aggregate() surfaces.render() │ ↓ │ │ notification diff with last filter, group, resolve records (auto_cleared?) rank by severity (dismiss / snooze / acted_via) │ │ └─→ write to _<tenant>.notifications ←───┘- At build time: each registered source produces its current notifications. The aggregator diffs against the previous build’s set. Rings that no longer apply get
auto_clearedin the KLS (the only resolution kind the aggregator itself writes). New rings get created. Existing rings keep theirid, so resolution state in SIS continues to point at them. - At read time: surfaces query
_<tenant>.notifications LEFT JOIN _<tenant>_sis.bell_resolutions USING (id), filtered byresolutions.resolved_at IS NULL AND (resolutions.snoozed_until IS NULL OR resolutions.snoozed_until <= now()). They group by source, anchor, severity. - At resolution: the surface writes to the SIS overlay (
_<tenant>_sis.bell_resolutions), never to the KLS. Same write target whether the resolution is a dismiss, a snooze, or anacted_via. The KLS stays read-only afterjin makeproduces it.
Snooze uses calendar time, not build counts. Builds are bursty and meaningless from the user’s perspective: three builds could be three minutes or three weeks. A snooze answers “I don’t want to think about this until next Tuesday”, not “I don’t want to think about this until N builds from now”. Default: +7 days. Per-source override via default_snooze_days in the source registration — some sources are weekly by nature, others want a longer beat.
This trades one property for another. Builds are reproducible; calendar time isn’t. So snooze states are not part of the reproducibility contract — only the produced rings themselves are. Resolution states are operational state, layered on top of the KLS, not baked into the analytical truth.
Abstain — silence is not the same as an all-clear
Section titled “Abstain — silence is not the same as an all-clear”Amended 2026-09-12, after an incremental build cleared two real rings.
The aggregator’s auto-clear rests on one inference: the source ran, looked, and found nothing. That inference is only sound when the source could actually look.
It often can’t. A lean KLS drops bronze and silver at finalization, so the next
incremental build presents the validity source with no silver_* tables at
all — not clean data, no data. Under the original contract the source returned an
empty list, indistinguishable from “every silver table is valid”, and the
aggregator cleared rings describing thousands of genuinely invalid rows. The same
hole swallowed any source that crashed or failed to import: its rings were cleared
because nothing came back from something that never ran.
A probe that reads a surface where the failure cannot appear will always report health. The fix is to let a source say I could not evaluate:
from jinflow.notifications.engine import SourceAbstained
def produce(con, tenant_id): tables = _eligible_tables(con, tenant_id) if not tables: raise SourceAbstained("no silver_* tables carry is_valid — lean or unbuilt KLS") ...The contract. A source’s produce() has three outcomes, not two:
| Outcome | Means | Effect on that source’s existing rings |
|---|---|---|
| returns rings | evaluated; these hold | re-fired (content refreshed) |
returns [] | evaluated; nothing holds | auto-cleared |
raises SourceAbstained | could not evaluate | left exactly as they are |
Partial abstention. A source that loops over many inputs — audits, silver tables — can establish part of its surface and fail on the rest. It passes what it did establish along:
raise SourceAbstained(f"audit '{aid}' did not run", rings=out)Those rings are written as normal; only the clearing is withheld. One unreadable table must not suppress the rings the readable ones just earned, and must not clear anything either.
Two failures are folded into abstain, because neither supports the all-clear inference either: a plugin that raises any other exception, and a plugin that failed to load at all. The rule the aggregator applies is therefore positive rather than negative — a ring is only auto-cleared when its own source ran to completion this build — which covers the load-failure case that no in-plugin signal could reach.
For that rule to work, the aggregator has to know whose rings just fell silent, and a source producing nothing tells it nothing. So a source module declares the name its rings carry:
SOURCE = "validity" # defaults to the module nameGet this wrong in the silent direction and rings can never clear; the aggregator
warns when a produced ring’s source disagrees with the module’s declaration.
Deliberate disabling is not abstaining. A source turned off via
bell.sources.<name>.enabled: false produces nothing and its rings do clear;
switching a source off is a statement about what should ring, not a gap in
evidence. Re-enabling re-fires them under the same stable ids.
Abstention is reported, never silent. aggregate() returns an abstained
count and jin make prints which sources abstained and why. A build that clears
nothing because it could see nothing must say so — otherwise the fix reintroduces
the original failure one level up.
Re-fired rings refresh their content. A re-fire used to bump last_seen_at
and nothing else, so a ring’s message, severity and anchor froze at whatever the
first build wrote — an action cell could be rewritten and the Inbox would keep
quoting the old text forever. The re-fire now writes the produced ring’s fields
over the stored row. The id and created_at stay put: it is the same ring,
saying what it says today. Resolution state is untouched by the refresh (human
resolutions still stand; only a prior auto_cleared is lifted).
The persistence boundary — rings in KLS, resolutions in SIS
Section titled “The persistence boundary — rings in KLS, resolutions in SIS”The Bell straddles two stores, on purpose:
-
Rings live in the KLS.
_<tenant>.notificationsis baked by the aggregator at the end of everyjin make. Rings are derived from the AFS (source plugins read engine + tenant artifacts and produce them deterministically), so they belong with the rest of the analytical truth. Reproducible from AFS, immutable for the lifetime of the build. -
Resolutions live in the SIS. When a human dismisses, snoozes, or acts on a ring, that decision writes to a sibling table in
_<tenant>_sis.duckdb. JinDesk readsKLS.notifications LEFT JOIN SIS.bell_resolutionsat request time. Resolutions persist across rebuilds — SIS is its own DuckDB file andjin makenever touches it on the write side.2026-05-17 update. This section’s claim that resolutions “are not flushed to AFS” turned out to be wrong as the design evolved. The current truth: durable resolutions (dismissed, acted_via, comments) ARE flushed to
afs/bell/*.yamland live on a dedicatedbellorphan branch alongside notes / bookmarks / recents. Snoozes stay SIS-only. The schema split intobell_resolutions(durable) +bell_snoozes(operational) shipped as schema v5. See/architecture/the_bell/for the current design. This section is preserved as the original framing.Resolutions are operational state, not analytical content: they describe how a particular human treated a particular ring on a particular deployment. They are not flushed to AFS at Phase 1d (the SIS → AFS publish step that handles note edits and bookmark coordinates). That exclusion is deliberate and permanent — make never reads bell resolutions and never should. If it ever does, that’s a bug.
Schema sits alongside notes / bookmarks / preferences in SIS:
_<tenant>_sis.bell_resolutions {id VARCHAR PRIMARY KEY -- matches notifications.idresolved_at VARCHAR -- ISO 8601 UTC, NULL when only snoozedresolution VARCHAR -- dismissed | snoozed | acted_via | (NULL)resolution_detail VARCHAR -- free-text or JSONsnoozed_until VARCHAR -- ISO 8601 UTC, NULL when not snoozedactor VARCHAR -- who resolved it (identity from Sense 18)resolved_via VARCHAR -- ring-card / cli / atelier / per-page bannercomment_thread_json VARCHAR -- lightweight markdown thread for read-mode collaboration}The corresponding columns on
_<tenant>.notifications(resolved_at, resolution, resolution_detail, snoozed_until) stay in the schema for now but get populated only byauto_cleared— the system’s own way of saying “the source plugin stopped producing this ring in this build.” Human-driven resolutions never touch the KLS. (A future cleanup may drop those columns from the KLS schema once the SIS overlay is the only write path.)
Cloud-mode behavior
Section titled “Cloud-mode behavior”Same rule as notes and bookmarks: there is one SIS per tenant, wherever it lives. In Local mode it’s on the developer’s machine; in Proxy / Cloud / Semi-Cloud / MaaS modes it’s in the cloud. The JinDesk reads + writes whichever SIS its deployment points at.
Local jin make’s Phase 1d already knows how to reach a remote
SIS for notes and bookmarks (today partially designed, partially
wired). Bell resolutions inherit nothing extra: Phase 1d never
reads them, so the transport story for resolutions is “the SIS
stays where it is, JinDesk keeps reading it.” No new
machinery needed.
Cross-reference. The SIS-as-overlay pattern is re-used by Sense 46 for a different substrate — see Sense 46 §Browser-side writes and the SIS overlay. That case is easier because Clerk (not SIS) is the source of truth for principals, so no ship-back is required — the pattern still works, but the round-trip closes through Clerk instead of through Phase 1d.
Why this matters
Section titled “Why this matters”This separation is what makes the Bell honest across operating modes.
- P2P2P: KLS is read-only on the owner’s machine. JinDesk
serves remote users; their resolutions write to the cloud SIS,
not back to the owner’s KLS. The owner’s
jin makecontinues to produce reproducible rings; resolutions accumulate independently. - Cloud (R2): KLS lives in R2 (built locally, uploaded). SIS lives in the cloud beside JinDesk. Resolutions never touch R2. The local make path stays pure.
- Make-as-a-Service: cloud build, cloud SIS. Same exclusion.
Taxonomy — severity, polarity, lifecycle
Section titled “Taxonomy — severity, polarity, lifecycle”Three orthogonal axes keep the Bell from collapsing into one giant unsorted bag.
Severity — how loud:
info: “you might want to know.” No badge count.heads_up: “this is worth a glance.” Counted, low-color badge.concern: “this needs attention soon.” Counted, amber badge.alarm: “this is blocking or wrong.” Counted, red badge. Rare.
Polarity — what kind of news:
positive: a wisdom check just confirmed in a good way; a thesis advanced from plausible to confirmed; a data-quality fix flipped a flag fromviolatedback toconfirmed. Rare today; common once the loop tightens.neutral: a state of the system the human should be aware of but doesn’t change anything.negative: the usual case — something is wrong or missing.
Lifecycle — how it dies:
ephemeral: cleared automatically by the next successful build (e.g. a build warning that doesn’t recur). Surfaces show them, but they don’t accumulate.persistent: stays until explicitly resolved or until the underlying noticing is gone for N consecutive builds (configurable; default 3).
Anchor — where it lives:
- Every notification has an
anchor: { surface, target_id }pointing at the page / entity / model that it’s about. Surfaces use this to render inline panels. Notifications without a meaningful anchor anchor totenant:*and show only in the Inbox (and any consumer that doesn’t filter by anchor).
What stays out of the Bell
Section titled “What stays out of the Bell”Equally important. Anti-patterns to avoid:
- Every signal finding. Findings are the analytical surface; they have their own pages, their own lenses. The Bell carries only change-in-state signals (a wisdom flipped, a data-gap appeared, an action moved) — not the millions of normal findings produced by the analytical pipeline.
- Aesthetic preferences. “This tenant hides the UoM Candidates list” is a
config.ymlchoice, not a Bell ring. The Bell is for things the system noticed, not for things the user configured. - Internal pipeline state. dbt run times, query plans, cache hits. Not the Bell’s business.
- Sales metrics. Engagement, click-throughs, time-on-page. Not the Bell’s business; if such a thing ever exists it has its own surface.
The discipline: every notification answers “what specifically should a human do or decide about this?” If the answer is “nothing”, the ring shouldn’t exist.
How this manifests in jinflow
Section titled “How this manifests in jinflow”The Bell is the pub/sub channel between writers (build, Atelier, evolve) and readers (JinDesk’s Inbox surface). Rings persist in the KLS (so every build carries its build-time notifications); per-Principal resolutions persist in the SIS (so dismissals are per-viewer, not global). Every commit through the Scribe boundary can raise a ring; the Inbox renders them grouped by severity, with snooze + dismiss + history.
- In the app: Inbox (the live ring surface, grouped by severity and lifecycle stage)
- CLI:
jin bell(subcommands for inspecting + flushing the pub/sub channel) - Persistence: rings → KLS table (build-time, deterministic) · resolutions → SIS table (per-viewer state)
Status: shipped. The Inbox surface is the first-class consumer; the Beat 2 path of the Heartbeat (Sense 21) routes through the Bell on every Atelier save.
Open questions
Section titled “Open questions”The first six open questions raised during this Sense’s drafting are settled in-line above (placement, pass-aware inbox, cross-tenant aggregation, calendar-time snooze, history tab, pattern-id hooks). They moved from this section into the body where they belong.
What remains genuinely open — landing in companion docs as the architecture meets implementation:
- Configurability. Which dimensions are per-tenant config (
config.yml) vs pack defaults? Initial cuts: per-source enable/disable per tenant, severity thresholds,default_snooze_daysper source. Likely needs a config sub-tree underbell:inconfig.yml. - Aggregator placement. Does aggregation happen as a post-build phase in
jin make, or as a separatejin pulsestep (Sense 21 / 32)? Lean: in-make for now, factor out only if the Bell starts producing latency. - Suggested-action vocabulary. The set of structured actions a notification can carry (
open new action cell,open new wisdom,open new note,open data-gap audit query, …). Each needs an Atelier-side handler. Inventory to be drawn before Phase 3. - Notification ID stability. A ring’s
idshould survive cosmetic source changes (a wisdom retitled, an entity renamed) so resolution history stays intact. Likelyhash(source, source_ref, anchor)— to be specified.
Relationship to existing surfaces
Section titled “Relationship to existing surfaces”| Surface | Today | After the Bell |
|---|---|---|
| Subject Matter detail | Shows the wisdom’s own check status | Unchanged — Bell aggregates rather than displacing |
/actions page | Shows note-action cells | Continues; Bell consumes the same source and surfaces the same items |
/findings page | Shows signal findings | Untouched. The Bell does not summarise findings. |
| Engine diagnostics | Console output only | Selected codes become Bell notifications when user_visible: true |
Inbox (/[tenant]/inbox) | Doesn’t exist | First-class Bell surface — top-level nav, reachable by every audience |
| Atelier (Sense 29) | Doesn’t exist | Optional projection panel showing rings anchored to authorable artefacts |
| JinDesk header | Locale switcher, identity, search | Plus a bell icon with severity-colored badge, dropdown linking to the Inbox |
The Bell is additive. Nothing existing breaks; existing surfaces continue to serve their narrow purpose; the Bell is the cross-cutting awareness layer.
Why now
Section titled “Why now”Three reasons the Bell becomes load-bearing in May 2026:
- RMC noon ship surfaced three different “empty” patterns, each with its own resolution path. Without the Bell each one would have its own ad-hoc surface or stay buried. The Bell is the structural answer to the question “how does JinDesk know what doesn’t apply” — which is the question that drove this Sense.
- The Atelier (Sense 29) is being designed in parallel, and it’s one of several consumers that will project the Inbox onto authorable artefacts. Defining the Bell as a singleton substrate now keeps the Atelier from accidentally owning a primitive that has to serve many audiences — including Readers who never open an authoring tool.
- Multi-tenant has tipped past the threshold where a person can keep “what’s interesting on tenant X” in their head. Five tenants is fine; fifty is a different game. The Bell is the surface that scales attention.
Implementation phases (sketch, for the companion docs)
Section titled “Implementation phases (sketch, for the companion docs)”Not part of this Sense, but the rough sequencing:
- Phase 0 — table. Define
_<tenant>.notificationsschema and the aggregator skeleton. No sources wired yet. - Phase 1 — first wire. Smebit verdicts → notifications. Renders in a stub Inbox at
/[tenant]/inbox. - Phase 2 — second wire. Data-gap detection (the supply-chains-shaped finding) → notifications. The first real “the system noticed something fishy” use case.
- Phase 3 — the Inbox surface. Real route with grouping, filtering, suggested-action handoff, read-mode for
view:bell, resolution foract:bell. Top-level nav entry. - Phase 4 — JinDesk bell. Header icon with badge, opens the Inbox.
- Phase 5 — Atelier projection. Filtered panel inside Sense 29 showing rings anchored to authorable artefacts, with one-click handoff to the relevant authoring form.
- Phase 6 — remaining sources. Diagnostics, validity, actions wire in one by one.
- Phase 7 — CLI / TUI.
jin belland downstream headless consumers.
Each phase ships independently. The architecture is the contract; the wiring is the work.
Naming notes
Section titled “Naming notes”“The Bell” was chosen for:
- Compact. One syllable. Pairs naturally with verbs (“ring the Bell”, “the Bell rang”, “an unrung notice”).
- Workshop register. Pairs with the Atelier (a workshop bell, not a phone notification).
- Tone-honest. A bell is heard, not pushed. The Bell rings; the person decides whether and when to answer.
- Not overloaded. “Inbox” carries email baggage; “Watch” is ambiguous; “Concern” is too negative for the positive-polarity case.
Alternatives considered: The Concern, The Watch, The Bulletin. Each had a corner where it fit better than the Bell — and a bigger corner where it fit worse. (The Inbox is the name of the primary surface, not of the primitive. The Bell is what rings; the Inbox is where you read the rings — same separation as a notification system and its mailbox.)
Numerical neighbors: ← Sense 32: The Chores — The Pulse That Doesn’t Beat Itself · Sense 34: The Breeze — Lateral Signalling Through the Jinflow Void →