Estimate: 70m · Depends on: 6.14.3, 6.12.4
The load-bearing correctness work: for a public/non-member viewer, EXCLUDE a private epic’s children — and STRIP the epic row’s aggregate tells — from EVERY read path, server-side, by extending 6.12’s public PROJECTION. Per 6.14.2. Nothing under a private epic may ever be transmitted to a non-member (no leak in the API / network tab).
- The single exclusion predicate, in the 6.12.4 projection. Thread ONE "descends from a private epic" predicate into the SAME public projection 6.12 centralises, gated on the viewer being a NON-MEMBER on a
public project: when true, a work item whose epic ancestor has the privacy flag set is EXCLUDED from the projected result. Defined once so every read through the projection inherits it — NOT N independent filters. The predicate is an indexable where clause over the parent/ancestor walk (finding #57 — no load-all post-filter).
- Apply it to every read path (the checklist from 6.14.2): (1) the TREE projection — children of a private epic are absent; (2) the work-item DETAIL child-panel read — the epic’s child list returns EMPTY-with-marker for a non-member; (3) the 6.12 public BOARD read; (4) the 7.0 ready SET read; (5) the 6.1 FilterAST SEARCH compilation (a child of a private epic does NOT match for a public viewer, mirroring GitLab confidential issues being hidden from search). A parameterized shape so adding a new public read without the predicate is caught by 6.14.8.
- Strip the tells + return the marker. For a private epic ROW in the public projection, OMIT the child count, progress / rollup, and point total, and SET the "children-hidden" marker (per 6.14.2’s DTO decision, e.g.
childrenHidden: true) so the UI renders the placeholder without ever receiving a child. The epic title / kind / status stay (the row is a visible placeholder).
- Members bypass. The predicate is reached ONLY for a non-member viewer on a public project — a project member’s reads return the children + the real rollups unchanged; a non-public project is a no-op.
Stay 4-layer: the predicate + the tell-strip live in the public projection at the service/repository read layer (where 6.12.4 put it), never in a route or the client; no raw Prisma in routes.
Acceptance criteria
- For a public/non-member viewer, a private epic’s children are ABSENT from the payload of EVERY read path — tree, detail child-panel, public board, ready set, AND FilterAST search — verified at the response level (not the DOM); the predicate is defined ONCE in the 6.12.4 projection (a single auditable branch).
- The private epic’s public ROW carries title / kind / status + the "children-hidden" marker but OMITS child count, progress / rollup, and point total; the marker lets the UI render the placeholder with no child in the payload.
- A project MEMBER’s reads return the children + the real rollups unchanged; the flag is a no-op on a non-public project and for a member on a public project.
- 4-layer respected (predicate + strip in the service/repository projection, no raw Prisma in routes); the exclusion is an indexable
where, not a load-all post-filter.
Context refs
- 6.14.2 (the read-path checklist + the projection-predicate + the tell-strip / marker decision), 6.14.3 (the flag the predicate reads).
scripts/plan-seed/data/story-6.12.ts § 6.12.4 (the public PROJECTION this extends — the use-the-real-public-read-subtask-id dependency) + § 6.12.7 (the public roadmap read it also covers).
motir-core/lib/services/workItemsService.ts + the tree / detail-child / board / ready-set read paths + the 6.1.1 FilterAST search compiler — the reads the projection predicate threads into.
- GitLab confidential issues (https://docs.gitlab.com/ee/user/project/issues/confidential_issues.html) — hidden from non-members incl. search, server-side; Jira issue-level security (https://confluence.atlassian.com/adminjiraserver/configuring-issue-level-security-938847117.html) — "not visible anywhere" server-enforced + member/admin bypass.
motir-core/CLAUDE.md § 4-layer.