ADR-095 — Public pool catalogue: subscriptions, read-only course links, unpublishing¶
Status¶
Accepted (2026-10-10, product owner, issue #680; the six defaults of the review confirmed the
same day). Delivery progress is in docs/merge/PROGRESS.md and changes/.
Scope: how a teacher finds and follows a colleague's public pool, how a course draws from it without making its staff contributors, and what taking a pool out of the catalogue ends.
Relations: amends ADR-013 §2 (who reaches a pool is unchanged,
what a teacher's list shows is not) and §3 rule 3 (course staff are contributors through an
edit link only); supersedes the wording of F-POOL-06 (open question 9); uses the gateway of
ADR-058, the nightly cadence of
ADR-060 and the normalisation of
ADR-081.
Context¶
A public pool showed in every teacher's pool list: noise, and the only way to use a colleague's
questions was to be invited or to copy them (copies diverge, their statistics restart). A course
can only draw from pools linked to it (coursePoolIds), and linking takes contributor
because a link makes the whole staff contributors (rule 3 of ADR-013): a teacher could read a
public pool but never use its questions.
Decision¶
- "My pools" is a shelf, not the reach.
GET /pools, MCPlist_pools, the assistant'suserPoolsand the SSEpool:topics list the pools the caller owns, sits on, reaches through a linked course (either mode) or subscribed to (myPoolsinguards.ts). Reach itself (poolAccess) is unchanged: every teacher still reads any public pool, andfind_similar_questionsand the similar-question search still span all of them.canLinkis true for a public pool. - Subscriptions (
pool_subscriptions(pool_id, user_id, created_at), PK on the pair, the pool module's table). Any teacher may subscribe to a public pool they neither own nor sit on; it joins their shelf and nothing else changes — never apool_membersreader row, which would enter the succession by seniority (ADR-013 §5), flip the visibility and clutter the roster. Idempotent;409 pool_not_publicfor a pool that is not public,409 already_memberfor its owner and members. Auditedpool.subscribe/pool.unsubscribe. - Subscribers are listed by name only, to the owner and the members of the pool (an admin
under Super Powers reads as an owner): no id, no address, no avatar, so
seesUser(ADR-013 §7) is not widened. The owner cannot remove one: the subscriber would still read the public pool. A card shows a discreet counter (lucideusers, never a star, which means question favourites in ADR-040) of subscribers plus members, on public pools only. - Link modes.
course_pools.modeisedit(the default, as before) orread. Course staff are contributors only through aneditlink; areadlink makes themreader. Rule 3 is resolved in one place,linkedCourseStaff(match, mode?): the role facts,poolRoleOfandpoolAccessreadeditlinks only (a public pool is open to every teacher anyway), while the shelf (myPools) and the derived visibility also read areadlink and a subscription, and the shelf only while the pool is public: the predicate states the rule instead of trusting the rows, so a row that survived an unpublication opens nothing. - Any owner of a course may create a
readlink to a public pool they reach, without subscribing. Areadlink to a pool that is not public is refused (409 pool_not_public). - A NEW
editlink, and the upgrade of areadlink toedit, still needmayLinkPool(contributoror above on the pool). PUT /courses/:id/poolscarries{ poolId, mode }[]. A link already there keeps its mode unless the body asks for a stronger one; a pool sent twice takes the stronger. Downgrading is done by unlinking and linking again.- Unsubscribing does not drop a link.
moveQuestionskeeps an existingreadlinkread. MCPlink_pool_to_coursegainsmode(defaultedit, as before). - Audit: the
course.pools_updatepayload records each link's mode (links). - Unpublishing a pool (
PATCH isPublic: false). The owner first reads a count (GET /pools/:id/unpublish-impact): the subscribers, the courses linkedread, and the templates of those courses that use the pool's questions. Nothing to count, nothing to confirm. Unpublishing then, in one transaction, drops thereadlinks and all subscriptions; once committed it notifies each subscriber and the owners of each read-linked course (pool_unpublished, one kind with three sentences:unpublished,course,deleted) and closes their streams (accessRevoked), asremoveMemberdoes.editlinks survive: they were granted by someone who could edit. Evaluations keep their pinned versions; the templates of the read-linked courses will reporttemplate_pool_unlinked(422) when instantiated next year, which is why the confirmation counts them. Deleting an unused public pool tells its subscribers the same way (statedeleted). The notification carries no pool id: the pool is gone or out of reach.pool_question_addedstays members-only: a subscriber is not told of new questions. - The catalogue is a panel of the pools page listing
is_publicpools only, regardless of Super Powers, with the owner beside each name. Its search covers the name, the description, the inferred domain and thelabelFrandlabelEnof the concepts of the pool's published, non-deleted questions, with thesimilarityTermsnormalisation of ADR-081 andILIKE, nopg_trgm(tests run on PGlite). The ranking is subscribers plus members, thenusedCount. - The domain of a pool is one short label in both languages (
domain_fr,domain_en), inferred by the model from the concept labels of its published questions (purposedomain, ADR-058 gateway), recomputed when the pool is published and by the nightly job (ADR-060 cadence), never on every edit. Only concept labels are sent (open question 43). It is shown on the card and in the catalogue; the owner does not edit it. - Consequences kept on purpose. ADR-017 refuses a move out of a pool whose question a classroom the owner does not reach plays, so a subscriber's classroom can block a move. A pool whose versions are pinned anywhere cannot be deleted (F-POOL-09).
Consequences¶
- Teachers lose the public pools they never followed from their shelf at the upgrade; the catalogue finds them again. Pools linked through a course stay.
- A subscriber to a pool is not told of its changes; the owner learns who follows by name only.
- Rule 3 has one implementation to keep; a new link mode only needs the
modeargument. - One more LLM flow (open question 43) and one more nightly pass, bounded by the gateway's cap.
Alternatives considered¶
- A
readerrow inpool_members. Rejected by the product owner: succession, visibility and roster side effects (above). - Copying the pool's questions. Statistics restart and copies diverge; this is the status quo the catalogue replaces.
- A closed list of domains. Rejected by the product owner; the domain is inferred and free.
pg_trgmfor the search. Not available on PGlite, and a normalisedILIKEover a few hundred public pools is enough.- Dropping every link on unpublishing. Rejected: an
editlink was granted by a writer of the pool, who may keep working in it.