Compare commits

...

2 Commits

Author SHA1 Message Date
mAi
2ba0ba15e6 feat(submissions): t-paliad-215 Slice 2 — patent_number_upc helper
UPC briefs parenthesise the patent kind code ("EP 1 234 567 (B1)")
where the DE convention runs it inline ("EP 1 234 567 B1"). Slice 2
adds the {{project.patent_number_upc}} placeholder for the new UPC
templates (Q-S2-4 locked at 'all yes' on 2026-05-20).

Pure function alongside legalSourcePretty. Trailing single-letter +
single-digit kind code regex; everything else preserved. Pass-through
on unrecognised shapes — the lawyer's draft never sees a number worse
than the source value.

Wired into addProjectVars so every render exposes both forms
({{project.patent_number}} and {{project.patent_number_upc}}). UPC
templates pull the parenthesised form; DE templates ignore it.

8 test cases (more than the 6 in the brief) covering:
- EP B1 / EP A1 — common case
- DE national with kind code
- No kind code → pass-through
- Whitespace trimming
- Empty input
- WO publication number (no kind-code shape) → pass-through
- Two-digit kind code (B12) → pass-through (intentional — real EP
  kind codes are single-letter + single-digit)

No schema change, no migration, no var-bag namespace additions
beyond the one new placeholder.
2026-05-20 12:58:13 +02:00
mAi
0817c04609 docs(submissions): t-paliad-215 Slice 2 brief — 3-5 next templates
Appended §19–§28 to docs/design-submission-generator-2026-05-19.md
covering the Slice 2 scope per the head's 2026-05-20 brief. NO code
changes; doc only. NO AskUserQuestion fired — head batches the six
m-decision items (Q-S2-1..Q-S2-6) across four inventors today.

Inventor picks (defaults):
- Q-S2-1: 5 templates (3 DE-LG: klage/replik/anzeige, 2 UPC-CFI:
  soc/sod) + 2 family-tier skeletons (de.inf.lg.docx,
  upc.inf.cfi.docx).
- Q-S2-2: render {{upc.language_of_proceedings}} as missing marker;
  defer paliad.projects.upc_language column to Slice 3.
- Q-S2-3: ship both family-tier skeletons — exercises the fallback
  chain's third tier (wired in Slice 1, never tested).
- Q-S2-4: ship a {{project.patent_number_upc}} pretty-print helper
  (~40 LoC + 6 tests). Pure function, no schema change.
- Q-S2-5: I author bootstrap .docx via python-docx (Slice 1 pattern),
  HLC legal team replaces with polished versions in a follow-up.
- Q-S2-6: defer EPO + DPMA to Slice 3 (different party model needs
  separate var namespace).

Net code change if picks land as recommended: ~40 LoC + 6 tests, no
migration, no new bag entries. The work is template authoring.

Live-verified premises (2026-05-20):
- Slice 1 merged at defa516; Schriftsätze tab live on paliad.de.
- Corpus = 100 published filing rules across 20 active proceedings;
  top by count: upc.inf.cfi (15), upc.rev.cfi (14), de.inf.lg (7).
- legalSourcePretty covers every UPC.RoP.* citation in the corpus —
  no code change needed for Tier-B templates.
- {family}.docx fallback tier is wired in Slice 1 but unexercised;
  Slice 2 is the moment to test it.

Inventor parks; awaiting head's batched picks before any coder shift.
2026-05-20 12:54:36 +02:00
3 changed files with 448 additions and 0 deletions

View File

@@ -782,3 +782,365 @@ go/no-go before any coder is hired. After m approves:
- Phase 11 (AI-drafted body) is a SEPARATE task — not auto-spawned.
Inventor parks here.
---
# Slice 2 brief — 3-5 next templates
**Status:** brief (no AskUserQuestion per head's instruction
2026-05-20; decisions batched with three other inventors).
**Author:** copernicus (inventor → continued)
**Branch:** `mai/copernicus/submission-slice-1` (appended on top of
the merged Slice 1).
**Slice 1 merge:** `defa516` on 2026-05-19, live on paliad.de.
## §19 What this brief is
Slice 1 shipped the engine + template registry + one template
(`de.inf.lg.erwidg` Klageerwiderung) end-to-end on the project detail
page. Slice 2 expands the template roster to 35 more submissions
without touching the engine — the question is *which* and *what
new variables, if any*. This brief surfaces the inventor's ranking
and a short list of decisions m needs to weigh in on; no
AskUserQuestion is fired (head's batch path).
## §20 Premises verified live (2026-05-20)
| Claim | Verification |
|---|---|
| Slice 1 merged at `defa516` | `git log origin/main` shows `defa516`-ancestor commits + Schriftsätze tab live on paliad.de. |
| Engine, registry, vars bag from Slice 1 unchanged | No commits to `internal/services/submission_*.go` since `ad7b705` (Slice 1 frontend). |
| Submission corpus is **100 published filing rules** across **20 active proceedings** | `SELECT COUNT(*) FROM paliad.deadline_rules WHERE event_type='filing' AND lifecycle_state='published' AND submission_code IS NOT NULL` = 100. Top by filing count: `upc.inf.cfi` (15), `upc.rev.cfi` (14), `de.inf.lg` (7), `de.null.bpatg` (7), `epa.opp.opd` (7), `de.inf.bgh` (6), `epa.opp.boa` (6). |
| Klageerwiderung template still resolves on the live `templates/_base/de.inf.lg.erwidg.docx` path | Live registry `HasTemplate("de.inf.lg.erwidg")` returns true; SHA `7f97b7f9` from Slice 1's upload. |
| `{family}.docx` fallback tier is wired but unexercised | `services/submission_templates.go::candidates()` emits 4 candidates including the family tier; no `_base/{family}.docx` file exists in mWorkRepo yet. Slice 2 is the moment to author one. |
| `legalSourcePretty` already covers `UPC.RoP.*` | Slice 1 test table includes `UPC.RoP.23.1` → "Regel 23.1 VerfO UPC" / "Rule 23.1 RoP UPC". Sampling the corpus: every UPC.RoP.* citation is two- or three-segment, all hit the existing patterns. No code change needed for Tier-B templates. |
## §21 Candidate templates
Ranked by (HLC usage frequency × engine-stress diversity × var-bag
overlap with Slice 1). The 100-row corpus and the proceeding-frequency
table both inform this; the cluster of 7+ DE-LG, UPC-CFI, BPatG, EPO
submissions hints at the practice's gravity centre.
### §21.1 Tier A — drop-in (share Slice 1's variable bag)
These map onto the existing ~30-placeholder bag with **zero** new
variables. Authoring is the only work; engine + registry are
sufficient.
| # | submission_code | Name (DE / EN) | Party | Legal source | Notes |
|---|---|---|---|---|---|
| 1 | `de.inf.lg.klage` | Klageerhebung / Filing of Action | claimant | DE.ZPO.253 | The *other side* of Klageerwiderung. Symmetric counterpart; HLC represents claimants in a non-trivial fraction of patent disputes. Same vars as the shipped template. |
| 2 | `de.inf.lg.replik` | Replik / Reply | claimant | (none) | Claimant's response to the Klageerwiderung. Rounds out the LG pleading exchange. Same vars. |
| 3 | `de.inf.lg.anzeige` | Anzeige der Verteidigungsbereitschaft / Notice of Intent to Defend | defendant | DE.ZPO.276.1 | Short, frequent, mechanical. Tests the engine on the lighter end (one-paragraph notice). Same vars. |
### §21.2 Tier B — UPC-specific (stress the var bag)
UPC pleadings are HLC's growth surface and the place where the
variable bag needs to flex. Tier B templates expose:
- UPC-specific party labels (parties.{claimant,defendant} still work
— UPC uses Claimant/Defendant in English) but with local-division
context that DE doesn't have.
- UPC.RoP.* citation density: every UPC filing cites at least one
Rule, often three.
- A different patent-number convention (UPC briefs typically
parenthesise the kind code: "EP 1 234 567 (B1)" vs the DE form
"EP 1 234 567 B1").
| # | submission_code | Name (DE / EN) | Party | Legal source | Notes |
|---|---|---|---|---|---|
| 4 | `upc.inf.cfi.soc` | Klageerhebung / Statement of Claim | claimant | (none in rule; UPC.RoP.13 by convention) | Highest-frequency UPC submission. Exposes `{{upc.local_division}}` (or render `{{project.court}}` directly), `{{upc.language_of_proceedings}}`, UPC patent-number format. |
| 5 | `upc.inf.cfi.sod` | Klageerwiderung / Statement of Defence | defendant | UPC.RoP.23.1 | Symmetric SoD; shares the UPC-specific vars from #4. Bundling SoC + SoD lets one slice cover both perspectives without doubling authoring. |
Optional Tier-B extension if scope allows:
- `upc.inf.cfi.ccr` (Nichtigkeitswiderklage / Counterclaim for
Revocation) — UPC's distinctive bifurcation-free feature. Deeper
substantive document; deferred to Slice 3 candidate.
### §21.3 What we DON'T ship in Slice 2
- **EPO opposition (`epa.opp.opd.*`):** very different party model
(opponent + patentee + intervener) and the proceeding-type code
doesn't carry a single `parties.claimant`/`parties.defendant`
mapping cleanly. Needs a new namespace (e.g.
`parties.opponent`/`parties.patentee`). Slice 3 candidate.
- **DPMA appeals (`dpma.appeal.bpatg.*`, `dpma.appeal.bgh.*`):**
appellate posture differs again; defer with EPO.
- **UPC revocation (`upc.rev.cfi.*`):** structurally close to UPC
infringement but with reversed our_side semantics — claimant
attacks the patent. Mostly works with current vars, but the
template body needs different prose. Slice 3 candidate.
- **Appellate DE (`de.inf.olg.*`, `de.null.bgh.*`):** appeal-tier
templates need `{{prior_instance.judgment_date}}` and
`{{prior_instance.case_number}}` references — new var namespace.
Slice 4 candidate when the project-tree appeal model lands.
## §22 Variable bag delta
Slice 1's bag (~30 placeholders, see §6.2 of the original brief)
covers Tier A drop-in templates **with no additions to
`services/submission_vars.go`**. The work is entirely in the .docx
files.
Tier B (`upc.inf.cfi.*`) wants 3-4 UPC-specific placeholders.
Inventor recommends shipping the minimum:
| Placeholder | Source | Slice 2 plan |
|---|---|---|
| `{{upc.local_division}}` | derive from `project.court` (strip "Local Division" suffix; for v1, render `project.court` directly) | **No new var.** Templates use the existing `{{project.court}}` placeholder. Pretty-print helper deferred. |
| `{{upc.language_of_proceedings}}` | not on `paliad.projects` today | **Decision Q-S2-2**: ship missing marker, or add `paliad.projects.upc_language` (mig 107)? Inventor leans missing-marker — adding a column for one cluster of templates is premature; promote if Slice 3 demands it. |
| `{{project.patent_number_upc}}` | reformat `project.patent_number` ("EP 1 234 567 B1" → "EP 1 234 567 (B1)") | **Decision Q-S2-4**: ~10 LoC pretty-print helper alongside `legalSourcePretty` (no schema change). Inventor leans **ship the helper** — small, isolated, unit-testable. |
| Multi-claimant / multi-defendant blocks | `paliad.parties` has multiple rows per role | **No change.** Slice 1 §13.6 noted multi-party as Phase 2 work via `lukasjarosch/go-docx`'s loop syntax. Slice 2 stays single-party; templates that want multi-party use static numbered placeholders (`{{parties.claimant.1.name}}`, etc., if m wants to ship this). Deferred. |
Net code change for Slice 2: **0 LoC if Q-S2-2 = missing-marker AND
Q-S2-4 = no helper. ~40 LoC + 6 tests if Q-S2-4 = helper.**
The variable contract surface stays additive — every Slice 2
placeholder either reuses Slice 1's bag or renders the missing
marker on unbound keys, so lawyers see explicit gaps in Word rather
than silent omissions.
## §23 Generic vs jurisdiction-specific split
The fallback chain (§5 of the original brief) has four tiers:
```
templates/{FIRM}/{code}.docx firm-specific override
templates/_base/{code}.docx per-code baseline
templates/_base/{family}.docx proceeding-family fallback
templates/_base/_skeleton.docx ultra-generic
```
Slice 1 exercised only the per-code tier. Slice 2 is the natural
moment to exercise the family tier with two concrete files:
- `templates/_base/de.inf.lg.docx` — DE Landgericht filing skeleton
(firm letterhead + court address block + party block + signature
stub). Every `de.inf.lg.*` submission_code that lacks a per-code
override falls back to this. Authors who want all-DE-LG headers
to match the firm's house style only edit this file.
- `templates/_base/upc.inf.cfi.docx` — UPC CFI infringement family
skeleton. Same shape, but with the UPC local-division block
instead of the DE court block.
**Recommendation: ship both family files in Slice 2.** This:
1. Lights up the third tier of the fallback chain that's been wired
since Slice 1 but never tested in production.
2. Gives every DE-LG and UPC-CFI submission_code (including the
ones not authored per-code yet) a usable [Generieren] button on
the project page — currently they all show "Keine Vorlage".
3. Provides the natural authoring surface for HLC's house-style
review: edit *one* family file to set firm letterhead + footer
for an entire jurisdiction, instead of N per-code files.
**Do NOT generalise across jurisdictions.** A
`templates/_base/_skeleton.docx` exists in the chain but UPC ≠ DE
≠ EPO at the formatting level; the skeleton stays as the
last-resort fallback for codes outside the three jurisdictions we
serve. Trying to make one file cover all jurisdictions guarantees
none of them look right.
## §24 Slice 2 deliverable shape
Assuming the inventor picks (§25) hold:
### §24.1 Template files (7 .docx in `HL/mWorkRepo/templates/_base/`)
| Path | Tier | Source |
|---|---|---|
| `de.inf.lg.docx` | family | NEW (Slice 2 ships) |
| `de.inf.lg.klage.docx` | per-code | NEW |
| `de.inf.lg.replik.docx` | per-code | NEW |
| `de.inf.lg.anzeige.docx` | per-code | NEW |
| `de.inf.lg.erwidg.docx` | per-code | Slice 1 (no change) |
| `upc.inf.cfi.docx` | family | NEW |
| `upc.inf.cfi.soc.docx` | per-code | NEW |
| `upc.inf.cfi.sod.docx` | per-code | NEW |
Total: 7 new .docx files, ~30-50 KB each, authored via python-docx
on the same machine that authored Slice 1's bootstrap (per Slice 1
§14 follow-up). HLC legal team's polished versions are a follow-up
task — Slice 2 ships the engine coverage, not the substantive prose
each firm uses.
### §24.2 Code
- **Zero changes** if Q-S2-2 + Q-S2-4 both land as inventor-recommended
defaults (missing-marker, no helper).
- **~40 LoC + tests** for Q-S2-4 = ship the patent-number-pretty helper:
one function in `services/submission_vars.go`, six unit tests
covering the EP/UP variants. Trivial isolated change.
- **~80 LoC + mig 107** for Q-S2-2 = add `paliad.projects.upc_language`:
migration + `Project` struct field + form picker + bag entry. Heavier;
inventor leans against unless m wants the field everywhere now.
### §24.3 Tests
Unit-test coverage of any code added (above). Manual smoke after
each template upload: open a sample DE-LG project + a UPC-CFI
project, walk through the Schriftsätze tab, verify the new buttons
appear and download a rendered .docx with all placeholders
resolved or surfacing the missing marker.
### §24.4 Migration
**None** under the inventor-recommended picks. Slice 2 is a pure
template-authoring + (optional) helper slice.
## §25 Open decisions for m (head's batch)
Six items. Each carries an inventor pick + 2-3 alternatives so the
head can run them by m as a single round. No AskUserQuestion per
the head's instruction.
### Q-S2-1. Template roster
**Pick:** ship the 5 templates in §21.1 + §21.2 (de.inf.lg.klage,
de.inf.lg.replik, de.inf.lg.anzeige, upc.inf.cfi.soc,
upc.inf.cfi.sod) + 2 family-tier skeletons (de.inf.lg.docx,
upc.inf.cfi.docx). 7 .docx files.
Why this pick: hits both jurisdictions HLC sees most (DE + UPC),
covers both party perspectives (claimant + defendant on each),
exercises the family-tier fallback that's been wired since Slice 1
but never tested, scope stays one-day-of-authoring.
- **Alt A — DE-only (4-5 DE templates, no UPC yet).** Safer scope.
But leaves the UPC growth surface uncovered and doesn't stress
the var bag.
- **Alt B — UPC-only (SoC + SoD + Reply + Rejoinder).** Higher
strategic value; same authoring effort as the picked tier.
Leaves DE practice with only Klageerwiderung. Reasonable if m
reads paliad's near-term growth as UPC-first.
- **Alt C — 5 DE + 5 UPC + family files (10+ docs).** Doubles the
authoring scope; probably too much for one slice.
### Q-S2-2. UPC language-of-proceedings field
**Pick:** render `{{upc.language_of_proceedings}}` as missing
marker in Slice 2. Add `paliad.projects.upc_language` (mig 107) only
if Slice 3's UPC additions demand structured storage.
Why: adding a column for one cluster of templates is premature;
the missing marker gives the lawyer an explicit gap to fill in
Word for now, and we promote to a real field once we see real
HLC UPC usage.
- **Alt:** add mig 107 + column + form picker + bag entry now.
Pros: lawyers don't see the missing marker. Cons: 80+ LoC and
a migration for one var; if m doesn't end up using the UPC
templates we've added unused infrastructure.
### Q-S2-3. Family-tier skeletons
**Pick:** ship `templates/_base/de.inf.lg.docx` AND
`templates/_base/upc.inf.cfi.docx` in Slice 2.
Why: lights up the fallback chain's third tier; gives every DE-LG
and UPC-CFI submission_code a usable [Generieren] button even
before HLC reviews per-code templates; concentrates house-style
maintenance to one file per jurisdiction.
- **Alt:** per-code only; leave the family tier empty until a
future slice. Pros: simpler authoring (5 files instead of 7).
Cons: rules without per-code templates show "Keine Vorlage" for
longer; the fallback chain's third tier stays untested in
production.
### Q-S2-4. Patent-number pretty-printer
**Pick:** ship a `{{project.patent_number_upc}}` helper (~40 LoC +
6 tests). Reformats `project.patent_number` from "EP 1 234 567 B1"
→ "EP 1 234 567 (B1)" for UPC briefs. Pure function, no schema
change, mirrors `legalSourcePretty`'s shape.
Why: lawyers should not have to hand-tweak the patent number on
every UPC filing; the convention is mechanical.
- **Alt:** skip the helper; lawyers fix in Word. Pros: 0 LoC.
Cons: tiny but recurring friction for every UPC submission.
### Q-S2-5. Template authoring path
**Pick:** I author the bootstrap .docx files via python-docx (same
pattern as Slice 1) and commit as mAi via the Gitea API. HLC
legal team replaces with polished versions in a follow-up; Slice 2
ships the engine coverage.
Why: matches Slice 1's pattern; doesn't block engine validation on
legal-team availability; the documentation tooling already
supports the "minimal demo, polished later" framing.
- **Alt:** wait for HLC to author the real ones before merging
Slice 2. Pros: lawyers don't see bootstrap-quality templates in
production. Cons: blocks the slice on legal-team scheduling; the
Schriftsätze tab stays at "1 template" for an unknown duration.
### Q-S2-6. EPO/DPMA timing
**Pick:** defer EPO + DPMA templates to Slice 3 (separate variable
namespace, separate authoring effort).
Why: EPO opposition has a different party model
(opponent/patentee/intervener) that needs a new var namespace;
adding it inside Slice 2 mixes scopes and slows the slice. Slice 3
can be EPO-focused with its own coherent shape.
- **Alt:** include 1-2 EPO templates in Slice 2 to validate the
cross-jurisdiction shape early. Pros: catches design gaps now.
Cons: adds scope; EPO's distinctive party model probably needs
schema work that Slice 2 otherwise avoids.
## §26 Slice 2 hand-off
If m approves the inventor picks roughly as-is:
1. Coder shift (any pattern-fluent Sonnet, NOT cronus) authors the
7 .docx files via python-docx, uploads to HL/mWorkRepo via
Gitea API as mAi (same path as Slice 1's bootstrap).
2. Coder ships the patent-number pretty-print helper if Q-S2-4
lands as picked.
3. Manual smoke: open a DE-LG project and a UPC-CFI project on
paliad.de, walk through the Schriftsätze tab, verify all 6
per-code buttons + the 2 family-tier fallbacks render correctly.
4. PR opens; maria's gate merges to main; Dokploy auto-deploys;
registry SHA-cache picks up the new templates on first request.
No PR opens until m's batch picks land. Slice 2 stays parked here.
## §27 Trade-offs flagged
1. **Bootstrap fidelity (recurring from Slice 1).** Six more
python-docx-authored templates compound the "engine smoke, real
doc later" gap. Recommend: add a one-line caveat in the doc-
detail UI ("Bootstrap-Vorlage — überprüfen vor Einreichung") or
in the Verlauf event title for generated submissions. Small
frontend change; can ride along in Slice 2 or land as a
follow-up.
2. **Family-tier precedence ambiguity.** When both
`templates/_base/de.inf.lg.klage.docx` and
`templates/_base/de.inf.lg.docx` exist, the registry resolves
per-code first (correct per §5 of the original brief). Authors
editing only the family file may not see their changes reflect
on per-code surfaces. Document the precedence in
`templates/README.md` (which still doesn't exist — Slice 1
§14 follow-up). Slice 2 is a natural moment to add it.
3. **Slice 2 leaves a long tail.** Even after Slice 2 ships, paliad
has roughly 6 of 100 published filing codes covered. The
pattern is fine — high-frequency codes first — but the long tail
needs an authoring strategy beyond "the next inventor picks 5
more". A meta-question for after Slice 2: do we want a
self-service `/admin/submission-templates` upload UI sooner
(Slice 3 candidate per the original §12) or keep the
Gitea-authoring path for several more slices?
## §28 Inventor parks
Brief committed; no AskUserQuestion fired; no code touched. Head
batches the Q-S2-1..Q-S2-6 picks with the other three inventors'
items and surfaces them to m.

View File

@@ -352,3 +352,38 @@ func TestTemplateRegistry_Tiers(t *testing.T) {
}
}
}
// TestPatentNumberUPC covers the kind-code parenthesisation that UPC
// briefs use (t-paliad-215 Slice 2, design §22 Q-S2-4).
func TestPatentNumberUPC(t *testing.T) {
tests := []struct {
in, want string
}{
// EP variants — the common case.
{"EP 1 234 567 B1", "EP 1 234 567 (B1)"},
{"EP 4 056 049 A1", "EP 4 056 049 (A1)"},
// DE national number with kind code.
{"DE 10 2020 123 456 A1", "DE 10 2020 123 456 (A1)"},
// No kind code → pass-through unchanged.
{"EP 1 234 567", "EP 1 234 567"},
// Leading + trailing whitespace trimmed.
{" EP 1 234 567 B1 ", "EP 1 234 567 (B1)"},
// Empty input.
{"", ""},
// Slash-separated forms (WO publication numbers) don't match
// the kind-code shape → pass through.
{"WO/2023/123456", "WO/2023/123456"},
// Two-digit kind code (e.g. B12) doesn't match the single-digit
// pattern; pass through. This is intentional — real EP kind
// codes are single-letter + single-digit.
{"EP 1 234 567 B12", "EP 1 234 567 B12"},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
got := patentNumberUPC(tc.in)
if got != tc.want {
t.Errorf("patentNumberUPC(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}

View File

@@ -27,6 +27,7 @@ import (
"database/sql"
"errors"
"fmt"
"regexp"
"strings"
"time"
@@ -264,6 +265,12 @@ func addProjectVars(bag PlaceholderMap, p *models.Project, pt *models.Proceeding
bag["project.case_number"] = derefString(p.CaseNumber)
bag["project.court"] = derefString(p.Court)
bag["project.patent_number"] = derefString(p.PatentNumber)
// project.patent_number_upc is the UPC-brief convention — kind code
// parenthesised ("EP 1 234 567 (B1)") instead of the DE form
// ("EP 1 234 567 B1"). Pure-function rewrite; pass-through when no
// kind code is present so the lawyer's draft never sees a worse
// number than the source value.
bag["project.patent_number_upc"] = patentNumberUPC(derefString(p.PatentNumber))
bag["project.filing_date"] = formatDatePtr(p.FilingDate, "2006-01-02")
bag["project.grant_date"] = formatDatePtr(p.GrantDate, "2006-01-02")
bag["project.our_side"] = derefString(p.OurSide)
@@ -482,3 +489,47 @@ func legalSourcePretty(src, lang string) string {
}
return src
}
// patentNumberKindCodeRegex matches a trailing kind code on a patent
// number: a whitespace-separated single uppercase letter followed by
// a single digit (B1, A1, A2, B2, B9, C1, T2, U1, …). Capturing
// groups split the base from the kind code so the formatter can
// parenthesise the kind without touching the rest of the number.
var patentNumberKindCodeRegex = regexp.MustCompile(`^(.*?)\s+([A-Z]\d)$`)
// patentNumberUPC reformats a patent number from the DE convention
// ("EP 1 234 567 B1") to the UPC-brief convention
// ("EP 1 234 567 (B1)"). The kind code is parenthesised; everything
// else is preserved verbatim. Numbers without a recognised trailing
// kind code pass through unchanged so a lawyer's draft never sees a
// number worse than the source value.
//
// Recognised inputs:
//
// "EP 1 234 567 B1" → "EP 1 234 567 (B1)"
// "EP 4 056 049 A1" → "EP 4 056 049 (A1)"
// "DE 10 2020 123 456 A1" → "DE 10 2020 123 456 (A1)"
// " EP 1 234 567 B1 " → "EP 1 234 567 (B1)" (trimmed)
//
// Pass-through:
//
// "EP 1 234 567" → "EP 1 234 567"
// "WO/2023/123456" → "WO/2023/123456" (no kind code shape)
// "" → ""
//
// Pure function; unit-tested in submission_vars_test.go.
func patentNumberUPC(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if m := patentNumberKindCodeRegex.FindStringSubmatch(s); m != nil {
base := strings.TrimSpace(m[1])
kind := m[2]
if base == "" {
return s
}
return base + " (" + kind + ")"
}
return s
}