docs(14.2.1): create phase plan
This commit is contained in:
227
.planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md
Normal file
227
.planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
phase: 14.2.1-translate-plugin
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- ../sm-translate-plugin/go.mod
|
||||
- ../sm-translate-plugin/plugin.go
|
||||
- ../sm-translate-plugin/README.md
|
||||
- ../sm-translate-plugin/config/config.yaml
|
||||
- ../sm-translate-plugin/models/registry.go
|
||||
- ../sm-translate-plugin/models/locale.go
|
||||
- ../sm-translate-plugin/updates/registry.go
|
||||
- ../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go
|
||||
- ../sm-translate-plugin/updates/202610060002_create_winter_translate_attributes.go
|
||||
- ../sm-translate-plugin/updates/202610060003_create_winter_translate_indexes.go
|
||||
- ../sm-translate-plugin/updates/202610060004_create_winter_translate_messages.go
|
||||
- ../sm-translate-plugin/updates/202610060005_seed_en_pl_locales.go
|
||||
- ../sm-translate-plugin/classes/translator.go
|
||||
- modules/surf/locale_resolver.go
|
||||
- modules/surf/router.go
|
||||
- modules/surf/README.md
|
||||
autonomous: false
|
||||
requirements: [D-01, D-02, D-03, D-04, D-07, D-08, D-10, D-12, D-15, D-16]
|
||||
must_haves:
|
||||
truths:
|
||||
- "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."
|
||||
- "D-10: gormigrate creates final `winter_translate_locales`, `winter_translate_attributes`, `winter_translate_indexes`, and empty `winter_translate_messages`; it creates no rainlab-era or `golem15_translate_*` tables."
|
||||
- "D-08: an idempotent seed creates enabled `en` as default at sort_order 1 and enabled non-default `pl` as Polski at sort_order 2, and never seeds `de`."
|
||||
- "D-07/D-12: a backpack-published resolver selects URL prefix, valid user preferred_locale, remembered `golem15.translate.locale`, cookie-gated Accept-Language, then default, and surf stores only a validated code on context."
|
||||
- "KERN-07: no package-level mutable current locale exists; `Translator.Locale(ctx)` and `towel.WithLocale` carry request state."
|
||||
- "D-15/D-16: the new module is `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, plugin ID `golem15.translate`, in the intended sibling checkout."
|
||||
artifacts:
|
||||
- path: "../sm-translate-plugin/plugin.go"
|
||||
provides: "compiled Plugin registration, migrations/models/config, Resolver publication"
|
||||
contains: "golem15.translate"
|
||||
- path: "../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go"
|
||||
provides: "squashed Locale schema"
|
||||
contains: "winter_translate_locales"
|
||||
- path: "../sm-translate-plugin/updates/202610060004_create_winter_translate_messages.go"
|
||||
provides: "empty compatibility Messages table without admin"
|
||||
contains: "winter_translate_messages"
|
||||
- path: "../sm-translate-plugin/classes/translator.go"
|
||||
provides: "context-only Translator and request Resolver"
|
||||
contains: "func ("
|
||||
- path: "modules/surf/locale_resolver.go"
|
||||
provides: "framework-owned optional resolver contract"
|
||||
contains: "LocaleResolver"
|
||||
key_links:
|
||||
- from: "../sm-translate-plugin/plugin.go"
|
||||
to: "modules/surf/locale_resolver.go"
|
||||
via: "Boot publishes the framework interface into backpack"
|
||||
pattern: "LocaleResolver"
|
||||
- from: "modules/surf/router.go"
|
||||
to: "modules/towel/context.go"
|
||||
via: "validated resolver result is written with towel.WithLocale"
|
||||
pattern: "WithLocale"
|
||||
- from: "../sm-translate-plugin/classes/translator.go"
|
||||
to: "../sm-translate-plugin/models/locale.go"
|
||||
via: "every candidate is checked against enabled Locale rows"
|
||||
pattern: "is_enabled"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the translate plugin repository, exact Winter-compatible schema and seed, Locale model, context-safe Translator, and optional surf resolver seam.
|
||||
|
||||
Purpose: prove one real request locale path from a mounted compiled plugin through Postgres-backed enabled locales into `towel.WithLocale` before adding the broader model/admin surface.
|
||||
Output: a compiling sibling plugin and framework seam, with no new dependencies and no Messages admin.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.codex/gsd-core/workflows/execute-plan.md
|
||||
@~/.codex/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
|
||||
@../fonoteka.go/plugins/golem15/user/plugin.go
|
||||
@../fonoteka.go/plugins/golem15/user/updates/00_base.go
|
||||
@modules/surf/router.go
|
||||
@modules/surf/locale_from_principal.go
|
||||
</context>
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
Phase 14.2.1 has no mapped REQUIREMENTS.md IDs, so requirement probing is visibly skipped. This plan uses D-01/D-02/D-03/D-04/D-07/D-08/D-10/D-12/D-15/D-16 and the RESEARCH validation rows as its acceptance contract.
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Module `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, `Plugin.ID() == "golem15.translate"`.
|
||||
- `models.Locale`, model and migration registries, five gormigrate entries, and exact `winter_translate_*` tables.
|
||||
- `classes.Translator`, `Translator.Locale(context.Context)`, and a request resolver implementing surf's `LocaleResolver`.
|
||||
- Surf optional resolver seam with unchanged fallback behavior when the translate plugin is absent.
|
||||
- Application-neutral plugin and surf README updates for exported contracts.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking-human">
|
||||
<name>Task 1: Create the public Gitea remote and authorize local repository setup</name>
|
||||
<files>../sm-translate-plugin/</files>
|
||||
<read_first>.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md (D-15/D-16 and Remotes), .planning/notes/core-plugins-own-repos.md</read_first>
|
||||
<action>The user creates the public empty Gitea repository `git@git.golem15.com:golem15/sm-translate-plugin.git`, because this account-level operation is the only non-automatable prerequisite. After the user resumes, verify it with `git ls-remote`, then create or clone `/media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin`, initialize `main` as required by the server, and set origin. Also clone the already-existing `sm-grzybyfunkcjonalne-app` remote into `/media/nvme/dev/golem15/summercms.io/summercms/sm-grzybyfunkcjonalne-app`; leave its source layout empty until the D-13 decision gate in Plan 03.</action>
|
||||
<instructions>Create one empty public repository in Gitea with owner `golem15` and name `sm-translate-plugin`; do not add generated README, license, or gitignore. Return here after Gitea displays the SSH URL.</instructions>
|
||||
<verification>The executor runs `git ls-remote`, clones/initializes both local checkouts, and verifies their origin URLs before continuing.</verification>
|
||||
<verify>
|
||||
<automated>git ls-remote git@git.golem15.com:golem15/sm-translate-plugin.git</automated>
|
||||
<fails_when>Non-zero exit, authentication denial, or repository-not-found text.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The remote is public and reachable at the exact D-15 SSH URL.
|
||||
- `git -C ../sm-translate-plugin remote get-url origin` prints `git@git.golem15.com:golem15/sm-translate-plugin.git`.
|
||||
- The local checkout is the D-16 sibling path, not an install/runtime mirror.
|
||||
- `git -C ../sm-grzybyfunkcjonalne-app rev-parse --is-inside-work-tree` prints `true`, and its origin names the already-created proof-host remote; no submodule layout has been committed yet.
|
||||
</acceptance_criteria>
|
||||
<done>The intended tracked plugin repository exists locally and remotely, ready for source commits.</done>
|
||||
<resume-signal>Reply `remote-created` after the empty public repository exists.</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 2: Confirm the one-way Winter table contract</name>
|
||||
<files>../sm-translate-plugin/updates/</files>
|
||||
<read_first>.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md (D-10), .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Tables, columns, indexes), /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/updates/version.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/Locale.php</read_first>
|
||||
<action>Record the selected schema contract before migrations are implemented. Research resolved D-10's provisional prefix against the frozen source: the only compatible selection is the final `winter_translate_*` table family. This gate records the one-way import/storage choice; it does not reopen the PHP pin or permit a JSON-column alternative.</action>
|
||||
<decision>Which persistent table contract should this shared plugin publish?</decision>
|
||||
<context>Once released and imported by Journal, renaming these tables requires coordinated data migrations in every host. The frozen PHP pin explicitly reads `winter_translate_*` after its RainLab rename history.</context>
|
||||
<options>
|
||||
<option id="winter-final">
|
||||
<name>Squash directly to the four final winter_translate_* tables</name>
|
||||
<pros>Matches SHA 725d547, Winter imports, and Journal cutover.</pros>
|
||||
<cons>The names become a shared persistent contract.</cons>
|
||||
</option>
|
||||
<option id="golem15-prefix">
|
||||
<name>Use provisional golem15_translate_* names</name>
|
||||
<pros>Vendor-style naming.</pros>
|
||||
<cons>Contradicts the frozen source and breaks direct imports.</cons>
|
||||
</option>
|
||||
<option id="host-json">
|
||||
<name>Store translations on host-model JSON columns</name>
|
||||
<pros>Fewer tables.</pros>
|
||||
<cons>Contradicts D-10 and PHP storage semantics.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<verify>
|
||||
<automated>test -f /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/Locale.php && git -C /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate rev-parse HEAD</automated>
|
||||
<fails_when>Non-zero exit or the printed SHA is not `725d547ec839f02b5fdc0f0a6faaed601a414d50`.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The response is recorded in the summary.
|
||||
- Task 3 starts only for `winter-final`; either alternative stops execution as a locked-decision conflict.
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Select `winter-final` to implement the locked/researched contract, or select an alternative to stop.</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 3: Resolve one URL-prefixed request through the plugin, enabled Locale row, surf, and context</name>
|
||||
<reversibility rating="one-way">The four table names and columns become the import target; changing them after release requires data migrations in every host.</reversibility>
|
||||
<precondition>Tasks 1 and 2 completed, with schema option `winter-final`.</precondition>
|
||||
<files>../sm-translate-plugin/go.mod, ../sm-translate-plugin/plugin.go, ../sm-translate-plugin/README.md, ../sm-translate-plugin/config/config.yaml, ../sm-translate-plugin/models/registry.go, ../sm-translate-plugin/models/locale.go, ../sm-translate-plugin/updates/registry.go, ../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go, ../sm-translate-plugin/updates/202610060002_create_winter_translate_attributes.go, ../sm-translate-plugin/updates/202610060003_create_winter_translate_indexes.go, ../sm-translate-plugin/updates/202610060004_create_winter_translate_messages.go, ../sm-translate-plugin/updates/202610060005_seed_en_pl_locales.go, ../sm-translate-plugin/classes/translator.go, modules/surf/locale_resolver.go, modules/surf/router.go, modules/surf/README.md</files>
|
||||
<read_first>../fonoteka.go/plugins/golem15/user/go.mod, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/user/models/registry.go, ../fonoteka.go/plugins/golem15/user/updates/registry.go, ../fonoteka.go/plugins/golem15/user/updates/00_base.go, modules/backpack/app.go, modules/surf/router.go, modules/surf/locale_from_principal.go, modules/towel/context.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/Translator.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/LocaleMiddleware.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/ApiLocaleMiddleware.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/config/config.php, .planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md</read_first>
|
||||
<action>Implement the production tracer and the complete Plan 01 scope.
|
||||
|
||||
Create the Go 1.27 module using only existing GORM/gormigrate/framework dependencies. Register the compiled plugin in `init`, expose config/models/migrations, and publish a framework-owned `surf.LocaleResolver` in Boot. Keep `Requires()` empty so user-preference lookup is optional.
|
||||
|
||||
Implement exact squashed DDL for `winter_translate_locales`, `winter_translate_attributes`, `winter_translate_indexes`, and `winter_translate_messages`, including all final columns and PHP-indexed columns from RESEARCH. Messages is DDL only and remains empty. Use explicit gormigrate up/down; never AutoMigrate and never create rainlab or provisional-prefixed tables. Seed only en/pl idempotently with D-08 values.
|
||||
|
||||
Implement `Locale` with no timestamps, table name, code/name rules, and Fillable limited to code/name/is_enabled. Implement Translator/Resolver with the exact D-07 order: enabled URL first segment; valid principal preferred_locale; remembered `golem15.translate.locale`; Accept-Language only when browser detection is enabled and `locale_manually_set` is absent; enabled default. The manual cookie is a flag with value 1, never a locale. Validate every candidate against enabled rows. URL-prefix handling strips only a valid enabled prefix before routing and sets the manual flag using configured expiry. Provide the API resolver variant preferred → Accept-Language → default without persistence. All current-locale access takes context.
|
||||
|
||||
Add the surf interface and optional lookup without importing the plugin. When no resolver is published, retain existing Accept-Language behavior and principal overlay so hosts without translate do not regress. Update surf README for the exported seam and the new plugin README using only neutral host-application examples.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1 && go vet ./modules/surf/... && go test ./modules/surf -short -count=1</automated>
|
||||
<fails_when>Non-zero exit, any package reports build failed, or either test command reports FAIL.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- All four migration files contain their exact `winter_translate_*` table name; no source file contains `golem15_translate_` or `rainlab_translate_`.
|
||||
- Seed source contains en and pl with en default/enabled and pl enabled/non-default, and contains no Deutsch seed.
|
||||
- Plugin module/ID/package match D-15 and plugin Boot publishes the framework resolver interface.
|
||||
- Resolver order is URL → principal → remembered locale → cookie-gated Accept-Language → default.
|
||||
- `locale_manually_set` is treated only as a flag; locale candidates are checked against enabled rows.
|
||||
- There is no mutable package-level request locale and surf writes the selected code with `towel.WithLocale`.
|
||||
- PHP source tree has no diff.
|
||||
</acceptance_criteria>
|
||||
<done>A production URL-prefix request resolves through a mounted compiled plugin to a validated locale on context, while the schema and en/pl rows are ready for later model/admin slices.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| HTTP URL/header/cookies → Translator | Untrusted locale candidates enter request context |
|
||||
| Plugin → surf service registry | Optional compiled plugin supplies a framework interface |
|
||||
| gormigrate → Postgres | Persistent import-compatible schema is created |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-14.2.1-01 | Tampering | Translator locale inputs | high | mitigate | Validate every URL, principal, remembered, and header candidate against enabled Locale rows |
|
||||
| T-14.2.1-02 | Information Disclosure | request locale state | high | mitigate | Context-only locale; no process singleton; concurrent-request test in Plan 04 |
|
||||
| T-14.2.1-03 | Tampering | manual-selection cookie | medium | mitigate | Treat `locale_manually_set` as flag-only and revalidate remembered locale |
|
||||
| T-14.2.1-04 | Tampering | schema source | high | mitigate | Explicit gormigrate DDL from frozen SHA; no AutoMigrate or alternate prefixes |
|
||||
| T-14.2.1-SC | Tampering | package installs | high | mitigate | No new external package installation in this plan |
|
||||
|
||||
ASVS L1: all high threats are blocked by implementation requirements and become fail-closed tests in Plan 04.
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run the Task 3 command. Confirm `git diff -- /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate` is empty and `git ls-files` in the new plugin lists every created source file.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The plugin and surf packages compile and pass short tests.
|
||||
- Exact final tables and en/pl seed are implemented through gormigrate.
|
||||
- Full resolver order is implemented without process-wide locale state.
|
||||
- No deferred Messages UI, CMS components, AI/theme commands, import/export, or PHP edits are introduced.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md` when done.
|
||||
</output>
|
||||
211
.planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md
Normal file
211
.planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
phase: 14.2.1-translate-plugin
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["14.2.1-01"]
|
||||
files_modified:
|
||||
- ../sm-translate-plugin/classes/translatable.go
|
||||
- ../sm-translate-plugin/classes/translatable_smoke_test.go
|
||||
- ../sm-translate-plugin/models/attribute.go
|
||||
- ../sm-translate-plugin/models/index.go
|
||||
- ../sm-translate-plugin/admin.go
|
||||
- ../sm-translate-plugin/admin_permissions.go
|
||||
- ../sm-translate-plugin/admin_navigation.go
|
||||
- ../sm-translate-plugin/controllers/admin_registry.go
|
||||
- ../sm-translate-plugin/controllers/locales.go
|
||||
- ../sm-translate-plugin/controllers/locales/config_list.yaml
|
||||
- ../sm-translate-plugin/controllers/locales/config_form.yaml
|
||||
- ../sm-translate-plugin/models/locale/fields.yaml
|
||||
- ../sm-translate-plugin/models/locale/columns.yaml
|
||||
- ../sm-translate-plugin/lang/en/lang.yaml
|
||||
- ../sm-translate-plugin/lang/pl/lang.yaml
|
||||
- ../sm-translate-plugin/locales_admin_smoke_test.go
|
||||
- ../sm-translate-plugin/README.md
|
||||
autonomous: true
|
||||
requirements: [D-05, D-08, D-09, D-10, D-11, D-12, D-17]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-09/D-12: a host model declares `Translatable() []string`, `TranslatableIndexes() []string`, and `MorphName() string`; callers use exported get/set and `WithLocale` APIs."
|
||||
- "D-10: default-locale values remain in host columns; each non-default locale is one JSON object row in `winter_translate_attributes`, and indexed values are mirrored to `winter_translate_indexes`."
|
||||
- "D-11: a missing non-default value reads the host model's default-locale field."
|
||||
- "D-05: Locales is ordinary cabana CRUD, permission-gated by exactly `golem15.translate.manage_locales`; no Messages controller, permission, or navigation exists."
|
||||
- "Locale form writes only code/name/is_enabled; is_default and sort_order cannot be mass-assigned, while a permissioned make-default action preserves disabled/default lifecycle guards."
|
||||
- "D-17: a neutral fixture saves and reads en/pl and performs an indexed locale lookup through the production APIs."
|
||||
artifacts:
|
||||
- path: "../sm-translate-plugin/classes/translatable.go"
|
||||
provides: "Translatable contracts, WithLocale, Translated, SetTranslated, indexed lookup"
|
||||
contains: "type Translatable interface"
|
||||
- path: "../sm-translate-plugin/controllers/locales.go"
|
||||
provides: "permissioned Locale admin controller and default guards"
|
||||
contains: "golem15.translate.manage_locales"
|
||||
- path: "../sm-translate-plugin/controllers/locales/config_list.yaml"
|
||||
provides: "sort_order asc Locales list"
|
||||
contains: "sort_order"
|
||||
- path: "../sm-translate-plugin/lang/pl/lang.yaml"
|
||||
provides: "Polish Locales admin phrasebook"
|
||||
contains: "locale:"
|
||||
key_links:
|
||||
- from: "../sm-translate-plugin/classes/translatable.go"
|
||||
to: "winter_translate_attributes"
|
||||
via: "non-default SetTranslated upserts one locale/model row"
|
||||
pattern: "attribute_data"
|
||||
- from: "../sm-translate-plugin/classes/translatable.go"
|
||||
to: "winter_translate_indexes"
|
||||
via: "indexed fields upsert locale/model/item/value"
|
||||
pattern: "TranslatableIndexes"
|
||||
- from: "../sm-translate-plugin/controllers/locales.go"
|
||||
to: "../sm-translate-plugin/models/locale.go"
|
||||
via: "cabana CRUD hooks enforce PHP default-locale invariants"
|
||||
pattern: "RequiredPermissions"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Ship the explicit Translatable model API and the permissioned Locales admin, proven by an en/pl fixture through real Postgres storage.
|
||||
|
||||
Purpose: give Journal a stable compiled API and let operators manage enabled/default locales without exposing raw attribute rows or Messages.
|
||||
Output: translatable contracts/helpers/index lookup, Locales YAML/controller/navigation/permissions/phrasebook, smoke fixture.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.codex/gsd-core/workflows/execute-plan.md
|
||||
@~/.codex/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
|
||||
@../fonoteka.go/plugins/golem15/user/admin.go
|
||||
@../fonoteka.go/plugins/golem15/user/controllers/usergroups_admin_controller.go
|
||||
@../fonoteka.go/plugins/golem15/user/admin_harness_test.go
|
||||
</context>
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
No REQUIREMENTS.md IDs are mapped to this phase. Probe predicates are intentionally omitted; D-05/D-08/D-09/D-10/D-11/D-12/D-17 and RESEARCH's validation rows are the visible source contract.
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Exported `classes.Translatable`, optional indexed-field contract, `WithLocale`, `Translated`, `SetTranslated`, and locale-aware indexed lookup.
|
||||
- Internal `models.Attribute` and index record mapping with no public CRUD controller.
|
||||
- `golem15.translate.manage_locales`, Locales navigation/controller, embedded list/form/fields/columns YAML.
|
||||
- English and Polish phrasebook catalogs for plugin/locale admin strings, separate from model translation JSON.
|
||||
- Neutral fixture smoke proving en/pl save/read, fallback, and indexed lookup.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Save one fixture title in en and pl, then read pl through the exported API</name>
|
||||
<files>../sm-translate-plugin/classes/translatable.go, ../sm-translate-plugin/classes/translatable_smoke_test.go, ../sm-translate-plugin/models/attribute.go, ../sm-translate-plugin/models/index.go, ../sm-translate-plugin/models/registry.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/behaviors/TranslatableModel.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/classes/TranslatableBehavior.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/tests/unit/behaviors/TranslatableModelTest.php, ../sm-translate-plugin/models/locale.go, ../sm-translate-plugin/updates/202610060002_create_winter_translate_attributes.go, ../sm-translate-plugin/updates/202610060003_create_winter_translate_indexes.go, ../fonoteka.go/plugins/golem15/user/models/user.go (MorphName), .planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md (Translatable assignment)</read_first>
|
||||
<action>Implement the minimum stable D-09/D-12 API without Eloquent-style magic. Define a host-model contract with `Translatable() []string` and `MorphName() string`, plus an explicit indexed-field contract `TranslatableIndexes() []string`. Reject fields not declared by the model and locale codes not enabled in `winter_translate_locales`.
|
||||
|
||||
`SetTranslated` writes the default locale directly to the host model field/column and writes non-default values to one `(locale, model_id, model_type)` `winter_translate_attributes` row as a JSON object keyed by field. Use explicit model primary-key extraction and explicit exported-field/GORM-column mapping; do not use `reflect.Type.String()` as morph identity. Attribute rows have no public controller.
|
||||
|
||||
`Translated` returns the host field for default locale; for another enabled locale it reads the JSON key and, when absent, returns the default host field per D-11. Keep an explicit locale argument even when context has a UI locale.
|
||||
|
||||
Create a neutral fixture model only in the smoke test, migrate its host table explicitly, seed en/pl through Plan 01 migrations, save English and Polish title values, and read Polish back. This smoke is the production tracer, not the full test matrix reserved for Plan 04.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-translate-plugin test ./classes -count=1 -v -run '^(TestFixtureTranslatableSaveRead)$'</automated>
|
||||
<fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestFixtureTranslatableSaveRead".</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Exported interfaces and functions compile from an external test package.
|
||||
- The default English value is in the fixture host row, not duplicated into `winter_translate_attributes`.
|
||||
- The Polish value is present under `attribute_data.title` in exactly one pl/model row.
|
||||
- Undeclared fields and unenabled locale codes return errors without database writes.
|
||||
- Test fixture is test-only and no production fixture plugin is globally registered.
|
||||
</acceptance_criteria>
|
||||
<done>A neutral model persists its default title on its own row, persists Polish in Winter storage, and reads Polish through the exported API.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add indexed locale lookup, WithLocale query scope, and default fallback</name>
|
||||
<files>../sm-translate-plugin/classes/translatable.go, ../sm-translate-plugin/classes/translatable_smoke_test.go, ../sm-translate-plugin/README.md</files>
|
||||
<read_first>../sm-translate-plugin/classes/translatable.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/behaviors/TranslatableModel.php (scopeTransWhere and index writes), /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/Post.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/Category.php, .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Minimum exported Go API)</read_first>
|
||||
<action>Expand from the proven save/read path. `WithLocale(ctx, db, locale)` validates the explicit locale and carries it on the GORM statement context; it must not mutate global state. Add an indexed equality helper/scope that joins `winter_translate_indexes` by locale, morph name, model id, and item, while default-locale lookup uses the host column and non-default lookup falls back to that host column only when no translated index match exists, matching the frozen PHP behavior.
|
||||
|
||||
When SetTranslated writes a declared indexed field, upsert the corresponding index row atomically with the attribute JSON update. Rewriting one locale/field must preserve other translated fields in the same JSON object. Empty translated values stay explicit empty values rather than silently borrowing fallback during admin editing; ordinary `Translated` still applies D-11 fallback.
|
||||
|
||||
Document only the exported identifiers that exist, MorphName's import-stability requirement, and the default-row/non-default-attribute storage model. Use neutral `blog`/`acme` examples and do not mention a consuming application.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./classes -count=1 -v -run '^(TestFixtureTranslatableSaveRead|TestFixtureTranslatedIndexSmoke)$'</automated>
|
||||
<fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `WithLocale` returns a GORM handle carrying an explicit validated locale on context and has no singleton mutation.
|
||||
- Setting an indexed Polish slug writes one matching `winter_translate_indexes` row.
|
||||
- Polish indexed lookup finds the fixture; default-locale lookup uses the host slug.
|
||||
- Missing Polish title returns English under normal reads per D-11.
|
||||
- README names every exported API identifier accurately and remains application-neutral.
|
||||
</acceptance_criteria>
|
||||
<done>Journal can query and mutate indexed translated slugs and ordinary translated fields through the minimum public contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Manage Locales through permissioned YAML CRUD and separate phrasebook catalogs</name>
|
||||
<files>../sm-translate-plugin/admin.go, ../sm-translate-plugin/admin_permissions.go, ../sm-translate-plugin/admin_navigation.go, ../sm-translate-plugin/controllers/admin_registry.go, ../sm-translate-plugin/controllers/locales.go, ../sm-translate-plugin/controllers/locales/config_list.yaml, ../sm-translate-plugin/controllers/locales/config_form.yaml, ../sm-translate-plugin/models/locale/fields.yaml, ../sm-translate-plugin/models/locale/columns.yaml, ../sm-translate-plugin/lang/en/lang.yaml, ../sm-translate-plugin/lang/pl/lang.yaml, ../sm-translate-plugin/locales_admin_smoke_test.go, ../sm-translate-plugin/README.md</files>
|
||||
<read_first>../fonoteka.go/plugins/golem15/user/admin.go, ../fonoteka.go/plugins/golem15/user/admin_permissions.go, ../fonoteka.go/plugins/golem15/user/admin_navigation.go, ../fonoteka.go/plugins/golem15/user/controllers/admin_registry.go, ../fonoteka.go/plugins/golem15/user/controllers/usergroups_admin_controller.go, ../fonoteka.go/plugins/golem15/user/controllers/usergroups/config_list.yaml, ../fonoteka.go/plugins/golem15/user/admin_harness_test.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/controllers/Locales.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/controllers/locales/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/locale/fields.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/models/locale/columns.yaml</read_first>
|
||||
<action>Implement Locales as `HasAdminControllers` plus `HasNavigation`, never as singleton settings. Register only `golem15.translate.manage_locales` with developer role and require it on controller `golem15.translate.locales`. Do not register manage_messages.
|
||||
|
||||
Embed exact YAML paths. Port name/code/enabled/default form fields and searchable list columns; map unsupported invisible number display safely, keep default sort by sort_order ascending and 20 records per page, and use cabana recordUrl/create/update conventions. Do not invent ReorderController.
|
||||
|
||||
Protect server-owned state: Locale Fillable remains code/name/is_enabled, so request bodies cannot set is_default or sort_order. Controller/model hooks refuse deleting the default, unsetting the default, and making a disabled locale default with cabana validation errors. Provide an explicit `golem15.translate.manage_locales`-permissioned make-default controller action that atomically clears the previous default and sets the selected enabled locale while preserving those guards; keep direct `is_default` form input read-only.
|
||||
|
||||
Port the plugin.* and locale.* UI phrases to `lang/en/lang.yaml` and `lang/pl/lang.yaml` via HasLang. These catalogs are UI phrasebook data and must not read or write attribute JSON. Add smoke coverage for permitted list access and a forbidden request; Plan 04 expands the matrix.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1 -v -run '^(TestLocalesAdminSmoke|TestLocalesAdminForbiddenSmoke)$'</automated>
|
||||
<fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Controller ID is `golem15.translate.locales` and RequiredPermissions contains only `golem15.translate.manage_locales`.
|
||||
- No production source contains `manage_messages`, a Messages controller, ReorderController, or Messages navigation.
|
||||
- List defaults to sort_order asc and returns en before pl from the seed.
|
||||
- Unknown/unprivileged admin gets 403; privileged admin gets the Locales schema/list.
|
||||
- A crafted body cannot persist is_default or sort_order.
|
||||
- The permissioned make-default action rejects disabled locales and atomically leaves exactly one enabled default locale.
|
||||
- English/Polish phrasebook files are independent from `winter_translate_attributes`.
|
||||
</acceptance_criteria>
|
||||
<done>Authorized administrators can manage the lean Locale surface while default-state and phrasebook/model-translation boundaries remain enforced.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Host model → Translatable helpers | Shared plugin trusts only declared fields and explicit morph names |
|
||||
| Admin JSON → Locale CRUD | Untrusted nested/scalar writes cross permission and fillable boundaries |
|
||||
| Translation JSON → Postgres | Field maps and indexes must stay scoped to one model/locale |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-14.2.1-05 | Elevation of Privilege | Locales admin | high | mitigate | RequiredPermissions and 403 smoke; full tests in Plan 04 |
|
||||
| T-14.2.1-06 | Tampering | Locale writes | high | mitigate | Fillable excludes is_default/sort_order and hooks protect default lifecycle |
|
||||
| T-14.2.1-07 | Tampering | SetTranslated | high | mitigate | Allow only declared fields and enabled locale codes; atomic scoped upserts |
|
||||
| T-14.2.1-08 | Information Disclosure | Morph/index queries | medium | mitigate | Explicit MorphName plus model id/locale/item predicates; no broad attribute endpoint |
|
||||
| T-14.2.1-SC | Tampering | package installs | high | mitigate | No new packages |
|
||||
|
||||
ASVS L1: all high threats are mitigated in production paths and receive removal/failure tests in Plan 04.
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run all three task commands, then `go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1`.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Fixture en/pl save/read and indexed lookup pass.
|
||||
- Missing translations fall back to the default host column.
|
||||
- Locales admin is permissioned, YAML-driven, and protects default/server-owned fields.
|
||||
- Messages admin, CMS components, AI/theme commands, import/export, ReorderController, and extra locale seeds remain absent.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-02-SUMMARY.md` when done.
|
||||
</output>
|
||||
242
.planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md
Normal file
242
.planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
phase: 14.2.1-translate-plugin
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["14.2.1-02"]
|
||||
files_modified:
|
||||
- modules/cabana/form_schema.go
|
||||
- modules/cabana/crud.go
|
||||
- modules/cabana/field_ml.go
|
||||
- modules/cabana/field_markdown.go
|
||||
- modules/cabana/ml_smoke_test.go
|
||||
- modules/cabana/README.md
|
||||
- docs/backend/forms.md
|
||||
- docs/backend/admin-controllers.md
|
||||
- admin/src/components/form/registry.ts
|
||||
- admin/src/components/form/formState.ts
|
||||
- admin/src/components/form/fields/MarkdownField.vue
|
||||
- admin/src/components/form/fields/MLTextField.vue
|
||||
- admin/src/components/form/fields/MLMarkdownField.vue
|
||||
- admin/tests/form/registry.test.ts
|
||||
- admin/tests/form/MLFields.test.ts
|
||||
- admin/openapi/admin.json
|
||||
- admin/src/api/schema.d.ts
|
||||
- modules/boardwalk/dist/
|
||||
- ../sm-translate-plugin/classes/admin_writer.go
|
||||
- ../sm-translate-plugin/plugin.go
|
||||
- ../sm-grzybyfunkcjonalne-app/go.mod
|
||||
- ../sm-grzybyfunkcjonalne-app/go.work
|
||||
- ../sm-grzybyfunkcjonalne-app/summer.yaml
|
||||
- ../sm-grzybyfunkcjonalne-app/.gitmodules
|
||||
- ../sm-grzybyfunkcjonalne-app/main.go
|
||||
- ../sm-grzybyfunkcjonalne-app/plugins.gen.go
|
||||
- ../sm-grzybyfunkcjonalne-app/boot_test.go
|
||||
autonomous: false
|
||||
requirements: [D-06, D-09, D-12, D-13, D-14, D-15, D-16, D-17]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-06: cabana accepts `markdown`, `mltext`, and `mlmarkdown`; `mlmarkdown` composes the same markdown primitive with locale switching."
|
||||
- "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."
|
||||
- "The SPA exposes one locale selector per ML field, switches all ML controls together, supports copy-from-locale, and sends every locale as `Record<string,string>`."
|
||||
- "Markdown preview uses goldmark without unsafe HTML and cannot execute translated raw HTML/script/event-handler/javascript content."
|
||||
- "Cabana README, forms/admin docs, OpenAPI, generated TS types, and committed `modules/boardwalk/dist/` are regenerated in the same change."
|
||||
- "D-13/D-14/D-15/D-16/D-17: `sm-grzybyfunkcjonalne-app` mounts user and translate as submodules, uses go.work/local replaces, and boots both compiled plugins."
|
||||
artifacts:
|
||||
- path: "modules/cabana/field_ml.go"
|
||||
provides: "ML nested-value lift and TranslationWriter bridge"
|
||||
contains: "TranslationWriter"
|
||||
- path: "admin/src/components/form/fields/MLMarkdownField.vue"
|
||||
provides: "locale-aware markdown source editor"
|
||||
contains: "modelValue"
|
||||
- path: "docs/backend/forms.md"
|
||||
provides: "verified field-type documentation"
|
||||
contains: "mlmarkdown"
|
||||
- path: "../sm-grzybyfunkcjonalne-app/summer.yaml"
|
||||
provides: "compiled user+translate plugin list"
|
||||
contains: "golem15.translate"
|
||||
- path: "../sm-grzybyfunkcjonalne-app/boot_test.go"
|
||||
provides: "proof-host boot smoke"
|
||||
contains: "TestBootUserTranslate"
|
||||
key_links:
|
||||
- from: "modules/cabana/crud.go"
|
||||
to: "modules/cabana/field_ml.go"
|
||||
via: "ML values are lifted before ProjectWritableFields"
|
||||
pattern: "liftMLValues"
|
||||
- from: "../sm-translate-plugin/classes/admin_writer.go"
|
||||
to: "../sm-translate-plugin/classes/translatable.go"
|
||||
via: "published cabana TranslationWriter delegates each locale to SetTranslated"
|
||||
pattern: "SetTranslated"
|
||||
- from: "../sm-grzybyfunkcjonalne-app/summer.yaml"
|
||||
to: "../sm-grzybyfunkcjonalne-app/plugins.gen.go"
|
||||
via: "summer build generates blank imports for both submodules"
|
||||
pattern: "sm-translate-plugin"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add cabana's markdown and multilingual fields end to end, preserve nested locale writes, regenerate every public/generated artifact, and boot the user+translate proof host.
|
||||
|
||||
Purpose: prove a Journal-shaped form can submit localized text/markdown through the generic admin stack without a Node extension or dropped nested JSON.
|
||||
Output: framework schema/save/UI/docs/OpenAPI/dist changes, translate writer adapter, and the D-13 proof-host layout.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.codex/gsd-core/workflows/execute-plan.md
|
||||
@~/.codex/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-02-SUMMARY.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
|
||||
@modules/cabana/form_schema.go
|
||||
@modules/cabana/crud.go
|
||||
@modules/cabana/field_permission.go
|
||||
@admin/src/components/form/registry.ts
|
||||
@admin/src/components/form/formState.ts
|
||||
@../sm-bm-app/summer.yaml
|
||||
@../sm-bm-app/go.work
|
||||
</context>
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
The phase has no mapped requirement IDs, so no speculative requirement probes are generated. D-06/D-09/D-12/D-13/D-14/D-15/D-16/D-17 and RESEARCH's cabana/host validation rows are the acceptance source.
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Cabana field types `markdown`, `mltext`, `mlmarkdown`, `TranslationWriter`, nested-map lifting, and safe `RenderMarkdown`.
|
||||
- Vue `MarkdownField`, `MLTextField`, `MLMarkdownField`, synchronized selector/copy behavior, and registry/form-state support.
|
||||
- Updated cabana README and backend docs; regenerated `admin/openapi/admin.json`, `admin/src/api/schema.d.ts`, and `modules/boardwalk/dist/`.
|
||||
- Translate plugin's cabana writer adapter published at Boot.
|
||||
- Proof host source layout with user and translate submodules, local workspace/replaces, generated plugin list, and `TestBootUserTranslate`.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Submit one mltext map through cabana and persist default plus Polish values</name>
|
||||
<files>modules/cabana/form_schema.go, modules/cabana/crud.go, modules/cabana/field_ml.go, modules/cabana/field_markdown.go, modules/cabana/ml_smoke_test.go, admin/src/components/form/registry.ts, admin/src/components/form/formState.ts, admin/src/components/form/fields/MarkdownField.vue, admin/src/components/form/fields/MLTextField.vue, admin/src/components/form/fields/MLMarkdownField.vue, admin/tests/form/MLFields.test.ts, ../sm-translate-plugin/classes/admin_writer.go, ../sm-translate-plugin/plugin.go</files>
|
||||
<read_first>modules/cabana/form_schema.go (formFieldTypes and compileFieldNode), modules/cabana/crud.go (save, ProjectWritableFields, BindWritableFields, scalarFormField), modules/cabana/field_permission.go (liftPermissionValues), modules/postcard/templates.go (goldmark safe path), admin/src/components/form/registry.ts, admin/src/components/form/formState.ts, admin/src/components/form/fields/TextField.vue, admin/src/components/form/fields/TextareaField.vue, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/traits/MLControl.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/traits/mlcontrol/partials/_locale_selector.htm, ../sm-translate-plugin/classes/translatable.go</read_first>
|
||||
<action>Register exactly `markdown`, `mltext`, and `mlmarkdown` as fail-loud schema types without adding unnecessary YAML keys. Define a framework-owned cabana `TranslationWriter` service contract so cabana never imports the translate plugin; implement and publish its adapter from sm-translate-plugin.
|
||||
|
||||
In CRUD save, identify declared ML fields and lift each `map[locale]text` before `ProjectWritableFields` evaluates nested values. Reject non-string values, undeclared locales, and extra field names with validation errors. Fill only the default-locale scalar into the host model, then call the writer for non-default values inside the same lagoon/cabana transaction and after controller permission/query scoping has succeeded. Never expose a public translate-write endpoint.
|
||||
|
||||
Implement `RenderMarkdown` using the already-pinned goldmark without unsafe HTML; reject unsafe output patterns consistently with postcard. `MarkdownField` edits source and may preview only sanitized output. Build ML components around a `Record<string,string>` value: active locale editor, selector, copy-from-locale, and a shared form event so changing one selector changes all ML controls. `mlmarkdown` composes `MarkdownField`, not a duplicate parser/editor. Ensure formState sends nested ML records.
|
||||
|
||||
Create one focused Go smoke fixture using the Plan 02 writer adapter and a cabana save body `{title:{en,pl}}`. It must prove the nested map survives projection, English reaches the host field, and Polish reaches attribute JSON. Add a focused SPA smoke asserting registry resolution and emitted nested values; full edge/coverage tests remain Plan 04.</action>
|
||||
<verify>
|
||||
<automated>go test ./modules/cabana -count=1 -v -run '^(TestMLNestedSaveSmoke)$' && npm --prefix admin test -- --run admin/tests/form/MLFields.test.ts</automated>
|
||||
<fails_when>Non-zero exit; Go output contains "--- FAIL", "--- SKIP", or "no tests to run", lacks "--- PASS: TestMLNestedSaveSmoke", or Vitest reports no test files/tests or any failed test.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- A compiled schema accepts all three exact field types and still rejects unknown types.
|
||||
- ML map lifting occurs before `ProjectWritableFields`; generic nested maps remain rejected/dropped by existing rules.
|
||||
- Default locale fills the host scalar and is not duplicated in attributes; Polish persists through TranslationWriter.
|
||||
- Writer execution occurs only after host-controller authorization/scoping and inside the save transaction.
|
||||
- Raw script/iframe/event-handler/javascript markdown cannot become executable preview HTML.
|
||||
- SPA payload contains all locale keys, and `mlmarkdown` reuses `MarkdownField`.
|
||||
</acceptance_criteria>
|
||||
<done>A real cabana save carries one multilingual title from Vue-shaped JSON through projection and transactional persistence without dropping or broadening the write.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 2: Confirm the one-way proof-host submodule layout</name>
|
||||
<files>../sm-grzybyfunkcjonalne-app/.gitmodules, ../sm-grzybyfunkcjonalne-app/go.work, ../sm-grzybyfunkcjonalne-app/go.mod, ../sm-grzybyfunkcjonalne-app/summer.yaml</files>
|
||||
<read_first>.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md (D-13 through D-17), .planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md (Proof host), ../sm-bm-app/.gitmodules, ../sm-bm-app/go.work, ../sm-bm-app/go.mod, ../sm-bm-app/summer.yaml</read_first>
|
||||
<action>Record the D-13 host layout before adding gitlinks. The selected contract is the existing `sm-grzybyfunkcjonalne-app` repository with `plugins/golem15/user` and `plugins/golem15/translate` submodules plus go.work and local replace entries, following sm-bm-app. Journal is deliberately absent until Phase 15.</action>
|
||||
<decision>Which durable proof-host layout should receive the translate gitlink?</decision>
|
||||
<context>Changing submodule paths after downstream clones and Phase 15 Journal mounts requires coordinated gitlink, workspace, replace, and deployment changes.</context>
|
||||
<options>
|
||||
<option id="grzyby-submodules">
|
||||
<name>Use sm-grzybyfunkcjonalne-app with plugins/golem15/user and plugins/golem15/translate</name>
|
||||
<pros>Matches locked D-13/D-16 and the established sm-bm-app pattern.</pros>
|
||||
<cons>The submodule paths become durable host layout.</cons>
|
||||
</option>
|
||||
<option id="fonoteka-host">
|
||||
<name>Use fonoteka.go as the proof host</name>
|
||||
<pros>Already has a large application harness.</pros>
|
||||
<cons>Contradicts D-13 and couples the shared plugin to the wrong app.</cons>
|
||||
</option>
|
||||
<option id="vendored-copy">
|
||||
<name>Copy plugin source into the host</name>
|
||||
<pros>No gitlinks.</pros>
|
||||
<cons>Contradicts compiled core-plugin repository ownership and creates a fork.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<verify>
|
||||
<automated>git -C ../sm-grzybyfunkcjonalne-app rev-parse --is-inside-work-tree && git -C ../sm-grzybyfunkcjonalne-app remote get-url origin</automated>
|
||||
<fails_when>Non-zero exit or origin is not the existing sm-grzybyfunkcjonalne-app remote.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The selection is recorded in the summary.
|
||||
- Task 3 proceeds only with `grzyby-submodules`; another selection stops as a D-13/D-16 conflict.
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Select `grzyby-submodules` to implement the locked host, or an alternative to stop.</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Complete docs/OpenAPI/TS/dist and boot the user+translate proof host</name>
|
||||
<reversibility rating="one-way">The committed user/translate gitlink paths become the Phase 15 host layout; moving them later requires coordinated submodule and workspace migration.</reversibility>
|
||||
<precondition>Task 2 selected `grzyby-submodules`, and the Plan 02 translate commit is reachable from its Gitea remote.</precondition>
|
||||
<files>modules/cabana/README.md, docs/backend/forms.md, docs/backend/admin-controllers.md, admin/tests/form/registry.test.ts, admin/tests/form/MLFields.test.ts, admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/boardwalk/dist/, ../sm-grzybyfunkcjonalne-app/go.mod, ../sm-grzybyfunkcjonalne-app/go.work, ../sm-grzybyfunkcjonalne-app/summer.yaml, ../sm-grzybyfunkcjonalne-app/.gitmodules, ../sm-grzybyfunkcjonalne-app/main.go, ../sm-grzybyfunkcjonalne-app/plugins.gen.go, ../sm-grzybyfunkcjonalne-app/boot_test.go</files>
|
||||
<read_first>modules/cabana/README.md, docs/backend/forms.md, docs/backend/admin-controllers.md, modules/cabana/openapi_conformance_test.go, admin/package.json, admin/tests/form/registry.test.ts, ../sm-bm-app/go.mod, ../sm-bm-app/go.work, ../sm-bm-app/summer.yaml, ../sm-bm-app/.gitmodules, ../sm-bm-app/main.go, ../sm-bm-app/plugins.gen.go, .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Docs/OpenAPI/TS/dist and Proof-host layout)</read_first>
|
||||
<action>Update cabana README and backend forms documentation for `markdown`, `mltext`, `mlmarkdown`, nested locale payloads, TranslationWriter, safe markdown, and fail-loud behavior. Update admin-controller docs only where activation/save semantics changed. All examples use neutral blog/acme names. Remove the stale claim that markdown is unavailable.
|
||||
|
||||
Regenerate admin OpenAPI from the established command, regenerate TypeScript schema with `npm --prefix admin run gen:api`, run the admin build, and copy the resulting Vite output to committed `modules/boardwalk/dist/` using the existing boardwalk build workflow. Do not hand-edit generated JSON, d.ts, or dist assets. Extend registry tests for all three field types.
|
||||
|
||||
In the already-cloned D-14 host, use the sm-bm-app layout: module `git.golem15.com/golem15/sm-grzybyfunkcjonalne-app`, framework replace `../summercms.go`, submodules at `plugins/golem15/user` and `plugins/golem15/translate`, workspace uses and host replaces for each, and summer.yaml entries for only `golem15.user` and `golem15.translate`. Add submodules from their Gitea remotes, never copy runtime mirrors. Generate main.go/plugins.gen.go with `summer build`. Add `TestBootUserTranslate` that activates the real two plugins and verifies the Locales admin controller is registered; the fixture save/read proof remains the plugin integration smoke from Plans 02/03 rather than a third production plugin.</action>
|
||||
<verify>
|
||||
<automated>go test ./cmd/summer -count=1 -run 'TestDocsTree' && go run ./cmd/summer docs:build --check && npm --prefix admin run typecheck && npm --prefix admin test -- --run admin/tests/form/registry.test.ts admin/tests/form/MLFields.test.ts && npm --prefix admin run build && go -C ../sm-grzybyfunkcjonalne-app vet ./... && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslate)$'</automated>
|
||||
<fails_when>Non-zero exit; docs checker reports stale identifiers/links/forbidden names; Vitest reports no tests or failures; build omits index.html/assets; host output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestBootUserTranslate".</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- README/docs identifiers resolve and contain no consuming-application name.
|
||||
- OpenAPI, schema.d.ts, and boardwalk dist are regenerated and tracked; no generated file is hand-edited.
|
||||
- Registry resolves markdown/mltext/mlmarkdown and unknown ml types still fail boot.
|
||||
- Host `.gitmodules`, `go.work`, `go.mod`, and `summer.yaml` use exact D-16 paths/module IDs.
|
||||
- Generated plugin list imports sm-user-plugin and sm-translate-plugin; Journal is absent.
|
||||
- Host smoke activates both plugins and sees the Locales admin controller.
|
||||
</acceptance_criteria>
|
||||
<done>The framework ships typed multilingual controls and the intended host boots user+translate from durable submodules with generated admin artifacts in sync.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Admin browser → cabana CRUD | Untrusted nested multilingual JSON crosses authenticated controller boundary |
|
||||
| Translated markdown → preview DOM | Stored content can carry active HTML payloads |
|
||||
| Cabana → plugin TranslationWriter | Framework delegates scoped writes to an optional plugin service |
|
||||
| Host gitlinks → compiled binary | Remote plugin commits become trusted build inputs |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-14.2.1-09 | Tampering | ML nested request | high | mitigate | Lift only declared ML fields; require string locale map; validate locales; execute after host authorization |
|
||||
| T-14.2.1-10 | Elevation of Privilege | TranslationWriter | high | mitigate | No standalone endpoint; writer runs inside permissioned/scoped host save transaction |
|
||||
| T-14.2.1-11 | Tampering | mass assignment | high | mitigate | Default scalar only enters writable host field; locale map never reaches generic Fill |
|
||||
| T-14.2.1-12 | Tampering | markdown preview | high | mitigate | Goldmark safe mode plus unsafe output rejection and CSP-compatible rendering |
|
||||
| T-14.2.1-13 | Tampering | host submodule provenance | medium | mitigate | Exact Gitea remotes and committed gitlinks; generated plugin imports |
|
||||
| T-14.2.1-SC | Tampering | npm packages | high | mitigate | No package installation or version change; existing exact pins only |
|
||||
|
||||
ASVS L1: all high threats are mitigated; Plan 04 adds fail-when-broken evidence.
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run the Task 3 combined gate. Confirm `git status --short` in all three repositories contains only intended tracked source/generated artifacts and gitlink updates.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- ML nested saves persist both locales without widening mass assignment.
|
||||
- Markdown and multilingual controls compile, test, and appear in generated API/types/dist.
|
||||
- Docs checks pass with application-neutral examples.
|
||||
- Proof host boots real user+translate plugins and exposes Locales admin.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-03-SUMMARY.md` when done.
|
||||
</output>
|
||||
246
.planning/phases/14.2.1-translate-plugin/14.2.1-04-PLAN.md
Normal file
246
.planning/phases/14.2.1-translate-plugin/14.2.1-04-PLAN.md
Normal file
@@ -0,0 +1,246 @@
|
||||
---
|
||||
phase: 14.2.1-translate-plugin
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["14.2.1-03"]
|
||||
files_modified:
|
||||
- ../sm-translate-plugin/updates/postgres_test.go
|
||||
- ../sm-translate-plugin/updates/migrations_test.go
|
||||
- ../sm-translate-plugin/classes/translator_test.go
|
||||
- ../sm-translate-plugin/classes/translatable_test.go
|
||||
- ../sm-translate-plugin/admin_harness_test.go
|
||||
- ../sm-translate-plugin/locales_admin_test.go
|
||||
- ../sm-translate-plugin/integration_test.go
|
||||
- modules/surf/locale_resolver_test.go
|
||||
- modules/cabana/ml_test.go
|
||||
- modules/cabana/markdown_test.go
|
||||
- modules/cabana/openapi_conformance_test.go
|
||||
- admin/tests/form/MLFields.test.ts
|
||||
- admin/tests/form/MarkdownField.test.ts
|
||||
- ../sm-grzybyfunkcjonalne-app/boot_test.go
|
||||
- scripts/check-phase14.2.1.sh
|
||||
- .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md
|
||||
- .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md
|
||||
autonomous: true
|
||||
requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11, D-12, D-13, D-14, D-15, D-16, D-17]
|
||||
must_haves:
|
||||
truths:
|
||||
- "A real-Postgres integration test migrates all four winter_translate tables, activates user+translate+fixture, saves en/pl through cabana ML fields, reads via WithLocale, and reaches Locales admin."
|
||||
- "Migration up/down verifies every final column/index, empty Messages table, idempotent en/pl seed, absence of de, and complete rollback."
|
||||
- "Translator tests prove URL → preferred_locale → remembered locale → cookie-gated Accept-Language → default, invalid-code rejection, API order, plugin-absent surf behavior, and concurrent request isolation."
|
||||
- "Translatable tests prove default-row storage, non-default JSON, default fallback, indexed lookup, undeclared-field rejection, invalid-locale rejection, and atomic preservation of sibling fields."
|
||||
- "Admin/ML tests prove manage_locales authorization, default-locale lifecycle guards, mass-assignment resistance, nested-map preservation, synchronized locale controls, and stored-XSS rejection."
|
||||
- "All three repositories pass vet/tests; docs/OpenAPI/types/dist consistency checks pass; a security review closes every high threat."
|
||||
artifacts:
|
||||
- path: "../sm-translate-plugin/integration_test.go"
|
||||
provides: "full production-path integration proof"
|
||||
contains: "TestTranslateEndToEnd"
|
||||
- path: "../sm-translate-plugin/updates/migrations_test.go"
|
||||
provides: "real Postgres migration up/down and seed proof"
|
||||
contains: "winter_translate_messages"
|
||||
- path: "modules/surf/locale_resolver_test.go"
|
||||
provides: "resolver-present/absent and request-isolation tests"
|
||||
contains: "TestLocaleResolver"
|
||||
- path: "modules/cabana/ml_test.go"
|
||||
provides: "nested ML write and mass-assignment tests"
|
||||
contains: "TestML"
|
||||
- path: "scripts/check-phase14.2.1.sh"
|
||||
provides: "fail-closed phase gate"
|
||||
contains: "sm-translate-plugin"
|
||||
- path: ".planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md"
|
||||
provides: "ASVS L1 threat evidence"
|
||||
contains: "T-14.2.1-01"
|
||||
key_links:
|
||||
- from: "../sm-translate-plugin/integration_test.go"
|
||||
to: "modules/cabana/field_ml.go"
|
||||
via: "fixture admin save uses real TranslationWriter"
|
||||
pattern: "TestTranslateEndToEnd"
|
||||
- from: "scripts/check-phase14.2.1.sh"
|
||||
to: "../sm-translate-plugin/updates/migrations_test.go"
|
||||
via: "full plugin test suite runs with Docker/Postgres, not -short"
|
||||
pattern: "go -C"
|
||||
- from: ".planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md"
|
||||
to: "modules/cabana/ml_test.go"
|
||||
via: "each mitigated high threat cites executed failure evidence"
|
||||
pattern: "T-14.2.1"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Finish Phase 14.2.1 with the dedicated unit/integration/security test plan and one fail-closed gate across plugin, framework, admin SPA, and proof host.
|
||||
|
||||
Purpose: make every locked decision and high-risk locale/admin write behavior observably fail when broken.
|
||||
Output: full test matrix, migration rollback proof, phase gate, security review, and validated validation map.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.codex/gsd-core/workflows/execute-plan.md
|
||||
@~/.codex/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-01-SUMMARY.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-SUMMARY.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md
|
||||
@.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
|
||||
@../fonoteka.go/plugins/golem15/user/updates/postgres_test.go
|
||||
@../fonoteka.go/plugins/golem15/user/admin_harness_test.go
|
||||
@scripts/check-phase14.sh
|
||||
</context>
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
The phase has no mapped REQUIREMENTS.md IDs. This is a visible, intentional skip of spec-less probes. Tests and gate rows map directly to D-01 through D-17, the frozen PHP pin, and the RESEARCH validation/threat tables.
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `TestTranslateEndToEnd` spanning migration, activation, resolver, permissioned admin, nested ML write, en/pl storage, fallback, and indexed query.
|
||||
- Real-Postgres migration/seed/rollback suite.
|
||||
- Translator, surf, Translatable, Locales admin, cabana ML/markdown, SPA, generated-contract, and proof-host tests.
|
||||
- `scripts/check-phase14.2.1.sh` with short/full/docs/admin/host/security stages.
|
||||
- `14.2.1-SECURITY-REVIEW.md` and a completed `14.2.1-VALIDATION.md`.
|
||||
|
||||
## Multi-source coverage audit
|
||||
|
||||
| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| GOAL | — | Lean Translate core lets Journal keep translatable fields and boots in proof host | 01-04 | COVERED | Schema, API, admin, ML fields, host, tests |
|
||||
| REQ | — | No mapped requirement IDs | — | COVERED | Visible spec-less fallback; D-IDs are used |
|
||||
| CONTEXT | D-01..D-04 | Frozen/read-only PHP SHA | 01,04 | COVERED | Pin asserted; PHP tree unchanged |
|
||||
| CONTEXT | D-05 | Locale/admin/behavior lean scope; deferred surfaces absent | 02,04 | COVERED | Negative scope checks in gate |
|
||||
| CONTEXT | D-06 | markdown/mltext/mlmarkdown compose | 03,04 | COVERED | Go + SPA + generated artifacts |
|
||||
| CONTEXT | D-07 | full request locale resolution | 01,04 | COVERED | Ordered and concurrent tests |
|
||||
| CONTEXT | D-08 | en/pl only | 01,04 | COVERED | Seed/absence tests |
|
||||
| CONTEXT | D-09..D-12 | explicit Translatable APIs/storage/fallback | 02-04 | COVERED | Fixture and full matrix |
|
||||
| CONTEXT | D-13..D-17 | proof host, remotes, submodules, boot/proof | 01,03,04 | COVERED | Host smoke and layout checks |
|
||||
| RESEARCH | — | Exact four final tables/columns/indexes and migrations up/down | 01,04 | COVERED | Real Postgres |
|
||||
| RESEARCH | — | Permission, default-locale guards, phrasebook separation | 02,04 | COVERED | Admin suite |
|
||||
| RESEARCH | — | Nested ML map before projection, safe markdown | 03,04 | COVERED | Security tests |
|
||||
| RESEARCH | — | README/docs/OpenAPI/TS/dist same change | 03,04 | COVERED | Consistency gate |
|
||||
| RESEARCH | — | No external SaaS API integration | 01-04 | COVERED | No COVERAGE matrix or fabricated API tests |
|
||||
|
||||
Excluded by explicit scope: Messages catalogue/admin, CMS locale components, locale picker/hreflang/banner, AI/theme commands, message import/export, extra locale seeds, PHP edits, and Phase 15 Journal implementation.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Prove migration → activation → resolver → Locales admin → ML save → WithLocale read end to end</name>
|
||||
<files>../sm-translate-plugin/updates/postgres_test.go, ../sm-translate-plugin/integration_test.go, ../sm-translate-plugin/admin_harness_test.go, ../sm-grzybyfunkcjonalne-app/boot_test.go</files>
|
||||
<read_first>../fonoteka.go/plugins/golem15/user/updates/postgres_test.go, ../fonoteka.go/plugins/golem15/user/admin_harness_test.go, ../sm-translate-plugin/plugin.go, ../sm-translate-plugin/classes/translator.go, ../sm-translate-plugin/classes/translatable.go, ../sm-translate-plugin/controllers/locales.go, modules/cabana/ml_smoke_test.go, ../sm-grzybyfunkcjonalne-app/boot_test.go, .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md</read_first>
|
||||
<action>Build the final integration harness on testcontainers Postgres, copying the proven user-plugin fail-closed TestMain/dedicated database pattern. Docker unavailability is a failure for full runs; only an explicit `-short` invocation may skip integration.
|
||||
|
||||
`TestTranslateEndToEnd` must migrate user and translate plugin sets in dependency order, activate the real user and translate plugins plus a test-only neutral fixture controller/model, assemble surf/cabana, and create backend principals with and without `golem15.translate.manage_locales`. Assert unauthorized Locales access is 403 and authorized schema/list sees en then pl. Send one real fixture create/update body with mltext title and mlmarkdown body maps for en/pl. Assert host columns contain English, only the Polish non-default attribute row exists, safe markdown source round-trips, `WithLocale(...,"pl")` reads Polish, a missing Polish field falls back to English, and indexed Polish slug lookup finds only the fixture.
|
||||
|
||||
Exercise a request with `/pl/...` and conflicting preferred/session/header candidates to prove URL precedence reaches `towel.Locale(ctx) == "pl"`. Keep all fixture plugin/model registration process-local to the test.
|
||||
|
||||
Expand host `TestBootUserTranslate` to prove both actual gitlink plugins activate, migrations are discoverable, and the Locales controller/permission are registered. It need not duplicate the fixture model.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-translate-plugin test ./... -count=1 -v -run '^(TestTranslateEndToEnd)$' && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslate)$'</automated>
|
||||
<fails_when>Non-zero exit; either run prints "--- FAIL", "--- SKIP", "no tests to run", container startup failure treated as skip, or lacks its named "--- PASS" line.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Integration uses real Postgres and actual gormigrate/party/surf/cabana production paths.
|
||||
- Unauthorized Locales is 403; authorized list includes only seeded en/pl in order.
|
||||
- One nested admin save persists English on the host and Polish in attributes/indexes.
|
||||
- WithLocale Polish read, default fallback, and indexed lookup all pass.
|
||||
- URL prefix wins over all conflicting candidates and locale is context-only.
|
||||
- Fixture registration cannot appear in the production plugin list or host binary.
|
||||
</acceptance_criteria>
|
||||
<done>The entire Phase 14.2.1 user-visible path is proven through production wiring on real Postgres before horizontal test expansion.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Complete migration, Translator, Translatable, admin, ML, markdown, and SPA test matrices</name>
|
||||
<files>../sm-translate-plugin/updates/migrations_test.go, ../sm-translate-plugin/classes/translator_test.go, ../sm-translate-plugin/classes/translatable_test.go, ../sm-translate-plugin/locales_admin_test.go, modules/surf/locale_resolver_test.go, modules/cabana/ml_test.go, modules/cabana/markdown_test.go, modules/cabana/openapi_conformance_test.go, admin/tests/form/MLFields.test.ts, admin/tests/form/MarkdownField.test.ts</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/translate/tests/unit/behaviors/TranslatableModelTest.php, ../sm-translate-plugin/integration_test.go, ../sm-translate-plugin/updates/202610060001_create_winter_translate_locales.go, ../sm-translate-plugin/classes/translator.go, ../sm-translate-plugin/classes/translatable.go, modules/surf/locale_from_principal_test.go, modules/cabana/form_schema_test.go, modules/cabana/field_permission.go, admin/tests/form/DatepickerField.test.ts, admin/tests/form/registry.test.ts</read_first>
|
||||
<action>Complete the decision and security matrix without adding unrelated PHP-suite surfaces.
|
||||
|
||||
Migration tests: assert every final table, column type/default, PHP-indexed column, empty Messages row count, no rainlab/provisional tables, idempotent seed, en/pl exact flags/order/names, no de, and rollback removes all four tables in reverse-safe order. Re-migrate after rollback.
|
||||
|
||||
Translator tests: table-drive URL prefix precedence and stripping; preferred locale; remembered locale; absent/present manual flag behavior; weighted Accept-Language reduced only to enabled codes; default fallback; invalid URL/session/header ignored; API resolver preferred → header → default without persistence; plugin-absent surf retains old behavior. Run parallel requests with conflicting locales under `-race` and assert no cross-request leak.
|
||||
|
||||
Translatable tests: default/non-default storage, D-11 fallback, explicit empty translation, invalid locale and undeclared field no-write, sibling JSON field preservation, indexed upsert/update and morph/model isolation, WithLocale context isolation, transaction rollback. Admin tests: exact permission 403/allowed, is_default/sort_order mass-assignment rejection, delete/unset/disabled-default guards, no manage_messages surface, phrasebook keys in en/pl.
|
||||
|
||||
Cabana tests: three types compile and unknown `mlunknown` fails; non-ML nested values remain blocked; ML values lift before projection; malformed/extra locale keys fail; writer failure rolls back host write; writer is not invoked before permission/query checks. Markdown tests include script, iframe, event attributes, javascript/vbscript/data schemes. SPA tests cover selector synchronization, per-locale editing, copy, complete nested payload, and markdown composition without raw-HTML sinks. Extend OpenAPI conformance for all types.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-translate-plugin test ./... -count=1 -race && go test ./modules/surf ./modules/cabana -count=1 -race && npm --prefix admin test -- --run admin/tests/form/registry.test.ts admin/tests/form/MLFields.test.ts admin/tests/form/MarkdownField.test.ts</automated>
|
||||
<fails_when>Non-zero exit; Go race detector reports a race; any package/test reports FAIL; integration tests unexpectedly SKIP in the plugin run; or Vitest reports no tests or failures.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Every D-01..D-17 behavior assigned to code has at least one named assertion or gate check.
|
||||
- Migrate, rollback, and re-migrate are proven against real Postgres.
|
||||
- Parallel locale tests pass under `-race` with no shared current-locale state.
|
||||
- High threats T-14.2.1-01/02/04/05/06/07/09/10/11/12 have concrete fail-when-broken tests.
|
||||
- No tests require Messages admin, CMS components, AI/theme commands, import/export, or extra locale seeds.
|
||||
</acceptance_criteria>
|
||||
<done>All production branches introduced by Plans 01-03 have focused unit or integration evidence, including race, rollback, authorization, mass-assignment, and XSS cases.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Build the fail-closed phase gate, security review, and validation sign-off</name>
|
||||
<files>scripts/check-phase14.2.1.sh, .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md, .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md</files>
|
||||
<read_first>scripts/check-phase14.sh, scripts/check-phase12.2.sh, .planning/phases/14.2.1-translate-plugin/14.2.1-VALIDATION.md, .planning/phases/14.2.1-translate-plugin/14.2.1-RESEARCH.md (Security Domain and Validation Architecture), .planning/phases/14.2.1-translate-plugin/14.2.1-01-PLAN.md (T-14.2.1-01..04), .planning/phases/14.2.1-translate-plugin/14.2.1-02-PLAN.md (T-14.2.1-05..08), .planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md (T-14.2.1-09..13)</read_first>
|
||||
<action>Create `scripts/check-phase14.2.1.sh` with explicit stages: frozen PHP SHA and no PHP diff; tracked-source/module/layout checks; plugin vet/full tests/race; framework cabana+surf vet/tests/race; docs tree and docs build check; admin typecheck/tests/build plus generated OpenAPI/schema/dist cleanliness; proof-host vet/test/build; forbidden-scope scan; security evidence. Use `go -C <repo>` exactly for sibling repositories. Full mode must run Postgres integration and fail if Docker is unavailable; do not treat `-short` as final evidence. Detect zero-test filters by requiring named PASS lines where a filter is used.
|
||||
|
||||
Run the requested security-review lane over the local Phase 14.2.1 changes. Produce `14.2.1-SECURITY-REVIEW.md` at ASVS L1, preserving unique threat IDs T-14.2.1-01 through T-14.2.1-18. For every high threat, cite the exact source control and executed named test; status must be mitigated or the phase gate remains red. Review locale injection, manage_locales privilege, nested ML JSON, stored XSS, mass assignment, process-wide leakage, and submodule provenance. Do not fabricate an external-API matrix: state `No external API integration: this phase ports a compiled plugin and local framework/host contracts only.`
|
||||
|
||||
Update VALIDATION frontmatter to validated/nyquist compliant/wave 0 complete only after all mapped commands pass. Replace pending rows with exact test names and threat references, record the proof-host/manual Locales SPA check as end-of-phase UAT, and run the phase gate once in full.</action>
|
||||
<verify>
|
||||
<automated>bash scripts/check-phase14.2.1.sh --all</automated>
|
||||
<fails_when>Non-zero exit; any stage is absent/skipped; output contains FAIL, a Go race, no-tests-to-run, unexpected SKIP, stale generated artifacts, forbidden deferred surface, unmitigated high threat, or lacks the final `Phase 14.2.1 gate passed` line.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Gate uses `go -C ../sm-translate-plugin` and `go -C ../sm-grzybyfunkcjonalne-app`; it does not invent a nested application directory.
|
||||
- Plugin, cabana, surf, admin, docs, generated artifacts, and proof host all have explicit fail-closed stages.
|
||||
- Security review contains every T-14.2.1-NN exactly once and blocks on all high findings.
|
||||
- Validation rows name existing tests/commands and frontmatter is marked validated/nyquist compliant only after green execution.
|
||||
- Gate confirms no Messages admin/manage_messages, CMS locale components, AI/theme commands, import/export, de seed, runtime plugin loading, AutoMigrate, or PHP modifications.
|
||||
</acceptance_criteria>
|
||||
<done>The full three-repository phase gate is green, all high threats are mitigated with executed evidence, and validation is signed off.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Test/gate → production claims | A no-op, skipped container, or stale generated artifact must not pass |
|
||||
| Concurrent requests → context locale | Conflicting request locales must stay isolated |
|
||||
| Security review → phase completion | High findings block completion |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-14.2.1-14 | Repudiation | phase gate | high | mitigate | Require named PASS/final marker; reject no-tests and unexpected skips |
|
||||
| T-14.2.1-15 | Information Disclosure | concurrent Translator | high | mitigate | Race-enabled conflicting-locale test asserts context isolation |
|
||||
| T-14.2.1-16 | Tampering | migration rollback | medium | mitigate | Up/down/re-up on dedicated Postgres with exact schema assertions |
|
||||
| T-14.2.1-17 | Tampering | generated admin artifacts | medium | mitigate | Regenerate and require clean OpenAPI/TS/dist diff after build |
|
||||
| T-14.2.1-18 | Elevation of Privilege | test fixture | high | mitigate | Fixture plugin is process-local/test-only and absent from generated production imports |
|
||||
| T-14.2.1-SC | Tampering | package installs | high | mitigate | No dependency installation; existing exact pins and lockfile only |
|
||||
|
||||
ASVS L1: phase completion is blocked on every high finding.
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
`bash scripts/check-phase14.2.1.sh --all` is the authoritative final command and must end with `Phase 14.2.1 gate passed`.
|
||||
|
||||
<human-check>
|
||||
With the executor-started proof host and admin SPA, sign in as an administrator holding `golem15.translate.manage_locales`; open Locales, confirm English and Polski appear in order, edit the permitted name/enabled fields, and verify a user without the permission cannot open the controller.
|
||||
</human-check>
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Full unit, integration, race, migration rollback, SPA, docs, generated-artifact, and host gates pass.
|
||||
- Every locked D-01..D-17 decision is covered.
|
||||
- Every high STRIDE threat is mitigated with source and named-test evidence.
|
||||
- No deferred surface or new external dependency entered the phase.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-04-SUMMARY.md` when done.
|
||||
</output>
|
||||
629
.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
Normal file
629
.planning/phases/14.2.1-translate-plugin/14.2.1-PATTERNS.md
Normal file
@@ -0,0 +1,629 @@
|
||||
# Phase 14.2.1: Translate plugin - Pattern Map
|
||||
|
||||
**Mapped:** 2026-10-06
|
||||
**Files analyzed:** 42 new or modified files (plugin + framework + host + tests)
|
||||
**Analogs found:** 40 / 42
|
||||
|
||||
Path roots:
|
||||
|
||||
- `FW/` = `/media/nvme/dev/golem15/summercms.io/summercms/summercms.go` (working directory; relative paths below are from here unless prefixed).
|
||||
- `USR/` = `../fonoteka.go/plugins/golem15/user/` — the `sm-user-plugin` submodule. This is the plugin-mount analog RESEARCH named. `/media/nvme/dev/golem15/fonoteka.go/plugins/golem15/user/` does **not** exist.
|
||||
- `BM/` = `../sm-bm-app/` — proof-host analog (`summer.yaml` + `go.work` + `.gitmodules` + `replace`). Copy this layout, not `fonoteka.go`.
|
||||
- `PHP/` = `/media/nvme/dev/golem15/fonoteka/plugins/golem15/translate` at SHA `725d547ec839f02b5fdc0f0a6faaed601a414d50` (verified `git rev-parse HEAD` this session; 2026-08-26). Read-only contract. Do not edit.
|
||||
- `TR/` = `../sm-translate-plugin/` — **does not exist yet** (D-16). Plan 01 creates it.
|
||||
- `APP/` = `../sm-grzybyfunkcjonalne-app/` — **does not exist yet** (D-14). Plan 01 clones the empty remote.
|
||||
|
||||
All analog paths below are git-tracked (`git ls-files` in `summercms.go`, inside the user submodule, inside `sm-bm-app`, and inside the PHP plugin). No gitignored mirror is named. `modules/boardwalk/dist`, `admin/openapi/admin.json` and `admin/src/api/schema.d.ts` are generated outputs: regenerate them, never hand-edit.
|
||||
|
||||
**Table names (locked by RESEARCH against the frozen PHP models):** `winter_translate_locales`, `winter_translate_attributes`, `winter_translate_indexes`, `winter_translate_messages`. CONTEXT D-10's `golem15_translate_*` placeholder is wrong. Do not invent `golem15_translate_*`.
|
||||
|
||||
Suggested plan split from RESEARCH (present at the plan-count checkpoint; unit tests last): **14.2.1-01** plugin repo + squashed schema + Locale + Translator + surf Resolver seam; **14.2.1-02** Translatable API + Locales admin + fixture; **14.2.1-03** cabana `markdown`/`mltext`/`mlmarkdown` + SPA + proof host boot; **14.2.1-04** unit/integration tests last.
|
||||
|
||||
## File Classification
|
||||
|
||||
### New plugin (`TR/` = `sm-translate-plugin`)
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|-------------------|------|-----------|----------------|---------------|
|
||||
| `TR/go.mod` | config | — | `USR/go.mod` | exact |
|
||||
| `TR/plugin.go` | provider | request-response | `USR/plugin.go` | exact |
|
||||
| `TR/README.md` | docs | — | `USR/README.md` | exact |
|
||||
| `TR/admin.go` | provider | — | `USR/admin.go` | exact |
|
||||
| `TR/admin_permissions.go` | config | — | `USR/admin_permissions.go` | exact |
|
||||
| `TR/admin_navigation.go` | config | — | `USR/admin_navigation.go` | exact |
|
||||
| `TR/config/config.yaml` | config | — | `USR/config/config.yaml` + `PHP/config/config.php` | exact |
|
||||
| `TR/lang/en/lang.yaml`, `TR/lang/pl/lang.yaml` | config | transform | `USR/lang/{en,pl}/lang.yaml` + `PHP/lang/en/lang.php` keys `plugin.*` / `locale.*` | exact |
|
||||
| `TR/models/registry.go` | utility | — | `USR/models/registry.go` | exact |
|
||||
| `TR/models/locale.go` | model | CRUD | `USR/models/user_group.go` (Fillable/Rules/TableName) + `PHP/models/Locale.php` (guards) | exact |
|
||||
| `TR/models/attribute.go` | model | CRUD | `USR/models/api_token.go` shape + `PHP/models/Attribute.php` fillable | exact |
|
||||
| `TR/updates/registry.go` | utility | — | `USR/updates/registry.go` | exact |
|
||||
| `TR/updates/202610060001_create_winter_translate_locales.go` | migration | CRUD | `USR/updates/00_base.go` + `USR/updates/202610020001_create_user_groups.go` (CREATE + seed style) | exact |
|
||||
| `TR/updates/202610060002_create_winter_translate_attributes.go` | migration | CRUD | `USR/updates/00_base.go` | exact |
|
||||
| `TR/updates/202610060003_create_winter_translate_indexes.go` | migration | CRUD | `USR/updates/00_base.go` | exact |
|
||||
| `TR/updates/202610060004_create_winter_translate_messages.go` | migration | CRUD | `USR/updates/00_base.go` (DDL only; no Message admin) | exact |
|
||||
| `TR/updates/202610060005_seed_en_pl_locales.go` | migration | CRUD | `USR/updates/202610020001_create_user_groups.go` INSERT + `PHP/updates/v1.3.1/seed_all_tables.php` and `v2.4.0/seed_additional_locales.php` | exact |
|
||||
| `TR/classes/translator.go` | service | request-response | `PHP/classes/Translator.php` + `LocaleMiddleware.php` + `ApiLocaleMiddleware.php` (contract) and `USR/plugin.go` Boot `app.Publish` | role-match |
|
||||
| `TR/classes/translatable.go` | service | CRUD | `PHP/behaviors/TranslatableModel.php` + `classes/TranslatableBehavior.php` (no Go translatable analog) | partial |
|
||||
| `TR/controllers/admin_registry.go` | config | — | `USR/controllers/admin_registry.go` | exact |
|
||||
| `TR/controllers/locales.go` | controller | CRUD | `USR/controllers/usergroups_admin_controller.go` | exact |
|
||||
| `TR/controllers/locales/config_list.yaml` | config | file-I/O | `USR/controllers/usergroups/config_list.yaml` + `PHP/controllers/locales/config_list.yaml` | exact |
|
||||
| `TR/controllers/locales/config_form.yaml` | config | file-I/O | `USR/controllers/usergroups/config_form.yaml` + `PHP/controllers/locales/config_form.yaml` | exact |
|
||||
| `TR/models/locale/fields.yaml` | config | file-I/O | `USR/models/usergroup/fields.yaml` + `PHP/models/locale/fields.yaml` | exact |
|
||||
| `TR/models/locale/columns.yaml` | config | file-I/O | `USR/models/usergroup/columns.yaml` + `PHP/models/locale/columns.yaml` | exact |
|
||||
|
||||
### Framework (`FW/`)
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|-------------------|------|-----------|----------------|---------------|
|
||||
| `modules/surf/router.go` (`locale()` seam) | middleware | request-response | same file `locale` 707-710 and `wrap` 439; `backpack.Lookup` from `USR/plugin.go` Boot | exact |
|
||||
| `modules/surf/locale_resolver.go` (new interface, if extracted) | middleware | request-response | `modules/surf/locale_from_principal.go` + `modules/pact/capabilities.go` `HasHouseMiddleware` (do **not** use house-MW to replace `locale()`) | role-match |
|
||||
| `modules/cabana/form_schema.go` | config compiler | transform | same file `formFieldTypes` 24-29, `compileFieldNode` 454-478 | exact |
|
||||
| `modules/cabana/crud.go` | service | CRUD | same file `ProjectWritableFields` 154-176, `scalarFormField` 1203-1209, `save` lifts 575-590 | exact |
|
||||
| `modules/cabana/field_ml.go` (new: `markdown` / `mltext` / `mlmarkdown`) | service | transform + CRUD | `modules/cabana/field_permission.go` (`liftPermissionValues` nested object) + `field_date.go` (`compileDatepickerKeys`) | role-match |
|
||||
| `modules/cabana/README.md` | docs | — | same file Features list | exact |
|
||||
| `docs/backend/forms.md` | docs | — | same file Field types table 86-101 | exact |
|
||||
| `docs/backend/admin-controllers.md` | docs | — | same file Compilation at boot 159-161 (only if activation rules change) | exact |
|
||||
| `admin/src/components/form/registry.ts` | config (registry) | transform | same file 49-63 | exact |
|
||||
| `admin/src/components/form/fields/MarkdownField.vue` | component | request-response | `admin/src/components/form/fields/TextareaField.vue` | exact |
|
||||
| `admin/src/components/form/fields/MLTextField.vue` | component | request-response | `TextField.vue` (editor) + `PHP/formwidgets/mltext/partials/_mltext.htm` (chrome) | role-match |
|
||||
| `admin/src/components/form/fields/MLMarkdownField.vue` | component | request-response | MarkdownField + ML chrome (compose; do not duplicate markdown) | role-match |
|
||||
| `admin/src/components/form/formState.ts` | utility | transform | same file `editablePayload` 50-69 (permissioneditor nested object already sent) | exact |
|
||||
| `admin/openapi/admin.json`, `admin/src/api/schema.d.ts`, `modules/boardwalk/dist/` | generated | — | regenerate; never analog-copy | — |
|
||||
|
||||
### Proof host (`APP/` = `sm-grzybyfunkcjonalne-app`)
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|-------------------|------|-----------|----------------|---------------|
|
||||
| `APP/go.mod` | config | — | `BM/go.mod` | exact |
|
||||
| `APP/go.work` | config | — | `BM/go.work` | exact |
|
||||
| `APP/summer.yaml` | config | — | `BM/summer.yaml` | exact |
|
||||
| `APP/.gitmodules` | config | — | `BM/.gitmodules` | exact |
|
||||
| `APP/main.go`, `APP/plugins.gen.go` | route | — | `BM/main.go`, `BM/plugins.gen.go` (`summer build` emits these; do not hand-author after first boot) | exact |
|
||||
|
||||
### Tests (plan 04 last)
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|-------------------|------|-----------|----------------|---------------|
|
||||
| `TR/admin_harness_test.go` + Locales admin tests | test | request-response | `USR/admin_harness_test.go` `newAdminEnv` | exact |
|
||||
| `TR/updates/postgres_test.go` | test | CRUD | `USR/updates/postgres_test.go` TestMain + testcontainers | exact |
|
||||
| `TR/classes/*_test.go` Translator + Translatable | test | request-response / CRUD | PHP `tests/unit/behaviors/TranslatableModelTest.php` landmines (lean subset) | partial |
|
||||
| `modules/cabana/*_test.go` ML compile/save | test | transform | `modules/cabana/form_schema_test.go` `TestFormSchemaRejects` | exact |
|
||||
| `modules/surf/*_test.go` Resolver present vs absent | test | request-response | `modules/surf/locale_from_principal_test.go` | exact |
|
||||
| `admin/tests/form/registry.test.ts` + ML field tests | test | — | `admin/tests/form/registry.test.ts`, `admin/tests/form/DatepickerField.test.ts` | exact |
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `TR/go.mod` (config) — plan 01
|
||||
|
||||
**Analog:** `USR/go.mod` lines 1-14, 120
|
||||
|
||||
```
|
||||
module git.golem15.com/golem15/sm-user-plugin
|
||||
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
git.golem15.com/golem15/summercms v0.0.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7
|
||||
...
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
|
||||
replace git.golem15.com/golem15/summercms => ../../../../summercms.go
|
||||
```
|
||||
|
||||
Copy: module `git.golem15.com/golem15/sm-translate-plugin`, Go 1.27.0, require GORM + gormigrate + summercms, **identical** `replace … => ../../../../summercms.go` when mounted at `plugins/golem15/translate`. Do not add goldmark here unless the plugin itself parses markdown; goldmark stays a framework dep.
|
||||
|
||||
Decision note: `.planning/notes/core-plugins-own-repos.md` lines 12-15 — module path equals repo path; package `translate`; plugin ID `golem15.translate`. README never names a consuming app.
|
||||
|
||||
---
|
||||
|
||||
### `TR/plugin.go` (provider) — plan 01
|
||||
|
||||
**Analog:** `USR/plugin.go` lines 28-64, 178-180, 207-209, 236-238
|
||||
|
||||
```go
|
||||
var (
|
||||
_ party.Plugin = (*Plugin)(nil)
|
||||
_ pact.HasConfig = (*Plugin)(nil)
|
||||
_ pact.HasMigrations = (*Plugin)(nil)
|
||||
_ pact.HasModels = (*Plugin)(nil)
|
||||
_ pact.HasLang = (*Plugin)(nil)
|
||||
)
|
||||
|
||||
//go:embed config
|
||||
var configFS embed.FS
|
||||
//go:embed lang
|
||||
var langFS embed.FS
|
||||
|
||||
func (p *Plugin) ID() string { return "golem15.user" }
|
||||
func (p *Plugin) Requires() []string { return nil }
|
||||
func (p *Plugin) Register(app *backpack.App) error { p.app = app; return nil }
|
||||
func (p *Plugin) ConfigFS() fs.FS { return configFS }
|
||||
func (p *Plugin) LangFS() fs.FS { return langFS }
|
||||
func (p *Plugin) Migrations() []*gormigrate.Migration { return updates.All() }
|
||||
func (p *Plugin) Models() []any { return models.All() }
|
||||
|
||||
func init() { party.Register(&Plugin{}) }
|
||||
```
|
||||
|
||||
Copy: `ID() "golem15.translate"`, `Requires()` empty (Translator works without user; locale-from-user is optional). **Do not** copy JWT/bouncer/mail/commands/middleware from user. **Do** `app.Publish` a Resolver in `Boot` (see Translator assignment). Admin capability assertions live in `admin.go`, not here.
|
||||
|
||||
PHP contract (do **not** port): `PHP/Plugin.php` `registerComponents`, `manage_messages`, `registerFormWidgets`, console commands, CMS extend — all deferred. Port only `manage_locales` permission (lines 71-75) and Locales as admin CRUD, not `HasSettings` (PHP `registerSettings` points at a list controller; cabana `HasSettings` is a singleton — RESEARCH §2).
|
||||
|
||||
---
|
||||
|
||||
### `TR/admin.go` + permissions + navigation (provider / config) — plan 02
|
||||
|
||||
**Analog:** `USR/admin.go` lines 12-38, `USR/admin_permissions.go` 14-21, `USR/admin_navigation.go` 11-19
|
||||
|
||||
```go
|
||||
//go:embed controllers/usergroups/config_list.yaml controllers/usergroups/config_form.yaml models/usergroup/fields.yaml models/usergroup/columns.yaml
|
||||
var adminFS embed.FS
|
||||
|
||||
func (p *Plugin) AdminFS() fs.FS { return adminFS }
|
||||
func (p *Plugin) AdminControllers() []pact.AdminController {
|
||||
return controllers.AdminControllers(func() *backpack.App { return p.app })
|
||||
}
|
||||
var (
|
||||
_ pact.AdminAssets = (*Plugin)(nil)
|
||||
_ pact.HasAdminControllers = (*Plugin)(nil)
|
||||
_ pact.HasPermissions = (*Plugin)(nil)
|
||||
_ pact.HasNavigation = (*Plugin)(nil)
|
||||
)
|
||||
```
|
||||
|
||||
Permissions analog (`USR/admin_permissions.go`):
|
||||
|
||||
```go
|
||||
{
|
||||
Code: "golem15.users.access_users",
|
||||
Tab: userPermissionTab,
|
||||
Label: "golem15.user::lang.plugin.access_users",
|
||||
Roles: []string{"developer"},
|
||||
},
|
||||
```
|
||||
|
||||
Translate: `Code: "golem15.translate.manage_locales"`, tab `golem15.translate::lang.plugin.tab`, label `golem15.translate::lang.plugin.manage_locales`, `Roles: []string{"developer"}`. **Do not** register `golem15.translate.manage_messages` this phase.
|
||||
|
||||
Navigation analog: main item `Code: "translate"`, `Icon` lucide (`languages` or similar; PHP was `icon-language`), `Permissions: []string{"golem15.translate.manage_locales"}`, `Controller: "golem15.translate.locales"`, `Order` ~550 (PHP settings order 550). Locales is CRUD list+form, not a settings singleton.
|
||||
|
||||
---
|
||||
|
||||
### `TR/models/locale.go` (model, CRUD) — plan 01/02
|
||||
|
||||
**Analog (Go struct + Fillable + Rules):** `USR/models/user_group.go` lines 9-50
|
||||
|
||||
```go
|
||||
type UserGroup struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Code *string `gorm:"column:code"`
|
||||
// ...
|
||||
}
|
||||
func (UserGroup) TableName() string { return "user_groups" }
|
||||
func (UserGroup) Fillable() []string { return []string{"name", "code", "description"} }
|
||||
func (UserGroup) Rules() map[string]string {
|
||||
return map[string]string{"name": "required|between:3,64", "code": "required|unique:user_groups"}
|
||||
}
|
||||
func init() { Register(UserGroup{}) }
|
||||
```
|
||||
|
||||
**Contract (PHP):** `PHP/models/Locale.php` lines 24-43, 77-109
|
||||
|
||||
```php
|
||||
public $table = 'winter_translate_locales';
|
||||
public $rules = ['code' => 'required', 'name' => 'required'];
|
||||
public $fillable = ['code', 'name', 'is_enabled'];
|
||||
public $timestamps = false;
|
||||
// beforeDelete: cannot delete default
|
||||
// beforeUpdate: cannot unset default; makeDefault() writes is_default
|
||||
// makeDefault: cannot make a disabled locale default
|
||||
```
|
||||
|
||||
Copy: `TableName() "winter_translate_locales"`, no `CreatedAt`/`UpdatedAt`, Fillable **only** `code`, `name`, `is_enabled`. `is_default` and `sort_order` are **not** fillable (PHP). Rules: `code` required, `name` required. Port delete/unset/disabled-default as `FormBeforeDelete` / `FormBeforeUpdate` returning `&cabana.ValidationError{Details: ...}` (422) — analog `USR/controllers/usergroups_admin_controller.go` 162-175.
|
||||
|
||||
`isValid` = code in enabled list (`PHP/models/Locale.php` 228-232). Default locale: `is_default` true row; seed `en` as default.
|
||||
|
||||
---
|
||||
|
||||
### `TR/models/attribute.go` (model, CRUD) — plan 02
|
||||
|
||||
**Contract:** `PHP/models/Attribute.php` lines 15-34 — table `winter_translate_attributes`, fillable `locale`, `model_type`, `model_id`, `attribute_data`, `$guarded = ['*']`.
|
||||
|
||||
**Go analog:** `USR/models/user_group.go` TableName + Register. No public Attribute controller. Writes only through Translatable helpers (RESEARCH T-SEC). Optional: omit a Message model entirely and only DDL `winter_translate_messages` (RESEARCH §9). If Attribute exists, Fillable matches PHP; never expose an admin controller.
|
||||
|
||||
---
|
||||
|
||||
### `TR/updates/*` (migration, CRUD) — plan 01
|
||||
|
||||
**Analog (CREATE + Register):** `USR/updates/00_base.go` lines 1-33
|
||||
|
||||
```go
|
||||
var migrations = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202609170001_create_users",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
...
|
||||
)`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS users`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
func init() { Register(migrations...) }
|
||||
```
|
||||
|
||||
**Analog (indexes + idempotent seed):** `USR/updates/202610020001_create_user_groups.go` lines 17-45 — `CREATE INDEX …`, `INSERT INTO … VALUES`.
|
||||
|
||||
**Contract columns (squash to final names; do not emit `rainlab_translate_*`):**
|
||||
|
||||
| Table | Columns (from PHP create + later updates) |
|
||||
|-------|-------------------------------------------|
|
||||
| `winter_translate_locales` | `id SERIAL PK`, `code TEXT` indexed, `name TEXT` indexed nullable, `is_default BOOLEAN DEFAULT FALSE`, `is_enabled BOOLEAN DEFAULT FALSE`, `sort_order INTEGER DEFAULT 0`. No timestamps. |
|
||||
| `winter_translate_attributes` | `id`, `locale` indexed, `model_id` indexed nullable, `model_type` indexed nullable, `attribute_data TEXT` nullable |
|
||||
| `winter_translate_indexes` | `id`, `locale` indexed, `model_id` indexed nullable, `model_type` indexed nullable, `item` indexed nullable, `value TEXT` nullable |
|
||||
| `winter_translate_messages` | `id`, `code` indexed nullable, `message_data TEXT` nullable, `found BOOLEAN DEFAULT TRUE`, `code_pre_2_1_0 TEXT` indexed nullable — create empty; no Messages admin |
|
||||
|
||||
Suggested IDs (planner may adjust date prefix, not table names): `202610060001_create_winter_translate_locales` … `202610060005_seed_en_pl_locales`.
|
||||
|
||||
**Seed contract:** `PHP/updates/v1.3.1/seed_all_tables.php` 16-22 (`en` / English / default / enabled) and `PHP/updates/v2.4.0/seed_additional_locales.php` 13-20 (`pl` / Polski / not default / enabled / `sort_order` 2). Set `en.sort_order = 1`. **Do not seed `de`.** Idempotent: insert by `code` if missing (`$exists` check lines 33-36).
|
||||
|
||||
---
|
||||
|
||||
### `TR/classes/translator.go` (service, request-response) — plan 01
|
||||
|
||||
**Contract order (do not reduce):** `PHP/classes/LocaleMiddleware.php` 24-41
|
||||
|
||||
1. URL prefix (`Translator::loadLocaleFromRequest` — first path segment, only if `Locale::isValid`) — `PHP/classes/Translator.php` 129-138
|
||||
2. Else authenticated user's `preferred_locale` if valid — middleware 54-83
|
||||
3. Else session `golem15.translate.locale` — Translator 204-213
|
||||
4. Else Accept-Language **only if** `browserDetection.enabled` **and** cookie `locale_manually_set` is absent — middleware 33-36, 96-100, 214-228. Parse q-values; take first two letters; allow-list against enabled codes (126-132). **Do not** pass the raw header into `towel.WithLocale`.
|
||||
5. Else default locale.
|
||||
|
||||
API chain (ship Translator so Phase 15 can call it): `PHP/classes/ApiLocaleMiddleware.php` 40-59 — user preferred → Accept-Language → default; `setLocale($locale, false)` no session.
|
||||
|
||||
Keys (`PHP/classes/Translator.php` 23-25, `config/config.php` 77-86):
|
||||
|
||||
- session/cookie locale: `golem15.translate.locale`
|
||||
- configured flag: `golem15.translate.configured`
|
||||
- manual-selection cookie **flag only**: `locale_manually_set` = `'1'`, expiry 525600 minutes. Does **not** contain a locale code. URL-prefix visit queues it (`PHP/routes.php` 29-35).
|
||||
|
||||
`setLocale` requires `Locale::isValid`; invalid codes return false and are never persisted (`Translator.php` 58-72).
|
||||
|
||||
**Go seam analog:** `USR/plugin.go` Boot Publish (lines 77-95) + `FW/modules/backpack/app.go` 59-73
|
||||
|
||||
```go
|
||||
func (a *App) Publish[T any](value T) error { return a.Services.Publish(value) }
|
||||
func (a *App) Lookup[T any]() (T, bool) { ... }
|
||||
```
|
||||
|
||||
User plugin publishes `*bouncer.Registry` / `bouncer.BlacklistStore`. Translate Boot publishes a `surf.LocaleResolver` (or equivalent interface **in the framework**, because `surf` must not import the plugin). Methods take `ctx` and `*http.Request`; they must not store the active locale on a process-wide struct (KERN-07). Write the resolved code with `towel.WithLocale`.
|
||||
|
||||
**Do not** implement PHP Singleton `Translator::instance()`. **Do not** use `pact.HasHouseMiddleware` to replace house `locale()`: house-MW is a named middleware table (`FW/modules/pact/capabilities.go` 47-59); `locale()` is hardcoded in `wrap` after named MW (`router.go` 439).
|
||||
|
||||
Config keys under plugin namespace (`PHP/config/config.php`): `forceDefaultLocale`, `prefixDefaultLocale` (default true), `disableLocalePrefixRoutes` (default false), `browserDetection.enabled` (default true), `browserDetection.manualSelectionCookie`, `browserDetection.manualSelectionExpiry`. Port into `TR/config/config.yaml` via `HasConfig` like `USR/config/config.yaml`.
|
||||
|
||||
---
|
||||
|
||||
### `modules/surf/router.go` locale seam (middleware) — plan 01
|
||||
|
||||
**Analog (current, to wrap):** `FW/modules/surf/router.go` 439 and 707-710
|
||||
|
||||
```go
|
||||
h = locale(h)
|
||||
|
||||
func locale(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(w, r.WithContext(towel.WithLocale(r.Context(), r.Header.Get("Accept-Language"))))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`wrap` currently has `*Router` but **no** `*backpack.App`. `BuildRouter(app, plugins)` has `app` (459-464). Prescription: store `app` on `Router` during `BuildRouter`, then `locale()` Lookup Resolver; if published, call it and write a **validated** code; else keep today's Accept-Language behavior so `fonoteka.go` (no translate plugin) does not change.
|
||||
|
||||
**Existing overlay (keep when no Resolver):** `FW/modules/surf/locale_from_principal.go` 10-18 — `PreferredLocale` on the principal. With Resolver, user preferred is step 2 of the PHP chain (after URL prefix), not a post-hoc overlay of the raw header.
|
||||
|
||||
**Context bag:** `FW/modules/towel/context.go` 55-62
|
||||
|
||||
```go
|
||||
func WithLocale(ctx context.Context, locale string) context.Context {
|
||||
return withValue(ctx, localeKey{}, locale)
|
||||
}
|
||||
func Locale(ctx context.Context) (string, bool) {
|
||||
return stringValue(ctx, localeKey{})
|
||||
}
|
||||
```
|
||||
|
||||
Phrasebook UI locale and model content locale usually match after Translator runs; Translatable helpers still take an **explicit** locale argument (admin SPA UI can stay `en` while content is `pl`).
|
||||
|
||||
**Cookie attributes analog (not JWT):** `FW/modules/cabana/auth.go` 243-252 — `HttpOnly`, `Secure`, `SameSite`. Locale cookies are not admin session tokens; re-validate enabled codes every request. RESEARCH A3: signed cookie `golem15.translate.locale` is the Go stand-in for Laravel session; do not invent a second JWT library.
|
||||
|
||||
---
|
||||
|
||||
### `TR/classes/translatable.go` (service, CRUD) — plan 02
|
||||
|
||||
**No Go translatable analog.** Copy PHP storage, not Eloquent magic.
|
||||
|
||||
**Contract:** `PHP/classes/TranslatableBehavior.php` 141-216 and `PHP/behaviors/TranslatableModel.php` 89-108, 224-294, 345-348
|
||||
|
||||
- Default locale values live on the **host model's own columns**. `isTranslatable` is false when context equals default.
|
||||
- Other locales: JSON object in `winter_translate_attributes.attribute_data`, one row per `(locale, model_id, model_type)`.
|
||||
- Missing key + fallback on (default): return default-locale column.
|
||||
- Indexed attributes (`['slug', 'index' => true]`) also write `winter_translate_indexes` (`item` = attribute, `value` = translated string).
|
||||
- `model_type` = `getMorphClass()` → Go `MorphName() string`.
|
||||
- `scopeTransWhere`: look up index table; if no rows, `where` on the host column (104-108).
|
||||
- Do **not** port Redis `translation:%s:%s:%s` cache this phase.
|
||||
|
||||
**MorphName analog:** `USR/models/user.go` 59-60 and `FW/modules/lagoon/attach/example_test.go` 21
|
||||
|
||||
```go
|
||||
func (User) MorphName() string { return `Golem15\User\Models\User` }
|
||||
func (Post) MorphName() string { return `Acme\Blog\Models\Post` }
|
||||
```
|
||||
|
||||
Fixture models may use a Go type string. Document that Phase 15 Journal import must pin PHP class strings (`Golem15\Journal\Models\Post`).
|
||||
|
||||
Minimum exported API (RESEARCH §3; identifiers prescribed there):
|
||||
|
||||
```go
|
||||
type Translatable interface {
|
||||
Translatable() []string
|
||||
MorphName() string
|
||||
}
|
||||
func WithLocale(ctx context.Context, db *gorm.DB, locale string) *gorm.DB
|
||||
func Translated(...) (any, error)
|
||||
func SetTranslated(...) error
|
||||
```
|
||||
|
||||
Plus `TranslatableIndexes() []string` (or options) for Journal slugs. Do not 1:1 every PHP method. Do not export deprecated `noFallbackLocale`.
|
||||
|
||||
---
|
||||
|
||||
### `TR/controllers/locales.go` (controller, CRUD) — plan 02
|
||||
|
||||
**Analog:** `USR/controllers/usergroups_admin_controller.go` 41-53, 123-131, 162-175 + `USR/controllers/admin_registry.go` 12-18, 42-50
|
||||
|
||||
```go
|
||||
func (usergroupsAdminController) ID() string { return "golem15.user.usergroups" }
|
||||
func (usergroupsAdminController) ModelName() string { return `Golem15\User\Models\UserGroup` }
|
||||
func (usergroupsAdminController) ConfigDir() string { return "controllers/usergroups" }
|
||||
func (usergroupsAdminController) RequiredPermissions() []string {
|
||||
return []string{PermissionAccessGroups}
|
||||
}
|
||||
func (usergroupsAdminController) NewRecord() any { return &models.UserGroup{} }
|
||||
```
|
||||
|
||||
Translate: `ID() "golem15.translate.locales"`, `ModelName() \`Golem15\Translate\Models\Locale\``, `ConfigDir() "controllers/locales"`, `RequiredPermissions() []string{"golem15.translate.manage_locales"}`, `NewRecord() &models.Locale{}`.
|
||||
|
||||
PHP `Locales.php` 21: `$requiredPermissions = ['golem15.translate.manage_locales']`. Implement `pact.AdminPermissioned`. 403 without it.
|
||||
|
||||
Hooks: `FormBeforeDelete` refuse default locale; `FormBeforeUpdate` refuse unsetting default / making disabled default — return `&cabana.ValidationError` (422) with phrase keys from `PHP/lang/en/lang.php` 27-29 (`unset_default`, `delete_default`, `disabled_default`). Analog Forbidden vs Validation: usergroups uses `ForbiddenError` for permission (403) and `ValidationError` for code format (422). Locale guards are validation, not 403.
|
||||
|
||||
PHP ReorderController has **no cabana analog**. Keep `sort_order`, seed `en=1` `pl=2`, list `defaultSort` `sort_order` asc. Do not invent drag-reorder.
|
||||
|
||||
**Error types:** `FW/modules/cabana/crud.go` 62-81 `ValidationError` / `ForbiddenError`.
|
||||
|
||||
---
|
||||
|
||||
### Admin YAML (config, file-I/O) — plan 02
|
||||
|
||||
**Go analog:** `USR/controllers/usergroups/config_list.yaml` (recordUrl, recordsPerPage 20, toolbar create, search) and `config_form.yaml` (form path, modelClass, redirects).
|
||||
|
||||
**PHP contract fields:** `PHP/models/locale/fields.yaml` — `name`, `code`, `is_enabled` checkbox, `is_default` checkbox. `PHP/models/locale/columns.yaml` — name/code searchable, is_default switch, is_enabled switch invisible, sort_order number invisible.
|
||||
|
||||
Cabana list types (`FW/modules/cabana/list_schema.go` 22-24): `"text"`, `"datetime"`, `"switch"`, `"date"`, `"time"`. Map PHP `type: number` on invisible `sort_order` → omit type (text) or drop from visible columns.
|
||||
|
||||
`config_list.yaml`: PHP `recordOnClick` is Winter AJAX; Go uses `recordUrl: golem15/translate/locales/update/:id` like usergroups. `title: golem15.translate::lang.locale.label_plural`. `defaultSort.column: sort_order`, `direction: asc`.
|
||||
|
||||
Embed YAML file-by-file in `admin.go` (`//go:embed controllers/locales/... models/locale/...`), same as user — do not embed the whole `controllers/` tree (it will hold `.go` files).
|
||||
|
||||
---
|
||||
|
||||
### Phrasebook lang YAML (config) — plan 02
|
||||
|
||||
**Analog:** `USR/lang/en/lang.yaml` `plugin:` block lines 4-10 and `USR/lang/pl/lang.yaml` same keys.
|
||||
|
||||
**Contract keys to port (UI only):** `PHP/lang/en/lang.php` 4-41 `plugin.name/description/tab/manage_locales` and `locale.*` (label, label_plural, title, create/update titles, name, code, is_default, is_enabled, help, delete/unset/disabled_default, sort_order, hint). Do **not** load PHP `unsupported_lang/` or `messages.*` this phase.
|
||||
|
||||
Do not conflate phrasebook files with Locale seed rows (D-08 is two Locale rows; two YAML trees for plugin UI).
|
||||
|
||||
---
|
||||
|
||||
### Cabana `markdown` / `mltext` / `mlmarkdown` — plan 03
|
||||
|
||||
**Registry analog:** `FW/modules/cabana/form_schema.go` 24-47 and 465-478
|
||||
|
||||
```go
|
||||
formFieldTypes = map[string]struct{}{
|
||||
"text": {}, "textarea": {}, ... "datepicker": {}, "password": {}, "permissioneditor": {},
|
||||
}
|
||||
// unknown key → "unknown field %s"
|
||||
// unknown type → "unsupported type %s"
|
||||
```
|
||||
|
||||
Add `"markdown"`, `"mltext"`, `"mlmarkdown"` to `formFieldTypes`. Prefer **no extra YAML keys** (reuse `label`, `comment`, `span`, `size`, `required`, `tab`, `context`). If a key is added, it must go in `formFieldKeys` or boot fails.
|
||||
|
||||
**Compile analog:** `FW/modules/cabana/field_date.go` 42-52 `compileDatepickerKeys` — refuse type-specific keys on other types; call from `compileFieldNode` next to `compilePermissionKeys` / `compileDatepickerKeys` (form_schema.go 551-558).
|
||||
|
||||
**Nested save analog:** `FW/modules/cabana/field_permission.go` 177-210 `liftPermissionValues` — read `body[name]` as `map[string]any` **before** scalar projection. Hook the lift from `CRUDService.save` (`crud.go` 575-590) the same way relations/virtual/permissions are lifted.
|
||||
|
||||
**The landmine:** `ProjectWritableFields` (`crud.go` 154-176) drops `nestedValue` (maps/slices, 1224-1230). `scalarFormField` (1203-1209) is only `text/textarea/number/checkbox/switch/dropdown/datepicker` — `mltext`/`mlmarkdown`/`markdown` are skipped by `BindWritableFields` (201) unless treated as scalar **or** lifted like permissioneditor.
|
||||
|
||||
**PHP save contract:** `PHP/traits/MLControl.php` 186-216 — POST all locales; `setAttributeTranslated` per locale; return value is the **default locale** entry only. `getLocaleValue` uses `setTranslatableUseFallback(false)` so empty translations stay empty in hidden fields (158-159). Host column = default locale; attribute rows = other locales only (do not duplicate default into `winter_translate_attributes`).
|
||||
|
||||
**SPA registry analog:** `FW/admin/src/components/form/registry.ts` 49-63 — add `markdown`, `mltext`, `mlmarkdown`. `isRegistered` must stay true so `formState.editablePayload` (50-69) includes them. permissioneditor already sends a nested object whenever shown (60-63) — ML fields should send `Record<locale, string>` the same way.
|
||||
|
||||
**Chrome contract:** `PHP/formwidgets/mltext/partials/_mltext.htm` — wrapper `data-default-locale`, locale selector, hidden inputs per locale, copy-from-locale optional. One switcher per ML field; switching one switches all (PHP Ctrl/Cmd-click). Visible editor shows the active locale.
|
||||
|
||||
**Markdown primitive analog:** `FW/modules/postcard/templates.go` 24-29 `goldmark.New()` already in framework `go.mod` v1.8.6. Land shared `markdown` **this phase** (`mlmarkdown` = markdown editor + ML chrome). Safe mode: no `html.WithUnsafe`; reject leftover script/iframe (postcard `rawUnsafeTag`). Minimum this phase is edit+save of markdown source per locale, not WYSIWYG.
|
||||
|
||||
**Docs analog:** `FW/docs/backend/forms.md` 86-101 — add the three types; **replace** the sentence that the markdown editor is not provided (line 101). Neutral names only (`acme`, `blog`). Same change: `modules/cabana/README.md` Features, OpenAPI, openapi-typescript, committed `boardwalk/dist/`. `go test ./cmd/summer -run TestDocsTree` and `summer docs:build --check`.
|
||||
|
||||
**Fail-loud test analog:** `FW/modules/cabana/form_schema_test.go` `TestFormSchemaRejects` 184-197 (`unknown key` / `unknown type`). Add `type: mlunknown` fails boot.
|
||||
|
||||
---
|
||||
|
||||
### SPA field components — plan 03
|
||||
|
||||
**Text analog:** `FW/admin/src/components/form/fields/TextField.vue` 1-26 — `FieldControlProps`, `update:modelValue`, `controlAttributes` / `controlClass`.
|
||||
|
||||
**Textarea analog for markdown source:** `FW/admin/src/components/form/fields/TextareaField.vue` 1-30 — size → rows.
|
||||
|
||||
**Value-field test analog:** `FW/admin/tests/form/DatepickerField.test.ts` mount + emit; `FW/admin/tests/form/registry.test.ts` `it.each` renderer map (31-42).
|
||||
|
||||
No existing multilingual control. Compose: inner TextField/MarkdownField + locale switcher chrome. `modelValue` is `Record<string, string>` (locale → text), not a scalar.
|
||||
|
||||
---
|
||||
|
||||
### Proof host (`APP/`) — plan 03 (clone in plan 01)
|
||||
|
||||
**Analog:** `BM/go.mod` 1-20, `BM/go.work` 5-10, `BM/summer.yaml` 1-9, `BM/.gitmodules` 1-3, `BM/main.go` 22-38, `BM/plugins.gen.go` 1-16
|
||||
|
||||
```
|
||||
module git.golem15.com/jakub/sm-bm-app
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
replace git.golem15.com/golem15/sm-user-plugin => ./plugins/golem15/user
|
||||
```
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
- id: golem15.user
|
||||
module: git.golem15.com/golem15/sm-user-plugin
|
||||
```
|
||||
|
||||
```
|
||||
[submodule "plugins/golem15/user"]
|
||||
path = plugins/golem15/user
|
||||
url = git@git.golem15.com:golem15/sm-user-plugin.git
|
||||
```
|
||||
|
||||
```go
|
||||
plugins, err := party.Activate(app, PluginIDs)
|
||||
```
|
||||
|
||||
Proof host: module `git.golem15.com/golem15/sm-grzybyfunkcjonalne-app` (or whatever the empty remote uses), plugins **only** `golem15.user` + `golem15.translate`, submodule paths `plugins/golem15/user` and `plugins/golem15/translate`, `go.work` use both, `replace` both to `./plugins/...`, framework replace `../summercms.go`. `main.go` / `plugins.gen.go` come from `summer build` after `summer.yaml` exists (`FW/examples/hello/plugins.gen.go` same stamp `// Code generated by summer build. DO NOT EDIT.`).
|
||||
|
||||
D-17 fixture: RESEARCH A1 — prefer plugin integration test (`party.Activate([]{user, translate, in-process fixture})`) plus host smoke `migrate`/`serve` without a third production plugin. Do not block on a host demo model.
|
||||
|
||||
Manual Wave 0: user creates `git@git.golem15.com:golem15/sm-translate-plugin.git` (does not exist). Host remote already exists.
|
||||
|
||||
---
|
||||
|
||||
### Tests — plan 04
|
||||
|
||||
**Admin boot analog:** `USR/admin_harness_test.go` 129-191 `newAdminEnv` — `party.Activate`, `lagoon.Migrate`, `surf.Assemble`, mint backend admin with a permission set. Locales 403 without `golem15.translate.manage_locales`.
|
||||
|
||||
**Postgres analog:** `USR/updates/postgres_test.go` 25-36 TestMain + testcontainers `postgres:16-alpine`, `-short` skip.
|
||||
|
||||
**Surf analog:** `FW/modules/surf/locale_from_principal_test.go` — table of Resolver-absent (raw Accept-Language still set, fonoteka) vs Resolver-present (URL wins; invalid prefix ignored; cookie flag skips Accept-Language; unlisted code rejected).
|
||||
|
||||
Lean PHP landmines (do not 1:1 the suite): fallback default; set `pl` does not change default column; `WithLocale` + indexed slug; skip morphMap, `addTranslatableAttributes`, Messages, CMS page/url.
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Plugin mount (submodule + go.work + replace)
|
||||
|
||||
**Source:** `USR/go.mod`, `BM/go.mod` / `go.work` / `summer.yaml` / `.gitmodules`, `.planning/notes/core-plugins-own-repos.md`
|
||||
**Apply to:** `TR/` repo creation and `APP/` first mount
|
||||
|
||||
Module `git.golem15.com/golem15/sm-translate-plugin`, package `translate`, ID `golem15.translate`, `party.Register` in `init`, `summer.yaml` lists the id+module, submodule `plugins/golem15/translate`, plugin `replace` framework `../../../../summercms.go`, host `replace` plugin `./plugins/golem15/translate`.
|
||||
|
||||
### Backpack Publish / Lookup (optional Translator)
|
||||
|
||||
**Source:** `FW/modules/backpack/app.go` 59-73; `USR/plugin.go` Boot 77-95
|
||||
**Apply to:** Translator Resolver; surf `locale()` Lookup
|
||||
|
||||
Interface lives in **framework** (`surf` or a tiny contract next to `locale()`). Plugin Boot publishes. Surf looks up; if missing, today's Accept-Language. No process-wide locale (KERN-07).
|
||||
|
||||
### Request locale bag
|
||||
|
||||
**Source:** `FW/modules/towel/context.go` 55-62
|
||||
**Apply to:** Translator and phrasebook
|
||||
|
||||
Always `towel.WithLocale(ctx, validatedCode)`. Never `var currentLocale`. Never stuff the raw `Accept-Language` header into context when Resolver is present.
|
||||
|
||||
### Admin CRUD + permissions
|
||||
|
||||
**Source:** `USR/admin.go`, `usergroups_admin_controller.go`, `pact.HasAdminControllers` / `AdminPermissioned` (`FW/modules/pact/capabilities.go` 182-195)
|
||||
**Apply to:** Locales controller
|
||||
|
||||
YAML + `CRUDService`. `RequiredPermissions` `golem15.translate.manage_locales`. Fillable allow-list. Lifecycle hooks return `ValidationError` (422) or `ForbiddenError` (403). Fail-loud YAML at `cabana.Activate` (`docs/backend/admin-controllers.md` 161).
|
||||
|
||||
### Nested form values (ML save)
|
||||
|
||||
**Source:** `FW/modules/cabana/field_permission.go` `liftPermissionValues`; `crud.go` `save` lifts before `ProjectWritableFields`
|
||||
**Apply to:** `mltext` / `mlmarkdown` locale maps
|
||||
|
||||
Lift `map[string]string` per ML field before nested drop. Default locale → host column; other locales → `SetTranslated`. Do not bind ML maps as extra model columns.
|
||||
|
||||
### Morph type strings
|
||||
|
||||
**Source:** `USR/models/user.go` `MorphName`; `FW/modules/lagoon/attach/example_test.go`
|
||||
**Apply to:** `winter_translate_attributes.model_type` / indexes
|
||||
|
||||
Explicit `MorphName() string`. Do not use `reflect.TypeOf(x).String()`.
|
||||
|
||||
### Phrasebook vs model translations
|
||||
|
||||
**Source:** `USR/lang/{en,pl}/lang.yaml`; `FW/modules/phrasebook` (HasLang)
|
||||
**Apply to:** Locales UI strings only
|
||||
|
||||
`I18N-01` is phrasebook. Attribute JSON is the model layer. Seed Locale **rows** independently of YAML files.
|
||||
|
||||
### Markdown
|
||||
|
||||
**Source:** `FW/modules/postcard/templates.go` goldmark pipeline
|
||||
**Apply to:** cabana `markdown` / `mlmarkdown` preview if any
|
||||
|
||||
One library (`github.com/yuin/goldmark` v1.8.6). Safe HTML. No second markdown parser.
|
||||
|
||||
### Cookie flags
|
||||
|
||||
**Source:** `FW/modules/cabana/auth.go` `sessionCookie` 243-252
|
||||
**Apply to:** `locale_manually_set` and locale cookie
|
||||
|
||||
HttpOnly + Secure + SameSite. Manual cookie is flag `"1"`, not a locale code. Re-validate locale cookies against enabled list every request.
|
||||
|
||||
### Test harness
|
||||
|
||||
**Source:** `USR/admin_harness_test.go` `newAdminEnv`; `USR/updates/postgres_test.go`
|
||||
**Apply to:** plugin admin + migration tests last
|
||||
|
||||
`party.Activate` + `lagoon.Migrate` + `surf.Assemble`. testcontainers behind `-short`.
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Data Flow | Reason |
|
||||
|------|------|-----------|--------|
|
||||
| `TR/classes/translatable.go` (attribute JSON + `WithLocale` index lookup) | service | CRUD | No Go model currently stores translations in `winter_translate_*`. Copy PHP `TranslatableModel` / `TranslatableBehavior` storage; use `MorphName` + GORM only as structural analogs. |
|
||||
| `admin/.../MLTextField.vue` / `MLMarkdownField.vue` locale switcher | component | request-response | No multilingual SPA control exists. Compose TextField/TextareaField + PHP `_mltext.htm` chrome; nested save follows permissioneditor. |
|
||||
|
||||
Planner should use RESEARCH.md §3–§5 for those two, not invent JSON columns or `type: widget` ML controls.
|
||||
|
||||
## Do not copy / anti-patterns
|
||||
|
||||
- **`golem15_translate_*` table names** — frozen PHP uses `winter_translate_*`.
|
||||
- **JSON column on host models** — D-10.
|
||||
- **Translator singleton / package-level request locale** — KERN-07.
|
||||
- **Accept-Language as the only resolver** — D-07; also stop stuffing the raw header into context when Resolver runs.
|
||||
- **Plugin-owned `type: widget` ML controls** — field types belong in cabana (`formFieldTypes`).
|
||||
- **`HasSettings` for Locales** — it is CRUD; use `HasAdminControllers` + `HasNavigation`.
|
||||
- **`HasHouseMiddleware` as the locale seam** — house-MW is named; `locale()` is hardcoded in `wrap`.
|
||||
- **Messages admin, locale picker, AI/theme commands, message import** — deferred.
|
||||
- **Seeding `de`** — D-08 is `en`+`pl` only.
|
||||
- **Editing `wn-translate-plugin` / PHP tree**.
|
||||
- **AutoMigrate as schema** — gormigrate squash only.
|
||||
- **Hand-editing `boardwalk/dist` or OpenAPI JSON**.
|
||||
- **Naming a consuming app in the plugin README**.
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `USR/` (sm-user-plugin), `BM/` (sm-bm-app), `FW/modules/{cabana,surf,towel,backpack,pact,postcard,lagoon}`, `FW/admin/src/components/form`, `FW/docs/backend`, `PHP/` translate plugin at `725d547ec839f02b5fdc0f0a6faaed601a414d50`
|
||||
**Files scanned:** ~90 analog files read or grepped; 3–5 strong matches per new file; PHP pin SHA verified
|
||||
**Pattern extraction date:** 2026-10-06
|
||||
**Tracked-source gate:** every analog path printed by `git ls-files` in its own repository
|
||||
@@ -699,21 +699,18 @@ DATA_r3t6y0ab_END
|
||||
|
||||
A1–A3 are execution details, not stack choices. A4 follows locked D-08.
|
||||
|
||||
## Open Questions
|
||||
## Open Questions (RESOLVED)
|
||||
|
||||
1. **Proof fixture location**
|
||||
1. **Proof fixture location — RESOLVED**
|
||||
- What we know: D-17 wants a fixture model; host checkout is empty/missing.
|
||||
- What's unclear: host app plugin vs test-only fixture.
|
||||
- Recommendation: plugin integration test + host boot without Journal; no third production plugin.
|
||||
- Resolution: use a test-only fixture plugin/model in the translate plugin integration harness, plus a real `sm-grzybyfunkcjonalne-app` host boot with user+translate and no third production plugin. Plans 02–04 implement this split.
|
||||
|
||||
2. **Admin session for Translator**
|
||||
2. **Admin session for Translator — RESOLVED**
|
||||
- What we know: PHP uses Laravel `Session::put(SESSION_LOCALE)`.
|
||||
- What's unclear: SummerCMS public requests may not have a session store yet (admin uses JWT cookie).
|
||||
- Recommendation: signed cookie `golem15.translate.locale` plus `locale_manually_set`; document as the Go analog of session+cookie.
|
||||
- Resolution: use a signed remembered-locale cookie named `golem15.translate.locale`, with `locale_manually_set` remaining a separate flag-only cookie; validate the remembered code against enabled locales on every request. Plan 01 implements this Go session analog.
|
||||
|
||||
3. **Phase 15 ROADMAP depends_on**
|
||||
- Deferred idea: `$gsd-phase --edit 15` not done in discuss.
|
||||
- Recommendation: planner notes it; do not block this phase.
|
||||
3. **Phase 15 ROADMAP depends_on — RESOLVED**
|
||||
- Resolution: keep the ROADMAP dependency edit deferred and out of Phase 14.2.1. The plans document Phase 15 as the consumer without mutating its roadmap metadata or blocking this phase.
|
||||
|
||||
## Environment Availability
|
||||
|
||||
|
||||
Reference in New Issue
Block a user