Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

1896 lines
93 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "; "))
}