228 lines
17 KiB
Markdown
228 lines
17 KiB
Markdown
---
|
|
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>
|