Pre-flight environment-semantics linter for ML workloads — catches code/hardware mismatches (e.g. scikit-learn on a GPU runtime) before you occupy the hardware.
Project description
nocando
A pre-flight environment-semantics linter for ML workloads. It answers one question before you occupy hardware — in either direction:
Is the code you wrote coherent with the environment you declared?
That covers CPU-only workloads parked on GPUs (waste), unconditional
.cuda() calls headed for a CPU runtime (crash at minute 40), CUDA-requiring
libraries on Apple Silicon (import failure), and wrong-generation hardware
(flash-attn needs compute capability >= 8.0; a Colab T4 is 7.5). The unit of
analysis is the mismatch. Deliberate slow-on-CPU choices are not flagged —
can't legislate morality.
Syntax linters know your language. Type checkers know your data shapes.
Nothing knows that from sklearn.svm import SVC plus "Runtime: T4" is an
incoherent sentence — the constraint lives at the intersection of
library × hardware × platform, and nobody owns intersections. nocando owns
this one. Zero LLM calls, zero telemetry, pure static analysis.
Certified by Ted (stoop tabby, QA).
Why
Wasted accelerator time is deadweight loss: a scikit-learn grid search on a
Colab T4 produces heat, not gradients, for however many hours you sit there.
The knowledge to prevent this already exists — scattered across Stack threads
and subreddits, indexed by symptom, findable only after you've failed.
nocando inverts the indexing: it audits your declared intent (code + runtime)
at the moment of declaration.
Usage
python nocando.py train.py --runtime t4
python nocando.py analysis.ipynb --runtime a100
python nocando.py train.py # autodetect via nvidia-smi / MPS
python nocando.py train.py --runtime t4 --json # machine-readable, for CI
In Colab, one cell before your run:
!wget -q https://<your-host>/nocando.py
!python nocando.py /content/drive/MyDrive/train.py --runtime t4
Locations
Every finding names where it is AND quotes the actual offending line, not
just what's wrong — pandas (line 3: import pandas as pd), or in a
notebook, scikit-learn (cell 1: clf = SVC().fit(...)). Scripts and R
files get real line numbers; notebooks get cell numbers instead,
deliberately — Colab and Jupyter show you cells, not one continuous file's
line count, so a raw line number would be actively misleading there. A
two-location fact like a contradictory pin shows both, each quoted
(line 6: numpy=1.26.4and line 9:numpy==2.1.0``). One honest gap:
prose-vocabulary hits (a workload named in an assignment's markdown, not
yet in code) have no precise anchor yet — see AGENTS.md rather than a
faked location.
Verdicts
| Verdict | Exit | Meaning |
|---|---|---|
| ✓ CLEARED FOR TAKEOFF | 0 | Accelerator request is coherent with this code |
| △ WILL RUN, BUT | 1 | Runs, but the allocation is wasteful or unconfigured (e.g. torch imported, no .to(device); XGBoost without device='cuda') |
| ✗ NO CAN DO | 2 | Nothing in this code can address the requested accelerator |
Design constraint: the false-positive budget is ~zero (an advisor that interrupts wrongly gets disabled in a week). So verdicts are conservative:
- BLOCK fires only on true incoherence: no GPU-capable library at all,
a hard CUDA requirement in a non-CUDA environment, unguarded
cudaplacement on a CPU runtime, or a compute-capability generation mismatch. - A script that is one config line away from coherent (XGBoost, LightGBM, CatBoost, spaCy) gets ADVISE, never BLOCK.
numpy/scipyare treated as ancillary — present in everything, never the story — and can't independently drive a verdict.
Three audit dimensions
A third dimension answers the "how many other such mismatches exist" question honestly, rather than by trying to enumerate an unbounded space. Ingestion / coverage-honesty: nocando builds a bill of materials from whatever Frankenstein mix of libraries an artifact contains — including ones it has never seen combined before, or never seen at all — and reports two things without hardcoding pairwise trivia that would expire (numpy 2.0's ABI break was a real hazard in 2024; baking it in as a permanent alarm today would itself be shipping a stale fact as evergreen, the exact failure this project exists to prevent). What it checks for free, requiring zero domain knowledge: self-contradictory version pins — the same package pinned to two incompatible exact versions in the same artifact (e.g. conda says numpy=1.26, pip says numpy==2.1) is unconditionally a real conflict, no ecosystem expertise needed, never goes stale. What it reports instead of guessing: every recognized-but-unscanned library, explicitly, so a clean report is never mistaken for "verified compatible" — absence of a warning means no rule fired, not that no problem exists.
Two audit dimensions
nocando checks two independent things, and it's worth naming the difference. Runtime coherence (the original scope): can this code use the declared hardware. Environment-manager coherence (new): does the install sequence match how these tools actually cohere with each other — a fragility/reproducibility question, never a "will not run" one, so these findings are always ADVISE-capped and can never produce BLOCK on their own.
Two grounded cases today: conda+pip ordering (conda's own docs: "once pip
has been used, conda will be unaware of these changes and may make
modifications that would break the environment" — the documented fix is
conda-first-pip-last), and notebook bang-vs-magic installs (!pip install
shells out and may target a different Python than the kernel; %pip install is an IPython magic guaranteed to hit the active kernel — nothing
in either command's output signals which one you needed). environment.yml
is audited directly for channel-mixing and unpinned git dependencies.
Languages and formats
nocando audits Python and R — Colab serves both kernels, and R's
classical stack (tree, rpart, randomForest, gbm, e1071, cluster...) is
uniformly CPU-bound, so the GPU trap is identical in either tongue. Inputs:
.py, .ipynb (Python or R kernels), .R/.Rmd, and prose artifacts
(.txt, .md, .pdf via optional pypdf) — assignment handouts are audited
directly: embedded library()/import preambles are parsed as imports, and
named workloads in the prose (GridSearchCV, cv.tree, k-means...) are caught
prospectively, before any code exists.
Capability map
Hand-verified deterministic tier covering the common catastrophic cases:
CPU-only libraries (scikit-learn, pandas, statsmodels, NLTK, gensim,
Prophet…), conditionally-GPU libraries (XGBoost, LightGBM, CatBoost, spaCy),
and native accelerator frameworks (PyTorch, TensorFlow, JAX, RAPIDS,
transformers). Extend or override with --map your_map.json.
Intake: the constraint ledger (LEDGER.md)
The audit is only as good as the declared intent, so the package now fronts
with a constraint-ledger protocol (LEDGER.md): goal, must-stay, must-not,
and a disposability default stating that anything unlisted — including the
entire current implementation — is negotiable. It prevents the failure mode
where an assistant promotes your current attempt into a requirement.
Participation is optional by design. When no ledger is provided, inference
mode applies: the auditor constructs a provisional ledger from evidence,
tags every entry [inferred], and asks the single cheapest high-leverage
ratification question before any expensive work. The declared and inferred
ledgers are the same object at different confidence levels — participation
buys speed, never access. (nocando already does this at the code layer:
--runtime undeclared triggers autodetection.)
Evolution (GROWTH.md, AGENTS.md)
The engine never learns; the artifact evolves. The repo is the genome
(heredity via git), wild artifacts supply variation, and the ratification
gates are selection. Agent sessions are generations: stateless operators
that read the inheritance, metabolize new specimens, and write additions
back as diffs. AGENTS.md is the orientation file that lets any successor
instance pick this up cold — invariants, loops, lineage, open gaps. The
map grows via a propose-ratify loop
(GROWTH.md). --record "what actually happened" captures any
verdict-vs-reality disagreement as a case file; a maintainer or agent turns
cases into map entries + fixtures + tests; a human ratifies the diff. The
agent is stateless — the map is the memory, and every learned rule is a
reviewable line in git history.
Roadmap (tier 2)
The deterministic tier handles "can it." The agent tier handles "should it":
- workload sizing (dataset dims, params → rough FLOPs / VRAM estimate)
- allocation options with time/energy tradeoffs ("your ask is X; options A/B/C")
- syllabus/org aperture: audit a course's notebooks or a team's repo in batch
- retrieval-grounded advisories synthesized from the distributed corpus, reserved for cases where being occasionally wrong is survivable
- ledger-first sessions: intake (declared or inferred) -> coherence audit -> allocation options, in that order, always
Deployment: in the path of the cut
"Measure twice, cut once" presumes you own a ruler. Here, measurement
normally costs more than cutting — hours of scattered-doc hunting vs. one
click of "Runtime > T4" — so cutting-first is rational and wheel-spinning is
the equilibrium. nocando's job is a cost inversion: one second, zero hunting.
That only works if the measure fires where the cut happens, so it ships as
drop-in surfaces (deploy/), not a ritual to remember:
- Colab, cell 1 (
deploy/colab_bootstrap.py): fetches nocando, finds the active notebook, audits it against the autodetected runtime — measuring the real environment, not the declared one. - pre-commit hook (
deploy/.pre-commit-config.yaml): zero-participation enforcement; every commit is measured whether or not anyone remembers. - assistant layer (
LEDGER.md): paste-in intake + inference-mode rules for any chat assistant; section 7 covers users who never paste anything. - CI:
--json+ exit codes (0/1/2) drop into any pipeline gate.
Tests
tests/ contains the canonical fixtures:
tuesday_night.py— sklearn SVC + GridSearchCV on a declared T4 → BLOCKtorch_configured.py— torch with device placement → PASStorch_forgot_device.py— torch, no device call → ADVISExgb_default.py— XGBoost with default (CPU) params → ADVISEsvm_demo.ipynb— notebook parsing incl.%magic/!shellstripping → BLOCKcuda_on_cpu.py— unconditional.to("cuda")on a CPU runtime → BLOCKguarded_cuda.py— cuda placement behindis_available()guard → PASSflash_on_t4.py— flash-attn on a T4 (cc 7.5 < 8.0) → BLOCK; same file on A100 → PASSbnb_on_mac.py— bitsandbytes on Apple Silicon (MPS) → BLOCK
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nocando-0.3.0.tar.gz.
File metadata
- Download URL: nocando-0.3.0.tar.gz
- Upload date:
- Size: 24.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
071fa49e877649335ac57974ff9068540d1d7598d529ef96eb462627c126d56f
|
|
| MD5 |
a02c4b8e2d192b380407422e2bdbd029
|
|
| BLAKE2b-256 |
99be9443f97127e2d1e30652a98ed82704bf49f0e9b66594c64df16c6b67b9d0
|
File details
Details for the file nocando-0.3.0-py3-none-any.whl.
File metadata
- Download URL: nocando-0.3.0-py3-none-any.whl
- Upload date:
- Size: 24.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d28f9c7675379012e2d0947d278feb612dd57d1d5184921e7f91c3721d09b0fc
|
|
| MD5 |
ca70efeb997a2048b073c93b22c8df1d
|
|
| BLAKE2b-256 |
5a82b3324f66b9d13d08bcf9f4d2fd2af2fdf0c95c0900d130752483fe6e96f0
|