17 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14.2.1-translate-plugin | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context>
@/.codex/gsd-core/workflows/execute-plan.md
@/.codex/gsd-core/templates/summary.md
</execution_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.Attributeand 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.
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.
go -C ../sm-translate-plugin test ./classes -count=1 -v -run '^(TestFixtureTranslatableSaveRead)$'
<fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestFixtureTranslatableSaveRead".</fails_when>
<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>
A neutral model persists its default title on its own row, persists Polish in Winter storage, and reads Polish through the exported API.
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.
go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./classes -count=1 -v -run '^(TestFixtureTranslatableSaveRead|TestFixtureTranslatedIndexSmoke)$'
<fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.</fails_when>
<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>
Journal can query and mutate indexed translated slugs and ordinary translated fields through the minimum public contract.
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.
go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1 -v -run '^(TestLocalesAdminSmoke|TestLocalesAdminForbiddenSmoke)$'
<fails_when>Non-zero exit, output contains "--- FAIL", "--- SKIP", or "no tests to run", or either named PASS line is absent.</fails_when>
<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>
Authorized administrators can manage the lean Locale surface while default-state and phrasebook/model-translation boundaries remain enforced.
<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>
Run all three task commands, then `go -C ../sm-translate-plugin vet ./... && go -C ../sm-translate-plugin test ./... -short -count=1`.<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>