janela 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +5 -5
- data/UPGRADING.md +63 -0
- data/app/assets/stylesheets/janela.css +21 -0
- data/docs/decisions/032-janela-will-not-read-a-model-it-cannot-scope.md +1 -0
- data/docs/decisions/035-a-check-does-what-janela-does-or-says-what-it-saw.md +262 -0
- data/docs/decisions/036-janela-publishes-what-a-theme-may-target.md +171 -0
- data/docs/decisions/037-what-1-0-means.md +172 -0
- data/docs/decisions/INDEX.md +13 -8
- data/docs/roadmap.md +97 -0
- data/docs/theming.md +177 -0
- data/lib/janela/definition.rb +8 -1
- data/lib/janela/doctor.rb +121 -32
- data/lib/janela/model.rb +6 -1
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +47 -3
- metadata +21 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a28af383164e7bc1176c8b41bcff4d4ee384bbdaac7313b66a781c5c047a4fb8
|
|
4
|
+
data.tar.gz: 7611b5e28e04d9dc3cf45a940eba1aae33e2b80beb708f3a24b7ecc12a650819
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b32263ebc50a9d33ca95a9629bde4a58937370ea70df0fe17c58d651a0f87953aeb668dc363933c0aaa5e36a2b4bd663ffbdf4ea670cf5eec3dd5e3f7ee380d
|
|
7
|
+
data.tar.gz: 7651430b673d289b5bfa1a650c66351d580714c542cba06ec046cac4236dde21079c5debeddc9d27439442596b8a317aa60adccda1d1c3c9f1216b60a0d73f54
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.0] - 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `docs/theming.md`, "Theming Janela": the contract a theme may target. Every class the engine renders in your own pages, the three custom properties that carry `janela.css`, what a theme is expected to leave alone, and how to write one of your own. A theme is any stylesheet you name, `Janela.theme = "midnight"` resolving from your own asset paths, and vitral is one of them rather than the one. Ships in the gem; `test/dummy` has a live page at `/vitral` showing all of it against real panes (ADR 036).
|
|
13
|
+
- `janela-own-headings`, a class you put on any ancestor when your own markup already says what a pane is. A table pane's `<caption>` and a single value's label stop being drawn and stay in the accessibility tree. Hidden rather than removed on purpose: a caption is the table's accessible name, so `display: none` lands a screen reader on a grid of numbers with nothing to say what they measure, which is the easy wrong answer this saves you writing. A chart pane needs nothing, since its title was only ever an `aria-label` (ADR 036, #26).
|
|
14
|
+
- `docs/roadmap.md`, "Where Janela Is Going": what 1.0 means, which open issues are in it, and what is deliberately not coming. Three claims that can be checked rather than a feature count, since ADR 001 rules out a 1.0 measured against what a commercial BI tool ships: the public surface stops moving, a pane can be read, and the doctor can be trusted. It ships in the gem and renders on the demo at `/docs/roadmap`, because a reader who has to open an issue tracker to find out where a library is going has been told to do the maintainer's filing. It carries no dates on purpose, so that it can only ever be wrong about substance (ADR 037).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Breaking.** `Janela.parent_controller=` raises when the assignment cannot take effect. The superclass of `Janela::ApplicationController` is resolved once, the first time the class loads, so naming a different controller after that did nothing at all: dashboards kept inheriting whatever was named first, and the host's authentication and `policy_scope` were not the ones it had written. Nothing said so. Set it in `config/initializers/janela.rb`, which runs before anything can load a Janela controller; `config.to_prepare` and `config.after_initialize` are both too late, and the README's own layout recipe is a `to_prepare` block that loads the controller. Naming the controller Janela already inherits is still allowed, since nothing is being asked for (ADR 035, #38).
|
|
19
|
+
- **The class names only Janela's own pages use are no longer public API.** ADR 016 said "the class names are public API", which read literally promised that `janela-card`, `janela-crumb`, `janela-flash`, `janela-button`, `janela-form` and the rest of the engine's own chrome would never be renamed without an upgrade note. That was a promise made to nobody about markup only the engine renders, and it made Janela's own pages harder to change than the library they serve. They are scoped under `janela-page`, which only the engine's layout sets, so they cannot reach your pages, and they may now change in any release. Everything a host's own markup contains, the grid scale and the pane primitives, is the contract and is unchanged: `docs/theming.md` lists it (ADR 036).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Janela believes only the definitions Janela built. `Janela::Model` is extended onto every ActiveRecord model, so `janela` is a question any of them can answer, and an application whose own model defines a class method by that name wins the collision, as Ruby says it should. Janela treated anything truthy that came back as a definition: `rails janela:doctor` raised `NoMethodError: undefined method 'dimensions' for an instance of String`, a named subclass of that model was registered so `/dashboards/<its route key>/...` resolved, and `Janela.definitions` handed out a value that was not a definition to anything iterating it, such as a form offering a choice of model. Nothing about your own method changes; Janela ignores it now instead of choking on it. A *column* named `janela` was never affected either way, because an attribute is an instance method and this is a class method (#54, found while investigating #12).
|
|
24
|
+
|
|
25
|
+
- `rails janela:doctor`'s `through-dimensions-without-an-allowlist` reported once per dimension and once per class inheriting a declaration, where there is only ever one allowlist to write. Two dimensions reading through the same association produced two findings whose suggested lines contradicted each other, `%w[region]` and `%w[name]`, so pasting both kept the second and silently lost the first; an STI family produced a copy of each per subclass, since a subclass shares its parent's associations. It is now one finding per associated class, naming every column that class needs and every dimension that wants one, with a line that allows all of them. A subclass that reflects an association its parent does not still gets a finding of its own, because that is a different fix (#44).
|
|
26
|
+
- `rails janela:doctor` checked whether your controller *defines* `policy_scope` rather than whether calling it works, so a Pundit host with no policy for Janela's own models got a clean report and a `Pundit::NotDefinedError` on every dashboard request. Pundit defines `policy_scope` the moment it is included, whether or not the model has a policy, so for the commonest authorisation library those are different questions. `unscoped-reads` now makes the call Janela makes, against `Janela::Frame` and `Janela::Snapshot`, and reports a raise as an error quoting your own exception, which names the policy to write. ADR 032 called this check "exact: the method is defined or it is not"; ADR 035 supersedes that. Its other claim stands: Pundit raises rather than leaking, so nothing was ever exposed (ADR 035, #51).
|
|
27
|
+
- `frames-nobody-will-own` and `snapshots-nobody-will-see` never fired for any application whose `policy_scope` reaches for the signed in user, which is most of them. Both asked your policy on a controller with no request, where `session`, `params` and `current_user` are unreachable, and a blanket rescue read the resulting exception as "does not filter by owner". The checks now ask the way a request does, so those are empty rather than missing, which is an unauthenticated visitor and the right thing for a check to ask about. Expect findings that were always true and never printed (ADR 035).
|
|
28
|
+
- Every check about your controller read `Janela.parent_controller`, the setting, rather than `Janela::ApplicationController.superclass`, what Janela actually inherits. When a host named a parent controller too late the two differed, and the doctor reported on a class that was not in the chain, including the sentence "Janela's controllers inherit X" about a class it does not (ADR 035, #38).
|
|
29
|
+
- `hardcoded-disallowed-predicates` reported an error saying a file filters a model, having only found the dimension's name followed by a predicate somewhere under `app`, `config` or `lib`. An unrelated model's Ransack call, a comment warning against the predicate, and a key in a locale file all read the same to it, and a host was told an error about code that had nothing to do with Janela. It is now a warning, says the file *mentions* the key, and admits that it cannot tell which model a match belongs to. It also reports once per `janela` declaration rather than once per class inheriting it, so an STI family no longer multiplies the same finding (ADR 021, ADR 035, #51, part of #44).
|
|
30
|
+
|
|
8
31
|
## [0.7.0] - 2026-09-22
|
|
9
32
|
|
|
10
33
|
### Added
|
|
@@ -169,6 +192,7 @@ First alpha, installed from GitHub for testing in a single host application.
|
|
|
169
192
|
- Only models that declare a `janela` block are addressable over HTTP.
|
|
170
193
|
- ADRs 001 to 004 in `docs/decisions/`, shipped inside the gem.
|
|
171
194
|
|
|
195
|
+
[0.8.0]: https://github.com/retail-tasker/janela/releases/tag/v0.8.0
|
|
172
196
|
[0.7.0]: https://github.com/retail-tasker/janela/releases/tag/v0.7.0
|
|
173
197
|
[0.6.0]: https://github.com/retail-tasker/janela/releases/tag/v0.6.0
|
|
174
198
|
[0.5.0]: https://github.com/retail-tasker/janela/releases/tag/v0.5.0
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Janela is an alpha on [rubygems.org](https://rubygems.org/gems/janela). It has t
|
|
|
27
27
|
|
|
28
28
|
```ruby
|
|
29
29
|
# Gemfile
|
|
30
|
-
gem "janela", "~> 0.
|
|
30
|
+
gem "janela", "~> 0.8"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
@@ -428,9 +428,9 @@ It is *prepended* so it runs before any filter on your `ApplicationController` t
|
|
|
428
428
|
|
|
429
429
|
Your own route helpers work in there. Janela is an isolated engine, so a bare `new_session_path` would normally resolve against Janela's routes and raise, and this bites any host code that generates a URL while inside the engine: an authentication concern, a `rescue_from` that redirects, an `after_action`. Janela forwards the route helpers it does not define itself to your application, so they behave as they do everywhere else (ADR 022). Two things to know. A name Janela also uses means Janela's in here, and `main_app.frames_path` says yours. And `url_for(@record)` resolves polymorphically with no name to forward, so that one still needs `main_app.`.
|
|
430
430
|
|
|
431
|
-
Scoping is automatic when you use Pundit: `Janela::ApplicationController` calls `policy_scope(model)` if your `ApplicationController` defines it, and raises `Janela::Unscoped` if it does not, rather than reading everything on the strength of an omission (ADR 032). An application with nothing to hide answers `def policy_scope(model) = model.all` once and is done; `bin/rails janela:doctor` reports the absence as `unscoped-reads` before a visitor finds it. Every model you put on a dashboard needs a policy with a `Scope`, and so do `Janela::Frame` and `Janela::Snapshot`: frames, pane rows and stored panes are all read through the scope, never around it. `test/dummy/app/controllers/application_controller.rb` is the smallest honest example of the wiring.
|
|
431
|
+
Scoping is automatic when you use Pundit: `Janela::ApplicationController` calls `policy_scope(model)` if your `ApplicationController` defines it, and raises `Janela::Unscoped` if it does not, rather than reading everything on the strength of an omission (ADR 032). An application with nothing to hide answers `def policy_scope(model) = model.all` once and is done; `bin/rails janela:doctor` reports the absence as `unscoped-reads` before a visitor finds it. Every model you put on a dashboard needs a policy with a `Scope`, and so do `Janela::Frame` and `Janela::Snapshot`: frames, pane rows and stored panes are all read through the scope, never around it. The doctor checks that by making the call rather than by looking for the method, because Pundit defines `policy_scope` the moment it is included and raises only when the model has no policy, so the two are different questions (ADR 035). `test/dummy/app/controllers/application_controller.rb` is the smallest honest example of the wiring.
|
|
432
432
|
|
|
433
|
-
**Multi tenancy** has its own guide: [docs/multi-tenancy.md](docs/multi-tenancy.md). It covers what goes through your scope, worked wiring for Pundit, acts_as_tenant and CanCanCan, what owns a frame the analyst creates, and
|
|
433
|
+
**Multi tenancy** has its own guide: [docs/multi-tenancy.md](docs/multi-tenancy.md). It covers what goes through your scope, worked wiring for Pundit, acts_as_tenant and CanCanCan, what owns a frame the analyst creates, and what rows a scheduled snapshot freezes.
|
|
434
434
|
|
|
435
435
|
### The pages Janela serves
|
|
436
436
|
|
|
@@ -497,13 +497,13 @@ Janela ships the load-bearing core of a BI tool and nothing else. The reasoning
|
|
|
497
497
|
- **Querying rides on [Ransack](https://github.com/activerecord-hackery/ransack)'s association-path traversal.** Janela does not invent a query language.
|
|
498
498
|
- **Cross-filtering is a Stimulus controller plus Turbo Frames.** Click a value in one pane, shared filter state updates, every other frame on the page re-renders.
|
|
499
499
|
- **Charts are [Chart.js](https://www.chartjs.org)**, driven by one small Stimulus controller from the same values the tables show. Not a charting engine.
|
|
500
|
-
- **Publishing creates a Snapshot.** An ActiveJob freezes the result set into a new record; the live dashboard stays editable and the published view is a point-in-time fork, not a toggle on the same record.
|
|
500
|
+
- **Publishing creates a Snapshot.** An ActiveJob freezes the result set into a new record; the live dashboard stays editable and the published view is a point-in-time fork, not a toggle on the same record. The job will not freeze a scope you have not named (ADR 034).
|
|
501
501
|
|
|
502
502
|
Deliberately out of scope: natural-language query, a separate data warehouse, a row-level-security subsystem (use your app's Pundit/CanCanCan), refresh-scheduling UI (schedule the Snapshot job with whatever you already use), embedding SDK, mobile app, print/paginated reports. If you need one of those, the codebase is meant to be small enough to fork and add your own.
|
|
503
503
|
|
|
504
504
|
## Status
|
|
505
505
|
|
|
506
|
-
**v0.
|
|
506
|
+
**v0.8.0 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar and line charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames, STI subclasses, the engine's own pages for reading and editing them and the optional vitral theme work and are covered by unit and real-browser tests, with the classes a theme may target documented in [Theming Janela](docs/theming.md). Not yet built: a visual editor, drill-down on time panes, other chart types. [Where Janela Is Going](docs/roadmap.md) says what 1.0 means and which of these are in it; open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
|
|
507
507
|
|
|
508
508
|
## Development
|
|
509
509
|
|
data/UPGRADING.md
CHANGED
|
@@ -12,6 +12,69 @@ bin/rails janela:doctor
|
|
|
12
12
|
|
|
13
13
|
It reads your application and lists what still needs changing.
|
|
14
14
|
|
|
15
|
+
## 0.7.0 to 0.8.0
|
|
16
|
+
|
|
17
|
+
Two steps, each only if it applies to you: one if you name a parent
|
|
18
|
+
controller, one if you wrote CSS against Janela's own pages. The doctor
|
|
19
|
+
also starts reporting things it always should have; that needs nothing
|
|
20
|
+
from you but a read.
|
|
21
|
+
|
|
22
|
+
**1. Set `Janela.parent_controller` in an initializer, if you set it.**
|
|
23
|
+
|
|
24
|
+
`Janela::ApplicationController` resolves its superclass once, the first
|
|
25
|
+
time the class loads. Naming a different one after that did nothing at
|
|
26
|
+
all, in silence, so your dashboards kept inheriting whatever was named
|
|
27
|
+
first and your authentication and `policy_scope` were not the ones you
|
|
28
|
+
wrote. It raises now rather than being ignored.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# config/initializers/janela.rb <- runs before anything can load it
|
|
32
|
+
Janela.parent_controller = "Admin::BaseController"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Too late, all of them: `config.to_prepare`, `config.after_initialize`,
|
|
36
|
+
an `initializer` block ordered after `load_config_initializers`, and
|
|
37
|
+
anything that runs once the application is serving. The README's own
|
|
38
|
+
recipe for giving Janela a layout is a `to_prepare` block that loads the
|
|
39
|
+
controller, so if you use that, the initializer has to come first, and
|
|
40
|
+
it does.
|
|
41
|
+
|
|
42
|
+
Naming the controller Janela already inherits is still allowed, because
|
|
43
|
+
nothing is being asked for.
|
|
44
|
+
|
|
45
|
+
**2. Check any CSS you wrote against Janela's own pages.**
|
|
46
|
+
|
|
47
|
+
ADR 016 said "the class names are public API", which read literally
|
|
48
|
+
promised that `janela-card`, `janela-crumb`, `janela-flash`,
|
|
49
|
+
`janela-button`, `janela-form` and the rest of the chrome the engine
|
|
50
|
+
renders on *its own* pages would never be renamed without an entry here.
|
|
51
|
+
That was a promise made to nobody about markup only the engine draws,
|
|
52
|
+
and it made Janela's own pages harder to change than the library they
|
|
53
|
+
serve.
|
|
54
|
+
|
|
55
|
+
They are now scoped under `janela-page`, which only the engine's own
|
|
56
|
+
layout sets, so they cannot reach your pages, and they may change in any
|
|
57
|
+
release. Nothing is renamed in this release, so nothing breaks today. If
|
|
58
|
+
you styled Janela's own pages by targeting those class names, that
|
|
59
|
+
stylesheet is no longer standing on a contract.
|
|
60
|
+
|
|
61
|
+
What *is* the contract is unchanged: everything your own markup
|
|
62
|
+
contains, the grid scale and the pane primitives.
|
|
63
|
+
[docs/theming.md](docs/theming.md) lists all of it, the three custom
|
|
64
|
+
properties that carry `janela.css`, and what a theme is expected to
|
|
65
|
+
leave alone.
|
|
66
|
+
|
|
67
|
+
**What to expect from the doctor.** Two checks that could not fire for
|
|
68
|
+
an application whose `policy_scope` reaches for the signed in user now
|
|
69
|
+
do, so `bin/rails janela:doctor` may report findings that were always
|
|
70
|
+
true and never printed. `unscoped-reads` calls your `policy_scope`
|
|
71
|
+
rather than looking for the method, which catches a Pundit application
|
|
72
|
+
with no policy for `Janela::Frame` or `Janela::Snapshot`: that used to
|
|
73
|
+
pass the doctor and raise on every request.
|
|
74
|
+
`hardcoded-disallowed-predicates` drops to a warning and says a file
|
|
75
|
+
*mentions* a key rather than claiming it filters a model, because it
|
|
76
|
+
only ever grepped for the name.
|
|
77
|
+
|
|
15
78
|
## 0.6.0 to 0.7.0
|
|
16
79
|
|
|
17
80
|
Janela has stopped guessing what may be read, in the two places it used
|
|
@@ -87,6 +87,27 @@ table.janela-pane button[aria-pressed="true"] { background: var(--janela-accent)
|
|
|
87
87
|
|
|
88
88
|
canvas.janela-chart { width: 100% !important; max-height: 20rem; }
|
|
89
89
|
|
|
90
|
+
/* A host whose own markup already says what a pane is puts this on any
|
|
91
|
+
ancestor, and the caption and the single value's label stop being drawn
|
|
92
|
+
without leaving the accessibility tree. Hidden rather than removed on
|
|
93
|
+
purpose: a caption is the table's accessible name, so display: none would
|
|
94
|
+
land a screen reader on a grid of numbers with nothing to say what they
|
|
95
|
+
measure. The rule ships here because that is easy to get wrong and every
|
|
96
|
+
host would otherwise write it (ADR 036, #26). A chart pane needs nothing:
|
|
97
|
+
its title was only ever an aria-label. */
|
|
98
|
+
.janela-own-headings table.janela-pane caption,
|
|
99
|
+
.janela-own-headings .janela-value-label {
|
|
100
|
+
position: absolute;
|
|
101
|
+
width: 1px;
|
|
102
|
+
height: 1px;
|
|
103
|
+
margin: -1px;
|
|
104
|
+
padding: 0;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
clip-path: inset(50%);
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
border: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
/* Janela's own pages (ADR 013). Scoped to a class the engine's layout sets,
|
|
91
112
|
so including this stylesheet changes nothing about a host's own pages. */
|
|
92
113
|
.janela-page {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Date: 2026-09-20
|
|
3
3
|
Status: Accepted
|
|
4
4
|
Related: ADR 002, ADR 004, ADR 015, ADR 019, ADR 021, ADR 025
|
|
5
|
+
Superseded in part by: ADR 035
|
|
5
6
|
Triggers:
|
|
6
7
|
- deciding what Janela should do when a host has configured nothing
|
|
7
8
|
- adding a hook a host answers by defining a method
|
|
@@ -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.
|