Statement

A source path indexes its dependents, not the reverse. A mutation walks only its dependency frontier. Cost is O(k) — the size of that frontier — never O(n), the size of the graph.

Definitions

refSubscribers: Record<string, Set<string>>
  // source path -> Set of dependent target paths

k = |affected dependency frontier|   (a mutation's cost)
n = |total graph size|               (irrelevant to that cost)

recomputeMode: "eager" | "lazy"      (kernel-wide setting)

Registration

me.order["="]("total", "price * quantity");

// refSubscribers["order.price"].add("order.total")
// refSubscribers["order.quantity"].add("order.total")

Refs are extracted and resolved when the derivation is declared, not discovered lazily at mutation time. Resolution rule: a dotted label is an absolute path; an undotted label resolves relative to the declaring scope.

Propagation — Eager Mode

queue ← [changed path]
seen  ← {}
while queue not empty:
  p ← queue.pop()
  for target in refSubscribers[p]:
    if target in seen: continue
    seen.add(target)
    recompute(target)
    queue.push(target)

A push-BFS over refSubscribers, not single-level fanout: order.price → order.total → order.withTax propagates in full, deduplicated against cycles. This is the mechanism an earlier draft of this page called "bubbling" — a descriptive label, not a term that appears in the kernel source itself.

Propagation — Lazy Mode

me["!"].runtime.setRecomputeMode("lazy");

write(path):
  bumpRefVersion(path)          // no walk, no recompute

read(path):
  if stale(path.refVersions):   // ensureTargetFresh — pull, recursive
    recompute(path)
  return value

Lazy mode skips the BFS entirely on write — it only bumps a version counter. Staleness is resolved on read, by walking downward through the derivation's own inputs rather than outward through subscribers. Eager and lazy are mechanistically different code paths, not the same logic on a delay.

Distinct from bumpSecretEpoch, which exists in the kernel but drives only key-derivation cache invalidation on a ~ noise-boundary reset — unrelated to recompute timing. The two are easy to conflate by name; the code keeps them fully separate.

Complexity

cost(mutation) = O(k), not O(n)

Holds when dependencies are local. If a single path has 100,000 subscribers, k = 100,000 — the mutation is genuinely expensive; the index doesn't hide that cost, it just refuses to add graph size on top of it.

Explainability

me["!"].explain("order.total")
→ {
    path, value, expr,
    derivation: { expression, inputs },
    meta: { dependsOn, lastComputedAt, k, recomputed, recomputedAt, sourcePath }
  }

meta.k and meta.sourcePath are real returned fields. There is no field literally named "last recompute wave" — that phrase, where it appears in prior drafts, is a gloss over recomputedAt and recomputed together, not a verbatim key. On why this trace matters more than an AI's self-report of its own reasoning: me.explain() — Why Did You Say That?

Secret Scopes

{
  label: "walletBalance",
  path: "wallet.balance",
  value: "●●●●",
  origin: "stealth",
  masked: true
}

When a dependency input crosses a stealth-scoped path, explain() masks the value instead of exposing or erroring. Dependency tracking still functions across the boundary — the structure of the derivation is visible, the secret value is not.