Answer product-owner round 1: 0 🔴, 1 🟡 (0 misread); scripts a plugin links stay unversioned until item 43, assetHref listed as reserved, the per-render stat goes to the architect

This commit is contained in:
2026-10-31 10:16:05 +01:00
parent 236f4c4610
commit a7135763bb
3 changed files with 16 additions and 5 deletions
+2 -1
View File
@@ -246,7 +246,8 @@ Revisit only if the stated reason stops holding.
- **`locale` is a host-owned query param** — in `parseListQuery`'s reserved set, so a localized list - **`locale` is a host-owned query param** — in `parseListQuery`'s reserved set, so a localized list
page doesn't hand a plugin a phantom `locale` filter. The i18n view locals (`t`, `locale`, `locales`, page doesn't hand a plugin a phantom `locale` filter. The i18n view locals (`t`, `locale`, `locales`,
`localeHref`, `localeParam`, `localeSwitch`, `dir`) are likewise reserved, merged after a handler's `localeHref`, `localeParam`, `localeSwitch`, `dir`) are likewise reserved, merged after a handler's
`data` so a collision loses the key instead of breaking the shell. `data` so a collision loses the key instead of breaking the shell — as is `assetHref`
(`src/http/static.ts`), which stays undocumented for plugins because calling it is not yet contract.
- **Catalogs are checked at boot, not at render.** Every locale is compared against its set's `en-US` - **Catalogs are checked at boot, not at render.** Every locale is compared against its set's `en-US`
— keys, string-vs-plural kind, and the plural categories `Intl.PluralRules` requires — and a — keys, string-vs-plural kind, and the plural categories `Intl.PluralRules` requires — and a
mismatch stops startup. A plugin may ship fewer locales than the host (its strings fall back to mismatch stops startup. A plugin may ship fewer locales than the host (its strings fall back to
+3 -3
View File
@@ -511,8 +511,8 @@ export async function listThings(ctx: RequestContext) {
`include()` the core building-block partials and its own. To load the plugin's own CSS, pass its `include()` the core building-block partials and its own. To load the plugin's own CSS, pass its
`/public/<id>/x.css` href in the shell's `styles` slot — see the reference's `views/shifts.ejs`. `/public/<id>/x.css` href in the shell's `styles` slot — see the reference's `views/shifts.ejs`.
The host adds the file's content hash to every `/public/` href the shell renders, so browsers and The host adds the file's content hash to every `/public/` href the shell renders, so browsers and
any cache in front of Plainpages keep it for a year; a `/public/` URL your own markup writes is any cache in front of Plainpages keep the file for a year; a `/public/` URL your own markup writes
kept for five minutes, then revalidated. is kept for five minutes, then revalidated.
- **Finer authorization than the route `permission`** uses the guards from `@plainpages/plugin-api`: - **Finer authorization than the route `permission`** uses the guards from `@plainpages/plugin-api`:
`requireSession(ctx)`, `can(ctx, permission)` (coarse JWT-claim check, zero I/O), and `requireSession(ctx)`, `can(ctx, permission)` (coarse JWT-claim check, zero I/O), and
`check(keto, ctx, {namespace, object, relation})` (a live Keto check; anonymous ⇒ denied). Throw `check(keto, ctx, {namespace, object, relation})` (a live Keto check; anonymous ⇒ denied). Throw
@@ -1175,7 +1175,7 @@ Rules worth knowing:
edit/new/remove/save`, `filter.*`, `pagination.*`, `table.*` — and a plugin's lookup falls through edit/new/remove/save`, `filter.*`, `pagination.*`, `table.*` — and a plugin's lookup falls through
to them. Keep your catalog for your domain words. to them. Keep your catalog for your domain words.
- **Reserved view locals:** `t`, `locale`, `locales`, `localeHref`, `localeParam`, `localeSwitch`, - **Reserved view locals:** `t`, `locale`, `locales`, `localeHref`, `localeParam`, `localeSwitch`,
`dir`. They are merged after your `data`, so a colliding key of yours is ignored rather than `dir`, and the host-internal `assetHref`. They are merged after your `data`, so a colliding key of yours is ignored rather than
breaking the shell. `locale` is likewise reserved in `parseListQuery`, never returned as a filter. breaking the shell. `locale` is likewise reserved in `parseListQuery`, never returned as a filter.
**Kratos writes the auth flow's own text** (field labels, validation errors), tagging each string **Kratos writes the auth flow's own text** (field labels, validation errors), tagging each string
+11 -1
View File
@@ -6,7 +6,7 @@
`Bar = 1` `Bar = 1`
`Next ID = 43` `Next ID = 44`
## Items ## Items
@@ -22,6 +22,7 @@
| 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 | | 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 |
| 6 | 0.5.0 | | | **Warn at discovery when a route or nav node gates on a permission no plugin declares.** | 2 | 1 | 1 | 2 | 2 | 7 by 3, 9 by 3 | 8.0 | | 6 | 0.5.0 | | | **Warn at discovery when a route or nav node gates on a permission no plugin declares.** | 2 | 1 | 1 | 2 | 2 | 7 by 3, 9 by 3 | 8.0 |
| 23 | 0.5.0 | | | **Give list pages one view-model helper from query to pagination, instead of ~40 hand-written lines each.** | 3 | 1 | 2 | 4 | 5 | 1 by 4, 9 by 2, code health by 3 | 7.9 | | 23 | 0.5.0 | | | **Give list pages one view-model helper from query to pagination, instead of ~40 hand-written lines each.** | 3 | 1 | 2 | 4 | 5 | 1 by 4, 9 by 2, code health by 3 | 7.9 |
| 43 | 0.5.0 | | | **Let a plugin version the scripts and other `/public/` files its own markup links, so they are cached as immutable too.** | 2 | 2 | 1 | 3 | 3 | 8 by 3, 7 by 2 | 5.0 |
| 28 | 0.5.0 | | | **Let a plugin add entries to the profile menu through the plugin API.** | 2 | 2 | 2 | 4 | 4 | 7 by 4, 1 by 2 | 4.2 | | 28 | 0.5.0 | | | **Let a plugin add entries to the profile menu through the plugin API.** | 2 | 2 | 2 | 4 | 4 | 7 by 4, 1 by 2 | 4.2 |
| 2 | 0.5.0 | | | **Rename the admin example plugin to a name that says what it administers.** | 3 | 1 | 2 | 4 | 6 | 9 by 3, code health by 2 | 2.6 | | 2 | 0.5.0 | | | **Rename the admin example plugin to a name that says what it administers.** | 3 | 1 | 2 | 4 | 6 | 9 by 3, code health by 2 | 2.6 |
| 5 | 0.5.0 | | | **List grants that no installed plugin declares, with a remove action.** | 2 | 2 | 2 | 4 | 4 | 2 by 3 | 2.1 | | 5 | 0.5.0 | | | **List grants that no installed plugin declares, with a remove action.** | 2 | 2 | 2 | 4 | 4 | 2 by 3 | 2.1 |
@@ -104,6 +105,15 @@ is ungrantable from the GUI and answers a permanent 403. A warning, not an error
Every list screen bridges `parseListQuery`/`paginate` to the partials by hand. At minimum a Every list screen bridges `parseListQuery`/`paginate` to the partials by hand. At minimum a
`buildPaginationModel(page, hrefFor)`. Exported through the barrel, so it is contract surface. `buildPaginationModel(page, hrefFor)`. Exported through the barrel, so it is contract surface.
### 43. Let a plugin version the scripts and other `/public/` files its own markup links, so they are cached as immutable too.
The host versions every `/public/` href it renders (`src/http/static.ts`): the shell's stylesheets,
favicon and logo, and a plugin's `styles`. A file a plugin's own markup links, such as fastjira's
`board-drag.js`, keeps an unversioned URL and is revalidated every five minutes. Offering a way —
`assetHref` documented as a view local, a `ctx.assetHref`, or a `scripts` slot in the shell — adds to
the plugin API, hence a minor. Pick one with the plugin authors' panel, then document it in README →
Reserved view locals.
### 28. Let a plugin add entries to the profile menu through the plugin API. ### 28. Let a plugin add entries to the profile menu through the plugin API.
The profile dropdown stays a dropdown: Sign out is its only entry today, and it will gain more. The profile dropdown stays a dropdown: Sign out is its only entry today, and it will gain more.