--- epic: 1 story: 1.8 title: "Sheet Context" status: draft --- ## Epic 1 — Core Calculation Engine (Rust Crate) **Goal:** Build `calcpad-engine` as a standalone Rust crate that powers all platforms. This is the foundation. ### Story 1.8: Sheet Context As a CalcPad engine consumer, I want a `SheetContext` struct that holds all evaluation state including variables, line results, and dependency graphs, So that multi-line sheets are evaluated correctly and undo/redo is cheap. **Acceptance Criteria:** **Given** a new `SheetContext` is created via `SheetContext::new()` **When** it is inspected **Then** it has no lines, no variables, and an empty dependency graph **Given** a `SheetContext` with `ctx.set_line(0, "x = 10")` and `ctx.set_line(1, "x * 2")` **When** `ctx.eval()` is called **Then** it returns a `Vec` of length 2 **And** the first result is `10` and the second result is `20` **Given** a `SheetContext` where line 0 defines `x = 10` and line 2 references `x` **When** line 0 is changed to `x = 20` and `ctx.eval()` is called **Then** line 2 is re-evaluated and reflects the updated value `x = 20` **Given** a `SheetContext` with established state **When** it is cloned via `ctx.clone()` **Then** the clone is a deep copy that can be modified independently **And** the clone operation is cheap enough for undo/redo snapshots (sub-millisecond for 100-line sheets) **Given** a `SheetContext` with a dependency graph where line 3 depends on line 1 **When** only line 1 is modified **Then** the engine re-evaluates line 1 and line 3 but skips lines that are not in the dependency chain **And** unchanged, independent lines are not recomputed **Given** a `SheetContext` where a circular dependency is introduced (e.g., line 1 references line 2 and line 2 references line 1) **When** `ctx.eval()` is called **Then** both lines return `CalcResult::Error` with a circular dependency message **And** the engine does not enter an infinite loop **Given** a `SheetContext` with lines that include aggregator references such as `total` or `sum` **When** `ctx.eval()` is called **Then** the aggregators compute over the appropriate preceding lines **And** the dependency graph correctly tracks which lines feed into each aggregator