Files
paliad/docs/design-submission-generator-2026-05-19.md
mAi 265f240151 docs(submission-generator): t-paliad-215 inventor design
DESIGN READY FOR REVIEW — copernicus inventor pass on the submission
generator (t-paliad-215). 5 questions answered with m's picks captured
in §2; awaiting head's go/no-go on coder shift.

Locked decisions:
- Scope: template-render to .docx (no LLM in v1)
- Template registry: Gitea (mWorkRepo proxy, same pattern as
  HL Patents Style)
- Output: direct download, no server-side binary persistence
- Mapping: fallback chain (firm → base/code → base/family → skeleton)
- Slice 1: one template end-to-end on one project
  (de.inf.lg.erwidg / Klageerwiderung)

No code, no migrations, no schema additions. Read-only design phase
per inventor SKILL.md.
2026-05-19 13:20:59 +02:00

38 KiB
Raw Blame History

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_codes, 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.goEmailTemplateVariable{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 / 35 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 APIGET /api/projects/{id}/submissions returns the list of (submission_code, name, has_template) so the frontend can render enabled/disabled state.
  • Generate endpointPOST /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

// 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 <w:r> 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.

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> § <§> Abs. <Abs> ZPO Section <§>(<Abs>) ZPO
DE.ZPO.<§> § <§> ZPO Section <§> ZPO
UPC.RoP.<Rule>.<Sub> Regel <Rule>.<Sub> VerfO UPC Rule <Rule>.<Sub> RoP UPC
UPC.RoP.<Rule> Regel <Rule> VerfO UPC Rule <Rule> RoP UPC
DE.PatG.<§> § <§> PatG Section <§> PatG
EPC.<Art> Art. <Art> EPÜ Art. <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)

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

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 <w:p> 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 35 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 25 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.


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.