From 0754a6e5ab88b1a4ad3b6fc3599a338490220077 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sat, 10 Oct 2026 02:27:05 +0200 Subject: [PATCH] docs(16.2): revise phase plans after phase 16.1 --- .planning/ROADMAP.md | 12 +- .planning/STATE.md | 4 +- .../16.2-01-PLAN.md | 152 +++++++++---- .../16.2-02-PLAN.md | 53 +++-- .../16.2-03-PLAN.md | 206 +++++++++++++----- .../16.2-04-PLAN.md | 101 ++++++--- .../16.2-05-PLAN.md | 70 +++--- .../16.2-06-PLAN.md | 77 ++++--- .../16.2-VALIDATION.md | 23 +- 9 files changed, 475 insertions(+), 223 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 354246f7..72065ee7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1003,18 +1003,18 @@ Plans: Plans: **Wave 1** -- [ ] 16.2-01-PLAN.md — Framework (summercms.go): pact field-type API (human API checkpoint), cabana registry, boot option checks, Decode/Load/list in the save transaction, untouched skip, FieldWritesFromContext, field-types and field upload routes, OpenAPI and TS types, SPA PluginField/MLPluginField and global loader, dist, READMEs and docs -- [ ] 16.2-02-PLAN.md — Journal Blog Editor element (sm-journal-plugin assets/src to assets/dist): TipTap package check (human), untouched value contract, lossless schema and serializer, uploads and YouTube embeds, JS build, vitest, check-dist, embed -- [ ] 16.2-04-PLAN.md — vue-journal-layer: expandEmbeds after gateJournalHtml, SSR proof, tag v0.2.0 (human push gate), pin in vue-grzybyfunkcjonalne-app +- [ ] 16.2-01-PLAN.md — Framework (summercms.go): pact field-type API (human API checkpoint), cabana registry, boot option checks, Decode/Load/list in the save transaction, Decode notices as meta.notices (D-10 strip report), untouched skip, FieldWritesFromContext, field-types and field upload routes, OpenAPI and TS types, SPA PluginField/MLPluginField, global loader and notice display, dist, READMEs and docs +- [ ] 16.2-02-PLAN.md — Journal Blog Editor element (sm-journal-plugin assets/src to assets/dist): TipTap package check (human), untouched value contract, lossless schema and serializer, policy-safe links, uploads and YouTube embeds, the committed editor-output corpus, JS build, vitest, check-dist, embed **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 16.2-03-PLAN.md — Journal Go integration: blogeditor and mlblogeditor types, CheckHTML per locale, D-07 Load, post and category fields, content_html companion writes without goldmark, editor uploads through the extracted media storage +- [ ] 16.2-03-PLAN.md — Journal Go integration: blogeditor and mlblogeditor types, strip-and-report Decode per locale (SanitizeHTMLReport), D-07 Load, post and category fields (category 422 gate retired), content_html companion writes without goldmark, bluemonday policy extended for the editor's own output (corpus test), editor uploads through the extracted media storage +- [ ] 16.2-04-PLAN.md — vue-journal-layer: sanitize-html mirror extended for the editor's own output (corpus copy), expandEmbeds as the last step after sanitizeJournalHtml, SSR proof, tag v0.2.0 (human push gate), pin in vue-grzybyfunkcjonalne-app **Wave 3** *(blocked on Wave 2 completion)* -- [ ] 16.2-05-PLAN.md — Release gate (human), summercms tag v0.1.17, journal and host bumps, UAT redeploy and on-server smoke, UAT acceptance: untouched re-saves byte-identical, editor edit with upload and embed +- [ ] 16.2-05-PLAN.md — Release gate (human), summercms tag v0.1.18 (next free), journal and host bumps, UAT redeploy and on-server smoke, UAT acceptance: untouched re-saves byte-identical, editor edit with upload and embed, unsafe markup stripped and reported **Wave 4** *(blocked on Wave 3 completion)* -- [ ] 16.2-06-PLAN.md — Unit tests: resave contract at every layer, full Go and JS coverage, Postgres tests for a Docker host, phase gate scripts/check-phase16.2.sh, validation map +- [ ] 16.2-06-PLAN.md — Unit tests: resave contract at every layer, strip-and-report and notices, policy cross-check corpus (both policies, identical copies), category strip, full Go and JS coverage, Postgres tests for a Docker host, phase gate scripts/check-phase16.2.sh, validation map ### Phase 17: Blog companions diff --git a/.planning/STATE.md b/.planning/STATE.md index 8431f0aa..72367d85 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ current_phase: "16.2" current_phase_name: plugin-provided-field-types-and-the-blog-editor-formwidget status: executing stopped_at: Completed 16.1-03-PLAN.md -last_updated: "2026-10-09T21:54:52.956Z" +last_updated: "2026-10-10T00:27:04.316Z" last_activity: 2026-10-09 last_activity_desc: Phase 16.1 execution started -state_head: cd25af08782e69da40515350c21a30aaa229e8c2 +state_head: 94900f233bf34d8dc585d404021c5038c8c621b6 progress: total_phases: 34 completed_phases: 13 diff --git a/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-01-PLAN.md b/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-01-PLAN.md index de181381..46cac29e 100644 --- a/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-01-PLAN.md +++ b/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-01-PLAN.md @@ -21,6 +21,7 @@ files_modified: - modules/cabana/settings.go - modules/cabana/relation_form.go - modules/cabana/extension.go + - modules/cabana/relation_field.go - modules/cabana/admin_openapi.go - modules/cabana/security_coverage_test.go - modules/cabana/README.md @@ -37,6 +38,7 @@ files_modified: - admin/src/components/form/FieldRenderer.vue - admin/src/components/form/PreviewField.vue - admin/src/components/form/FormField.vue + - admin/src/components/form/FormGrid.vue - admin/src/components/form/formState.ts - admin/src/components/form/mlLocale.ts - admin/src/components/form/formContext.ts @@ -44,21 +46,24 @@ files_modified: - admin/src/components/form/fields/MLPluginField.vue - admin/src/components/relation/RelationChildModal.vue - admin/tests/form/PluginField.test.ts + - admin/tests/form/FormField.test.ts + - admin/tests/form/formState.test.ts - modules/boardwalk/dist - docs/backend/field-types.md - docs/backend/forms.md autonomous: false requirements: [D-03, D-06, D-10, D-11, D-12, D-13, D-14, D-15, D-18] estimate: - tokens: 130000 - raw_tokens: 130000 + tokens: 140000 + raw_tokens: 140000 tasks: 4 confidence: low must_haves: truths: - "D-11: a plugin registers a value-bearing admin field type under a short alias (pact.HasFormFieldTypes); boot fails with a message naming both owners when two plugins claim one alias, and fails when an alias equals a cabana core type (text, markdown, mlmarkdown, widget, partial and the rest of formFieldTypes)." - "D-12.1: a plugin-typed field accepts the general field keys plus the type's own Options keys; any other key, and any CheckOptions error, fails boot naming the field and the type." - - "D-12.2 and D-10: a save runs the type's Decode inside the save transaction after the record is loaded, for the host value and for every non-default locale value of a translatable type; a pact.FormFieldError is a 422 validation_failed on that field and nothing is written; any other plugin error is an opaque 500 without the plugin's text." + - "D-12.2 and D-10: a save runs the type's Decode inside the save transaction after the record is loaded, for the host value and for every non-default locale value of a translatable type, and stores the returned pact.FormFieldDecoded Value; a pact.FormFieldError is a 422 validation_failed on that field and nothing is written; any other plugin error is an opaque 500 without the plugin's text." + - "D-10 (framework half, amended 2026-10-10): a Decode may return pact.FormFieldNotice values next to the value it stores (for example what a sanitizer removed); cabana localizes them in the request locale and answers the create or update with meta.notices [{field, locale, message}]; a save whose Decode calls returned no notice answers exactly today's JSON (no notices key); the admin SPA shows each notice under its field and in the saved toast, and reloads the stored value from the response into the form and the element." - "D-12.3: Show and the save response serve each plugin field through the type's Load (per locale for a translatable type)." - "D-12.4: a list column typed with a plugin alias shows the type's ListValue string." - "D-03 (server half): on update, a submitted value equal to the value the form was served for that field and locale is not decoded and not written, neither to the host column nor as a translation." @@ -69,8 +74,11 @@ must_haves: - "D-15: the form schema carries plugin fields generically (element, translatable, typeOptions); admin/openapi/admin.json and admin/src/api/schema.d.ts are regenerated, modules/boardwalk/dist is rebuilt, and the pact and cabana READMEs plus docs/backend/field-types.md describe the API with neutral names." artifacts: - path: "modules/pact/capabilities.go" - provides: "FormFieldType, FormFieldValue, FormFieldUpload, FormFieldUploadResult, FormFieldError, HasFormFieldTypes" + provides: "FormFieldType, FormFieldValue, FormFieldDecoded, FormFieldNotice, FormFieldUpload, FormFieldUploadResult, FormFieldError, HasFormFieldTypes" contains: "HasFormFieldTypes" + - path: "modules/cabana/relation_field.go" + provides: "RecordMeta.Notices and FieldNotice (the save response's meta.notices)" + contains: "notices,omitempty" - path: "modules/cabana/field_type.go" provides: "per-boot field type registry, compile checks, decode/skip/load/list runner" contains: "compileFieldTypes" @@ -98,6 +106,14 @@ must_haves: to: "pact.FormFieldType Decode and Load" via: "decodePluginFields between loadRecord and lagoon.Fill inside the save transaction" pattern: "decodePluginFields" + - from: "modules/cabana/crud.go save" + to: "RecordResult.Meta.Notices (JSON meta.notices)" + via: "localizeNotices over the FormFieldDecoded notices, after the transaction commits" + pattern: "localizeNotices" + - from: "admin/src/views/FormView.vue save" + to: "FormField notice line and the saved toast" + via: "noticeLines(result.data.meta.notices) passed through FormGrid like errors" + pattern: "noticeLines" - from: "admin/src/main.ts and admin/src/views/LoginView.vue" to: "GET {api}/field-types" via: "loadFieldTypes next to loadNavigation" @@ -127,6 +143,12 @@ ROADMAP Goal (not in user-story format): plugins can register their own admin fo This plan's slice (user decision 1, plan 01): the framework half in summercms.go. A plugin registers a value-bearing field type through `pact`; cabana compiles, decodes, loads, lists, uploads and stores it (ML included); the admin SPA hosts its element globally. Framework first: plans 02 and 03 build the Journal editor on this API. +## Revision 2026-10-10 (CONTEXT D-10 amended) + +D-10 now strips instead of refusing, and an admin save must tell the admin what was removed. That report is a generic field-type capability, not Journal code in cabana: `Decode` returns `pact.FormFieldDecoded{Value, Notices}`, cabana localizes the notices and answers the save with `meta.notices`, and the SPA shows them and reloads the stored value. `pact.FormFieldError` (422) stays for values a type cannot accept at all, such as a non-string. The release tag named in Task 1 is now v0.1.18 or the next free v0.1.x, because 16.1 already tagged v0.1.17. Everything else in this plan is unchanged. + +RESEARCH and PATTERNS were written before 16.1 replaced the Journal regex gate with bluemonday and before D-10 was amended. Where they say "CheckHTML refuses" or "no sanitizer", this plan's text wins. + ## Spec-less probe fallback Visible skip: "spec-less probe fallback skipped: phase has no requirement IDs to probe". No SPEC and no edge-probe predicates were generated this run. CONTEXT D-01 to D-19, the three folded todos and RESEARCH Patterns 1-9 are the source of truth. Prohibitions above were recalled in-prompt and are descriptor-less (flagged-unverified). @@ -149,15 +171,15 @@ The locked plan count (6) puts the whole framework half in this plan. It has fou | GOAL | — | Plugins register field types; Journal Blog Editor edits imported HTML losslessly | 01-06 | COVERED | | REQ | — | none mapped (ROADMAP "Requirements: TBD") | — | n/a | | CONTEXT | D-01 | WYSIWYG + source toggle on TipTap | 02 | COVERED | -| CONTEXT | D-02 | HTML in content and content_html on admin save | 03 | COVERED | +| CONTEXT | D-02 | HTML in content and content_html on admin save, both sanitized | 03 | COVERED | | CONTEXT | D-03 | Byte-preserving unless edited | 01 (server skip), 02 (element), 03, 05 (UAT), 06 | COVERED | -| CONTEXT | D-04 | Text, images, tables, embeds, raw HTML block | 02 | COVERED | -| CONTEXT | D-05 | Embed placeholder, CheckHTML not relaxed | 02, 03, 04 | COVERED | +| CONTEXT | D-04 | Text, images, tables, embeds, raw HTML block (raw block goes through the sanitizer) | 02, 03 | COVERED | +| CONTEXT | D-05 | Embed placeholder; both policies extended only for the editor's own output, pinned by a corpus test; iframes still refused | 02 (corpus), 03 (Go policy), 04 (layer mirror), 06 | COVERED | | CONTEXT | D-06 | Translatable like mlmarkdown | 01, 03 | COVERED | | CONTEXT | D-07 | Markdown post opens with content_html | 03 | COVERED | | CONTEXT | D-08 | mlblogeditor replaces mlmarkdown for post content | 03 | COVERED | | CONTEXT | D-09 | Public write API unchanged | 03 | COVERED | -| CONTEXT | D-10 | CheckHTML refuses on every locale, no goldmark on admin path | 01, 03 | COVERED | +| CONTEXT | D-10 | Strip, never refuse, on every locale for posts and categories; the save names what was removed; no goldmark on the admin path | 01 (notices), 03 (Journal strip), 05 (UAT), 06 | COVERED | | CONTEXT | D-11 | Short global alias, collision fails boot | 01 | COVERED | | CONTEXT | D-12 | pact interface: options, decode, load, list | 01 | COVERED | | CONTEXT | D-13 | Element loads globally, value contract, --c-* only | 01, 02 | COVERED | @@ -166,7 +188,7 @@ The locked plan count (6) puts the whole framework half in this plan. It has fou | CONTEXT | D-16 | TipTap in a custom element, plugin JS build, committed dist | 02 | COVERED | | CONTEXT | D-17 | Layer expands embeds, new tag pinned in the app | 04, 05 | COVERED | | CONTEXT | D-18 | Image upload with alt and caption | 01 (route), 02 (UI), 03 (storage) | COVERED | -| CONTEXT | D-19 | Category content uses blogeditor | 03 | COVERED | +| CONTEXT | D-19 | Category content uses blogeditor and is stripped like posts (the 422 gate is retired) | 03, 06 | COVERED | | TODO | — | cabana-plugin-registered-field-types | 01 | COVERED | | TODO | — | cabana-richeditor-field-type | 03 | COVERED | | TODO | — | journal-imported-html-admin-resave (and its test) | 03, 05, 06 | COVERED | @@ -176,30 +198,31 @@ The locked plan count (6) puts the whole framework half in this plan. It has fou | RESEARCH | P4 | Preservation-first schema and serializer | 02 | COVERED | | RESEARCH | P5 | Element contract, global CSS owner (Pitfall 8) | 01, 02 | COVERED | | RESEARCH | P6 | Journal integration, category not translatable | 03 | COVERED | -| RESEARCH | P7 | Embed expansion after the gate | 04 | COVERED | +| RESEARCH | P7 | Embed expansion after the sanitizer (last step at the v-html sink) | 04 | COVERED | | RESEARCH | P8 | Plugin JS build, no eval, injectCSS false, check-dist | 02 | COVERED | | RESEARCH | P9 | Collision-free upload route, OpenAPI + phase09Routes | 01 | COVERED | | RESEARCH | Pitfall 7 | Non-default content_html companion write | 01 (accessor), 03 | COVERED | | RESEARCH | Pitfall 11 | No Docker: DryRun/unit smoke + human Docker-host gate | 05, 06 | COVERED | | RESEARCH | Security | Magic-byte image check on the editor upload | 03 | COVERED | | RESEARCH | OQ3 | Release scope (tags, bumps, UAT) | 04, 05 | COVERED | +| DRIFT | 16.1 | bluemonday policy (`SanitizeHTML`, `FormatTranslation`, the `CheckHTML` predicate (reports any change)), layer sanitize-html mirror, v0.1.17 and layer v0.1.1 already tagged, post 64 re-saved without its style block | 01-06 (revision 2026-10-10) | COVERED | ## Artifacts this phase produces (this plan) -- `pact.FormFieldType`, `pact.FormFieldValue`, `pact.FormFieldUpload`, `pact.FormFieldUploadResult`, `pact.FormFieldError` (with `Error() string`), `pact.HasFormFieldTypes`. -- `cabana.FieldWrite`, `cabana.FieldWritesFromContext`, `cabana.WithFieldWrites`, `cabana.CompileFormWithTypes`, `cabana.FieldTypeAssets`, `cabana.FieldTypesResponse`, `cabana.FieldUploadResponse`. +- `pact.FormFieldType`, `pact.FormFieldValue`, `pact.FormFieldDecoded`, `pact.FormFieldNotice`, `pact.FormFieldUpload`, `pact.FormFieldUploadResult`, `pact.FormFieldError` (with `Error() string`), `pact.HasFormFieldTypes`. +- `cabana.FieldWrite`, `cabana.FieldWritesFromContext`, `cabana.WithFieldWrites`, `cabana.CompileFormWithTypes`, `cabana.FieldTypeAssets`, `cabana.FieldTypesResponse`, `cabana.FieldUploadResponse`, `cabana.FieldNotice`, and the `RecordMeta.Notices` field (JSON `meta.notices`, omitted when empty). - `cabana.FormField` JSON keys `element`, `translatable`, `typeOptions` (Go fields `Element`, `Translatable`, `TypeOptions`). -- Unexported: `fieldTypeRegistry`, `compileFieldTypes`, `readClientAssets`, `liftPluginValues`, `decodePluginFields`, `hydratePluginFields`, `servedFieldValue`, `isMLField`, `fieldUpload`, `fieldTypesList`, `withFieldWrites`. +- Unexported: `fieldTypeRegistry`, `compileFieldTypes`, `readClientAssets`, `liftPluginValues`, `decodePluginFields`, `hydratePluginFields`, `servedFieldValue`, `isMLField`, `fieldUpload`, `fieldTypesList`, `withFieldWrites`, `localizeNotices`. - Routes: `GET {api}/field-types`, `POST {api}/{vendor}/{plugin}/{controller}/{id}/fields/{field}/upload`. - New files: `modules/cabana/field_type.go`, `modules/cabana/field_type_upload.go`, `modules/cabana/field_type_smoke_test.go`, `admin/src/app/fieldTypes.ts`, `admin/src/components/form/fields/PluginField.vue`, `admin/src/components/form/fields/MLPluginField.vue`, `admin/tests/form/PluginField.test.ts`, `docs/backend/field-types.md`. -- SPA exports: `loadFieldTypes`, `loadGlobalStyles`, `GLOBAL_STYLE_OWNER`, `rendererForField`, `isRegisteredField`, `isMLField`, `FIELD_CHANGE_EVENT`, `FIELD_UPLOAD_EVENT`, `FIELD_UPLOAD_RESULT_EVENT`. +- SPA exports: `loadFieldTypes`, `loadGlobalStyles`, `GLOBAL_STYLE_OWNER`, `rendererForField`, `isRegisteredField`, `isMLField`, `noticeLines`, `FIELD_CHANGE_EVENT`, `FIELD_UPLOAD_EVENT`, `FIELD_UPLOAD_RESULT_EVENT`; FormField and FormGrid gain a `notices` prop. - Smoke tests: `TestFieldTypeTracerSmoke`, `TestFieldTypeExpansionSmoke`. -Add the plugin field type API to pact and cabana and host plugin field elements in the admin SPA (D-11 to D-15, D-03 server half, D-06/D-14 storage path, D-10 refusal mapping, D-18 upload route). +Add the plugin field type API to pact and cabana and host plugin field elements in the admin SPA (D-11 to D-15, D-03 server half, D-06/D-14 storage path, D-10 notices and refusal mapping, D-18 upload route). -Purpose: Journal (plans 02 and 03) and later plugins ship their own value-bearing field types without a framework release per type. -Output: the pact types, the cabana registry, compile, save, show, list and upload paths, the two routes, regenerated OpenAPI and TS types, PluginField/MLPluginField and the global loader, rebuilt dist, READMEs and docs. +Purpose: Journal (plans 02 and 03) and later plugins ship their own value-bearing field types without a framework release per type, and a type that changes a value on save (Journal strips unsafe HTML per D-10) can tell the admin what it changed. +Output: the pact types, the cabana registry, compile, save (with notices), show, list and upload paths, the two routes, regenerated OpenAPI and TS types, PluginField/MLPluginField, the global loader and the notice display, rebuilt dist, READMEs and docs. @@ -230,7 +253,7 @@ type FormFieldType struct { Styles []string // AdminFS paths under assets/ (.css), enabled on every admin page Options []string // the type's own fields.yaml keys CheckOptions func(options map[string]any) error `json:"-"` // optional - Decode func(ctx context.Context, in FormFieldValue) (any, error) `json:"-"` // required + Decode func(ctx context.Context, in FormFieldValue) (FormFieldDecoded, error) `json:"-"` // required Load func(ctx context.Context, in FormFieldValue) (any, error) `json:"-"` // optional ListValue func(value any) string `json:"-"` // optional Upload func(ctx context.Context, in FormFieldUpload) (FormFieldUploadResult, error) `json:"-"` // optional @@ -245,6 +268,21 @@ type FormFieldValue struct { Value any // submitted (Decode) or stored (Load); a string for a translatable type Translated func(attribute string) (string, bool, error) // exact read of another attribute of Record in Locale; nil when not translatable or no writer } +// FormFieldDecoded is Decode's answer: Value is stored (a string for a +// translatable type); Notices are non-blocking messages about it, such as what +// a sanitizer removed. No notices means the save answers exactly as before. +type FormFieldDecoded struct { + Value any + Notices []FormFieldNotice +} +// FormFieldNotice is one message for the admin. cabana localizes Message +// (phrase key or text) in the request locale with Params; Count > 0 selects +// the plural form (phrasebook Choice, :count set). +type FormFieldNotice struct { + Message string + Params map[string]string + Count int +} type FormFieldUpload struct { DB *gorm.DB Record any // nil on the create form ({id} 0) @@ -271,8 +309,16 @@ func WithFieldWrites(ctx context.Context, writes map[string][]FieldWrite) contex func CompileFormWithTypes(pluginID string, ctl pact.AdminController, fsys fs.FS, types []pact.FormFieldType) (*FormSchema, error) // FormField gains: Element string `json:"element,omitempty"`, Translatable bool `json:"translatable,omitempty"`, // TypeOptions map[string]any `json:"typeOptions,omitempty"`, fieldType *compiledFieldType `json:"-"` +type FieldNotice struct { + Field string `json:"field"` + Locale string `json:"locale,omitempty"` // set for a translatable type's value + Message string `json:"message"` // already localized +} +// RecordMeta (relation_field.go) gains: Notices []FieldNotice `json:"notices,omitempty"` ``` +Save responses: `POST /{vendor}/{plugin}/{controller}` and `PUT .../{id}` keep `{"data":...,"meta":{"labels":...}}`; when a Decode returned notices, `meta` also carries `"notices":[{"field":"content","locale":"pl","message":"2 elements were removed: style, iframe"}]`. Without notices the JSON is byte-for-byte today's. + Routes (relative to `/api/v1`): - `GET /field-types` → `{"data":[{"name","element","translatable","scripts":["/assets/vendor/plugin/...js?v=<12 hex>"],"styles":[...]}]}`, sorted by name. - `POST /{vendor}/{plugin}/{controller}/{id}/fields/{field}/upload` (multipart, one `file` part; `{id}` 0 = create form) → 201 `{"data":{"url":"..."}}`; 422 `validation_failed` on the field for a FormFieldError; 404 for an unknown field, a field without a plugin type or without Upload, or a record outside the FormExtendQuery scope; 413 over the cap. @@ -291,6 +337,8 @@ Existing code the executor builds on (read, do not re-derive): - `modules/cabana/field_ml.go`: `mlFieldType`, `liftMLValues`, `applyMLTranslations`, `hydrateMLRecord`, `TranslationWriter`. - `modules/cabana/extension.go`: `widgetTagPrefix`, `checkWidgetTag`, `compileClientAssets` (253-300, the `read` closure to extract), `checkAssetPath`, `bootErr`, `pluginAsset`. - `modules/cabana/tx_context.go`: `withVirtualFields`/`VirtualFieldsFromContext` (36-60), the pattern for the writes accessor. +- `modules/cabana/relation_field.go`: `RecordMeta` (91), `RecordEnvelope`, `RecordResult`; `modules/cabana/crud.go`: `localizeForbidden` (the pattern for `localizeNotices`: request locale via `schemaLocale`, `translateKey`), `CRUDService.tr`; `modules/phrasebook/translator.go`: `Get(ctx, key, params)` and `Choice(ctx, key, count, params)`. +- `admin/src/views/FormView.vue`: `adopt` (173, reloads the saved record into the form), `save` (315), the saved toast (337); `admin/src/components/form/FormGrid.vue` (47, `:errors` per field) and `FormField.vue` (the `errors` prop and error line): the path notices follow. - `admin/src/components/form/fields/WidgetField.vue`: element creation, `defined()` with `WIDGET_TIMEOUT`, `api.POST` with `params.path` from the control props, ExtensionFailure. - `admin/src/app/pluginAssets.ts`: `assetAllowed`, `loadScript`, `loadStyles`, `activateStyles` (disables links whose owner is not active: Pitfall 8). @@ -303,9 +351,9 @@ Existing code the executor builds on (read, do not re-derive): modules/pact/capabilities.go (AdminAction, AdminActionInput, HasAdminActions, AdminClientAssets: the style to copy), .planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-CONTEXT.md (D-11 to D-15, Claude's Discretion), .planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-RESEARCH.md (Patterns 1, 2, 5, 9; Code Example 1), this plan's interfaces block Do the automatable part, then stop. Run the verify command (baseline: the contract and schema tests this plan must keep green pass before any change) and confirm `git status --porcelain -- modules admin docs scripts` prints nothing; if it prints anything, report the in-flight files (concurrent session, 16.1 D-02) and stop without editing. -Then print the proposal sheet: the pact types and func signatures exactly as in the interfaces block; which funcs are optional (CheckOptions, Load, ListValue, Upload) and which is required (Decode); the cabana additions (FieldWrite, FieldWritesFromContext, WithFieldWrites, CompileFormWithTypes, the FormField keys element, translatable and typeOptions); the two routes and their answers; the element contract (value property, attributes, summer-change, summer-upload, summer-upload-result), labelled "fixed: shared with plan 02, not open at this checkpoint"; the alias rule (^[a-z][a-z0-9]*$, no core type, no duplicate) and the option-key rule (^[A-Za-z][A-Za-z0-9]*$, no core field key); and the assumptions section of this plan (A5 refusal in settings and relation forms, field-types route, string values for translatable types). Stop for the human. +Then print the proposal sheet: the pact types and func signatures exactly as in the interfaces block, including Decode's FormFieldDecoded answer and FormFieldNotice (the D-10 amendment: a type reports what it changed instead of refusing); which funcs are optional (CheckOptions, Load, ListValue, Upload) and which is required (Decode); the cabana additions (FieldWrite, FieldWritesFromContext, WithFieldWrites, CompileFormWithTypes, FieldNotice, RecordMeta.Notices as meta.notices omitted when empty, the FormField keys element, translatable and typeOptions); the two routes and their answers; the element contract (value property, attributes, summer-change, summer-upload, summer-upload-result), labelled "fixed: shared with plan 02, not open at this checkpoint"; the alias rule (^[a-z][a-z0-9]*$, no core type, no duplicate) and the option-key rule (^[A-Za-z][A-Za-z0-9]*$, no core field key); and the assumptions section of this plan (A5 refusal in settings and relation forms, field-types route, string values for translatable types). Stop for the human. Adopt this pact and cabana field-type API as proposed, or adjust named Go identifiers before they are written? - D-12 rates the pact interface one-way: once summercms is tagged (plan 05, v0.1.17) other repositories compile against these names, and the todo cabana-plugin-registered-field-types expects payment and other plugins to build on it. Before the tag, a rename costs one search-and-replace in this phase; after it, every consuming plugin changes. Journal's Go integration (plan 03) is the first consumer and is written against these exact names. Scope of this checkpoint: pact and cabana Go identifiers and the GET /field-types answer only. The element contract (tag prefix rule, value property, the attributes, summer-change, summer-upload, summer-upload-result) is fixed by this plan per D-13 and is being implemented by plan 02 in parallel, so it is not adjustable here; a human who wants it changed replies "stop" here and at plan 02's package checkpoint, and plans 01 and 02 are re-planned together. + D-12 rates the pact interface one-way: once summercms is tagged (plan 05: v0.1.18, or the next free v0.1.x; v0.1.17 is already the 16.1 security release) other repositories compile against these names, and the todo cabana-plugin-registered-field-types expects payment and other plugins to build on it. Before the tag, a rename costs one search-and-replace in this phase; after it, every consuming plugin changes. Journal's Go integration (plan 03) is the first consumer and is written against these exact names. Scope of this checkpoint: pact and cabana Go identifiers and the GET /field-types answer only. The element contract (tag prefix rule, value property, the attributes, summer-change, summer-upload, summer-upload-result) is fixed by this plan per D-13 and is being implemented by plan 02 in parallel, so it is not adjustable here; a human who wants it changed replies "stop" here and at plan 02's package checkpoint, and plans 01 and 02 are re-planned together.