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.
38 KiB
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
.dotmininternal/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.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 existingDeadlineRuleService) with a[Generieren]button per row. Button is enabled iff a template resolves ANDevent_type='filing'(no[Generieren]on hearings/decisions — those don't have submissions). - Project detail API —
GET /api/projects/{id}/submissionsreturns the list of (submission_code, name, has_template) so the frontend can render enabled/disabled state. - Generate endpoint —
POST /api/projects/{id}/submissions/{code}/generatereturnsapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentwithContent-Disposition: attachment; filename="...".
Slice 1 does NOT add:
- A
/admin/submission-templateseditor (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:
branding.Namefor{{firm.*}}time.Now()(withEurope/Berlinlocale for the long forms) for{{today.*}}userService.GetByID()for{{user.*}}projectService.GetByID()for{{project.*}}partyService.ListByProject()for{{parties.*}}deadlineRuleService.GetByCode()for{{rule.*}}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> |
§ <§> 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.namehonours user locale (Klageerwiderungfor DE,Statement of Defencefor EN).project.case_numberslash/backslash → underscore (Word file name hygiene), other characters preserved.- Date is ISO at server-local (
Europe/Berlin) date. - If
project.case_numberis empty → fall back to a short hash ofproject_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 NULLis 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_extractedjsonb 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'sproceeding_typeare listed. Hearings and decisions don't have submissions. - Per-row state:
has_templatereturned byGET /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
/submissionsindex 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_codein 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
-
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.documentsaudit row records WHAT was generated (template SHA + project state hash, optionally), but not the bytes. -
Gitea round-trip for template edits is friction. Template authors edit
.docxin 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 viaPOST /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. -
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. -
lukasjarosch/go-docxlibrary 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. -
paliad.documents.ai_extractedis 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 likemetadatais out of scope for this task but should be folded into the migration that lands when Phase 5 addsdeadline_id. -
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 oflukasjarosch/go-docx's loop support). -
No Word-side
MERGEFIELDsupport. 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 thetemplates/ README.md. -
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.mdin 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.generatedaudit event_type. - Cleanup task for
ai_extractednaming. 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
.docxfiles 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.documentsis 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.gois the template to lift. - Variable contract pattern:
internal/services/email_template_variables.gois 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:
- Pushing a
.docxtom/mWorkRepo/templates/HLC/de.inf.lg.erwidg.docxand visiting any project withproceeding_type=de.inf.lgsurfaces a[Generieren]Klageerwiderung button. - Clicking it downloads a
.docxnamed per §7 with all §6.2 placeholders resolved (or[KEIN WERT: …]markers for genuinely missing project fields). - Opening the downloaded .docx in Word renders cleanly (no run fragmentation artefacts, no broken styles).
- A row appears in
paliad.documentswithdoc_type='generated_submission',file_path=NULL, andai_extractedjsonb carrying the template path + SHA. - A row appears in
paliad.system_audit_logwithevent_type='submission.generated'. - A row appears in
paliad.project_eventswithevent_type='submission_generated'and shows up in the project's Verlauf / SmartTimeline. - Calling the endpoint without project visibility returns 404.
- Calling the endpoint with no template anywhere in the fallback chain returns 503 with a clear error.
- 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.
go build ./... && go vet ./... && go test ./... && bun run buildall clean.- Manual test on the live database (test admin
tester@hlc.deper memory) against a project with a realde.inf.lgproceeding 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-coderwith 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.