doc/concepts/declarative-pipeline.md — vollständige Design-Spezifikation:
1. Kernidee: Processing Units mit expliziten require/provide-Contracts
2. Token-Vokabular: 30 Tokens in 7 Kategorien
input, setup, Gauss-Bonnet, solver, topology, layout, period/domain/output
3. YAML-Schema: Vollständige Syntax inkl. Parameterdefaults aller Units
4. Validierungsalgorithmus: monoton wachsendes provided-Set, Pre-Execution-Check
5. 5 vollständige Beispiele:
A — Euklidische Uniformisierung Torus (τ-Ausgabe)
B — Sphärische Uniformisierung (cathead.obj)
C — Hyperbolische Uniformisierung Torus (Poincaré-Disk)
D — Volle Pipeline mit Periodenmatrix + 5×5-Kachelung
E — Absichtlich fehlerhaftes Beispiel mit Validator-Fehlermeldungen
6. C++-Mapping: alle YAML-Unit-Namen → C++-Funktionen + Header
7. Implementierungsplan (Phase 8e): pipeline.hpp + CLI-App + YAML-Abhängigkeit
8. Design-Entscheidungen: YAML vs. JSON/TOML, explizit vs. auto-inference,
linear vs. DAG, eine Geometrie pro Datei
doc/api/cgal-package.md: Link zum Konzeptdokument ergänzt.
README.md: Link in Dokumentationstabelle ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
572 lines
18 KiB
Markdown
572 lines
18 KiB
Markdown
# Declarative YAML Pipeline — Concept & Design
|
||
|
||
> **Status: Phase 8e — designed, not yet implemented.**
|
||
> This document is the authoritative design specification.
|
||
> Implementation target: `code/include/pipeline.hpp` + CLI flag `--pipeline`.
|
||
|
||
---
|
||
|
||
## 1 — Core idea
|
||
|
||
Every algorithm in conformallab++ is a **Processing Unit**: a function with
|
||
explicit preconditions (*require*) and guarantees (*provide*).
|
||
The contract table in [doc/api/contracts.md](../api/contracts.md) lists them all.
|
||
|
||
A **declarative pipeline** is a YAML file that:
|
||
1. Lists the units to execute and their parameters.
|
||
2. Annotates each step with the tokens it `require`s and `provide`s.
|
||
3. Is validated **before any code runs** — the validator walks the dependency
|
||
graph and rejects the file if any `require` token is not yet provided by
|
||
a preceding step.
|
||
|
||
The result is a **self-documenting, reproducible experiment** that can be
|
||
version-controlled, shared, and re-run identically.
|
||
|
||
---
|
||
|
||
## 2 — Token vocabulary
|
||
|
||
Tokens are short strings. Each Processing Unit consumes and produces a fixed
|
||
set of tokens. The validator treats them as a monotonically growing *provided set*.
|
||
|
||
### Input tokens (provided by the `input:` block)
|
||
| Token | Meaning |
|
||
|---|---|
|
||
| `mesh` | A loaded, triangulated, oriented `ConformalMesh` |
|
||
| `mesh_closed` | Mesh has no boundary (required for cut graph) |
|
||
| `mesh_open` | Mesh has at least one boundary component |
|
||
|
||
### Setup tokens
|
||
| Token | Produced by | Required by |
|
||
|---|---|---|
|
||
| `maps_euclidean` | `setup_euclidean_maps` | `lambda0_euclidean`, `gauss_bonnet`, `newton_euclidean` |
|
||
| `maps_spherical` | `setup_spherical_maps` | `lambda0_spherical`, `gauss_bonnet`, `newton_spherical` |
|
||
| `maps_hyper_ideal` | `setup_hyper_ideal_maps` | `lambda0_hyper_ideal`, `gauss_bonnet`, `newton_hyper_ideal` |
|
||
| `lambda0_euclidean` | `compute_euclidean_lambda0_from_mesh` | `gauss_bonnet_euclidean`, `newton_euclidean` |
|
||
| `lambda0_spherical` | `compute_spherical_lambda0_from_mesh` | `gauss_bonnet_spherical`, `newton_spherical` |
|
||
| `lambda0_hyper_ideal` | `compute_hyper_ideal_lambda0_from_mesh` | `gauss_bonnet_hyper_ideal`, `newton_hyper_ideal` |
|
||
| `dof_indices` | DOF assignment step | `newton_*` |
|
||
|
||
### Gauss–Bonnet tokens
|
||
| Token | Produced by |
|
||
|---|---|
|
||
| `gauss_bonnet_euclidean` | `check_gauss_bonnet` or `enforce_gauss_bonnet` (Euclidean maps) |
|
||
| `gauss_bonnet_spherical` | same, Spherical maps |
|
||
| `gauss_bonnet_hyper_ideal` | same, HyperIdeal maps |
|
||
|
||
### Solver tokens
|
||
| Token | Produced by |
|
||
|---|---|
|
||
| `x_euclidean` | `newton_euclidean` (converged) |
|
||
| `x_spherical` | `newton_spherical` (converged) |
|
||
| `x_hyper_ideal` | `newton_hyper_ideal` (converged) |
|
||
|
||
### Topology tokens
|
||
| Token | Produced by | Required by |
|
||
|---|---|---|
|
||
| `cut_graph` | `compute_cut_graph` | `euclidean_layout` (closed mesh), `hyper_ideal_layout` |
|
||
|
||
### Layout tokens
|
||
| Token | Produced by |
|
||
|---|---|
|
||
| `layout_euclidean` | `euclidean_layout` |
|
||
| `layout_spherical` | `spherical_layout` |
|
||
| `layout_hyper_ideal` | `hyper_ideal_layout` |
|
||
| `holonomy_euclidean` | `euclidean_layout` (with cut graph) |
|
||
| `holonomy_hyper_ideal` | `hyper_ideal_layout` (with cut graph) |
|
||
|
||
### Period / domain tokens
|
||
| Token | Produced by | Required by |
|
||
|---|---|---|
|
||
| `period_matrix` | `compute_period_matrix` | `fundamental_domain` |
|
||
| `fundamental_domain` | `compute_fundamental_domain` | `tiling` |
|
||
| `tiling` | `tiling_neighbourhood` | output steps |
|
||
|
||
### Output tokens
|
||
| Token | Produced by |
|
||
|---|---|
|
||
| `saved_layout` | `save_layout_off` |
|
||
| `saved_result` | `save_result_json` / `save_result_xml` |
|
||
|
||
---
|
||
|
||
## 3 — YAML schema
|
||
|
||
```yaml
|
||
pipeline:
|
||
name: <string> # human-readable experiment name
|
||
geometry: euclidean # euclidean | spherical | hyper_ideal
|
||
description: | # optional multi-line description
|
||
...
|
||
|
||
input:
|
||
source: <path> # mesh file (.off / .obj / .ply)
|
||
# Optional overrides:
|
||
theta_v: flat # flat (2π everywhere) | cone:<file> | custom:<file>
|
||
|
||
steps:
|
||
- id: <string> # unique step identifier
|
||
unit: <function> # C++ function name (see token table)
|
||
require: [<token>, ...] # tokens that must be in the provided set
|
||
provide: [<token>, ...] # tokens added to provided set after this step
|
||
params: # optional parameter overrides
|
||
<key>: <value>
|
||
|
||
output:
|
||
layout: <path> # optional: save Layout2D as .off with UVs
|
||
json: <path> # optional: save NewtonResult + Layout as JSON
|
||
xml: <path> # optional: save NewtonResult + Layout as XML
|
||
tau: <path> # optional: write τ (period matrix) as text
|
||
report: <path> # optional: write human-readable summary
|
||
```
|
||
|
||
### Parameter defaults by unit
|
||
|
||
| Unit | Parameter | Default |
|
||
|---|---|---|
|
||
| `newton_euclidean` | `tol` | `1e-8` |
|
||
| `newton_euclidean` | `max_iter` | `200` |
|
||
| `newton_spherical` | `tol` | `1e-8` |
|
||
| `newton_spherical` | `max_iter` | `200` |
|
||
| `newton_hyper_ideal` | `tol` | `1e-8` |
|
||
| `newton_hyper_ideal` | `max_iter` | `200` |
|
||
| `newton_hyper_ideal` | `hess_eps` | `1e-5` |
|
||
| `euclidean_layout` | `normalise` | `false` |
|
||
| `hyper_ideal_layout` | `normalise` | `false` |
|
||
| `spherical_layout` | `normalise` | `false` |
|
||
| `compute_period_matrix` | `reduce` | `true` (SL(2,ℤ)) |
|
||
| `tiling_neighbourhood` | `m` | `1` |
|
||
| `tiling_neighbourhood` | `n` | `1` |
|
||
|
||
---
|
||
|
||
## 4 — Validation algorithm
|
||
|
||
```
|
||
provided = { "mesh" } ← always available after input is loaded
|
||
|
||
if input.source has no boundary:
|
||
provided ← provided ∪ { "mesh_closed" }
|
||
else:
|
||
provided ← provided ∪ { "mesh_open" }
|
||
|
||
for each step in pipeline.steps:
|
||
for token in step.require:
|
||
if token ∉ provided:
|
||
ERROR: "Step '<id>' requires '<token>' which is not yet provided.
|
||
Add a step that provides it before step '<id>'."
|
||
provided ← provided ∪ step.provide
|
||
|
||
for each output key in pipeline.output:
|
||
check that its required token is in provided
|
||
(e.g. 'tau' requires 'period_matrix')
|
||
```
|
||
|
||
The validator runs **before any C++ code executes**. If validation passes,
|
||
the steps are executed in order. There is no parallelism — steps are sequential.
|
||
|
||
---
|
||
|
||
## 5 — Complete examples
|
||
|
||
### Example A — Euclidean uniformization of a flat torus (genus 1)
|
||
|
||
```yaml
|
||
pipeline:
|
||
name: flat_torus_euclidean
|
||
geometry: euclidean
|
||
description: |
|
||
Euclidean uniformization of the 4×4 torus of revolution.
|
||
Computes the period matrix τ and the fundamental domain parallelogram.
|
||
|
||
input:
|
||
source: code/data/off/torus_4x4.off
|
||
|
||
steps:
|
||
- id: setup
|
||
unit: setup_euclidean_maps
|
||
require: [mesh]
|
||
provide: [maps_euclidean]
|
||
|
||
- id: lambda0
|
||
unit: compute_euclidean_lambda0_from_mesh
|
||
require: [mesh, maps_euclidean]
|
||
provide: [lambda0_euclidean]
|
||
|
||
- id: dofs
|
||
unit: assign_dof_indices_euclidean
|
||
require: [maps_euclidean]
|
||
provide: [dof_indices]
|
||
params:
|
||
pin_strategy: first_vertex # pin v₀, assign sequential to rest
|
||
|
||
- id: gauss_bonnet
|
||
unit: enforce_gauss_bonnet
|
||
require: [maps_euclidean, lambda0_euclidean]
|
||
provide: [gauss_bonnet_euclidean]
|
||
|
||
- id: solve
|
||
unit: newton_euclidean
|
||
require: [gauss_bonnet_euclidean, dof_indices]
|
||
provide: [x_euclidean]
|
||
params:
|
||
tol: 1.0e-10
|
||
max_iter: 200
|
||
|
||
- id: cut
|
||
unit: compute_cut_graph
|
||
require: [mesh_closed]
|
||
provide: [cut_graph]
|
||
|
||
- id: layout
|
||
unit: euclidean_layout
|
||
require: [x_euclidean, cut_graph]
|
||
provide: [layout_euclidean, holonomy_euclidean]
|
||
params:
|
||
normalise: true
|
||
|
||
- id: period
|
||
unit: compute_period_matrix
|
||
require: [holonomy_euclidean]
|
||
provide: [period_matrix]
|
||
params:
|
||
reduce: true
|
||
|
||
- id: domain
|
||
unit: compute_fundamental_domain
|
||
require: [holonomy_euclidean]
|
||
provide: [fundamental_domain]
|
||
|
||
output:
|
||
layout: out/torus_layout.off
|
||
json: out/torus_result.json
|
||
tau: out/torus_tau.txt
|
||
```
|
||
|
||
**Expected output (`torus_tau.txt`):**
|
||
```
|
||
tau = 0.000... + 0.9...i # Re(τ) ≈ 0 (4-fold symmetry), Im(τ) ≈ 1
|
||
|tau| = 0.9... # ≥ 1 after SL(2,ℤ) reduction
|
||
```
|
||
|
||
---
|
||
|
||
### Example B — Spherical uniformization (genus 0)
|
||
|
||
```yaml
|
||
pipeline:
|
||
name: cathead_spherical
|
||
geometry: spherical
|
||
description: |
|
||
Map the open cathead mesh to the sphere.
|
||
|
||
input:
|
||
source: code/data/obj/cathead.obj
|
||
|
||
steps:
|
||
- id: setup
|
||
unit: setup_spherical_maps
|
||
require: [mesh]
|
||
provide: [maps_spherical]
|
||
|
||
- id: lambda0
|
||
unit: compute_spherical_lambda0_from_mesh
|
||
require: [mesh, maps_spherical]
|
||
provide: [lambda0_spherical]
|
||
|
||
- id: dofs
|
||
unit: assign_dof_indices_spherical
|
||
require: [maps_spherical]
|
||
provide: [dof_indices]
|
||
|
||
- id: gauss_bonnet
|
||
unit: enforce_gauss_bonnet
|
||
require: [maps_spherical, lambda0_spherical]
|
||
provide: [gauss_bonnet_spherical]
|
||
|
||
- id: solve
|
||
unit: newton_spherical
|
||
require: [gauss_bonnet_spherical, dof_indices]
|
||
provide: [x_spherical]
|
||
|
||
- id: layout
|
||
unit: spherical_layout
|
||
require: [x_spherical]
|
||
provide: [layout_spherical]
|
||
params:
|
||
normalise: true # rotate centroid to north pole
|
||
|
||
output:
|
||
json: out/cathead_spherical.json
|
||
```
|
||
|
||
---
|
||
|
||
### Example C — Hyperbolic uniformization (genus 1, Poincaré disk)
|
||
|
||
```yaml
|
||
pipeline:
|
||
name: torus_hyperbolic
|
||
geometry: hyper_ideal
|
||
description: |
|
||
Hyperbolic (hyper-ideal) uniformization of the 8×8 torus.
|
||
Lays out the mesh in the Poincaré disk with correct Möbius holonomy.
|
||
|
||
input:
|
||
source: code/data/off/torus_8x8.off
|
||
|
||
steps:
|
||
- id: setup
|
||
unit: setup_hyper_ideal_maps
|
||
require: [mesh]
|
||
provide: [maps_hyper_ideal]
|
||
|
||
- id: lambda0
|
||
unit: compute_hyper_ideal_lambda0_from_mesh
|
||
require: [mesh, maps_hyper_ideal]
|
||
provide: [lambda0_hyper_ideal]
|
||
|
||
- id: dofs
|
||
unit: assign_all_dof_indices
|
||
require: [maps_hyper_ideal]
|
||
provide: [dof_indices]
|
||
# HyperIdeal: all vertices AND edges are free DOFs — no vertex pinned.
|
||
|
||
- id: gauss_bonnet
|
||
unit: enforce_gauss_bonnet
|
||
require: [maps_hyper_ideal, lambda0_hyper_ideal]
|
||
provide: [gauss_bonnet_hyper_ideal]
|
||
|
||
- id: solve
|
||
unit: newton_hyper_ideal
|
||
require: [gauss_bonnet_hyper_ideal, dof_indices]
|
||
provide: [x_hyper_ideal]
|
||
params:
|
||
tol: 1.0e-10
|
||
|
||
- id: cut
|
||
unit: compute_cut_graph
|
||
require: [mesh_closed]
|
||
provide: [cut_graph]
|
||
|
||
- id: layout
|
||
unit: hyper_ideal_layout
|
||
require: [x_hyper_ideal, cut_graph]
|
||
provide: [layout_hyper_ideal, holonomy_hyper_ideal]
|
||
params:
|
||
normalise: true # Möbius-centre to disk origin
|
||
|
||
output:
|
||
layout: out/torus_disk.off
|
||
json: out/torus_disk.json
|
||
```
|
||
|
||
---
|
||
|
||
### Example D — Full pipeline with period matrix and tiling
|
||
|
||
```yaml
|
||
pipeline:
|
||
name: torus_full
|
||
geometry: euclidean
|
||
|
||
input:
|
||
source: code/data/off/torus_hex_6x6.off
|
||
|
||
steps:
|
||
- id: setup
|
||
unit: setup_euclidean_maps
|
||
require: [mesh]
|
||
provide: [maps_euclidean]
|
||
|
||
- id: lambda0
|
||
unit: compute_euclidean_lambda0_from_mesh
|
||
require: [mesh, maps_euclidean]
|
||
provide: [lambda0_euclidean]
|
||
|
||
- id: dofs
|
||
unit: assign_dof_indices_euclidean
|
||
require: [maps_euclidean]
|
||
provide: [dof_indices]
|
||
|
||
- id: gauss_bonnet
|
||
unit: enforce_gauss_bonnet
|
||
require: [maps_euclidean, lambda0_euclidean]
|
||
provide: [gauss_bonnet_euclidean]
|
||
|
||
- id: solve
|
||
unit: newton_euclidean
|
||
require: [gauss_bonnet_euclidean, dof_indices]
|
||
provide: [x_euclidean]
|
||
|
||
- id: cut
|
||
unit: compute_cut_graph
|
||
require: [mesh_closed]
|
||
provide: [cut_graph]
|
||
|
||
- id: layout
|
||
unit: euclidean_layout
|
||
require: [x_euclidean, cut_graph]
|
||
provide: [layout_euclidean, holonomy_euclidean]
|
||
params:
|
||
normalise: true
|
||
|
||
- id: period
|
||
unit: compute_period_matrix
|
||
require: [holonomy_euclidean]
|
||
provide: [period_matrix]
|
||
|
||
- id: domain
|
||
unit: compute_fundamental_domain
|
||
require: [holonomy_euclidean]
|
||
provide: [fundamental_domain]
|
||
|
||
- id: tiling
|
||
unit: tiling_neighbourhood
|
||
require: [layout_euclidean, holonomy_euclidean]
|
||
provide: [tiling]
|
||
params:
|
||
m: 2 # 5×5 tile grid around origin
|
||
n: 2
|
||
|
||
output:
|
||
layout: out/hex_torus_layout.off
|
||
tau: out/hex_torus_tau.txt
|
||
json: out/hex_torus_full.json
|
||
report: out/hex_torus_summary.txt
|
||
|
||
# Expected tau for 6-fold symmetric torus:
|
||
# Re(τ) ≈ 0.5, Im(τ) ≈ 0.866 (approaches e^{iπ/3} as mesh is refined)
|
||
```
|
||
|
||
---
|
||
|
||
### Example E — Validation error (intentional mistake)
|
||
|
||
```yaml
|
||
pipeline:
|
||
name: broken_example
|
||
geometry: euclidean
|
||
|
||
input:
|
||
source: code/data/off/torus_4x4.off
|
||
|
||
steps:
|
||
- id: solve # ← WRONG: skipped setup and lambda0
|
||
unit: newton_euclidean
|
||
require: [gauss_bonnet_euclidean, dof_indices]
|
||
provide: [x_euclidean]
|
||
|
||
- id: layout
|
||
unit: euclidean_layout
|
||
require: [x_euclidean, cut_graph] # ← WRONG: cut_graph never provided
|
||
provide: [layout_euclidean]
|
||
```
|
||
|
||
**Validator output:**
|
||
```
|
||
ERROR [step 'solve']: requires 'gauss_bonnet_euclidean' which is not yet provided.
|
||
Hint: add setup_euclidean_maps → compute_euclidean_lambda0_from_mesh
|
||
→ enforce_gauss_bonnet before 'solve'.
|
||
|
||
ERROR [step 'layout']: requires 'cut_graph' which is not yet provided.
|
||
Hint: add compute_cut_graph (requires: mesh_closed) before 'layout'.
|
||
|
||
Pipeline rejected. 2 contract violations.
|
||
```
|
||
|
||
---
|
||
|
||
## 6 — Mapping to C++ code
|
||
|
||
Each `unit:` name in the YAML maps directly to a C++ function:
|
||
|
||
| YAML unit | C++ function | Header |
|
||
|---|---|---|
|
||
| `setup_euclidean_maps` | `conformallab::setup_euclidean_maps()` | `euclidean_functional.hpp` |
|
||
| `compute_euclidean_lambda0_from_mesh` | `conformallab::compute_euclidean_lambda0_from_mesh()` | `euclidean_functional.hpp` |
|
||
| `enforce_gauss_bonnet` | `conformallab::enforce_gauss_bonnet()` | `gauss_bonnet.hpp` |
|
||
| `assign_dof_indices_euclidean` | manual pin + sequential loop | `euclidean_functional.hpp` |
|
||
| `assign_all_dof_indices` | `conformallab::assign_all_dof_indices()` | `hyper_ideal_functional.hpp` |
|
||
| `newton_euclidean` | `conformallab::newton_euclidean()` | `newton_solver.hpp` |
|
||
| `newton_spherical` | `conformallab::newton_spherical()` | `newton_solver.hpp` |
|
||
| `newton_hyper_ideal` | `conformallab::newton_hyper_ideal()` | `newton_solver.hpp` |
|
||
| `compute_cut_graph` | `conformallab::compute_cut_graph()` | `cut_graph.hpp` |
|
||
| `euclidean_layout` | `conformallab::euclidean_layout()` | `layout.hpp` |
|
||
| `spherical_layout` | `conformallab::spherical_layout()` | `layout.hpp` |
|
||
| `hyper_ideal_layout` | `conformallab::hyper_ideal_layout()` | `layout.hpp` |
|
||
| `compute_period_matrix` | `conformallab::compute_period_matrix()` | `period_matrix.hpp` |
|
||
| `compute_fundamental_domain` | `conformallab::compute_fundamental_domain()` | `fundamental_domain.hpp` |
|
||
| `tiling_neighbourhood` | `conformallab::tiling_neighbourhood()` | `fundamental_domain.hpp` |
|
||
| `save_layout_off` | `conformallab::save_layout_off()` | `mesh_io.hpp` |
|
||
| `save_result_json` | `conformallab::save_result_json()` | `serialization.hpp` |
|
||
|
||
---
|
||
|
||
## 7 — Implementation plan (Phase 8e)
|
||
|
||
```
|
||
code/include/pipeline.hpp ← YAML parser + validator + executor
|
||
code/apps/conformallab_pipeline.cpp ← CLI: conformallab_pipeline --pipeline foo.yml
|
||
|
||
External YAML dependency (header-only, already bundled):
|
||
code/deps/single_includes/yaml-cpp/yaml.h ← or single-include yaml.hpp
|
||
Alternative: use the bundled single_includes/nlohmann/json.hpp for a
|
||
JSON-based pipeline format (simpler, no new dependency).
|
||
```
|
||
|
||
### Validator pseudocode
|
||
|
||
```cpp
|
||
struct PipelineValidator {
|
||
std::set<std::string> provided;
|
||
|
||
void load_input(const YAML::Node& input) {
|
||
provided.insert("mesh");
|
||
auto mesh = load_mesh(input["source"].as<std::string>());
|
||
if (is_closed(mesh)) provided.insert("mesh_closed");
|
||
else provided.insert("mesh_open");
|
||
}
|
||
|
||
void validate_step(const YAML::Node& step) {
|
||
for (auto& tok : step["require"])
|
||
if (!provided.count(tok.as<std::string>()))
|
||
throw PipelineError("Step '" + step["id"].as<std::string>()
|
||
+ "' requires '" + tok.as<std::string>() + "' not yet provided.");
|
||
for (auto& tok : step["provide"])
|
||
provided.insert(tok.as<std::string>());
|
||
}
|
||
|
||
void validate(const YAML::Node& pipeline) {
|
||
load_input(pipeline["input"]);
|
||
for (auto& step : pipeline["steps"])
|
||
validate_step(step);
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 8 — Design decisions
|
||
|
||
**Why YAML and not JSON or TOML?**
|
||
YAML supports multi-line strings (for `description:`), comments (`#`), and
|
||
anchors/aliases — useful for parametric experiments. JSON lacks comments.
|
||
TOML lacks the list syntax needed for `require:` / `provide:`.
|
||
|
||
**Why explicit require/provide instead of auto-inference?**
|
||
Auto-inference would require the validator to know all function signatures
|
||
at parse time — this couples the validator tightly to the C++ code.
|
||
Explicit tokens make the contract visible in the YAML, making it
|
||
self-documenting and readable without the source code.
|
||
|
||
**Why sequential steps and not a DAG?**
|
||
The pipeline is a linear sequence for now (Phase 8e target). A DAG-based
|
||
executor (parallel steps where contracts allow) is a natural Phase 10+ extension
|
||
but adds significant complexity. Linear execution is correct and debuggable.
|
||
|
||
**Why one YAML per geometry mode?**
|
||
A single YAML could support multiple geometries with conditional blocks, but
|
||
this adds syntactic complexity. The `geometry:` field at the top locks the mode
|
||
and keeps the YAML readable. Cross-geometry experiments can chain two pipelines.
|