diff --git a/.planning/phases/17-blog-companions/17-01-SUMMARY.md b/.planning/phases/17-blog-companions/17-01-SUMMARY.md new file mode 100644 index 0000000..b476ff9 --- /dev/null +++ b/.planning/phases/17-blog-companions/17-01-SUMMARY.md @@ -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.