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 nbReviewing 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 is useful before publishing exported code because it combines fast.ai style hints with notebook hygiene reports, while 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 must combine capped style output with notebook hygiene diagnostics, and diff_nb must show code-cell behavior changes without raw notebook metadata noise.
Style feedback as a tool
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 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.
public_function_literacy_problems
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.
_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'}]
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")}notebook_validation_problems
def notebook_validation_problems(
path:str='.'
):Return notebook metadata and Python syntax validation problems.
notebook_size_problems
def notebook_size_problems(
path:str='.'
):Return size warnings for notebook cells and generated Python files.
style_report
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.
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.
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])run_style_check
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.
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/")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("."))style_check
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.
validate_nbs
def validate_nbs(
path:str='nbs', strict:bool=True, # Raise when invalid metadata is found
):Validate nbskill metadata needed for safe notebook tools.
code_source
def code_source(
cell
):Return source for code cells; ignore markdown and raw cells.
style_check is the direct Python wrapper around 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.
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]))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)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"} <= codeswith 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 is narrower than style_report: it checks nbskill metadata plus Python syntax in code cells, the prerequisites for reliable notebook tooling. validate_nbs exposes the same failures to Python callers.
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 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 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.
visible_text_inventory
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.
diff_nb
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.
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 reportdef _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 pathroot = 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)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)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.
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"),
])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_codeswith 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)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
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)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_mapwith 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")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 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.
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.
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 and style_check when a change needs broader diagnostics.