Merge remote-tracking branch 'origin/main' into mai/fermi/coder-implement-nomen
This commit is contained in:
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
146
internal/services/submission_vars_caption_test.go
Normal file
146
internal/services/submission_vars_caption_test.go
Normal 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"])
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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+")")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user