docforge UX slice S3 (PRD §2.1 / §3 S3). A shared, body-attached modal lets
the lawyer browse template bases, flip output language + data-mode, and — in
the editor — commit a base with 'Diese Basis verwenden', replacing the blind
<select> as the chooser. Opened from the editor 👁 button AND both catalog row
kebabs (the S1/S2 stubs are now live).
Body renders the EXISTING structural preview on cheap rails; the body region is
swappable so S4 drops a truthful .docx→image render behind the same modal +
endpoint shape. Spinner + pager are the shell S4 expects.
Backend (minimal, no truthful render — that's S4):
- GET /api/submission-preview?base&code&lang&data&draft?&project? → {preview_html}.
Draft+mine uses the draft's resolved bag (previews a base the draft has NOT
committed to — no persistence); else a fresh context bag (project-less fill,
t-paliad-364). data=sample swaps a sample missing-marker so unresolved keys
render readable sample text, not [KEIN WERT].
- SubmissionDraftService.RenderPreviewWithMarker + RenderContextPreviewHTML
(thin: reuse BuildRenderBag / vars.Build + RenderHTML).
- resolvePreviewTemplateBytes: tpl:→carrier, base_id→bytes IFF it carries merge
placeholders, else fall back to the code's merge template (anchors-only
Composer bases can't be shown as structural HTML — their letterhead/styling
surfaces in S4's truthful render). sampleMissingMarker by key suffix.
- Unit tests: sampleMissingMarker + normalizePreviewLang.
Frontend:
- New shared client/base-preview-modal.ts (built once, body-attached; serves
editor + project tab + global picker). Base-switcher, DE/EN + meine/Beispiel
toggles, swappable body, spinner, pager stub, conditional 'Diese Basis
verwenden' (editor commits via onBaseChange; catalog = look-only).
- Editor 👁 button un-stubbed + wired; both catalog kebab 'Vorschau' items
un-stubbed + wired. global.css: .base-preview-* modal styles.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail).
docforge UX slice S2 (PRD docs/plans/prd-docforge-ux-2026-06-01.md §3).
De-densifies the draft editor (G1-d): draft management + template controls
move out of the variable sidebar into a horizontal, wrap-friendly header
strip above the working grid. The sidebar then holds only the fill-in work
— 'Aus Projekt importieren', parties, and the variable fields.
- submission-draft.tsx: new .submission-draft-toolbar strip carrying the
switcher (+ '+ Neuer Entwurf'), name (+ Löschen), Stichwort (+ hint),
Vorlagenbasis picker, language toggle, fallback notice + savestatus. The
'Als .docx exportieren' button stays top-right in the header.
- New 👁 Vorschau button next to the base picker (id
submission-draft-preview-base-btn), disabled 'Bald verfügbar' stub — S3
wires it to the truthful base-preview modal (PRD §2).
- global.css: .submission-draft-toolbar* layout + .submission-draft-base-
controls (select + 👁 on one row); single-column collapse < 900px.
- i18n: submissions.draft.base.preview(.soon) (DE/EN).
Pure relayout — NOT a behavior change. Every control keeps its id, so the
existing wiring is untouched (switcher, name save, keyword→composer_meta,
base picker PATCH base_id, language autosave, savestatus). Grounded in the
2-panel reality (§1.4): the conditional Abschnitte section panel is
unchanged (still display:none when a draft has no sections; S5 addresses
its discoverability). TS/CSS only, no backend.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail).
docforge UX slice S1 (PRD docs/plans/prd-docforge-ux-2026-06-01.md §3).
Kills the invisible two-equal-buttons confusion (G1-b) on both catalog
surfaces, consistently, while keeping each surface's distinct context.
- New shared kebab helper frontend/src/client/row-action-menu.ts: an
accessible ⋯ trigger + body-attached, position:fixed popover (the
.entity-table-wrap clips with overflow-x:auto, so the popover is
body-attached like the event-card choices popover). Outside-click /
Escape / scroll / resize close it; one open at a time.
- Project Schriftsätze tab (client/submissions.ts): primary 'Entwurf
öffnen' (was 'Bearbeiten') + ⋯ menu { Direkt exportieren (.docx) [was
the 'Generieren' button], Vorschau Vorlagenbasis [disabled stub, S3
wires it] }. onGenerateClick→generateAndDownload (busy-cursor feedback,
no button to relabel).
- Global picker (client/submissions-new.ts): primary 'Entwurf starten'
(project-less free-start, kept first-class) + ⋯ menu { Mit Projekt
verknüpfen… [the modal picker], Vorschau Vorlagenbasis [disabled stub] }.
- i18n: new projects.detail.submissions.action.open (DE/EN); kebab item
labels inline isEN() (matches the picker's dynamic-row convention).
- CSS: .row-action-menu* in global.css.
The 'Vorschau Vorlagenbasis' entries are disabled stubs ('Bald verfügbar')
until S3 lands the truthful base-preview modal. TS/CSS only, no backend.
bun build + go vet ./... + go test ./... clean.
Audit of the end-to-end docforge journey (project Schriftsätze tab, global
/submissions/new picker, draft editor, /admin/templates authoring) + a
prioritized UX plan. Captures m's grill answers + 4 option-picker decisions:
- base preview = truthful (LibreOffice .docx→image, on-demand+cached), via a
'Vorschau' button → modal with a base-switcher
- catalog rows = one primary 'Entwurf öffnen' + ⋯ kebab (kills the invisible
Bearbeiten-vs-Generieren split)
- editor meta → header toolbar (de-densifies the sidebar)
- free-start stays first-class; entry points kept distinct-but-consistent
- live editing preview stays structural (only the base preview is pixel-true)
Resolved the 2-vs-3-panel discrepancy: the Abschnitte panel is display:none
when a draft has no seeded sections, so m correctly sees 2 panels.
Design only — no engine redesign, no code. Tracer-bullet slice train S1..S5.
m mandate (2026-06-01): 'I dont want that formatting in code! it should use
our word files and styles.' The t-paliad-364 generator synthesised a flat
Rubrum in Go, borrowing only the firm's style NAMES. The HLC Patents Style
.dotm already contains the real, firm-authored letterhead + Rubrum in Word —
header table (logo via header2 + address blocks), case-info table, recitals
table — all in HLCpat- styles.
This rewrites gen-hl-skeleton-template to LIFT that authored layout verbatim:
- ConvertDotmToDocx strips macros → clean .docx carrier (idempotent on .docx)
- footer1 firm-NAME SDT → {{firm.name}} (A-S3); footer2 legal block kept
- document.xml body kept verbatim through the recitals table; the TOC +
checklist demo + 'Template Info And Manual' style-guide section truncated
- carrier <w:document> open tag + <w:sectPr> reused → header1/2 + footer1/2 +
titlePg letterhead wiring preserved
- structure-aware, occurrence-ordered paragraph walk swaps the firm's example
text for paliad merge tokens (parties/caption/project/user/today/proc-event);
transformed paragraphs keep their <w:pPr> verbatim — the named style supplies
formatting, none authored in code
Reconciles t-paliad-366 item 1 (HLpat→HLCpat drift): carrier + Rubrum both HLCpat-.
DATA GAP flagged to m: no structured HL office postal addresses + no court_id FK
(project.court is free text), so the sender address block + recipient court
address lines are kept as the .dotm's authored editable text; only the court
NAME is wired to {{project.court}}. Rules flip to placeholders when that data lands.
Verified: HasMergePlaceholders=true (de+en); header2/footer2/media/styles.xml/
theme/numbering byte-identical to the .dotm; package integrity clean (macros
stripped, no dangling rels, main type demoted); merge dry-run fills with zero
leftover {{}} / zero [KEIN WERT]. go vet + go test ./... + bun build green.
P3(b) fill-what-we-can (submission_vars.go):
The project-less branch of SubmissionVarsService.Build ran only firm/today/
user/proceduralEvent resolvers, so caption.*/project.proceeding.* never
populated and every Rubrum value rendered [KEIN WERT]. The rule is already
loaded on this path and carries ProceedingTypeID. Now the branch loads the
proceeding type via loadProceedingType(rule.ProceedingTypeID) (tolerates a nil
id) and appends a proceeding-only projectResolver{project:nil} + a
captionResolver{project:nil} — both nil-project safe. Result: caption heading /
designations / versus / subject + the proceeding line fill from the
submission_code's proceeding; only party names / case number / court stay blank
for the lawyer. Preserves the "Ohne Projekt" affordance (t-paliad-243).
addProjectVars is now nil-project safe (guards the project.* direct fields,
keeps the pt-driven project.proceeding.* block) so projectResolver can serve as
the proceeding-only resolver. Pinned by TestProjectlessFill_* +
TestAddProjectVars_NilProjectFillsProceedingOnly (no DB).
P3(a) styling — merge-safe styled firm-skeleton (scripts/gen-hl-skeleton-template):
Generation landed on docx.BuildFallbackSkeleton (generic Heading1/2/Normal)
because the firm-skeleton's body had been repurposed into an anchors-only
Composer base, which HasMergePlaceholders rejects. Rewrote the generator to take
the deployed clean .docx carrier and replace ONLY word/document.xml with a clean
caption-driven Rubrum that uses the firm Rubrum styles (Table-Recitals-Party/
PartyDetails/PartyRoles/Sequencers, Heading-H2, Signature, Body-B0) and the same
{{key}}/{{caption.*}} placeholders the in-process fallback uses — preserving the
carrier's styles/theme/numbering/letterhead/logo and its sectPr verbatim. Adds a
-lang flag (DE _firm-skeleton.docx, EN _skeleton.en.docx) and auto-detects the
firm style prefix (HLpat-/HLCpat-) so it stays correct across the .dotm rebrand
drift. The resolver's tier-4/tier-3 merge-safe guard auto-prefers the restored
templates — no handler change.
Dry-run (TEST_DATABASE_URL, de.inf.lg.erwidg): project-less render fills caption
heading="In dem Rechtsstreit"/Klägerin/gegen/Beklagte/Patentverletzung + the
proceeding line, only party/case/court blank; full-project render additionally
fills parties + case number + court. Both carry the HLpat-Table-Recitals-* /
HLpat-Heading-H2 / HLpat-Signature styles, HasMergePlaceholders=true, no
{{#section}} junk.
P1: 8 anchor/trigger rows (Zustellung des Urteils / Veröffentlichung der
Erteilung) were mislabeled event_type='filing'+primary_party='both' and leaked
into the /submissions/new draftable-submission picker. Migration 164 re-kinds
them to event_type='decision'+primary_party='court', aligning them with the 16
sibling court-act rows the model already has; the picker's event_type='filing'
filter then excludes them. Defensive guard added to loadSubmissionCatalog
(primary_party IS DISTINCT FROM 'court') as belt-and-braces against future drift.
Safety: the only event_type coupling for these rows is ruleAnchorKind
(projection_service.go) — they now anchor as appointments (correct, sibling-
consistent); deadline computation keys off is_court_set not event_type, and the
child sequence guard parentHasAnchoredActual UNIONs both anchor tables, so no
chain breaks. Verified: catalog 113->105, 8 court acts gone, 48 'both' party
submissions (incl. UPC appeal briefs) retained.
P2: the grouped picker table already renders a correct colspan group-header row
(a911a2d); the defect was contrast — the band used --color-bg-subtle, the SAME
token as the thead, so groups read as undelimited floating text. New
--color-bg-group-header token (cool/deeper in light, raised-cream in dark) +
heavier top divider + neutral left accent + darker label make each proceeding a
distinct section. Live deployed bundle confirmed current (not stale).
t-paliad-361, follow-up to t-paliad-358 A-S2. m ruled on the 7 lexy-wording
flags (AskUserQuestion 2026-06-01 14:30). Most flags CONFIRMED the live
wording; three changes land here, all caption (Rubrum) wording, all in one
reversible migration 163.
Change 1 — UPC appeal responding party (EN): 'Appellee' → 'Respondent'.
Fixed at the data source: the mig-137 role-label override on
upc.apl.unified (the only place 'Appellee' was stored). The caption
resolver's instance-derived EN fallback already said 'Respondent', so no
code change. DE side (Berufungsbeklagter) untouched per m.
Change 2 — restore the standalone 'Streitpatent: {{project.patent_number_upc}}'
(DE) / 'Patent in suit:' (EN) line in the upc-formal Composer caption seed,
dropped in A-S2 (mig 161). Keeps the parametric 'In der Sache' heading (m did
not revert that). Only the upc-formal base is touched; grouped with the case
number ahead of {{project.court}}.
Change 3 — backfill lexy-confirmed role-label overrides for the four DE
appeal/nullity proceedings that carried none, so designations are correct
even when project.instance_level is unset (statute-grounded: §§ 511/542/544
ZPO, §§ 81/110 PatG; bracketed-inclusive gender style):
de.inf.olg Berufungskläger(in) / Berufungsbeklagte(r) // Appellant / Respondent
de.inf.bgh Revisionskläger(in) / Revisionsbeklagte(r) // Appellant / Respondent
de.null.bpatg Nichtigkeitskläger(in) / Beklagte(r) (Patentinhaber(in)) // Nullity claimant / Defendant (patent proprietor)
de.null.bgh Berufungskläger(in) / Berufungsbeklagte(r) // Appellant / Respondent
Updates submission_vars_caption_test.go: adds EN assertions + cases pinning the
Respondent change and the four backfilled designations (each with instance_level
unset, proving the override path). go vet + go test ./... + bun build clean.
A-S3 part 1 (merge-fallback letterhead, in-repo): the fallback skeleton
(docx.BuildFallbackSkeleton) gains a minimal Word page-header letterhead
(word/header1.xml) carrying only {{firm.name}}, filled from branding by the
variable bag. A generated fallback-path document now repeats a correct,
firm-agnostic firm identity on every page (the firm name moved out of the body
into the header; no hard-coded firm name anywhere). Wired the header part:
Content_Types override + document.xml.rels relationship (rId2) + sectPr
headerReference; document gains xmlns:r. Deliberately minimal — the fallback is
a starter, not full firm chrome.
A-S3 part 2 (firm-skeleton .docx, mWorkRepo — separate commit): _firm-skeleton.docx
footer1's "Firm name" SDT content control hard-coded "Hogan Lovells" → now
{{firm.name}}, filled by the Composer's final renderer pass / merge.go (both run
SubmissionRenderer over header/footer parts). Surgical <w:t> edit + repack (all
38 parts preserved, verified renders + fills cleanly, integrity OK). Pushed to
HL/mWorkRepo as mAi (commit 5a3a1722).
Path taken: option (a) for the firm NAME (cleanly placeholderised). NOT option
(a) for footer2's HL legal-entity boilerplate (registered no. OC323639, LLP
structure, 40+ office cities) — token-swapping the name into it would assert
false legal facts for a non-HLC firm; left intact and flagged (needs per-firm
legal-footer config, not templating). No corrupt .docx shipped.
Completes the Rubrum + letterhead auto-fill Option A train (A-S1 signature_block
+ generate-fallback fix; A-S2 parametric caption.*; A-S3 firm-agnostic letterhead).
Tests: fallback header asserts {{firm.name}} present + renders firm-agnostically
(de+en); patched firm-skeleton verified to render + fill (transient check).
go vet ./... + bun build clean; touched packages green.
Completes the nomen train (S1–S5). Adds the FIRM tier of the name-composition
precedence chain — per-document → user → FIRM → system (PRD §3.1/§3.2) —
mirroring firm_dashboard_default exactly.
Storage + service:
- Migration 162: paliad.firm_name_compositions singleton (id=1, CHECK id=1,
RLS read-all + service-role writes) — same shape as firm_dashboard_default
(mig 117), holding a validated { artifact_id: Composition } jsonb map.
- FirmNameCompositionService (Get/Set/Clear) + getFirmNameCompositions /
setFirmNameCompositions / clearFirmNameCompositions singleton helpers in
name_composition_spec.go.
Resolution:
- resolveComposition is now variadic over ordered specs (user, firm); first
valid wins, else system default. Existing single-spec callers unchanged.
- Render path threads the firm tier: renderSubmissionDraftTitle /
RenderSubmissionFilenameFor gain a firm param; newDraftName +
submissionDownloadFilename load it (nil-safe). A firm default thus changes
the effective name for every user without a personal override.
Admin surface (mirrors firm_dashboard_default):
- GET/PUT/DELETE /api/admin/name-compositions{/artifact_id} (adminGate) read
back / set / clear the firm default per artifact.
- /settings Namensschemata cards gain an admin-only "Firmenstandard" block
(set from the current template field / clear) revealed via is_admin, plus a
"Firmenstandard" badge for non-admin users whose effective name comes from
the firm tier. SettingsNameArtifact now resolves user→firm→system and
exposes firm_is_set/firm_template.
Tests: pure precedence (user>firm>system) + firm-tier view + live firm
round-trip/Validate-rejection (via db.ApplyMigrations). go vet, go test ./...,
bun build all clean; gated live tests green against TEST_DATABASE_URL.
NOTE (merge ordering): golang-migrate is forward-only. Migration 162 must not
reach a DB before bohr's 161 (Rubrum Composer seed) exists, or 161 will be
skipped (current>161 → never applied). Merge 161 before/with 162.
Browser Playwright of the admin firm controls deferred to post-deploy
mai-tester — shared Supabase login wall blocks pre-merge browser login (same
ceiling as t-paliad-354).
Promotes the case caption (Rubrum) to ONE parametric set of resolver keys
(caption.*) consumed identically by every render path, so the wording no
longer diverges per path and reflects the forum.
New caption.* keys (addCaptionVars, submission_vars.go), each in bare +
_de + _en forms (bare resolves to the draft language):
caption.heading · caption.claimant_designation · caption.defendant_designation
caption.versus · caption.subject
Parametrised from data already in the bag — NO new schema:
- designations reuse the proceeding-type role-label overrides (mig 137:
upc.apl.unified→Berufungskläger, upc.rev.cfi→Antragsteller (Nichtigkeit),
epa.opp.*→Einsprechende(r)/Patentinhaber(in)); else instance-derived
appeal/cassation (project.instance_level); else civil default Klägerin/
Beklagte // Claimant/Defendant.
- heading + subject from jurisdiction + the dotted code's nature segment
(inf→"In dem Rechtsstreit"/"Patentverletzung", null→"In der
Patentnichtigkeitssache", UPC→"In der Sache"/"In the matter",
opp→"Im Einspruchsverfahren").
Also exposes project.proceeding.jurisdiction.
All three render paths now reference the SAME keys:
1. docx.BuildFallbackSkeleton (merge fallback) — heading/designations/versus/
wegen-subject are {{caption.*}} placeholders.
2. demo per-code template de.inf.lg.erwidg.docx (mWorkRepo) — regenerated via
scripts/gen-demo-submission-template; caption wired to caption.*; closing
drops the duplicate {{firm.name}} line (now carried by signature_block).
Pushed to HL/mWorkRepo as mAi (commit 3682299).
3. Composer caption seeds — mig 161 rewrites the caption section seed_md of
all 4 bases (hlc-letterhead, neutral, lg-duesseldorf, upc-formal) to the
parametric form (position-independent jsonb_agg patch; reversible down).
our_side is intentionally NOT a caption driver — the caption designates both
parties by procedural role regardless of which side we act for.
Tests: resolveCaption forum matrix (DE-LG/BPatG/UPC/role-label/instance-appeal/
EPA-opp), bare-resolves-to-lang, fallback skeleton renders caption.* keys.
mig 161 passes TestMigrations_DryRun. go vet ./... + bun build clean; all
touched packages green (the live approval/migration_136 failures are
pre-existing shared-DB env issues, unrelated).
LEXY-REVIEW FLAGS in the report — DE caption conventions are practitioner
convention, not in the youpc corpus; specific wordings flagged for sign-off.
Adds the /settings "Namensschemata" tab so users can customise the two wired
name artifacts (submission_draft_title, submission_docx_filename) via a
single-line {token} template, with a clickable palette, live preview, and
reset-to-default — PRD §7.
Engine (pure, pkg/nomen):
- Composition.Template() serialises a composition to "{var}" shorthand;
ParseTemplate() is its inverse — tokens + literal separators (trailing,
owned by the left segment) + paren Wrap. Missing-rules are NOT in the
shorthand (PRD §7); the parser leaves every segment KindOmit. Leading /
trailing literals are rejected (the trailing-separator model can't carry
them) so a save never silently drops characters. Table + round-trip tests.
Paliad glue (internal/services/name_template.go):
- ParseNameTemplate overlays each segment's missing-rule from the artifact's
system default and validates against the catalog.
- PreviewNameComposition renders against the fixed PRD sample (Bayer AG / UPC
/ Sandoz / UPC_CFI_123/2026 / today) and an empties resolver so the
missing-rule behaviour is visible. The frontend never parses templates —
the nomen engine stays the single source of truth.
- SettingsNameArtifacts / SettingsNameArtifact build the per-artifact cards
(current template, system default, override flag, ordered palette, previews).
API (internal/handlers/name_compositions.go):
- GET /api/me/name-compositions — cards
- POST /api/me/name-compositions/preview — live preview + validation
- PUT /api/me/name-compositions/{artifact_id} — store override
- DELETE /api/me/name-compositions/{artifact_id} — reset to system default
Storage reuses the Slice-3 service surface (UserNameCompositions /
SetUserNameCompositions) via read-modify-write; no new column, no migration.
Frontend: new tab + JS-built cards (palette insert-at-cursor, 250ms-debounced
preview, save/reset, DE/EN labels), CSS, and i18n keys (de + en).
Gates: go vet, go test ./..., bun build all clean. Browser verification of the
settings UX is deferred to post-deploy mai-tester — the shared Supabase login
wall blocks pre-merge browser login (same ceiling as t-paliad-354).
Two letterhead/Rubrum auto-fill fixes (Option A, no schema change):
1. firm.signature_block: was hardcoded "" ("reserved for Phase 2"), so every
template referencing {{firm.signature_block}} rendered blank. Now filled
from branding.Name — the firm identity line of a submission's signature
block (the signature section seeds with signature_block + user.display_name).
Firm-agnostic: a FIRM_NAME redeploy signs with the right firm.
2. Generate-fallback junk (kepler audit §1 Path 3): resolveSubmissionTemplate is
the merge-path resolver (every caller feeds merge.go), but its lower tiers
fetched _firm-skeleton.docx / _skeleton.docx — which were repurposed into
anchors-only Composer bases (t-paliad-313 Slice B). Their bodies hold only
{{#section:KEY}} markers, which placeholderRegex ignores, so merge.go emitted
them verbatim as literal "{{#section:letterhead}}…" junk for every code
without a per-code template (i.e. everything except de.inf.lg.erwidg).
Fix:
- docx.BuildFallbackSkeleton(lang): in-process, lang-aware, merge-safe basic
Schriftsatz with a data-driven basic Rubrum (real {{key}} placeholders the
var bag fills). Always available, no Gitea round-trip.
- docx.HasMergePlaceholders guards tiers 3/4/5: a fetched skeleton is used
only if it carries real placeholders, else we fall through to the embedded
fallback. Today's anchors-only/placeholder-free files are skipped; a future
merge-safe firm-skeleton (with letterhead) is preferred again automatically.
- merge.go strips stray {{#section:…}}/{{/section:…}} markers defensively so
no anchors-only carrier can ever leak Composer junk into a merged document.
Verified: confirmed live that deployed _firm-skeleton.docx + _skeleton.docx are
anchors-only (fetch+unzip); unit tests cover BuildFallbackSkeleton rendering a
real Rubrum (de+en), HasMergePlaceholders classification, marker stripping, and
the signature_block fill. go build / vet ./... / test ./... + bun build clean.
Out of scope (flagged for next slices): demo template's closing prints
{{firm.name}} then {{firm.signature_block}} (=firm.name) → A-S2 dedups the demo
wording. Restoring firm letterhead chrome to the merge fallback → A-S3.