Skip to content

latent.lint.engine

latent lint engine — file reading, waivers, rule dispatch, output, exit codes.

Stdlib ast + tokenize + pyyaml only. Never imports the rest of latent: it must lint a repo whose extras are not installed (ADR-003 §B).

Functions

compose_review_prompt

compose_review_prompt() -> str

The packaged Tier-2 prompt with its generated rule table filled in.

The table is built from RULES instead of hand-copied into the markdown: eight of the file's rows duplicated RULES[*].machinery and would drift with no test to catch it.

exit_code

exit_code(findings: Sequence[Finding]) -> int

2 on any LT999 (engine error), 1 on any unwaived finding, else 0.

Baseline-blind; the CLI's ratchet path is :func:ratchet_exit_code.

format_github

format_github(findings: Sequence[Finding]) -> str

One ::error annotation per unwaived finding, input order.

Grammar: ::error file={path},line={line}::{rule} {message}. Waived findings are not annotated — a waiver is the accepted decision, and re-surfacing it on the PR is exactly the noise waivers exist to remove.

format_json

format_json(findings: Sequence[Finding], exceedances: Sequence[Exceedance] = ()) -> str

CI telemetry: every finding (waived included) plus waiver and ratchet counts.

The only format that carries waived findings — text and github show unwaived only, so a dashboard tracking waiver debt has exactly one source. Key order is the wire contract and comes from these literals; sort_keys stays off. Findings sort by (path, line, rule) and exceedances by (path, rule) so two runs over the same tree produce byte-identical output regardless of caller order.

format_text

format_text(findings: Sequence[Finding], collapse: bool = False) -> str

One path:line rule message line per unwaived finding, input order.

collapse instead folds repeats of one (path, rule, message) into a single line carrying the count and the lines it sits on; groups appear in the input order of their first finding. Only render("text") asks for that form — it is the one output a person reads. The default is per-finding: see :func:latent.lint.rules.lt004 for the invariant.

lint_files

lint_files(paths: Iterable[Path | str], repo_root: Path | str) -> list[Finding]

Lint the given files; the result includes waived findings (waived=True).

Only flow-dir modules (parent has catalog.yaml or parameters.yaml) are checked; other files contribute zero findings. Selecting any module of a flow dir also pulls in that dir's anchor module, because dir-level rules report there. Known input errors — syntax errors, unreadable files, corrupt yaml sidecars — become LT999 findings; exceptions from rule checks propagate. A path outside repo_root raises ValueError: its finding key would be a ../.. string that no baseline can match.

Waivers are applied here rather than inside a rule, and what they matched is fed back to :func:_hygiene_findings — the whole file's findings have to exist before a waiver can be called stale.

Naming a file is the caller's decision and always wins: this is the one entry point that ignores config/lint.yaml's exclusions, which are a discovery boundary (see :func:_excluded_dirs).

lint_paths

lint_paths(paths: Sequence[Path | str] | None = None, repo_root: Path | str | None = None, changed: bool = False, base: str = 'origin/main') -> list[Finding]

Select flow-dir modules and lint them with lint_files.

Selection modes, first match wins:

  • paths is not None: lint exactly those roots — files pass through, directories are walked for flow dirs (flat per dir); changed and base are ignored (--paths overrides --changed). An empty sequence lints nothing. A root that does not exist becomes an LT999 finding (exit 2); a root outside repo_root raises ValueError.
  • changed inside a git worktree: files changed since merge-base(base, HEAD) plus working-tree changes (staged, unstaged, untracked), filtered to existing *.py under repo_root — deleted paths are skipped. An unresolvable base raises RuntimeError.
  • otherwise (including changed outside a git worktree): full scan of repo_root.

repo_root defaults to the git toplevel of the cwd, else the cwd. Every mode skips the directories config/lint.yaml excludes; only lint_files ignores them.

ratchet_exit_code

ratchet_exit_code(findings: Sequence[Finding], exceedances: Sequence[Exceedance]) -> int

2 on any LT999, 1 on any exceedance, else 0 — exit_code's ratchet twin.

Callers pass compare(findings, load_baseline(path)); an absent baseline loads as {}, making every unwaived finding an exceedance, so this equals exit_code(findings) when no baseline file exists — the ratchet is always on, no mode branch. LT999 dominance is unconditional: engine errors are never baselined ("crash, never a silent pass").

render

render(findings: Sequence[Finding], fmt: str, exceedances: Sequence[Exceedance] = ()) -> str

Dispatch to a formatter by name — what check --format renders with.

Not the only rendering path: hook-exec calls :func:format_text directly, because its reader is an agent and no --format choice selects that shape.

exceedances reaches only the json format — text and github have no place to show a ratchet delta, and silently dropping it there is the intended behavior, not a gap.

text is read by a person, so it asks for the collapsed form; github and json are consumed per location and emit one entry per finding.

resolve_repo_root

resolve_repo_root(repo_root: Path | str | None = None) -> Path

Explicit root resolved; else the git toplevel of the cwd, else the cwd.

Public so the CLI anchors lint_paths, the baseline read path, and baseline write's destination to one root — Finding.path keys and baseline keys must share it.

Attributes

LINT_FORMATS

LT000

LT999