MOTIR-413Done
6.12.5 Public submit-to-triage for any account + DUPLICATE DETECTION (surface the existing request to upvote)
Estimate: 60m · Depends on: 6.12.3, 6.11.4
The cross-account public submission path, REUSING 6.11.4’s intake (no second submissions table), plus the duplicate-detection pre-check (Canny’s core behaviour). Per 6.12.2:
- Cross-org-account submit: a service + route taking
{ kind (bug|feature), title, descriptionMd }that creates a triagework_itemthroughworkItemsService(the SAME 6.11.4 creation path), attributed to the SUBMITTING cross-org account (a real authenticatedsubmittedByUserId, not an external/anonymous submitter), scoped to the public project. Gated by 6.12.3’scanSubmitToTriage(true for any authed account on a public project) — NOTcanEdit. Rate-limited + abuse-guarded per the 6.11.4 precedent (per-account throttle, size cap), since this is an internet-facing write. - Duplicate detection (BEFORE create): a service method that, given a draft title/text, finds matching EXISTING public requests for the project (a deterministic title/text match — e.g. normalized token / trigram similarity, reusing the 6.1.1 search where it fits; NOT an AI call — AI dedupe is an Epic-7 enhancement) and returns the candidates so the UI can offer "upvote this instead". If the user chooses an existing request, NO new item is created — the flow hands off to the 6.12.6 upvote. If they choose "submit as new", the create path runs. The match read must respect the public projection (it only searches public-facing requests) and the triage queue (a duplicate of a still-in-triage request is still surfaceable).
Stay 4-layer: routes parse + call one service method; the service owns the transaction + the throttle; creation goes through workItemsService.
Acceptance criteria
- A cross-org authenticated account submits a request on a public project → a triage
work_itemis created viaworkItemsService, attributed to that account, invisible to the normal tree (it shows in the project’s triage queue); gated bycanSubmitToTriage, NOTcanEdit; rate-limited (rapid repeats throttle with a typed error, not a 500). - The duplicate-detection method returns matching existing public requests for a draft title BEFORE creation; choosing an existing one creates NO new item and routes to upvote (6.12.6); choosing "submit as new" creates the item.
- The match is deterministic (no AI call) and searches only public-facing requests; no raw Prisma in the route; creation reuses 6.11.4’s path (no second submissions table).
Context refs
- 6.12.3 —
canSubmitToTriage(the grant this checks) + the public projection the match respects. scripts/plan-seed/data/story-6.11.ts§ 6.11.4 (the intake creation path REUSED) + § 6.11.3 (the triage queue the item lands in).motir-core/lib/services/workItemsService.ts— the create authority; the 6.1.1 FilterAST search (shipped) — the match read reuses it where it fits.- Canny duplicate detection (https://canny.io/use-cases/feature-request-management) — the "upvote the existing request instead of creating a dupe" behaviour.
motir-core/CLAUDE.md§ 4-layer; the 6.11.4 rate-limit precedent.