From 74e30c98788a77bb8e7643565f91ceee090a8a6b Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 29 Sep 2026 03:37:38 +0200 Subject: [PATCH] test(10.1): persist human verification items as UAT --- .../10.1-UAT.md | 52 +++++ .../10.1-VERIFICATION.md | 218 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 .planning/phases/10.1-runtime-admin-extension-point/10.1-UAT.md create mode 100644 .planning/phases/10.1-runtime-admin-extension-point/10.1-VERIFICATION.md diff --git a/.planning/phases/10.1-runtime-admin-extension-point/10.1-UAT.md b/.planning/phases/10.1-runtime-admin-extension-point/10.1-UAT.md new file mode 100644 index 0000000..d52986d --- /dev/null +++ b/.planning/phases/10.1-runtime-admin-extension-point/10.1-UAT.md @@ -0,0 +1,52 @@ +--- +status: testing +phase: 10.1-runtime-admin-extension-point +source: [10.1-VERIFICATION.md] +started: 2026-09-29T01:37:38Z +updated: 2026-09-29T01:37:38Z +--- + +## Current Test + +number: 1 +name: Real browser, CSP and custom elements: run `summer serve` in fonoteka.go and open /plytadmin > Albumy (pl). Open an album's form and the create form. Watch the console. +expected: | + No CSP violation. /plytadmin/assets/golem15/fonoteka/js/discogs-lookup.js?v=… loads as a module script. The golem15-fonoteka-discogs-lookup element upgrades and shows the localized 'Load from Discogs' button on create and update. Clicking it shows the busy label, then fills Release year 1977 and Format LP into the unsaved form with a success toast. Save persists both, and they survive a reload. +awaiting: user response + +## Tests + +### 1. Real browser, CSP and custom elements: run `summer serve` in fonoteka.go and open /plytadmin > Albumy (pl). Open an album's form and the create form. Watch the console. +expected: No CSP violation. /plytadmin/assets/golem15/fonoteka/js/discogs-lookup.js?v=… loads as a module script. The golem15-fonoteka-discogs-lookup element upgrades and shows the localized 'Load from Discogs' button on create and update. Clicking it shows the busy label, then fills Release year 1977 and Format LP into the unsaved form with a success toast. Save persists both, and they survive a reload. +result: [pending] + +### 2. 768px layout with the pl copy: narrow the window to 768px on Albumy. +expected: The stats strip items wrap inside one card with no horizontal scroll and no truncated labels. The widget button grows past its 160px min-width. The toolbar cluster (Create, Delete, Sync with Discogs) wraps and the labels do not truncate. Sync with Discogs shows the test-mode toast and refetches the strip. +result: [pending] + +### 3. Vite dev proxy: run the admin dev server (`npm --prefix admin run dev`) against a running fonoteka backend and open Albumy. +expected: Plugin JS and CSS load through the Vite {prefix}/assets proxy and the widget mounts. +result: [pending] + +### 4. Plugin CSS isolation: open Albumy, then Gatunki (Genres), then Settings. Also try fast navigation: open Albumy and immediately click Gatunki before the Albums schema returns (throttle the network in devtools). +expected: The albums.css link is disabled on Gatunki. Open decision: the code keeps it enabled on Settings/non-controller views (review WR-02), and a late Albums schema response can re-enable albums.css over Gatunki (review WR-01). Decide whether to fix both now or accept them as low-severity (T-10.1-16 is rated low). +result: [pending] + +### 5. Decide the open code-review findings (10.1-REVIEW-DISPOSITION.md still records all 14 as open). CR-01: typing 1977.5 (or 1e21) into the new Albums Release year field and saving gives a generic server-error toast (HTTP 500) instead of a 422 field message. WR-03: the partial view-model guard only checks the top-level type. WR-04: isJSONScalar trusts reflect.Kind. WR-05: widgets are shown to admins who lack the action permission. WR-06: the widget route ignores the field's context. +expected: Each finding is set to fixed, deferred (with a reason and target phase) or skipped before the phase is closed. Recommendation: fix CR-01 now. It is a user-facing error on a field this phase added, and no later phase covers it. +result: [pending] + +### 6. Review the 14 judgment-tier prohibitions in the four plans (table 'Prohibitions' in this report). +expected: Confirm the non-authoritative verdict: all 14 hold in the shipped code. One (10.1-01 'no template receives a GORM model … or a raw HTML string marked safe') holds only because the Albums and fixture view models are curated structs. The framework guard that should enforce it is shallow (WR-03). +result: [pending] + +## Summary + +total: 6 +passed: 0 +issues: 0 +pending: 6 +skipped: 0 +blocked: 0 + +## Gaps diff --git a/.planning/phases/10.1-runtime-admin-extension-point/10.1-VERIFICATION.md b/.planning/phases/10.1-runtime-admin-extension-point/10.1-VERIFICATION.md new file mode 100644 index 0000000..8078c72 --- /dev/null +++ b/.planning/phases/10.1-runtime-admin-extension-point/10.1-VERIFICATION.md @@ -0,0 +1,218 @@ +--- +phase: 10.1-runtime-admin-extension-point +verified: 2026-09-29T01:40:00Z +status: human_needed +score: 53/57 must-haves verified (5/5 roadmap success criteria; 48/52 plan truths — 3 backstop truths need a real browser, 1 uncertain because of review finding WR-01) +covered_files: + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-01-PLAN.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-02-PLAN.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-02-SUMMARY.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-03-PLAN.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-03-SUMMARY.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-04-PLAN.md" + - ".planning/phases/10.1-runtime-admin-extension-point/10.1-04-SUMMARY.md" + - "admin/openapi/admin.json" + - "admin/src/app/pluginAssets.ts" + - "admin/src/components/form/fields/WidgetField.vue" + - "admin/src/components/list/ListToolbar.vue" + - "admin/src/components/partial/PartialHost.vue" + - "admin/src/components/partial/partialNodes.ts" + - "admin/src/views/FormView.vue" + - "admin/src/views/ListView.vue" + - "modules/boardwalk/boardwalk.go" + - "modules/cabana/actions.go" + - "modules/cabana/crud.go" + - "modules/cabana/extension.go" + - "modules/cabana/form_schema.go" + - "modules/cabana/http.go" + - "modules/cabana/list_schema.go" + - "modules/cabana/partial_render.go" + - "modules/cabana/plugin_assets.go" + - "modules/cabana/registry.go" + - "modules/lagoon/fill.go" + - "modules/pact/capabilities.go" + - "scripts/check-phase10.1.sh" +covered_digest: "v2:sha256:de162c1152f0f52db5fdedb54f8d2a311c5ee087777027f77771fc41f7dab3ef" +behavior_unverified: 0 +overrides_applied: 0 +human_verification: + - test: "Real browser, CSP and custom elements: run `summer serve` in fonoteka.go and open /plytadmin > Albumy (pl). Open an album's form and the create form. Watch the console." + expected: "No CSP violation. /plytadmin/assets/golem15/fonoteka/js/discogs-lookup.js?v=… loads as a module script. The golem15-fonoteka-discogs-lookup element upgrades and shows the localized 'Load from Discogs' button on create and update. Clicking it shows the busy label, then fills Release year 1977 and Format LP into the unsaved form with a success toast. Save persists both, and they survive a reload." + why_human: "happy-dom neither enforces CSP nor loads module scripts, and the element is mocked in Vitest (backstop truth 10.1-02 S6, 10.1-03 D5)." + - test: "768px layout with the pl copy: narrow the window to 768px on Albumy." + expected: "The stats strip items wrap inside one card with no horizontal scroll and no truncated labels. The widget button grows past its 160px min-width. The toolbar cluster (Create, Delete, Sync with Discogs) wraps and the labels do not truncate. Sync with Discogs shows the test-mode toast and refetches the strip." + why_human: "Visual layout. Backstop truths 10.1-03 long-text S1 and S3/S4 require a visual check." + - test: "Vite dev proxy: run the admin dev server (`npm --prefix admin run dev`) against a running fonoteka backend and open Albumy." + expected: "Plugin JS and CSS load through the Vite {prefix}/assets proxy and the widget mounts." + why_human: "Needs two running servers and a browser." + - test: "Plugin CSS isolation: open Albumy, then Gatunki (Genres), then Settings. Also try fast navigation: open Albumy and immediately click Gatunki before the Albums schema returns (throttle the network in devtools)." + expected: "The albums.css link is disabled on Gatunki. Open decision: the code keeps it enabled on Settings/non-controller views (review WR-02), and a late Albums schema response can re-enable albums.css over Gatunki (review WR-01). Decide whether to fix both now or accept them as low-severity (T-10.1-16 is rated low)." + why_human: "Race and cross-view state are not exercised by any test. The sequential path is tested (pluginAssets.test.ts, extension.smoke.test.ts)." + - test: "Decide the open code-review findings (10.1-REVIEW-DISPOSITION.md still records all 14 as open). CR-01: typing 1977.5 (or 1e21) into the new Albums Release year field and saving gives a generic server-error toast (HTTP 500) instead of a 422 field message. WR-03: the partial view-model guard only checks the top-level type. WR-04: isJSONScalar trusts reflect.Kind. WR-05: widgets are shown to admins who lack the action permission. WR-06: the widget route ignores the field's context." + expected: "Each finding is set to fixed, deferred (with a reason and target phase) or skipped before the phase is closed. Recommendation: fix CR-01 now. It is a user-facing error on a field this phase added, and no later phase covers it." + why_human: "None of these defeats a roadmap success criterion or a plan must-have (see the report), so none is a blocker. Accepting or fixing them is a developer decision." + - test: "Review the 14 judgment-tier prohibitions in the four plans (table 'Prohibitions' in this report)." + expected: "Confirm the non-authoritative verdict: all 14 hold in the shipped code. One (10.1-01 'no template receives a GORM model … or a raw HTML string marked safe') holds only because the Albums and fixture view models are curated structs. The framework guard that should enforce it is shallow (WR-03)." + why_human: "unverified-prohibition: human review recommended. The prohibitions carry no test-tier enforcement tag, and the verifier's verdict is an LLM judgment." +--- + +# Phase 10.1: Runtime admin extension point Verification Report + +**Phase goal:** A plugin extends the compiled admin SPA without a Node rebuild. Controllers declare their own JS/CSS, served same-origin from the plugin's embedded files. `type: widget` fields mount plugin custom elements whose actions the SPA posts. `type: partial` form fields and a list `headerPartial` render server-side through `html/template` and reach the page without any raw-HTML sink. Controllers register named toolbar actions. The framework contract is proven on a nameless fixture plugin. The application proof is three Albums surfaces: a statistics strip, a Discogs lookup widget and a Discogs sync toolbar action, both Discogs actions being stubs that Phase 14 replaces. +**Verified:** 2026-09-29T01:40:00Z +**Status:** human_needed +**Re-verification:** No (initial verification) + +## Goal Achievement + +### Roadmap success criteria + +| # | Success criterion | Status | Evidence | +|---|-------------------|--------|----------| +| 1 | Controller JS/CSS served from embedded files under `{backend.uri}/assets/{vendor}/{plugin}/…` through an exact allowlist, loaded only when that controller's list/form opens, under CSP `script-src 'self'`; undeclared files and traversal never leave the plugin tree | ✓ VERIFIED (real-browser CSP load → human) | `compileClientAssets` (extension.go) reads and hashes only declared `assets/*.js/.mjs/.css`; `pluginAsset` (plugin_assets.go) looks up `s.reg.assets[vendor/plugin/file]` by exact key, and a miss falls through to `serveSPA`. Route at http.go:254. `boardwalk.SetSecurityHeaders` sets CSP containing `script-src 'self'` (boardwalk.go:32). `TestPhase101Assets` asserts CSP and rejects `_stats.htm`, `..%2F…config_list.yaml`, `%2e%2e/…`. The fonoteka test asserts a GET of `/plytadmin/assets/golem15/fonoteka/models/album/fields.yaml` is refused. SPA: `loadControllerAssets` is called only from ListView.vue:128 and FormView.vue:159 after the schema arrives. `assetAllowed` enforces the `{base}/assets/` prefix, including encoded dot segments | +| 2 | `type: widget` mounts the plugin custom element; its event makes the SPA POST the declared action with the admin cookie and CSRF header; only declared `fill` keys are patched onto the unsaved form | ✓ VERIFIED (real element upgrade → human) | WidgetField.vue creates the element imperatively, sets attributes only, and on `summer-action` calls `api.POST('/{vendor}/{plugin}/{controller}/widgets/{field}')`. client.ts sets `X-Requested-With` and `credentials: 'same-origin'`. The patch loop applies only `field.fill` keys present in `result.fill`. The server route is `requireAjax(s.widgetAction)`, and `runAction` passes the result through `onlyFillScalars(field.Fill, …)`. WidgetField.test.ts (22 tests) and extension.smoke.test.ts pass | +| 3 | `headerPartial` and `type: partial` render server-side with `html/template` from a controller view model and reach the DOM only as an allowlisted node tree | ✓ VERIFIED | partial_render.go: `html/template` parse at boot, `Clone` per request, `html.ParseFragment`, allowlist walk into `[]PartialNode` with 64 KiB / 2000-node / depth-32 caps. The SPA rebuilds nodes with `h()` under the mirrored allowlist (partialNodes.ts). No `v-html`/`innerHTML`/`DOMParser`/`insertAdjacentHTML` anywhere in admin/src (grep empty). `TestPhase101PartialSanitizer` and PartialHost.test.ts (127 tests) pass | +| 4 | Named toolbar actions in `toolbar.buttons`; click POSTs and toasts; create/delete unchanged; unknown YAML keys, missing templates, unregistered actions and unknown permissions fail boot | ✓ VERIFIED | `compileToolbarButtons` (list_schema.go:328-354) rejects unknown names and missing labels. `compileActions` rejects reserved `create`/`delete`. `registry.go:193` validates action permissions. `compilePartials` fails on a missing or unparsable template. The unknown-key case is in `TestPhase101FormExtensionSchema` ("unknown key on a widget"), and the "unknown action permission" and "template missing" cases are in the schema tests. ListView.vue `onAction` posts `/toolbar/{action}`. ListToolbar.test.ts and ListView.test.ts pass | +| 5 | Albums list shows a collection-scoped stats strip; Albums form shows a "Load from Discogs" widget whose stub fills Release year and Format; "Sync with Discogs" toolbar action toasts from its stub | ✓ VERIFIED | fonoteka.go: `headerPartial: stats` plus `_stats.htm`. `statsView` runs three queries, each through `scopeAlbums`, into a curated `albumsStatsView`. fields.yaml `discogs` widget (fill `[year, format]`). The `discogsLookup` stub returns `{year: 1977, format: "LP"}`. `discogsSync` sits in `toolbar.buttons`. `TestPhase101AlbumsExtension` (5 subtests, PostgreSQL, two collections, save persists 1977/LP, 403 for a Genres-only admin) and `TestPhase101AlbumsSmoke` (4 subtests) pass, re-run by the verifier | + +### Plan must-have truths (merged) + +| Plan | Truths | Status | Notes | +|------|--------|--------|-------| +| 10.1-01 (framework Go) | 9 | 9 ✓ VERIFIED | Widget boot rules (extension.go `compileExtension`, `checkWidgetTag` prefix/reserved/pattern, fill = writable scalar, JS required); strict `{record_id, values}` decode with `DisallowUnknownFields` and a trailing-token check; `readScopedRecord` via `FormExtendQuery`, where out of scope is 404; asset headers (nosniff, CSP, CORP same-origin, no-cache, sha256 ETag, `?v=`); toolbar namespace with reserved create/delete and the permission-filtered `toolbarActions` (http.go:542-551); partial boot compile, 500 on caps or model view model; nil record on create; `golang.org/x/net` promoted to direct in go.mod, go.sum unchanged; application-agnostic (no app names in any 10.1-touched framework file); OpenAPI check passes (`--openapi` stage) | +| 10.1-02 (SPA) | 26 | 24 ✓ VERIFIED, 1 ? UNCERTAIN, 1 backstop → human | Verified by code reading plus pluginAssets (30), WidgetField (22), PartialField (5), PartialHost (127), ListToolbar (8), ListView (16) and extension smoke (25) suites, all passing. **UNCERTAIN:** D-14/D-16 truth "stylesheet links of other controllers are disabled". It holds on sequential navigation (tested). Review WR-01 is a confirmed race: `load()` in ListView.vue/FormView.vue calls `loadControllerAssets` after `await` with no unmount or generation guard, so a late response re-activates the previous controller's CSS. **Backstop (S6):** CSP module loading and CSS bleed need a real browser | +| 10.1-03 (Albums) | 11 | 9 ✓ VERIFIED, 2 backstop → human | Stats scoping, curated view model, year and discogs fields, stub messages and fill, toolbar `[create, delete, discogsSync]`, `golem15.fonoteka.access_albums` on both actions, assets embedded (admin.go:12 embed list) and served, plain JS with no network/cookie/storage (read in full), 0/1/many layout and Phase 10 pins updated. **Backstop:** long-text S1 and S3/S4 at 768px need a visual check | +| 10.1-04 (tests and gate) | 6 | 6 ✓ VERIFIED | All 8 named Go tests exist and pass (re-run: 6 cabana, 1 boardwalk, 1 fonoteka; 0 skips in `TestPhase101Actions`). The Vitest suites exist and pass. `check-phase10.1.sh --self-test`, `--hygiene`, `--openapi`, `--evidence` and `--dist` pass (re-run). SECURITY-REVIEW lists T-10.1-01…22 + SC with RC-01…RC-23. VALIDATION has `nyquist_compliant: true` | + +**Score:** 53/57 verified (0 present-but-behavior-unverified; 3 backstop truths routed to human; 1 uncertain) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `modules/pact/capabilities.go` | 6 extension contracts | ✓ VERIFIED | `AdminClientAssets`, `AdminAction`, `AdminActionInput`, `AdminActionResult`, `HasAdminActions`, `AdminPartialData` (lines 138-192) | +| `modules/cabana/extension.go` | boot validation | ✓ VERIFIED | 278 lines; called for every controller; errors name the plugin, controller and file | +| `modules/cabana/actions.go` | widget and toolbar handlers | ✓ VERIFIED | 243 lines; wired at http.go:223/226 inside `requireAjax` | +| `modules/cabana/plugin_assets.go` | exact-allowlist asset handler | ✓ VERIFIED | wired at http.go:254 | +| `modules/cabana/partial_render.go` | template render, sanitizer, caps, handler | ✓ VERIFIED | wired at http.go:229 | +| `admin/openapi/admin.json` | typed ops and schemas | ✓ VERIFIED | `--openapi` stage passes; the conformance test covers the widgets/toolbar/partials routes | +| `admin/src/app/pluginAssets.ts` | loader | ✓ VERIFIED | exports `loadScript`, `loadStyles`, `activateStyles`, `loadControllerAssets` | +| `admin/src/components/form/fields/WidgetField.vue` | custom-element host | ✓ VERIFIED | registered in registry.ts | +| `admin/src/components/partial/PartialHost.vue` / `partialNodes.ts` | partial host and renderer | ✓ VERIFIED | used by ListView.vue:278 and PartialField | +| `modules/boardwalk/dist/index.html` | rebuilt SPA | ✓ VERIFIED | `check-admin-dist`: "matches a fresh build" | +| fonoteka `_stats.htm`, `discogs-lookup.js`, `albums.css`, `albums_admin_controller.go`, `admin_phase101_smoke_test.go`, `admin_phase101_albums_test.go` | Albums surfaces and tests | ✓ VERIFIED | all present, embedded and exercised | +| `scripts/check-phase10.1.sh` | fail-closed gate | ✓ VERIFIED | self-test passes | + +### Key Link Verification + +| From | To | Via | Status | +|------|----|-----|--------| +| cabana/http.go | actions.go | `requireAjax(s.widgetAction)`, `requireAjax(s.toolbarAction)` | ✓ WIRED | +| cabana/extension.go | pact | `pact.HasAdminActions`, `pact.AdminClientAssets`, `pact.AdminPartialData` assertions | ✓ WIRED | +| cabana/plugin_assets.go | boardwalk | `boardwalk.SetSecurityHeaders`, `boardwalk.ContentType` (extension.go) | ✓ WIRED | +| cabana/partial_render.go | x/net/html | `html.ParseFragment` | ✓ WIRED | +| WidgetField.vue | POST …/widgets/{field} | typed `api.POST` | ✓ WIRED | +| PartialHost.vue | GET …/partials/{name} | typed `api.GET` | ✓ WIRED | +| ListView.vue | POST …/toolbar/{action} | `onAction` | ✓ WIRED | +| fonoteka config_list.yaml | `_stats.htm` | `headerPartial: stats` | ✓ WIRED | +| albums_admin_controller.go | scopeAlbums | each of the 3 stats queries | ✓ WIRED | +| fonoteka fields.yaml | discogsLookup | `action: discogsLookup` | ✓ WIRED | +| fonoteka admin.go | assets/js/discogs-lookup.js | `//go:embed` | ✓ WIRED | +| check-phase10.1.sh | check-phase10.sh --hygiene | reused stage | ✓ WIRED (output: "phase10 hygiene passed", then "phase10.1 hygiene passed") | + +### Data-Flow Trace (Level 4) + +| Artifact | Data | Source | Real data | Status | +|----------|------|--------|-----------|--------| +| Albums stats strip | Total, Formats, NoShelf | `scopeAlbums(db.Model(&Album{}))` Count / Group / Count | Yes (PostgreSQL test with two collections) | ✓ FLOWING | +| Discogs widget fill | `{year, format}` | `discogsLookup` stub (intentional; Phase 14, WINDOWS.md entries 6-7) | Stub by design (D-02) | ✓ FLOWING (stub is the spec) | +| Toolbar toast | message | `discogsSync` stub, localized via `translateKey` | Stub by design | ✓ FLOWING | +| Schema asset URLs | `assets.scripts/styles` | `controllerAssets(cc)` from boot-hashed files | Yes | ✓ FLOWING | + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| Framework named tests | `go test ./modules/cabana -run TestPhase101 -count=1 -v` | 6 PASS, 0 SKIP | ✓ PASS | +| Boardwalk and lagoon | `go test ./modules/boardwalk ./modules/lagoon -run 'TestPhase101BoardwalkExports\|TestFillJSONNumber'` | PASS | ✓ PASS | +| Albums acceptance on PostgreSQL | `go test ./plugins/golem15/fonoteka -run 'TestPhase101AlbumsExtension\|TestPhase101AlbumsSmoke' -v` | 9 subtests PASS | ✓ PASS | +| SPA suites | `npx vitest run` (7 Phase 10.1 files) | 233/233 pass | ✓ PASS | +| Gate stages | `check-phase10.1.sh --self-test / --hygiene / --openapi / --evidence / --dist` | all passed | ✓ PASS | +| Full gate | `check-phase10.1.sh --all`, full `go test ./...` in both repos | reported passing by the orchestrator; not re-run (several minutes; the stages above are a subset re-run) | ? SKIP (trusted) | + +### Probe Execution + +No `scripts/*/tests/probe-*.sh` is declared or present for this phase. The phase gate is `scripts/check-phase10.1.sh`, whose stages ran above. + +### Requirements Coverage + +| Requirement | Source plans | Description | Status | Evidence | +|-------------|--------------|-------------|--------|----------| +| ADMIN-07 | 10.1-01, 02, 03, 04 | Runtime admin extension point (assets, widget, partial, toolbar, boot failures) | ✓ SATISFIED (pending the browser checks) | SC 1-5 above. REQUIREMENTS.md maps only ADMIN-07 to Phase 10.1, so no requirement is orphaned | + +### Prohibitions (judgment-tier; non-authoritative LLM verdict, flagged for human review) + +| Plan | Prohibition | Verdict | Evidence | +|------|-------------|---------|----------| +| 01 | No plugin mounts a route under the admin prefix | holds | The fonoteka phase diff adds no route. Actions run only via cabana routes | +| 01 | Asset route never serves AdminFS wholesale | holds | exact-key map lookup; YAML/template GETs refused in tests | +| 01 | No template receives a GORM model, request or safe-marked HTML | holds with caveat | The shipped view models are curated (`albumsStatsView`, fixture). The guard only checks the top-level type and fields, not nested models or methods (WR-03) | +| 01 | No npm package; x/net promoted with no new go.sum module | holds | no commits to package.json/lock/go.sum since 9b98d84; go.mod only drops `// indirect` | +| 02 | No raw-HTML sink or HTML-string parser in admin/src | holds | grep empty; the hygiene stage passes | +| 02 | No network call outside client.ts | holds | only schema.d.ts comments mention XMLHttpRequest | +| 02 | No npm package added or re-pinned | holds | as above | +| 03 | No real Discogs client/job/credential | holds | stubs return constants | +| 03 | No new permission code | holds | admin_permissions.go is untouched in the phase diff | +| 03 | Plugin JS makes no network request and reads no cookie or storage | holds | read in full; the hygiene_101 rule passes | +| 04 | Acceptance not resting on skipped or zero-test runs or a hand-edited dist | holds | 0 skips; dist matches a fresh build | +| 04 | No application names in summercms.go tests or fixtures | holds | grep over the 10.1-touched framework files is empty | +| 04 | No high threat mitigated without a failing-when-removed check | holds | `--evidence` passes; RC rows present | +| 04 | No coverage provider or package added | holds | no package changes | + +### Anti-Patterns Found + +No TBD/FIXME/XXX/TODO/HACK markers in any 10.1-touched file in either repository. The Discogs stubs are intentional (D-02), tracked in `.planning/WINDOWS.md` and scheduled for Phase 14. + +### Code Review Findings Assessment (10.1-REVIEW.md; all 14 still `open`) + +| Finding | Confirmed in code | Defeats an SC or must-have? | Classification | +|---------|-------------------|-----------------------------|----------------| +| CR-01 fractional/overflow number → 500 | Yes. crud.go:327-329 maps any `lagoon.Fill` error to `CapabilityError`, which becomes a 500. `convertNumber` errors on `1977.5`. NumberField sends `Number(raw)` with `inputmode="decimal"` | No. SC5 and the 10.1-03/04 truths require the stub fill (integer 1977) to save, and it does (tested). No truth covers malformed numeric input. This is not a regression: before c3efbc3 every JSON number into an int column was a 500 | ⚠️ WARNING. It is user-facing on a field this phase added, and no later phase covers it. Recommend fixing before close | +| WR-01 late schema re-activates stale CSS | Yes (no alive/generation guard in `load()` of ListView/FormView) | Partially contradicts 10.1-02 truth "stylesheet links of other controllers are disabled" under a race | ⚠️ WARNING. That truth is marked UNCERTAIN; T-10.1-16 is rated low | +| WR-02 CSS stays on non-controller views | Yes (`activateStyles` has no other caller) | No (the truth speaks of other controllers). It contradicts the security-review residual text | ⚠️ WARNING | +| WR-03 shallow view-model guard | Yes (`refusedViewModel` compares only `baseType(vm)`; `carriesTrustedContent` ignores methods) | No. SC3's "only as an allowlisted node tree" still holds (server and client allowlists). It weakens a 10.1-01 prohibition's enforcement, and the SECURITY-REVIEW overstates T-10.1-09 | ⚠️ WARNING | +| WR-04 `isJSONScalar` by Kind | Yes | No (Kind-level scalars hold for every registered action) | ⚠️ WARNING | +| WR-05 widget shown without action permission | Yes (`formSchema` does not filter widget fields) | No (the server refuses with 403; the truth only requires toolbar filtering) | ⚠️ WARNING | +| WR-06 widget route ignores `context` | Yes | No | ⚠️ WARNING | +| IN-01…IN-07 | — | No | ℹ️ Info | + +### Human Verification Required + +1. **Real browser, CSP and custom elements.** Run `summer serve` in fonoteka.go and open /plytadmin > Albumy (pl) on update and create. Expected: no CSP violation, the module script loads, the element upgrades, the widget goes busy, then fills 1977/LP with a toast, and Save persists both across a reload. +2. **768px layout with pl copy.** Expected: the stats strip wraps inside the card with no horizontal scroll and no truncated labels, the widget button grows past 160px, and the toolbar cluster wraps. Sync with Discogs toasts and refetches the strip. +3. **Vite `/assets` dev proxy.** Expected: plugin JS and CSS load through the dev server. +4. **Plugin CSS isolation, including WR-01 and WR-02.** Expected: albums.css is disabled on Gatunki. Decide whether the fast-navigation race and the CSS left on Settings get fixed now or are accepted as low severity. +5. **Code-review triage.** Set CR-01 and WR-01…06 to fixed, deferred or skipped in 10.1-REVIEW-DISPOSITION.md. Recommendation: fix CR-01 now (1977.5 in Release year is a 500 today). +6. **Prohibition review.** Confirm the 14 judgment-tier verdicts above. WR-03 is the only caveat. + +### Gaps Summary + +No success criterion or must-have fails. The extension point exists and is wired end to end in both repositories: + +- **Framework:** cabana-owned widget, toolbar and partial routes, the exact-key asset route, boot validation, the sanitizer and the typed OpenAPI. +- **SPA:** the loader, WidgetField, PartialHost and toolbar actions, with no raw-HTML sink. +- **Application:** the three Albums surfaces. + +The verifier re-ran every named test, and all pass on PostgreSQL with no skips. + +The status is `human_needed` rather than `passed` for three reasons: + +1. Three backstop truths (CSP module loading and CSS bleed, and 768px wrapping twice) need a real browser. +2. One SPA truth (stylesheet isolation) has a confirmed race counterexample (WR-01). +3. Seven confirmed code-review findings (CR-01, WR-01…06) are still `open`. CR-01 is the most important of them: the new Release year field answers malformed numeric input with a 500 instead of a 422. It does not defeat SC5 (the stub fill saves correctly), so it is reported as a developer decision, not a blocker. No later milestone phase covers it, so it cannot be deferred by roadmap. + +--- + +_Verified: 2026-09-29T01:40:00Z_ +_Verifier: Claude (gsd-verifier)_