docs(14.2.1): create phase plan

This commit is contained in:
Jakub Zych
2026-10-06 11:22:45 +02:00
parent e723c391fb
commit f829d17dca
8 changed files with 1673 additions and 29 deletions

View 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 ./... &amp;&amp; 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 ./... &amp;&amp; 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>