From f7374a67cd87c5ccebfb5f09035407888445428c Mon Sep 17 00:00:00 2001 From: mAi Date: Fri, 22 May 2026 23:43:51 +0200 Subject: [PATCH] =?UTF-8?q?docs(submissions):=20t-paliad-238=20design=20?= =?UTF-8?q?=E2=80=94=20dedicated=20Submissions/Schrifts=C3=A4tze=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/design-submission-page-2026-05-22.md (~600 lines) — a dedicated submission-draft page at /projects/{id}/submissions/{code}/draft with sidebar variable editor + read-only HTML preview + .docx export. Reuses (resurrects from git) the deleted Slice 1 backend that t-paliad-230 ripped out — SubmissionVarsService (3677c81 + 1765d5e), in-house SubmissionRenderer (8ea3509), TemplateRegistry (3677c81). All four compile against today's services with zero API drift. New schema: paliad.submission_drafts keyed on (project_id, submission_code, user_id, name) with RLS via can_see_project. Three slices: A = schema + page + variables-only export against universal .docx; B = per-submission_code templates with fallback-chain registry; C = toggleable passages. Four material picks escalated to head in §11 (template authoring effort, paliad.documents row, server-vs-client preview, inter-user draft visibility). All other open questions defaulted to inventor (R) recommendations from task brief. No code. Read-only design phase per inventor → coder gate. --- docs/design-submission-page-2026-05-22.md | 668 ++++++++++++++++++++++ 1 file changed, 668 insertions(+) create mode 100644 docs/design-submission-page-2026-05-22.md diff --git a/docs/design-submission-page-2026-05-22.md b/docs/design-submission-page-2026-05-22.md new file mode 100644 index 0000000..87c8dce --- /dev/null +++ b/docs/design-submission-page-2026-05-22.md @@ -0,0 +1,668 @@ +# Design — Dedicated Submission/Schriftsätze page (t-paliad-238) + +**Author:** cronus (inventor) +**Date:** 2026-05-22 +**Issue:** m/paliad (mai task t-paliad-238) +**Branch:** `mai/cronus/inventor-dedicated` +**Status:** DESIGN READY FOR REVIEW +**Prior art:** `docs/design-submission-generator-2026-05-19.md` (t-paliad-215). This doc deepens that design rather than replacing it — every section below references the corresponding §x there when the shape is reused. + +--- + +## §0 TL;DR + +Today's "Schriftsätze" tab on the project detail page lists each filing-type rule and offers a one-click [Generieren] that streams a clean (format-only) `.docx` of the universal HL Patents Style template. There is no customization — variables, parties, dates, optional passages: none of it is filled in. The lawyer downloads the firm style, opens it in Word, and types everything by hand. + +This design adds a **dedicated submission page** at `/projects/{id}/submissions/{code}/draft` where the lawyer: + +1. Picks (or creates) a named **draft** for one (project, submission_code). +2. Sees a sidebar with every `{{placeholder}}` the merge engine knows, pre-filled from the project's data (parties, court, case number, dates, legal_source) — editable inline. Auto-saved. +3. Sees a read-only preview pane showing the merged document body as HTML. +4. Clicks **Export → .docx** to download a fully-merged Word file (template + project + lawyer overrides), ready to edit. + +Old [Generieren] button stays as the one-click "quick export with empty placeholders" path; the new [Bearbeiten] button next to it deep-links to the draft editor. Drafts persist as `paliad.submission_drafts` rows so the lawyer can come back next week, multiple drafts per submission code, RLS through `paliad.can_see_project`. + +**Reuses** the deleted Slice 1 backend (`SubmissionVarsService` from commit `3677c81`, in-house `SubmissionRenderer` from `8ea3509`, `patent_number_upc` helper from `1765d5e`) wholesale — those files are salvageable from git history and slot back in with one new service (`SubmissionDraftService`) and the new schema. **Reuses** the `internal/handlers/files.go` Gitea proxy pattern for per-submission_code templates in `m/mWorkRepo/templates/{FIRM_NAME}/{code}.docx` (chain: firm → base/code → base/family → skeleton) — same fallback chain m locked in the 2026-05-19 design (§5). + +Three slices: **A** = schema + new page + variables-only export against the universal `.dotm` (one slice; ships the editor end-to-end); **B** = per-`submission_code` `.docx` templates with the fallback-chain registry (template authoring is the bottleneck, not code); **C** = toggleable passages (boilerplate sections the lawyer can include/exclude before export). + +Read-only inventor design. Implementation gate is m's go/no-go on this doc through head. + +--- + +## §1 Premises verified live (2026-05-22) + +Anchored against the running paliad codebase + youpc Supabase, not against CLAUDE.md or memory. Every claim that load-bears the design was checked against the live system. + +| Claim | Verification | +|---|---| +| Today's `/api/projects/{id}/submissions` is **format-only**; no variables. | `internal/handlers/submissions.go:155-245`: handler fetches the universal `hl-patents-style.dotm` from the in-process `fileRegistry` cache, calls `services.ConvertDotmToDocx`, writes one `system_audit_log` row, streams. No project data merged. `frontend/src/client/submissions.ts` confirms the client side: POST → blob → download. The richer engine (`SubmissionVarsService` + `TemplateRegistry` + `SubmissionRenderer`) was reverted to format-only in commit `d86cac0` (t-paliad-230). | +| Original Slice 1 backend is preserved in git history and salvageable. | `git show 3677c81:internal/services/submission_vars.go` (484 LoC, 7-namespace placeholder bag), `git show 8ea3509:internal/services/submission_render.go` (in-house run-fragmentation-aware merger, ~315 LoC), `git show 3677c81:internal/services/submission_templates.go` (442 LoC fallback-chain registry), `git show 1765d5e:internal/services/submission_vars.go` (Slice 2 added `{{project.patent_number_upc}}` helper). All four files compile against today's services (`ProjectService`, `PartyService`, `UserService`, `branding.Name`) — no API drift since 2026-05-20. | +| `paliad.projects` carries everything the variable bag needs. | `internal/models/project.go:123` — `Title, Reference, CaseNumber, Court, PatentNumber, FilingDate, GrantDate, OurSide, InstanceLevel, ProceedingTypeID, ClientNumber, MatterNumber`. Unchanged since the original design. | +| `paliad.parties` carries party data scoped per project. | `internal/models/project.go:567` (`Party{ID, ProjectID, Name, Role, Representative, ContactInfo}`); `PartyService.ListForProject(ctx, userID, projectID)` already exists at `internal/services/party_service.go`. Visibility flows from `ProjectService.GetByID` → `can_see_project`. | +| `paliad.deadline_rules` published rows resolve by `submission_code`. | The same query the format-only handler uses (`internal/handlers/submissions.go:255-280`) — `lifecycle_state='published' AND is_active=true ORDER BY sequence_order LIMIT 1`. Today the corpus carries ~254 rules, ~214 published; covers DE-inf-LG, DE-inf-OLG, DE-inf-BGH, UPC-inf-CFI, DE-PatG-DPMA, DE-PatG-BPatG, EPO oppositions. | +| Migration tracker is at 106; file list extends to 118 (other-branch work) on the worktree. | `SELECT version FROM paliad.paliad_schema_migrations ORDER BY version DESC LIMIT 1` → 106. `ls internal/db/migrations/` → `…118_paliadin_aichat_conversation`. The next free number for *this* branch's migration is **119** (collisions only if another worktree commits 119 first, in which case the coder picks the next unused). | +| `paliad.can_see_project(uuid)` is the canonical RLS predicate. | Mig 055; every other table that gates on project visibility uses it. The new `submission_drafts` table follows the same pattern. | +| The Schriftsätze tab already exists on project detail. | `frontend/src/projects-detail.tsx:91` (`data-tab="submissions"`), section `#tab-submissions` at line 629. Empty / no-proceeding / table-of-rules states already wired. The page-level route `GET /projects/{id}/submissions` exists at `internal/handlers/handlers.go:472` and renders the same project detail page with the submissions tab pre-selected (`#tab-submissions` URL fragment + tab activation). **No new top-level route needed**; this design adds a *deeper* route `/projects/{id}/submissions/{code}/draft` and `/draft/{draftID}`. | +| `internal/handlers/files.go` carries the Gitea proxy + SHA-cache pattern. | Same template the original Slice 1 design lifted (in `templates/_base/de.inf.lg.erwidg.docx @ SHA 7f97b7f9` per memory). 5-min refresh, in-process cache, single-replica deployment. Reusable wholesale. | +| `lukasjarosch/go-docx` is NOT a deal we made. | The original 2026-05-19 design recommended it, but the shipped Slice 1 (commit `8ea3509`) went with an **in-house renderer** because the library refuses to replace sibling `{{a}} ./. {{b}}` placeholders in the same run. The in-house engine handles cross-run fragmentation in ~315 LoC. **This design reuses the in-house engine, no new Go dependency.** Memory entry `ca6de586` corroborates the engine decision verbatim. | +| `FIRM_NAME` defaults to "HLC", overridable. | `internal/branding.Name` (read once at process start). Templates land under `templates/HLC/...` for the default; the fallback chain handles per-firm overrides without code change. | +| The PoC Paliadin is owner-gated; the submission page is NOT. | `internal/services/paliadin.go:52` — `PaliadinOwnerEmail = "matthias.siebels@hoganlovells.com"`. This is the LLM-shell-out boundary, irrelevant to template-merge. Every paliad user who can see a project can edit its submission drafts. | +| The `entity-table` row contract is enforced. | `.claude/CLAUDE.md` → "Whole-card / whole-row click → use a JS row handler". The new draft list (when a submission_code has multiple drafts) follows the same pattern. | + +**Doc-vs-live conflicts found:** none material. `docs/project-status.md` doesn't mention t-paliad-215 or t-paliad-230 yet — that's a documentation lag, not a design risk. + +--- + +## §2 m's decisions (2026-05-22) + +The task brief (mai task t-paliad-238 description) carries inventor recommendations (R) for ten open questions. Per project CLAUDE.md inventor → head escalation policy: inventor defaults to (R) unless the pick is materially expensive or risk-bearing, in which case the head escalates to m. The matrix below records the (R) defaults this design adopts; the four genuinely-material picks are escalated to head in §11. + +| # | Question (from task brief) | Default adopted | Source | +|---|---|---|---| +| Q1 | Page location | **Deep page under project — `/projects/{id}/submissions/{code}/draft` and `…/draft/{draftID}`** | (R) — keeps URL self-describing and shareable with the project. | +| Q2 | State persistence | **Server-side draft, `paliad.submission_drafts` keyed on `(project_id, submission_code, user_id, name)` with autosave** | (R) — multiple drafts per code, named; resumable across sessions. | +| Q3 | Variable layer | **Resurrect `submission_vars.go` from commit `3677c81` + `1765d5e` (incl. `patent_number_upc`); resolve at export time** | (R) — proven shape, ~30 placeholders, 7 namespaces. | +| Q4 | Customization surface (UI) | **Structured sidebar (variable list, editable values) + read-only HTML preview pane** | (R) — sidebar drives the merge; preview reflects the result. | +| Q5 | Template source | **(a) per-`submission_code` `.docx` in `m/mWorkRepo/templates/{FIRM_NAME}/...` via Gitea proxy + fallback chain** | (R) — Word is the authoring surface lawyers know; mWorkRepo is the existing vehicle. | +| Q6 | Customization options beyond variables | **v1: variables only. Toggleable passages = Slice C** | (R) — citation insertion waits for the sources system. | +| Q7 | Migration / data shape | **See §4** | (R) — followed task brief's column list, refined for RLS + cascade + index. | +| Q8 | Export endpoint | **`POST /api/projects/{id}/submissions/{code}/drafts/{draftID}/export` → `.docx`** | (R) — reuses `ConvertDotmToDocx` strip-macros path but adds merge step. | +| Q9 | Schriftsätze tab integration | **Keep list + existing "Generieren" (one-click format-only); add new "Bearbeiten" button per row that deep-links to `/projects/{id}/submissions/{code}/draft`** | (R) — additive, no churn for users who only want the firm style. | +| Q10 | Variable-merge library | **In-house renderer from commit `8ea3509` (no new Go module)** | (R) — the 2026-05-19 design recommended `lukasjarosch/go-docx`, but the shipped Slice 1 reverted to an in-house engine because the library refused sibling placeholders. The in-house engine handles run-fragmentation in ~315 LoC and is already battle-tested against the corpus. | + +Inventor-defaulted (not in the (R) matrix; clear right answer): + +| # | Topic | Default | Reasoning | +|---|---|---|---| +| D1 | Authorization | `paliad.can_see_project(project_id)` only. No profession floor. | Matches every other write surface on a project. Draft is a Word doc; lawyer's substantive review happens downstream. | +| D2 | Missing-placeholder behaviour | `[KEIN WERT: {key}]` / `[NO VALUE: {key}]` in the rendered preview AND in the exported .docx, per `DefaultMissingMarker(lang)` from `8ea3509` | Same call the original design made. Lawyer sees the gap in Word, fixes in paliad, regenerates. Better than 400ing. | +| D3 | Editor surface for templates | Gitea-only for v1 (admin edits .docx in Word, commits to mWorkRepo). | Per the original design §5. A paliad-side uploader is a Slice C+ affordance only if Gitea round-trip is friction. | +| D4 | Audit trail | One `paliad.system_audit_log` row per export (`event_type='submission.exported'`) + one `paliad.project_events` row (`event_type='submission_exported'`) so the export surfaces in Verlauf / SmartTimeline. **Draft create/update do NOT audit** — autosave noise would dominate the log. | Mirrors the existing `submission.generated` row from `internal/handlers/submissions.go:333-339`. The Verlauf entry is the user-visible footprint; the system_audit_log entry is the admin-visible audit footprint. | +| D5 | Preview engine | Server-side merge → render to HTML for preview pane. Same `SubmissionRenderer` walks the .docx, but for the preview it strips `` / `` to plain HTML paragraphs (no styling beyond paragraph breaks + bold/italic carry-through). The export endpoint produces the real .docx with all formatting preserved. | Cheaper than client-side OOXML parsing; matches the read-only Q4 contract. The preview is a fidelity guide, not a WYSIWYG editor — final formatting comes from Word. | +| D6 | Draft autosave cadence | Debounce 500ms after the lawyer stops typing in a variable field; PATCH `…/drafts/{draftID}` with the diff. No optimistic locking — last-write-wins per (project, submission_code, user) draft, and we never multi-user a single draft (one row per `user_id`). | Standard textarea autosave; the data is the lawyer's own draft, not a shared object. | +| D7 | Variable contract surfacing | Each placeholder in the sidebar shows: dotted key (e.g. `project.case_number`), human label (DE/EN), current resolved value (from project state), and an editable override field. Override empty → fall back to project state at export. Override filled → carry the lawyer's value into the merge. | Lawyer never has to leave the page to fix a project-level field; AND lawyer can locally override (e.g. "Court is wrong on this draft, but I don't want to edit the project") without polluting project state. | +| D8 | Draft naming | First draft per (project, submission_code, user) auto-named "Entwurf 1" (DE) / "Draft 1" (EN). Lawyer can rename inline. Subsequent drafts auto-name "Entwurf 2", etc. The (project_id, submission_code, user_id, name) tuple is the unique constraint — two drafts can't share a name for the same submission of the same project. | Lets the lawyer keep a "submitted version" and a "scratch" version side-by-side. | + +--- + +## §3 Architecture overview + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Project detail (existing) — /projects/{id} with #tab-submissions │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ Schriftsätze tab │ │ +│ │ Klageerwiderung [Bearbeiten ↗] [Generieren ↓] │ │ +│ │ Schriftsatz der Klägerin (SoC) [Bearbeiten ↗] [Generieren ↓] │ │ +│ │ Replik [Bearbeiten ↗] [Generieren ↓] │ │ +│ └────────┬──────────────────────────────────────────┬─────────────────────┘ │ +│ │ "Bearbeiten" deep-link │ "Generieren" = │ +│ │ │ existing one-click │ +│ ▼ │ format-only export │ +└───────────┼───────────────────────────────────────────┼──────────────────────┘ + │ │ + ▼ │ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ NEW Submission draft page — /projects/{id}/submissions/{code}/draft │ +│ (lands on most-recent draft for this user, or creates "Entwurf 1") │ +│ │ +│ ┌──── Sidebar (sticky left) ────────┐ ┌──── Preview pane (right) ───────┐ │ +│ │ Schriftsatz: Klageerwiderung │ │ [HTML-rendered merge of │ │ +│ │ Entwurf 1 ▼ [+ Neuer Entwurf] │ │ template + variables + │ │ +│ │ ───────────────────────────────── │ │ overrides] │ │ +│ │ project.case_number │ │ │ │ +│ │ 2 O 123/25 [override?] │ │ Klage gegen die │ │ +│ │ parties.claimant.name │ │ Beklagte BMW AG, vertreten │ │ +│ │ BMW AG [override?] │ │ durch … │ │ +│ │ deadline.due_date │ │ │ │ +│ │ 2026-06-12 [override?] │ │ Sehr geehrte Damen und Herren, │ │ +│ │ ... │ │ │ │ +│ │ │ │ [KEIN WERT: project.our_side] │ │ +│ │ [✎ Bearbeiten] inline │ │ │ │ +│ └───────────────────────────────────┘ └──────────────────────────────────┘ │ +│ │ +│ [⬇ Als .docx exportieren] │ +└──────────────────────────────────┬───────────────────────────────────────────┘ + │ + ▼ + POST /api/projects/{id}/submissions/{code}/drafts/{draftID}/export + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ handlers/submission_drafts.go (NEW) │ +│ 1. Auth: ProjectService.GetByID → can_see_project │ +│ 2. Load draft row (RLS via project visibility) │ +│ 3. SubmissionVarsService.Build (project + parties + rule + next-Frist) │ +│ 4. Apply draft.overrides on top of bag │ +│ 5. TemplateRegistry.Resolve(code) — fallback chain → bytes + SHA │ +│ (Slice A: skips registry, fetches the universal .dotm directly) │ +│ 6. SubmissionRenderer.Render(bytes, bag, missingMarker) → .docx bytes │ +│ 7. Audit: system_audit_log + project_events │ +│ 8. Stream .docx with Content-Disposition: attachment │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +**No backend changes to today's Schriftsätze tab** — its list endpoint + one-click generate stay exactly as they are. The new page is additive. + +--- + +## §4 Schema (`paliad.submission_drafts`) + +Migration `119_submission_drafts.up.sql` (next free number on this branch; coder bumps if 119 is taken at write time). + +```sql +CREATE TABLE paliad.submission_drafts ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + project_id uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE, + submission_code text NOT NULL, + user_id uuid NOT NULL REFERENCES paliad.users(id) ON DELETE CASCADE, + name text NOT NULL, -- "Entwurf 1", lawyer-renameable + + overrides jsonb NOT NULL DEFAULT '{}'::jsonb, -- { "project.case_number": "2 O 999/25", ... } + -- empty value = "don't override, use bag" + -- present key = "use this verbatim" + + last_exported_at timestamptz, -- NULL until first export + last_exported_sha text, -- template SHA at last export (audit aid) + + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + CONSTRAINT submission_drafts_unique_per_user + UNIQUE (project_id, submission_code, user_id, name) +); + +CREATE INDEX submission_drafts_project_user_idx + ON paliad.submission_drafts (project_id, user_id, submission_code, updated_at DESC); + +ALTER TABLE paliad.submission_drafts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY submission_drafts_visible + ON paliad.submission_drafts + FOR ALL + USING (paliad.can_see_project(project_id)); + +-- updated_at trigger pattern (same shape as paliad.notizen, etc.). +CREATE TRIGGER submission_drafts_updated_at + BEFORE UPDATE ON paliad.submission_drafts + FOR EACH ROW EXECUTE FUNCTION paliad.tg_set_updated_at(); +``` + +**No changes to `paliad.deadline_rules`.** The task brief floats a `template_body_de/_en` Markdown column as alternative (b) — rejected per Q5 default. Templates stay in Gitea. + +**No changes to `paliad.documents`.** The original Slice 1 design wrote an `audit-only` row (`file_path NULL`) per generation; this design **does not** — `system_audit_log` + `project_events` carry the audit trail, and `paliad.documents` is reserved for actually-uploaded documents (a Phase 2 affordance per §13.5 of the 2026-05-19 design). If m wants the `documents` row for symmetry with future "I uploaded my edited version" UX, the coder can land it in a follow-up migration; it's not load-bearing for this design. + +### 4.1 RLS read-vs-write + +`can_see_project` is the only gate — the policy applies to FOR ALL operations. Anyone who can see a project can create / read / update / export drafts under that project, for their own user_id. Inter-user draft visibility (paralegal sees associate's drafts) is **NOT** a requirement in v1 — the unique constraint includes `user_id` and we don't expose a "drafts by other users on this project" endpoint. Multi-user collaboration on a single draft is out of scope. + +### 4.2 Down migration + +```sql +DROP TABLE IF EXISTS paliad.submission_drafts; +``` + +No data loss concern at design time — feature ships without legacy drafts. + +--- + +## §5 Service layer + +### 5.1 Resurrect from git (no new code) + +``` +internal/services/submission_vars.go RESURRECT from 3677c81 + Slice 2 patch from 1765d5e (patent_number_upc) +internal/services/submission_render.go REPLACE the format-only convert with the in-house renderer from 8ea3509. + KEEP the convert helper (ConvertDotmToDocx) — Slice A still needs it to + strip macros from the universal .dotm before the merge step runs. +internal/services/submission_templates.go RESURRECT from 3677c81 — Gitea-backed TemplateRegistry with fallback chain. + NOT wired in Slice A (universal .dotm only); wired in Slice B. +``` + +The three files were ~926 LoC + 350 LoC + 35 LoC patch when shipped. They compile against today's services (`ProjectService`, `PartyService`, `UserService`, `branding.Name`); zero API drift since their deletion. The resurrection is a copy-paste from `git show`, plus a one-line wiring in `cmd/server/main.go` + `internal/handlers/handlers.go`. + +### 5.2 New service — `SubmissionDraftService` + +```go +// internal/services/submission_draft_service.go (NEW, ~300 LoC) + +type SubmissionDraftService struct { + db *sqlx.DB + projects *ProjectService +} + +type SubmissionDraft struct { + ID uuid.UUID + ProjectID uuid.UUID + SubmissionCode string + UserID uuid.UUID + Name string + Overrides PlaceholderMap // jsonb → map[string]string + LastExportedAt *time.Time + LastExportedSHA *string + CreatedAt, UpdatedAt time.Time +} + +// List returns every draft for (project, submission_code, user) ordered by updated_at DESC. +// Visibility flows through projects.GetByID. +func (s *SubmissionDraftService) List(ctx context.Context, userID, projectID uuid.UUID, submissionCode string) ([]SubmissionDraft, error) + +// Get returns a single draft by ID, gated on project visibility. +func (s *SubmissionDraftService) Get(ctx context.Context, userID, draftID uuid.UUID) (*SubmissionDraft, error) + +// EnsureLatest returns the user's most-recently-updated draft for (project, submission_code). +// Creates "Entwurf 1" / "Draft 1" if none exists. Idempotent on repeat calls. +func (s *SubmissionDraftService) EnsureLatest(ctx context.Context, userID, projectID uuid.UUID, submissionCode, lang string) (*SubmissionDraft, error) + +// Create makes a new draft with an auto-incremented "Entwurf N" name (lawyer can rename via Update). +func (s *SubmissionDraftService) Create(ctx context.Context, userID, projectID uuid.UUID, submissionCode, lang string) (*SubmissionDraft, error) + +// Update patches the draft. Permitted fields: name, overrides. last_exported_* is set by the export handler. +func (s *SubmissionDraftService) Update(ctx context.Context, userID, draftID uuid.UUID, patch DraftPatch) (*SubmissionDraft, error) + +// Delete archives the draft. ON DELETE CASCADE from project takes care of project-archival fallout. +func (s *SubmissionDraftService) Delete(ctx context.Context, userID, draftID uuid.UUID) error + +// MarkExported updates last_exported_at + last_exported_sha after a successful export. +func (s *SubmissionDraftService) MarkExported(ctx context.Context, draftID uuid.UUID, sha string) error +``` + +`DraftPatch` is a `struct { Name *string; Overrides *PlaceholderMap }` — nil pointer = "no change", non-nil = "set to this". `Overrides` is replace-semantics (lawyer's sidebar sends the full map); the service does not merge. + +### 5.3 Wiring + +```go +// cmd/server/main.go (additions, no replacements) +draftSvc := services.NewSubmissionDraftService(db, projectSvc) +varsSvc := services.NewSubmissionVarsService(db, projectSvc, partySvc, userSvc) +// Slice B only: +tplRegistry := services.NewTemplateRegistry(os.Getenv("GITEA_TOKEN"), branding.Name) +``` + +No new env var. `GITEA_TOKEN` is already documented in CLAUDE.md and used by `internal/handlers/files.go`. + +--- + +## §6 UI surface + +### 6.1 Page layout + +`/projects/{id}/submissions/{code}/draft` lands on the user's latest draft for that (project, code). `/projects/{id}/submissions/{code}/draft/{draftID}` opens a specific draft (e.g. "Entwurf 2"). Both routes call the same renderer + client bundle; the difference is which draft `EnsureLatest` vs `Get` returns. + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ← Zurück zum Projekt: BMW AG ./. Bosch GmbH │ +│ Schriftsatz: Klageerwiderung (DE.ZPO.276.1) • Entwurf 1 │ +│ [⬇ Als .docx exportieren] │ +├────────── Sidebar (sticky) ────────────────┬─────── Preview ────────────────┤ +│ Entwurf 1 ▼ │ [HTML-rendered merge] │ +│ • Entwurf 1 (zuletzt 23 Mai 2026) │ │ +│ • Entwurf 2 (zuletzt 20 Mai 2026) │ An das Landgericht München I │ +│ • [+ Neuer Entwurf] │ Pacellistr. 5 │ +│ │ 80333 München │ +│ ───────────────────────────────────────── │ │ +│ Variablen │ In der Sache │ +│ firm.name HLC │ │ +│ project.case_number 2 O 123/25 [✎] │ BMW AG, vertreten durch … │ +│ project.court LG München I [✎] │ — Klägerin — │ +│ parties.claimant.name BMW AG [✎] │ │ +│ parties.defendant.name Bosch GmbH [✎] │ gegen │ +│ parties.defendant.representative │ │ +│ Dr. Maria Schmidt [✎] │ Bosch GmbH … │ +│ deadline.due_date 2026-06-12 [✎] │ — Beklagte — │ +│ rule.legal_source_pretty │ │ +│ § 276 Abs. 1 ZPO ✓ │ Sehr geehrte Damen und Herren,│ +│ … │ │ +│ │ [KEIN WERT: project.our_side] │ +│ ───────────────────────────────────────── │ │ +│ [Entwurf umbenennen] [Entwurf löschen] │ │ +└────────────────────────────────────────────┴────────────────────────────────┘ +``` + +Sidebar grouping (top-to-bottom, locale-aware labels): + +1. **Schriftsatz** (rule.* — read-only metadata: name, legal_source_pretty, primary_party) +2. **Mandanten & Parteien** (parties.*) +3. **Verfahren** (project.* — case_number, court, patent_number, patent_number_upc, our_side, …) +4. **Frist** (deadline.* — due_date, computed_from) +5. **Kanzlei & Datum** (firm.*, user.*, today.*) + +Each placeholder row shows: human label (DE/EN), resolved value, edit icon. Click [✎] expands an inline text input pre-filled with the current value. Blur or Enter → debounced autosave (500ms). Empty override → revert to bag value. + +### 6.2 Preview pane + +Read-only HTML. The same `SubmissionRenderer.Render(...)` call that produces the .docx for export ALSO produces a sidecar HTML preview (the in-house renderer walks `` / `` runs and emits `

` / inline `` / `` based on `` / `` flags). Preview re-renders on every autosave round-trip (cheap: server-side merge, ~10ms for a 5-page brief). Loading state: ghost-skeleton paragraphs during the round-trip. + +This is the SLIGHTLY non-trivial coder piece: the in-house renderer today emits .docx; the coder adds a parallel `RenderHTML` path that walks the same tree but emits HTML. Same regex, same run-merge logic, different writer. Coder estimates ~120 LoC on top of the resurrected `submission_render.go`. + +### 6.3 Routing + handlers + +``` +GET /projects/{id}/submissions/{code}/draft → page (lands on latest, creates if none) +GET /projects/{id}/submissions/{code}/draft/{draftID} → page (specific draft) +GET /api/projects/{id}/submissions/{code}/drafts → list drafts for current user +POST /api/projects/{id}/submissions/{code}/drafts → create new draft → returns row + redirect target +GET /api/projects/{id}/submissions/{code}/drafts/{draftID} → single draft + resolved bag + HTML preview +PATCH /api/projects/{id}/submissions/{code}/drafts/{draftID} → update name / overrides; returns new preview +DELETE /api/projects/{id}/submissions/{code}/drafts/{draftID} → delete +POST /api/projects/{id}/submissions/{code}/drafts/{draftID}/export + → .docx download (application/vnd.openxmlformats-officedocument.wordprocessingml.document) +``` + +Page-route handler is `handleSubmissionDraftPage` in `internal/handlers/submission_drafts.go` — calls `handleProjectsDetailPage` shape (returns HTML for an SSR layout) with a deep-route flag, OR ships an entirely new TSX page module. Inventor default: **new TSX page** at `frontend/src/submission-draft.tsx` rendering its own layout. Lighter than retrofitting the existing project-detail page with conditional panels, and the URL semantics demand it (`#tab-submissions` is the tab; `/draft/{id}` is a distinct page). + +### 6.4 Schriftsätze tab — additive change + +```diff +- // Each row: [Generieren ↓] ++ // Each row: [Bearbeiten ↗] [Generieren ↓] +``` + +`[Bearbeiten ↗]` → `window.location.href = "/projects/{id}/submissions/{code}/draft"`. `[Generieren ↓]` stays as today (one-click format-only export of the universal .dotm). For users who want zero-config "give me a clean firm style template", `[Generieren]` is the path; for users who want a merged draft, `[Bearbeiten]` is the path. + +Per the `.entity-table` row contract in CLAUDE.md, the row itself becomes clickable (navigates to `/draft`), with the `Generieren` button stopping propagation. The `entity-table--readonly` modifier is removed. + +--- + +## §7 Variable contract (v1 placeholder set) + +Reproduced from the resurrected `submission_vars.go` (commits `3677c81` + `1765d5e`). The sidebar's "Variablen" section enumerates this list in the exact same order as `addProjectVars` / `addPartyVars` / etc., grouped per §6.1. + +``` +firm.name — branding.Name (HLC or FIRM_NAME override) +firm.signature_block — empty in v1 (Phase 2 affordance) + +today — 2026-05-22 (ISO, Europe/Berlin) +today.iso — ISO short +today.long_de — "22. Mai 2026" +today.long_en — "22 May 2026" + +user.display_name — paliad.users.display_name +user.email — paliad.users.email +user.office — paliad.users.office + +project.title — paliad.projects.title +project.reference — paliad.projects.reference +project.case_number — paliad.projects.case_number +project.court — paliad.projects.court +project.patent_number — DE/inline form "EP 1 234 567 B1" +project.patent_number_upc — UPC parenthesised form "EP 1 234 567 (B1)" (Slice 2 helper, 1765d5e) +project.filing_date — ISO date +project.grant_date — ISO date +project.our_side — claimant | defendant +project.our_side_de — "Klägerin" | "Beklagte" +project.our_side_en — "Claimant" | "Defendant" +project.instance_level — lg | olg | bgh | cfi | … +project.client_number — paliad.projects.client_number +project.matter_number — paliad.projects.matter_number +project.proceeding.code — e.g. "de.inf.lg" +project.proceeding.name — locale-aware (DE: Verletzungsklage am Landgericht) +project.proceeding.name_de — explicit DE +project.proceeding.name_en — explicit EN + +parties.claimant.name — first paliad.parties row with role='claimant' +parties.claimant.representative +parties.defendant.name — first row with role='defendant' +parties.defendant.representative +parties.other.name — first non-claimant/defendant row +parties.other.representative + +rule.submission_code — "de.inf.lg.erwidg" +rule.name — locale-aware ("Klageerwiderung" / "Statement of Defence") +rule.name_de +rule.name_en +rule.legal_source — "DE.ZPO.276.1" +rule.legal_source_pretty — "§ 276 Abs. 1 ZPO" / "Section 276(1) ZPO" +rule.primary_party — claimant | defendant | court | both +rule.event_type — filing | hearing | decision + +deadline.due_date — ISO of next pending deadline for this rule on this project +deadline.due_date_long_de — "12. Juni 2026" +deadline.due_date_long_en — "12 June 2026" +deadline.original_due_date — ISO if extended +deadline.computed_from — anchor description (e.g. "Klagezustellung am 14.05.2026 + 6 Wochen") +deadline.title — deadline.title +deadline.source — "rule" | "manual" | … +``` + +Variable bag construction is `SubmissionVarsService.Build(ctx, in)` — exactly the function in `3677c81`'s `submission_vars.go`, no changes. + +### 7.1 Override semantics + +The lawyer's `overrides` map (jsonb in `submission_drafts`) shadows the bag at export time: + +```go +bag := varsSvc.Build(ctx, ...).Placeholders // ~30 keys, resolved from project state +for k, v := range draft.Overrides { // lawyer's edits + if v == "" { + delete(bag, k) // empty override means "force missing marker" + } else { + bag[k] = v // non-empty override replaces + } +} +docx := renderer.Render(templateBytes, bag, missingMarker(lang)) +``` + +Edge case: lawyer types empty string into a field that was resolved from the project. Decision: empty override forces the `[KEIN WERT: …]` marker. Lawyer's intent ("blank this out, I'll fill manually in Word") is honoured rather than silently falling back to project state. Sidebar UX: empty override field is annotated "→ [KEIN WERT: …]" so the lawyer sees the consequence before exporting. + +--- + +## §8 Template authoring (mWorkRepo layout, naming, fallback chain) + +### 8.1 Slice A — universal .dotm only + +Slice A merges the variable bag into the same universal HL Patents Style .dotm that today's format-only convert ships. **The .dotm body must carry `{{placeholder}}` tokens** — currently it doesn't (it's a firm style template, not a per-submission template). m has two ways to seed Slice A: + +- **8.1.a** — author one universal template (`m/mWorkRepo/templates/_base/_universal.docx` or similar) with `{{firm.name}}`, `{{rule.name}}`, `{{project.case_number}}`, `{{parties.claimant.name}}`, etc. The merge engine fills these and outputs a draft that's still a generic letter shape but pre-populated. +- **8.1.b** — author one Klageerwiderung-shaped template (`m/mWorkRepo/templates/HLC/de.inf.lg.erwidg.docx`) and route Slice A's export to that path for `submission_code='de.inf.lg.erwidg'`, with a hard-coded fall-back to `_universal.docx` for any other code. This is essentially Slice A + B's first template — wins both rounds. + +Inventor recommendation: **8.1.b**. Strictly more useful, identical engine code, identical mWorkRepo round-trip. The Slice A → Slice B transition is then "add more templates", not "rewire the resolver". + +### 8.2 Slice B — fallback-chain registry + +Layout reproduced from the 2026-05-19 design §5.1: + +``` +m/mWorkRepo (existing repo, already proxied) +└── templates/ + ├── HLC/ # FIRM_NAME-keyed override dir + │ ├── de.inf.lg.erwidg.docx # Slice A target (per 8.1.b above) + │ ├── de.inf.lg.klage.docx # Slice B addition + │ ├── upc.inf.cfi.soc.docx # Slice B addition + │ └── upc.inf.cfi.sod.docx # Slice B addition + ├── _base/ # Cross-firm baseline + │ ├── de.inf.lg.erwidg.docx # base equivalent (Slice C+) + │ ├── de.inf.lg.docx # proceeding-family fallback + │ ├── upc.inf.cfi.docx + │ ├── _skeleton.docx # ultra-generic fallback + │ └── _universal.docx # the v1 Slice A "any code" template + └── README.md # placeholder reference for template authors +``` + +Naming: `{submission_code}.docx`. Family fallback uses the first three dot-segments (`de.inf.lg` from `de.inf.lg.erwidg`). Skeleton is the ultra-generic fallback (letterhead + party block + court address + signature stub). + +### 8.3 Lookup algorithm + +```go +// services/submission_templates.go (resurrected from 3677c81) +func (r *TemplateRegistry) candidates(submissionCode string) []string { + family := familyOf(submissionCode) + out := []string{ + fmt.Sprintf("templates/%s/%s.docx", r.firmName, submissionCode), + fmt.Sprintf("templates/_base/%s.docx", submissionCode), + } + if family != "" && family != submissionCode { + out = append(out, fmt.Sprintf("templates/_base/%s.docx", family)) + } + out = append(out, "templates/_base/_skeleton.docx") + return out +} +``` + +Gitea proxy: same `internal/handlers/files.go` shape. 5-min SHA refresh, in-process cache, `GITEA_TOKEN` for auth. The original `submission_templates.go` already implements this end-to-end; the coder re-applies it from `git show 3677c81`. + +### 8.4 No template at all — Slice A vs Slice B + +Slice A: the universal template always resolves; `ErrNoTemplate` is impossible. + +Slice B: if every candidate in the fallback chain 404s, the handler returns 503 + `"Vorlagen-Repository nicht erreichbar"` in the UI (same handling as the original Slice 1 design §5.4). Since the chain ends at `_skeleton.docx`, this only fires when the mWorkRepo itself is misconfigured. + +### 8.5 Template authoring task lands outside this design + +Inventor flags but does not assign: HLC must author the per-submission_code `.docx` templates. Slice A's `_universal.docx` is one document. Slice B adds Klageerwiderung, Klageerhebung, SoC, SoD, … iteratively. **Template authoring runs in parallel with engine code**; the coder ships the engine, m + HLC ships the templates. The two converge before the slice closes. + +This is the m-escalated piece (see §11): without per-submission templates, Slice B is engine-only. + +--- + +## §9 Slice plan + +### Slice A — schema + new page + variables-only export against universal .docx + +Ships the editor end-to-end with one template. + +| Deliverable | Files | +|---|---| +| Migration 119 — `submission_drafts` table + RLS + trigger | `internal/db/migrations/119_submission_drafts.{up,down}.sql` | +| `SubmissionVarsService` resurrected | `internal/services/submission_vars.go` (from `3677c81` + Slice 2 patch `1765d5e`) | +| `SubmissionRenderer` resurrected with new `RenderHTML` | `internal/services/submission_render.go` (from `8ea3509`); adds `RenderHTML(...) string` for preview | +| `SubmissionDraftService` | `internal/services/submission_draft_service.go` (NEW) | +| Handlers (page + 7 API endpoints) | `internal/handlers/submission_drafts.go` (NEW) | +| Wiring | `cmd/server/main.go`, `internal/handlers/handlers.go` | +| Page TSX | `frontend/src/submission-draft.tsx` (NEW) | +| Client bundle | `frontend/src/client/submission-draft.ts` (NEW) | +| Schriftsätze tab update | `frontend/src/projects-detail.tsx` (rows get [Bearbeiten]), `frontend/src/client/submissions.ts` (handler) | +| i18n | new keys under `projects.detail.submissions.draft.*` and `submissions.draft.*` (page-level) | +| One template at `m/mWorkRepo/templates/_base/_universal.docx` (8.1.b → also `templates/HLC/de.inf.lg.erwidg.docx`) | mWorkRepo, separate PR by m | +| Tests | `internal/services/submission_render_test.go` (resurrected + RenderHTML), `internal/services/submission_vars_test.go` (round-trip), handler smoke | + +Acceptance: + +1. Opening `/projects/{id}/submissions/de.inf.lg.erwidg/draft` lands on the user's latest draft (or creates "Entwurf 1"). +2. Sidebar renders ~30 placeholders, pre-filled from project state. +3. Editing a sidebar value autosaves within 500ms and updates the preview pane. +4. Multiple drafts per (project, code, user) supported; switcher in sidebar. +5. Clicking "Als .docx exportieren" downloads a merged `.docx` (universal template + project + lawyer overrides). +6. `system_audit_log` row appears on export (`event_type='submission.exported'`). +7. `project_events` row appears on export and surfaces in Verlauf. +8. RLS: caller without `can_see_project` gets 404 on the page and 404 on every draft API. +9. Schriftsätze tab on project detail shows [Bearbeiten] alongside [Generieren]. +10. `go build ./... && go vet ./... && go test ./... && bun run build` clean. + +### Slice B — per-submission_code templates + fallback chain + +Engine is unchanged from Slice A; this slice wires `TemplateRegistry` into the export endpoint and lights up per-code templates. + +| Deliverable | Files | +|---|---| +| `TemplateRegistry` resurrected | `internal/services/submission_templates.go` (from `3677c81`) | +| Handler swaps Slice A's `fetchHLPatentsStyleBytes` for `templateRegistry.Resolve(code)` | `internal/handlers/submission_drafts.go` | +| `has_template` boolean per row in Schriftsätze tab list (today: unconditionally true; under Slice B: depends on registry probe) | `internal/handlers/submissions.go` | +| Templates authored in mWorkRepo: at least Klageerwiderung + Klageerhebung + SoC + SoD | mWorkRepo PR by m | +| Tests for fallback chain | `internal/services/submission_templates_test.go` (resurrect from history if it existed; otherwise new) | + +Acceptance: + +1. Pushing `m/mWorkRepo/templates/HLC/upc.inf.cfi.soc.docx` makes the SoC draft page resolve that template within 5 min (or instantly via `POST /api/files/refresh`). +2. `has_template=false` rows in the Schriftsätze tab show [Keine Vorlage] instead of [Bearbeiten]/[Generieren]. Existing list ordering preserved. +3. `last_exported_sha` on `submission_drafts` records which SHA the lawyer exported against. +4. Misconfigured repo (every fallback 404s) → 503 with clear error. + +### Slice C — toggleable passages + +Lawyer can include/exclude boilerplate sections before export. + +| Deliverable | Notes | +|---|---| +| `passages` jsonb column on `submission_drafts` | `migration 120` (or whatever's free at land time): `passages jsonb NOT NULL DEFAULT '{}'::jsonb` — `{"intro": true, "patent_validity_attack": false, "non_infringement": true}`. | +| Template syntax for passage blocks | `{{#passage intro}}…{{/passage}}` — start/end markers, merger drops the block when the corresponding `passages.{key}` is false. The in-house renderer's run-fragmentation handling extends to the new tokens cleanly. | +| Sidebar UI | "Passagen" group above "Variablen", per-passage toggle (on by default), help text per passage. | +| Template author API | `templates/README.md` documents the passage syntax + a worked example. | + +Acceptance: turning off `non_infringement` in the sidebar of a Klageerwiderung draft removes the corresponding section from the exported .docx; preview reflects immediately. + +Slices D+ (not detailed here): citation insertion from the sources system (waits for that surface), per-firm template overrides (registry already supports this), `/admin/submission-templates` variable contract sidebar. + +--- + +## §10 Out of scope + +- AI-drafted prose (the 2026-05-19 design §11 sketch; still deferred). +- PDF export. v1 ships `.docx` only; the lawyer's Word does the PDF step. +- Multi-user collaboration on a single draft. Each draft is owner-scoped (`user_id`). +- Real-time co-editing. Last-write-wins per draft; no operational transforms. +- An in-paliad WYSIWYG editor for `.docx` content. Preview is read-only; final edits happen in Word. +- A paliad-side template uploader. Gitea stays as the editor for templates until lawyers complain about the round-trip. +- Translation of templates DE↔EN. Templates are mono-locale; the variable bag is bilingual. +- Citation insertion from the sources system. Waits for the sources surface m parked. +- Frist-detail "Exportieren" button. The submission page is reachable only from the project's Schriftsätze tab in v1; a Frist-level deep-link is a Slice D+ affordance. +- Validation of the rendered draft against any legal rule. The engine produces text; the lawyer's substantive review is downstream. +- Sending the draft to court / e-filing. The lawyer downloads and handles transmission outside paliad. + +--- + +## §11 Material picks escalated to head + +Per project CLAUDE.md inventor → head policy, the four picks below carry enough cost or risk to deserve head's read. Head ratifies (or escalates to m) before the coder shift starts. + +### Q-E1 — Template authoring effort + +Slice A needs at least one custom-authored template (`_universal.docx` or `de.inf.lg.erwidg.docx`) carrying `{{placeholder}}` tokens. Slice B needs four more (Klageerhebung, SoC, SoD, Erwiderung). The engine ships independently of template content, but the feature is unfinished without lawyer-authored templates. + +**Inventor pick:** ship Slice A with **one** lawyer-authored template (8.1.b: `templates/HLC/de.inf.lg.erwidg.docx`) + the universal fallback. m + HLC owns the authoring; the coder owns the engine. Slices A and template-1 land together. + +**Material because:** without a template, the feature looks broken in user testing. Head decides: does m commit to authoring or reviewing the first template before Slice A merges, or does Slice A merge engine-only and we accept the "format-only export with placeholders" intermediate state for a week? + +### Q-E2 — `paliad.documents` row on export + +The original Slice 1 design wrote an audit-only `paliad.documents` row (`file_path NULL`, `doc_type='generated_submission'`) per generation, on the theory that "Documents" would become the canonical listing UI. This design defers that. + +**Inventor pick:** **no** `paliad.documents` write. `system_audit_log` + `project_events` carry the audit trail. The `documents` table is reserved for actually-uploaded documents (Phase 2 of the broader docs roadmap). + +**Material because:** if head agrees, we skip a column repurpose (`ai_extracted` jsonb being used for generation provenance — the 2026-05-19 design noted this was ugly). If head disagrees, the coder lands the row inside Slice A. + +### Q-E3 — Preview render — server or client? + +Server-side: `RenderHTML(...)` on the in-house renderer, round-trip per autosave. Cheaper to build, costs ~10ms server-side per keystroke (debounced 500ms). + +Client-side: ship the merged document body as JSON of paragraph runs, render in TS. Faster preview, harder to build (parallel render path in TS), and **diverges** the preview from the export shape (export still goes server-side). + +**Inventor pick:** **server-side**. Single source of truth for the merge logic. The 500ms debounce already absorbs the round-trip; a 10ms server merge plus 50ms HTTP RTT is sub-perceptible. + +**Material because:** if head wants the client-side preview for fully-offline draft editing, the coder needs a TS port of `substituteInDocumentXML`. Bigger build, but no round-trip latency on every keystroke. + +### Q-E4 — Inter-user draft visibility + +Today's design: each user sees only their own drafts. If two associates on the same project both draft a Klageerwiderung, they don't see each other's drafts (each has their own row). + +**Inventor pick:** **owner-scoped (status quo of this design)**. The unique constraint includes `user_id`; the `List` endpoint filters by current user. + +**Material because:** if head wants project-team visibility ("paralegal sees associate's draft for review"), the unique constraint shifts to `(project_id, submission_code, name)` (drop `user_id`), the RLS already covers the read path (`can_see_project`), and `submission_drafts` becomes a project-team resource. **This is a Phase-shape change** — the lawyer model differs. Inventor flags it because the change is cheap to make now (one column + one constraint) and expensive to make later (drafts already accumulate per-user). Head's call. + +--- + +## §12 Implementation notes + +For the coder, not for head. + +- **Resurrection is `git show`, not "re-write".** The four file revisions (`3677c81:internal/services/submission_vars.go`, `1765d5e:internal/services/submission_vars.go` for the Slice 2 patch, `8ea3509:internal/services/submission_render.go`, `3677c81:internal/services/submission_templates.go`) can be applied via `git checkout 3677c81 -- internal/services/submission_vars.go` etc. The coder should verify each compiles against today's `cmd/server/main.go` wiring before applying. +- **Renderer's `RenderHTML` is new.** The .docx walker today emits OOXML bytes; the HTML emitter walks the same tree and emits `

` / `` / `` / `
`. ~120 LoC on top of the resurrected file. Same regex (`placeholderRegex`), same run-merge logic, different writer. +- **Sidebar variable schema needs a label table.** The variable contract from §7 is keyed by dotted paths; the sidebar UI needs DE/EN labels per key. Coder adds `services/submission_var_labels.go` with a `map[string]struct{LabelDE, LabelEN, HelpDE, HelpEN}` for the ~30 keys. (Mirrors `internal/services/email_template_variables.go` shape — same lawyer-facing pattern paliad already ships at `/admin/email-templates`.) +- **Autosave race.** The lawyer types fast → multiple PATCHes in flight. Coder uses a request-ID-debouncing pattern on the client (cancel in-flight PATCH when a new one starts) and last-write-wins on the server. No version column on the draft row in v1. +- **Empty-override semantics in the jsonb.** `overrides = {"project.case_number": ""}` means "force missing marker". `overrides = {}` (key absent) means "fall back to bag". The service code distinguishes — careful with `omitempty`. +- **i18n key audit.** Add `projects.detail.submissions.action.edit`, `submissions.draft.title`, `submissions.draft.export`, `submissions.draft.sidebar.{firm,project,parties,deadline,user}.group`, `submissions.draft.rename`, `submissions.draft.delete`, `submissions.draft.new`, etc. Roughly 35 new keys in DE + EN. +- **`entity-table` row contract.** Schriftsätze tab today carries `entity-table--readonly`. Slice A removes that modifier and adds a row-click handler that navigates to `/projects/{id}/submissions/{code}/draft`, skipping clicks on the inner [Generieren] button. Matches the pattern in `frontend/src/client/checklists.ts`, `client/projects-detail.ts`, `client/deadlines.ts`. +- **Migration 119 may collide.** Other worktrees (paliadin aichat, mig 118) may land 119 before this branch merges. Coder verifies at land time; bump to the next free number if needed. + +--- + +## §13 Acceptance gate + +Per inventor SKILL.md + project CLAUDE.md: this design needs head's go/no-go before any coder is hired. After head ratifies (with or without escalating §11 to m): + +- The head decides whether to hire the same worker as `/mai-coder` with this design as the brief, or a fresh coder. +- A coder shift takes this doc as the spec, ships Slice A, opens a PR (no self-merge). +- Slices B and C are SEPARATE tasks — not auto-spawned. + +Inventor parks here.