jevkit-lint 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jevkit_lint-0.1.0/.gitignore +12 -0
- jevkit_lint-0.1.0/PKG-INFO +135 -0
- jevkit_lint-0.1.0/README.md +115 -0
- jevkit_lint-0.1.0/pyproject.toml +33 -0
- jevkit_lint-0.1.0/src/jevkit_lint/__init__.py +14 -0
- jevkit_lint-0.1.0/src/jevkit_lint/cli.py +140 -0
- jevkit_lint-0.1.0/src/jevkit_lint/diagnostic.py +57 -0
- jevkit_lint-0.1.0/src/jevkit_lint/linter.py +92 -0
- jevkit_lint-0.1.0/src/jevkit_lint/rules.py +595 -0
- jevkit_lint-0.1.0/tests/test_cli.py +58 -0
- jevkit_lint-0.1.0/tests/test_linter.py +52 -0
- jevkit_lint-0.1.0/tests/test_rules.py +202 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jevkit-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Static linter for TypeSafe Jev questions. Catches the documented jev-1.13 failure modes before you spend a token. No API key required.
|
|
5
|
+
Project-URL: Homepage, https://github.com/pjdurden/jevkit-py
|
|
6
|
+
Project-URL: Issues, https://github.com/pjdurden/jevkit-py/issues
|
|
7
|
+
Author: Prajjwal Chittori
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: jev,lint,linter,static-analysis,system-one,typesafe
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: jevkit-core>=0.1.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# jevkit-lint
|
|
22
|
+
|
|
23
|
+
Static linter for [TypeSafe](https://typesafe.ai) Jev questions.
|
|
24
|
+
|
|
25
|
+
TypeSafe publishes a [list of failure modes](https://docs.typesafe.ai/model-jaggedness/jev-1.13)
|
|
26
|
+
for `jev-1.13`: it reads instructions literally, it cannot count, it reads dates
|
|
27
|
+
as text rather than ordered quantities, it loses accuracy on indirection. Most
|
|
28
|
+
of those are visible in your question definitions before you send anything.
|
|
29
|
+
|
|
30
|
+
`jevkit-lint` reads the definitions and tells you. It never calls the API, so it
|
|
31
|
+
needs no key and costs nothing to run in CI.
|
|
32
|
+
|
|
33
|
+
> Unofficial and unaffiliated with TypeSafe.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install jevkit-lint
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from jevkit_lint import lint
|
|
45
|
+
|
|
46
|
+
result = lint({
|
|
47
|
+
"urgency": {"type": "noul", "instructions": "How many days has the customer waited?"},
|
|
48
|
+
"team": {"type": "choice", "instructions": "Which team should handle this",
|
|
49
|
+
"criteria": {"billing": "Payment issues", "technical": "Bugs"}},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
print(result.format())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
urgency: error [JEV001] Question asks the model to count or do arithmetic.
|
|
57
|
+
found: How many
|
|
58
|
+
hint: jev-1.13 is not a calculator and does not count reliably. Iterate the
|
|
59
|
+
candidates in code, ask one Noul per item, and sum the answers yourself.
|
|
60
|
+
team: warning [JEV008] Choice has no no-match option.
|
|
61
|
+
found: billing, technical
|
|
62
|
+
hint: A Choice always returns one of its options. With nothing meaning 'none of
|
|
63
|
+
these', a state that fits no option still produces a confident-looking
|
|
64
|
+
answer. Add an explicit none/unknown option, or gate on a separate
|
|
65
|
+
presence Noul.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`result.ok` is `True` when nothing rose to an error, so it drops straight into a
|
|
69
|
+
guard:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
if not lint(questions, state).ok:
|
|
73
|
+
raise ValueError("refusing to send a request that will not answer what we meant")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## CLI
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jevkit-lint request.json # a {state, questions} object, or bare questions
|
|
80
|
+
jevkit-lint cassette.jevl # lint every recorded request
|
|
81
|
+
jevkit-lint - < request.json # stdin
|
|
82
|
+
jevkit-lint request.json --strict # exit non-zero on warnings too
|
|
83
|
+
jevkit-lint request.json --format json
|
|
84
|
+
jevkit-lint --list-rules
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Exit codes: `0` clean, `1` findings, `2` bad usage.
|
|
88
|
+
|
|
89
|
+
## Rules
|
|
90
|
+
|
|
91
|
+
Each rule names the documented failure mode it comes from.
|
|
92
|
+
|
|
93
|
+
| Code | Rule | Failure mode |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| JEV001 | math-and-counting | Math and Numbers |
|
|
96
|
+
| JEV002 | date-comparison | Date and time comparison |
|
|
97
|
+
| JEV003 | generation-request | Generation |
|
|
98
|
+
| JEV004 | negation | Literal reading |
|
|
99
|
+
| JEV005 | vague-scoping | Literal reading |
|
|
100
|
+
| JEV006 | indirection | Indirection |
|
|
101
|
+
| JEV007 | noul-polarity | Contradictory instructions and criteria |
|
|
102
|
+
| JEV008 | choice-no-match | Common-sense structural invariants |
|
|
103
|
+
| JEV009 | choice-arity | Common-sense structural invariants |
|
|
104
|
+
| JEV010 | option-descriptions | Literal reading |
|
|
105
|
+
| JEV011 | score-levels | Literal reading |
|
|
106
|
+
| JEV012 | instructions-present | Literal reading |
|
|
107
|
+
| JEV013 | question-id-reference | Indirection |
|
|
108
|
+
| JEV014 | state-budget | Large state full of irrelevant detail |
|
|
109
|
+
| JEV015 | total-budget | Large state full of irrelevant detail |
|
|
110
|
+
| JEV016 | state-noise-ratio | Large state full of irrelevant detail |
|
|
111
|
+
| JEV017 | numeric-representation | Math and Numbers |
|
|
112
|
+
| JEV018 | duplicate-questions | Common-sense structural invariants |
|
|
113
|
+
|
|
114
|
+
Select or suppress by code:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
lint(questions, select=["JEV001", "JEV002"])
|
|
118
|
+
lint(questions, ignore=["JEV008"])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## What it cannot do
|
|
122
|
+
|
|
123
|
+
Adversarial content is a documented failure mode and is **not** linted. Whether
|
|
124
|
+
a state is hostile depends on the content at runtime, not on the question
|
|
125
|
+
definition, so a static rule would be theatre. Screen untrusted state at
|
|
126
|
+
request time instead.
|
|
127
|
+
|
|
128
|
+
Token counts are estimates. jevkit deliberately does not bundle a tokenizer:
|
|
129
|
+
TypeSafe does not publish which one Jev uses, and a confidently wrong count is
|
|
130
|
+
worse than an honest approximation. The estimator errs conservative, so leave
|
|
131
|
+
headroom near the limits.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# jevkit-lint
|
|
2
|
+
|
|
3
|
+
Static linter for [TypeSafe](https://typesafe.ai) Jev questions.
|
|
4
|
+
|
|
5
|
+
TypeSafe publishes a [list of failure modes](https://docs.typesafe.ai/model-jaggedness/jev-1.13)
|
|
6
|
+
for `jev-1.13`: it reads instructions literally, it cannot count, it reads dates
|
|
7
|
+
as text rather than ordered quantities, it loses accuracy on indirection. Most
|
|
8
|
+
of those are visible in your question definitions before you send anything.
|
|
9
|
+
|
|
10
|
+
`jevkit-lint` reads the definitions and tells you. It never calls the API, so it
|
|
11
|
+
needs no key and costs nothing to run in CI.
|
|
12
|
+
|
|
13
|
+
> Unofficial and unaffiliated with TypeSafe.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install jevkit-lint
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from jevkit_lint import lint
|
|
25
|
+
|
|
26
|
+
result = lint({
|
|
27
|
+
"urgency": {"type": "noul", "instructions": "How many days has the customer waited?"},
|
|
28
|
+
"team": {"type": "choice", "instructions": "Which team should handle this",
|
|
29
|
+
"criteria": {"billing": "Payment issues", "technical": "Bugs"}},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
print(result.format())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
urgency: error [JEV001] Question asks the model to count or do arithmetic.
|
|
37
|
+
found: How many
|
|
38
|
+
hint: jev-1.13 is not a calculator and does not count reliably. Iterate the
|
|
39
|
+
candidates in code, ask one Noul per item, and sum the answers yourself.
|
|
40
|
+
team: warning [JEV008] Choice has no no-match option.
|
|
41
|
+
found: billing, technical
|
|
42
|
+
hint: A Choice always returns one of its options. With nothing meaning 'none of
|
|
43
|
+
these', a state that fits no option still produces a confident-looking
|
|
44
|
+
answer. Add an explicit none/unknown option, or gate on a separate
|
|
45
|
+
presence Noul.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`result.ok` is `True` when nothing rose to an error, so it drops straight into a
|
|
49
|
+
guard:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
if not lint(questions, state).ok:
|
|
53
|
+
raise ValueError("refusing to send a request that will not answer what we meant")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
jevkit-lint request.json # a {state, questions} object, or bare questions
|
|
60
|
+
jevkit-lint cassette.jevl # lint every recorded request
|
|
61
|
+
jevkit-lint - < request.json # stdin
|
|
62
|
+
jevkit-lint request.json --strict # exit non-zero on warnings too
|
|
63
|
+
jevkit-lint request.json --format json
|
|
64
|
+
jevkit-lint --list-rules
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Exit codes: `0` clean, `1` findings, `2` bad usage.
|
|
68
|
+
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
71
|
+
Each rule names the documented failure mode it comes from.
|
|
72
|
+
|
|
73
|
+
| Code | Rule | Failure mode |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| JEV001 | math-and-counting | Math and Numbers |
|
|
76
|
+
| JEV002 | date-comparison | Date and time comparison |
|
|
77
|
+
| JEV003 | generation-request | Generation |
|
|
78
|
+
| JEV004 | negation | Literal reading |
|
|
79
|
+
| JEV005 | vague-scoping | Literal reading |
|
|
80
|
+
| JEV006 | indirection | Indirection |
|
|
81
|
+
| JEV007 | noul-polarity | Contradictory instructions and criteria |
|
|
82
|
+
| JEV008 | choice-no-match | Common-sense structural invariants |
|
|
83
|
+
| JEV009 | choice-arity | Common-sense structural invariants |
|
|
84
|
+
| JEV010 | option-descriptions | Literal reading |
|
|
85
|
+
| JEV011 | score-levels | Literal reading |
|
|
86
|
+
| JEV012 | instructions-present | Literal reading |
|
|
87
|
+
| JEV013 | question-id-reference | Indirection |
|
|
88
|
+
| JEV014 | state-budget | Large state full of irrelevant detail |
|
|
89
|
+
| JEV015 | total-budget | Large state full of irrelevant detail |
|
|
90
|
+
| JEV016 | state-noise-ratio | Large state full of irrelevant detail |
|
|
91
|
+
| JEV017 | numeric-representation | Math and Numbers |
|
|
92
|
+
| JEV018 | duplicate-questions | Common-sense structural invariants |
|
|
93
|
+
|
|
94
|
+
Select or suppress by code:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
lint(questions, select=["JEV001", "JEV002"])
|
|
98
|
+
lint(questions, ignore=["JEV008"])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## What it cannot do
|
|
102
|
+
|
|
103
|
+
Adversarial content is a documented failure mode and is **not** linted. Whether
|
|
104
|
+
a state is hostile depends on the content at runtime, not on the question
|
|
105
|
+
definition, so a static rule would be theatre. Screen untrusted state at
|
|
106
|
+
request time instead.
|
|
107
|
+
|
|
108
|
+
Token counts are estimates. jevkit deliberately does not bundle a tokenizer:
|
|
109
|
+
TypeSafe does not publish which one Jev uses, and a confidently wrong count is
|
|
110
|
+
worse than an honest approximation. The estimator errs conservative, so leave
|
|
111
|
+
headroom near the limits.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jevkit-lint"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Static linter for TypeSafe Jev questions. Catches the documented jev-1.13 failure modes before you spend a token. No API key required."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Prajjwal Chittori" }]
|
|
9
|
+
keywords = ["jev", "typesafe", "system-one", "lint", "linter", "static-analysis"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["jevkit-core>=0.1.0"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
jevkit-lint = "jevkit_lint.cli:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/pjdurden/jevkit-py"
|
|
26
|
+
Issues = "https://github.com/pjdurden/jevkit-py/issues"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/jevkit_lint"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Static linter for TypeSafe Jev questions.
|
|
2
|
+
|
|
3
|
+
Catches the failure modes TypeSafe documents for jev-1.13 before you spend a
|
|
4
|
+
token on them. Needs no API key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .diagnostic import Diagnostic, Severity
|
|
8
|
+
from .linter import LintResult, lint
|
|
9
|
+
from .rules import RULES, Rule, all_codes
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["lint", "LintResult", "Diagnostic", "Severity", "Rule", "RULES",
|
|
14
|
+
"all_codes", "__version__"]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""``jevkit-lint`` command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from jevkit_core import RecordFormatError, read_records
|
|
11
|
+
|
|
12
|
+
from .diagnostic import Severity
|
|
13
|
+
from .linter import lint
|
|
14
|
+
from .rules import RULES
|
|
15
|
+
|
|
16
|
+
EXIT_OK = 0
|
|
17
|
+
EXIT_FINDINGS = 1
|
|
18
|
+
EXIT_USAGE = 2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_payload(path: str) -> list[tuple[str, Any, dict[str, Any]]]:
|
|
22
|
+
"""Return (label, state, questions) triples from a file.
|
|
23
|
+
|
|
24
|
+
Accepts a `.jevl` record file, a request object with `state` and
|
|
25
|
+
`questions`, or a bare questions object.
|
|
26
|
+
"""
|
|
27
|
+
if path == "-":
|
|
28
|
+
text = sys.stdin.read()
|
|
29
|
+
source = "<stdin>"
|
|
30
|
+
else:
|
|
31
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
32
|
+
text = fh.read()
|
|
33
|
+
source = path
|
|
34
|
+
|
|
35
|
+
if path.endswith(".jevl"):
|
|
36
|
+
import io
|
|
37
|
+
out = []
|
|
38
|
+
for i, record in enumerate(read_records(io.StringIO(text))):
|
|
39
|
+
out.append((f"{source}#{i}", record.state, record.questions))
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
data = json.loads(text)
|
|
43
|
+
if not isinstance(data, dict):
|
|
44
|
+
raise ValueError(f"{source}: expected a JSON object at the top level")
|
|
45
|
+
|
|
46
|
+
if "questions" in data and isinstance(data["questions"], dict):
|
|
47
|
+
return [(source, data.get("state", ""), data["questions"])]
|
|
48
|
+
return [(source, "", data)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _print_rules() -> None:
|
|
52
|
+
width = max(len(r.name) for r in RULES)
|
|
53
|
+
print("code name" + " " * (width - 4) + " documented failure mode")
|
|
54
|
+
print("-" * (8 + width + 2 + 40))
|
|
55
|
+
for rule in RULES:
|
|
56
|
+
print(f"{rule.code} {rule.name:<{width}} {rule.mode}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> int:
|
|
60
|
+
parser = argparse.ArgumentParser(
|
|
61
|
+
prog="jevkit-lint",
|
|
62
|
+
description="Statically lint TypeSafe Jev questions against the documented "
|
|
63
|
+
"jev-1.13 failure modes. Never calls the API.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument("files", nargs="*",
|
|
66
|
+
help="JSON request files, .jevl record files, or - for stdin")
|
|
67
|
+
parser.add_argument("--select", metavar="CODES",
|
|
68
|
+
help="comma-separated rule codes to run exclusively")
|
|
69
|
+
parser.add_argument("--ignore", metavar="CODES",
|
|
70
|
+
help="comma-separated rule codes to skip")
|
|
71
|
+
parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
72
|
+
parser.add_argument("--strict", action="store_true",
|
|
73
|
+
help="exit non-zero on warnings too, not just errors")
|
|
74
|
+
parser.add_argument("--list-rules", action="store_true", help="list rules and exit")
|
|
75
|
+
parser.add_argument("--no-color", action="store_true")
|
|
76
|
+
args = parser.parse_args(argv)
|
|
77
|
+
|
|
78
|
+
if args.list_rules:
|
|
79
|
+
_print_rules()
|
|
80
|
+
return EXIT_OK
|
|
81
|
+
|
|
82
|
+
if not args.files:
|
|
83
|
+
parser.error("no input files (use - to read stdin, or --list-rules)")
|
|
84
|
+
|
|
85
|
+
select = args.select.split(",") if args.select else None
|
|
86
|
+
ignore = args.ignore.split(",") if args.ignore else None
|
|
87
|
+
color = sys.stdout.isatty() and not args.no_color
|
|
88
|
+
|
|
89
|
+
payloads: list[tuple[str, Any, dict[str, Any]]] = []
|
|
90
|
+
for path in args.files:
|
|
91
|
+
try:
|
|
92
|
+
payloads.extend(_load_payload(path))
|
|
93
|
+
except (OSError, ValueError, RecordFormatError) as exc:
|
|
94
|
+
print(f"jevkit-lint: {exc}", file=sys.stderr)
|
|
95
|
+
return EXIT_USAGE
|
|
96
|
+
|
|
97
|
+
reports = []
|
|
98
|
+
worst = Severity.INFO
|
|
99
|
+
any_error = any_warning = False
|
|
100
|
+
|
|
101
|
+
for label, state, questions in payloads:
|
|
102
|
+
try:
|
|
103
|
+
result = lint(questions, state, select=select, ignore=ignore)
|
|
104
|
+
except ValueError as exc:
|
|
105
|
+
print(f"jevkit-lint: {label}: {exc}", file=sys.stderr)
|
|
106
|
+
return EXIT_USAGE
|
|
107
|
+
any_error = any_error or bool(result.errors)
|
|
108
|
+
any_warning = any_warning or bool(result.warnings)
|
|
109
|
+
reports.append((label, result))
|
|
110
|
+
|
|
111
|
+
if args.format == "json":
|
|
112
|
+
print(json.dumps(
|
|
113
|
+
{"results": [{"source": label, **result.to_dict()} for label, result in reports]},
|
|
114
|
+
indent=2,
|
|
115
|
+
))
|
|
116
|
+
else:
|
|
117
|
+
for label, result in reports:
|
|
118
|
+
if len(payloads) > 1:
|
|
119
|
+
print(f"== {label}")
|
|
120
|
+
print(result.format(color=color))
|
|
121
|
+
if len(payloads) > 1:
|
|
122
|
+
print()
|
|
123
|
+
total = {"error": 0, "warning": 0, "info": 0}
|
|
124
|
+
for _, result in reports:
|
|
125
|
+
for key, value in result.counts().items():
|
|
126
|
+
total[key] += value
|
|
127
|
+
if sum(total.values()):
|
|
128
|
+
print(f"\n{total['error']} error(s), {total['warning']} warning(s), "
|
|
129
|
+
f"{total['info']} info")
|
|
130
|
+
|
|
131
|
+
del worst
|
|
132
|
+
if any_error:
|
|
133
|
+
return EXIT_FINDINGS
|
|
134
|
+
if args.strict and any_warning:
|
|
135
|
+
return EXIT_FINDINGS
|
|
136
|
+
return EXIT_OK
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__": # pragma: no cover
|
|
140
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Diagnostics produced by the linter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
__all__ = ["Severity", "Diagnostic"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Severity(str, Enum):
|
|
13
|
+
ERROR = "error" # Will very likely produce wrong answers or be rejected.
|
|
14
|
+
WARNING = "warning" # A documented failure mode is in play.
|
|
15
|
+
INFO = "info" # Worth a look; may be intentional.
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str: # pragma: no cover - display only
|
|
18
|
+
return self.value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_ORDER = {Severity.ERROR: 0, Severity.WARNING: 1, Severity.INFO: 2}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Diagnostic:
|
|
26
|
+
code: str
|
|
27
|
+
severity: Severity
|
|
28
|
+
question_id: str | None
|
|
29
|
+
message: str
|
|
30
|
+
hint: str
|
|
31
|
+
evidence: str = ""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def sort_key(self) -> tuple[int, str, str]:
|
|
35
|
+
return (_ORDER[self.severity], self.code, self.question_id or "")
|
|
36
|
+
|
|
37
|
+
def format(self, *, color: bool = False) -> str:
|
|
38
|
+
loc = self.question_id or "<request>"
|
|
39
|
+
head = f"{loc}: {self.severity.value} [{self.code}] {self.message}"
|
|
40
|
+
if color:
|
|
41
|
+
tint = {"error": "\033[31m", "warning": "\033[33m", "info": "\033[36m"}
|
|
42
|
+
head = f"{tint[self.severity.value]}{head}\033[0m"
|
|
43
|
+
lines = [head]
|
|
44
|
+
if self.evidence:
|
|
45
|
+
lines.append(f" found: {self.evidence}")
|
|
46
|
+
lines.append(f" hint: {self.hint}")
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"code": self.code,
|
|
52
|
+
"severity": self.severity.value,
|
|
53
|
+
"question_id": self.question_id,
|
|
54
|
+
"message": self.message,
|
|
55
|
+
"hint": self.hint,
|
|
56
|
+
"evidence": self.evidence,
|
|
57
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""The lint entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
from jevkit_core import normalize_questions
|
|
8
|
+
|
|
9
|
+
from .diagnostic import Diagnostic, Severity
|
|
10
|
+
from .rules import LintContext, rules_for
|
|
11
|
+
|
|
12
|
+
__all__ = ["lint", "LintResult"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LintResult:
|
|
16
|
+
"""Diagnostics for one request, ordered most severe first."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, diagnostics: list[Diagnostic]) -> None:
|
|
19
|
+
self.diagnostics = sorted(diagnostics, key=lambda d: d.sort_key)
|
|
20
|
+
|
|
21
|
+
def __iter__(self):
|
|
22
|
+
return iter(self.diagnostics)
|
|
23
|
+
|
|
24
|
+
def __len__(self) -> int:
|
|
25
|
+
return len(self.diagnostics)
|
|
26
|
+
|
|
27
|
+
def __bool__(self) -> bool:
|
|
28
|
+
return bool(self.diagnostics)
|
|
29
|
+
|
|
30
|
+
def by_severity(self, severity: Severity) -> list[Diagnostic]:
|
|
31
|
+
return [d for d in self.diagnostics if d.severity is severity]
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def errors(self) -> list[Diagnostic]:
|
|
35
|
+
return self.by_severity(Severity.ERROR)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def warnings(self) -> list[Diagnostic]:
|
|
39
|
+
return self.by_severity(Severity.WARNING)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def infos(self) -> list[Diagnostic]:
|
|
43
|
+
return self.by_severity(Severity.INFO)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def ok(self) -> bool:
|
|
47
|
+
"""True when nothing rose to an error."""
|
|
48
|
+
return not self.errors
|
|
49
|
+
|
|
50
|
+
def counts(self) -> dict[str, int]:
|
|
51
|
+
return {
|
|
52
|
+
"error": len(self.errors),
|
|
53
|
+
"warning": len(self.warnings),
|
|
54
|
+
"info": len(self.infos),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"ok": self.ok,
|
|
60
|
+
"counts": self.counts(),
|
|
61
|
+
"diagnostics": [d.to_dict() for d in self.diagnostics],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def format(self, *, color: bool = False) -> str:
|
|
65
|
+
if not self.diagnostics:
|
|
66
|
+
return "No problems found."
|
|
67
|
+
return "\n".join(d.format(color=color) for d in self.diagnostics)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def lint(
|
|
71
|
+
questions: dict[str, Any],
|
|
72
|
+
state: Any = "",
|
|
73
|
+
*,
|
|
74
|
+
select: Iterable[str] | None = None,
|
|
75
|
+
ignore: Iterable[str] | None = None,
|
|
76
|
+
) -> LintResult:
|
|
77
|
+
"""Lint a jev request without calling the API.
|
|
78
|
+
|
|
79
|
+
``questions`` accepts SDK objects or plain dicts. ``state`` is optional: omit
|
|
80
|
+
it to check the questions alone, though the budget rules can only report
|
|
81
|
+
meaningfully when the real state is supplied.
|
|
82
|
+
"""
|
|
83
|
+
normalized = normalize_questions(questions)
|
|
84
|
+
ctx = LintContext.build(state, normalized)
|
|
85
|
+
ignored = {c.upper() for c in (ignore or ())}
|
|
86
|
+
|
|
87
|
+
found: list[Diagnostic] = []
|
|
88
|
+
for rule in rules_for(select):
|
|
89
|
+
if rule.code in ignored:
|
|
90
|
+
continue
|
|
91
|
+
found.extend(rule.check(ctx))
|
|
92
|
+
return LintResult(found)
|