Files
llmgw/internal/docgen/docgen.go
T
Ilker ManapandClaude Opus 5.5 9ab93a0aa6 Watch the cluster and edit every setting from the console
The console gains live views of nodes, GPUs, models, replicas, usage and
agent logs, and a settings tree over every key of server.yaml (agent.yaml on
a node), generated from the configuration structs: values with their
defaults and doc comments, edited in dialogs, saved only once the service's
own loader accepts them, comments kept. The generator now tells blocks apart
by package, which brings the TLS, engine and supervisor settings back into
the reference.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 22:54:09 +02:00

189 lines
5.5 KiB
Go

// Package docgen turns the configuration structs into reference documentation.
//
// Generated rather than written, because a hand-written reference is a promise
// that someone will remember. This project added six settings in a fortnight —
// store, servers, lease_ttl, thread and memory retention, min_disk_gb — and
// every one of them reached an operator only through a narrative document that
// happened to mention it. A reference nobody maintains is worse than none: it
// is the same list, quietly missing the setting you came to look up.
//
// Derived from the source, so the only way for it to be wrong is for the code
// to be wrong. A test regenerates it and fails when it has drifted, which is
// what keeps that true after everyone has forgotten this exists.
package docgen
//go:generate go run ./gen
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"reflect"
"sort"
"strings"
)
// Field is one configuration key.
type Field struct {
Key string
Type string
Doc string
// Nested names the struct this key expands into, by its ID
// (config.Auth), empty for a leaf. It is what lets the reference say
// "see Auth" rather than printing the same twenty keys under four
// parents.
Nested string
}
// Block is a struct that appears in configuration.
type Block struct {
// Package and Name are where the struct is declared: what tells
// tlsconf.Config from the agent's Config.
Package string
Name string
// Title is what it is called in the reference and on the screens: the
// name alone where no other block shares it, the package's name with it
// where one does.
Title string
Doc string
Fields []Field
}
// ID is the block's package and name, unique where Name is not.
func (b Block) ID() string { return b.Package + "." + b.Name }
// Collect reads koanf-tagged structs out of Go source files.
func Collect(files []string) ([]Block, error) {
fset := token.NewFileSet()
var blocks []Block
seen := map[string]bool{}
for _, path := range files {
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
ast.Inspect(f, func(n ast.Node) bool {
ts, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
st, ok := ts.Type.(*ast.StructType)
if !ok {
return true
}
b, ok := blockOf(f.Name.Name, ts, st)
if !ok || seen[b.ID()] {
return true
}
seen[b.ID()] = true
blocks = append(blocks, b)
return true
})
}
// Named by their own name unless another block has it too. Keyed by
// name alone, tlsconf.Config, engines.Config and supervisor.Config were
// each taken for the agent's Config and dropped: the reference had no
// TLS settings at all.
count := map[string]int{}
for _, b := range blocks {
count[b.Name]++
}
for i := range blocks {
blocks[i].Title = blocks[i].Name
if count[blocks[i].Name] > 1 {
blocks[i].Title = blocks[i].ID()
}
}
sort.Slice(blocks, func(i, j int) bool { return blocks[i].Title < blocks[j].Title })
return blocks, nil
}
// blockOf builds a Block, or reports that this struct carries no settings.
func blockOf(pkg string, ts *ast.TypeSpec, st *ast.StructType) (Block, bool) {
b := Block{Package: pkg, Name: ts.Name.Name, Doc: docText(ts.Doc)}
for _, f := range st.Fields.List {
if f.Tag == nil {
continue
}
tag := reflect.StructTag(strings.Trim(f.Tag.Value, "`"))
key, ok := tag.Lookup("koanf")
// "-" is a field carried in the struct but never read from a file;
// listing it would send someone looking for a setting that does not
// exist.
if !ok || key == "" || key == "-" {
continue
}
typ, nested := typeOf(pkg, f.Type)
b.Fields = append(b.Fields, Field{
Key: key, Type: typ, Doc: docText(f.Doc), Nested: nested,
})
}
return b, len(b.Fields) > 0
}
// typeOf renders a field's type, and names the struct it expands into.
func typeOf(pkg string, e ast.Expr) (rendered, nested string) {
switch t := e.(type) {
case *ast.Ident:
if isNamedStruct(t.Name) {
return t.Name, pkg + "." + t.Name
}
return t.Name, ""
case *ast.SelectorExpr:
// A type from another package, such as tlsconf.Config. Standard
// library types are values, not blocks: a time.Duration is a
// setting, written "30s".
name := t.Sel.Name
if x, ok := t.X.(*ast.Ident); ok {
if x.Name == "time" {
return x.Name + "." + name, ""
}
return x.Name + "." + name, x.Name + "." + name
}
return name, ""
case *ast.ArrayType:
inner, _ := typeOf(pkg, t.Elt)
return "[]" + inner, ""
case *ast.MapType:
k, _ := typeOf(pkg, t.Key)
v, _ := typeOf(pkg, t.Value)
return "map[" + k + "]" + v, ""
case *ast.StarExpr:
inner, n := typeOf(pkg, t.X)
return "*" + inner, n
default:
return "value", ""
}
}
// isNamedStruct guesses whether an identifier names a configuration block.
//
// By capitalisation, because the alternative is resolving types across
// packages for a document. A wrong guess costs a "see also" that points at
// nothing, which the renderer drops.
func isNamedStruct(name string) bool {
if name == "" {
return false
}
switch name {
case "string", "bool", "int", "int64", "float64", "any", "byte", "rune":
return false
}
return name[0] >= 'A' && name[0] <= 'Z'
}
// docText flattens a comment group into one paragraph per blank line.
func docText(g *ast.CommentGroup) string {
if g == nil {
return ""
}
var lines []string
for _, c := range g.List {
t := strings.TrimPrefix(c.Text, "//")
lines = append(lines, strings.TrimPrefix(t, " "))
}
return strings.TrimSpace(strings.Join(lines, "\n"))
}