Repository navigation
Zéro ambiguïté : refuser plutôt que répondre à côté #20
Description
Activity
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) explainsur un nom porté par plusieurs nœuds d'un même fichier (ex. deux méthodes.vmsg()de deux classes, stubs sans fichierEntityManagerInterface)répond avec l'un d'eux, sans prévenir pathrefuse ce cas ; mesuré sur 1 500 libellés tirés au hasard : 22 (projet A), 12 (ClientXCMS), 4 (projet B) réponses devinéesexplainintrouvablemessage, sortie 0 indiscernable d'un succès pour un script pathambigu / introuvablesortie 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 querytronquétotal exact affiché, --budgetcité sans la valeur qui suffitvaleur 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:fix/explain-same-file-tie:explainet les outils MCPget_node/get_neighborsrefusent un nom porté par plusieurs nœuds d'un même fichier, commepath.fix/lookup-exit-codes: codes de sortie distincts pourexplain,path,query(0 succès, 3 ambigu, 4 introuvable ; 1 et 2 gardent leur sens actuel : erreur, usage).feat/query-strict(empilée sur 2) :query --strictne part que d'un nœud nommé exactement (id, libellé exact, chemin,fichier::Symbole) et refuse sinon en orientant versexplain/path. Le comportement par défaut dequeryet l'outil MCP restent inchangés : les consignes installées chez les agents et les hooks appellentquery "<question>"en langage naturel.fix/truncation-full-answer-hint:explain --all, et la valeur exacte de--budget/token_budgetqui 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
- addedenhancementNew feature or requestNew feature or requestp2Défaut visible par l'utilisateurDéfaut visible par l'utilisateur
on Oct 11, 2026 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 :
Graphify-Labs/graphify#4289(ouverte, mergeable, revue en cours) :query --seedpour partir de nœuds donnés. Recoupefeat/query-strict: construire dessus ou l'intégrer plutôt que proposer un second mécanisme.- Correction de l'issue : le critère «
explainrésout un nom ambigu sans avertir » venait du titre de l'amont explain resolves ambiguous terms silently (no warning), unlike path; resolution degrades as graph grows Graphify-Labs/graphify#1969, déjà corrigé (explain: silently returns one arbitrary match when a symbol name is ambiguous (no warning, no match count, no way to qualify) Graphify-Labs/graphify#3176, explain: the ambiguity message says retry with the repo-relative path, but a path resolves to the file node, not the symbol; there is no path-scoped symbol form Graphify-Labs/graphify#3485). Le vrai écart restant est celui mesuré dans le commentaire de prise en charge : homonymes dans un même fichier.
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 detests/test_cli_export.pyinterrogeaient"test", absent de leur fixture, et attendaient la sortie 0 ; ils interrogent désormaisTransformer, 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
queryest une option--strict, pas le défaut. Raison : les consignes installées chez les agents (hooksPreToolUse, skill) prescriventgraphify 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 (
pathdevine encore par score), #9 (MCPquery_graphsans mode strict), #10 (affectedsort 0 sur un nom absent), #11 (abréviations restantes). L'intégration de--strictet des codes de sortie dans la consigne globale des agents relève de #21.Commentaire d'origine du 2026-10-11, repris lors du retrait de noms de projets privés.
Fait
explainet les outils MCPget_node/get_neighborsrefusent un nom porté par plusieurs nœuds d'un même fichier (deux méthodes homonymes de deux classes, stubs sans fichier), commepathle faisait déjà ; avant, un nœud était choisi par l'ordre du graphe. Le refus entre fichiers existait déjà.explain,pathetqueryont des codes de sortie distincts :0réponse,3nom ambigu (candidats listés),4aucun nœud. Avant :explainetquerysortaient 0 sur un nom absent,pathsortait 1 pour tout.query --strictne part que d'un nœud que la question nomme exactement (id, libellé exact, chemin,fichier::Symbole) et refuse sinon, en orientant versexplain "<path>::<Symbol>"etpath "<A>" "<B>". Sans l'option,queryest inchangé.- Toute sortie tronquée de
explain,query,get_neighbors,get_communitydonne 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 ( main6148f31)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 0inchangé sans option ; le bandeau de troncature donne --budget 271125(relancé avec cette valeur : 7 314 nœuds sur 7 314)même question avec --strictoption ignorée refus, sortie 4, orientation vers explain/pathquery "src/Repository/ItemRepository.php::ItemRepository" --strictoption 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) » ; --allen liste 421Comment
graphify/serve.py:1883_tier_refusal: décide si le palier gagnant nomme un seul nœud ; appelé par_resolve_single_node(:1863, explain, get_node, get_neighbors) et_resolve_exact_node(:1965, path etquery --strict).graphify/serve.py:1901_is_ambiguity_refusal: distingue un refus ambigu d'un nom absent par le préfixe que partagent les deux messages d'ambiguïté (une seule constante).graphify/serve.py:1524_question_seeds: la sélection de graines par mots-clés, déplacée sans changement ;_query_graph_textacceptestart_nodes.graphify/serve.py:1407et:1435: budget exact dans les bandeaux de troncature.graphify/cli.py:89EXIT_AMBIGUOUS/EXIT_NOT_FOUND;:1328--strict;:1872--all.graphify/__main__.py(--help) etREADME.md(« Using the graph directly ») documentent les codes,--strictet--all.- Tests ajoutés :
tests/test_explain_same_file_tie.py,tests/test_lookup_exit_codes.py,tests/test_query_strict.py,tests/test_truncation_full_answer.py. Chacun échoue sans son correctif (5/7, 8/13, 5/6, 4/4 en échec ; les tests qui passent sont des garde-fous du comportement inchangé). - Branches, une par préoccupation, PR amont : fix(explain): refuse a name that several nodes of one file carry Graphify-Labs/graphify#4320 (
fix/explain-same-file-tie), fix(cli): give lookups distinct exit codes for ambiguous and missing names Graphify-Labs/graphify#4321 (fix/lookup-exit-codes, empilée), feat(query): add --strict to refuse a question that names no node Graphify-Labs/graphify#4322 (feat/query-strict, empilée), fix(explain,query): say how to get the full answer when output is cut Graphify-Labs/graphify#4323 (fix/truncation-full-answer-hint). Toutes fusionnées dansmaindu fork en--no-ff.
Pourquoi ce choix
- Le refus de
queryest une option (--strict) et non le défaut : les hooksPreToolUseet la skill installés chez les agents prescriventgraphify 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 parquery --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 paraffected.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
- Le refus d'
explainentre fichiers existait déjà en amont (explain: silently returns one arbitrary match when a symbol name is ambiguous (no warning, no match count, no way to qualify) Graphify-Labs/graphify#3176, explain: the ambiguity message says retry with the repo-relative path, but a path resolves to the file node, not the symbol; there is no path-scoped symbol form Graphify-Labs/graphify#3485) ; le trou réel était le même fichier, mesuré à 22 / 12 / 4 réponses devinées sur 1 500 libellés tirés au hasard (projet A, ClientXCMS, projet B). - Dans
_subgraph_to_text, les arêtes ne sont jamais coupées dès que tous les nœuds tiennent (queryprints the TRUNCATED banner when 0 nodes were cut Graphify-Labs/graphify#2601) : le budget « tout obtenir » se calcule sur la fin du dernier nœud, pas sur la longueur totale. - L'intégration PHP (fix(php): consolidated PHP support — member-call resolution, use-import handling, node identity, cross-language isolation, query seeding (#1682, #2615) Graphify-Labs/graphify#2617) poussée sur
mainen parallèle a changé deux choses que nos tests touchaient : chaque stub sans fichier devient son propre rival (libellé du refus différent) et la sélection de graines. Résolu à la fusion (9d45f0e,a34eb28,6148f31). - Six tests de
tests/test_cli_export.pyinterrogeaient un nom absent de leur fixture et attendaient la sortie 0 : ils prouvaient « pas d'erreur », pas « réponse ». Ils interrogent maintenantTransformer, présent.
Faiblesses restantes
pathdevine encore une extrémité qui ne nomme aucun nœud exactement (classement par score) et répond avec la sortie 0 : path : refuser un point d'extrémité deviné par score au lieu de tracer un chemin #8.- L'outil MCP
query_graphn'a pas de mode strict : MCP query_graph : un mode strict qui refuse au lieu de deviner #9. affectedsort 0 sur un nom absent : affected : signaler un nom ambigu ou introuvable par le code de sortie #10.- Abréviations restantes sans option complète (attributs dans
query,get_node,get_pr_impact) : Abréviations restantes : dire comment obtenir la valeur complète #11. - Les codes de sortie et
--strictne sont pas encore dans la consigne des agents : relève de Intégration globale : Graphify automatique pour tous les agents, tous les projets #21 (commentaire posté). - PR fix(cli): give lookups distinct exit codes for ambiguous and missing names Graphify-Labs/graphify#4321 et feat(query): add --strict to refuse a question that names no node Graphify-Labs/graphify#4322 sont empilées : l'amont devra fusionner fix(explain): refuse a name that several nodes of one file carry Graphify-Labs/graphify#4320 d'abord.
- Vérifié sous Linux uniquement.
Quality
- Tests :
uv run pytest tests/ -qsurmain6148f31: 7 528 réussis, 14 ignorés, 0 échec. Par branche, partie deb4e6307(7 165 réussis) : 7 172, 7 185, 7 191, 7 169 réussis, 0 échec. - Analyse statique :
uv run pyright: 617 erreurs surmain, ensemble identique (fichier + message) àorigin/mainavant fusion ; 607 sur chaque branche, identique àb4e6307. Aucune erreur dans le code ajouté. - Linter :
uv run ruff check .: propre sur chaque branche et surmain. - CI du fork sur
main6148f31: succès (run 38130958081 : skillgen-check, security-scan, tests Python 3.10, 3.12, 3.13, 3.14).
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.
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.
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) ;explainrésout un nom ambigu sans avertir (amont#1969), alors quepathrefuse ;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
explainsur un nom ambigu refuse et liste les candidats, commepathquerysans symbole exact reconnu refuse et proposeexplain/pathavecfichier::ClasseHors scope