Submission templates: select court address + team members for letterhead (variable bag extension) #139
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?
Goal (m, 2026-05-26 17:49)
In the submission-draft editor, lawyers should be able to:
competent_court_id, but overridable for letters going to a different court / co-defendant's court / etc.). The chosen court's name + address fields flow into the template via{{court.*}}variables.{{lawyer.1.*}},{{lawyer.2.*}}etc. Same shape as the t-paliad-277 multi-party pattern.Existing data
internal/handlers/courts.goalready carries full address per court (e.g. UPC-CD München: "Cincinnatistraße 64, 81549 München, Deutschland"). All UPC + DE Berufung + NL + Paris + Milan courts have it. No data gap on court side.paliad.userscarriesdisplay_name,email,office(8-office enum),profession(partner/of_counsel/associate/senior_pa/pa/paralegal),job_title(free text),additional_offices[]. No DB gap for the names.internal/offices/offices.gocarries only{key, label_de, label_en}per office. Missing: street address, phone, fax per office. Same shape ascourts.go— needs the 8 office addresses hardcoded.direct_phoneorfaxcolumn onpaliad.users. Optional gap for individualised contact lines.Scope (one issue, one branch — m's pick)
Data
internal/offices/offices.goto addAddress+Phoneper office (hardcoded directory). Head needs the 8 office addresses from m before dispatch — see open question §A below.direct_phone text NULL,fax text NULLtopaliad.users(additive migration, noset_configneeded since user-table mutations don't fire the deadline_rules audit trigger — verify via Supabase MCP audit). User edits via the existing/profilepage (add the two fields). NOT a blocker — letterhead can render an empty phone line until users fill it.Submission-draft editor UI
project.competent_court_id. Reads the courts catalog (already an HTTP endpoint at/api/courts). Selected court_id stored onpaliad.submission_drafts(new nullable columnselected_court_id).selected_lawyers uuid[]onpaliad.submission_drafts.Variable bag (
internal/services/submission_vars.go){{court.name}},{{court.name_en}},{{court.address}},{{court.city}},{{court.country}}populated from the selected court (or empty when none selected).{{lawyers.N.name}},{{lawyers.N.title}}(=job_title||profession_label),{{lawyers.N.email}},{{lawyers.N.office}},{{lawyers.N.office_address}},{{lawyers.N.office_phone}},{{lawyers.N.direct_phone}}for N ∈ [1, len(selected_lawyers)]. Mirror the existing multi-party emit-grouping pattern inemitPartyGroup.{{lawyers.names}}(comma-joined) and{{lawyers.offices}}for free-text letterhead variants — same way{{parties.claimants}}works.submission_vars_*_test.go.Word template hookup
de.inf.lg.erwidg.docx's letterhead block — that's m authoring the template in Word + push to mWorkRepo. Document the variable list in the README so template authors know what's available.Open questions for head before dispatch
hoganlovells.comsite footer. If m doesn't have time today, head can stubAddress: ""for now and lawyers fill it via a follow-up.Hard rules (lessons from today's mig 134 hotfix-3)
mcp__supabase__execute_sqlBEFORE drafting the migration. Specifically: confirmdirect_phone/faxdon't already exist onpaliad.users; confirmselected_court_id/selected_lawyersdon't already exist onpaliad.submission_drafts; confirm no audit trigger on either table that requiresset_config. Today's mig 134 had 3 hotfixes from skipped audits.updated_at = now()writes unless the column exists (it does onpaliad.usersandpaliad.submission_drafts— verify both before any UPDATE).Acceptance
/api/submission_drafts/{id}/renderproduces a .docx with{{court.*}}and{{lawyers.N.*}}substituted.go test ./...+bun run buildclean.Slice A (backend + contract) is on
mai/diesel/issue-139-letterhead-vars, CI run 677 green. Slice B (the editor surface) is a separate slice, approved by head. Not closing — m closes.Commits
af87252— migration 212e6cd7b3— column type fixfe47b12— Slice AThe issue's "Existing data" section is two months stale
Checked before building, because most of the proposed work is already done:
court.*is built and shipping.court.name/address/city/email/chamber, acourtResolver, andsubmission_vars_court_test.go, sourced fromproject.court_id. Item 1 is therefore not "add court variables" — it is only the per-submission override.firm.office.*already resolves the letterhead sender for the acting user (t-368 S5). Item 2 is only the generalisation from one person to N.internal/officesis no longer{key, label_de, label_en}. It is a DB-backed service overpaliad.officescarryingaddress_lines_de/en,phone,fax. Following the issue's recommendation to hardcode the 8 office addresses would today create a second copy of a fact the DB masters and exports to patentstyle. Open question §A is dead.Verified read-only against prod:
submission_draftshad neither column;paliad.usershas nodirect_phone/fax;paliad.officeshas phone/fax/address lines.What Slice A adds
Migration 212 adds
selected_court_id(FK topaliad.courts,ON DELETE SET NULL, mirroringprojects.court_id) andselected_lawyers uuid[]. Both default to today's behaviour, so no existing draft moves — an unset court means the project's court, an empty lawyer list means the acting user alone.selected_parties(mig 131) was the precedent for the array end to end — rawpq.StringArrayplus decoded[]uuid.UUID, the*[]uuid.UUIDthree-state patch contract, the handler field. Mirrored rather than reinvented.Variable shape follows t-277, not a second convention:
{{lawyers}}joined, zero-based{{lawyer.0.name}},{{lawyer.name}}for the first — exactlyparties.claimants/parties.claimant.0.name/parties.claimant.name. m wrote{{lawyer.1.*}}; head is putting the 0-based choice to him while nothing consumes the keys and a rename is still a find/replace.Two smaller calls worth stating:
lawyer.N.titleisjob_titleand nothing else.professionis the approval-ladder tier (senior_pa), not a form of address. It must not reach a filing. The issue proposedjob_title || profession_label; no profession-label table exists and inventing one would put a permissions enum on a court document.firm.office.*(extracted tolookupOffice), so the sender block and the office record cannot disagree about the same office.direct_phone/faxon users are not in this slice, per head. A per-user direct line is a genuinely absent fact rather than a duplicate, but columns nobody fills buy nothing.A live defect this uncovered, reported in its own right
filterPartiesBySelectionhas silently dropped unresolvable ids since mig 131. Delete a party from a project while a draft still lists it and the caption loses a named claimant with nothing said anywhere. The document exports looking finished.Mig 131 chose the prune deliberately — its comment says so, to avoid FK cascades across two tables — so the rendering is unchanged. Only the silence is.
Buildnow returnsUnresolvedSelectionsnaming the court, parties and lawyers it could not resolve.This is the same class as the two inert generator rules in #158: a state that produces no error, no marker, and no visible difference from the correct one.
resolveLawyerstakes a narrowuserLookuprather than the wholeUserService, so the deleted-colleague branch is testable without a database. It is the point of the file; it must not be the one path nothing exercises.Both detections were mutation-checked. Reverting each to its silent form fails exactly its own test and nothing else — 8 tests, no DB required.
One migration bug caught before it shipped
paliad.courts.idistext, notuuid— it is a slug, and so isprojects.court_id. The first draft of migration 212 declaredselected_court_id uuid REFERENCES paliad.courts(id), which would have failed at deploy on the FK type. CI run 674 failed on that commit and 675 passed with only the column type changed. (I read the type from the live schema; I did not read 674's log — the Gitea API returns no job detail for it.)Still open
.dotmletterhead block must actually USElawyer.*— patentstyle's artifact, needs a template edit by m plus a regeneration. Head owns it.Slice B is on
mai/diesel/issue-139-slice-b@cf3e4d7, CI run 682 green. Not closing — m closes.Read this part first: the feature is not working end to end
The
.dotmletterhead block does not consumelawyer.*yet. The pickers work, the variables resolve correctly, and they reach a template that ignores them. Editing that block is patentstyle's artifact and needs m — a template edit plus a regeneration. Until that lands, choosing three colleagues changes what the bag contains and not what the document prints.Same for the still-open
lawyer.0vslawyer.1naming: with m, and still a find/replace while nothing consumes the keys.The warning is the point of this slice
Slice A made
Buildreport every selection that resolved to nothing — a retired court, a party removed from the project, a deleted colleague. Nothing rendered it. A report that reaches no user is worse than no report, because the next person reads the case as covered while a lawyer still exports a document with a named person silently missing from it.So:
unresolved— the ids and a rendered sentence, both on purpose. The ids let the editor offer a repair; the sentence is what it shows when it cannot name what is missing, because a deleted user has no name left to print.role="alert", before the document is generated — not in the sidebar where it competes with the variable list.unresolvedJSONis its own function so that link is testable without a database. It was the untested link, and it was the one that did not exist.hasUnresolvedkeys on the three id lists and never on the message string — keying on the string would make an empty-but-present message read as all clear. There is a test for exactly that.Order is meaningful, and the picker keeps it
The backend prints
lawyer.0,lawyer.1, … in stored order, so the selection is a list from checkbox to PATCH, never a Set. A Set-based picker returns whatever order the DOM gives and would silently reshuffle a letterhead.Order is selection order, shown as a position number, and changeable with up/down buttons. Without those, reordering means unchecking everyone and re-picking in sequence — data entry rather than a choice.
Smaller calls
job_title, and the only other title on that row isprofession, the approval-ladder tier. Slice A ruled that out of the letterhead; previewing it here would show a title the document will never print.Verification
nilfromunresolvedJSONfails exactly its two Go tests; keyinghasUnresolvedon the message fails exactly its one TS test. Neither touched anything else.bun run buildclean, gofmt clean.Re-measured before building — two of three asks are already shipped
Branch
mai/knuth/issue-139-letterhead-vars, commit08c58df. Writeup:docs/findings-issue-139-remeasured-2026-07-30.md. Nothing built.{{court.*}}{{lawyer.N.*}}Items 1 and 2 are Slice A (
af87252,e6cd7b3,fe47b12) and Slice B(
cf3e4d7), both merged; mig 212 is applied in prod. The "Existing data"section above is dead — all four bullets are wrong now, and open questions
§A and §B are both closed. Anyone picking this up should not work from it.
Item 3 is not where this issue says it is
The issue, both slice comments and the dispatch all place the remainder in the
Word template — "patentstyle's artifact, needs a template edit by m plus a
regeneration". Measured, that is not the missing step:
HLC-Patents-Style.dotmcarries zero{{…}}placeholders. I scanned everyXML part. It is the macro-only letterhead.
gen-skeleton-submission-templateemits none either in its default mode.anchorsOnly = trueis the default and writes ten{{#section:KEY}}pairs andnothing else; the placeholder-carrying body needs
-anchors=falseand servespre-Composer drafts.
The letterhead text a draft gets comes from
paliad.submission_bases.section_spec, thehlc-letterheadrow:So the recipient half is already wired — the caption seed carries the court
and its address, and Slice A made which court that is overridable per submission.
The sender half is not:
{{user.display_name}}is whoever has the draftopen.
lawyer.*appears in no seed. Picking three colleagues changes thevariable bag and nothing the document prints.
That is the whole remaining gap: one field of DB-mastered content, not a Word
template and not a vendored artifact.
The question that has to be answered first
lawyer.0vslawyer.1is still open with m, and it is about to stop beingfree. The head's reason for deferring it was "nothing consumes the keys and a
rename is still a find/replace". That holds only while no seed names the key: a
seed is copied into
submission_sectionsat draft creation, so afterwards thestring lives in per-draft rows a lawyer may have edited, and the rename becomes a
data migration over user-edited text.
Measured now: 0 of 33 drafts have
selected_lawyersset, 0 haveselected_court_id. Nobody has used either picker yet. The rename is stillfree today, and it is free precisely because the seed does not reference the keys.
The split falls on exactly that line:
{{lawyers}}(chosen names, comma-joined) plus
{{lawyer.office_address}}/{{lawyer.office_phone}}for the first-named. No index reaches stored content.lines, which needs
{{lawyer.0.name}}or{{lawyer.1.name}}in the seed.A firm letterhead is normally the second shape, which is why this is worth
settling rather than shipping the first and redoing it.
Two things I checked rather than assumed, both favourable
resolveLawyersreturns theacting user when
selected_lawyersis empty(
submission_vars_letterhead.go:171), so a seed naming{{lawyer.name}}isbehaviour-preserving for all 33 existing drafts by construction. No separate
fallback work needed.
edited, none empty. A backfill discards nothing today. It stops being free the
moment somebody edits one, which argues for doing it in the same migration as
the seed change rather than later.
Together those make the remaining work one migration, not a slice.
One separate thing found on the way
gen-skeleton-submission-template's doc comment promises a file "exercisingevery placeholder SubmissionVarsService resolves". Its legacy body covers 38 of
78 catalogue keys — missing all of
court.*,lawyer.*,caption.*,deadline.*,procedural_event.*,firm.office.*. Part is deliberate(
deadline.*, t-paliad-287, and the comment says so) and part is drift; thepackage has no test file, so the claim is unchecked either way. Low stakes,
since the default path emits no placeholders at all — but the comment should
either stop promising completeness or be held to it.
Not closing — m closes.