Changelog: 2.x: 2.63.1 to 2.40.1
Every release of this major version, newest first, as CHANGELOG.md records it.
2.63.1 - The memory cap claim, narrowed to where it holds #
The tests workflow had failed on every macos-latest leg since 2.62, in
check_library.py: "a memory cap STOPS a program that eats memory".
The cap is RLIMIT_AS, and macOS treats that limit as best-effort -
the doubling program ran to the 60 second timeout (E610) instead of
being stopped at 150 MB (E611). Linux honours the limit; Windows has
no equivalent and was already skipped. The CrewAI tool's test had been
narrowed to Linux for the same reason.
The assertion now runs on Linux only, unchanged there; macOS prints a
skip line saying why, Windows keeps its skip. Every place that stated
the platform truth says the same thing now - EMBEDDING.md,
THREAT_MODEL.md, COMPLIANCE.md, SECURITY.md, the MCP tool description,
the run() docstring (a comment-only change, the only edit to the
compiler), the benchmark README and the header the harness writes:
enforced on Linux, best-effort on macOS, not applied on Windows; the
timeout is enforced everywhere. Neither check_termination.py nor the
benchmark harness asserts a memory-cap outcome, so nothing else
needed narrowing.
2.63 - What a security reviewer looks for #
Nothing in the compiler changed. Six pieces for the person who has to decide whether agent-written Velaris may run on a machine they answer for.
THREAT_MODEL.md. The trust boundary (the operator sets the budget,
the program is untrusted, the compiler and the granted ffi modules
are trusted in full), what is defended against with the mechanism and
the suite that tests each, and what is explicitly not: anything a
granted module does, side channels, use below the limits, request
volume within net, logic errors with no contract, the meaning of
text, code not written in Velaris, the memory cap on Windows, a
tampered compiler. Two items that were not in any list before: io
includes env(), so an io-only program can read and print the
environment; and fs has no path list. Each residual risk has a
recommendation. The benchmark numbers are cited; the misses are named.
COMPLIANCE.md. One row per guarantee against the OWASP Top 10 for LLM Applications (2025) and NIST AI RMF functions - mechanism, framework item, suite, what it does not cover. Nearly every cell says "partially addresses", because that is the truth; the items Velaris does nothing for are listed by name.
Signed releases with an SBOM. release.yml now signs the wheel and
sdist through sigstore, the three executables and velaris.mcpb
through cosign, attaches the bundles, signatures and certificates, adds
a CycloneDX SBOM and SHA256 checksums, builds the wheel twice under
the same SOURCE_DATE_EPOCH and fails if they differ, and publishes to
PyPI the same files it signed. Every third-party action is pinned to a
commit with the tag beside it. SECURITY.md says how to verify a
download, with the exact identity string. None of this could be
exercised locally; the v2.63 tag is its first run, and the recap of
this release names what to watch.
A standing challenge. SECURITY.md: make Velaris report "proven"
for a promise that is false at runtime, or escape --allow io, and
you are credited by name in the changelog and in HALL_OF_FAME.md, with
the report treated as a security issue and fixed within a week. There
is no money. HALL_OF_FAME.md opens with the three model-family reviews
of August 2026 and the review bot of September; the changelog never
recorded which family produced which review, and the file says so
rather than guessing.
A card eval. evals/card_eval.py gives a model LLM.md and five
fixed tasks - a CSV total, a grade filter with a proven contract, a
sandboxed file read, a JSON path read, a stack calculator using pop
and div_or_fail - and checks, audits and runs each answer under
--allow io with a 10 s timeout, writing compiled-first-try, ran
correctly and proven promises to evals/RESULTS.md. It skips with one
line when no key is set, and was not run against any API for this
release; the results file holds only the three August reviews, marked
as reported rather than reproduced.
PR audit comments. The GitHub Action gains pr-comment: on a pull
request it posts one comment with the audit of every changed .vel
file - effects, modules, proven share, safe command, warnings - and
edits that comment on later runs, found by a hidden marker. Plain
curl against the REST API with the job's token; no third-party
action. The comment builder was exercised locally against real files,
including one that does not compile; the posting itself was not.
2.62 - Loops that provably end, and a corpus built to fool it #
The 2.61 benchmark named three programs Velaris could not catch. Two
stay misses, by construction: an off-by-one that stops early instead of
reading past the end (no contract, so nothing to refuse), and a program
that prints rm -rf build for its caller (the only effect is io, and a
harmless warning prints the same words). The third - a loop that ends,
slowly - is now shown to end before the program runs.
Termination. Every loop gets one of two verdicts, by a syntactic
rule that needs no solver and so answers the same with and without the
prover. terminates is claimed for exactly one shape: the condition
is, or has as an and conjunct, v < E, v <= E, v > E or v >= E,
where E mentions nothing the body assigns and calls only pure
functions, and every path through the body moves v by exactly one step
toward E, with v assigned nowhere else. Everything else - a step of two,
a step on one arm only, a counter reset on some path, a limit the body
grows, a flag-only condition, an or - is unshown, whether or not it
happens to end. A for loop goes through the same rule rather than
being exempted: for i in 0 to length(xs) with a push inside does not
end, and the rule says so. SPEC.md section 9.5 states it.
It surfaces in three places. velaris explain prints "loops: 2
terminate, 1 not shown" per function. velaris audit and
velaris.audit() gain loops_unshown per function and a warning
naming the functions, in schema velaris.audit/1 (added fields, not a
change); they also gain contract_coverage, the functions that take or
return a List, a Map or a record and promise nothing about it - a
coverage note, not a defect. velaris check --strict refuses a loop
whose end is not shown with E612; without --strict it is not an
error, and the time limit in velaris.run remains the guard.
Stressed before trusted. check_termination.py holds 44
adversarial loops with their required verdicts: the wrong direction, a
step of two, a step spelled 1 + i, a reset on one path, a limit the
body changes, a limit changed only inside a nested if, length(xs)
with a push inside, nested loops where only the inner qualifies, a flag
alone, a flag with or, a float counter, the step inside a check on
both arms and on one, a return with and without a step, recursion
instead of a loop. The analysis was wrong twice on the first attempt,
both times in the same direction - refusing a loop that ends: it
counted the standard library's loops when a program imported it (a
scoping error in the report, fixed by carrying the file), and it did
not accept a text literal or a pure builtin such as split inside a
limit, so for part in split(line, " ") was unshown and a control
program in the benchmark was a false positive. Both are fixed in the
analysis, not the tests. No existing example changed proof status
(velaris proofs examples --json before and after, diffed).
examples/termination.vel and examples/termination_bad.vel join the
suite; the latter runs and is refused only by --strict.
The benchmark, doubled. Sixty programs now, thirty of them written
against the tools: effects three helpers deep or inside a record, a
division guarded on one path and not the other, an off-by-one only on
empty input, an overflow inside a record field and inside a map, a
failure ignored inside an inline function, a loop that ends only when
input says so, growth by repeated text concatenation, subprocess
reached through the JSON-shaped call and through a handle, and control
programs that look suspicious - printing "rm -rf", reading their own
arguments, importing math in a loop - and are harmless. Same three
languages, same rules. On the machine that produced RESULTS.md
(Windows, prover present, Deno 2.9.6): of 53 dangerous programs
Velaris caught 51 - 41 before running, 10 while running - and missed
the two above; Deno caught 29 (5 before, by its lint on a while (true)) and Python 28; no tool flagged any of the 7 control programs,
the slow loop among them. Ten consecutive runs produce identical
output. What the doubling found: the prover does not flag a division on
an unguarded path when another path guards it, a remainder inside a
loop, or a get(xs, i + 1) - all three are caught while running, not
before, and the table says so.
args(). Under --allow io the program's args() used to contain
--allow and io. It no longer contains --allow, --deny,
--timeout or their values, from the command line or from
velaris.run; check_sandbox.py has the case.
2.61 - A table anyone can rerun #
Every claim this project makes about catching what a model writes has
been a claim. benchmark/ turns it into a table that one command
regenerates: python benchmark/run.py. Thirty programs in ten
categories - a file write hidden in a helper, a network call hidden in
a helper, division by input, an off-by-one read, integer overflow, an
ignored failure, an infinite loop, runaway memory, reaching subprocess
or os.system, and three correct programs that must not be flagged -
each written three times with the same behaviour, in Velaris, in
JavaScript for Deno and in plain Python. The harness runs every program
through check + audit + run under the budget the task needs (io,
plus ffi:math for one control program, with timeout 5 and
max_memory_mb 256), through deno run --no-prompt with no flags, and
through a Python subprocess with the same timeout, and records
caught-before-run, caught-during-run, missed, not-applicable,
false-positive or tool-absent, with the evidence in the cell.
On the machine that produced the committed RESULTS.md (Windows, prover
present, Deno 2.9.6): of 27 dangerous programs Velaris caught 24 - 15
before running and 9 while running - and missed 3; Deno caught 13 and
Python 13; no tool flagged a control program. The three misses are in
the corpus on purpose and are named in the results: an off-by-one that
stops early instead of reading past the end (no contract, so nothing
to refuse), a slow but finite loop (ends before the deadline), and a
program that prints rm -rf build for its caller (the only effect is
io). Deno's no-unreachable lint flags the three memory-growth
programs before running, where Velaris only stops them while running;
and on Windows it stops them with the timeout, not the memory cap,
because the cap is not enforced there and the interpreter allocates
slowly. Both are in the table.
Two things learned while building it. Under --allow, args() hands
the budget words to the program as well (['--allow', 'io', ...]), so
the corpus reads its input from stdin; that is a compiler bug to fix in
its own release, not here. And a Deno permission denial is an ordinary
exception, so a fetch inside try/catch exits 0 - the harness had to
watch the socket rather than trust the exit status. A Velaris refusal
cannot be caught by the program, which is the difference the benchmark
exists to show.
benchmark/README.md has the rules and the exact commands so a
stranger can rerun it and dispute a row. CI runs run.py --quick --check on the legs with the prover, with Deno absent there; a verdict
that changes between compiler versions fails the build and names the
row.
2.60 - The ffi cliff becomes a permission #
Every review of this project, from three model families and one
automated reviewer, raised the same caveat: allow ffi grants
everything Python can do. That was true, and it was the sentence a
security reviewer would stop reading at.
The budget can now name modules. --allow io,ffi:math,json grants the
ffi effect for those top-level packages only; anything else is refused
with E311, and the message names the exact flag that would permit it.
Plain ffi still grants every module, for programs whose author you
trust. The library takes the same form: run(source, allow={"io", "ffi:math"}), and refused_effect reports "ffi:os" when that is
what was reached for.
The audit reads the modules a program names in its py* calls and
reports them as ffi_modules; its safe_command grants exactly those.
A reviewer no longer has to choose between "no Python" and "all of
Python".
Four ways around it were tried and refused, in check_sandbox.py: a
module outside the list, a submodule path (os.path), the same module
through py_json, and through a handle via py_new. All three FFI
import sites go through one gate, and the bounded child process
receives the same list. An allowed module still works.
2.59 - Time and memory limits, prompted by a review bot #
The CrewAI pull request's automated reviewer flagged what three human
reviews had also noted and this project kept deferring: the effect
budget bounds what a program may touch, but nothing bounded how long
it could run or how much memory it could take. For an agent framework
calling run in a loop, that is the first thing that goes wrong.
velaris.run(source, allow={"io"}, timeout=30, max_memory_mb=512).
With either limit set the program runs in a separate, killable
process. A program that never ends is stopped at the deadline (E610,
timed_out=True); one that eats memory is stopped at the cap (E611,
out_of_memory=True, caught in 0.4 seconds in the test). The effect
budget still holds inside that child - verified - and an honest program
is unaffected.
Memory caps use the OS address-space limit, so they apply on Linux and macOS; on Windows the timeout applies and the cap is recorded but not enforced, which the docs say plainly rather than implying otherwise.
The MCP server and the HTTP door now default to 30 seconds and 512 MB. The CrewAI tool does too, reports STOPPED with the limit it hit, and gained the assertion the reviewer asked for: a refused effect must not reach the program's own fail branch either.
Attribution, added in 3.2 from the public record: the reviewer was
CodeRabbit (coderabbitai[bot]), on
crewAIInc/crewAI#7279
at 2026-09-05 06:56 UTC, under the heading "Denial of Service (CWE-400):
Uncontrolled Resource Consumption"; the fail-branch assertion answers a
second finding in the same review.
2.58 - Ready to submit to the frameworks #
Three integrations in integrations/, each written to the target's
own conventions and each with tests that assert the effect budget
holds through the framework's tool interface - because a budget that
leaks one layer up would be the worst kind of promise.
CrewAI (crewai/): VelarisAuditTool and VelarisRunTool(allow= ["io"]), five tests, a README, and the pull-request text ready to
paste. crewai-tools accepts community tools; this goes first.
LangChain (langchain_velaris/): a partner package,
langchain-velaris, since LangChain lists packages rather than
merging tools. Verified through .invoke() that a program granted
only io is refused fs.
MCP registry (mcp_registry/): the server.json for
registry.modelcontextprotocol.io, which every MCP client reads. A form
rather than a PR, and the highest reach of the three.
integrations/README.md says what to submit, where, in what order,
and what makes a maintainer say yes: a test that runs in their CI, a
description of the problem rather than the product, no marketing
words, and fast replies to review.
2.57 - --strict, from a question on r/Compilers #
Someone asked whether users could choose between strict and flexible
proof modes rather than having leniency imposed on them. They were
right, and half the answer already existed - velaris proofs --min 80
holds a line across a project - but the compiler itself always
accepted a promise that fell back to a runtime check.
velaris check program.vel --strict refuses when any promise could not
be proven, and names them:
examples/wordcount.vel: 2 promise(s) could not be proven, and
--strict does not accept runtime checks:
bar
needs biggest > 0
report
needs top > 0With no prover installed it refuses rather than pretending - a strict check that silently proves nothing would be the worst of both.
The default stays lenient because the solver is optional and strict-by-default would mean the language does not run for anyone who has not installed z3. That is an argument about the default, not a reason the flag should not exist.
2.56 - Seamless means tested, not claimed #
Three gaps between "the door exists" and "the door works".
The Jupyter magic was never installed. velaris_magic.py was not
in the wheel, so %load_ext velaris_magic worked in the repository and
failed for everyone who installed from PyPI. Found by checking from
outside the repo rather than inside it - the difference between a door
that opens and a door that appears to.
One shape for problems. audit() returned dicts while check()
and run() returned objects, so callers - including this project's own
Jupyter magic - had to handle both. Now everything returns Problem
objects and as_dict() flattens them for JSON. The magic got simpler
by six lines, which is what an API fix should look like.
The version guard covers every version. It watched velaris.py, pyproject.toml and the VS Code extension, but not the npm package or the .mcpb manifest - either could have shipped claiming a version the compiler never had. Both are guarded now.
check_library.py grew a section that asserts every door works after
a real install: all four modules importable, both APIs reporting the
same shape, the npm version following the compiler, the hooks present.
39 checks with the prover, 38 without, and rule 6 was followed - the
no-prover run happened before this was written down.
2.55 - Four more doors #
Velaris was reachable from a terminal, Python, MCP, CI, Docker and HTTP. Four populations were still locked out.
npm. npx velaris-lang script.vel --allow io, plus a real library
with TypeScript types: audit(source) and run(source, {allow: ["io"]}) from Node. Verified through Node that a program granted only
io is still refused fs - the same guarantee, one process further
away. The compiler stays a Python package; the wrapper says so plainly
when it is missing instead of failing with a spawn error.
Jupyter. %%velaris --audit --allow io runs a cell in a box and
prints what it can touch and how much is proven first. When an effect
is refused it names the flag that would permit it. The natural home for
the finance and measurement work where proven contracts earn their
keep.
pre-commit. Three hooks - velaris-check, velaris-fmt,
velaris-proofs - so a repository can require that its Velaris
compiles, is formatted, and keeps its proven share above a threshold.
Homebrew and winget manifests in packaging/, both with tests
that assert the effect budget still holds in a packaged build. They
need a tap and a pull request respectively, which is paperwork rather
than code, and the files say exactly what to do.
Building the Jupyter magic surfaced a wart in the library: audit()
returns problems as dicts while run() returns them as objects. The
magic handles both; the API should not need it, and that is worth
straightening when the format version next moves.
2.54 - A door for languages that are not Python #
Velaris was reachable from a terminal, from Python, from an MCP client and from CI. Everything else - a Node service, a Go tool, a Rust agent, a shell script - was locked out.
velaris serve opens a local HTTP door with the same three calls:
POST /check, POST /audit, POST /run, plus GET /card and
GET /health. Same library underneath, so the same guarantees.
Two ceilings, both enforced. The allow in a request is the
program's budget. --max-allow is the server's own limit - a caller
asking for ffi on a server started with --max-allow io,fs gets 403
and is told what it does grant. Verified both ways in
check_library.py, which now drives a real server on a real port.
It binds to localhost unless told otherwise, warns when it is not, and
warns when ffi is grantable - because this endpoint runs programs and
that should be said out loud rather than buried.
ARCHITECTURE.md gained a table of what each suite is for, and a
sixth rule: run every new suite without the prover BEFORE wiring it
into CI. That mistake has now been made three times; this release is
the first where the rule was followed rather than learned again.
2.53.1 - The library suite knows what needs the prover #
Three of the 25 checks in check_library.py are about proofs - what
was proven, a refuted promise, a 100% proven share - and the no-solver
CI legs have no prover, so every minimal leg failed the moment the
suite joined CI. The same mistake as 2.39.1, in a new suite.
They are now conditional, and without the prover the suite asserts the fallback instead: that a false promise breaks while running (E600 or E601) and that the audit reports nothing proven. That is a stronger test than skipping, because it checks the degraded path rather than ignoring it.
25 with the prover, 24 with the fallback. Both green.
2.53 - Setting it up should not be a chore #
The MCP server worked; getting it into an assistant meant finding a Python path and editing JSON. Two ways to skip that.
velaris mcp-install finds every MCP client on the machine -
Claude Code, Cline, Cursor, Windsurf, Continue, Zed - and adds a
velaris server to each, using the Python that is running it. It
never disturbs what is already there: tested against a config holding
another server with its own env block and an unrelated top-level key,
both of which survived, and every file is backed up before writing.
--list shows what it found without touching anything, --remove
undoes it.
velaris.mcpb is the double-click bundle, 94 KB, with the
compiler and standard library inside it - newer Claude Desktop builds
only accept remote connectors in the add-connector dialog, so a local
server has to arrive as a bundle. Verified by extracting it somewhere
with no Velaris installed, driving it as a client would, and watching
the sandbox still refuse fs to a program granted only io. It is
built and attached to every release automatically.
The bundled server now finds the compiler whether it was pip-installed, vendored beside it, or sitting in the repo next door - a user's own install still wins.
2.52 - Velaris from inside other programs #
The effect budget is the idea most easily copied out of this project. The way to make copying pointless is to make importing cheaper - so Velaris is now a library, a documented format, and an MCP server, as well as a command.
The library. velaris.check(source), velaris.audit(source),
velaris.run(source, allow={"io"}) and velaris.card(). run
captures stdout and stderr, accepts stdin and args, reports which
effect was refused, and restores the previous budget afterwards so a
process can audit and run many programs. The guarantee is identical to
the command line: a refused effect stops the program and cannot be
caught by it.
A versioned format. audit().as_dict() is velaris.audit/1:
effects, per-function contracts with proven-or-runtime status, the
proven share, the safe command, and warnings - including that ffi
cannot be contained by a budget. Documented field by field in
EMBEDDING.md, with a stated compatibility rule. Formats outlive the
tools that produce them.
An MCP server. velaris_mcp.py offers velaris_card,
velaris_check, velaris_audit and velaris_run over the Model
Context Protocol, so an assistant can write Velaris, check it, see
what it touches and run it in a box without leaving the conversation.
velaris_run defaults to allow: ["io"] - the least that is useful.
check_library.py proves the library and the server keep the same
promises as the command: 25 checks, including that a program refused
fs does not carry on, that budgets are restored between runs, and
that a refusal through MCP is reported as such. In CI on every push.
2.51 - Smaller per-call costs, and an honest note about the ceiling #
Two more measured savings on the interpreted path, both verified against every suite and the fuzzer.
The evaluator and statement runner compared node types with
isinstance, which walks a class hierarchy; the AST dataclasses have
no subclasses, so 22 of those became pointer comparisons. And a
function with no requires or ensures was copying its entire scope
on every call to snapshot values for promises it does not have.
Interpreted record work is now 1073ms where it was 1314ms at the start of this run - about 18% - on top of startup halving in 2.48.
The honest ceiling: the remaining cost is the sheer number of evaluator calls, roughly 580,000 for that benchmark. Removing it needs the AST compiled to closures or bytecode, which is a rewrite of the execution core rather than an optimisation of it. Native compilation for records has the same character - it is the LLVM struct ABI work that already caused a Windows-only bug once. Both are worth doing and neither is worth starting at the end of a long session; they need a plan, a branch, and the fuzzer running between every step.
2.50 - Function values carry their surroundings #
Two reviewing models named the no-capture rule as a real expressiveness
cost, and they were right: writing fn(n: Int) -> Bool { return n > limit } meant hand-writing a loop instead. Inline functions now
capture by value.
The values are copied when the function value is made, so later
assignment to those locals cannot change what the function sees -
examples/lambda_capture.vel demonstrates a captured cutoff staying
2 after the local is set to 99. There are no reference cells, so a
function value can never observe a change it was not handed.
What did not change, verified: a capturing inline function that tries to print is still rejected (E300 - effects cannot be smuggled past a signature); a false promise on one is still caught while running (E601); a name that exists nowhere is still E402. The prover treats captured values as unknown, which is the conservative direction.
examples/lambda_bad.vel was the test asserting capture is an error.
It is now examples/lambda_capture.vel and asserts the opposite - the
right way for a language to record a change of mind.
2.49 - The card's gaps were hiding three real bugs #
A model reviewed LLM.md without a compiler and reported five things
the card never explained. Writing them down meant testing them first,
and three turned out to be defects rather than omissions.
Deep recursion crashed. Past about 300 frames a program died with a
Python RecursionError traceback. Velaris now stops at 2000 frames
with E609 and says the recursion looks like it never ends - and
Python's own ceiling is lifted so that ours is the one that fires.
write_file crashed on any OS failure. An unwritable path threw a
raw traceback. It is now E608 with the reason from the operating
system. It stays non-catchable by design - a program that cannot write
where it was told to should stop - but it stops as a Velaris error.
A dead write_file branch sat unreachable in the interpreter,
left from an earlier edit.
Two card claims were simply wrong. Recursion works, carries
contracts, and is checked at call sites - the card never mentioned it,
so a reviewing model refused to use it. And invariant works on for
loops as well as while; the card documented only while.
examples/recursion.vel proves three contracts across both.
Handle lifecycle, tested and written down: double close is a no-op, use after close fails catchably, handles copy by reference.
Full module signatures for http, db, dates, csv, log and env_tools - every parameter type, every return type, which functions can fail, which carry proven contracts. Guessing these was the commonest source of wasted attempts for a model with no compiler.
2.48 - Speed, without touching a single guarantee #
Three measured wins, none of which changes what the language promises.
Startup halved: 133ms to 67ms. Every run - including every hello
world - was importing z3 (about 350ms of the cold cost) whether or not
anything needed proving. The prover is now located rather than
imported at startup, and check_proofs returns immediately when no
function carries a contract, no division or list read creates an
obligation, and no callee has a promise to satisfy. Programs that do
need proofs pay exactly what they paid before; the counterexamples in
avg_bad, offbyone_bad and conj_bad still appear.
Interpreted work is faster. sorted() was running on every single
builtin call to check the effect budget - now precomputed once.
length, get and push sat behind thirty string comparisons and two
module imports - now first, with the bounds guard intact. The
evaluator and statement runner dispatch the hottest node classes
directly instead of walking an isinstance chain: 5.8 million isinstance
calls became 3.8 million on a record-heavy benchmark, and that
benchmark went from 1314ms to 1139ms.
Measured, not claimed: a 3-million-iteration loop on the native path runs in about 80ms against Python's 445ms for the same loop. The fuzzer confirms both engines still agree exactly on 30 random programs, and all 89 examples, 25 fallible builtins, 15 sandbox cases and both stress suites pass unchanged.
2.47 - The periphery round #
A third adversarial pass executed everything the card mentions - every builtin, all seven modules against live systems, every error code - and held the score at 88 while finding the roughness had moved from the core to the edges. All five findings, fixed:
Division joins the catchable family. A zero divisor from user
input was the one remaining way to kill a checked program: E403 stops
the process and no check sees it. div_or_fail and mod_or_fail
fail the normal way, for exactly the input-driven case; plain / and
% stay strict for divisors the code controls. The card says which to
use when.
The http envelope is a record. call returned Text and get
returned Text, so feeding a raw body to code_of compiled and died at
runtime with a misleading JSON error. call now returns an Answer
record (status, body, raw); code_of and body_of read fields and
cannot fail; the wrong pairing is a type error. linkcheck got
simpler for it. Writing this found a genuine language subtlety: a
local named status shadowed the module's status function and
produced a confusing E530 - renamed, and worth remembering.
log.die says what it does. fail_with logged and killed the
process - correct behaviour, wrong name in a language where "fail"
means catchable. die is the new name; fail_with remains as an
alias so nothing breaks.
velaris check treats a library as a library. Requiring main at
check time (v2.44) was too broad: velaris check stdlib/http.vel is a
legitimate thing to do. A missing main is now only an error for the
file being run - which the runtime already enforced.
The card grew the last empirical truths: E525 (binding a void-returning fallible call), sort_by keys are Int, the _or_fail guidance, the Answer record, and log.die's semantics.
Attribution, added in 3.2 from the maintainer's account: the third pass was by the Claude model whose review is 2.44.
2.46 - Contents, not just lengths #
Two additions, both from the adversarial rubric's remaining points.
map_to(xs, f) - projection that changes type, fn(T) -> R, so a
record becomes one of its fields in one call instead of a hand-written
loop. The generics system supported two type variables all along;
nobody had written the function. Its ensures length(result) == length(xs) proves.
Quantified contents prove through loops. ensures all_of(result, is_positive) on a filtering loop was runtime-only; now the inference
harvests each all_of predicate from the function's own ensures and
tries "everything pushed so far satisfies it" as an invariant. Entry
is vacuous, each push must satisfy it on its path, and the promise
follows. The unguarded version correctly does NOT prove - the
candidate is dropped when a step can break it - and the runtime check
catches it with the actual offending list. Sound in both directions,
and the suite's wall time did not move.
Attribution, added in 3.2 from the maintainer's account: the rubric is that of the Claude model's review in 2.44.
2.45 - The road from 84 #
Three of the four items that separate this language from the low 90s, by its own adversarial grading.
The v2.42 mistake cannot recur. check_fallible.py reads
FALLIBLE_BUILTINS from the compiler itself, generates an
ignore-the-failure program for every member, and asserts each is
refused with E520 - then a caught-failure program for each, asserting
the failure formats and never escapes as a traceback. 23 builtins, all
enforced, in CI on every push. A fallible builtin added without
enforcement now fails the build by construction.
The prover reads record fields through lists. A List of Row is
modelled as one Int array per provable field, so
get(rows, i).amount is a real array read: the off-by-one over a
record list - the adversarial report's exact deferral - is refused
before running (E705), and ensures result >= 0 on a total over
record amounts is proven, which was flatly impossible before.
Building this introduced a truthiness bug on a Z3 array (or on an
array is not a None-check) that silently un-refuted a v2.38 regression
test; the refusal harness caught it within the same session, which is
the layered suites doing exactly their job.
The break question has an answer in writing. SPEC.md ยง13a: no
break, because the prover's exit knowledge - "the condition is
false" - is what pins counters at boundaries and proves loop promises;
a break turns that into a disjunction over hidden paths and abandons
most loop proofs. The supported idiom is the exit in the loop test
(while i < n and not found), which the prover can see, and the card
now teaches it with the reasoning. The decision names the condition
under which it would be revisited.
The fourth item is not code: another adversarial round, finding less.
Attribution, added in 3.2 from the maintainer's account: the grading is the second pass of the Claude model's review in 2.44.
2.44 - Everything the adversarial report found #
A model ran 86 adversarial artifacts against 2.43 - one production program, 34 broken fragments, 52 single-point mutations - and scored the language 76/100 with a list of defects. All of them are fixed.
The soundness hole (critical). pop, slice and set_at were
documented fallible but the checker never demanded handling, so a
clean velaris check could be followed by a raw Python traceback at
runtime. The list-operations type check returned before the
fallibility check ran. They now require check or try like every
other fallible call (E520).
The checker cannot crash. An unknown parameter type escaped as a
Python traceback instead of E500, breaking --json consumers and any
automated fix loop. Every checker pass now reports instead of raising.
main is validated at check time. No main at all (E400), a
main with parameters (E401), and a main marked or fail (E523,
new) are all compile-time findings now, not runtime surprises.
The typed FFI carries numbers. py_float("math", "sqrt", ["16"])
sent Python the string "16" and failed. Arguments that read as numbers
are now passed as numbers, with an all-strings retry so functions
genuinely wanting text still get it.
Conjunctions stop masking. requires divisor > 0 and length(items) > 0 over a record list dropped the WHOLE clause when
one conjunct could not translate - the checkable divisor > 0
included. Conjunctions are now split and each part checked on its own,
and record lists carry a modelled length even where their contents
cannot be seen. The report's exact shape is rejected at the call site
with a counterexample.
The card grew fifteen truths the reviewer had to discover by
experiment: % exists, random(n) is 0..n-1, random(0) is E405,
record fields go one per line, oversized literals are accepted but
arithmetic is checked, unused effects are viral, time needs ffi and
can fail, apply_to_each maps T to T only, the --allow/--deny
budget flags, velaris proofs --detail, and codes E400 E405 E509 E513
E521 E523 E542 E602 E704 - plus the E506/E507 correction (E507 is
about duplicate records, not empty maps).
Attribution, added in 3.2 from the maintainer's account: the reviewing model was a Claude model, and the same review's later passes are behind 2.45, 2.46 and 2.47.
2.43 - The prover crosses the loop boundary #
The sharpest finding in the last review was that the prover went blind
the moment a loop touched a list: a promise like
ensures length(result) == length(xs) - 1 would not prove even with a
hand-written invariant, and the textbook off-by-one
while i <= length(xs) { get(xs, i) } was caught only at runtime. Both
are fixed.
Two invariants were missing. A counter walking toward a limit stops
at the limit - without that, the state after a loop only says
i >= limit, so "the loop ran exactly that many times" could never
follow. And a list built one item per turn has exactly as many items as
the counter has turns. Both are inferred automatically now; the
promises in examples/loop_lists.vel prove with no invariant written
at all. The proven share across the examples went from 60% to 64%.
The off-by-one is refused before running. Reporting a bug about a
loop's index used to be unsound, because the index inside a loop stands
for any state the invariants allow. The sound route is the loop's
last real turn: a counter that starts inside the limit and steps by
exactly one takes every value up to the largest the condition allows,
so that turn genuinely happens and a read on it is a genuine read.
examples/offbyone_bad.vel is rejected with the position and the
length. Correct loops - i < length(xs), guarded reads, and counting
backwards - are unaffected, which was checked before anything shipped.
2.42 - Findings from a model that installed it and tried to break it #
A third model read velaris card, installed the language, wrote a
30-function calculator that compiled and ran correctly first try, then
spent its time attacking it. Nearly everything it reported was true.
Lists can shrink. pop, slice and set_at, all fallible, all
returning new lists. A stack machine - the natural shape for a
calculator - previously required rebuilding the whole list one element
shorter for every pop. examples/stack.vel is that program.
Overflow can be caught. a * b that outgrows 64 bits is still
E407, which stops the program and no check can catch - correct for a
bug, wrong when the numbers come from a user. add_or_fail,
sub_or_fail and mul_or_fail fail in the normal way instead.
break says something true. It used to report "unknown variable
'break'" and suggest declaring one. It now says the language has no
break, and suggests keeping a flag.
The formatter matches the documentation. requires, ensures and
invariant were being flattened to the margin, contradicting the style
in Velaris's own docs. Every shipped file is reformatted.
The card states the ceiling. The sharpest finding was that the
prover goes blind when a loop mutates a list: ensures length(result) == length(xs) - 1 will not prove even with an invariant, and
while i <= length(xs) { get(xs, i) } is caught at runtime rather than
before. That limit is real and unfixed; LLM.md now says so, along
with the overflow rule and the absence of break.
Left as-is, deliberately: the ffi escape hatch is total, and
velaris audit already says so unprompted - which the model noted
approvingly.
Attribution, added in 3.2 from the maintainer's account: the model was a Claude model.
2.41.2 - A loop no longer hides a divide by zero #
A second model read velaris card, wrote an expense report, then
deliberately removed a requires length(items) > 0 guard and predicted
the compiler would catch the division. It did not.
The cause: the divide-by-zero proof skipped itself whenever any condition on the path mentioned a value a loop had made unknown. Since almost every average sums in a loop before dividing, the check was hiding exactly where it was needed. The divisor itself was perfectly knowable the whole time.
Now the divisor is judged on its own terms and loop conditions stay in
the solver rather than cancelling the proof - dropping them would have
invented counterexamples, keeping them costs nothing.
examples/avg_bad.vel is the shape, rejected before running.
Still runtime-checked: dividing by the length of a list of records, because the prover cannot model those at all. That limit is real and documented; this release fixes the case where the limit was being claimed falsely.
Two models, two programs, two real defects found in one evening. The card is doing what it was built for.
Attribution, added in 3.2 from the maintainer's account: the model was a ChatGPT model (the one in 2.41.1 was a Gemini model).
2.41.1 - The card worked, and the first program it produced found a bug #
Pasting velaris card into a model that had never heard of Velaris
produced a correct program on the first attempt - and that program
crashed the prover. Pushing a record onto a list inside a check
inside a loop reached a comparison that assumed every value has a Z3
sort. Records do not. The rule was already right (a list of records
cannot be modelled, so abandon the proof and let the runtime check);
the translator simply asked the question in a way that crashed instead
of answering it.
examples/rec_push.vel is that program, kept as a regression test.
Worth recording plainly: the card's first user found a real defect within minutes, which is exactly why it was worth building.
Attribution, added in 3.2 from the maintainer's account: the model was a Gemini model.
2.41 - Written by a model, audited by you, run in a box #
Three pieces that make one story.
velaris card prints LLM.md - about 1,500 words containing the
whole language, the ten rules models actually get wrong, every builtin
with its effects and whether it can fail, the error table, and a
complete program to imitate. Paste it into any model and it can write
Velaris that compiles. Until now the language's biggest problem with
generated code was that no model had heard of it.
velaris audit program.vel answers the reviewer's question rather
than the author's: what this program can touch and which functions
reach outside, what it promises and how much of that is proven
before running versus checked while running, what can fail, and the
exact command to run it under a budget. When a program calls Python it
says plainly that an effect budget cannot contain that. --json for
tooling.
agent_loop.py writes a program with a model and iterates against
the compiler: velaris check --json hands back codes, lines and
numbered fixes, which go straight back to the model, up to six rounds,
then the result is audited. Most agent loops iterate against tests;
this one iterates against a proof, which is a stronger signal - the
compiler does not say "a test failed", it says which input breaks which
promise.
2.40.1 - Leading with the sandbox #
The README now opens with the thing that matters most in 2026: an AI wrote you a script, and you can run it anyway because the runtime refuses whatever you did not allow. The proof story - promises checked before the program runs - follows immediately after, where it reads as the reason to believe the first claim rather than competing with it. New hero image to match, and the limits stated in the same breath as the feature.