24 KiB
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) |
|
v2:sha256:de162c1152f0f52db5fdedb54f8d2a311c5ee087777027f77771fc41f7dab3ef | 0 | 0 |
|
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
- Real browser, CSP and custom elements. Run
summer servein 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. - 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.
- Vite
/assetsdev proxy. Expected: plugin JS and CSS load through the dev server. - 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.
- 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).
- 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:
- Three backstop truths (CSP module loading and CSS bleed, and 768px wrapping twice) need a real browser.
- One SPA truth (stylesheet isolation) has a confirmed race counterexample (WR-01).
- 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)