Gaze Analyzer Protocol Specification

July 3, 2026 · View on GitHub

Version: 1.1.0 Transport: JSON-RPC 2.0 over stdin/stdout

Overview

The Gaze Analyzer Protocol enables external language analyzers to provide side effect detection, complexity analysis, and coverage data to Gaze. Gaze spawns the analyzer as a subprocess, communicates via JSON-RPC 2.0 over stdin/stdout, and uses the responses to compute CRAP scores, GazeCRAP scores, quadrant classifications, and fix strategies.

This protocol follows the same transport model as the Language Server Protocol (LSP): line-delimited JSON over stdin/stdout, with stderr reserved for diagnostics.

Lifecycle

gaze crap --analyzer snake-eyes ./src
|
+-- 1. Discover analyzer binary (CLI flag / .gaze.yaml / PATH)
+-- 2. Spawn: snake-eyes --stdio
+-- 3. initialize --> capabilities handshake
+-- 4. discover --> find source + test files (optional)
+-- 5. analyze --> detect side effects per function
+-- 6. complexity --> cyclomatic complexity per function
+-- 7. coverage --> line coverage per function
+-- 8. test_mapping --> map assertions to effects (optional)
+-- 9. classify_signals --> classification signals (optional)
+-- 10. shutdown --> clean exit
+-- 11. Gaze computes CRAP, GazeCRAP, quadrants, fix strategies

Discovery

Gaze finds analyzer binaries using a three-tier mechanism:

  1. CLI flag: --analyzer <binary> -- explicit binary name or path
  2. Config file: .gaze.yaml analyzers.<language>.command
  3. PATH convention: gaze-analyzer-<language> on PATH

The --language flag determines which language key to look up in tiers 2 and 3. When --analyzer is specified directly, --language is optional.

Message Format

Request

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "analyze",
  "params": {
    "root_path": "/path/to/project",
    "patterns": ["./..."]
  }
}

Response (success)

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { ... }
}

Response (error)

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32603,
    "message": "Internal error",
    "data": "optional details"
  }
}

Error Codes

Standard JSON-RPC 2.0 error codes:

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error

Methods

Required Methods

Every analyzer must implement these 5 methods.


initialize

Handshake method. Must be the first method called. Returns the analyzer's capabilities and identity.

Timeout: 30 seconds

Request params:

{
  "root_path": "/absolute/path/to/project",
  "config": {}
}
FieldTypeRequiredDescription
root_pathstringyesAbsolute path to the project root
configobjectnoAnalyzer-specific config from .gaze.yaml

Response result:

{
  "capabilities": {
    "discover": true,
    "test_mapping": true,
    "classify_signals": false,
    "streaming": false
  },
  "protocol_version": "1.1.0",
  "analyzer_name": "snake-eyes",
  "language": "python",
  "language_version": "3.12.0"
}
FieldTypeDescription
capabilities.discoverbooleanSupports the discover method
capabilities.test_mappingbooleanSupports the test_mapping method
capabilities.classify_signalsbooleanSupports the classify_signals method
capabilities.streamingbooleanSupports the analyze/stream method
protocol_versionstringProtocol version (semver)
analyzer_namestringHuman-readable analyzer name
languagestringPrimary language (e.g., "python", "rust")
language_versionstringRuntime/compiler version (optional)

analyze

Detect side effects for all functions in the project.

Timeout: 5 minutes

Request params:

{
  "root_path": "/path/to/project",
  "patterns": ["./..."]
}

Response result:

{
  "functions": [
    {
      "name": "divide",
      "package": "math_utils",
      "file": "math_utils/ops.py",
      "line": 20,
      "side_effects": [
        {
          "type": "ReturnValue",
          "description": "returns division result",
          "location": "math_utils/ops.py:25:5",
          "target": "result",
          "detail": {
            "python_type": "float"
          },
          "classification": {
            "label": "contractual",
            "confidence": 90
          }
        },
        {
          "type": "ErrorReturn",
          "description": "raises ZeroDivisionError",
          "location": "math_utils/ops.py:22:9",
          "target": "ZeroDivisionError",
          "detail": {
            "exception_class": "ZeroDivisionError"
          },
          "classification": {
            "label": "contractual",
            "confidence": 85
          }
        }
      ]
    }
  ]
}

Side Effect Types

Analyzers map their language concepts to Gaze's universal taxonomy of 48 canonical side effect types. Each type belongs to exactly one priority tier (P0–P4). Unknown types default to P4 tier with a warning.

P0 — Must Detect (6 types)
TypeTierDefinitionGo MappingPython Mapping
ReturnValueP0Function returns a valuereturn xreturn x
ErrorReturnP0Function signals an errorreturn errraise Exception
SentinelErrorP0Named error constant/value callers match againstvar ErrFoo = errors.New(...)class FooError(Exception)
ReceiverMutationP0Mutates the receiver/selfs.field = xself.field = x
PointerArgMutationP0Mutates a parameter passed by reference*arg = xmutable arg mutation
ErrorSignalP0Language-neutral error signaling mechanismreturn errraise, throw
P1 — High Value (11 types)
TypeTierDefinitionGo MappingPython Mapping
SliceMutationP1Mutates a dynamic array/list parameterslice[i] = xlist[i] = x
MapMutationP1Mutates a dictionary/hash map parameterm[k] = vdict[k] = v
GlobalMutationP1Writes to module-level / global statepkg.Var = xmodule-level write
WriterOutputP1Writes to an injected output streamw.Write(data)writer.write(data)
HTTPResponseWriteP1Writes to an HTTP response objectw.WriteHeader(200)response.write(data)
ChannelSendP1Sends a value to a concurrency channel/queuech <- vqueue.put(v)
ChannelCloseP1Closes a concurrency channel/queueclose(ch)queue.close()
DeferredReturnMutationP1Mutates a return value in a deferred/finally blockdefer func() { err = wrap(err) }()finally: result = ...
GeneratorYieldP1Yields a value from a generator/iterator(no direct equivalent)yield x
ContainerMutationP1Mutates a generic container parameter (set, deque, etc.)(no direct equivalent)set.add(x), deque.append(x)
StreamOutputP1Writes to a generic output streamstream.Write(data)stream.write(data)
P2 — Important (16 types)
TypeTierDefinitionGo MappingPython Mapping
FileSystemWriteP2Writes to filesystemos.WriteFile(...)open().write()
FileSystemDeleteP2Deletes a filesystem entryos.Remove(...)os.remove(...)
FileSystemMetaP2Modifies filesystem metadata (permissions, timestamps)os.Chmod(...)os.chmod(...)
DatabaseWriteP2Writes to a databasedb.Exec(...)cursor.execute(...)
DatabaseTransactionP2Manages a database transactiontx.Commit()conn.commit()
GoroutineSpawnP2Spawns a concurrent taskgo func(){}()threading.Thread(...)
PanicP2Unrecoverable error / panic / abortpanic(msg)sys.exit(1)
CallbackInvocationP2Invokes a function parameter (callback, closure)fn()callback()
LogWriteP2Writes to a logging systemlog.Printf(...)logging.info(...)
ContextCancellationP2Cancels a context or cancellation tokencancel()event.set()
AsyncGeneratorYieldP2Yields from an async generator(no direct equivalent)async def gen(): yield x
MetaprogrammingMutationP2Mutates program structure at runtime(no direct equivalent)setattr(obj, name, val)
DescriptorEffectP2Side effect via descriptor protocol(no direct equivalent)__set__, __delete__
ResourceManagementP2Acquires or releases an external resourcefile.Close()__enter__/__exit__
ImportSideEffectP2Module import triggers observable side effectsimport _ "pkg"import module (with side effects)
MonkeyPatchP2Runtime replacement of functions/methods(no direct equivalent)module.func = mock
P3 — Nice to Have (9 types)
TypeTierDefinitionGo MappingPython Mapping
StdoutWriteP3Writes to standard outputfmt.Println(...)print(...)
StderrWriteP3Writes to standard errorfmt.Fprintln(os.Stderr, ...)print(..., file=sys.stderr)
EnvVarMutationP3Modifies environment variablesos.Setenv(k, v)os.environ[k] = v
MutexOpP3Acquires or releases a mutex/lockmu.Lock()lock.acquire()
WaitGroupOpP3Operates on a synchronization barrierwg.Add(1)barrier.wait()
AtomicOpP3Performs an atomic memory operationatomic.AddInt64(...)(no direct equivalent)
TimeDependencyP3Depends on wall-clock timetime.Now()time.time()
ProcessExitP3Terminates the processos.Exit(1)sys.exit(1)
RecoverBehaviorP3Recovers from a panic/exceptionrecover()except Exception:
P4 — Exotic (6 types)
TypeTierDefinitionGo MappingPython Mapping
ReflectionMutationP4Mutates state via reflectionreflect.ValueOf(&x).Elem().Set(...)setattr(obj, name, val)
UnsafeMutationP4Mutates state via unsafe pointer operationsunsafe.Pointerctypes pointer writes
CgoCallP4Calls foreign function interface (FFI)C.function()ctypes.cdll.func()
FinalizerRegistrationP4Registers a finalizer/destructor callbackruntime.SetFinalizer(...)weakref.finalize(...)
SyncPoolOpP4Operates on an object poolsync.Pool.Get/Put(no direct equivalent)
ClosureCaptureMutationP4Mutates a variable captured by a closurecaptured var writecaptured var write
Language-Neutral Aliases

The following aliases allow analyzers to use language-neutral names that resolve to the same canonical type:

AliasResolves ToUse When
AsyncTaskSpawnGoroutineSpawnSpawning async tasks, threads, coroutines
AsyncMessageSendChannelSendSending to queues, mailboxes, actors
AsyncChannelCloseChannelCloseClosing queues, mailboxes, streams
BarrierOpWaitGroupOpBarriers, latches, countdown events
PanicRecoveryRecoverBehaviorException handling, panic recovery
FFICallCgoCallForeign function interface calls (ctypes, napi, JNI)
ObjectPoolOpSyncPoolOpObject/connection pool operations
DeferredMutationDeferredReturnMutationFinally blocks, scope guards, RAII cleanup
ArgumentMutationPointerArgMutationMutating parameters passed by reference
ProcessTerminationPanicProcess abort, unrecoverable errors
SentinelErrorDeclSentinelErrorNamed error type/constant declarations

Aliases are accepted in analyze responses and resolve to the canonical type for scoring and classification. The full taxonomy is defined in internal/taxonomy/types.go.

The detail Field

Each side effect in the analyze response may include an optional detail object containing language-specific metadata. This field is opaque to Gaze — it is passed through to JSON output and AI report pipelines without interpretation. Gaze does not use detail for scoring, classification, or CRAP computation.

Use detail to provide context that helps AI report generators or downstream tooling understand the effect in language-specific terms (e.g., exception class names, decorator metadata, type annotations).

Size guidance: Keep detail payloads small — under 1KB per effect. Large payloads increase JSON output size and may degrade AI report quality. Include only the metadata needed for downstream interpretation.

Classification

Each side effect may include a classification object:

FieldTypeDescription
labelstring"contractual", "incidental", or "ambiguous"
confidenceinteger0-100 confidence score

When classification is null, Gaze uses default classification based on the effect type's tier.

Language-neutral side effect type aliases: External analyzers may use either Go-specific type names (e.g., GoroutineSpawn, ChannelSend, CgoCall) or language-neutral aliases (e.g., AsyncTaskSpawn, AsyncMessageSend, FFICall). The aliases map to the same string values as the Go-specific names: AsyncTaskSpawn = GoroutineSpawn, AsyncMessageSend = ChannelSend, AsyncChannelClose = ChannelClose, BarrierOp = WaitGroupOp, PanicRecovery = RecoverBehavior, FFICall = CgoCall, ObjectPoolOp = SyncPoolOp.


analyze/stream (optional)

Streaming alternative to the batch analyze method. When the analyzer declares capabilities.streaming: true in the initialize response, Gaze calls analyze/stream instead of analyze.

Timeout: 5 minutes

Request params: Same as analyze.

Response format: Instead of a single JSON-RPC response, the analyzer writes one JSON object per line (JSONL) to stdout. Each line represents one AnalyzedFunction — the same schema as the functions array elements in the batch response.

{"name":"add","package":"math_utils","file":"math_utils/ops.py","line":1,"side_effects":[]}
{"name":"multiply","package":"math_utils","file":"math_utils/ops.py","line":10,"side_effects":[{"type":"ReturnValue","description":"returns result","location":"ops.py:12:5","target":"result"}]}
{"name":"divide","package":"math_utils","file":"math_utils/ops.py","line":20,"side_effects":[{"type":"ReturnValue","description":"returns result","location":"ops.py:25:5","target":"result"},{"type":"ErrorReturn","description":"raises ZeroDivisionError","location":"ops.py:22:9","target":"ZeroDivisionError"}]}

Error handling: If a line contains invalid JSON, Gaze stops processing (fail-fast) and reports the error with the line number and content.

When to use streaming: Streaming is beneficial for large codebases (10K+ functions) where a single batch response would be multi-megabyte. For small codebases, the batch analyze method is simpler and sufficient.


complexity

Compute cyclomatic complexity for all functions.

Timeout: 5 minutes

Request params:

{
  "root_path": "/path/to/project",
  "patterns": ["./..."]
}

Response result:

{
  "functions": [
    {
      "name": "divide",
      "package": "math_utils",
      "file": "math_utils/ops.py",
      "line": 20,
      "complexity": 5
    }
  ]
}
FieldTypeDescription
namestringFunction or method name
packagestringPackage/module path
filestringSource file path (relative or absolute)
lineintegerLine number of function declaration
complexityintegerCyclomatic complexity value

coverage

Produce per-function line coverage data. The analyzer is responsible for running tests and collecting coverage internally.

Timeout: 5 minutes

Request params:

{
  "root_path": "/path/to/project",
  "patterns": ["./..."]
}

Response result:

{
  "functions": [
    {
      "file": "math_utils/ops.py",
      "function": "divide",
      "start_line": 20,
      "end_line": 30,
      "covered_stmts": 0,
      "total_stmts": 10,
      "percentage": 0.0
    }
  ]
}
FieldTypeDescription
filestringSource file path
functionstringFunction or method name
start_lineintegerFunction declaration start line
end_lineintegerFunction body end line
covered_stmtsintegerNumber of statements covered by tests
total_stmtsintegerTotal number of statements
percentagenumberCoverage percentage (0.0-100.0)

shutdown

Request the analyzer to shut down cleanly. The analyzer should finish any pending work and exit.

Timeout: 10 seconds

Request params: none (or null)

Response result: {}

After receiving the shutdown response, Gaze closes stdin and waits for the process to exit.


Optional Methods

These methods are declared via capabilities in the initialize response. Gaze only calls them when the corresponding capability is true.


discover (optional)

Find source and test files in the project. Reserved for future use -- currently not consumed by Gaze's provider interfaces.

Capability: discover

Request params:

{
  "root_path": "/path/to/project"
}

Response result:

{
  "source_files": ["math_utils/ops.py", "math_utils/helpers.py"],
  "test_files": ["tests/test_ops.py"],
  "framework": "pytest"
}

test_mapping (optional)

Map test assertions to side effects. When supported, this enables GazeCRAP scoring (contract coverage).

Capability: test_mapping

Request params:

{
  "root_path": "/path/to/project",
  "patterns": ["./..."]
}

Response result:

{
  "mappings": [
    {
      "test_function": "test_multiply",
      "test_file": "tests/test_ops.py",
      "assertion_location": "tests/test_ops.py:10",
      "assertion_type": "equality",
      "target_function": "multiply",
      "target_package": "math_utils",
      "side_effect_type": "ReturnValue",
      "confidence": 80
    }
  ]
}
FieldTypeDescription
test_functionstringTest function name
test_filestringTest file path
assertion_locationstringSource position of the assertion
assertion_typestringKind of assertion (e.g., "equality", "error_check")
target_functionstringFunction under test
target_packagestringPackage of the function under test
side_effect_typestringType of side effect being asserted on
confidenceintegerMapping confidence (0-100)

classify_signals (optional)

Provide raw classification signals for Gaze's scoring engine. When supported, these signals are fed into classify.ComputeScore alongside Gaze's built-in signals.

Capability: classify_signals

Request params:

{
  "root_path": "/path/to/project",
  "patterns": ["./..."]
}

Response result:

{
  "signals": [
    {
      "function": "divide",
      "package": "math_utils",
      "side_effect_type": "ErrorReturn",
      "source": "docstring",
      "weight": 15,
      "reasoning": "docstring mentions ZeroDivisionError"
    }
  ]
}
FieldTypeDescription
functionstringFunction name
packagestringPackage path
side_effect_typestringSide effect type this signal relates to
sourcestringSignal type (e.g., "docstring", "type_annotation", "decorator")
weightintegerNumeric contribution to confidence score
reasoningstring(optional) Explanation of why this signal was applied

Error Handling

Required method errors

When a required method (analyze, complexity, coverage) returns a JSON-RPC error, Gaze propagates the error and exits non-zero. The error message is displayed to the user.

Optional method errors

When an optional method (discover, test_mapping, classify_signals) returns an error, Gaze logs a warning to stderr and degrades gracefully:

  • discover error: no impact (not currently consumed)
  • test_mapping error: GazeCRAP is unavailable
  • classify_signals error: uses pre-classified effects from analyze

Process crashes

If the analyzer process exits unexpectedly (crash, segfault), Gaze detects the closed stdin/stdout pipes and returns an error with the process's stderr output for diagnostics.

Timeouts

Each method has a default timeout (30s for handshake methods, 5min for analysis methods). When the timeout expires, Gaze kills the subprocess and returns a timeout error.

Configuration

.gaze.yaml

analyzers:
  python:
    command: snake-eyes
    args: ["--stdio"]
  rust:
    command: /usr/local/bin/gaze-analyzer-rust
    args: ["--stdio", "--edition=2021"]

CLI Flags

FlagDescription
--analyzer <binary>Explicit analyzer binary name or path
--language <lang>Target language for config/PATH discovery

Building an Analyzer

To build a Gaze-compatible analyzer:

  1. Accept --stdio flag: Read JSON-RPC requests from stdin, write responses to stdout, diagnostics to stderr.
  2. Implement the 5 required methods: initialize, analyze, complexity, coverage, shutdown.
  3. Declare capabilities: In the initialize response, set test_mapping: true if you can map assertions to effects (enables GazeCRAP).
  4. Map to Gaze's taxonomy: Use Gaze's SideEffectType constants for the type field in analyze responses.
  5. Follow naming convention: Name your binary gaze-analyzer-<language> for automatic PATH discovery.

Minimal Example (Python)

#!/usr/bin/env python3
"""Minimal Gaze analyzer for demonstration."""
import json
import sys

def handle(request):
    method = request["method"]
    rid = request["id"]

    if method == "initialize":
        return {"jsonrpc": "2.0", "id": rid, "result": {
            "capabilities": {"discover": False, "test_mapping": False, "classify_signals": False},
            "protocol_version": "1.1.0",
            "analyzer_name": "minimal-python",
            "language": "python"
        }}
    elif method == "analyze":
        return {"jsonrpc": "2.0", "id": rid, "result": {"functions": []}}
    elif method == "complexity":
        return {"jsonrpc": "2.0", "id": rid, "result": {"functions": []}}
    elif method == "coverage":
        return {"jsonrpc": "2.0", "id": rid, "result": {"functions": []}}
    elif method == "shutdown":
        return {"jsonrpc": "2.0", "id": rid, "result": {}}
    else:
        return {"jsonrpc": "2.0", "id": rid, "error": {
            "code": -32601, "message": f"Method not found: {method}"
        }}

for line in sys.stdin:
    request = json.loads(line.strip())
    response = handle(request)
    print(json.dumps(response), flush=True)
    if request["method"] == "shutdown":
        break

Testing Your Analyzer

Use Gaze's fake analyzer as a reference implementation: internal/protocol/testdata/fake_analyzer/main.go.

# Test with gaze crap
gaze crap --analyzer ./my-analyzer --language python ./src

# Test with JSON output (no AI adapter needed)
gaze report --analyzer ./my-analyzer --format=json ./src