roles

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 18 Imported by: 0

README

roles

Go library for classifying repository paths as source, tests, fixtures, vendored code, generated files and other roles. Path classification reads no file contents and requires no checkout.

The same contents under src/sqlite3.c, vendor/sqlite/sqlite3.c and test/fixtures/sqlite3.c carry different context. Content analysis can run once per unique blob, with roles describing each occurrence separately.

Results contain additive roles and optional evidence. Callers decide what to scan, count or include in a report. The API and corpus are early and may change.

For fixed inputs, corpus revision and context, classification is deterministic. Tools can use these labels to select files before expensive scans or LLM calls, without asking a model to infer roles on every run.

Installation

go get github.com/git-pkgs/roles

Use cases

Composition What it enables
licenses + roles Licence findings associated with legal files, source layouts and bundled code
manifests + roles Dependency observations separated by project, fixture, example and vendor paths
brief/distill + roles Repository summaries and scoped project classification
scrutineer + roles Analysis routing and role context attached to security findings

Search indexes can filter for C source excluding vendor/generated, CI YAML or test-only Python. Summaries could describe a Go application with generated protobufs and vendored C, while diff reports could distinguish generated-file changes from changes to source files.

Scanners can select build scripts for execution review or newly vendored trees for dependency investigation. Callers supply the filtering and ranking criteria; a role does not establish ownership or whether a file is safe to skip.

Classification

A path can have several roles, or none when it carries no recognized convention. Directory matches apply to descendants; filename rules add independent evidence. Classification results use empty role and evidence slices rather than nil slices.

Path Roles
LICENSE legal
LICENSES/MIT.txt legal
vendor/sqlite/LICENSE vendor, legal
src/parser.go source
internal/parser_test.go source, test
test/fixtures/input.json test, fixture
testdata/package-lock.json test, fixture, generated
packages/api/uv.lock generated
src/Form.Designer.cs source, generated
src/app.min.js source, generated, minified
service.pb.go generated
.pytest_cache/v/cache/nodeids generated, cache
CMakeFiles/app.dir/main.o generated, build-output
Pods/library/main.swift vendor
gradlew build
.gitattributes configuration
examples/client/main.go example
docs/api.md documentation
.github/SECURITY.md documentation
CODE_OF_CONDUCT.md documentation
.github/workflows/test.yml ci
packages/service/.gitlab-ci.yml ci
vendor/component/Makefile.in vendor, build
test/fixtures/setup.py test, fixture, packaging, configuration
.github/SUPPORT.md documentation
main.go None

The vocabulary is source, test, fixture, example, benchmark, fuzz, vendor, generated, build-output, cache, documentation, legal, build, ci, packaging, tooling, configuration and minified. build identifies build instructions and wrappers. build-output identifies specific emitted build-system files and directories. These labels describe path conventions; they do not establish that a tracked file is safe to delete. Coverage is based on explicit conventions, not a complete inventory of every ecosystem.

Evidence identifies the rule, role, matched path and source, with a subtype or ecosystem where applicable. Caller-supplied context can also identify the repository path that supplied the evidence. Legal matches distinguish licence and notice files. Roles follow a fixed order; evidence follows ancestor order and then filename matches. A source role describes layout and does not establish compilability or authorship.

Usage

result, err := roles.Classify("vendor/sqlite/LICENSE")
if err != nil {
    return err
}
for _, role := range result.Roles {
    fmt.Println(role)
}

Output:

vendor
legal

For filtering without evidence allocation, use Match:

labels, err := roles.Match("testdata/package-lock.json")
if err != nil {
    return err
}
if labels.Has(roles.Fixture) {
    // Apply the caller's fixture policy.
}

Scanners that already enumerate files, such as brief, can call Match on each accepted repository-relative path during their existing scan. This adds no file reads or evidence allocations and does not require a second traversal. Match supports concurrent calls; content analysis and directory exclusion policies remain with the caller.

Paths use / separators. A trailing slash denotes a directory, so vendor/ is a vendor directory while vendor alone is a filename. Empty paths, absolute paths, NUL bytes, repeated separators and . or .. components return ErrInvalidPath. Backslashes and invalid UTF-8 bytes are literal filename bytes; the library does not normalize them.

Configured vendor roots can supply additional evidence:

classifier, err := roles.New([]roles.VendorRoot{
    {Path: "deps/crates", Ecosystem: "cargo", EvidencePath: ".cargo/config.toml"},
})
if err != nil {
    return err
}
result, err := classifier.Classify("deps/crates/example/src/lib.rs")

The classifier copies this context and supports concurrent use. Several ecosystems or evidence paths can describe the same root; exact duplicates are ignored, and evidence order is deterministic. The context.vendor-root evidence uses context as its source and records the caller-supplied path separately. LegalFileName and IsLegalDirectory expose the same legal-name corpus for consumers that already traverse paths themselves. LegalFileNameBytes and IsLegalDirectoryBytes accept raw Git tree entry names without allocating for ASCII corpus names.

Git tree walkers can classify components without rebuilding full paths. A state is a value, so one parent can produce independent child states:

state := classifier.RootState()
sourceState, err := state.Enter([]byte("src"))
if err != nil {
    return err
}
labels, err := sourceState.Match([]byte("main.go"))

Enter accepts one directory component and Match accepts one file component. Both accept raw non-NUL Git filename bytes. Roles returns the directory roles accumulated by a state. Key returns a comparable value that captures the corpus version, configured vendor paths, inherited roles and partial path-rule progress. A history walker can combine that key with a tree object ID when caching classified subtrees. Evidence for selected occurrences can be recovered later with Classify and the full repository-relative path.

Trees and content

Walk accepts an fs.FS and a callback, reusing directory matches as it visits descendants. It emits directories with trailing slashes, skips symlinks and special files, and propagates traversal and callback errors. Callbacks may return fs.SkipDir or fs.SkipAll to control traversal.

err := roles.Walk(tree, roles.WalkOptions{}, func(path string, result roles.Result) error {
    fmt.Println(path, result.Roles)
    return nil
})

Use WalkMatch when the visitor only needs labels. It reuses inherited role sets without collecting or copying evidence, which reduces allocation for deep trees. It has the same traversal order, limits and callback error handling as Walk, and also has a classifier method for vendor-root context.

err := roles.WalkMatch(tree, roles.WalkOptions{}, func(path string, set roles.Set) error {
    if set.Has(roles.CI) {
        fmt.Println(path)
    }
    return nil
})

The default limits are one million entries and 256 path components. WalkOptions can change either limit; exceeding one returns ErrLimit, with earlier callback results already delivered. Traversal is lexical and buffers directory listings, so the entry limit is not a strict memory cap. The caller supplies a rooted filesystem; os.Root.FS provides containment for a directory on disk. No files are read for content classification during Walk, and no directory names are automatically excluded.

ClassifyReader(path, reader) adds optional generated and minified detection and reads at most 8 KiB plus one byte used to detect truncation. ClassifyBlob provides the same result for callers that already hold the complete contents. Both have classifier methods that retain vendor-root context. Inspection is limited to the first 8 KiB and 40 lines. Go retains its strict generated marker check, ending at the first non-comment token. Go tokenization prevents markers in strings or block comments from becoming generated evidence.

Other supported source files match Code generated by, DO NOT EDIT or @generated in leading comments, ignoring case. Hash comments are checked for .py, .rb and .sh; line and block comments are checked for .js, .mjs, .cjs, .jsx, .ts, .tsx, .c, .h, .cc, .cpp, .hpp, .cs, .java, .rs, .swift, .kt and .dart. CSS uses block comments. Inspection stops at code, so a marker in a string or a later comment is not a generated header.

JavaScript, TypeScript and CSS also receive generated for source-map comment directives within the inspected prefix. A line with at least 1,000 code bytes and fewer than one space or tab per ten code bytes receives both minified and generated. Strings and comments do not count toward that threshold. These are heuristics: short minified files and source-map directives beyond the inspection limits can be missed. No tail reads are performed.

The result includes HeaderChecked, BytesExamined and HeaderLimited. Unsupported file types receive path-only classification. Missing generated evidence does not prove a file was handwritten, and a generated lockfile may still be essential dependency evidence.

Content caches should include the blob ID, ContentVersion and the occurrence filename extension, including its casing. The same blob can receive path-only classification at one occurrence and generated-header evidence at another.

Corpus

The embedded rule corpus uses exact directory and filename matches, case-insensitive stems with allowed extensions, bounded prefixes and suffixes. It does not evaluate regexes or repository-supplied rules. Legal names are case insensitive; most layout names are case sensitive. Directory conventions apply at any depth, including package directories in monorepos.

CI filenames cover GitLab CI, Buildkite, Jenkins, Azure Pipelines, Drone and Travis CI alongside the GitHub Actions, CircleCI and .buildkite directories. Build conventions include Make, CMake, Meson, Autotools, Rake, Gradle and Maven. Packaging matches include Python setup files, Composer manifests and Ruby gemspecs. These are filename conventions; file contents can use those names for other purposes.

Generated dependency files include lockfiles for Bundler, Python tools, Swift, Gradle, NuGet and other package managers. Selected dependency reports such as npm-ls.json and go.graph have subtype dependency-report; NuGet assets and .deps.json files use dependency-resolution. There is no blanket *.lock rule.

Other generated-file conventions include Python bytecode and tool caches, CMake metadata, TypeScript build information, Flutter plugin registrants and GCC coverage files. Specific caches such as .pytest_cache/, .parcel-cache/ and .turbo/ receive cache. Specific output such as CMakeFiles/, .next/, .nuxt/ and .svelte-kit/ receives build-output. These roles are additive with generated. Generic names such as .cache/, build/, dist/ and out/, and virtual environments, receive no additional role. Bower, JSPM and vcpkg dependency directories receive vendor alongside any roles matched inside them.

Named minified JavaScript/CSS files, source maps and .NET designer files also receive generated. Source-map and designer suffixes accept mixed case; minified suffixes use lowercase .min.js, -min.js, .min.css and -min.css and also receive minified. Protobuf output names ending in .pb.go, _pb2.py, _pb2_grpc.py, _pb2.pyi or _pb2_grpc.pyi receive generated. A plain .d.ts filename or a name such as jquery.js does not establish generated or vendored content.

Named community documents include support, governance, maintainers, authors and roadmaps, using case-insensitive stems and selected document extensions, including .markdown and .rdoc. For example, SUPPORT.md, support.md and Support.md match, while support.py and SUPPORT.md.bak do not. Rakefile matching accepts any casing with an optional .rb extension. Citation files, API/manual directories and installation/change documents provide further documentation conventions. Bare INSTALL, CHANGE and CHANGES match; their lowercase command names require a document extension.

Sources include legal-name matching from licenses, layout and tool conventions from brief, manifest, lockfile and dependency-output names from manifests, and selected generated-file and dependency-directory conventions from GitHub's gitignore templates. Tool-specific output directories also come from outline. GitHub Linguist supplies additional vendor, documentation and generated-file conventions, adapted to these roles. NOTICE records source revisions and attribution. CorpusVersion identifies the embedded classification semantics for caller caches. Tests bind each version to a fingerprint of the canonical rules and their classification output, so semantic changes require a new version entry.

Coverage differs from GitHub Linguist: roles treats testdata as test/fixture rather than vendor. Generic output and cache names such as dist and cache receive no directory role.

Potential consumers include git-pkgs for dependency analysis, licenses and git-spdx for licence reporting, brief for repository summaries, distill for project classification, and outline for code context. scrutineer could attach roles to security findings. These integrations are not shipped.

manifests retains package-manager configuration parsing and vendor-root discovery; magic detects formats and encodings. Generated and vendored files may still require scanning, and a manifest's path role does not change dependency scopes declared inside it.

Cache results with their path, corpus version and supplied context; a blob ID or subtree hash alone is insufficient. Content observations that depend on filenames or repository settings need those inputs in their cache keys too. The library currently keeps no cross-tree cache.

CLI

The development CLI emits one JSON record per path or tree entry. It rejects non-UTF-8 paths rather than replacing their bytes in JSON:

go run ./cmd/roles vendor/sqlite/LICENSE testdata/package-lock.json
go run ./cmd/roles -root .
go run ./cmd/roles -labels-only -root .
go run ./cmd/roles -version

Each JSONL record includes corpus_version. Full records contain roles and evidence arrays, including empty arrays for unmatched paths. -labels-only uses Match or WalkMatch and omits the evidence field. JSON encoding still allocates output records; library callers can use Set directly. Tree mode prunes Git metadata directories and .git files. Library traversal applies no automatic directory exclusions.

Testing

Tests cover corpus examples, rule reachability, path validation, inherited context, generated headers and CLI behaviour. Pinned path inventories from three repositories check agreement between tree and individual-path classification. These inventories are regression inputs, not independently labelled ground truth.

go test -race ./...
go test -run '^$' -fuzz '^FuzzClassify$' -fuzztime=30s
go test -run '^$' -fuzz '^FuzzBlob$' -fuzztime=30s
go test -run '^$' -fuzz '^FuzzWalk$' -fuzztime=30s

Benchmarks

go test -run '^$' -bench . -benchmem
go test -run '^$' -bench BenchmarkClassify -o /tmp/roles-profile.test -cpuprofile=/tmp/roles.cpu -memprofile=/tmp/roles.heap
go tool pprof /tmp/roles.cpu

Benchmarks separate label matching, evidence allocation, bounded content checks and filesystem traversal. BenchmarkWalkDisk and BenchmarkWalkMatchDisk compare evidence-producing and label-only walks over the same wide, monorepo and deep layouts. BenchmarkMillionPaths measures a million synthetic monorepo paths; repository inventories provide additional path workloads. These measurements exclude blob I/O and are not a throughput claim for a complete repository scan.

License

Released under the MIT License. See NOTICE for corpus source attribution and modification details, including conventions adapted from GitHub's CC0-1.0 gitignore templates.

Documentation

Overview

Package roles adds repository context to file occurrences through path classification. Roles are additive; classification never excludes a file. Paths are repository-relative, use slash separators, and end in a slash for directories. Optional evidence explains each matching convention.

Index

Constants

View Source
const (
	MaxHeaderBytes = 8192
	MaxHeaderLines = 40
	ContentVersion = "2"
)
View Source
const (
	DefaultMaxEntries = 1_000_000
	DefaultMaxDepth   = 256
)
View Source
const CorpusVersion = "6"

CorpusVersion identifies classification semantics and evidence ordering.

Variables

View Source
var ErrInvalidPath = errors.New("expected a clean repository-relative path")

ErrInvalidPath indicates an absolute, unclean, empty, or NUL-containing path.

View Source
var ErrLimit = errors.New("repository traversal limit reached")

ErrLimit indicates that a walk stopped before visiting the complete tree.

Functions

func IsLegalDirectory

func IsLegalDirectory(name string) bool

IsLegalDirectory reports whether one directory name confers licence context. It accepts a single name, independent of the repository path contract.

func IsLegalDirectoryBytes

func IsLegalDirectoryBytes(name []byte) bool

IsLegalDirectoryBytes is the raw Git-name form of IsLegalDirectory.

func LegalFileName

func LegalFileName(name string) (license, notice bool)

LegalFileName reports licence and notice matches for one filename. These predicates use the same corpus as Classify and allocate no evidence.

func LegalFileNameBytes

func LegalFileNameBytes(name []byte) (license, notice bool)

LegalFileNameBytes is the raw Git-name form of LegalFileName.

func Walk

func Walk(tree fs.FS, options WalkOptions, visit func(string, Result) error) error

Walk classifies regular files and directories in lexical order. It skips symlinks and special files, propagates errors, and never reads file contents. The filesystem must be rooted by the caller. fs.SkipDir and fs.SkipAll from visit have the same meaning as in fs.WalkDir. The root itself is not emitted.

func WalkMatch

func WalkMatch(tree fs.FS, options WalkOptions, visit func(string, Set) error) error

WalkMatch visits the same entries as Walk without collecting evidence.

Types

type BlobResult

type BlobResult struct {
	Result
	HeaderChecked bool `json:"header_checked"`
	BytesExamined int  `json:"bytes_examined"`
	HeaderLimited bool `json:"header_limited"`
}

BlobResult describes bounded content inspection, where applicable. Absence of Generated is not proof that a file was handwritten.

func ClassifyBlob

func ClassifyBlob(name string, contents []byte) (BlobResult, error)

ClassifyBlob adds generated and minified content evidence to path classification. Contents must be the complete file. At most MaxHeaderBytes and MaxHeaderLines are inspected. Unsupported file types receive path-only classification.

func ClassifyReader

func ClassifyReader(name string, reader io.Reader) (BlobResult, error)

ClassifyReader adds content evidence using bounded input. It reads at most MaxHeaderBytes plus one byte used to detect truncation.

type Classifier

type Classifier struct {
	// contains filtered or unexported fields
}

Classifier combines the embedded corpus with immutable vendor-root context. A classifier can be shared by concurrent callers.

func New

func New(roots []VendorRoot) (*Classifier, error)

New validates, normalizes and copies caller-supplied vendor roots. Exact duplicates are ignored, and distinct observations for one path are retained.

func (*Classifier) Classify

func (c *Classifier) Classify(name string) (Result, error)

Classify returns labels and evidence for a file or trailing-slash directory.

func (*Classifier) ClassifyBlob

func (c *Classifier) ClassifyBlob(name string, contents []byte) (BlobResult, error)

ClassifyBlob preserves this classifier's vendor-root context.

func (*Classifier) ClassifyReader

func (c *Classifier) ClassifyReader(name string, reader io.Reader) (BlobResult, error)

ClassifyReader preserves this classifier's vendor-root context while bounding content reads.

func (*Classifier) Match

func (c *Classifier) Match(name string) (Set, error)

Match returns labels without collecting evidence.

func (*Classifier) RootState

func (c *Classifier) RootState() State

RootState returns this classifier's root state.

func (*Classifier) Walk

func (c *Classifier) Walk(tree fs.FS, options WalkOptions, visit func(string, Result) error) error

Walk reuses inherited matches and includes this classifier's vendor roots.

func (*Classifier) WalkMatch

func (c *Classifier) WalkMatch(tree fs.FS, options WalkOptions, visit func(string, Set) error) error

WalkMatch includes this classifier's vendor roots without collecting evidence.

type Evidence

type Evidence struct {
	Rule         string `json:"rule"`
	Role         Role   `json:"role"`
	Path         string `json:"path"`
	Subtype      string `json:"subtype,omitempty"`
	Ecosystem    string `json:"ecosystem,omitempty"`
	Source       string `json:"source"`
	EvidencePath string `json:"evidence_path,omitempty"`
}

Evidence identifies a matched corpus rule or caller-supplied vendor root. Source names the rule provider. EvidencePath names the repository input that supplied contextual evidence, when one exists.

type Result

type Result struct {
	Roles    []Role     `json:"roles"`
	Evidence []Evidence `json:"evidence"`
}

Result contains labels and the independent matches that produced them. Classification functions return non-nil Roles and Evidence slices.

func Classify

func Classify(name string) (Result, error)

Classify returns additive roles and all matching evidence, in stable order. Unknown conventions produce an empty result. A trailing slash denotes a directory. Paths use slash separators; backslashes and non-UTF-8 bytes are literal names.

func (Result) Has

func (r Result) Has(role Role) bool

Has reports whether the result contains role.

type Role

type Role string

Role describes a convention at a file or directory occurrence.

const (
	Source        Role = "source"
	Test          Role = "test"
	Fixture       Role = "fixture"
	Example       Role = "example"
	Benchmark     Role = "benchmark"
	Fuzz          Role = "fuzz"
	Vendor        Role = "vendor"
	Generated     Role = "generated"
	BuildOutput   Role = "build-output"
	Cache         Role = "cache"
	Documentation Role = "documentation"
	Legal         Role = "legal"
	Build         Role = "build"
	CI            Role = "ci"
	Packaging     Role = "packaging"
	Tooling       Role = "tooling"
	Configuration Role = "configuration"
	Minified      Role = "minified"
)

Keep explicit Role types so the constant-to-bit mapping test includes each role.

type Set

type Set uint32

Set is a compact collection of roles in corpus-defined order.

func Match

func Match(name string) (Set, error)

Match returns the same labels as Classify without collecting evidence. It returns ErrInvalidPath for paths outside the repository-relative contract.

func (Set) Has

func (s Set) Has(role Role) bool

Has reports whether the set contains role. Unknown role names return false.

func (Set) List

func (s Set) List() []Role

List returns the roles in deterministic order.

func (Set) MarshalJSON

func (s Set) MarshalJSON() ([]byte, error)

MarshalJSON encodes the set as role names in corpus order.

func (*Set) UnmarshalJSON

func (s *Set) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes role names into a set.

type State

type State struct {
	// contains filtered or unexported fields
}

State incrementally classifies one repository path. Its zero value is the root state for the package-level classifier.

func RootState

func RootState() State

RootState returns the package-level classifier's root state.

func (State) Enter

func (s State) Enter(name []byte) (State, error)

Enter returns the state for a child directory component. Git tree names may contain arbitrary non-NUL bytes but cannot contain a slash.

func (State) Key

func (s State) Key() StateKey

Key returns the semantic context needed to cache descendant tree matches.

func (State) Match

func (s State) Match(name []byte) (Set, error)

Match returns the roles for a child file component without changing the parent state.

func (State) Roles

func (s State) Roles() Set

Roles returns the roles accumulated by entered directories.

type StateKey

type StateKey struct {
	// contains filtered or unexported fields
}

StateKey is a stable, comparable summary of the context that can affect descendant matches. It can be combined with a Git tree object ID for a semantic subtree cache.

type VendorRoot

type VendorRoot struct {
	Path         string
	Ecosystem    string
	EvidencePath string
}

VendorRoot adds a vendor directory discovered by a caller.

type WalkOptions

type WalkOptions struct {
	MaxEntries int
	MaxDepth   int
}

WalkOptions bounds traversal. Zero values select the documented defaults.

Directories

Path Synopsis
cmd
roles command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL