

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

## Reviewing notebooks

Diff and style feedback that focuses on code cells.

Review should focus on the behavior encoded in code cells, not on
notebook metadata churn. This notebook keeps that review loop small: run
fast.ai style hints when desired, and print nbdev-style code diffs when
comparing notebooks.

Review tools draw a line between source changes and notebook noise.
[`style_check`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_check)
is useful before publishing exported code because it combines fast.ai
style hints with notebook hygiene reports, while
[`diff_nb`](https://MorsCerta-crypto.github.io/nbskill/review.html#diff_nb)
is useful during agent edits because it ignores outputs and metadata
unless metadata is the only thing that changed.

### Production contract

Review tools are production core. Validation must fail on missing or
stale nbskill metadata,
[`style_check`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_check)
must combine capped style output with notebook hygiene diagnostics, and
[`diff_nb`](https://MorsCerta-crypto.github.io/nbskill/review.html#diff_nb)
must show code-cell behavior changes without raw notebook metadata
noise.

``` python
import os
from fastcore.nbio import new_nb
from fastcore.test import test_eq
from nbskill.foundation import demo_path, remove_demo_path, write_demo_notebook, write_tool_notebook


def _write_review_notebook(path, cells, stamp=True):
    nb = new_nb(cells)
    if stamp: stamp_notebook_metadata(nb)
    _write_nb(nb, path)
    return nb
```

### Style feedback as a tool

[`style_check`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_check)
wraps the fast.ai style checker as a direct Python API. Combine it with
`doctor` when a notebook change needs broader diagnostics.

Public notebook APIs stay maintainable when their docs, implementation,
examples, and tests travel together.
[`public_function_literacy_problems`](https://MorsCerta-crypto.github.io/nbskill/review.html#public_function_literacy_problems)
checks exported public functions for that compact contract so
maintainers and agents can quickly understand what each function is for,
how to call it, and what behavior is protected.

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

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

### public_function_literacy_problems

``` python
def public_function_literacy_problems(
    path:str='.'
):
```

*Return docs/example/test contract warnings for exported public notebook
functions.*

Notebook craft is a small subset of style diagnostics that should steer
agents toward literate, reviewable notebook changes.

``` python
_literacy_preview = public_function_literacy_problems("nbs/04_review.ipynb")[:2]
_literacy_preview
```

    [{'code': 'public-function-markdown',
      'path': '04_review.ipynb',
      'severity': 'warning',
      'source': 'nbskill',
      'detail': "public function 'notebook_size_problems' needs Markdown docs cell mentioning the function",
      'cell_id': '93269841',
      'symbol': 'notebook_size_problems',
      'line': 1,
      'missing': 'Markdown docs cell mentioning the function'},
     {'code': 'public-function-example',
      'path': '04_review.ipynb',
      'severity': 'warning',
      'source': 'nbskill',
      'detail': "public function 'notebook_size_problems' needs example cell that calls the function",
      'cell_id': '93269841',
      'symbol': 'notebook_size_problems',
      'line': 1,
      'missing': 'example cell that calls the function'}]

``` python
assert {
    "public-function-docstring", "public-function-markdown",
    "public-function-example", "public-function-test",
} <= {problem["code"] for problem in public_function_literacy_problems("nbs/04_review.ipynb")}
```

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

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

### notebook_validation_problems

``` python
def notebook_validation_problems(
    path:str='.'
):
```

*Return notebook metadata and Python syntax validation problems.*

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

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

### notebook_size_problems

``` python
def notebook_size_problems(
    path:str='.'
):
```

*Return size warnings for notebook cells and generated Python files.*

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

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

### reset_global_usage_summary

``` python
def reset_global_usage_summary():
```

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

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

### notebook_autofix

``` python
def notebook_autofix(
    path:str='.', dry_run:bool=False
):
```

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

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

### style_report

``` python
def style_report(
    path:str='.', # File or folder to check
    chkstyle:dict | None=None, # Captured chkstyle result to include
    max_output_chars:int=12000, # Maximum raw chkstyle text to keep in report
    max_diagnostics:int=200, # Maximum parsed chkstyle diagnostics
    changed_only:bool=False, # Report only diagnostics for changed code cells
    ref_a:str | None='HEAD', # First git ref for changed_only filtering
    ref_b:str | None=None, # Second git ref; defaults to working tree
    skip_folder_re:str | None=None, # Regex for notebook paths to skip
    skip_path:str | None=None, # Folder name/path to skip
):
```

*Return structured style diagnostics, problem chart, and global nbskill
usage data.*

``` python
with write_tool_notebook("04_review_style_report.ipynb") as path:
    report = style_report(path)
    test_eq({"summary", "notebook_problems"} <= set(report), True)
    test_eq(isinstance(report["notebook_problems"], list), True)
    test_eq("Global nbskill usage:" in report["text"], True)
```

The structured report is meant for tools as much as people. `summary`
gives a compact count of problems, `problem_chart` groups diagnostics by
source and code, and `text` is the human-readable report.

``` python
sample_chkstyle = {"status": 1, "output": "# demo.ipynb:cell[abc123]:2: Missing whitespace (hint: add a blank line)"}
with write_tool_notebook("04_review_sample_report.ipynb") as path:
    sample_report = style_report(path, chkstyle=sample_chkstyle, max_output_chars=200, max_diagnostics=5)
    print(sample_report["summary"])
    print(sample_report["problem_chart"]["by_source"])
    print(sample_report["diagnostics"][0])
```

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

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

### run_style_check

``` python
def run_style_check(
    path:str='.', skip_folder_re:NoneType=None, skip_path:NoneType=None, strict:bool=False,
    max_output_chars:NoneType=None
):
```

*Run chkstyle with nbskill’s default skip paths and capped output.*

``` python
argv = _style_check_argv(".")
assert "--skip-path" in argv
assert "_proc" in argv
assert "--skip-folder-re" not in argv
assert "--skip-path-re" in _style_check_argv(".", skip_folder_re=r"^_proc/")
```

``` python
proc_report = run_style_check("_proc/01_read.ipynb")
assert proc_report["status"] == 0
assert proc_report["output"] == ""
assert notebook_validation_problems("_proc/01_read.ipynb") == []
assert not any(_style_root_is_skipped(problem["path"], skip_style_paths) for problem in _notebook_style_problems("."))
```

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

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

### style_check

``` python
def style_check(
    path:str='.', skip_folder_re:str | None=None, # Regex for folders to skip
    skip_path:str | None=None, # Folder name/path to skip
    strict:bool=False, # Raise when style hints or notebook hygiene problems are found
    delete_after_output:bool=False, # Reset ~/.nbskill-errors.json after printing the global summary
    max_output_chars:int=12000, # Cap printed chkstyle output
    max_diagnostics:int=200, # Cap returned diagnostics
    fix:bool=False, # Show conservative fix suggestions
    dry_run:bool=True, # Keep fix mode non-mutating by default
    changed_only:bool=False, # Report only diagnostics for changed code cells
    ref_a:str | None='HEAD', # First git ref for changed_only filtering
    ref_b:str | None=None, # Second git ref; defaults to working tree
):
```

*Print capped fast.ai style hints, notebook hygiene warnings, and global
tool usage.*

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

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

### validate_nbs

``` python
def validate_nbs(
    path:str='nbs', strict:bool=True, # Raise when invalid metadata is found
):
```

*Validate nbskill metadata needed for safe notebook tools.*

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

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

### code_source

``` python
def code_source(
    cell
):
```

*Return source for code cells; ignore markdown and raw cells.*

[`style_check`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_check)
is the direct Python wrapper around
[`style_report`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_report).
It captures fast.ai style output, appends notebook hygiene findings,
prints the combined text report, and raises in strict mode when
diagnostics are present.

``` python
with write_tool_notebook("04_review_style_check.ipynb") as path:
    style_output = StringIO()
    with redirect_stdout(style_output): style_check(path, strict=False, max_output_chars=500, max_diagnostics=5)
    print("\n".join(style_output.getvalue().splitlines()[:6]))
```

``` python
with write_demo_notebook("04_review_validate_valid.ipynb") as valid_path:
    _write_review_notebook(valid_path, [mk_cell("assert True", cell_type="code")])
    assert notebook_validation_problems(valid_path) == []
    validate_nbs(str(valid_path), strict=False)
```

``` python
with write_demo_notebook("04_review_validate_invalid.ipynb") as invalid_path:
    invalid_nb = _write_review_notebook(invalid_path, [
        mk_cell("x = 1", cell_type="code"),
        mk_cell("Some docs", cell_type="markdown"),
        mk_cell("assert True", cell_type="code"),
    ])
    invalid_nb.cells[1].metadata["nbskill"]["semantic_types"] = []
    invalid_nb.cells[2].metadata["nbskill"]["cell_type"] = "markdown"
    _write_nb(invalid_nb, invalid_path)
    codes = {problem["code"] for problem in notebook_validation_problems(invalid_path)}
    assert {"missing-cell-semantic-types", "cell-type-mismatch"} <= codes
```

``` python
with write_demo_notebook("04_review_export_hash.ipynb", base="nbs") as export_nb_path:
    export_nb = _write_review_notebook(export_nb_path, [mk_cell("#| default_exp sample_tool")])
    generated_py_path = exported_py_path(export_nb_path, export_nb)
    try:
        generated_py_path.parent.mkdir(parents=True, exist_ok=True)
        generated_py_path.write_text("actual = 1\n", encoding="utf-8")
        export_nb.metadata["nbskill"] = {"exported_py_hash": "bad"}
        _write_nb(export_nb, export_nb_path)
        codes = {problem["code"] for problem in notebook_validation_problems(export_nb_path)}
        assert "exported-py-hash-mismatch" in codes
    finally:
        if generated_py_path is not None: remove_demo_path(generated_py_path)
```

[`notebook_validation_problems`](https://MorsCerta-crypto.github.io/nbskill/review.html#notebook_validation_problems)
is narrower than
[`style_report`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_report):
it checks nbskill metadata plus Python syntax in code cells, the
prerequisites for reliable notebook tooling.
[`validate_nbs`](https://MorsCerta-crypto.github.io/nbskill/review.html#validate_nbs)
exposes the same failures to Python callers.

``` python
with write_demo_notebook("04_review_validation_example.ipynb") as validation_path:
    _write_review_notebook(validation_path, [
        mk_cell("x = 1", cell_type="code"),
        mk_cell("Some docs", cell_type="markdown"),
    ], stamp=False)
    problems = notebook_validation_problems(validation_path)
    print([problem["code"] for problem in problems[:4]])
```

### Code-cell diffs

Notebook diffs are noisy when metadata and outputs are included.
[`diff_nb`](https://MorsCerta-crypto.github.io/nbskill/review.html#diff_nb)
asks nbdev for code-cell source on each side of a comparison and prints
only the added, changed, or deleted code blocks the caller requested.

### Public UI text surface

FastHTML and HTML-producing notebook cells often hide customer-visible
copy inside Python component calls. A normal text search finds too much:
headings, button labels, docstrings, route names, tests, exceptions, and
implementation notes all look like strings. The public UI text surface
gives agents a smaller first pass over likely visible copy before
editing.

Use
[`visible_text_inventory`](https://MorsCerta-crypto.github.io/nbskill/review.html#visible_text_inventory)
before an edit when the task is phrased in product or customer language.
It answers: which notebook cell and symbol owns the visible words I may
need to change? That reduces the manual `rg` loop where an agent has to
compare notebook source, exported modules, and internal strings by hand.

Use `diff_nb(surface="public-ui")` after an edit when the review
question is user-facing copy rather than Python structure. It keeps
headings, labels, link text, image alt text, and similar UI literals
separate from code churn, metadata churn, tests, and docstring edits.

This is deliberately heuristic. It is meant to support a faster notebook
iteration loop, not to prove accessibility or render every possible
runtime branch: inventory the likely visible text, make a focused edit,
run the route/example smoke snippet, then review the public UI diff.

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

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

### visible_text_inventory

``` python
def visible_text_inventory(
    path:str='.', include_re:NoneType=None, max_records:int=200
):
```

*Print likely user-visible text from FastHTML/HTML-producing notebook
code cells.*

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

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

### diff_nb

``` python
def diff_nb(
    path:str, # Notebook path
    ref_a:str | None='HEAD', # First git ref; use None for working tree
    ref_b:str | None=None, # Second git ref; defaults to working tree
    adds:bool=True, # Include code cells added in ref_b
    changes:bool=True, # Include changed code cells
    dels:bool=False, # Include deleted code cells
    cell_id:str | None=None, # Limit output to comma/space-separated cell ids
    after_id:str | None=None, # Limit output to cells after this id in ref_b/the working tree
    surface:str='code', # Review surface: "code" or "public-ui"
):
```

*Print nbdev-style diffs for code cells, or a public UI text surface.*

``` python
from tempfile import gettempdir

with write_demo_notebook("04_review_no_git.ipynb", base=gettempdir()) as path:
    _write_review_notebook(path, [mk_cell("x = 1", cell_type="code")])
    try:
        diff_nb(str(path))
    except (SystemExit, ValueError) as exc:
        if isinstance(exc, SystemExit): assert exc.code == 1
        else:
            msg = str(exc)
            assert "disposable notebook" in msg and "ref_a=None" in msg and "ref_b=None" in msg
    out = StringIO()
    with redirect_stdout(out):
        diff_nb(str(path), ref_a=None, ref_b=None)
    report = out.getvalue()
    assert "--- code cell" in report and "+x = 1" in report
```

``` python
def _committed_review_notebook(root, cells=None):
    root.mkdir()
    path = root / "demo.ipynb"
    _write_review_notebook(path, cells or [mk_cell("x = 1", cell_type="code")])
    subprocess.run(["git", "init"], cwd=root, check=True, capture_output=True)
    subprocess.run(["git", "add", "demo.ipynb"], cwd=root, check=True, capture_output=True)
    subprocess.run([
        "git", "-c", "user.name=Nbskill", "-c", "user.email=nbskill@example.com",
        "commit", "-m", "base",
    ], cwd=root, check=True, capture_output=True)
    return path
```

``` python
root = demo_path("04_review_git_metadata")
try:
    path = _committed_review_notebook(root)
    nb = read_nb(path)
    nb.cells[0].metadata["nbskill"] = {"cell_type": "code", "semantic_types": []}
    _write_nb(nb, path)
    out = StringIO()
    with redirect_stdout(out):
        diff_nb(str(path))
    text = out.getvalue()
    assert "No code cell changes" in text
    assert "Ignored nbskill metadata changes in 1 cell" in text
finally:
    remove_demo_path(root)
```

``` python
root = demo_path("04_review_git_filters")
try:
    path = _committed_review_notebook(root)
    nb = read_nb(path)
    first_id = nb.cells[0].id
    nb.cells[0].source = "x = 2"
    nb.cells.append(mk_cell("y = 3", cell_type="code"))
    _write_nb(nb, path)
    out = StringIO()
    with redirect_stdout(out): diff_nb(str(path), cell_id=first_id)
    text = out.getvalue()
    assert "x = 2" in text and "y = 3" not in text
    out = StringIO()
    with redirect_stdout(out): diff_nb(str(path), after_id=first_id)
    text = out.getvalue()
    assert "y = 3" in text and "x = 2" not in text
    delta = style_report(str(path), changed_only=True)
    assert delta["summary"]["changed_only"] is True
    assert delta["summary"]["changed_cell_count"] == 2
finally:
    remove_demo_path(root)
```

``` python
def _write_style_problem_notebook(path):
    long_source = "\n".join([f"x{i} = {i}" for i in range(31)])
    long_markdown = "\n".join([f"paragraph {i}" for i in range(21)])
    mixed_import = mk_cell("import json", cell_type="code")
    mixed_import.outputs = [dict(output_type="stream", name="stdout", text="json\n")]
    _write_review_notebook(path, [
        mk_cell(
            "#| export\n"
            "def a():\n"
            "    pass\n"
            "def b():\n"
            "    \"\"\"Describe b.\"\"\"\n"
            "    pass\n"
            "def c():\n"
            "    \"\"\"Line one.\n"
            "    Line two.\"\"\"\n"
            "    pass\n"
            "def _private():\n"
            "    pass",
            cell_type="code",
        ),
        mk_cell(long_source, cell_type="code"),
        mk_cell(long_markdown, cell_type="markdown"),
        mk_cell("assert 1 == 1\nassert 2 == 2\nassert 3 == 3\nassert 4 == 4", cell_type="code"),
        mk_cell("#| export\nimport os", cell_type="code"),
        mk_cell("#| export\nimport os", cell_type="code"),
        mixed_import,
        mk_cell("import sys", cell_type="code"),
        mk_cell("import sys", cell_type="code"),
        mk_cell("result = later_helper()", cell_type="code"),
        mk_cell("def later_helper():\n    return 1", cell_type="code"),
        mk_cell("def loader():\n    return MissingPath('x')", cell_type="code"),
    ])
```

`_write_literacy_problem_notebook` builds a compact demo notebook for
the literate review contract. The fixture keeps compliant, missing-docs,
missing-example, missing-test, and multiline-docstring cases in one
place so each warning code has a precise example.

``` python
def _write_literacy_problem_notebook(path):
    _write_review_notebook(path, [
        mk_cell("`ready` has docs, a docstring, an example, and a test.", cell_type="markdown"),
        mk_cell("#| export\ndef ready(x):\n    \"Return x plus one.\"\n    return x + 1", cell_type="code"),
        mk_cell("`no_example` has docs and a test but no example call.", cell_type="markdown"),
        mk_cell("#| export\ndef no_example(x):\n    \"Return x plus one.\"\n    return x + 1", cell_type="code"),
        mk_cell("`no_test` has docs and an example but no test assertion.", cell_type="markdown"),
        mk_cell("#| export\ndef no_test(x):\n    \"Return x plus one.\"\n    return x + 1", cell_type="code"),
        mk_cell("`bad_doc` has a Markdown note but its docstring is too long.", cell_type="markdown"),
        mk_cell("#| export\ndef bad_doc(x):\n    \"\"\"Return x plus one.\n    Keep this second line out of file_context.\"\"\"\n    return x + 1", cell_type="code"),
        mk_cell("`eq_checked`, `ne_checked`, and `fail_checked` cover fastcore test helpers.", cell_type="markdown"),
        mk_cell("#| export\ndef eq_checked(x):\n    \"Return x plus one.\"\n    return x + 1", cell_type="code"),
        mk_cell("#| export\ndef ne_checked(x):\n    \"Return x plus one.\"\n    return x + 1", cell_type="code"),
        mk_cell("#| export\ndef fail_checked():\n    \"Raise a demo failure.\"\n    raise ValueError(\"boom\")", cell_type="code"),
        mk_cell("#| export\ndef no_docs(x):\n    \"Return x plus one.\"\n    return x + 1", cell_type="code"),
        mk_cell("#| export\ndef _hidden(x):\n    return x\n\ndef test_helper(x):\n    return x", cell_type="code"),
        mk_cell("ready(1)\nno_docs(1)\nno_test(1)\nbad_doc(1)\neq_checked(1)\nne_checked(1)\ntry:\n    fail_checked()\nexcept ValueError:\n    pass", cell_type="code"),
        mk_cell("assert ready(1) == 2\nassert no_docs(1) == 2\nassert no_example(1) == 2\nassert bad_doc(1) == 2\ntest_eq(eq_checked(1), 2)\ntest_ne(ne_checked(1), 3)\ntest_fail(lambda: fail_checked())\nassert _hidden(1) == 1\nassert test_helper(1) == 1", cell_type="code"),
    ])
```

``` python
with write_demo_notebook("04_review_literacy.ipynb") as path:
    _write_literacy_problem_notebook(path)
    problems = public_function_literacy_problems(path)
    codes = {(problem.get("symbol"), problem["code"]) for problem in problems}
    missing_codes = {("no_docs", "public-function-markdown"), ("no_example", "public-function-example"), ("no_test", "public-function-test")}
    ready_codes = {code for symbol, code in codes if symbol == "ready"}
    assert missing_codes <= codes
    assert not ready_codes
```

``` python
with write_demo_notebook("04_review_literacy_docstrings.ipynb") as path:
    _write_literacy_problem_notebook(path)
    problems = public_function_literacy_problems(path)
    codes = {(problem.get("symbol"), problem["code"]) for problem in problems}
    tested_codes = {
        ("eq_checked", "public-function-test"),
        ("ne_checked", "public-function-test"),
        ("fail_checked", "public-function-test"),
    }
    assert ("bad_doc", "public-function-docstring") in codes
    assert not tested_codes & codes
    assert not any(problem.get("symbol") in {"_hidden", "test_helper"} for problem in problems)
```

``` python
print(report)
```

    Notebook style report:
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=18cf6a7f
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=3e6417a6
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=9d964403
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=56e0e6c4
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=4debe573
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=742a5cf5
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=f41bdaef
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=3b1110e1
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=23f2d90a
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=4268cb78
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=762d5962
    - missing-cell-nbskill-metadata: nbs/data/04_review_style.ipynb id=711b360d
    - large-cell: nbs/data/04_review_style.ipynb id=18cf6a7f function_count=4 4 top-level functions; split into one-idea cells
    - large-cell: nbs/data/04_review_style.ipynb id=3e6417a6 line_count=31 31 code content lines; keep one idea per cell
    - large-cell: nbs/data/04_review_style.ipynb id=9d964403 line_count=21 21 markdown content lines; keep one idea per cell
    - public-function-docstring: nbs/data/04_review_style.ipynb id=18cf6a7f line=1 symbol='a' missing='one-line docstring' docstring_lines=0 public function 'a' needs a one-line docstring for context summaries
    - public-function-markdown: nbs/data/04_review_style.ipynb id=18cf6a7f line=1 symbol='a' missing='Markdown docs cell mentioning the function' public function 'a' needs Markdown docs cell mentioning the function
    - public-function-example: nbs/data/04_review_style.ipynb id=18cf6a7f line=1 symbol='a' missing='example cell that calls the function' public function 'a' needs example cell that calls the function
    - public-function-test: nbs/data/04_review_style.ipynb id=18cf6a7f line=1 symbol='a' missing='test cell that asserts or checks the function' public function 'a' needs test cell that asserts or checks the function
    - public-function-markdown: nbs/data/04_review_style.ipynb id=18cf6a7f line=3 symbol='b' missing='Markdown docs cell mentioning the function' public function 'b' needs Markdown docs cell mentioning the function
    - public-function-example: nbs/data/04_review_style.ipynb id=18cf6a7f line=3 symbol='b' missing='example cell that calls the function' public function 'b' needs example cell that calls the function
    - public-function-test: nbs/data/04_review_style.ipynb id=18cf6a7f line=3 symbol='b' missing='test cell that asserts or checks the function' public function 'b' needs test cell that asserts or checks the function
    - public-function-docstring: nbs/data/04_review_style.ipynb id=18cf6a7f line=6 symbol='c' missing='one-line docstring' docstring_lines=2 public function 'c' has 2 docstring lines; keep it to one line for context summaries
    - public-function-markdown: nbs/data/04_review_style.ipynb id=18cf6a7f line=6 symbol='c' missing='Markdown docs cell mentioning the function' public function 'c' needs Markdown docs cell mentioning the function
    - public-function-example: nbs/data/04_review_style.ipynb id=18cf6a7f line=6 symbol='c' missing='example cell that calls the function' public function 'c' needs example cell that calls the function
    - public-function-test: nbs/data/04_review_style.ipynb id=18cf6a7f line=6 symbol='c' missing='test cell that asserts or checks the function' public function 'c' needs test cell that asserts or checks the function
    - multi-problem-test: nbs/data/04_review_style.ipynb id=56e0e6c4 4 asserts/test functions; split into one-problem-at-a-time cells
    - unclean-cell: nbs/data/04_review_style.ipynb id=f41bdaef semantic_types=test_cell, private_code
    - duplicate-import: nbs/data/04_review_style.ipynb scope=exported import_key='import os as os' cells=4debe573, 742a5cf5
    - duplicate-import: nbs/data/04_review_style.ipynb scope=internal import_key='import sys as sys' cells=3b1110e1, 23f2d90a
    - cell-order: nbs/data/04_review_style.ipynb id=4268cb78 line=1 symbol='later_helper' confidence=high called before definition/import in later cell id=762d5962
    - missing-import: nbs/data/04_review_style.ipynb id=711b360d line=2 symbol='MissingPath' confidence=medium called without an earlier definition/import

``` python
with write_demo_notebook("04_review_generated.ipynb") as generated_path:
    generated_nb = _write_review_notebook(generated_path, [mk_cell("#| default_exp big_review_demo")])
    generated_py_path = exported_py_path(generated_path, generated_nb)
    try:
        generated_py_path.parent.mkdir(parents=True, exist_ok=True)
        generated_py_path.write_text("\n".join(f"x{i} = {i}" for i in range(1001)), encoding="utf-8")
        size_codes = {problem["code"] for problem in notebook_size_problems(generated_path)}
        assert "large-generated-py" in size_codes
    finally:
        if generated_py_path is not None: remove_demo_path(generated_py_path)
```

``` python
with write_demo_notebook("04_review_style.ipynb") as path:
    _write_style_problem_notebook(path)
    custom_map = demo_path("04_review_errors.json")
    old_map = os.environ.get("NBSKILL_FAILURE_MAP")
    try:
        os.environ["NBSKILL_FAILURE_MAP"] = str(custom_map)
        with redirect_stdout(StringIO()):
            style_check(str(path), delete_after_output=True)
        assert "usage:" in _format_global_usage_summary()
        assert not custom_map.exists()
    finally:
        if old_map is None: os.environ.pop("NBSKILL_FAILURE_MAP", None)
        else: os.environ["NBSKILL_FAILURE_MAP"] = old_map
```

``` python
with write_demo_notebook("04_review_style.ipynb") as path:
    _write_style_problem_notebook(path)
    try:
        with redirect_stdout(StringIO()): style_check(str(path), strict=True)
    except RuntimeError:
        pass
    else:
        raise AssertionError("strict style_check should raise for notebook hygiene problems")
```

``` python
assert code_source(mk_cell("plain docs", cell_type="markdown")) is None
assert code_source(mk_cell("answer = 42", cell_type="code")) == "answer = 42"
```

[`diff_nb`](https://MorsCerta-crypto.github.io/nbskill/review.html#diff_nb)
deliberately compares code-cell source only. Markdown edits and notebook
metadata churn stay out of the main diff so reviewers can see the
executable behavior that changed.

``` python
diff_root = demo_path("04_review_diff_example")
remove_demo_path(diff_root)
try:
    diff_root.mkdir()
    diff_path = diff_root / "demo.ipynb"
    _write_nb(new_nb([
        mk_cell("value = 1\nvalue", cell_type="code"),
        mk_cell("Original note", cell_type="markdown"),
    ]), diff_path)
    subprocess.run(["git", "init"], cwd=diff_root, check=True, capture_output=True)
    subprocess.run(["git", "add", "demo.ipynb"], cwd=diff_root, check=True, capture_output=True)
    subprocess.run([
        "git", "-c", "user.name=Nbskill", "-c", "user.email=nbskill@example.com",
        "commit", "-m", "base",
    ], cwd=diff_root, check=True, capture_output=True)

    diff_nb_json = read_nb(diff_path)
    diff_nb_json.cells[0].source = "value = 2\nvalue"
    diff_nb_json.cells[1].source = "Updated note that will not appear in the code diff"
    _write_nb(diff_nb_json, diff_path)

    diff_output = StringIO()
    with redirect_stdout(diff_output):
        diff_nb(str(diff_path), ref_a="HEAD")
    print(diff_output.getvalue())
finally:
    remove_demo_path(diff_root)
```

Here is the intended workflow on a tiny FastHTML-shaped notebook: first
inventory likely visible text, then review just the public UI surface.
The output is compact enough for an agent to paste into a status update
or use as a pre-edit checklist.

``` python
from tempfile import gettempdir

ui_source = """
def _hero():
    return Div(
        H1('Welcome'),
        P('Compare providers in minutes.'),
        Button('Start now'),
        Img(src='/logo.png', alt='Company logo'),
    )

raise ValueError('internal diagnostic, not customer copy')
"""

with write_demo_notebook("04_review_public_ui_demo.ipynb", base=gettempdir()) as ui_path:
    _write_review_notebook(ui_path, [mk_cell(ui_source, cell_type="code")])

    print("Inventory before editing:")
    visible_text_inventory(ui_path)

    print("\nPublic UI diff for the current notebook contents:")
    diff_nb(str(ui_path), ref_a=None, ref_b=None, surface="public-ui")
```

    Inventory before editing:
    cell_id | line | symbol | call | text
    20a783b4 | 4 | _hero | H1 | Welcome
    20a783b4 | 5 | _hero | P | Compare providers in minutes.
    20a783b4 | 6 | _hero | Button | Start now
    20a783b4 | 7 | _hero | Img.alt | Company logo

    Public UI diff for the current notebook contents:
    --- public-ui cell 20a783b4 ---
    --- 
    +++ 
    @@ -0,0 +1,4 @@
    +H1: Welcome
    +P: Compare providers in minutes.
    +Button: Start now
    +Img.alt: Company logo

Review functions are direct Python APIs. Use `doctor` with
[`diff_nb`](https://MorsCerta-crypto.github.io/nbskill/review.html#diff_nb)
and
[`style_check`](https://MorsCerta-crypto.github.io/nbskill/review.html#style_check)
when a change needs broader diagnostics.
