Files
summercms/.planning/phases/14.2.1-translate-plugin/14.2.1-03-PLAN.md
2026-10-06 11:22:45 +02:00

243 lines
19 KiB
Markdown

---
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)$' &amp;&amp; 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 &amp;&amp; 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' &amp;&amp; go run ./cmd/summer docs:build --check &amp;&amp; npm --prefix admin run typecheck &amp;&amp; npm --prefix admin test -- --run admin/tests/form/registry.test.ts admin/tests/form/MLFields.test.ts &amp;&amp; npm --prefix admin run build &amp;&amp; go -C ../sm-grzybyfunkcjonalne-app vet ./... &amp;&amp; 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>