Files
summercms/.planning/phases/10.1-runtime-admin-extension-point/10.1-VERIFICATION.md
2026-09-29 03:37:38 +02:00

24 KiB
Raw Blame History

phase, verified, status, score, covered_files, covered_digest, behavior_unverified, overrides_applied, human_verification
phase verified status score covered_files covered_digest behavior_unverified overrides_applied human_verification
10.1-runtime-admin-extension-point 2026-09-29T01:40:00Z human_needed 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)
.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
v2:sha256:de162c1152f0f52db5fdedb54f8d2a311c5ee087777027f77771fc41f7dab3ef 0 0
test expected why_human
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. 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. 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 expected why_human
768px layout with the pl copy: narrow the window to 768px on Albumy. 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. Visual layout. Backstop truths 10.1-03 long-text S1 and S3/S4 require a visual check.
test expected why_human
Vite dev proxy: run the admin dev server (`npm --prefix admin run dev`) against a running fonoteka backend and open Albumy. Plugin JS and CSS load through the Vite {prefix}/assets proxy and the widget mounts. Needs two running servers and a browser.
test expected why_human
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). 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). Race and cross-view state are not exercised by any test. The sequential path is tested (pluginAssets.test.ts, extension.smoke.test.ts).
test expected why_human
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. 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. 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 expected why_human
Review the 14 judgment-tier prohibitions in the four plans (table 'Prohibitions' in this report). 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). 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
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)