[{"data":1,"prerenderedAt":2613},["ShallowReactive",2],{"search-sections-sentinel":3,"nav-sentinel":603,"content-tree-sentinel":656,"footer-resources":680,"content-/v1.0.4/learn/overview":2498,"surround-/v1.0.4/learn/overview":2610},[4,10,14,20,25,30,35,40,44,49,54,59,64,69,73,78,82,87,92,97,102,107,111,116,120,125,129,135,140,145,150,155,160,165,170,175,179,183,187,192,197,202,207,212,217,222,227,232,236,240,243,248,253,258,263,267,272,277,282,286,291,294,299,304,309,314,319,324,328,333,337,342,347,352,357,362,367,371,376,380,385,389,394,399,404,409,414,418,423,428,433,437,441,445,450,454,459,463,467,471,476,481,485,490,493,497,502,506,510,515,519,524,529,534,539,544,548,553,558,563,568,571,575,580,585,589,594,598],{"id":5,"title":6,"titles":7,"content":8,"level":9},"/v1.0.4/learn/overview","Overview",[],"Struct metadata extraction and relationship discovery for Go",1,{"id":11,"title":6,"titles":12,"content":13,"level":9},"/v1.0.4/learn/overview#overview",[],"Sentinel is a Go package for extracting struct metadata. It takes what reflection gives you—field names, types, kinds, tags, relationships—and normalizes it into a consistent, queryable format. It's the foundation for type-driven development: scan your types once at startup, then access that metadata anywhere in your application.",{"id":15,"title":16,"titles":17,"content":18,"level":19},"/v1.0.4/learn/overview#the-idea","The Idea",[6],"Go's type system already knows a lot about your domain: field names, types, struct tags, how types relate to each other. The problem is that reflection makes you work for it—low-level APIs, careful handling, repetitive boilerplate. What if accessing struct metadata was trivial? That's the question Sentinel answers. The goal is to make domain types the single source of truth—define your structs once, and let tooling derive schemas, documentation, validation, and queries from that definition. Sentinel handles the extraction and normalization. From there, other packages do their work: schema generators read field types, documentation tools map relationships, validators pull rules from tags, query builders use field metadata. Sentinel does the extraction—what gets built on top is up to you.",2,{"id":21,"title":22,"titles":23,"content":24,"level":19},"/v1.0.4/learn/overview#the-implementation","The Implementation",[6],"Two functions, two use cases: Inspect examines a single type in isolation. Scan traverses your entire domain model from one entry point, discovering all related types within your module. Both return normalized metadata—field names, types, kinds, tags, relationships—cached permanently after the first call.",{"id":26,"title":27,"titles":28,"content":29,"level":19},"/v1.0.4/learn/overview#what-it-enables","What It Enables",[6],"See it in action: Entity relationship diagrams with erdType-safe SQL queries with soyOpenAPI documentation with rocco",{"id":31,"title":32,"titles":33,"content":34,"level":19},"/v1.0.4/learn/overview#next-steps","Next Steps",[6],"Quickstart — installation and basic usageConcepts — metadata structures and cachingArchitecture — internal designAPI Reference — complete function documentation",{"id":36,"title":37,"titles":38,"content":39,"level":9},"/v1.0.4/learn/quickstart","Quickstart",[],"Get started with sentinel in 5 minutes",{"id":41,"title":37,"titles":42,"content":43,"level":9},"/v1.0.4/learn/quickstart#quickstart",[],"",{"id":45,"title":46,"titles":47,"content":48,"level":19},"/v1.0.4/learn/quickstart#installation","Installation",[37],"go get github.com/zoobz-io/sentinel@latest Requires Go 1.24 or later.",{"id":50,"title":51,"titles":52,"content":53,"level":19},"/v1.0.4/learn/quickstart#basic-usage","Basic Usage",[37],"package main\n\nimport (\n    \"fmt\"\n    \"github.com/zoobz-io/sentinel\"\n)\n\ntype User struct {\n    ID    string `json:\"id\" db:\"user_id\"`\n    Name  string `json:\"name\" validate:\"required\"`\n    Email string `json:\"email\" validate:\"email\"`\n}\n\nfunc main() {\n    metadata := sentinel.Inspect[User]()\n\n    fmt.Printf(\"Type: %s\\n\", metadata.TypeName)\n    fmt.Printf(\"Fields: %d\\n\", len(metadata.Fields))\n\n    for _, field := range metadata.Fields {\n        fmt.Printf(\"  %s: %s\\n\", field.Name, field.Type)\n        for tag, value := range field.Tags {\n            fmt.Printf(\"    @%s = %s\\n\", tag, value)\n        }\n    }\n}",{"id":55,"title":56,"titles":57,"content":58,"level":19},"/v1.0.4/learn/quickstart#scanning","Scanning",[37],"Inspect extracts a single type. Scan recursively discovers all related types within your module, following struct fields to build a complete type graph. type User struct {\n    ID      string   `json:\"id\"`\n    Profile *Profile `json:\"profile\"` // reference\n    Orders  []Order  `json:\"orders\"`  // collection\n}\n\ntype Profile struct { Bio string `json:\"bio\"` }\ntype Order struct { ID string `json:\"id\"` }\n\nmetadata := sentinel.Scan[User]() Sentinel detects four relationship kinds: references (struct or pointer fields), collections (slices and arrays), embeddings (anonymous fields), and maps (map values). After scanning, all discovered types are cached. // List all cached type names\nfqdns := sentinel.Browse()\n// [\"github.com/you/app.User\", \"github.com/you/app.Profile\", \"github.com/you/app.Order\"]\n\n// Retrieve by fully qualified name\nprofile, ok := sentinel.Lookup(\"github.com/you/app.Profile\")\n\n// Export complete schema\nschema := sentinel.Schema() See the scanning guide for module boundary behaviour and when to use each mode.",{"id":60,"title":61,"titles":62,"content":63,"level":19},"/v1.0.4/learn/quickstart#custom-tags","Custom Tags",[37],"Sentinel extracts common tags (json, db, validate) by default. Register additional tags before extraction: sentinel.Tag(\"graphql\") See custom tags for the full list of built-in tags and usage patterns.",{"id":65,"title":66,"titles":67,"content":68,"level":19},"/v1.0.4/learn/quickstart#testing","Testing",[37],"The global cache persists across tests. For isolation, use the testing helpers with -tags testing: sentineltest.ResetCache(t) See testing for patterns and benchmarks.",{"id":70,"title":32,"titles":71,"content":72,"level":19},"/v1.0.4/learn/quickstart#next-steps",[37],"Concepts — metadata structures, relationships, and cachingArchitecture — internal design and component interactionsAPI Reference — complete function documentation html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}",{"id":74,"title":75,"titles":76,"content":77,"level":9},"/v1.0.4/learn/concepts","Concepts",[],"Mental models for understanding sentinel",{"id":79,"title":75,"titles":80,"content":81,"level":9},"/v1.0.4/learn/concepts#concepts",[],"Sentinel operates on a few core abstractions. Understanding these helps you reason about what the library does and why.",{"id":83,"title":84,"titles":85,"content":86,"level":19},"/v1.0.4/learn/concepts#metadata","Metadata",[75],"Metadata is a normalized representation of a struct type. Where Go's reflect package gives you low-level access to type information, metadata gives you a structured view: the type's identity, its fields, and its relationships to other types. Sentinel extracts metadata at two levels. Type metadata captures the struct as a whole—its name, package, and the graph of types it references. Field metadata captures each exported field—its name, type, kind, and struct tags. Only exported fields are visible. Go's reflection cannot access unexported fields from outside their package, so sentinel doesn't try.",{"id":88,"title":89,"titles":90,"content":91,"level":19},"/v1.0.4/learn/concepts#type-identity","Type Identity",[75],"Every type has a fully qualified domain name (FQDN): the package path combined with the type name. For example, github.com/you/app/models.User. FQDNs matter because type names alone aren't unique. Two packages can define a User type. The FQDN distinguishes them, and sentinel uses it as the cache key and in relationship references.",{"id":93,"title":94,"titles":95,"content":96,"level":19},"/v1.0.4/learn/concepts#relationships","Relationships",[75],"Struct fields that reference other structs create relationships. Sentinel categorizes these into four kinds: Reference — a direct struct or pointer field (Profile *Profile)Collection — a slice or array of structs (Orders []Order)Embedding — an anonymous field (BaseModel)Map — a map with struct values (Items map[string]Item) Relationships are directional: they point from the containing type to the referenced type. Together, a set of relationships forms a type graph that you can traverse.",{"id":98,"title":99,"titles":100,"content":101,"level":19},"/v1.0.4/learn/concepts#field-kinds","Field Kinds",[75],"Each field has a kind that categorizes its type for conditional logic: scalar, pointer, slice, struct, map, or interface. This lets you branch on the shape of a field without parsing type strings.",{"id":103,"title":104,"titles":105,"content":106,"level":19},"/v1.0.4/learn/concepts#immutability","Immutability",[75],"Go's type system is fixed at compile time. A struct's fields, types, and tags cannot change while the program runs. Sentinel exploits this: once metadata is extracted, it never needs to be re-extracted or invalidated. The cache is permanent. This also means the cache is global. There's one type system, so there's one metadata cache. No instance management, no lifecycle, no expiration.",{"id":108,"title":32,"titles":109,"content":110,"level":19},"/v1.0.4/learn/concepts#next-steps",[75],"Architecture — internal design and implementationAPI Reference — complete type and function documentation",{"id":112,"title":113,"titles":114,"content":115,"level":9},"/v1.0.4/learn/architecture","Architecture",[],"Internal design and component interactions in sentinel",{"id":117,"title":113,"titles":118,"content":119,"level":9},"/v1.0.4/learn/architecture#architecture",[],"How sentinel's components work together to extract, cache, and query struct metadata.",{"id":121,"title":122,"titles":123,"content":124,"level":19},"/v1.0.4/learn/architecture#component-overview","Component Overview",[113],"┌─────────────────────────────────────────────────────────────┐\n│                        Public API                           │\n│  Inspect[T]()  Scan[T]()  Browse()  Lookup()  Schema()      │\n└─────────────────────────────────┬───────────────────────────┘\n                                  │\n                                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│                      Sentinel Instance                      │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │\n│  │    Cache    │  │ Tag Registry│  │   Module Path       │  │\n│  │  (RWMutex)  │  │  (RWMutex)  │  │ (debug.BuildInfo)   │  │\n│  └─────────────┘  └─────────────┘  └─────────────────────┘  │\n└─────────────────────────────────┬───────────────────────────┘\n                                  │\n                                  ▼\n┌─────────────────────────────────────────────────────────────┐\n│                    Extraction Pipeline                      │\n│  ┌──────────────────┐    ┌──────────────────────────────┐   │\n│  │ extractMetadata  │───▶│ extractMetadataInternal      │   │\n│  │ (single type)    │    │ (with cycle detection)       │   │\n│  └──────────────────┘    └───────────────┬──────────────┘   │\n│                                          │                  │\n│  ┌──────────────────┐    ┌───────────────▼──────────────┐   │\n│  │extractFieldMeta  │◀───│  extractRelationships        │   │\n│  │ (tags, kinds)    │    │  (recursive if Scan mode)    │   │\n│  └──────────────────┘    └──────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────┘ The singleton initialises at package load, detecting the module path via debug.ReadBuildInfo().",{"id":126,"title":127,"titles":128,"content":43,"level":19},"/v1.0.4/learn/architecture#extraction-pipeline","Extraction Pipeline",[113],{"id":130,"title":131,"titles":132,"content":133,"level":134},"/v1.0.4/learn/architecture#inspect-flow","Inspect Flow",[113,127],"Inspect[T]()\n    │\n    ├─▶ Check cache ─── hit ──▶ Return cached\n    │\n    └─▶ miss\n         │\n         ├─▶ extractFieldMetadata(t)\n         ├─▶ extractRelationships(t, nil)  // nil = no recursion\n         └─▶ Cache and return",3,{"id":136,"title":137,"titles":138,"content":139,"level":134},"/v1.0.4/learn/architecture#scan-flow","Scan Flow",[113,127],"Scan[T]()\n    │\n    ▼\nCreate visited = map[string]bool{}\n    │\n    ▼\nextractMetadataInternal(t, visited)\n    │\n    ├─▶ Check visited ─── yes ──▶ Return (cycle prevention)\n    │\n    └─▶ Mark visited[fqdn] = true\n         │\n         ├─▶ extractFieldMetadata(t)\n         ├─▶ extractRelationships(t, visited)\n         │        │\n         │        └─▶ For each relationship in module:\n         │             └─▶ extractMetadataInternal(relatedType, visited)\n         │\n         └─▶ Cache and return The visited map serves dual purpose: cycle detection and mode signal (non-nil triggers recursion).",{"id":141,"title":142,"titles":143,"content":144,"level":19},"/v1.0.4/learn/architecture#cycle-detection","Cycle Detection",[113],"Circular references are common in domain models: type A struct { B *B }\ntype B struct { A *A } When scanning A: mark visited → extract A.B → recurse to B → mark visited → extract B.A → A already visited → return. No infinite loop.",{"id":146,"title":147,"titles":148,"content":149,"level":19},"/v1.0.4/learn/architecture#domain-boundaries","Domain Boundaries",[113],"Sentinel uses two-tier boundary detection to control relationship discovery: Package boundary: In Inspect mode, only types in the exact same package create relationships. Module boundary: In Scan mode, types anywhere in your module are recursed into, using the module path from debug.ReadBuildInfo(). ModeBoundaryMatchInspectPackagetargetPkg == sourcePkgScanModulestrings.HasPrefix(targetPkg, modulePath) If build info is unavailable, modulePath is empty and Scan degrades to Inspect behaviour—no recursion, but no crash.",{"id":151,"title":152,"titles":153,"content":154,"level":19},"/v1.0.4/learn/architecture#tag-extraction","Tag Extraction",[113],"Field metadata includes struct tags. Eight common tags are always extracted: json, validate, db, scope, encrypt, redact, desc, example Custom tags registered via Tag() are checked dynamically at extraction time. The registry is protected by sync.RWMutex.",{"id":156,"title":157,"titles":158,"content":159,"level":19},"/v1.0.4/learn/architecture#field-kind-classification","Field Kind Classification",[113],"Each field is categorized into a FieldKind: scalar, pointer, slice, struct, map, or interface. This enables conditional logic without parsing type strings.",{"id":161,"title":162,"titles":163,"content":164,"level":19},"/v1.0.4/learn/architecture#thread-safety","Thread Safety",[113],"OperationLock TypeContentionCache readRLockLow (concurrent reads)Cache writeLockMomentary (first access only)Tag registry readRLockLowTag registry writeLockRare (registration only) After initial extraction, operations are read-only.",{"id":166,"title":167,"titles":168,"content":169,"level":19},"/v1.0.4/learn/architecture#performance","Performance",[113],"OperationFirst CallSubsequent CallsInspect[T]()Reflection + cache writeCache read onlyScan[T]()Reflection + recursive scanCache read onlyBrowse()—O(n) key copyLookup()—O(1) map accessSchema()—O(n) map copy",{"id":171,"title":172,"titles":173,"content":174,"level":19},"/v1.0.4/learn/architecture#design-qa","Design Q&A",[113],"Why is the cache global? Go's type system is global—one program, one set of types. A global cache mirrors this. There's no benefit to per-instance caches since the underlying type system is shared. Why package-level functions instead of methods? Go doesn't permit methods with type parameters. Inspect[T]() must be a function. Why does Scan respect module boundaries? Without boundaries, scanning a type that references time.Time or sql.NullString would pull in standard library and third-party types. Module awareness keeps focus on your domain. What if build info isn't available? Sentinel degrades gracefully—Scan behaves like Inspect, extracting only the requested type without recursion.",{"id":176,"title":32,"titles":177,"content":178,"level":19},"/v1.0.4/learn/architecture#next-steps",[113],"Scanning Guide — practical Inspect vs Scan usageTesting Guide — testing patterns with sentinelAPI Reference — complete function documentation html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":180,"title":56,"titles":181,"content":182,"level":9},"/v1.0.4/guides/scanning",[],"Inspect vs Scan and module boundary detection",{"id":184,"title":185,"titles":186,"content":43,"level":9},"/v1.0.4/guides/scanning#scanning-guide","Scanning Guide",[],{"id":188,"title":189,"titles":190,"content":191,"level":19},"/v1.0.4/guides/scanning#inspect-vs-scan","Inspect vs Scan",[185],"Sentinel provides two extraction modes:",{"id":193,"title":194,"titles":195,"content":196,"level":134},"/v1.0.4/guides/scanning#inspect","Inspect",[185,189],"Extracts metadata for a single type only: metadata := sentinel.Inspect[User]()\n// Only User is cached\n// Related types (Profile, Order) are NOT cached Use Inspect when: You need metadata for specific typesYou want fine-grained control over cachingYou're working with types that have external dependencies",{"id":198,"title":199,"titles":200,"content":201,"level":134},"/v1.0.4/guides/scanning#scan","Scan",[185,189],"Recursively extracts a type and all related types within your module: metadata := sentinel.Scan[User]()\n// User is cached\n// Profile, Order, Address, and all transitively related types are cached Use Scan when: You want to discover your entire domain modelYou're building schema exports or documentationYou need relationship graphs",{"id":203,"title":204,"titles":205,"content":206,"level":19},"/v1.0.4/guides/scanning#module-boundary-detection","Module Boundary Detection",[185],"Scan only processes types within your module to avoid pulling in external library types.",{"id":208,"title":209,"titles":210,"content":211,"level":134},"/v1.0.4/guides/scanning#how-it-works","How It Works",[185,204],"Sentinel uses debug.ReadBuildInfo() to read your module path from go.mod: go.mod: module github.com/you/myapp Types are scanned if their package path starts with your module path: // Your module: github.com/you/myapp\n\ngithub.com/you/myapp/models.User      // ✓ Scanned\ngithub.com/you/myapp/api/types.Request // ✓ Scanned\ngithub.com/lib/pq.NullString           // ✗ Ignored\ntime.Time                               // ✗ Ignored (stdlib)",{"id":213,"title":214,"titles":215,"content":216,"level":134},"/v1.0.4/guides/scanning#vanity-imports","Vanity Imports",[185,204],"Module detection correctly handles vanity import paths: go.mod: module go.yourcompany.com/service This works regardless of where the code is actually hosted.",{"id":218,"title":219,"titles":220,"content":221,"level":19},"/v1.0.4/guides/scanning#relationship-kinds","Relationship Kinds",[185],"When scanning, Sentinel tracks how types relate: type User struct {\n    Profile  *Profile          // reference\n    Orders   []Order           // collection\n    Settings                   // embedding (anonymous)\n    Metadata map[string]Meta   // map\n}",{"id":223,"title":224,"titles":225,"content":226,"level":19},"/v1.0.4/guides/scanning#inspecting-relationships","Inspecting Relationships",[185],"After scanning, explore relationships: sentinel.Scan[User]()\n\n// All types that User references\nrels := sentinel.GetRelationships[User]()\n\n// All types that reference Profile\nrefs := sentinel.GetReferencedBy[Profile]()\n\n// List all cached types\ntypes := sentinel.Browse()\n\n// Export complete schema\nschema := sentinel.Schema()",{"id":228,"title":229,"titles":230,"content":231,"level":19},"/v1.0.4/guides/scanning#best-practices","Best Practices",[185],"Scan once at startup from your root domain typeUse Inspect for targeted extraction of specific typesRegister custom tags before first Scan/Inspect callExport Schema for documentation or code generation",{"id":233,"title":32,"titles":234,"content":235,"level":19},"/v1.0.4/guides/scanning#next-steps",[185],"Custom Tags — registering and extracting struct tagsTesting — test isolation and patternsAPI Reference — complete Scan documentation html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}",{"id":237,"title":61,"titles":238,"content":239,"level":9},"/v1.0.4/guides/tags",[],"Registering and extracting custom struct tags",{"id":241,"title":61,"titles":242,"content":43,"level":9},"/v1.0.4/guides/tags#custom-tags",[],{"id":244,"title":245,"titles":246,"content":247,"level":19},"/v1.0.4/guides/tags#built-in-tags","Built-in Tags",[61],"Sentinel extracts these tags by default: TagPurposejsonJSON serializationdbDatabase mappingvalidateValidation rulesscopeAuthorization scopesencryptEncryption markersredactRedaction hintsdescField descriptionsexampleExample values",{"id":249,"title":250,"titles":251,"content":252,"level":19},"/v1.0.4/guides/tags#registering-custom-tags","Registering Custom Tags",[61],"Register additional tags before extraction: // Register at init or before first Inspect/Scan\nsentinel.Tag(\"graphql\")\nsentinel.Tag(\"proto\")\nsentinel.Tag(\"xml\") Tags can be registered at any time, but only affect future extractions: sentinel.Inspect[User]() // \"custom\" tag NOT extracted\n\nsentinel.Tag(\"custom\")\n\nsentinel.Inspect[Product]() // \"custom\" tag IS extracted\nsentinel.Inspect[User]()    // Still cached without \"custom\"",{"id":254,"title":255,"titles":256,"content":257,"level":19},"/v1.0.4/guides/tags#accessing-tags","Accessing Tags",[61],"Tags are stored as a map on each field: type User struct {\n    Email string `json:\"email\" validate:\"required,email\" custom:\"pii\"`\n}\n\nmetadata := sentinel.Inspect[User]()\nfield := metadata.Fields[0]\n\nfield.Tags[\"json\"]     // \"email\"\nfield.Tags[\"validate\"] // \"required,email\"\nfield.Tags[\"custom\"]   // \"pii\"",{"id":259,"title":260,"titles":261,"content":262,"level":19},"/v1.0.4/guides/tags#tag-parsing","Tag Parsing",[61],"Sentinel preserves the raw tag value. Parsing tag syntax is your responsibility: // Tag value: \"required,email,max=100\"\nrules := field.Tags[\"validate\"]\n\n// Parse as needed for your validation library\nparts := strings.Split(rules, \",\")",{"id":264,"title":265,"titles":266,"content":43,"level":19},"/v1.0.4/guides/tags#use-cases","Use Cases",[61],{"id":268,"title":269,"titles":270,"content":271,"level":134},"/v1.0.4/guides/tags#validation-rules","Validation Rules",[61,265],"type Form struct {\n    Email    string `validate:\"required,email\"`\n    Password string `validate:\"required,min=8\"`\n}\n\nfor _, field := range metadata.Fields {\n    if rules, ok := field.Tags[\"validate\"]; ok {\n        // Apply validation logic\n    }\n}",{"id":273,"title":274,"titles":275,"content":276,"level":134},"/v1.0.4/guides/tags#database-schema","Database Schema",[61,265],"type Model struct {\n    ID   string `db:\"id,primarykey\"`\n    Name string `db:\"name,index\"`\n}\n\nfor _, field := range metadata.Fields {\n    if col, ok := field.Tags[\"db\"]; ok {\n        // Generate DDL or migrations\n    }\n}",{"id":278,"title":279,"titles":280,"content":281,"level":134},"/v1.0.4/guides/tags#api-documentation","API Documentation",[61,265],"type Request struct {\n    UserID string `json:\"user_id\" example:\"usr_123\" desc:\"The user identifier\"`\n}\n\nfor _, field := range metadata.Fields {\n    example := field.Tags[\"example\"]\n    desc := field.Tags[\"desc\"]\n    // Generate OpenAPI spec\n}",{"id":283,"title":32,"titles":284,"content":285,"level":19},"/v1.0.4/guides/tags#next-steps",[61],"Scanning — recursive type discoveryTesting — testing tag extractionAPI Reference — Tag() function documentation html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}",{"id":287,"title":288,"titles":289,"content":290,"level":9},"/v1.0.4/guides/testing","Testing with Sentinel",[],"Testing patterns and cache behavior",{"id":292,"title":288,"titles":293,"content":43,"level":9},"/v1.0.4/guides/testing#testing-with-sentinel",[],{"id":295,"title":296,"titles":297,"content":298,"level":19},"/v1.0.4/guides/testing#cache-behavior-in-tests","Cache Behavior in Tests",[288],"Sentinel uses a global cache that persists across tests. This is usually fine because: Type metadata is deterministicCache hits return identical dataNo side effects from cached metadata",{"id":300,"title":301,"titles":302,"content":303,"level":19},"/v1.0.4/guides/testing#test-isolation","Test Isolation",[288],"If you need cache isolation, use the testing helpers by running with the testing build tag: import (\n    \"testing\"\n\n    \"github.com/zoobz-io/sentinel\"\n    sentineltest \"github.com/zoobz-io/sentinel/testing\"\n)\n\nfunc TestSomething(t *testing.T) {\n    sentineltest.ResetCache(t)\n    metadata := sentinel.Inspect[MyType]()\n    // ...\n} Run tests with -tags testing to enable the helpers: go test -tags testing ./...\n# or use the Makefile\nmake test The Reset() function and testing helpers are only available when building with -tags testing. This prevents accidental cache clearing in production.",{"id":305,"title":306,"titles":307,"content":308,"level":19},"/v1.0.4/guides/testing#testing-metadata-extraction","Testing Metadata Extraction",[288],"func TestUserMetadata(t *testing.T) {\n    metadata := sentinel.Inspect[User]()\n\n    if metadata.TypeName != \"User\" {\n        t.Errorf(\"expected User, got %s\", metadata.TypeName)\n    }\n\n    if len(metadata.Fields) != 3 {\n        t.Errorf(\"expected 3 fields, got %d\", len(metadata.Fields))\n    }\n}",{"id":310,"title":311,"titles":312,"content":313,"level":19},"/v1.0.4/guides/testing#testing-relationships","Testing Relationships",[288],"func TestUserRelationships(t *testing.T) {\n    metadata := sentinel.Scan[User]()\n\n    var hasProfile bool\n    for _, rel := range metadata.Relationships {\n        if strings.HasSuffix(rel.To, \".Profile\") && rel.Kind == \"reference\" {\n            hasProfile = true\n        }\n    }\n\n    if !hasProfile {\n        t.Error(\"expected relationship to Profile\")\n    }\n}",{"id":315,"title":316,"titles":317,"content":318,"level":19},"/v1.0.4/guides/testing#testing-tag-extraction","Testing Tag Extraction",[288],"func TestCustomTags(t *testing.T) {\n    sentinel.Tag(\"custom\")\n\n    type Tagged struct {\n        Field string `custom:\"value\"`\n    }\n\n    metadata := sentinel.Inspect[Tagged]()\n\n    if metadata.Fields[0].Tags[\"custom\"] != \"value\" {\n        t.Error(\"custom tag not extracted\")\n    }\n}",{"id":320,"title":321,"titles":322,"content":323,"level":19},"/v1.0.4/guides/testing#benchmarking","Benchmarking",[288],"Run benchmarks to verify performance: make bench\n# or\ngo test -bench=. -benchmem ./testing/benchmarks/ Expected performance: Cache hit: ~80ns, 0 allocationsConcurrent access: ~40ns per operation",{"id":325,"title":32,"titles":326,"content":327,"level":19},"/v1.0.4/guides/testing#next-steps",[288],"Troubleshooting — common issues and fixesAPI Reference — complete function documentation html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":329,"title":330,"titles":331,"content":332,"level":9},"/v1.0.4/guides/troubleshooting","Troubleshooting",[],"Common issues and how to resolve them",{"id":334,"title":330,"titles":335,"content":336,"level":9},"/v1.0.4/guides/troubleshooting#troubleshooting",[],"Common issues when using sentinel and how to resolve them.",{"id":338,"title":339,"titles":340,"content":341,"level":19},"/v1.0.4/guides/troubleshooting#panic-on-inspect-or-scan","Panic on Inspect or Scan",[330],"Symptom: panic: sentinel: type must be a struct Cause: You passed a non-struct type to Inspect[T]() or Scan[T](). Solution: Use the Try variants which return an error instead of panicking: metadata, err := sentinel.TryInspect[MyType]()\nif err != nil {\n    // Handle non-struct type\n}",{"id":343,"title":344,"titles":345,"content":346,"level":19},"/v1.0.4/guides/troubleshooting#custom-tags-not-appearing","Custom Tags Not Appearing",[330],"Symptom: Your custom tag is missing from field.Tags. Cause: Tags were registered after the type was already extracted and cached. Solution: Register tags before the first Inspect or Scan call: // Do this first\nsentinel.Tag(\"custom\")\n\n// Then extract\nmetadata := sentinel.Inspect[User]() If the type is already cached, the original extraction (without your tag) will be returned. In tests, use sentineltest.ResetCache(t) to clear the cache.",{"id":348,"title":349,"titles":350,"content":351,"level":19},"/v1.0.4/guides/troubleshooting#types-not-discovered-by-scan","Types Not Discovered by Scan",[330],"Symptom: Related types are missing from the cache after Scan. Cause: The types are outside your module boundary. Solution: Scan only recurses into types within your module (determined by go.mod). Types from external packages are recorded as relationships but not extracted. // Your module: github.com/you/app\n\ntype User struct {\n    Profile  Profile       // ✓ Extracted (same module)\n    Created  time.Time     // ✗ Not extracted (stdlib)\n    NullName sql.NullString // ✗ Not extracted (external)\n} If you need metadata for external types, use Inspect explicitly: sentinel.Inspect[sql.NullString]()",{"id":353,"title":354,"titles":355,"content":356,"level":19},"/v1.0.4/guides/troubleshooting#scan-not-recursing","Scan Not Recursing",[330],"Symptom: Scan behaves like Inspect—only the root type is cached. Cause: Module path detection failed. This happens when debug.ReadBuildInfo() returns no data. When this occurs: Running with go run outside a moduleSome build configurations that strip debug info Solution: Ensure you're running within a Go module context, or use Inspect explicitly for each type you need.",{"id":358,"title":359,"titles":360,"content":361,"level":19},"/v1.0.4/guides/troubleshooting#unexported-fields-missing","Unexported Fields Missing",[330],"Symptom: Some struct fields don't appear in metadata.Fields. Cause: Only exported fields (uppercase names) are included. Go's reflection cannot access unexported fields from outside their package. type User struct {\n    ID   string // ✓ Included\n    name string // ✗ Not included (unexported)\n} Solution: This is expected behaviour. Export fields you need metadata for.",{"id":363,"title":364,"titles":365,"content":366,"level":19},"/v1.0.4/guides/troubleshooting#stale-cache-in-tests","Stale Cache in Tests",[330],"Symptom: Test assertions fail because metadata reflects a previous extraction. Cause: Sentinel's cache persists across tests. Solution: Use the testing helpers with -tags testing: func TestSomething(t *testing.T) {\n    sentineltest.ResetCache(t)\n    // Fresh extraction\n} Run with: go test -tags testing ./...",{"id":368,"title":32,"titles":369,"content":370,"level":19},"/v1.0.4/guides/troubleshooting#next-steps",[330],"Testing — test isolation patternsScanning — module boundary detailsAPI Reference — TryInspect and TryScan documentation html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}",{"id":372,"title":373,"titles":374,"content":375,"level":9},"/v1.0.4/integrations/erd","Entity Relationship Diagrams",[],"Entity relationship diagrams from Go types",{"id":377,"title":378,"titles":379,"content":43,"level":9},"/v1.0.4/integrations/erd#entity-relationship-diagrams-erd","Entity Relationship Diagrams (ERD)",[],{"id":381,"title":382,"titles":383,"content":384,"level":19},"/v1.0.4/integrations/erd#what-is-erd","What is erd?",[378],"erd generates Entity Relationship Diagrams from Go struct definitions. Entity Relationship Diagrams visualize how domain types connect—which entities exist, what attributes they have, and how they relate to each other. They're useful for documentation, onboarding, and validating that your mental model matches the code. erd outputs two formats: Mermaid — embeds in Markdown, renders in GitHub, documentation sitesGraphViz DOT — print-quality diagrams, PDF export, detailed layouts",{"id":386,"title":209,"titles":387,"content":388,"level":19},"/v1.0.4/integrations/erd#how-it-works",[378],"erd can build diagrams manually via a builder API, but the real power comes from automatic extraction. Point it at your domain model and it generates the diagram. Define types with optional erd tags for key constraints: type User struct {\n    ID      string   `erd:\"pk\"`\n    Email   string   `erd:\"uk\"`\n    Profile *Profile\n    Orders  []Order\n}\n\ntype Profile struct {\n    ID  string `erd:\"pk\"`\n    Bio *string\n}\n\ntype Order struct {\n    ID     string  `erd:\"pk,note:Auto-generated\"`\n    UserID string  `erd:\"fk\"`\n    Total  float64\n} Scan your domain and export the schema: sentinel.Scan[User]()\nschema := sentinel.Schema() Convert to a diagram: diagram := erd.FromSchema(\"Domain Model\", schema)\n\n// For markdown/web\nfmt.Println(diagram.ToMermaid())\n\n// For print/PDF\nfmt.Println(diagram.ToDOT()) erDiagram\n    User {\n        string ID PK\n        string Email UK\n    }\n    Profile {\n        string ID PK\n        string Bio \"nullable\"\n    }\n    Order {\n        string ID PK \"Auto-generated\"\n        string UserID FK\n        float64 Total\n    }\n    User ||--|| Profile : Profile\n    User ||--o{ Order : Orders Relationships and cardinality are inferred automatically—no manual wiring.",{"id":390,"title":391,"titles":392,"content":393,"level":19},"/v1.0.4/integrations/erd#what-sentinel-provides","What Sentinel Provides",[378],"erd needsSentinel providesEntity namesMetadata.TypeNameAttributesFieldMetadata.Name, FieldMetadata.TypeNullabilityFieldMetadata.Kind — pointers are nullableKey markersFieldMetadata.Tags[\"erd\"]RelationshipsTypeRelationship with From, To, KindCardinalityTypeRelationship.Kind maps to ERD notation",{"id":395,"title":396,"titles":397,"content":398,"level":19},"/v1.0.4/integrations/erd#tag-reference","Tag Reference",[378],"TagPurposeExamplepkPrimary keyerd:\"pk\"fkForeign keyerd:\"fk\"ukUnique keyerd:\"uk\"note:xAdd annotationerd:\"note:Auto-generated UUID\" Tags can be combined: erd:\"pk,note:Auto-generated\"",{"id":400,"title":401,"titles":402,"content":403,"level":19},"/v1.0.4/integrations/erd#cardinality-mapping","Cardinality Mapping",[378],"Sentinel's relationship kinds map directly to ERD notation: Relationship KindGo TypeDiagram Notationreference*Profile`collection[]Order`embeddingBaseModel`mapmap[string]Tag}o--o{ M:N",{"id":405,"title":406,"titles":407,"content":408,"level":19},"/v1.0.4/integrations/erd#learn-more","Learn More",[378],"erd repository html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}",{"id":410,"title":411,"titles":412,"content":413,"level":9},"/v1.0.4/integrations/soy","Type-Safe Queries",[],"SQL query building from Go types",{"id":415,"title":416,"titles":417,"content":43,"level":9},"/v1.0.4/integrations/soy#type-safe-queries-soy","Type-Safe Queries (soy)",[],{"id":419,"title":420,"titles":421,"content":422,"level":19},"/v1.0.4/integrations/soy#what-is-soy","What is soy?",[416],"soy is a type-safe SQL query builder. It validates queries against your schema at initialization—not execution—so invalid field names, operators, or constraints fail immediately. Queries return *T or []*T, never interface{}.",{"id":424,"title":425,"titles":426,"content":427,"level":19},"/v1.0.4/integrations/soy#the-problem","The Problem",[416],"Traditional query builders accept arbitrary strings: // Field name comes from user input — SQL injection vector\ndb.Where(userInput + \" = ?\", value) User input can inject query structure, not just values. Even with parameterized values, the field name itself is a vector.",{"id":429,"title":430,"titles":431,"content":432,"level":19},"/v1.0.4/integrations/soy#how-sentinel-enables-this","How Sentinel Enables This",[416],"Sentinel extracts your schema once at initialization. soy converts that metadata into a validation layer—every query is checked against the struct definition before it runs. Define types with database tags: type User struct {\n    ID    int64  `db:\"id\" constraints:\"primary_key\"`\n    Email string `db:\"email\" constraints:\"unique,not_null\"`\n    Name  string `db:\"name\"`\n} Initialize soy with your type: users, err := soy.New[User](db, \"users\", postgres.New()) Now, sentinel extracts the metadata: // Internally, soy calls:\nmetadata := sentinel.Inspect[User]()\n\n// metadata.Fields contains:\n// - {Name: \"ID\", Tags: {\"db\": \"id\", \"constraints\": \"primary_key\"}}\n// - {Name: \"Email\", Tags: {\"db\": \"email\", \"constraints\": \"unique,not_null\"}}\n// - {Name: \"Name\", Tags: {\"db\": \"name\"}} This metadata becomes the allowlist. Queries validate against it: // Valid: \"email\" exists in schema\nusers.Select().Where(\"email\", \"=\", \"email_param\").Exec(ctx, params)\n\n// Rejected at initialization: \"emai\" doesn't exist\nusers.Select().Where(\"emai\", \"=\", \"email_param\")  // Error: unknown field The struct definition fixes query shape. User input provides values only.",{"id":434,"title":391,"titles":435,"content":436,"level":19},"/v1.0.4/integrations/soy#what-sentinel-provides",[416],"soy needsSentinel providesValid column namesFieldMetadata.Tags[\"db\"]Column typesFieldMetadata.TypePrimary keysTags[\"constraints\"] parsingUnique constraintsTags[\"constraints\"] parsingForeign keysTags[\"references\"]Check constraintsTags[\"check\"]Default valuesTags[\"default\"] Sentinel's one-time extraction means zero reflection on the query path—metadata is cached permanently.",{"id":438,"title":396,"titles":439,"content":440,"level":19},"/v1.0.4/integrations/soy#tag-reference",[416],"TagPurposeExampledbColumn namedb:\"user_id\"typeSQL type overridetype:\"bigserial\"constraintsColumn constraintsconstraints:\"primary_key\"referencesForeign keyreferences:\"users(id)\"defaultDefault valuedefault:\"now()\"checkCheck constraintcheck:\"status IN ('a','b')\"indexIndex nameindex:\"idx_email\"",{"id":442,"title":406,"titles":443,"content":444,"level":19},"/v1.0.4/integrations/soy#learn-more",[416],"soy repositorydbml repository — schema representation html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":446,"title":447,"titles":448,"content":449,"level":9},"/v1.0.4/integrations/rocco","OpenAPI Generation",[],"OpenAPI generation from Go types",{"id":451,"title":452,"titles":453,"content":43,"level":9},"/v1.0.4/integrations/rocco#openapi-generation-rocco","OpenAPI Generation (rocco)",[],{"id":455,"title":456,"titles":457,"content":458,"level":19},"/v1.0.4/integrations/rocco#what-is-rocco","What is rocco?",[452],"rocco is a type-safe HTTP framework. Handlers are generic—Handler[In, Out]—so request and response types are known at compile time. From these types, rocco automatically generates OpenAPI 3.1.0 specifications. The spec is served at /openapi, with interactive documentation at /docs.",{"id":460,"title":425,"titles":461,"content":462,"level":19},"/v1.0.4/integrations/rocco#the-problem",[452],"API documentation drifts. You update a handler, forget to update the spec, and clients break. Manual spec maintenance doesn't scale.",{"id":464,"title":430,"titles":465,"content":466,"level":19},"/v1.0.4/integrations/rocco#how-sentinel-enables-this",[452],"When you create a handler, rocco calls sentinel.Scan on both input and output types: handler := rocco.NewHandler[CreateUserRequest, User](\n    \"create-user\", \"POST\", \"/users\",\n    func(req *rocco.Request[CreateUserRequest]) (User, error) {\n        // ...\n    },\n) Internally, rocco extracts metadata for schema generation: // rocco calls this at handler creation:\nsentinel.Scan[CreateUserRequest]()\nsentinel.Scan[User]() Scan (not Inspect) is key—it recursively discovers all related types within your module. If User has a *Profile field, sentinel finds Profile too. Define types with validation and documentation tags: type CreateUserRequest struct {\n    Name  string `json:\"name\" validate:\"required,min=3,max=100\" description:\"User's full name\"`\n    Email string `json:\"email\" validate:\"required,email\" description:\"Contact email\"`\n    Age   int    `json:\"age,omitempty\" validate:\"gte=0,lte=150\"`\n} Sentinel extracts the metadata: // metadata.Fields contains:\n// - {Name: \"Name\", Tags: {\"json\": \"name\", \"validate\": \"required,min=3,max=100\", \"description\": \"User's full name\"}}\n// - {Name: \"Email\", Tags: {\"json\": \"email\", \"validate\": \"required,email\", \"description\": \"Contact email\"}}\n// - {Name: \"Age\", Tags: {\"json\": \"age,omitempty\", \"validate\": \"gte=0,lte=150\"}} rocco transforms this to OpenAPI schemas: {\n  \"type\": \"object\",\n  \"required\": [\"name\", \"email\"],\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"minLength\": 3,\n      \"maxLength\": 100,\n      \"description\": \"User's full name\"\n    },\n    \"email\": {\n      \"type\": \"string\",\n      \"format\": \"email\",\n      \"description\": \"Contact email\"\n    },\n    \"age\": {\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 150\n    }\n  }\n} Change the struct, the docs update.",{"id":468,"title":391,"titles":469,"content":470,"level":19},"/v1.0.4/integrations/rocco#what-sentinel-provides",[452],"rocco needsSentinel providesProperty namesFieldMetadata.Tags[\"json\"]Property typesFieldMetadata.Type → OpenAPI type mappingRequired fieldsTags[\"validate\"] containing requiredConstraintsTags[\"validate\"] → OpenAPI constraintsDescriptionsTags[\"description\"]ExamplesTags[\"example\"]Nested schemasMetadata.Relationships for $ref generation Sentinel's recursive scanning means rocco discovers your entire API surface from handler types alone.",{"id":472,"title":473,"titles":474,"content":475,"level":19},"/v1.0.4/integrations/rocco#tag-mappings","Tag Mappings",[452],"Validate TagOpenAPIrequiredfield in required arraymin=3minLength (strings) or minimum (numbers)max=100maxLength or maximumgt=0exclusiveMinimumlt=100exclusiveMaximumemailformat: \"email\"urlformat: \"uri\"uuidformat: \"uuid\"datetimeformat: \"date-time\"oneof=a b cenum: [\"a\", \"b\", \"c\"]uniqueuniqueItems: true (arrays)",{"id":477,"title":478,"titles":479,"content":480,"level":19},"/v1.0.4/integrations/rocco#nested-types","Nested Types",[452],"Sentinel's relationship discovery handles complex types: type Order struct {\n    ID       string    `json:\"id\"`\n    Customer *Customer `json:\"customer\"`\n    Items    []Item    `json:\"items\"`\n} When Order is scanned, sentinel discovers Customer and Item via Metadata.Relationships. rocco uses sentinel.Lookup() to retrieve each type's metadata and includes all three in components.schemas with proper $ref links.",{"id":482,"title":406,"titles":483,"content":484,"level":19},"/v1.0.4/integrations/rocco#learn-more",[452],"rocco repositoryopenapi repository — OpenAPI type definitions html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":486,"title":487,"titles":488,"content":489,"level":9},"/v1.0.4/reference/api","API Reference",[],"Complete function documentation for sentinel",{"id":491,"title":487,"titles":492,"content":43,"level":9},"/v1.0.4/reference/api#api-reference",[],{"id":494,"title":495,"titles":496,"content":43,"level":19},"/v1.0.4/reference/api#errors","Errors",[487],{"id":498,"title":499,"titles":500,"content":501,"level":134},"/v1.0.4/reference/api#errnotstruct","ErrNotStruct",[487,495],"var ErrNotStruct = errors.New(\"sentinel: only struct types are supported\") Returned by TryInspect and TryScan when the type parameter is not a struct type.",{"id":503,"title":504,"titles":505,"content":43,"level":19},"/v1.0.4/reference/api#core-functions","Core Functions",[487],{"id":507,"title":194,"titles":508,"content":509,"level":134},"/v1.0.4/reference/api#inspect",[487,504],"func Inspect[T any]() Metadata Extracts metadata for a single type. Results are cached permanently. Panics if T is not a struct type. Pointer-to-struct types are automatically dereferenced. Inspect[*User]() is equivalent to Inspect[User](). metadata := sentinel.Inspect[User]()",{"id":511,"title":512,"titles":513,"content":514,"level":134},"/v1.0.4/reference/api#tryinspect","TryInspect",[487,504],"func TryInspect[T any]() (Metadata, error) Like Inspect, but returns an error instead of panicking if T is not a struct type. metadata, err := sentinel.TryInspect[MyType]()\nif err != nil {\n    // Handle non-struct type\n}",{"id":516,"title":199,"titles":517,"content":518,"level":134},"/v1.0.4/reference/api#scan",[487,504],"func Scan[T any]() Metadata Recursively extracts metadata for a type and all related types within the same module. Panics if T is not a struct type. Pointer-to-struct types are automatically dereferenced. Scan[*User]() is equivalent to Scan[User](). metadata := sentinel.Scan[User]()\n// User and all related types (Profile, Order, etc.) are now cached",{"id":520,"title":521,"titles":522,"content":523,"level":134},"/v1.0.4/reference/api#tryscan","TryScan",[487,504],"func TryScan[T any]() (Metadata, error) Like Scan, but returns an error instead of panicking if T is not a struct type. metadata, err := sentinel.TryScan[User]()\nif err != nil {\n    // Handle non-struct type\n}",{"id":525,"title":526,"titles":527,"content":528,"level":134},"/v1.0.4/reference/api#tag","Tag",[487,504],"func Tag(tagName string) Registers a custom struct tag for extraction. Call before Inspect or Scan. sentinel.Tag(\"graphql\")\nsentinel.Tag(\"proto\")",{"id":530,"title":531,"titles":532,"content":533,"level":134},"/v1.0.4/reference/api#browse","Browse",[487,504],"func Browse() []string Returns all cached type FQDNs. fqdns := sentinel.Browse()\n// [\"github.com/you/app/models.User\", \"github.com/you/app/models.Profile\"]",{"id":535,"title":536,"titles":537,"content":538,"level":134},"/v1.0.4/reference/api#lookup","Lookup",[487,504],"func Lookup(fqdn string) (Metadata, bool) Retrieves cached metadata by FQDN. meta, ok := sentinel.Lookup(\"github.com/you/app/models.User\")\nif ok {\n    // Use meta\n}\n\n// Or use the FQDN from previously inspected metadata\nuserMeta := sentinel.Inspect[User]()\nmeta, ok = sentinel.Lookup(userMeta.FQDN)",{"id":540,"title":541,"titles":542,"content":543,"level":134},"/v1.0.4/reference/api#schema","Schema",[487,504],"func Schema() map[string]Metadata Returns all cached metadata as a map, keyed by FQDN. schema := sentinel.Schema()\nfor fqdn, meta := range schema {\n    fmt.Printf(\"%s: %d fields\\n\", fqdn, len(meta.Fields))\n}",{"id":545,"title":546,"titles":547,"content":43,"level":19},"/v1.0.4/reference/api#relationship-functions","Relationship Functions",[487],{"id":549,"title":550,"titles":551,"content":552,"level":134},"/v1.0.4/reference/api#getrelationships","GetRelationships",[487,546],"func GetRelationships[T any]() []TypeRelationship Returns all types that T references. rels := sentinel.GetRelationships[User]()\n// [{From: \"github.com/.../models.User\", To: \"github.com/.../models.Profile\", Kind: \"reference\", ...}]",{"id":554,"title":555,"titles":556,"content":557,"level":134},"/v1.0.4/reference/api#getreferencedby","GetReferencedBy",[487,546],"func GetReferencedBy[T any]() []TypeRelationship Returns all types that reference T. Requires prior caching of those types. refs := sentinel.GetReferencedBy[Profile]()\n// [{From: \"github.com/.../models.User\", To: \"github.com/.../models.Profile\", Kind: \"reference\", ...}]",{"id":559,"title":560,"titles":561,"content":562,"level":19},"/v1.0.4/reference/api#types","Types",[487],"See Types Reference for complete type documentation: Metadata — complete metadata for a struct typeFieldMetadata — metadata for a single fieldFieldKind — field type categorizationTypeRelationship — relationship between typesRelationship Kinds — reference, collection, embedding, map html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":564,"title":565,"titles":566,"content":567,"level":9},"/v1.0.4/reference/types","Type Reference",[],"Complete type documentation for sentinel",{"id":569,"title":565,"titles":570,"content":43,"level":9},"/v1.0.4/reference/types#type-reference",[],{"id":572,"title":84,"titles":573,"content":574,"level":19},"/v1.0.4/reference/types#metadata",[565],"Complete metadata for a struct type. type Metadata struct {\n    ReflectType   reflect.Type       `json:\"-\"`\n    FQDN          string             `json:\"fqdn\"`\n    TypeName      string             `json:\"type_name\"`\n    PackageName   string             `json:\"package_name\"`\n    Fields        []FieldMetadata    `json:\"fields\"`\n    Relationships []TypeRelationship `json:\"relationships,omitempty\"`\n} FieldTypeDescriptionReflectTypereflect.TypeActual reflect.Type (excluded from JSON)FQDNstringFully qualified type name (e.g., \"github.com/you/app/models.User\")TypeNamestringShort type name (e.g., \"User\")PackageNamestringFull package path (e.g., \"github.com/you/app/models\")Fields[]FieldMetadataAll exported fieldsRelationships[]TypeRelationshipReferences to other struct types",{"id":576,"title":577,"titles":578,"content":579,"level":19},"/v1.0.4/reference/types#fieldmetadata","FieldMetadata",[565],"Metadata for a single struct field. type FieldMetadata struct {\n    ReflectType reflect.Type      `json:\"-\"`\n    Tags        map[string]string `json:\"tags,omitempty\"`\n    Name        string            `json:\"name\"`\n    Type        string            `json:\"type\"`\n    Kind        FieldKind         `json:\"kind\"`\n    Index       []int             `json:\"index\"`\n} FieldTypeDescriptionIndex[]intField index path for reflect.Value.FieldByIndex()NamestringField name (e.g., \"Email\")TypestringGo type as string (e.g., \"string\", \"*Profile\", \"[]Order\")KindFieldKindType category (see below)ReflectTypereflect.TypeActual reflect.Type for programmatic use (excluded from JSON)Tagsmap[string]stringAll extracted struct tags",{"id":581,"title":582,"titles":583,"content":584,"level":134},"/v1.0.4/reference/types#fieldkind","FieldKind",[565,577],"Type categorization for fields. type FieldKind string\n\nconst (\n    KindScalar    FieldKind = \"scalar\"    // string, int, float, bool, etc.\n    KindPointer   FieldKind = \"pointer\"   // *T\n    KindSlice     FieldKind = \"slice\"     // []T, [N]T\n    KindStruct    FieldKind = \"struct\"    // struct types\n    KindMap       FieldKind = \"map\"       // map[K]V\n    KindInterface FieldKind = \"interface\" // interface{}\n)",{"id":586,"title":152,"titles":587,"content":588,"level":134},"/v1.0.4/reference/types#tag-extraction",[565,577],"Only registered tags are extracted. Built-in tags: json, db, validate, scope, encrypt, redact, desc, example Register custom tags with sentinel.Tag(name).",{"id":590,"title":591,"titles":592,"content":593,"level":19},"/v1.0.4/reference/types#typerelationship","TypeRelationship",[565],"A relationship between two struct types. type TypeRelationship struct {\n    From      string `json:\"from\"`\n    To        string `json:\"to\"`\n    Field     string `json:\"field\"`\n    Kind      string `json:\"kind\"`\n    ToPackage string `json:\"to_package\"`\n} FieldTypeDescriptionFromstringSource type FQDN (e.g., \"github.com/you/app/models.User\")TostringTarget type FQDN (e.g., \"github.com/you/app/models.Profile\")FieldstringField that creates the relationshipKindstringRelationship kind (see below)ToPackagestringTarget type's full package path",{"id":595,"title":219,"titles":596,"content":597,"level":134},"/v1.0.4/reference/types#relationship-kinds",[565,591],"const (\n    RelationshipReference  = \"reference\"  // *Profile, Profile\n    RelationshipCollection = \"collection\" // []Order, [5]Order\n    RelationshipEmbedding  = \"embedding\"  // Anonymous embedded struct\n    RelationshipMap        = \"map\"        // map[string]Item\n)",{"id":599,"title":600,"titles":601,"content":602,"level":19},"/v1.0.4/reference/types#json-serialization","JSON Serialization",[565],"All types have JSON tags for easy serialization: schema := sentinel.Schema()\ndata, _ := json.MarshalIndent(schema, \"\", \"  \")\nfmt.Println(string(data)) Output: {\n  \"github.com/you/app/models.User\": {\n    \"fqdn\": \"github.com/you/app/models.User\",\n    \"type_name\": \"User\",\n    \"package_name\": \"github.com/you/app/models\",\n    \"fields\": [\n      {\n        \"index\": [0],\n        \"name\": \"ID\",\n        \"type\": \"string\",\n        \"kind\": \"scalar\",\n        \"tags\": {\n          \"json\": \"id\",\n          \"db\": \"user_id\"\n        }\n      },\n      {\n        \"index\": [1],\n        \"name\": \"Profile\",\n        \"type\": \"*Profile\",\n        \"kind\": \"pointer\"\n      }\n    ],\n    \"relationships\": [\n      {\n        \"from\": \"github.com/you/app/models.User\",\n        \"to\": \"github.com/you/app/models.Profile\",\n        \"field\": \"Profile\",\n        \"kind\": \"reference\",\n        \"to_package\": \"github.com/you/app/models\"\n      }\n    ]\n  }\n} ReflectType is excluded from JSON serialization but available for programmatic use. html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sfm-E, html code.shiki .sfm-E{--shiki-default:var(--shiki-variable)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",[604],{"title":605,"path":606,"stem":607,"children":608,"page":622},"V104","/v1.0.4","v1.0.4",[609,623,636,647],{"title":610,"path":611,"stem":612,"children":613,"page":622},"Learn","/v1.0.4/learn","v1.0.4/1.learn",[614,616,618,620],{"title":6,"path":5,"stem":615,"description":8},"v1.0.4/1.learn/1.overview",{"title":37,"path":36,"stem":617,"description":39},"v1.0.4/1.learn/2.quickstart",{"title":75,"path":74,"stem":619,"description":77},"v1.0.4/1.learn/3.concepts",{"title":113,"path":112,"stem":621,"description":115},"v1.0.4/1.learn/4.architecture",false,{"title":624,"path":625,"stem":626,"children":627,"page":622},"Guides","/v1.0.4/guides","v1.0.4/2.guides",[628,630,632,634],{"title":56,"path":180,"stem":629,"description":182},"v1.0.4/2.guides/1.scanning",{"title":61,"path":237,"stem":631,"description":239},"v1.0.4/2.guides/2.tags",{"title":288,"path":287,"stem":633,"description":290},"v1.0.4/2.guides/3.testing",{"title":330,"path":329,"stem":635,"description":332},"v1.0.4/2.guides/4.troubleshooting",{"title":637,"path":638,"stem":639,"children":640,"page":622},"Integrations","/v1.0.4/integrations","v1.0.4/3.integrations",[641,643,645],{"title":373,"path":372,"stem":642,"description":375},"v1.0.4/3.integrations/1.erd",{"title":411,"path":410,"stem":644,"description":413},"v1.0.4/3.integrations/2.soy",{"title":447,"path":446,"stem":646,"description":449},"v1.0.4/3.integrations/3.rocco",{"title":648,"path":649,"stem":650,"children":651,"page":622},"Reference","/v1.0.4/reference","v1.0.4/4.reference",[652,654],{"title":487,"path":486,"stem":653,"description":489},"v1.0.4/4.reference/1.api",{"title":565,"path":564,"stem":655,"description":567},"v1.0.4/4.reference/2.types",[657],{"title":605,"path":606,"stem":607,"children":658,"page":622},[659,665,671,676],{"title":610,"path":611,"stem":612,"children":660,"page":622},[661,662,663,664],{"title":6,"path":5,"stem":615},{"title":37,"path":36,"stem":617},{"title":75,"path":74,"stem":619},{"title":113,"path":112,"stem":621},{"title":624,"path":625,"stem":626,"children":666,"page":622},[667,668,669,670],{"title":56,"path":180,"stem":629},{"title":61,"path":237,"stem":631},{"title":288,"path":287,"stem":633},{"title":330,"path":329,"stem":635},{"title":637,"path":638,"stem":639,"children":672,"page":622},[673,674,675],{"title":373,"path":372,"stem":642},{"title":411,"path":410,"stem":644},{"title":447,"path":446,"stem":646},{"title":648,"path":649,"stem":650,"children":677,"page":622},[678,679],{"title":487,"path":486,"stem":653},{"title":565,"path":564,"stem":655},[681,2059,2225],{"id":682,"title":683,"body":684,"description":43,"extension":2052,"icon":2053,"meta":2054,"navigation":858,"path":2055,"seo":2056,"stem":2057,"__hash__":2058},"resources/readme.md","README",{"type":685,"value":686,"toc":2041},"minimark",[687,691,759,762,765,770,973,987,1106,1109,1113,1130,1133,1137,1718,1722,1825,1829,1877,1881,1887,1890,1910,1914,2016,2020,2028,2031,2037],[688,689,690],"h1",{"id":690},"sentinel",[692,693,694,705,713,721,729,737,744,751],"p",{},[695,696,700],"a",{"href":697,"rel":698},"https://github.com/zoobz-io/sentinel/actions/workflows/ci.yml",[699],"nofollow",[701,702],"img",{"alt":703,"src":704},"CI Status","https://github.com/zoobz-io/sentinel/workflows/CI/badge.svg",[695,706,709],{"href":707,"rel":708},"https://codecov.io/gh/zoobz-io/sentinel",[699],[701,710],{"alt":711,"src":712},"codecov","https://codecov.io/gh/zoobz-io/sentinel/graph/badge.svg?branch=main",[695,714,717],{"href":715,"rel":716},"https://goreportcard.com/report/github.com/zoobz-io/sentinel",[699],[701,718],{"alt":719,"src":720},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/sentinel",[695,722,725],{"href":723,"rel":724},"https://github.com/zoobz-io/sentinel/security/code-scanning",[699],[701,726],{"alt":727,"src":728},"CodeQL","https://github.com/zoobz-io/sentinel/workflows/CodeQL/badge.svg",[695,730,733],{"href":731,"rel":732},"https://pkg.go.dev/github.com/zoobz-io/sentinel",[699],[701,734],{"alt":735,"src":736},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/sentinel.svg",[695,738,740],{"href":739},"LICENSE",[701,741],{"alt":742,"src":743},"License","https://img.shields.io/github/license/zoobz-io/sentinel",[695,745,747],{"href":746},"go.mod",[701,748],{"alt":749,"src":750},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/sentinel",[695,752,755],{"href":753,"rel":754},"https://github.com/zoobz-io/sentinel/releases",[699],[701,756],{"alt":757,"src":758},"Release","https://img.shields.io/github/v/release/zoobz-io/sentinel",[692,760,761],{},"Zero-dependency struct introspection for Go.",[692,763,764],{},"Extract struct metadata once, cache it permanently, and discover relationships between types.",[766,767,769],"h2",{"id":768},"structured-type-intelligence","Structured Type Intelligence",[771,772,776],"pre",{"className":773,"code":774,"language":775,"meta":43,"style":43},"language-go shiki shiki-themes","type User struct {\n    ID      string   `json:\"id\" db:\"id\" validate:\"required\"`\n    Email   string   `json:\"email\" validate:\"required,email\"`\n    Profile *Profile\n    Orders  []Order\n}\n\nmetadata := sentinel.Scan[User]()\n// metadata.TypeName → \"User\"\n// metadata.FQDN     → \"github.com/app/models.User\"\n// metadata.Fields   → []FieldMetadata (4 fields)\n// metadata.Relationships → []TypeRelationship (2 relationships)\n\nfield := metadata.Fields[0]\n// field.Name  → \"ID\"\n// field.Type  → \"string\"\n// field.Kind  → \"scalar\"\n// field.Tags  → {\"json\": \"id\", \"db\": \"id\", \"validate\": \"required\"}\n// field.Index → []int{0}\n","go",[777,778,779,798,811,822,835,847,853,860,888,895,901,907,913,918,943,949,955,961,967],"code",{"__ignoreMap":43},[780,781,783,787,791,794],"span",{"class":782,"line":9},"line",[780,784,786],{"class":785},"sUt3r","type",[780,788,790],{"class":789},"sYBwO"," User",[780,792,793],{"class":785}," struct",[780,795,797],{"class":796},"sq5bi"," {\n",[780,799,800,804,807],{"class":782,"line":19},[780,801,803],{"class":802},"sBGCq","    ID",[780,805,806],{"class":789},"      string",[780,808,810],{"class":809},"sxAnc","   `json:\"id\" db:\"id\" validate:\"required\"`\n",[780,812,813,816,819],{"class":782,"line":134},[780,814,815],{"class":802},"    Email",[780,817,818],{"class":789},"   string",[780,820,821],{"class":809},"   `json:\"email\" validate:\"required,email\"`\n",[780,823,825,828,832],{"class":782,"line":824},4,[780,826,827],{"class":802},"    Profile",[780,829,831],{"class":830},"sW3Qg"," *",[780,833,834],{"class":789},"Profile\n",[780,836,838,841,844],{"class":782,"line":837},5,[780,839,840],{"class":802},"    Orders",[780,842,843],{"class":796},"  []",[780,845,846],{"class":789},"Order\n",[780,848,850],{"class":782,"line":849},6,[780,851,852],{"class":796},"}\n",[780,854,856],{"class":782,"line":855},7,[780,857,859],{"emptyLinePlaceholder":858},true,"\n",[780,861,863,867,870,873,876,879,882,885],{"class":782,"line":862},8,[780,864,866],{"class":865},"sh8_p","metadata",[780,868,869],{"class":865}," :=",[780,871,872],{"class":865}," sentinel",[780,874,875],{"class":796},".",[780,877,199],{"class":878},"s5klm",[780,880,881],{"class":796},"[",[780,883,884],{"class":789},"User",[780,886,887],{"class":796},"]()\n",[780,889,891],{"class":782,"line":890},9,[780,892,894],{"class":893},"sLkEo","// metadata.TypeName → \"User\"\n",[780,896,898],{"class":782,"line":897},10,[780,899,900],{"class":893},"// metadata.FQDN     → \"github.com/app/models.User\"\n",[780,902,904],{"class":782,"line":903},11,[780,905,906],{"class":893},"// metadata.Fields   → []FieldMetadata (4 fields)\n",[780,908,910],{"class":782,"line":909},12,[780,911,912],{"class":893},"// metadata.Relationships → []TypeRelationship (2 relationships)\n",[780,914,916],{"class":782,"line":915},13,[780,917,859],{"emptyLinePlaceholder":858},[780,919,921,924,926,929,931,934,936,940],{"class":782,"line":920},14,[780,922,923],{"class":865},"field",[780,925,869],{"class":865},[780,927,928],{"class":865}," metadata",[780,930,875],{"class":796},[780,932,933],{"class":865},"Fields",[780,935,881],{"class":796},[780,937,939],{"class":938},"sMAmT","0",[780,941,942],{"class":796},"]\n",[780,944,946],{"class":782,"line":945},15,[780,947,948],{"class":893},"// field.Name  → \"ID\"\n",[780,950,952],{"class":782,"line":951},16,[780,953,954],{"class":893},"// field.Type  → \"string\"\n",[780,956,958],{"class":782,"line":957},17,[780,959,960],{"class":893},"// field.Kind  → \"scalar\"\n",[780,962,964],{"class":782,"line":963},18,[780,965,966],{"class":893},"// field.Tags  → {\"json\": \"id\", \"db\": \"id\", \"validate\": \"required\"}\n",[780,968,970],{"class":782,"line":969},19,[780,971,972],{"class":893},"// field.Index → []int{0}\n",[692,974,975,976,978,979,982,983,986],{},"One call extracts metadata for ",[777,977,884],{}," and every type it touches — ",[777,980,981],{},"Profile",", ",[777,984,985],{},"Order",", and anything they reference. All cached permanently.",[771,988,990],{"className":773,"code":989,"language":775,"meta":43,"style":43},"types := sentinel.Browse()\n// [\n//   \"github.com/app/models.User\",\n//   \"github.com/app/models.Profile\",\n//   \"github.com/app/models.Order\",\n// ]\n\nrelationships := sentinel.GetRelationships[User]()\n// []TypeRelationship (2 relationships)\n\nrel := relationships[0]\n// rel.From      → \"github.com/app/models.User\"\n// rel.To        → \"github.com/app/models.Profile\"\n// rel.Field     → \"Profile\"\n// rel.Kind      → \"reference\"\n// rel.ToPackage → \"github.com/app/models\"\n",[777,991,992,1008,1013,1018,1023,1028,1033,1037,1056,1061,1065,1081,1086,1091,1096,1101],{"__ignoreMap":43},[780,993,994,997,999,1001,1003,1005],{"class":782,"line":9},[780,995,996],{"class":865},"types",[780,998,869],{"class":865},[780,1000,872],{"class":865},[780,1002,875],{"class":796},[780,1004,531],{"class":878},[780,1006,1007],{"class":796},"()\n",[780,1009,1010],{"class":782,"line":19},[780,1011,1012],{"class":893},"// [\n",[780,1014,1015],{"class":782,"line":134},[780,1016,1017],{"class":893},"//   \"github.com/app/models.User\",\n",[780,1019,1020],{"class":782,"line":824},[780,1021,1022],{"class":893},"//   \"github.com/app/models.Profile\",\n",[780,1024,1025],{"class":782,"line":837},[780,1026,1027],{"class":893},"//   \"github.com/app/models.Order\",\n",[780,1029,1030],{"class":782,"line":849},[780,1031,1032],{"class":893},"// ]\n",[780,1034,1035],{"class":782,"line":855},[780,1036,859],{"emptyLinePlaceholder":858},[780,1038,1039,1042,1044,1046,1048,1050,1052,1054],{"class":782,"line":862},[780,1040,1041],{"class":865},"relationships",[780,1043,869],{"class":865},[780,1045,872],{"class":865},[780,1047,875],{"class":796},[780,1049,550],{"class":878},[780,1051,881],{"class":796},[780,1053,884],{"class":789},[780,1055,887],{"class":796},[780,1057,1058],{"class":782,"line":890},[780,1059,1060],{"class":893},"// []TypeRelationship (2 relationships)\n",[780,1062,1063],{"class":782,"line":897},[780,1064,859],{"emptyLinePlaceholder":858},[780,1066,1067,1070,1072,1075,1077,1079],{"class":782,"line":903},[780,1068,1069],{"class":865},"rel",[780,1071,869],{"class":865},[780,1073,1074],{"class":865}," relationships",[780,1076,881],{"class":796},[780,1078,939],{"class":938},[780,1080,942],{"class":796},[780,1082,1083],{"class":782,"line":909},[780,1084,1085],{"class":893},"// rel.From      → \"github.com/app/models.User\"\n",[780,1087,1088],{"class":782,"line":915},[780,1089,1090],{"class":893},"// rel.To        → \"github.com/app/models.Profile\"\n",[780,1092,1093],{"class":782,"line":920},[780,1094,1095],{"class":893},"// rel.Field     → \"Profile\"\n",[780,1097,1098],{"class":782,"line":945},[780,1099,1100],{"class":893},"// rel.Kind      → \"reference\"\n",[780,1102,1103],{"class":782,"line":951},[780,1104,1105],{"class":893},"// rel.ToPackage → \"github.com/app/models\"\n",[692,1107,1108],{},"Types don't change at runtime. Neither does their metadata.",[766,1110,1112],{"id":1111},"install","Install",[771,1114,1118],{"className":1115,"code":1116,"language":1117,"meta":43,"style":43},"language-bash shiki shiki-themes","go get github.com/zoobz-io/sentinel@latest\n","bash",[777,1119,1120],{"__ignoreMap":43},[780,1121,1122,1124,1127],{"class":782,"line":9},[780,1123,775],{"class":878},[780,1125,1126],{"class":809}," get",[780,1128,1129],{"class":809}," github.com/zoobz-io/sentinel@latest\n",[692,1131,1132],{},"Requires Go 1.24+.",[766,1134,1136],{"id":1135},"quick-start","Quick Start",[771,1138,1140],{"className":773,"code":1139,"language":775,"meta":43,"style":43},"package main\n\nimport (\n    \"fmt\"\n    \"github.com/zoobz-io/sentinel\"\n)\n\ntype Order struct {\n    ID     string  `json:\"id\" db:\"order_id\" validate:\"required\"`\n    Total  float64 `json:\"total\" validate:\"gte=0\"`\n    Status string  `json:\"status\"`\n}\n\ntype User struct {\n    ID     string  `json:\"id\" db:\"user_id\"`\n    Email  string  `json:\"email\" validate:\"required,email\"`\n    Orders []Order\n}\n\nfunc main() {\n    // Scan extracts User, Order, and their relationship\n    metadata := sentinel.Scan[User]()\n\n    // Type information\n    fmt.Println(metadata.TypeName) // \"User\"\n    fmt.Println(metadata.FQDN)     // \"main.User\" (reflects actual package path)\n\n    // Field metadata\n    for _, field := range metadata.Fields {\n        fmt.Printf(\"%s (%s): %v\\n\", field.Name, field.Kind, field.Tags)\n    }\n    // ID (scalar): map[json:id db:user_id]\n    // Email (scalar): map[json:email validate:required,email]\n    // Orders (slice): map[]\n\n    // Relationships\n    for _, rel := range metadata.Relationships {\n        fmt.Printf(\"%s → %s (%s)\\n\", metadata.TypeName, rel.To, rel.Kind)\n    }\n    // User → main.Order (collection)\n\n    // Everything is cached\n    fmt.Println(sentinel.Browse())\n    // [main.User main.Order]\n\n    // Export the full schema\n    schema := sentinel.Schema()\n    fmt.Printf(\"%d types cached\\n\", len(schema))\n}\n",[777,1141,1142,1150,1154,1163,1168,1173,1178,1182,1193,1203,1214,1225,1229,1233,1243,1252,1262,1271,1275,1279,1293,1299,1319,1324,1330,1357,1380,1385,1391,1419,1485,1491,1497,1503,1509,1514,1520,1544,1601,1606,1612,1617,1623,1643,1649,1654,1660,1676,1713],{"__ignoreMap":43},[780,1143,1144,1147],{"class":782,"line":9},[780,1145,1146],{"class":785},"package",[780,1148,1149],{"class":789}," main\n",[780,1151,1152],{"class":782,"line":19},[780,1153,859],{"emptyLinePlaceholder":858},[780,1155,1156,1159],{"class":782,"line":134},[780,1157,1158],{"class":785},"import",[780,1160,1162],{"class":1161},"soy-K"," (\n",[780,1164,1165],{"class":782,"line":824},[780,1166,1167],{"class":809},"    \"fmt\"\n",[780,1169,1170],{"class":782,"line":837},[780,1171,1172],{"class":809},"    \"github.com/zoobz-io/sentinel\"\n",[780,1174,1175],{"class":782,"line":849},[780,1176,1177],{"class":1161},")\n",[780,1179,1180],{"class":782,"line":855},[780,1181,859],{"emptyLinePlaceholder":858},[780,1183,1184,1186,1189,1191],{"class":782,"line":862},[780,1185,786],{"class":785},[780,1187,1188],{"class":789}," Order",[780,1190,793],{"class":785},[780,1192,797],{"class":796},[780,1194,1195,1197,1200],{"class":782,"line":890},[780,1196,803],{"class":802},[780,1198,1199],{"class":789},"     string",[780,1201,1202],{"class":809},"  `json:\"id\" db:\"order_id\" validate:\"required\"`\n",[780,1204,1205,1208,1211],{"class":782,"line":897},[780,1206,1207],{"class":802},"    Total",[780,1209,1210],{"class":789},"  float64",[780,1212,1213],{"class":809}," `json:\"total\" validate:\"gte=0\"`\n",[780,1215,1216,1219,1222],{"class":782,"line":903},[780,1217,1218],{"class":802},"    Status",[780,1220,1221],{"class":789}," string",[780,1223,1224],{"class":809},"  `json:\"status\"`\n",[780,1226,1227],{"class":782,"line":909},[780,1228,852],{"class":796},[780,1230,1231],{"class":782,"line":915},[780,1232,859],{"emptyLinePlaceholder":858},[780,1234,1235,1237,1239,1241],{"class":782,"line":920},[780,1236,786],{"class":785},[780,1238,790],{"class":789},[780,1240,793],{"class":785},[780,1242,797],{"class":796},[780,1244,1245,1247,1249],{"class":782,"line":945},[780,1246,803],{"class":802},[780,1248,1199],{"class":789},[780,1250,1251],{"class":809},"  `json:\"id\" db:\"user_id\"`\n",[780,1253,1254,1256,1259],{"class":782,"line":951},[780,1255,815],{"class":802},[780,1257,1258],{"class":789},"  string",[780,1260,1261],{"class":809},"  `json:\"email\" validate:\"required,email\"`\n",[780,1263,1264,1266,1269],{"class":782,"line":957},[780,1265,840],{"class":802},[780,1267,1268],{"class":796}," []",[780,1270,846],{"class":789},[780,1272,1273],{"class":782,"line":963},[780,1274,852],{"class":796},[780,1276,1277],{"class":782,"line":969},[780,1278,859],{"emptyLinePlaceholder":858},[780,1280,1282,1285,1288,1291],{"class":782,"line":1281},20,[780,1283,1284],{"class":785},"func",[780,1286,1287],{"class":878}," main",[780,1289,1290],{"class":796},"()",[780,1292,797],{"class":796},[780,1294,1296],{"class":782,"line":1295},21,[780,1297,1298],{"class":893},"    // Scan extracts User, Order, and their relationship\n",[780,1300,1302,1305,1307,1309,1311,1313,1315,1317],{"class":782,"line":1301},22,[780,1303,1304],{"class":865},"    metadata",[780,1306,869],{"class":865},[780,1308,872],{"class":865},[780,1310,875],{"class":796},[780,1312,199],{"class":878},[780,1314,881],{"class":796},[780,1316,884],{"class":789},[780,1318,887],{"class":796},[780,1320,1322],{"class":782,"line":1321},23,[780,1323,859],{"emptyLinePlaceholder":858},[780,1325,1327],{"class":782,"line":1326},24,[780,1328,1329],{"class":893},"    // Type information\n",[780,1331,1333,1336,1338,1341,1344,1346,1348,1351,1354],{"class":782,"line":1332},25,[780,1334,1335],{"class":865},"    fmt",[780,1337,875],{"class":796},[780,1339,1340],{"class":878},"Println",[780,1342,1343],{"class":796},"(",[780,1345,866],{"class":865},[780,1347,875],{"class":796},[780,1349,1350],{"class":865},"TypeName",[780,1352,1353],{"class":796},")",[780,1355,1356],{"class":893}," // \"User\"\n",[780,1358,1360,1362,1364,1366,1368,1370,1372,1375,1377],{"class":782,"line":1359},26,[780,1361,1335],{"class":865},[780,1363,875],{"class":796},[780,1365,1340],{"class":878},[780,1367,1343],{"class":796},[780,1369,866],{"class":865},[780,1371,875],{"class":796},[780,1373,1374],{"class":865},"FQDN",[780,1376,1353],{"class":796},[780,1378,1379],{"class":893},"     // \"main.User\" (reflects actual package path)\n",[780,1381,1383],{"class":782,"line":1382},27,[780,1384,859],{"emptyLinePlaceholder":858},[780,1386,1388],{"class":782,"line":1387},28,[780,1389,1390],{"class":893},"    // Field metadata\n",[780,1392,1394,1397,1400,1403,1406,1408,1411,1413,1415,1417],{"class":782,"line":1393},29,[780,1395,1396],{"class":830},"    for",[780,1398,1399],{"class":865}," _",[780,1401,1402],{"class":796},",",[780,1404,1405],{"class":865}," field",[780,1407,869],{"class":865},[780,1409,1410],{"class":830}," range",[780,1412,928],{"class":865},[780,1414,875],{"class":796},[780,1416,933],{"class":865},[780,1418,797],{"class":796},[780,1420,1422,1425,1427,1430,1432,1435,1439,1442,1444,1447,1450,1454,1456,1458,1460,1462,1465,1467,1469,1471,1474,1476,1478,1480,1483],{"class":782,"line":1421},30,[780,1423,1424],{"class":865},"        fmt",[780,1426,875],{"class":796},[780,1428,1429],{"class":878},"Printf",[780,1431,1343],{"class":796},[780,1433,1434],{"class":809},"\"",[780,1436,1438],{"class":1437},"scyPU","%s",[780,1440,1441],{"class":809}," (",[780,1443,1438],{"class":1437},[780,1445,1446],{"class":809},"): ",[780,1448,1449],{"class":1437},"%v",[780,1451,1453],{"class":1452},"suWN2","\\n",[780,1455,1434],{"class":809},[780,1457,1402],{"class":796},[780,1459,1405],{"class":865},[780,1461,875],{"class":796},[780,1463,1464],{"class":865},"Name",[780,1466,1402],{"class":796},[780,1468,1405],{"class":865},[780,1470,875],{"class":796},[780,1472,1473],{"class":865},"Kind",[780,1475,1402],{"class":796},[780,1477,1405],{"class":865},[780,1479,875],{"class":796},[780,1481,1482],{"class":865},"Tags",[780,1484,1177],{"class":796},[780,1486,1488],{"class":782,"line":1487},31,[780,1489,1490],{"class":796},"    }\n",[780,1492,1494],{"class":782,"line":1493},32,[780,1495,1496],{"class":893},"    // ID (scalar): map[json:id db:user_id]\n",[780,1498,1500],{"class":782,"line":1499},33,[780,1501,1502],{"class":893},"    // Email (scalar): map[json:email validate:required,email]\n",[780,1504,1506],{"class":782,"line":1505},34,[780,1507,1508],{"class":893},"    // Orders (slice): map[]\n",[780,1510,1512],{"class":782,"line":1511},35,[780,1513,859],{"emptyLinePlaceholder":858},[780,1515,1517],{"class":782,"line":1516},36,[780,1518,1519],{"class":893},"    // Relationships\n",[780,1521,1523,1525,1527,1529,1532,1534,1536,1538,1540,1542],{"class":782,"line":1522},37,[780,1524,1396],{"class":830},[780,1526,1399],{"class":865},[780,1528,1402],{"class":796},[780,1530,1531],{"class":865}," rel",[780,1533,869],{"class":865},[780,1535,1410],{"class":830},[780,1537,928],{"class":865},[780,1539,875],{"class":796},[780,1541,94],{"class":865},[780,1543,797],{"class":796},[780,1545,1547,1549,1551,1553,1555,1557,1559,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580,1582,1584,1586,1588,1591,1593,1595,1597,1599],{"class":782,"line":1546},38,[780,1548,1424],{"class":865},[780,1550,875],{"class":796},[780,1552,1429],{"class":878},[780,1554,1343],{"class":796},[780,1556,1434],{"class":809},[780,1558,1438],{"class":1437},[780,1560,1561],{"class":809}," → ",[780,1563,1438],{"class":1437},[780,1565,1441],{"class":809},[780,1567,1438],{"class":1437},[780,1569,1353],{"class":809},[780,1571,1453],{"class":1452},[780,1573,1434],{"class":809},[780,1575,1402],{"class":796},[780,1577,928],{"class":865},[780,1579,875],{"class":796},[780,1581,1350],{"class":865},[780,1583,1402],{"class":796},[780,1585,1531],{"class":865},[780,1587,875],{"class":796},[780,1589,1590],{"class":865},"To",[780,1592,1402],{"class":796},[780,1594,1531],{"class":865},[780,1596,875],{"class":796},[780,1598,1473],{"class":865},[780,1600,1177],{"class":796},[780,1602,1604],{"class":782,"line":1603},39,[780,1605,1490],{"class":796},[780,1607,1609],{"class":782,"line":1608},40,[780,1610,1611],{"class":893},"    // User → main.Order (collection)\n",[780,1613,1615],{"class":782,"line":1614},41,[780,1616,859],{"emptyLinePlaceholder":858},[780,1618,1620],{"class":782,"line":1619},42,[780,1621,1622],{"class":893},"    // Everything is cached\n",[780,1624,1626,1628,1630,1632,1634,1636,1638,1640],{"class":782,"line":1625},43,[780,1627,1335],{"class":865},[780,1629,875],{"class":796},[780,1631,1340],{"class":878},[780,1633,1343],{"class":796},[780,1635,690],{"class":865},[780,1637,875],{"class":796},[780,1639,531],{"class":878},[780,1641,1642],{"class":796},"())\n",[780,1644,1646],{"class":782,"line":1645},44,[780,1647,1648],{"class":893},"    // [main.User main.Order]\n",[780,1650,1652],{"class":782,"line":1651},45,[780,1653,859],{"emptyLinePlaceholder":858},[780,1655,1657],{"class":782,"line":1656},46,[780,1658,1659],{"class":893},"    // Export the full schema\n",[780,1661,1663,1666,1668,1670,1672,1674],{"class":782,"line":1662},47,[780,1664,1665],{"class":865},"    schema",[780,1667,869],{"class":865},[780,1669,872],{"class":865},[780,1671,875],{"class":796},[780,1673,541],{"class":878},[780,1675,1007],{"class":796},[780,1677,1679,1681,1683,1685,1687,1689,1692,1695,1697,1699,1701,1705,1707,1710],{"class":782,"line":1678},48,[780,1680,1335],{"class":865},[780,1682,875],{"class":796},[780,1684,1429],{"class":878},[780,1686,1343],{"class":796},[780,1688,1434],{"class":809},[780,1690,1691],{"class":1437},"%d",[780,1693,1694],{"class":809}," types cached",[780,1696,1453],{"class":1452},[780,1698,1434],{"class":809},[780,1700,1402],{"class":796},[780,1702,1704],{"class":1703},"skxcq"," len",[780,1706,1343],{"class":796},[780,1708,1709],{"class":865},"schema",[780,1711,1712],{"class":796},"))\n",[780,1714,1716],{"class":782,"line":1715},49,[780,1717,852],{"class":796},[766,1719,1721],{"id":1720},"capabilities","Capabilities",[1723,1724,1725,1741],"table",{},[1726,1727,1728],"thead",{},[1729,1730,1731,1735,1738],"tr",{},[1732,1733,1734],"th",{},"Feature",[1732,1736,1737],{},"Description",[1732,1739,1740],{},"Docs",[1742,1743,1744,1758,1771,1784,1796,1808],"tbody",{},[1729,1745,1746,1750,1753],{},[1747,1748,1749],"td",{},"Metadata Extraction",[1747,1751,1752],{},"Fields, types, indices, categories, struct tags",[1747,1754,1755],{},[695,1756,75],{"href":1757},"docs/learn/concepts",[1729,1759,1760,1763,1766],{},[1747,1761,1762],{},"Relationship Discovery",[1747,1764,1765],{},"References, collections, embeddings, maps",[1747,1767,1768],{},[695,1769,56],{"href":1770},"docs/guides/scanning",[1729,1772,1773,1776,1779],{},[1747,1774,1775],{},"Permanent Caching",[1747,1777,1778],{},"Extract once, cached forever",[1747,1780,1781],{},[695,1782,113],{"href":1783},"docs/learn/architecture",[1729,1785,1786,1788,1791],{},[1747,1787,61],{},[1747,1789,1790],{},"Register additional struct tags",[1747,1792,1793],{},[695,1794,1482],{"href":1795},"docs/guides/tags",[1729,1797,1798,1801,1804],{},[1747,1799,1800],{},"Module-Aware Scanning",[1747,1802,1803],{},"Recursive extraction within module boundaries",[1747,1805,1806],{},[695,1807,56],{"href":1770},[1729,1809,1810,1813,1819],{},[1747,1811,1812],{},"Schema Export",[1747,1814,1815,1818],{},[777,1816,1817],{},"Schema()"," returns all cached metadata",[1747,1820,1821],{},[695,1822,1824],{"href":1823},"docs/reference/api","API",[766,1826,1828],{"id":1827},"why-sentinel","Why sentinel?",[1830,1831,1832,1840,1846,1856,1862,1871],"ul",{},[1833,1834,1835,1839],"li",{},[1836,1837,1838],"strong",{},"Zero dependencies"," — only the Go standard library",[1833,1841,1842,1845],{},[1836,1843,1844],{},"Permanent caching"," — types are immutable at runtime, so metadata is cached once",[1833,1847,1848,1851,1852,1855],{},[1836,1849,1850],{},"Type-safe generics"," — ",[777,1853,1854],{},"Inspect[T]()"," catches type errors at compile time",[1833,1857,1858,1861],{},[1836,1859,1860],{},"Relationship discovery"," — automatically maps references, collections, embeddings, and maps",[1833,1863,1864,1851,1867,1870],{},[1836,1865,1866],{},"Module-aware scanning",[777,1868,1869],{},"Scan[T]()"," recursively extracts related types within your module",[1833,1872,1873,1876],{},[1836,1874,1875],{},"Thread-safe"," — concurrent access after initial extraction",[766,1878,1880],{"id":1879},"type-driven-generation","Type-Driven Generation",[692,1882,1883,1884,875],{},"Sentinel metadata enables a pattern: ",[1836,1885,1886],{},"define types once, generate everything else",[692,1888,1889],{},"Your struct definitions become the single source of truth. Downstream tools consume sentinel's metadata to generate:",[1830,1891,1892,1898,1904],{},[1833,1893,1894,1897],{},[1836,1895,1896],{},"Entity diagrams"," — Visualize domain models directly from type relationships",[1833,1899,1900,1903],{},[1836,1901,1902],{},"Database schemas"," — Generate DDL and type-safe queries from struct tags",[1833,1905,1906,1909],{},[1836,1907,1908],{},"API documentation"," — Produce OpenAPI specs from request/response types",[766,1911,1913],{"id":1912},"documentation","Documentation",[1830,1915,1916,1944,1972,1999],{},[1833,1917,1918,1920],{},[1836,1919,610],{},[1830,1921,1922,1928,1934,1939],{},[1833,1923,1924,1927],{},[695,1925,6],{"href":1926},"docs/learn/overview"," — purpose and motivation",[1833,1929,1930,1933],{},[695,1931,37],{"href":1932},"docs/learn/quickstart"," — get started in 5 minutes",[1833,1935,1936,1938],{},[695,1937,75],{"href":1757}," — metadata, relationships, caching",[1833,1940,1941,1943],{},[695,1942,113],{"href":1783}," — internal design and components",[1833,1945,1946,1948],{},[1836,1947,624],{},[1830,1949,1950,1955,1960,1966],{},[1833,1951,1952,1954],{},[695,1953,56],{"href":1770}," — Inspect vs Scan, module boundaries",[1833,1956,1957,1959],{},[695,1958,1482],{"href":1795}," — custom tag registration",[1833,1961,1962,1965],{},[695,1963,66],{"href":1964},"docs/guides/testing"," — testing with sentinel",[1833,1967,1968,1971],{},[695,1969,330],{"href":1970},"docs/guides/troubleshooting"," — common issues and solutions",[1833,1973,1974,1976],{},[1836,1975,637],{},[1830,1977,1978,1985,1992],{},[1833,1979,1980,1984],{},[695,1981,1983],{"href":1982},"docs/integrations/erd","erd"," — entity relationship diagrams",[1833,1986,1987,1991],{},[695,1988,1990],{"href":1989},"docs/integrations/soy","soy"," — SQL injection-safe queries",[1833,1993,1994,1998],{},[695,1995,1997],{"href":1996},"docs/integrations/rocco","rocco"," — OpenAPI generation",[1833,2000,2001,2003],{},[1836,2002,648],{},[1830,2004,2005,2010],{},[1833,2006,2007,2009],{},[695,2008,1824],{"href":1823}," — complete function documentation",[1833,2011,2012,2015],{},[695,2013,560],{"href":2014},"docs/reference/types"," — Metadata, FieldMetadata, TypeRelationship",[766,2017,2019],{"id":2018},"contributing","Contributing",[692,2021,2022,2023,2027],{},"See ",[695,2024,2026],{"href":2025},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines.",[766,2029,742],{"id":2030},"license",[692,2032,2033,2034,2036],{},"MIT License — see ",[695,2035,739],{"href":739}," for details.",[2038,2039,2040],"style",{},"html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"title":43,"searchDepth":19,"depth":19,"links":2042},[2043,2044,2045,2046,2047,2048,2049,2050,2051],{"id":768,"depth":19,"text":769},{"id":1111,"depth":19,"text":1112},{"id":1135,"depth":19,"text":1136},{"id":1720,"depth":19,"text":1721},{"id":1827,"depth":19,"text":1828},{"id":1879,"depth":19,"text":1880},{"id":1912,"depth":19,"text":1913},{"id":2018,"depth":19,"text":2019},{"id":2030,"depth":19,"text":742},"md","book-open",{},"/readme",{"title":683,"description":43},"readme","MUUSL3MBdLHmW_Knfm5wKIcSVK-ry2DHxqi-TiQ5NNE",{"id":2060,"title":2061,"body":2062,"description":43,"extension":2052,"icon":2219,"meta":2220,"navigation":858,"path":2221,"seo":2222,"stem":2223,"__hash__":2224},"resources/security.md","Security",{"type":685,"value":2063,"toc":2212},[2064,2068,2072,2094,2098,2101,2130,2134,2137,2181,2185,2188,2205,2209],[688,2065,2067],{"id":2066},"security-policy","Security Policy",[766,2069,2071],{"id":2070},"supported-versions","Supported Versions",[1723,2073,2074,2084],{},[1726,2075,2076],{},[1729,2077,2078,2081],{},[1732,2079,2080],{},"Version",[1732,2082,2083],{},"Supported",[1742,2085,2086],{},[1729,2087,2088,2091],{},[1747,2089,2090],{},"Latest",[1747,2092,2093],{},"✅",[766,2095,2097],{"id":2096},"reporting-a-vulnerability","Reporting a Vulnerability",[692,2099,2100],{},"We take security vulnerabilities seriously. If you discover a security issue, please follow these steps:",[2102,2103,2104,2110,2113],"ol",{},[1833,2105,2106,2109],{},[1836,2107,2108],{},"DO NOT"," create a public GitHub issue",[1833,2111,2112],{},"Email security details to the maintainers",[1833,2114,2115,2116],{},"Include:\n",[1830,2117,2118,2121,2124,2127],{},[1833,2119,2120],{},"Description of the vulnerability",[1833,2122,2123],{},"Steps to reproduce",[1833,2125,2126],{},"Potential impact",[1833,2128,2129],{},"Suggested fix (if available)",[766,2131,2133],{"id":2132},"security-best-practices","Security Best Practices",[692,2135,2136],{},"When using Sentinel:",[2102,2138,2139,2145,2170,2176],{},[1833,2140,2141,2144],{},[1836,2142,2143],{},"Metadata Exposure",": Be aware that Sentinel extracts and caches struct metadata. Ensure sensitive field names or tags don't leak information.",[1833,2146,2147,2150,2151],{},[1836,2148,2149],{},"Tag Values",": Struct tags may contain sensitive information. Review your tags carefully:",[1830,2152,2153,2156,2163],{},[1833,2154,2155],{},"Don't include credentials in tags",[1833,2157,2158,2159,2162],{},"Be cautious with ",[777,2160,2161],{},"example"," tags",[1833,2164,2165,2166,2169],{},"Review ",[777,2167,2168],{},"desc"," tags for information disclosure",[1833,2171,2172,2175],{},[1836,2173,2174],{},"Caching",": Sentinel uses permanent caching. Ensure your application doesn't expose the cache to untrusted sources.",[1833,2177,2178,2180],{},[1836,2179,1762],{},": The relationship extraction feature reveals connections between types. Consider if this information should be protected in your application.",[766,2182,2184],{"id":2183},"security-features","Security Features",[692,2186,2187],{},"Sentinel is designed with security in mind:",[1830,2189,2190,2193,2196,2199,2202],{},[1833,2191,2192],{},"Zero external dependencies (reduces supply chain risks)",[1833,2194,2195],{},"No network operations",[1833,2197,2198],{},"No file system operations beyond normal Go imports",[1833,2200,2201],{},"Read-only metadata extraction",[1833,2203,2204],{},"Thread-safe caching",[766,2206,2208],{"id":2207},"acknowledgments","Acknowledgments",[692,2210,2211],{},"We appreciate responsible disclosure of security vulnerabilities.",{"title":43,"searchDepth":19,"depth":19,"links":2213},[2214,2215,2216,2217,2218],{"id":2070,"depth":19,"text":2071},{"id":2096,"depth":19,"text":2097},{"id":2132,"depth":19,"text":2133},{"id":2183,"depth":19,"text":2184},{"id":2207,"depth":19,"text":2208},"shield",{},"/security",{"title":2061,"description":43},"security","PbTuWS3xUygout-uLYcQ4QCxnUHHp2CUdZz1zEMKOLI",{"id":2226,"title":2019,"body":2227,"description":2235,"extension":2052,"icon":777,"meta":2494,"navigation":858,"path":2495,"seo":2496,"stem":2018,"__hash__":2497},"resources/contributing.md",{"type":685,"value":2228,"toc":2482},[2229,2233,2236,2240,2278,2282,2287,2298,2302,2358,2362,2382,2386,2403,2407,2421,2425,2428,2472,2476,2479],[688,2230,2232],{"id":2231},"contributing-to-sentinel","Contributing to Sentinel",[692,2234,2235],{},"Thank you for your interest in contributing to Sentinel! We welcome contributions from the community.",[766,2237,2239],{"id":2238},"getting-started","Getting Started",[2102,2241,2242,2245,2251,2257,2260,2266,2272,2275],{},[1833,2243,2244],{},"Fork the repository",[1833,2246,2247,2248],{},"Clone your fork: ",[777,2249,2250],{},"git clone https://github.com/your-username/sentinel.git",[1833,2252,2253,2254],{},"Create a new branch: ",[777,2255,2256],{},"git checkout -b feature/your-feature-name",[1833,2258,2259],{},"Make your changes",[1833,2261,2262,2263],{},"Run tests: ",[777,2264,2265],{},"make test",[1833,2267,2268,2269],{},"Run linters: ",[777,2270,2271],{},"make lint",[1833,2273,2274],{},"Commit your changes with a descriptive message",[1833,2276,2277],{},"Push to your fork and submit a pull request",[766,2279,2281],{"id":2280},"development-setup","Development Setup",[2283,2284,2286],"h3",{"id":2285},"prerequisites","Prerequisites",[1830,2288,2289,2292],{},[1833,2290,2291],{},"Go 1.24 or higher",[1833,2293,2294,2295,1353],{},"golangci-lint (install with ",[777,2296,2297],{},"make install-tools",[2283,2299,2301],{"id":2300},"running-tests","Running Tests",[771,2303,2305],{"className":1115,"code":2304,"language":1117,"meta":43,"style":43},"make test        # Run all tests\nmake test-bench  # Run benchmarks\nmake coverage    # Generate coverage report\nmake lint        # Run linters\nmake check       # Run tests and lint\n",[777,2306,2307,2318,2328,2338,2348],{"__ignoreMap":43},[780,2308,2309,2312,2315],{"class":782,"line":9},[780,2310,2311],{"class":878},"make",[780,2313,2314],{"class":809}," test",[780,2316,2317],{"class":893},"        # Run all tests\n",[780,2319,2320,2322,2325],{"class":782,"line":19},[780,2321,2311],{"class":878},[780,2323,2324],{"class":809}," test-bench",[780,2326,2327],{"class":893},"  # Run benchmarks\n",[780,2329,2330,2332,2335],{"class":782,"line":134},[780,2331,2311],{"class":878},[780,2333,2334],{"class":809}," coverage",[780,2336,2337],{"class":893},"    # Generate coverage report\n",[780,2339,2340,2342,2345],{"class":782,"line":824},[780,2341,2311],{"class":878},[780,2343,2344],{"class":809}," lint",[780,2346,2347],{"class":893},"        # Run linters\n",[780,2349,2350,2352,2355],{"class":782,"line":837},[780,2351,2311],{"class":878},[780,2353,2354],{"class":809}," check",[780,2356,2357],{"class":893},"       # Run tests and lint\n",[766,2359,2361],{"id":2360},"code-style","Code Style",[1830,2363,2364,2367,2373,2376,2379],{},[1833,2365,2366],{},"Follow standard Go conventions",[1833,2368,2369,2370],{},"Ensure all code passes ",[777,2371,2372],{},"golangci-lint",[1833,2374,2375],{},"Write tests for new functionality",[1833,2377,2378],{},"Keep test coverage above 90%",[1833,2380,2381],{},"Document exported functions and types",[766,2383,2385],{"id":2384},"pull-request-process","Pull Request Process",[2102,2387,2388,2391,2394,2397,2400],{},[1833,2389,2390],{},"Ensure all tests pass",[1833,2392,2393],{},"Update documentation if needed",[1833,2395,2396],{},"Add entries to CHANGELOG.md if applicable",[1833,2398,2399],{},"Ensure your PR description clearly describes the problem and solution",[1833,2401,2402],{},"Link any relevant issues",[766,2404,2406],{"id":2405},"testing-guidelines","Testing Guidelines",[1830,2408,2409,2412,2415,2418],{},[1833,2410,2411],{},"Each source file should have a corresponding test file",[1833,2413,2414],{},"Write both positive and negative test cases",[1833,2416,2417],{},"Use table-driven tests where appropriate",[1833,2419,2420],{},"Ensure tests are deterministic and don't depend on external services",[766,2422,2424],{"id":2423},"commit-message-format","Commit Message Format",[692,2426,2427],{},"Use conventional commits format:",[1830,2429,2430,2436,2442,2448,2454,2460,2466],{},[1833,2431,2432,2435],{},[777,2433,2434],{},"feat:"," New feature",[1833,2437,2438,2441],{},[777,2439,2440],{},"fix:"," Bug fix",[1833,2443,2444,2447],{},[777,2445,2446],{},"docs:"," Documentation changes",[1833,2449,2450,2453],{},[777,2451,2452],{},"test:"," Test additions or changes",[1833,2455,2456,2459],{},[777,2457,2458],{},"refactor:"," Code refactoring",[1833,2461,2462,2465],{},[777,2463,2464],{},"perf:"," Performance improvements",[1833,2467,2468,2471],{},[777,2469,2470],{},"chore:"," Maintenance tasks",[766,2473,2475],{"id":2474},"questions","Questions?",[692,2477,2478],{},"Feel free to open an issue for any questions or concerns.",[2038,2480,2481],{},"html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":43,"searchDepth":19,"depth":19,"links":2483},[2484,2485,2489,2490,2491,2492,2493],{"id":2238,"depth":19,"text":2239},{"id":2280,"depth":19,"text":2281,"children":2486},[2487,2488],{"id":2285,"depth":134,"text":2286},{"id":2300,"depth":134,"text":2301},{"id":2360,"depth":19,"text":2361},{"id":2384,"depth":19,"text":2385},{"id":2405,"depth":19,"text":2406},{"id":2423,"depth":19,"text":2424},{"id":2474,"depth":19,"text":2475},{},"/contributing",{"title":2019,"description":2235},"tT6pj4CAvhIOHGkCwwB-9UxTqL7Kmyw1jbBT9E3iLTA",{"id":2499,"title":6,"author":2500,"body":2501,"description":8,"extension":2052,"meta":2603,"navigation":858,"path":5,"published":2604,"readtime":2605,"seo":2606,"stem":615,"tags":2607,"updated":2604,"__hash__":2609},"sentinel/v1.0.4/1.learn/1.overview.md","zoobzio",{"type":685,"value":2502,"toc":2597},[2503,2506,2508,2511,2514,2517,2520,2523,2526,2529,2537,2540,2543,2546,2569,2572],[688,2504,6],{"id":2505},"overview",[692,2507,13],{},[766,2509,16],{"id":2510},"the-idea",[692,2512,2513],{},"Go's type system already knows a lot about your domain: field names, types, struct tags, how types relate to each other. The problem is that reflection makes you work for it—low-level APIs, careful handling, repetitive boilerplate.",[692,2515,2516],{},"What if accessing struct metadata was trivial?",[692,2518,2519],{},"That's the question Sentinel answers. The goal is to make domain types the single source of truth—define your structs once, and let tooling derive schemas, documentation, validation, and queries from that definition. Sentinel handles the extraction and normalization.",[692,2521,2522],{},"From there, other packages do their work: schema generators read field types, documentation tools map relationships, validators pull rules from tags, query builders use field metadata. Sentinel does the extraction—what gets built on top is up to you.",[766,2524,22],{"id":2525},"the-implementation",[692,2527,2528],{},"Two functions, two use cases:",[692,2530,2531,2533,2534,2536],{},[1836,2532,194],{}," examines a single type in isolation. ",[1836,2535,199],{}," traverses your entire domain model from one entry point, discovering all related types within your module.",[692,2538,2539],{},"Both return normalized metadata—field names, types, kinds, tags, relationships—cached permanently after the first call.",[766,2541,27],{"id":2542},"what-it-enables",[692,2544,2545],{},"See it in action:",[1830,2547,2548,2555,2562],{},[1833,2549,2550,2554],{},[695,2551,2553],{"href":2552},"../integrations/erd","Entity relationship diagrams"," with erd",[1833,2556,2557,2561],{},[695,2558,2560],{"href":2559},"../integrations/soy","Type-safe SQL queries"," with soy",[1833,2563,2564,2568],{},[695,2565,2567],{"href":2566},"../integrations/rocco","OpenAPI documentation"," with rocco",[766,2570,32],{"id":2571},"next-steps",[1830,2573,2574,2580,2586,2592],{},[1833,2575,2576,2579],{},[695,2577,37],{"href":2578},"quickstart"," — installation and basic usage",[1833,2581,2582,2585],{},[695,2583,75],{"href":2584},"concepts"," — metadata structures and caching",[1833,2587,2588,2591],{},[695,2589,113],{"href":2590},"architecture"," — internal design",[1833,2593,2594,2009],{},[695,2595,487],{"href":2596},"../reference/api",{"title":43,"searchDepth":19,"depth":19,"links":2598},[2599,2600,2601,2602],{"id":2510,"depth":19,"text":16},{"id":2525,"depth":19,"text":22},{"id":2542,"depth":19,"text":27},{"id":2571,"depth":19,"text":32},{},"2025-12-30T00:00:00.000Z","2 mins",{"title":6,"description":8},[6,2608],"Introduction","lDvLifdG-xUH_wh7M2T8aLBChCrKz1W5Ep5xD0CFCus",[2611,2612],null,{"title":37,"path":36,"stem":617,"description":39,"children":-1},1776110684284]