From 16da74f03fa90ef3fe67ef01ced84f212f3ed6e0 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 02:14:39 +0100 Subject: [PATCH 1/6] Record the reading cost of the last chunks --- docs/reading-cost.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 9a758d9..abc5218 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -47,3 +47,15 @@ Read most (opens and searches): - `examples/plugins/scheduling/views/overview.ejs` 1 - `examples/plugins/scheduling/views/shifts.ejs` 1 - `src/http/app.test.ts` 1 + +## Reading cost, chunk 91d11dbe63f9, finished 2026-10-31T01:13:44Z + +69010 new input tokens before the first commit. + +Read most (opens and searches): + +- `todo.md` 7 +- `README.md` 4 +- `public/css/styles.css` 4 +- `CHANGELOG.md` 1 +- `src/ui/stylesheet-floor.test.ts` 1 -- 2.52.0 From 1905d2c3101179f4f6825e390d17f015adda5267 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 02:14:39 +0100 Subject: [PATCH 2/6] Add a README glossary and intended scale; correct the TOKENS comment --- CHANGELOG.md | 5 +++++ README.md | 24 ++++++++++++++++++++++++ public/css/styles.css | 11 +++++++---- todo.md | 16 ---------------- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21666af..170f195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ at boot. Entries start at 0.3.0. ## Unreleased +### The README defines its terms and states its intended scale + +A new Glossary defines host, manifest, chrome, nav fragment, permission token, system plugin and +denylist. It also states the scale Plainpages is sized for: a handful of plugins per deployment. + ### The mobile nav stays reachable on engines without `:has()` At 860px and narrower, an engine without `:has()` (Chromium below 105, such as webOS TV 22 and 23, diff --git a/README.md b/README.md index b5deb5c..1dfc79a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ From here, render real pages against the app shell and fetch upstream data — s - [Goals](#goals) - [Audience](#audience) - [Overview](#overview) +- [Glossary](#glossary) - [Users, groups & permissions](#users-groups--permissions) - [naming a permission](#naming-a-permission) - [a worked example](#a-worked-example) @@ -210,6 +211,29 @@ feature removes the need for JS (theme switching, popovers, disclosure) we use i avoid is shipping a client-side runtime, not using the platform. Markup is semantic and accessible (see [AGENTS.md](AGENTS.md)). +## Glossary + +- **Host** — the `web` container: the Node process that discovers plugins, serves every page and + owns no state of its own. Everything outside it (a plugin's upstream, Ory) is reached over the network. +- **Plugin** — a folder under `plugins/` that adds domain screens; the only way to add a feature. +- **Manifest** — the object a plugin's `index.ts` exports through `definePlugin`: its routes, nav, + permissions and hooks. The host checks it at boot and refuses a bad one ([The manifest](#the-manifest)). +- **Chrome** — everything on a page that isn't the page's own content: brand, menu, user, theme and + CSRF token. A handler gets it as `ctx.chrome` ([RequestContext](#requestcontext)). +- **Nav fragment** — the `nav` entries one plugin contributes. The host merges every plugin's + fragment into the one global menu and filters it per user ([The menu system](#the-menu-system)). +- **Permission token** — a `:` string such as `shifts:write`, granted in Keto and + carried in the session JWT's `permissions` claim ([Naming a permission](#naming-a-permission)). +- **System plugin** — a plugin that administers Plainpages' own identity stack, through the + privileged `ctx.system` ([System capabilities](#system-capabilities-the-ctxsystem-surface)). +- **Denylist** — the optional in-memory list of subjects revoked just now, so a permission change + lands before the JWT expires ([Instant revoke](#instant-revoke-the-optional-denylist)). + +**Intended scale.** One deployment serves a handful of plugins, up to about ten, so a plugin's own +dependency tree ([Plugin dependencies](#plugin-dependencies)) costs disk and memory in proportion to +how many are installed. The `web` container scales out because it is stateless ([Stateless +core](#stateless-core)); a catalogue of hundreds of plugins is not a goal. + ## Users, groups & permissions Authorization here is two hops: a **user** — directly, or through a **group** — is granted a diff --git a/public/css/styles.css b/public/css/styles.css index 2b71a45..11aaa0f 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -9,14 +9,17 @@ /* ---------- 1. TOKENS ---------------------------------------- Two raw palettes (--l-* light, --d-* dark) are defined once. A small mapping (--bg: var(--l-bg) ...) picks the active set. - Theme resolution (no JS): + Theme resolution (no JS), every rule below needing :has(): • default + @media(dark) → follows the OS (Auto) • html:has(#theme-light) → force light • html:has(#theme-dark) → force dark + Engines without :has() (Chromium below 105, Firefox below 121) + ignore all three and stay light; see the @supports not + selector(:has(a)) block further down. We avoid light-dark()/color-scheme for app colors because their - resolution is unreliable across engines; :has() variable swaps - are rock-solid. color-scheme is still set as a hint for native - widgets (date picker, scrollbars, selects). + resolution is unreliable across engines. color-scheme is still + set as a hint for native widgets (date picker, scrollbars, + selects). ------------------------------------------------------------- */ :root { /* Hex, never oklch: below Chromium 111 `var(--accent)` is invalid at computed-value time and the diff --git a/todo.md b/todo.md index a1c3da1..aeba5fa 100644 --- a/todo.md +++ b/todo.md @@ -12,13 +12,11 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| -| 26 | 0.4.21 | | | **Add a domain glossary and the project's intended horizon to the README.** | 1 | 2 | 1 | 2 | 3 | 9 by 3, code health by 2 | 11.9 | | 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 | | 9 | 0.4.21 | | | **Typecheck `e2e-tests/` in the gate.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 | | 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 | | 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 | | 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 | -| 38 | 0.4.21 | | | **Correct the TOKENS comment at the top of `public/css/styles.css` so it no longer claims `:has()` swaps are "rock-solid" or that the theme follows the OS on every engine.** | 1 | 2 | 1 | 2 | 1 | code health by 1 | 7.2 | | 32 | 0.4.21 | | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 | | 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 | | 4 | 0.4.21 | | | **Refuse a permission save whose baseline no longer matches Keto, so concurrent edits are not silently lost.** | 2 | 2 | 2 | 4 | 4 | 2 by 4 | 2.8 | @@ -39,12 +37,6 @@ ## Details -### 26. Add a domain glossary and the project's intended horizon to the README. - -Glossary: host, manifest, chrome, nav fragment, permission token, system plugin, denylist. Also the -expected plugin count, since per-plugin dependency isolation prices N dependency trees on disk and -in RSS. - ### 24. Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`. `ShellModel`/`buildShellContext` has one consumer, the dashboard, and duplicates `PageChrome` on @@ -59,14 +51,6 @@ AGENTS.md → "`ICON_NAMES` is a host-owned registry" already makes this routine Only `tsc --strict` runs today. A new tool is a new dependency, so justify it. -### 38. Correct the TOKENS comment at the top of `public/css/styles.css` so it no longer claims `:has()` swaps are "rock-solid" or that the theme follows the OS on every engine. - -Section 1's TOKENS comment says the theme follows the OS by default and that `:has()` variable swaps -are "rock-solid". On an engine without `:has()` (Chromium below 105, Firefox below 121) every theme -rule is inert, the OS-dark rule included, so those engines stay light, as the -`@supports not selector(:has(a))` block further down says. A reader trusting the comment expects dark -mode there. - ### 32. Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded. Nothing checks README → "Two browser floors", and no E2E engine is old enough to show a break. -- 2.52.0 From 25b24a1057d5a41a3a79386f38c928682f7b6eb9 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 02:17:30 +0100 Subject: [PATCH 3/6] =?UTF-8?q?Answer=20prose=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=205=20=F0=9F=9F=A1=20(1=20misread);=20manifest=20?= =?UTF-8?q?is=20plugin.ts,=20rename=20permission=20token=20to=20permission?= =?UTF-8?q?=20name,=20narrow=20the=20denylist=20claim,=20move=20the=20scal?= =?UTF-8?q?e=20into=20Plugin=20dependencies,=20add=20Safari=20to=20the=20:?= =?UTF-8?q?has()=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- CHANGELOG.md | 5 +++-- README.md | 18 ++++++++---------- public/css/styles.css | 6 +++--- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6e599ea..f4f7e00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -370,7 +370,7 @@ docker compose -f compose.yml up --build -d # production stack up and a *minimal* plugin live. Nothing comes before Quick start. Keep its commands copy-pasteable; deeper detail lives in its own section, linked. 2. **Returning developer (rest).** A **Contents** ToC right after Quick start, then sections ordered - by **what an adopter reaches for first**, not by architectural layering: Goals → Audience → Overview → Users, groups + by **what an adopter reaches for first**, not by architectural layering: Goals → Audience → Overview → Glossary → Users, groups & permissions → Building plugins → menu/blocks/interactivity → Configuration → Auth → Email → Architecture → Testing → Production → Observability → JWT-rotation runbook → Project-layout file map → Extending. Place a new section by how early an adopter needs it. **Users, groups & diff --git a/CHANGELOG.md b/CHANGELOG.md index 170f195..d4ee1ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,9 @@ at boot. Entries start at 0.3.0. ### The README defines its terms and states its intended scale -A new Glossary defines host, manifest, chrome, nav fragment, permission token, system plugin and -denylist. It also states the scale Plainpages is sized for: a handful of plugins per deployment. +A new Glossary defines host, plugin, manifest, chrome, nav fragment, permission name, system plugin +and denylist. Plugin dependencies also states the scale Plainpages is sized for: a handful of plugins +per deployment. ### The mobile nav stays reachable on engines without `:has()` diff --git a/README.md b/README.md index 1dfc79a..7d64b51 100644 --- a/README.md +++ b/README.md @@ -215,24 +215,19 @@ avoid is shipping a client-side runtime, not using the platform. Markup is seman - **Host** — the `web` container: the Node process that discovers plugins, serves every page and owns no state of its own. Everything outside it (a plugin's upstream, Ory) is reached over the network. -- **Plugin** — a folder under `plugins/` that adds domain screens; the only way to add a feature. -- **Manifest** — the object a plugin's `index.ts` exports through `definePlugin`: its routes, nav, +- **Plugin** — a folder under `plugins/` that adds domain features. +- **Manifest** — the object a plugin's `plugin.ts` exports as its default: its routes, nav, permissions and hooks. The host checks it at boot and refuses a bad one ([The manifest](#the-manifest)). - **Chrome** — everything on a page that isn't the page's own content: brand, menu, user, theme and CSRF token. A handler gets it as `ctx.chrome` ([RequestContext](#requestcontext)). - **Nav fragment** — the `nav` entries one plugin contributes. The host merges every plugin's fragment into the one global menu and filters it per user ([The menu system](#the-menu-system)). -- **Permission token** — a `:` string such as `shifts:write`, granted in Keto and +- **Permission name** — a `:` string such as `scheduling:write`, granted in Keto and carried in the session JWT's `permissions` claim ([Naming a permission](#naming-a-permission)). - **System plugin** — a plugin that administers Plainpages' own identity stack, through the privileged `ctx.system` ([System capabilities](#system-capabilities-the-ctxsystem-surface)). -- **Denylist** — the optional in-memory list of subjects revoked just now, so a permission change - lands before the JWT expires ([Instant revoke](#instant-revoke-the-optional-denylist)). - -**Intended scale.** One deployment serves a handful of plugins, up to about ten, so a plugin's own -dependency tree ([Plugin dependencies](#plugin-dependencies)) costs disk and memory in proportion to -how many are installed. The `web` container scales out because it is stateless ([Stateless -core](#stateless-core)); a catalogue of hundreds of plugins is not a goal. +- **Denylist** — the optional in-memory list, per instance, of subjects revoked just now, so a + deactivation or a user's own permission change lands before the JWT expires ([Instant revoke](#instant-revoke-the-optional-denylist)). ## Users, groups & permissions @@ -817,6 +812,9 @@ included, since the plugin folder *is* the repo. A baked image needs no extra st above every plugin, and a plugin resolves it from there — nothing to declare, just import it. A copy inside your plugin's own `node_modules` would shadow it with a *second* instance of the host's contract, turning a sign-in redirect into a 500, so discovery refuses one there at boot. +- **Plainpages is sized for a handful of plugins per deployment, about ten.** Each plugin carries + its own dependency tree, so every one adds to disk use and to the memory of the `web` process; + hundreds of plugins is not a goal. - **The host never upgrades or dedupes your dependencies.** Two plugins depending on the same package each get their own copy at their own version, so neither can break the other by upgrading — and keeping yours current, and audited, is yours to own. Renovate here watches every manifest in this diff --git a/public/css/styles.css b/public/css/styles.css index 11aaa0f..92add70 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -9,12 +9,12 @@ /* ---------- 1. TOKENS ---------------------------------------- Two raw palettes (--l-* light, --d-* dark) are defined once. A small mapping (--bg: var(--l-bg) ...) picks the active set. - Theme resolution (no JS), every rule below needing :has(): + Theme resolution (no JS); all three rules below need :has(): • default + @media(dark) → follows the OS (Auto) • html:has(#theme-light) → force light • html:has(#theme-dark) → force dark - Engines without :has() (Chromium below 105, Firefox below 121) - ignore all three and stay light; see the @supports not + Engines without :has() (Chromium below 105, Firefox below 121, + Safari below 15.4) ignore all three and stay light; see the @supports not selector(:has(a)) block further down. We avoid light-dark()/color-scheme for app colors because their resolution is unreliable across engines. color-scheme is still -- 2.52.0 From 3f30cfafe6aaeffa4333a7dc24979d317ddf1d55 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 02:17:30 +0100 Subject: [PATCH 4/6] =?UTF-8?q?Answer=20product-owner=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=203=20=F0=9F=9F=A1=20(1=20misread);=20fixes=20lan?= =?UTF-8?q?ded=20with=20the=20prose=20round,=20the=20about-ten=20figure=20?= =?UTF-8?q?waits=20on=20the=20owner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- 2.52.0 From af157c95fe1f379989285679d95c647580b9e22f Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 02:18:12 +0100 Subject: [PATCH 5/6] =?UTF-8?q?Answer=20product-owner=20round=202:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=200=20=F0=9F=9F=A1=20(0=20misread);=20approved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- 2.52.0 From b336dee6d7986d91cc8249ce1f6e2cb591b742de Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 02:19:51 +0100 Subject: [PATCH 6/6] Record the comprehension check: paired ruling better for PR #184 --- docs/comprehension-history.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/comprehension-history.md b/docs/comprehension-history.md index 4c560a8..8ddb7a7 100644 --- a/docs/comprehension-history.md +++ b/docs/comprehension-history.md @@ -62,3 +62,24 @@ Inherited architect, decided by: - Self-sufficiency, Overall: `public/css/styles.css:5` `Architecture` header: the false `light-dark()` bullet is gone, so the header no longer contradicts the TOKENS comment - Locality, Self-sufficiency, Overall: `public/css/styles.css:687` `@supports selector(:has(a))` in `@media (max-width: 860px)`: the guard and its comment put the dependency of the open state on `:has()` where an editor would change it + +## 2026-10-31T01:19:49Z, PR #184 at af157c9, against bb8e6bc + +Ruling: better + +| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | +|---|---|---|---|---|---| +| Mid A | better | same | same | better | better | +| Inherited architect | better | better | same | better | better | + +Mid A, decided by: + +- Self-sufficiency, Navigation, Overall: `public/css/styles.css:12` `TOKENS` header comment: says all three theme rules need `:has()`, names the engines without it and points to the `@supports not selector(:has(a))` block, where the old text called the swaps "rock-solid" +- Navigation, Self-sufficiency, Overall: `README.md:214` `## Glossary`: defines host, plugin, manifest, chrome, nav fragment, permission name, system plugin and denylist and links each to its home section +- Self-sufficiency: `README.md:815` `Plugin dependencies`: the scale bullet states the plugin count the per-plugin dependency design assumes + +Inherited architect, decided by: + +- Self-sufficiency, Locality, Overall: `public/css/styles.css:12` `1. TOKENS` comment: an editor of the theme mapping learns of the distant `:has()` fallback from the comment, where the old text said the swap could not fail +- Navigation, Self-sufficiency, Overall: `README.md:214` `## Glossary`: a cold reader reaches "chrome" or "denylist" and their home sections in one hop +- Self-sufficiency: `README.md:815` `### Plugin dependencies`: the scale bullet gives the plugin count the dependency design assumes -- 2.52.0