Audits · septembre 2026Audit — relations utiles entre users, stores, organisations, profils

Audit — relations utiles entre users, stores, organisations, profils

Demande directe, pas une rotation @codebase-auditor. Lecture seule sur le code, sauf un constat corrige dans la meme session (voir « Ce qui a ete corrige »). Perimetre : toute primitive relationnelle entre User, Store…

Demande directe, pas une rotation @codebase-auditor. Lecture seule sur le code, sauf un constat corrige dans la meme session (voir « Ce qui a ete corrige »).

Perimetre : toute primitive relationnelle entre User, Store, Organization, OrganizationMember, et les profils publics (Watchlist, StoreTracker, IntelligenceWatchlist, username, leaderboardOptOut, NetworkApplication, forum, seller profile). Permissions et confidentialite explicitement hors perimetre (deja couvertes ailleurs — getStoreAccess/hasAccessPermission, src/lib/security/).

Etat du depot au moment du constat : origin/main a 235d1f1b (11 septembre 2026, ~01:45 UTC). Le correctif ci-dessous a merge en #1034.

Objectif

Trouver les primitives relationnelles reelles mais mal ou pas cablees, les connecter sans en inventer de nouvelles, pour que l'arrivee de plus d'utilisateurs et de stores rende l'ecosysteme progressivement plus utile — sans transformer artificiellement le produit en reseau social.

Ce qui a ete corrige

IntelligenceWatchlist — un modele avec zero ecrivain, jamais

git log -S sur tout l'historique : aucun create/upsert/createMany/ $executeRaw n'a jamais existe pour ce modele. /[orgSlug]/intelligence le lisait et rendait donc l'etat vide pour toujours — un etat vide qui promettait en plus un outil @Atlas « watch » qui n'existe pas non plus dans src/features/ai/tools/.

La vraie primitive, ecrite et lue par deux crons (prediction-fanout, tracker-anomaly-fanout), est StoreTracker — per-store, posee par le bouton « Track in Brand Tracker » du Store Spy, via /api/intelligence/hub/tracker. Elle n'etait simplement jamais agregee au niveau organisation, alors que getStoreAccess derive deja entierement de getOrgAccess (pas d'ACL par store) : agreger les trackers de tous les stores d'une org n'expose donc rien qu'un membre ne pouvait deja voir store par store.

Correctif (#1034, pilier intelligence) : rollupOrgTrackers() aplati les StoreTracker de tous les stores d'une org en une liste dedupliquee par domaine (les deux stores qui suivent le meme concurrent produisent une entree, avec les deux attributions et la date d'ajout la plus ancienne). /[orgSlug]/intelligence lit desormais ca. Le modele mort et sa relation Organization sont retires du schema.

Ce qui a ete verifie et n'est PAS un constat

Quatre pistes qui ressemblaient au meme defaut, refutees a la ligne :

PisteVerdict
User.username — unique, collecte a l'onboardingCosmetique uniquement (@username dans le shell). Rien ne le lit pour une relation. Pas casse : n'a jamais promis plus.
Watchlist.itemType === "store"Suspecte morte au premier grep (aucune occurrence litterale itemType="store"). En realite cablee via <FollowButton itemType={type.toLowerCase()} …> sur /marketplace/[type]/[slug], ou type est MarketplaceType — donc vivante, juste dynamique.
NetworkApplication / /networkFormulaire de lead (affiliate/agency/referral), pas une primitive relationnelle. /network est une vitrine marketing statique, pas un annuaire d'utilisateurs reels.
User.leaderboardOptOutLa copie UI (messages/en.json, dashboard.account.privacy.leaderboardDescription) promet exactement « votre nom et avatar s'affichent sur /community/levels » — et services/community/xp.ts:leaderboard() masque bien name/image quand optedOut. La promesse est tenue a la lettre : rien a corriger.

Ce qui reste une piste, pas un bug

Deux surfaces auraient un gain reel a etre connectees, mais aucune n'est une primitive existante mal cablee — construire l'une ou l'autre inventerait une nouvelle surface produit (un profil public generique), ce qui sort du mandat de cet audit :

  • Auteur du forum non cliquable — ForumThread.author et ForumPost.author rendent nom + avatar (services/community/forum.ts) sans lien. Le seul profil public existant, /sellers/[id] (services/marketplace/sellers.ts), rend null pour quiconque n'a pas de listing marketplace LIVE — y lier l'auteur casserait pour la quasi- totalite des membres du forum plutot que de les connecter.
  • Activite par membre d'organisation — /[orgSlug]/~/members est un roster (nom, email, role, date d'entree), sans vue croisee « qui a fait quoi ». PlatformActivity porte deja actor (userId) + orgId, donc la donnee existe ; aucune page ne la projette par membre aujourd'hui.

Les deux demandent un vrai profil ou une vraie vue d'activite par utilisateur — une decision produit (jusqu'ou aller vers de l'identite publique), pas une correction de cablage. Documente ici plutot que construit, sur decision explicite au moment de cet audit.

Perimetre couvert, methode

Inspection directe (grep/lecture) de chaque modele relationnel du schema, verification de chaque FollowButton/lien d'auteur/route publique par son ou ses vrais appelants (jamais par le nom du fichier ou un commentaire), et verification croisee des trois primitives de « watch » du schema (Watchlist, StoreTracker, IntelligenceWatchlist) avant de conclure laquelle etait reellement vivante. Pas de fan-out par sous-agents — un seul passage, execute et verifie de bout en bout (tests, guards, merge, push, PR mergee).