Skip to content

Detector interface

The verify.Detector interface decouples the verify tools from specific toolchains. The sandbox ships four detectors — Go, Rust, Node, Python — that automatically pick the right commands based on which marker file (go.mod, Cargo.toml, package.json, pyproject.toml / setup.py) is at the workspace root.

type Detector interface {
Language() string // "go", "node", "python", ...
TestCmd() []string // argv for the test runner
LintCmd() []string // argv for the linter
TypecheckCmd() []string // argv for the type checker
ParseLint(stdout, stderr string) []LintFinding
ParseTestFailures(stdout, stderr string) []TestFailure
}
func Detect(root string) Detector

ParseTestFailures powers the last_test_failures tool. Detectors with no parser (v1: everyone except Go) return nil — the tool surfaces a clear “not supported for ” notice rather than silently claiming zero failures.

Detect inspects the workspace root for marker files and returns the first matching detector, or nil if none are found. Only the immediate root is inspected; markers in subdirectories don’t count (the workspace root is the authoritative anchor).

Marker: go.mod in the workspace root.

type goDetector struct { root string }
func (*goDetector) Language() string { return "go" }
func (*goDetector) TestCmd() []string { return []string{"go", "test", "-json", "-count=1", "./..."} }
func (*goDetector) LintCmd() []string { return []string{"golangci-lint", "run", "./..."} }
func (*goDetector) TypecheckCmd() []string { return []string{"go", "vet", "./..."} }
  1. Pick markers. package.json for Node, pyproject.toml or setup.py for Python, Cargo.toml for Rust.

  2. Implement Detector. Example for Node:

    type nodeDetector struct{ root string }
    func (*nodeDetector) Language() string { return "node" }
    func (*nodeDetector) TestCmd() []string { return []string{"npm", "test", "--silent"} }
    func (*nodeDetector) LintCmd() []string { return []string{"npx", "eslint", ".", "--format=compact"} }
    func (*nodeDetector) TypecheckCmd() []string { return []string{"npx", "tsc", "--noEmit"} }
  3. Register in Detect. Add a check for the marker in the if/else chain in internal/verify/verify.go.

  4. Docker image. Add the runtime (e.g. apk add nodejs npm) to the Dockerfile.

  5. Parser. If the new linter uses a different output format than <file>:<line>:<col>: <msg> (<rule>), update verify.ParseLint (or add a per-detector ParseLint method).

verify.ParseLint is currently golangci-lint-specific. It uses a non-greedy regex to correctly isolate the trailing (rule) suffix even when the message contains parentheses. The sk-ant- problem from the scrub package doesn’t apply here (no two linters share an output shape), but when a second detector lands the parser should be generalised.

Today’s exit-code convention assumption: exit 1 means “findings exist” (golangci-lint), exit ≥ 2 means “genuine failure.” That’s NOT universal — eslint exits 1 for warnings AND errors, ruff exits 1 for any finding. When Node / Python detectors land, the exit-code semantics should move into Detector (e.g. LintErrorThreshold() int).

LanguageMarkerTestLintTypecheck
Gogo.modgo test -json -count=1 ./...golangci-lint run ./...go vet ./...
RustCargo.tomlcargo testcargo clippy --all-targets -- -D warningscargo check --all-targets
Nodepackage.jsonnpm test --silentnpx --no-install eslint . --format=compactnpx --no-install tsc --noEmit
Pythonpyproject.toml / setup.pypytestruff check .mypy .

Detection order (first match wins): Go → Rust → Node → Python. A Go service with a frontend package.json detects as Go — operators whose workspaces need the frontend detected should split them into separate workspace roots.

The sandbox invokes the commands above with exec.Command. When a binary isn’t on PATH:

  • For run_tests/run_typecheck — the spawn fails and the handler returns ErrorResult("<tool>: <binary>: not found on PATH").
  • For run_lint — the handler returns linter not installed: <binary> (the ErrLinterMissing sentinel), naming the missing binary so operators can tell whether it’s a dev-env or Docker-image misconfiguration.

This is how the tools-layer Docker pattern works: the sandbox binary is language-agnostic and just invokes commands; the operator’s base image provides the toolchain. A Python image without pytest installed returns a clear error instead of silently falling back.

Each detector implements ParseLint(stdout, stderr string) []LintFinding to convert its linter’s raw output into structured records. Two strings are passed because linters differ in which stream they use:

DetectorLinterEmits findings onRule capture
Gogolangci-lint v2stdoutrule name, e.g. errcheck
Pythonruffstdoutrule code, e.g. F401
Nodeeslint (—format=compact)stdoutrule name, e.g. semi / no-unused-vars
Rustcargo clippy (—message-format=short)stderrseverity (warning or error) — clippy’s short format drops the rule name

Unrecognised lines (context, summary blocks, cargo’s “Checking…” banner) are silently skipped. Each parser is unit-tested against sample output in parser_test.go.

  1. Create internal/verify/<language>.go with a struct implementing the five Detector methods.
  2. Extend Detect in internal/verify/verify.go to check for the marker file (specific markers first).
  3. Write the ParseLint implementation. If the linter’s output resembles <file>:<line>:<col>: <msg> (<rule>) or similar, reuse parseLintRegex with a tailored regex. Otherwise write a custom parser.
  4. Update the Dockerfile / example image to install the runtime.
  5. Add unit tests: marker detection (cheap), parser regex against sample output (cheap), and optionally a live integration test behind a requireXxx(t) skip helper (like requireGolangciLint) for CI environments that have the tool installed.