Project hierarchy aggregation — child rollup, parent inheritance, per-surface policy #4

Open
opened 2026-05-06 13:24:36 +00:00 by mAi · 0 comments
Collaborator

Problem

paliad's project model is hierarchical (Client → Litigation → Patent → Case), but most read surfaces only show direct-project rows — they don't aggregate descendants. Concrete bug m hit today: a Client project with multiple Case-level deadlines renders "Keine Fristen" on its /projects/{client_id} page because the deadlines live on descendant Case rows, not on the Client itself.

The fix is conceptually obvious — "include children" — but the right answer is surface-specific:

  • Aggregation up (descendants roll into ancestors) is right for read views: deadlines list on a Client should show all deadlines under that subtree.
  • Narrowing (only this project) is right for editing context: when you create a deadline, it lives on the specific Case row, not the parent.
  • Team aggregation has its own twist: m wants a Client's "effective team" to include all descendants' team members for some purposes, but RLS today already walks the path the other direction (Client member sees all descendants).
  • Partner-unit-derived membership is a second team axis: PAs auto-derive membership through their assigned lawyers' partner-unit involvement on a project, so the firm doesn't need to staff every PA explicitly per matter.

This issue scopes a unified inventor design covering all three axes.

Goals

  1. Bug fix/projects/{client_id} (and similar parent-level views) show aggregated child deadlines, termine, and activity — not "Keine Einträge".
  2. Per-surface aggregation policy — each list / detail view declares a clear default (narrow vs aggregate-down) and a UI toggle if both are meaningful for that surface.
  3. Effective-team semantics — when looking at a Client, the displayed team includes descendants' team members AND PAs derived through partner-unit assignments (with semantics m clarifies in design pass).
  4. Path-aware queries — leverage existing paliad.projects.path (ltree-style materialised path) for "all descendants of X" — no new schema axis needed if avoidable.
  5. PA derivation honesty — derived membership MUST surface in team overviews (annotated as derived), not be a hidden under-the-hood permission grant.

Out of scope (v1)

  • Changing the project tree shape (Client / Litigation / Patent / Case stays).
  • Changing RLS direction (descendants visible to ancestors stays as is).
  • Cross-project aggregation (rolling up rows from unrelated projects).
  • Multi-tenancy / firm-tier hierarchy.

Locked design constraints (m, 2026-05-06)

  • Behaviour is surface-specific: aggregation up makes sense in some views, narrowing in others. The inventor proposes a per-surface policy table; m signs off.
  • The fix MUST cover events / deadlines / termine surfaces — m's primary complaint.
  • "Team of a Client" = direct Client team ∪ all descendants' teams ∪ partner-unit-derived PAs (with semantics m clarifies in design pass).
  • PA derivation rule — PAs assigned to a lawyer in a partner-unit auto-derive team membership on every project where that partner-unit is involved. Removes the need to individually staff PAs per project. Derived membership is honest: it shows up in the project team overview (annotated as derived from 's unit), not silently in the permission system only.
  • paliad-only scope.

Open design questions (for inventor — m will answer in design pass)

Surface-by-surface aggregation policy

  1. /projects/{id} detail page sections — Fristen / Termine / Aktivität / Verlauf — each aggregate descendants by default? Per-section toggle to narrow?
  2. Project tree sidebar count badges — e.g. "Client (12 Fristen)" — direct count, descendant-aggregate count, or both?
  3. Dashboard widgets — deadline traffic-light, upcoming termine, recent activity. Today scoped "all visible projects". Should there be a per-project subtree filter?
  4. /events list (merged Fristen + Termine) — when filtered by a project, include descendants? (Today's bug surface.)
  5. /deadlines and /appointments list pages — same question. Likely answer: aggregate by default, narrow toggle.
  6. Search results — should the global search match across the current project's descendants?
  7. CalDAV sync — which projects materialise into the user's calendar feed? Direct memberships only, or all path-reachable?
  8. Email reminders — same scope question for any future "your deadlines this week" digest.

Team / membership semantics

  1. "Effective team of a Client" — concretely:
    • (a) display-time aggregation only (when rendering "who's on this matter", show direct team ∪ all descendants' teams ∪ partner-unit-derived PAs), or
    • (b) does it grant visibility / authority somewhere new? Today can_see_project() already gives Client team members visibility of descendants — so (a) is the obvious read. Is that all m wants, or does (b) also apply?
  2. Approval policies (t-paliad-138 coordination) — the 4-eye design (docs/design-approvals-2026-05-06.md) uses project_teams.role on the specific project carrying the deadline / termin. If a deadline lives on a Case but the policy is authored on the Client, does the policy inherit down the path? First place where hierarchy aggregation interacts with the new approval system. Inventor proposes resolution.

Partner-unit-derived membership

  1. Derivation rule shape — m's plain statement: "PAs are automatically part of teams when a partner unit is involved. PAs have derived permissions by the lawyers they are assigned to." Concretely:
    • Where is "lawyer ↔ PA assignment" stored today? (paliad.partner_units / paliad.partner_unit_events migration 027 lives in the codebase — inventor should map this.)
    • Trigger condition for derivation: does a single lawyer-on-project trigger their unit's PAs onto the project, or must the explicit "partner_unit" be assigned to the project?
    • What roles does the derivation propagate to the PA — pa only, or can it carry richer authority?
  2. Approval interaction with derivation (t-138 cross-cut) — the approval ladder defaults to associate and can be lowered to pa. If a derived PA is on a project at level=pa, can they approve? The integrity question: does derivation carry the same authority as direct staffing, or is derived membership "visibility-only" while authority requires explicit roster?
  3. Display annotation — in the "Project Team" overview row, how is a derived member annotated? Inline label ("PA, abgeleitet aus Müller-Unit"), separate "Derived team" subsection, hover tooltip? UX preference.
  4. Cascading + hierarchy — if Anna PA derives onto Client A through Müller-Unit, does Anna inherit visibility of all descendants of Client A (yes, because she's effectively on the team and can_see_project walks down)? Or do we need to recompute derivation per descendant? (The simpler answer is yes — derivation makes you a team member at the level it derives, and visibility walks down from there.)
  5. Removal / churn — when Anna is reassigned to a different lawyer, does her derived membership lapse on all old projects retroactively, or only on new projects going forward? Audit trail: do we record the derivation timeline?

Architecture / performance

  1. Default direction — when a surface answer isn't obvious, default to aggregate (safer "no missed information") or narrow (safer "no irrelevant noise")?
  2. Query shapepaliad.projects.path is ltree-shaped. The aggregation pattern is WHERE p.path <@ $ancestor_path. Inventor: confirm this scales for deadline aggregation; propose materialised counts where it doesn't.
  3. Performance escape hatch — for very large hierarchies (a Client with hundreds of Cases / Patents), some aggregations may get expensive. Acceptable to bound depth ("descendants ≤ N levels"), or always show the full subtree?
  4. Aggregation indicator — when a Client view shows 47 deadlines that actually live on descendants, does each row carry a project-attribution badge ("auf: Case 14-vs-Müller") so the user knows where to drill?

References

  • paliad.projects — schema with path ltree (materialised path)
  • paliad.can_see_project() — visibility predicate (path-walks ancestors-of)
  • paliad.partner_units / paliad.partner_unit_events — migration 027, partner-unit infrastructure (the inventor must map this for derivation Q11)
  • internal/services/project_service.go — current narrow queries
  • internal/services/event_deadline_service.go — events read path (the surface m noticed the bug on)
  • internal/services/visibility_predicate.go — Go mirror of can_see_project
  • t-paliad-138 — 4-eye approval design (docs/design-approvals-2026-05-06.md) — interacts via Q10 + Q12

Inventor brief

  • Role: inventor (fresh, separate worker — cronus is mid-coder-shift on t-138 in the same repo)
  • Branch: mai/<inventor>/inventor-hierarchy-aggregation
  • Deliverable: docs/design-hierarchy-aggregation-2026-05-06.md. Three coordinated sub-designs in one doc:
    1. Surface-by-surface aggregation policy table (Q1–Q8, Q16–Q19).
    2. Effective-team semantics including hierarchy + partner-unit derivation (Q9, Q11, Q13–Q15).
    3. Approval-policy interaction with hierarchy + derivation (Q10, Q12) — explicit coordination notes with t-138.
  • Inventor STOPs after design. No /mai-coder self-load. Awaits m's explicit go on the design before any coder shift.
  • m available during the design pass to answer the 19 open questions above.
## Problem paliad's project model is hierarchical (Client → Litigation → Patent → Case), but most read surfaces only show **direct-project rows** — they don't aggregate descendants. Concrete bug m hit today: a Client project with multiple Case-level deadlines renders "Keine Fristen" on its `/projects/{client_id}` page because the deadlines live on descendant Case rows, not on the Client itself. The fix is conceptually obvious — "include children" — but the right answer is **surface-specific**: - **Aggregation up** (descendants roll into ancestors) is right for read views: deadlines list on a Client should show all deadlines under that subtree. - **Narrowing** (only this project) is right for editing context: when you create a deadline, it lives on the specific Case row, not the parent. - **Team aggregation** has its own twist: m wants a Client's "effective team" to include all descendants' team members for some purposes, but RLS today already walks the path the *other* direction (Client member sees all descendants). - **Partner-unit-derived membership** is a second team axis: PAs auto-derive membership through their assigned lawyers' partner-unit involvement on a project, so the firm doesn't need to staff every PA explicitly per matter. This issue scopes a unified inventor design covering all three axes. ## Goals 1. **Bug fix** — `/projects/{client_id}` (and similar parent-level views) show aggregated child deadlines, termine, and activity — not "Keine Einträge". 2. **Per-surface aggregation policy** — each list / detail view declares a clear default (narrow vs aggregate-down) and a UI toggle if both are meaningful for that surface. 3. **Effective-team semantics** — when looking at a Client, the displayed team includes descendants' team members AND PAs derived through partner-unit assignments (with semantics m clarifies in design pass). 4. **Path-aware queries** — leverage existing `paliad.projects.path` (ltree-style materialised path) for "all descendants of X" — no new schema axis needed if avoidable. 5. **PA derivation honesty** — derived membership MUST surface in team overviews (annotated as derived), not be a hidden under-the-hood permission grant. ## Out of scope (v1) - Changing the project tree shape (Client / Litigation / Patent / Case stays). - Changing RLS direction (descendants visible to ancestors stays as is). - Cross-project aggregation (rolling up rows from unrelated projects). - Multi-tenancy / firm-tier hierarchy. ## Locked design constraints (m, 2026-05-06) - Behaviour is **surface-specific**: aggregation up makes sense in some views, narrowing in others. The inventor proposes a per-surface policy table; m signs off. - The fix MUST cover events / deadlines / termine surfaces — m's primary complaint. - **"Team of a Client"** = direct Client team ∪ all descendants' teams ∪ partner-unit-derived PAs (with semantics m clarifies in design pass). - **PA derivation rule** — PAs assigned to a lawyer in a partner-unit auto-derive team membership on every project where that partner-unit is involved. Removes the need to individually staff PAs per project. Derived membership is **honest**: it shows up in the project team overview (annotated as derived from <Lawyer>'s unit), not silently in the permission system only. - paliad-only scope. ## Open design questions (for inventor — m will answer in design pass) ### Surface-by-surface aggregation policy 1. **`/projects/{id}` detail page sections** — Fristen / Termine / Aktivität / Verlauf — each aggregate descendants by default? Per-section toggle to narrow? 2. **Project tree sidebar count badges** — e.g. "Client (12 Fristen)" — direct count, descendant-aggregate count, or both? 3. **Dashboard widgets** — deadline traffic-light, upcoming termine, recent activity. Today scoped "all visible projects". Should there be a per-project subtree filter? 4. **`/events` list (merged Fristen + Termine)** — when filtered by a project, include descendants? (Today's bug surface.) 5. **`/deadlines` and `/appointments` list pages** — same question. Likely answer: aggregate by default, narrow toggle. 6. **Search results** — should the global search match across the current project's descendants? 7. **CalDAV sync** — which projects materialise into the user's calendar feed? Direct memberships only, or all path-reachable? 8. **Email reminders** — same scope question for any future "your deadlines this week" digest. ### Team / membership semantics 9. **"Effective team of a Client"** — concretely: - (a) display-time aggregation only (when rendering "who's on this matter", show direct team ∪ all descendants' teams ∪ partner-unit-derived PAs), or - (b) does it grant visibility / authority somewhere new? Today `can_see_project()` already gives Client team members visibility of descendants — so (a) is the obvious read. Is that all m wants, or does (b) also apply? 10. **Approval policies (t-paliad-138 coordination)** — the 4-eye design (`docs/design-approvals-2026-05-06.md`) uses `project_teams.role` on the specific project carrying the deadline / termin. If a deadline lives on a Case but the policy is authored on the Client, does the policy inherit down the path? First place where hierarchy aggregation interacts with the new approval system. Inventor proposes resolution. ### Partner-unit-derived membership 11. **Derivation rule shape** — m's plain statement: "PAs are automatically part of teams when a partner unit is involved. PAs have derived permissions by the lawyers they are assigned to." Concretely: - Where is "lawyer ↔ PA assignment" stored today? (`paliad.partner_units` / `paliad.partner_unit_events` migration 027 lives in the codebase — inventor should map this.) - Trigger condition for derivation: does a single lawyer-on-project trigger their unit's PAs onto the project, or must the explicit "partner_unit" be assigned to the project? - What roles does the derivation propagate to the PA — `pa` only, or can it carry richer authority? 12. **Approval interaction with derivation (t-138 cross-cut)** — the approval ladder defaults to `associate` and can be lowered to `pa`. If a derived PA is on a project at level=pa, can they approve? The integrity question: does derivation carry the same authority as direct staffing, or is derived membership "visibility-only" while authority requires explicit roster? 13. **Display annotation** — in the "Project Team" overview row, how is a derived member annotated? Inline label ("PA, abgeleitet aus Müller-Unit"), separate "Derived team" subsection, hover tooltip? UX preference. 14. **Cascading + hierarchy** — if Anna PA derives onto Client A through Müller-Unit, does Anna inherit visibility of all descendants of Client A (yes, because she's effectively on the team and `can_see_project` walks down)? Or do we need to recompute derivation per descendant? (The simpler answer is yes — derivation makes you a team member at the level it derives, and visibility walks down from there.) 15. **Removal / churn** — when Anna is reassigned to a different lawyer, does her derived membership lapse on all old projects retroactively, or only on new projects going forward? Audit trail: do we record the derivation timeline? ### Architecture / performance 16. **Default direction** — when a surface answer isn't obvious, default to **aggregate** (safer "no missed information") or **narrow** (safer "no irrelevant noise")? 17. **Query shape** — `paliad.projects.path` is ltree-shaped. The aggregation pattern is `WHERE p.path <@ $ancestor_path`. Inventor: confirm this scales for deadline aggregation; propose materialised counts where it doesn't. 18. **Performance escape hatch** — for very large hierarchies (a Client with hundreds of Cases / Patents), some aggregations may get expensive. Acceptable to bound depth ("descendants ≤ N levels"), or always show the full subtree? 19. **Aggregation indicator** — when a Client view shows 47 deadlines that actually live on descendants, does each row carry a project-attribution badge ("auf: Case 14-vs-Müller") so the user knows where to drill? ## References - `paliad.projects` — schema with `path ltree` (materialised path) - `paliad.can_see_project()` — visibility predicate (path-walks ancestors-of) - `paliad.partner_units` / `paliad.partner_unit_events` — migration 027, partner-unit infrastructure (the inventor must map this for derivation Q11) - `internal/services/project_service.go` — current narrow queries - `internal/services/event_deadline_service.go` — events read path (the surface m noticed the bug on) - `internal/services/visibility_predicate.go` — Go mirror of `can_see_project` - t-paliad-138 — 4-eye approval design (`docs/design-approvals-2026-05-06.md`) — interacts via Q10 + Q12 ## Inventor brief - Role: inventor (fresh, separate worker — cronus is mid-coder-shift on t-138 in the same repo) - Branch: `mai/<inventor>/inventor-hierarchy-aggregation` - Deliverable: `docs/design-hierarchy-aggregation-2026-05-06.md`. Three coordinated sub-designs in one doc: 1. Surface-by-surface aggregation policy table (Q1–Q8, Q16–Q19). 2. Effective-team semantics including hierarchy + partner-unit derivation (Q9, Q11, Q13–Q15). 3. Approval-policy interaction with hierarchy + derivation (Q10, Q12) — explicit coordination notes with t-138. - **Inventor STOPs after design.** No `/mai-coder` self-load. Awaits m's explicit go on the design before any coder shift. - m available during the design pass to answer the 19 open questions above.
mAi self-assigned this 2026-05-06 13:24:36 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#4
No description provided.