Part of #1 — data lineage canvas. The substantive new work.
Goal
New pure analysis crates/pseudoscript-model/src/lineage.rs: Lineage::for_symbol(workspace, data_fqn) -> Lineage, mirroring how Graph::build walks the AST (graph.rs:289).
pub struct Lineage {
pub of: String,
pub flows: Vec<FlowEdge>, // from_data_fqn -> to_data_fqn, with the callable + span it occurs in
pub usages: Vec<Usage>,
}
pub struct FlowEdge { pub from: String, pub to: String, pub via_callable: String /* + span */ }
pub enum UsageRole { ReturnType, Param, Field, FromTarget, FromSource }
pub struct Usage { pub owner_fqn: String, pub role: UsageRole, pub line: u32, pub col: u32 }
Why
Today provenance edges only connect resolved node/data FQNs (graph.rs:627); a bare from source like raw makes expr_node_fqn (graph.rs:812) return None, dropping the edge. The flattened Step bodies (graph.rs:208) don't keep binding names. Binding-level analysis over the AST is required.
Flow edges (the lineage tree)
Walk every Callable body (reuse the trace_block/trace_stmt traversal shape, graph.rs:533). Maintain a per-body bindings: HashMap<name, &Expr> from each StmtKind::Assign. On ExprKind::From { ty, sources }, resolve each source to its originating data type:
- bare binding
raw → look up bindings["raw"] → resolve recursively;
- call
X.Method() → resolve X via resolve_path, take that method's return_ty (strip Result/Option/[]);
- field access
req.field → type of binding/param req (params from the enclosing Callable.params).
Emit FlowEdge { from: upstream_data_fqn, to: resolve_path(ty) }. Cap recursion depth, guard cycles.
Usages
Reuse the references engine logic (resolve::resolve_at, mirrored at crates/pseudoscript-wasm/src/lib.rs:445): scan identifier tokens that resolve to the data symbol; classify each by syntactic role. Keep it in model so it is reusable off-wasm.
Wiring
Add pub mod lineage; to crates/pseudoscript-model/src/lib.rs. Build lineage in Graph::build and expose graph.lineage(fqn) so emit stays a pure graph→scene projector.
Tests
raw = X.Fetch(); account = T from { raw } → FlowEdge { from: "..AccountRecord", to: "..AccountData" } + Usage{FromTarget} at the composition site.
- transitive:
a = T1 from {..}; b = T2 from { a } → chain.
- param, field, and cycle-guard cases.
cargo test -p pseudoscript-model.
Rust authoring: use the idiomatic-rust skill.
Part of #1 — data lineage canvas. The substantive new work.
Goal
New pure analysis
crates/pseudoscript-model/src/lineage.rs:Lineage::for_symbol(workspace, data_fqn) -> Lineage, mirroring howGraph::buildwalks the AST (graph.rs:289).Why
Today provenance edges only connect resolved node/data FQNs (
graph.rs:627); a barefromsource likerawmakesexpr_node_fqn(graph.rs:812) returnNone, dropping the edge. The flattenedStepbodies (graph.rs:208) don't keep binding names. Binding-level analysis over the AST is required.Flow edges (the lineage tree)
Walk every
Callablebody (reuse thetrace_block/trace_stmttraversal shape,graph.rs:533). Maintain a per-bodybindings: HashMap<name, &Expr>from eachStmtKind::Assign. OnExprKind::From { ty, sources }, resolve each source to its originating data type:raw→ look upbindings["raw"]→ resolve recursively;X.Method()→ resolveXviaresolve_path, take that method'sreturn_ty(stripResult/Option/[]);req.field→ type of binding/paramreq(params from the enclosingCallable.params).Emit
FlowEdge { from: upstream_data_fqn, to: resolve_path(ty) }. Cap recursion depth, guard cycles.Usages
Reuse the references engine logic (
resolve::resolve_at, mirrored atcrates/pseudoscript-wasm/src/lib.rs:445): scan identifier tokens that resolve to the data symbol; classify each by syntactic role. Keep it inmodelso it is reusable off-wasm.Wiring
Add
pub mod lineage;tocrates/pseudoscript-model/src/lib.rs. Build lineage inGraph::buildand exposegraph.lineage(fqn)so emit stays a pure graph→scene projector.Tests
raw = X.Fetch(); account = T from { raw }→FlowEdge { from: "..AccountRecord", to: "..AccountData" }+Usage{FromTarget}at the composition site.a = T1 from {..}; b = T2 from { a }→ chain.cargo test -p pseudoscript-model.Rust authoring: use the idiomatic-rust skill.