MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-430

6.14 Epic-level privacy on public projects

Done
Description

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):

  • An epic-kind privacy flag on 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.
  • SERVER-SIDE enforcement on EVERY read path (the load-bearing rule). A private epic’s children must NEVER be transmitted to a public/non-member viewer by ANY read — the tree projection, the work-item detail child-panel, the 6.12 public board, the 7.0 ready set, and the 6.1 FilterAST search are ALL filtered server-side, so nothing leaks over the wire (no client-only hide, nothing in the network tab). The epic row’s aggregate tells (child count / progress / point rollup) are stripped from the public projection too — just title + a "private" marker + the placeholder.
  • Members bypass. A project member (6.4) reads the children and the real rollups exactly as today; the exclusion applies ONLY to a public/non-member viewer (the 6.12 cross-org population). The project ADMIN (6.4) is who sets/unsets an epic private.
  • Extends 6.12’s public PROJECTION, never forks it. 6.12 centralised the public read in a single projection (6.12.4) that already strips internal fields; 6.14 threads ONE more predicate (descended-from-a-private-epic → excluded for non-members) into that SAME projection, as a single auditable branch, not N scattered filters.

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).

Verification

  • Pull the Story branch; run the migration + 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.
  • Set an epic private (admin). As the 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).
  • Public viewer — the tree (the load-bearing check). Sign in as a SECOND Motir account in a DIFFERENT org with NO membership in the project (the 6.12 cross-org public viewer). Open the public project tree → the private epic ROW is still visible (title shown), but it carries a "private" badge and NO child count / progress / point total; EXPAND it → instead of children, the "this epic is not public" placeholder renders. Open the browser network tab and confirm the children are ABSENT from the response payload (not merely hidden in the DOM) — nothing under the private epic crosses the wire.
  • Public viewer — the detail child-panel. Open the private epic’s work-item DETAIL page as the same public viewer → the CHILD PANEL shows the "this epic is not public" statement instead of a child list; again confirm the payload carries no children.
  • Public viewer — every OTHER read path. Confirm the private epic’s children are absent from: the 6.12 public BOARD, the 7.0 ready SET, and a 6.1 SEARCH that would otherwise match a child’s title — all for the public viewer, all server-side.
  • Member bypass. Sign in as a PROJECT MEMBER → the same epic shows its children normally, with the real child count / progress / point rollup; the tree-expand and the detail child-panel list the children (no placeholder, no stripped tells). Unset the epic’s privacy as the admin → the public viewer now sees the children too.
  • 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.
  • 4-layer + token review. No raw Prisma in any route; the epic-privacy predicate lives in the 6.12.4 public projection at the service/repository read layer (one auditable branch, not N filters); the set/unset write routes through a service → workItemsService; the placeholder + badge + admin control reference only --el-* / [data-display-style] tokens + shipped components/ui/*.
  • Dep audit. Confirm no 6.14 subtask references any id > 6.14 (deps are 6.14.x / 6.12.x / 6.4.x / Epic-2 only).
  • If every step holds, approve and merge the Story PR. If anything fails, comment with what didn’t work and Motir will produce a follow-up Subtask under the same Story.
Child work items