~bigbes/sr-ht-spec

33a3deafbd02e8f74b9cd588d48867e0c9f77a69 — Eugene Blikh 27 days ago a733623
feat: foundation — go.mod with every dependency, and the core/ domain

Phase 1 foundation commit. Two things, so that later parallel waves write
disjoint directories and never touch go.mod:

  - go.mod / go.sum carrying every external dependency the whole module will
    need (go-git, bleve, goldmark, chi, lib/pq, yaml.v3, the MCP SDK, brant,
    auxilia, testify, and the sr-ht-core fork). Populated by building a
    throwaway blank-import file, which is then deleted; `go mod tidy` runs
    once, at the very end of the build-out.
  - core/, the pure domain: owner/space names, safe relative paths, the
    globally-unique document ID grammar, frontmatter parsing and schema
    validation, `.spec.yml` policy with auto_merge glob matching, and the
    proposal state machine. Standard library plus yaml.v3, nothing else.

Two design invariants are enforced here rather than documented and hoped for:
"approved" is not a status (it is a property of the branch a document is
reachable from), and the proposal machine has exactly open/merged/rejected.
A per-space `.spec.yml` cannot reintroduce either.

Note on the sr-ht-core pin: the design calls for a `replace` onto
git.srht.bigb.es/~bigbes/core-go at c2c2f38, but that commit's go.mod still
declares `module git.sr.ht/~sircmpwn/core-go`, so Go rejects the replacement.
Both siblings pin the later dd418a20 under the canonical path with no
replace; this does the same.
A .gitignore => .gitignore +17 -0
@@ 0,0 1,17 @@
# Compiled service binaries
/specsrht
/specsrht-migrate

# Transient build artifacts
*.tmp

# Local instance config
/config.ini

# Local bare repos and the bleve index + render cache. Both are the service's
# runtime state, not source; the cache is safe to delete at any time.
/repos/
/cache/

# Intermediate stylesheet; only the content-hashed main.min.<sha>.css ships.
/web/static/main.css

A Makefile => Makefile +79 -0
@@ 0,0 1,79 @@
# spec.sr.ht — build scaffolding (compare.sr.ht / sourcehut-dolt style).
#
# One Go binary plus a brant migration wrapper, both under cmd/. The CSS is
# built from the shared sourcehut scss partials with sassc; there is no
# frontend bundle — the prose differ renders server-side.

SERVICE=spec.sr.ht
BIN=specsrht
MIGRATE_BIN=specsrht-migrate

PREFIX?=/usr/local
BINDIR?=$(PREFIX)/bin
SHAREDIR?=$(PREFIX)/share
ASSETS?=/usr/share/sourcehut
STATICDIR?=$(ASSETS)/$(SERVICE)/static
MIGRATIONDIR?=$(SHAREDIR)/sourcehut/migrations/$(SERVICE)

SASSC?=sassc
SASSC_INCLUDE=-I$(ASSETS)/scss

all: build

# Compile the binaries. cmd/ lands in Phase 1's Wave B, so until then each
# target skips rather than failing — a red default target during the build-out
# trains everyone to ignore it.
build: $(BIN) $(MIGRATE_BIN)

$(BIN):
	@if [ -d ./cmd/$(BIN) ]; then \
		echo "go build -o $@ ./cmd/$(BIN)"; \
		go build -o $@ ./cmd/$(BIN); \
	else \
		echo "skip $@: ./cmd/$(BIN) not present yet"; \
	fi

$(MIGRATE_BIN):
	@if [ -d ./cmd/$(MIGRATE_BIN) ]; then \
		echo "go build -o $@ ./cmd/$(MIGRATE_BIN)"; \
		go build -o $@ ./cmd/$(MIGRATE_BIN); \
	else \
		echo "skip $@: ./cmd/$(MIGRATE_BIN) not present yet"; \
	fi

test:
	go test ./...

# CSS pipeline: sassc -> minify -> content-hashed filename. The running service
# globs web/static/main.min.*.css at startup, so the hash in the name is the
# cache-busting version. Requires the shared scss partials installed at
# $(ASSETS)/scss (core.sr.ht `make install`) and scss/main.scss to exist.
# Produces exactly ONE web/static/main.min.<sha256[:8]>.css; old ones and the
# intermediate main.css are removed.
css:
	mkdir -p web/static
	rm -f web/static/main.css web/static/main.min.*.css
	$(SASSC) $(SASSC_INCLUDE) scss/main.scss web/static/main.css
	minify -o web/static/main.min.css web/static/main.css
	mv web/static/main.min.css \
		web/static/main.min.$$(sha256sum web/static/main.min.css | cut -c1-8).css
	rm -f web/static/main.css

# Local development run. Requires a ./config.ini in the working directory (or
# ../config.ini, /etc/sr.ht/config.ini) carrying the instance's shared
# [sr.ht]/[webhooks] keys plus a [spec.sr.ht] section.
run-dev: build
	./$(BIN) -b localhost:5091

install: build
	install -Dm755 $(BIN) $(DESTDIR)$(BINDIR)/$(BIN)
	install -Dm755 $(MIGRATE_BIN) $(DESTDIR)$(BINDIR)/$(MIGRATE_BIN)
	mkdir -p $(DESTDIR)$(STATICDIR) $(DESTDIR)$(MIGRATIONDIR)
	install -Dm644 -t $(DESTDIR)$(STATICDIR) web/static/*
	install -Dm644 -t $(DESTDIR)$(MIGRATIONDIR) migrations/*.sql

clean:
	rm -f $(BIN) $(MIGRATE_BIN)
	rm -f web/static/main.css web/static/main.min.*.css

.PHONY: all build test css run-dev install clean $(BIN) $(MIGRATE_BIN)

A core/errors.go => core/errors.go +69 -0
@@ 0,0 1,69 @@
// Package core holds the pure domain logic of spec.sr.ht: owner and space name
// validation, safe document paths, the globally-unique document ID grammar, the
// frontmatter schema contract, `.spec.yml` space policy, and the proposal state
// machine.
//
// It depends on nothing but the standard library and gopkg.in/yaml.v3, never
// touches the network or the filesystem, and knows nothing about git, Postgres
// or HTTP. Dependency direction is strictly downward: gitx, db, service, api and
// web import core; core imports none of them. That is what keeps the three
// agent-facing surfaces (REST, MCP, GraphQL) behaviourally identical — they
// share these rules rather than each re-deriving them.
package core

import "errors"

// Sentinel errors, one per failure class. Callers compare with errors.Is;
// wrapping with %w adds the offending value without losing the class, which is
// what lets the API layer map a failure to a status code (422 for a schema
// violation, 400 for a malformed name) without string matching.
var (
	// ErrInvalidName is returned for a malformed owner or space name.
	ErrInvalidName = errors.New("invalid name")

	// ErrInvalidPath is returned for a path that is not a safe relative path
	// inside a space: absolute, traversing, or carrying bytes that would be
	// unsafe in a git tree or misleading in the review UI.
	ErrInvalidPath = errors.New("invalid path")

	// ErrInvalidDocID is returned for a document ID that does not match the
	// PREFIX-DIGITS grammar. IDs are globally unique and are the anchor for
	// cross-space links, comments and staleness checks, so shape is enforced
	// at every door.
	ErrInvalidDocID = errors.New("invalid document id")

	// ErrMalformedFrontmatter marks a document whose YAML frontmatter block is
	// missing, unterminated, not a mapping, or not parseable. It is distinct
	// from ErrMissingField: this is "cannot read", not "read but incomplete".
	ErrMalformedFrontmatter = errors.New("malformed frontmatter")

	// ErrMissingField marks frontmatter that parsed but omits a key the space's
	// schema requires. This is the 422 an agent gets for forgetting `status:`.
	ErrMissingField = errors.New("missing required frontmatter field")

	// ErrInvalidStatus is returned for a `status:` value outside the allowed
	// enum. Note "approved" is deliberately not a status — approval is a
	// property of the branch a document is reachable from, never of authored
	// metadata.
	ErrInvalidStatus = errors.New("invalid status")

	// ErrInvalidPolicy is returned for a `.spec.yml` that parses as YAML but
	// does not describe a usable policy.
	ErrInvalidPolicy = errors.New("invalid .spec.yml policy")

	// ErrInvalidPattern is returned for a malformed auto_merge path pattern.
	// A pattern that silently matches nothing would quietly turn the bimodal
	// cadence back into "everything waits for a human", so it is rejected.
	ErrInvalidPattern = errors.New("invalid path pattern")

	// ErrInvalidState is returned for a proposal state string outside the
	// open/merged/rejected set, typically read back from Postgres.
	ErrInvalidState = errors.New("invalid proposal state")

	// ErrInvalidTransition is returned for a proposal state change the machine
	// does not allow, such as re-merging an already rejected proposal.
	ErrInvalidTransition = errors.New("invalid proposal state transition")

	// ErrInvalidApproval is returned for an approval kind outside human/policy.
	ErrInvalidApproval = errors.New("invalid approval kind")
)

A core/frontmatter.go => core/frontmatter.go +276 -0
@@ 0,0 1,276 @@
package core

import (
	"fmt"
	"strings"

	"gopkg.in/yaml.v3"
)

// Status is the authored lifecycle marker of a document.
//
// The enum is exactly draft/review/superseded. "approved" is deliberately not a
// status: a document is approved when it is reachable from the approved ref,
// and that is the whole definition. Had "approved" stayed here, either the
// approved branch would permanently carry `status: draft`, or the merge would
// have to rewrite frontmatter nobody authored — which would also invalidate the
// agent's If-Match base on its next read.
type Status string

const (
	StatusDraft      Status = "draft"
	StatusReview     Status = "review"
	StatusSuperseded Status = "superseded"
)

// DefaultStatuses returns the full status enum, in the order a space's
// `.spec.yml` would list it. A fresh slice each call, so a caller cannot mutate
// the default out from under everyone else.
func DefaultStatuses() []Status {
	return []Status{StatusDraft, StatusReview, StatusSuperseded}
}

// ParseStatus validates a status string against the enum.
func ParseStatus(s string) (Status, error) {
	switch Status(s) {
	case StatusDraft, StatusReview, StatusSuperseded:
		return Status(s), nil
	}
	return "", fmt.Errorf("%w: %q is not one of %s", ErrInvalidStatus, s, joinStatuses(DefaultStatuses()))
}

func joinStatuses(ss []Status) string {
	parts := make([]string, len(ss))
	for i, s := range ss {
		parts[i] = string(s)
	}
	return strings.Join(parts, "|")
}

// Frontmatter is the YAML header of a document. The modelled fields are the
// ones the service reasons about; anything else an author writes is preserved
// in git and reported by Present, but is not interpreted here.
type Frontmatter struct {
	ID         string   `yaml:"id"`
	Title      string   `yaml:"title"`
	Status     Status   `yaml:"status"`
	Supersedes string   `yaml:"supersedes"`
	Owners     []string `yaml:"owners"`
	Tags       []string `yaml:"tags"`
	Type       string   `yaml:"type"`
	Summary    string   `yaml:"summary"`

	// Present is the set of top-level keys that actually appeared in the
	// source. Required-key validation asks about presence, not emptiness:
	// `title:` with no value is present and fails for being blank, whereas an
	// absent `title:` is a different error with a different fix. A zero-valued
	// struct field cannot tell those apart.
	Present map[string]bool `yaml:"-"`
}

// Has reports whether key appeared in the parsed source. Nil-safe, so a
// hand-constructed Frontmatter reports every key as absent rather than panicking.
func (f Frontmatter) Has(key string) bool { return f.Present[key] }

const (
	frontDelim = "---"
	// docEndDelim is YAML's explicit end-of-document marker. Editors emit it
	// occasionally, and treating it as a closing fence costs one comparison.
	docEndDelim = "..."
)

// SplitFrontmatter separates the YAML frontmatter block from the markdown body.
// The document must open with a "---" line and close the block with a "---"
// (or "...") line; both delimiters are excluded from the returned slices.
//
// It is strict on purpose. A document whose frontmatter is unterminated would
// otherwise parse as a body-only document with no ID, silently dropping out of
// the registry and the index instead of failing at the door.
func SplitFrontmatter(src []byte) (front, body []byte, err error) {
	s := string(src)
	if strings.HasPrefix(s, "\ufeff") {
		return nil, nil, fmt.Errorf("%w: document starts with a UTF-8 BOM before the %q delimiter",
			ErrMalformedFrontmatter, frontDelim)
	}

	first, rest, hasMore := strings.Cut(s, "\n")
	if strings.TrimSuffix(first, "\r") != frontDelim {
		return nil, nil, fmt.Errorf("%w: document does not start with a %q delimiter",
			ErrMalformedFrontmatter, frontDelim)
	}
	if !hasMore {
		return nil, nil, fmt.Errorf("%w: frontmatter block is not terminated by %q",
			ErrMalformedFrontmatter, frontDelim)
	}

	for pos := 0; pos <= len(rest); {
		line, tail, ok := strings.Cut(rest[pos:], "\n")
		switch strings.TrimSuffix(line, "\r") {
		case frontDelim, docEndDelim:
			return []byte(rest[:pos]), []byte(tail), nil
		}
		if !ok {
			break
		}
		pos = len(rest) - len(tail)
	}
	return nil, nil, fmt.Errorf("%w: frontmatter block is not terminated by %q",
		ErrMalformedFrontmatter, frontDelim)
}

// ParseFrontmatter decodes a frontmatter block into a Frontmatter, recording
// which keys were present.
//
// Duplicate keys are rejected explicitly rather than last-one-wins: a document
// carrying two `id:` lines is exactly the typo that corrupts the global ID
// registry, and it is far cheaper to reject at the door than to find weeks later.
func ParseFrontmatter(front []byte) (Frontmatter, error) {
	var node yaml.Node
	if err := yaml.Unmarshal(front, &node); err != nil {
		return Frontmatter{}, fmt.Errorf("%w: %v", ErrMalformedFrontmatter, err)
	}
	if node.Kind == yaml.DocumentNode {
		if len(node.Content) != 1 {
			return Frontmatter{}, fmt.Errorf("%w: expected exactly one YAML document, got %d",
				ErrMalformedFrontmatter, len(node.Content))
		}
		node = *node.Content[0]
	}

	// An empty block is well-formed YAML and yields a zero node. It is not a
	// parse failure; it fails schema validation instead, which names the
	// missing keys and is the more useful error.
	if node.Kind == 0 {
		return Frontmatter{Present: map[string]bool{}}, nil
	}
	if node.Kind != yaml.MappingNode {
		return Frontmatter{}, fmt.Errorf("%w: frontmatter must be a YAML mapping", ErrMalformedFrontmatter)
	}

	present := make(map[string]bool, len(node.Content)/2)
	for i := 0; i+1 < len(node.Content); i += 2 {
		key := node.Content[i]
		if key.Kind != yaml.ScalarNode {
			return Frontmatter{}, fmt.Errorf("%w: non-scalar key at line %d", ErrMalformedFrontmatter, key.Line)
		}
		if present[key.Value] {
			return Frontmatter{}, fmt.Errorf("%w: duplicate key %q at line %d",
				ErrMalformedFrontmatter, key.Value, key.Line)
		}
		present[key.Value] = true
	}

	var fm Frontmatter
	if err := node.Decode(&fm); err != nil {
		return Frontmatter{}, fmt.Errorf("%w: %v", ErrMalformedFrontmatter, err)
	}
	fm.Present = present
	return fm, nil
}

// ParseDocument splits and parses a whole document, returning its frontmatter
// and its markdown body.
func ParseDocument(src []byte) (Frontmatter, []byte, error) {
	front, body, err := SplitFrontmatter(src)
	if err != nil {
		return Frontmatter{}, nil, err
	}
	fm, err := ParseFrontmatter(front)
	if err != nil {
		return Frontmatter{}, nil, err
	}
	return fm, body, nil
}

// Schema is a space's frontmatter contract, as carried by `.spec.yml`. It is
// enforced at propose time and at push time — schema validation at the door is
// the cheapest available defence against agent slop.
type Schema struct {
	Required []string `yaml:"required"`
	Status   []Status `yaml:"status"`
}

// DefaultSchema is the contract a space gets when its `.spec.yml` says nothing.
func DefaultSchema() Schema {
	return Schema{
		Required: []string{"id", "title", "status"},
		Status:   DefaultStatuses(),
	}
}

// Validate reports whether the schema itself is usable. The status list is the
// interesting half: a space may narrow the enum (say, forbid `review`) but may
// not invent a member, which is what keeps "approved" from creeping back in via
// a per-space config file.
func (s Schema) Validate() error {
	seenKey := make(map[string]bool, len(s.Required))
	for _, key := range s.Required {
		if key == "" {
			return fmt.Errorf("%w: schema.required contains an empty key", ErrInvalidPolicy)
		}
		if seenKey[key] {
			return fmt.Errorf("%w: schema.required lists %q twice", ErrInvalidPolicy, key)
		}
		seenKey[key] = true
	}

	if len(s.Status) == 0 {
		return fmt.Errorf("%w: schema.status must list at least one status", ErrInvalidPolicy)
	}
	seenStatus := make(map[Status]bool, len(s.Status))
	for _, st := range s.Status {
		if _, err := ParseStatus(string(st)); err != nil {
			return fmt.Errorf("%w: schema.status: %v", ErrInvalidPolicy, err)
		}
		if seenStatus[st] {
			return fmt.Errorf("%w: schema.status lists %q twice", ErrInvalidPolicy, st)
		}
		seenStatus[st] = true
	}
	return nil
}

// AllowsStatus reports whether st is permitted by this schema.
func (s Schema) AllowsStatus(st Status) bool {
	for _, allowed := range s.Status {
		if st == allowed {
			return true
		}
	}
	return false
}

// ValidateFrontmatter checks a parsed document header against the schema. It
// answers "may this document be proposed into this space", and every write path
// (REST, MCP, and the update hook on your own pushes) runs it.
func (s Schema) ValidateFrontmatter(fm Frontmatter) error {
	for _, key := range s.Required {
		if !fm.Has(key) {
			return fmt.Errorf("%w: %q", ErrMissingField, key)
		}
	}
	if fm.Has("id") {
		if err := ValidateDocID(fm.ID); err != nil {
			return err
		}
	}
	if fm.Has("title") && strings.TrimSpace(fm.Title) == "" {
		return fmt.Errorf("%w: %q is present but blank", ErrMissingField, "title")
	}
	if fm.Has("status") && !s.AllowsStatus(fm.Status) {
		return fmt.Errorf("%w: %q is not one of %s", ErrInvalidStatus, fm.Status, joinStatuses(s.Status))
	}
	// `supersedes:` with no value is a half-finished edit, not an absent key,
	// so it is rejected rather than ignored.
	if fm.Has("supersedes") {
		if err := ValidateDocID(fm.Supersedes); err != nil {
			return fmt.Errorf("supersedes: %w", err)
		}
	}
	for _, owner := range fm.Owners {
		if err := ValidateOwner(strings.TrimPrefix(owner, "~")); err != nil {
			return fmt.Errorf("owners: %w", err)
		}
	}
	return nil
}

A core/frontmatter_test.go => core/frontmatter_test.go +405 -0
@@ 0,0 1,405 @@
package core

import (
	"errors"
	"strings"
	"testing"
)

func TestParseStatus(t *testing.T) {
	tests := []struct {
		in string
		ok bool
	}{
		{"draft", true},
		{"review", true},
		{"superseded", true},

		// "approved" is a property of the branch, never of the frontmatter.
		// This case is the whole reason the enum is validated at all.
		{"approved", false},
		{"Approved", false},
		{"APPROVED", false},
		{"merged", false},
		{"Draft", false},
		{"draft ", false},
		{" draft", false},
		{"", false},
		{"черновик", false},
	}
	for _, tc := range tests {
		st, err := ParseStatus(tc.in)
		if (err == nil) != tc.ok {
			t.Errorf("ParseStatus(%q) = %q, %v, want ok=%v", tc.in, st, err, tc.ok)
		}
		if err != nil && !errors.Is(err, ErrInvalidStatus) {
			t.Errorf("ParseStatus(%q) error %v is not ErrInvalidStatus", tc.in, err)
		}
	}
}

func TestSplitFrontmatter(t *testing.T) {
	tests := []struct {
		name      string
		in        string
		wantFront string
		wantBody  string
		ok        bool
	}{
		{"minimal", "---\nid: SPEC-1\n---\n# Title\n", "id: SPEC-1\n", "# Title\n", true},
		{"empty block", "---\n---\n", "", "", true},
		{"empty body", "---\nid: SPEC-1\n---\n", "id: SPEC-1\n", "", true},
		{"no trailing newline after fence", "---\nid: SPEC-1\n---", "id: SPEC-1\n", "", true},
		{"crlf", "---\r\nid: SPEC-1\r\n---\r\nbody\r\n", "id: SPEC-1\r\n", "body\r\n", true},
		{"dot fence", "---\nid: SPEC-1\n...\nbody\n", "id: SPEC-1\n", "body\n", true},
		{"hr in body is not a fence", "---\nid: SPEC-1\n---\ntext\n\n---\n\nmore\n", "id: SPEC-1\n", "text\n\n---\n\nmore\n", true},

		{"empty document", "", "", "", false},
		{"no frontmatter", "# Just markdown\n", "", "", false},
		{"leading blank line", "\n---\nid: SPEC-1\n---\n", "", "", false},
		{"fence only", "---", "", "", false},
		{"unterminated", "---\nid: SPEC-1\nbody\n", "", "", false},
		{"indented fence", "  ---\nid: SPEC-1\n---\n", "", "", false},
		{"four dashes", "----\nid: SPEC-1\n----\n", "", "", false},
		{"utf-8 bom", "\ufeff---\nid: SPEC-1\n---\n", "", "", false},
		// A fence with trailing whitespace is not a fence; treating it as one
		// would make an unterminated block parse as a body-only document.
		{"fence with trailing space", "--- \nid: SPEC-1\n---\n", "", "", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			front, body, err := SplitFrontmatter([]byte(tc.in))
			if (err == nil) != tc.ok {
				t.Fatalf("SplitFrontmatter(%q) = %q, %q, %v, want ok=%v", tc.in, front, body, err, tc.ok)
			}
			if !tc.ok {
				if !errors.Is(err, ErrMalformedFrontmatter) {
					t.Fatalf("error %v is not ErrMalformedFrontmatter", err)
				}
				return
			}
			if string(front) != tc.wantFront {
				t.Errorf("front = %q, want %q", front, tc.wantFront)
			}
			if string(body) != tc.wantBody {
				t.Errorf("body = %q, want %q", body, tc.wantBody)
			}
		})
	}
}

func TestParseFrontmatter(t *testing.T) {
	full := "id: SPEC-0007\n" +
		"title: Proposal storage model\n" +
		"status: draft\n" +
		"supersedes: SPEC-0003\n" +
		"owners: [~bigbes]\n" +
		"tags: [storage, review]\n" +
		"type: spec\n" +
		"summary: how proposals are stored\n"

	fm, err := ParseFrontmatter([]byte(full))
	if err != nil {
		t.Fatalf("ParseFrontmatter: %v", err)
	}
	if fm.ID != "SPEC-0007" || fm.Title != "Proposal storage model" || fm.Status != StatusDraft {
		t.Fatalf("unexpected frontmatter: %+v", fm)
	}
	if fm.Supersedes != "SPEC-0003" || fm.Type != "spec" || fm.Summary != "how proposals are stored" {
		t.Fatalf("unexpected frontmatter: %+v", fm)
	}
	if len(fm.Owners) != 1 || fm.Owners[0] != "~bigbes" {
		t.Fatalf("owners = %v", fm.Owners)
	}
	if len(fm.Tags) != 2 || fm.Tags[0] != "storage" {
		t.Fatalf("tags = %v", fm.Tags)
	}
	for _, key := range []string{"id", "title", "status", "supersedes", "owners", "tags", "type", "summary"} {
		if !fm.Has(key) {
			t.Errorf("Has(%q) = false, want true", key)
		}
	}
	if fm.Has("approved") {
		t.Error(`Has("approved") = true for a key that is not in the source`)
	}
}

func TestParseFrontmatterPresenceVsEmptiness(t *testing.T) {
	// `title:` with no value is present-but-blank, which is a different error
	// from an absent title. The distinction is the reason Present exists.
	fm, err := ParseFrontmatter([]byte("id: SPEC-1\ntitle:\n"))
	if err != nil {
		t.Fatalf("ParseFrontmatter: %v", err)
	}
	if !fm.Has("title") {
		t.Fatal(`Has("title") = false for a key that is present with a null value`)
	}
	if fm.Title != "" {
		t.Fatalf("Title = %q, want empty", fm.Title)
	}
	if fm.Has("status") {
		t.Fatal(`Has("status") = true for an absent key`)
	}
}

func TestParseFrontmatterUnknownKeysArePreserved(t *testing.T) {
	// Unknown keys are authored metadata we do not model; they must not fail
	// the parse, and they must be reportable so a schema can require them.
	fm, err := ParseFrontmatter([]byte("id: SPEC-1\nreviewers: [~someone]\n"))
	if err != nil {
		t.Fatalf("ParseFrontmatter: %v", err)
	}
	if !fm.Has("reviewers") {
		t.Fatal(`Has("reviewers") = false, want true`)
	}
}

func TestParseFrontmatterErrors(t *testing.T) {
	tests := []struct {
		name string
		in   string
		ok   bool
	}{
		{"empty block", "", true},
		{"whitespace only", "\n \n", true},
		{"comment only", "# nothing here\n", true},

		{"duplicate id", "id: SPEC-1\nid: SPEC-2\n", false},
		{"duplicate title far apart", "id: SPEC-1\ntitle: a\nstatus: draft\ntitle: b\n", false},
		{"sequence not mapping", "- id: SPEC-1\n", false},
		{"scalar not mapping", "just a string\n", false},
		{"tab indentation", "id: SPEC-1\n\ttitle: x\n", false},
		{"unclosed flow seq", "tags: [a, b\n", false},
		{"owners as scalar", "owners: bigbes\n", false},
		{"tags as mapping", "tags: {a: 1}\n", false},
		{"complex key", "? [a, b]\n: value\n", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			fm, err := ParseFrontmatter([]byte(tc.in))
			if (err == nil) != tc.ok {
				t.Fatalf("ParseFrontmatter(%q) = %+v, %v, want ok=%v", tc.in, fm, err, tc.ok)
			}
			if err != nil && !errors.Is(err, ErrMalformedFrontmatter) {
				t.Fatalf("error %v is not ErrMalformedFrontmatter", err)
			}
			if err == nil && fm.Present == nil {
				t.Fatal("Present must never be nil on a successful parse")
			}
		})
	}
}

func TestParseDocument(t *testing.T) {
	src := "---\nid: SPEC-0007\ntitle: T\nstatus: draft\n---\n\n# Heading\n\nbody\n"
	fm, body, err := ParseDocument([]byte(src))
	if err != nil {
		t.Fatalf("ParseDocument: %v", err)
	}
	if fm.ID != "SPEC-0007" || fm.Status != StatusDraft {
		t.Fatalf("frontmatter = %+v", fm)
	}
	if string(body) != "\n# Heading\n\nbody\n" {
		t.Fatalf("body = %q", body)
	}

	if _, _, err := ParseDocument([]byte("# no frontmatter\n")); !errors.Is(err, ErrMalformedFrontmatter) {
		t.Fatalf("ParseDocument without frontmatter = %v, want ErrMalformedFrontmatter", err)
	}
}

func TestSchemaValidate(t *testing.T) {
	tests := []struct {
		name string
		in   Schema
		ok   bool
	}{
		{"default", DefaultSchema(), true},
		{"narrowed status", Schema{Required: []string{"id"}, Status: []Status{StatusDraft}}, true},
		{"no required keys", Schema{Required: nil, Status: DefaultStatuses()}, true},
		{"custom required key", Schema{Required: []string{"id", "reviewers"}, Status: DefaultStatuses()}, true},

		{"empty status list", Schema{Required: []string{"id"}, Status: nil}, false},
		// A per-space config file must not be able to reintroduce "approved".
		{"approved status", Schema{Required: []string{"id"}, Status: []Status{StatusDraft, "approved"}}, false},
		{"unknown status", Schema{Required: []string{"id"}, Status: []Status{"wip"}}, false},
		{"duplicate status", Schema{Required: []string{"id"}, Status: []Status{StatusDraft, StatusDraft}}, false},
		{"empty required key", Schema{Required: []string{""}, Status: DefaultStatuses()}, false},
		{"duplicate required key", Schema{Required: []string{"id", "id"}, Status: DefaultStatuses()}, false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.in.Validate()
			if (err == nil) != tc.ok {
				t.Fatalf("Schema.Validate() = %v, want ok=%v", err, tc.ok)
			}
			if err != nil && !errors.Is(err, ErrInvalidPolicy) {
				t.Fatalf("error %v is not ErrInvalidPolicy", err)
			}
		})
	}
}

func TestSchemaValidateFrontmatter(t *testing.T) {
	tests := []struct {
		name    string
		schema  Schema
		front   string
		wantErr error // nil means "must succeed"
	}{
		{
			name:   "complete document",
			schema: DefaultSchema(),
			front:  "id: SPEC-0007\ntitle: Storage\nstatus: draft\n",
		},
		{
			name:   "optional keys absent",
			schema: DefaultSchema(),
			front:  "id: SPEC-0007\ntitle: Storage\nstatus: review\n",
		},
		{
			name:   "owners with and without tilde",
			schema: DefaultSchema(),
			front:  "id: SPEC-1\ntitle: T\nstatus: draft\nowners: [~bigbes, bigbes]\n",
		},
		{
			name:   "extra keys ignored",
			schema: DefaultSchema(),
			front:  "id: SPEC-1\ntitle: T\nstatus: draft\nreviewers: [~x]\n",
		},
		{
			name:    "missing status",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: T\n",
			wantErr: ErrMissingField,
		},
		{
			name:    "missing id",
			schema:  DefaultSchema(),
			front:   "title: T\nstatus: draft\n",
			wantErr: ErrMissingField,
		},
		{
			name:    "empty frontmatter",
			schema:  DefaultSchema(),
			front:   "",
			wantErr: ErrMissingField,
		},
		{
			name:    "blank title",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: \"   \"\nstatus: draft\n",
			wantErr: ErrMissingField,
		},
		{
			name:    "null id",
			schema:  DefaultSchema(),
			front:   "id:\ntitle: T\nstatus: draft\n",
			wantErr: ErrInvalidDocID,
		},
		{
			name:    "malformed id",
			schema:  DefaultSchema(),
			front:   "id: spec-7\ntitle: T\nstatus: draft\n",
			wantErr: ErrInvalidDocID,
		},
		{
			name:    "approved status rejected",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: T\nstatus: approved\n",
			wantErr: ErrInvalidStatus,
		},
		{
			name:    "status outside narrowed enum",
			schema:  Schema{Required: []string{"id", "title", "status"}, Status: []Status{StatusDraft}},
			front:   "id: SPEC-1\ntitle: T\nstatus: review\n",
			wantErr: ErrInvalidStatus,
		},
		{
			name:    "supersedes present but empty",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: T\nstatus: draft\nsupersedes:\n",
			wantErr: ErrInvalidDocID,
		},
		{
			name:    "supersedes malformed",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: T\nstatus: draft\nsupersedes: SPEC 3\n",
			wantErr: ErrInvalidDocID,
		},
		{
			name:    "bad owner",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: T\nstatus: draft\nowners: [~Big Bes]\n",
			wantErr: ErrInvalidName,
		},
		{
			name:    "empty owner entry",
			schema:  DefaultSchema(),
			front:   "id: SPEC-1\ntitle: T\nstatus: draft\nowners: ['']\n",
			wantErr: ErrInvalidName,
		},
		{
			name:    "custom required key missing",
			schema:  Schema{Required: []string{"id", "reviewers"}, Status: DefaultStatuses()},
			front:   "id: SPEC-1\ntitle: T\nstatus: draft\n",
			wantErr: ErrMissingField,
		},
		{
			name:   "status not required and absent",
			schema: Schema{Required: []string{"id"}, Status: DefaultStatuses()},
			front:  "id: SPEC-1\n",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			fm, err := ParseFrontmatter([]byte(tc.front))
			if err != nil {
				t.Fatalf("ParseFrontmatter(%q): %v", tc.front, err)
			}
			err = tc.schema.ValidateFrontmatter(fm)
			if tc.wantErr == nil {
				if err != nil {
					t.Fatalf("ValidateFrontmatter = %v, want nil", err)
				}
				return
			}
			if !errors.Is(err, tc.wantErr) {
				t.Fatalf("ValidateFrontmatter = %v, want %v", err, tc.wantErr)
			}
		})
	}
}

func TestSchemaAllowsStatus(t *testing.T) {
	s := DefaultSchema()
	for _, st := range DefaultStatuses() {
		if !s.AllowsStatus(st) {
			t.Errorf("default schema must allow %q", st)
		}
	}
	if s.AllowsStatus("approved") {
		t.Error(`default schema must not allow "approved"`)
	}
	if s.AllowsStatus("") {
		t.Error("default schema must not allow the empty status")
	}
}

func TestDefaultStatusesIsNotShared(t *testing.T) {
	a := DefaultStatuses()
	a[0] = "approved"
	if b := DefaultStatuses(); b[0] != StatusDraft {
		t.Fatalf("DefaultStatuses() leaked a shared slice: %v", b)
	}
}

func TestDefaultSchemaMatchesDesign(t *testing.T) {
	s := DefaultSchema()
	if got, want := strings.Join(s.Required, ","), "id,title,status"; got != want {
		t.Fatalf("default required = %q, want %q", got, want)
	}
	if got, want := joinStatuses(s.Status), "draft|review|superseded"; got != want {
		t.Fatalf("default status enum = %q, want %q", got, want)
	}
}

A core/id.go => core/id.go +101 -0
@@ 0,0 1,101 @@
package core

import (
	"fmt"
	"strings"
)

const (
	// MaxDocIDPrefixLen bounds the alphabetic part of an ID ("SPEC", "RFC").
	MaxDocIDPrefixLen = 16

	// MaxDocIDSeqLen bounds the numeric part. Eight digits is far past the
	// volume this service will ever see; the cap exists so a pathological ID
	// cannot become a pathological registry key.
	MaxDocIDSeqLen = 8
)

// DocID is a parsed document identifier such as "SPEC-0007".
//
// Seq is kept as a string rather than an int on purpose: leading zeros are part
// of how these IDs are written and read, and "SPEC-7" and "SPEC-0007" are
// therefore different IDs in a registry that is globally unique. Normalizing
// them to the same integer would silently merge two documents.
type DocID struct {
	Prefix string
	Seq    string
}

// String renders the canonical "PREFIX-SEQ" form.
func (d DocID) String() string { return d.Prefix + "-" + d.Seq }

// ParseDocID parses and validates a document ID. The grammar is deliberately
// tiny:
//
//	ID     = PREFIX "-" SEQ
//	PREFIX = [A-Z] [A-Z0-9]*        (1..MaxDocIDPrefixLen)
//	SEQ    = [0-9]+                 (1..MaxDocIDSeqLen)
//
// Exactly one '-' separates the two, so the split is unambiguous and IDs sort
// predictably. ASCII-only and uppercase-only is the load-bearing part: IDs are
// the global registry key, so a Cyrillic "С" or a lowercase "spec" must be a
// different-looking ID that is rejected outright rather than a homograph that
// quietly registers alongside the real one.
//
// Core validates shape only. Global uniqueness is enforced by the registry
// table, which is the only thing that can know about other spaces.
func ParseDocID(s string) (DocID, error) {
	if s == "" {
		return DocID{}, fmt.Errorf("%w: empty id", ErrInvalidDocID)
	}
	i := strings.IndexByte(s, '-')
	if i < 0 {
		return DocID{}, fmt.Errorf("%w: id %q has no '-' separator", ErrInvalidDocID, s)
	}
	prefix, seq := s[:i], s[i+1:]
	if strings.IndexByte(seq, '-') >= 0 {
		return DocID{}, fmt.Errorf("%w: id %q has more than one '-'", ErrInvalidDocID, s)
	}

	if prefix == "" {
		return DocID{}, fmt.Errorf("%w: id %q has an empty prefix", ErrInvalidDocID, s)
	}
	if len(prefix) > MaxDocIDPrefixLen {
		return DocID{}, fmt.Errorf("%w: id %q prefix is too long (%d > %d)",
			ErrInvalidDocID, s, len(prefix), MaxDocIDPrefixLen)
	}
	if !isUpperAlpha(prefix[0]) {
		return DocID{}, fmt.Errorf("%w: id %q prefix must start with A-Z", ErrInvalidDocID, s)
	}
	for i := 0; i < len(prefix); i++ {
		if c := prefix[i]; !isUpperAlpha(c) && !isDigit(c) {
			return DocID{}, fmt.Errorf("%w: id %q prefix contains disallowed byte %q", ErrInvalidDocID, s, c)
		}
	}

	if seq == "" {
		return DocID{}, fmt.Errorf("%w: id %q has an empty sequence", ErrInvalidDocID, s)
	}
	if len(seq) > MaxDocIDSeqLen {
		return DocID{}, fmt.Errorf("%w: id %q sequence is too long (%d > %d)",
			ErrInvalidDocID, s, len(seq), MaxDocIDSeqLen)
	}
	for i := 0; i < len(seq); i++ {
		if !isDigit(seq[i]) {
			return DocID{}, fmt.Errorf("%w: id %q sequence contains disallowed byte %q", ErrInvalidDocID, s, seq[i])
		}
	}

	return DocID{Prefix: prefix, Seq: seq}, nil
}

// ValidateDocID reports whether s is a well-formed document ID, discarding the
// parse. Use it where only the verdict matters (frontmatter schema checks, push
// validation).
func ValidateDocID(s string) error {
	_, err := ParseDocID(s)
	return err
}

func isUpperAlpha(c byte) bool { return c >= 'A' && c <= 'Z' }
func isDigit(c byte) bool      { return c >= '0' && c <= '9' }

A core/id_test.go => core/id_test.go +101 -0
@@ 0,0 1,101 @@
package core

import (
	"errors"
	"strings"
	"testing"
)

func TestParseDocID(t *testing.T) {
	tests := []struct {
		name       string
		in         string
		wantPrefix string
		wantSeq    string
		ok         bool
	}{
		{"spec", "SPEC-0007", "SPEC", "0007", true},
		{"rfc", "RFC-1", "RFC", "1", true},
		{"single letter prefix", "X-42", "X", "42", true},
		{"digits in prefix", "C4-0001", "C4", "0001", true},
		{"all zeros", "SPEC-0000", "SPEC", "0000", true},
		{"max prefix", strings.Repeat("A", MaxDocIDPrefixLen) + "-1", strings.Repeat("A", MaxDocIDPrefixLen), "1", true},
		{"max seq", "SPEC-" + strings.Repeat("9", MaxDocIDSeqLen), "SPEC", strings.Repeat("9", MaxDocIDSeqLen), true},

		{"empty", "", "", "", false},
		{"no separator", "SPEC0007", "", "", false},
		{"two separators", "HOME-OPS-0001", "", "", false},
		{"double dash", "SPEC--1", "", "", false},
		{"empty prefix", "-0007", "", "", false},
		{"empty seq", "SPEC-", "", "", false},
		{"lowercase prefix", "spec-0007", "", "", false},
		{"mixed case prefix", "Spec-0007", "", "", false},
		{"digit-leading prefix", "0SPEC-1", "", "", false},
		{"underscore in prefix", "SPEC_A-1", "", "", false},
		{"dot in prefix", "SPEC.A-1", "", "", false},
		{"non-digit seq", "SPEC-007a", "", "", false},
		{"signed seq", "SPEC-+1", "", "", false},
		{"hex seq", "SPEC-0x7", "", "", false},
		{"prefix too long", strings.Repeat("A", MaxDocIDPrefixLen+1) + "-1", "", "", false},
		{"seq too long", "SPEC-" + strings.Repeat("9", MaxDocIDSeqLen+1), "", "", false},
		{"leading space", " SPEC-0007", "", "", false},
		{"trailing space", "SPEC-0007 ", "", "", false},
		{"inner space", "SPEC - 0007", "", "", false},
		{"newline", "SPEC-0007\n", "", "", false},
		// Homograph: Cyrillic С/Р/Е/С look identical to Latin in most fonts.
		// Accepting them would let two visually identical IDs both register.
		{"cyrillic homograph", "СПЕС-0007", "", "", false},
		{"fullwidth digits", "SPEC-0007", "", "", false},
		{"arabic-indic digits", "SPEC-٠٠٠٧", "", "", false},
		{"path injection", "SPEC-0007/../x", "", "", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			id, err := ParseDocID(tc.in)
			if (err == nil) != tc.ok {
				t.Fatalf("ParseDocID(%q) = %+v, %v, want ok=%v", tc.in, id, err, tc.ok)
			}
			if !tc.ok {
				if !errors.Is(err, ErrInvalidDocID) {
					t.Fatalf("error %v is not ErrInvalidDocID", err)
				}
				return
			}
			if id.Prefix != tc.wantPrefix || id.Seq != tc.wantSeq {
				t.Fatalf("ParseDocID(%q) = %+v, want {%q %q}", tc.in, id, tc.wantPrefix, tc.wantSeq)
			}
			if got := id.String(); got != tc.in {
				t.Fatalf("String() = %q, want round-trip %q", got, tc.in)
			}
		})
	}
}

// Leading zeros are significant: the registry is global and "SPEC-7" is a
// different key from "SPEC-0007". Normalizing them together would silently
// merge two documents.
func TestDocIDLeadingZerosAreSignificant(t *testing.T) {
	a, err := ParseDocID("SPEC-7")
	if err != nil {
		t.Fatal(err)
	}
	b, err := ParseDocID("SPEC-0007")
	if err != nil {
		t.Fatal(err)
	}
	if a == b {
		t.Fatalf("SPEC-7 and SPEC-0007 must not compare equal, got %+v", a)
	}
	if a.String() == b.String() {
		t.Fatal("SPEC-7 and SPEC-0007 must not render identically")
	}
}

func TestValidateDocID(t *testing.T) {
	if err := ValidateDocID("SPEC-0007"); err != nil {
		t.Fatalf("ValidateDocID(SPEC-0007) = %v, want nil", err)
	}
	if err := ValidateDocID("spec-0007"); !errors.Is(err, ErrInvalidDocID) {
		t.Fatalf("ValidateDocID(spec-0007) = %v, want ErrInvalidDocID", err)
	}
}

A core/names.go => core/names.go +220 -0
@@ 0,0 1,220 @@
package core

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

const (
	// MaxOwnerLen bounds owner names. meta.sr.ht already bounds them at
	// registration, so this is a sanity cap rather than the authority.
	MaxOwnerLen = 64

	// MaxSpaceNameLen bounds space names. A space name becomes a directory
	// under the repos root and a URL segment, so it stays short.
	MaxSpaceNameLen = 100

	// MaxPathLen bounds a document or attachment path within a space. Well
	// under any filesystem limit; the point is to keep a hostile path out of
	// the index and the render cache, not to be permissive.
	MaxPathLen = 512
)

const (
	// DocExt is the only extension a document may carry. Matched
	// case-sensitively: git trees are case-sensitive, so accepting ".MD" would
	// create documents that the renderer and indexer disagree about.
	DocExt = ".md"

	// PolicyFile is the space policy, versioned in the space itself so that
	// policy changes are reviewable like any other change.
	PolicyFile = ".spec.yml"
)

// SpaceRef identifies a space: one bare git repo, owned by one user. Both
// fields are stored without decoration — Owner never carries the leading '~'.
type SpaceRef struct {
	Owner string
	Name  string
}

// String renders the canonical URL and on-disk form, "~owner/name".
func (r SpaceRef) String() string { return "~" + r.Owner + "/" + r.Name }

// isNameByte reports whether c is allowed in an owner or space name: lowercase
// alphanumerics plus '_', '-' and '.'. '/' is deliberately excluded, so a name
// can never span path components.
func isNameByte(c byte) bool {
	switch {
	case c >= 'a' && c <= 'z':
		return true
	case c >= '0' && c <= '9':
		return true
	case c == '_' || c == '-' || c == '.':
		return true
	default:
		return false
	}
}

// validateName holds the rules shared by owners and spaces: non-empty, within
// the allowed byte set, not starting with '-' (which would read as an option to
// anything shelling out) and containing no ".." (path traversal, since both
// kinds of name become a path segment under the repos root).
func validateName(kind, s string, maxLen int) error {
	if s == "" {
		return fmt.Errorf("%w: empty %s", ErrInvalidName, kind)
	}
	if len(s) > maxLen {
		return fmt.Errorf("%w: %s %q is too long (%d > %d)", ErrInvalidName, kind, s, len(s), maxLen)
	}
	if s[0] == '-' {
		return fmt.Errorf("%w: %s %q must not start with '-'", ErrInvalidName, kind, s)
	}
	if strings.Contains(s, "..") {
		return fmt.Errorf("%w: %s %q must not contain '..'", ErrInvalidName, kind, s)
	}
	// '.' is in the allowed byte set, so the bare current-directory name has to
	// be excluded by hand: a space named "." would resolve to the repos root.
	if s == "." {
		return fmt.Errorf("%w: %s %q is not allowed", ErrInvalidName, kind, s)
	}
	for i := 0; i < len(s); i++ {
		if !isNameByte(s[i]) {
			return fmt.Errorf("%w: %s %q contains disallowed byte %q", ErrInvalidName, kind, s, s[i])
		}
	}
	return nil
}

// ValidateOwner reports whether s is a well-formed sourcehut owner name (the
// part after '~' in a URL). Callers must strip the leading '~' first.
func ValidateOwner(s string) error { return validateName("owner", s, MaxOwnerLen) }

// ValidateSpaceName reports whether s is a well-formed space name — the same
// character family as an owner, capped at MaxSpaceNameLen.
func ValidateSpaceName(s string) error { return validateName("space", s, MaxSpaceNameLen) }

// ParseSpaceRef parses "~owner/name" (or "owner/name") into a validated
// SpaceRef. Surrounding slashes are tolerated because the same string arrives
// both as a URL path and as a config value, but anything else that does not
// split into exactly two non-empty segments is rejected rather than repaired.
func ParseSpaceRef(s string) (SpaceRef, error) {
	trimmed := strings.Trim(s, "/")
	if trimmed == "" {
		return SpaceRef{}, fmt.Errorf("%w: empty space reference", ErrInvalidName)
	}
	segs := strings.Split(trimmed, "/")
	if len(segs) != 2 {
		return SpaceRef{}, fmt.Errorf("%w: space reference %q must have exactly 2 segments, got %d",
			ErrInvalidName, s, len(segs))
	}
	ref := SpaceRef{Owner: strings.TrimPrefix(segs[0], "~"), Name: segs[1]}
	if err := ValidateOwner(ref.Owner); err != nil {
		return SpaceRef{}, err
	}
	if err := ValidateSpaceName(ref.Name); err != nil {
		return SpaceRef{}, err
	}
	return ref, nil
}

// badPathRune reports whether r must never appear in a path. Two families:
// control characters, which git tolerates in a tree entry but which corrupt
// logs, JSON and the index; and the Unicode bidirectional overrides, which can
// make a path render in the review UI as something other than what will be
// committed. Reviewing agent output is the product, so a path that lies about
// itself on screen is a correctness bug, not a nicety.
func badPathRune(r rune) bool {
	if r < 0x20 || r == 0x7f {
		return true
	}
	switch r {
	case 0x200e, 0x200f, // LRM, RLM
		0x202a, 0x202b, 0x202c, 0x202d, 0x202e, // LRE, RLE, PDF, LRO, RLO
		0x2066, 0x2067, 0x2068, 0x2069: // LRI, RLI, FSI, PDI
		return true
	}
	return false
}

// ValidatePath reports whether p is a safe relative path inside a space, usable
// as a git tree path for a document or an attachment. The rules:
//
//   - non-empty and no longer than MaxPathLen;
//   - valid UTF-8, no control characters, no bidi overrides (see badPathRune);
//   - relative: no leading '/', no trailing '/';
//   - no backslashes — on a git tree a '\' is an ordinary filename byte, so
//     accepting it produces paths that mean different things to different
//     clients;
//   - no empty, "." or ".." components: traversal, and the whole point of this
//     function;
//   - no ".git" component, which git refuses to track and which is the classic
//     checkout-escape vector;
//   - no component ending in '.' or ' ', which are invisible on screen and
//     therefore an easy way to shadow an existing document.
//
// ValidatePath deliberately allows dotfiles (".spec.yml" is one) and non-ASCII
// letters (specs here are written in Russian as well as English).
func ValidatePath(p string) error {
	if p == "" {
		return fmt.Errorf("%w: empty path", ErrInvalidPath)
	}
	if len(p) > MaxPathLen {
		return fmt.Errorf("%w: path is too long (%d > %d)", ErrInvalidPath, len(p), MaxPathLen)
	}
	if !utf8.ValidString(p) {
		return fmt.Errorf("%w: path is not valid UTF-8", ErrInvalidPath)
	}
	for _, r := range p {
		if badPathRune(r) {
			return fmt.Errorf("%w: path %q contains disallowed rune %U", ErrInvalidPath, p, r)
		}
	}
	if strings.HasPrefix(p, "/") {
		return fmt.Errorf("%w: path %q must be relative", ErrInvalidPath, p)
	}
	if strings.HasSuffix(p, "/") {
		return fmt.Errorf("%w: path %q must not end in '/'", ErrInvalidPath, p)
	}
	if strings.Contains(p, `\`) {
		return fmt.Errorf("%w: path %q must not contain a backslash", ErrInvalidPath, p)
	}
	for _, comp := range strings.Split(p, "/") {
		switch comp {
		case "":
			return fmt.Errorf("%w: path %q has an empty component", ErrInvalidPath, p)
		case ".", "..":
			return fmt.Errorf("%w: path %q has a traversal component %q", ErrInvalidPath, p, comp)
		case ".git":
			return fmt.Errorf("%w: path %q has a %q component", ErrInvalidPath, p, comp)
		}
		if strings.HasSuffix(comp, ".") || strings.HasSuffix(comp, " ") {
			return fmt.Errorf("%w: path %q component %q ends in '.' or a space", ErrInvalidPath, p, comp)
		}
	}
	return nil
}

// ValidateDocPath reports whether p is a valid path for a markdown document:
// everything ValidatePath requires, plus a ".md" extension on a non-empty base
// name. The extension carries meaning here — it is what tells the indexer and
// the renderer that a blob is a document rather than an attachment — so a
// document named exactly ".md" is rejected as having no name at all.
func ValidateDocPath(p string) error {
	if err := ValidatePath(p); err != nil {
		return err
	}
	if !strings.HasSuffix(p, DocExt) {
		return fmt.Errorf("%w: document path %q must end in %q", ErrInvalidPath, p, DocExt)
	}
	base := p
	if i := strings.LastIndex(p, "/"); i >= 0 {
		base = p[i+1:]
	}
	if base == DocExt {
		return fmt.Errorf("%w: document path %q has an empty base name", ErrInvalidPath, p)
	}
	return nil
}

A core/names_test.go => core/names_test.go +216 -0
@@ 0,0 1,216 @@
package core

import (
	"errors"
	"strings"
	"testing"
)

func TestValidateOwner(t *testing.T) {
	tests := []struct {
		in string
		ok bool
	}{
		{"bigbes", true},
		{"user_name", true},
		{"user-name", true},
		{"user.name", true},
		{"u123", true},
		{"a", true},
		{strings.Repeat("a", MaxOwnerLen), true},

		{"", false},
		{strings.Repeat("a", MaxOwnerLen+1), false},
		{"-leading", false},
		{"~bigbes", false}, // caller must strip the '~' first
		{"has/slash", false},
		{"has..dots", false},
		{"..", false},
		{"Upper", false},
		{"has space", false},
		{"tilde~", false},
		{"emoji😀", false},
		{"привет", false},
		{"nul\x00byte", false},
	}
	for _, tc := range tests {
		err := ValidateOwner(tc.in)
		if (err == nil) != tc.ok {
			t.Errorf("ValidateOwner(%q) = %v, want ok=%v", tc.in, err, tc.ok)
		}
		if err != nil && !errors.Is(err, ErrInvalidName) {
			t.Errorf("ValidateOwner(%q) error %v is not ErrInvalidName", tc.in, err)
		}
	}
}

func TestValidateSpaceName(t *testing.T) {
	tests := []struct {
		in string
		ok bool
	}{
		{"rfcs", true},
		{"tarantool-rfcs", true},
		{"home_ops", true},
		{"v1.0", true},
		{strings.Repeat("a", MaxSpaceNameLen), true},

		{"", false},
		{strings.Repeat("a", MaxSpaceNameLen+1), false},
		{"-dashfirst", false},
		{"a/b", false},
		{"a..b", false},
		{".", false},  // single dot is a legal byte set but "." alone is a directory
		{"..", false}, // traversal
		{"CamelCase", false},
		{"+everything", false}, // project namespace, not a space name
	}
	for _, tc := range tests {
		err := ValidateSpaceName(tc.in)
		if (err == nil) != tc.ok {
			t.Errorf("ValidateSpaceName(%q) = %v, want ok=%v", tc.in, err, tc.ok)
		}
	}
}

func TestValidateSpaceNameRejectsBareDot(t *testing.T) {
	// "." passes the byte-set rule, so it must be caught somewhere: a space
	// literally named "." would resolve to the repos root.
	if err := ValidateSpaceName("."); err == nil {
		t.Fatal(`ValidateSpaceName(".") must fail: it would resolve to the repos root`)
	}
}

func TestParseSpaceRef(t *testing.T) {
	tests := []struct {
		name      string
		in        string
		wantOwner string
		wantName  string
		ok        bool
	}{
		{"tilde form", "~bigbes/rfcs", "bigbes", "rfcs", true},
		{"bare form", "bigbes/rfcs", "bigbes", "rfcs", true},
		{"leading slash", "/~bigbes/rfcs", "bigbes", "rfcs", true},
		{"trailing slash", "~bigbes/rfcs/", "bigbes", "rfcs", true},

		{"empty", "", "", "", false},
		{"slashes only", "///", "", "", false},
		{"one segment", "~bigbes", "", "", false},
		{"three segments", "~bigbes/rfcs/docs", "", "", false},
		{"empty owner", "~/rfcs", "", "", false},
		{"empty name", "~bigbes/", "", "", false},
		{"traversal owner", "~../rfcs", "", "", false},
		{"traversal name", "~bigbes/..", "", "", false},
		{"uppercase owner", "~BigBes/rfcs", "", "", false},
		{"space in name", "~bigbes/my rfcs", "", "", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ref, err := ParseSpaceRef(tc.in)
			if (err == nil) != tc.ok {
				t.Fatalf("ParseSpaceRef(%q) = %v, %v, want ok=%v", tc.in, ref, err, tc.ok)
			}
			if !tc.ok {
				if !errors.Is(err, ErrInvalidName) {
					t.Fatalf("error %v is not ErrInvalidName", err)
				}
				return
			}
			if ref.Owner != tc.wantOwner || ref.Name != tc.wantName {
				t.Fatalf("ParseSpaceRef(%q) = %+v, want {%q %q}", tc.in, ref, tc.wantOwner, tc.wantName)
			}
			if got, want := ref.String(), "~"+tc.wantOwner+"/"+tc.wantName; got != want {
				t.Fatalf("String() = %q, want %q", got, want)
			}
		})
	}
}

func TestValidatePath(t *testing.T) {
	tests := []struct {
		name string
		in   string
		ok   bool
	}{
		{"simple", "specs/0007-storage.md", true},
		{"root file", "README.md", true},
		{"dotfile", ".spec.yml", true},
		{"deep", "a/b/c/d/e.md", true},
		{"cyrillic", "спеки/хранилище.md", true},
		{"attachment", "assets/diagram.png", true},
		{"dot in name", "v1.2.3.md", true},
		{"leading dot component", "notes/.private.md", true},
		{"max length", strings.Repeat("a", MaxPathLen), true},

		{"empty", "", false},
		{"too long", strings.Repeat("a", MaxPathLen+1), false},
		{"absolute", "/etc/passwd", false},
		{"trailing slash", "specs/", false},
		{"parent traversal", "../etc/passwd", false},
		{"embedded traversal", "specs/../../etc/passwd", false},
		{"interior traversal", "specs/../notes/a.md", false},
		{"dot component", "./a.md", false},
		{"interior dot component", "a/./b.md", false},
		{"empty component", "specs//a.md", false},
		{"git dir", ".git/config", false},
		{"nested git dir", "specs/.git/HEAD", false},
		{"backslash", `specs\a.md`, false},
		{"windows traversal", `..\a.md`, false},
		{"nul byte", "specs/a\x00.md", false},
		{"newline", "specs/a\nb.md", false},
		{"tab", "specs/a\tb.md", false},
		{"del", "specs/a\x7f.md", false},
		{"rtl override", "specs/exe\u202egnp.md", false},
		{"rtl mark", "specs/a\u200fb.md", false},
		{"pop directional isolate", "specs/a\u2069b.md", false},
		{"invalid utf-8", "specs/\xff\xfe.md", false},
		{"trailing dot component", "specs/a./b.md", false},
		{"trailing space component", "specs/a /b.md", false},
		{"trailing space at end", "specs/a.md ", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidatePath(tc.in)
			if (err == nil) != tc.ok {
				t.Fatalf("ValidatePath(%q) = %v, want ok=%v", tc.in, err, tc.ok)
			}
			if err != nil && !errors.Is(err, ErrInvalidPath) {
				t.Fatalf("error %v is not ErrInvalidPath", err)
			}
		})
	}
}

func TestValidateDocPath(t *testing.T) {
	tests := []struct {
		name string
		in   string
		ok   bool
	}{
		{"spec", "specs/0007-storage.md", true},
		{"root", "README.md", true},
		{"hidden doc", "notes/.draft.md", true},
		{"cyrillic", "спеки/хранилище.md", true},

		{"no extension", "specs/0007-storage", false},
		{"wrong extension", "specs/0007-storage.markdown", false},
		{"uppercase extension", "specs/0007-storage.MD", false},
		{"extension only", ".md", false},
		{"extension only in dir", "specs/.md", false},
		{"extension in middle", "specs/a.md.txt", false},
		{"traversal wins", "../a.md", false},
		{"empty", "", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidateDocPath(tc.in)
			if (err == nil) != tc.ok {
				t.Fatalf("ValidateDocPath(%q) = %v, want ok=%v", tc.in, err, tc.ok)
			}
			if err != nil && !errors.Is(err, ErrInvalidPath) {
				t.Fatalf("error %v is not ErrInvalidPath", err)
			}
		})
	}
}

A core/policy.go => core/policy.go +240 -0
@@ 0,0 1,240 @@
package core

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"strings"
	"unicode/utf8"

	"gopkg.in/yaml.v3"
)

// Policy is a space's `.spec.yml`, versioned in the space itself so that policy
// changes are reviewable like any other change.
//
//	review:
//	  auto_merge: [notes/**, reports/**]
//	schema:
//	  required: [id, title, status]
//	  status: [draft, review, superseded]
type Policy struct {
	Review ReviewPolicy `yaml:"review"`
	Schema Schema       `yaml:"schema"`
}

// ReviewPolicy carries the one real review knob. Single-user means the owner is
// the only approver, so there is no approver list and no approval count — only
// which paths skip the gate.
type ReviewPolicy struct {
	// AutoMerge lists path patterns whose proposals land immediately. This is
	// what implements the bimodal cadence: `specs/` waits for a human, `notes/`
	// and `reports/` flow through.
	AutoMerge []string `yaml:"auto_merge"`
}

// DefaultPolicy is what a space with no `.spec.yml` gets: the house frontmatter
// contract and nothing auto-merged. Defaulting auto_merge to empty is the
// fail-closed direction — a space that has not said anything about review must
// not be quietly laundering unreviewed agent output onto the approved branch.
func DefaultPolicy() Policy {
	return Policy{Schema: DefaultSchema()}
}

// ParsePolicy parses and validates a `.spec.yml`.
//
// Unknown keys are rejected: a `auto-merge:` typo would otherwise parse fine
// and silently do nothing, and a policy file that lies about what it enforces
// is worse than one that fails to load.
//
// A section the file omits falls back to DefaultPolicy. An omitted `schema:`
// means "the house contract", not "a space where no status is allowed and
// therefore no document can ever validate". An explicitly empty list
// (`required: []`) is honoured as written — YAML distinguishes it from an
// absent key by nil-ness, and a space that deliberately turns off required-key
// checking must be able to say so.
func ParsePolicy(data []byte) (Policy, error) {
	var p Policy
	dec := yaml.NewDecoder(bytes.NewReader(data))
	dec.KnownFields(true)
	if err := dec.Decode(&p); err != nil {
		// An empty or comment-only file decodes to io.EOF. That is a valid way
		// to say "defaults", unlike a file that fails to parse.
		if !errors.Is(err, io.EOF) {
			return Policy{}, fmt.Errorf("%w: %v", ErrInvalidPolicy, err)
		}
		return DefaultPolicy(), nil
	}
	// A second YAML document would be silently ignored, so a policy split over
	// two '---' blocks would enforce only half of what it says.
	if err := dec.Decode(new(Policy)); !errors.Is(err, io.EOF) {
		return Policy{}, fmt.Errorf("%w: %s must contain exactly one YAML document", ErrInvalidPolicy, PolicyFile)
	}

	def := DefaultPolicy()
	if p.Schema.Required == nil {
		p.Schema.Required = def.Schema.Required
	}
	if p.Schema.Status == nil {
		p.Schema.Status = def.Schema.Status
	}

	if err := p.Validate(); err != nil {
		return Policy{}, err
	}
	return p, nil
}

// Validate reports whether the policy is internally usable: every auto_merge
// pattern well-formed, and the schema itself well-formed.
func (p Policy) Validate() error {
	for _, pat := range p.Review.AutoMerge {
		if err := ValidatePattern(pat); err != nil {
			return fmt.Errorf("%w: review.auto_merge: %v", ErrInvalidPolicy, err)
		}
	}
	return p.Schema.Validate()
}

// AutoMerges reports whether path may skip human review under this policy.
//
// Fail-closed: a path that does not match, or that is not a valid path at all,
// means "needs a human". The cost of a wrong false is one click; the cost of a
// wrong true is unreviewed agent output landing on the approved branch, which
// is the exact failure this service exists to prevent.
func (p Policy) AutoMerges(path string) bool {
	if err := ValidatePath(path); err != nil {
		return false
	}
	for _, pat := range p.Review.AutoMerge {
		if MatchPattern(pat, path) {
			return true
		}
	}
	return false
}

// ValidatePattern reports whether pat is a well-formed auto_merge pattern.
// The grammar is the familiar globstar subset, and nothing more:
//
//	**   as a whole component, matches zero or more path components
//	*    matches any run of characters within one component, never '/'
//	?    matches exactly one character within one component
//
// Bracket expressions are deliberately absent, so '[' is a literal. Structural
// rules mirror ValidatePath (relative, no empty or traversal components), plus
// one of its own: "**" must be an entire component, because "a**b" has no
// obvious meaning and guessing one is how a policy comes to mean something its
// author did not intend.
func ValidatePattern(pat string) error {
	if pat == "" {
		return fmt.Errorf("%w: empty pattern", ErrInvalidPattern)
	}
	if len(pat) > MaxPathLen {
		return fmt.Errorf("%w: pattern is too long (%d > %d)", ErrInvalidPattern, len(pat), MaxPathLen)
	}
	if !utf8.ValidString(pat) {
		return fmt.Errorf("%w: pattern is not valid UTF-8", ErrInvalidPattern)
	}
	for _, r := range pat {
		if badPathRune(r) {
			return fmt.Errorf("%w: pattern %q contains disallowed rune %U", ErrInvalidPattern, pat, r)
		}
	}
	if strings.HasPrefix(pat, "/") {
		return fmt.Errorf("%w: pattern %q must be relative", ErrInvalidPattern, pat)
	}
	if strings.HasSuffix(pat, "/") {
		return fmt.Errorf("%w: pattern %q must not end in '/' (write %q to match a subtree)",
			ErrInvalidPattern, pat, pat+"**")
	}
	if strings.Contains(pat, `\`) {
		return fmt.Errorf("%w: pattern %q must not contain a backslash", ErrInvalidPattern, pat)
	}
	for _, comp := range strings.Split(pat, "/") {
		switch comp {
		case "":
			return fmt.Errorf("%w: pattern %q has an empty component", ErrInvalidPattern, pat)
		case ".", "..":
			return fmt.Errorf("%w: pattern %q has a traversal component %q", ErrInvalidPattern, pat, comp)
		case "**":
			continue
		}
		if strings.Contains(comp, "**") {
			return fmt.Errorf("%w: pattern %q: %q must be a whole path component", ErrInvalidPattern, pat, "**")
		}
	}
	return nil
}

// MatchPattern reports whether path matches pat under the grammar documented on
// ValidatePattern. An invalid pattern matches nothing; Policy.Validate rejects
// those up front, so a live policy never contains one.
//
// Note that "**" matching zero components means "notes/**" also matches the
// bare path "notes". That is the price of "**/*.md" matching a document at the
// space root, which is the case that actually comes up.
func MatchPattern(pat, path string) bool {
	if ValidatePattern(pat) != nil {
		return false
	}
	return matchComponents(strings.Split(pat, "/"), strings.Split(path, "/"))
}

func matchComponents(pats, segs []string) bool {
	for len(pats) > 0 {
		if pats[0] == "**" {
			if len(pats) == 1 {
				return true
			}
			for i := 0; i <= len(segs); i++ {
				if matchComponents(pats[1:], segs[i:]) {
					return true
				}
			}
			return false
		}
		if len(segs) == 0 {
			return false
		}
		if !matchSegment(pats[0], segs[0]) {
			return false
		}
		pats, segs = pats[1:], segs[1:]
	}
	return len(segs) == 0
}

// matchSegment matches one path component against one pattern component using
// '*' and '?'. Iterative with a single backtrack point, so a pathological
// pattern such as "*a*a*a*a*" stays linear-ish instead of exponential — these
// patterns come from a config file, but the config file is versioned content
// that an agent can propose.
func matchSegment(pat, s string) bool {
	p := []rune(pat)
	t := []rune(s)
	var pi, ti, resume int
	star := -1
	for ti < len(t) {
		switch {
		case pi < len(p) && (p[pi] == '?' || p[pi] == t[ti]):
			pi++
			ti++
		case pi < len(p) && p[pi] == '*':
			star = pi
			resume = ti
			pi++
		case star >= 0:
			resume++
			pi = star + 1
			ti = resume
		default:
			return false
		}
	}
	for pi < len(p) && p[pi] == '*' {
		pi++
	}
	return pi == len(p)
}

A core/policy_test.go => core/policy_test.go +321 -0
@@ 0,0 1,321 @@
package core

import (
	"errors"
	"strings"
	"testing"
)

func TestValidatePattern(t *testing.T) {
	tests := []struct {
		name string
		in   string
		ok   bool
	}{
		{"subtree", "notes/**", true},
		{"literal", "specs/0007.md", true},
		{"star in component", "specs/*.md", true},
		{"question mark", "specs/000?.md", true},
		{"leading globstar", "**/*.md", true},
		{"interior globstar", "notes/**/drafts/*.md", true},
		{"bare globstar", "**", true},
		{"bare star", "*", true},
		{"cyrillic", "спеки/**", true},
		{"bracket is literal", "specs/[draft].md", true},

		{"empty", "", false},
		{"too long", strings.Repeat("a", MaxPathLen+1), false},
		{"absolute", "/notes/**", false},
		{"trailing slash", "notes/", false},
		{"empty component", "notes//**", false},
		{"traversal", "../notes/**", false},
		{"interior traversal", "notes/../specs/**", false},
		{"dot component", "./notes/**", false},
		{"backslash", `notes\**`, false},
		// "a**b" has no obvious meaning; guessing one is how a policy comes to
		// mean something its author did not intend.
		{"globstar not whole component", "notes**", false},
		{"globstar prefixed", "**notes/x", false},
		{"globstar suffixed", "notes/**x", false},
		{"triple star", "notes/***", false},
		{"nul byte", "notes/\x00**", false},
		{"newline", "notes/**\n", false},
		{"rtl override", "notes/\u202e**", false},
		{"invalid utf-8", "notes/\xff\xfe", false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidatePattern(tc.in)
			if (err == nil) != tc.ok {
				t.Fatalf("ValidatePattern(%q) = %v, want ok=%v", tc.in, err, tc.ok)
			}
			if err != nil && !errors.Is(err, ErrInvalidPattern) {
				t.Fatalf("error %v is not ErrInvalidPattern", err)
			}
		})
	}
}

func TestMatchPattern(t *testing.T) {
	tests := []struct {
		pat  string
		path string
		want bool
	}{
		// Subtree matching, the shape the design's example uses.
		{"notes/**", "notes/a.md", true},
		{"notes/**", "notes/2026/07/a.md", true},
		{"notes/**", "notes", true}, // "**" matches zero components; documented
		{"notes/**", "notesx/a.md", false},
		{"notes/**", "specs/a.md", false},
		{"notes/**", "a/notes/b.md", false},
		{"reports/**", "reports/weekly/2026-07.md", true},

		// "*" never crosses a component boundary.
		{"specs/*.md", "specs/0007.md", true},
		{"specs/*.md", "specs/sub/0007.md", false},
		{"specs/*", "specs/0007.md", true},
		{"specs/*", "specs", false},
		{"*", "a.md", true},
		{"*", "a/b.md", false},
		{"*.md", "a.md", true},
		{"*.md", "a.markdown", false},

		// "?" is exactly one character, counted in runes.
		{"specs/000?.md", "specs/0007.md", true},
		{"specs/000?.md", "specs/00007.md", false},
		{"?.md", "я.md", true},
		{"??.md", "я.md", false},

		// "**" in the middle and at the front.
		{"**/*.md", "a.md", true},
		{"**/*.md", "notes/a.md", true},
		{"**/*.md", "a/b/c.md", true},
		{"**/*.md", "a/b/c.png", false},
		{"notes/**/*.md", "notes/a.md", true},
		{"notes/**/*.md", "notes/2026/a.md", true},
		{"notes/**/drafts/*.md", "notes/x/y/drafts/a.md", true},
		{"notes/**/drafts/*.md", "notes/drafts/a.md", true},
		{"notes/**/drafts/*.md", "notes/drafts/sub/a.md", false},
		{"**", "anything/at/all.md", true},

		// Backtracking within a component.
		{"a*b*c", "abc", true},
		{"a*b*c", "axxbxxc", true},
		{"a*b*c", "axxbxx", false},
		{"*a*a*a*a*b", strings.Repeat("a", 40), false},

		// Literals, including the deliberately unsupported bracket syntax.
		{"specs/0007.md", "specs/0007.md", true},
		{"specs/0007.md", "specs/0008.md", false},
		{"specs/[ab].md", "specs/a.md", false},
		{"specs/[ab].md", "specs/[ab].md", true},

		// An invalid pattern matches nothing rather than everything.
		{"", "a.md", false},
		{"/notes/**", "notes/a.md", false},
		{"notes**", "notes/a.md", false},
		{"../**", "a.md", false},
	}
	for _, tc := range tests {
		t.Run(tc.pat+" ~ "+tc.path, func(t *testing.T) {
			if got := MatchPattern(tc.pat, tc.path); got != tc.want {
				t.Fatalf("MatchPattern(%q, %q) = %v, want %v", tc.pat, tc.path, got, tc.want)
			}
		})
	}
}

func TestParsePolicy(t *testing.T) {
	const full = `review:
  auto_merge: [notes/**, reports/**]
schema:
  required: [id, title, status]
  status: [draft, review, superseded]
`
	p, err := ParsePolicy([]byte(full))
	if err != nil {
		t.Fatalf("ParsePolicy: %v", err)
	}
	if got, want := strings.Join(p.Review.AutoMerge, ","), "notes/**,reports/**"; got != want {
		t.Fatalf("auto_merge = %q, want %q", got, want)
	}
	if got, want := joinStatuses(p.Schema.Status), "draft|review|superseded"; got != want {
		t.Fatalf("schema.status = %q, want %q", got, want)
	}
	if got, want := strings.Join(p.Schema.Required, ","), "id,title,status"; got != want {
		t.Fatalf("schema.required = %q, want %q", got, want)
	}
}

func TestParsePolicyDefaults(t *testing.T) {
	tests := []struct {
		name string
		in   string
	}{
		{"empty file", ""},
		{"comment only", "# nothing configured yet\n"},
		{"review section only", "review:\n  auto_merge: [notes/**]\n"},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			p, err := ParsePolicy([]byte(tc.in))
			if err != nil {
				t.Fatalf("ParsePolicy: %v", err)
			}
			// An omitted schema means the house contract, not "no status is
			// allowed and therefore no document can ever validate".
			if got, want := joinStatuses(p.Schema.Status), "draft|review|superseded"; got != want {
				t.Fatalf("schema.status = %q, want the default %q", got, want)
			}
			if got, want := strings.Join(p.Schema.Required, ","), "id,title,status"; got != want {
				t.Fatalf("schema.required = %q, want the default %q", got, want)
			}
		})
	}
}

func TestParsePolicyEmptyListIsHonoured(t *testing.T) {
	// `required: []` is an explicit "no required keys", which YAML tells apart
	// from an absent key by nil-ness. Overwriting it with the default would
	// make the setting unexpressible.
	p, err := ParsePolicy([]byte("schema:\n  required: []\n"))
	if err != nil {
		t.Fatalf("ParsePolicy: %v", err)
	}
	if len(p.Schema.Required) != 0 {
		t.Fatalf("schema.required = %v, want an empty list", p.Schema.Required)
	}
	if len(p.Schema.Status) != 3 {
		t.Fatalf("schema.status = %v, want the default enum", p.Schema.Status)
	}
}

func TestParsePolicyErrors(t *testing.T) {
	tests := []struct {
		name string
		in   string
	}{
		// A typo'd key would parse fine and silently enforce nothing.
		{"typo'd auto_merge", "review:\n  auto-merge: [notes/**]\n"},
		{"unknown top-level key", "reviews:\n  auto_merge: [notes/**]\n"},
		{"unknown schema key", "schema:\n  requires: [id]\n"},
		{"approvers reintroduced", "review:\n  approvers: [~bigbes]\n"},

		{"not a mapping", "- review\n"},
		{"scalar", "just a string\n"},
		{"broken yaml", "review:\n  auto_merge: [notes/**\n"},
		{"tab indentation", "review:\n\tauto_merge: [a]\n"},
		{"two documents", "review:\n  auto_merge: [notes/**]\n---\nschema:\n  required: [id]\n"},
		{"auto_merge as scalar", "review:\n  auto_merge: notes/**\n"},

		{"invalid pattern", "review:\n  auto_merge: [../etc/**]\n"},
		{"absolute pattern", "review:\n  auto_merge: [/notes/**]\n"},
		{"malformed globstar", "review:\n  auto_merge: [notes**]\n"},
		// The critical one: a per-space file must not be able to make
		// "approved" an authored status.
		{"approved status", "schema:\n  status: [draft, approved]\n"},
		{"unknown status", "schema:\n  status: [wip]\n"},
		{"empty status list", "schema:\n  status: []\n"},
		{"duplicate required key", "schema:\n  required: [id, id]\n"},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			p, err := ParsePolicy([]byte(tc.in))
			if err == nil {
				t.Fatalf("ParsePolicy(%q) = %+v, want an error", tc.in, p)
			}
			if !errors.Is(err, ErrInvalidPolicy) {
				t.Fatalf("error %v is not ErrInvalidPolicy", err)
			}
		})
	}
}

func TestPolicyAutoMerges(t *testing.T) {
	p, err := ParsePolicy([]byte("review:\n  auto_merge: [notes/**, reports/**, drafts/*.md]\n"))
	if err != nil {
		t.Fatalf("ParsePolicy: %v", err)
	}

	tests := []struct {
		path string
		want bool
	}{
		{"notes/a.md", true},
		{"notes/2026/07/standup.md", true},
		{"reports/weekly.md", true},
		{"drafts/x.md", true},

		{"specs/0007-storage.md", false},
		{"drafts/sub/x.md", false},
		{"README.md", false},
		{".spec.yml", false},
		{"notesx/a.md", false},

		// Fail-closed: a path that is not a valid path cannot auto-merge, so a
		// traversal attempt goes to a human rather than straight to approved.
		{"notes/../specs/0007.md", false},
		{"../notes/a.md", false},
		{"/notes/a.md", false},
		{"notes/a\x00.md", false},
		{"", false},
	}
	for _, tc := range tests {
		t.Run(tc.path, func(t *testing.T) {
			if got := p.AutoMerges(tc.path); got != tc.want {
				t.Fatalf("AutoMerges(%q) = %v, want %v", tc.path, got, tc.want)
			}
		})
	}
}

func TestDefaultPolicyAutoMergesNothing(t *testing.T) {
	// A space that has said nothing about review must not be laundering
	// unreviewed agent output onto the approved branch.
	p := DefaultPolicy()
	if err := p.Validate(); err != nil {
		t.Fatalf("DefaultPolicy().Validate() = %v, want nil", err)
	}
	for _, path := range []string{"notes/a.md", "specs/a.md", "anything.md"} {
		if p.AutoMerges(path) {
			t.Errorf("DefaultPolicy().AutoMerges(%q) = true, want false", path)
		}
	}
}

func TestPolicyValidate(t *testing.T) {
	tests := []struct {
		name string
		in   Policy
		ok   bool
	}{
		{"default", DefaultPolicy(), true},
		{
			"good patterns",
			Policy{Review: ReviewPolicy{AutoMerge: []string{"notes/**", "*.md"}}, Schema: DefaultSchema()},
			true,
		},
		{
			"bad pattern",
			Policy{Review: ReviewPolicy{AutoMerge: []string{"notes/**", "../x"}}, Schema: DefaultSchema()},
			false,
		},
		{
			"bad schema",
			Policy{Schema: Schema{Status: []Status{"approved"}}},
			false,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.in.Validate()
			if (err == nil) != tc.ok {
				t.Fatalf("Policy.Validate() = %v, want ok=%v", err, tc.ok)
			}
			if err != nil && !errors.Is(err, ErrInvalidPolicy) {
				t.Fatalf("error %v is not ErrInvalidPolicy", err)
			}
		})
	}
}

A core/proposal.go => core/proposal.go +93 -0
@@ 0,0 1,93 @@
package core

import "fmt"

// ProposalState is the lifecycle of a proposal.
//
//	open ──► merged
//	  └───► rejected
//
// Collapsed from the usual five-state machine because there is exactly one
// reviewer: with nobody else in the loop, "approve" is "merge now", and there
// is no one to request changes from — a proposal you dislike is rejected and
// the agent proposes again. Keeping `approved` and `merged` apart, or a
// `changes-requested` cycle, would be machinery serving a review conversation
// that has no second party.
type ProposalState string

const (
	StateOpen     ProposalState = "open"
	StateMerged   ProposalState = "merged"
	StateRejected ProposalState = "rejected"
)

// ProposalStates returns every state, in lifecycle order.
func ProposalStates() []ProposalState {
	return []ProposalState{StateOpen, StateMerged, StateRejected}
}

// ParseProposalState validates a state string, typically one read back from
// Postgres or an API request.
func ParseProposalState(s string) (ProposalState, error) {
	switch ProposalState(s) {
	case StateOpen, StateMerged, StateRejected:
		return ProposalState(s), nil
	}
	return "", fmt.Errorf("%w: %q is not one of open|merged|rejected", ErrInvalidState, s)
}

// Terminal reports whether the proposal has been resolved. Terminal proposals
// keep their URL — a link still resolves after merge or rejection, showing the
// outcome — but they never move again.
func (s ProposalState) Terminal() bool {
	return s == StateMerged || s == StateRejected
}

// CanTransitionTo reports whether the proposal may move from s to next,
// returning ErrInvalidTransition with both states named if it may not.
//
// Only open→merged and open→rejected are legal. Self-transitions are rejected
// too: the reconciler repairs a crashed merge by comparing the ref against the
// row and only writing when they differ, so a "merged→merged" call is a bug in
// the caller rather than an idempotent retry.
func (s ProposalState) CanTransitionTo(next ProposalState) error {
	if _, err := ParseProposalState(string(s)); err != nil {
		return err
	}
	if _, err := ParseProposalState(string(next)); err != nil {
		return err
	}
	if !ValidTransition(s, next) {
		return fmt.Errorf("%w: %s -> %s", ErrInvalidTransition, s, next)
	}
	return nil
}

// ValidTransition is the transition table itself.
func ValidTransition(from, to ProposalState) bool {
	return from == StateOpen && (to == StateMerged || to == StateRejected)
}

// Approval records how a merge was authorized, and is set on merge.
//
// Auto-merged is not human-approved, and readers must be able to tell: a bot
// asking for the approved text of a spec should be able to require human
// approval and get a different answer than for a firehose note. Collapsing the
// two would quietly launder unreviewed agent output as blessed.
type Approval string

const (
	// ApprovalHuman means the owner clicked approve.
	ApprovalHuman Approval = "human"
	// ApprovalPolicy means the path matched the space's auto_merge patterns.
	ApprovalPolicy Approval = "policy"
)

// ParseApproval validates an approval kind read back from Postgres or an API.
func ParseApproval(s string) (Approval, error) {
	switch Approval(s) {
	case ApprovalHuman, ApprovalPolicy:
		return Approval(s), nil
	}
	return "", fmt.Errorf("%w: %q is not one of human|policy", ErrInvalidApproval, s)
}

A core/proposal_test.go => core/proposal_test.go +147 -0
@@ 0,0 1,147 @@
package core

import (
	"errors"
	"testing"
)

func TestParseProposalState(t *testing.T) {
	tests := []struct {
		in string
		ok bool
	}{
		{"open", true},
		{"merged", true},
		{"rejected", true},

		// The states that were deliberately collapsed away. Accepting either
		// would resurrect a review conversation that has no second party.
		{"approved", false},
		{"changes-requested", false},
		{"closed", false},
		{"draft", false},

		{"", false},
		{"Open", false},
		{"OPEN", false},
		{"open ", false},
		{" open", false},
		{"открыт", false},
	}
	for _, tc := range tests {
		st, err := ParseProposalState(tc.in)
		if (err == nil) != tc.ok {
			t.Errorf("ParseProposalState(%q) = %q, %v, want ok=%v", tc.in, st, err, tc.ok)
		}
		if err != nil && !errors.Is(err, ErrInvalidState) {
			t.Errorf("ParseProposalState(%q) error %v is not ErrInvalidState", tc.in, err)
		}
	}
}

func TestProposalStatesIsExactlyThree(t *testing.T) {
	got := ProposalStates()
	if len(got) != 3 {
		t.Fatalf("ProposalStates() = %v, want exactly open/merged/rejected", got)
	}
	want := []ProposalState{StateOpen, StateMerged, StateRejected}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("ProposalStates() = %v, want %v", got, want)
		}
	}
}

func TestProposalTransitions(t *testing.T) {
	all := []ProposalState{StateOpen, StateMerged, StateRejected}

	// The whole table, explicitly: only the two edges out of `open` exist.
	want := map[ProposalState]map[ProposalState]bool{
		StateOpen:     {StateOpen: false, StateMerged: true, StateRejected: true},
		StateMerged:   {StateOpen: false, StateMerged: false, StateRejected: false},
		StateRejected: {StateOpen: false, StateMerged: false, StateRejected: false},
	}

	for _, from := range all {
		for _, to := range all {
			t.Run(string(from)+"->"+string(to), func(t *testing.T) {
				expect := want[from][to]
				if got := ValidTransition(from, to); got != expect {
					t.Fatalf("ValidTransition(%s, %s) = %v, want %v", from, to, got, expect)
				}
				err := from.CanTransitionTo(to)
				if (err == nil) != expect {
					t.Fatalf("%s.CanTransitionTo(%s) = %v, want ok=%v", from, to, err, expect)
				}
				if err != nil && !errors.Is(err, ErrInvalidTransition) {
					t.Fatalf("error %v is not ErrInvalidTransition", err)
				}
			})
		}
	}
}

func TestCanTransitionToRejectsUnknownStates(t *testing.T) {
	// An unparseable state is a different failure class from an illegal edge:
	// it means something wrote garbage into the proposal row.
	if err := ProposalState("approved").CanTransitionTo(StateMerged); !errors.Is(err, ErrInvalidState) {
		t.Fatalf("from=approved: %v, want ErrInvalidState", err)
	}
	if err := StateOpen.CanTransitionTo("approved"); !errors.Is(err, ErrInvalidState) {
		t.Fatalf("to=approved: %v, want ErrInvalidState", err)
	}
	if err := ProposalState("").CanTransitionTo(StateMerged); !errors.Is(err, ErrInvalidState) {
		t.Fatalf("from=empty: %v, want ErrInvalidState", err)
	}
}

func TestProposalStateTerminal(t *testing.T) {
	tests := []struct {
		in   ProposalState
		want bool
	}{
		{StateOpen, false},
		{StateMerged, true},
		{StateRejected, true},
		{"", false},
		{"approved", false},
	}
	for _, tc := range tests {
		if got := tc.in.Terminal(); got != tc.want {
			t.Errorf("%q.Terminal() = %v, want %v", tc.in, got, tc.want)
		}
	}
}

func TestParseApproval(t *testing.T) {
	tests := []struct {
		in string
		ok bool
	}{
		{"human", true},
		{"policy", true},

		{"", false},
		{"auto", false},
		{"Human", false},
		{"human ", false},
		{"approved", false},
	}
	for _, tc := range tests {
		a, err := ParseApproval(tc.in)
		if (err == nil) != tc.ok {
			t.Errorf("ParseApproval(%q) = %q, %v, want ok=%v", tc.in, a, err, tc.ok)
		}
		if err != nil && !errors.Is(err, ErrInvalidApproval) {
			t.Errorf("ParseApproval(%q) error %v is not ErrInvalidApproval", tc.in, err)
		}
	}
}

// A policy-merged document must be distinguishable from a human-approved one:
// collapsing the two would quietly launder unreviewed agent output as blessed.
func TestApprovalKindsAreDistinct(t *testing.T) {
	if ApprovalHuman == ApprovalPolicy {
		t.Fatal("human and policy approval must not be the same value")
	}
}

A go.mod => go.mod +107 -0
@@ 0,0 1,107 @@
module sourcecraft.dev/bigbes/sr-ht-spec

go 1.26.4

require (
	git.sr.ht/~bitfehler/brant v0.5.1
	github.com/blevesearch/bleve/v2 v2.6.0
	github.com/go-chi/chi/v5 v5.3.1
	github.com/go-git/go-git/v5 v5.19.1
	github.com/lib/pq v1.10.9
	github.com/modelcontextprotocol/go-sdk v1.6.1
	github.com/stretchr/testify v1.11.1
	github.com/yuin/goldmark v1.8.2
	go.bigb.es/auxilia v0.5.0
	gopkg.in/yaml.v3 v3.0.1
	sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152
)

require (
	dario.cat/mergo v1.0.0 // indirect
	git.sr.ht/~sircmpwn/dowork v0.0.0-20241216125407-2b00aa42322c // indirect
	git.sr.ht/~sircmpwn/getopt v1.0.0 // indirect
	git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 // indirect
	github.com/99designs/gqlgen v0.17.36 // indirect
	github.com/Masterminds/squirrel v1.5.4 // indirect
	github.com/ProtonMail/go-crypto v1.3.0 // indirect
	github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
	github.com/agnivade/levenshtein v1.1.1 // indirect
	github.com/alexflint/go-arg v1.6.0 // indirect
	github.com/alexflint/go-scalar v1.2.0 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/bits-and-blooms/bitset v1.24.2 // indirect
	github.com/blevesearch/bleve_index_api v1.3.11 // indirect
	github.com/blevesearch/geo v0.2.5 // indirect
	github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
	github.com/blevesearch/gtreap v0.1.1 // indirect
	github.com/blevesearch/mmap-go v1.2.0 // indirect
	github.com/blevesearch/scorch_segment_api/v2 v2.4.7 // indirect
	github.com/blevesearch/segment v0.9.1 // indirect
	github.com/blevesearch/snowballstem v0.9.0 // indirect
	github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
	github.com/blevesearch/vellum v1.2.0 // indirect
	github.com/blevesearch/zapx/v11 v11.4.3 // indirect
	github.com/blevesearch/zapx/v12 v12.4.3 // indirect
	github.com/blevesearch/zapx/v13 v13.4.3 // indirect
	github.com/blevesearch/zapx/v14 v14.4.3 // indirect
	github.com/blevesearch/zapx/v15 v15.4.3 // indirect
	github.com/blevesearch/zapx/v16 v16.3.4 // indirect
	github.com/blevesearch/zapx/v17 v17.1.2 // indirect
	github.com/cespare/xxhash/v2 v2.2.0 // indirect
	github.com/cloudflare/circl v1.6.3 // indirect
	github.com/cyphar/filepath-securejoin v0.6.1 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
	github.com/emersion/go-message v0.18.2 // indirect
	github.com/emersion/go-pgpmail v0.2.2 // indirect
	github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
	github.com/emersion/go-smtp v0.21.3 // indirect
	github.com/emirpasic/gods v1.18.1 // indirect
	github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee // indirect
	github.com/go-chi/cors v1.2.2 // indirect
	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
	github.com/go-git/go-billy/v5 v5.9.0 // indirect
	github.com/go-redis/redis/v8 v8.11.5 // indirect
	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/golang/snappy v1.0.0 // indirect
	github.com/google/jsonschema-go v0.4.3 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/gorilla/websocket v1.5.0 // indirect
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/kavu/go_reuseport v1.5.0 // indirect
	github.com/kevinburke/ssh_config v1.6.0 // indirect
	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
	github.com/mfridman/interpolate v0.0.2 // indirect
	github.com/mitchellh/mapstructure v1.5.0 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/mschoch/smat v0.2.0 // indirect
	github.com/pjbgf/sha1cd v0.6.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/prometheus/client_golang v1.16.0 // indirect
	github.com/prometheus/client_model v0.4.0 // indirect
	github.com/prometheus/common v0.44.0 // indirect
	github.com/prometheus/procfs v0.12.0 // indirect
	github.com/segmentio/asm v1.1.3 // indirect
	github.com/segmentio/encoding v0.5.4 // indirect
	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
	github.com/skeema/knownhosts v1.3.1 // indirect
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec // indirect
	github.com/vektah/gqlparser/v2 v2.5.8 // indirect
	github.com/xanzy/ssh-agent v0.3.3 // indirect
	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
	go.etcd.io/bbolt v1.4.0 // indirect
	golang.org/x/crypto v0.52.0 // indirect
	golang.org/x/net v0.54.0 // indirect
	golang.org/x/oauth2 v0.35.0 // indirect
	golang.org/x/sys v0.45.0 // indirect
	golang.org/x/text v0.37.0 // indirect
	google.golang.org/protobuf v1.36.6 // indirect
	gopkg.in/warnings.v0 v0.1.2 // indirect
)

A go.sum => go.sum +358 -0
@@ 0,0 1,358 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
git.sr.ht/~bitfehler/brant v0.5.1 h1:emUbT5r1P0p2mgC/52NMAzGa2zw2To/Bim5ySeyGrks=
git.sr.ht/~bitfehler/brant v0.5.1/go.mod h1:XD6RmtKTOT/RL+2iQ+KBcPhLYAjeJWdLj3yfM5+7k/g=
git.sr.ht/~sircmpwn/dowork v0.0.0-20241216125407-2b00aa42322c h1:v2opuaN0C5ZpuCifRNR9ZQ8V9IG+Ja80otK1MFj5RnI=
git.sr.ht/~sircmpwn/dowork v0.0.0-20241216125407-2b00aa42322c/go.mod h1:8neHEO3503w/rNtttnR0JFpQgM/GFhaafVwvkPsFIDw=
git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
git.sr.ht/~sircmpwn/getopt v1.0.0 h1:/pRHjO6/OCbBF4puqD98n6xtPEgE//oq5U8NXjP7ROc=
git.sr.ht/~sircmpwn/getopt v1.0.0/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA=
git.srht.bigb.es/~bigbes/core-go v0.0.0-20260718172441-c2c2f3848fa9 h1:S/CncMS83NIBz+V5OlZEHlnrl1Yd5I4C7hDQ7lzQsTM=
git.srht.bigb.es/~bigbes/core-go v0.0.0-20260718172441-c2c2f3848fa9/go.mod h1:JmathMemB+hBk1IgrMn6RuBGusd+fTg1s/X6rNVKXXU=
github.com/99designs/gqlgen v0.17.36 h1:u/o/rv2SZ9s5280dyUOOrkpIIkr/7kITMXYD3rkJ9go=
github.com/99designs/gqlgen v0.17.36/go.mod h1:6RdyY8puhCoWAQVr2qzF2OMVfudQzc8ACxzpzluoQm4=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw=
github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0=
github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4=
github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY=
github.com/blevesearch/bleve_index_api v1.3.11 h1:x29vbV8OjWfLcrDVd7Lr1q+BkLNS0JWNEig0MCVnKH4=
github.com/blevesearch/bleve_index_api v1.3.11/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
github.com/blevesearch/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw=
github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY=
github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo=
github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE=
github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM=
github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU=
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
github.com/blevesearch/zapx/v17 v17.1.2 h1:avbOk2igaASNoiy0BE/jPgcxAnRI2PGeydeP4hg7Ikk=
github.com/blevesearch/zapx/v17 v17.1.2/go.mod h1:WQObxKrqUX7cd0G1GMvDfc/bmZzQvoy7APOPimx7DiI=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw=
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-pgpmail v0.2.2 h1:cO2jwsE0gb8aDdCcVH5Dfe1XV3Rhhw2GVWsmQd3CbaI=
github.com/emersion/go-pgpmail v0.2.2/go.mod h1:mRB5P7QKiAuOvcT36tdRZvm7nSt7V+f6jbzzup3HuvU=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee h1:v6Eju/FhxsACGNipFEPBZZAzGr1F/jlRQr1qiBw2nEE=
github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk=
github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks=
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
github.com/vektah/gqlparser/v2 v2.5.8 h1:pm6WOnGdzFOCfcQo9L3+xzW51mKrlwTEg4Wr7AH1JW4=
github.com/vektah/gqlparser/v2 v2.5.8/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.bigb.es/auxilia v0.5.0 h1:S5+btW6++4CQDOfAEZe1UxrXRl6nxtmu0rI3uIXwCaQ=
go.bigb.es/auxilia v0.5.0/go.mod h1:hBkJvydQRfmgSTR2U4PvYgJcZnh7Bj/otukrk14iJU4=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152 h1:9kQC+tDO2CO8avlKadb9Z0if4a6vJuEK80+4zcb6/fU=
sourcecraft.dev/bigbes/sr-ht-core v0.0.0-20260718185800-dd418a200152/go.mod h1:Mu1Vx39ws/OTKWGoVERXvkdRSPLBdhuFTYv0ftVV31c=