repolint

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package repolint contains lint checks that operate on the repository layout itself rather than on Go source code: migration version numbering, worklog numbering, and sync between canonical and chart-bundled copies of files.

These checks exist because real production incidents have come from repo-layout drift (worklog 0097 — two agents both numbered a migration "000009", one was silently skipped on cluster, schema ended up missing required columns).

The package is consumed by both `cmd/repolint` (the CLI used in pre-commit hooks and CI) and `*_test.go` files that assert today's repository is in good shape.

Index

Constants

This section is empty.

Variables

View Source
var MigrationPattern = regexp.MustCompile(`^(\d{6})_([a-z0-9_]+)\.(up|down)\.sql$`)

MigrationPattern matches `NNNNNN_<name>.up.sql` and `.down.sql` where NNNNNN is six digits. Captures: 1=version, 2=name, 3=direction.

The 6-digit prefix is what golang-migrate uses; matches today's `api/migrations/000001_initial_schema.up.sql` style.

View Source
var WorklogPattern = regexp.MustCompile(`^(\d{4})_(\d{4}-\d{2}-\d{2})_([a-z0-9._-]+)\.md$`)

WorklogPattern matches `NNNN_YYYY-MM-DD_<slug>.md` (numbered worklog, the final form after the post-merge bot assigns a number). Captures: 1=version, 2=date, 3=slug.

View Source
var WorklogSentinelPattern = regexp.MustCompile(`^NNNN_(\d{4}-\d{2}-\d{2})_([a-z0-9._-]+)\.md$`)

WorklogSentinelPattern matches `NNNN_YYYY-MM-DD_<slug>.md` — the sentinel form authors write before the post-merge bot assigns a real number. The literal `NNNN` is a placeholder meaning "assign me a number at merge." Captures: 1=date, 2=slug.

Functions

This section is empty.

Types

type CRDBinding

type CRDBinding struct {
	// GoFile is the path to the Go source file (relative to repo root)
	// containing GoStruct. The file is parsed standalone; cross-file
	// type references are not followed (drift checks should bind to
	// the leaf struct directly, not to a parent that embeds it).
	GoFile string
	// GoStruct is the unqualified struct type name to inspect.
	GoStruct string
	// CRDFile is the path to the YAML manifest containing the
	// CustomResourceDefinition (relative to repo root).
	CRDFile string
	// CRDPath is the sequence of map keys (and string-encoded array
	// indices) that walks from the YAML document root to the schema
	// object whose `properties:` map should mirror GoStruct's fields.
	CRDPath []string
	// IgnoreGoFields lists Go JSON tags that the CRD does not need
	// to declare (rare; e.g. computed-only fields the API server
	// would never accept). Empty in normal operation.
	IgnoreGoFields []string
	// IgnoreCRDProperties lists CRD property keys that the Go struct
	// does not need to declare (rare; e.g. metadata sentinels that
	// were intentionally added to the schema for kubectl tooling but
	// are not deserialized by the controller).
	IgnoreCRDProperties []string
}

CRDBinding declares one Go-struct-to-CRD-path drift check.

CRDPath walks into the parsed YAML; each element is either a map key (most commonly), or a string-encoded array index ("0", "1", …) when the path crosses an array. The path must terminate at a YAML node that is itself an `openAPIV3Schema`-style object (i.e. it has a `properties:` map). The check then compares that node's property keys to the JSON-tagged fields of GoStruct.

For nested-struct comparisons (e.g. WorkspaceStatus has a `sessions: []AgentSessionStatus` field), declare a separate binding for AgentSessionStatus and point its CRDPath at the array's `items` schema (so the path ends at "items" rather than at the `sessions` array property itself).

func LiveBindings

func LiveBindings() []CRDBinding

LiveBindings returns the canonical (Go struct → CRD path) pairs that must remain drift-free in this repository. Both the CLI driver (cmd/repolint) and the live-tree test (TestLive_CRDDrift_NoDrift) consume this so they stay in sync; adding a binding here surfaces it in pre-commit, CI, and the test suite simultaneously.

Binding rules:

  1. Bind each leaf struct that the controller reads/writes through the apiserver. Embedded/inline parents don't need bindings; their fields are accounted for through the leaf.

  2. Slice-of-struct fields need a separate binding for the element type, with CRDPath ending in "items" rather than at the slice property. AgentSessionStatus is the canonical example.

  3. IgnoreGoFields / IgnoreCRDProperties are deliberately empty in a healthy repo. Any non-empty entry should be paired with a comment explaining why the asymmetry is intentional.

type CRDDriftReport

type CRDDriftReport struct {
	// Binding records which check produced this report (so callers
	// can format messages tying the diff back to the file pair).
	Binding CRDBinding
	// GoMissingInCRD lists JSON tag names declared on GoStruct but
	// absent from the CRD's properties map. These are fields the
	// controller will write but kube-apiserver will silently drop.
	GoMissingInCRD []string
	// CRDMissingInGo lists property keys declared on the CRD but
	// absent from GoStruct. These are fields kube-apiserver will
	// accept but no Go reader unmarshals — usually a stale schema
	// from a renamed field.
	CRDMissingInGo []string
}

CRDDriftReport is the result of a single CRDDriftCheck run.

func CRDDriftCheck

func CRDDriftCheck(root string, b CRDBinding) (CRDDriftReport, error)

CRDDriftCheck loads the Go file and CRD YAML referenced by `b` (resolved relative to `root`) and compares the field/property names.

Returns an error only on parse failure or path-not-found (i.e. the CRDPath does not resolve in the YAML document, or GoStruct is not found in GoFile). Drift itself is non-fatal and surfaces via CRDDriftReport.OK()==false.

func (CRDDriftReport) OK

func (r CRDDriftReport) OK() bool

OK reports whether the binding is drift-free. A nil-valued report (zero struct) is OK; this matches how SequenceReport/DriftReport behave in this package and lets callers do `if !rep.OK() {…}`.

func (CRDDriftReport) String

func (r CRDDriftReport) String() string

String returns a human-readable, unified-style diff of the drift, or "(ok)" when there is none.

type CRDFetcher

type CRDFetcher interface {
	GetCRD(ctx context.Context, name string) (*apiextv1.CustomResourceDefinition, error)
}

CRDFetcher abstracts the apiserver call so the diff logic is unit- testable without a live cluster. Production wiring uses NewKubeCRDFetcher; tests pass a stub.

func NewKubeCRDFetcher

func NewKubeCRDFetcher() (CRDFetcher, error)

NewKubeCRDFetcher loads kubeconfig (KUBECONFIG env, then default merge rules) and constructs a CRDFetcher backed by the live apiextensions API. Returns an error if no current-context can be resolved — the operator should set KUBECONFIG or run inside a pod.

type ClusterDriftBinding

type ClusterDriftBinding struct {
	// CRDName is the metadata.name of the deployed CustomResourceDefinition
	// (e.g. "workspaces.llmsafespaces.dev").
	CRDName string
	// CRDFile is the path to the chart's CRD YAML, relative to repo root.
	CRDFile string
	// CRDPath walks from the document root to the schema node whose
	// `properties:` keys are compared. Same shape as CRDBinding.CRDPath.
	// Typically ["spec","versions","0","schema","openAPIV3Schema","properties","spec"].
	CRDPath []string
	// IgnoreClusterProperties lists keys present on the deployed CRD
	// but intentionally absent from the chart (rare; e.g. a field
	// deprecated server-side that callers no longer set).
	IgnoreClusterProperties []string
	// IgnoreChartProperties lists keys present in the chart but
	// intentionally not yet rolled out to the cluster (rare; only
	// useful during a multi-step migration where the chart leads).
	IgnoreChartProperties []string
}

ClusterDriftBinding declares one (chart-yaml ↔ deployed-CRD) drift check. The CRDName is the metadata.name of the deployed CRD on the cluster; the CRDFile and CRDPath address the same schema location in the chart YAML. The check terminates at a node whose `properties:` map is what gets compared, identical semantics to CRDBinding.CRDPath.

func LiveClusterBindings

func LiveClusterBindings() []ClusterDriftBinding

LiveClusterBindings returns the (chart YAML ↔ deployed CRD) pairs that the cluster-drift check evaluates by default. These mirror LiveBindings() but are addressed by the deployed CRD's metadata.name rather than by Go struct.

Adding a binding here surfaces it in `repolint -cluster-drift`.

type ClusterDriftReport

type ClusterDriftReport struct {
	Binding ClusterDriftBinding
	// ChartMissingInCluster lists keys declared in the chart YAML but
	// absent from the deployed CRD. These are fields the binary will
	// try to write but the apiserver will silently drop. This is the
	// worklog 0465 incident symptom and the primary thing this check
	// is here to surface.
	ChartMissingInCluster []string
	// ClusterMissingInChart lists keys declared on the deployed CRD
	// but absent from the chart. Indicates a stale CRD on the cluster
	// from a previous chart version that has since been pruned.
	ClusterMissingInChart []string
}

ClusterDriftReport is the result of one ClusterDriftCheck run.

func ClusterDriftCheck

func ClusterDriftCheck(ctx context.Context, root string, b ClusterDriftBinding, f CRDFetcher) (ClusterDriftReport, error)

ClusterDriftCheck fetches the deployed CRD and compares its properties at CRDPath against the chart YAML's properties at the same path.

Returns an error only on unrecoverable input failure: chart YAML missing/malformed, deployed CRD not found, or path not resolvable in either side. Drift itself is non-fatal and surfaces via ClusterDriftReport.OK()==false.

func (ClusterDriftReport) OK

func (r ClusterDriftReport) OK() bool

OK reports whether the binding is drift-free.

func (ClusterDriftReport) String

func (r ClusterDriftReport) String() string

String returns a human-readable, unified-style diff.

type DriftConfig

type DriftConfig struct {
	// CanonicalDir is the source-of-truth directory.
	CanonicalDir string
	// MirrorDir is the secondary copy that must mirror CanonicalDir.
	MirrorDir string
	// Glob restricts the comparison to files matching this pattern
	// (filepath.Match syntax; relative to dir entries' base names).
	// Files not matching are ignored entirely — including READMEs and
	// other side files that are allowed to differ.
	Glob string
}

DriftConfig configures a DriftCheck run.

type DriftReport

type DriftReport struct {
	// MissingInMirror are files present in CanonicalDir but absent
	// (or ignored by Glob) in MirrorDir.
	MissingInMirror []string
	// ExtraInMirror are files present in MirrorDir but absent in
	// CanonicalDir.
	ExtraInMirror []string
	// ContentDiffers lists files present in both but with different
	// SHA-256 content hashes.
	ContentDiffers []string
}

DriftReport is the result of a DriftCheck run.

func DriftCheck

func DriftCheck(cfg DriftConfig) (DriftReport, error)

DriftCheck verifies cfg.MirrorDir holds the same files (matching cfg.Glob) as cfg.CanonicalDir, byte-for-byte.

func (DriftReport) OK

func (r DriftReport) OK() bool

OK reports whether the mirror is byte-identical to the canonical.

func (DriftReport) String

func (r DriftReport) String() string

String returns a human-readable failure description, or "(ok)".

type Duplicate

type Duplicate struct {
	Version int
	Files   []string
}

Duplicate is two or more files claiming the same version number.

type MainlineCollision

type MainlineCollision struct {
	Version     int
	LocalFiles  []string
	RemoteFiles []string
}

MainlineCollision reports worklog version numbers that exist both locally and on the target branch (typically origin/main).

type MainlineReport

type MainlineReport struct {
	Collisions []MainlineCollision
	NextNumber int
}

MainlineReport is the result of a MainlineCheck run.

func MainlineCheck

func MainlineCheck(dir string) (MainlineReport, error)

MainlineCheck compares local worklog versions against origin/main to detect collisions that would cause repolint failures when the branch is merged. It also reports the next available worklog number.

The function uses `git ls-tree` to enumerate remote worklog filenames without needing a network fetch (assumes origin/main is present in the local clone's remote-tracking refs, which is always true after `git clone` or `git fetch`). MainlineCheck detects worklog version collisions between a branch's NEW worklogs (those not yet on origin/main) and worklogs already on origin/main. This prevents two branches from choosing the same worklog number and causing a repolint failure on merge.

Worklogs that exist identically on both local and remote are NOT flagged — they are shared ancestry. Only new worklogs unique to this branch are checked for collisions against the remote set.

It also reports the next available worklog number (max of local and remote + 1).

func (MainlineReport) OK

func (r MainlineReport) OK() bool

OK reports whether there are no collisions with mainline.

func (MainlineReport) String

func (r MainlineReport) String() string

String returns a human-readable failure description, or "(ok)".

type SentinelReport

type SentinelReport struct {
	// Sentinels lists the basenames of NNNN_ files found, sorted lexically.
	Sentinels []string
}

SentinelReport lists NNNN_ sentinel worklog files found in the dir. On a healthy main, this is empty — the post-merge bot rewrites every NNNN_ file to a real number immediately after merge. A non-empty report on main means the bot is broken or hasn't run yet.

func SentinelCheck

func SentinelCheck(dir string) (SentinelReport, error)

SentinelCheck scans dir for NNNN_ placeholder worklog files. Used as a non-gating warning on main (a persistent NNNN_ on main means the post-merge bot is broken) and as a gating check in pre-commit (authors must use the NNNN_ sentinel for new worklogs, not pick their own number).

func (SentinelReport) OK

func (r SentinelReport) OK() bool

OK reports whether no sentinel files were found.

func (SentinelReport) String

func (r SentinelReport) String() string

String returns a human-readable description.

type SequenceConfig

type SequenceConfig struct {
	// Dir is the directory to scan.
	Dir string
	// Pattern matches versioned filenames. Capture group 1 MUST be the
	// numeric version. If RequirePaired is true, capture group 3 MUST
	// be "up" or "down".
	Pattern *regexp.Regexp
	// RequirePaired, when true, asserts every (version, name) tuple
	// has both an up and a down file. Use false for single-file
	// schemes like worklogs.
	RequirePaired bool
	// GrandfatherBelow, when > 0, exempts existing collisions and
	// gaps at versions strictly less than this value from failing the
	// check. New entries at or above this threshold are still subject
	// to all rules. Use this when historical duplicates exist and
	// rewriting them is impractical (e.g. cross-references in 20+
	// files); the goal is to prevent NEW drift, not relitigate old.
	GrandfatherBelow int
	// AllowGaps, when true, treats sequence gaps as warnings rather
	// than failures. Duplicates and unpaired-files are still hard
	// failures. Use this for append-only artifacts (worklogs) where
	// gaps from concurrent merges + auto-renames are an expected
	// failure mode that the autofix bot cannot heal without breaking
	// MainlineCheck. Migrations should NEVER allow gaps — an
	// out-of-sequence migration breaks schema rebuild.
	AllowGaps bool
}

SequenceConfig configures a single SequenceCheck run.

type SequenceReport

type SequenceReport struct {
	// MaxVersion is the highest version found. Zero if dir is empty.
	MaxVersion int
	// SeenVersions are all unique version numbers, sorted.
	SeenVersions []int
	// MissingVersions lists numbers in [1, MaxVersion] that no file
	// covers. (A run with MaxVersion=0 has no missing versions.)
	MissingVersions []int
	// Duplicates lists every version that has more than one
	// (name) entry. Note: a paired up+down counts as ONE entry.
	Duplicates []Duplicate
	// UnpairedFiles lists filenames that lack their matching up/down
	// counterpart (only populated when RequirePaired=true).
	UnpairedFiles []string
	// GapsAllowed mirrors SequenceConfig.AllowGaps. When true, OK()
	// ignores MissingVersions; callers can detect the warning state
	// via HasWarnings().
	GapsAllowed bool
}

SequenceReport is the result of a SequenceCheck run.

func SequenceCheck

func SequenceCheck(cfg SequenceConfig) (SequenceReport, error)

SequenceCheck scans cfg.Dir for files matching cfg.Pattern and reports duplicates, gaps, and (if RequirePaired) unpaired files.

func (SequenceReport) HasWarnings

func (r SequenceReport) HasWarnings() bool

HasWarnings reports whether the report contains warning-class findings — currently only "gap-allowed sequence has gaps". Always false when GapsAllowed is false (in which case gaps are reported via OK() == false).

func (SequenceReport) OK

func (r SequenceReport) OK() bool

OK reports whether the dir is in a healthy state. When GapsAllowed is true, missing versions do not affect OK; use HasWarnings() to detect those.

func (SequenceReport) String

func (r SequenceReport) String() string

String returns a human-readable failure description, or "(ok)".

type WorklogRename

type WorklogRename struct {
	From string // original filename (basename only)
	To   string // new filename (basename only)
}

WorklogRename records a single rename performed by FixWorklogs.

func FixWorklogs

func FixWorklogs(dir string) ([]WorklogRename, error)

FixWorklogs resolves duplicate worklog numbers in dir by renaming the conflicting file(s) to the next available number, AND assigns real numbers to all `NNNN_` sentinel files (the placeholder form authors write before the post-merge bot runs).

Sentinel pass runs first: each `NNNN_<date>_<slug>.md` is renamed to `<next-number>_<date>_<slug>.md`. Sentinels are processed in lexical order so same-branch batches get contiguous numbers in a stable order.

When origin/main is reachable, files that exist there are treated as incumbents — they stay; files unique to this working copy are renumbered. This is the correct signal after `git rebase origin/main`: mainline's worklog and yours both end up in worklogs/, and mainline's was merged first. Mainline files also participate in collision detection as "phantoms" — if a local file's number matches a mainline file with a different slug (the pre-rebase case), the local file is renumbered.

When origin/main is not reachable (fresh clone without fetch, detached HEAD, network error, no git in this tree), the lexically-last file at each duplicated version is treated as the newcomer — the original pre-mainline-aware behavior.

The function iterates until no sentinels, duplicates, or mainline collisions remain, handling the pathological case where multiple files all collide on the same number. It returns the list of renames performed (empty if nothing was needed).

Only files matching WorklogPattern or WorklogSentinelPattern are considered; other files in dir are ignored. Versions below the grandfather threshold (97) are never touched — historical duplicates stay grandfathered.

After renaming, any occurrence of the old basename inside the file's own content is replaced with the new basename, so self-referential lines like "worklogs/0140_..._foo.md — This worklog" stay accurate.

Jump to

Keyboard shortcuts

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