Submission templates: select court address + team members for letterhead (variable bag extension) #139

Open
opened 2026-05-26 15:59:14 +00:00 by mAi · 3 comments
Collaborator

Goal (m, 2026-05-26 17:49)

In the submission-draft editor, lawyers should be able to:

  1. Select a court — pick which court's address goes on the document (defaults to the project's 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.
  2. Select team members for the letterhead — multi-select of users involved on this submission. Each picked member contributes their name, title, office address, phone, email to the variable bag as {{lawyer.1.*}}, {{lawyer.2.*}} etc. Same shape as the t-paliad-277 multi-party pattern.
  3. Use these in the Word template's letterhead block so the rendered .docx carries the right recipient + the right sender block per submission.

Existing data

  • Courtsinternal/handlers/courts.go already 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.
  • Userspaliad.users carries display_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.
  • Office addressinternal/offices/offices.go carries only {key, label_de, label_en} per office. Missing: street address, phone, fax per office. Same shape as courts.go — needs the 8 office addresses hardcoded.
  • Per-user phone — no direct_phone or fax column on paliad.users. Optional gap for individualised contact lines.

Scope (one issue, one branch — m's pick)

Data

  1. Extend internal/offices/offices.go to add Address + Phone per office (hardcoded directory). Head needs the 8 office addresses from m before dispatch — see open question §A below.
  2. Add direct_phone text NULL, fax text NULL to paliad.users (additive migration, no set_config needed since user-table mutations don't fire the deadline_rules audit trigger — verify via Supabase MCP audit). User edits via the existing /profile page (add the two fields). NOT a blocker — letterhead can render an empty phone line until users fill it.

Submission-draft editor UI

  1. Court picker: dropdown in the submission-draft editor, pre-populated from project.competent_court_id. Reads the courts catalog (already an HTTP endpoint at /api/courts). Selected court_id stored on paliad.submission_drafts (new nullable column selected_court_id).
  2. Lawyer multi-select: multi-select of team members for this project. The project_team_members list filters the user table. Selected user_ids stored as selected_lawyers uuid[] on paliad.submission_drafts.
  3. Both selectors live in a new "Briefkopf / Letterhead" collapsible section above the variable form. UI matches the existing perspective+date strip pattern.

Variable bag (internal/services/submission_vars.go)

  1. Add {{court.name}}, {{court.name_en}}, {{court.address}}, {{court.city}}, {{court.country}} populated from the selected court (or empty when none selected).
  2. Add {{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 in emitPartyGroup.
  3. Also expose joined-list aliases like {{lawyers.names}} (comma-joined) and {{lawyers.offices}} for free-text letterhead variants — same way {{parties.claimants}} works.
  4. Cover with unit tests in submission_vars_*_test.go.

Word template hookup

  1. Not in scope of this slice: editorial work to actually USE the new variables in the existing 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

  • §A — m needs to supply the 8 office addresses (or paste them somewhere head can find them). Defaults: HLC's published office addresses, probably the same as hoganlovells.com site footer. If m doesn't have time today, head can stub Address: "" for now and lawyers fill it via a follow-up.
  • §B — direct_phone and fax: ship as optional columns now or skip entirely until someone asks for per-user phone lines? Recommended: ship now (cheap, harmless, additive).

Hard rules (lessons from today's mig 134 hotfix-3)

  • MUST audit live schema via mcp__supabase__execute_sql BEFORE drafting the migration. Specifically: confirm direct_phone/fax don't already exist on paliad.users; confirm selected_court_id/selected_lawyers don't already exist on paliad.submission_drafts; confirm no audit trigger on either table that requires set_config. Today's mig 134 had 3 hotfixes from skipped audits.
  • English identifiers in pkg/litigationplanner-adjacent code; pure logic where possible; tests follow code.
  • No updated_at = now() writes unless the column exists (it does on paliad.users and paliad.submission_drafts — verify both before any UPDATE).

Acceptance

  1. Submission-draft editor shows the Briefkopf section with court dropdown + lawyer multi-select.
  2. Switching court / picking lawyers updates the variable preview live.
  3. Word render via /api/submission_drafts/{id}/render produces a .docx with {{court.*}} and {{lawyers.N.*}} substituted.
  4. Re-opening a saved draft restores the court + lawyer selection.
  5. go test ./... + bun run build clean.
  6. Migration applies cleanly via in-process boot path.
## Goal (m, 2026-05-26 17:49) In the submission-draft editor, lawyers should be able to: 1. **Select a court** — pick which court's address goes on the document (defaults to the project's `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. 2. **Select team members for the letterhead** — multi-select of users involved on this submission. Each picked member contributes their name, title, office address, phone, email to the variable bag as `{{lawyer.1.*}}`, `{{lawyer.2.*}}` etc. Same shape as the t-paliad-277 multi-party pattern. 3. **Use these in the Word template's letterhead block** so the rendered .docx carries the right recipient + the right sender block per submission. ## Existing data - **Courts** — `internal/handlers/courts.go` already 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.** - **Users** — `paliad.users` carries `display_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.** - **Office address** — `internal/offices/offices.go` carries only `{key, label_de, label_en}` per office. **Missing: street address, phone, fax per office.** Same shape as `courts.go` — needs the 8 office addresses hardcoded. - **Per-user phone** — no `direct_phone` or `fax` column on `paliad.users`. **Optional gap** for individualised contact lines. ## Scope (one issue, one branch — m's pick) ### Data 1. Extend `internal/offices/offices.go` to add `Address` + `Phone` per office (hardcoded directory). Head needs the 8 office addresses from m before dispatch — see open question §A below. 2. Add `direct_phone text NULL`, `fax text NULL` to `paliad.users` (additive migration, no `set_config` needed since user-table mutations don't fire the deadline_rules audit trigger — verify via Supabase MCP audit). User edits via the existing `/profile` page (add the two fields). NOT a blocker — letterhead can render an empty phone line until users fill it. ### Submission-draft editor UI 3. **Court picker**: dropdown in the submission-draft editor, pre-populated from `project.competent_court_id`. Reads the courts catalog (already an HTTP endpoint at `/api/courts`). Selected court_id stored on `paliad.submission_drafts` (new nullable column `selected_court_id`). 4. **Lawyer multi-select**: multi-select of team members for this project. The project_team_members list filters the user table. Selected user_ids stored as `selected_lawyers uuid[]` on `paliad.submission_drafts`. 5. Both selectors live in a new "Briefkopf / Letterhead" collapsible section above the variable form. UI matches the existing perspective+date strip pattern. ### Variable bag (`internal/services/submission_vars.go`) 6. Add `{{court.name}}`, `{{court.name_en}}`, `{{court.address}}`, `{{court.city}}`, `{{court.country}}` populated from the selected court (or empty when none selected). 7. Add `{{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 in `emitPartyGroup`. 8. Also expose joined-list aliases like `{{lawyers.names}}` (comma-joined) and `{{lawyers.offices}}` for free-text letterhead variants — same way `{{parties.claimants}}` works. 9. Cover with unit tests in `submission_vars_*_test.go`. ### Word template hookup 10. **Not in scope of this slice**: editorial work to actually USE the new variables in the existing `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 - **§A** — m needs to supply the 8 office addresses (or paste them somewhere head can find them). Defaults: HLC's published office addresses, probably the same as `hoganlovells.com` site footer. If m doesn't have time today, head can stub `Address: ""` for now and lawyers fill it via a follow-up. - **§B** — direct_phone and fax: ship as optional columns now or skip entirely until someone asks for per-user phone lines? Recommended: ship now (cheap, harmless, additive). ## Hard rules (lessons from today's mig 134 hotfix-3) - **MUST audit live schema** via `mcp__supabase__execute_sql` BEFORE drafting the migration. Specifically: confirm `direct_phone`/`fax` don't already exist on `paliad.users`; confirm `selected_court_id`/`selected_lawyers` don't already exist on `paliad.submission_drafts`; confirm no audit trigger on either table that requires `set_config`. Today's mig 134 had 3 hotfixes from skipped audits. - English identifiers in pkg/litigationplanner-adjacent code; pure logic where possible; tests follow code. - No `updated_at = now()` writes unless the column exists (it does on `paliad.users` and `paliad.submission_drafts` — verify both before any UPDATE). ## Acceptance 1. Submission-draft editor shows the Briefkopf section with court dropdown + lawyer multi-select. 2. Switching court / picking lawyers updates the variable preview live. 3. Word render via `/api/submission_drafts/{id}/render` produces a .docx with `{{court.*}}` and `{{lawyers.N.*}}` substituted. 4. Re-opening a saved draft restores the court + lawyer selection. 5. `go test ./...` + `bun run build` clean. 6. Migration applies cleanly via in-process boot path.
mAi self-assigned this 2026-05-26 15:59:14 +00:00
Author
Collaborator

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

The 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, a courtResolver, and submission_vars_court_test.go, sourced from project.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/offices is no longer {key, label_de, label_en}. It is a DB-backed service over paliad.offices carrying address_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_drafts had neither column; paliad.users has no direct_phone/fax; paliad.offices has phone/fax/address lines.

What Slice A adds

Migration 212 adds selected_court_id (FK to paliad.courts, ON DELETE SET NULL, mirroring projects.court_id) and selected_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 — raw pq.StringArray plus decoded []uuid.UUID, the *[]uuid.UUID three-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 — exactly parties.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.title is job_title and nothing else. profession is the approval-ladder tier (senior_pa), not a form of address. It must not reach a filing. The issue proposed job_title || profession_label; no profession-label table exists and inventing one would put a permissions enum on a court document.
  • Office address, phone and fax resolve through the same lookup as firm.office.* (extracted to lookupOffice), so the sender block and the office record cannot disagree about the same office.

direct_phone/fax on 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

filterPartiesBySelection has 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. Build now returns UnresolvedSelections naming 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.

resolveLawyers takes a narrow userLookup rather than the whole UserService, 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.id is text, not uuid — it is a slug, and so is projects.court_id. The first draft of migration 212 declared selected_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

  • Slice B — the Briefkopf section in the draft editor: court dropdown, lawyer multi-select mirroring the party picker, live preview, restore on reopen.
  • The .dotm letterhead block must actually USE lawyer.* — patentstyle's artifact, needs a template edit by m plus a regeneration. Head owns it.
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`](https://mgit.msbls.de/m/paliad/commit/af87252) — migration 212 - [`e6cd7b3`](https://mgit.msbls.de/m/paliad/commit/e6cd7b3) — column type fix - [`fe47b12`](https://mgit.msbls.de/m/paliad/commit/fe47b12) — Slice A ## The 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`, a `courtResolver`, and `submission_vars_court_test.go`, sourced from `project.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/offices` is no longer `{key, label_de, label_en}`.** It is a DB-backed service over `paliad.offices` carrying `address_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_drafts` had neither column; `paliad.users` has no `direct_phone`/`fax`; `paliad.offices` has phone/fax/address lines. ## What Slice A adds Migration 212 adds `selected_court_id` (FK to `paliad.courts`, `ON DELETE SET NULL`, mirroring `projects.court_id`) and `selected_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 — raw `pq.StringArray` plus decoded `[]uuid.UUID`, the `*[]uuid.UUID` three-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 — exactly `parties.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.title` is `job_title` and nothing else.** `profession` is the approval-ladder tier (`senior_pa`), not a form of address. It must not reach a filing. The issue proposed `job_title || profession_label`; no profession-label table exists and inventing one would put a permissions enum on a court document. - **Office address, phone and fax resolve through the same lookup as `firm.office.*`** (extracted to `lookupOffice`), so the sender block and the office record cannot disagree about the same office. `direct_phone`/`fax` on 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 **`filterPartiesBySelection` has 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. `Build` now returns `UnresolvedSelections` naming 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. `resolveLawyers` takes a narrow `userLookup` rather than the whole `UserService`, 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.id` is **`text`**, not `uuid` — it is a slug, and so is `projects.court_id`. The first draft of migration 212 declared `selected_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 - **Slice B** — the Briefkopf section in the draft editor: court dropdown, lawyer multi-select mirroring the party picker, live preview, restore on reopen. - **The `.dotm` letterhead block must actually USE `lawyer.*`** — patentstyle's artifact, needs a template edit by m plus a regeneration. Head owns it.
Author
Collaborator

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 .dotm letterhead block does not consume lawyer.* 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.0 vs lawyer.1 naming: with m, and still a find/replace while nothing consumes the keys.

The warning is the point of this slice

Slice A made Build report 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:

  • the draft payload now carries 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.
  • the editor renders it beside the export controls, role="alert", before the document is generated — not in the sidebar where it competes with the variable list.
  • clicking it drops the dead selections. Being told something is wrong with no way to act on it is not much better than not being told; a deleted colleague has no row left to untick.
  • unresolvedJSON is 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.
  • hasUnresolved keys 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

  • A court no longer in the catalogue stays visible in the dropdown, labelled unknown, rather than disappearing and reading as "court of the project". That is the same silent absence in a different place.
  • The picker shows no title. The team query carries no job_title, and the only other title on that row is profession, the approval-ladder tier. Slice A ruled that out of the letterhead; previewing it here would show a title the document will never print.
  • The party picker was the precedent for the markup, the patch semantics and the repaint flow. Mirrored, so the editor teaches one interaction rather than two.

Verification

  • Mutation-checked, both halves. Returning nil from unresolvedJSON fails exactly its two Go tests; keying hasUnresolved on the message fails exactly its one TS test. Neither touched anything else.
  • 406 frontend tests green (13 new), Go suite green, bun run build clean, gofmt clean.
  • No migration in this slice.
Slice B is on `mai/diesel/issue-139-slice-b` @ [`cf3e4d7`](https://mgit.msbls.de/m/paliad/commit/cf3e4d7), CI run 682 green. Not closing — m closes. ## Read this part first: the feature is not working end to end **The `.dotm` letterhead block does not consume `lawyer.*` 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.0` vs `lawyer.1` naming: with m, and still a find/replace while nothing consumes the keys. ## The warning is the point of this slice Slice A made `Build` report 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: - the draft payload now carries `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. - the editor renders it **beside the export controls**, `role="alert"`, before the document is generated — not in the sidebar where it competes with the variable list. - clicking it drops the dead selections. Being told something is wrong with no way to act on it is not much better than not being told; a deleted colleague has no row left to untick. - `unresolvedJSON` is 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. - `hasUnresolved` keys 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 - **A court no longer in the catalogue stays visible** in the dropdown, labelled unknown, rather than disappearing and reading as "court of the project". That is the same silent absence in a different place. - **The picker shows no title.** The team query carries no `job_title`, and the only other title on that row is `profession`, the approval-ladder tier. Slice A ruled that out of the letterhead; previewing it here would show a title the document will never print. - The party picker was the precedent for the markup, the patch semantics and the repaint flow. Mirrored, so the editor teaches one interaction rather than two. ## Verification - **Mutation-checked, both halves.** Returning `nil` from `unresolvedJSON` fails exactly its two Go tests; keying `hasUnresolved` on the message fails exactly its one TS test. Neither touched anything else. - 406 frontend tests green (13 new), Go suite green, `bun run build` clean, gofmt clean. - No migration in this slice.
Author
Collaborator

Re-measured before building — two of three asks are already shipped

Branch mai/knuth/issue-139-letterhead-vars, commit
08c58df. Writeup:
docs/findings-issue-139-remeasured-2026-07-30.md. Nothing built.

# m's ask State
1 Select a court; address flows in as {{court.*}} SHIPPED
2 Select team members → {{lawyer.N.*}} SHIPPED
3 Letterhead carries the right recipient and sender recipient shipped, sender not

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.dotm carries zero {{…}} placeholders. I scanned every
    XML part. It is the macro-only letterhead.
  • gen-skeleton-submission-template emits none either in its default mode.
    anchorsOnly = true is the default and writes ten {{#section:KEY}} pairs and
    nothing else; the placeholder-carrying body needs -anchors=false and serves
    pre-Composer drafts.

The letterhead text a draft gets comes from
paliad.submission_bases.section_spec, the hlc-letterhead row:

letterhead:  Schriftsatz von {{firm.name}}
             {{user.display_name}}, {{user.office}}
caption:     … {{project.court}} / {{caption.court_address}} …

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 draft
open. lawyer.* appears in no seed. Picking three colleagues changes the
variable 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.0 vs lawyer.1 is still open with m, and it is about to stop being
free.
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_sections at draft creation, so afterwards the
string 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_lawyers set, 0 have
selected_court_id.
Nobody has used either picker yet. The rename is still
free today, and it is free precisely because the seed does not reference the keys.

The split falls on exactly that line:

  • Not blocked — a sender block using only unindexed keys: {{lawyers}}
    (chosen names, comma-joined) plus {{lawyer.office_address}} /
    {{lawyer.office_phone}} for the first-named. No index reaches stored content.
  • Blocked — a per-colleague block with name, title and office on their own
    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

  • The empty-selection fallback already exists. resolveLawyers returns the
    acting user when selected_lawyers is empty
    (submission_vars_letterhead.go:171), so a seed naming {{lawyer.name}} is
    behaviour-preserving for all 33 existing drafts by construction. No separate
    fallback work needed.
  • All 24 existing letterhead rows are byte-identical to the seed — none
    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 "exercising
every 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; the
package 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.

## Re-measured before building — two of three asks are already shipped Branch `mai/knuth/issue-139-letterhead-vars`, commit [`08c58df`](https://mgit.msbls.de/m/paliad/commit/08c58df). Writeup: `docs/findings-issue-139-remeasured-2026-07-30.md`. **Nothing built.** | # | m's ask | State | |---|---|---| | 1 | Select a court; address flows in as `{{court.*}}` | **SHIPPED** | | 2 | Select team members → `{{lawyer.N.*}}` | **SHIPPED** | | 3 | Letterhead carries the right recipient **and sender** | recipient shipped, **sender not** | 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.dotm` carries zero `{{…}}` placeholders.** I scanned every XML part. It is the macro-only letterhead. - **`gen-skeleton-submission-template` emits none either** in its default mode. `anchorsOnly = true` is the default and writes ten `{{#section:KEY}}` pairs and nothing else; the placeholder-carrying body needs `-anchors=false` and serves pre-Composer drafts. The letterhead text a draft gets comes from **`paliad.submission_bases.section_spec`**, the `hlc-letterhead` row: ``` letterhead: Schriftsatz von {{firm.name}} {{user.display_name}}, {{user.office}} caption: … {{project.court}} / {{caption.court_address}} … ``` 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 draft open. `lawyer.*` appears in no seed. Picking three colleagues changes the variable 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.0` vs `lawyer.1` is still open with m, and it is about to stop being free.** 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_sections` at draft creation, so afterwards the string 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_lawyers` set, 0 have `selected_court_id`.** Nobody has used either picker yet. The rename is still free today, and it is free precisely because the seed does not reference the keys. The split falls on exactly that line: - **Not blocked** — a sender block using only unindexed keys: `{{lawyers}}` (chosen names, comma-joined) plus `{{lawyer.office_address}}` / `{{lawyer.office_phone}}` for the first-named. No index reaches stored content. - **Blocked** — a per-colleague block with name, title and office on their own 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 - **The empty-selection fallback already exists.** `resolveLawyers` returns the acting user when `selected_lawyers` is empty (`submission_vars_letterhead.go:171`), so a seed naming `{{lawyer.name}}` is behaviour-preserving for all 33 existing drafts by construction. No separate fallback work needed. - **All 24 existing letterhead rows are byte-identical to the seed** — none 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 "exercising every 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; the package 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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#139
No description provided.