DBax127 commited on
Commit
3ac608a
·
1 Parent(s): 2fcf136

check: the eval rows re-measured on the subjects left, and checked

Browse files

README's nexa check rows rested on eval-check-2026-09.json of
2026-09-14, and two of its five subjects went with the TypeScript and
JavaScript corpora in 8d155aa. Re-measured over the three left, five
runs each: 10 findings in 3 of 3 files, the same on every run, and 3
advisory reported apart; PASS, WARN and REJECT 5 each; 1 of 3 files
with one verdict on every run; 1 rule cited beyond the checkers in 15
reviews, the pinned one, on the plain script.

Reading the first run found the eval miscounting. file_finding_count
kept advisory hits, though its comment said live findings only, so it
read 13 where check prints 10 and fieldtest counts 10. It counts live
findings now, and advisory ones in a field of their own that the report
prints the way check does. The committed run is the second, made after
that and the citation fix in the previous commit (ARCHITECTURE 3.74).

figures.py reads both eval runs. check.* comes from the check rows, and
--check fails on a row for a file the repository no longer has, and on
files reviewed unevenly. retrieval.* comes from retrieval-2026-09.json,
the rows of nexa eval --retrieval --json, which now writes a head with
k, the embedder and the corpus hash it ranked. --check fails on a rule
the run never asked, and on a corpus that moved under it, which a rule
id cannot show when only the wording changed. The README rows are
marked.

The plain-script sample is re-captured from subjects/etl/load_prices.py,
since upload_handler.py is not in the repository. nexa check --no-model
is under a second rather than 0.26 s: 0.42 to 0.52 s on the subjects
and 0.78 s on area_core.py. The nexa ask stability and escalation rows
now carry their date and say that half that run was TypeScript and
JavaScript; re-running them needs the 30B. subjects/README.md loses its
stale "nineteen" ORM rules, and nexa help check and one comment lose the
old counts. ARCHITECTURE 3.75.

812 passed, 2 skipped; every marked figure agrees.

ARCHITECTURE.md CHANGED
@@ -3856,6 +3856,99 @@ fewer. The exemption stays whole, now on a measurement rather than on a premise
3856
  and a test pins it. Nothing moves on the three trees, and all 288 hand-classified true
3857
  positives are still reported.
3858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3859
  ## 4. Performance traps, all found by measurement
3860
 
3861
  Within four instrumented runs the report showed the 7B at ~10 tok/s against a
@@ -4165,9 +4258,9 @@ produces on those files with no model involved.
4165
  their line numbers, and the excerpt is chosen by symbol so the method carrying
4166
  three of them is reviewed rather than cut off. What remains true is the shape of
4167
  the risk: the deterministic checkers carry the weight on real code, and the
4168
- generative half still degrades as the file grows -- a median of one symbol named
4169
- out of nineteen on a 7,929-character file. The difference is that the tool no
4170
- longer depends on that half to find anything.
4171
 
4172
  ### Untested platforms
4173
 
 
3856
  and a test pins it. Nothing moves on the three trees, and all 288 hand-classified true
3857
  positives are still reported.
3858
 
3859
+ ### 3.74 A claim written as a list
3860
+
3861
+ **Measured 2026-09-27.** `layers.cited_ids` decides what an answer claims to have applied, and
3862
+ it read the `Rules applied:` line from the colon on. The prompt asks for the ids comma separated
3863
+ on that line, and the 7B does not always oblige. Re-capturing the README's plain-script sample
3864
+ (3.75), it wrote the label on its own and listed its four ids under it, one per line. That
3865
+ answer is a fixture now, `LOAD_PRICES_BULLETED`. Read from the label's line, the list was an
3866
+ empty claim: the knowledge layer printed "the answer cites no rule ids" under an answer citing
3867
+ four, and returned UNKNOWN without asking whether any of them exist.
3868
+
3869
+ The second half is the one that mattered. On the line, an id with no rule behind it is a
3870
+ critical finding and a FAIL. In a list it was never looked at, so an invented citation in that
3871
+ shape went through. `corpus.report_citations`, behind `nexa verify`, kept its own copy of the
3872
+ parse with the same gap, and calls `cited_ids` now.
3873
+
3874
+ The claim is the label's line and, when nothing follows the colon, the list directly under it.
3875
+ Blank lines before the first item are skipped, and the first line that is not a list item ends
3876
+ it. A code block is still never a source of citations, and a rule named under `Not handled:` is
3877
+ still not a claim to have applied it. Tests pin the recorded answer, an invented id in its list,
3878
+ and where the list ends.
3879
+
3880
+ On the check subjects, the first run of 3.75, made before the fix, had 4 of its 15 reviews
3881
+ citing nothing the parser could read: all four on `load_prices.py`, and all four PASS. The
3882
+ committed run, made after it, has none. The verdicts did not follow the citations, though.
3883
+ `load_prices.py` passed on all five committed runs with its citations read, since UNKNOWN on the
3884
+ knowledge layer never stood in the way of a PASS. What those answers escaped was the check on
3885
+ what they cited, not a verdict.
3886
+
3887
+ ### 3.75 The check and retrieval rows, read from their runs
3888
+
3889
+ **Measured 2026-09-27.** The README's `nexa check` rows rested on `eval-check-2026-09.json` from
3890
+ 2026-09-14. Two of its five subjects, `subjects/web/src/partners.ts` and `board.js`, went with the
3891
+ TypeScript and JavaScript corpora in 8d155aa, and `nexa check` refuses both now. The rows still
3892
+ agreed with the artifact, and nothing read the artifact, so nothing said so. The retrieval rows
3893
+ had the opposite weakness: re-measured by hand whenever a rule changed, with no run committed to
3894
+ check them against, and item 12 found them a rule behind.
3895
+
3896
+ The check run is redone over the three subjects left, five runs each, and committed over the old
3897
+ one. The old artifact held the second of the two runs the README quoted:
3898
+
3899
+ | | 2026-09-14, five files | 2026-09-27, three files |
3900
+ |---|---|---|
3901
+ | Reviews | 25 | 15 |
3902
+ | Findings reported about the files | 23, in 5 of 5 | 10, in 3 of 3, and 3 advisory apart |
3903
+ | The same findings on every run | 5 of 5 files | 3 of 3 files |
3904
+ | PASS / WARN / REJECT | 0 / 12 / 13 | 5 / 5 / 5 |
3905
+ | Same verdict every run | 3 of 5 | 1 of 3 |
3906
+ | Rules the model cited beyond the checkers | 3 | 1 |
3907
+
3908
+ **Two counts in the eval were wrong, and neither was the tool's.** `file_finding_count` dropped
3909
+ suppressed hits and kept advisory ones, though its comment said live findings only: the advisory
3910
+ grade came after it. On these files that read 13 where `check` prints 10 and `fieldtest` counts
3911
+ 10, with 3 advisory apart. It counts what `check` counts now, and advisory hits go in a field of
3912
+ their own that the report prints the way `check` does. The other was the citation parse, 3.74.
3913
+ Both were found reading the first run, which is why the committed run is the second.
3914
+
3915
+ **What the run says.** The deterministic half is what it was: the same ten findings on every
3916
+ review, and the same ten `fieldtest` finds with no model. `load_prices.py` carries four of them
3917
+ where it carried eight, since three of the plain-Python rules were narrowed in between (3.47,
3918
+ 3.61). The model
3919
+ cited one rule beyond the checkers in fifteen reviews, `check-constraint-cannot-cross-tables`,
3920
+ the rule pinned into every review, and it cited it on the plain script, where there is no
3921
+ constraint for it to be about. The verdicts spread evenly, and only `load_prices.py` held one for
3922
+ all five runs: PASS, for an answer that reproduces the file with its defects repaired. The file
3923
+ itself still exits 1. `--no-model` took 0.42 to 0.52 seconds on the subjects and 0.78 on
3924
+ `area_core.py`, against 0.26 on 2026-09-14, so the README says under a second.
3925
+
3926
+ **figures.py reads both runs now.** `check.*` comes from `eval-check-2026-09.json`: files, runs
3927
+ and reviews, the findings taken once per file, whether they came out the same on every run, the
3928
+ verdict counts, stability, and citations beyond the checkers. `--check` fails on a row naming a
3929
+ file the repository no longer has, which is the drift this entry started from, and on files
3930
+ reviewed unevenly.
3931
+
3932
+ `retrieval.*` comes from `retrieval-2026-09.json`, the rows of `nexa eval --retrieval --json`,
3933
+ which now writes a head beside them: the depth it ranked at, the embedder, and the corpus hash
3934
+ it ranked against. One run at the operating depth answers both published figures, since
3935
+ `retrieve()` fills its slots in one fixed order, and a separate top-3 run agreed with it on all
3936
+ 28 rules. `--check` fails when a rule is missing from the run and when the hash differs from
3937
+ `corpus_meta.json`. The ids alone would have caught item 12's drift, a rule added, and missed
3938
+ this entry's other change, a rule reworded.
3939
+
3940
+ That rewording is `constrains-ignores-dotted-names`, whose title said "silently ignores". Odoo
3941
+ 18 logs "@constrains parameter ... is not a field name" when the model first collects its
3942
+ constraints (3.70), so the title reads "ignores dotted field names without an error". Retrieval
3943
+ was measured before and after it: 28 of 28 at top 6 and 26 at top 3 both times, and no rule
3944
+ changed rank. The rule itself stays second for its own probe, behind the pinned rule, at a
3945
+ score of 0.700 where it was 0.694.
3946
+
3947
+ **Left.** The README's `nexa ask` stability row and escalation row rest on `eval-2026-09.json`
3948
+ of 2026-09-14, and six of its twelve questions were TypeScript and JavaScript: 10 of the 21
3949
+ escalations and 5 of the 11 rescues. The rows now carry that date and say so. Re-running them
3950
+ needs the 30B for the escalations, which is a session of its own.
3951
+
3952
  ## 4. Performance traps, all found by measurement
3953
 
3954
  Within four instrumented runs the report showed the 7B at ~10 tok/s against a
 
4258
  their line numbers, and the excerpt is chosen by symbol so the method carrying
4259
  three of them is reviewed rather than cut off. What remains true is the shape of
4260
  the risk: the deterministic checkers carry the weight on real code, and the
4261
+ generative half still degrades as the file grows -- a median of two symbols named
4262
+ out of nineteen on a 7,929-character file, in the run of 2026-09-27 (3.75). The
4263
+ difference is that the tool no longer depends on that half to find anything.
4264
 
4265
  ### Untested platforms
4266
 
README.md CHANGED
@@ -36,7 +36,7 @@ def _compute_area_level(self):
36
  The method reads `parent_id.area_level` and declares only `parent_id`. Move a district to
37
  another region and the district's own level is recomputed; every area beneath it keeps the
38
  old one, stored, with nothing to say it changed. The GIS API groups areas by that level.
39
- `nexa check --no-model` names it, on line 84, in a quarter of a second.
40
 
41
  It runs entirely on your machine. No API keys, no network at review time, no client
42
  code leaving the laptop — which for a consultancy under an NDA is not a preference.
@@ -431,15 +431,14 @@ The verdict carries it too, so a review of part of a file cannot print a bare PA
431
  WARN (INSUFFICIENT EVIDENCE: 9 of 20 symbols were not reviewed)
432
  ```
433
 
434
- **`--no-model` stops after the checkers.** 0.26 seconds, no Ollama, and the same
435
  output every run — because what the generative half adds on top of the checkers is
436
- small and does not hold still: **1** citation across 25 reviews on one run, **3** on
437
- the next, and not the same rules either time. On those same two runs the deterministic
438
- half produced the identical 23 findings both times.
439
 
440
  That is the argument. A person reading a file wants the part no pattern can decide,
441
  and it is worth the wait. A CI gate wants the part that is the same every time, and
442
- three of five files gave different verdicts between those two runs. Those had been
443
  the same command.
444
 
445
  It prints **no verdict**, because nothing generated an answer to have a verdict
@@ -460,18 +459,31 @@ rules have nothing to match in it, the <!--fig:corpus.origin.python-->8<!--/fig-
460
  which of the two it is rather than leaving the reader to infer it from a short list:
461
 
462
  ```
463
- checking upload_handler.py as odoo
464
- ! no Odoo imports here and no __manifest__.py above it -- the 20 ORM rules
465
- have nothing to match, so this file is effectively reviewed against the
466
- 8 plain-Python ones
 
 
 
 
 
 
 
467
 
468
  Rules retrieved:
469
- 0.698 [critical] python.assert-is-not-validation
470
- 0.685 [critical] python.eval-and-pickle-execute-their-input
471
- 0.674 [high] python.path-join-does-not-contain-a-path
472
- 0.643 [critical] python.subprocess-shell-true-is-injection
 
 
473
  ```
474
 
 
 
 
 
475
  Unlike `nexa ask`, this one has **surroundings to read**: the nearest
476
  `__manifest__.py` gives the Odoo series, the file gives its model names and existing
477
  imports, `ir.model.access.csv` and the record rules in XML say what Python cannot.
@@ -608,7 +620,7 @@ Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, n
608
  | Pattern false positives | 0 cross-hits across the corpus |
609
  | Recall under meaning-preserving rewrite | 82/82 |
610
  | Satisfied patterns bypassable by a comment | 0 of <!--fig:corpus.satisfied_patterns-->1<!--/fig--> |
611
- | Retrievable by their own probe | 28 of 28 at top 6, 26 of them at top 3 |
612
  | Hit rate on <!--fig:oca.lines-->33,960<!--/fig--> lines of reviewed OCA code | <!--fig:oca.rate-->3.0<!--/fig--> per 1000 lines: <!--fig:oca.findings-->101<!--/fig--> findings |
613
  | Those findings, read by hand | **<!--fig:oca.read.true-->85<!--/fig--> true positives**, <!--fig:oca.read.false-->6<!--/fig--> false, <!--fig:oca.read.undecidable-->7<!--/fig--> undecidable from one file, <!--fig:oca.unread-->3<!--/fig--> not yet read |
614
  | Hit rate on <!--fig:openspp.lines-->226,937<!--/fig--> lines of OpenSPP (Odoo 19) | <!--fig:openspp.rate-->0.7<!--/fig--> per 1000 lines: <!--fig:openspp.findings-->161<!--/fig--> findings, +<!--fig:openspp.decided-->127<!--/fig--> another tool had already flagged, +<!--fig:openspp.advisory-->82<!--/fig--> advisory |
@@ -619,13 +631,13 @@ Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, n
619
  | Hit rate on <!--fig:core.lines-->559,663<!--/fig--> lines of odoo core 18.0 | <!--fig:core.rate-->4.2<!--/fig--> per 1000 lines: <!--fig:core.findings-->2,357<!--/fig--> findings, <!--fig:core.read-->89<!--/fig--> of them read by hand: <!--fig:core.read.true-->45<!--/fig--> true, <!--fig:core.read.false-->44<!--/fig--> false |
620
  | Hand-classified true positives still reported | **<!--fig:survival.found-->288<!--/fig--> of <!--fig:survival.true-->288<!--/fig-->**, across <!--fig:survival.artifacts-->9<!--/fig--> classifications |
621
  | **Recall against real Odoo bugfixes** | **<!--fig:recall.all.caught-->20<!--/fig--> of <!--fig:recall.all.in_scope-->334<!--/fig-->, <!--fig:recall.all.pct-->6<!--/fig-->%, across three ecosystems. See below.** |
622
- | Verdict stability, 12 questions x 5 runs | 12/12 same verdict every run |
623
- | Escalation rescued a bad draft | 11 of 21, 95% CI 32–72% |
624
- | **`nexa check`, 5 files x 5 runs** | **23 findings reported, in 5 of 5 files — every one the corpus can make** |
625
- | The same 23, on a second run of the identical command | 23 — the deterministic half does not move |
626
- | Rules the model added beyond the checkers | 1 on one run, 3 on the next, out of 25 reviews |
627
- | `nexa check` verdict stability | 4 of 5 files on one run, 3 of 5 on the next |
628
- | `nexa check --no-model` | 0.26s, identical output every run |
629
 
630
  <!--fig:corpus.checkable-->27<!--/fig--> of <!--fig:corpus.rules-->28<!--/fig--> rules carry an automated check. The one that does not —
631
  `schema-change-needs-migration`, whose violation lives in the diff between two module
@@ -1274,9 +1286,9 @@ nexa eval --retrieval
1274
  asks each rule its own probe and reports whether it comes back. No generation, so it
1275
  needs only the embedding model and finishes in seconds.
1276
 
1277
- All 28 are retrievable at the operating `TOP_K` of 6, and 26 of them rank in the top
1278
- 3, re-measured on 2026-09-27 with the `_sql_constraints` rule added. That number got
1279
- harder rather than easier when the corpus became one stack: with 19 rules competing,
1280
  `prefer-sql-constraint` came back first for *"stop anyone entering a negative quantity
1281
  on a sale order line"*; with 27 it stopped coming back at all, because that is the
1282
  generic validation question every constraint rule here answers.
@@ -1290,6 +1302,11 @@ The report states its own limits. Pinned rules are prepended regardless of score
1290
  ranking against them measures the pin rather than the wording, and the header says so
1291
  instead of counting them.
1292
 
 
 
 
 
 
1293
  ### And what does `nexa check` actually report?
1294
 
1295
  Every number above was measured on `nexa ask`. `nexa check` is the command you
@@ -1305,42 +1322,54 @@ adds the four things only a file can produce: which corpus it was routed to, how
1305
  much of it fitted, what the surroundings on disk let the context layer say, and
1306
  whether the answer mentions the file at all.
1307
 
1308
- Five files, five runs each:
1309
 
1310
  | | |
1311
  |---|---|
1312
- | Findings reported about the files | **23**, in 5 of 5 files |
1313
- | …which is what the corpus can find in them | **23** — `nexa fieldtest`, no model |
1314
- | WARN | 18 of 25, 95% CI 52–86% |
1315
- | REJECT | 7 of 25, 95% CI 14–48% |
1316
- | PASS | 0 of 25 |
1317
- | Same verdict every run | 4 of 5 files |
1318
- | Rules the model cited that no checker found | **1**, across 25 reviews |
1319
-
1320
- **The deterministic half does everything it is capable of.** Those 23 findings are
1321
- exactly what `nexa fieldtest` extracts from the same five files with no model
1322
- installed: an incomplete `@api.depends` on three stored computes, an
1323
- `@api.constrains` reading two fields it does not declare, `shell=True`, a bare
1324
- `except`, a mutable default argument, `JSON.parse` unchecked, a rejected promise
1325
- nothing catches. Each carries a line number and none depends on what a 7B says.
 
 
 
1326
 
1327
  The first run of this eval reported **none** of them, because every layer ran over
1328
  the model's reply and nothing had ever read the file.
1329
 
1330
- **The generative half adds close to nothing, and now there is a number.** Across
1331
- 25 reviews the model cited exactly **one** rule the checkers had not already
1332
- found. It does make the answers more about the file than they were — feeding it
1333
- the findings raised the symbols it names from 34 to 90 across the set — but what
1334
- it *adds* to the corpus's own verdict is one citation in twenty-five.
 
 
1335
 
1336
  That is worth knowing rather than worth hiding. It says where to spend effort:
1337
  another rule in the corpus is worth more than another prompt.
1338
 
 
 
 
 
 
 
1339
  **Coverage is evidence.** `fsm_visit.py` is 7,929 characters against a
1340
  6,000-character budget, so nine of its twenty symbols do not fit — but the excerpt
1341
  is chosen by symbol and the flagged ones go first, so `_check_no_overlap`, its
1342
- last method and the home of three of its six findings, is reviewed rather than cut
1343
- off 1,900 characters early. What is left out is named, and the verdict says so:
 
1344
 
1345
  ```
1346
  WARN (INSUFFICIENT EVIDENCE: 9 of 20 symbols were not reviewed)
 
36
  The method reads `parent_id.area_level` and declares only `parent_id`. Move a district to
37
  another region and the district's own level is recomputed; every area beneath it keeps the
38
  old one, stored, with nothing to say it changed. The GIS API groups areas by that level.
39
+ `nexa check --no-model` names it, on line 84, in under a second.
40
 
41
  It runs entirely on your machine. No API keys, no network at review time, no client
42
  code leaving the laptop — which for a consultancy under an NDA is not a preference.
 
431
  WARN (INSUFFICIENT EVIDENCE: 9 of 20 symbols were not reviewed)
432
  ```
433
 
434
+ **`--no-model` stops after the checkers.** Under a second, no Ollama, and the same
435
  output every run — because what the generative half adds on top of the checkers is
436
+ small: **<!--fig:check.beyond-->1<!--/fig-->** citation beyond them across <!--fig:check.reviews-->15<!--/fig--> reviews. On every one of those
437
+ reviews the deterministic half produced the identical <!--fig:check.findings-->10<!--/fig--> findings.
 
438
 
439
  That is the argument. A person reading a file wants the part no pattern can decide,
440
  and it is worth the wait. A CI gate wants the part that is the same every time, and
441
+ only <!--fig:check.stable-->1<!--/fig--> of the <!--fig:check.files-->3<!--/fig--> files reached the same verdict on all <!--fig:check.runs-->5<!--/fig--> runs. Those had been
442
  the same command.
443
 
444
  It prints **no verdict**, because nothing generated an answer to have a verdict
 
459
  which of the two it is rather than leaving the reader to infer it from a short list:
460
 
461
  ```
462
+ $ nexa check subjects/etl/load_prices.py
463
+
464
+ checking load_prices.py as odoo
465
+ ! no Odoo imports here and no __manifest__.py above it -- the 20 ORM rules
466
+ have nothing to match, so this file is effectively reviewed against the
467
+ 8 plain-Python ones
468
+
469
+ 4 checker finding(s) in the file
470
+ critical unmeasured line 29 python.subprocess-shell-true-is-injection
471
+ subprocess.run("cp " + target + " " + local, shell=True, check=True)
472
+ ...
473
 
474
  Rules retrieved:
475
+ PINNED [critical] odoo.check-constraint-cannot-cross-tables
476
+ 0.721 [critical] python.assert-is-not-validation
477
+ 0.691 [critical] python.subprocess-shell-true-is-injection
478
+ 0.600 [high] python.bare-except-catches-the-exit
479
+ 0.580 [high] python.mutable-default-argument
480
+ 0.732 [critical] python.eval-and-pickle-execute-their-input
481
  ```
482
 
483
+ The four rules the checkers fired on come back ahead of a better cosine, because a
484
+ finding on line 29 is better evidence than a similarity score, and the last place
485
+ goes to similarity. The one ORM rule in the list is pinned into every review.
486
+
487
  Unlike `nexa ask`, this one has **surroundings to read**: the nearest
488
  `__manifest__.py` gives the Odoo series, the file gives its model names and existing
489
  imports, `ir.model.access.csv` and the record rules in XML say what Python cannot.
 
620
  | Pattern false positives | 0 cross-hits across the corpus |
621
  | Recall under meaning-preserving rewrite | 82/82 |
622
  | Satisfied patterns bypassable by a comment | 0 of <!--fig:corpus.satisfied_patterns-->1<!--/fig--> |
623
+ | Retrievable by their own probe | <!--fig:retrieval.found-->28<!--/fig--> of <!--fig:retrieval.rules-->28<!--/fig--> at top <!--fig:retrieval.k-->6<!--/fig-->, <!--fig:retrieval.top3-->26<!--/fig--> of them at top 3 |
624
  | Hit rate on <!--fig:oca.lines-->33,960<!--/fig--> lines of reviewed OCA code | <!--fig:oca.rate-->3.0<!--/fig--> per 1000 lines: <!--fig:oca.findings-->101<!--/fig--> findings |
625
  | Those findings, read by hand | **<!--fig:oca.read.true-->85<!--/fig--> true positives**, <!--fig:oca.read.false-->6<!--/fig--> false, <!--fig:oca.read.undecidable-->7<!--/fig--> undecidable from one file, <!--fig:oca.unread-->3<!--/fig--> not yet read |
626
  | Hit rate on <!--fig:openspp.lines-->226,937<!--/fig--> lines of OpenSPP (Odoo 19) | <!--fig:openspp.rate-->0.7<!--/fig--> per 1000 lines: <!--fig:openspp.findings-->161<!--/fig--> findings, +<!--fig:openspp.decided-->127<!--/fig--> another tool had already flagged, +<!--fig:openspp.advisory-->82<!--/fig--> advisory |
 
631
  | Hit rate on <!--fig:core.lines-->559,663<!--/fig--> lines of odoo core 18.0 | <!--fig:core.rate-->4.2<!--/fig--> per 1000 lines: <!--fig:core.findings-->2,357<!--/fig--> findings, <!--fig:core.read-->89<!--/fig--> of them read by hand: <!--fig:core.read.true-->45<!--/fig--> true, <!--fig:core.read.false-->44<!--/fig--> false |
632
  | Hand-classified true positives still reported | **<!--fig:survival.found-->288<!--/fig--> of <!--fig:survival.true-->288<!--/fig-->**, across <!--fig:survival.artifacts-->9<!--/fig--> classifications |
633
  | **Recall against real Odoo bugfixes** | **<!--fig:recall.all.caught-->20<!--/fig--> of <!--fig:recall.all.in_scope-->334<!--/fig-->, <!--fig:recall.all.pct-->6<!--/fig-->%, across three ecosystems. See below.** |
634
+ | Verdict stability, `nexa ask`, 12 questions x 5 runs on 2026-09-14 | 12/12 same verdict every run; 6 of the 12 were TypeScript and JavaScript, since removed |
635
+ | Escalation rescued a bad draft, the same run | 11 of 21, 95% CI 32–72%; 10 of the 21 were TypeScript and JavaScript |
636
+ | **`nexa check`, <!--fig:check.files-->3<!--/fig--> files x <!--fig:check.runs-->5<!--/fig--> runs** | **<!--fig:check.findings-->10<!--/fig--> findings reported, in <!--fig:check.flagged-->3<!--/fig--> of <!--fig:check.files-->3<!--/fig--> files — every one the corpus can make**, and <!--fig:check.advisory-->3<!--/fig--> advisory reported apart |
637
+ | The same findings on every run | <!--fig:check.steady-->3<!--/fig--> of <!--fig:check.files-->3<!--/fig--> files — the deterministic half does not move |
638
+ | Rules the model added beyond the checkers | <!--fig:check.beyond-->1<!--/fig--> across <!--fig:check.reviews-->15<!--/fig--> reviews |
639
+ | `nexa check` verdict stability | <!--fig:check.stable-->1<!--/fig--> of <!--fig:check.files-->3<!--/fig--> files gave the same verdict every run |
640
+ | `nexa check --no-model` | under a second, identical output every run |
641
 
642
  <!--fig:corpus.checkable-->27<!--/fig--> of <!--fig:corpus.rules-->28<!--/fig--> rules carry an automated check. The one that does not —
643
  `schema-change-needs-migration`, whose violation lives in the diff between two module
 
1286
  asks each rule its own probe and reports whether it comes back. No generation, so it
1287
  needs only the embedding model and finishes in seconds.
1288
 
1289
+ All <!--fig:retrieval.found-->28<!--/fig--> are retrievable at the operating `TOP_K` of <!--fig:retrieval.k-->6<!--/fig-->, and <!--fig:retrieval.top3-->26<!--/fig--> of them rank
1290
+ in the top 3. That number got harder rather than easier when the corpus became one
1291
+ stack: with 19 rules competing,
1292
  `prefer-sql-constraint` came back first for *"stop anyone entering a negative quantity
1293
  on a sale order line"*; with 27 it stopped coming back at all, because that is the
1294
  generic validation question every constraint rule here answers.
 
1302
  ranking against them measures the pin rather than the wording, and the header says so
1303
  instead of counting them.
1304
 
1305
+ The run behind those figures is committed as `retrieval-2026-09.json`, with the hash
1306
+ of the corpus it ranked, and `python bin/figures.py --check` fails once the corpus
1307
+ moves under it. That includes a rule that was only reworded: it keeps its id and
1308
+ moves its embedding.
1309
+
1310
  ### And what does `nexa check` actually report?
1311
 
1312
  Every number above was measured on `nexa ask`. `nexa check` is the command you
 
1322
  much of it fitted, what the surroundings on disk let the context layer say, and
1323
  whether the answer mentions the file at all.
1324
 
1325
+ <!--fig:check.files-->3<!--/fig--> files, <!--fig:check.runs-->5<!--/fig--> runs each, committed as `eval-check-2026-09.json`:
1326
 
1327
  | | |
1328
  |---|---|
1329
+ | Findings reported about the files | **<!--fig:check.findings-->10<!--/fig-->**, in <!--fig:check.flagged-->3<!--/fig--> of <!--fig:check.files-->3<!--/fig--> files, and <!--fig:check.advisory-->3<!--/fig--> advisory reported apart |
1330
+ | …which is what the corpus can find in them | all of them — `nexa fieldtest` finds the same, no model |
1331
+ | PASS | <!--fig:check.verdict.PASS-->5<!--/fig--> of <!--fig:check.reviews-->15<!--/fig-->, 95% CI 15–58% |
1332
+ | WARN | <!--fig:check.verdict.WARN-->5<!--/fig--> of <!--fig:check.reviews-->15<!--/fig-->, 95% CI 15–58% |
1333
+ | REJECT | <!--fig:check.verdict.REJECT-->5<!--/fig--> of <!--fig:check.reviews-->15<!--/fig-->, 95% CI 15–58% |
1334
+ | Same verdict every run | <!--fig:check.stable-->1<!--/fig--> of <!--fig:check.files-->3<!--/fig--> files |
1335
+ | Rules the model cited that no checker found | **<!--fig:check.beyond-->1<!--/fig-->**, across <!--fig:check.reviews-->15<!--/fig--> reviews |
1336
+
1337
+ **The deterministic half does everything it is capable of.** Those findings are
1338
+ exactly what `nexa fieldtest` extracts from the same three files with no model
1339
+ installed: an incomplete `@api.depends` on two stored computes, a float compared
1340
+ with `==` against zero, an `@api.constrains` reading two fields it does not
1341
+ declare, `shell=True`, an `assert` doing validation's job, a bare `except` and a
1342
+ mutable default argument. Each carries a line number, none depends on what a 7B
1343
+ says, and they came out identical on every review. The advisory three, two `sudo()`
1344
+ calls and a check that could be a database constraint, are printed apart and gate
1345
+ nothing, as `check` itself prints them.
1346
 
1347
  The first run of this eval reported **none** of them, because every layer ran over
1348
  the model's reply and nothing had ever read the file.
1349
 
1350
+ **The generative half adds close to nothing.** Across <!--fig:check.reviews-->15<!--/fig--> reviews the model cited
1351
+ <!--fig:check.beyond-->1<!--/fig--> rule the checkers had not already found. In the run of 2026-09-27 that one
1352
+ was `check-constraint-cannot-cross-tables`, the rule pinned into every review, cited
1353
+ on the plain script, where there is no constraint for it to be about. Handing the
1354
+ model the findings did make its answers more about the file (the symbols they name
1355
+ went from 34 to 90 on the five files measured on 2026-09-14), but what it *adds* to
1356
+ the corpus's own verdict is next to nothing.
1357
 
1358
  That is worth knowing rather than worth hiding. It says where to spend effort:
1359
  another rule in the corpus is worth more than another prompt.
1360
 
1361
+ **PASS is about the answer, not the file.** In the run of 2026-09-27 all five PASS
1362
+ verdicts were `load_prices.py`, whose answer reproduces the file with all five of its
1363
+ functions named, and nothing that ran over the answer's code found a problem in it.
1364
+ The file itself still has its four findings, and `nexa check` exits 1 on it every
1365
+ time: the exit code follows the file.
1366
+
1367
  **Coverage is evidence.** `fsm_visit.py` is 7,929 characters against a
1368
  6,000-character budget, so nine of its twenty symbols do not fit — but the excerpt
1369
  is chosen by symbol and the flagged ones go first, so `_check_no_overlap`, its
1370
+ last method and the home of three of its six findings (one of the three advisory), is
1371
+ reviewed rather than cut off 1,900 characters early. What is left out is named, and
1372
+ the verdict says so:
1373
 
1374
  ```
1375
  WARN (INSUFFICIENT EVIDENCE: 9 of 20 symbols were not reviewed)
bin/eval.py CHANGED
@@ -45,6 +45,7 @@ import os
45
  import re
46
  import sys
47
  import time
 
48
 
49
  HERE = os.path.dirname(os.path.abspath(__file__))
50
  sys.path.insert(0, HERE)
@@ -486,6 +487,12 @@ def quiet_check(path, forced=None):
486
  found_ids = {hit.rule for hit in plan["findings"] if not hit.suppressed}
487
  cited = layers.cited_ids(text)
488
  beyond = sorted(cited - found_ids)
 
 
 
 
 
 
489
 
490
  eval_ns = final.get("eval_duration") or 0
491
  return {
@@ -504,12 +511,14 @@ def quiet_check(path, forced=None):
504
  "coverage": plan["coverage"],
505
  "symbols_shown": len(plan["shown_names"]),
506
  "symbols_total": len(plan["shown_names"]) + len(plan["omitted_names"]),
507
- "file_findings": sorted(found_ids),
508
  # Live findings only, so this and file_findings count the same things.
509
  # A hit on a line another tool already flagged is reported (3.33) and is
510
- # not part of what the checkers are telling this run to look at.
511
- "file_finding_count": sum(1 for hit in plan["findings"]
512
- if not hit.suppressed),
 
 
513
  "file_suppressed": plan["suppressed"],
514
  "cited": sorted(cited),
515
  "cited_beyond_checkers": beyond,
@@ -574,7 +583,7 @@ def evaluate_checks(paths, runs, forced=None):
574
  "dropped_chars": 0, "coverage": "?",
575
  "symbols_shown": 0, "symbols_total": 0,
576
  "file_findings": [], "file_finding_count": 0,
577
- "file_suppressed": 0, "facts": 0,
578
  "cited": [], "cited_beyond_checkers": [],
579
  "insufficient": [],
580
  "context_verdict": "?", "context_checked": 0,
@@ -797,6 +806,11 @@ def summarise_check(rows, runs, skipped=0):
797
  print(" {0} more sat on lines another tool had already flagged, reported"
798
  .format(quiet))
799
  print(" by `check` and left out of the counts above (3.33).")
 
 
 
 
 
800
  print()
801
 
802
  print("CONTEXT -- the layer `ask` can never run")
@@ -937,8 +951,20 @@ def main(argv):
937
  return 2
938
  code = summarise_retrieval(rows, k)
939
  if args.json:
 
 
 
 
 
940
  with io.open(args.json, "w", encoding="utf-8") as fh:
941
- fh.write(json.dumps(rows, indent=2, ensure_ascii=False))
 
 
 
 
 
 
 
942
  print("raw rows written to {0}".format(args.json))
943
  return code
944
 
 
45
  import re
46
  import sys
47
  import time
48
+ from datetime import datetime, timezone
49
 
50
  HERE = os.path.dirname(os.path.abspath(__file__))
51
  sys.path.insert(0, HERE)
 
487
  found_ids = {hit.rule for hit in plan["findings"] if not hit.suppressed}
488
  cited = layers.cited_ids(text)
489
  beyond = sorted(cited - found_ids)
490
+ # Counted the way `nexa check` and fieldtest count: an advisory rule fired,
491
+ # so citing it adds nothing above, but check prints it apart and never
492
+ # counts it. Counting it here made this eval report 13 findings on the
493
+ # subjects where fieldtest reports 10 and 3 advisory.
494
+ live = [hit for hit in plan["findings"]
495
+ if not hit.suppressed and not getattr(hit, "advisory", False)]
496
 
497
  eval_ns = final.get("eval_duration") or 0
498
  return {
 
511
  "coverage": plan["coverage"],
512
  "symbols_shown": len(plan["shown_names"]),
513
  "symbols_total": len(plan["shown_names"]) + len(plan["omitted_names"]),
514
+ "file_findings": sorted({hit.rule for hit in live}),
515
  # Live findings only, so this and file_findings count the same things.
516
  # A hit on a line another tool already flagged is reported (3.33) and is
517
+ # not part of what the checkers are telling this run to look at, and an
518
+ # advisory one is reported and gates nothing.
519
+ "file_finding_count": len(live),
520
+ "file_advisory": sum(1 for hit in plan["findings"]
521
+ if not hit.suppressed and getattr(hit, "advisory", False)),
522
  "file_suppressed": plan["suppressed"],
523
  "cited": sorted(cited),
524
  "cited_beyond_checkers": beyond,
 
583
  "dropped_chars": 0, "coverage": "?",
584
  "symbols_shown": 0, "symbols_total": 0,
585
  "file_findings": [], "file_finding_count": 0,
586
+ "file_advisory": 0, "file_suppressed": 0, "facts": 0,
587
  "cited": [], "cited_beyond_checkers": [],
588
  "insufficient": [],
589
  "context_verdict": "?", "context_checked": 0,
 
806
  print(" {0} more sat on lines another tool had already flagged, reported"
807
  .format(quiet))
808
  print(" by `check` and left out of the counts above (3.33).")
809
+ advisory = sum(r.get("file_advisory") or 0 for r in files.values())
810
+ if advisory:
811
+ print(" {0} more came from advisory rules, which `check` prints apart and"
812
+ .format(advisory))
813
+ print(" counts in nothing.")
814
  print()
815
 
816
  print("CONTEXT -- the layer `ask` can never run")
 
951
  return 2
952
  code = summarise_retrieval(rows, k)
953
  if args.json:
954
+ # The rows alone cannot be checked against anything. A rank means
955
+ # nothing without the depth it was taken at, and a run from before
956
+ # the last rule edit ranks rules that no longer read that way: the
957
+ # ids stay the same when only the wording moves. Both go in beside
958
+ # the rows, as recall's head did (3.53), so figures.py can tell.
959
  with io.open(args.json, "w", encoding="utf-8") as fh:
960
+ fh.write(json.dumps({
961
+ "generated_utc": datetime.now(timezone.utc).isoformat(
962
+ timespec="seconds"),
963
+ "k": k,
964
+ "embedder": corpus.EMBED_MODEL,
965
+ "corpus_sha256": evidence.corpus_sha256(),
966
+ "rows": rows,
967
+ }, indent=2, ensure_ascii=False))
968
  print("raw rows written to {0}".format(args.json))
969
  return code
970
 
bin/figures.py CHANGED
@@ -47,6 +47,16 @@ A classification pinned to a different commit than the fieldtest artifact is a
47
  fault rather than a mismatch to be matched around: line numbers from two
48
  commits say nothing about each other.
49
 
 
 
 
 
 
 
 
 
 
 
50
  Needs no checkout, no model and no network: only the committed artifacts.
51
  Stdlib only.
52
  """
@@ -81,6 +91,9 @@ RECALLS_XML = collections.OrderedDict([
81
  ])
82
  CLASSIFICATION_GLOB = "classification-*-2026-09.json"
83
  CORPUS_RULES = "corpus/rules/odoo.json"
 
 
 
84
  DOCUMENTS = ("README.md", "ARCHITECTURE.md")
85
 
86
  VERDICTS = ("true", "false", "undecidable")
@@ -390,6 +403,109 @@ def corpus_figures(fig):
390
  fig.put("corpus.origin." + origin, _count(n), src)
391
 
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  def compute():
394
  fig = Figures()
395
  corpus_figures(fig)
@@ -398,6 +514,8 @@ def compute():
398
  classification_figures(fig, trees)
399
  recall_figures(fig)
400
  recall_figures(fig, RECALLS_XML, "recall.xml")
 
 
401
  return fig
402
 
403
 
 
47
  fault rather than a mismatch to be matched around: line numbers from two
48
  commits say nothing about each other.
49
 
50
+ THE TWO EVAL RUNS
51
+ -----------------
52
+ `nexa eval --retrieval` and `nexa eval --check` need a model to run, and CI has
53
+ none, so their rows are committed and read here like any other artifact. Each
54
+ goes stale in its own way, and the check says so rather than recomputing a
55
+ number from rows that describe something else. A retrieval run records the
56
+ corpus hash it ranked, because a reworded rule keeps its id; a check run names
57
+ its files, and a file the repository no longer has is a file nothing can
58
+ re-review.
59
+
60
  Needs no checkout, no model and no network: only the committed artifacts.
61
  Stdlib only.
62
  """
 
91
  ])
92
  CLASSIFICATION_GLOB = "classification-*-2026-09.json"
93
  CORPUS_RULES = "corpus/rules/odoo.json"
94
+ CORPUS_META = "corpus/corpus_meta.json"
95
+ RETRIEVAL = "retrieval-2026-09.json"
96
+ CHECK = "eval-check-2026-09.json"
97
  DOCUMENTS = ("README.md", "ARCHITECTURE.md")
98
 
99
  VERDICTS = ("true", "false", "undecidable")
 
403
  fig.put("corpus.origin." + origin, _count(n), src)
404
 
405
 
406
+ def retrieval_figures(fig):
407
+ """Which rules their own probe brings back, from one committed run.
408
+
409
+ One run at the operating depth answers both published questions. retrieve()
410
+ fills its slots in one fixed order, pins first and then by score, so a rule
411
+ ranked 3 or better at k=6 is exactly a rule a k=3 run returns.
412
+ """
413
+ src = RETRIEVAL
414
+ art = _load(src)
415
+ rows = art["rows"]
416
+ ranked = [r for r in rows if r.get("rank")]
417
+ fig.put("retrieval.rules", _count(len(rows)), src)
418
+ fig.put("retrieval.k", _count(art["k"]), src)
419
+ fig.put("retrieval.found", _count(len(ranked)), src)
420
+ fig.put("retrieval.top3", _count(sum(r["rank"] <= 3 for r in ranked)), src)
421
+ if art["k"] < 3:
422
+ fig.faults.append("{0}: ranked at k={1}, which cannot say what a top 3 "
423
+ "returns".format(src, art["k"]))
424
+
425
+ # The drift item 12 fixed by hand: a rule added after the run, which the
426
+ # rows then cannot know about.
427
+ corpus_ids = {r["id"] for r in _load(CORPUS_RULES)}
428
+ measured = {r["id"] for r in rows}
429
+ for rid in sorted(corpus_ids - measured):
430
+ fig.faults.append("{0}: {1} is in the corpus and was never asked its "
431
+ "probe".format(src, rid))
432
+ for rid in sorted(measured - corpus_ids):
433
+ fig.faults.append("{0}: {1} was measured and is not in the corpus".format(src, rid))
434
+
435
+ # And the drift the ids cannot see. A reworded title keeps its id and moves
436
+ # its embedding, so the run is pinned to the corpus hash it ranked, as a
437
+ # classification is pinned to the commit its lines were read at.
438
+ ranked_on = art.get("corpus_sha256")
439
+ now = _load(CORPUS_META).get("corpus_sha256")
440
+ if not ranked_on or ranked_on != now:
441
+ fig.faults.append("{0}: ranked against corpus {1}, which is {2} now; re-run "
442
+ "nexa eval --retrieval --json {0}".format(
443
+ src, (ranked_on or "unrecorded")[:12], (now or "?")[:12]))
444
+
445
+
446
+ def _file_row(runs):
447
+ """The row that speaks for a file: the first one that got as far as reading
448
+ it. A failed generation reports 0 characters and no findings, and letting it
449
+ represent the file would publish a run that broke as a file that is clean."""
450
+ for row in runs:
451
+ if row.get("chars"):
452
+ return row
453
+ return runs[0]
454
+
455
+
456
+ def check_figures(fig):
457
+ """What `nexa eval --check` measured, from its committed rows.
458
+
459
+ One row per review. The checker findings are a fact about the file and are
460
+ taken once per file; whether they came out the same on every run is a
461
+ figure of its own, because "the deterministic half does not move" is a
462
+ published claim, and a file whose findings moved is counted out of it
463
+ rather than averaged into it.
464
+ """
465
+ src = CHECK
466
+ rows = _load(src)
467
+ files = collections.OrderedDict()
468
+ for row in rows:
469
+ files.setdefault(row["path"], []).append(row)
470
+
471
+ # The drift 13a found: two of five subjects deleted, and every figure still
472
+ # counting them because nothing read the paths.
473
+ for path in files:
474
+ if not os.path.isfile(os.path.join(ROOT, path)):
475
+ fig.faults.append("{0}: {1} is not in the repository, so nothing can "
476
+ "re-review it".format(src, path))
477
+ runs = sorted({len(r) for r in files.values()})
478
+ if len(runs) > 1:
479
+ fig.faults.append("{0}: files were reviewed unevenly, {1} times".format(
480
+ src, " or ".join(str(n) for n in runs)))
481
+
482
+ fig.put("check.files", _count(len(files)), src)
483
+ fig.put("check.runs", _count(runs[-1] if runs else 0), src)
484
+ fig.put("check.reviews", _count(len(rows)), src)
485
+
486
+ speaking = [_file_row(r) for r in files.values()]
487
+ fig.put("check.findings", _count(sum(r["file_finding_count"] for r in speaking)), src)
488
+ fig.put("check.advisory", _count(sum(r.get("file_advisory") or 0 for r in speaking)), src)
489
+ fig.put("check.flagged", _count(sum(1 for r in speaking if r["file_finding_count"])), src)
490
+ steady = 0
491
+ for reviews in files.values():
492
+ read = [(tuple(r["file_findings"]), r["file_finding_count"])
493
+ for r in reviews if r["verdict"] != "ERROR"]
494
+ if read and len(set(read)) == 1:
495
+ steady += 1
496
+ fig.put("check.steady", _count(steady), src)
497
+
498
+ verdicts = collections.Counter(r["verdict"] for r in rows)
499
+ for verdict in sorted(set(verdicts) | {"PASS", "WARN", "REJECT"}):
500
+ fig.put("check.verdict." + verdict, _count(verdicts[verdict]), src)
501
+ fig.put("check.stable", _count(sum(1 for reviews in files.values()
502
+ if len({r["verdict"] for r in reviews}) == 1)), src)
503
+
504
+ beyond = [rid for r in rows for rid in r.get("cited_beyond_checkers") or []]
505
+ fig.put("check.beyond", _count(len(beyond)), src)
506
+ fig.put("check.beyond.rules", _count(len(set(beyond))), src)
507
+
508
+
509
  def compute():
510
  fig = Figures()
511
  corpus_figures(fig)
 
514
  classification_figures(fig, trees)
515
  recall_figures(fig)
516
  recall_figures(fig, RECALLS_XML, "recall.xml")
517
+ retrieval_figures(fig)
518
+ check_figures(fig)
519
  return fig
520
 
521
 
eval-check-2026-09.json CHANGED
@@ -1,23 +1,30 @@
1
  [
2
  {
3
- "verdict": "WARN",
4
  "insufficient": [],
5
- "seconds": 9.5,
6
- "tokens_per_second": 38.9,
7
- "rules_fired": [],
 
 
 
 
 
8
  "layers_with_findings": [
9
- "structural"
 
 
10
  ],
11
  "retrieved": [
12
  "odoo.check-constraint-cannot-cross-tables",
13
  "odoo.stored-compute-needs-complete-depends",
14
- "odoo.onchange-is-not-validation",
15
  "odoo.float-comparison-needs-float-utils",
16
- "odoo.constrains-must-list-every-field-read",
17
- "odoo.constrains-ignores-dotted-names"
18
  ],
19
  "stack": "odoo",
20
- "routed_by": "odoo imports in the file",
21
  "chars": 2441,
22
  "dropped_chars": 0,
23
  "coverage": "whole file",
@@ -25,40 +32,47 @@
25
  "symbols_total": 0,
26
  "file_findings": [
27
  "odoo.float-comparison-needs-float-utils",
28
- "odoo.onchange-is-not-validation",
29
  "odoo.stored-compute-needs-complete-depends"
30
  ],
31
- "file_finding_count": 3,
 
32
  "file_suppressed": 0,
33
- "cited": [],
 
 
 
 
34
  "cited_beyond_checkers": [],
35
  "facts": 4,
36
- "context_verdict": "PASS",
37
- "context_checked": 18,
38
  "names_declared": 7,
39
  "names_mentioned": 2,
40
- "answer_chars": 1258,
41
  "path": "subjects/odoo_module/models/fsm_job.py"
42
  },
43
  {
44
  "verdict": "WARN",
45
  "insufficient": [],
46
- "seconds": 7.2,
47
- "tokens_per_second": 42.0,
48
- "rules_fired": [],
 
 
49
  "layers_with_findings": [
 
50
  "structural"
51
  ],
52
  "retrieved": [
53
  "odoo.check-constraint-cannot-cross-tables",
54
  "odoo.stored-compute-needs-complete-depends",
55
- "odoo.onchange-is-not-validation",
56
  "odoo.float-comparison-needs-float-utils",
57
- "odoo.constrains-must-list-every-field-read",
58
- "odoo.constrains-ignores-dotted-names"
59
  ],
60
  "stack": "odoo",
61
- "routed_by": "odoo imports in the file",
62
  "chars": 2441,
63
  "dropped_chars": 0,
64
  "coverage": "whole file",
@@ -66,43 +80,49 @@
66
  "symbols_total": 0,
67
  "file_findings": [
68
  "odoo.float-comparison-needs-float-utils",
69
- "odoo.onchange-is-not-validation",
70
  "odoo.stored-compute-needs-complete-depends"
71
  ],
72
- "file_finding_count": 3,
 
73
  "file_suppressed": 0,
74
- "cited": [],
 
 
 
75
  "cited_beyond_checkers": [],
76
  "facts": 4,
77
- "context_verdict": "PASS",
78
- "context_checked": 18,
79
  "names_declared": 7,
80
- "names_mentioned": 2,
81
- "answer_chars": 1259,
82
  "path": "subjects/odoo_module/models/fsm_job.py"
83
  },
84
  {
85
  "verdict": "REJECT",
86
- "insufficient": [
87
- "the domain layer could not run"
 
 
 
 
 
88
  ],
89
- "seconds": 20.2,
90
- "tokens_per_second": 41.0,
91
- "rules_fired": [],
92
  "layers_with_findings": [
93
- "structural",
94
- "syntactic"
 
95
  ],
96
  "retrieved": [
97
  "odoo.check-constraint-cannot-cross-tables",
98
  "odoo.stored-compute-needs-complete-depends",
99
- "odoo.onchange-is-not-validation",
100
  "odoo.float-comparison-needs-float-utils",
101
- "odoo.constrains-must-list-every-field-read",
102
- "odoo.constrains-ignores-dotted-names"
103
  ],
104
  "stack": "odoo",
105
- "routed_by": "odoo imports in the file",
106
  "chars": 2441,
107
  "dropped_chars": 0,
108
  "coverage": "whole file",
@@ -110,47 +130,47 @@
110
  "symbols_total": 0,
111
  "file_findings": [
112
  "odoo.float-comparison-needs-float-utils",
113
- "odoo.onchange-is-not-validation",
114
  "odoo.stored-compute-needs-complete-depends"
115
  ],
116
- "file_finding_count": 3,
 
117
  "file_suppressed": 0,
118
  "cited": [
119
- "odoo.constrains-must-list-every-field-read",
120
  "odoo.float-comparison-needs-float-utils",
121
- "odoo.onchange-is-not-validation",
122
- "odoo.stored-compute-needs-complete-depends"
123
- ],
124
- "cited_beyond_checkers": [
125
- "odoo.constrains-must-list-every-field-read"
126
  ],
 
127
  "facts": 4,
128
- "context_verdict": "PASS",
129
- "context_checked": 18,
130
  "names_declared": 7,
131
  "names_mentioned": 2,
132
- "answer_chars": 3600,
133
  "path": "subjects/odoo_module/models/fsm_job.py"
134
  },
135
  {
136
  "verdict": "WARN",
137
  "insufficient": [],
138
- "seconds": 4.6,
139
- "tokens_per_second": 41.4,
140
- "rules_fired": [],
 
 
141
  "layers_with_findings": [
 
142
  "structural"
143
  ],
144
  "retrieved": [
145
  "odoo.check-constraint-cannot-cross-tables",
146
  "odoo.stored-compute-needs-complete-depends",
147
- "odoo.onchange-is-not-validation",
148
  "odoo.float-comparison-needs-float-utils",
149
- "odoo.constrains-must-list-every-field-read",
150
- "odoo.constrains-ignores-dotted-names"
151
  ],
152
  "stack": "odoo",
153
- "routed_by": "odoo imports in the file",
154
  "chars": 2441,
155
  "dropped_chars": 0,
156
  "coverage": "whole file",
@@ -158,45 +178,49 @@
158
  "symbols_total": 0,
159
  "file_findings": [
160
  "odoo.float-comparison-needs-float-utils",
161
- "odoo.onchange-is-not-validation",
162
  "odoo.stored-compute-needs-complete-depends"
163
  ],
164
- "file_finding_count": 3,
 
165
  "file_suppressed": 0,
166
  "cited": [
167
- "odoo.onchange-is-not-validation"
 
168
  ],
169
  "cited_beyond_checkers": [],
170
  "facts": 4,
171
- "context_verdict": "PASS",
172
- "context_checked": 18,
173
  "names_declared": 7,
174
  "names_mentioned": 1,
175
- "answer_chars": 773,
176
  "path": "subjects/odoo_module/models/fsm_job.py"
177
  },
178
  {
179
  "verdict": "WARN",
180
  "insufficient": [],
181
- "seconds": 11.6,
182
- "tokens_per_second": 38.1,
183
  "rules_fired": [
184
- "odoo.prefer-sql-constraint"
 
 
185
  ],
186
  "layers_with_findings": [
187
- "semantic",
188
- "structural"
 
189
  ],
190
  "retrieved": [
191
  "odoo.check-constraint-cannot-cross-tables",
192
  "odoo.stored-compute-needs-complete-depends",
193
- "odoo.onchange-is-not-validation",
194
  "odoo.float-comparison-needs-float-utils",
195
- "odoo.constrains-must-list-every-field-read",
196
- "odoo.constrains-ignores-dotted-names"
197
  ],
198
  "stack": "odoo",
199
- "routed_by": "odoo imports in the file",
200
  "chars": 2441,
201
  "dropped_chars": 0,
202
  "coverage": "whole file",
@@ -204,68 +228,74 @@
204
  "symbols_total": 0,
205
  "file_findings": [
206
  "odoo.float-comparison-needs-float-utils",
207
- "odoo.onchange-is-not-validation",
208
  "odoo.stored-compute-needs-complete-depends"
209
  ],
210
- "file_finding_count": 3,
 
211
  "file_suppressed": 0,
212
- "cited": [],
 
 
 
 
213
  "cited_beyond_checkers": [],
214
  "facts": 4,
215
- "context_verdict": "PASS",
216
- "context_checked": 18,
217
  "names_declared": 7,
218
  "names_mentioned": 2,
219
- "answer_chars": 1882,
220
  "path": "subjects/odoo_module/models/fsm_job.py"
221
  },
222
  {
223
- "verdict": "REJECT",
224
  "insufficient": [
225
  "9 of 20 symbols were not reviewed"
226
  ],
227
- "seconds": 10.3,
228
- "tokens_per_second": 40.3,
229
  "rules_fired": [
 
230
  "odoo.prefer-sql-constraint",
231
- "odoo.stored-compute-needs-complete-depends"
232
  ],
233
  "layers_with_findings": [
 
 
234
  "semantic"
235
  ],
236
  "retrieved": [
237
  "odoo.check-constraint-cannot-cross-tables",
238
  "odoo.constrains-must-list-every-field-read",
239
  "odoo.stored-compute-needs-complete-depends",
 
240
  "odoo.prefer-sql-constraint",
241
- "odoo.onchange-is-not-validation",
242
- "odoo.constrains-ignores-dotted-names"
243
  ],
244
  "stack": "odoo",
245
- "routed_by": "odoo imports in the file",
246
  "chars": 7929,
247
- "dropped_chars": 2058,
248
  "coverage": "by symbol",
249
  "symbols_shown": 11,
250
  "symbols_total": 20,
251
  "file_findings": [
252
  "odoo.constrains-must-list-every-field-read",
253
- "odoo.prefer-sql-constraint",
254
  "odoo.stored-compute-needs-complete-depends"
255
  ],
256
- "file_finding_count": 6,
 
257
  "file_suppressed": 0,
258
  "cited": [
259
- "odoo.constrains-must-list-every-field-read",
260
- "odoo.stored-compute-needs-complete-depends"
261
  ],
262
  "cited_beyond_checkers": [],
263
  "facts": 4,
264
- "context_verdict": "PASS",
265
- "context_checked": 18,
266
  "names_declared": 19,
267
- "names_mentioned": 3,
268
- "answer_chars": 1378,
269
  "path": "subjects/odoo_module/models/fsm_visit.py"
270
  },
271
  {
@@ -273,48 +303,54 @@
273
  "insufficient": [
274
  "9 of 20 symbols were not reviewed"
275
  ],
276
- "seconds": 7.1,
277
- "tokens_per_second": 41.4,
278
  "rules_fired": [
279
  "odoo.prefer-sql-constraint",
 
280
  "odoo.stored-compute-needs-complete-depends"
281
  ],
282
  "layers_with_findings": [
283
- "semantic"
 
 
 
284
  ],
285
  "retrieved": [
286
  "odoo.check-constraint-cannot-cross-tables",
287
  "odoo.constrains-must-list-every-field-read",
288
  "odoo.stored-compute-needs-complete-depends",
 
289
  "odoo.prefer-sql-constraint",
290
- "odoo.onchange-is-not-validation",
291
- "odoo.constrains-ignores-dotted-names"
292
  ],
293
  "stack": "odoo",
294
- "routed_by": "odoo imports in the file",
295
  "chars": 7929,
296
- "dropped_chars": 2058,
297
  "coverage": "by symbol",
298
  "symbols_shown": 11,
299
  "symbols_total": 20,
300
  "file_findings": [
301
  "odoo.constrains-must-list-every-field-read",
302
- "odoo.prefer-sql-constraint",
303
  "odoo.stored-compute-needs-complete-depends"
304
  ],
305
- "file_finding_count": 6,
 
306
  "file_suppressed": 0,
307
  "cited": [
308
  "odoo.constrains-must-list-every-field-read",
309
- "odoo.stored-compute-needs-complete-depends"
 
 
310
  ],
311
  "cited_beyond_checkers": [],
312
  "facts": 4,
313
- "context_verdict": "PASS",
314
- "context_checked": 18,
315
  "names_declared": 19,
316
- "names_mentioned": 3,
317
- "answer_chars": 1130,
318
  "path": "subjects/odoo_module/models/fsm_visit.py"
319
  },
320
  {
@@ -322,97 +358,106 @@
322
  "insufficient": [
323
  "9 of 20 symbols were not reviewed"
324
  ],
325
- "seconds": 8.3,
326
- "tokens_per_second": 41.3,
327
  "rules_fired": [
328
  "odoo.prefer-sql-constraint",
 
329
  "odoo.stored-compute-needs-complete-depends"
330
  ],
331
  "layers_with_findings": [
332
- "semantic"
 
 
 
 
333
  ],
334
  "retrieved": [
335
  "odoo.check-constraint-cannot-cross-tables",
336
  "odoo.constrains-must-list-every-field-read",
337
  "odoo.stored-compute-needs-complete-depends",
 
338
  "odoo.prefer-sql-constraint",
339
- "odoo.onchange-is-not-validation",
340
- "odoo.constrains-ignores-dotted-names"
341
  ],
342
  "stack": "odoo",
343
- "routed_by": "odoo imports in the file",
344
  "chars": 7929,
345
- "dropped_chars": 2058,
346
  "coverage": "by symbol",
347
  "symbols_shown": 11,
348
  "symbols_total": 20,
349
  "file_findings": [
350
  "odoo.constrains-must-list-every-field-read",
351
- "odoo.prefer-sql-constraint",
352
  "odoo.stored-compute-needs-complete-depends"
353
  ],
354
- "file_finding_count": 6,
 
355
  "file_suppressed": 0,
356
  "cited": [
357
  "odoo.constrains-must-list-every-field-read",
358
- "odoo.stored-compute-needs-complete-depends"
 
 
359
  ],
360
  "cited_beyond_checkers": [],
361
  "facts": 4,
362
- "context_verdict": "PASS",
363
- "context_checked": 18,
364
  "names_declared": 19,
365
- "names_mentioned": 3,
366
- "answer_chars": 1378,
367
  "path": "subjects/odoo_module/models/fsm_visit.py"
368
  },
369
  {
370
- "verdict": "REJECT",
371
  "insufficient": [
372
  "9 of 20 symbols were not reviewed"
373
  ],
374
- "seconds": 8.2,
375
- "tokens_per_second": 41.7,
376
  "rules_fired": [
 
377
  "odoo.prefer-sql-constraint",
378
- "odoo.stored-compute-needs-complete-depends"
379
  ],
380
  "layers_with_findings": [
 
 
381
  "semantic"
382
  ],
383
  "retrieved": [
384
  "odoo.check-constraint-cannot-cross-tables",
385
  "odoo.constrains-must-list-every-field-read",
386
  "odoo.stored-compute-needs-complete-depends",
 
387
  "odoo.prefer-sql-constraint",
388
- "odoo.onchange-is-not-validation",
389
- "odoo.constrains-ignores-dotted-names"
390
  ],
391
  "stack": "odoo",
392
- "routed_by": "odoo imports in the file",
393
  "chars": 7929,
394
- "dropped_chars": 2058,
395
  "coverage": "by symbol",
396
  "symbols_shown": 11,
397
  "symbols_total": 20,
398
  "file_findings": [
399
  "odoo.constrains-must-list-every-field-read",
400
- "odoo.prefer-sql-constraint",
401
  "odoo.stored-compute-needs-complete-depends"
402
  ],
403
- "file_finding_count": 6,
 
404
  "file_suppressed": 0,
405
  "cited": [
406
- "odoo.constrains-must-list-every-field-read",
407
- "odoo.stored-compute-needs-complete-depends"
408
  ],
409
  "cited_beyond_checkers": [],
410
  "facts": 4,
411
- "context_verdict": "PASS",
412
- "context_checked": 18,
413
  "names_declared": 19,
414
- "names_mentioned": 3,
415
- "answer_chars": 1378,
416
  "path": "subjects/odoo_module/models/fsm_visit.py"
417
  },
418
  {
@@ -420,12 +465,16 @@
420
  "insufficient": [
421
  "9 of 20 symbols were not reviewed"
422
  ],
423
- "seconds": 5.1,
424
- "tokens_per_second": 41.8,
425
  "rules_fired": [
 
 
426
  "odoo.stored-compute-needs-complete-depends"
427
  ],
428
  "layers_with_findings": [
 
 
429
  "semantic",
430
  "structural"
431
  ],
@@ -433,62 +482,60 @@
433
  "odoo.check-constraint-cannot-cross-tables",
434
  "odoo.constrains-must-list-every-field-read",
435
  "odoo.stored-compute-needs-complete-depends",
 
436
  "odoo.prefer-sql-constraint",
437
- "odoo.onchange-is-not-validation",
438
- "odoo.constrains-ignores-dotted-names"
439
  ],
440
  "stack": "odoo",
441
- "routed_by": "odoo imports in the file",
442
  "chars": 7929,
443
- "dropped_chars": 2058,
444
  "coverage": "by symbol",
445
  "symbols_shown": 11,
446
  "symbols_total": 20,
447
  "file_findings": [
448
  "odoo.constrains-must-list-every-field-read",
449
- "odoo.prefer-sql-constraint",
450
  "odoo.stored-compute-needs-complete-depends"
451
  ],
452
- "file_finding_count": 6,
 
453
  "file_suppressed": 0,
454
  "cited": [
455
  "odoo.constrains-must-list-every-field-read",
456
  "odoo.prefer-sql-constraint",
457
- "odoo.stored-compute-needs-complete-depends"
 
458
  ],
459
  "cited_beyond_checkers": [],
460
  "facts": 4,
461
- "context_verdict": "PASS",
462
- "context_checked": 18,
463
  "names_declared": 19,
464
  "names_mentioned": 2,
465
- "answer_chars": 771,
466
  "path": "subjects/odoo_module/models/fsm_visit.py"
467
  },
468
  {
469
- "verdict": "WARN",
470
  "insufficient": [],
471
- "seconds": 19.1,
472
- "tokens_per_second": 42.7,
473
  "rules_fired": [
474
- "python.decimal-for-money-not-float",
475
- "python.naive-and-aware-datetimes-do-not-compare",
476
- "python.path-join-does-not-contain-a-path"
477
  ],
478
  "layers_with_findings": [
479
- "domain",
480
- "knowledge"
481
  ],
482
  "retrieved": [
 
483
  "python.assert-is-not-validation",
484
  "python.subprocess-shell-true-is-injection",
485
- "python.path-join-does-not-contain-a-path",
486
  "python.bare-except-catches-the-exit",
487
  "python.mutable-default-argument",
488
- "python.decimal-for-money-not-float"
489
  ],
490
- "stack": "python",
491
- "routed_by": "a .py file with no Odoo in it",
492
  "chars": 2033,
493
  "dropped_chars": 0,
494
  "coverage": "whole file",
@@ -497,55 +544,48 @@
497
  "file_findings": [
498
  "python.assert-is-not-validation",
499
  "python.bare-except-catches-the-exit",
500
- "python.decimal-for-money-not-float",
501
  "python.mutable-default-argument",
502
- "python.naive-and-aware-datetimes-do-not-compare",
503
- "python.path-join-does-not-contain-a-path",
504
  "python.subprocess-shell-true-is-injection"
505
  ],
506
- "file_finding_count": 8,
 
507
  "file_suppressed": 0,
508
  "cited": [
509
  "python.assert-is-not-validation",
510
  "python.bare-except-catches-the-exit",
511
- "python.decimal-for-money-not-float",
512
  "python.mutable-default-argument",
513
- "python.naive-and-aware-datetimes-do-not-compare",
514
- "python.path-join-does-not-contain-a-path",
515
  "python.subprocess-shell-true-is-injection"
516
  ],
517
  "cited_beyond_checkers": [],
518
  "facts": 0,
519
- "context_verdict": "UNKNOWN",
520
- "context_checked": 0,
521
  "names_declared": 5,
522
  "names_mentioned": 5,
523
- "answer_chars": 3157,
524
  "path": "subjects/etl/load_prices.py"
525
  },
526
  {
527
- "verdict": "WARN",
528
  "insufficient": [],
529
- "seconds": 11.9,
530
- "tokens_per_second": 42.8,
531
  "rules_fired": [
532
- "python.decimal-for-money-not-float",
533
- "python.naive-and-aware-datetimes-do-not-compare",
534
- "python.path-join-does-not-contain-a-path"
535
  ],
536
  "layers_with_findings": [
537
- "domain"
538
  ],
539
  "retrieved": [
 
540
  "python.assert-is-not-validation",
541
  "python.subprocess-shell-true-is-injection",
542
- "python.path-join-does-not-contain-a-path",
543
  "python.bare-except-catches-the-exit",
544
  "python.mutable-default-argument",
545
- "python.decimal-for-money-not-float"
546
  ],
547
- "stack": "python",
548
- "routed_by": "a .py file with no Odoo in it",
549
  "chars": 2033,
550
  "dropped_chars": 0,
551
  "coverage": "whole file",
@@ -554,54 +594,48 @@
554
  "file_findings": [
555
  "python.assert-is-not-validation",
556
  "python.bare-except-catches-the-exit",
557
- "python.decimal-for-money-not-float",
558
  "python.mutable-default-argument",
559
- "python.naive-and-aware-datetimes-do-not-compare",
560
- "python.path-join-does-not-contain-a-path",
561
  "python.subprocess-shell-true-is-injection"
562
  ],
563
- "file_finding_count": 8,
 
564
  "file_suppressed": 0,
565
  "cited": [
566
  "python.assert-is-not-validation",
567
  "python.bare-except-catches-the-exit",
568
- "python.decimal-for-money-not-float",
569
  "python.mutable-default-argument",
570
- "python.path-join-does-not-contain-a-path",
571
  "python.subprocess-shell-true-is-injection"
572
  ],
573
  "cited_beyond_checkers": [],
574
  "facts": 0,
575
- "context_verdict": "UNKNOWN",
576
- "context_checked": 0,
577
  "names_declared": 5,
578
  "names_mentioned": 5,
579
- "answer_chars": 2078,
580
  "path": "subjects/etl/load_prices.py"
581
  },
582
  {
583
- "verdict": "WARN",
584
  "insufficient": [],
585
- "seconds": 10.8,
586
- "tokens_per_second": 42.7,
587
  "rules_fired": [
588
- "python.decimal-for-money-not-float",
589
- "python.naive-and-aware-datetimes-do-not-compare",
590
- "python.path-join-does-not-contain-a-path"
591
  ],
592
  "layers_with_findings": [
593
- "domain"
594
  ],
595
  "retrieved": [
 
596
  "python.assert-is-not-validation",
597
  "python.subprocess-shell-true-is-injection",
598
- "python.path-join-does-not-contain-a-path",
599
  "python.bare-except-catches-the-exit",
600
  "python.mutable-default-argument",
601
- "python.decimal-for-money-not-float"
602
  ],
603
- "stack": "python",
604
- "routed_by": "a .py file with no Odoo in it",
605
  "chars": 2033,
606
  "dropped_chars": 0,
607
  "coverage": "whole file",
@@ -610,49 +644,45 @@
610
  "file_findings": [
611
  "python.assert-is-not-validation",
612
  "python.bare-except-catches-the-exit",
613
- "python.decimal-for-money-not-float",
614
  "python.mutable-default-argument",
615
- "python.naive-and-aware-datetimes-do-not-compare",
616
- "python.path-join-does-not-contain-a-path",
617
  "python.subprocess-shell-true-is-injection"
618
  ],
619
- "file_finding_count": 8,
 
620
  "file_suppressed": 0,
621
  "cited": [
622
  "python.mutable-default-argument"
623
  ],
624
  "cited_beyond_checkers": [],
625
  "facts": 0,
626
- "context_verdict": "UNKNOWN",
627
- "context_checked": 0,
628
  "names_declared": 5,
629
  "names_mentioned": 5,
630
- "answer_chars": 1914,
631
  "path": "subjects/etl/load_prices.py"
632
  },
633
  {
634
- "verdict": "WARN",
635
  "insufficient": [],
636
- "seconds": 16.0,
637
- "tokens_per_second": 42.2,
638
  "rules_fired": [
639
- "python.decimal-for-money-not-float",
640
- "python.naive-and-aware-datetimes-do-not-compare",
641
- "python.path-join-does-not-contain-a-path"
642
  ],
643
  "layers_with_findings": [
644
- "domain"
645
  ],
646
  "retrieved": [
 
647
  "python.assert-is-not-validation",
648
  "python.subprocess-shell-true-is-injection",
649
- "python.path-join-does-not-contain-a-path",
650
  "python.bare-except-catches-the-exit",
651
  "python.mutable-default-argument",
652
- "python.decimal-for-money-not-float"
653
  ],
654
- "stack": "python",
655
- "routed_by": "a .py file with no Odoo in it",
656
  "chars": 2033,
657
  "dropped_chars": 0,
658
  "coverage": "whole file",
@@ -661,47 +691,51 @@
661
  "file_findings": [
662
  "python.assert-is-not-validation",
663
  "python.bare-except-catches-the-exit",
664
- "python.decimal-for-money-not-float",
665
  "python.mutable-default-argument",
666
- "python.naive-and-aware-datetimes-do-not-compare",
667
- "python.path-join-does-not-contain-a-path",
668
  "python.subprocess-shell-true-is-injection"
669
  ],
670
- "file_finding_count": 8,
 
671
  "file_suppressed": 0,
672
- "cited": [],
673
- "cited_beyond_checkers": [],
 
 
 
 
 
 
 
 
674
  "facts": 0,
675
- "context_verdict": "UNKNOWN",
676
- "context_checked": 0,
677
  "names_declared": 5,
678
  "names_mentioned": 5,
679
- "answer_chars": 2834,
680
  "path": "subjects/etl/load_prices.py"
681
  },
682
  {
683
- "verdict": "WARN",
684
  "insufficient": [],
685
- "seconds": 11.0,
686
- "tokens_per_second": 42.1,
687
  "rules_fired": [
688
- "python.decimal-for-money-not-float",
689
- "python.naive-and-aware-datetimes-do-not-compare",
690
- "python.path-join-does-not-contain-a-path"
691
  ],
692
  "layers_with_findings": [
693
- "domain"
694
  ],
695
  "retrieved": [
 
696
  "python.assert-is-not-validation",
697
  "python.subprocess-shell-true-is-injection",
698
- "python.path-join-does-not-contain-a-path",
699
  "python.bare-except-catches-the-exit",
700
  "python.mutable-default-argument",
701
- "python.decimal-for-money-not-float"
702
  ],
703
- "stack": "python",
704
- "routed_by": "a .py file with no Odoo in it",
705
  "chars": 2033,
706
  "dropped_chars": 0,
707
  "coverage": "whole file",
@@ -710,507 +744,25 @@
710
  "file_findings": [
711
  "python.assert-is-not-validation",
712
  "python.bare-except-catches-the-exit",
713
- "python.decimal-for-money-not-float",
714
  "python.mutable-default-argument",
715
- "python.naive-and-aware-datetimes-do-not-compare",
716
- "python.path-join-does-not-contain-a-path",
717
  "python.subprocess-shell-true-is-injection"
718
  ],
719
- "file_finding_count": 8,
 
720
  "file_suppressed": 0,
721
  "cited": [
722
- "python.mutable-default-argument"
 
 
 
723
  ],
724
  "cited_beyond_checkers": [],
725
  "facts": 0,
726
- "context_verdict": "UNKNOWN",
727
- "context_checked": 0,
728
  "names_declared": 5,
729
  "names_mentioned": 5,
730
- "answer_chars": 1914,
731
  "path": "subjects/etl/load_prices.py"
732
- },
733
- {
734
- "verdict": "WARN",
735
- "insufficient": [],
736
- "seconds": 14.2,
737
- "tokens_per_second": 41.8,
738
- "rules_fired": [
739
- "typescript.unknown-not-any-at-boundaries"
740
- ],
741
- "layers_with_findings": [
742
- "domain",
743
- "structural"
744
- ],
745
- "retrieved": [
746
- "typescript.unknown-not-any-at-boundaries",
747
- "typescript.validate-env-at-startup",
748
- "typescript.odoo-returns-false-for-every-empty-field",
749
- "typescript.types-are-erased-at-runtime",
750
- "typescript.brand-validated-primitives",
751
- "typescript.safeparse-when-failure-is-expected"
752
- ],
753
- "stack": "typescript",
754
- "routed_by": "extension",
755
- "chars": 1687,
756
- "dropped_chars": 0,
757
- "coverage": "whole file",
758
- "symbols_shown": 0,
759
- "symbols_total": 0,
760
- "file_findings": [
761
- "typescript.unknown-not-any-at-boundaries",
762
- "typescript.validate-env-at-startup"
763
- ],
764
- "file_finding_count": 2,
765
- "file_suppressed": 0,
766
- "cited": [
767
- "typescript.types-are-erased-at-runtime",
768
- "typescript.unknown-not-any-at-boundaries",
769
- "typescript.validate-env-at-startup"
770
- ],
771
- "cited_beyond_checkers": [
772
- "typescript.types-are-erased-at-runtime"
773
- ],
774
- "facts": 1,
775
- "context_verdict": "UNKNOWN",
776
- "context_checked": 0,
777
- "names_declared": 7,
778
- "names_mentioned": 7,
779
- "answer_chars": 2073,
780
- "path": "subjects/web/src/partners.ts"
781
- },
782
- {
783
- "verdict": "REJECT",
784
- "insufficient": [],
785
- "seconds": 12.8,
786
- "tokens_per_second": 41.4,
787
- "rules_fired": [
788
- "typescript.odoo-returns-false-for-every-empty-field",
789
- "typescript.unknown-not-any-at-boundaries"
790
- ],
791
- "layers_with_findings": [
792
- "domain",
793
- "structural"
794
- ],
795
- "retrieved": [
796
- "typescript.unknown-not-any-at-boundaries",
797
- "typescript.validate-env-at-startup",
798
- "typescript.odoo-returns-false-for-every-empty-field",
799
- "typescript.types-are-erased-at-runtime",
800
- "typescript.brand-validated-primitives",
801
- "typescript.safeparse-when-failure-is-expected"
802
- ],
803
- "stack": "typescript",
804
- "routed_by": "extension",
805
- "chars": 1687,
806
- "dropped_chars": 0,
807
- "coverage": "whole file",
808
- "symbols_shown": 0,
809
- "symbols_total": 0,
810
- "file_findings": [
811
- "typescript.unknown-not-any-at-boundaries",
812
- "typescript.validate-env-at-startup"
813
- ],
814
- "file_finding_count": 2,
815
- "file_suppressed": 0,
816
- "cited": [
817
- "typescript.unknown-not-any-at-boundaries",
818
- "typescript.validate-env-at-startup"
819
- ],
820
- "cited_beyond_checkers": [],
821
- "facts": 1,
822
- "context_verdict": "UNKNOWN",
823
- "context_checked": 0,
824
- "names_declared": 7,
825
- "names_mentioned": 6,
826
- "answer_chars": 2036,
827
- "path": "subjects/web/src/partners.ts"
828
- },
829
- {
830
- "verdict": "WARN",
831
- "insufficient": [],
832
- "seconds": 9.2,
833
- "tokens_per_second": 41.5,
834
- "rules_fired": [
835
- "typescript.unknown-not-any-at-boundaries"
836
- ],
837
- "layers_with_findings": [
838
- "domain",
839
- "structural"
840
- ],
841
- "retrieved": [
842
- "typescript.unknown-not-any-at-boundaries",
843
- "typescript.validate-env-at-startup",
844
- "typescript.odoo-returns-false-for-every-empty-field",
845
- "typescript.types-are-erased-at-runtime",
846
- "typescript.brand-validated-primitives",
847
- "typescript.safeparse-when-failure-is-expected"
848
- ],
849
- "stack": "typescript",
850
- "routed_by": "extension",
851
- "chars": 1687,
852
- "dropped_chars": 0,
853
- "coverage": "whole file",
854
- "symbols_shown": 0,
855
- "symbols_total": 0,
856
- "file_findings": [
857
- "typescript.unknown-not-any-at-boundaries",
858
- "typescript.validate-env-at-startup"
859
- ],
860
- "file_finding_count": 2,
861
- "file_suppressed": 0,
862
- "cited": [
863
- "typescript.types-are-erased-at-runtime",
864
- "typescript.unknown-not-any-at-boundaries",
865
- "typescript.validate-env-at-startup"
866
- ],
867
- "cited_beyond_checkers": [
868
- "typescript.types-are-erased-at-runtime"
869
- ],
870
- "facts": 1,
871
- "context_verdict": "UNKNOWN",
872
- "context_checked": 0,
873
- "names_declared": 7,
874
- "names_mentioned": 2,
875
- "answer_chars": 1400,
876
- "path": "subjects/web/src/partners.ts"
877
- },
878
- {
879
- "verdict": "WARN",
880
- "insufficient": [],
881
- "seconds": 12.8,
882
- "tokens_per_second": 41.7,
883
- "rules_fired": [
884
- "typescript.unknown-not-any-at-boundaries"
885
- ],
886
- "layers_with_findings": [
887
- "domain",
888
- "structural"
889
- ],
890
- "retrieved": [
891
- "typescript.unknown-not-any-at-boundaries",
892
- "typescript.validate-env-at-startup",
893
- "typescript.odoo-returns-false-for-every-empty-field",
894
- "typescript.types-are-erased-at-runtime",
895
- "typescript.brand-validated-primitives",
896
- "typescript.safeparse-when-failure-is-expected"
897
- ],
898
- "stack": "typescript",
899
- "routed_by": "extension",
900
- "chars": 1687,
901
- "dropped_chars": 0,
902
- "coverage": "whole file",
903
- "symbols_shown": 0,
904
- "symbols_total": 0,
905
- "file_findings": [
906
- "typescript.unknown-not-any-at-boundaries",
907
- "typescript.validate-env-at-startup"
908
- ],
909
- "file_finding_count": 2,
910
- "file_suppressed": 0,
911
- "cited": [
912
- "typescript.unknown-not-any-at-boundaries",
913
- "typescript.validate-env-at-startup"
914
- ],
915
- "cited_beyond_checkers": [],
916
- "facts": 1,
917
- "context_verdict": "UNKNOWN",
918
- "context_checked": 0,
919
- "names_declared": 7,
920
- "names_mentioned": 7,
921
- "answer_chars": 2080,
922
- "path": "subjects/web/src/partners.ts"
923
- },
924
- {
925
- "verdict": "REJECT",
926
- "insufficient": [],
927
- "seconds": 12.8,
928
- "tokens_per_second": 41.6,
929
- "rules_fired": [
930
- "typescript.odoo-returns-false-for-every-empty-field",
931
- "typescript.unknown-not-any-at-boundaries"
932
- ],
933
- "layers_with_findings": [
934
- "domain",
935
- "structural"
936
- ],
937
- "retrieved": [
938
- "typescript.unknown-not-any-at-boundaries",
939
- "typescript.validate-env-at-startup",
940
- "typescript.odoo-returns-false-for-every-empty-field",
941
- "typescript.types-are-erased-at-runtime",
942
- "typescript.brand-validated-primitives",
943
- "typescript.safeparse-when-failure-is-expected"
944
- ],
945
- "stack": "typescript",
946
- "routed_by": "extension",
947
- "chars": 1687,
948
- "dropped_chars": 0,
949
- "coverage": "whole file",
950
- "symbols_shown": 0,
951
- "symbols_total": 0,
952
- "file_findings": [
953
- "typescript.unknown-not-any-at-boundaries",
954
- "typescript.validate-env-at-startup"
955
- ],
956
- "file_finding_count": 2,
957
- "file_suppressed": 0,
958
- "cited": [
959
- "typescript.unknown-not-any-at-boundaries",
960
- "typescript.validate-env-at-startup"
961
- ],
962
- "cited_beyond_checkers": [],
963
- "facts": 1,
964
- "context_verdict": "UNKNOWN",
965
- "context_checked": 0,
966
- "names_declared": 7,
967
- "names_mentioned": 7,
968
- "answer_chars": 2055,
969
- "path": "subjects/web/src/partners.ts"
970
- },
971
- {
972
- "verdict": "REJECT",
973
- "insufficient": [],
974
- "seconds": 17.0,
975
- "tokens_per_second": 41.1,
976
- "rules_fired": [
977
- "javascript.json-parse-returns-any"
978
- ],
979
- "layers_with_findings": [
980
- "domain"
981
- ],
982
- "retrieved": [
983
- "javascript.json-parse-returns-any",
984
- "javascript.async-errors-need-explicit-handling",
985
- "javascript.number-parsing-is-lossy",
986
- "javascript.date-parsing-is-implementation-defined",
987
- "javascript.loose-equality-and-falsy-defaults",
988
- "javascript.validate-at-the-boundary-once"
989
- ],
990
- "stack": "javascript",
991
- "routed_by": "extension",
992
- "chars": 1401,
993
- "dropped_chars": 0,
994
- "coverage": "whole file",
995
- "symbols_shown": 0,
996
- "symbols_total": 0,
997
- "file_findings": [
998
- "javascript.async-errors-need-explicit-handling",
999
- "javascript.date-parsing-is-implementation-defined",
1000
- "javascript.json-parse-returns-any",
1001
- "javascript.number-parsing-is-lossy"
1002
- ],
1003
- "file_finding_count": 4,
1004
- "file_suppressed": 0,
1005
- "cited": [
1006
- "javascript.async-errors-need-explicit-handling",
1007
- "javascript.date-parsing-is-implementation-defined",
1008
- "javascript.json-parse-returns-any",
1009
- "javascript.number-parsing-is-lossy"
1010
- ],
1011
- "cited_beyond_checkers": [],
1012
- "facts": 1,
1013
- "context_verdict": "UNKNOWN",
1014
- "context_checked": 0,
1015
- "names_declared": 1,
1016
- "names_mentioned": 1,
1017
- "answer_chars": 2424,
1018
- "path": "subjects/web/src/board.js"
1019
- },
1020
- {
1021
- "verdict": "REJECT",
1022
- "insufficient": [],
1023
- "seconds": 16.9,
1024
- "tokens_per_second": 41.1,
1025
- "rules_fired": [
1026
- "javascript.json-parse-returns-any"
1027
- ],
1028
- "layers_with_findings": [
1029
- "domain"
1030
- ],
1031
- "retrieved": [
1032
- "javascript.json-parse-returns-any",
1033
- "javascript.async-errors-need-explicit-handling",
1034
- "javascript.number-parsing-is-lossy",
1035
- "javascript.date-parsing-is-implementation-defined",
1036
- "javascript.loose-equality-and-falsy-defaults",
1037
- "javascript.validate-at-the-boundary-once"
1038
- ],
1039
- "stack": "javascript",
1040
- "routed_by": "extension",
1041
- "chars": 1401,
1042
- "dropped_chars": 0,
1043
- "coverage": "whole file",
1044
- "symbols_shown": 0,
1045
- "symbols_total": 0,
1046
- "file_findings": [
1047
- "javascript.async-errors-need-explicit-handling",
1048
- "javascript.date-parsing-is-implementation-defined",
1049
- "javascript.json-parse-returns-any",
1050
- "javascript.number-parsing-is-lossy"
1051
- ],
1052
- "file_finding_count": 4,
1053
- "file_suppressed": 0,
1054
- "cited": [
1055
- "javascript.async-errors-need-explicit-handling",
1056
- "javascript.date-parsing-is-implementation-defined",
1057
- "javascript.json-parse-returns-any",
1058
- "javascript.number-parsing-is-lossy"
1059
- ],
1060
- "cited_beyond_checkers": [],
1061
- "facts": 1,
1062
- "context_verdict": "UNKNOWN",
1063
- "context_checked": 0,
1064
- "names_declared": 1,
1065
- "names_mentioned": 1,
1066
- "answer_chars": 2595,
1067
- "path": "subjects/web/src/board.js"
1068
- },
1069
- {
1070
- "verdict": "REJECT",
1071
- "insufficient": [],
1072
- "seconds": 15.8,
1073
- "tokens_per_second": 40.6,
1074
- "rules_fired": [
1075
- "javascript.json-parse-returns-any"
1076
- ],
1077
- "layers_with_findings": [
1078
- "domain"
1079
- ],
1080
- "retrieved": [
1081
- "javascript.json-parse-returns-any",
1082
- "javascript.async-errors-need-explicit-handling",
1083
- "javascript.number-parsing-is-lossy",
1084
- "javascript.date-parsing-is-implementation-defined",
1085
- "javascript.loose-equality-and-falsy-defaults",
1086
- "javascript.validate-at-the-boundary-once"
1087
- ],
1088
- "stack": "javascript",
1089
- "routed_by": "extension",
1090
- "chars": 1401,
1091
- "dropped_chars": 0,
1092
- "coverage": "whole file",
1093
- "symbols_shown": 0,
1094
- "symbols_total": 0,
1095
- "file_findings": [
1096
- "javascript.async-errors-need-explicit-handling",
1097
- "javascript.date-parsing-is-implementation-defined",
1098
- "javascript.json-parse-returns-any",
1099
- "javascript.number-parsing-is-lossy"
1100
- ],
1101
- "file_finding_count": 4,
1102
- "file_suppressed": 0,
1103
- "cited": [
1104
- "javascript.async-errors-need-explicit-handling",
1105
- "javascript.date-parsing-is-implementation-defined",
1106
- "javascript.json-parse-returns-any",
1107
- "javascript.number-parsing-is-lossy"
1108
- ],
1109
- "cited_beyond_checkers": [],
1110
- "facts": 1,
1111
- "context_verdict": "UNKNOWN",
1112
- "context_checked": 0,
1113
- "names_declared": 1,
1114
- "names_mentioned": 1,
1115
- "answer_chars": 2434,
1116
- "path": "subjects/web/src/board.js"
1117
- },
1118
- {
1119
- "verdict": "REJECT",
1120
- "insufficient": [],
1121
- "seconds": 17.4,
1122
- "tokens_per_second": 39.8,
1123
- "rules_fired": [
1124
- "javascript.json-parse-returns-any"
1125
- ],
1126
- "layers_with_findings": [
1127
- "domain"
1128
- ],
1129
- "retrieved": [
1130
- "javascript.json-parse-returns-any",
1131
- "javascript.async-errors-need-explicit-handling",
1132
- "javascript.number-parsing-is-lossy",
1133
- "javascript.date-parsing-is-implementation-defined",
1134
- "javascript.loose-equality-and-falsy-defaults",
1135
- "javascript.validate-at-the-boundary-once"
1136
- ],
1137
- "stack": "javascript",
1138
- "routed_by": "extension",
1139
- "chars": 1401,
1140
- "dropped_chars": 0,
1141
- "coverage": "whole file",
1142
- "symbols_shown": 0,
1143
- "symbols_total": 0,
1144
- "file_findings": [
1145
- "javascript.async-errors-need-explicit-handling",
1146
- "javascript.date-parsing-is-implementation-defined",
1147
- "javascript.json-parse-returns-any",
1148
- "javascript.number-parsing-is-lossy"
1149
- ],
1150
- "file_finding_count": 4,
1151
- "file_suppressed": 0,
1152
- "cited": [
1153
- "javascript.async-errors-need-explicit-handling",
1154
- "javascript.date-parsing-is-implementation-defined",
1155
- "javascript.json-parse-returns-any",
1156
- "javascript.number-parsing-is-lossy"
1157
- ],
1158
- "cited_beyond_checkers": [],
1159
- "facts": 1,
1160
- "context_verdict": "UNKNOWN",
1161
- "context_checked": 0,
1162
- "names_declared": 1,
1163
- "names_mentioned": 1,
1164
- "answer_chars": 2586,
1165
- "path": "subjects/web/src/board.js"
1166
- },
1167
- {
1168
- "verdict": "REJECT",
1169
- "insufficient": [],
1170
- "seconds": 17.2,
1171
- "tokens_per_second": 40.2,
1172
- "rules_fired": [
1173
- "javascript.json-parse-returns-any"
1174
- ],
1175
- "layers_with_findings": [
1176
- "domain"
1177
- ],
1178
- "retrieved": [
1179
- "javascript.json-parse-returns-any",
1180
- "javascript.async-errors-need-explicit-handling",
1181
- "javascript.number-parsing-is-lossy",
1182
- "javascript.date-parsing-is-implementation-defined",
1183
- "javascript.loose-equality-and-falsy-defaults",
1184
- "javascript.validate-at-the-boundary-once"
1185
- ],
1186
- "stack": "javascript",
1187
- "routed_by": "extension",
1188
- "chars": 1401,
1189
- "dropped_chars": 0,
1190
- "coverage": "whole file",
1191
- "symbols_shown": 0,
1192
- "symbols_total": 0,
1193
- "file_findings": [
1194
- "javascript.async-errors-need-explicit-handling",
1195
- "javascript.date-parsing-is-implementation-defined",
1196
- "javascript.json-parse-returns-any",
1197
- "javascript.number-parsing-is-lossy"
1198
- ],
1199
- "file_finding_count": 4,
1200
- "file_suppressed": 0,
1201
- "cited": [
1202
- "javascript.async-errors-need-explicit-handling",
1203
- "javascript.date-parsing-is-implementation-defined",
1204
- "javascript.json-parse-returns-any",
1205
- "javascript.number-parsing-is-lossy"
1206
- ],
1207
- "cited_beyond_checkers": [],
1208
- "facts": 1,
1209
- "context_verdict": "UNKNOWN",
1210
- "context_checked": 0,
1211
- "names_declared": 1,
1212
- "names_mentioned": 1,
1213
- "answer_chars": 2583,
1214
- "path": "subjects/web/src/board.js"
1215
  }
1216
  ]
 
1
  [
2
  {
3
+ "verdict": "REJECT",
4
  "insufficient": [],
5
+ "seconds": 28.8,
6
+ "tokens_per_second": 26.3,
7
+ "rules_fired": [
8
+ "odoo.constrains-must-list-every-field-read",
9
+ "odoo.float-comparison-needs-float-utils",
10
+ "odoo.prefer-sql-constraint",
11
+ "odoo.sql-constraints-are-ignored-from-19"
12
+ ],
13
  "layers_with_findings": [
14
+ "context",
15
+ "domain",
16
+ "semantic"
17
  ],
18
  "retrieved": [
19
  "odoo.check-constraint-cannot-cross-tables",
20
  "odoo.stored-compute-needs-complete-depends",
21
+ "odoo.sudo-bypasses-record-rules",
22
  "odoo.float-comparison-needs-float-utils",
23
+ "odoo.sql-constraints-are-ignored-from-19",
24
+ "odoo.constrains-must-list-every-field-read"
25
  ],
26
  "stack": "odoo",
27
+ "routed_by": "extension",
28
  "chars": 2441,
29
  "dropped_chars": 0,
30
  "coverage": "whole file",
 
32
  "symbols_total": 0,
33
  "file_findings": [
34
  "odoo.float-comparison-needs-float-utils",
 
35
  "odoo.stored-compute-needs-complete-depends"
36
  ],
37
+ "file_finding_count": 2,
38
+ "file_advisory": 1,
39
  "file_suppressed": 0,
40
+ "cited": [
41
+ "odoo.float-comparison-needs-float-utils",
42
+ "odoo.stored-compute-needs-complete-depends",
43
+ "odoo.sudo-bypasses-record-rules"
44
+ ],
45
  "cited_beyond_checkers": [],
46
  "facts": 4,
47
+ "context_verdict": "WARN",
48
+ "context_checked": 20,
49
  "names_declared": 7,
50
  "names_mentioned": 2,
51
+ "answer_chars": 2117,
52
  "path": "subjects/odoo_module/models/fsm_job.py"
53
  },
54
  {
55
  "verdict": "WARN",
56
  "insufficient": [],
57
+ "seconds": 4.1,
58
+ "tokens_per_second": 26.8,
59
+ "rules_fired": [
60
+ "odoo.sql-constraints-are-ignored-from-19"
61
+ ],
62
  "layers_with_findings": [
63
+ "context",
64
  "structural"
65
  ],
66
  "retrieved": [
67
  "odoo.check-constraint-cannot-cross-tables",
68
  "odoo.stored-compute-needs-complete-depends",
69
+ "odoo.sudo-bypasses-record-rules",
70
  "odoo.float-comparison-needs-float-utils",
71
+ "odoo.sql-constraints-are-ignored-from-19",
72
+ "odoo.constrains-must-list-every-field-read"
73
  ],
74
  "stack": "odoo",
75
+ "routed_by": "extension",
76
  "chars": 2441,
77
  "dropped_chars": 0,
78
  "coverage": "whole file",
 
80
  "symbols_total": 0,
81
  "file_findings": [
82
  "odoo.float-comparison-needs-float-utils",
 
83
  "odoo.stored-compute-needs-complete-depends"
84
  ],
85
+ "file_finding_count": 2,
86
+ "file_advisory": 1,
87
  "file_suppressed": 0,
88
+ "cited": [
89
+ "odoo.float-comparison-needs-float-utils",
90
+ "odoo.sudo-bypasses-record-rules"
91
+ ],
92
  "cited_beyond_checkers": [],
93
  "facts": 4,
94
+ "context_verdict": "WARN",
95
+ "context_checked": 20,
96
  "names_declared": 7,
97
+ "names_mentioned": 1,
98
+ "answer_chars": 401,
99
  "path": "subjects/odoo_module/models/fsm_job.py"
100
  },
101
  {
102
  "verdict": "REJECT",
103
+ "insufficient": [],
104
+ "seconds": 13.5,
105
+ "tokens_per_second": 27.2,
106
+ "rules_fired": [
107
+ "odoo.constrains-must-list-every-field-read",
108
+ "odoo.float-comparison-needs-float-utils",
109
+ "odoo.sql-constraints-are-ignored-from-19"
110
  ],
 
 
 
111
  "layers_with_findings": [
112
+ "context",
113
+ "domain",
114
+ "semantic"
115
  ],
116
  "retrieved": [
117
  "odoo.check-constraint-cannot-cross-tables",
118
  "odoo.stored-compute-needs-complete-depends",
119
+ "odoo.sudo-bypasses-record-rules",
120
  "odoo.float-comparison-needs-float-utils",
121
+ "odoo.sql-constraints-are-ignored-from-19",
122
+ "odoo.constrains-must-list-every-field-read"
123
  ],
124
  "stack": "odoo",
125
+ "routed_by": "extension",
126
  "chars": 2441,
127
  "dropped_chars": 0,
128
  "coverage": "whole file",
 
130
  "symbols_total": 0,
131
  "file_findings": [
132
  "odoo.float-comparison-needs-float-utils",
 
133
  "odoo.stored-compute-needs-complete-depends"
134
  ],
135
+ "file_finding_count": 2,
136
+ "file_advisory": 1,
137
  "file_suppressed": 0,
138
  "cited": [
 
139
  "odoo.float-comparison-needs-float-utils",
140
+ "odoo.stored-compute-needs-complete-depends",
141
+ "odoo.sudo-bypasses-record-rules"
 
 
 
142
  ],
143
+ "cited_beyond_checkers": [],
144
  "facts": 4,
145
+ "context_verdict": "WARN",
146
+ "context_checked": 20,
147
  "names_declared": 7,
148
  "names_mentioned": 2,
149
+ "answer_chars": 1471,
150
  "path": "subjects/odoo_module/models/fsm_job.py"
151
  },
152
  {
153
  "verdict": "WARN",
154
  "insufficient": [],
155
+ "seconds": 4.2,
156
+ "tokens_per_second": 26.4,
157
+ "rules_fired": [
158
+ "odoo.sql-constraints-are-ignored-from-19"
159
+ ],
160
  "layers_with_findings": [
161
+ "context",
162
  "structural"
163
  ],
164
  "retrieved": [
165
  "odoo.check-constraint-cannot-cross-tables",
166
  "odoo.stored-compute-needs-complete-depends",
167
+ "odoo.sudo-bypasses-record-rules",
168
  "odoo.float-comparison-needs-float-utils",
169
+ "odoo.sql-constraints-are-ignored-from-19",
170
+ "odoo.constrains-must-list-every-field-read"
171
  ],
172
  "stack": "odoo",
173
+ "routed_by": "extension",
174
  "chars": 2441,
175
  "dropped_chars": 0,
176
  "coverage": "whole file",
 
178
  "symbols_total": 0,
179
  "file_findings": [
180
  "odoo.float-comparison-needs-float-utils",
 
181
  "odoo.stored-compute-needs-complete-depends"
182
  ],
183
+ "file_finding_count": 2,
184
+ "file_advisory": 1,
185
  "file_suppressed": 0,
186
  "cited": [
187
+ "odoo.float-comparison-needs-float-utils",
188
+ "odoo.sudo-bypasses-record-rules"
189
  ],
190
  "cited_beyond_checkers": [],
191
  "facts": 4,
192
+ "context_verdict": "WARN",
193
+ "context_checked": 20,
194
  "names_declared": 7,
195
  "names_mentioned": 1,
196
+ "answer_chars": 401,
197
  "path": "subjects/odoo_module/models/fsm_job.py"
198
  },
199
  {
200
  "verdict": "WARN",
201
  "insufficient": [],
202
+ "seconds": 17.8,
203
+ "tokens_per_second": 26.8,
204
  "rules_fired": [
205
+ "odoo.float-comparison-needs-float-utils",
206
+ "odoo.prefer-sql-constraint",
207
+ "odoo.sql-constraints-are-ignored-from-19"
208
  ],
209
  "layers_with_findings": [
210
+ "context",
211
+ "domain",
212
+ "semantic"
213
  ],
214
  "retrieved": [
215
  "odoo.check-constraint-cannot-cross-tables",
216
  "odoo.stored-compute-needs-complete-depends",
217
+ "odoo.sudo-bypasses-record-rules",
218
  "odoo.float-comparison-needs-float-utils",
219
+ "odoo.sql-constraints-are-ignored-from-19",
220
+ "odoo.constrains-must-list-every-field-read"
221
  ],
222
  "stack": "odoo",
223
+ "routed_by": "extension",
224
  "chars": 2441,
225
  "dropped_chars": 0,
226
  "coverage": "whole file",
 
228
  "symbols_total": 0,
229
  "file_findings": [
230
  "odoo.float-comparison-needs-float-utils",
 
231
  "odoo.stored-compute-needs-complete-depends"
232
  ],
233
+ "file_finding_count": 2,
234
+ "file_advisory": 1,
235
  "file_suppressed": 0,
236
+ "cited": [
237
+ "odoo.float-comparison-needs-float-utils",
238
+ "odoo.stored-compute-needs-complete-depends",
239
+ "odoo.sudo-bypasses-record-rules"
240
+ ],
241
  "cited_beyond_checkers": [],
242
  "facts": 4,
243
+ "context_verdict": "WARN",
244
+ "context_checked": 20,
245
  "names_declared": 7,
246
  "names_mentioned": 2,
247
+ "answer_chars": 1940,
248
  "path": "subjects/odoo_module/models/fsm_job.py"
249
  },
250
  {
251
+ "verdict": "WARN",
252
  "insufficient": [
253
  "9 of 20 symbols were not reviewed"
254
  ],
255
+ "seconds": 15.3,
256
+ "tokens_per_second": 27.6,
257
  "rules_fired": [
258
+ "odoo.depends-must-not-name-unread-fields",
259
  "odoo.prefer-sql-constraint",
260
+ "odoo.sql-constraints-are-ignored-from-19"
261
  ],
262
  "layers_with_findings": [
263
+ "context",
264
+ "domain",
265
  "semantic"
266
  ],
267
  "retrieved": [
268
  "odoo.check-constraint-cannot-cross-tables",
269
  "odoo.constrains-must-list-every-field-read",
270
  "odoo.stored-compute-needs-complete-depends",
271
+ "odoo.sudo-bypasses-record-rules",
272
  "odoo.prefer-sql-constraint",
273
+ "odoo.sql-constraints-are-ignored-from-19"
 
274
  ],
275
  "stack": "odoo",
276
+ "routed_by": "extension",
277
  "chars": 7929,
278
+ "dropped_chars": 2662,
279
  "coverage": "by symbol",
280
  "symbols_shown": 11,
281
  "symbols_total": 20,
282
  "file_findings": [
283
  "odoo.constrains-must-list-every-field-read",
 
284
  "odoo.stored-compute-needs-complete-depends"
285
  ],
286
+ "file_finding_count": 4,
287
+ "file_advisory": 2,
288
  "file_suppressed": 0,
289
  "cited": [
290
+ "odoo.constrains-must-list-every-field-read"
 
291
  ],
292
  "cited_beyond_checkers": [],
293
  "facts": 4,
294
+ "context_verdict": "WARN",
295
+ "context_checked": 20,
296
  "names_declared": 19,
297
+ "names_mentioned": 2,
298
+ "answer_chars": 1404,
299
  "path": "subjects/odoo_module/models/fsm_visit.py"
300
  },
301
  {
 
303
  "insufficient": [
304
  "9 of 20 symbols were not reviewed"
305
  ],
306
+ "seconds": 15.5,
307
+ "tokens_per_second": 27.3,
308
  "rules_fired": [
309
  "odoo.prefer-sql-constraint",
310
+ "odoo.sql-constraints-are-ignored-from-19",
311
  "odoo.stored-compute-needs-complete-depends"
312
  ],
313
  "layers_with_findings": [
314
+ "context",
315
+ "domain",
316
+ "semantic",
317
+ "structural"
318
  ],
319
  "retrieved": [
320
  "odoo.check-constraint-cannot-cross-tables",
321
  "odoo.constrains-must-list-every-field-read",
322
  "odoo.stored-compute-needs-complete-depends",
323
+ "odoo.sudo-bypasses-record-rules",
324
  "odoo.prefer-sql-constraint",
325
+ "odoo.sql-constraints-are-ignored-from-19"
 
326
  ],
327
  "stack": "odoo",
328
+ "routed_by": "extension",
329
  "chars": 7929,
330
+ "dropped_chars": 2662,
331
  "coverage": "by symbol",
332
  "symbols_shown": 11,
333
  "symbols_total": 20,
334
  "file_findings": [
335
  "odoo.constrains-must-list-every-field-read",
 
336
  "odoo.stored-compute-needs-complete-depends"
337
  ],
338
+ "file_finding_count": 4,
339
+ "file_advisory": 2,
340
  "file_suppressed": 0,
341
  "cited": [
342
  "odoo.constrains-must-list-every-field-read",
343
+ "odoo.prefer-sql-constraint",
344
+ "odoo.stored-compute-needs-complete-depends",
345
+ "odoo.sudo-bypasses-record-rules"
346
  ],
347
  "cited_beyond_checkers": [],
348
  "facts": 4,
349
+ "context_verdict": "WARN",
350
+ "context_checked": 20,
351
  "names_declared": 19,
352
+ "names_mentioned": 2,
353
+ "answer_chars": 1676,
354
  "path": "subjects/odoo_module/models/fsm_visit.py"
355
  },
356
  {
 
358
  "insufficient": [
359
  "9 of 20 symbols were not reviewed"
360
  ],
361
+ "seconds": 18.2,
362
+ "tokens_per_second": 26.5,
363
  "rules_fired": [
364
  "odoo.prefer-sql-constraint",
365
+ "odoo.sql-constraints-are-ignored-from-19",
366
  "odoo.stored-compute-needs-complete-depends"
367
  ],
368
  "layers_with_findings": [
369
+ "context",
370
+ "domain",
371
+ "semantic",
372
+ "structural",
373
+ "syntactic"
374
  ],
375
  "retrieved": [
376
  "odoo.check-constraint-cannot-cross-tables",
377
  "odoo.constrains-must-list-every-field-read",
378
  "odoo.stored-compute-needs-complete-depends",
379
+ "odoo.sudo-bypasses-record-rules",
380
  "odoo.prefer-sql-constraint",
381
+ "odoo.sql-constraints-are-ignored-from-19"
 
382
  ],
383
  "stack": "odoo",
384
+ "routed_by": "extension",
385
  "chars": 7929,
386
+ "dropped_chars": 2662,
387
  "coverage": "by symbol",
388
  "symbols_shown": 11,
389
  "symbols_total": 20,
390
  "file_findings": [
391
  "odoo.constrains-must-list-every-field-read",
 
392
  "odoo.stored-compute-needs-complete-depends"
393
  ],
394
+ "file_finding_count": 4,
395
+ "file_advisory": 2,
396
  "file_suppressed": 0,
397
  "cited": [
398
  "odoo.constrains-must-list-every-field-read",
399
+ "odoo.prefer-sql-constraint",
400
+ "odoo.stored-compute-needs-complete-depends",
401
+ "odoo.sudo-bypasses-record-rules"
402
  ],
403
  "cited_beyond_checkers": [],
404
  "facts": 4,
405
+ "context_verdict": "WARN",
406
+ "context_checked": 20,
407
  "names_declared": 19,
408
+ "names_mentioned": 2,
409
+ "answer_chars": 1936,
410
  "path": "subjects/odoo_module/models/fsm_visit.py"
411
  },
412
  {
413
+ "verdict": "WARN",
414
  "insufficient": [
415
  "9 of 20 symbols were not reviewed"
416
  ],
417
+ "seconds": 14.8,
418
+ "tokens_per_second": 27.0,
419
  "rules_fired": [
420
+ "odoo.depends-must-not-name-unread-fields",
421
  "odoo.prefer-sql-constraint",
422
+ "odoo.sql-constraints-are-ignored-from-19"
423
  ],
424
  "layers_with_findings": [
425
+ "context",
426
+ "domain",
427
  "semantic"
428
  ],
429
  "retrieved": [
430
  "odoo.check-constraint-cannot-cross-tables",
431
  "odoo.constrains-must-list-every-field-read",
432
  "odoo.stored-compute-needs-complete-depends",
433
+ "odoo.sudo-bypasses-record-rules",
434
  "odoo.prefer-sql-constraint",
435
+ "odoo.sql-constraints-are-ignored-from-19"
 
436
  ],
437
  "stack": "odoo",
438
+ "routed_by": "extension",
439
  "chars": 7929,
440
+ "dropped_chars": 2662,
441
  "coverage": "by symbol",
442
  "symbols_shown": 11,
443
  "symbols_total": 20,
444
  "file_findings": [
445
  "odoo.constrains-must-list-every-field-read",
 
446
  "odoo.stored-compute-needs-complete-depends"
447
  ],
448
+ "file_finding_count": 4,
449
+ "file_advisory": 2,
450
  "file_suppressed": 0,
451
  "cited": [
452
+ "odoo.constrains-must-list-every-field-read"
 
453
  ],
454
  "cited_beyond_checkers": [],
455
  "facts": 4,
456
+ "context_verdict": "WARN",
457
+ "context_checked": 20,
458
  "names_declared": 19,
459
+ "names_mentioned": 2,
460
+ "answer_chars": 1584,
461
  "path": "subjects/odoo_module/models/fsm_visit.py"
462
  },
463
  {
 
465
  "insufficient": [
466
  "9 of 20 symbols were not reviewed"
467
  ],
468
+ "seconds": 13.9,
469
+ "tokens_per_second": 27.1,
470
  "rules_fired": [
471
+ "odoo.prefer-sql-constraint",
472
+ "odoo.sql-constraints-are-ignored-from-19",
473
  "odoo.stored-compute-needs-complete-depends"
474
  ],
475
  "layers_with_findings": [
476
+ "context",
477
+ "domain",
478
  "semantic",
479
  "structural"
480
  ],
 
482
  "odoo.check-constraint-cannot-cross-tables",
483
  "odoo.constrains-must-list-every-field-read",
484
  "odoo.stored-compute-needs-complete-depends",
485
+ "odoo.sudo-bypasses-record-rules",
486
  "odoo.prefer-sql-constraint",
487
+ "odoo.sql-constraints-are-ignored-from-19"
 
488
  ],
489
  "stack": "odoo",
490
+ "routed_by": "extension",
491
  "chars": 7929,
492
+ "dropped_chars": 2662,
493
  "coverage": "by symbol",
494
  "symbols_shown": 11,
495
  "symbols_total": 20,
496
  "file_findings": [
497
  "odoo.constrains-must-list-every-field-read",
 
498
  "odoo.stored-compute-needs-complete-depends"
499
  ],
500
+ "file_finding_count": 4,
501
+ "file_advisory": 2,
502
  "file_suppressed": 0,
503
  "cited": [
504
  "odoo.constrains-must-list-every-field-read",
505
  "odoo.prefer-sql-constraint",
506
+ "odoo.stored-compute-needs-complete-depends",
507
+ "odoo.sudo-bypasses-record-rules"
508
  ],
509
  "cited_beyond_checkers": [],
510
  "facts": 4,
511
+ "context_verdict": "WARN",
512
+ "context_checked": 20,
513
  "names_declared": 19,
514
  "names_mentioned": 2,
515
+ "answer_chars": 1444,
516
  "path": "subjects/odoo_module/models/fsm_visit.py"
517
  },
518
  {
519
+ "verdict": "PASS",
520
  "insufficient": [],
521
+ "seconds": 22.4,
522
+ "tokens_per_second": 26.9,
523
  "rules_fired": [
524
+ "python.decimal-for-money-not-float"
 
 
525
  ],
526
  "layers_with_findings": [
527
+ "adversarial"
 
528
  ],
529
  "retrieved": [
530
+ "odoo.check-constraint-cannot-cross-tables",
531
  "python.assert-is-not-validation",
532
  "python.subprocess-shell-true-is-injection",
 
533
  "python.bare-except-catches-the-exit",
534
  "python.mutable-default-argument",
535
+ "python.eval-and-pickle-execute-their-input"
536
  ],
537
+ "stack": "odoo",
538
+ "routed_by": "extension",
539
  "chars": 2033,
540
  "dropped_chars": 0,
541
  "coverage": "whole file",
 
544
  "file_findings": [
545
  "python.assert-is-not-validation",
546
  "python.bare-except-catches-the-exit",
 
547
  "python.mutable-default-argument",
 
 
548
  "python.subprocess-shell-true-is-injection"
549
  ],
550
+ "file_finding_count": 4,
551
+ "file_advisory": 0,
552
  "file_suppressed": 0,
553
  "cited": [
554
  "python.assert-is-not-validation",
555
  "python.bare-except-catches-the-exit",
 
556
  "python.mutable-default-argument",
 
 
557
  "python.subprocess-shell-true-is-injection"
558
  ],
559
  "cited_beyond_checkers": [],
560
  "facts": 0,
561
+ "context_verdict": "PASS",
562
+ "context_checked": 20,
563
  "names_declared": 5,
564
  "names_mentioned": 5,
565
+ "answer_chars": 2300,
566
  "path": "subjects/etl/load_prices.py"
567
  },
568
  {
569
+ "verdict": "PASS",
570
  "insufficient": [],
571
+ "seconds": 25.1,
572
+ "tokens_per_second": 26.8,
573
  "rules_fired": [
574
+ "python.decimal-for-money-not-float"
 
 
575
  ],
576
  "layers_with_findings": [
577
+ "adversarial"
578
  ],
579
  "retrieved": [
580
+ "odoo.check-constraint-cannot-cross-tables",
581
  "python.assert-is-not-validation",
582
  "python.subprocess-shell-true-is-injection",
 
583
  "python.bare-except-catches-the-exit",
584
  "python.mutable-default-argument",
585
+ "python.eval-and-pickle-execute-their-input"
586
  ],
587
+ "stack": "odoo",
588
+ "routed_by": "extension",
589
  "chars": 2033,
590
  "dropped_chars": 0,
591
  "coverage": "whole file",
 
594
  "file_findings": [
595
  "python.assert-is-not-validation",
596
  "python.bare-except-catches-the-exit",
 
597
  "python.mutable-default-argument",
 
 
598
  "python.subprocess-shell-true-is-injection"
599
  ],
600
+ "file_finding_count": 4,
601
+ "file_advisory": 0,
602
  "file_suppressed": 0,
603
  "cited": [
604
  "python.assert-is-not-validation",
605
  "python.bare-except-catches-the-exit",
 
606
  "python.mutable-default-argument",
 
607
  "python.subprocess-shell-true-is-injection"
608
  ],
609
  "cited_beyond_checkers": [],
610
  "facts": 0,
611
+ "context_verdict": "PASS",
612
+ "context_checked": 20,
613
  "names_declared": 5,
614
  "names_mentioned": 5,
615
+ "answer_chars": 2793,
616
  "path": "subjects/etl/load_prices.py"
617
  },
618
  {
619
+ "verdict": "PASS",
620
  "insufficient": [],
621
+ "seconds": 17.0,
622
+ "tokens_per_second": 26.9,
623
  "rules_fired": [
624
+ "python.decimal-for-money-not-float"
 
 
625
  ],
626
  "layers_with_findings": [
627
+ "adversarial"
628
  ],
629
  "retrieved": [
630
+ "odoo.check-constraint-cannot-cross-tables",
631
  "python.assert-is-not-validation",
632
  "python.subprocess-shell-true-is-injection",
 
633
  "python.bare-except-catches-the-exit",
634
  "python.mutable-default-argument",
635
+ "python.eval-and-pickle-execute-their-input"
636
  ],
637
+ "stack": "odoo",
638
+ "routed_by": "extension",
639
  "chars": 2033,
640
  "dropped_chars": 0,
641
  "coverage": "whole file",
 
644
  "file_findings": [
645
  "python.assert-is-not-validation",
646
  "python.bare-except-catches-the-exit",
 
647
  "python.mutable-default-argument",
 
 
648
  "python.subprocess-shell-true-is-injection"
649
  ],
650
+ "file_finding_count": 4,
651
+ "file_advisory": 0,
652
  "file_suppressed": 0,
653
  "cited": [
654
  "python.mutable-default-argument"
655
  ],
656
  "cited_beyond_checkers": [],
657
  "facts": 0,
658
+ "context_verdict": "PASS",
659
+ "context_checked": 20,
660
  "names_declared": 5,
661
  "names_mentioned": 5,
662
+ "answer_chars": 1836,
663
  "path": "subjects/etl/load_prices.py"
664
  },
665
  {
666
+ "verdict": "PASS",
667
  "insufficient": [],
668
+ "seconds": 21.1,
669
+ "tokens_per_second": 26.6,
670
  "rules_fired": [
671
+ "python.decimal-for-money-not-float"
 
 
672
  ],
673
  "layers_with_findings": [
674
+ "adversarial"
675
  ],
676
  "retrieved": [
677
+ "odoo.check-constraint-cannot-cross-tables",
678
  "python.assert-is-not-validation",
679
  "python.subprocess-shell-true-is-injection",
 
680
  "python.bare-except-catches-the-exit",
681
  "python.mutable-default-argument",
682
+ "python.eval-and-pickle-execute-their-input"
683
  ],
684
+ "stack": "odoo",
685
+ "routed_by": "extension",
686
  "chars": 2033,
687
  "dropped_chars": 0,
688
  "coverage": "whole file",
 
691
  "file_findings": [
692
  "python.assert-is-not-validation",
693
  "python.bare-except-catches-the-exit",
 
694
  "python.mutable-default-argument",
 
 
695
  "python.subprocess-shell-true-is-injection"
696
  ],
697
+ "file_finding_count": 4,
698
+ "file_advisory": 0,
699
  "file_suppressed": 0,
700
+ "cited": [
701
+ "odoo.check-constraint-cannot-cross-tables",
702
+ "python.assert-is-not-validation",
703
+ "python.bare-except-catches-the-exit",
704
+ "python.mutable-default-argument",
705
+ "python.subprocess-shell-true-is-injection"
706
+ ],
707
+ "cited_beyond_checkers": [
708
+ "odoo.check-constraint-cannot-cross-tables"
709
+ ],
710
  "facts": 0,
711
+ "context_verdict": "PASS",
712
+ "context_checked": 20,
713
  "names_declared": 5,
714
  "names_mentioned": 5,
715
+ "answer_chars": 2265,
716
  "path": "subjects/etl/load_prices.py"
717
  },
718
  {
719
+ "verdict": "PASS",
720
  "insufficient": [],
721
+ "seconds": 25.0,
722
+ "tokens_per_second": 26.7,
723
  "rules_fired": [
724
+ "python.decimal-for-money-not-float"
 
 
725
  ],
726
  "layers_with_findings": [
727
+ "adversarial"
728
  ],
729
  "retrieved": [
730
+ "odoo.check-constraint-cannot-cross-tables",
731
  "python.assert-is-not-validation",
732
  "python.subprocess-shell-true-is-injection",
 
733
  "python.bare-except-catches-the-exit",
734
  "python.mutable-default-argument",
735
+ "python.eval-and-pickle-execute-their-input"
736
  ],
737
+ "stack": "odoo",
738
+ "routed_by": "extension",
739
  "chars": 2033,
740
  "dropped_chars": 0,
741
  "coverage": "whole file",
 
744
  "file_findings": [
745
  "python.assert-is-not-validation",
746
  "python.bare-except-catches-the-exit",
 
747
  "python.mutable-default-argument",
 
 
748
  "python.subprocess-shell-true-is-injection"
749
  ],
750
+ "file_finding_count": 4,
751
+ "file_advisory": 0,
752
  "file_suppressed": 0,
753
  "cited": [
754
+ "python.assert-is-not-validation",
755
+ "python.bare-except-catches-the-exit",
756
+ "python.mutable-default-argument",
757
+ "python.subprocess-shell-true-is-injection"
758
  ],
759
  "cited_beyond_checkers": [],
760
  "facts": 0,
761
+ "context_verdict": "PASS",
762
+ "context_checked": 20,
763
  "names_declared": 5,
764
  "names_mentioned": 5,
765
+ "answer_chars": 2798,
766
  "path": "subjects/etl/load_prices.py"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
  }
768
  ]
nexa.py CHANGED
@@ -309,16 +309,16 @@ COMMANDS = {
309
  "refused by name -- Odoo rules over a .ts file find nothing, and\n"
310
  "nothing reads as a clean bill of health.\n\n"
311
  " --no-model\n"
312
- " stop after the checkers. No generation, no Ollama, a fifth\n"
313
- " of a second, and the same output every run.\n\n"
314
- " What the model adds on top of the checkers was measured\n"
315
- " twice, over 25 reviews each time: 1 rule cited beyond them\n"
316
- " on one run and 3 on the next, and not the same rules. The\n"
317
- " checkers produced the identical 23 findings both times.\n\n"
318
  " So the generative half stays the default, because a person\n"
319
  " reading a file wants the part no pattern can decide. And a\n"
320
- " gate does not, because three of five files gave different\n"
321
- " verdicts between those two runs.\n\n"
322
  " It prints no verdict, because there is nothing to have a\n"
323
  " verdict about: a clean run says no pattern fired, and says\n"
324
  " so in those words. Exit 1 if anything did.",
 
309
  "refused by name -- Odoo rules over a .ts file find nothing, and\n"
310
  "nothing reads as a clean bill of health.\n\n"
311
  " --no-model\n"
312
+ " stop after the checkers. No generation, no Ollama, under a\n"
313
+ " second, and the same output every run.\n\n"
314
+ " What the model adds on top of the checkers is measured by\n"
315
+ " nexa eval --check: it rarely cites a rule the checkers had\n"
316
+ " not already found, and the checkers give the identical\n"
317
+ " findings on every run.\n\n"
318
  " So the generative half stays the default, because a person\n"
319
  " reading a file wants the part no pattern can decide. And a\n"
320
+ " gate does not, because the same file does not always reach\n"
321
+ " the same verdict twice.\n\n"
322
  " It prints no verdict, because there is nothing to have a\n"
323
  " verdict about: a clean run says no pattern fired, and says\n"
324
  " so in those words. Exit 1 if anything did.",
nexa_cmds.py CHANGED
@@ -326,10 +326,10 @@ def cmd_check(args):
326
  print()
327
 
328
  # Everything above is the checkers, and the checkers are the whole of
329
- # --no-model. Measured over 25 reviews, the model added ONE citation the
330
- # checkers had not already made, so the generative half is buying the part
331
- # no pattern can decide -- and a pipeline that only wants the part patterns
332
- # CAN decide should not be paying seconds and non-determinism for it.
333
  if no_model:
334
  print(" {0}--no-model: no model ran{1}".format(DIM, RESET))
335
  print(" {0}This is not a verdict about the file. What a checker can "
 
326
  print()
327
 
328
  # Everything above is the checkers, and the checkers are the whole of
329
+ # --no-model. Measured by `nexa eval --check`, the model rarely cites a rule
330
+ # the checkers had not already found, so the generative half is buying the
331
+ # part no pattern can decide -- and a pipeline that only wants the part
332
+ # patterns CAN decide should not be paying seconds and non-determinism for it.
333
  if no_model:
334
  print(" {0}--no-model: no model ran{1}".format(DIM, RESET))
335
  print(" {0}This is not a verdict about the file. What a checker can "
retrieval-2026-09.json ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "generated_utc": "2026-09-27T21:48:25+00:00",
3
+ "k": 6,
4
+ "embedder": "nomic-embed-text",
5
+ "corpus_sha256": "85e403bf09cb22dee3c0401af08234ec5197e78a070e2f11cd66a72fb2c590a6",
6
+ "rows": [
7
+ {
8
+ "id": "odoo.onchange-is-not-validation",
9
+ "stack": "odoo",
10
+ "probe": "I added an onchange to reject a bad value but a CSV import still creates the record",
11
+ "pinned": false,
12
+ "rank": 2,
13
+ "rank_unpinned": 1,
14
+ "score": 0.7373,
15
+ "instead": [
16
+ "odoo.check-constraint-cannot-cross-tables",
17
+ "odoo.constrains-iterate-recordset",
18
+ "odoo.noupdate-on-user-editable-data"
19
+ ]
20
+ },
21
+ {
22
+ "id": "odoo.constrains-must-list-every-field-read",
23
+ "stack": "odoo",
24
+ "probe": "my constraint does not run when only one of the two dates is written",
25
+ "pinned": false,
26
+ "rank": 2,
27
+ "rank_unpinned": 1,
28
+ "score": 0.7273,
29
+ "instead": [
30
+ "odoo.check-constraint-cannot-cross-tables",
31
+ "odoo.prefer-sql-constraint",
32
+ "odoo.sql-constraints-are-ignored-from-19"
33
+ ]
34
+ },
35
+ {
36
+ "id": "odoo.constrains-ignores-dotted-names",
37
+ "stack": "odoo",
38
+ "probe": "constrains on partner_id.country_id never fires and raises no error",
39
+ "pinned": false,
40
+ "rank": 2,
41
+ "rank_unpinned": 1,
42
+ "score": 0.7002,
43
+ "instead": [
44
+ "odoo.check-constraint-cannot-cross-tables",
45
+ "odoo.constrains-must-list-every-field-read",
46
+ "odoo.onchange-is-not-validation"
47
+ ]
48
+ },
49
+ {
50
+ "id": "odoo.float-comparison-needs-float-utils",
51
+ "stack": "odoo",
52
+ "probe": "comparing a price or a quantity to zero gives the wrong answer sometimes",
53
+ "pinned": false,
54
+ "rank": 2,
55
+ "rank_unpinned": 1,
56
+ "score": 0.7337,
57
+ "instead": [
58
+ "odoo.check-constraint-cannot-cross-tables",
59
+ "python.decimal-for-money-not-float",
60
+ "python.assert-is-not-validation"
61
+ ]
62
+ },
63
+ {
64
+ "id": "odoo.constrains-iterate-recordset",
65
+ "stack": "odoo",
66
+ "probe": "expected singleton error when a batch import writes several records at once",
67
+ "pinned": false,
68
+ "rank": 2,
69
+ "rank_unpinned": 1,
70
+ "score": 0.8097,
71
+ "instead": [
72
+ "odoo.check-constraint-cannot-cross-tables",
73
+ "odoo.sql-constraints-are-ignored-from-19",
74
+ "odoo.onchange-is-not-validation"
75
+ ]
76
+ },
77
+ {
78
+ "id": "odoo.prefer-sql-constraint",
79
+ "stack": "odoo",
80
+ "probe": "should this check be a database constraint or an api.constrains method",
81
+ "pinned": false,
82
+ "rank": 2,
83
+ "rank_unpinned": 1,
84
+ "score": 0.8032,
85
+ "instead": [
86
+ "odoo.check-constraint-cannot-cross-tables",
87
+ "odoo.constrains-must-list-every-field-read",
88
+ "odoo.constrains-ignores-dotted-names"
89
+ ]
90
+ },
91
+ {
92
+ "id": "odoo.sql-constraints-are-ignored-from-19",
93
+ "stack": "odoo",
94
+ "probe": "after upgrading to Odoo 19 my unique constraint is not enforced and duplicates are saved",
95
+ "pinned": false,
96
+ "rank": 2,
97
+ "rank_unpinned": 1,
98
+ "score": 0.7976,
99
+ "instead": [
100
+ "odoo.check-constraint-cannot-cross-tables",
101
+ "odoo.prefer-sql-constraint",
102
+ "odoo.constrains-iterate-recordset"
103
+ ]
104
+ },
105
+ {
106
+ "id": "odoo.check-constraint-cannot-cross-tables",
107
+ "stack": "odoo",
108
+ "probe": "my CHECK constraint will not install because it reads the product stock",
109
+ "pinned": true,
110
+ "rank": 1,
111
+ "rank_unpinned": null,
112
+ "score": 1.0,
113
+ "instead": [
114
+ "odoo.sql-constraints-are-ignored-from-19",
115
+ "odoo.constrains-must-list-every-field-read",
116
+ "odoo.prefer-sql-constraint"
117
+ ]
118
+ },
119
+ {
120
+ "id": "odoo.check-company-on-relations",
121
+ "stack": "odoo",
122
+ "probe": "a user in one company can select another company's journal on a document",
123
+ "pinned": false,
124
+ "rank": 2,
125
+ "rank_unpinned": 1,
126
+ "score": 0.6868,
127
+ "instead": [
128
+ "odoo.check-constraint-cannot-cross-tables",
129
+ "odoo.sudo-bypasses-record-rules",
130
+ "odoo.onchange-is-not-validation"
131
+ ]
132
+ },
133
+ {
134
+ "id": "odoo.stored-compute-needs-complete-depends",
135
+ "stack": "odoo",
136
+ "probe": "my stored computed total goes stale and never recomputes after a line changes",
137
+ "pinned": false,
138
+ "rank": 2,
139
+ "rank_unpinned": 1,
140
+ "score": 0.7324,
141
+ "instead": [
142
+ "odoo.check-constraint-cannot-cross-tables",
143
+ "odoo.depends-must-not-name-unread-fields",
144
+ "python.decimal-for-money-not-float"
145
+ ]
146
+ },
147
+ {
148
+ "id": "odoo.ondelete-is-a-decision",
149
+ "stack": "odoo",
150
+ "probe": "what happens to the child records when someone deletes the parent",
151
+ "pinned": false,
152
+ "rank": 2,
153
+ "rank_unpinned": 1,
154
+ "score": 0.5918,
155
+ "instead": [
156
+ "odoo.check-constraint-cannot-cross-tables",
157
+ "odoo.sudo-bypasses-record-rules",
158
+ "odoo.check-company-on-relations"
159
+ ]
160
+ },
161
+ {
162
+ "id": "odoo.recursion-check-on-hierarchy",
163
+ "stack": "odoo",
164
+ "probe": "the server hangs when a category is made its own parent",
165
+ "pinned": false,
166
+ "rank": 2,
167
+ "rank_unpinned": 1,
168
+ "score": 0.6896,
169
+ "instead": [
170
+ "odoo.check-constraint-cannot-cross-tables",
171
+ "odoo.constrains-must-list-every-field-read",
172
+ "odoo.constrains-ignores-dotted-names"
173
+ ]
174
+ },
175
+ {
176
+ "id": "odoo.schema-change-needs-migration",
177
+ "stack": "odoo",
178
+ "probe": "I made an existing field required and the module upgrade fails on live data",
179
+ "pinned": false,
180
+ "rank": 2,
181
+ "rank_unpinned": 1,
182
+ "score": 0.7745,
183
+ "instead": [
184
+ "odoo.check-constraint-cannot-cross-tables",
185
+ "odoo.stored-compute-needs-complete-depends",
186
+ "odoo.noupdate-on-user-editable-data"
187
+ ]
188
+ },
189
+ {
190
+ "id": "odoo.sudo-bypasses-record-rules",
191
+ "stack": "odoo",
192
+ "probe": "a search returns records belonging to every company instead of the current one",
193
+ "pinned": false,
194
+ "rank": 3,
195
+ "rank_unpinned": 2,
196
+ "score": 0.7199,
197
+ "instead": [
198
+ "odoo.check-constraint-cannot-cross-tables",
199
+ "odoo.check-company-on-relations",
200
+ "odoo.constrains-must-list-every-field-read"
201
+ ]
202
+ },
203
+ {
204
+ "id": "odoo.no-monkey-patching",
205
+ "stack": "odoo",
206
+ "probe": "change what a core sale.order method does without editing the sale module",
207
+ "pinned": false,
208
+ "rank": 2,
209
+ "rank_unpinned": 1,
210
+ "score": 0.6225,
211
+ "instead": [
212
+ "odoo.check-constraint-cannot-cross-tables",
213
+ "odoo.schema-change-needs-migration",
214
+ "odoo.onchange-is-not-validation"
215
+ ]
216
+ },
217
+ {
218
+ "id": "odoo.noupdate-on-user-editable-data",
219
+ "stack": "odoo",
220
+ "probe": "the customer's configuration resets itself every time we upgrade the module",
221
+ "pinned": false,
222
+ "rank": 3,
223
+ "rank_unpinned": 2,
224
+ "score": 0.6179,
225
+ "instead": [
226
+ "odoo.check-constraint-cannot-cross-tables",
227
+ "odoo.schema-change-needs-migration",
228
+ "odoo.no-monkey-patching"
229
+ ]
230
+ },
231
+ {
232
+ "id": "odoo.commit-inside-a-loop-splits-the-transaction",
233
+ "stack": "odoo",
234
+ "probe": "half my records were updated and the rest were not after the job raised",
235
+ "pinned": false,
236
+ "rank": 6,
237
+ "rank_unpinned": 5,
238
+ "score": 0.5035,
239
+ "instead": [
240
+ "odoo.check-constraint-cannot-cross-tables",
241
+ "odoo.noupdate-on-user-editable-data",
242
+ "odoo.check-company-on-relations"
243
+ ]
244
+ },
245
+ {
246
+ "id": "odoo.sql-needs-query-parameters",
247
+ "stack": "odoo",
248
+ "probe": "a raw query breaks when a partner name contains an apostrophe",
249
+ "pinned": false,
250
+ "rank": 2,
251
+ "rank_unpinned": 1,
252
+ "score": 0.6372,
253
+ "instead": [
254
+ "odoo.check-constraint-cannot-cross-tables",
255
+ "odoo.check-company-on-relations",
256
+ "odoo.constrains-ignores-dotted-names"
257
+ ]
258
+ },
259
+ {
260
+ "id": "odoo.depends-must-not-name-unread-fields",
261
+ "stack": "odoo",
262
+ "probe": "my api.depends lists a field the compute never uses",
263
+ "pinned": false,
264
+ "rank": 3,
265
+ "rank_unpinned": 2,
266
+ "score": 0.7893,
267
+ "instead": [
268
+ "odoo.check-constraint-cannot-cross-tables",
269
+ "odoo.stored-compute-needs-complete-depends",
270
+ "odoo.constrains-ignores-dotted-names"
271
+ ]
272
+ },
273
+ {
274
+ "id": "odoo.except-order-follows-the-hierarchy",
275
+ "stack": "odoo",
276
+ "probe": "my except AccessError block never runs and the API returns 409",
277
+ "pinned": false,
278
+ "rank": 2,
279
+ "rank_unpinned": 1,
280
+ "score": 0.6631,
281
+ "instead": [
282
+ "odoo.check-constraint-cannot-cross-tables",
283
+ "odoo.constrains-must-list-every-field-read",
284
+ "python.eval-and-pickle-execute-their-input"
285
+ ]
286
+ },
287
+ {
288
+ "id": "python.assert-is-not-validation",
289
+ "stack": "odoo",
290
+ "probe": "check that an amount is positive before using it",
291
+ "pinned": false,
292
+ "rank": 2,
293
+ "rank_unpinned": 1,
294
+ "score": 0.5731,
295
+ "instead": [
296
+ "odoo.check-constraint-cannot-cross-tables",
297
+ "python.decimal-for-money-not-float",
298
+ "odoo.float-comparison-needs-float-utils"
299
+ ]
300
+ },
301
+ {
302
+ "id": "python.mutable-default-argument",
303
+ "stack": "odoo",
304
+ "probe": "a list keeps items from the previous call to the same function",
305
+ "pinned": false,
306
+ "rank": 2,
307
+ "rank_unpinned": 1,
308
+ "score": 0.623,
309
+ "instead": [
310
+ "odoo.check-constraint-cannot-cross-tables",
311
+ "odoo.constrains-must-list-every-field-read",
312
+ "odoo.stored-compute-needs-complete-depends"
313
+ ]
314
+ },
315
+ {
316
+ "id": "python.bare-except-catches-the-exit",
317
+ "stack": "odoo",
318
+ "probe": "ctrl-c stops working and the script will not exit",
319
+ "pinned": false,
320
+ "rank": 2,
321
+ "rank_unpinned": 1,
322
+ "score": 0.6574,
323
+ "instead": [
324
+ "odoo.check-constraint-cannot-cross-tables",
325
+ "python.subprocess-shell-true-is-injection",
326
+ "python.assert-is-not-validation"
327
+ ]
328
+ },
329
+ {
330
+ "id": "python.eval-and-pickle-execute-their-input",
331
+ "stack": "odoo",
332
+ "probe": "turn a config string sent by a client into a Python object",
333
+ "pinned": false,
334
+ "rank": 5,
335
+ "rank_unpinned": 4,
336
+ "score": 0.5698,
337
+ "instead": [
338
+ "odoo.check-constraint-cannot-cross-tables",
339
+ "odoo.sql-needs-query-parameters",
340
+ "odoo.onchange-is-not-validation"
341
+ ]
342
+ },
343
+ {
344
+ "id": "python.subprocess-shell-true-is-injection",
345
+ "stack": "odoo",
346
+ "probe": "run a command line built from a filename the user gave me",
347
+ "pinned": false,
348
+ "rank": 2,
349
+ "rank_unpinned": 1,
350
+ "score": 0.5807,
351
+ "instead": [
352
+ "odoo.check-constraint-cannot-cross-tables",
353
+ "python.eval-and-pickle-execute-their-input",
354
+ "odoo.sql-needs-query-parameters"
355
+ ]
356
+ },
357
+ {
358
+ "id": "python.path-join-does-not-contain-a-path",
359
+ "stack": "odoo",
360
+ "probe": "open a file the user named inside my uploads directory",
361
+ "pinned": false,
362
+ "rank": 2,
363
+ "rank_unpinned": 1,
364
+ "score": 0.5659,
365
+ "instead": [
366
+ "odoo.check-constraint-cannot-cross-tables",
367
+ "python.eval-and-pickle-execute-their-input",
368
+ "odoo.check-company-on-relations"
369
+ ]
370
+ },
371
+ {
372
+ "id": "python.decimal-for-money-not-float",
373
+ "stack": "odoo",
374
+ "probe": "totals are off by a fraction of a penny after adding prices up",
375
+ "pinned": false,
376
+ "rank": 3,
377
+ "rank_unpinned": 2,
378
+ "score": 0.5922,
379
+ "instead": [
380
+ "odoo.check-constraint-cannot-cross-tables",
381
+ "odoo.float-comparison-needs-float-utils",
382
+ "python.assert-is-not-validation"
383
+ ]
384
+ },
385
+ {
386
+ "id": "python.naive-and-aware-datetimes-do-not-compare",
387
+ "stack": "odoo",
388
+ "probe": "TypeError comparing offset-naive and offset-aware datetimes",
389
+ "pinned": false,
390
+ "rank": 2,
391
+ "rank_unpinned": 1,
392
+ "score": 0.7583,
393
+ "instead": [
394
+ "odoo.check-constraint-cannot-cross-tables",
395
+ "odoo.constrains-must-list-every-field-read",
396
+ "odoo.float-comparison-needs-float-utils"
397
+ ]
398
+ }
399
+ ]
400
+ }
subjects/README.md CHANGED
@@ -22,9 +22,9 @@ the file:
22
  it — do the retrieved rules declare a range covering 17.0? — which is the entire
23
  difference between `check` and `ask`.
24
  - `etl/load_prices.py` has no manifest above it and imports no odoo. It is there
25
- because the corpus composes eight plain-Python rules in alongside the nineteen
26
- ORM ones, and this is the file that shows what a review says when only the
27
- first eight can match.
28
  - `odoo_module/models/fsm_visit.py` is 7,929 characters against a 6,000-character
29
  budget, and three of its six checker findings sit in its last method. It is the
30
  file that shows whether the excerpt is chosen well.
 
22
  it — do the retrieved rules declare a range covering 17.0? — which is the entire
23
  difference between `check` and `ask`.
24
  - `etl/load_prices.py` has no manifest above it and imports no odoo. It is there
25
+ because the corpus composes its plain-Python rules in alongside the ORM ones,
26
+ and this is the file that shows what a review says when only the plain-Python
27
+ ones can match.
28
  - `odoo_module/models/fsm_visit.py` is 7,929 characters against a 6,000-character
29
  budget, and three of its six checker findings sit in its last method. It is the
30
  file that shows whether the excerpt is chosen well.
tests/test_eval.py CHANGED
@@ -294,6 +294,27 @@ def test_the_report_counts_live_findings_and_says_so_about_the_rest(capsys):
294
  assert "3 more sat on lines another tool had already flagged" in out
295
 
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  # ----------------------------------------------------- the confidence interval
298
 
299
  def test_wilson_stays_inside_zero_and_one_at_tiny_n():
@@ -439,3 +460,17 @@ def test_a_k_that_cannot_fail_says_so(capsys):
439
  rows = [retrieval_row("odoo.onchange-is-not-validation")]
440
  evalmod.summarise_retrieval(rows, k=40)
441
  assert "nothing can miss" in capsys.readouterr().out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  assert "3 more sat on lines another tool had already flagged" in out
295
 
296
 
297
+ def test_a_review_counts_findings_the_way_check_does(fake_ollama, seeded_index):
298
+ """An advisory finding is printed apart by `nexa check` and counted in
299
+ nothing. This eval counted it anyway, and reported 13 findings on the
300
+ subjects where fieldtest reports 10 and 3 advisory."""
301
+ fake_ollama(["The file is fine as it is."])
302
+ path = evalmod.DEFAULT_SUBJECTS[0]
303
+ hits = [h for h in subject.plan(path)["findings"] if not h.suppressed]
304
+ live = [h for h in hits if not h.advisory]
305
+ advisory = [h for h in hits if h.advisory]
306
+ assert advisory, "this subject no longer has an advisory finding to count"
307
+ row = evalmod.quiet_check(path)
308
+ assert row["file_finding_count"] == len(live)
309
+ assert row["file_findings"] == sorted({h.rule for h in live})
310
+ assert row["file_advisory"] == len(advisory)
311
+
312
+
313
+ def test_the_report_says_what_it_left_out_as_advisory(capsys):
314
+ evalmod.summarise_check(rows_for(["PASS"], file_advisory=2), 1)
315
+ assert "2 more came from advisory rules" in capsys.readouterr().out
316
+
317
+
318
  # ----------------------------------------------------- the confidence interval
319
 
320
  def test_wilson_stays_inside_zero_and_one_at_tiny_n():
 
460
  rows = [retrieval_row("odoo.onchange-is-not-validation")]
461
  evalmod.summarise_retrieval(rows, k=40)
462
  assert "nothing can miss" in capsys.readouterr().out
463
+
464
+
465
+ def test_the_retrieval_artifact_says_its_depth_and_its_corpus(tmp_path, monkeypatch):
466
+ """Bare rows could be checked against nothing: a rank means nothing without
467
+ the k it was taken at, and a reworded rule keeps its id. figures.py reads
468
+ both from here."""
469
+ monkeypatch.setattr(evalmod, "retrievability", lambda stack, k: [
470
+ retrieval_row("odoo.onchange-is-not-validation")])
471
+ out = tmp_path / "retrieval.json"
472
+ assert evalmod.main(["--retrieval", "--top-k", "4", "--json", str(out)]) == 0
473
+ art = json.loads(out.read_text(encoding="utf-8"))
474
+ assert art["k"] == 4
475
+ assert art["corpus_sha256"] == evalmod.evidence.corpus_sha256()
476
+ assert [r["id"] for r in art["rows"]] == ["odoo.onchange-is-not-validation"]
tests/test_figures.py CHANGED
@@ -33,9 +33,26 @@ def _row(rule, path, line, verdict):
33
  return {"rule": rule, "path": path, "line": line, "verdict": verdict}
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  @pytest.fixture()
37
  def tree(tmp_path, monkeypatch):
38
- """One ecosystem, 2,000 lines, four findings of which two are counted."""
 
 
 
 
39
  root = str(tmp_path)
40
  _write(root, "ft.json", {
41
  "roots": ["repo"], "checkouts": {"repo": SHA},
@@ -64,8 +81,24 @@ def tree(tmp_path, monkeypatch):
64
  "semantic_check": "b"},
65
  {"id": "odoo.z", "severity": "critical"},
66
  ])
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  monkeypatch.setattr(figures, "ROOT", root)
68
  monkeypatch.setattr(figures, "CORPUS_RULES", "rules.json")
 
 
 
69
  monkeypatch.setattr(figures, "FIELDTESTS", {"x": "ft.json"})
70
  monkeypatch.setattr(figures, "RECALLS", {"x": "rc.json"})
71
  monkeypatch.setattr(figures, "RECALLS_XML", {"x": "rc.json"})
@@ -212,6 +245,104 @@ def test_an_agreeing_document_passes(tree, capsys):
212
  assert "every marked figure agrees" in capsys.readouterr().out
213
 
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  def test_the_committed_documents_agree_with_the_committed_artifacts(capsys):
216
  """The check CI runs. A failure here names the sentence to correct."""
217
  code = figures.main(["--check"])
 
33
  return {"rule": rule, "path": path, "line": line, "verdict": verdict}
34
 
35
 
36
+ def _probe(rid, rank):
37
+ return {"id": rid, "probe": "a question", "rank": rank}
38
+
39
+
40
+ def _review(path, verdict, findings, beyond=(), chars=100, advisory=0):
41
+ return {"path": path, "verdict": verdict, "chars": chars,
42
+ "file_findings": list(findings), "file_finding_count": len(findings),
43
+ "file_advisory": advisory, "cited_beyond_checkers": list(beyond)}
44
+
45
+
46
+ CORPUS_SHA = "c" * 64
47
+
48
+
49
  @pytest.fixture()
50
  def tree(tmp_path, monkeypatch):
51
+ """One ecosystem, 2,000 lines, four findings of which two are counted.
52
+
53
+ Beside it, one retrieval run over the four rules and one check run over
54
+ two files, twice each.
55
+ """
56
  root = str(tmp_path)
57
  _write(root, "ft.json", {
58
  "roots": ["repo"], "checkouts": {"repo": SHA},
 
81
  "semantic_check": "b"},
82
  {"id": "odoo.z", "severity": "critical"},
83
  ])
84
+ _write(root, "meta.json", {"corpus_sha256": CORPUS_SHA})
85
+ _write(root, "retrieval.json", {"k": 6, "corpus_sha256": CORPUS_SHA, "rows": [
86
+ _probe("odoo.w", 1), _probe("odoo.x", 3), _probe("python.y", 5),
87
+ _probe("odoo.z", None)]})
88
+ os.mkdir(os.path.join(root, "s"))
89
+ _write(root, "s/a.py", "")
90
+ _write(root, "s/b.py", "")
91
+ _write(root, "check.json", [
92
+ _review("s/a.py", "WARN", ["odoo.a", "odoo.b"], advisory=1),
93
+ _review("s/a.py", "WARN", ["odoo.a", "odoo.b"], advisory=1),
94
+ _review("s/b.py", "PASS", []),
95
+ _review("s/b.py", "REJECT", [], beyond=["odoo.c"]),
96
+ ])
97
  monkeypatch.setattr(figures, "ROOT", root)
98
  monkeypatch.setattr(figures, "CORPUS_RULES", "rules.json")
99
+ monkeypatch.setattr(figures, "CORPUS_META", "meta.json")
100
+ monkeypatch.setattr(figures, "RETRIEVAL", "retrieval.json")
101
+ monkeypatch.setattr(figures, "CHECK", "check.json")
102
  monkeypatch.setattr(figures, "FIELDTESTS", {"x": "ft.json"})
103
  monkeypatch.setattr(figures, "RECALLS", {"x": "rc.json"})
104
  monkeypatch.setattr(figures, "RECALLS_XML", {"x": "rc.json"})
 
245
  assert "every marked figure agrees" in capsys.readouterr().out
246
 
247
 
248
+ # ------------------------------------------------------ the two eval runs
249
+
250
+ def test_one_top_6_run_answers_the_top_3_question_too(tree):
251
+ _classify(tree, [])
252
+ fig = figures.compute()
253
+ assert fig.values["retrieval.rules"] == "4"
254
+ assert fig.values["retrieval.k"] == "6"
255
+ assert fig.values["retrieval.found"] == "3"
256
+ assert fig.values["retrieval.top3"] == "2"
257
+ assert not fig.faults
258
+
259
+
260
+ def test_a_rule_the_retrieval_run_never_asked_is_a_fault(tree):
261
+ """Item 12's drift: a 28th rule, and a row still saying 27 of 27."""
262
+ _classify(tree, [])
263
+ rules = figures._load("rules.json") + [{"id": "odoo.new", "severity": "high"}]
264
+ _write(tree, "rules.json", rules)
265
+ fig = figures.compute()
266
+ assert ("retrieval.json: odoo.new is in the corpus and was never asked "
267
+ "its probe") in fig.faults
268
+
269
+
270
+ def test_a_retrieval_run_on_another_corpus_is_a_fault_though_every_id_matches(tree):
271
+ """A reworded title keeps its id. Only the hash sees it."""
272
+ _classify(tree, [])
273
+ _write(tree, "meta.json", {"corpus_sha256": "d" * 64})
274
+ fig = figures.compute()
275
+ assert fig.faults == ["retrieval.json: ranked against corpus cccccccccccc, which is "
276
+ "dddddddddddd now; re-run nexa eval --retrieval --json "
277
+ "retrieval.json"]
278
+
279
+
280
+ def test_a_retrieval_run_too_shallow_for_its_top_3_is_a_fault(tree):
281
+ _classify(tree, [])
282
+ art = figures._load("retrieval.json")
283
+ art["k"] = 2
284
+ _write(tree, "retrieval.json", art)
285
+ fig = figures.compute()
286
+ assert any("cannot say what a top 3 returns" in f for f in fig.faults)
287
+
288
+
289
+ def test_the_check_run_counts_findings_once_per_file(tree):
290
+ _classify(tree, [])
291
+ fig = figures.compute()
292
+ assert fig.values["check.files"] == "2"
293
+ assert fig.values["check.runs"] == "2"
294
+ assert fig.values["check.reviews"] == "4"
295
+ assert fig.values["check.findings"] == "2"
296
+ assert fig.values["check.advisory"] == "1"
297
+ assert fig.values["check.flagged"] == "1"
298
+ assert fig.values["check.steady"] == "2"
299
+ assert fig.values["check.verdict.WARN"] == "2"
300
+ assert fig.values["check.verdict.PASS"] == "1"
301
+ assert fig.values["check.verdict.REJECT"] == "1"
302
+ assert fig.values["check.stable"] == "1"
303
+ assert fig.values["check.beyond"] == "1"
304
+ assert fig.values["check.beyond.rules"] == "1"
305
+
306
+
307
+ def test_findings_that_moved_between_runs_are_not_steady(tree):
308
+ _classify(tree, [])
309
+ _write(tree, "check.json", [_review("s/a.py", "WARN", ["odoo.a"]),
310
+ _review("s/a.py", "WARN", ["odoo.a", "odoo.b"])])
311
+ fig = figures.compute()
312
+ assert fig.values["check.steady"] == "0"
313
+
314
+
315
+ def test_a_failed_review_neither_speaks_for_its_file_nor_unsteadies_it(tree):
316
+ """An ERROR row reports no characters and no findings. Taken as the file's
317
+ row it would publish a run that broke as a file that is clean."""
318
+ _classify(tree, [])
319
+ _write(tree, "check.json", [_review("s/a.py", "ERROR", [], chars=0),
320
+ _review("s/a.py", "WARN", ["odoo.a"])])
321
+ fig = figures.compute()
322
+ assert fig.values["check.findings"] == "1"
323
+ assert fig.values["check.steady"] == "1"
324
+ assert fig.values["check.stable"] == "0"
325
+
326
+
327
+ def test_a_check_row_for_a_file_that_is_gone_is_a_fault(tree):
328
+ """13a's drift: two subjects deleted, and the rows still counted them."""
329
+ _classify(tree, [])
330
+ _write(tree, "check.json", [_review("s/a.py", "WARN", []),
331
+ _review("s/gone.py", "WARN", [])])
332
+ fig = figures.compute()
333
+ assert fig.faults == ["check.json: s/gone.py is not in the repository, so "
334
+ "nothing can re-review it"]
335
+
336
+
337
+ def test_files_reviewed_unevenly_are_a_fault(tree):
338
+ _classify(tree, [])
339
+ _write(tree, "check.json", [_review("s/a.py", "WARN", []),
340
+ _review("s/a.py", "WARN", []),
341
+ _review("s/b.py", "WARN", [])])
342
+ fig = figures.compute()
343
+ assert "check.json: files were reviewed unevenly, 1 or 2 times" in fig.faults
344
+
345
+
346
  def test_the_committed_documents_agree_with_the_committed_artifacts(capsys):
347
  """The check CI runs. A failure here names the sentence to correct."""
348
  code = figures.main(["--check"])