Files
paliad/internal/db/migrations/150_submission_bases_specialist.up.sql
mAi 94310ba498
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(submissions): Composer Slice E — specialist bases + base-swap content survival (m/paliad#141)
Two new firm-agnostic base templates + the generic generator that
produced them + a regression test pinning Q10's base-swap-content-
survival contract.

Mig 150: seeds two `submission_bases` rows with firm=NULL.

- lg-duesseldorf — proceeding_family='de.inf.lg'. Conservative
  German legal style: Times New Roman 11pt; plain black headings.
  Stylemap targets LG-Body / LG-Heading1..3 / LG-ListBullet /
  LG-ListNumber / LG-Quote.

- upc-formal — proceeding_family='upc.inf.cfi'. UPC court style:
  Calibri 11pt body; UPC-blue (#1F3864) headings; Cambria italic
  for blockquotes. Stylemap targets UPC-Body / UPC-Heading1..3 / …

Both rows ship the same 10-section spec.defaults shape as the Slice A
bases (letterhead → signature) with their own seed Markdown.

scripts/gen-submission-base/main.go (NEW, ~240 LoC):

- Generic generator with -preset flag. Two presets baked in
  (lg-duesseldorf + upc-formal). Each preset hard-codes typography
  (font, sizes, colour) so the lawyer can swap between bases and
  see chrome change while section content carries through unchanged.
- Output is byte-reproducible (zip mtime pinned to 2026-05-26 UTC).
- Emits a minimal Composer-mode .docx: [Content_Types].xml,
  _rels/.rels, word/_rels/document.xml.rels (empty envelope so the
  composer's hyperlink-rels patch from Slice D has somewhere to land),
  word/styles.xml (preset's full named-style block + "Hyperlink"
  character style for Slice D link runs), word/document.xml (anchor-
  only body in §6.1 default section order).

Gitea uploads (via mAi):

- 6 - material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx
  blob SHA: 82f57b3cb3b54c755fc5ab36862bfd61b8aaa73e
- 6 - material/Templates/Word/Paliad/Composer/upc-formal.docx
  blob SHA: 41b9a388263ccc43ddc28b55caab301a4cf74fe8

These live under Composer/ (not under HLC/) so a future non-HLC
deployment serves the same cross-firm files.

Backend wiring:

- internal/handlers/files.go: two new fileRegistry entries
  (composerBaseLGDuesseldorfSlug, composerBaseUPCFormalSlug) +
  matching slugs in composerBaseSlugMap so fetchComposerBaseBytes
  routes the new catalog rows to the new Gitea objects.

Tests:

- TestComposer_BaseSwapPreservesContent — composes the same draft
  against an HLC-style stylemap AND an LG-style stylemap; asserts
  (a) content survives both ways, (b) each output carries the
  correct stylemap-entry stylenames, (c) neither output leaks the
  other's stylenames. Pins Q10's base-swap-survives-content
  contract.

Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean.

NOT in scope (Slice E's brief was specialist bases + survival test):

- Generator coverage for HL Patents Style bases — gen-hl-skeleton-
  template stays as the per-firm path (it needs the proprietary
  .dotm source). gen-submission-base is for firm-agnostic bases.
- LG-Düsseldorf-court-style-guide deep fidelity — the LG preset is
  a conservative starting point; admin refines via the admin editor
  in a later slice if needed.
- numbering.xml carrying numId=1/2 — Slice D's MD walker emits
  visible "• " / "N. " prefixes that don't need numbering.xml;
  honours stylemap entry for indentation.

Hard rules honoured:

- Migration purely additive (`ON CONFLICT (slug) DO NOTHING`).
- NO behavior change for pre-Composer drafts.
- NO behavior change for existing hlc-letterhead + neutral seed
  rows.
- {{rule.X}} aliases preserved (walker passes placeholders through;
  v1 SubmissionRenderer pass substitutes).
- Q10 base-swap-content-survival pinned by new test.

t-paliad-317 Slice E
2026-05-26 20:21:12 +02:00

129 lines
9.7 KiB
SQL

-- t-paliad-317 (m/paliad#141): Composer Slice E — specialist bases.
--
-- Two firm-agnostic bases for proceeding-family-specific styling:
--
-- lg-duesseldorf — DE LG (de.inf.lg) conservative German legal style.
-- Times New Roman 11pt; black headings.
-- upc-formal — UPC court of first instance (upc.inf.cfi) formal
-- style. Calibri 11pt body; UPC-blue (1F3864) headings;
-- Cambria italic for blockquotes.
--
-- The .docx body for each is a minimal Composer-mode skeleton with
-- the 10 default section anchors and an empty rels envelope. The
-- styles.xml declares the {prefix}-Body / -Heading1/2/3 / -ListBullet
-- / -ListNumber / -Quote paragraph styles + a "Hyperlink" character
-- style (matches the MD walker's emitted r:id="rIdComposerN" link
-- runs from Slice D).
--
-- Generator: scripts/gen-submission-base/main.go (each preset hard-
-- codes the typography). The .docx files are uploaded to Gitea at
-- 6 - material/Templates/Word/Paliad/Composer/{slug}.docx as mAi.
--
-- The mig is additive only: ON CONFLICT (slug) DO NOTHING keeps a
-- re-run safe and existing rows untouched.
INSERT INTO paliad.submission_bases
(slug, firm, proceeding_family, label_de, label_en,
description_de, description_en,
gitea_path, section_spec, is_default_for)
VALUES
('lg-duesseldorf', NULL, 'de.inf.lg',
'LG-Düsseldorf-Stil', 'LG-Düsseldorf style',
'Konservativer DE-LG-Stil: Times New Roman 11pt, schlichte Überschriften.',
'Conservative DE LG style: Times New Roman 11pt, plain headings.',
'6 - material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx',
jsonb_build_object(
'version', 1,
'stylemap', jsonb_build_object(
'paragraph', 'LG-Body',
'heading_1', 'LG-Heading1',
'heading_2', 'LG-Heading2',
'heading_3', 'LG-Heading3',
'list_bullet', 'LG-ListBullet',
'list_numbered', 'LG-ListNumber',
'blockquote', 'LG-Quote'
),
'defaults', jsonb_build_array(
jsonb_build_object('section_key','letterhead', 'kind','prose', 'order_index', 1, 'label_de','Briefkopf', 'label_en','Letterhead',
'included',true,
'seed_md_de', E'Schriftsatz von {{firm.name}}\n\n{{user.display_name}}',
'seed_md_en', E'Submission by {{firm.name}}\n\n{{user.display_name}}'),
jsonb_build_object('section_key','caption', 'kind','prose', 'order_index', 2, 'label_de','Rubrum', 'label_en','Caption',
'included',true,
'seed_md_de', E'In der Sache\n\n**{{parties.claimant.0.name}}**\n— Klägerin —\n\ngegen\n\n**{{parties.defendant.0.name}}**\n— Beklagte —\n\nAktenzeichen: {{project.case_number}}\n{{project.court}}',
'seed_md_en', E'In the matter\n\n**{{parties.claimant.0.name}}**\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\n— Defendant —\n\nCase number: {{project.case_number}}\n{{project.court}}'),
jsonb_build_object('section_key','introduction', 'kind','prose', 'order_index', 3, 'label_de','Einleitung', 'label_en','Introduction',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','requests', 'kind','requests', 'order_index', 4, 'label_de','Anträge', 'label_en','Requests',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','facts', 'kind','prose', 'order_index', 5, 'label_de','Sachverhalt', 'label_en','Facts',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','legal_argument', 'kind','prose', 'order_index', 6, 'label_de','Rechtliche Würdigung', 'label_en','Legal argument',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','evidence', 'kind','evidence', 'order_index', 7, 'label_de','Beweisangebote', 'label_en','Evidence offering',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','exhibits', 'kind','prose', 'order_index', 8, 'label_de','Anlagen', 'label_en','Exhibits',
'included',false, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','closing', 'kind','prose', 'order_index', 9, 'label_de','Schlussformel', 'label_en','Closing',
'included',true,
'seed_md_de', E'Mit freundlichen Grüßen',
'seed_md_en', E'Yours sincerely,'),
jsonb_build_object('section_key','signature', 'kind','prose', 'order_index',10, 'label_de','Unterschrift', 'label_en','Signature',
'included',true,
'seed_md_de', E'{{user.display_name}}',
'seed_md_en', E'{{user.display_name}}')
)
),
'{}'::text[]
),
('upc-formal', NULL, 'upc.inf.cfi',
'UPC-Verfahren', 'UPC formal',
'UPC-Verfahrensstil: Calibri 11pt, UPC-blaue Überschriften, Cambria-Zitate.',
'UPC court style: Calibri 11pt, UPC-blue headings, Cambria quotes.',
'6 - material/Templates/Word/Paliad/Composer/upc-formal.docx',
jsonb_build_object(
'version', 1,
'stylemap', jsonb_build_object(
'paragraph', 'UPC-Body',
'heading_1', 'UPC-Heading1',
'heading_2', 'UPC-Heading2',
'heading_3', 'UPC-Heading3',
'list_bullet', 'UPC-ListBullet',
'list_numbered', 'UPC-ListNumber',
'blockquote', 'UPC-Quote'
),
'defaults', jsonb_build_array(
jsonb_build_object('section_key','letterhead', 'kind','prose', 'order_index', 1, 'label_de','Briefkopf', 'label_en','Letterhead',
'included',true,
'seed_md_de', E'Schriftsatz von {{firm.name}}\n\n{{user.display_name}}',
'seed_md_en', E'Submission by {{firm.name}}\n\n{{user.display_name}}'),
jsonb_build_object('section_key','caption', 'kind','prose', 'order_index', 2, 'label_de','Rubrum', 'label_en','Caption',
'included',true,
'seed_md_de', E'# In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n— Defendant —\n\nUPC-Aktenzeichen: {{project.case_number}}\nStreitpatent: {{project.patent_number_upc}}',
'seed_md_en', E'# In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n— Defendant —\n\nUPC case number: {{project.case_number}}\nPatent in suit: {{project.patent_number_upc}}'),
jsonb_build_object('section_key','introduction', 'kind','prose', 'order_index', 3, 'label_de','Einleitung', 'label_en','Introduction',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','requests', 'kind','requests', 'order_index', 4, 'label_de','Anträge', 'label_en','Requests',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','facts', 'kind','prose', 'order_index', 5, 'label_de','Sachverhalt', 'label_en','Facts',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','legal_argument', 'kind','prose', 'order_index', 6, 'label_de','Rechtliche Würdigung', 'label_en','Legal argument',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','evidence', 'kind','evidence', 'order_index', 7, 'label_de','Beweisangebote', 'label_en','Evidence offering',
'included',true, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','exhibits', 'kind','prose', 'order_index', 8, 'label_de','Anlagen', 'label_en','Exhibits',
'included',false, 'seed_md_de', '', 'seed_md_en', ''),
jsonb_build_object('section_key','closing', 'kind','prose', 'order_index', 9, 'label_de','Schlussformel', 'label_en','Closing',
'included',true,
'seed_md_de', E'Mit freundlichen Grüßen',
'seed_md_en', E'Yours sincerely,'),
jsonb_build_object('section_key','signature', 'kind','prose', 'order_index',10, 'label_de','Unterschrift', 'label_en','Signature',
'included',true,
'seed_md_de', E'{{user.display_name}}',
'seed_md_en', E'{{user.display_name}}')
)
),
'{}'::text[]
)
ON CONFLICT (slug) DO NOTHING;