docs(design+audit): navigate coords consumption gaps + dead code C-MORT audit
DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md: 3 structural gaps with code evidence (Gap A/B/C), 2 resolution options with comparative table, test rouge proposal. AUDIT_CODE_MORT_2026-07-02.md: 8 C-MORT, 5 B-ORPHELIN, 4 duplicats, 3 QG-gated suppression lots (~1900 lines).
This commit is contained in:
218
docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md
Normal file
218
docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# 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.*
|
||||
Reference in New Issue
Block a user