diff --git a/docs/design-submission-generator-2026-05-19.md b/docs/design-submission-generator-2026-05-19.md new file mode 100644 index 0000000..52393f4 --- /dev/null +++ b/docs/design-submission-generator-2026-05-19.md @@ -0,0 +1,784 @@ +# Design — Submission generator (t-paliad-215) + +**Author:** copernicus (inventor) +**Date:** 2026-05-19 +**Issue:** m/paliad (task t-paliad-215, no Gitea issue filed yet) +**Branch:** `mai/copernicus/inventor-submission` +**Status:** DESIGN READY FOR REVIEW + +--- + +## §0 TL;DR + +Each row in `paliad.deadline_rules` represents a SUBMISSION — a filing, +hearing, or decision inside a proceeding (`submission_code` shape +`de.inf.lg.erwidg`, `upc.inf.cfi.soc`, …). The submission generator +takes a project + a submission_code, pulls a `.docx` template from +Gitea, merges in project variables (party names, court, case number, +patent number, our_side, deadline date, legal_source citation, firm +header), and streams the result to the browser as a download. + +- **Scope (locked by m):** template-render to `.docx`. No LLM in v1. +- **Template registry (locked):** Gitea — same proxy pattern as the + existing HL Patents Style `.dotm` in `internal/handlers/files.go`. +- **Output (locked):** direct download, NO server-side binary + persistence. One audit row per generation; the bytes themselves are + regenerable from inputs on demand. +- **Lookup (locked):** fallback chain — firm-specific override → + base for the exact `submission_code` → generic for the proceeding + family → ultra-generic skeleton. +- **Slice 1 (locked):** one template, end-to-end, on one project. + Pick `de.inf.lg.erwidg` (Klageerwiderung) as the proof template. +- **AI-drafted body:** explicitly OUT of scope for this task. Lives + in §11 as a follow-up sketch only. + +This design is read-only. No code, no migrations, no schema +additions. Implementation gate is m's go/no-go on this doc. + +--- + +## §1 Premises verified live (2026-05-19) + +Anchored against the running paliad codebase + youpc Supabase, not +against CLAUDE.md or memory. Where a claim load-bears the design, it +was checked against the live system. + +| Claim | Verification | +|---|---| +| Migration tracker at **102** (next is 103) | `ls internal/db/migrations/` — `102_system_audit_log` is the latest applied. | +| `paliad.documents` table exists, is empty, no code writes to it yet | `SELECT COUNT(*) FROM paliad.documents` → 0 rows. Columns: `id, title, doc_type, file_path NULLABLE, file_size, mime_type, ai_extracted jsonb, uploaded_by, created_at, updated_at, project_id NOT NULL`. `grep` shows only `export_service.go` (audit-export only) and a comment in `render_spec.go`. No `document_service.go`, no `/api/documents` handler. | +| `paliad.deadline_rules` carries the submission corpus | 254 total rows, 158 unique `submission_code`s, 214 `published`. Per-row fields used by the generator: `name`, `name_en`, `submission_code`, `primary_party` (claimant/defendant/court/both), `event_type` (filing/hearing/decision), `legal_source` (e.g. `DE.ZPO.276.1`, `UPC.RoP.23.1`), `is_bilateral`. | +| Slice 1 target row exists in published state | `SELECT … WHERE submission_code='de.inf.lg.erwidg'` → `{name:"Klageerwiderung", name_en:"Statement of Defence", primary_party:"defendant", legal_source:"DE.ZPO.276.1"}`. | +| Project rows carry all variables we need to merge | `paliad.projects` has `case_number, court, patent_number, filing_date, grant_date, our_side, instance_level, proceeding_type_id, title, reference, client_number, matter_number`. | +| Party rows carry party variables | `paliad.parties` has `name, role, representative, contact_info jsonb` and is project-scoped via `project_id`. | +| The HL Patents Style proxy pattern is reusable | `internal/handlers/files.go`: `fileRegistry` map → Gitea raw URL + SHA-based cache + 5-min refresh check + binary download response with `Content-Disposition`. Cache is in-process (`sync.Mutex` over a `map[string]*cacheEntry`). Single web replica today (`docker-compose.yml`), so in-process cache is fine. | +| Email templates already use `{{.VarName}}` placeholders + a "variable contract" sidebar pattern | `internal/services/email_template_variables.go` — `EmailTemplateVariable{Name, Type, Description, SampleDE, SampleEN}` rendered in `/admin/email-templates`. Submission generator can copy this contract pattern. | +| Audit infrastructure landed in mig 102 | `paliad.system_audit_log(id, event_type, actor_id, actor_email, scope, scope_root, metadata jsonb, created_at, updated_at)` — submission_generated events slot straight in. | +| Branding source is `internal/branding.Name` | Default `"HLC"`, overridable via `FIRM_NAME`. Inlined into client bundles by `frontend/build.ts`. Submission templates honour this via the `{{firm.name}}` placeholder. | +| `paliad.can_see_project(project_id)` is the canonical visibility predicate | mig 055; `internal/services/visibility.go` mirrors it. Generator gates on this; no new auth surface. | +| Paliadin runs on the aichat backend (mRiver) with persona system | `internal/services/aichat_paliadin.go` + `personas.yaml` in `m/mAi/internal/aichat/persona/`. Owner-gated to `PaliadinOwnerEmail = matthias.siebels@hoganlovells.com`. A future AI-drafted body would be a new persona, not a new Go service. | + +**Doc-vs-live conflicts found:** none material for this design. +`docs/project-status.md` still lists "Phase H AI Frist-Extraktion +deferred" — this design does NOT revive Phase H (different surface; +this is template merge, not document understanding). + +--- + +## §2 m's decisions (2026-05-19) + +Locked via AskUserQuestion before drafting the rest of the design. + +| # | Question | m's pick | Inventor recommended? | +|---|---|---|---| +| Q1 | Generator scope (template / AI-draft / brief / other) | **Template-render to `.docx`** | ✅ yes | +| Q2 | Template registry (Gitea / paliad DB / hybrid) | **Gitea** | ✅ yes | +| Q3 | Output flow (download-only / persist binary / attach to Frist) | **Direct download, no server-side binary** | ✅ yes | +| Q4 | Mapping (fallback chain / 1:1 / 1:N user picks) | **Fallback chain** | ✅ yes | +| Q5 | Slice 1 scope (1 template / 3–5 templates / full corpus / skeleton-only) | **One template, end-to-end on one project** (`de.inf.lg.erwidg` Klageerwiderung) | ✅ yes | + +Inventor-defaulted (not asked because there's a clear right answer or +because the question is implementation-level, not architecture-level): + +| # | Topic | Default | Reasoning | +|---|---|---|---| +| D1 | Variable engine | `{{path.dot.notation}}` placeholders in the .docx body, replaced via a Go library that handles run-fragmentation | Matches the existing email-template `{{.Var}}` shape lawyers already see in `/admin/email-templates`. See §6. | +| D2 | Authorization | Project-team visibility only (`paliad.can_see_project`) + audit row | Matches every other write surface in paliad. No profession floor (generation is read-only on source data and produces a draft, not a binding action). | +| D3 | Naming convention | `{rule.name}-{project.case_number}-{YYYY-MM-DD}.docx`, slashes → underscores, FIRM_NAME-aware | Mirrors how lawyers name files manually. See §7. | +| D4 | Missing-variable behaviour | Render `[KEIN WERT: {field}]` / `[NO VALUE: {field}]` marker inline | Lets the lawyer see the gap in Word, fix in paliad, regenerate. Better than 400ing. | +| D5 | Editor surface | Gitea-only for v1 (admin edits .docx in Word, commits to mWorkRepo) | A paliad uploader UI is Phase 2 affordance if Gitea round-trip is painful. | +| D6 | AI-drafted body | OUT of scope for this task | §11 sketches the natural follow-up shape (new aichat persona) but does not commit to it. | + +--- + +## §3 Architecture overview + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Project detail page │ +│ ├─ "Submissions" panel (or button row) │ +│ │ [Generate Klageerwiderung] [Generate Klageerhebung] [...] │ +│ │ Each button enabled iff a template exists for that │ +│ │ submission_code AND user passes paliad.can_see_project. │ +│ └─ Click → POST /api/projects/{id}/submissions/{code}/generate │ +└──────────────────────────────────┬─────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────┐ +│ handlers/submissions.go (NEW) │ +│ 1. Auth: UserIDFromContext + can_see_project gate │ +│ 2. Load deadline_rule by submission_code │ +│ 3. Resolve template via fallback chain (TemplateRegistry) │ +│ 4. Build variable bag (services/submission_vars.go) │ +│ 5. Render via SubmissionRenderer (services/submission_render.go) │ +│ 6. Write paliad.documents audit row (NO file_path) │ +│ 7. Write paliad.system_audit_log entry (event_type= │ +│ 'submission.generated') │ +│ 8. Stream .docx bytes with Content-Disposition: attachment │ +└──────────────────────────────────┬─────────────────────────────────────┘ + │ (template fetch) + ▼ +┌────────────────────────────────────────────────────────────────────────┐ +│ TemplateRegistry (services/submission_templates.go) — NEW │ +│ • In-process cache (same shape as handlers/files.go cacheEntry) │ +│ • Lookup path: │ +│ (1) templates/{FIRM_NAME}/{submission_code}.docx │ +│ (2) templates/_base/{submission_code}.docx │ +│ (3) templates/_base/{proceeding_family}.docx (e.g. upc.inf.cfi) │ +│ (4) templates/_base/_skeleton.docx │ +│ • Fetched from m/mWorkRepo via Gitea raw URL │ +│ • 5-min SHA refresh check (identical pattern to files.go) │ +└──────────────────────────────────┬─────────────────────────────────────┘ + │ + ▼ + Gitea: m/mWorkRepo + templates/HLC/de.inf.lg.erwidg.docx + templates/_base/de.inf.lg.erwidg.docx + templates/_base/de.inf.lg.docx + templates/_base/_skeleton.docx +``` + +**No new tables.** `paliad.documents` already exists; we write audit +rows there but leave `file_path` NULL. The fallback chain uses +filesystem-style paths inside the existing Gitea repo; no +`submission_templates` table needed for Slice 1. + +--- + +## §4 Slice 1 — what ships first + +Locked by Q5: **one template, end-to-end, on one project.** + +### 4.1 Target submission + +**`de.inf.lg.erwidg`** — Klageerwiderung (DE Verletzungs-LG). +Reasoning: + +- High-frequency submission in patent practice; lawyers draft these + often enough that the tool earns its keep on day 1. +- `primary_party='defendant'` — exercises the our_side variable. +- `legal_source='DE.ZPO.276.1'` — exercises citation injection. +- Pure-DE (no UPC complexity); easier first template for HLC's + Munich/Düsseldorf practice to author and review. +- Klageerhebung (`de.inf.lg.klage`) is an obvious alternative; either + works. m can flip the target in his decision review if Klageerhebung + is the better proof case. + +### 4.2 Surfaces in Slice 1 + +- **Project detail page** — new "Submissions" panel listing every + submission_code from the project's `proceeding_type` (via existing + `DeadlineRuleService`) with a `[Generieren]` button per row. Button + is enabled iff a template resolves AND `event_type='filing'` (no + `[Generieren]` on hearings/decisions — those don't have submissions). +- **Project detail API** — `GET /api/projects/{id}/submissions` returns + the list of (submission_code, name, has_template) so the frontend + can render enabled/disabled state. +- **Generate endpoint** — `POST /api/projects/{id}/submissions/{code}/generate` + returns `application/vnd.openxmlformats-officedocument.wordprocessingml.document` + with `Content-Disposition: attachment; filename="..."`. + +Slice 1 does NOT add: + +- A `/admin/submission-templates` editor (Gitea is the editor). +- A Frist-detail "Generate" button (project-detail only in Slice 1; + Frist-level surface is a Slice 2 affordance). +- A "Submissions" tab as a dedicated page (project-detail panel only). +- Per-firm overrides beyond `templates/HLC/...` (the fallback chain is + WIRED but the only override directory exercised in Slice 1 is HLC). +- The variable-contract sidebar UI (mirrors email-template editor) — + the contract is documented in §6 as code constants, surfaced as a + Slice 2 admin affordance. + +### 4.3 Slice 1 LoC estimate (informational, no time estimate) + +| File | Approx | +|---|---| +| `internal/handlers/submissions.go` (NEW) | 180 | +| `internal/services/submission_templates.go` (NEW — registry + Gitea proxy, reuses files.go cache idea) | 200 | +| `internal/services/submission_vars.go` (NEW — variable bag builder) | 220 | +| `internal/services/submission_render.go` (NEW — docx merge engine wrapper) | 120 | +| `internal/services/submission_render_test.go` (placeholder coverage + missing-var marker) | 180 | +| `frontend/src/components/SubmissionsPanel.tsx` (NEW) | 80 | +| `frontend/src/client/submissions.ts` (NEW — fetch + download) | 60 | +| Wiring in `cmd/server/main.go` + `internal/handlers/handlers.go` | 30 | +| i18n keys (`submissions.*`) DE+EN | 20 | +| **Total** | **~1090 LoC** | + +Plus: ONE `.docx` template authored by HLC at +`m/mWorkRepo/templates/HLC/de.inf.lg.erwidg.docx`, lawyer-reviewed +before Slice 1 closes. + +--- + +## §5 Template registry (Gitea-backed) + +### 5.1 Gitea layout + +``` +m/mWorkRepo (existing repo) +└── templates/ + ├── HLC/ # FIRM_NAME-keyed override dir + │ └── de.inf.lg.erwidg.docx # Slice 1 ships THIS file + ├── _base/ # Cross-firm baseline + │ ├── de.inf.lg.erwidg.docx # (Phase 2+) + │ ├── de.inf.lg.docx # proceeding-family fallback + │ ├── upc.inf.cfi.docx # (Phase 2+) + │ └── _skeleton.docx # ultra-generic fallback + └── README.md # placeholder reference for authors +``` + +Naming convention is the submission_code with a `.docx` suffix. +Proceeding-family fallback is the submission_code's first two +dot-segments (`de.inf.lg` from `de.inf.lg.erwidg`). + +### 5.2 Lookup algorithm + +```go +// services/submission_templates.go +func (r *TemplateRegistry) Resolve(ctx context.Context, code string) (Template, error) { + firm := branding.Name // "HLC", or whatever FIRM_NAME is + family := familyOf(code) // "de.inf.lg" from "de.inf.lg.erwidg" + candidates := []string{ + fmt.Sprintf("templates/%s/%s.docx", firm, code), + fmt.Sprintf("templates/_base/%s.docx", code), + fmt.Sprintf("templates/_base/%s.docx", family), + "templates/_base/_skeleton.docx", + } + for _, path := range candidates { + if tmpl, ok := r.fetch(ctx, path); ok { + return tmpl, nil + } + } + return Template{}, ErrNoTemplate +} +``` + +`fetch` does the same SHA-cache dance `handlers/files.go` already +does, scoped to the templates subtree. + +### 5.3 Gitea auth + +Reuses `GITEA_TOKEN` env var that already exists for the HL Patents +Style proxy. `m/mWorkRepo` is the same repo, same access token. No +new secret to configure. + +### 5.4 What happens when no template resolves + +The fallback chain ends at `_skeleton.docx`. The skeleton is an +intentionally bare-bones .docx (firm letterhead + party block + court +address + case number + signature stub) that ships as part of the +initial template set. In practice every Generate request resolves to +something — but if even the skeleton 404s (misconfigured repo), the +generator returns `503` with a clear error, the SubmissionsPanel +button surfaces "Vorlagen-Repository nicht erreichbar". + +--- + +## §6 Variable interpolation + +### 6.1 Engine + +Plain text replacement of `{{path.dot.notation}}` placeholders in the +.docx body. Whitespace inside braces is trimmed +(`{{ project.case_number }}` ≡ `{{project.case_number}}`). + +Implementation: a Go library that handles Word's run-fragmentation +correctly (Word may split `{{project.case_number}}` across multiple +`` runs during editing; naive find/replace breaks). Candidates: + +- **`github.com/lukasjarosch/go-docx`** (~2k stars, MIT, pure Go, + maintained). Handles run-merging before replacement. **Inventor + recommendation.** +- `github.com/nguyenthenguyen/docx` — older, less active. +- Custom in-house implementation — ~200 LoC for a minimal robust + replacer that walks the document XML and merges runs that fall + inside a `{{…}}` span. Fallback if the library doesn't pan out. + +Slice 1: try `lukasjarosch/go-docx` first; if it has dealbreaker bugs +(e.g. blows up on Word's autocorrect runs), fall back to the in-house +~200 LoC walker. The library choice is an implementation detail; the +placeholder syntax stays the same either way. + +### 6.2 Variable contract (v1 placeholder set) + +``` +{{firm.name}} — HLC (or whatever FIRM_NAME is) +{{firm.signature_block}} — Phase 2; v1 renders empty string + +{{today}} — 2026-05-19 (ISO) +{{today.long_de}} — "19. Mai 2026" +{{today.long_en}} — "19 May 2026" + +{{user.display_name}} — "Maria Schmidt" +{{user.email}} — "maria.schmidt@hlc.com" +{{user.office}} — "Munich" + +{{project.title}} — paliad.projects.title +{{project.reference}} — paliad.projects.reference +{{project.case_number}} — paliad.projects.case_number +{{project.court}} — paliad.projects.court +{{project.patent_number}} — paliad.projects.patent_number +{{project.filing_date}} — ISO date +{{project.grant_date}} — ISO date +{{project.our_side}} — "claimant" | "defendant" +{{project.our_side_de}} — "Klägerin" | "Beklagte" +{{project.instance_level}} — "lg" | "olg" | "bgh" | ... +{{project.proceeding.code}} — e.g. "de.inf.lg" +{{project.proceeding.name}} — Verletzungsklage am Landgericht +{{project.client_number}} — paliad.projects.client_number +{{project.matter_number}} — paliad.projects.matter_number + +{{parties.claimant.name}} — first paliad.parties row with role='claimant' +{{parties.claimant.representative}} — paliad.parties.representative +{{parties.defendant.name}} — first row with role='defendant' +{{parties.defendant.representative}} — paliad.parties.representative +{{parties.other.name}} — first row with role NOT IN ('claimant','defendant') — court, intervener, etc. + +{{rule.submission_code}} — "de.inf.lg.erwidg" +{{rule.name}} — "Klageerwiderung" +{{rule.name_en}} — "Statement of Defence" +{{rule.legal_source}} — "DE.ZPO.276.1" +{{rule.legal_source_pretty}} — "§ 276 Abs. 1 ZPO" +{{rule.primary_party}} — "defendant" +{{rule.event_type}} — "filing" + +{{deadline.due_date}} — date of the next pending deadline for this rule on this project +{{deadline.due_date_long_de}} — "26. Juni 2026" +{{deadline.computed_from}} — anchor description (e.g. "Klageerhebung am 12.05.2026 +6 Wochen") +``` + +Per-firm extensions (e.g. `{{firm.signature_block}}` filled from a +table) are Phase 2. + +### 6.3 Variable bag construction + +`services/submission_vars.go` builds a flat `map[string]string` +keyed by the dotted-path placeholders above. One pass over: + +1. `branding.Name` for `{{firm.*}}` +2. `time.Now()` (with `Europe/Berlin` locale for the long forms) for + `{{today.*}}` +3. `userService.GetByID()` for `{{user.*}}` +4. `projectService.GetByID()` for `{{project.*}}` +5. `partyService.ListByProject()` for `{{parties.*}}` +6. `deadlineRuleService.GetByCode()` for `{{rule.*}}` +7. `deadlineService.NextByRuleOnProject()` for `{{deadline.*}}` + +Missing values render as `[KEIN WERT: {dotted.path}]` (DE) or +`[NO VALUE: {dotted.path}]` (EN) based on user locale. This is by +design — the lawyer sees the gap in Word, fixes it (either in Word +or in paliad and regenerates), rather than getting a 400 with a list +of missing fields they then have to chase. + +### 6.4 Pretty-printing the legal_source + +`legal_source` in the rule corpus is shorthand +(`DE.ZPO.276.1`, `UPC.RoP.23.1`). Lawyers don't want that in a brief; +they want `§ 276 Abs. 1 ZPO` or `Rule 23.1 RoP UPC`. + +Slice 1 ships a small pretty-printer (`legalSourcePretty`) that knows +the families we currently use: + +| Prefix | Pretty form (DE) | Pretty form (EN) | +|---|---|---| +| `DE.ZPO.<§>.` | `§ <§> Abs. ZPO` | `Section <§>() ZPO` | +| `DE.ZPO.<§>` | `§ <§> ZPO` | `Section <§> ZPO` | +| `UPC.RoP..` | `Regel . VerfO UPC` | `Rule . RoP UPC` | +| `UPC.RoP.` | `Regel VerfO UPC` | `Rule RoP UPC` | +| `DE.PatG.<§>` | `§ <§> PatG` | `Section <§> PatG` | +| `EPC.` | `Art. EPÜ` | `Art. EPC` | +| (unknown) | original string | original string | + +Unrecognised prefixes pass through unchanged (better than an +incorrect prettification). The function is pure and unit-tested. + +--- + +## §7 File naming + +Generated file name: + +``` +{rule.name}-{project.case_number}-{YYYY-MM-DD}.docx +``` + +Concrete example for the Slice 1 happy path: + +``` +Klageerwiderung-2 O 123_25-2026-05-19.docx +``` + +Rules: + +- `rule.name` honours user locale (`Klageerwiderung` for DE, + `Statement of Defence` for EN). +- `project.case_number` slash/backslash → underscore (Word file name + hygiene), other characters preserved. +- Date is ISO at server-local (`Europe/Berlin`) date. +- If `project.case_number` is empty → fall back to a short hash of + `project_id` (8 hex chars) so the file still has a stable identifier + the lawyer can rename without losing track. + +--- + +## §8 Authorization + +- **Visibility gate:** `paliad.can_see_project(project_id)` — anyone + who can see the project can generate. Matches every other write + surface on the project. The endpoint inlines the predicate; + unauthorised callers get 404 (not 403, to avoid project + enumeration). +- **No profession floor.** A paralegal can generate a draft of a + Klageerwiderung; the draft is a Word doc that needs the associate's + approval downstream (in Word, on the document itself). Adding an + approval gate on generation would slow the workflow without + preventing anything that paliad's existing approval system doesn't + already cover at the substantive-act layer. +- **Owner gate (Paliadin) does NOT apply.** This is the + submission-template engine, not Paliadin. All paliad users get the + feature once a template exists for the proceeding their project is + in. + +--- + +## §9 Audit trail + +Two records per generation: + +### 9.1 `paliad.documents` row (audit-only, no binary) + +```sql +INSERT INTO paliad.documents (id, title, doc_type, file_path, file_size, + mime_type, ai_extracted, uploaded_by, + project_id) +VALUES (gen_random_uuid(), + '{rule.name} (generiert {YYYY-MM-DD})', + 'generated_submission', -- new doc_type value + NULL, -- no on-disk path + NULL, -- no file size (binary not persisted) + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + jsonb_build_object( + 'submission_code', $1, + 'template_path', $2, -- the gitea path we resolved + 'template_sha', $3, -- pinned SHA from the cache fetch + 'firm', $4), + $user_id, + $project_id); +``` + +- `doc_type='generated_submission'` is a new value; no CHECK constraint + on doc_type today so this is additive. +- `file_path NULL` is the marker that says "regenerate from inputs on + demand". The /api/projects/{id}/documents listing UI (Phase 2) will + surface a `[Erneut generieren]` action for these rows. +- `ai_extracted` jsonb is repurposed for generation provenance + (template SHA, firm at time of generation). Naming is unfortunate + but the column shape fits; renaming the column is out of scope for + this task. + +### 9.2 `paliad.system_audit_log` row + +```sql +INSERT INTO paliad.system_audit_log (event_type, actor_id, actor_email, + scope, scope_root, metadata) +VALUES ('submission.generated', + $user_id, + $user_email, + 'project', + $project_id::text, + jsonb_build_object( + 'submission_code', $1, + 'template_path', $2, + 'template_sha', $3, + 'document_id', $document_id, + 'firm', $4)); +``` + +Mirrors the existing `system_audit_log` event_type convention +(`*.created`, `*.updated`, etc., from t-paliad-214). + +### 9.3 Verlauf entry (project event) + +`paliad.project_events` gets a row with `event_type='submission_generated'` +and `timeline_kind='custom_milestone'` so the generation surfaces in +SmartTimeline's audit-log toggle and on the project's Verlauf list. +This is the user-visible footprint; the `system_audit_log` entry is +the admin-visible audit footprint. + +--- + +## §10 Frontend surface + +### 10.1 Slice 1 — SubmissionsPanel on project detail + +A new panel below the existing Verlauf / Deadlines panels on +`/projects/{id}`: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Schriftsätze │ +├─────────────────────────────────────────────────────────────────┤ +│ Klageerhebung [— Vorlage fehlt] │ +│ Klageerwiderung [Generieren ↓] │ +│ Replik [— Vorlage fehlt] │ +│ Duplik [— Vorlage fehlt] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +- Filter: only `event_type='filing'` rules from the project's + `proceeding_type` are listed. Hearings and decisions don't have + submissions. +- Per-row state: `has_template` returned by + `GET /api/projects/{id}/submissions`. Disabled buttons show the + "Vorlage fehlt" hint (German default, English in EN locale). +- Click `[Generieren ↓]` → POST → browser triggers download. +- `aria-busy="true"` on the panel while a generation is in flight + (cheap, but lawyers feel slow networks). + +### 10.2 Out of scope for Slice 1 + +- A standalone `/submissions` index page. +- A Frist-detail "Generate" button. +- A picker for template variants (1:N) — locked to fallback chain + (Q4), which is 1:1 from the user's perspective. +- An "edit project, then regenerate" loop on the same UI. + +--- + +## §11 AI-drafted body (deferred — sketch only) + +NOT in scope for t-paliad-215. Documented here so the next inventor +picking up the "AI Klageerwiderung body" task has a clear starting +shape. + +The natural fit: a new aichat persona (e.g. `paliadin-draft`) on +mRiver, parallel to the existing `paliadin` persona. + +``` +{{ai.draft_body}} # placeholder in the template + +→ generator detects {{ai.*}} placeholders in the template +→ POSTs to aichat with persona=paliadin-draft + context: + - project state (variables already built) + - relevant project notes (paliad.notes) + - the deadline_rule corpus (rule + family) + - HL Patents Style guide chunks (RAG, eventually) +→ aichat returns Markdown body +→ generator injects into the .docx as one or more paragraphs + (Word-friendly Markdown → docx mapping needed; substantive + formatting question for that follow-up) +``` + +Open shape questions for that follow-up (NOT for this design): + +- One persona per submission type, or one persona that branches on + `submission_code` in its system prompt? +- Owner gate (m only) like current paliadin, or open to all + authenticated users? +- Approval gate before the AI body lands in the .docx? +- Cost accounting per generation? +- Where does the prose context come from (notes / uploaded patent + spec / prior pleadings)? + +Re-uses, when that task fires: + +- This task's template engine, variable contract, fallback chain, + audit trail — all unchanged. +- Just a new placeholder family (`{{ai.*}}`) + a new aichat persona + + a new admin gate. + +--- + +## §12 Slice plan beyond Slice 1 + +| Slice | Scope | +|---|---| +| 1 | One template (`de.inf.lg.erwidg`), engine + fallback chain + audit + SubmissionsPanel on project detail. THIS DESIGN. | +| 2 | 3–5 more templates (Klageerhebung, SoC `upc.inf.cfi.soc`, SoD `upc.inf.cfi.sod`, Berufungsbegründung `de.inf.olg.begruendung`). Template authoring effort, no new architecture. | +| 3 | Variable-contract sidebar in a new `/admin/submission-templates` page (mirrors `/admin/email-templates` shape). Shows what placeholders exist, with samples. Does NOT add an uploader UI — Gitea remains the editor. | +| 4 | Per-firm override directory exercised (first non-HLC firm onboarded). | +| 5 | Frist-detail "Generate" button + paliad.documents.deadline_id FK (mig 103+) for per-Frist draft history. | +| 6 | (Separate task) AI-drafted body via Paliadin persona — see §11. | +| 7 | (Future) Paliad UI uploader as alternative to Gitea, if the round-trip is friction. | + +Slices 2–5 are roadmap markers, not commitments — m decides cadence. + +--- + +## §13 Trade-offs flagged + +1. **No binary persistence is a deliberate retention choice.** If a + lawyer regenerates after the project state changes (party renamed, + case_number corrected), the "regenerated" doc differs from the + "original generated" doc. This is a feature, not a bug — the source + of truth is paliad's project state, and the .docx is a derivative. + But the lawyer needs to be aware: there is no "what did I generate + last Thursday" recovery without re-saving locally. The + `paliad.documents` audit row records WHAT was generated (template + SHA + project state hash, optionally), but not the bytes. + +2. **Gitea round-trip for template edits is friction.** Template + authors edit `.docx` in Word, save, drag to Gitea web UI (or push + from a local clone). The 5-min SHA cache means edits surface + within 5 minutes (or instantly via `POST /api/files/refresh` — + already wired for the HL Patents Style template). If lawyers + complain, Phase 7 adds an in-paliad uploader. Until then, Gitea is + the editor. + +3. **Variable contract changes are coordinated edits.** Adding a new + `{{project.*}}` placeholder needs both a code change (var bag) AND + template edits (templates won't auto-discover new placeholders). + The variable-contract sidebar (Slice 3) is the mitigation — + template authors see what's available without reading the Go code. + +4. **`lukasjarosch/go-docx` library risk.** ~2k stars, MIT, maintained + — but it's a third-party dep we haven't used before. Fallback is + the in-house ~200-LoC walker. The placeholder syntax doesn't change + either way; Slice 1 can swap engines without touching templates or + callers. + +5. **`paliad.documents.ai_extracted` is repurposed for generation + provenance.** Slightly ugly naming because the column was added for + Phase H (AI Frist-Extraktion), which never shipped. Renaming the + column to something like `metadata` is out of scope for this task + but should be folded into the migration that lands when Phase 5 + adds `deadline_id`. + +6. **`paliad.parties.role='claimant'`** — multiple claimants on a + project (multi-party suit) → Slice 1 picks the first row. v1 + shortcut. Templates needing multi-claimant blocks become Phase 2 + work (with a `{{#each parties.claimants}}` shape on top of + `lukasjarosch/go-docx`'s loop support). + +7. **No Word-side `MERGEFIELD` support.** Lawyers who insert Word + merge fields (via Insert → Quick Parts → Field) instead of typing + `{{…}}` will get untouched MERGEFIELD codes in the rendered output. + Decision: standardise on `{{…}}` syntax (cheap to type, visible + in the template, predictable). Document this in the `templates/ + README.md`. + +8. **No template versioning UI.** Gitea provides git history; that's + the canonical version trail. Bumping to "use template X as of + commit Y" for an old project is a manual git-checkout-and-pin + exercise. Phase 2+ if anyone asks; not today. + +--- + +## §14 Open follow-ups (NOT blocking) + +These items are NOT m-decisions; they're follow-ups for the coder +shift or future inventor passes: + +- **Template authoring effort.** Slice 1 needs HLC to author/review + the actual Klageerwiderung template. That's a legal-review task that + can run in parallel with the engine code (template uploaded last + before the slice ships). Coordinate with m on who reviews. +- **English version of `legalSourcePretty`.** Pretty-printer table in + §6.4 needs an EN column for every prefix — populated from existing + glossary entries where possible. +- **i18n key sweep.** `submissions.*` namespace; ~20 keys for Slice 1 + (panel title, button labels, "Vorlage fehlt" hints, error messages + for 503/404/422). +- **README for template authors.** A `templates/README.md` in + m/mWorkRepo listing the available placeholders + naming convention + + a screenshot of a working template. Coder ships this alongside + Slice 1. +- **CLAUDE.md update.** Add a "Submission templates" section + documenting the Gitea proxy, placeholder syntax, and the + `submission.generated` audit event_type. +- **Cleanup task for `ai_extracted` naming.** Issue + Phase 5 mig. + +--- + +## §15 What this design does NOT do + +To set the scope boundary cleanly: + +- ❌ Generate PDFs. +- ❌ Generate emails or any non-.docx format. +- ❌ Edit `.docx` files inside paliad (no in-browser Word editor). +- ❌ Upload .docx to NetDocuments or any external DMS. +- ❌ Translate templates DE↔EN automatically. +- ❌ Validate the generated draft against any legal rule. +- ❌ Sign, certify, or notarise the output. +- ❌ Send the draft to court / e-filing. +- ❌ AI-draft any prose. (See §11.) +- ❌ Provide a paliad-UI template editor. (Gitea is the editor.) +- ❌ Persist generated .docx bytes server-side. (Audit row only.) +- ❌ Add a new database table. (`paliad.documents` is enough for v1.) +- ❌ Require a database migration. (Slice 1 is migration-free.) + +Each of these is a defensible future-scope item; none belong in +Slice 1. + +--- + +## §16 Recommended implementer + +Pattern-fluent Sonnet coder. The substrate is well-trodden: + +- Gitea proxy + cache: `internal/handlers/files.go` is the template + to lift. +- Variable contract pattern: `internal/services/email_template_variables.go` + is the template to mirror (different surface, identical shape). +- Visibility gate: `internal/services/visibility.go` + + `paliad.can_see_project()` — standard everywhere. +- Audit insert: `paliad.system_audit_log` (mig 102) + `paliad.documents` + (existing table, first writer). +- Frontend SubmissionsPanel: stock TSX + client/.ts pattern, same shape + as the existing CardLayout / EventsList panels. + +The only novel piece is the docx merge library integration — that's a +~200 LoC isolated module the coder can prototype on a sample .docx +before wiring into the project flow. + +NOT cronus per project memory directive. + +--- + +## §17 Acceptance criteria for Slice 1 + +The coder considers Slice 1 done when: + +1. Pushing a `.docx` to `m/mWorkRepo/templates/HLC/de.inf.lg.erwidg.docx` + and visiting any project with `proceeding_type=de.inf.lg` surfaces + a `[Generieren]` Klageerwiderung button. +2. Clicking it downloads a `.docx` named per §7 with all §6.2 + placeholders resolved (or `[KEIN WERT: …]` markers for genuinely + missing project fields). +3. Opening the downloaded .docx in Word renders cleanly (no run + fragmentation artefacts, no broken styles). +4. A row appears in `paliad.documents` with `doc_type='generated_submission'`, + `file_path=NULL`, and `ai_extracted` jsonb carrying the template + path + SHA. +5. A row appears in `paliad.system_audit_log` with `event_type='submission.generated'`. +6. A row appears in `paliad.project_events` with + `event_type='submission_generated'` and shows up in the project's + Verlauf / SmartTimeline. +7. Calling the endpoint without project visibility returns 404. +8. Calling the endpoint with no template anywhere in the fallback + chain returns 503 with a clear error. +9. Unit tests cover: placeholder rendering happy path, missing-var + marker, fallback chain (all 4 levels), file naming, slash + sanitization, legalSourcePretty for every prefix in §6.4. +10. `go build ./... && go vet ./... && go test ./... && bun run build` + all clean. +11. Manual test on the live database (test admin + `tester@hlc.de` per memory) against a project with a real + `de.inf.lg` proceeding succeeds end-to-end. + +--- + +## §18 Approval gate + +Per inventor SKILL.md and project CLAUDE.md: this design needs m's +go/no-go before any coder is hired. After m approves: + +- The head decides whether to hire the same worker as `/mai-coder` + with this design as the brief, or a fresh coder. +- A coder shift takes this doc as the spec, ships Slice 1, opens a + PR (no self-merge — maria's gate). +- Phase 11 (AI-drafted body) is a SEPARATE task — not auto-spawned. + +Inventor parks here.