janela 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ ---
2
+ Date: 2026-09-20
3
+ Status: Accepted
4
+ Related: ADR 002, ADR 004, ADR 015, ADR 019, ADR 021, ADR 025
5
+ Superseded in part by: ADR 035
6
+ Triggers:
7
+ - deciding what Janela should do when a host has configured nothing
8
+ - adding a hook a host answers by defining a method
9
+ - a dashboard showing rows the person reading it should not see
10
+ - choosing a default for anything that decides what may be read
11
+ - writing a doctor check about authorisation
12
+ Topics: authorisation, tenancy, host-integration, security, configuration, releases
13
+ ---
14
+
15
+ # ADR 032: Janela Will Not Read a Model It Has Not Been Told How to Scope
16
+
17
+ ## Context
18
+
19
+ `janela_scope` asks the host's controller for `policy_scope` and returns
20
+ `model.all` when there is none. ADR 002 called that authorisation being a
21
+ hook rather than a dependency, and ADR 004 recorded the gap in its
22
+ consequences as "tracked as a pre-public issue". The gem is public now,
23
+ so the condition that deferral was written under has passed.
24
+
25
+ What the fallback does, measured against the demo with `policy_scope`
26
+ removed from the host, which is the state an application using a
27
+ different authorisation library is already in:
28
+
29
+ ```
30
+ a pane the scope hides, with policy_scope: 404
31
+ the same pane, without: 200, showing $375.00
32
+ log lines mentioning scope, policy or janela: 0
33
+ ```
34
+
35
+ The request that should be a refusal becomes a number belonging to
36
+ somebody else, with nothing on the page or in the log to say so. Janela
37
+ treats a confidently wrong number as its worst failure, and this is the
38
+ purest form of it: not a pane that errors, a pane that lies.
39
+
40
+ Two facts narrow who this actually hits, and both were wrong in the
41
+ issue as filed.
42
+
43
+ **A host using Pundit is not affected, even one missing a policy.**
44
+ Pundit's `policy_scope` resolves through `policy_scope!`, which raises
45
+ `NotDefinedError`. Verified in Pundit 2.5.2: `Authorization#policy_scope`
46
+ calls `pundit_policy_scope`, which calls `pundit.policy_scope!`. A
47
+ forgetful Pundit host gets an exception, which is the correct outcome
48
+ and needs nothing from us. The exposure is a host that defines no
49
+ `policy_scope` at all.
50
+
51
+ **The two copies of the fallback do not agree with each other.** The
52
+ controller copy asks `self`, which is the controller; the helper copy
53
+ asks the view, which cannot see a private controller method. A host
54
+ following this project's own multi-tenancy guide, which writes
55
+ `policy_scope` as a private method with no `helper_method`, is scoped on
56
+ the engine's pages and unscoped on a frame embedded in its own. That is
57
+ a bug rather than a decision and is tracked separately; this ADR assumes
58
+ it is fixed and that both paths ask the controller.
59
+
60
+ Four options were considered.
61
+
62
+ **Log loudly.** Breaks nothing, and a log line in an application that is
63
+ already noisy is close to the silence it replaces. It also leaves the
64
+ wrong number on the screen, which is the part that matters.
65
+
66
+ **A configured scope resolver**, `Janela.scope = ->(model, controller)`.
67
+ Rejected on ADR 019, which found that a decision needing the current
68
+ user and tenant belongs inside a request rather than in a setting, and
69
+ on ADR 001, because the host's controller already answers this question
70
+ and a second way to answer it is a second thing to understand.
71
+
72
+ **A doctor check alone.** Rejected as insufficient rather than wrong.
73
+ The guide currently instructs a single tenant host to define nothing, so
74
+ a check flagging that would contradict the documentation a reader just
75
+ followed, which is the false alarm ADR 021 exists to prevent. It is also
76
+ advisory: it speaks at setup time and cannot stop a request.
77
+
78
+ **Raise.** Chosen, below.
79
+
80
+ The tension is real. `model.all` is the correct relation for a single
81
+ tenant application, and most applications are single tenant, so raising
82
+ asks something of hosts that were never wrong. Two things settle it.
83
+ This project has met the same shape twice and chosen the same way both
84
+ times: ADR 002 raises where Ransack silently dropped a filter, and ADR
85
+ 025 raises where any predicate silently ran. And "single tenant" is not
86
+ the same claim as "every visitor may total every row of every model on a
87
+ dashboard". Today that second claim is made by omission. It should be
88
+ made on purpose or not at all.
89
+
90
+ ## Decision
91
+
92
+ **Janela raises `Janela::Unscoped` when it would otherwise read a model
93
+ the host has not told it how to scope.** The fallback to `model.all` is
94
+ removed from both the controller and the helper.
95
+
96
+ A host says what may be read by answering the question Janela already
97
+ asks, in the place it already asks it:
98
+
99
+ ```ruby
100
+ class ApplicationController < ActionController::Base
101
+ private
102
+ def policy_scope(model) = model.all
103
+ end
104
+ ```
105
+
106
+ That line is not ceremony. It is the assertion that every visitor who
107
+ can reach a dashboard may read every row behind it, which is a sentence
108
+ a maintainer should have to write rather than inherit.
109
+
110
+ No new setting. ADR 019's pattern stands: Janela asks the host's
111
+ controller by duck typing and never by configuration.
112
+
113
+ **It raises at request time, not at boot.** Resolving
114
+ `Janela.parent_controller` during initialisation is a trap this project
115
+ has already paid for, and a boot raise would break a console or a
116
+ migration for a host part way through upgrading.
117
+
118
+ **It is not rescued into a pane.** A missing scope is not a data
119
+ condition, and dressing it as the sentence a 404 shows would hide the
120
+ thing the host has to fix.
121
+
122
+ **The doctor gains `unscoped-reads`, at error severity.** Unlike the
123
+ `unauthenticated-endpoints` check, which hedges because authentication
124
+ cannot be determined by reading, this one is exact: the parent
125
+ controller either responds to `policy_scope` or it does not.
126
+
127
+ ## Consequences
128
+
129
+ - A host that defines no scope stops getting numbers and starts getting
130
+ an exception naming what to add. Loud, once, instead of quiet forever.
131
+ - Breaking, so it goes in a minor release with an `UPGRADING.md` section
132
+ and a `post_install_message` (ADR 015). A host using Pundit, or one
133
+ that followed the multi-tenancy guide, does nothing. A single tenant
134
+ host writes one method.
135
+ - ADR 004's consequence recording this as a pre-public issue is
136
+ superseded.
137
+ - **ADR 019 is narrowed rather than superseded.** It generalised taking
138
+ silence as an answer into a precedent and cited `policy_scope` as the
139
+ model for it. That precedent survives with a limit: silence is an
140
+ acceptable answer when the silent default is the narrow one, as a nil
141
+ owner is, and never when it is the widest one. A future hook that
142
+ defaults to reading everything is this decision again, not ADR 019.
143
+ - ADR 002's `on:` parameter keeps defaulting to `model.all`. That is a
144
+ call a maintainer writes in their own Ruby, where the scope is theirs
145
+ to choose and nothing is being decided on their behalf. The seam is
146
+ between what Janela chooses inside a request and what a host asks for
147
+ directly.
148
+ - The multi-tenancy guide's "define nothing" section, its summary table
149
+ and the README's description of the fallback all become wrong the
150
+ moment this lands, and change in the same commit.
151
+ - Duck typing still means a typo in the method name reads as absence.
152
+ Before this, that silently widened the scope; now it raises, so the
153
+ doctor check is a convenience rather than the only defence.
154
+ - What would change this decision: evidence that the raise fires for
155
+ hosts that had genuinely done nothing wrong and had no reasonable way
156
+ to know, in numbers rather than anecdote. A permissive default is not
157
+ coming back, but where the question is asked could.
@@ -0,0 +1,151 @@
1
+ ---
2
+ Date: 2026-09-21
3
+ Status: Accepted
4
+ Related: ADR 002, ADR 009, ADR 014, ADR 019, ADR 032
5
+ Triggers:
6
+ - adding an owner or a tenancy column to anything the engine stores
7
+ - creating a record outside a request, in a job, a task or a console
8
+ - scheduling a snapshot, or writing a job around Snapshot.take
9
+ - a snapshot the wrong audience can read
10
+ - extending ADR 019's controller hook to something new
11
+ Topics: snapshots, tenancy, authorisation, persistence, activejob, host-integration
12
+ ---
13
+
14
+ # ADR 033: A Snapshot Is Told Who Owns It, Because There Is No Request to Ask
15
+
16
+ ## Context
17
+
18
+ `janela_snapshots` holds a name, an instant, the filters it was taken
19
+ under and the results. It has no owner column, so
20
+ `policy_scope(Janela::Snapshot)` has nothing to filter on. ADR 014 gave
21
+ a frame a nullable polymorphic owner for exactly this reason and said
22
+ Janela never interprets it. A snapshot never got the same.
23
+
24
+ The guide calls this the rough edge and offers three workarounds: keep
25
+ snapshots to one tenant, add a column of your own, or encode the tenant
26
+ in the filters and scope on the name. The demo does the first, crudely
27
+ and on purpose. None of them are a column a policy can filter on.
28
+
29
+ Measured against the demo's fixtures before deciding anything:
30
+
31
+ ```
32
+ every order: 375.0
33
+ one tenant's orders: 150.0
34
+ what Janela::SnapshotJob stored: 375.0
35
+ what take with an explicit on: 150.0
36
+ owner column on janela_snapshots: none
37
+ ```
38
+
39
+ Two separate problems sit in those five lines, and only the first is
40
+ this ADR's.
41
+
42
+ **A snapshot cannot be owned.** There is nowhere to put the answer, so a
43
+ multi tenant host cannot scope reads of one.
44
+
45
+ **Janela's own job chooses a scope.** `SnapshotJob` calls `take` with no
46
+ `on:`, which ADR 009 decided deliberately, because a job cannot receive
47
+ a relation. Whether that is wrong depends on where a host keeps its
48
+ tenancy: at the model layer, through acts_as_tenant or a default scope,
49
+ `model.all` is already the tenant's rows and the job is correct, which
50
+ is what the guide's own example means by "acts_as_tenant has already
51
+ scoped your own models". At the policy layer, through Pundit or
52
+ CanCanCan, it is every row and the host's scope never runs. Janela
53
+ cannot tell which it is in, so what it has is a default chosen by the
54
+ library where ADR 032 would have it stated by the host. Not solved
55
+ here: it is a different question with a different shape, and folding it
56
+ in would make this one about two things. Filed as #47 so it cannot be
57
+ lost.
58
+
59
+ The interesting part of the first problem is where the answer comes
60
+ from. ADR 019 settled that for a frame by asking the host's controller
61
+ for `janela_frame_owner`, and generalised it into a precedent. It does
62
+ not reach here. A frame is created in a request, by an analyst using
63
+ the engine's own form, so there is a controller to ask and a
64
+ `Current.tenant` to ask it about. A snapshot is never created in a
65
+ request: there is no create route, no controller action and no form,
66
+ only `Snapshot.take` called from a job, a rake task, a console or a
67
+ host's own code. Verified rather than assumed, by reading every call
68
+ site in the repository.
69
+
70
+ So the options were not "which hook" but "what does a method called
71
+ outside a request know".
72
+
73
+ **A controller hook anyway**, with `Snapshot.take` reaching for
74
+ `Current.something`. Rejected: it would work in a console and quietly
75
+ return nil in the job that is the whole point of the feature, which is
76
+ the silent-failure shape ADR 019 was written to avoid in the first
77
+ place.
78
+
79
+ **A setting**, `Janela.snapshot_owner = -> { ... }`. Rejected on ADR
80
+ 019's own reasoning, which found that a judgement needing the current
81
+ tenant cannot live in a setting, and on ADR 001, since it is a second
82
+ way to answer a question an argument already answers.
83
+
84
+ **Inferring it from the panes' relations.** Rejected as impossible
85
+ rather than unwise: a relation does not name a tenant, and reading one
86
+ to guess would be Janela interpreting an owner, which ADR 014 forbids.
87
+
88
+ ## Decision
89
+
90
+ **A snapshot's owner is an argument to `Snapshot.take`, because the
91
+ caller is the only thing that knows it.**
92
+
93
+ ```ruby
94
+ Janela::Snapshot.take(name: "September 2026", owner: Current.account) do |take|
95
+ take.pane Order, :revenue, on: policy_scope(Order)
96
+ end
97
+ ```
98
+
99
+ `janela_snapshots` gains nullable polymorphic `owner_type` and
100
+ `owner_id`, the same shape as a frame's, through the usual
101
+ `rails janela:install:migrations`. Janela assigns what it is handed and
102
+ reads nothing from it. Scoping stays entirely the host's policy, exactly
103
+ as ADR 014 said of a frame.
104
+
105
+ **`SnapshotJob` takes an `owner:` too.** ActiveJob serialises an
106
+ ActiveRecord object through its GlobalID, so unlike a relation an owner
107
+ crosses the queue boundary without anything new being invented.
108
+
109
+ **The owner is optional.** ADR 032 narrowed ADR 019's
110
+ silence-as-an-answer rule to permit it only where the silent default is
111
+ the narrow one, and named a nil owner as the example. A snapshot nobody
112
+ owns is invisible to a policy that filters on owner, which is the safe
113
+ direction to fail, and correct for a single tenant application where
114
+ nothing is filtering on it.
115
+
116
+ **This ADR does not extend ADR 019, it bounds it.** The rule is now
117
+ legible: the engine asks the host's controller when the engine is the
118
+ thing doing the creating inside a request, and takes an argument when
119
+ the host is. A future record written by Janela should be read against
120
+ which of those two it is, rather than reaching for the controller hook
121
+ because a frame does.
122
+
123
+ ## Consequences
124
+
125
+ - A multi tenant host can scope snapshots with the same policy it
126
+ already writes for frames, and the guide loses its rough edge.
127
+ - A migration a host must run, so it goes in `UPGRADING.md` with the
128
+ install task and the fact that existing snapshots keep a nil owner
129
+ (ADR 015). Nothing breaks for a host that ignores it.
130
+ - `Snapshot.take`'s signature grows one keyword with a default, so every
131
+ existing call keeps working unchanged.
132
+ - **The shipped `SnapshotJob` still picks each model's default scope.**
133
+ Right for a host whose tenancy is in the model layer, silently wrong
134
+ for one whose tenancy is in its policies. Adding an owner sharpens
135
+ that rather than softening it: the snapshot is now scopable, so its
136
+ contents are the only unscoped thing left, and a host may reasonably
137
+ read the new column as a promise the numbers inside do not keep. This
138
+ wants its own decision and has one, #47; until it is settled the guide
139
+ has to keep saying plainly that a policy scoped host writes its own
140
+ job.
141
+ - Janela reads nothing from the owner, so a host that assigns one and
142
+ writes no policy has changed nothing. That is the frame's trap again,
143
+ and `frames_nobody_will_own` is the doctor check that catches the
144
+ frame version. The snapshot version is not the same shape, because the
145
+ caller assigns the owner in its own Ruby rather than the engine doing
146
+ it silently, so it is a thinner case for a check. Worth revisiting if
147
+ it bites.
148
+ - What would change this decision: a snapshot being created inside a
149
+ request, through an engine form the way a frame is. That would put a
150
+ controller back in the picture and make ADR 019's hook the consistent
151
+ answer, and this ADR should be read again rather than worked around.
@@ -0,0 +1,238 @@
1
+ ---
2
+ Date: 2026-09-21
3
+ Status: Accepted
4
+ Related: ADR 001, ADR 002, ADR 009, ADR 014, ADR 019, ADR 021, ADR 032, ADR 033
5
+ Triggers:
6
+ - scheduling a snapshot, or changing what Janela::SnapshotJob takes
7
+ - choosing a default for anything that decides what may be read outside a request
8
+ - a snapshot whose numbers disagree with the page it was published from
9
+ - adding a setting so a host can replace something the gem ships
10
+ - wondering whether the shipped snapshot job should exist at all
11
+ Topics: snapshots, tenancy, authorisation, activejob, host-integration, security, releases
12
+ ---
13
+
14
+ # ADR 034: Janela Will Not Freeze a Scope the Host Has Not Named
15
+
16
+ ## Context
17
+
18
+ `Janela::SnapshotJob` calls `Snapshot.take` with no `on:`, so every pane
19
+ it freezes reads the model's default scope. ADR 009 decided that
20
+ deliberately and documented it: a relation cannot be serialised into a
21
+ job, so "a tenanted host writes its own job around `Snapshot.take` and
22
+ passes `on:`".
23
+
24
+ Whether that is wrong depends on something Janela cannot see.
25
+
26
+ **Tenancy at the model layer**, through acts_as_tenant, a `default_scope`,
27
+ a per tenant connection or a schema. `model.all` is already the tenant's
28
+ rows. The shipped job is correct and nothing here applies. This is what
29
+ the multi tenancy guide's own example means when it writes
30
+ `else model.all # acts_as_tenant has already scoped your own models`.
31
+
32
+ **Tenancy at the policy layer**, through Pundit or CanCanCan. `model.all`
33
+ is every row, and the scope the host wrote never runs.
34
+
35
+ So this is not the flat bug the issue title says. It is a default that is
36
+ right for one common architecture and silently wrong for another, chosen
37
+ by the library where ADR 032 would have it stated by the host.
38
+
39
+ ### Measured
40
+
41
+ The demo does not scope `Order` by tenant: its `policy_scope` filters
42
+ Janela's own owned records and answers `model.all` for everything else,
43
+ which makes it a model layer host for its own data. The measurement below
44
+ was taken with that one method narrowed to what a policy layer host
45
+ writes, `when "Order" then model.where(customer: Current.tenant)`, and
46
+ nothing else changed.
47
+
48
+ ```
49
+ live pane, signed in as one tenant: $150.00
50
+ snapshot the shipped job froze for them: 375.0
51
+ that snapshot's owner column: that tenant
52
+ what they are then served from the snapshot: $375.00, answered 200
53
+ ```
54
+
55
+ The same application, at the same instant, shows one number on the page
56
+ and publishes another from the row it labelled as theirs. Nothing errors
57
+ and nothing in the log says so. ADR 032 called a pane that lies the worst
58
+ thing this library can do, and this is that pane with a URL and a
59
+ lifetime.
60
+
61
+ ### Why it is worth a decision rather than a patch
62
+
63
+ **It persists, and it publishes.** A live unscoped read is wrong once, on
64
+ a screen, to somebody already authenticated. A snapshot freezes the
65
+ number into a row and serves it at an address. ADR 009 names the
66
+ motivating case as "an external audience with no accounts", so the
67
+ failure mode is frozen cross tenant data published to people outside the
68
+ application altogether.
69
+
70
+ **ADR 033 made the promise louder.** Giving `janela_snapshots` an owner
71
+ invites a host to read the row as "this snapshot belongs to this tenant".
72
+ The row now says so. Its contents do not have to agree, and ADR 033 said
73
+ in as many words that adding the column sharpens this rather than
74
+ softening it.
75
+
76
+ ### What a fix has to get past
77
+
78
+ A relation cannot be serialised into a job. That constraint produced the
79
+ current behaviour and has not moved.
80
+
81
+ **Require `on:` on `Taking#pane`.** Makes the guess impossible, and
82
+ supersedes ADR 009's default for every caller, including the console and
83
+ rake callers for whom it was always right. ADR 032 explicitly left `on:`
84
+ alone on the grounds that it is a call a maintainer writes in their own
85
+ Ruby, where nothing is being decided on their behalf.
86
+
87
+ **A serialisable way to name a scope**, such as
88
+ `{ model: "orders", scope: "for_tenant" }`. Rejected twice over: it is a
89
+ query language in miniature, which ADR 001 and ADR 002 both push against,
90
+ and it is arbitrary class method invocation from queue arguments, so
91
+ whoever can enqueue a job can call anything.
92
+
93
+ **Derive the scope from the owner ADR 033 added.** Janela would have to
94
+ know how a host's models relate to a tenant, which means interpreting the
95
+ owner, which ADR 014 and ADR 033 both forbid.
96
+
97
+ **Detect which kind of host this is.** The obvious idea, and ADR 032 is
98
+ what kills it. After ADR 032 every host defines `policy_scope`, and a
99
+ model layer host and a single tenant host both define it as `model.all`,
100
+ so responding to it distinguishes nothing. There is no controller
101
+ instance in a job to ask in any case, and no current tenant for it to
102
+ answer about.
103
+
104
+ **A setting naming the host's own job**, `Janela.snapshot_job =
105
+ "TenantSnapshotJob"`. Rejected because it has nothing to indirect.
106
+ Verified by reading the engine: nothing in `app/` or `lib/` enqueues
107
+ `SnapshotJob`, there is no publish action and no create route, and ADR
108
+ 009 decided there would not be. The host's own scheduler is the only
109
+ caller and it already names a class, so the setting would be read by
110
+ nobody. It is the shape to reach for on the day Janela becomes the
111
+ caller, which ADR 033 already named as the thing that would reopen all of
112
+ this, and until then it fails the test ADR 021 and ADR 023 set for
113
+ whether a setting has earned itself.
114
+
115
+ **Remove `SnapshotJob`.** Not a straw man. ADR 009's own consequences say
116
+ "the shipped job is deliberately naive. If most hosts turn out to write
117
+ their own, the job should be removed rather than grown", and the
118
+ documentation has been sending tenanted hosts to write their own since
119
+ the day it shipped. It loses to two things. The evidence ADR 009 asked
120
+ for does not exist: there is one demo, not a population of hosts. And
121
+ removal costs model layer and single tenant hosts a real convenience to
122
+ solve a problem neither of them has.
123
+
124
+ **A doctor check alone.** Rejected for ADR 032's reason and one of its
125
+ own. It is advisory, so it speaks at setup time and cannot stop a job.
126
+ And it cannot tell the two architectures apart any better than the
127
+ library can, so it would either stay silent or cry wolf at every host,
128
+ which is the false alarm ADR 021 exists to prevent.
129
+
130
+ ## Decision
131
+
132
+ **The snapshot job asks one question, `scope_for(model)`, and refuses to
133
+ answer it on the host's behalf.**
134
+
135
+ Everything above is one question wearing two costumes: what rows does
136
+ this pane freeze? It becomes one method on the job, and the two
137
+ architectures answer it in the two ways each can answer honestly.
138
+
139
+ **A model layer or single tenant host answers in a symbol.** The shipped
140
+ `scope_for` reads a `scope:` argument and has no default:
141
+
142
+ ```ruby
143
+ Janela::SnapshotJob.perform_later(name: "September 2026", owner: account,
144
+ scope: :model_default,
145
+ panes: [ { "model" => "orders", "measure" => "revenue" } ])
146
+ ```
147
+
148
+ `:model_default` says each pane is taken over its model's default scope,
149
+ and that this is the host's claim rather than Janela's guess. It is the
150
+ same move as `private def policy_scope(model) = model.all`: not
151
+ ceremony, a sentence somebody should have to write rather than inherit.
152
+ Absent, `scope_for` raises `Janela::Unscoped`, the same class and the
153
+ same word as ADR 032, because it is the same refusal in the other half
154
+ of the library. A value Janela does not know raises `ArgumentError`,
155
+ plainly, because a job's `perform` is a method call and not a request.
156
+
157
+ **A policy layer host answers in Ruby, by subclassing.** A relation
158
+ cannot cross the queue, but a class name can, and the host's scheduler
159
+ already names one:
160
+
161
+ ```ruby
162
+ class TenantSnapshotJob < Janela::SnapshotJob
163
+ private def scope_for(model) = model.where(account: owner)
164
+ end
165
+ ```
166
+
167
+ They schedule that instead, and pass no `scope:`, because the method that
168
+ reads it is the one they replaced. `Janela::SnapshotJob` is a documented
169
+ base class rather than a leaf, and the serialisable `panes:` list, which
170
+ is the only genuinely reusable part of it and the part a host would
171
+ otherwise copy, keeps working. `perform` records what it was told and
172
+ exposes `owner`, `name` and `filters` as private readers, so a subclass
173
+ never has to override `perform` or reach into ActiveJob's `arguments`.
174
+
175
+ **This is a hook, and ADR 032 is why it is allowed to be one.** A
176
+ `scope_for` whose unanswered case returns `model.all` is exactly the
177
+ shape ADR 032 removed, and was rejected on that ground while the keyword
178
+ was a separate idea. Folding the two together inverts it: the default is
179
+ not the widest scope, it is a refusal. A host who has never heard of
180
+ `scope_for` does not get somebody else's numbers, they get an exception
181
+ naming both ways to answer.
182
+
183
+ **One question, so one place to answer it.** A symbol and a subclass are
184
+ not two competing APIs in ADR 001's sense. They are the same method,
185
+ answered by a caller who has nothing to add and by a caller who does.
186
+
187
+ **`Snapshot.take` and `Taking#pane` are untouched.** `on:` keeps
188
+ defaulting to `model.all`. ADR 032 drew that seam and it holds: the line
189
+ is between what Janela chooses inside its own code and what a host asks
190
+ for directly, and `take` is the host's own Ruby.
191
+
192
+ **No new doctor check.** A check earns itself by catching something quiet
193
+ (ADR 021), and after this there is nothing quiet left to catch: the job
194
+ either was answered or it raises. That is the opposite of the
195
+ `snapshots-nobody-will-see` case, where the failure was a row that simply
196
+ sat there.
197
+
198
+ **ADR 009 is narrowed, not superseded.** Everything it decided about what
199
+ a snapshot stores, how a stored pane is addressed and what static mode is
200
+ stands. Two sentences change. "The shipped job uses each model's default
201
+ scope" becomes "when told to", and "removed rather than grown" is
202
+ answered: it grows by one overridable method, which is how Rails ships a
203
+ default a host can replace.
204
+
205
+ ## Consequences
206
+
207
+ - A policy layer host's instruction stops being "write your own job" and
208
+ becomes "override one method", which is a sentence the multi tenancy
209
+ guide can end its rough edge section with instead of a link to an
210
+ issue.
211
+ - **Breaking, by one keyword**, so it goes in a minor release with an
212
+ `UPGRADING.md` section and a `post_install_message` (ADR 015).
213
+ - **A job already on the queue at upgrade time was serialised without
214
+ `scope:` and will raise when it performs.** A deployment hazard rather
215
+ than a code change, invisible from the diff, and it belongs in the
216
+ upgrade note beside the keyword: drain or re-enqueue before deploying,
217
+ or expect the retries.
218
+ - **`SnapshotJob`'s insides become public surface.** `scope_for` and the
219
+ readers beside it are a host's to override, so changing the shape of
220
+ the `panes:` loop is a breaking change from here on, where today it is
221
+ an internal detail. That is the real price of a base class over a leaf,
222
+ and it is paid knowingly: the surface is one method and three readers,
223
+ which is small enough to keep legible and small enough to fork past.
224
+ - The job stays naive on purpose. It does not become the place where
225
+ scoping is solved. It becomes the place where the host says who is
226
+ solving it.
227
+ - The guide's "what is still rough" section and the README's paragraph on
228
+ the job both change in the same commit that lands the code.
229
+ - A host who writes `scope: :model_default` without reading what it means
230
+ is no better off than before. True of ADR 032's one line too. The claim
231
+ is not that a keyword teaches, it is that a library should not make
232
+ this particular claim on a host's behalf.
233
+ - What would change this decision: Janela growing a caller of its own, a
234
+ publish button or a scheduled frame, at which point the class to
235
+ enqueue is Janela's question rather than the scheduler's and a setting
236
+ earns itself for the first time. ADR 033 named that same event as the
237
+ one that would reopen how a snapshot learns its owner, so the two
238
+ should be read together when it happens.