diff --git a/PLAN_SCOPED_VARIABLES.md b/PLAN_SCOPED_VARIABLES.md new file mode 100644 index 00000000..e718b214 --- /dev/null +++ b/PLAN_SCOPED_VARIABLES.md @@ -0,0 +1,214 @@ +# Plan : Scoped Variables pour les Includes de Taskfiles + +## Objectif + +Implémenter le scoping des variables pour les Taskfiles inclus. Les variables d'un include seront accessibles via `{{.namespace.VAR}}` au lieu d'être mergées globalement (problème actuel de conflits). + +## Décisions prises + +- **Accès** : Préfixe namespace (`{{.db.HOST}}`) +- **Conflits** : Préfixage obligatoire, pas de merge global +- **Requires/Preconditions** : Reporté à une phase ultérieure + +--- + +## Visibilité des variables + +| Relation | Accès | Mécanisme | +|----------|-------|-----------| +| Parent → Include | `{{.ns.VAR}}` | Le but principal | +| Include → Parent | Non (via `includes.vars:` si besoin) | Isolation pour réutilisabilité | +| Include → Sibling | Non (via parent comme passeur) | Évite les dépendances implicites | +| Include → Soi-même | `{{.VAR}}` | Naturel | + +**Exemple de passage explicite du parent vers l'include :** +```yaml +includes: + db: + taskfile: ./db/Taskfile.yml + vars: + HOST: "{{.MY_HOST}}" # Passage explicite des vars nécessaires +``` + +**Exemple de passage entre siblings via le parent :** +```yaml +includes: + a: ./a/Taskfile.yml + b: + taskfile: ./b/Taskfile.yml + vars: + A_VAR: "{{.a.SOME_VAR}}" # Parent passe les vars de a à b +``` + +--- + +## Étape 1 : Ajouter l'experiment flag + +**Fichier** : `experiments/experiments.go` + +```go +var ( + GentleForce Experiment + RemoteTaskfiles Experiment + EnvPrecedence Experiment + ScopedVariables Experiment // NEW +) + +func ParseWithConfig(dir string, config *ast.TaskRC) { + // ... + ScopedVariables = New("SCOPED_VARIABLES", config, 1) +} +``` + +Activation : `TASK_X_SCOPED_VARIABLES=1` + +--- + +## Étape 2 : Modifier la structure `Vars` + +**Fichier** : `taskfile/ast/vars.go` + +Ajouter un champ pour stocker les variables scopées par namespace : + +```go +type Vars struct { + om *orderedmap.OrderedMap[string, Var] + mutex sync.RWMutex + scoped map[string]*Vars // NEW: namespace -> vars + scopedMutex sync.RWMutex +} +``` + +Nouvelles méthodes à ajouter : + +```go +// SetScoped stocke les variables d'un namespace +func (vars *Vars) SetScoped(namespace string, scopedVars *Vars) + +// GetScoped récupère les variables d'un namespace +func (vars *Vars) GetScoped(namespace string) (*Vars, bool) + +// MergeScoped merge les variables dans un namespace au lieu de globalement +func (vars *Vars) MergeScoped(namespace string, other *Vars, include *Include) +``` + +Modifier `ToCacheMap()` pour inclure les namespaces comme maps imbriquées : + +```go +func (vars *Vars) ToCacheMap() map[string]any { + m := make(map[string]any, vars.Len()) + // Variables plates existantes + for k, v := range vars.All() { ... } + + // NEW: Ajouter les namespaces comme maps imbriquées + for namespace, scopedVars := range vars.scoped { + m[namespace] = scopedVars.ToCacheMap() + } + return m +} +``` + +Cela permet `{{.db.HOST}}` naturellement via Go templates. + +--- + +## Étape 3 : Modifier `Taskfile.Merge()` + +**Fichier** : `taskfile/ast/taskfile.go` + +```go +func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error { + // ... validations existantes ... + + if experiments.ScopedVariables.Enabled() && include != nil && !include.Flatten { + // Scoped merge : variables dans le namespace + t1.Vars.MergeScoped(include.Namespace, t2.Vars, include) + t1.Env.MergeScoped(include.Namespace, t2.Env, include) + } else { + // Comportement legacy : merge plat + t1.Vars.Merge(t2.Vars, include) + t1.Env.Merge(t2.Env, include) + } + + return t1.Tasks.Merge(t2.Tasks, include, t1.Vars) +} +``` + +--- + +## Étape 4 : Accès local sans préfixe pour les tasks de l'include + +**Fichier** : `compiler.go` + +Dans `getVariables()`, ajouter les variables du namespace courant pour qu'une task de l'include puisse y accéder sans préfixe : + +```go +// Si la task a un namespace, ajouter ses variables localement +if experiments.ScopedVariables.Enabled() && t != nil && t.Namespace != "" { + if scopedVars, ok := c.TaskfileVars.GetScoped(t.Namespace); ok { + for k, v := range scopedVars.All() { + result.Set(k, v) // Accessible via {{.VAR}} dans le namespace + } + } +} +``` + +--- + +## Étape 5 : Gérer les includes imbriqués + +**Fichier** : `taskfile/ast/graph.go` + +Pour les includes imbriqués (a→b→c), propager le path complet du namespace : +- `{{.a.VAR}}` - variable de a +- `{{.a.b.VAR}}` - variable de b (inclus par a) + +Option : ajouter `FullNamespacePath []string` dans `Include` ou calculer lors du merge. + +--- + +## Étape 6 : Gérer le cas `flatten: true` + +Déjà couvert par la condition `!include.Flatten` dans l'étape 3. Les includes aplatis conservent le comportement actuel (merge global). + +--- + +## Étape 7 : Tests + +**Nouveau fichier** : `testdata/includes/scoped_vars/` + +Scénarios de test : +1. **Accès cross-namespace** : `{{.db.HOST}}` depuis le Taskfile parent +2. **Accès local** : `{{.HOST}}` depuis une task de l'include `db` +3. **Override via include.vars** : Les vars passées dans `includes:` continuent de fonctionner +4. **Pas de conflits** : Deux includes avec même nom de variable ne se marchent pas dessus +5. **Flatten** : `flatten: true` conserve le merge global +6. **Includes imbriqués** : `{{.a.b.VAR}}` + +--- + +## Fichiers à modifier + +| Fichier | Changement | +|---------|------------| +| `experiments/experiments.go` | Ajouter `ScopedVariables` experiment | +| `taskfile/ast/vars.go` | Ajouter `scoped` map, `MergeScoped()`, modifier `ToCacheMap()` | +| `taskfile/ast/taskfile.go` | Conditionner le merge scoped/plat | +| `compiler.go` | Injecter les vars du namespace dans le scope local de la task | +| `taskfile/ast/graph.go` | (optionnel) Propager le path namespace pour les includes imbriqués | + +--- + +## Stratégie de migration + +1. **v3.x** : Feature derrière `TASK_X_SCOPED_VARIABLES=1` +2. **v4.0** : Activer par défaut (breaking change documenté) +3. **Migration** : Mettre à jour les templates `{{.VAR}}` → `{{.namespace.VAR}}` + +--- + +## Risques et considérations + +- **Breaking change** : Les Taskfiles existants utilisant des variables d'includes sans préfixe casseront +- **Performance** : Négligeable (ajout d'une map) +- **Complexité** : Modérée, mais bien isolée dans quelques fichiers diff --git a/PLAN_SCOPED_VARIABLES_FINAL.md b/PLAN_SCOPED_VARIABLES_FINAL.md new file mode 100644 index 00000000..abdb6e09 --- /dev/null +++ b/PLAN_SCOPED_VARIABLES_FINAL.md @@ -0,0 +1,248 @@ +# Plan : Scoped Includes (Lazy DAG) + +## Objectif + +Scoper les variables des Taskfiles inclus via **lazy resolution** sur le DAG, avec **isolation stricte**. + +## Experiment Flag + +```bash +TASK_X_SCOPED_INCLUDES=1 +``` + +--- + +## Modèle de scopes + +### Priorité (croissante) + +``` +1. Environment ← Shell + CLI (task FOO=bar) +2. Root vars ← Taskfile racine +3. Include vars ← Chaîne d'héritage (parent → enfant) +4. Task vars ← Plus haut +``` + +### Visibilité (isolation stricte) + +| Depuis | Voit | Ne voit PAS | +|--------|------|-------------| +| Root | Ses vars | Vars des includes | +| Include | Ses vars + héritage parent | Vars des siblings | +| Task | Toute la chaîne d'héritage | - | + +### Partage de variables + +Pour partager des vars entre plusieurs Taskfiles, utiliser `flatten: true` : + +```yaml +includes: + common: + taskfile: ../common/Taskfile.yml + flatten: true # Merge global, pas de scoping +``` + +--- + +## Considérations spéciales + +### Variables dynamiques (`sh:`) + +```yaml +vars: + VERSION: + sh: git describe --tags +``` + +- Exécutées dans le `Dir` de leur Taskfile d'origine +- Le champ `Var.Dir` stocke le répertoire d'exécution +- Cache des résultats pour éviter les re-exécutions + +### Defer + +- Utilise le même cache de variables résolu +- Pas d'impact sur l'implémentation + +### Ordre de résolution + +```yaml +vars: + A: "hello" + B: "{{.A}} world" + C: + sh: echo "{{.B}}" +``` + +- Variables résolues dans l'ordre de déclaration +- Chaîne d'héritage résolue AVANT les vars locales + +--- + +## Architecture + +### Avant (merge global) + +``` +Reader.Read() → TaskfileGraph + ↓ +TaskfileGraph.Merge() → Taskfile unique + - Vars mergées globalement (last-one-wins) + ↓ +Compiler.TaskfileVars = Toutes les vars mergées +``` + +### Après (lazy DAG) + +``` +Reader.Read() → TaskfileGraph + ↓ +TaskfileGraph.Merge() → Taskfile racine + - Vars NON mergées (restent dans le DAG) + - Tasks mergées (comme avant) + ↓ +Executor.Graph = DAG préservé + ↓ +Compiler.getVariablesLazy(task) → Traverse le DAG +``` + +--- + +## Implémentation + +### Étape 1 : Experiment flag + +**Fichier** : `experiments/experiments.go` + +```go +var ScopedIncludes Experiment + +func ParseWithConfig(dir string, config *ast.TaskRC) { + // ... + ScopedIncludes = New("SCOPED_INCLUDES", config, 1) +} +``` + +### Étape 2 : Stocker le DAG dans l'Executor + +**Fichier** : `executor.go` + +```go +type Executor struct { + // ... + Graph *ast.TaskfileGraph +} +``` + +**Fichier** : `setup.go` + +```go +func (e *Executor) readTaskfile(node taskfile.Node) error { + graph, err := reader.Read(ctx, node) + e.Graph = graph + e.Taskfile, err = graph.Merge() + return nil +} +``` + +### Étape 3 : Ne pas merger les vars (si experiment ON) + +**Fichier** : `taskfile/ast/taskfile.go` + +```go +func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include, experimentEnabled bool) error { + if !experimentEnabled || include.Flatten { + // Legacy ou flatten : merge global + t1.Vars.Merge(t2.Vars, include) + t1.Env.Merge(t2.Env, include) + } + // Sinon : vars restent dans le DAG + + return t1.Tasks.Merge(t2.Tasks, include, t1.Vars) +} +``` + +### Étape 4 : Helpers pour le DAG + +**Fichier** : `taskfile/ast/graph.go` + +```go +func (tfg *TaskfileGraph) Root() (*TaskfileVertex, error) + +func (tfg *TaskfileGraph) GetVertexByNamespace(namespace string) (*TaskfileVertex, error) +``` + +### Étape 5 : Résolution lazy dans le Compiler + +**Fichier** : `compiler.go` + +```go +type Compiler struct { + // ... + Graph *ast.TaskfileGraph + varsCache map[string]*ast.Vars // Cache par namespace +} + +func (c *Compiler) getVariables(t *ast.Task, call *Call, eval bool) (*ast.Vars, error) { + if experiments.ScopedIncludes.Enabled() { + return c.getVariablesLazy(t, call, eval) + } + // Legacy... +} + +func (c *Compiler) getVariablesLazy(t *ast.Task, call *Call, eval bool) (*ast.Vars, error) { + result := env.GetEnviron() + + // 1. Special vars + // 2. Root vars (depuis DAG.Root()) + // 3. Include chain vars (traverse DAG selon t.Namespace) + // 4. CLI vars (call.Vars) + // 5. Task vars (t.Vars) + + return result, nil +} +``` + +--- + +## Fichiers à modifier + +| Fichier | Changement | +|---------|------------| +| `experiments/experiments.go` | Ajouter `ScopedIncludes` | +| `executor.go` | Ajouter `Graph` | +| `setup.go` | Stocker le DAG | +| `taskfile/ast/taskfile.go` | Ne pas merger vars si experiment ON | +| `taskfile/ast/graph.go` | Helpers `Root()`, `GetVertexByNamespace()` | +| `compiler.go` | `getVariablesLazy()` + cache | + +--- + +## Tests + +### Variables +1. Héritage : include voit vars du parent +2. Override : include peut override une var du parent +3. Isolation : parent ne voit PAS vars de l'include +4. Siblings : includes ne se voient pas entre eux +5. Chaîne : root → a → b fonctionne +6. Flatten : `flatten: true` = merge global +7. Legacy : flag OFF = comportement inchangé + +### Variables dynamiques +8. `sh:` exécuté dans le Dir de l'include +9. Var dynamique référençant une var héritée +10. Cache des résultats + +### Defer +11. Defer a accès aux vars scopées + +--- + +## Ordre des commits + +1. `feat(experiments): add SCOPED_INCLUDES experiment` +2. `feat(executor): store TaskfileGraph for lazy resolution` +3. `feat(graph): add Root() and GetVertexByNamespace() helpers` +4. `feat(taskfile): skip var merge when SCOPED_INCLUDES enabled` +5. `feat(compiler): implement lazy variable resolution` +6. `test: add scoped includes variable tests` diff --git a/PLAN_SCOPED_VARIABLES_V2_LAZY_DAG.md b/PLAN_SCOPED_VARIABLES_V2_LAZY_DAG.md new file mode 100644 index 00000000..e0e68167 --- /dev/null +++ b/PLAN_SCOPED_VARIABLES_V2_LAZY_DAG.md @@ -0,0 +1,315 @@ +# Plan V2 : Scoped Variables avec Lazy DAG + +## Objectif + +Implémenter le scoping des variables pour les Taskfiles inclus avec **lazy resolution via DAG** et **accès cross-namespace**. + +## Décisions prises + +- **Architecture** : Lazy resolution via DAG (pas de merge global des variables) +- **Accès cross-namespace** : `{{.namespace.VAR}}` pour accéder aux variables d'un include +- **Isolation** : Un include ne voit pas les vars du parent (sauf passage explicite via `includes.vars:`) +- **Requires/Preconditions** : Reporté à une phase ultérieure + +--- + +## Comparaison des approches + +| Aspect | Plan V1 (scoped merge) | Plan V2 (lazy DAG) | +|--------|------------------------|---------------------| +| Merge des vars | Oui, dans map scopée | Non, préservées dans le DAG | +| Résolution | Build time | Runtime (lazy) | +| Performance | Résout tout | Résout seulement le nécessaire | +| Complexité | Modérée | Plus élevée | +| Accès cross-namespace | `{{.ns.VAR}}` via map | `{{.ns.VAR}}` via traversée DAG | + +--- + +## Modèle de scopes (priorité croissante) + +``` +1. Environment Scope (plus bas) + - Variables d'environnement shell + - Variables CLI (task VAR=value) + +2. Entrypoint Scope + - Dotenv globaux du Taskfile racine + - Vars globales du Taskfile racine + +3. Include Scope(s) (dans l'ordre d'inclusion) + - Variables passées via `includes.vars:` + - Dotenv de l'include (futur) + - Vars globales de l'include + +4. Task Scope (plus haut) + - Variables passées via une autre task + - Dotenv au niveau task + - Vars au niveau task +``` + +**Règle** : Chaque scope hérite du précédent et peut override. + +--- + +## Visibilité des variables + +| Depuis... | Accès à... | Mécanisme | +|-----------|------------|-----------| +| Parent | Ses propres vars | `{{.VAR}}` | +| Parent | Vars d'un include | `{{.namespace.VAR}}` (traversée DAG) | +| Include | Ses propres vars | `{{.VAR}}` | +| Include | Vars du parent | Non (via `includes.vars:` si besoin) | +| Include | Vars d'un sibling | Non (via parent comme passeur) | + +--- + +## Architecture technique + +### Avant (merge global) + +``` +Reader.Read() → TaskfileGraph + ↓ +TaskfileGraph.Merge() → Taskfile unique + - Vars mergées globalement (last-one-wins) + - Tasks mergées avec namespace + ↓ +Executor.Taskfile = Taskfile unique + ↓ +Compiler.TaskfileVars = Taskfile.Vars (toutes mergées) +``` + +### Après (lazy DAG) + +``` +Reader.Read() → TaskfileGraph + ↓ +TaskfileGraph.Merge() → Taskfile racine + - Vars NON mergées (restent dans chaque vertex du DAG) + - Tasks mergées avec namespace (comme avant) + ↓ +Executor.Taskfile = Taskfile racine +Executor.Graph = TaskfileGraph (NOUVEAU) + ↓ +Compiler.Graph = TaskfileGraph +Compiler.resolveVariables(task) → traverse le DAG lazy +``` + +--- + +## Étapes d'implémentation + +### Étape 1 : Ajouter l'experiment flag + +**Fichier** : `experiments/experiments.go` + +```go +var ScopedVariables Experiment + +func ParseWithConfig(dir string, config *ast.TaskRC) { + ScopedVariables = New("SCOPED_VARIABLES", config, 1) +} +``` + +--- + +### Étape 2 : Préserver le DAG dans l'Executor + +**Fichier** : `executor.go` + `setup.go` + +```go +type Executor struct { + // ... existant ... + Taskfile *ast.Taskfile + Graph *ast.TaskfileGraph // NOUVEAU +} + +func (e *Executor) readTaskfile(node taskfile.Node) error { + graph, err := reader.Read(ctx, node) + e.Graph = graph // Stocker le DAG + e.Taskfile, err = graph.Merge() // Merge pour les tasks + return nil +} +``` + +--- + +### Étape 3 : Modifier le merge pour ne pas merger les variables + +**Fichier** : `taskfile/ast/taskfile.go` + +```go +func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error { + // ... validations existantes ... + + if experiments.ScopedVariables.Enabled() { + // NE PAS merger les variables - elles restent dans le DAG + // Optionnel : merger seulement les vars passées via include.Vars + } else { + // Comportement legacy + t1.Vars.Merge(t2.Vars, include) + t1.Env.Merge(t2.Env, include) + } + + return t1.Tasks.Merge(t2.Tasks, include, t1.Vars) +} +``` + +--- + +### Étape 4 : Passer le DAG au Compiler + +**Fichier** : `compiler.go` + +```go +type Compiler struct { + // ... existant ... + Graph *ast.TaskfileGraph // NOUVEAU +} +``` + +--- + +### Étape 5 : Résolution lazy des variables + +**Fichier** : `compiler.go` + +Nouvelle méthode pour résoudre les variables en traversant le DAG : + +```go +func (c *Compiler) resolveVariablesFromGraph(t *ast.Task) (*ast.Vars, error) { + result := env.GetEnviron() + + // 1. Variables spéciales + specialVars, _ := c.getSpecialVars(t, call) + + // 2. Variables du Taskfile racine (entrypoint scope) + rootVars := c.getRootTaskfileVars() + + // 3. Variables de la chaîne d'includes jusqu'à cette task (include scope) + // Traverser le DAG en suivant le namespace de la task + includeChainVars := c.getIncludeChainVars(t.Namespace) + + // 4. Variables de la task elle-même (task scope) + taskVars := t.Vars + + // Merger dans l'ordre de priorité + // ... + + return result, nil +} + +func (c *Compiler) getIncludeChainVars(namespace string) *ast.Vars { + // Trouver le vertex de l'include dans le DAG + // Récupérer ses variables + // Récursivement remonter si includes imbriqués +} +``` + +--- + +### Étape 6 : Supporter `{{.namespace.VAR}}` dans le templater + +**Fichier** : `internal/templater/templater.go` + +Deux approches possibles : + +**A) Pré-populer une map avec les namespaces accessibles :** +```go +func (c *Compiler) buildTemplateVars(t *ast.Task) map[string]any { + m := make(map[string]any) + + // Variables locales + for k, v := range localVars { + m[k] = v + } + + // Namespaces accessibles (depuis la perspective de cette task) + for _, include := range c.getAccessibleIncludes(t) { + m[include.Namespace] = c.getIncludeVarsAsMap(include) + } + + return m +} +``` + +**B) Custom template function :** +```go +// {{ns "db" "HOST"}} au lieu de {{.db.HOST}} +funcMap := template.FuncMap{ + "ns": func(namespace, varName string) any { + return c.resolveNamespacedVar(namespace, varName) + }, +} +``` + +**Recommandation** : Approche A pour garder la syntaxe `{{.db.HOST}}` + +--- + +### Étape 7 : Gérer les includes imbriqués + +Pour `root → a → b`, les variables doivent être accessibles comme : +- `{{.a.VAR}}` depuis root +- `{{.a.b.VAR}}` depuis root (via a) +- `{{.b.VAR}}` depuis a +- `{{.VAR}}` depuis b (ses propres vars) + +Le namespace complet est déjà tracké dans `Task.Namespace` (e.g., `"a:b:taskname"`). + +--- + +### Étape 8 : Tests + +**Nouveau répertoire** : `testdata/includes/scoped_vars/` + +Scénarios : +1. Accès cross-namespace : `{{.db.HOST}}` depuis root +2. Accès local : `{{.HOST}}` depuis l'include +3. Isolation : include ne voit pas les vars du parent +4. Override via `includes.vars:` +5. Pas de conflits entre includes +6. `flatten: true` conserve le comportement legacy +7. Includes imbriqués : `{{.a.b.VAR}}` +8. Variables dynamiques (`sh:`) dans un include + +--- + +## Fichiers à modifier + +| Fichier | Changement | +|---------|------------| +| `experiments/experiments.go` | Ajouter `ScopedVariables` | +| `executor.go` | Ajouter champ `Graph` | +| `setup.go` | Stocker le DAG dans l'Executor | +| `taskfile/ast/taskfile.go` | Conditionner le merge des vars | +| `compiler.go` | Traversée lazy du DAG, accès au Graph | +| `internal/templater/templater.go` | (optionnel) Support `{{.ns.VAR}}` | + +--- + +## Stratégie de migration + +1. **v3.x** : Feature derrière `TASK_X_SCOPED_VARIABLES=1` +2. **v4.0** : Activer par défaut (breaking change documenté) +3. **Migration** : + - Les vars d'includes ne sont plus accessibles directement → utiliser `{{.namespace.VAR}}` + - Les vars du parent ne sont plus visibles dans l'include → passer via `includes.vars:` + +--- + +## Avantages de cette approche + +1. **Performance** : Résolution lazy, on ne calcule que ce qui est nécessaire +2. **Clarté** : Chaque scope est explicite +3. **Pas de conflits** : Les variables ne se marchent plus dessus +4. **Réutilisabilité** : Un include peut être utilisé dans différents contextes +5. **Utilise l'existant** : Le DAG existe déjà, on l'exploite mieux + +--- + +## Risques et considérations + +- **Breaking change** : Important - comportement fondamentalement différent +- **Complexité** : La traversée du DAG est plus complexe que le merge +- **Debug** : Plus difficile de comprendre d'où vient une variable (ajouter du logging) diff --git a/PLAN_SCOPED_VARIABLES_V3_LAZY_SIMPLE.md b/PLAN_SCOPED_VARIABLES_V3_LAZY_SIMPLE.md new file mode 100644 index 00000000..dd1a38b9 --- /dev/null +++ b/PLAN_SCOPED_VARIABLES_V3_LAZY_SIMPLE.md @@ -0,0 +1,208 @@ +# Plan V3 : Lazy DAG avec Isolation Stricte + +## Objectif + +Implémenter le scoping des variables via **lazy resolution** et **isolation stricte** (pas de `{{.namespace.VAR}}`). + +## Décisions + +- **Architecture** : Lazy resolution via DAG +- **Isolation stricte** : Pas d'accès cross-namespace +- **Héritage** : Include hérite du parent, pas l'inverse +- **Experiment** : `TASK_X_SCOPED_VARIABLES=1` + +--- + +## Modèle de scopes + +``` +Environment → Shell + CLI vars + ↓ hérite +Entrypoint → Root Taskfile vars + ↓ hérite +Include(s) → Vars de chaque include (dans l'ordre) + ↓ hérite +Task → Vars de la task +``` + +Chaque scope hérite du précédent et peut override. + +--- + +## Visibilité + +| Depuis | Voit | Ne voit PAS | +|--------|------|-------------| +| Root | Ses vars | Vars des includes | +| Include | Ses vars + parent | Vars des siblings | +| Task | Toute la chaîne | - | + +**Communication unidirectionnelle** : parent → enfant via `includes.vars:` + +--- + +## Changements requis + +### 1. Experiment flag + +**Fichier** : `experiments/experiments.go` + +```go +var ScopedVariables Experiment + +func ParseWithConfig(dir string, config *ast.TaskRC) { + ScopedVariables = New("SCOPED_VARIABLES", config, 1) +} +``` + +--- + +### 2. Préserver le DAG + +**Fichier** : `executor.go` + +```go +type Executor struct { + Taskfile *ast.Taskfile + Graph *ast.TaskfileGraph // NOUVEAU +} +``` + +**Fichier** : `setup.go` + +```go +func (e *Executor) readTaskfile(node taskfile.Node) error { + graph, err := reader.Read(ctx, node) + e.Graph = graph + e.Taskfile, err = graph.Merge() + return nil +} +``` + +--- + +### 3. Ne pas merger les vars + +**Fichier** : `taskfile/ast/taskfile.go` + +```go +func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error { + if !experiments.ScopedVariables.Enabled() { + // Legacy : merge global + t1.Vars.Merge(t2.Vars, include) + t1.Env.Merge(t2.Env, include) + } + // Les vars restent dans le DAG, pas de merge + + return t1.Tasks.Merge(t2.Tasks, include, t1.Vars) +} +``` + +--- + +### 4. Résolution lazy dans le Compiler + +**Fichier** : `compiler.go` + +```go +type Compiler struct { + Graph *ast.TaskfileGraph // NOUVEAU +} + +func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) { + if experiments.ScopedVariables.Enabled() { + return c.getVariablesLazy(t, call, evaluateShVars) + } + // Legacy behavior... +} + +func (c *Compiler) getVariablesLazy(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) { + result := env.GetEnviron() + + // 1. Special vars + specialVars, _ := c.getSpecialVars(t, call) + + // 2. Root taskfile vars (depuis le DAG) + rootVertex := c.Graph.Root() + for k, v := range rootVertex.Taskfile.Vars.All() { + result.Set(k, v) + } + + // 3. Include chain vars (traverser le DAG jusqu'à cette task) + if t != nil && t.Namespace != "" { + includeVars := c.resolveIncludeChain(t.Namespace) + for k, v := range includeVars.All() { + result.Set(k, v) + } + } + + // 4. Task vars + if t != nil { + for k, v := range t.Vars.All() { + result.Set(k, v) + } + } + + return result, nil +} + +func (c *Compiler) resolveIncludeChain(namespace string) *ast.Vars { + // namespace = "a:b:taskname" → trouver les vars de a, puis b + // Traverser le DAG en suivant les edges +} +``` + +--- + +### 5. Helper pour trouver un vertex par namespace + +**Fichier** : `taskfile/ast/graph.go` + +```go +func (tfg *TaskfileGraph) GetVertexByNamespace(namespace string) (*TaskfileVertex, error) { + // Trouver le vertex correspondant au namespace +} + +func (tfg *TaskfileGraph) Root() *TaskfileVertex { + // Retourner le vertex racine +} +``` + +--- + +## Fichiers à modifier + +| Fichier | Changement | +|---------|------------| +| `experiments/experiments.go` | Ajouter `ScopedVariables` | +| `executor.go` | Ajouter `Graph` | +| `setup.go` | Stocker le DAG | +| `taskfile/ast/taskfile.go` | Conditionner le merge | +| `taskfile/ast/graph.go` | Helpers pour accès au DAG | +| `compiler.go` | `getVariablesLazy()` | + +--- + +## Tests + +1. **Héritage** : Include voit les vars du parent +2. **Override** : Include peut override une var du parent +3. **Isolation** : Parent ne voit pas les vars de l'include +4. **Siblings** : Un include ne voit pas les vars d'un autre +5. **Chaîne** : `root → a → b` - b voit les vars de a et root +6. **Legacy** : Sans le flag, comportement inchangé + +--- + +## Avantages + +1. **Simple** : Pas de mapping cross-namespace +2. **Performance** : Lazy, résout que le nécessaire +3. **Clair** : Héritage linéaire, facile à comprendre +4. **Safe** : Experiment flag, rollback facile + +--- + +## Inconvénient + +Le parent ne peut pas "lire" les vars d'un include. Si nécessaire, il faudrait ajouter `{{.namespace.VAR}}` plus tard (V2).