Files
pkh/plans/pkh-lint.md
T
vhaudiquet f608071f44
CI / build (push) Successful in 2m56s
CI / test (push) Skipped
CI / snap (push) Successful in 4m29s
lint: add pkh lint, wrapping lintian for parity plus pkh-native checks
pkh covered the package lifecycle but never validated the packaging
itself: broken control stanzas, unparsable changelog versions or
uncommitted debian/ edits only surfaced at build or upload time. pkh
lint lints a source tree with day-one lintian parity plus a native Rust
engine for the checks lintian cannot have.

The wrapper reuses the pkh build output next to the tree when it matches
the current changelog entry and no tree content is newer (mtime walk,
skipping .git/.pc), else packs fresh with dpkg-source -b using weak gzip
compression (the artifact is ephemeral; xz dominated the run at 9.8 s
versus 2.7 s on a 111 MB tree) and symlinks quilt orig tarballs from the
tree's parent, which dpkg-source searches in cwd. Findings are parsed
from the installed lintian into a unified report, deduplicated by tag
name against the native engine, and rendered lintian-shaped
(<L>: <pkg> <type>: <tag> <details>) as text or JSON, colorized at
render time (--color auto/always/never). Exit codes follow lintian's
contract (0 clean, 1 findings at/above --fail-on, 2 runtime error);
lintian's own exit code is ignored because it uses 2 both for findings
and for runtime errors. -d/--dist maps to lintian --profile so the
target distro's rules apply even on a foreign host.

The native engine hosts the first workflow check lintian cannot know:
pkh-debian-changes-not-committed flags debian/ content that is not
committed to git, since the pkh flow builds and uploads the tree as-is.
Checks register in a static registry validated by a unit test, and the
wrapper's parser is pinned by golden tests captured from lintian 2.129
output. Strategies for lintian's Ubuntu blind spots (its vendor data
there is one file plus 14 disabled tags) are specced in
plans/pkh-lint.md, deliberately not implemented yet.
2026-09-19 23:43:30 +02:00

556 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `pkh lint` — Package linting — Spec
Status: **phase 1 (wrapper + native skeleton) implemented**; phases 2–4 proposal / discussion draft
## Phase 1 implementation notes
What landed (all of it behind `pkh lint`):
- `src/lint/`: tag model, check trait + registry (`check.rs`), collectors
(`collect.rs`: source name from control/changelog, git dirty-state under
`debian/`), emitter + report (`emit.rs`), text/JSON rendering + exit-code
decision (`output.rs`), lintian wrapper (`wrapper.rs`), and the first
native check `pkh-git` (`checks/pkh.rs`, tag
`pkh-debian-changes-not-committed`).
- CLI: every option of the table below; `--list-tags` prints the native
catalog. Tests: parser goldens captured from real lintian 2.129 output
(source, binary, overridden, `--info` shapes), registry lint, merge dedup,
display/exit-code matrix, git-check fixtures.
- Measured deltas from the original draft, resolved during implementation:
- lintian accepts only built package files, never trees — the wrapper
prefers the `pkh build` output next to the tree when current
(freshness-guarded), else packs fresh fast (`-Zgzip -z1`); see
[Wrapper mode](#wrapper-mode-the-parity-layer). A tree with uncommitted
quilt changes makes dpkg-source refuse; the error points at committing
or `--native`.
- lintian's own exit code is 2 both for "fail-on met" and "runtime error",
so the wrapper never trusts it: the verdict comes from parsed findings,
and pkh keeps its 0 (clean) / 1 (fail-on met) / 2 (runtime error) contract.
- `--display-info` (lintian's `-I`) exists as its own flag: `--info` shows
explanations (lintian's `-i`), it does not raise the display level.
- Licensing outcome (see risks): pkh will be dual-licensed GPL-2+ / MIT;
any future lintian-derived files stay GPL-2+-only, file-level separated.
## Problem
pkh covers the package lifecycle (`pull`, `chlog`, `build`, `deb`, `put`) but
never validates the packaging itself: nothing catches a broken
`debian/control` stanza, an unparsable changelog version, a missing copyright
file, or a leftover `UNRELEASED` entry *before* a build or a PPA upload —
and when the build does fail, the error often surfaces late and far from the
cause. The standard tool, lintian, is a separate invocation with its own
conventions, its own lab setup, and no integration with pkh's workflow.
The requirement is **feature parity with lintian**. Rewriting lintian
outright is a multi-year project; ignoring it leaves no parity at all. The
strategy below gets parity on day one (wrap lintian), then migrates checks
to a native Rust engine incrementally, with a differential harness proving
equivalence as checks move.
## What lintian actually contains (measured, v2.129)
Numbers from the installed package, because they drive every decision:
| Component | Size | Declarative? |
|---|---|---|
| Tag catalog | **1,546** tag files (`Tag/Severity/Check/Explanation/See-Also`) | **Yes — already data** |
| Check logic | **346** Perl modules, **47,586** lines total | No — code |
| — small modules (280 of 346) | < 150 lines (≈40–60 of those are GPL headers/boilerplate; real logic often 10–100 lines) | Partly expressible as predicates |
| — heavy modules (18) | 400–1,089 lines (Debhelper, Scripts, Changelog, Dep5, Cruft, Menus, Debconf, InitD, Rules…) | No — mini-programs |
| Static data | **110** files, **2.2 MB** (spelling dictionaries, section lists, debhelper command tables…) | Yes |
| Infrastructure | unpackers, file index, ELF analysis (objdump integration), changelog/copyright parsers | No — code |
The split that matters: **157 of the 346 checks** walk the unpacked binary
tree (`visit_installed_files`); the rest operate on source/control data.
Binary-tree checks are the majority of the long tail and need unpack
infrastructure before they can be native.
## Goal
`pkh lint [path]` is the single linting entry point of pkh:
- **Day-one parity via wrapping**: by default, `pkh lint` runs the installed
lintian with mapped flags and merges its findings with pkh's own. Every
lintian check, tag, profile and override works through pkh.
- **A native Rust engine growing underneath**: pkh-native checks (git
workflow, PPA conventions — things lintian cannot know) plus checks
ported from lintian one batch at a time, each gated by a differential
harness against real lintian. Parity never regresses because the wrapper
keeps covering whatever is not yet native.
- **New checks are cheap to add**: one Rust file per check area, one
registration line, fixture tests — plus a data-driven escape hatch for
one-off, package-specific rules without writing Rust.
- **Lintian-compatible where it matters**: lintian's tag naming, override
file formats and names, output letter conventions and exit codes, so
muscle memory, docs, and in-tree overrides carry over. Where a check is
equivalent to a lintian check, we use lintian's tag name (this also makes
deduplication between wrapper and native findings trivial).
### Non-goals
- **No monolithic rewrite upfront.** Native coverage grows check by check
under the harness; the wrapper keeps full parity in the meantime.
- **No YAML as a check-logic language.** YAML carries the tag catalog, data
tables and a thin predicate layer; real check logic is Rust (see
[Why not YAML for check logic](#why-not-yaml-for-check-logic)).
- **No lintian profile files**, no Perl/Python plugin loading.
- **No auto-fixing** (`--fix` is a possible follow-up; checks report, they
don't edit).
## Proposed UX
```
$ pkh lint # lints the source tree in the current directory
$ pkh lint ../hello_2.10/ # or an explicit path
$ pkh lint --json # machine-readable (CI, editor integrations)
$ pkh lint --native # skip the lintian wrapper, native engine only
```
Default mode: **wrapper + native** when lintian is installed (notice printed
if it is absent, native-only fallback); `--native` forces native only.
Native findings and parsed lintian findings are merged into one report;
duplicates are dropped by tag name (works because we mirror lintian's tag
names for equivalent checks).
Typical output, lintian-shaped:
```
E: hello source: broken-debian-changelog-line line 12: unterminated line
W: hello source: maintainer-name-missing John
I: hello source: out-of-date-standards-version 4.5.0 (current is 4.7.2)
W: hello source: pkh-debian-changes-not-committed (native: uncommitted debian/ edits)
N: 2 overridden tags were shown as O:
```
Options (lintian names kept where equivalent; pkh common options where
relevant):
| Option | Meaning |
|---|---|
| `[path]` | Source tree to lint (default: cwd) |
| `-d, --dist <dist>` | Target distribution (debian/ubuntu); default: from changelog / host |
| `-s, --series <series>` | Target series; enables series-validity checks |
| `--native` | Native engine only, do not invoke lintian |
| `--repack` | Ignore existing pkh build output; pack the tree fresh for linting |
| `--fail-on <list>` | Severities that make the exit code 1, comma-separated (default: `error`) |
| `--info` / `-i` | Show long descriptions and references after each finding |
| `--display-info` / `-I` | Also display info-level tags |
| `--pedantic` | Also display pedantic tags |
| `--experimental` | Also display experimental tags |
| `--show-overrides` | Also display suppressed tags marked `O:` |
| `--suppress-tags <list>` | Ignore specific tag names for this run |
| `--check <name>` | Run only the named native checks (repeatable) |
| `--list-tags` | Print the merged tag catalog (name, severity, description) and exit |
| `--json` | Emit findings as JSON instead of text (same exit code) |
| `--color <when>` | `auto` (default: TTY without `NO_COLOR`), `always`, `never` |
Exit codes, mirroring lintian: `0` no findings at/above `--fail-on`; `1`
findings at/above the threshold (in wrapper mode: the merge of lintian's
verdict and native findings); `2` runtime error. Default display level:
`error` + `warning` (deliberately quieter than lintian, which also shows
`I:`; info is one `-i` away).
## Wrapper mode (the parity layer)
`src/lint/wrapper.rs`:
- **Requires the `lintian` binary** on PATH; without it, prints a notice and
runs native-only (exit code unaffected by the missing wrapper).
- **Artifact strategy — reuse pkh build output, never lint stale state.**
Lintian accepts only built package files, so the wrapper lints
`../<source>_<version>.dsc` (pkh build's own naming) when it matches the
current changelog entry and no tree content is newer (mtimes, skipping
`.git`/`.pc`). Current output → linted directly, no packing at all
(the fast path: 2.7 s vs 12.5 s on a 111 MB tree). Missing, stale, or
`--repack` → the tree is packed fresh with `dpkg-source -b` into a temp
dir, using weak compression (`-Zgzip -z1`) since the artifact is
ephemeral; quilt trees get their orig tarballs symlinked from the tree's
parent (dpkg-source searches cwd, which is the temp dir). Each run prints
an `N:` note stating which artifact was linted and why.
- **Flag mapping**: `--info`, `--display-info`, `--pedantic`,
`--experimental`, `--show-overrides`, `--suppress-tags` translate 1:1;
`path`, `--repack`, `--check` and `--json` are pkh-side.
- **Output parsing is load-bearing here** (unlike a convenience-only
bridge): lintian's line format (`X: <pkg> <type>: <tag> <details>`) is
parsed into pkh's `LintReport` model so both worlds merge, `--json` works
uniformly, and native checks can be deduplicated against lintian's. The
parser is tested against golden samples of real lintian output (the
format is stable and version-pinned by the installed lintian).
- **Exit code**: max(lintian's verdict, native findings at/above
`--fail-on`); lintian crash → exit 2 with raw output retained.
- The wrapper streams nothing to the terminal until merged: pkh renders the
unified report (same renderer in all modes).
## Native engine
New module `src/lint/`, library-first like every other subcommand; `main.rs`
only parses flags and picks a view.
```
src/lint/
mod.rs engine entry point: run(options) -> LintReport
tag.rs Severity, Certainty, Tag metadata
check.rs Check trait, registry
collect.rs collectors producing LintData (see below)
emit.rs Emitter (dedup, override matching) + LintReport
output.rs text / JSON rendering, exit-code decision
overrides.rs lintian override file parsing and matching
wrapper.rs lintian invocation, flag mapping, output parsing, merge
checks/
control.rs debian/control checks
changelog.rs debian/changelog checks
copyright.rs debian/copyright checks
source_format.rs debian/source/format checks
rules.rs debian/rules checks
watch.rs debian/watch checks
patches.rs debian/patches/ checks (DEP-3)
deps.rs dependency-structure checks (uses debian::deps)
pkh.rs pkh-native checks (git workflow, PPA conventions)
```
### Tag model
```rust
pub enum Severity { Error, Warning, Info, Pedantic }
pub enum Certainty { Certain, Possible, WildGuess }
pub struct Tag {
pub name: &'static str, // lintian-compatible kebab-case
pub severity: Severity,
pub certainty: Certainty,
pub experimental: bool,
pub description: &'static str, // shown by --info and --list-tags
pub references: &'static [&'static str],
}
```
Certainty exists so gates can be tuned later (`--fail-on error,certain` is
out of scope for v1, but the data is captured from day one).
### Check trait and registry
```rust
pub trait Check: Sync {
fn id(&self) -> &'static str; // "control", "changelog", ...
fn tags(&self) -> &'static [Tag];
fn run(&self, data: &LintData, emit: &mut Emitter);
}
```
Checks are zero-sized structs registered in one explicit array in
`checks/mod.rs` (`pub static CHECKS: &[&dyn Check] = &[&control::Control, ...]`),
in line with the codebase's low-magic style (no proc macros, no `inventory`
crate). A unit test walks the registry and fails on duplicate tag names,
missing descriptions, or malformed tag casing — the registry cannot rot
silently.
`Emitter` wraps the report and knows the tag registry; `emit.tag("name",
"detail")` looks up metadata once and applies overrides immediately, so
suppressed findings never reach the view. The engine shares the `report.rs`
ports philosophy: the emitter produces a `LintReport`; rendering
(text/JSON/quiet) is a separate concern.
### Collectors: `LintData`
One struct built per run, lazily where expensive (source trees are small;
lintian's on-disk lab is unnecessary at this stage):
```rust
pub struct LintData<'a> {
pub root: &'a Path,
pub dist: Option<String>, // resolved from --dist / changelog
pub series: Option<String>,
pub control: Option<ControlInfo>, // debian::control
pub changelog: Option<...>, // debian::changelog
pub deps: Option<Deps>, // debian::deps (Build-Depends etc.)
pub files: Vec<TreeEntry>, // walked tree: path, mode, size
pub source_format: Option<String>,
pub copyright: Option<Dep5Copyright>,
pub patches: Vec<PatchInfo>, // series + DEP-3 headers
pub read(&self, rel: &Path) -> Option<&str>, // cached file content
}
```
Collector failures (e.g. unparsable control) are themselves findings: the
collector records a tag and downstream checks that need that data are
skipped — exactly lintian's behavior.
### Overrides
Parse the formats and file names lintian already defines, so existing and
shared trees work with both tools unchanged:
- `debian/source.lintian-overrides` — applies to the source package
- `debian/<package>.lintian-overrides` — applies to that binary package's
tags (accepted in v1, enforced when binary tags land)
Syntax supported: `#` comments, bare `tag-name`, and `tag-name [restrictions]`
with arch/type restrictions parsed but honored only as far as native tags
go (source type only). Unknown tag names in override files emit an
`override-file-unknown-tag` info tag — cheap guard against typos (lintian
calls this out too).
### Distro scoping (assessment + design)
Ground truth from the installed lintian 2.129: lintian **is** distro-aware
via a vendor/profile mechanism, but Ubuntu's layer is thin. The
`ubuntu/main` profile only disables 14 Debian-specific tags (NMU
bookkeeping, `bugs-field-does-not-refer-to-debian-infrastructure`, upstart
leftovers, ...) and the `vendors/ubuntu/` data adds exactly one file
(`changes-file/known-dists`). On an Ubuntu host, lintian auto-selects the
ubuntu vendor via `/etc/dpkg/origins/default`. Consequences, visible on e.g.
`linux-riscv`:
- `unknown-field Ubuntu-Compatible-Signing` fires even on Ubuntu hosts:
lintian's derivative-field allowlist mechanism exists
(`data/fields/derivative-fields`, format `field ~~ regex ~~ explanation`),
Ubuntu just never shipped its fields in `vendors/ubuntu/`. The proper fix
is upstreaming to lintian (bug/merge proposal against Ubuntu's lintian);
pkh should carry a stopgap suppression meanwhile.
- `malformed-debian-changelog-version 7.2.0-5.5.2 (for native)` and
`binary-nmu-debian-revision-in-source`: lintian's changelog logic rejects
native-format-with-revision, which is how Ubuntu kernel packages are
shaped by convention. Profiles can only disable whole tags, not reshape
logic, so this class can't be fixed by lintian data — it is exactly the
pkh-side scoping below (and eventually our own native checks, which know
the dist).
- Ordinary Ubuntu backport versions (`...~26.04` tildes) are fine: tilde is
valid dpkg version syntax, and the ubuntu profile already disables the
Debian upload-version bookkeeping checks.
Design (phase 2, needs sign-off on the tag lists):
- **Distro → wrapper profile**: done in phase 1 — `-d ubuntu`/`-d debian`
passes `--profile` to lintian so the target distro's rules apply even on
a foreign host (auto-detection already covers the same-host case).
- **Distro-level suppression data** (`data/lint.yml`, embedded): per-dist
lists of tags to downgrade or drop for wrapper-origin findings, e.g.
ubuntu: `unknown-field` once the field list is upstreamed — deliberately
kept minimal; suppression is policy, not taste.
- **Package-level suppression via `quirks.yml`** (existing package-keyed
pattern), e.g. for the kernel family:
```yaml
linux-riscv:
lint:
suppressed_tags: [malformed-debian-changelog-version,
binary-nmu-debian-revision-in-source]
```
In-tree `debian/source.lintian-overrides` remains the package's own voice
(shared with lintian, already honored); quirks are pkh's voice for
conventions the ecosystem hasn't written down.
## Checks — first native batch
~20 tags across 7 check areas, chosen for what bites pkh users before an
upload. Severities below are the proposal; tuning happens in review.
**control** (`debian/control`):
- `missing-package-field` (E) — required source fields absent
- `maintainer-name-missing` / `maintainer-email-missing` (W) — malformed
Maintainer/Uploaders
- `unknown-priority` (E), `unknown-section` (W) — validated against
`data/` lists (new small embedded data files)
- `description-too-short` (I) — Description under ~20 chars
- `missing-tests-control` (I) — no `Testsuite: autopkgtest` nor
`debian/tests/`
- `binary-field-mismatch` (W) — Architecture/Section inconsistencies between
stanzas
**changelog** (uses `debian::changelog` + `debian::version`):
- `changelog-file-missing` (E) — no parsable `debian/changelog`
- `bad-urgent-value` (W) — urgency outside low/medium/high/critical
- `unreleased-changes-present` (W) — top entry is UNRELEASED
- `unknown-distribution` (W) — distribution not in the dist's series list
(reuses `distro_info` data!) nor a known alias
- `version-syntax-error` (E) — `DebianVersion::parse` failure
- `version-native-mismatch` (E) — native format with revision, or quilt
format with native version
**copyright**:
- `missing-debian-copyright-file` (E)
- `copyright-not-machine-readable` (I) — no DEP-5 header
- `missing-copyright-license-paragraph` (W) — files referenced but no
License stanza
**source_format**: `missing-debian-source-format` (W),
`non-3.0-source-format` (I)
**rules**: `debian-rules-not-executable` (E), `debian-rules-missing` (E),
`debian-rules-no-required-target` (W) — conservative static scan, no make
invocation
**pkh** (native only, the differentiator — lintian can't know these):
- `pkh-debian-changes-not-committed` (W) — git tree dirty in `debian/`
(pkh's whole flow is git-centric; catching "edited control, forgot to
commit" before `pkh put`)
- `pkh-new-template-drift` (I) — tree was scaffolded by `pkh new` and
template files have drifted from the current templates in `data/`
Note: `pkh new`'s existing structural self-checks should eventually be
rebuilt on this engine so there is one definition of "a healthy tree".
## Why not YAML for check logic
The question "can the 1,500 checks live in YAML?" decomposes into three
different things, with three different answers:
1. **Tag catalog: yes — and it is already data.** Lintian stores all 1,546
tag descriptions as flat files (`Tag/Severity/Check/Explanation/See-Also`).
Converting them to a YAML/JSON catalog is a mechanical script job — no
agentic engineering required. The native registry should load or generate
from this catalog so native tags and wrapper-parsed tags share one
metadata source.
2. **Static data tables: yes.** The 2.2 MB under lintian's `data/` (spelling
dictionaries, section/priority lists, debhelper command tables) is
exactly the kind of content `data/*.yml` + `embed_data!` exists for.
License applies (see risks): copying these files makes pkh GPL-derived.
3. **Check logic: no.** Even the *smallest* lintian check is code —
`Files/LdSo.pm` is one regex on a filename plus one condition on the
package name, as a Perl method; most of the 280 small checks are similar
("visit file X, apply conditions, emit tag"), and 18 checks are
400–1,089-line mini-programs with loops, cross-file state, package
relation graphs and version arithmetic. A DSL expressive enough for
those is a scripting language with an interpreter you now have to
maintain, debug, and document — worse than Rust, with no compiler
safety net. YAML is a data format, and check logic is not data.
Where YAML *does* fit in logic: a **thin predicate layer** for the simple
tail (field present/absent, file exists, glob match, regex on a field,
version comparison) — this is what `debian/pkh-lint.yml` (below) exposes
for user-specific rules. That covers a meaningful slice of the small
checks, but pursuing 100% parity through it would mean maintaining a
bespoke interpreter as big as the problem.
**Agentic engineering changes the economics of porting, not the target
representation.** LLM agents are good at mechanical translation
(Perl check → Rust check) and at writing the differential tests; the
correctness gate is the harness below, not the faithfulness of the
translation. Porting is per-check, priority-ordered, and reviewable —
compatible with the wrapper keeping parity in the meantime.
## Differential harness (the trust anchor for porting)
Before any ported check ships:
1. **Corpus**: a set of real source packages (the pkh pull flow already
fetches them) plus lintian's own test suite fixtures (in the lintian
source package).
2. **Record**: run real lintian over the corpus, store expected tag sets
per package.
3. **Diff**: `pkh lint --native` over the same corpus; report tag-set
deltas (missing / extra / severity-mismatch) per package. The CI gate
for ported checks is an empty delta on the covered corpus subset.
4. **Graduation**: a check is "native" once its delta is empty; the wrapper
continues covering everything else, and both-mode dedup by tag name
means graduation is invisible to users.
## Declarative rules (user-specific, no Rust)
The user-facing "add specific rules" path, mirroring the `quirks.yml`
philosophy: a small YAML file linted against the tree.
`debian/pkh-lint.yml`:
```yaml
rules:
- id: keep-changelog-synced
tag: pkh-changelog-sync-rule
severity: warning
when:
control-field-present: Vcs-Browser
expect:
file-present: debian/salsa-ci.yml
- id: no-embedded-js
tag: pkh-embedded-javascript
severity: error
expect:
no-file-matching: "*.min.js"
```
Phase 2; the predicates are the collector primitives (field present/absent,
file present/absent, glob, regex on a field). Custom `pkh-*` tags get
auto-generated catalog entries. This keeps the Rust registry for real
checks while giving package maintainers one-off rules.
## Output
- **Text**: lintian's `<L:> <pkg> <type>: <tag> <detail>` line format,
colors only when TTY (reuse the ui.rs conventions; `--info` appends
indented description + references). Summary line: `N: N tags (3 E, 5 W,
...), M overridden`.
- **JSON** (`--json`): array of `{tag, severity, certainty, experimental,
check, message, overridden, file?, line?, origin: native|lintian}` plus a
summary object — schema-versioned for CI use. Works identically in
wrapper and native modes (wrapper findings are parsed into the same
model).
- **Exit**: 0/1/2 as above; `--fail-on` decides the threshold
(`error` default; `error,warning` for upload gating).
## Testing
- **Wrapper parser**: golden tests against captured output of real lintian
(several packages, all display levels, overridden/`O:` and `C:` lines).
Version the fixtures by lintian version.
- **Differential harness**: as above; the CI gate for native checks.
- **Fixture trees** (`tests/fixtures/lint/<case>/`): minimal source trees
exercising one tag each, with a manifest of expected tags; negative
fixtures (clean trees, zero tags) live next to positive ones — same
pattern `pkh new` verification uses.
- **Registry lint test**: duplicate/undocumented tag names fail the test
suite.
- **Override parser**: table-driven tests against lintian's documented
syntax, including the weird corners ([restrictions], inline comments).
- **Golden text output**: one snapshot test of the renderer so formatting
changes are deliberate.
## Phases
1. **Wrapper v1 (parity day one)** — `src/lint/` skeleton, lintian
invocation + flag mapping + output parsing, unified report
(text + JSON, exit codes), native engine skeleton with the pkh-native
checks (`pkh-*`), CLI wiring. Feature parity achieved here.
2. **Native core checks + harness** — collectors, first native batch
(control/changelog/copyright/source_format/rules), overrides, tag
catalog import from lintian's tag files, differential harness in CI,
both-mode dedup. `pkh new` self-checks migrate onto the engine.
3. **Porting track (agentic, incremental)** — batches of lintian checks
ported Perl→Rust, priority-ordered by pkh-user relevance (source checks
first); each batch gated by the harness. Predicate layer +
`debian/pkh-lint.yml`. Data-table import once licensing is decided.
4. **Binary artifacts + unpack infra** — lint `.changes` + built `.deb`s
(temp unpack): file index unlocks the 157 installed-files checks family
(manpages, md5sums, maintainer scripts, init/systemd units, scripts).
The heavy checks (Debhelper, Scripts, Dep5…) and their data land here,
batch by batch under the harness. `<pkg>.lintian-overrides` enforcement;
optional `pkh deb --lint` and a `pkh put` upload gate.
## Risks & open questions
- **Licensing is a blocking decision for the porting track.** Lintian is
GPL-2+. Porting its check logic or copying its data files produces a
derivative work; pkh currently declares no license. Options: (a) pkh goes
GPL-2+ — porting and data import are clean; (b) pkh stays unlicensed/
permissive — then only the wrapper plus independently-authored checks
(the `pkh-*` set) are possible, and the porting track is off the table.
The wrapper alone is always fine (subprocess boundary, no derivative).
This decision gates phase 3, not phases 1–2.
- **Wrapper parsing depends on lintian's output format.** Stable for years
and pinned to the installed version, but a lintian major change would
break wrapper mode; mitigated by golden fixtures and the native track
reducing dependency over time.
- **False positives on PPA workflows.** Lintian targets archive uploads;
some checks are noise for quick PPA builds. Mitigation: quiet default
display level, `--fail-on error` default, `pkh-*` severities tuned for
the pkh flow.
- **Porting velocity is the open question of phase 3.** The harness makes
each port verifiable, but 346 checks (and their data) is a campaign, not
a sprint; the wrapper exists precisely so this never blocks users.
- **Tag-name mirroring is a commitment.** Reusing lintian names buys
familiarity, shared override files, and trivial both-mode dedup — at the
cost of renames breaking overrides. Accepted; lintian tag names are
stable.
- **Overlap with `pkh new` self-checks** — migrating them onto the engine
needs a small "run subset of checks programmatically" API; the
library-first design allows it but it should be an explicit decision when
touched.