a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
1896 lines
93 KiB
Go
1896 lines
93 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"runtime"
|
||
"slices"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/spf13/viper"
|
||
|
||
"github.com/zzet/gortex/internal/llm"
|
||
"github.com/zzet/gortex/internal/platform"
|
||
)
|
||
|
||
type GuardRule struct {
|
||
Name string `mapstructure:"name" yaml:"name"`
|
||
Kind string `mapstructure:"kind" yaml:"kind"` // "co-change" | "boundary"
|
||
Source string `mapstructure:"source" yaml:"source"` // package/path prefix
|
||
Target string `mapstructure:"target" yaml:"target"` // package/path prefix
|
||
Message string `mapstructure:"message" yaml:"message,omitempty"` // human-readable explanation
|
||
// Severity tiers the violation: "error" → a change_contract refuse,
|
||
// "warn" → warn, "info" → annotate. Empty defaults to "warn" so a rule
|
||
// advises until it explicitly opts into blocking.
|
||
Severity string `mapstructure:"severity" yaml:"severity,omitempty"`
|
||
// Except lists path globs exempt from this rule — a source symbol whose
|
||
// file matches one is never flagged.
|
||
Except []string `mapstructure:"except" yaml:"except,omitempty"`
|
||
}
|
||
|
||
type GuardsConfig struct {
|
||
Rules []GuardRule `mapstructure:"rules" yaml:"rules,omitempty"`
|
||
}
|
||
|
||
// EventRule is one declarative event-boundary / dispatch-guard constraint over
|
||
// the pub/sub graph: which paths may produce or consume a topic, whether a
|
||
// produced topic must have a consumer, and which paths are forbidden from it.
|
||
type EventRule struct {
|
||
Name string `mapstructure:"name" yaml:"name,omitempty"`
|
||
Topic string `mapstructure:"topic" yaml:"topic,omitempty"` // glob on event/topic name; empty = any
|
||
Producer string `mapstructure:"producer" yaml:"producer,omitempty"` // path glob allowed to produce
|
||
Consumer string `mapstructure:"consumer" yaml:"consumer,omitempty"` // path glob allowed to consume
|
||
RequireConsumer bool `mapstructure:"require_consumer" yaml:"require_consumer,omitempty"` // a produced topic must have a consumer
|
||
Forbid []string `mapstructure:"forbid" yaml:"forbid,omitempty"` // path globs forbidden to produce / consume
|
||
Severity string `mapstructure:"severity" yaml:"severity,omitempty"`
|
||
Message string `mapstructure:"message" yaml:"message,omitempty"`
|
||
}
|
||
|
||
// EventsConfig is the `events:` block of .gortex.yaml — declarative
|
||
// event-boundary rules over the pub/sub graph (EdgeEmits / EdgeProducesTopic /
|
||
// EdgeListensOn / EdgeConsumesTopic), evaluated as a change_contract rule
|
||
// family.
|
||
type EventsConfig struct {
|
||
Rules []EventRule `mapstructure:"rules" yaml:"rules,omitempty"`
|
||
}
|
||
|
||
// ArchitectureConfig is the declarative architecture-rules block of
|
||
// .gortex.yaml — the top-level `architecture:` key. It promotes the
|
||
// flat guards: list into named layers with directional allow/deny
|
||
// dependency constraints; check_guards evaluates it on every change
|
||
// set. The flat guards: list keeps working unchanged alongside it.
|
||
type ArchitectureConfig struct {
|
||
// Layers maps a layer name to its definition. A file belongs to
|
||
// the layer whose Paths globs match it — or, when a layer
|
||
// declares no Paths, whose name appears as a path segment.
|
||
Layers map[string]LayerRule `mapstructure:"layers" yaml:"layers,omitempty"`
|
||
// Rules are per-layer / per-pattern dependency-cone constraints —
|
||
// fan-out caps and caller-boundary restrictions — evaluated on
|
||
// top of the layer allow/deny graph.
|
||
Rules []ArchRule `mapstructure:"rules" yaml:"rules,omitempty"`
|
||
// Severity tiers the layer allow/deny violations: "error" → a
|
||
// change_contract refuse, "warn" → warn, "info" → annotate. Empty
|
||
// defaults to "warn".
|
||
Severity string `mapstructure:"severity" yaml:"severity,omitempty"`
|
||
}
|
||
|
||
// ArchRule is one architecture constraint scoped to a layer or a file
|
||
// glob: a dependency-cone fan-out limit, a caller-boundary
|
||
// restriction, or both.
|
||
type ArchRule struct {
|
||
// Name is an optional human label surfaced on violations.
|
||
Name string `mapstructure:"name" yaml:"name,omitempty"`
|
||
// Layer scopes the rule to symbols in this architecture layer.
|
||
Layer string `mapstructure:"layer" yaml:"layer,omitempty"`
|
||
// Pattern scopes the rule to symbols whose file matches this glob.
|
||
// When both Layer and Pattern are set a symbol must match both.
|
||
Pattern string `mapstructure:"pattern" yaml:"pattern,omitempty"`
|
||
// MaxFanOut caps the number of distinct symbols a scoped symbol
|
||
// may call or reference. 0 disables the fan-out check.
|
||
MaxFanOut int `mapstructure:"max_fan_out" yaml:"max_fan_out,omitempty"`
|
||
// DenyCallersOutside restricts who may call into the scoped set:
|
||
// every caller's file must match one of these globs. The scoped
|
||
// set is always allowed to call within itself.
|
||
DenyCallersOutside []string `mapstructure:"deny_callers_outside" yaml:"deny_callers_outside,omitempty"`
|
||
// Message is an optional human-readable explanation.
|
||
Message string `mapstructure:"message" yaml:"message,omitempty"`
|
||
// Severity tiers the violation: "error" → a change_contract refuse,
|
||
// "warn" → warn, "info" → annotate. Empty defaults to "warn".
|
||
Severity string `mapstructure:"severity" yaml:"severity,omitempty"`
|
||
// Except lists path globs exempt from this rule.
|
||
Except []string `mapstructure:"except" yaml:"except,omitempty"`
|
||
}
|
||
|
||
// ArtifactEntry is one row of the `artifacts:` manifest — a non-code
|
||
// knowledge file (DB schema, API spec, infra config, ADR) tracked as
|
||
// a first-class KindArtifact graph node.
|
||
type ArtifactEntry struct {
|
||
// Path is a file path or glob, repo-relative. ** matches any
|
||
// number of directory segments.
|
||
Path string `mapstructure:"path" yaml:"path"`
|
||
// Kind is schema | api | infra | doc. Auto-detected from the file
|
||
// extension when empty.
|
||
Kind string `mapstructure:"kind" yaml:"kind,omitempty"`
|
||
// Name is an optional display name; defaults to the file's base
|
||
// name.
|
||
Name string `mapstructure:"name" yaml:"name,omitempty"`
|
||
}
|
||
|
||
// NamedQuery is one row of the `queries:` block — a named, reusable
|
||
// bundle of structural detectors. Running it (`analyze kind=named
|
||
// name=<name>`) fans every selected detector across the codebase and
|
||
// aggregates the matches. A detector joins the bundle when it carries
|
||
// one of the Tags or its name is listed in Detectors.
|
||
type NamedQuery struct {
|
||
// Name is the bundle identifier passed to `analyze kind=named`.
|
||
Name string `mapstructure:"name" yaml:"name"`
|
||
// Description is a human-readable summary of the bundle's intent.
|
||
Description string `mapstructure:"description" yaml:"description,omitempty"`
|
||
// Tags selects every detector carrying any of these tags
|
||
// (e.g. injection, secrets, crypto).
|
||
Tags []string `mapstructure:"tags" yaml:"tags,omitempty"`
|
||
// Detectors selects detectors by exact name — for pulling in a
|
||
// specific rule (including a user rule from rule_files).
|
||
Detectors []string `mapstructure:"detectors" yaml:"detectors,omitempty"`
|
||
// Severity is an optional minimum severity floor — info | warning
|
||
// | error. Detectors below it are dropped from the bundle.
|
||
Severity string `mapstructure:"severity" yaml:"severity,omitempty"`
|
||
}
|
||
|
||
// LayerRule defines one architecture layer: the files that belong to
|
||
// it and the layers it may or may not depend on.
|
||
type LayerRule struct {
|
||
// Paths are globs (with ** matching any number of path segments)
|
||
// selecting the files that belong to this layer. When empty the
|
||
// layer matches files whose path carries the layer name as a
|
||
// segment — so a terse `domain: {allow: [...]}` still works.
|
||
Paths []string `mapstructure:"paths" yaml:"paths,omitempty"`
|
||
// Allow whitelists the layers this layer may depend on; "*"
|
||
// permits any. A non-empty Allow makes every unlisted layer a
|
||
// violation.
|
||
Allow []string `mapstructure:"allow" yaml:"allow,omitempty"`
|
||
// Deny blacklists layers this layer must not depend on; "*"
|
||
// denies every cross-layer dependency not rescued by Allow.
|
||
Deny []string `mapstructure:"deny" yaml:"deny,omitempty"`
|
||
}
|
||
|
||
// IsEmpty reports whether no architecture rules are configured — the
|
||
// signal for check_guards to skip the layered evaluation entirely.
|
||
func (a ArchitectureConfig) IsEmpty() bool {
|
||
return len(a.Layers) == 0 && len(a.Rules) == 0
|
||
}
|
||
|
||
// MultiRepoConfig holds workspace-discovery settings used by the
|
||
// multi-repo bootstrapper. Carries the (formerly `workspace.auto_detect`)
|
||
// flag — moved out from under `workspace:` because that key is now
|
||
// reclaimed for the workspace-identity slug.
|
||
type MultiRepoConfig struct {
|
||
// AutoDetect — when true, tracking a parent directory walks its
|
||
// immediate subdirectories looking for `.git/`, treating each
|
||
// match as a tracked repo. The legacy YAML key
|
||
// `workspace.auto_detect: true` is still accepted by the custom
|
||
// Config unmarshaller for one release; the canonical key going
|
||
// forward is `multi.auto_detect`.
|
||
AutoDetect bool `mapstructure:"auto_detect" yaml:"auto_detect,omitempty"`
|
||
}
|
||
|
||
// ProjectGlob declares a project's path-globs inside a monorepo.
|
||
//
|
||
// projects:
|
||
// - name: api
|
||
// paths: ["services/api/**"]
|
||
// - name: worker
|
||
// paths: ["services/worker/**"]
|
||
//
|
||
// A file is assigned to the first project whose globs match (longest
|
||
// glob wins on overlap). Files matching no glob get the workspace-
|
||
// default project name with a warning at index time.
|
||
type ProjectGlob struct {
|
||
Name string `mapstructure:"name" yaml:"name"`
|
||
Paths []string `mapstructure:"paths" yaml:"paths"`
|
||
}
|
||
|
||
// CrossWorkspaceDep declares an explicit, opt-in dependency from this
|
||
// workspace into another.
|
||
//
|
||
// cross_workspace_deps:
|
||
// - workspace: gortex
|
||
// modules: [github.com/gortexhq/gortex]
|
||
// mode: read-only
|
||
//
|
||
// `Mode` must be `read-only` in iteration 1 — any other value is
|
||
// rejected at config-load time with a clear error.
|
||
type CrossWorkspaceDep struct {
|
||
Workspace string `mapstructure:"workspace" yaml:"workspace"`
|
||
Modules []string `mapstructure:"modules" yaml:"modules"`
|
||
Mode string `mapstructure:"mode" yaml:"mode"`
|
||
}
|
||
|
||
// SemanticConfig holds settings for the semantic enrichment layer.
|
||
type SemanticConfig struct {
|
||
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
||
TimeoutSeconds int `mapstructure:"timeout_seconds" yaml:"timeout_seconds,omitempty"`
|
||
EnrichOnWatch bool `mapstructure:"enrich_on_watch" yaml:"enrich_on_watch,omitempty"`
|
||
WatchDebounceMs int `mapstructure:"watch_debounce_ms" yaml:"watch_debounce_ms,omitempty"`
|
||
RefuteUnconfirmed bool `mapstructure:"refute_unconfirmed" yaml:"refute_unconfirmed,omitempty"`
|
||
Providers []SemanticProviderConfig `mapstructure:"providers" yaml:"providers,omitempty"`
|
||
// GoTypes enables the heavyweight go/packages type-check provider
|
||
// ("go-types"): a full `go list ./...` + go/types pass over the module
|
||
// (and its dependencies) on every index. The in-process tree-sitter Go
|
||
// resolver (go-ast-types) is the always-on floor and resolves the common
|
||
// receiver / cross-file call cases with no toolchain; the go/packages
|
||
// provider adds full cross-package / generic type precision but costs
|
||
// tens of seconds per Go module per warmup and, on an already-resolved
|
||
// graph, frequently confirms zero new edges. Tri-state, DEFAULT OFF —
|
||
// opt in for maximum Go type precision. Env override GORTEX_GO_TYPES=1/0.
|
||
GoTypes *bool `mapstructure:"go_types" yaml:"go_types,omitempty"`
|
||
// AdditionalWorkspaceFolders are extra directory roots passed to
|
||
// every LSP server's `initialize` request as workspace folders.
|
||
// This is how cross-package resolution works for a TypeScript (or
|
||
// any) project that imports from a sibling package living outside
|
||
// the indexed repo root — the language server is told to treat
|
||
// those folders as part of the same workspace.
|
||
AdditionalWorkspaceFolders []string `mapstructure:"additional_workspace_folders" yaml:"additional_workspace_folders,omitempty"`
|
||
// ExcludeGlobs lists path globs to skip for semantic enrichment, in
|
||
// addition to the built-in generated/vendored heuristic (vendored deps,
|
||
// tree-sitter generated parser.c, protobuf-generated files, …). A file
|
||
// matching any glob is not enriched and does not count toward a repo's
|
||
// present languages, so a language server is not spawned just to index it.
|
||
ExcludeGlobs []string `mapstructure:"exclude_globs" yaml:"exclude_globs,omitempty"`
|
||
// LSPSweep selects how much of the per-file LSP hover / call-hierarchy
|
||
// enrichment sweep runs. On an already-resolved graph that sweep re-opens
|
||
// and re-hovers every file to confirm zero new edges, so gating it cuts
|
||
// warm-restart churn:
|
||
// - "demand" (DEFAULT / empty): sweep only files whose declarations
|
||
// still carry unresolved same-name call candidates.
|
||
// - "full": sweep every file of the language (pre-knob behaviour).
|
||
// - "off": skip the per-file sweep; the tier-deciding confirm / add
|
||
// passes still run.
|
||
// The GORTEX_LSP_SWEEP env override wins over this setting.
|
||
LSPSweep string `mapstructure:"lsp_sweep" yaml:"lsp_sweep,omitempty"`
|
||
// EagerLSP runs the subprocess LSP servers during synchronous enrichment.
|
||
// Default false: LSP is the slowest part of a cold index and the in-process
|
||
// tiers (go-types, tree-sitter floor) cover the fast baseline, so LSP is
|
||
// lazy-spawned on demand instead. GORTEX_LSP_EAGER=1 restores eager LSP.
|
||
EagerLSP bool `mapstructure:"eager_lsp" yaml:"eager_lsp,omitempty"`
|
||
// SkipEmbed lists (language, kind) combinations that should be
|
||
// indexed for graph queries but *not* embedded into the vector
|
||
// search. Design tokens (CSS custom properties), terraform
|
||
// resource blocks, YAML/TOML/shell config variables are usually
|
||
// searched by literal name, so paying the embedding + HNSW cost
|
||
// buys nothing. See excludes.DefaultSkipEmbed for the baseline.
|
||
SkipEmbed []SkipEmbedRule `mapstructure:"skip_embed" yaml:"skip_embed,omitempty"`
|
||
|
||
// SkipSearch lists (language, kind) combinations that should be
|
||
// kept in the graph but excluded from the text search index
|
||
// (BM25/Bleve). Same shape as SkipEmbed but targets a different
|
||
// index. The motivating case: a big monorepo with ~135k JSON
|
||
// `variable` nodes (package.json keys, tsconfig entries, etc.)
|
||
// pushed total symbol count over search.AutoThreshold and
|
||
// triggered an auto-upgrade from BM25 (~900 B/doc) to Bleve
|
||
// (~32 KiB/doc). Those config-key nodes aren't useful search
|
||
// targets — users who want to find them by name still can via
|
||
// graph queries. Defaults are a superset of SkipEmbed because
|
||
// anything that isn't worth embedding usually isn't worth
|
||
// full-text-indexing either. See DefaultSkipSearch.
|
||
SkipSearch []SkipEmbedRule `mapstructure:"skip_search" yaml:"skip_search,omitempty"`
|
||
}
|
||
|
||
// GoTypesEnabledOrDefault resolves the tri-state SemanticConfig.GoTypes.
|
||
// The heavyweight go/packages type-check provider is OFF by default — the
|
||
// always-on tree-sitter Go floor (go-ast-types) serves Go resolution — so
|
||
// opt in explicitly for full cross-package type precision.
|
||
func (s SemanticConfig) GoTypesEnabledOrDefault() bool {
|
||
if s.GoTypes == nil {
|
||
return false
|
||
}
|
||
return *s.GoTypes
|
||
}
|
||
|
||
// SkipEmbedRule says: when a node's Language matches Language AND its
|
||
// Kind is in Kinds, skip it during vector-index construction.
|
||
type SkipEmbedRule struct {
|
||
Language string `mapstructure:"language" yaml:"language"`
|
||
Kinds []string `mapstructure:"kinds" yaml:"kinds"`
|
||
}
|
||
|
||
// ShouldSkipEmbed reports whether a node of the given (language, kind)
|
||
// falls under any rule in the list. Matching is case-sensitive and
|
||
// exact — parser output is canonical already.
|
||
func ShouldSkipEmbed(rules []SkipEmbedRule, language, kind string) bool {
|
||
return matchesSkipRule(rules, language, kind)
|
||
}
|
||
|
||
// ShouldSkipSearch reports whether a node of the given (language, kind)
|
||
// falls under any text-index skip rule. Same matching semantics as
|
||
// ShouldSkipEmbed — kept as a distinct function so callers make the
|
||
// embed/search distinction explicit, and so the two defaults can
|
||
// diverge over time.
|
||
func ShouldSkipSearch(rules []SkipEmbedRule, language, kind string) bool {
|
||
return matchesSkipRule(rules, language, kind)
|
||
}
|
||
|
||
// matchesSkipRule is the shared (language, kind) matcher for SkipEmbed
|
||
// and SkipSearch. Case-sensitive and exact; parser output is canonical.
|
||
func matchesSkipRule(rules []SkipEmbedRule, language, kind string) bool {
|
||
for _, r := range rules {
|
||
if r.Language == language && slices.Contains(r.Kinds, kind) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// DefaultSkipEmbed returns the compiled-in baseline for which node
|
||
// kinds skip embedding. Kept as a function (rather than a var) so
|
||
// callers who mutate the returned slice don't affect each other.
|
||
func DefaultSkipEmbed() []SkipEmbedRule {
|
||
return []SkipEmbedRule{
|
||
// Design tokens — searched by literal name, not concept.
|
||
{Language: "css", Kinds: []string{"variable", "type"}},
|
||
// Terraform resource/locals/variable blocks — searched
|
||
// literally (aws_vpc.main, module.foo).
|
||
{Language: "hcl", Kinds: []string{"type", "variable"}},
|
||
// Config keys — usually not meaningful prose.
|
||
{Language: "yaml", Kinds: []string{"variable"}},
|
||
{Language: "toml", Kinds: []string{"variable"}},
|
||
// Shell variables are nearly always noise for semantic search.
|
||
{Language: "bash", Kinds: []string{"variable"}},
|
||
}
|
||
}
|
||
|
||
// DefaultSkipSearch returns the baseline (language, kind) pairs that
|
||
// are kept out of the text search index. Superset of DefaultSkipEmbed:
|
||
// if a node isn't worth a vector slot it generally isn't worth a BM25/
|
||
// Bleve slot either, and on big monorepos these config-key nodes are
|
||
// what pushes the backend into its Bleve auto-upgrade (~32 KiB/doc).
|
||
// JSON is the heaviest of the additions — tsconfig / package.json /
|
||
// lockfile keys alone can account for >100k variable nodes.
|
||
func DefaultSkipSearch() []SkipEmbedRule {
|
||
rules := DefaultSkipEmbed()
|
||
rules = append(rules,
|
||
// Object keys — searched by exact path, not full-text.
|
||
SkipEmbedRule{Language: "json", Kinds: []string{"variable"}},
|
||
// Template/markup variables — too noisy to index by name.
|
||
SkipEmbedRule{Language: "liquid", Kinds: []string{"variable"}},
|
||
SkipEmbedRule{Language: "jinja", Kinds: []string{"variable"}},
|
||
// Markdown code-block `variable` nodes (one per fenced block)
|
||
// are noise -- searched by literal name, never by concept. The
|
||
// rule is scoped to `variable` ONLY: the first-class KindDoc
|
||
// prose-section nodes (kind "doc") are deliberately left IN the
|
||
// search index so a prose query ranks the right section.
|
||
SkipEmbedRule{Language: "markdown", Kinds: []string{"variable"}},
|
||
// Build-system variables (Makefile/Dockerfile ARG/ENV) are
|
||
// typically searched by literal name, not concept.
|
||
SkipEmbedRule{Language: "makefile", Kinds: []string{"variable"}},
|
||
SkipEmbedRule{Language: "dockerfile", Kinds: []string{"variable"}},
|
||
)
|
||
return rules
|
||
}
|
||
|
||
// SemanticProviderConfig configures a single semantic provider.
|
||
type SemanticProviderConfig struct {
|
||
Name string `mapstructure:"name" yaml:"name"`
|
||
Command string `mapstructure:"command" yaml:"command,omitempty"`
|
||
Args []string `mapstructure:"args" yaml:"args,omitempty"`
|
||
Languages []string `mapstructure:"languages" yaml:"languages"`
|
||
Priority int `mapstructure:"priority" yaml:"priority,omitempty"`
|
||
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
||
Mode string `mapstructure:"mode" yaml:"mode,omitempty"`
|
||
Daemon bool `mapstructure:"daemon" yaml:"daemon,omitempty"`
|
||
MaxParallel int `mapstructure:"max_parallel" yaml:"max_parallel,omitempty"`
|
||
// Env adds KEY=VALUE environment entries to the provider's
|
||
// subprocess. The motivating case is pinning a JRE for the
|
||
// heavyweight jdtls Java server (env: ["JAVA_HOME=/path/to/jdk"]),
|
||
// but it works for any LSP server that needs a tuned environment.
|
||
// Command / Args / Env set here override the built-in spec.
|
||
Env []string `mapstructure:"env" yaml:"env,omitempty"`
|
||
// Connect, when non-nil, switches this provider from spawning its
|
||
// own LSP subprocess to dialing an already-running endpoint —
|
||
// typically the LSP server the user's IDE is already managing.
|
||
// When set, validation requires non-empty network + address; an
|
||
// empty block is rejected with a clear error.
|
||
Connect *SemanticConnectConfig `mapstructure:"connect" yaml:"connect,omitempty"`
|
||
}
|
||
|
||
// SemanticConnectConfig is the YAML-bound passive-attach block under
|
||
// `semantic.providers[*].connect`. Carrier is tcp or unix; address is
|
||
// host:port (tcp) or socket path (unix). When fallback_spawn is true,
|
||
// a failed dial falls back to spawning the configured subprocess.
|
||
type SemanticConnectConfig struct {
|
||
Network string `mapstructure:"network" yaml:"network"`
|
||
Address string `mapstructure:"address" yaml:"address"`
|
||
FallbackSpawn bool `mapstructure:"fallback_spawn" yaml:"fallback_spawn,omitempty"`
|
||
}
|
||
|
||
// Validate reports a clear error for a malformed Connect block — both
|
||
// fields empty, or an unsupported network. Returns nil for a nil
|
||
// receiver (spawn-mode is fine), and nil for a well-formed block.
|
||
func (c *SemanticConnectConfig) Validate() error {
|
||
if c == nil {
|
||
return nil
|
||
}
|
||
if strings.TrimSpace(c.Network) == "" && strings.TrimSpace(c.Address) == "" {
|
||
return fmt.Errorf("semantic.providers.connect: network and address are required")
|
||
}
|
||
if strings.TrimSpace(c.Network) == "" {
|
||
return fmt.Errorf("semantic.providers.connect: network is required (tcp or unix)")
|
||
}
|
||
if strings.TrimSpace(c.Address) == "" {
|
||
return fmt.Errorf("semantic.providers.connect: address is required")
|
||
}
|
||
switch strings.ToLower(strings.TrimSpace(c.Network)) {
|
||
case "tcp", "tcp4", "tcp6", "unix":
|
||
default:
|
||
return fmt.Errorf("semantic.providers.connect: unsupported network %q (want tcp or unix)", c.Network)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type Config struct {
|
||
// Exclude is the unified ignore list (gitignore semantics) used by
|
||
// both indexing and watching. Workspace-level patterns are appended
|
||
// to builtin + global + per-RepoEntry layers; use `!pattern` to
|
||
// re-include something an outer layer excluded.
|
||
Exclude []string `mapstructure:"exclude" yaml:"exclude,omitempty"`
|
||
|
||
// Include force-indexes paths an outer layer (notably the repo
|
||
// .gitignore) excludes — the dedicated, readable counterpart to
|
||
// hand-writing `!pattern` negations in Exclude. Each entry is appended
|
||
// last as a gitignore `!pattern` re-include, so it wins over the
|
||
// builtin / .gitignore / global / repo exclude layers. Use it for a
|
||
// vendored or generated tree you nonetheless want in the graph
|
||
// (e.g. Pods/, a per-customer customer/ directory).
|
||
Include []string `mapstructure:"include" yaml:"include,omitempty"`
|
||
|
||
// RuleFiles are paths (or globs) to TOML files of user-defined
|
||
// domain-extractor rules. Each rule is a tree-sitter pattern that
|
||
// becomes a registered detector surfaced through `analyze
|
||
// kind=domain` — the pluggable way to extract HTTP routes, CLI
|
||
// commands, feature flags, i18n keys, event-bus topics, etc.
|
||
RuleFiles []string `mapstructure:"rule_files" yaml:"rule_files,omitempty"`
|
||
|
||
// RespectGitignore controls whether the repo's `.gitignore` file is
|
||
// read and its patterns added to the effective exclude list. Default
|
||
// is true (respect .gitignore); set `respect_gitignore: false` in
|
||
// `.gortex.yaml` to opt out for repos that rely on indexing
|
||
// otherwise-ignored generated code or vendored sources. Pointer so
|
||
// the loader can distinguish "explicitly false" from "absent".
|
||
RespectGitignore *bool `mapstructure:"respect_gitignore" yaml:"respect_gitignore,omitempty"`
|
||
|
||
// Workspace is the hard-boundary slug this repo belongs to.
|
||
// Top-level `workspace: <slug>` in `.gortex.yaml`. Empty → defaults
|
||
// to the repo name (resolved by the indexer; see
|
||
// resolveWorkspaceID). Two repos with different non-empty slugs
|
||
// have their contract surfaces and queries strictly isolated.
|
||
Workspace string `mapstructure:"workspace" yaml:"workspace,omitempty"`
|
||
|
||
// Project is the soft sub-boundary slug for single-project repos.
|
||
// Top-level `project: <slug>`. When `Projects[]` is set
|
||
// (monorepo case) this scalar field is ignored — file-to-project
|
||
// mapping comes from the glob list instead.
|
||
Project string `mapstructure:"project" yaml:"project,omitempty"`
|
||
|
||
// Projects is the monorepo per-file project mapping. Top-level
|
||
// `projects: [{name, paths: [globs]}]`. Mutually exclusive with
|
||
// `Project` for clarity; when both are set the loader rejects
|
||
// the config.
|
||
Projects []ProjectGlob `mapstructure:"projects" yaml:"projects,omitempty"`
|
||
|
||
// CrossWorkspaceDeps declares opt-in dependencies into other
|
||
// workspaces. Only `mode: read-only` is accepted in iteration 1.
|
||
CrossWorkspaceDeps []CrossWorkspaceDep `mapstructure:"cross_workspace_deps" yaml:"cross_workspace_deps,omitempty"`
|
||
|
||
Index IndexConfig `mapstructure:"index" yaml:"index,omitempty"`
|
||
Watch WatchConfig `mapstructure:"watch" yaml:"watch,omitempty"`
|
||
Query QueryConfig `mapstructure:"query" yaml:"query,omitempty"`
|
||
Scope ScopeConfig `mapstructure:"scope" yaml:"scope,omitempty"`
|
||
Search SearchConfig `mapstructure:"search" yaml:"search,omitempty"`
|
||
// Embedding configures the semantic-search vector channel: the
|
||
// embedding provider plus the chunking / concurrency knobs the
|
||
// indexer uses to build the vector index. The vector channel is
|
||
// on by default with the zero-download static GloVe provider.
|
||
Embedding EmbeddingConfig `mapstructure:"embedding" yaml:"embedding,omitempty"`
|
||
MCP MCPConfig `mapstructure:"mcp" yaml:"mcp,omitempty"`
|
||
Guards GuardsConfig `mapstructure:"guards" yaml:"guards,omitempty"`
|
||
// Federation tunes the read-only cross-daemon fan-out:
|
||
// per-remote deadline, global budget, circuit breaker, and the
|
||
// opt-in name-keyed fallback. Zero values fall back to defaults.
|
||
Federation FederationConfig `mapstructure:"federation" yaml:"federation,omitempty"`
|
||
// Architecture is the declarative layer / allow-deny DSL. Empty
|
||
// by default; the flat Guards list above keeps working when it is
|
||
// unset.
|
||
Architecture ArchitectureConfig `mapstructure:"architecture" yaml:"architecture,omitempty"`
|
||
// Events is the declarative event-boundary rule family — pub/sub
|
||
// producer/consumer path constraints, evaluated by change_contract.
|
||
Events EventsConfig `mapstructure:"events" yaml:"events,omitempty"`
|
||
// Artifacts is the non-code knowledge manifest — schemas, API
|
||
// specs, infra configs, and ADRs surfaced as KindArtifact nodes.
|
||
Artifacts []ArtifactEntry `mapstructure:"artifacts" yaml:"artifacts,omitempty"`
|
||
// Queries are named, reusable detector bundles runnable via
|
||
// `analyze kind=named`.
|
||
Queries []NamedQuery `mapstructure:"queries" yaml:"queries,omitempty"`
|
||
Multi MultiRepoConfig `mapstructure:"multi" yaml:"multi,omitempty"`
|
||
Semantic SemanticConfig `mapstructure:"semantic" yaml:"semantic,omitempty"`
|
||
// LLM configures the LLM service that backs the `ask` MCP tool and
|
||
// the search-assist passes. Empty by default — daemon skips LLM
|
||
// wiring entirely when the active provider has no model configured.
|
||
// The `llm.provider` key selects the backend (local / anthropic /
|
||
// openai / ollama / claudecli); env vars GORTEX_LLM_* override file
|
||
// values; see internal/llm/config.go::Config.MergeEnv.
|
||
LLM llm.Config `mapstructure:"llm" yaml:"llm,omitempty"`
|
||
// Review tunes the PR-review surface: the layered path-glob rule
|
||
// list, the gating thresholds applied to surfaced findings, the
|
||
// depth-selection bounds, and the posting gate. Empty by default;
|
||
// the embedded default rules apply when no rule is configured.
|
||
Review ReviewConfig `mapstructure:"review" yaml:"review,omitempty"`
|
||
}
|
||
|
||
// ReviewConfig is the `review:` block. It carries every knob the
|
||
// PR-review layers read: the path-glob rule list (resolved by
|
||
// review.RuleResolver), the gating thresholds applied to surfaced
|
||
// findings, the depth-selection bounds, and the comment-posting gate.
|
||
// The whole struct lands here so later layers only read these fields.
|
||
type ReviewConfig struct {
|
||
// Rules is the ordered path-glob rule list. The first rule whose
|
||
// glob matches a changed file selects its severity floor and
|
||
// rulepack; an empty list falls through to the embedded defaults.
|
||
Rules []ReviewRule `mapstructure:"rules" yaml:"rules,omitempty"`
|
||
// MinConfidence drops any finding scored below this confidence
|
||
// (0..1). Zero keeps every finding.
|
||
MinConfidence float64 `mapstructure:"min_confidence" yaml:"min_confidence,omitempty"`
|
||
// MinSeverity is the minimum severity floor — info | warning |
|
||
// error | critical. Findings below it are dropped. Empty keeps all.
|
||
MinSeverity string `mapstructure:"min_severity" yaml:"min_severity,omitempty"`
|
||
// Categories, when set, restricts surfaced findings to these
|
||
// categories. Empty keeps every category.
|
||
Categories []string `mapstructure:"categories" yaml:"categories,omitempty"`
|
||
// MaxFindings caps how many findings are surfaced. Zero is no cap.
|
||
MaxFindings int `mapstructure:"max_findings" yaml:"max_findings,omitempty"`
|
||
// QuickMaxLines is the changed-line ceiling under which a review
|
||
// runs in the quick (shallow) mode. Zero uses the built-in default.
|
||
QuickMaxLines int `mapstructure:"quick_max_lines" yaml:"quick_max_lines,omitempty"`
|
||
// DeepMinLines is the changed-line floor at or above which a review
|
||
// escalates to the deep mode. Zero uses the built-in default.
|
||
DeepMinLines int `mapstructure:"deep_min_lines" yaml:"deep_min_lines,omitempty"`
|
||
// DeepMinFiles is the changed-file floor at or above which a review
|
||
// escalates to the deep mode. Zero uses the built-in default.
|
||
DeepMinFiles int `mapstructure:"deep_min_files" yaml:"deep_min_files,omitempty"`
|
||
// Post gates comment posting.
|
||
Post ReviewPostConfig `mapstructure:"post" yaml:"post,omitempty"`
|
||
}
|
||
|
||
// ReviewPostConfig gates how review findings are posted back. Posting
|
||
// to a public or forked repository is opt-in.
|
||
type ReviewPostConfig struct {
|
||
// AllowPublic permits posting comments on public / fork repos.
|
||
// Off by default so a misconfigured token never leaks comments.
|
||
AllowPublic bool `mapstructure:"allow_public" yaml:"allow_public,omitempty"`
|
||
}
|
||
|
||
// ReviewRule is one path-glob review rule. Path is a gitignore-style
|
||
// glob (so `**` works); the first rule whose Path matches a changed
|
||
// file selects its Severity floor and Rulepack. A Disabled rule is
|
||
// skipped during resolution.
|
||
type ReviewRule struct {
|
||
// Name is the human-readable rule identifier.
|
||
Name string `mapstructure:"name" yaml:"name,omitempty"`
|
||
// Path is the gitignore-style glob the changed file is matched
|
||
// against (e.g. `**/*_test.go`, `internal/auth/**`, `**`).
|
||
Path string `mapstructure:"path" yaml:"path,omitempty"`
|
||
// Severity is an optional severity floor for findings grounded to
|
||
// a file this rule selects — info | warning | error | critical.
|
||
Severity string `mapstructure:"severity" yaml:"severity,omitempty"`
|
||
// Rulepack names the detector bundle to run for a matched file.
|
||
Rulepack string `mapstructure:"rulepack" yaml:"rulepack,omitempty"`
|
||
// Disabled skips this rule entirely during resolution.
|
||
Disabled bool `mapstructure:"disabled" yaml:"disabled,omitempty"`
|
||
}
|
||
|
||
type IndexConfig struct {
|
||
Languages []string `mapstructure:"languages" yaml:"languages,omitempty"`
|
||
// Exclude is deprecated — use top-level Config.Exclude instead.
|
||
// Still read for one release so existing .gortex.yaml files don't
|
||
// silently stop working; merged into the unified list by ConfigManager.
|
||
Exclude []string `mapstructure:"exclude" yaml:"exclude,omitempty"`
|
||
Workers int `mapstructure:"workers" yaml:"workers,omitempty"`
|
||
// SkipEmbed is the effective skip-embedding rules resolved from
|
||
// Semantic.SkipEmbed. Not part of the on-disk YAML schema — it's
|
||
// populated by ConfigManager.GetRepoConfig so the indexer gets it
|
||
// through the same struct it already receives. Surface it to users
|
||
// under semantic.skip_embed, not under index.
|
||
SkipEmbed []SkipEmbedRule `mapstructure:"-" yaml:"-"`
|
||
// SkipSearch is the effective text-index skip rules resolved from
|
||
// Semantic.SkipSearch, same propagation pattern as SkipEmbed.
|
||
// Users configure this under semantic.skip_search; the indexer
|
||
// reads it here. Controls what goes into BM25/Bleve — unlike
|
||
// SkipEmbed it doesn't affect the graph or vector index.
|
||
SkipSearch []SkipEmbedRule `mapstructure:"-" yaml:"-"`
|
||
// IndexProse is the effective prose-indexing toggle resolved from
|
||
// Search.IndexProse -- same `-` (not on-disk) propagation pattern
|
||
// as SkipSearch. When false, Markdown KindDoc prose-section nodes
|
||
// are kept out of the BM25 search index. Defaults to true.
|
||
IndexProse bool `mapstructure:"-" yaml:"-"`
|
||
// MaxFileSize skips files larger than this during indexing. Zero
|
||
// (the default) disables the cap — full coverage is preferred so
|
||
// generated code like `*.pb.go`, schema files, and large data
|
||
// constants stay queryable. Users with very heavy generated /
|
||
// minified files that dominate parse time can set a cap (e.g.
|
||
// 2 MiB) via `.gortex.yaml` to trade coverage for speed. A cap
|
||
// that drops real symbols silently is a worse default than a
|
||
// slightly slower full index.
|
||
MaxFileSize int64 `mapstructure:"max_file_size" yaml:"max_file_size,omitempty"`
|
||
// MaxParseBytesInFlight bounds the total raw source bytes admitted
|
||
// into concurrent extraction at once. A weighted, bytes-in-flight
|
||
// semaphore admits each file by its size before it is read + parsed,
|
||
// so a cluster of large files (PDFs, office docs in a content repo)
|
||
// serialises instead of all workers materialising whole files and
|
||
// their parse trees simultaneously — the content-repo OOM class.
|
||
// Zero disables the cap; Default() sets a conservative non-zero budget.
|
||
MaxParseBytesInFlight int64 `mapstructure:"max_parse_bytes_in_flight" yaml:"max_parse_bytes_in_flight,omitempty"`
|
||
// IndexMinified controls whether bundled / minified build
|
||
// artifacts are indexed. Off by default: a minified bundle or a
|
||
// sourcemap is synthetic source — it has no meaningful symbols and
|
||
// only pollutes the graph and search results — so it is detected by
|
||
// content and skipped with a synthetic telemetry node. Set true to
|
||
// index such files anyway. Configured under `index.index_minified`.
|
||
IndexMinified bool `mapstructure:"index_minified" yaml:"index_minified,omitempty"`
|
||
// CrashIsolation runs tree-sitter extraction in worker
|
||
// subprocesses so a grammar SIGSEGV / OOM / hang on one
|
||
// pathological file is contained: the bad file is quarantined
|
||
// with Meta["parse_error"] and the index pass still completes.
|
||
// Off by default — the subprocess round-trip adds measurable
|
||
// overhead and an in-process index is the common case. Opt in via
|
||
// `.gortex.yaml::index::crash_isolation: true` or the
|
||
// GORTEX_PARSER_ISOLATION=1 environment override.
|
||
CrashIsolation bool `mapstructure:"crash_isolation" yaml:"crash_isolation,omitempty"`
|
||
// Merkle switches incremental re-index change detection from
|
||
// per-file mtime comparison to a BLAKE3 Merkle tree. The tree is
|
||
// content-addressed, so a file touched without a content change is
|
||
// not re-indexed, and an unchanged subtree is skipped wholesale.
|
||
// Off by default — mtime detection is cheaper when most touches
|
||
// are real edits. Opt in via `.gortex.yaml::index::merkle: true`
|
||
// or the GORTEX_MERKLE=1 environment override.
|
||
Merkle bool `mapstructure:"merkle" yaml:"merkle,omitempty"`
|
||
// MaxExtractMillis caps how long a single file's extraction may
|
||
// run. A file that exceeds the budget is skipped with a synthetic
|
||
// node carrying Meta["skipped_due_to_timeout"] instead of stalling
|
||
// the pass — the microsoft/TypeScript reallyLargeFile.ts class of
|
||
// pathological input. Zero (the default) disables the cap, for the
|
||
// same full-coverage reason MaxFileSize defaults off. Set
|
||
// generously (e.g. 10000) so only genuinely pathological files
|
||
// trip it. In crash-isolation mode it also bounds the worker
|
||
// round-trip.
|
||
MaxExtractMillis int `mapstructure:"max_extract_millis" yaml:"max_extract_millis,omitempty"`
|
||
// SynthesizeExternalCalls turns a call into an un-indexed third
|
||
// party (an npm/pip/cargo/maven/nuget package, or a sibling
|
||
// microservice) into an explicit graph terminal. Without it such a
|
||
// call edge stays an `unresolved::` placeholder with no destination
|
||
// node, and a call-chain walk silently stops there — losing the fact
|
||
// that the function reaches out to an external system at all. When
|
||
// on, the resolver synthesises a clearly-marked synthetic node per
|
||
// (ecosystem, import path) and retargets the call edge to it, so
|
||
// `get_call_chain` / `get_callers` keep the external hop visible and
|
||
// every call into the same package shares one stable, cross-repo
|
||
// identity node — `analyze kind=external_calls` then aggregates a
|
||
// service's whole external surface across repositories.
|
||
//
|
||
// Tri-state, default ON: a nil pointer (the key absent) resolves to
|
||
// enabled via ExternalCallSynthesisEnabledOrDefault, so external-call
|
||
// qualification is automatic for every language whose extractor lands
|
||
// calls on a dep/stdlib/external terminal (Go/Rust/Java/Python/C#/TS).
|
||
// Affordable by default because synthesis is incremental on the hot
|
||
// reindex path (only the edited file's terminals are re-materialised)
|
||
// and the disk backend pushes candidate-edge selection down to a
|
||
// partial-indexed query. Opt out per-repo with
|
||
// `.gortex.yaml::index::synthesize_external_calls: false` or the
|
||
// GORTEX_SYNTH_EXTERNAL_CALLS=0 environment override; language /
|
||
// stdlib calls are filtered out regardless so the synthetic nodes
|
||
// only ever name genuine third-party packages.
|
||
SynthesizeExternalCalls *bool `mapstructure:"synthesize_external_calls" yaml:"synthesize_external_calls,omitempty"`
|
||
// SynthesizeSpeculativeDispatch mints opt-in, best-guess `calls` edges for
|
||
// dynamic-dispatch blind spots (computed-member calls obj["foo"](),
|
||
// getattr, decorator registries). Unlike external-call synthesis it
|
||
// DEFAULTS OFF (tri-state, nil = off) because the edges are heuristic
|
||
// fan-outs; when on they ride a strictly-lower confidence tier and are
|
||
// hidden from every default query (opt in per query with
|
||
// include_speculative, or audit via `analyze kind=speculative`). Enable
|
||
// per-repo with `.gortex.yaml::index::synthesize_speculative_dispatch:
|
||
// true` or the GORTEX_SYNTH_SPECULATIVE=1 environment override.
|
||
SynthesizeSpeculativeDispatch *bool `mapstructure:"synthesize_speculative_dispatch" yaml:"synthesize_speculative_dispatch,omitempty"`
|
||
// ScopedGlobalPasses scopes the global inference passes (InferImplements /
|
||
// InferOverrides) on incremental reindex to only the types/interfaces a
|
||
// change can affect, instead of re-running the whole-graph type×interface
|
||
// cross product on every single-file edit. Add-parity with the full pass
|
||
// (it re-derives exactly the edges eviction dropped). Tri-state, DEFAULT
|
||
// ON. Set false (or GORTEX_INDEX_SCOPED_GLOBAL_PASSES=0) to restore the
|
||
// whole-graph behaviour.
|
||
ScopedGlobalPasses *bool `mapstructure:"scoped_global_passes" yaml:"scoped_global_passes,omitempty"`
|
||
// AffectedByReresolveMax caps how many referencing files a single
|
||
// save's affected-by re-resolution pass will re-resolve. When a
|
||
// file's symbol signatures change (or symbols are removed / change
|
||
// kind), the files that referenced those symbols are re-resolved so
|
||
// their edges and persisted reference facts track the new shape; a
|
||
// symbol with thousands of callers must not turn one save into a
|
||
// near-whole-graph resolve, so the affected set is truncated at this
|
||
// bound (truncation is logged with the dropped count). Zero (the
|
||
// default) uses the built-in cap of 200 files. Configured under
|
||
// `index.affected_by_reresolve_max` in .gortex.yaml.
|
||
AffectedByReresolveMax int `mapstructure:"affected_by_reresolve_max" yaml:"affected_by_reresolve_max,omitempty"`
|
||
// Transforms are pluggable pre-ingestion content processors. Each
|
||
// rewrites a matching file's bytes before the parser sees them —
|
||
// expanding minified bundles, normalising SVG/TOON, converting a
|
||
// PDF to markdown, etc. Empty by default.
|
||
Transforms []TransformRule `mapstructure:"transforms" yaml:"transforms,omitempty"`
|
||
// Grammars registers user-supplied tree-sitter grammars — drop-in
|
||
// languages Gortex was not compiled with. Each entry points at a
|
||
// compiled grammar shared object (.so / .dylib / .dll); see
|
||
// GrammarSpec. Empty by default. Configured under `index.grammars`
|
||
// in .gortex.yaml.
|
||
Grammars []GrammarSpec `mapstructure:"grammars" yaml:"grammars,omitempty"`
|
||
// Coverage gates the per-domain coverage extractors (todos,
|
||
// licenses, ownership, function shape, etc.). Each sub-block has
|
||
// its own default; an empty Coverage block means "use the
|
||
// documented per-domain defaults" — cheap structural domains on,
|
||
// expensive ones off.
|
||
Coverage CoverageConfig `mapstructure:"coverage" yaml:"coverage,omitempty"`
|
||
// HTTPClientAliases names project-defined wrapped HTTP-client
|
||
// functions so calls to them are recognised as HTTP consumer
|
||
// contracts (RoleConsumer) — without relying on the type-driven
|
||
// fetch/axios heuristics. Each entry is a bare function name (or
|
||
// receiver.method) that forwards a string-literal path to an
|
||
// underlying client, e.g. `apiGet`, `apiPost`, `client.request`.
|
||
// The contracts HTTP extractor mints an `http::METHOD::/path`
|
||
// consumer contract for every call to one of these names. The
|
||
// method is taken from the name's verb suffix (`apiGet` → GET,
|
||
// `apiPost` → POST, ...) when present, falling back to the first
|
||
// string-literal argument when the call passes the method
|
||
// explicitly (`apiCall('GET', '/users')`); the path is the first
|
||
// path-shaped string literal. Empty by default. Configured under
|
||
// `index.http_client_aliases` in .gortex.yaml.
|
||
HTTPClientAliases []string `mapstructure:"http_client_aliases" yaml:"http_client_aliases,omitempty"`
|
||
// ExtractorPlugins registers external post-parse extractor passes —
|
||
// the "add a custom extractor without forking Gortex" entry point.
|
||
// Each spec names a language + file extensions and an external
|
||
// command; matching files are piped (content on stdin,
|
||
// GORTEX_FILE_PATH in the env) to that command, which emits a JSON
|
||
// document of nodes and edges that Gortex folds into the graph. See
|
||
// ExtractorPluginSpec. Empty by default. Configured under
|
||
// `index.extractor_plugins` in .gortex.yaml.
|
||
ExtractorPlugins []ExtractorPluginSpec `mapstructure:"extractor_plugins" yaml:"extractor_plugins,omitempty"`
|
||
// FallbackChunkers registers declarative regex chunkers for
|
||
// languages Gortex has no tree-sitter grammar for — the "give a
|
||
// grammar-less language a coarse outline without writing Go" entry
|
||
// point. Each spec names a language + file extensions and a list of
|
||
// regex patterns; matching files yield one node per pattern match
|
||
// plus an EdgeDefines from the file. See FallbackChunkerSpec. Empty
|
||
// by default. Configured under `index.fallback_chunkers` in
|
||
// .gortex.yaml.
|
||
FallbackChunkers []FallbackChunkerSpec `mapstructure:"fallback_chunkers" yaml:"fallback_chunkers,omitempty"`
|
||
|
||
// EventBus declares producer/consumer boundaries for a bespoke event
|
||
// bus / SSE flow purely in config — no code change. Each spec matches
|
||
// call sites / decorators / interface bases and Gortex synthesizes real
|
||
// traversable provider↔consumer contract pairs (with dispatch guards)
|
||
// for the bus, so find_usages / trace_path / analyze pubsub all work on
|
||
// it. Empty by default. Configured under `index.event_bus` in
|
||
// .gortex.yaml; the CODEGRAPH_EVENT_CONFIG env var (a JSON list)
|
||
// overrides it for drop-in compatibility with pygraph/tsgraph.
|
||
EventBus []EventBusBoundarySpec `mapstructure:"event_bus" yaml:"event_bus,omitempty"`
|
||
|
||
// Content bounds admission of large non-source content / data
|
||
// artifacts — PDF / office documents and binary / columnar / vector
|
||
// data files — so a content-heavy corpus (a RAG repo of hundreds of
|
||
// decks, spreadsheets, and dataset shards) can't pull gigabytes of
|
||
// non-code files into the parse pipeline and OOM the indexer (#120).
|
||
// Documents over a per-file cap and (by default) all data assets are
|
||
// dropped at the walk with a skip-telemetry node, so they stay
|
||
// listable without being read or extracted. See
|
||
// ContentAdmissionConfig. Configured under `index.content`.
|
||
Content ContentAdmissionConfig `mapstructure:"content" yaml:"content,omitempty"`
|
||
|
||
// SkipUntrackedAssets drops document / data / image assets that git
|
||
// does not track (uncommitted working-tree files) during a full index.
|
||
// `.gitignore` is already honoured (respect_gitignore), but it can't
|
||
// catch files that are merely untracked — never `git add`ed and not in
|
||
// .gitignore — which is exactly how RAG corpora, downloaded datasets,
|
||
// and build outputs end up admitted (#120). When on, an untracked
|
||
// asset-class file is skipped at the walk with an `untracked_asset`
|
||
// telemetry node; untracked CODE is still indexed, so new / unsaved
|
||
// source keeps working. Off by default. Inert on a non-git repo or when
|
||
// `git ls-files` is unavailable (everything is admitted as before).
|
||
// Applies to the cold full-index walk; the incremental watcher path
|
||
// keeps the size / class caps only. Configured under
|
||
// `index.skip_untracked_assets`.
|
||
SkipUntrackedAssets bool `mapstructure:"skip_untracked_assets" yaml:"skip_untracked_assets,omitempty"`
|
||
}
|
||
|
||
// ContentAdmissionConfig gates which large non-source artifacts enter the
|
||
// index. The defaults keep the multimodal feature working for normal-size
|
||
// documents while bounding the corpus-admission memory blow-up that a
|
||
// content-heavy repo otherwise triggers.
|
||
type ContentAdmissionConfig struct {
|
||
// MaxDocumentBytes caps individual document assets — PDF, pptx, xlsx,
|
||
// plain text — that are ingested as searchable content (one KindDoc
|
||
// node per page / slide / sheet). A file larger than the cap is
|
||
// skipped at the walk with a `large_document` telemetry node instead
|
||
// of being read and extracted. Tri-state: >0 caps at that many bytes;
|
||
// 0 (absent) uses the built-in default (defaultMaxDocumentBytes,
|
||
// 10 MiB); <0 disables the cap (index documents of any size).
|
||
// Configured under `index.content.max_document_bytes`.
|
||
MaxDocumentBytes int64 `mapstructure:"max_document_bytes" yaml:"max_document_bytes,omitempty"`
|
||
// IndexData admits binary / columnar / vector data artifacts
|
||
// (.parquet, .npy, .npz, .lance, .arrow, .feather) as metadata-only
|
||
// nodes. Off by default: these are never code-intelligence content and
|
||
// in a RAG / data repo they dominate the admitted byte weight, so they
|
||
// are skipped at the walk with a `vector_data` telemetry node unless
|
||
// explicitly opted in. Configured under `index.content.index_data`.
|
||
IndexData bool `mapstructure:"index_data" yaml:"index_data,omitempty"`
|
||
// MaxDataBytes caps data assets when IndexData is on — a guard against
|
||
// admitting a multi-GB shard even when data indexing is wanted. Same
|
||
// tri-state as MaxDocumentBytes (>0 caps; 0 uses the built-in default,
|
||
// defaultMaxDataBytes; <0 disables the cap). Ignored when IndexData is
|
||
// off. Configured under `index.content.max_data_bytes`.
|
||
MaxDataBytes int64 `mapstructure:"max_data_bytes" yaml:"max_data_bytes,omitempty"`
|
||
}
|
||
|
||
// EffectiveMaxDocumentBytes resolves MaxDocumentBytes' tri-state into a
|
||
// concrete cap. capped is false when documents are admitted at any size
|
||
// (the field was set negative); otherwise limit is the active per-file cap.
|
||
func (c ContentAdmissionConfig) EffectiveMaxDocumentBytes() (limit int64, capped bool) {
|
||
switch {
|
||
case c.MaxDocumentBytes < 0:
|
||
return 0, false
|
||
case c.MaxDocumentBytes == 0:
|
||
return defaultMaxDocumentBytes, true
|
||
default:
|
||
return c.MaxDocumentBytes, true
|
||
}
|
||
}
|
||
|
||
// EffectiveMaxDataBytes resolves MaxDataBytes' tri-state into a concrete
|
||
// cap, used only when IndexData is on. capped is false when admitted data
|
||
// assets are uncapped (the field was set negative).
|
||
func (c ContentAdmissionConfig) EffectiveMaxDataBytes() (limit int64, capped bool) {
|
||
switch {
|
||
case c.MaxDataBytes < 0:
|
||
return 0, false
|
||
case c.MaxDataBytes == 0:
|
||
return defaultMaxDataBytes, true
|
||
default:
|
||
return c.MaxDataBytes, true
|
||
}
|
||
}
|
||
|
||
// EventBusBoundarySpec declares one producer or consumer boundary of a
|
||
// user-defined event bus / SSE flow. The schema is language-neutral and
|
||
// drop-in compatible with pygraph/tsgraph's CODEGRAPH_EVENT_CONFIG entries.
|
||
type EventBusBoundarySpec struct {
|
||
// Name groups producers and consumers of the same logical bus. A
|
||
// producer and consumer with the same Name + topic pair into an edge.
|
||
Name string `mapstructure:"name" yaml:"name"`
|
||
// Type is "producer" or "consumer".
|
||
Type string `mapstructure:"type" yaml:"type"`
|
||
// Callee is a dotted call suffix matched against producer/consumer call
|
||
// sites (e.g. "bus.publish", "producer.send"). Matched as a suffix so
|
||
// "self.producer.send(" matches "producer.send".
|
||
Callee string `mapstructure:"callee" yaml:"callee,omitempty"`
|
||
// CalleePattern is a looser substring match on the callee for SSE / hook
|
||
// forms (e.g. "EventSource", "useEventStream"). Alias: HookPattern.
|
||
CalleePattern string `mapstructure:"callee_pattern" yaml:"callee_pattern,omitempty"`
|
||
// HookPattern is an alias for CalleePattern (tsgraph compatibility).
|
||
HookPattern string `mapstructure:"hook_pattern" yaml:"hook_pattern,omitempty"`
|
||
// Decorator names a decorator that marks the decorated function as a
|
||
// consumer (e.g. "kafka_consumer"); the topic is read from its args.
|
||
Decorator string `mapstructure:"decorator" yaml:"decorator,omitempty"`
|
||
// Interface names a class base; every method of a class extending it is
|
||
// a consumer (paired on the bus wildcard topic).
|
||
Interface string `mapstructure:"interface" yaml:"interface,omitempty"`
|
||
// TopicArg names the argument carrying the topic / url that keys the
|
||
// producer↔consumer join: a positional index ("0") or a kwarg name
|
||
// ("topic"). Defaults to "topic".
|
||
TopicArg string `mapstructure:"topic_arg" yaml:"topic_arg,omitempty"`
|
||
// Guards, when true, extracts the consumer's first if/elif dispatch
|
||
// chain (field==value rules) onto the contract for routing queries.
|
||
Guards bool `mapstructure:"guards" yaml:"guards,omitempty"`
|
||
}
|
||
|
||
// GrammarSpec declares a user-supplied tree-sitter grammar to load at
|
||
// startup — a drop-in language Gortex was not compiled with. The
|
||
// grammar must be a compiled shared library (.so / .dylib / .dll)
|
||
// exporting the standard tree-sitter `tree_sitter_<language>` entry
|
||
// point, built against a compatible tree-sitter ABI.
|
||
type GrammarSpec struct {
|
||
// Language is the name the extractor registers under. A name that
|
||
// collides with a built-in extractor is skipped — built-ins win.
|
||
Language string `mapstructure:"language" yaml:"language"`
|
||
// Library is the path to the compiled grammar shared object. A
|
||
// relative path resolves against the working directory.
|
||
Library string `mapstructure:"library" yaml:"library"`
|
||
// Symbol overrides the C entry-point symbol. Defaults to
|
||
// `tree_sitter_<language>` with non-alphanumerics folded to `_`.
|
||
Symbol string `mapstructure:"symbol" yaml:"symbol,omitempty"`
|
||
// Extensions are the file extensions (leading dot) the grammar
|
||
// claims, e.g. [".foo", ".foobar"]. An extension already claimed
|
||
// by a built-in extractor is skipped.
|
||
Extensions []string `mapstructure:"extensions" yaml:"extensions"`
|
||
}
|
||
|
||
// TransformRule declares a pluggable pre-ingestion content transform.
|
||
// Before a matching file is parsed, its bytes are piped through the
|
||
// command (file content on stdin, transformed content on stdout). This
|
||
// is how non-native inputs reach the graph — minified bundles expanded,
|
||
// SVG/TOON normalised, a PDF converted to markdown, and so on.
|
||
type TransformRule struct {
|
||
// Name identifies the transform in logs.
|
||
Name string `mapstructure:"name" yaml:"name"`
|
||
// Extensions are the file extensions (with the dot, e.g. ".svg")
|
||
// this transform applies to. Empty means every file.
|
||
Extensions []string `mapstructure:"extensions" yaml:"extensions,omitempty"`
|
||
// Command is the transform program as argv. The file content is
|
||
// written to its stdin; its stdout replaces the content.
|
||
Command []string `mapstructure:"command" yaml:"command"`
|
||
// AsLanguage, when set, indexes matching files as this language
|
||
// even if their extension is not natively recognised — a rule with
|
||
// extensions [".pdf"] + as_language "markdown" lets a PDF→markdown
|
||
// command feed the markdown extractor.
|
||
AsLanguage string `mapstructure:"as_language" yaml:"as_language,omitempty"`
|
||
// TimeoutMillis bounds the transform subprocess; 0 uses a default.
|
||
TimeoutMillis int `mapstructure:"timeout_millis" yaml:"timeout_millis,omitempty"`
|
||
}
|
||
|
||
// ExtractorPluginSpec declares an external post-parse extractor pass —
|
||
// the config-driven SPI for adding a custom extractor without forking
|
||
// Gortex. Files whose extension matches are piped (content on stdin,
|
||
// GORTEX_FILE_PATH carrying the path in the env) to the command, which
|
||
// must emit a JSON document of the shape:
|
||
//
|
||
// {"nodes":[{"id","kind","name","file_path","start_line","end_line",
|
||
// "language","meta":{...}}],
|
||
// "edges":[{"from","to","kind","file_path","line","meta":{...}}]}
|
||
//
|
||
// Each node whose Kind is a valid graph node kind becomes a graph.Node;
|
||
// each edge becomes a graph.Edge. The owning file node is always
|
||
// emitted. A non-zero exit, malformed JSON, or a timeout degrades to
|
||
// just the file node (the index never fails). A language or extension
|
||
// that collides with a built-in extractor is skipped — built-ins win.
|
||
type ExtractorPluginSpec struct {
|
||
// Language is the name the extractor registers under. A name that
|
||
// collides with a built-in extractor is skipped.
|
||
Language string `mapstructure:"language" yaml:"language"`
|
||
// Extensions are the file extensions (leading dot) the plugin
|
||
// claims, e.g. [".foo"]. An extension already claimed by a built-in
|
||
// extractor is skipped.
|
||
Extensions []string `mapstructure:"extensions" yaml:"extensions"`
|
||
// Command is the plugin program (argv[0]). The file content is
|
||
// written to its stdin; GORTEX_FILE_PATH names the file in the env.
|
||
Command string `mapstructure:"command" yaml:"command"`
|
||
// Args are the trailing arguments passed to Command.
|
||
Args []string `mapstructure:"args" yaml:"args,omitempty"`
|
||
// TimeoutMs bounds the plugin subprocess; 0 uses a default.
|
||
TimeoutMs int `mapstructure:"timeout_ms" yaml:"timeout_ms,omitempty"`
|
||
}
|
||
|
||
// FallbackChunkerSpec declares a declarative regex chunker for a
|
||
// grammar-less language — the config-driven SPI for giving a language
|
||
// Gortex has no tree-sitter grammar a coarse symbol outline without
|
||
// writing Go. Files whose extension matches are scanned with each
|
||
// pattern; every match emits a node of the pattern's kind (validated
|
||
// against the graph node kinds) plus an EdgeDefines from the file. A
|
||
// language or extension that collides with a built-in extractor is
|
||
// skipped — built-ins win.
|
||
type FallbackChunkerSpec struct {
|
||
// Language is the name the chunker registers under. A name that
|
||
// collides with a built-in extractor is skipped.
|
||
Language string `mapstructure:"language" yaml:"language"`
|
||
// Extensions are the file extensions (leading dot) the chunker
|
||
// claims. An extension already claimed by a built-in extractor is
|
||
// skipped.
|
||
Extensions []string `mapstructure:"extensions" yaml:"extensions"`
|
||
// Patterns are the regex rules applied to a matching file's content.
|
||
Patterns []ChunkPattern `mapstructure:"patterns" yaml:"patterns"`
|
||
}
|
||
|
||
// ChunkPattern is a single regex rule of a FallbackChunkerSpec. Each
|
||
// match yields one node of Kind named by the NameGroup-th capture.
|
||
type ChunkPattern struct {
|
||
// Kind is the graph node kind to emit (e.g. "function", "type").
|
||
// A kind that is not a valid graph node kind is skipped.
|
||
Kind string `mapstructure:"kind" yaml:"kind"`
|
||
// Regex is the (Go-syntax) pattern matched against the file content.
|
||
// A pattern that fails to compile skips the whole chunker spec.
|
||
Regex string `mapstructure:"regex" yaml:"regex"`
|
||
// NameGroup is the capture-group index whose text names the node.
|
||
// Zero (the default) uses group 1 — the first parenthesised group.
|
||
NameGroup int `mapstructure:"name_group" yaml:"name_group,omitempty"`
|
||
}
|
||
|
||
// CoverageConfig collects the per-domain coverage extraction gates.
|
||
// Each sub-block is independently shippable and gated.
|
||
//
|
||
// Defaults (per-domain Enabled value when not set in YAML):
|
||
//
|
||
// - FunctionShape: true (params/returns/generics/closures — Phase 1)
|
||
// - Concurrency: true (closures already covered by FunctionShape;
|
||
// this gates EdgeSpawns / channel I/O)
|
||
// - Constants: true (cheap, structural)
|
||
// - TypeShape: true (aliases vs newtypes vs composition)
|
||
// - Codegen: true (// Code generated markers)
|
||
// - Todos: true (comment scanner)
|
||
// - Ownership: true (CODEOWNERS — only emits when file present)
|
||
// - Licenses: true (SPDX header scan)
|
||
// - Modules: true (lockfile parse — cheap)
|
||
// - Configs: false (recognizers per provider; opt-in)
|
||
// - Flags: false (auto-on if a provider client is detected)
|
||
// - Observability: false (logging/metric/trace event names)
|
||
// - Pubsub: false (event pub/sub publish/subscribe edges)
|
||
// - SQL: false (Phase 3 — noisy and slow)
|
||
// - Fixtures: true (testdata path detection)
|
||
// - CrossLanguage: false (cgo / wasm-bindgen)
|
||
// - Clones: true (MinHash + LSH near-duplicate detection)
|
||
//
|
||
// Setting any leaf Enabled explicitly overrides the default.
|
||
type CoverageConfig struct {
|
||
FunctionShape DomainToggle `mapstructure:"function_shape" yaml:"function_shape,omitempty"`
|
||
Concurrency DomainToggle `mapstructure:"concurrency" yaml:"concurrency,omitempty"`
|
||
Constants DomainToggle `mapstructure:"constants" yaml:"constants,omitempty"`
|
||
TypeShape DomainToggle `mapstructure:"type_shape" yaml:"type_shape,omitempty"`
|
||
Codegen DomainToggle `mapstructure:"codegen" yaml:"codegen,omitempty"`
|
||
Todos TodoConfig `mapstructure:"todos" yaml:"todos,omitempty"`
|
||
Ownership DomainToggle `mapstructure:"ownership" yaml:"ownership,omitempty"`
|
||
Licenses DomainToggle `mapstructure:"licenses" yaml:"licenses,omitempty"`
|
||
Modules DomainToggle `mapstructure:"modules" yaml:"modules,omitempty"`
|
||
Configs DomainToggle `mapstructure:"configs" yaml:"configs,omitempty"`
|
||
Flags FlagConfig `mapstructure:"flags" yaml:"flags,omitempty"`
|
||
Observability DomainToggle `mapstructure:"observability" yaml:"observability,omitempty"`
|
||
Pubsub DomainToggle `mapstructure:"pubsub" yaml:"pubsub,omitempty"`
|
||
SQL SQLConfig `mapstructure:"sql" yaml:"sql,omitempty"`
|
||
Fixtures DomainToggle `mapstructure:"fixtures" yaml:"fixtures,omitempty"`
|
||
CrossLanguage DomainToggle `mapstructure:"cross_language" yaml:"cross_language,omitempty"`
|
||
Clones ClonesConfig `mapstructure:"clones" yaml:"clones,omitempty"`
|
||
}
|
||
|
||
// ClonesConfig gates MinHash + LSH near-duplicate ("clone") detection.
|
||
// When enabled, the indexer stamps a 64-slot MinHash signature on every
|
||
// substantial function/method node at parse time and a graph-wide pass
|
||
// emits EdgeSimilarTo edges between bodies whose estimated Jaccard
|
||
// similarity is at or above Threshold. Default-on: signature
|
||
// computation is a cheap per-function tokenise + hash, and the LSH pass
|
||
// is near-linear.
|
||
type ClonesConfig struct {
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
// Threshold is the Jaccard similarity at or above which a candidate
|
||
// pair is recorded as a clone. Zero (unset) falls back to the
|
||
// clones package default (0.82). Values are clamped to (0, 1].
|
||
Threshold float64 `mapstructure:"threshold" yaml:"threshold,omitempty"`
|
||
}
|
||
|
||
// DomainToggle is the minimal config for a domain whose only knob is
|
||
// on/off plus an optional language allow-list. The Enabled field uses
|
||
// a tri-state pointer so we can distinguish "user set false" from
|
||
// "user did not set" (and apply the per-domain default for the
|
||
// latter). The language list, when non-empty, restricts extraction to
|
||
// the listed source languages.
|
||
type DomainToggle struct {
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
Languages []string `mapstructure:"languages" yaml:"languages,omitempty"`
|
||
}
|
||
|
||
// TodoConfig configures the TODO scanner. Tags is the set of
|
||
// recognised marker tokens (default: TODO, FIXME, HACK, XXX, NOTE);
|
||
// MaxText caps the stored text length (default 200) so a wall of
|
||
// commented-out code in a TODO doesn't bloat the graph.
|
||
type TodoConfig struct {
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
Tags []string `mapstructure:"tags" yaml:"tags,omitempty"`
|
||
MaxText int `mapstructure:"max_text" yaml:"max_text,omitempty"`
|
||
}
|
||
|
||
// FlagConfig configures feature-flag recognition. Recognizers is a
|
||
// list of (provider, function-name) pairs to treat as flag checks;
|
||
// the built-in recognizers cover GrowthBook, LaunchDarkly, Unleash by
|
||
// default.
|
||
type FlagConfig struct {
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
Recognizers []FlagRecognizer `mapstructure:"recognizers" yaml:"recognizers,omitempty"`
|
||
}
|
||
|
||
// FlagRecognizer maps a fully-qualified function name to a flag
|
||
// provider and operation. Op ∈ read|write|register.
|
||
type FlagRecognizer struct {
|
||
Provider string `mapstructure:"provider" yaml:"provider"`
|
||
Func string `mapstructure:"func" yaml:"func"`
|
||
Op string `mapstructure:"op" yaml:"op,omitempty"`
|
||
}
|
||
|
||
// SQLConfig gates SQL schema extraction. Default-off because SQL
|
||
// parsing is slow and string-literal SQL produces false positives.
|
||
type SQLConfig struct {
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
Dialect string `mapstructure:"dialect" yaml:"dialect,omitempty"`
|
||
Migrations SQLMigrations `mapstructure:"migrations" yaml:"migrations,omitempty"`
|
||
ORM SQLOrm `mapstructure:"orm" yaml:"orm,omitempty"`
|
||
StringLiterals SQLStringLits `mapstructure:"string_literals" yaml:"string_literals,omitempty"`
|
||
}
|
||
|
||
type SQLMigrations struct {
|
||
Paths []string `mapstructure:"paths" yaml:"paths,omitempty"`
|
||
Format string `mapstructure:"format" yaml:"format,omitempty"`
|
||
}
|
||
|
||
type SQLOrm struct {
|
||
Detect []string `mapstructure:"detect" yaml:"detect,omitempty"`
|
||
}
|
||
|
||
type SQLStringLits struct {
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
MinConfidence string `mapstructure:"min_confidence" yaml:"min_confidence,omitempty"`
|
||
}
|
||
|
||
// coverageDomainDefault is the per-domain default that applies when a
|
||
// user has not explicitly set Enabled in their YAML. Cheap structural
|
||
// domains default on; expensive or noisy domains default off. See the
|
||
// CoverageConfig doc comment for the canonical list.
|
||
var coverageDomainDefault = map[string]bool{
|
||
"function_shape": true,
|
||
"concurrency": true,
|
||
"constants": true,
|
||
"type_shape": true,
|
||
"codegen": true,
|
||
"todos": true,
|
||
"ownership": true,
|
||
"licenses": true,
|
||
"modules": true,
|
||
"configs": false,
|
||
"flags": false,
|
||
"observability": false,
|
||
"pubsub": false,
|
||
"sql": false,
|
||
"fixtures": true,
|
||
"cross_language": false,
|
||
"clones": true,
|
||
}
|
||
|
||
// resolveDomainEnabled returns the effective Enabled flag for a
|
||
// domain — explicit YAML setting if present, else the documented
|
||
// default. Used by IsCoverageEnabled and the per-domain accessors.
|
||
func resolveDomainEnabled(explicit *bool, domain string) bool {
|
||
if explicit != nil {
|
||
return *explicit
|
||
}
|
||
return coverageDomainDefault[domain]
|
||
}
|
||
|
||
// IsCoverageEnabled reports whether a coverage domain's extractor
|
||
// should run for this Config. Pass a domain string from the
|
||
// CoverageConfig YAML field names: "function_shape", "todos",
|
||
// "licenses", etc. Unknown domain strings return false.
|
||
func (c *Config) IsCoverageEnabled(domain string) bool {
|
||
return c.Index.Coverage.IsEnabled(domain)
|
||
}
|
||
|
||
// IsEnabled is the IndexConfig-level form of IsCoverageEnabled. The
|
||
// indexer holds an IndexConfig directly (not the parent Config) so
|
||
// the gate accessor lives here too — keeps the call-site short and
|
||
// avoids passing the whole Config around for one boolean check.
|
||
func (cov CoverageConfig) IsEnabled(domain string) bool {
|
||
switch domain {
|
||
case "function_shape":
|
||
return resolveDomainEnabled(cov.FunctionShape.Enabled, domain)
|
||
case "concurrency":
|
||
return resolveDomainEnabled(cov.Concurrency.Enabled, domain)
|
||
case "constants":
|
||
return resolveDomainEnabled(cov.Constants.Enabled, domain)
|
||
case "type_shape":
|
||
return resolveDomainEnabled(cov.TypeShape.Enabled, domain)
|
||
case "codegen":
|
||
return resolveDomainEnabled(cov.Codegen.Enabled, domain)
|
||
case "todos":
|
||
return resolveDomainEnabled(cov.Todos.Enabled, domain)
|
||
case "ownership":
|
||
return resolveDomainEnabled(cov.Ownership.Enabled, domain)
|
||
case "licenses":
|
||
return resolveDomainEnabled(cov.Licenses.Enabled, domain)
|
||
case "modules":
|
||
return resolveDomainEnabled(cov.Modules.Enabled, domain)
|
||
case "configs":
|
||
return resolveDomainEnabled(cov.Configs.Enabled, domain)
|
||
case "flags":
|
||
return resolveDomainEnabled(cov.Flags.Enabled, domain)
|
||
case "observability":
|
||
return resolveDomainEnabled(cov.Observability.Enabled, domain)
|
||
case "pubsub":
|
||
return resolveDomainEnabled(cov.Pubsub.Enabled, domain)
|
||
case "sql":
|
||
return resolveDomainEnabled(cov.SQL.Enabled, domain)
|
||
case "fixtures":
|
||
return resolveDomainEnabled(cov.Fixtures.Enabled, domain)
|
||
case "cross_language":
|
||
return resolveDomainEnabled(cov.CrossLanguage.Enabled, domain)
|
||
case "clones":
|
||
return resolveDomainEnabled(cov.Clones.Enabled, domain)
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ClonesThreshold returns the configured Jaccard similarity threshold
|
||
// for clone detection, or 0 when the user has not set one (callers
|
||
// then fall back to the clones package default). Out-of-range values
|
||
// are clamped to (0, 1].
|
||
func (cov CoverageConfig) ClonesThreshold() float64 {
|
||
t := cov.Clones.Threshold
|
||
if t <= 0 {
|
||
return 0
|
||
}
|
||
if t > 1 {
|
||
return 1
|
||
}
|
||
return t
|
||
}
|
||
|
||
// TodoTags returns the configured TODO tag set, or the default set
|
||
// when the user has not provided one. The defaults match the spec
|
||
// (TODO, FIXME, HACK, XXX, NOTE).
|
||
func (c *Config) TodoTags() []string {
|
||
if tags := c.Index.Coverage.Todos.Tags; len(tags) > 0 {
|
||
return tags
|
||
}
|
||
return []string{"TODO", "FIXME", "HACK", "XXX", "NOTE"}
|
||
}
|
||
|
||
// TodoMaxText returns the configured cap on stored TODO text, or the
|
||
// default of 200 characters.
|
||
func (c *Config) TodoMaxText() int {
|
||
if n := c.Index.Coverage.Todos.MaxText; n > 0 {
|
||
return n
|
||
}
|
||
return 200
|
||
}
|
||
|
||
type WatchConfig struct {
|
||
Enabled bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
Paths []string `mapstructure:"paths" yaml:"paths,omitempty"`
|
||
DebounceMs int `mapstructure:"debounce_ms" yaml:"debounce_ms,omitempty"`
|
||
// Exclude is deprecated — use top-level Config.Exclude instead.
|
||
// Kept for one release as a fallback merged into the unified list.
|
||
Exclude []string `mapstructure:"exclude" yaml:"exclude,omitempty"`
|
||
|
||
// StormThreshold — when more than this many events arrive within
|
||
// StormWindowMs, the watcher switches from per-file debounced
|
||
// patching to a batched reconcile that defers resolver + search
|
||
// rebuild until a quiet period has passed. Protects against event
|
||
// floods from bulk operations: `rsync`, `npm install`, branch
|
||
// checkout, bulk format-on-save, find-and-replace across a repo.
|
||
// Zero disables storm mode (pure per-file behaviour).
|
||
StormThreshold int `mapstructure:"storm_threshold" yaml:"storm_threshold,omitempty"`
|
||
// StormWindowMs is the sliding window over which events are counted
|
||
// against StormThreshold. Defaults to 500.
|
||
StormWindowMs int `mapstructure:"storm_window_ms" yaml:"storm_window_ms,omitempty"`
|
||
// StormQuietPeriodMs is how long the watcher waits for no events
|
||
// before draining the batch. Defaults to 500.
|
||
StormQuietPeriodMs int `mapstructure:"storm_quiet_period_ms" yaml:"storm_quiet_period_ms,omitempty"`
|
||
}
|
||
|
||
type QueryConfig struct {
|
||
DefaultDepth int `mapstructure:"default_depth" yaml:"default_depth,omitempty"`
|
||
MaxDepth int `mapstructure:"max_depth" yaml:"max_depth,omitempty"`
|
||
}
|
||
|
||
type ScopeConfig struct {
|
||
IntentDefaults bool `mapstructure:"intent_defaults" yaml:"intent_defaults,omitempty"`
|
||
}
|
||
|
||
// MergeEnv overlays the scope-specific environment knobs on top of
|
||
// file/default config values. Invalid values are ignored so a typo does not
|
||
// silently disable scoped queries.
|
||
func (c ScopeConfig) MergeEnv() ScopeConfig {
|
||
switch strings.ToLower(strings.TrimSpace(os.Getenv("GORTEX_SCOPE_INTENT_DEFAULTS"))) {
|
||
case "0", "false", "no":
|
||
c.IntentDefaults = false
|
||
case "1", "true", "yes":
|
||
c.IntentDefaults = true
|
||
}
|
||
return c
|
||
}
|
||
|
||
// SearchConfig configures the I13 11-signal rerank pipeline that
|
||
// orders `search_symbols` / `winnow_symbols` results. The Weights
|
||
// map is keyed by canonical signal name (rerank.SignalBM25,
|
||
// rerank.SignalSemantic, …) and overrides the tuned defaults
|
||
// returned by rerank.DefaultWeights(). Missing keys keep the
|
||
// default weight; setting a key to 0 disables that signal.
|
||
// FederationConfig is the .gortex.yaml `federation:` block. All knobs are
|
||
// optional; an unset field uses the daemon's built-in default.
|
||
type FederationConfig struct {
|
||
PerRemoteTimeoutMs int `mapstructure:"per_remote_timeout_ms" yaml:"per_remote_timeout_ms,omitempty"`
|
||
BudgetMs int `mapstructure:"budget_ms" yaml:"budget_ms,omitempty"`
|
||
BreakerThreshold int `mapstructure:"breaker_threshold" yaml:"breaker_threshold,omitempty"`
|
||
BreakerCooldownMs int `mapstructure:"breaker_cooldown_ms" yaml:"breaker_cooldown_ms,omitempty"`
|
||
NameKeyedFallback bool `mapstructure:"name_keyed_fallback" yaml:"name_keyed_fallback,omitempty"`
|
||
// Edges is the cross-daemon proxy-edge minting block. Off by
|
||
// default — federation stays read-only fan-out only.
|
||
Edges FederationEdgesConfig `mapstructure:"edges" yaml:"edges,omitempty"`
|
||
}
|
||
|
||
// FederationEdgesConfig is the `federation.edges` block — the
|
||
// cross-daemon proxy-node edge feature. Research-grade and
|
||
// off by default; none of the mint/hydrate paths run unless IsEnabled().
|
||
type FederationEdgesConfig struct {
|
||
// Enabled turns on proxy-node minting + lazy hydration.
|
||
Enabled bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
// TTLMs is the proxy-node neighbour-cache TTL in ms (default 5m).
|
||
TTLMs int `mapstructure:"ttl_ms" yaml:"ttl_ms,omitempty"`
|
||
// MaxProxyNodes is the hard heap bound across all remotes (default
|
||
// 5000); overflow refuses the mint.
|
||
MaxProxyNodes int `mapstructure:"max_proxy_nodes" yaml:"max_proxy_nodes,omitempty"`
|
||
// HydrateDepth is the neighbour rings pulled per access (default 1).
|
||
HydrateDepth int `mapstructure:"hydrate_depth" yaml:"hydrate_depth,omitempty"`
|
||
}
|
||
|
||
// IsEnabled reports whether proxy edges are on, honouring the
|
||
// GORTEX_FEDERATION_EDGES env override (1/true) over the config field.
|
||
func (c FederationEdgesConfig) IsEnabled() bool {
|
||
if v := strings.TrimSpace(os.Getenv("GORTEX_FEDERATION_EDGES")); v != "" {
|
||
return v == "1" || strings.EqualFold(v, "true")
|
||
}
|
||
return c.Enabled
|
||
}
|
||
|
||
// TTL is the proxy-node neighbour-cache TTL (default 5m).
|
||
func (c FederationEdgesConfig) TTL() time.Duration {
|
||
if c.TTLMs > 0 {
|
||
return time.Duration(c.TTLMs) * time.Millisecond
|
||
}
|
||
return 5 * time.Minute
|
||
}
|
||
|
||
// MaxNodes is the proxy-node heap bound across all remotes (default 5000).
|
||
func (c FederationEdgesConfig) MaxNodes() int {
|
||
if c.MaxProxyNodes > 0 {
|
||
return c.MaxProxyNodes
|
||
}
|
||
return 5000
|
||
}
|
||
|
||
// Depth is the neighbour rings pulled per hydration (default 1).
|
||
func (c FederationEdgesConfig) Depth() int {
|
||
if c.HydrateDepth > 0 {
|
||
return c.HydrateDepth
|
||
}
|
||
return 1
|
||
}
|
||
|
||
type SearchConfig struct {
|
||
// Weights overrides per-signal scoring weights. Canonical names:
|
||
// bm25, semantic, fan_in, hits, fan_out, churn, co_change,
|
||
// community, minhash, api_signature, type_signature, recency,
|
||
// feedback, file_coherence, path_penalty, definition_bias,
|
||
// source_bias, provenance, proximity.
|
||
Weights map[string]float64 `mapstructure:"weights" yaml:"weights,omitempty"`
|
||
|
||
// KeywordSoupRewrite controls how `search_symbols` handles a
|
||
// degenerate boolean / OR-soup query (`A OR B OR 'no access'`).
|
||
// - "split" (default): split the soup on its boolean operators
|
||
// and run a BM25 OR-merge over the disjuncts; LLM expansion is
|
||
// suppressed and a `query_advice` nudge rides on the response.
|
||
// - "nudge": leave retrieval unchanged but still attach the
|
||
// `query_advice` nudge.
|
||
// - "off": disable soup handling entirely.
|
||
// An empty value is treated as "split".
|
||
KeywordSoupRewrite string `mapstructure:"keyword_soup_rewrite" yaml:"keyword_soup_rewrite,omitempty"`
|
||
|
||
// EquivalenceClasses enables deterministic, LLM-free query
|
||
// expansion through the curated software-concept synonym table
|
||
// plus the per-repo auto-mined concept vocabulary. On by default
|
||
// (the pointer is nil-checked: nil means "on"). Set it false to
|
||
// disable equivalence expansion entirely.
|
||
EquivalenceClasses *bool `mapstructure:"equivalence_classes" yaml:"equivalence_classes,omitempty"`
|
||
|
||
// EquivalenceExtra adds repo-custom synonym classes to the curated
|
||
// equivalence table. Each entry maps a class label to its member
|
||
// words; the label itself joins the class. A label that names a
|
||
// curated word extends that curated class rather than forking it.
|
||
EquivalenceExtra map[string][]string `mapstructure:"equivalence_extra" yaml:"equivalence_extra,omitempty"`
|
||
|
||
// IndexProse controls whether Markdown prose-section (KindDoc)
|
||
// nodes are fed into the BM25 search index, so search_symbols
|
||
// with corpus:"docs" / "all" can return documentation hits. On
|
||
// by default (a nil pointer means "on"). Set false to keep prose
|
||
// out of the index entirely.
|
||
IndexProse *bool `mapstructure:"index_prose" yaml:"index_prose,omitempty"`
|
||
|
||
// VocabAnchoredExpansion sets the server-wide default for the
|
||
// search_symbols `vocab_anchored` argument: when on, the LLM
|
||
// query-expander's freely-invented synonyms are post-filtered to
|
||
// the words that actually appear in this repo's symbol names
|
||
// before they feed the BM25 OR-merge, so a model that hallucinates
|
||
// a plausible-but-absent term can't dilute the candidate pool. Off
|
||
// by default (a nil pointer means "off") -- the per-call argument
|
||
// is the primary control; this only changes the default when the
|
||
// argument is omitted. Degrades to unconstrained expansion when
|
||
// the repo's mined vocabulary is empty.
|
||
VocabAnchoredExpansion *bool `mapstructure:"vocab_anchored_expansion" yaml:"vocab_anchored_expansion,omitempty"`
|
||
|
||
// CosineRerank enables the post-rerank exact-cosine refinement
|
||
// stage: after the 11-signal rerank produces the ranked order,
|
||
// the top results are re-scored by exact cosine similarity
|
||
// between the query's embedding and each candidate's stored
|
||
// embedding, recovering the precise semantic distance the
|
||
// rank-based SemanticSignal discards. On by default (a nil
|
||
// pointer means "on"); the stage is a no-op whenever the vector
|
||
// channel is inactive (no embedder, no stored vectors, or the
|
||
// query fails to embed), so enabling it can never regress a
|
||
// text-only search. Set false to keep the pure rank-fusion order.
|
||
CosineRerank *bool `mapstructure:"cosine_rerank" yaml:"cosine_rerank,omitempty"`
|
||
}
|
||
|
||
// Keyword-soup rewrite modes for SearchConfig.KeywordSoupRewrite.
|
||
const (
|
||
KeywordSoupSplit = "split"
|
||
KeywordSoupNudge = "nudge"
|
||
KeywordSoupOff = "off"
|
||
)
|
||
|
||
// EquivalenceClassesEnabled reports whether deterministic
|
||
// equivalence-class query expansion is on. It defaults to true --
|
||
// a nil EquivalenceClasses pointer (the unset state) means enabled.
|
||
func (c SearchConfig) EquivalenceClassesEnabled() bool {
|
||
return c.EquivalenceClasses == nil || *c.EquivalenceClasses
|
||
}
|
||
|
||
// IndexProseEnabled reports whether Markdown prose-section nodes are
|
||
// indexed for search. Defaults to true -- a nil IndexProse pointer
|
||
// (the unset state) means enabled.
|
||
func (c SearchConfig) IndexProseEnabled() bool {
|
||
return c.IndexProse == nil || *c.IndexProse
|
||
}
|
||
|
||
// CosineRerankEnabled reports whether the post-rerank exact-cosine
|
||
// refinement stage is on. Defaults to true -- a nil CosineRerank
|
||
// pointer (the unset state) means enabled. The stage still no-ops
|
||
// when the vector channel is unavailable, so "enabled" is a
|
||
// permission, not a guarantee that it runs.
|
||
func (c SearchConfig) CosineRerankEnabled() bool {
|
||
return c.CosineRerank == nil || *c.CosineRerank
|
||
}
|
||
|
||
// VocabAnchoredExpansionDefault reports the server-wide default for
|
||
// the search_symbols `vocab_anchored` argument when the caller omits
|
||
// it. Defaults to false -- a nil VocabAnchoredExpansion pointer (the
|
||
// unset state) means off, so expansion stays unconstrained unless a
|
||
// caller (or this config) opts in.
|
||
func (c SearchConfig) VocabAnchoredExpansionDefault() bool {
|
||
return c.VocabAnchoredExpansion != nil && *c.VocabAnchoredExpansion
|
||
}
|
||
|
||
// EffectiveKeywordSoupRewrite folds the empty default into the
|
||
// canonical "split" mode and lower-cases the value, so callers can
|
||
// switch on it without re-normalising.
|
||
func (c SearchConfig) EffectiveKeywordSoupRewrite() string {
|
||
switch v := strings.ToLower(strings.TrimSpace(c.KeywordSoupRewrite)); v {
|
||
case "", KeywordSoupSplit:
|
||
return KeywordSoupSplit
|
||
case KeywordSoupNudge:
|
||
return KeywordSoupNudge
|
||
case KeywordSoupOff:
|
||
return KeywordSoupOff
|
||
default:
|
||
return KeywordSoupSplit
|
||
}
|
||
}
|
||
|
||
// EmbeddingConfig controls the semantic-search vector channel: which
|
||
// embedding provider builds the per-symbol vectors, and the chunking /
|
||
// concurrency knobs the indexer uses while building the vector index.
|
||
//
|
||
// Semantic search is a *fusion signal* layered alongside BM25 — the
|
||
// HybridBackend down-weights the vector channel for identifier-shaped
|
||
// queries — never a replacement for text search. The default provider
|
||
// is `static` (baked GloVe word vectors): it needs zero download and
|
||
// is CPU-only. As of the lean-index default, the static provider no
|
||
// longer builds a vector index automatically — FTS5/BM25 text search
|
||
// serves the default, and the semantic channel turns on when a real
|
||
// `local`/`api` provider (or `embedding.enabled: true`) is configured.
|
||
type EmbeddingConfig struct {
|
||
// Enabled is tri-state. A nil pointer means "not configured" —
|
||
// the vector channel is then built only when a real `local`/`api`
|
||
// provider is configured (the static default stays text-only). An
|
||
// explicit `embedding.enabled: false` turns the vector channel off;
|
||
// `true` forces it on, building whatever provider is configured
|
||
// (including the static one). Pointer so the loader can tell
|
||
// "absent" from "explicitly false", mirroring RespectGitignore.
|
||
Enabled *bool `mapstructure:"enabled" yaml:"enabled,omitempty"`
|
||
|
||
// Provider selects the embedding backend: `static` (baked GloVe,
|
||
// the default), `local` (best available transformer — Hugot
|
||
// MiniLM, auto-downloads ~87 MB on first use), or `api` (an
|
||
// external Ollama / OpenAI-compatible embedding endpoint). An
|
||
// empty value is treated as `static`.
|
||
Provider string `mapstructure:"provider" yaml:"provider,omitempty"`
|
||
|
||
// APIURL is the embedding endpoint base URL, used only when
|
||
// Provider is `api`. Ollama vs OpenAI wire format is auto-detected
|
||
// from the URL.
|
||
APIURL string `mapstructure:"api_url" yaml:"api_url,omitempty"`
|
||
|
||
// APIModel is the model name passed to the `api` provider. Empty
|
||
// lets the provider pick its format-appropriate default
|
||
// (nomic-embed-text for Ollama, text-embedding-3-small for OpenAI).
|
||
APIModel string `mapstructure:"api_model" yaml:"api_model,omitempty"`
|
||
|
||
// MaxSymbols overrides the indexer's hard cap on how many symbols
|
||
// are embedded into the vector index. Zero keeps the built-in
|
||
// default. Above the cap the vector channel is skipped and BM25
|
||
// alone serves search — an OOM during index is worse than a
|
||
// missing semantic boost.
|
||
MaxSymbols int `mapstructure:"max_symbols" yaml:"max_symbols,omitempty"`
|
||
|
||
// ChunkThresholdLines is the source-span line count above which a
|
||
// symbol is split into AST windows before embedding instead of
|
||
// being embedded as a single metadata-only vector. Zero keeps the
|
||
// built-in default (~60).
|
||
ChunkThresholdLines int `mapstructure:"chunk_threshold_lines" yaml:"chunk_threshold_lines,omitempty"`
|
||
|
||
// ChunkWindowLines caps the line span of each AST window emitted
|
||
// by the sub-chunker. Zero keeps the built-in default (~40).
|
||
ChunkWindowLines int `mapstructure:"chunk_window_lines" yaml:"chunk_window_lines,omitempty"`
|
||
|
||
// APIConcurrency bounds how many embedding requests the indexer
|
||
// issues in parallel against an `api` provider. Zero keeps the
|
||
// built-in default (4). Only the `api` provider runs concurrently —
|
||
// in-process transformer backends serialize on an inference mutex,
|
||
// so the pool would give them no speedup.
|
||
APIConcurrency int `mapstructure:"api_concurrency" yaml:"api_concurrency,omitempty"`
|
||
|
||
// Variant names a specific local transformer model to load when
|
||
// Provider is `local` -- a key from embedding.KnownHugotVariants
|
||
// (e.g. `fp32`, `bge_small`, `jina_code`). A non-empty Variant
|
||
// pins that exact model instead of the auto-selected default
|
||
// backend. Empty preserves the current default-selection
|
||
// behaviour. Ignored for the `static` and `api` providers.
|
||
// A variant change alters the embedding dimension, which the
|
||
// indexer's dims-mismatch guard detects -- stale vectors of the
|
||
// old width are discarded and the corpus is re-embedded.
|
||
Variant string `mapstructure:"variant" yaml:"variant,omitempty"`
|
||
}
|
||
|
||
// EmbeddingEnabledOrDefault resolves the tri-state Enabled flag against
|
||
// the default-on policy: an unset flag means semantic search is ON.
|
||
func (e EmbeddingConfig) EmbeddingEnabledOrDefault() bool {
|
||
if e.Enabled == nil {
|
||
return true
|
||
}
|
||
return *e.Enabled
|
||
}
|
||
|
||
// ExternalCallSynthesisEnabledOrDefault resolves the tri-state
|
||
// SynthesizeExternalCalls flag against the default-on policy: an unset
|
||
// flag (key absent) means external-package call qualification is ON, so
|
||
// every external call gets a stable cross-repo identity node by default.
|
||
// Default-on is affordable because the synthesis is incrementalised — a
|
||
// single-file reindex re-materialises only that file's external terminals
|
||
// (SynthesizeExternalCallsForFiles) instead of recomputing the whole
|
||
// graph — and the disk backend selects candidate edges through a pushdown
|
||
// (graph.ExternalCallCandidates) served by a partial index. Opt out
|
||
// per-repo with the key set to false or GORTEX_SYNTH_EXTERNAL_CALLS=0.
|
||
func (i IndexConfig) ExternalCallSynthesisEnabledOrDefault() bool {
|
||
if i.SynthesizeExternalCalls == nil {
|
||
return true
|
||
}
|
||
return *i.SynthesizeExternalCalls
|
||
}
|
||
|
||
// SpeculativeDispatchEnabledOrDefault resolves the tri-state
|
||
// SynthesizeSpeculativeDispatch flag against a default-OFF policy: an unset
|
||
// flag means speculative dynamic-dispatch synthesis is disabled (the edges are
|
||
// heuristic fan-outs, opt-in only). Enable per-repo with the key set to true
|
||
// or GORTEX_SYNTH_SPECULATIVE=1.
|
||
func (i IndexConfig) SpeculativeDispatchEnabledOrDefault() bool {
|
||
if i.SynthesizeSpeculativeDispatch == nil {
|
||
return false
|
||
}
|
||
return *i.SynthesizeSpeculativeDispatch
|
||
}
|
||
|
||
// ScopedGlobalPassesEnabledOrDefault resolves the tri-state ScopedGlobalPasses
|
||
// flag against a default-ON policy.
|
||
func (i IndexConfig) ScopedGlobalPassesEnabledOrDefault() bool {
|
||
if i.ScopedGlobalPasses == nil {
|
||
return true
|
||
}
|
||
return *i.ScopedGlobalPasses
|
||
}
|
||
|
||
// EmbeddingProviderOrDefault returns the configured provider name,
|
||
// falling back to `static` (the zero-download default) when unset.
|
||
func (e EmbeddingConfig) EmbeddingProviderOrDefault() string {
|
||
if e.Provider == "" {
|
||
return "static"
|
||
}
|
||
return e.Provider
|
||
}
|
||
|
||
type MCPConfig struct {
|
||
Transport string `mapstructure:"transport" yaml:"transport,omitempty"`
|
||
Port int `mapstructure:"port" yaml:"port,omitempty"`
|
||
Tools MCPToolsConfig `mapstructure:"tools" yaml:"tools,omitempty"`
|
||
// RedactConfigSecrets governs whether the content-serving read sinks
|
||
// (read_file / get_symbol_source / smart_context) withhold secret-shaped
|
||
// values from config / data-leaf files. A nil pointer means the default
|
||
// (redaction ON); set it to false to serve config-leaf values verbatim.
|
||
RedactConfigSecrets *bool `mapstructure:"redact_config_secrets" yaml:"redact_config_secrets,omitempty"`
|
||
// SmartContext governs the optional in-pack enrichment sections
|
||
// smart_context attaches to its pack. Every section is off by default.
|
||
SmartContext SmartContextConfig `mapstructure:"smart_context" yaml:"smart_context,omitempty"`
|
||
}
|
||
|
||
// RedactConfigSecretsEnabled reports whether config-leaf secret redaction is
|
||
// active for this MCP config. Redaction is on by default (a nil pointer), so an
|
||
// agent reading a populated .env / secrets.yaml gets the keys with their values
|
||
// withheld unless the caller opts out per call or sets this field false.
|
||
func (c MCPConfig) RedactConfigSecretsEnabled() bool {
|
||
return c.RedactConfigSecrets == nil || *c.RedactConfigSecrets
|
||
}
|
||
|
||
// MCPToolsConfig selects which MCP tools the server publishes. Preset is
|
||
// one of core (default) / full / readonly / edit / nav; Allow / Deny are
|
||
// per-tool deltas layered on the preset; Mode is "hide" (remove
|
||
// non-allowed tools from tools/list and hard-block calls) or "defer"
|
||
// (hide from the cold tools/list but keep reachable via tools_search).
|
||
// The default is core/defer — a lean, curated dev-cycle surface ships
|
||
// eagerly and the rest stay one tools_search away — so a cold session
|
||
// pays for ~34 tool schemas instead of the full ~180. Opt back into the
|
||
// whole surface with preset "full" (or GORTEX_TOOLS=full).
|
||
// GORTEX_TOOLS / GORTEX_TOOLS_MODE override these at runtime.
|
||
type MCPToolsConfig struct {
|
||
Preset string `mapstructure:"preset" yaml:"preset,omitempty"`
|
||
Mode string `mapstructure:"mode" yaml:"mode,omitempty"`
|
||
Allow []string `mapstructure:"allow" yaml:"allow,omitempty"`
|
||
Deny []string `mapstructure:"deny" yaml:"deny,omitempty"`
|
||
}
|
||
|
||
// defaultMaxParseBytesInFlight caps concurrent raw source bytes in
|
||
// extraction. 512 MiB keeps normal source indexing fully parallel (code
|
||
// files are tiny) while bounding peak memory when a repo carries large
|
||
// content assets (PDFs, office documents, datasets).
|
||
const defaultMaxParseBytesInFlight = 512 << 20
|
||
|
||
// defaultMaxDocumentBytes is the built-in per-file cap for document
|
||
// assets (PDF / pptx / xlsx / text). 10 MiB keeps normal-size documents
|
||
// searchable while dropping the few huge decks / spreadsheets that
|
||
// dominate a content repo's admitted bytes and blow up parse memory.
|
||
const defaultMaxDocumentBytes = 10 << 20
|
||
|
||
// defaultMaxDataBytes is the built-in per-file cap applied to data assets
|
||
// only when index.content.index_data opts them in — a backstop against a
|
||
// multi-GB shard. Matches the data extractor's stream-hash ceiling.
|
||
const defaultMaxDataBytes = 64 << 20
|
||
|
||
// Default returns a Config with sensible defaults.
|
||
//
|
||
// Exclude is intentionally empty here — the builtin baseline lives in
|
||
// excludes.Builtin and is layered in by ConfigManager.EffectiveExclude.
|
||
// Callers that need the full effective list should go through the
|
||
// ConfigManager, not Default().
|
||
func Default() *Config {
|
||
return &Config{
|
||
Index: IndexConfig{
|
||
Workers: runtime.NumCPU(),
|
||
MaxParseBytesInFlight: defaultMaxParseBytesInFlight,
|
||
// MaxFileSize: 0 = no cap. Opt-in knob for users who want
|
||
// to skip large generated/minified files.
|
||
// Prose indexing is on by default; the ConfigManager
|
||
// re-derives this from search.index_prose.
|
||
IndexProse: true,
|
||
// Corpus-admission defaults: cap document assets at 10 MiB
|
||
// and skip binary/vector data assets unless opted in (#120).
|
||
Content: ContentAdmissionConfig{
|
||
MaxDocumentBytes: defaultMaxDocumentBytes,
|
||
IndexData: false,
|
||
MaxDataBytes: defaultMaxDataBytes,
|
||
},
|
||
},
|
||
Watch: WatchConfig{
|
||
Enabled: false,
|
||
Paths: []string{"."},
|
||
DebounceMs: 150,
|
||
},
|
||
Query: QueryConfig{
|
||
DefaultDepth: 3,
|
||
MaxDepth: 10,
|
||
},
|
||
Scope: ScopeConfig{
|
||
IntentDefaults: true,
|
||
},
|
||
MCP: MCPConfig{
|
||
Transport: "stdio",
|
||
Port: 8765,
|
||
// Default tool surface: a curated dev-cycle "core" preset in
|
||
// defer mode — the lean cold tools/list is the workhorse set,
|
||
// the rest stay reachable on demand via tools_search. Opt into
|
||
// the full eager surface with preset "full" / GORTEX_TOOLS=full.
|
||
Tools: MCPToolsConfig{
|
||
Preset: "core",
|
||
Mode: "defer",
|
||
},
|
||
},
|
||
Multi: MultiRepoConfig{
|
||
AutoDetect: false,
|
||
},
|
||
Semantic: SemanticConfig{
|
||
Enabled: true,
|
||
TimeoutSeconds: 120,
|
||
EnrichOnWatch: false,
|
||
WatchDebounceMs: 500,
|
||
SkipEmbed: DefaultSkipEmbed(),
|
||
SkipSearch: DefaultSkipSearch(),
|
||
},
|
||
}
|
||
}
|
||
|
||
// Load reads config from file, environment, and returns a merged Config.
|
||
// configPath may be empty; in that case only default locations are searched.
|
||
//
|
||
// Legacy-shape handling: previously the `workspace:` key held a struct
|
||
// (`workspace: { auto_detect: true }`). The new schema
|
||
// reclaims `workspace:` as a scalar slug. Existing configs are migrated
|
||
// in place — `workspace.auto_detect` lifts into `multi.auto_detect`,
|
||
// and the loader emits a one-line deprecation note via the returned
|
||
// error chain (callers can choose whether to surface or swallow it).
|
||
func Load(configPath string) (*Config, error) {
|
||
v := viper.New()
|
||
v.SetConfigName(".gortex")
|
||
v.SetConfigType("yaml")
|
||
|
||
if configPath != "" {
|
||
v.SetConfigFile(configPath)
|
||
} else {
|
||
v.AddConfigPath(".")
|
||
v.AddConfigPath(platform.ConfigDir())
|
||
}
|
||
|
||
v.SetEnvPrefix("GORTEX")
|
||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||
v.AutomaticEnv()
|
||
|
||
cfg := Default()
|
||
|
||
if err := v.ReadInConfig(); err != nil {
|
||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||
return nil, err
|
||
}
|
||
// No config file found — use defaults + env.
|
||
}
|
||
|
||
// Migrate legacy `workspace:` mapping shape (held a struct with
|
||
// `auto_detect`) into the new `multi:` block so the v.Unmarshal
|
||
// below decodes the new schema cleanly. We do the migration on the
|
||
// viper key map so env-var overrides and viper's own merge logic
|
||
// stay consistent.
|
||
migrateLegacyWorkspaceKey(v)
|
||
|
||
if err := v.Unmarshal(cfg); err != nil {
|
||
return nil, err
|
||
}
|
||
cfg.Scope = cfg.Scope.MergeEnv()
|
||
|
||
if err := cfg.validateWorkspaceSchema(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return cfg, nil
|
||
}
|
||
|
||
// migrateLegacyWorkspaceKey rewrites `workspace.auto_detect` → `multi.auto_detect`
|
||
// in the viper key store before unmarshal, so a `.gortex.yaml` written
|
||
// against the legacy schema still produces a working Config without the
|
||
// caller seeing a parse error. The migration is silent — there's no
|
||
// global logger here — but the audit step (`gortex audit_agent_config`,
|
||
// reserved for a follow-up) can flag the deprecated key.
|
||
//
|
||
// Only the documented legacy field is migrated. Any other map under
|
||
// `workspace:` is rejected by `validateWorkspaceSchema` so unknown
|
||
// shapes don't get silently ignored.
|
||
func migrateLegacyWorkspaceKey(v *viper.Viper) {
|
||
raw := v.Get("workspace")
|
||
if raw == nil {
|
||
return
|
||
}
|
||
switch t := raw.(type) {
|
||
case string:
|
||
// Already in new shape; nothing to do.
|
||
case map[string]interface{}:
|
||
if ad, ok := t["auto_detect"]; ok {
|
||
// Move to the new home unless `multi.auto_detect`
|
||
// is already set explicitly (caller wins).
|
||
if v.Get("multi.auto_detect") == nil {
|
||
v.Set("multi.auto_detect", ad)
|
||
}
|
||
}
|
||
// The old shape never carried a workspace identity slug,
|
||
// so we clear the polymorphic key so v.Unmarshal doesn't
|
||
// fail trying to coerce a map into a string.
|
||
v.Set("workspace", "")
|
||
case map[interface{}]interface{}:
|
||
// yaml.v2 / older path — same semantics.
|
||
if ad, ok := t["auto_detect"]; ok {
|
||
if v.Get("multi.auto_detect") == nil {
|
||
v.Set("multi.auto_detect", ad)
|
||
}
|
||
}
|
||
v.Set("workspace", "")
|
||
default:
|
||
// Unrecognised shape; downstream coercion will surface
|
||
// a precise error rather than us silently dropping it.
|
||
}
|
||
}
|
||
|
||
// validateWorkspaceSchema enforces the defaults / boundaries that
|
||
// can't be expressed via struct tags alone:
|
||
//
|
||
// - `Project` and `Projects[]` are mutually exclusive (a repo is
|
||
// either single-project or a monorepo, never both).
|
||
// - Every `CrossWorkspaceDeps[].Mode` must be `read-only`. Iteration 1
|
||
// ships only the read-only mode.
|
||
// - `Workspace` slug, when set, may not be empty after trimming.
|
||
//
|
||
// Errors are concatenated so a malformed file surfaces every problem
|
||
// in one pass rather than one round-trip per fix.
|
||
func (c *Config) validateWorkspaceSchema() error {
|
||
var errs []string
|
||
if c.Project != "" && len(c.Projects) > 0 {
|
||
errs = append(errs, "config: 'project' and 'projects' are mutually exclusive — pick one")
|
||
}
|
||
for _, dep := range c.CrossWorkspaceDeps {
|
||
if dep.Workspace == "" {
|
||
errs = append(errs, "config: cross_workspace_deps[].workspace is required")
|
||
}
|
||
if len(dep.Modules) == 0 {
|
||
errs = append(errs, "config: cross_workspace_deps[\""+dep.Workspace+"\"].modules must be non-empty")
|
||
}
|
||
switch dep.Mode {
|
||
case "", "read-only":
|
||
// Empty defaults to read-only (the only iteration-1 mode).
|
||
default:
|
||
errs = append(errs,
|
||
"config: cross_workspace_deps[\""+dep.Workspace+"\"].mode = "+
|
||
strconv.Quote(dep.Mode)+" is unsupported in iteration 1; only \"read-only\" is allowed")
|
||
}
|
||
}
|
||
// Validate passive-attach blocks on each semantic provider. An
|
||
// empty `connect: {}` (both fields blank) is rejected here so a
|
||
// misconfigured YAML produces a clear error at load time rather
|
||
// than silently spawning the subprocess and surprising the user
|
||
// when the IDE coexistence story fails.
|
||
for _, pc := range c.Semantic.Providers {
|
||
if err := pc.Connect.Validate(); err != nil {
|
||
errs = append(errs, fmt.Sprintf("config: semantic.providers[%q]: %v", pc.Name, err))
|
||
}
|
||
}
|
||
if len(errs) == 0 {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||
}
|
||
|
||
// ValidateSemanticConnectForTest exposes the connect-block validation
|
||
// loop for tests that build a Config in memory and want to assert the
|
||
// loader would reject it. Production callers go through Load() which
|
||
// runs the full validateWorkspaceSchema pass; this thin export keeps
|
||
// the test surface small without forcing tests to also satisfy the
|
||
// cross_workspace_deps preconditions.
|
||
func (c *Config) ValidateSemanticConnectForTest() error {
|
||
var errs []string
|
||
for _, pc := range c.Semantic.Providers {
|
||
if err := pc.Connect.Validate(); err != nil {
|
||
errs = append(errs, fmt.Sprintf("config: semantic.providers[%q]: %v", pc.Name, err))
|
||
}
|
||
}
|
||
if len(errs) == 0 {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||
}
|