You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Le staging par hunk n'est pas représentable dans le contrat de revue #163
Suivi séparé de l'item de #128 — « combined staged/unstaged/untracked review with file/hunk staging and conflict state ». La moitié fichier est livrée (PR #162, adossée à bext-stack/automonique #225) ; la moitié hunk ne l'est pas, et ne peut pas l'être en l'état.
Pourquoi ce n'est pas un manque d'implémentation
Le contrat ne peut pas l'exprimer :
ReviewProposal porte des identifiants de fichiers ;
une action de staging nomme une proposition, pas un fragment ;
l'identifiant de hunk de ReviewAnchor ne sert qu'aux commentaires.
Aucun chemin ne permet donc de désigner un hunk comme cible d'écriture, et en inventer un côté client produirait une cible que le serveur ne peut ni valider ni clôturer.
Ce qu'il manque vraiment
Deux choses, en amont dans bext-stack/automonique :
Une proposition ou une action cadrée au hunk, pour que la cible soit nommable sur le fil.
Une identité de hunk assez stable pour clôturer une écriture dessus. C'est le point difficile, et c'est le même raisonnement qui a conduit #225 à faire porter à une projection l'oid de HEAD et l'état de l'index : un digest doit s'engager sur ce qu'un préflight peut réellement prouver. Un numéro de hunk est une position dans un diff, pas une identité — il se déplace dès que le fichier bouge. Sans identité stable, un digest de staging par hunk ressemblerait à une barrière sans rien retenir.
Recommandation
Chiffrer l'identité de hunk avant de construire quoi que ce soit — c'est la question qui décide si la fonctionnalité est raisonnable, pas un détail d'implémentation. Le staging fichier reste utilisable entre-temps.
Non. Il n'existe pas d'identité de hunk assez stable pour clôturer une écriture, et ce n'est pas un champ qui manque. Trois constats, chacun suffisant à lui seul :
un hunk n'est pas une propriété du dépôt, mais d'un rendu : il n'y a donc rien de stable à adresser ;
le hunk qui circule sur le fil n'est pas une observation du worktree que l'écriture toucherait : les deux ne se rencontrent jamais ;
git n'offre aucun mécanisme clôturable : le seul qui existe, git apply --cached, écrit avec succès dans une région que personne n'a relue.
Le staging par hunk ne doit donc pas être rendu représentable sous ce nom. Ce qui est représentable, et que je décris en fin de commentaire, est une écriture d'index partielle scellée sur l'oid du blob résultant, où le fichier reste l'unité que le serveur clôture. Ce n'est pas une identité de hunk, c'est son contournement, et cela coûte une famille en amont plus l'inversion d'un invariant de #225. Je ne recommande pas de le construire aujourd'hui.
Tout ce qui suit est mesuré, avec de quoi rejouer.
1. Un hunk n'est pas une propriété du dépôt
1.1 Adresser le contenu ne suffit pas : deux hunks peuvent être rigoureusement identiques
Un fichier contenant deux fois le même bloc de 7 lignes, séparés par 30 lignes de remplissage, la même ligne éditée dans les deux :
Identiques. Le même pré-image, le même fichier, deux hunks distincts, une seule empreinte. Élargir le contexte ne corrige rien : il suffit de répéter un bloc plus large. Une adresse de contenu seule est ambiguë à l'intérieur d'un seul fichier.
Reste la position pour désambiguïser. Mais :
1.2 La position dépend de paramètres que rien n'observe
Trois éditions aux lignes 10, 16 et 40 d'un fichier de 60 lignes, mêmes deux blobs, seul le contexte change :
Départ ligne 5 contre ligne 4, et des lignes ajoutées différentes. Une empreinte de contenu calculée sous une configuration n'existe pas sous l'autre.
Un pilote textconv déclaré dans .gitattributes supprime le diff entièrement :
c.txt diff=lower + git config diff.lower.textconv 'tr A-Z a-z <'
diff par défaut : 1 hunk (-beta / +BETA)
avec le pilote : diff VIDE, zéro hunk
--no-textconv : le hunk revient
Enfin, le rendu que l'humain lit et celui que le serveur dériverait doivent être le même diff. Sur un fichier où la première modification n'est que de l'espacement :
Si le relecteur lit le diff -w, « hunk 1 » vaut ligne 15. Si le serveur dérive le diff par défaut, « hunk 1 » vaut ligne 5.
Un hunk est fonction de (blob A, blob B, algorithme, largeur de contexte, heuristique d'indentation, attributs/textconv, version de git). Aucun de ces paramètres de dérivation n'est observé ni porté nulle part dans le contrat. J'ai relu GitWorktreeObservation::digest (rust/crates/automonique-daemon/src/platform_v2_git_worktree_adapter.rs, rev 524073c3) : il scelle l'étiquette de version, la famille, le commit HEAD, la branche, le digest d'index, le côté du conflit, le (mode, objet) résolu, puis par fichier le chemin, l'objet dans HEAD, l'objet dans l'index, les étages de conflit, l'empreinte stat et les drapeaux staged/unstaged/untracked, puis les staged_paths. Rien sur le rendu d'un diff.
Conclusion partielle. La position n'est pas une identité en général. Elle en devient une relativement à un couple de blobs épinglé et à des paramètres de dérivation figés. C'est le seul point de sauvetage possible, et il ne survit pas au constat suivant.
2. Le hunk du fil n'est pas une observation du worktree
C'est le constat le plus structurel, et il ne dépend d'aucune configuration.
Ce que le contrat porte réellement (rust/crates/automonique-protocol/src/platform_v2_review.rs) :
Le hunk transporte des positions et un aperçu de 512 octets. Pas son contenu. Le document de conception du contrat (docs/product-plan/platform-v2-review-contract.md) le dit dans ses propres termes : la projection « file and hunk » se termine par « bounded sanitized hunk preview », la proposition porte « an exact bounded ordered file set », et « Binary bytes [...] have no representation ». Un client ne peut donc ni calculer une adresse de contenu de hunk à partir de ce qu'il reçoit, ni en émettre une.
Et surtout, à la révision épinglée 524073c3, sur l'arbre entier :
DiffHunk::new -> 1 seul site hors tests : platform_v2_review_api.rs:274 (le décodeur)
ReviewFile::new -> 1 seul site hors tests : platform_v2_review_api.rs:310 (le décodeur)
Aucun code de production de bext-stack/automonique ne construit un hunk à partir d'un dépôt git. La liste de hunks est décodée d'un document, pas observée.
Symétriquement, l'adaptateur que #225 a ajouté pour observer et écrire le dépôt, platform_v2_git_worktree_adapter.rs (1990 lignes), ne contient aucune occurrence de « hunk ». Il observe par chemin :
Les deux moitiés ne se rencontrent jamais. Le magasin le formule mieux que moi, dans le commentaire de ReviewPlan::git_staging (rust/crates/automonique-store/src/review_store.rs) :
Everything sealed here is either operator-owned (the canonical root), server-owned (the proposal, its files, the commit subject the snapshot carried) or observed (the head, the index, the observation digest). No client string is admitted [...]
Un chemin est à la fois server-owned et observed : l'adaptateur peut aller le chercher dans le dépôt et prouver ce qu'il y trouve. Un identifiant de hunk ne peut être que le premier. Il n'existe rien dans le dépôt que l'adaptateur puisse consulter par identifiant de hunk pour confirmer que ce hunk existe. C'est exactement la distinction que l'issue appelle « méritée, pas déduite », dans le vocabulaire du code lui-même.
Bornes annexes, qui achèvent le tableau : MAX_REVIEW_HUNKS_PER_FILE = 128 et MAX_REVIEW_HUNKS = 512. Un jeu de modifications plus large n'est pas représentable du tout, même en lecture.
Côté ShellDeck, c'est cohérent
ReviewHunkId n'apparaît que dans deux endroits hors tests, tous deux à l'intérieur de ReviewAction::AddComment → ReviewAnchor : crates/shelldeck-core/src/config/platform_review.rs:149 et crates/shelldeck-core/src/config/platform_review/custody.rs:500. Dans l'interface, le seul élément conscient d'un hunk est review_anchor_element_id (crates/shelldeck-ui/src/fleet_view.rs:219), le bouton d'ancre de commentaire. Aucun contrôle par hunk n'existe.
Un piège trouvé au passage, et c'est la raison de la PR jointe.crates/shelldeck-core/src/workspace_review.rs modélise déjà ReviewMutationKind::StageHunks / UnstageHunks, autorisés par AuthorityScope::Repository { checkout, stage_hunks } et scellés par MutationTargetFence::LocalReview { checkout, review_revision }, en #[allow(dead_code)], à côté du commentaire « adapter wiring is a later integration milestone ». Rien hors tests ne produit de ReviewHunk, donc ces identifiants sont des UUID v4 qui n'étiquettent rien. Et review_revision épingle l'instantané de ce crate, pas le worktree : il ne bouge pas quand un autre processus tournant sous le même uid déplace HEAD, l'index ou le fichier. Câbler cela à un adaptateur git installerait très exactement la barrière creuse que cette issue existe pour empêcher.
3. git apply --cached n'est pas une barrière
3.1 C'est pourtant le seul mécanisme que git propose
git add -p rend le diff, prend la sélection, et applique un patch reconstruit. Il n'existe aucune plomberie qui indexe un fragment par chemin.
3.2 Il applique à n'importe quel décalage, et le dit avec un code de retour 0
Patch frappé quand l'index tenait le blob b5c3d22, ciblant @@ -17,7 +17,7 @@. Un autre processus déplace ensuite l'index (10 lignes insérées en tête, blob 550e70a, la cible d'édition est passée en ligne 30) :
La ligne index b5c3d22..65da919 que porte le patch est ignorée. --3way n'est pas une contrainte, c'est un repli quand l'application directe échoue : ici elle réussit, donc le repli ne sert jamais. Il n'existe aucune option de git apply qui refuse tant que le pré-image n'est pas exactement un oid donné.
3.3 Et il écrit dans une région que personne n'a relue
Le cas qui décide. Fichier avec deux occurrences du même bloc (lignes 11-17 et 26-32). Le relecteur édite la première, le patch dit @@ -11,7 +11,7 @@. Entre le préflight et l'exécution, un autre processus retire la première occurrence de l'index. Le patch est alors appliqué :
### apply the reviewer's patch to that index
EXIT 0 -- APPLIQUÉ
### résultat
19 aaa
20 bbb
21 ccc
22 TARGET-EDITED <-- la SECONDE occurrence, jamais relue, jamais approuvée
23 ddd
Code de retour 0, aucune erreur, aucun avertissement, et l'écriture atterrit dans une région différente de celle qui a été revue. C'est la définition même du mode de défaillance que l'issue décrit : une règle qui a l'air de garder quelque chose et ne garde rien.
(Note pour qui rejouerait : un hunk qui commence à la ligne 1 arme match_beginning dans apply.c et refuse. Il faut un hunk en milieu de fichier pour observer le comportement ci-dessus. Ma première tentative a été un faux négatif pour cette raison.)
3.4 L'autre voie casse l'invariant central de #225
La seule alternative à git apply est de construire le blob résultant et de l'écrire avec update-index --cacheinfo. Or #225 a bâti tout son module sur l'inverse, et le dit :
// Git writes the working-tree bytes, from the stage it is already// holding. This process never opens the file, so it can neither choose// the content nor follow a symlink out of the worktree.
Les trois écritures respectent cette règle : git add -- <pathspec>, git restore --staged --source=<oid HEAD observé> -- <pathspec>, et checkout-index --force --stage=N suivi de update-index --add --cacheinfo <mode>,<objet observé>,<chemin>. Chacune nomme un chemin et laisse git fournir le contenu depuis un endroit que le processus ne choisit pas.
Une écriture par hunk doit ouvrir le fichier, choisir les octets et écrire un nouvel objet. Ce n'est pas un champ en plus, c'est une posture de sécurité différente, qui mérite son propre argument et sa propre autorisation.
4. L'observation par fichier est déjà trop faible pour dériver
GitFileObservation épingle head_object et index_object comme oids de blob, donc deux engagements de contenu réels. Mais le côté worktree n'est qu'une empreinte stat, et #225 admet lui-même le résidu :
Its one residual is git's: a rewrite that preserves size, inode and both timestamps to the nanosecond is not distinguished.
Même taille, même mtime, même inode, contenu différent.
Pour le staging fichier, ce résidu est borné : git add indexe ce qui est sur le disque et la vérification post-écriture ne contrôle que « diffère de HEAD ». Pour le staging hunk, il ne l'est pas : le contenu décide quels hunks existent. Une réécriture racée change silencieusement la décomposition contre laquelle la sélection est résolue. Le staging par hunk exigerait donc un engagement de contenu que le staging par fichier peut se permettre de ne pas avoir.
Corollaire à noter : git diff-files --raw affiche des zéros du côté worktree. Seul git hash-object donne cet oid. Bonne nouvelle au passage, git hash-objectsans-w calcule sans écrire (vérifié : l'objet n'est pas dans la base ensuite), donc un préflight resterait sans mutation.
5. Ce qui serait représentable, si un jour on le veut
Pas du staging par hunk. Une écriture d'index partielle, scellée sur l'oid du blob résultant, où le fichier reste l'unité que le serveur clôture. La question de l'identité de hunk se dissout : on n'a pas besoin d'identifier un hunk, on a besoin d'identifier un résultat, et un oid de blob est l'objet le plus vérifiable que git possède.
Forme, si la décision amont était prise :
famille PartialIndexWrite, toujours cadrée au fichier, la proposition nommant les fichiers exactement comme aujourd'hui ;
l'observation par chemin gagne un engagement de contenu pour le côté worktree (git hash-object, sans -w), de sorte que le couple (index_object, worktree_object) soit intégralement épinglé ;
la sélection voyage comme un jeu de sélecteurs que le serveur re-dérive : (début pré-image, longueur pré-image, début post-image, longueur post-image, sha256 du corps du hunk). Ce n'est pas un identifiant : c'est un prédicat, qui n'a de sens que résolu contre un couple de blobs épinglé. Refus sur 0 correspondance et refus sur 2 ou plus, parce que le point 1.1 rend le cas « 2 ou plus » réel ;
les paramètres de dérivation sont figés et entrent dans le digest : -U<n>, algorithme, heuristique d'indentation explicitement posée, --no-textconv --no-ext-diff, et aucun drapeau ignorant l'espacement. Le diff que l'humain a lu et celui que le serveur dérive doivent être le même ;
le collage se fait dans l'espace des blobs, pas sur les octets du disque. Avec * text=auto eol=crlf, les octets du disque et ceux du blob diffèrent (mesuré : \r\n sur le disque, \n dans le blob, hash-object applique le filtre, hash-object --no-filters non), donc coller des lignes brutes du worktree dans le blob d'index produirait le mauvais objet ;
et ce que le digest clôture réellement est l'oid du blob résultant B', calculé au préflight. Vérifié :
blob d'index épinglé A = b5c3d22... blob worktree épinglé W = c15b41d...
hunks dérivés du couple : @@ -7,7 +7,7 @@ @@ -27,7 +27,7 @@
B' calculé trois fois : ef909cc4b24b760b14430b2cb25f04dbd6bef528 (identique)
update-index --cacheinfo -> index = ef909cc4...
diff indexé : -line10 / +line10-EDIT (exactement le hunk 1, rien d'autre)
diff non indexé : -line30 / +line30-EDIT (exactement le reste)
Aucun git apply, aucune recherche de contexte, aucun décalage possible. Un sélecteur mal résolu ne peut pas produire une écriture différente, parce qu'une écriture différente est un B' différent et fait échouer le digest.
Pourquoi je ne recommande quand même pas de le construire
Coût amont : nouvelle famille, nouveau champ d'observation, nouveaux champs de digest, dans un dépôt qui n'est pas le nôtre.
Inversion d'invariant : cela retire à #225 son « ce processus n'ouvre jamais le fichier ». Cela mérite sa propre autorisation, pas un élargissement d'index_write.
Sérialisation, et c'est le point qui tue l'intérêt du produit. Comme la confirmation scelle le digest de l'index entier, chaque écriture par hunk invalide toutes les autres confirmations en cours du dépôt. Mesuré : après avoir indexé le seul hunk 1, le blob d'index passe de b5c3d22 à ef909cc4 et l'empreinte de git diff-index --cached HEAD de e3b0c442 à 35f4cb01. Une interface qui propose « indexer les hunks 1, 2, 3 » doit donc relire les capacités entre chaque, un aller-retour par hunk, chacun refusable. C'est une interaction nettement moins bonne que le contrôle par fichier déjà livré, pour une fonctionnalité dont tout l'attrait est la rapidité.
À noter aussi : la direction désindexation est en meilleure posture que l'indexation, parce que ses deux extrémités (HEAD → index) sont déjà des oids de blob observés, sans nouveau champ. Si l'on devait un jour ouvrir cette porte, c'est par là qu'elle serait la moins chère et la mieux prouvée.
6. Ce que je recommande pour cette issue
Requalifier Le staging par hunk n'est pas représentable dans le contrat de revue #163 : le titre est juste, la conclusion est que le staging par hunk n'est pas représentable et n'a pas à le devenir sous ce nom. Ce qui reste ouvert, si quelqu'un en veut un jour, est une écriture d'index partielle scellée sur un oid de blob, qui est une décision amont.
Ne pas câbler le réducteur dormant. C'est le seul risque concret que l'enquête a fait apparaître.
Le staging fichier reste utilisable et inchangé.
Ce qui est livré
PR #165, documentation uniquement : le commentaire au site sur StageHunks / UnstageHunks dans workspace_review.rs, avec les trois raisons, plus une entrée de journal dans docs/testing/USE_CASES.md à côté de celle du staging fichier. Pas de SDUC ni de SDTEST : rien ne change de comportement, et un test ici affirmerait qu'une variante non câblée est toujours non câblée, ce que l'absence d'appelant garantit déjà.
La question que ce ticket demandait de chiffrer avant de construire quoi que ce soit a reçu sa réponse : il n'existe pas d'identité de hunk capable de clôturer honnêtement une écriture. Ce n'est pas un champ manquant, et aucune version du contrat ne le rendrait représentable.
Trois constats, chacun suffisant seul, détaillés dans le commentaire d'analyse et consignés par la PR #165 :
Un hunk est une propriété d'un rendu, pas du dépôt. Mesuré sur la même paire de blobs : trois hunks en -U2 et deux en -U3 ; diff.indentHeuristic (config utilisateur, active par défaut depuis git 2.14) déplace la frontière ; -w donne un autre découpage, donc un relecteur et un serveur qui ne rendent pas le même diff ne parlent pas du même « hunk 1 » ; un pilote textconv fait produire zéro hunk à la même paire. Et l'adressage par contenu ne sauve rien : deux hunks d'un même fichier, contexte complet inclus, ont été construits avec un sha256 identique.
Le hunk sur le fil n'est pas une observation de l'arbre de travail.DiffHunk::new et ReviewFile::new n'ont qu'un seul appelant non-test : le décodeur. Rien, côté Rust ou SDK TypeScript, ne construit un hunk à partir d'un dépôt. Ce que porte DiffHunk, la doc l'appelle un aperçu.
git apply --cached n'est pas une barrière, et c'est le seul mécanisme dont git dispose. Il applique à n'importe quel décalage : sortie 0 contre un blob d'index dont le patch n'est pas issu, hunk écrit dix lignes plus loin, idem avec -C1, --recount, --ignore-whitespace, --3way. Pire cas mesuré : région relue retirée de l'index, sortie 0 et écriture dans une autre région semblable que personne n'avait relue.
Ce qui reste possible, et pourquoi ce n'est pas mieux
Une écriture d'index partielle mais clôturée sur l'oid du blob résultant, la sélection étant re-dérivée côté serveur depuis une paire de blobs épinglée : le fichier reste l'unité que le serveur clôture, l'identité de hunk n'est jamais requise. Spécifié et vérifié de bout en bout. Mais cela coûte une famille amont, inverse l'invariant central de #225 (« ce processus n'ouvre jamais le fichier, il ne peut donc pas choisir le contenu »), et surtout sérialise : la confirmation scelle l'empreinte d'index entière, donc chaque écriture de hunk invalide toutes les autres confirmations en cours. L'interaction serait moins bonne que le contrôle par fichier déjà livré.
⚠️ Un piège laissé en place volontairement
crates/shelldeck-core/src/workspace_review.rs modélise déjà StageHunks / UnstageHunks en #[allow(dead_code)], derrière MutationTargetFence::LocalReview { checkout, review_revision }, à côté d'un commentaire disant que le câblage viendra plus tard. review_revision épingle l'instantané de ShellDeck, pas l'arbre de travail : les câbler installerait exactement la barrière creuse que ce ticket existe pour empêcher. La PR #165 pose les raisons à cet endroit-là, pour que la prochaine personne les lise avant de tirer le fil.
Le staging par fichier reste livré et utilisable. Fermé.
Activity
benfavre commentedon Aug 30, 2026
Verdict
Non. Il n'existe pas d'identité de hunk assez stable pour clôturer une écriture, et ce n'est pas un champ qui manque. Trois constats, chacun suffisant à lui seul :
git apply --cached, écrit avec succès dans une région que personne n'a relue.Le staging par hunk ne doit donc pas être rendu représentable sous ce nom. Ce qui est représentable, et que je décris en fin de commentaire, est une écriture d'index partielle scellée sur l'oid du blob résultant, où le fichier reste l'unité que le serveur clôture. Ce n'est pas une identité de hunk, c'est son contournement, et cela coûte une famille en amont plus l'inversion d'un invariant de #225. Je ne recommande pas de le construire aujourd'hui.
Tout ce qui suit est mesuré, avec de quoi rejouer.
1. Un hunk n'est pas une propriété du dépôt
1.1 Adresser le contenu ne suffit pas : deux hunks peuvent être rigoureusement identiques
Un fichier contenant deux fois le même bloc de 7 lignes, séparés par 30 lignes de remplissage, la même ligne éditée dans les deux :
Corps des deux hunks, en-tête exclu, hachés séparément :
Identiques. Le même pré-image, le même fichier, deux hunks distincts, une seule empreinte. Élargir le contexte ne corrige rien : il suffit de répéter un bloc plus large. Une adresse de contenu seule est ambiguë à l'intérieur d'un seul fichier.
Reste la position pour désambiguïser. Mais :
1.2 La position dépend de paramètres que rien n'observe
Trois éditions aux lignes 10, 16 et 40 d'un fichier de 60 lignes, mêmes deux blobs, seul le contexte change :
« Hunk 2 » désigne l'édition de la ligne 16 à
-U2et celle de la ligne 40 à-U3. Le nombre même de hunks change.diff.indentHeuristic, réglage utilisateur actif par défaut depuis git 2.14, déplace la frontière et le corps :Départ ligne 5 contre ligne 4, et des lignes ajoutées différentes. Une empreinte de contenu calculée sous une configuration n'existe pas sous l'autre.
Un pilote
textconvdéclaré dans.gitattributessupprime le diff entièrement :Enfin, le rendu que l'humain lit et celui que le serveur dériverait doivent être le même diff. Sur un fichier où la première modification n'est que de l'espacement :
Si le relecteur lit le diff
-w, « hunk 1 » vaut ligne 15. Si le serveur dérive le diff par défaut, « hunk 1 » vaut ligne 5.Un hunk est fonction de (blob A, blob B, algorithme, largeur de contexte, heuristique d'indentation, attributs/textconv, version de git). Aucun de ces paramètres de dérivation n'est observé ni porté nulle part dans le contrat. J'ai relu
GitWorktreeObservation::digest(rust/crates/automonique-daemon/src/platform_v2_git_worktree_adapter.rs, rev524073c3) : il scelle l'étiquette de version, la famille, le commitHEAD, la branche, le digest d'index, le côté du conflit, le(mode, objet)résolu, puis par fichier le chemin, l'objet dansHEAD, l'objet dans l'index, les étages de conflit, l'empreintestatet les drapeauxstaged/unstaged/untracked, puis lesstaged_paths. Rien sur le rendu d'un diff.Conclusion partielle. La position n'est pas une identité en général. Elle en devient une relativement à un couple de blobs épinglé et à des paramètres de dérivation figés. C'est le seul point de sauvetage possible, et il ne survit pas au constat suivant.
2. Le hunk du fil n'est pas une observation du worktree
C'est le constat le plus structurel, et il ne dépend d'aucune configuration.
Ce que le contrat porte réellement (
rust/crates/automonique-protocol/src/platform_v2_review.rs) :Le hunk transporte des positions et un aperçu de 512 octets. Pas son contenu. Le document de conception du contrat (
docs/product-plan/platform-v2-review-contract.md) le dit dans ses propres termes : la projection « file and hunk » se termine par « bounded sanitized hunk preview », la proposition porte « an exact bounded ordered file set », et « Binary bytes [...] have no representation ». Un client ne peut donc ni calculer une adresse de contenu de hunk à partir de ce qu'il reçoit, ni en émettre une.Et surtout, à la révision épinglée
524073c3, sur l'arbre entier :Aucun code de production de
bext-stack/automoniquene construit un hunk à partir d'un dépôt git. La liste de hunks est décodée d'un document, pas observée.Symétriquement, l'adaptateur que #225 a ajouté pour observer et écrire le dépôt,
platform_v2_git_worktree_adapter.rs(1990 lignes), ne contient aucune occurrence de « hunk ». Il observe par chemin :Les deux moitiés ne se rencontrent jamais. Le magasin le formule mieux que moi, dans le commentaire de
ReviewPlan::git_staging(rust/crates/automonique-store/src/review_store.rs) :Un chemin est à la fois server-owned et observed : l'adaptateur peut aller le chercher dans le dépôt et prouver ce qu'il y trouve. Un identifiant de hunk ne peut être que le premier. Il n'existe rien dans le dépôt que l'adaptateur puisse consulter par identifiant de hunk pour confirmer que ce hunk existe. C'est exactement la distinction que l'issue appelle « méritée, pas déduite », dans le vocabulaire du code lui-même.
Bornes annexes, qui achèvent le tableau :
MAX_REVIEW_HUNKS_PER_FILE = 128etMAX_REVIEW_HUNKS = 512. Un jeu de modifications plus large n'est pas représentable du tout, même en lecture.Côté ShellDeck, c'est cohérent
ReviewHunkIdn'apparaît que dans deux endroits hors tests, tous deux à l'intérieur deReviewAction::AddComment→ReviewAnchor:crates/shelldeck-core/src/config/platform_review.rs:149etcrates/shelldeck-core/src/config/platform_review/custody.rs:500. Dans l'interface, le seul élément conscient d'un hunk estreview_anchor_element_id(crates/shelldeck-ui/src/fleet_view.rs:219), le bouton d'ancre de commentaire. Aucun contrôle par hunk n'existe.Un piège trouvé au passage, et c'est la raison de la PR jointe.
crates/shelldeck-core/src/workspace_review.rsmodélise déjàReviewMutationKind::StageHunks/UnstageHunks, autorisés parAuthorityScope::Repository { checkout, stage_hunks }et scellés parMutationTargetFence::LocalReview { checkout, review_revision }, en#[allow(dead_code)], à côté du commentaire « adapter wiring is a later integration milestone ». Rien hors tests ne produit deReviewHunk, donc ces identifiants sont des UUID v4 qui n'étiquettent rien. Etreview_revisionépingle l'instantané de ce crate, pas le worktree : il ne bouge pas quand un autre processus tournant sous le même uid déplaceHEAD, l'index ou le fichier. Câbler cela à un adaptateur git installerait très exactement la barrière creuse que cette issue existe pour empêcher.3.
git apply --cachedn'est pas une barrière3.1 C'est pourtant le seul mécanisme que git propose
GIT_TRACE=2 git add -p:git add -prend le diff, prend la sélection, et applique un patch reconstruit. Il n'existe aucune plomberie qui indexe un fragment par chemin.3.2 Il applique à n'importe quel décalage, et le dit avec un code de retour 0
Patch frappé quand l'index tenait le blob
b5c3d22, ciblant@@ -17,7 +17,7 @@. Un autre processus déplace ensuite l'index (10 lignes insérées en tête, blob550e70a, la cible d'édition est passée en ligne 30) :La ligne
index b5c3d22..65da919que porte le patch est ignorée.--3wayn'est pas une contrainte, c'est un repli quand l'application directe échoue : ici elle réussit, donc le repli ne sert jamais. Il n'existe aucune option degit applyqui refuse tant que le pré-image n'est pas exactement un oid donné.3.3 Et il écrit dans une région que personne n'a relue
Le cas qui décide. Fichier avec deux occurrences du même bloc (lignes 11-17 et 26-32). Le relecteur édite la première, le patch dit
@@ -11,7 +11,7 @@. Entre le préflight et l'exécution, un autre processus retire la première occurrence de l'index. Le patch est alors appliqué :Code de retour 0, aucune erreur, aucun avertissement, et l'écriture atterrit dans une région différente de celle qui a été revue. C'est la définition même du mode de défaillance que l'issue décrit : une règle qui a l'air de garder quelque chose et ne garde rien.
(Note pour qui rejouerait : un hunk qui commence à la ligne 1 arme
match_beginningdansapply.cet refuse. Il faut un hunk en milieu de fichier pour observer le comportement ci-dessus. Ma première tentative a été un faux négatif pour cette raison.)3.4 L'autre voie casse l'invariant central de #225
La seule alternative à
git applyest de construire le blob résultant et de l'écrire avecupdate-index --cacheinfo. Or #225 a bâti tout son module sur l'inverse, et le dit :Les trois écritures respectent cette règle :
git add -- <pathspec>,git restore --staged --source=<oid HEAD observé> -- <pathspec>, etcheckout-index --force --stage=Nsuivi deupdate-index --add --cacheinfo <mode>,<objet observé>,<chemin>. Chacune nomme un chemin et laisse git fournir le contenu depuis un endroit que le processus ne choisit pas.Une écriture par hunk doit ouvrir le fichier, choisir les octets et écrire un nouvel objet. Ce n'est pas un champ en plus, c'est une posture de sécurité différente, qui mérite son propre argument et sa propre autorisation.
4. L'observation par fichier est déjà trop faible pour dériver
GitFileObservationépinglehead_objectetindex_objectcomme oids de blob, donc deux engagements de contenu réels. Mais le côté worktree n'est qu'une empreintestat, et #225 admet lui-même le résidu :Mesuré :
Même taille, même mtime, même inode, contenu différent.
Pour le staging fichier, ce résidu est borné :
git addindexe ce qui est sur le disque et la vérification post-écriture ne contrôle que « diffère de HEAD ». Pour le staging hunk, il ne l'est pas : le contenu décide quels hunks existent. Une réécriture racée change silencieusement la décomposition contre laquelle la sélection est résolue. Le staging par hunk exigerait donc un engagement de contenu que le staging par fichier peut se permettre de ne pas avoir.Corollaire à noter :
git diff-files --rawaffiche des zéros du côté worktree. Seulgit hash-objectdonne cet oid. Bonne nouvelle au passage,git hash-objectsans-wcalcule sans écrire (vérifié : l'objet n'est pas dans la base ensuite), donc un préflight resterait sans mutation.5. Ce qui serait représentable, si un jour on le veut
Pas du staging par hunk. Une écriture d'index partielle, scellée sur l'oid du blob résultant, où le fichier reste l'unité que le serveur clôture. La question de l'identité de hunk se dissout : on n'a pas besoin d'identifier un hunk, on a besoin d'identifier un résultat, et un oid de blob est l'objet le plus vérifiable que git possède.
Forme, si la décision amont était prise :
PartialIndexWrite, toujours cadrée au fichier, la proposition nommant les fichiers exactement comme aujourd'hui ;git hash-object, sans-w), de sorte que le couple(index_object, worktree_object)soit intégralement épinglé ;(début pré-image, longueur pré-image, début post-image, longueur post-image, sha256 du corps du hunk). Ce n'est pas un identifiant : c'est un prédicat, qui n'a de sens que résolu contre un couple de blobs épinglé. Refus sur 0 correspondance et refus sur 2 ou plus, parce que le point 1.1 rend le cas « 2 ou plus » réel ;-U<n>, algorithme, heuristique d'indentation explicitement posée,--no-textconv --no-ext-diff, et aucun drapeau ignorant l'espacement. Le diff que l'humain a lu et celui que le serveur dérive doivent être le même ;* text=auto eol=crlf, les octets du disque et ceux du blob diffèrent (mesuré :\r\nsur le disque,\ndans le blob,hash-objectapplique le filtre,hash-object --no-filtersnon), donc coller des lignes brutes du worktree dans le blob d'index produirait le mauvais objet ;B', calculé au préflight. Vérifié :Aucun
git apply, aucune recherche de contexte, aucun décalage possible. Un sélecteur mal résolu ne peut pas produire une écriture différente, parce qu'une écriture différente est unB'différent et fait échouer le digest.Pourquoi je ne recommande quand même pas de le construire
index_write.b5c3d22àef909cc4et l'empreinte degit diff-index --cached HEADdee3b0c442à35f4cb01. Une interface qui propose « indexer les hunks 1, 2, 3 » doit donc relire les capacités entre chaque, un aller-retour par hunk, chacun refusable. C'est une interaction nettement moins bonne que le contrôle par fichier déjà livré, pour une fonctionnalité dont tout l'attrait est la rapidité.À noter aussi : la direction désindexation est en meilleure posture que l'indexation, parce que ses deux extrémités (
HEAD→ index) sont déjà des oids de blob observés, sans nouveau champ. Si l'on devait un jour ouvrir cette porte, c'est par là qu'elle serait la moins chère et la mieux prouvée.6. Ce que je recommande pour cette issue
Ce qui est livré
PR #165, documentation uniquement : le commentaire au site sur
StageHunks/UnstageHunksdansworkspace_review.rs, avec les trois raisons, plus une entrée de journal dansdocs/testing/USE_CASES.mdà côté de celle du staging fichier. Pas de SDUC ni de SDTEST : rien ne change de comportement, et un test ici affirmerait qu'une variante non câblée est toujours non câblée, ce que l'absence d'appelant garantit déjà.benfavre commentedon Aug 30, 2026
Fermé comme décision consignée, pas comme dette
La question que ce ticket demandait de chiffrer avant de construire quoi que ce soit a reçu sa réponse : il n'existe pas d'identité de hunk capable de clôturer honnêtement une écriture. Ce n'est pas un champ manquant, et aucune version du contrat ne le rendrait représentable.
Trois constats, chacun suffisant seul, détaillés dans le commentaire d'analyse et consignés par la PR #165 :
-U2et deux en-U3;diff.indentHeuristic(config utilisateur, active par défaut depuis git 2.14) déplace la frontière ;-wdonne un autre découpage, donc un relecteur et un serveur qui ne rendent pas le même diff ne parlent pas du même « hunk 1 » ; un pilotetextconvfait produire zéro hunk à la même paire. Et l'adressage par contenu ne sauve rien : deux hunks d'un même fichier, contexte complet inclus, ont été construits avec un sha256 identique.DiffHunk::newetReviewFile::newn'ont qu'un seul appelant non-test : le décodeur. Rien, côté Rust ou SDK TypeScript, ne construit un hunk à partir d'un dépôt. Ce que porteDiffHunk, la doc l'appelle un aperçu.git apply --cachedn'est pas une barrière, et c'est le seul mécanisme dont git dispose. Il applique à n'importe quel décalage : sortie 0 contre un blob d'index dont le patch n'est pas issu, hunk écrit dix lignes plus loin, idem avec-C1,--recount,--ignore-whitespace,--3way. Pire cas mesuré : région relue retirée de l'index, sortie 0 et écriture dans une autre région semblable que personne n'avait relue.Ce qui reste possible, et pourquoi ce n'est pas mieux
Une écriture d'index partielle mais clôturée sur l'oid du blob résultant, la sélection étant re-dérivée côté serveur depuis une paire de blobs épinglée : le fichier reste l'unité que le serveur clôture, l'identité de hunk n'est jamais requise. Spécifié et vérifié de bout en bout. Mais cela coûte une famille amont, inverse l'invariant central de #225 (« ce processus n'ouvre jamais le fichier, il ne peut donc pas choisir le contenu »), et surtout sérialise : la confirmation scelle l'empreinte d'index entière, donc chaque écriture de hunk invalide toutes les autres confirmations en cours. L'interaction serait moins bonne que le contrôle par fichier déjà livré.
crates/shelldeck-core/src/workspace_review.rsmodélise déjàStageHunks/UnstageHunksen#[allow(dead_code)], derrièreMutationTargetFence::LocalReview { checkout, review_revision }, à côté d'un commentaire disant que le câblage viendra plus tard.review_revisionépingle l'instantané de ShellDeck, pas l'arbre de travail : les câbler installerait exactement la barrière creuse que ce ticket existe pour empêcher. La PR #165 pose les raisons à cet endroit-là, pour que la prochaine personne les lise avant de tirer le fil.Le staging par fichier reste livré et utilisable. Fermé.