Skip to content

feat(api): include area ancestor path in work item and household item responses#1249

Merged
steilerDev merged 2 commits into
betafrom
feat/1236-area-ancestors-api
Apr 17, 2026
Merged

feat(api): include area ancestor path in work item and household item responses#1249
steilerDev merged 2 commits into
betafrom
feat/1236-area-ancestors-api

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Add AreaAncestor type to the shared package and an ancestors field on AreaSummary, exposing the full parent-chain path for every area in API responses (work items, household items, milestones, timeline, dependencies)
  • Introduce N+1-safe loadAreaMap / resolveAreaAncestors helpers in areaService.ts that batch-load all areas once and walk the parent tree in memory — no additional per-item DB queries
  • Thread the area map through all service callers and add route-level + service-level test coverage for ancestor resolution and response shape

Fixes #1236

Test plan

  • Unit tests: areaService.ancestors.test.ts — ancestor resolution logic (flat, nested, multi-level, root)
  • Integration tests: workItems.ancestors.test.ts + householdItems.ancestors.test.ts — route responses include correct ancestors array
  • Shared type tests: area.test.ts, workItem.test.ts, householdItem.test.ts, timeline.test.ts updated
  • Pre-commit hook quality gates pass

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com
Co-Authored-By: Claude backend-developer (Haiku 4.5) noreply@anthropic.com
Co-Authored-By: Claude qa-integration-tester (Haiku 4.5) noreply@anthropic.com
Co-Authored-By: Claude product-architect (Sonnet 4.6) noreply@anthropic.com

@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your submission! We require all contributors to sign our Contributor License Agreement before we can accept your contribution.

To sign, please comment on this PR with:
I have read the CLA Document and I hereby sign the CLA


I have read the CLA Document and I hereby sign the CLA


Frank Steiler seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

Frank Steiler and others added 2 commits April 17, 2026 12:51
… responses

- Add `AreaAncestor` type to shared package and `ancestors` field on `AreaSummary`, surfacing the full parent-chain path for every area referenced in API responses
- Introduce N+1-safe `loadAreaMap` / `resolveAreaAncestors` helpers in `areaService.ts` that batch-load all areas once and walk the parent tree in memory
- Thread the area map through all callers (workItemService, householdItemService, milestoneService, dependencyService, timelineService, converters) and add test coverage for ancestor resolution and response shape

Fixes #1236

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude product-architect (Sonnet 4.6) <noreply@anthropic.com>
ESM exports are live read-only bindings, so jest.spyOn on a named export
from an ESM module throws TypeError at test-suite setup. Removed the spy
approach in workItems.ancestors.test.ts and rewrote the test to assert
actual ancestor population across all 5 work items. Also added defensive
try/finally blocks around FK-disable pragma statements in both test files
to ensure FK constraints are always re-enabled even on assertion failure.

Refs #1236

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Haiku 4.5) <noreply@anthropic.com>
@steilerDev steilerDev force-pushed the feat/1236-area-ancestors-api branch from d3df50e to 9f2b77b Compare April 17, 2026 10:52
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-owner] All 4 acceptance criteria from #1236 are satisfied. (Using --comment because GitHub blocks self-approval; this represents a PO sign-off from a requirements-coverage standpoint.)

AC coverage

  • AC1 (happy path, root-first, leaf excluded)resolveAreaAncestors walks from areaMap.get(areaId)?.parentId (excludes the leaf) and reverses to root-first. Verified by areaService.ancestors.test.ts "AC1 — linear 3-level chain" and workItems.ancestors.test.ts "AC1 — 3-level chain". Leaf-not-in-ancestors explicitly asserted.
  • AC2 (null-area fallback, 200)getAreaWithAncestors returns null when areaId is null. Covered by workItems.ancestors.test.ts "AC2" and the household list null-area case.
  • AC3 (5-level deep tree, no N+1)areaService.ancestors.test.ts "AC3 — 5-level deep tree" and householdItems.ancestors.test.ts "AC3 — 5-level deep chain" verify correctness. The per-item db.select().from(areas).where(eq(...)) query is replaced by a single loadAreaMap(db) call at the top of listWorkItems and listHouseholdItems, threaded through summary/detail converters — N+1 is eliminated by construction. Timeline, milestone, and dependency services are also threaded through the batched map.
  • AC4 (orphaned area, partial chain, 200)resolveAreaAncestors breaks the loop on a missing ancestor and returns what was resolved so far. workItems.ancestors.test.ts "AC4" inserts a dangling parentId, hits the API, and asserts 200 with area.ancestors === []. Cycle protection (MAX_DEPTH=20 + visited set) is also exercised.

Observations

  • Additive, non-breaking API change — existing AreaSummary consumers continue to work (ancestors defaults to []).
  • Shared type tests across area, workItem, householdItem, and timeline updated consistently.
  • The explicit jest.spyOn efficiency check for "loadAreaMap called once" was removed due to ESM read-only bindings (documented with comment + MEMORY entry). Acceptable — the batching is verified by code inspection of the clear refactor from per-item query to single pre-loaded map.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-architect] Approved (comment — cannot self-approve own PR).

Verified

  • API contractAreaAncestor and ancestors: AreaAncestor[] on AreaSummary are purely additive. Wiki API-Contract.md reflects both the interface definition (lines 679-691) and example payloads across work items, household items, timeline, and milestones.
  • Ordering — root-first ordering is guaranteed: the loop climbs parent-ward and ancestors.reverse() flips the chain before return. Leaf is correctly excluded (walk starts at areaMap.get(areaId)?.parentId).
  • Performance / no N+1loadAreaMap is called exactly once per list request in listWorkItems, listHouseholdItems, getDependencies, and timeline's existing batch-fetch path. Detail endpoints load once per call. Nested callers receive the map as a threaded argument — no per-item DB queries.
  • Cycle & orphan guardsvisited: Set<string> + MAX_DEPTH = 20 bound the walk; if (!entry) break returns a partial chain on orphan. Covered by cycle protection: does not throw, orphaned parent: returns empty array, and two integration tests flipping PRAGMA foreign_keys = OFF with try/finally to restore.
  • Code quality — Strict TS, import type, .js ESM extensions, no any in new code. Test coverage is thorough (service unit tests + route integration tests on both detail and list endpoints).

Informational (non-blocking)

  • getAreaWithAncestors in workItemService.ts and householdItemService.ts casts the narrow AreaMapEntry via as typeof areas.$inferSelect to satisfy toAreaSummary. A cleaner future refactor would be to adjust toAreaSummary's input to accept the minimal AreaMapEntry shape directly. Not blocking.
  • Milestone and work-item detail/update paths each load the full area map per call. Fine at current scale (<5 users); worth revisiting if a batch milestone API ever lands.
  • The jest.spyOn ESM limitation noted in qa-integration-tester/MEMORY.md is a useful learning — verifying "called once" via observable behavior is the right call here.

@steilerDev steilerDev merged commit d753e14 into beta Apr 17, 2026
31 of 32 checks passed
@steilerDev steilerDev deleted the feat/1236-area-ancestors-api branch April 17, 2026 11:08
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant