Classification

April 9, 2026 · View on GitHub

Once Gaze detects a function's side effects, the next question is: which of these effects are part of the function's contract? Classification answers this by assigning each side effect one of three labels — contractual, ambiguous, or incidental — based on weighted evidence from five mechanical signal analyzers.

Classification is the bridge between raw side effect detection and meaningful quality metrics. Only contractual effects count toward contract coverage. Only incidental effects count toward over-specification. Ambiguous effects are excluded from both metrics.

The Three Labels

LabelMeaningMetric Impact
ContractualThe effect is part of the function's behavioral contract — callers depend on itCounted in contract coverage (denominator and potentially numerator)
IncidentalThe effect is an implementation detail — callers should not depend on itCounted in over-specification if asserted on
AmbiguousInsufficient evidence to classify — could be eitherExcluded from both metrics

Confidence Scoring

Each side effect receives a confidence score from 0 to 100. The score determines the label:

  • Score >= 75 (default contractual threshold): Contractual
  • Score >= 50 and < 75: Ambiguous
  • Score < 50 (default incidental threshold): Incidental

These thresholds are configurable via .gaze.yaml (see Configuring Thresholds below).

How the Score Is Computed

The confidence score starts at a base value that depends on the effect's tier, then accumulates evidence from five signal analyzers, applies a contradiction penalty if conflicting signals exist, and clamps to the 0–100 range.

Step 1: Base + Tier Boost

Every effect starts at a base confidence of 50. A tier-based boost is added:

TierBoostEffective Starting ScoreRationale
P0+2575P0 effects (returns, errors, mutations) are definitionally contractual — they are a function's direct observable outputs
P1+1060P1 effects (channels, writers, globals) are frequently contractual but context-dependent
P2–P4+050Higher-tier effects genuinely depend on context for classification

This means P0 effects reach the default contractual threshold (75) with no additional signals. A ReturnValue effect is contractual by default — you need negative evidence to push it below the threshold.

Step 2: Signal Accumulation

Each of the five signal analyzers contributes a weighted signal (positive or negative). Signals with zero weight or empty source are skipped. The weights are added to the running score.

Step 3: Contradiction Penalty

If both positive and negative signals are present (e.g., the function name suggests contractual but the godoc says "logs"), a contradiction penalty of -20 is applied. This pushes conflicting evidence toward the ambiguous range, reflecting genuine uncertainty.

Step 4: Clamping

The final score is clamped to the range [0, 100].

The Five Signal Analyzers

1. Interface Satisfaction (max weight: +30)

Checks whether the function's receiver type satisfies any interface defined in the module. When a method appears in an interface, its side effects are strong contractual evidence — the interface defines the contract.

Example: If (*Store).Save satisfies Repository.Save, the ReceiverMutation effect of Save receives a +30 signal.

Weight: +30 when the method satisfies an interface that declares it; 0 otherwise.

2. API Surface Visibility (max weight: +20)

Evaluates whether the side effect is observable through the exported API. Three dimensions contribute independently:

DimensionWeightCondition
Exported function+8The function itself is exported (starts with uppercase)
Exported return type+6At least one return type is exported
Exported receiver type+6The receiver type is exported

The total is capped at +20. An exported method on an exported type with exported return types receives the full +20.

Weight: 0 to +20 depending on how many dimensions match.

3. Caller Dependency (max weight: +15)

Scans all packages in the module for call sites that reference the target function. More callers means more code depends on the function's behavior, strengthening the contractual case.

Caller CountWeight
00 (no signal)
1+5
2–3+10
4++15

Weight: 0 to +15 based on the number of distinct packages that call the function.

4. Naming Convention (max weight: +10 / -10, sentinel: +30)

Matches the function name against Go community naming conventions. Certain prefixes strongly imply contractual or incidental behavior.

Contractual prefixes (weight: +10 when the effect type matches the prefix's implied effects):

PrefixImplied Effect Types
Get, Fetch, Load, ReadReturnValue, ErrorReturn
Save, Write, UpdateReceiverMutation, PointerArgMutation, ErrorReturn
SetReceiverMutation, PointerArgMutation
Delete, RemoveReceiverMutation, ErrorReturn
Handle, ProcessAll effect types
Compute, Analyze, Classify, Parse, Build, NewReturnValue, ErrorReturn

Incidental prefixes (weight: -10): log, Log, debug, Debug, trace, Trace, print, Print

Sentinel error naming (weight: +30): Variables with the Err prefix and SentinelError type receive a boosted +30 weight. Sentinel errors are unambiguously contractual by convention — they are exported, named with the Err prefix, and exist solely to be matched by callers. The higher weight ensures sentinels reach the contractual threshold even without other signals (since package-level variables cannot receive interface, visibility, or godoc signals).

5. GoDoc Comment (max weight: +15 / -15)

Parses the function's documentation comment for behavioral declarations.

Contractual keywords (weight: +15 when the effect type matches, +5 when a contractual keyword is found but the effect type doesn't directly match):

KeywordImplied Effect Types
returnsReturnValue, ErrorReturn
writes, modifies, updates, sets, persists, storesReceiverMutation, PointerArgMutation
deletes, removesReceiverMutation

Incidental keywords (weight: -15): logs, prints, traces, debugs

Worked Example

Consider an exported method (*Store).Save that has two detected side effects:

  1. ErrorReturn (P0): The function returns an error
  2. ReceiverMutation (P0): The function mutates s.data

For the ErrorReturn effect:

StepValueRunning Score
Base5050
Tier boost (P0)+2575
Interface signal (Repository.Save)+30105
Visibility signal (exported function + exported receiver)+14119
Caller signal (3 callers)+10129
Naming signal (Save prefix implies ErrorReturn)+10139
GoDoc signal ("persists" keyword)+5144
Contradiction penalty0 (no negative signals)144
Clamp to [0, 100]100

Result: Contractual (confidence 100 >= 75)

For a LogWrite effect on a function named logRequest:

StepValueRunning Score
Base5050
Tier boost (P2)+050
Naming signal (log prefix)-1040
GoDoc signal ("logs" keyword)-1525
Contradiction penalty0 (only negative signals)25

Result: Incidental (confidence 25 < 50)

Configuring Thresholds

The default thresholds (contractual >= 75, incidental < 50) can be adjusted in .gaze.yaml:

classification:
  thresholds:
    contractual: 75   # Score at or above this = contractual
    incidental: 50     # Score below this = incidental
                       # Scores in [incidental, contractual) = ambiguous

Lowering the contractual threshold makes more effects contractual (stricter contract coverage requirements). Raising the incidental threshold makes more effects incidental (more lenient contract coverage).

What's Next

  • Scoring — how classification feeds into CRAP and GazeCRAP scores
  • Quality Assessment — how contract coverage and over-specification are computed from classified effects
  • Side Effects — the full taxonomy of 37 effect types