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).
Read-only audit. Maps the 3 doc-generation fill paths (merge.go fills
header/footer placeholders; compose.go passes headers byte-for-byte;
skeleton-as-merge-fallback latent bug), three gap tables (template /
var-bag / data-model), forum-dependence grounded on UPC RoP r.13, and a
tracer-bullet-first wiring proposal. States the A/B fork for m: wire what
current data supports (basic caption, no schema) vs. forum-correct Rubrum
(structured party address/Rechtsform/Sitz + court details + capture UI).
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.
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.
Engine (pkg/nomen) renders a name from (composition, var-bag, render-
target). Structured segments + string shorthand; omit/placeholder/literal
missing-rules; title vs filename targets. Both shipped schemes (#155
draft title, 354 export filename) fold in as data-driven seed defaults
with byte-equality as the acceptance gate. Non-project drafts get a
date-first <date> <keyword> name (m's immediate ask).
Settings built ON the dashboard-layout-spec precedent: system -> firm ->
user -> per-document precedence, validated jsonb spec. Project-level
deferred to v1.1 (storage path reserved on projects.metadata).
5-slice train: engine+faithful-refactor -> non-project fix -> user
override -> settings UX -> firm default. All 8 grilling questions
answered (matched recommendations), captured in the decisions section.
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
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).
planck flagged via mai report feedback (id 12301) after the B5+B6
verification round caught them:
- §5.4 'INSERT into paliad.project_parties' → real table is paliad.parties
- §5.4 'status=open' → real CHECK constraint allows pending/completed/cancelled/waived
- §7.4 listed verfahrensablauf-detail-mode.ts as dead code, but builder
imports filterByDetailMode from it; struck through with KEEP note.
Code shipped (B5+B6) used the correct values throughout; this aligns
the historical PRD with reality so a future reader doesn't repeat the
verification time planck spent.
Litigation Builder slice B6 (m/paliad#153 PRD §7.1 + §7.4 + §10) — last
slice of the train; the Builder is now complete.
Mobile basic-read (<640px, PRD §10):
- builder.ts wireMobileGuard — a capture-phase document click listener that,
only when matchMedia("(max-width:640px)") matches, intercepts taps on
mutating affordances (rename/share/promote/new-scenario/add-proceeding +
every in-triplet button/input/select), preventDefault+stopPropagation,
and surfaces a "Auf größerem Bildschirm öffnen" toast. Desktop code paths
are untouched (guard early-returns off-mobile). Column-triplets already
collapse to a single column via the reused .fr-columns-view @640px rule;
reading (open/switch scenarios, search, mode tabs) stays fully functional.
- global.css — .builder-mobile-toast + full-bleed modal on phones.
Dead U0-U4 catalog cleanup (PRD §7.4) — deleted, no remaining references
(grep "from.*fristenrechner-|from.*verfahrensablauf" shows only the kept
verfahrensablauf-core + verfahrensablauf-detail-mode the builder reuses):
- client/fristenrechner-mode-a.ts, fristenrechner-result.ts(+test),
fristenrechner-wizard.ts(+test)
- client/verfahrensablauf.ts, client/views/event-card-choices.ts,
client/views/verfahrensablauf-state.ts(+test)
- components/VerfahrensablaufBody.tsx
(/tools/fristenrechner + /tools/verfahrensablauf stay as 301 redirects to
/tools/procedures — handlers already redirectToProcedures.)
i18n finalised (DE+EN): removed 4 duplicate deadlines.party.* keys per
block (behaviour-preserving — the later, winning copy is kept) and added
the missing DE "cal.today". Codegen clean, no dupes, full DE/EN parity.
go build/vet clean; bun build + 227 frontend tests pass. Playwright-
verified: at 375px the triplet collapses to one column + the scenario
list reads, while "+ Verfahren hinzufügen" and "Teilen" are blocked
(toast shown, no action); at 1280px the same actions work normally.
The final slice: land the format-neutral document model with REAL consumers
and unify the Markdown parser — no duplication, byte-identical output.
Neutral model (pkg/docforge/model.go): Document / Block / InlineSpan.
BlockKind values are the stylemap keys. A hyperlink is a span with Link set
+ Children (the label's spans), preserving link boundaries so adjacent
same-URL links stay distinct — byte-exact with the pre-model walker.
Markdown importer (pkg/docforge/markdown): Import(md) → Document. The SINGLE
Markdown parser for docforge — block split, marker detection, inline
bold/italic/link tokenisation, {{placeholder}} pass-through (the b78a984
fix). Relocated out of the docx walker.
docx renderer (pkg/docforge/docx/markdown.go): now RENDERS a Document →
OOXML (RenderDocumentToOOXML); RenderMarkdownToOOXML[WithStyles] = render(
markdown.Import(md)). The shipped submission walker routes through the model,
so there is one parser, not two. The comprehensive byte-exact render tests
(RenderMarkdownToOOXML_*) all PASS unchanged = output identical.
Exporter interface (pkg/docforge/exporter.go, PRD §4 B4): Exporter{Format,
MIMEType, RenderBody(Document)} with the .docx impl (pkg/docforge/docx/
exporter.go). The seam a future PDF/HTML exporter slots into.
Tests: parser tests relocated to the markdown pkg (parseSpans/detectBlockMarker)
+ new importer Document tests + exporter conformance test.
Verification: go build/vet clean; gofmt clean; full NO-DB test suite GREEN
(authoritative — proves no regression); docforge byte-exact render oracle
PASS; composer live test renders through the rewired walker (PASS); bun build
+ bun test 274/274. The shared-DB live run fails ~85 tests across unrelated
services from a harness pq-42P08 $1-type seeding quirk + a stale
deadline_rules test — systemic/environmental (the no-DB run is clean), not
this change.
docforge train complete: 8 slices, the engine extracted + cleaned + a working
author→generate→export loop on uploaded templates, plus the neutral model +
importer + exporter seam for future formats/consumers.
m/paliad#157
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
The WYSIWYG authoring surface at /admin/templates (admin-gated page route):
- templates-authoring.tsx — page shell (upload form, template list,
workspace: palette / run-addressable preview / placed slots).
- client/templates-authoring.ts — hydrates it: lists templates, uploads a
.docx (multipart), renders the run-span preview, builds the variable
palette from the Go catalogue (GET /api/docforge/variables), and wires
the select-then-pick gesture: select text within one .docforge-run, click
a palette variable → POST the slot → re-render with the response. Reuses
the docforge-editor lib (escapeHtml, catalogue client). Cross-run
selections rejected with a hint (v1: single-run text slots).
- build.ts emits dist/templates-authoring.html + bundles the client.
- handleTemplatesAuthoringPage serves the shell; GET /admin/templates
registered under adminGate.
- 12 i18n keys (DE+EN) for the page; i18n-keys.ts regenerated (3079).
Verification: go build/vet/test green (13 pkgs); bun run build.ts clean
(i18n scan passes); bun test 274/274; gofmt-clean. The docx surgery + store
+ catalogue are unit/live-tested. VERIFICATION CEILING: the integrated live
flow (upload→render→select→inject→save in a browser) needs the app running
with DATABASE_URL + Supabase auth + Playwright — verified post-merge, not in
this env.
m/paliad#157
Admin-gated authoring API over docforge.TemplateStore + the docx authoring
engine (handlers/templates.go, routes under adminGate):
GET /api/admin/templates — catalog list
POST /api/admin/templates — multipart upload → ImportForAuthoring
(validate + detect slots) → Create v1
GET /api/admin/templates/{id} — authoring view (run-addressable
preview + slots)
POST /api/admin/templates/{id}/slots — InjectSlot at the selection →
AddVersion (re-detect slots from the
new carrier so template_slots mirror
the carrier's actual {{tokens}})
docforge.ErrTemplateNotFound → 404; injection failures (bad selection/key)
→ 400 with the engine's message for the UI to surface. Upload capped at
10 MB. Slot placement creates a version per placement (immutable snapshot);
batching a session into one version on explicit save is a documented
refinement.
Verification: go build/vet clean, handlers test green, gofmt-clean. The docx
surgery + store are unit/live-tested; the integrated HTTP flow is verified
post-merge (needs DATABASE_URL + Supabase auth).
m/paliad#157
The verifiable backend heart of the authoring surface, before the HTTP +
frontend layers.
pkg/docforge/docx/authoring.go:
- ImportForAuthoring(carrier) → AuthoringView{PreviewHTML, Slots}: parses
an uploaded .docx into a run-addressable preview (one
<span class="docforge-run" data-run="N"> per <w:t>, document order)
plus the {{placeholder}} slots already present.
- InjectSlot(carrier, runIndex, selectedText, slotKey) → new carrier:
replaces the selection inside run N with a {{slot_key}} token. Keys on
the selected TEXT (not a byte/UTF-16 offset) so umlauts can't desync the
client selection from the server slice; preview + injection walk runs in
the identical paragraph→<w:t> order so data-run indices line up.
- v1 scope: text slots in body paragraphs; out-of-run / cross-run / not-
found selections return an error the UI turns into a hint.
6 unit tests cover run-addressable preview, slot detection, injection +
round-trip re-import, umlaut/run-targeting, and the error paths (selection
absent, out-of-range run, invalid slot key) — all passing.
Wired PgTemplateStore through the stack (main.go → handlers.Services →
dbServices) so the upcoming authoring endpoints can reach it.
Verification: go build/vet clean, full module test green (13 pkgs), new
files gofmt-clean. The HTTP endpoints + frontend authoring page land next;
their live flow needs the post-merge e2e/manual loop (DB+Supabase).
m/paliad#157
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
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
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
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
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 b78a984 underscore-fix)
submission_render.go -> pkg/docforge/docx/dotm.go (.dotm->.docx)
+ their _test.go files (git-tracked renames, 84-99% identical)
internal/services keeps thin type-alias + forwarder shims
(docforge_shims.go) so every caller in services/handlers/main compiles
and behaves identically: PlaceholderMap, MissingPlaceholderFn,
SubmissionRenderer, HyperlinkAllocator (aliases); NewSubmissionRenderer,
DefaultMissingMarker, RenderMarkdownToOOXML[WithStyles], ConvertDotmToDocx,
SanitiseSubmissionFileName (forwarders). docx.XMLAttrEscape is exported so
submission_compose.go's hyperlink-rels inserts reuse the walker's escaping.
Three mis-filed pretty-printer tests (legalSourcePretty, ourSideDE/EN,
patentNumberUPC) that exercise the vars layer move back to
internal/services/submission_vars_pretty_test.go.
Placeholder grammar + PlaceholderMap stay co-located with the renderer in
docx for now; slice 3 hoists the format-neutral grammar to the docforge
root with the VariableResolver interface.
Verification: go build ./... clean, go vet clean, full module test green
(the byte-exact OOXML golden tests in merge/compose/render pass unchanged
= behaviour preserved). gofmt drift on the moved files is pre-existing
(72/169 services files already drift; no gofmt gate).
m/paliad#157
Extract the submission generator into pkg/docforge: neutral document model
+ opaque carrier (lossless .docx), VariableResolver interface per namespace,
pluggable importer/exporter (.docx first), WYSIWYG authoring page, generic
editor UI package. 8-slice train, extract-in-place migration that protects
the b78a984 underscore fix, the placeholderRegex + data-var contracts, and
the building-block/section model.
Includes all 13 of m's decisions (5 prose-grill metaphor + 8 structured).
upc-kommentar deferred as a live consumer (it is Bun/SvelteKit/TS, zero Go);
abstractions sized for a later HTTP veneer.
m/paliad#157
m had a one-off /tmp/paliad-deadline-export.py (work/head delegation
#2572) that dumped every published sequencing_rules row. Output
showed 37 entries on upc.inf.cfi including optional rules
(Lodging of translations, Review of CMO, ...) which fights the
engine's IncludeOptional=false default and m's "naked proceeding
with options but not always displayed" mental model.
Move to exports/gen-deadline-list.py as the canonical re-runnable
script and add a SQL-level priority filter that matches the
engine. Default suppresses priority='optional'; --include-optional
opts back in for an exhaustive catalog dump.
- DSN overridable via PALIAD_DEADLINE_EXPORT_DSN env var.
- argparse-driven: --include-optional / -o OUT / --generated-for LABEL.
- Header explains the mode so the PA reader knows what's suppressed.
- Regenerated exports/upc-deadlines-2026-05-28.md: now 178 rules across
25 proceedings (vs the unfiltered run). upc.inf.cfi section drops
from ~37 to 28 mandatory + conditional rules - the optional ones
are gone; trigger_event_id mandatory rules stay in the catalog
(they're a real PA-knowable surface; runtime anchor state is what
decides whether they project into a timeline, separate concern).
Run:
uv run exports/gen-deadline-list.py [--include-optional]
(m/paliad#153)
The Litigation Builder triplet renders /api/tools/fristenrechner output
verbatim and never applied the pre-existing filterByDetailMode pass that
the legacy /tools/verfahrensablauf page uses. With the engine fix
(3c840c0 — pkg/litigationplanner default IncludeOptional=false + trigger
event semantic anchoring) already in main, optional rules are dropped
server-side but rules with an unsatisfied trigger_event_id surface as
IsConditional. Without filterByDetailMode those still rendered as
"abhängig von ..." cards on the triplet, polluting m's "naked
proceeding with options but not always displayed" mental model.
upc.inf.cfi went from 7 mandatory backbone events to 29 visible cards
(22 conditional noise — Lodging of translations, Mängelbeseitigung,
Antrag auf Verweisung, Wiedereinsetzung, ...). Live BEFORE/AFTER
captured in exports/screenshots/.
Fix layers:
- Go handler (internal/handlers/fristenrechner.go): accept
includeOptional + triggerEventAnchors from request body and
forward to services.CalcOptions. Default zero values match the
engine defaults (suppress optionals + no fabricated dates for
trigger_event_id rules), so the wire is unchanged when callers
don't set them.
- TS calc surface (frontend/src/client/views/verfahrensablauf-core.ts):
add the same two fields to CalcParams + forward in the fetch body;
surface rulesAwaitingAnchor on DeadlineResponse mirroring
Timeline.RulesAwaitingAnchor.
- Builder triplet (frontend/src/client/builder.ts hydrateTriplet):
apply filterByDetailMode(detailgrad) before renderColumnsBody, with
detailgrad sourced from the proceeding row. "selected" (default)
drops conditional + optional rules; "all_options" passes
includeOptional=true so the engine returns the optional rules the
user can opt into.
- Legacy /tools/verfahrensablauf (frontend/src/client/verfahrensablauf.ts):
pass includeOptional based on detailMode + a small hasOptionalOptIn
helper so per-rule rule:<uuid>=true deviations still surface their
optional rule even in "selected" mode (the engine has no rule:<uuid>
awareness; without the opt-in the user's pick would silently no-op).
Tests:
- frontend/src/client/views/verfahrensablauf-core.test.ts: pin the
fetch body shape - includeOptional=true and triggerEventAnchors={...}
round-trip through the request; empty/default values are omitted so
the wire stays minimal.
bun build + bun test (269 pass) + go vet + go test
./internal/handlers/... ./pkg/litigationplanner/... all clean.
(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
PRD §2.2 + §3.1: the page-header search box drives a typed dropdown
returning grouped event / scenario / project hits, and the "Ereignis"
entry mode is enabled. Picking an event creates a scratch scenario
with one triplet anchored on that event's proceeding type, with the
event card auto-anchored (lime band + "━━━━ DU BIST HIER ━━━━" divider
above the next-coming events).
Backend: new GET /api/builder/search reuses
DeadlineSearchService.SearchEvents for the events corpus (UPC v1),
filters owned scenarios by ILIKE on name, and reuses ProjectService.List
for the Akten group (team-RLS via visibilityPredicate). Each group is
capped independently (default 8 events / 5 scenarios / 5 projects, max
30). Missing services degrade gracefully — empty group, not 503.
Frontend: builder-search.ts owns the dropdown (debounced 180ms,
arrow-key navigation, Enter to pick, abort on next query). builder.ts
gains mode state ("cold" | "event" | "akte"), wires the mode bar +
search input, and runs applyAnchorHighlight after triplet hydration —
the helper finds the .fr-col-item with the picked rule_id, adds the
.builder-anchor-card lime band, and inserts a full-width
.builder-anchor-divider after the anchor's row in the columns grid
via JS row-index math (the grid is row-major with 3 header cells
+ 3-cells-per-row body).
Filter pill reset: setMode() clears the search input and closes the
dropdown when switching entry modes. Forum/proc/party/kind chips are
not yet rendered separately (they live in the search dropdown today);
the reset hook attaches there too when those land in a follow-up.
Verification:
- bun build (frontend bundles + i18n scan clean)
- go vet ./... + go test ./... (all packages pass)
- Playwright: mode switch focuses search, debounced fetch fires,
typed result groups render with N · M · K pluralization, event
pick creates scratch scenario + adds proceeding, anchor card
+ DU BIST HIER divider render in the columns grid (screenshots
confirmed visually)