HapLint is an offline command-line linter for Apple Core Haptics .ahap files. It catches malformed assets before they reach a device, produces deterministic console, JSON, and SARIF reports for CI, verifies optional audio resources inside an explicit bundle root, and renders a script-free SVG timeline.
It is deliberately not another haptic editor, generator, or playback runtime. Existing tools are strongest at authoring and previewing; HapLint owns the headless validation step between an authored asset and a shipped bundle.
Static success proves the documented AHAP contract passed. It does not certify playback on Apple hardware or perceptual quality.
- Recursive, case-insensitive
.ahapdiscovery with deterministic ordering. - UTF-8, 5 MiB size, JSON syntax, and duplicate-key checks.
- AHAP version 1 events, fixed parameters, dynamic parameters, and curves.
- Apple-documented value ranges, event/parameter compatibility, and the 30-second continuous-haptic limit.
- Lexical traversal rejection and optional on-disk checks for
AudioCustomresources. - Stable rule IDs with error/warning severity and JSON Pointer locations.
- Console, machine-readable JSON, GitHub-compatible SARIF 2.1.0, and inert SVG output.
- Zero runtime dependencies and no network access.
The complete rule catalog is in docs/rules.md.
Download the wheel from the v0.1.0 release, then install it with Python 3.11 or newer:
python -m pip install haplint-0.1.0-py3-none-any.whl
haplint versionTo work from source:
git clone https://github.com/KanadeK/haplint.git
cd haplint
uv sync --locked --dev
uv run --locked haplint versionCheck one file or any number of files and directories:
haplint check Assets/Haptics
haplint check tap.ahap confirmation.ahap --strictProduce CI reports without changing the input:
haplint check Assets/Haptics --format json --output haplint.json
haplint check Assets/Haptics --format sarif --output haplint.sarif --strictRender a valid pattern as a standalone SVG:
haplint render examples/valid/ramp.ahap --output ramp.svgCheck that every AudioCustom waveform exists as a regular file under one bundle root:
haplint check Assets/Haptics --bundle-root AssetsHapLint never searches for waveform files unless --bundle-root is supplied.
haplint check PATH [PATH ...] [--format console|json|sarif] [--output FILE]
[--strict] [--bundle-root DIRECTORY]
haplint render FILE --output FILE.svg [--bundle-root DIRECTORY]
haplint rules [--format console|json]
haplint version
0: no errors; warnings are allowed unless--strictis set.1: an input, resource, output, or lint failure occurred.2: invalid command syntax, reported byargparse.
Example warning:
examples/warnings/clamped.ahap
/Pattern/0/Event/EventParameters/0/ParameterValue [warning AHP027] HapticIntensity will be clamped to the range 0 through 1.
Summary: 1 files, 0 errors, 1 warnings
The repository workflow runs the same release gate on Linux/Python 3.11 and Windows/Python 3.14. In another repository, the minimum useful gate is:
haplint check . --strict --format sarif --output haplint.sarifUpload haplint.sarif with the full-commit-pinned SARIF action already approved by your organization. Each result includes a stable rule ID, severity, source URI, and jsonPointer property.
| Symptom | Meaning | Repair |
|---|---|---|
AHP001, AHP003, or AHP004 |
The file cannot be decoded as unambiguous UTF-8 JSON. | Save as UTF-8, repair JSON syntax, and remove duplicate object keys. |
AHP023 |
A continuous event is missing a positive duration or a haptic duration exceeds 30 seconds. | Add EventDuration; split haptics longer than 30 seconds. |
AHP027 |
Core Haptics will clamp a parameter. | Move the value into the range shown by the diagnostic, or omit --strict only when the clamp is intentional. |
AHP030 or AHP031 |
An AudioCustom path is unsafe or missing under the selected bundle root. |
Use a relative path without .., then place a regular file at that exact location. |
Exit 2 |
The command shape is invalid. | Run haplint --help or haplint COMMAND --help and correct the arguments. |
| Local release gate fails before tests | The locked development environment is absent or stale. | Run uv sync --locked --dev, then rerun the acceptance command below. |
Do not suppress an error to get a green build. Run haplint rules for the rule-specific fix, repair the asset or command boundary, and rerun the exact failing command.
The project uses Python 3.11+, uv 0.11.20, pytest, Ruff, mypy, build, coverage, and pip-audit. Behavior changes begin with a failing test.
uv sync --locked --dev
uv run --locked python scripts/check.pyThat one acceptance command checks the lock file, formatting, lint, strict types, branch coverage, dependency advisories, committed pass/warn/fail examples, wheel and sdist contents, and a clean-environment wheel installation. See CONTRIBUTING.md for the small-step workflow.
The implementation follows Apple's public AHAP representation and event-duration documentation. Apple documents defaults for omitted values, clamping for out-of-range values, and ignored unsupported keys, so HapLint reports range and unknown-key findings as warnings while malformed required structure and checked-resource failures are errors.
- Representing haptic patterns in AHAP files
- CHHapticEvent duration
- Specification and non-goals
- Threat model
- Security policy
HapLint does not execute asset content, contact a service, play haptics, inspect an app bundle without an explicit root, or replace real-device QA.
MIT. See LICENSE.