diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-VERIFICATION.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-VERIFICATION.md new file mode 100644 index 0000000..da3aa91 --- /dev/null +++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-VERIFICATION.md @@ -0,0 +1,302 @@ +--- +phase: 14.2.1-translate-plugin +verified: 2026-10-06T14:40:00Z +status: human_needed +score: 16/16 must-haves verified +covered_files: + - .planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-02-SUMMARY.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-03-SUMMARY.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-04-PLAN.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-04-SUMMARY.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-05-PLAN.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-05-SUMMARY.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-06-PLAN.md + - .planning/phases/14.2.1-translate-plugin/14.2.1-06-SUMMARY.md + - admin/src/api/schema.d.ts + - admin/src/components/form/fields/MLMarkdownField.vue + - admin/src/components/form/fields/MLTextField.vue + - admin/src/components/form/formContext.ts + - admin/src/components/form/formState.ts + - admin/src/components/form/mlLocale.ts + - admin/src/components/form/registry.ts + - admin/src/components/relation/RelationChildModal.vue + - admin/src/views/FormView.vue + - admin/tests/form/MLFields.test.ts + - admin/tests/form/formState.test.ts + - docs/backend/admin-controllers.md + - docs/backend/forms.md + - modules/cabana/README.md + - modules/cabana/crud.go + - modules/cabana/field_markdown.go + - modules/cabana/field_ml.go + - modules/cabana/form_schema.go + - modules/cabana/http.go + - modules/cabana/ml_smoke_test.go + - modules/cabana/ml_test.go + - modules/cabana/openapi_conformance_test.go + - modules/cabana/relation.go + - modules/cabana/relation_child.go + - modules/cabana/relation_child_ml_test.go + - modules/cabana/schema_types.go + - modules/surf/locale_from_principal.go + - modules/surf/locale_resolver.go + - modules/surf/router.go + - scripts/check-phase14.2.1.sh +covered_digest: "v2:sha256:e8c9c72cd5252f44d6193fa658ea95663432905c4a061fd228887f46a07c4402" +covered_files_note: "verification.fingerprint covers only paths under the summercms.go root. Sibling implementation read this pass: sm-translate-plugin b3e0e26 (plugin.go, Locale/Attribute/Index models, five golem15_translate_* migrations, Translator, Translatable, TranslatedExact, Locales admin, AdminWriter, integration/admin/migration tests); sm-grzybyfunkcjonalne-app 1860ad6 (summer.yaml, plugins.gen.go, go.work, gitlinks, boot_test.go). Host translate gitlink is b3e0e26. PHP pin /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate HEAD 725d547ec839f02b5fdc0f0a6faaed601a414d50, clean tree, not edited." +behavior_unverified: 0 +overrides_applied: 0 +re_verification: + previous_status: gaps_found + previous_score: 14/16 + gaps_closed: + - "Cabana markdown/mltext/mlmarkdown compose; nested locale writes are not dropped" + - "The SPA exposes one locale selector per ML field, switches all ML controls together, supports copy-from-locale, and sends every locale as Record" + gaps_remaining: [] + regressions: [] +advisory: [] +human_verification: + - test: "Boot the proof host and open Locales in the admin SPA" + expected: "Sign in as an administrator holding golem15.translate.manage_locales; English then Polski appear in sort_order; name/enabled can be edited; a user without the permission cannot open the controller" + why_human: "Needs a running host, admin login, and visual confirmation of the Locales screen. Harvested from 14.2.1-04-PLAN.md and 14.2.1-06-PLAN.md human-checks. Automated tests cover HTTP 403/list order, not the SPA chrome." + - test: "With the proof host and admin SPA, open a Journal-shaped form with mltext when one is available" + expected: "Create lists en and pl (not a single English box); one locale selector per ML field; switching one selector switches the others; save/reload still shows both locales" + why_human: "Harvested from 14.2.1-06-PLAN.md. Vitest proves seed/selector/broadcast/copy-from against provided ['en','pl'], not a running SPA against schema meta from the live writer. The proof host has no production mltext YAML (only test-harness fields); do not invent a pass. Relation-child ML UAT can wait for Phase 15 if this host has no child ML form." +--- + +# Phase 14.2.1: Translate plugin Verification Report + +**Phase Goal:** Golem15.Translate is ported to Go as `sm-translate-plugin` so Journal (Phase 15) can keep translatable fields. Lean core only: Locale model and Locales admin, Translatable API, cabana `mltext`/`mlmarkdown`, and PHP Translator locale resolution (URL prefix / session / cookie / default). +**Verified:** 2026-10-06T14:40:00Z +**Status:** human_needed +**Re-verification:** Yes — after gap closure (plans 05 CR-01 and 06 WR-02) + +**MVP note:** ROADMAP does not mark this phase `mode: mvp`. User Flow Coverage is derived from ROADMAP success criteria and CONTEXT D-17. + +Prior report (2026-10-06T12:48:00Z) was `gaps_found` 14/16. CR-01 and WR-02 are closed in source. All 16 must-have truths are now VERIFIED. Status is `human_needed` because harvested SPA UAT remains; it is not a pass. + +## User Flow Coverage + +| Step | Expected | Evidence | Status | +|------|----------|----------|--------| +| Proof host boots user+translate | `party.Activate` loads `golem15.user` then `golem15.translate`; Locales controller and `manage_locales` are registered; submodules are gitlinks | `sm-grzybyfunkcjonalne-app/plugins.gen.go`, `summer.yaml`, gitlink `b3e0e26`; `TestBootUserTranslate` exists | ✓ | +| Operators manage locales | YAML CRUD gated by exactly `golem15.translate.manage_locales`; seed en then pl; no Messages admin | `controllers/locales.go` `RequiredPermissions`; production Permissions() is only `manage_locales` | ✓ API; SPA list/edit is human | +| Fixture saves and reads en+pl through Translatable / WithLocale | Default on host columns; pl in `golem15_translate_attributes`; missing pl falls back | `classes/translatable.go`; `Translated` still does D-11; `TranslatedExact` does not | ✓ API | +| Journal-shaped admin form is one screen with a locale switch | Enabled locales on create/load/save; nested `{en,pl}` posted and rehydrated | `FormMeta.EnabledLocales`; `hydrateMLRecord` + `TranslatedExact`; SPA `FORM_ENABLED_LOCALES`; `TestMLHydration` / Vitest create+GET; live SPA UAT still human | ✓ code; ⏳ SPA UAT | + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +| --- | --- | --- | --- | +| 1 | Plugin repo exists with `golem15_translate_*` schema (locales, attributes, indexes, empty messages table), Locale model, en/pl seed, and context-safe Translator | ✓ VERIFIED | Module `git.golem15.com/golem15/sm-translate-plugin` HEAD `b3e0e26`, ID `golem15.translate`. Five gormigrate files create `golem15_translate_locales\|attributes\|indexes\|messages`. `Locale.TableName()` is `golem15_translate_locales`. `--php` / `--layout` / `--forbidden` PASS this pass. `Translator.Locale` reads `towel.Locale(ctx)` only. | +| 2 | Translatable API (`Translatable()`, `WithLocale`, get/set, default-locale fallback) and permissioned Locales admin work; a fixture model saves and reads en+pl | ✓ VERIFIED | `classes.Translatable` plus `Translated` / `SetTranslated` / `WithLocale`. Locales permission is exactly `golem15.translate.manage_locales`. `Translated` still applies D-11; admin hydration uses `TranslatedExact`. | +| 3 | Cabana `markdown` / `mltext` / `mlmarkdown` compose | ✓ VERIFIED | `formFieldTypes` includes all three (`form_schema.go:29`). `MLMarkdownField.vue` renders `MarkdownField`. `TestMLFieldTypes` PASS this pass (`go test ./modules/cabana -count=1 -run 'TestMLFieldTypes$'`). Registry tests register the three types. | +| 4 | Nested locale writes are not dropped | ✓ VERIFIED | CR-01/WR-02 closed. Host `CRUDService.save` still `liftMLValues` then `applyMLTranslations` then `hydrateMLRecord` (`crud.go:600`, `:688`, `:709`). `CreateChild`/`UpdateChild` lift on `in.Body` **before** `fillChild` (`relation_child.go:168-172`, `:330-334`) and apply after PK (`:183`, `:345`). `ShowChild` hydrates (`:296`). `TestMLNestedSave` PASS; `TestMLHydration` PASS (create/show `{en,pl}`; english-only `pl: ""`); `TestRelationChildMLNestedSave` PASS (create/update/show + `de` 422 with no insert). SPA create seeds `{[code]: ""}`; `mergeMLValue` keeps siblings on a GET string. | +| 5 | Docs, OpenAPI, TS types and `boardwalk` `dist/` update in the same change | ✓ VERIFIED | `docs/backend/forms.md`, `docs/backend/admin-controllers.md`, `modules/cabana/README.md` document `EnabledLocales`, `TranslatedExact`, `hydrateMLRecord`, and child lift. `schema.d.ts` has `enabledLocales?: string[]`. `boardwalk/dist/assets/index-B08bxLq5.js` contains `mltext`/`mlmarkdown`. Gate `--forbidden` found no consuming-application names. | +| 6 | Proof host `sm-grzybyfunkcjonalne-app` boots with user+translate; unit tests are the last plan | ✓ VERIFIED | Host HEAD `1860ad6`. `summer.yaml` lists user then translate; gitlink `plugins/golem15/translate` → `b3e0e26`. Plan 06 is the dedicated named-test/gate close of the gap round; plan 04 remains the original last-of-phase test plan. `--layout` PASS this pass. | +| 7 | D-01/D-02/D-03/D-04: implementation is derived only from the read-only PHP tree at SHA `725d547ec839f02b5fdc0f0a6faaed601a414d50`; no PHP file changes | ✓ VERIFIED | `bash scripts/check-phase14.2.1.sh --php` this pass: `phase14.2.1 php passed (725d547…)`. Porcelain empty. | +| 8 | D-07/D-12: resolver selects URL prefix, valid user preferred_locale, remembered `golem15.translate.locale`, cookie-gated Accept-Language, then default; surf stores only a validated code on context | ✓ VERIFIED | `Translator.Resolve` order still URL → preferred → remembered → AL (unless manual flag) → default (`translator.go:75-103`). Surf `router.go:712-714` writes `towel.WithLocale` from `resolver.Resolve`. | +| 9 | KERN-07: no package-level mutable current locale; `Translator.Locale(ctx)` and `towel.WithLocale` carry request state | ✓ VERIFIED | No package `currentLocale`. `Translator.Locale` reads context then `defaultCode`. `Rewrite` clones the request. | +| 10 | D-15/D-16: module is `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, plugin ID `golem15.translate`, sibling checkout | ✓ VERIFIED | `Plugin.ID()`; remote `git@git.golem15.com:golem15/sm-translate-plugin.git`; path `/media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin`. Host submodule `plugins/golem15/translate` mode `160000`. | +| 11 | D-10: default-locale values remain in host columns; each non-default locale is one JSON object row in `golem15_translate_attributes`; indexed values are mirrored to `golem15_translate_indexes` | ✓ VERIFIED | `SetTranslated` still writes default via host `Update`; non-default upserts one attribute JSON object. `hydrateMLRecord` does not change that write path. | +| 12 | D-05: Locales is ordinary cabana CRUD, permission-gated by exactly `golem15.translate.manage_locales`; no Messages controller, permission, or navigation | ✓ VERIFIED | `RequiredPermissions` returns only `PermissionManageLocales`. `Permissions()` is only `manage_locales`. `--forbidden` PASS (no production `manage_messages`). | +| 13 | Locale form writes only code/name/is_enabled; `is_default` and `sort_order` cannot be mass-assigned; permissioned make-default preserves disabled/default lifecycle guards | ✓ VERIFIED | `Fillable` is `code,name,is_enabled`. WR-01 concurrent two-default race remains untested (advisory, not a gap). | +| 14 | The SPA exposes one locale selector per ML field, switches all ML controls together, supports copy-from-locale, and sends every locale as `Record` | ✓ VERIFIED | CR-01 closed. `MLTextField`/`MLMarkdownField` inject `FORM_ENABLED_LOCALES` and list those codes (`locales` computed is no longer `Object.keys` / `['en']`). `FormView` provides `schemaEnabledLocales(schema.meta)`; `RelationChildModal` re-provides the parent inject. Create `initialValues(fields, enabledLocales)` seeds `{[code]: ""}`. `adopt` uses `mergeMLValue`. `editablePayload` sends `localeRecord`. Vitest this pass: `npx vitest run tests/form/MLFields.test.ts tests/form/formState.test.ts` — 42 passed, including create seed listing en+pl, GET-string merge keeping `pl`, one selector per field, broadcast switch, copy-from, empty-pl payload. Live SPA UAT is still human (below). | +| 15 | Markdown preview uses goldmark without unsafe HTML and cannot execute translated raw HTML/script/event-handler/javascript content | ✓ VERIFIED | `RenderMarkdown` uses `goldmark.New()`; no `html.WithUnsafe`. SPA preview is text interpolation, no `v-html=`. | +| 16 | Nested locale maps are lifted before `ProjectWritableFields` drops maps; default locale fills the host field and non-default locales reach the translate writer inside the host save transaction | ✓ VERIFIED | Host path unchanged (`crud.go:600` then `:688` inside `s.transaction`). Child path now mirrors it on `tx` (`relation_child.go`). `TestMLNestedSave` and `TestRelationChildMLNestedSave` both assert unlifted maps empty at projection, English on host, pl via writer, `de` rejected. | + +**Score:** 16/16 truths verified (0 present, behavior-unverified) + +### Advisory (New Scope, Unevidenced) + +None. WR-01 (makeDefault race), WR-03 (enabled `backend` prefix), and IN-02/IN-03/IN-04 were out of the gap-closure contract, were not in the prior `gaps:` list, and were not re-opened. Evidence gate: not blocking. + +### Required Artifacts + +gsd-tools `verify.artifacts` still does not emit per-file JSON for sibling `../sm-translate-plugin/...` paths. Manual three-level check: + +| Artifact | Expected | Status | Details | +| -------- | -------- | ------ | ------- | +| `../sm-translate-plugin/plugin.go` | Compiled plugin, Resolver + TranslationWriter publish | ✓ EXISTS + SUBSTANTIVE + WIRED | Boot publishes `surf.LocaleResolver` and `cabana.TranslationWriter`. | +| `../sm-translate-plugin/updates/202610060001_create_golem15_translate_locales.go` | Squashed Locale schema | ✓ EXISTS + SUBSTANTIVE | `golem15_translate_locales`. | +| `../sm-translate-plugin/updates/202610060004_create_golem15_translate_messages.go` | Empty compatibility Messages table | ✓ EXISTS + SUBSTANTIVE | Created; no admin. | +| `../sm-translate-plugin/classes/translator.go` | Context-only Translator | ✓ EXISTS + SUBSTANTIVE + WIRED | Published from Boot; surf stores result on context. | +| `../sm-translate-plugin/classes/translatable.go` | Translatable contracts | ✓ EXISTS + SUBSTANTIVE + WIRED | `Translated` (D-11) and `TranslatedExact` (no fallback). | +| `../sm-translate-plugin/classes/admin_writer.go` | Plugin adapter including TranslatedExact | ✓ EXISTS + SUBSTANTIVE + WIRED | `AdminWriter.TranslatedExact` delegates to `classes.TranslatedExact`; `var _ cabana.TranslationWriter`. | +| `../sm-translate-plugin/controllers/locales.go` | Permissioned Locales admin | ✓ EXISTS + SUBSTANTIVE + WIRED | `RequiredPermissions`; registered via `admin.go`. | +| `modules/surf/locale_resolver.go` | Optional resolver contract | ✓ EXISTS + SUBSTANTIVE + WIRED | Used in `router.go`. | +| `modules/cabana/schema_types.go` | `FormMeta.EnabledLocales` | ✓ EXISTS + SUBSTANTIVE + WIRED | `json:"enabledLocales,omitempty"`. | +| `modules/cabana/field_ml.go` | Lift + hydrate + TranslationWriter | ✓ EXISTS + SUBSTANTIVE + WIRED | `hydrateMLRecord` writes `""` when `TranslatedExact` `ok` is false. Wired from CRUD Show/save and relation-child create/update/show. Not used from list projection. | +| `modules/cabana/relation.go` | `RelationService.writer` | ✓ EXISTS + SUBSTANTIVE + WIRED | Same unexported Lookup as `CRUDService`. | +| `modules/cabana/relation_child.go` | lift before fillChild, apply after PK, hydrate | ✓ EXISTS + SUBSTANTIVE + WIRED | WR-02 closed. | +| `modules/cabana/relation_child_ml_test.go` | Named child nested-ML proof | ✓ EXISTS + SUBSTANTIVE | `TestRelationChildMLNestedSave` PASS this pass. | +| `admin/src/components/form/formContext.ts` | `FORM_ENABLED_LOCALES` | ✓ EXISTS + SUBSTANTIVE + WIRED | Injected by `FormView`; re-provided by `RelationChildModal`. | +| `admin/src/components/form/fields/MLMarkdownField.vue` | Locale-aware markdown editor | ✓ EXISTS + SUBSTANTIVE + WIRED | Options from schema inject, not value keys. | +| `admin/src/components/form/fields/MLTextField.vue` | Locale-aware text editor | ✓ EXISTS + SUBSTANTIVE + WIRED | Same. | +| `admin/src/views/FormView.vue` | adopt merges hydrated maps | ✓ EXISTS + SUBSTANTIVE + WIRED | `mergeMLValue`; create seeds via `initialValues`. | +| `docs/backend/forms.md` | Field-type docs | ✓ EXISTS + SUBSTANTIVE | Documents EnabledLocales, exact hydration, child lift. | +| `../sm-grzybyfunkcjonalne-app/summer.yaml` | user+translate plugin list | ✓ EXISTS + SUBSTANTIVE + WIRED | Generates `plugins.gen.go`. | +| `scripts/check-phase14.2.1.sh` | Fail-closed phase gate | ✓ EXISTS + SUBSTANTIVE | `FRAMEWORK_REQUIRE` includes `TestMLHydration` and `TestRelationChildMLNestedSave`. Echoes `Phase 14.2.1 gate passed`. Cheap stages PASS this pass. | +| `.planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md` | ASVS L1 threat evidence | ✓ EXISTS + SUBSTANTIVE | T-14.2.1-19/22/23 mapped to named tests. | + +**Artifacts:** fully verified at exists + substantive + wired. Prior hollow SPA locale options are gone. + +### Key Link Verification + +| From | To | Via | Status | Details | +| ---- | --- | --- | ------ | ------- | +| `plugin.go` Boot | `modules/surf/locale_resolver.go` | `Publish[surf.LocaleResolver]` | ✓ WIRED | Translator implements the interface. | +| `modules/surf/router.go` | `modules/towel` | `towel.WithLocale(resolver.Resolve(...))` | ✓ WIRED | Lines 712-714. | +| `translator.go` | `models/locale.go` | `is_enabled` query in `enabledCodes` | ✓ WIRED | Candidates must match enabled rows. | +| `translatable.go` | `golem15_translate_attributes` | `SetTranslated` upsert JSON | ✓ WIRED | `attribute_data`. | +| `translatable.go` | `golem15_translate_indexes` | indexed upsert | ✓ WIRED | `TranslatableIndexes`. | +| `controllers/locales.go` | `models/locale.go` | cabana CRUD + hooks | ✓ WIRED | `RequiredPermissions`. | +| `modules/cabana/http.go` | `modules/cabana/schema_types.go` | `enabledContentLocales` → `FormMeta.EnabledLocales` | ✓ WIRED | `formSchema` copies codes onto `view.Meta` and envelope; `relationSchema` envelope meta; list schemas do not call the helper. | +| `modules/cabana/crud.go` | `modules/cabana/field_ml.go` | `liftMLValues` / `hydrateMLRecord` | ✓ WIRED | ShowRecord and save. | +| `modules/cabana/http.go` | `modules/cabana/relation.go` | `relations()` `Lookup[TranslationWriter]` | ✓ WIRED | Matches `crud()`. | +| `modules/cabana/relation_child.go` | `modules/cabana/field_ml.go` | `liftMLValues` then `applyMLTranslations` then `hydrateMLRecord` | ✓ WIRED | WR-02. | +| `classes/admin_writer.go` | `classes/translatable.go` | `TranslatedExact` / `SetTranslated` | ✓ WIRED | Exact path never calls `Translated` (D-11). | +| `admin/src/views/FormView.vue` | ML field components | `FORM_ENABLED_LOCALES` | ✓ WIRED | Inject drives selector options. | +| `FormView.adopt` / `RelationChildModal.adopt` | nested ML maps | `mergeMLValue` | ✓ WIRED | Map overlays seed; host string becomes first enabled code and keeps siblings. | +| `summer.yaml` | `plugins.gen.go` | `summer build` blank imports | ✓ WIRED | Both submodule modules. | + +**Wiring:** 14/14 connections verified (prior 5 NOT WIRED links are now WIRED). + +### Data-Flow Trace (Level 4) + +| Artifact | Data Variable | Source | Produces Real Data | Status | +| -------- | ------------- | ------ | ------------------ | ------ | +| Locales list/form | locale rows | GORM `golem15_translate_locales` via cabana CRUD | Yes | ✓ FLOWING | +| Translator.Resolve | request locale | enabled Locale rows + URL/cookie/AL | Yes, validated codes | ✓ FLOWING | +| `Translated` / `SetTranslated` | field text | host column or `attribute_data` JSON | Yes | ✓ FLOWING | +| Cabana ML save (HTTP) | nested locale map | request body → lift → host scalar + writer | Yes | ✓ FLOWING | +| Cabana GET/Show ML field | `data[field]` | `hydrateMLRecord` after `projectFullRecord`: default from host column, others from `TranslatedExact` | Locale map; missing pl is `""` | ✓ FLOWING | +| Relation-child create/update/show | child ML field | same lift/apply/hydrate on `tx` | Yes (`TestRelationChildMLNestedSave`) | ✓ FLOWING | +| Form schema meta | `enabledLocales` | `TranslationWriter.EnabledLocales` | Enabled codes when writer published | ✓ FLOWING | +| SPA ML locale `