docs(17-01): complete framework Go plan summary

This commit is contained in:
2026-10-12 15:37:21 +02:00
parent 23f33c3911
commit 000c9aee6e

View File

@@ -0,0 +1,305 @@
---
phase: 17-blog-companions
plan: 01
subsystem: api
tags: [go, cabana, pact, signpost, seaglass, bluemonday, openapi, admin-api, postgres]
requires:
- phase: 16.2
provides: Journal blogeditor (category content stays on it), plugin field type notices plumbing (meta.notices)
- phase: 15
provides: markdown field framework-first pattern (D-11), cabana field type and route conventions
provides:
- pact menu item type contracts (HasMenuItemTypes, MenuItemType, MenuItemReference, MenuItemResolve, ResolvedMenuItem, MenuItemAlternate)
- pact DependentOptionsProvider / DependentOptionSet, AdminSingleton, Option.Depth
- signpost module (menu item type registry, layered URL patterns, ReferenceOptions), activated by party before Boot
- seaglass module (bluemonday v1.0.27 sanitizer with byte-stable SanitizeReport, Sanitize, Check, Removals)
- cabana field types richeditor and repeater, dropdown dependsOn with the dependent-options route and save recheck
- cabana config_reorder.yaml tree reorder (GET/POST reorder) and singleton controllers (GET singleton)
- regenerated admin/openapi/admin.json and admin/src/api/schema.d.ts
affects: [17-02 admin SPA, 17-03 journal types and sitemap, 17-04 menu and faq plugins, 17-05 site integration, 17-06 unit tests]
actuals:
tokens: 61081
tasks: 4
commits: 5
plan_head_before: 9016e2a6fd010997e892b3972f013e7bf2e45fca
plan_head_after: 23f33c3911e90cab12bd2936a662e5bd2be78ede
tech-stack:
added: [github.com/microcosm-cc/bluemonday v1.0.27 (framework go.mod), github.com/aymerick/douceur v0.2.0 (indirect), github.com/gorilla/css v1.0.1 (indirect)]
patterns:
- "Boot-time typed registry published on the app (signpost.Activate after phrasebook/postcard, before Boot), fail-loud with module-prefixed errors"
- "Byte-stable sanitize: return the input when the policy output tokenizes to the same tokens; report removals as meta.notices"
- "Controller-only field features (richeditor, repeater, dependsOn) refused on settings and relation forms so no save path bypasses their checks"
- "Schema-driven exemption from the nested-value refusal (repeater lists only), nestedValue unchanged"
- "Locked exact-set rule for tree writes: 409 reorder_stale on any difference, 422 for structural errors"
key-files:
created:
- modules/signpost/signpost.go
- modules/signpost/README.md
- modules/signpost/signpost_smoke_test.go
- modules/signpost/example_test.go
- modules/seaglass/seaglass.go
- modules/seaglass/README.md
- modules/seaglass/seaglass_smoke_test.go
- modules/cabana/field_dependent.go
- modules/cabana/field_richeditor.go
- modules/cabana/field_repeater.go
- modules/cabana/reorder.go
- modules/cabana/singleton.go
- modules/cabana/companions_smoke_test.go
- modules/cabana/testdata/companions/
- docs/plugins/menu-item-types.md
- .planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md
modified:
- modules/pact/capabilities.go
- modules/party/registry.go
- modules/cabana/form_schema.go
- modules/cabana/crud.go
- modules/cabana/schema_types.go
- modules/cabana/http.go
- modules/cabana/registry.go
- modules/cabana/list_schema.go
- modules/cabana/settings.go
- modules/cabana/relation_form.go
- modules/cabana/field_file.go
- modules/cabana/actions.go
- modules/cabana/contracts.go
- modules/cabana/admin_openapi.go
- modules/cabana/openapi_conformance_test.go
- modules/cabana/security_coverage_test.go
- modules/cabana/token_routes_test.go
- modules/cabana/phase10_coverage_test.go
- modules/cabana/phase10_csrf_test.go
- modules/phrasebook/backend/lang/en/lang.yaml
- modules/phrasebook/backend/lang/pl/lang.yaml
- admin/openapi/admin.json
- admin/src/api/schema.d.ts
- go.mod
- go.sum
- README.md
- docs/backend/forms.md
- docs/backend/lists-and-filters.md
- docs/backend/admin-controllers.md
- docs/backend/settings.md
key-decisions:
- "Registry module signpost, sanitizer module seaglass; host pattern overrides at menu_items.patterns (YAML file only, dashed keys)"
- "List types declare PatternFrom (one level, must name a registered non-list type) so a host override of the record type reaches list children; every type still declares a DefaultPattern"
- "seaglass ports Journal's policy without the four Blog Editor rules and adds tel: links; Removals carries ElementCount/AttributeCount next to the names for plural notices"
- "D-10 Journal clause not executed: Journal category content stays blogeditor (resolved deviation, decision note)"
- "Dependent options for a dropdown inside a repeater row are addressed as {field}=repeater.nested, the same path the provider receives"
- "Dependent sibling values are normalized to strings (nil when empty) for both the options route and the save recheck"
- "Repeater-level required is checked on the rows (at least one row), not through lagoon rules on the Jsonable column"
- "Reorder saves write parent and sort columns with UpdateColumns (no model hooks), so a save hook that renumbers sort values cannot undo the order"
- "Reorder partition = a belongsTo relation field of the form; its options (RelationExtendOptionsQuery scope) are the partitions offered"
patterns-established:
- "Phase 17 cabana smoke fixture acme.companions (testdata/companions) shared by the companions smokes and the OpenAPI conformance env"
- "Docs Go snippets come from example_test.go functions that a test calls directly (docs:sync)"
requirements-completed: [SC-1, SC-2, D-01, D-02, D-03, D-06, D-07, D-08, D-09, D-10, D-11]
coverage:
- id: D1
description: "Typed menu item type registry: HasMenuItemTypes collected at boot by signpost.Activate in party, duplicate/empty/invalid types fail boot, layered patterns incl. PatternFrom, Resolve and ReferenceOptions"
requirement: "SC-2"
verification:
- kind: unit
ref: "modules/signpost/signpost_smoke_test.go#TestSignpostSmoke"
status: pass
- kind: unit
ref: "modules/signpost/example_test.go#ExampleRegistry_Resolve"
status: pass
human_judgment: false
- id: D2
description: "Dependent dropdown (dependsOn) with GET fields/{field}/dependent-options fed by signpost.ReferenceOptions, depths, applies false for a type without references, 401 anonymous"
requirement: "D-08"
verification:
- kind: integration
ref: "modules/cabana/companions_smoke_test.go#TestCompanionsDependentSmoke"
status: pass
human_judgment: false
- id: D3
description: "richeditor field type sanitized through seaglass on every save, clean HTML byte-identical, removals in meta.notices, non-string 422"
requirement: "D-10"
verification:
- kind: unit
ref: "modules/seaglass/seaglass_smoke_test.go#TestSeaglassSmoke"
status: pass
- kind: integration
ref: "modules/cabana/companions_smoke_test.go#TestCompanionsRichEditorSmoke"
status: pass
human_judgment: false
- id: D4
description: "repeater field type: nested keys only, per-row required (rows.1.title), nested richeditor sanitized, nested and top-level dependent recheck (form.dependent_invalid), row bounds"
requirement: "D-07"
verification:
- kind: integration
ref: "modules/cabana/companions_smoke_test.go#TestCompanionsRepeaterSmoke"
status: pass
human_judgment: false
- id: D5
description: "Tree reorder: config_reorder.yaml, GET/POST reorder with partition, both columns written, 409 reorder_stale for omitted or foreign nodes, 422 for cycles"
requirement: "D-09"
verification:
- kind: integration
ref: "modules/cabana/companions_smoke_test.go#TestCompanionsReorderSmoke"
status: pass
human_judgment: false
- id: D6
description: "Singleton controller: GET singleton first-or-create returns a stable id, list/create/delete 404, form schema singleton"
requirement: "D-11"
verification:
- kind: integration
ref: "modules/cabana/companions_smoke_test.go#TestCompanionsSingletonSmoke"
status: pass
human_judgment: false
- id: D7
description: "Every new route has swag annotations, regenerated admin.json/schema.d.ts, inventory entries, token classes, CSRF walk and conformance cases; docs and READMEs pass the docs checker"
requirement: "D-06"
verification:
- kind: unit
ref: "go test ./modules/cabana -run '^(TestPhase09ContractInventory|TestPhase10ConformanceInventory|TestTokenRouteGates|TestPhase09PermissionMatrix|TestPhase10CSRF)$'"
status: pass
- kind: integration
ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance"
status: pass
- kind: other
ref: "bash scripts/check-admin-openapi.sh --check && npm --prefix admin run typecheck && go run ./cmd/summer docs:build --check"
status: pass
human_judgment: false
duration: 41min
completed: 2026-10-12
status: complete
---
# Phase 17 Plan 01: Framework Go for blog companions Summary
**Typed menu item type registry (signpost) with layered URL patterns, a bluemonday-backed sanitizer (seaglass), and cabana richeditor, repeater, dependent dropdown, tree reorder and singleton controllers, all with boot checks, swag/OpenAPI, inventories, docs and Postgres smoke tests**
## Performance
- **Duration:** 41 min
- **Started:** 2026-10-12T12:54:21Z
- **Completed:** 2026-10-12T13:35:49Z
- **Tasks:** 4 (Task 1 tracer, verified end-to-end before expansion)
- **Files modified:** 55 (5462 insertions, 81 deletions)
## Accomplishments
- **pact:** `HasMenuItemTypes`, `MenuItemType` (Key, Label, DefaultPattern, PatternFrom, Placeholders, Nesting, DynamicItems, References, Resolve), `MenuItemReference`, `MenuItemResolve`, `ResolvedMenuItem` (ordered `[]MenuItemAlternate`), `MenuItemAlternate`, `DependentOptionsProvider`, `DependentOptionSet`, `AdminSingleton`; `Option.Depth` (`depth,omitempty`, existing payloads unchanged).
- **signpost (new):** `Activate` (called by party after postcard, before Boot), `Registry` (`Types`, `Type`, `Pattern`, `Resolve`), `ReferenceOptions`, `ConfigKey` = `menu_items.patterns`. Boot errors prefixed `signpost:` for empty/duplicate keys (both plugin IDs named), nil Resolve, empty or relative DefaultPattern, dangling/self/chained PatternFrom, unknown or relative host overrides.
- **seaglass (new):** `Sanitize`, `SanitizeReport`, `Check`, `Removals` over bluemonday v1.0.27 with Journal's byte-stable token comparison.
- **cabana:** `type: richeditor`, `type: repeater` (`cabana.RepeaterForm`), `dependsOn` on dropdowns, `config_reorder.yaml` + `reorder` toolbar button (`cabana.ListReorder`), singleton controllers. New routes: `GET .../fields/{field}/dependent-options`, `GET` and `POST .../reorder`, `GET .../singleton`, with result types `AdminDependentOptionsResult`/`AdminDependentOption`, `AdminReorderTree`/`AdminReorderNode`/`AdminReorderPartition`/`AdminReorderPartitionOption`, `AdminReorderRequest`/`AdminReorderNodeInput`, `AdminSingletonResult`.
- Backend phrase keys (en, pl with CLDR plurals): `richeditor.removed_elements|removed_attributes|removed_comments|markup_changed`, `repeater.max_items|min_items`, `form.dependent_invalid`, `reorder.stale|load_failed`.
- Docs: new `docs/plugins/menu-item-types.md`; sections in `docs/backend/forms.md` (dependent dropdowns, repeater, rich text), `lists-and-filters.md` (tree reorder), `admin-controllers.md` (singleton controllers), `settings.md` (link to a singleton); READMEs for signpost, seaglass, pact, party, cabana and the root table.
## Task Commits
1. **Task 1 (tracer): menu item type registry and dependent dropdown binding** - `0cdff4c` (feat)
2. **Task 2: decision note** - `69bb1cc` (docs, planning doc committed alone before the dependency)
3. **Task 2: richeditor on the seaglass sanitizer** - `71fa597` (feat)
4. **Task 3: repeater and the dependent dropdown save recheck** - `46835fd` (feat)
5. **Task 4: tree reorder and singleton controllers** - `23f33c3` (feat)
**Plan metadata:** recorded in the docs commit that adds this SUMMARY.
## Docker smoke tests
All ran on the local Docker daemon (testcontainers Postgres) and passed: `TestCompanionsDependentSmoke`, `TestCompanionsRichEditorSmoke`, `TestCompanionsRepeaterSmoke`, `TestCompanionsReorderSmoke`, `TestCompanionsSingletonSmoke`, `TestPhase10OpenAPIConformance`. The full `go test ./modules/cabana` (with Docker) passed after Task 4. `go vet ./...` and `go test -short ./...` were green at every task commit; `examples/hello` tests pass with the new dependency.
## Files Created/Modified
- `modules/signpost/*` - registry, README, smoke test, runnable examples used by the docs
- `modules/seaglass/*` - sanitizer, README, smoke test
- `modules/cabana/field_dependent.go` - dependsOn compile, controller-only refusals, dependent-options route, save recheck
- `modules/cabana/field_richeditor.go` - richeditor keys, value check, sanitize and notices
- `modules/cabana/field_repeater.go` - repeater compile, row decode/coerce, nested sanitize, record hydration
- `modules/cabana/reorder.go`, `modules/cabana/singleton.go` - reorder config and routes, singleton route
- `modules/cabana/testdata/companions/` - acme.companions fixture (links: type/reference/description/rows/group; profiles singleton)
- `modules/cabana/companions_smoke_test.go` - fixture plugin, env and the five smoke tests (also mounted in the conformance env)
## Decisions Made
See `key-decisions` in the front matter. In short: module names signpost and seaglass; `menu_items.patterns` read once with compass `LoadSection`, YAML-file only; PatternFrom one level; Removals carries counts; nested dependent options addressed as `repeater.field`; reorder writes via `UpdateColumns` without model hooks; reorder partition is a belongsTo relation field of the form.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Route constraints for the nested dispatcher**
- **Found during:** Task 1 and Task 3
- **Issue:** `fields/{field}/dependent-options` cannot be its own ServeMux pattern next to the shared six-segment GET (it is served through `nestedGet`, like `fields/{field}/options`), and the `name` constraint refused the hyphen of `dependent-options`; the `segment` constraint refused the `repeater.field` path.
- **Fix:** `constrainNested` admits a hyphen in `name` and one dot in `segment`; handlers still resolve names against the compiled schema, so anything else answers 404. Inventory entry uses `mounted: nestedGetRoute`.
- **Files modified:** modules/cabana/http.go, modules/cabana/security_coverage_test.go
- **Committed in:** 0cdff4c, 46835fd
**2. [Rule 2 - Missing critical] seaglass.Removals counts**
- **Found during:** Task 2
- **Issue:** The planned `Removals{Elements, Attributes, Comments, Changed}` keeps at most ten distinct names, so plural notices (`:count elements were removed`) could not be exact.
- **Fix:** Added `ElementCount` and `AttributeCount` (Journal's report has the same fields).
- **Files modified:** modules/seaglass/seaglass.go
- **Committed in:** 71fa597
**3. [Rule 2 - Missing critical] Repeater accepts tab and context**
- **Found during:** Task 3
- **Issue:** The plan's key list for repeater omitted `tab` and `context`; ported Winter forms flatten their tabs into `tab:` keys (RESEARCH Pitfall 9), so FAQ content in a tab would fail boot.
- **Fix:** `tab` and `context` accepted on the repeater field itself (nested fields still refuse them).
- **Files modified:** modules/cabana/field_repeater.go
- **Committed in:** 46835fd
**4. [Rule 3 - Blocking] `prompt` was fileupload-only**
- **Found during:** Task 3
- **Issue:** `compileFileuploadKeys` refused `prompt` on every other type, so the repeater's add-button text failed boot.
- **Fix:** `prompt` allowed on `repeater` (error message updated to "fileupload or repeater"); the repeater compile decodes it.
- **Files modified:** modules/cabana/field_file.go, modules/cabana/field_repeater.go
- **Committed in:** 46835fd
**5. [Rule 3 - Blocking] OpenAPI regenerated in Task 3**
- **Found during:** Task 3
- **Issue:** `FormField` gained `form`, `maxItems`, `minItems`, `titleFrom`, so the committed admin.json and schema.d.ts drifted (Task 3's file list did not name them).
- **Fix:** Ran `scripts/check-admin-openapi.sh`, typecheck passed, committed the regenerated files with Task 3.
- **Files modified:** admin/openapi/admin.json, admin/src/api/schema.d.ts
- **Committed in:** 46835fd
**6. [Rule 3 - Blocking] Docs Go snippets need src= references**
- **Found during:** Task 1
- **Issue:** The docs checker refuses Go code blocks without a `src=` reference to code a test runs.
- **Fix:** `modules/signpost/example_test.go` (runnable examples plus a test that calls the documented methods directly); docs blocks synced with `summer docs:sync`.
- **Files modified:** modules/signpost/example_test.go, docs/plugins/menu-item-types.md, docs/backend/forms.md
- **Committed in:** 0cdff4c
### Resolved deviation recorded by the plan
- **D-10 Journal clause:** Journal category content stays `type: blogeditor` (shipped in 16.2); this plan does not switch it to `richeditor`. Recorded in the decision note `.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md`.
---
**Total deviations:** 6 auto-fixed (2 missing critical, 4 blocking). **Impact:** all additive and needed for the planned contracts to boot, render or be documented; no scope creep, and no existing payload changed (Option.Depth is omitempty; new schema keys are omitempty).
## Issues Encountered
- The fixture login name first used a temp-dir base name, which repeats across tests and hit the unique `backend_users.login`; switched to a nanosecond stamp.
- A reorder smoke assertion reused one GORM struct for two `First` calls (the stale primary key filtered the second); fixed with separate variables.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Plan 02 (admin SPA) can build RepeaterField, RichEditorField, the dependent DropdownField fetch, ReorderView and the singleton form on the regenerated `schema.d.ts`; server-side phrase keys exist, SPA copy keys (UI-SPEC) are still to be added there.
- Plans 03 and 04 can implement `pact.HasMenuItemTypes` (Journal), `pact.DependentOptionsProvider` with `signpost.ReferenceOptions` (menu entries, sitemap rows), `config_reorder.yaml` with a menu partition, and `pact.AdminSingleton` for the sitemap definition.
- No framework tag yet (plan 02 owns the release gate and dist).
---
*Phase: 17-blog-companions*
*Completed: 2026-10-12*
## Self-Check: PASSED
All 16 created key files exist; commits 0cdff4c, 69bb1cc, 71fa597, 46835fd and 23f33c3 are in the log; `commits` (5) is measured from the plan ledger.