Estimate: 45m
Type: decision (the keystone ADR the schema + enforcement cards [6.14.3+] build against; no app behavior ships, but the shapes it fixes are load-bearing). Write motir-core/docs/decisions/epic-privacy.md, EXTENDING (not forking) 6.12’s public-projection ADR. It MUST fix:
- The model: an epic-kind privacy flag on
work_item. Decide the flag shape — a publicChildrenHidden: boolean @default(false) column (or an epic visibility enum public | private_children; pick the boolean unless the enum buys a named future state, and justify). It is meaningful ONLY for an EPIC-kind item; setting it on a non-epic is rejected (or ignored) — state which and why. The epic ROW is NEVER hidden by it; only the epic’s DESCENDANTS + its aggregate tells are.
- Scoped to public projects (no-op otherwise). The flag changes NOTHING on a non-public project: members are the only viewers there, and 6.12’s cross-org read exception is the only thing that creates a "public/non-member viewer" population. State that the enforcement branch is reached ONLY for a non-member viewer on a
public project, so the flag is inert (a no-op) everywhere else — including for a member on a public project.
- SERVER-SIDE enforcement on EVERY read path (the load-bearing requirement). Enumerate the EXACT read paths a private epic’s children must be excluded from for a public/non-member viewer: the TREE projection, the work-item DETAIL child-panel read, the 6.12 public BOARD, the 7.0 ready SET, and the 6.1 FilterAST SEARCH. Fix that the exclusion is a SINGLE predicate threaded into 6.12’s public PROJECTION (the read shape 6.12.4 already centralises) — "the item descends from a private epic" → excluded — NOT N independent filters, so a future read that goes through the projection inherits it and cannot leak. State explicitly that the children must be EXCLUDED at the read/service layer (never sent), not hidden client-side (no leak in the API/network tab).
- The aggregate-tell strip + the "children hidden" marker. Fix that the public projection of a private epic ROW carries title + kind + status + a "private"/"children-hidden" MARKER, but NOT the child count, progress / rollup, or point total (those are tells that leak the hidden subtree’s shape). Decide how the marker is represented in the projection DTO (e.g. a
childrenHidden: true flag + childCount / progress omitted-or-null) so the UI (6.14.5/6.14.6) can render the placeholder without ever receiving a child.
- Members bypass; admin sets the flag. A PROJECT MEMBER (6.4) reads the children + the real rollups exactly as today — the exclusion predicate is gated on "non-member viewer", so a member never hits it. The PROJECT ADMIN (6.4) is the only role that can set/unset the flag (reuse the 6.4 project-admin check — NOT a new permission). State that no read path may key the exclusion off anything but "non-member-on-public-project + descends-from-private-epic".
- The descendant test. Decide HOW "descends from a private epic" is computed for the exclusion — the work_item tree is epic → story → task/subtask (leaf depth ≤ 3 per the kind-parent matrix), so a child’s epic ancestor is a bounded walk; decide whether to resolve it via the stored parent/ancestor path already used by the tree read or a join, so the predicate stays an indexable
where clause (finding #57 — no load-all post-filter).
Acceptance criteria
- The ADR fixes all six sections, naming the chosen flag shape (+ a one-paragraph justification for boolean-vs-enum and for epic-only) and the descendant-resolution approach as an indexable predicate.
- It enumerates EVERY read path that must exclude a private epic’s children for a non-member (tree, detail child-panel, public board, ready set, FilterAST search) as the checklist 6.14.4 + 6.14.8 implement and test, and fixes that the exclusion is ONE predicate in 6.12’s public projection, not N filters.
- It states the children are EXCLUDED server-side (never transmitted, no network-tab leak), the aggregate tells (count / progress / points) are stripped from the private epic’s public row, and the projection carries a "children-hidden" marker the UI renders.
- It fixes the public-project scope (no-op otherwise), the member-bypass, and the project-admin-only set/unset (reusing the 6.4 admin check).
- It cites the verified mirror (GitLab confidential issues + Jira issue-level security as server-enforced-hidden-with-no-leak, and Canny/Productboard per-item public-roadmap visibility) for the epic-privacy shape.
Context refs