chore: add scoped variables planning documents (to be reverted)

This commit is contained in:
Valentin Maerten
2026-01-14 19:36:03 +01:00
parent 2810c267dd
commit 17257a1c31
4 changed files with 985 additions and 0 deletions

214
PLAN_SCOPED_VARIABLES.md Normal file
View File

@@ -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

View File

@@ -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`

View File

@@ -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)

View File

@@ -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).