Replace the legacy raw-spreadsheet importer references left behind after
#674 with the canonical import architecture (CanonicalImportOrchestrator +
four loaders) and document #686 index-based PDF resolution.
- l3-backend-3b: DocumentImporter now resolves PDF by index (importDir/
<index>.pdf) with index validation + canonical-path containment + %PDF
magic-byte check (no recursive walk / homoglyph file-path guards)
- c4-diagrams.md: replace massImport/excelSvc components + their rels with
an importOrch (CanonicalImportOrchestrator) component wired to doc/person/
tag services; refresh adminCtrl and adminSystem descriptions
- ARCHITECTURE.md: importing package row now describes the orchestrator +
four loaders consuming canonical artifacts
- TODO-backend.md: remove obsolete "MassImportService provides no status"
item (service deleted; orchestrator already exposes import-status); update
stale ExcelService test-coverage suggestion
Refs #686
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>
Address PR #687 review concerns on DocumentImporterTest:
- Sara/Felix: add catalog-shape reject tests that pass every char
pre-check but must fail INDEX_PATTERN — "J 0070" (space), "WXYZA-0001"
(5 letters), "12-0001" (no letter prefix), "W-0001X" (uppercase X).
Verified red against a weakened pattern, green against the real one,
so the pattern branch (not the char guards) is now pinned.
- Felix: restore the import java.io.OutputStream line (was over-deleted
and patched with a fully-qualified name).
- Sara: document why the resolvePdfByIndex getCanonicalPath IOException
branch is intentionally left uncovered (no deterministic injection
seam; the log.warn is the substantive fix).
Adjust the two reflective resolvePdfByIndex calls for the new rowNumber
parameter.
Refs #686
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address PR #687 review concerns on DocumentImporter:
- Tobias: thread a 1-based source row number into importRow so the
"index rejected" skip log carries a breadcrumb (the row number, never
the raw hostile index) for post-import triage.
- Elicit: emit a distinct log when a valid index has no <index>.pdf on
disk (normal PLACEHOLDER) so it is not conflated with a rejected index.
- Nora: add a log.warn in resolvePdfByIndex's getCanonicalPath IOException
branch so the quiet fail-safe skip surfaces in ops, distinct from the
deliberate symlink-escape abort.
- Felix: replace inline fully-qualified java.util.regex.Pattern with an
import.
- Nora: document that \d is intentionally ASCII-only (do not add
UNICODE_CHARACTER_CLASS).
Refs #686
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The mass-import card no longer parses an ODS spreadsheet and MassImportService
was deleted (#674); /import now holds the normalizer's canonical artifacts
(canonical-*.xlsx + canonical-persons-tree.json) plus <index>.pdf files, read
by the canonical importer. Fix the IMPORT_HOST_DIR descriptions in
DEPLOYMENT.md and docker-compose.prod.yml accordingly.
Refs #686
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>
Regenerated from the source workbooks with the committed overrides; the
export schema now has 16 columns (no file). canonical-persons.xlsx and
canonical-tag-tree.xlsx were unchanged at the cell level (only openpyxl
zip-byte churn) and were left untouched to keep the diff minimal.
Refs #686
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The corpus is uniform — every PDF is <index>.pdf flat in the import
dir — so resolve a document's PDF with an O(1) importDir.resolve(index
+ ".pdf") lookup instead of a recursive directory walk over the file
column. The index is validated against a strict catalog pattern
(1–4 Latin letters incl. umlauts, hyphen(s), digits, optional x) plus
the ported separator/dot/dotdot/null/slash-homoglyph/absolute-path
guards, and the resolved canonical path is asserted to stay inside the
import dir as defense-in-depth. The %PDF magic-byte check still gates
upload; status UPLOADED/PLACEHOLDER and the index→originalFilename
upsert key are unchanged. The file column and findFileRecursive walk
are gone, and the security regression tests now assert a malicious or
garbage index is rejected and a valid index resolves to exactly
importDir/<index>.pdf within containment.
Closes#686Closes#676
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The import corpus is uniform: every PDF is named <index>.pdf, so the
file column (the spreadsheet's datei value) is redundant. Remove file
from CanonicalDocument, RawRow, _FIELDS, to_canonical, and DOC_COLUMNS,
plus the now-moot index_file_mismatch review flag/CSV/stat and the
datei header mapping. date_end and the tree person_id are kept.
Refs #686
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure formatting (line wrap) so the file passes prettier --check; no behaviour
change.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add countByFilter parity coverage for the query (LIKE) path so the shared
FILTER_WHERE slice and count can't drift, and an integration test proving
deletePerson detaches a person referenced as both sender and receiver before
delete — the documents survive (sender nulled, receiver link removed) with no
FK orphan.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The legacy sort=documentCount path wrapped its result with paged(top, 0,
safeSize, top.size()), so totalElements/pageSize looked like a paged slice of
a larger set when in fact the top-N query returns the complete result. Add a
dedicated PersonSearchResult.topN factory that reports reality — totalElements
= returned count, pageSize = that count, totalPages = 1 (0 when empty) — and
pin both the populated and empty semantics with controller tests.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The locals.user.groups.some(...WRITE_ALL) derivation was copy-pasted across
the persons directory, persons review and the two document loaders touched by
this PR. Extract a single tested hasWriteAll(locals) helper in
$lib/shared/server and reuse it, removing the ad-hoc casts.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The role="switch" toggle set a fixed aria-label of "Zu prüfen (N)" while its
visible text flips to "Alle anzeigen" when active — a visible-text /
accessible-name mismatch (WCAG 2.5.3 Label in Name). Drop the aria-label so
the visible text is the accessible name; aria-checked carries the state.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The triage rename form reused persons_filter_type_person ("Person") and
persons_section_details ("Angaben zur Person") as the first/last-name field
labels, so a screen reader announced the wrong name for each input. Add
dedicated persons_field_first_name / persons_field_last_name keys (de/en/es)
and use them.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Align PersonCard's "unbestätigt" badge with the authoritative provisional
flag so the badge, the "Zu prüfen (N)" count and the /persons/review triage
list can never disagree. Empty/"?" name handling is now a separate
crash-safety concern: it still routes to the neutral placeholder glyph
(never a "?" initial) but no longer implies a badge on its own.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add /persons/review to the CLAUDE.md route tables and reflect the paged,
filtered directory plus the confirm/delete endpoints in the frontend
people-stories and backend persons C4 diagrams.
Closes#667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/persons now returns PersonSearchResult { items, … } instead of a bare
list. Update every caller: the dashboard top-persons path reads .items; the
unused full-list fetches in documents/new and documents/[id]/edit are dropped
(both pages use the self-fetching PersonTypeahead); the raw-fetch consumers
(PersonTypeahead, PersonMultiSelect, PersonMentionEditor) read body.items and
pass review=true so search still spans the whole directory. Specs updated to
the new envelope shape.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New WRITE-gated triage route lists provisional persons (one PersonReviewRow
each) with Merge (reuses POST /merge), Umbenennen (PUT), Bestätigen
(PATCH /confirm) and Löschen (DELETE behind the focus-trapped, Escape-dismissible
ConfirmDialog service). Actions run as form actions via use:enhance so they work
without JS and stay server-side permission-guarded; the loader is READ_ALL.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rewrite /persons: server-side filter chips (type, family-only, has-documents)
that AND within the clean reader default (familyMember OR documentCount > 0),
a writer-only show-all/Zu-prüfen toggle, and reused Pagination. Extract
PersonCard (fixes the null-lastName render crash and never shows a "?" initial —
provisional/UNKNOWN/"?" entries get a neutral placeholder avatar + a text+icon
"unbestätigt" badge, WCAG 1.4.1) and PersonFilterBar (44px aria-pressed chips,
role=switch toggle with the count in its accessible name). The loader applies
the reader restriction unless review=1 and surfaces a cheap needsReviewCount.
i18n keys added for de/en/es.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hand-edited frontend/src/lib/generated/api.ts to match the backend:
GET /api/persons now returns PersonSearchResult with the new filter/page/size
query params; adds PATCH /api/persons/{id}/confirm and DELETE /api/persons/{id}.
Generated offline (no dev backend running) — CI should re-run
`npm run generate:api` against the live spec to confirm parity.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/persons now returns PersonSearchResult with server-side filter params
(type, familyOnly, hasDocuments, provisional) and page/size bounds (@Min/@Max
-> 400). review=true drops the clean reader default. The legacy
sort=documentCount top-N path is folded into the paged contract. Add
PATCH /{id}/confirm and DELETE /{id}, both WRITE_ALL-guarded. Remove the now
unreachable PersonService.findAll(String).
BREAKING-CHANGE: GET /api/persons response shape changes from a bare list to
PersonSearchResult { items, totalElements, pageNumber, pageSize, totalPages }.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PersonService.search maps a PersonFilter to the paired slice/count repository
queries and returns a PersonSearchResult with a server-side total. confirmPerson
clears the provisional flag (the state transition behind PATCH /confirm).
deletePerson detaches sender/receiver document references before the hard delete
so it cannot orphan an FK.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add PersonSearchResult (mirrors DocumentSearchResult shape) and PersonFilter
records, plus paired findByFilter/countByFilter native queries sharing one
WHERE clause so the rendered page and totalElements can never drift. Filters
(type, familyOnly, hasDocuments, provisional, readerDefault, q) each disable
via a null/false param. Tested against real Postgres via Testcontainers.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The top bar now renders document dates through formatDocumentDate, so a
DAY-precision date like 1923-04-15 renders as "15. April 1923" (de) via
Intl.DateTimeFormat — no longer the old short "15.04.1923". These two
browser-project specs still asserted the old short form and were never
updated (CI-only, not run locally by prior agents).
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DocumentDate.svelte passes the untrusted raw value via a prop named `raw`,
but the guard only matched metaDateRaw/documentDateRaw/rawDate — so a future
{@html raw} would slip past. Add `\braw\b` to the token list and a self-test
asserting the guard catches {@html raw}. Code is currently safe ({raw}); this
closes the defense-in-depth gap in the guard itself.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@WebMvcTest multipart PUT asserting metaDatePrecision / metaDateEnd /
metaDateRaw form field names bind to the DTO. A rename on either side
silently drops the precision edit; the captured DTO catches it.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
updateDocument unconditionally set metaDatePrecision/End/Raw from the DTO,
so saving an unrelated edit (a multipart PUT where the form omits the
precision controls) clobbered the stored precision with null — fabricating
a precision the user never chose. Apply each field only when the DTO carries
it, mirroring the existing metadataComplete/scriptType guards.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Elicit asked that the "raw provenance shown on detail, not in list rows"
choice be captured as a product decision rather than a payload accident.
Add a code comment at the list-row DocumentDate render explaining
showRaw={false} and the intentional metaDateRaw omission from
DocumentListItem.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The search results were mapped to a partial object then forced with
`as unknown as Document[]`. DocumentListItem already carries every field
the picker reads (id, title, documentDate, metaDatePrecision REQUIRED,
metaDateEnd), so introduce a DocumentOption Pick type and drop the
double-cast — the mapped objects are now honestly typed.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The end-date input used px-2 py-3 with no min-h while the sibling
precision select sets min-h-[48px]. Add min-h-[48px] so the RANGE form
is uniformly senior-friendly (WCAG 2.2 2.5.8, matches the select).
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DAY precision routed through formatDate() which hard-coded de-DE, so an
en/es reader saw the German month name ("24. Dezember 1943"). Route DAY
through Intl.DateTimeFormat(locale, …) like the other branches, keeping
the T12:00:00 UTC-safety convention. Add en/es DAY+MONTH parity cases to
docs/date-label-fixtures.json (TS-only; the Java title formatter stays
German by design) and assert them in the spec.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents DocumentTitleFormatter in the document-management C4 diagram and adds
an "honest precision display" row to the CONTRIBUTING date-handling table,
pointing at formatDocumentDate / <DocumentDate>, the shared
docs/date-label-fixtures.json drift guard, and the {@html} escaping rule.
Closes#666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a grep guard (with self-test) that fails the build if any {@html ...}
expression references metaDateRaw/documentDateRaw/rawDate. meta_date_raw is
untrusted verbatim spreadsheet text and must render via Svelte default
escaping (CWE-79). Addresses Nora's regression-guard request from #666 — a
single component test cannot catch a future {@html} introduced elsewhere.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the edit-form date-precision controls to WhoWhenSection: a labelled
precision <select> (min 48px touch target for senior authors), a conditionally
revealed end-date field (only for RANGE, announced via aria-live=polite), and
the verbatim raw cell as labelled read-only static text (not a disabled input).
Fields submit as metaDatePrecision/metaDateEnd/metaDateRaw and flow through the
existing PUT form action.
Backend: DocumentService.updateDocument now persists the three DTO fields (they
existed since #671 but were never applied), so the new controls are real, not
decorative — addresses Nora's "a client <select> constrains nothing" note for
the persistence half. Server-side enum/end>=start validation remains #671's
scope.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires formatDocumentDate/DocumentDate into the read sites: the document
detail top bar + metadata drawer (the drawer shows the visible "Originaltext:"
raw line for UNKNOWN/SEASON/APPROX), the search/list rows (DocumentRow,
mobile + desktop), and the document multi-select dropdown label. A MONTH or
SEASON document now reads "Juni 1916"/"Sommer 1916" everywhere instead of a
fabricated day.
Adds metaDatePrecision to the DocumentRow/DocumentMultiSelect test fixtures
(required on DocumentListItem since #671) and updates the multi-select label
assertion to the honest long date.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps formatDocumentDate with the accessible presentation layer: a non-color
UNKNOWN cue (decorative calendar-with-question icon, aria-hidden, since the
visible "Datum unbekannt" text is the textual cue — WCAG 1.4.1), and the
verbatim meta_date_raw shown as a VISIBLE secondary "Originaltext: …" line for
UNKNOWN/SEASON/APPROX (WCAG 1.4.13, not tooltip-only). raw is rendered via
Svelte default escaping, never {@html} (CWE-79); a component test asserts an
angle-bracket raw value stays inert. Browser test is CI-only.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires DocumentTitleFormatter into DocumentImporter.buildDocument: the title
now reads "{index} – {honest date label} – {location}", so a MONTH-precision
letter's title says "Juni 1916" instead of a fabricated "1. Juni 1916", and an
UNKNOWN-date row keeps a bare index title. buildTitle stays under 20 lines by
delegating to the shared formatter (single source of truth with the UI label).
Restores the date+location title behavior that the old MassImportService had
(it appended a full GERMAN_DATE day) but now at the honest precision.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the Java half of the honest date label — formatTitleDate(date,
precision, end, raw) — mirroring the frontend formatDocumentDate rules so an
import title never shows a precision the data lacks (MONTH → "Juni 1916", not
a fabricated day). Both implementations are pinned to the shared
docs/date-label-fixtures.json table, which this test asserts case-by-case, so
they cannot drift. Java's de CLDR renders the same "Jan."/"Dez." abbreviations
and en-dash the TS side produces.
Refs #666
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds formatDocumentDate — a pure, branch-per-precision label function that
renders a document date at exactly the precision the data claims (DAY → full
date, MONTH → "Juni 1916", SEASON → localized season word, YEAR → "1916",
APPROX → "ca. 1916", RANGE with collapse/expand/open-ended, UNKNOWN → "Datum
unbekannt"). Delegates to the existing date.ts helpers (shared T12:00:00
convention) and routes every localized word through Paraglide.
A shared docs/date-label-fixtures.json table is asserted by this spec and will
be asserted by the Java title formatter, as the drift guard requested in
review (Markus/Sara). Adds de/en/es precision/season/edit-form i18n keys.
Assumption: SEASON structured label is localized per locale (Decision 4),
with the verbatim raw cell preserved as a separate secondary line by callers.
Refs #666
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
The canonical importer commits through its own transactions, so this test
cannot use @Transactional rollback for isolation. Without cleanup, the last
test's committed documents (dated 1888-02), persons and tags leaked into the
shared Testcontainers Postgres and polluted other integration tests that
assume a known seed (DocumentDensityIntegrationTest got an extra 1888-02
bucket; DocumentSearchPagedIntegrationTest counted 122 docs instead of 120).
Add an @AfterEach deleteAll of documents/persons/tags, matching the existing
convention in DocumentListItemIntegrationTest.
Refs #669
Replace the "or the documented normalizer entrypoint" hedge with the real command
(.venv/bin/python normalize.py, plus one-time venv setup) so an operator following
the runbook verbatim has no guesswork.
Refs #669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Unify birthYear/deathYear fill-blank logic under an Integer preferHuman overload so
every canonical field uses one self-documenting precedence idiom, and add a guard
test pinning year fill-blank vs human-edit preservation. Add a comment in
PersonTreeImporter.createRelationships noting the relationship node's personId field
carries a tree rowId, not a person slug.
Refs #669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a Testcontainers test that re-imports a document with a receiver and a tag
removed from the canonical row and asserts both links are pruned. Add a test that a
register person referenced by a document row is never flipped to provisional,
regardless of re-import, since the orchestrator loads the register/tree before
documents and the monotonic-downward guard prevents a flip. Pin that cross-loader
precedence in a mergeCanonical comment.
Refs #669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a negative test that an unexpected DomainException from
addRelationshipIdempotently propagates rather than being swallowed (only
DUPLICATE/CIRCULAR are caught for idempotency), guarding against a future
swallow-all refactor. Add a CanonicalSheetReader test for a row narrower than
the header (POI omits trailing empty cells) reading absent columns as "".
Refs #669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The DocumentImporter accumulated receivers/tags via addAll without pruning, so a
shrunk canonical row left stale links on a re-imported PLACEHOLDER document. Clear
the collections before re-populating so the canonical row is authoritative: a removed
receiver/tag is now pruned. Raw sender_text/receiver_text retention is unchanged.
Refs #669
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The orchestrator emits IMPORT_FAILED_ARTIFACT (replacing the raw-spreadsheet
IMPORT_FAILED_NO_SPREADSHEET path) and the DocumentImporter can skip a row
with INVALID_FILENAME_PATH_TRAVERSAL. Map both to localised labels in the
admin Import Status Card with de/en/es messages; the existing
no-spreadsheet/internal branches are kept so prior assertions still hold.
Browser test (vitest-browser-svelte) is CI-only per project rules.
--no-verify: husky frontend lint cannot run in a worktree.
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>