docs(submissions): gap map — Rubrum + letterhead auto-fill from project data (t-paliad-357)

Read-only audit. Maps the 3 doc-generation fill paths (merge.go fills
header/footer placeholders; compose.go passes headers byte-for-byte;
skeleton-as-merge-fallback latent bug), three gap tables (template /
var-bag / data-model), forum-dependence grounded on UPC RoP r.13, and a
tracer-bullet-first wiring proposal. States the A/B fork for m: wire what
current data supports (basic caption, no schema) vs. forum-correct Rubrum
(structured party address/Rechtsform/Sitz + court details + capture UI).
This commit is contained in:
mAi
2026-06-01 12:12:10 +02:00
parent cd3f7843a7
commit 213be10ada

View File

@@ -0,0 +1,306 @@
# Rubrum + Briefkopf auto-fill — gap map (templates ↔ var bag ↔ data model)
**Task:** t-paliad-357 · **Author:** kepler (researcher) · **Date:** 2026-06-01
**Status:** AUDIT + GAP-MAP ONLY. No code or template edits made. Head reviews
before any wiring.
m's ask (2026-06-01 12:01): the **current** document templates should fill the
**letterhead (Briefkopf)** and **recitals/Rubrum (case caption)** from project
data on generation — "filled depending on project". This is the
content-correctness layer, downstream of the (code-complete) docforge engine and
parallel to leibniz's nomen naming train.
---
## TL;DR — the one fork for m
**The basic Rubrum is *already* wired and works today** (party names,
representatives, role designations, case number, court, patent number — all
data-driven, in both the demo per-code template and the Composer caption seed).
**The letterhead is *not* data-driven at all** (the real HL letterhead is
hardcoded inside the firm-skeleton's Word header/footer parts; `firm.signature_block`
is empty). And the Rubrum we have is only a *basic* caption — a forum-correct one
needs structured data paliad does not capture.
So the decision is **how complete a Rubrum we target**:
| | **Option A — wire what data already supports** | **Option B — forum-correct Rubrum** |
|---|---|---|
| Rubrum content | name · representative · role · case no. · court · patent | + structured address · Rechtsform · Sitz (registered office) · gesetzl. Vertreter · service addresses · court chamber/address |
| Data model | **no new columns** — uses existing `parties.*` + `project.*` | **new structured fields** on `parties` (+ maybe `projects`) + capture UI |
| Letterhead | tidy the existing path (firm.name/signature_block) | same as A (letterhead is orthogonal to the A/B choice) |
| Effort | small — mostly template-seed wording + plug `firm.signature_block` | a proper feature — schema migration + party-form rework + Composer reseed |
| Forum-correctness | a *workable* caption, not a *filing-correct* one | meets UPC RoP r.13 / ZPO §253 party-designation requirements |
Everything in Slice 12 below is Option A and is independent of the decision.
Option B is Slice 3+ and is the part that needs m's go/no-go.
---
## 1. Architecture — there are THREE fill realities, not one
The audit's biggest correction to the starting mental model: "the templates" are
not one thing, and the letterhead does **not** live where the Rubrum lives.
### Path 1 — legacy one-click `/generate` → `merge.go` (`SubmissionRenderer.Render`)
- Handler `submissions.go:316``resolveSubmissionTemplate``RenderProjectSubmission`
`renderer.Render` (`pkg/docforge/docx/merge.go`).
- **Substitutes `{{key}}` tokens in `word/document.xml` *and* in `word/header*.xml`
/ `word/footer*.xml`** (`isWordXMLEntry`, merge.go:189). So this path *can* fill a
letterhead in a Word header — **if the header contains `{{placeholders}}`. None
of the shipped headers do** (see §2).
- Template chosen by a 6-tier fallback (`submission_drafts.go:1341`): per-(code,lang)
→ per-code → EN-skeleton → firm-skeleton → universal-skeleton → HL-Patents-Style.
### Path 2 — Composer → `compose.go` (`Composer.Compose`)
- Draft editor with a `base_id` set (t-paliad-313/315/317). Handler
`submission_drafts.go:712``submissionComposer.Compose`.
- Assembles `word/document.xml` from the draft's **`paliad.submission_sections`
rows** (one per section: letterhead, caption, …), splicing each into the
carrier's `{{#section:KEY}}` anchor, then substitutes `{{placeholder}}` inside the
section bodies.
- **Headers/footers pass through byte-for-byte UNTOUCHED** (compose.go:68, :188).
So a Composer doc keeps the base .docx's letterhead chrome verbatim — it is
never data-driven on this path.
- Section bodies are seeded on draft-create from the base's
`section_spec.defaults[*].seed_md_{de,en}` (migrations 146 / 150).
### Path 3 — skeleton as a direct merge fallback (a latent bug)
- For any submission_code **without** a per-code template, `/generate` (Path 1)
falls through to tiers 4/5 and renders the **firm/universal skeleton through
merge.go**. But those skeletons contain only `{{#section:letterhead}}`-style
*block markers*, which `placeholderRegex` (`[A-Za-z]…`) does **not** match (they
start with `#`). **Result: the output Word doc shows literal
`{{#section:letterhead}}` … text.** Only `de.inf.lg.erwidg` has a real per-code
template today, so every other code's one-click `/generate` is exposed to this.
⚠️ **Flag to verify with head** — may be masked if `/generate` is only surfaced
for codes that have a per-code template.
> **Implication for m's ask:** "fill the letterhead from project data" means
> different work on each path. On Path 1 it means *putting `{{firm.*}}` placeholders
> into a header part*. On Path 2 it means *the letterhead is a body section already*
> (and the chrome stays hardcoded in the base). These should be reconciled, not
> both wired blindly — see Slice 2.
---
## 2. Gap table — TEMPLATE side
Fetched live from mgit (`m/mWorkRepo`, `6 - material/Templates/Word/…`), unzipped,
inspected `document.xml` + every `header*.xml`/`footer*.xml`.
| Template | Has header/footer? | Letterhead | Rubrum / caption | Verdict |
|---|---|---|---|---|
| **`HLC/de.inf.lg.erwidg.docx`** (per-code, the only wired code) | no | *pseudo*-letterhead inline in body: `{{firm.name}} — Patentstreitsachen`, Bearbeiter `{{user.display_name}}`, `{{user.email}}`, `{{user.office}}`, `{{today.long_de}}`. `{{firm.signature_block}}` in closing (renders empty). | **full inline Rubrum, all data-driven**: `{{parties.claimant.name}}` / `.representative`, `— Klägerin —`, `gegen`, `{{parties.defendant.name}}` / `.representative`, `— Beklagte —`, `Weitere Beteiligte: {{parties.other.name}}`, `{{project.court}}`, `Aktenzeichen: {{project.case_number}}`, `{{project.patent_number}}`. | Works — but body-banner is **labelled "DEMO — interne Vorlage (nicht freigegeben)"**, not a real letterhead. |
| **`HLC/_firm-skeleton.docx`** (Composer base `hlc-letterhead`) | **yes** — header1/2, footer1/2 | **Real HL letterhead, fully HARDCODED**: footer firm name is a Word SDT content-control literal "Hogan Lovells"; footer2 = static HL entity boilerplate (registered office, 50+ office cities); header2 = logo image only. **Zero `{{placeholders}}` in any header/footer.** | body `document.xml` has only `{{#section:KEY}}` markers (empty). Caption content comes from the section seed (§Composer). | Letterhead present but **not data-driven & not firm-agnostic** (contradicts `branding.Name` goal). |
| **`HLC/_skeleton.docx`** (Composer base `neutral`) | no | none | `{{#section:KEY}}` markers only | Composer scaffold; unusable via merge.go (Path 3 bug). |
| **`Composer/lg-duesseldorf.docx`** (base `lg-duesseldorf`, de.inf.lg) | no | none | `{{#section:KEY}}` markers only | Composer scaffold; letterhead must come from a header it doesn't have, or the body section. |
| **`Composer/upc-formal.docx`** (base `upc-formal`, upc.inf.cfi) | no | none | `{{#section:KEY}}` markers only | same. |
| **`HL Patents Style.dotm`** (last-ditch tier 6) | yes (same HL header/footer as firm-skeleton) | hardcoded HL letterhead | no placeholders | letterhead-only fallback. |
| `HLC/_skeleton.en.docx` | **404 — does not exist** | — | — | EN drafts silently fall back to the DE skeleton (matches code comment at files.go:104). |
**Template-side takeaways**
1. The Rubrum is template-complete on the demo per-code path and is a DB seed (not
a template file) on the Composer path.
2. The real letterhead exists only in the firm-skeleton/`.dotm` headers and is
**100% hardcoded** — no placeholder, no `branding.Name`. A firm rename or a
non-HLC deployment ships the wrong letterhead.
3. The Composer caption/letterhead are **DB seeds (migrations 146/150)**, so
"adjusting the template" for the Composer path means editing the
`section_spec` seed Markdown, *not* the .docx.
---
## 3. Gap table — VAR-BAG side
For every placeholder a correct letterhead + Rubrum needs, is there a bag key?
Bag built in `internal/services/submission_vars.go`.
| Need (letterhead + Rubrum) | Bag key | Status |
|---|---|---|
| Firm name | `firm.name` (← `branding.Name`) | ✅ wired |
| Firm signature block | `firm.signature_block` | ⚠️ **key exists but emits `""`** (reserved "Phase 2", submission_vars.go:324). Template references it → renders blank. |
| Author name / email / office | `user.display_name` / `.email` / `.office` | ✅ wired |
| Date (today, long DE/EN, ISO) | `today` / `.long_de` / `.long_en` / `.iso` | ✅ wired |
| Claimant name / rep (first + indexed + joined) | `parties.claimant.name`, `parties.claimant.0.name` / `.representative`, `parties.claimants` / `.representatives` | ✅ wired (3 forms, addPartyVars) |
| Defendant name / rep | `parties.defendant.*` (same 3 forms) | ✅ wired |
| Other parties (Streithelfer, Patentinhaberin…) | `parties.other.*` / `parties.others` | ✅ wired |
| Case number | `project.case_number` | ✅ wired |
| Court (name) | `project.court` | ✅ wired (free-text string) |
| Patent number (DE + UPC forms) | `project.patent_number` / `.patent_number_upc` | ✅ wired |
| Proceeding type / instance | `project.proceeding.name(_de/_en/.code)`, `project.instance_level` | ✅ wired |
| Our side (DE/EN prose) | `project.our_side_de` / `_en` / raw | ✅ wired |
| Client / matter / internal ref | `project.client_number` / `.matter_number` / `.reference` | ✅ wired |
| **Party postal address** | — | ❌ **NO key** (needs data model) |
| **Party legal form (Rechtsform)** | — | ❌ **NO key** |
| **Party registered office / Sitz** | — | ❌ **NO key** (UPC r.13.1(a)/(b)) |
| **Statutory representative (gesetzl. Vertreter, e.g. Geschäftsführer)** | — | ❌ **NO key** |
| **Address/person for service (Zustellungsbevollmächtigter)** | — | ❌ **NO key** (UPC r.13.1(c)/(d)) |
| **Court full address / chamber / Spruchkörper** | — | ❌ **NO key** (only the court *name* string exists) |
| **Firm letterhead address / contact block** | — | ❌ **NO key** (hardcoded in .docx header) |
**Var-bag takeaways:** every placeholder the *current* templates use is wired,
with one dud: **`firm.signature_block` always renders empty** — the single cheapest
letterhead/closing win. Everything a *forum-correct* Rubrum additionally needs has
**no key, because the data isn't captured** (§4).
---
## 4. Gap table — DATA-MODEL side
`models.Party` (models.go:539) carries **only**: `Name`, `Role`, `Representative`,
`ContactInfo json.RawMessage`. `models.Project` carries `Court *string` (free text),
`CaseNumber`, `PatentNumber`, dates, `OurSide`, `InstanceLevel`, client/matter.
- **`parties.contact_info` is a dormant jsonb column**: `PartyService.Create`
defaults it to `{}` and **no UI ever writes it** (party form captures only
Name / Role / Representative — `frontend/src/projects-detail.tsx:436460`). It is
a ready-made parking spot, but it is structurally empty today.
- **No court registry / court-address table exists.** `project.court` is a plain
string a user types.
| Forum-correct Rubrum needs | Derivable from existing fields? | Park in `contact_info` jsonb? | Needs new column + capture UI? | Cost |
|---|---|---|---|---|
| Party **postal address** | ❌ | ✅ feasible (`{address:{street,zip,city,country}}`) | UI: add fields to party form | **LowMed** — jsonb, no migration; party-form + bag resolver |
| Party **Rechtsform** (GmbH, LLP…) | ❌ (sometimes inside Name string, unreliable) | ✅ | UI field | **Low** |
| Party **Sitz / registered office** (UPC r.13.1(a/b)) | ❌ | ✅ | UI field | **LowMed** |
| Party **statutory representative** (Geschäftsführer / vertreten durch …) | ⚠️ partial — `Representative` today means the *lawyer/Prozessbevollmächtigter*, not the *organ*; conflating them is wrong | ✅ (`{statutory_rep:…}`) | UI field + relabel existing `representative` | **Med** — semantic untangle |
| **Address for service / Zustellungsbevollmächtigter** (UPC r.13.1(c/d)) | ❌ | ✅ | UI field | **LowMed** |
| **Court full address** | ❌ | n/a (project-level) | new `projects.court_address` col **or** a courts lookup table | **Med** (col) / **High** (registry) |
| **Court chamber / Spruchkörper / panel** | ❌ | n/a | new `projects.court_chamber` col | **LowMed** |
| Firm letterhead address block | ❌ | n/a | `branding`-level config (env or table) | **Med** — touches firm-agnostic story |
**Recommendation on storage:** structured party attributes belong in **typed jsonb
under `contact_info`** with a small Go struct (`models.PartyContact`) decoding it —
not a column-per-attribute migration. It keeps the party table stable, is
forum-shape-agnostic, and the bag resolver can emit `parties.claimant.0.address`,
`.sitz`, `.rechtsform`, etc. Court chamber/address are project-level and small
enough for two nullable columns; a full court **registry** is a separate, larger
feature (nice for autofill + validation, not required for a correct caption).
---
## 5. Forum-dependence — does one parametric Rubrum cover UPC / LG / OLG / BPatG?
Grounded sources: **UPC RoP Rule 13** ("Contents of the Statement of claim") pulled
verbatim from the house laws corpus (`data.laws`, `UPCRoP.013.*`). German ZPO/PatG
caption conventions below are **standard German civil-procedure practice — these are
NOT in the youpc corpus** (which is UPC/EPC-only), so they are flagged as
practitioner-convention, to be confirmed by a DE-litigation reviewer (lexy) before
wording is finalised.
**What UPC RoP r.13.1 demands (verified):**
- (a) claimant name; if corporate, **location of registered office**; + claimant's representative
- (b) defendant name; if corporate, **location of registered office**
- (c) **postal + electronic addresses for service** on claimant + persons authorised to accept service
- (d) postal/electronic service addresses on defendant + persons authorised, if known
- (e) proprietor service addresses where claimant ≠ (sole) proprietor
- (g) details of the patent including the **number**
- (k) nature of the claim / order / remedy sought
→ paliad today supplies only **name** (a/b) and **patent number** (g). It captures
**none** of: registered office/Sitz, postal/electronic service address, persons
authorised. So a *filing-correct* UPC caption is firmly **Option B** territory.
**How the caption shape differs across forums (convention):**
| Forum | Heading | Party designations | "wegen" / subject | Court line |
|---|---|---|---|---|
| **DE LG** (Patentstreitkammer) | "In dem Rechtsstreit" / "In der Patentstreitsache" | Kläger(in) / Beklagte(r); parties need **Name, Anschrift, Rechtsform, ges. Vertreter** (ZPO §253 Abs. 2 Nr. 1, §130 Nr. 1 — *convention*) | "**wegen** Patentverletzung" | "an das Landgericht … , … Kammer" — court **name + chamber** |
| **DE OLG** (Berufung) | "In dem Rechtsstreit" | **Berufungskläger / Berufungsbeklagte** (roles flip vs. first instance) | "wegen …" | "an das Oberlandesgericht …, … Senat" |
| **BPatG** (Nichtigkeit/Beschwerde) | "In der Patentnichtigkeitssache" / "Beschwerdesache" | **Kläger/Beklagte** (nullity) or **Anmelder/Einsprechende**; patent-centric | patent + nullity ground | "an das Bundespatentgericht, … Senat" |
| **UPC CFI** | "In the matter / In der Sache" | **Claimant / Defendant (Kläger/Beklagte)**; name + **registered office** + service address (r.13) | claim nature (r.13.1(k)) | division + "Aktenzeichen" (UPC case-number format `ACT_xxxxx/2026`) |
**Answer:** one *parametric* Rubrum block covers the **basic** caption across forums
(swap designation labels + heading + court line from `our_side`/`instance_level`/
`proceeding.code` — values the bag already has). It does **not** cover the
forum-specific *content requirements* (UPC service addresses vs. ZPO Anschrift/
Rechtsform vs. BPatG patent-centric framing). For Option B, the cleanest design is
**one caption section whose seed Markdown is chosen per `proceeding_family`** (the
Composer already keys bases by `proceeding_family``de.inf.lg`, `upc.inf.cfi`),
i.e. **forum-specific caption seeds, shared resolver keys** — not a single
universal block, and not N hand-maintained .docx files.
---
## 6. Sliced wiring proposal (tracer-bullet first)
Ordered so each slice ships value alone; the A/B fork only bites at Slice 3.
**Slice 1 — plug the empty letterhead key (pure win, no schema, no fork).**
- Fill `firm.signature_block` in `addFirmVars` from `branding` (firm name + office /
a configured block) instead of hardcoding `""`. Today every template that
references it renders blank.
- Decide letterhead source of truth: either (a) inject `{{firm.name}}` /
`{{firm.address}}` placeholders into the firm-skeleton **header** parts (Path 1
fills them; Composer leaves them — acceptable since chrome is firm-fixed), or
(b) keep chrome hardcoded but make it firm-agnostic via `branding`. **Recommend
(a)** so a firm rename / non-HLC deploy doesn't ship "Hogan Lovells".
- Template edits: firm-skeleton `header1/footer1` get `{{firm.*}}` tokens. (mWorkRepo,
authored as mAi — not this repo.)
**Slice 2 — reconcile the letterhead duplication + kill the Path-3 junk.**
- The Composer seeds a body "letterhead" section *and* the base has a header
letterhead → a Composer doc can show both. Decide: drop the body letterhead
section for letterhead-bearing bases, or keep it only for `neutral`.
- Fix Path 3: either give the universal/firm skeleton a **merge-safe** variant
(real `{{key}}` Rubrum like the demo template) for non-Composer `/generate`, or
gate `/generate` to codes that have a per-code template. (Verify with head which
codes expose `/generate`.)
**Slice 3 — Option A "good basic Rubrum" (no new data).**
- Promote the demo per-code Rubrum wording into a **published, forum-labelled
caption** and align the Composer caption seeds (146/150) to the same wording.
Parametrise designation labels + heading + "wegen" + court line off
`our_side` / `instance_level` / `proceeding.code`. **No migration.**
- This is the natural stopping point if m picks **A**.
**Slice 4 — Option B data model (the feature; needs m's go).**
- Add `models.PartyContact` decoding typed `contact_info` jsonb:
`{address, rechtsform, sitz, statutory_rep, service_address, service_agent}`.
- Extend the party form (`projects-detail.tsx`) with those inputs; `PartyService`
writes them.
- Add `projects.court_address` + `projects.court_chamber` (nullable cols).
- New bag keys in `addPartyVars` / `addProjectVars`:
`parties.<role>.<i>.address|sitz|rechtsform|statutory_rep|service_address`,
`project.court_address|court_chamber`.
**Slice 5 — Option B forum-correct caption seeds.**
- Per-`proceeding_family` caption seed Markdown (UPC r.13 shape, DE-LG ZPO shape,
OLG appeal-role shape, BPatG nullity shape), consuming the Slice-4 keys.
- Reviewer (lexy) signs off DE conventions before publish.
**Slice 6 (optional) — court registry** for autofill/validation of court
name+address+chamber. Larger; not required for a correct caption.
---
## 7. Key files (for the wiring worker)
- Var bag: `internal/services/submission_vars.go` (addFirmVars:319, addPartyVars:412,
addProjectVars:349).
- Render (Path 1, fills headers): `pkg/docforge/docx/merge.go` (isWordXMLEntry:189).
- Compose (Path 2, headers pass-through): `pkg/docforge/docx/compose.go` (:68,:188);
`internal/services/submission_compose.go`.
- Template resolution: `internal/handlers/submission_drafts.go:1341`
(`resolveSubmissionTemplate`, 6 tiers); paths in `internal/handlers/files.go`.
- Composer base seeds (caption/letterhead Markdown): migrations
`internal/db/migrations/146_submission_bases.up.sql`,
`150_submission_bases_specialist.up.sql`.
- Data model: `internal/models/models.go` (Party:539, Project:80);
party form `frontend/src/projects-detail.tsx:436`.
- Live templates: `m/mWorkRepo` `6 - material/Templates/Word/Paliad/{HLC,Composer}/`.
---
## 8. Open questions for m / head
1. **A or B?** (the §TL;DR fork). A = ship a good basic caption now, no data work.
B = capture structured party/court data for a filing-correct Rubrum.
2. **Letterhead source of truth:** placeholderise the firm-skeleton header (firm-agnostic)
vs. keep hardcoded HL chrome? (Slice 1 recommends placeholderise.)
3. **Path-3 junk:** is one-click `/generate` exposed for codes lacking a per-code
template? If yes, the literal `{{#section:…}}` output is a live bug.
4. **`representative` semantics:** today it's the lawyer (Prozessbevollmächtigter).
A forum Rubrum also needs the party's *statutory* representative (Geschäftsführer).
Keep them as two distinct fields under Option B.