No model. A subfolder/frontmatter wing must be a plain name of an existing wing and then places; conflicting signals and client-marker text park; everything else goes to the default wing. Wings are read from disk per decision. Mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
71 lines
2.6 KiB
Go
71 lines
2.6 KiB
Go
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}
|
|
}
|