Parent
#328.
What to build
A pad @ list is designed but waits on evidence. Nobody knows if writers type @username in pad text today. This issue produces one set of counts from production data. The counts decide if the pad @ list (#349) opens. The proposed gate: pad mentions reach 10% of chat mentions over the same pads and the same 90 days.
Acceptance criteria
Blocked by
None — can start now.
Agent brief
Type: HITL — the maintainer must run the production export and the script, because only the maintainer has production access. The maintainer also confirms or changes the 10% threshold. Both the counts and the verdict go in a comment on this issue.
Category: ruling
Current behavior:
- Each saved pad version is a row in the Prisma table
Documents (data is the Yjs update, createdAt is the save time). See apps/hocuspocus.server/prisma/schema.prisma, models Documents and DocumentMetadata.
DocumentMetadata has isPrivate and deletedAt.
- The server decodes a snapshot with
Y.applyUpdate(ydoc, bytes) then TiptapTransformer.fromYdoc(ydoc, 'default') (apps/hocuspocus.server/src/lib/documentGridPreview.ts:153-154).
- A pad's chat workspace id is
DocumentMetadata.documentId, with the same casing. public.workspaces.id, public.workspace_members.workspace_id and public.channels.workspace_id hold it. Checked on local data: 122 of 122 shared ids match exactly, and 0 match only after lowercasing. Do not lowercase the join key; workspaces.slug is the lowercase copy.
- Active members are
workspace_members rows with left_at IS NULL. Usernames live in public.users.username.
- Chat mentions use one token rule in SQL (
packages/supabase/scripts/10-func-notifications.sql:69): (?:^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]+). A token names a user only on an exact, case-sensitive username match.
- Copy to Doc writes a paragraph whose first text carries a
hyperlink mark with an href containing ?act=ch (apps/webapp/src/components/chatroom/components/MessageCard/hooks/useCopyMessageToDocHandler.ts:17, :31-47). The chat body follows as plain text, so it can hold @username.
Desired behavior: one comment on this issue with these totals, over pads in scope:
- Pads in scope, and pads with at least one pad mention.
- Paragraphs scanned, and paragraphs with at least one pad mention.
- Chat messages in scope, and chat messages with at least one mention.
- The ratio: paragraphs with a pad mention divided by chat messages with a mention.
- The verdict against the confirmed threshold.
Definitions the script must follow:
- Window: the 90 days before the run.
- Pads in scope:
isPrivate = false, deletedAt IS NULL, and at least one Documents row in the window. Private pads are out of scope. Filter them in the export query, so their rows never leave production.
- Snapshot: the newest
Documents row of each pad in scope.
- Paragraph: every
paragraph node at any depth, including those in lists, tables and blockquotes.
- Pad mention: a token, by the rule above, that equals the username of an active member of that pad's workspace. Skip
system. Skip a paragraph whose first text node carries a hyperlink mark with ?act=ch in its href.
- Chat mention: the same token rule, over
messages.content in channels whose workspace_id is a pad in scope. Count only rows with created_at in the window, deleted_at IS NULL and type IS DISTINCT FROM 'notification'. The username must be an active member of that workspace, not system, and not everyone.
Stop rules:
- If zero pads in scope resolve any active member, the export or the join is wrong. Report that and give no verdict.
- If the chat count is zero, report the raw counts and give no ratio.
Where to start:
- Export from the Prisma database:
DocumentMetadata."documentId" and the newest Documents.data per pad in scope.
- Export from the Supabase database: active
workspace_members joined to users (workspace_id, username). Also messages.content joined to channels.workspace_id, for pads in scope and the window.
- Write the script in a temporary folder outside the repository. Import
yjs and @hocuspocus/transformer by absolute path from apps/hocuspocus.server/node_modules in this checkout. The root node_modules does not hold them. Or install both in that folder with bun add.
- Local data for a dry run: the Prisma database runs in docker container
docsy-postgres-local (database docsplus, user from DATABASE_URL in the root .env.local). Query it with docker exec -i docsy-postgres-local psql, and strip the ?connection_limit=… params from the URL. Supabase runs in supabase_db_docsplus_supabase (user postgres). See AGENTS.md §Learned Workspace Facts.
Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Rules that apply:
- Never decode snapshots inside a production container. It shares memory with the live server. Run only small count queries there, and stream the rows out for a local decode.
- The script prints counts only. It never prints or saves text, usernames, titles or slugs. Delete the local export after the run.
- AGENTS.md §Standalone Bun Scripts, and §Package Manager.
- No test file. AGENTS.md §Test Policy: this is a one-off count, not a regression or dense branching logic.
- The repository is public. The comment holds totals only.
Verify:
- Smoke-check the script:
bun build <script>.ts --target=bun --outfile=/tmp/out.js.
- Dry run on local data. Start the stack with
make dev-local, sign in, and open a new public pad. Opening it signed in makes you an active member of its workspace (useJoinWorkspace). Type @<your username> in one paragraph. Send one chat message with the same token. The script must count one pad mention and one chat mention for that pad.
- Put a Copy to Doc paragraph with
@<username> in the same pad. The pad count must not change.
Out of scope
Parent
#328.
What to build
A pad
@list is designed but waits on evidence. Nobody knows if writers type@usernamein pad text today. This issue produces one set of counts from production data. The counts decide if the pad@list (#349) opens. The proposed gate: pad mentions reach 10% of chat mentions over the same pads and the same 90 days.Acceptance criteria
Blocked by
None — can start now.
Agent brief
Type: HITL — the maintainer must run the production export and the script, because only the maintainer has production access. The maintainer also confirms or changes the 10% threshold. Both the counts and the verdict go in a comment on this issue.
Category: ruling
Current behavior:
Documents(datais the Yjs update,createdAtis the save time). Seeapps/hocuspocus.server/prisma/schema.prisma, modelsDocumentsandDocumentMetadata.DocumentMetadatahasisPrivateanddeletedAt.Y.applyUpdate(ydoc, bytes)thenTiptapTransformer.fromYdoc(ydoc, 'default')(apps/hocuspocus.server/src/lib/documentGridPreview.ts:153-154).DocumentMetadata.documentId, with the same casing.public.workspaces.id,public.workspace_members.workspace_idandpublic.channels.workspace_idhold it. Checked on local data: 122 of 122 shared ids match exactly, and 0 match only after lowercasing. Do not lowercase the join key;workspaces.slugis the lowercase copy.workspace_membersrows withleft_at IS NULL. Usernames live inpublic.users.username.packages/supabase/scripts/10-func-notifications.sql:69):(?:^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]+). A token names a user only on an exact, case-sensitive username match.hyperlinkmark with an href containing?act=ch(apps/webapp/src/components/chatroom/components/MessageCard/hooks/useCopyMessageToDocHandler.ts:17,:31-47). The chat body follows as plain text, so it can hold@username.Desired behavior: one comment on this issue with these totals, over pads in scope:
Definitions the script must follow:
isPrivate = false,deletedAt IS NULL, and at least oneDocumentsrow in the window. Private pads are out of scope. Filter them in the export query, so their rows never leave production.Documentsrow of each pad in scope.paragraphnode at any depth, including those in lists, tables and blockquotes.system. Skip a paragraph whose first text node carries ahyperlinkmark with?act=chin its href.messages.contentin channels whoseworkspace_idis a pad in scope. Count only rows withcreated_atin the window,deleted_at IS NULLandtype IS DISTINCT FROM 'notification'. The username must be an active member of that workspace, notsystem, and noteveryone.Stop rules:
Where to start:
DocumentMetadata."documentId"and the newestDocuments.dataper pad in scope.workspace_membersjoined tousers(workspace_id,username). Alsomessages.contentjoined tochannels.workspace_id, for pads in scope and the window.yjsand@hocuspocus/transformerby absolute path fromapps/hocuspocus.server/node_modulesin this checkout. The rootnode_modulesdoes not hold them. Or install both in that folder withbun add.docsy-postgres-local(databasedocsplus, user fromDATABASE_URLin the root.env.local). Query it withdocker exec -i docsy-postgres-local psql, and strip the?connection_limit=…params from the URL. Supabase runs insupabase_db_docsplus_supabase(userpostgres). See AGENTS.md §Learned Workspace Facts.Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Rules that apply:
Verify:
bun build <script>.ts --target=bun --outfile=/tmp/out.js.make dev-local, sign in, and open a new public pad. Opening it signed in makes you an active member of its workspace (useJoinWorkspace). Type@<your username>in one paragraph. Send one chat message with the same token. The script must count one pad mention and one chat mention for that pad.@<username>in the same pad. The pad count must not change.Out of scope
@list. Add a pad @ list where Members open a Comment and Headings insert a link #349 owns that.