On a public project (Story 6.12), let the project admin mark an individual EPIC as private. Everyone — any signed-in account, any public/non-member viewer — still sees the epic ROW (its title stays visible), but a private epic HIDES ALL its children (stories / tasks / subtasks) and its aggregate TELLS (child count / progress / point totals) from public/non-member viewers, replacing them with a "this epic is not public" statement. Project MEMBERS see everything normally. A pure motir-core, per-project capability layered on 6.12 — no AI boundary, no forward dependency.
The model (locked — see the module header for the full rationale + the verified mirror):
work_item, scoped to public projects. The flag is meaningful only for an EPIC on a public project; on a non-public project it is a no-op (members are the only viewers, and 6.12’s cross-org read exception is what makes "public/non-member viewer" a real population at all). It is NOT a deletion and NOT a 404 — the epic row stays a deliberate, visible "not public" placeholder.The "this epic is not public" statement appears in TWO places: (a) when a public viewer EXPANDS the private epic in the work-item TREE (the children rows are replaced by the placeholder), and (b) the CHILD PANEL on the epic’s work-item DETAIL page (the panel that would list children shows the statement instead).
Scope: the placeholder + admin-control + private-row design (6.14.1); the epic-privacy-model decision — flag, semantics, server-side-everywhere enforcement, public-project scope (6.14.2); the schema flag + migration (6.14.3); the SERVER-SIDE enforcement across every read path + the "children hidden" marker (6.14.4); the tree placeholder UI (6.14.5); the detail child-panel placeholder UI (6.14.6); the project-admin set/unset control (6.14.7); the enforcement + toggle tests (6.14.8); the public-viewer-vs-member e2e (6.14.9).
Out of scope (named so they land in their own story, not here): story-level / task-level privacy (this story is EPIC-granularity — the mirror’s per-item security generalises, but Yue’s spec is epic-level and a finer grain is a later story); a per-VIEWER allow-list on a private epic (GitLab’s "assigned non-members can still see it" — Motir’s line is member-vs-non-member, no per-item grants); hiding the epic ROW itself (deliberately kept as a placeholder — full invisibility is a different, non-goal); and ANONYMOUS public access (6.12 already scoped that out — a viewer is always a signed-in account).
pnpm db:seed against the local Postgres (localhost:5433); pnpm dev. Use a project already made public (6.12) with at least one epic that has children.motir project admin, open an epic and use the "set epic private" control (6.14.7) to mark it private; confirm the control is project-admin-gated (a non-admin member does not see the toggle, or sees it read-only).pnpm test (6.14.8) covers: a public/non-member viewer CANNOT read a private epic’s children via ANY path (tree / detail child-panel / board / ready / search) — asserted at the PAYLOAD level, not the DOM — and the aggregate tells are stripped; a MEMBER reads them; the admin toggle set/unset flips enforcement; the flag is a no-op on a non-public project; all on a real Postgres respecting the per-file coverage gate.workItemsService; the placeholder + badge + admin control reference only --el-* / [data-display-style] tokens + shipped components/ui/*.