habit-hooks 1.0.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.
- habit_hooks-1.0.0/.github/dependabot.yml +22 -0
- habit_hooks-1.0.0/.github/workflows/ci.yml +27 -0
- habit_hooks-1.0.0/.github/workflows/dependabot-auto-merge.yml +19 -0
- habit_hooks-1.0.0/.github/workflows/release.yml +48 -0
- habit_hooks-1.0.0/.gitignore +20 -0
- habit_hooks-1.0.0/.jscpd.json +6 -0
- habit_hooks-1.0.0/CHANGELOG.md +68 -0
- habit_hooks-1.0.0/CLAUDE.md +165 -0
- habit_hooks-1.0.0/LICENSE.md +21 -0
- habit_hooks-1.0.0/PKG-INFO +20 -0
- habit_hooks-1.0.0/README.md +370 -0
- habit_hooks-1.0.0/conftest.py +57 -0
- habit_hooks-1.0.0/docs/DECISIONS.md +134 -0
- habit_hooks-1.0.0/docs/architecture.md +226 -0
- habit_hooks-1.0.0/docs/authoring-plugins.spec.md +516 -0
- habit_hooks-1.0.0/docs/checklist.md +149 -0
- habit_hooks-1.0.0/docs/config.md +271 -0
- habit_hooks-1.0.0/docs/executable_spec.md +92 -0
- habit_hooks-1.0.0/docs/guide-includes.spec.md +256 -0
- habit_hooks-1.0.0/docs/habit-hooks.spec.md +133 -0
- habit_hooks-1.0.0/docs/habit-mapper.spec.md +671 -0
- habit_hooks-1.0.0/docs/habit-sensors.spec.md +560 -0
- habit_hooks-1.0.0/docs/habit-snooze.spec.md +271 -0
- habit_hooks-1.0.0/docs/sensor-interface.spec.md +127 -0
- habit_hooks-1.0.0/docs/smell-vocabulary.md +137 -0
- habit_hooks-1.0.0/habit-hooks +13 -0
- habit_hooks-1.0.0/habit-mapper +10 -0
- habit_hooks-1.0.0/habit-sensors +10 -0
- habit_hooks-1.0.0/habit-snooze +10 -0
- habit_hooks-1.0.0/plugins/generic/docs/generic-plugin.spec.md +134 -0
- habit_hooks-1.0.0/plugins/generic/package-lock.json +1440 -0
- habit_hooks-1.0.0/plugins/generic/package.json +8 -0
- habit_hooks-1.0.0/plugins/generic/pyproject.toml +17 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/.jscpd.json +7 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/__init__.py +6 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/config.toml +1 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/deep-nesting.md +14 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/duplicated-code.md +14 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/high-complexity.md +10 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/non-essential-comment.md +8 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/oversized-file.md +10 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/oversized-function.md +10 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/parse-error.md +6 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/swallowed-exception.md +10 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/too-many-parameters.md +10 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/unused-export.md +8 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/warning-comment.md +2 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/jscpd.py +71 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/jscpd.toml +1 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/line-count.py +50 -0
- habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/line-count.toml +2 -0
- habit_hooks-1.0.0/plugins/php/docs/php-plugin.spec.md +120 -0
- habit_hooks-1.0.0/plugins/php/pyproject.toml +17 -0
- habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/__init__.py +1 -0
- habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/config.toml +5 -0
- habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/guides/unused-variable.md +4 -0
- habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/sensors/phpmd.phar +0 -0
- habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/sensors/phpmd.toml +1 -0
- habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/sensors/phpmd_sensor.py +91 -0
- habit_hooks-1.0.0/plugins/python/docs/python-plugin.spec.md +249 -0
- habit_hooks-1.0.0/plugins/python/pyproject.toml +16 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/__init__.py +1 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/config.toml +5 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/guides/high-complexity.md +12 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/guides/swallowed-exception.md +12 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/ruff.toml +6 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/sensors/deptry.toml +1 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/sensors/deptry_sensor.py +70 -0
- habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/sensors/ruff.toml +28 -0
- habit_hooks-1.0.0/plugins/typescript/docs/typescript-plugin.spec.md +178 -0
- habit_hooks-1.0.0/plugins/typescript/package-lock.json +2048 -0
- habit_hooks-1.0.0/plugins/typescript/package.json +13 -0
- habit_hooks-1.0.0/plugins/typescript/pyproject.toml +16 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/__init__.py +1 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/config.toml +4 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/eslint.config.mjs +49 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/guides/non-null-assertion.md +11 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/knip.json +5 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/comment.js +78 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/comment.toml +1 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/eslint.toml +34 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/knip.js +58 -0
- habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/knip.toml +1 -0
- habit_hooks-1.0.0/prompts/build-habit-hooks-overnight.md +315 -0
- habit_hooks-1.0.0/pyproject.toml +76 -0
- habit_hooks-1.0.0/skills/REVIEW.md +24 -0
- habit_hooks-1.0.0/skills/habit-hooks-prompting/SKILL.md +37 -0
- habit_hooks-1.0.0/skills/habit-hooks-review/SKILL.md +108 -0
- habit_hooks-1.0.0/src/habit_hooks/__init__.py +0 -0
- habit_hooks-1.0.0/src/habit_hooks/catalogue.py +34 -0
- habit_hooks-1.0.0/src/habit_hooks/config.py +61 -0
- habit_hooks-1.0.0/src/habit_hooks/guides/clean.md +3 -0
- habit_hooks-1.0.0/src/habit_hooks/guides/includes/file_level_issues.md +3 -0
- habit_hooks-1.0.0/src/habit_hooks/guides/includes/line_level_issues.md +3 -0
- habit_hooks-1.0.0/src/habit_hooks/guides/uncoached.md +8 -0
- habit_hooks-1.0.0/src/habit_hooks/hooks.py +27 -0
- habit_hooks-1.0.0/src/habit_hooks/mapper.py +131 -0
- habit_hooks-1.0.0/src/habit_hooks/recommend.py +46 -0
- habit_hooks-1.0.0/src/habit_hooks/resolve.py +101 -0
- habit_hooks-1.0.0/src/habit_hooks/scope.py +106 -0
- habit_hooks-1.0.0/src/habit_hooks/sensors/__init__.py +71 -0
- habit_hooks-1.0.0/src/habit_hooks/sensors/execution.py +108 -0
- habit_hooks-1.0.0/src/habit_hooks/sensors/loader.py +63 -0
- habit_hooks-1.0.0/src/habit_hooks/sensors/model.py +38 -0
- habit_hooks-1.0.0/src/habit_hooks/snooze.py +81 -0
- habit_hooks-1.0.0/tests/harness/__init__.py +13 -0
- habit_hooks-1.0.0/tests/harness/errors.py +9 -0
- habit_hooks-1.0.0/tests/harness/glyphs.py +12 -0
- habit_hooks-1.0.0/tests/harness/markdown.py +61 -0
- habit_hooks-1.0.0/tests/harness/parser.py +155 -0
- habit_hooks-1.0.0/tests/harness/runner.py +16 -0
- habit_hooks-1.0.0/tests/harness/steps.py +123 -0
- habit_hooks-1.0.0/tests/harness/text.py +13 -0
- habit_hooks-1.0.0/tests/test_harness.py +187 -0
- habit_hooks-1.0.0/tests/test_installed_wheel_smoke.py +249 -0
- habit_hooks-1.0.0/uv.lock +555 -0
- habit_hooks-1.0.0/write_code_like_this.png +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: uv
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
groups:
|
|
8
|
+
uv-minor-patch:
|
|
9
|
+
dependency-type:
|
|
10
|
+
- production
|
|
11
|
+
- development
|
|
12
|
+
update-types:
|
|
13
|
+
- patch
|
|
14
|
+
- minor
|
|
15
|
+
- package-ecosystem: github-actions
|
|
16
|
+
directory: /
|
|
17
|
+
schedule:
|
|
18
|
+
interval: weekly
|
|
19
|
+
groups:
|
|
20
|
+
github-actions:
|
|
21
|
+
patterns:
|
|
22
|
+
- "*"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- name: Checkout
|
|
11
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
12
|
+
- name: Setup uv
|
|
13
|
+
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.11"
|
|
16
|
+
- name: Install dependencies
|
|
17
|
+
run: uv sync --frozen
|
|
18
|
+
- name: Setup Node
|
|
19
|
+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
20
|
+
with:
|
|
21
|
+
node-version: '22'
|
|
22
|
+
- name: Install Node tool deps
|
|
23
|
+
run: |
|
|
24
|
+
npm ci --prefix plugins/typescript
|
|
25
|
+
npm ci --prefix plugins/generic
|
|
26
|
+
- name: Test
|
|
27
|
+
run: uv run pytest
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Dependabot auto-merge
|
|
2
|
+
on: pull_request
|
|
3
|
+
jobs:
|
|
4
|
+
auto-merge:
|
|
5
|
+
if: github.actor == 'dependabot[bot]'
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
steps:
|
|
11
|
+
- name: Fetch metadata
|
|
12
|
+
id: meta
|
|
13
|
+
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
|
14
|
+
- name: Enable auto-merge for patch and minor updates
|
|
15
|
+
if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor'
|
|
16
|
+
run: gh pr merge --auto --squash "$PR_URL"
|
|
17
|
+
env:
|
|
18
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
19
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
- name: Build every workspace package
|
|
15
|
+
run: uv build --all-packages --out-dir dist
|
|
16
|
+
- uses: actions/upload-artifact@v4
|
|
17
|
+
with:
|
|
18
|
+
name: dist
|
|
19
|
+
path: dist/
|
|
20
|
+
|
|
21
|
+
publish:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment: ${{ matrix.environment }}
|
|
25
|
+
permissions:
|
|
26
|
+
id-token: write
|
|
27
|
+
strategy:
|
|
28
|
+
fail-fast: false
|
|
29
|
+
matrix:
|
|
30
|
+
include:
|
|
31
|
+
- { dist: habit_hooks, environment: pypi }
|
|
32
|
+
- { dist: habit_hooks_generic, environment: pypi-generic }
|
|
33
|
+
- { dist: habit_hooks_python, environment: pypi-python }
|
|
34
|
+
- { dist: habit_hooks_typescript, environment: pypi-typescript }
|
|
35
|
+
- { dist: habit_hooks_php, environment: pypi-php }
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
- name: Isolate this package's distributions
|
|
42
|
+
run: |
|
|
43
|
+
mkdir -p upload
|
|
44
|
+
mv "dist/${{ matrix.dist }}-"*.whl "dist/${{ matrix.dist }}-"*.tar.gz upload/
|
|
45
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
46
|
+
with:
|
|
47
|
+
packages-dir: upload/
|
|
48
|
+
skip-existing: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Per-plugin Node tool deps (installed via npm ci; see plugins/*/package.json)
|
|
2
|
+
node_modules/
|
|
3
|
+
dist
|
|
4
|
+
coverage
|
|
5
|
+
.DS_Store
|
|
6
|
+
.idea
|
|
7
|
+
.claude-channel/
|
|
8
|
+
*.tgz
|
|
9
|
+
*.log
|
|
10
|
+
.vscode/
|
|
11
|
+
.venv/
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.pyc
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.spec-runs/
|
|
16
|
+
|
|
17
|
+
/ruff.toml
|
|
18
|
+
|
|
19
|
+
# Workflow orchestration script (run from ~/.claude, never a repo deliverable)
|
|
20
|
+
.claude/workflows-build-overnight.js
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Sensors & languages
|
|
6
|
+
- **Consumer-defined sensors** (#16): the `sensors` config map is now the single way sensors are assembled — built-in and custom alike. Each entry is one of three mutually exclusive modes: `use` (reference a bundled sensor by id — `eslint`/`comment`/`jscpd`/`knip`/`ruff`/`deptry`/`line-count`/`needs-extraction`), a **wrapper script** (`command` + `produces` printing bag JSON), or a **declarative adapter** (`command` + `produces` + `items`/`fields`/`group`/`map`). The sensor id is the map key; `dependsOn` wires multi sensors. See `docs/sensors.md`.
|
|
7
|
+
- **Authoritative `sensors` semantics**: when `sensors` is present it replaces the language preset entirely (no merge), so removing a built-in is just deleting its entry. When `sensors` is absent the preset is used and a deprecation warning is emitted — this implicit fallback is **removed in the 1.0.0 release**.
|
|
8
|
+
- **Consumer-defined languages** (#16): `language` accepts any string. The built-ins (`typescript`/`python`) keep their preset + default file globs; any other value relies on the open `files` discovery globs plus a `sensors` map. A non-built-in language with no `files` emits a warning and discovers no source files.
|
|
9
|
+
- A custom sensor must be declared as a pair — its `sensors.<id>` entry **and** a matching `smells.<smell>` entry (a custom smell needs `id` + `source: "custom"` + `severity`). New config validation rejects malformed sensor specs (mode mixing, missing required fields) and bad `files` globs.
|
|
10
|
+
|
|
11
|
+
## 0.2.0
|
|
12
|
+
|
|
13
|
+
### Highlights
|
|
14
|
+
- Habit Hooks is now a smell-agnostic, config-driven coach: a three-stage pipeline (sensor → mapper → guide) connected by a JSON bag. Sensors detect findings and translate them into a canonical, tool-independent **smell vocabulary**; the mapper routes each smell to a fix; the guide coaches the agent and sets the exit code.
|
|
15
|
+
- Two language presets ship out of the box — **TypeScript/JavaScript** (ESLint + knip + jscpd + a ts-morph comment scan) and **Python** (ruff + jscpd + deptry + a line-count sensor). No sensors run by default; `init` enables the preset for the project's language.
|
|
16
|
+
- The smell catalogue, language presets, and per-language tool config drive behaviour. Concrete smell knowledge lives only in config and the language initializers — the runner, mapper, sensors, and checks are smell-agnostic.
|
|
17
|
+
|
|
18
|
+
### CLI & config
|
|
19
|
+
- `habit-hooks` runs the configured sensors over a project, groups findings by smell, prints each smell's coaching, and sets the process exit code — non-zero when an enforced smell fires, zero on a clean run or suggested-only findings.
|
|
20
|
+
- Git-aware scope flags restrict a run to a change set: `--last <n>` (files changed in the last N commits), `--branch [name]` (vs a branch, default `scope.branchBase`), `--since <hash>` (since a commit), and `--all` (force every file). The four are mutually exclusive; the default scope and per-rule `changedFilesOnly` come from config. `--config <path>` points at an explicit config file; `--version` prints the version.
|
|
21
|
+
- `habit-hooks.config.{ts,js,mjs}` is intentionally small: per-smell/per-rule `include`/`exclude` globs, `severity` overrides, `disabled`, and `changedFilesOnly`; a `scope` block (`onlyChangedFiles`, `branchBase`); a `prompts` directory for custom/override coaching text; and `commentCheck` thresholds. All tool thresholds stay in the consumer's own eslint/knip/jscpd/ruff config.
|
|
22
|
+
|
|
23
|
+
### Wrap model & coaching
|
|
24
|
+
- Habit Hooks drives the consumer's **own** installed eslint / knip / jscpd, surfacing whatever rules and thresholds those configs define; it falls back to the bundled binaries only when the project has none. The coaching layer (why-it's-a-smell + how-to-fix) is what Habit Hooks adds on top.
|
|
25
|
+
- The bundled coaching prompts for the size/complexity smells are adapted from the refakts refactoring-quality system — keeping its analyse-first / anti-mechanical-fix structure — with the remaining prompts explaining why each smell matters rather than restating the threshold.
|
|
26
|
+
- knip 5 and 6 are both supported: the consumer's installed major version is auto-detected so v5's `classMembers` and v6's per-issue `files`/`exports`/`dependencies` shapes are each read correctly, and v6 no longer loses every knip check over a rejected flag.
|
|
27
|
+
|
|
28
|
+
### Smell catalogue
|
|
29
|
+
- A canonical, tool-independent catalogue (kebab-case keys, see `docs/smell-vocabulary.md`). `enforced` smells fail the run (exit 1); `suggested` smells coach but exit 0; the mapper config can override severity per project.
|
|
30
|
+
- Enforced size/complexity smells: `oversized-function`, `too-many-parameters`, `high-complexity`, `deep-nesting`, `oversized-file`.
|
|
31
|
+
- Enforced correctness smells: `unused-variable`, `loose-equality`, `var-declaration`, `non-const-binding`, `duplicate-import`, `redundant-type-annotation`, and the unused-code family from knip/deptry (`unused-file`, `unused-export`, `unused-dependency`, `unused-class-member`, `unused-import`).
|
|
32
|
+
- Suggested smells: `warning-comment`, `explicit-any`, `non-null-assertion`, `non-essential-comment`, `duplicated-code`.
|
|
33
|
+
- `needs-extraction` (enforced) is a **composite** smell; `parse-error` (enforced) is a supplemental smell for ESLint fatals with no catalogue rule.
|
|
34
|
+
- Each sensor owns its raw rule ID → smell translation; the mapper and prompts only ever key off the canonical smell, never the tool.
|
|
35
|
+
|
|
36
|
+
### Sensors & presets
|
|
37
|
+
- **TypeScript/JavaScript preset**: ESLint (size/complexity/correctness/TS smells + `parse-error`), knip (`unused-file`/`unused-export`/`unused-dependency`/`unused-class-member`), jscpd (`duplicated-code`), and an in-process ts-morph scan (`non-essential-comment`). `comment-check` still runs in-process via ts-morph — it is not a shell-out sensor.
|
|
38
|
+
- **Python preset**: ruff (`high-complexity`/`too-many-parameters`/`oversized-function`/`unused-variable`/`unused-import`), jscpd on `.py` (`duplicated-code`), deptry (`unused-dependency`), and a language-agnostic line-count sensor (`oversized-file`).
|
|
39
|
+
- Preset thresholds come from the consumer's own tool config (e.g. ESLint `max-lines`/`complexity`, ruff `mccabe.max-complexity`/`pylint.max-args`), not from Habit Hooks.
|
|
40
|
+
- **Composite sensors via `dependsOn`** (#17): a multi sensor declares the smells it consumes, receives their issues in `ctx.deps`, and emits a derived smell. `needs-extraction` fires when one file is both `oversized-file` **and** `duplicated-code`. It augments by default (all three smells show); `needsExtraction.replace: true` suppresses the two inputs so only `needs-extraction` remains. The augment-vs-replace switch runs in the sensor stage, keeping the mapper a pure single-smell function. Wired into the TS preset and the Python preset.
|
|
41
|
+
- **deep-nesting** (#26): new enforced TS smell via ESLint `max-depth`. Python `deep-nesting` (ruff `PLR1702`) is deferred while that rule is preview/unstable.
|
|
42
|
+
- **Python `oversized-file`** (#19): a language-agnostic line-count sensor emits it for files over a threshold (`max-module-lines`, default 200). ruff has no `C0302` port and rejects an unknown `max-module-lines` key under `[tool.ruff]`, so the threshold is read by a no-TOML-parser text scan of the consumer's ruff config + `pyproject.toml`; set it in a ruff-ignored location such as `[tool.habit-hooks]`.
|
|
43
|
+
- **`command` fix action** (#18): a smell's fix can be a script instead of a prompt. The guide runs the command once per smell that has issues, streams its output into that smell's section, and folds its exit code into the run's exit code.
|
|
44
|
+
- **Declarative adapter**: a tool that already emits JSON can be wired as a sensor by declaring how to read it (`group`/`items`/`fields`/`map`, up to two levels of array nesting) — no wrapper script needed. Anything it can't express falls back to a wrapper script.
|
|
45
|
+
- **Sensor failures fail the run** (#25): a sensor spawn/timeout failure now exits 1 (instead of a false-clean) while still rendering every successful sensor's output. Failures travel on a shared `SensorSink`; the failure notice is shown on stderr.
|
|
46
|
+
|
|
47
|
+
### Baseline, snooze & auto-prune
|
|
48
|
+
- A file-level baseline (snooze) is committed to the repo at `.habit-hooks-baseline.json`, so a whole team shares one snapshot of legacy violations. A snoozed file stays snoozed for every sensor only while it appears in the baseline, its last-commit hash matches, and its working tree is clean — touching the file re-arms every smell, so you cannot silently drift past snoozed violations.
|
|
49
|
+
- `habit-hooks baseline` subcommands manage it: `generate` (record current violations), `status` (list snoozed files and freshness), `snooze <files...>`, `forget <files...>`, and `prune` (drop stale/resolved entries).
|
|
50
|
+
- **Auto-prune of dead snooze entries** (#11): on a full-repo run, Habit Hooks re-scans baseline-free and reaps snooze entries whose file is present but no longer produces the smell, printing the pruned set. Scoped runs never mutate the baseline (a file can look clean only because its smell is outside the diff), so they are a guaranteed no-op. Auto-prune shares one reaper with the manual `baseline prune` command.
|
|
51
|
+
- A memoized, batched snooze index collapses the per-rule git spawns to O(1) status + O(files) memoized log calls.
|
|
52
|
+
|
|
53
|
+
### Init
|
|
54
|
+
- `habit-hooks init [language]` onboards a project for its language. With no argument it detects the language and prints a report-only message; an explicit language threads through with no re-detect, and an unsupported language exits 2 before any side effect.
|
|
55
|
+
- Detects which tools are already installed/configured and scaffolds starter configs only for the missing pieces: an ESLint flat config (TS) or `ruff.toml` + `.jscpd.json` (Python), with package-manager install commands spanning pip and node ecosystems.
|
|
56
|
+
- The scaffolded ESLint config writes tunable thresholds including `max-depth: 4` (deep-nesting) alongside the other size/complexity rules, and exempts test files from size rules. Test globs derive from a single shared exclude list.
|
|
57
|
+
- Recommended thresholds mirror across languages from one source (ESLint `complexity 10` / `max-params 3` ↔ ruff `mccabe.max-complexity 10` / `pylint.max-args 3`); a freshly-scaffolded config is pinned to satisfy the drift check.
|
|
58
|
+
- Drift detection is additive — a recommended value is flagged only when its key is absent, never when you've tuned it. `--accept-recommendations` runs the install commands and additively merges absent recommended keys into Habit-Hooks-owned config, never overwriting user values or editing user-owned `ruff.toml`/`pyproject.toml`/ESLint config.
|
|
59
|
+
- Prompts cover package.json scripts, a pre-commit hook, and the bundled `habit-hooks-review` skill. `--dry-run` prints every intended write without touching disk.
|
|
60
|
+
|
|
61
|
+
### Architecture
|
|
62
|
+
- **Config-driven, smell-agnostic** (#24): all tool/smell knowledge lives in `src/config/tool-smells.ts` — the ESLint raw→smell map, the eslint/knip/jscpd/comment `produces`, and the ruff + deptry adapter specs. ESLint/jscpd/comment data is **derived from the catalogue**, so adding a smell there auto-wires its translation and produces; the runner, sensors, checks, and rules registry import these instead of hardcoding any smell id. `deep-nesting` (#26) was added by touching only the catalogue and the init ESLint template — the live proof of #24.
|
|
63
|
+
- Single source for tool config discovery: `TOOL_CONFIG_FILENAMES` / `TOOL_PACKAGE_JSON_KEYS` in `src/detect/tool.ts`.
|
|
64
|
+
- A routed smell with no tuned `<smell>.md` template falls back to a generic `uncoached.md` body while keeping its severity; a truly unknown smell (no routing at all) goes to the uncoached bucket and never escalates the exit code.
|
|
65
|
+
|
|
66
|
+
### Breaking changes
|
|
67
|
+
- The bundled "default rule set" and programmatic tool pinning of the beta are gone. Behaviour is driven by the smell catalogue plus the consumer's own tool configs; `knip` is no longer version-pinned, so the consumer's installed version determines available issue types.
|
|
68
|
+
- The `rules` config field is **deprecated in favour of `smells`** (#21). `rules` is still accepted and folded in (with `smells` winning on conflict), but a config using it now emits a deprecation warning on stderr. Hard removal of `rules` is scheduled for a release after 0.2.0.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# habit-hooks notes
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
### Plugins are installed packages discovered via entry points (human-requested by Ivett)
|
|
6
|
+
|
|
7
|
+
The core finds plugins through the `habit_hooks.plugins` entry-point group, NOT
|
|
8
|
+
by walking a sibling `plugins/` directory. Each plugin is a separately
|
|
9
|
+
installable dist `habit-hooks-<name>` whose import package `habit_hooks_<name>`
|
|
10
|
+
ships its `config.toml`/`sensors/`/`guides/`/helper scripts/phar as package data
|
|
11
|
+
(importlib.resources-accessible). `resolve.installed_plugin_dirs()` maps plugin
|
|
12
|
+
name -> package-data dir via `importlib.metadata.entry_points` +
|
|
13
|
+
`importlib.resources.files`. The override chain is
|
|
14
|
+
`.habit-hooks/<plugin>/<file>` (project) -> `<plugin package data>/<file>`
|
|
15
|
+
(default). A configured plugin that is neither overridden under `.habit-hooks/`
|
|
16
|
+
nor installed raises a clear error naming `pip install habit-hooks-<name>`
|
|
17
|
+
(`Resolver.require_plugin`) — that is the bug-1 root-cause guard.
|
|
18
|
+
|
|
19
|
+
The repo is a uv workspace (`[tool.uv.workspace] members = ["plugins/*"]`); the
|
|
20
|
+
four in-repo plugins live under `plugins/<name>/src/habit_hooks_<name>/` and are
|
|
21
|
+
installed editable by `uv sync` for dev. Keeping them in-repo is only a dev
|
|
22
|
+
convenience — they do not need to live here. `tests/test_installed_wheel_smoke.py`
|
|
23
|
+
builds + installs the core + generic wheels into a throwaway venv and asserts a
|
|
24
|
+
real finding comes out; it is the gate that catches "installed runs can't locate
|
|
25
|
+
plugins". `${dir}` in a sensor command resolves to the plugin's package-data dir,
|
|
26
|
+
so helper-script paths (`${dir}/line-count.py`, `${dir}/../.jscpd.json`) keep
|
|
27
|
+
working once the layout is preserved under the import package.
|
|
28
|
+
|
|
29
|
+
### Sensor `args` live in the sensor's own toml, not the plugin `config.toml` (agent decision)
|
|
30
|
+
|
|
31
|
+
A sensor's default CLI args (e.g. line-count's `--max 200`) live as `args = [...]`
|
|
32
|
+
in `sensors/<name>.toml` and expand into the command via `${args}`. They cannot go
|
|
33
|
+
in the plugin `config.toml` because `sensors = [...]` (the ordered list) and a
|
|
34
|
+
`[sensors.<name>]` table collide as the same TOML key. A project replaces them
|
|
35
|
+
wholesale via `.habit-hooks/config.toml` `[sensors.<name>] args = [...]`
|
|
36
|
+
(replace-on-override — `SensorOverride.args`, threaded in `sensors._sensor_args`).
|
|
37
|
+
|
|
38
|
+
### jscpd resolves a config's relative `path` against the config file, not cwd (agent decision)
|
|
39
|
+
|
|
40
|
+
When `jscpd --config <abs path>` loads `.jscpd.json`, its `path: ["src"]` resolves
|
|
41
|
+
relative to the config file's directory, so a plugin-shipped config scans nothing
|
|
42
|
+
in the consumer repo. `plugins/generic/sensors/jscpd.py` therefore reads `path`
|
|
43
|
+
out of the config and passes those as positional args (resolved against cwd),
|
|
44
|
+
keeping the config the single source for threshold/ignore/minLines/minTokens.
|
|
45
|
+
|
|
46
|
+
## Gotchas
|
|
47
|
+
|
|
48
|
+
### knip runs a gated second pass in production mode (issue #59)
|
|
49
|
+
|
|
50
|
+
`knipWrap` runs knip twice when — and only when — the consumer's knip
|
|
51
|
+
config marks production patterns with a trailing `!` (detected by
|
|
52
|
+
`knipConfigMarksProduction` in `knip-resolve.ts`). The default pass is
|
|
53
|
+
authoritative for everything (incl. unused devDependencies); the
|
|
54
|
+
`--production` pass contributes only dead-code findings
|
|
55
|
+
(`PRODUCTION_PASS_SOURCES` in `knip-merge.ts`), merged + deduped. This
|
|
56
|
+
catches code reached only by tests without losing devDep detection.
|
|
57
|
+
Gotchas: `--production` analyses NOTHING unless `!` is on BOTH `entry`
|
|
58
|
+
and `project` (a no-`!` config under `--production` silently reports
|
|
59
|
+
zero — so we never pass it there). Test files must be listed as
|
|
60
|
+
unmarked (non-production) `entry`, else knip 5 + a vitest config falsely
|
|
61
|
+
reports them as unused files. The merge intentionally keeps `knip:files`
|
|
62
|
+
from the production pass, so a wholly test-only production file can
|
|
63
|
+
surface as an unused file — that's the feature, not a bug.
|
|
64
|
+
|
|
65
|
+
### JSDoc nodes are not MultiLineCommentTrivia in ts-morph
|
|
66
|
+
|
|
67
|
+
`/** ... */` blocks are `SyntaxKind.JSDoc` (321) when attached to a
|
|
68
|
+
declaration, NOT `MultiLineCommentTrivia`. To find them, query both. See
|
|
69
|
+
`src/checks/comment-check.ts`.
|
|
70
|
+
|
|
71
|
+
### knip's `exports` field omits the bin path
|
|
72
|
+
|
|
73
|
+
`knip` exports only `.` and `./session`, so
|
|
74
|
+
`require.resolve('knip/bin/knip.js')` fails. The bundled-fallback resolver
|
|
75
|
+
in `src/checks/knip-wrap.ts` (`bundledKnipBin`) resolves `'knip'` (main
|
|
76
|
+
entry) and navigates up to `../bin/knip.js` instead. Consumer-detected
|
|
77
|
+
knip is found via `detectTool`, which walks `package.json#bin.knip` and
|
|
78
|
+
does not hit this hazard.
|
|
79
|
+
|
|
80
|
+
### knip needs `package.json` in cwd
|
|
81
|
+
|
|
82
|
+
Running knip in a directory without `package.json` exits 2 with a help
|
|
83
|
+
message. `knipWrap` skips silently when no `package.json` is present —
|
|
84
|
+
the user's project always has one, but our internal test temp dirs
|
|
85
|
+
often don't.
|
|
86
|
+
|
|
87
|
+
### knip 5 vs 6 — issue type drift
|
|
88
|
+
|
|
89
|
+
We no longer pin knip; the consumer's installed version drives what
|
|
90
|
+
fires. v5 emits `classMembers`; v6 dropped that key and surfaces unused
|
|
91
|
+
exports via `files` / `exports` / `dependencies` instead. We ship
|
|
92
|
+
coaching prompts for all four so either version is covered. If a future
|
|
93
|
+
knip introduces a new top-level issue key, the wrap surfaces it as an
|
|
94
|
+
uncoached violation (see `unknownKeysForIssue` in
|
|
95
|
+
`src/checks/knip-wrap.ts`); add a prompt to coach it.
|
|
96
|
+
|
|
97
|
+
### comment-check file discovery doesn't honour project ignores
|
|
98
|
+
|
|
99
|
+
`runner.discoverFiles` uses fast-glob with a hardcoded ignore set
|
|
100
|
+
(`node_modules`, `dist`, `coverage`). Only `comment-check` consumes that
|
|
101
|
+
list directly — the eslint, knip, and jscpd wraps delegate discovery to
|
|
102
|
+
their respective tools. Fixtures under `tests/fixtures/**` therefore get
|
|
103
|
+
swept by comment-check when you run `node dist/cli.js` against the repo
|
|
104
|
+
root, which is why a smoke run on our own source shows comment
|
|
105
|
+
violations from inside fixtures.
|
|
106
|
+
|
|
107
|
+
### Bumping pnpm 10 → 11 needs Corepack, not auto-switch
|
|
108
|
+
|
|
109
|
+
pnpm 11 split its launcher: the main `pnpm` npm package owns
|
|
110
|
+
`dist/pnpm.mjs`, while `@pnpm/macos-arm64` (and siblings) ship only the
|
|
111
|
+
native loader. pnpm 10's `packageManager` auto-switch fetches only the
|
|
112
|
+
platform package, producing a binary missing its JS bootstrap —
|
|
113
|
+
`Cannot find module .../dist/pnpm.mjs`. Bootstrap pnpm 11 via Corepack
|
|
114
|
+
(`corepack prepare pnpm@<v> --activate`) or the official installer
|
|
115
|
+
instead. The standalone shim at `~/Library/pnpm/pnpm` is from the old
|
|
116
|
+
installer; once Corepack is on PATH, remove the shim so it stops
|
|
117
|
+
shadowing it.
|
|
118
|
+
|
|
119
|
+
### Wrap shell-out semantics — failures sit in `result.warnings`
|
|
120
|
+
|
|
121
|
+
`src/wrap/shell.ts` never throws on spawn/timeout failure. A spawn
|
|
122
|
+
failure surfaces as `exitCode === -1` with the cause in `warnings`; a
|
|
123
|
+
non-zero exit from the tool itself comes back with the real `exitCode`
|
|
124
|
+
and an empty `warnings`. The helpers `isSpawnFailure` and
|
|
125
|
+
`spawnFailureWarning` in `src/wrap/notices.ts` separate the two so a
|
|
126
|
+
crashed tool produces a stderr notice (and zero violations) instead of
|
|
127
|
+
silently swallowing the run.
|
|
128
|
+
|
|
129
|
+
### jscpd `-n` (noSymlinks) is baked in
|
|
130
|
+
|
|
131
|
+
`jscpdWrap` always passes `-n`. A consumer with intentionally symlinked
|
|
132
|
+
source directories (monorepo `src/shared -> ../shared-lib`) silently
|
|
133
|
+
will not get duplication detection on the linked paths. Surface this if
|
|
134
|
+
a user reports missing jscpd hits on a symlinked tree.
|
|
135
|
+
|
|
136
|
+
### Bundled habit-hooks ESLint vs project-local ESLint disagree on type-position param names
|
|
137
|
+
|
|
138
|
+
Our bundled config flags an unused parameter in a type-only function
|
|
139
|
+
signature (e.g. `resolve: (_result: ShellResult) => void`); the
|
|
140
|
+
project's flat config does not. Underscoring the param satisfies both.
|
|
141
|
+
This bit us during Phase 1/2 — if a wrap introduces a new callback type
|
|
142
|
+
and the build trips on `no-unused-vars` for a positional name, prefix
|
|
143
|
+
with `_`.
|
|
144
|
+
|
|
145
|
+
### Tool config filename lists live in `src/detect/tool.ts`
|
|
146
|
+
|
|
147
|
+
`TOOL_CONFIG_FILENAMES` and `TOOL_PACKAGE_JSON_KEYS` are the single
|
|
148
|
+
source for tool config discovery. When you add a new tool, a new config
|
|
149
|
+
filename, or a new `package.json` key (e.g. `eslint.config.cjs`,
|
|
150
|
+
`knip.jsonc`), update those tables only — `detectTool` and every
|
|
151
|
+
caller flows through them. Individual wraps may keep their own narrower
|
|
152
|
+
lists for internal "has-config" checks, but those should mirror the
|
|
153
|
+
canonical set.
|
|
154
|
+
|
|
155
|
+
### A sensor named `ruff.toml` collides with ruff's config discovery
|
|
156
|
+
|
|
157
|
+
`plugins/python/sensors/ruff.toml` is a sensor spec (`command = ...`),
|
|
158
|
+
but ruff treats any file literally named `ruff.toml` as its own config.
|
|
159
|
+
A `ruff check` whose upward config-discovery walk passes through
|
|
160
|
+
`plugins/python/sensors/` hard-fails with `unknown field 'command'`.
|
|
161
|
+
Harmless in normal consumer operation — the file lives inside the
|
|
162
|
+
habit-hooks package, off the consumer's discovery path — but a future
|
|
163
|
+
dogfooding ruff run from inside that tree will be mystifying. Point ruff
|
|
164
|
+
at an explicit `--config` (the repo-root `ruff.toml`, gitignored as
|
|
165
|
+
`/ruff.toml`) if you hit this.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivett Ördög
|
|
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,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: habit-hooks
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Turn best-practice coding advice into AI habits — structural code-smell coaching for AI agents
|
|
5
|
+
License-File: LICENSE.md
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: habit-hooks-generic~=1.0
|
|
8
|
+
Requires-Dist: jinja2>=3
|
|
9
|
+
Requires-Dist: pathspec>=0.12
|
|
10
|
+
Requires-Dist: pydantic>=2
|
|
11
|
+
Provides-Extra: all
|
|
12
|
+
Requires-Dist: habit-hooks-php~=1.0; extra == 'all'
|
|
13
|
+
Requires-Dist: habit-hooks-python~=1.0; extra == 'all'
|
|
14
|
+
Requires-Dist: habit-hooks-typescript~=1.0; extra == 'all'
|
|
15
|
+
Provides-Extra: php
|
|
16
|
+
Requires-Dist: habit-hooks-php~=1.0; extra == 'php'
|
|
17
|
+
Provides-Extra: python
|
|
18
|
+
Requires-Dist: habit-hooks-python~=1.0; extra == 'python'
|
|
19
|
+
Provides-Extra: typescript
|
|
20
|
+
Requires-Dist: habit-hooks-typescript~=1.0; extra == 'typescript'
|