shockharness 0.1.0a1__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.
- shockharness-0.1.0a1/.gitignore +20 -0
- shockharness-0.1.0a1/.shockharness.toml +120 -0
- shockharness-0.1.0a1/CHANGELOG.md +69 -0
- shockharness-0.1.0a1/LICENSE +21 -0
- shockharness-0.1.0a1/PKG-INFO +197 -0
- shockharness-0.1.0a1/README.md +163 -0
- shockharness-0.1.0a1/STATUS.md +494 -0
- shockharness-0.1.0a1/docs/architecture.md +112 -0
- shockharness-0.1.0a1/docs/configuration.md +182 -0
- shockharness-0.1.0a1/docs/limitations.md +127 -0
- shockharness-0.1.0a1/docs/rules.md +313 -0
- shockharness-0.1.0a1/docs/shockharness_distribution_integration.md +168 -0
- shockharness-0.1.0a1/pyproject.toml +85 -0
- shockharness-0.1.0a1/schemas/report-v1.schema.json +127 -0
- shockharness-0.1.0a1/src/shockharness/__init__.py +13 -0
- shockharness-0.1.0a1/src/shockharness/__main__.py +5 -0
- shockharness-0.1.0a1/src/shockharness/analysis.py +262 -0
- shockharness-0.1.0a1/src/shockharness/api.py +68 -0
- shockharness-0.1.0a1/src/shockharness/cli.py +197 -0
- shockharness-0.1.0a1/src/shockharness/config.py +331 -0
- shockharness-0.1.0a1/src/shockharness/constants.py +53 -0
- shockharness-0.1.0a1/src/shockharness/defaults.toml +73 -0
- shockharness-0.1.0a1/src/shockharness/discovery.py +143 -0
- shockharness-0.1.0a1/src/shockharness/errors.py +42 -0
- shockharness-0.1.0a1/src/shockharness/findings.py +70 -0
- shockharness-0.1.0a1/src/shockharness/gate.py +102 -0
- shockharness-0.1.0a1/src/shockharness/globs.py +142 -0
- shockharness-0.1.0a1/src/shockharness/ir/__init__.py +1 -0
- shockharness-0.1.0a1/src/shockharness/ir/javascript.py +176 -0
- shockharness-0.1.0a1/src/shockharness/ir/javascript_catches.py +570 -0
- shockharness-0.1.0a1/src/shockharness/ir/javascript_defaults.py +874 -0
- shockharness-0.1.0a1/src/shockharness/ir/model.py +236 -0
- shockharness-0.1.0a1/src/shockharness/ir/python.py +192 -0
- shockharness-0.1.0a1/src/shockharness/ir/python_catches.py +441 -0
- shockharness-0.1.0a1/src/shockharness/ir/python_defaults.py +917 -0
- shockharness-0.1.0a1/src/shockharness/output/__init__.py +1 -0
- shockharness-0.1.0a1/src/shockharness/output/json.py +10 -0
- shockharness-0.1.0a1/src/shockharness/output/text.py +108 -0
- shockharness-0.1.0a1/src/shockharness/report.py +150 -0
- shockharness-0.1.0a1/src/shockharness/rules/__init__.py +1 -0
- shockharness-0.1.0a1/src/shockharness/rules/defaults.py +279 -0
- shockharness-0.1.0a1/src/shockharness/rules/exceptions.py +304 -0
- shockharness-0.1.0a1/src/shockharness/rules/magic_numbers.py +55 -0
- shockharness-0.1.0a1/src/shockharness/rules/registry.py +736 -0
- shockharness-0.1.0a1/src/shockharness/schema.py +246 -0
- shockharness-0.1.0a1/src/shockharness/source.py +52 -0
- shockharness-0.1.0a1/src/shockharness/suppressions.py +191 -0
- shockharness-0.1.0a1/src/shockharness/version.py +3 -0
- shockharness-0.1.0a1/tests/__init__.py +0 -0
- shockharness-0.1.0a1/tests/cli/__init__.py +0 -0
- shockharness-0.1.0a1/tests/cli/test_cli.py +296 -0
- shockharness-0.1.0a1/tests/conftest.py +98 -0
- shockharness-0.1.0a1/tests/fixtures/expected.json +3046 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/clean.js +3 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/common.cjs +1 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/def_destructuring.js +21 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/def_exception_default.js +74 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/def_logical_or.js +43 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/def_nullish.js +40 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/def_parameters.js +38 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/def_process_env.js +15 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_async_await.js +35 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_boundary_catch.js +87 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_nested_catches.js +28 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_promise_catch.js +32 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_rethrow.js +40 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_return_fallback.js +58 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/exc_untyped_catch.js +32 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/jsx.js +2 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/magic_basic.js +7 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/magic_encodings.js +3 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/magic_negatives.js +10 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/module.mjs +2 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/scopes.js +9 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/strings_regex.js +6 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/suppressions.js +4 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/syntax_error.js +3 -0
- shockharness-0.1.0a1/tests/fixtures/javascript/unicode.js +2 -0
- shockharness-0.1.0a1/tests/fixtures/python/clean.py +15 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_dataclasses.py +30 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_environment.py +23 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_exception_default.py +78 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_mapping_get.py +55 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_nullish.py +28 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_parameters.py +59 -0
- shockharness-0.1.0a1/tests/fixtures/python/def_truthiness.py +42 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_bare.py +12 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_broad.py +26 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_except_star.py +10 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_logging_only.py +49 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_multiple_operations.py +60 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_nested.py +29 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_rethrow.py +44 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_rethrow_transparent.py +30 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_return_fallback.py +62 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_scoped_timeout.py +55 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_tuple.py +22 -0
- shockharness-0.1.0a1/tests/fixtures/python/exc_unresolved.py +91 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_annotations.py +8 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_basic.py +13 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_booleans.py +5 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_encodings.py +3 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_negatives.py +9 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_scopes.py +32 -0
- shockharness-0.1.0a1/tests/fixtures/python/magic_strings_not_numbers.py +5 -0
- shockharness-0.1.0a1/tests/fixtures/python/suppressions.py +9 -0
- shockharness-0.1.0a1/tests/fixtures/python/syntax_error.py +2 -0
- shockharness-0.1.0a1/tests/fixtures/python/unicode.py +3 -0
- shockharness-0.1.0a1/tests/property/__init__.py +0 -0
- shockharness-0.1.0a1/tests/property/test_properties.py +159 -0
- shockharness-0.1.0a1/tests/test_self_compliance.py +21 -0
- shockharness-0.1.0a1/tests/unit/__init__.py +0 -0
- shockharness-0.1.0a1/tests/unit/test_analysis.py +127 -0
- shockharness-0.1.0a1/tests/unit/test_config.py +221 -0
- shockharness-0.1.0a1/tests/unit/test_defaults.py +626 -0
- shockharness-0.1.0a1/tests/unit/test_discovery.py +110 -0
- shockharness-0.1.0a1/tests/unit/test_exceptions.py +473 -0
- shockharness-0.1.0a1/tests/unit/test_gate_and_report.py +222 -0
- shockharness-0.1.0a1/tests/unit/test_globs.py +76 -0
- shockharness-0.1.0a1/tests/unit/test_golden_fixtures.py +49 -0
- shockharness-0.1.0a1/tests/unit/test_javascript_adapter.py +311 -0
- shockharness-0.1.0a1/tests/unit/test_magic001.py +109 -0
- shockharness-0.1.0a1/tests/unit/test_python_adapter.py +280 -0
- shockharness-0.1.0a1/tests/unit/test_registry.py +69 -0
- shockharness-0.1.0a1/tests/unit/test_schema.py +74 -0
- shockharness-0.1.0a1/tests/unit/test_source.py +38 -0
- shockharness-0.1.0a1/tests/unit/test_suppressions.py +99 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ShockHarness analyzing itself. Every exemption below is deliberate and reported.
|
|
2
|
+
# See agent_code_integrity_project_spec.md section 15.5 for the bootstrapping policy.
|
|
3
|
+
|
|
4
|
+
[tool.shockharness]
|
|
5
|
+
profile = "strict"
|
|
6
|
+
include = ["src/**/*.py", "tests/**/*.py"]
|
|
7
|
+
exclude = ["**/.venv/**", "**/node_modules/**", "**/dist/**", "**/build/**", "tests/fixtures/**"]
|
|
8
|
+
parse_errors = "fail"
|
|
9
|
+
|
|
10
|
+
[tool.shockharness.rules.magic_numbers]
|
|
11
|
+
enabled = true
|
|
12
|
+
allowed_values = [0, 1]
|
|
13
|
+
allow_unary_negative_one = false
|
|
14
|
+
exclude_configuration_files = false
|
|
15
|
+
|
|
16
|
+
[tool.shockharness.rules.exceptions]
|
|
17
|
+
enabled = true
|
|
18
|
+
mode = "boundary_whitelist"
|
|
19
|
+
|
|
20
|
+
# Declared boundaries where the tool meets the outside world. Each converts the
|
|
21
|
+
# named failure into a typed analyzer diagnostic; none may swallow it.
|
|
22
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
23
|
+
path = "src/shockharness/config.py"
|
|
24
|
+
language = "python"
|
|
25
|
+
exception = "OSError"
|
|
26
|
+
reason = "Reading a configuration file is I/O; failure becomes a ConfigError (exit 2)"
|
|
27
|
+
|
|
28
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
29
|
+
path = "src/shockharness/config.py"
|
|
30
|
+
language = "python"
|
|
31
|
+
exception = "tomllib.TOMLDecodeError"
|
|
32
|
+
reason = "Config file content is external input; decode failure becomes a ConfigError (exit 2)"
|
|
33
|
+
|
|
34
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
35
|
+
path = "src/shockharness/config.py"
|
|
36
|
+
language = "python"
|
|
37
|
+
exception = "UnicodeDecodeError"
|
|
38
|
+
reason = "Config file bytes are external input; non-UTF-8 becomes a ConfigError (exit 2)"
|
|
39
|
+
|
|
40
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
41
|
+
path = "src/shockharness/analysis.py"
|
|
42
|
+
language = "python"
|
|
43
|
+
exception = "OSError"
|
|
44
|
+
reason = "Reading analyzed files is I/O; failure becomes a per-file read_error status, never a pass"
|
|
45
|
+
|
|
46
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
47
|
+
path = "src/shockharness/analysis.py"
|
|
48
|
+
language = "python"
|
|
49
|
+
exception = "UnicodeDecodeError"
|
|
50
|
+
reason = "Analyzed source is external bytes; undecodable input becomes a decode_error file status"
|
|
51
|
+
|
|
52
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
53
|
+
path = "src/shockharness/ir/python.py"
|
|
54
|
+
language = "python"
|
|
55
|
+
exception = "SyntaxError"
|
|
56
|
+
reason = "ast.parse and tokenize on external source; failure becomes a ParseFailure fact feeding the parse_errors gate"
|
|
57
|
+
|
|
58
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
59
|
+
path = "src/shockharness/ir/python.py"
|
|
60
|
+
language = "python"
|
|
61
|
+
exception = "ValueError"
|
|
62
|
+
reason = "ast.parse raises ValueError for NUL bytes on 3.11 and tokenize raises UnicodeDecodeError (a ValueError) on some CR sequences; both become a ParseFailure"
|
|
63
|
+
|
|
64
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
65
|
+
path = "src/shockharness/ir/python.py"
|
|
66
|
+
language = "python"
|
|
67
|
+
exception = "tokenize.TokenError"
|
|
68
|
+
reason = "Comment extraction tokenizes external source; failure becomes a ParseFailure fact"
|
|
69
|
+
|
|
70
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
71
|
+
path = "src/shockharness/cli.py"
|
|
72
|
+
language = "python"
|
|
73
|
+
exception = "OSError"
|
|
74
|
+
reason = "Writing the report or init file to a user path is I/O; failure becomes a UsageError (exit 2)"
|
|
75
|
+
|
|
76
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
77
|
+
path = "src/shockharness/cli.py"
|
|
78
|
+
language = "python"
|
|
79
|
+
exception = "shockharness.errors.ShockHarnessError"
|
|
80
|
+
reason = "Process boundary: typed configuration and usage errors print one line and exit 2"
|
|
81
|
+
|
|
82
|
+
[[tool.shockharness.rules.exceptions.boundaries]]
|
|
83
|
+
path = "src/shockharness/cli.py"
|
|
84
|
+
language = "python"
|
|
85
|
+
exception = "Exception"
|
|
86
|
+
reason = "Process boundary: an unexpected internal failure prints its full traceback and exits 3 instead of Python's default 1; nothing is hidden"
|
|
87
|
+
|
|
88
|
+
[tool.shockharness.rules.defaults]
|
|
89
|
+
enabled = true
|
|
90
|
+
function_parameters = "forbid"
|
|
91
|
+
dataclass_fields = "forbid"
|
|
92
|
+
missing_value_fallbacks = "forbid"
|
|
93
|
+
truthiness_fallbacks = "review"
|
|
94
|
+
nullish_fallbacks = "forbid"
|
|
95
|
+
configuration_fallbacks = "forbid"
|
|
96
|
+
|
|
97
|
+
[tool.shockharness.rules.magic_strings]
|
|
98
|
+
enabled = true
|
|
99
|
+
mode = "review"
|
|
100
|
+
|
|
101
|
+
[tool.shockharness.complexity]
|
|
102
|
+
enabled = true
|
|
103
|
+
cognitive_warn = 15
|
|
104
|
+
cyclomatic_warn = 15
|
|
105
|
+
max_nesting_warn = 4
|
|
106
|
+
|
|
107
|
+
[tool.shockharness.gates]
|
|
108
|
+
new_hard_violations = "fail"
|
|
109
|
+
legacy = "baseline"
|
|
110
|
+
complexity_regressions = "warn"
|
|
111
|
+
|
|
112
|
+
[[tool.shockharness.overrides]]
|
|
113
|
+
paths = ["src/shockharness/constants.py"]
|
|
114
|
+
disable = ["MAGIC001"]
|
|
115
|
+
reason = "Single designated module holding every numeric constant the tool needs (exit codes, schema version, caps)"
|
|
116
|
+
|
|
117
|
+
[[tool.shockharness.overrides]]
|
|
118
|
+
paths = ["tests/**"]
|
|
119
|
+
disable = ["MAGIC001", "DEF001"]
|
|
120
|
+
reason = "Test code encodes literal expected outputs and pytest fixture signatures"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to ShockHarness. Format follows Keep a Changelog; versions
|
|
4
|
+
follow Semantic Versioning. The report `schema_version` is tracked separately.
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Milestone A: package skeleton, MIT license, configuration schema with full
|
|
10
|
+
shape and semantic validation, precedence (`--config` > `.shockharness.toml` >
|
|
11
|
+
`pyproject.toml` > bundled defaults), rule registry with every specified rule
|
|
12
|
+
ID, diagnostic and report models, `scan`, `config validate`, `config effective`,
|
|
13
|
+
`rules list`, `rules explain`, `init`, exit-code contract, GitHub Actions matrix.
|
|
14
|
+
- Milestone B: Python (`ast`) and JavaScript (tree-sitter) adapters with
|
|
15
|
+
byte-to-character column mapping, explicit parse-failure propagation,
|
|
16
|
+
`MAGIC001` (value-based 0/1, folded unary sign, annotations flagged),
|
|
17
|
+
`SUPP001`/`SUPP002` inline suppression checks, per-path overrides,
|
|
18
|
+
configuration-file exemption, JSON report (`schema_version` 1) and text output.
|
|
19
|
+
- Milestone C: catch facts in the IR (exception identities with
|
|
20
|
+
exact/approximate/unresolved confidence, handler body classification,
|
|
21
|
+
try-body operation attribution) for Python `except`/`except*` and JavaScript
|
|
22
|
+
`catch`, `.catch(handler)`, `.then(ok, handler)`; `EXC001` forbidden catch
|
|
23
|
+
(boundary whitelist, fail-closed on bare/unresolved identities, JavaScript
|
|
24
|
+
instanceof-guard convention), `EXC002` suppressed or converted exception,
|
|
25
|
+
`EXC003` broad try scope; `rules.exceptions.transparent_rethrow` (default
|
|
26
|
+
false); boundary `exception` names validated as dotted class names; every
|
|
27
|
+
boundary and `transparent_rethrow` reported under `config.deviations`.
|
|
28
|
+
- Milestone D: default facts in the IR (`DefaultFact`: family, kind, subject,
|
|
29
|
+
fallback, constant-shapedness, syntactic context, confidence) extracted by
|
|
30
|
+
`ir/python_defaults.py` and `ir/javascript_defaults.py`; `DEF001` parameter
|
|
31
|
+
and dataclass-field defaults, `DEF002` mapping/attribute/property fallbacks
|
|
32
|
+
(plus the implicit `None` of `.get(key)` as a review finding), `DEF003`
|
|
33
|
+
truthiness fallbacks (review by default; Boolean logic excluded), `DEF004`
|
|
34
|
+
nullish fallbacks, `DEF005` exception-to-default (literal returns and
|
|
35
|
+
one-step static traces to module constants and one-return helpers), `DEF006`
|
|
36
|
+
configuration fallbacks (`os.getenv`/`os.environ`/`process.env` in every
|
|
37
|
+
spelling); new `rules.defaults` keys `dataclass_fields`, `nullish_fallbacks`,
|
|
38
|
+
`configuration_fallbacks` (all `forbid`); loosened modes reported under
|
|
39
|
+
`config.deviations`; 13 golden fixtures and 25 unit tests; `safe_autofix` is
|
|
40
|
+
false for every finding.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- `EXC002` no longer reports the `return_fallback` classification while `DEF005`
|
|
44
|
+
is active: the handler is reported once, as `DEF005`, with the EXC002
|
|
45
|
+
classification named in the message. Disabling `DEF005` restores the EXC002
|
|
46
|
+
finding. Golden expectations for the eleven affected handlers changed rule ID
|
|
47
|
+
only.
|
|
48
|
+
- The tool's own code was brought under `DEF002`/`DEF004`: three `setdefault`
|
|
49
|
+
accumulators became explicit membership checks, the report's
|
|
50
|
+
`discovery`/`explicit_targets_excluded` fields are built with an explicit
|
|
51
|
+
branch, and nineteen `x if x is not None else v` expressions on parser fields
|
|
52
|
+
became either explicit branches (genuinely optional values: import aliases,
|
|
53
|
+
`from . import`, source segments) or assertions of grammar invariants (fields
|
|
54
|
+
tree-sitter and `ast` always populate). A Python `SyntaxError` position is now
|
|
55
|
+
recorded only when the interpreter reports both a line and a column; before,
|
|
56
|
+
a missing column was silently replaced by column 1.
|
|
57
|
+
- The arities that distinguish `getattr(o, a, v)` and `m.get(k, v)` from plain
|
|
58
|
+
lookups are named in `constants.py`.
|
|
59
|
+
- The tool's own boundaries were narrowed to satisfy `EXC003`: config decoding
|
|
60
|
+
and TOML parsing are two `try` blocks, analyzed-file decoding is bound before
|
|
61
|
+
the result is returned, and comment tokenization materializes the token list
|
|
62
|
+
in the one protected call. Behaviour is unchanged.
|
|
63
|
+
|
|
64
|
+
### Dependencies
|
|
65
|
+
- `tree-sitter` 0.25.x (MIT) and `tree-sitter-javascript` 0.25.x (MIT). Pinned
|
|
66
|
+
as a tested pair; language ABI 15. `tree-sitter` 0.26.0 is excluded: with
|
|
67
|
+
CPython 3.14 it segfaults when walking trees nested deeper than about 200
|
|
68
|
+
levels (CPython 3.13 with 0.26.0 and CPython 3.14 with 0.25.2 are both fine).
|
|
69
|
+
The 300-deep nesting test in `tests/unit/test_javascript_adapter.py` guards this.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Lahore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: shockharness
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Strict static-analysis quality gate for Python and JavaScript: no silent failures, no implicit defaults, no magic numbers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/evolvingstuff/ShockHarness
|
|
6
|
+
Project-URL: Repository, https://github.com/evolvingstuff/ShockHarness
|
|
7
|
+
Project-URL: Issues, https://github.com/evolvingstuff/ShockHarness/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/evolvingstuff/ShockHarness/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Thomas Lahore
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ci,exceptions,linter,magic-numbers,quality-gate,static-analysis
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: JavaScript
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: tree-sitter-javascript<0.26,>=0.25
|
|
27
|
+
Requires-Dist: tree-sitter<0.26,>=0.25.1
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
30
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# ShockHarness
|
|
36
|
+
|
|
37
|
+
A strict, deterministic static-analysis quality gate for Python and JavaScript.
|
|
38
|
+
Its one idea: unexpected failures, missing information, and unexplained
|
|
39
|
+
decisions must not silently become apparently valid program behavior.
|
|
40
|
+
|
|
41
|
+
Three default-on hard policies:
|
|
42
|
+
|
|
43
|
+
1. **Exception transparency.** No catch except at narrow, explicitly approved
|
|
44
|
+
external boundaries. No catch-and-continue, no success-shaped fallbacks.
|
|
45
|
+
2. **No implicit defaults.** No fallback values for missing required data, no
|
|
46
|
+
function-parameter defaults in strict mode.
|
|
47
|
+
3. **No magic numbers.** Every numeric literal in executable code must be
|
|
48
|
+
exactly `0` or `1`. `range(2)` is a violation. The tool invents no exemptions.
|
|
49
|
+
|
|
50
|
+
Plus a separate, advisory complexity family (cyclomatic, cognitive, nesting,
|
|
51
|
+
NPath, call graph, churn). Hard violations are never averaged into a score.
|
|
52
|
+
|
|
53
|
+
ShockHarness complements Ruff and ESLint; it is not a formatter or a general
|
|
54
|
+
linter. It runs offline, needs no LLM, and never uploads source.
|
|
55
|
+
|
|
56
|
+
## Status
|
|
57
|
+
|
|
58
|
+
Pre-release. Milestones A (skeleton and contracts), B (parsers and
|
|
59
|
+
`MAGIC001`), C (exception rules `EXC001`-`EXC003`) and D (default and fallback
|
|
60
|
+
rules `DEF001`-`DEF006`) are implemented. `MAGIC002`, complexity, baselines,
|
|
61
|
+
SARIF, and agent skills are registered but **not yet implemented**; every
|
|
62
|
+
report lists them under `rules_not_implemented` so a clean run never implies
|
|
63
|
+
coverage the tool does not have. See `STATUS.md` for exact evidence.
|
|
64
|
+
|
|
65
|
+
Implemented rules:
|
|
66
|
+
|
|
67
|
+
| ID | What it reports | Default |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `MAGIC001` | numeric literal other than `0`/`1` in executable code | error |
|
|
70
|
+
| `EXC001` | catch outside an approved boundary (bare, broad, unresolved, untyped) | error |
|
|
71
|
+
| `EXC002` | handler that swallows, only logs, or replaces the exception without its cause | error |
|
|
72
|
+
| `EXC003` | approved `try` wrapping more than the one sanctioned operation | error |
|
|
73
|
+
| `DEF001` | parameter default, dataclass field default / `default_factory` | error |
|
|
74
|
+
| `DEF002` | `m.get(k, v)`, `getattr(o, a, v)`, `o?.x ?? v`, `const {x = v} = o`; `.get(k)` implicit `None` as review | error |
|
|
75
|
+
| `DEF003` | `x or v`, `x || v` outside Boolean logic | review |
|
|
76
|
+
| `DEF004` | `x ?? v`, `x if x is not None else v` | error |
|
|
77
|
+
| `DEF005` | handler that returns a default (literal, module constant, one-return helper) | error |
|
|
78
|
+
| `DEF006` | `os.getenv("X", v)`, `process.env.X || v`, `const { X = v } = process.env` | error |
|
|
79
|
+
| `SUPP001` / `SUPP002` | malformed / unused suppression comment | error / warning |
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
Requires Python 3.11+. Runtime dependencies are `tree-sitter` and
|
|
84
|
+
`tree-sitter-javascript` only.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install shockharness
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
From a checkout:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv pip install -e ".[dev]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Use
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
shockharness init # writes .shockharness.toml (strict profile)
|
|
100
|
+
shockharness config validate # exit 0 if the configuration is valid
|
|
101
|
+
shockharness config effective # merged config with per-key origins
|
|
102
|
+
shockharness scan src/ # human-readable report, exit = gate status
|
|
103
|
+
shockharness scan src/ --format json --output report.json
|
|
104
|
+
shockharness rules list
|
|
105
|
+
shockharness rules explain MAGIC001
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Exit codes: `0` gates pass, `1` policy violations, `2` invalid configuration or
|
|
109
|
+
invocation, `3` internal error or (with `parse_errors = "fail"`) a file that
|
|
110
|
+
could not be parsed. A report is still produced when the gate fails.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
src/client.py:12:11: MAGIC001 error: Numeric literal 30 has no permitted justification in executable code [timeout = 30]
|
|
116
|
+
src/client.py:15:24: DEF001 error: parameter 'port' of connect() has a default value: 5432 [def connect(host, port=5432):]
|
|
117
|
+
src/client.py:18:15: DEF006 error: environment variable "RETRIES" falls back to "3"; os.getenv(...) substitutes a fallback when environment variable "RETRIES" is unset (getenv) [retries = os.getenv("RETRIES", "3")]
|
|
118
|
+
src/client.py:24:5: DEF005 error: except handler converts the exception into a fallback value: return None (EXC002 classification return_fallback; the handler is reported once, here) [except OSError:]
|
|
119
|
+
src/client.py:24:5: EXC001 error: except handler of 'OSError' is forbidden: catch of 'OSError' is not approved by any boundary for this file [except OSError:]
|
|
120
|
+
|
|
121
|
+
shockharness 0.1.0: 1 file(s), 1 analyzed, 0 failed; 5 finding(s), 0 suppressed
|
|
122
|
+
by severity: error=5
|
|
123
|
+
rules applied: MAGIC001, EXC001, EXC002, EXC003, DEF001, DEF002, DEF003, DEF004, DEF005, DEF006, SUPP001, SUPP002
|
|
124
|
+
rules not implemented (not analyzed): MAGIC002, CPLX001, CPLX002, CPLX003, CPLX004, CPLX005
|
|
125
|
+
gate: FAIL (exit 1)
|
|
126
|
+
5 hard violation(s) and gates.new_hard_violations = "fail"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Configuration
|
|
130
|
+
|
|
131
|
+
Either `.shockharness.toml` or a `[tool.shockharness]` table in
|
|
132
|
+
`pyproject.toml` (the dedicated file wins and the shadowed table is reported).
|
|
133
|
+
Unknown keys, bad types, unknown rule IDs, and invalid globs are errors, never
|
|
134
|
+
ignored. Every rule can be disabled; every disable is listed in the report.
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
[tool.shockharness]
|
|
138
|
+
profile = "strict"
|
|
139
|
+
include = ["src/**/*.py", "src/**/*.js"]
|
|
140
|
+
exclude = ["**/node_modules/**", "**/.venv/**"]
|
|
141
|
+
parse_errors = "fail"
|
|
142
|
+
|
|
143
|
+
[tool.shockharness.rules.magic_numbers]
|
|
144
|
+
enabled = true
|
|
145
|
+
allowed_values = [0, 1]
|
|
146
|
+
allow_unary_negative_one = false
|
|
147
|
+
|
|
148
|
+
[[tool.shockharness.overrides]]
|
|
149
|
+
paths = ["tests/**"]
|
|
150
|
+
disable = ["MAGIC001"]
|
|
151
|
+
reason = "Fixtures encode literal expected outputs"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Inline suppressions need an exact rule ID and a reason, on the same line:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
status = 404 # shockharness: ignore[MAGIC001] reason="RFC 9110 section 15.5.5"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A malformed suppression is a `SUPP001` error; one that silences nothing is a
|
|
161
|
+
`SUPP002` warning. `--no-inline-suppressions` disables them for centrally
|
|
162
|
+
governed policies.
|
|
163
|
+
|
|
164
|
+
See `docs/` for configuration, rules, architecture, and limitations.
|
|
165
|
+
|
|
166
|
+
## Library
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
from shockharness import analyze_paths
|
|
171
|
+
from shockharness.analysis import AnalysisOptions
|
|
172
|
+
from shockharness.config import load_config
|
|
173
|
+
|
|
174
|
+
config = load_config(Path("."), None)
|
|
175
|
+
report = analyze_paths([Path("src")], Path("."), config, AnalysisOptions(inline_suppressions=True))
|
|
176
|
+
print(report.gate.status, len(report.findings))
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
uv pip install -e ".[dev]"
|
|
183
|
+
pytest -q
|
|
184
|
+
ruff check src tests && ruff format --check src tests
|
|
185
|
+
shockharness scan src tests # the tool gates its own source
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
ShockHarness runs on itself as a hard CI gate. Its one `MAGIC001` exemption is
|
|
189
|
+
`src/shockharness/constants.py`; its declared exception boundaries are listed in
|
|
190
|
+
`.shockharness.toml` with reasons and enforced by `EXC001`-`EXC003`: every
|
|
191
|
+
`try` in the tool wraps exactly one external call and converts the failure into
|
|
192
|
+
a typed diagnostic. `DEF001`-`DEF006` hold too: the tool has no parameter
|
|
193
|
+
defaults outside tests, no mapping fallbacks, and no `or`/`is None` defaults.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# ShockHarness
|
|
2
|
+
|
|
3
|
+
A strict, deterministic static-analysis quality gate for Python and JavaScript.
|
|
4
|
+
Its one idea: unexpected failures, missing information, and unexplained
|
|
5
|
+
decisions must not silently become apparently valid program behavior.
|
|
6
|
+
|
|
7
|
+
Three default-on hard policies:
|
|
8
|
+
|
|
9
|
+
1. **Exception transparency.** No catch except at narrow, explicitly approved
|
|
10
|
+
external boundaries. No catch-and-continue, no success-shaped fallbacks.
|
|
11
|
+
2. **No implicit defaults.** No fallback values for missing required data, no
|
|
12
|
+
function-parameter defaults in strict mode.
|
|
13
|
+
3. **No magic numbers.** Every numeric literal in executable code must be
|
|
14
|
+
exactly `0` or `1`. `range(2)` is a violation. The tool invents no exemptions.
|
|
15
|
+
|
|
16
|
+
Plus a separate, advisory complexity family (cyclomatic, cognitive, nesting,
|
|
17
|
+
NPath, call graph, churn). Hard violations are never averaged into a score.
|
|
18
|
+
|
|
19
|
+
ShockHarness complements Ruff and ESLint; it is not a formatter or a general
|
|
20
|
+
linter. It runs offline, needs no LLM, and never uploads source.
|
|
21
|
+
|
|
22
|
+
## Status
|
|
23
|
+
|
|
24
|
+
Pre-release. Milestones A (skeleton and contracts), B (parsers and
|
|
25
|
+
`MAGIC001`), C (exception rules `EXC001`-`EXC003`) and D (default and fallback
|
|
26
|
+
rules `DEF001`-`DEF006`) are implemented. `MAGIC002`, complexity, baselines,
|
|
27
|
+
SARIF, and agent skills are registered but **not yet implemented**; every
|
|
28
|
+
report lists them under `rules_not_implemented` so a clean run never implies
|
|
29
|
+
coverage the tool does not have. See `STATUS.md` for exact evidence.
|
|
30
|
+
|
|
31
|
+
Implemented rules:
|
|
32
|
+
|
|
33
|
+
| ID | What it reports | Default |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `MAGIC001` | numeric literal other than `0`/`1` in executable code | error |
|
|
36
|
+
| `EXC001` | catch outside an approved boundary (bare, broad, unresolved, untyped) | error |
|
|
37
|
+
| `EXC002` | handler that swallows, only logs, or replaces the exception without its cause | error |
|
|
38
|
+
| `EXC003` | approved `try` wrapping more than the one sanctioned operation | error |
|
|
39
|
+
| `DEF001` | parameter default, dataclass field default / `default_factory` | error |
|
|
40
|
+
| `DEF002` | `m.get(k, v)`, `getattr(o, a, v)`, `o?.x ?? v`, `const {x = v} = o`; `.get(k)` implicit `None` as review | error |
|
|
41
|
+
| `DEF003` | `x or v`, `x || v` outside Boolean logic | review |
|
|
42
|
+
| `DEF004` | `x ?? v`, `x if x is not None else v` | error |
|
|
43
|
+
| `DEF005` | handler that returns a default (literal, module constant, one-return helper) | error |
|
|
44
|
+
| `DEF006` | `os.getenv("X", v)`, `process.env.X || v`, `const { X = v } = process.env` | error |
|
|
45
|
+
| `SUPP001` / `SUPP002` | malformed / unused suppression comment | error / warning |
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Requires Python 3.11+. Runtime dependencies are `tree-sitter` and
|
|
50
|
+
`tree-sitter-javascript` only.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install shockharness
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From a checkout:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv pip install -e ".[dev]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Use
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
shockharness init # writes .shockharness.toml (strict profile)
|
|
66
|
+
shockharness config validate # exit 0 if the configuration is valid
|
|
67
|
+
shockharness config effective # merged config with per-key origins
|
|
68
|
+
shockharness scan src/ # human-readable report, exit = gate status
|
|
69
|
+
shockharness scan src/ --format json --output report.json
|
|
70
|
+
shockharness rules list
|
|
71
|
+
shockharness rules explain MAGIC001
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Exit codes: `0` gates pass, `1` policy violations, `2` invalid configuration or
|
|
75
|
+
invocation, `3` internal error or (with `parse_errors = "fail"`) a file that
|
|
76
|
+
could not be parsed. A report is still produced when the gate fails.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
src/client.py:12:11: MAGIC001 error: Numeric literal 30 has no permitted justification in executable code [timeout = 30]
|
|
82
|
+
src/client.py:15:24: DEF001 error: parameter 'port' of connect() has a default value: 5432 [def connect(host, port=5432):]
|
|
83
|
+
src/client.py:18:15: DEF006 error: environment variable "RETRIES" falls back to "3"; os.getenv(...) substitutes a fallback when environment variable "RETRIES" is unset (getenv) [retries = os.getenv("RETRIES", "3")]
|
|
84
|
+
src/client.py:24:5: DEF005 error: except handler converts the exception into a fallback value: return None (EXC002 classification return_fallback; the handler is reported once, here) [except OSError:]
|
|
85
|
+
src/client.py:24:5: EXC001 error: except handler of 'OSError' is forbidden: catch of 'OSError' is not approved by any boundary for this file [except OSError:]
|
|
86
|
+
|
|
87
|
+
shockharness 0.1.0: 1 file(s), 1 analyzed, 0 failed; 5 finding(s), 0 suppressed
|
|
88
|
+
by severity: error=5
|
|
89
|
+
rules applied: MAGIC001, EXC001, EXC002, EXC003, DEF001, DEF002, DEF003, DEF004, DEF005, DEF006, SUPP001, SUPP002
|
|
90
|
+
rules not implemented (not analyzed): MAGIC002, CPLX001, CPLX002, CPLX003, CPLX004, CPLX005
|
|
91
|
+
gate: FAIL (exit 1)
|
|
92
|
+
5 hard violation(s) and gates.new_hard_violations = "fail"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Either `.shockharness.toml` or a `[tool.shockharness]` table in
|
|
98
|
+
`pyproject.toml` (the dedicated file wins and the shadowed table is reported).
|
|
99
|
+
Unknown keys, bad types, unknown rule IDs, and invalid globs are errors, never
|
|
100
|
+
ignored. Every rule can be disabled; every disable is listed in the report.
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
[tool.shockharness]
|
|
104
|
+
profile = "strict"
|
|
105
|
+
include = ["src/**/*.py", "src/**/*.js"]
|
|
106
|
+
exclude = ["**/node_modules/**", "**/.venv/**"]
|
|
107
|
+
parse_errors = "fail"
|
|
108
|
+
|
|
109
|
+
[tool.shockharness.rules.magic_numbers]
|
|
110
|
+
enabled = true
|
|
111
|
+
allowed_values = [0, 1]
|
|
112
|
+
allow_unary_negative_one = false
|
|
113
|
+
|
|
114
|
+
[[tool.shockharness.overrides]]
|
|
115
|
+
paths = ["tests/**"]
|
|
116
|
+
disable = ["MAGIC001"]
|
|
117
|
+
reason = "Fixtures encode literal expected outputs"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Inline suppressions need an exact rule ID and a reason, on the same line:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
status = 404 # shockharness: ignore[MAGIC001] reason="RFC 9110 section 15.5.5"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A malformed suppression is a `SUPP001` error; one that silences nothing is a
|
|
127
|
+
`SUPP002` warning. `--no-inline-suppressions` disables them for centrally
|
|
128
|
+
governed policies.
|
|
129
|
+
|
|
130
|
+
See `docs/` for configuration, rules, architecture, and limitations.
|
|
131
|
+
|
|
132
|
+
## Library
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pathlib import Path
|
|
136
|
+
from shockharness import analyze_paths
|
|
137
|
+
from shockharness.analysis import AnalysisOptions
|
|
138
|
+
from shockharness.config import load_config
|
|
139
|
+
|
|
140
|
+
config = load_config(Path("."), None)
|
|
141
|
+
report = analyze_paths([Path("src")], Path("."), config, AnalysisOptions(inline_suppressions=True))
|
|
142
|
+
print(report.gate.status, len(report.findings))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
uv pip install -e ".[dev]"
|
|
149
|
+
pytest -q
|
|
150
|
+
ruff check src tests && ruff format --check src tests
|
|
151
|
+
shockharness scan src tests # the tool gates its own source
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
ShockHarness runs on itself as a hard CI gate. Its one `MAGIC001` exemption is
|
|
155
|
+
`src/shockharness/constants.py`; its declared exception boundaries are listed in
|
|
156
|
+
`.shockharness.toml` with reasons and enforced by `EXC001`-`EXC003`: every
|
|
157
|
+
`try` in the tool wraps exactly one external call and converts the failure into
|
|
158
|
+
a typed diagnostic. `DEF001`-`DEF006` hold too: the tool has no parameter
|
|
159
|
+
defaults outside tests, no mapping fallbacks, and no `or`/`is None` defaults.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT. See `LICENSE`.
|