Adds a second binding invariant section to ADR-012 covering the
duplicate-id mechanism named in #553's follow-up investigation: same
resolved module URL referenced via two distinct vi.mock id strings →
@vitest/browser-playwright leaks an orphan Playwright route → birpc-closed
crash in the next session.
Records the rule (one canonical id per mocked module, prefer the spelling
production uses, no-extension for .svelte rune modules), the in-suite
detector (no-duplicate-mock-ids.test.ts), and the patch-package backport
of vitest PR #10267 with its removal trigger.
Extends the existing Consequences enforcement list from four layers to
six, adding the duplicate-id detector and the patch-package layer.
Refs: #553 · vitest-dev/vitest#9957 · vitest-dev/vitest#10267
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous revision allowed vi.mock for virtual modules on the "consumer
import is static" argument. #553 proved that argument wrong: a statically-
imported module with an async factory body whose dynamic import landed
after teardown still produced the race. The factory body — not the
consumer — is the failure surface.
- Drop the "residual exceptions" table.
- Add the binding invariant: factory bodies under `**/*.svelte.{test,spec}.ts`
must be synchronous (no `await`, no `import(...)`).
- Document the canonical vi.hoisted + getter pattern, with file references.
- Record the $app/stores → $app/state architectural call (Markus's
recommendation), removing one of the last two deprecated-import
outliers.
- Record the preload-data=off hardening (Tobias's recommendation) as a
pattern note.
- Update the Enforcement section to list all four defence layers (ESLint,
CI grep, in-suite meta-test, CI birpc assert) and the coverage-flake-
probe verification workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a no-restricted-syntax rule scoped to *.spec.ts / *.test.ts that
flags any vi.mock call whose first argument starts with 'pdfjs-dist'.
Turns the ~2-min CI wait into an immediate lint error on save.
Updates ADR 012 Enforcement section to document the rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Explicitly states no lint rule is planned; CI guard is the backstop
(addresses Elicit OQ-001 from PR #536 round 4)
- Adds a "when to revisit" note: extract shared DynamicImportLoader<T>
if 3+ components adopt the libLoader pattern
(addresses Markus Keller round-4 observation on PR #536)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents why vi.mock(module, factory) races with birpc teardown for
dynamically-imported modules, the libLoader injection pattern used to fix
#535, and the residual exceptions ($app/*, $env/*) that are safe to keep
as vi.mock because they are resolved statically before any test runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records the operational assumption that nightly.yml and release.yml
bake in: the self-hosted runner is single-tenant, so writing secrets
to .env.staging / .env.production on disk and removing them via an
`if: always()` cleanup step is acceptable for v1.
Documents the three migration triggers (second repo on the runner,
untrusted PR execution, move to shared infrastructure) and the
one-step migration path (--env-file <(printf '%s' "$SECRET_BLOB"))
so the next operator does not silently break the trust assumption.
The in-comment notes at the top of both workflow files already point
at this ADR's content; this commit records the decision in the durable
location the doc-currency table demands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the reversal of the earlier "migrate to Hetzner Object Storage"
direction in docs/infrastructure/production-compose.md. Documents the
cost/benefit (current 13 GB fits trivially on the VPS; OBS billing is
dominated by base fee at this size; migration is a three-env-var swap
plus `mc mirror`, no application rewrite cost).
Captures the four triggers that should re-open the decision (50 GB
threshold, healthcheck latency, VPS upgrade cost, backup runtime) so
the deferral does not become an indefinite punt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the decision to make docker-compose.prod.yml a fully self-contained
file rather than an overlay over docker-compose.yml. Captures the cost
(env-var duplication across dev and prod files) and the benefit (single
file the reviewer can hold in their head, no Compose merge-rule
surprises, automatic project-name namespacing for cohabiting staging +
production on one host).
Surfaces the retirement of the earlier overlay narrative in
docs/infrastructure/production-compose.md so a future maintainer does
not reverse the choice out of ignorance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the architectural decision behind isReader = !canWrite &&
!canAnnotate, why BLOG_WRITE intentionally lands on the reader
dashboard, the alternatives considered (separate route, AppUser
column, middleware redirect, BLOG_WRITE exclusion), and the
implications for future permission additions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Markus #4 (PR #366 review). PersonDisplayNameChangedEvent is the first
custom application event in this codebase — the prior @EventListener
(OcrTrainingService.recoverOrphanedRuns) consumed Spring's built-in
ApplicationReadyEvent. The pattern is load-bearing for future cross-domain
decoupling and warrants a documented decision rather than a comment buried
in the listener.
Captures: synchronous-by-default rationale, package layout (event in
publisher's model/, listener in consumer's service/), saveAllAndFlush vs
saveAll for exception surfacing, the migration path to @TransactionalEvent
Listener + @Async if archive growth forces it, and the rejected
alternatives (direct call, DB trigger, Hibernate entity listener).
Refs #362#366
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the reasoning behind persisting two scalar columns on
documents rather than deriving aspect client-side or standing up a
thumbnail_metadata table. Also documents the 1.1 landscape threshold,
the null-during-rollout state, and the ordering invariants inside
ThumbnailService.generate().
Refs #305
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses @mkeller (Markus) — fixes(adr): "the ADR doesn't mention
in-memory BackfillStatus" and "treat this as a layering exception,
acknowledge it explicitly". Two new paragraphs under Operational caveats.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures why thumbnails render in-process rather than being delegated
to ocr-service. Prevents a future reviewer from rehashing the decision
or moving it to the Python side without knowing the trade-offs.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- docs/adr/003-chronik-unified-activity-feed.md: records the session-rollup
decision (LAG + 120-min gap), the dedupe deletion, the single-endpoint
composition, and the German-URL convention.
- frontend/messages/{de,en,es}.json: adds chronik_* keys for page title,
Für-dich box, filter pills, day headers, singleton/rollup verb variants
per kind, empty states, error card, Mehr-laden pagination, and the Bell
footer link retarget.
No pluralization via ICU match — separate singleton/rollup keys per verb,
per the Felix discussion (comment #3573).
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-001 documents the decision to use a separate Python container for
OCR (Surya + Kraken), the interface contract, and why alternatives
like Tess4J were rejected.
ADR-002 documents the decision to store polygon annotations as JSONB
with a 4-point CHECK constraint, backed by an AttributeConverter.
Refs #226, #227
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>