parties.role is free text and other is the default arm of a switch — a misspelled role silently drops a party out of the Rubrum
#163
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Raised by patentstyle/head on 2026-07-30 while designing their n-parties form against our bucketing, and measured on our side before answering.
The measurement
paliad.parties.roleis free text with no CHECK constraint. Prod today, seven rows:KlägerindefendantclaimantThree spellings, two languages, one of them inflected — at n=7. That is the shape of the data, not drift over time. patentstyle's observation: at n=70 the recognisable set does not grow, the unrecognisable one does.
Why it matters on filed text
addPartyVars(internal/services/submission_vars.go:806) buckets into claimant / defendant / other, whereotheris thedefault:arm of a switch — everythingpartyRoleBucketdoes not recognise falls into it.So
othercarries two different facts in the same value:A party whose role is misspelled
Beklagteenrenders as an intervenor. A template using{{parties.defendants}}then produces a caption missing a defendant, with nothing said — the party is not dropped, it is silently re-sided.That is the same failure class m/paliad#139 Slice A fixed for deleted parties (
filterPartiesBySelectiondropped unresolvable ids silently, and now reports them). This one is not about a missing row; it is about a present row put on the wrong side.The direction that matches our own precedent
patentstyle's framing, and it is right: we already solved this shape eight days ago, with the arms swapped.
An empty
label_encarrying anot_deliveredrecord is an honest empty; an empty one with no record is a defect. Before schema 4 those were the same bytes, so refusing everything was the only safe reading (mig 211,docs/design-block-editor-rescope-2026-07-29.md§7.1).otheris that problem again: a recognised third-side role and an unrecognised string are the honest and defective cases, and today they are identical.So the direction is not a wider bucket and not necessarily a CHECK constraint:
otherfor a role recognised as a third side;Free text is fine when something downstream says which arm it took.
Establish before building
internal/emaildomainand the formalitiesstatusfield for two different answers to the same question in this repo.UnresolvedSelections(m/paliad#139) and the export guard'sMissingKeys(m/paliad#162) are the two existing shapes. Reuse rather than add a third.Klägerinis recognised today; the question is whether it should be normalised at rest or left and recognised at read time.Cross-repo note
patentstyle has the mirror of this defect:
FieldIndexForKeyin their Variable Manager scans tags key-agnostically, gates against a fixed registry, and returns -1 for anything unknown — an unknown party key is read and silently discarded. Their icarus is instructed to report the unresolvable rather than drop it. Same family, different repo, and worth keeping the two fixes shaped alike.Filed by paliad/head 2026-07-30, from patentstyle/head's reading of our answer. The bucketing itself is unchanged and their design may rely on the third side existing — that part was verified.
Live exposure measured: zero. The fix is preventive, not corrective.
patentstyle/head's point, and it was right: the defect is invisible to every existing check but not to a query, and at n=7 that costs one statement. Whether this has already produced a wrong Rubrum, or is a hazard that has not yet fired, deserve different urgency — and the issue as filed read as the second when it might have been the first.
Run against prod, 2026-07-30, normalising exactly as
partyRoleBucketdoes (internal/services/submission_autoname.go:159— lower, trim, then the two literal sets):klägerinclaimantdefendantNothing falls through to the default arm. All seven rows are recognised, so no party is currently re-sided and no generated Rubrum can be wrong from this cause today.
Recording the zero deliberately: it dates the exposure. Anyone picking this up now knows they are preventing a defect rather than repairing one, and that a re-sided party appearing later is new rather than pre-existing.
The recognised set, for reference — it is small, and it is the whole of what stands between a typo and a wrong caption:
Note what is not in it:
beklagtin,antragsteller,antragsgegner,respondent,appellant,applicant— several of whichinternal/models/models.go:139already names as real UPC party roles in its own comment ("Active: claimant, applicant, appellant / Reactive: defendant, respondent"). So the model documents roles the bucketer does not recognise.One addition to the reasoning, from patentstyle
On why a
CHECKconstraint is the wrong instrument, sharper than the issue body puts it:That is strictly worse than free text plus a reported unresolvable, and it is the argument to keep if anyone proposes an enum later.
Reframing: this is not a normaliser gap, and "add three more strings" would not fix it
patentstyle/head, reading the applicant finding above:
That is the accurate statement of the defect, and it changes the scope.
The two places:
internal/models/models.go:139partyRoleBucket,submission_autoname.go:159The authority is the switch. The knowledge is in the comment. Nothing links them, so they can drift apart silently and have.
Why the obvious fix is wrong. Adding
applicant,appellantandrespondentto the switch corrects today's list and leaves the disagreement exactly where it is — two independent answers to one question, one of them load-bearing, still with no mechanical link. The next role added to the comment, or to the UPC, reintroduces the same defect.So the fix has to make the two agree by construction, not by both being edited. Whatever the vocabulary ends up being, one place must own it and the other must derive from it — the same move as
provenancedFieldsversus mig 206's CHECK, where a doc comment claimed the two lists were pinned together and nothing verified it until a test was written to compare them (seecmd/gen-offices-export/provenanced_fields_pin_test.go).And the typo case does not disappear — it is now the smaller half. Free text plus a reported unresolvable still covers
Beklagteen. What the reframing adds is that a correctly recordedapplicantis today's live defect shape, not a hypothetical future one.One cross-repo fact worth recording here
patentstyle cannot have this defect, and the reason is structural rather than careful: their four shipped keys are
party.proactive.group_name,party.proactive.group_role,party.reactive.group_name,party.reactive.group_role(HLCpatVars.bas:52-55). The side is in the key. Their role is free text and purely editorial — m's own example is "Beklagte 1 bis 4", a phrase a lawyer composes.So on their side the side is authored; on ours it is inferred from a string. If their template ever consumes our party data, the side must come from something authored and never from
partyRoleBucket. Their design already says so at §4.3:Shipped on
mai/jason/issue-163-parties-role.Commit: https://mgit.msbls.de/m/paliad/commit/7997691
Design:
docs/design-party-role-recognition-2026-07-30.mdThe measurement changes the answer
The issue read three spellings at n=7 as the shape of free-text data. It is not. Every one of the seven rows traces to a paliad write path, by timestamp. None was typed by a user into a free-text field.
KlägerindefaultRoleForinfrontend/src/client/submission-draft.ts, shipped 2026-05-26 (t-paliad-287)claimant<select>infrontend/src/projects-detail.tsxdefendant<select>Three things follow:
defaultRoleForis UI-language-dependent. The same button writesclaimantfor an EN user andKlägerinfor a DE user. The UI language picked the stored vocabulary.""for the third side. An empty role is not a third side — it is "nobody said which side this party is on", and it landed in a caption position all the same.<select>offersthirdparty, whichpartyRoleBucketdid not know. Our primary write path and our reader disagreed in the tree.There was a fourth copy:
groupPartiesByRoleinsubmission-draft.tshand-duplicated the Go spelling table. Today they agreed; nothing enforced it.The three questions
Does a CHECK belong? No. A CHECK would have refused paliad's own writes, not a user's typo — it would have turned three UI bugs into three write failures and told nobody what the defect was. On the merits the Rubrum vocabulary is genuinely open, and closing it makes people pick the nearest permitted wrong role deliberately — which is worse than an unrecognised string, because a deliberately wrong role is recognised, gets silently mis-sided, and produces no report at all.
Both precedents agree once you read what each is about.
internal/emaildomaindoes not refuse addresses, it normalises and keeps one source both callers import. The formalitiesstatusfield (migs 210/211) did not close a vocabulary either — it made the refusal recordable. Neither closes the set; both make the unresolved case first-class.Where does it surface?
UnresolvedSelections, a fourth kind — not a third shape.MissingKeysanswers "a placeholder the bag could not fill"; ours is not an unbound key, the key binds to the wrong group.UnresolvedSelectionsalready reaches all three places this needs to land (editor warning before generating,submission.exportedaudit row, draft view JSON).One thing about this kind is different and it is the useful part: the row exists. The other three are ids pointing at something gone, which is exactly why #162 concluded a deleted party cannot be marked at all. Here the party has a name and a role string, so the report says who is on the wrong side and what the field says. A count would be useless — the repair is an edit to one party's role field, and a lawyer cannot find that party from a number.
The seven rows: recognise at read, no normalisation anywhere — not at rest, not on write, despite emaildomain's shape being available.
rolegoes on a court caption.KlägerinandKlägerare not spelling variants of one word in a Rubrum; the form is the party's own designation. Normalising would putKlägeron a filing for a party a lawyer entered asKlägerin, and a migration would do it to four live rows in one step. The fourKlägerinrows are recognised today and stay recognised.What shipped
partyRoleBucketreturns(bucket string, recognised bool). Bucketing is unchanged: the claimant and defendant sets are byte-identical to the pre-#163 switch, and every spelling added to the third-side set already bucketed toothervia the default arm — no party changes side.TestPartyRoleBucket_RecognitionDoesNotMoveAnybodypins that, since your note says patentstyle's design relies on the third side existing.UnresolvedSelections.PartyRoles []UnresolvedPartyRole{ID, Name, Role}, computed over the kept parties only — a party the draft does not select is not in the caption, and reporting it would train the lawyer to ignore the warning.defaultRoleFor's third side pre-fillsStreithelferin/intervenor;thirdpartyis recognised;groupPartiesByRolereadsrole_bucketfrom the server, so the duplicated TS table is gone.TestPartyRoleVocabularyCoversEveryUISuggestionis the recurrence guard and is the test that would have caught the original defect. It reads the role strings out ofprojects-detail.tsxandsubmission-draft.tsand asserts the bucketing recognises each — reading the sources, not a list in the test, because a maintained list is a third copy of the vocabulary and drifts the same way. It fails on an empty extraction: a renamed id must be a red build, not a blind check. That rule earned itself immediately — the first draft's non-greedy regex captured only the first arm of each ternary, so it checkedclaimantandintervenorwhileKlägerinandStreithelferin, the values production actually holds, went unmeasured. Three mutations now confirm it fires, one per UI vocabulary.Limits, stated
other, as before. Proceeding-aware bucketing is a separate question.submissionOpponentNamestill omits silently when no party matches the opposing bucket. An unrecognised role reaches it as a non-match, so the auto-title drops a segment. The editor warning fires on the same draft, so the lawyer is told; the title itself says nothing.claimant) is invisible to every check here. That is a data error, not a vocabulary one.Cross-repo
For patentstyle's
FieldIndexForKey: the point the two fixes should share is that the unresolvable is reported by the layer that failed to resolve it, and the report names the thing rather than counting it. Therole_bucket/role_recognisedpair on the wire is the shape to copy if icarus needs the same signal client-side.Verification
go build ./...,go vet ./internal/..., full Go suite (./internal/... ./pkg/... ./cmd/...) green;scripts/check-gofmt.shrc=0;bun run buildclean; 417 frontend tests pass. No migration in this change.