Documentation
¶
Overview ¶
Package boundarylint is a small, extensible policy engine for "boundary tells": source patterns where the code makes a claim about the outside world (the OS, the network, the clock, a peer process) without the check that would make the claim true. It is the DOS witness idea turned on the codebase itself — "the kernel is the part that doesn't believe the agents," here applied to the author: a green build is a self-report, and these tells are where a green build hides a latent boundary failure.
pathlint (UNEXPANDED_USER_PATH) and urllint (UNVERIFIED_EXTERNAL_URL) were the first two such witnesses, each in its own package. This package is the registry they generalize into: each tell is a Rule with a closed-vocabulary Code, the scanner runs them in a single pass, and TestBoundaryPolicy enforces the whole family at once. See catalog.go for the full set of tells — enforced and proposed.
A finding can be suppressed in place with a line comment:
resp, err := http.Get(url) //boundarylint:ignore MISSING_HTTP_TIMEOUT one-shot localhost probe
as a trailing comment on the offending line. Suppressions are deliberately visible and greppable, so an exception is a recorded decision, not a silent gap.
Index ¶
Constants ¶
const CodeUnparseableSource = "UNPARSEABLE_SOURCE"
CodeUnparseableSource is the closed-vocabulary code for a source file the scanner could not parse, and therefore could not lint.
Variables ¶
var Catalog = []CatalogEntry{ { Code: "UNEXPANDED_USER_PATH", Title: "path flag opened without ~ expansion", Status: StatusEnforced, Note: "enforced by internal/pathlint. A -gguf/-hf/-tok/-dir flag given as ~/x is opened as a literal '~' dir.", }, { Code: "UNVERIFIED_EXTERNAL_URL", Title: "hardcoded download URL outside the audited builder", Status: StatusEnforced, Note: "enforced by internal/urllint. A pasted model-download URL literal escapes the reachability-tested chokepoint.", }, { Code: "MISSING_HTTP_TIMEOUT", Title: "outbound HTTP with no timeout", Status: StatusEnforced, Note: "http.Get/Post/Head/PostForm and http.DefaultClient cannot carry a timeout; a dead peer hangs forever.", }, { Code: "CHANGE_DETECTOR_TEST", Title: "test freezes a current value instead of asserting an invariant", Status: StatusEnforced, Note: "enforced by DefaultTestRules over _test.go. A magic enumeration count (len(verbs)==109), a wholly-literal list equality, or a pinned version string passes/fails on churn, not correctness — assert the relation the value must hold, or //boundarylint:ignore CHANGE_DETECTOR_TEST a deliberate fixed-width invariant.", }, { Code: "SKIP_DEBT", Title: "test skips itself with no platform/short/env guard", Status: StatusSoft, Note: "SOFT (not gated): a bare t.Skip/Skipf/SkipNow removes a test from the suite unconditionally, so a presence KPI still counts it while the body never runs. A skip guarded by testing.Short()/runtime.GOOS/os.Getenv is an honest conditional and is not flagged; a deliberate always-skip is //boundarylint:ignore SKIP_DEBT with a tracking issue. Reported by `fak boundary` and folded as a qa-process scorecard KPI, never a build gate.", }, { Code: "UNPARSEABLE_SOURCE", Title: "source the linter could not parse, reported as a skip instead of a clean pass", Status: StatusSoft, Note: "SOFT (not gated): a file parser.ParseFile cannot read yields zero findings, which is indistinguishable from a clean file — the scanner reports success over source it never read. ScanUnparseable records the skip so it is visible and greppable; the inverse of the fail-OPEN default, and the detector-side analogue of the adjudicator's MALFORMED on undecidable input. Kept SOFT because a shared peer-dirty trunk carries half-written .go files from live sessions; the compiler stays the authority on validity. Reported by `fak boundary`, never a build gate.", }, { Code: "UNCHECKED_HTTP_STATUS", Title: "response body used without checking StatusCode", Status: StatusProposed, Note: "FP hazard: track the resp var per-function and allow code that returns/branches on StatusCode in any form before reading Body.", }, { Code: "IGNORED_BOUNDARY_ERROR", Title: "error from a boundary call discarded with _", Status: StatusProposed, Note: "scope to a boundary-call allowlist (os.Open/ReadFile, exec *.Run/Output, http *.Do) so ordinary _-drops (fmt.Fprintln) aren't flagged.", }, { Code: "UNCLOSED_RESPONSE_BODY", Title: "http.Response.Body never closed", Status: StatusProposed, Note: "the bodyclose analyzer already does this well; wire it in rather than re-implementing the escape analysis.", }, { Code: "ASSUMED_EXECUTABLE", Title: "exec.Command on a literal binary not preflighted", Status: StatusProposed, Note: "needs an allowlist: git/go are fair to assume; curl/node/npm/npx/python/playwright are environment claims that deserve a LookPath preflight with a clear error.", }, { Code: "MANUAL_PATH_SEPARATOR", Title: "filesystem path built by string-joining with '/'", Status: StatusProposed, Note: "the class the original ~ bug came from. FP hazard: distinguish filesystem paths from URLs/format strings — only flag concat that flows into os.Open/Stat/Create.", }, { Code: "UNVALIDATED_ENV", Title: "os.Getenv consumed without a default or validation", Status: StatusProposed, Note: "60+ call sites; only valuable with a severity/allowlist model, else it's noise.", }, { Code: "NONDETERMINISTIC_DEFAULT", Title: "time.Now()/rand seeding a value that should be reproducible", Status: StatusProposed, Note: "reproducibility tell rather than a network/OS one; narrow to seeds/IDs that feed persisted or compared output.", }, }
Catalog is the full family of boundary tells — the "policy by default" view. Enforced entries are checked in CI (here, or in pathlint/urllint for the first two); proposed entries are the prioritized backlog, each annotated with the FP hazard that must be handled before it can graduate to enforced without crying wolf.
Functions ¶
This section is empty.
Types ¶
type CatalogEntry ¶
type CatalogEntry struct {
Code string
Title string
Status Status
Note string // why it's a tell, and (for proposed) the false-positive hazard to solve first
}
CatalogEntry documents one boundary tell.
type ChangeDetectorTest ¶ added in v0.38.0
type ChangeDetectorTest struct{}
ChangeDetectorTest flags test assertions that freeze a current value instead of asserting how two pieces of data must relate: a magic enumeration count (len(verbs) != 109), a wholly-literal list equality (reflect.DeepEqual against a six-element literal), or a pinned version string (version == "v1.42.7"). Such a test passes/fails on churn, not on correctness — it goes red when the enumeration legitimately grows and stays green when a real relation breaks — so it rots into a change detector the suite drags along. This is the same boundary-tell shape as the rest of the family: the assertion CLAIMS "this value is correct" while only checking "this value is what it was the day the test was written".
The fix is an invariant: relate the count to the thing it must track (every dispatch verb has a help entry), relate the list to its source of truth, assert the version PARSES and orders after the previous release rather than equalling a literal. A deliberate fixed-width check (sha256 hex is 64 bytes) is a real invariant — suppress it in place with //boundarylint:ignore CHANGE_DETECTOR_TEST and the reason, so the exception is a recorded decision.
func (ChangeDetectorTest) Code ¶ added in v0.38.0
func (ChangeDetectorTest) Code() string
Code returns this rule's stable finding code, "CHANGE_DETECTOR_TEST".
type Finding ¶
type Finding struct {
Code string // closed-vocabulary reason, e.g. "MISSING_HTTP_TIMEOUT"
File string // repo-relative, slash-separated
Line int
Detail string // what was found and how to resolve it
}
Finding is one boundary tell at one source location.
func Scan ¶
Scan walks each root, parses every non-test Go file once, runs rules, and drops any finding suppressed by a //boundarylint:ignore comment on its line or the line above.
func ScanNewChangeDetectors ¶ added in v0.38.0
ScanNewChangeDetectors runs DefaultTestRules over root's cmd/ and internal/ test trees and returns only findings in files NOT grandfathered in changeDetectorBaseline — the NEW change-detectors the ratchet fails on. Each returned Finding.File is rewritten to a repo-relative slash path so it reads the same as the baseline. This is the single shared entrypoint for both TestTestSuitePolicy (the gate) and `fak boundary` (the report), so the shrink-only ratchet is defined in exactly one place.
func ScanTests ¶ added in v0.38.0
ScanTests is Scan's counterpart for the test suite: it walks ONLY _test.go files. The two walks partition the tree so a rule runs over exactly the file class its tell is about — DefaultRules over production source, DefaultTestRules over tests — with the same skip-dirs and the same //boundarylint:ignore suppression contract.
func ScanUnparseable ¶ added in v0.42.0
ScanUnparseable walks each root and reports every .go file that parser.ParseFile could not read — the scanner's own blind spot, surfaced as a recorded skip.
The tell it closes is a fail-OPEN default. Every other walk here silently drops an unparseable file ("the compiler owns that error"), so such a file contributes zero findings and is indistinguishable from a genuinely clean one: the linter reports success over source it never actually read. That is the inverse of the fail-CLOSED discipline the kernel applies to undecidable input elsewhere (internal/adjudicator's argcanon returns MALFORMED rather than a default-allow when an arg's quotes never close). A detector that cannot decide should say so, not say "nothing found".
UNPARSEABLE_SOURCE is a SOFT signal, the same class as SKIP_DEBT: it is reported by `fak boundary` and never gates. That is deliberate rather than a weaker compromise — this is a shared, permanently peer-dirty trunk where half-written .go files from other live sessions are normal, so failing the build on one would red the trunk for everyone on a peer's in-flight edit while saying nothing about the committed tree. The compiler (and `fak buildcheck`) remain the authority on whether source is valid; this witness only ensures the LINTER's silence is never mistaken for a clean bill of health.
It walks both production and test sources, since Scan (non-test) and ScanTests (_test.go) each skip unparseable files in their own half of the tree.
Findings carry no //boundarylint:ignore suppression: the directive is collected from the parsed AST, which by definition does not exist here. A soft, never-gating tell needs no escape hatch — the fix is to make the file parse, or to let it leave the tree.
type MissingHTTPTimeout ¶
type MissingHTTPTimeout struct{}
MissingHTTPTimeout flags outbound HTTP made through an API that cannot carry a timeout: the net/http package-level helpers (http.Get/Post/Head/PostForm) and http.DefaultClient both use a client with Timeout==0 and no transport deadlines, so a dead or stalled peer hangs the caller forever. This is an external-boundary claim — "the network will answer" — with nothing enforcing it.
The fix is a configured client: for normal calls `&http.Client{Timeout: ...}`; for large streamed downloads, a client whose Transport sets DialContext / TLSHandshake / ResponseHeaderTimeout but leaves Client.Timeout at 0 (so a multi-GB body is not cut off mid-stream). The rule does NOT flag &http.Client{} literals — those may carry either form of timeout — only the helpers that structurally cannot.
func (MissingHTTPTimeout) Code ¶
func (MissingHTTPTimeout) Code() string
Code returns this rule's stable finding code, "MISSING_HTTP_TIMEOUT".
type Rule ¶
type Rule interface {
Code() string
Check(fset *token.FileSet, file *ast.File, relPath string) []Finding
}
Rule is one boundary tell. Check inspects a single parsed file and returns findings; the scanner fills in File and applies suppression comments.
func DefaultRules ¶
func DefaultRules() []Rule
DefaultRules is the policy enforced by TestBoundaryPolicy. Add a tell by appending a Rule here and a catalog.go entry.
func DefaultTestRules ¶ added in v0.38.0
func DefaultTestRules() []Rule
DefaultTestRules is the policy family for the repo's OWN test files — the tells where a test's assertion freezes a current value instead of stating an invariant. It is a separate set from DefaultRules because the two run over disjoint file classes: Scan deliberately skips _test.go, ScanTests walks only _test.go. Enforced over the repo by TestTestSuitePolicy and reported by `fak boundary`.
type SkipDebt ¶ added in v0.38.0
type SkipDebt struct{}
SkipDebt flags a test that removes itself from the suite with a bare, unconditional t.Skip("...") / t.Skipf(...) / t.SkipNow() — a skip NOT guarded by a platform, short- mode, or environment condition. A skipped-into-silence test is invisible to a presence KPI (the enclosing Test func still exists, so "we have tests" stays green) while the body never runs, which is how a suite quietly rots to all-skips. This is the same boundary-tell shape as the rest of the family: the test CLAIMS coverage while its assertions never execute.
A skip guarded by a documented condition is an HONEST conditional skip — the test still runs in the configuration it targets — and is NOT flagged:
if testing.Short() { t.Skip("slow; run without -short") } // short-mode guard
if runtime.GOOS == "windows" { t.Skip("POSIX-only") } // platform guard
if os.Getenv("CI") == "" { t.Skip("needs CI credentials") } // environment guard
A deliberate always-skip (a quarantined flake tracked by an issue) is a recorded decision — suppress it in place with //boundarylint:ignore SKIP_DEBT and the issue link, so the exception is greppable rather than silent.
SkipDebt is a SOFT signal: unlike the enforced DefaultRules/DefaultTestRules families it is not part of any gate, so surfacing skip debt never reds the build. It is scanned over the whole test tree (documented skips DO exist, e.g. platform guards) and reported as a trend — `fak boundary` lists it separately from the gating tells, and the qa-process scorecard folds it as a SOFT KPI with a work-list of every skip site.