nouls 0.1.1__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.
@@ -0,0 +1,117 @@
1
+ name: CI and Publish
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+
18
+ - name: Set up Python
19
+ run: uv python install 3.12
20
+
21
+ - name: Install dependencies
22
+ run: uv sync --all-extras --frozen
23
+
24
+ - name: Lint with ruff
25
+ run: uv run ruff check .
26
+
27
+ - name: Format check with ruff
28
+ run: uv run ruff format --check .
29
+
30
+ - name: Type check with basedpyright
31
+ run: uv run basedpyright
32
+
33
+ - name: Run unit tests (parallel)
34
+ run: uv run pytest -m unit -n auto --cov=src/nouls --cov-report=term-missing
35
+
36
+ - name: Run integration tests (sequential)
37
+ run: uv run pytest -m integration --cov=src/nouls --cov-append --cov-report=term-missing
38
+
39
+ publish:
40
+ needs: test
41
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
42
+ runs-on: ubuntu-latest
43
+ environment:
44
+ name: pypi
45
+ url: https://pypi.org/p/nouls
46
+ permissions:
47
+ id-token: write
48
+ contents: write
49
+
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ with:
53
+ fetch-depth: 0
54
+ token: ${{ secrets.GITHUB_TOKEN }}
55
+ - name: Install uv
56
+ uses: astral-sh/setup-uv@v5
57
+
58
+ - name: Set up Python
59
+ run: uv python install 3.12
60
+
61
+ - name: Install dependencies
62
+ run: uv sync --all-extras --frozen
63
+
64
+ - name: Configure git
65
+ run: |
66
+ git config user.name "github-actions[bot]"
67
+ git config user.email "github-actions[bot]@users.noreply.github.com"
68
+
69
+ - name: Bump version
70
+ run: uv run bump-my-version bump patch
71
+
72
+ - name: Push version bump
73
+ run: git push --follow-tags
74
+
75
+ - name: Build package
76
+ run: uv build
77
+
78
+ - name: Publish to PyPI
79
+ uses: pypa/gh-action-pypi-publish@release/v1
80
+
81
+ docs:
82
+ needs: test
83
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
84
+ runs-on: ubuntu-latest
85
+ permissions:
86
+ contents: read
87
+ pages: write
88
+ id-token: write
89
+ environment:
90
+ name: github-pages
91
+ url: ${{ steps.deployment.outputs.page_url }}
92
+ steps:
93
+ - uses: actions/checkout@v4
94
+
95
+ - name: Install uv
96
+ uses: astral-sh/setup-uv@v5
97
+
98
+ - name: Set up Python
99
+ run: uv python install 3.12
100
+
101
+ - name: Install dependencies
102
+ run: uv sync --all-extras --frozen
103
+
104
+ - name: Build docs
105
+ run: uv run zensical build
106
+
107
+ - name: Setup Pages
108
+ uses: actions/configure-pages@v5
109
+
110
+ - name: Upload artifact
111
+ uses: actions/upload-pages-artifact@v3
112
+ with:
113
+ path: site
114
+
115
+ - name: Deploy to GitHub Pages
116
+ id: deployment
117
+ uses: actions/deploy-pages@v4
nouls-0.1.1/.gitignore ADDED
@@ -0,0 +1,59 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Virtual environments
25
+ .venv
26
+ venv/
27
+ ENV/
28
+ env/
29
+
30
+ # IDEs
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ *.swo
35
+ *~
36
+
37
+ # Testing
38
+ .pytest_cache/
39
+ .coverage
40
+ htmlcov/
41
+ .tox/
42
+
43
+ # Type checking
44
+ .mypy_cache/
45
+ .pytype/
46
+ .pyre/
47
+ .basedpyright/
48
+
49
+ # Ruff
50
+ .ruff_cache/
51
+
52
+ # OS
53
+ .DS_Store
54
+ Thumbs.db
55
+
56
+ # Project specific
57
+ *.log
58
+ .env
59
+ .env.local
@@ -0,0 +1,94 @@
1
+ default_language_version:
2
+ python: python3
3
+ default_stages: [pre-commit]
4
+ repos:
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: v6.0.0
7
+ hooks:
8
+ - id: trailing-whitespace
9
+ args: [--markdown-linebreak-ext=md]
10
+ - id: end-of-file-fixer
11
+ - id: mixed-line-ending
12
+ args: [--fix=lf]
13
+ - id: check-yaml
14
+ args: [--unsafe]
15
+ - id: check-json
16
+ - id: check-toml
17
+ - id: check-merge-conflict
18
+ - id: check-case-conflict
19
+ - id: check-ast
20
+ - id: check-docstring-first
21
+ - id: debug-statements
22
+ - id: name-tests-test
23
+ args: [--pytest-test-first]
24
+ - id: detect-private-key
25
+ - id: check-added-large-files
26
+ args: [--maxkb=1000]
27
+
28
+ - repo: https://github.com/igorshubovych/markdownlint-cli
29
+ rev: v0.47.0
30
+ hooks:
31
+ - id: markdownlint
32
+ args: [--fix, --disable, MD013, MD024, MD033, MD036, MD040, MD041, MD060, --]
33
+
34
+ - repo: https://github.com/commitizen-tools/commitizen
35
+ rev: v4.12.1
36
+ hooks:
37
+ - id: commitizen
38
+ stages: [commit-msg]
39
+
40
+ - repo: https://github.com/benomahony/nasa-lsp
41
+ rev: v0.1.7
42
+ hooks:
43
+ - id: nasa-lsp
44
+ exclude: ^tests/
45
+
46
+ - repo: local
47
+ hooks:
48
+ - id: ruff-format
49
+ name: ruff-format
50
+ entry: uv run ruff format
51
+ language: system
52
+ types_or: [python, pyi]
53
+ require_serial: true
54
+ - id: ruff
55
+ name: ruff
56
+ entry: uv run ruff check --fix --exit-non-zero-on-fix
57
+ language: system
58
+ types_or: [python, pyi]
59
+ require_serial: true
60
+ - id: basedpyright
61
+ name: basedpyright
62
+ entry: uv run basedpyright
63
+ language: system
64
+ types: [python]
65
+ pass_filenames: true
66
+ require_serial: true
67
+ - id: vulture
68
+ name: vulture
69
+ entry: uv run vulture src/nouls/ --min-confidence 80
70
+ language: system
71
+ types: [python]
72
+ files: ^src/nouls/
73
+ pass_filenames: false
74
+ require_serial: true
75
+ - id: bandit
76
+ name: bandit
77
+ entry: uv run bandit -c pyproject.toml -r src/nouls/
78
+ language: system
79
+ files: ^src/nouls/
80
+ exclude: ^tests/
81
+ pass_filenames: false
82
+ - id: cliqa
83
+ name: cliqa
84
+ entry: uv run cliqa analyze nouls
85
+ language: system
86
+ files: ^(pyproject\.toml|src/.*\.py)$
87
+ pass_filenames: false
88
+ require_serial: true
89
+ - id: deptry
90
+ name: deptry
91
+ entry: uv run deptry . --extend-exclude "\.venv|tests"
92
+ language: system
93
+ pass_filenames: false
94
+ require_serial: true
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: nouls
3
+ description: Help users work with nouls, the semantic linter and language server built on TypeSafe yes/no questions. Use when the user asks about nouls rules, configuration, thresholds, labels or stats, or wants to add a rule or language.
4
+ ---
5
+
6
+ # nouls Skill
7
+
8
+ This skill helps you work with nouls.
9
+
10
+ ## When to Use This Skill
11
+
12
+ Use this skill when:
13
+
14
+ - User asks about nouls features or capabilities
15
+ - User wants to add or tune a rule in `nouls.yaml`
16
+ - User wants to add a language through tree-sitter node types
17
+ - User needs help with the nouls CLI or language server
18
+ - User wants to label findings or read `nouls stats`
19
+
20
+ ## Project Information
21
+
22
+ - **Description**: Semantic linter and language server that asks TypeSafe yes/no questions about every function
23
+ - **Author**: Ben O'Mahony
24
+ - **Documentation**: See docs/index.md for full documentation
25
+ - **Source**: src/nouls/
26
+
27
+ ## Quick Reference
28
+
29
+ ### CLI Usage
30
+
31
+ ```bash
32
+ nouls check src/
33
+ nouls rules
34
+ nouls label src/app.py 42 unit_mismatch false
35
+ nouls review unit_mismatch
36
+ nouls stats thresholds --ask
37
+ nouls serve
38
+ ```
39
+
40
+ ### Rule shape
41
+
42
+ ```yaml
43
+ rules:
44
+ ledger_sign:
45
+ question: Does the function add a debit where the domain requires subtracting it, or the reverse?
46
+ message: Debit and credit signs look inverted
47
+ severity: error
48
+ threshold: 0.9
49
+ languages: [python]
50
+ files: ["*/ledger/*.py"]
51
+ ```
52
+
53
+ Questions must be a single yes/no judgement about the function in the `function` state field. Never ask for a number.
54
+
55
+ ## Resources
56
+
57
+ - Check docs/index.md for comprehensive documentation
58
+ - Check llms.txt for LLM-friendly documentation summary
59
+ - Check src/nouls/defaults.yaml for built in languages and rules
60
+ - Check src/nouls/ for implementation details
nouls-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.5
2
+ Name: nouls
3
+ Version: 0.1.1
4
+ Summary: Semantic linter and language server that asks TypeSafe yes/no questions about every function
5
+ Author-email: Ben O'Mahony <bomarni@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: cyclopts>=4.25.2
9
+ Requires-Dist: lsprotocol>=2025.0.0
10
+ Requires-Dist: pydantic>=2.13.5
11
+ Requires-Dist: pygls>=2.1.1
12
+ Requires-Dist: pyyaml>=6.0.3
13
+ Requires-Dist: rich>=15.0.0
14
+ Requires-Dist: tree-sitter-language-pack>=1.20.0
15
+ Requires-Dist: tree-sitter>=0.26.0
16
+ Requires-Dist: typesafe-sdk>=0.6.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: bandit>=1.7.0; extra == 'dev'
19
+ Requires-Dist: basedpyright>=1.10.0; extra == 'dev'
20
+ Requires-Dist: bump-my-version>=0.28.0; extra == 'dev'
21
+ Requires-Dist: cliqa>=0.1.0; extra == 'dev'
22
+ Requires-Dist: deptry>=0.12.0; extra == 'dev'
23
+ Requires-Dist: prek>=0.1.0; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == 'dev'
25
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
26
+ Requires-Dist: pytest-examples>=0.0.10; extra == 'dev'
27
+ Requires-Dist: pytest-testmon>=2.0.0; extra == 'dev'
28
+ Requires-Dist: pytest-xdist>=3.0.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.3.0; extra == 'dev'
31
+ Requires-Dist: vulture>=2.11; extra == 'dev'
32
+ Requires-Dist: zensical>=0.0.10; extra == 'dev'
33
+ Provides-Extra: mcp
34
+ Requires-Dist: mcp<2,>=1.2.0; extra == 'mcp'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # nouls
38
+
39
+ A semantic linter and language server for the problems deterministic tools cannot see.
40
+
41
+ nouls splits every file into functions with tree-sitter and asks [TypeSafe](https://docs.typesafe.ai) a batch of yes/no questions about each one. A rule fires when the probability of yes reaches its threshold. Every question is scored independently, so one call per function answers every rule, and unchanged functions are never asked again.
42
+
43
+ The default rules target intent, not syntax:
44
+
45
+ | Rule | Severity |
46
+ | --- | --- |
47
+ | `name_behaviour_mismatch` | warning |
48
+ | `docstring_drift` | warning |
49
+ | `query_with_side_effect` | warning |
50
+ | `partial_failure` | warning |
51
+ | `check_then_act` | warning |
52
+ | `missing_authorisation` | warning |
53
+ | `non_idempotent_retry` | error |
54
+ | `unit_mismatch` | error |
55
+ | `boundary_error` | error |
56
+ | `misleading_error` | info |
57
+ | `mixed_abstraction` | info |
58
+
59
+ Anything ruff, a type checker or a security scanner already catches is deliberately out of scope.
60
+
61
+ Test files also get rules drawn from Kent Beck's [Test Desiderata](https://testdesiderata.com). Each asks whether a test violates one property.
62
+
63
+ | Rule | Desideratum | Severity |
64
+ | --- | --- | --- |
65
+ | `test_not_isolated` | Isolated | warning |
66
+ | `test_not_composable` | Composable | info |
67
+ | `test_nondeterministic` | Deterministic | error |
68
+ | `test_slow` | Fast | warning |
69
+ | `test_hard_to_write` | Writable | info |
70
+ | `test_unreadable` | Readable | info |
71
+ | `test_not_behavioural` | Behavioural | error |
72
+ | `test_structure_sensitive` | Structure insensitive | warning |
73
+ | `test_not_automated` | Automated | error |
74
+ | `test_not_specific` | Specific | warning |
75
+ | `test_not_predictive` | Predictive | warning |
76
+ | `test_not_inspiring` | Inspiring | warning |
77
+
78
+ They only run on files matching the default test patterns, such as `test_*.py`, `*_test.go`, `*.spec.ts`, `*Test.java` and `*/tests/*.rs`. Rust unit tests inside `mod tests` in a source file are not matched.
79
+
80
+ ## Installation
81
+
82
+ ```bash
83
+ uv tool install nouls --index typesafe=https://pypi.typesafe.ai/
84
+ export TYPESAFE_API_KEY="your-api-key"
85
+ ```
86
+
87
+ ## Usage
88
+
89
+ ```bash
90
+ nouls check src/
91
+ nouls rules
92
+ nouls serve
93
+ nouls label src/billing.py 42 unit_mismatch false
94
+ nouls review unit_mismatch
95
+ nouls stats rules
96
+ nouls stats hotspots
97
+ nouls stats cost
98
+ nouls stats thresholds unit_mismatch --ask
99
+ ```
100
+
101
+ `check` prints `path:line:column: severity [rule] message (probability)` and exits 1 when an error level rule fires. Set `show_probability: false` to drop the probability from both the command line and editor diagnostics.
102
+
103
+ ## Store
104
+
105
+ Every answer lives in one SQLite file, `~/.cache/nouls/nouls.db` by default, shared by the language server, the command line and every repo on the machine. It runs in WAL mode, so several processes can use it at once.
106
+
107
+ - `answers` caches one probability per model, question and function source. Rewording a rule only re-asks that rule. Editing a function only re-asks that function.
108
+ - `observations` holds the latest answer for every rule on every function in every file checked.
109
+ - `labels` holds your verdicts. A finding labelled `false` is no longer reported for that function.
110
+ - `runs` records questions asked, cache hits and tokens for every file checked.
111
+
112
+ ## Labels and thresholds
113
+
114
+ Label findings from the editor with the `not a problem` and `confirm finding` code actions, from the command line with `nouls label`, or in bulk with `nouls review`. `review` shows unlabelled functions for one rule, sampled evenly across probability bands, so the labels cover misses as well as hits.
115
+
116
+ `nouls stats thresholds` compares your labels with the current wording of each question and prints precision and recall at a range of thresholds. Labels belong to the rule, not the wording, so you can rewrite a question, run `nouls stats thresholds --ask` to re-ask it for every labelled function, and compare.
117
+
118
+ `nouls stats rules` shows how often each rule fires, how many answers sit in the ambiguous 0.35 to 0.65 band, and a histogram of probabilities. A well posed question piles up at both ends.
119
+
120
+ For anything else, attach the store read only from DuckDB:
121
+
122
+ ```sql
123
+ ATTACH '~/.cache/nouls/nouls.db' AS nouls (TYPE sqlite, READ_ONLY);
124
+ SELECT rule, quantile_cont(probability, [0.1, 0.5, 0.9]) FROM nouls.observations GROUP BY rule;
125
+ ```
126
+
127
+ ## Configuration
128
+
129
+ nouls merges its built in defaults with the first `nouls.yaml`, `nouls.yml`, `.nouls.yaml` or `.nouls.yml` found walking up from the target, or the file passed with `--config`. Maps merge key by key, so you only write what changes.
130
+
131
+ ```yaml
132
+ model: jev-latest
133
+ threshold: 0.8
134
+ concurrency: 8
135
+ debounce_ms: 1000
136
+ show_probability: true
137
+ lint_on: change
138
+ store: ~/.cache/nouls/nouls.db
139
+ exclude: [".*", node_modules, __pycache__, target, dist, build, venv]
140
+
141
+ languages:
142
+ kotlin:
143
+ grammar: kotlin
144
+ extensions: [.kt, .kts]
145
+ units: [function_declaration]
146
+
147
+ rules:
148
+ mixed_abstraction:
149
+ enabled: false
150
+ unit_mismatch:
151
+ threshold: 0.9
152
+ ledger_sign:
153
+ question: Does the function add a debit where the domain requires subtracting it, or the reverse?
154
+ message: Debit and credit signs look inverted
155
+ severity: error
156
+ languages: [python, go]
157
+ test_not_isolated:
158
+ files: ["*_test.py", "*/integration/*.py"]
159
+ ```
160
+
161
+ `lint_on: save` stops the language server checking while you type. `store` moves the SQLite file.
162
+
163
+ `files` limits a rule to file names or paths matching any of its globs. Setting it replaces the default list.
164
+
165
+ Languages are pure configuration. `grammar` is any name from [tree-sitter-language-pack](https://github.com/Goldziher/tree-sitter-language-pack), and `units` lists the node types to send as individual questions. The diagnostic sits on the node's `name` field, or its first line when it has none.
166
+
167
+ Every question is answered against this state:
168
+
169
+ ```json
170
+ {"language": "python", "function": "<source of the unit>"}
171
+ ```
172
+
173
+ Write questions as a single yes/no judgement about that function.
174
+
175
+ ## Neovim
176
+
177
+ ```lua
178
+ vim.lsp.config("nouls", {
179
+ cmd = { "nouls", "serve" },
180
+ filetypes = { "python", "javascript", "typescript", "typescriptreact", "go", "rust", "java", "c", "cpp", "lua", "ruby" },
181
+ root_markers = { "nouls.yaml", ".nouls.yaml", ".git" },
182
+ })
183
+ vim.lsp.enable("nouls")
184
+ ```
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ uv sync --all-extras
190
+ uv run prek install
191
+ uv run prek run --all-files
192
+ uv run pytest
193
+ ```
194
+
195
+ ## AI Integration
196
+
197
+ An MCP server for the docs lives in `src/nouls/mcp_server.py`:
198
+
199
+ ```bash
200
+ claude mcp add nouls --transport stdio uv run --with mcp python src/nouls/mcp_server.py
201
+ ```
202
+
203
+ A Claude Code Agent Skill lives in `.skills/nouls`:
204
+
205
+ ```bash
206
+ claude skill add .skills/nouls
207
+ ```