β–ˆβ–ˆ   β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ   β–ˆβ–ˆ  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 
   β–ˆβ–ˆβ–ˆ  β–ˆβ–ˆ β–ˆβ–ˆ      β–ˆβ–ˆ β–ˆβ–ˆ  β–ˆβ–ˆ   β–ˆβ–ˆ
   β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β–ˆβ–ˆβ–ˆ   β–ˆβ–ˆ   β–ˆβ–ˆ
   β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β–ˆβ–ˆβ–ˆ   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
   β–ˆβ–ˆ  β–ˆβ–ˆβ–ˆ β–ˆβ–ˆ      β–ˆβ–ˆ β–ˆβ–ˆ  β–ˆβ–ˆ   β–ˆβ–ˆ
   β–ˆβ–ˆ   β–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆ   β–ˆβ–ˆ β–ˆβ–ˆ   β–ˆβ–ˆ
   β–“β–“β–“β–“β–“β–“β–“β–“β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘

Nexa

Offline validation intelligence for Odoo and TypeScript/JavaScript.

A local AI assistant that answers from a schema-validated rule corpus, checks its own output against that corpus, and escalates to a larger model only when a check fails.

Everything runs on your machine. No API keys, no network calls, no client code leaving the laptop.

nexa, with no arguments

No model weights live here. Nexa is the corpus, the checking pipeline, and the CLI β€” a few hundred kilobytes. It orchestrates open models you pull from Ollama's registry (Qwen2.5-Coder, Qwen3-Coder, nomic-embed-text), which keep their own upstream licenses. See INSTALL.md.


What makes it different from a chat window

A general model will confidently tell you to guard a value with @api.onchange. Nexa will not, because "onchange is not validation" is a rule in a corpus, not a hope about the prompt β€” and because Nexa checks its own answer against that rule before showing it to you.

Every answer goes through nine layers. Each one reports a verdict, what it checked, and what it could not check and why:

Layer Catches
Syntactic Code that does not parse or compile
Structural An answer missing the sections the format requires, or echoing the template
Semantic An @api.constrains that omits a field its body reads β€” an AST pass, not a regex
Knowledge A rule id that does not exist, or one recited rather than read
Domain A rule cited correctly and then broken anyway
Context An answer that is right in general and wrong for this module's Odoo series
Empirical An import naming a module that is not installed here
Adversarial A verdict that only holds for one particular formatting
Provenance A verdict nobody could reconstruct later

Most tools stop at the knowledge layer -- do the cited rules exist? Nexa does not, because a model will cite a rule correctly and break it in the same answer β€” every citation checking out while the code does the opposite of what it just quoted.

Citation existence is not rule compliance.

Compliance checking alone is not the end of it either. A regex over generated code cannot see that @api.depends('line_ids') is incomplete when the body reads line_ids.price. That is a set comparison, so Nexa parses the code and does it properly.

It tells you what it did not check

Every layer reports its own coverage. A layer that cannot run says so, and the verdict carries that forward as INSUFFICIENT EVIDENCE rather than quietly counting as a pass β€” because nothing found and nothing to find are different claims.

Confidence comes back as four independent readings β€” structural, semantic, contextual, empirical β€” never averaged into a single score. One number reads high whenever the cheap checks ran, which is exactly the false reassurance this design removes.

REJECT  (INSUFFICIENT EVIDENCE: the domain layer could not run)
  confidence  contextual partial   empirical none   semantic partial   structural checked

Knowing the difference between verified and not yet falsified is what separates a validator you can build on from a green tick you cannot.


Install

See INSTALL.md. Short version: clone this repo, install Ollama, pull three models (~22 GB, from Ollama's registry β€” not from here), run nexa build.

Usage

nexa                      banner and command list
nexa help <command>       detail for one command

Ask

nexa ask odoo "constrain invoice date to the fiscal year of its period"
nexa ask typescript "validate an Odoo res.partner JSON-RPC payload"
nexa ask odoo --deep "should this be a stored computed field or a related field?"

--deep uses the 30B instead of the 7B: roughly 11 tok/s instead of 37, noticeably better at weighing trade-offs.

Run the full pipeline

nexa run odoo "cap a sale order line quantity at available stock"

Retrieves rules, drafts on the 7B, runs all nine layers, and escalates to the 30B once -- but only for findings a larger model could plausibly repair. A missing section label is a real finding that no bigger model fixes better, and spending 11 tok/s to discover that would make the rescue rate meaningless.

Escalation is bounded by declared workflow topology, so a run can never burn a second pass on the slow model.

Review a file

nexa check addons/my_module/models/sale_order.py

The checkers run over your file first, before any model is involved. Patterns and AST passes, each finding carrying the line it sits on. These do not depend on what a 7B says β€” or on whether it says anything useful at all:

6 checker finding(s) in the file
  critical   line 75    odoo.stored-compute-needs-complete-depends
    _compute_labour_cost() reads 'job_id' but @api.depends does not declare it
    -- the stored value goes stale when 'job_id' changes
  critical   line 181   odoo.constrains-must-list-every-field-read
    _check_no_overlap() reads 'end_at' but @api.constrains declares only
    engineer_id -- writing 'end_at' alone skips the check entirely

Then the model is asked to explain and fix those, and to look for what no pattern can decide. It is shown the findings, so it works from them instead of re-deriving them.

A file too big for the context window loses whole symbols, and they are named. The excerpt is chosen by symbol β€” methods, classes, functions β€” and the ones a checker fired on go in first, so the part of the file somebody needs an answer about is the part that survives:

! file is 7929 characters; 11 of its 20 symbols were reviewed
  (not shown: FsmVisit.action_arrive, FsmVisit._timesheet_vals, ...)

The verdict carries it too, so a review of part of a file cannot print a bare PASS:

WARN  (INSUFFICIENT EVIDENCE: 9 of 20 symbols were not reviewed)

--no-model stops after the checkers. 0.26 seconds, no Ollama, and the same output every run β€” because what the generative half adds on top of the checkers is small and does not hold still: 1 citation across 25 reviews on one run, 3 on the next, and not the same rules either time. On those same two runs the deterministic half produced the identical 23 findings both times.

That is the argument. A person reading a file wants the part no pattern can decide, and it is worth the wait. A CI gate wants the part that is the same every time, and three of five files gave different verdicts between those two runs. Those had been the same command.

It prints no verdict, because nothing generated an answer to have a verdict about. A clean run says no pattern fired in those words, and exits 1 if one did.

For a .py file the extension only really says not TypeScript, so the file decides which corpus applies: an odoo import or a __manifest__.py above it means the Odoo rules, anything else means the plain-Python ones.

checking upload_handler.py as python

Rules retrieved:
  0.698   [critical] python.assert-is-not-validation
  0.685   [critical] python.eval-and-pickle-execute-their-input
  0.674   [high]     python.path-join-does-not-contain-a-path
  0.643   [critical] python.subprocess-shell-true-is-injection

--stack overrides the detection in either direction. Unlike nexa ask, this one has surroundings to read: the nearest __manifest__.py gives the Odoo series, the file gives its model names and existing imports, the nearest tsconfig.json says whether strict is on. Those facts go into the question and into the context layer.

Where they say the corpus was verified somewhere else, that is reported as a gap, not a finding β€” it is true of the corpus whatever the answer says, and a verdict that fires on every run in every project off the newest series stops being read:

Not checked
  context      version-specific behaviour
               this module targets Odoo 17.0 and the corpus was checked against
               18.0, so anything version-specific in these rules is unverified here

A rule that was actually retrieved and genuinely does not apply to this series is a different claim, and stays a finding with the rule id attached.

Ask about your own code

nexa index C:/work/our-standards
nexa advise "how do we handle schema migrations here?"

index embeds a folder of your own source and documentation into a separate table from the rule index. advise reads it back.

This is the one command not grounded in the rule corpus, so nothing it says is checked against a rule id or a violation pattern β€” it answers from whatever you indexed, and says so when the material does not cover the question. Use ask, run and check for anything you want verified.

Show the record behind a verdict

nexa evidence
nexa evidence ask-34cf01432145

Every run writes one bundle: the corpus hash, the rules retrieved with their scores and which arrived by pin, every layer's verdict and findings, the checker versions, the model and its options, and the timings.

A verdict nobody can reconstruct is a claim, not a result. Question and answer text are omitted unless LOCALAI_TELEMETRY_CONTENT=1, for the same reason telemetry omits them β€” this runs on a machine used for client work. NEXA_EVIDENCE=0 turns bundles off.

Explore the corpus

nexa rules
nexa rules onchange
nexa rules --full odoo.constrains-ignores-dotted-names

β—† marks a rule with an automated check. β—‡ marks one you have to read the answer for yourself. PIN marks a rule injected into every answer regardless of retrieval.

Keep it healthy

nexa doctor

Checks Ollama, the models, GPU residency, and the traps that silently cost ~3x throughput on a 6 GB card. Run this first whenever something feels slow.

nexa models
nexa report

Change the rules

Edit corpus/build_corpus.py, then:

nexa build

That regenerates the rule JSON, validates it, measures pattern false positives, attacks the checkers, re-embeds the retrieval index, and rewrites the Modelfiles. It prints the ollama create commands to register the updated models.


The corpus is the source of truth

corpus/rules/*.json holds the rules as structured data β€” a citable id, two graded axes, the failure it prevents, a wrong/correct code pair, and where possible an upstream documentation URL and a regex that detects the violation in generated code.

The axes are separate because they answer different questions. severity is how bad it is if this happens; likelihood is how likely a finding is the bug, graded from what nexa recall --aim measured against real fix commits rather than from judgement. A rule can be severe and rarely the point β€” a Python uniqueness check really does race, and across 160 fixes not one of them was about that β€” and one field could not say so.

Everything else is derived and must never be hand-edited:

corpus/rules/*.json --+--> modelfiles/gen-*.Modelfile   (system prompts)
                      +--> bin/index.sqlite             (retrieval)
                      +--> every answer                 (grounding + checking)

Same principle as z.infer<typeof Schema>: one definition, derived artifacts, no drift.

Current coverage

Rules 44 β€” 19 Odoo, 10 TypeScript, 8 Python, 7 JavaScript
Critical 17
Machine-checkable 41 (93%) β€” 32 by pattern, 9 by AST
Traceable to upstream docs 41 (93%), each audited for topic fit
Pattern false positives 0 cross-hits across the corpus
Recall under meaning-preserving rewrite 101/101
Satisfied patterns bypassable by a comment 0 of 14
Retrievable by their own probe 44 of 44, at top 3
Hit rate on 34,550 lines of reviewed OCA code 3.9 per 1000 lines
Hit rate on 229,600 lines of OpenSPP (Odoo 19) 1.4 per 1000 lines, +26 another tool had already flagged, +71 advisory
Recall against real Odoo bugfixes 1 of 56 (OCA), 6 of 160 (OpenSPP). See below.
Verdict stability, 12 questions x 5 runs 12/12 same verdict every run
Escalation rescued a bad draft 11 of 21, 95% CI 32–72%
nexa check, 5 files x 5 runs 23 findings reported, in 5 of 5 files β€” every one the corpus can make
The same 23, on a second run of the identical command 23 β€” the deterministic half does not move
Rules the model added beyond the checkers 1 on one run, 3 on the next, out of 25 reviews
nexa check verdict stability 4 of 5 files on one run, 3 of 5 on the next
nexa check --no-model 0.26s, identical output every run

41 of 44 rules carry an automated check. The remaining three are decidable only with context a snippet does not contain, so they return no verdict β€” never a false pass β€” and nexa coverage names each one and why.


The checkers are tested, then attacked

Patterns are tested against their own samples

Every rule carries a wrong and a correct sample, so nexa validate tests each pattern against both. Three faults are hard errors:

  • a pattern that does not match its own wrong sample β€” it catches nothing
  • a pattern that fires on its own correct sample β€” it cries wolf on good code
  • a satisfied_pattern that clears its own wrong sample β€” inert by construction

AST checkers owe the same contract and are self-tested the same way.

Cross-checking measures false positives

nexa crosscheck fires every checker at every other rule's correct sample. That is the false-positive rate, and it decides whether a checker gets trusted or ignored.

Then the checkers get attacked

nexa adversarial

validate and crosscheck both compare a checker against code written alongside it, by the same hand, in the same sitting. Neither asks what it takes to get bad code past it. Three probes do:

Probe Question
evasion Reformat the wrong sample without changing its meaning. Does the checker still see it?
counterexample Can a satisfied_pattern be cleared by a comment quoting the fix instead of applying it?
contradict When one rule fires on another's correct sample, is that breadth or do the two rules disagree?

This runs as part of nexa build. A checker that has never been attacked is a checker nobody has tested.

The probes hold the corpus to a measured standard, and it currently meets all three: 64/64 rewrites still caught, 0 of 12 satisfied patterns bypassable by a comment, 0 contradictions between rules. Those numbers are regenerated on every build, so a checker cannot quietly weaken.

And then pointed at code nobody wrote for them

validate, crosscheck and adversarial all compare a checker against the 32 samples in this corpus β€” written by the same hand that wrote the checkers. Passing proves self-consistency and nothing else.

nexa fieldtest ../server-tools ../account-financial-tools

fires every checker at a tree of somebody else's source and reports the hit rate per thousand lines. On code that already works, every hit is a question about the checker, not about the code.

Against OCA/server-tools and OCA/account-financial-tools β€” 606 files, 34,550 lines of reviewed, merged Odoo:

Hits Per 1000 lines
Before 397 11.3
After narrowing five checkers 135 3.9

What the first run found:

  • onchange-is-not-validation fired on all 16 @api.onchange declarations that had no constraint elsewhere in the file. Not one was validating anything β€” they set a field from a provider, fill a default, narrow a domain. It now requires the onchange to actually try to enforce something. 16 β†’ 0.
  • stored-compute-needs-complete-depends demanded @api.depends entries the ORM would refuse: paths ending in .id, and fields the method computes itself. 121 β†’ 95. Its true positives stand β€” auditlog's stored display_name reads user_id.name and declares only user_id, so the name goes stale on a rename.
  • prefer-sql-constraint read a guard as the whole rule. if record.storage == "url" and not record.url: β€” the first half is an ast.Compare and the second is not, so it concluded the function "does nothing else". 8 β†’ 2, and both survivors are true positives.
  • ondelete-is-a-decision is the one left loud on purpose. Odoo defaults a required Many2one to ondelete='restrict', which is what the rule recommends, so 56 of its 198 hits were fields already doing the right thing and are now exempt. The remaining 142 are optional Many2ones genuinely defaulting to set null β€” true positives at 4.1 per 1000 lines, and the rate to expect on a first run against an existing codebase.

What it misses

Every number above is a precision number. A corpus containing nothing at all would score perfectly on all of them, so none of them is evidence that the corpus is worth having.

nexa recall ../server-tools ../account-financial-tools

reads a repository's git history, finds the commits where somebody fixed something, and runs the checkers over each file as it stood before the fix and again after. A finding that was there before, is gone after, and sat on a line the fix actually changed is the corpus noticing the bug that was really fixed.

Across 385 fix commits in those two repositories, 56 of which touched a construct the corpus covers:

Caught 1
Missed 55
Rules that caught anything 1 of 14

That last condition, on a line the fix changed, is the entire measurement. Without it, any refactor that moves code counts as a catch. The first version of this tool did not have it and reported 12. Reading the commit subjects killed the result: "fix image url in locales with thousand separator", "poor performance updating assets massively", "fix JS crash in analytic distribution". Not one was an ondelete bug. The checker had simply gone quiet because a dereference moved.

What this means, stated plainly. Nexa is precise and narrow. It is right about what it describes, and it describes very little of what actually goes wrong. The 55 misses include bugs squarely inside its own stated territory:

  • "remove sudo() when importing record" β€” sudo-bypasses-record-rules exists
  • "prevent removal of x2many values from inaccessible companies" β€” a multi-company leak, which check-company-on-relations is about
  • "log computed fields stored in db as expected" β€” a stored compute

What this does not mean. The denominator is generous. Many of those 56 commits touched a covered construct while fixing something else entirely, so 2% understates recall against validation bugs specifically. Two repositories in one ecosystem is also a narrow sample. The number is a floor, and it is the first floor this project has had.

The missed list is the more useful output. Every entry is a bug somebody actually fixed that the corpus had nothing to say about, which makes it a candidate rule with a real incident behind it rather than another one written from an opinion.

What the embedder drops

Retrieval embeds the question and sorts the rule vectors against it. For a long file that question is large, and nomic-embed-text truncates what will not fit without saying so. Appending a marker and watching the vector fail to move puts the cut at ~7,400 characters β€” the last 6% of the longest subject's question, not the back half anybody would have guessed.

nexa eval --compare-retrieval

scores a compact description of a file against the whole question, using the rules the checkers proved apply as ground truth. The compact query wins 15–14 out of 19: one rule, on one file, which is a coin toss with a table around it. So it is not adopted. The function stays as the thing that comparison compares, and the committed behaviour waits for a number that says something.

What is adopted needed no measuring: a rule the checkers already fired on at line 75 goes into the grounding without asking similarity to guess at it.

The missed list is the worklist

nexa recall ../OpenSPP2-19.0 --harvest

clusters the fixes the corpus did not flag on the line of the fix by the construct each diff touched, so the reading order is the writing order. Against OpenSPP 2 (Odoo 19) β€” a second ecosystem on a newer series than the OCA repositories:

Fix commits examined 406
…touching a construct the corpus covers 160
Caught 6
Recall against in-scope fixes 4%

Two repositories, two ecosystems, 2–4% both times. The floor is a floor β€” and the clustering says where to dig:

        missed  caught  spoke  silent
.sudo(      70       6     47      23
api.depends 23       0     16       7
api.constrains 16    0     12       4
ondelete    13       0     10       3

spoke is the column that changed the reading. A miss is not automatically a blind spot: a checker firing somewhere else in the same file means the rule exists and is aimed wrong, which wants a sharper rule rather than a new one. Ninety-two of the 154 misses are that. The first version of this table reported 21x api.depends (nothing caught here), which reads as a missing rule β€” and the checker was in fact firing on sixteen of those files while the fixes went elsewhere.

Reading the diffs behind the seven that really were silent produced one rule, odoo.depends-must-not-name-unread-fields. Two of the remaining rules in this corpus came from the crosswalk below.

An outside opinion, which is the one thing the corpus cannot give itself

nexa crosswalk ../OpenSPP2-19.0/.semgrep/odoo-security.yml --checkout ../OpenSPP2-19.0

Every other number here tests whether what we wrote behaves. None of them can ask whether it is the right set of rules to have written, because a rule nobody thought of is invisible to every check that reads the rules. OpenSPP runs semgrep against 44 hand-written Odoo rules of its own β€” somebody else's judgement, on the same platform, formed without reading ours.

Semgrep is never run: each of their patterns is turned into the smallest snippet it would match and our checkers run over that, so a pairing rests on a checker firing rather than on two rule descriptions sounding alike.

first run now
Their patterns turned into a probe 146 of 146 146 of 146
Both corpora fire on the same construct 6 10
Theirs fires, nothing here does 38 34
Lines their analyser fired on (# nosemgrep: markers) 631 631
…where a checker here also fires 20 20

The agreement moved from 6 to 10 because the gap it named got closed, not because anything was re-scored β€” the first six were all reached by the two Python rules about executing input, and not one by an Odoo rule.

Most of that gap is a different subject, not a missing rule: theirs is about security and this one is about ORM correctness. Two entries were neither, and are now rules β€” odoo.commit-inside-a-loop-splits-the-transaction and odoo.sql-needs-query-parameters. The commit rule fires exactly once in 229,600 lines of OpenSPP, inside a loop their analyser had flagged too: # nosemgrep: odoo-commit-in-loop β€” batch consent expiry cron. Two analysers, one construct, and a person who had already decided.

They do not anchor to the same line, though, and that is worth knowing. Semgrep matches the whole loop and its marker sits on the for; this checker reports the commit() ten lines inside it. scan_source only reads a marker on the finding's own line, so it does not carry across β€” deliberately, because a marker that silences findings it was not written about is the mute button this project will not build.

Every version claim is checked against two kinds of evidence

python corpus/check_sources.py            # what the docs describe
python corpus/check_sources.py --source   # what the source defines

Each Odoo rule declares the series it holds for. The first mode checks that claim against the documentation: 42 of 45 versioned rule-series cite a page that exists and names the API, 2 more are tutorials that moved while the API stayed, and 1 is not supported by any 16.0 page.

Documentation has a blind spot, and the corpus had fallen into it β€” a function Odoo shipped and never described reads as missing. So the second mode reads odoo/odoo itself and asks whether the API is defined in that series' module: def float_compare( in odoo/tools/float_utils.py, not the word anywhere on a page.

Rule-series defined in source across 16.0, 17.0 and 18.0 54 of 54

That closes it. No version claim in this corpus rests on an assertion, and the one documentation gap now carries a measurement instead of a belief: float_compare's signature is byte-identical in all three series. Neither mode is a test run β€” a definition says the API existed and was spelled the same way, not that it behaved identically β€” and both say so in their own output.

Authoring-time and the only thing here that touches the network. Nothing at query time depends on it.

Is every rule findable?

A rule the retrieval never surfaces is unreachable whatever it says. Every rule carries a probe β€” one question phrased the way somebody meets that problem β€” and

nexa eval --retrieval

asks each rule its own probe and reports whether it comes back. No generation, so it needs only the embedding model and finishes in seconds.

All 44 rules are retrievable at the operating TOP_K of 6, and at top 3. Two were not, and both failed the way a rule does: correctly worded, in the vocabulary of somebody who already knows the answer. no-monkey-patching did not answer "change what a core sale.order method does"; unknown-not-any-at-boundaries did not answer "what type should a webhook payload have". Reworded, measured again, both found.

The report states its own limits. Pinned rules are prepended regardless of score, so ranking against them measures the pin; JavaScript has 7 rules against a TOP_K of 6, so passing there is nearly free, and the header says so rather than printing 7/7 and letting it read as a result.

And what does nexa check actually report?

Every number above was measured on nexa ask. nexa check is the command you would point at your own file, and nothing measured it β€” it was assumed to inherit ask's numbers because it ends in the same call.

nexa eval --check

runs a fixed set of files the way nexa eval runs a fixed set of questions, and adds the four things only a file can produce: which corpus it was routed to, how much of it fitted, what the surroundings on disk let the context layer say, and whether the answer mentions the file at all.

Five files, five runs each:

Findings reported about the files 23, in 5 of 5 files
…which is what the corpus can find in them 23 β€” nexa fieldtest, no model
WARN 18 of 25, 95% CI 52–86%
REJECT 7 of 25, 95% CI 14–48%
PASS 0 of 25
Same verdict every run 4 of 5 files
Rules the model cited that no checker found 1, across 25 reviews

The deterministic half does everything it is capable of. Those 23 findings are exactly what nexa fieldtest extracts from the same five files with no model installed: an incomplete @api.depends on three stored computes, an @api.constrains reading two fields it does not declare, shell=True, a bare except, a mutable default argument, JSON.parse unchecked, a rejected promise nothing catches. Each carries a line number and none depends on what a 7B says.

The first run of this eval reported none of them, because every layer ran over the model's reply and nothing had ever read the file.

The generative half adds close to nothing, and now there is a number. Across 25 reviews the model cited exactly one rule the checkers had not already found. It does make the answers more about the file than they were β€” feeding it the findings raised the symbols it names from 34 to 90 across the set β€” but what it adds to the corpus's own verdict is one citation in twenty-five.

That is worth knowing rather than worth hiding. It says where to spend effort: another rule in the corpus is worth more than another prompt.

Coverage is evidence. fsm_visit.py is 7,929 characters against a 6,000-character budget, so nine of its twenty symbols do not fit β€” but the excerpt is chosen by symbol and the flagged ones go first, so _check_no_overlap, its last method and the home of three of its six findings, is reviewed rather than cut off 1,900 characters early. What is left out is named, and the verdict says so:

WARN  (INSUFFICIENT EVIDENCE: 9 of 20 symbols were not reviewed)

That file used to PASS on five runs of five, because nothing could see it.

How it got here. The first run of this eval found check the wrong shape in two ways, neither about the model's quality. The checkers ran over the model's answer, so the corpus could find six things in a file and the command reported none of them. And the excerpt was the first 6,000 characters, which cut one subject 1,900 characters above the method carrying half its findings. Both are fixed, and the table above is what that is worth.

subjects/ is committed so the run repeats. Point it at your own tree for the half a written-for-the-measurement set cannot tell you:

nexa eval --check addons/my_module --limit 6

Environment

Everything below is optional; unset, each is the default it has always been.

Variable Effect
OLLAMA_HOST Where Ollama is. 11500, box:11434 or https://gpu.lan all work β€” the same spellings the server accepts. Default http://127.0.0.1:11434; nexa doctor reports which it used and whether you set it.
NEXA_EVIDENCE=0 Write no evidence bundles.
NEXA_EVIDENCE_DIR Where bundles go. Default bin/evidence/.
NEXA_EVIDENCE_KEEP How many to keep. Default 200, pruned oldest first.
LOCALAI_TELEMETRY=0 Record no telemetry at all.
LOCALAI_TELEMETRY_FILE Where the JSONL goes. Default bin/telemetry.jsonl.
LOCALAI_TELEMETRY_CONTENT=1 Record question and answer text. Off by default: this runs on a machine used for client work.
NO_COLOR / TERM=dumb No ANSI escapes. Colour is also off automatically when stdout is not a terminal.
NEXA_PYTHON Which interpreter the ./nexa launcher uses.

Requirements

  • NVIDIA GPU with 6 GB VRAM or more (developed on an RTX A3000 Laptop)
  • 32 GB system RAM for the 30B escalation model; 16 GB works without it
  • Python 3.10+ β€” stdlib only, nothing to pip install
  • Ollama 0.30 or later
  • ~25 GB free disk

Optional: LLM Workflow Router for nexa run. nexa ask works without it.


Documentation

  • INSTALL.md β€” setup from scratch
  • ARCHITECTURE.md β€” how it works, every design decision and why it was made, and an honest account of what it cannot do
  • LICENSING.md β€” what is free and what needs a license

License

Source-available under the PolyForm Noncommercial License 1.0.0 β€” free to read, run, modify and evaluate for any noncommercial purpose. Commercial and client-work use requires a license; see LICENSING.md.

Nexa runs entirely on your own hardware, so a commercial license carries no per-seat metering and no dependency on anything of mine staying online. Nothing phones home.

The models Nexa orchestrates are pulled from Ollama's registry and carry their own upstream licenses.

Copyright Β© 2026 Doby Baxter


What you get

Nexa is built to close one gap precisely: the distance between an answer that sounds right and an answer that has been checked. Every response is grounded in your rules, verified against them by nine independent layers, and returned with its own coverage stated.

It is free, it runs entirely offline, your client code never leaves the machine, and the standard it applies is the one you wrote β€” not a general impression of good practice.

Read the output. Two parts are worth more than the verdict itself: the model's own "Not handled:" section, and the "Not checked" list the layers print beneath it.

The full design record β€” every decision, the evidence behind it, and the trade-offs taken β€” is in ARCHITECTURE.md.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support