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 blockDo 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.