docs: ratify Q1-Q12 — submission generator v2 design final (m/paliad#141)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled

All 12 §11 design questions ratified by m on 2026-05-26 via
AskUserQuestion (paliadin-authorised override per instruction msg #2391).

Picks matching inventor recommendations (9 of 12):
 Q1 separate submission_sections table
 Q3 Gitea-backed body + thin DB row
 Q4 contentEditable + Markdown + in-house serializer
 Q5 section anchors + in-house MD->OOXML walker
 Q7 split content_md_de + content_md_en from day 1
 Q8 Go map for per-submission_code section defaults
 Q9 4 visibility tiers (private/team/firm/global)
 Q11 collapsed preview pane by default
 Q12 moot (superseded by Q2 simplification)

Deviations from recommendation (3 of 12):
 Q2 SIMPLIFY further — m: "sounds overengineered". Building blocks
    become plain text paste sources. No building_block_id column on
    sections, no _versions table referenced from sections, no
    refresh-from-library affordance. Slice G dropped.
 Q6 Auto-upgrade all 11 existing drafts at mig-148 apply time (not
    opt-in per draft). v1 fallback render path stays compiled in.
 Q10 *_auto kind removed. Caption/letterhead/signature sections are
    regular prose rows seeded with bag-driven Markdown; lawyer can
    edit/hide. Untouched drafts export identically to v1.

Body sections updated inline (§4.3 schema, §4.4 BB tables, §6.3
seeding, §8.3+8.4 BB insert, slice plan A/C/G, §11 ratification notes,
§14 risks #8+11, §17+18 acceptance + gate). §11 retains the historical
recommendation matrix.

Status: ALL DESIGN QUESTIONS RATIFIED — design doc final, ready for
Slice A coder shift. Inventor parks per hard gate. Head decides hire.

t-paliad-312
This commit is contained in:
mAi
2026-05-26 19:04:21 +02:00
parent 635457474a
commit 0e1691f00e

View File

@@ -5,7 +5,7 @@
**Task:** t-paliad-312
**Issue:** m/paliad#141
**Branch:** `mai/cronus/inventor-prd-for`
**Status:** DESIGN READY FOR REVIEW
**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)
@@ -14,6 +14,60 @@ This design deepens — it does **not** replace — both prior docs. Every contr
---
## § 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.
@@ -156,14 +210,12 @@ CREATE TABLE paliad.submission_sections (
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' | 'caption_auto' | 'letterhead_auto' | 'signature_auto'
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; empty for *_auto kinds
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 '',
building_block_id uuid REFERENCES paliad.submission_building_blocks(id) ON DELETE SET NULL,
building_block_version int, -- snapshot version at insert time
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
@@ -177,13 +229,17 @@ 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 free-prose sections (`prose`, `requests`, `evidence`) from sections rendered automatically from the variable bag (`*_auto`) — those can be hidden but not edited as text. `label_de` / `label_en` start from the section spec defaults but can be renamed per draft.
`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`.
`building_block_id` retains lineage but the row's content is the snapshot at insertion time (§8.3 copy-on-insert).
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` + `_versions`
### §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 (
@@ -201,7 +257,6 @@ CREATE TABLE paliad.submission_building_blocks (
author_id uuid REFERENCES paliad.users(id),
visibility text NOT NULL, -- 'private' | 'team' | 'firm' | 'global'
is_published bool NOT NULL DEFAULT false,
version int NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
@@ -209,17 +264,15 @@ CREATE TABLE paliad.submission_building_blocks (
UNIQUE (slug, firm)
);
CREATE TABLE paliad.submission_building_block_versions (
-- 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,
version int NOT NULL,
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(),
UNIQUE (building_block_id, version)
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX submission_building_blocks_section_visibility_idx
@@ -234,7 +287,7 @@ Visibility tiers (mirror the team-visibility model elsewhere in paliad):
- `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 Save appends a `_versions` row + GCs to 20 in-transaction.
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
@@ -242,12 +295,12 @@ One migration per concern (atomic, drift-safe), suggested in this order — code
| Slot | Purpose |
|---|---|
| 146 | `ALTER TABLE submission_drafts ADD COLUMN base_id uuid REFERENCES submission_bases(id); ADD COLUMN composer_meta jsonb NOT NULL DEFAULT '{}'::jsonb;` (must come after `submission_bases` exists — alternative: bundle 146+147) |
| 147 | `CREATE TABLE submission_bases` + RLS + seed rows for `hlc-letterhead`, `neutral`. (Specialist bases land in Slice E migrations.) |
| 148 | `CREATE TABLE submission_sections` + RLS + indexes. |
| 149 | `CREATE TABLE submission_building_blocks` + `_versions` + RLS + indexes. |
| 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`). |
The 146 column add must reference the bases table — practically, 147 (create bases) ships first and 146 becomes the next slot. Slot numbers are flexible; the coder picks contiguous free slots at impl time.
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.
---
@@ -353,15 +406,17 @@ var sectionOverrides = map[string][]sectionAdjustment{
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 `*_auto` sections — content comes from the bag, not the editor
### §6.3 Caption / Letterhead / Signature — seeded but editable (per Q10)
Three section kinds render entirely from the base + variable bag, with no user-editable content body:
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_auto`** — typically a `.docx` header part (HL Patents Style header1.xml) is the actual letterhead; the in-body section is either empty or a small "Schriftsatz von:" preamble using `{{firm.name}}`, `{{user.display_name}}`, etc. The lawyer toggles include/exclude but doesn't type text here.
- **`caption_auto`** — the "Rubrum" block. Renders from `{{parties.claimant.0.name}}`, `{{parties.defendant.0.name}}`, `{{project.case_number}}`, `{{project.court}}` etc. via a fixed micro-template embedded in the base (paragraphs using HLpat-Table-Recitals-Party styles). Lawyer toggles include/exclude.
- **`signature_auto`** — same shape. Uses `{{firm.signature_block}}` + `{{user.display_name}}`.
- **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}}`.
The base's section anchor for each `*_auto` section contains the OOXML for the canonical block. When the section is `included=false`, the entire `{{#section:KEY}} … {{/section:KEY}}` slot is dropped at render time.
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
@@ -398,10 +453,12 @@ The main body grows from a single read-only preview pane to a **section stack**:
│ Basis: [HLC-Briefkopf ▾] [Basis wechseln…] │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Briefkopf [Auto] [☐ verstecken]
│ │ 1. Briefkopf [+ Baustein] [⋮] │
│ │ (seeded — bearbeitbar) │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 2. Rubrum [Auto] [☐ verstecken]
│ │ 2. Rubrum [+ Baustein] [⋮] │
│ │ (seeded — bearbeitbar) │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 3. Einleitung [+ Baustein] [⋮] │ │
@@ -456,9 +513,9 @@ In section content, the lawyer types `{{` and an inline picker drops below the c
`+ 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 with a `\n\n` separator. The lawyer can drag/edit afterwards.
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.
A small badge in the section header — "Bausteinquelle: <block title> (Version <N>)" — surfaces lineage. Clicking the badge opens a one-step "Mit Bibliotheksversion abgleichen" diff view (Slice C+).
No lineage badge, no "refresh from library" surface — see §8.4. The picker is a paste source, nothing more.
### §7.6 Autosave
@@ -484,25 +541,19 @@ New admin page `/admin/submission-blocks` (mirrors `/admin/email-templates` shap
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 Copy-on-insert + lineage
### §8.3 Insert mechanics — plain text paste (per Q2)
Insert mechanics:
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.
1. Render Markdown copy of block's `content_md_<active_lang>` into `submission_sections.content_md_<lang>` at the cursor offset.
2. Stamp `submission_sections.building_block_id = <block.id>` and `building_block_version = <block.version>`.
3. If the section already had a `building_block_id` pinned: do NOT override the lineage — instead, append the new block's content with a `\n\n---\n\n` separator and clear the lineage stamp (mixed origin).
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.
Rationale: the lawyer's edits never get clobbered by library updates. Lineage stays so we can show "this was inserted from BB-Antrag-Standardfassung v3 — Library is now at v5, [refresh?]".
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 Refresh-from-library
### §8.4 No refresh-from-library affordance (per Q12, superseded by Q2)
A per-section button "Aus Bibliothek aktualisieren" (only shown when `building_block_id` is set AND the library version exceeds the snapshot version). Click flow:
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.
1. Render a diff view (text-level) of "current section content" vs "library version N+M content_md_<lang>".
2. Lawyer picks: (a) keep current, (b) overwrite with library version, (c) merge — opens a side-by-side editor with conflict markers.
3. On overwrite/merge: bump `building_block_version` to current.
Defaults to "off" — no automatic propagation. The lawyer's call always.
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
@@ -647,6 +698,8 @@ Alternatives:
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`).
@@ -689,6 +742,8 @@ Alternatives:
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.**
@@ -729,6 +784,8 @@ Alternatives:
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.
@@ -749,6 +806,8 @@ Alternatives:
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
@@ -758,18 +817,18 @@ Each slice independently shippable. Coder picks up the next slice only after the
### 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.
- Backend: `BaseService` (list, get-by-slug, get-default-for-code). `SubmissionDraftService.Create` seeds `base_id` + section rows.
- Frontend: sidebar picker "Vorlagenbasis" with the seeded bases. Section list rendered read-only above the existing preview (no edit yet).
- Render: unchanged — still uses v1 `resolveSubmissionTemplate` chain. `base_id` is informational.
- **Ships:** lawyer sees the base concept land, no behavior change.
- 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 continue to render via v1 path (their `base_id` is NULL).
- Unit tests cover: base default picker, section-list payload, migration round-trip.
- 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
@@ -792,15 +851,15 @@ Each slice independently shippable. Coder picks up the next slice only after the
### Slice C — Building blocks library + section picker
**Scope:**
- Migrations: `submission_building_blocks` + `submission_building_block_versions` tables.
- Backend: `BuildingBlockService` (CRUD + visibility filtering + version GC retention=20). Endpoints: `/api/blocks` (personal list), `/api/admin/submission-blocks` (admin CRUD), `/api/sections/{id}/insert-block` (the actual insert mechanic).
- Frontend: `/admin/submission-blocks` editor (full-page list + edit + version log + restore, mirrors `/admin/email-templates` shape). Per-section `+ Baustein einfügen` modal with filter chips.
- **Ships:** reusable building blocks.
- 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; `building_block_id` + version stamped.
- Block update → existing drafts unaffected; "Aus Bibliothek aktualisieren" button surfaces on the section.
- 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)
@@ -829,13 +888,9 @@ Each slice independently shippable. Coder picks up the next slice only after the
- Per-section "Nicht aufnehmen" toggle.
- **Ships:** full lawyer control over composition.
### Slice G — Diff & merge for building-block refresh
### Slice G — *Removed per Q2 / Q12 simplification*
**Scope:**
- "Aus Bibliothek aktualisieren" button surfaces when section's snapshot_version < library_version.
- Diff view (text-level, library-side library version vs section content).
- Three-way: keep / overwrite / merge (side-by-side conflict markers).
- **Ships:** lawyer can actively curate library updates per draft.
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.
---
@@ -886,8 +941,8 @@ Issue text also mentioned `submission_drafts.audit_log` — this column doesn't
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. **Existing drafts orphaned.** If a lawyer leaves a v1 draft un-migrated and later edits it, the "upgrade" prompt might confuse them.
- **Mitigation:** prompt is non-destructive (default NO, "Composer is opt-in for this draft"). v1 path keeps working indefinitely.
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.
@@ -895,6 +950,9 @@ Issue text also mentioned `submission_drafts.audit_log` — this column doesn't
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)
@@ -937,27 +995,24 @@ 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. Every open design question (12 of them) has a recommended default + at least one alternative.
3. Slice plan present (A G), each independently shippable; Slice B is the smallest "Composer works" milestone.
4. Migration path documented: existing 11 drafts continue via v1 path; opt-in upgrade per draft.
5. Hard constraints honoured: `submission_drafts` shape preserved, `{{rule.X}}` aliases preserved, audit contract preserved, no new third-party Go dep.
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.
8. Inventor reports `DESIGN READY FOR REVIEW`. **Inventor MUST NOT shift to coder.** Head gates the inventorcoder transition.
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 inventorcoder transition.
---
## §18 What m needs to ratify before coder shift
## §18 Coder gate
Per task brief: no AskUserQuestion. Escalation via `mai instruct head` for the 12 §11 questions, batched. Default picks are listed inline; m can flip any to an alternative with one word.
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 after this doc lands:
Head's job:
1. Surface §11's Q1Q12 to m with the recommended defaults.
2. Capture m's ratifications (or alternative picks) as an addendum at the top of this doc m's decisions, 2026-MM-DD) same shape as t-paliad-215's §2.
3. Decide who codes Slice A: same worker as `/mai-coder` with this design + addendum as brief, fresh coder, or rescope.
4. Park Slice BG until Slice A ships + lawyer validation lands.
The decisions section gets committed on the same branch as this design (no separate branch).
1. Decide who codes Slice A: same worker as `/mai-coder` with this design as brief, fresh coder, or rescope.
2. Park Slice BF until Slice A ships + lawyer validation lands.
3. Slice G was removed per Q2 — don't spawn a successor for it.
Inventor parks here.