# 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 `<w:r>` / `<w:p>` 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 `<w:p>` / `<w:r>` runs and emits `<p>` / inline `<strong>` / `<em>` based on `<w:b>` / `<w:i>` 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 `<p>` / `<strong>` / `<em>` / `<br>`. ~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.
