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.
- nouls-0.1.1/.github/workflows/ci.yml +117 -0
- nouls-0.1.1/.gitignore +59 -0
- nouls-0.1.1/.pre-commit-config.yaml +94 -0
- nouls-0.1.1/.python-version +1 -0
- nouls-0.1.1/.skills/nouls/SKILL.md +60 -0
- nouls-0.1.1/PKG-INFO +207 -0
- nouls-0.1.1/README.md +171 -0
- nouls-0.1.1/docs/index.md +171 -0
- nouls-0.1.1/llms.txt +22 -0
- nouls-0.1.1/pyproject.toml +139 -0
- nouls-0.1.1/src/nouls/__init__.py +1 -0
- nouls-0.1.1/src/nouls/analyser.py +172 -0
- nouls-0.1.1/src/nouls/cli.py +259 -0
- nouls-0.1.1/src/nouls/config.py +115 -0
- nouls-0.1.1/src/nouls/defaults.yaml +176 -0
- nouls-0.1.1/src/nouls/mcp_server.py +76 -0
- nouls-0.1.1/src/nouls/py.typed +0 -0
- nouls-0.1.1/src/nouls/server.py +163 -0
- nouls-0.1.1/src/nouls/stats.py +272 -0
- nouls-0.1.1/src/nouls/store.py +202 -0
- nouls-0.1.1/src/nouls/units.py +107 -0
- nouls-0.1.1/tests/__init__.py +0 -0
- nouls-0.1.1/tests/conftest.py +94 -0
- nouls-0.1.1/tests/test_cli.py +221 -0
- nouls-0.1.1/tests/test_integration.py +65 -0
- nouls-0.1.1/tests/test_nouls.py +174 -0
- nouls-0.1.1/tests/test_server.py +153 -0
- nouls-0.1.1/tests/test_stats.py +101 -0
- nouls-0.1.1/uv.lock +3111 -0
- nouls-0.1.1/zensical.toml +12 -0
|
@@ -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
|
+
```
|