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,262 @@
1
+ ---
2
+ Date: 2026-09-22
3
+ Status: Accepted
4
+ Related: ADR 002, ADR 019, ADR 021, ADR 025, ADR 031, ADR 032, ADR 033
5
+ Triggers:
6
+ - adding a check to janela:doctor, or changing what one reads
7
+ - a check that reports nothing for a host that has a real problem
8
+ - a check that reports something about code a host did not write
9
+ - rescuing an exception raised by a host's own code
10
+ - reading Janela.parent_controller anywhere
11
+ - assigning Janela.parent_controller outside an initializer
12
+ Topics: tooling, configuration, authorisation, host-integration, security, releases
13
+ ---
14
+
15
+ # ADR 035: A Check Does What Janela Does, or It Says What It Saw
16
+
17
+ ## Context
18
+
19
+ Three reports arrived within a day of 0.7.0, two of them from a real host
20
+ integration rather than from this repository (#51, #38). They read as
21
+ three unrelated bugs in `janela:doctor` and they are one.
22
+
23
+ Every failing check reads a **proxy** for the thing it reports on, and
24
+ then words the finding as though it had checked the thing.
25
+
26
+ | Check | Reads | Asserts |
27
+ | --- | --- | --- |
28
+ | every controller check | `Janela.parent_controller`, the setting | "Janela's controllers inherit X" |
29
+ | `unscoped_reads` | whether a method is defined | "defines no policy_scope, so every pane will raise" |
30
+ | `disallowed_uses` | a string anywhere under `app`, `config`, `lib` | "this file filters this model on this key" |
31
+ | `scope_filters_by_owner?` | an exception, swallowed | "does not filter by owner" |
32
+
33
+ ### Measured
34
+
35
+ All against the demo, plus Pundit 2.5.2 installed outside the bundle so
36
+ it could be run rather than read. ADR 032 verified Pundit by reading, and
37
+ that is the half of it that turned out wrong.
38
+
39
+ **A Pundit host with no policy for Janela's own models.** This is the
40
+ configuration the README treats as the common case, minus one file.
41
+
42
+ ```
43
+ policy_scope defined on the host? true
44
+ a request (Janela.scope, the real path):
45
+ Pundit::NotDefinedError: unable to find scope `Janela::FramePolicy::Scope`
46
+ the doctor's entire output:
47
+ WARNING (unauthenticated-endpoints): no authentication filter found
48
+ ```
49
+
50
+ Every dashboard raises. The doctor says nothing about it.
51
+
52
+ **A host that names its parent controller too late.** Measured at
53
+ development boot, `config/initializers/*.rb` is safe because `to_prepare`
54
+ runs after all of them, `config.after_initialize` is already too late,
55
+ and within `to_prepare` it depends on registration order against anything
56
+ else that touches the controller. The README's own layout recipe is such
57
+ a block.
58
+
59
+ ```
60
+ host named: SecureHost (authenticated, policy_scope -> none)
61
+ Janela actually uses: ApplicationController
62
+ doctor says: WARNING ... no authentication filter found on SecureHost
63
+ "Janela's controllers inherit SecureHost, so they are as public as it is"
64
+ a pane served: $375.00 (200)
65
+ ```
66
+
67
+ That quoted sentence is false at the moment it is printed.
68
+
69
+ **A predicate the host never wrote.** One planted file that does not
70
+ mention `Order`:
71
+
72
+ ```
73
+ ERROR (hardcoded-disallowed-predicates): Order does not allow status_cont
74
+ app/models/shipment.rb filters Order on status_cont
75
+ ERROR (hardcoded-disallowed-predicates): WholesaleOrder does not allow status_cont
76
+ ```
77
+
78
+ Twice, because #44 repeats a finding per STI class. What trips it, one
79
+ case at a time:
80
+
81
+ | Planted | Result |
82
+ | --- | --- |
83
+ | a ransack call on an unrelated model | 2 errors |
84
+ | `def status_changed? = true` | nothing |
85
+ | `def status_count = 0` | nothing |
86
+ | `# never filter on status_cont, it is not allowed` | 2 errors |
87
+ | `status_matches:` as a key in `config/locales/en.yml` | 2 errors |
88
+
89
+ Ordinary identifiers are safe, so the predicate detection is doing real
90
+ work and this is not a bare grep. A comment warning against the predicate
91
+ still reports that you use it, at `error`, the loudest thing the doctor
92
+ can say.
93
+
94
+ **The worst of it, which nobody had reported.** The two owner checks call
95
+ a host's `policy_scope` on `parent.allocate`, an instance with no
96
+ request, and rescue everything:
97
+
98
+ ```
99
+ RealisticOwnerHost scope_filters_by_owner? -> false raised: NameError
100
+ SessionlessOwnerHost scope_filters_by_owner? -> true raised: no
101
+
102
+ does frames-nobody-will-own fire?
103
+ RealisticOwnerHost no finding
104
+ SessionlessOwnerHost ... scopes frames by owner but defines no janela_frame_owner
105
+ ```
106
+
107
+ `RealisticOwnerHost` differs from the other by one line: its
108
+ `policy_scope` reaches for the signed in user through the session, which
109
+ is what every authentication library does. On an allocated controller
110
+ `session` is nil, the call raises `NameError`, the blanket rescue turns
111
+ that into "does not filter by owner", and the check goes quiet.
112
+
113
+ So `frames-nobody-will-own` and `snapshots-nobody-will-see` do not fire
114
+ for any host with real authentication. The second of those shipped in
115
+ 0.7.0 yesterday, written because the case bit five times in one
116
+ afternoon. It cannot fire for the hosts that need it.
117
+
118
+ **Why this repository did not catch it.** The demo's `policy_scope` reads
119
+ `Current.tenant`, a thread local, where a host reads the session. It is
120
+ the #46 lesson a third time: a stand in wired differently to the
121
+ documentation cannot catch a bug in the documentation's wiring. That is
122
+ already a hard constraint in `/jan-orient`, and it did not extend to
123
+ "wired differently to how hosts actually work".
124
+
125
+ ### The thing that makes this fixable
126
+
127
+ An allocated controller can be given a request:
128
+
129
+ ```
130
+ allocate, no request: NameError: undefined method 'session' for nil
131
+ allocate + ActionDispatch::TestRequest, policy present: ok, SELECT "janela_frames".*
132
+ allocate + ActionDispatch::TestRequest, policy absent: Pundit::NotDefinedError
133
+ ```
134
+
135
+ A correctly wired host succeeds. A host missing a policy raises the same
136
+ error its visitors get. The check can tell them apart without knowing
137
+ that Pundit exists, which ADR 002 requires.
138
+
139
+ ### Options considered
140
+
141
+ **Keep reading, and hedge in the prose.** Cheap, and leaves both owner
142
+ checks dead and the Pundit gap unreported. A check that cannot fire is
143
+ not improved by better wording.
144
+
145
+ **Depend on Pundit so a check can name `Pundit::NotDefinedError`.**
146
+ Rejected on ADR 002: authorisation here is a hook, not a dependency, and
147
+ a doctor that knows one library's exception classes is a doctor that is
148
+ wrong about every other library.
149
+
150
+ **Build a real request through the integration stack.** Heavier than
151
+ `ActionDispatch::TestRequest` and no more faithful. The controller never
152
+ processes an action here; it is asked one question.
153
+
154
+ **Statically resolve the receiver of a `ransack` call** so
155
+ `disallowed_uses` can attribute a match honestly. That is a parser, for
156
+ one advisory check, and it would still miss a call through a variable.
157
+
158
+ **Remove `disallowed_uses`.** Seriously considered. It was written for
159
+ the 0.5.0 upgrade (ADR 025) to help hosts find filters the predicate
160
+ narrowing broke, two releases ago, and its noise is now measured. Kept,
161
+ because a host upgrading from 0.4.x still exists and the check is
162
+ salvageable as an observation.
163
+
164
+ **Make `Janela.parent_controller=` raise when the assignment cannot take
165
+ effect.** Chosen, below, after being weighed against a doctor check that
166
+ names the disagreement instead.
167
+
168
+ ## Decision
169
+
170
+ **A check does what Janela does, or it says what it saw.** Where Janela
171
+ calls a method, the check calls the same method. Where Janela resolves a
172
+ constant, the check resolves the same constant. Where a check cannot do
173
+ what Janela does, it reports its observation and not a conclusion it did
174
+ not earn.
175
+
176
+ That is the rule the rest of this follows from.
177
+
178
+ **A check that calls host code calls it the way a request does.** The
179
+ controller is given an `ActionDispatch::TestRequest`, so a host's
180
+ `policy_scope` can reach `session`, `params` and `current_user` and find
181
+ them empty rather than absent. Empty is the honest condition: it is an
182
+ unauthenticated visitor, which is exactly who a doctor should be asking
183
+ about. Measured above to separate a wired host from an unwired one with
184
+ no knowledge of the host's library.
185
+
186
+ **An exception from host code is reported, never swallowed.**
187
+ `scope_filters_by_owner?` collapses "raised" and "returned something
188
+ unfiltered" into `false`, and both owner checks are silent as a result.
189
+ The question becomes three-valued: filtered, not filtered, or raised. A
190
+ raise is a finding in its own right, quoting the exception class and
191
+ message, because Janela is about to do the same call on every request.
192
+
193
+ **`unscoped_reads` calls `policy_scope` against `Janela::Frame` and
194
+ `Janela::Snapshot`.** Those two are always present and are what the
195
+ engine's own pages read first, so a host that cannot be asked about them
196
+ has a broken dashboard whatever else is true. It stops testing for the
197
+ method. **ADR 032's claim that this check is "exact: the method is
198
+ defined or it is not, and that is the whole contract" is wrong and this
199
+ supersedes it.** The contract is that calling it returns a relation.
200
+ ADR 032's other claim, that a Pundit host missing a policy is not
201
+ exposed, stands: Pundit raises rather than leaking, so no data is at
202
+ risk and this is a diagnosis failure rather than a safety one.
203
+
204
+ **Every check reads `Janela::ApplicationController.superclass`, not
205
+ `Janela.parent_controller`.** The superclass is what Janela uses; the
206
+ setting is what a host asked for, and the two can differ (#38). One line,
207
+ four checks, and it removes the possibility of printing "Janela's
208
+ controllers inherit X" about a class that is not in the chain.
209
+
210
+ **`Janela.parent_controller=` raises when the assignment cannot take
211
+ effect and names a different class.** Assigning before the controller
212
+ loads is fine, and reassigning the value already resolved is fine, so the
213
+ raise fires only for the case that is silently broken today. `Janela` can
214
+ tell without forcing the autoload it is asking about, through
215
+ `autoload?` and `const_defined?`. This is ADR 032's trade again, loud once
216
+ at the exact wrong line rather than quiet forever, and it is why no new
217
+ doctor check is added for #38: preventing the state is better than
218
+ reporting it, and two mechanisms for one problem is what ADR 001 declines.
219
+ It does not contradict ADR 032's refusal to raise at boot, which was
220
+ about raising for a host that had done nothing wrong.
221
+
222
+ **`disallowed_uses` says what it saw.** Janela never reads a host's
223
+ source, so this check cannot do what Janela does and falls to the other
224
+ half of the rule. It reports that a file *mentions* a key the model does
225
+ not allow, rather than that the file *filters* that model, drops from
226
+ error to warning, and reports once per declaration rather than once per
227
+ inheriting class. It stays a hint to grep rather than a claim about the
228
+ host's code.
229
+
230
+ ## Consequences
231
+
232
+ - Two checks that could not fire for a host with real authentication
233
+ begin firing. Expect the first hosts to run this to see findings that
234
+ were always true and never printed.
235
+ - A check now runs a host's own `policy_scope` during `janela:doctor`.
236
+ That is host code executing in a rake task, which it was already, but
237
+ deliberately rather than by accident. The contract for `policy_scope`
238
+ is that it returns a relation, so it reads; a host whose implementation
239
+ writes something has a larger problem than this check.
240
+ - **A host assigning `Janela.parent_controller` too late goes from
241
+ silently ignored to an exception at boot.** Breaking, so it goes in
242
+ `UPGRADING.md` with the three places that are too late and the one that
243
+ is not (ADR 015). Most hosts assign in an initializer and see nothing.
244
+ - `with_parent_controller` in this repository's own doctor tests works by
245
+ assigning after load, which is the very thing the setter now refuses.
246
+ Those tests have to set up their scenarios by another route, and they
247
+ were relying on the bug: they only worked because the checks read the
248
+ setting rather than the chain.
249
+ - `disallowed_uses` dropping to warning means a host who genuinely broke
250
+ a filter is told less loudly. Accepted: it was error severity on
251
+ evidence it did not have, and a warning that is usually right beats an
252
+ error that is sometimes about a comment.
253
+ - #44 is narrowed rather than closed. Reporting once per declaration
254
+ fixes the duplication in this one check; the rest of the model checks
255
+ still repeat per STI subclass.
256
+ - The demo has to gain a host whose `policy_scope` reaches through the
257
+ session, because nothing else in this repository would have caught the
258
+ dead owner checks. That is the #46 lesson applied rather than restated.
259
+ - What would change this decision: a host reporting that
260
+ `ActionDispatch::TestRequest` is not enough for their authorisation to
261
+ run, at which point the question is whether a check should be asking at
262
+ all rather than how hard it should try.
@@ -0,0 +1,171 @@
1
+ ---
2
+ Date: 2026-09-22
3
+ Status: Accepted
4
+ Related: ADR 011, ADR 015, ADR 016, ADR 021, ADR 023
5
+ Triggers:
6
+ - adding a class, a custom property or a rule to either stylesheet
7
+ - deciding whether a piece of look belongs to the gem or to a theme
8
+ - writing a theme, or reading one somebody else wrote
9
+ - renaming anything a stylesheet targets
10
+ - adding a helper that renders markup a host will style
11
+ Topics: styling, theming, host-integration, naming, public-api, releases
12
+ ---
13
+
14
+ # ADR 036: Janela Publishes What a Theme May Target, and Vitral Is Only One
15
+
16
+ ## Context
17
+
18
+ ADR 023 split the styling in two: `janela.css` is structure, frozen and
19
+ boring, and `vitral.css` is taste, free to move. That was the right cut
20
+ and it has held for the theme. It no longer describes the other file.
21
+
22
+ ### What `janela.css` actually contains
23
+
24
+ Counted by where each name is used, in the engine's own views and in the
25
+ demo standing in for a host:
26
+
27
+ | Group | Names | Where they appear |
28
+ | --- | --- | --- |
29
+ | The grid scale | `janela-cols-1..12`, `janela-gap-0..8`, `janela-span-1..12` | chosen by a frame's own integers (ADR 016) |
30
+ | Pane primitives | `janela-frame`, `janela-pane`, `janela-value`, `janela-value-label`, `janela-value-number`, `janela-chart`, `janela-empty`, `janela-error` | in a host's own pages, wherever a pane is rendered |
31
+ | The engine's own chrome | `janela-card`, `janela-button`, `janela-crumb`, `janela-flash`, `janela-page`, `janela-form`, `janela-field`, `janela-heading`, `janela-subheading`, `janela-list`, `janela-list-row`, `janela-list-name`, `janela-actions`, `janela-exit`, `janela-hint`, `janela-muted`, `janela-danger`, `janela-errors` | only Janela's own pages: 2 to 6 engine views each, and none in the demo but `janela-form`, twice |
32
+
33
+ The third group is a small UI kit for pages a host may never visit. ADR
34
+ 016 said "the class names are public API", meaning renaming one is
35
+ breaking and belongs in `UPGRADING.md`. Applied to the third group that
36
+ is a promise made to nobody, about markup only the engine renders, which
37
+ makes the engine's own pages harder to change than the thing the gem is
38
+ for.
39
+
40
+ ### A theme other than vitral already works
41
+
42
+ `Janela.theme` is one line in the engine's layout,
43
+ `stylesheet_link_tag Janela.theme if Janela.theme.present?`, so it
44
+ resolves any stylesheet name against the host's own asset paths. Measured
45
+ against Janela's own layout:
46
+
47
+ ```
48
+ "vitral" -> janela.css + vitral.css
49
+ "application" -> janela.css + the host's own application.css
50
+ "no_such_theme" -> raises, "The asset 'no_such_theme.css' was not found in the load path"
51
+ nil -> janela.css alone
52
+ ```
53
+
54
+ So somebody else's skin needs nothing built. What it needs is to know
55
+ what it may target, and today nothing says. A theme author reads
56
+ `vitral.css` to find out, which makes every name vitral happens to use
57
+ into an accidental interface, and makes the three groups above
58
+ indistinguishable.
59
+
60
+ ### Four open issues are the same question
61
+
62
+ `#26` a caption that cannot be hidden without losing it for screen
63
+ readers, `#29` inter-pane layout being entirely the host's problem, `#30`
64
+ a categorical palette, `#31` a single value pane with no typography and
65
+ no way to say how prominent it is. Each one asks where a piece of look
66
+ belongs, and each has been answered one at a time so far, which is how a
67
+ split stops meaning anything: `janela.css` grew a UI kit that way,
68
+ without anyone deciding it should.
69
+
70
+ ### Options considered
71
+
72
+ **Leave it, and decide each issue as it comes.** What has been happening.
73
+ It produced the drift above and gives a theme author nothing to read.
74
+
75
+ **A third stylesheet**, splitting the engine's own chrome out of
76
+ `janela.css`. Tempting and rejected on ADR 023's own consequence: a third
77
+ file is a third thing every Sprockets host has to declare for
78
+ precompilation, and #14 says the second one is already a rough edge. The
79
+ problem is a missing promise, not a missing file.
80
+
81
+ **Rename the chrome to mark it private**, `janela-internal-card` or
82
+ similar. Rejected: the rename is itself the breaking change it is trying
83
+ to make unnecessary, paid now for a benefit that is only cosmetic. Saying
84
+ which names are the contract costs nothing and is just as clear.
85
+
86
+ **Move the pane primitives into the theme**, so a host with no theme gets
87
+ unstyled panes. Rejected: a pane has to be legible on install, which is
88
+ ADR 016's founding reason, and #26 is the sharp case, since hiding a
89
+ caption accessibly is correctness rather than taste and a host that never
90
+ opts into a theme still needs it.
91
+
92
+ ## Decision
93
+
94
+ **Janela publishes the hooks; a theme supplies the taste. What a theme
95
+ may target is written down, and what is not written down is Janela's own
96
+ chrome.**
97
+
98
+ **Three layers, named, in two files.** No new stylesheet.
99
+
100
+ - **The grid scale and the pane primitives are the contract.** These are
101
+ what a host's own pages contain and what a theme targets. Renaming one
102
+ is breaking and goes in `UPGRADING.md` with the doctor taught to find
103
+ the old name (ADR 015).
104
+ - **The engine's own chrome is not the contract.** The classes only
105
+ Janela's own views render may change in any release. A host restyling
106
+ Janela's own pages is welcome to target them and should expect to
107
+ revisit it, which is the honest version of what was already true.
108
+ - ADR 016's "the class names are public API" is **narrowed, not
109
+ superseded**: it is true of everything a host's markup contains, and
110
+ was never meant as a promise about the engine's breadcrumbs.
111
+
112
+ **`docs/theming.md` carries the contract**, because a theme author is not
113
+ going to read an ADR to find a class list. It names the two layers, every
114
+ class in them, every custom property, and what a theme is expected to
115
+ leave alone. Shipped in the gem beside the other docs.
116
+
117
+ **A theme is any stylesheet the host names, and vitral is one of them.**
118
+ Nothing more is built for this: `Janela.theme = "midnight"` already
119
+ resolves against the host's asset paths, and a name that does not resolve
120
+ already raises rather than failing quietly. What changes is that this is
121
+ documented and supported rather than merely true, so a third party can
122
+ publish a theme against a contract instead of against whatever vitral
123
+ happens to do this month.
124
+
125
+ **Where a new piece of look goes.** The rule that answers the four open
126
+ issues without arguing each one separately:
127
+
128
+ - Something a pane needs in order to be correct or legible on install is
129
+ a **hook**, and belongs in `janela.css` on the contract. A caption that
130
+ is announced but not seen is this (#26).
131
+ - Something that is a choice about appearance is **taste**, and belongs
132
+ in a theme. A categorical palette is this (#30).
133
+ - Something a host arranges is **layout**, and belongs in the grid scale
134
+ as an integer that selects a rule, never as a value interpolated into a
135
+ style attribute (ADR 016). Prominence for a single value pane is this
136
+ (#31), and so is inter-pane layout (#29).
137
+
138
+ Where a feature has both halves, the hook ships in `janela.css` and the
139
+ taste in the theme. A palette is the clean example: the class that marks
140
+ which series a mark belongs to is a hook, and the colours it resolves to
141
+ are a theme's.
142
+
143
+ **Vitral stays in this repository**, and stays one theme among any
144
+ others. It is the reference implementation of the contract, which is a
145
+ reason to keep it here rather than to privilege it: if a rule cannot be
146
+ written against the published names, the contract is wrong.
147
+
148
+ ## Consequences
149
+
150
+ - A theme author has something to read, and a theme written against
151
+ `docs/theming.md` keeps working across releases in a way one written
152
+ against `vitral.css` never could.
153
+ - The engine's own pages get easier to change, because their markup stops
154
+ being an interface. That is a real loosening: a host that today targets
155
+ `janela-card` will find it moves one day, and the doc says so rather
156
+ than leaving them to discover it.
157
+ - Writing the contract down means reading every class in both files once
158
+ and deciding which side it is on. That is the work, and it is the point.
159
+ - **Not decided here: whether Vitral grows to answer #29, #30 and #31.**
160
+ This says where each half of each of those belongs; it does not commit
161
+ to building them, and a component library large enough to lay out a
162
+ host's page is its own decision against ADR 001's preference for
163
+ staying small enough to fork.
164
+ - A theme is still linked only into Janela's own layout. A host wanting
165
+ the look on its own pages links the stylesheet itself and uses the
166
+ theme's public classes, exactly as ADR 023 decided. The contract does
167
+ not change that asymmetry, it explains it.
168
+ - What would change this decision: the engine's own chrome turning out to
169
+ be something hosts genuinely restyle, in reports rather than in
170
+ anticipation, at which point it has earned the promise this declines to
171
+ make and should be moved onto the contract deliberately.
@@ -0,0 +1,172 @@
1
+ ---
2
+ Date: 2026-09-23
3
+ Status: Accepted
4
+ Related: ADR 001, ADR 010, ADR 015, ADR 021
5
+ Triggers:
6
+ - deciding whether an open issue belongs in the next release
7
+ - proposing a feature, or arguing that one is out of scope
8
+ - cutting a release and choosing the version number
9
+ - deciding whether a breaking change is still affordable
10
+ - wondering whether the project is drifting
11
+ Topics: vision, scope, forkability, releases, roadmap, planning
12
+ ---
13
+
14
+ # ADR 037: 1.0 Means the Surface Stops Moving, Not That Janela Is Finished
15
+
16
+ ## Context
17
+
18
+ Janela has a vision and a backlog and nothing in between. ADR 001 says
19
+ what the project is, the load-bearing 5% of a BI tool, and the README's
20
+ Design section lists what it will never be: natural-language query, a
21
+ warehouse, a row-level-security subsystem, a scheduling UI, an embedding
22
+ SDK, a mobile app, paginated reports. That is a stronger statement of
23
+ scope than most libraries manage. What neither says is what order things
24
+ happen in, or what has to be true before a host can build on this
25
+ without expecting the ground to move.
26
+
27
+ The gap shows up in three measurements taken today.
28
+
29
+ **The surface moves every release.** Counting entries marked breaking in
30
+ `CHANGELOG.md`: 0.5.0 carried one, 0.7.0 carried two, and the unreleased
31
+ section carries two more. Five breaking changes across four releases in
32
+ five days, every one of them correct and every one of them announced the
33
+ way ADR 015 requires. This is what alpha is for and there is nothing to
34
+ apologise for in it. It also means nobody outside this repository can
35
+ build anything on Janela yet, because the DSL, the helpers, the pane
36
+ URLs and the class names a theme targets have all changed underneath a
37
+ host at least once.
38
+
39
+ **The declared horizon ran out.** The `Before public release` milestone
40
+ stands at ten closed and one open. It worked, it is nearly spent, and
41
+ nothing replaced it. Thirteen of the fourteen open issues carry no
42
+ milestone at all.
43
+
44
+ **The issue list cannot rank itself.** Eight of those issues are
45
+ enhancements, four of them raised from a real install, and none has been
46
+ picked up while the last four releases went to hardening. That looked
47
+ like drift and is worth being precise about, because it was not. ADRs
48
+ 025, 032, 034 and 035 are one argument made four times: Janela refuses
49
+ rather than guesses about scope. Bounded predicates, a refused unscoped
50
+ read, a refused unnamed snapshot scope, and a doctor that reports what it
51
+ actually saw. That is the most coherent stretch of work in the project.
52
+ The problem is that the theme was never declared, so it was invisible
53
+ while it was happening, and now that it is finished there is nothing
54
+ declared to follow it. A flat list of fourteen issues is what a project
55
+ looks like between themes when it has no way of saying so.
56
+
57
+ Three options were considered.
58
+
59
+ **A roadmap with dates or quarters** was rejected. This project's
60
+ throughput is not predictable enough to commit to a calendar, and a
61
+ published roadmap that slips teaches people to stop reading it. The cost
62
+ is ongoing and lands every release; the benefit is mostly the appearance
63
+ of planning.
64
+
65
+ **Leaving it to the issue list** was rejected because that is the current
66
+ state, and the current state is what prompted the question. Labels
67
+ describe what an issue is. Nothing describes what it is for.
68
+
69
+ **Declaring 1.0 feature-complete** against what a commercial BI tool ships
70
+ was rejected for contradicting ADR 001 outright. Janela is not trying to
71
+ reach parity with anything. A 1.0 defined as "the features are all there"
72
+ has no end, because the list it is measured against is somebody else's.
73
+
74
+ ## Decision
75
+
76
+ **1.0 means the public surface stops moving, the 5% can be read, and the
77
+ doctor can be trusted. It does not mean Janela is finished, and it is not
78
+ a feature count.**
79
+
80
+ Three claims, each of which can be checked rather than argued about.
81
+
82
+ **The public surface stops moving.** After 1.0, everything
83
+ `docs/theming.md` lists as the contract, the measures and dimensions DSL,
84
+ `janela_pane` and `janela_frame`, the pane URL shape and the dashboard
85
+ filter parameters change only on a major version. Before 1.0 they may
86
+ change in any release, with the upgrade note ADR 015 requires. This is
87
+ the load-bearing claim and the other two exist to serve it: a surface is
88
+ only worth freezing once it is the right shape, which is why the feature
89
+ work below sits inside 1.0 rather than after it. Freezing an API that is
90
+ missing a limb only guarantees the limb arrives as a breaking change
91
+ later.
92
+
93
+ **The 5% can be read.** Janela draws three renderers, `table`, `bar` and
94
+ `line`. A part-to-whole split has to be drawn as two bars. A table pane
95
+ carries exactly two cells per row, so a label that needs context to be
96
+ understood cannot have any. A chart takes Chart.js's 2:1 default and a
97
+ host cannot correct it from outside. None of these is a feature beyond
98
+ the 5%; each is the 5% not finished, and the distinguishing test is that
99
+ four of them were raised by someone trying to use the gem rather than by
100
+ someone reading its source. The theme that follows "Janela refuses rather
101
+ than guesses" is legibility: a pane a person can actually read.
102
+
103
+ **The doctor can be trusted.** ADR 021 holds that a check people stop
104
+ trusting has stopped working. Every check in `CHECKS` bar two now has a
105
+ test that makes it fire and one that leaves it quiet.
106
+
107
+ The open issues sort as follows, and this sort is the substance of the
108
+ decision rather than an illustration of it.
109
+
110
+ In 1.0: #14 (a Sprockets host serves the engine's JavaScript or is told
111
+ it cannot), #53 (the last two untested checks), #27 (a ratio measure, so
112
+ the refusal in #20 offers somewhere to go), #30 (doughnut and pie, and
113
+ the categorical palette that makes them readable), #31 (a pane says how
114
+ prominent it is), #34 (a table carries an attribute column beside its
115
+ label), #24 (a chart's height is the host's to set), and #6 (how
116
+ contributions are accepted, who cuts a release, where to report a
117
+ vulnerability privately).
118
+
119
+ After 1.0: #18 (drill-down on time panes, which ADR 006 excluded
120
+ deliberately and which needs an ADR of its own before any code), #41 (a
121
+ command palette, which the issue itself argues belongs to the demo and
122
+ not the gem), and #45 (a cosmetic scroll drift on one demo page, where
123
+ the issue already allows that deciding not to fix it is a legitimate
124
+ answer).
125
+
126
+ Neither, and tracked as itself: #5 is an upstream pin on ActiveSupport
127
+ and is removed when ActiveSupport is fixed. #56 is a one-in-160 test
128
+ flake deliberately recorded rather than chased, and is evidence, not
129
+ work.
130
+
131
+ Needing a decision rather than a place: #29 asks Janela to help arrange
132
+ panes on a page. ADR 011 has panes not rendering in the host's layout and
133
+ ADR 001 prefers a fork to a configuration surface, so the default answer
134
+ is that arrangement belongs to the host and the issue closes into the
135
+ README's out-of-scope list. That is a scope decision and gets argued on
136
+ its own rather than settled here by omission.
137
+
138
+ **The roadmap is published rather than kept in the issue tracker.**
139
+ `docs/roadmap.md` ships in the gem and renders on the demo at
140
+ `/docs/roadmap`, because the demo reads the gem's own markdown rather
141
+ than restating it (ADR 010). One file is therefore the public statement,
142
+ the page a visitor reads and the copy in the installed gem at once. The
143
+ GitHub milestone remains the working tracker and the progress bar; it
144
+ says which issues, while the document says what the release is for. A
145
+ reader who has to open an issue tracker to find out where a library is
146
+ going has been told to do the maintainer's filing.
147
+
148
+ ## Consequences
149
+
150
+ - A breaking change is cheap now and expensive after 1.0. Anything that
151
+ wants to change the DSL, the helpers or the published class names
152
+ should be argued for before 1.0 rather than after, and the sort above
153
+ is deliberately biased that way.
154
+ - An issue outside 1.0 is not rejected. It is not blocking a release,
155
+ which is a different and weaker statement, and the roadmap says so in
156
+ those words so that nobody reads the list as a refusal.
157
+ - The eight issues in 1.0 are a commitment made in public. That is the
158
+ point of publishing it and also its only real cost: a roadmap on the
159
+ website can be held against the project in a way a milestone cannot.
160
+ - The current release is 0.8.0 and not 1.0, which this ADR answers
161
+ without further argument, because it carries two breaking changes.
162
+ - `docs/roadmap.md` has to be updated when a release lands, or it becomes
163
+ the stale artifact this ADR rejected dated roadmaps for being. It is
164
+ named in `/jan-release` for that reason.
165
+ - The README's Design section currently says snapshots are "not built
166
+ yet" and they shipped in 0.7.0. Publishing a roadmap beside a stale
167
+ status line makes the staleness worse, so that sentence is corrected in
168
+ the same change.
169
+ - What would change this decision: a real install blocked by something in
170
+ the after-1.0 list. The sort is a judgement about what a host needs to
171
+ read a dashboard, and a host saying otherwise is better evidence than
172
+ the judgement.
@@ -20,26 +20,28 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
20
20
 
21
21
  | Topic | ADRs |
22
22
  |-------|------|
23
- | **Vision, scope, forkability** | 001, 010, 012 |
24
- | **Open-source & host-decoupling** | 001, 022 |
23
+ | **Vision, scope, forkability** | 001, 010, 012, 037 |
24
+ | **Open-source & host-decoupling** | 001, 022, 036 |
25
25
  | **DSL & query layer** | 002, 006, 007, 020, 025 |
26
26
  | **Dependencies** | 002, 003, 004, 006, 017, 025 |
27
- | **Authorisation** | 002, 003, 004, 009, 017, 019, 022 |
27
+ | **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035 |
28
28
  | **Performance & storage** | 007, 017, 025 |
29
29
  | **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025 |
30
30
  | **Layouts & views** | 011, 012, 016, 018, 020, 027 |
31
- | **CSS & styling** | 016, 018, 023, 026, 027 |
32
- | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030 |
33
- | **Naming rule** | 014, 023 |
31
+ | **CSS & styling** | 016, 018, 023, 026, 027, 036 |
32
+ | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033 |
33
+ | **Naming rule** | 014, 023, 036 |
34
34
  | **JavaScript delivery & charts** | 004, 006, 026 |
35
35
  | **Time dimensions** | 006, 025 |
36
36
  | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025 |
37
- | **Snapshots & publishing** | 009, 020, 028 |
37
+ | **Snapshots & publishing** | 009, 020, 028, 033, 034 |
38
38
  | **AI agents & guidance** | 010, 015, 021 |
39
- | **Releases & upgrades** | 015, 021 |
39
+ | **The doctor & checks** | 021, 025, 032, 033, 035 |
40
+ | **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037 |
40
41
  | **Accessibility & keyboard** | 024 |
41
- | **Security** | 003, 025, 028, 031 |
42
+ | **Security** | 003, 025, 028, 031, 032, 034, 035 |
42
43
  | **Testing** | 003 |
44
+ | **Roadmap & planning** | 001, 037 |
43
45
 
44
46
  ## Chronological
45
47
 
@@ -76,7 +78,13 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
76
78
  | 029 | A Pane's Frame Is Identified by Who It Is, Not by What It Shows | 2026-09-18 | Accepted |
77
79
  | 030 | A Pane's src Belongs to Turbo, So a Host Talks to the Frame | 2026-09-18 | Accepted |
78
80
  | 031 | A Subclass Inherits the Dashboard Its Parent Declared | 2026-09-19 | Accepted |
81
+ | 032 | Janela Will Not Read a Model It Cannot Scope | 2026-09-20 | Accepted |
82
+ | 033 | A Snapshot Is Told Who Owns It | 2026-09-21 | Accepted |
83
+ | 034 | Janela Will Not Freeze a Scope the Host Has Not Named | 2026-09-21 | Accepted |
84
+ | 035 | A Check Does What Janela Does, or It Says What It Saw | 2026-09-22 | Accepted |
85
+ | 036 | Janela Publishes What a Theme May Target, and Vitral Is Only One | 2026-09-22 | Accepted |
86
+ | 037 | 1.0 Means the Surface Stops Moving, Not That Janela Is Finished | 2026-09-23 | Accepted |
79
87
 
80
88
  ## Next number
81
89
 
82
- Next ADR: 032
90
+ Next ADR: 038