smells

package
v0.22.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const SmellRulesEnvVar = "MACHE_SMELL_RULES_DIR"

SmellRulesEnvVar names the environment variable that points to a directory of JSON files containing additional SmellRule entries. External rules are merged with the built-in registry per find_smells request (see activeSmellRules) and are otherwise indistinguishable from built-ins — same MCP shape, same query contract, same pre-flight table check. Loading per-request (rather than once at package init) is what makes rule files live-reloadable: drop a new JSON in the dir and the next call sees it, no restart.

Each file in the directory must be a single SmellRule object (not an array — one rule per file keeps diffs clean and avoids "the third rule errored, what was the first?" debugging). The loader is fail-fast: a single read / parse / validation / collision error aborts loading, returning the error to the caller. serve logs and falls back to built-ins in that case so a typo can't take down the server; the CLI surfaces the error directly.

Variables

View Source
var ErrPathShapedRule = errors.New("rule reads node ids as paths, which projection-v6 does not store")

ErrPathShapedRule is returned for a rule marked PathShapedIDs on a database whose node ids are not paths.

Functions

func EnsureCanonicalViews

func EnsureCanonicalViews(qg graph.RefsQuerier) error

EnsureCanonicalViews installs every view a smell rule may read: the producer-translation vocabulary from internal/lloschema, then the three views derived FROM it for particular rule questions.

The split is the layering. v_ast / v_nodes / v_defs / v_refs translate ley-line-open's physical schema and belong to that boundary, so `graph` can install them without depending on the smell engine (mache-be17ce). v_test_nodes, v_vendored_files and v_doc_refs answer questions only rules ask, and stay here.

Installed as one call rather than at each rule-run site so every caller gets all of them — production and the 26 tests that build fixtures by hand. Rules reference the derived views unconditionally, so a caller that installed the vocabulary but not these produces "no such table" at query time.

func FindSmellsCmd

func FindSmellsCmd() *cobra.Command

FindSmellsCmd returns the find-smells cobra command for registration on the root command. Registration hook #1 of the cmd decomposition (mache-96c378): the package defines the command; cmd/register.go decides it exists on the CLI. smells never imports cmd.

func MakeFindSmellsHandler

func MakeFindSmellsHandler(g graph.Graph, rulesDir ...string) server.ToolHandlerFunc

MakeFindSmellsHandler returns the MCP handler. With no `rule` arg it lists all registered rules; with a rule it runs the scan.

The optional rulesDir is the directory of external rule JSON files resolved once at serve startup (flag / env / .mache.json). The handler re-scans it PER REQUEST via activeSmellRules, so a rule file dropped into the dir is picked up on the next call with no restart — the live-reload property. It's variadic so the ~80 existing test callsites that pass only a graph keep compiling (dir defaults to "" = built-in rules only). On an external-load error the handler logs and falls back to the built-ins so a bad rule file can't take the daemon down.

func ResolveSmellRulesDir

func ResolveSmellRulesDir(flagVal, projectDir string) string

ResolveSmellRulesDir picks the external smell-rules directory for the current invocation using a fixed precedence:

explicit flag > MACHE_SMELL_RULES_DIR env > .mache.json smellRulesDir > ""

Flag and env values are operator-supplied at invocation time and are used verbatim (the operator points them wherever they like). A smellRulesDir read from .mache.json is treated as project-relative: if it's a relative path it's joined onto projectDir (the directory that holds the .mache.json) and containment-checked with the same guard resolveDataSource uses, so an untrusted config can't escape the project tree via `../`. An empty string means "built-in rules only".

func RunFindSmells

func RunFindSmells(opts FindSmellsOptions) (int, error)

RunFindSmells executes one find-smells run from opts and returns the intended process exit code. It drives the same package-level command state the CLI does (snapshotted and restored), so a run through here and a run through the flags are the same code path — which is exactly what the MCP↔CLI byte-parity gate needs to stay a single assertion. Not safe for concurrent use, same as the CLI singleton it wraps.

func RunSmellRule

func RunSmellRule(qg graph.RefsQuerier, rule *SmellRule, sourceID string, limit int) ([]smellFinding, error)

RunSmellRule executes the rule's SQL, optionally scoped to one source. Single-rule callers (the MCP find_smells tool, one rule per request) use this directly. Callers that run multiple rules against the same qg in one process must call ensureSmellQueryContext once before the loop and use runSmellRuleQuery per rule instead — see ensureSmellQueryContext.

Types

type FindSmellsOptions

type FindSmellsOptions struct {
	DB, Rule, SourceID, Format, FailOn, Tags, RulesDir string
	Baseline, WriteBaseline, BaselineRoot              string
	Limit                                              int
	MinMetric                                          int64
	Out, Err                                           io.Writer
}

FindSmellsOptions parameterizes a programmatic find-smells run — the same engine the CLI flags drive, as a struct. Zero values take the CLI defaults (Limit 200, Format json, FailOn error).

type Severity

type Severity string

Severity is one of off / warn / error. ESLint precedent (string names, no numeric aliases). See mache-ec1a06 for the prior-art rationale across ruff/pylint/eslint/clippy/semgrep.

const (
	SeverityOff   Severity = "off"
	SeverityWarn  Severity = "warn"
	SeverityError Severity = "error"
)

type SmellRule

type SmellRule struct {
	ID          string   // stable identifier, used as the MCP tool argument
	Languages   []string // matches `_source.language` values; empty = any
	Description string   // shown in the help payload
	Query       string   // SQL with one `%s` placeholder for the optional scope clause
	ScopeColumn string   // SQL expression to compare to source_id; empty disables source_id scoping
	// Requires lists the SQL tables this rule reads. Surfaced in the
	// rules listing so an agent can decide upfront whether the rule is
	// usable on the active backend. Standalone mache emits `nodes`,
	// `node_refs`, `node_defs`; LLO-built .db additionally has `_ast`,
	// `_source`, `_imports`, `_lsp*`. See docs/ARCHITECTURE.md
	// "Interplay with ley-line-open" for the full table.
	Requires []string
	// DefaultMinMetric is the rule's recommended metric threshold
	// when the request omits min_metric. Zero means "no default
	// threshold; return all rows." Used for metric-bearing rules
	// where the query's natural output includes a long tail of
	// uninteresting low-metric rows (e.g. long_function returns
	// every function regardless of size; without a default
	// threshold the response is dominated by 1-line setters).
	// Callers can pass min_metric=0 explicitly to override the
	// default and see everything; any non-zero min_metric also
	// overrides.
	DefaultMinMetric int64
	// Severity controls whether a finding from this rule is fatal to
	// the gate. Three tiers per ESLint precedent (off/warn/error) —
	// see ADR-0018 and bead mache-ec1a06 for the prior-art rationale.
	//
	//   - "off"   — rule defined but never produces findings; useful
	//     for shipping a rule in a pack that's disabled for now
	//   - "warn"  — emits findings, exit 0 (observability default)
	//   - "error" — emits findings, exit 1 (rule author asserts this
	//     drift must not ship)
	//
	// Default ("") is treated as "warn" — preserves the existing
	// observability contract for all rules that haven't opted in.
	// Gate decision is made at CLI invocation via --fail-on (pylint
	// precedent): rule says what's true, gate decides whether truth
	// is fatal.
	Severity Severity
	// Tags is a free-form classification used for CLI selection via
	// --tags=foo,bar. Per ADR-0018 + research, NOT a closed enum:
	// stages (pre-commit, ci) emerge as CLI profiles from
	// (--tags × --fail-on) combinations, not from a fixed schema.
	// Cap at 3-5 tags per rule to avoid the clippy-group explosion;
	// expand the vocabulary deliberately, not reactively.
	Tags []string
	// PathShapedIDs marks a rule whose SQL reads meaning into node_id's
	// shape: a path prefix for "descendant", '/' counted for depth, a path
	// segment such as "/functions/". The vocabulary leaves node_id opaque,
	// and on projection-v6 it is an integer nid, so such a rule's answers
	// there would be silently wrong. It is refused on v6 instead, with an
	// error naming the work that removes the dependence (mache-2dc105).
	PathShapedIDs bool
}

SmellRule describes one structural code-smell pattern. Rules are language-aware: each carries the language(s) it applies to plus a SQL query template that runs against the `_ast` / `nodes` / `node_defs` / `node_refs` tables produced by `leyline parse`.

The query MUST select exactly seven columns:

source_id, node_id, start_byte, end_byte, start_row, start_col, metric

in that order. `metric` is an integer score (cyclomatic complexity, fan-out count, line length, etc.); binary rules that don't carry a metric emit `0 AS metric`. The handler shapes the row into a uniform response with 1-based line/column and an optional snippet.

`ScopeColumn` is the SQL expression the handler will compare to source_id when the caller passes `source_id` to find_smells (e.g. "lit.source_id" for AST-walking rules; "n.source_file" for rules that join via nodes). The query template MUST contain a single `%s` placeholder where the scope clause should be spliced in; rules unconcerned with scoping can leave ScopeColumn blank.

Threshold filtering on the metric column is server-side via the `min_metric` request arg — handler drops findings whose metric is below the cutoff before returning. Default 0 keeps every row.

Bead mache-6z2e tracks the broader "machelint" idea — declarative rules consumed via this tool. Today the registry is hard-coded; in the future it becomes user-extensible (declarative JSON, per-repo overrides, etc.).

func LoadExternalSmellRules

func LoadExternalSmellRules(dir string) ([]SmellRule, error)

LoadExternalSmellRules reads `*.json` files from dir, parses each as a SmellRule, validates the result, and returns the parsed rules. Fail-fast: any read, parse, validation, or collision error aborts and returns immediately — caller decides whether to log+skip or treat as fatal. init() logs and skips; tests fail loudly.

Validation:

  • ID must be non-empty and not collide with a built-in rule (or another external loaded from this same dir)
  • Query must be non-empty, contain exactly one '%s' placeholder for the scope clause, AND be a valid fmt.Sprintf format string (no stray `%` chars that fmt would treat as verbs — common trap with SQL `LIKE '%foo%'` patterns; escape as `%%`)
  • Requires may be empty (rule reads no tables) or list table names

func RegisteredRule

func RegisteredRule(id string) *SmellRule

RegisteredRule returns the registered rule with the given ID, or nil. The registry itself stays unexported — consumers outside the package get lookup, not mutation.

func (*SmellRule) Effective

func (r *SmellRule) Effective() Severity

Effective returns the resolved severity, treating the zero value ("") as warn — the default for any rule that hasn't opted in. Callers asking "is this rule fatal" should always go through Effective() rather than reading Severity directly.

Jump to

Keyboard shortcuts

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