feat(server): INGEST_WING_RESOLVE=1 wires the wing resolver into the watcher (#205 S3)

Off by default (retired layout unchanged). On: eligibility from
classification.yaml, local ingest LLM as classifier, BM25 as cross-check,
INGEST_WING_THRESHOLD (default 0.9, rejected outside [0,1]). Build failure
is fatal when explicitly enabled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 06:09:17 +02:00
co-authored by Claude Sonnet 5.5
parent 2c6220782d
commit 20d874a23d
7 changed files with 196 additions and 27 deletions
+28
View File
@@ -36,6 +36,7 @@ import (
"github.com/mathiasbq/hyperguild/ingestion/internal/vectorstore"
"github.com/mathiasbq/hyperguild/ingestion/internal/watcher"
"github.com/mathiasbq/hyperguild/ingestion/internal/webhook"
"github.com/mathiasbq/hyperguild/ingestion/internal/wing"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
@@ -144,6 +145,17 @@ func envInt(key string, fallback int) int {
return fallback
}
// envFloat reads a confidence-style value in [0,1]. Anything unparseable or
// outside the range falls back: a threshold of 0 would auto-place everything.
func envFloat(key string, fallback float64) float64 {
if v := os.Getenv(key); v != "" {
if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 {
return f
}
}
return fallback
}
// buildNotifier returns the ntfy notifier, or a nil interface when
// BRAIN_NTFY_URL is unset. A nil *interface* (not a typed-nil) is required so
// DegradingSink/Reconcile and the governance observer skip it cleanly.
@@ -389,10 +401,26 @@ func main() {
}
if watchInterval > 0 {
// INGEST_WING_RESOLVE=1 routes raw/ notes into wiki/<wing>/ by
// subfolder, frontmatter or classifier+scorer, and parks the rest in
// raw/needs-review/ (hyperguild#205). Off = the retired layout. Set
// explicitly, a failure to build the resolver is fatal: silently
// falling back would write into the layout this flag exists to leave.
var wingResolver *wing.Resolver
if os.Getenv("INGEST_WING_RESOLVE") == "1" {
var rerr error
wingResolver, rerr = wing.NewConfiguredResolver(brainDir, llmClient.Complete, envFloat("INGEST_WING_THRESHOLD", 0.9))
if rerr != nil {
logger.Error("build wing resolver", "err", rerr)
os.Exit(1)
}
logger.Info("wing resolver enabled", "wings", len(wingResolver.Known), "threshold", wingResolver.Threshold)
}
watcher.Start(ctx, watcher.Config{
BrainDir: brainDir,
Interval: time.Duration(watchInterval) * time.Second,
Pipeline: pipelineCfg,
Resolver: wingResolver,
})
}
+20
View File
@@ -76,3 +76,23 @@ func TestAlertingWarnings(t *testing.T) {
})
}
}
func TestEnvFloat(t *testing.T) {
tests := []struct {
name string
set string
want float64
}{
{"unset falls back", "", 0.9},
{"valid value", "0.75", 0.75},
{"garbage falls back rather than becoming 0", "high", 0.9},
{"out of range is rejected, not clamped silently into auto-placing everything", "-1", 0.9},
{"above one is rejected", "1.5", 0.9},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("TEST_ENV_FLOAT", tt.set)
assert.InDelta(t, tt.want, envFloat("TEST_ENV_FLOAT", 0.9), 1e-9)
})
}
}
+1 -27
View File
@@ -24,8 +24,6 @@ import (
"github.com/mathiasbq/hyperguild/ingestion/internal/wing"
)
var legacyDirs = map[string]bool{"concepts": true, "entities": true, "sources": true}
func main() {
brain := flag.String("brain", "/var/lib/supervisor/brain", "brain directory (read only)")
wingsFlag := flag.String("wings", "", "comma-separated wings to sample pages FROM (must be wings you would allow)")
@@ -42,7 +40,7 @@ func main() {
}
func run(brain string, sampleWings []string, perWing int, llmURL, model string, seed int64) error {
known, err := knownWings(brain)
known, err := wing.KnownWings(brain)
if err != nil {
return err
}
@@ -112,30 +110,6 @@ func report(samples []wing.Sample, per map[string][]wing.Sample) {
}
}
// knownWings lists wiki/<wing> dirs that hold hall subdirectories, skipping
// the retired legacy layout.
func knownWings(brain string) ([]string, error) {
entries, err := os.ReadDir(filepath.Join(brain, "wiki"))
if err != nil {
return nil, fmt.Errorf("list wings: %w", err)
}
var out []string
for _, e := range entries {
if !e.IsDir() || legacyDirs[e.Name()] {
continue
}
subs, _ := os.ReadDir(filepath.Join(brain, "wiki", e.Name()))
for _, s := range subs {
if s.IsDir() {
out = append(out, e.Name())
break
}
}
}
sort.Strings(out)
return out, nil
}
func listPages(brain, w string) ([]string, error) {
var out []string
root := filepath.Join(brain, "wiki", w)