3.38 says an advisory finding gates nothing, and 3.77 found the answer
layers letting one decide. Reading the code for the change found four
places, not two: the domain layer's verdict; decide(), which combines
the layers by their findings rather than their verdicts, so a
preference printed WARN and an answer whose only critical finding was
advisory printed REJECT (nexa ask exiting 1); worth_escalating; and the
grade a trigger gives one finding, which check reads and the answer
layers did not (sudo's touch and escape clauses, a legacy
_sql_constraints in a bare snippet). Of the three answers 3.77 left
open, the first is taken: advisory decides neither a verdict nor
escalation.
Finding.advisory is set by one function, is_advisory(rule, grade),
which scan_source now calls too, so a file and an answer cannot
disagree. render labels an advisory finding "advisory, gates nothing".
test_pipeline escalates on the onchange question, both answers recorded
verbatim from a nexa run; NEGATIVE_QTY …
layers: a citation written as a list under the label is a claim
cited_ids read the 'Rules applied:' line from the colon on. The prompt
asks for the ids comma separated there, and the 7B also writes the label
on its own with the ids listed under it, one per line. Read from the
line, that list was an empty claim: the knowledge layer printed "the
answer cites no rule ids" under an answer citing four, and returned
UNKNOWN without asking whether any of them exist. An invented id in such
a list was never looked at, where on the line it is a critical finding
and a FAIL.
When nothing follows the colon, the list directly under the label is
the claim, up to the first line that is not a list item. Code blocks are
still never a source, and a rule named under 'Not handled:' is still not
a claim. corpus.report_citations, behind nexa verify, kept its own copy
of the parse with the same gap, and calls cited_ids now.
The recorded answer is a fixture, LOAD_PRICES_BULLETED, from nexa check
on subjects/etl/load_prices.py. Tests pin it, an invented id in its
list, wh…
odoo.sql-constraints-are-ignored-from-19, from OpenSPP 208d9758 (#394):
a _sql_constraints block on a 19.0 module, which Odoo 19 logs once and
never creates, so duplicates were saved silently. An AST pass over the
file plus the manifest's series, legacy_sql_constraints_on_19. Reported
on 19+; silent on 16 to 18, where it is the only spelling; silent when a
walked tree pins no series (odoo core, 174 files); advisory per finding
when nothing was walked at all.
Fires nowhere at HEAD on the three trees, as expected: OpenSPP had
migrated, OCA and core are 18.0. OpenSPP's history holds the evidence:
it catches all four fixed instances (three in ca44b5e3, one in 208d9758)
on the files before their fix and none after, and nexa check reports the
#394 file at the line the fix removed. Graded defect: on 19 the attribute
does nothing, so every finding is a constraint that does not exist.
Also corrects "models.Constraint arrived in Odoo 17": 18.0 core has none
and the 18.0 ORM reference has no Co…
naive-and-aware-datetimes-do-not-compare was a regex on every
datetime.now() and utcnow(). It is an AST pass now,
naive_time_leaves_or_dates_a_day, reporting the two shapes that survived
reading (ARCHITECTURE 3.56): a naive timestamp leaving with no offset (its
isoformat() anywhere, or the value in a response class or returned dict
at an HTTP edge), and the server's UTC date used as a calendar day (a
.date() into a date key, domain, Date default or date attribute, or a
naive now() into a key named `date`). Sources unchanged: date.today() and
fields.Date.today() were measured, not added.
OCA 7 -> 1, OpenSPP 25 -> 5, core 167 -> 2. Every OpenSPP/OCA finding that
went had been read false; the three true positives stay, and both recall
catches still fire on the pre-fix files. Core's removals were triaged and
the 84 near HTTP or a date/serialisation shape read. That reading found two
true positives an HTTP-only draft had dropped: l10n_es_edi_facturae and
l10n_es_edi_tbai write a naive isof…
xml: read the data files, and ask noupdate the question per record
The noupdate cluster was silent for a structural reason. The rule checks
XML, and fieldtest and recall read .py only, so it had never fired at real
code. nexa check was worse: it parsed XML as Python, failed on line 1, and
reported a valid file "not fully checked".
XML is a file kind now. Rules declare applies_to, scan_source runs only the
rules for the kind it reads, and bad XML is a gap with its line. fieldtest
reads XML and counts it apart, so no published Python rate moves; recall
--xml is its own measurement with its own denominator. Demo files are
skipped (Odoo loads them noupdate), and test_*.xml data a manifest lists is
no longer skipped as a test by its name.
The rule was a regex that fired once on <odoo> for any record, views
included, and was cleared by one noupdate anywhere. It is now an expat pass
per <record>, over the models odoo core itself keeps noupdate: parameters
26/28, sequences 16/17, decimal precision 7/8, stages 30/34. Crons came in
on OpenSPP #473 and were na…
precision: read the named false positives, and the misses that named themselves
Six false-positive causes the README had written down, closed by cause rather
than by rule, because the six had three causes between them.
Four rules matched a variable's NAME and read that as evidence about its value.
semantics.py grew a local value trace -- what a scope binds, what each name was
assigned, whether an expression is certainly an integer, whether its root came
from outside the file -- and float-comparison, path-join and decimal-for-money
moved onto it. onchange-is-not-validation had a different fault: the regex read
400 characters past the decorator and could not see where the method ended, so
all three of its false positives were a short onchange followed by an action
method that raises. Scoped to the decorated function by an AST pass.
Docstrings are emptied before matching, quotes and geometry preserved. A
suppression marker that names a worded code now covers only rules sharing one
of those words; an opaque code keeps the line as before, and the gap that
leaves is n…
Four stacks become one. The TypeScript and JavaScript rules are removed, the
eight plain-Python rules are composed into the Odoo stack, and a file in any
other language is refused by name rather than reviewed against rules that
cannot match it.
WHY REMOVE RATHER THAN LABEL. Firing the other twenty-five rules at real code
decided it: JavaScript classified out at 21 true against 22 false, TypeScript
was measured at 3.8/kloc and never classified. A rule that costs nothing at
query time is not free -- it is a claim the README carries and a file `check`
accepts and then says nothing useful about. An empty finding list reads as a
clean bill of health, so `subject.Unreviewable` refuses the file instead.
WHY COMPOSE RATHER THAN DELETE. Selection was strict equality on `stack`, so an
Odoo module got the 19 ORM rules and none of the 8 Python ones -- and Odoo
modules are Python. `fieldtest --stack python` over OpenSPP found 32 hits
`--stack odoo` never reported. "ruff covers it" does not hold a…
review: a pass over the tool itself, and the silence it was keeping
The corpus has been measured from every angle; the code running it had not
been read once. The worst thing here was not a wrong answer but an absent
one.
A SYNTAX ERROR ANYWHERE ERASED EVERY AST FINDING. Each semantic checker
parses for itself and returns [] when that fails, so nine of them agreed
there was nothing to say and `check` printed "no checker findings" and
exited 0. A file with a real stale-compute bug reported it; the same file
with a trailing `def oops(:` reported nothing. scan_source now parses once
and returns a gap with the line number in it.
@api.depends DID NOT COVER ITS OWN PREFIX. Declaring 'line_ids.price'
registers the whole chain, so a body reading rec.line_ids needs nothing
more. Exact string matching called that stale: 42 of the rule's 104 hits
on OpenSPP, 40% of its output. Its own `correct` sample hid it by
declaring the prefix redundantly, which is why validate never saw it.
THE OTHER 25 RULES HAD NEVER BEEN FIRED AT REAL CODE. Every committed
artifact wa…
rules: advisory, and prefer-sql-constraint becomes the first one
3.37 ended with two honest options for a rule that has never once fired on a
line anybody fixed: label it advisory or drop it. This is the first.
`advisory: true` is a new optional schema field meaning the rule states a design
preference rather than a defect. Following it is better; not following it is not
a bug.
The label has teeth. An advisory finding is reported everywhere and gates
nothing:
check its own heading, and out of the exit code
fieldtest counted apart from the per-kloc rate
the semantic layer the finding is recorded, the verdict does not follow it
Reported and NOT dropped, which is the deliberate half. A Python uniqueness
check really does race and the reader still wants to see it; what changes is
that nothing fails because of one. The precedent is `decided` -- printed,
excluded from the rate, never dropped -- and the reasoning is its mirror. A
report that re-raises settled questions gets skimmed. A gate that fails a build
over a pref…
crosswalk: an outside opinion, and three rules with something behind them
Two gaps in `nexa check`, both of them about the corpus rather than the code:
it can only find what it has a rule for, and the worklist that was meant to
say where the missing rules are had been pointing at the wrong place.
AN OUTSIDE OPINION
crosscheck, adversarial, fieldtest and recall all ask whether what we wrote
behaves. None of them can ask whether it is the right set of things 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. `nexa
crosswalk` compares the two corpora without running semgrep -- not installed
here, and an offline tool will not grow a package index for one question.
Each of their patterns becomes the smallest snippet it would match and OUR
checkers run over that, so every pairing rests on a checker firing rather
than on two rule descriptions sounding alike.
their patterns turned into a probe 146 of 146
both corpora fire on the sam…
check: read the file, instead of only asking a model about it
3.26 measured `nexa check` and found it the wrong shape in two ways, neither of
them about the model's quality.
THE CHECKERS NEVER SAW THE FILE
Every layer ran over the model's ANSWER. So the corpus could find six things in
fsm_visit.py -- an incomplete @api.depends on three stored computes, an
@api.constrains reading two fields it does not declare, a uniqueness test racing
in Python -- and the command reported none of them, because the 7B had not
happened to mention them. The same corpus through `nexa fieldtest` found all six
with no model installed.
layers.scan_source is now the one answer to "what do the checkers say about this
source file", and check and fieldtest both route through it. check runs it before
generation and prints the findings with their line numbers. The model is then
shown them and asked to explain and repair them, and to look for what no pattern
can decide -- a better question than "review this file", which invited a rewrite
and reliably got one.
THE EXCERPT W…
context: the version floor is a gap, not a finding
Two of this layer's findings never depended on the answer:
this module targets Odoo 17.0; the corpus was checked against 18.0
tsconfig has strict off, so the compile-time half is not enforced
Both are true and both were in the wrong slot. A finding says something in THIS
ANSWER is wrong or unproven. These say the corpus was verified against a
different series, and that this project has not finished turning strict on --
facts about the corpus and the tree, which hold whatever the answer says.
As findings they were a floor. Every Odoo module off the newest series and every
project mid-migration drew a WARN on every run, so PASS was unreachable for most
real users and the WARN stopped carrying information -- the same argument that
keeps `check` silent about a file that fits.
The deciding detail: 0 of the 15 Odoo rules carries an odoo_versions field, so
_version_applies -- the check that names a retrieved rule which genuinely does
not hold on this series -- has never fired and …
corpus: correct two rules against Odoo 18's source, and settle the cross-hit
The 32 rules had been taken on faith for the whole test pass. Read against
odoo/odoo at 18.0, which is the series corpus_meta claims they were checked
against. Two are wrong there:
recursion-check-on-hierarchy recommended _check_recursion(). In 18.0 that
method warns -- "Since 18.0, one must use not _has_cycle() instead"
-- and delegates. The rule now says _has_cycle(), with the sense
inverted, and names the older spelling for 17.0 and earlier. Its
satisfied_pattern accepts both: code carrying _check_recursion is out
of date, not broken, and this rule is not the place to say so.
ondelete-is-a-decision said "the ORM default is set null". True only for
an optional Many2one. fields.py defaults a required one to restrict
-- and to cascade from a TransientModel -- so the default differs
between two fields declared a line apart, and adding required=True
later silently changes deletion behaviour. Said properly now…
tests: pytest suite seeded from the fixed defects
147 tests, no Ollama, no GPU. Includes a replayed-Ollama fixture so the escalation topology is reachable on purpose rather than by luck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>