Total canvas width is the wrong metric: centring every ancestor makes a 24-root
forest wider overall (an accepted trade-off, pan/zoom handles navigation). The
actual fix is per-bloodline compactness. Assert every contiguous bloodline's
span stays far under the old full-canvas smear (4860px) — today the widest,
Albert de Gruyter's, is ~960px, down from being smeared across the whole canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canonical graph is a forest of 24 roots spread across generations 0-4.
Packing every root at tree-depth 0 stacked all of them horizontally even when
they sit at different generations (different y), blowing the canvas out to
~9660px. Indexing the contour by absolute level (the rank buildLayout already
passes as level) lets unrelated roots at different generations share x-columns,
and keeps the no-overlap guarantee per-row. level falls back to tree depth when
omitted, so the abstract tidyTree tests are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No node outside a root's structural subtree may intrude into that bloodline's
[minX, maxX] horizontal span — the contiguity guarantee that fixes the smeared
bloodline symptom.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O(n^2) sweep over canonical + synthetic: any two nodes sharing a y are at least
NODE_W + COL_GAP apart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixture-wide loop over the canonical forest and a synthetic tree: each unit's
run centre is within [min, max] of its child-unit centres — the ancestor
centring invariant, asserted on real data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 5-generation single bloodline fanning out wide at the bottom: the apex
great-great-grandparent (and every ancestor in the chain) sits at the centre of
the descendant span, the exact symptom the old per-generation packer produced
in reverse (apex pinned to the left edge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
StammbaumConnectors takes the layout's crossLinks and draws those parent->child
connectors with a 2 6 dash at reduced opacity — deliberately distinct from the
ended-marriage spouse dash (4 4) and from a solid parent drop. Geometry still
lands on the child top, so the meaning is carried redundantly (WCAG 1.4.1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the two spouses' parents sit at different structural levels, the
structural owner keeps its hierarchy edge and the other parent->spouse edge is
recorded in layout.crossLinks (rendered with a distinct dash). The couple still
sits exactly adjacent in the owner's run and B keeps a real position.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the existing adjacency contract: the couple is exactly adjacent in the
run AND, because both parents are roots (same structural level), the displaced
parent edge stays solid — layout.crossLinks is empty for this case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
buildLayout now builds the family forest, packs it bottom-up via tidyTree, and
maps each unit's run x back to per-person positions (x from structure, y from
rank). assignRanks, the generations map, and computeViewBox are reused
unchanged. The unknown-id guard now covers PARENT_OF as well as SPOUSE_OF, and
displaced cross-level edges are exposed as crossLinks for distinct rendering.
The ~210-line block packer (and its block/merge helpers) is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Net-new ordering coverage: roots and every unit's children sort by birthYear
ASC (undated last), then displayName, then stable id — so horizontal x never
depends on Map iteration order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assigns every person to one unit: a primary, or a spouse absorbed into the
primary's run (marriage-year order, #361 preserved). Wires the parent/child
hierarchy from each primary's structural-owner parent and records displaced
parent edges as cross-links (classified same-level vs cross-level for later
distinct rendering). Unknown-id guard covers PARENT_OF and SPOUSE_OF.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Structural-owner rule for couples: earlier birth year wins, missing year sorts
last, ties break on stable id. The single definition reused by the cross-link,
cycle and intra-family paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New domain-agnostic bottom-up tidy-tree module (Reingold-Tilford contour pack)
operating on abstract { id, width, children } nodes — zero generated-API
imports. First rung of the TDD ladder: a single leaf lays out at x=0. The full
contour/centring machinery is in place; subsequent commits add tests that
exercise it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing node() factory never sets birthYear, but the new sibling/branch
comparator (birthYear ASC NULLS LAST) needs it. Add makeNode(id, name,
{birthYear, generation}) alongside it; unblocks every ordering test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The component-test browser env (src/test-setup.ts) loads no Tailwind
stylesheet, so the footer buttons' min-h/min-w-[44px] classes have no
layout effect there and the elements collapse to their 16px icon —
making the getBoundingClientRect size assertions fail in CI.
Assert the sizing utility classes instead; they are the exact mechanism
that produces the WCAG 2.2 §2.5.8 target size in the real app. The
compiled pixel size remains covered by the full-app e2e.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix a stale test title that still claimed a delete button is visible.
- Strengthen the two "never renders a delete button" contract tests
(AnnotationShape + AnnotationLayer specs) to assert the annotation
element has zero descendant <button> elements, not just the absence of
the removed testid (a near-tautology now that the testid is gone).
- Harden the e2e delete test: guard countBefore > 0 so a missing seed
fails clearly instead of asserting toHaveCount(-1), and capture the
deleted annotation's testid to assert that specific element is gone
(identity check) alongside the count drop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The panel footer's delete and review-toggle controls were icon-only ~16px
hit areas. After #722 removed the on-canvas delete button, the panel delete
button became the only touch-reachable delete path, so it must meet the WCAG
2.2 §2.5.8 minimum target size (44×44px). Give both icon-only footer actions
a >=44px inline-flex hit area with negative margins so the row layout and the
visible icon size are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-annotation delete button (a 44px circular control pinned to the
box's top-right) overlapped the box below and obscured the underlying
document text. It was redundant: every user-drawn annotation has a
transcription block, and the right-hand panel already offers a
non-overlapping delete per block that cascades to the annotation.
Remove the visible button and its `deleteVisible` derived. Keep the
keyboard Delete shortcut (and its `showDelete`/`onDeleteRequest`/
`deleteAnnotation` wiring) — it obscures nothing and remains a
power-user path and the only cleanup route for orphan annotations.
Tests: replace the button-render/click specs with contract tests
asserting no delete button ever renders; repoint the e2e delete flow
to the keyboard shortcut + confirm dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI showed the single/many a11y tests failing with count 0: init()'s async
fetchUnreadCount resolved to {count:0} AFTER setNotifications() ran,
clobbering the seeded count (the flake Sara predicted in review). Stub
fetch to never settle so the announced count is driven solely by
setNotifications — deterministic, no race. Also rewrites the 'error' test
to seed a count then fail the load and assert the count SURVIVES, so it is
a meaningful state distinct from 'empty' (was byte-identical, flagged by
Felix/Sara/Leonie). Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI proved cross-file sharing of a virtual-module mock body cannot work in
@vitest/browser-playwright 4.1.6: the static-import spread fails the hoist
("no top level variables"), and the await-vi.hoisted-import form fails to
parse ("Unexpected identifier 'vi'"). vi.hoisted has the same hoist
constraint as vi.mock, so there is no way to thread an external module's
body into the factory here.
Reverts Phase 1: restores the 4 $app/forms/$app/navigation specs to their
inline factories, inlines NotificationBell.spec's forms stub, deletes the
src/__mocks__/$app/* modules and the $mocks alias (vite, vitest-coverage,
kit). The no-factory-ban meta-test stays (no-factory vi.mock is still
banned). ADR-012 amended to record the infeasibility. Everything else
($app/state migration, confirm context-inject, notification refactor, the
pin, the meta-test) is unaffected. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI caught that vi.mock('$app/forms', () => ({ ...formsMock })) with a
static `import * as formsMock` fails: vitest hoists vi.mock above the
import, so the factory references an uninitialised binding
("no top level variables inside"). Load the shared mock module via
`const formsMock = await vi.hoisted(() => import('$mocks/...'))` instead —
the factory may reference a vi.hoisted binding, and the dynamic import runs
at collection time (not in the lazily-invoked factory), so it stays clear
of ADR-012's birpc race and the no-async-mock-factories guard. Applies to
all 5 shared-mock consumers ($app/forms x4, $app/navigation x1). Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Converts the module-singleton notificationStore into a context-provided
store so its specs can drive it without mocking the module. notifications.svelte
now exports createNotificationStore() (the former singleton body), plus
provideNotificationStore()/getNotificationStore()/NOTIFICATION_KEY mirroring
the confirm service. Root +layout provides it; NotificationBell and the
Chronik page read it via getNotificationStore().
Tests:
- notifications.svelte.spec drives a fresh createNotificationStore() per test
(replacing __resetForTest/__setNavigateForTest with setNavigate()).
- notification.test-fixture.svelte wraps the bell, provides the store, and
exposes setNotifications(items) via onReady (option b).
- NotificationBell.svelte.spec asserts the announced unread count across the
empty / single / many / error a11y states (AC#5), stubbing EventSource+fetch.
- aktivitaeten page spec injects a real store via render context.
Per the recorded Phase-2b decision (full context refactor). Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes Phase 2a: geschichten/[id], persons/[id]/edit and admin/tags/[id]
page specs now provide a real createConfirmService() via render context
instead of mocking confirm.svelte. Zero confirm.svelte vi.mocks remain
across the client suite (AC#4). Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the vi.mock('$lib/shared/services/confirm.svelte') stub with a
real createConfirmService() provided through render's context map, mirroring
the existing admin/tags/[id]/page.svelte.spec.ts pattern. The generic
confirm.test-fixture.svelte renders only ConfirmDialog and cannot wrap an
arbitrary page; none of these specs trigger confirm(), so the children's
getConfirmService() simply reads the provided context instead of a module
mock. No vi.mock of confirm.svelte remains in these 5 specs. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the local beforeNavigate-capture plumbing and simulateNavigate
helper with the shared $mocks/$app/navigation module via a sync factory.
The per-test reset now comes from the shared module's embedded beforeEach.
Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exports the standard nav functions as vi.fn() and a beforeNavigate that
captures the registered callback. The exported simulateNavigate(href)
helper fires that callback and returns the cancel spy — the whole
capture-and-fire pattern lives in the shared module, not the raw callback.
An embedded beforeEach clears the captured callback and the mock call
histories before every test. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes Phase 1a after the load-bearing ChronikFuerDichBox spec proved
the pattern. ChronikFuerDichBox.test and NotificationDropdown.test (rich
result-firing interceptors) keep their submit-fired assertions
(optimisticMarkRead/MarkAllRead) and use formsMock.setFormResult for the
failure branch. NotificationBell.spec used the simpler intercept-only
factory and renders no form of its own, so it adopts the shared superset
purely as a render-time stub. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Load-bearing first migration (ADR-012): this is the hardest case — its
enhance submit callback actually fires and reads the form result. Replaces
the duplicated 23-line interceptor factory with vi.mock('$app/forms',
() => ({ ...formsMock })) via $mocks, and the per-test mockFormResult
mutation with formsMock.setFormResult({ type: 'failure' }). The reset now
comes from the shared module's embedded beforeEach. The existing
optimisticMarkRead/optimisticMarkAllRead-on-submit assertions remain as the
positive proof the callback fired. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single home for the non-trivial form-interceptor enhance() shared by the
four complex consumers: it intercepts submit, invokes the SubmitFunction,
and fires the returned callback with a configurable result. setFormResult()
drives the success/failure branch; an embedded beforeEach resets it before
every test so isolation is structural. Consumed via vi.mock('$app/forms',
() => ({ ...formsMock })) through the $mocks alias. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vite resolve.alias (added for the client + coverage runs) does not
reach svelte-check, which resolves paths through the generated tsconfig.
Declaring $mocks in kit.alias feeds both the generated tsconfig paths and
the sveltekit() vite plugin, so editor/type-check resolve it too. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the 2026-06-02 revision from #560: (1) no-factory vi.mock of a
SvelteKit virtual module is forbidden (the PR #657 partial-mock failure),
guarded by a seventh enforcement layer; (2) shared mock body + per-spec
sync factory via the $mocks alias is the sanctioned dedup; (3) Option C
config-level auto-resolve is rejected. Also corrects the stale 4.1.0
patch filename to 4.1.6 and links #657. Part of #560.
A vi.mock('$app/navigation') with no factory does not auto-resolve to a
__mocks__ file for SvelteKit virtual modules — it substitutes some
exports and leaves others (replaceState) bound to the live router, which
is exactly the PR #657 failure. This Node-mode source scan, mirroring
no-async-mock-factories and no-duplicate-mock-ids, fails at every vitest
invocation if any *.svelte.{spec,test}.ts reintroduces the pattern, and
forecloses ADR-012's rejected Option C. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Declares $mocks -> src/__mocks__ in both vite.config.ts and
vitest.client-coverage.config.ts so shared mock modules resolve in the
client test run and the coverage job alike. Enables the sync-factory
dedup pattern from ADR-012 (vi.mock('$app/forms', () => ({ ...formsMock }))).
Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the caret so the version cannot float off the patched release.
patches/@vitest+browser-playwright+4.1.6.patch backports vitest PR #10267
(the duplicate-mock-id birpc race, ADR-012) and only applies to 4.1.6; a
caret range could resolve to a version the patch rejects. A top-level
"//" key records the removal condition since package.json forbids
comments. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The legacy $app/stores subscription API is replaced with the modern
$app/state reactive proxy (page.url.pathname), per ADR-012's
architectural follow-on. The two spec mocks of $app/stores are replaced
with sync-factory $app/state mocks, matching the existing convention in
aktivitaeten/documents specs. Part of #560.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
toHaveAttribute compares by equality, so passing a regex asserted against
the literal RegExp object and failed. Assert the full title against
m.person_correspondents_search_title(...) instead — it names both persons
and avoids retyping the copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The e2e README still listed the deleted korrespondenz.spec.ts. Replace it
with the new briefwechsel-removed.spec.ts guard entry — closing the last
dangling reference flagged in review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing the Briefwechsel view retargets its one inbound link to document
search, which filters sender AND receiver — A->B only. The bidirectional
"replies" direction is intentionally dropped. ADR-030 records the
context, decision and consequences, and notes a bidirectional search
filter as the superseding future enhancement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the Briefwechsel route and the conversation derived-domain /
conversation-thread prose from the route tables (CLAUDE.md,
frontend/CLAUDE.md), ARCHITECTURE.md, the C4 frontend/backend diagrams,
and GLOSSARY.md (term + derived-domain list). Delete the two superseded
Briefwechsel design specs. Historical ADRs and dated analyses are left
untouched as point-in-time context.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the /api/documents/conversation path and its getConversation
operation from the generated client to match the removed backend
endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete findConversation and findSinglePersonCorrespondence (no remaining
callers after the service methods were removed) and their integration
test section. Drops the now-unused LocalDate import.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete getConversationFiltered (the endpoint's only caller is gone) and
the dead 2-arg getConversation(personA, personB) which had zero callers,
along with both getConversationFiltered test blocks. The hasSender/
hasReceiver specifications stay — document search still uses them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete GET /api/documents/conversation and its controller handler — the
only client was the removed Briefwechsel view. Drops the now-unused Sort
import.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With the Briefwechsel view gone, lib/conversation/ held a single shared
component whose only consumer is lib/document/ThumbnailRow. Move it (and
its spec) into lib/document/, update the import, delete the now-empty
lib/conversation/ folder, and fix the stale frontend/CLAUDE.md lib map.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the 22 message keys that only the deleted Briefwechsel view used
(conv_* except the still-used conv_sort_newest/oldest, plus
nav_conversations, doc_conversation_title and person_correspondents_hint,
all now superseded by the retargeted card's new search keys).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>