// Package index defines the on-disk schema of the Einherjar framework index // and provides a loader for the embedded JSON blob. // // The index is built once at deploy time by cmd/indexer and consumed by every // MCP tool. Keeping it small, denormalised, and JSON-shaped means tools can // be implemented as straightforward in-memory filters. package index import ( "encoding/json" "fmt" "strings" "time" ) // SchemaVersion identifies the on-disk index format. Bump when fields change // in a way that breaks older consumers. const SchemaVersion = "einherjar.mcp/index/v1" // Index is the root of the embedded framework knowledge. type Index struct { Schema string `json:"schema"` Framework string `json:"framework"` BuiltAt time.Time `json:"builtAt"` Modules []Module `json:"modules"` } // Module describes one Einherjar module (e.g. core, web, auth-jwt). type Module struct { Name string `json:"name"` ImportPath string `json:"importPath"` Purpose string `json:"purpose"` Doc string `json:"doc,omitempty"` GoVersion string `json:"goVersion"` DependsOn []string `json:"dependsOn"` SubPackages []SubPackage `json:"subPackages"` Symbols []Symbol `json:"symbols"` ADRs []ADR `json:"adrs"` Examples []Example `json:"examples"` Compliance Compliance `json:"compliance"` Readme string `json:"readme,omitempty"` Changelog string `json:"changelog,omitempty"` } // Compliance captures a module's compliance_test.go contents: compile-time // interface assertions and the names of structural tests. It exists so an AI // assistant can know about machine-checked conventions before it writes code // that would violate them. type Compliance struct { InterfaceAsserts []InterfaceAssert `json:"interfaceAsserts"` Tests []ComplianceTest `json:"tests"` } // InterfaceAssert mirrors one `var _ Iface = impl` line in compliance_test.go. type InterfaceAssert struct { Module string `json:"module"` Interface string `json:"interface"` Impl string `json:"impl"` File string `json:"file"` Line int `json:"line"` } // ComplianceTest mirrors one Test* function in compliance_test.go. type ComplianceTest struct { Module string `json:"module"` Name string `json:"name"` Doc string `json:"doc"` File string `json:"file"` Line int `json:"line"` } // SubPackage is one importable sub-package of a module. type SubPackage struct { Name string `json:"name"` ImportPath string `json:"importPath"` Doc string `json:"doc"` } // Symbol is one exported declaration (type, func, interface, const, var). // // For struct types Fields carries the field set (the Signature is only the // one-line `type X struct` header, so without Fields the field names, types, // and — crucially — struct tags would be lost). For interface types Methods // carries the method set for the same reason. Both are empty for every other // kind. type Symbol struct { Module string `json:"module"` SubPackage string `json:"subPackage"` Kind string `json:"kind"` Name string `json:"name"` Signature string `json:"signature"` Doc string `json:"doc"` File string `json:"file"` Line int `json:"line"` Fields []Field `json:"fields,omitempty"` Methods []Method `json:"methods,omitempty"` } // Field is one field of a struct-type Symbol. Name is empty for an embedded // field (its type then appears in Type, with Embedded true). Tag is the raw // struct tag with the surrounding backticks stripped (e.g. // `env:"EINHERJAR_PG_HOST" envDefault:"postgres"`) — the single most useful // thing a config consumer needs and the thing the old index dropped. type Field struct { Name string `json:"name,omitempty"` Type string `json:"type"` Tag string `json:"tag,omitempty"` Doc string `json:"doc,omitempty"` Embedded bool `json:"embedded,omitempty"` } // Method is one method of an interface-type Symbol. Signature is the method // as written without the leading func keyword (e.g. // `GetExecutor(ctx context.Context) Executor`). For an embedded interface // Name is empty and Signature is the embedded interface's name. type Method struct { Name string `json:"name,omitempty"` Signature string `json:"signature"` Doc string `json:"doc,omitempty"` } // ADR is one architectural decision record. type ADR struct { Module string `json:"module"` ID string `json:"id"` Title string `json:"title"` Body string `json:"body"` } // Example is a fenced code block lifted from a module README. type Example struct { Module string `json:"module"` SubPackage string `json:"subPackage"` Title string `json:"title"` Code string `json:"code"` Language string `json:"language"` } // Load parses the embedded JSON blob into an Index. It validates the schema // version and returns an empty (but non-nil) Index when the blob is the // placeholder shipped before the indexer has been run. func Load(raw []byte) (*Index, error) { if len(raw) == 0 { return &Index{Schema: SchemaVersion, Framework: "einherjar"}, nil } idx := &Index{} if err := json.Unmarshal(raw, idx); err != nil { return nil, fmt.Errorf("index: parse: %w", err) } if idx.Schema != "" && idx.Schema != SchemaVersion { return nil, fmt.Errorf("index: schema mismatch: got %q want %q", idx.Schema, SchemaVersion) } return idx, nil } // FindModule returns the module with the given name, or nil if absent. func (i *Index) FindModule(name string) *Module { for k := range i.Modules { if i.Modules[k].Name == name { return &i.Modules[k] } } return nil } // SearchSymbols returns up to limit symbols whose name or doc contains q // (case-insensitive). Module name and sub-package are also searched. func (i *Index) SearchSymbols(q string, limit int) []Symbol { if limit <= 0 { limit = 25 } needle := strings.ToLower(q) out := make([]Symbol, 0, limit) for _, m := range i.Modules { for _, s := range m.Symbols { if matches(s, needle) { out = append(out, s) if len(out) >= limit { return out } } } } return out } func matches(s Symbol, needle string) bool { if needle == "" { return true } if strings.Contains(strings.ToLower(s.Name), needle) || strings.Contains(strings.ToLower(s.Doc), needle) || strings.Contains(strings.ToLower(s.SubPackage), needle) || strings.Contains(strings.ToLower(s.Module), needle) { return true } // A struct is also findable by a field name, type, or tag — so a query // like an env-var key or json field resolves to the type that declares it. for _, f := range s.Fields { if strings.Contains(strings.ToLower(f.Name), needle) || strings.Contains(strings.ToLower(f.Type), needle) || strings.Contains(strings.ToLower(f.Tag), needle) { return true } } // An interface is findable by a method it requires. for _, mth := range s.Methods { if strings.Contains(strings.ToLower(mth.Name), needle) || strings.Contains(strings.ToLower(mth.Signature), needle) { return true } } return false }