package wing import ( "context" "fmt" "regexp" "slices" "strings" ) // Placer decides where a raw note goes. Router is the production // implementation; the interface lets the watcher stay ignorant of how. type Placer interface { Resolve(ctx context.Context, n Note) Decision } // Router places a note with rules only, no model (hyperguild#208): // // 1. A subfolder or frontmatter `wing:` is the owner's deliberate choice. It // must name a plain, EXISTING wing (a typo never creates one) and then it // places, whatever the wing is: extraction runs on the local model, and it // is the owner who dropped the note there. Two signals that disagree park. // 2. With no signal, a note whose text matches ClientBlock is parked; any // other note goes to Default, the one wing that is created on first use. // // Wings are read from disk on every decision, so a wing created while the // server runs is usable at once. type Router struct { BrainDir string Default string ClientBlock *regexp.Regexp // nil = no content check } // Resolve implements Placer. It never returns an error: anything it cannot // decide safely is an Exception with the reason spelled out. func (r Router) Resolve(_ context.Context, n Note) Decision { dir, fm := norm(n.Dir), norm(n.FrontmatterWing) switch { case dir != "" && fm != "" && dir != fm: return exception("", 0, fmt.Sprintf("subfolder %q and frontmatter wing %q disagree", dir, fm)) case dir != "": return r.explicit(dir, "subfolder") case fm != "": return r.explicit(fm, "frontmatter") } if r.ClientBlock != nil && r.ClientBlock.MatchString(n.Text) { return exception("", 0, "text matches the client-content marker; not defaulted into a shared wing") } if r.Default == "" || !plainWingName(r.Default) { return exception("", 0, "no valid default wing configured") } return Decision{Wing: r.Default, Source: Default, Confidence: 1, Reason: "no wing named; default wing"} } func (r Router) explicit(w, via string) Decision { if !plainWingName(w) { return exception("", 0, fmt.Sprintf("%s names %q, which is not a plain wing name", via, w)) } if w == r.Default && plainWingName(w) { return Decision{Wing: w, Source: Explicit, Confidence: 1, Reason: "named by " + via} } known, err := KnownWings(r.BrainDir) if err != nil { return exception(w, 0, fmt.Sprintf("cannot list wings: %v", err)) } if !slices.Contains(known, w) { return exception(w, 0, fmt.Sprintf("%s names %q: not an existing wing (new wings are never created from a folder name)", via, strings.TrimSpace(w))) } return Decision{Wing: w, Source: Explicit, Confidence: 1, Reason: "named by " + via} }