Pourquoi une GUI de base ne devrait jamais écrire directement
Toute GUI de base a un moment où un clic devient une écriture contre la production. Nous avons décidé que ce moment ne devait pas être le défaut. Dans DynoTable, une édition d’item, une suppression en staging et le changement d’un ne touchent pas DynamoDB directement : chacun atterrit dans une sous forme de diff vérifiable attribut par attribut, et ne part que lorsqu’un humain le commit. Deux échappatoires délibérées écrivent encore en direct, et la doc du staging les nomme.
Nous avons construit vers un modèle mental façon 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 remote ressemble encore à ce que tu as vu en dernier. Cet
article parle du bug cross-tab qui a remodelé tout le design, du refactor dont
la principale sortie était du code supprimé, et pourquoi les agents IA ont
transformé une niceté UX en mur de sécurité porteur.
Les écritures sont des diffs
Les éditions s’accumulent dans un stage local adossé à SQLite et se rendent en cartes de diff (ancienne valeur, nouvelle valeur, par attribut) dans un panneau latéral. Les lignes se teintent dans la grille pour que l’état staged soit visible depuis les données, pas seulement depuis le panneau. Le commit envoie l’ensemble staged comme DynamoDB, découpé aux limites du service (100 items par transaction, avec budgets d’octets par opération et par requête), séquentiellement, avec un timeout de 30 secondes par chunk.

La doc du staging couvre le workflow quotidien. Ce qu’elle ne couvre pas, c’est sur quoi le stage est clé, 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 scopait chaque stage à l’onglet qui l’avait créé. Ce design a produit un vrai bug : l’outil de staging de l’IA dérivait sa cible de l’onglet actif, et sa garde sautait 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, l’édition é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 aussi par onglet. Deux onglets qui regardaient la même table tenaient deux verrous indépendants, ce qui voulait dire que les deux pouvaient committer les mêmes lignes staged vers le vrai DynamoDB en concurrence. Une course de double-commit contre la production, construite dans le modèle de données.
Le correctif a été de re-cléer tout par identité de table
({profile, region, tableName}) plutôt que par onglet :
- Un stage par table, visible depuis chaque vue de celle-ci, survivant à la fermeture et réouverture d’onglet. L’IA peut stager sans aucun onglet table ouvert.
- Un verrou de commit par table. La course de double-commit est irreprésentable.
- La classe de bugs mauvais-onglet a disparu par construction : il n’y a plus d’onglet dans la clé.
Et re-cléer 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 reviewer 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. Discard-on-tab-close et sa boîte de confirmation : retirés. Le seul vrai orphelin restant est un profil de connexion supprimé, qui cascade 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 lignes legacy
par onglet avec une fenêtre ROW_NUMBER() OVER (PARTITION BY …) (l’édition la
plus récente gagne) et remplit la nouvelle clé avec des séparateurs char(0),
byte-identical au builder de clé du runtime. Les migrations mutantes de lignes
ont eu leur propre suite de tests seed-then-migrate ce jour-là.
--force-with-lease, pour DynamoDB
Une UI de revue ne sert à rien si la chose que tu as revue n’est pas celle qui est écrite. 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 revu, , attribut par attribut :
- Un create affirme que l’item n’existe pas déjà.
- Un update affirme que chaque attribut que tu changes tient encore la valeur que tu as vue au moment du staging.
- Même un removal affirme que l’attribut égale encore son ancienne valeur :
si un collègue a changé
notede"old"à"new"pendant que ta suppression denoteétait staged, le commit ne doit pas effacer silencieusement son édition.
Quand une condition échoue, DynamoDB annule toute la transaction et nous dit quel item a dérivé. Cette ligne reçoit une bannière de conflit, rebase contre la valeur live ou abort, plutôt qu’une écrasement silencieux dans un sens ou dans l’autre.
Et sur tout échec de chunk, le committer s’arrête. Les chunks commités restent commités, le chunk en échec rollback atomiquement, rien d’autre n’est tenté. Nous avons délibérément rejeté la continuation best-effort : un outil de revue qui continue d’écrire après un conflit applique un jeu de changements que personne n’a revu.
Le refactor qui a supprimé un sous-système
Les commits sont longs : l’app enregistre chacun dans un registre de replay pour
qu’un reload du renderer (ou une seconde fenêtre) puisse se réattacher et le
voir finir. Avec le temps, trois chemins de code différents attachaient chacun
deux observers par commit, et tout un mécanisme d’arbitrage a grandi autour
d’une question : quel observer a le droit de libérer le verrou de commit ? Il
avait son propre nom anxiogène (ownsLockOnBeforeRegistration), un bloc de
commentaire expliquant une course d’ordre IPC, et un tracker de toast de 230
lignes.
Le correctif a été un seul propriétaire, une Commit Session qui s’attache exactement une fois par commit, possède le verrou exclusivement, projette la progression dans le store, et garantit que son appel de start se termine sur chaque chemin de sortie, ne hang jamais, ne reject 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 heap. Le meilleur commentaire de code review de ce mois-là : « cette 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 en production : un commit en vol survit à un reload du renderer (la session se réattache et termine), et il survit même à ta licence qui bascule en lecture seule en milieu de commit ; les nouveaux commits sont bloqués, mais une écriture déjà en vol est observée jusqu’au bout, jamais abandonnée à moitié appliquée.
Pourquoi le wire format est délibérément laid
Les valeurs staged sont stockées comme enveloppes typées DynamoDB brutes
({N: "42"}, {S: "42"}), pas comme du JSON unmarshalled amical. Deux raisons.
Le diff doit être aware des types : {N: "1"} et {S: "1"} sont des valeurs
différentes, et un hash de clé primaire construit sur des valeurs unmarshalled
les ferait collisionner. Et l’aller-retour doit être lossless : les wrappers de
nombres unmarshalled du SDK ne survivent pas à la sérialisation JSON dans
SQLite et retour, et l’égalité profonde contre l’entrée utilisateur casse. Le
chemin de commit utilise le client brut pour la même raison : ce que tu as revu
est byte pour byte 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. L’outil d’écriture de l’assistant stage ; sa propre description dit au modèle, verbatim :
The user reviews + commits from the staging panel —
this tool never writes to DynamoDB directly.Il n’y a pas d’outil de commit. Rien à permission-gater ni à auditer. La primitive est absente. Le serveur MCP expose la même frontière structurellement : sa portée de consentement du milieu s’appelle littéralement « read + stage ». Un agent à cette portée peut proposer n’importe quoi, et le rayon d’explosion est une carte de diff qu’un humain lit. Et parce que les commits portent des verrous optimistes par attribut, même une édition d’agent périmée ne peut pas écraser silencieusement une édition humaine concurrente : elle remonte comme un conflit, comme tout le reste.
Nous avons écrit sur 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
- Une étape de revue entre l’intention et l’écriture, des diffs, pas des boîtes de confirmation.
- De la concurrence optimiste sur le snapshot revu, y compris les deletes et removals, jamais last-writer-wins.
- Des lots atomiques avec halt-on-failure, pas de continuation best-effort après un conflit.
- Un chemin d’écriture qui survit à un crash ou un reload sans laisser un ensemble à moitié appliqué.
- Pour l’IA : le staging comme seule primitive d’écriture dont dispose un agent ; une capacité absente bat une capacité gardée.
La doc du staging montre le workflow, et éditer des données DynamoDB couvre les bases qu’il protège. Ou télécharge DynoTable, stage quelques éditions avec ⌘S, et regarde un changement en masse devenir quelque chose que tu peux vraiment lire avant que ça arrive.


