Metadata-Version: 2.4
Name: nocando
Version: 0.4.1
Summary: 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.
Author: Phinn Markson
Project-URL: Homepage, https://github.com/phinnphace/nocando
Project-URL: Repository, https://github.com/phinnphace/nocando
Project-URL: Issues, https://github.com/phinnphace/nocando/issues
Keywords: machine-learning,linter,colab,gpu,static-analysis,mlops
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pdf
Requires-Dist: pypdf>=4.0; extra == "pdf"
Dynamic: license-file

# 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).

## Quickstart

**nocando points AT your file.** There is nothing to launch, activate, or
deploy — nocando is not an app or a service. You give it the path to
something you already wrote; it reads that file, prints a report, and
exits. Read-only, one direction: your file to nocando's report. It never
runs your code and never modifies it.

```bash
pip install nocando
nocando your_script.py --runtime t4    # substitute a real file you have
```

`your_script.py` is required — a script, notebook, or manifest you
already have. nocando has nothing built in to point at; give it one of
your own files. A couple more shapes it reads directly:

```bash
nocando analysis.ipynb                 # runtime autodetected
nocando environment.yml                # audit conda+pip coherence directly
```

Exit codes: `0` PASS · `1` ADVISE (fragile/unconfirmed) · `2` BLOCK
(cannot run as declared) — CI/pre-commit friendly. Full flag list:
`nocando --help`. See **Usage** below for every input type this reads.

## 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

```bash
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:

```python
!wget -q https://<your-host>/nocando.py
!python nocando.py /content/drive/MyDrive/train.py --runtime t4
```

## Task vocabulary (below the library-name floor)

Some real assignments never name a library at all — "train a deep MLP...
save checkpoints, use early stopping, plot learning curves using
TensorBoard" describes a task entirely in English, with zero code
identifiers for the existing prose scan to match. nocando recognizes a
small set of deep-learning-infrastructure phrases (TensorBoard,
checkpoint, early stopping, learning curve, epochs...) as signaling "this
is a training task" without guessing WHICH framework — TensorBoard reads
as TensorFlow to most people but PyTorch supports it too, so nocando names
the ambiguity and states the actual next step (confirm the framework
before picking a runtime) rather than fabricating confidence it doesn't
have.

## 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.4` and 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 `cuda`
  placement 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`/`scipy` are 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 → **BLOCK**
- `torch_configured.py` — torch with device placement → **PASS**
- `torch_forgot_device.py` — torch, no device call → **ADVISE**
- `xgb_default.py` — XGBoost with default (CPU) params → **ADVISE**
- `svm_demo.ipynb` — notebook parsing incl. `%magic`/`!shell` stripping → **BLOCK**
- `cuda_on_cpu.py` — unconditional `.to("cuda")` on a CPU runtime → **BLOCK**
- `guarded_cuda.py` — cuda placement behind `is_available()` guard → **PASS**
- `flash_on_t4.py` — flash-attn on a T4 (cc 7.5 < 8.0) → **BLOCK**; same file on A100 → **PASS**
- `bnb_on_mac.py` — bitsandbytes on Apple Silicon (MPS) → **BLOCK**
