Documentation Home
ADR-009: Edge-Based Composition DAG for Multi-Directional Melding
Status
Phase 3 Complete (Composition extension + cross-axis sub-containers)
Context
Element melding in QNTX creates spatial compositions through proximity. Phase 1 implemented N-element compositions using flat arrays (glyphIds: string[]) for horizontal chains: [ax|py|prompt].
However, QNTX requires multi-directional melding beyond linear chains:
Horizontal (right): Data flow chains
ax → py → prompt (query drives script drives template)
py → py (sequential Python pipelines)
Vertical (top): Configuration injection
system-prompt ↓ prompt (system prompt modifies prompt behavior)
config ↓ element (runtime configuration)
Vertical (bottom): Result attachment
chart ↑ ax (real-time monitoring feeds back into query)
output ↑ processor (downstream results influence upstream behavior)
Flat arrays cannot represent DAG topologies. We need arbitrary graph structures with directional edges.
Decision
Migrate from flat elementIds: string[] to edge-based composition DAG structure.
Data model:
message CompositionEdge {
string from = 1; // source element ID
string to = 2; // target element ID
string direction = 3; // 'right', 'top', 'bottom'
int32 position = 4; // ordering
}
message Composition {
string id = 1;
repeated CompositionEdge edges = 2;
reserved 3; // formerly element_ids (removed for DAG-native approach)
double x = 4;
double y = 5;
}
Proto as source of truth:
Defined in element/proto/canvas.proto
Follows ADR-006 (proto as single source of truth)
Follows ADR-007 (TypeScript interfaces only)
Rationale
Why edges instead of alternatives?
Nested structures (children: Composition[]): Doesn't support multiple parents (DAG requires this)
Adjacency matrix : O(N²) space, sparse for typical compositions
Port-based connections : Over-engineered for current needs, can add later
Edge list : Flexible, SQL-friendly, supports arbitrary topologies ✓
Why breaking change instead of migration?
No production compositions exist yet (melding just added)
Clean slate simpler than dual-format compatibility
Follows Phase 1 precedent
Why remove type field?
With arbitrary DAG, computing type becomes ambiguous
Edges ARE the type information
Simplifies schema, reduces maintenance
Why remove glyph_ids field?
During Phase 1bb, removed derived glyph_ids for true DAG-native approach
Traverse edges to find elements (e.g., composition.edges.some(e => e.from === glyphId))
Proto field 3 reserved to prevent accidental reuse
Reduces duplication and maintains single source of truth (edges)
Implementation
Phase 1ba: Backend (DB + storage)
Migration 021_dag_composition_edges.sql
Drop composition_glyphs junction table
Create composition_edges table with from, to, direction, position
Update Go storage layer to use proto edges
Phase 1bb: API + Frontend state ✅
API handlers accept/return proto edges
TypeScript state uses proto Composition directly
Remove type field from all layers
Remove glyph_ids field - DAG-native edge traversal only
All 728 tests passing (352 Go + 376 TypeScript)
Phase 1bc: UI integration
Meld system creates edges with direction: 'right'
Reconstruction loads from edges
Vertical melding support deferred (structure ready)
Phase 2: Port-aware meldability + multi-directional melding ✅
MELDABILITY registry restructured: each element class maps to PortRule[] with direction + targets
Proximity detection, performMeld, reconstructMeld all respect edge direction
Edges created with actual direction ('right', 'bottom') not just 'right'
Result elements auto-meld below py on execution (bottom port)
Port-based model pulled forward from "future work" — spatial ports are concrete, not abstract
Phase 3: Composition extension + cross-axis sub-containers ✅
Drag-to-extend: standalone element melds into existing composition (append to leaf / prepend to root)
Cross-axis sub-containers: nested meld-sub-container flex divs for mixed-direction edges
Meld system split into focused modules: meld-detect.ts, meld-feedback.ts, meld-composition.ts with barrel re-export
All guards use .closest('.melded-composition') to traverse through sub-containers
3+ element chains in browser, composition persistence across page reload
Consequences
Positive
Supports arbitrary DAG topologies for future features
Single source of truth via proto (cross-language safety)
Database schema matches graph semantics (edges table)
Follows established patterns (ADR-006, ADR-007)
No technical debt from temporary solutions
DAG-native thinking: No derived fields, traverse edges directly
Proto field reservation prevents future mistakes
Negative
Breaking change requires database recreation
More complex than flat arrays for simple chains
Edge traversal requires graph algorithms (topological sort for ordering)
Neutral
Proto generation adds build step (already exists)
Go uses proto at boundaries, internal structs for DB (ADR-006 pattern)
Alternatives Considered
Keep flat arrays, add separate structure for complex cases:
Creates dual model complexity
Still need migration path eventually
Defers inevitable refactoring
Edge-based composition uses a port-based model (à la GoFlow).
Graph library (Graphology, dominikbraun/graph):
Runtime overhead for storage/retrieval
Database still needs edge table
Can use for client-side traversal algorithms
Future Work
Cycle detection validation (DAG invariant enforcement)
Topological sort for execution order
Graph visualization/debugging tools
Unmeld granularity: splitting a composition at a middle element
References