Metadata-Version: 2.5
Name: haconf
Version: 0.1.0
Summary: Check Home Assistant YAML and Jinja before reload — no instance needed.
License: MIT License
        
        Copyright (c) 2026 WraithWall <contact@wraithwall.online>
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: jinja2==3.1.6
Requires-Dist: pyyaml==6.0.3
Provides-Extra: dev
Requires-Dist: build==1.6.1; extra == 'dev'
Requires-Dist: pytest==8.3.4; extra == 'dev'
Requires-Dist: twine==7.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# haconf

Check Home Assistant YAML and Jinja before you reload — no instance needed.

Status: **I1–I8 — the v1 CLI is feature-complete.** It walks a configuration
directory, runs a robust YAML loader (duplicate keys, multi-document files,
quoting pitfalls such as `on`/`off`/`yes`/`no` and number-looking strings,
tab/indentation errors), applies a bundled, versioned integration catalog
(unknown keys under a known integration are errors, integration-name case
mistakes are errors naming the correct form, unknown integrations are
**informational only**), checks Jinja templates (compiled with Home Assistant's
strict environment shape, rendered against a stub of HA's documented
functions/filters — the **real** error text surfaces, e.g. `No filter named
'strftime'`, instead of HA core's masked shape error), and runs the I4 integrity
checks (`!include`/`!include_dir_*` targets must exist and stay inside the config
dir; `!secret` keys must be defined in `secrets.yaml` — values are never read or
printed; `packages:` entries must not be empty; automation/script
trigger/condition/action shapes; duplicate `entity_id`s reported once each;
template references to entities neither declared in the config dir nor present in
`.storage/core.entity_registry` are flagged).

**I8 closes the three scopes the audit left open:** a `homeassistant: packages:`
body is checked like the top level (a package name is user-chosen and is never a
key); the nested branches are walked (`choose`, `repeat` with its `while`/`until`,
`parallel`, `if`/`then`/`else`, `wait_for_trigger`, and `condition: and`/`or`/`not`
recursively), so a bare string three levels down is the same error as one at the
top; and `custom_templates/*.jinja` files are checked as Jinja through one shared
environment, so a macro imported from a sibling file is not an "undefined name".
A `script:` block is a map of id → script config, in `configuration.yaml`,
`packages:` and `scripts.yaml` alike — the id is a name, the body holds the
options.

I5 completes the contract: every finding carries a **severity** (error / warning
/ info) and a **plain-language hint**; warnings and infos pass the run unless
`--strict` escalates warnings; findings can be **suppressed visibly** via
`.haconfignore` or an inline `# haconf: ignore=<rule-id>` (suppressed findings
are always counted and printed, never hidden); and `--json` emits one complete
machine-readable object including the suppressed counts. The catalog ships as
package data and is never fetched at runtime.

Release work is complete: packaging **I5c** (`docs/RELEASE.md`), the security
pass **I6** (`scripts/security_sweep.py`; the N8–N12 absent-behaviour tests),
and real-config validation **I7** (`docs/REAL-CONFIG-VALIDATION.md` — two pinned
public config repositories; the validation exposed real gaps, all fixed and
pinned by `tests/test_i7_fixes.py`).

Exit codes: `0` clean (or informational findings only) · `1` findings with at
least one error · `64` usage. The `64` is a deliberate extension of the
contract's 0/1 (deviation **D-HC-1**, same rationale as RestVerify's D-I1: a
usage failure is not a lint outcome and must not collide with 1).

Read-only by construction: nothing under `src/` opens a file for writing, and the
test suite proves it (static AST check + a dynamic `open()` guard).

## Development

```bash
pip install -e '.[dev]'
python -m pytest -q                  # 174 tests, ~2 s
python scripts/security_sweep.py     # read-only / no-network / no-eval sweep
python -m build                      # wheel + sdist in dist/
```

The same two commands run in CI (`.github/workflows/ci.yml`) on Python 3.10,
3.11 and 3.12, plus a packaging job that installs the built wheel into a clean
venv and asserts the catalog is inside it while `tests/` is not.

## License

MIT — see [`LICENSE`](LICENSE).
