49b61cfa7328eae4240b7acfcd89f0c8bed9e8d1
348 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 8ccf64cf83 |
feat(submissions): t-paliad-370 S4 — truthful base-preview render (code)
docforge UX slice S4 (PRD §2.3). Extends the S3 /api/submission-preview
endpoint with a truthful page-image render behind the SAME modal+endpoint
shape (the swappable body region). Resolves the S3 caveat: anchors-only
Composer/Gitea bases now render their REAL letterhead/Rubrum styling instead
of falling back to a base-agnostic structural view.
Pipeline (PRD §2.3): build the .docx via the EXISTING export pipeline
(byte-faithful) → Gotenberg sidecar (.docx→PDF over HTTP, keeps the Go image
lean) → poppler pdftoppm (PDF→PNG-per-page) → cache + single-flight.
Code only — NO Dokploy/compose change (head provisions the sidecar + fonts).
When GOTENBERG_URL is unset or poppler is absent, Available()=false and the
endpoint gracefully falls back to S3 structural HTML, so merging this is safe
before the infra lands.
Backend:
- internal/services/preview_render.go: PreviewImageRenderer interface +
GotenbergRenderer (.docx→PDF→PNG) + PreviewImageCache (bounded FIFO,
single-flight per key, serialised conversions) + NewPreviewImageCacheFromEnv
(GOTENBERG_URL / PREVIEW_DPI). The PNG cache is regenerable, not a retained
document (no disk/DB persistence; rebuildable from inputs).
- /api/submission-preview gains fidelity=truthful: builds the truthful .docx
(editor = clone draft + base override → full export pipeline incl. Composer;
catalog = context bag → render), caches by (draft id+updated_at | code+
project) × base × lang × data-mode, returns page data-URIs. Structural
responses now carry truthful:false.
- exportSubmissionDraft + Export gain a pluggable missing-marker (nil = default,
so every existing export path is byte-identical); enables sample-data truthful
render. New SubmissionDraftService.{ExportWithMarker,RenderContextPreviewDocx}.
- Unit tests: cache hit/miss, single-flight, eviction, availability, gotenberg
multipart request shape (httptest, no poppler needed).
Frontend:
- base-preview-modal.ts requests fidelity=truthful; renders <img> page(s) with
a ‹ n/N › prev/next pager (instant — pages held client-side); falls back to
the S3 structural sheet when truthful:false. CSS for .base-preview-img +
page-nav.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail;
+6 new service tests).
|
|||
| a70508a7d5 |
feat(submissions): t-paliad-370 S3 — Vorschau base-preview modal scaffold
docforge UX slice S3 (PRD §2.1 / §3 S3). A shared, body-attached modal lets
the lawyer browse template bases, flip output language + data-mode, and — in
the editor — commit a base with 'Diese Basis verwenden', replacing the blind
<select> as the chooser. Opened from the editor 👁 button AND both catalog row
kebabs (the S1/S2 stubs are now live).
Body renders the EXISTING structural preview on cheap rails; the body region is
swappable so S4 drops a truthful .docx→image render behind the same modal +
endpoint shape. Spinner + pager are the shell S4 expects.
Backend (minimal, no truthful render — that's S4):
- GET /api/submission-preview?base&code&lang&data&draft?&project? → {preview_html}.
Draft+mine uses the draft's resolved bag (previews a base the draft has NOT
committed to — no persistence); else a fresh context bag (project-less fill,
t-paliad-364). data=sample swaps a sample missing-marker so unresolved keys
render readable sample text, not [KEIN WERT].
- SubmissionDraftService.RenderPreviewWithMarker + RenderContextPreviewHTML
(thin: reuse BuildRenderBag / vars.Build + RenderHTML).
- resolvePreviewTemplateBytes: tpl:→carrier, base_id→bytes IFF it carries merge
placeholders, else fall back to the code's merge template (anchors-only
Composer bases can't be shown as structural HTML — their letterhead/styling
surfaces in S4's truthful render). sampleMissingMarker by key suffix.
- Unit tests: sampleMissingMarker + normalizePreviewLang.
Frontend:
- New shared client/base-preview-modal.ts (built once, body-attached; serves
editor + project tab + global picker). Base-switcher, DE/EN + meine/Beispiel
toggles, swappable body, spinner, pager stub, conditional 'Diese Basis
verwenden' (editor commits via onBaseChange; catalog = look-only).
- Editor 👁 button un-stubbed + wired; both catalog kebab 'Vorschau' items
un-stubbed + wired. global.css: .base-preview-* modal styles.
bun build (i18n scan clean) + go vet ./... + go test ./... (15 ok, 0 fail).
|
|||
| c1781c9a89 |
feat(generation): t-paliad-364 styled+filled submissions — project-less caption fill (P3b) + merge-safe styled firm-skeleton generator (P3a Option B)
P3(b) fill-what-we-can (submission_vars.go):
The project-less branch of SubmissionVarsService.Build ran only firm/today/
user/proceduralEvent resolvers, so caption.*/project.proceeding.* never
populated and every Rubrum value rendered [KEIN WERT]. The rule is already
loaded on this path and carries ProceedingTypeID. Now the branch loads the
proceeding type via loadProceedingType(rule.ProceedingTypeID) (tolerates a nil
id) and appends a proceeding-only projectResolver{project:nil} + a
captionResolver{project:nil} — both nil-project safe. Result: caption heading /
designations / versus / subject + the proceeding line fill from the
submission_code's proceeding; only party names / case number / court stay blank
for the lawyer. Preserves the "Ohne Projekt" affordance (t-paliad-243).
addProjectVars is now nil-project safe (guards the project.* direct fields,
keeps the pt-driven project.proceeding.* block) so projectResolver can serve as
the proceeding-only resolver. Pinned by TestProjectlessFill_* +
TestAddProjectVars_NilProjectFillsProceedingOnly (no DB).
P3(a) styling — merge-safe styled firm-skeleton (scripts/gen-hl-skeleton-template):
Generation landed on docx.BuildFallbackSkeleton (generic Heading1/2/Normal)
because the firm-skeleton's body had been repurposed into an anchors-only
Composer base, which HasMergePlaceholders rejects. Rewrote the generator to take
the deployed clean .docx carrier and replace ONLY word/document.xml with a clean
caption-driven Rubrum that uses the firm Rubrum styles (Table-Recitals-Party/
PartyDetails/PartyRoles/Sequencers, Heading-H2, Signature, Body-B0) and the same
{{key}}/{{caption.*}} placeholders the in-process fallback uses — preserving the
carrier's styles/theme/numbering/letterhead/logo and its sectPr verbatim. Adds a
-lang flag (DE _firm-skeleton.docx, EN _skeleton.en.docx) and auto-detects the
firm style prefix (HLpat-/HLCpat-) so it stays correct across the .dotm rebrand
drift. The resolver's tier-4/tier-3 merge-safe guard auto-prefers the restored
templates — no handler change.
Dry-run (TEST_DATABASE_URL, de.inf.lg.erwidg): project-less render fills caption
heading="In dem Rechtsstreit"/Klägerin/gegen/Beklagte/Patentverletzung + the
proceeding line, only party/case/court blank; full-project render additionally
fills parties + case number + court. Both carry the HLpat-Table-Recitals-* /
HLpat-Heading-H2 / HLpat-Signature styles, HasMergePlaceholders=true, no
{{#section}} junk.
|
|||
| 1882468780 |
feat(caption): apply m's caption-wording decisions — Respondent + Streitpatent line + DE appeal/nullity role labels
t-paliad-361, follow-up to t-paliad-358 A-S2. m ruled on the 7 lexy-wording
flags (AskUserQuestion 2026-06-01 14:30). Most flags CONFIRMED the live
wording; three changes land here, all caption (Rubrum) wording, all in one
reversible migration 163.
Change 1 — UPC appeal responding party (EN): 'Appellee' → 'Respondent'.
Fixed at the data source: the mig-137 role-label override on
upc.apl.unified (the only place 'Appellee' was stored). The caption
resolver's instance-derived EN fallback already said 'Respondent', so no
code change. DE side (Berufungsbeklagter) untouched per m.
Change 2 — restore the standalone 'Streitpatent: {{project.patent_number_upc}}'
(DE) / 'Patent in suit:' (EN) line in the upc-formal Composer caption seed,
dropped in A-S2 (mig 161). Keeps the parametric 'In der Sache' heading (m did
not revert that). Only the upc-formal base is touched; grouped with the case
number ahead of {{project.court}}.
Change 3 — backfill lexy-confirmed role-label overrides for the four DE
appeal/nullity proceedings that carried none, so designations are correct
even when project.instance_level is unset (statute-grounded: §§ 511/542/544
ZPO, §§ 81/110 PatG; bracketed-inclusive gender style):
de.inf.olg Berufungskläger(in) / Berufungsbeklagte(r) // Appellant / Respondent
de.inf.bgh Revisionskläger(in) / Revisionsbeklagte(r) // Appellant / Respondent
de.null.bpatg Nichtigkeitskläger(in) / Beklagte(r) (Patentinhaber(in)) // Nullity claimant / Defendant (patent proprietor)
de.null.bgh Berufungskläger(in) / Berufungsbeklagte(r) // Appellant / Respondent
Updates submission_vars_caption_test.go: adds EN assertions + cases pinning the
Respondent change and the four backfilled designations (each with instance_level
unset, proving the override path). go vet + go test ./... + bun build clean.
|
|||
| c639c5695c | Merge remote-tracking branch 'origin/main' into mai/fermi/coder-implement-nomen | |||
| a05ae1f2ae |
feat(settings): firm-wide default name compositions (t-paliad-356 Slice 5)
Completes the nomen train (S1–S5). Adds the FIRM tier of the name-composition
precedence chain — per-document → user → FIRM → system (PRD §3.1/§3.2) —
mirroring firm_dashboard_default exactly.
Storage + service:
- Migration 162: paliad.firm_name_compositions singleton (id=1, CHECK id=1,
RLS read-all + service-role writes) — same shape as firm_dashboard_default
(mig 117), holding a validated { artifact_id: Composition } jsonb map.
- FirmNameCompositionService (Get/Set/Clear) + getFirmNameCompositions /
setFirmNameCompositions / clearFirmNameCompositions singleton helpers in
name_composition_spec.go.
Resolution:
- resolveComposition is now variadic over ordered specs (user, firm); first
valid wins, else system default. Existing single-spec callers unchanged.
- Render path threads the firm tier: renderSubmissionDraftTitle /
RenderSubmissionFilenameFor gain a firm param; newDraftName +
submissionDownloadFilename load it (nil-safe). A firm default thus changes
the effective name for every user without a personal override.
Admin surface (mirrors firm_dashboard_default):
- GET/PUT/DELETE /api/admin/name-compositions{/artifact_id} (adminGate) read
back / set / clear the firm default per artifact.
- /settings Namensschemata cards gain an admin-only "Firmenstandard" block
(set from the current template field / clear) revealed via is_admin, plus a
"Firmenstandard" badge for non-admin users whose effective name comes from
the firm tier. SettingsNameArtifact now resolves user→firm→system and
exposes firm_is_set/firm_template.
Tests: pure precedence (user>firm>system) + firm-tier view + live firm
round-trip/Validate-rejection (via db.ApplyMigrations). go vet, go test ./...,
bun build all clean; gated live tests green against TEST_DATABASE_URL.
NOTE (merge ordering): golang-migrate is forward-only. Migration 162 must not
reach a DB before bohr's 161 (Rubrum Composer seed) exists, or 161 will be
skipped (current>161 → never applied). Merge 161 before/with 162.
Browser Playwright of the admin firm controls deferred to post-deploy
mai-tester — shared Supabase login wall blocks pre-merge browser login (same
ceiling as t-paliad-354).
|
|||
| 57310ab3a4 | Merge remote-tracking branch 'origin/main' into mai/bohr/coder-rubrum-letterhead | |||
| b99b6d6fb5 |
feat(submissions): parametric caption.* keys — unify Rubrum across all render paths (t-paliad-358 A-S2)
Promotes the case caption (Rubrum) to ONE parametric set of resolver keys
(caption.*) consumed identically by every render path, so the wording no
longer diverges per path and reflects the forum.
New caption.* keys (addCaptionVars, submission_vars.go), each in bare +
_de + _en forms (bare resolves to the draft language):
caption.heading · caption.claimant_designation · caption.defendant_designation
caption.versus · caption.subject
Parametrised from data already in the bag — NO new schema:
- designations reuse the proceeding-type role-label overrides (mig 137:
upc.apl.unified→Berufungskläger, upc.rev.cfi→Antragsteller (Nichtigkeit),
epa.opp.*→Einsprechende(r)/Patentinhaber(in)); else instance-derived
appeal/cassation (project.instance_level); else civil default Klägerin/
Beklagte // Claimant/Defendant.
- heading + subject from jurisdiction + the dotted code's nature segment
(inf→"In dem Rechtsstreit"/"Patentverletzung", null→"In der
Patentnichtigkeitssache", UPC→"In der Sache"/"In the matter",
opp→"Im Einspruchsverfahren").
Also exposes project.proceeding.jurisdiction.
All three render paths now reference the SAME keys:
1. docx.BuildFallbackSkeleton (merge fallback) — heading/designations/versus/
wegen-subject are {{caption.*}} placeholders.
2. demo per-code template de.inf.lg.erwidg.docx (mWorkRepo) — regenerated via
scripts/gen-demo-submission-template; caption wired to caption.*; closing
drops the duplicate {{firm.name}} line (now carried by signature_block).
Pushed to HL/mWorkRepo as mAi (commit 3682299).
3. Composer caption seeds — mig 161 rewrites the caption section seed_md of
all 4 bases (hlc-letterhead, neutral, lg-duesseldorf, upc-formal) to the
parametric form (position-independent jsonb_agg patch; reversible down).
our_side is intentionally NOT a caption driver — the caption designates both
parties by procedural role regardless of which side we act for.
Tests: resolveCaption forum matrix (DE-LG/BPatG/UPC/role-label/instance-appeal/
EPA-opp), bare-resolves-to-lang, fallback skeleton renders caption.* keys.
mig 161 passes TestMigrations_DryRun. go vet ./... + bun build clean; all
touched packages green (the live approval/migration_136 failures are
pre-existing shared-DB env issues, unrelated).
LEXY-REVIEW FLAGS in the report — DE caption conventions are practitioner
convention, not in the youpc corpus; specific wordings flagged for sign-off.
|
|||
| 230306518d | Merge remote-tracking branch 'origin/main' into mai/fermi/coder-implement-nomen | |||
| 6e56b9d51f |
feat(settings): name-composition token-template editor (t-paliad-356 Slice 4)
Adds the /settings "Namensschemata" tab so users can customise the two wired
name artifacts (submission_draft_title, submission_docx_filename) via a
single-line {token} template, with a clickable palette, live preview, and
reset-to-default — PRD §7.
Engine (pure, pkg/nomen):
- Composition.Template() serialises a composition to "{var}" shorthand;
ParseTemplate() is its inverse — tokens + literal separators (trailing,
owned by the left segment) + paren Wrap. Missing-rules are NOT in the
shorthand (PRD §7); the parser leaves every segment KindOmit. Leading /
trailing literals are rejected (the trailing-separator model can't carry
them) so a save never silently drops characters. Table + round-trip tests.
Paliad glue (internal/services/name_template.go):
- ParseNameTemplate overlays each segment's missing-rule from the artifact's
system default and validates against the catalog.
- PreviewNameComposition renders against the fixed PRD sample (Bayer AG / UPC
/ Sandoz / UPC_CFI_123/2026 / today) and an empties resolver so the
missing-rule behaviour is visible. The frontend never parses templates —
the nomen engine stays the single source of truth.
- SettingsNameArtifacts / SettingsNameArtifact build the per-artifact cards
(current template, system default, override flag, ordered palette, previews).
API (internal/handlers/name_compositions.go):
- GET /api/me/name-compositions — cards
- POST /api/me/name-compositions/preview — live preview + validation
- PUT /api/me/name-compositions/{artifact_id} — store override
- DELETE /api/me/name-compositions/{artifact_id} — reset to system default
Storage reuses the Slice-3 service surface (UserNameCompositions /
SetUserNameCompositions) via read-modify-write; no new column, no migration.
Frontend: new tab + JS-built cards (palette insert-at-cursor, 250ms-debounced
preview, save/reset, DE/EN labels), CSS, and i18n keys (de + en).
Gates: go vet, go test ./..., bun build all clean. Browser verification of the
settings UX is deferred to post-deploy mai-tester — the shared Supabase login
wall blocks pre-merge browser login (same ceiling as t-paliad-354).
|
|||
| 0763b7daa2 |
feat(submissions): fill firm.signature_block + fix generate-fallback junk (t-paliad-358 A-S1)
Two letterhead/Rubrum auto-fill fixes (Option A, no schema change):
1. firm.signature_block: was hardcoded "" ("reserved for Phase 2"), so every
template referencing {{firm.signature_block}} rendered blank. Now filled
from branding.Name — the firm identity line of a submission's signature
block (the signature section seeds with signature_block + user.display_name).
Firm-agnostic: a FIRM_NAME redeploy signs with the right firm.
2. Generate-fallback junk (kepler audit §1 Path 3): resolveSubmissionTemplate is
the merge-path resolver (every caller feeds merge.go), but its lower tiers
fetched _firm-skeleton.docx / _skeleton.docx — which were repurposed into
anchors-only Composer bases (t-paliad-313 Slice B). Their bodies hold only
{{#section:KEY}} markers, which placeholderRegex ignores, so merge.go emitted
them verbatim as literal "{{#section:letterhead}}…" junk for every code
without a per-code template (i.e. everything except de.inf.lg.erwidg).
Fix:
- docx.BuildFallbackSkeleton(lang): in-process, lang-aware, merge-safe basic
Schriftsatz with a data-driven basic Rubrum (real {{key}} placeholders the
var bag fills). Always available, no Gitea round-trip.
- docx.HasMergePlaceholders guards tiers 3/4/5: a fetched skeleton is used
only if it carries real placeholders, else we fall through to the embedded
fallback. Today's anchors-only/placeholder-free files are skipped; a future
merge-safe firm-skeleton (with letterhead) is preferred again automatically.
- merge.go strips stray {{#section:…}}/{{/section:…}} markers defensively so
no anchors-only carrier can ever leak Composer junk into a merged document.
Verified: confirmed live that deployed _firm-skeleton.docx + _skeleton.docx are
anchors-only (fetch+unzip); unit tests cover BuildFallbackSkeleton rendering a
real Rubrum (de+en), HasMergePlaceholders classification, marker stripping, and
the signature_block fill. go build / vet ./... / test ./... + bun build clean.
Out of scope (flagged for next slices): demo template's closing prints
{{firm.name}} then {{firm.signature_block}} (=firm.name) → A-S2 dedups the demo
wording. Restoring firm letterhead chrome to the merge fallback → A-S3.
|
|||
| 73f379d305 |
feat(submissions): per-user name-composition overrides, system→user precedence (t-paliad-356 Slice 3)
PRD §3. A user can now override the system-default name composition for an
artifact; rendering prefers a valid user override over the system default.
No UI yet (Slice 4) — overrides are settable via the service API + tests.
- Migration 160: users.name_compositions jsonb NOT NULL DEFAULT '{}' — a
{ artifact_id: Composition } map.
- NameCompositionSpec (internal/services/name_composition_spec.go): Validate
(write: known artifact + segments reference the artifact's catalog vars)
and SanitizeForRead (read: drop unknown artifacts, drop segments with
unknown vars, clamp version) — mirrors DashboardLayoutSpec. Plus
nomen.Composition.SanitizeForRead and lowercase JSON tags on the nomen
types so the stored shape is stable.
- Resolution: resolveComposition(artifact, overrides) returns the user
override when present + valid, else the system default. The firm slot
(PRD §3.1) is reserved for Slice 5 — system is the fallback below user
here. Wired into BOTH artifacts: the draft-title via the create path
(newDraftName → autoNameFor{Project,NonProject}) and the .docx filename
via RenderSubmissionFilenameFor in the three download handlers.
AutoSubmissionTitle / RenderSubmissionFilename stay as the nil-override
(system-default) references the #155/354 matrices pin.
- Per-document keyword override generalised: writes now land at
composer_meta.name_overrides.keyword (the general {var:value} shape);
reads honour both that and the legacy composer_meta.filename_keyword
(back-compat). The read moved to services.SubmissionFilenameKeyword
(handlers delegate) so it is live-testable.
BACK-COMPAT FINDING: the one shipped filename_keyword row needs no data
migration — the read resolves legacy composer_meta.filename_keyword as
name_overrides.keyword. Verified by a live round-trip (seed legacy jsonb →
Get → SubmissionFilenameKeyword == the legacy value).
Verified (TEST_DATABASE_URL): (a) name_compositions Set/Get round-trip +
write-time Validate rejection of an out-of-catalog variable; (b) a user
override beats the system default for the title (through Create:
'Klageerwiderung <date>' vs default '<date> Klageerwiderung') and the
filename (through RenderSubmissionFilenameFor), with the nil-override
default unchanged; (c) legacy filename_keyword back-compat read. Plus unit
tests for nomen.SanitizeForRead, NameCompositionSpec Validate/Sanitize/
resolve, and the keyword back-compat read. go vet + go test ./... (15
pkgs) + bun build clean. No Playwright (no UI this slice).
|
|||
| e10b5e6546 |
feat(submissions): non-project drafts get a date-first name (t-paliad-356 Slice 2)
PRD §6. Project-less drafts no longer fall back to the bare 'Entwurf N' counter — they render the submission_draft_title artifact through the nomen engine, leading with the date like project drafts do (m's ask). newDraftName's non-project branch now resolves a keyword (document type) from the draft's submission_code and renders '<date> <keyword>', e.g. '2026-06-01 Klageerwiderung'. When the code has no published filing rule the keyword degrades to the localized 'Entwurf'/'Draft' word, yielding '<date> Entwurf'. Collisions get the usual ' (N)' suffix via uniqueDraftName. FLAG resolved: project-less drafts DO carry a required submission_code (the global-create handler rejects an empty code), and submission_code -> rule name is a function across the published filing rules (the code encodes the proceeding, e.g. de.inf.lg.erwidg -> Klageerwiderung), so the keyword resolves project-independently via a LIMIT 1 catalog lookup. Both branches (rule-name / Entwurf fallback) flow through the same composition. The keyword is a new, normally-omitted segment on the title composition; project drafts leave it empty so they render identically to #155 — the regression guard. The identity trio and the keyword are mutually exclusive by construction (project => trio, no project => keyword). Verification: PRIMARY gate is a service-level live test (TEST_DATABASE_URL) asserting Create(projectID=nil) yields '<date> <keyword>', the ' (2)' collision suffix, EN document type, and the '<date> Entwurf'/'Draft' fallback — all green against the real DB. Existing project-title matrix (TestAutoSubmissionTitle) unchanged. go vet + go test ./... + bun build clean. Browser (Playwright) verification not meaningful pre-merge: S2 is not yet deployed (only S1/cd3f784 is) and the naming is server-side in Create behind the shared-auth login wall. |
|||
| 4920328b09 |
feat(nomen): name-composition engine + fold in the two shipped schemes (t-paliad-356 Slice 1)
Slice 1 of the filename-generator train (PRD 2026-06-01 §8). Pure refactor behind byte-equality — no user-visible change. pkg/nomen: the reusable engine. A Composition (ordered Segments, each with a trailing separator, optional wrap, and an omit/placeholder/literal missing-rule) renders against a VarResolver and a RenderTarget. Targets split into SanitiseValue (per-variable) + Finalise (whole-string + suffix) so a human title and a sanitised filename are two targets of one composition. VarCatalog + Validate guard stored compositions. internal/services/namegen.go: paliad-side wiring — the two seed system- default compositions that reproduce AutoSubmissionTitle (#155) and submissionFileName (354) as DATA, their variable catalogs, the resolvers (built from the existing submission_autoname helpers), and the artifact registry binding artifact -> catalog -> target -> default. Repointed call-sites: AutoSubmissionTitle and handlers.submissionFileName are now thin wrappers rendering through the registry; the assembly logic lives in the engine. Removed the hardcoded title/filename assembly and the handler's Az.-folgt const (now the case_number segment's placeholder). FLAG resolved (separators): the PRD sketched LEADING separators; that can't reproduce #155's client-absent case (date must join forum with a space while forum->opponent stays ' ./. '). Switched to TRAILING separators (owned by the left segment) — the minimal faithful fix. PRD §2.1 annotated. FLAG resolved (back-compat): the shipped composer_meta.filename_keyword override still flows through the engine — live round-trip test green. Acceptance: all existing #155/354 test matrices pass UNCHANGED (the byte-equality gate); new pkg/nomen unit tests cover trailing-sep, the three missing-rules, targets, and Validate; namegen_test validates the seeds against their catalogs. go vet + go test ./... + bun build all clean. |
|||
| d834b36313 | test(submissions): live-DB round-trip for filename_keyword composer_meta merge/clear (t-paliad-354) | |||
| 4092c889c4 |
feat(submissions): generated-doc filename <date> <keyword> (<case>) + user-replaceable keyword
Generated documents now download as "YYYY-MM-DD keyword (case number).docx" (date first/sortable, case number bracketed) instead of the old "rule-case-date.docx" shape. - submissionFileName: date-led frame; keyword = user override > lang-aware rule name > "submission"; case number always bracketed, placeholder "Az. folgt" (named const) when the project has no Aktenzeichen. - SanitiseSubmissionFileName hardened to fold the full Windows-reserved set (colon star question angle pipe) on top of slash/backslash, while preserving spaces + parentheses so the assembled frame stays human-facing yet filesystem-safe. - User-replaceable keyword stored in the draft's composer_meta jsonb (filename_keyword, no migration). Editor gains a "Stichwort (Dateiname)" input that placeholders the auto rule name and persists via the draft PATCH path. One-click /generate has no draft row -> keeps auto keyword. Tests: submissionFileName (full / no-AZ / override / EN / slash case-no / blank override / empty rule), submissionFilenameKeyword, extended sanitiser cases. t-paliad-354 |
|||
| f292338919 |
feat(submissions): auto-name new drafts <date> <client>./.<forum>./.<opponent> (m/paliad#155)
New project-bound submission drafts now default to a sortable, legal-
convention title instead of the bare "Entwurf N" counter:
<YYYY-MM-DD> <ClientName> ./. <ForumShort> ./. <OpponentName>
- Date leads (ISO, Europe/Berlin) so drafts list chronologically; " ./. "
is the German legal "gegen" separator.
- Client = root 'client' ancestor of the project tree.
- Forum = proceeding-type jurisdiction (UPC/EPA/DPMA); German proceedings
resolve to the deciding court (LG/OLG/BGH/BPatG) from the code tail.
- Opponent = primary opposing party, picked by our_side posture
(active → defendant bucket, reactive → claimant bucket).
- Any segment that resolves empty is omitted with its leading separator;
a project-less draft keeps the legacy "Entwurf N" scheme entirely.
- Create-time only: existing drafts are never renamed, and a lawyer's
later manual rename via Update is untouched. Same-slot collisions
de-duplicate with a " (N)" suffix.
Customization scope (per-user / firm / template, issue #155 Q4) is v1.1 —
the template is hardcoded in submission_autoname.go for now; the override
string is documented as the single extension point on AutoSubmissionTitle.
Example output:
full: 2026-05-31 Bayer AG ./. UPC ./. Novartis Pharma
no opponent: 2026-05-31 Bayer AG ./. BPatG
no forum: 2026-05-31 Bayer AG ./. Novartis Pharma
date only: 2026-05-31
AutoSubmissionTitle + segment resolvers are pure and table-tested
(submission_autoname_test.go); the Create flow is covered end-to-end
against real Postgres in submission_draft_autoname_live_test.go (gated
on TEST_DATABASE_URL).
|
|||
| d913f4fc30 |
feat(builder): B5 — share + promote-to-project wizard (t-paliad-350)
Litigation Builder slice B5 (m/paliad#153 PRD §2.4 + §2.5 + §5.4 + §10). Backend (internal/services/scenario_builder_service.go): - ListSharedWithMe — scenarios shared read-only with the caller (the "Geteilt mit mir" bucket). - PromoteScenario — transactional promote-to-project (PRD §10, no partial promotions). One Postgres tx: INSERT paliad.projects ('case', origin_scenario_id, proceeding_type_id + scenario_flags from the primary triplet) → creator team lead + wizard-selected colleagues → parties → deadlines (filed→completed, planned→pending with computed/actual date, skipped→none) → flip scenario to 'promoted' + promoted_project_id. The primary top-level proceeding + its spawned descendants form the one case file; additional standalone proceedings are reported via ProceedingsSkipped and stay in the scenario. Planned dates come from the injected FristenrechnerService.Calculate; court-set/undated planned events are skipped + counted. - NewScenarioBuilderService gains a *FristenrechnerService dep (wired in cmd/server/main.go; nil in tests that don't promote). Handlers/routes: - GET /api/builder/scenarios/shared, POST /api/builder/scenarios/{id}/promote. Frontend: - builder-shares.ts — share modal (HLC user picker + current-shares list + revoke). - builder-promote.ts — 3-step wizard (Bestätigen → Parteien ergänzen → Akte-Metadaten) → POST /promote → navigate to /projects/{id}. - builder.ts — bucketed side panel (Aktiv / Geteilt mit mir / Als Projekt angelegt / Archiviert), read-only chrome (watermark + locked affordances) for shared/promoted scenarios, wired share + promote buttons, deep-link auto-load now covers shared scenarios. - procedures.tsx — enabled buttons, bucket containers, readonly watermark slot. - global.css — modal scaffold, share UI, promote wizard, buckets, readonly state. i18n.ts + i18n-keys.ts — DE+EN keys. Tests: TestScenarioBuilderPromote (live-DB) pins the transactional cascade + readonly-after-promote + re-promote rejection. go build/vet/test + bun build clean. Verified end-to-end via Playwright: Journey E (share → 2nd user read-only watermark + locked canvas, incl. deep-link) and Journey D (promote wizard 3 steps → project created with party → navigate → scenario flipped to promoted). |
|||
| b746ec36c7 |
feat(docforge): slice 7 — generation on uploaded templates (t-paliad-349)
A submission draft can now render from an uploaded docforge template
instead of a legacy Gitea base. DB-VERIFIED against TEST_DATABASE_URL (the
head greenlit option C) before commit — not just compiled.
Schema: migration 159 adds submission_drafts.template_version_id (nullable,
FK template_versions ON DELETE SET NULL) — the snapshot pin (PRD A3). A
later template edit creates a new version; the pinned draft keeps rendering
its version.
Draft service: TemplateVersionID on the model + draftColumns + the JOIN
list + DraftPatch (two-level pointer like base_id) + Update SET. Column-sync
verified live (Create_seeds_section_rows + the new pin test both pass).
Export/preview (handlers): a template-version path checked FIRST — load the
carrier via TemplateStore.GetVersion, render via the existing Export/
RenderPreview (the carrier already carries {{slots}}; no Composer/sections
needed). Falls through to base_id / v1 if the pin is missing. Both preview
sites + the view assembly branch on it.
Store: TemplateMeta.VersionID exposes the current version's row id (slice-4
gap — a consumer needs it to pin); populated in List/Get/GetVersion + the
authoring JSON. New GET /api/templates (authenticated, firm-filtered) is the
picker list any lawyer reads; admin authoring endpoints stay gated.
Frontend: the submission editor's base picker now offers uploaded templates
as a 'tpl:<version_id>' optgroup; selecting one PATCHes template_version_id
(clearing base_id) and vice versa — mutually exclusive render paths.
Live test (submission_draft_template_live_test.go, gated): pin round-trips
Update→Get, the uploaded carrier renders ({{firm.name}}→HLC via Export), and
clearing nulls it — all PASS against real Postgres.
Verification: go build/vet/gofmt clean; bun build + bun test 274/274; slice-7
+ slice-4 store + draft/composer live tests PASS against TEST_DATABASE_URL.
Pre-existing env failures (approval/projection seed $1-type quirk,
migration136 stale deadline_rules table) are unrelated — confirmed my branch
touches none of that code.
m/paliad#157
|
|||
| b8709b903d |
feat(docforge): slice 5 — docforge-editor pkg + variable catalogue SSOT (t-paliad-349)
Establish the shared frontend editor package and make the Go resolvers the
single source of truth for variable labels.
Go — catalogue SSOT:
- VariableResolver gains Keys() []VariableKey; ResolverSet gains
Catalogue(). The 7 submission resolvers implement Keys() with the
bilingual labels ported from the TS VARIABLE_LABELS table (incl. the
legacy rule.* aliases). Keys() is entity-independent, so
SubmissionVariableCatalogue() builds a metadata-only ResolverSet.
- GET /api/docforge/variables serves the catalogue (auth-gated, static).
- Tests: docforge ResolverSet (BuildBag merge + Catalogue order) and the
submission catalogue integrity (no dupes, labels present, spot-checks).
Frontend — frontend/src/lib/docforge-editor/ (new shared package):
- dom.ts: escapeHtml + cssEscape (pure), with bun tests. Dedupes the two
identical escapeHtml/escapeHTML copies + the cssEscape copy that lived
in the submission editor.
- catalogue.ts: fetchVariableCatalogue() + labelMap() — the client for
the Go catalogue.
- submission-draft.ts now imports escapeHtml/cssEscape from the lib and
fetches the catalogue on boot into state.varLabels (labelFor reads it,
falling back to the raw key if the fetch fails — graceful degrade). The
hardcoded VARIABLE_LABELS table is removed; VARIABLE_GROUPS stays
(presentation: which keys to show + how to section them, legitimately
frontend).
Scope note: the DOM-coupled editor plumbing (wireDraftVars/focus
preservation/autosave debounce) is extracted in slice 6 alongside its first
reuse — the authoring page — rather than speculatively now (extract with the
consumer; same principle as slices 2-3). Slice 5 lands the pure utilities +
the catalogue, which the slice-6 authoring palette consumes.
Verification: go build/vet/test green (Go files gofmt-clean; handlers.go
pre-existing drift, added region clean); bun run build.ts clean;
bun test 274/274 (incl. 5 new docforge-editor tests).
m/paliad#157
|
|||
| 47deeaf5ed |
feat(docforge): slice 4 — template tables + TemplateStore (t-paliad-349)
Persistence foundation for authoring (slice 6) + generation-on-templates
(slice 7). docforge owns no tables — it defines the contract; paliad
implements it (litigationplanner pattern).
Migration 158_docforge_templates (additive, generic — NOT submission_*-named
so a second docforge consumer reuses it):
- templates — catalog row; current_version_id pins the live
version (FK added post-create to break the
templates<->versions cycle; ON DELETE SET NULL).
- template_versions — immutable snapshots; carrier .docx in a bytea
column (the TemplateStore bytea backend) + stylemap
jsonb. Versioning = snapshot-at-create (PRD A3).
- template_slots — variable slots per version; anchor = sentinel token
locating the slot in the carrier OOXML (PRD §5
lean), slot_key = the bound variable.
RLS mirrors submission_bases: firm-shared SELECT for authenticated,
mutations admin-only + gated in Go (no mutation policy = denied).
docforge root: TemplateStore interface + neutral types (TemplateMeta,
Template, TemplateSlot, *Input, TemplateFilter) + ErrTemplateNotFound.
CarrierBytes is format-opaque []byte so the root never imports the docx
adapter; the exporter wraps (CarrierBytes, Stylemap) into a docx.Carrier.
paliad: PgTemplateStore (sqlx, follows the submission_base_service pattern):
List / Get (current version) / GetVersion (pinned snapshot) / Create
(version 1 + pin) / AddVersion (next version + re-pin), all transactional.
Gated live round-trip test (TEST_DATABASE_URL) covers carrier+stylemap+slot
round-trip and the version bump. No handler wires this yet (PRD: no UI in
slice 4).
Verification: go build ./... clean, go vet clean, gofmt clean, full module
test green, migration NoDuplicateSlot structural test green.
m/paliad#157
|
|||
| 8ea78fd376 |
refactor(docforge): slice 3 — VariableResolver interface + ResolverSet (t-paliad-349)
Move the variable-bag contract (PlaceholderMap, MissingPlaceholderFn,
DefaultMissingMarker) up to the pkg/docforge root (placeholder.go) — it is
format-neutral, consumed by the resolver layer and any future exporter.
The {{key}} substitution grammar (placeholderRegex, PUA preview sentinels,
replacePlaceholders) stays in pkg/docforge/docx: it is the .docx renderer's
own machinery, not a root concern.
New at the root (vars.go):
- VariableResolver{Namespace() string; Populate(bag PlaceholderMap)} —
a PUSH interface, deliberately not pull Resolve(key): some namespaces
emit a data-dependent key set (parties.claimant.0.name, .1.name, … one
per party) that a fixed key-by-key pull can't enumerate.
- ResolverSet + BuildBag() — composes resolvers into one bag, replacing
the hard-coded addFooVars-then-addBarVars sequencing in Build.
paliad side (submission_vars_resolvers.go): seven resolver types wrap the
UNCHANGED addXxxVars push-builders (firm/today/user/procedural_event/
project/parties/deadline), each capturing the entity it needs. The builder
bodies are byte-for-byte untouched, so the bag is identical by
construction; SubmissionVarsService.Build now wires the applicable
resolvers and calls ResolverSet.BuildBag(). Resolvers stay in paliad
because they read paliad's domain model; a second docforge consumer plugs
its own resolvers into a ResolverSet the same way.
Keys()/Catalogue() (the static key list that will data-drive the authoring
palette + kill the hardcoded VARIABLE_GROUPS in submission-draft.ts) is
deferred to the UI slice that consumes it, sourced from the frontend's
existing labels — building it now, ahead of its consumer, would be
speculative (PRD §4 B3 principle).
Verification: go build ./... clean, go vet clean, full module test green.
Alias-parity (procedural_event ≡ rule) and party-form tests pass unchanged
= bag byte-identical.
m/paliad#157
|
|||
| f8067c2fe5 |
refactor(docforge): slice 2 — composer to pkg/docforge/docx + Carrier (t-paliad-349)
Move the full compose pipeline (anchor-pair splicing, append-before-sectPr,
hyperlink-rels patching, zip split/repack, final placeholder pass) into
pkg/docforge/docx/compose.go, decoupled from paliad's DB row types. The
engine now owns the entire .docx assembly.
New neutral types in docx:
- Carrier{Bytes, Stylemap} — the opaque base .docx, preserved
byte-for-byte outside the spliced regions (the lossless docforge
carrier for .docx).
- Section{Key, OrderIndex, Included, ContentMDDE, ContentMDEN} — the
format-neutral content input.
- Composer / NewComposer / ComposeOptions on those neutral types.
internal/services keeps SubmissionComposer + ComposeOptions as a thin
mapping wrapper (SubmissionSection -> docx.Section, Base.SectionSpec.Stylemap
+ BaseBytes -> docx.Carrier). handlers + the comprehensive compose_test are
unchanged; the test drives the wrapper end-to-end and its byte-exact OOXML
assertions pass = behaviour preserved.
Retired the slice-1 docx.XMLAttrEscape wrapper + its services forwarder:
compose now calls the local xmlAttrEscape inside the docx package.
Sequencing note: the paragraph-level neutral model (Document/Block/Slot the
PRD §3.2 sketches) is deferred to slice 6, where the authoring importer +
format exporters consume it. Building it now, ahead of any consumer, would
be speculative and risk the byte-identical guarantee for no gain (PRD §4 B3
principle). Carrier is the part of the model that earns its keep this cycle.
Verification: go build ./... clean, go vet clean, full module test green.
m/paliad#157
|
|||
| 78a30a7ee0 |
refactor(docforge): slice 1 — extract .docx engine to pkg/docforge/docx (t-paliad-349)
Relocate the in-house OOXML machinery out of internal/services into the
first docforge adapter, with zero behaviour change:
submission_merge.go -> pkg/docforge/docx/merge.go (placeholder
substitution renderer + preview-HTML emitter)
submission_md.go -> pkg/docforge/docx/markdown.go (Markdown->OOXML
walker incl. the
|
|||
| 9679a98666 |
feat(builder): B4 — Akte mode + project-backed scenarios (m/paliad#153)
PRD §2.3 + §10. Implements the dual-write rule (load-bearing complexity per PRD §10): project-backed scenarios mirror flag toggles to paliad.projects.scenario_flags and filed event states to paliad.deadlines, while kontextfrei scenarios continue writing only to paliad.scenario_events. Visible affordances: page-header Akte picker, enabled "Aus Akte" mode tab, Akte banner on the project-backed canvas, cross-surface scenario-flag-changed dispatch + listener for live peer-surface coherence. Backend - ScenarioBuilderService takes ProjectService + ScenarioFlagsService deps so dual-write hits live tables. - CreateScenarioFromProject seeds a scenario from a project: copies proceeding_type_id + scenario_flags, normalises our_side to the builder's binary claimant|defendant axis, surfaces existing rule-bound deadlines as scenario_events (filed when completed, planned otherwise). - PatchProceeding on a project-backed top-level triplet dual-writes scenario_flags to projects.scenario_flags via flagDeltaFromBuilder. - PatchEvent transitioning to state='filed' on a project-backed scenario upserts paliad.deadlines (status='completed', completed_ at, source='rule') inside the same tx as the scenario_events UPDATE — canvas and project surfaces never diverge mid-flight. - POST /api/builder/scenarios/from-project handler wires the entry point. Frontend - builder-akte.ts: project list fetch + dropdown render, Akte banner, createScenarioFromProject POST helper. - builder.ts: mode branching — picking an Akte (search hit or page-header pick) creates a project-backed scenario and loads it; loaded scenarios reflect their origin_project_id on the picker + banner; flag toggles on Akte-backed top-level triplets dispatch scenario-flag-changed so the Verfahrensablauf strip / project surfaces refresh; the builder listens to inbound scenario-flag- changed and refetches its scenario when the changed project matches origin_project_id. - procedures.tsx: enable the previously-disabled Aus Akte tab. - i18n + CSS: builder.akte.banner.prefix key (DE+EN); lime-tinted banner styling. Tests - TestScenarioBuilderAkteDualWrite (live DB) pins the dual-write contract: Akte flag toggle → projects.scenario_flags updated, Akte filed event → deadlines row inserted; kontextfrei flag toggle leaves projects.scenario_flags untouched, kontextfrei filed event leaves deadlines untouched. - Existing TestScenarioBuilderService passes against the new signature (nil deps short-circuit dual-write paths). Verification: go test ./... + go vet ./... + bun run build all clean. Playwright smoke against the static dist build confirms the Akte tab + picker render correctly, fetchAkteProjects fires on mount, and the scenario-flag-changed CustomEvent dispatches + receives without runtime errors. t-paliad-347 |
|||
| 1c77cb6e67 |
fix(builder): surface proceeding_type id so add-proceeding POST works (t-paliad-345)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / build (pull_request) Has been cancelled
Paliad CI gate / test-go (pull_request) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Paliad CI gate / deploy (pull_request) Has been cancelled
The Litigation Builder's "+ Verfahren hinzufügen" silently failed in
prod after t-paliad-343 B2 shipped — clicking a Verfahren chip in the
picker did nothing, no visible error.
Root cause: the wire shape FristenrechnerType (the response of
/api/tools/proceeding-types) carried code+name+nameEN+group but not
id. Builder.ts mountAddProceedingPicker's callback POSTed
`{proceeding_type_id: meta.id}` to
/api/builder/scenarios/{id}/proceedings — meta.id was undefined,
JSON.stringify dropped the key, the server returned 400 ("invalid
input: proceeding_type_id is required"), and fetchJSON swallowed the
error to console. The user saw "nothing happens".
Fix:
- Add `ID int json:"id"` to lp.FristenrechnerType.
- SELECT id in FristenrechnerService.ListProceedings + Scan into the
new field.
- Defensive guard in builder.ts openAddProceedingPicker — refuse to
POST without a positive integer id and log a clear error, so a
future wire-shape regression cannot recreate the silent-fail.
- Regression test in pkg/litigationplanner/types_wire_test.go pins the
contract (id present in JSON, round-trips as integer).
Side-benefit: fristenrechner-wizard.ts:599-628 documented this exact
gap as a known limitation ("S5/follow-up can extend the wire shape to
include id"). That workaround can now be retired in a follow-up.
Refs m/paliad#153 (Litigation Builder)
|
|||
| a4b865d6bd |
fix(builder): initialise scenario sub-arrays + client null-guard (t-paliad-344)
GetScenarioDeep returned nil slices for proceedings/events/shares when a scenario had zero rows, which Go's encoding/json serialises as `null` rather than `[]`. The builder's renderCanvas then unconditionally calls `state.active.proceedings.filter(...)` on a null and dies with `procedures.js:101 TypeError: Cannot read properties of null (reading 'filter')` — every cold-open scenario crashed the page before the empty canvas could render. Backend (root cause): initialise Proceedings / Events / Shares to empty slices in BuilderScenarioDeep before SelectContext, so the wire shape is always arrays. Existing rows still load via SelectContext, which truncates the placeholder and refills from the DB. Frontend (defence in depth): on loadScenario(), normalise each of the three arrays to `[]` if the server response is not an array. Catches a future regression (or an older deployed build) without re-introducing the same crash class. bun build clean, go vet + go test ./... green. |
|||
| a905911cf4 |
fix(deadlines): restore /api/events deadline rail after mig 140 column drop (t-paliad-344)
Two SELECTs still referenced paliad.deadlines.rule_id after mig 140
(Slice B.4) dropped that column in favour of sequencing_rule_id:
- internal/services/deadline_service.go:268 — DeadlineService.
ListVisibleForUser. Powers /api/events?type=deadline (dashboard
deadline rail, /deadlines page, every status bucket). Threw
`pq: column f.rule_id does not exist` on every request → 500
for any authenticated user hitting the dashboard.
- internal/services/projection_service.go:1250 — collectActualsForOverrides.
Same column on `paliad.deadlines d`. Logged once per projection
pass (`ERROR service: projection: deadlines: ...`); aliased the
rename to `rule_id` so the receiving struct tag still scans.
Live container logs confirmed the failure mode — a 60-row burst of
`pq: column f.rule_id does not exist at position 3:36 (42703)` starting
the minute the post-B0 container came up (mig 140 had applied to the
DB but the SELECT still used the dropped name). EXPLAIN against the
live schema after the edit plans cleanly; the LEFT JOIN to
paliad.deadline_rules_unified on sequencing_rule_id was already correct
(only the SELECT projection was stale).
Root cause: mig 140 commit (
|
|||
| b78a984a7c |
fix(submission-md): preserve {{...}} placeholders verbatim through inline scanner
The Markdown inline scanner (parseInlineSpans) treats _ and * as
italic delimiters. A placeholder like {{project.case_number}} fed
through the scanner had its underscores consumed as italic markers,
leaving {{project.casenumber}} in the composed OOXML. The v1
placeholder pass then looked up the wrong key, surfacing
[KEIN WERT: project.casenumber] in the preview. The form ↔ preview
highlighting also stopped working because data-var attributes
mismatched between the input (snake_case) and the rendered span
(stripped).
parseInlineSpans now detects {{ at the cursor and skips ahead to
the matching }}, copying the entire placeholder verbatim into the
current text run. Unmatched {{ falls through to the existing
character handling so legal prose with stray braces still renders.
Tests: regression test for underscored keys (single + multiple +
mixed-with-italics), direct guard on parseInlineSpans, and an
italic-around-placeholder structural test.
|
|||
| 0f3c30a647 |
feat(scenario-builder): B0 schema foundation + minimal API (m/paliad#153)
t-paliad-340 — B0 of edison's 7-slice train (PRD §7.1). DB-only: schema + RLS land, dev-only test route exercises the surface, no user-facing change. B1 wires the actual builder UI on top. Migration 157 (additive on the legacy mig-145 scenarios table — 0 rows in prod, safe to relax): - paliad.scenarios gets owner_id / status / origin_project_id / promoted_project_id / stichtag / notes. spec drops NOT NULL and the scenarios_unique_per_scope constraint drops (the builder allows multiple scratch + Unbenanntes Szenario rows per user). - New tables: scenario_proceedings, scenario_events, scenario_shares. - paliad.projects.origin_scenario_id for the promote-to-project audit trail (the FK lands now; the wizard ships in B5). - paliad.can_see_scenario(uuid) STABLE SECURITY DEFINER helper covering owner / share / global_admin / two legacy paths. - Replacement RLS on scenarios + RLS on the three new tables; legacy service + handlers stay live and unchanged. PRD §5.1 deviations called out in the migration header: - proceeding_type_id is integer (live schema), not uuid (PRD draft). - FK target is paliad.users, matching the rest of paliad's schema. Go surface: - ScenarioBuilderService — list/create/get-deep/patch scenarios, add/patch/delete proceedings, add/patch/delete events, add/delete shares. Writes wrap in transactions with set_config( paliad.audit_reason, ..., true) per event_choice_service.go pattern. - /api/builder/scenarios/* — handlers register under a builder/ prefix so the legacy /api/scenarios surface still works. - /dev/scenario-builder — single-page HTML form gated to PaliadinOwnerEmail, exercises the B0 surface without Postman. - Live-DB integration test (TEST_DATABASE_URL gated) covers create + list + deep-get + share + visibility negatives + patch. Audit-first: every DDL block ran clean via BEGIN/ROLLBACK against the live DB before commit; end-to-end sanity (insert chain + CHECK constraints + CASCADE-on-delete) verified via the Supabase MCP. bun build clean. go vet + go test -short ./... green. |
|||
| 39c8ef343b |
feat(procedures): U4 hard-cut legacy URLs + retire dual surfaces (m/paliad#151)
Per m's Q11 divergence in the design (no 2-week dual-ship), this slice flips /tools/fristenrechner and /tools/verfahrensablauf to permanent 301 redirects to /tools/procedures and deletes the legacy frontend pages. Bookmarks resolve via Location preservation of query params; no ?legacy=1 escape, no in-product affordance pointed back at the retired URLs after the merge. Server: - handleFristenrechnerPage + handleVerfahrensablaufPage now 301 to /tools/procedures, carrying any query string through unchanged. - pillDrillURL in deadline_search_service.go retargets to /tools/procedures so freshly indexed search pills land on the new page directly (cached snapshots still work via the 301). Frontend: - Deleted src/fristenrechner.tsx, src/verfahrensablauf.tsx, src/client/fristenrechner.ts. - src/client/verfahrensablauf.ts loses its DOMContentLoaded auto-boot and the now-unused initI18n / initSidebar imports; procedures.ts is the sole caller of initVerfahrensablauf(). - frontend/build.ts drops the legacy entrypoints and renderXxx HTML outputs. - Sidebar.tsx, Header.tsx, index.tsx, paliadin-context.ts repointed to /tools/procedures. - Unused nav.fristenrechner / nav.verfahrensablauf / tools.verfahrensablauf.* i18n keys removed. Tests: - verfahrensablauf_test.go rewritten to assert both legacy URLs return 301 with the correct Location (query string preserved). |
|||
| 9940dd8216 |
feat(deadline-system): P2 — condition_expr write-validator (m/paliad#149)
Phase 2 P2 (design §4.1). Locks the condition_expr grammar to:
CondExpr := { "flag": "<known_flag>" }
| { "op": "and"|"or", "args": [<CondExpr>, ...] }
Where <known_flag> must exist in paliad.scenario_flag_catalog (today:
with_ccr / with_amend / with_cci; editorial adds via the catalog
table as needed).
Wire-time validation in RuleEditorService.Create and UpdateDraft —
the rule editor surfaces a 400 with a friendly message before the row
hits the DB. Empty / JSON null inputs pass through (the "no gate"
shape; stored as NULL column).
The validator:
* walks the JSON tree once, collecting every leaf flag name
* rejects mutually-exclusive shapes (leaf + composite in one node)
* rejects empty args, bad op values, empty flag strings
* does ONE batch lookup of the collected leaf names against the
catalog (regardless of expression depth)
Tests:
* 9 shape-only unit tests covering every reject path (no DB needed)
* TestValidateConditionExpr_LiveCatalog covers 6 good shapes + 2
unknown-flag cases against the live catalog
* TestConditionExpr_AllLiveRowsValidate runs the validator over
every active+published condition_expr in paliad.sequencing_rules
to enforce the §4.1 invariant on every deploy (today's 18 rows
all conform — verified via Supabase MCP pre-flight)
Live-DB tests skip cleanly when TEST_DATABASE_URL is unset (same
posture as sibling live tests in this package).
Design: docs/design-deadline-system-revision-2026-05-27.md §4.1
(grammar formalisation). t-paliad-331.
|
|||
| 3a4e99cb92 |
feat(deadline-system): P1 — upc.apl re-split into merits/cost/order (m/paliad#149)
Phase 2 P1 / m's Q5 divergence (2026-05-27, verbatim):
"Reverse the unification as suggested in 3. They are different
proceedings, I only wanted the approach to be unified in the
'determinator' — but they are actually different proceedings!"
Mig 155 reverts the mig-096 unification:
Before: id=160 upc.apl.unified active (16 rules), id=11/19/20 inactive
After: id=11 upc.apl.merits (7 rules), id=19 upc.apl.cost (2 rules),
id=20 upc.apl.order (7 rules) all active; id=160 inactive
The 16 rules under id=160 split cleanly by event_code prefix; all 10
parent_id edges among them are bucket-local (pre-flight audit), so
the tree shape survives the rebind unchanged.
Spawn FK retarget: pi.cfi.appeal_spawn flips from 11 (merits) → 20
(orders track) per design §3.1 — PI appeals land on orders, not
merits. The inf/rev/dmgs spawns keep target=11 (merits), now active.
Determinator routing layer (proceeding_mapping.go) keeps its single
"Berufung" front door per m's intent — only the data shape changes.
Pre-flight verified: 0 projects bound to id=160, 0 scenarios reference
upc.apl. Zero data migration on the project side.
Tests: lookup_events_test.go assertions on the three appeal_target
buckets updated to the new codes (endentscheidung → upc.apl.merits,
schadensbemessung → upc.apl.merits, bucheinsicht → upc.apl.order).
Same rule set, post-split coordinates.
Snapshot regen (pkg/litigationplanner/embedded/upc/) deferred: the
current snapshot only contains inf+rev so the apl re-split doesn't
shift its contents; regenerating would surface unrelated active PTs
and pollute this slice. Tracked as a follow-up.
Verified: go vet clean, go test ./internal/services/... -run
LookupEvents|proceeding_codes clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §3.1
(re-split mig), §1.3 (spawn graph post-Q5). t-paliad-331.
|
|||
| 3533d79a25 |
feat(deadline-system): S1+S1a — cross-party display + spawn-only picker (m/paliad#149)
Phase 2 S1 + S1a (pre-ratified from t-paliad-327, folded into the
Phase 2 train).
S1 — Cross-party display:
- FristenrechnerService.LookupFollowUps stops filtering by party
server-side; queryFollowUpRows drops the perspective WHERE clause
and returns every published+active child.
- Server now computes is_cross_party per row (true only when
perspective ∈ {claimant,defendant} AND primary_party is the
opposite side; NULL/both/court is never cross-party).
- FollowUpRule wire shape gains the boolean.
- Frontend renderRule adds a "Gegenseitig" badge + is-cross-party
row class (muted styling, disabled checkbox affordance).
- defaultChecked returns false for cross-party rows.
- countSelected + submitWriteBack skip cross-party rows
unconditionally — even if a user manually checks the box, they
describe opposing-side filings and don't belong in our Akte set
(design §2.4 write-back exclusion).
- i18n: deadlines.overhaul.crossparty.badge / .tooltip (DE+EN).
- CSS: .fristen-overhaul-rule-crossparty + .is-cross-party row
modifier.
S1a — Spawn-only picker filter:
- SearchEvents WHERE now adds `sr.is_spawn = false` so spawn rules
(e.g. appeal_spawn, the inf.cfi → upc.apl.merits hop) no longer
surface as picker hits. Spawn rules are consequences, not
triggers — a lawyer searching "Berufung" wants the appeal-tree
root, not the inf.cfi spawn link.
- Terminal leaves (Duplik etc.) stay pickable per design §2.2's
carve-out: their own anchor is non-spawn, so they surface and
render an honest empty follow-up list.
Honest UX: hiding cross-party follow-ups lied about what the
workflow does next (cf. RoP.029.d falling off when perspective=
claimant on def_to_ccr — the workflow continues, just on the
defendant's docket). The fix makes the data legible without
contaminating the write-back path.
Verified: go vet clean, bun build clean, bun test 256/256,
go test ./internal/services/... -run LookupFollowUps... clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.4
(cross-party) + §2.2 (spawn-only picker). t-paliad-331.
|
|||
| d36cc9ee15 |
feat(deadline-system): P0 — per-project scenario_flags SSoT (m/paliad#149)
Phase 2 P0 of the deadline + procedural-events revision. Establishes
paliad.projects.scenario_flags (jsonb) + paliad.scenario_flag_catalog as
the single source of truth for per-project scenario state — replacing
the three fragmented stores athena flagged (project_event_choices,
scenarios.spec, DOM-only). All three were empty per the audit so no
data migration is needed.
The jsonb map carries two key shapes:
* named flags (whitelist via scenario_flag_catalog) — today
with_ccr / with_amend / with_cci
* per-rule selection deviations of shape "rule:<uuid>" — wired up
here for validation; the consumer UI lands in P3
Endpoints:
GET /api/projects/{id}/scenario-flags
PATCH /api/projects/{id}/scenario-flags
PATCH semantics: bool = write; null = delete (priority-driven default
returns); missing key = leave alone. The service validates every key
on write (catalog lookup + UUID rule-membership + mandatory-cannot-be-
deselected) before persisting, so a single bad key fails the whole
patch.
Frontend bind: new scenario-flags.ts client module + Mode B's flag
checkboxes (ccr-flag / inf-amend-flag / rev-amend-flag / rev-cci-flag)
now hydrate from / persist to the project's scenario_flags on every
toggle. Kontextfrei (no project) is unchanged. Cross-surface coherence
via a scenario-flag-changed CustomEvent (peer surfaces — Verfahrens-
ablauf strip, Mode B result-view — will subscribe in P3).
Mig 154 is audit-defensive (set_config of paliad.audit_reason); no
audit trigger fires on paliad.projects today but a future one will
inherit the reason. Seeds the three known flags. CHECK constraints
enforce the top-level shape (jsonb_typeof = 'object') and the
catalog key pattern (lowercase, not 'rule:%' prefix).
Verified against the live DB: 18 projects default to '{}', catalog
has 3 rows, applied_migrations advanced to 154.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.3, §2.4a,
§4.1, §5 (P0 row). t-paliad-331.
|
|||
| 70985d88b0 |
feat(fristenrechner): Slice S4 — Mode B wizard (m/paliad#146)
Mode B "🧭 Geführt" — the guided 3-5 row wizard defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §3.2. Lands the
user on a single procedural_event (the trigger), then transitions
to the shared §4 result view.
Frontend:
* `fristenrechner-wizard.ts` — row stack with R1..R5:
R1 Was ist passiert? (event_kind, always asked)
R2 Vor welchem Gericht? (jurisdiction, skip if R1 narrows)
R3 In welchem Verfahren? (proceeding_type, auto-skip when
narrowed pool has 1 option)
R4 Welches Schriftstück? (procedural_event, landing)
R5 Welche Seite vertreten Sie? (party, only when follow-ups
differ by primary_party)
Row badges per §11.Q3: R1+R2 = Filter, R3+R4+R5 = Qualifier.
R5 has NO "Beide" option per §11.Q8 — Mode B is the file-mode
where perspective is a qualifier.
* Project prefill — derives R3 + R2 jurisdiction from
project.proceeding_type, R5 from project.our_side. Annotates
pre-filled rows with "aus Akte" tag and implicit rows with
"implizit" tag per §11.Q10 ("erhalten" annotation when a pick is
carried across an upstream change).
* R4-to-result transition — after R4 the wizard fetches /follow-
ups (no dates) to inspect primary_party variance. If both
claimant and defendant rules exist AND R5 isn't already set,
swaps the loading row for the R5 chip picker. Otherwise jumps
straight to mountResultView.
* URL state — `?mode=wizard&kind=…&forum=…&pt=…&r4=…&party=…`
keeps deep-link / back-nav consistent (the launchResult step
sets `event=` so the result view picks up).
* `fristenrechner-result.ts` mountModeShell now dispatches the
"wizard" tab to the wizard module (was a coming-soon
placeholder).
* 18 i18n keys added (DE + EN parity), 145-line CSS block for the
wizard row stack with Filter / Qualifier badge styling and
"aus Akte" annotation chip.
Backend:
* `ProceedingListOptions.EventKind` adds an EXISTS subquery
filter on `paliad.sequencing_rules` ⨯ `paliad.procedural_events`
so Mode B R3 chips only show proceedings whose event roster
contains at least one event of the requested kind (design
§6.3). Endpoint param: `event_kind=` on
/api/tools/proceeding-types.
Test updates:
* `TestListProceedings` switched from SKIP-when-column-missing to
asserting the live filter — mig 153 has landed, `kind` column
is in place. New subtests: kind=proceeding includes
upc.inf.cfi and excludes the phase row upc.cfi.interim;
event_kind=filing narrows to proceedings with filing events.
* `fristenrechner-wizard.test.ts` covers
`followUpsDifferByParty` — the R5 trigger predicate. 7 cases:
asymmetric → true; uniform / both / court / empty → false.
Verified — bun build clean (2971 i18n keys), 256 frontend tests
pass (incl. 7 new), go build + vet clean, live-DB
TestListProceedings passes all 6 subtests against mig 153 data.
|
|||
| 06d6c7540e |
Merge: t-paliad-323 Slice S3 — Fristenrechner Mode A direct search (m/paliad#146)
knuth shipped S3 of the Fristenrechner overhaul (design §3.1, §7-S3):
- New frontend/src/client/fristenrechner-mode-a.ts (507 LoC) — 'Direkt suchen' UI per design §3.1: Filter strip (Forum · Verfahren · Was passierte · Partei) with section-split visual hierarchy per m §11.Q3, free-text search box, ranked result list of procedural_events with click-to-lock-as-trigger.
- Inbox channel as secondary 'Erweitert' chip per §3.3 with CMS→UPC / beA→DE forum nudge.
- Mode tabs pair (Direkt suchen / Geführt) under Step-0 per §11.Q2; wizard tab placeholder until S4.
- Backend ListProceedings(jurisdiction, kind) — kind='proceeding' filter targets mig 153's column (just merged in
|
|||
| 9d688459e3 |
feat(db): mig 153 — proceeding_types kind discriminator + ProjectService hardening
Adds a `kind` column to paliad.proceeding_types (proceeding / phase /
side_action / meta) so the Mode B R3 Fristenrechner wizard, the
projects.proceeding_type_id binding, and the pkg/litigationplanner
snapshot can filter to primary proceedings only.
Implements the ratified design from docs/design-proceeding-types-
taxonomy-2026-05-26.md (m greenlit 2026-05-27 09:57 after the 11-question
AskUserQuestion round-trip).
Mig 153 is purely additive — ADD COLUMN with a safe DEFAULT, UPDATEs
reclassify 23 non-primary rows (4 phase + 10 side_action + 9 meta), and
a BEFORE INSERT/UPDATE trigger on paliad.projects backstops the new
invariant. Pre-mig audit (Supabase MCP, 2026-05-27) confirmed zero
downstream pressure on the 23 reclassified rows.
- internal/db/migrations/153_proceeding_types_kind.up.sql + .down.sql
- snapshot to paliad.proceeding_types_pre_153 in the same TX
- set_config('paliad.audit_reason', …) defensively
- DO-block asserts 23 reclassified rows before the trigger ships
- Q9 carve-out: is_active=false on every phase/side_action/meta row
- new trigger paliad.projects_proceeding_type_kind_check on
paliad.projects.proceeding_type_id
- internal/services/project_service.go
- extend validateProceedingTypeCategory to also enforce
kind='proceeding' AND is_active=true; new typed error
ErrInvalidProceedingTypeKind
- single SELECT picks up category + kind + is_active
- internal/services/project_service_test.go
- TestProjectService_ProceedingTypeKindGuard covers service-layer
rejection, the active-but-non-proceeding edge, mig 153 trigger
backstop, and the kind='proceeding' happy path
- cmd/gen-upc-snapshot/main.go
- filter proceeding_types query to kind='proceeding' for forward-
compat (the embedded UPC snapshot JSON regen requires DATABASE_URL
access and will land in a follow-up; the current placeholder is
already empty of non-primary rows)
t-paliad-325 / m/paliad#147
|
|||
| 2a2c5b8033 |
feat(fristenrechner): Slice S3 — Mode A direct search (m/paliad#146)
Mode A "⚡ Direkt suchen" — the power-user entry path defined in docs/design-fristenrechner-overhaul-2026-05-26.md §3.1. Renders above the §4 result view; clicking a result row locks the trigger event and transitions to the shared result surface from S2. Frontend: * `fristenrechner-mode-a.ts` — filter strip (Forum / Verfahren / Was passierte / Partei) + free-text search input + result list. Section-split visual hierarchy per m §11.Q3: filter chips in a bordered "Filter (eingrenzen)" strip on top, result list below. Inbox channel chip lives behind an "Erweitert" details summary per §3.3; picking CMS / beA auto-nudges the Forum chip. Party chip retains a "Beide" option (Mode A is filter mode per §11.Q8; Mode B drops it in S4). * `fristenrechner-result.ts` — new `mountModeShell(activeTab)` renders the two mode tabs per §11.Q2 and lazy-imports Mode A. Mode B tab is a placeholder until S4 lands. * `fristenrechner.ts` boot — when `?overhaul=1` is set and `?event` is empty, mountModeShell takes over (default tab = search; `?mode= wizard` opens the wizard tab when S4 ships). With `?event=` the flow still jumps straight to the result view. URL state syncs forum / pt / kind / party / q on every chip click. * 28 i18n keys added (DE + EN parity), 310-line CSS block for the mode tabs + Mode A surface. Backend: * New `ProceedingListOptions { Jurisdiction, Kind }` + service method `ListProceedings(ctx, opts)`. Legacy `ListFristenrechnerTypes` keeps the no-filter signature for existing callers. Handler `/api/tools/proceeding-types` accepts `?jurisdiction=` and `?kind=` query params. * `kind=proceeding` filter targets the taxonomy column landed in mig 153 (parallel branch t-paliad-325, m/paliad#147). Sequenced per the taxonomy doc §7 option (c): mig 153 merges before S3 ships to main, so the filter is never false-positive (no phase / side_action / meta rows leak into the chip strip). Verified — bun build clean (2955 i18n keys, data-i18n attributes clean), 249 frontend tests pass, go build + vet clean. New TestListProceedings — 4 PASS (no-filter, jurisdiction=UPC, jurisdiction=DE, ListFristenrechnerTypes alias) + 1 SKIP for the kind=proceeding case that probes the column and skips when mig 153 hasn't landed yet. S1 + S2 live tests still green. |
|||
| 7ea415145f |
feat(fristenrechner): Slice S1 — backend ?kind=events + /follow-ups (m/paliad#146)
Two additive endpoints behind the Fristenrechner overhaul (design §6.1 + §6.2 in docs/design-fristenrechner-overhaul-2026-05-26.md): 1. GET /api/tools/fristenrechner/search?kind=events — returns procedural_events rows directly (not aggregated concept-cards), one hit per (event × proceeding_type) tuple. Trigram-ranked against name / name_en / code. Filters: jurisdiction, proc, event_kind, party. Powers Mode A's result list and Mode B's R4 landing chips. Default search shape unchanged. 2. GET /api/tools/fristenrechner/follow-ups?event=...&trigger_date=... — given a trigger event (by code or uuid) + date, returns the immediate follow-up sequencing rules with computed due dates via litigationplanner.CalculateRule. Each row carries priority / primary_party / is_court_set / is_spawn / has_condition / legal source / spawn target so the result view can group into Mandatory / Recommended / Optional / Conditional with the SPAWNED badge. party=claimant|defendant filters keep "both" rules visible. No schema changes — unified sequencing_rules already has every column needed. Live-DB tests cover the SoC follow-up shape, party narrowing, jurisdiction + event_kind filters, and the unknown- event sentinel. |
|||
| 6acb1167dd |
feat(admin): add proceeding-type column to /admin/procedural-events list (t-paliad-321 / m/paliad#144)
Surfaces the 3-segment proceeding code (e.g. upc.inf.cfi) on the admin
rules list so the 4 legitimately-distinct same-named groups are
visually disambiguated without opening each row's edit page.
Specifically helps with:
- "Antrag auf Patentänderung" × 4 (distinct proceeding_type_ids)
- "Beginn des Hauptsacheverfahrens" × 2
- "Berufungsbegründung-R.220.1" × 2
- "Berufungsschrift-R.220.1" × 2
(The 6× "Mängelbeseitigung / Zahlung" identical clones are dedup'd by
mig 152 in the sibling commit; this column lets m verify the dedupe
landed and confirms the remaining same-named groups are intentional.)
* internal/services/rule_editor_service.go —
- LoadProceedingTypeCodes(ctx, rows) — batch SELECT id, code FROM
paliad.proceeding_types WHERE id = ANY(...) for every distinct
non-NULL proceeding_type_id in rows. Returns id → code map.
Single round-trip, firm-wide reference data (no RLS / visibility
gate). Used only by the LIST endpoint; GetByID etc. don't need it.
* internal/handlers/admin_rules.go —
- adminRuleResponse gains ProceedingTypeCode *string field
(json:"proceeding_type_code,omitempty"). Populated by
wrapRuleListResponse from the id → code map.
- handleAdminListRules calls LoadProceedingTypeCodes after fetching
rows, passes the map to wrapRuleListResponse.
* frontend/src/admin-rules-list.tsx —
- Adds Proceeding column header in position 2 (between Submission
Code and Legal Citation) per paliadin's "Place between submission-
code and the existing columns" spec. Binds to canonical i18n
key admin.procedural_events.col.proceeding (added below).
- Drops the legacy Verfahrenstyp column at position 4 — the new
code-only column at position 2 replaces it; the old column
showed `code · name` which duplicates the new content.
* frontend/src/client/admin-rules-list.ts —
- Rule type gains proceeding_type_code?: string | null.
- New proceedingCodeCell(r) helper: prefers server-side
proceeding_type_code, falls back to dropdown-lookup
proceedingLabel for defense-in-depth on older API responses
(the old behaviour broke for rules whose proceeding_type_id
pointed at non-fristenrechner category proceedings; the new
column never has that bug because the join is server-side).
- Row rendering: new <td class="admin-rules-col-proceeding"><code>
proceedingCodeCell(r) </code></td> in column 2.
* frontend/src/client/i18n.ts —
- admin.procedural_events.col.proceeding alias added for DE +
EN ("Verfahren" / "Proceeding"). Mirror style of the other
canonical aliases from Slice A.
* frontend/src/i18n-keys.ts —
- Generated key union extended with
"admin.procedural_events.col.proceeding".
Build + vet clean. No new SQL — proceeding_types is firm-wide
reference data and the join uses an existing primary key.
|
|||
| bd7896ef68 |
feat(submissions): Composer Slice F — section reorder / hide / add custom (m/paliad#141)
The final Composer slice per design doc §12. Lawyer gains full
control over section composition: drag-and-drop reorder, per-section
delete, "+ Add section" picker for custom slugs that don't appear in
the base's default spec. Combined with Slice B's hide toggle, this
closes out the A→F sequence — Composer A→F is complete.
Backend (internal/services/submission_section_service.go, +120 LoC):
- SectionService.Create — adds a new section row to a draft. Validates
section_key + labels + kind (must be prose/requests/evidence).
Auto-assigns next order_index when OrderIndex=0; collisions on
(draft_id, section_key) surface as ErrInvalidInput.
- SectionService.Delete — removes one section by id. Returns
ErrSubmissionSectionNotFound when nothing was deleted.
- SectionService.Reorder — accepts a sequence of section_ids, rewrites
every row's order_index to (1..N)×10 transactionally. Returns the
refreshed list. Sections not present in the sequence are silently
ignored (defensive — partial reorder doesn't lose rows).
Handlers (internal/handlers/submission_sections.go, +180 LoC):
- POST /api/submission-drafts/{draft_id}/sections — owner-scoped via
SubmissionDraftService.Get. 400 on slug collision / invalid kind.
- DELETE /api/submission-drafts/{draft_id}/sections/{section_id} —
owner + section-belongs-to-draft cross-check. 204 on success.
- POST /api/submission-drafts/{draft_id}/sections/reorder — accepts
{"section_order": [uuid, uuid, ...]}; returns refreshed sections list.
Frontend (frontend/src/client/submission-draft.ts, +260 LoC):
- Each section row gains a drag handle (⋮⋮) on the left of the head.
Drag handle is the only draggable element; contentEditable
selections inside the editor body keep working. HTML5 native DnD,
no library.
- Drop-target highlighting via .submission-draft-section--drop-target
(border-top accent). Cleanup on dragend / drop / cancel.
- Per-section "Delete" button next to the existing Hide/Include
toggle. Confirm prompt prevents accidental loss of typed prose.
- "+ Add section" trailing affordance below the section list opens an
inline form (slug + DE label + EN label + kind dropdown). Submit
POSTs to the new endpoint; on success splices the row into
state.view.sections and re-paints.
CSS (frontend/src/styles/global.css, +65 LoC):
- .submission-draft-section-handle (grab cursor + hover background +
active=grabbing).
- .submission-draft-section--dragging / --drop-target visual states.
- .submission-draft-add-section form layout (dashed border + lime
primary submit).
Tests (internal/services/submission_section_slice_f_test.go, NEW,
TEST_DATABASE_URL-gated):
- Create custom section + slug-collision surface as ErrInvalidInput.
- Delete + repeat-delete returns ErrSubmissionSectionNotFound.
- Reorder reverses 10 seeded sections + verifies the resulting
order_index sequence is ascending and matches the input order.
Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean (2906 i18n keys, data-i18n scan clean).
Hard rules honoured:
- NO new migrations (Slice F is pure code on Slice A's schema).
- NO behavior change for pre-Composer drafts (no section rows → no
drag handles to drag).
- {{rule.X}} aliases preserved (custom sections render through the
same composer pipeline as default sections).
- Q2/Q9/Q10 ratifications preserved.
This closes the Composer slice sequence A → F. The full feature set
ratified by m on 2026-05-26 is now in place:
A — base picker + read-only section list (mig 146/147/148)
B — editable prose + anchor-spliced render + MD→OOXML walker
C — building-blocks library + section picker (mig 149)
D — rich prose (headings, lists, blockquote, hyperlinks)
E — specialist bases lg-duesseldorf + upc-formal (mig 150)
F — section reorder / delete / add custom
t-paliad-318 Slice F
|
|||
| 94310ba498 |
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
|
|||
| 5834e3dc66 | Merge: Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) in MD→OOXML walker (m/paliad#141) | |||
| 677849784c |
feat(submissions): Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) (m/paliad#141)
Extends the Composer's MD → OOXML walker per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice D from
Slice B's paragraphs + B/I baseline to the full rich-prose feature set:
headings 1-3, bullet + numbered lists, blockquote, inline hyperlinks.
MD walker (internal/services/submission_md.go, +320 / -75 LoC):
- RenderMarkdownToOOXMLWithStyles is the new Slice-D entry point;
RenderMarkdownToOOXML stays as a thin back-compat wrapper.
- splitMarkdownBlocks classifies every line into one of:
paragraph, heading_1/2/3, list_bullet, list_numbered, blockquote.
CommonMark-style 3-space indent tolerance; "N. " and "N) " for
numbered. Blank-line spacing semantics preserved from Slice B.
- renderBlockParagraph applies stylemap[blk.styleKey] (with
fall-back to stylemap["paragraph"]). List blocks emit visible
"• " / "N. " prefix runs so the structure surfaces even if Word
isn't configured with auto-list-numbering — lawyer can apply a
real Word list style post-export. Numbered-list ordinals reset
on every non-list block (so "1. A\nplain\n1. C" renders 1./1.,
not 1./2.).
- parseInlineRuns adds `[label](url)` recognition. Each link gets
routed through the optional HyperlinkAllocator; the walker emits
`<w:hyperlink r:id="{rId}">…runs…</w:hyperlink>` with the
"Hyperlink" character style on each child run. Nil allocator
falls back to plain-text label (URL drops, label survives).
Composer (internal/services/submission_compose.go, +130 / -10 LoC):
- composerLinkAllocator hands the walker fresh rIds (rIdComposer1,
rIdComposer2, …) outside the base's existing namespace; same URL
shared across multiple sections dedupes to one rId.
- patchDocumentXMLRels appends matching <Relationship Type="…/hyperlink"
Target="URL" TargetMode="External"/> entries to
word/_rels/document.xml.rels. Idempotent on rIds already present;
synthesizes a fresh rels part when missing (defensive for stripped
bases). Returns the patched parts slice (caller must overwrite
because append may grow the backing array — fixed in this slice).
- Compose now passes the full stylemap (paragraph + heading_1/2/3 +
list_bullet + list_numbered + blockquote) into the walker, not
just the paragraph-style entry.
Frontend (frontend/src/client/submission-draft.ts):
- Toolbar adds H1/H2/H3 buttons (formatBlock h1/h2/h3), bullet
list, numbered list, blockquote, and a link button that prompts
for a URL + wraps the selection via execCommand("createLink").
- domToMarkdown serializer extends to <h1>/<h2>/<h3>, <ul>/<ol>
with per-item ordinal counter for numbered lists, <blockquote>,
and <a href="…"> → `[label](url)`. Nested <li> handling sits in
the ul/ol branch.
Tests (internal/services/submission_md_test.go, internal/services/
submission_compose_test.go):
- TestRenderMarkdownToOOXML_Heading1 / _Heading2And3 — stylemap
applied.
- _BulletList / _NumberedList / _NumberedListResetsOnNonList —
prefixes + ordinal counter.
- _Blockquote — stylemap applied.
- _Hyperlink — allocator called, w:hyperlink rId wired, Hyperlink
character style on label runs.
- _HyperlinkNilAllocatorFallsBackToPlain — label survives, no
hyperlink tag emitted.
- TestDetectBlockMarker — 13 marker / non-marker cases.
- TestComposer_HeadingsAndLists — end-to-end through Compose with
a multi-construct draft; verifies stylemap presence + content +
ordinal prefixes.
- TestComposer_HyperlinkWiresRels — body has the right
<w:hyperlink r:id="rIdComposer{N}">, document.xml.rels has the
matching <Relationship> rows with External target mode.
- TestComposer_HyperlinkDedupesByURL — two `[label](url)` references
to the same URL share one rId; second allocation gets no new
Relationship row.
Build hygiene: go build/vet/test -short clean (all packages); bun run
build clean (2906 i18n keys).
NOT in scope (Slice D's brief was rich-prose + toolbar):
- Numbering.xml audit on bases — current approach emits visible
"• " / "N. " prefix runs without depending on numbering.xml. A
future slice can swap to `<w:numPr>` if firm-style auto-numbering
becomes a hard requirement.
- DOM-from-Markdown on initial editor paint — the editor still uses
textContent=md, so toolbar-applied formatting reverts to literal
Markdown text after autosave + repaint. Acceptable trade-off for
Slice D; a future polish could parse MD into the DOM on paint.
- Tables, images, footnotes (still design §13 out of scope).
Hard rules honoured:
- NO new migrations (Slice D is pure code).
- NO behavior change for pre-Composer drafts (gate on draft.BaseID
unchanged).
- {{rule.X}} aliases preserved (placeholders pass through the walker
verbatim, get substituted by the v1 SubmissionRenderer pass).
- Q2 ratification preserved (no building_block_id lineage).
- Q9 ratification preserved (4-tier BB visibility from Slice C).
t-paliad-316 Slice D
|
|||
| 2c0efc396c | Merge: Slice B.5 — Go type aliases (SequencingRule = DeadlineRule) + JSON envelope dual-emit + Deprecation headers (m/paliad#93) | |||
| 5c6a0095e3 |
feat(models,services,handlers): Slice B.5 Go rename + JSON envelope dual-emit (t-paliad-305 / m/paliad#93)
Adds the Slice B.5 canonical Go names (SequencingRule, ProceduralEvent,
LegalSource, SequencingRuleService) without breaking any existing
call-site, and dual-emits / dual-accepts the two JSON envelope key
renames on /admin/api/rules with a Deprecation header.
* internal/models/models.go —
- type SequencingRule = DeadlineRule (alias; same struct, same db /
json tags). DeadlineRule remains the underlying type for now —
deferred hard-rename keeps the slice small.
- type ProceduralEvent struct mirroring paliad.procedural_events
(id, code, name, name_en, description, event_kind,
primary_party_default, legal_source_id, concept_id,
lifecycle_state, draft_of, published_at, is_active, timestamps).
Used by future code that needs the PE identity row alone.
- type LegalSource struct mirroring paliad.legal_sources (citation,
jurisdiction, pretty_de / pretty_en — both nullable per mig 136).
* internal/services/deadline_rule_service.go —
- type SequencingRuleService = DeadlineRuleService (alias).
- var NewSequencingRuleService = NewDeadlineRuleService (constructor
alias). Internal callers can adopt either name.
* internal/services/rule_editor_service.go —
- CreateRuleInput gains Code + EventKind fields tagged
json:"code" / json:"event_kind". CoalesceCanonicalKeys() folds
canonical → legacy after json.Decode so the rest of the service
keeps using SubmissionCode / EventType. Canonical wins when
both are sent.
- RulePatch gains EventKind field with the same fold.
* internal/handlers/admin_rules.go —
- adminRuleResponse wraps *models.DeadlineRule and adds Code +
EventKind fields alongside the legacy SubmissionCode /
EventType. Outputs both keys per response for one
deprecation-window slice.
- wrapRuleResponse / wrapRuleListResponse helpers.
- adminRuleDeprecationHeaders emits IETF Deprecation + Link/Sunset
headers on every Rule-bearing response so clients see the
migration signal in transit.
- All 8 Rule-returning handlers (List, Get, Create, Patch, Clone,
Publish, Archive, Restore) now wrap their result and add the
headers.
- Create + Patch handlers call CoalesceCanonicalKeys after decode
so legacy AND canonical request bodies are both accepted.
Scope decisions (documented in commit):
- Type renames use aliases instead of a hard 200-LOC rename. Same
semantics, no call-site churn. A future cleanup slice can flip
the underlying type definitions when convenient.
- ProceduralEvent + LegalSource are NEW structs (not aliases) since
they represent new conceptual rows; no legacy callers exist yet.
- Frontend admin .tsx i18n key rebinds (mentioned in parent task
brief B.5 deliverable list) are deferred — i18n keys themselves
already exist from Slice A (t-paliad-262); rebinding only changes
which key the .tsx file looks up. Pulling this into B.5 ballooned
scope; flagging as a small follow-up slice or B.6 sibling.
- Only /admin/api/rules emits dual keys today. Other handlers that
surface rule rows (Schriftsätze list, deadlines join) continue to
emit the legacy keys via models.DeadlineRule's existing JSON tags
— they're read paths, not the editor surface, and the deprecation
signal is most important where clients write.
Build + vet clean. TestMigrations_NoDuplicateSlot passes.
|
|||
| 6e0961cc30 | Merge: Composer Slice C — building blocks library + section picker (mig 149) (m/paliad#141) | |||
| ee98db94fa |
feat(submissions): Composer Slice C — building blocks library (m/paliad#141)
Per the design at docs/design-submission-generator-v2-2026-05-26.md §8
and the Q2 / Q9 ratifications:
- Q2 (m, 2026-05-26): building blocks are plain text paste sources.
No building_block_id reference is stored on submission_sections.
- Q9 (m, 2026-05-26): four visibility tiers — private / team / firm
/ global.
Schema (mig 149):
- paliad.submission_building_blocks — library catalog. Columns: slug,
firm (NULL = cross-firm), section_key (binds to one section kind),
proceeding_family (NULL = any), title_de/_en + description_de/_en
+ content_md_de/_en, author_id, visibility (CHECK in 4-tier set),
is_published, created_at, updated_at, deleted_at (soft delete).
RLS: coarse-grained SELECT — every authenticated user sees
non-deleted non-private rows + own private rows. Tier-specific
predicate (private/team/firm/global) applied in Go-layer service so
semantics evolve without RLS migrations. Mutations admin-only (no
RLS write paths).
- paliad.submission_building_block_admin_versions — append-only
history per block, retention=20. Admin-side only; NOT referenced
from submission_sections (per Q2's plain-text-paste model). Exists
so accidental delete / overwrite are recoverable.
Backend:
- internal/services/submission_building_block_service.go (~510 LoC):
BuildingBlockService. ListVisible applies tier predicate at query
time (private = author_id match; firm = firm column NULL OR matches
branding.Name; team = author shares a project_team with caller via
paliad.project_teams self-join; global = open). ListAllForAdmin
drops the predicate. Create + Update + SoftDelete + RestoreVersion
all transactional; appendVersionTx writes one audit row +
GC-deletes anything past the retention=20 horizon in the same tx.
InsertIntoSection (the paste mechanic) clones content_md_<lang>
into the section row with a "\n\n" separator if section already has
content. NO building_block_id stamped per Q2.
- internal/handlers/submission_building_blocks.go (~480 LoC): nine
handlers split between the lawyer-facing picker (list, insert) and
the admin editor (list, get, create, update, delete, list-versions,
restore-version, page). buildingBlockUpdateInput uses presence-
tracking UnmarshalJSON for the four nullable fields (firm,
proceeding_family, description_de/_en) so PATCH can distinguish
"no change" from "set to null".
- Routes registered: lawyer-facing under /api/submission-building-blocks,
admin-gated under /api/admin/submission-building-blocks/* and
/admin/submission-building-blocks (page).
- Wiring: handlers.Services + dbServices + cmd/server/main.go all
gain SubmissionBuildingBlock. NewBuildingBlockService takes the
branding.Name firm hint for the visibility predicate.
Frontend:
- frontend/src/admin-submission-building-blocks.tsx (~85 LoC):
three-pane admin shell (list / editor / version log) registered
in build.ts.
- frontend/src/client/admin-submission-building-blocks.ts (~370
LoC): admin client — list paint, edit form (slug + firm +
section_key + proceeding_family + title/desc/content per lang +
visibility radio + is_published toggle), per-block version log
with restore button. Bilingual labels.
- frontend/src/client/submission-draft.ts: per-section "+ Baustein"
button on the Composer editor toolbar (Slice B substrate gets one
more affordance). openBlockPicker opens a modal filtered to the
section's section_key, 200ms-debounced search by free text against
title/description/content. Click a hit → POST insert-into-section
→ section row's content_md_<lang> gains the block's content
appended at the end (Q2's plain-text paste semantic, no lineage).
- ~240 LoC of CSS: modal overlay + picker rows with tier-colored
visibility chips + admin editor 3-pane grid + form rows + version
list.
- 12 new i18n keys × 2 langs (admin.building_blocks.*).
Tests:
- TestValidVisibility (8 cases including case-sensitivity + empty).
- TestAppendBlockContent (8 cases covering empty-existing / empty-
addition / whitespace-only / trailing newline collapse).
- TestBuildingBlockVisibilityConstants pins the 4 string literals
against drift (RLS predicate + DB CHECK depend on them).
Build hygiene: go build/vet/test -short clean; bun run build clean
(2906 i18n keys, data-i18n scan clean).
Hard rules per ratifications honoured:
- Q2: no building_block_id lineage on sections (paste is plain text).
- Q9: 4 visibility tiers (private/team/firm/global).
- NO behavior change for pre-Composer drafts (the picker just doesn't
show — section list is hidden for base_id NULL drafts).
- {{rule.X}} aliases preserved (block content goes through the same
v1 placeholder pass on export as section prose).
NOT in scope per Slice C brief:
- User-authored private blocks (Slice C ships admin curation only;
any-user create is a follow-up).
- Tier promotion review workflow (admin sets tier directly today).
- Per-section "where is this block used" reverse lookup (no lineage
to query).
- Slice D's rich-prose features (headings, lists, blockquote) still
Slice D's job; this Slice doesn't extend the MD walker.
t-paliad-315 Slice C
|
|||
| 987db27831 | Merge: t-paliad-305 — Slice B.4 destructive drop: paliad.deadline_rules retired, INSTEAD OF triggers on view (mig 140, snapshot pre_140 same-TX) (m/paliad#93) |