# Notebook state


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

``` python
from tempfile import TemporaryDirectory

from fastcore.nbio import mk_cell, new_nb, write_nb
from fastcore.test import test_eq
```

------------------------------------------------------------------------

<a
href="https://github.com/MorsCerta-crypto/nbskill/blob/main/nbskill/state.py#L108"
target="_blank" style="float:right; font-size:smaller">source</a>

### notebook_state

``` python
def notebook_state(
    path
):
```

*Return evidence-backed execution state and likely downstream impact for
one notebook.*

## Evidence-backed notebook state

[`notebook_state`](https://MorsCerta-crypto.github.io/nbskill/state.html#notebook_state)
compares every code cell with the exact source hash recorded by
[`exec_nb`](https://MorsCerta-crypto.github.io/nbskill/execute.html#exec_nb).
A matching hash is `clean`, a changed hash is `dirty`, and missing
execution evidence is `unknown`. Static name dependencies propagate a
changed upstream cell as `stale` without claiming that Python is fully
reactive.

Each dependency is marked `likely`. Calls such as `globals()` and
assignment through attributes or subscripts appear under `uncertainty`,
because AST and symbol-table analysis cannot prove the complete effect
of dynamic Python.

A fresh notebook has three cells with exact execution fingerprints and
one untracked dynamic cell. The report distinguishes `clean` from
`unknown` without guessing.

``` python
state_notebook = Path("15_state.ipynb")
if not state_notebook.exists(): state_notebook = Path("nbs") / state_notebook
notebook_state(state_notebook)["counts"]
```

``` python
state_folder = TemporaryDirectory()
state_path = Path(state_folder.name) / "state.ipynb"
state_nb = new_nb([mk_cell("raw = 1"), mk_cell("clean = raw + 1"),
    mk_cell("result = clean * 2"), mk_cell('globals()["candidate"] = result')])
for state_cell in state_nb.cells[:3]: nbskill_cell_metadata(state_cell)["executed_hash"] = source_hash(cell_source(state_cell), length=None)
write_nb(state_nb, state_path)
initial_state = notebook_state(state_path)
test_eq(notebook_state(state_path)["counts"]["clean"], 3)
test_eq([cell["state"] for cell in initial_state["cells"]], ["clean", "clean", "clean", "unknown"])
test_eq(initial_state["cells"][3]["uncertainty"], ["dynamic-namespace", "in-place-mutation"])
initial_state
```

Changing only the first cell makes it `dirty`. Static name dependencies
mark the other three cells `stale`, while `stale_from` keeps the changed
root visible.

``` python
changed_nb = read_nb(state_path)
changed_nb.cells[0].source = "raw = 2"
write_nb(changed_nb, state_path)
changed_state = notebook_state(state_path)
test_eq([cell["state"] for cell in changed_state["cells"]], ["dirty", "stale", "stale", "stale"])
test_eq(changed_state["cells"][1]["stale_from"], [changed_nb.cells[0].id])
state_folder.cleanup()
changed_state
```
