# Design — Submission generator v2 ("Composer")

**Author:** cronus (inventor)
**Date:** 2026-05-26
**Task:** t-paliad-312
**Issue:** m/paliad#141
**Branch:** `mai/cronus/inventor-prd-for`
**Status:** ALL DESIGN QUESTIONS RATIFIED — final, ready for Slice A coder shift
**Prior art:**
- `docs/design-submission-generator-2026-05-19.md` (t-paliad-215, Slice 1: template-merge engine, fallback chain, audit shape)
- `docs/design-submission-page-2026-05-22.md` (t-paliad-238, Slice A: dedicated draft editor at `/projects/{id}/submissions/{code}/draft`, sidebar + preview, autosave)

This design deepens — it does **not** replace — both prior docs. Every contract they locked in (placeholder grammar, `{{rule.X}}` aliases, audit-log shape, fallback chain, in-house renderer) is preserved. v2 adds sections, swappable bases, in-app prose editing, and building blocks **on top of** that substrate.

---

## § m's decisions (2026-05-26)

m ratified the 12 §11 questions via AskUserQuestion (paliadin override per instruction msg #2391 — "interactive ratification, no doc round-trip"). Picks below; where m deviated from the inventor's recommendation, the design body has been updated inline. The original §11 entries stay as the historical record.

| # | Topic | m's pick | vs Recommended? |
|---|---|---|---|
| Q1 | Section content storage | Separate `submission_sections` table | ✅ matches |
| Q2 | Building block insert semantics | **SIMPLIFY** — m: "I don't even understand the question, that sounds a bit overengineered" → drop lineage/refresh machinery. Building blocks are plain text snippets pasted into sections. No `building_block_id`, no `_versions` table, no diff/merge UI. | ⚠️ rescope |
| Q3 | Base storage | Gitea-backed body + thin DB row | ✅ matches |
| Q4 | Editor surface | contentEditable + minimal toolbar + Markdown source-of-truth, in-house serializer | ✅ matches |
| Q5 | Render pipeline | Section anchors in base + in-house MD → OOXML walker (pure Go) | ✅ matches |
| Q6 | Migration of existing 11 drafts | **Auto-upgrade all on migration apply** — every draft auto-seeded with firm-default base + sections derived from spec. v1 fallback path stays compiled in for safety. | ⚠️ deviation — risk noted in §14 |
| Q7 | Multi-language storage | Split `content_md_de` + `content_md_en` from day 1 | ✅ matches |
| Q8 | Section defaults per submission_code | Go map (`submission_section_defaults.go`) | ✅ matches |
| Q9 | Building block visibility tiers | 4 tiers (private / team / firm / global) | ✅ matches |
| Q10 | `*_auto` section editability | **Fully editable** — lawyer can override auto-rendered Rubrum / letterhead / signature text. No special read-only kind. | ⚠️ deviation — risk noted in §14 |
| Q11 | Preview pane default | Collapsed by default, `[Vorschau ▾]` toggle expands | ✅ matches |
| Q12 | Refresh-from-library trigger | **Moot** — superseded by Q2's simplification (no lineage tracked, so no "refresh" affordance exists). |  — |

### Rescope: Q2 + Q12 simplification (combined effect)

The Q2 + Q12 picks collapse the building-block machinery to its simplest form:

- **No** `building_block_id` / `building_block_version` columns on `submission_sections`.
- **No** `submission_building_block_versions` table.
- **No** "refresh from library" button, no diff view, no Slice G.
- Building blocks become a one-way clipboard: the lawyer browses the library, clicks "Einfügen", the text appears at the cursor, end of story. The section row keeps no record of where the text came from.
- The library admin editor (Slice C) still has its own version history (retention-20, like email templates) for audit + accidental-delete recovery — that's an internal admin concern, not a per-section concept.

### Rescope: Q6 auto-upgrade

The migration applying `submission_sections` + `submission_drafts.base_id` auto-seeds every existing row:

- `base_id` set to the firm default base for the draft's proceeding family (matched at migration time via `submission_codes_shape` / proceeding lookup).
- `submission_sections` rows seeded from `base.section_spec.defaults` per draft, with empty `content_md_de`/`content_md_en` for `prose`/`requests`/`evidence` kinds.
- `composer_meta` populated with the default `section_order` so the editor paints immediately on next open.
- v1 fallback render path stays compiled in (gate: `base_id IS NULL OR no sections rows`) as a safety net should the auto-seed miss a draft.

**Risk:** the lawyer's next visit to a previously-edited draft shows the new Composer UI. The lawyer's existing `variables` jsonb overrides survive untouched; section content starts empty. Lawyer experiences this as "my variable fills are still here, plus a new section editor I can ignore until I want to use it". Acceptable per m. Documented in §14 risk #8.

### Rescope: Q10 fully-editable `*_auto` sections

`*_auto` kinds disappear from the design. All sections become `prose` (default seeded from base + bag where the base supplies canonical OOXML), `requests`, or `evidence`. The base.docx still ships canonical Rubrum / letterhead / signature OOXML inside each section's anchor — but on first edit the lawyer's content_md replaces it.

UX implication: on draft open, "Rubrum" / "Briefkopf" / "Unterschrift" sections show the auto-rendered content as initial text inside the editor. The lawyer can leave it alone (renders identically to old `*_auto` behaviour at export), edit it (their MD replaces the base's OOXML at render), or hide it (`included=false`).

**Risk:** the lawyer types a party name into the Rubrum manually that doesn't match the variable bag's `{{parties.claimant.0.name}}`. The exported `.docx` shows the typed name — variable substitution is a no-op against literal text. Acceptable: the lawyer is in control, this is a feature not a bug. Documented in §14 risk #11.

### Status

Design final. Inventor parks per hard gate. Head decides Slice A coder shift.

---

## §0 TL;DR

Today (v1): one Word `.docx` template per `submission_code`, picked from Gitea via a fallback chain, merged with a flat `{{placeholder}}` variable bag built from project + parties + deadline + user + firm state. The lawyer's only authoring surface is the sidebar — they fill in variables, see a read-only HTML preview, export to `.docx`, and finish in Word.

What m wants (v2 / "Composer"):

1. **Sections** — every submission is composed of named, independently-editable section blocks (`letterhead`, `caption`, `introduction`, `requests`, `facts`, `legal_argument`, `evidence`, `exhibits`, `closing`, `signature`). The lawyer can reorder, hide, and rename them.
2. **Selectable base templates** — the lawyer picks the *base* (firm letterhead / neutral skeleton / UPC-formal / LG-Düsseldorf-style); the base supplies the document chrome, fonts, paragraph styles, and seeds the default section set. Lawyer can swap mid-draft without losing prose.
3. **Editability of content** — inside paliad, the lawyer authors free prose per section (Markdown source-of-truth, contentEditable surface, minimal bold/italic/heading/list/quote toolbar). Variables `{{…}}` are still legal anywhere in section content.
4. **Building blocks** — a library of reusable section-shaped chunks (standard Anträge phrasing, *Verspätungseinrede* boilerplate, *Auslandszustellung* evidence offering, …). Authored once, inserted into any compatible section across drafts, with private / team / firm / global visibility tiers. Copy-on-insert so the lawyer's edits never get clobbered; insertion records lineage for opt-in "refresh from library".

**Hard constraints honoured:**
- The existing `submission_drafts` contract — `variables jsonb`, `selected_parties uuid[]`, `language`, `last_exported_*` columns, owner-scoped RLS — stays load-bearing. v1 drafts continue to render via the v1 path (template-only fallback) until the lawyer opts in to Composer for that draft.
- The `{{rule.X}}` legacy aliases (still resolved by `SubmissionVarsService` alongside the canonical `{{procedural_event.X}}`) are honoured inside section content too.
- The audit-log shape (`system_audit_log` event_type `submission.exported`, `project_events` event_type `submission_exported`) is preserved; v2 exports add `composer: true` to the metadata jsonb.
- No new third-party Go dependency. The in-house renderer (from commit `8ea3509`) extends to MD → OOXML and section-anchor splicing.

**Slice plan:** seven slices A → G, each independently shippable. Slice A lands the base picker and a read-only section list (no behavior change). Slice B is the smallest "Composer actually works" milestone: editable prose sections, anchor-spliced render, MD → OOXML walker for paragraphs.

**No code in this doc.** Implementation gate is m's go/no-go through the head.

---

## §1 Premises verified live (2026-05-26)

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 |
|---|---|
| `paliad.submission_drafts` exists with the v1 contract. | `information_schema.columns` query: `id, project_id (nullable), submission_code, user_id, name, variables jsonb DEFAULT '{}', last_exported_at, last_exported_sha, selected_parties uuid[] DEFAULT '{}', last_imported_at, language text DEFAULT 'de', created_at, updated_at`. RLS on, four policies (select/insert/update/delete) gated on `paliad.can_see_project` + owner-scoped UPDATE/DELETE. |
| There is **no** `submission_drafts.audit_log` column — the issue text is inaccurate on this point. | Same `information_schema` query: no column matching `%audit%`. Audit rows live in `paliad.system_audit_log` (event_type `submission.exported`) plus `paliad.project_events` (event_type `submission_exported`). This is the same shape t-paliad-238 §D4 locked in. **Issue #141 needs a one-line correction; flagged in §13.** |
| 11 live draft rows on prod (the issue text said "7"). | `SELECT COUNT(*) FROM paliad.submission_drafts` → 11 rows across 1 user, 4 projects, 10 submission_codes. Mostly small variable maps (25–224 bytes JSONB). All language `de` except one `en`. Migration to v2 must keep these renderable without lawyer intervention. |
| `{{rule.X}}` legacy aliases are alive and resolved. | `internal/services/submission_vars.go` resolves `procedural_event.code/name/name_de/name_en/legal_source/legal_source_pretty/primary_party/event_kind` as canonical; the `rule.*` aliases (`rule.submission_code`, `rule.name`, `rule.name_en`, `rule.legal_source`, `rule.legal_source_pretty`, `rule.primary_party`, `rule.event_type`) resolve to identical values per `submission_vars_aliases_test.go` (~85 LoC of golden tests). Any v2 surface that touches placeholder text must preserve this. |
| In-house renderer handles single-run AND cross-run placeholder fragmentation. | `internal/services/submission_merge.go`, ~530 LoC. Two-pass: text-node replace first (preserves run-level formatting); paragraph-merge fallback when `{{` / `}}` survives pass 1 (handles Word's autocorrect-split placeholders). `lukasjarosch/go-docx` was explicitly rejected (refuses sibling placeholders inside one run). v2 reuses this renderer wholesale. |
| Templates resolve via a five-tier fallback chain. | `internal/handlers/submission_drafts.go` `resolveSubmissionTemplate`: `per_code_lang` (`templates/<FIRM>/<code>.<lang>.docx`) → `per_code` → `skeleton_lang` (`_skeleton.<lang>.docx`) → `skeleton` (`_firm-skeleton.docx`, `_skeleton.docx`) → `letterhead` (HL Patents Style `.dotm`). v2's base concept slots in at the `_firm-skeleton` / `_skeleton` tier but moves the section-anchor wiring into the base body. |
| Templates live in Gitea at `HL/mWorkRepo:6 - material/Templates/Word/Paliad/<FIRM>/...`. | `internal/handlers/files.go` Gitea proxy, in-process SHA cache, 5-min refresh. `_firm-skeleton.docx` carries 168 HL paragraph styles (memory `01cdd589`); `_skeleton.docx` carries a universal layout. Same proxy pattern reused for bases + building blocks if they need .docx-backed content. |
| Frontend draft editor exists at `/projects/{id}/submissions/{code}/draft`. | `frontend/src/submission-draft.tsx` (231 LoC) + `frontend/src/client/submission-draft.ts` (1913 LoC) — sidebar with variable groups (Mandant & Verfahren, Parteien, Frist, Sonstiges), language toggle (DE/EN), party multi-select, "Aus Projekt importieren", read-only preview pane, autosave. v2 grafts new sections + base picker into the same shell. |
| Migration tracker is at **106 in the live DB**; worktree filesystem has migration files up to **145**. | `SELECT version FROM paliad.paliad_schema_migrations ORDER BY version DESC LIMIT 1` → 106. `ls internal/db/migrations/` → newest is `145_scenarios.up.sql`. Multiple branches have queued migrations (e.g. 119–145). Next free slot for v2 work on this branch is **146**; coder must re-check at implementation time in case other branches commit first. |
| `paliad.can_see_project(uuid)` is the canonical RLS predicate. | Mig 055; used by every project-scoped table. v2's new tables (`submission_bases`, `submission_sections`, `submission_building_blocks` + `_versions`) all gate on it. |
| `internal/branding.Name` defaults to "HLC", overridable via `FIRM_NAME`. | Plumbing reused for per-firm bases + per-firm building blocks (visibility tier "firm"). |
| The `entity-table` row contract is enforced. | `.claude/CLAUDE.md` → "Whole-card / whole-row click → use a JS row handler, not a `::before` overlay". Composer's section list (when rendered as cards/rows) follows this. |

**Doc-vs-live conflicts found:**
- Issue #141 mentions `submission_drafts.audit_log` and "7 live rows". The live table has no `audit_log` column (audit goes to `system_audit_log` + `project_events`); live row count is 11. Issue body should be corrected post-design-approval; the design does **not** depend on either claim being true.

---

## §2 m's headline requirements → design surface

| Issue text | Design surface |
|---|---|
| "different sections" | §4 data model (`submission_sections` table) + §6 default section set per `submission_code` + Slices B / G. |
| "selectable 'base templates'" | §5 base picker, `submission_bases` table with Gitea-backed body + section-default spec + Slice A / E. |
| "editability of content" | §7 editor UX — contentEditable per section with minimal toolbar, Markdown source-of-truth + Slice B. |
| "building blocks" | §8 `submission_building_blocks` table + library admin + section picker + Slice C. |

The four are interlocking but ship in independent slices.

---

## §3 Naming & vocabulary

For clarity throughout this doc and downstream code:

| Term | Meaning |
|---|---|
| **Composer** | The v2 submission-authoring experience (the editor page + render pipeline). NOT a separate product or feature flag — the existing `/projects/{id}/submissions/{code}/draft` URL stays; the editor evolves. |
| **Base** | A `.docx` chrome supplying letterhead, paragraph styles, font families, header/footer, and the default section spec for a draft. Replaces the loose "skeleton" terminology in v1's fallback chain. |
| **Section** | A named, ordered, lawyer-editable block of content within a draft (e.g. `requests`, `facts`). Stored as one row per (draft, section_key) in `submission_sections`. |
| **Building block** | A reusable, library-scoped chunk of section-shaped content. Inserted into a section copies its Markdown into the section; the section row retains `building_block_id` lineage. |
| **Variable bag** | The flat `{{path.dot.notation}} → string` map built by `SubmissionVarsService`. Unchanged from v1. |
| **Composer override** | The contents of `submission_drafts.variables jsonb` — lawyer's per-draft variable overrides. Unchanged from v1. |
| **Section anchor** | A pair of tokens `{{#section:KEY}} … {{/section:KEY}}` inside the base body that the render pipeline replaces with rendered section content. |
| **Section spec** | A JSON document on each base declaring (a) the default section set, (b) per-section default order, (c) per-section style anchors, (d) which Markdown constructs map to which paragraph styles. |

All identifiers in code follow the project's English convention (per project CLAUDE.md). User-facing labels are bilingual (DE primary, EN secondary).

---

## §4 Data model

### §4.1 Existing — preserved unchanged

`paliad.submission_drafts` keeps every column from v1. v2 adds two nullable columns:

| Column | Type | Nullable | Purpose |
|---|---|---|---|
| `base_id` | uuid | YES (FK → `submission_bases.id`) | Which base the draft renders against. NULL = legacy / v1 render path (fall back to `resolveSubmissionTemplate` chain). |
| `composer_meta` | jsonb | NO, DEFAULT `'{}'::jsonb` | Composer-specific metadata: `{"section_order": ["caption","requests","facts",…], "hidden_sections": ["evidence"], "active_locale": "de"}`. Order/hidden tracked here so a single fast read paints the editor without joining sections. |

Both additive; existing 11 rows get `base_id = NULL` (interpreted: not yet upgraded → render via v1 fallback chain) and an empty `composer_meta`. The migration is purely additive; no row rewrites.

The existing `variables jsonb` column still holds the lawyer's per-draft variable overrides. The `language` column still drives the active locale. `selected_parties uuid[]` still drives party scoping. Nothing about v1 is removed.

### §4.2 New — `submission_bases`

```sql
CREATE TABLE paliad.submission_bases (
    id                 uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    slug               text NOT NULL UNIQUE,          -- 'hlc-letterhead', 'neutral', 'upc-formal', 'lg-duesseldorf'
    firm               text,                          -- 'HLC' or NULL (global)
    proceeding_family  text,                          -- 'de.inf.lg', 'upc.inf.cfi', or NULL (any)
    label_de           text NOT NULL,
    label_en           text NOT NULL,
    description_de     text,
    description_en     text,
    gitea_path         text NOT NULL,                 -- 'Templates/Word/Paliad/HLC/_firm-skeleton.docx'
    section_spec       jsonb NOT NULL,                -- §6.1 section-spec JSON
    is_default_for     text[] NOT NULL DEFAULT '{}',  -- submission_codes this base seeds by default
    is_active          bool NOT NULL DEFAULT true,
    created_at         timestamptz NOT NULL DEFAULT now(),
    updated_at         timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX submission_bases_firm_family_idx
    ON paliad.submission_bases (firm, proceeding_family) WHERE is_active;

ALTER TABLE paliad.submission_bases ENABLE ROW LEVEL SECURITY;
-- All authenticated users SELECT; mutations admin-only (see §10).
```

Each base is a thin pointer at a `.docx` in Gitea plus the section-spec JSON. The actual body XML, paragraph styles, fonts, and section anchors live in the `.docx`; the table gives us listable, RLS-aware metadata, and a stable id for `submission_drafts.base_id` to reference.

Seed rows for v2.0:

| slug | firm | proceeding_family | label_de | label_en |
|---|---|---|---|---|
| `hlc-letterhead` | HLC | (any) | HLC-Briefkopf | HLC letterhead |
| `neutral` | (null) | (any) | Neutraler Schriftsatz | Neutral skeleton |
| `lg-duesseldorf` | (null) | `de.inf.lg` | LG-Düsseldorf-Stil | LG-Düsseldorf style |
| `upc-formal` | (null) | `upc.inf.cfi` | UPC-Verfahren | UPC formal |

The Gitea `_firm-skeleton.docx` becomes the body for `hlc-letterhead`; the universal `_skeleton.docx` becomes `neutral`. The two specialist bases are new authoring work for Slice E.

### §4.3 New — `submission_sections`

```sql
CREATE TABLE paliad.submission_sections (
    id                 uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    draft_id           uuid NOT NULL REFERENCES paliad.submission_drafts(id) ON DELETE CASCADE,
    section_key        text NOT NULL,                  -- 'requests', 'facts', 'legal_argument', …
    order_index        int  NOT NULL,                  -- lawyer-orderable
    kind               text NOT NULL,                  -- 'prose' | 'requests' | 'evidence'   (per Q10: no *_auto kind)
    label_de           text NOT NULL,                  -- lawyer-renameable
    label_en           text NOT NULL,
    included           bool NOT NULL DEFAULT true,
    content_md_de      text NOT NULL DEFAULT '',       -- Markdown source. Seeded with base's canonical OOXML rendering for caption/letterhead/signature; editable.
    content_md_en      text NOT NULL DEFAULT '',
    created_at         timestamptz NOT NULL DEFAULT now(),
    updated_at         timestamptz NOT NULL DEFAULT now(),

    UNIQUE (draft_id, section_key)
);

CREATE INDEX submission_sections_draft_idx
    ON paliad.submission_sections (draft_id, order_index);

ALTER TABLE paliad.submission_sections ENABLE ROW LEVEL SECURITY;
-- Visibility flows through draft_id → submission_drafts → can_see_project + owner-scoped.
```

`section_key` is a stable identifier (English slug) the render pipeline keys off. `kind` distinguishes prose, anträge, and evidence-style content for the editor's affordances (e.g. Anträge sections may auto-number; evidence sections show a "Beweis: " inline prefix). Per Q10, there is no read-only `*_auto` kind — every section is lawyer-editable, but caption/letterhead/signature sections are *seeded* with the base's canonical OOXML rendering so leaving them untouched produces the same output v1 lawyers see today.

`label_de` / `label_en` start from the section spec defaults but can be renamed per draft.

`content_md_de` + `content_md_en` are both columns from the start (Slice F is not deferred — see §11 Q7) so a draft is bilingual-by-construction; the active rendering picks the column matching `submission_drafts.language`.

Per Q2: there is no `building_block_id` lineage. Building blocks are plain text snippets; once inserted, the snippet's prose belongs to the section row, full stop.

### §4.4 New — `submission_building_blocks`

Per Q2: building blocks are plain text snippets. No `_versions` companion table exposed to the section side; admin-side version history lives in a private `_admin_versions` table for accidental-delete recovery only (mirrors the email-templates retention=20 pattern).

```sql
CREATE TABLE paliad.submission_building_blocks (
    id                 uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    slug               text NOT NULL,
    firm               text,                          -- 'HLC' or NULL (global)
    section_key        text NOT NULL,                  -- which section kind this block fits
    proceeding_family  text,                          -- 'de.inf.lg', NULL = any
    title_de           text NOT NULL,
    title_en           text NOT NULL,
    description_de     text,
    description_en     text,
    content_md_de      text NOT NULL DEFAULT '',
    content_md_en      text NOT NULL DEFAULT '',
    author_id          uuid REFERENCES paliad.users(id),
    visibility         text NOT NULL,                 -- 'private' | 'team' | 'firm' | 'global'
    is_published       bool NOT NULL DEFAULT false,
    created_at         timestamptz NOT NULL DEFAULT now(),
    updated_at         timestamptz NOT NULL DEFAULT now(),
    deleted_at         timestamptz,

    UNIQUE (slug, firm)
);

-- Admin-side audit only; not referenced from submission_sections.
CREATE TABLE paliad.submission_building_block_admin_versions (
    id                 uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    building_block_id  uuid NOT NULL REFERENCES paliad.submission_building_blocks(id) ON DELETE CASCADE,
    content_md_de      text NOT NULL,
    content_md_en      text NOT NULL,
    edited_by          uuid REFERENCES paliad.users(id),
    note               text,
    created_at         timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX submission_building_blocks_section_visibility_idx
    ON paliad.submission_building_blocks (section_key, visibility, firm, proceeding_family)
    WHERE deleted_at IS NULL AND is_published;
```

Visibility tiers (mirror the team-visibility model elsewhere in paliad):

- `private` — visible only to `author_id`.
- `team` — visible to every member of any team the author belongs to that touches the project context (resolved via the standard team-visibility helpers).
- `firm` — visible to every user whose `email` ends in any of `ALLOWED_EMAIL_DOMAINS` and whose `branding.Name` matches the block's `firm`.
- `global` — visible to every authenticated user.

Versioning: same retention-20 policy as email templates (`internal/services/email_template_service.go`). Each admin Save appends a `_admin_versions` row + GCs to 20 in-transaction. This is an admin-side concern — sections never reference it. The lawyer's section row is unaware which version of which library block its content originated from.

### §4.5 Migration count and order

One migration per concern (atomic, drift-safe), suggested in this order — coder confirms slot numbers at impl time:

| Slot | Purpose |
|---|---|
| 146 | `CREATE TABLE submission_bases` + RLS + seed rows for `hlc-letterhead`, `neutral`. (Specialist bases land in Slice E migrations.) |
| 147 | `ALTER TABLE submission_drafts ADD COLUMN base_id uuid REFERENCES submission_bases(id); ADD COLUMN composer_meta jsonb NOT NULL DEFAULT '{}'::jsonb;` |
| 148 | `CREATE TABLE submission_sections` + RLS + indexes. **Auto-seed all 11 existing drafts** (per Q6) — for each draft: set `base_id` from firm default for the proceeding family; INSERT one `submission_sections` row per default in the base's `section_spec.defaults`, copying `seed_md_de` / `seed_md_en` into `content_md_de` / `content_md_en`; populate `composer_meta.section_order`. Idempotent (ON CONFLICT DO NOTHING). |
| 149 | `CREATE TABLE submission_building_blocks` + `submission_building_block_admin_versions` + RLS + indexes (per Q2, no lineage column on `submission_sections`). |

Slot numbers are flexible; the coder picks contiguous free slots at impl time. The auto-seed (mig 148) is the migration-time write that Q6 ratified — every existing draft gains its Composer surface at apply.

---

## §5 Bases — selection, swap, default

### §5.1 What a base supplies

A base provides:

1. **Document chrome** — the `.docx`'s headers, footers, page setup, font tables, paragraph styles. From the Gitea body.
2. **Section anchors** — `{{#section:KEY}} … {{/section:KEY}}` pairs in the body, declaring where each section's rendered content goes. From the Gitea body.
3. **Section spec** — JSON on the base row declaring the default section set, ordering, kinds, labels, and the stylemap (which Markdown construct maps to which paragraph style in this base). From the DB.
4. **Variable bag context** — the base inherits the same `SubmissionVarsService` placeholder set as v1. `{{firm.name}}`, `{{user.display_name}}`, etc. work unchanged.

### §5.2 Picking a base on draft create

On `POST /api/projects/{id}/submissions/{code}/drafts`:

1. Compute the proceeding family (`de.inf.lg` from `de.inf.lg.erwidg`).
2. Look up the firm's default base for this family: `SELECT … FROM submission_bases WHERE firm=$FIRM AND is_active AND $code = ANY(is_default_for) ORDER BY priority` (priority resolved by spec — see §11 Q3 alternatives).
3. Fall back: firm + family → firm + any-family → global + family → global + any → first active row.
4. Seed `submission_drafts.base_id` with the picked base.
5. Seed `submission_sections` rows from the base's `section_spec.defaults` array.

The lawyer can change the base via the sidebar picker; on swap the section content rows are preserved (only the base.docx body and styles change at render time). See §5.3.

### §5.3 Swapping a base mid-draft

Hard rule: **section content is base-agnostic.** Markdown source carries no inline style references; lists, headings, bold/italic are semantic tokens. The MD → OOXML walker applies the *current base's* stylemap at render time. Swap is safe by construction.

What DOES change on swap:

- Default section set: if the new base spec defaults to a different ordering or includes sections the old base lacked, the lawyer is offered a non-destructive "Apply base's default section order? (keeps existing content)" prompt. Default answer: NO.
- `composer_meta.section_order` honoured if set; else `base.section_spec.defaults` honoured.
- Sections that exist in the draft but not the new base's spec stay visible but get an "ungrounded" badge so the lawyer notices.
- Sections in the new base's spec but not in the draft: they're created lazily on first focus, empty content.

### §5.4 Authoring a new base

Each base is one `.docx` in Gitea plus one row in `submission_bases`. Authoring:

1. Lawyer (or admin) creates the `.docx` in Word with HL Patents Style applied. Inserts section anchors as plain text `{{#section:KEY}}` / `{{/section:KEY}}` in the body where each section should go.
2. Generator script (`scripts/gen-submission-base/main.go`, modelled on `scripts/gen-hl-skeleton-template/main.go` from t-paliad-275) strips macros, validates anchors, embeds default section spec into the file as a custom XML part.
3. Admin uploads to Gitea via the contents API (as mAi via `--netrc-file ~/.netrc-mai`).
4. Admin inserts the `submission_bases` row via the new `/admin/submission-bases` editor (Slice E).

This mirrors the t-paliad-275 workflow that already shipped `_firm-skeleton.docx`. No new tooling beyond a slightly extended generator script.

---

## §6 Sections — the default set

### §6.1 Section spec on a base

JSON document stored on each `submission_bases.section_spec` row. Shape:

```json
{
  "version": 1,
  "stylemap": {
    "paragraph":  "HLpat-Body-B0",
    "heading_1":  "HLpat-Heading-H1",
    "heading_2":  "HLpat-Heading-H2",
    "heading_3":  "HLpat-Heading-H3",
    "list_bullet":  "HLpat-Body-B0",
    "list_numbered": "HLpat-Body-B0",
    "blockquote": "HLpat-Body-B1"
  },
  "defaults": [
    {"section_key": "letterhead", "kind": "letterhead_auto", "order_index": 1, "label_de": "Briefkopf", "label_en": "Letterhead", "included": true},
    {"section_key": "caption",    "kind": "caption_auto",    "order_index": 2, "label_de": "Rubrum", "label_en": "Caption", "included": true},
    {"section_key": "introduction", "kind": "prose",        "order_index": 3, "label_de": "Einleitung", "label_en": "Introduction", "included": true},
    {"section_key": "requests",   "kind": "requests",        "order_index": 4, "label_de": "Anträge", "label_en": "Requests", "included": true},
    {"section_key": "facts",      "kind": "prose",           "order_index": 5, "label_de": "Sachverhalt", "label_en": "Facts", "included": true},
    {"section_key": "legal_argument", "kind": "prose",       "order_index": 6, "label_de": "Rechtliche Würdigung", "label_en": "Legal argument", "included": true},
    {"section_key": "evidence",   "kind": "evidence",        "order_index": 7, "label_de": "Beweisangebote", "label_en": "Evidence offering", "included": true},
    {"section_key": "exhibits",   "kind": "prose",           "order_index": 8, "label_de": "Anlagen", "label_en": "Exhibits", "included": false},
    {"section_key": "closing",    "kind": "prose",           "order_index": 9, "label_de": "Schlussformel", "label_en": "Closing", "included": true},
    {"section_key": "signature",  "kind": "signature_auto", "order_index": 10, "label_de": "Unterschrift", "label_en": "Signature", "included": true}
  ]
}
```

The `stylemap` is per-base because different bases use different paragraph style names (HLpat-* on the HLC base, generic Word styles on the neutral base). The default section list is a starting point; per-submission_code overrides land in §6.2.

### §6.2 Section default overrides per submission_code

Some submission codes naturally invert the section order (e.g. `upc.inf.cfi.sod` — Statement of Defence — puts requests last; `de.inf.lg.berufung` has a "Berufungsanträge" before facts, etc.). A small Go-side override table maps `submission_code → (section adjustments)`:

```go
// internal/services/submission_section_defaults.go (NEW, Slice B)
var sectionOverrides = map[string][]sectionAdjustment{
    "upc.inf.cfi.sod": {
        {SectionKey: "requests", OrderIndex: 9},  // last
    },
    "de.inf.lg.berufung": {
        {SectionKey: "requests", OrderIndex: 4},  // before facts
        {SectionKey: "berufungsantraege", Insert: true, OrderIndex: 4, Kind: "requests", LabelDE: "Berufungsanträge", LabelEN: "Appeal requests"},
    },
    // …
}
```

This is intentionally code-driven (not DB-driven) for v2.0 (see §11 Q8). The override applies once at draft creation; subsequent lawyer reorders win.

### §6.3 Caption / Letterhead / Signature — seeded but editable (per Q10)

The "auto" concept is gone. Caption, letterhead, and signature sections are regular `prose`-kind rows. Their distinguishing trait is the *seed*: on draft create, their `content_md_*` is pre-filled with a Markdown rendering of the canonical bag-driven content (so a draft that's untouched after create exports identically to v1's auto-rendered Rubrum / letterhead / signature).

- **Letterhead** — typically a `.docx` header part (HL Patents Style header1.xml) is the actual letterhead. The in-body letterhead section seeds with a "Schriftsatz von:" preamble using `{{firm.name}}`, `{{user.display_name}}`. Lawyer can edit, hide, or rename freely.
- **Caption (Rubrum)** — seeds with paragraphs using `{{parties.claimant.0.name}}`, `{{parties.defendant.0.name}}`, `{{project.case_number}}`, `{{project.court}}`. Lawyer can edit the prose around the placeholders (e.g. add an intervener block) or override entirely.
- **Signature** — seeds with `{{firm.signature_block}}` + `{{user.display_name}}`.

When `included=false`, the section is dropped at render time. When `included=true` and the lawyer hasn't touched the content, the seeded Markdown renders identically to v1's auto-block — same placeholders resolve, same OOXML output. When the lawyer edits, their Markdown wins.

**Seeding mechanism:** the base's `section_spec.defaults` declares a `seed_md_de` / `seed_md_en` per section. On draft create, the section row's `content_md_*` is initialised from the seed. Subsequent base swaps do NOT re-seed (the lawyer may have edited the content already; re-seeding would clobber).

### §6.4 Custom sections

Slice G adds "+ Abschnitt hinzufügen". UX:

- A picker shows existing slugs the base spec knows about that aren't yet in this draft, plus a "Neue Abschnitts-Slug" form (lawyer types `slug`, picks kind from `prose`/`requests`/`evidence`, default labels).
- Inserting writes a new `submission_sections` row.
- The render pipeline expects the base to declare an anchor for every section key. **Custom slugs without a matching anchor in the base** render after the last anchored section, before the signature, as plain paragraphs using the base's `paragraph` stylemap entry — the "trailing zone" semantic.

This keeps custom sections rendering predictably without requiring lawyer edits to the base `.docx`.

### §6.5 Reorder / hide

- Reorder: drag-and-drop in the editor; PATCH writes the new `order_index` array AND mirrors it into `composer_meta.section_order` for fast paint.
- Hide: per-section "Nicht aufnehmen" toggle flips `included` on the row.
- Both safe to round-trip; the base's anchor slots are stable, only the content (or absence of content) changes.

---

## §7 Editor UX

### §7.1 Page shell (unchanged)

`/projects/{id}/submissions/{code}/draft` and `…/draft/{draftID}`. Sidebar + main body grid (the existing pattern in `frontend/src/submission-draft.tsx`). Sidebar carries: switcher, name, language toggle, save status, import-from-project, party picker, variable groups (Mandant/Verfahren/Sonstiges).

### §7.2 What's new in the body

The main body grows from a single read-only preview pane to a **section stack**:

```
┌──────────────────────────────────────────────────────────┐
│  Composer                                                │
│  ──────────────────────────────────────────────────       │
│  Basis: [HLC-Briefkopf  ▾]   [Basis wechseln…]          │
│                                                          │
│  ┌──────────────────────────────────────────────────┐    │
│  │ 1. Briefkopf                   [+ Baustein] [⋮]  │    │
│  │  (seeded — bearbeitbar)                          │    │
│  └──────────────────────────────────────────────────┘    │
│  ┌──────────────────────────────────────────────────┐    │
│  │ 2. Rubrum                      [+ Baustein] [⋮]  │    │
│  │  (seeded — bearbeitbar)                          │    │
│  └──────────────────────────────────────────────────┘    │
│  ┌──────────────────────────────────────────────────┐    │
│  │ 3. Einleitung                  [+ Baustein] [⋮]  │    │
│  │  ┌────────────────────────────────────────────┐  │    │
│  │  │ [B] [I] [H₁] [H₂] [• ] [1.] [❝]            │  │    │
│  │  ├────────────────────────────────────────────┤  │    │
│  │  │ Namens und in Vollmacht der Klägerin       │  │    │
│  │  │ {{parties.claimant.0.name}} legen wir      │  │    │
│  │  │ folgende Klageschrift…                     │  │    │
│  │  └────────────────────────────────────────────┘  │    │
│  └──────────────────────────────────────────────────┘    │
│  ┌──────────────────────────────────────────────────┐    │
│  │ 4. Anträge                     [+ Baustein] [⋮]  │    │
│  │   …                                              │    │
│  └──────────────────────────────────────────────────┘    │
│  …                                                       │
│                                                          │
│  [Vorschau ▾]   ← collapsible read-only preview          │
└──────────────────────────────────────────────────────────┘
```

Per-section affordances:
- **`[B] [I] [H₁] [H₂] [• ] [1.] [❝]`** — a thin shared toolbar that activates on focus of any contentEditable region. One toolbar instance hops between sections.
- **`{{var}}` autocomplete** — typing `{{` opens a picker filtered by the resolved bag's keys. Inserts as plain text.
- **`+ Baustein einfügen`** — opens the building-block picker filtered to this section's `section_key` + project context. Insertion appends the block's Markdown to current cursor position.
- **`⋮` per-section menu** — Rename, Hide/Show, Move up/down, Reset to base default, Remove (custom sections only).
- **Read-only preview** is collapsed by default in v2 (the in-place editor is the primary surface); a `[Vorschau ▾]` toggle shows the merged HTML beneath the section stack as a fidelity guide. Same renderer as v1.

### §7.3 Editor surface — contentEditable + Markdown serialization

- Each section's contentEditable region is wired to a tiny TS shim (`frontend/src/client/composer-md.ts`, target ~250 LoC) that:
  - On keystroke, debounces 500ms, serialises the DOM to Markdown via a deterministic walker (headings, paragraphs, lists, blockquote, bold/italic, links — no images, no tables in v2.0).
  - PATCH-saves `content_md_<lang>` to `/api/.../sections/{section_id}`.
  - On focus, re-renders Markdown → DOM (idempotent on round-trip).
- The Markdown serializer is the source of truth. The DOM is a view. This guarantees swap-base survives content.
- A `[Quelltext anzeigen]` toggle exposes the Markdown source in a `<textarea>` for lawyers who prefer typing raw.

**Why not Tiptap / ProseMirror:**
- ~80kB bundle add per editor instance; 8 sections per page means rendering cost.
- Paliad's frontend ethos is HTML-first, custom JSX renderer, no React. Adding ProseMirror pulls a heavy dep.
- Lawyers already accept "final formatting in Word"; the editor's job is structural + first-draft prose, not WYSIWYG fidelity.

Slice D extends the toolbar; Slice B ships with bold/italic/headings/bullet-list/quote only (the minimum that exercises every stylemap entry).

### §7.4 Variable insertion

The sidebar's existing `paintVariables` flow stays unchanged — it renders one input per placeholder for the lawyer's overrides on flat values (`{{project.case_number}}`, `{{parties.claimant.0.name}}`, etc.).

In section content, the lawyer types `{{` and an inline picker drops below the cursor showing the resolved-bag keys + their current values. Selecting one inserts the literal `{{path.dot.notation}}` text. No expansion — the text stays as the placeholder until render.

### §7.5 Per-section building-block insertion

`+ Baustein einfügen` button → modal listing visible blocks filtered to `section_key = <this section's key>` AND `proceeding_family ∈ {project family, NULL}` AND visibility within reach. Each row shows title, description, first 200 chars of `content_md_<lang>`. Click → block's full `content_md_<lang>` is appended to current cursor position; section row gets `building_block_id` + `building_block_version` stamped.

If the lawyer already has content in the section, the insert is appended at the cursor with a `\n\n` separator. The lawyer can drag/edit afterwards.

No lineage badge, no "refresh from library" surface — see §8.4. The picker is a paste source, nothing more.

### §7.6 Autosave

Per-keystroke debounce 500ms (matches v1). PATCH `/api/.../drafts/{id}/sections/{section_id}` with `{content_md_de | content_md_en | order_index | included | label_de | label_en}`. No optimistic locking — drafts are owner-scoped per row, no multi-user co-editing.

The existing draft-level autosave (variables, selected_parties, name, language) is independent and unchanged.

---

## §8 Building blocks

### §8.1 Library structure

One row per block in `submission_building_blocks`. Each block is bound to one `section_key` (`requests` blocks can't be inserted into `facts` sections, etc.). The block carries both DE and EN content from the start.

### §8.2 Authoring surface

New admin page `/admin/submission-blocks` (mirrors `/admin/email-templates` shape):

- Left pane: list of blocks scoped to user's reach + a "Neuer Baustein" button.
- Centre pane: editor with `slug`, `firm`, `section_key`, `proceeding_family`, titles, descriptions, the `content_md_de` + `content_md_en` textareas. Visibility radio. "Speichern" / "Speichern und veröffentlichen" buttons. Append-only versions list with restore.
- Right pane: live render preview using `SubmissionRenderer.RenderHTML` against a sample bag.

Non-admin users get `/blocks` (personal) — same UI but filtered to `visibility = 'private'` and their authored rows. Private → published bumps visibility tier; tier upgrade routes the block past an admin moderation step if going `firm` or `global`.

### §8.3 Insert mechanics — plain text paste (per Q2)

Insert is dumb-paste: clone the block's `content_md_<active_lang>` into the section row's `content_md_<lang>` at the cursor offset. End of story. No `building_block_id` stamp, no version snapshot, no lineage tracking.

Rationale (m's call): the building-block-as-text-snippet mental model maps to how lawyers already think about boilerplate. They don't think about "this passage is a live reference to a library entry" — they think "I pasted this in, I might edit it". The simpler design honours that intuition.

Consequence: library edits do NOT propagate to in-flight drafts (by construction — no link exists). Lawyer who wants the latest version re-opens the picker and inserts again. That's the trade.

### §8.4 No refresh-from-library affordance (per Q12, superseded by Q2)

There is no "Aus Bibliothek aktualisieren" button. There is no diff view. The lawyer's content is the lawyer's content; the library is a paste source. If the lawyer wants to re-sync, they delete the section text and re-insert from the library.

Library admins still get the `_admin_versions` audit trail for accidental-delete recovery on the library side (§4.4) — that's an internal admin concern, not a per-section UX.

### §8.5 Visibility tiers — concrete examples

- **Private** "MS-Klageerhebung-Variante" — Matthias's draft prose for his style; nobody else sees it.
- **Team** "TeamPatentLitMUC-Klageerwiderung-Lieblingsphrase" — the Munich patent team's preferred opener; team members see it.
- **Firm** "HLC-Standardantrag-§43" — the firm's vetted Anträge boilerplate; every HLC user sees it.
- **Global** "Universal-Beweisangebot-Auslandszustellung" — open library curated cross-firm; everyone sees it.

The moderation step (tier upgrade to `firm` or `global`) is a content-review responsibility — admin review required, audit-logged.

---

## §9 Render pipeline

### §9.1 High-level

```
1. Load draft + sections + base.
2. Fetch base.docx bytes from Gitea (cached per SHA, 5-min refresh).
3. Build variable bag via SubmissionVarsService (unchanged from v1).
4. Apply lawyer's variables overrides on top (unchanged).
5. For each section in order_index order, with included=true:
   a. If kind ∈ {prose, requests, evidence}: render content_md_<lang> → OOXML
      using base.section_spec.stylemap.
   b. If kind = *_auto: pass through the base's anchor slot's inline OOXML.
6. Splice rendered OOXML into base body at matching {{#section:KEY}} … {{/section:KEY}} anchors.
7. Sections with included=false: drop the entire {{#section:KEY}} … {{/section:KEY}} slot.
8. Custom sections without anchors: render to base.stylemap.paragraph and append after the last anchored section, before {{#section:signature}}.
9. Pass the assembled XML through the v1 placeholder-substitution (single-run + cross-run merge). {{rule.X}} aliases resolved identically.
10. Strip leftover unfilled section anchors.
11. ConvertDotmToDocx pre-pass (existing behaviour; idempotent on .docx).
12. Stream as application/vnd.openxmlformats-officedocument.wordprocessingml.document.
```

Step 9 reuses the existing `internal/services/submission_merge.go` walker untouched. Steps 1–8 are new (`internal/services/submission_compose.go`, target ~400 LoC). Step 10 is a small regex post-pass.

### §9.2 MD → OOXML walker (Slice B, extended in Slice D)

Subset supported in v2.0:

| Markdown construct | OOXML output | Stylemap key |
|---|---|---|
| `# Heading` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.heading_1>"/></w:pPr><w:r><w:t>…</w:t></w:r></w:p>` | `heading_1` |
| `## Heading` | same with `heading_2` | `heading_2` |
| `### Heading` | same with `heading_3` | `heading_3` |
| `paragraph` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.paragraph>"/></w:pPr><w:r>…</w:r></w:p>` | `paragraph` |
| `**bold**` | `<w:r><w:rPr><w:b/></w:rPr><w:t>…</w:t></w:r>` (inline) | — |
| `*italic*` | `<w:r><w:rPr><w:i/></w:rPr><w:t>…</w:t></w:r>` (inline) | — |
| `- item` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.list_bullet>"/><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>…</w:p>` | `list_bullet` |
| `1. item` | same with `list_numbered`, `numId=2` | `list_numbered` |
| `> quote` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.blockquote>"/></w:pPr>…</w:p>` | `blockquote` |
| `[label](url)` | `<w:hyperlink r:id="…">` (with rel registered in document.xml.rels) | — |

Not supported in v2.0 (defer to later slice or document as "edit in Word post-export"):

- Tables (Slice F-or-later; lawyers expect Word for tabular data anyway)
- Images (the base supplies letterhead images; section content stays text-only)
- Footnotes
- Cross-references (Word's REF fields)
- Code blocks (no legal use case)

Lists need the base's `numbering.xml` to declare numId=1 (bullet) and numId=2 (numbered). The `_skeleton.docx` already carries numbering definitions; existing bases need an audit (Slice B prereq).

### §9.3 Section anchor format

In base.docx body XML, anchors live as plain text inside a `<w:p>` paragraph:

```xml
<w:p><w:r><w:t>{{#section:requests}}</w:t></w:r></w:p>
<w:p><w:r><w:t>{{/section:requests}}</w:t></w:r></w:p>
```

Renderer:

1. Find both paragraphs (regex on `<w:p[^>]*>…{{#section:KEY}}…</w:p>` for the opening; ditto for closing).
2. Replace the entire range between them (inclusive of both anchor paragraphs) with the rendered section OOXML.
3. If section is `included=false`, drop the range entirely (no replacement).
4. If section is `*_auto`, the anchor range carries the inline OOXML for the canonical block — keep it, don't replace.

Anchors that aren't matched (leftover from a missing section in the draft): regex-strip to `<w:p></w:p>` (empty paragraph) at step 10.

### §9.4 Preserving existing v1 contracts

- **Placeholder grammar:** unchanged. Same `{{path.dot.notation}}` regex.
- **`{{rule.X}}` aliases:** resolved identically inside section content. The composer's MD → OOXML walker preserves them as literal `{{rule.X}}` text; substitution happens in step 9 against the canonical resolver.
- **Missing-marker behaviour:** unchanged — `[KEIN WERT: <key>]` / `[NO VALUE: <key>]`.
- **Variable bag construction:** unchanged. `SubmissionVarsService.Build` returns the same shape.
- **Audit:** one `system_audit_log` row + one `project_events` row per export. Metadata jsonb gains `composer: true` flag when `base_id IS NOT NULL`. event_type stays `submission.exported` / `submission_exported`.
- **`last_exported_at` / `last_exported_sha`:** still bumped on each export. SHA is the base's `gitea_path` SHA.

### §9.5 Performance

A 10-section draft with 500 words each → ~50kB of Markdown total → ~150kB of OOXML to splice. The base is typically 100–200kB. Total assembly under 200ms on a warm cache. The Gitea fetch (cached) is the only meaningful network cost.

---

## §10 Authorization

### §10.1 Visibility (read)

- `submission_bases`: every authenticated user reads. Filter by `firm` + `proceeding_family` at the picker layer (not RLS — the base list is the same for everyone).
- `submission_sections`: gated through draft_id → `submission_drafts` → `can_see_project` + owner-scoped. RLS mirrors `submission_drafts`'s policies.
- `submission_building_blocks`: visibility tier predicate per row. Private/team/firm/global filter logic enforced in Go-layer service + DB RLS using the same `auth.uid()` helpers other tables use.

### §10.2 Mutations

- `submission_bases`: admin-only (`/admin/submission-bases` page; gated by the existing `adminGate`).
- `submission_sections`: owner-scoped on the draft (matches v1 `submission_drafts` rules).
- `submission_building_blocks`:
  - Create + edit `private`: any authenticated user.
  - Promote to `team`: any team member of the author's relevant team.
  - Promote to `firm` or `global`: admin moderation step (audit-logged).
  - Delete (soft, `deleted_at`): author or admin.

### §10.3 No new profession floor

Same call as v1: any user who can see the project can author Composer drafts. Substantive approval happens downstream of paliad (in Word, before filing). Adding a profession gate would slow the workflow without preventing anything paliad's existing approval system doesn't already cover.

---

## §11 Open design questions — recommended defaults + alternatives

m to ratify these via head escalation (no AskUserQuestion per task brief). The defaults are the inventor's pick; alternatives are listed so m can flip with one word.

### Q1 — Section content storage shape

**Default: separate `submission_sections` table** with one row per (draft, section).

Alternatives:
- (a) jsonb column on `submission_drafts.sections` — simpler migration, no joins, but blocks the "which drafts use building block X" queries we'll want for refresh-from-library.
- (b) Hybrid: jsonb by default, promote to table only when queries demand it — extra concept, schema drift over time.

Why default: building-block lineage queries (refresh, "where is this block used?", "is this block stale across N drafts?") become trivial with a relational table; jsonb makes them awkward joins on jsonb_array_elements. The cost of an extra table is small (one migration, RLS mirror).

### Q2 — Building block insert semantics

**Default: copy-on-insert with lineage retained.**

Alternatives:
- (a) Live reference — every render re-fetches the library version. Surprising for the lawyer working a sealed pleading.
- (b) Mixed — opt-in per block (the library row carries a `live_reference bool` column).

Why default: the lawyer's edits never get clobbered. The "library was updated, refresh?" affordance is opt-in via the lineage. Predictable; matches Word's "Paste as plain text" mental model.

> **m's ratification (2026-05-26):** "I don't even understand the question, that sounds a bit overengineered." → **SIMPLIFY** further than the recommendation. No `building_block_id` column on sections at all. Building blocks are plain text snippets pasted into sections; once pasted, the prose belongs to the section, no link back to the library. This dissolves Q12 (no refresh-from-library) and Slice G (no diff/merge UI).

### Q3 — Base storage

**Default: Gitea-backed body + thin DB row** (`submission_bases` row points at `gitea_path`).

Alternatives:
- (a) Full-DB-stored — admin authors base body directly in paliad. Eliminates Gitea round-trip but requires a server-side .docx editor (complex; not core competency).
- (b) Gitea-only — no DB row, base picked by directory scan. Loses the `is_default_for[]`, `section_spec`, RLS-able metadata.

Why default: lawyers + admins already author `.docx` in Word; mWorkRepo is the existing distribution vehicle. The DB row gives us listability, default-per-proceeding policy, and section spec storage without duplicating .docx content.

### Q4 — Editor surface

**Default: contentEditable per section with minimal toolbar (Markdown source-of-truth, in-house serializer).**

Alternatives:
- (a) Tiptap (ProseMirror-based) — ~80kB bundle add per editor; matches WYSIWYG expectations but pulls a React-ecosystem dep into paliad's no-React frontend.
- (b) Plain `<textarea>` with Markdown + side-by-side preview — simplest, but no inline bold/italic affordance.
- (c) ProseMirror direct — same as Tiptap but more code; not a meaningful step up.

Why default: keeps paliad's HTML-first ethos, no React/ProseMirror dep, lawyers can still see bold/italic visually, source-of-truth is portable. Slice D extends the toolbar; the editor architecture survives.

### Q5 — Render pipeline

**Default: section anchors in base.docx + in-house MD → OOXML walker (pure Go).**

Alternatives:
- (a) LibreOffice server-side compose — heavy dep (200MB+ runtime), licensing considerations, slower per-render.
- (b) Pandoc shellout — same shape, smaller dep but still binary requirement on the Dokploy image.
- (c) Pre-rendered base + section-injection via Office.js (browser-side) — pushes work to the client, requires Word desktop API, not portable.

Why default: pure-Go, no new runtime dep, integrates cleanly with the existing in-house renderer, performance budget under 200ms per export.

### Q6 — Migration of existing 11 drafts

**Default: implicit upgrade.** Existing rows get `base_id = NULL` (interpreted as "not yet upgraded"); the v1 render path stays as fallback. On the lawyer's next edit to a v1 draft, the editor prompts "Auf Composer umstellen?" and on yes seeds sections from the firm default base.

Alternatives:
- (a) Auto-upgrade all rows on migration apply — sections lazily parsed from existing variables jsonb. Risky: lawyer's mental model of "my draft" changes without consent.
- (b) Manual-only — no prompt; lawyer must explicitly create a new draft on the Composer to migrate. Slower adoption.

Why default: zero blast radius on existing rows; explicit consent on first edit. Lawyer can defer indefinitely.

> **m's ratification (2026-05-26):** picked **(a) Auto-upgrade**. The migration walks every existing draft in-transaction, sets `base_id` from the firm default for its proceeding family, and writes seeded section rows. v1 fallback path stays compiled in as a safety net. Risk noted in §14 #8.

### Q7 — Multi-language storage

**Default: split `content_md_de` + `content_md_en` columns from day one.**

Alternatives:
- (a) Single `content_md` column, relies on draft.language to pick the right author surface (lawyer maintains one language at a time, switching language re-authors).
- (b) Two-row pattern: one section row per language (4× rows, double the RLS).

Why default: same draft can render cleanly in either language with no re-authoring. Bilingual building blocks fold in naturally. Two columns are cheap.

### Q8 — Section default order per submission_code

**Default: code-driven (Go map in `submission_section_defaults.go`).**

Alternatives:
- (a) DB-driven (`proceeding_section_templates` table) — flexible (admin can edit defaults without code change) but adds infra (table, RLS, admin editor).
- (b) Per-base only (no per-code override) — simpler; loses the ability to differentiate Klageerwiderung vs. Statement of Defence section order.

Why default: section-default tweaks are rare and almost always require coordinated changes to the base's section_spec anyway. Keeping defaults in Go means one place to read; promote to DB if the override list grows past ~20 submission_codes.

### Q9 — Building block visibility tiers

**Default: 4 tiers — `private` / `team` / `firm` / `global`.**

Alternatives:
- (a) 2 tiers — `private` / `firm`. Simpler RLS, loses the team-level curation that paliad already uses elsewhere.
- (b) 3 tiers — drop `global` until cross-firm exists in practice. Hedges complexity.

Why default: matches paliad's existing team-visibility model. The moderation step gates the `firm` and `global` promotions; tier upgrade is an admin-reviewable event.

### Q10 — `*_auto` section semantics

**Default: lawyer toggles include/exclude, content rendered from base + bag; no edit.**

Alternatives:
- (a) `*_auto` is editable (lawyer can override the auto-rendered Rubrum text). Risk: lawyer types content that conflicts with the variable bag.
- (b) `*_auto` is fixed (lawyer can't even hide). Less flexible; "draft without signature" use cases break.

Why default: keeps the auto-rendered blocks consistent across drafts in the firm (no per-draft Rubrum drift) while letting the lawyer drop them when they don't apply (e.g., a "Schutzschrift" might omit the Rubrum block).

> **m's ratification (2026-05-26):** picked **(a) Fully editable**. The `*_auto` kind is removed. Caption, letterhead, and signature sections are regular `prose` rows seeded with bag-driven Markdown (so an untouched draft exports identically to v1). Lawyer edit replaces the seed at render time. Risk noted in §14 #11.

### Q11 — Preview pane

**Default: collapsed by default in v2 (in-place editor IS the surface).** Toggle `[Vorschau ▾]` opens a read-only HTML render below the section stack.

Alternatives:
- (a) Side-by-side preview (always visible) — matches v1's layout, takes more screen real estate.
- (b) No preview — risky; lawyers want to see the final render before exporting.

Why default: with editable sections, the editor surface IS the preview for prose. The toggle covers the "let me check the final layout" need; rare enough to not warrant permanent screen estate.

### Q12 — Refresh-from-library trigger

**Default: explicit per-section button, only visible when library version > snapshot.**

Alternatives:
- (a) Auto-prompt on draft open ("4 building blocks have updates; refresh?") — annoying for active drafts.
- (b) No surfacing — lineage exists only in metadata; lawyer must manually check.

Why default: opt-in keeps lawyer in control; the surfaced affordance triggers only when there's a real update.

> **m's ratification (2026-05-26):** **moot** — Q2's simplification removed the lineage tracking this question presupposed. No "refresh from library" affordance exists. If the lawyer wants the latest version of a block, they delete the section text and re-insert from the picker.

---

## §12 Slice plan

Each slice independently shippable. Coder picks up the next slice only after the previous one ships + lawyer validation.

### Slice A — Base picker (read-only) and section list shell

**Scope:**
- Migrations: `submission_bases` table, seed `hlc-letterhead` + `neutral` rows pointing at existing `.docx` files; `submission_sections` table; `submission_drafts.base_id` + `composer_meta` columns. Per Q6 auto-upgrade: the section-seed mig (148) walks every existing draft and writes its `base_id` + section rows in-transaction.
- Backend: `BaseService` (list, get-by-slug, get-default-for-code). `SubmissionDraftService.Create` seeds `base_id` + section rows. `SubmissionDraftService.Get` returns sections in `composer_meta.section_order`.
- Frontend: sidebar picker "Vorlagenbasis" with the seeded bases. Section list rendered read-only above the existing preview (no edit yet) — every draft shows the section stack from mig-148 day one.
- Render: unchanged — still uses v1 `resolveSubmissionTemplate` chain. `base_id` is informational this slice; v1 path stays compiled in as the safety net (gate: `base_id IS NULL OR no sections rows`).
- **Ships:** lawyer sees the base concept land, no behavior change at export.

**Acceptance:**
- Creating a new draft seeds `base_id` from firm + family default.
- Sidebar picker renders ≥2 bases; selecting one PATCHes `base_id`.
- Section list renders the base's default section set (read-only labels in active language).
- All 11 existing drafts now carry sections rows + non-NULL `base_id` after mig 148; export still produces v1-equivalent output (sections rendered with their seeded Markdown via the seeded base, identical to v1's auto-Rubrum/letterhead/signature).
- Unit tests cover: base default picker, section-list payload, mig-148 auto-seed idempotency, v1-equivalence after seed.

### Slice B — Editable prose sections + composer render pipeline

**Scope:**
- Backend: `SubmissionSectionService` (CRUD on section rows). New endpoints: `GET/POST/PATCH/DELETE /api/projects/{id}/submissions/{code}/drafts/{draft_id}/sections/{section_id}`. `SubmissionComposer` (render pipeline §9.1 + MD → OOXML walker §9.2, paragraphs + bold/italic + headings 1–3 + bulleted list only).
- Frontend: section stack on the body, contentEditable per section, shared toolbar (B/I/H1/H2/bullet), autosave per section. The existing variable sidebar stays.
- Render: when `base_id IS NOT NULL`, use the composer; else fall back to v1 path. Audit metadata gets `composer: true`.
- Base `.docx` audit: regenerate `_firm-skeleton.docx` and `_skeleton.docx` to add `{{#section:KEY}}` anchors for the default section set. Done via extended `scripts/gen-submission-base/main.go`.
- **Ships:** the smallest milestone where Composer actually works. Lawyer can author prose, see the rendered .docx.

**Acceptance:**
- Lawyer can edit any `prose`/`requests`/`evidence`-kind section.
- Toolbar applies bold, italic, headings 1–3, bullet list.
- Export renders all sections at their correct anchors with bag substitution.
- `{{rule.X}}` aliases still resolve inside section content.
- `*_auto` sections render as before (from base + bag).
- v1 drafts (base_id=NULL) render via v1 path unchanged.
- Golden tests on a sample draft → expected OOXML output.

### Slice C — Building blocks library + section picker

**Scope:**
- Migrations: `submission_building_blocks` + `submission_building_block_admin_versions` tables.
- Backend: `BuildingBlockService` (CRUD + visibility filtering + admin-side retention=20). Endpoints: `/api/blocks` (personal list, search), `/api/admin/submission-blocks` (admin CRUD), `/api/sections/{id}/insert-block` (the actual insert mechanic — copy text into section, no lineage stamp).
- Frontend: `/admin/submission-blocks` editor (full-page list + edit + admin version log + restore, mirrors `/admin/email-templates` shape). Per-section `+ Baustein einfügen` modal with filter chips (section_key, proceeding_family, visibility tier reach).
- **Ships:** reusable building blocks as paste sources.

**Acceptance:**
- Admin creates a block at firm visibility → visible to every HLC user in the picker.
- Lawyer inserts a block into a `requests` section → block content appended at cursor; section row has NO `building_block_id` reference (per Q2).
- Library edits to the block do NOT propagate to in-flight drafts (by design — no link).
- Visibility tiers enforced (private not visible to other users; team requires team membership; firm requires firm match; global open).

### Slice D — Rich-prose features (lists, numbered, blockquote, links, stylemap)

**Scope:**
- MD → OOXML walker: add numbered lists, blockquote, hyperlinks.
- Base `.docx` audit: ensure `numbering.xml` carries numId=1 (bullet) and numId=2 (numbered) definitions; backfill any base that lacks them.
- Each base's `section_spec.stylemap` gets all 8 entries populated.
- Frontend toolbar gains `[1.]` (numbered) and `[❝]` (quote).
- **Ships:** full prose feature set.

### Slice E — Specialist base templates (LG-Düsseldorf, UPC-formal) + base swap survives content

**Scope:**
- New `.docx` bases authored in Word: `lg-duesseldorf.docx`, `upc-formal.docx`. Each carries proceeding-family-specific letterhead, paragraph styles, default section spec.
- Verification: base swap mid-draft preserves section content; only chrome/styles change.
- Generator script (`scripts/gen-submission-base/main.go`) supports the new bases.
- **Ships:** lawyer can swap firm letterhead → court-style mid-draft.

### Slice F — Section reorder / hide / add custom

**Scope:**
- Frontend: drag-and-drop on the section list (small custom drag library or HTMX-style; no React DnD).
- "+ Abschnitt hinzufügen" picker (existing slugs + new slug form).
- `composer_meta.section_order` mirrors `order_index` for fast paint.
- Per-section "Nicht aufnehmen" toggle.
- **Ships:** full lawyer control over composition.

### Slice G — *Removed per Q2 / Q12 simplification*

The original Slice G ("diff & merge for building-block refresh") is dropped. m's ratification of Q2 dissolved the lineage machinery the slice depended on. There is no "refresh from library" affordance — see §8.4.

---

## §13 Out of scope (explicit)

To set the boundary cleanly:

- **Real-time collaborative editing** — no Operational Transform / CRDT. One draft, one author, last-write-wins on owner-scoped writes. Two lawyers wanting to co-edit start two drafts.
- **Comment threads on sections** — lawyer-level review happens in Word post-export.
- **Deep version history** — beyond `updated_at` on the draft + retention-20 on building-block library. Per-section version history is a Slice H+ idea.
- **AI-drafted prose** — the existing Paliadin persona handles ad-hoc Q&A; AI-drafted section content (a `{{ai.draft_facts}}` placeholder, say) is a future task with its own design doc. v1's §11 sketch still applies.
- **Cross-draft section sharing** — use building blocks. We won't add a "this section is a live reference to that section in another draft" concept.
- **Tables, images, footnotes, cross-references** — author in Word after export. The Composer is for structural composition + first-draft prose.
- **PDF output** — `.docx` only. Lawyers convert to PDF in Word.
- **Email or e-filing integration** — explicit Word-then-NetDocuments-then-court flow is the current contract; not in scope.
- **Translation between DE and EN** — both columns exist on every section + block, but content authoring is manual. No machine translation pass.

A handful of issue-#141 items that DON'T survive into v2 by inventor judgement:

- "Live FK to building block (no copy)" — replaced by copy-on-insert + lineage (Q2).
- "Tiptap / ProseMirror" — replaced by contentEditable + in-house serializer (Q4).
- The implied bigger ProseMirror push back into "rich editor" — addressed with simpler approach.

Issue text also mentioned `submission_drafts.audit_log` — this column doesn't exist; the audit contract uses `system_audit_log` + `project_events`. The design respects the **actual** audit contract; if m wants a new column added, that's a separate decision tracked here.

---

## §14 Risks + mitigations

1. **MD → OOXML walker fidelity.** Tiny rendering bugs (numbering, indentation, bullet styles) annoy lawyers who expect Word-perfect output.
   - **Mitigation:** stylemap per base; golden-test set against HL Patents Style for every Markdown construct. Slice D's rich features only after Slice B's prose-only path proves stable. Lawyer can always fix layout in Word post-export.

2. **Base swap blast radius.** If section content depends inadvertently on base styles (e.g. lawyer pastes Word content with inline formatting), swap can look broken.
   - **Mitigation:** Markdown source-of-truth is base-agnostic by construction. The contentEditable → MD serializer strips inline styles. Pasted Word content goes through a paste-normalizer (drops `<style>`, `<font>`, color attributes). Bullet/numbered styles resolved per-base at render.

3. **Building block proliferation.** Without curation, every lawyer creates their own variant of the same boilerplate.
   - **Mitigation:** visibility tiers (private → team → firm → global); admin moderation gate on firm + global promotions; "duplicate-detector" check at create time (slug + content_md hash) suggests existing similar blocks.

4. **Section anchor robustness.** If a base.docx is malformed (missing closing anchor, anchor inside a table cell), the renderer breaks.
   - **Mitigation:** base validation at upload (the `submission_bases` admin editor refuses bases with unmatched anchors); regex-based stripper at step 10 silently drops unclosed openings as empty paragraphs.

5. **{{rule.X}} alias forward-compat.** Section content authored today may use either canonical or legacy forms. If we ever drop the aliases, sections break silently.
   - **Mitigation:** the alias contract is pinned (memory `01cdd589`, knuth's t-paliad-275 ship notes). Composer's editor surfaces the canonical name in the autocomplete picker so new content tends canonical; the renderer resolves both forms identically.

6. **Bundle size.** contentEditable + toolbar adds ~10kB; section stack of 8–10 instances → ~40kB extra over v1. Acceptable.
   - **Mitigation:** one shared toolbar instance hops between sections (single DOM tree, reactivated on focus). MD ↔ DOM serializer is ~3kB shared across all sections.

7. **Performance with 100+ building blocks per firm.** Picker render time grows.
   - **Mitigation:** server-side filter + paginated list (limit 25 most-recent + free-text search input). Same shape as the party DB picker in t-paliad-287.

8. **Auto-upgrade surprise (Q6).** The lawyer's next visit to a previously-edited draft shows the new Composer UI (section stack instead of the variable-only sidebar). Could be disorienting on first encounter.
   - **Mitigation:** existing `variables` jsonb overrides survive untouched; section content seeded with bag-driven Markdown so export produces v1-equivalent output until the lawyer edits a section. v1 fallback render path stays compiled in (gate: `base_id IS NULL OR no sections rows`) as a safety net. A one-time "Wir haben Ihre Entwürfe ins Composer-Format übernommen" Hinweis banner on first post-deploy visit explains the change.

9. **Section spec versioning.** If we tweak the default section set, in-flight drafts created before the change have a stale `composer_meta.section_order`.
   - **Mitigation:** `section_spec.version` field. Compose service uses the lawyer's `composer_meta.section_order` if set (always); else seeds from current base spec. Spec bumps only affect new drafts.

10. **Coupling between base and stylemap.** A base swap could fail if the new base doesn't declare every stylemap key the old base did.
    - **Mitigation:** required keys list enforced at base-upload-time validation. Specialist bases must declare paragraph + heading_1/2/3 + list_bullet + list_numbered + blockquote.

11. **Lawyer-typed content conflicts with variable bag (Q10).** With fully-editable caption/letterhead/signature sections, the lawyer might type a party name into the Rubrum that doesn't match `{{parties.claimant.0.name}}`. Variable substitution is a no-op on literal text — the exported `.docx` shows the typed name. If the project's party list later changes, the draft's Rubrum is stale.
    - **Mitigation:** the seed Markdown uses placeholders (`{{parties.claimant.0.name}}`), so an untouched section auto-tracks the bag. The lawyer's edit is opt-in: typing a literal name is an explicit choice. A small inline hint on the caption section ("Tipp: `{{parties.claimant.0.name}}` bleibt mit dem Projekt synchron") nudges placeholder usage. No automated detection of literal-vs-placeholder drift — out of scope.

---

## §15 Open follow-ups (NOT blocking m's go/no-go)

These are coder/inventor follow-ups, not m-decisions:

- **Issue #141 body correction** — flag the inaccurate `submission_drafts.audit_log` reference + "7 live rows" count. Update issue text post-design-approval.
- **`scripts/gen-submission-base/main.go`** — extension of t-paliad-275's HL skeleton generator to embed `{{#section:KEY}}` anchors and a section_spec custom XML part. Builds on existing pattern.
- **Stylemap audit on `_firm-skeleton.docx`** — verify every required style name exists (HLpat-Heading-H1/H2/H3, HLpat-Body-B0, HLpat-Body-B1). Pre-Slice-B work.
- **Numbering.xml audit on `_skeleton.docx` + `_firm-skeleton.docx`** — verify numId=1 (bullet) and numId=2 (numbered) declared. Pre-Slice-D work.
- **i18n keys** — ~40 new keys for Slice A (sidebar picker, base label, "Vorlagenbasis", "Basis wechseln", section toolbar). ~20 more for Slice C (building-block library labels). DE + EN.
- **CLAUDE.md updates** — add a "Composer" section documenting the base concept, section spec, MD-source-of-truth contract, the `composer: true` audit metadata flag.
- **mBrian topic node** — file the synthesis post-approval; topic hub probably `submission-composer` under `paliad`.
- **Storyboard / Loom walk-through** — Slice B ships → record a 90-second screen-cap of the lawyer experience for the HLC validation round.

---

## §16 Recommended implementer

Pattern-fluent Sonnet coder. The substrate is well-trodden:

- Existing draft service: `internal/services/submission_draft_service.go` — extend, don't replace.
- Existing renderer: `internal/services/submission_merge.go` — reuse for the placeholder-substitution pass; add `internal/services/submission_compose.go` for the assembly + MD walker.
- Existing template fetch: `internal/handlers/files.go` Gitea proxy — reused for base.docx and (if Slice C+ adds Markdown blocks stored externally) building-block content.
- Existing audit shape: `paliad.system_audit_log` + `paliad.project_events` — extend `metadata jsonb` with `composer: true`; no schema change there.
- Existing admin pattern: `/admin/email-templates` for the building-blocks admin editor; mirror exactly.
- Existing visibility helpers: `internal/services/visibility.go` + team-membership predicates for tier filtering.

Novel pieces (all isolated):

- MD → OOXML walker (~350 LoC including tests).
- Section anchor parser + splicer (~150 LoC).
- contentEditable ↔ Markdown serializer (~250 LoC TS).
- Composer admin editor (`/admin/submission-bases`, `/admin/submission-blocks`).

NOT cronus (per project memory directive).

---

## §17 Acceptance for the design (this doc)

1. ✅ All four headline requirements (sections, bases, editability, building blocks) have concrete data-model + UX + render-pipeline proposals.
2. ✅ All 12 open design questions ratified by m on 2026-05-26 — see § m's decisions at the top of this doc. Body sections updated inline; §11 retains the historical recommendation matrix.
3. ✅ Slice plan present (A → F, with G dropped per Q2 simplification). Slice B is the smallest "Composer works" milestone.
4. ✅ Migration path documented: auto-upgrade all 11 existing drafts at mig-148 apply time per Q6. v1 fallback render path stays compiled in.
5. ✅ Hard constraints honoured: `submission_drafts` shape preserved (additive columns only), `{{rule.X}}` aliases preserved, audit contract preserved, no new third-party Go dep.
6. ✅ Premises verified live against the running paliad + DB; doc-vs-live conflicts flagged in §1 and §13.
7. ✅ Risks identified with mitigations (11 risks, including Q6 + Q10 deviations).
8. ✅ Inventor reports `ALL DESIGN QUESTIONS RATIFIED — design doc final, ready for Slice A coder shift`. **Inventor MUST NOT shift to coder.** Head gates the inventor→coder transition.

---

## §18 Coder gate

All 12 design questions ratified by m on 2026-05-26 via AskUserQuestion (paliadin-authorised override of the task brief's no-AskUserQuestion rule, per instruction msg #2391). The § m's decisions section at the top of this doc captures every pick; body sections + Slice plan updated inline.

Head's job:

1. Decide who codes Slice A: same worker as `/mai-coder` with this design as brief, fresh coder, or rescope.
2. Park Slice B–F until Slice A ships + lawyer validation lands.
3. Slice G was removed per Q2 — don't spawn a successor for it.

Inventor parks here.
