# NB Shift > One Jupyter notebook, four lanes: a reproducibility audit, a port to a marimo > notebook, a WebAssembly shareability check, and a scheduled batch-run plan. > A real parser reads the notebook in the browser first, and every finding it > raises must be answered by the lane you run. URL: https://nb-shift.skillsafe.ai/ Platform: SkillSafe apps (static bundle, strict CSP, one model behind `ss.run`) Model: `gpt-terra` (resolves to `gpt-5.6-terra`), markup 1000 bps Price: no publisher fee; you pay the platform for the tokens a run actually uses ## What it is for You have a notebook that works on your machine and nowhere else. NB Shift takes that one document and answers four different questions about it, without you re-pasting anything: - **audit** — can someone else run this top to bottom and get the same answer? - **port** — what does this look like as a marimo notebook that actually loads? - **wasm** — could this run in a browser with no install, and what has to change? - **batch** — what does this look like as a job a scheduler runs at 4am? The work object is the same for all four: one `.ipynb`, one marimo `.py`, or one plain script with `# %%` cell markers. ## What runs in your browser, for free, before anything is sent `nbscan.js` is not a regex pass. It is a Python lexer plus a binding analyser: - Comments and every string form — triple-quoted, raw, byte and f-strings with their prefixes — are masked before a single identifier is counted, so `x = "import pandas"` is not an import and the `f` of an f-string is not a name. - IPython magics and `!` shell escapes are removed from the Python analysis rather than parsed as if they were code, and a cell whose first line is a `%%` cell magic is not treated as Python at all. - Per cell, the analyser records the names bound **at indentation zero**. That is exactly marimo's rule for what a cell defines, which is why the port lane can say up front which cells collide. - `df.columns = [...]` and `df[0] = 1` bind nothing and use `df`; `a, b = f()` binds both; `x += 1` both reads and writes `x`; `def`/`lambda` parameters and comprehension variables are locals, not undefined globals. From that it derives: - names defined at the top level of more than one cell (marimo rejects these); - names a cell uses that are only defined in a **later** cell (hidden kernel state — the notebook cannot run top to bottom); - names used but never bound anywhere (a missing import, or a deleted cell); - execution-count forensics: counts that run backwards, cells that never ran, duplicated counts, and stored output beside code with no count at all; - an import inventory mapped against a Pyodide availability table (`ok`, `limited`, `no`, `unknown`), which is what the WASM lane starts from; - credential-shaped literals, absolute paths, `os.chdir`, writes to disk, interactive calls, and unseeded randomness. The prescan does not stop at describing the problem. Because it knows, per cell, which names are bound at the top level and which are needed from elsewhere, it also emits the repaired notebook itself: **Download the reordered notebook (.ipynb)** topologically sorts the cells so every cell follows every cell that defines a name it needs, keeps each markdown cell anchored to the code cell it introduces, clears all stored outputs and resets every `execution_count` to `null`, and writes a valid nbformat 4 document. Where two cells each need a name the other defines, no order can satisfy both: the cycle is broken at the lowest original cell index and named on the page, rather than silently mangled. This costs nothing and needs no account. The prescan's own limits are published in the UI and in `NBScan.LIMITS`: the analysis is lexical, not a full parse, so a name bound only by a star-import or by dynamic attribute setting is not seen. ## What is sent, and what is not The model receives a **cell-structured digest**, never the raw `.ipynb`: - Stored outputs — usually the bulk of a real notebook, and usually base64 images — are replaced by a one-line description per cell. - Credentials are masked in the browser before the request is built. - If the digest is over budget, the **middles** of the longest cells are cut, with the head and tail of each cell kept and the cut announced in-band, because a cell carries its result at the end. ## Input contract ```json { "task": "audit | port | wasm | batch", "notebook": "", "depth": "standard | exhaustive | triage", "audience": "author | reviewer | ops", "python": "3.12 | 3.11 | 3.13 | unchanged", "context": "free text, may be empty", "prescan_facts": { "flags": [{"id": "nb-hidden-state", "severity": "critical", "detail": "..."}], "resources": [{"id": "nb-cells", "detail": "..."}], "stats": {} } } ``` `task` is the router field and is required. Only the flags relevant to the chosen lane are sent, and every one of them must come back in `coverage_check`. ## Output contract One JSON object, the same envelope for every lane: `lane`, `title`, `headline`, `verdict` (`ready` | `needs-work` | `blocked`), `summary`, `checks[]` (`{check, status, evidence, requirement}` with status `pass|warn|fail|unknown`), `findings[]` (`{id, severity, area, cell_ref, problem, impact, fix, code}` with severity `critical|high|medium|low`), `artifact` (`{filename, language, note, content}`), `steps[]`, `coverage_check[]` (`{flag_id, status, note}` with status `confirmed|cleared|not-applicable`), `assumptions[]`, `open_questions[]`. Per-lane specifics: the audit lane returns eight named checks and a repaired cell order; the port lane returns six checks and a complete runnable marimo notebook; the WASM lane returns five checks and a substitution list; the batch lane returns six checks and a document with exactly three sections — Parameters, The runner, Failure handling. ## Reconciliation Every prescan flag sent with a run must be answered exactly once, as `confirmed`, `cleared` or `not-applicable`. The UI shows that table and **names any flag that was sent and never mentioned**, so a quiet omission is visible rather than read as a clean bill of health. The prescan can be wrong; clearing a bad flag with a reason is a correct answer. ## Sources A derived work built on four published SkillSafe skills, credited in full: - `@openai/jupyter-notebook` — the reproducibility audit lane - `@marimo-team/jupyter-to-marimo` — the port lane - `@marimo-team/wasm-compatibility` — the WASM lane - `@marimo-team/marimo-batch` — the scheduled batch lane Not affiliated with, endorsed by, or operated by OpenAI, Project Jupyter or marimo. ## Automation Base URL: `https://api.skillsafe.ai/v1/app-api`. Every call carries `Authorization: Bearer ` and returns `{"ok":true,"data":{...}}` or `{"ok":false,"error":{"code","message"}}`. The endpoints this app uses are `/me`, `/estimate`, `/run`, `/jobs/{id}` and `/run-stream` — the token carries the app's identity, so the slug never appears in the path. `https://nb-shift.skillsafe.ai/api.html` documents the whole API — the `task` field first, then one worked example per lane, tabbed across cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#. `https://nb-shift.skillsafe.ai/tokens.html` manages the token in this browser.