docs: PRD/design — submission generator v2 ("Composer") (m/paliad#141)
Sectioned composition, swappable base templates, in-app prose editing,
building-blocks library. Deepens t-paliad-215 + t-paliad-238 without
replacing them — v1 contracts (submission_drafts shape, {{rule.X}}
aliases, audit shape) preserved.
7 slices A→G; Slice B is the smallest "Composer works" milestone.
Existing 11 v1 drafts continue via v1 path; opt-in upgrade per draft.
12 open design questions with recommended defaults + alternatives for m
to ratify via head escalation (no AskUserQuestion per task brief).
Flags two issue-body inaccuracies: no submission_drafts.audit_log column
(audit lives in system_audit_log + project_events); live row count is
11, not 7.
t-paliad-312
This commit is contained in:
963
docs/design-submission-generator-v2-2026-05-26.md
Normal file
963
docs/design-submission-generator-v2-2026-05-26.md
Normal file
@@ -0,0 +1,963 @@
|
||||
# Design — Submission generator v2 ("Composer")
|
||||
|
||||
**Author:** cronus (inventor)
|
||||
**Date:** 2026-05-26
|
||||
**Task:** t-paliad-312
|
||||
**Issue:** m/paliad#141
|
||||
**Branch:** `mai/cronus/inventor-prd-for`
|
||||
**Status:** DESIGN READY FOR REVIEW
|
||||
**Prior art:**
|
||||
- `docs/design-submission-generator-2026-05-19.md` (t-paliad-215, Slice 1: template-merge engine, fallback chain, audit shape)
|
||||
- `docs/design-submission-page-2026-05-22.md` (t-paliad-238, Slice A: dedicated draft editor at `/projects/{id}/submissions/{code}/draft`, sidebar + preview, autosave)
|
||||
|
||||
This design deepens — it does **not** replace — both prior docs. Every contract they locked in (placeholder grammar, `{{rule.X}}` aliases, audit-log shape, fallback chain, in-house renderer) is preserved. v2 adds sections, swappable bases, in-app prose editing, and building blocks **on top of** that substrate.
|
||||
|
||||
---
|
||||
|
||||
## §0 TL;DR
|
||||
|
||||
Today (v1): one Word `.docx` template per `submission_code`, picked from Gitea via a fallback chain, merged with a flat `{{placeholder}}` variable bag built from project + parties + deadline + user + firm state. The lawyer's only authoring surface is the sidebar — they fill in variables, see a read-only HTML preview, export to `.docx`, and finish in Word.
|
||||
|
||||
What m wants (v2 / "Composer"):
|
||||
|
||||
1. **Sections** — every submission is composed of named, independently-editable section blocks (`letterhead`, `caption`, `introduction`, `requests`, `facts`, `legal_argument`, `evidence`, `exhibits`, `closing`, `signature`). The lawyer can reorder, hide, and rename them.
|
||||
2. **Selectable base templates** — the lawyer picks the *base* (firm letterhead / neutral skeleton / UPC-formal / LG-Düsseldorf-style); the base supplies the document chrome, fonts, paragraph styles, and seeds the default section set. Lawyer can swap mid-draft without losing prose.
|
||||
3. **Editability of content** — inside paliad, the lawyer authors free prose per section (Markdown source-of-truth, contentEditable surface, minimal bold/italic/heading/list/quote toolbar). Variables `{{…}}` are still legal anywhere in section content.
|
||||
4. **Building blocks** — a library of reusable section-shaped chunks (standard Anträge phrasing, *Verspätungseinrede* boilerplate, *Auslandszustellung* evidence offering, …). Authored once, inserted into any compatible section across drafts, with private / team / firm / global visibility tiers. Copy-on-insert so the lawyer's edits never get clobbered; insertion records lineage for opt-in "refresh from library".
|
||||
|
||||
**Hard constraints honoured:**
|
||||
- The existing `submission_drafts` contract — `variables jsonb`, `selected_parties uuid[]`, `language`, `last_exported_*` columns, owner-scoped RLS — stays load-bearing. v1 drafts continue to render via the v1 path (template-only fallback) until the lawyer opts in to Composer for that draft.
|
||||
- The `{{rule.X}}` legacy aliases (still resolved by `SubmissionVarsService` alongside the canonical `{{procedural_event.X}}`) are honoured inside section content too.
|
||||
- The audit-log shape (`system_audit_log` event_type `submission.exported`, `project_events` event_type `submission_exported`) is preserved; v2 exports add `composer: true` to the metadata jsonb.
|
||||
- No new third-party Go dependency. The in-house renderer (from commit `8ea3509`) extends to MD → OOXML and section-anchor splicing.
|
||||
|
||||
**Slice plan:** seven slices A → G, each independently shippable. Slice A lands the base picker and a read-only section list (no behavior change). Slice B is the smallest "Composer actually works" milestone: editable prose sections, anchor-spliced render, MD → OOXML walker for paragraphs.
|
||||
|
||||
**No code in this doc.** Implementation gate is m's go/no-go through the head.
|
||||
|
||||
---
|
||||
|
||||
## §1 Premises verified live (2026-05-26)
|
||||
|
||||
Anchored against the running paliad codebase + youpc Supabase, not against CLAUDE.md or memory. Every claim that load-bears the design was checked against the live system.
|
||||
|
||||
| Claim | Verification |
|
||||
|---|---|
|
||||
| `paliad.submission_drafts` exists with the v1 contract. | `information_schema.columns` query: `id, project_id (nullable), submission_code, user_id, name, variables jsonb DEFAULT '{}', last_exported_at, last_exported_sha, selected_parties uuid[] DEFAULT '{}', last_imported_at, language text DEFAULT 'de', created_at, updated_at`. RLS on, four policies (select/insert/update/delete) gated on `paliad.can_see_project` + owner-scoped UPDATE/DELETE. |
|
||||
| There is **no** `submission_drafts.audit_log` column — the issue text is inaccurate on this point. | Same `information_schema` query: no column matching `%audit%`. Audit rows live in `paliad.system_audit_log` (event_type `submission.exported`) plus `paliad.project_events` (event_type `submission_exported`). This is the same shape t-paliad-238 §D4 locked in. **Issue #141 needs a one-line correction; flagged in §13.** |
|
||||
| 11 live draft rows on prod (the issue text said "7"). | `SELECT COUNT(*) FROM paliad.submission_drafts` → 11 rows across 1 user, 4 projects, 10 submission_codes. Mostly small variable maps (25–224 bytes JSONB). All language `de` except one `en`. Migration to v2 must keep these renderable without lawyer intervention. |
|
||||
| `{{rule.X}}` legacy aliases are alive and resolved. | `internal/services/submission_vars.go` resolves `procedural_event.code/name/name_de/name_en/legal_source/legal_source_pretty/primary_party/event_kind` as canonical; the `rule.*` aliases (`rule.submission_code`, `rule.name`, `rule.name_en`, `rule.legal_source`, `rule.legal_source_pretty`, `rule.primary_party`, `rule.event_type`) resolve to identical values per `submission_vars_aliases_test.go` (~85 LoC of golden tests). Any v2 surface that touches placeholder text must preserve this. |
|
||||
| In-house renderer handles single-run AND cross-run placeholder fragmentation. | `internal/services/submission_merge.go`, ~530 LoC. Two-pass: text-node replace first (preserves run-level formatting); paragraph-merge fallback when `{{` / `}}` survives pass 1 (handles Word's autocorrect-split placeholders). `lukasjarosch/go-docx` was explicitly rejected (refuses sibling placeholders inside one run). v2 reuses this renderer wholesale. |
|
||||
| Templates resolve via a five-tier fallback chain. | `internal/handlers/submission_drafts.go` `resolveSubmissionTemplate`: `per_code_lang` (`templates/<FIRM>/<code>.<lang>.docx`) → `per_code` → `skeleton_lang` (`_skeleton.<lang>.docx`) → `skeleton` (`_firm-skeleton.docx`, `_skeleton.docx`) → `letterhead` (HL Patents Style `.dotm`). v2's base concept slots in at the `_firm-skeleton` / `_skeleton` tier but moves the section-anchor wiring into the base body. |
|
||||
| Templates live in Gitea at `HL/mWorkRepo:6 - material/Templates/Word/Paliad/<FIRM>/...`. | `internal/handlers/files.go` Gitea proxy, in-process SHA cache, 5-min refresh. `_firm-skeleton.docx` carries 168 HL paragraph styles (memory `01cdd589`); `_skeleton.docx` carries a universal layout. Same proxy pattern reused for bases + building blocks if they need .docx-backed content. |
|
||||
| Frontend draft editor exists at `/projects/{id}/submissions/{code}/draft`. | `frontend/src/submission-draft.tsx` (231 LoC) + `frontend/src/client/submission-draft.ts` (1913 LoC) — sidebar with variable groups (Mandant & Verfahren, Parteien, Frist, Sonstiges), language toggle (DE/EN), party multi-select, "Aus Projekt importieren", read-only preview pane, autosave. v2 grafts new sections + base picker into the same shell. |
|
||||
| Migration tracker is at **106 in the live DB**; worktree filesystem has migration files up to **145**. | `SELECT version FROM paliad.paliad_schema_migrations ORDER BY version DESC LIMIT 1` → 106. `ls internal/db/migrations/` → newest is `145_scenarios.up.sql`. Multiple branches have queued migrations (e.g. 119–145). Next free slot for v2 work on this branch is **146**; coder must re-check at implementation time in case other branches commit first. |
|
||||
| `paliad.can_see_project(uuid)` is the canonical RLS predicate. | Mig 055; used by every project-scoped table. v2's new tables (`submission_bases`, `submission_sections`, `submission_building_blocks` + `_versions`) all gate on it. |
|
||||
| `internal/branding.Name` defaults to "HLC", overridable via `FIRM_NAME`. | Plumbing reused for per-firm bases + per-firm building blocks (visibility tier "firm"). |
|
||||
| The `entity-table` row contract is enforced. | `.claude/CLAUDE.md` → "Whole-card / whole-row click → use a JS row handler, not a `::before` overlay". Composer's section list (when rendered as cards/rows) follows this. |
|
||||
|
||||
**Doc-vs-live conflicts found:**
|
||||
- Issue #141 mentions `submission_drafts.audit_log` and "7 live rows". The live table has no `audit_log` column (audit goes to `system_audit_log` + `project_events`); live row count is 11. Issue body should be corrected post-design-approval; the design does **not** depend on either claim being true.
|
||||
|
||||
---
|
||||
|
||||
## §2 m's headline requirements → design surface
|
||||
|
||||
| Issue text | Design surface |
|
||||
|---|---|
|
||||
| "different sections" | §4 data model (`submission_sections` table) + §6 default section set per `submission_code` + Slices B / G. |
|
||||
| "selectable 'base templates'" | §5 base picker, `submission_bases` table with Gitea-backed body + section-default spec + Slice A / E. |
|
||||
| "editability of content" | §7 editor UX — contentEditable per section with minimal toolbar, Markdown source-of-truth + Slice B. |
|
||||
| "building blocks" | §8 `submission_building_blocks` table + library admin + section picker + Slice C. |
|
||||
|
||||
The four are interlocking but ship in independent slices.
|
||||
|
||||
---
|
||||
|
||||
## §3 Naming & vocabulary
|
||||
|
||||
For clarity throughout this doc and downstream code:
|
||||
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **Composer** | The v2 submission-authoring experience (the editor page + render pipeline). NOT a separate product or feature flag — the existing `/projects/{id}/submissions/{code}/draft` URL stays; the editor evolves. |
|
||||
| **Base** | A `.docx` chrome supplying letterhead, paragraph styles, font families, header/footer, and the default section spec for a draft. Replaces the loose "skeleton" terminology in v1's fallback chain. |
|
||||
| **Section** | A named, ordered, lawyer-editable block of content within a draft (e.g. `requests`, `facts`). Stored as one row per (draft, section_key) in `submission_sections`. |
|
||||
| **Building block** | A reusable, library-scoped chunk of section-shaped content. Inserted into a section copies its Markdown into the section; the section row retains `building_block_id` lineage. |
|
||||
| **Variable bag** | The flat `{{path.dot.notation}} → string` map built by `SubmissionVarsService`. Unchanged from v1. |
|
||||
| **Composer override** | The contents of `submission_drafts.variables jsonb` — lawyer's per-draft variable overrides. Unchanged from v1. |
|
||||
| **Section anchor** | A pair of tokens `{{#section:KEY}} … {{/section:KEY}}` inside the base body that the render pipeline replaces with rendered section content. |
|
||||
| **Section spec** | A JSON document on each base declaring (a) the default section set, (b) per-section default order, (c) per-section style anchors, (d) which Markdown constructs map to which paragraph styles. |
|
||||
|
||||
All identifiers in code follow the project's English convention (per project CLAUDE.md). User-facing labels are bilingual (DE primary, EN secondary).
|
||||
|
||||
---
|
||||
|
||||
## §4 Data model
|
||||
|
||||
### §4.1 Existing — preserved unchanged
|
||||
|
||||
`paliad.submission_drafts` keeps every column from v1. v2 adds two nullable columns:
|
||||
|
||||
| Column | Type | Nullable | Purpose |
|
||||
|---|---|---|---|
|
||||
| `base_id` | uuid | YES (FK → `submission_bases.id`) | Which base the draft renders against. NULL = legacy / v1 render path (fall back to `resolveSubmissionTemplate` chain). |
|
||||
| `composer_meta` | jsonb | NO, DEFAULT `'{}'::jsonb` | Composer-specific metadata: `{"section_order": ["caption","requests","facts",…], "hidden_sections": ["evidence"], "active_locale": "de"}`. Order/hidden tracked here so a single fast read paints the editor without joining sections. |
|
||||
|
||||
Both additive; existing 11 rows get `base_id = NULL` (interpreted: not yet upgraded → render via v1 fallback chain) and an empty `composer_meta`. The migration is purely additive; no row rewrites.
|
||||
|
||||
The existing `variables jsonb` column still holds the lawyer's per-draft variable overrides. The `language` column still drives the active locale. `selected_parties uuid[]` still drives party scoping. Nothing about v1 is removed.
|
||||
|
||||
### §4.2 New — `submission_bases`
|
||||
|
||||
```sql
|
||||
CREATE TABLE paliad.submission_bases (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug text NOT NULL UNIQUE, -- 'hlc-letterhead', 'neutral', 'upc-formal', 'lg-duesseldorf'
|
||||
firm text, -- 'HLC' or NULL (global)
|
||||
proceeding_family text, -- 'de.inf.lg', 'upc.inf.cfi', or NULL (any)
|
||||
label_de text NOT NULL,
|
||||
label_en text NOT NULL,
|
||||
description_de text,
|
||||
description_en text,
|
||||
gitea_path text NOT NULL, -- 'Templates/Word/Paliad/HLC/_firm-skeleton.docx'
|
||||
section_spec jsonb NOT NULL, -- §6.1 section-spec JSON
|
||||
is_default_for text[] NOT NULL DEFAULT '{}', -- submission_codes this base seeds by default
|
||||
is_active bool NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX submission_bases_firm_family_idx
|
||||
ON paliad.submission_bases (firm, proceeding_family) WHERE is_active;
|
||||
|
||||
ALTER TABLE paliad.submission_bases ENABLE ROW LEVEL SECURITY;
|
||||
-- All authenticated users SELECT; mutations admin-only (see §10).
|
||||
```
|
||||
|
||||
Each base is a thin pointer at a `.docx` in Gitea plus the section-spec JSON. The actual body XML, paragraph styles, fonts, and section anchors live in the `.docx`; the table gives us listable, RLS-aware metadata, and a stable id for `submission_drafts.base_id` to reference.
|
||||
|
||||
Seed rows for v2.0:
|
||||
|
||||
| slug | firm | proceeding_family | label_de | label_en |
|
||||
|---|---|---|---|---|
|
||||
| `hlc-letterhead` | HLC | (any) | HLC-Briefkopf | HLC letterhead |
|
||||
| `neutral` | (null) | (any) | Neutraler Schriftsatz | Neutral skeleton |
|
||||
| `lg-duesseldorf` | (null) | `de.inf.lg` | LG-Düsseldorf-Stil | LG-Düsseldorf style |
|
||||
| `upc-formal` | (null) | `upc.inf.cfi` | UPC-Verfahren | UPC formal |
|
||||
|
||||
The Gitea `_firm-skeleton.docx` becomes the body for `hlc-letterhead`; the universal `_skeleton.docx` becomes `neutral`. The two specialist bases are new authoring work for Slice E.
|
||||
|
||||
### §4.3 New — `submission_sections`
|
||||
|
||||
```sql
|
||||
CREATE TABLE paliad.submission_sections (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
draft_id uuid NOT NULL REFERENCES paliad.submission_drafts(id) ON DELETE CASCADE,
|
||||
section_key text NOT NULL, -- 'requests', 'facts', 'legal_argument', …
|
||||
order_index int NOT NULL, -- lawyer-orderable
|
||||
kind text NOT NULL, -- 'prose' | 'requests' | 'evidence' | 'caption_auto' | 'letterhead_auto' | 'signature_auto'
|
||||
label_de text NOT NULL, -- lawyer-renameable
|
||||
label_en text NOT NULL,
|
||||
included bool NOT NULL DEFAULT true,
|
||||
content_md_de text NOT NULL DEFAULT '', -- Markdown source; empty for *_auto kinds
|
||||
content_md_en text NOT NULL DEFAULT '',
|
||||
building_block_id uuid REFERENCES paliad.submission_building_blocks(id) ON DELETE SET NULL,
|
||||
building_block_version int, -- snapshot version at insert time
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
UNIQUE (draft_id, section_key)
|
||||
);
|
||||
|
||||
CREATE INDEX submission_sections_draft_idx
|
||||
ON paliad.submission_sections (draft_id, order_index);
|
||||
|
||||
ALTER TABLE paliad.submission_sections ENABLE ROW LEVEL SECURITY;
|
||||
-- Visibility flows through draft_id → submission_drafts → can_see_project + owner-scoped.
|
||||
```
|
||||
|
||||
`section_key` is a stable identifier (English slug) the render pipeline keys off. `kind` distinguishes free-prose sections (`prose`, `requests`, `evidence`) from sections rendered automatically from the variable bag (`*_auto`) — those can be hidden but not edited as text. `label_de` / `label_en` start from the section spec defaults but can be renamed per draft.
|
||||
|
||||
`content_md_de` + `content_md_en` are both columns from the start (Slice F is not deferred — see §11 Q7) so a draft is bilingual-by-construction; the active rendering picks the column matching `submission_drafts.language`.
|
||||
|
||||
`building_block_id` retains lineage but the row's content is the snapshot at insertion time (§8.3 copy-on-insert).
|
||||
|
||||
### §4.4 New — `submission_building_blocks` + `_versions`
|
||||
|
||||
```sql
|
||||
CREATE TABLE paliad.submission_building_blocks (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug text NOT NULL,
|
||||
firm text, -- 'HLC' or NULL (global)
|
||||
section_key text NOT NULL, -- which section kind this block fits
|
||||
proceeding_family text, -- 'de.inf.lg', NULL = any
|
||||
title_de text NOT NULL,
|
||||
title_en text NOT NULL,
|
||||
description_de text,
|
||||
description_en text,
|
||||
content_md_de text NOT NULL DEFAULT '',
|
||||
content_md_en text NOT NULL DEFAULT '',
|
||||
author_id uuid REFERENCES paliad.users(id),
|
||||
visibility text NOT NULL, -- 'private' | 'team' | 'firm' | 'global'
|
||||
is_published bool NOT NULL DEFAULT false,
|
||||
version int NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
|
||||
UNIQUE (slug, firm)
|
||||
);
|
||||
|
||||
CREATE TABLE paliad.submission_building_block_versions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
building_block_id uuid NOT NULL REFERENCES paliad.submission_building_blocks(id) ON DELETE CASCADE,
|
||||
version int NOT NULL,
|
||||
content_md_de text NOT NULL,
|
||||
content_md_en text NOT NULL,
|
||||
edited_by uuid REFERENCES paliad.users(id),
|
||||
note text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
UNIQUE (building_block_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX submission_building_blocks_section_visibility_idx
|
||||
ON paliad.submission_building_blocks (section_key, visibility, firm, proceeding_family)
|
||||
WHERE deleted_at IS NULL AND is_published;
|
||||
```
|
||||
|
||||
Visibility tiers (mirror the team-visibility model elsewhere in paliad):
|
||||
|
||||
- `private` — visible only to `author_id`.
|
||||
- `team` — visible to every member of any team the author belongs to that touches the project context (resolved via the standard team-visibility helpers).
|
||||
- `firm` — visible to every user whose `email` ends in any of `ALLOWED_EMAIL_DOMAINS` and whose `branding.Name` matches the block's `firm`.
|
||||
- `global` — visible to every authenticated user.
|
||||
|
||||
Versioning: same retention-20 policy as email templates (`internal/services/email_template_service.go`). Each Save appends a `_versions` row + GCs to 20 in-transaction.
|
||||
|
||||
### §4.5 Migration count and order
|
||||
|
||||
One migration per concern (atomic, drift-safe), suggested in this order — coder confirms slot numbers at impl time:
|
||||
|
||||
| Slot | Purpose |
|
||||
|---|---|
|
||||
| 146 | `ALTER TABLE submission_drafts ADD COLUMN base_id uuid REFERENCES submission_bases(id); ADD COLUMN composer_meta jsonb NOT NULL DEFAULT '{}'::jsonb;` (must come after `submission_bases` exists — alternative: bundle 146+147) |
|
||||
| 147 | `CREATE TABLE submission_bases` + RLS + seed rows for `hlc-letterhead`, `neutral`. (Specialist bases land in Slice E migrations.) |
|
||||
| 148 | `CREATE TABLE submission_sections` + RLS + indexes. |
|
||||
| 149 | `CREATE TABLE submission_building_blocks` + `_versions` + RLS + indexes. |
|
||||
|
||||
The 146 column add must reference the bases table — practically, 147 (create bases) ships first and 146 becomes the next slot. Slot numbers are flexible; the coder picks contiguous free slots at impl time.
|
||||
|
||||
---
|
||||
|
||||
## §5 Bases — selection, swap, default
|
||||
|
||||
### §5.1 What a base supplies
|
||||
|
||||
A base provides:
|
||||
|
||||
1. **Document chrome** — the `.docx`'s headers, footers, page setup, font tables, paragraph styles. From the Gitea body.
|
||||
2. **Section anchors** — `{{#section:KEY}} … {{/section:KEY}}` pairs in the body, declaring where each section's rendered content goes. From the Gitea body.
|
||||
3. **Section spec** — JSON on the base row declaring the default section set, ordering, kinds, labels, and the stylemap (which Markdown construct maps to which paragraph style in this base). From the DB.
|
||||
4. **Variable bag context** — the base inherits the same `SubmissionVarsService` placeholder set as v1. `{{firm.name}}`, `{{user.display_name}}`, etc. work unchanged.
|
||||
|
||||
### §5.2 Picking a base on draft create
|
||||
|
||||
On `POST /api/projects/{id}/submissions/{code}/drafts`:
|
||||
|
||||
1. Compute the proceeding family (`de.inf.lg` from `de.inf.lg.erwidg`).
|
||||
2. Look up the firm's default base for this family: `SELECT … FROM submission_bases WHERE firm=$FIRM AND is_active AND $code = ANY(is_default_for) ORDER BY priority` (priority resolved by spec — see §11 Q3 alternatives).
|
||||
3. Fall back: firm + family → firm + any-family → global + family → global + any → first active row.
|
||||
4. Seed `submission_drafts.base_id` with the picked base.
|
||||
5. Seed `submission_sections` rows from the base's `section_spec.defaults` array.
|
||||
|
||||
The lawyer can change the base via the sidebar picker; on swap the section content rows are preserved (only the base.docx body and styles change at render time). See §5.3.
|
||||
|
||||
### §5.3 Swapping a base mid-draft
|
||||
|
||||
Hard rule: **section content is base-agnostic.** Markdown source carries no inline style references; lists, headings, bold/italic are semantic tokens. The MD → OOXML walker applies the *current base's* stylemap at render time. Swap is safe by construction.
|
||||
|
||||
What DOES change on swap:
|
||||
|
||||
- Default section set: if the new base spec defaults to a different ordering or includes sections the old base lacked, the lawyer is offered a non-destructive "Apply base's default section order? (keeps existing content)" prompt. Default answer: NO.
|
||||
- `composer_meta.section_order` honoured if set; else `base.section_spec.defaults` honoured.
|
||||
- Sections that exist in the draft but not the new base's spec stay visible but get an "ungrounded" badge so the lawyer notices.
|
||||
- Sections in the new base's spec but not in the draft: they're created lazily on first focus, empty content.
|
||||
|
||||
### §5.4 Authoring a new base
|
||||
|
||||
Each base is one `.docx` in Gitea plus one row in `submission_bases`. Authoring:
|
||||
|
||||
1. Lawyer (or admin) creates the `.docx` in Word with HL Patents Style applied. Inserts section anchors as plain text `{{#section:KEY}}` / `{{/section:KEY}}` in the body where each section should go.
|
||||
2. Generator script (`scripts/gen-submission-base/main.go`, modelled on `scripts/gen-hl-skeleton-template/main.go` from t-paliad-275) strips macros, validates anchors, embeds default section spec into the file as a custom XML part.
|
||||
3. Admin uploads to Gitea via the contents API (as mAi via `--netrc-file ~/.netrc-mai`).
|
||||
4. Admin inserts the `submission_bases` row via the new `/admin/submission-bases` editor (Slice E).
|
||||
|
||||
This mirrors the t-paliad-275 workflow that already shipped `_firm-skeleton.docx`. No new tooling beyond a slightly extended generator script.
|
||||
|
||||
---
|
||||
|
||||
## §6 Sections — the default set
|
||||
|
||||
### §6.1 Section spec on a base
|
||||
|
||||
JSON document stored on each `submission_bases.section_spec` row. Shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"stylemap": {
|
||||
"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": [
|
||||
{"section_key": "letterhead", "kind": "letterhead_auto", "order_index": 1, "label_de": "Briefkopf", "label_en": "Letterhead", "included": true},
|
||||
{"section_key": "caption", "kind": "caption_auto", "order_index": 2, "label_de": "Rubrum", "label_en": "Caption", "included": true},
|
||||
{"section_key": "introduction", "kind": "prose", "order_index": 3, "label_de": "Einleitung", "label_en": "Introduction", "included": true},
|
||||
{"section_key": "requests", "kind": "requests", "order_index": 4, "label_de": "Anträge", "label_en": "Requests", "included": true},
|
||||
{"section_key": "facts", "kind": "prose", "order_index": 5, "label_de": "Sachverhalt", "label_en": "Facts", "included": true},
|
||||
{"section_key": "legal_argument", "kind": "prose", "order_index": 6, "label_de": "Rechtliche Würdigung", "label_en": "Legal argument", "included": true},
|
||||
{"section_key": "evidence", "kind": "evidence", "order_index": 7, "label_de": "Beweisangebote", "label_en": "Evidence offering", "included": true},
|
||||
{"section_key": "exhibits", "kind": "prose", "order_index": 8, "label_de": "Anlagen", "label_en": "Exhibits", "included": false},
|
||||
{"section_key": "closing", "kind": "prose", "order_index": 9, "label_de": "Schlussformel", "label_en": "Closing", "included": true},
|
||||
{"section_key": "signature", "kind": "signature_auto", "order_index": 10, "label_de": "Unterschrift", "label_en": "Signature", "included": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `stylemap` is per-base because different bases use different paragraph style names (HLpat-* on the HLC base, generic Word styles on the neutral base). The default section list is a starting point; per-submission_code overrides land in §6.2.
|
||||
|
||||
### §6.2 Section default overrides per submission_code
|
||||
|
||||
Some submission codes naturally invert the section order (e.g. `upc.inf.cfi.sod` — Statement of Defence — puts requests last; `de.inf.lg.berufung` has a "Berufungsanträge" before facts, etc.). A small Go-side override table maps `submission_code → (section adjustments)`:
|
||||
|
||||
```go
|
||||
// internal/services/submission_section_defaults.go (NEW, Slice B)
|
||||
var sectionOverrides = map[string][]sectionAdjustment{
|
||||
"upc.inf.cfi.sod": {
|
||||
{SectionKey: "requests", OrderIndex: 9}, // last
|
||||
},
|
||||
"de.inf.lg.berufung": {
|
||||
{SectionKey: "requests", OrderIndex: 4}, // before facts
|
||||
{SectionKey: "berufungsantraege", Insert: true, OrderIndex: 4, Kind: "requests", LabelDE: "Berufungsanträge", LabelEN: "Appeal requests"},
|
||||
},
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
This is intentionally code-driven (not DB-driven) for v2.0 (see §11 Q8). The override applies once at draft creation; subsequent lawyer reorders win.
|
||||
|
||||
### §6.3 `*_auto` sections — content comes from the bag, not the editor
|
||||
|
||||
Three section kinds render entirely from the base + variable bag, with no user-editable content body:
|
||||
|
||||
- **`letterhead_auto`** — typically a `.docx` header part (HL Patents Style header1.xml) is the actual letterhead; the in-body section is either empty or a small "Schriftsatz von:" preamble using `{{firm.name}}`, `{{user.display_name}}`, etc. The lawyer toggles include/exclude but doesn't type text here.
|
||||
- **`caption_auto`** — the "Rubrum" block. Renders from `{{parties.claimant.0.name}}`, `{{parties.defendant.0.name}}`, `{{project.case_number}}`, `{{project.court}}` etc. via a fixed micro-template embedded in the base (paragraphs using HLpat-Table-Recitals-Party styles). Lawyer toggles include/exclude.
|
||||
- **`signature_auto`** — same shape. Uses `{{firm.signature_block}}` + `{{user.display_name}}`.
|
||||
|
||||
The base's section anchor for each `*_auto` section contains the OOXML for the canonical block. When the section is `included=false`, the entire `{{#section:KEY}} … {{/section:KEY}}` slot is dropped at render time.
|
||||
|
||||
### §6.4 Custom sections
|
||||
|
||||
Slice G adds "+ Abschnitt hinzufügen". UX:
|
||||
|
||||
- A picker shows existing slugs the base spec knows about that aren't yet in this draft, plus a "Neue Abschnitts-Slug" form (lawyer types `slug`, picks kind from `prose`/`requests`/`evidence`, default labels).
|
||||
- Inserting writes a new `submission_sections` row.
|
||||
- The render pipeline expects the base to declare an anchor for every section key. **Custom slugs without a matching anchor in the base** render after the last anchored section, before the signature, as plain paragraphs using the base's `paragraph` stylemap entry — the "trailing zone" semantic.
|
||||
|
||||
This keeps custom sections rendering predictably without requiring lawyer edits to the base `.docx`.
|
||||
|
||||
### §6.5 Reorder / hide
|
||||
|
||||
- Reorder: drag-and-drop in the editor; PATCH writes the new `order_index` array AND mirrors it into `composer_meta.section_order` for fast paint.
|
||||
- Hide: per-section "Nicht aufnehmen" toggle flips `included` on the row.
|
||||
- Both safe to round-trip; the base's anchor slots are stable, only the content (or absence of content) changes.
|
||||
|
||||
---
|
||||
|
||||
## §7 Editor UX
|
||||
|
||||
### §7.1 Page shell (unchanged)
|
||||
|
||||
`/projects/{id}/submissions/{code}/draft` and `…/draft/{draftID}`. Sidebar + main body grid (the existing pattern in `frontend/src/submission-draft.tsx`). Sidebar carries: switcher, name, language toggle, save status, import-from-project, party picker, variable groups (Mandant/Verfahren/Sonstiges).
|
||||
|
||||
### §7.2 What's new in the body
|
||||
|
||||
The main body grows from a single read-only preview pane to a **section stack**:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Composer │
|
||||
│ ────────────────────────────────────────────────── │
|
||||
│ Basis: [HLC-Briefkopf ▾] [Basis wechseln…] │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 1. Briefkopf [Auto] [☐ verstecken] │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 2. Rubrum [Auto] [☐ verstecken] │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 3. Einleitung [+ Baustein] [⋮] │ │
|
||||
│ │ ┌────────────────────────────────────────────┐ │ │
|
||||
│ │ │ [B] [I] [H₁] [H₂] [• ] [1.] [❝] │ │ │
|
||||
│ │ ├────────────────────────────────────────────┤ │ │
|
||||
│ │ │ Namens und in Vollmacht der Klägerin │ │ │
|
||||
│ │ │ {{parties.claimant.0.name}} legen wir │ │ │
|
||||
│ │ │ folgende Klageschrift… │ │ │
|
||||
│ │ └────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 4. Anträge [+ Baustein] [⋮] │ │
|
||||
│ │ … │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ … │
|
||||
│ │
|
||||
│ [Vorschau ▾] ← collapsible read-only preview │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Per-section affordances:
|
||||
- **`[B] [I] [H₁] [H₂] [• ] [1.] [❝]`** — a thin shared toolbar that activates on focus of any contentEditable region. One toolbar instance hops between sections.
|
||||
- **`{{var}}` autocomplete** — typing `{{` opens a picker filtered by the resolved bag's keys. Inserts as plain text.
|
||||
- **`+ Baustein einfügen`** — opens the building-block picker filtered to this section's `section_key` + project context. Insertion appends the block's Markdown to current cursor position.
|
||||
- **`⋮` per-section menu** — Rename, Hide/Show, Move up/down, Reset to base default, Remove (custom sections only).
|
||||
- **Read-only preview** is collapsed by default in v2 (the in-place editor is the primary surface); a `[Vorschau ▾]` toggle shows the merged HTML beneath the section stack as a fidelity guide. Same renderer as v1.
|
||||
|
||||
### §7.3 Editor surface — contentEditable + Markdown serialization
|
||||
|
||||
- Each section's contentEditable region is wired to a tiny TS shim (`frontend/src/client/composer-md.ts`, target ~250 LoC) that:
|
||||
- On keystroke, debounces 500ms, serialises the DOM to Markdown via a deterministic walker (headings, paragraphs, lists, blockquote, bold/italic, links — no images, no tables in v2.0).
|
||||
- PATCH-saves `content_md_<lang>` to `/api/.../sections/{section_id}`.
|
||||
- On focus, re-renders Markdown → DOM (idempotent on round-trip).
|
||||
- The Markdown serializer is the source of truth. The DOM is a view. This guarantees swap-base survives content.
|
||||
- A `[Quelltext anzeigen]` toggle exposes the Markdown source in a `<textarea>` for lawyers who prefer typing raw.
|
||||
|
||||
**Why not Tiptap / ProseMirror:**
|
||||
- ~80kB bundle add per editor instance; 8 sections per page means rendering cost.
|
||||
- Paliad's frontend ethos is HTML-first, custom JSX renderer, no React. Adding ProseMirror pulls a heavy dep.
|
||||
- Lawyers already accept "final formatting in Word"; the editor's job is structural + first-draft prose, not WYSIWYG fidelity.
|
||||
|
||||
Slice D extends the toolbar; Slice B ships with bold/italic/headings/bullet-list/quote only (the minimum that exercises every stylemap entry).
|
||||
|
||||
### §7.4 Variable insertion
|
||||
|
||||
The sidebar's existing `paintVariables` flow stays unchanged — it renders one input per placeholder for the lawyer's overrides on flat values (`{{project.case_number}}`, `{{parties.claimant.0.name}}`, etc.).
|
||||
|
||||
In section content, the lawyer types `{{` and an inline picker drops below the cursor showing the resolved-bag keys + their current values. Selecting one inserts the literal `{{path.dot.notation}}` text. No expansion — the text stays as the placeholder until render.
|
||||
|
||||
### §7.5 Per-section building-block insertion
|
||||
|
||||
`+ Baustein einfügen` button → modal listing visible blocks filtered to `section_key = <this section's key>` AND `proceeding_family ∈ {project family, NULL}` AND visibility within reach. Each row shows title, description, first 200 chars of `content_md_<lang>`. Click → block's full `content_md_<lang>` is appended to current cursor position; section row gets `building_block_id` + `building_block_version` stamped.
|
||||
|
||||
If the lawyer already has content in the section, the insert is appended with a `\n\n` separator. The lawyer can drag/edit afterwards.
|
||||
|
||||
A small badge in the section header — "Bausteinquelle: <block title> (Version <N>)" — surfaces lineage. Clicking the badge opens a one-step "Mit Bibliotheksversion abgleichen" diff view (Slice C+).
|
||||
|
||||
### §7.6 Autosave
|
||||
|
||||
Per-keystroke debounce 500ms (matches v1). PATCH `/api/.../drafts/{id}/sections/{section_id}` with `{content_md_de | content_md_en | order_index | included | label_de | label_en}`. No optimistic locking — drafts are owner-scoped per row, no multi-user co-editing.
|
||||
|
||||
The existing draft-level autosave (variables, selected_parties, name, language) is independent and unchanged.
|
||||
|
||||
---
|
||||
|
||||
## §8 Building blocks
|
||||
|
||||
### §8.1 Library structure
|
||||
|
||||
One row per block in `submission_building_blocks`. Each block is bound to one `section_key` (`requests` blocks can't be inserted into `facts` sections, etc.). The block carries both DE and EN content from the start.
|
||||
|
||||
### §8.2 Authoring surface
|
||||
|
||||
New admin page `/admin/submission-blocks` (mirrors `/admin/email-templates` shape):
|
||||
|
||||
- Left pane: list of blocks scoped to user's reach + a "Neuer Baustein" button.
|
||||
- Centre pane: editor with `slug`, `firm`, `section_key`, `proceeding_family`, titles, descriptions, the `content_md_de` + `content_md_en` textareas. Visibility radio. "Speichern" / "Speichern und veröffentlichen" buttons. Append-only versions list with restore.
|
||||
- Right pane: live render preview using `SubmissionRenderer.RenderHTML` against a sample bag.
|
||||
|
||||
Non-admin users get `/blocks` (personal) — same UI but filtered to `visibility = 'private'` and their authored rows. Private → published bumps visibility tier; tier upgrade routes the block past an admin moderation step if going `firm` or `global`.
|
||||
|
||||
### §8.3 Copy-on-insert + lineage
|
||||
|
||||
Insert mechanics:
|
||||
|
||||
1. Render Markdown copy of block's `content_md_<active_lang>` into `submission_sections.content_md_<lang>` at the cursor offset.
|
||||
2. Stamp `submission_sections.building_block_id = <block.id>` and `building_block_version = <block.version>`.
|
||||
3. If the section already had a `building_block_id` pinned: do NOT override the lineage — instead, append the new block's content with a `\n\n---\n\n` separator and clear the lineage stamp (mixed origin).
|
||||
|
||||
Rationale: the lawyer's edits never get clobbered by library updates. Lineage stays so we can show "this was inserted from BB-Antrag-Standardfassung v3 — Library is now at v5, [refresh?]".
|
||||
|
||||
### §8.4 Refresh-from-library
|
||||
|
||||
A per-section button "Aus Bibliothek aktualisieren" (only shown when `building_block_id` is set AND the library version exceeds the snapshot version). Click flow:
|
||||
|
||||
1. Render a diff view (text-level) of "current section content" vs "library version N+M content_md_<lang>".
|
||||
2. Lawyer picks: (a) keep current, (b) overwrite with library version, (c) merge — opens a side-by-side editor with conflict markers.
|
||||
3. On overwrite/merge: bump `building_block_version` to current.
|
||||
|
||||
Defaults to "off" — no automatic propagation. The lawyer's call always.
|
||||
|
||||
### §8.5 Visibility tiers — concrete examples
|
||||
|
||||
- **Private** "MS-Klageerhebung-Variante" — Matthias's draft prose for his style; nobody else sees it.
|
||||
- **Team** "TeamPatentLitMUC-Klageerwiderung-Lieblingsphrase" — the Munich patent team's preferred opener; team members see it.
|
||||
- **Firm** "HLC-Standardantrag-§43" — the firm's vetted Anträge boilerplate; every HLC user sees it.
|
||||
- **Global** "Universal-Beweisangebot-Auslandszustellung" — open library curated cross-firm; everyone sees it.
|
||||
|
||||
The moderation step (tier upgrade to `firm` or `global`) is a content-review responsibility — admin review required, audit-logged.
|
||||
|
||||
---
|
||||
|
||||
## §9 Render pipeline
|
||||
|
||||
### §9.1 High-level
|
||||
|
||||
```
|
||||
1. Load draft + sections + base.
|
||||
2. Fetch base.docx bytes from Gitea (cached per SHA, 5-min refresh).
|
||||
3. Build variable bag via SubmissionVarsService (unchanged from v1).
|
||||
4. Apply lawyer's variables overrides on top (unchanged).
|
||||
5. For each section in order_index order, with included=true:
|
||||
a. If kind ∈ {prose, requests, evidence}: render content_md_<lang> → OOXML
|
||||
using base.section_spec.stylemap.
|
||||
b. If kind = *_auto: pass through the base's anchor slot's inline OOXML.
|
||||
6. Splice rendered OOXML into base body at matching {{#section:KEY}} … {{/section:KEY}} anchors.
|
||||
7. Sections with included=false: drop the entire {{#section:KEY}} … {{/section:KEY}} slot.
|
||||
8. Custom sections without anchors: render to base.stylemap.paragraph and append after the last anchored section, before {{#section:signature}}.
|
||||
9. Pass the assembled XML through the v1 placeholder-substitution (single-run + cross-run merge). {{rule.X}} aliases resolved identically.
|
||||
10. Strip leftover unfilled section anchors.
|
||||
11. ConvertDotmToDocx pre-pass (existing behaviour; idempotent on .docx).
|
||||
12. Stream as application/vnd.openxmlformats-officedocument.wordprocessingml.document.
|
||||
```
|
||||
|
||||
Step 9 reuses the existing `internal/services/submission_merge.go` walker untouched. Steps 1–8 are new (`internal/services/submission_compose.go`, target ~400 LoC). Step 10 is a small regex post-pass.
|
||||
|
||||
### §9.2 MD → OOXML walker (Slice B, extended in Slice D)
|
||||
|
||||
Subset supported in v2.0:
|
||||
|
||||
| Markdown construct | OOXML output | Stylemap key |
|
||||
|---|---|---|
|
||||
| `# Heading` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.heading_1>"/></w:pPr><w:r><w:t>…</w:t></w:r></w:p>` | `heading_1` |
|
||||
| `## Heading` | same with `heading_2` | `heading_2` |
|
||||
| `### Heading` | same with `heading_3` | `heading_3` |
|
||||
| `paragraph` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.paragraph>"/></w:pPr><w:r>…</w:r></w:p>` | `paragraph` |
|
||||
| `**bold**` | `<w:r><w:rPr><w:b/></w:rPr><w:t>…</w:t></w:r>` (inline) | — |
|
||||
| `*italic*` | `<w:r><w:rPr><w:i/></w:rPr><w:t>…</w:t></w:r>` (inline) | — |
|
||||
| `- item` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.list_bullet>"/><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>…</w:p>` | `list_bullet` |
|
||||
| `1. item` | same with `list_numbered`, `numId=2` | `list_numbered` |
|
||||
| `> quote` | `<w:p><w:pPr><w:pStyle w:val="<stylemap.blockquote>"/></w:pPr>…</w:p>` | `blockquote` |
|
||||
| `[label](url)` | `<w:hyperlink r:id="…">` (with rel registered in document.xml.rels) | — |
|
||||
|
||||
Not supported in v2.0 (defer to later slice or document as "edit in Word post-export"):
|
||||
|
||||
- Tables (Slice F-or-later; lawyers expect Word for tabular data anyway)
|
||||
- Images (the base supplies letterhead images; section content stays text-only)
|
||||
- Footnotes
|
||||
- Cross-references (Word's REF fields)
|
||||
- Code blocks (no legal use case)
|
||||
|
||||
Lists need the base's `numbering.xml` to declare numId=1 (bullet) and numId=2 (numbered). The `_skeleton.docx` already carries numbering definitions; existing bases need an audit (Slice B prereq).
|
||||
|
||||
### §9.3 Section anchor format
|
||||
|
||||
In base.docx body XML, anchors live as plain text inside a `<w:p>` paragraph:
|
||||
|
||||
```xml
|
||||
<w:p><w:r><w:t>{{#section:requests}}</w:t></w:r></w:p>
|
||||
<w:p><w:r><w:t>{{/section:requests}}</w:t></w:r></w:p>
|
||||
```
|
||||
|
||||
Renderer:
|
||||
|
||||
1. Find both paragraphs (regex on `<w:p[^>]*>…{{#section:KEY}}…</w:p>` for the opening; ditto for closing).
|
||||
2. Replace the entire range between them (inclusive of both anchor paragraphs) with the rendered section OOXML.
|
||||
3. If section is `included=false`, drop the range entirely (no replacement).
|
||||
4. If section is `*_auto`, the anchor range carries the inline OOXML for the canonical block — keep it, don't replace.
|
||||
|
||||
Anchors that aren't matched (leftover from a missing section in the draft): regex-strip to `<w:p></w:p>` (empty paragraph) at step 10.
|
||||
|
||||
### §9.4 Preserving existing v1 contracts
|
||||
|
||||
- **Placeholder grammar:** unchanged. Same `{{path.dot.notation}}` regex.
|
||||
- **`{{rule.X}}` aliases:** resolved identically inside section content. The composer's MD → OOXML walker preserves them as literal `{{rule.X}}` text; substitution happens in step 9 against the canonical resolver.
|
||||
- **Missing-marker behaviour:** unchanged — `[KEIN WERT: <key>]` / `[NO VALUE: <key>]`.
|
||||
- **Variable bag construction:** unchanged. `SubmissionVarsService.Build` returns the same shape.
|
||||
- **Audit:** one `system_audit_log` row + one `project_events` row per export. Metadata jsonb gains `composer: true` flag when `base_id IS NOT NULL`. event_type stays `submission.exported` / `submission_exported`.
|
||||
- **`last_exported_at` / `last_exported_sha`:** still bumped on each export. SHA is the base's `gitea_path` SHA.
|
||||
|
||||
### §9.5 Performance
|
||||
|
||||
A 10-section draft with 500 words each → ~50kB of Markdown total → ~150kB of OOXML to splice. The base is typically 100–200kB. Total assembly under 200ms on a warm cache. The Gitea fetch (cached) is the only meaningful network cost.
|
||||
|
||||
---
|
||||
|
||||
## §10 Authorization
|
||||
|
||||
### §10.1 Visibility (read)
|
||||
|
||||
- `submission_bases`: every authenticated user reads. Filter by `firm` + `proceeding_family` at the picker layer (not RLS — the base list is the same for everyone).
|
||||
- `submission_sections`: gated through draft_id → `submission_drafts` → `can_see_project` + owner-scoped. RLS mirrors `submission_drafts`'s policies.
|
||||
- `submission_building_blocks`: visibility tier predicate per row. Private/team/firm/global filter logic enforced in Go-layer service + DB RLS using the same `auth.uid()` helpers other tables use.
|
||||
|
||||
### §10.2 Mutations
|
||||
|
||||
- `submission_bases`: admin-only (`/admin/submission-bases` page; gated by the existing `adminGate`).
|
||||
- `submission_sections`: owner-scoped on the draft (matches v1 `submission_drafts` rules).
|
||||
- `submission_building_blocks`:
|
||||
- Create + edit `private`: any authenticated user.
|
||||
- Promote to `team`: any team member of the author's relevant team.
|
||||
- Promote to `firm` or `global`: admin moderation step (audit-logged).
|
||||
- Delete (soft, `deleted_at`): author or admin.
|
||||
|
||||
### §10.3 No new profession floor
|
||||
|
||||
Same call as v1: any user who can see the project can author Composer drafts. Substantive approval happens downstream of paliad (in Word, before filing). Adding a profession gate would slow the workflow without preventing anything paliad's existing approval system doesn't already cover.
|
||||
|
||||
---
|
||||
|
||||
## §11 Open design questions — recommended defaults + alternatives
|
||||
|
||||
m to ratify these via head escalation (no AskUserQuestion per task brief). The defaults are the inventor's pick; alternatives are listed so m can flip with one word.
|
||||
|
||||
### Q1 — Section content storage shape
|
||||
|
||||
**Default: separate `submission_sections` table** with one row per (draft, section).
|
||||
|
||||
Alternatives:
|
||||
- (a) jsonb column on `submission_drafts.sections` — simpler migration, no joins, but blocks the "which drafts use building block X" queries we'll want for refresh-from-library.
|
||||
- (b) Hybrid: jsonb by default, promote to table only when queries demand it — extra concept, schema drift over time.
|
||||
|
||||
Why default: building-block lineage queries (refresh, "where is this block used?", "is this block stale across N drafts?") become trivial with a relational table; jsonb makes them awkward joins on jsonb_array_elements. The cost of an extra table is small (one migration, RLS mirror).
|
||||
|
||||
### Q2 — Building block insert semantics
|
||||
|
||||
**Default: copy-on-insert with lineage retained.**
|
||||
|
||||
Alternatives:
|
||||
- (a) Live reference — every render re-fetches the library version. Surprising for the lawyer working a sealed pleading.
|
||||
- (b) Mixed — opt-in per block (the library row carries a `live_reference bool` column).
|
||||
|
||||
Why default: the lawyer's edits never get clobbered. The "library was updated, refresh?" affordance is opt-in via the lineage. Predictable; matches Word's "Paste as plain text" mental model.
|
||||
|
||||
### Q3 — Base storage
|
||||
|
||||
**Default: Gitea-backed body + thin DB row** (`submission_bases` row points at `gitea_path`).
|
||||
|
||||
Alternatives:
|
||||
- (a) Full-DB-stored — admin authors base body directly in paliad. Eliminates Gitea round-trip but requires a server-side .docx editor (complex; not core competency).
|
||||
- (b) Gitea-only — no DB row, base picked by directory scan. Loses the `is_default_for[]`, `section_spec`, RLS-able metadata.
|
||||
|
||||
Why default: lawyers + admins already author `.docx` in Word; mWorkRepo is the existing distribution vehicle. The DB row gives us listability, default-per-proceeding policy, and section spec storage without duplicating .docx content.
|
||||
|
||||
### Q4 — Editor surface
|
||||
|
||||
**Default: contentEditable per section with minimal toolbar (Markdown source-of-truth, in-house serializer).**
|
||||
|
||||
Alternatives:
|
||||
- (a) Tiptap (ProseMirror-based) — ~80kB bundle add per editor; matches WYSIWYG expectations but pulls a React-ecosystem dep into paliad's no-React frontend.
|
||||
- (b) Plain `<textarea>` with Markdown + side-by-side preview — simplest, but no inline bold/italic affordance.
|
||||
- (c) ProseMirror direct — same as Tiptap but more code; not a meaningful step up.
|
||||
|
||||
Why default: keeps paliad's HTML-first ethos, no React/ProseMirror dep, lawyers can still see bold/italic visually, source-of-truth is portable. Slice D extends the toolbar; the editor architecture survives.
|
||||
|
||||
### Q5 — Render pipeline
|
||||
|
||||
**Default: section anchors in base.docx + in-house MD → OOXML walker (pure Go).**
|
||||
|
||||
Alternatives:
|
||||
- (a) LibreOffice server-side compose — heavy dep (200MB+ runtime), licensing considerations, slower per-render.
|
||||
- (b) Pandoc shellout — same shape, smaller dep but still binary requirement on the Dokploy image.
|
||||
- (c) Pre-rendered base + section-injection via Office.js (browser-side) — pushes work to the client, requires Word desktop API, not portable.
|
||||
|
||||
Why default: pure-Go, no new runtime dep, integrates cleanly with the existing in-house renderer, performance budget under 200ms per export.
|
||||
|
||||
### Q6 — Migration of existing 11 drafts
|
||||
|
||||
**Default: implicit upgrade.** Existing rows get `base_id = NULL` (interpreted as "not yet upgraded"); the v1 render path stays as fallback. On the lawyer's next edit to a v1 draft, the editor prompts "Auf Composer umstellen?" and on yes seeds sections from the firm default base.
|
||||
|
||||
Alternatives:
|
||||
- (a) Auto-upgrade all rows on migration apply — sections lazily parsed from existing variables jsonb. Risky: lawyer's mental model of "my draft" changes without consent.
|
||||
- (b) Manual-only — no prompt; lawyer must explicitly create a new draft on the Composer to migrate. Slower adoption.
|
||||
|
||||
Why default: zero blast radius on existing rows; explicit consent on first edit. Lawyer can defer indefinitely.
|
||||
|
||||
### Q7 — Multi-language storage
|
||||
|
||||
**Default: split `content_md_de` + `content_md_en` columns from day one.**
|
||||
|
||||
Alternatives:
|
||||
- (a) Single `content_md` column, relies on draft.language to pick the right author surface (lawyer maintains one language at a time, switching language re-authors).
|
||||
- (b) Two-row pattern: one section row per language (4× rows, double the RLS).
|
||||
|
||||
Why default: same draft can render cleanly in either language with no re-authoring. Bilingual building blocks fold in naturally. Two columns are cheap.
|
||||
|
||||
### Q8 — Section default order per submission_code
|
||||
|
||||
**Default: code-driven (Go map in `submission_section_defaults.go`).**
|
||||
|
||||
Alternatives:
|
||||
- (a) DB-driven (`proceeding_section_templates` table) — flexible (admin can edit defaults without code change) but adds infra (table, RLS, admin editor).
|
||||
- (b) Per-base only (no per-code override) — simpler; loses the ability to differentiate Klageerwiderung vs. Statement of Defence section order.
|
||||
|
||||
Why default: section-default tweaks are rare and almost always require coordinated changes to the base's section_spec anyway. Keeping defaults in Go means one place to read; promote to DB if the override list grows past ~20 submission_codes.
|
||||
|
||||
### Q9 — Building block visibility tiers
|
||||
|
||||
**Default: 4 tiers — `private` / `team` / `firm` / `global`.**
|
||||
|
||||
Alternatives:
|
||||
- (a) 2 tiers — `private` / `firm`. Simpler RLS, loses the team-level curation that paliad already uses elsewhere.
|
||||
- (b) 3 tiers — drop `global` until cross-firm exists in practice. Hedges complexity.
|
||||
|
||||
Why default: matches paliad's existing team-visibility model. The moderation step gates the `firm` and `global` promotions; tier upgrade is an admin-reviewable event.
|
||||
|
||||
### Q10 — `*_auto` section semantics
|
||||
|
||||
**Default: lawyer toggles include/exclude, content rendered from base + bag; no edit.**
|
||||
|
||||
Alternatives:
|
||||
- (a) `*_auto` is editable (lawyer can override the auto-rendered Rubrum text). Risk: lawyer types content that conflicts with the variable bag.
|
||||
- (b) `*_auto` is fixed (lawyer can't even hide). Less flexible; "draft without signature" use cases break.
|
||||
|
||||
Why default: keeps the auto-rendered blocks consistent across drafts in the firm (no per-draft Rubrum drift) while letting the lawyer drop them when they don't apply (e.g., a "Schutzschrift" might omit the Rubrum block).
|
||||
|
||||
### Q11 — Preview pane
|
||||
|
||||
**Default: collapsed by default in v2 (in-place editor IS the surface).** Toggle `[Vorschau ▾]` opens a read-only HTML render below the section stack.
|
||||
|
||||
Alternatives:
|
||||
- (a) Side-by-side preview (always visible) — matches v1's layout, takes more screen real estate.
|
||||
- (b) No preview — risky; lawyers want to see the final render before exporting.
|
||||
|
||||
Why default: with editable sections, the editor surface IS the preview for prose. The toggle covers the "let me check the final layout" need; rare enough to not warrant permanent screen estate.
|
||||
|
||||
### Q12 — Refresh-from-library trigger
|
||||
|
||||
**Default: explicit per-section button, only visible when library version > snapshot.**
|
||||
|
||||
Alternatives:
|
||||
- (a) Auto-prompt on draft open ("4 building blocks have updates; refresh?") — annoying for active drafts.
|
||||
- (b) No surfacing — lineage exists only in metadata; lawyer must manually check.
|
||||
|
||||
Why default: opt-in keeps lawyer in control; the surfaced affordance triggers only when there's a real update.
|
||||
|
||||
---
|
||||
|
||||
## §12 Slice plan
|
||||
|
||||
Each slice independently shippable. Coder picks up the next slice only after the previous one ships + lawyer validation.
|
||||
|
||||
### Slice A — Base picker (read-only) and section list shell
|
||||
|
||||
**Scope:**
|
||||
- Migrations: `submission_bases` table, seed `hlc-letterhead` + `neutral` rows pointing at existing `.docx` files; `submission_sections` table; `submission_drafts.base_id` + `composer_meta` columns.
|
||||
- Backend: `BaseService` (list, get-by-slug, get-default-for-code). `SubmissionDraftService.Create` seeds `base_id` + section rows.
|
||||
- Frontend: sidebar picker "Vorlagenbasis" with the seeded bases. Section list rendered read-only above the existing preview (no edit yet).
|
||||
- Render: unchanged — still uses v1 `resolveSubmissionTemplate` chain. `base_id` is informational.
|
||||
- **Ships:** lawyer sees the base concept land, no behavior change.
|
||||
|
||||
**Acceptance:**
|
||||
- Creating a new draft seeds `base_id` from firm + family default.
|
||||
- Sidebar picker renders ≥2 bases; selecting one PATCHes `base_id`.
|
||||
- Section list renders the base's default section set (read-only labels in active language).
|
||||
- All 11 existing drafts continue to render via v1 path (their `base_id` is NULL).
|
||||
- Unit tests cover: base default picker, section-list payload, migration round-trip.
|
||||
|
||||
### Slice B — Editable prose sections + composer render pipeline
|
||||
|
||||
**Scope:**
|
||||
- Backend: `SubmissionSectionService` (CRUD on section rows). New endpoints: `GET/POST/PATCH/DELETE /api/projects/{id}/submissions/{code}/drafts/{draft_id}/sections/{section_id}`. `SubmissionComposer` (render pipeline §9.1 + MD → OOXML walker §9.2, paragraphs + bold/italic + headings 1–3 + bulleted list only).
|
||||
- Frontend: section stack on the body, contentEditable per section, shared toolbar (B/I/H1/H2/bullet), autosave per section. The existing variable sidebar stays.
|
||||
- Render: when `base_id IS NOT NULL`, use the composer; else fall back to v1 path. Audit metadata gets `composer: true`.
|
||||
- Base `.docx` audit: regenerate `_firm-skeleton.docx` and `_skeleton.docx` to add `{{#section:KEY}}` anchors for the default section set. Done via extended `scripts/gen-submission-base/main.go`.
|
||||
- **Ships:** the smallest milestone where Composer actually works. Lawyer can author prose, see the rendered .docx.
|
||||
|
||||
**Acceptance:**
|
||||
- Lawyer can edit any `prose`/`requests`/`evidence`-kind section.
|
||||
- Toolbar applies bold, italic, headings 1–3, bullet list.
|
||||
- Export renders all sections at their correct anchors with bag substitution.
|
||||
- `{{rule.X}}` aliases still resolve inside section content.
|
||||
- `*_auto` sections render as before (from base + bag).
|
||||
- v1 drafts (base_id=NULL) render via v1 path unchanged.
|
||||
- Golden tests on a sample draft → expected OOXML output.
|
||||
|
||||
### Slice C — Building blocks library + section picker
|
||||
|
||||
**Scope:**
|
||||
- Migrations: `submission_building_blocks` + `submission_building_block_versions` tables.
|
||||
- Backend: `BuildingBlockService` (CRUD + visibility filtering + version GC retention=20). Endpoints: `/api/blocks` (personal list), `/api/admin/submission-blocks` (admin CRUD), `/api/sections/{id}/insert-block` (the actual insert mechanic).
|
||||
- Frontend: `/admin/submission-blocks` editor (full-page list + edit + version log + restore, mirrors `/admin/email-templates` shape). Per-section `+ Baustein einfügen` modal with filter chips.
|
||||
- **Ships:** reusable building blocks.
|
||||
|
||||
**Acceptance:**
|
||||
- Admin creates a block at firm visibility → visible to every HLC user in the picker.
|
||||
- Lawyer inserts a block into a `requests` section → block content appended at cursor; `building_block_id` + version stamped.
|
||||
- Block update → existing drafts unaffected; "Aus Bibliothek aktualisieren" button surfaces on the section.
|
||||
- Visibility tiers enforced (private not visible to other users; team requires team membership; firm requires firm match; global open).
|
||||
|
||||
### Slice D — Rich-prose features (lists, numbered, blockquote, links, stylemap)
|
||||
|
||||
**Scope:**
|
||||
- MD → OOXML walker: add numbered lists, blockquote, hyperlinks.
|
||||
- Base `.docx` audit: ensure `numbering.xml` carries numId=1 (bullet) and numId=2 (numbered) definitions; backfill any base that lacks them.
|
||||
- Each base's `section_spec.stylemap` gets all 8 entries populated.
|
||||
- Frontend toolbar gains `[1.]` (numbered) and `[❝]` (quote).
|
||||
- **Ships:** full prose feature set.
|
||||
|
||||
### Slice E — Specialist base templates (LG-Düsseldorf, UPC-formal) + base swap survives content
|
||||
|
||||
**Scope:**
|
||||
- New `.docx` bases authored in Word: `lg-duesseldorf.docx`, `upc-formal.docx`. Each carries proceeding-family-specific letterhead, paragraph styles, default section spec.
|
||||
- Verification: base swap mid-draft preserves section content; only chrome/styles change.
|
||||
- Generator script (`scripts/gen-submission-base/main.go`) supports the new bases.
|
||||
- **Ships:** lawyer can swap firm letterhead → court-style mid-draft.
|
||||
|
||||
### Slice F — Section reorder / hide / add custom
|
||||
|
||||
**Scope:**
|
||||
- Frontend: drag-and-drop on the section list (small custom drag library or HTMX-style; no React DnD).
|
||||
- "+ Abschnitt hinzufügen" picker (existing slugs + new slug form).
|
||||
- `composer_meta.section_order` mirrors `order_index` for fast paint.
|
||||
- Per-section "Nicht aufnehmen" toggle.
|
||||
- **Ships:** full lawyer control over composition.
|
||||
|
||||
### Slice G — Diff & merge for building-block refresh
|
||||
|
||||
**Scope:**
|
||||
- "Aus Bibliothek aktualisieren" button surfaces when section's snapshot_version < library_version.
|
||||
- Diff view (text-level, library-side library version vs section content).
|
||||
- Three-way: keep / overwrite / merge (side-by-side conflict markers).
|
||||
- **Ships:** lawyer can actively curate library updates per draft.
|
||||
|
||||
---
|
||||
|
||||
## §13 Out of scope (explicit)
|
||||
|
||||
To set the boundary cleanly:
|
||||
|
||||
- **Real-time collaborative editing** — no Operational Transform / CRDT. One draft, one author, last-write-wins on owner-scoped writes. Two lawyers wanting to co-edit start two drafts.
|
||||
- **Comment threads on sections** — lawyer-level review happens in Word post-export.
|
||||
- **Deep version history** — beyond `updated_at` on the draft + retention-20 on building-block library. Per-section version history is a Slice H+ idea.
|
||||
- **AI-drafted prose** — the existing Paliadin persona handles ad-hoc Q&A; AI-drafted section content (a `{{ai.draft_facts}}` placeholder, say) is a future task with its own design doc. v1's §11 sketch still applies.
|
||||
- **Cross-draft section sharing** — use building blocks. We won't add a "this section is a live reference to that section in another draft" concept.
|
||||
- **Tables, images, footnotes, cross-references** — author in Word after export. The Composer is for structural composition + first-draft prose.
|
||||
- **PDF output** — `.docx` only. Lawyers convert to PDF in Word.
|
||||
- **Email or e-filing integration** — explicit Word-then-NetDocuments-then-court flow is the current contract; not in scope.
|
||||
- **Translation between DE and EN** — both columns exist on every section + block, but content authoring is manual. No machine translation pass.
|
||||
|
||||
A handful of issue-#141 items that DON'T survive into v2 by inventor judgement:
|
||||
|
||||
- "Live FK to building block (no copy)" — replaced by copy-on-insert + lineage (Q2).
|
||||
- "Tiptap / ProseMirror" — replaced by contentEditable + in-house serializer (Q4).
|
||||
- The implied bigger ProseMirror push back into "rich editor" — addressed with simpler approach.
|
||||
|
||||
Issue text also mentioned `submission_drafts.audit_log` — this column doesn't exist; the audit contract uses `system_audit_log` + `project_events`. The design respects the **actual** audit contract; if m wants a new column added, that's a separate decision tracked here.
|
||||
|
||||
---
|
||||
|
||||
## §14 Risks + mitigations
|
||||
|
||||
1. **MD → OOXML walker fidelity.** Tiny rendering bugs (numbering, indentation, bullet styles) annoy lawyers who expect Word-perfect output.
|
||||
- **Mitigation:** stylemap per base; golden-test set against HL Patents Style for every Markdown construct. Slice D's rich features only after Slice B's prose-only path proves stable. Lawyer can always fix layout in Word post-export.
|
||||
|
||||
2. **Base swap blast radius.** If section content depends inadvertently on base styles (e.g. lawyer pastes Word content with inline formatting), swap can look broken.
|
||||
- **Mitigation:** Markdown source-of-truth is base-agnostic by construction. The contentEditable → MD serializer strips inline styles. Pasted Word content goes through a paste-normalizer (drops `<style>`, `<font>`, color attributes). Bullet/numbered styles resolved per-base at render.
|
||||
|
||||
3. **Building block proliferation.** Without curation, every lawyer creates their own variant of the same boilerplate.
|
||||
- **Mitigation:** visibility tiers (private → team → firm → global); admin moderation gate on firm + global promotions; "duplicate-detector" check at create time (slug + content_md hash) suggests existing similar blocks.
|
||||
|
||||
4. **Section anchor robustness.** If a base.docx is malformed (missing closing anchor, anchor inside a table cell), the renderer breaks.
|
||||
- **Mitigation:** base validation at upload (the `submission_bases` admin editor refuses bases with unmatched anchors); regex-based stripper at step 10 silently drops unclosed openings as empty paragraphs.
|
||||
|
||||
5. **{{rule.X}} alias forward-compat.** Section content authored today may use either canonical or legacy forms. If we ever drop the aliases, sections break silently.
|
||||
- **Mitigation:** the alias contract is pinned (memory `01cdd589`, knuth's t-paliad-275 ship notes). Composer's editor surfaces the canonical name in the autocomplete picker so new content tends canonical; the renderer resolves both forms identically.
|
||||
|
||||
6. **Bundle size.** contentEditable + toolbar adds ~10kB; section stack of 8–10 instances → ~40kB extra over v1. Acceptable.
|
||||
- **Mitigation:** one shared toolbar instance hops between sections (single DOM tree, reactivated on focus). MD ↔ DOM serializer is ~3kB shared across all sections.
|
||||
|
||||
7. **Performance with 100+ building blocks per firm.** Picker render time grows.
|
||||
- **Mitigation:** server-side filter + paginated list (limit 25 most-recent + free-text search input). Same shape as the party DB picker in t-paliad-287.
|
||||
|
||||
8. **Existing drafts orphaned.** If a lawyer leaves a v1 draft un-migrated and later edits it, the "upgrade" prompt might confuse them.
|
||||
- **Mitigation:** prompt is non-destructive (default NO, "Composer is opt-in for this draft"). v1 path keeps working indefinitely.
|
||||
|
||||
9. **Section spec versioning.** If we tweak the default section set, in-flight drafts created before the change have a stale `composer_meta.section_order`.
|
||||
- **Mitigation:** `section_spec.version` field. Compose service uses the lawyer's `composer_meta.section_order` if set (always); else seeds from current base spec. Spec bumps only affect new drafts.
|
||||
|
||||
10. **Coupling between base and stylemap.** A base swap could fail if the new base doesn't declare every stylemap key the old base did.
|
||||
- **Mitigation:** required keys list enforced at base-upload-time validation. Specialist bases must declare paragraph + heading_1/2/3 + list_bullet + list_numbered + blockquote.
|
||||
|
||||
---
|
||||
|
||||
## §15 Open follow-ups (NOT blocking m's go/no-go)
|
||||
|
||||
These are coder/inventor follow-ups, not m-decisions:
|
||||
|
||||
- **Issue #141 body correction** — flag the inaccurate `submission_drafts.audit_log` reference + "7 live rows" count. Update issue text post-design-approval.
|
||||
- **`scripts/gen-submission-base/main.go`** — extension of t-paliad-275's HL skeleton generator to embed `{{#section:KEY}}` anchors and a section_spec custom XML part. Builds on existing pattern.
|
||||
- **Stylemap audit on `_firm-skeleton.docx`** — verify every required style name exists (HLpat-Heading-H1/H2/H3, HLpat-Body-B0, HLpat-Body-B1). Pre-Slice-B work.
|
||||
- **Numbering.xml audit on `_skeleton.docx` + `_firm-skeleton.docx`** — verify numId=1 (bullet) and numId=2 (numbered) declared. Pre-Slice-D work.
|
||||
- **i18n keys** — ~40 new keys for Slice A (sidebar picker, base label, "Vorlagenbasis", "Basis wechseln", section toolbar). ~20 more for Slice C (building-block library labels). DE + EN.
|
||||
- **CLAUDE.md updates** — add a "Composer" section documenting the base concept, section spec, MD-source-of-truth contract, the `composer: true` audit metadata flag.
|
||||
- **mBrian topic node** — file the synthesis post-approval; topic hub probably `submission-composer` under `paliad`.
|
||||
- **Storyboard / Loom walk-through** — Slice B ships → record a 90-second screen-cap of the lawyer experience for the HLC validation round.
|
||||
|
||||
---
|
||||
|
||||
## §16 Recommended implementer
|
||||
|
||||
Pattern-fluent Sonnet coder. The substrate is well-trodden:
|
||||
|
||||
- Existing draft service: `internal/services/submission_draft_service.go` — extend, don't replace.
|
||||
- Existing renderer: `internal/services/submission_merge.go` — reuse for the placeholder-substitution pass; add `internal/services/submission_compose.go` for the assembly + MD walker.
|
||||
- Existing template fetch: `internal/handlers/files.go` Gitea proxy — reused for base.docx and (if Slice C+ adds Markdown blocks stored externally) building-block content.
|
||||
- Existing audit shape: `paliad.system_audit_log` + `paliad.project_events` — extend `metadata jsonb` with `composer: true`; no schema change there.
|
||||
- Existing admin pattern: `/admin/email-templates` for the building-blocks admin editor; mirror exactly.
|
||||
- Existing visibility helpers: `internal/services/visibility.go` + team-membership predicates for tier filtering.
|
||||
|
||||
Novel pieces (all isolated):
|
||||
|
||||
- MD → OOXML walker (~350 LoC including tests).
|
||||
- Section anchor parser + splicer (~150 LoC).
|
||||
- contentEditable ↔ Markdown serializer (~250 LoC TS).
|
||||
- Composer admin editor (`/admin/submission-bases`, `/admin/submission-blocks`).
|
||||
|
||||
NOT cronus (per project memory directive).
|
||||
|
||||
---
|
||||
|
||||
## §17 Acceptance for the design (this doc)
|
||||
|
||||
1. ✅ All four headline requirements (sections, bases, editability, building blocks) have concrete data-model + UX + render-pipeline proposals.
|
||||
2. ✅ Every open design question (12 of them) has a recommended default + at least one alternative.
|
||||
3. ✅ Slice plan present (A → G), each independently shippable; Slice B is the smallest "Composer works" milestone.
|
||||
4. ✅ Migration path documented: existing 11 drafts continue via v1 path; opt-in upgrade per draft.
|
||||
5. ✅ Hard constraints honoured: `submission_drafts` shape preserved, `{{rule.X}}` aliases preserved, audit contract preserved, no new third-party Go dep.
|
||||
6. ✅ Premises verified live against the running paliad + DB; doc-vs-live conflicts flagged in §1 and §13.
|
||||
7. ✅ Risks identified with mitigations.
|
||||
8. ⏳ Inventor reports `DESIGN READY FOR REVIEW`. **Inventor MUST NOT shift to coder.** Head gates the inventor→coder transition.
|
||||
|
||||
---
|
||||
|
||||
## §18 What m needs to ratify before coder shift
|
||||
|
||||
Per task brief: no AskUserQuestion. Escalation via `mai instruct head` for the 12 §11 questions, batched. Default picks are listed inline; m can flip any to an alternative with one word.
|
||||
|
||||
Head's job after this doc lands:
|
||||
|
||||
1. Surface §11's Q1–Q12 to m with the recommended defaults.
|
||||
2. Capture m's ratifications (or alternative picks) as an addendum at the top of this doc (§ m's decisions, 2026-MM-DD) — same shape as t-paliad-215's §2.
|
||||
3. Decide who codes Slice A: same worker as `/mai-coder` with this design + addendum as brief, fresh coder, or rescope.
|
||||
4. Park Slice B–G until Slice A ships + lawyer validation lands.
|
||||
|
||||
The decisions section gets committed on the same branch as this design (no separate branch).
|
||||
|
||||
Inventor parks here.
|
||||
Reference in New Issue
Block a user