Domain: spreadsheet-style formula recalculation engine

Issue:
Since the derived-metrics dashboards moved onto the incremental engine, two kinds of wrong numbers keep coming in, and our own meters back both reports up. First, cells that pick their source dynamically go stale: a cell that reads whichever input its selector currently points at stops seeing edits to that input — it keeps serving the old cached number until some unrelated edit happens to shake it loose, so a dashboard can sit there contradicting the very inputs displayed next to it. Second, the meters bill recomputation we cannot justify: cells re-evaluate when an edit touches an input the formula does not even read anymore, and per-cell evaluation counts are billed to downstream teams, so "it recomputed but the value is still right" is itself a defect here. Both have to go: a cell must re-evaluate exactly when something it actually read in its latest evaluation changes — served values always equal to what a fresh evaluation would produce, the dependency listing telling the truth about what was read, and the meters recording only work that was genuinely required. Formulas that always read the same cells behave correctly today and must keep behaving exactly as they do.

Repository files:
content: # Present so pytest adds the repository root to sys.path, making `import src`
# resolve from the test suite.

path: conftest.py

content: # Pure standard library unless the environment image pre-bakes wheels.

path: requirements.lock

content: """Formula-cell calculation engine: parsed formulas, memoized recomputation, metered evaluations."""

from src.engine import Engine

__all__ = ["Engine"]

path: src/__init__.py

content: """Formula AST.

Nodes are immutable. ``Ref`` is a direct reference to another cell by
name; ``Call`` invokes a builtin function (see :mod:`src.functions`),
including ``INDIRECT``, whose target cell is computed at evaluation time
from its argument.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Tuple


@dataclass(frozen=True)
class Literal:
    value: object


@dataclass(frozen=True)
class Ref:
    name: str


@dataclass(frozen=True)
class Neg:
    operand: object


@dataclass(frozen=True)
class BinOp:
    op: str
    left: object
    right: object


@dataclass(frozen=True)
class Compare:
    op: str
    left: object
    right: object


@dataclass(frozen=True)
class Call:
    func: str
    args: Tuple[object, ...]

path: src/ast_nodes.py

content: """Cell state.

Two kinds of cell share one shape: *input* cells hold a value set from
outside, and *formula* cells hold a parsed formula plus a memoized value
and a dirty flag. ``refs`` is the formula's syntactic reference set
(kept for definition-time cycle checking); ``deps`` is the set of cells
whose changes invalidate this one — the engine's subscription edges hang
off it.
"""

from __future__ import annotations


class Cell:
    __slots__ = ("name", "formula", "source", "refs", "deps", "value", "dirty")

    def __init__(self, name, formula=None, source=None, refs=frozenset()):
        self.name = name
        self.formula = formula
        self.source = source
        self.refs = refs
        self.deps = None
        self.value = None
        self.dirty = formula is not None

    @property
    def is_input(self):
        return self.formula is None

    def __repr__(self):
        kind = "input" if self.is_input else "formula"
        return f"Cell({self.name!r}, {kind}, dirty={self.dirty})"

path: src/cells.py

content: """The calculation engine facade.

One object wiring the pieces together:

* ``define(name, source)`` — create a formula cell (defined once).
* ``set_input(name, value)`` — create or edit an input cell.
* ``get(name)`` — the current value, recomputing only what is dirty.
* ``dependencies(name)`` — what the cell actually read last evaluation.
* ``evaluations(name)`` / ``metrics_snapshot()`` — the billing meters.

The engine is deterministic: the same definitions, edits, and reads
produce the same values and the same meter readings, and a value read
twice without an intervening edit is served from cache, not recomputed.
"""

from __future__ import annotations

from src import introspect
from src.graph import Graph
from src.metrics import Metrics
from src.values import is_number


class Engine:
    def __init__(self):
        self._metrics = Metrics()
        self._graph = Graph(self._metrics)

    def define(self, name, source):
        self._graph.define(name, source)

    def set_input(self, name, value):
        if not (is_number(value) or isinstance(value, (bool, str))):
            raise TypeError(
                f"input values are numbers, booleans, or strings, got {value!r}"
            )
        self._graph.set_input(name, value)

    def get(self, name):
        return self._graph.get(name)

    def dependencies(self, name):
        return introspect.dependencies(self._graph, name)

    def evaluations(self, name):
        return self._metrics.evaluations(name)

    def metrics_snapshot(self):
        return self._metrics.snapshot()

path: src/engine.py

content: """Exception types for the calculation engine."""


class EngineError(Exception):
    """Base class for all engine errors."""


class ParseError(EngineError):
    """The formula source could not be tokenized or parsed."""


class NameError_(EngineError):
    """Invalid cell name at definition time."""


class RedefineError(EngineError):
    """A cell with this name already exists."""


class CycleError(EngineError):
    """The definition would create a reference cycle."""


class UnknownCellError(EngineError):
    """A formula referenced a cell that does not exist."""


class EvaluationError(EngineError):
    """A formula could not be evaluated (bad types, division by zero, ...)."""

path: src/errors.py

content: """Formula evaluation.

``evaluate(node, resolve)`` computes a formula's value, resolving every
cell read — direct ``Ref`` or ``INDIRECT`` target — through the supplied
``resolve(name)`` callable. Laziness lives in the builtins: only the
arguments a function actually needs are evaluated, so the set of cells a
formula reads can differ from one evaluation to the next.
"""

from __future__ import annotations

from src.ast_nodes import BinOp, Call, Compare, Literal, Neg, Ref
from src.errors import EvaluationError
from src.functions import FUNCTIONS
from src.values import require_number, to_text


def evaluate(node, resolve):
    if isinstance(node, Literal):
        return node.value
    if isinstance(node, Ref):
        return resolve(node.name)
    if isinstance(node, Neg):
        return -require_number(evaluate(node.operand, resolve), "negation")
    if isinstance(node, BinOp):
        return _binop(node, resolve)
    if isinstance(node, Compare):
        return _compare(node, resolve)
    if isinstance(node, Call):
        spec = FUNCTIONS[node.func]
        return spec.impl(
            node.args, lambda n: evaluate(n, resolve), resolve
        )
    raise EvaluationError(f"unknown AST node {node!r}")


def _binop(node, resolve):
    left = evaluate(node.left, resolve)
    right = evaluate(node.right, resolve)
    if node.op == "&":
        return to_text(left) + to_text(right)
    left = require_number(left, f"operator {node.op}")
    right = require_number(right, f"operator {node.op}")
    if node.op == "+":
        return left + right
    if node.op == "-":
        return left - right
    if node.op == "*":
        return left * right
    if node.op == "/":
        if right == 0:
            raise EvaluationError("division by zero")
        return left / right
    raise EvaluationError(f"unknown operator {node.op!r}")


def _compare(node, resolve):
    left = evaluate(node.left, resolve)
    right = evaluate(node.right, resolve)
    if node.op in ("==", "!="):
        if type(left) is not type(right):
            equal = False
        else:
            equal = left == right
        return equal if node.op == "==" else not equal
    left = require_number(left, f"comparison {node.op}")
    right = require_number(right, f"comparison {node.op}")
    if node.op == "<":
        return left < right
    if node.op == "<=":
        return left <= right
    if node.op == ">":
        return left > right
    return left >= right

path: src/evaluate.py

content: """Builtin formula functions.

Implementations receive their argument ASTs unevaluated together with an
evaluator callback, because several builtins are deliberately lazy:
``IF`` evaluates only the branch it takes, and ``AND``/``OR``
short-circuit left to right. What a formula reads can therefore depend
on the values it encounters, not just on its text.

``INDIRECT(name)`` reads the cell whose *name* is computed at evaluation
time — the argument produces a string, and the referenced cell is
resolved through the same lookup a direct reference uses.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Optional

from src.errors import EvaluationError
from src.values import require_number, truthy


@dataclass(frozen=True)
class FunctionSpec:
    name: str
    min_args: int
    max_args: Optional[int]  # None means unbounded
    impl: Callable

    def arity_ok(self, n):
        if n < self.min_args:
            return False
        return self.max_args is None or n <= self.max_args

    def arity_text(self):
        if self.max_args is None:
            return f"at least {self.min_args} argument(s)"
        if self.min_args == self.max_args:
            return f"exactly {self.min_args} argument(s)"
        return f"{self.min_args}-{self.max_args} arguments"


def _if(args, ev, resolve):
    condition = truthy(ev(args[0]), "IF condition")
    return ev(args[1]) if condition else ev(args[2])


def _and(args, ev, resolve):
    for arg in args:
        if not truthy(ev(arg), "AND argument"):
            return False
    return True


def _or(args, ev, resolve):
    for arg in args:
        if truthy(ev(arg), "OR argument"):
            return True
    return False


def _not(args, ev, resolve):
    return not truthy(ev(args[0]), "NOT argument")


def _min(args, ev, resolve):
    return min(require_number(ev(a), "MIN") for a in args)


def _max(args, ev, resolve):
    return max(require_number(ev(a), "MAX") for a in args)


def _abs(args, ev, resolve):
    return abs(require_number(ev(args[0]), "ABS"))


def _round(args, ev, resolve):
    value = require_number(ev(args[0]), "ROUND")
    digits = 0
    if len(args) == 2:
        digits = require_number(ev(args[1]), "ROUND digits")
        if int(digits) != digits:
            raise EvaluationError("ROUND digits must be an integer")
    return round(value, int(digits))


def _indirect(args, ev, resolve):
    name = ev(args[0])
    if not isinstance(name, str) or not name:
        raise EvaluationError(
            f"INDIRECT needs a non-empty cell-name string, got {name!r}"
        )
    return resolve(name)


FUNCTIONS = {
    spec.name: spec
    for spec in (
        FunctionSpec("IF", 3, 3, _if),
        FunctionSpec("AND", 2, None, _and),
        FunctionSpec("OR", 2, None, _or),
        FunctionSpec("NOT", 1, 1, _not),
        FunctionSpec("MIN", 1, None, _min),
        FunctionSpec("MAX", 1, None, _max),
        FunctionSpec("ABS", 1, 1, _abs),
        FunctionSpec("ROUND", 1, 2, _round),
        FunctionSpec("INDIRECT", 1, 1, _indirect),
    )
}

path: src/functions.py

content: """The dependency graph: definitions, edits, and memoized recomputation.

The engine is pull-based with push invalidation: reading a cell computes
it only if it is marked dirty, and editing an input marks exactly the
subscribed dependents dirty (transitively). Recomputation must stay
minimal and honest — a cell re-evaluates only when something it actually
read last time has changed, and a served value must always equal what a
fresh evaluation would produce. Per-cell evaluation counts are recorded
in :mod:`src.metrics` and are billed downstream, so unnecessary
recomputes are not merely wasteful, they are wrong numbers.
"""

from __future__ import annotations

import re
from collections import defaultdict

from src.cells import Cell
from src.errors import (
    CycleError,
    NameError_,
    RedefineError,
    UnknownCellError,
)
from src.evaluate import evaluate
from src.invalidate import mark_dependents_dirty
from src.parser import parse
from src.refs import static_refs

_NAME = re.compile(r"^[a-z_][a-z0-9_]*$")


class Graph:
    def __init__(self, metrics):
        self._cells = {}
        self._subscribers = defaultdict(set)
        self._metrics = metrics

    def define(self, name, source):
        """Create a formula cell. Names are defined once."""
        self._check_name(name)
        ast = parse(source)
        refs = static_refs(ast)
        self._check_cycle(name, refs)
        cell = Cell(name, formula=ast, source=source, refs=refs)
        cell.deps = frozenset(refs)
        self._cells[name] = cell
        for dep in cell.deps:
            self._subscribers[dep].add(name)

    def set_input(self, name, value):
        """Create or update an input cell and invalidate its dependents."""
        existing = self._cells.get(name)
        if existing is not None and not existing.is_input:
            raise RedefineError(f"{name!r} is a formula cell, not an input")
        if existing is None:
            self._check_name(name)
            existing = Cell(name)
            self._cells[name] = existing
        existing.value = value
        self._metrics.record_set(name)
        mark_dependents_dirty(name, self._subscribers, self._cells)

    def get(self, name):
        """The cell's current value, recomputing only what is dirty."""
        return self._resolve(name)

    def cell(self, name):
        cell = self._cells.get(name)
        if cell is None:
            raise UnknownCellError(f"no cell named {name!r}")
        return cell

    def _resolve(self, name):
        cell = self.cell(name)
        if cell.is_input:
            return cell.value
        if cell.dirty:
            self._compute(cell)
        return cell.value

    def _compute(self, cell):
        value = evaluate(cell.formula, self._resolve)
        self._metrics.record_evaluation(cell.name)
        cell.value = value
        cell.dirty = False

    def _check_name(self, name):
        if not isinstance(name, str) or not _NAME.match(name):
            raise NameError_(
                f"cell names are lower_snake_case identifiers, got {name!r}"
            )
        if name in self._cells:
            raise RedefineError(f"cell {name!r} already exists")

    def _check_cycle(self, name, refs):
        """Reject definitions whose syntactic references reach back to
        the new cell through existing formula cells."""
        stack = list(refs)
        seen = set()
        while stack:
            current = stack.pop()
            if current == name:
                raise CycleError(f"defining {name!r} would create a cycle")
            if current in seen:
                continue
            seen.add(current)
            existing = self._cells.get(current)
            if existing is not None and not existing.is_input:
                stack.extend(existing.refs)

path: src/graph.py

content: """Dependency introspection.

``dependencies(graph, name)`` is the authoritative statement of what can
affect a cell's next value: exactly the cells its formula read in its
most recent evaluation — no more, no less. Ops tooling uses it to answer
"why did this recompute?" and "what do I invalidate if I edit X?", so it
must reflect actual reads, not guesses. A formula cell that has never
been evaluated reports an empty set; input cells always report an empty
set.
"""

from __future__ import annotations


def dependencies(graph, name):
    cell = graph.cell(name)
    if cell.is_input or cell.deps is None:
        return frozenset()
    return frozenset(cell.deps)

path: src/introspect.py

content: """Push invalidation.

Editing an input marks its subscribed dependents dirty, transitively:
anything whose next read could be affected must recompute, and nothing
else. Cells already dirty are not re-walked (their dependents were
marked when they first went dirty).
"""

from __future__ import annotations


def mark_dependents_dirty(name, subscribers, cells):
    stack = [name]
    while stack:
        current = stack.pop()
        for dependent_name in subscribers.get(current, ()):
            dependent = cells.get(dependent_name)
            if dependent is None or dependent.is_input or dependent.dirty:
                continue
            dependent.dirty = True
            stack.append(dependent_name)

path: src/invalidate.py

content: """Evaluation metering.

Per-cell evaluation counts are the engine's operational record: they are
exported to the capacity dashboards and billed to downstream teams per
formula evaluation, so they must record exactly what ran, when it ran.
A served cache hit is not an evaluation; an evaluation that only
happened because bookkeeping was sloppy is still billed — which is why
recomputation has to be minimal, not merely correct.
"""

from __future__ import annotations

from collections import defaultdict


class Metrics:
    def __init__(self):
        self._evaluations = defaultdict(int)
        self._sets = 0

    def record_evaluation(self, name):
        self._evaluations[name] += 1

    def record_set(self, name):
        self._sets += 1

    def evaluations(self, name):
        """How many times this cell's formula has actually run."""
        return self._evaluations[name]

    def evaluations_total(self):
        return sum(self._evaluations.values())

    def sets_applied(self):
        return self._sets

    def snapshot(self):
        return {
            "evaluations": dict(self._evaluations),
            "evaluations_total": self.evaluations_total(),
            "sets_applied": self._sets,
        }

path: src/metrics.py

content: """Recursive-descent parser for the formula language.

Grammar (loosest binding first):

    compare  :=  concat (('<'|'<='|'>'|'>='|'=='|'!=') concat)?
    concat   :=  additive ('&' additive)*
    additive :=  mult (('+'|'-') mult)*
    mult     :=  unary (('*'|'/') unary)*
    unary    :=  '-' unary | primary
    primary  :=  NUMBER | STRING | FUNC '(' args ')' | IDENT | '(' compare ')'

Upper-case identifiers are function calls and must name a builtin;
everything else is a cell reference.
"""

from __future__ import annotations

from src.ast_nodes import BinOp, Call, Compare, Literal, Neg, Ref
from src.errors import ParseError
from src.functions import FUNCTIONS
from src.tokens import tokenize


class _Parser:
    def __init__(self, tokens):
        self._tokens = tokens
        self._pos = 0

    def _peek(self):
        return self._tokens[self._pos]

    def _next(self):
        token = self._tokens[self._pos]
        self._pos += 1
        return token

    def _expect_op(self, op):
        token = self._next()
        if token.kind != "OP" or token.value != op:
            raise ParseError(f"expected {op!r} at position {token.pos}")

    def parse(self):
        node = self._compare()
        tail = self._next()
        if tail.kind != "EOF":
            raise ParseError(f"unexpected trailing input at position {tail.pos}")
        return node

    def _compare(self):
        left = self._concat()
        token = self._peek()
        if token.kind == "OP" and token.value in ("<", "<=", ">", ">=", "==", "!="):
            self._next()
            right = self._concat()
            return Compare(token.value, left, right)
        return left

    def _concat(self):
        node = self._additive()
        while self._peek().kind == "OP" and self._peek().value == "&":
            self._next()
            node = BinOp("&", node, self._additive())
        return node

    def _additive(self):
        node = self._mult()
        while self._peek().kind == "OP" and self._peek().value in ("+", "-"):
            op = self._next().value
            node = BinOp(op, node, self._mult())
        return node

    def _mult(self):
        node = self._unary()
        while self._peek().kind == "OP" and self._peek().value in ("*", "/"):
            op = self._next().value
            node = BinOp(op, node, self._unary())
        return node

    def _unary(self):
        token = self._peek()
        if token.kind == "OP" and token.value == "-":
            self._next()
            return Neg(self._unary())
        return self._primary()

    def _primary(self):
        token = self._next()
        if token.kind == "NUMBER" or token.kind == "STRING":
            return Literal(token.value)
        if token.kind == "FUNC":
            if token.value not in FUNCTIONS:
                raise ParseError(f"unknown function {token.value!r}")
            self._expect_op("(")
            args = []
            if not (self._peek().kind == "OP" and self._peek().value == ")"):
                args.append(self._compare())
                while self._peek().kind == "OP" and self._peek().value == ",":
                    self._next()
                    args.append(self._compare())
            self._expect_op(")")
            spec = FUNCTIONS[token.value]
            if not spec.arity_ok(len(args)):
                raise ParseError(
                    f"{token.value} takes {spec.arity_text()}, got {len(args)}"
                )
            return Call(token.value, tuple(args))
        if token.kind == "IDENT":
            return Ref(token.value)
        if token.kind == "OP" and token.value == "(":
            node = self._compare()
            self._expect_op(")")
            return node
        raise ParseError(f"unexpected token at position {token.pos}")


def parse(source):
    """Parse formula source text into an AST."""
    return _Parser(tokenize(source)).parse()

path: src/parser.py

content: """Syntactic reference extraction.

``static_refs`` walks a formula AST and collects every cell name that
appears as a direct reference — a purely *syntactic* over-approximation
used for definition-time cycle checking: a reference inside a branch
that is never taken still participates in cycle detection, because an
untaken cycle is still a definition error. Note what this walk cannot
see: it reports what the formula text mentions, which is not the same
thing as what an evaluation actually reads — lazy builtins skip
arguments, and an ``INDIRECT`` target exists only at evaluation time.
"""

from __future__ import annotations

from src.ast_nodes import BinOp, Call, Compare, Neg, Ref


def static_refs(node):
    """Every cell name appearing as a direct reference in the formula."""
    found = set()
    _walk(node, found)
    return frozenset(found)


def _walk(node, found):
    if isinstance(node, Ref):
        found.add(node.name)
    elif isinstance(node, Neg):
        _walk(node.operand, found)
    elif isinstance(node, (BinOp, Compare)):
        _walk(node.left, found)
        _walk(node.right, found)
    elif isinstance(node, Call):
        for arg in node.args:
            _walk(arg, found)

path: src/refs.py

content: """Plain-text reporting for dashboards and audits."""

from __future__ import annotations

from src.values import to_text


def format_cells(engine, names):
    """One line per cell: ``name = value`` in the order given."""
    lines = []
    for name in names:
        lines.append(f"{name} = {to_text(engine.get(name))}")
    return "\n".join(lines)


def format_metrics(engine):
    """Deterministic meter dump: per-cell evaluations, then totals."""
    snapshot = engine.metrics_snapshot()
    lines = []
    for name in sorted(snapshot["evaluations"]):
        lines.append(f"evaluations[{name}]={snapshot['evaluations'][name]}")
    lines.append(f"evaluations_total={snapshot['evaluations_total']}")
    lines.append(f"sets_applied={snapshot['sets_applied']}")
    return "\n".join(lines)

path: src/report.py

content: """Tokenizer for the formula language.

Token kinds: NUMBER, STRING, IDENT, FUNC (upper-case identifier), OP
(one of ``+ - * / & ( ) , < <= > >= == !=``), and EOF. Strings are
double-quoted with no escape sequences.
"""

from __future__ import annotations

from dataclasses import dataclass

from src.errors import ParseError

_TWO_CHAR_OPS = ("<=", ">=", "==", "!=")
_ONE_CHAR_OPS = "+-*/&(),<>"


@dataclass(frozen=True)
class Token:
    kind: str
    value: object
    pos: int


def tokenize(source):
    if not isinstance(source, str) or not source.strip():
        raise ParseError("formula source must be a non-empty string")
    tokens = []
    i, n = 0, len(source)
    while i < n:
        ch = source[i]
        if ch.isspace():
            i += 1
            continue
        if source[i:i + 2] in _TWO_CHAR_OPS:
            tokens.append(Token("OP", source[i:i + 2], i))
            i += 2
            continue
        if ch in _ONE_CHAR_OPS:
            tokens.append(Token("OP", ch, i))
            i += 1
            continue
        if ch == '"':
            end = source.find('"', i + 1)
            if end < 0:
                raise ParseError(f"unterminated string at position {i}")
            tokens.append(Token("STRING", source[i + 1:end], i))
            i = end + 1
            continue
        if ch.isdigit() or (ch == "." and i + 1 < n and source[i + 1].isdigit()):
            j = i
            seen_dot = False
            while j < n and (source[j].isdigit() or (source[j] == "." and not seen_dot)):
                if source[j] == ".":
                    seen_dot = True
                j += 1
            text = source[i:j]
            tokens.append(Token("NUMBER", float(text) if seen_dot else int(text), i))
            i = j
            continue
        if ch.isalpha() or ch == "_":
            j = i
            while j < n and (source[j].isalnum() or source[j] == "_"):
                j += 1
            word = source[i:j]
            if word == "true":
                tokens.append(Token("NUMBER", True, i))
            elif word == "false":
                tokens.append(Token("NUMBER", False, i))
            elif word.isupper():
                tokens.append(Token("FUNC", word, i))
            else:
                tokens.append(Token("IDENT", word, i))
            i = j
            continue
        raise ParseError(f"unexpected character {ch!r} at position {i}")
    tokens.append(Token("EOF", None, n))
    return tokens

path: src/tokens.py

content: """Value semantics: numbers, booleans, strings.

Arithmetic and ordering comparisons require numbers; equality works
within any one type; ``&`` concatenates the text form of any value.
Truthiness is defined for booleans and numbers only — a string has no
truth value.
"""

from __future__ import annotations

from src.errors import EvaluationError


def is_number(value):
    return isinstance(value, (int, float)) and not isinstance(value, bool)


def require_number(value, context):
    if isinstance(value, bool) or not is_number(value):
        raise EvaluationError(f"{context} requires a number, got {value!r}")
    return value


def truthy(value, context="condition"):
    if isinstance(value, bool):
        return value
    if is_number(value):
        return value != 0
    raise EvaluationError(f"{context} has no truth value: {value!r}")


def to_text(value):
    """Text form used by ``&``: integral floats render without '.0'."""
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, float) and value.is_integer():
        return str(int(value))
    return str(value)

path: src/values.py

content: from src import Engine, report


def engine():
    e = Engine()
    e.set_input("x", 2)
    e.set_input("y", 3)
    e.define("total", "x + y")
    e.define("scaled", "total * 10")
    return e


class TestDependencies:
    def test_static_formula_reports_its_reads(self):
        e = engine()
        e.get("scaled")
        assert e.dependencies("total") == frozenset({"x", "y"})
        assert e.dependencies("scaled") == frozenset({"total"})

    def test_input_cells_have_no_dependencies(self):
        e = engine()
        e.get("total")
        assert e.dependencies("x") == frozenset()


class TestReport:
    def test_format_cells(self):
        e = engine()
        text = report.format_cells(e, ["x", "total", "scaled"])
        assert text == "x = 2\ntotal = 5\nscaled = 50"

    def test_format_metrics(self):
        e = engine()
        e.get("scaled")
        e.set_input("x", 4)
        e.get("scaled")
        assert report.format_metrics(e).splitlines() == [
            "evaluations[scaled]=2",
            "evaluations[total]=2",
            "evaluations_total=4",
            "sets_applied=3",
        ]

path: tests/public/test_deps_report.py

content: import pytest

from src import Engine
from src.errors import (
    CycleError,
    NameError_,
    RedefineError,
    UnknownCellError,
)


class TestDefinitions:
    def test_inputs_and_formulas(self):
        e = Engine()
        e.set_input("x", 4)
        e.define("double", "x * 2")
        assert e.get("x") == 4
        assert e.get("double") == 8

    def test_edits_propagate_through_chains(self):
        e = Engine()
        e.set_input("x", 1)
        e.define("a", "x + 1")
        e.define("b", "a * 10")
        assert e.get("b") == 20
        e.set_input("x", 5)
        assert e.get("b") == 60

    def test_define_before_input_exists(self):
        e = Engine()
        e.define("later", "x + 1")
        e.set_input("x", 2)
        assert e.get("later") == 3

    def test_names_are_defined_once(self):
        e = Engine()
        e.define("a", "1 + 1")
        with pytest.raises(RedefineError):
            e.define("a", "2 + 2")
        e.set_input("i", 1)
        with pytest.raises(RedefineError):
            e.define("i", "3")
        with pytest.raises(RedefineError):
            e.set_input("a", 5)

    def test_name_and_value_validation(self):
        e = Engine()
        with pytest.raises(NameError_):
            e.define("Bad-Name", "1")
        with pytest.raises(NameError_):
            e.set_input("UPPER", 1)
        with pytest.raises(TypeError):
            e.set_input("x", [1, 2])

    def test_unknown_cell(self):
        e = Engine()
        with pytest.raises(UnknownCellError):
            e.get("ghost")


class TestCycles:
    def test_direct_cycle(self):
        e = Engine()
        with pytest.raises(CycleError):
            e.define("a", "a + 1")

    def test_transitive_cycle(self):
        e = Engine()
        e.define("a", "b + 1")
        with pytest.raises(CycleError):
            e.define("b", "a * 2")

    def test_cycle_through_an_untaken_branch_still_counts(self):
        e = Engine()
        e.set_input("flag", True)
        e.define("a", "IF(flag, 1, b)")
        with pytest.raises(CycleError):
            e.define("b", "a + 1")

path: tests/public/test_graph_basics.py

content: import pytest

from src import Engine
from src.errors import EvaluationError, ParseError


def calc(source, **inputs):
    engine = Engine()
    for name, value in inputs.items():
        engine.set_input(name, value)
    engine.define("out", source)
    return engine.get("out")


class TestParsing:
    def test_precedence(self):
        assert calc("1 + 2 * 3") == 7
        assert calc("(1 + 2) * 3") == 9
        assert calc("-2 * 3") == -6

    def test_parse_errors(self):
        for bad in ("1 +", "(1", 'UNKNOWNFN(1)', '"open', "1 $ 2", ""):
            with pytest.raises(ParseError):
                calc(bad)

    def test_arity_checked_at_parse_time(self):
        with pytest.raises(ParseError):
            calc("IF(1, 2)")
        with pytest.raises(ParseError):
            calc("NOT(1, 2)")


class TestValues:
    def test_concat_renders_text(self):
        assert calc('"rate_" & tier', tier="basic") == "rate_basic"
        assert calc('"n=" & (4 / 2)') == "n=2"
        assert calc('"flag=" & (2 > 1)') == "flag=true"

    def test_comparisons(self):
        assert calc("3 >= 3") is True
        assert calc("2 != 3") is True
        assert calc('"a" == "a"') is True
        assert calc('"a" == 1') is False

    def test_ordering_requires_numbers(self):
        with pytest.raises(EvaluationError):
            calc('"a" < "b"')

    def test_division_by_zero(self):
        with pytest.raises(EvaluationError):
            calc("1 / (2 - 2)")

    def test_string_has_no_truth_value(self):
        with pytest.raises(EvaluationError):
            calc('IF("yes", 1, 2)')


class TestBuiltins:
    def test_numeric_builtins(self):
        assert calc("MIN(3, 1, 2)") == 1
        assert calc("MAX(3, 1, 2)") == 3
        assert calc("ABS(2 - 5)") == 3
        assert calc("ROUND(2.567, 2)") == 2.57
        assert calc("ROUND(2.5)") == 2

    def test_conditionals_and_boolean_builtins(self):
        assert calc("IF(flag, hi, lo)", flag=True, hi=10, lo=20) == 10
        assert calc("AND(2 > 1, 3 > 2)") is True
        assert calc("OR(1 > 2, 2 > 3)") is False
        assert calc("NOT(1 > 2)") is True

    def test_indirect_reads_computed_name(self):
        assert calc('INDIRECT("rate_" & tier)', tier="a", rate_a=7) == 7
        with pytest.raises(EvaluationError):
            calc("INDIRECT(5)")

path: tests/public/test_language.py

content: from src import Engine


def chain():
    e = Engine()
    e.set_input("x", 1)
    e.define("a", "x + 1")
    e.define("b", "a * 10")
    return e


class TestMemoization:
    def test_repeat_reads_are_cache_hits(self):
        e = chain()
        assert e.get("b") == 20
        assert e.get("b") == 20
        assert e.evaluations("a") == 1
        assert e.evaluations("b") == 1

    def test_edit_recomputes_the_chain_once(self):
        e = chain()
        e.get("b")
        e.set_input("x", 2)
        assert e.get("b") == 30
        assert e.evaluations("a") == 2
        assert e.evaluations("b") == 2

    def test_unrelated_edit_recomputes_nothing(self):
        e = chain()
        e.set_input("y", 100)
        e.define("other", "y * 2")
        e.get("b")
        e.get("other")
        e.set_input("y", 200)
        assert e.get("b") == 20
        assert e.evaluations("a") == 1
        assert e.evaluations("b") == 1
        assert e.get("other") == 400
        assert e.evaluations("other") == 2

    def test_diamond_computes_shared_child_once(self):
        e = Engine()
        e.set_input("base", 3)
        e.define("left", "base * 2")
        e.define("right", "base + 1")
        e.define("top", "left + right")
        assert e.get("top") == 10
        assert e.evaluations("left") == 1
        assert e.evaluations("right") == 1
        assert e.evaluations("top") == 1
        e.set_input("base", 5)
        assert e.get("top") == 16
        assert e.evaluations("left") == 2
        assert e.evaluations("right") == 2
        assert e.evaluations("top") == 2

    def test_taken_branch_edit_recomputes(self):
        e = Engine()
        e.set_input("flag", True)
        e.set_input("hi", 10)
        e.set_input("lo", 20)
        e.define("pick", "IF(flag, hi, lo)")
        assert e.get("pick") == 10
        e.set_input("hi", 11)
        assert e.get("pick") == 11
        assert e.evaluations("pick") == 2

    def test_totals_and_sets(self):
        e = chain()
        e.get("b")
        e.set_input("x", 9)
        e.get("b")
        snapshot = e.metrics_snapshot()
        assert snapshot["evaluations_total"] == 4
        assert snapshot["sets_applied"] == 2

path: tests/public/test_memo_counters.py

Reference patch:
--- a/src/graph.py
+++ b/src/graph.py
@@ -43,10 +43,7 @@ class Graph:
         refs = static_refs(ast)
         self._check_cycle(name, refs)
         cell = Cell(name, formula=ast, source=source, refs=refs)
-        cell.deps = frozenset(refs)
         self._cells[name] = cell
-        for dep in cell.deps:
-            self._subscribers[dep].add(name)
 
     def set_input(self, name, value):
         """Create or update an input cell and invalidate its dependents."""
@@ -80,11 +77,27 @@ class Graph:
         return cell.value
 
     def _compute(self, cell):
-        value = evaluate(cell.formula, self._resolve)
+        reads = set()
+
+        def resolve_and_record(name):
+            reads.add(name)
+            return self._resolve(name)
+
+        value = evaluate(cell.formula, resolve_and_record)
         self._metrics.record_evaluation(cell.name)
+        self._rewire(cell, frozenset(reads))
         cell.value = value
         cell.dirty = False
 
+    def _rewire(self, cell, reads):
+        """Point the subscription edges at what this evaluation read."""
+        old = cell.deps if cell.deps is not None else frozenset()
+        for name in old - reads:
+            self._subscribers[name].discard(cell.name)
+        for name in reads - old:
+            self._subscribers[name].add(cell.name)
+        cell.deps = reads
+
     def _check_name(self, name):
         if not isinstance(name, str) or not _NAME.match(name):
             raise NameError_(


Hidden acceptance tests (diff):
diff --git a/tests/hidden/test_hidden.py b/tests/hidden/test_hidden.py
new file mode 100644
--- /dev/null
+++ b/tests/hidden/test_hidden.py
@@ -0,0 +1,225 @@
+"""Hidden FAIL_TO_PASS suite: dependencies are what a formula actually
+READ in its latest evaluation, not what its text mentions. Never
+agent-visible (DEC-015).
+
+Every test exercises a dynamic read set (INDIRECT targets, lazy IF /
+AND short-circuit), so all twelve fail on the buggy fixture
+(syntactic define-time subscription). Tests are fused: served values,
+the dependencies() contract, and exact per-cell evaluation meters are
+asserted together, so partial trackers — add-only edge sets, static
+augmentation, traces that miss cache-hit reads — score materially
+below a complete fix.
+"""
+
+from src import Engine, report
+
+
+def indirect_engine():
+    e = Engine()
+    e.set_input("region", "east")
+    e.set_input("src_east", 10)
+    e.set_input("src_west", 20)
+    e.define("price", 'INDIRECT("src_" & region)')
+    return e
+
+
+def test_dynamic_source_edits_propagate():
+    e = indirect_engine()
+    assert e.get("price") == 10
+    e.set_input("src_east", 11)
+    assert e.get("price") == 11
+    assert e.evaluations("price") == 2
+    assert e.dependencies("price") == frozenset({"region", "src_east"})
+
+
+def test_retarget_tracks_the_new_source():
+    e = indirect_engine()
+    assert e.get("price") == 10
+    e.set_input("region", "west")
+    assert e.get("price") == 20
+    e.set_input("src_west", 25)
+    assert e.get("price") == 25
+    assert e.evaluations("price") == 3
+    assert e.dependencies("price") == frozenset({"region", "src_west"})
+    e.set_input("src_east", 99)
+    assert e.get("price") == 25
+    assert e.evaluations("price") == 3
+
+
+def test_pointer_chain():
+    e = Engine()
+    e.set_input("region", "east")
+    e.set_input("src_east", 10)
+    e.set_input("src_west", 20)
+    e.define("pointer", '"src_" & region')
+    e.define("price", "INDIRECT(pointer)")
+    assert e.get("price") == 10
+    e.set_input("src_east", 12)
+    assert e.get("price") == 12
+    assert e.evaluations("price") == 2
+    assert e.evaluations("pointer") == 1
+    assert e.dependencies("price") == frozenset({"pointer", "src_east"})
+
+
+def fee_engine():
+    e = Engine()
+    e.set_input("vip", True)
+    e.set_input("base", 100)
+    e.set_input("rate_high", 2)
+    e.set_input("rate_low", 1)
+    e.define("fee", "IF(vip, base*rate_high, base*rate_low)")
+    return e
+
+
+def test_untaken_branch_edits_are_free():
+    e = fee_engine()
+    assert e.get("fee") == 200
+    assert e.dependencies("fee") == frozenset({"vip", "base", "rate_high"})
+    e.set_input("rate_low", 9)
+    assert e.get("fee") == 200
+    assert e.evaluations("fee") == 1
+
+
+def test_flip_rewires_the_branch():
+    e = fee_engine()
+    assert e.get("fee") == 200
+    e.set_input("rate_low", 9)
+    e.set_input("vip", False)
+    assert e.get("fee") == 900
+    assert e.dependencies("fee") == frozenset({"vip", "base", "rate_low"})
+    e.set_input("rate_high", 7)
+    assert e.get("fee") == 900
+    assert e.evaluations("fee") == 2
+    e.set_input("rate_low", 3)
+    assert e.get("fee") == 300
+    assert e.evaluations("fee") == 3
+
+
+def test_short_circuit_reads_only_what_it_needs():
+    e = Engine()
+    e.set_input("enabled", False)
+    e.set_input("ratio", 5)
+    e.set_input("limit", 3)
+    e.define("alert", "AND(enabled, ratio > limit)")
+    assert e.get("alert") is False
+    assert e.dependencies("alert") == frozenset({"enabled"})
+    e.set_input("ratio", 99)
+    assert e.get("alert") is False
+    assert e.evaluations("alert") == 1
+    e.set_input("enabled", True)
+    assert e.get("alert") is True
+    assert e.dependencies("alert") == frozenset({"enabled", "ratio", "limit"})
+    e.set_input("ratio", 1)
+    assert e.get("alert") is False
+    assert e.evaluations("alert") == 3
+
+
+def test_cached_reads_still_create_edges():
+    e = Engine()
+    e.set_input("base", 3)
+    e.set_input("which", "a")
+    e.define("mid_a", "base * 2")
+    e.define("mid_b", "base + 1")
+    e.define("top", 'INDIRECT("mid_" & which)')
+    assert e.get("mid_a") == 6
+    assert e.get("top") == 6
+    assert e.dependencies("top") == frozenset({"which", "mid_a"})
+    e.set_input("base", 5)
+    assert e.get("top") == 10
+    assert e.evaluations("top") == 2
+    assert e.evaluations("mid_a") == 2
+
+
+def test_dynamic_cells_stay_isolated():
+    e = Engine()
+    e.set_input("r1", "x")
+    e.set_input("s_x", 1)
+    e.set_input("s_y", 2)
+    e.set_input("r2", "m")
+    e.set_input("t_m", 3)
+    e.set_input("t_n", 4)
+    e.define("p1", 'INDIRECT("s_" & r1)')
+    e.define("p2", 'INDIRECT("t_" & r2)')
+    assert e.get("p1") == 1
+    assert e.get("p2") == 3
+    e.set_input("r1", "y")
+    assert e.get("p1") == 2
+    e.set_input("s_x", 99)
+    assert e.get("p1") == 2
+    assert e.evaluations("p1") == 2
+    assert e.get("p2") == 3
+    assert e.evaluations("p2") == 1
+    assert e.dependencies("p2") == frozenset({"r2", "t_m"})
+
+
+def test_stale_chain_downstream():
+    e = indirect_engine()
+    e.set_input("qty", 4)
+    e.define("total", "price * qty")
+    assert e.get("total") == 40
+    e.set_input("src_east", 12)
+    assert e.get("total") == 48
+    assert e.evaluations("price") == 2
+    assert e.evaluations("total") == 2
+    assert e.dependencies("total") == frozenset({"price", "qty"})
+
+
+def test_dependencies_report_actual_reads():
+    e = Engine()
+    e.set_input("flag", True)
+    e.set_input("a", 1)
+    e.set_input("b", 2)
+    e.set_input("sel", "a")
+    e.define("cond", "IF(flag, a, b)")
+    e.define("ind", "INDIRECT(sel)")
+    e.define("gate", "OR(flag, a > b)")
+    e.define("plain", "a + b")
+    for name in ("cond", "ind", "gate", "plain"):
+        e.get(name)
+    assert e.dependencies("cond") == frozenset({"flag", "a"})
+    assert e.dependencies("ind") == frozenset({"sel", "a"})
+    assert e.dependencies("gate") == frozenset({"flag"})
+    assert e.dependencies("plain") == frozenset({"a", "b"})
+
+
+def test_edit_order_equivalence():
+    def run(order):
+        e = indirect_engine()
+        assert e.get("price") == 10
+        if order == "retarget_first":
+            e.set_input("region", "west")
+            e.get("price")
+            e.set_input("src_west", 25)
+        else:
+            e.set_input("src_west", 25)
+            e.get("price")
+            e.set_input("region", "west")
+        return e.get("price"), e.dependencies("price")
+
+    value_a, deps_a = run("retarget_first")
+    value_b, deps_b = run("edit_first")
+    assert value_a == value_b == 25
+    assert deps_a == deps_b == frozenset({"region", "src_west"})
+
+
+def test_report_after_retarget():
+    e = indirect_engine()
+    e.set_input("qty", 4)
+    e.set_input("discount", 5)
+    e.define("total", "price * qty")
+    e.define("net", "total - discount")
+    assert e.get("net") == 35
+    e.set_input("region", "west")
+    assert e.get("net") == 75
+    e.set_input("src_west", 30)
+    assert report.format_cells(e, ["price", "total", "net"]) == (
+        "price = 30\ntotal = 120\nnet = 115"
+    )
+    assert e.dependencies("price") == frozenset({"region", "src_west"})
+    assert report.format_metrics(e).splitlines() == [
+        "evaluations[net]=3",
+        "evaluations[price]=3",
+        "evaluations[total]=3",
+        "evaluations_total=9",
+        "sets_applied=7",
+    ]