Skip to main content

Writing a New Check

Zenzic's checks live in src/zenzic/core/. Each check is a standalone function in either scanner.py (filesystem traversal) or validator.py (content validation). CLI wiring is in the cli/ package (src/zenzic/cli/).


Six-Step Checklist

  1. Implement the logic in the appropriate core module (zenzic.core.scanner or zenzic.core.validator).
  2. Delegate resolution to InMemoryPathResolver — never call os.path.exists(), Path.is_file(), or any other filesystem probe inside a per-link loop. The resolver is instantiated once before the loop; re-instantiation per file defeats the pre-computed _lookup_map and drops throughput from 430 000+ to below 30 000 resolutions/s.
  3. Test i18n — if the check involves file paths, test it in all three i18n configurations (none, folder mode, suffix mode).
  4. Wire the CLI — add a corresponding command or sub-command in the cli/ package. See the CLI Architecture reference. If your command accepts a PATH argument, you must call find_repo_root(search_from=resolved_path) and invoke _apply_target() to respect Path Sovereignty.
  5. Write tests in tests/ covering both passing and failing cases, including a performance baseline (5 000 links resolved in < 100 ms against a mock in-memory corpus).
  6. Update examples in examples/ to exercise the new check — Zenzic validates its own examples on every commit.

Performance contract: the zenzic.core hot path must remain allocation-free. No Path object construction, no syscalls, and no relative_to() calls inside the resolution loop.


Core Laws

All checks must respect the Core Laws governing the scanner. Before writing a check, ensure you are familiar with the invariants detailed in the Core Laws of the Scanner.


CLI Wiring

Depending on whether you are adding a command to an existing sub-app or introducing a new top-level sub-app, follow the steps below:

Adding a Command to an Existing Sub-App

In the appropriate sub-app module (e.g., src/zenzic/cli/_check.py):

@check_app.command(name="metadata")
def check_metadata(path: Path = ...) -> None:
...

No changes to __init__.py, main.py, or _metadata.py are required.

Adding a New Top-Level Sub-App

  1. Create a new module src/zenzic/cli/_myfeature.py defining the sub-app: myfeature_app = typer.Typer(...).
  2. Export myfeature_app from src/zenzic/cli/__init__.py.
  3. Register the sub-app in src/zenzic/main.py: app.add_typer(myfeature_app, name="myfeature", rich_help_panel="...").
  4. Add a CommandMeta(...) entry in src/zenzic/cli/_metadata.py so root help panels and short help stay authoritative.
  5. If the sub-app uses no_args_is_help=True, add "myfeature" to the _SUBAPPS_WITH_MENU frozenset in cli_main() so the Zenzic banner appears when the sub-app is invoked with no arguments.

Credential Scanner Obligations

If your check touches the credential scanner or harvest(), see the dedicated Credential Scanner Obligations reference. The four obligations (Worker Timeout, Regex-Canary, Dual-Stream Invariant, Mutation Score ≥ 90%) are enforced on every PR touching src/zenzic/core/.


Finding Codes

Every new check must emit findings using a code registered in FROZEN_CODES. Before adding a new code:

  1. Run zenzic inspect codes — confirm the code does not already exist.
  2. Add the code to FROZEN_CODES in the appropriate tier (Core, Structure, or Governance).
  3. Update CHANGELOG.md with the new code in the same commit.

Do not reuse retired codes. Retired codes stay in FROZEN_CODES with status retired.