feat(themen): count documents across the whole sub-topic tree (#698) #701

Merged
marcel merged 8 commits from worktree-feat+issue-698-themen-subtree-count into main 2026-05-31 12:58:10 +02:00
Owner

Closes #698.

Each /themen box now counts documents across its whole sub-topic tree (distinct), so the number matches what /documents?tag=X actually returns. Implements Option B: a new subtreeDocumentCount field — reader surfaces read the rollup, admin surfaces keep the direct documentCount.

What changed

Backend

  • TagTreeNodeDTO gains subtreeDocumentCount; getTagTree() maps both counts onto each node from two aggregate queries (no N+1 — NFR-PERF-THEMEN-01).
  • New TagRepository.findSubtreeDocumentCountsPerTag(): one recursive-CTE tag closure (depth guard ≤50) joined to document_tags, GROUP BY ancestor_id with COUNT(DISTINCT document_id) (REQ-THEMEN-01/02/03/06). Direct documentCount is unchanged.
  • Corrected the stale getTagTree() JavaDoc + tag/README.md.

Frontend

  • hasAnyDocuments keys on subtreeDocumentCount > 0 (single field read).
  • /themen page (header, child rows, aria-labels) and the dashboard ThemenWidget display subtreeDocumentCount. A parent with 0 direct docs but populated children now shows a non-zero total.
  • Admin sidebar, merge preview and delete-impact guard are unchanged (still direct documentCount).

Tests

  • Real-Postgres (postgres:16-alpine, Testcontainers) integration tests: leaf = direct (AC#4), shared doc counted once (AC#1+#2), full grandchild depth (AC#3), empty subtree absent (REQ-THEMEN-05), cycle terminates via the guard (REQ-THEMEN-06), and rollup == distinct documents from the real search expansion (AC#7 parity).
  • Service unit tests for the two-query wiring; frontend hasAnyDocuments tests; AC#5 component test; admin characterization tests pinning previews to the direct count (AC#8).

Notes for the reviewer

  • generated/api.ts was hand-edited to match generate:api output (codegen needs a live dev backend); re-running it should be byte-equivalent.
  • Browser-mode component tests are validated on CI, not locally (project convention).

🤖 Generated with Claude Code

Closes #698. Each `/themen` box now counts documents across its **whole sub-topic tree** (distinct), so the number matches what `/documents?tag=X` actually returns. Implements **Option B**: a new `subtreeDocumentCount` field — reader surfaces read the rollup, admin surfaces keep the direct `documentCount`. ## What changed **Backend** - `TagTreeNodeDTO` gains `subtreeDocumentCount`; `getTagTree()` maps **both** counts onto each node from two aggregate queries (no N+1 — **NFR-PERF-THEMEN-01**). - New `TagRepository.findSubtreeDocumentCountsPerTag()`: one recursive-CTE tag closure (depth guard ≤50) joined to `document_tags`, `GROUP BY ancestor_id` with `COUNT(DISTINCT document_id)` (REQ-THEMEN-01/02/03/06). Direct `documentCount` is unchanged. - Corrected the stale `getTagTree()` JavaDoc + `tag/README.md`. **Frontend** - `hasAnyDocuments` keys on `subtreeDocumentCount > 0` (single field read). - `/themen` page (header, child rows, aria-labels) and the dashboard `ThemenWidget` display `subtreeDocumentCount`. A parent with 0 direct docs but populated children now shows a non-zero total. - Admin sidebar, merge preview and delete-impact guard are **unchanged** (still direct `documentCount`). ## Tests - **Real-Postgres** (`postgres:16-alpine`, Testcontainers) integration tests: leaf = direct (AC#4), shared doc counted once (AC#1+#2), full grandchild depth (AC#3), empty subtree absent (REQ-THEMEN-05), cycle terminates via the guard (REQ-THEMEN-06), and rollup == distinct documents from the real search expansion (AC#7 parity). - Service unit tests for the two-query wiring; frontend `hasAnyDocuments` tests; AC#5 component test; admin characterization tests pinning previews to the direct count (AC#8). ## Notes for the reviewer - `generated/api.ts` was **hand-edited** to match `generate:api` output (codegen needs a live dev backend); re-running it should be byte-equivalent. - Browser-mode component tests are validated on CI, not locally (project convention). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 8 commits 2026-05-31 12:57:43 +02:00
Add subtreeDocumentCount to TagTreeNodeDTO, populated by a new recursive-CTE
aggregate query that builds a tag closure and counts distinct documents per
ancestor subtree. The direct documentCount is unchanged; getTagTree now maps
both counts onto each node from two aggregate queries (no N+1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover AC#1-4 (leaf=direct, distinct overlap counted once, full descendant
depth), REQ-THEMEN-05 (empty subtree absent), REQ-THEMEN-06 (cycle terminates
via the 50-level guard) and AC#7 (rollup equals distinct documents found by the
real tag-search expansion — count↔destination parity). Testcontainers
postgres:16-alpine since the recursive CTE + COUNT(DISTINCT) needs real PG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record that getTagTree returns both documentCount (direct, read by admin
surfaces) and subtreeDocumentCount (rollup, read by the reader surfaces),
matching the corrected getTagTree JavaDoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regenerate the TagTreeNodeDTO type with subtreeDocumentCount and switch
hasAnyDocuments to read it directly — the backend rollup already includes all
descendants, so the recursive children walk is no longer needed. Reader
surfaces now hide a topic only when its whole subtree is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /themen page (box header, child rows, aria-labels) and the dashboard
ThemenWidget now display subtreeDocumentCount instead of the direct
documentCount, so a topic's number reflects its whole sub-topic tree and
matches what /documents?tag=X actually returns. A parent with 0 direct
documents but documents under its children now shows a non-zero total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TagTreeNodeDTO now requires subtreeDocumentCount, so the admin sidebar test
fixtures (TagTreeNode, TagsListPanel) need the field to type-check. The admin
sidebar still renders the direct documentCount — these fixtures only gain the
new field, no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Characterization tests for AC#8: the merge preview and the delete-impact
warning describe direct-document operations, so they must report the tag's
direct documentCount, never a subtree rollup. Both tests pass a stray
subtreeDocumentCount and assert it does not leak into the preview, so a future
change can't silently desync a destructive-action preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(tag): explicitly stub the subtree rollup query in getTagTree tests (#698)
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 19s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 3m18s
CI / OCR Service Tests (push) Successful in 21s
CI / Backend Unit Tests (push) Successful in 3m25s
CI / fail2ban Regex (push) Successful in 43s
CI / Semgrep Security Scan (push) Successful in 20s
CI / Compose Bucket Idempotency (push) Successful in 29s
a76999c3d4
Address review nit: the older getTagTree tests relied on Mockito's default
empty-list return for findSubtreeDocumentCountsPerTag. Stub it explicitly so
the two-query contract is self-documenting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
marcel force-pushed worktree-feat+issue-698-themen-subtree-count from fc2c726961 to a76999c3d4 2026-05-31 12:57:43 +02:00 Compare
marcel merged commit a76999c3d4 into main 2026-05-31 12:58:10 +02:00
marcel deleted branch worktree-feat+issue-698-themen-subtree-count 2026-05-31 12:58:11 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#701