Ctrl K
ComunidadGuías…

internal/autogen/validate.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package autogen

// Flag validation for the launch-arguments editor (ui-svelte/launch-args.md).
// Composition never blocks on this: the text is the user's and is stored
// verbatim. Validation exists so the editor can say "llama-server will refuse to
// start" before a save that looks successful and a spawn that dies, and it needs
// BOTH sources: the checked-in table knows what quartermaster emits, while the
// selected binary's --help knows what this build actually accepts.

import (
	"sort"
	"strings"
)

// Issue kinds for a composed launch command. The editor treats IssueUnknown as
// blocking (it confirms before saving) and the other two as warnings.
const (
	// IssueUnknown: the flag is in neither the checked-in table nor the selected
	// backend's --help, so a spawn carrying it dies with "invalid argument".
	IssueUnknown = "unknown"
	// IssueUnverified: the flag is not in the table and the backend's --help
	// could not be read, so nothing is known about it either way.
	IssueUnverified = "unverified"
	// IssueBackendMissing: the table knows the flag, this binary's --help does
	// not, normally a backend build older than the emitter.
	IssueBackendMissing = "backend-missing"
)

// CmdIssue is one complaint about a composed command, keyed to the token that
// caused it. The editor renders these under the custom text and confirms before
// saving while an IssueUnknown stands.
type CmdIssue struct {
	Token   string `json:"token"`
	Kind    string `json:"kind"`
	Message string `json:"message"`
	// Suggestions are the closest known spellings, for typos. Empty when nothing
	// is close enough to be worth naming.
	Suggestions []string `json:"suggestions,omitempty"`
}

// ValidateTokens checks every flag token of a composed command. backendFlags is
// the selected binary's --help spellings; nil means the probe did not run, which
// downgrades an unknown flag from "the spawn will fail" to "could not be
// checked" instead of inventing certainty.
//
// Non-flag arguments are skipped, and the value after a known value-taking flag
// is consumed (inline --flag=value included), so a negative number never reads
// as a flag. A flag the binary lists but the table does not is fine: the table
// describes what quartermaster emits, the union describes what will run.
func (ft *FlagTable) ValidateTokens(tokens []CmdToken, backendFlags []string) []CmdIssue {
	var help map[string]bool
	if backendFlags != nil {
		help = make(map[string]bool, len(backendFlags))
		for _, f := range backendFlags {
			help[f] = true
		}
	}
	var issues []CmdIssue
	for i := 0; i < len(tokens); i++ {
		tok := tokens[i].Text
		if !strings.HasPrefix(tok, "-") || tok == "-" || tok == "--" {
			continue
		}
		name, _, inline := SplitFlagToken(tok)
		if def, known := ft.Lookup(name); known {
			if help != nil && !anySpellingInHelp(def, help) {
				issues = append(issues, CmdIssue{
					Token:   name,
					Kind:    IssueBackendMissing,
					Message: "this backend build does not list this flag",
				})
			}
			if def.Value && !inline {
				i++
			}
			continue
		}
		if help != nil && help[name] {
			// The binary accepts a spelling the table does not model. Whether it
			// takes a value is unknown, so do not consume the next token.
			continue
		}
		issue := CmdIssue{Token: name, Suggestions: ft.suggestFlags(name, backendFlags)}
		if help == nil {
			issue.Kind = IssueUnverified
			issue.Message = "not a flag quartermaster knows; the backend --help could not be read"
		} else {
			issue.Kind = IssueUnknown
			issue.Message = ft.Backend + " does not accept this flag, so it will refuse to start"
		}
		issues = append(issues, issue)
	}
	return issues
}

// ValidateTokens checks a llama-server command. The diffusion path calls the
// method with SdFlags instead, so an sd-server flag is checked against
// sd-server's table and its --help rather than being reported as unknown.
func ValidateTokens(tokens []CmdToken, backendFlags []string) []CmdIssue {
	return LlamaFlags.ValidateTokens(tokens, backendFlags)
}

// anySpellingInHelp reports whether the binary advertises any of a known flag's
// spellings. The check is per spelling, not per name: a build that lists only
// --checkpoint-min-step still accepts the -cms the emitter writes.
func anySpellingInHelp(def FlagDef, help map[string]bool) bool {
	if help[def.Name] {
		return true
	}
	for _, a := range def.Aliases {
		if help[a] {
			return true
		}
	}
	return false
}

// suggestFlags names the closest known spellings for a flag neither source
// knows. A spelling that differs only in dashes wins outright (---cache-ram ->
// -cram, --cache-ram, since llama.cpp pairs those names); otherwise the closest
// names by edit distance, capped so a wild guess is never dressed up as a
// suggestion. backendFlags may be nil.
func (ft *FlagTable) suggestFlags(name string, backendFlags []string) []string {
	want := strings.ToLower(strings.TrimLeft(name, "-"))
	if want == "" {
		return nil
	}
	// Dash-count mistakes: same letters, one knob. Return every spelling the
	// union offers for it so the user sees both the short and the long form.
	for _, d := range ft.Defs {
		spellings := append([]string{d.Name}, d.Aliases...)
		for _, sp := range spellings {
			if strings.ToLower(strings.TrimLeft(sp, "-")) == want {
				return spellings
			}
		}
	}
	type cand struct {
		spelling string
		dist     int
	}
	var cands []cand
	seen := map[string]bool{}
	add := func(sp string) {
		if sp == "" || seen[sp] {
			return
		}
		seen[sp] = true
		cands = append(cands, cand{spelling: sp, dist: editDistance(want, strings.ToLower(strings.TrimLeft(sp, "-")))})
	}
	for _, d := range ft.Defs {
		add(d.Name)
		for _, a := range d.Aliases {
			add(a)
		}
	}
	for _, f := range backendFlags {
		add(f)
	}
	sort.SliceStable(cands, func(i, j int) bool {
		if cands[i].dist != cands[j].dist {
			return cands[i].dist < cands[j].dist
		}
		return cands[i].spelling < cands[j].spelling
	})
	limit := 3
	if len(want) <= 4 {
		limit = 1
	}
	var out []string
	for _, c := range cands {
		if c.dist == 0 || c.dist > limit {
			continue
		}
		out = append(out, c.spelling)
		if len(out) == 2 {
			break
		}
	}
	return out
}

// validateComposed runs the editor's check over a composed command. Only a
// command carrying custom text is checked: with an empty box the command is the
// emitter's own, which the golden flag-table test holds to the table and which
// internal/server/flagcheck.go checks against the installed binary at boot.
// When there IS custom text the whole command is walked, so a backend older than
// the emitter shows up as a warning next to the user's own flags.
func validateComposedWith(ft *FlagTable, cc ComposedCmd, backendExe string) []CmdIssue {
	if len(cc.Tokens) == 0 || strings.TrimSpace(cc.Custom) == "" {
		return nil
	}
	var flags []string
	if fs, err := BackendFlags(backendExe); err == nil {
		flags = fs
	}
	return ft.ValidateTokens(cc.Tokens, flags)
}

// validateComposed is the llama-server spelling, kept for its callers.
func validateComposed(cc ComposedCmd, backendExe string) []CmdIssue {
	return validateComposedWith(LlamaFlags, cc, backendExe)
}

// editDistance is the Levenshtein distance, two rows of DP. Flags are short and
// this runs only for a token neither source recognized.
func editDistance(a, b string) int {
	if a == b {
		return 0
	}
	if len(a) == 0 {
		return len(b)
	}
	if len(b) == 0 {
		return len(a)
	}
	prev := make([]int, len(b)+1)
	cur := make([]int, len(b)+1)
	for j := range prev {
		prev[j] = j
	}
	for i := 1; i <= len(a); i++ {
		cur[0] = i
		for j := 1; j <= len(b); j++ {
			cost := 1
			if a[i-1] == b[j-1] {
				cost = 0
			}
			cur[j] = min3(prev[j]+1, cur[j-1]+1, prev[j-1]+cost)
		}
		prev, cur = cur, prev
	}
	return prev[len(b)]
}

func min3(a, b, c int) int {
	if b < a {
		a = b
	}
	if c < a {
		a = c
	}
	return a
}