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-423

6.13.2 Public-projects directory service — list ALL `public` projects cross-org, cursor-paginated, card projection; EXCLUDES non-public

Done
Description

Estimate: 60m · Depends on: 6.12.3

The backend foundation of the square: the cross-org directory read that lists EVERY public project and EXCLUDES every non-public one. This is the load-bearing correctness work the whole story rides — it must surface only the card-projection fields and never an internal project field.

  • The directory read (the single auditable filter): a service + route returning public projects ACROSS every org/workspace, with the repository read filtered on access_level = 'public' (the 6.12.3 enum value) — the public-only filter lives in ONE repository read, so no non-public project can leak through any code path. A private / open / limited project NEVER appears, for any viewer; the 6.12.3 404-not-403 posture for non-public projects is untouched (a non-public project is simply absent from the set, never "forbidden"). Anonymous-readable: the directory read requires NO session (the page is fully public — model revision 2026-06-14); it must run on the server with no getSession() gate so crawlers / logged-out visitors get the full list.
  • The card projection (a dedicated read shape / DTO): the read returns ONLY the card fields — project name, the owning ORG (name / slug — the cross-org context the square shows), description, and the three 6.12.6 public stats (viewer count, upvote total, a recent-activity signal) — NEVER an internal project field. The stats are read from the 6.12.6 vote / activity signals (this card surfaces the projection; 6.13.4 owns the ranking ORDER over them).
  • Cursor pagination (finding #57): a system-level list of public projects could be thousands, so the read is CURSOR-paginated (a stable keyset cursor, never OFFSET-the-world, never load-all) with a bounded page size. The default ordering is a deterministic total order (a stable tiebreak on project id) so the cursor never skips/duplicates a row — 6.13.4 swaps in the trending/popular/recent sort keys over this same cursored read.

Stay 4-layer: the route parses + calls ONE service method returning the card-projection page; the access_level = public filter + the projection live in the service/repository read layer so no future read can leak a non-public project or an internal field; no raw Prisma in the route.

Acceptance criteria

  • The directory service returns public projects ACROSS orgs (including orgs the requesting account has no membership in) and EXCLUDES every non-public project — asserted with a mixed seed (public + private/open/limited across multiple orgs); the public-only filter is a single repository read.
  • The card projection returns ONLY name + org + description + the three 6.12.6 stats; no internal project field is present in the payload (verified at the payload level, not the DOM).
  • The read is cursor-paginated with a bounded page size and a deterministic total order (stable id tiebreak) — paging past a boundary skips/duplicates no row; it is NOT load-all / not OFFSET-the-world.
  • Anonymous-readable (NO session required — the route has no getSession() gate); 4-layer respected (the filter + projection in the service/repository, no raw Prisma in the route).

Context refs

  • scripts/plan-seed/data/story-6.12.ts § 6.12.3 (the public ProjectAccessLevel value + the cross-org read exception) + § 6.12.6 (the upvote / activity signals the stats read from) + § 6.12.4 (the public projection posture to mirror).
  • motir-core/lib/repositories/ + lib/services/ — the project read layer the directory threads into; projectAccessService (6.4 / 6.12.3) — the public level the filter keys off.
  • finding #57 (cursor pagination, no load-all); motir-core/CLAUDE.md § 4-layer.