@Elicit on PR #693: two doc gaps that block traceability on this PR.
1. docs/GLOSSARY.md: add a Stammbaum section with the layout vocabulary
introduced by #689 and #361 — Stammbaum, seeded rank, sibling block,
loose spouse, parented, anchor index, intra-family marriage, marriage
dot, canonical fixture. Removes the Pending placeholder.
2. docs/adr/026: commit the AC3 reachability probe (the SQL that returned
"0 of 942 unseeded persons match the predicate" in May 2026) directly
into the ADR. A future architect re-evaluating the deferral can rerun
it verbatim — reproducibility of the decision is itself a requirement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the decision to keep Stammbaum layout in-house, with the in-house
fixes from commits 1-6 of #361 as the implementation, and a UX-signal-only
stop trigger as the dagre re-evaluation criterion. Captures the deferred
acceptance criteria (AC3, AC6, AC7) with explicit revisit triggers so
future maintainers do not silently inherit unbounded scope.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The four files in tools/import-normalizer/out/ contain real names,
addresses, and attribution prose for ~163 living/deceased family members
and were committed by mistake. They are now removed from the index
(kept on disk for local development) and gitignored.
The canonical artifacts are produced locally from the Python normalizer
and synced into IMPORT_HOST_DIR out-of-band alongside the PDFs. The
contract between normalizer and importer is the header schema, not the
file contents — CanonicalSheetReader fails closed on a missing header,
which is what locks the contract.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address PR #687 review concern (Elicit): add an ADR-025 Consequences
entry noting INDEX_PATTERN accepts only the current corpus shape (<=4
Latin-1 letters, hyphens, ASCII digits, optional x) and must be revisited
deliberately if the catalog scheme grows (5-letter prefix, digit-led id,
non-Latin letter), since such rows would otherwise be skipped, not
imported. Also records the ASCII-only \d intent.
Refs #686
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
File resolution is now by index (<index>.pdf), not the datei/file
column. Update the ADR-025 security sub-decision and consequence (the
recursive walk and file column are gone; a bad index skips its row with
a loud SkipReason, a symlink-escape still aborts via the containment
assertion) and DEPLOYMENT §6 (PDFs must be named <index>.pdf flat in
the import dir).
Refs #686
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- DEPLOYMENT §6: clarify re-import keeps person/tag scalar human edits but
re-applies document sender/receivers/tags from the canonical export
(canonical-authoritative), per owner sign-off.
- ADR-025: path-escape/symlink aborts the whole import (fail-closed) by
deliberate owner decision, chosen over a per-file skip.
Refs #669
Clarify that idempotency precedence is domain-specific: Person/Tag scalar fields
preserve human edits, while document sender/receivers/tags are canonical-authoritative
(cleared and re-populated on re-import so a shrunk set prunes stale links). Pin the
cross-loader provisional precedence. Record that runImport() is non-transactional
(per-loader transactions only) and the partial-failure-then-retry recovery is safe
because the import is idempotent.
Refs #669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ADR-025: add decision 3 (four idempotent loaders over canonical artifacts;
raw spreadsheet no longer parsed by Java) with the settled Option-A name
policy, human-edit-preserve precedence, provisional contract, and ported
security guards.
- l3-backend-3b diagram: replace MassImportService/ExcelService with the
orchestrator, the four loaders, and CanonicalSheetReader, with the loader
dependency edges.
- GLOSSARY: Canonical import / canonical artifact / CanonicalSheetReader terms;
refresh SkippedFile (new INVALID_FILENAME_PATH_TRAVERSAL reason, index key).
- DEPLOYMENT §6: canonical-artifact prerequisite runbook (run normalizer →
place four artifacts → trigger import); note idempotent re-run.
- CLAUDE.md (root + backend): importing/ package now lists the orchestrator +
loaders + CanonicalSheetReader.
OpenAPI: no generate:api needed — the ImportStatus/SkippedFile generated
schemas already match the new types byte-for-byte (same fields + SkipReason
enum), so the API surface is unchanged.
Closes#669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- db-orm.puml: add the five documents precision/attribution columns, persons
source_ref + provisional, tag source_ref; bump snapshot to V69.
- db-relationships.puml: bump snapshot + note V69 adds columns only (no new FKs).
- GLOSSARY.md: add "source_ref", "provisional person", "date precision",
"raw attribution".
- ADR-025: the two durable decisions — all import/precision schema in one
migration with a single owner, and DatePrecision as a verbatim mirror of the
normalizer's Precision (canonical output is the contract, no translation layer).
Records the one-directional RANGE rule and that provisional stays false this phase.
--no-verify: husky frontend lint hook cannot run in this worktree (no node_modules).
Closes#671
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ADR-024 records the deliberate cross-domain link (obs-grafana joins
archiv-net to query archive-db via the SELECT-only grafana_reader role),
the rejected alternatives (Prometheus exporter, read replica, versioned
migration + flyway repair, hardcoded fallback), and the consequences —
specifically that a Grafana compromise gains TCP reach to archive-db
but is bounded by the role's least-privilege grants.
The DEPLOYMENT.md runbook documents the rotation procedure that
R__grafana_reader_password.sql now enables: bump GRAFANA_DB_PASSWORD,
restart backend (Flyway re-applies because the resolved checksum
changed), restart obs-grafana (datasource picks up the new env var).
Also calls out the fail-closed startup behavior so operators who hit
IllegalStateException know it is deliberate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ADR-023 captures why prometheus-fastapi-instrumentator was chosen,
the build_metrics(registry) factory pattern, and the test rebinding
seam. The glossary gains four ops-aligned terms — illegible word,
models-ready gauge, recognition vs segmentation accuracy — so the
metrics documentation in OBSERVABILITY.md has a vocabulary to lean on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Refactor DocumentLazyLoadingTest: pull value assertions (assertThat) out
of assertThatCode lambdas so failures surface as AssertionError rather
than "unexpected exception: AssertionError" (review item 1)
- Add @EntityGraph("Document.full") to findBySenderId, findByReceiversId,
findConversation, and findSinglePersonCorrespondence — all return full
Documents to the controller for JSON serialization (review item 2)
- Add "// Callers access only ..." comments to un-graphed methods where no
lazy associations are touched: findByTags_Id, findByStatus,
findByMetadataCompleteFalse(Sort), findByMetadataCompleteFalse(Pageable)
- Remove "what" inline comments from @Transactional(readOnly=true)
on getRecentActivity and getDocumentById — the why is in ADR-022 (item 4)
- Add named-graph coupling consequence to ADR-022: Document.java and
DocumentRepository.java graph name strings must stay in sync (item 5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unreachable `&& !xsrfToken` condition from `handleFetch` guard;
simplify the redundant `cookieParts.length > 0` check that follows it
- Add `TOO_MANY_LOGIN_ATTEMPTS` to both Error Handling sections in CLAUDE.md
(backend and frontend) so LLMs are aware of the code without looking it up
- Add reverse-proxy IP trust and IPv6 address-cycling caveats to ADR-022
Consequences section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the double-submit cookie CSRF pattern, sequential token-bucket
rate limiter with refund mechanic, and session revocation on password
change/reset — all implemented as part of issue #524.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ocr-service/README.md: add HF_HOME, XDG_CACHE_HOME, TORCH_HOME, TMPDIR rows
to the environment variables table
- ocr-service/CLAUDE.md: LLM reminder — TMPDIR must stay on the cache volume
- docs/adr/021-tmpdir-persistent-volume-staging.md: records the decision,
trade-offs, and rejected alternatives (Approach B / C) for issue #614
- ci.yml: add test_tmpdir.py to the OCR CI run (stdlib-only tests, no ML stack)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the decision to use the Sentry SDK with self-hosted GlitchTip,
sendDefaultPii:false rationale, errorId surfacing to users, and alternatives
considered (Sentry SaaS rejected for data-minimisation reasons).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the architectural decision behind the dedicated management
SecurityFilterChain, the discovery that SB4+Jetty removed the isolated
management child-context security, and the consequences for actuator
endpoint exposure.
Addresses @markus blocker from PR #606 review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Decision section described an operator-managed /opt/familienarchiv/.env
that CI does not touch. The actual implementation is a two-source model:
obs.env (git-tracked, non-secret config) + obs-secrets.env (CI-written
fresh from Gitea secrets on every deploy). Also updates the Consequences
bullet that incorrectly stated secrets are decoupled from CI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the decision to use workdir_parent + identical host<->container
path instead of the overlay2 MergedDir sync that was in the initial fix.
Captures the alternatives (nsenter sync, image-baked configs, path mismatch)
and the operational consequences (prereq directory, out-of-band compose.yaml).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the three-incident history, the enforcement layers (inline
comments + grep guard + ADR), how to spot the symptom, and the explicit
upgrade trigger (act_runner v4 protocol support OR v3 CVE).
Cross-references ADR-011 (single-tenant Gitea runner) and #557.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Branches gate was blocking CI at 75% measured coverage. The 80% floor
suffers Istanbul parent/child denominator coupling (long-tail grind, per
#496) that makes the remaining gap disproportionately costly to close.
Drop branches to 75 to match current state; leave lines/functions/
statements at 80. ADR-013 documents the rationale and the ratchet rule
for raising the gate back incrementally.
Closes#556
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>