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

233 lines
18 KiB
Markdown

---
phase: 14.2.1-translate-plugin
plan: 06
type: execute
wave: 6
depends_on: ["14.2.1-05"]
files_modified:
- modules/cabana/relation.go
- modules/cabana/relation_child.go
- modules/cabana/http.go
- modules/cabana/relation_child_ml_test.go
- modules/cabana/ml_test.go
- modules/cabana/README.md
- docs/backend/forms.md
- admin/tests/form/MLFields.test.ts
- scripts/check-phase14.2.1.sh
- .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md
autonomous: true
gap_closure: true
requirements: [D-06, D-17]
must_haves:
truths:
- "Cabana markdown/mltext/mlmarkdown compose; nested locale writes are not dropped"
- "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>"
artifacts:
- path: "modules/cabana/relation.go"
provides: "RelationService carries the same TranslationWriter as CRUDService"
contains: "writer"
- path: "modules/cabana/http.go"
provides: "relations() Lookup of TranslationWriter matches crud()"
contains: "TranslationWriter"
- path: "modules/cabana/relation_child.go"
provides: "CreateChild/UpdateChild lift ML maps before fillChild and apply after PK"
contains: "liftMLValues"
- path: "modules/cabana/relation_child.go"
provides: "ShowChild/CreateChild/UpdateChild hydrate ML maps like CRUD save"
contains: "hydrateMLRecord"
- path: "modules/cabana/relation_child_ml_test.go"
provides: "named relation-child nested ML save/read proof"
contains: "TestRelationChildMLNestedSave"
- path: "scripts/check-phase14.2.1.sh"
provides: "gate requires hydration and relation-child ML tests plus new high threat IDs"
contains: "TestRelationChildMLNestedSave"
key_links:
- from: "modules/cabana/http.go"
to: "modules/cabana/relation.go"
via: "relations() copies Lookup[TranslationWriter] onto RelationService.writer"
pattern: "Lookup[TranslationWriter]"
- from: "modules/cabana/relation_child.go"
to: "modules/cabana/field_ml.go"
via: "CreateChild/UpdateChild call liftMLValues then applyMLTranslations like CRUDService.save"
pattern: "applyMLTranslations"
- from: "modules/cabana/relation_child.go"
to: "modules/cabana/field_ml.go"
via: "child Show/save hydrate through hydrateMLRecord"
pattern: "hydrateMLRecord"
---
<objective>
Close WR-02 so D-06/D-17 nested locale writes are not dropped on relation-child saves: RelationService looks up TranslationWriter, lifts maps before fillChild the way CRUDService.save does, applies translations after the child PK, hydrates ShowChild, and lands the phase's remaining named tests.
Purpose: a relation-manager child form with mltext/mlmarkdown must persist English on the host column and Polish through the writer, then GET the hydrated map, matching host CRUD.
Output: RelationService.writer wiring, CreateChild/UpdateChild/ShowChild ML lift+apply+hydrate, TestRelationChildMLNestedSave, Vitest/gate named tests as the last tasks of Phase 14.2.1.
</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-05-SUMMARY.md
@.planning/phases/14.2.1-translate-plugin/14.2.1-REVIEW.md
@.planning/phases/14.2.1-translate-plugin/14.2.1-VERIFICATION.md
@.planning/phases/14.2.1-translate-plugin/14.2.1-CONTEXT.md
@modules/cabana/relation.go
@modules/cabana/relation_child.go
@modules/cabana/http.go
@modules/cabana/crud.go
@modules/cabana/field_ml.go
@modules/cabana/ml_test.go
@modules/cabana/ml_smoke_test.go
</context>
## Spec-less probe fallback
The phase has no mapped requirement IDs, so no speculative requirement probes are generated. Edge coverage is WR-02 and the failed nested-write truth (D-06 / D-17 on relation-child).
## Gap-closure source audit
Audited only WR-02 and the failed nested-write truth. Plan 05 CR-01 hydration and already-verified D-01..D-05, D-07..D-16 are EXCLUDED from re-planning here; Plan 06 consumes hydrateMLRecord / TranslatedExact from 05.
| Decision | Source | Status | Why this plan |
|----------|--------|--------|---------------|
| D-06 | CONTEXT.md; REVIEW WR-02; VERIFICATION truth 4 | NOT WIRED on fillChild | projectOperation drops nested maps; no liftMLValues / applyMLTranslations |
| D-17 | CONTEXT.md; VERIFICATION missing "Relation-child saves lift nested ML maps" | NOT WIRED | Phase 15 related records would silently lose translations |
No new gormigrate file: same runtime lift/apply as CRUDService.save. Skip Prisma/Payload schema-push.
No new HTTP routes. Child create/update/show already exist; this plan hydrates those bodies. OpenAPI regen is not required unless Task 1 accidentally changes a documented type (it should not).
## Artifacts this phase produces
- `RelationService.writer TranslationWriter` and `relations()` Lookup matching `crud()`.
- `CreateChild` / `UpdateChild`: `liftMLValues` on `in.Body` with op create/update and `cr.child` as the compiled form, then `fillChild` seeing host scalars, then `applyMLTranslations` after `Create`/`Save` so the child has a primary key, inside the existing child transaction.
- `ShowChild` / child save results: `hydrateMLRecord` after `projectFullRecord` using `s.writer` and `cr.child` / `cr.childForm()`.
- Named tests `TestRelationChildMLNestedSave`, remaining Vitest create/GET assertions, `FRAMEWORK_REQUIRE` entries, and SECURITY-REVIEW rows for T-14.2.1-19..23.
<tasks>
<task type="tracer">
<name>Task 1: Lift one nested mltext map through CreateChild</name>
<files>modules/cabana/relation.go, modules/cabana/http.go, modules/cabana/relation_child.go, modules/cabana/relation_child_ml_test.go, modules/cabana/ml_smoke_test.go</files>
<read_first>modules/cabana/relation.go (RelationService), modules/cabana/http.go (relations, crud Lookup), modules/cabana/relation_child.go (fillChild, CreateChild, projectFullRecord), modules/cabana/field_ml.go (liftMLValues, applyMLTranslations, hydrateMLRecord), modules/cabana/crud.go (save ML sequence), modules/cabana/ml_test.go (TestMLNestedSave, mlCompiled, mlPost), modules/cabana/ml_smoke_test.go (recordingWriter), .planning/phases/14.2.1-translate-plugin/14.2.1-REVIEW.md (WR-02)</read_first>
<action>Close WR-02 on the create-child tracer (D-06 / D-17 nested writes).
Add writer TranslationWriter to RelationService next to bucket/tr, documented as the same optional plugin adapter CRUDService uses. In http.go relations(), after constructing RelationService, Lookup TranslationWriter exactly as crud() does (s.app.Lookup[TranslationWriter]) and assign it. Do not add a new route. deferred.go RelationService literals used only for pivot/file bind do not call fillChild; leave them writer-less unless a compile-time struct literal requires the new field (zero value nil is correct).
In CreateChild, before fillChild, call liftMLValues(ctx, cr.child, in.Body, "create", s.writer, tx) using the transaction already opened (so default/enabled codes match apply). Then fillChild as today (projectOperation + Fill on scalars). After tx.Create(child) succeeds and the child has a primary key, call applyMLTranslations(ctx, tx, s.writer, child, translations) before commitChildFiles / AfterCreate. After projectFullRecord, call hydrateMLRecord with s.writer and cr.child so the create response is the same map shape as CRUDService.save (Plan 05). Validation errors from lift (undeclared locale, non-string, missing default, nil writer with a nested map) remain 422 ValidationError and must abort before Fill.
Add modules/cabana/relation_child_ml_test.go in package cabana. Fixture: a parent model plus a hasMany child whose manage form declares mltext title, compiled like mlCompiled, with recordingWriter {en, pl}. Construct RelationService{DB: db, writer: writer} and call CreateChild with Body title `{en: Hello, pl: Witaj}`. Assert ProjectWritableFields on the unlifted copy is empty of nested maps, host child.Title is Hello, writer.attrs pl title is Witaj, default locale is not duplicated, and the returned RecordResult.Data title is the hydrated map containing both locales. A second CreateChild with locale de must 422 and must not insert a child row. Name the test TestRelationChildMLNestedSave.
Reuse recordingWriter; add TranslatedExact on it only if Plan 05 did not (it must already exist). Do not implement WR-01/WR-03. No new migration.</action>
<verify>
<automated>go test ./modules/cabana -count=1 -v -run '^(TestRelationChildMLNestedSave)$'</automated>
<fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; output lacks "--- PASS: TestRelationChildMLNestedSave".</fails_when>
</verify>
<acceptance_criteria>
- relations() assigns Lookup[TranslationWriter] onto RelationService.writer; no new HTTP endpoint exists.
- CreateChild lifts declared ML maps before fillChild/projectOperation; generic nested maps on non-ML fields remain dropped.
- Default locale fills the child host column; Polish reaches TranslationWriter inside the child transaction after PK.
- Undeclared locale de is 422 and creates no row.
- CreateChild response data for title is the hydrated locale map from Plan 05 hydrateMLRecord.
- TestRelationChildMLNestedSave is the named proof.
</acceptance_criteria>
<done>A relation-child create carries one multilingual title from Vue-shaped JSON through lift, Fill, writer, and hydrated response (D-17).</done>
</task>
<task type="auto">
<name>Task 2: Mirror lift/apply/hydrate on UpdateChild and ShowChild</name>
<files>modules/cabana/relation_child.go, modules/cabana/relation_child_ml_test.go, modules/cabana/README.md, docs/backend/forms.md</files>
<read_first>modules/cabana/relation_child.go (UpdateChild, ShowChild, fillChild), modules/cabana/field_ml.go (hydrateMLRecord), modules/cabana/README.md, docs/backend/forms.md (Markdown and multilingual fields)</read_first>
<action>Use the same D-06/D-17 contract on update and GET child.
UpdateChild: liftMLValues on in.Body with op update, cr.child, s.writer, and the open tx before fillChild; applyMLTranslations after Save when the child PK is present; hydrateMLRecord after projectFullRecord. ShowChild: after projectFullRecord on cr.childForm(), call hydrateMLRecord with s.writer so GET of a child is a locale map, not the host scalar. Writer-nil nested maps still 422 on update the same way as host CRUD. Authorization remains loadParent/loadChild 404-not-403; do not run the writer before those load checks.
Extend TestRelationChildMLNestedSave (subtests are fine) to UpdateChild a stored child with a new Polish string and ShowChild asserting the hydrated map. Keep host English on the column.
Document in cabana README and docs/backend/forms.md that relation-child create/update use the same lift/apply/hydrate path as controller save, still with no standalone translate-write route. Neutral blog/acme names. Do not name a consuming application. RelationService.writer is unexported; document the behaviour on TranslationWriter / child records, not a new exported type unless you export it (do not export writer).</action>
<verify>
<automated>go test ./modules/cabana -count=1 -v -run '^(TestRelationChildMLNestedSave)$' &amp;&amp; go test ./cmd/summer -count=1 -run 'TestDocsTree' &amp;&amp; go run ./cmd/summer docs:build --check</automated>
<fails_when>Non-zero exit; cabana output contains "--- FAIL", "--- SKIP", or "no tests to run", or lacks "--- PASS: TestRelationChildMLNestedSave"; docs checker reports stale identifiers, broken links, or a consuming-application name.</fails_when>
</verify>
<acceptance_criteria>
- UpdateChild persists nested ML maps the same way CreateChild does.
- ShowChild returns hydrated locale maps for declared ML fields when a writer is present.
- Writer still runs only after parent/child scope checks and after the child row has a PK.
- README/docs identifiers resolve; no consuming-application name.
</acceptance_criteria>
<done>Relation-child GET/update match host CRUD ML hydration and nested writes (WR-02 closed).</done>
</task>
<task type="auto">
<name>Task 3: Named unit/Vitest/cabana tests and the phase gate (last plan of 14.2.1)</name>
<files>modules/cabana/ml_test.go, modules/cabana/relation_child_ml_test.go, admin/tests/form/MLFields.test.ts, scripts/check-phase14.2.1.sh, .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md</files>
<read_first>modules/cabana/ml_test.go (TestMLNestedSave projection), admin/tests/form/MLFields.test.ts, scripts/check-phase14.2.1.sh (FRAMEWORK_REQUIRE, HIGH_THREATS, ALL_THREATS, --admin), .planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md, .planning/phases/14.2.1-translate-plugin/14.2.1-VERIFICATION.md (failed truths)</read_first>
<action>This is the last plan of the phase: named tests last (lean-mode rule). Do not add product features here.
Confirm TestMLNestedSave still expects the hydrated title map from Plan 05 and still proves lift-before-projection plus de rejection. Confirm TestMLHydration still passes. Confirm TestRelationChildMLNestedSave covers create, update, show, and de 422.
In MLFields.test.ts keep Plan 05 create-empty and GET-string cases and assert: one locale selector per ML field, broadcastMLLocale still switches mltext and mlmarkdown together, copy-from-locale works, and editablePayload sends every enabled locale as Record&lt;string,string&gt; including empty pl. Mounts provide FORM_ENABLED_LOCALES.
Add TestMLHydration and TestRelationChildMLNestedSave to FRAMEWORK_REQUIRE in scripts/check-phase14.2.1.sh. Add T-14.2.1-19, T-14.2.1-22, and T-14.2.1-23 to HIGH_THREATS and ALL_THREATS; add T-14.2.1-20 and T-14.2.1-21 to ALL_THREATS. Keep T-14.2.1-SC. Do not reuse T-14.2.1-01..18.
Append SECURITY-REVIEW rows for T-14.2.1-19 (hydrate only declared ML record fields, no D-11, no list hydration), T-14.2.1-22 (fillChild lift validates locales like save), T-14.2.1-23 (RelationService writer Lookup, no new route), plus medium T-14.2.1-20/21, each with source citation and the named test that fails if the mitigation is removed. No npm install (T-14.2.1-SC).
Run go vet and go test in summercms.go, the plugin, and the host. Run admin vitest and the phase gate.</action>
<verify>
<automated>go vet ./modules/cabana ./modules/surf ./cmd/summer &amp;&amp; go test ./modules/cabana -count=1 -v -run '^(TestMLHydration|TestMLNestedSave|TestRelationChildMLNestedSave|TestMLFieldTypes)$' &amp;&amp; npm --prefix admin test -- --run admin/tests/form/MLFields.test.ts &amp;&amp; go -C ../sm-translate-plugin vet ./... &amp;&amp; go -C ../sm-translate-plugin test ./... -count=1 -v -run '^(TestTranslateEndToEnd)$' &amp;&amp; go -C ../sm-grzybyfunkcjonalne-app vet ./... &amp;&amp; go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslate)$' &amp;&amp; bash scripts/check-phase14.2.1.sh --all</automated>
<fails_when>Non-zero exit; any Go output contains "--- FAIL", "--- SKIP", or "no tests to run"; output lacks "--- PASS: TestMLHydration", "--- PASS: TestMLNestedSave", "--- PASS: TestRelationChildMLNestedSave", "--- PASS: TestMLFieldTypes", "--- PASS: TestTranslateEndToEnd", or "--- PASS: TestBootUserTranslate"; Vitest reports no tests or failures; gate output lacks the exact line "Phase 14.2.1 gate passed" or reports a missing required test / unmapped high threat.</fails_when>
</verify>
<acceptance_criteria>
- FRAMEWORK_REQUIRE includes TestMLHydration and TestRelationChildMLNestedSave.
- Vitest proves create-empty, GET-scalar merge, synchronized selectors, copy-from, and full-locale payloads (failed SPA truth).
- Cabana tests prove host CRUD and relation-child nested maps are not dropped (failed nested-write truth, including WR-02).
- SECURITY-REVIEW maps T-14.2.1-19 through T-14.2.1-23 with named-test evidence; T-14.2.1-SC unchanged; IDs 01-18 are not reused.
- `bash scripts/check-phase14.2.1.sh --all` prints `Phase 14.2.1 gate passed`.
</acceptance_criteria>
<done>Phase 14.2.1 unit/Vitest/cabana named tests and the fail-closed gate are green with CR-01 and WR-02 closed.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Admin browser → relation child create/update | Untrusted nested multilingual JSON crosses the parent-scoped child write |
| Cabana RelationService → TranslationWriter | Same published adapter as host CRUD; no extra privilege surface |
| Test/gate → production claims | A skipped or missing named test must not close WR-02 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-14.2.1-22 | Tampering | fillChild / CreateChild / UpdateChild | high | mitigate | liftMLValues before projectOperation; same locale/string/default validation as CRUDService.save; 422 before Fill |
| T-14.2.1-23 | Elevation of Privilege | RelationService.writer | high | mitigate | Lookup only inside existing protect()'d child routes; apply after loadParent/loadChild and after child PK; no new endpoint |
| T-14.2.1-19 | Information Disclosure | ShowChild hydrateMLRecord | high | mitigate | Same Plan 05 helper; hydrate only declared ML fields on the child record payload |
| T-14.2.1-SC | Tampering | npm packages | high | mitigate | No package installation or version change |
ASVS L1: high threats are mitigated with named tests in Task 3. Reserved T-14.2.1-SC is unchanged.
</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. When a Journal-shaped form with mltext is available, confirm one locale selector per ML field listing en and pl on create (not a single English box), that switching one selector switches the others, and that save/reload still shows both locales. Relation-child ML UAT can wait for Phase 15 if this host has no child ML form yet.
</human-check>
</verification>
<success_criteria>
- WR-02 closed: relation-child create/update lift nested ML maps; ShowChild hydrates them.
- Both failed VERIFICATION truths have named Go and Vitest evidence.
- D-06 and D-17 are cited and covered on host CRUD (Plan 05) and relation-child (this plan).
- Phase gate passed; high threats T-14.2.1-19..23 mapped; no new migration or npm pin.
</success_criteria>
<output>
Create `.planning/phases/14.2.1-translate-plugin/14.2.1-06-SUMMARY.md` when done.
</output>