Pourquoi une interface graphique de base de données ne devrait jamais écrire directement
Toute interface graphique de base de données a ce moment où un clic se transforme en écriture sur la production. Nous avons décidé que ce moment ne devait pas exister. Dans DynoTable, rien de ce que tu fais — ni une modification d’item, ni une suppression en masse, ni le changement d’un — ne touche DynamoDB directement. Chaque écriture atterrit dans une sous forme de diff vérifiable attribut par attribut, et ne part que lorsqu’un humain la valide.
Le modèle mental vers lequel nous avons construit, c’est git : une zone de
staging pour la revue, et un commit qui se comporte comme
git push --force-with-lease — il ne réussit que si le distant ressemble encore
à ce que tu as vu la dernière fois. Cet article, c’est l’ingénierie en dessous :
le bug inter-onglets qui a remodelé toute la conception, le refactor dont le
principal produit était du code supprimé, et pourquoi l’arrivée des agents IA a
transformé un raffinement d’UX en mur de sécurité porteur.
Les écritures sont des diffs
Les modifications s’accumulent dans un stage local adossé à SQLite et s’affichent sous forme de cartes de diff — ancienne valeur, nouvelle valeur, par attribut — dans un panneau latéral. Les lignes se teintent dans la grille, si bien que l’état staged est visible depuis les données, pas seulement depuis le panneau. Le commit expédie l’ensemble staged sous forme de DynamoDB, découpées selon les limites du service (100 items par transaction, avec des budgets d’octets par opération et par requête), séquentiellement, avec un timeout de 30 secondes par lot.

La doc du staging couvre le workflow quotidien. Ce qu’elle ne couvre pas, c’est ce qui sert de clé au stage — ce qui s’est avéré être la décision la plus lourde de conséquences du système.
Clé sur la table, pas sur l’onglet
La première version limitait chaque stage à l’onglet qui l’avait créé. Cette conception produisait un vrai bug : l’outil de staging de l’IA dérivait sa cible de l’onglet actif, et son garde ignorait les onglets qui ne sont pas des vues de table. Donc si tu demandais à l’assistant de corriger une ligne alors qu’un onglet SQL Workbench était au premier plan, la modification était staged dans le stage du Workbench — et la puce « révéler le changement staged » ouvrait le mauvais onglet.
Pire, le verrou de commit était lui aussi par onglet. Deux onglets affichant la même table détenaient deux verrous indépendants — ce qui signifiait que tous deux pouvaient valider les mêmes lignes staged vers le vrai DynamoDB de manière concurrente. Une course au double commit contre la production, intégrée au modèle de données.
Le correctif a consisté à re-cléer tout par identité de table —
{profile, region, tableName} — au lieu de par onglet :
- Un stage par table, visible depuis chacune de ses vues, survivant à la fermeture et à la réouverture d’onglet. L’IA peut stager sans aucun onglet de table ouvert.
- Un verrou de commit par table. La course au double commit n’est pas « gérée » — elle est irreprésentable.
- La classe de bug du mauvais onglet a disparu par construction : il n’y a pas d’onglet dans la clé.
Et le re-cléage a supprimé plus de code qu’il n’en a ajouté. Le balayage au démarrage qui garbage-collectait les stages des onglets fermés ? Un relecteur l’a signalé comme destructeur de fonctionnalité sous le nouveau modèle — les stages ne meurent plus avec les onglets — donc il a été retiré purement et simplement. L’abandon à la fermeture d’onglet et sa boîte de dialogue de confirmation : retirés. Le seul véritable orphelin restant est un profil de connexion supprimé, qui se propage explicitement.
La migration elle-même a été la première du dépôt à muter des lignes : une
reconstruction manuelle de table SQLite qui dédupliquait les anciennes lignes
par onglet avec une fenêtre ROW_NUMBER() OVER (PARTITION BY …) (la modification
la plus récente l’emporte) et remplissait la nouvelle clé avec des séparateurs
char(0), octet-identiques au constructeur de clé du runtime. Les migrations qui
mutent des lignes ont eu leur propre harnais de test seed-puis-migrate ce
jour-là.
--force-with-lease, pour DynamoDB
Une interface de revue ne vaut rien si ce que tu as relu n’est pas ce qui est écrit. Entre le staging et le commit, quelqu’un d’autre a pu changer la ligne. Donc chaque opération de commit porte des qui épinglent l’écriture au snapshot exact que tu as relu — , attribut par attribut :
- Une création affirme que l’item n’existe pas déjà.
- Une mise à jour affirme que chaque attribut que tu modifies détient encore la valeur que tu as vue au moment du staging.
- Même une suppression affirme que l’attribut est encore égal à son ancienne
valeur — si un coéquipier a changé
notede"old"en"new"pendant que ta suppression denoteétait staged, le commit ne doit pas supprimer silencieusement sa modification.
Quand une condition échoue, DynamoDB annule toute la transaction et nous indique quel item a dérivé. Cette ligne obtient une bannière de conflit — rebaser sur la valeur en direct ou abandonner — plutôt qu’un écrasement silencieux dans un sens ou dans l’autre.
Et à tout échec de lot, le committer s’arrête. Les lots validés restent validés, le lot échoué est annulé atomiquement, rien de plus n’est tenté. Nous avons délibérément rejeté la continuation au mieux : un outil de revue qui continue d’écrire au-delà d’un conflit applique un ensemble de changements que personne n’a relu.
Le refactor qui a supprimé un sous-système
Les commits sont de longue durée : l’app enregistre chacun dans un replay
registry pour qu’un rechargement du renderer — ou une seconde fenêtre — puisse se
rattacher et le regarder se terminer. Au fil du temps, trois chemins de code
différents rattachaient chacun deux observateurs par commit, et tout un
mécanisme d’arbitrage a grandi autour d’une seule question : quel observateur est
autorisé à libérer le verrou de commit ? Il avait son propre nom effrayant
(ownsLockOnBeforeRegistration), un bloc de commentaire expliquant une course
d’ordonnancement IPC, et un tracker de toasts de 230 lignes.
Le correctif a été un propriétaire unique : une Commit Session qui se rattache exactement une fois par commit, détient le verrou exclusivement, projette la progression dans le store, et garantit que son appel de démarrage se résout sur chaque chemin de sortie — ne bloque jamais, ne rejette jamais. La machinerie d’arbitrage et le tracker n’ont pas été déplacés ; ils ont été supprimés. Le benchmark mémoire qui malmène le panneau de staging a perdu environ un tiers de son tas. Le meilleur commentaire de revue de code que nous ayons reçu ce mois-là : « ce PR est surtout du rouge ».
Deux comportements découlent de ce modèle de propriété que nous considérons comme le minimum pour tout outil qui écrit sur la production : un commit en vol survit à un rechargement du renderer (la session se rattache et termine), et il survit même au basculement de ta licence en lecture seule en plein commit — de nouveaux commits sont bloqués, mais une écriture déjà en vol est observée jusqu’à son terme, jamais abandonnée à moitié appliquée.
Pourquoi le format filaire est délibérément moche
Les valeurs staged sont stockées sous forme d’enveloppes typées DynamoDB brutes —
{N: "42"}, {S: "42"} — et non sous forme de JSON unmarshalled convivial. Deux
raisons. Le diff doit être conscient du type : {N: "1"} et {S: "1"} sont des
valeurs différentes, et un hash de clé primaire construit sur des valeurs
unmarshalled les ferait entrer en collision. Et l’aller-retour doit être sans
perte : les wrappers de nombres unmarshalled du SDK ne survivent pas à la
sérialisation JSON vers SQLite et retour, et l’égalité profonde par rapport à la
saisie de l’utilisateur casse. Le chemin de commit utilise le client brut pour la
même raison — ce que tu as relu est octet pour octet ce qui est conditionné et
écrit.
Puis les agents sont arrivés
Le staging précède nos fonctionnalités IA, mais c’est la raison pour laquelle elles ont pu être livrées tout court. L’outil d’écriture de l’assistant stage ; sa propre description dit au modèle — mot pour mot :
The user reviews + commits from the staging panel —
this tool never writes to DynamoDB directly.Il n’y a pas d’outil de commit. Ni protégé par permission, ni audité — absent. Le serveur MCP expose la même frontière de façon structurelle : sa portée de consentement intermédiaire est littéralement nommée « read + stage ». Un agent à cette portée peut proposer n’importe quelle absurdité, et le rayon de souffle est une carte de diff qu’un humain lit. Et parce que les commits portent des verrous optimistes par attribut, même une modification d’agent périmée ne peut pas écraser silencieusement celle d’un humain concurrent — elle remonte sous forme de conflit comme tout le reste.
Nous avons écrit sur comment rendre les requêtes de l’agent fiables ; le staging est l’autre moitié — rendre ses écritures ennuyeuses.
Ce qu’il faut exiger de tout outil qui écrit dans ta base de données
- Une étape de revue entre l’intention et l’écriture — des diffs, pas des boîtes de dialogue de confirmation.
- De la concurrence optimiste sur le snapshot relu, y compris les suppressions et les retraits — jamais le dernier-écrivain-gagne.
- Des lots atomiques avec arrêt à l’échec, pas de continuation au mieux au-delà d’un conflit.
- Un chemin d’écriture qui survit à un crash ou à un rechargement sans laisser un ensemble à moitié appliqué.
- Pour l’IA : le staging comme seule primitive d’écriture dont dispose un agent — une capacité absente vaut mieux qu’une capacité sous garde.
La doc du staging montre le workflow, et éditer les données DynamoDB couvre les bases qu’il protège. Ou télécharge DynoTable, stage quelques modifications avec ⌘S, et regarde un changement en masse devenir quelque chose que tu peux vraiment lire avant qu’il n’arrive.


