Merge remote-tracking branch 'origin/main' into mai/fermi/coder-implement-nomen

This commit is contained in:
mAi
2026-06-01 13:05:30 +02:00
8 changed files with 532 additions and 75 deletions

View File

@@ -0,0 +1,55 @@
-- Revert t-paliad-358 A-S2: restore each base's original (pre-parametric)
-- caption seed_md from migrations 146 / 150, verbatim. One UPDATE per slug
-- because the originals differed per base.
-- hlc-letterhead (mig 146): heading + parties with "vertreten durch" + court.
UPDATE paliad.submission_bases AS b
SET section_spec = jsonb_set(b.section_spec, '{defaults}', (
SELECT jsonb_agg(
CASE WHEN elem->>'section_key' = 'caption'
THEN elem || jsonb_build_object(
'seed_md_de', E'In der Sache\n\n**{{parties.claimant.0.name}}**\nvertreten durch {{parties.claimant.0.representative}}\n\n— Klägerin —\n\ngegen\n\n**{{parties.defendant.0.name}}**\nvertreten durch {{parties.defendant.0.representative}}\n\n— Beklagte —\n\nAktenzeichen: {{project.case_number}}\n{{project.court}}',
'seed_md_en', E'In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n\n— Defendant —\n\nCase number: {{project.case_number}}\n{{project.court}}')
ELSE elem END
ORDER BY ord)
FROM jsonb_array_elements(b.section_spec->'defaults') WITH ORDINALITY AS d(elem, ord)))
WHERE b.slug = 'hlc-letterhead' AND b.section_spec ? 'defaults';
-- neutral (mig 146): heading + parties (no representative) + Aktenzeichen, no court.
UPDATE paliad.submission_bases AS b
SET section_spec = jsonb_set(b.section_spec, '{defaults}', (
SELECT jsonb_agg(
CASE WHEN elem->>'section_key' = 'caption'
THEN elem || jsonb_build_object(
'seed_md_de', E'In der Sache\n\n**{{parties.claimant.0.name}}**\n— Klägerin —\n\ngegen\n\n**{{parties.defendant.0.name}}**\n— Beklagte —\n\nAktenzeichen: {{project.case_number}}',
'seed_md_en', E'In the matter\n\n**{{parties.claimant.0.name}}**\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\n— Defendant —\n\nCase number: {{project.case_number}}')
ELSE elem END
ORDER BY ord)
FROM jsonb_array_elements(b.section_spec->'defaults') WITH ORDINALITY AS d(elem, ord)))
WHERE b.slug = 'neutral' AND b.section_spec ? 'defaults';
-- lg-duesseldorf (mig 150): heading + parties (no representative) + court.
UPDATE paliad.submission_bases AS b
SET section_spec = jsonb_set(b.section_spec, '{defaults}', (
SELECT jsonb_agg(
CASE WHEN elem->>'section_key' = 'caption'
THEN elem || jsonb_build_object(
'seed_md_de', E'In der Sache\n\n**{{parties.claimant.0.name}}**\n— Klägerin —\n\ngegen\n\n**{{parties.defendant.0.name}}**\n— Beklagte —\n\nAktenzeichen: {{project.case_number}}\n{{project.court}}',
'seed_md_en', E'In the matter\n\n**{{parties.claimant.0.name}}**\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\n— Defendant —\n\nCase number: {{project.case_number}}\n{{project.court}}')
ELSE elem END
ORDER BY ord)
FROM jsonb_array_elements(b.section_spec->'defaults') WITH ORDINALITY AS d(elem, ord)))
WHERE b.slug = 'lg-duesseldorf' AND b.section_spec ? 'defaults';
-- upc-formal (mig 150): UPC heading + parties with "represented by" + UPC case number + patent.
UPDATE paliad.submission_bases AS b
SET section_spec = jsonb_set(b.section_spec, '{defaults}', (
SELECT jsonb_agg(
CASE WHEN elem->>'section_key' = 'caption'
THEN elem || jsonb_build_object(
'seed_md_de', E'# In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n— Defendant —\n\nUPC-Aktenzeichen: {{project.case_number}}\nStreitpatent: {{project.patent_number_upc}}',
'seed_md_en', E'# In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n— Defendant —\n\nUPC case number: {{project.case_number}}\nPatent in suit: {{project.patent_number_upc}}')
ELSE elem END
ORDER BY ord)
FROM jsonb_array_elements(b.section_spec->'defaults') WITH ORDINALITY AS d(elem, ord)))
WHERE b.slug = 'upc-formal' AND b.section_spec ? 'defaults';

View File

@@ -0,0 +1,43 @@
-- t-paliad-358 A-S2 — unify the Composer caption (Rubrum) seed across every
-- base onto the shared parametric caption.* resolver keys.
--
-- Before: each base seeded a hand-written caption with hard-coded designations
-- ("— Klägerin —" / "— Claimant —") and heading ("In der Sache" / "In the
-- matter"). That wording diverged from the per-code .docx templates and the
-- merge-fallback skeleton, and could not reflect the forum (UPC vs DE-LG vs
-- nullity vs appeal).
--
-- After: every base's caption section references the {{caption.*}} keys
-- (addCaptionVars, submission_vars.go), so the heading, party designations,
-- versus connector and "wegen" subject are resolved per forum from
-- project.proceeding (jurisdiction + code + role-label overrides) +
-- project.instance_level — the SAME wording the templates and the fallback
-- skeleton now use. One parametric caption, shared keys.
--
-- Forward-only effect: section seeds are applied when a NEW draft is created
-- from a base; existing drafts keep their already-seeded (possibly user-edited)
-- caption text untouched.
--
-- Position-independent: rewrites only the element whose section_key='caption'
-- inside section_spec->'defaults', preserving order (WITH ORDINALITY) and every
-- other field on the element (elem || patch).
UPDATE paliad.submission_bases AS b
SET section_spec = jsonb_set(
b.section_spec,
'{defaults}',
(
SELECT jsonb_agg(
CASE WHEN elem->>'section_key' = 'caption'
THEN elem || jsonb_build_object(
'seed_md_de', E'{{caption.heading_de}}\n\n**{{parties.claimant.0.name}}**\nvertreten durch {{parties.claimant.0.representative}}\n\n— {{caption.claimant_designation_de}} —\n\n{{caption.versus_de}}\n\n**{{parties.defendant.0.name}}**\nvertreten durch {{parties.defendant.0.representative}}\n\n— {{caption.defendant_designation_de}} —\n\nwegen {{caption.subject_de}}\n\nAktenzeichen: {{project.case_number}}\n{{project.court}}',
'seed_md_en', E'{{caption.heading_en}}\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n\n— {{caption.claimant_designation_en}} —\n\n{{caption.versus_en}}\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n\n— {{caption.defendant_designation_en}} —\n\nre {{caption.subject_en}}\n\nCase number: {{project.case_number}}\n{{project.court}}')
ELSE elem
END
ORDER BY ord
)
FROM jsonb_array_elements(b.section_spec->'defaults') WITH ORDINALITY AS d(elem, ord)
)
)
WHERE b.slug IN ('hlc-letterhead', 'neutral', 'lg-duesseldorf', 'upc-formal')
AND b.section_spec ? 'defaults';

View File

@@ -202,6 +202,7 @@ func (s *SubmissionVarsService) Build(ctx context.Context, in SubmissionVarsCont
resolvers = append(resolvers,
projectResolver{project: project, pt: pt, lang: lang},
captionResolver{project: project, pt: pt, lang: lang},
partiesResolver{parties: filterPartiesBySelection(parties, in.SelectedParties)},
deadlineResolver{deadline: next, project: project, lang: lang},
)
@@ -377,6 +378,7 @@ func addProjectVars(bag PlaceholderMap, p *models.Project, pt *models.Proceeding
bag["project.matter_number"] = derefString(p.MatterNumber)
if pt != nil {
bag["project.proceeding.code"] = pt.Code
bag["project.proceeding.jurisdiction"] = derefString(pt.Jurisdiction)
if strings.EqualFold(lang, "en") {
bag["project.proceeding.name"] = pt.NameEN
} else {
@@ -387,6 +389,160 @@ func addProjectVars(bag PlaceholderMap, p *models.Project, pt *models.Proceeding
}
}
// addCaptionVars populates the caption.* namespace — the parametric pieces of
// the case caption (Rubrum) shared by every render path (the merge fallback
// skeleton, the per-code .docx templates, and the Composer caption seeds) so
// the wording stays unified rather than diverging per path (t-paliad-358 A-S2).
//
// Each piece is offered in three forms, mirroring the project.proceeding.name
// convention: a bare key resolved to the draft language, plus explicit _de /
// _en variants (the bilingual .docx/seed surfaces reference the explicit
// variant for the language they are written in).
//
// Parametrisation is driven by data the bag already has — no new schema:
// - designations (claimant/defendant) reuse the proceeding-type role-label
// overrides (Berufungskläger, Antragsteller (Nichtigkeit), Einsprechende(r),
// …; mig 137). Where a proceeding carries no override the caption falls back
// to the civil default Klägerin/Beklagte // Claimant/Defendant. This means
// DE appeal/nullity/cassation forums that lack role-label data today
// (de.inf.olg, de.inf.bgh, de.null.bpatg, de.null.bgh) render the generic
// designation — flagged for a lexy review + role-label backfill, NOT
// guessed here.
// - heading / subject are computed from the proceeding jurisdiction + the
// "nature" segment of the dotted code (inf / null / rev / opp / …). These
// are practitioner-convention wordings (German caption conventions are not
// in the youpc corpus) — flagged for lexy.
// - the court line is left as {{project.court}} (free text); forum-specific
// framing ("an das Landgericht …, … Kammer/Senat") needs chamber data we
// do not capture (Option B).
//
// our_side is intentionally NOT a driver: the caption designates BOTH parties
// by their procedural role regardless of which side we act for; our_side has
// its own prose keys (project.our_side_*).
func addCaptionVars(bag PlaceholderMap, p *models.Project, pt *models.ProceedingType, lang string) {
c := resolveCaption(p, pt)
set := func(base, de, en string) {
bag["caption."+base+"_de"] = de
bag["caption."+base+"_en"] = en
if strings.EqualFold(lang, "en") {
bag["caption."+base] = en
} else {
bag["caption."+base] = de
}
}
set("heading", c.headingDE, c.headingEN)
set("claimant_designation", c.claimantDE, c.claimantEN)
set("defendant_designation", c.defendantDE, c.defendantEN)
set("versus", c.versusDE, c.versusEN)
set("subject", c.subjectDE, c.subjectEN)
}
// captionParts holds the resolved bilingual caption pieces.
type captionParts struct {
headingDE, headingEN string
claimantDE, claimantEN string
defendantDE, defendantEN string
versusDE, versusEN string
subjectDE, subjectEN string
}
// resolveCaption computes the parametric caption pieces from the proceeding
// type (jurisdiction + dotted code + role-label overrides). Pure function for
// unit testing — no DB, no bag.
func resolveCaption(p *models.Project, pt *models.ProceedingType) captionParts {
c := captionParts{
// Civil defaults — overridden below per forum / role-label data.
headingDE: "In der Sache", headingEN: "In the matter",
claimantDE: "Klägerin", claimantEN: "Claimant",
defendantDE: "Beklagte", defendantEN: "Defendant",
versusDE: "gegen", versusEN: "v.",
subjectDE: "Patentstreitsache", subjectEN: "patent matter",
}
var jurisdiction, nature string
if pt != nil {
jurisdiction = strings.ToUpper(derefString(pt.Jurisdiction))
nature = captionNature(pt.Code)
}
// Heading + subject by jurisdiction and proceeding nature.
switch {
case jurisdiction == "UPC":
c.headingDE, c.headingEN = "In der Sache", "In the matter"
case jurisdiction == "DE" && nature == "null":
c.headingDE, c.headingEN = "In der Patentnichtigkeitssache", "In the nullity matter"
case jurisdiction == "DE" && nature == "inf":
c.headingDE, c.headingEN = "In dem Rechtsstreit", "In the matter"
case nature == "opp": // EPA / DPMA opposition
c.headingDE, c.headingEN = "Im Einspruchsverfahren", "In the opposition proceedings"
}
switch nature {
case "inf":
c.subjectDE, c.subjectEN = "Patentverletzung", "patent infringement"
case "null", "rev":
c.subjectDE, c.subjectEN = "Nichtigkeit des Streitpatents", "revocation of the patent in suit"
case "opp":
c.subjectDE, c.subjectEN = "Einspruch gegen das Streitpatent", "opposition to the patent in suit"
}
// Designations — precedence: explicit proceeding role-label override >
// instance-derived (appeal/cassation) > civil default.
//
// 1. Role-label overrides (mig 137) capture the proceedings whose naming
// diverges in a forum-specific way: upc.apl.unified (Berufungskläger),
// upc.rev.cfi (Antragsteller (Nichtigkeit)), epa.opp.* (Einsprechende(r)
// / Patentinhaber(in)). These are authoritative — use them verbatim.
// 2. Otherwise the procedural instance shifts the civil designation: an
// appeal makes the parties Berufungskläger(in)/Berufungsbeklagte(r)
// (Appellant/Respondent), a cassation Revisionskläger(in)/
// Revisionsbeklagte(r). DE appeal/nullity forums (de.inf.olg,
// de.null.bgh, …) carry no role-label override today, so this fills the
// gap when project.instance_level is set.
// 3. Else the first-instance civil default (Klägerin/Beklagte // Claimant/
// Defendant) already in c.
instance := ""
if p != nil {
instance = strings.ToLower(derefString(p.InstanceLevel))
}
switch instance {
case "appeal":
c.claimantDE, c.defendantDE = "Berufungskläger(in)", "Berufungsbeklagte(r)"
c.claimantEN, c.defendantEN = "Appellant", "Respondent"
case "cassation":
c.claimantDE, c.defendantDE = "Revisionskläger(in)", "Revisionsbeklagte(r)"
c.claimantEN, c.defendantEN = "Appellant", "Respondent"
}
if pt != nil {
if v := derefString(pt.RoleProactiveLabelDE); v != "" {
c.claimantDE = v
}
if v := derefString(pt.RoleProactiveLabelEN); v != "" {
c.claimantEN = v
}
if v := derefString(pt.RoleReactiveLabelDE); v != "" {
c.defendantDE = v
}
if v := derefString(pt.RoleReactiveLabelEN); v != "" {
c.defendantEN = v
}
}
return c
}
// captionNature returns the proceeding "nature" segment of a dotted proceeding
// code (e.g. "de.inf.lg" → "inf", "upc.rev.cfi" → "rev", "epa.opp.opd" →
// "opp", "de.null.bpatg" → "null"). Empty when the code has no second segment.
func captionNature(code string) string {
parts := strings.Split(code, ".")
if len(parts) >= 2 {
return parts[1]
}
return ""
}
// addPartyVars populates the parties.* namespace from the (already
// filtered) list of parties.
//

View File

@@ -0,0 +1,146 @@
package services
// Pins the parametric caption resolver (t-paliad-358 A-S2): heading / subject
// derive from jurisdiction + the proceeding code's nature segment; designations
// reuse the proceeding role-label overrides, fall back to instance-derived
// appeal/cassation wording, then to the civil default.
import (
"testing"
"mgit.msbls.de/m/paliad/internal/models"
)
func sp(s string) *string { return &s }
func ptType(code, jurisdiction string) *models.ProceedingType {
return &models.ProceedingType{Code: code, Jurisdiction: sp(jurisdiction)}
}
func TestResolveCaption(t *testing.T) {
cases := []struct {
name string
project *models.Project
pt *models.ProceedingType
wantHeadDE string
wantClaimDE string
wantDefDE string
wantSubjDE string
}{
{
name: "DE LG infringement → Rechtsstreit / Kläger-Beklagte / Patentverletzung",
project: &models.Project{},
pt: ptType("de.inf.lg", "DE"),
wantHeadDE: "In dem Rechtsstreit",
wantClaimDE: "Klägerin",
wantDefDE: "Beklagte",
wantSubjDE: "Patentverletzung",
},
{
name: "DE BPatG nullity → Patentnichtigkeitssache",
project: &models.Project{},
pt: ptType("de.null.bpatg", "DE"),
wantHeadDE: "In der Patentnichtigkeitssache",
wantClaimDE: "Klägerin",
wantDefDE: "Beklagte",
wantSubjDE: "Nichtigkeit des Streitpatents",
},
{
name: "UPC infringement → In der Sache / civil default",
project: &models.Project{},
pt: ptType("upc.inf.cfi", "UPC"),
wantHeadDE: "In der Sache",
wantClaimDE: "Klägerin",
wantDefDE: "Beklagte",
wantSubjDE: "Patentverletzung",
},
{
name: "UPC revocation → role-label override (Antragsteller Nichtigkeit)",
project: &models.Project{},
pt: &models.ProceedingType{
Code: "upc.rev.cfi", Jurisdiction: sp("UPC"),
RoleProactiveLabelDE: sp("Antragsteller (Nichtigkeit)"),
RoleReactiveLabelDE: sp("Antragsgegner (Nichtigkeit)"),
},
wantHeadDE: "In der Sache",
wantClaimDE: "Antragsteller (Nichtigkeit)",
wantDefDE: "Antragsgegner (Nichtigkeit)",
wantSubjDE: "Nichtigkeit des Streitpatents",
},
{
name: "UPC appeal → role-label override wins over instance",
project: &models.Project{InstanceLevel: sp("appeal")},
pt: &models.ProceedingType{
Code: "upc.apl.unified", Jurisdiction: sp("UPC"),
RoleProactiveLabelDE: sp("Berufungskläger"),
RoleReactiveLabelDE: sp("Berufungsbeklagter"),
},
wantHeadDE: "In der Sache",
wantClaimDE: "Berufungskläger",
wantDefDE: "Berufungsbeklagter",
wantSubjDE: "Patentstreitsache",
},
{
name: "DE OLG appeal via instance_level (no role-label data)",
project: &models.Project{InstanceLevel: sp("appeal")},
pt: ptType("de.inf.olg", "DE"),
wantHeadDE: "In dem Rechtsstreit",
wantClaimDE: "Berufungskläger(in)",
wantDefDE: "Berufungsbeklagte(r)",
wantSubjDE: "Patentverletzung",
},
{
name: "EPA opposition → Einsprechende(r) / Patentinhaber(in)",
project: &models.Project{},
pt: &models.ProceedingType{
Code: "epa.opp.opd", Jurisdiction: sp("EPA"),
RoleProactiveLabelDE: sp("Einsprechende(r)"),
RoleReactiveLabelDE: sp("Patentinhaber(in)"),
},
wantHeadDE: "Im Einspruchsverfahren",
wantClaimDE: "Einsprechende(r)",
wantDefDE: "Patentinhaber(in)",
wantSubjDE: "Einspruch gegen das Streitpatent",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := resolveCaption(c.project, c.pt)
if got.headingDE != c.wantHeadDE {
t.Errorf("headingDE = %q, want %q", got.headingDE, c.wantHeadDE)
}
if got.claimantDE != c.wantClaimDE {
t.Errorf("claimantDE = %q, want %q", got.claimantDE, c.wantClaimDE)
}
if got.defendantDE != c.wantDefDE {
t.Errorf("defendantDE = %q, want %q", got.defendantDE, c.wantDefDE)
}
if got.subjectDE != c.wantSubjDE {
t.Errorf("subjectDE = %q, want %q", got.subjectDE, c.wantSubjDE)
}
})
}
}
// addCaptionVars must emit bare + _de + _en forms, with the bare form resolved
// to the draft language.
func TestAddCaptionVars_BareResolvesToLang(t *testing.T) {
pt := ptType("de.inf.lg", "DE")
proj := &models.Project{}
bagDE := PlaceholderMap{}
addCaptionVars(bagDE, proj, pt, "de")
if bagDE["caption.heading"] != "In dem Rechtsstreit" {
t.Errorf("DE bare heading = %q", bagDE["caption.heading"])
}
if bagDE["caption.heading_en"] != "In the matter" {
t.Errorf("heading_en = %q", bagDE["caption.heading_en"])
}
bagEN := PlaceholderMap{}
addCaptionVars(bagEN, proj, pt, "en")
if bagEN["caption.heading"] != "In the matter" {
t.Errorf("EN bare heading = %q", bagEN["caption.heading"])
}
}

View File

@@ -26,6 +26,7 @@ var (
_ docforge.VariableResolver = userResolver{}
_ docforge.VariableResolver = proceduralEventResolver{}
_ docforge.VariableResolver = projectResolver{}
_ docforge.VariableResolver = captionResolver{}
_ docforge.VariableResolver = partiesResolver{}
_ docforge.VariableResolver = deadlineResolver{}
)
@@ -48,6 +49,7 @@ func SubmissionVariableCatalogue() []docforge.VariableKey {
userResolver{},
proceduralEventResolver{},
projectResolver{},
captionResolver{},
partiesResolver{},
deadlineResolver{},
).Catalogue()
@@ -149,12 +151,38 @@ func (projectResolver) Keys() []docforge.VariableKey {
vk("project", "project.client_number", "Mandantennummer", "Client number"),
vk("project", "project.matter_number", "Matter-Nummer", "Matter number"),
vk("project", "project.proceeding.code", "Verfahrenstyp (Code)", "Proceeding type (code)"),
vk("project", "project.proceeding.jurisdiction", "Gerichtsbarkeit", "Jurisdiction"),
vk("project", "project.proceeding.name", "Verfahrenstyp", "Proceeding type"),
vk("project", "project.proceeding.name_de", "Verfahrenstyp (DE)", "Proceeding type (DE)"),
vk("project", "project.proceeding.name_en", "Verfahrenstyp (EN)", "Proceeding type (EN)"),
}
}
// captionResolver populates caption.* — the parametric case-caption (Rubrum)
// pieces shared across every render path (merge fallback skeleton, per-code
// .docx templates, Composer caption seeds) so the wording stays unified
// (t-paliad-358 A-S2). Needs the project (instance level) + proceeding type
// (jurisdiction, code, role-label overrides); see addCaptionVars.
type captionResolver struct {
project *models.Project
pt *models.ProceedingType
lang string
}
func (captionResolver) Namespace() string { return "caption" }
func (r captionResolver) Populate(bag PlaceholderMap) {
addCaptionVars(bag, r.project, r.pt, r.lang)
}
func (captionResolver) Keys() []docforge.VariableKey {
return []docforge.VariableKey{
vk("caption", "caption.heading", "Rubrum-Überschrift", "Caption heading"),
vk("caption", "caption.claimant_designation", "Bezeichnung Klägerseite", "Claimant designation"),
vk("caption", "caption.defendant_designation", "Bezeichnung Beklagtenseite", "Defendant designation"),
vk("caption", "caption.versus", "Gegen-Konnektor", "Versus connector"),
vk("caption", "caption.subject", "Streitgegenstand (wegen)", "Subject matter (re)"),
}
}
// partiesResolver populates parties.* from the (already filtered) party list.
type partiesResolver struct{ parties []models.Party }

View File

@@ -123,68 +123,79 @@ const fallbackStylesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"
</w:styles>`
// fallbackLabels holds the language-dependent static text for the skeleton.
// Dynamic values stay as {{key}} placeholders regardless of language.
// Dynamic values stay as {{key}} placeholders regardless of language. The
// caption pieces (heading / designations / versus / subject) are themselves
// {{caption.*}} placeholders so the Rubrum wording is the SAME parametric
// caption the per-code templates and Composer seeds use (t-paliad-358 A-S2) —
// resolved per forum by addCaptionVars from the variable bag.
type fallbackLabels struct {
editor string // "Bearbeiter:" / "Attorney:"
dateKey string // {{today.long_de}} / {{today.long_en}}
caseNo string // "Aktenzeichen:" / "Case no.:"
inTheMatter string // "In der Sache" / "In the matter"
representedBy string // "vertreten durch" / "represented by"
claimantRole string // role designation line
versus string // "gegen" / "against"
defendantRole string
others string // "Weitere Beteiligte:" / "Further parties:"
subject string // "Betreff" / "Subject"
patent string // "Streitpatent:" / "Patent in suit:"
proceeding string // "Verfahrensart:" / "Proceeding:"
ourSideKey string // {{project.our_side_de}} / {{project.our_side_en}}
bodyHint string // editorial placeholder for the actual submission text
closing string // "Schlussformel" / "Closing"
editor string // "Bearbeiter:" / "Attorney:"
dateKey string // {{today.long_de}} / {{today.long_en}}
caseNo string // "Aktenzeichen:" / "Case no.:"
heading string // {{caption.heading_de}} / {{caption.heading_en}}
representedBy string // "vertreten durch" / "represented by"
claimantDesig string // — {{caption.claimant_designation_*}} —
versus string // {{caption.versus_de}} / {{caption.versus_en}}
defendantDesig string
others string // "Weitere Beteiligte:" / "Further parties:"
wegen string // "wegen" / "re"
subjectKey string // {{caption.subject_de}} / {{caption.subject_en}}
subjectLabel string // "Betreff" / "Subject"
patent string // "Streitpatent:" / "Patent in suit:"
proceeding string // "Verfahrensart:" / "Proceeding:"
ourSideKey string // {{project.our_side_de}} / {{project.our_side_en}}
bodyHint string // editorial placeholder for the actual submission text
closing string // "Schlussformel" / "Closing"
}
func fallbackLabelsFor(lang string) fallbackLabels {
if strings.EqualFold(lang, "en") {
return fallbackLabels{
editor: "Attorney:",
dateKey: "{{today.long_en}}",
caseNo: "Case no.:",
inTheMatter: "In the matter",
representedBy: "represented by",
claimantRole: "— Claimant / Patent proprietor / Applicant —",
versus: "against",
defendantRole: "— Defendant / Opponent / Respondent —",
others: "Further parties:",
subject: "Subject",
patent: "Patent in suit:",
proceeding: "Proceeding:",
ourSideKey: "{{project.our_side_en}}",
bodyHint: "[Body of the submission goes here. This is a basic skeleton — fill in according to the submission type.]",
closing: "Closing",
editor: "Attorney:",
dateKey: "{{today.long_en}}",
caseNo: "Case no.:",
heading: "{{caption.heading_en}}",
representedBy: "represented by",
claimantDesig: "— {{caption.claimant_designation_en}} —",
versus: "{{caption.versus_en}}",
defendantDesig: "— {{caption.defendant_designation_en}} —",
others: "Further parties:",
wegen: "re",
subjectKey: "{{caption.subject_en}}",
subjectLabel: "Subject",
patent: "Patent in suit:",
proceeding: "Proceeding:",
ourSideKey: "{{project.our_side_en}}",
bodyHint: "[Body of the submission goes here. This is a basic skeleton — fill in according to the submission type.]",
closing: "Closing",
}
}
return fallbackLabels{
editor: "Bearbeiter:",
dateKey: "{{today.long_de}}",
caseNo: "Aktenzeichen:",
inTheMatter: "In der Sache",
representedBy: "vertreten durch",
claimantRole: "— Klägerin / Patentinhaberin / Anmelderin —",
versus: "gegen",
defendantRole: "— Beklagte / Einsprechende / Beschwerdegegnerin —",
others: "Weitere Beteiligte:",
subject: "Betreff",
patent: "Streitpatent:",
proceeding: "Verfahrensart:",
ourSideKey: "{{project.our_side_de}}",
bodyHint: "[Hier folgt der Schriftsatztext. Diese Skelett-Vorlage trägt keine vorgefertigte Struktur — bitte gemäß Schriftsatz-Typ ergänzen.]",
closing: "Schlussformel",
editor: "Bearbeiter:",
dateKey: "{{today.long_de}}",
caseNo: "Aktenzeichen:",
heading: "{{caption.heading_de}}",
representedBy: "vertreten durch",
claimantDesig: "— {{caption.claimant_designation_de}} —",
versus: "{{caption.versus_de}}",
defendantDesig: "— {{caption.defendant_designation_de}} —",
others: "Weitere Beteiligte:",
wegen: "wegen",
subjectKey: "{{caption.subject_de}}",
subjectLabel: "Betreff",
patent: "Streitpatent:",
proceeding: "Verfahrensart:",
ourSideKey: "{{project.our_side_de}}",
bodyHint: "[Hier folgt der Schriftsatztext. Diese Skelett-Vorlage trägt keine vorgefertigte Struktur — bitte gemäß Schriftsatz-Typ ergänzen.]",
closing: "Schlussformel",
}
}
// buildFallbackDocumentXML emits the document body. Layout: firm header line →
// court + case number → basic Rubrum (claimant / vs / defendant / others) →
// subject (patent) → submission body placeholder → closing (date / author /
// firm signature block). Every placeholder occupies its own run so the
// court + case number → basic Rubrum (heading / claimant / vs / defendant /
// others / wegen-subject) → patent details → submission body placeholder →
// closing (date / author / firm signature block). Caption wording comes from
// the shared {{caption.*}} keys. Every placeholder occupies its own run so the
// renderer's pass-1 single-run substitution catches it.
func buildFallbackDocumentXML(lang string) string {
l := fallbackLabelsFor(lang)
@@ -205,21 +216,22 @@ func buildFallbackDocumentXML(lang string) string {
fbPlain(&b, l.caseNo+" {{project.case_number}}")
fbPlain(&b, l.proceeding+" {{project.proceeding.name}}")
// Basic Rubrum.
fbHeading2(&b, l.inTheMatter)
// Basic Rubrum — parametric caption.* wording.
fbHeading2(&b, l.heading)
fbPlain(&b, "{{parties.claimant.name}}")
fbPlain(&b, l.representedBy+" {{parties.claimant.representative}}")
fbBold(&b, l.claimantRole)
fbBold(&b, l.claimantDesig)
fbPlain(&b, "")
fbPlain(&b, l.versus)
fbPlain(&b, "")
fbPlain(&b, "{{parties.defendant.name}}")
fbPlain(&b, l.representedBy+" {{parties.defendant.representative}}")
fbBold(&b, l.defendantRole)
fbBold(&b, l.defendantDesig)
fbPlain(&b, l.others+" {{parties.other.name}}")
fbPlain(&b, l.wegen+" "+l.subjectKey)
// Subject (patent in suit).
fbHeading2(&b, l.subject)
// Patent in suit.
fbHeading2(&b, l.subjectLabel)
fbPlain(&b, l.patent+" {{project.patent_number}}")
fbPlain(&b, "{{project.title}} ("+l.ourSideKey+")")

View File

@@ -31,16 +31,22 @@ func TestBuildFallbackSkeleton_IsMergeSafeAndRendersRubrum(t *testing.T) {
// Render it the way the merge path does and confirm the basic Rubrum
// fills from the bag (claimant + defendant + court + case number).
suffix := "_" + lang
r := NewSubmissionRenderer()
out, err := r.Render(tpl, docforge.PlaceholderMap{
"firm.name": "HLC",
"firm.signature_block": "HLC",
"user.display_name": "Dr. Max Mustermann",
"parties.claimant.name": "Acme Corp.",
"parties.defendant.name": "Globex GmbH",
"project.court": "Landgericht München I",
"project.case_number": "7 O 1234/26",
"project.patent_number": "EP 1 234 567 B1",
"firm.name": "HLC",
"firm.signature_block": "HLC",
"user.display_name": "Dr. Max Mustermann",
"parties.claimant.name": "Acme Corp.",
"parties.defendant.name": "Globex GmbH",
"project.court": "Landgericht München I",
"project.case_number": "7 O 1234/26",
"project.patent_number": "EP 1 234 567 B1",
"caption.heading" + suffix: "FORUM-HEADING",
"caption.claimant_designation" + suffix: "FORUM-CLAIMANT",
"caption.defendant_designation" + suffix: "FORUM-DEFENDANT",
"caption.versus" + suffix: "FORUM-VS",
"caption.subject" + suffix: "FORUM-SUBJECT",
}, docforge.DefaultMissingMarker(lang))
if err != nil {
t.Fatalf("render fallback (%s): %v", lang, err)
@@ -49,6 +55,8 @@ func TestBuildFallbackSkeleton_IsMergeSafeAndRendersRubrum(t *testing.T) {
for _, want := range []string{
"Acme Corp.", "Globex GmbH", "Landgericht München I",
"7 O 1234/26", "EP 1 234 567 B1", "HLC",
// Parametric caption wording fills from the shared caption.* keys.
"FORUM-HEADING", "FORUM-CLAIMANT", "FORUM-DEFENDANT", "FORUM-VS", "FORUM-SUBJECT",
} {
if !strings.Contains(rendered, want) {
t.Errorf("rendered fallback (%s) missing %q\n%s", lang, want, rendered)

View File

@@ -152,12 +152,12 @@ const stylesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
//
// Structure mirrors a real submission:
//
// 1. Firm letterhead + author block (firm.*, user.*, today.*)
// 2. Court caption (project.*, project.proceeding.*)
// 3. Parties block (parties.*)
// 4. Submission title + legal source (rule.*)
// 5. Deadline (deadline.*)
// 6. Boilerplate body + signature
// 1. Firm letterhead + author block (firm.*, user.*, today.*)
// 2. Court caption (project.*, project.proceeding.*)
// 3. Parties block (parties.*)
// 4. Submission title + legal source (rule.*)
// 5. Deadline (deadline.*)
// 6. Boilerplate body + signature
//
// Order matches what a lawyer drafting a real Klageerwiderung would put
// at the top of the document, so when the lawyer customises this
@@ -180,17 +180,23 @@ func buildDocumentXML() string {
plain(&b, "Verfahrensart: {{project.proceeding.name}} ({{project.proceeding.code}})")
plain(&b, "Instanz: {{project.instance_level}}")
heading2(&b, "In der Patentstreitsache")
// Rubrum — parametric caption.* wording (t-paliad-358 A-S2). Heading,
// designations, versus connector and "wegen" subject resolve per forum
// from the variable bag (addCaptionVars), so this caption renders the same
// wording as the Composer seeds and the merge-fallback skeleton.
heading2(&b, "{{caption.heading_de}}")
plain(&b, "{{parties.claimant.name}}")
plain(&b, "vertreten durch {{parties.claimant.representative}}")
bold(&b, "— Klägerin —")
bold(&b, "— {{caption.claimant_designation_de}} —")
plain(&b, "")
plain(&b, "gegen")
plain(&b, "{{caption.versus_de}}")
plain(&b, "")
plain(&b, "{{parties.defendant.name}}")
plain(&b, "vertreten durch {{parties.defendant.representative}}")
bold(&b, "— Beklagte —")
bold(&b, "— {{caption.defendant_designation_de}} —")
plainOptional(&b, "Weitere Beteiligte: {{parties.other.name}}, vertreten durch {{parties.other.representative}}")
plain(&b, "")
plain(&b, "wegen {{caption.subject_de}}")
heading2(&b, "Betreff")
plain(&b, "Streitpatent: {{project.patent_number}} (UPC: {{project.patent_number_upc}})")
@@ -227,8 +233,11 @@ func buildDocumentXML() string {
plain(&b, "{{today.long_de}}")
plain(&b, "")
plain(&b, "{{user.display_name}}")
plain(&b, "{{firm.name}}")
plainOptional(&b, "{{firm.signature_block}}")
// firm.signature_block now carries the firm identity (branding.Name) since
// A-S1, so the standalone {{firm.name}} line that used to sit here was a
// duplicate — dropped (t-paliad-358 A-S2). The firm appears once, via the
// signature block.
plain(&b, "{{firm.signature_block}}")
// English-locale exercise — lets the lawyer verify the EN long-form
// date and EN proceeding name resolve correctly when the user's