DBax127 Claude Opus 5 commited on
Commit
8d155aa
·
1 Parent(s): 9012cfa

narrow: one corpus, and it is Odoo

Browse files

Four stacks become one. The TypeScript and JavaScript rules are removed, the
eight plain-Python rules are composed into the Odoo stack, and a file in any
other language is refused by name rather than reviewed against rules that
cannot match it.

WHY REMOVE RATHER THAN LABEL. Firing the other twenty-five rules at real code
decided it: JavaScript classified out at 21 true against 22 false, TypeScript
was measured at 3.8/kloc and never classified. A rule that costs nothing at
query time is not free -- it is a claim the README carries and a file `check`
accepts and then says nothing useful about. An empty finding list reads as a
clean bill of health, so `subject.Unreviewable` refuses the file instead.

WHY COMPOSE RATHER THAN DELETE. Selection was strict equality on `stack`, so an
Odoo module got the 19 ORM rules and none of the 8 Python ones -- and Odoo
modules are Python. `fieldtest --stack python` over OpenSPP found 32 hits
`--stack odoo` never reported. "ruff covers it" does not hold against
OpenSPP's own .ruff.toml, which selects B/C90/E501/I/UP and leaves S and DTZ
off, so six of the eight are genuinely unreported there.

MEASURED BEFORE SHIPPING, to the same bar as a new rule. 50 new findings across
OpenSPP and OCA, every one read in its own file: 26 true, 24 false, in
classification-python-2026-09.json. OpenSPP goes 162 -> 194 live findings
(0.71 -> 0.9/kloc), OCA 102 -> 120 (3.0 -> 3.5). Twenty of the 26 true are one
rule and one fact: an Odoo Datetime is naive UTC by contract, and a naive local
datetime reaching one is off by the server's offset. Three rules fired and were
never right, three never fired at all, and both lists are in the README rather
than quietly dropped.

Two defects the classification found and no test would have: scan_source
matches inside docstrings, and suppression reads any marker in a statement
without asking which rule it names. Named, not fixed here.

The id prefix now means provenance, not stack -- python.* rules carry stack
odoo -- so every committed classification, LIKELIHOOD grade and evidence bundle
still points at the same rule. cmd_validate checks the prefix against
corpus.ORIGINS instead.

prefer-sql-constraint had to be reworded: 27 rules competing where 19 were made
its old probe, the generic "stop anyone entering a negative quantity", stop
retrieving it. 27 of 27 retrievable, 25 in the top 3.

Also: the wordmark loses a row, five instead of six.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

ARCHITECTURE.md CHANGED
@@ -14,7 +14,7 @@ A decision record that only lists the final answer hides the most useful informa
14
  question
15
  │
16
  ▼
17
- ┌──────────┐ embed on CPU, cosine over the stack's rules
18
  │ retrieve │ + pinned rules that bypass retrieval
19
  └────┬─────┘
20
  ▼
@@ -2120,6 +2120,78 @@ nothing, which is the right place for a finding whose harm is real and latent.
2120
  | Machine-checkable | 41 (93%) | 40 (93%) |
2121
  | JavaScript findings, classified | 21 true / 24 false | **21 true / 22 false** |
2122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2123
  ## 4. Performance traps, all found by measurement
2124
 
2125
  Within four instrumented runs the report showed the 7B at ~10 tok/s against a
@@ -2191,22 +2263,25 @@ in a corpus; it does not close the reasoning gap.
2191
 
2192
  ### Checking is partial, and the layers say where
2193
 
2194
- - 3 of 43 rules have no automated check at all. They return **no verdict**, never a
2195
- false pass, and `nexa coverage` prints the reason for each.
2196
- - 32 of the 40 checked rules are decided by a **regex over generated code**. That
2197
  catches textual signatures, not meaning. Code wrong in a way no pattern describes
2198
- passes silently. The other 8 are decided by an AST pass (§3.12).
2199
- - Semantic analysis is **Python only**. The standard library has no TypeScript or
2200
- JavaScript parser, so TS/JS answers get delimiter balance and nothing deeper, and
2201
- the layer reports UNKNOWN rather than PASS for the rest.
 
 
 
2202
  - The empirical layer resolves imports against this interpreter and assembles
2203
  bytecode. It does not execute anything unless `NEXA_EMPIRICAL_EXEC=1`, and even then
2204
  only for snippets importing nothing outside a short allowlist — which in practice
2205
  means no Odoo answer is ever executed, because they all import `odoo`. A subprocess
2206
  with a timeout reduces blast radius; it is **not a security boundary**.
2207
  - The context layer is only as good as the file it was given. `nexa ask` has no file,
2208
- so it reports UNKNOWN; `nexa check <file>` reads the manifest, the model names and
2209
- the tsconfig, and can then say something real.
2210
  - Every Odoo rule declares `odoo_versions: >=16.0`, so the context layer has a
2211
  question it can answer about a module on any series and reports per-rule when one
2212
  does not apply. `corpus/check_sources.py` supports that range from the
@@ -2338,13 +2413,13 @@ unawares.
2338
 
2339
  ### Corpus provenance
2340
 
2341
- 40 of 43 rules cite upstream documentation. The rest rest on general knowledge and
2342
  deserve more scepticism. Most of the rules were written by an assistant rather than
2343
  harvested from production incidents, and rules drawn from real incidents are worth
2344
  more. Three now are: `commit-inside-a-loop` and `sql-needs-query-parameters` came
2345
  from an external ruleset disagreeing with this one (3.34), and
2346
  `depends-must-not-name-unread-fields` from a fix commit in OpenSPP's history (3.35).
2347
- Three of forty-three is not a change of method yet.
2348
 
2349
  This is the deepest unresolved problem in the design and no layer addresses it: the
2350
  validator and the thing being validated are the same class of system. Provenance
@@ -2353,21 +2428,17 @@ tracks sources, versions and checker identities. It cannot establish that a rule
2353
 
2354
  ### Scope
2355
 
2356
- Odoo, Python, TypeScript and JavaScript. Odoo rules were checked against 18.0
2357
- documentation and its source; behaviour differs across major versions.
2358
 
2359
- Every stack now carries field evidence (§3.44), and the four sit at very different
2360
- stages of it. The nineteen Odoo rules have two ecosystems behind them and a
2361
- hand classification of both. The seven JavaScript rules have one ecosystem and a
2362
- classification. The ten TypeScript rules are measured and not yet classified. The
2363
- eight Python rules have fired three times in ten thousand lines, which is too few
2364
- to say anything with. Until each has a second codebase, every one of those numbers
2365
- is one sample.
2366
 
2367
- The Python stack is eight rules and is deliberately narrow — the same subject as the
2368
- rest of the corpus, which is validation and the places a check that looks like it
2369
- holds does not. It is not a style guide and does not try to be. `nexa check` on a
2370
- `.py` file now decides from the file itself which of the two Python corpora applies.
2371
 
2372
  ### Known imprecision, not yet fixed
2373
 
@@ -2473,7 +2544,7 @@ nexa/
2473
  │ ├── build_corpus.py RULE SOURCE OF TRUTH + patterns + provenance
2474
  │ ├── corpus_meta.json generated; hash, build time, checked_against
2475
  │ ├── selftest_patterns.py negative test for both self-checks
2476
- │ └── rules/*.json generated; do not hand-edit
2477
  ├── bin/
2478
  │ ├── corpus.py validate, index, ask, crosscheck, coverage
2479
  │ ├── layers.py the nine layers, and the rule that combines them
@@ -2494,7 +2565,7 @@ nexa/
2494
  │ ├── local-ai.ps1 PowerShell helpers
2495
  │ └── bench.ps1 raw throughput benchmark
2496
  ├── modelfiles/
2497
- │ ├── gen-*.Modelfile generated; do not hand-edit
2498
  │ └── arch-advisor.Modelfile hand-written 30B persona
2499
  └── subjects/ the fixed file set `nexa eval --check` reviews
2500
  ```
 
14
  question
15
  │
16
  ▼
17
+ ┌──────────┐ embed on CPU, cosine over the corpus
18
  │ retrieve │ + pinned rules that bypass retrieval
19
  └────┬─────┘
20
  ▼
 
2120
  | Machine-checkable | 41 (93%) | 40 (93%) |
2121
  | JavaScript findings, classified | 21 true / 24 false | **21 true / 22 false** |
2122
 
2123
+ ### 3.46 Narrowing to Odoo, and composing the Python rules into it
2124
+
2125
+ **Decided 2026-09-16.** The corpus had four stacks. It has one, and the eight
2126
+ plain-Python rules are inside it rather than beside it.
2127
+
2128
+ **Why narrow.** §3.44 fired the other twenty-five rules at real code for the first
2129
+ time, and the answer decided this. JavaScript classified out at **21 true against 22
2130
+ false**. TypeScript was measured at 3.8 per thousand lines and never classified, so
2131
+ its number said nothing. Against those, the Odoo half carries 87 true / 15 false on
2132
+ its two flagship rules across two ecosystems. The edge is Odoo ORM semantics;
2133
+ everywhere else this corpus competes with mature tools and loses.
2134
+
2135
+ Removing them rather than labelling them experimental is the part worth arguing. A
2136
+ rule that costs nothing at query time is not free: it is a claim the README has to
2137
+ carry, a number somebody will quote, and a file `nexa check` will accept and then say
2138
+ nothing useful about. A `.ts` file now raises `subject.Unreviewable` and is refused by
2139
+ name, because **an empty finding list reads as a clean bill of health**, and silence
2140
+ about a file nobody checked is the one lie this design exists to prevent.
2141
+
2142
+ **Why the Python rules were composed in instead.** Rules were selected by strict
2143
+ equality on `stack` (`corpus.py`), so an Odoo module got the 19 ORM rules and none of
2144
+ the 8 Python ones. Odoo modules are Python. `fieldtest --stack python` over OpenSPP
2145
+ found **32 hits `--stack odoo` never reported**.
2146
+
2147
+ "ruff covers it" was the first answer and it does not hold. OpenSPP's own `.ruff.toml`
2148
+ selects `B, C90, E501, I, UP` plus defaults — `S` and `DTZ` are off — so six of the
2149
+ eight are genuinely unreported in that deployment, and a narrow selection is what most
2150
+ Odoo shops run.
2151
+
2152
+ **What it cost, measured before shipping.** The composition was held to the bar in
2153
+ §3.43: 50 new findings across OpenSPP and OCA, every one read in its own file.
2154
+ **26 true, 24 false**, in `classification-python-2026-09.json`.
2155
+
2156
+ | | | |
2157
+ |---|---|---|
2158
+ | OpenSPP live findings | 162 | **194** (0.71 → 0.9 per kloc) |
2159
+ | OCA live findings | 102 | **120** (3.0 → 3.5 per kloc) |
2160
+ | Corpus precision, composed-in half | — | 26 true / 24 false |
2161
+
2162
+ Twenty of the twenty-six are one rule, `naive-and-aware-datetimes-do-not-compare`, and
2163
+ one fact: an Odoo `Datetime` is **naive UTC by contract**, so a naive local datetime
2164
+ reaching one is off by the server's offset. Latent on a UTC-clocked server, wrong on
2165
+ any other, and OpenSPP's history carries two fixes of exactly it.
2166
+
2167
+ Three rules fired and were never right, and the most interesting is
2168
+ `decimal-for-money-not-float`: Odoo's `Monetary` field **is** a float, and the
2169
+ framework's answer to precision is `float_round`/`float_compare`, not `Decimal`. The
2170
+ rule fires on the idiom and names a fix that cannot be taken. Three findings do not
2171
+ settle a checker either way, so it is recorded rather than removed.
2172
+
2173
+ **Two defects the classification found that no test would have.** `scan_source` strips
2174
+ `#` comments before matching and not string literals, so `eval()` named in a docstring
2175
+ reads as code. And suppression reads any marker within a simple statement without
2176
+ asking which rule it names, so a `# nosemgrep: odoo-sudo-without-context` silenced a
2177
+ datetime finding four lines above it — the mute button of §3.42 reached from the other
2178
+ side. Both are named in the README rather than quietly fixed.
2179
+
2180
+ **What the id prefix means now.** `python.decimal-for-money-not-float` carries
2181
+ `stack: odoo`. The prefix is **provenance** — where the rule came from and which
2182
+ documentation `check_sources` verifies it against — and the stack is **selection**. The
2183
+ validator's old invariant was "id prefix equals stack"; it is now "id prefix is one of
2184
+ `corpus.ORIGINS`". Keeping the ids is what keeps every committed classification,
2185
+ `LIKELIHOOD` grade and evidence bundle pointing at the same rule: renaming the subject
2186
+ of a measurement is the quiet way a number stops meaning what it says.
2187
+
2188
+ **One rule had to be reworded.** Retrieval is scoped to a stack, so merging them put
2189
+ 27 rules in competition where 19 had been. `prefer-sql-constraint` ranked first for
2190
+ *"stop anyone entering a negative quantity on a sale order line"* against 19 and
2191
+ stopped being retrieved at all against 27 — that probe is the generic validation
2192
+ question every constraint rule here answers. Reworded to the one only it answers.
2193
+ **27 of 27 retrievable, 25 in the top 3.**
2194
+
2195
  ## 4. Performance traps, all found by measurement
2196
 
2197
  Within four instrumented runs the report showed the 7B at ~10 tok/s against a
 
2263
 
2264
  ### Checking is partial, and the layers say where
2265
 
2266
+ - 1 of 27 rules has no automated check at all. It returns **no verdict**, never a
2267
+ false pass, and `nexa coverage` prints the reason.
2268
+ - 16 of the 26 checked rules are decided by a **regex over generated code**. That
2269
  catches textual signatures, not meaning. Code wrong in a way no pattern describes
2270
+ passes silently. The other 10 are decided by an AST pass (§3.12). The regexes also
2271
+ match inside **docstrings**, because `scan_source` strips `#` comments and not
2272
+ string literals — two of the fifty findings in §3.46 were that.
2273
+ - Semantic analysis is **Python only**, which is the whole subject now. An answer may
2274
+ still contain a JavaScript block, Odoo's web client being OWL; that block gets
2275
+ delimiter balance and nothing deeper, and the layer reports UNKNOWN rather than
2276
+ PASS for it.
2277
  - The empirical layer resolves imports against this interpreter and assembles
2278
  bytecode. It does not execute anything unless `NEXA_EMPIRICAL_EXEC=1`, and even then
2279
  only for snippets importing nothing outside a short allowlist — which in practice
2280
  means no Odoo answer is ever executed, because they all import `odoo`. A subprocess
2281
  with a timeout reduces blast radius; it is **not a security boundary**.
2282
  - The context layer is only as good as the file it was given. `nexa ask` has no file,
2283
+ so it reports UNKNOWN; `nexa check <file>` reads the manifest, the model names,
2284
+ `ir.model.access.csv` and the record rules in XML, and can then say something real.
2285
  - Every Odoo rule declares `odoo_versions: >=16.0`, so the context layer has a
2286
  question it can answer about a module on any series and reports per-rule when one
2287
  does not apply. `corpus/check_sources.py` supports that range from the
 
2413
 
2414
  ### Corpus provenance
2415
 
2416
+ 25 of 27 rules cite upstream documentation. The rest rest on general knowledge and
2417
  deserve more scepticism. Most of the rules were written by an assistant rather than
2418
  harvested from production incidents, and rules drawn from real incidents are worth
2419
  more. Three now are: `commit-inside-a-loop` and `sql-needs-query-parameters` came
2420
  from an external ruleset disagreeing with this one (3.34), and
2421
  `depends-must-not-name-unread-fields` from a fix commit in OpenSPP's history (3.35).
2422
+ Three of twenty-seven is not a change of method yet.
2423
 
2424
  This is the deepest unresolved problem in the design and no layer addresses it: the
2425
  validator and the thing being validated are the same class of system. Provenance
 
2428
 
2429
  ### Scope
2430
 
2431
+ Odoo, and nothing else (§3.46). Odoo rules were checked against 18.0 documentation and
2432
+ its source; behaviour differs across major versions.
2433
 
2434
+ One stack, 27 rules: 19 about the ORM and 8 about the Python language, which apply
2435
+ because an Odoo module is a Python module. Both halves carry field evidence from two
2436
+ ecosystems and a hand classification of every finding, and they are **not the same
2437
+ quality** — 85% on the two flagship ORM rules, 52% on the composed-in Python half.
2438
+ Reporting one blended number would describe neither.
 
 
2439
 
2440
+ A `.py` or `.xml` file is reviewed. Anything else is refused by name rather than
2441
+ reviewed against rules that cannot match it.
 
 
2442
 
2443
  ### Known imprecision, not yet fixed
2444
 
 
2544
  │ ├── build_corpus.py RULE SOURCE OF TRUTH + patterns + provenance
2545
  │ ├── corpus_meta.json generated; hash, build time, checked_against
2546
  │ ├── selftest_patterns.py negative test for both self-checks
2547
+ │ └── rules/odoo.json generated; the whole corpus, do not hand-edit
2548
  ├── bin/
2549
  │ ├── corpus.py validate, index, ask, crosscheck, coverage
2550
  │ ├── layers.py the nine layers, and the rule that combines them
 
2565
  │ ├── local-ai.ps1 PowerShell helpers
2566
  │ └── bench.ps1 raw throughput benchmark
2567
  ├── modelfiles/
2568
+ │ ├── gen-odoo.Modelfile generated; do not hand-edit
2569
  │ └── arch-advisor.Modelfile hand-written 30B persona
2570
  └── subjects/ the fixed file set `nexa eval --check` reviews
2571
  ```
INSTALL.md CHANGED
@@ -135,22 +135,14 @@ This generates the rule JSON, validates it against its schema, self-tests every
135
  violation pattern, measures pattern false positives, embeds the retrieval index, and
136
  writes the Modelfiles.
137
 
138
- It finishes by printing three `ollama create` commands. Run them to register the
139
- personas — each takes about two seconds, because only metadata is written. The weights
140
- are shared with `qwen2.5-coder:7b`, so the three personas cost no extra disk.
141
 
142
  ```bash
143
  ollama create ai-odoo -f modelfiles/gen-odoo.Modelfile
144
  ```
145
 
146
- ```bash
147
- ollama create ai-ts -f modelfiles/gen-typescript.Modelfile
148
- ```
149
-
150
- ```bash
151
- ollama create ai-js -f modelfiles/gen-javascript.Modelfile
152
- ```
153
-
154
  Optional, for the 30B persona used by `nexa ask --deep`:
155
 
156
  ```bash
@@ -284,7 +276,7 @@ rule and the problem. Fix it in `corpus/build_corpus.py` and rebuild.
284
  ## Uninstall
285
 
286
  ```bash
287
- ollama rm ai-odoo ai-ts ai-js arch-advisor qwen2.5-coder:7b qwen3-coder:30b nomic-embed-text
288
  ```
289
 
290
  Then delete the cloned directory. Nexa writes nothing outside its own folder except the
 
135
  violation pattern, measures pattern false positives, embeds the retrieval index, and
136
  writes the Modelfiles.
137
 
138
+ It finishes by printing the `ollama create` command. Run it to register the persona —
139
+ about two seconds, because only metadata is written. The weights are shared with
140
+ `qwen2.5-coder:7b`, so it costs no extra disk.
141
 
142
  ```bash
143
  ollama create ai-odoo -f modelfiles/gen-odoo.Modelfile
144
  ```
145
 
 
 
 
 
 
 
 
 
146
  Optional, for the 30B persona used by `nexa ask --deep`:
147
 
148
  ```bash
 
276
  ## Uninstall
277
 
278
  ```bash
279
+ ollama rm ai-odoo arch-advisor qwen2.5-coder:7b qwen3-coder:30b nomic-embed-text
280
  ```
281
 
282
  Then delete the cloned directory. Nexa writes nothing outside its own folder except the
README.md CHANGED
@@ -1,8 +1,7 @@
1
  ```
2
  ██ ██ ██████ ██ ██ █████
3
  ███ ██ ██ ██ ██ ██ ██
4
- ████ ██ █████ ███ ██ ██
5
- ██ ████ █████ ███ ███████
6
  ██ ███ ██ ██ ██ ██ ██
7
  ██ ██ ██████ ██ ██ ██ ██
8
  ▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
@@ -128,17 +127,125 @@ publishing a classification.
128
 
129
  ## What it is not
130
 
131
- **Not a general linter.** 43 rules. It will not replace `pylint-odoo`, and after the
132
  wrapper above it does not have to.
133
 
134
- **Not a bug finder.** Against real Odoo bugfixes it catches 7 of 162 — about 4%. Most
135
- bugs are domain logic ("finished plans should not be the current plan") and no rule
136
  corpus finds those. Measured and published here because a precision number without a
137
  recall number beside it is the more flattering half of the picture.
138
 
139
- **The TypeScript and JavaScript rules are experimental.** They got their first field
140
- evidence in September 2026 and it was not good: JavaScript classified out at 21 true
141
- against 22 false. Use the Odoo half.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  ---
144
 
@@ -208,10 +315,6 @@ nexa help <command> detail for one command
208
  nexa ask odoo "constrain invoice date to the fiscal year of its period"
209
  ```
210
 
211
- ```bash
212
- nexa ask typescript "validate an Odoo res.partner JSON-RPC payload"
213
- ```
214
-
215
  ```bash
216
  nexa ask odoo --deep "should this be a stored computed field or a related field?"
217
  ```
@@ -286,12 +389,25 @@ the same command.
286
  It prints **no verdict**, because nothing generated an answer to have a verdict
287
  about. A clean run says *no pattern fired* in those words, and exits 1 if one did.
288
 
289
- For a `.py` file the extension only really says *not TypeScript*, so **the file
290
- decides** which corpus applies: an `odoo` import or a `__manifest__.py` above it means the Odoo rules,
291
- anything else means the plain-Python ones.
 
 
 
 
 
 
 
 
 
 
292
 
293
  ```
294
- checking upload_handler.py as python
 
 
 
295
 
296
  Rules retrieved:
297
  0.698 [critical] python.assert-is-not-validation
@@ -300,10 +416,10 @@ Rules retrieved:
300
  0.643 [critical] python.subprocess-shell-true-is-injection
301
  ```
302
 
303
- `--stack` overrides the detection in either direction. Unlike `nexa ask`, this one has **surroundings to read**: the nearest
304
  `__manifest__.py` gives the Odoo series, the file gives its model names and existing
305
- imports, the nearest `tsconfig.json` says whether `strict` is on. Those facts go into
306
- the question *and* into the context layer.
307
 
308
  Where they say the corpus was verified somewhere else, that is reported as a **gap**,
309
  not a finding — it is true of the corpus whatever the answer says, and a verdict that
@@ -418,9 +534,9 @@ field could not say so.
418
  Everything else is **derived** and must never be hand-edited:
419
 
420
  ```
421
- corpus/rules/*.json --+--> modelfiles/gen-*.Modelfile (system prompts)
422
- +--> bin/index.sqlite (retrieval)
423
- +--> every answer (grounding + checking)
424
  ```
425
 
426
  Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, no drift.
@@ -429,23 +545,21 @@ Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, n
429
 
430
  | | |
431
  |---|---|
432
- | Rules | 43 — 19 Odoo, 10 TypeScript, 8 Python, 6 JavaScript |
433
- | Critical | 17 |
434
- | Machine-checkable | 40 (93%) — 30 by pattern, 10 by AST |
435
- | Traceable to upstream docs | 41 (95%), each audited for topic fit |
436
  | Pattern false positives | 0 cross-hits across the corpus |
437
- | Recall under meaning-preserving rewrite | 99/99 |
438
- | Satisfied patterns bypassable by a comment | 0 of 13 |
439
- | Retrievable by their own probe | 43 of 43, at top 3 |
440
- | Hit rate on 33,960 lines of reviewed OCA code | 3.0 per 1000 lines |
441
- | Those 102 hits, classified by hand | **79 true positives**, 10 false, 13 undecidable from one file |
442
- | Hit rate on 226,937 lines of OpenSPP (Odoo 19) | 0.7 per 1000 lines, +68 another tool had already flagged, +71 advisory |
443
- | Every one of those findings, classified by hand | **138 true positives**, 124 false, 6 undecidable |
444
  | …the two `depends`/`constrains` rules alone | **87 true**, 15 false, 3 undecidable — 85% |
 
445
  | …`sudo-bypasses-record-rules` alone, before it was narrowed | 6 true, 97 false — 5.8%, and the reason it is now an AST pass |
446
- | Hit rate on 73,762 lines of TypeScript (hono, trpc) | 3.8 per 1000 lines — **measured, not yet classified** |
447
- | Hit rate on 9,304 lines of Odoo frontend JavaScript | 4.6 per 1000 lines — 21 true positives, 22 false |
448
- | Hit rate on 10,225 lines of Python (llm-router) | 0.3 per 1000 lines |
449
  | **Recall against real Odoo bugfixes** | **1 of 56 (OCA), 7 of 162 (OpenSPP). See below.** |
450
  | Verdict stability, 12 questions x 5 runs | 12/12 same verdict every run |
451
  | Escalation rescued a bad draft | 11 of 21, 95% CI 32–72% |
@@ -455,9 +569,15 @@ Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, n
455
  | `nexa check` verdict stability | 4 of 5 files on one run, 3 of 5 on the next |
456
  | `nexa check --no-model` | 0.26s, identical output every run |
457
 
458
- 40 of 43 rules carry an automated check. The remaining three are decidable only with
459
- context a snippet does not contain, so they return **no verdict** — never a false pass
460
- — and `nexa coverage` names each one and why.
 
 
 
 
 
 
461
 
462
  ---
463
 
@@ -549,6 +669,12 @@ false positive left, a `sql-needs-query-parameters` carrying a marker on the lin
549
  it that suppression now reads. **All 79 true positives survived both changes**, which is
550
  again the only thing that makes the rest of it safe.
551
 
 
 
 
 
 
 
552
  ### The same treatment on a second ecosystem
553
 
554
  A precision number from one ecosystem is a precision number for that ecosystem. The two
@@ -614,6 +740,11 @@ model nobody declared is UNKNOWN, not "no rule".
614
 
615
  The last row is the only one that makes the others safe.
616
 
 
 
 
 
 
617
  ### What is still wrong, named
618
 
619
  `float-comparison-needs-float-utils` is next at **2 true, 5 false**. All five compare an
@@ -625,6 +756,12 @@ means tracing the assignment, so it is written down here rather than quietly lef
625
  onchange that sets a field rather than enforcing anything — which the rule's existing
626
  narrowing was supposed to exclude and does not catch when the write is to a Many2one.
627
 
 
 
 
 
 
 
628
  What the first run found:
629
 
630
  - **`onchange-is-not-validation`** fired on all 16 `@api.onchange` declarations that
@@ -829,9 +966,11 @@ python corpus/check_sources.py --source # what the source defines
829
  ```
830
 
831
  Each Odoo rule declares the series it holds for. The first mode checks that claim
832
- against the documentation: 42 of 45 versioned rule-series cite a page that exists and
833
- names the API, 2 more are tutorials that moved while the API stayed, and **1** is not
834
- supported by any 16.0 page.
 
 
835
 
836
  Documentation has a blind spot, and the corpus had fallen into it — a function Odoo
837
  shipped and never described reads as missing. So the second mode reads odoo/odoo
@@ -840,7 +979,7 @@ float_compare(` in `odoo/tools/float_utils.py`, not the word anywhere on a page.
840
 
841
  | | |
842
  |---|---|
843
- | Rule-series defined in source across 16.0, 17.0 and 18.0 | **54 of 54** |
844
 
845
  That closes it. No version claim in this corpus rests on an assertion, and the one
846
  documentation gap now carries a measurement instead of a belief: `float_compare`'s
@@ -863,16 +1002,20 @@ nexa eval --retrieval
863
  asks each rule its own probe and reports whether it comes back. No generation, so it
864
  needs only the embedding model and finishes in seconds.
865
 
866
- All 43 rules are retrievable at the operating `TOP_K` of 6, and at top 3. Two were
867
- not, and both failed the way a rule does: correctly worded, in the vocabulary of
 
 
 
 
 
 
868
  somebody who already knows the answer. `no-monkey-patching` did not answer *"change
869
- what a core sale.order method does"*; `unknown-not-any-at-boundaries` did not answer
870
- *"what type should a webhook payload have"*. Reworded, measured again, both found.
871
 
872
  The report states its own limits. Pinned rules are prepended regardless of score, so
873
- ranking against them measures the pin; JavaScript has 7 rules against a `TOP_K` of 6,
874
- so passing there is nearly free, and the header says so rather than printing 7/7 and
875
- letting it read as a result.
876
 
877
  ### And what does `nexa check` actually report?
878
 
 
1
  ```
2
  ██ ██ ██████ ██ ██ █████
3
  ███ ██ ██ ██ ██ ██ ██
4
+ ██ █ ██ █████ ███ ███████
 
5
  ██ ███ ██ ██ ██ ██ ██
6
  ██ ██ ██████ ██ ██ ██ ██
7
  ▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
 
127
 
128
  ## What it is not
129
 
130
+ **Not a general linter.** 27 rules. It will not replace `pylint-odoo`, and after the
131
  wrapper above it does not have to.
132
 
133
+ **Not a bug finder.** Against real Odoo bugfixes the ORM rules catch 7 of 162 — about
134
+ 4%. Most bugs are domain logic ("finished plans should not be the current plan") and no rule
135
  corpus finds those. Measured and published here because a precision number without a
136
  recall number beside it is the more flattering half of the picture.
137
 
138
+ **Odoo only, and that is the whole of it.** There was a TypeScript corpus and a
139
+ JavaScript one. They got their first field evidence in September 2026 and it was not
140
+ good — JavaScript classified out at 21 true against 22 false, TypeScript was never
141
+ classified at all — so both were **removed** rather than left in with a warning on
142
+ them. A `.ts` or `.js` file is now refused by name instead of reviewed against rules
143
+ that would find nothing in it, because nothing found reads as nothing to find.
144
+
145
+ **The plain-Python rules did not go the same way, and the reason is in
146
+ [the composition](#one-corpus-and-why-the-python-rules-are-in-it).**
147
+
148
+ ---
149
+
150
+ ## One corpus, and why the Python rules are in it
151
+
152
+ There is one stack. Every rule in it is a rule for reviewing Odoo code: 19 about the
153
+ ORM, and 8 about the Python language, which apply because **an Odoo module is a Python
154
+ module**. The `python.` prefix on those eight records where the rule comes from and
155
+ which documentation it is checked against. It is not a second mode you can select.
156
+ There is no second mode.
157
+
158
+ They were unreachable until September 2026, and it was one line: selection was strict
159
+ equality on the stack field, so an Odoo file got the 19 ORM rules and none of the
160
+ other 8. `fieldtest --stack python` over OpenSPP found **32 hits that `--stack odoo`
161
+ never reported**.
162
+
163
+ The first instinct was the opposite one — drop the Python corpus, because ruff covers
164
+ it. Checked against OpenSPP's actual `.ruff.toml`, that is wrong. They select `B`,
165
+ `C90`, `E501`, `I`, `UP` and ruff's defaults. `S` and `DTZ` are not selected:
166
+
167
+ | | ruff rule | selected there? |
168
+ |---|---|---|
169
+ | `assert-is-not-validation` | S101 | no |
170
+ | `eval-and-pickle-execute-their-input` | S301/S307 | no |
171
+ | `subprocess-shell-true-is-injection` | S602 | no |
172
+ | `naive-and-aware-datetimes-do-not-compare` | DTZ | no |
173
+ | `path-join-does-not-contain-a-path` | — | no such rule |
174
+ | `decimal-for-money-not-float` | — | no such rule |
175
+ | `mutable-default-argument` | B006 | **yes** |
176
+ | `bare-except-catches-the-exit` | E722 | **yes** (default) |
177
+
178
+ A narrow ruff selection is what most Odoo shops run, so six of the eight are genuinely
179
+ unreported in that deployment. Against ruff this is not a competing tool; inside an
180
+ Odoo review it is part of one.
181
+
182
+ ### What the composition actually found, all 50 read by hand
183
+
184
+ Composing them moves every Odoo number, so the standard the rest of the corpus is held
185
+ to applies here too: 50 findings across OpenSPP and OCA, each read in its own file.
186
+ Every verdict is in `classification-python-2026-09.json`.
187
+
188
+ | | true | false | |
189
+ |---|---|---|---|
190
+ | `naive-and-aware-datetimes-do-not-compare` | 20 | 12 | 62% |
191
+ | `assert-is-not-validation` | 6 | 4 | 60% |
192
+ | `decimal-for-money-not-float` | 0 | 3 | |
193
+ | `path-join-does-not-contain-a-path` | 0 | 4 | |
194
+ | `eval-and-pickle-execute-their-input` | 0 | 1 | |
195
+ | `mutable-default-argument` | — | — | never fired |
196
+ | `bare-except-catches-the-exit` | — | — | never fired |
197
+ | `subprocess-shell-true-is-injection` | — | — | never fired |
198
+ | | **26** | **24** | **52%** |
199
+
200
+ **One rule carries it.** Twenty of the twenty-six true positives are the datetime rule,
201
+ and they are one shape: an Odoo `Datetime` field is **naive UTC by contract**, and a
202
+ naive *local* datetime reaches one. `membership.ended_date > datetime.now()` filters by
203
+ the server's offset. An autovacuum cron builds its deadline from local now and compares
204
+ it against `create_date`, deleting audit logs early or late. A linked-data proof appends
205
+ a literal `Z` — which asserts UTC — to a local time. A fiscal-year cron compares an ORM
206
+ `Date` against `datetime.now().date()`, so on any non-UTC server the year is created on
207
+ the wrong side of midnight.
208
+
209
+ Say what that makes them: on a server clocked to UTC every one is latent. On any other
210
+ it is wrong by the offset. OpenSPP's own history carries two fixes of exactly this class
211
+ — a newborn birthdate evaluated against the server's UTC date, and `Date.today()` storing
212
+ midnight UTC on a `Datetime` field — which is why it is graded a defect rather than a
213
+ theoretical one.
214
+
215
+ The twelve false ones are near enough one shape too: a timestamp that is formatted and
216
+ never compared — a `Generated:` line in a report, a backup filename, a `%Y-%m` period
217
+ label. Telling those from the twenty needs to see what happens to the value afterwards,
218
+ which a regex cannot.
219
+
220
+ ### What is wrong with the other half, named
221
+
222
+ **Three rules fired and were never right.** `decimal-for-money-not-float` is 0 of 3, and
223
+ this one has a *reason* behind it rather than a sample size: Odoo's `Monetary` field **is
224
+ a float** — the ORM defines it that way — and the framework's answer to the precision
225
+ problem is `float_round` and `float_compare` against the currency's rounding, not
226
+ `Decimal`. Inside an Odoo module the rule fires on the idiom and names a fix that cannot
227
+ be taken. That is an argument and not a measurement, and three findings settle nothing,
228
+ so it is written down here rather than acted on.
229
+
230
+ `path-join-does-not-contain-a-path` is 0 of 4 — all four join a configured directory to
231
+ a name this module generated, and the pattern matches the *word* `filename` rather than
232
+ anything about where the value came from. `eval-and-pickle-execute-their-input` is 0 of
233
+ 1. Four findings between them is not enough to conclude a checker is wrong.
234
+
235
+ **Three more never fired at all**, and silence is not a pass: it says 260,000 lines
236
+ contain none of what they look for, which is also what a broken checker says. Two of the
237
+ three are the two ruff already covers, which is the more likely explanation.
238
+
239
+ **A checker defect, found by classifying rather than by testing.** Two of the 50 are the
240
+ same fault: `scan_source` strips `#` comments before matching and does **not** strip
241
+ string literals, so `eval()` named in a docstring arguing *against* using it, and a
242
+ `datetime.now()` inside a doctest example, both read as code.
243
+
244
+ **A suppression that should not have fired.** One finding — a consent-receipt timestamp
245
+ — sits inside a `return` statement that carries a `# nosemgrep: odoo-sudo-without-context`
246
+ marker four lines below it. Suppression reads any marker within the statement without
247
+ asking which rule it names. That is the mute button this project says it will not build,
248
+ reached from the other direction, and it is counted apart here rather than dropped.
249
 
250
  ---
251
 
 
315
  nexa ask odoo "constrain invoice date to the fiscal year of its period"
316
  ```
317
 
 
 
 
 
318
  ```bash
319
  nexa ask odoo --deep "should this be a stored computed field or a related field?"
320
  ```
 
389
  It prints **no verdict**, because nothing generated an answer to have a verdict
390
  about. A clean run says *no pattern fired* in those words, and exits 1 if one did.
391
 
392
+ **A `.py` or `.xml` file is reviewed against all 27 rules. Anything else is refused
393
+ by name.**
394
+
395
+ ```
396
+ $ nexa check web/src/partners.ts
397
+ ! nexa reviews Odoo code: .py and .xml files. .ts is not one, and there
398
+ are no rules in this corpus that would say anything about it.
399
+ ```
400
+
401
+ There is no second corpus to route to, so a plain Python script inside an Odoo tree —
402
+ `scripts/`, a migration helper — is reviewed like anything else. The nineteen ORM
403
+ rules have nothing to match in it, the eight plain-Python ones do, and the report says
404
+ which of the two it is rather than leaving the reader to infer it from a short list:
405
 
406
  ```
407
+ checking upload_handler.py as odoo
408
+ ! no Odoo imports here and no __manifest__.py above it -- the 19 ORM rules
409
+ have nothing to match, so this file is effectively reviewed against the
410
+ 8 plain-Python ones
411
 
412
  Rules retrieved:
413
  0.698 [critical] python.assert-is-not-validation
 
416
  0.643 [critical] python.subprocess-shell-true-is-injection
417
  ```
418
 
419
+ Unlike `nexa ask`, this one has **surroundings to read**: the nearest
420
  `__manifest__.py` gives the Odoo series, the file gives its model names and existing
421
+ imports, `ir.model.access.csv` and the record rules in XML say what Python cannot.
422
+ Those facts go into the question *and* into the context layer.
423
 
424
  Where they say the corpus was verified somewhere else, that is reported as a **gap**,
425
  not a finding — it is true of the corpus whatever the answer says, and a verdict that
 
534
  Everything else is **derived** and must never be hand-edited:
535
 
536
  ```
537
+ corpus/rules/odoo.json --+--> modelfiles/gen-odoo.Modelfile (system prompt)
538
+ +--> bin/index.sqlite (retrieval)
539
+ +--> every answer (grounding + checking)
540
  ```
541
 
542
  Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, no drift.
 
545
 
546
  | | |
547
  |---|---|
548
+ | Rules | 27 — 19 Odoo ORM, 8 plain-Python, one stack |
549
+ | Critical | 12 |
550
+ | Machine-checkable | 26 (96%) — 16 by pattern, 10 by AST |
551
+ | Traceable to upstream docs | 25 (93%), each audited for topic fit |
552
  | Pattern false positives | 0 cross-hits across the corpus |
553
+ | Recall under meaning-preserving rewrite | 79/79 |
554
+ | Satisfied patterns bypassable by a comment | 0 of 7 |
555
+ | Retrievable by their own probe | 27 of 27 at top 6, 25 of them at top 3 |
556
+ | Hit rate on 33,960 lines of reviewed OCA code | 3.5 per 1000 lines |
557
+ | Those 120 hits, classified by hand | **86 true positives**, 21 false, 13 undecidable from one file |
558
+ | Hit rate on 226,937 lines of OpenSPP (Odoo 19) | 0.9 per 1000 lines, +69 another tool had already flagged, +71 advisory |
559
+ | Every one of those findings, classified by hand | **157 true positives**, 137 false, 6 undecidable |
560
  | …the two `depends`/`constrains` rules alone | **87 true**, 15 false, 3 undecidable — 85% |
561
+ | …the 8 composed-in Python rules alone | **26 true**, 24 false — 52%, and 20 of the 26 are one rule |
562
  | …`sudo-bypasses-record-rules` alone, before it was narrowed | 6 true, 97 false — 5.8%, and the reason it is now an AST pass |
 
 
 
563
  | **Recall against real Odoo bugfixes** | **1 of 56 (OCA), 7 of 162 (OpenSPP). See below.** |
564
  | Verdict stability, 12 questions x 5 runs | 12/12 same verdict every run |
565
  | Escalation rescued a bad draft | 11 of 21, 95% CI 32–72% |
 
569
  | `nexa check` verdict stability | 4 of 5 files on one run, 3 of 5 on the next |
570
  | `nexa check --no-model` | 0.26s, identical output every run |
571
 
572
+ 26 of 27 rules carry an automated check. The one that does not —
573
+ `schema-change-needs-migration`, whose violation lives in the diff between two module
574
+ versions — returns **no verdict** rather than a false pass, and `nexa coverage` names
575
+ it and says why.
576
+
577
+ Two lines above are worth reading together. The Odoo half is 85% on its two flagship
578
+ rules; the composed-in Python half is 52%. They are in the same corpus and they are
579
+ not the same quality, and the table says so rather than reporting one blended number
580
+ that describes neither.
581
 
582
  ---
583
 
 
669
  it that suppression now reads. **All 79 true positives survived both changes**, which is
670
  again the only thing that makes the rest of it safe.
671
 
672
+ **Re-measured again on 2026-09-16, after the eight Python rules were composed in:
673
+ 120 hits, 3.5 per 1000 lines, 86 true, 21 false, 13 undecidable.** The 18 new ones are
674
+ 7 true and 11 false, read the same way and listed in
675
+ `classification-python-2026-09.json`. The rate went up and the precision went down, and
676
+ that is the trade the section above prices.
677
+
678
  ### The same treatment on a second ecosystem
679
 
680
  A precision number from one ecosystem is a precision number for that ecosystem. The two
 
740
 
741
  The last row is the only one that makes the others safe.
742
 
743
+ That 162 is the Odoo rules alone. Composing the eight Python rules in took it to
744
+ **194, 0.9 per 1000 lines** — the 32 new findings are 19 true and 13 false, and they
745
+ are the subject of
746
+ [the composition section](#what-the-composition-actually-found-all-50-read-by-hand).
747
+
748
  ### What is still wrong, named
749
 
750
  `float-comparison-needs-float-utils` is next at **2 true, 5 false**. All five compare an
 
756
  onchange that sets a field rather than enforcing anything — which the rule's existing
757
  narrowing was supposed to exclude and does not catch when the write is to a Many2one.
758
 
759
+ The composed-in Python half has four more, each with a number on it:
760
+ `decimal-for-money-not-float` firing on Odoo's own `Monetary` idiom, `path-join`
761
+ matching a variable's name rather than its origin, patterns matching inside docstrings,
762
+ and a suppression marker silencing a rule it was not written about. All four are in
763
+ [the section above](#what-is-wrong-with-the-other-half-named).
764
+
765
  What the first run found:
766
 
767
  - **`onchange-is-not-validation`** fired on all 16 `@api.onchange` declarations that
 
966
  ```
967
 
968
  Each Odoo rule declares the series it holds for. The first mode checks that claim
969
+ against the documentation: 45 of the 48 versioned rule-series cite a page that exists
970
+ and names the API, 2 more are tutorials that moved while the API stayed, and **1** is
971
+ not supported by any 16.0 page. The 9 remaining cite something other than the Odoo
972
+ docs — Postgres, the contributing guidelines — and are reported as unversioned rather
973
+ than counted as passing.
974
 
975
  Documentation has a blind spot, and the corpus had fallen into it — a function Odoo
976
  shipped and never described reads as missing. So the second mode reads odoo/odoo
 
979
 
980
  | | |
981
  |---|---|
982
+ | Rule-series defined in source across 16.0, 17.0 and 18.0 | **57 of 57** |
983
 
984
  That closes it. No version claim in this corpus rests on an assertion, and the one
985
  documentation gap now carries a measurement instead of a belief: `float_compare`'s
 
1002
  asks each rule its own probe and reports whether it comes back. No generation, so it
1003
  needs only the embedding model and finishes in seconds.
1004
 
1005
+ All 27 are retrievable at the operating `TOP_K` of 6, and 25 of them rank in the top
1006
+ 3. That number got harder rather than easier when the corpus became one stack: with
1007
+ 19 rules competing, `prefer-sql-constraint` came back first for *"stop anyone entering
1008
+ a negative quantity on a sale order line"*; with 27 it stopped coming back at all,
1009
+ because that is the generic validation question every constraint rule here answers.
1010
+ Reworded to the question only it answers — *"should this check be a database
1011
+ constraint or an api.constrains method"* — and measured again. It is the third rule to
1012
+ fail this way, and all three failed identically: correctly worded, in the vocabulary of
1013
  somebody who already knows the answer. `no-monkey-patching` did not answer *"change
1014
+ what a core sale.order method does"* either.
 
1015
 
1016
  The report states its own limits. Pinned rules are prepended regardless of score, so
1017
+ ranking against them measures the pin rather than the wording, and the header says so
1018
+ instead of counting them.
 
1019
 
1020
  ### And what does `nexa check` actually report?
1021
 
bin/adversarial.py CHANGED
@@ -41,7 +41,10 @@ HERE = os.path.dirname(os.path.abspath(__file__))
41
  ROOT = os.path.dirname(HERE)
42
  RULES_DIR = os.path.join(ROOT, "corpus", "rules")
43
 
44
- COMMENT = {"odoo": "#", "python": "#", "typescript": "//", "javascript": "//"}
 
 
 
45
 
46
 
47
  def load_rules():
 
41
  ROOT = os.path.dirname(HERE)
42
  RULES_DIR = os.path.join(ROOT, "corpus", "rules")
43
 
44
+ # Keyed by stack, of which there is one. A dict rather than a constant because
45
+ # the probes index it by the rule's stack and a KeyError is the right answer if
46
+ # a rule ever carries one this does not know about.
47
+ COMMENT = {"odoo": "#"}
48
 
49
 
50
  def load_rules():
bin/context.py CHANGED
@@ -4,14 +4,15 @@
4
  WHY THIS EXISTS
5
  ---------------
6
  ARCHITECTURE records the context layer as absent by design, and names the reason
7
- honestly: typescript.safeparse-when-failure-is-expected cannot be decided from a
8
- snippet, because whether parse or safeParse is right depends on whether the
9
- caller can recover -- context a snippet does not contain.
10
 
11
  That reasoning is correct about snippets and wrong about files. When the input is
12
  a real path in a real tree, the context is sitting on disk: the module manifest
13
  says which Odoo version this has to run on, the file says which model it extends,
14
- the tsconfig says whether strict mode is on. None of it needs a model to guess.
 
15
 
16
  So this module reads the surroundings, and the context layer reports UNKNOWN --
17
  never PASS -- whenever it was handed a snippet with no surroundings to read. An
@@ -53,29 +54,31 @@ _MODEL_RE = re.compile(r"^\s*_(?:name|inherit)\s*=\s*[\"']([a-z0-9_.]+)[\"']", r
53
  _ODOO_IMPORT_RE = re.compile(r"^\s*from\s+odoo[\w.]*\s+import\s+(.+)$", re.M)
54
 
55
 
56
- # One table, read by subject.route as well, so a file cannot be routed to one
57
- # stack by the command and contextualised against another here. They disagreed
58
- # for a while: .mts and .cts were TypeScript to this module and plain Python to
59
- # the router, so a .mts file was reviewed against the Python corpus while this
60
- # layer went looking for its tsconfig.
61
- EXTENSIONS = {
62
- ".ts": "typescript", ".tsx": "typescript",
63
- ".mts": "typescript", ".cts": "typescript",
64
- ".js": "javascript", ".jsx": "javascript",
65
- ".mjs": "javascript", ".cjs": "javascript",
66
- }
67
 
68
 
69
  def stack_for(path):
70
- """The rule stack a file belongs to, as far as its extension can say.
71
-
72
- A .py file is "python" here, and that is a statement about the extension
73
- rather than about the file: whether it wants the Odoo rules or the plain
74
- Python ones is decided by what is inside it and what sits above it, which is
75
- subject.route's job. A caller that has read the file passes the answer back
76
- in through from_file(stack=...).
 
 
77
  """
78
- return EXTENSIONS.get(os.path.splitext(path)[1].lower(), "python")
 
 
79
 
80
 
81
  def _walk_up(start, names):
@@ -129,8 +132,6 @@ def from_file(path, stack=None):
129
  "manifest": None,
130
  "models": [],
131
  "odoo_imports": [],
132
- "ts_strict": None,
133
- "tsconfig": None,
134
  "is_test": False,
135
  # How much of the file the answer was about. Filled in by subject.plan,
136
  # which is the only thing that knows; None means nobody chose an
@@ -162,35 +163,9 @@ def from_file(path, stack=None):
162
  for line in _ODOO_IMPORT_RE.findall(body):
163
  imports.extend(part.strip() for part in line.split(",") if part.strip())
164
  ctx["odoo_imports"] = sorted(set(imports))
165
- else:
166
- tsconfig = _walk_up(path, ("tsconfig.json",))
167
- if tsconfig:
168
- ctx["tsconfig"] = tsconfig
169
- ctx["ts_strict"] = _strict_from(tsconfig)
170
  return ctx
171
 
172
 
173
- def _strict_from(tsconfig):
174
- """strict from a tsconfig, tolerating the comments TypeScript allows in it.
175
-
176
- JSON with comments is not JSON, and tsconfig.json is full of them. Stripping
177
- line comments first is enough in practice; a file that still will not parse
178
- returns None, which reads as 'not determinable' rather than 'not strict'.
179
- """
180
- text = _read(tsconfig)
181
- cleaned = re.sub(r"^\s*//.*$", "", text, flags=re.M)
182
- cleaned = re.sub(r"/\*.*?\*/", "", cleaned, flags=re.S)
183
- cleaned = re.sub(r",(\s*[}\]])", r"\1", cleaned)
184
- try:
185
- data = json.loads(cleaned)
186
- except ValueError:
187
- return None
188
- options = data.get("compilerOptions")
189
- if not isinstance(options, dict):
190
- return None
191
- return options.get("strict")
192
-
193
-
194
  def empty(stack):
195
  """The context for a bare question: nothing known, and said so.
196
 
@@ -206,8 +181,6 @@ def empty(stack):
206
  "manifest": None,
207
  "models": [],
208
  "odoo_imports": [],
209
- "ts_strict": None,
210
- "tsconfig": None,
211
  "is_test": False,
212
  # How much of the file the answer was about. Filled in by subject.plan,
213
  # which is the only thing that knows; None means nobody chose an
@@ -521,9 +494,6 @@ def describe(ctx):
521
  lines.append("Models in this file: {0}".format(", ".join(ctx["models"])))
522
  if ctx.get("odoo_imports"):
523
  lines.append("Already imported from odoo: {0}".format(", ".join(ctx["odoo_imports"])))
524
- if ctx.get("ts_strict") is not None:
525
- lines.append("TypeScript strict mode: {0}".format(
526
- "on" if ctx["ts_strict"] else "OFF -- optional properties and null are not checked"))
527
  if ctx.get("is_test"):
528
  lines.append("This file is a test, so throwing on bad input is acceptable here.")
529
  return lines
 
4
  WHY THIS EXISTS
5
  ---------------
6
  ARCHITECTURE records the context layer as absent by design, and names the reason
7
+ honestly: odoo.ondelete-is-a-decision cannot be decided from a snippet, because
8
+ whether a dangling reference matters depends on whether anything ever reads the
9
+ field back -- context a snippet does not contain.
10
 
11
  That reasoning is correct about snippets and wrong about files. When the input is
12
  a real path in a real tree, the context is sitting on disk: the module manifest
13
  says which Odoo version this has to run on, the file says which model it extends,
14
+ ir.model.access.csv says whether the model can be deleted at all. None of it
15
+ needs a model to guess.
16
 
17
  So this module reads the surroundings, and the context layer reports UNKNOWN --
18
  never PASS -- whenever it was handed a snippet with no surroundings to read. An
 
54
  _ODOO_IMPORT_RE = re.compile(r"^\s*from\s+odoo[\w.]*\s+import\s+(.+)$", re.M)
55
 
56
 
57
+ # What this corpus is able to review, and nothing else. One table, read by
58
+ # subject.route as well, so a file cannot be refused by the command and
59
+ # contextualised here anyway.
60
+ #
61
+ # .py is where every rule but one lives; .xml carries the noupdate rule, whose
62
+ # violation is a <record> in a data file. Anything else is refused by name
63
+ # rather than reviewed: 27 Odoo rules run over a .ts file find nothing, and
64
+ # nothing reads as a clean bill of health.
65
+ REVIEWABLE = (".py", ".xml")
 
 
66
 
67
 
68
  def stack_for(path):
69
+ """The rule stack a file belongs to. There is one, and this says whether
70
+ the file is in it.
71
+
72
+ Returns "odoo" for a file the corpus can review and None for one it cannot.
73
+ Both an Odoo module and a plain Python script return "odoo": the corpus
74
+ holds the ORM rules and the plain-Python rules together, so there is no
75
+ longer a second stack to send a script to, and a script inside an Odoo
76
+ repository -- scripts/, tools/, a migration helper -- is reviewed against
77
+ the eight language rules with the 19 ORM rules finding nothing in it.
78
  """
79
+ if os.path.splitext(path)[1].lower() in REVIEWABLE:
80
+ return "odoo"
81
+ return None
82
 
83
 
84
  def _walk_up(start, names):
 
132
  "manifest": None,
133
  "models": [],
134
  "odoo_imports": [],
 
 
135
  "is_test": False,
136
  # How much of the file the answer was about. Filled in by subject.plan,
137
  # which is the only thing that knows; None means nobody chose an
 
163
  for line in _ODOO_IMPORT_RE.findall(body):
164
  imports.extend(part.strip() for part in line.split(",") if part.strip())
165
  ctx["odoo_imports"] = sorted(set(imports))
 
 
 
 
 
166
  return ctx
167
 
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def empty(stack):
170
  """The context for a bare question: nothing known, and said so.
171
 
 
181
  "manifest": None,
182
  "models": [],
183
  "odoo_imports": [],
 
 
184
  "is_test": False,
185
  # How much of the file the answer was about. Filled in by subject.plan,
186
  # which is the only thing that knows; None means nobody chose an
 
494
  lines.append("Models in this file: {0}".format(", ".join(ctx["models"])))
495
  if ctx.get("odoo_imports"):
496
  lines.append("Already imported from odoo: {0}".format(", ".join(ctx["odoo_imports"])))
 
 
 
497
  if ctx.get("is_test"):
498
  lines.append("This file is a test, so throwing on bad input is acceptable here.")
499
  return lines
bin/corpus.py CHANGED
@@ -5,15 +5,20 @@ The corpus in ../corpus/rules/*.json is the SINGLE SOURCE OF TRUTH at query time
5
  System prompts and the retrieval index are both derived from it, so they cannot
6
  drift apart.
7
 
8
- It is not where rules are EDITED. `nexa build` regenerates every one of those files
9
- from corpus/build_corpus.py, so a rule added to the JSON by hand survives until the
10
- next build and then disappears without a word. Add rules there, in four places: the
11
- stack's list, ENRICH or SEMANTIC_CHECK, PROBES, and check_sources.SYMBOLS if the
12
- stack is Odoo. Each of the four fails loudly when it is the one you forgot.
 
 
 
 
 
13
 
14
  python corpus.py validate preflight the corpus against its schema
15
  python corpus.py prompt odoo emit a SYSTEM block derived from the rules
16
- python corpus.py modelfiles regenerate all Modelfiles from the corpus
17
  python corpus.py index embed every rule for retrieval
18
  python corpus.py ask odoo "question" answer, grounded in retrieved rules
19
  python corpus.py verify answer.txt check every cited rule id actually exists
@@ -103,8 +108,12 @@ def rank_findings(hits):
103
  hit.line, hit.rule))
104
 
105
 
106
- STACK_MODEL = {"odoo": "ai-odoo", "python": "ai-py",
107
- "typescript": "ai-ts", "javascript": "ai-ts"}
 
 
 
 
108
  # Splitting an answer into code and prose, and deciding what counts as a
109
  # citation, live in layers.py -- so the CLI, the pipeline and the layer engine
110
  # cannot drift apart on either question. Re-exported under the old names
@@ -245,8 +254,14 @@ def cmd_validate():
245
  errors.append("{0}: duplicate id (also in stack '{1}')".format(path, seen[rid]))
246
  seen[rid] = rule.get("stack")
247
 
248
- if "stack" in rule and isinstance(rid, str) and not rid.startswith(rule["stack"] + "."):
249
- errors.append("{0}: id prefix does not match stack '{1}'".format(path, rule["stack"]))
 
 
 
 
 
 
250
 
251
  # advisory is derived from likelihood by build_corpus.py. If the two
252
  # disagree, the JSON was hand-edited, and a rule claiming to gate while
@@ -352,23 +367,14 @@ def cmd_validate():
352
  # --------------------------------------------------------------------- prompt
353
 
354
  HEADER = {
 
 
 
355
  "odoo": (
356
- "You are an Odoo engineer. You write Python using the Odoo ORM. You never write "
357
- "TypeScript, JavaScript, Zod or Pydantic. Every answer is Odoo Python."
358
- ),
359
- "python": (
360
- "You are a Python engineer specialising in input validation and the places a "
361
- "check appears to hold and does not. You write plain Python and the standard "
362
- "library. You never write Odoo ORM code, TypeScript or JavaScript: if the "
363
- "question is about Odoo models, say so rather than answering it here."
364
- ),
365
- "typescript": (
366
- "You are a TypeScript engineer specialising in runtime validation at system "
367
- "boundaries. You never write Python or Odoo server code. Every answer is TypeScript."
368
- ),
369
- "javascript": (
370
- "You are a JavaScript engineer specialising in runtime validation at system "
371
- "boundaries. You never write Python or Odoo server code. Every answer is JavaScript."
372
  ),
373
  }
374
 
@@ -432,8 +438,7 @@ def cmd_prompt(stack):
432
  REGISTRY = "registry.ollama.ai/library/"
433
 
434
  DEEP_MODEL = "qwen3-coder:30b"
435
- BASE = {"odoo": "qwen2.5-coder:7b", "python": "qwen2.5-coder:7b",
436
- "typescript": "qwen2.5-coder:7b", "javascript": "qwen2.5-coder:7b"}
437
  PARAMS = """PARAMETER temperature 0.2
438
  PARAMETER top_p 0.85
439
  PARAMETER top_k 30
@@ -445,8 +450,7 @@ PARAMETER num_gpu 99"""
445
  def cmd_modelfiles():
446
  """Regenerate Modelfiles from the corpus. Never hand-edit the generated files."""
447
  made = []
448
- for stack, model in (("odoo", "ai-odoo"), ("python", "ai-py"),
449
- ("typescript", "ai-ts"), ("javascript", "ai-js")):
450
  body = render_prompt(stack).replace('"""', "'''")
451
  text = (
452
  "# GENERATED by bin/corpus.py modelfiles - do not hand-edit.\n"
 
5
  System prompts and the retrieval index are both derived from it, so they cannot
6
  drift apart.
7
 
8
+ It is not where rules are EDITED. `nexa build` regenerates that file from
9
+ corpus/build_corpus.py, so a rule added to the JSON by hand survives until the
10
+ next build and then disappears without a word. Add rules there, in four places:
11
+ ODOO (or PYTHON, for a rule about the language rather than the ORM), ENRICH or
12
+ SEMANTIC_CHECK, PROBES, and check_sources.SYMBOLS. Each of the four fails
13
+ loudly when it is the one you forgot.
14
+
15
+ There is one stack, `odoo`, and every rule is selected for it. The `python.`
16
+ prefix on eight of them is provenance, not a second stack: an Odoo module is a
17
+ Python module, so the language rules apply to it.
18
 
19
  python corpus.py validate preflight the corpus against its schema
20
  python corpus.py prompt odoo emit a SYSTEM block derived from the rules
21
+ python corpus.py modelfiles regenerate the Modelfile from the corpus
22
  python corpus.py index embed every rule for retrieval
23
  python corpus.py ask odoo "question" answer, grounded in retrieved rules
24
  python corpus.py verify answer.txt check every cited rule id actually exists
 
108
  hit.line, hit.rule))
109
 
110
 
111
+ STACK_MODEL = {"odoo": "ai-odoo"}
112
+
113
+ # The id prefixes a rule may carry. Provenance: "odoo." is a rule about the ORM
114
+ # and is sourced to the Odoo documentation, "python." one about the language,
115
+ # sourced to docs.python.org. Both are selected for the one stack.
116
+ ORIGINS = ("odoo", "python")
117
  # Splitting an answer into code and prose, and deciding what counts as a
118
  # citation, live in layers.py -- so the CLI, the pipeline and the layer engine
119
  # cannot drift apart on either question. Re-exported under the old names
 
254
  errors.append("{0}: duplicate id (also in stack '{1}')".format(path, seen[rid]))
255
  seen[rid] = rule.get("stack")
256
 
257
+ # The prefix names the rule's ORIGIN, not its stack -- there is one
258
+ # stack and eight of its rules come from the Python corpus. It is still
259
+ # checked, because an id is the thing every measurement and every
260
+ # citation is keyed on: a typo'd prefix silently orphans a rule from
261
+ # its own classification artifact.
262
+ if isinstance(rid, str) and rid.split(".")[0] not in ORIGINS:
263
+ errors.append("{0}: id prefix is not one of {1}".format(
264
+ path, ", ".join(sorted(ORIGINS))))
265
 
266
  # advisory is derived from likelihood by build_corpus.py. If the two
267
  # disagree, the JSON was hand-edited, and a rule claiming to gate while
 
367
  # --------------------------------------------------------------------- prompt
368
 
369
  HEADER = {
370
+ # One stack, one header. It names the Python half explicitly because the
371
+ # corpus holds eight rules about the language rather than the ORM, and an
372
+ # answer that treats "Odoo engineer" as "ORM only" drops them.
373
  "odoo": (
374
+ "You are an Odoo engineer. You write Python using the Odoo ORM, and plain "
375
+ "Python where the ORM is not involved -- an Odoo module is a Python module, "
376
+ "so the standard library's traps are yours too. You never write TypeScript, "
377
+ "JavaScript, Zod or Pydantic. Every answer is Python."
 
 
 
 
 
 
 
 
 
 
 
 
378
  ),
379
  }
380
 
 
438
  REGISTRY = "registry.ollama.ai/library/"
439
 
440
  DEEP_MODEL = "qwen3-coder:30b"
441
+ BASE = {"odoo": "qwen2.5-coder:7b"}
 
442
  PARAMS = """PARAMETER temperature 0.2
443
  PARAMETER top_p 0.85
444
  PARAMETER top_k 30
 
450
  def cmd_modelfiles():
451
  """Regenerate Modelfiles from the corpus. Never hand-edit the generated files."""
452
  made = []
453
+ for stack, model in (("odoo", "ai-odoo"),):
 
454
  body = render_prompt(stack).replace('"""', "'''")
455
  text = (
456
  "# GENERATED by bin/corpus.py modelfiles - do not hand-edit.\n"
bin/crosswalk.py CHANGED
@@ -55,16 +55,13 @@ import layers # noqa: E402
55
 
56
  CROSSWALK_VERSION = "1.0.0"
57
 
58
- # A semgrep rule says which languages it is about. Ours say which stack, and the
59
- # two do not line up one to one: a `languages: [python]` rule is answerable by
60
- # anything in the odoo or python stacks, because both of those are Python.
 
61
  LANG_STACKS = {
62
- "python": ("odoo", "python"),
63
- "py": ("odoo", "python"),
64
- "js": ("javascript", "typescript"),
65
- "javascript": ("javascript", "typescript"),
66
- "ts": ("typescript", "javascript"),
67
- "typescript": ("typescript", "javascript"),
68
  }
69
 
70
  # Directories that are never the code under review. The same list fieldtest
@@ -426,10 +423,10 @@ def crosswalk(external, our_rules):
426
  def covered_stacks(external):
427
  """The stacks their ruleset is even about.
428
 
429
- A Python-only ruleset having nothing to say about a TypeScript rule of ours
430
- is not a finding, it is the languages line at the bottom of every one of
431
- their rules. Comparing across it would pad the gap with entries whose answer
432
- was known before the tool ran.
433
  """
434
  stacks = set()
435
  for ext in external:
 
55
 
56
  CROSSWALK_VERSION = "1.0.0"
57
 
58
+ # A semgrep rule says which languages it is about. Ours say which stack, of
59
+ # which there is one, and it is Python: a `languages: [python]` rule is
60
+ # answerable by this corpus and a `languages: [js]` rule is not, which is the
61
+ # whole of the mapping now.
62
  LANG_STACKS = {
63
+ "python": ("odoo",),
64
+ "py": ("odoo",),
 
 
 
 
65
  }
66
 
67
  # Directories that are never the code under review. The same list fieldtest
 
423
  def covered_stacks(external):
424
  """The stacks their ruleset is even about.
425
 
426
+ A JavaScript-only ruleset having nothing to say about any rule of ours is
427
+ not a finding, it is the languages line at the bottom of every one of their
428
+ rules. Comparing across it would pad the gap with entries whose answer was
429
+ known before the tool ran.
430
  """
431
  stacks = set()
432
  for ext in external:
bin/eval.py CHANGED
@@ -68,12 +68,12 @@ DEFAULT_QUESTIONS = [
68
  ("odoo", "compute a stored total from the order lines"),
69
  ("odoo", "make the partner code unique across the table"),
70
  ("odoo", "compare two prices for equality"),
71
- ("typescript", "type the result of a res.partner search_read call over JSON-RPC"),
72
- ("typescript", "validate an env var holding a database URL at startup"),
73
- ("typescript", "read the parent company name off a many2one field"),
74
- ("javascript", "look up a value in a config object using a key from the request query string"),
75
- ("javascript", "parse a JSON body from an incoming request"),
76
- ("javascript", "read a page number out of the query string"),
77
  ]
78
 
79
 
@@ -179,9 +179,9 @@ def retrievability(stack=None, k=None):
179
  This is the measurement `nexa report rules` cannot make. Telemetry can only
180
  say what WAS retrieved by the questions somebody happened to ask, and a rule
181
  absent from that list may simply never have had a relevant question put to
182
- it. Fifteen recorded queries, twelve of them Odoo, against a TOP_K of 6 and
183
- ten TypeScript rules, is a sample in which four TypeScript rules could not
184
- have appeared however well they were written.
185
 
186
  A probe is a question aimed at one rule. If the rule does not come back for
187
  it, retrieval has genuinely missed it, and the rule is unreachable no matter
@@ -366,9 +366,9 @@ def summarise(rows, runs, pipeline_mode):
366
 
367
  retrieved = collections.Counter(
368
  rid for r in rows for rid in r["retrieved"])
369
- # Scoped to the stacks that actually ran. Reporting every Odoo rule as
370
- # "never retrieved" after a JavaScript-only eval is true and useless,
371
- # and it buries the rules that really were missed.
372
  stacks = {r["stack"] for r in rows}
373
  every = {r["id"] for r in corpus.load_rules() if r["stack"] in stacks}
374
  never = sorted(every - set(retrieved))
@@ -395,22 +395,17 @@ DEFAULT_SUBJECTS = [
395
  os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"),
396
  os.path.join(SUBJECTS, "odoo_module", "models", "fsm_visit.py"),
397
  os.path.join(SUBJECTS, "etl", "load_prices.py"),
398
- os.path.join(SUBJECTS, "web", "src", "partners.ts"),
399
- os.path.join(SUBJECTS, "web", "src", "board.js"),
400
  ]
401
 
402
- CHECKABLE = (".py", ".ts", ".tsx", ".js", ".jsx", ".mjs")
 
 
 
403
  SKIP_DIRS = frozenset([".git", "node_modules", "__pycache__", ".venv", "venv",
404
  "dist", "build", ".tox", "static"])
405
 
406
  _PY_NAME = re.compile(r"^(?:class|def)\s+(\w+)", re.M)
407
  _PY_METHOD = re.compile(r"^\s+def\s+(\w+)", re.M)
408
- # Column zero on purpose. Indented const and let are locals inside a handler,
409
- # and counting `page` or `size` as something the file declares would score any
410
- # answer that mentions pagination as a review of this file.
411
- _WEB_NAME = re.compile(
412
- r"^(?:export\s+(?:default\s+)?)?(?:async\s+)?"
413
- r"(?:function|class|interface|type|enum|const|let|var)\s+(\w+)", re.M)
414
 
415
 
416
  def declared_names(body, stack):
@@ -419,17 +414,12 @@ def declared_names(body, stack):
419
  Not a quality measure. An answer that never mentions a single function in
420
  the file it was handed is not reviewing that file, whatever its verdict
421
  says, and there is no way to see that from the verdict alone.
 
 
 
 
422
  """
423
- if stack in ("odoo", "python"):
424
- names = set(_PY_NAME.findall(body)) | set(_PY_METHOD.findall(body))
425
- else:
426
- names = set()
427
- for line in body.splitlines():
428
- if "require(" in line or line.startswith("import "):
429
- continue # a binding for somebody else's name, not this file's
430
- found = _WEB_NAME.match(line)
431
- if found:
432
- names.add(found.group(1))
433
  # Dunders are noise, and a name too short to be a coincidence-free match
434
  # against English prose cannot carry this measurement.
435
  return sorted(n for n in names if len(n) > 3 and not n.startswith("__"))
@@ -811,10 +801,10 @@ def summarise_check(rows, runs, skipped=0):
811
 
812
  print("CONTEXT -- the layer `ask` can never run")
813
  print(" ask has no file, so this layer always reports UNKNOWN. With a file it")
814
- print(" reads the manifest and the tsconfig -- but reading is not checking. A")
815
- print(" verdict needs a question it can answer: a series the corpus was checked")
816
- print(" against, strict on, or a rule carrying odoo_versions. Anything else is")
817
- print(" read, stated as a gap, and left UNKNOWN.")
818
  reached, read = 0, 0
819
  for path, row in files.items():
820
  verdicts = {r["context_verdict"] for r in rows if r["path"] == path}
@@ -913,7 +903,7 @@ def main(argv):
913
  parser.add_argument("--runs", type=int, default=3,
914
  help="repetitions per question (default 3)")
915
  parser.add_argument("--stack",
916
- help="odoo, python, typescript or javascript")
917
  parser.add_argument("--pipeline", action="store_true",
918
  help="measure through run(), for escalation numbers")
919
  parser.add_argument("--retrieval", action="store_true",
 
68
  ("odoo", "compute a stored total from the order lines"),
69
  ("odoo", "make the partner code unique across the table"),
70
  ("odoo", "compare two prices for equality"),
71
+ # The plain-Python half of the corpus, reached through Odoo-shaped
72
+ # questions because that is where these rules are now selected from: an
73
+ # eval that never asks one measures 19 of the 27 rules.
74
+ ("odoo", "check an amount is positive before posting a payment"),
75
+ ("odoo", "store a total in a monetary field without losing a penny"),
76
+ ("odoo", "compare a date typed by a user against the current time"),
77
  ]
78
 
79
 
 
179
  This is the measurement `nexa report rules` cannot make. Telemetry can only
180
  say what WAS retrieved by the questions somebody happened to ask, and a rule
181
  absent from that list may simply never have had a relevant question put to
182
+ it. Nine recorded queries against a TOP_K of 6 and 27 rules is a sample in
183
+ which most of the corpus could not have appeared however well it was
184
+ written -- arithmetic, not evidence about the rules.
185
 
186
  A probe is a question aimed at one rule. If the rule does not come back for
187
  it, retrieval has genuinely missed it, and the rule is unreachable no matter
 
366
 
367
  retrieved = collections.Counter(
368
  rid for r in rows for rid in r["retrieved"])
369
+ # Scoped to the stacks that actually ran. There is one, so this is a
370
+ # formality now -- kept because the scoping is what stops a partial run
371
+ # reporting rules it never had the chance to retrieve.
372
  stacks = {r["stack"] for r in rows}
373
  every = {r["id"] for r in corpus.load_rules() if r["stack"] in stacks}
374
  never = sorted(every - set(retrieved))
 
395
  os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"),
396
  os.path.join(SUBJECTS, "odoo_module", "models", "fsm_visit.py"),
397
  os.path.join(SUBJECTS, "etl", "load_prices.py"),
 
 
398
  ]
399
 
400
+ # The extensions `check` will review, and the same list context.REVIEWABLE
401
+ # holds. A file this expands to that the router then refuses would be an
402
+ # eval that counts a file nobody checked.
403
+ CHECKABLE = (".py", ".xml")
404
  SKIP_DIRS = frozenset([".git", "node_modules", "__pycache__", ".venv", "venv",
405
  "dist", "build", ".tox", "static"])
406
 
407
  _PY_NAME = re.compile(r"^(?:class|def)\s+(\w+)", re.M)
408
  _PY_METHOD = re.compile(r"^\s+def\s+(\w+)", re.M)
 
 
 
 
 
 
409
 
410
 
411
  def declared_names(body, stack):
 
414
  Not a quality measure. An answer that never mentions a single function in
415
  the file it was handed is not reviewing that file, whatever its verdict
416
  says, and there is no way to see that from the verdict alone.
417
+
418
+ `stack` is still taken, and still the one stack: the signature is what the
419
+ caller has, and a function that silently ignores an argument is worse than
420
+ one that names why it has it.
421
  """
422
+ names = set(_PY_NAME.findall(body)) | set(_PY_METHOD.findall(body))
 
 
 
 
 
 
 
 
 
423
  # Dunders are noise, and a name too short to be a coincidence-free match
424
  # against English prose cannot carry this measurement.
425
  return sorted(n for n in names if len(n) > 3 and not n.startswith("__"))
 
801
 
802
  print("CONTEXT -- the layer `ask` can never run")
803
  print(" ask has no file, so this layer always reports UNKNOWN. With a file it")
804
+ print(" reads the manifest -- but reading is not checking. A verdict needs a")
805
+ print(" question it can answer: a series the corpus was checked against, or a")
806
+ print(" rule carrying odoo_versions. Anything else is read, stated as a gap,")
807
+ print(" and left UNKNOWN.")
808
  reached, read = 0, 0
809
  for path, row in files.items():
810
  verdicts = {r["context_verdict"] for r in rows if r["path"] == path}
 
903
  parser.add_argument("--runs", type=int, default=3,
904
  help="repetitions per question (default 3)")
905
  parser.add_argument("--stack",
906
+ help="odoo, the only stack")
907
  parser.add_argument("--pipeline", action="store_true",
908
  help="measure through run(), for escalation numbers")
909
  parser.add_argument("--retrieval", action="store_true",
bin/fieldtest.py CHANGED
@@ -49,59 +49,23 @@ import external as ext # noqa: E402
49
  import layers # noqa: E402
50
  import semantics # noqa: E402
51
 
52
- # Directories that are never the code under review.
53
- #
54
- # `static` is not in the shared list, because whether it is source depends on
55
- # the stack asking. Reviewing Python it holds assets and vendored bundles.
56
- # Reviewing JavaScript on an Odoo tree it holds ALL of it: every line of
57
- # first-party JS an Odoo module ships lives under static/src. Skipping it
58
- # unconditionally made `fieldtest --stack javascript` answer "0 files. Wrong
59
- # folder, or the wrong --stack" on any Odoo checkout, which is a large part of
60
- # why those seven rules have never been measured against real code.
61
  SKIP_DIRS = frozenset([".git", ".hg", "node_modules", "__pycache__",
62
- ".tox", ".venv", "venv", "dist", "build", "migrations"])
63
- PYTHON_SKIP_DIRS = SKIP_DIRS | {"static"}
64
 
65
 
66
- def skip_dirs(stack):
67
- return PYTHON_SKIP_DIRS if stack in ("odoo", "python") else SKIP_DIRS
68
-
69
-
70
- # A bundle is not the code under review either, and unlike a directory it has to
71
- # be recognised from the file itself. Both markers are ordinary JavaScript
72
- # convention rather than anything Odoo-specific: the name says so, or one line
73
- # is longer than any line a person types. Measured on OpenSPP -- its five
74
- # vendored libraries under static/lib carry single lines of 32,071, 95,621,
75
- # 198,502 and 375,248 characters, while its 59 first-party files under
76
- # static/src top out at 87.
77
- BUNDLE_SUFFIXES = (".min.js", ".min.ts", ".bundle.js", ".bundle.ts")
78
- BUNDLE_LINE = 1000
79
-
80
-
81
- def _is_bundle(path, name):
82
- """Generated or vendored JavaScript, whatever directory it sits in."""
83
- if name.endswith(BUNDLE_SUFFIXES):
84
- return True
85
- try:
86
- with io.open(path, encoding="utf-8", errors="ignore") as handle:
87
- return any(len(line) > BUNDLE_LINE for line in handle)
88
- except OSError:
89
- return False
90
-
91
- EXTENSIONS = {"odoo": (".py",),
92
- "python": (".py",),
93
- "typescript": (".ts", ".tsx"),
94
- "javascript": (".js", ".jsx", ".mjs")}
95
 
96
 
97
  def sources(roots, stack, include_tests):
98
  """Every file under roots that belongs to this stack."""
99
  wanted = EXTENSIONS[stack]
100
- skip = skip_dirs(stack)
101
- js = stack in ("javascript", "typescript")
102
  for root in roots:
103
  for dirpath, dirnames, filenames in os.walk(root):
104
- dirnames[:] = [d for d in dirnames if d not in skip]
105
  if not include_tests and _is_test_path(dirpath):
106
  continue
107
  for name in sorted(filenames):
@@ -109,10 +73,7 @@ def sources(roots, stack, include_tests):
109
  continue
110
  if not include_tests and _is_test_file(name):
111
  continue
112
- path = os.path.join(dirpath, name)
113
- if js and _is_bundle(path, name):
114
- continue
115
- yield path
116
 
117
 
118
  # The extensions the TRIGGERS are read from, which is a wider set than the ones
@@ -133,11 +94,10 @@ def trigger_sources(roots, include_tests=False):
133
  """
134
  for root in roots:
135
  for dirpath, dirnames, filenames in os.walk(root):
136
- # PYTHON_SKIP_DIRS, not the shared list: the facts are read off the
137
- # Python side of the module, and an Odoo tree keeps owl templates
138
- # under static/src/xml. Those are not record rules and must not be
139
- # read as though they were.
140
- dirnames[:] = [d for d in dirnames if d not in PYTHON_SKIP_DIRS]
141
  if not include_tests and _is_test_path(dirpath):
142
  continue
143
  for name in sorted(filenames):
@@ -150,16 +110,15 @@ def _is_test_path(path):
150
  return any(p in ("tests", "test", "__tests__", "spec") for p in parts)
151
 
152
 
153
- # A test named by its FILE rather than by the directory holding it. Python and
154
- # Odoo put tests in a tests/ package, which _is_test_path reads; the JavaScript
155
- # convention is a suffix on a file sitting beside the thing it tests. Anchored
156
- # rather than a bare substring, because `latest_version.py` contains "test_".
157
- TEST_FILE = re.compile(r"(^test_|_test\.|\.test\.|\.spec\.)", re.I)
158
 
159
 
160
  def _is_test_file(name):
161
- """src/hono.test.ts is a test. 45 of the 355 hits in the first TypeScript
162
- field test sat in files like it, none of them shipped code."""
163
  return bool(TEST_FILE.search(name))
164
 
165
 
@@ -412,7 +371,7 @@ def main(argv):
412
  parser = argparse.ArgumentParser(prog="nexa fieldtest", add_help=True)
413
  parser.add_argument("folders", nargs="*", help="trees of real source to read")
414
  parser.add_argument("--stack", default="odoo",
415
- help="odoo (default), python, typescript or javascript")
416
  parser.add_argument("--examples", type=int, default=3,
417
  help="example hits to print per rule (default 3)")
418
  parser.add_argument("--json", help="write every hit here as well")
 
49
  import layers # noqa: E402
50
  import semantics # noqa: E402
51
 
52
+ # Directories that are never the code under review. `static` is in the list:
53
+ # on an Odoo tree it holds assets, vendored bundles and the web client's own
54
+ # JavaScript, and none of the 27 rules has anything to say about any of it.
 
 
 
 
 
 
55
  SKIP_DIRS = frozenset([".git", ".hg", "node_modules", "__pycache__",
56
+ ".tox", ".venv", "venv", "dist", "build", "migrations",
57
+ "static"])
58
 
59
 
60
+ EXTENSIONS = {"odoo": (".py",)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
 
63
  def sources(roots, stack, include_tests):
64
  """Every file under roots that belongs to this stack."""
65
  wanted = EXTENSIONS[stack]
 
 
66
  for root in roots:
67
  for dirpath, dirnames, filenames in os.walk(root):
68
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
69
  if not include_tests and _is_test_path(dirpath):
70
  continue
71
  for name in sorted(filenames):
 
73
  continue
74
  if not include_tests and _is_test_file(name):
75
  continue
76
+ yield os.path.join(dirpath, name)
 
 
 
77
 
78
 
79
  # The extensions the TRIGGERS are read from, which is a wider set than the ones
 
94
  """
95
  for root in roots:
96
  for dirpath, dirnames, filenames in os.walk(root):
97
+ # static/ is skipped here too, and for a reason of its own: an
98
+ # Odoo tree keeps owl templates under static/src/xml, and those are
99
+ # not record rules and must not be read as though they were.
100
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
 
101
  if not include_tests and _is_test_path(dirpath):
102
  continue
103
  for name in sorted(filenames):
 
110
  return any(p in ("tests", "test", "__tests__", "spec") for p in parts)
111
 
112
 
113
+ # A test named by its FILE rather than by the directory holding it. Odoo puts
114
+ # tests in a tests/ package, which _is_test_path reads; a test named test_x.py
115
+ # outside one still has to be recognised. Anchored rather than a bare
116
+ # substring, because `latest_version.py` contains "test_".
117
+ TEST_FILE = re.compile(r"(^test_|_test\.)", re.I)
118
 
119
 
120
  def _is_test_file(name):
121
+ """test_partner.py is a test; latest_version.py is not."""
 
122
  return bool(TEST_FILE.search(name))
123
 
124
 
 
371
  parser = argparse.ArgumentParser(prog="nexa fieldtest", add_help=True)
372
  parser.add_argument("folders", nargs="*", help="trees of real source to read")
373
  parser.add_argument("--stack", default="odoo",
374
+ help="odoo, the only stack (default)")
375
  parser.add_argument("--examples", type=int, default=3,
376
  help="example hits to print per rule (default 3)")
377
  parser.add_argument("--json", help="write every hit here as well")
bin/layers.py CHANGED
@@ -56,8 +56,10 @@ META = os.path.join(ROOT, "corpus", "corpus_meta.json")
56
  PASS, WARN, FAIL, UNKNOWN = "PASS", "WARN", "FAIL", "UNKNOWN"
57
  _RANK = {PASS: 0, UNKNOWN: 1, WARN: 2, FAIL: 3}
58
 
 
 
59
  CITATION_RE = re.compile(
60
- r"\b(odoo|python|typescript|javascript)\.[a-z0-9]+(?:-[a-z0-9]+)*\b")
61
 
62
  # The labels corpus.build_messages requires the answer to carry. Structural
63
  # validation of the ANSWER, as distinct from schema validation of the corpus --
@@ -74,6 +76,10 @@ LEAKED_TEMPLATE = (
74
  )
75
 
76
  PY_LANGS = ("python", "py", "python3", "odoo")
 
 
 
 
77
  JS_LANGS = ("ts", "tsx", "typescript", "js", "jsx", "javascript", "mjs", "node")
78
 
79
  _ESSENTIAL = ("syntactic", "structural", "domain")
@@ -212,8 +218,11 @@ def cited_ids(text):
212
 
213
 
214
  def comment_kind(stack):
215
- """Which comment syntax a stack's generated code uses."""
216
- return "python" if stack in ("odoo", "python") else "js"
 
 
 
217
 
218
 
219
  def matchable(code, stack):
@@ -504,7 +513,8 @@ def _lang_of(lang, stack):
504
  return "js"
505
  if lang:
506
  return "other"
507
- return "python" if stack in ("odoo", "python") else "js"
 
508
 
509
 
510
  # -------------------------------------------------------------- 1 syntactic
@@ -512,10 +522,11 @@ def _lang_of(lang, stack):
512
  def layer_syntactic(text, stack):
513
  """Does the generated code parse at all?
514
 
515
- Python gets a real parse and compile. TS/JS gets delimiter balance, because
516
- there is no JavaScript grammar in the standard library -- so the TS/JS
517
- not_checked entry is permanent and says so rather than implying a parse
518
- happened.
 
519
  """
520
  blocks, unterminated = code_blocks(text)
521
  findings, checked, gaps = [], [], []
@@ -670,7 +681,7 @@ def layer_semantic(text, stack, rules):
670
  for r in semantic_rules:
671
  gaps.append((r["id"], "no Python block in the answer to analyse"))
672
  gaps.append(("the answer", "semantic analysis is Python-only: the standard "
673
- "library has no TypeScript or JavaScript parser"))
674
  return LayerResult("semantic", UNKNOWN, findings, checked, gaps)
675
 
676
  # The checkers see the repaired source, so an indented method fragment is
@@ -814,8 +825,8 @@ def layer_context(rules, ctx):
814
 
815
  if not ctx or ctx.get("source") == "none":
816
  gaps.append(("the environment",
817
- "no file was supplied, so the module, Odoo series and tsconfig "
818
- "are unknown; run 'nexa check <file>' for a contextual verdict"))
819
  return LayerResult("context", UNKNOWN, findings, checked, gaps)
820
 
821
  series = ctx.get("odoo_series")
@@ -842,6 +853,10 @@ def layer_context(rules, ctx):
842
  checked.append("Odoo series {0} from {1}".format(
843
  series, os.path.basename(ctx.get("manifest") or "manifest")))
844
  else:
 
 
 
 
845
  gaps.append(("Odoo series", "no __manifest__.py with a version was found "
846
  "above this file"))
847
 
@@ -855,29 +870,6 @@ def layer_context(rules, ctx):
855
  "rule applies to Odoo {0}, this module is {1}".format(
856
  declared, series), rule=r["id"]))
857
 
858
- if ctx.get("ts_strict") is False:
859
- # Demoted with the series mismatch above, for the same reason: it fires
860
- # on the project rather than on the answer, and it fires on every run in
861
- # any project that has not finished turning strict on. Making it a
862
- # finding again would mean checking whether this answer leans on the
863
- # type system at all -- its declared enforcement layer -- rather than
864
- # assuming every answer does.
865
- gaps.append(("type-based guarantees",
866
- "tsconfig has strict off, so the compile-time half of any "
867
- "type-based guarantee in this answer is not enforced here"))
868
- elif ctx.get("ts_strict") is True:
869
- checked.append("tsconfig strict")
870
- elif ctx.get("stack") in ("typescript", "javascript"):
871
- gaps.append(("tsconfig strict", "no tsconfig.json was found above this file"))
872
- elif ctx.get("stack") == "python":
873
- # Neither branch above applies to a plain Python script, and this used to
874
- # fall through the odoo one and report a missing __manifest__.py -- a
875
- # true sentence about a file that was never going to have one, and the
876
- # only thing this layer said about every Python file it was given.
877
- gaps.append(("the environment",
878
- "a plain Python file has no manifest and no tsconfig, so "
879
- "nothing on disk says which framework or interpreter version "
880
- "this has to run under"))
881
 
882
  unversioned = [r["id"] for r in rules if not r.get("odoo_versions")
883
  and r.get("stack") == "odoo"]
 
56
  PASS, WARN, FAIL, UNKNOWN = "PASS", "WARN", "FAIL", "UNKNOWN"
57
  _RANK = {PASS: 0, UNKNOWN: 1, WARN: 2, FAIL: 3}
58
 
59
+ # The two id prefixes the corpus uses. Both are the one stack; the prefix says
60
+ # where the rule came from, and corpus.ORIGINS is the list this mirrors.
61
  CITATION_RE = re.compile(
62
+ r"\b(odoo|python)\.[a-z0-9]+(?:-[a-z0-9]+)*\b")
63
 
64
  # The labels corpus.build_messages requires the answer to carry. Structural
65
  # validation of the ANSWER, as distinct from schema validation of the corpus --
 
76
  )
77
 
78
  PY_LANGS = ("python", "py", "python3", "odoo")
79
+ # Not a stack, and not reviewed. Odoo ships an OWL front end, so an answer can
80
+ # legitimately contain a JavaScript block -- it is recognised here so the
81
+ # syntactic layer can say it went unchecked, which is the only thing this
82
+ # corpus has to say about JavaScript.
83
  JS_LANGS = ("ts", "tsx", "typescript", "js", "jsx", "javascript", "mjs", "node")
84
 
85
  _ESSENTIAL = ("syntactic", "structural", "domain")
 
218
 
219
 
220
  def comment_kind(stack):
221
+ """Which comment syntax a stack's generated code uses. One stack, one
222
+ answer -- kept as a function because semantics.strip_comments takes the
223
+ kind as an argument, and inlining the constant at each call site is how
224
+ the two would come to disagree."""
225
+ return "python"
226
 
227
 
228
  def matchable(code, stack):
 
513
  return "js"
514
  if lang:
515
  return "other"
516
+ # Untagged, and there is one stack: Python.
517
+ return "python"
518
 
519
 
520
  # -------------------------------------------------------------- 1 syntactic
 
522
  def layer_syntactic(text, stack):
523
  """Does the generated code parse at all?
524
 
525
+ Python gets a real parse and compile. A JavaScript block -- which an Odoo
526
+ answer can legitimately contain, the web client being OWL -- gets delimiter
527
+ balance, because there is no JavaScript grammar in the standard library.
528
+ That not_checked entry is permanent and says so rather than implying a
529
+ parse happened.
530
  """
531
  blocks, unterminated = code_blocks(text)
532
  findings, checked, gaps = [], [], []
 
681
  for r in semantic_rules:
682
  gaps.append((r["id"], "no Python block in the answer to analyse"))
683
  gaps.append(("the answer", "semantic analysis is Python-only: the standard "
684
+ "library has no JavaScript parser"))
685
  return LayerResult("semantic", UNKNOWN, findings, checked, gaps)
686
 
687
  # The checkers see the repaired source, so an indented method fragment is
 
825
 
826
  if not ctx or ctx.get("source") == "none":
827
  gaps.append(("the environment",
828
+ "no file was supplied, so the module and Odoo series are "
829
+ "unknown; run 'nexa check <file>' for a contextual verdict"))
830
  return LayerResult("context", UNKNOWN, findings, checked, gaps)
831
 
832
  series = ctx.get("odoo_series")
 
853
  checked.append("Odoo series {0} from {1}".format(
854
  series, os.path.basename(ctx.get("manifest") or "manifest")))
855
  else:
856
+ # True of an Odoo module whose manifest has no version, and of a
857
+ # plain script in the repository that was never going to have one.
858
+ # Said either way: nothing on disk states the series, so every
859
+ # version-specific rule below is unverified here.
860
  gaps.append(("Odoo series", "no __manifest__.py with a version was found "
861
  "above this file"))
862
 
 
870
  "rule applies to Odoo {0}, this module is {1}".format(
871
  declared, series), rule=r["id"]))
872
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
873
 
874
  unversioned = [r["id"] for r in rules if not r.get("odoo_versions")
875
  and r.get("stack") == "odoo"]
bin/recall.py CHANGED
@@ -68,27 +68,32 @@ import layers # noqa: E402
68
  import semantics # noqa: E402
69
  import subject # noqa: E402
70
 
71
- EXTENSIONS = {"odoo": (".py",), "python": (".py",),
72
- "typescript": (".ts", ".tsx"),
73
- "javascript": (".js", ".jsx", ".mjs")}
74
 
75
  # A commit is IN SCOPE when its diff touches one of these. Hand-written, and
76
  # therefore the weakest link in the measurement -- which is why it is printed
77
  # in the report rather than buried here. Derived from what the rules are about,
78
  # not from their patterns: using the patterns would make the denominator depend
79
  # on the thing being measured, and inflate the answer.
 
 
 
 
 
 
 
 
 
 
80
  SCOPE_TOKENS = {
81
  "odoo": ("api.constrains", "api.depends", "api.onchange", "_sql_constraints",
82
  "ondelete", "check_company", ".sudo(", "Many2one", "Many2many",
83
  "float_compare", "float_is_zero", "_check_recursion", "_has_cycle",
84
- "ValidationError", "store=True", "noupdate"),
85
- "python": ("assert ", "except:", "eval(", "exec(", "pickle.load", "shell=True",
86
- "os.system(", "os.path.join(", "float(", "Decimal", "datetime.now(",
87
- "utcnow(", "def "),
88
- "typescript": ("z.object", "z.string", "z.number", "parse(", "safeParse",
89
- "as ", ": any", "process.env", "z.union", "nullable("),
90
- "javascript": ("JSON.parse", "parseInt", "parseFloat", "forEach", "new Date(",
91
- "||", "hasOwnProperty", "req.body", "req.query"),
92
  }
93
 
94
  # Commit subjects that mean "this fixed something". OCA's own convention is a
@@ -616,7 +621,7 @@ def main(argv):
616
  parser = argparse.ArgumentParser(prog="nexa recall", add_help=True)
617
  parser.add_argument("repos", nargs="*", help="git checkouts WITH history")
618
  parser.add_argument("--stack", default="odoo",
619
- help="odoo (default), python, typescript or javascript")
620
  parser.add_argument("--limit", type=int, default=0,
621
  help="stop after this many fix commits per repo")
622
  parser.add_argument("--misses", type=int, default=25,
 
68
  import semantics # noqa: E402
69
  import subject # noqa: E402
70
 
71
+ EXTENSIONS = {"odoo": (".py",)}
 
 
72
 
73
  # A commit is IN SCOPE when its diff touches one of these. Hand-written, and
74
  # therefore the weakest link in the measurement -- which is why it is printed
75
  # in the report rather than buried here. Derived from what the rules are about,
76
  # not from their patterns: using the patterns would make the denominator depend
77
  # on the thing being measured, and inflate the answer.
78
+ # One list, because there is one stack, and the constructs the composed-in
79
+ # Python rules are about are now inside it. Leaving them out would shrink the
80
+ # denominator against rules that are being counted in the numerator, which is
81
+ # the direction that flatters the tool.
82
+ #
83
+ # One substitution, stated because it is a judgment: the plain-Python list used
84
+ # to carry "def " for mutable-default-argument. On an Odoo tree that matches
85
+ # nearly every diff, and a denominator that large says nothing about anything.
86
+ # The construct the rule is actually about is a mutable default, so that is
87
+ # what is listed.
88
  SCOPE_TOKENS = {
89
  "odoo": ("api.constrains", "api.depends", "api.onchange", "_sql_constraints",
90
  "ondelete", "check_company", ".sudo(", "Many2one", "Many2many",
91
  "float_compare", "float_is_zero", "_check_recursion", "_has_cycle",
92
+ "ValidationError", "store=True", "noupdate",
93
+ # the plain-Python half
94
+ "assert ", "except:", "eval(", "exec(", "pickle.load", "shell=True",
95
+ "os.system(", "os.path.join(", "float(", "Decimal", "datetime.now(",
96
+ "utcnow(", "=[]", "={}", "=set()"),
 
 
 
97
  }
98
 
99
  # Commit subjects that mean "this fixed something". OCA's own convention is a
 
621
  parser = argparse.ArgumentParser(prog="nexa recall", add_help=True)
622
  parser.add_argument("repos", nargs="*", help="git checkouts WITH history")
623
  parser.add_argument("--stack", default="odoo",
624
+ help="odoo, the only stack (default)")
625
  parser.add_argument("--limit", type=int, default=0,
626
  help="stop after this many fix commits per repo")
627
  parser.add_argument("--misses", type=int, default=25,
bin/report.py CHANGED
@@ -149,7 +149,7 @@ def cmd_rules(events):
149
  if known is None:
150
  print(" never retrieved: corpus/rules could not be read")
151
  else:
152
- missing = sorted(known - set(retrieved))
153
  if not missing:
154
  print(" never retrieved: none, every rule has surfaced at least once")
155
  else:
@@ -164,7 +164,8 @@ def _explain_absence(retrieves, missing, known):
164
  """Say how much of that list the sample size explains by itself.
165
 
166
  One query retrieves at most TOP_K rules, so a stack with more rules than
167
- queries x TOP_K has rules that COULD NOT have appeared. Reporting those
 
168
  beside rules that genuinely lost their own ranking, under one heading that
169
  calls both dead weight, is how a measurement turns into a wrong conclusion:
170
  four of the seven names this used to print were on it for arithmetic.
@@ -174,7 +175,7 @@ def _explain_absence(retrieves, missing, known):
174
  for r in retrieves:
175
  if r.get("stack"):
176
  per_stack[r["stack"]] += 1
177
- sizes = collections.Counter(rid.split(".")[0] for rid in known)
178
 
179
  print()
180
  print(" queries recorded, by stack:")
@@ -215,7 +216,13 @@ def _top_k():
215
 
216
 
217
  def _corpus_rule_ids():
218
- """Every rule id in the corpus, or None if it cannot be read.
 
 
 
 
 
 
219
 
220
  Telemetry can only say what WAS retrieved. "Never retrieved" is a statement
221
  about the corpus, so the corpus has to be opened to make it -- which is why
@@ -225,7 +232,7 @@ def _corpus_rule_ids():
225
  rules_dir = os.path.join(os.path.dirname(HERE), "corpus", "rules")
226
  if not os.path.isdir(rules_dir):
227
  return None
228
- ids = set()
229
  try:
230
  for name in sorted(os.listdir(rules_dir)):
231
  if not name.endswith(".json"):
@@ -233,7 +240,7 @@ def _corpus_rule_ids():
233
  with io.open(os.path.join(rules_dir, name), encoding="utf-8") as fh:
234
  data = json.load(fh)
235
  for rule in (data if isinstance(data, list) else data.get("rules", [])):
236
- ids.add(rule["id"])
237
  except (OSError, ValueError, KeyError, TypeError):
238
  return None
239
  return ids or None
 
149
  if known is None:
150
  print(" never retrieved: corpus/rules could not be read")
151
  else:
152
+ missing = sorted(set(known) - set(retrieved))
153
  if not missing:
154
  print(" never retrieved: none, every rule has surfaced at least once")
155
  else:
 
164
  """Say how much of that list the sample size explains by itself.
165
 
166
  One query retrieves at most TOP_K rules, so a stack with more rules than
167
+ queries x TOP_K has rules that COULD NOT have appeared. `known` is
168
+ {id: stack} from _corpus_rule_ids. Reporting those
169
  beside rules that genuinely lost their own ranking, under one heading that
170
  calls both dead weight, is how a measurement turns into a wrong conclusion:
171
  four of the seven names this used to print were on it for arithmetic.
 
175
  for r in retrieves:
176
  if r.get("stack"):
177
  per_stack[r["stack"]] += 1
178
+ sizes = collections.Counter(known.values())
179
 
180
  print()
181
  print(" queries recorded, by stack:")
 
216
 
217
 
218
  def _corpus_rule_ids():
219
+ """{rule id: its stack} for the whole corpus, or None if it cannot be read.
220
+
221
+ The stack comes off the rule and not off the id prefix. Those were the same
222
+ thing until the plain-Python rules were composed into the Odoo stack, and
223
+ splitting on the dot afterwards invented a "python" stack with eight rules
224
+ and no queries -- then reported all eight as unreachable by arithmetic,
225
+ which is a conclusion about a stack that does not exist.
226
 
227
  Telemetry can only say what WAS retrieved. "Never retrieved" is a statement
228
  about the corpus, so the corpus has to be opened to make it -- which is why
 
232
  rules_dir = os.path.join(os.path.dirname(HERE), "corpus", "rules")
233
  if not os.path.isdir(rules_dir):
234
  return None
235
+ ids = {}
236
  try:
237
  for name in sorted(os.listdir(rules_dir)):
238
  if not name.endswith(".json"):
 
240
  with io.open(os.path.join(rules_dir, name), encoding="utf-8") as fh:
241
  data = json.load(fh)
242
  for rule in (data if isinstance(data, list) else data.get("rules", [])):
243
+ ids[rule["id"]] = rule["stack"]
244
  except (OSError, ValueError, KeyError, TypeError):
245
  return None
246
  return ids or None
bin/subject.py CHANGED
@@ -39,7 +39,17 @@ sys.path.insert(0, HERE)
39
 
40
  import context # noqa: E402
41
 
42
- STACKS = ("odoo", "python", "typescript", "javascript")
 
 
 
 
 
 
 
 
 
 
43
 
44
  # How much of a file `nexa check` sends. The 7B runs at num_ctx 8192, which has
45
  # to hold the retrieved rules, the system prompt and the answer as well, so the
@@ -72,27 +82,34 @@ REVIEW_QUESTION = (
72
  def route(path, body, ctx, forced=None):
73
  """(stack, how it was decided). The stack decision on its own, for testing.
74
 
75
- For a .py file the extension only says "not TypeScript". WHICH Python corpus
76
- applies is decided by the file: an odoo import, or a __manifest__.py
77
- somewhere above it, means the Odoo rules; anything else is plain Python and
78
- gets the python stack, which exists precisely so that answer is not fifteen
79
- findings about a framework the file does not use.
80
-
81
- Before the python stack existed this refused instead, with exit 2. That was
82
- honest and it was not useful, which is why the stack is here.
 
 
 
83
  """
84
  if forced:
 
 
 
 
85
  return forced, "--stack"
86
- # One extension table, context.py's, so this cannot decide .mts is Python
87
- # while the layer that reads the surroundings decides it is TypeScript.
88
  by_extension = context.stack_for(path)
89
- if by_extension != "python":
90
- return by_extension, "extension"
91
- if any(s in body for s in ODOO_SIGNALS):
92
- return "odoo", "odoo imports in the file"
93
- if ctx.get("manifest"):
94
- return "odoo", "__manifest__.py above it"
95
- return "python", "a .py file with no Odoo in it"
96
 
97
 
98
  # A line that starts a new top-level thing in TS/JS. Column zero, because an
@@ -370,9 +387,10 @@ def plan(path, forced=None):
370
  with io.open(path, encoding="utf-8", errors="ignore") as fh:
371
  body = fh.read()
372
 
373
- # Gathered first, because routing a .py file needs to know whether there is
374
- # a __manifest__.py above it, and labelled afterwards: the extension cannot
375
- # tell an Odoo module from a plain Python script and this can.
 
376
  ctx = context.from_file(path)
377
  stack, routed_by = route(path, body, ctx, forced)
378
  ctx["stack"] = stack
@@ -385,11 +403,10 @@ def plan(path, forced=None):
385
  # its content does not support is still worth checking -- somebody asked --
386
  # but the reader has to be told which of the two it is.
387
  notes = []
388
- if stack == "odoo" and not looks_odoo:
389
- notes.append("no Odoo imports here and no __manifest__.py above it -- reviewing "
390
- "against the Odoo corpus because --stack odoo was given")
391
- if stack == "python" and odoo_signals:
392
- notes.append("this imports odoo -- run with --stack odoo for the ORM rules")
393
 
394
  # The surroundings go into the question, not just into the context layer. A
395
  # model told the module targets Odoo 17.0 answers differently from one left to
 
39
 
40
  import context # noqa: E402
41
 
42
+ STACKS = ("odoo",)
43
+
44
+
45
+ class Unreviewable(Exception):
46
+ """The file is in a language this corpus has no rules for.
47
+
48
+ Refusing is the point. Running 27 Odoo rules over a .ts file produces an
49
+ empty finding list, and an empty finding list is read as "nothing wrong
50
+ here" -- so silence about a file nobody checked would be the one lie the
51
+ tool is built not to tell.
52
+ """
53
 
54
  # How much of a file `nexa check` sends. The 7B runs at num_ctx 8192, which has
55
  # to hold the retrieved rules, the system prompt and the answer as well, so the
 
82
  def route(path, body, ctx, forced=None):
83
  """(stack, how it was decided). The stack decision on its own, for testing.
84
 
85
+ There is one stack, so this is no longer a choice between corpora -- it is
86
+ the decision whether the corpus applies to this file at all. A .py or .xml
87
+ file is reviewed; anything else raises Unreviewable.
88
+
89
+ It used to pick between an odoo and a python corpus by reading the file for
90
+ an odoo import or walking up for a __manifest__.py. That choice is gone,
91
+ because the two corpora are one: the plain-Python rules are composed into
92
+ the Odoo stack, so a script in an Odoo repository gets them without being
93
+ routed away from the ORM rules first. What the file looks like is still
94
+ read -- plan() states it in the report -- it just no longer decides which
95
+ rules run.
96
  """
97
  if forced:
98
+ if forced not in STACKS:
99
+ raise Unreviewable(
100
+ "unknown stack '{0}'. There is one: {1}".format(
101
+ forced, ", ".join(STACKS)))
102
  return forced, "--stack"
103
+ # One extension table, context.py's, so this cannot refuse a file the layer
104
+ # that reads the surroundings would have contextualised.
105
  by_extension = context.stack_for(path)
106
+ if by_extension is None:
107
+ raise Unreviewable(
108
+ "nexa reviews Odoo code: {0} files. {1} is not one, and there are "
109
+ "no rules in this corpus that would say anything about it.".format(
110
+ " and ".join(context.REVIEWABLE),
111
+ os.path.splitext(path)[1] or "that extension"))
112
+ return by_extension, "extension"
113
 
114
 
115
  # A line that starts a new top-level thing in TS/JS. Column zero, because an
 
387
  with io.open(path, encoding="utf-8", errors="ignore") as fh:
388
  body = fh.read()
389
 
390
+ # Gathered first, and labelled afterwards: the extension cannot tell an Odoo
391
+ # module from a plain Python script and this can. It no longer decides which
392
+ # rules run -- there is one corpus -- but it still decides what the report
393
+ # says about the file, and the context layer needs it either way.
394
  ctx = context.from_file(path)
395
  stack, routed_by = route(path, body, ctx, forced)
396
  ctx["stack"] = stack
 
403
  # its content does not support is still worth checking -- somebody asked --
404
  # but the reader has to be told which of the two it is.
405
  notes = []
406
+ if not looks_odoo:
407
+ notes.append("no Odoo imports here and no __manifest__.py above it -- the 19 "
408
+ "ORM rules have nothing to match, so this file is effectively "
409
+ "reviewed against the 8 plain-Python ones")
 
410
 
411
  # The surroundings go into the question, not just into the context layer. A
412
  # model told the module targets Odoo 17.0 answers differently from one left to
classification-js-2026-09.json DELETED
@@ -1,408 +0,0 @@
1
- {
2
- "generated_utc": "2026-09-16",
3
- "stack": "javascript",
4
- "roots": [
5
- "OpenSPP2-19.0",
6
- "server-tools",
7
- "account-financial-tools"
8
- ],
9
- "files": 62,
10
- "lines": 9304,
11
- "totals": {
12
- "false": 22,
13
- "true": 21
14
- },
15
- "classes": {
16
- "string-default": 21,
17
- "boolean-logic": 1
18
- },
19
- "hits": [
20
- {
21
- "rule": "javascript.loose-equality-and-falsy-defaults",
22
- "path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js",
23
- "line": 11,
24
- "verdict": "false",
25
- "class": "string-default",
26
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
27
- "evidence": "const docUrl = session.spp_documentation_url || \"https://docs.openspp.org\";"
28
- },
29
- {
30
- "rule": "javascript.loose-equality-and-falsy-defaults",
31
- "path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js",
32
- "line": 12,
33
- "verdict": "false",
34
- "class": "string-default",
35
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
36
- "evidence": "const supportUrl = session.spp_support_url || \"https://openspp.org\";"
37
- },
38
- {
39
- "rule": "javascript.loose-equality-and-falsy-defaults",
40
- "path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/webclient.js",
41
- "line": 9,
42
- "verdict": "false",
43
- "class": "string-default",
44
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
45
- "evidence": "const systemName = session.spp_system_name || \"OpenSPP Platform\";"
46
- },
47
- {
48
- "rule": "javascript.loose-equality-and-falsy-defaults",
49
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js",
50
- "line": 128,
51
- "verdict": "true",
52
- "class": null,
53
- "why": null,
54
- "evidence": "return this._currentProfile || this.props.profile || \"registry_individuals\";"
55
- },
56
- {
57
- "rule": "javascript.loose-equality-and-falsy-defaults",
58
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js",
59
- "line": 243,
60
- "verdict": "false",
61
- "class": "string-default",
62
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
63
- "evidence": "minHeight: this.props.minHeight || \"80px\","
64
- },
65
- {
66
- "rule": "javascript.loose-equality-and-falsy-defaults",
67
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js",
68
- "line": 467,
69
- "verdict": "false",
70
- "class": "string-default",
71
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
72
- "evidence": "return firstError?.message || \"Invalid expression\";"
73
- },
74
- {
75
- "rule": "javascript.loose-equality-and-falsy-defaults",
76
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_symbol_service.js",
77
- "line": 78,
78
- "verdict": "false",
79
- "class": "string-default",
80
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
81
- "evidence": "message: error.message || \"Validation failed\","
82
- },
83
- {
84
- "rule": "javascript.loose-equality-and-falsy-defaults",
85
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/autocomplete.js",
86
- "line": 198,
87
- "verdict": "false",
88
- "class": "boolean-logic",
89
- "why": "genuine boolean logic, which the rule explicitly reserves || for",
90
- "evidence": "if (!prefix || \"andornot\".includes(prefix)) {"
91
- },
92
- {
93
- "rule": "javascript.loose-equality-and-falsy-defaults",
94
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/lint.js",
95
- "line": 105,
96
- "verdict": "false",
97
- "class": "string-default",
98
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
99
- "evidence": "message: error.message || \"Unknown error\","
100
- },
101
- {
102
- "rule": "javascript.loose-equality-and-falsy-defaults",
103
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js",
104
- "line": 143,
105
- "verdict": "true",
106
- "class": null,
107
- "why": null,
108
- "evidence": "currentTier: pendingReview.current_tier || 1,"
109
- },
110
- {
111
- "rule": "javascript.loose-equality-and-falsy-defaults",
112
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js",
113
- "line": 144,
114
- "verdict": "true",
115
- "class": null,
116
- "why": null,
117
- "evidence": "totalTiers: pendingReview.tier_review_ids?.length || 1,"
118
- },
119
- {
120
- "rule": "javascript.number-parsing-is-lossy",
121
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_review_documents.js",
122
- "line": 37,
123
- "verdict": "true",
124
- "class": null,
125
- "why": null,
126
- "evidence": "const docId = parseInt(el.dataset.docId, 10);"
127
- },
128
- {
129
- "rule": "javascript.number-parsing-is-lossy",
130
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js",
131
- "line": 36,
132
- "verdict": "true",
133
- "class": null,
134
- "why": null,
135
- "evidence": "const partnerId = parseInt(row.dataset.partnerId, 10);"
136
- },
137
- {
138
- "rule": "javascript.number-parsing-is-lossy",
139
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js",
140
- "line": 47,
141
- "verdict": "true",
142
- "class": null,
143
- "why": null,
144
- "evidence": "const page = parseInt(link.dataset.page);"
145
- },
146
- {
147
- "rule": "javascript.loose-equality-and-falsy-defaults",
148
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js",
149
- "line": 27,
150
- "verdict": "false",
151
- "class": "string-default",
152
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
153
- "evidence": "name: params.name || \"Change Request Details\","
154
- },
155
- {
156
- "rule": "javascript.loose-equality-and-falsy-defaults",
157
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js",
158
- "line": 51,
159
- "verdict": "false",
160
- "class": "string-default",
161
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
162
- "evidence": "name: params.name || \"Change Request\","
163
- },
164
- {
165
- "rule": "javascript.number-parsing-is-lossy",
166
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js",
167
- "line": 73,
168
- "verdict": "true",
169
- "class": null,
170
- "why": null,
171
- "evidence": "limit: limit ? parseInt(limit, 10) : null,"
172
- },
173
- {
174
- "rule": "javascript.number-parsing-is-lossy",
175
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js",
176
- "line": 74,
177
- "verdict": "true",
178
- "class": null,
179
- "why": null,
180
- "evidence": "countLimit: countLimit ? parseInt(countLimit, 10) : null,"
181
- },
182
- {
183
- "rule": "javascript.json-parse-returns-any",
184
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
185
- "line": 397,
186
- "verdict": "true",
187
- "class": null,
188
- "why": null,
189
- "evidence": "defaultCenter = JSON.parse(editInfo.default_center);"
190
- },
191
- {
192
- "rule": "javascript.loose-equality-and-falsy-defaults",
193
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
194
- "line": 606,
195
- "verdict": "false",
196
- "class": "string-default",
197
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
198
- "evidence": "return layer.begin_color || \"#3388ff\";"
199
- },
200
- {
201
- "rule": "javascript.loose-equality-and-falsy-defaults",
202
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
203
- "line": 611,
204
- "verdict": "true",
205
- "class": null,
206
- "why": null,
207
- "evidence": "const maxValue = config.max_value || 100;"
208
- },
209
- {
210
- "rule": "javascript.loose-equality-and-falsy-defaults",
211
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
212
- "line": 649,
213
- "verdict": "false",
214
- "class": "string-default",
215
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
216
- "evidence": "return layer.begin_color || \"#3388ff\";"
217
- },
218
- {
219
- "rule": "javascript.loose-equality-and-falsy-defaults",
220
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
221
- "line": 665,
222
- "verdict": "true",
223
- "class": null,
224
- "why": null,
225
- "evidence": "const classCount = config.class_count || 5;"
226
- },
227
- {
228
- "rule": "javascript.loose-equality-and-falsy-defaults",
229
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
230
- "line": 702,
231
- "verdict": "false",
232
- "class": "string-default",
233
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
234
- "evidence": "fillColor = layer.begin_color || \"#3388ff\";"
235
- },
236
- {
237
- "rule": "javascript.loose-equality-and-falsy-defaults",
238
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
239
- "line": 913,
240
- "verdict": "false",
241
- "class": "string-default",
242
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
243
- "evidence": "fillColor = layer.begin_color || \"#3388ff\";"
244
- },
245
- {
246
- "rule": "javascript.loose-equality-and-falsy-defaults",
247
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
248
- "line": 1040,
249
- "verdict": "true",
250
- "class": null,
251
- "why": null,
252
- "evidence": "const maxValue = config.max_value || 100;"
253
- },
254
- {
255
- "rule": "javascript.loose-equality-and-falsy-defaults",
256
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
257
- "line": 1087,
258
- "verdict": "true",
259
- "class": null,
260
- "why": null,
261
- "evidence": "const classCount = config.class_count || 5;"
262
- },
263
- {
264
- "rule": "javascript.json-parse-returns-any",
265
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
266
- "line": 46,
267
- "verdict": "true",
268
- "class": null,
269
- "why": null,
270
- "evidence": "const default_center = JSON.parse(editInfo.default_center);"
271
- },
272
- {
273
- "rule": "javascript.json-parse-returns-any",
274
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
275
- "line": 96,
276
- "verdict": "true",
277
- "class": null,
278
- "why": null,
279
- "evidence": "const obj = JSON.parse(this.props.record.data[this.props.name]);"
280
- },
281
- {
282
- "rule": "javascript.json-parse-returns-any",
283
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
284
- "line": 140,
285
- "verdict": "true",
286
- "class": null,
287
- "why": null,
288
- "evidence": "const obj = JSON.parse(record.data[fieldName]);"
289
- },
290
- {
291
- "rule": "javascript.json-parse-returns-any",
292
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
293
- "line": 220,
294
- "verdict": "true",
295
- "class": null,
296
- "why": null,
297
- "evidence": "const geom = JSON.parse(this.props.record.data[this.props.name]);"
298
- },
299
- {
300
- "rule": "javascript.number-parsing-is-lossy",
301
- "path": "OpenSPP2-19.0/spp_import_match/static/src/import_match_selector/import_match_selector.js",
302
- "line": 85,
303
- "verdict": "true",
304
- "class": null,
305
- "why": null,
306
- "evidence": "const matchId = parseInt(ev.target.value, 10) || false;"
307
- },
308
- {
309
- "rule": "javascript.loose-equality-and-falsy-defaults",
310
- "path": "OpenSPP2-19.0/spp_pii_encryption/static/src/js/masked_field.js",
311
- "line": 117,
312
- "verdict": "false",
313
- "class": "string-default",
314
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
315
- "evidence": "return result || \"••••••••\";"
316
- },
317
- {
318
- "rule": "javascript.loose-equality-and-falsy-defaults",
319
- "path": "OpenSPP2-19.0/spp_programs/static/src/js/create_program.js",
320
- "line": 18,
321
- "verdict": "false",
322
- "class": "string-default",
323
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
324
- "evidence": "const programName = action.params?.name || \"Program\";"
325
- },
326
- {
327
- "rule": "javascript.loose-equality-and-falsy-defaults",
328
- "path": "OpenSPP2-19.0/spp_registry/static/src/js/custom_client_action.js",
329
- "line": 15,
330
- "verdict": "false",
331
- "class": "string-default",
332
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
333
- "evidence": "type: params.type || \"info\","
334
- },
335
- {
336
- "rule": "javascript.loose-equality-and-falsy-defaults",
337
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
338
- "line": 89,
339
- "verdict": "false",
340
- "class": "string-default",
341
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
342
- "evidence": "this.state.searchMode = config.search_mode || \"unified\";"
343
- },
344
- {
345
- "rule": "javascript.loose-equality-and-falsy-defaults",
346
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
347
- "line": 90,
348
- "verdict": "false",
349
- "class": "string-default",
350
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
351
- "evidence": "this.state.searchField = config.target_field || \"name\";"
352
- },
353
- {
354
- "rule": "javascript.loose-equality-and-falsy-defaults",
355
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
356
- "line": 91,
357
- "verdict": "true",
358
- "class": null,
359
- "why": null,
360
- "evidence": "this.resultLimit = config.result_limit || 50;"
361
- },
362
- {
363
- "rule": "javascript.loose-equality-and-falsy-defaults",
364
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
365
- "line": 92,
366
- "verdict": "true",
367
- "class": null,
368
- "why": null,
369
- "evidence": "this.minSearchChars = config.min_chars || 3;"
370
- },
371
- {
372
- "rule": "javascript.loose-equality-and-falsy-defaults",
373
- "path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js",
374
- "line": 289,
375
- "verdict": "false",
376
- "class": "string-default",
377
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
378
- "evidence": "const scope = this.state.scopeFilter || \"all\";"
379
- },
380
- {
381
- "rule": "javascript.loose-equality-and-falsy-defaults",
382
- "path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js",
383
- "line": 295,
384
- "verdict": "false",
385
- "class": "string-default",
386
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
387
- "evidence": "const applies = v.applies_to || \"both\";"
388
- },
389
- {
390
- "rule": "javascript.loose-equality-and-falsy-defaults",
391
- "path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js",
392
- "line": 313,
393
- "verdict": "false",
394
- "class": "string-default",
395
- "why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
396
- "evidence": "const applies = variable.applies_to || \"both\";"
397
- },
398
- {
399
- "rule": "javascript.json-parse-returns-any",
400
- "path": "server-tools/base_exception/static/src/js/base_exception.esm.js",
401
- "line": 25,
402
- "verdict": "true",
403
- "class": null,
404
- "why": null,
405
- "evidence": "const excData = JSON.parse(originalError.data.message);"
406
- }
407
- ]
408
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
classification-python-2026-09.json ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "generated_utc": "2026-09-16T07:59:24.115329+00:00",
3
+ "stack": "odoo",
4
+ "subject": "the 8 python.* rules composed into the Odoo stack",
5
+ "checkouts": {
6
+ "OpenSPP2-19.0": "cfa0dc8e",
7
+ "server-tools": "60cd54e5e",
8
+ "account-financial-tools": "e4c1b86aa"
9
+ },
10
+ "lines": {
11
+ "OpenSPP2-19.0": 226937,
12
+ "OCA": 33960
13
+ },
14
+ "totals": {
15
+ "true": 26,
16
+ "false": 24
17
+ },
18
+ "by_rule": {
19
+ "python.assert-is-not-validation": {
20
+ "true": 6,
21
+ "false": 4
22
+ },
23
+ "python.decimal-for-money-not-float": {
24
+ "true": 0,
25
+ "false": 3
26
+ },
27
+ "python.eval-and-pickle-execute-their-input": {
28
+ "true": 0,
29
+ "false": 1
30
+ },
31
+ "python.naive-and-aware-datetimes-do-not-compare": {
32
+ "true": 20,
33
+ "false": 12
34
+ },
35
+ "python.path-join-does-not-contain-a-path": {
36
+ "true": 0,
37
+ "false": 4
38
+ }
39
+ },
40
+ "classes": {
41
+ "validates-caller-data": 6,
42
+ "internal-invariant": 3,
43
+ "odoo-monetary-is-float": 3,
44
+ "matched-inside-a-docstring": 2,
45
+ "display-only": 8,
46
+ "naive-utc-across-an-api-boundary": 2,
47
+ "local-now-against-an-orm-datetime": 5,
48
+ "local-now-into-an-orm-datetime": 9,
49
+ "local-date-as-an-api-default": 2,
50
+ "self-consistent-utc": 2,
51
+ "local-time-labelled-as-utc": 1,
52
+ "no-user-supplied-component": 4,
53
+ "re-enforced-on-the-next-line": 1,
54
+ "local-date-against-an-orm-date": 1,
55
+ "self-consistent-local": 1
56
+ },
57
+ "hits": [
58
+ {
59
+ "rule": "python.assert-is-not-validation",
60
+ "tree": "OpenSPP2-19.0",
61
+ "path": "endpoint_route_handler/registry.py",
62
+ "line": 314,
63
+ "verdict": "true",
64
+ "class": "validates-caller-data",
65
+ "why": "options come back off a jsonb row in from_row(), so this asserts about stored data rather than a program invariant; under -O a malformed handler dict is accepted silently",
66
+ "evidence": "assert \"klass_dotted_path\" in value[\"handler\"]"
67
+ },
68
+ {
69
+ "rule": "python.assert-is-not-validation",
70
+ "tree": "OpenSPP2-19.0",
71
+ "path": "endpoint_route_handler/registry.py",
72
+ "line": 315,
73
+ "verdict": "true",
74
+ "class": "validates-caller-data",
75
+ "why": "same setter, same stored data, second key",
76
+ "evidence": "assert \"method_name\" in value[\"handler\"]"
77
+ },
78
+ {
79
+ "rule": "python.assert-is-not-validation",
80
+ "tree": "OpenSPP2-19.0",
81
+ "path": "spp_registry/models/reg_relationship.py",
82
+ "line": 165,
83
+ "verdict": "false",
84
+ "class": "internal-invariant",
85
+ "why": "_check_partner is called from two @api.constrains with the literal 'source' and 'destination'; the assert is impossible unless the module itself is broken, which is what assert is for",
86
+ "evidence": "assert side in [\"source\", \"destination\"]"
87
+ },
88
+ {
89
+ "rule": "python.decimal-for-money-not-float",
90
+ "tree": "OpenSPP2-19.0",
91
+ "path": "spp_programs/models/cel/entitlement_amount_cel.py",
92
+ "line": 300,
93
+ "verdict": "false",
94
+ "class": "odoo-monetary-is-float",
95
+ "why": "self.amount is a Monetary field, which the ORM defines AS a float; the rule's remedy (Decimal) is not the Odoo idiom and float_round/float_compare are",
96
+ "evidence": "\"base_amount\": float(self.amount or 0.0),"
97
+ },
98
+ {
99
+ "rule": "python.decimal-for-money-not-float",
100
+ "tree": "OpenSPP2-19.0",
101
+ "path": "spp_programs/models/cel/entitlement_amount_cel.py",
102
+ "line": 316,
103
+ "verdict": "false",
104
+ "class": "odoo-monetary-is-float",
105
+ "why": "the CEL result is coerced for a Monetary field; same reason",
106
+ "evidence": "amount = float(result)"
107
+ },
108
+ {
109
+ "rule": "python.eval-and-pickle-execute-their-input",
110
+ "tree": "OpenSPP2-19.0",
111
+ "path": "spp_cel_domain/models/cel_service.py",
112
+ "line": 417,
113
+ "verdict": "false",
114
+ "class": "matched-inside-a-docstring",
115
+ "why": "the text is prose in a method docstring -- 'more robust and secure than regex-based conversion to Python eval()' -- and there is no eval call here at all",
116
+ "evidence": "and secure than regex-based conversion to Python eval()."
117
+ },
118
+ {
119
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
120
+ "tree": "OpenSPP2-19.0",
121
+ "path": "scripts/compliance/checker.py",
122
+ "line": 745,
123
+ "verdict": "false",
124
+ "class": "display-only",
125
+ "why": "a Generated: line in a markdown report; formatted once, never compared",
126
+ "evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\","
127
+ },
128
+ {
129
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
130
+ "tree": "OpenSPP2-19.0",
131
+ "path": "scripts/compliance/checker.py",
132
+ "line": 813,
133
+ "verdict": "false",
134
+ "class": "display-only",
135
+ "why": "the same timestamp in the JSON version of that report",
136
+ "evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),"
137
+ },
138
+ {
139
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
140
+ "tree": "OpenSPP2-19.0",
141
+ "path": "spp_analytics/models/service_aggregation.py",
142
+ "line": 97,
143
+ "verdict": "false",
144
+ "class": "display-only",
145
+ "why": "computed_at in a result dict that is returned, not stored or compared",
146
+ "evidence": "\"computed_at\": datetime.now().isoformat(),"
147
+ },
148
+ {
149
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
150
+ "tree": "OpenSPP2-19.0",
151
+ "path": "spp_api_v2/routers/consent.py",
152
+ "line": 178,
153
+ "verdict": "true",
154
+ "class": "naive-utc-across-an-api-boundary",
155
+ "why": "datetime.utcnow() is naive, is returned in a ConsentRevokeResponse, and serialises with no offset, so the consumer cannot tell the zone; deprecated in 3.12 for this reason",
156
+ "evidence": "revoked_at=datetime.utcnow(),"
157
+ },
158
+ {
159
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
160
+ "tree": "OpenSPP2-19.0",
161
+ "path": "spp_api_v2/routers/consent.py",
162
+ "line": 270,
163
+ "verdict": "true",
164
+ "class": "naive-utc-across-an-api-boundary",
165
+ "why": "the consent receipt timestamp, same construct. SUPPRESSED, and wrongly: the marker inside that return statement reads 'nosemgrep: odoo-sudo-without-context' and is about a different rule",
166
+ "evidence": "timestamp=datetime.utcnow(),",
167
+ "suppressed": true
168
+ },
169
+ {
170
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
171
+ "tree": "OpenSPP2-19.0",
172
+ "path": "spp_api_v2/routers/oauth.py",
173
+ "line": 217,
174
+ "verdict": "true",
175
+ "class": "naive-utc-across-an-api-boundary",
176
+ "why": "utcnow() feeds a JWT exp and iat; the lifetime is correct only because every value in the payload comes from the same naive call, and a later switch to datetime.now() would shift every token by the server's offset with nothing failing",
177
+ "evidence": "now = datetime.utcnow()"
178
+ },
179
+ {
180
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
181
+ "tree": "OpenSPP2-19.0",
182
+ "path": "spp_api_v2/services/group_service.py",
183
+ "line": 142,
184
+ "verdict": "true",
185
+ "class": "local-now-against-an-orm-datetime",
186
+ "why": "m.ended_date > now, where ended_date is an Odoo Datetime (naive UTC) and now is naive LOCAL -- membership filtering is off by the server's offset",
187
+ "evidence": "now = datetime.now()"
188
+ },
189
+ {
190
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
191
+ "tree": "OpenSPP2-19.0",
192
+ "path": "spp_api_v2/services/group_service.py",
193
+ "line": 748,
194
+ "verdict": "true",
195
+ "class": "local-now-into-an-orm-datetime",
196
+ "why": "datetime.now() is written straight into ended_date",
197
+ "evidence": "ended_datetime = datetime.now()"
198
+ },
199
+ {
200
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
201
+ "tree": "OpenSPP2-19.0",
202
+ "path": "spp_api_v2/services/group_service.py",
203
+ "line": 886,
204
+ "verdict": "true",
205
+ "class": "local-now-into-an-orm-datetime",
206
+ "why": "same write, in the group-merge path",
207
+ "evidence": "now = datetime.now()"
208
+ },
209
+ {
210
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
211
+ "tree": "OpenSPP2-19.0",
212
+ "path": "spp_api_v2/services/group_service.py",
213
+ "line": 1100,
214
+ "verdict": "true",
215
+ "class": "local-now-into-an-orm-datetime",
216
+ "why": "same write, moving members to a new group",
217
+ "evidence": "now = datetime.now()"
218
+ },
219
+ {
220
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
221
+ "tree": "OpenSPP2-19.0",
222
+ "path": "spp_attendance/controllers/controllers.py",
223
+ "line": 198,
224
+ "verdict": "true",
225
+ "class": "local-date-as-an-api-default",
226
+ "why": "the local date is the default for submitted_datetime on a controller endpoint; within the offset of midnight the attendance record carries the wrong day, which is the class OpenSPP has already fixed twice",
227
+ "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")"
228
+ },
229
+ {
230
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
231
+ "tree": "OpenSPP2-19.0",
232
+ "path": "spp_attendance/controllers/controllers.py",
233
+ "line": 351,
234
+ "verdict": "true",
235
+ "class": "local-date-as-an-api-default",
236
+ "why": "the same default on the second endpoint",
237
+ "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")"
238
+ },
239
+ {
240
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
241
+ "tree": "OpenSPP2-19.0",
242
+ "path": "spp_audit/models/spp_audit_backend.py",
243
+ "line": 198,
244
+ "verdict": "false",
245
+ "class": "self-consistent-utc",
246
+ "why": "utcnow() names an audit file and the rotation check compares it against a value from the same call; nothing aware is ever involved",
247
+ "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")"
248
+ },
249
+ {
250
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
251
+ "tree": "OpenSPP2-19.0",
252
+ "path": "spp_audit/models/spp_audit_backend.py",
253
+ "line": 216,
254
+ "verdict": "false",
255
+ "class": "self-consistent-utc",
256
+ "why": "the same value, in the rotation check itself",
257
+ "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")"
258
+ },
259
+ {
260
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
261
+ "tree": "OpenSPP2-19.0",
262
+ "path": "spp_cel_domain/services/cel_functions.py",
263
+ "line": 92,
264
+ "verdict": "true",
265
+ "class": "local-now-against-an-orm-datetime",
266
+ "why": "now() is exposed to CEL expressions that compare it against record datetimes read from the ORM, which are naive UTC",
267
+ "evidence": "return datetime.now()"
268
+ },
269
+ {
270
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
271
+ "tree": "OpenSPP2-19.0",
272
+ "path": "spp_cel_domain/services/cel_parser.py",
273
+ "line": 757,
274
+ "verdict": "false",
275
+ "class": "matched-inside-a-docstring",
276
+ "why": "a doctest example inside the docstring of hours_since, not code",
277
+ "evidence": ">>> two_hours_ago = datetime.now() - timedelta(hours=2)"
278
+ },
279
+ {
280
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
281
+ "tree": "OpenSPP2-19.0",
282
+ "path": "spp_cel_domain/services/cel_parser.py",
283
+ "line": 766,
284
+ "verdict": "true",
285
+ "class": "local-now-against-an-orm-datetime",
286
+ "why": "datetime.now() - datetime_value, where datetime_value arrives from an ORM field; hours_since is wrong by the offset and is used in eligibility expressions",
287
+ "evidence": "delta = datetime.now() - datetime_value"
288
+ },
289
+ {
290
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
291
+ "tree": "OpenSPP2-19.0",
292
+ "path": "spp_dci_server/middleware/rate_limit.py",
293
+ "line": 52,
294
+ "verdict": "true",
295
+ "class": "local-now-against-an-orm-datetime",
296
+ "why": "the rate limiter compares now against sender.last_request_minute, an ORM Datetime",
297
+ "evidence": "now = datetime.now()"
298
+ },
299
+ {
300
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
301
+ "tree": "OpenSPP2-19.0",
302
+ "path": "spp_encryption/models/encryption_provider.py",
303
+ "line": 110,
304
+ "verdict": "true",
305
+ "class": "local-time-labelled-as-utc",
306
+ "why": "f\"{datetime.now().isoformat(...)}Z\" appends a literal Z, which asserts UTC, to a LOCAL time, inside the created field of a linked-data cryptographic proof",
307
+ "evidence": "curr_datetime = f\"{datetime.now().isoformat(timespec='milliseconds')}Z\""
308
+ },
309
+ {
310
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
311
+ "tree": "OpenSPP2-19.0",
312
+ "path": "spp_farmer_registry_demo/models/seeded_farm_generator.py",
313
+ "line": 1127,
314
+ "verdict": "true",
315
+ "class": "local-now-into-an-orm-datetime",
316
+ "why": "written by raw SQL into spp_program_membership.enrollment_date, which the ORM will read back as UTC",
317
+ "evidence": "enrollment_dt = datetime.datetime.now()"
318
+ },
319
+ {
320
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
321
+ "tree": "OpenSPP2-19.0",
322
+ "path": "spp_grm_demo/models/generate_tickets.py",
323
+ "line": 922,
324
+ "verdict": "true",
325
+ "class": "local-now-into-an-orm-datetime",
326
+ "why": "_random_date_in_range builds demo ticket dates from local now and they are stored on records",
327
+ "evidence": "now = datetime.now()"
328
+ },
329
+ {
330
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
331
+ "tree": "OpenSPP2-19.0",
332
+ "path": "spp_mis_demo_v2/models/mis_demo_generator.py",
333
+ "line": 3388,
334
+ "verdict": "false",
335
+ "class": "display-only",
336
+ "why": "strftime('%Y-%m') builds a period label",
337
+ "evidence": "current_period = datetime.datetime.now().strftime(\"%Y-%m\")"
338
+ },
339
+ {
340
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
341
+ "tree": "OpenSPP2-19.0",
342
+ "path": "spp_mis_demo_v2/models/mis_demo_generator.py",
343
+ "line": 3390,
344
+ "verdict": "false",
345
+ "class": "display-only",
346
+ "why": "the same label for the previous three months",
347
+ "evidence": "(datetime.datetime.now() - datetime.timedelta(days=30 * i)).strftime(\"%Y-%m\")"
348
+ },
349
+ {
350
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
351
+ "tree": "OpenSPP2-19.0",
352
+ "path": "spp_mis_demo_v2/models/mis_demo_generator.py",
353
+ "line": 3740,
354
+ "verdict": "true",
355
+ "class": "local-now-into-an-orm-datetime",
356
+ "why": "now and expires_at go onto a credential record and the expiry is compared against the current time later",
357
+ "evidence": "now = datetime.datetime.now()"
358
+ },
359
+ {
360
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
361
+ "tree": "OpenSPP2-19.0",
362
+ "path": "spp_mis_demo_v2/models/seeded_volume_generator.py",
363
+ "line": 406,
364
+ "verdict": "true",
365
+ "class": "local-now-into-an-orm-datetime",
366
+ "why": "the same raw-SQL enrollment_date write in the volume generator",
367
+ "evidence": "enrollment_dt = datetime.datetime.now()"
368
+ },
369
+ {
370
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
371
+ "tree": "OpenSPP2-19.0",
372
+ "path": "spp_programs/models/managers/program_manager.py",
373
+ "line": 141,
374
+ "verdict": "true",
375
+ "class": "local-now-into-an-orm-datetime",
376
+ "why": "datetime.now() becomes a cycle start date on spp.cycle",
377
+ "evidence": "new_cycle = cm.new_cycle(\"Cycle 1\", datetime.now(), 1)"
378
+ },
379
+ {
380
+ "rule": "python.path-join-does-not-contain-a-path",
381
+ "tree": "OpenSPP2-19.0",
382
+ "path": "spp_drims_sl_demo/wizard/drims_demo_generator.py",
383
+ "line": 486,
384
+ "verdict": "false",
385
+ "class": "no-user-supplied-component",
386
+ "why": "file_config['filename'] comes from a list of literals declared in the same function; there is nothing here a caller can point out of the directory",
387
+ "evidence": "file_path = os.path.join(data_path, file_config[\"filename\"])"
388
+ },
389
+ {
390
+ "rule": "python.assert-is-not-validation",
391
+ "tree": "OCA",
392
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py",
393
+ "line": 265,
394
+ "verdict": "true",
395
+ "class": "validates-caller-data",
396
+ "why": "confirm_order() is a public method reachable over XML-RPC; under -O the operation-type guard is gone and the wrong record is confirmed",
397
+ "evidence": "assert self.operation_type == \"order\", \"Wrong operation type\""
398
+ },
399
+ {
400
+ "rule": "python.assert-is-not-validation",
401
+ "tree": "OCA",
402
+ "path": "account-financial-tools/account_cash_deposit/wizards/account_cash_order_reception.py",
403
+ "line": 27,
404
+ "verdict": "true",
405
+ "class": "validates-caller-data",
406
+ "why": "the assert is about self._context, which the client supplies; under -O default_get proceeds with whatever active_model it was given",
407
+ "evidence": "assert self._context.get(\"active_model\") == \"account.cash.deposit\""
408
+ },
409
+ {
410
+ "rule": "python.assert-is-not-validation",
411
+ "tree": "OCA",
412
+ "path": "account-financial-tools/account_dashboard_banner/models/account_dashboard_banner_cell.py",
413
+ "line": 289,
414
+ "verdict": "false",
415
+ "class": "internal-invariant",
416
+ "why": "sign is set by this module a few lines up and can only be 1 or -1 unless the module is broken",
417
+ "evidence": "assert sign in (1, -1)"
418
+ },
419
+ {
420
+ "rule": "python.assert-is-not-validation",
421
+ "tree": "OCA",
422
+ "path": "account-financial-tools/account_move_template/wizard/account_move_template_run.py",
423
+ "line": 117,
424
+ "verdict": "true",
425
+ "class": "validates-caller-data",
426
+ "why": "self.overwrite is a text field a user types; the assert is the type check on literal_eval's result, and the except clause catches AssertionError explicitly -- this IS the validation, and under -O it disappears and .keys() raises AttributeError instead of the ValidationError the code means to raise",
427
+ "evidence": "assert isinstance(overwrite_vals, dict)"
428
+ },
429
+ {
430
+ "rule": "python.assert-is-not-validation",
431
+ "tree": "OCA",
432
+ "path": "server-tools/base_time_parameter/models/base.py",
433
+ "line": 19,
434
+ "verdict": "true",
435
+ "class": "validates-caller-data",
436
+ "why": "get_time_parameter is public and date arrives from the caller, possibly resolved off a field by name; under -O a wrong type reaches the search",
437
+ "evidence": "assert type(date) is datetime.date or date is None, \"Wrong date\""
438
+ },
439
+ {
440
+ "rule": "python.assert-is-not-validation",
441
+ "tree": "OCA",
442
+ "path": "server-tools/jsonifier/models/ir_exports_resolver.py",
443
+ "line": 42,
444
+ "verdict": "false",
445
+ "class": "re-enforced-on-the-next-line",
446
+ "why": "the assert guards a zip(..., strict=True) one line below, which raises ValueError on the same mismatch; removing the assert changes the exception type and nothing else",
447
+ "evidence": "assert len(param) == len(records)"
448
+ },
449
+ {
450
+ "rule": "python.assert-is-not-validation",
451
+ "tree": "OCA",
452
+ "path": "server-tools/jsonifier/models/utils.py",
453
+ "line": 3,
454
+ "verdict": "false",
455
+ "class": "internal-invariant",
456
+ "why": "a module-private helper's contract with its only callers",
457
+ "evidence": "assert isinstance(parser, list)"
458
+ },
459
+ {
460
+ "rule": "python.decimal-for-money-not-float",
461
+ "tree": "OCA",
462
+ "path": "account-financial-tools/account_payroll_sheet_import/wizard/payroll_import_wizard.py",
463
+ "line": 110,
464
+ "verdict": "false",
465
+ "class": "odoo-monetary-is-float",
466
+ "why": "a spreadsheet cell becomes an accounting amount on a Monetary field, which the ORM defines as float",
467
+ "evidence": "amount = float(raw_value)"
468
+ },
469
+ {
470
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
471
+ "tree": "OCA",
472
+ "path": "account-financial-tools/account_fiscal_year_auto_create/models/account_fiscal_year.py",
473
+ "line": 23,
474
+ "verdict": "true",
475
+ "class": "local-date-against-an-orm-date",
476
+ "why": "last_fiscal_year.date_to < datetime.now().date() in a cron: the ORM Date is UTC-based and now().date() is the server's LOCAL date, so the fiscal year is created on the wrong side of midnight on any non-UTC server. fields.Date.context_today is the idiom",
477
+ "evidence": "last_fiscal_year.date_to < datetime.now().date() + relativedelta(days=1)"
478
+ },
479
+ {
480
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
481
+ "tree": "OCA",
482
+ "path": "account-financial-tools/purchase_unreconciled/models/account_move_line.py",
483
+ "line": 45,
484
+ "verdict": "true",
485
+ "class": "local-now-into-an-orm-datetime",
486
+ "why": "the default accounting date for an automatic writeoff move",
487
+ "evidence": "move_date = writeoff_vals.get(\"date\", datetime.now())"
488
+ },
489
+ {
490
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
491
+ "tree": "OCA",
492
+ "path": "server-tools/auditlog/models/autovacuum.py",
493
+ "line": 25,
494
+ "verdict": "true",
495
+ "class": "local-now-against-an-orm-datetime",
496
+ "why": "deadline is built from local now and compared against create_date, which is UTC; the autovacuum cron deletes logs early or late by the server's offset",
497
+ "evidence": "deadline = datetime.now() - timedelta(days=days)"
498
+ },
499
+ {
500
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
501
+ "tree": "OCA",
502
+ "path": "server-tools/auto_backup/models/db_backup.py",
503
+ "line": 146,
504
+ "verdict": "false",
505
+ "class": "display-only",
506
+ "why": "the timestamp names a backup file",
507
+ "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)"
508
+ },
509
+ {
510
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
511
+ "tree": "OCA",
512
+ "path": "server-tools/auto_backup/models/db_backup.py",
513
+ "line": 171,
514
+ "verdict": "false",
515
+ "class": "display-only",
516
+ "why": "the same, on the sftp path",
517
+ "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)"
518
+ },
519
+ {
520
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
521
+ "tree": "OCA",
522
+ "path": "server-tools/auto_backup/models/db_backup.py",
523
+ "line": 218,
524
+ "verdict": "false",
525
+ "class": "self-consistent-local",
526
+ "why": "cleanup() builds the oldest filename from the same local clock the filenames were written with, so the two agree",
527
+ "evidence": "now = datetime.now()"
528
+ },
529
+ {
530
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
531
+ "tree": "OCA",
532
+ "path": "server-tools/excel_import_export/models/xlsx_export.py",
533
+ "line": 268,
534
+ "verdict": "false",
535
+ "class": "display-only",
536
+ "why": "a temp filename uniquifier",
537
+ "evidence": "stamp = dt.utcnow().strftime(\"%H%M%S%f\")[:-3]"
538
+ },
539
+ {
540
+ "rule": "python.path-join-does-not-contain-a-path",
541
+ "tree": "OCA",
542
+ "path": "server-tools/auto_backup/models/db_backup.py",
543
+ "line": 154,
544
+ "verdict": "false",
545
+ "class": "no-user-supplied-component",
546
+ "why": "rec.folder is configuration and filename is generated from a timestamp; neither is a path a caller can traverse with",
547
+ "evidence": "with open(os.path.join(rec.folder, filename), \"wb\") as destiny:"
548
+ },
549
+ {
550
+ "rule": "python.path-join-does-not-contain-a-path",
551
+ "tree": "OCA",
552
+ "path": "server-tools/auto_backup/models/db_backup.py",
553
+ "line": 186,
554
+ "verdict": "false",
555
+ "class": "no-user-supplied-component",
556
+ "why": "the same join on the sftp path",
557
+ "evidence": "os.path.join(rec.folder, filename), \"wb\""
558
+ },
559
+ {
560
+ "rule": "python.path-join-does-not-contain-a-path",
561
+ "tree": "OCA",
562
+ "path": "server-tools/upgrade_analysis/models/upgrade_analysis.py",
563
+ "line": 106,
564
+ "verdict": "false",
565
+ "class": "no-user-supplied-component",
566
+ "why": "full_path is built from get_module_path and filename is internal",
567
+ "evidence": "logfile = os.path.join(full_path, filename)"
568
+ }
569
+ ],
570
+ "note": "Every finding the composition ADDS to an Odoo review, read by hand in its own file. These 8 rules were previously unreachable from the odoo stack: selection was strict equality on the stack field, so an Odoo module got the 19 ORM rules and none of these.\n\nThe verdict test for the datetime rule, which is 32 of the 50: an Odoo Datetime field is naive UTC by contract, so a naive LOCAL datetime that reaches one -- written to it, compared against it, or handed to something that does -- is a true positive, and one that is only formatted for display or used to name a file is not. Note what that makes these: latent on a server clocked to UTC, wrong by the offset on any other. OpenSPP's own history carries two fixes of exactly this class, which is why it is graded true rather than theoretical.\n\nThree rules fired and were never right: decimal-for-money-not-float (0 of 3), path-join-does-not-contain-a-path (0 of 4), and eval-and-pickle-execute-their-input (0 of 1). The first has a reason rather than a sample size behind it -- Odoo's Monetary field IS a float, so the rule's remedy is not the Odoo idiom -- and the other two have four findings between them, which is not enough to conclude anything about a checker. All three are named in the README rather than quietly dropped.\n\nThree more never fired at all on either tree: mutable-default-argument, bare-except-catches-the-exit and subprocess-shell-true-is-injection. Silence is not a pass.\n\nTwo of the 50 are the same checker defect: the pattern scanner strips # comments before matching and does not strip docstrings, so `eval()` named in prose and a `datetime.now()` inside a doctest both read as code. That is a fault in scan_source, not in either rule.\n\nOne finding is suppressed and should not be. The consent-receipt timestamp sits inside a return statement that carries a `# nosemgrep: odoo-sudo-without-context` marker four lines below it, and suppression reads any marker in the statement without asking which rule it names. It is counted apart here, as fieldtest counts it."
571
+ }
corpus/build_corpus.py CHANGED
@@ -1,8 +1,15 @@
1
- """Generates the validation rule corpus JSON files from the definitions below.
2
 
3
  The corpus is the single source of truth. System prompts and the RAG index are
4
  both DERIVED from it (corpus.py prompt / corpus.py index) so they cannot drift.
5
 
 
 
 
 
 
 
 
6
  Edit the rules here, then: python build_corpus.py && python ../bin/corpus.py validate
7
  """
8
  import io
@@ -21,15 +28,16 @@ import evidence # noqa: E402 -- one hashing implementation, not two
21
  # must be one list, not one here and another in the validator.
22
  from corpus import ADVISORY_LIKELIHOODS, LIKELIHOOD_DEFAULT # noqa: E402
23
 
24
- # The upstream documentation version each stack's rules were read against.
 
 
 
25
  # Recorded rather than assumed: a rule verified on 18.0 is not thereby
26
  # verified on 16.0, and the context layer needs to be able to say so when it
27
  # meets a module pinned to another series.
28
  CHECKED_AGAINST = {
29
  "odoo": "18.0",
30
  "python": "3.12",
31
- "typescript": "5.x",
32
- "javascript": "ES2023",
33
  }
34
 
35
  ODOO = [
@@ -443,273 +451,6 @@ except UserError as exc:
443
  },
444
  ]
445
 
446
- TYPESCRIPT = [
447
- {
448
- "id": "typescript.types-are-erased-at-runtime",
449
- "severity": "critical",
450
- "title": "Static types guarantee nothing about external data",
451
- "rule": "Every value crossing a boundary must be parsed by a runtime schema. A type assertion on external data is a lie the compiler cannot catch.",
452
- "why": "Interfaces and type annotations are erased during compilation. Casting an API response with 'as User' produces zero runtime checks, so a changed or malformed payload propagates as undefined deep inside unrelated code, far from its origin.",
453
- "enforcement_layer": "runtime-parse",
454
- "bypassed_by": ["type assertions (as T)", "any", "unchecked JSON.parse"],
455
- "wrong": """const user = await res.json() as User;""",
456
- "correct": """const UserSchema = z.object({ id: z.number(), name: z.string() });
457
- type User = z.infer<typeof UserSchema>;
458
-
459
- const user = UserSchema.parse(await res.json());""",
460
- "tags": ["zod", "boundary", "type-erasure"],
461
- },
462
- {
463
- "id": "typescript.derive-type-from-schema",
464
- "severity": "high",
465
- "title": "Derive the type from the schema, never maintain both",
466
- "rule": "Write the schema first and derive the static type with z.infer. Never hand-write an interface alongside a schema for the same data.",
467
- "why": "Two hand-maintained definitions drift. The compiler cannot detect the drift because each is internally consistent, so the type says one thing while the parser enforces another and the mismatch only appears at runtime.",
468
- "enforcement_layer": "compile-time",
469
- "bypassed_by": [],
470
- "wrong": """interface Order { id: number; total: number; }
471
- const OrderSchema = z.object({ id: z.number(), total: z.number(), currency: z.string() });""",
472
- "correct": """const OrderSchema = z.object({
473
- id: z.number(),
474
- total: z.number(),
475
- currency: z.string(),
476
- });
477
- type Order = z.infer<typeof OrderSchema>;""",
478
- "tags": ["zod", "single-source-of-truth", "drift"],
479
- },
480
- {
481
- "id": "typescript.odoo-returns-false-for-every-empty-field",
482
- "severity": "critical",
483
- "title": "Odoo JSON-RPC returns false for every empty field",
484
- "rule": "Model every optional Odoo scalar as a union with literal false, normalised through one shared helper. This applies to Char, Text, Date, Datetime, Binary and Many2one alike, not only to relations.",
485
- "why": "Odoo returns boolean false for any empty value, never null, never an empty string. Typing an optional email as string().nullable() throws at the parse boundary the first time a partner has no email, which is ordinary production data rather than an edge case.",
486
- "enforcement_layer": "runtime-parse",
487
- "bypassed_by": [],
488
- "wrong": """const PartnerSchema = z.object({
489
- email: z.string().nullable(),
490
- parent_id: z.array(z.number()),
491
- });""",
492
- "correct": """const odooNullable = <T extends z.ZodTypeAny>(inner: T) =>
493
- z.union([inner, z.literal(false)]).transform((v) => (v === false ? null : v));
494
-
495
- const PartnerSchema = z.object({
496
- id: z.number().int(),
497
- name: z.string(),
498
- email: odooNullable(z.string().email()),
499
- parent_id: odooNullable(z.tuple([z.number().int(), z.string()])),
500
- });""",
501
- "tags": ["odoo", "json-rpc", "boundary", "zod"],
502
- # .nullable() on an Odoo scalar means the author forgot that empty comes back as false.
503
- "violation_pattern": r"z\.(?:string|number|date|coerce)[^\n]*\.nullable\(\)",
504
- "satisfied_pattern": r"odooNullable|z\.literal\(false\)",
505
- },
506
- {
507
- "id": "typescript.odoo-many2one-is-a-tuple",
508
- "severity": "high",
509
- "title": "A set Many2one is a two-element tuple, not an array of ids",
510
- "rule": "Model a populated Many2one as z.tuple([z.number(), z.string()]) holding id and display_name. Model an empty one as false.",
511
- "why": "Odoo read() returns [id, display_name] for a set Many2one. Typing it as an array of numbers parses the id but discards display_name and fails the moment the schema meets real data, because element one is a string.",
512
- "enforcement_layer": "runtime-parse",
513
- "bypassed_by": [],
514
- "wrong": """parent_id: z.array(z.number())""",
515
- "correct": """parent_id: odooNullable(z.tuple([z.number().int(), z.string()]))""",
516
- "tags": ["odoo", "json-rpc", "many2one"],
517
- },
518
- {
519
- "id": "typescript.odoo-read-returns-an-array",
520
- "severity": "medium",
521
- "title": "Odoo read returns an array even for one id",
522
- "rule": "Parse the response of read() with z.array(Schema) and index into it, rather than parsing it as a single object.",
523
- "why": "read() always returns a list of dictionaries. Parsing it directly against an object schema fails, and defensively coercing with Array.isArray hides whether the call actually returned the record or an empty list for a missing id.",
524
- "enforcement_layer": "runtime-parse",
525
- "bypassed_by": [],
526
- "wrong": """const partner = PartnerSchema.parse(await callOdoo('res.partner', 'read', [id]));""",
527
- "correct": """const rows = z.array(PartnerSchema).parse(
528
- await callOdoo('res.partner', 'read', [id]),
529
- );
530
- if (rows.length === 0) throw new Error(`res.partner ${id} not found or not readable`);
531
- const partner = rows[0];""",
532
- "tags": ["odoo", "json-rpc", "read"],
533
- },
534
- {
535
- "id": "typescript.unknown-not-any-at-boundaries",
536
- "severity": "high",
537
- "title": "Use unknown at boundaries, never any",
538
- "rule": "Type every value that arrives unparsed as unknown: the body of an incoming webhook, a request payload, a handler parameter, the result of JSON.parse, anything from an untyped import. Reserve any for nothing.",
539
- "why": "any silently disables checking for every expression derived from it, so one any on a webhook handler's payload removes type safety from the whole call chain downstream, and the compiler reports nothing. unknown refuses every use until the value has been parsed or narrowed, which is the behaviour that was wanted from the annotation in the first place.",
540
- "enforcement_layer": "compile-time",
541
- "bypassed_by": ["explicit any", "implicit any from untyped imports"],
542
- "wrong": """function handle(payload: any) { return payload.order.total; }""",
543
- "correct": """function handle(payload: unknown) {
544
- const { order } = WebhookSchema.parse(payload);
545
- return order.total;
546
- }""",
547
- "tags": ["any", "unknown", "boundary", "webhook", "payload", "request-body",
548
- "handler", "external-input"],
549
- },
550
- {
551
- "id": "typescript.safeparse-when-failure-is-expected",
552
- "severity": "medium",
553
- "title": "Choose parse or safeParse deliberately",
554
- "rule": "Use safeParse where a malformed value is expected and recoverable; use parse only where throwing is the correct response. State which and why.",
555
- "why": "parse throws a ZodError that an unprepared caller turns into a 500 and an opaque stack trace. safeParse returns a discriminated result carrying the field-level issues, which is what a user-facing error or a per-row import skip actually needs.",
556
- "enforcement_layer": "runtime-parse",
557
- "bypassed_by": [],
558
- "wrong": """const row = RowSchema.parse(csvRow); // aborts the whole import on row 4000""",
559
- "correct": """const result = RowSchema.safeParse(csvRow);
560
- if (!result.success) {
561
- errors.push({ line, issues: result.error.issues });
562
- continue;
563
- }
564
- const row = result.data;""",
565
- "tags": ["zod", "error-handling"],
566
- },
567
- {
568
- "id": "typescript.validate-env-at-startup",
569
- "severity": "high",
570
- "title": "Parse environment variables once at startup",
571
- "rule": "Validate the whole environment against a schema at process start and export the parsed object. Never read process.env deep inside application code.",
572
- "why": "process.env values are string or undefined and are unchecked. A missing variable read lazily inside a rarely-exercised branch fails in production at the worst moment, whereas a startup parse turns the same fault into a deployment that refuses to boot.",
573
- "enforcement_layer": "startup",
574
- "bypassed_by": [],
575
- "wrong": """const url = process.env.ODOO_URL; // string | undefined, used 40 files away""",
576
- "correct": """const EnvSchema = z.object({
577
- ODOO_URL: z.string().url(),
578
- ODOO_DB: z.string().min(1),
579
- PORT: z.coerce.number().int().positive().default(3000),
580
- });
581
- export const env = EnvSchema.parse(process.env);""",
582
- "tags": ["config", "startup", "fail-fast"],
583
- },
584
- {
585
- "id": "typescript.discriminated-union-for-variants",
586
- "severity": "medium",
587
- "title": "Use discriminatedUnion for tagged variant payloads",
588
- "rule": "When a payload has a literal tag field selecting its shape, use z.discriminatedUnion on that field rather than z.union.",
589
- "why": "A plain union tries every member and reports the failure of all of them, producing an unreadable error and slower parsing. discriminatedUnion selects the branch by tag, reports issues only for the intended shape, and narrows correctly in TypeScript afterwards.",
590
- "enforcement_layer": "runtime-parse",
591
- "bypassed_by": [],
592
- "wrong": """z.union([PaymentSucceeded, PaymentFailed, PaymentPending])""",
593
- "correct": """z.discriminatedUnion('status', [
594
- z.object({ status: z.literal('succeeded'), chargeId: z.string() }),
595
- z.object({ status: z.literal('failed'), reason: z.string() }),
596
- z.object({ status: z.literal('pending') }),
597
- ])""",
598
- "tags": ["zod", "unions", "narrowing"],
599
- },
600
- {
601
- "id": "typescript.brand-validated-primitives",
602
- "severity": "medium",
603
- "title": "Brand validated primitives so they cannot be swapped for raw ones",
604
- "rule": "Give checked primitives a branded type so a validated value is not assignable from an arbitrary string or number.",
605
- "why": "Without branding, every string is assignable to every string-typed parameter, so a raw user input can be passed where a validated email or a trusted id is required and the compiler accepts it. Branding makes that a compile error.",
606
- "enforcement_layer": "compile-time",
607
- "bypassed_by": ["casting through as"],
608
- "wrong": """function send(to: string) { /* any string at all */ }""",
609
- "correct": """const Email = z.string().email().brand<'Email'>();
610
- type Email = z.infer<typeof Email>;
611
-
612
- function send(to: Email) { /* only a parsed Email */ }""",
613
- "tags": ["branding", "nominal-typing"],
614
- },
615
- ]
616
-
617
- JAVASCRIPT = [
618
- {
619
- "id": "javascript.json-parse-returns-any",
620
- "severity": "critical",
621
- "title": "JSON.parse output is unchecked",
622
- "rule": "Never use the result of JSON.parse directly. Pass it through a schema or an explicit shape check first.",
623
- "why": "JSON.parse succeeds for any syntactically valid JSON, including null, a bare number, or an object missing every field you expect. Property access then yields undefined, which propagates through arithmetic as NaN and through templates as the literal string undefined.",
624
- "enforcement_layer": "runtime-parse",
625
- "bypassed_by": [],
626
- "wrong": """const cfg = JSON.parse(raw);
627
- startServer(cfg.port);""",
628
- "correct": """const cfg = ConfigSchema.parse(JSON.parse(raw));
629
- startServer(cfg.port);""",
630
- "tags": ["json", "boundary"],
631
- },
632
- {
633
- "id": "javascript.loose-equality-and-falsy-defaults",
634
- "severity": "high",
635
- "title": "Falsy defaults discard legitimate zero and empty string",
636
- "rule": "Use the nullish coalescing operator for defaults and strict equality for comparisons. Reserve the logical OR operator for genuine boolean logic.",
637
- "why": "The OR operator treats 0, empty string and false as absent, so a quantity of 0 or an intentionally blank note is silently replaced by the default. This is the most common way a validated value is destroyed after it has passed validation.",
638
- "enforcement_layer": "runtime-parse",
639
- "bypassed_by": [],
640
- "wrong": """const qty = input.qty || 1; // qty of 0 becomes 1
641
- if (status == null) { } // also matches undefined by design, but == elsewhere surprises""",
642
- "correct": """const qty = input.qty ?? 1; // only null or undefined fall back
643
- if (status === null || status === undefined) { }""",
644
- "tags": ["coercion", "defaults", "silent-failure"],
645
- },
646
- {
647
- "id": "javascript.number-parsing-is-lossy",
648
- "severity": "high",
649
- "title": "Coercing user input to a number accepts nonsense",
650
- "rule": "Validate numeric input with an explicit check for finiteness rather than relying on Number, parseInt or the unary plus.",
651
- "why": "parseInt('12abc') returns 12, Number('') returns 0, and Number(' ') returns 0 as well. Each converts invalid input into a plausible number that passes every downstream check and corrupts totals rather than raising an error.",
652
- "enforcement_layer": "runtime-parse",
653
- "bypassed_by": [],
654
- "wrong": """const qty = parseInt(req.body.qty);""",
655
- "correct": """const qty = Number(req.body.qty);
656
- if (!Number.isFinite(qty) || !Number.isInteger(qty) || qty <= 0) {
657
- throw new Error(`qty must be a positive integer, received: ${req.body.qty}`);
658
- }""",
659
- "tags": ["numbers", "coercion", "input"],
660
- },
661
- {
662
- "id": "javascript.async-errors-need-explicit-handling",
663
- "severity": "critical",
664
- "title": "A rejected promise in a callback is not caught by the surrounding try",
665
- "rule": "Await every promise you create, or attach a catch to it. Never pass an async function where a synchronous callback is expected.",
666
- "why": "An async callback passed to forEach or to an event emitter returns a promise nobody holds. A rejection inside it escapes the enclosing try/catch entirely and becomes an unhandled rejection, which terminates the process on modern Node and silently skips work before that.",
667
- "enforcement_layer": "runtime-parse",
668
- "bypassed_by": [],
669
- "wrong": """rows.forEach(async (row) => {
670
- await save(row); // rejections escape, loop does not wait
671
- });""",
672
- "correct": """await Promise.all(rows.map((row) => save(row)));
673
- // or, when order and back-pressure matter:
674
- for (const row of rows) {
675
- await save(row);
676
- }""",
677
- "tags": ["async", "promises", "error-handling"],
678
- },
679
- {
680
- "id": "javascript.object-injection-via-user-keys",
681
- "severity": "critical",
682
- "title": "Never index an object with an unvalidated user-supplied key",
683
- "rule": "Check a user-supplied key against an allowlist, or use a Map, before using it to read or write a property.",
684
- "why": "A key of __proto__ or constructor reaches the prototype chain. Writing through it pollutes every object in the process, which turns a validation gap into remote code execution in the worst case and into corrupted defaults everywhere in the mildest.",
685
- "enforcement_layer": "runtime-parse",
686
- "bypassed_by": [],
687
- "wrong": """target[req.body.key] = req.body.value;""",
688
- "correct": """const ALLOWED = new Set(['name', 'email', 'phone']);
689
- if (!ALLOWED.has(req.body.key)) {
690
- throw new Error(`Unsupported field: ${req.body.key}`);
691
- }
692
- target[req.body.key] = req.body.value;""",
693
- "tags": ["security", "prototype-pollution", "input"],
694
- },
695
- {
696
- "id": "javascript.date-parsing-is-implementation-defined",
697
- "severity": "medium",
698
- "title": "The Date constructor accepts ambiguous strings",
699
- "rule": "Parse dates from an explicit ISO-8601 string and reject anything that produces an invalid date. Never feed arbitrary user text to new Date.",
700
- "why": "new Date('01/02/2026') is January the second in one locale's reading and the second of January in another, and non-ISO formats are implementation-defined across engines. new Date('nonsense') returns an Invalid Date object rather than throwing, so the failure travels onward as NaN.",
701
- "enforcement_layer": "runtime-parse",
702
- "bypassed_by": [],
703
- "wrong": """const when = new Date(req.body.date);""",
704
- "correct": """const when = new Date(req.body.date);
705
- if (Number.isNaN(when.getTime())) {
706
- throw new Error(`Invalid date: ${req.body.date}`);
707
- }""",
708
- "tags": ["dates", "coercion", "locale"],
709
- },
710
- ]
711
-
712
-
713
  PYTHON = [
714
  {
715
  "id": "python.assert-is-not-validation",
@@ -869,8 +610,6 @@ ODOO_ONCHANGE = "https://www.odoo.com/documentation/18.0/developer/tutorials/ser
869
  # page does, and says the thing the rule says.
870
  ODOO_GUIDELINES = "https://www.odoo.com/documentation/18.0/contributing/development/coding_guidelines.html"
871
  PG_CREATE_TABLE = "https://www.postgresql.org/docs/current/ddl-constraints.html"
872
- # The Zod API reference, not the homepage. Four rules cited zod.dev/, which
873
- # supports nothing: a landing page is not provenance.
874
  PY_DOCS = "https://docs.python.org/3"
875
  PY_ASSERT = PY_DOCS + "/reference/simple_stmts.html#the-assert-statement"
876
  PY_DEFAULTS = PY_DOCS + "/tutorial/controlflow.html#default-argument-values"
@@ -881,11 +620,6 @@ PY_OSPATH = PY_DOCS + "/library/os.path.html#os.path.join"
881
  PY_DECIMAL = PY_DOCS + "/library/decimal.html"
882
  PY_DATETIME = PY_DOCS + "/library/datetime.html"
883
 
884
- ZOD = "https://zod.dev/api"
885
- TS_HANDBOOK = "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html"
886
- NODE_PROCESS = "https://nodejs.org/api/process.html"
887
- MDN = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference"
888
-
889
  ENRICH = {
890
  # ---- Odoo -------------------------------------------------------------
891
  # Both decorators are specified on the ORM reference page, which is where the
@@ -1038,53 +772,34 @@ ENRICH = {
1038
  "source": ODOO_ORM,
1039
  },
1040
 
1041
- # ---- TypeScript -------------------------------------------------------
1042
- "typescript.types-are-erased-at-runtime": {
1043
- "violation_pattern": r"\.json\(\)\s+as\s+\w+|as\s+unknown\s+as\s+\w+",
1044
- # Type erasure is a TypeScript fact. Zod is one answer to it, not the
1045
- # thing that documents it.
1046
- "source": TS_HANDBOOK,
1047
- },
1048
- "typescript.derive-type-from-schema": {
1049
- "violation_pattern": r"interface\s+\w+\s*\{[\s\S]*?\}[\s\S]*?z\.object\(",
1050
- "source": ZOD,
1051
- },
1052
- "typescript.odoo-returns-false-for-every-empty-field": {
1053
- "violation_pattern": r"z\.(?:string|number|date|coerce)[^\n]*\.nullable\(\)",
1054
- "satisfied_pattern": r"odooNullable|z\.literal\(false\)",
1055
- # What comes back over JSON-RPC is the External API's subject, not the
1056
- # ORM reference's.
1057
- "source": ODOO_EXTERNAL_API,
1058
- },
1059
- "typescript.odoo-many2one-is-a-tuple": {
1060
- "violation_pattern": r"_id\s*:\s*(?:\w+\()?z\.array\(\s*z\.number",
1061
- "source": ODOO_EXTERNAL_API,
1062
- },
1063
- "typescript.odoo-read-returns-an-array": {
1064
- # Must be an Odoo read() call specifically. Without this the pattern fired
1065
- # on any Schema.parse(await ...), including correct non-Odoo fetch parsing.
1066
- "violation_pattern": r"\w+Schema\.(?:safe)?[Pp]arse\(\s*await[^)]*(?:callOdoo|jsonrpc|'read'|\"read\")",
1067
- "satisfied_pattern": r"z\.array\(",
1068
- "source": ODOO_EXTERNAL_API,
1069
- },
1070
- "typescript.unknown-not-any-at-boundaries": {
1071
- "violation_pattern": r":\s*any\b",
1072
- "source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html",
1073
- },
1074
- "typescript.validate-env-at-startup": {
1075
- "violation_pattern": r"process\.env\.[A-Z][A-Z0-9_]*",
1076
- # The claim is about process.env being string | undefined, which is
1077
- # Node's documentation, not Zod's.
1078
- "source": NODE_PROCESS,
1079
- },
1080
- "typescript.discriminated-union-for-variants": {
1081
- "violation_pattern": r"z\.union\(\[\s*[A-Z]",
1082
- "satisfied_pattern": r"z\.discriminatedUnion",
1083
- "source": ZOD,
1084
- },
1085
-
1086
  # ---- Python -----------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
  "python.assert-is-not-validation": {
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  # Lookbehind, not a consumed newline. The match's start is what
1089
  # scan_source turns into a line number, so a pattern that begins
1090
  # by eating the PRECEDING newline reported the line above the
@@ -1093,24 +808,48 @@ ENRICH = {
1093
  "source": PY_ASSERT,
1094
  },
1095
  "python.mutable-default-argument": {
 
 
 
 
1096
  "violation_pattern": r"def\s+\w+\([^)]*=\s*(?:\[\]|\{\}|set\(\))",
1097
  "source": PY_DEFAULTS,
1098
  },
1099
  "python.bare-except-catches-the-exit": {
 
 
 
1100
  # A bare except and nothing else: `except (KeyError,) as exc:` has text
1101
  # between the keyword and the colon and is not matched.
1102
  "violation_pattern": r"except\s*:",
1103
  "source": PY_ERRORS,
1104
  },
1105
  "python.eval-and-pickle-execute-their-input": {
 
 
 
 
 
 
1106
  "violation_pattern": r"\beval\(|\bexec\(|pickle\.loads?\(",
1107
  "source": PY_PICKLE,
1108
  },
1109
  "python.subprocess-shell-true-is-injection": {
 
 
 
1110
  "violation_pattern": r"shell\s*=\s*True|os\.system\(|os\.popen\(",
1111
  "source": PY_SUBPROCESS,
1112
  },
1113
  "python.path-join-does-not-contain-a-path": {
 
 
 
 
 
 
 
 
1114
  # Word-anchored. Unanchored, `form` matched inside `.format(`, so
1115
  # every os.path.join(..., "x {0}".format(y)) in a codebase was a hit.
1116
  "violation_pattern": (r"os\.path\.join\([^)]*"
@@ -1123,6 +862,15 @@ ENRICH = {
1123
  "source": PY_OSPATH,
1124
  },
1125
  "python.decimal-for-money-not-float": {
 
 
 
 
 
 
 
 
 
1126
  # Money, not arithmetic. A bare \bfloat\( caught `float(successes) /
1127
  # total` inside a Wilson score interval, which is exactly what binary
1128
  # floating point is for. The name has to say it is an amount.
@@ -1134,51 +882,27 @@ ENRICH = {
1134
  "source": PY_DECIMAL,
1135
  },
1136
  "python.naive-and-aware-datetimes-do-not-compare": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1137
  # Empty parentheses specifically: datetime.now(timezone.utc) is the fix.
1138
  "violation_pattern": r"datetime\.now\(\s*\)|utcnow\(\s*\)",
1139
  "source": PY_DATETIME,
1140
  },
1141
 
1142
- # ---- JavaScript -------------------------------------------------------
1143
- "javascript.json-parse-returns-any": {
1144
- "violation_pattern": r"=\s*JSON\.parse\(",
1145
- "source": MDN + "/Global_Objects/JSON/parse",
1146
- },
1147
- "javascript.loose-equality-and-falsy-defaults": {
1148
- # A default that is ITSELF falsy destroys nothing: `x || 0` is 0
1149
- # when x is 0, and `x || ""` is "" when x is blank. The rule is
1150
- # about a legitimate value being REPLACED, and here there is no
1151
- # replacement to make. 31 of this rule's 69 hits on 9,641 lines of
1152
- # Odoo JS were that -- the largest single class, found the first
1153
- # time these seven rules were ever fired at code nobody wrote for
1154
- # them.
1155
- "violation_pattern": r"\w\s*\|\|\s*(?!0(?!\d|\.)|\"\"|'')(?:\d|['\"])",
1156
- "source": MDN + "/Operators/Nullish_coalescing",
1157
- },
1158
- "javascript.async-errors-need-explicit-handling": {
1159
- "violation_pattern": r"\.forEach\(\s*async",
1160
- "source": MDN + "/Statements/async_function",
1161
- },
1162
- "javascript.number-parsing-is-lossy": {
1163
- "violation_pattern": r"parseInt\(|parseFloat\(",
1164
- "satisfied_pattern": r"Number\.isFinite|Number\.isInteger",
1165
- "source": MDN + "/Global_Objects/parseInt",
1166
- },
1167
- "javascript.object-injection-via-user-keys": {
1168
- "violation_pattern": r"\w+\[\s*(?:req\.(?:body|query|params)|input|payload)\.\w+\s*\]\s*=",
1169
- # hasOwnProperty was dropped from this alternation deliberately. Bare
1170
- # obj.hasOwnProperty(key) is the prototype-unsafe spelling -- the method
1171
- # can be shadowed by the very key being probed -- and the token also
1172
- # matched any passing MENTION of it, including an answer arguing
1173
- # against its use. The two safe spellings are named explicitly instead.
1174
- "satisfied_pattern": r"ALLOWED|allowlist|\.has\(|Object\.hasOwn\(|Object\.prototype\.hasOwnProperty\.call|Object\.create\(null\)",
1175
- "source": MDN + "/Global_Objects/Object/proto",
1176
- },
1177
- "javascript.date-parsing-is-implementation-defined": {
1178
- "violation_pattern": r"new Date\(\s*(?:req|input|body|payload|row)\.",
1179
- "satisfied_pattern": r"Number\.isNaN|isNaN\(",
1180
- "source": MDN + "/Global_Objects/Date/Date",
1181
- },
1182
  }
1183
 
1184
  # ----------------------------------------------------------------- LIKELIHOOD
@@ -1273,8 +997,15 @@ PROBES = {
1273
  "comparing a price or a quantity to zero gives the wrong answer sometimes",
1274
  "odoo.constrains-iterate-recordset":
1275
  "expected singleton error when a batch import writes several records at once",
 
 
 
 
 
 
 
1276
  "odoo.prefer-sql-constraint":
1277
- "stop anyone entering a negative quantity on a sale order line",
1278
  "odoo.check-constraint-cannot-cross-tables":
1279
  "my CHECK constraint will not install because it reads the product stock",
1280
  "odoo.check-company-on-relations":
@@ -1302,28 +1033,6 @@ PROBES = {
1302
  "odoo.except-order-follows-the-hierarchy":
1303
  "my except AccessError block never runs and the API returns 409",
1304
 
1305
- # ---- TypeScript -------------------------------------------------------
1306
- "typescript.types-are-erased-at-runtime":
1307
- "cast the JSON from an API response to my interface",
1308
- "typescript.derive-type-from-schema":
1309
- "keep my interface and my validation schema from drifting apart",
1310
- "typescript.odoo-returns-false-for-every-empty-field":
1311
- "parsing fails because Odoo sent false for a partner with no email",
1312
- "typescript.odoo-many2one-is-a-tuple":
1313
- "read the parent company name off a many2one field from Odoo",
1314
- "typescript.odoo-read-returns-an-array":
1315
- "parse the result of an Odoo read call for a single id",
1316
- "typescript.unknown-not-any-at-boundaries":
1317
- "what type should the payload of an incoming webhook handler have",
1318
- "typescript.safeparse-when-failure-is-expected":
1319
- "skip the bad rows in a CSV import instead of aborting the whole file",
1320
- "typescript.validate-env-at-startup":
1321
- "make sure the required environment variables are set before the app runs",
1322
- "typescript.discriminated-union-for-variants":
1323
- "parse a webhook whose body shape depends on a status field",
1324
- "typescript.brand-validated-primitives":
1325
- "stop a raw string being passed where a checked email address is required",
1326
-
1327
  # ---- Python -----------------------------------------------------------
1328
  "python.assert-is-not-validation":
1329
  "check that an amount is positive before using it",
@@ -1342,19 +1051,6 @@ PROBES = {
1342
  "python.naive-and-aware-datetimes-do-not-compare":
1343
  "TypeError comparing offset-naive and offset-aware datetimes",
1344
 
1345
- # ---- JavaScript -------------------------------------------------------
1346
- "javascript.json-parse-returns-any":
1347
- "use the parsed JSON body of an incoming request",
1348
- "javascript.loose-equality-and-falsy-defaults":
1349
- "a quantity of zero keeps being replaced by the default value",
1350
- "javascript.number-parsing-is-lossy":
1351
- "turn a page number from the query string into a number",
1352
- "javascript.async-errors-need-explicit-handling":
1353
- "await inside forEach does not wait and the errors disappear",
1354
- "javascript.object-injection-via-user-keys":
1355
- "set a property on an object using a key taken from the request body",
1356
- "javascript.date-parsing-is-implementation-defined":
1357
- "turn a date string typed by a user into a Date object",
1358
  }
1359
 
1360
 
@@ -1381,23 +1077,10 @@ SEMANTIC_CHECK = {
1381
  }
1382
 
1383
 
1384
- # Rules with no violation_pattern still have provenance worth recording.
1385
- EXTRA_SOURCES = {
1386
- "typescript.safeparse-when-failure-is-expected": ZOD,
1387
- "typescript.brand-validated-primitives": ZOD,
1388
- }
1389
-
1390
-
1391
  NO_PATTERN_REASON = {
1392
  "odoo.schema-change-needs-migration":
1393
  "The violation lives in the diff between two module versions plus the presence of a "
1394
  "migrations/ directory. Nothing in a single generated snippet reveals it.",
1395
- "typescript.safeparse-when-failure-is-expected":
1396
- "Whether parse or safeParse is correct depends on whether the caller can recover, "
1397
- "which is context the snippet does not contain. Any pattern here would be noise.",
1398
- "typescript.brand-validated-primitives":
1399
- "Absence of branding is not detectable: plain string parameters are correct in most "
1400
- "code. Flagging them all would bury the real findings.",
1401
  }
1402
 
1403
 
@@ -1406,12 +1089,18 @@ def main():
1406
  os.makedirs(out, exist_ok=True)
1407
  total = 0
1408
  unknown = (set(ENRICH) | set(NO_PATTERN_REASON) | set(SEMANTIC_CHECK)
1409
- | set(PROBES) | set(EXTRA_SOURCES) | set(LIKELIHOOD))
1410
  unprobed = []
1411
  covered = 0
1412
  semantic = 0
1413
- for stack, rules in (("odoo", ODOO), ("python", PYTHON),
1414
- ("typescript", TYPESCRIPT), ("javascript", JAVASCRIPT)):
 
 
 
 
 
 
1415
  for r in rules:
1416
  r["stack"] = stack
1417
  r.setdefault("bypassed_by", [])
@@ -1422,8 +1111,6 @@ def main():
1422
  # deriving it costs a line.
1423
  r["likelihood"] = LIKELIHOOD.get(r["id"], LIKELIHOOD_DEFAULT)
1424
  r["advisory"] = r["likelihood"] in ADVISORY_LIKELIHOODS
1425
- if r["id"] in EXTRA_SOURCES:
1426
- r["source"] = EXTRA_SOURCES[r["id"]]
1427
  if r["id"] in PROBES:
1428
  r["probe"] = PROBES[r["id"]]
1429
  else:
@@ -1448,6 +1135,8 @@ def main():
1448
  json.dump(rules, fh, indent=2, ensure_ascii=False)
1449
  crit = sum(1 for r in rules if r["severity"] == "critical")
1450
  print(" {0:<12} {1:>2} rules ({2} critical) -> {3}.json".format(stack, len(rules), crit, stack))
 
 
1451
  total += len(rules)
1452
  checked = covered + semantic
1453
  print("\n {0} rules total, {1} machine-checkable ({2}%)".format(
 
1
+ """Generates the validation rule corpus JSON from the definitions below.
2
 
3
  The corpus is the single source of truth. System prompts and the RAG index are
4
  both DERIVED from it (corpus.py prompt / corpus.py index) so they cannot drift.
5
 
6
+ ONE corpus, one stack, and everything in it is for reviewing Odoo code. Two
7
+ lists feed it: ODOO, the ORM rules, and PYTHON, plain-Python rules that apply
8
+ because an Odoo module is a Python module. Both are written to odoo.json and
9
+ both carry stack "odoo". The `python.` id prefix is provenance -- where the
10
+ rule came from and which documentation check_sources verifies it against --
11
+ and not a second mode anybody can select. There is no second mode.
12
+
13
  Edit the rules here, then: python build_corpus.py && python ../bin/corpus.py validate
14
  """
15
  import io
 
28
  # must be one list, not one here and another in the validator.
29
  from corpus import ADVISORY_LIKELIHOODS, LIKELIHOOD_DEFAULT # noqa: E402
30
 
31
+ # The upstream documentation version each set of rules was read against. Two
32
+ # entries under one stack: the ORM rules are read against an Odoo series, the
33
+ # composed-in Python rules against a language version.
34
+ #
35
  # Recorded rather than assumed: a rule verified on 18.0 is not thereby
36
  # verified on 16.0, and the context layer needs to be able to say so when it
37
  # meets a module pinned to another series.
38
  CHECKED_AGAINST = {
39
  "odoo": "18.0",
40
  "python": "3.12",
 
 
41
  }
42
 
43
  ODOO = [
 
451
  },
452
  ]
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  PYTHON = [
455
  {
456
  "id": "python.assert-is-not-validation",
 
610
  # page does, and says the thing the rule says.
611
  ODOO_GUIDELINES = "https://www.odoo.com/documentation/18.0/contributing/development/coding_guidelines.html"
612
  PG_CREATE_TABLE = "https://www.postgresql.org/docs/current/ddl-constraints.html"
 
 
613
  PY_DOCS = "https://docs.python.org/3"
614
  PY_ASSERT = PY_DOCS + "/reference/simple_stmts.html#the-assert-statement"
615
  PY_DEFAULTS = PY_DOCS + "/tutorial/controlflow.html#default-argument-values"
 
620
  PY_DECIMAL = PY_DOCS + "/library/decimal.html"
621
  PY_DATETIME = PY_DOCS + "/library/datetime.html"
622
 
 
 
 
 
 
623
  ENRICH = {
624
  # ---- Odoo -------------------------------------------------------------
625
  # Both decorators are specified on the ORM reference page, which is where the
 
772
  "source": ODOO_ORM,
773
  },
774
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
  # ---- Python -----------------------------------------------------------
776
+ #
777
+ # These eight are selected for the Odoo stack, and were measured there for
778
+ # the first time on 2026-09-16: 50 findings across OpenSPP (226,937 lines)
779
+ # and OCA (33,960), every one read in its own file. 26 true, 24 false.
780
+ # Verdicts and reasons are in classification-python-2026-09.json; the
781
+ # per-rule result is written under each rule below, including the three
782
+ # that fired and were never right and the three that never fired at all.
783
+ #
784
+ # One test decides most of it, so it is stated once. An Odoo Datetime field
785
+ # is naive UTC by contract. A naive LOCAL datetime that reaches one --
786
+ # written to it, compared against it, or handed to something that does --
787
+ # is the defect. One that is only formatted for display or used to name a
788
+ # file is not, and eight of the twelve false positives are that.
789
  "python.assert-is-not-validation": {
790
+ # 6 true, 4 false on the two trees. True where the asserted value comes
791
+ # from outside the module -- a jsonb row, self._context, a public
792
+ # method's argument, a user-typed field parsed with literal_eval. False
793
+ # in three shapes, and they are worth naming because a narrowing would
794
+ # have to recognise them: an invariant the module's own callers cannot
795
+ # break (a helper called with two literals), a private helper's contract
796
+ # with its only caller, and an assert whose condition the NEXT line
797
+ # re-enforces anyway (a zip with strict=True).
798
+ #
799
+ # Not narrowed on that. Telling the three apart from the six needs to
800
+ # know where a value came from, which is the same assignment-tracing
801
+ # pass float-comparison is waiting on, and guessing at it from the
802
+ # variable's name is what made the sudo rule 5.8%.
803
  # Lookbehind, not a consumed newline. The match's start is what
804
  # scan_source turns into a line number, so a pattern that begins
805
  # by eating the PRECEDING newline reported the line above the
 
808
  "source": PY_ASSERT,
809
  },
810
  "python.mutable-default-argument": {
811
+ # Fired on neither tree. Silence is not a pass: it says these 260,000
812
+ # lines contain no mutable default, which is also what a broken checker
813
+ # says. ruff's B006 is selected in OpenSPP's own .ruff.toml, which is
814
+ # the more likely explanation.
815
  "violation_pattern": r"def\s+\w+\([^)]*=\s*(?:\[\]|\{\}|set\(\))",
816
  "source": PY_DEFAULTS,
817
  },
818
  "python.bare-except-catches-the-exit": {
819
+ # Fired on neither tree, and E722 is on by default in ruff, so a repo
820
+ # running ruff at all has none of these left to find.
821
+ #
822
  # A bare except and nothing else: `except (KeyError,) as exc:` has text
823
  # between the keyword and the colon and is not matched.
824
  "violation_pattern": r"except\s*:",
825
  "source": PY_ERRORS,
826
  },
827
  "python.eval-and-pickle-execute-their-input": {
828
+ # 0 true, 1 false. The one finding is the word eval() written in a
829
+ # method's DOCSTRING, in a sentence arguing against using it. That is
830
+ # not this rule's fault: scan_source strips # comments before matching
831
+ # and does not strip string literals, so prose in a docstring is
832
+ # matched as code. One sample decides nothing about the rule, and the
833
+ # checker defect it exposes is named in the README.
834
  "violation_pattern": r"\beval\(|\bexec\(|pickle\.loads?\(",
835
  "source": PY_PICKLE,
836
  },
837
  "python.subprocess-shell-true-is-injection": {
838
+ # Fired on neither tree. Worth keeping on that evidence rather than in
839
+ # spite of it: ruff's S602 covers it and neither repo selects S, so the
840
+ # silence here is a fact about the code and not about the coverage.
841
  "violation_pattern": r"shell\s*=\s*True|os\.system\(|os\.popen\(",
842
  "source": PY_SUBPROCESS,
843
  },
844
  "python.path-join-does-not-contain-a-path": {
845
+ # 0 true, 4 false. All four join a configured directory to a name this
846
+ # module generated -- a timestamped backup filename, a literal from a
847
+ # list declared in the same function, a module path from
848
+ # get_module_path. The name-matching is what fires: `filename` is in
849
+ # the alternation and says nothing about where the value came from.
850
+ # Four findings is not enough to conclude a checker is wrong, so it is
851
+ # reported here rather than acted on.
852
+ #
853
  # Word-anchored. Unanchored, `form` matched inside `.format(`, so
854
  # every os.path.join(..., "x {0}".format(y)) in a codebase was a hit.
855
  "violation_pattern": (r"os\.path\.join\([^)]*"
 
862
  "source": PY_OSPATH,
863
  },
864
  "python.decimal-for-money-not-float": {
865
+ # 0 true, 3 false, and this one has a reason behind it rather than a
866
+ # sample size. Odoo's Monetary field IS a float -- the ORM defines it
867
+ # that way -- and the framework's answer to the precision problem is
868
+ # float_round and float_compare against the currency's rounding, not
869
+ # Decimal. So inside an Odoo module the rule fires on the idiom and its
870
+ # remedy cannot be taken. That is an argument, not a measurement, and
871
+ # three findings do not settle it either way; it is written down in the
872
+ # README as the composition's open question rather than acted on here.
873
+ #
874
  # Money, not arithmetic. A bare \bfloat\( caught `float(successes) /
875
  # total` inside a Wilson score interval, which is exactly what binary
876
  # floating point is for. The name has to say it is an amount.
 
882
  "source": PY_DECIMAL,
883
  },
884
  "python.naive-and-aware-datetimes-do-not-compare": {
885
+ # 20 true, 12 false -- 32 of the 50 findings the composition adds, and
886
+ # the reason the composition is worth having at all. The true ones are
887
+ # a naive LOCAL datetime reaching an ORM Datetime: written to
888
+ # ended_date, compared against create_date in an autovacuum cron,
889
+ # subtracted from a field value in an eligibility expression, or
890
+ # labelled with a literal "Z" inside a cryptographic proof. On a server
891
+ # clocked to UTC every one of them is latent; on any other it is wrong
892
+ # by the offset, and OpenSPP's own history carries two fixes of exactly
893
+ # this class.
894
+ #
895
+ # The twelve false ones are one shape, near enough: a timestamp that is
896
+ # formatted and never compared -- a Generated: line in a report, a
897
+ # backup filename, a period label. A narrowing would have to see what
898
+ # happens to the value afterwards, which the regex cannot; two more are
899
+ # the docstring defect named above.
900
+ #
901
  # Empty parentheses specifically: datetime.now(timezone.utc) is the fix.
902
  "violation_pattern": r"datetime\.now\(\s*\)|utcnow\(\s*\)",
903
  "source": PY_DATETIME,
904
  },
905
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
906
  }
907
 
908
  # ----------------------------------------------------------------- LIKELIHOOD
 
997
  "comparing a price or a quantity to zero gives the wrong answer sometimes",
998
  "odoo.constrains-iterate-recordset":
999
  "expected singleton error when a batch import writes several records at once",
1000
+ # Reworded 2026-09-16, and re-measured. The old probe was "stop anyone
1001
+ # entering a negative quantity on a sale order line", which is the generic
1002
+ # validation question every constraint rule in the corpus answers -- with
1003
+ # 19 rules in the stack it still came back first, and with 27 it stopped
1004
+ # coming back at all. The rule's own subject is narrower than that: a
1005
+ # Python check that two concurrent transactions both pass. Nothing else
1006
+ # here is about a race.
1007
  "odoo.prefer-sql-constraint":
1008
+ "should this check be a database constraint or an api.constrains method",
1009
  "odoo.check-constraint-cannot-cross-tables":
1010
  "my CHECK constraint will not install because it reads the product stock",
1011
  "odoo.check-company-on-relations":
 
1033
  "odoo.except-order-follows-the-hierarchy":
1034
  "my except AccessError block never runs and the API returns 409",
1035
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  # ---- Python -----------------------------------------------------------
1037
  "python.assert-is-not-validation":
1038
  "check that an amount is positive before using it",
 
1051
  "python.naive-and-aware-datetimes-do-not-compare":
1052
  "TypeError comparing offset-naive and offset-aware datetimes",
1053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  }
1055
 
1056
 
 
1077
  }
1078
 
1079
 
 
 
 
 
 
 
 
1080
  NO_PATTERN_REASON = {
1081
  "odoo.schema-change-needs-migration":
1082
  "The violation lives in the diff between two module versions plus the presence of a "
1083
  "migrations/ directory. Nothing in a single generated snippet reveals it.",
 
 
 
 
 
 
1084
  }
1085
 
1086
 
 
1089
  os.makedirs(out, exist_ok=True)
1090
  total = 0
1091
  unknown = (set(ENRICH) | set(NO_PATTERN_REASON) | set(SEMANTIC_CHECK)
1092
+ | set(PROBES) | set(LIKELIHOOD))
1093
  unprobed = []
1094
  covered = 0
1095
  semantic = 0
1096
+ # One stack, and everything in it is written for reviewing Odoo code. The
1097
+ # `python.` rules are in it because an Odoo module IS Python: selection was
1098
+ # strict equality on this field, so an Odoo file got the 19 ORM rules and
1099
+ # none of the 8 Python ones, and `fieldtest --stack python` over OpenSPP
1100
+ # found 32 hits `--stack odoo` never reported. The id prefix stays as it
1101
+ # is -- it names where the rule came from and which documentation it is
1102
+ # sourced to, which is not the same question as which stack selects it.
1103
+ for stack, rules in (("odoo", ODOO + PYTHON),):
1104
  for r in rules:
1105
  r["stack"] = stack
1106
  r.setdefault("bypassed_by", [])
 
1111
  # deriving it costs a line.
1112
  r["likelihood"] = LIKELIHOOD.get(r["id"], LIKELIHOOD_DEFAULT)
1113
  r["advisory"] = r["likelihood"] in ADVISORY_LIKELIHOODS
 
 
1114
  if r["id"] in PROBES:
1115
  r["probe"] = PROBES[r["id"]]
1116
  else:
 
1135
  json.dump(rules, fh, indent=2, ensure_ascii=False)
1136
  crit = sum(1 for r in rules if r["severity"] == "critical")
1137
  print(" {0:<12} {1:>2} rules ({2} critical) -> {3}.json".format(stack, len(rules), crit, stack))
1138
+ print(" {0:<12} {1:>2} Odoo ORM rules + {2} plain-Python rules composed in"
1139
+ .format("", len(ODOO), len(PYTHON)))
1140
  total += len(rules)
1141
  checked = covered + semantic
1142
  print("\n {0} rules total, {1} machine-checkable ({2}%)".format(
corpus/check_sources.py CHANGED
@@ -84,9 +84,20 @@ REFERENCE = "https://www.odoo.com/documentation/{0}/developer/reference/backend/
84
 
85
 
86
  def load():
 
 
 
 
 
 
 
 
 
 
87
  with io.open(RULES, encoding="utf-8-sig") as fh:
88
  data = json.load(fh)
89
- return data if isinstance(data, list) else data.get("rules", data)
 
90
 
91
 
92
  def fetch(url, cache):
 
84
 
85
 
86
  def load():
87
+ """The rules that make a claim about an Odoo series.
88
+
89
+ odoo.json holds the whole corpus, which since the composition includes
90
+ eight plain-Python rules. Those carry no `odoo_versions` and cite
91
+ docs.python.org, so there is no series claim here to check and no Odoo API
92
+ to look for -- running them through this would report eight rules as
93
+ "unversioned" every time, which is a true sentence that makes the real
94
+ count harder to read. They are excluded by their origin prefix, the same
95
+ thing corpus.ORIGINS names.
96
+ """
97
  with io.open(RULES, encoding="utf-8-sig") as fh:
98
  data = json.load(fh)
99
+ rules = data if isinstance(data, list) else data.get("rules", data)
100
+ return [r for r in rules if r["id"].startswith("odoo.")]
101
 
102
 
103
  def fetch(url, cache):
corpus/corpus_meta.json CHANGED
@@ -1,14 +1,12 @@
1
  {
2
- "built_utc": "2026-09-16T07:06:37.091305+00:00",
3
- "rules": 43,
4
- "machine_checkable": 40,
5
- "by_pattern": 30,
6
  "by_semantic_check": 10,
7
  "checked_against": {
8
  "odoo": "18.0",
9
- "python": "3.12",
10
- "typescript": "5.x",
11
- "javascript": "ES2023"
12
  },
13
- "corpus_sha256": "a7e3adddd78657b2c19cab0616f95a93de4784d5a422089c3eae4552b0750f35"
14
  }
 
1
  {
2
+ "built_utc": "2026-09-16T08:00:31.662425+00:00",
3
+ "rules": 27,
4
+ "machine_checkable": 26,
5
+ "by_pattern": 16,
6
  "by_semantic_check": 10,
7
  "checked_against": {
8
  "odoo": "18.0",
9
+ "python": "3.12"
 
 
10
  },
11
+ "corpus_sha256": "68dd1fc12d9d546627767f840e3acffb1d0c78fd2ff5571d259261f5312ece03"
12
  }
corpus/rule.schema.json CHANGED
@@ -19,17 +19,15 @@
19
  "properties": {
20
  "id": {
21
  "type": "string",
22
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*\\.[a-z0-9]+(-[a-z0-9]+)*$",
23
- "description": "Stable citable identifier '<stack>.<slug>'. Models cite this; verify-citations checks it exists."
24
  },
25
  "stack": {
26
  "type": "string",
27
  "enum": [
28
- "odoo",
29
- "python",
30
- "typescript",
31
- "javascript"
32
- ]
33
  },
34
  "severity": {
35
  "type": "string",
 
19
  "properties": {
20
  "id": {
21
  "type": "string",
22
+ "pattern": "^(odoo|python)\\.[a-z0-9]+(-[a-z0-9]+)*$",
23
+ "description": "Stable citable identifier '<origin>.<slug>'. The prefix names where the rule came from -- odoo. for the ORM rules, python. for the plain-Python rules composed into the same stack -- and is not the stack, of which there is one. Models cite this; verify-citations checks it exists."
24
  },
25
  "stack": {
26
  "type": "string",
27
  "enum": [
28
+ "odoo"
29
+ ],
30
+ "description": "The one stack. It stays a field because prompts, the RAG index and every measurement are keyed on it. It is not the id prefix: python.decimal-for-money-not-float carries stack odoo, because it is selected when reviewing Odoo code while being sourced to docs.python.org."
 
 
31
  },
32
  "severity": {
33
  "type": "string",
corpus/rules/javascript.json DELETED
@@ -1,136 +0,0 @@
1
- [
2
- {
3
- "id": "javascript.json-parse-returns-any",
4
- "severity": "critical",
5
- "title": "JSON.parse output is unchecked",
6
- "rule": "Never use the result of JSON.parse directly. Pass it through a schema or an explicit shape check first.",
7
- "why": "JSON.parse succeeds for any syntactically valid JSON, including null, a bare number, or an object missing every field you expect. Property access then yields undefined, which propagates through arithmetic as NaN and through templates as the literal string undefined.",
8
- "enforcement_layer": "runtime-parse",
9
- "bypassed_by": [],
10
- "wrong": "const cfg = JSON.parse(raw);\nstartServer(cfg.port);",
11
- "correct": "const cfg = ConfigSchema.parse(JSON.parse(raw));\nstartServer(cfg.port);",
12
- "tags": [
13
- "json",
14
- "boundary"
15
- ],
16
- "stack": "javascript",
17
- "violation_pattern": "=\\s*JSON\\.parse\\(",
18
- "source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse",
19
- "likelihood": "unmeasured",
20
- "advisory": false,
21
- "probe": "use the parsed JSON body of an incoming request"
22
- },
23
- {
24
- "id": "javascript.loose-equality-and-falsy-defaults",
25
- "severity": "high",
26
- "title": "Falsy defaults discard legitimate zero and empty string",
27
- "rule": "Use the nullish coalescing operator for defaults and strict equality for comparisons. Reserve the logical OR operator for genuine boolean logic.",
28
- "why": "The OR operator treats 0, empty string and false as absent, so a quantity of 0 or an intentionally blank note is silently replaced by the default. This is the most common way a validated value is destroyed after it has passed validation.",
29
- "enforcement_layer": "runtime-parse",
30
- "bypassed_by": [],
31
- "wrong": "const qty = input.qty || 1; // qty of 0 becomes 1\nif (status == null) { } // also matches undefined by design, but == elsewhere surprises",
32
- "correct": "const qty = input.qty ?? 1; // only null or undefined fall back\nif (status === null || status === undefined) { }",
33
- "tags": [
34
- "coercion",
35
- "defaults",
36
- "silent-failure"
37
- ],
38
- "stack": "javascript",
39
- "violation_pattern": "\\w\\s*\\|\\|\\s*(?!0(?!\\d|\\.)|\\\"\\\"|'')(?:\\d|['\\\"])",
40
- "source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing",
41
- "likelihood": "unmeasured",
42
- "advisory": false,
43
- "probe": "a quantity of zero keeps being replaced by the default value"
44
- },
45
- {
46
- "id": "javascript.number-parsing-is-lossy",
47
- "severity": "high",
48
- "title": "Coercing user input to a number accepts nonsense",
49
- "rule": "Validate numeric input with an explicit check for finiteness rather than relying on Number, parseInt or the unary plus.",
50
- "why": "parseInt('12abc') returns 12, Number('') returns 0, and Number(' ') returns 0 as well. Each converts invalid input into a plausible number that passes every downstream check and corrupts totals rather than raising an error.",
51
- "enforcement_layer": "runtime-parse",
52
- "bypassed_by": [],
53
- "wrong": "const qty = parseInt(req.body.qty);",
54
- "correct": "const qty = Number(req.body.qty);\nif (!Number.isFinite(qty) || !Number.isInteger(qty) || qty <= 0) {\n throw new Error(`qty must be a positive integer, received: ${req.body.qty}`);\n}",
55
- "tags": [
56
- "numbers",
57
- "coercion",
58
- "input"
59
- ],
60
- "stack": "javascript",
61
- "violation_pattern": "parseInt\\(|parseFloat\\(",
62
- "satisfied_pattern": "Number\\.isFinite|Number\\.isInteger",
63
- "source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt",
64
- "likelihood": "unmeasured",
65
- "advisory": false,
66
- "probe": "turn a page number from the query string into a number"
67
- },
68
- {
69
- "id": "javascript.async-errors-need-explicit-handling",
70
- "severity": "critical",
71
- "title": "A rejected promise in a callback is not caught by the surrounding try",
72
- "rule": "Await every promise you create, or attach a catch to it. Never pass an async function where a synchronous callback is expected.",
73
- "why": "An async callback passed to forEach or to an event emitter returns a promise nobody holds. A rejection inside it escapes the enclosing try/catch entirely and becomes an unhandled rejection, which terminates the process on modern Node and silently skips work before that.",
74
- "enforcement_layer": "runtime-parse",
75
- "bypassed_by": [],
76
- "wrong": "rows.forEach(async (row) => {\n await save(row); // rejections escape, loop does not wait\n});",
77
- "correct": "await Promise.all(rows.map((row) => save(row)));\n// or, when order and back-pressure matter:\nfor (const row of rows) {\n await save(row);\n}",
78
- "tags": [
79
- "async",
80
- "promises",
81
- "error-handling"
82
- ],
83
- "stack": "javascript",
84
- "violation_pattern": "\\.forEach\\(\\s*async",
85
- "source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function",
86
- "likelihood": "unmeasured",
87
- "advisory": false,
88
- "probe": "await inside forEach does not wait and the errors disappear"
89
- },
90
- {
91
- "id": "javascript.object-injection-via-user-keys",
92
- "severity": "critical",
93
- "title": "Never index an object with an unvalidated user-supplied key",
94
- "rule": "Check a user-supplied key against an allowlist, or use a Map, before using it to read or write a property.",
95
- "why": "A key of __proto__ or constructor reaches the prototype chain. Writing through it pollutes every object in the process, which turns a validation gap into remote code execution in the worst case and into corrupted defaults everywhere in the mildest.",
96
- "enforcement_layer": "runtime-parse",
97
- "bypassed_by": [],
98
- "wrong": "target[req.body.key] = req.body.value;",
99
- "correct": "const ALLOWED = new Set(['name', 'email', 'phone']);\nif (!ALLOWED.has(req.body.key)) {\n throw new Error(`Unsupported field: ${req.body.key}`);\n}\ntarget[req.body.key] = req.body.value;",
100
- "tags": [
101
- "security",
102
- "prototype-pollution",
103
- "input"
104
- ],
105
- "stack": "javascript",
106
- "violation_pattern": "\\w+\\[\\s*(?:req\\.(?:body|query|params)|input|payload)\\.\\w+\\s*\\]\\s*=",
107
- "satisfied_pattern": "ALLOWED|allowlist|\\.has\\(|Object\\.hasOwn\\(|Object\\.prototype\\.hasOwnProperty\\.call|Object\\.create\\(null\\)",
108
- "source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto",
109
- "likelihood": "unmeasured",
110
- "advisory": false,
111
- "probe": "set a property on an object using a key taken from the request body"
112
- },
113
- {
114
- "id": "javascript.date-parsing-is-implementation-defined",
115
- "severity": "medium",
116
- "title": "The Date constructor accepts ambiguous strings",
117
- "rule": "Parse dates from an explicit ISO-8601 string and reject anything that produces an invalid date. Never feed arbitrary user text to new Date.",
118
- "why": "new Date('01/02/2026') is January the second in one locale's reading and the second of January in another, and non-ISO formats are implementation-defined across engines. new Date('nonsense') returns an Invalid Date object rather than throwing, so the failure travels onward as NaN.",
119
- "enforcement_layer": "runtime-parse",
120
- "bypassed_by": [],
121
- "wrong": "const when = new Date(req.body.date);",
122
- "correct": "const when = new Date(req.body.date);\nif (Number.isNaN(when.getTime())) {\n throw new Error(`Invalid date: ${req.body.date}`);\n}",
123
- "tags": [
124
- "dates",
125
- "coercion",
126
- "locale"
127
- ],
128
- "stack": "javascript",
129
- "violation_pattern": "new Date\\(\\s*(?:req|input|body|payload|row)\\.",
130
- "satisfied_pattern": "Number\\.isNaN|isNaN\\(",
131
- "source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date",
132
- "likelihood": "unmeasured",
133
- "advisory": false,
134
- "probe": "turn a date string typed by a user into a Date object"
135
- }
136
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
corpus/rules/odoo.json CHANGED
@@ -159,7 +159,7 @@
159
  "source": "https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101/10_constraints.html",
160
  "likelihood": "preference",
161
  "advisory": true,
162
- "probe": "stop anyone entering a negative quantity on a sale order line"
163
  },
164
  {
165
  "id": "odoo.check-constraint-cannot-cross-tables",
@@ -479,5 +479,200 @@
479
  "likelihood": "unproven",
480
  "advisory": true,
481
  "probe": "my except AccessError block never runs and the API returns 409"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  }
483
  ]
 
159
  "source": "https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101/10_constraints.html",
160
  "likelihood": "preference",
161
  "advisory": true,
162
+ "probe": "should this check be a database constraint or an api.constrains method"
163
  },
164
  {
165
  "id": "odoo.check-constraint-cannot-cross-tables",
 
479
  "likelihood": "unproven",
480
  "advisory": true,
481
  "probe": "my except AccessError block never runs and the API returns 409"
482
+ },
483
+ {
484
+ "id": "python.assert-is-not-validation",
485
+ "severity": "critical",
486
+ "title": "assert disappears under -O, so it cannot enforce anything",
487
+ "rule": "Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.",
488
+ "why": "Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.",
489
+ "enforcement_layer": "runtime-parse",
490
+ "bypassed_by": [
491
+ "python -O",
492
+ "PYTHONOPTIMIZE=1",
493
+ "a .pyc built with optimisation"
494
+ ],
495
+ "wrong": "def withdraw(account, amount):\n assert amount > 0, 'amount must be positive'\n account.balance -= amount",
496
+ "correct": "def withdraw(account, amount):\n if amount <= 0:\n raise ValueError('amount must be positive, got {0!r}'.format(amount))\n account.balance -= amount",
497
+ "tags": [
498
+ "assert",
499
+ "input-validation",
500
+ "optimisation",
501
+ "silent-failure"
502
+ ],
503
+ "stack": "odoo",
504
+ "violation_pattern": "(?<=\\n)[ \\t]*assert\\s+",
505
+ "source": "https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement",
506
+ "likelihood": "unmeasured",
507
+ "advisory": false,
508
+ "probe": "check that an amount is positive before using it"
509
+ },
510
+ {
511
+ "id": "python.mutable-default-argument",
512
+ "severity": "high",
513
+ "title": "A default argument is evaluated once, not once per call",
514
+ "rule": "Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.",
515
+ "why": "The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.",
516
+ "enforcement_layer": "runtime-parse",
517
+ "bypassed_by": [],
518
+ "wrong": "def collect(item, seen=[]):\n seen.append(item)\n return seen",
519
+ "correct": "def collect(item, seen=None):\n if seen is None:\n seen = []\n seen.append(item)\n return seen",
520
+ "tags": [
521
+ "defaults",
522
+ "shared-state",
523
+ "silent-failure"
524
+ ],
525
+ "stack": "odoo",
526
+ "violation_pattern": "def\\s+\\w+\\([^)]*=\\s*(?:\\[\\]|\\{\\}|set\\(\\))",
527
+ "source": "https://docs.python.org/3/tutorial/controlflow.html#default-argument-values",
528
+ "likelihood": "unmeasured",
529
+ "advisory": false,
530
+ "probe": "a list keeps items from the previous call to the same function"
531
+ },
532
+ {
533
+ "id": "python.bare-except-catches-the-exit",
534
+ "severity": "high",
535
+ "title": "A bare except swallows Ctrl-C and sys.exit too",
536
+ "rule": "Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.",
537
+ "why": "A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.",
538
+ "enforcement_layer": "runtime-parse",
539
+ "bypassed_by": [],
540
+ "wrong": "try:\n qty = int(row['qty'])\nexcept:\n qty = 0",
541
+ "correct": "try:\n qty = int(row['qty'])\nexcept (KeyError, TypeError, ValueError) as exc:\n raise ValueError('bad qty in row {0!r}'.format(row)) from exc",
542
+ "tags": [
543
+ "exceptions",
544
+ "error-handling",
545
+ "silent-failure"
546
+ ],
547
+ "stack": "odoo",
548
+ "violation_pattern": "except\\s*:",
549
+ "source": "https://docs.python.org/3/tutorial/errors.html",
550
+ "likelihood": "unmeasured",
551
+ "advisory": false,
552
+ "probe": "ctrl-c stops working and the script will not exit"
553
+ },
554
+ {
555
+ "id": "python.eval-and-pickle-execute-their-input",
556
+ "severity": "critical",
557
+ "title": "eval, exec and pickle run whatever they are handed",
558
+ "rule": "Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.",
559
+ "why": "eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.",
560
+ "enforcement_layer": "runtime-parse",
561
+ "bypassed_by": [],
562
+ "wrong": "config = eval(request.body)",
563
+ "correct": "import json\n\nconfig = json.loads(request.body)\nif not isinstance(config, dict):\n raise ValueError('config must be a JSON object')",
564
+ "tags": [
565
+ "security",
566
+ "deserialisation",
567
+ "remote-code-execution",
568
+ "input"
569
+ ],
570
+ "stack": "odoo",
571
+ "violation_pattern": "\\beval\\(|\\bexec\\(|pickle\\.loads?\\(",
572
+ "source": "https://docs.python.org/3/library/pickle.html",
573
+ "likelihood": "unmeasured",
574
+ "advisory": false,
575
+ "probe": "turn a config string sent by a client into a Python object"
576
+ },
577
+ {
578
+ "id": "python.subprocess-shell-true-is-injection",
579
+ "severity": "critical",
580
+ "title": "shell=True hands your string to the shell",
581
+ "rule": "Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.",
582
+ "why": "With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.",
583
+ "enforcement_layer": "runtime-parse",
584
+ "bypassed_by": [
585
+ "os.system",
586
+ "os.popen",
587
+ "shlex.split on an already-joined string"
588
+ ],
589
+ "wrong": "subprocess.run('convert {0} out.png'.format(path), shell=True)",
590
+ "correct": "subprocess.run(['convert', path, 'out.png'], check=True)",
591
+ "tags": [
592
+ "security",
593
+ "injection",
594
+ "subprocess",
595
+ "input"
596
+ ],
597
+ "stack": "odoo",
598
+ "violation_pattern": "shell\\s*=\\s*True|os\\.system\\(|os\\.popen\\(",
599
+ "source": "https://docs.python.org/3/library/subprocess.html#security-considerations",
600
+ "likelihood": "unmeasured",
601
+ "advisory": false,
602
+ "probe": "run a command line built from a filename the user gave me"
603
+ },
604
+ {
605
+ "id": "python.path-join-does-not-contain-a-path",
606
+ "severity": "high",
607
+ "title": "os.path.join discards everything before an absolute component",
608
+ "rule": "Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.",
609
+ "why": "os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.",
610
+ "enforcement_layer": "runtime-parse",
611
+ "bypassed_by": [
612
+ "symlinks inside the base directory"
613
+ ],
614
+ "wrong": "full = os.path.join(UPLOAD_DIR, request.args['name'])\nreturn open(full, 'rb').read()",
615
+ "correct": "base = os.path.realpath(UPLOAD_DIR)\nfull = os.path.realpath(os.path.join(base, request.args['name']))\nif os.path.commonpath([base, full]) != base:\n raise ValueError('path escapes the upload directory')\nreturn open(full, 'rb').read()",
616
+ "tags": [
617
+ "security",
618
+ "paths",
619
+ "traversal",
620
+ "input"
621
+ ],
622
+ "stack": "odoo",
623
+ "violation_pattern": "os\\.path\\.join\\([^)]*\\b(?:request|args|params|payload|form|user_input|uploaded|filename)\\b",
624
+ "satisfied_pattern": "commonpath|commonprefix|relpath|is_relative_to",
625
+ "source": "https://docs.python.org/3/library/os.path.html#os.path.join",
626
+ "likelihood": "unmeasured",
627
+ "advisory": false,
628
+ "probe": "open a file the user named inside my uploads directory"
629
+ },
630
+ {
631
+ "id": "python.decimal-for-money-not-float",
632
+ "severity": "high",
633
+ "title": "Binary floats cannot hold a decimal amount",
634
+ "rule": "Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().",
635
+ "why": "0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.",
636
+ "enforcement_layer": "runtime-parse",
637
+ "bypassed_by": [
638
+ "json.loads, which parses every JSON number as a float"
639
+ ],
640
+ "wrong": "price = float(payload['price'])\ntotal = price * qty",
641
+ "correct": "from decimal import Decimal\n\nprice = Decimal(payload['price']) # from the string, never through float\ntotal = price * qty",
642
+ "tags": [
643
+ "money",
644
+ "precision",
645
+ "floats",
646
+ "decimal"
647
+ ],
648
+ "stack": "odoo",
649
+ "violation_pattern": "(?:price|amount|total|cost|balance|fee|tax|subtotal|payment|salary)\\w*\\s*=\\s*float\\(|float\\(\\s*[^)]*\\b(?:price|amount|cost|balance|fee|tax|subtotal|payment|salary)\\b",
650
+ "satisfied_pattern": "Decimal",
651
+ "source": "https://docs.python.org/3/library/decimal.html",
652
+ "likelihood": "unmeasured",
653
+ "advisory": false,
654
+ "probe": "totals are off by a fraction of a penny after adding prices up"
655
+ },
656
+ {
657
+ "id": "python.naive-and-aware-datetimes-do-not-compare",
658
+ "severity": "medium",
659
+ "title": "A naive datetime cannot be compared with an aware one",
660
+ "rule": "Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.",
661
+ "why": "Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.",
662
+ "enforcement_layer": "runtime-parse",
663
+ "bypassed_by": [],
664
+ "wrong": "if expires_at < datetime.datetime.now():\n raise ValueError('token expired')",
665
+ "correct": "from datetime import datetime, timezone\n\nif expires_at < datetime.now(timezone.utc):\n raise ValueError('token expired')",
666
+ "tags": [
667
+ "datetime",
668
+ "timezone",
669
+ "comparison"
670
+ ],
671
+ "stack": "odoo",
672
+ "violation_pattern": "datetime\\.now\\(\\s*\\)|utcnow\\(\\s*\\)",
673
+ "source": "https://docs.python.org/3/library/datetime.html",
674
+ "likelihood": "unmeasured",
675
+ "advisory": false,
676
+ "probe": "TypeError comparing offset-naive and offset-aware datetimes"
677
  }
678
  ]
corpus/rules/python.json DELETED
@@ -1,197 +0,0 @@
1
- [
2
- {
3
- "id": "python.assert-is-not-validation",
4
- "severity": "critical",
5
- "title": "assert disappears under -O, so it cannot enforce anything",
6
- "rule": "Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.",
7
- "why": "Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.",
8
- "enforcement_layer": "runtime-parse",
9
- "bypassed_by": [
10
- "python -O",
11
- "PYTHONOPTIMIZE=1",
12
- "a .pyc built with optimisation"
13
- ],
14
- "wrong": "def withdraw(account, amount):\n assert amount > 0, 'amount must be positive'\n account.balance -= amount",
15
- "correct": "def withdraw(account, amount):\n if amount <= 0:\n raise ValueError('amount must be positive, got {0!r}'.format(amount))\n account.balance -= amount",
16
- "tags": [
17
- "assert",
18
- "input-validation",
19
- "optimisation",
20
- "silent-failure"
21
- ],
22
- "stack": "python",
23
- "violation_pattern": "(?<=\\n)[ \\t]*assert\\s+",
24
- "source": "https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement",
25
- "likelihood": "unmeasured",
26
- "advisory": false,
27
- "probe": "check that an amount is positive before using it"
28
- },
29
- {
30
- "id": "python.mutable-default-argument",
31
- "severity": "high",
32
- "title": "A default argument is evaluated once, not once per call",
33
- "rule": "Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.",
34
- "why": "The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.",
35
- "enforcement_layer": "runtime-parse",
36
- "bypassed_by": [],
37
- "wrong": "def collect(item, seen=[]):\n seen.append(item)\n return seen",
38
- "correct": "def collect(item, seen=None):\n if seen is None:\n seen = []\n seen.append(item)\n return seen",
39
- "tags": [
40
- "defaults",
41
- "shared-state",
42
- "silent-failure"
43
- ],
44
- "stack": "python",
45
- "violation_pattern": "def\\s+\\w+\\([^)]*=\\s*(?:\\[\\]|\\{\\}|set\\(\\))",
46
- "source": "https://docs.python.org/3/tutorial/controlflow.html#default-argument-values",
47
- "likelihood": "unmeasured",
48
- "advisory": false,
49
- "probe": "a list keeps items from the previous call to the same function"
50
- },
51
- {
52
- "id": "python.bare-except-catches-the-exit",
53
- "severity": "high",
54
- "title": "A bare except swallows Ctrl-C and sys.exit too",
55
- "rule": "Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.",
56
- "why": "A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.",
57
- "enforcement_layer": "runtime-parse",
58
- "bypassed_by": [],
59
- "wrong": "try:\n qty = int(row['qty'])\nexcept:\n qty = 0",
60
- "correct": "try:\n qty = int(row['qty'])\nexcept (KeyError, TypeError, ValueError) as exc:\n raise ValueError('bad qty in row {0!r}'.format(row)) from exc",
61
- "tags": [
62
- "exceptions",
63
- "error-handling",
64
- "silent-failure"
65
- ],
66
- "stack": "python",
67
- "violation_pattern": "except\\s*:",
68
- "source": "https://docs.python.org/3/tutorial/errors.html",
69
- "likelihood": "unmeasured",
70
- "advisory": false,
71
- "probe": "ctrl-c stops working and the script will not exit"
72
- },
73
- {
74
- "id": "python.eval-and-pickle-execute-their-input",
75
- "severity": "critical",
76
- "title": "eval, exec and pickle run whatever they are handed",
77
- "rule": "Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.",
78
- "why": "eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.",
79
- "enforcement_layer": "runtime-parse",
80
- "bypassed_by": [],
81
- "wrong": "config = eval(request.body)",
82
- "correct": "import json\n\nconfig = json.loads(request.body)\nif not isinstance(config, dict):\n raise ValueError('config must be a JSON object')",
83
- "tags": [
84
- "security",
85
- "deserialisation",
86
- "remote-code-execution",
87
- "input"
88
- ],
89
- "stack": "python",
90
- "violation_pattern": "\\beval\\(|\\bexec\\(|pickle\\.loads?\\(",
91
- "source": "https://docs.python.org/3/library/pickle.html",
92
- "likelihood": "unmeasured",
93
- "advisory": false,
94
- "probe": "turn a config string sent by a client into a Python object"
95
- },
96
- {
97
- "id": "python.subprocess-shell-true-is-injection",
98
- "severity": "critical",
99
- "title": "shell=True hands your string to the shell",
100
- "rule": "Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.",
101
- "why": "With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.",
102
- "enforcement_layer": "runtime-parse",
103
- "bypassed_by": [
104
- "os.system",
105
- "os.popen",
106
- "shlex.split on an already-joined string"
107
- ],
108
- "wrong": "subprocess.run('convert {0} out.png'.format(path), shell=True)",
109
- "correct": "subprocess.run(['convert', path, 'out.png'], check=True)",
110
- "tags": [
111
- "security",
112
- "injection",
113
- "subprocess",
114
- "input"
115
- ],
116
- "stack": "python",
117
- "violation_pattern": "shell\\s*=\\s*True|os\\.system\\(|os\\.popen\\(",
118
- "source": "https://docs.python.org/3/library/subprocess.html#security-considerations",
119
- "likelihood": "unmeasured",
120
- "advisory": false,
121
- "probe": "run a command line built from a filename the user gave me"
122
- },
123
- {
124
- "id": "python.path-join-does-not-contain-a-path",
125
- "severity": "high",
126
- "title": "os.path.join discards everything before an absolute component",
127
- "rule": "Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.",
128
- "why": "os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.",
129
- "enforcement_layer": "runtime-parse",
130
- "bypassed_by": [
131
- "symlinks inside the base directory"
132
- ],
133
- "wrong": "full = os.path.join(UPLOAD_DIR, request.args['name'])\nreturn open(full, 'rb').read()",
134
- "correct": "base = os.path.realpath(UPLOAD_DIR)\nfull = os.path.realpath(os.path.join(base, request.args['name']))\nif os.path.commonpath([base, full]) != base:\n raise ValueError('path escapes the upload directory')\nreturn open(full, 'rb').read()",
135
- "tags": [
136
- "security",
137
- "paths",
138
- "traversal",
139
- "input"
140
- ],
141
- "stack": "python",
142
- "violation_pattern": "os\\.path\\.join\\([^)]*\\b(?:request|args|params|payload|form|user_input|uploaded|filename)\\b",
143
- "satisfied_pattern": "commonpath|commonprefix|relpath|is_relative_to",
144
- "source": "https://docs.python.org/3/library/os.path.html#os.path.join",
145
- "likelihood": "unmeasured",
146
- "advisory": false,
147
- "probe": "open a file the user named inside my uploads directory"
148
- },
149
- {
150
- "id": "python.decimal-for-money-not-float",
151
- "severity": "high",
152
- "title": "Binary floats cannot hold a decimal amount",
153
- "rule": "Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().",
154
- "why": "0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.",
155
- "enforcement_layer": "runtime-parse",
156
- "bypassed_by": [
157
- "json.loads, which parses every JSON number as a float"
158
- ],
159
- "wrong": "price = float(payload['price'])\ntotal = price * qty",
160
- "correct": "from decimal import Decimal\n\nprice = Decimal(payload['price']) # from the string, never through float\ntotal = price * qty",
161
- "tags": [
162
- "money",
163
- "precision",
164
- "floats",
165
- "decimal"
166
- ],
167
- "stack": "python",
168
- "violation_pattern": "(?:price|amount|total|cost|balance|fee|tax|subtotal|payment|salary)\\w*\\s*=\\s*float\\(|float\\(\\s*[^)]*\\b(?:price|amount|cost|balance|fee|tax|subtotal|payment|salary)\\b",
169
- "satisfied_pattern": "Decimal",
170
- "source": "https://docs.python.org/3/library/decimal.html",
171
- "likelihood": "unmeasured",
172
- "advisory": false,
173
- "probe": "totals are off by a fraction of a penny after adding prices up"
174
- },
175
- {
176
- "id": "python.naive-and-aware-datetimes-do-not-compare",
177
- "severity": "medium",
178
- "title": "A naive datetime cannot be compared with an aware one",
179
- "rule": "Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.",
180
- "why": "Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.",
181
- "enforcement_layer": "runtime-parse",
182
- "bypassed_by": [],
183
- "wrong": "if expires_at < datetime.datetime.now():\n raise ValueError('token expired')",
184
- "correct": "from datetime import datetime, timezone\n\nif expires_at < datetime.now(timezone.utc):\n raise ValueError('token expired')",
185
- "tags": [
186
- "datetime",
187
- "timezone",
188
- "comparison"
189
- ],
190
- "stack": "python",
191
- "violation_pattern": "datetime\\.now\\(\\s*\\)|utcnow\\(\\s*\\)",
192
- "source": "https://docs.python.org/3/library/datetime.html",
193
- "likelihood": "unmeasured",
194
- "advisory": false,
195
- "probe": "TypeError comparing offset-naive and offset-aware datetimes"
196
- }
197
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
corpus/rules/typescript.json DELETED
@@ -1,238 +0,0 @@
1
- [
2
- {
3
- "id": "typescript.types-are-erased-at-runtime",
4
- "severity": "critical",
5
- "title": "Static types guarantee nothing about external data",
6
- "rule": "Every value crossing a boundary must be parsed by a runtime schema. A type assertion on external data is a lie the compiler cannot catch.",
7
- "why": "Interfaces and type annotations are erased during compilation. Casting an API response with 'as User' produces zero runtime checks, so a changed or malformed payload propagates as undefined deep inside unrelated code, far from its origin.",
8
- "enforcement_layer": "runtime-parse",
9
- "bypassed_by": [
10
- "type assertions (as T)",
11
- "any",
12
- "unchecked JSON.parse"
13
- ],
14
- "wrong": "const user = await res.json() as User;",
15
- "correct": "const UserSchema = z.object({ id: z.number(), name: z.string() });\ntype User = z.infer<typeof UserSchema>;\n\nconst user = UserSchema.parse(await res.json());",
16
- "tags": [
17
- "zod",
18
- "boundary",
19
- "type-erasure"
20
- ],
21
- "stack": "typescript",
22
- "violation_pattern": "\\.json\\(\\)\\s+as\\s+\\w+|as\\s+unknown\\s+as\\s+\\w+",
23
- "source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html",
24
- "likelihood": "unmeasured",
25
- "advisory": false,
26
- "probe": "cast the JSON from an API response to my interface"
27
- },
28
- {
29
- "id": "typescript.derive-type-from-schema",
30
- "severity": "high",
31
- "title": "Derive the type from the schema, never maintain both",
32
- "rule": "Write the schema first and derive the static type with z.infer. Never hand-write an interface alongside a schema for the same data.",
33
- "why": "Two hand-maintained definitions drift. The compiler cannot detect the drift because each is internally consistent, so the type says one thing while the parser enforces another and the mismatch only appears at runtime.",
34
- "enforcement_layer": "compile-time",
35
- "bypassed_by": [],
36
- "wrong": "interface Order { id: number; total: number; }\nconst OrderSchema = z.object({ id: z.number(), total: z.number(), currency: z.string() });",
37
- "correct": "const OrderSchema = z.object({\n id: z.number(),\n total: z.number(),\n currency: z.string(),\n});\ntype Order = z.infer<typeof OrderSchema>;",
38
- "tags": [
39
- "zod",
40
- "single-source-of-truth",
41
- "drift"
42
- ],
43
- "stack": "typescript",
44
- "violation_pattern": "interface\\s+\\w+\\s*\\{[\\s\\S]*?\\}[\\s\\S]*?z\\.object\\(",
45
- "source": "https://zod.dev/api",
46
- "likelihood": "unmeasured",
47
- "advisory": false,
48
- "probe": "keep my interface and my validation schema from drifting apart"
49
- },
50
- {
51
- "id": "typescript.odoo-returns-false-for-every-empty-field",
52
- "severity": "critical",
53
- "title": "Odoo JSON-RPC returns false for every empty field",
54
- "rule": "Model every optional Odoo scalar as a union with literal false, normalised through one shared helper. This applies to Char, Text, Date, Datetime, Binary and Many2one alike, not only to relations.",
55
- "why": "Odoo returns boolean false for any empty value, never null, never an empty string. Typing an optional email as string().nullable() throws at the parse boundary the first time a partner has no email, which is ordinary production data rather than an edge case.",
56
- "enforcement_layer": "runtime-parse",
57
- "bypassed_by": [],
58
- "wrong": "const PartnerSchema = z.object({\n email: z.string().nullable(),\n parent_id: z.array(z.number()),\n});",
59
- "correct": "const odooNullable = <T extends z.ZodTypeAny>(inner: T) =>\n z.union([inner, z.literal(false)]).transform((v) => (v === false ? null : v));\n\nconst PartnerSchema = z.object({\n id: z.number().int(),\n name: z.string(),\n email: odooNullable(z.string().email()),\n parent_id: odooNullable(z.tuple([z.number().int(), z.string()])),\n});",
60
- "tags": [
61
- "odoo",
62
- "json-rpc",
63
- "boundary",
64
- "zod"
65
- ],
66
- "violation_pattern": "z\\.(?:string|number|date|coerce)[^\\n]*\\.nullable\\(\\)",
67
- "satisfied_pattern": "odooNullable|z\\.literal\\(false\\)",
68
- "stack": "typescript",
69
- "source": "https://www.odoo.com/documentation/18.0/developer/reference/external_api.html",
70
- "likelihood": "unmeasured",
71
- "advisory": false,
72
- "probe": "parsing fails because Odoo sent false for a partner with no email"
73
- },
74
- {
75
- "id": "typescript.odoo-many2one-is-a-tuple",
76
- "severity": "high",
77
- "title": "A set Many2one is a two-element tuple, not an array of ids",
78
- "rule": "Model a populated Many2one as z.tuple([z.number(), z.string()]) holding id and display_name. Model an empty one as false.",
79
- "why": "Odoo read() returns [id, display_name] for a set Many2one. Typing it as an array of numbers parses the id but discards display_name and fails the moment the schema meets real data, because element one is a string.",
80
- "enforcement_layer": "runtime-parse",
81
- "bypassed_by": [],
82
- "wrong": "parent_id: z.array(z.number())",
83
- "correct": "parent_id: odooNullable(z.tuple([z.number().int(), z.string()]))",
84
- "tags": [
85
- "odoo",
86
- "json-rpc",
87
- "many2one"
88
- ],
89
- "stack": "typescript",
90
- "violation_pattern": "_id\\s*:\\s*(?:\\w+\\()?z\\.array\\(\\s*z\\.number",
91
- "source": "https://www.odoo.com/documentation/18.0/developer/reference/external_api.html",
92
- "likelihood": "unmeasured",
93
- "advisory": false,
94
- "probe": "read the parent company name off a many2one field from Odoo"
95
- },
96
- {
97
- "id": "typescript.odoo-read-returns-an-array",
98
- "severity": "medium",
99
- "title": "Odoo read returns an array even for one id",
100
- "rule": "Parse the response of read() with z.array(Schema) and index into it, rather than parsing it as a single object.",
101
- "why": "read() always returns a list of dictionaries. Parsing it directly against an object schema fails, and defensively coercing with Array.isArray hides whether the call actually returned the record or an empty list for a missing id.",
102
- "enforcement_layer": "runtime-parse",
103
- "bypassed_by": [],
104
- "wrong": "const partner = PartnerSchema.parse(await callOdoo('res.partner', 'read', [id]));",
105
- "correct": "const rows = z.array(PartnerSchema).parse(\n await callOdoo('res.partner', 'read', [id]),\n);\nif (rows.length === 0) throw new Error(`res.partner ${id} not found or not readable`);\nconst partner = rows[0];",
106
- "tags": [
107
- "odoo",
108
- "json-rpc",
109
- "read"
110
- ],
111
- "stack": "typescript",
112
- "violation_pattern": "\\w+Schema\\.(?:safe)?[Pp]arse\\(\\s*await[^)]*(?:callOdoo|jsonrpc|'read'|\\\"read\\\")",
113
- "satisfied_pattern": "z\\.array\\(",
114
- "source": "https://www.odoo.com/documentation/18.0/developer/reference/external_api.html",
115
- "likelihood": "unmeasured",
116
- "advisory": false,
117
- "probe": "parse the result of an Odoo read call for a single id"
118
- },
119
- {
120
- "id": "typescript.unknown-not-any-at-boundaries",
121
- "severity": "high",
122
- "title": "Use unknown at boundaries, never any",
123
- "rule": "Type every value that arrives unparsed as unknown: the body of an incoming webhook, a request payload, a handler parameter, the result of JSON.parse, anything from an untyped import. Reserve any for nothing.",
124
- "why": "any silently disables checking for every expression derived from it, so one any on a webhook handler's payload removes type safety from the whole call chain downstream, and the compiler reports nothing. unknown refuses every use until the value has been parsed or narrowed, which is the behaviour that was wanted from the annotation in the first place.",
125
- "enforcement_layer": "compile-time",
126
- "bypassed_by": [
127
- "explicit any",
128
- "implicit any from untyped imports"
129
- ],
130
- "wrong": "function handle(payload: any) { return payload.order.total; }",
131
- "correct": "function handle(payload: unknown) {\n const { order } = WebhookSchema.parse(payload);\n return order.total;\n}",
132
- "tags": [
133
- "any",
134
- "unknown",
135
- "boundary",
136
- "webhook",
137
- "payload",
138
- "request-body",
139
- "handler",
140
- "external-input"
141
- ],
142
- "stack": "typescript",
143
- "violation_pattern": ":\\s*any\\b",
144
- "source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html",
145
- "likelihood": "unmeasured",
146
- "advisory": false,
147
- "probe": "what type should the payload of an incoming webhook handler have"
148
- },
149
- {
150
- "id": "typescript.safeparse-when-failure-is-expected",
151
- "severity": "medium",
152
- "title": "Choose parse or safeParse deliberately",
153
- "rule": "Use safeParse where a malformed value is expected and recoverable; use parse only where throwing is the correct response. State which and why.",
154
- "why": "parse throws a ZodError that an unprepared caller turns into a 500 and an opaque stack trace. safeParse returns a discriminated result carrying the field-level issues, which is what a user-facing error or a per-row import skip actually needs.",
155
- "enforcement_layer": "runtime-parse",
156
- "bypassed_by": [],
157
- "wrong": "const row = RowSchema.parse(csvRow); // aborts the whole import on row 4000",
158
- "correct": "const result = RowSchema.safeParse(csvRow);\nif (!result.success) {\n errors.push({ line, issues: result.error.issues });\n continue;\n}\nconst row = result.data;",
159
- "tags": [
160
- "zod",
161
- "error-handling"
162
- ],
163
- "stack": "typescript",
164
- "likelihood": "unmeasured",
165
- "advisory": false,
166
- "source": "https://zod.dev/api",
167
- "probe": "skip the bad rows in a CSV import instead of aborting the whole file",
168
- "no_check_reason": "Whether parse or safeParse is correct depends on whether the caller can recover, which is context the snippet does not contain. Any pattern here would be noise."
169
- },
170
- {
171
- "id": "typescript.validate-env-at-startup",
172
- "severity": "high",
173
- "title": "Parse environment variables once at startup",
174
- "rule": "Validate the whole environment against a schema at process start and export the parsed object. Never read process.env deep inside application code.",
175
- "why": "process.env values are string or undefined and are unchecked. A missing variable read lazily inside a rarely-exercised branch fails in production at the worst moment, whereas a startup parse turns the same fault into a deployment that refuses to boot.",
176
- "enforcement_layer": "startup",
177
- "bypassed_by": [],
178
- "wrong": "const url = process.env.ODOO_URL; // string | undefined, used 40 files away",
179
- "correct": "const EnvSchema = z.object({\n ODOO_URL: z.string().url(),\n ODOO_DB: z.string().min(1),\n PORT: z.coerce.number().int().positive().default(3000),\n});\nexport const env = EnvSchema.parse(process.env);",
180
- "tags": [
181
- "config",
182
- "startup",
183
- "fail-fast"
184
- ],
185
- "stack": "typescript",
186
- "violation_pattern": "process\\.env\\.[A-Z][A-Z0-9_]*",
187
- "source": "https://nodejs.org/api/process.html",
188
- "likelihood": "unmeasured",
189
- "advisory": false,
190
- "probe": "make sure the required environment variables are set before the app runs"
191
- },
192
- {
193
- "id": "typescript.discriminated-union-for-variants",
194
- "severity": "medium",
195
- "title": "Use discriminatedUnion for tagged variant payloads",
196
- "rule": "When a payload has a literal tag field selecting its shape, use z.discriminatedUnion on that field rather than z.union.",
197
- "why": "A plain union tries every member and reports the failure of all of them, producing an unreadable error and slower parsing. discriminatedUnion selects the branch by tag, reports issues only for the intended shape, and narrows correctly in TypeScript afterwards.",
198
- "enforcement_layer": "runtime-parse",
199
- "bypassed_by": [],
200
- "wrong": "z.union([PaymentSucceeded, PaymentFailed, PaymentPending])",
201
- "correct": "z.discriminatedUnion('status', [\n z.object({ status: z.literal('succeeded'), chargeId: z.string() }),\n z.object({ status: z.literal('failed'), reason: z.string() }),\n z.object({ status: z.literal('pending') }),\n])",
202
- "tags": [
203
- "zod",
204
- "unions",
205
- "narrowing"
206
- ],
207
- "stack": "typescript",
208
- "violation_pattern": "z\\.union\\(\\[\\s*[A-Z]",
209
- "satisfied_pattern": "z\\.discriminatedUnion",
210
- "source": "https://zod.dev/api",
211
- "likelihood": "unmeasured",
212
- "advisory": false,
213
- "probe": "parse a webhook whose body shape depends on a status field"
214
- },
215
- {
216
- "id": "typescript.brand-validated-primitives",
217
- "severity": "medium",
218
- "title": "Brand validated primitives so they cannot be swapped for raw ones",
219
- "rule": "Give checked primitives a branded type so a validated value is not assignable from an arbitrary string or number.",
220
- "why": "Without branding, every string is assignable to every string-typed parameter, so a raw user input can be passed where a validated email or a trusted id is required and the compiler accepts it. Branding makes that a compile error.",
221
- "enforcement_layer": "compile-time",
222
- "bypassed_by": [
223
- "casting through as"
224
- ],
225
- "wrong": "function send(to: string) { /* any string at all */ }",
226
- "correct": "const Email = z.string().email().brand<'Email'>();\ntype Email = z.infer<typeof Email>;\n\nfunction send(to: Email) { /* only a parsed Email */ }",
227
- "tags": [
228
- "branding",
229
- "nominal-typing"
230
- ],
231
- "stack": "typescript",
232
- "likelihood": "unmeasured",
233
- "advisory": false,
234
- "source": "https://zod.dev/api",
235
- "probe": "stop a raw string being passed where a checked email address is required",
236
- "no_check_reason": "Absence of branding is not detectable: plain string parameters are correct in most code. Flagging them all would bury the real findings."
237
- }
238
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fieldtest-js-2026-09.json DELETED
@@ -1,451 +0,0 @@
1
- {
2
- "stack": "javascript",
3
- "roots": [
4
- "OpenSPP2-19.0",
5
- "server-tools",
6
- "account-financial-tools"
7
- ],
8
- "files": 62,
9
- "lines": 9304,
10
- "checkers": 5,
11
- "triggers": {
12
- "files": 3144,
13
- "models_declared": 631,
14
- "models_deleted": 526,
15
- "declared_and_deleted": 516,
16
- "never_deleted": 115,
17
- "multi_company": true
18
- },
19
- "hits": [
20
- {
21
- "rule": "javascript.loose-equality-and-falsy-defaults",
22
- "severity": "high",
23
- "line": 11,
24
- "evidence": "const docUrl = session.spp_documentation_url || \"https://docs.openspp.org\";",
25
- "suppressed": false,
26
- "advisory": false,
27
- "likelihood": "unmeasured",
28
- "path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js"
29
- },
30
- {
31
- "rule": "javascript.loose-equality-and-falsy-defaults",
32
- "severity": "high",
33
- "line": 12,
34
- "evidence": "const supportUrl = session.spp_support_url || \"https://openspp.org\";",
35
- "suppressed": false,
36
- "advisory": false,
37
- "likelihood": "unmeasured",
38
- "path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js"
39
- },
40
- {
41
- "rule": "javascript.loose-equality-and-falsy-defaults",
42
- "severity": "high",
43
- "line": 9,
44
- "evidence": "const systemName = session.spp_system_name || \"OpenSPP Platform\";",
45
- "suppressed": false,
46
- "advisory": false,
47
- "likelihood": "unmeasured",
48
- "path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/webclient.js"
49
- },
50
- {
51
- "rule": "javascript.loose-equality-and-falsy-defaults",
52
- "severity": "high",
53
- "line": 128,
54
- "evidence": "return this._currentProfile || this.props.profile || \"registry_individuals\";",
55
- "suppressed": false,
56
- "advisory": false,
57
- "likelihood": "unmeasured",
58
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js"
59
- },
60
- {
61
- "rule": "javascript.loose-equality-and-falsy-defaults",
62
- "severity": "high",
63
- "line": 243,
64
- "evidence": "minHeight: this.props.minHeight || \"80px\",",
65
- "suppressed": false,
66
- "advisory": false,
67
- "likelihood": "unmeasured",
68
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js"
69
- },
70
- {
71
- "rule": "javascript.loose-equality-and-falsy-defaults",
72
- "severity": "high",
73
- "line": 467,
74
- "evidence": "return firstError?.message || \"Invalid expression\";",
75
- "suppressed": false,
76
- "advisory": false,
77
- "likelihood": "unmeasured",
78
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js"
79
- },
80
- {
81
- "rule": "javascript.loose-equality-and-falsy-defaults",
82
- "severity": "high",
83
- "line": 78,
84
- "evidence": "message: error.message || \"Validation failed\",",
85
- "suppressed": false,
86
- "advisory": false,
87
- "likelihood": "unmeasured",
88
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_symbol_service.js"
89
- },
90
- {
91
- "rule": "javascript.loose-equality-and-falsy-defaults",
92
- "severity": "high",
93
- "line": 198,
94
- "evidence": "if (!prefix || \"andornot\".includes(prefix)) {",
95
- "suppressed": false,
96
- "advisory": false,
97
- "likelihood": "unmeasured",
98
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/autocomplete.js"
99
- },
100
- {
101
- "rule": "javascript.loose-equality-and-falsy-defaults",
102
- "severity": "high",
103
- "line": 105,
104
- "evidence": "message: error.message || \"Unknown error\",",
105
- "suppressed": false,
106
- "advisory": false,
107
- "likelihood": "unmeasured",
108
- "path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/lint.js"
109
- },
110
- {
111
- "rule": "javascript.loose-equality-and-falsy-defaults",
112
- "severity": "high",
113
- "line": 143,
114
- "evidence": "currentTier: pendingReview.current_tier || 1,",
115
- "suppressed": false,
116
- "advisory": false,
117
- "likelihood": "unmeasured",
118
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js"
119
- },
120
- {
121
- "rule": "javascript.loose-equality-and-falsy-defaults",
122
- "severity": "high",
123
- "line": 144,
124
- "evidence": "totalTiers: pendingReview.tier_review_ids?.length || 1,",
125
- "suppressed": false,
126
- "advisory": false,
127
- "likelihood": "unmeasured",
128
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js"
129
- },
130
- {
131
- "rule": "javascript.number-parsing-is-lossy",
132
- "severity": "high",
133
- "line": 37,
134
- "evidence": "const docId = parseInt(el.dataset.docId, 10);",
135
- "suppressed": false,
136
- "advisory": false,
137
- "likelihood": "unmeasured",
138
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_review_documents.js"
139
- },
140
- {
141
- "rule": "javascript.number-parsing-is-lossy",
142
- "severity": "high",
143
- "line": 36,
144
- "evidence": "const partnerId = parseInt(row.dataset.partnerId, 10);",
145
- "suppressed": false,
146
- "advisory": false,
147
- "likelihood": "unmeasured",
148
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js"
149
- },
150
- {
151
- "rule": "javascript.number-parsing-is-lossy",
152
- "severity": "high",
153
- "line": 47,
154
- "evidence": "const page = parseInt(link.dataset.page);",
155
- "suppressed": false,
156
- "advisory": false,
157
- "likelihood": "unmeasured",
158
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js"
159
- },
160
- {
161
- "rule": "javascript.loose-equality-and-falsy-defaults",
162
- "severity": "high",
163
- "line": 27,
164
- "evidence": "name: params.name || \"Change Request Details\",",
165
- "suppressed": false,
166
- "advisory": false,
167
- "likelihood": "unmeasured",
168
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js"
169
- },
170
- {
171
- "rule": "javascript.loose-equality-and-falsy-defaults",
172
- "severity": "high",
173
- "line": 51,
174
- "evidence": "name: params.name || \"Change Request\",",
175
- "suppressed": false,
176
- "advisory": false,
177
- "likelihood": "unmeasured",
178
- "path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js"
179
- },
180
- {
181
- "rule": "javascript.number-parsing-is-lossy",
182
- "severity": "high",
183
- "line": 73,
184
- "evidence": "limit: limit ? parseInt(limit, 10) : null,",
185
- "suppressed": false,
186
- "advisory": false,
187
- "likelihood": "unmeasured",
188
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js"
189
- },
190
- {
191
- "rule": "javascript.number-parsing-is-lossy",
192
- "severity": "high",
193
- "line": 74,
194
- "evidence": "countLimit: countLimit ? parseInt(countLimit, 10) : null,",
195
- "suppressed": false,
196
- "advisory": false,
197
- "likelihood": "unmeasured",
198
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js"
199
- },
200
- {
201
- "rule": "javascript.json-parse-returns-any",
202
- "severity": "critical",
203
- "line": 397,
204
- "evidence": "defaultCenter = JSON.parse(editInfo.default_center);",
205
- "suppressed": false,
206
- "advisory": false,
207
- "likelihood": "unmeasured",
208
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
209
- },
210
- {
211
- "rule": "javascript.loose-equality-and-falsy-defaults",
212
- "severity": "high",
213
- "line": 606,
214
- "evidence": "return layer.begin_color || \"#3388ff\";",
215
- "suppressed": false,
216
- "advisory": false,
217
- "likelihood": "unmeasured",
218
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
219
- },
220
- {
221
- "rule": "javascript.loose-equality-and-falsy-defaults",
222
- "severity": "high",
223
- "line": 611,
224
- "evidence": "const maxValue = config.max_value || 100;",
225
- "suppressed": false,
226
- "advisory": false,
227
- "likelihood": "unmeasured",
228
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
229
- },
230
- {
231
- "rule": "javascript.loose-equality-and-falsy-defaults",
232
- "severity": "high",
233
- "line": 649,
234
- "evidence": "return layer.begin_color || \"#3388ff\";",
235
- "suppressed": false,
236
- "advisory": false,
237
- "likelihood": "unmeasured",
238
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
239
- },
240
- {
241
- "rule": "javascript.loose-equality-and-falsy-defaults",
242
- "severity": "high",
243
- "line": 665,
244
- "evidence": "const classCount = config.class_count || 5;",
245
- "suppressed": false,
246
- "advisory": false,
247
- "likelihood": "unmeasured",
248
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
249
- },
250
- {
251
- "rule": "javascript.loose-equality-and-falsy-defaults",
252
- "severity": "high",
253
- "line": 702,
254
- "evidence": "fillColor = layer.begin_color || \"#3388ff\";",
255
- "suppressed": false,
256
- "advisory": false,
257
- "likelihood": "unmeasured",
258
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
259
- },
260
- {
261
- "rule": "javascript.loose-equality-and-falsy-defaults",
262
- "severity": "high",
263
- "line": 913,
264
- "evidence": "fillColor = layer.begin_color || \"#3388ff\";",
265
- "suppressed": false,
266
- "advisory": false,
267
- "likelihood": "unmeasured",
268
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
269
- },
270
- {
271
- "rule": "javascript.loose-equality-and-falsy-defaults",
272
- "severity": "high",
273
- "line": 1040,
274
- "evidence": "const maxValue = config.max_value || 100;",
275
- "suppressed": false,
276
- "advisory": false,
277
- "likelihood": "unmeasured",
278
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
279
- },
280
- {
281
- "rule": "javascript.loose-equality-and-falsy-defaults",
282
- "severity": "high",
283
- "line": 1087,
284
- "evidence": "const classCount = config.class_count || 5;",
285
- "suppressed": false,
286
- "advisory": false,
287
- "likelihood": "unmeasured",
288
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
289
- },
290
- {
291
- "rule": "javascript.json-parse-returns-any",
292
- "severity": "critical",
293
- "line": 46,
294
- "evidence": "const default_center = JSON.parse(editInfo.default_center);",
295
- "suppressed": false,
296
- "advisory": false,
297
- "likelihood": "unmeasured",
298
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
299
- },
300
- {
301
- "rule": "javascript.json-parse-returns-any",
302
- "severity": "critical",
303
- "line": 96,
304
- "evidence": "const obj = JSON.parse(this.props.record.data[this.props.name]);",
305
- "suppressed": false,
306
- "advisory": false,
307
- "likelihood": "unmeasured",
308
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
309
- },
310
- {
311
- "rule": "javascript.json-parse-returns-any",
312
- "severity": "critical",
313
- "line": 140,
314
- "evidence": "const obj = JSON.parse(record.data[fieldName]);",
315
- "suppressed": false,
316
- "advisory": false,
317
- "likelihood": "unmeasured",
318
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
319
- },
320
- {
321
- "rule": "javascript.json-parse-returns-any",
322
- "severity": "critical",
323
- "line": 220,
324
- "evidence": "const geom = JSON.parse(this.props.record.data[this.props.name]);",
325
- "suppressed": false,
326
- "advisory": false,
327
- "likelihood": "unmeasured",
328
- "path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
329
- },
330
- {
331
- "rule": "javascript.number-parsing-is-lossy",
332
- "severity": "high",
333
- "line": 85,
334
- "evidence": "const matchId = parseInt(ev.target.value, 10) || false;",
335
- "suppressed": false,
336
- "advisory": false,
337
- "likelihood": "unmeasured",
338
- "path": "OpenSPP2-19.0/spp_import_match/static/src/import_match_selector/import_match_selector.js"
339
- },
340
- {
341
- "rule": "javascript.loose-equality-and-falsy-defaults",
342
- "severity": "high",
343
- "line": 117,
344
- "evidence": "return result || \"••••••••\";",
345
- "suppressed": false,
346
- "advisory": false,
347
- "likelihood": "unmeasured",
348
- "path": "OpenSPP2-19.0/spp_pii_encryption/static/src/js/masked_field.js"
349
- },
350
- {
351
- "rule": "javascript.loose-equality-and-falsy-defaults",
352
- "severity": "high",
353
- "line": 18,
354
- "evidence": "const programName = action.params?.name || \"Program\";",
355
- "suppressed": false,
356
- "advisory": false,
357
- "likelihood": "unmeasured",
358
- "path": "OpenSPP2-19.0/spp_programs/static/src/js/create_program.js"
359
- },
360
- {
361
- "rule": "javascript.loose-equality-and-falsy-defaults",
362
- "severity": "high",
363
- "line": 15,
364
- "evidence": "type: params.type || \"info\",",
365
- "suppressed": false,
366
- "advisory": false,
367
- "likelihood": "unmeasured",
368
- "path": "OpenSPP2-19.0/spp_registry/static/src/js/custom_client_action.js"
369
- },
370
- {
371
- "rule": "javascript.loose-equality-and-falsy-defaults",
372
- "severity": "high",
373
- "line": 89,
374
- "evidence": "this.state.searchMode = config.search_mode || \"unified\";",
375
- "suppressed": false,
376
- "advisory": false,
377
- "likelihood": "unmeasured",
378
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
379
- },
380
- {
381
- "rule": "javascript.loose-equality-and-falsy-defaults",
382
- "severity": "high",
383
- "line": 90,
384
- "evidence": "this.state.searchField = config.target_field || \"name\";",
385
- "suppressed": false,
386
- "advisory": false,
387
- "likelihood": "unmeasured",
388
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
389
- },
390
- {
391
- "rule": "javascript.loose-equality-and-falsy-defaults",
392
- "severity": "high",
393
- "line": 91,
394
- "evidence": "this.resultLimit = config.result_limit || 50;",
395
- "suppressed": false,
396
- "advisory": false,
397
- "likelihood": "unmeasured",
398
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
399
- },
400
- {
401
- "rule": "javascript.loose-equality-and-falsy-defaults",
402
- "severity": "high",
403
- "line": 92,
404
- "evidence": "this.minSearchChars = config.min_chars || 3;",
405
- "suppressed": false,
406
- "advisory": false,
407
- "likelihood": "unmeasured",
408
- "path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
409
- },
410
- {
411
- "rule": "javascript.loose-equality-and-falsy-defaults",
412
- "severity": "high",
413
- "line": 289,
414
- "evidence": "const scope = this.state.scopeFilter || \"all\";",
415
- "suppressed": false,
416
- "advisory": false,
417
- "likelihood": "unmeasured",
418
- "path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js"
419
- },
420
- {
421
- "rule": "javascript.loose-equality-and-falsy-defaults",
422
- "severity": "high",
423
- "line": 295,
424
- "evidence": "const applies = v.applies_to || \"both\";",
425
- "suppressed": false,
426
- "advisory": false,
427
- "likelihood": "unmeasured",
428
- "path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js"
429
- },
430
- {
431
- "rule": "javascript.loose-equality-and-falsy-defaults",
432
- "severity": "high",
433
- "line": 313,
434
- "evidence": "const applies = variable.applies_to || \"both\";",
435
- "suppressed": false,
436
- "advisory": false,
437
- "likelihood": "unmeasured",
438
- "path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js"
439
- },
440
- {
441
- "rule": "javascript.json-parse-returns-any",
442
- "severity": "critical",
443
- "line": 25,
444
- "evidence": "const excData = JSON.parse(originalError.data.message);",
445
- "suppressed": false,
446
- "advisory": false,
447
- "likelihood": "unmeasured",
448
- "path": "server-tools/base_exception/static/src/js/base_exception.esm.js"
449
- }
450
- ]
451
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fieldtest-oca-2026-09.json ADDED
@@ -0,0 +1,1260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "stack": "odoo",
3
+ "roots": [
4
+ "server-tools",
5
+ "account-financial-tools"
6
+ ],
7
+ "files": 606,
8
+ "lines": 33960,
9
+ "checkers": 26,
10
+ "triggers": {
11
+ "files": 879,
12
+ "models_declared": 139,
13
+ "models_deleted": 123,
14
+ "declared_and_deleted": 113,
15
+ "never_deleted": 26,
16
+ "multi_company": true
17
+ },
18
+ "hits": [
19
+ {
20
+ "rule": "odoo.sudo-bypasses-record-rules",
21
+ "severity": "critical",
22
+ "line": 71,
23
+ "evidence": "sudo().search() on ir.model runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
24
+ "suppressed": false,
25
+ "advisory": false,
26
+ "likelihood": "defect",
27
+ "path": "server-tools/attachment_delete_restrict/models/ir_attachment.py"
28
+ },
29
+ {
30
+ "rule": "odoo.ondelete-is-a-decision",
31
+ "severity": "high",
32
+ "line": 14,
33
+ "evidence": "task_id declares no ondelete, so the ORM defaults it to 'set null' -- and task_id.filepath on line 37 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete attachment.synchronize.task",
34
+ "suppressed": false,
35
+ "advisory": false,
36
+ "likelihood": "defect",
37
+ "path": "server-tools/attachment_synchronize/models/attachment_queue.py"
38
+ },
39
+ {
40
+ "rule": "odoo.ondelete-is-a-decision",
41
+ "severity": "high",
42
+ "line": 64,
43
+ "evidence": "backend_id declares no ondelete, so the ORM defaults it to 'set null' -- and backend_id.fs on line 166 reads through it, which yields False rather than raising once the parent is deleted",
44
+ "suppressed": false,
45
+ "advisory": false,
46
+ "likelihood": "construct",
47
+ "path": "server-tools/attachment_synchronize/models/attachment_synchronize_task.py"
48
+ },
49
+ {
50
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
51
+ "severity": "medium",
52
+ "line": 25,
53
+ "evidence": "deadline = datetime.now() - timedelta(days=days)",
54
+ "suppressed": false,
55
+ "advisory": false,
56
+ "likelihood": "unmeasured",
57
+ "path": "server-tools/auditlog/models/autovacuum.py"
58
+ },
59
+ {
60
+ "rule": "odoo.stored-compute-needs-complete-depends",
61
+ "severity": "critical",
62
+ "line": 29,
63
+ "evidence": "_compute_display_name() reads 'user_id.name' but @api.depends does not declare it -- the stored value goes stale when 'user_id.name' changes",
64
+ "suppressed": false,
65
+ "advisory": false,
66
+ "likelihood": "construct",
67
+ "path": "server-tools/auditlog/models/http_session.py"
68
+ },
69
+ {
70
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
71
+ "severity": "medium",
72
+ "line": 146,
73
+ "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)",
74
+ "suppressed": false,
75
+ "advisory": false,
76
+ "likelihood": "unmeasured",
77
+ "path": "server-tools/auto_backup/models/db_backup.py"
78
+ },
79
+ {
80
+ "rule": "python.path-join-does-not-contain-a-path",
81
+ "severity": "high",
82
+ "line": 154,
83
+ "evidence": "with open(os.path.join(rec.folder, filename), \"wb\") as destiny:",
84
+ "suppressed": false,
85
+ "advisory": false,
86
+ "likelihood": "unmeasured",
87
+ "path": "server-tools/auto_backup/models/db_backup.py"
88
+ },
89
+ {
90
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
91
+ "severity": "medium",
92
+ "line": 171,
93
+ "evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)",
94
+ "suppressed": false,
95
+ "advisory": false,
96
+ "likelihood": "unmeasured",
97
+ "path": "server-tools/auto_backup/models/db_backup.py"
98
+ },
99
+ {
100
+ "rule": "python.path-join-does-not-contain-a-path",
101
+ "severity": "high",
102
+ "line": 186,
103
+ "evidence": "os.path.join(rec.folder, filename), \"wb\"",
104
+ "suppressed": false,
105
+ "advisory": false,
106
+ "likelihood": "unmeasured",
107
+ "path": "server-tools/auto_backup/models/db_backup.py"
108
+ },
109
+ {
110
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
111
+ "severity": "medium",
112
+ "line": 218,
113
+ "evidence": "now = datetime.now()",
114
+ "suppressed": false,
115
+ "advisory": false,
116
+ "likelihood": "unmeasured",
117
+ "path": "server-tools/auto_backup/models/db_backup.py"
118
+ },
119
+ {
120
+ "rule": "odoo.constrains-must-list-every-field-read",
121
+ "severity": "critical",
122
+ "line": 100,
123
+ "evidence": "_check_inheriting_model() reads 'ttype' but @api.constrains declares only inheriting_model -- writing 'ttype' alone skips the check entirely",
124
+ "suppressed": false,
125
+ "advisory": false,
126
+ "likelihood": "unmeasured",
127
+ "path": "server-tools/autovacuum_message_attachment/models/vacuum_rule.py"
128
+ },
129
+ {
130
+ "rule": "odoo.constrains-must-list-every-field-read",
131
+ "severity": "critical",
132
+ "line": 59,
133
+ "evidence": "check_exception_type_consistency() reads 'method' but @api.constrains declares only code, domain, exception_type, model -- writing 'method' alone skips the check entirely",
134
+ "suppressed": false,
135
+ "advisory": false,
136
+ "likelihood": "unmeasured",
137
+ "path": "server-tools/base_exception/models/exception_rule.py"
138
+ },
139
+ {
140
+ "rule": "odoo.sql-needs-query-parameters",
141
+ "severity": "critical",
142
+ "line": 47,
143
+ "evidence": "the query handed to cr.execute() is built by an f-string -- pass the values as the second argument so the driver quotes them",
144
+ "suppressed": true,
145
+ "advisory": false,
146
+ "likelihood": "unmeasured",
147
+ "path": "server-tools/base_multi_image/hooks.py"
148
+ },
149
+ {
150
+ "rule": "odoo.depends-must-not-name-unread-fields",
151
+ "severity": "medium",
152
+ "line": 103,
153
+ "evidence": "_compute_show_technical() declares 'owner_id', 'owner_model' in @api.depends and never reads them -- the value recomputes on writes that cannot change it, and the decorator stops describing the method",
154
+ "suppressed": false,
155
+ "advisory": false,
156
+ "likelihood": "unmeasured",
157
+ "path": "server-tools/base_multi_image/models/image.py"
158
+ },
159
+ {
160
+ "rule": "odoo.sudo-bypasses-record-rules",
161
+ "severity": "critical",
162
+ "line": 95,
163
+ "evidence": "sudo().search() runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
164
+ "suppressed": false,
165
+ "advisory": false,
166
+ "likelihood": "defect",
167
+ "path": "server-tools/base_sequence_option/models/ir_sequence_option.py"
168
+ },
169
+ {
170
+ "rule": "python.assert-is-not-validation",
171
+ "severity": "critical",
172
+ "line": 19,
173
+ "evidence": "assert type(date) is datetime.date or date is None, \"Wrong date\"",
174
+ "suppressed": false,
175
+ "advisory": false,
176
+ "likelihood": "unmeasured",
177
+ "path": "server-tools/base_time_parameter/models/base.py"
178
+ },
179
+ {
180
+ "rule": "odoo.sql-needs-query-parameters",
181
+ "severity": "critical",
182
+ "line": 24,
183
+ "evidence": "the query handed to cr.execute() is built by an f-string -- pass the values as the second argument so the driver quotes them",
184
+ "suppressed": false,
185
+ "advisory": false,
186
+ "likelihood": "unmeasured",
187
+ "path": "server-tools/database_autovacuum_tuning/models/database_autovacuum_tuning.py"
188
+ },
189
+ {
190
+ "rule": "odoo.commit-inside-a-loop-splits-the-transaction",
191
+ "severity": "high",
192
+ "line": 58,
193
+ "evidence": "cr.commit() inside a loop -- an exception on a later iteration leaves the earlier ones written, and the caller cannot roll the batch back",
194
+ "suppressed": true,
195
+ "advisory": false,
196
+ "likelihood": "unmeasured",
197
+ "path": "server-tools/database_cleanup/models/purge_columns.py"
198
+ },
199
+ {
200
+ "rule": "odoo.sudo-bypasses-record-rules",
201
+ "severity": "critical",
202
+ "line": 87,
203
+ "evidence": "sudo().search() on ir.model runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
204
+ "suppressed": false,
205
+ "advisory": false,
206
+ "likelihood": "defect",
207
+ "path": "server-tools/database_size/models/ir_model_size.py"
208
+ },
209
+ {
210
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
211
+ "severity": "medium",
212
+ "line": 268,
213
+ "evidence": "stamp = dt.utcnow().strftime(\"%H%M%S%f\")[:-3]",
214
+ "suppressed": false,
215
+ "advisory": false,
216
+ "likelihood": "unmeasured",
217
+ "path": "server-tools/excel_import_export/models/xlsx_export.py"
218
+ },
219
+ {
220
+ "rule": "odoo.ondelete-is-a-decision",
221
+ "severity": "high",
222
+ "line": 108,
223
+ "evidence": "result_model_id declares no ondelete, so the ORM defaults it to 'set null' -- and result_model_id.model on line 199 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete ir.model",
224
+ "suppressed": false,
225
+ "advisory": false,
226
+ "likelihood": "defect",
227
+ "path": "server-tools/excel_import_export/models/xlsx_template.py"
228
+ },
229
+ {
230
+ "rule": "odoo.ondelete-is-a-decision",
231
+ "severity": "high",
232
+ "line": 25,
233
+ "evidence": "server_id declares no ondelete, so the ORM defaults it to 'set null' -- and server_id.connect on line 139 reads through it, which yields False rather than raising once the parent is deleted",
234
+ "suppressed": false,
235
+ "advisory": false,
236
+ "likelihood": "construct",
237
+ "path": "server-tools/fetchmail_attach_from_folder/models/fetchmail_server_folder.py"
238
+ },
239
+ {
240
+ "rule": "python.assert-is-not-validation",
241
+ "severity": "critical",
242
+ "line": 42,
243
+ "evidence": "assert len(param) == len(records)",
244
+ "suppressed": false,
245
+ "advisory": false,
246
+ "likelihood": "unmeasured",
247
+ "path": "server-tools/jsonifier/models/ir_exports_resolver.py"
248
+ },
249
+ {
250
+ "rule": "python.assert-is-not-validation",
251
+ "severity": "critical",
252
+ "line": 3,
253
+ "evidence": "assert isinstance(parser, list)",
254
+ "suppressed": false,
255
+ "advisory": false,
256
+ "likelihood": "unmeasured",
257
+ "path": "server-tools/jsonifier/models/utils.py"
258
+ },
259
+ {
260
+ "rule": "odoo.sudo-bypasses-record-rules",
261
+ "severity": "critical",
262
+ "line": 34,
263
+ "evidence": "sudo().search() runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
264
+ "suppressed": false,
265
+ "advisory": false,
266
+ "likelihood": "defect",
267
+ "path": "server-tools/tracking_manager/models/ir_model.py"
268
+ },
269
+ {
270
+ "rule": "odoo.depends-must-not-name-unread-fields",
271
+ "severity": "medium",
272
+ "line": 101,
273
+ "evidence": "_compute_automatic_custom_tracking() declares 'active_custom_tracking' in @api.depends and never reads it -- the value recomputes on writes that cannot change it, and the decorator stops describing the method",
274
+ "suppressed": false,
275
+ "advisory": false,
276
+ "likelihood": "unmeasured",
277
+ "path": "server-tools/tracking_manager/models/ir_model.py"
278
+ },
279
+ {
280
+ "rule": "odoo.stored-compute-needs-complete-depends",
281
+ "severity": "critical",
282
+ "line": 135,
283
+ "evidence": "_compute_automatic_custom_tracking_domain() reads 'model' but @api.depends does not declare it -- the stored value goes stale when 'model' changes",
284
+ "suppressed": false,
285
+ "advisory": false,
286
+ "likelihood": "construct",
287
+ "path": "server-tools/tracking_manager/models/ir_model.py"
288
+ },
289
+ {
290
+ "rule": "odoo.stored-compute-needs-complete-depends",
291
+ "severity": "critical",
292
+ "line": 30,
293
+ "evidence": "_compute_custom_tracking() reads 'model_id' but @api.depends does not declare it -- the stored value goes stale when 'model_id' changes",
294
+ "suppressed": false,
295
+ "advisory": false,
296
+ "likelihood": "construct",
297
+ "path": "server-tools/tracking_manager/models/ir_model_fields.py"
298
+ },
299
+ {
300
+ "rule": "odoo.stored-compute-needs-complete-depends",
301
+ "severity": "critical",
302
+ "line": 30,
303
+ "evidence": "_compute_custom_tracking() reads 'model_id.automatic_custom_tracking' but @api.depends does not declare it -- the stored value goes stale when 'model_id.automatic_custom_tracking' changes",
304
+ "suppressed": false,
305
+ "advisory": false,
306
+ "likelihood": "construct",
307
+ "path": "server-tools/tracking_manager/models/ir_model_fields.py"
308
+ },
309
+ {
310
+ "rule": "odoo.stored-compute-needs-complete-depends",
311
+ "severity": "critical",
312
+ "line": 31,
313
+ "evidence": "_compute_custom_tracking() reads 'model_id.automatic_custom_tracking_domain' but @api.depends does not declare it -- the stored value goes stale when 'model_id.automatic_custom_tracking_domain' changes",
314
+ "suppressed": false,
315
+ "advisory": false,
316
+ "likelihood": "construct",
317
+ "path": "server-tools/tracking_manager/models/ir_model_fields.py"
318
+ },
319
+ {
320
+ "rule": "odoo.stored-compute-needs-complete-depends",
321
+ "severity": "critical",
322
+ "line": 52,
323
+ "evidence": "_compute_trackable() reads 'name' but @api.depends does not declare it -- the stored value goes stale when 'name' changes",
324
+ "suppressed": false,
325
+ "advisory": false,
326
+ "likelihood": "construct",
327
+ "path": "server-tools/tracking_manager/models/ir_model_fields.py"
328
+ },
329
+ {
330
+ "rule": "python.path-join-does-not-contain-a-path",
331
+ "severity": "high",
332
+ "line": 106,
333
+ "evidence": "logfile = os.path.join(full_path, filename)",
334
+ "suppressed": false,
335
+ "advisory": false,
336
+ "likelihood": "unmeasured",
337
+ "path": "server-tools/upgrade_analysis/models/upgrade_analysis.py"
338
+ },
339
+ {
340
+ "rule": "odoo.stored-compute-needs-complete-depends",
341
+ "severity": "critical",
342
+ "line": 60,
343
+ "evidence": "_compute_account_asset_id() reads 'state' but @api.depends does not declare it -- the stored value goes stale when 'state' changes",
344
+ "suppressed": false,
345
+ "advisory": false,
346
+ "likelihood": "construct",
347
+ "path": "account-financial-tools/account_asset_force_account/models/account_asset.py"
348
+ },
349
+ {
350
+ "rule": "odoo.stored-compute-needs-complete-depends",
351
+ "severity": "critical",
352
+ "line": 64,
353
+ "evidence": "_compute_account_asset_id() reads 'account_move_line_ids.account_id' but @api.depends does not declare it -- the stored value goes stale when 'account_move_line_ids.account_id' changes",
354
+ "suppressed": false,
355
+ "advisory": false,
356
+ "likelihood": "construct",
357
+ "path": "account-financial-tools/account_asset_force_account/models/account_asset.py"
358
+ },
359
+ {
360
+ "rule": "odoo.stored-compute-needs-complete-depends",
361
+ "severity": "critical",
362
+ "line": 72,
363
+ "evidence": "_compute_account_asset_id() reads 'profile_id.account_asset_id' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.account_asset_id' changes",
364
+ "suppressed": false,
365
+ "advisory": false,
366
+ "likelihood": "construct",
367
+ "path": "account-financial-tools/account_asset_force_account/models/account_asset.py"
368
+ },
369
+ {
370
+ "rule": "odoo.stored-compute-needs-complete-depends",
371
+ "severity": "critical",
372
+ "line": 311,
373
+ "evidence": "_compute_group_ids() reads 'profile_id.group_ids' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.group_ids' changes",
374
+ "suppressed": false,
375
+ "advisory": false,
376
+ "likelihood": "construct",
377
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
378
+ },
379
+ {
380
+ "rule": "odoo.stored-compute-needs-complete-depends",
381
+ "severity": "critical",
382
+ "line": 316,
383
+ "evidence": "_compute_method() reads 'profile_id.method' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method' changes",
384
+ "suppressed": false,
385
+ "advisory": false,
386
+ "likelihood": "construct",
387
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
388
+ },
389
+ {
390
+ "rule": "odoo.stored-compute-needs-complete-depends",
391
+ "severity": "critical",
392
+ "line": 324,
393
+ "evidence": "_compute_method_number() reads 'profile_id.method_number' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_number' changes",
394
+ "suppressed": false,
395
+ "advisory": false,
396
+ "likelihood": "construct",
397
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
398
+ },
399
+ {
400
+ "rule": "odoo.stored-compute-needs-complete-depends",
401
+ "severity": "critical",
402
+ "line": 329,
403
+ "evidence": "_compute_method_period() reads 'profile_id.method_period' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_period' changes",
404
+ "suppressed": false,
405
+ "advisory": false,
406
+ "likelihood": "construct",
407
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
408
+ },
409
+ {
410
+ "rule": "odoo.stored-compute-needs-complete-depends",
411
+ "severity": "critical",
412
+ "line": 340,
413
+ "evidence": "_compute_method_progress_factor() reads 'profile_id.method_progress_factor' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_progress_factor' changes",
414
+ "suppressed": false,
415
+ "advisory": false,
416
+ "likelihood": "construct",
417
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
418
+ },
419
+ {
420
+ "rule": "odoo.stored-compute-needs-complete-depends",
421
+ "severity": "critical",
422
+ "line": 345,
423
+ "evidence": "_compute_method_time() reads 'profile_id.method_time' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_time' changes",
424
+ "suppressed": false,
425
+ "advisory": false,
426
+ "likelihood": "construct",
427
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
428
+ },
429
+ {
430
+ "rule": "odoo.stored-compute-needs-complete-depends",
431
+ "severity": "critical",
432
+ "line": 350,
433
+ "evidence": "_compute_days_calc() reads 'profile_id.days_calc' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.days_calc' changes",
434
+ "suppressed": false,
435
+ "advisory": false,
436
+ "likelihood": "construct",
437
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
438
+ },
439
+ {
440
+ "rule": "odoo.stored-compute-needs-complete-depends",
441
+ "severity": "critical",
442
+ "line": 355,
443
+ "evidence": "_compute_use_leap_years() reads 'profile_id.use_leap_years' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.use_leap_years' changes",
444
+ "suppressed": false,
445
+ "advisory": false,
446
+ "likelihood": "construct",
447
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
448
+ },
449
+ {
450
+ "rule": "odoo.stored-compute-needs-complete-depends",
451
+ "severity": "critical",
452
+ "line": 363,
453
+ "evidence": "_compute_prorrata() reads 'profile_id.prorata' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.prorata' changes",
454
+ "suppressed": false,
455
+ "advisory": false,
456
+ "likelihood": "construct",
457
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
458
+ },
459
+ {
460
+ "rule": "odoo.stored-compute-needs-complete-depends",
461
+ "severity": "critical",
462
+ "line": 368,
463
+ "evidence": "_compute_account_analytic_id() reads 'profile_id.account_analytic_id' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.account_analytic_id' changes",
464
+ "suppressed": false,
465
+ "advisory": false,
466
+ "likelihood": "construct",
467
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
468
+ },
469
+ {
470
+ "rule": "odoo.stored-compute-needs-complete-depends",
471
+ "severity": "critical",
472
+ "line": 373,
473
+ "evidence": "_compute_analytic_distribution() reads 'profile_id.analytic_distribution' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.analytic_distribution' changes",
474
+ "suppressed": false,
475
+ "advisory": false,
476
+ "likelihood": "construct",
477
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
478
+ },
479
+ {
480
+ "rule": "odoo.constrains-must-list-every-field-read",
481
+ "severity": "critical",
482
+ "line": 398,
483
+ "evidence": "_check_profile_change() reads 'depreciation_line_ids' but @api.constrains declares only profile_id -- writing 'depreciation_line_ids' alone skips the check entirely",
484
+ "suppressed": false,
485
+ "advisory": false,
486
+ "likelihood": "unmeasured",
487
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
488
+ },
489
+ {
490
+ "rule": "odoo.sudo-bypasses-record-rules",
491
+ "severity": "critical",
492
+ "line": 1190,
493
+ "evidence": "sudo().search() runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
494
+ "suppressed": false,
495
+ "advisory": false,
496
+ "likelihood": "defect",
497
+ "path": "account-financial-tools/account_asset_management/models/account_asset.py"
498
+ },
499
+ {
500
+ "rule": "odoo.recursion-check-on-hierarchy",
501
+ "severity": "high",
502
+ "line": 27,
503
+ "evidence": "parent_id = fields.Many2one(",
504
+ "suppressed": false,
505
+ "advisory": false,
506
+ "likelihood": "unmeasured",
507
+ "path": "account-financial-tools/account_asset_management/models/account_asset_group.py"
508
+ },
509
+ {
510
+ "rule": "odoo.ondelete-is-a-decision",
511
+ "severity": "high",
512
+ "line": 174,
513
+ "evidence": "asset_profile_id declares no ondelete, so the ORM defaults it to 'set null' -- and asset_profile_id.account_asset_id on line 200 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete account.asset.profile",
514
+ "suppressed": false,
515
+ "advisory": false,
516
+ "likelihood": "defect",
517
+ "path": "account-financial-tools/account_asset_management/models/account_move.py"
518
+ },
519
+ {
520
+ "rule": "odoo.stored-compute-needs-complete-depends",
521
+ "severity": "critical",
522
+ "line": 192,
523
+ "evidence": "_compute_asset_profile() reads 'account_id.asset_profile_id' but @api.depends does not declare it -- the stored value goes stale when 'account_id.asset_profile_id' changes",
524
+ "suppressed": false,
525
+ "advisory": false,
526
+ "likelihood": "construct",
527
+ "path": "account-financial-tools/account_asset_management/models/account_move.py"
528
+ },
529
+ {
530
+ "rule": "odoo.stored-compute-needs-complete-depends",
531
+ "severity": "critical",
532
+ "line": 195,
533
+ "evidence": "_compute_asset_profile() reads 'asset_id.profile_id' but @api.depends does not declare it -- the stored value goes stale when 'asset_id.profile_id' changes",
534
+ "suppressed": false,
535
+ "advisory": false,
536
+ "likelihood": "construct",
537
+ "path": "account-financial-tools/account_asset_management/models/account_move.py"
538
+ },
539
+ {
540
+ "rule": "odoo.constrains-iterate-recordset",
541
+ "severity": "high",
542
+ "line": 80,
543
+ "evidence": "@api.constrains(\"sale_value\", \"company_id\")",
544
+ "suppressed": false,
545
+ "advisory": false,
546
+ "likelihood": "unmeasured",
547
+ "path": "account-financial-tools/account_asset_management/wizard/account_asset_remove.py"
548
+ },
549
+ {
550
+ "rule": "odoo.constrains-must-list-every-field-read",
551
+ "severity": "critical",
552
+ "line": 130,
553
+ "evidence": "_check_deposit() reads 'company_id' but @api.constrains declares only cash_journal_id, currency_id -- writing 'company_id' alone skips the check entirely",
554
+ "suppressed": false,
555
+ "advisory": false,
556
+ "likelihood": "unmeasured",
557
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
558
+ },
559
+ {
560
+ "rule": "odoo.stored-compute-needs-complete-depends",
561
+ "severity": "critical",
562
+ "line": 203,
563
+ "evidence": "_compute_is_reconcile() reads 'cash_journal_id' but @api.depends does not declare it -- the stored value goes stale when 'cash_journal_id' changes",
564
+ "suppressed": false,
565
+ "advisory": false,
566
+ "likelihood": "construct",
567
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
568
+ },
569
+ {
570
+ "rule": "odoo.stored-compute-needs-complete-depends",
571
+ "severity": "critical",
572
+ "line": 203,
573
+ "evidence": "_compute_is_reconcile() reads 'cash_journal_id.default_account_id' but @api.depends does not declare it -- the stored value goes stale when 'cash_journal_id.default_account_id' changes",
574
+ "suppressed": false,
575
+ "advisory": false,
576
+ "likelihood": "construct",
577
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
578
+ },
579
+ {
580
+ "rule": "odoo.stored-compute-needs-complete-depends",
581
+ "severity": "critical",
582
+ "line": 203,
583
+ "evidence": "_compute_is_reconcile() reads 'move_id.line_ids.account_id' but @api.depends does not declare it -- the stored value goes stale when 'move_id.line_ids.account_id' changes",
584
+ "suppressed": false,
585
+ "advisory": false,
586
+ "likelihood": "construct",
587
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
588
+ },
589
+ {
590
+ "rule": "python.assert-is-not-validation",
591
+ "severity": "critical",
592
+ "line": 265,
593
+ "evidence": "assert self.operation_type == \"order\", \"Wrong operation type\"",
594
+ "suppressed": false,
595
+ "advisory": false,
596
+ "likelihood": "unmeasured",
597
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
598
+ },
599
+ {
600
+ "rule": "odoo.float-comparison-needs-float-utils",
601
+ "severity": "high",
602
+ "line": 271,
603
+ "evidence": "self.line_ids.filtered(lambda x: x.qty == 0).unlink()",
604
+ "suppressed": false,
605
+ "advisory": false,
606
+ "likelihood": "unmeasured",
607
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
608
+ },
609
+ {
610
+ "rule": "odoo.recursion-check-on-hierarchy",
611
+ "severity": "high",
612
+ "line": 381,
613
+ "evidence": "parent_id = fields.Many2one(\"account.cash.deposit\", ondelete=\"cascade\")",
614
+ "suppressed": false,
615
+ "advisory": false,
616
+ "likelihood": "unmeasured",
617
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
618
+ },
619
+ {
620
+ "rule": "odoo.stored-compute-needs-complete-depends",
621
+ "severity": "critical",
622
+ "line": 420,
623
+ "evidence": "_compute_subtotal() reads 'cash_unit_id.total_value' but @api.depends does not declare it -- the stored value goes stale when 'cash_unit_id.total_value' changes",
624
+ "suppressed": false,
625
+ "advisory": false,
626
+ "likelihood": "construct",
627
+ "path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
628
+ },
629
+ {
630
+ "rule": "odoo.prefer-sql-constraint",
631
+ "severity": "high",
632
+ "line": 68,
633
+ "evidence": "_check_cash_unit() compares its own declared column against a literal and does nothing else -- the same rule as a CHECK in models.Constraint holds for every writer, including imports, XML-RPC and sudo",
634
+ "suppressed": false,
635
+ "advisory": true,
636
+ "likelihood": "preference",
637
+ "path": "account-financial-tools/account_cash_deposit/models/cash_unit.py"
638
+ },
639
+ {
640
+ "rule": "odoo.stored-compute-needs-complete-depends",
641
+ "severity": "critical",
642
+ "line": 114,
643
+ "evidence": "_compute_display_name() reads 'total_value' but @api.depends does not declare it -- the stored value goes stale when 'total_value' changes",
644
+ "suppressed": false,
645
+ "advisory": false,
646
+ "likelihood": "construct",
647
+ "path": "account-financial-tools/account_cash_deposit/models/cash_unit.py"
648
+ },
649
+ {
650
+ "rule": "python.assert-is-not-validation",
651
+ "severity": "critical",
652
+ "line": 27,
653
+ "evidence": "assert self._context.get(\"active_model\") == \"account.cash.deposit\"",
654
+ "suppressed": false,
655
+ "advisory": false,
656
+ "likelihood": "unmeasured",
657
+ "path": "account-financial-tools/account_cash_deposit/wizards/account_cash_order_reception.py"
658
+ },
659
+ {
660
+ "rule": "odoo.check-company-on-relations",
661
+ "severity": "critical",
662
+ "line": 41,
663
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
664
+ "suppressed": false,
665
+ "advisory": false,
666
+ "likelihood": "defect",
667
+ "path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
668
+ },
669
+ {
670
+ "rule": "odoo.stored-compute-needs-complete-depends",
671
+ "severity": "critical",
672
+ "line": 132,
673
+ "evidence": "_compute_check_deposit() reads 'company_id.currency_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.currency_id' changes",
674
+ "suppressed": false,
675
+ "advisory": false,
676
+ "likelihood": "construct",
677
+ "path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
678
+ },
679
+ {
680
+ "rule": "odoo.stored-compute-needs-complete-depends",
681
+ "severity": "critical",
682
+ "line": 145,
683
+ "evidence": "_compute_currency_id() reads 'company_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id' changes",
684
+ "suppressed": false,
685
+ "advisory": false,
686
+ "likelihood": "construct",
687
+ "path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
688
+ },
689
+ {
690
+ "rule": "odoo.stored-compute-needs-complete-depends",
691
+ "severity": "critical",
692
+ "line": 146,
693
+ "evidence": "_compute_currency_id() reads 'journal_id.currency_id' but @api.depends does not declare it -- the stored value goes stale when 'journal_id.currency_id' changes",
694
+ "suppressed": false,
695
+ "advisory": false,
696
+ "likelihood": "construct",
697
+ "path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
698
+ },
699
+ {
700
+ "rule": "odoo.stored-compute-needs-complete-depends",
701
+ "severity": "critical",
702
+ "line": 151,
703
+ "evidence": "_compute_in_hand_check_account_id() reads 'journal_id.inbound_payment_method_line_ids' but @api.depends does not declare it -- the stored value goes stale when 'journal_id.inbound_payment_method_line_ids' changes",
704
+ "suppressed": false,
705
+ "advisory": false,
706
+ "likelihood": "construct",
707
+ "path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
708
+ },
709
+ {
710
+ "rule": "python.assert-is-not-validation",
711
+ "severity": "critical",
712
+ "line": 289,
713
+ "evidence": "assert sign in (1, -1)",
714
+ "suppressed": false,
715
+ "advisory": false,
716
+ "likelihood": "unmeasured",
717
+ "path": "account-financial-tools/account_dashboard_banner/models/account_dashboard_banner_cell.py"
718
+ },
719
+ {
720
+ "rule": "odoo.ondelete-is-a-decision",
721
+ "severity": "high",
722
+ "line": 17,
723
+ "evidence": "mis_report_instance_id declares no ondelete, so the ORM defaults it to 'set null' -- and mis_report_instance_id.currency_id on line 105 reads through it, which yields False rather than raising once the parent is deleted",
724
+ "suppressed": false,
725
+ "advisory": false,
726
+ "likelihood": "construct",
727
+ "path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
728
+ },
729
+ {
730
+ "rule": "odoo.ondelete-is-a-decision",
731
+ "severity": "high",
732
+ "line": 35,
733
+ "evidence": "mis_report_instance_period_id declares no ondelete, so the ORM defaults it to 'set null' -- and mis_report_instance_period_id.date_from on line 103 reads through it, which yields False rather than raising once the parent is deleted",
734
+ "suppressed": false,
735
+ "advisory": false,
736
+ "likelihood": "construct",
737
+ "path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
738
+ },
739
+ {
740
+ "rule": "odoo.constrains-must-list-every-field-read",
741
+ "severity": "critical",
742
+ "line": 47,
743
+ "evidence": "_check_mis_builder() reads 'cell_type' but @api.constrains declares only mis_report_instance_id, mis_report_kpi_id -- writing 'cell_type' alone skips the check entirely",
744
+ "suppressed": false,
745
+ "advisory": false,
746
+ "likelihood": "unmeasured",
747
+ "path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
748
+ },
749
+ {
750
+ "rule": "odoo.constrains-must-list-every-field-read",
751
+ "severity": "critical",
752
+ "line": 74,
753
+ "evidence": "_check_mis_builder() reads 'mis_report_instance_period_id' but @api.constrains declares only mis_report_instance_id, mis_report_kpi_id -- writing 'mis_report_instance_period_id' alone skips the check entirely",
754
+ "suppressed": false,
755
+ "advisory": false,
756
+ "likelihood": "unmeasured",
757
+ "path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
758
+ },
759
+ {
760
+ "rule": "odoo.prefer-sql-constraint",
761
+ "severity": "high",
762
+ "line": 46,
763
+ "evidence": "_check_dates() tests uniqueness in Python -- two concurrent transactions can both pass it and both insert; a UNIQUE models.Constraint cannot race",
764
+ "suppressed": false,
765
+ "advisory": true,
766
+ "likelihood": "preference",
767
+ "path": "account-financial-tools/account_fiscal_year/models/account_fiscal_year.py"
768
+ },
769
+ {
770
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
771
+ "severity": "medium",
772
+ "line": 23,
773
+ "evidence": "last_fiscal_year.date_to < datetime.now().date() + relativedelta(days=1)",
774
+ "suppressed": false,
775
+ "advisory": false,
776
+ "likelihood": "unmeasured",
777
+ "path": "account-financial-tools/account_fiscal_year_auto_create/models/account_fiscal_year.py"
778
+ },
779
+ {
780
+ "rule": "odoo.stored-compute-needs-complete-depends",
781
+ "severity": "critical",
782
+ "line": 42,
783
+ "evidence": "_compute_entry_number_sequence() reads 'company_id.name' but @api.depends does not declare it -- the stored value goes stale when 'company_id.name' changes",
784
+ "suppressed": false,
785
+ "advisory": false,
786
+ "likelihood": "construct",
787
+ "path": "account-financial-tools/account_journal_general_sequence/models/account_journal.py"
788
+ },
789
+ {
790
+ "rule": "odoo.check-company-on-relations",
791
+ "severity": "critical",
792
+ "line": 144,
793
+ "evidence": "relation to account.journal with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.journal, and this codebase runs multi-company",
794
+ "suppressed": false,
795
+ "advisory": false,
796
+ "likelihood": "defect",
797
+ "path": "account-financial-tools/account_loan/models/account_loan.py"
798
+ },
799
+ {
800
+ "rule": "odoo.check-company-on-relations",
801
+ "severity": "critical",
802
+ "line": 149,
803
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
804
+ "suppressed": false,
805
+ "advisory": false,
806
+ "likelihood": "defect",
807
+ "path": "account-financial-tools/account_loan/models/account_loan.py"
808
+ },
809
+ {
810
+ "rule": "odoo.check-company-on-relations",
811
+ "severity": "critical",
812
+ "line": 156,
813
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
814
+ "suppressed": false,
815
+ "advisory": false,
816
+ "likelihood": "defect",
817
+ "path": "account-financial-tools/account_loan/models/account_loan.py"
818
+ },
819
+ {
820
+ "rule": "odoo.check-company-on-relations",
821
+ "severity": "critical",
822
+ "line": 162,
823
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
824
+ "suppressed": false,
825
+ "advisory": false,
826
+ "likelihood": "defect",
827
+ "path": "account-financial-tools/account_loan/models/account_loan.py"
828
+ },
829
+ {
830
+ "rule": "odoo.check-company-on-relations",
831
+ "severity": "critical",
832
+ "line": 170,
833
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
834
+ "suppressed": false,
835
+ "advisory": false,
836
+ "likelihood": "defect",
837
+ "path": "account-financial-tools/account_loan/models/account_loan.py"
838
+ },
839
+ {
840
+ "rule": "odoo.depends-must-not-name-unread-fields",
841
+ "severity": "medium",
842
+ "line": 246,
843
+ "evidence": "_compute_total_amounts() declares 'currency_id' in @api.depends and never reads it -- the value recomputes on writes that cannot change it, and the decorator stops describing the method",
844
+ "suppressed": false,
845
+ "advisory": false,
846
+ "likelihood": "unmeasured",
847
+ "path": "account-financial-tools/account_loan/models/account_loan.py"
848
+ },
849
+ {
850
+ "rule": "odoo.stored-compute-needs-complete-depends",
851
+ "severity": "critical",
852
+ "line": 132,
853
+ "evidence": "_compute_rate() reads 'pending_principal_amount' but @api.depends does not declare it -- the stored value goes stale when 'pending_principal_amount' changes",
854
+ "suppressed": false,
855
+ "advisory": false,
856
+ "likelihood": "construct",
857
+ "path": "account-financial-tools/account_loan/models/account_loan_line.py"
858
+ },
859
+ {
860
+ "rule": "odoo.stored-compute-needs-complete-depends",
861
+ "severity": "critical",
862
+ "line": 139,
863
+ "evidence": "_compute_interests_amount() reads 'pending_principal_amount' but @api.depends does not declare it -- the stored value goes stale when 'pending_principal_amount' changes",
864
+ "suppressed": false,
865
+ "advisory": false,
866
+ "likelihood": "construct",
867
+ "path": "account-financial-tools/account_loan/models/account_loan_line.py"
868
+ },
869
+ {
870
+ "rule": "odoo.stored-compute-needs-complete-depends",
871
+ "severity": "critical",
872
+ "line": 174,
873
+ "evidence": "_compute_principal_amount() reads 'interests_amount' but @api.depends does not declare it -- the stored value goes stale when 'interests_amount' changes",
874
+ "suppressed": false,
875
+ "advisory": false,
876
+ "likelihood": "construct",
877
+ "path": "account-financial-tools/account_loan/models/account_loan_line.py"
878
+ },
879
+ {
880
+ "rule": "odoo.stored-compute-needs-complete-depends",
881
+ "severity": "critical",
882
+ "line": 174,
883
+ "evidence": "_compute_principal_amount() reads 'payment_amount' but @api.depends does not declare it -- the stored value goes stale when 'payment_amount' changes",
884
+ "suppressed": false,
885
+ "advisory": false,
886
+ "likelihood": "construct",
887
+ "path": "account-financial-tools/account_loan/models/account_loan_line.py"
888
+ },
889
+ {
890
+ "rule": "odoo.sudo-bypasses-record-rules",
891
+ "severity": "critical",
892
+ "line": 60,
893
+ "evidence": "sudo().search_read() on ir.model.fields runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
894
+ "suppressed": false,
895
+ "advisory": false,
896
+ "likelihood": "defect",
897
+ "path": "account-financial-tools/account_lock_date_update/wizards/account_update_lock_date.py"
898
+ },
899
+ {
900
+ "rule": "odoo.ondelete-is-a-decision",
901
+ "severity": "high",
902
+ "line": 59,
903
+ "evidence": "oca_purchase_line_id declares no ondelete, so the ORM defaults it to 'set null' -- and oca_purchase_line_id.order_id on line 84 reads through it, which yields False rather than raising once the parent is deleted",
904
+ "suppressed": false,
905
+ "advisory": false,
906
+ "likelihood": "construct",
907
+ "path": "account-financial-tools/account_move_line_purchase_info/models/account_move.py"
908
+ },
909
+ {
910
+ "rule": "odoo.stored-compute-needs-complete-depends",
911
+ "severity": "critical",
912
+ "line": 84,
913
+ "evidence": "_compute_purchase_id() reads 'oca_purchase_line_id.order_id' but @api.depends does not declare it -- the stored value goes stale when 'oca_purchase_line_id.order_id' changes",
914
+ "suppressed": false,
915
+ "advisory": false,
916
+ "likelihood": "construct",
917
+ "path": "account-financial-tools/account_move_line_purchase_info/models/account_move.py"
918
+ },
919
+ {
920
+ "rule": "odoo.stored-compute-needs-complete-depends",
921
+ "severity": "critical",
922
+ "line": 84,
923
+ "evidence": "_compute_purchase_id() reads 'purchase_line_id.order_id' but @api.depends does not declare it -- the stored value goes stale when 'purchase_line_id.order_id' changes",
924
+ "suppressed": false,
925
+ "advisory": false,
926
+ "likelihood": "construct",
927
+ "path": "account-financial-tools/account_move_line_purchase_info/models/account_move.py"
928
+ },
929
+ {
930
+ "rule": "odoo.stored-compute-needs-complete-depends",
931
+ "severity": "critical",
932
+ "line": 17,
933
+ "evidence": "_compute_display_name() reads 'order_id' but @api.depends does not declare it -- the stored value goes stale when 'order_id' changes",
934
+ "suppressed": false,
935
+ "advisory": false,
936
+ "likelihood": "construct",
937
+ "path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
938
+ },
939
+ {
940
+ "rule": "odoo.stored-compute-needs-complete-depends",
941
+ "severity": "critical",
942
+ "line": 17,
943
+ "evidence": "_compute_display_name() reads 'order_id.name' but @api.depends does not declare it -- the stored value goes stale when 'order_id.name' changes",
944
+ "suppressed": false,
945
+ "advisory": false,
946
+ "likelihood": "construct",
947
+ "path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
948
+ },
949
+ {
950
+ "rule": "odoo.stored-compute-needs-complete-depends",
951
+ "severity": "critical",
952
+ "line": 17,
953
+ "evidence": "_compute_display_name() reads 'product_id' but @api.depends does not declare it -- the stored value goes stale when 'product_id' changes",
954
+ "suppressed": false,
955
+ "advisory": false,
956
+ "likelihood": "construct",
957
+ "path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
958
+ },
959
+ {
960
+ "rule": "odoo.stored-compute-needs-complete-depends",
961
+ "severity": "critical",
962
+ "line": 17,
963
+ "evidence": "_compute_display_name() reads 'product_id.name' but @api.depends does not declare it -- the stored value goes stale when 'product_id.name' changes",
964
+ "suppressed": false,
965
+ "advisory": false,
966
+ "likelihood": "construct",
967
+ "path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
968
+ },
969
+ {
970
+ "rule": "odoo.stored-compute-needs-complete-depends",
971
+ "severity": "critical",
972
+ "line": 18,
973
+ "evidence": "_compute_display_name() reads 'order_id.state' but @api.depends does not declare it -- the stored value goes stale when 'order_id.state' changes",
974
+ "suppressed": false,
975
+ "advisory": false,
976
+ "likelihood": "construct",
977
+ "path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
978
+ },
979
+ {
980
+ "rule": "odoo.ondelete-is-a-decision",
981
+ "severity": "high",
982
+ "line": 14,
983
+ "evidence": "tax_line_id declares no ondelete, so the ORM defaults it to 'set null' -- and tax_line_id.refund_repartition_line_ids on line 27 reads through it, which yields False rather than raising once the parent is deleted",
984
+ "suppressed": false,
985
+ "advisory": false,
986
+ "likelihood": "construct",
987
+ "path": "account-financial-tools/account_move_line_tax_editable/models/account_move_line.py"
988
+ },
989
+ {
990
+ "rule": "odoo.ondelete-is-a-decision",
991
+ "severity": "high",
992
+ "line": 132,
993
+ "evidence": "partner_id declares no ondelete, so the ORM defaults it to 'set null' -- and partner_id.category_id on line 206 reads through it, which yields False rather than raising once the parent is deleted",
994
+ "suppressed": false,
995
+ "advisory": false,
996
+ "likelihood": "construct",
997
+ "path": "account-financial-tools/account_move_template/models/account_move_template.py"
998
+ },
999
+ {
1000
+ "rule": "odoo.stored-compute-needs-complete-depends",
1001
+ "severity": "critical",
1002
+ "line": 206,
1003
+ "evidence": "_compute_analytic_distribution() reads 'partner_id.category_id' but @api.depends does not declare it -- the stored value goes stale when 'partner_id.category_id' changes",
1004
+ "suppressed": false,
1005
+ "advisory": false,
1006
+ "likelihood": "construct",
1007
+ "path": "account-financial-tools/account_move_template/models/account_move_template.py"
1008
+ },
1009
+ {
1010
+ "rule": "odoo.stored-compute-needs-complete-depends",
1011
+ "severity": "critical",
1012
+ "line": 209,
1013
+ "evidence": "_compute_analytic_distribution() reads 'account_id.code' but @api.depends does not declare it -- the stored value goes stale when 'account_id.code' changes",
1014
+ "suppressed": false,
1015
+ "advisory": false,
1016
+ "likelihood": "construct",
1017
+ "path": "account-financial-tools/account_move_template/models/account_move_template.py"
1018
+ },
1019
+ {
1020
+ "rule": "odoo.stored-compute-needs-complete-depends",
1021
+ "severity": "critical",
1022
+ "line": 210,
1023
+ "evidence": "_compute_analytic_distribution() reads 'template_id' but @api.depends does not declare it -- the stored value goes stale when 'template_id' changes",
1024
+ "suppressed": false,
1025
+ "advisory": false,
1026
+ "likelihood": "construct",
1027
+ "path": "account-financial-tools/account_move_template/models/account_move_template.py"
1028
+ },
1029
+ {
1030
+ "rule": "odoo.stored-compute-needs-complete-depends",
1031
+ "severity": "critical",
1032
+ "line": 210,
1033
+ "evidence": "_compute_analytic_distribution() reads 'template_id.company_id' but @api.depends does not declare it -- the stored value goes stale when 'template_id.company_id' changes",
1034
+ "suppressed": false,
1035
+ "advisory": false,
1036
+ "likelihood": "construct",
1037
+ "path": "account-financial-tools/account_move_template/models/account_move_template.py"
1038
+ },
1039
+ {
1040
+ "rule": "python.assert-is-not-validation",
1041
+ "severity": "critical",
1042
+ "line": 117,
1043
+ "evidence": "assert isinstance(overwrite_vals, dict)",
1044
+ "suppressed": false,
1045
+ "advisory": false,
1046
+ "likelihood": "unmeasured",
1047
+ "path": "account-financial-tools/account_move_template/wizard/account_move_template_run.py"
1048
+ },
1049
+ {
1050
+ "rule": "odoo.check-company-on-relations",
1051
+ "severity": "critical",
1052
+ "line": 12,
1053
+ "evidence": "relation to account.journal with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.journal, and this codebase runs multi-company",
1054
+ "suppressed": false,
1055
+ "advisory": false,
1056
+ "likelihood": "defect",
1057
+ "path": "account-financial-tools/account_payroll_sheet_import/models/payroll_import_mapping.py"
1058
+ },
1059
+ {
1060
+ "rule": "odoo.check-company-on-relations",
1061
+ "severity": "critical",
1062
+ "line": 36,
1063
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
1064
+ "suppressed": false,
1065
+ "advisory": false,
1066
+ "likelihood": "defect",
1067
+ "path": "account-financial-tools/account_payroll_sheet_import/models/payroll_import_mapping.py"
1068
+ },
1069
+ {
1070
+ "rule": "python.decimal-for-money-not-float",
1071
+ "severity": "high",
1072
+ "line": 110,
1073
+ "evidence": "amount = float(raw_value)",
1074
+ "suppressed": false,
1075
+ "advisory": false,
1076
+ "likelihood": "unmeasured",
1077
+ "path": "account-financial-tools/account_payroll_sheet_import/wizard/payroll_import_wizard.py"
1078
+ },
1079
+ {
1080
+ "rule": "odoo.stored-compute-needs-complete-depends",
1081
+ "severity": "critical",
1082
+ "line": 53,
1083
+ "evidence": "_compute_name() reads 'create_date' but @api.depends does not declare it -- the stored value goes stale when 'create_date' changes",
1084
+ "suppressed": false,
1085
+ "advisory": false,
1086
+ "likelihood": "construct",
1087
+ "path": "account-financial-tools/account_sequence_option/models/account_move.py"
1088
+ },
1089
+ {
1090
+ "rule": "odoo.constrains-must-list-every-field-read",
1091
+ "severity": "critical",
1092
+ "line": 71,
1093
+ "evidence": "_check_spread_account_balance_sheet() reads 'move_id' but @api.constrains declares only account_id, spread_id -- writing 'move_id' alone skips the check entirely",
1094
+ "suppressed": false,
1095
+ "advisory": false,
1096
+ "likelihood": "unmeasured",
1097
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_move_line.py"
1098
+ },
1099
+ {
1100
+ "rule": "odoo.ondelete-is-a-decision",
1101
+ "severity": "high",
1102
+ "line": 22,
1103
+ "evidence": "template_id declares no ondelete, so the ORM defaults it to 'set null' -- and template_id.spread_type on line 310 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete account.spread.template",
1104
+ "suppressed": false,
1105
+ "advisory": false,
1106
+ "likelihood": "defect",
1107
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1108
+ },
1109
+ {
1110
+ "rule": "odoo.check-company-on-relations",
1111
+ "severity": "critical",
1112
+ "line": 55,
1113
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
1114
+ "suppressed": false,
1115
+ "advisory": false,
1116
+ "likelihood": "defect",
1117
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1118
+ },
1119
+ {
1120
+ "rule": "odoo.check-company-on-relations",
1121
+ "severity": "critical",
1122
+ "line": 62,
1123
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
1124
+ "suppressed": false,
1125
+ "advisory": false,
1126
+ "likelihood": "defect",
1127
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1128
+ },
1129
+ {
1130
+ "rule": "odoo.ondelete-is-a-decision",
1131
+ "severity": "high",
1132
+ "line": 113,
1133
+ "evidence": "invoice_line_id declares no ondelete, so the ORM defaults it to 'set null' -- and invoice_line_id.name on line 537 reads through it, which yields False rather than raising once the parent is deleted",
1134
+ "suppressed": false,
1135
+ "advisory": false,
1136
+ "likelihood": "construct",
1137
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1138
+ },
1139
+ {
1140
+ "rule": "odoo.stored-compute-needs-complete-depends",
1141
+ "severity": "critical",
1142
+ "line": 218,
1143
+ "evidence": "_compute_all_posted() reads 'currency_id' but @api.depends does not declare it -- the stored value goes stale when 'currency_id' changes",
1144
+ "suppressed": false,
1145
+ "advisory": false,
1146
+ "likelihood": "construct",
1147
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1148
+ },
1149
+ {
1150
+ "rule": "odoo.stored-compute-needs-complete-depends",
1151
+ "severity": "critical",
1152
+ "line": 218,
1153
+ "evidence": "_compute_all_posted() reads 'currency_id.rounding' but @api.depends does not declare it -- the stored value goes stale when 'currency_id.rounding' changes",
1154
+ "suppressed": false,
1155
+ "advisory": false,
1156
+ "likelihood": "construct",
1157
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1158
+ },
1159
+ {
1160
+ "rule": "odoo.stored-compute-needs-complete-depends",
1161
+ "severity": "critical",
1162
+ "line": 268,
1163
+ "evidence": "_compute_journal_id() reads 'company_id.default_spread_expense_journal_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_expense_journal_id' changes",
1164
+ "suppressed": false,
1165
+ "advisory": false,
1166
+ "likelihood": "construct",
1167
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1168
+ },
1169
+ {
1170
+ "rule": "odoo.stored-compute-needs-complete-depends",
1171
+ "severity": "critical",
1172
+ "line": 270,
1173
+ "evidence": "_compute_journal_id() reads 'company_id.default_spread_revenue_journal_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_revenue_journal_id' changes",
1174
+ "suppressed": false,
1175
+ "advisory": false,
1176
+ "likelihood": "construct",
1177
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1178
+ },
1179
+ {
1180
+ "rule": "odoo.stored-compute-needs-complete-depends",
1181
+ "severity": "critical",
1182
+ "line": 280,
1183
+ "evidence": "_compute_debit_account_id() reads 'company_id.default_spread_revenue_account_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_revenue_account_id' changes",
1184
+ "suppressed": false,
1185
+ "advisory": false,
1186
+ "likelihood": "construct",
1187
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1188
+ },
1189
+ {
1190
+ "rule": "odoo.stored-compute-needs-complete-depends",
1191
+ "severity": "critical",
1192
+ "line": 288,
1193
+ "evidence": "_compute_credit_account_id() reads 'company_id.default_spread_expense_account_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_expense_account_id' changes",
1194
+ "suppressed": false,
1195
+ "advisory": false,
1196
+ "likelihood": "construct",
1197
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
1198
+ },
1199
+ {
1200
+ "rule": "odoo.check-company-on-relations",
1201
+ "severity": "critical",
1202
+ "line": 20,
1203
+ "evidence": "relation to account.journal with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.journal, and this codebase runs multi-company",
1204
+ "suppressed": false,
1205
+ "advisory": false,
1206
+ "likelihood": "defect",
1207
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
1208
+ },
1209
+ {
1210
+ "rule": "odoo.check-company-on-relations",
1211
+ "severity": "critical",
1212
+ "line": 33,
1213
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
1214
+ "suppressed": false,
1215
+ "advisory": false,
1216
+ "likelihood": "defect",
1217
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
1218
+ },
1219
+ {
1220
+ "rule": "odoo.check-company-on-relations",
1221
+ "severity": "critical",
1222
+ "line": 41,
1223
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
1224
+ "suppressed": false,
1225
+ "advisory": false,
1226
+ "likelihood": "defect",
1227
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
1228
+ },
1229
+ {
1230
+ "rule": "odoo.check-company-on-relations",
1231
+ "severity": "critical",
1232
+ "line": 176,
1233
+ "evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
1234
+ "suppressed": false,
1235
+ "advisory": false,
1236
+ "likelihood": "defect",
1237
+ "path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
1238
+ },
1239
+ {
1240
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1241
+ "severity": "medium",
1242
+ "line": 45,
1243
+ "evidence": "move_date = writeoff_vals.get(\"date\", datetime.now())",
1244
+ "suppressed": false,
1245
+ "advisory": false,
1246
+ "likelihood": "unmeasured",
1247
+ "path": "account-financial-tools/purchase_unreconciled/models/account_move_line.py"
1248
+ },
1249
+ {
1250
+ "rule": "odoo.sudo-bypasses-record-rules",
1251
+ "severity": "critical",
1252
+ "line": 14,
1253
+ "evidence": "sudo().search() on ir.model runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
1254
+ "suppressed": false,
1255
+ "advisory": false,
1256
+ "likelihood": "defect",
1257
+ "path": "account-financial-tools/purchase_unreconciled/wizards/purchase_unreconciled_exceeded.py"
1258
+ }
1259
+ ]
1260
+ }
fieldtest-openspp-2026-09.json CHANGED
@@ -5,7 +5,7 @@
5
  ],
6
  "files": 1351,
7
  "lines": 226937,
8
- "checkers": 18,
9
  "triggers": {
10
  "files": 2265,
11
  "models_declared": 492,
@@ -15,6 +15,26 @@
15
  "multi_company": true
16
  },
17
  "hits": [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  {
19
  "rule": "odoo.sudo-bypasses-record-rules",
20
  "severity": "critical",
@@ -55,6 +75,26 @@
55
  "likelihood": "defect",
56
  "path": "fastapi/models/fastapi_endpoint_demo.py"
57
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  {
59
  "rule": "odoo.ondelete-is-a-decision",
60
  "severity": "high",
@@ -135,6 +175,16 @@
135
  "likelihood": "defect",
136
  "path": "spp_analytics/models/indicator_registry.py"
137
  },
 
 
 
 
 
 
 
 
 
 
138
  {
139
  "rule": "odoo.sudo-bypasses-record-rules",
140
  "severity": "critical",
@@ -225,6 +275,46 @@
225
  "likelihood": "preference",
226
  "path": "spp_api_v2/models/api_path_filter.py"
227
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  {
229
  "rule": "odoo.sudo-bypasses-record-rules",
230
  "severity": "critical",
@@ -235,6 +325,26 @@
235
  "likelihood": "defect",
236
  "path": "spp_api_v2/services/group_service.py"
237
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  {
239
  "rule": "odoo.sudo-bypasses-record-rules",
240
  "severity": "critical",
@@ -245,6 +355,16 @@
245
  "likelihood": "defect",
246
  "path": "spp_api_v2/services/group_service.py"
247
  },
 
 
 
 
 
 
 
 
 
 
248
  {
249
  "rule": "odoo.sudo-bypasses-record-rules",
250
  "severity": "critical",
@@ -725,6 +845,46 @@
725
  "likelihood": "defect",
726
  "path": "spp_attendance/controllers/controllers.py"
727
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  {
729
  "rule": "odoo.recursion-check-on-hierarchy",
730
  "severity": "high",
@@ -825,6 +985,16 @@
825
  "likelihood": "unmeasured",
826
  "path": "spp_cel_domain/models/cel_expression.py"
827
  },
 
 
 
 
 
 
 
 
 
 
828
  {
829
  "rule": "odoo.constrains-must-list-every-field-read",
830
  "severity": "critical",
@@ -895,6 +1065,36 @@
895
  "likelihood": "preference",
896
  "path": "spp_cel_domain/models/data_provider.py"
897
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
  {
899
  "rule": "odoo.stored-compute-needs-complete-depends",
900
  "severity": "critical",
@@ -1255,6 +1455,16 @@
1255
  "likelihood": "construct",
1256
  "path": "spp_dci_client_sr/models/sr_record.py"
1257
  },
 
 
 
 
 
 
 
 
 
 
1258
  {
1259
  "rule": "odoo.prefer-sql-constraint",
1260
  "severity": "high",
@@ -1605,6 +1815,26 @@
1605
  "likelihood": "unmeasured",
1606
  "path": "spp_drims/wizard/report_4w_wizard.py"
1607
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1608
  {
1609
  "rule": "odoo.stored-compute-needs-complete-depends",
1610
  "severity": "critical",
@@ -1715,6 +1945,16 @@
1715
  "likelihood": "unmeasured",
1716
  "path": "spp_farmer_registry_demo/models/farmer_demo_generator.py"
1717
  },
 
 
 
 
 
 
 
 
 
 
1718
  {
1719
  "rule": "odoo.sudo-bypasses-record-rules",
1720
  "severity": "critical",
@@ -1995,6 +2235,16 @@
1995
  "likelihood": "preference",
1996
  "path": "spp_grm_demo/models/generate_tickets.py"
1997
  },
 
 
 
 
 
 
 
 
 
 
1998
  {
1999
  "rule": "odoo.stored-compute-needs-complete-depends",
2000
  "severity": "critical",
@@ -2165,6 +2415,46 @@
2165
  "likelihood": "unmeasured",
2166
  "path": "spp_mis_demo_v2/models/mis_demo_generator.py"
2167
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2168
  {
2169
  "rule": "odoo.stored-compute-needs-complete-depends",
2170
  "severity": "critical",
@@ -2355,6 +2645,26 @@
2355
  "likelihood": "unmeasured",
2356
  "path": "spp_programs/models/accounting/fund_report.py"
2357
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2358
  {
2359
  "rule": "odoo.sudo-bypasses-record-rules",
2360
  "severity": "critical",
@@ -2465,6 +2775,16 @@
2465
  "likelihood": "unmeasured",
2466
  "path": "spp_programs/models/managers/payment_manager.py"
2467
  },
 
 
 
 
 
 
 
 
 
 
2468
  {
2469
  "rule": "odoo.onchange-is-not-validation",
2470
  "severity": "critical",
@@ -2565,6 +2885,16 @@
2565
  "likelihood": "preference",
2566
  "path": "spp_registry/models/reg_id.py"
2567
  },
 
 
 
 
 
 
 
 
 
 
2568
  {
2569
  "rule": "odoo.constrains-must-list-every-field-read",
2570
  "severity": "critical",
 
5
  ],
6
  "files": 1351,
7
  "lines": 226937,
8
+ "checkers": 26,
9
  "triggers": {
10
  "files": 2265,
11
  "models_declared": 492,
 
15
  "multi_company": true
16
  },
17
  "hits": [
18
+ {
19
+ "rule": "python.assert-is-not-validation",
20
+ "severity": "critical",
21
+ "line": 314,
22
+ "evidence": "assert \"klass_dotted_path\" in value[\"handler\"]",
23
+ "suppressed": false,
24
+ "advisory": false,
25
+ "likelihood": "unmeasured",
26
+ "path": "endpoint_route_handler/registry.py"
27
+ },
28
+ {
29
+ "rule": "python.assert-is-not-validation",
30
+ "severity": "critical",
31
+ "line": 315,
32
+ "evidence": "assert \"method_name\" in value[\"handler\"]",
33
+ "suppressed": false,
34
+ "advisory": false,
35
+ "likelihood": "unmeasured",
36
+ "path": "endpoint_route_handler/registry.py"
37
+ },
38
  {
39
  "rule": "odoo.sudo-bypasses-record-rules",
40
  "severity": "critical",
 
75
  "likelihood": "defect",
76
  "path": "fastapi/models/fastapi_endpoint_demo.py"
77
  },
78
+ {
79
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
80
+ "severity": "medium",
81
+ "line": 745,
82
+ "evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\",",
83
+ "suppressed": false,
84
+ "advisory": false,
85
+ "likelihood": "unmeasured",
86
+ "path": "scripts/compliance/checker.py"
87
+ },
88
+ {
89
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
90
+ "severity": "medium",
91
+ "line": 813,
92
+ "evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),",
93
+ "suppressed": false,
94
+ "advisory": false,
95
+ "likelihood": "unmeasured",
96
+ "path": "scripts/compliance/checker.py"
97
+ },
98
  {
99
  "rule": "odoo.ondelete-is-a-decision",
100
  "severity": "high",
 
175
  "likelihood": "defect",
176
  "path": "spp_analytics/models/indicator_registry.py"
177
  },
178
+ {
179
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
180
+ "severity": "medium",
181
+ "line": 97,
182
+ "evidence": "\"computed_at\": datetime.now().isoformat(),",
183
+ "suppressed": false,
184
+ "advisory": false,
185
+ "likelihood": "unmeasured",
186
+ "path": "spp_analytics/models/service_aggregation.py"
187
+ },
188
  {
189
  "rule": "odoo.sudo-bypasses-record-rules",
190
  "severity": "critical",
 
275
  "likelihood": "preference",
276
  "path": "spp_api_v2/models/api_path_filter.py"
277
  },
278
+ {
279
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
280
+ "severity": "medium",
281
+ "line": 178,
282
+ "evidence": "revoked_at=datetime.utcnow(),",
283
+ "suppressed": false,
284
+ "advisory": false,
285
+ "likelihood": "unmeasured",
286
+ "path": "spp_api_v2/routers/consent.py"
287
+ },
288
+ {
289
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
290
+ "severity": "medium",
291
+ "line": 270,
292
+ "evidence": "timestamp=datetime.utcnow(),",
293
+ "suppressed": true,
294
+ "advisory": false,
295
+ "likelihood": "unmeasured",
296
+ "path": "spp_api_v2/routers/consent.py"
297
+ },
298
+ {
299
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
300
+ "severity": "medium",
301
+ "line": 217,
302
+ "evidence": "now = datetime.utcnow()",
303
+ "suppressed": false,
304
+ "advisory": false,
305
+ "likelihood": "unmeasured",
306
+ "path": "spp_api_v2/routers/oauth.py"
307
+ },
308
+ {
309
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
310
+ "severity": "medium",
311
+ "line": 142,
312
+ "evidence": "now = datetime.now()",
313
+ "suppressed": false,
314
+ "advisory": false,
315
+ "likelihood": "unmeasured",
316
+ "path": "spp_api_v2/services/group_service.py"
317
+ },
318
  {
319
  "rule": "odoo.sudo-bypasses-record-rules",
320
  "severity": "critical",
 
325
  "likelihood": "defect",
326
  "path": "spp_api_v2/services/group_service.py"
327
  },
328
+ {
329
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
330
+ "severity": "medium",
331
+ "line": 748,
332
+ "evidence": "ended_datetime = datetime.now()",
333
+ "suppressed": false,
334
+ "advisory": false,
335
+ "likelihood": "unmeasured",
336
+ "path": "spp_api_v2/services/group_service.py"
337
+ },
338
+ {
339
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
340
+ "severity": "medium",
341
+ "line": 886,
342
+ "evidence": "now = datetime.now()",
343
+ "suppressed": false,
344
+ "advisory": false,
345
+ "likelihood": "unmeasured",
346
+ "path": "spp_api_v2/services/group_service.py"
347
+ },
348
  {
349
  "rule": "odoo.sudo-bypasses-record-rules",
350
  "severity": "critical",
 
355
  "likelihood": "defect",
356
  "path": "spp_api_v2/services/group_service.py"
357
  },
358
+ {
359
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
360
+ "severity": "medium",
361
+ "line": 1100,
362
+ "evidence": "now = datetime.now()",
363
+ "suppressed": false,
364
+ "advisory": false,
365
+ "likelihood": "unmeasured",
366
+ "path": "spp_api_v2/services/group_service.py"
367
+ },
368
  {
369
  "rule": "odoo.sudo-bypasses-record-rules",
370
  "severity": "critical",
 
845
  "likelihood": "defect",
846
  "path": "spp_attendance/controllers/controllers.py"
847
  },
848
+ {
849
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
850
+ "severity": "medium",
851
+ "line": 198,
852
+ "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")",
853
+ "suppressed": false,
854
+ "advisory": false,
855
+ "likelihood": "unmeasured",
856
+ "path": "spp_attendance/controllers/controllers.py"
857
+ },
858
+ {
859
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
860
+ "severity": "medium",
861
+ "line": 351,
862
+ "evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")",
863
+ "suppressed": false,
864
+ "advisory": false,
865
+ "likelihood": "unmeasured",
866
+ "path": "spp_attendance/controllers/controllers.py"
867
+ },
868
+ {
869
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
870
+ "severity": "medium",
871
+ "line": 198,
872
+ "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")",
873
+ "suppressed": false,
874
+ "advisory": false,
875
+ "likelihood": "unmeasured",
876
+ "path": "spp_audit/models/spp_audit_backend.py"
877
+ },
878
+ {
879
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
880
+ "severity": "medium",
881
+ "line": 216,
882
+ "evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")",
883
+ "suppressed": false,
884
+ "advisory": false,
885
+ "likelihood": "unmeasured",
886
+ "path": "spp_audit/models/spp_audit_backend.py"
887
+ },
888
  {
889
  "rule": "odoo.recursion-check-on-hierarchy",
890
  "severity": "high",
 
985
  "likelihood": "unmeasured",
986
  "path": "spp_cel_domain/models/cel_expression.py"
987
  },
988
+ {
989
+ "rule": "python.eval-and-pickle-execute-their-input",
990
+ "severity": "critical",
991
+ "line": 417,
992
+ "evidence": "and secure than regex-based conversion to Python eval().",
993
+ "suppressed": false,
994
+ "advisory": false,
995
+ "likelihood": "unmeasured",
996
+ "path": "spp_cel_domain/models/cel_service.py"
997
+ },
998
  {
999
  "rule": "odoo.constrains-must-list-every-field-read",
1000
  "severity": "critical",
 
1065
  "likelihood": "preference",
1066
  "path": "spp_cel_domain/models/data_provider.py"
1067
  },
1068
+ {
1069
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1070
+ "severity": "medium",
1071
+ "line": 92,
1072
+ "evidence": "return datetime.now()",
1073
+ "suppressed": false,
1074
+ "advisory": false,
1075
+ "likelihood": "unmeasured",
1076
+ "path": "spp_cel_domain/services/cel_functions.py"
1077
+ },
1078
+ {
1079
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1080
+ "severity": "medium",
1081
+ "line": 757,
1082
+ "evidence": ">>> two_hours_ago = datetime.now() - timedelta(hours=2)",
1083
+ "suppressed": false,
1084
+ "advisory": false,
1085
+ "likelihood": "unmeasured",
1086
+ "path": "spp_cel_domain/services/cel_parser.py"
1087
+ },
1088
+ {
1089
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1090
+ "severity": "medium",
1091
+ "line": 766,
1092
+ "evidence": "delta = datetime.now() - datetime_value",
1093
+ "suppressed": false,
1094
+ "advisory": false,
1095
+ "likelihood": "unmeasured",
1096
+ "path": "spp_cel_domain/services/cel_parser.py"
1097
+ },
1098
  {
1099
  "rule": "odoo.stored-compute-needs-complete-depends",
1100
  "severity": "critical",
 
1455
  "likelihood": "construct",
1456
  "path": "spp_dci_client_sr/models/sr_record.py"
1457
  },
1458
+ {
1459
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1460
+ "severity": "medium",
1461
+ "line": 52,
1462
+ "evidence": "now = datetime.now()",
1463
+ "suppressed": false,
1464
+ "advisory": false,
1465
+ "likelihood": "unmeasured",
1466
+ "path": "spp_dci_server/middleware/rate_limit.py"
1467
+ },
1468
  {
1469
  "rule": "odoo.prefer-sql-constraint",
1470
  "severity": "high",
 
1815
  "likelihood": "unmeasured",
1816
  "path": "spp_drims/wizard/report_4w_wizard.py"
1817
  },
1818
+ {
1819
+ "rule": "python.path-join-does-not-contain-a-path",
1820
+ "severity": "high",
1821
+ "line": 486,
1822
+ "evidence": "file_path = os.path.join(data_path, file_config[\"filename\"])",
1823
+ "suppressed": false,
1824
+ "advisory": false,
1825
+ "likelihood": "unmeasured",
1826
+ "path": "spp_drims_sl_demo/wizard/drims_demo_generator.py"
1827
+ },
1828
+ {
1829
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1830
+ "severity": "medium",
1831
+ "line": 110,
1832
+ "evidence": "curr_datetime = f\"{datetime.now().isoformat(timespec='milliseconds')}Z\"",
1833
+ "suppressed": false,
1834
+ "advisory": false,
1835
+ "likelihood": "unmeasured",
1836
+ "path": "spp_encryption/models/encryption_provider.py"
1837
+ },
1838
  {
1839
  "rule": "odoo.stored-compute-needs-complete-depends",
1840
  "severity": "critical",
 
1945
  "likelihood": "unmeasured",
1946
  "path": "spp_farmer_registry_demo/models/farmer_demo_generator.py"
1947
  },
1948
+ {
1949
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
1950
+ "severity": "medium",
1951
+ "line": 1127,
1952
+ "evidence": "enrollment_dt = datetime.datetime.now()",
1953
+ "suppressed": false,
1954
+ "advisory": false,
1955
+ "likelihood": "unmeasured",
1956
+ "path": "spp_farmer_registry_demo/models/seeded_farm_generator.py"
1957
+ },
1958
  {
1959
  "rule": "odoo.sudo-bypasses-record-rules",
1960
  "severity": "critical",
 
2235
  "likelihood": "preference",
2236
  "path": "spp_grm_demo/models/generate_tickets.py"
2237
  },
2238
+ {
2239
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
2240
+ "severity": "medium",
2241
+ "line": 922,
2242
+ "evidence": "now = datetime.now()",
2243
+ "suppressed": false,
2244
+ "advisory": false,
2245
+ "likelihood": "unmeasured",
2246
+ "path": "spp_grm_demo/models/generate_tickets.py"
2247
+ },
2248
  {
2249
  "rule": "odoo.stored-compute-needs-complete-depends",
2250
  "severity": "critical",
 
2415
  "likelihood": "unmeasured",
2416
  "path": "spp_mis_demo_v2/models/mis_demo_generator.py"
2417
  },
2418
+ {
2419
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
2420
+ "severity": "medium",
2421
+ "line": 3388,
2422
+ "evidence": "current_period = datetime.datetime.now().strftime(\"%Y-%m\")",
2423
+ "suppressed": false,
2424
+ "advisory": false,
2425
+ "likelihood": "unmeasured",
2426
+ "path": "spp_mis_demo_v2/models/mis_demo_generator.py"
2427
+ },
2428
+ {
2429
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
2430
+ "severity": "medium",
2431
+ "line": 3390,
2432
+ "evidence": "(datetime.datetime.now() - datetime.timedelta(days=30 * i)).strftime(\"%Y-%m\")",
2433
+ "suppressed": false,
2434
+ "advisory": false,
2435
+ "likelihood": "unmeasured",
2436
+ "path": "spp_mis_demo_v2/models/mis_demo_generator.py"
2437
+ },
2438
+ {
2439
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
2440
+ "severity": "medium",
2441
+ "line": 3740,
2442
+ "evidence": "now = datetime.datetime.now()",
2443
+ "suppressed": false,
2444
+ "advisory": false,
2445
+ "likelihood": "unmeasured",
2446
+ "path": "spp_mis_demo_v2/models/mis_demo_generator.py"
2447
+ },
2448
+ {
2449
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
2450
+ "severity": "medium",
2451
+ "line": 406,
2452
+ "evidence": "enrollment_dt = datetime.datetime.now()",
2453
+ "suppressed": false,
2454
+ "advisory": false,
2455
+ "likelihood": "unmeasured",
2456
+ "path": "spp_mis_demo_v2/models/seeded_volume_generator.py"
2457
+ },
2458
  {
2459
  "rule": "odoo.stored-compute-needs-complete-depends",
2460
  "severity": "critical",
 
2645
  "likelihood": "unmeasured",
2646
  "path": "spp_programs/models/accounting/fund_report.py"
2647
  },
2648
+ {
2649
+ "rule": "python.decimal-for-money-not-float",
2650
+ "severity": "high",
2651
+ "line": 300,
2652
+ "evidence": "\"base_amount\": float(self.amount or 0.0),",
2653
+ "suppressed": false,
2654
+ "advisory": false,
2655
+ "likelihood": "unmeasured",
2656
+ "path": "spp_programs/models/cel/entitlement_amount_cel.py"
2657
+ },
2658
+ {
2659
+ "rule": "python.decimal-for-money-not-float",
2660
+ "severity": "high",
2661
+ "line": 316,
2662
+ "evidence": "amount = float(result)",
2663
+ "suppressed": false,
2664
+ "advisory": false,
2665
+ "likelihood": "unmeasured",
2666
+ "path": "spp_programs/models/cel/entitlement_amount_cel.py"
2667
+ },
2668
  {
2669
  "rule": "odoo.sudo-bypasses-record-rules",
2670
  "severity": "critical",
 
2775
  "likelihood": "unmeasured",
2776
  "path": "spp_programs/models/managers/payment_manager.py"
2777
  },
2778
+ {
2779
+ "rule": "python.naive-and-aware-datetimes-do-not-compare",
2780
+ "severity": "medium",
2781
+ "line": 141,
2782
+ "evidence": "new_cycle = cm.new_cycle(\"Cycle 1\", datetime.now(), 1)",
2783
+ "suppressed": false,
2784
+ "advisory": false,
2785
+ "likelihood": "unmeasured",
2786
+ "path": "spp_programs/models/managers/program_manager.py"
2787
+ },
2788
  {
2789
  "rule": "odoo.onchange-is-not-validation",
2790
  "severity": "critical",
 
2885
  "likelihood": "preference",
2886
  "path": "spp_registry/models/reg_id.py"
2887
  },
2888
+ {
2889
+ "rule": "python.assert-is-not-validation",
2890
+ "severity": "critical",
2891
+ "line": 165,
2892
+ "evidence": "assert side in [\"source\", \"destination\"]",
2893
+ "suppressed": false,
2894
+ "advisory": false,
2895
+ "likelihood": "unmeasured",
2896
+ "path": "spp_registry/models/reg_relationship.py"
2897
+ },
2898
  {
2899
  "rule": "odoo.constrains-must-list-every-field-read",
2900
  "severity": "critical",
fieldtest-py-2026-09.json DELETED
@@ -1,50 +0,0 @@
1
- {
2
- "stack": "python",
3
- "roots": [
4
- "llm-router-main",
5
- "scripts"
6
- ],
7
- "files": 42,
8
- "lines": 10225,
9
- "checkers": 8,
10
- "triggers": {
11
- "files": 40,
12
- "models_declared": 0,
13
- "models_deleted": 0,
14
- "declared_and_deleted": 0,
15
- "never_deleted": 0,
16
- "multi_company": true
17
- },
18
- "hits": [
19
- {
20
- "rule": "python.assert-is-not-validation",
21
- "severity": "critical",
22
- "line": 129,
23
- "evidence": "correlation_id=\"req-1\")",
24
- "suppressed": false,
25
- "advisory": false,
26
- "likelihood": "unmeasured",
27
- "path": "nexa/llm-router-main/router/security/audit.py"
28
- },
29
- {
30
- "rule": "python.naive-and-aware-datetimes-do-not-compare",
31
- "severity": "medium",
32
- "line": 745,
33
- "evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\",",
34
- "suppressed": false,
35
- "advisory": false,
36
- "likelihood": "unmeasured",
37
- "path": "OpenSPP2-19.0/scripts/compliance/checker.py"
38
- },
39
- {
40
- "rule": "python.naive-and-aware-datetimes-do-not-compare",
41
- "severity": "medium",
42
- "line": 813,
43
- "evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),",
44
- "suppressed": false,
45
- "advisory": false,
46
- "likelihood": "unmeasured",
47
- "path": "OpenSPP2-19.0/scripts/compliance/checker.py"
48
- }
49
- ]
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fieldtest-ts-2026-09.json DELETED
@@ -1,2790 +0,0 @@
1
- {
2
- "stack": "typescript",
3
- "roots": [
4
- "hono",
5
- "trpc"
6
- ],
7
- "files": 781,
8
- "lines": 73762,
9
- "checkers": 8,
10
- "triggers": {
11
- "files": 0,
12
- "models_declared": 0,
13
- "models_deleted": 0,
14
- "declared_and_deleted": 0,
15
- "never_deleted": 0,
16
- "multi_company": false
17
- },
18
- "hits": [
19
- {
20
- "rule": "typescript.unknown-not-any-at-boundaries",
21
- "severity": "high",
22
- "line": 4,
23
- "evidence": "export const buildPage = ({ jsx, Fragment }: { jsx: any; Fragment: any }) => {",
24
- "suppressed": false,
25
- "advisory": false,
26
- "likelihood": "unmeasured",
27
- "path": "hono/benchmarks/jsx/src/page-react.tsx"
28
- },
29
- {
30
- "rule": "typescript.unknown-not-any-at-boundaries",
31
- "severity": "high",
32
- "line": 4,
33
- "evidence": "export const buildPage = ({ jsx, Fragment }: { jsx: any; Fragment: any }) => {",
34
- "suppressed": false,
35
- "advisory": false,
36
- "likelihood": "unmeasured",
37
- "path": "hono/benchmarks/jsx/src/page.tsx"
38
- },
39
- {
40
- "rule": "typescript.unknown-not-any-at-boundaries",
41
- "severity": "high",
42
- "line": 46,
43
- "evidence": "props: any",
44
- "suppressed": false,
45
- "advisory": false,
46
- "likelihood": "unmeasured",
47
- "path": "hono/src/context.ts"
48
- },
49
- {
50
- "rule": "typescript.unknown-not-any-at-boundaries",
51
- "severity": "high",
52
- "line": 51,
53
- "evidence": "exports?: any",
54
- "suppressed": false,
55
- "advisory": false,
56
- "likelihood": "unmeasured",
57
- "path": "hono/src/context.ts"
58
- },
59
- {
60
- "rule": "typescript.unknown-not-any-at-boundaries",
61
- "severity": "high",
62
- "line": 333,
63
- "evidence": "applicationHandler: (request: Request, ...args: any) => Response | Promise<Response>,",
64
- "suppressed": false,
65
- "advisory": false,
66
- "likelihood": "unmeasured",
67
- "path": "hono/src/hono-base.ts"
68
- },
69
- {
70
- "rule": "typescript.unknown-not-any-at-boundaries",
71
- "severity": "high",
72
- "line": 21,
73
- "evidence": "json: any",
74
- "suppressed": false,
75
- "advisory": false,
76
- "likelihood": "unmeasured",
77
- "path": "hono/src/request.ts"
78
- },
79
- {
80
- "rule": "typescript.unknown-not-any-at-boundaries",
81
- "severity": "high",
82
- "line": 123,
83
- "evidence": "#getParamValue(paramKey: any): string | undefined {",
84
- "suppressed": false,
85
- "advisory": false,
86
- "likelihood": "unmeasured",
87
- "path": "hono/src/request.ts"
88
- },
89
- {
90
- "rule": "typescript.unknown-not-any-at-boundaries",
91
- "severity": "high",
92
- "line": 2547,
93
- "evidence": "input: any",
94
- "suppressed": false,
95
- "advisory": false,
96
- "likelihood": "unmeasured",
97
- "path": "hono/src/types.ts"
98
- },
99
- {
100
- "rule": "typescript.unknown-not-any-at-boundaries",
101
- "severity": "high",
102
- "line": 2548,
103
- "evidence": "output: any",
104
- "suppressed": false,
105
- "advisory": false,
106
- "likelihood": "unmeasured",
107
- "path": "hono/src/types.ts"
108
- },
109
- {
110
- "rule": "typescript.unknown-not-any-at-boundaries",
111
- "severity": "high",
112
- "line": 2662,
113
- "evidence": "type MergeMiddlewareResponse<T> = T extends (c: any, next: any) => Promise<infer R>",
114
- "suppressed": false,
115
- "advisory": false,
116
- "likelihood": "unmeasured",
117
- "path": "hono/src/types.ts"
118
- },
119
- {
120
- "rule": "typescript.unknown-not-any-at-boundaries",
121
- "severity": "high",
122
- "line": 2668,
123
- "evidence": ": T extends (c: any, next: any) => infer R",
124
- "suppressed": false,
125
- "advisory": false,
126
- "likelihood": "unmeasured",
127
- "path": "hono/src/types.ts"
128
- },
129
- {
130
- "rule": "typescript.unknown-not-any-at-boundaries",
131
- "severity": "high",
132
- "line": 2684,
133
- "evidence": "json: any",
134
- "suppressed": false,
135
- "advisory": false,
136
- "likelihood": "unmeasured",
137
- "path": "hono/src/types.ts"
138
- },
139
- {
140
- "rule": "typescript.unknown-not-any-at-boundaries",
141
- "severity": "high",
142
- "line": 2750,
143
- "evidence": "export type ExtractHandlerResponse<T> = T extends (c: any, next: any) => Promise<infer R>",
144
- "suppressed": false,
145
- "advisory": false,
146
- "likelihood": "unmeasured",
147
- "path": "hono/src/types.ts"
148
- },
149
- {
150
- "rule": "typescript.unknown-not-any-at-boundaries",
151
- "severity": "high",
152
- "line": 2756,
153
- "evidence": ": T extends (c: any, next: any) => infer R",
154
- "suppressed": false,
155
- "advisory": false,
156
- "likelihood": "unmeasured",
157
- "path": "hono/src/types.ts"
158
- },
159
- {
160
- "rule": "typescript.unknown-not-any-at-boundaries",
161
- "severity": "high",
162
- "line": 11,
163
- "evidence": "Custom?: any",
164
- "suppressed": false,
165
- "advisory": false,
166
- "likelihood": "unmeasured",
167
- "path": "hono/src/adapter/aws-lambda/types.ts"
168
- },
169
- {
170
- "rule": "typescript.unknown-not-any-at-boundaries",
171
- "severity": "high",
172
- "line": 18,
173
- "evidence": "props: any",
174
- "suppressed": false,
175
- "advisory": false,
176
- "likelihood": "unmeasured",
177
- "path": "hono/src/adapter/cloudflare-pages/handler.ts"
178
- },
179
- {
180
- "rule": "typescript.types-are-erased-at-runtime",
181
- "severity": "critical",
182
- "line": 49,
183
- "evidence": "return content as unknown as ReadableStream",
184
- "suppressed": false,
185
- "advisory": false,
186
- "likelihood": "unmeasured",
187
- "path": "hono/src/adapter/cloudflare-workers/utils.ts"
188
- },
189
- {
190
- "rule": "typescript.unknown-not-any-at-boundaries",
191
- "severity": "high",
192
- "line": 6,
193
- "evidence": "): ((req: Request, context: any) => Response | Promise<Response>) => {",
194
- "suppressed": false,
195
- "advisory": false,
196
- "likelihood": "unmeasured",
197
- "path": "hono/src/adapter/netlify/handler.ts"
198
- },
199
- {
200
- "rule": "typescript.unknown-not-any-at-boundaries",
201
- "severity": "high",
202
- "line": 7,
203
- "evidence": "return (req: Request, context: any) => {",
204
- "suppressed": false,
205
- "advisory": false,
206
- "likelihood": "unmeasured",
207
- "path": "hono/src/adapter/netlify/handler.ts"
208
- },
209
- {
210
- "rule": "typescript.types-are-erased-at-runtime",
211
- "severity": "critical",
212
- "line": 15,
213
- "evidence": "const _fetchRes = (await fetchRes) as unknown as Response & {",
214
- "suppressed": false,
215
- "advisory": false,
216
- "likelihood": "unmeasured",
217
- "path": "hono/src/client/fetch-result-please.ts"
218
- },
219
- {
220
- "rule": "typescript.unknown-not-any-at-boundaries",
221
- "severity": "high",
222
- "line": 16,
223
- "evidence": "_data: any",
224
- "suppressed": false,
225
- "advisory": false,
226
- "likelihood": "unmeasured",
227
- "path": "hono/src/client/fetch-result-please.ts"
228
- },
229
- {
230
- "rule": "typescript.unknown-not-any-at-boundaries",
231
- "severity": "high",
232
- "line": 22,
233
- "evidence": "_bodyInit?: any",
234
- "suppressed": false,
235
- "advisory": false,
236
- "likelihood": "unmeasured",
237
- "path": "hono/src/client/fetch-result-please.ts"
238
- },
239
- {
240
- "rule": "typescript.unknown-not-any-at-boundaries",
241
- "severity": "high",
242
- "line": 50,
243
- "evidence": "public detail?: any",
244
- "suppressed": false,
245
- "advisory": false,
246
- "likelihood": "unmeasured",
247
- "path": "hono/src/client/fetch-result-please.ts"
248
- },
249
- {
250
- "rule": "typescript.unknown-not-any-at-boundaries",
251
- "severity": "high",
252
- "line": 54,
253
- "evidence": "public code?: any",
254
- "suppressed": false,
255
- "advisory": false,
256
- "likelihood": "unmeasured",
257
- "path": "hono/src/client/fetch-result-please.ts"
258
- },
259
- {
260
- "rule": "typescript.unknown-not-any-at-boundaries",
261
- "severity": "high",
262
- "line": 58,
263
- "evidence": "public log?: any",
264
- "suppressed": false,
265
- "advisory": false,
266
- "likelihood": "unmeasured",
267
- "path": "hono/src/client/fetch-result-please.ts"
268
- },
269
- {
270
- "rule": "typescript.unknown-not-any-at-boundaries",
271
- "severity": "high",
272
- "line": 62,
273
- "evidence": "public statusCode?: any",
274
- "suppressed": false,
275
- "advisory": false,
276
- "likelihood": "unmeasured",
277
- "path": "hono/src/client/fetch-result-please.ts"
278
- },
279
- {
280
- "rule": "typescript.unknown-not-any-at-boundaries",
281
- "severity": "high",
282
- "line": 66,
283
- "evidence": "options: { detail?: any; code?: any; statusCode?: number; log?: any } = {}",
284
- "suppressed": false,
285
- "advisory": false,
286
- "likelihood": "unmeasured",
287
- "path": "hono/src/client/fetch-result-please.ts"
288
- },
289
- {
290
- "rule": "typescript.unknown-not-any-at-boundaries",
291
- "severity": "high",
292
- "line": 242,
293
- "evidence": "options: any | undefined",
294
- "suppressed": false,
295
- "advisory": false,
296
- "likelihood": "unmeasured",
297
- "path": "hono/src/client/types.ts"
298
- },
299
- {
300
- "rule": "typescript.unknown-not-any-at-boundaries",
301
- "severity": "high",
302
- "line": 268,
303
- "evidence": "options: any | undefined",
304
- "suppressed": false,
305
- "advisory": false,
306
- "likelihood": "unmeasured",
307
- "path": "hono/src/client/types.ts"
308
- },
309
- {
310
- "rule": "typescript.unknown-not-any-at-boundaries",
311
- "severity": "high",
312
- "line": 275,
313
- "evidence": "args: any,",
314
- "suppressed": false,
315
- "advisory": false,
316
- "likelihood": "unmeasured",
317
- "path": "hono/src/client/types.ts"
318
- },
319
- {
320
- "rule": "typescript.unknown-not-any-at-boundaries",
321
- "severity": "high",
322
- "line": 325,
323
- "evidence": "args: any[]",
324
- "suppressed": false,
325
- "advisory": false,
326
- "likelihood": "unmeasured",
327
- "path": "hono/src/client/types.ts"
328
- },
329
- {
330
- "rule": "typescript.types-are-erased-at-runtime",
331
- "severity": "critical",
332
- "line": 195,
333
- "evidence": "children ? (children as unknown as CssClassName)[STYLE_STRING] : ''",
334
- "suppressed": false,
335
- "advisory": false,
336
- "likelihood": "unmeasured",
337
- "path": "hono/src/helper/css/index.ts"
338
- },
339
- {
340
- "rule": "typescript.unknown-not-any-at-boundaries",
341
- "severity": "high",
342
- "line": 357,
343
- "evidence": "createHandlers: CreateHandlersInterface<E, P> = (...handlers: any) => {",
344
- "suppressed": false,
345
- "advisory": false,
346
- "likelihood": "unmeasured",
347
- "path": "hono/src/helper/factory/index.ts"
348
- },
349
- {
350
- "rule": "typescript.types-are-erased-at-runtime",
351
- "severity": "critical",
352
- "line": 240,
353
- "evidence": "forGetInfoURLRequest = maybeRequest as unknown as AddedSSGDataRequest",
354
- "suppressed": false,
355
- "advisory": false,
356
- "likelihood": "unmeasured",
357
- "path": "hono/src/helper/ssg/ssg.ts"
358
- },
359
- {
360
- "rule": "typescript.types-are-erased-at-runtime",
361
- "severity": "critical",
362
- "line": 150,
363
- "evidence": "} else if ((child as unknown as HtmlEscaped).isEscaped) {",
364
- "suppressed": false,
365
- "advisory": false,
366
- "likelihood": "unmeasured",
367
- "path": "hono/src/jsx/base.ts"
368
- },
369
- {
370
- "rule": "typescript.types-are-erased-at-runtime",
371
- "severity": "critical",
372
- "line": 152,
373
- "evidence": "const callbacks = (child as unknown as HtmlEscapedString).callbacks",
374
- "suppressed": false,
375
- "advisory": false,
376
- "likelihood": "unmeasured",
377
- "path": "hono/src/jsx/base.ts"
378
- },
379
- {
380
- "rule": "typescript.unknown-not-any-at-boundaries",
381
- "severity": "high",
382
- "line": 223,
383
- "evidence": "get ref(): any {",
384
- "suppressed": false,
385
- "advisory": false,
386
- "likelihood": "unmeasured",
387
- "path": "hono/src/jsx/base.ts"
388
- },
389
- {
390
- "rule": "typescript.types-are-erased-at-runtime",
391
- "severity": "critical",
392
- "line": 88,
393
- "evidence": "(awaitedFallback as unknown as HtmlEscapedString).callbacks",
394
- "suppressed": false,
395
- "advisory": false,
396
- "likelihood": "unmeasured",
397
- "path": "hono/src/jsx/components.ts"
398
- },
399
- {
400
- "rule": "typescript.types-are-erased-at-runtime",
401
- "severity": "critical",
402
- "line": 109,
403
- "evidence": "resArray = children.map(resolveChildEarly) as unknown as HtmlEscapedString[]",
404
- "suppressed": false,
405
- "advisory": false,
406
- "likelihood": "unmeasured",
407
- "path": "hono/src/jsx/components.ts"
408
- },
409
- {
410
- "rule": "typescript.unknown-not-any-at-boundaries",
411
- "severity": "high",
412
- "line": 19,
413
- "evidence": "type AnyAttributes = { [attributeName: string]: any }",
414
- "suppressed": false,
415
- "advisory": false,
416
- "likelihood": "unmeasured",
417
- "path": "hono/src/jsx/intrinsic-elements.ts"
418
- },
419
- {
420
- "rule": "typescript.types-are-erased-at-runtime",
421
- "severity": "critical",
422
- "line": 39,
423
- "evidence": "} else if (typeof v === 'number' || (v as unknown as HtmlEscaped).isEscaped) {",
424
- "suppressed": false,
425
- "advisory": false,
426
- "likelihood": "unmeasured",
427
- "path": "hono/src/jsx/jsx-runtime.ts"
428
- },
429
- {
430
- "rule": "typescript.unknown-not-any-at-boundaries",
431
- "severity": "high",
432
- "line": 51,
433
- "evidence": "export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = async ({",
434
- "suppressed": false,
435
- "advisory": false,
436
- "likelihood": "unmeasured",
437
- "path": "hono/src/jsx/streaming.ts"
438
- },
439
- {
440
- "rule": "typescript.types-are-erased-at-runtime",
441
- "severity": "critical",
442
- "line": 64,
443
- "evidence": "const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject",
444
- "suppressed": false,
445
- "advisory": false,
446
- "likelihood": "unmeasured",
447
- "path": "hono/src/jsx/streaming.ts"
448
- },
449
- {
450
- "rule": "typescript.unknown-not-any-at-boundaries",
451
- "severity": "high",
452
- "line": 13,
453
- "evidence": "> = (({ children, fallback, fallbackRender, onError }: any) => {",
454
- "suppressed": false,
455
- "advisory": false,
456
- "likelihood": "unmeasured",
457
- "path": "hono/src/jsx/dom/components.ts"
458
- },
459
- {
460
- "rule": "typescript.unknown-not-any-at-boundaries",
461
- "severity": "high",
462
- "line": 15,
463
- "evidence": ";(res as any)[DOM_ERROR_HANDLER] = (err: any) => {",
464
- "suppressed": false,
465
- "advisory": false,
466
- "likelihood": "unmeasured",
467
- "path": "hono/src/jsx/dom/components.ts"
468
- },
469
- {
470
- "rule": "typescript.unknown-not-any-at-boundaries",
471
- "severity": "high",
472
- "line": 25,
473
- "evidence": "export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = (({",
474
- "suppressed": false,
475
- "advisory": false,
476
- "likelihood": "unmeasured",
477
- "path": "hono/src/jsx/dom/components.ts"
478
- },
479
- {
480
- "rule": "typescript.unknown-not-any-at-boundaries",
481
- "severity": "high",
482
- "line": 28,
483
- "evidence": "}: any) => {",
484
- "suppressed": false,
485
- "advisory": false,
486
- "likelihood": "unmeasured",
487
- "path": "hono/src/jsx/dom/components.ts"
488
- },
489
- {
490
- "rule": "typescript.unknown-not-any-at-boundaries",
491
- "severity": "high",
492
- "line": 30,
493
- "evidence": ";(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => {",
494
- "suppressed": false,
495
- "advisory": false,
496
- "likelihood": "unmeasured",
497
- "path": "hono/src/jsx/dom/components.ts"
498
- },
499
- {
500
- "rule": "typescript.unknown-not-any-at-boundaries",
501
- "severity": "high",
502
- "line": 15,
503
- "evidence": "const props: { children: any } = {",
504
- "suppressed": false,
505
- "advisory": false,
506
- "likelihood": "unmeasured",
507
- "path": "hono/src/jsx/dom/context.ts"
508
- },
509
- {
510
- "rule": "typescript.types-are-erased-at-runtime",
511
- "severity": "critical",
512
- "line": 136,
513
- "evidence": "(c) => (c as unknown as CssClassName)[STYLE_STRING]",
514
- "suppressed": false,
515
- "advisory": false,
516
- "likelihood": "unmeasured",
517
- "path": "hono/src/jsx/dom/css.ts"
518
- },
519
- {
520
- "rule": "typescript.types-are-erased-at-runtime",
521
- "severity": "critical",
522
- "line": 193,
523
- "evidence": "return cssClassName as unknown as string",
524
- "suppressed": false,
525
- "advisory": false,
526
- "likelihood": "unmeasured",
527
- "path": "hono/src/jsx/dom/css.ts"
528
- },
529
- {
530
- "rule": "typescript.unknown-not-any-at-boundaries",
531
- "severity": "high",
532
- "line": 32,
533
- "evidence": "export type ErrorHandler = (error: any, retry: () => void) => Child | undefined",
534
- "suppressed": false,
535
- "advisory": false,
536
- "likelihood": "unmeasured",
537
- "path": "hono/src/jsx/dom/render.ts"
538
- },
539
- {
540
- "rule": "typescript.types-are-erased-at-runtime",
541
- "severity": "critical",
542
- "line": 216,
543
- "evidence": ";(container as unknown as HTMLInputElement).value = normalizeFormValue(value)",
544
- "suppressed": false,
545
- "advisory": false,
546
- "likelihood": "unmeasured",
547
- "path": "hono/src/jsx/dom/render.ts"
548
- },
549
- {
550
- "rule": "typescript.types-are-erased-at-runtime",
551
- "severity": "critical",
552
- "line": 612,
553
- "evidence": "if ((oldChild as unknown as NodeString).t !== child.t) {",
554
- "suppressed": false,
555
- "advisory": false,
556
- "likelihood": "unmeasured",
557
- "path": "hono/src/jsx/dom/render.ts"
558
- },
559
- {
560
- "rule": "typescript.types-are-erased-at-runtime",
561
- "severity": "critical",
562
- "line": 613,
563
- "evidence": ";(oldChild as unknown as NodeString).t = child.t // update text content",
564
- "suppressed": false,
565
- "advisory": false,
566
- "likelihood": "unmeasured",
567
- "path": "hono/src/jsx/dom/render.ts"
568
- },
569
- {
570
- "rule": "typescript.types-are-erased-at-runtime",
571
- "severity": "critical",
572
- "line": 614,
573
- "evidence": ";(oldChild as unknown as NodeString).d = true",
574
- "suppressed": false,
575
- "advisory": false,
576
- "likelihood": "unmeasured",
577
- "path": "hono/src/jsx/dom/render.ts"
578
- },
579
- {
580
- "rule": "typescript.types-are-erased-at-runtime",
581
- "severity": "critical",
582
- "line": 244,
583
- "evidence": "} as unknown as JSXNode & { e?: HTMLElement; p?: PreserveNodeType }",
584
- "suppressed": false,
585
- "advisory": false,
586
- "likelihood": "unmeasured",
587
- "path": "hono/src/jsx/dom/intrinsic-element/components.ts"
588
- },
589
- {
590
- "rule": "typescript.types-are-erased-at-runtime",
591
- "severity": "critical",
592
- "line": 267,
593
- "evidence": "} as unknown as JSXNode",
594
- "suppressed": false,
595
- "advisory": false,
596
- "likelihood": "unmeasured",
597
- "path": "hono/src/jsx/dom/intrinsic-element/components.ts"
598
- },
599
- {
600
- "rule": "typescript.types-are-erased-at-runtime",
601
- "severity": "critical",
602
- "line": 279,
603
- "evidence": "} as unknown as JSXNode",
604
- "suppressed": false,
605
- "advisory": false,
606
- "likelihood": "unmeasured",
607
- "path": "hono/src/jsx/dom/intrinsic-element/components.ts"
608
- },
609
- {
610
- "rule": "typescript.types-are-erased-at-runtime",
611
- "severity": "critical",
612
- "line": 291,
613
- "evidence": "} as unknown as JSXNode",
614
- "suppressed": false,
615
- "advisory": false,
616
- "likelihood": "unmeasured",
617
- "path": "hono/src/jsx/dom/intrinsic-element/components.ts"
618
- },
619
- {
620
- "rule": "typescript.types-are-erased-at-runtime",
621
- "severity": "critical",
622
- "line": 309,
623
- "evidence": "} as unknown as JSXNode",
624
- "suppressed": false,
625
- "advisory": false,
626
- "likelihood": "unmeasured",
627
- "path": "hono/src/jsx/dom/intrinsic-element/components.ts"
628
- },
629
- {
630
- "rule": "typescript.types-are-erased-at-runtime",
631
- "severity": "critical",
632
- "line": 365,
633
- "evidence": "tag: FormContext as unknown as Function,",
634
- "suppressed": false,
635
- "advisory": false,
636
- "likelihood": "unmeasured",
637
- "path": "hono/src/jsx/dom/intrinsic-element/components.ts"
638
- },
639
- {
640
- "rule": "typescript.types-are-erased-at-runtime",
641
- "severity": "critical",
642
- "line": 130,
643
- "evidence": ") as unknown as HtmlEscapedString",
644
- "suppressed": false,
645
- "advisory": false,
646
- "likelihood": "unmeasured",
647
- "path": "hono/src/jsx/intrinsic-element/components.ts"
648
- },
649
- {
650
- "rule": "typescript.unknown-not-any-at-boundaries",
651
- "severity": "high",
652
- "line": 49,
653
- "evidence": "(props: any) => component(props, c),",
654
- "suppressed": false,
655
- "advisory": false,
656
- "likelihood": "unmeasured",
657
- "path": "hono/src/middleware/jsx-renderer/index.ts"
658
- },
659
- {
660
- "rule": "typescript.types-are-erased-at-runtime",
661
- "severity": "critical",
662
- "line": 281,
663
- "evidence": "const clone = values[1].slice() as unknown as string[]",
664
- "suppressed": false,
665
- "advisory": false,
666
- "likelihood": "unmeasured",
667
- "path": "hono/src/middleware/secure-headers/secure-headers.ts"
668
- },
669
- {
670
- "rule": "typescript.types-are-erased-at-runtime",
671
- "severity": "critical",
672
- "line": 47,
673
- "evidence": "}, duration) as unknown as number",
674
- "suppressed": false,
675
- "advisory": false,
676
- "likelihood": "unmeasured",
677
- "path": "hono/src/middleware/timeout/index.ts"
678
- },
679
- {
680
- "rule": "typescript.types-are-erased-at-runtime",
681
- "severity": "critical",
682
- "line": 147,
683
- "evidence": "all[1][i] = all[1][i] ? [] : (0 as unknown as HandlerData<string>)",
684
- "suppressed": false,
685
- "advisory": false,
686
- "likelihood": "unmeasured",
687
- "path": "hono/src/router/reg-exp-router/prepared-router.ts"
688
- },
689
- {
690
- "rule": "typescript.types-are-erased-at-runtime",
691
- "severity": "critical",
692
- "line": 141,
693
- "evidence": "request.bodyCache.formData = formDataPromise as unknown as FormData",
694
- "suppressed": false,
695
- "advisory": false,
696
- "likelihood": "unmeasured",
697
- "path": "hono/src/utils/body.ts"
698
- },
699
- {
700
- "rule": "typescript.types-are-erased-at-runtime",
701
- "severity": "critical",
702
- "line": 256,
703
- "evidence": "nestedForm = nestedForm[key] as unknown as BodyData",
704
- "suppressed": false,
705
- "advisory": false,
706
- "likelihood": "unmeasured",
707
- "path": "hono/src/utils/body.ts"
708
- },
709
- {
710
- "rule": "typescript.types-are-erased-at-runtime",
711
- "severity": "critical",
712
- "line": 151,
713
- "evidence": "str = (str as unknown as string).toString() // HtmlEscapedString object to string",
714
- "suppressed": false,
715
- "advisory": false,
716
- "likelihood": "unmeasured",
717
- "path": "hono/src/utils/html.ts"
718
- },
719
- {
720
- "rule": "typescript.types-are-erased-at-runtime",
721
- "severity": "critical",
722
- "line": 154,
723
- "evidence": "str = await (str as unknown as Promise<string>)",
724
- "suppressed": false,
725
- "advisory": false,
726
- "likelihood": "unmeasured",
727
- "path": "hono/src/utils/html.ts"
728
- },
729
- {
730
- "rule": "typescript.unknown-not-any-at-boundaries",
731
- "severity": "high",
732
- "line": 28,
733
- "evidence": "export type ExtractValidationResponse<VF> = VF extends (value: any, c: any) => infer R",
734
- "suppressed": false,
735
- "advisory": false,
736
- "likelihood": "unmeasured",
737
- "path": "hono/src/validator/validator.ts"
738
- },
739
- {
740
- "rule": "typescript.unknown-not-any-at-boundaries",
741
- "severity": "high",
742
- "line": 175,
743
- "evidence": "export type ExtractValidatorOutput<VF> = VF extends (value: any, c: any) => infer R",
744
- "suppressed": false,
745
- "advisory": false,
746
- "likelihood": "unmeasured",
747
- "path": "hono/src/validator/validator.ts"
748
- },
749
- {
750
- "rule": "typescript.validate-env-at-startup",
751
- "severity": "high",
752
- "line": 3,
753
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
754
- "suppressed": false,
755
- "advisory": false,
756
- "likelihood": "unmeasured",
757
- "path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
758
- },
759
- {
760
- "rule": "typescript.validate-env-at-startup",
761
- "severity": "high",
762
- "line": 8,
763
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
764
- "suppressed": false,
765
- "advisory": false,
766
- "likelihood": "unmeasured",
767
- "path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
768
- },
769
- {
770
- "rule": "typescript.validate-env-at-startup",
771
- "severity": "high",
772
- "line": 17,
773
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
774
- "suppressed": false,
775
- "advisory": false,
776
- "likelihood": "unmeasured",
777
- "path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
778
- },
779
- {
780
- "rule": "typescript.validate-env-at-startup",
781
- "severity": "high",
782
- "line": 19,
783
- "evidence": "retries: process.env.CI ? 3 : 0,",
784
- "suppressed": false,
785
- "advisory": false,
786
- "likelihood": "unmeasured",
787
- "path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
788
- },
789
- {
790
- "rule": "typescript.validate-env-at-startup",
791
- "severity": "high",
792
- "line": 18,
793
- "evidence": "clientId: process.env.GITHUB_ID!,",
794
- "suppressed": false,
795
- "advisory": false,
796
- "likelihood": "unmeasured",
797
- "path": "trpc/examples/.experimental/next-app-dir/src/auth.ts"
798
- },
799
- {
800
- "rule": "typescript.validate-env-at-startup",
801
- "severity": "high",
802
- "line": 20,
803
- "evidence": "clientSecret: process.env.GITHUB_SECRET!,",
804
- "suppressed": false,
805
- "advisory": false,
806
- "likelihood": "unmeasured",
807
- "path": "trpc/examples/.experimental/next-app-dir/src/auth.ts"
808
- },
809
- {
810
- "rule": "typescript.validate-env-at-startup",
811
- "severity": "high",
812
- "line": 9,
813
- "evidence": "if (process.env.NODE_ENV === 'development') {",
814
- "suppressed": false,
815
- "advisory": false,
816
- "likelihood": "unmeasured",
817
- "path": "trpc/examples/.experimental/next-app-dir/src/app/posts/layout.tsx"
818
- },
819
- {
820
- "rule": "typescript.validate-env-at-startup",
821
- "severity": "high",
822
- "line": 39,
823
- "evidence": "export const db = process.env.POSTGRES_URL ? pgDb() : mockDb();",
824
- "suppressed": false,
825
- "advisory": false,
826
- "likelihood": "unmeasured",
827
- "path": "trpc/examples/.experimental/next-app-dir/src/app/posts/_lib/db.ts"
828
- },
829
- {
830
- "rule": "typescript.validate-env-at-startup",
831
- "severity": "high",
832
- "line": 37,
833
- "evidence": "process.env.NODE_ENV === 'development' ||",
834
- "suppressed": false,
835
- "advisory": false,
836
- "likelihood": "unmeasured",
837
- "path": "trpc/examples/.experimental/next-app-dir/src/trpc/rq-client.tsx"
838
- },
839
- {
840
- "rule": "typescript.validate-env-at-startup",
841
- "severity": "high",
842
- "line": 10,
843
- "evidence": "if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;",
844
- "suppressed": false,
845
- "advisory": false,
846
- "likelihood": "unmeasured",
847
- "path": "trpc/examples/.experimental/next-app-dir/src/trpc/shared.ts"
848
- },
849
- {
850
- "rule": "typescript.validate-env-at-startup",
851
- "severity": "high",
852
- "line": 5,
853
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
854
- "suppressed": false,
855
- "advisory": false,
856
- "likelihood": "unmeasured",
857
- "path": "trpc/examples/.test/diagnostics-big-router/next.config.ts"
858
- },
859
- {
860
- "rule": "typescript.validate-env-at-startup",
861
- "severity": "high",
862
- "line": 15,
863
- "evidence": "if (process.env.VERCEL_URL) {",
864
- "suppressed": false,
865
- "advisory": false,
866
- "likelihood": "unmeasured",
867
- "path": "trpc/examples/.test/diagnostics-big-router/src/utils/trpc.ts"
868
- },
869
- {
870
- "rule": "typescript.validate-env-at-startup",
871
- "severity": "high",
872
- "line": 16,
873
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
874
- "suppressed": false,
875
- "advisory": false,
876
- "likelihood": "unmeasured",
877
- "path": "trpc/examples/.test/diagnostics-big-router/src/utils/trpc.ts"
878
- },
879
- {
880
- "rule": "typescript.validate-env-at-startup",
881
- "severity": "high",
882
- "line": 20,
883
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
884
- "suppressed": false,
885
- "advisory": false,
886
- "likelihood": "unmeasured",
887
- "path": "trpc/examples/.test/diagnostics-big-router/src/utils/trpc.ts"
888
- },
889
- {
890
- "rule": "typescript.unknown-not-any-at-boundaries",
891
- "severity": "high",
892
- "line": 19,
893
- "evidence": "export function genericRouter<TSchema extends (value: any) => unknown>(",
894
- "suppressed": false,
895
- "advisory": false,
896
- "likelihood": "unmeasured",
897
- "path": "trpc/examples/.test/internal-types-export/src/server.ts"
898
- },
899
- {
900
- "rule": "typescript.validate-env-at-startup",
901
- "severity": "high",
902
- "line": 5,
903
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
904
- "suppressed": false,
905
- "advisory": false,
906
- "likelihood": "unmeasured",
907
- "path": "trpc/examples/.test/ssg-infinite-serialization/next.config.ts"
908
- },
909
- {
910
- "rule": "typescript.validate-env-at-startup",
911
- "severity": "high",
912
- "line": 3,
913
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
914
- "suppressed": false,
915
- "advisory": false,
916
- "likelihood": "unmeasured",
917
- "path": "trpc/examples/.test/ssg-infinite-serialization/playwright.config.ts"
918
- },
919
- {
920
- "rule": "typescript.validate-env-at-startup",
921
- "severity": "high",
922
- "line": 8,
923
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
924
- "suppressed": false,
925
- "advisory": false,
926
- "likelihood": "unmeasured",
927
- "path": "trpc/examples/.test/ssg-infinite-serialization/playwright.config.ts"
928
- },
929
- {
930
- "rule": "typescript.validate-env-at-startup",
931
- "severity": "high",
932
- "line": 10,
933
- "evidence": "if (process.env.VERCEL_URL) {",
934
- "suppressed": false,
935
- "advisory": false,
936
- "likelihood": "unmeasured",
937
- "path": "trpc/examples/.test/ssg-infinite-serialization/src/utils/trpc.ts"
938
- },
939
- {
940
- "rule": "typescript.validate-env-at-startup",
941
- "severity": "high",
942
- "line": 11,
943
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
944
- "suppressed": false,
945
- "advisory": false,
946
- "likelihood": "unmeasured",
947
- "path": "trpc/examples/.test/ssg-infinite-serialization/src/utils/trpc.ts"
948
- },
949
- {
950
- "rule": "typescript.validate-env-at-startup",
951
- "severity": "high",
952
- "line": 13,
953
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
954
- "suppressed": false,
955
- "advisory": false,
956
- "likelihood": "unmeasured",
957
- "path": "trpc/examples/.test/ssg-infinite-serialization/src/utils/trpc.ts"
958
- },
959
- {
960
- "rule": "typescript.validate-env-at-startup",
961
- "severity": "high",
962
- "line": 5,
963
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
964
- "suppressed": false,
965
- "advisory": false,
966
- "likelihood": "unmeasured",
967
- "path": "trpc/examples/.test/ssg/next.config.ts"
968
- },
969
- {
970
- "rule": "typescript.validate-env-at-startup",
971
- "severity": "high",
972
- "line": 3,
973
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
974
- "suppressed": false,
975
- "advisory": false,
976
- "likelihood": "unmeasured",
977
- "path": "trpc/examples/.test/ssg/playwright.config.ts"
978
- },
979
- {
980
- "rule": "typescript.validate-env-at-startup",
981
- "severity": "high",
982
- "line": 8,
983
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
984
- "suppressed": false,
985
- "advisory": false,
986
- "likelihood": "unmeasured",
987
- "path": "trpc/examples/.test/ssg/playwright.config.ts"
988
- },
989
- {
990
- "rule": "typescript.validate-env-at-startup",
991
- "severity": "high",
992
- "line": 17,
993
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
994
- "suppressed": false,
995
- "advisory": false,
996
- "likelihood": "unmeasured",
997
- "path": "trpc/examples/.test/ssg/playwright.config.ts"
998
- },
999
- {
1000
- "rule": "typescript.validate-env-at-startup",
1001
- "severity": "high",
1002
- "line": 20,
1003
- "evidence": "retries: process.env.CI ? 3 : 0,",
1004
- "suppressed": false,
1005
- "advisory": false,
1006
- "likelihood": "unmeasured",
1007
- "path": "trpc/examples/.test/ssg/playwright.config.ts"
1008
- },
1009
- {
1010
- "rule": "typescript.validate-env-at-startup",
1011
- "severity": "high",
1012
- "line": 10,
1013
- "evidence": "if (process.env.VERCEL_URL) {",
1014
- "suppressed": false,
1015
- "advisory": false,
1016
- "likelihood": "unmeasured",
1017
- "path": "trpc/examples/.test/ssg/src/utils/trpc.ts"
1018
- },
1019
- {
1020
- "rule": "typescript.validate-env-at-startup",
1021
- "severity": "high",
1022
- "line": 11,
1023
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1024
- "suppressed": false,
1025
- "advisory": false,
1026
- "likelihood": "unmeasured",
1027
- "path": "trpc/examples/.test/ssg/src/utils/trpc.ts"
1028
- },
1029
- {
1030
- "rule": "typescript.validate-env-at-startup",
1031
- "severity": "high",
1032
- "line": 13,
1033
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1034
- "suppressed": false,
1035
- "advisory": false,
1036
- "likelihood": "unmeasured",
1037
- "path": "trpc/examples/.test/ssg/src/utils/trpc.ts"
1038
- },
1039
- {
1040
- "rule": "typescript.derive-type-from-schema",
1041
- "severity": "high",
1042
- "line": 5,
1043
- "evidence": "interface Post {",
1044
- "suppressed": false,
1045
- "advisory": false,
1046
- "likelihood": "unmeasured",
1047
- "path": "trpc/examples/fastify-server/src/server/router/routers/posts.ts"
1048
- },
1049
- {
1050
- "rule": "typescript.validate-env-at-startup",
1051
- "severity": "high",
1052
- "line": 3,
1053
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
1054
- "suppressed": false,
1055
- "advisory": false,
1056
- "likelihood": "unmeasured",
1057
- "path": "trpc/examples/minimal-content-types/playwright.config.ts"
1058
- },
1059
- {
1060
- "rule": "typescript.validate-env-at-startup",
1061
- "severity": "high",
1062
- "line": 8,
1063
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
1064
- "suppressed": false,
1065
- "advisory": false,
1066
- "likelihood": "unmeasured",
1067
- "path": "trpc/examples/minimal-content-types/playwright.config.ts"
1068
- },
1069
- {
1070
- "rule": "typescript.validate-env-at-startup",
1071
- "severity": "high",
1072
- "line": 18,
1073
- "evidence": "retries: process.env.CI ? 3 : 0,",
1074
- "suppressed": false,
1075
- "advisory": false,
1076
- "likelihood": "unmeasured",
1077
- "path": "trpc/examples/minimal-content-types/playwright.config.ts"
1078
- },
1079
- {
1080
- "rule": "typescript.validate-env-at-startup",
1081
- "severity": "high",
1082
- "line": 4,
1083
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
1084
- "suppressed": false,
1085
- "advisory": false,
1086
- "likelihood": "unmeasured",
1087
- "path": "trpc/examples/minimal-react/playwright.config.ts"
1088
- },
1089
- {
1090
- "rule": "typescript.validate-env-at-startup",
1091
- "severity": "high",
1092
- "line": 9,
1093
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
1094
- "suppressed": false,
1095
- "advisory": false,
1096
- "likelihood": "unmeasured",
1097
- "path": "trpc/examples/minimal-react/playwright.config.ts"
1098
- },
1099
- {
1100
- "rule": "typescript.validate-env-at-startup",
1101
- "severity": "high",
1102
- "line": 18,
1103
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
1104
- "suppressed": false,
1105
- "advisory": false,
1106
- "likelihood": "unmeasured",
1107
- "path": "trpc/examples/minimal-react/playwright.config.ts"
1108
- },
1109
- {
1110
- "rule": "typescript.validate-env-at-startup",
1111
- "severity": "high",
1112
- "line": 5,
1113
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
1114
- "suppressed": false,
1115
- "advisory": false,
1116
- "likelihood": "unmeasured",
1117
- "path": "trpc/examples/next-big-router/next.config.ts"
1118
- },
1119
- {
1120
- "rule": "typescript.validate-env-at-startup",
1121
- "severity": "high",
1122
- "line": 13,
1123
- "evidence": "if (process.env.VERCEL_URL) {",
1124
- "suppressed": false,
1125
- "advisory": false,
1126
- "likelihood": "unmeasured",
1127
- "path": "trpc/examples/next-big-router/src/utils/trpc.ts"
1128
- },
1129
- {
1130
- "rule": "typescript.validate-env-at-startup",
1131
- "severity": "high",
1132
- "line": 14,
1133
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1134
- "suppressed": false,
1135
- "advisory": false,
1136
- "likelihood": "unmeasured",
1137
- "path": "trpc/examples/next-big-router/src/utils/trpc.ts"
1138
- },
1139
- {
1140
- "rule": "typescript.validate-env-at-startup",
1141
- "severity": "high",
1142
- "line": 18,
1143
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1144
- "suppressed": false,
1145
- "advisory": false,
1146
- "likelihood": "unmeasured",
1147
- "path": "trpc/examples/next-big-router/src/utils/trpc.ts"
1148
- },
1149
- {
1150
- "rule": "typescript.validate-env-at-startup",
1151
- "severity": "high",
1152
- "line": 5,
1153
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
1154
- "suppressed": false,
1155
- "advisory": false,
1156
- "likelihood": "unmeasured",
1157
- "path": "trpc/examples/next-edge-runtime/next.config.ts"
1158
- },
1159
- {
1160
- "rule": "typescript.validate-env-at-startup",
1161
- "severity": "high",
1162
- "line": 13,
1163
- "evidence": "if (process.env.VERCEL_URL) {",
1164
- "suppressed": false,
1165
- "advisory": false,
1166
- "likelihood": "unmeasured",
1167
- "path": "trpc/examples/next-edge-runtime/src/utils/trpc.ts"
1168
- },
1169
- {
1170
- "rule": "typescript.validate-env-at-startup",
1171
- "severity": "high",
1172
- "line": 14,
1173
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1174
- "suppressed": false,
1175
- "advisory": false,
1176
- "likelihood": "unmeasured",
1177
- "path": "trpc/examples/next-edge-runtime/src/utils/trpc.ts"
1178
- },
1179
- {
1180
- "rule": "typescript.validate-env-at-startup",
1181
- "severity": "high",
1182
- "line": 18,
1183
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1184
- "suppressed": false,
1185
- "advisory": false,
1186
- "likelihood": "unmeasured",
1187
- "path": "trpc/examples/next-edge-runtime/src/utils/trpc.ts"
1188
- },
1189
- {
1190
- "rule": "typescript.validate-env-at-startup",
1191
- "severity": "high",
1192
- "line": 5,
1193
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
1194
- "suppressed": false,
1195
- "advisory": false,
1196
- "likelihood": "unmeasured",
1197
- "path": "trpc/examples/next-formdata/next.config.ts"
1198
- },
1199
- {
1200
- "rule": "typescript.validate-env-at-startup",
1201
- "severity": "high",
1202
- "line": 20,
1203
- "evidence": "if (process.env.VERCEL_URL) {",
1204
- "suppressed": false,
1205
- "advisory": false,
1206
- "likelihood": "unmeasured",
1207
- "path": "trpc/examples/next-formdata/src/utils/trpc.ts"
1208
- },
1209
- {
1210
- "rule": "typescript.validate-env-at-startup",
1211
- "severity": "high",
1212
- "line": 21,
1213
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1214
- "suppressed": false,
1215
- "advisory": false,
1216
- "likelihood": "unmeasured",
1217
- "path": "trpc/examples/next-formdata/src/utils/trpc.ts"
1218
- },
1219
- {
1220
- "rule": "typescript.validate-env-at-startup",
1221
- "severity": "high",
1222
- "line": 25,
1223
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1224
- "suppressed": false,
1225
- "advisory": false,
1226
- "likelihood": "unmeasured",
1227
- "path": "trpc/examples/next-formdata/src/utils/trpc.ts"
1228
- },
1229
- {
1230
- "rule": "typescript.validate-env-at-startup",
1231
- "severity": "high",
1232
- "line": 55,
1233
- "evidence": "process.env.NODE_ENV === 'development' ||",
1234
- "suppressed": false,
1235
- "advisory": false,
1236
- "likelihood": "unmeasured",
1237
- "path": "trpc/examples/next-formdata/src/utils/trpc.ts"
1238
- },
1239
- {
1240
- "rule": "typescript.validate-env-at-startup",
1241
- "severity": "high",
1242
- "line": 5,
1243
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
1244
- "suppressed": false,
1245
- "advisory": false,
1246
- "likelihood": "unmeasured",
1247
- "path": "trpc/examples/next-minimal-starter/next.config.ts"
1248
- },
1249
- {
1250
- "rule": "typescript.validate-env-at-startup",
1251
- "severity": "high",
1252
- "line": 15,
1253
- "evidence": "if (process.env.VERCEL_URL) {",
1254
- "suppressed": false,
1255
- "advisory": false,
1256
- "likelihood": "unmeasured",
1257
- "path": "trpc/examples/next-minimal-starter/src/utils/trpc.ts"
1258
- },
1259
- {
1260
- "rule": "typescript.validate-env-at-startup",
1261
- "severity": "high",
1262
- "line": 16,
1263
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1264
- "suppressed": false,
1265
- "advisory": false,
1266
- "likelihood": "unmeasured",
1267
- "path": "trpc/examples/next-minimal-starter/src/utils/trpc.ts"
1268
- },
1269
- {
1270
- "rule": "typescript.validate-env-at-startup",
1271
- "severity": "high",
1272
- "line": 20,
1273
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1274
- "suppressed": false,
1275
- "advisory": false,
1276
- "likelihood": "unmeasured",
1277
- "path": "trpc/examples/next-minimal-starter/src/utils/trpc.ts"
1278
- },
1279
- {
1280
- "rule": "typescript.validate-env-at-startup",
1281
- "severity": "high",
1282
- "line": 5,
1283
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
1284
- "suppressed": false,
1285
- "advisory": false,
1286
- "likelihood": "unmeasured",
1287
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1288
- },
1289
- {
1290
- "rule": "typescript.validate-env-at-startup",
1291
- "severity": "high",
1292
- "line": 14,
1293
- "evidence": "reporter: process.env.CI ? 'github' : 'list',",
1294
- "suppressed": false,
1295
- "advisory": false,
1296
- "likelihood": "unmeasured",
1297
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1298
- },
1299
- {
1300
- "rule": "typescript.validate-env-at-startup",
1301
- "severity": "high",
1302
- "line": 18,
1303
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
1304
- "suppressed": false,
1305
- "advisory": false,
1306
- "likelihood": "unmeasured",
1307
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1308
- },
1309
- {
1310
- "rule": "typescript.validate-env-at-startup",
1311
- "severity": "high",
1312
- "line": 19,
1313
- "evidence": "video: process.env.CI ? 'off' : 'on',",
1314
- "suppressed": false,
1315
- "advisory": false,
1316
- "likelihood": "unmeasured",
1317
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1318
- },
1319
- {
1320
- "rule": "typescript.validate-env-at-startup",
1321
- "severity": "high",
1322
- "line": 21,
1323
- "evidence": "retries: process.env.CI ? 3 : 0,",
1324
- "suppressed": false,
1325
- "advisory": false,
1326
- "likelihood": "unmeasured",
1327
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1328
- },
1329
- {
1330
- "rule": "typescript.validate-env-at-startup",
1331
- "severity": "high",
1332
- "line": 23,
1333
- "evidence": "command: process.env.CI ? 'npm run start' : 'npm run dev',",
1334
- "suppressed": false,
1335
- "advisory": false,
1336
- "likelihood": "unmeasured",
1337
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1338
- },
1339
- {
1340
- "rule": "typescript.validate-env-at-startup",
1341
- "severity": "high",
1342
- "line": 24,
1343
- "evidence": "reuseExistingServer: Boolean(process.env.TEST_LOCAL === '1'),",
1344
- "suppressed": false,
1345
- "advisory": false,
1346
- "likelihood": "unmeasured",
1347
- "path": "trpc/examples/next-prisma-starter/playwright.config.ts"
1348
- },
1349
- {
1350
- "rule": "typescript.validate-env-at-startup",
1351
- "severity": "high",
1352
- "line": 16,
1353
- "evidence": "if (process.env.VERCEL_URL) {",
1354
- "suppressed": false,
1355
- "advisory": false,
1356
- "likelihood": "unmeasured",
1357
- "path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
1358
- },
1359
- {
1360
- "rule": "typescript.validate-env-at-startup",
1361
- "severity": "high",
1362
- "line": 17,
1363
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1364
- "suppressed": false,
1365
- "advisory": false,
1366
- "likelihood": "unmeasured",
1367
- "path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
1368
- },
1369
- {
1370
- "rule": "typescript.validate-env-at-startup",
1371
- "severity": "high",
1372
- "line": 21,
1373
- "evidence": "if (process.env.RENDER_INTERNAL_HOSTNAME) {",
1374
- "suppressed": false,
1375
- "advisory": false,
1376
- "likelihood": "unmeasured",
1377
- "path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
1378
- },
1379
- {
1380
- "rule": "typescript.validate-env-at-startup",
1381
- "severity": "high",
1382
- "line": 22,
1383
- "evidence": "return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;",
1384
- "suppressed": false,
1385
- "advisory": false,
1386
- "likelihood": "unmeasured",
1387
- "path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
1388
- },
1389
- {
1390
- "rule": "typescript.validate-env-at-startup",
1391
- "severity": "high",
1392
- "line": 26,
1393
- "evidence": "return `http://127.0.0.1:${process.env.PORT ?? 3000}`;",
1394
- "suppressed": false,
1395
- "advisory": false,
1396
- "likelihood": "unmeasured",
1397
- "path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
1398
- },
1399
- {
1400
- "rule": "typescript.validate-env-at-startup",
1401
- "severity": "high",
1402
- "line": 62,
1403
- "evidence": "process.env.NODE_ENV === 'development' ||",
1404
- "suppressed": false,
1405
- "advisory": false,
1406
- "likelihood": "unmeasured",
1407
- "path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
1408
- },
1409
- {
1410
- "rule": "typescript.validate-env-at-startup",
1411
- "severity": "high",
1412
- "line": 3,
1413
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
1414
- "suppressed": false,
1415
- "advisory": false,
1416
- "likelihood": "unmeasured",
1417
- "path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
1418
- },
1419
- {
1420
- "rule": "typescript.validate-env-at-startup",
1421
- "severity": "high",
1422
- "line": 8,
1423
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
1424
- "suppressed": false,
1425
- "advisory": false,
1426
- "likelihood": "unmeasured",
1427
- "path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
1428
- },
1429
- {
1430
- "rule": "typescript.validate-env-at-startup",
1431
- "severity": "high",
1432
- "line": 17,
1433
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
1434
- "suppressed": false,
1435
- "advisory": false,
1436
- "likelihood": "unmeasured",
1437
- "path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
1438
- },
1439
- {
1440
- "rule": "typescript.validate-env-at-startup",
1441
- "severity": "high",
1442
- "line": 19,
1443
- "evidence": "retries: process.env.CI ? 3 : 0,",
1444
- "suppressed": false,
1445
- "advisory": false,
1446
- "likelihood": "unmeasured",
1447
- "path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
1448
- },
1449
- {
1450
- "rule": "typescript.validate-env-at-startup",
1451
- "severity": "high",
1452
- "line": 10,
1453
- "evidence": "if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;",
1454
- "suppressed": false,
1455
- "advisory": false,
1456
- "likelihood": "unmeasured",
1457
- "path": "trpc/examples/next-prisma-todomvc/src/server/prisma.ts"
1458
- },
1459
- {
1460
- "rule": "typescript.validate-env-at-startup",
1461
- "severity": "high",
1462
- "line": 14,
1463
- "evidence": "if (process.env.VERCEL_URL) {",
1464
- "suppressed": false,
1465
- "advisory": false,
1466
- "likelihood": "unmeasured",
1467
- "path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
1468
- },
1469
- {
1470
- "rule": "typescript.validate-env-at-startup",
1471
- "severity": "high",
1472
- "line": 15,
1473
- "evidence": "return `https://${process.env.VERCEL_URL}`;",
1474
- "suppressed": false,
1475
- "advisory": false,
1476
- "likelihood": "unmeasured",
1477
- "path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
1478
- },
1479
- {
1480
- "rule": "typescript.validate-env-at-startup",
1481
- "severity": "high",
1482
- "line": 19,
1483
- "evidence": "if (process.env.RENDER_INTERNAL_HOSTNAME) {",
1484
- "suppressed": false,
1485
- "advisory": false,
1486
- "likelihood": "unmeasured",
1487
- "path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
1488
- },
1489
- {
1490
- "rule": "typescript.validate-env-at-startup",
1491
- "severity": "high",
1492
- "line": 20,
1493
- "evidence": "return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;",
1494
- "suppressed": false,
1495
- "advisory": false,
1496
- "likelihood": "unmeasured",
1497
- "path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
1498
- },
1499
- {
1500
- "rule": "typescript.validate-env-at-startup",
1501
- "severity": "high",
1502
- "line": 24,
1503
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1504
- "suppressed": false,
1505
- "advisory": false,
1506
- "likelihood": "unmeasured",
1507
- "path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
1508
- },
1509
- {
1510
- "rule": "typescript.validate-env-at-startup",
1511
- "severity": "high",
1512
- "line": 46,
1513
- "evidence": "process.env.NODE_ENV === 'development' ||",
1514
- "suppressed": false,
1515
- "advisory": false,
1516
- "likelihood": "unmeasured",
1517
- "path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
1518
- },
1519
- {
1520
- "rule": "typescript.validate-env-at-startup",
1521
- "severity": "high",
1522
- "line": 12,
1523
- "evidence": "APP_URL: process.env.APP_URL,",
1524
- "suppressed": false,
1525
- "advisory": false,
1526
- "likelihood": "unmeasured",
1527
- "path": "trpc/examples/next-prisma-websockets-starter/next.config.ts"
1528
- },
1529
- {
1530
- "rule": "typescript.validate-env-at-startup",
1531
- "severity": "high",
1532
- "line": 13,
1533
- "evidence": "WS_URL: process.env.WS_URL,",
1534
- "suppressed": false,
1535
- "advisory": false,
1536
- "likelihood": "unmeasured",
1537
- "path": "trpc/examples/next-prisma-websockets-starter/next.config.ts"
1538
- },
1539
- {
1540
- "rule": "typescript.validate-env-at-startup",
1541
- "severity": "high",
1542
- "line": 16,
1543
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
1544
- "suppressed": false,
1545
- "advisory": false,
1546
- "likelihood": "unmeasured",
1547
- "path": "trpc/examples/next-prisma-websockets-starter/next.config.ts"
1548
- },
1549
- {
1550
- "rule": "typescript.validate-env-at-startup",
1551
- "severity": "high",
1552
- "line": 3,
1553
- "evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
1554
- "suppressed": false,
1555
- "advisory": false,
1556
- "likelihood": "unmeasured",
1557
- "path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
1558
- },
1559
- {
1560
- "rule": "typescript.validate-env-at-startup",
1561
- "severity": "high",
1562
- "line": 8,
1563
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
1564
- "suppressed": false,
1565
- "advisory": false,
1566
- "likelihood": "unmeasured",
1567
- "path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
1568
- },
1569
- {
1570
- "rule": "typescript.validate-env-at-startup",
1571
- "severity": "high",
1572
- "line": 17,
1573
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
1574
- "suppressed": false,
1575
- "advisory": false,
1576
- "likelihood": "unmeasured",
1577
- "path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
1578
- },
1579
- {
1580
- "rule": "typescript.validate-env-at-startup",
1581
- "severity": "high",
1582
- "line": 19,
1583
- "evidence": "retries: process.env.CI ? 3 : 0,",
1584
- "suppressed": false,
1585
- "advisory": false,
1586
- "likelihood": "unmeasured",
1587
- "path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
1588
- },
1589
- {
1590
- "rule": "typescript.validate-env-at-startup",
1591
- "severity": "high",
1592
- "line": 297,
1593
- "evidence": "{process.env.NODE_ENV !== 'production' && (",
1594
- "suppressed": false,
1595
- "advisory": false,
1596
- "likelihood": "unmeasured",
1597
- "path": "trpc/examples/next-prisma-websockets-starter/src/pages/index.tsx"
1598
- },
1599
- {
1600
- "rule": "typescript.validate-env-at-startup",
1601
- "severity": "high",
1602
- "line": 7,
1603
- "evidence": "process.env.NODE_ENV === 'test' ||",
1604
- "suppressed": false,
1605
- "advisory": false,
1606
- "likelihood": "unmeasured",
1607
- "path": "trpc/examples/next-prisma-websockets-starter/src/pages/api/auth/[...nextauth].ts"
1608
- },
1609
- {
1610
- "rule": "typescript.validate-env-at-startup",
1611
- "severity": "high",
1612
- "line": 8,
1613
- "evidence": "process.env.RAILWAY_ENVIRONMENT_NAME?.includes('-pr-'); // example: 'trpc-pr-5821'",
1614
- "suppressed": false,
1615
- "advisory": false,
1616
- "likelihood": "unmeasured",
1617
- "path": "trpc/examples/next-prisma-websockets-starter/src/pages/api/auth/[...nextauth].ts"
1618
- },
1619
- {
1620
- "rule": "typescript.validate-env-at-startup",
1621
- "severity": "high",
1622
- "line": 15,
1623
- "evidence": "process.env.NODE_ENV === 'development'",
1624
- "suppressed": false,
1625
- "advisory": false,
1626
- "likelihood": "unmeasured",
1627
- "path": "trpc/examples/next-prisma-websockets-starter/src/server/prisma.ts"
1628
- },
1629
- {
1630
- "rule": "typescript.validate-env-at-startup",
1631
- "severity": "high",
1632
- "line": 20,
1633
- "evidence": "if (process.env.NODE_ENV !== 'production') {",
1634
- "suppressed": false,
1635
- "advisory": false,
1636
- "likelihood": "unmeasured",
1637
- "path": "trpc/examples/next-prisma-websockets-starter/src/server/prisma.ts"
1638
- },
1639
- {
1640
- "rule": "typescript.validate-env-at-startup",
1641
- "severity": "high",
1642
- "line": 11,
1643
- "evidence": "const port = parseInt(process.env.PORT || '3000', 10);",
1644
- "suppressed": false,
1645
- "advisory": false,
1646
- "likelihood": "unmeasured",
1647
- "path": "trpc/examples/next-prisma-websockets-starter/src/server/prodServer.ts"
1648
- },
1649
- {
1650
- "rule": "typescript.validate-env-at-startup",
1651
- "severity": "high",
1652
- "line": 12,
1653
- "evidence": "const dev = process.env.NODE_ENV !== 'production';",
1654
- "suppressed": false,
1655
- "advisory": false,
1656
- "likelihood": "unmeasured",
1657
- "path": "trpc/examples/next-prisma-websockets-starter/src/server/prodServer.ts"
1658
- },
1659
- {
1660
- "rule": "typescript.validate-env-at-startup",
1661
- "severity": "high",
1662
- "line": 46,
1663
- "evidence": "dev ? 'development' : process.env.NODE_ENV",
1664
- "suppressed": false,
1665
- "advisory": false,
1666
- "likelihood": "unmeasured",
1667
- "path": "trpc/examples/next-prisma-websockets-starter/src/server/prodServer.ts"
1668
- },
1669
- {
1670
- "rule": "typescript.derive-type-from-schema",
1671
- "severity": "high",
1672
- "line": 23,
1673
- "evidence": "interface MyEvents {",
1674
- "suppressed": false,
1675
- "advisory": false,
1676
- "likelihood": "unmeasured",
1677
- "path": "trpc/examples/next-prisma-websockets-starter/src/server/routers/post.ts"
1678
- },
1679
- {
1680
- "rule": "typescript.validate-env-at-startup",
1681
- "severity": "high",
1682
- "line": 79,
1683
- "evidence": "(process.env.NODE_ENV === 'development' &&",
1684
- "suppressed": false,
1685
- "advisory": false,
1686
- "likelihood": "unmeasured",
1687
- "path": "trpc/examples/next-prisma-websockets-starter/src/utils/trpc.ts"
1688
- },
1689
- {
1690
- "rule": "typescript.validate-env-at-startup",
1691
- "severity": "high",
1692
- "line": 3,
1693
- "evidence": "const DB_URL = process.env.DATABASE_URL ?? process.env.POSTGRES_URL;",
1694
- "suppressed": false,
1695
- "advisory": false,
1696
- "likelihood": "unmeasured",
1697
- "path": "trpc/examples/next-sse-chat/drizzle.config.ts"
1698
- },
1699
- {
1700
- "rule": "typescript.validate-env-at-startup",
1701
- "severity": "high",
1702
- "line": 31,
1703
- "evidence": "if (process.env.APP_URL) return process.env.APP_URL;",
1704
- "suppressed": false,
1705
- "advisory": false,
1706
- "likelihood": "unmeasured",
1707
- "path": "trpc/examples/next-sse-chat/src/app/providers.tsx"
1708
- },
1709
- {
1710
- "rule": "typescript.validate-env-at-startup",
1711
- "severity": "high",
1712
- "line": 32,
1713
- "evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
1714
- "suppressed": false,
1715
- "advisory": false,
1716
- "likelihood": "unmeasured",
1717
- "path": "trpc/examples/next-sse-chat/src/app/providers.tsx"
1718
- },
1719
- {
1720
- "rule": "typescript.validate-env-at-startup",
1721
- "severity": "high",
1722
- "line": 13,
1723
- "evidence": "process.env.NODE_ENV === 'test' ||",
1724
- "suppressed": false,
1725
- "advisory": false,
1726
- "likelihood": "unmeasured",
1727
- "path": "trpc/examples/next-sse-chat/src/server/auth.tsx"
1728
- },
1729
- {
1730
- "rule": "typescript.validate-env-at-startup",
1731
- "severity": "high",
1732
- "line": 14,
1733
- "evidence": "process.env.RAILWAY_ENVIRONMENT_NAME?.includes('-pr-'); // example: 'trpc-pr-5821'",
1734
- "suppressed": false,
1735
- "advisory": false,
1736
- "likelihood": "unmeasured",
1737
- "path": "trpc/examples/next-sse-chat/src/server/auth.tsx"
1738
- },
1739
- {
1740
- "rule": "typescript.validate-env-at-startup",
1741
- "severity": "high",
1742
- "line": 54,
1743
- "evidence": "if (process.env.NODE_ENV === 'development') {",
1744
- "suppressed": false,
1745
- "advisory": false,
1746
- "likelihood": "unmeasured",
1747
- "path": "trpc/examples/next-sse-chat/src/server/trpc.ts"
1748
- },
1749
- {
1750
- "rule": "typescript.validate-env-at-startup",
1751
- "severity": "high",
1752
- "line": 5,
1753
- "evidence": "const DB_URL = process.env.DATABASE_URL ?? process.env.POSTGRES_URL;",
1754
- "suppressed": false,
1755
- "advisory": false,
1756
- "likelihood": "unmeasured",
1757
- "path": "trpc/examples/next-sse-chat/src/server/db/client.ts"
1758
- },
1759
- {
1760
- "rule": "typescript.derive-type-from-schema",
1761
- "severity": "high",
1762
- "line": 21,
1763
- "evidence": "export interface MyEvents {",
1764
- "suppressed": false,
1765
- "advisory": false,
1766
- "likelihood": "unmeasured",
1767
- "path": "trpc/examples/next-sse-chat/src/server/routers/channel.ts"
1768
- },
1769
- {
1770
- "rule": "typescript.validate-env-at-startup",
1771
- "severity": "high",
1772
- "line": 5,
1773
- "evidence": "APP_URL: process.env.APP_URL ?? 'http://localhost:3000',",
1774
- "suppressed": false,
1775
- "advisory": false,
1776
- "likelihood": "unmeasured",
1777
- "path": "trpc/examples/next-websockets-encoder/next.config.ts"
1778
- },
1779
- {
1780
- "rule": "typescript.validate-env-at-startup",
1781
- "severity": "high",
1782
- "line": 6,
1783
- "evidence": "WS_URL: process.env.WS_URL ?? 'ws://localhost:3001',",
1784
- "suppressed": false,
1785
- "advisory": false,
1786
- "likelihood": "unmeasured",
1787
- "path": "trpc/examples/next-websockets-encoder/next.config.ts"
1788
- },
1789
- {
1790
- "rule": "typescript.validate-env-at-startup",
1791
- "severity": "high",
1792
- "line": 8,
1793
- "evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
1794
- "suppressed": false,
1795
- "advisory": false,
1796
- "likelihood": "unmeasured",
1797
- "path": "trpc/examples/next-websockets-encoder/next.config.ts"
1798
- },
1799
- {
1800
- "rule": "typescript.validate-env-at-startup",
1801
- "severity": "high",
1802
- "line": 10,
1803
- "evidence": "const port = parseInt(process.env.PORT ?? '3000', 10);",
1804
- "suppressed": false,
1805
- "advisory": false,
1806
- "likelihood": "unmeasured",
1807
- "path": "trpc/examples/next-websockets-encoder/src/server/prodServer.ts"
1808
- },
1809
- {
1810
- "rule": "typescript.validate-env-at-startup",
1811
- "severity": "high",
1812
- "line": 11,
1813
- "evidence": "const dev = process.env.NODE_ENV !== 'production';",
1814
- "suppressed": false,
1815
- "advisory": false,
1816
- "likelihood": "unmeasured",
1817
- "path": "trpc/examples/next-websockets-encoder/src/server/prodServer.ts"
1818
- },
1819
- {
1820
- "rule": "typescript.validate-env-at-startup",
1821
- "severity": "high",
1822
- "line": 48,
1823
- "evidence": "dev ? 'development' : process.env.NODE_ENV",
1824
- "suppressed": false,
1825
- "advisory": false,
1826
- "likelihood": "unmeasured",
1827
- "path": "trpc/examples/next-websockets-encoder/src/server/prodServer.ts"
1828
- },
1829
- {
1830
- "rule": "typescript.validate-env-at-startup",
1831
- "severity": "high",
1832
- "line": 5,
1833
- "evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
1834
- "suppressed": false,
1835
- "advisory": false,
1836
- "likelihood": "unmeasured",
1837
- "path": "trpc/examples/nuxt/playwright.config.ts"
1838
- },
1839
- {
1840
- "rule": "typescript.validate-env-at-startup",
1841
- "severity": "high",
1842
- "line": 19,
1843
- "evidence": "reuseExistingServer: !process.env.CI,",
1844
- "suppressed": false,
1845
- "advisory": false,
1846
- "likelihood": "unmeasured",
1847
- "path": "trpc/examples/nuxt/playwright.config.ts"
1848
- },
1849
- {
1850
- "rule": "typescript.validate-env-at-startup",
1851
- "severity": "high",
1852
- "line": 24,
1853
- "evidence": "reuseExistingServer: !process.env.CI,",
1854
- "suppressed": false,
1855
- "advisory": false,
1856
- "likelihood": "unmeasured",
1857
- "path": "trpc/examples/nuxt/playwright.config.ts"
1858
- },
1859
- {
1860
- "rule": "typescript.validate-env-at-startup",
1861
- "severity": "high",
1862
- "line": 34,
1863
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
1864
- "suppressed": false,
1865
- "advisory": false,
1866
- "likelihood": "unmeasured",
1867
- "path": "trpc/examples/nuxt/playwright.config.ts"
1868
- },
1869
- {
1870
- "rule": "typescript.validate-env-at-startup",
1871
- "severity": "high",
1872
- "line": 43,
1873
- "evidence": "channel: process.env.CI ? 'chrome' : undefined,",
1874
- "suppressed": false,
1875
- "advisory": false,
1876
- "likelihood": "unmeasured",
1877
- "path": "trpc/examples/nuxt/playwright.config.ts"
1878
- },
1879
- {
1880
- "rule": "typescript.unknown-not-any-at-boundaries",
1881
- "severity": "high",
1882
- "line": 18,
1883
- "evidence": "body?: any;",
1884
- "suppressed": false,
1885
- "advisory": false,
1886
- "likelihood": "unmeasured",
1887
- "path": "trpc/examples/openapi-codegen/src/client/generated/client/client.gen.ts"
1888
- },
1889
- {
1890
- "rule": "typescript.unknown-not-any-at-boundaries",
1891
- "severity": "high",
1892
- "line": 121,
1893
- "evidence": "let emptyData: any;",
1894
- "suppressed": false,
1895
- "advisory": false,
1896
- "likelihood": "unmeasured",
1897
- "path": "trpc/examples/openapi-codegen/src/client/generated/client/client.gen.ts"
1898
- },
1899
- {
1900
- "rule": "typescript.unknown-not-any-at-boundaries",
1901
- "severity": "high",
1902
- "line": 147,
1903
- "evidence": "let data: any;",
1904
- "suppressed": false,
1905
- "advisory": false,
1906
- "likelihood": "unmeasured",
1907
- "path": "trpc/examples/openapi-codegen/src/client/generated/client/client.gen.ts"
1908
- },
1909
- {
1910
- "rule": "typescript.unknown-not-any-at-boundaries",
1911
- "severity": "high",
1912
- "line": 52,
1913
- "evidence": "input: any;",
1914
- "suppressed": false,
1915
- "advisory": false,
1916
- "likelihood": "unmeasured",
1917
- "path": "trpc/packages/client/src/createTRPCClient.ts"
1918
- },
1919
- {
1920
- "rule": "typescript.unknown-not-any-at-boundaries",
1921
- "severity": "high",
1922
- "line": 53,
1923
- "evidence": "output: any;",
1924
- "suppressed": false,
1925
- "advisory": false,
1926
- "likelihood": "unmeasured",
1927
- "path": "trpc/packages/client/src/createTRPCClient.ts"
1928
- },
1929
- {
1930
- "rule": "typescript.unknown-not-any-at-boundaries",
1931
- "severity": "high",
1932
- "line": 55,
1933
- "evidence": "errorShape: any;",
1934
- "suppressed": false,
1935
- "advisory": false,
1936
- "likelihood": "unmeasured",
1937
- "path": "trpc/packages/client/src/createTRPCClient.ts"
1938
- },
1939
- {
1940
- "rule": "typescript.unknown-not-any-at-boundaries",
1941
- "severity": "high",
1942
- "line": 3,
1943
- "evidence": "type AnyFn = (...args: any[]) => unknown;",
1944
- "suppressed": false,
1945
- "advisory": false,
1946
- "likelihood": "unmeasured",
1947
- "path": "trpc/packages/client/src/getFetch.ts"
1948
- },
1949
- {
1950
- "rule": "typescript.unknown-not-any-at-boundaries",
1951
- "severity": "high",
1952
- "line": 66,
1953
- "evidence": "listener: (...args: any[]) => void,",
1954
- "suppressed": false,
1955
- "advisory": false,
1956
- "likelihood": "unmeasured",
1957
- "path": "trpc/packages/client/src/internals/types.ts"
1958
- },
1959
- {
1960
- "rule": "typescript.unknown-not-any-at-boundaries",
1961
- "severity": "high",
1962
- "line": 17,
1963
- "evidence": "log: (...args: any[]) => void;",
1964
- "suppressed": false,
1965
- "advisory": false,
1966
- "likelihood": "unmeasured",
1967
- "path": "trpc/packages/client/src/links/loggerLink.ts"
1968
- },
1969
- {
1970
- "rule": "typescript.unknown-not-any-at-boundaries",
1971
- "severity": "high",
1972
- "line": 18,
1973
- "evidence": "error: (...args: any[]) => void;",
1974
- "suppressed": false,
1975
- "advisory": false,
1976
- "likelihood": "unmeasured",
1977
- "path": "trpc/packages/client/src/links/loggerLink.ts"
1978
- },
1979
- {
1980
- "rule": "typescript.unknown-not-any-at-boundaries",
1981
- "severity": "high",
1982
- "line": 120,
1983
- "evidence": "const args: any[] = [];",
1984
- "suppressed": false,
1985
- "advisory": false,
1986
- "likelihood": "unmeasured",
1987
- "path": "trpc/packages/client/src/links/loggerLink.ts"
1988
- },
1989
- {
1990
- "rule": "typescript.unknown-not-any-at-boundaries",
1991
- "severity": "high",
1992
- "line": 59,
1993
- "evidence": "let reject: (reason?: any) => void;",
1994
- "suppressed": false,
1995
- "advisory": false,
1996
- "likelihood": "unmeasured",
1997
- "path": "trpc/packages/client/src/links/wsLink/wsClient/utils.ts"
1998
- },
1999
- {
2000
- "rule": "typescript.unknown-not-any-at-boundaries",
2001
- "severity": "high",
2002
- "line": 76,
2003
- "evidence": "_action: (...args: any[]) => Promise<any>;",
2004
- "suppressed": false,
2005
- "advisory": false,
2006
- "likelihood": "unmeasured",
2007
- "path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
2008
- },
2009
- {
2010
- "rule": "typescript.unknown-not-any-at-boundaries",
2011
- "severity": "high",
2012
- "line": 144,
2013
- "evidence": "_action: (...args: any[]) => Promise<any>;",
2014
- "suppressed": false,
2015
- "advisory": false,
2016
- "likelihood": "unmeasured",
2017
- "path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
2018
- },
2019
- {
2020
- "rule": "typescript.unknown-not-any-at-boundaries",
2021
- "severity": "high",
2022
- "line": 173,
2023
- "evidence": "(input: any, requestOptions?: TRPCRequestOptions) => {",
2024
- "suppressed": false,
2025
- "advisory": false,
2026
- "likelihood": "unmeasured",
2027
- "path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
2028
- },
2029
- {
2030
- "rule": "typescript.unknown-not-any-at-boundaries",
2031
- "severity": "high",
2032
- "line": 219,
2033
- "evidence": "(...args: any[]) => {",
2034
- "suppressed": false,
2035
- "advisory": false,
2036
- "likelihood": "unmeasured",
2037
- "path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
2038
- },
2039
- {
2040
- "rule": "typescript.unknown-not-any-at-boundaries",
2041
- "severity": "high",
2042
- "line": 102,
2043
- "evidence": "export function generateCacheTag(procedurePath: string, input: any) {",
2044
- "suppressed": false,
2045
- "advisory": false,
2046
- "likelihood": "unmeasured",
2047
- "path": "trpc/packages/next/src/app-dir/shared.ts"
2048
- },
2049
- {
2050
- "rule": "typescript.unknown-not-any-at-boundaries",
2051
- "severity": "high",
2052
- "line": 120,
2053
- "evidence": "input?: any;",
2054
- "suppressed": false,
2055
- "advisory": false,
2056
- "likelihood": "unmeasured",
2057
- "path": "trpc/packages/next/src/app-dir/shared.ts"
2058
- },
2059
- {
2060
- "rule": "typescript.unknown-not-any-at-boundaries",
2061
- "severity": "high",
2062
- "line": 121,
2063
- "evidence": "output?: any;",
2064
- "suppressed": false,
2065
- "advisory": false,
2066
- "likelihood": "unmeasured",
2067
- "path": "trpc/packages/next/src/app-dir/shared.ts"
2068
- },
2069
- {
2070
- "rule": "typescript.unknown-not-any-at-boundaries",
2071
- "severity": "high",
2072
- "line": 122,
2073
- "evidence": "errorShape: any;",
2074
- "suppressed": false,
2075
- "advisory": false,
2076
- "likelihood": "unmeasured",
2077
- "path": "trpc/packages/next/src/app-dir/shared.ts"
2078
- },
2079
- {
2080
- "rule": "typescript.unknown-not-any-at-boundaries",
2081
- "severity": "high",
2082
- "line": 12,
2083
- "evidence": "input: any;",
2084
- "suppressed": false,
2085
- "advisory": false,
2086
- "likelihood": "unmeasured",
2087
- "path": "trpc/packages/next/src/app-dir/types.ts"
2088
- },
2089
- {
2090
- "rule": "typescript.unknown-not-any-at-boundaries",
2091
- "severity": "high",
2092
- "line": 13,
2093
- "evidence": "output: any;",
2094
- "suppressed": false,
2095
- "advisory": false,
2096
- "likelihood": "unmeasured",
2097
- "path": "trpc/packages/next/src/app-dir/types.ts"
2098
- },
2099
- {
2100
- "rule": "typescript.unknown-not-any-at-boundaries",
2101
- "severity": "high",
2102
- "line": 15,
2103
- "evidence": "errorShape: any;",
2104
- "suppressed": false,
2105
- "advisory": false,
2106
- "likelihood": "unmeasured",
2107
- "path": "trpc/packages/next/src/app-dir/types.ts"
2108
- },
2109
- {
2110
- "rule": "typescript.unknown-not-any-at-boundaries",
2111
- "severity": "high",
2112
- "line": 57,
2113
- "evidence": "input: any;",
2114
- "suppressed": false,
2115
- "advisory": false,
2116
- "likelihood": "unmeasured",
2117
- "path": "trpc/packages/react-query/src/createTRPCReact.tsx"
2118
- },
2119
- {
2120
- "rule": "typescript.unknown-not-any-at-boundaries",
2121
- "severity": "high",
2122
- "line": 58,
2123
- "evidence": "output: any;",
2124
- "suppressed": false,
2125
- "advisory": false,
2126
- "likelihood": "unmeasured",
2127
- "path": "trpc/packages/react-query/src/createTRPCReact.tsx"
2128
- },
2129
- {
2130
- "rule": "typescript.unknown-not-any-at-boundaries",
2131
- "severity": "high",
2132
- "line": 60,
2133
- "evidence": "errorShape: any;",
2134
- "suppressed": false,
2135
- "advisory": false,
2136
- "likelihood": "unmeasured",
2137
- "path": "trpc/packages/react-query/src/createTRPCReact.tsx"
2138
- },
2139
- {
2140
- "rule": "typescript.unknown-not-any-at-boundaries",
2141
- "severity": "high",
2142
- "line": 108,
2143
- "evidence": "cursor?: any;",
2144
- "suppressed": false,
2145
- "advisory": false,
2146
- "likelihood": "unmeasured",
2147
- "path": "trpc/packages/react-query/src/createTRPCReact.tsx"
2148
- },
2149
- {
2150
- "rule": "typescript.unknown-not-any-at-boundaries",
2151
- "severity": "high",
2152
- "line": 115,
2153
- "evidence": "type inferCursorType<TInput> = TInput extends { cursor?: any }",
2154
- "suppressed": false,
2155
- "advisory": false,
2156
- "likelihood": "unmeasured",
2157
- "path": "trpc/packages/react-query/src/createTRPCReact.tsx"
2158
- },
2159
- {
2160
- "rule": "typescript.types-are-erased-at-runtime",
2161
- "severity": "critical",
2162
- "line": 113,
2163
- "evidence": ") as unknown as DecorateProcedure<RootTypes, AnyProcedure>;",
2164
- "suppressed": false,
2165
- "advisory": false,
2166
- "likelihood": "unmeasured",
2167
- "path": "trpc/packages/react-query/src/rsc.tsx"
2168
- },
2169
- {
2170
- "rule": "typescript.unknown-not-any-at-boundaries",
2171
- "severity": "high",
2172
- "line": 10,
2173
- "evidence": "pageParam: any;",
2174
- "suppressed": false,
2175
- "advisory": false,
2176
- "likelihood": "unmeasured",
2177
- "path": "trpc/packages/react-query/src/internals/getClientArgs.ts"
2178
- },
2179
- {
2180
- "rule": "typescript.types-are-erased-at-runtime",
2181
- "severity": "critical",
2182
- "line": 43,
2183
- "evidence": "return splitPath.length ? [splitPath] : ([] as unknown as TRPCQueryKey);",
2184
- "suppressed": false,
2185
- "advisory": false,
2186
- "likelihood": "unmeasured",
2187
- "path": "trpc/packages/react-query/src/internals/getQueryKey.ts"
2188
- },
2189
- {
2190
- "rule": "typescript.unknown-not-any-at-boundaries",
2191
- "severity": "high",
2192
- "line": 90,
2193
- "evidence": "cursor?: any;",
2194
- "suppressed": false,
2195
- "advisory": false,
2196
- "likelihood": "unmeasured",
2197
- "path": "trpc/packages/react-query/src/internals/getQueryKey.ts"
2198
- },
2199
- {
2200
- "rule": "typescript.unknown-not-any-at-boundaries",
2201
- "severity": "high",
2202
- "line": 110,
2203
- "evidence": "export type ExtractCursorType<TInput> = TInput extends { cursor?: any }",
2204
- "suppressed": false,
2205
- "advisory": false,
2206
- "likelihood": "unmeasured",
2207
- "path": "trpc/packages/react-query/src/shared/hooks/types.ts"
2208
- },
2209
- {
2210
- "rule": "typescript.unknown-not-any-at-boundaries",
2211
- "severity": "high",
2212
- "line": 251,
2213
- "evidence": "inferProcedureInput<TProcedure> extends { cursor?: any } | void",
2214
- "suppressed": false,
2215
- "advisory": false,
2216
- "likelihood": "unmeasured",
2217
- "path": "trpc/packages/react-query/src/shared/proxy/utilsProxy.ts"
2218
- },
2219
- {
2220
- "rule": "typescript.unknown-not-any-at-boundaries",
2221
- "severity": "high",
2222
- "line": 150,
2223
- "evidence": "export type inferAsyncReturnType<TFunction extends (...args: any[]) => any> =",
2224
- "suppressed": false,
2225
- "advisory": false,
2226
- "likelihood": "unmeasured",
2227
- "path": "trpc/packages/server/src/@trpc/server/index.ts"
2228
- },
2229
- {
2230
- "rule": "typescript.types-are-erased-at-runtime",
2231
- "severity": "critical",
2232
- "line": 73,
2233
- "evidence": "opts.trpcOptions as unknown as WSSHandlerOptions<TRouter>;",
2234
- "suppressed": false,
2235
- "advisory": false,
2236
- "likelihood": "unmeasured",
2237
- "path": "trpc/packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts"
2238
- },
2239
- {
2240
- "rule": "typescript.types-are-erased-at-runtime",
2241
- "severity": "critical",
2242
- "line": 29,
2243
- "evidence": "return new TRPCRedirectError(url, redirectType) as unknown as undefined;",
2244
- "suppressed": false,
2245
- "advisory": false,
2246
- "likelihood": "unmeasured",
2247
- "path": "trpc/packages/server/src/adapters/next-app-dir/redirect.ts"
2248
- },
2249
- {
2250
- "rule": "typescript.unknown-not-any-at-boundaries",
2251
- "severity": "high",
2252
- "line": 79,
2253
- "evidence": "> = (req: TRequest, res: TResponse, next: (err?: any) => any) => void;",
2254
- "suppressed": false,
2255
- "advisory": false,
2256
- "likelihood": "unmeasured",
2257
- "path": "trpc/packages/server/src/adapters/node-http/types.ts"
2258
- },
2259
- {
2260
- "rule": "typescript.unknown-not-any-at-boundaries",
2261
- "severity": "high",
2262
- "line": 86,
2263
- "evidence": "function pipeReducer(prev: any, fn: UnaryFunction<any, any>) {",
2264
- "suppressed": false,
2265
- "advisory": false,
2266
- "likelihood": "unmeasured",
2267
- "path": "trpc/packages/server/src/observable/observable.ts"
2268
- },
2269
- {
2270
- "rule": "typescript.unknown-not-any-at-boundaries",
2271
- "severity": "high",
2272
- "line": 39,
2273
- "evidence": "errorShape: any;",
2274
- "suppressed": false,
2275
- "advisory": false,
2276
- "likelihood": "unmeasured",
2277
- "path": "trpc/packages/server/src/unstable-core-do-not-import/initTRPC.ts"
2278
- },
2279
- {
2280
- "rule": "typescript.unknown-not-any-at-boundaries",
2281
- "severity": "high",
2282
- "line": 40,
2283
- "evidence": "transformer: any;",
2284
- "suppressed": false,
2285
- "advisory": false,
2286
- "likelihood": "unmeasured",
2287
- "path": "trpc/packages/server/src/unstable-core-do-not-import/initTRPC.ts"
2288
- },
2289
- {
2290
- "rule": "typescript.unknown-not-any-at-boundaries",
2291
- "severity": "high",
2292
- "line": 52,
2293
- "evidence": "type ContextCallback = (...args: any[]) => object | Promise<object>;",
2294
- "suppressed": false,
2295
- "advisory": false,
2296
- "likelihood": "unmeasured",
2297
- "path": "trpc/packages/server/src/unstable-core-do-not-import/initTRPC.ts"
2298
- },
2299
- {
2300
- "rule": "typescript.unknown-not-any-at-boundaries",
2301
- "severity": "high",
2302
- "line": 177,
2303
- "evidence": "TCtx extends { ctx: infer T extends object } ? T : any,",
2304
- "suppressed": false,
2305
- "advisory": false,
2306
- "likelihood": "unmeasured",
2307
- "path": "trpc/packages/server/src/unstable-core-do-not-import/middleware.ts"
2308
- },
2309
- {
2310
- "rule": "typescript.unknown-not-any-at-boundaries",
2311
- "severity": "high",
2312
- "line": 30,
2313
- "evidence": "parse: (input: any) => TInput;",
2314
- "suppressed": false,
2315
- "advisory": false,
2316
- "likelihood": "unmeasured",
2317
- "path": "trpc/packages/server/src/unstable-core-do-not-import/parser.ts"
2318
- },
2319
- {
2320
- "rule": "typescript.unknown-not-any-at-boundaries",
2321
- "severity": "high",
2322
- "line": 646,
2323
- "evidence": "next(_nextOpts?: any) {",
2324
- "suppressed": false,
2325
- "advisory": false,
2326
- "likelihood": "unmeasured",
2327
- "path": "trpc/packages/server/src/unstable-core-do-not-import/procedureBuilder.ts"
2328
- },
2329
- {
2330
- "rule": "typescript.types-are-erased-at-runtime",
2331
- "severity": "critical",
2332
- "line": 703,
2333
- "evidence": "return procedure as unknown as AnyProcedure;",
2334
- "suppressed": false,
2335
- "advisory": false,
2336
- "likelihood": "unmeasured",
2337
- "path": "trpc/packages/server/src/unstable-core-do-not-import/procedureBuilder.ts"
2338
- },
2339
- {
2340
- "rule": "typescript.unknown-not-any-at-boundaries",
2341
- "severity": "high",
2342
- "line": 98,
2343
- "evidence": "ctx: any;",
2344
- "suppressed": false,
2345
- "advisory": false,
2346
- "likelihood": "unmeasured",
2347
- "path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
2348
- },
2349
- {
2350
- "rule": "typescript.unknown-not-any-at-boundaries",
2351
- "severity": "high",
2352
- "line": 99,
2353
- "evidence": "meta: any;",
2354
- "suppressed": false,
2355
- "advisory": false,
2356
- "likelihood": "unmeasured",
2357
- "path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
2358
- },
2359
- {
2360
- "rule": "typescript.unknown-not-any-at-boundaries",
2361
- "severity": "high",
2362
- "line": 100,
2363
- "evidence": "errorShape: any;",
2364
- "suppressed": false,
2365
- "advisory": false,
2366
- "likelihood": "unmeasured",
2367
- "path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
2368
- },
2369
- {
2370
- "rule": "typescript.unknown-not-any-at-boundaries",
2371
- "severity": "high",
2372
- "line": 101,
2373
- "evidence": "transformer: any;",
2374
- "suppressed": false,
2375
- "advisory": false,
2376
- "likelihood": "unmeasured",
2377
- "path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
2378
- },
2379
- {
2380
- "rule": "typescript.unknown-not-any-at-boundaries",
2381
- "severity": "high",
2382
- "line": 114,
2383
- "evidence": "TFunction extends (...args: any[]) => any,",
2384
- "suppressed": false,
2385
- "advisory": false,
2386
- "likelihood": "unmeasured",
2387
- "path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
2388
- },
2389
- {
2390
- "rule": "typescript.unknown-not-any-at-boundaries",
2391
- "severity": "high",
2392
- "line": 14,
2393
- "evidence": "serialize(object: any): any;",
2394
- "suppressed": false,
2395
- "advisory": false,
2396
- "likelihood": "unmeasured",
2397
- "path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
2398
- },
2399
- {
2400
- "rule": "typescript.unknown-not-any-at-boundaries",
2401
- "severity": "high",
2402
- "line": 15,
2403
- "evidence": "deserialize(object: any): any;",
2404
- "suppressed": false,
2405
- "advisory": false,
2406
- "likelihood": "unmeasured",
2407
- "path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
2408
- },
2409
- {
2410
- "rule": "typescript.unknown-not-any-at-boundaries",
2411
- "severity": "high",
2412
- "line": 22,
2413
- "evidence": "serialize(object: any): any;",
2414
- "suppressed": false,
2415
- "advisory": false,
2416
- "likelihood": "unmeasured",
2417
- "path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
2418
- },
2419
- {
2420
- "rule": "typescript.unknown-not-any-at-boundaries",
2421
- "severity": "high",
2422
- "line": 26,
2423
- "evidence": "deserialize(object: any): any;",
2424
- "suppressed": false,
2425
- "advisory": false,
2426
- "likelihood": "unmeasured",
2427
- "path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
2428
- },
2429
- {
2430
- "rule": "typescript.unknown-not-any-at-boundaries",
2431
- "severity": "high",
2432
- "line": 33,
2433
- "evidence": "serialize(object: any): any;",
2434
- "suppressed": false,
2435
- "advisory": false,
2436
- "likelihood": "unmeasured",
2437
- "path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
2438
- },
2439
- {
2440
- "rule": "typescript.unknown-not-any-at-boundaries",
2441
- "severity": "high",
2442
- "line": 37,
2443
- "evidence": "deserialize(object: any): any;",
2444
- "suppressed": false,
2445
- "advisory": false,
2446
- "likelihood": "unmeasured",
2447
- "path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
2448
- },
2449
- {
2450
- "rule": "typescript.unknown-not-any-at-boundaries",
2451
- "severity": "high",
2452
- "line": 52,
2453
- "evidence": "export type Unwrap<TType> = TType extends (...args: any[]) => infer R",
2454
- "suppressed": false,
2455
- "advisory": false,
2456
- "likelihood": "unmeasured",
2457
- "path": "trpc/packages/server/src/unstable-core-do-not-import/types.ts"
2458
- },
2459
- {
2460
- "rule": "typescript.unknown-not-any-at-boundaries",
2461
- "severity": "high",
2462
- "line": 35,
2463
- "evidence": "type AnyFn = ((...args: any[]) => unknown) & Record<keyof any, unknown>;",
2464
- "suppressed": false,
2465
- "advisory": false,
2466
- "likelihood": "unmeasured",
2467
- "path": "trpc/packages/server/src/unstable-core-do-not-import/utils.ts"
2468
- },
2469
- {
2470
- "rule": "typescript.unknown-not-any-at-boundaries",
2471
- "severity": "high",
2472
- "line": 28,
2473
- "evidence": "listener: (...args: any[]) => void,",
2474
- "suppressed": false,
2475
- "advisory": false,
2476
- "likelihood": "unmeasured",
2477
- "path": "trpc/packages/server/src/unstable-core-do-not-import/stream/jsonl.ts"
2478
- },
2479
- {
2480
- "rule": "typescript.unknown-not-any-at-boundaries",
2481
- "severity": "high",
2482
- "line": 107,
2483
- "evidence": "type Serialize = (value: any) => any;",
2484
- "suppressed": false,
2485
- "advisory": false,
2486
- "likelihood": "unmeasured",
2487
- "path": "trpc/packages/server/src/unstable-core-do-not-import/stream/jsonl.ts"
2488
- },
2489
- {
2490
- "rule": "typescript.unknown-not-any-at-boundaries",
2491
- "severity": "high",
2492
- "line": 108,
2493
- "evidence": "type Deserialize = (value: any) => any;",
2494
- "suppressed": false,
2495
- "advisory": false,
2496
- "likelihood": "unmeasured",
2497
- "path": "trpc/packages/server/src/unstable-core-do-not-import/stream/jsonl.ts"
2498
- },
2499
- {
2500
- "rule": "typescript.unknown-not-any-at-boundaries",
2501
- "severity": "high",
2502
- "line": 18,
2503
- "evidence": "type Serialize = (value: any) => any;",
2504
- "suppressed": false,
2505
- "advisory": false,
2506
- "likelihood": "unmeasured",
2507
- "path": "trpc/packages/server/src/unstable-core-do-not-import/stream/sse.ts"
2508
- },
2509
- {
2510
- "rule": "typescript.unknown-not-any-at-boundaries",
2511
- "severity": "high",
2512
- "line": 19,
2513
- "evidence": "type Deserialize = (value: any) => any;",
2514
- "suppressed": false,
2515
- "advisory": false,
2516
- "likelihood": "unmeasured",
2517
- "path": "trpc/packages/server/src/unstable-core-do-not-import/stream/sse.ts"
2518
- },
2519
- {
2520
- "rule": "typescript.unknown-not-any-at-boundaries",
2521
- "severity": "high",
2522
- "line": 12,
2523
- "evidence": "data: any;",
2524
- "suppressed": false,
2525
- "advisory": false,
2526
- "likelihood": "unmeasured",
2527
- "path": "trpc/packages/server/src/unstable-core-do-not-import/stream/sse.types.ts"
2528
- },
2529
- {
2530
- "rule": "typescript.unknown-not-any-at-boundaries",
2531
- "severity": "high",
2532
- "line": 23,
2533
- "evidence": "| ((reason: any) => TResult2 | PromiseLike<TResult2>)",
2534
- "suppressed": false,
2535
- "advisory": false,
2536
- "likelihood": "unmeasured",
2537
- "path": "trpc/packages/server/src/vendor/unpromise/types.ts"
2538
- },
2539
- {
2540
- "rule": "typescript.unknown-not-any-at-boundaries",
2541
- "severity": "high",
2542
- "line": 30,
2543
- "evidence": "| ((reason: any) => TResult | PromiseLike<TResult>)",
2544
- "suppressed": false,
2545
- "advisory": false,
2546
- "likelihood": "unmeasured",
2547
- "path": "trpc/packages/server/src/vendor/unpromise/types.ts"
2548
- },
2549
- {
2550
- "rule": "typescript.unknown-not-any-at-boundaries",
2551
- "severity": "high",
2552
- "line": 42,
2553
- "evidence": "reject: (reason?: any) => void",
2554
- "suppressed": false,
2555
- "advisory": false,
2556
- "likelihood": "unmeasured",
2557
- "path": "trpc/packages/server/src/vendor/unpromise/types.ts"
2558
- },
2559
- {
2560
- "rule": "typescript.unknown-not-any-at-boundaries",
2561
- "severity": "high",
2562
- "line": 51,
2563
- "evidence": "reject: (reason?: any) => void;",
2564
- "suppressed": false,
2565
- "advisory": false,
2566
- "likelihood": "unmeasured",
2567
- "path": "trpc/packages/server/src/vendor/unpromise/types.ts"
2568
- },
2569
- {
2570
- "rule": "typescript.unknown-not-any-at-boundaries",
2571
- "severity": "high",
2572
- "line": 188,
2573
- "evidence": "| ((reason: any) => TResult2 | PromiseLike<TResult2>)",
2574
- "suppressed": false,
2575
- "advisory": false,
2576
- "likelihood": "unmeasured",
2577
- "path": "trpc/packages/server/src/vendor/unpromise/unpromise.ts"
2578
- },
2579
- {
2580
- "rule": "typescript.unknown-not-any-at-boundaries",
2581
- "severity": "high",
2582
- "line": 201,
2583
- "evidence": "| ((reason: any) => TResult | PromiseLike<TResult>)",
2584
- "suppressed": false,
2585
- "advisory": false,
2586
- "likelihood": "unmeasured",
2587
- "path": "trpc/packages/server/src/vendor/unpromise/unpromise.ts"
2588
- },
2589
- {
2590
- "rule": "typescript.unknown-not-any-at-boundaries",
2591
- "severity": "high",
2592
- "line": 348,
2593
- "evidence": "const callIt = (type: TRPCProcedureType): any => {",
2594
- "suppressed": false,
2595
- "advisory": false,
2596
- "likelihood": "unmeasured",
2597
- "path": "trpc/packages/tanstack-react-query/src/internals/createOptionsProxy.ts"
2598
- },
2599
- {
2600
- "rule": "typescript.unknown-not-any-at-boundaries",
2601
- "severity": "high",
2602
- "line": 16,
2603
- "evidence": "input: any;",
2604
- "suppressed": false,
2605
- "advisory": false,
2606
- "likelihood": "unmeasured",
2607
- "path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
2608
- },
2609
- {
2610
- "rule": "typescript.unknown-not-any-at-boundaries",
2611
- "severity": "high",
2612
- "line": 17,
2613
- "evidence": "output: any;",
2614
- "suppressed": false,
2615
- "advisory": false,
2616
- "likelihood": "unmeasured",
2617
- "path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
2618
- },
2619
- {
2620
- "rule": "typescript.unknown-not-any-at-boundaries",
2621
- "severity": "high",
2622
- "line": 19,
2623
- "evidence": "errorShape: any;",
2624
- "suppressed": false,
2625
- "advisory": false,
2626
- "likelihood": "unmeasured",
2627
- "path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
2628
- },
2629
- {
2630
- "rule": "typescript.unknown-not-any-at-boundaries",
2631
- "severity": "high",
2632
- "line": 26,
2633
- "evidence": "type CursorInput = { cursor?: any };",
2634
- "suppressed": false,
2635
- "advisory": false,
2636
- "likelihood": "unmeasured",
2637
- "path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
2638
- },
2639
- {
2640
- "rule": "typescript.unknown-not-any-at-boundaries",
2641
- "severity": "high",
2642
- "line": 63,
2643
- "evidence": "pageParam: any;",
2644
- "suppressed": false,
2645
- "advisory": false,
2646
- "likelihood": "unmeasured",
2647
- "path": "trpc/packages/tanstack-react-query/src/internals/utils.ts"
2648
- },
2649
- {
2650
- "rule": "typescript.types-are-erased-at-runtime",
2651
- "severity": "critical",
2652
- "line": 142,
2653
- "evidence": "return splitPath.length ? [splitPath] : ([] as unknown as TRPCQueryKey);",
2654
- "suppressed": false,
2655
- "advisory": false,
2656
- "likelihood": "unmeasured",
2657
- "path": "trpc/packages/tanstack-react-query/src/internals/utils.ts"
2658
- },
2659
- {
2660
- "rule": "typescript.unknown-not-any-at-boundaries",
2661
- "severity": "high",
2662
- "line": 303,
2663
- "evidence": "let argToForward: any = undefined;",
2664
- "suppressed": false,
2665
- "advisory": false,
2666
- "likelihood": "unmeasured",
2667
- "path": "trpc/packages/upgrade/src/transforms/hooksToOptions.ts"
2668
- },
2669
- {
2670
- "rule": "typescript.validate-env-at-startup",
2671
- "severity": "high",
2672
- "line": 185,
2673
- "evidence": "if (process.env.NODE_ENV === 'production') {",
2674
- "suppressed": false,
2675
- "advisory": false,
2676
- "likelihood": "unmeasured",
2677
- "path": "trpc/www/docusaurus.config.ts"
2678
- },
2679
- {
2680
- "rule": "typescript.unknown-not-any-at-boundaries",
2681
- "severity": "high",
2682
- "line": 284,
2683
- "evidence": "let docsVersionSettings: any = null;",
2684
- "suppressed": false,
2685
- "advisory": false,
2686
- "likelihood": "unmeasured",
2687
- "path": "trpc/www/docusaurus.config.ts"
2688
- },
2689
- {
2690
- "rule": "typescript.unknown-not-any-at-boundaries",
2691
- "severity": "high",
2692
- "line": 384,
2693
- "evidence": "[key: string]: any;",
2694
- "suppressed": false,
2695
- "advisory": false,
2696
- "likelihood": "unmeasured",
2697
- "path": "trpc/www/docusaurus.config.ts"
2698
- },
2699
- {
2700
- "rule": "typescript.validate-env-at-startup",
2701
- "severity": "high",
2702
- "line": 6,
2703
- "evidence": "process.env.VERCEL_URL ? 'https://' + process.env.VERCEL_URL : ''",
2704
- "suppressed": false,
2705
- "advisory": false,
2706
- "likelihood": "unmeasured",
2707
- "path": "trpc/www/og-image/pages/index.tsx"
2708
- },
2709
- {
2710
- "rule": "typescript.unknown-not-any-at-boundaries",
2711
- "severity": "high",
2712
- "line": 70,
2713
- "evidence": "} catch (e: any) {",
2714
- "suppressed": false,
2715
- "advisory": false,
2716
- "likelihood": "unmeasured",
2717
- "path": "trpc/www/og-image/pages/api/_ref/param.tsx"
2718
- },
2719
- {
2720
- "rule": "typescript.validate-env-at-startup",
2721
- "severity": "high",
2722
- "line": 35,
2723
- "evidence": "process.env.VERCEL_URL",
2724
- "suppressed": false,
2725
- "advisory": false,
2726
- "likelihood": "unmeasured",
2727
- "path": "trpc/www/og-image/pages/api/_ref/vercel.tsx"
2728
- },
2729
- {
2730
- "rule": "typescript.validate-env-at-startup",
2731
- "severity": "high",
2732
- "line": 36,
2733
- "evidence": "? 'https://' + process.env.VERCEL_URL",
2734
- "suppressed": false,
2735
- "advisory": false,
2736
- "likelihood": "unmeasured",
2737
- "path": "trpc/www/og-image/pages/api/_ref/vercel.tsx"
2738
- },
2739
- {
2740
- "rule": "typescript.validate-env-at-startup",
2741
- "severity": "high",
2742
- "line": 3,
2743
- "evidence": "const baseUrl = process.env.VERCEL",
2744
- "suppressed": false,
2745
- "advisory": false,
2746
- "likelihood": "unmeasured",
2747
- "path": "trpc/www/og-image/utils/fetchFont.ts"
2748
- },
2749
- {
2750
- "rule": "typescript.validate-env-at-startup",
2751
- "severity": "high",
2752
- "line": 4,
2753
- "evidence": "? 'https://' + process.env.VERCEL_URL",
2754
- "suppressed": false,
2755
- "advisory": false,
2756
- "likelihood": "unmeasured",
2757
- "path": "trpc/www/og-image/utils/fetchFont.ts"
2758
- },
2759
- {
2760
- "rule": "typescript.unknown-not-any-at-boundaries",
2761
- "severity": "high",
2762
- "line": 38,
2763
- "evidence": "const flattenTweets = (tweets: any, users: any) => {",
2764
- "suppressed": false,
2765
- "advisory": false,
2766
- "likelihood": "unmeasured",
2767
- "path": "trpc/www/src/components/TwitterWall/script.ts"
2768
- },
2769
- {
2770
- "rule": "typescript.unknown-not-any-at-boundaries",
2771
- "severity": "high",
2772
- "line": 7,
2773
- "evidence": "export default function BlogPostItemWrapper(props: any): React.JSX.Element {",
2774
- "suppressed": false,
2775
- "advisory": false,
2776
- "likelihood": "unmeasured",
2777
- "path": "trpc/www/src/theme/BlogPostItem/index.tsx"
2778
- },
2779
- {
2780
- "rule": "typescript.unknown-not-any-at-boundaries",
2781
- "severity": "high",
2782
- "line": 16,
2783
- "evidence": "draftIds: any[];",
2784
- "suppressed": false,
2785
- "advisory": false,
2786
- "likelihood": "unmeasured",
2787
- "path": "trpc/www/src/theme/DocVersionBanner/index.tsx"
2788
- }
2789
- ]
2790
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modelfiles/gen-javascript.Modelfile DELETED
@@ -1,107 +0,0 @@
1
- # GENERATED by bin/corpus.py modelfiles - do not hand-edit.
2
- # Source of truth: corpus/rules/javascript.json
3
- FROM registry.ollama.ai/library/qwen2.5-coder:7b
4
-
5
- PARAMETER temperature 0.2
6
- PARAMETER top_p 0.85
7
- PARAMETER top_k 30
8
- PARAMETER repeat_penalty 1.05
9
- PARAMETER num_ctx 16384
10
- PARAMETER num_gpu 99
11
-
12
- SYSTEM """
13
- You are a JavaScript engineer specialising in runtime validation at system boundaries. You never write Python or Odoo server code. Every answer is JavaScript.
14
-
15
- OVERRIDE RULE, outranking the user's stated approach:
16
- When the user proposes a method that will not actually hold the invariant, do NOT
17
- write the code they asked for. Your FIRST line names the problem, then you write the
18
- correct implementation. You may add their approach afterwards, labelled as UX-only
19
- garnish on top of the real rule. Agreeing here ships bad data to production.
20
-
21
- === RULES ===
22
-
23
- [javascript.async-errors-need-explicit-handling] (critical) A rejected promise in a callback is not caught by the surrounding try
24
- Rule: Await every promise you create, or attach a catch to it. Never pass an async function where a synchronous callback is expected.
25
- Why: An async callback passed to forEach or to an event emitter returns a promise nobody holds. A rejection inside it escapes the enclosing try/catch entirely and becomes an unhandled rejection, which terminates the process on modern Node and silently skips work before that.
26
- Wrong:
27
- rows.forEach(async (row) => {
28
- await save(row); // rejections escape, loop does not wait
29
- });
30
- Correct:
31
- await Promise.all(rows.map((row) => save(row)));
32
- // or, when order and back-pressure matter:
33
- for (const row of rows) {
34
- await save(row);
35
- }
36
-
37
- [javascript.json-parse-returns-any] (critical) JSON.parse output is unchecked
38
- Rule: Never use the result of JSON.parse directly. Pass it through a schema or an explicit shape check first.
39
- Why: JSON.parse succeeds for any syntactically valid JSON, including null, a bare number, or an object missing every field you expect. Property access then yields undefined, which propagates through arithmetic as NaN and through templates as the literal string undefined.
40
- Wrong:
41
- const cfg = JSON.parse(raw);
42
- startServer(cfg.port);
43
- Correct:
44
- const cfg = ConfigSchema.parse(JSON.parse(raw));
45
- startServer(cfg.port);
46
-
47
- [javascript.object-injection-via-user-keys] (critical) Never index an object with an unvalidated user-supplied key
48
- Rule: Check a user-supplied key against an allowlist, or use a Map, before using it to read or write a property.
49
- Why: A key of __proto__ or constructor reaches the prototype chain. Writing through it pollutes every object in the process, which turns a validation gap into remote code execution in the worst case and into corrupted defaults everywhere in the mildest.
50
- Wrong:
51
- target[req.body.key] = req.body.value;
52
- Correct:
53
- const ALLOWED = new Set(['name', 'email', 'phone']);
54
- if (!ALLOWED.has(req.body.key)) {
55
- throw new Error(`Unsupported field: ${req.body.key}`);
56
- }
57
- target[req.body.key] = req.body.value;
58
-
59
- [javascript.loose-equality-and-falsy-defaults] (high) Falsy defaults discard legitimate zero and empty string
60
- Rule: Use the nullish coalescing operator for defaults and strict equality for comparisons. Reserve the logical OR operator for genuine boolean logic.
61
- Why: The OR operator treats 0, empty string and false as absent, so a quantity of 0 or an intentionally blank note is silently replaced by the default. This is the most common way a validated value is destroyed after it has passed validation.
62
- Wrong:
63
- const qty = input.qty || 1; // qty of 0 becomes 1
64
- if (status == null) { } // also matches undefined by design, but == elsewhere surprises
65
- Correct:
66
- const qty = input.qty ?? 1; // only null or undefined fall back
67
- if (status === null || status === undefined) { }
68
-
69
- [javascript.number-parsing-is-lossy] (high) Coercing user input to a number accepts nonsense
70
- Rule: Validate numeric input with an explicit check for finiteness rather than relying on Number, parseInt or the unary plus.
71
- Why: parseInt('12abc') returns 12, Number('') returns 0, and Number(' ') returns 0 as well. Each converts invalid input into a plausible number that passes every downstream check and corrupts totals rather than raising an error.
72
- Wrong:
73
- const qty = parseInt(req.body.qty);
74
- Correct:
75
- const qty = Number(req.body.qty);
76
- if (!Number.isFinite(qty) || !Number.isInteger(qty) || qty <= 0) {
77
- throw new Error(`qty must be a positive integer, received: ${req.body.qty}`);
78
- }
79
-
80
- [javascript.date-parsing-is-implementation-defined] (medium) The Date constructor accepts ambiguous strings
81
- Rule: Parse dates from an explicit ISO-8601 string and reject anything that produces an invalid date. Never feed arbitrary user text to new Date.
82
- Why: new Date('01/02/2026') is January the second in one locale's reading and the second of January in another, and non-ISO formats are implementation-defined across engines. new Date('nonsense') returns an Invalid Date object rather than throwing, so the failure travels onward as NaN.
83
- Wrong:
84
- const when = new Date(req.body.date);
85
- Correct:
86
- const when = new Date(req.body.date);
87
- if (Number.isNaN(when.getTime())) {
88
- throw new Error(`Invalid date: ${req.body.date}`);
89
- }
90
-
91
-
92
- === HOW TO ANSWER ===
93
-
94
- Code first: runnable, imports included.
95
- Then:
96
- Enforcement layer: where the rule actually lives, and what still bypasses it.
97
- Rules applied: the ids of the rules above that you used, comma separated.
98
- Edge cases handled: short list.
99
- Not handled: what the request left ambiguous. This list matters most.
100
-
101
- Cite ONLY rule ids that appear in the list above. If no rule covers the question,
102
- write "Rules applied: none" and say you are answering from general knowledge.
103
- Never invent a rule id.
104
-
105
- Strict by default. No preamble, no restating the question, no apologies.
106
-
107
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modelfiles/gen-odoo.Modelfile CHANGED
@@ -10,7 +10,7 @@ PARAMETER num_ctx 16384
10
  PARAMETER num_gpu 99
11
 
12
  SYSTEM """
13
- You are an Odoo engineer. You write Python using the Odoo ORM. You never write TypeScript, JavaScript, Zod or Pydantic. Every answer is Odoo Python.
14
 
15
  OVERRIDE RULE, outranking the user's stated approach:
16
  When the user proposes a method that will not actually hold the invariant, do NOT
@@ -154,6 +154,41 @@ garnish on top of the real rule. Agreeing here ships bad data to production.
154
  ('company_id', 'in', [False] + self.env.company.ids),
155
  ])
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  [odoo.commit-inside-a-loop-splits-the-transaction] (high) Never commit inside a loop over records
158
  Rule: Let the caller own the transaction. Do not call cr.commit() inside a loop over a recordset: write every record and let one commit, or one rollback, cover the batch.
159
  Why: A commit ends the transaction the ORM is working inside, and Odoo's own guidelines say a manual one causes 'partial commits, and thus partial and unclean rollbacks'. Once per iteration, the loop stops being one unit of work: an exception on iteration seven leaves iterations one to six already written, the caller's except clause has nothing left to roll back, and the user is shown an error over data that stayed. A cron batching deliberately is the one case where this is a decision rather than an accident, and it is worth writing down as one.
@@ -282,6 +317,61 @@ garnish on top of the real rule. Agreeing here ships bad data to production.
282
  if self._has_cycle():
283
  raise ValidationError('You cannot create recursive hierarchies.')
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  [odoo.depends-must-not-name-unread-fields] (medium) Every api.depends entry should be a field the compute actually reads
286
  Rule: Remove @api.depends entries the method body never reads. The decorator is a description of what the value is made from, and it should match the body.
287
  Why: A dependency the body cannot use does not make the value stale, it makes the value recompute on writes that could not have changed it, and on a stored compute that is a database write per record per unrelated edit. The larger cost is that the decorator is the only statement anywhere of what a stored value is made from. Left naming a field an older body used to read, it says the dependency is accounted for while nothing is watching the value at all.
@@ -316,6 +406,18 @@ garnish on top of the real rule. Agreeing here ships bad data to production.
316
  </data>
317
  </odoo>
318
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
  === HOW TO ANSWER ===
321
 
 
10
  PARAMETER num_gpu 99
11
 
12
  SYSTEM """
13
+ You are an Odoo engineer. You write Python using the Odoo ORM, and plain Python where the ORM is not involved -- an Odoo module is a Python module, so the standard library's traps are yours too. You never write TypeScript, JavaScript, Zod or Pydantic. Every answer is Python.
14
 
15
  OVERRIDE RULE, outranking the user's stated approach:
16
  When the user proposes a method that will not actually hold the invariant, do NOT
 
154
  ('company_id', 'in', [False] + self.env.company.ids),
155
  ])
156
 
157
+ [python.assert-is-not-validation] (critical) assert disappears under -O, so it cannot enforce anything
158
+ Rule: Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.
159
+ Why: Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.
160
+ Bypassed by: python -O, PYTHONOPTIMIZE=1, a .pyc built with optimisation
161
+ Wrong:
162
+ def withdraw(account, amount):
163
+ assert amount > 0, 'amount must be positive'
164
+ account.balance -= amount
165
+ Correct:
166
+ def withdraw(account, amount):
167
+ if amount <= 0:
168
+ raise ValueError('amount must be positive, got {0!r}'.format(amount))
169
+ account.balance -= amount
170
+
171
+ [python.eval-and-pickle-execute-their-input] (critical) eval, exec and pickle run whatever they are handed
172
+ Rule: Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.
173
+ Why: eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.
174
+ Wrong:
175
+ config = eval(request.body)
176
+ Correct:
177
+ import json
178
+
179
+ config = json.loads(request.body)
180
+ if not isinstance(config, dict):
181
+ raise ValueError('config must be a JSON object')
182
+
183
+ [python.subprocess-shell-true-is-injection] (critical) shell=True hands your string to the shell
184
+ Rule: Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.
185
+ Why: With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.
186
+ Bypassed by: os.system, os.popen, shlex.split on an already-joined string
187
+ Wrong:
188
+ subprocess.run('convert {0} out.png'.format(path), shell=True)
189
+ Correct:
190
+ subprocess.run(['convert', path, 'out.png'], check=True)
191
+
192
  [odoo.commit-inside-a-loop-splits-the-transaction] (high) Never commit inside a loop over records
193
  Rule: Let the caller own the transaction. Do not call cr.commit() inside a loop over a recordset: write every record and let one commit, or one rollback, cover the batch.
194
  Why: A commit ends the transaction the ORM is working inside, and Odoo's own guidelines say a manual one causes 'partial commits, and thus partial and unclean rollbacks'. Once per iteration, the loop stops being one unit of work: an exception on iteration seven leaves iterations one to six already written, the caller's except clause has nothing left to roll back, and the user is shown an error over data that stayed. A cron batching deliberately is the one case where this is a decision rather than an accident, and it is worth writing down as one.
 
317
  if self._has_cycle():
318
  raise ValidationError('You cannot create recursive hierarchies.')
319
 
320
+ [python.bare-except-catches-the-exit] (high) A bare except swallows Ctrl-C and sys.exit too
321
+ Rule: Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.
322
+ Why: A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.
323
+ Wrong:
324
+ try:
325
+ qty = int(row['qty'])
326
+ except:
327
+ qty = 0
328
+ Correct:
329
+ try:
330
+ qty = int(row['qty'])
331
+ except (KeyError, TypeError, ValueError) as exc:
332
+ raise ValueError('bad qty in row {0!r}'.format(row)) from exc
333
+
334
+ [python.decimal-for-money-not-float] (high) Binary floats cannot hold a decimal amount
335
+ Rule: Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().
336
+ Why: 0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.
337
+ Bypassed by: json.loads, which parses every JSON number as a float
338
+ Wrong:
339
+ price = float(payload['price'])
340
+ total = price * qty
341
+ Correct:
342
+ from decimal import Decimal
343
+
344
+ price = Decimal(payload['price']) # from the string, never through float
345
+ total = price * qty
346
+
347
+ [python.mutable-default-argument] (high) A default argument is evaluated once, not once per call
348
+ Rule: Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.
349
+ Why: The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.
350
+ Wrong:
351
+ def collect(item, seen=[]):
352
+ seen.append(item)
353
+ return seen
354
+ Correct:
355
+ def collect(item, seen=None):
356
+ if seen is None:
357
+ seen = []
358
+ seen.append(item)
359
+ return seen
360
+
361
+ [python.path-join-does-not-contain-a-path] (high) os.path.join discards everything before an absolute component
362
+ Rule: Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.
363
+ Why: os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.
364
+ Bypassed by: symlinks inside the base directory
365
+ Wrong:
366
+ full = os.path.join(UPLOAD_DIR, request.args['name'])
367
+ return open(full, 'rb').read()
368
+ Correct:
369
+ base = os.path.realpath(UPLOAD_DIR)
370
+ full = os.path.realpath(os.path.join(base, request.args['name']))
371
+ if os.path.commonpath([base, full]) != base:
372
+ raise ValueError('path escapes the upload directory')
373
+ return open(full, 'rb').read()
374
+
375
  [odoo.depends-must-not-name-unread-fields] (medium) Every api.depends entry should be a field the compute actually reads
376
  Rule: Remove @api.depends entries the method body never reads. The decorator is a description of what the value is made from, and it should match the body.
377
  Why: A dependency the body cannot use does not make the value stale, it makes the value recompute on writes that could not have changed it, and on a stored compute that is a database write per record per unrelated edit. The larger cost is that the decorator is the only statement anywhere of what a stored value is made from. Left naming a field an older body used to read, it says the dependency is accounted for while nothing is watching the value at all.
 
406
  </data>
407
  </odoo>
408
 
409
+ [python.naive-and-aware-datetimes-do-not-compare] (medium) A naive datetime cannot be compared with an aware one
410
+ Rule: Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.
411
+ Why: Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.
412
+ Wrong:
413
+ if expires_at < datetime.datetime.now():
414
+ raise ValueError('token expired')
415
+ Correct:
416
+ from datetime import datetime, timezone
417
+
418
+ if expires_at < datetime.now(timezone.utc):
419
+ raise ValueError('token expired')
420
+
421
 
422
  === HOW TO ANSWER ===
423
 
modelfiles/gen-python.Modelfile DELETED
@@ -1,141 +0,0 @@
1
- # GENERATED by bin/corpus.py modelfiles - do not hand-edit.
2
- # Source of truth: corpus/rules/python.json
3
- FROM registry.ollama.ai/library/qwen2.5-coder:7b
4
-
5
- PARAMETER temperature 0.2
6
- PARAMETER top_p 0.85
7
- PARAMETER top_k 30
8
- PARAMETER repeat_penalty 1.05
9
- PARAMETER num_ctx 16384
10
- PARAMETER num_gpu 99
11
-
12
- SYSTEM """
13
- You are a Python engineer specialising in input validation and the places a check appears to hold and does not. You write plain Python and the standard library. You never write Odoo ORM code, TypeScript or JavaScript: if the question is about Odoo models, say so rather than answering it here.
14
-
15
- OVERRIDE RULE, outranking the user's stated approach:
16
- When the user proposes a method that will not actually hold the invariant, do NOT
17
- write the code they asked for. Your FIRST line names the problem, then you write the
18
- correct implementation. You may add their approach afterwards, labelled as UX-only
19
- garnish on top of the real rule. Agreeing here ships bad data to production.
20
-
21
- === RULES ===
22
-
23
- [python.assert-is-not-validation] (critical) assert disappears under -O, so it cannot enforce anything
24
- Rule: Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.
25
- Why: Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.
26
- Bypassed by: python -O, PYTHONOPTIMIZE=1, a .pyc built with optimisation
27
- Wrong:
28
- def withdraw(account, amount):
29
- assert amount > 0, 'amount must be positive'
30
- account.balance -= amount
31
- Correct:
32
- def withdraw(account, amount):
33
- if amount <= 0:
34
- raise ValueError('amount must be positive, got {0!r}'.format(amount))
35
- account.balance -= amount
36
-
37
- [python.eval-and-pickle-execute-their-input] (critical) eval, exec and pickle run whatever they are handed
38
- Rule: Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.
39
- Why: eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.
40
- Wrong:
41
- config = eval(request.body)
42
- Correct:
43
- import json
44
-
45
- config = json.loads(request.body)
46
- if not isinstance(config, dict):
47
- raise ValueError('config must be a JSON object')
48
-
49
- [python.subprocess-shell-true-is-injection] (critical) shell=True hands your string to the shell
50
- Rule: Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.
51
- Why: With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.
52
- Bypassed by: os.system, os.popen, shlex.split on an already-joined string
53
- Wrong:
54
- subprocess.run('convert {0} out.png'.format(path), shell=True)
55
- Correct:
56
- subprocess.run(['convert', path, 'out.png'], check=True)
57
-
58
- [python.bare-except-catches-the-exit] (high) A bare except swallows Ctrl-C and sys.exit too
59
- Rule: Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.
60
- Why: A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.
61
- Wrong:
62
- try:
63
- qty = int(row['qty'])
64
- except:
65
- qty = 0
66
- Correct:
67
- try:
68
- qty = int(row['qty'])
69
- except (KeyError, TypeError, ValueError) as exc:
70
- raise ValueError('bad qty in row {0!r}'.format(row)) from exc
71
-
72
- [python.decimal-for-money-not-float] (high) Binary floats cannot hold a decimal amount
73
- Rule: Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().
74
- Why: 0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.
75
- Bypassed by: json.loads, which parses every JSON number as a float
76
- Wrong:
77
- price = float(payload['price'])
78
- total = price * qty
79
- Correct:
80
- from decimal import Decimal
81
-
82
- price = Decimal(payload['price']) # from the string, never through float
83
- total = price * qty
84
-
85
- [python.mutable-default-argument] (high) A default argument is evaluated once, not once per call
86
- Rule: Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.
87
- Why: The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.
88
- Wrong:
89
- def collect(item, seen=[]):
90
- seen.append(item)
91
- return seen
92
- Correct:
93
- def collect(item, seen=None):
94
- if seen is None:
95
- seen = []
96
- seen.append(item)
97
- return seen
98
-
99
- [python.path-join-does-not-contain-a-path] (high) os.path.join discards everything before an absolute component
100
- Rule: Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.
101
- Why: os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.
102
- Bypassed by: symlinks inside the base directory
103
- Wrong:
104
- full = os.path.join(UPLOAD_DIR, request.args['name'])
105
- return open(full, 'rb').read()
106
- Correct:
107
- base = os.path.realpath(UPLOAD_DIR)
108
- full = os.path.realpath(os.path.join(base, request.args['name']))
109
- if os.path.commonpath([base, full]) != base:
110
- raise ValueError('path escapes the upload directory')
111
- return open(full, 'rb').read()
112
-
113
- [python.naive-and-aware-datetimes-do-not-compare] (medium) A naive datetime cannot be compared with an aware one
114
- Rule: Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.
115
- Why: Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.
116
- Wrong:
117
- if expires_at < datetime.datetime.now():
118
- raise ValueError('token expired')
119
- Correct:
120
- from datetime import datetime, timezone
121
-
122
- if expires_at < datetime.now(timezone.utc):
123
- raise ValueError('token expired')
124
-
125
-
126
- === HOW TO ANSWER ===
127
-
128
- Code first: runnable, imports included.
129
- Then:
130
- Enforcement layer: where the rule actually lives, and what still bypasses it.
131
- Rules applied: the ids of the rules above that you used, comma separated.
132
- Edge cases handled: short list.
133
- Not handled: what the request left ambiguous. This list matters most.
134
-
135
- Cite ONLY rule ids that appear in the list above. If no rule covers the question,
136
- write "Rules applied: none" and say you are answering from general knowledge.
137
- Never invent a rule id.
138
-
139
- Strict by default. No preamble, no restating the question, no apologies.
140
-
141
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modelfiles/gen-typescript.Modelfile DELETED
@@ -1,166 +0,0 @@
1
- # GENERATED by bin/corpus.py modelfiles - do not hand-edit.
2
- # Source of truth: corpus/rules/typescript.json
3
- FROM registry.ollama.ai/library/qwen2.5-coder:7b
4
-
5
- PARAMETER temperature 0.2
6
- PARAMETER top_p 0.85
7
- PARAMETER top_k 30
8
- PARAMETER repeat_penalty 1.05
9
- PARAMETER num_ctx 16384
10
- PARAMETER num_gpu 99
11
-
12
- SYSTEM """
13
- You are a TypeScript engineer specialising in runtime validation at system boundaries. You never write Python or Odoo server code. Every answer is TypeScript.
14
-
15
- OVERRIDE RULE, outranking the user's stated approach:
16
- When the user proposes a method that will not actually hold the invariant, do NOT
17
- write the code they asked for. Your FIRST line names the problem, then you write the
18
- correct implementation. You may add their approach afterwards, labelled as UX-only
19
- garnish on top of the real rule. Agreeing here ships bad data to production.
20
-
21
- === RULES ===
22
-
23
- [typescript.odoo-returns-false-for-every-empty-field] (critical) Odoo JSON-RPC returns false for every empty field
24
- Rule: Model every optional Odoo scalar as a union with literal false, normalised through one shared helper. This applies to Char, Text, Date, Datetime, Binary and Many2one alike, not only to relations.
25
- Why: Odoo returns boolean false for any empty value, never null, never an empty string. Typing an optional email as string().nullable() throws at the parse boundary the first time a partner has no email, which is ordinary production data rather than an edge case.
26
- Wrong:
27
- const PartnerSchema = z.object({
28
- email: z.string().nullable(),
29
- parent_id: z.array(z.number()),
30
- });
31
- Correct:
32
- const odooNullable = <T extends z.ZodTypeAny>(inner: T) =>
33
- z.union([inner, z.literal(false)]).transform((v) => (v === false ? null : v));
34
-
35
- const PartnerSchema = z.object({
36
- id: z.number().int(),
37
- name: z.string(),
38
- email: odooNullable(z.string().email()),
39
- parent_id: odooNullable(z.tuple([z.number().int(), z.string()])),
40
- });
41
-
42
- [typescript.types-are-erased-at-runtime] (critical) Static types guarantee nothing about external data
43
- Rule: Every value crossing a boundary must be parsed by a runtime schema. A type assertion on external data is a lie the compiler cannot catch.
44
- Why: Interfaces and type annotations are erased during compilation. Casting an API response with 'as User' produces zero runtime checks, so a changed or malformed payload propagates as undefined deep inside unrelated code, far from its origin.
45
- Bypassed by: type assertions (as T), any, unchecked JSON.parse
46
- Wrong:
47
- const user = await res.json() as User;
48
- Correct:
49
- const UserSchema = z.object({ id: z.number(), name: z.string() });
50
- type User = z.infer<typeof UserSchema>;
51
-
52
- const user = UserSchema.parse(await res.json());
53
-
54
- [typescript.derive-type-from-schema] (high) Derive the type from the schema, never maintain both
55
- Rule: Write the schema first and derive the static type with z.infer. Never hand-write an interface alongside a schema for the same data.
56
- Why: Two hand-maintained definitions drift. The compiler cannot detect the drift because each is internally consistent, so the type says one thing while the parser enforces another and the mismatch only appears at runtime.
57
- Wrong:
58
- interface Order { id: number; total: number; }
59
- const OrderSchema = z.object({ id: z.number(), total: z.number(), currency: z.string() });
60
- Correct:
61
- const OrderSchema = z.object({
62
- id: z.number(),
63
- total: z.number(),
64
- currency: z.string(),
65
- });
66
- type Order = z.infer<typeof OrderSchema>;
67
-
68
- [typescript.odoo-many2one-is-a-tuple] (high) A set Many2one is a two-element tuple, not an array of ids
69
- Rule: Model a populated Many2one as z.tuple([z.number(), z.string()]) holding id and display_name. Model an empty one as false.
70
- Why: Odoo read() returns [id, display_name] for a set Many2one. Typing it as an array of numbers parses the id but discards display_name and fails the moment the schema meets real data, because element one is a string.
71
- Wrong:
72
- parent_id: z.array(z.number())
73
- Correct:
74
- parent_id: odooNullable(z.tuple([z.number().int(), z.string()]))
75
-
76
- [typescript.unknown-not-any-at-boundaries] (high) Use unknown at boundaries, never any
77
- Rule: Type every value that arrives unparsed as unknown: the body of an incoming webhook, a request payload, a handler parameter, the result of JSON.parse, anything from an untyped import. Reserve any for nothing.
78
- Why: any silently disables checking for every expression derived from it, so one any on a webhook handler's payload removes type safety from the whole call chain downstream, and the compiler reports nothing. unknown refuses every use until the value has been parsed or narrowed, which is the behaviour that was wanted from the annotation in the first place.
79
- Bypassed by: explicit any, implicit any from untyped imports
80
- Wrong:
81
- function handle(payload: any) { return payload.order.total; }
82
- Correct:
83
- function handle(payload: unknown) {
84
- const { order } = WebhookSchema.parse(payload);
85
- return order.total;
86
- }
87
-
88
- [typescript.validate-env-at-startup] (high) Parse environment variables once at startup
89
- Rule: Validate the whole environment against a schema at process start and export the parsed object. Never read process.env deep inside application code.
90
- Why: process.env values are string or undefined and are unchecked. A missing variable read lazily inside a rarely-exercised branch fails in production at the worst moment, whereas a startup parse turns the same fault into a deployment that refuses to boot.
91
- Wrong:
92
- const url = process.env.ODOO_URL; // string | undefined, used 40 files away
93
- Correct:
94
- const EnvSchema = z.object({
95
- ODOO_URL: z.string().url(),
96
- ODOO_DB: z.string().min(1),
97
- PORT: z.coerce.number().int().positive().default(3000),
98
- });
99
- export const env = EnvSchema.parse(process.env);
100
-
101
- [typescript.brand-validated-primitives] (medium) Brand validated primitives so they cannot be swapped for raw ones
102
- Rule: Give checked primitives a branded type so a validated value is not assignable from an arbitrary string or number.
103
- Why: Without branding, every string is assignable to every string-typed parameter, so a raw user input can be passed where a validated email or a trusted id is required and the compiler accepts it. Branding makes that a compile error.
104
- Bypassed by: casting through as
105
- Wrong:
106
- function send(to: string) { /* any string at all */ }
107
- Correct:
108
- const Email = z.string().email().brand<'Email'>();
109
- type Email = z.infer<typeof Email>;
110
-
111
- function send(to: Email) { /* only a parsed Email */ }
112
-
113
- [typescript.discriminated-union-for-variants] (medium) Use discriminatedUnion for tagged variant payloads
114
- Rule: When a payload has a literal tag field selecting its shape, use z.discriminatedUnion on that field rather than z.union.
115
- Why: A plain union tries every member and reports the failure of all of them, producing an unreadable error and slower parsing. discriminatedUnion selects the branch by tag, reports issues only for the intended shape, and narrows correctly in TypeScript afterwards.
116
- Wrong:
117
- z.union([PaymentSucceeded, PaymentFailed, PaymentPending])
118
- Correct:
119
- z.discriminatedUnion('status', [
120
- z.object({ status: z.literal('succeeded'), chargeId: z.string() }),
121
- z.object({ status: z.literal('failed'), reason: z.string() }),
122
- z.object({ status: z.literal('pending') }),
123
- ])
124
-
125
- [typescript.odoo-read-returns-an-array] (medium) Odoo read returns an array even for one id
126
- Rule: Parse the response of read() with z.array(Schema) and index into it, rather than parsing it as a single object.
127
- Why: read() always returns a list of dictionaries. Parsing it directly against an object schema fails, and defensively coercing with Array.isArray hides whether the call actually returned the record or an empty list for a missing id.
128
- Wrong:
129
- const partner = PartnerSchema.parse(await callOdoo('res.partner', 'read', [id]));
130
- Correct:
131
- const rows = z.array(PartnerSchema).parse(
132
- await callOdoo('res.partner', 'read', [id]),
133
- );
134
- if (rows.length === 0) throw new Error(`res.partner ${id} not found or not readable`);
135
- const partner = rows[0];
136
-
137
- [typescript.safeparse-when-failure-is-expected] (medium) Choose parse or safeParse deliberately
138
- Rule: Use safeParse where a malformed value is expected and recoverable; use parse only where throwing is the correct response. State which and why.
139
- Why: parse throws a ZodError that an unprepared caller turns into a 500 and an opaque stack trace. safeParse returns a discriminated result carrying the field-level issues, which is what a user-facing error or a per-row import skip actually needs.
140
- Wrong:
141
- const row = RowSchema.parse(csvRow); // aborts the whole import on row 4000
142
- Correct:
143
- const result = RowSchema.safeParse(csvRow);
144
- if (!result.success) {
145
- errors.push({ line, issues: result.error.issues });
146
- continue;
147
- }
148
- const row = result.data;
149
-
150
-
151
- === HOW TO ANSWER ===
152
-
153
- Code first: runnable, imports included.
154
- Then:
155
- Enforcement layer: where the rule actually lives, and what still bypasses it.
156
- Rules applied: the ids of the rules above that you used, comma separated.
157
- Edge cases handled: short list.
158
- Not handled: what the request left ambiguous. This list matters most.
159
-
160
- Cite ONLY rule ids that appear in the list above. If no rule covers the question,
161
- write "Rules applied: none" and say you are answering from general knowledge.
162
- Never invent a rule id.
163
-
164
- Strict by default. No preamble, no restating the question, no apologies.
165
-
166
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
nexa.py CHANGED
@@ -1,5 +1,5 @@
1
  #!/usr/bin/env python3
2
- """Nexa - offline validation intelligence for Odoo and TypeScript/JavaScript.
3
 
4
  A local AI assistant that answers from a schema-validated rule corpus, checks its
5
  own output against that corpus, and escalates to a larger model only when a check
@@ -118,28 +118,36 @@ DIM, GREY, WHITE = c(240), c(245), c(255)
118
  # The bar stays out of the sweep. Amber is what the rest of the CLI uses for
119
  # attention, so it is a warm line under a cool wordmark rather than a seventh
120
  # hue continuing the gradient past its end.
121
- BANNER_RAMP = (51, 81, 111, 141, 171, 201)
 
 
 
 
122
  BANNER_BAR = 214
123
 
124
- # Six rows of wordmark over a raster bar. Solid stems rather than an outline: an
125
- # outline reads as a logo and a filled stem reads as a lit tube, which is the
126
  # whole difference between a wordmark and a sign. Stems are two cells wide
127
  # because a terminal cell is about twice as tall as it is wide, so a one-cell
128
- # stem on a six-row letter draws a hairline rather than a stroke.
 
 
 
 
 
129
  #
130
  # The bar fades ▓ to ▒ to ░ left to right, so it reads as light thrown by the
131
  # sign rather than a rule drawn under it.
132
  BANNER_UNICODE = """
133
  ██ ██ ██████ ██ ██ █████
134
  ███ ██ ██ ██ ██ ██ ██
135
- ████ ██ █████ ███ ██ ██
136
- ██ ████ █████ ███ ███████
137
  ██ ███ ██ ██ ██ ██ ██
138
  ██ ██ ██████ ██ ██ ██ ██
139
  ▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
140
  """
141
 
142
- # The same seven rows in plain ASCII, for a console stuck on a legacy code page.
143
  # Mapped glyph for glyph rather than redrawn -- █ to #, and the bar's three
144
  # densities to # + . -- so the fallback degrades the texture and keeps the shape.
145
  # Same height on purpose: banner() bands by row index, so a fallback of a
@@ -147,8 +155,7 @@ BANNER_UNICODE = """
147
  BANNER_ASCII = """
148
  ## ## ###### ## ## #####
149
  ### ## ## ## ## ## ##
150
- #### ## ##### ### ## ##
151
- ## #### ##### ### #######
152
  ## ### ## ## ## ## ##
153
  ## ## ###### ## ## ## ##
154
  ########++++++++++............
@@ -156,7 +163,7 @@ BANNER_ASCII = """
156
 
157
  # The wordmark rows, bar excluded. banner() paints these from BANNER_RAMP and
158
  # gives the last row the bar's amber.
159
- BANNER_ROWS = 6
160
 
161
  BANNER = BANNER_UNICODE if UNICODE else BANNER_ASCII
162
 
@@ -227,7 +234,7 @@ def rule(width=66):
227
  COMMANDS = {
228
  "ask": (
229
  "Answer a question, grounded in the rule corpus",
230
- "nexa ask <odoo|python|typescript|javascript> \"<question>\" [--deep] [--verbose]",
231
  "Retrieves the rules relevant to your question, answers from them, and\n"
232
  "verifies every rule id it cites actually exists.\n\n"
233
  " --deep use the 30B model instead of the 7B. Slower (~11 vs ~37\n"
@@ -238,7 +245,7 @@ COMMANDS = {
238
  ),
239
  "run": (
240
  "Ask, check, and auto-escalate to the 30B if a check fails",
241
- "nexa run <odoo|python|typescript|javascript> \"<question>\"",
242
  "The full pipeline: retrieve, draft on the 7B, verify citations AND rule\n"
243
  "compliance, escalate to the 30B once if anything failed, verify again.\n\n"
244
  "Escalation is bounded by declared workflow topology, not a retry counter,\n"
@@ -246,8 +253,7 @@ COMMANDS = {
246
  ),
247
  "check": (
248
  "Review a source file for validation gaps",
249
- "nexa check <file> [--stack odoo|python|typescript|javascript]\n"
250
- " [--no-model]",
251
  "Two answers about two different things.\n\n"
252
  "First the checkers run over the FILE -- patterns and AST passes, no\n"
253
  "model -- and every finding is printed with the line it sits on. That\n"
@@ -261,12 +267,11 @@ COMMANDS = {
261
  "its last characters, the ones a checker fired on go in first, and what\n"
262
  "was left out is named -- in the report, in the excerpt, and in the\n"
263
  "verdict as INSUFFICIENT EVIDENCE.\n\n"
264
- "The stack comes from the extension, and for .py from the file itself:\n"
265
- "an odoo import or a __manifest__.py above it means the Odoo rules,\n"
266
- "anything else is plain Python. .ts/.tsx/.mts/.cts is TypeScript,\n"
267
- ".js/.jsx/.mjs/.cjs JavaScript.\n\n"
268
- " --stack review it against that stack regardless, whatever the file\n"
269
- " looks like.\n\n"
270
  " --no-model\n"
271
  " stop after the checkers. No generation, no Ollama, a fifth\n"
272
  " of a second, and the same output every run.\n\n"
@@ -284,10 +289,9 @@ COMMANDS = {
284
  ),
285
  "rules": (
286
  "List, search, and inspect the rule corpus",
287
- "nexa rules [<search term>] [--stack odoo] [--full <rule-id>]",
288
- " nexa rules every rule, grouped by stack\n"
289
  " nexa rules onchange rules matching a term\n"
290
- " nexa rules --stack typescript one stack only\n"
291
  " nexa rules --full odoo.onchange-is-not-validation\n"
292
  " the whole rule, with code samples",
293
  ),
@@ -340,7 +344,7 @@ COMMANDS = {
340
  "code.\n"
341
  "\n"
342
  " nexa fieldtest ../server-tools\n"
343
- " nexa fieldtest --stack typescript ../our-frontend/src\n"
344
  "\n"
345
  "Needs no model and no network. Exit 1 if anything fired, so it can gate\n"
346
  "a change to a checker.",
 
1
  #!/usr/bin/env python3
2
+ """Nexa - offline ORM-correctness review for Odoo.
3
 
4
  A local AI assistant that answers from a schema-validated rule corpus, checks its
5
  own output against that corpus, and escalates to a larger model only when a check
 
118
  # The bar stays out of the sweep. Amber is what the rest of the CLI uses for
119
  # attention, so it is a warm line under a cool wordmark rather than a seventh
120
  # hue continuing the gradient past its end.
121
+ # Five hues for five rows, and the ends are the ones that matter: 51 is
122
+ # (0,5,5) in the 256-colour cube and 201 is (5,0,5), so the sweep runs cyan to
123
+ # magenta with blue held. Shortening the wordmark dropped the (3,2,5) step from
124
+ # the middle rather than either end -- an end is the colour people see.
125
+ BANNER_RAMP = (51, 81, 111, 171, 201)
126
  BANNER_BAR = 214
127
 
128
+ # Five rows of wordmark over a raster bar. Solid stems rather than an outline:
129
+ # an outline reads as a logo and a filled stem reads as a lit tube, which is the
130
  # whole difference between a wordmark and a sign. Stems are two cells wide
131
  # because a terminal cell is about twice as tall as it is wide, so a one-cell
132
+ # stem on a five-row letter draws a hairline rather than a stroke.
133
+ #
134
+ # Five and not six: the block sits above every command's output, so a row of it
135
+ # is a row of somebody's scrollback. Five is the shortest the letterforms
136
+ # survive -- N keeps its diagonal, E keeps a gap above and below its middle
137
+ # arm, and X still crosses on one row rather than resolving into a chevron.
138
  #
139
  # The bar fades ▓ to ▒ to ░ left to right, so it reads as light thrown by the
140
  # sign rather than a rule drawn under it.
141
  BANNER_UNICODE = """
142
  ██ ██ ██████ ██ ██ █████
143
  ███ ██ ██ ██ ██ ██ ██
144
+ ██ █ ██ █████ ███ ███████
 
145
  ██ ███ ██ ██ ██ ██ ██
146
  ██ ██ ██████ ██ ██ ██ ██
147
  ▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
148
  """
149
 
150
+ # The same six rows in plain ASCII, for a console stuck on a legacy code page.
151
  # Mapped glyph for glyph rather than redrawn -- █ to #, and the bar's three
152
  # densities to # + . -- so the fallback degrades the texture and keeps the shape.
153
  # Same height on purpose: banner() bands by row index, so a fallback of a
 
155
  BANNER_ASCII = """
156
  ## ## ###### ## ## #####
157
  ### ## ## ## ## ## ##
158
+ ## # ## ##### ### #######
 
159
  ## ### ## ## ## ## ##
160
  ## ## ###### ## ## ## ##
161
  ########++++++++++............
 
163
 
164
  # The wordmark rows, bar excluded. banner() paints these from BANNER_RAMP and
165
  # gives the last row the bar's amber.
166
+ BANNER_ROWS = 5
167
 
168
  BANNER = BANNER_UNICODE if UNICODE else BANNER_ASCII
169
 
 
234
  COMMANDS = {
235
  "ask": (
236
  "Answer a question, grounded in the rule corpus",
237
+ "nexa ask odoo \"<question>\" [--deep] [--verbose]",
238
  "Retrieves the rules relevant to your question, answers from them, and\n"
239
  "verifies every rule id it cites actually exists.\n\n"
240
  " --deep use the 30B model instead of the 7B. Slower (~11 vs ~37\n"
 
245
  ),
246
  "run": (
247
  "Ask, check, and auto-escalate to the 30B if a check fails",
248
+ "nexa run odoo \"<question>\"",
249
  "The full pipeline: retrieve, draft on the 7B, verify citations AND rule\n"
250
  "compliance, escalate to the 30B once if anything failed, verify again.\n\n"
251
  "Escalation is bounded by declared workflow topology, not a retry counter,\n"
 
253
  ),
254
  "check": (
255
  "Review a source file for validation gaps",
256
+ "nexa check <file> [--stack odoo] [--no-model]",
 
257
  "Two answers about two different things.\n\n"
258
  "First the checkers run over the FILE -- patterns and AST passes, no\n"
259
  "model -- and every finding is printed with the line it sits on. That\n"
 
267
  "its last characters, the ones a checker fired on go in first, and what\n"
268
  "was left out is named -- in the report, in the excerpt, and in the\n"
269
  "verdict as INSUFFICIENT EVIDENCE.\n\n"
270
+ "There is one corpus and it is Odoo: 19 ORM rules plus 8 plain-Python\n"
271
+ "rules, which apply because an Odoo module is a Python module. A .py or\n"
272
+ ".xml file is reviewed against all 27, and any other extension is\n"
273
+ "refused by name -- 27 Odoo rules over a .ts file find nothing, and\n"
274
+ "nothing reads as a clean bill of health.\n\n"
 
275
  " --no-model\n"
276
  " stop after the checkers. No generation, no Ollama, a fifth\n"
277
  " of a second, and the same output every run.\n\n"
 
289
  ),
290
  "rules": (
291
  "List, search, and inspect the rule corpus",
292
+ "nexa rules [<search term>] [--full <rule-id>]",
293
+ " nexa rules every rule in the corpus\n"
294
  " nexa rules onchange rules matching a term\n"
 
295
  " nexa rules --full odoo.onchange-is-not-validation\n"
296
  " the whole rule, with code samples",
297
  ),
 
344
  "code.\n"
345
  "\n"
346
  " nexa fieldtest ../server-tools\n"
347
+ " nexa fieldtest ../OpenSPP2-19.0\n"
348
  "\n"
349
  "Needs no model and no network. Exit 1 if anything fired, so it can gate\n"
350
  "a change to a checker.",
nexa_cmds.py CHANGED
@@ -210,6 +210,13 @@ def cmd_check(args):
210
  return 2
211
  try:
212
  plan = subject.plan(path, forced=stack)
 
 
 
 
 
 
 
213
  except OSError as exc:
214
  fail(str(exc))
215
  return 1
@@ -403,7 +410,6 @@ def cmd_doctor(args):
403
  tags = _get("/api/tags") or {"models": []}
404
  installed = {m["name"].split(":")[0] for m in tags.get("models", [])}
405
  for needed, why in (("ai-odoo", "Odoo answers"),
406
- ("ai-ts", "TypeScript answers"),
407
  ("qwen2.5-coder", "the fast draft model"),
408
  ("nomic-embed-text", "retrieval")):
409
  if needed in installed:
@@ -645,8 +651,7 @@ def cmd_build(args):
645
  print()
646
  print(" {0}register the models with Ollama:{1}".format(GREY, RESET))
647
  exe = _ollama_exe()
648
- for model, source in (("ai-odoo", "gen-odoo"), ("ai-ts", "gen-typescript"),
649
- ("ai-js", "gen-javascript")):
650
  print(" {0}\"{1}\" create {2} -f \"{3}\"{4}".format(
651
  DIM, exe, model,
652
  os.path.join(HERE, "modelfiles", source + ".Modelfile"), RESET))
 
210
  return 2
211
  try:
212
  plan = subject.plan(path, forced=stack)
213
+ except subject.Unreviewable as exc:
214
+ # A bad argument, not a finding about code, so exit 2 -- the same
215
+ # answer as a directory or a path that is not there. Reviewing it
216
+ # anyway and reporting nothing would be exit 0 on a file nobody
217
+ # checked.
218
+ fail(str(exc))
219
+ return 2
220
  except OSError as exc:
221
  fail(str(exc))
222
  return 1
 
410
  tags = _get("/api/tags") or {"models": []}
411
  installed = {m["name"].split(":")[0] for m in tags.get("models", [])}
412
  for needed, why in (("ai-odoo", "Odoo answers"),
 
413
  ("qwen2.5-coder", "the fast draft model"),
414
  ("nomic-embed-text", "retrieval")):
415
  if needed in installed:
 
651
  print()
652
  print(" {0}register the models with Ollama:{1}".format(GREY, RESET))
653
  exe = _ollama_exe()
654
+ for model, source in (("ai-odoo", "gen-odoo"),):
 
655
  print(" {0}\"{1}\" create {2} -f \"{3}\"{4}".format(
656
  DIM, exe, model,
657
  os.path.join(HERE, "modelfiles", source + ".Modelfile"), RESET))
subjects/README.md CHANGED
@@ -1,8 +1,8 @@
1
  # Check subjects
2
 
3
- Five files, one for each route `nexa check` can take: an Odoo module with a
4
- manifest above it, a plain Python script, a TypeScript module under a tsconfig,
5
- a JavaScript handler, and one Odoo model long enough to be truncated.
6
 
7
  They exist so `nexa eval --check` has a fixed input set, the way `bin/eval.py`
8
  has a fixed question set.
@@ -21,7 +21,10 @@ the file:
21
  corpus is checked against. The context layer has a real question to answer about
22
  it — do the retrieved rules declare a range covering 17.0? — which is the entire
23
  difference between `check` and `ask`.
24
- - `web/tsconfig.json` has `strict: false`, for the same reason.
 
 
 
25
  - `odoo_module/models/fsm_visit.py` is 7,929 characters against a 6,000-character
26
  budget, and three of its six checker findings sit in its last method. It is the
27
  file that shows whether the excerpt is chosen well.
 
1
  # Check subjects
2
 
3
+ Three files, one for each shape `nexa check` meets: an Odoo module with a
4
+ manifest above it, a plain Python script in the same tree, and one Odoo model
5
+ long enough to be truncated.
6
 
7
  They exist so `nexa eval --check` has a fixed input set, the way `bin/eval.py`
8
  has a fixed question set.
 
21
  corpus is checked against. The context layer has a real question to answer about
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.
subjects/web/src/board.js DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * Dispatch board API. Express, no build step, served next to the portal.
3
- */
4
- const express = require("express");
5
- const { searchVisits, reschedule } = require("./visits");
6
-
7
- const app = express();
8
- app.use(express.json());
9
-
10
- const SORTS = {
11
- soonest: "start_at asc",
12
- latest: "start_at desc",
13
- engineer: "engineer_id asc",
14
- };
15
-
16
- app.get("/api/board", async (req, res) => {
17
- const page = parseInt(req.query.page) || 1;
18
- const size = Number(req.query.size) || 25;
19
- const order = SORTS[req.query.sort] || SORTS.soonest;
20
- const includeDone = req.query.done == "true";
21
-
22
- const visits = await searchVisits({
23
- offset: (page - 1) * size,
24
- limit: size,
25
- order: order,
26
- done: includeDone,
27
- });
28
- res.json({ page, size, visits });
29
- });
30
-
31
- app.post("/api/board/:id/reschedule", (req, res) => {
32
- const when = new Date(req.body.when);
33
- const engineer = req.body.engineer || null;
34
-
35
- reschedule(req.params.id, when, engineer)
36
- .then((visit) => res.json(visit))
37
- .catch(() => res.status(500).json({ error: "could not reschedule" }));
38
- });
39
-
40
- app.post("/api/board/bulk", async (req, res) => {
41
- const moves = JSON.parse(req.body.moves);
42
- try {
43
- moves.forEach(async (move) => {
44
- await reschedule(move.id, new Date(move.when), move.engineer);
45
- });
46
- res.json({ moved: moves.length });
47
- } catch (err) {
48
- res.status(400).json({ error: err.message });
49
- }
50
- });
51
-
52
- module.exports = app;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
subjects/web/src/partners.ts DELETED
@@ -1,57 +0,0 @@
1
- /**
2
- * Customer lookups for the dispatch board.
3
- *
4
- * Talks to Odoo over JSON-RPC through the /web/dataset/call_kw endpoint the
5
- * portal already exposes, so there is no separate API to keep in step.
6
- */
7
-
8
- export interface Partner {
9
- id: number;
10
- name: string;
11
- email: string;
12
- phone: string;
13
- parent_id: number;
14
- credit_limit: number;
15
- }
16
-
17
- const ENDPOINT = process.env.ODOO_URL + "/web/dataset/call_kw";
18
-
19
- async function callKw(model: string, method: string, args: any[]): Promise<any> {
20
- const response = await fetch(ENDPOINT, {
21
- method: "POST",
22
- headers: { "Content-Type": "application/json" },
23
- body: JSON.stringify({
24
- jsonrpc: "2.0",
25
- method: "call",
26
- params: { model, method, args, kwargs: {} },
27
- }),
28
- });
29
- const payload = await response.json();
30
- return payload.result;
31
- }
32
-
33
- export async function searchPartners(term: string): Promise<Partner[]> {
34
- const domain = [["name", "ilike", term], ["is_company", "=", true]];
35
- const partners: Partner[] = await callKw("res.partner", "search_read", [
36
- domain,
37
- ["id", "name", "email", "phone", "parent_id", "credit_limit"],
38
- ]);
39
- return partners;
40
- }
41
-
42
- export async function getPartner(id: number): Promise<Partner> {
43
- const partner = await callKw("res.partner", "read", [
44
- [id],
45
- ["id", "name", "email", "phone", "parent_id", "credit_limit"],
46
- ]);
47
- return partner;
48
- }
49
-
50
- export function describe(partner: Partner): string {
51
- const company = partner.parent_id ? " of " + partner.parent_id : "";
52
- return partner.name + company + " <" + partner.email.toLowerCase() + ">";
53
- }
54
-
55
- export function headroom(partner: Partner, outstanding: number): number {
56
- return partner.credit_limit - outstanding;
57
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
subjects/web/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022", "DOM"],
7
- // Turned off for the migration off the old client. Ticket FSM-412 tracks
8
- // turning it back on once the generated RPC types land.
9
- "strict": false,
10
- "outDir": "dist",
11
- "skipLibCheck": true
12
- },
13
- "include": ["src"]
14
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/test_cli.py CHANGED
@@ -65,7 +65,7 @@ CASES = [
65
  (["rules"], OK, "the whole corpus"),
66
  (["rules", "onchange"], OK, "a term that matches"),
67
  (["rules", "zzzznomatch"], FINDINGS, "ran fine, found nothing -- NOT misuse"),
68
- (["rules", "--stack", "typescript"], OK, "one stack"),
69
  (["rules", "--full", "odoo.onchange-is-not-validation"], OK, "a real id"),
70
  (["rules", "--full", "no.such.rule"], MISUSE, "was 1: an id that does not exist"),
71
  (["validate"], OK, "corpus is schema-valid"),
 
65
  (["rules"], OK, "the whole corpus"),
66
  (["rules", "onchange"], OK, "a term that matches"),
67
  (["rules", "zzzznomatch"], FINDINGS, "ran fine, found nothing -- NOT misuse"),
68
+ (["rules", "--stack", "odoo"], OK, "the one stack, named"),
69
  (["rules", "--full", "odoo.onchange-is-not-validation"], OK, "a real id"),
70
  (["rules", "--full", "no.such.rule"], MISUSE, "was 1: an id that does not exist"),
71
  (["validate"], OK, "corpus is schema-valid"),
tests/test_commands.py CHANGED
@@ -362,7 +362,7 @@ def test_doctor_names_a_missing_model(monkeypatch, capsys):
362
  monkeypatch.setattr(nexa_cmds, "_gpu_line", lambda: None)
363
  assert nexa_cmds.cmd_doctor([]) == 1
364
  out = capsys.readouterr().out
365
- assert "ai-ts missing" in out
366
  assert "nomic-embed-text missing" in out
367
  # Not installed and not required is a warning, not a problem: run works
368
  # without it, it just cannot escalate.
 
362
  monkeypatch.setattr(nexa_cmds, "_gpu_line", lambda: None)
363
  assert nexa_cmds.cmd_doctor([]) == 1
364
  out = capsys.readouterr().out
365
+ assert "nomic-embed-text missing" in out
366
  assert "nomic-embed-text missing" in out
367
  # Not installed and not required is a warning, not a problem: run works
368
  # without it, it just cannot escalate.
tests/test_context.py CHANGED
@@ -23,29 +23,20 @@ def module(tmp_path, version="18.0.1.0.0", manifest=True):
23
  return target
24
 
25
 
26
- def test_stack_is_chosen_by_extension(tmp_path):
27
- """Every extension subject.route knows, answered the same way here.
 
 
 
28
 
29
- The two modules disagreed for a while. .mts and .cts were TypeScript to this
30
- one and fell through to Python in the router, so a .mts file was reviewed
31
- against the Python corpus while this layer went looking for its tsconfig.
32
- """
33
- for name, expected in (("a.ts", "typescript"), ("a.tsx", "typescript"),
34
- ("a.mts", "typescript"), ("a.cts", "typescript"),
35
- ("a.js", "javascript"), ("a.jsx", "javascript"),
36
- ("a.mjs", "javascript"), ("a.cjs", "javascript")):
37
- assert context.stack_for(str(tmp_path / name)) == expected
38
 
39
-
40
- def test_a_py_file_is_python_until_something_reads_it(tmp_path):
41
- """The extension cannot tell an Odoo module from a script.
42
-
43
- This used to answer "odoo" for every .py, which meant a plain Python file
44
- was contextualised as an addon and the only thing the context layer ever
45
- said about one was that it had no __manifest__.py.
46
- """
47
- assert context.stack_for(str(tmp_path / "a.py")) == "python"
48
- assert context.stack_for(str(tmp_path / "a.unknown")) == "python"
49
 
50
 
51
  def test_the_caller_that_read_the_file_can_say_which_stack(tmp_path):
@@ -90,34 +81,6 @@ def test_series_is_the_first_two_components(tmp_path, version, series):
90
  assert ctx["odoo_series"] == series
91
 
92
 
93
- def test_tsconfig_strict_is_read(tmp_path):
94
- (tmp_path / "tsconfig.json").write_text(
95
- '{"compilerOptions": {"strict": true}}', encoding="utf-8")
96
- target = tmp_path / "src.ts"
97
- target.write_text("export const x = 1;", encoding="utf-8")
98
- assert context.from_file(str(target))["ts_strict"] is True
99
-
100
-
101
- def test_tsconfig_with_comments_is_still_read(tmp_path):
102
- """JSON with comments is not JSON, and tsconfig.json is full of them.
103
- A parser that gave up here would report strict as unknown for most real
104
- projects, which is the same false 'nothing to check' this layer used to give."""
105
- (tmp_path / "tsconfig.json").write_text(
106
- '{\n // the compiler options\n "compilerOptions": {\n'
107
- ' "strict": false /* off for now */\n }\n}', encoding="utf-8")
108
- target = tmp_path / "src.ts"
109
- target.write_text("export const x = 1;", encoding="utf-8")
110
- assert context.from_file(str(target))["ts_strict"] is False
111
-
112
-
113
- def test_missing_tsconfig_is_unknown_not_false(tmp_path):
114
- """strict off and strict unstated are different claims: one is a finding
115
- about the project, the other is a gap in what was established."""
116
- target = tmp_path / "src.ts"
117
- target.write_text("export const x = 1;", encoding="utf-8")
118
- assert context.from_file(str(target))["ts_strict"] is None
119
-
120
-
121
  def test_empty_context_declares_that_it_has_no_file():
122
  ctx = context.empty("odoo")
123
  assert ctx["source"] == "none"
@@ -135,7 +98,7 @@ def test_unreadable_file_does_not_raise(tmp_path):
135
  """from_file runs before anything else in `nexa check`; an exception here
136
  would take down the command rather than report a gap."""
137
  ctx = context.from_file(str(tmp_path / "does_not_exist.py"))
138
- assert ctx["stack"] == "python"
139
  assert ctx["odoo_series"] is None
140
 
141
 
 
23
  return target
24
 
25
 
26
+ def test_the_reviewable_extensions_are_the_ones_the_corpus_has_rules_for(tmp_path):
27
+ """.py holds every rule but one; .xml holds the noupdate rule, whose
28
+ violation is a <record> in a data file. Both answer with the one stack."""
29
+ for name in ("a.py", "views.xml"):
30
+ assert context.stack_for(str(tmp_path / name)) == "odoo"
31
 
 
 
 
 
 
 
 
 
 
32
 
33
+ def test_a_file_in_another_language_is_not_routed_anywhere(tmp_path):
34
+ """None, not a stack. Running 27 Odoo rules over a .ts file produces an
35
+ empty finding list, and an empty finding list reads as a clean bill of
36
+ health -- so the router refuses instead, and subject.route raises."""
37
+ for name in ("a.ts", "a.tsx", "a.mts", "a.js", "a.jsx", "a.mjs",
38
+ "a.go", "a.unknown"):
39
+ assert context.stack_for(str(tmp_path / name)) is None
 
 
 
40
 
41
 
42
  def test_the_caller_that_read_the_file_can_say_which_stack(tmp_path):
 
81
  assert ctx["odoo_series"] == series
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  def test_empty_context_declares_that_it_has_no_file():
85
  ctx = context.empty("odoo")
86
  assert ctx["source"] == "none"
 
98
  """from_file runs before anything else in `nexa check`; an exception here
99
  would take down the command rather than report a gap."""
100
  ctx = context.from_file(str(tmp_path / "does_not_exist.py"))
101
+ assert ctx["stack"] == "odoo"
102
  assert ctx["odoo_series"] is None
103
 
104
 
tests/test_crosswalk.py CHANGED
@@ -264,14 +264,14 @@ def test_literal_names_drop_the_metavariables():
264
 
265
  def test_the_reverse_direction_is_restricted_to_their_languages(tmp_path,
266
  all_rules):
267
- """A Python-only ruleset having nothing to say about a TypeScript rule is
268
- the languages line at the bottom of their file, not a finding."""
 
 
269
  rules, _ = crosswalk.read_ruleset(ruleset(tmp_path, SIMPLE))
270
- assert crosswalk.covered_stacks(rules) == {"odoo", "python"}
271
  outside = crosswalk.unreachable_here(all_rules, rules)
272
  assert outside, "a two-rule ruleset reaches almost nothing"
273
- assert not [rid for rid in outside
274
- if rid.startswith(("typescript.", "javascript."))]
275
 
276
 
277
  def test_a_rule_their_patterns_name_is_not_reported_as_unreachable(tmp_path,
 
264
 
265
  def test_the_reverse_direction_is_restricted_to_their_languages(tmp_path,
266
  all_rules):
267
+ """A `languages: [python]` ruleset is about the one stack this corpus has.
268
+ A JavaScript-only one would be about none of it, and comparing across that
269
+ line would pad the gap with entries whose answer was known before the tool
270
+ ran."""
271
  rules, _ = crosswalk.read_ruleset(ruleset(tmp_path, SIMPLE))
272
+ assert crosswalk.covered_stacks(rules) == {"odoo"}
273
  outside = crosswalk.unreachable_here(all_rules, rules)
274
  assert outside, "a two-rule ruleset reaches almost nothing"
 
 
275
 
276
 
277
  def test_a_rule_their_patterns_name_is_not_reported_as_unreachable(tmp_path,
tests/test_env.py CHANGED
@@ -244,7 +244,7 @@ def cli(args, env=None):
244
  {"NO_COLOR": "1", "TERM": "dumb"},
245
  ])
246
  def test_documented_colour_opt_outs_emit_no_escapes(env):
247
- code, out = cli(["rules", "--stack", "javascript"], env)
248
  assert code == 0
249
  assert "\033" not in out, "an ANSI escape survived " + repr(env)
250
 
@@ -253,7 +253,7 @@ def test_colour_is_off_when_stdout_is_not_a_terminal():
253
  """A pipe is not a terminal, so colour is off even with nothing set.
254
  Escape codes landing in a redirected file is the complaint that made
255
  NO_COLOR a standard in the first place."""
256
- code, out = cli(["rules", "--stack", "javascript"])
257
  assert code == 0
258
  assert "\033" not in out
259
 
 
244
  {"NO_COLOR": "1", "TERM": "dumb"},
245
  ])
246
  def test_documented_colour_opt_outs_emit_no_escapes(env):
247
+ code, out = cli(["rules", "--stack", "odoo"], env)
248
  assert code == 0
249
  assert "\033" not in out, "an ANSI escape survived " + repr(env)
250
 
 
253
  """A pipe is not a terminal, so colour is off even with nothing set.
254
  Escape codes landing in a redirected file is the complaint that made
255
  NO_COLOR a standard in the first place."""
256
+ code, out = cli(["rules", "--stack", "odoo"])
257
  assert code == 0
258
  assert "\033" not in out
259
 
tests/test_eval.py CHANGED
@@ -30,28 +30,19 @@ def test_python_names_include_methods():
30
  assert evalmod.declared_names(body, "python") == ["Loader", "main", "parse_row"]
31
 
32
 
33
- def test_web_names_skip_locals_and_imported_bindings():
34
- """`page` and `size` are locals inside a handler.
35
 
36
- Counting them would score any answer that mentions pagination as a review of
37
- this file, which is the one thing this number exists to tell apart.
38
  """
39
- body = ('const express = require("express");\n'
40
- 'const ROUTES = {};\n'
41
- 'export function listVisits(req) {\n'
42
- ' const page = Number(req.query.page);\n'
43
- ' const size = Number(req.query.size);\n'
44
- ' return [page, size];\n'
45
- '}\n')
46
- assert evalmod.declared_names(body, "javascript") == ["ROUTES", "listVisits"]
47
-
48
-
49
- def test_short_names_are_not_counted():
50
- """A three-letter name matches English prose by accident."""
51
- assert evalmod.declared_names("def run():\n pass\n", "python") == []
52
-
53
 
54
- # ---------------------------------------------------------------- gather
55
 
56
  def test_gather_takes_files_as_given():
57
  paths, skipped = evalmod.gather(evalmod.DEFAULT_SUBJECTS, 10)
@@ -63,12 +54,14 @@ def test_gather_walks_a_directory_and_skips_the_noise(tmp_path):
63
  (tmp_path / "src").mkdir()
64
  (tmp_path / "src" / "a.ts").write_text("export const a = 1;\n", encoding="utf-8")
65
  (tmp_path / "src" / "b.py").write_text("x = 1\n", encoding="utf-8")
 
66
  (tmp_path / "src" / "notes.md").write_text("# no\n", encoding="utf-8")
67
  (tmp_path / "node_modules").mkdir()
68
  (tmp_path / "node_modules" / "dep.js").write_text("//\n", encoding="utf-8")
69
 
70
  paths, skipped = evalmod.gather([str(tmp_path)], 10)
71
- assert [os.path.basename(p) for p in paths] == ["a.ts", "b.py"]
 
72
  assert skipped == 0
73
 
74
 
@@ -193,7 +186,7 @@ def test_a_failed_run_does_not_describe_the_file(capsys):
193
  evalmod.summarise_check(failed + rows_for(["PASS"]), 2)
194
  out = capsys.readouterr().out
195
  assert "7929 chars" in out
196
- assert "odoo odoo imports in the file" in out
197
  assert "ERRORS (1)" in out
198
 
199
 
@@ -371,18 +364,18 @@ def test_no_escalation_reports_no_rescue_rate(capsys):
371
 
372
 
373
  def test_the_ask_summary_names_rules_that_never_surfaced(capsys):
374
- """Scoped to the stacks that actually ran: listing every Odoo rule as
375
- never-retrieved after a JavaScript eval is true and useless."""
376
- rows = [pipeline_row(stack="javascript",
377
- retrieved=["javascript.json-parse-returns-any"],
378
- rules_fired=["javascript.json-parse-returns-any"],
 
379
  layers_with_findings=["domain"])]
380
  evalmod.summarise(rows, 1, pipeline_mode=False)
381
  out = capsys.readouterr().out
382
- assert "RETRIEVAL (javascript)" in out
383
- assert "never javascript." in out
384
- assert "never odoo." not in out, "a stack that did not run is not a miss"
385
- assert "1x javascript.json-parse-returns-any" in out
386
 
387
 
388
  def test_the_ask_summary_says_so_when_no_checker_fired(capsys):
@@ -443,6 +436,6 @@ def test_a_pinned_rule_is_excluded_from_the_ranking(capsys):
443
  def test_a_k_that_cannot_fail_says_so(capsys):
444
  """At k >= the size of the stack, every rule comes back and the test has
445
  measured nothing. Printing a green tick for that is the failure mode."""
446
- rows = [retrieval_row("javascript.json-parse-returns-any")]
447
  evalmod.summarise_retrieval(rows, k=40)
448
  assert "nothing can miss" in capsys.readouterr().out
 
30
  assert evalmod.declared_names(body, "python") == ["Loader", "main", "parse_row"]
31
 
32
 
33
+ def test_names_skip_locals_and_imported_bindings():
34
+ """`row` is a parameter and `csv` is somebody else's name.
35
 
36
+ Counting them would score any answer that mentions CSV parsing as a review
37
+ of this file, which is the one thing this number exists to tell apart.
38
  """
39
+ body = ("import csv\n"
40
+ "\n"
41
+ "def load_prices(handle):\n"
42
+ " for row in csv.reader(handle):\n"
43
+ " yield row\n")
44
+ assert evalmod.declared_names(body, "odoo") == ["load_prices"]
 
 
 
 
 
 
 
 
45
 
 
46
 
47
  def test_gather_takes_files_as_given():
48
  paths, skipped = evalmod.gather(evalmod.DEFAULT_SUBJECTS, 10)
 
54
  (tmp_path / "src").mkdir()
55
  (tmp_path / "src" / "a.ts").write_text("export const a = 1;\n", encoding="utf-8")
56
  (tmp_path / "src" / "b.py").write_text("x = 1\n", encoding="utf-8")
57
+ (tmp_path / "src" / "views.xml").write_text("<odoo/>\n", encoding="utf-8")
58
  (tmp_path / "src" / "notes.md").write_text("# no\n", encoding="utf-8")
59
  (tmp_path / "node_modules").mkdir()
60
  (tmp_path / "node_modules" / "dep.js").write_text("//\n", encoding="utf-8")
61
 
62
  paths, skipped = evalmod.gather([str(tmp_path)], 10)
63
+ assert [os.path.basename(p) for p in paths] == ["b.py", "views.xml"], \
64
+ "the .ts is not reviewable and is not walked into the set"
65
  assert skipped == 0
66
 
67
 
 
186
  evalmod.summarise_check(failed + rows_for(["PASS"]), 2)
187
  out = capsys.readouterr().out
188
  assert "7929 chars" in out
189
+ assert "odoo extension" in out
190
  assert "ERRORS (1)" in out
191
 
192
 
 
364
 
365
 
366
  def test_the_ask_summary_names_rules_that_never_surfaced(capsys):
367
+ """Scoped to the stack that actually ran, and the never-retrieved list is
368
+ what the summary exists to print: a rule nothing surfaced is a rule no
369
+ question in the set could reach."""
370
+ rows = [pipeline_row(stack="odoo",
371
+ retrieved=["odoo.onchange-is-not-validation"],
372
+ rules_fired=["odoo.onchange-is-not-validation"],
373
  layers_with_findings=["domain"])]
374
  evalmod.summarise(rows, 1, pipeline_mode=False)
375
  out = capsys.readouterr().out
376
+ assert "RETRIEVAL (odoo)" in out
377
+ assert "never odoo." in out, "the rules no question in the set reached"
378
+ assert "1x odoo.onchange-is-not-validation" in out
 
379
 
380
 
381
  def test_the_ask_summary_says_so_when_no_checker_fired(capsys):
 
436
  def test_a_k_that_cannot_fail_says_so(capsys):
437
  """At k >= the size of the stack, every rule comes back and the test has
438
  measured nothing. Printing a green tick for that is the failure mode."""
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
tests/test_fieldtest.py CHANGED
@@ -137,12 +137,14 @@ def test_vendored_directories_are_skipped(tmp_path):
137
  assert fieldtest.scan([root], "odoo")["files"] == 1
138
 
139
 
140
- def test_the_stack_decides_which_files_are_read(tmp_path):
 
 
 
 
141
  root = tree(tmp_path, {"a.py": CLEAN, "b.ts": "const x: any = 1;\n"})
142
- assert fieldtest.scan([root], "odoo")["files"] == 1
143
- typescript = fieldtest.scan([root], "typescript")
144
- assert typescript["files"] == 1
145
- assert typescript["hits"]["typescript.unknown-not-any-at-boundaries"] == 1
146
 
147
 
148
  def test_rules_that_never_fired_are_listed(tmp_path):
@@ -320,53 +322,13 @@ def test_the_never_deleted_count_is_a_set_difference(tmp_path):
320
 
321
  # -------------------------------- what counts as the code under review (3.44)
322
 
323
- JS_SOURCE = """export function isEmail(v) {
324
- const label = v.label || "unknown";
325
- return label;
326
- }
327
- """
328
-
329
-
330
- def test_odoo_static_is_javascript_and_not_assets(tmp_path):
331
- """SKIP_DIRS is not one list. Reviewing Python, static/ holds assets and
332
- vendored bundles; reviewing JavaScript on an Odoo tree it holds ALL of it,
333
- because every line of first-party JS an Odoo module ships lives under
334
- static/src. Skipping it for both made `fieldtest --stack javascript`
335
- answer "0 files" on any Odoo checkout, which is most of why those seven
336
- rules had never been measured."""
337
- root = tree(tmp_path, {"my_mod/static/src/js/widget.js": JS_SOURCE})
338
- assert list(fieldtest.sources([root], "javascript", False)), "static skipped"
339
- # And the python side keeps the exemption it has always had.
340
- root2 = tree(tmp_path / "two", {"my_mod/static/src/thing.py": "x = 1\n"})
341
- assert list(fieldtest.sources([root2], "python", False)) == []
342
-
343
-
344
- def test_a_vendored_bundle_is_not_reviewed(tmp_path):
345
- """Recognised from the file rather than the directory, and by ordinary
346
- JavaScript convention rather than anything Odoo-shaped: the name says so,
347
- or one line is longer than any line a person types. OpenSPP's five vendored
348
- libraries carry single lines of 32,071 to 375,248 characters; its 59
349
- first-party files top out at 87."""
350
- root = tree(tmp_path, {
351
- "static/src/app.js": JS_SOURCE,
352
- "static/lib/turf.min.js": "var t=1;\n",
353
- "static/lib/maptiler.umd.js": "var x=" + ("a" * 2000) + ";\n",
354
- })
355
- found = [os.path.basename(f) for f in fieldtest.sources([root], "javascript", False)]
356
- assert found == ["app.js"], found
357
-
358
-
359
- def test_a_test_named_by_its_file_is_still_a_test(tmp_path):
360
- """_is_test_path reads directory components, which is where Python and Odoo
361
- put tests. The JavaScript convention puts them beside the thing they test,
362
- and 45 of the first 355 TypeScript hits were in files like hono.test.ts."""
363
- root = tree(tmp_path, {
364
- "src/app.ts": JS_SOURCE,
365
- "src/app.test.ts": JS_SOURCE,
366
- "src/app.spec.ts": JS_SOURCE,
367
- })
368
- found = [os.path.basename(f) for f in fieldtest.sources([root], "typescript", False)]
369
- assert found == ["app.ts"], found
370
 
371
 
372
  def test_a_filename_that_merely_contains_test_is_not_one(tmp_path):
@@ -378,15 +340,15 @@ def test_a_filename_that_merely_contains_test_is_not_one(tmp_path):
378
 
379
  def test_two_matches_on_one_line_are_one_finding():
380
  """Pattern rules emitted one hit per regex match, and the evidence IS the
381
- line -- so a line declaring two `any` parameters reported itself twice,
382
- word for word. The semantic checkers have always deduplicated."""
383
  import corpus
384
  import layers
385
  rules = [r for r in corpus.load_rules()
386
- if r["id"] == "typescript.unknown-not-any-at-boundaries"]
387
  assert rules, "rule missing from the corpus"
388
- body = "export function f(a: any, b: any) {\n return a;\n}\n"
389
- hits = layers.scan_source(body, rules, "typescript", errors=[])
390
  assert len(hits) == 1, hits
391
  assert hits[0].line == 1
392
 
 
137
  assert fieldtest.scan([root], "odoo")["files"] == 1
138
 
139
 
140
+ def test_only_python_is_read(tmp_path):
141
+ """One stack, and the files it reviews are Python. A .ts file beside them
142
+ is not counted as reviewed and is not counted in the line total either --
143
+ a per-kloc rate whose denominator includes lines nothing ran over is the
144
+ quiet way that number gets better."""
145
  root = tree(tmp_path, {"a.py": CLEAN, "b.ts": "const x: any = 1;\n"})
146
+ result = fieldtest.scan([root], "odoo")
147
+ assert result["files"] == 1
 
 
148
 
149
 
150
  def test_rules_that_never_fired_are_listed(tmp_path):
 
322
 
323
  # -------------------------------- what counts as the code under review (3.44)
324
 
325
+ def test_static_is_never_the_code_under_review(tmp_path):
326
+ """It used to be one list per stack, because static/ holds every line of
327
+ first-party JavaScript an Odoo module ships. With the JS rules gone there
328
+ is nothing in there for the corpus to say anything about, so it is skipped
329
+ for good -- including the stray .py file people leave in it."""
330
+ root = tree(tmp_path, {"my_mod/static/src/thing.py": "x = 1\n"})
331
+ assert list(fieldtest.sources([root], "odoo", False)) == []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
 
334
  def test_a_filename_that_merely_contains_test_is_not_one(tmp_path):
 
340
 
341
  def test_two_matches_on_one_line_are_one_finding():
342
  """Pattern rules emitted one hit per regex match, and the evidence IS the
343
+ line -- so a line with two matches on it reported itself twice, word for
344
+ word. The semantic checkers have always deduplicated."""
345
  import corpus
346
  import layers
347
  rules = [r for r in corpus.load_rules()
348
+ if r["id"] == "python.mutable-default-argument"]
349
  assert rules, "rule missing from the corpus"
350
+ body = "def f(a=[], b=[]):\n return a\n"
351
+ hits = layers.scan_source(body, rules, "odoo", errors=[])
352
  assert len(hits) == 1, hits
353
  assert hits[0].line == 1
354
 
tests/test_layers.py CHANGED
@@ -156,23 +156,6 @@ def test_context_still_warns_about_a_rule_that_does_not_apply(odoo_rules):
156
  assert all(f.rule for f in result.findings), "and each one names its rule"
157
 
158
 
159
- def test_strict_off_is_a_gap_too():
160
- """Same shape as the series mismatch: true of the project, not of the
161
- answer, and true on every run until somebody finishes the migration."""
162
- result = layers.layer_context([], _ctx(stack="typescript", ts_strict=False))
163
- assert result.findings == []
164
- assert result.verdict == layers.UNKNOWN
165
- assert any("strict off" in why for _, why in result.not_checked)
166
-
167
-
168
- def test_strict_on_is_something_this_layer_checked():
169
- result = layers.layer_context([], _ctx(stack="typescript", ts_strict=True))
170
- assert result.verdict == layers.PASS
171
- assert "tsconfig strict" in result.checked
172
-
173
-
174
- # ------------------------------------------------------------ semantic gating
175
-
176
  def test_lifted_fragment_moves_imports_out_of_findings(odoo_rules):
177
  """Two medium findings for imports a method fragment could never carry were
178
  enough to turn this correct answer into WARN."""
 
156
  assert all(f.rule for f in result.findings), "and each one names its rule"
157
 
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  def test_lifted_fragment_moves_imports_out_of_findings(odoo_rules):
160
  """Two medium findings for imports a method fragment could never carry were
161
  enough to turn this correct answer into WARN."""
tests/test_report.py CHANGED
@@ -33,10 +33,17 @@ def rules_events(retrieved, violated=()):
33
  def test_corpus_rule_ids_reads_the_whole_corpus():
34
  ids = report._corpus_rule_ids()
35
  assert ids is not None
36
- assert len(ids) >= 30
37
  assert "odoo.prefer-sql-constraint" in ids
38
- assert any(i.startswith("typescript.") for i in ids)
39
- assert any(i.startswith("javascript.") for i in ids)
 
 
 
 
 
 
 
40
 
41
 
42
  def test_corpus_rule_ids_returns_none_when_unreadable(monkeypatch, tmp_path):
@@ -74,19 +81,22 @@ def test_absence_is_not_reported_as_a_corpus_defect(capsys):
74
  assert "nexa eval --retrieval" in out, "and the real measurement pointed at"
75
 
76
 
77
- def test_absence_arithmetic_counts_each_stack_separately(capsys):
78
- """Retrieval is scoped to one stack, so the headroom is per stack. Pooling
79
- them would hide a stack nobody has asked about behind one that is busy."""
 
80
  report.cmd_rules(rules_events(["odoo.prefer-sql-constraint"]))
81
  out = capsys.readouterr().out
82
  assert "queries recorded, by stack:" in out
83
- for stack in ("odoo", "typescript", "javascript"):
84
- assert stack in out.split("queries recorded, by stack:")[1]
 
85
 
86
 
87
  def test_full_coverage_says_so_rather_than_printing_nothing(capsys, monkeypatch):
88
- monkeypatch.setattr(report, "_corpus_rule_ids", lambda: {"a.one", "a.two"})
89
- report.cmd_rules(rules_events(["a.one", "a.two"]))
 
90
  assert "every rule has surfaced" in capsys.readouterr().out
91
 
92
 
 
33
  def test_corpus_rule_ids_reads_the_whole_corpus():
34
  ids = report._corpus_rule_ids()
35
  assert ids is not None
36
+ assert len(ids) >= 27
37
  assert "odoo.prefer-sql-constraint" in ids
38
+ assert any(i.startswith("python.") for i in ids), "the composed-in half"
39
+
40
+
41
+ def test_a_rules_stack_comes_off_the_rule_and_not_off_its_id():
42
+ """python.decimal-for-money-not-float is in the odoo stack. Splitting the
43
+ id on the dot instead invents a stack with eight rules and no queries, and
44
+ then reports all eight as unreachable by arithmetic."""
45
+ ids = report._corpus_rule_ids()
46
+ assert set(ids.values()) == {"odoo"}
47
 
48
 
49
  def test_corpus_rule_ids_returns_none_when_unreadable(monkeypatch, tmp_path):
 
81
  assert "nexa eval --retrieval" in out, "and the real measurement pointed at"
82
 
83
 
84
+ def test_absence_arithmetic_counts_the_stack_the_rules_are_actually_in(capsys):
85
+ """Retrieval is scoped to a stack, so the headroom is per stack. There is
86
+ one, and every rule is in it -- including the eight whose id says python,
87
+ which must not be counted as a stack of their own with no queries."""
88
  report.cmd_rules(rules_events(["odoo.prefer-sql-constraint"]))
89
  out = capsys.readouterr().out
90
  assert "queries recorded, by stack:" in out
91
+ tail = out.split("queries recorded, by stack:")[1]
92
+ assert "odoo" in tail
93
+ assert "python" not in tail, "python is an id prefix, not a stack"
94
 
95
 
96
  def test_full_coverage_says_so_rather_than_printing_nothing(capsys, monkeypatch):
97
+ monkeypatch.setattr(report, "_corpus_rule_ids",
98
+ lambda: {"odoo.one": "odoo", "odoo.two": "odoo"})
99
+ report.cmd_rules(rules_events(["odoo.one", "odoo.two"]))
100
  assert "every rule has surfaced" in capsys.readouterr().out
101
 
102
 
tests/test_subject.py CHANGED
@@ -31,42 +31,52 @@ def plan_for(tmp_path, name, body, forced=None):
31
  return subject.plan(str(path), forced)
32
 
33
 
34
- def test_a_py_file_that_imports_odoo_gets_the_odoo_corpus(tmp_path):
35
  plan = plan_for(tmp_path, "models/thing.py", ODOO)
36
  assert plan["stack"] == "odoo"
37
- assert "odoo imports" in plan["routed_by"]
 
38
 
39
 
40
- def test_a_py_file_with_no_odoo_in_it_gets_the_python_corpus(tmp_path):
41
- """The python stack exists so this answer is not fifteen findings about a
42
- framework the file does not use."""
 
43
  plan = plan_for(tmp_path, "load.py", "import csv\n\n\ndef main():\n pass\n")
44
- assert plan["stack"] == "python"
 
45
 
46
 
47
- def test_a_manifest_above_the_file_is_enough(tmp_path):
48
- """A view mixin need not import odoo itself to be part of a module."""
 
 
49
  (tmp_path / "mod").mkdir()
50
  (tmp_path / "mod" / "__manifest__.py").write_text(
51
  '{"name": "m", "version": "17.0.1.0.0"}', encoding="utf-8")
52
  plan = plan_for(tmp_path, "mod/models/helper.py", "def helper():\n return 1\n")
53
  assert plan["stack"] == "odoo"
54
- assert "__manifest__.py" in plan["routed_by"]
55
 
56
 
57
- @pytest.mark.parametrize("name,stack", [
58
- ("app.ts", "typescript"), ("app.tsx", "typescript"),
59
- ("app.js", "javascript"), ("app.jsx", "javascript"), ("app.mjs", "javascript"),
60
- ])
61
- def test_the_extension_decides_the_web_stacks(tmp_path, name, stack):
62
- assert plan_for(tmp_path, name, "export const x = 1;\n")["stack"] == stack
 
63
 
64
 
65
  def test_stack_can_be_forced_and_says_so(tmp_path):
66
  plan = plan_for(tmp_path, "load.py", "import csv\n", forced="odoo")
67
  assert plan["stack"] == "odoo"
68
  assert plan["routed_by"] == "--stack"
69
- assert any("no Odoo imports here" in n for n in plan["notes"])
 
 
 
 
70
 
71
 
72
  def test_a_file_over_budget_loses_whole_symbols_and_names_them(tmp_path):
@@ -93,9 +103,7 @@ def test_a_file_that_fits_says_nothing(tmp_path):
93
  SUBJECT_ROUTES = [
94
  ("odoo_module/models/fsm_job.py", "odoo"),
95
  ("odoo_module/models/fsm_visit.py", "odoo"),
96
- ("etl/load_prices.py", "python"),
97
- ("web/src/partners.ts", "typescript"),
98
- ("web/src/board.js", "javascript"),
99
  ]
100
 
101
 
@@ -103,9 +111,9 @@ SUBJECT_ROUTES = [
103
  def test_every_subject_reaches_the_route_it_was_written_for(relative, stack):
104
  """subjects/ is the input set for `nexa eval --check`.
105
 
106
- A subject that stops routing where it was meant to route silently changes
107
- what the measurement covers -- the eval would still print five files and a
108
- distribution, with one of the four corpora no longer in it.
109
  """
110
  plan = subject.plan(os.path.join(SUBJECTS, *relative.split("/")))
111
  assert plan["stack"] == stack
@@ -127,12 +135,14 @@ def test_no_subject_is_mistaken_for_a_test_file():
127
 
128
 
129
  def test_the_subjects_carry_the_surroundings_the_context_layer_reads():
130
- """Two of them exist to give that layer something real to say."""
 
131
  odoo = subject.plan(os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"))
132
  assert odoo["ctx"]["odoo_series"] == "17.0"
133
 
134
- web = subject.plan(os.path.join(SUBJECTS, "web", "src", "partners.ts"))
135
- assert web["ctx"]["ts_strict"] is False
 
136
 
137
 
138
  def test_the_flagged_symbol_survives_the_budget():
 
31
  return subject.plan(str(path), forced)
32
 
33
 
34
+ def test_a_py_file_that_imports_odoo_is_reviewed(tmp_path):
35
  plan = plan_for(tmp_path, "models/thing.py", ODOO)
36
  assert plan["stack"] == "odoo"
37
+ assert plan["routed_by"] == "extension"
38
+ assert not plan["notes"], "an Odoo file needs no caveat"
39
 
40
 
41
+ def test_a_py_file_with_no_odoo_in_it_is_still_reviewed_and_says_so(tmp_path):
42
+ """There is no second corpus to send it to. The 19 ORM rules have nothing
43
+ to match in a plain script, the 8 plain-Python rules do, and the report
44
+ says which of the two it is rather than leaving the reader to guess."""
45
  plan = plan_for(tmp_path, "load.py", "import csv\n\n\ndef main():\n pass\n")
46
+ assert plan["stack"] == "odoo"
47
+ assert any("plain-Python" in n for n in plan["notes"])
48
 
49
 
50
+ def test_a_manifest_above_the_file_counts_as_odoo_in_the_report(tmp_path):
51
+ """A view mixin need not import odoo itself to be part of a module. That no
52
+ longer decides which rules run -- one corpus does -- but it does decide
53
+ whether the report calls this an Odoo file."""
54
  (tmp_path / "mod").mkdir()
55
  (tmp_path / "mod" / "__manifest__.py").write_text(
56
  '{"name": "m", "version": "17.0.1.0.0"}', encoding="utf-8")
57
  plan = plan_for(tmp_path, "mod/models/helper.py", "def helper():\n return 1\n")
58
  assert plan["stack"] == "odoo"
59
+ assert not plan["notes"]
60
 
61
 
62
+ @pytest.mark.parametrize("name", ["app.ts", "app.tsx", "app.js", "app.jsx",
63
+ "app.mjs", "app.go"])
64
+ def test_a_file_in_another_language_is_refused_not_reviewed(tmp_path, name):
65
+ """The rules that used to answer for these are gone, and silence about a
66
+ file nobody checked is the one lie this tool is built not to tell."""
67
+ with pytest.raises(subject.Unreviewable):
68
+ plan_for(tmp_path, name, "export const x = 1;\n")
69
 
70
 
71
  def test_stack_can_be_forced_and_says_so(tmp_path):
72
  plan = plan_for(tmp_path, "load.py", "import csv\n", forced="odoo")
73
  assert plan["stack"] == "odoo"
74
  assert plan["routed_by"] == "--stack"
75
+
76
+
77
+ def test_forcing_a_stack_that_does_not_exist_is_refused(tmp_path):
78
+ with pytest.raises(subject.Unreviewable):
79
+ plan_for(tmp_path, "load.py", "import csv\n", forced="typescript")
80
 
81
 
82
  def test_a_file_over_budget_loses_whole_symbols_and_names_them(tmp_path):
 
103
  SUBJECT_ROUTES = [
104
  ("odoo_module/models/fsm_job.py", "odoo"),
105
  ("odoo_module/models/fsm_visit.py", "odoo"),
106
+ ("etl/load_prices.py", "odoo"),
 
 
107
  ]
108
 
109
 
 
111
  def test_every_subject_reaches_the_route_it_was_written_for(relative, stack):
112
  """subjects/ is the input set for `nexa eval --check`.
113
 
114
+ A subject that stops being reviewable silently changes what the
115
+ measurement covers -- the eval would still print its files and its
116
+ distribution, with one of them quietly missing from the set.
117
  """
118
  plan = subject.plan(os.path.join(SUBJECTS, *relative.split("/")))
119
  assert plan["stack"] == stack
 
135
 
136
 
137
  def test_the_subjects_carry_the_surroundings_the_context_layer_reads():
138
+ """The module exists to give that layer something real to say: it pins
139
+ 17.0, one series below the 18.0 the corpus was checked against."""
140
  odoo = subject.plan(os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"))
141
  assert odoo["ctx"]["odoo_series"] == "17.0"
142
 
143
+ script = subject.plan(os.path.join(SUBJECTS, "etl", "load_prices.py"))
144
+ assert script["ctx"]["odoo_series"] is None
145
+ assert any("plain-Python" in n for n in script["notes"])
146
 
147
 
148
  def test_the_flagged_symbol_survives_the_budget():
tests/test_validate.py CHANGED
@@ -27,7 +27,7 @@ import corpus
27
  def good_rule(**overrides):
28
  rule = {
29
  "id": "python.a-sample-rule",
30
- "stack": "python",
31
  "severity": "high",
32
  # The second axis. `unmeasured` is the default the build gives a rule
33
  # nothing has placed, and it derives advisory false.
@@ -50,7 +50,7 @@ def corpus_dir(tmp_path, monkeypatch):
50
  path.mkdir()
51
  monkeypatch.setattr(corpus, "RULES_DIR", str(path))
52
 
53
- def write(rules, name="python.json"):
54
  with io.open(str(path / name), "w", encoding="utf-8") as handle:
55
  handle.write(json.dumps(rules, ensure_ascii=False))
56
 
@@ -108,11 +108,22 @@ def test_a_duplicate_id_fails(corpus_dir, capsys):
108
  assert any("duplicate id" in line for line in failures(capsys))
109
 
110
 
111
- def test_an_id_that_does_not_match_its_stack_fails(corpus_dir, capsys):
112
- corpus_dir([good_rule(id="odoo.a-sample-rule")])
 
 
 
113
  assert corpus.cmd_validate() == 1
114
- assert any("id prefix does not match stack" in line
115
- for line in failures(capsys))
 
 
 
 
 
 
 
 
116
 
117
 
118
  def test_ui_only_enforcement_must_say_what_bypasses_it(corpus_dir, capsys):
 
27
  def good_rule(**overrides):
28
  rule = {
29
  "id": "python.a-sample-rule",
30
+ "stack": "odoo",
31
  "severity": "high",
32
  # The second axis. `unmeasured` is the default the build gives a rule
33
  # nothing has placed, and it derives advisory false.
 
50
  path.mkdir()
51
  monkeypatch.setattr(corpus, "RULES_DIR", str(path))
52
 
53
+ def write(rules, name="odoo.json"):
54
  with io.open(str(path / name), "w", encoding="utf-8") as handle:
55
  handle.write(json.dumps(rules, ensure_ascii=False))
56
 
 
108
  assert any("duplicate id" in line for line in failures(capsys))
109
 
110
 
111
+ def test_an_id_with_an_unknown_origin_prefix_fails(corpus_dir, capsys):
112
+ """The prefix names where a rule came from -- odoo. or python. -- and it is
113
+ what every measurement and every citation is keyed on. A rule whose prefix
114
+ is neither is orphaned from its own classification artifact, silently."""
115
+ corpus_dir([good_rule(id="ruff.a-sample-rule")])
116
  assert corpus.cmd_validate() == 1
117
+ assert any("id prefix is not one of" in line for line in failures(capsys))
118
+
119
+
120
+ def test_both_origin_prefixes_are_accepted_under_the_one_stack(corpus_dir):
121
+ """python.decimal-for-money-not-float carries stack odoo. The prefix is
122
+ provenance, the stack is selection, and they are allowed to differ."""
123
+ corpus_dir([good_rule(id="odoo.a-sample-rule"),
124
+ good_rule(id="python.another-sample-rule",
125
+ title="A second sample rule for the validator")])
126
+ assert corpus.cmd_validate() == 0
127
 
128
 
129
  def test_ui_only_enforcement_must_say_what_bypasses_it(corpus_dir, capsys):