MOTIR-411Done
6.12.3 Schema + access — add `public` to `ProjectAccessLevel`; extend the access-check cross-org for READ; migration
Estimate: 60m · Depends on: 6.12.2
Implement the public access level + the access-check extension per 6.12.2. This is the load-bearing correctness work the whole story rides — it must EXTEND 6.4’s shipped projectAccessService, not fork a parallel policy.
- Schema: add
publicto the existing 6.4ProjectAccessLevelenum (open | limited | private | public) with a migration; no default change (existing projects stay their current level — adding an enum value locks nothing out). Add thePublicRequestVotejoin ({ id, workItemId, userId, createdAt }, unique on(workItemId, userId)) modelled as Prisma@relationon BOTH sides (towork_item+User) per the CLAUDE.md FK-as-@relationrule — NO raw-SQL-only FK (6.12.6 uses it, but the model lands with the access foundation so the schema is coherent in one migration). publicOverviewMd(the public Overview/README field): add a nullableString?columnpublicOverviewMdtoproject(Markdown authored by the admin in 6.12.8, rendered on the public Overview tab in 6.12.4). It lands in THIS migration so the schema is coherent in one shot; it is a public-safe field included in the public projection (6.12.4) only when the project is public.- Access-check extension (the single auditable branch): extend
canBrowseso that when a project ispublic, it returns true for ANYONE — INCLUDING an unauthenticated request (no session) — bypassing the 6.10 org/workspace membership gate FOR READ ON PUBLIC PROJECTS ONLY (so the public view route does NOTgetSession()-gate the read; it renders server-side for crawlers + logged-out visitors). Every other level keeps its 6.4 semantics; the 404-not-403 posture is UNTOUCHED for non-public projects (a cross-org user on a non-public project still gets not-found).canEditis UNCHANGED — a public viewer (non-member) gets false for every normal write. Add the three explicit grantscanSubmitToTriage/canUpvotePublicRequest/canCommentPublicRequest(true for any authed account on a public project), independent ofcanEdit, so later cards check THEM, never a relaxed edit gate.
Stay 4-layer: the enum + vote model in prisma/schema.prisma + the repository, the access policy in the service layer (extend the existing projectAccessService), no raw Prisma in routes.
Acceptance criteria
- The migration adds
publictoProjectAccessLevel, thePublicRequestVotejoin (every FK an@relationon both sides), and the nullableproject.publicOverviewMdMarkdown column;prisma migrate devreports no drift; no existing project’s level changes. canBrowsereturns true for ANYONE — including an unauthenticated request — on apublicproject (cross-org), in a single auditable branch; non-public projects keep 6.4 semantics + the 404-not-403 cross-tenant posture unchanged.canEditis unchanged (a public non-member viewer → false for every normal write); the three explicit grants (canSubmitToTriage/canUpvotePublicRequest/canCommentPublicRequest) exist and are true only for an authenticated account on a public project, independent ofcanEdit.- 4-layer respected (policy in the extended service, vote model in a single-op repository, no raw Prisma in routes).
Context refs
- 6.12.2 — the semantics this implements (the ladder + the cross-org read exception + the explicit grants + the projection contract).
scripts/plan-seed/data/story-6.4.ts(6.4.2 schema + 6.4.3projectAccessService) — the enum +canBrowse/canEditpolicy this EXTENDS (mirror its shape; do not fork).motir-core/lib/repositories/+lib/services/— the project access service + repositories the extension threads into.motir-core/CLAUDE.md§ 4-layer + § migration FK-as-@relationrule.