The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.
Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
(slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
users, mutations admin-only (handler-enforced, no RLS write paths).
Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
_skeleton.docx. Each section_spec carries the 10-section default
(letterhead, caption, introduction, requests, facts, legal_argument,
evidence, exhibits, closing, signature) with bilingual labels +
bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
composer_meta jsonb (default '{}'). Purely additive; pre-Composer
drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
section_key, order_index, kind ∈ {prose,requests,evidence},
label_de/en, included, content_md_de/en). RLS mirrors
submission_drafts (owner-scoped + can_see_project, four policies).
Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
the firm default base and seeds base_id + section rows in one tx.
Composer wiring is additive — when bases==nil the service stays
v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
on both project-scoped and global drafts accept "base_id".
Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
(hidden until catalog loads); section-list pane above the preview
(hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
paintBasePicker rebuilds <option> list on every paint; onBaseChange
PATCHes base_id and repaints; paintSectionList renders each section
read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
Pre-Composer drafts get the picker (catalog still loads) but the
section pane stays hidden until they pick a base on a new draft.
Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
(pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
seeded 10 default sections on both bases; GetDefaultForCode picks
hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
base_id + 10 section rows in tx with ascending order_index and
bilingual labels populated.
NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.
Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).
t-paliad-313
174 lines
12 KiB
SQL
174 lines
12 KiB
SQL
-- t-paliad-313 (m/paliad#141): Composer Slice A — submission base catalog.
|
|
--
|
|
-- paliad.submission_bases is a thin pointer table — each row maps a
|
|
-- short, stable slug ("hlc-letterhead", "neutral", …) onto a Gitea path
|
|
-- that holds the actual .docx body, plus a JSON section-spec describing
|
|
-- the base's default section set, stylemap, and per-section seed
|
|
-- Markdown. The .docx in Gitea stays the source of truth for the
|
|
-- chrome, fonts, paragraph styles, and (in later slices) the
|
|
-- {{#section:KEY}} anchors. The DB row carries the listable metadata
|
|
-- the picker needs.
|
|
--
|
|
-- Visibility: every authenticated user SELECTs (the catalog is shared
|
|
-- firm-wide). Mutations are admin-only and enforced in Go at the
|
|
-- handler layer — RLS only gates reads.
|
|
--
|
|
-- Slice A seeds two rows:
|
|
-- 1. hlc-letterhead — points at the existing HLC firm skeleton
|
|
-- (_firm-skeleton.docx with HL Patents Style typography).
|
|
-- 2. neutral — points at the universal _skeleton.docx.
|
|
-- Specialist bases (lg-duesseldorf, upc-formal) land in Slice E with
|
|
-- their own .docx authoring task.
|
|
|
|
CREATE TABLE IF NOT EXISTS paliad.submission_bases (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
slug text NOT NULL UNIQUE,
|
|
firm text,
|
|
proceeding_family text,
|
|
label_de text NOT NULL,
|
|
label_en text NOT NULL,
|
|
description_de text,
|
|
description_en text,
|
|
gitea_path text NOT NULL,
|
|
section_spec jsonb NOT NULL,
|
|
is_default_for text[] NOT NULL DEFAULT '{}'::text[],
|
|
is_active bool NOT NULL DEFAULT true,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS submission_bases_firm_family_idx
|
|
ON paliad.submission_bases (firm, proceeding_family) WHERE is_active;
|
|
|
|
ALTER TABLE paliad.submission_bases ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS submission_bases_select ON paliad.submission_bases;
|
|
CREATE POLICY submission_bases_select
|
|
ON paliad.submission_bases FOR SELECT TO authenticated
|
|
USING (true);
|
|
|
|
-- INSERT / UPDATE / DELETE intentionally absent — admin-only mutations
|
|
-- happen via the handler layer with explicit role checks. No RLS path
|
|
-- for mutations means RLS denies them by default.
|
|
|
|
DROP TRIGGER IF EXISTS submission_bases_set_updated_at ON paliad.submission_bases;
|
|
CREATE TRIGGER submission_bases_set_updated_at
|
|
BEFORE UPDATE ON paliad.submission_bases
|
|
FOR EACH ROW EXECUTE FUNCTION paliad.tg_set_updated_at();
|
|
|
|
COMMENT ON TABLE paliad.submission_bases IS
|
|
't-paliad-313: Composer base catalog. One row per base template (HLC letterhead, neutral, …) pointing at a .docx in Gitea + a JSON section spec.';
|
|
|
|
-- Seed: HLC letterhead + neutral skeleton. The section_spec carries the
|
|
-- 10 default sections (letterhead, caption, introduction, requests,
|
|
-- facts, legal_argument, evidence, exhibits, closing, signature) with
|
|
-- their kinds, default order, and bilingual labels. seed_md_de /
|
|
-- seed_md_en are populated for the bag-driven sections (letterhead,
|
|
-- caption, signature); the remaining sections seed empty.
|
|
--
|
|
-- exhibits.included=false by default (lawyer opts in when an attachment
|
|
-- list applies). Every other section ships included=true.
|
|
|
|
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
|
|
('hlc-letterhead', 'HLC', NULL,
|
|
'HLC-Briefkopf', 'HLC letterhead',
|
|
'Mit HL Patents Style — Firmen-Header, Schriftarten, Absatzformaten.',
|
|
'With HL Patents Style — firm header, fonts, paragraph styles.',
|
|
'6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx',
|
|
jsonb_build_object(
|
|
'version', 1,
|
|
'stylemap', jsonb_build_object(
|
|
'paragraph', 'HLpat-Body-B0',
|
|
'heading_1', 'HLpat-Heading-H1',
|
|
'heading_2', 'HLpat-Heading-H2',
|
|
'heading_3', 'HLpat-Heading-H3',
|
|
'list_bullet', 'HLpat-Body-B0',
|
|
'list_numbered', 'HLpat-Body-B0',
|
|
'blockquote', 'HLpat-Body-B1'
|
|
),
|
|
'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}}, {{user.office}}',
|
|
'seed_md_en', E'Submission by {{firm.name}}\n\n{{user.display_name}}, {{user.office}}'),
|
|
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}}**\nvertreten durch {{parties.claimant.0.representative}}\n\n— Klägerin —\n\ngegen\n\n**{{parties.defendant.0.name}}**\nvertreten durch {{parties.defendant.0.representative}}\n\n— Beklagte —\n\nAktenzeichen: {{project.case_number}}\n{{project.court}}',
|
|
'seed_md_en', E'In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n\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}}\n{{user.office}}',
|
|
'seed_md_en', E'{{user.display_name}}\n{{user.office}}')
|
|
)
|
|
),
|
|
'{}'::text[]
|
|
),
|
|
('neutral', NULL, NULL,
|
|
'Neutraler Schriftsatz', 'Neutral skeleton',
|
|
'Universelle Vorlage ohne firmenspezifisches Branding.',
|
|
'Universal template with no firm-specific branding.',
|
|
'6 - material/Templates/Word/Paliad/HLC/_skeleton.docx',
|
|
jsonb_build_object(
|
|
'version', 1,
|
|
'stylemap', jsonb_build_object(
|
|
'paragraph', 'Normal',
|
|
'heading_1', 'Heading 1',
|
|
'heading_2', 'Heading 2',
|
|
'heading_3', 'Heading 3',
|
|
'list_bullet', 'Normal',
|
|
'list_numbered', 'Normal',
|
|
'blockquote', '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}}',
|
|
'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}}'),
|
|
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;
|