Skip to content

Zéro ambiguïté : refuser plutôt que répondre à côté #20

Description

@alexwrite

Context

Un outil de confiance pour agents ne doit jamais répondre à côté sans le dire. Constaté :

  • graphify query "<langage naturel>" part de mots isolés et renvoie des nœuds hors sujet (projet A : 74 affichés sur 7 314, aucun pertinent) ;
  • explain résout un nom ambigu sans avertir (amont #1969), alors que path refuse ;
  • les sorties tronquées (« and N more ») ne disent pas comment obtenir le compte exact.

User story

En tant qu'agent, je veux une réponse exacte ou un refus explicite, afin de ne jamais agir sur une réponse devinée.

Acceptance criteria

  • explain sur un nom ambigu refuse et liste les candidats, comme path
  • query sans symbole exact reconnu refuse et propose explain / path avec fichier::Classe
  • Toute sortie tronquée affiche le total exact et l'option pour tout obtenir
  • Codes de sortie distincts : succès, ambigu, introuvable (scriptable par un agent)

Hors scope

  • Recherche sémantique par LLM

Activity

  1. alexwrite commented on Oct 11, 2026

    @alexwrite
    OwnerAuthor

    Commentaire d'origine du 2026-10-11, repris lors du retrait de noms de projets privés.

    Prise en charge.

    Constat sur b4e6307 (code du dépôt, uv run graphify) :

    Cas Comportement actuel Écart
    explain "ItemRepository" (2 fichiers) refuse, liste les 2 candidats, sortie 1 déjà conforme (amont Graphify-Labs#3176/Graphify-Labs#3485)
    explain sur un nom porté par plusieurs nœuds d'un même fichier (ex. deux méthodes .vmsg() de deux classes, stubs sans fichier EntityManagerInterface) répond avec l'un d'eux, sans prévenir path refuse ce cas ; mesuré sur 1 500 libellés tirés au hasard : 22 (projet A), 12 (ClientXCMS), 4 (projet B) réponses devinées
    explain introuvable message, sortie 0 indiscernable d'un succès pour un script
    path ambigu / introuvable sortie 1 dans les deux cas codes non distincts
    query "<langage naturel>" répond toujours, graines choisies par mots isolés pas de mode qui refuse
    explain > 20 connexions « ... and N more », aucune option pour tout voir option manquante
    query tronqué total exact affiché, --budget cité sans la valeur qui suffit valeur manquante

    Aucune PR amont fusionnée ne couvre ces écarts (Graphify-Labs#1970, Graphify-Labs#2305, Graphify-Labs#3491, Graphify-Labs#4134, Graphify-Labs#4289 ouvertes ou fermées sans fusion).

    Découpage, une branche et une PR amont par préoccupation, chacune partie de upstream/v8 :

    1. fix/explain-same-file-tie : explain et les outils MCP get_node / get_neighbors refusent un nom porté par plusieurs nœuds d'un même fichier, comme path.
    2. fix/lookup-exit-codes : codes de sortie distincts pour explain, path, query (0 succès, 3 ambigu, 4 introuvable ; 1 et 2 gardent leur sens actuel : erreur, usage).
    3. feat/query-strict (empilée sur 2) : query --strict ne part que d'un nœud nommé exactement (id, libellé exact, chemin, fichier::Symbole) et refuse sinon en orientant vers explain / path. Le comportement par défaut de query et l'outil MCP restent inchangés : les consignes installées chez les agents et les hooks appellent query "<question>" en langage naturel.
    4. fix/truncation-full-answer-hint : explain --all, et la valeur exacte de --budget / token_budget qui donne la réponse complète dans les messages de troncature.

    Périmètre de fichiers visé :

    • graphify/serve.py
    • graphify/cli.py
    • graphify/main.py (aide)
    • README.md (section « Using the graph directly »)
    • tests/test_explain_cli.py, tests/test_path_cli.py, tests/test_query_cli.py, nouveaux fichiers de test dédiés
  2. added
    enhancementNew feature or request
    p2Défaut visible par l'utilisateur
    on Oct 11, 2026
  3. alexwrite commented on Oct 11, 2026

    @alexwrite
    OwnerAuthor

    Commentaire d'origine du 2026-10-11, repris lors du retrait de noms de projets privés.

    Analyse de l'amont (2026-10-11), à prendre en compte avant d'ouvrir les PR prévues :

  4. alexwrite commented on Oct 11, 2026

    @alexwrite
    OwnerAuthor

    Commentaire d'origine du 2026-10-11, repris lors du retrait de noms de projets privés.

    Point d'étape.

    • fix/explain-same-file-tie (6012a96) : commité, suite complète 7 172 passed, 14 skipped.
    • fix/lookup-exit-codes (13dc240, empilée sur la précédente) : commité, suite complète 7 185 passed, 14 skipped. Effet de bord assumé : six tests de tests/test_cli_export.py interrogeaient "test", absent de leur fixture, et attendaient la sortie 0 ; ils interrogent désormais Transformer, présent, pour continuer à prouver que la commande a lu le bon graphe.
    • feat/query-strict (empilée sur la précédente) : implémentée, suite complète en cours.

    Décision : le mode refus de query est une option --strict, pas le défaut. Raison : les consignes installées chez les agents (hooks PreToolUse, skill) prescrivent graphify query "<question>" en langage naturel ; un refus par défaut casserait ce flux et le serveur MCP. Alternative écartée : refuser par défaut toute question sans symbole exact.

    Issues dérivées : #8 (path devine encore par score), #9 (MCP query_graph sans mode strict), #10 (affected sort 0 sur un nom absent), #11 (abréviations restantes). L'intégration de --strict et des codes de sortie dans la consigne globale des agents relève de #21.

  5. alexwrite commented on Oct 11, 2026

    @alexwrite
    OwnerAuthor

    Commentaire d'origine du 2026-10-11, repris lors du retrait de noms de projets privés.

    Fait

    • explain et les outils MCP get_node / get_neighbors refusent un nom porté par plusieurs nœuds d'un même fichier (deux méthodes homonymes de deux classes, stubs sans fichier), comme path le faisait déjà ; avant, un nœud était choisi par l'ordre du graphe. Le refus entre fichiers existait déjà.
    • explain, path et query ont des codes de sortie distincts : 0 réponse, 3 nom ambigu (candidats listés), 4 aucun nœud. Avant : explain et query sortaient 0 sur un nom absent, path sortait 1 pour tout.
    • query --strict ne part que d'un nœud que la question nomme exactement (id, libellé exact, chemin, fichier::Symbole) et refuse sinon, en orientant vers explain "<path>::<Symbol>" et path "<A>" "<B>". Sans l'option, query est inchangé.
    • Toute sortie tronquée de explain, query, get_neighbors, get_community donne le total et l'option qui donne tout : explain --all, et la valeur exacte de --budget / token_budget.

    Cas réels, graphe projet A, code du dépôt :

    Commande Avant (b4e6307) Après (main 6148f31)
    explain "ItemRepository" refus, 2 candidats, sortie 1 refus, 2 candidats, sortie 3
    query "how does a host listing page reach the database" 74 nœuds sur 7 314, graines LISTING, page(), Item, DATABASES, un test, sortie 0 inchangé sans option ; le bandeau de troncature donne --budget 271125 (relancé avec cette valeur : 7 314 nœuds sur 7 314)
    même question avec --strict option ignorée refus, sortie 4, orientation vers explain / path
    query "src/Repository/ItemRepository.php::ItemRepository" --strict option ignorée part de ce seul nœud, 1 496 nœuds atteints, sortie 0
    explain "src/Repository/ItemRepository.php::ItemRepository" « and 401 more » « and 401 more (--all lists all 421) » ; --all en liste 421

    Comment

    Pourquoi ce choix

    • Le refus de query est une option (--strict) et non le défaut : les hooks PreToolUse et la skill installés chez les agents prescrivent graphify query "<question>" en langage naturel, et le serveur MCP sert le même texte. Un refus par défaut casserait ce flux. Alternative écartée : refuser par défaut toute question sans symbole exact.
    • « Nommer un nœud » a une seule définition, celle des extrémités de path (id, libellé exact, chemin, fichier::Symbole), partagée par query --strict. Alternative écartée : bâtir sur la PR amont feat(query): --seed to start the traversal at given nodes Graphify-Labs/graphify#4289 (--seed), qui résout par affected.resolve_seed (un second résolveur) et ignore les graines non résolues au lieu de refuser.
    • Codes 3 et 4 nouveaux plutôt que 1 : 1 sert déjà à toutes les erreurs (graphe absent, usage), il ne peut pas porter l'ambiguïté. Les tests qui figeaient 1 ont été mis à jour.
    • Le budget annoncé est calculé par la règle même qui coupe : c'est le plus petit qui rend toute la réponse (vérifié : un jeton de moins coupe encore).

    Apprentissages

    Faiblesses restantes

    Quality

    • Tests : uv run pytest tests/ -q sur main 6148f31 : 7 528 réussis, 14 ignorés, 0 échec. Par branche, partie de b4e6307 (7 165 réussis) : 7 172, 7 185, 7 191, 7 169 réussis, 0 échec.
    • Analyse statique : uv run pyright : 617 erreurs sur main, ensemble identique (fichier + message) à origin/main avant fusion ; 607 sur chaque branche, identique à b4e6307. Aucune erreur dans le code ajouté.
    • Linter : uv run ruff check . : propre sur chaque branche et sur main.
    • CI du fork sur main 6148f31 : succès (run 38130958081 : skillgen-check, security-scan, tests Python 3.10, 3.12, 3.13, 3.14).
  6. alexwrite commented on Oct 11, 2026

    @alexwrite
    OwnerAuthor

    Commentaire d'origine du 2026-10-11, repris lors du retrait de noms de projets privés.

    Livré sur main (6148f31), voir la synthèse ci-dessus.

  7. github-actions commented on Oct 11, 2026

    @github-actions

    Thanks for opening this issue, @alexwrite. A maintainer will take a look soon.

    If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.

    To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestp2Défaut visible par l'utilisateur

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions