sourceanalysis

package
v0.0.0-...-86b21bc Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package sourceanalysis defines the abstract code-analysis surface the planner consumes when computing semantic conflicts (gm-s47n.3, docs/design/work-planning.md §4 Layer 2).

The interface is intentionally small: four methods cover everything the planner needs to detect that two beads with disjoint file targets nonetheless invalidate each other's API contracts.

Implementations

  • GitNexus: the rich one — uses the gitnexus index (gm-s47n.3.3).
  • Noop: graceful-degradation default — every query returns an empty slice with ErrUnavailable in Describe so callers can decide whether to skip semantic conflict detection or surface a warning (gm-s47n.3.2).

The planner MUST handle either implementation transparently. When source analysis is unavailable, target-glob conflict detection still works; semantic-conflict detection is silently skipped per docs/design/work-planning.md §5.3, with the skip logged so an operator can see why two beads got dispatched in parallel that later turned out to conflict.

Wire-shape stability

Target / Symbol / Diff are wire shapes: they cross the planner ↔ analysis boundary and may eventually cross a process boundary if a future implementation runs out-of-band. Keep them JSON-friendly (no interface-typed fields, no unexported state). Adding fields is fine; renaming or repurposing existing fields is a breaking change.

Index

Constants

View Source
const DefaultFreshnessThreshold = 4 * time.Hour

DefaultFreshnessThreshold matches spec §8.1's wall-clock floor. Operators tune per rig via the planner's settings; this is the fallback when callers pass 0.

Variables

View Source
var ErrUnavailable = errors.New("sourceanalysis: backend unavailable")

ErrUnavailable is returned by Describe (or wrapped via errors.Is by any other method) when the underlying source analysis backend is configured but cannot answer queries right now — index missing, daemon down, network partition, etc.

Callers (the planner, work-planning.md §5.3) treat ErrUnavailable as "skip semantic conflict detection, log the skip" rather than hard failure. It is never appropriate to surface ErrUnavailable as a fatal error to the operator; the planner degrades gracefully to glob-only conflict detection.

Functions

func RunContract

func RunContract(t *testing.T, sa SourceAnalysis, probe Target)

RunContract exercises the universal SourceAnalysis contract a binding MUST satisfy regardless of backend (gm-s47n.3.2 noop and gm-s47n.3.3 GitNexus both pass it). Bindings call this from a test that supplies a fresh SourceAnalysis instance plus a single known-existing target in the bound index.

Contract:

  • Dependents and Dependencies return nil (not nil-safe slice) OR a non-nil slice; never panic.
  • When the backend is unavailable they return (nil, ErrUnavailable) wrapped via errors.Is — no other error sentinels.
  • PublicContractChanges over an empty diff returns an empty slice with no error (degenerate case, never spuriously fails).
  • Describe never returns ErrUnavailable itself; the field is reported via Capabilities.Available + Capabilities.Reason.

The probe target may be the zero value when the backend is the noop binding — the contract still holds because every method returns the empty/unavailable answer for any input.

Types

type Capabilities

type Capabilities struct {
	// Backend names the implementation in human-readable form
	// ("gitnexus", "noop", "ctags", "lsp", ...). Free string so a
	// future binding doesn't need a SourceAnalysisKind enum churn.
	Backend string `json:"backend"`
	// Available is false when the backend is configured but cannot
	// answer queries right now (index missing, daemon down, etc).
	// The planner treats !Available as "skip semantic conflict
	// detection, log the skip" — a degraded-but-functional fall
	// through, not an error.
	Available bool `json:"available"`
	// IndexUpdatedAt is the most recent index refresh. Zero value
	// means "no index, or freshness unknown".
	IndexUpdatedAt time.Time `json:"index_updated_at,omitempty"`
	// Reason carries a short human string when Available is false
	// or the index is stale. Surface text only — never parsed.
	Reason string `json:"reason,omitempty"`
}

Capabilities describes a SourceAnalysis implementation's current state. The planner reads this once per dispatch cycle to decide whether semantic-conflict detection is reliable enough to act on.

IndexUpdatedAt is the wall-clock when the backing index last caught up with the working tree. The planner compares it against recent merge waves (work-planning.md §8) to decide whether a scheduled re-index is overdue.

type Diff

type Diff struct {
	Repository string     `json:"repository"`
	Files      []string   `json:"files"`
	Hunks      []DiffHunk `json:"hunks,omitempty"`
}

Diff is the input to PublicContractChanges. The planner builds it from a bead's actual or projected file changes; SourceAnalysis returns the subset that touch a public contract.

Files is repo-relative. Hunks is optional — implementations that can do better than file-level analysis read it; ones that can't fall back to "every public symbol declared in this file" and over-report rather than under-report.

type DiffHunk

type DiffHunk struct {
	Path      string `json:"path"`
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
}

DiffHunk is the optional, line-granular view of a single file edit. Implementations that don't consume it can ignore the field.

type FreshGateConfig

type FreshGateConfig struct {
	// Threshold is the staleness budget. Zero → DefaultFreshnessThreshold.
	Threshold time.Duration
	// CacheTTL bounds how often the gate consults Describe. Zero
	// → 30 seconds. The cache is per-instance, in-memory, only
	// active during a single planner pass.
	CacheTTL time.Duration
	// Now is injected for tests. Zero → time.Now.
	Now func() time.Time
}

FreshGateConfig configures NewFreshGate. Zero-value fields fall back to defaults so a caller building a "use the typical setup" gate is one line:

gated := sourceanalysis.NewFreshGate(real, sourceanalysis.FreshGateConfig{})

type FreshnessReport

type FreshnessReport struct {
	Backend   string `json:"backend"`
	Available bool   `json:"available"`
	// Stale is true when Available is true AND IndexUpdatedAt is
	// non-zero AND the index is older than Threshold. False on
	// every other path — including unavailable backends and
	// missing IndexUpdatedAt — so the planner doesn't double-fire
	// "skip + warn" against a backend already returning
	// ErrUnavailable for an unrelated reason.
	Stale     bool          `json:"stale"`
	Age       time.Duration `json:"age"`
	Threshold time.Duration `json:"threshold"`
	// Reason is a one-line operator-facing explanation. Empty
	// when the index is fresh (callers don't need a "why" for
	// the happy path). Surface text only.
	Reason string `json:"reason,omitempty"`
}

FreshnessReport summarises whether the backend's index is fresh enough to trust for semantic-conflict detection. Returned by CheckFreshness; also threaded through the gate's stale-skip reason.

Wire shape: stable JSON tags so the planner's audit / notices payloads can carry the report without reinventing the schema.

func CheckFreshness

func CheckFreshness(caps Capabilities, threshold time.Duration, now time.Time) FreshnessReport

CheckFreshness inspects the given Capabilities snapshot and returns the freshness report. Pure — same inputs, same output.

now is injected for deterministic tests; pass time.Now in production.

type GitNexus

type GitNexus struct {
	// BinPath is the path to the gitnexus executable. Empty means
	// "look up `gitnexus` on PATH".
	BinPath string
	// RepoName is the logical name registered in the gitnexus index
	// registry (`gitnexus list`). Required when the operator has
	// indexed multiple repos — the CLI rejects ambiguous calls.
	RepoName string
	// RepoPath is the filesystem path to the repository checkout.
	// Used to locate .gitnexus/meta.json for [Describe].
	RepoPath string
	// contains filtered or unexported fields
}

GitNexus is the SourceAnalysis binding backed by the local gitnexus CLI (gm-s47n.3.3). It shells out to `gitnexus cypher` for the dependency / contract-change queries and reads the per-repo .gitnexus/meta.json directly for [Describe].

The binding deliberately stays narrow:

  • Dependents / Dependencies operate at file-path granularity (the planner cares about which files import which); the gitnexus index already encodes node→file relationships, so a single Cypher query handles the lookup without per-symbol iteration.
  • PublicContractChanges over-reports per the interface contract: every exported symbol declared in any file in the diff comes back, even if the line-level diff didn't touch it. The planner's downstream consumers prefer false positives to false negatives.
  • Describe never shells out — it parses meta.json on disk so the planner can poll cheaply.

Concurrency: safe for concurrent use. The exec hook is set once and never mutated; the rest is stateless beyond struct fields.

When the gitnexus binary cannot be found on PATH (or the configured BinPath), every query returns ErrUnavailable and Describe surfaces the missing backend via Capabilities.Reason.

func NewGitNexus

func NewGitNexus(repoName, repoPath string) *GitNexus

NewGitNexus builds a binding pointed at one repo. RepoPath is the filesystem checkout (used by Describe); RepoName is the gitnexus registry name passed via -r to subcommands.

func (*GitNexus) Dependencies

func (g *GitNexus) Dependencies(ctx context.Context, target Target) ([]Target, error)

Dependencies is the inverse of Dependents over the same edge set.

func (*GitNexus) Dependents

func (g *GitNexus) Dependents(ctx context.Context, target Target) ([]Target, error)

Dependents returns the set of files containing a node with an edge into ANY node declared in target.Path. The result is deduplicated by file path; the target file itself is excluded.

func (*GitNexus) Describe

func (g *GitNexus) Describe(_ context.Context) (Capabilities, error)

Describe reads .gitnexus/meta.json on disk. Available is true only when the file parses cleanly; otherwise Reason carries a short operator-visible explanation. Describe never returns ErrUnavailable per the SourceAnalysis contract.

func (*GitNexus) PublicContractChanges

func (g *GitNexus) PublicContractChanges(ctx context.Context, diff Diff) ([]Symbol, error)

PublicContractChanges returns every exported symbol declared in any file in the diff. Per the interface contract this is the over-reporting fallback — implementations that can reason about hunks would narrow further; this binding does not.

type Noop

type Noop struct {
	Reason string
}

Noop is the graceful-degradation SourceAnalysis (gm-s47n.3.2). Use it as the default binding when the operator hasn't installed a real backend (gitnexus, ctags, lsp). Every query returns (nil, ErrUnavailable) so the planner sees a uniform "skip semantic conflict detection, log the skip" signal — degraded but functional, per docs/design/work-planning.md §5.3.

Reason is the operator-visible explanation surfaced via Describe; the planner copies it into its own dispatch log so an operator browsing "why didn't gemba catch this conflict?" sees a clear pointer to the missing backend.

Concurrency: trivially safe — the struct holds no mutable state.

func NewNoop

func NewNoop() *Noop

NewNoop builds a Noop with a default reason. Operators who care about the wording can construct &Noop{Reason: "..."} directly.

func (*Noop) Dependencies

func (n *Noop) Dependencies(_ context.Context, _ Target) ([]Target, error)

func (*Noop) Dependents

func (n *Noop) Dependents(_ context.Context, _ Target) ([]Target, error)

func (*Noop) Describe

func (n *Noop) Describe(_ context.Context) (Capabilities, error)

func (*Noop) PublicContractChanges

func (n *Noop) PublicContractChanges(_ context.Context, _ Diff) ([]Symbol, error)

type SourceAnalysis

type SourceAnalysis interface {
	// Dependents returns files that import, call, or otherwise
	// depend on the given target. The result MAY include the
	// target itself when a self-reference is meaningful; callers
	// filter by identity if they need a strict "other files" set.
	//
	// Returns (nil, ErrUnavailable) when the backend is configured
	// but cannot answer (index missing, daemon down).
	Dependents(ctx context.Context, target Target) ([]Target, error)

	// Dependencies returns files the given target depends on —
	// inverse of Dependents over the same edge set.
	//
	// Same ErrUnavailable contract as Dependents.
	Dependencies(ctx context.Context, target Target) ([]Target, error)

	// PublicContractChanges returns the subset of symbols touched by
	// the diff that are public-contract entities (exported APIs,
	// route signatures, exported types). Best-effort — backends
	// that can't reason about diffs SHOULD return the file-level
	// projection (every exported symbol declared in any file in
	// the diff) rather than nothing, so the planner over-reports
	// rather than misses a real semantic conflict.
	//
	// Same ErrUnavailable contract.
	PublicContractChanges(ctx context.Context, diff Diff) ([]Symbol, error)

	// Describe reports the backend's current health: which
	// implementation is bound, whether it can answer, when its
	// index last refreshed, and a one-line reason if degraded.
	//
	// Describe MUST NOT itself return ErrUnavailable — the whole
	// point is for it to TELL you the backend is unavailable.
	// Implementation-internal errors (process exec failure, etc)
	// are returned as a normal error.
	Describe(ctx context.Context) (Capabilities, error)
}

SourceAnalysis is the abstract code-analysis surface the planner consumes when computing semantic conflicts (work-planning.md §4 Layer 2 + §5.3).

The four methods are intentionally independent so a binding can implement the cheap ones (Dependents / Dependencies) without committing to a diff-aware contract analysis. Methods that a backend cannot answer return an empty result with ErrUnavailable rather than fabricating data.

Implementations MUST be safe for concurrent use — the planner fans queries out across the ready set and expects independent (target, target) pairs to scale linearly.

func NewFreshGate

func NewFreshGate(inner SourceAnalysis, cfg FreshGateConfig) SourceAnalysis

NewFreshGate wraps inner so its responses are gated on freshness. nil inner returns nil (callers building optional decorators chain freely). The gate itself implements SourceAnalysis.

type Symbol

type Symbol struct {
	Target Target `json:"target"`
	Name   string `json:"name"`
	Kind   string `json:"kind,omitempty"`
}

Symbol names a single exported entity (function, type, method, route handler, …) in a Target file. The planner uses these to reason about public-contract changes — see docs/design/work-planning.md §5.3.

Kind is a free string so the interface doesn't have to enumerate every language's symbol vocabulary. Concrete implementations document the kinds they emit; the planner treats them opaquely.

type Target

type Target struct {
	// Repository is the bead's primary repository id (matches
	// core.Repository.id). Required when the planner's working set
	// spans multiple repos so an "auth.go" in repo A doesn't get
	// linked to "auth.go" in repo B.
	Repository string `json:"repository"`
	// Path is the repo-relative file path or glob.
	Path string `json:"path"`
}

Target identifies a file (or path glob) the planner cares about. In practice this is the same shape as a WorkItem.targets[] entry (gm-s47n.1) — keeping the type local rather than reaching into core avoids a cyclic import and lets sourceanalysis evolve its representation independently if globbing later needs metadata (e.g. role hints, exclusion flags) the data layer doesn't carry.

Path is repo-relative, forward-slashed, glob-expanded by the caller before reaching SourceAnalysis methods that take a concrete file (e.g. Dependents). Implementations MAY reject values containing `..` or absolute paths.

Jump to

Keyboard shortcuts

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