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.
The interface
Section titled “The interface”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) DetectorParseTestFailures powers the last_test_failures tool. Detectors with no parser (v1: everyone except Go) return nil — the tool surfaces a clear “not supported for
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).
Go implementation
Section titled “Go implementation”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", "./..."} }Adding a new language
Section titled “Adding a new language”-
Pick markers.
package.jsonfor Node,pyproject.tomlorsetup.pyfor Python,Cargo.tomlfor Rust. -
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"} } -
Register in
Detect. Add a check for the marker in the if/else chain ininternal/verify/verify.go. -
Docker image. Add the runtime (e.g.
apk add nodejs npm) to the Dockerfile. -
Parser. If the new linter uses a different output format than
<file>:<line>:<col>: <msg> (<rule>), updateverify.ParseLint(or add a per-detectorParseLintmethod).
Parser limits
Section titled “Parser limits”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).
Shipped detectors
Section titled “Shipped detectors”| Language | Marker | Test | Lint | Typecheck |
|---|---|---|---|---|
| Go | go.mod | go test -json -count=1 ./... | golangci-lint run ./... | go vet ./... |
| Rust | Cargo.toml | cargo test | cargo clippy --all-targets -- -D warnings | cargo check --all-targets |
| Node | package.json | npm test --silent | npx --no-install eslint . --format=compact | npx --no-install tsc --noEmit |
| Python | pyproject.toml / setup.py | pytest | ruff 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.
Binary availability
Section titled “Binary availability”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 returnsErrorResult("<tool>: <binary>: not found on PATH"). - For
run_lint— the handler returnslinter not installed: <binary>(theErrLinterMissingsentinel), 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.
Lint-output parsing
Section titled “Lint-output parsing”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:
| Detector | Linter | Emits findings on | Rule capture |
|---|---|---|---|
| Go | golangci-lint v2 | stdout | rule name, e.g. errcheck |
| Python | ruff | stdout | rule code, e.g. F401 |
| Node | eslint (—format=compact) | stdout | rule name, e.g. semi / no-unused-vars |
| Rust | cargo clippy (—message-format=short) | stderr | severity (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.
Adding a detector
Section titled “Adding a detector”- Create
internal/verify/<language>.gowith a struct implementing the fiveDetectormethods. - Extend
Detectininternal/verify/verify.goto check for the marker file (specific markers first). - Write the
ParseLintimplementation. If the linter’s output resembles<file>:<line>:<col>: <msg> (<rule>)or similar, reuseparseLintRegexwith a tailored regex. Otherwise write a custom parser. - Update the Dockerfile / example image to install the runtime.
- Add unit tests: marker detection (cheap), parser regex against sample output (cheap), and optionally a live integration test behind a
requireXxx(t)skip helper (likerequireGolangciLint) for CI environments that have the tool installed.