# Design Note — NavigateCoords Consumption Gap (Write-Only) **Auteur**: Qwen **Date**: 2026-07-02 **Statut**: DESIGN NOTE — pas de câblage sans GO Dom **Référence**: `tests/unit/test_coords_consumption_gap.py` (10 tests PASSING documenting the gap) --- ## Executive Summary Le module navigation (`core/navigation`) produit des coords normalisés (`NavigateCoords`) via OCR/VLM, les stocke dans `replay_state["variables"]`, mais **aucun consommateur** dans le runtime n'utilise ces coords. Le résultat est un pattern **write-only** : coords générés mais jamais consommés par les actions suivantes (click/type). Trois gaps structurels confirmés par code lecture : --- ## Gap A — Compiler Produces Literals, Not Templates **Localisation**: `replay_engine.py:1832-1846` (`_edge_to_normalized_actions`) **Problème**: Pour `mouse_click`, le compiler bake `x_pct` et `y_pct` comme **floats littéraux** depuis `by_position` : ```python # replay_engine.py:1843-1846 normalized["type"] = "click" normalized["x_pct"] = x_pct # float littéral (ex: 0.15) normalized["y_pct"] = y_pct # float littéral (ex: 0.07) ``` Ces floats sont **hardcodés** dans le step definition. Il n'existe pas de mécanisme pour référencer les coords navigate via templates comme `{{navigate_login_coords.x_pct}}`. **La substitution existante ne couvre pas ce cas** : - `_substitute_variables()` → `${var}` → appliqué uniquement à `text_input.text` - `_RUNTIME_VAR_PATTERN` → `{{var.field}}` → compilé regex, **jamais appliqué à `x_pct/y_pct`** **Conséquence**: Un navigate step qui résolve coords login à (0.15, 0.07) ne peut PAS injecter ces coords dans un click step suivant, car le click step a ses propres `x_pct/y_pct` hardcodés. --- ## Gap B — Zero Consumers in Runtime **Localisation**: `core/navigation/__init__.py:43-113` (`_handle_navigate_action`) **Problème**: `_handle_navigate_action` stocke coords dans `replay_state["variables"]` : ```python # core/navigation/__init__.py:100-105 if result.login_coords: variables[login_var] = result.login_coords.to_dict() # → {"x_pct": 0.15, "y_pct": 0.07, "method": "ocr_anchor"} ``` **Zéro consommateur** : aucun action handler (click, type, double_click, right_click) lit `variables["navigate_login_coords"]` pour résoudre ses propres coords. Chaque action utilise exclusivement `by_position` depuis son edge definition. **Preuve par grep** : `navigate_login_coords|navigate_password_coords|navigate_submit_coords` apparaît uniquement dans : - `core/navigation/__init__.py` (write) - `tests/unit/test_*.py` (test verification) - **0 occurrences** dans `replay_engine.py` action dispatch ou `api_stream.py` action handlers --- ## Gap C — Navigate Edge → Empty Actions List **Localisation**: `replay_engine.py:1806-1955` (`_edge_to_normalized_actions`) **Problème**: Le type `navigate` est dans `_ALLOWED_ACTION_TYPES` (ligne 44) et possède un handler câblé dans `api_stream.py` (ligne 4459-4463 via `_handle_navigate_action`). Mais `_edge_to_normalized_actions` **n'a pas de branche** pour `navigate` : ```python # replay_engine.py:1954-1955 (else branch) else: logger.warning(f"Type d'action inconnu : {action_type}") return [] ``` **Conséquence** : Quand le BFS traverse un edge navigate, `_edge_to_normalized_actions(edge, params)` retourne `[]`. L'action navigate est **skippée** dans le path. Le handler existe dans `api_stream.py` mais est **inaccessible** car le normalized action dict n'est jamais produit. **Paradoxe** : Le navigate handler est câblé et fonctionnel, mais le pipeline edge→action le bloque à l'entrée. --- ## Options de Résolution ### Option 1 — Compiler Injection (modifier `_edge_to_normalized_actions`) **Approche**: Ajouter une branche `navigate` dans `_edge_to_normalized_actions` qui produit un normalized action dict. Modifier les actions click/type pour permettre des template refs `{{navigate_login_coords.x_pct}}` dans `x_pct/y_pct`, avec résolution runtime. ```python # Option 1 — Branch navigate dans _edge_to_normalized_actions elif action_type == "navigate": normalized["type"] = "navigate" normalized["parameters"] = { "action": action_params.get("action", "login"), "login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"), "password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"), "submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"), } return [normalized] ``` **+ Avantages** : - Minimal change — 1 branche ajoutée + template resolution dans click/type - Compatible avec handler existant (`_handle_navigate_action`) - BFS path inclut navigate → handler appelé → coords stockés → consommés **– Risques** : - Template resolution dans `x_pct/y_pct` nécessite modification de click/type dispatch - Float vs string : `{{navigate_login_coords.x_pct}}` résout en `"0.15"` (string), pas `0.15` (float) — nécessite conversion - Ordonnancement : navigate doit s'exécuter AVANT les actions click/type qui consomment ses coords — scheduling implication ### Option 2 — Declarative YAML Templates (step definitions avec coords_template) **Approche**: Ajouter un champ `coords_template` dans les step YAML definitions. Au runtime, le template est résolu par substitution des variables navigate. ```yaml # Option 2 — YAML step definition avec coords_template steps: - action: navigate parameters: action: login login_coords_var: navigate_login_coords - action: mouse_click coords_template: "{{navigate_login_coords}}" # Au runtime : x_pct/y_pct résolus depuis navigate_login_coords dict ``` **+ Avantages** : - Déclaratif — coords templates dans YAML, pas hardcoded - Séparation compiler/runtime : compiler produit templates, runtime résout - Extensible à autres types de coords (search, dossier) **– Risques** : - Plus de changement : schema YAML + template resolver + compiler modifications - Retro-compatibilité : workflows existants sans coords_template doivent continuer à fonctionner (fallback by_position) - Validation : templates malformés → runtime errors subtiles --- ## Table Comparative | Critère | Option 1 (Compiler Injection) | Option 2 (YAML Templates) | |---------|-------------------------------|---------------------------| | Changement code | Small — 1 branch + template resolve | Medium — schema + resolver + compiler | | Retro-compat | Full — by_position fallback intact | Full — fallback by_position si pas de template | | Ordonnancement | Navigate avant click (BFS order) | Navigate avant click (step order) | | Extensibilité | Navigate-specific | General — coords_template applicable à tout | | Risque runtime | Float/string conversion | Template validation errors | | Tests impact | 1-3 nouveaux tests | 5-8 nouveaux tests (schema + resolver) | | GO Dom needed | YES | YES | | Timeline | ~2h implementation | ~4h implementation + schema design | --- ## Test Rouge Proposal **Objectif**: Démontrer Gap C avec 1 test unitaire qui montre qu'un edge navigate produit une empty action list. ```python # tests/unit/test_coords_consumption_gap.py — ajout proposé def test_gap_c_navigate_edge_produces_empty_actions(): """Gap C: _edge_to_normalized_actions returns [] for navigate edge. Prove: navigate is in _ALLOWED_ACTION_TYPES but has no branch in _edge_to_normalized_actions → falls into else → empty list. """ from agent_v0.server_v1.replay_engine import _edge_to_normalized_actions # Minimal mock edge with navigate action type edge = MockEdge( edge_id="e1", from_node="start", to_node="login", action=MockAction( type="navigate", target=None, parameters={"action": "login"}, ), ) result = _edge_to_normalized_actions(edge, {}) # GAP: navigate edge produces zero actions assert result == [], f"Expected empty list, got {result}" # This proves the handler in api_stream.py is unreachable ``` **Note**: Ce test est un **red flag** — il doit FAIL quand le gap est résolu (navigate branch ajoutée → result ≠ []). Il sert de guardrail : si quelqu'un câble navigate sans résoudre les gaps A+B, le test rouge continue à signaler le problème. --- ## Decision Required from Dom **⚠️ PAS DE CÂBLAGE SANS GO DOM** Ce design note documente les gaps et propose des options. La décision appartient à Dom : 1. **Option préférée** : 1 (compiler injection) ou 2 (YAML templates) ? 2. **Timeline** : implémenter maintenant (POC phase) ou post-POC ? 3. **Scope** : navigate login only, ou general coords template system ? 4. **Test rouge** : ajouter le test gap C maintenant (documentation) ou attendre GO ? --- ## Appendix — Code References | Fichier | Lignes | Rôle | |---------|--------|------| | `replay_engine.py:44` | `_ALLOWED_ACTION_TYPES` includes "navigate" | Allowlist | | `replay_engine.py:1806-1955` | `_edge_to_normalized_actions` — no navigate branch | Gap C | | `replay_engine.py:1843-1846` | mouse_click bakes literal x_pct/y_pct | Gap A | | `core/navigation/__init__.py:43-113` | `_handle_navigate_action` — writes coords to variables | Gap B (write) | | `core/navigation/action_resolver.py:47-62` | `NavigateCoords` dataclass definition | Data model | | `api_stream.py:4459-4463` | navigate handler dispatch | Wired but unreachable | | `tests/unit/test_coords_consumption_gap.py` | 10 tests documenting write-only gap | Evidence | --- *Qwen — design note, pas wiring. GO Dom required.*