Nano v1.0: CLI, static typing, look-ahead protection, indicators + pre-production hardening - #5
Merged
Merged
Conversation
Nano 1.0 core. The compiler emits the lowest IR version that can express a program, so all 21 .nano/_ir.json corpus pairs stay byte-identical and a host pinned to 0.1.0 keeps working; programs reaching past baseline become 1.0.0. - nano/types/: type system (series<T>, confidence, assignability), semantic checker, and look-ahead protection. Series offsets count backwards and must fold to a non-negative constant, so close[t+1] and close[-1] are both compile errors with exact positions. - nano/indicators/: 33 typed indicator signatures plus deterministic kernels. Absent cells stay absent; recursive kernels re-seed across feed gaps rather than smoothing over them. - nano/ir/module.py: IR v1.0 as a flat typed DAG. Backward-only references, effects as capability grants, tier gating, and a moduleHash over executable content only -- separate from sourceHash so a comment edit changes one and not the other. - nano/runtime/vm.py: one evaluator for both IR versions. Baseline graphs lift via StrategyGraph.to_module(); a conformance test asserts the two paths agree bar-for-bar across the whole corpus. - nano/cli/: check, compile, replay, visualize, indicators, version, with documented exit codes and editor-parseable diagnostics. - nano/data/: the only module that reads a file. Timestamps parse as UTC, rows sort, duplicates are rejected, blank cells are absent rather than zero. Grammar gains tier/param/input/let/risk/signature/route, arithmetic with precedence, series indexing, else branches, and multiple rules per schedule. Tests 173 -> 268 passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…aths
A mutation sweep disabled each safety guard in turn and re-ran the suite. Six
survived -- meaning nothing tested them:
- effect manifest as a capability grant
- tier gating
- forward-reference / cycle rejection
- negative series.index offset in a hand-written document
- fastmath refusal
- baseline tier restriction in version inference
"Load-time validation is the security boundary" was an untested claim.
tests/test_module.py now covers all of it (44 tests) and every mutation is
killed.
Two robustness defects found by chaos-testing the CLI with malformed input:
- A non-UTF-8 source file produced a traceback instead of a diagnostic.
UnicodeDecodeError is a ValueError, not an OSError, so the existing
"except OSError" missed it: a missing file got a clean message while
binary junk crashed.
- The same gap existed in nano/data/frames.py for market data. load_frame
now funnels every failure through FeedError, so a caller handling bad data
does not also have to handle OSError and UnicodeDecodeError separately.
Also: Ctrl-C now exits 130 with "interrupted" rather than a traceback, and a
broken pipe (piping compile output into head) exits cleanly instead of raising
during interpreter shutdown.
Tests 280 -> 327 passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Production polish over v1.0. No new language features. Full report in
docs/HARDENING-2026-07-26.md. Tests 280 -> 338.
Governance -- the boundary was reachable around
-----------------------------------------------
NanoModule is a frozen dataclass, so in-process code could construct one and
skip from_dict entirely. An audit built a module whose manifest granted only
log.append and ran three BUY intents through the VM; the identical document,
loaded properly, is rejected. NanoModule.validate() now re-runs load-time
validation and run_module calls it before executing -- one pass over the nodes
against an evaluation that is nodes x bars.
Two asymmetries closed:
- compile_to_dict did not round-trip through the loader while compile_module
did, so "nano compile -o ir.json" wrote a document the validator never saw.
That contradicted a comment in the same file.
- StrategyGraph.to_module() built a module directly; now validated too.
Effect manifests reject duplicate entries: two byte-different documents granting
the same capability would make moduleHash depend on spelling.
A mutation sweep disabled each safety guard in turn. Six survived -- nothing
tested them. tests/test_module.py now covers all of it and every mutation dies.
CLI correctness
---------------
- replay and visualize caught only NanoCompileError, but compile_module
round-trips through the loader -- IRValidationError escaped as a traceback
while compile reported it cleanly. One shared _compile helper now.
- --emit types and --emit plan accepted -o, ignored it, and exited 0 having
written nothing.
- check abandoned remaining files at the first unreadable one, so the exit
code depended on argument order.
- the --verify second run sat outside the error guard.
- exit codes reclassified: a malformed --date, an unknown indicator, and an
impossible --ir-version are usage errors (2); a signal the data lacks and an
empty date selection are diagnostics (1), since both inputs read fine.
- "nano indicators" with an empty name printed the whole list -- truthiness
where identity belonged.
Simplification
--------------
- _is_plain_int was defined twice, byte-identical including its docstring.
- the comparison-operator set was written out four times; one source now.
- _NAMED_OPS existed in two files.
- removed canonical_effects() and the CompiledIR alias -- no call sites.
Documentation
-------------
CONTRIBUTING said the shipped CLI was "designed but not built", inviting a
contributor to rebuild working code. Three documented examples had not compiled
for several releases: README's showcase used "observe market", its roadmap prose
used "buy when RSI < 30", and BUILD_ORDER's exit criterion used "buy()".
tests/test_docs.py now compiles every fenced nano block in the repository and
checks the advertised test count against the suite, so this class of drift fails
CI. Blocks opt out with an explicit "// doc: illustrative" marker.
Also corrected: stale counts (including a 121 the last sweep missed), RiskEngine
-> DecisionGate, ProvenanceRiskEngine -> ProvenanceGate, and the status tables.
No doc claims broker execution, live feeds, a loop runner, self-modifying
deployment, or quantum dispatch -- none of those were built.
Robustness
----------
A non-UTF-8 source file crashed instead of reporting: UnicodeDecodeError is a
ValueError, not an OSError, so a missing file got a clean diagnostic while
binary junk produced a stack trace. Same gap in nano/data/frames.py. Ctrl-C now
exits 130 with "interrupted"; a broken pipe exits cleanly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
origin/main gained 7 commits while this branch was open: a documentation
overhaul (README 380 -> 113 lines, new docs/architecture.md, docs/language.md,
docs/status.md, edits across all 17 papers), issue and PR templates, and
setuptools package-data.
That rewrite is newer and better structured than the doc corrections drafted on
this branch, so it was adopted wholesale for README.md, CONTRIBUTING.md,
BUILD_ORDER.md, and the papers. Only the v0.1.0-specific claims that v1.0
actually invalidates were patched on top:
- "Nano does not calculate indicators" -> two forms now exist. RSI(14) is
still a host-supplied feed signal; RSI(close, 14) is computed by
nano/indicators/. Nano still never fetches data.
- "Number literals cannot be negative" -> the v1.0 grammar supports unary
minus. The constraint survives only as what baseline IR can carry, which is
why library entries still negate in the feed.
- "one schedule and one rule per strategy" -> reframed as the v0.1.0 subset
library entries deliberately stay inside so their IR fixtures remain
byte-stable, rather than as a limit of the grammar.
- docs/status.md now lists static typing, look-ahead protection, computed
indicators, and the CLI as implemented -- and marks risk-limit enforcement
Partial, since a risk block parses and reaches the IR but nothing enforces
the limits at run time yet.
Code conflicts resolved toward upstream where upstream was better: pyproject
keeps their description, urls, and package-data, and gains version 1.0.0, the
console script, and the Beta/Console classifiers. nano/__init__.py keeps their
framing -- "does not include an LLM runtime, live-action connector, or general
agent executor" is still true under v1.0, since a reasoning provider is a
protocol the host implements.
339 passed, 2 skipped. The second skip is the doc test's count check, which
stands down because upstream's README no longer advertises a test count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships Nano v1.0, then hardens it. Two phases in one branch, plus a merge reconciling with the docs overhaul that landed on
mainin parallel.Tests 173 → 339. All 21
.nano/_ir.jsoncorpus pairs remain byte-identical.Part 1 — Nano v1.0
The CLI
nanoinstalls as a console script with zero dependencies:nano checknano compilenano replaynano visualizenano indicatorsnano versionOne correction worth making explicit:
replayreplays a strategy against recorded bars, not previous runs. There is no run history to replay —--data bars.csv --date 2026-01-15selects a UTC day from a file you supply.--verifyruns it twice and fails if the two results differ.Look-ahead protection
Offsets count backwards only, and must fold to a non-negative compile-time constant:
close[t+1]is rejected becausetis not a compile-time constant, so the offset cannot be bounded — not because it names a future bar. There is no forward-indexing syntax at all, which is what makes look-ahead unrepresentable rather than merely detected. Warm-up is never fabricated either: a bar without enough history yields no value, and the VM will not emit an intent from it.The version-inference decision
The compiler emits the lowest IR version that can express the program. A v0.1.0-era strategy still compiles to byte-identical
0.1.0output — so the corpus, the strategy library, and Aether Code's pinned snapshot are untouched. Reach a v1.0 feature and the document becomes1.0.0, where that feature has a representation.--ir-versionforces the choice.Also: static typing with
series<T>, 33 deterministic indicator kernels, IR v1.0 as a flat typed DAG, and one VM for both IR versions — with a conformance test asserting the reference interpreter and the VM agree bar-for-bar across the whole corpus.Part 2 — Hardening sweep
Four read-only audits in parallel, then a mutation sweep that disabled each safety guard in turn to see whether any test noticed.
The finding that mattered
nano/ir/module.pyopens by claiming manifest violations, tier violations, cycles, and future-reading offsets are "enforced at load time, not discovered at run time". Six of those guards could be disabled with all 280 tests still passing. The checks worked; nothing would have told us if they stopped.Worse, the boundary was reachable around.
NanoModuleis a frozen dataclass, so in-process code could construct one and skipfrom_dict— the audit built a module whose manifest granted onlylog.appendand ran threeBUYintents through the VM.Fixed by making the claim true rather than softening it:
validate()re-runs load-time validation andrun_modulecalls it before executing. One pass over the nodes, against an evaluation that is nodes × bars.Two asymmetries closed alongside:
compile_to_dictdid not round-trip through the loader whilecompile_moduledid — sonano compile -o ir.jsonwrote a document the validator never saw, contradicting a comment three lines above it. AndStrategyGraph.to_module()built a module directly.Nine CLI defects
The worst two:
nano compile --emit types -o out.txtreported success and created nothing — the flag was accepted, ignored, and the command exited 0.nano checkabandoned remaining files at the first unreadable one, so the exit code depended on argument order.Also:
replay/visualizecaught onlyNanoCompileErrorwhilecompile_moduleround-trips through the loader, so anIRValidationErrorescaped as a traceback; a non-UTF-8 file crashed (UnicodeDecodeErroris aValueError, not anOSError); Ctrl-C tracebacked; a broken pipe raised at shutdown; and exit codes conflated usage errors with I/O errors.Documentation
Three documented examples had not compiled for months, including the README's headline strategy (
observe market— the grammar requiresobserve()).tests/test_docs.pynow compiles every fencednanoblock in the repo, so this class of drift fails CI.Part 3 — Merge reconciliation
origin/maingained 7 commits while this branch was open: a documentation overhaul (README 380 → 113 lines, newdocs/{architecture,language,status}.md, all 17 papers edited), issue and PR templates, and setuptools package-data.That rewrite is newer and better structured than the corrections drafted here, so it was adopted wholesale and only the v0.1.0 claims v1.0 actually invalidates were patched on top:
RSI(14)is a host feed signal,RSI(close, 14)is computed. Nano still never fetches data.nano/__init__.pykeeps upstream's framing — "does not include an LLM runtime, live-action connector, or general agent executor" — because it is still true under v1.0: a reasoning provider is a protocol the host implements.Known gaps, named not hidden
docs/status.mdnow marks these honestly rather than leaving a reader to discover them:risk { max_daily_loss 0.02 }parses, range-checks (fractions, not percentages), and reaches the IR — but nothing enforces the limits at run time yet. A strategy declaring a 2% cap is not stopped at 2%.moduleHashandsourceHashexist and are separable, but there is no hash-linked decision history.eval, or credential handling anywhere innano/— which is what makes "a Nano program cannot act on the world" structural rather than a policy.nano/types/checker.pyworst at 945. It splits cleanly into three passes; deferred because a refactor that size at the end of a hardening pass trades a known-good state for a rushed one.Full detail:
docs/HARDENING-2026-07-26.md.Test plan
pytest -q→ 339 passed, 2 skipped (optionalaether-protocol-cextra; README no longer advertises a count)intent.emitis refused🤖 Generated with Claude Code