CtrlK
BlogDocsLog inGet started
Tessl Logo

rockcrafters/mason

Agent kit for working on canonical/chisel-releases. Cross-agent skills + scripts for authoring and reviewing chisel slice definition files.

73

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

check-slice.pyskills/chisel-releases/scripts/

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["pyyaml"]
# ///
"""check-slice: deterministic static linter for chisel slice definition files.

Checks the mechanical conventions reviewers and CI enforce -- the rules that do
NOT need judgement or a rootfs cut. Use it two ways:

  - authoring: self-check an SDF before commit (write-slice step 8).
  - review:    the deterministic first pass of a review (review-slice), so the
               agent only spends judgement on deps, tests, and design.

It does NOT replace `chisel cut` (installability) or spread tests
(functionality). It only checks the SDF text. yamllint cosmetics (line
length <= 100, blank-line count, indent) are also NOT covered -- CI's lint
job runs yamllint separately.

Usage:
  check-slice.py <slice.yaml> [<slice2.yaml> ...] [--format N | --branch NAME]

Format detection (needed for version-gated fields hint/prefer/v3-essential and
essential-as-map): --format wins, else --branch maps to a format, else the
format: field of ./chisel.yaml, else unknown (gated checks are skipped, noted).

Output: one finding per line, `SEVERITY  file: where: message`.
  block  hard gate -- chisel won't parse it, or CI/lint fails. exit code 1.
  warn   reviewers reliably reject this. exit code stays 0.
  info   nit, or a check skipped because format is unknown.

Exit code is 1 if any block finding, else 0. Stdlib + pyyaml only; runs under
`uv run` (per the shebang) or `python3 check-slice.py` if pyyaml is importable.
"""
from __future__ import annotations

import re
import sys
from pathlib import Path
from typing import Any

import yaml

# known-bad slice names reviewers reject, mapped to the right one. by-function
# and per-binary names (e.g. printing-text, ls-bin, journal) are legitimate, so
# only these exact mistakes are flagged -- not "anything off a fixed vocabulary".
BAD_SLICE_NAMES = {"bin": "bins", "lib": "libs", "all": "core"}

# Debian arch names -- always lowercase, never x86_64/aarch64. (arch-list order
# is not enforced: real SDFs use a priority order, not alphabetical.)
VALID_ARCHES = {"amd64", "arm64", "armhf", "i386", "ppc64el", "riscv64", "s390x"}

# branch -> chisel.yaml format version (mirrors tests/scorers/_lib.py).
BRANCH_FORMAT = {
    "ubuntu-20.04": 1, "ubuntu-22.04": 1, "ubuntu-24.04": 1,
    "ubuntu-25.10": 2, "ubuntu-26.04": 3,
}

# chisel's own slice-name rule (internal/setup: SnameExp) -- no trailing or
# doubled hyphens, which a plain charset class would let through.
SNAME_RE = re.compile(r"^[a-z](?:-?[a-z0-9]){2,}$")

# chisel's SDF filename rule (internal/setup: FnameExp). a non-matching .yaml
# basename fails the whole release parse; a non-.yaml file is silently ignored.
FNAME_RE = re.compile(r"^[a-z0-9](?:-?[.a-z0-9+]){1,}\.yaml$")

# paths a minimal rootfs never needs -- see shared/CHISEL.md "Exclude by Default".
CLUTTER = {
    "man pages": ("/usr/share/man/", "/usr/man/"),
    "shell completions": (
        "/usr/share/bash-completion/", "/usr/share/fish/",
        "/usr/share/zsh/", "/etc/bash_completion.d/",
    ),
    "doc-base/lintian metadata": ("/usr/share/doc-base/", "/usr/share/lintian/"),
}

# legal files that legitimately live under /usr/share/doc alongside copyright
# (not clutter). basename stem, uppercased, after stripping a text/compress suffix.
LEGAL_DOC = {
    "COPYRIGHT", "NOTICE", "LICENSE", "LICENCE", "COPYING",
    "AUTHORS", "THIRDPARTYNOTICES", "THIRD-PARTY-NOTICES",
}


def is_legal_doc(basename: str) -> bool:
    stem = basename
    for suf in (".gz", ".xz", ".bz2", ".txt", ".md", ".rst"):
        if stem.lower().endswith(suf):
            stem = stem[: -len(suf)]
    return stem.upper() in LEGAL_DOC


class Findings:
    def __init__(self, file: str) -> None:
        self.file = file
        self.rows: list[tuple[str, str, str]] = []

    def add(self, sev: str, where: str, msg: str) -> None:
        self.rows.append((sev, where, msg))

    def block(self, where: str, msg: str) -> None:
        self.add("block", where, msg)

    def warn(self, where: str, msg: str) -> None:
        self.add("warn", where, msg)

    def info(self, where: str, msg: str) -> None:
        self.add("info", where, msg)


def slices_of(doc: Any) -> dict:
    s = doc.get("slices") if isinstance(doc, dict) else None
    return s if isinstance(s, dict) else {}


def is_sorted(keys: list) -> bool:
    strs = [k for k in keys if isinstance(k, str)]
    return strs == sorted(strs)


def check_filename(doc: Any, path: Path, f: Findings) -> None:
    if path.name.endswith(".yml"):
        f.block("filename", "chisel silently ignores non-.yaml files -- rename to .yaml")
    elif not FNAME_RE.match(path.name):
        f.block("filename", f"'{path.name}' fails chisel's filename rule {FNAME_RE.pattern} (whole-release parse error)")
    stem = path.name[:-5] if path.name.endswith(".yaml") else path.stem
    pkg = doc.get("package") if isinstance(doc, dict) else None
    if not isinstance(pkg, str) or not pkg:
        f.block("package:", "missing top-level package: field")
    elif pkg != stem:
        f.block("package:", f"package: '{pkg}' != filename stem '{stem}'")


def check_essential_sorted(doc: Any, f: Findings) -> None:
    # per-slice essential + contents sorting is the CI lint gate (LC_COLLATE=C,
    # sort -C). the top-level essential is not CI-gated, but keep it sorted for
    # consistency -- so warn, don't block.
    if isinstance(doc.get("essential"), list):
        if not is_sorted(doc["essential"]):
            f.warn("essential:", "global essential entries not sorted")
    for name, body in slices_of(doc).items():
        ess = body.get("essential") if isinstance(body, dict) else None
        if isinstance(ess, list) and not is_sorted(ess):
            f.block(f"{name}.essential", "essential entries not sorted")
        elif isinstance(ess, dict) and not is_sorted(list(ess.keys())):
            f.block(f"{name}.essential", "essential map keys not sorted")


def check_slices(doc: Any, fmt: int | None, f: Findings) -> None:
    slices = slices_of(doc)
    if not slices:
        f.block("slices:", "no slices: map -- not a slice definition file")
        return
    pkg = doc.get("package") if isinstance(doc.get("package"), str) else ""

    # copyright: a strong convention, not a CI parse/lint gate (some merged
    # library SDFs skip it), so warn. it ships either the copyright file
    # directly, or the doc dir as a symlink to another package's (shared).
    cp = slices.get("copyright")
    if not isinstance(cp, dict):
        f.warn("slices:", "no copyright slice -- reviewers expect every SDF to ship one")
    elif pkg:
        contents = cp.get("contents") if isinstance(cp.get("contents"), dict) else {}
        docdir = f"/usr/share/doc/{pkg}"
        if not any(k in contents for k in (f"{docdir}/copyright", docdir, docdir + "/")):
            f.warn("copyright", f"ships no copyright ({docdir}/copyright, or the doc dir as a shared-copyright symlink)")
    ess = doc.get("essential")
    if pkg and not (isinstance(ess, list) and f"{pkg}_copyright" in ess) \
            and not (isinstance(ess, dict) and f"{pkg}_copyright" in ess):
        f.warn("essential:", f"{pkg}_copyright not in global essential (so not every slice ships it)")

    for name, body in slices.items():
        if not isinstance(name, str) or not SNAME_RE.match(name):
            f.block(f"slices.{name}", f"slice name must match {SNAME_RE.pattern} (chisel parse error)")
        if not isinstance(body, dict):
            continue
        check_slice_body(name, body, pkg, fmt, f)


# validate-hints allows only these chars; chisel core caps length at 40 and
# rejects non-printable. see shared/CHISEL.md "hint: style".
_HINT_ALLOWED = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,;()")


def check_hint(name: str, hint: Any, f: Findings) -> None:
    if not isinstance(hint, str):
        return
    where = f"{name}.hint"
    # chisel core (parse errors):
    if len(hint) > 40:
        f.block(where, f"hint is {len(hint)} chars -- chisel caps it at 40 (parse error)")
    if "\n" in hint or any(not c.isprintable() for c in hint):
        f.block(where, "hint must be a single line of printable chars (parse error)")
    # validate-hints CI style (noun phrase; the finite-verb rule needs NLP, skipped):
    if hint[:1].islower():
        f.warn(where, "hint should be sentence case (capitalise the first letter)")
    if re.match(r"(?i)^(a|an|the)\b", hint):
        f.warn(where, "hint should not start with an article (a/an/the)")
    if hint[-1:] in {".", ",", ";", ":", "!"}:
        f.warn(where, "hint should not end with punctuation")
    if "  " in hint or hint != hint.strip():
        f.warn(where, "hint should have no double, leading, or trailing spaces")
    stray = sorted(set(hint) - _HINT_ALLOWED - {"\n"})
    if stray:
        f.warn(where, f"hint has chars outside validate-hints' set {stray}; allowed: letters, digits, space, . , ; ( )")


def check_slice_body(name: str, body: dict, pkg: str, fmt: int | None, f: Findings) -> None:
    if name in BAD_SLICE_NAMES:
        f.warn(f"slices.{name}", f"use '{BAD_SLICE_NAMES[name]}' not '{name}'")

    if "hint" in body:
        if fmt is not None and fmt < 3:
            f.block(f"{name}.hint", f"hint: is v3+ only (format is v{fmt})")
        else:
            check_hint(name, body["hint"], f)

    contents = body.get("contents")
    if not isinstance(contents, dict):
        return
    keys = list(contents.keys())
    if not is_sorted(keys):
        f.block(f"{name}.contents", "contents paths not sorted (bytewise/ASCII)")
    for path, entry in contents.items():
        check_path(name, path, entry, pkg, fmt, f)


def check_path(sname: str, path: Any, entry: Any, pkg: str, fmt: int | None, f: Findings) -> None:
    if not isinstance(path, str) or not path.startswith("/"):
        f.block(f"{sname}: {path}", "path must be absolute (start with /)")
        return
    for label, prefixes in CLUTTER.items():
        if any(path.startswith(p) for p in prefixes):
            f.warn(f"{sname}: {path}", f"{label} not shipped in minimal rootfs")
    if path.startswith("/usr/share/doc/"):
        docdir = f"/usr/share/doc/{pkg}"
        base = path.rstrip("/").rsplit("/", 1)[-1]
        # exempt the package's own doc dir (shared-copyright symlink) and legal files.
        if pkg and path.rstrip("/") != docdir and not is_legal_doc(base):
            f.warn(f"{sname}: {path}", "doc clutter: ship only the copyright/notice/licence files")

    if not isinstance(entry, dict):
        return
    if "prefer" in entry and (fmt is not None and fmt < 2):
        f.block(f"{sname}: {path}", f"prefer: is v2+ only (format is v{fmt})")
    arch = entry.get("arch")
    archs = [arch] if isinstance(arch, str) else arch if isinstance(arch, list) else None
    if archs is not None:
        bad = [a for a in archs if a not in VALID_ARCHES]
        if bad:
            f.block(f"{sname}: {path}", f"invalid arch name(s) {bad}: use Debian names {sorted(VALID_ARCHES)} (chisel parse error)")


def check_v3_essential(doc: Any, fmt: int | None, f: Findings) -> None:
    # essential shape is format-gated both ways: map on v1/v2 is a parse error,
    # list on v3 is a parse error ("essential expects a map"). v3-essential is
    # the pre-v3 backport (needs chisel>=1.3.0): valid on v1/v2, a hard parse
    # error on v3 (top-level or per-slice).
    if fmt is None:
        return
    if "v3-essential" in doc and fmt >= 3:
        f.block("v3-essential:", f"v3-essential is rejected on v3 (chisel parse error) -- fold into the essential: map (format is v{fmt})")
    if isinstance(doc.get("essential"), dict) and fmt < 3:
        f.block("essential:", f"essential-as-map is v3+ only (format is v{fmt})")
    if isinstance(doc.get("essential"), list) and fmt >= 3:
        f.block("essential:", f"essential must be a map on v3 (chisel parse error: 'essential expects a map'; format is v{fmt})")
    for name, body in slices_of(doc).items():
        if not isinstance(body, dict):
            continue
        if "v3-essential" in body and fmt >= 3:
            f.block(f"{name}.v3-essential", f"v3-essential is rejected on v3 (chisel parse error) -- fold into the essential: map (format is v{fmt})")
        ess = body.get("essential")
        if isinstance(ess, dict) and fmt < 3:
            f.block(f"{name}.essential", f"essential-as-map is v3+ only (format is v{fmt})")
        elif isinstance(ess, list) and fmt >= 3:
            f.block(f"{name}.essential", f"essential must be a map on v3 (chisel parse error: 'essential expects a map'; format is v{fmt})")


def detect_format(argv_fmt: int | None, argv_branch: str | None) -> int | None:
    if argv_fmt is not None:
        return argv_fmt
    if argv_branch:
        return BRANCH_FORMAT.get(argv_branch)
    cy = Path("chisel.yaml")
    if cy.exists():
        try:
            doc = yaml.safe_load(cy.read_text(encoding="utf-8"))
            v = str(doc.get("format", "")) if isinstance(doc, dict) else ""
            m = re.search(r"(\d+)", v)
            if m:
                return int(m.group(1))
        except (yaml.YAMLError, OSError):
            pass
    return None


class _StrictLoader(yaml.SafeLoader):
    """SafeLoader that errors on duplicate mapping keys (safe_load keeps the
    last one silently, so a pasted-twice contents path is invisible to it --
    but CI's yamllint fails on it)."""


def _no_dup_keys(loader, node, deep=False):
    seen = set()
    for key_node, _ in node.value:
        key = loader.construct_object(key_node, deep=deep)
        if key in seen:
            raise yaml.YAMLError(f"duplicate mapping key {key!r} (line {key_node.start_mark.line + 1})")
        seen.add(key)
    return yaml.SafeLoader.construct_mapping(loader, node, deep)


_StrictLoader.add_constructor(
    yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_dup_keys)


def check_file(path: Path, fmt: int | None) -> Findings:
    f = Findings(str(path))
    try:
        text = path.read_text(encoding="utf-8")
    except OSError as e:
        f.block("", f"cannot read: {e}")
        return f
    try:
        doc = yaml.load(text, Loader=_StrictLoader)
    except yaml.YAMLError as e:
        f.block("", f"YAML parse error: {str(e).splitlines()[0]}")
        return f
    if not isinstance(doc, dict):
        f.block("", "top level is not a mapping")
        return f
    check_filename(doc, path, f)
    check_essential_sorted(doc, f)
    check_slices(doc, fmt, f)
    check_v3_essential(doc, fmt, f)
    if fmt is None:
        f.info("", "format unknown: version-gated checks (hint/prefer/v3-essential/essential-map) skipped -- pass --format or --branch")
    return f


def main(argv: list[str]) -> int:
    files: list[str] = []
    fmt_arg: int | None = None
    branch_arg: str | None = None
    def parse_fmt(value: str) -> int:
        # accept "3", "v3", "chisel-v3" -- orientation prints "v1"-style strings.
        m = re.search(r"(\d+)", value)
        if not m:
            print(f"check-slice.py: bad --format value {value!r}", file=sys.stderr)
            print("usage: check-slice.py <slice.yaml> [...] [--format N | --branch NAME]", file=sys.stderr)
            raise SystemExit(2)
        return int(m.group(1))

    it = iter(argv)
    for a in it:
        if a in ("--format", "--branch"):
            value = next(it, None)
            if value is None:
                print(f"check-slice.py: {a} needs a value", file=sys.stderr)
                return 2
            if a == "--format":
                fmt_arg = parse_fmt(value)
            else:
                branch_arg = value
        elif a.startswith("--format="):
            fmt_arg = parse_fmt(a.split("=", 1)[1])
        elif a.startswith("--branch="):
            branch_arg = a.split("=", 1)[1]
        elif a in ("-h", "--help"):
            print(__doc__)
            return 0
        else:
            files.append(a)
    if not files:
        print("usage: check-slice.py <slice.yaml> [...] [--format N | --branch NAME]", file=sys.stderr)
        return 2

    fmt = detect_format(fmt_arg, branch_arg)
    had_block = False
    total = 0
    for name in files:
        f = check_file(Path(name), fmt)
        for sev, where, msg in f.rows:
            total += 1
            had_block = had_block or sev == "block"
            loc = f"{f.file}: {where}: " if where else f"{f.file}: "
            print(f"{sev:5}  {loc}{msg}")
    if total == 0:
        print(f"ok: {len(files)} file(s) clean" + (f" (format v{fmt})" if fmt else ""))
    return 1 if had_block else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

tile.json