fix(api,watcher): name the queue, list parked non-markdown, reserved wings, top-level-only frontmatter key; document the inbox (review of #211)
CI / Lint / Test / Vet (pull_request) Successful in 1m2s
CI / Mirror to GitHub (pull_request) Skipped

- brain_promote/brain_reject take an optional queue (inbox | needs-review); a
  name present in both is refused instead of silently acting on the top-level
  note; brain_pending labels every entry with its queue
- parked pdf/txt files are listed (they were invisible); only markdown can be
  promoted, the others are moved or rejected
- promote refuses sources/concepts/entities/wiki as a wing
- frontmatterWing reads only a top-level key inside a properly fenced block
- docs/raw-inbox.md: the contract, the three env vars, the known limits
Each behaviour has a test that fails without it (mutation-checked).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:34:52 +02:00
co-authored by Claude Sonnet 5.5
parent 6b6d430383
commit 87c40fe364
10 changed files with 316 additions and 47 deletions
+2 -1
View File
@@ -617,6 +617,7 @@ type promoteRequest struct {
Wing string `json:"wing"`
Hall string `json:"hall"`
Slug string `json:"slug,omitempty"`
Queue string `json:"queue,omitempty"`
}
// Promote handles POST /promote — move a raw/ note into the wiki. A bad
@@ -630,7 +631,7 @@ func (h *Handler) Promote(w http.ResponseWriter, r *http.Request) {
// Field by field: this door carries no principal, so the promoted note is
// stamped origin: unknown and the body cannot set provenance (#145).
rel, err := PromoteNote(h.brainDir, PromoteOptions{
Filename: req.Filename, Wing: req.Wing, Hall: req.Hall, Slug: req.Slug,
Filename: req.Filename, Wing: req.Wing, Hall: req.Hall, Slug: req.Slug, Queue: req.Queue,
})
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
@@ -34,7 +34,7 @@ func TestListPending_ShowsParkedNotesWithTheirProposal(t *testing.T) {
assert.NotContains(t, got[0].Excerpt, "proposed_wing", "excerpt is body, not frontmatter")
assert.Equal(t, "fresh.md", got[1].Filename)
assert.Empty(t, got[1].Queue, "a top-level note has no queue label")
assert.Equal(t, QueueInbox, got[1].Queue)
assert.Empty(t, got[1].ProposedWing)
}
+75 -28
View File
@@ -26,7 +26,7 @@ type PendingNote struct {
// (hyperguild#205): it was not placed automatically, and the proposal says
// what the resolver would have done and why it did not. Approve by moving
// the note into raw/<wing>/ and the watcher ingests it there.
Queue string `json:"queue,omitempty"`
Queue string `json:"queue"`
ProposedWing string `json:"proposed_wing,omitempty"`
ProposalReason string `json:"proposal_reason,omitempty"`
ProposalConfidence float64 `json:"proposal_confidence,omitempty"`
@@ -49,7 +49,7 @@ func ListPending(brainDir string) ([]PendingNote, error) {
}
defer func() { _ = root.Close() }()
out, err := listNotes(root, "")
out, err := listNotes(root, QueueInbox)
if err != nil {
return nil, err
}
@@ -57,7 +57,7 @@ func ListPending(brainDir string) ([]PendingNote, error) {
// or note there is refused exactly like at the top level (#153).
if sub, serr := root.OpenRoot("needs-review"); serr == nil {
defer func() { _ = sub.Close() }()
parked, perr := listNotes(sub, "needs-review")
parked, perr := listNotes(sub, QueueNeedsReview)
if perr != nil {
return nil, perr
}
@@ -76,9 +76,25 @@ func listNotes(root *os.Root, queue string) ([]PendingNote, error) {
}
out := make([]PendingNote, 0, len(entries))
for _, e := range entries {
if !e.Type().IsRegular() || !strings.HasSuffix(e.Name(), ".md") || strings.HasPrefix(e.Name(), "tunnel-candidates-") {
isMD := strings.HasSuffix(e.Name(), ".md")
// A parked pdf or txt is listed too (the watcher parks any supported
// file); at the top level only markdown ever was.
parkedOther := queue == QueueNeedsReview && (strings.HasSuffix(e.Name(), ".txt") || strings.HasSuffix(e.Name(), ".pdf"))
if !e.Type().IsRegular() || (!isMD && !parkedOther) || strings.HasPrefix(e.Name(), "tunnel-candidates-") {
continue // directories, and symlinks whose target may be anywhere (#153)
}
if !isMD {
info, ierr := root.Lstat(e.Name())
if ierr != nil {
continue
}
out = append(out, PendingNote{
Filename: e.Name(), CreatedAt: info.ModTime().UTC().Format(time.RFC3339), SizeBytes: info.Size(),
Excerpt: "(not markdown; the wing proposal is in log.md. Move the file into raw/<wing>/ to ingest it.)",
Queue: queue,
})
continue
}
raw, info, readErr := readRawNote(root, e.Name())
if readErr != nil {
continue
@@ -95,7 +111,7 @@ func listNotes(root *os.Root, queue string) ([]PendingNote, error) {
Excerpt: excerpt(body, 200),
Queue: queue,
}
if queue != "" {
if queue == QueueNeedsReview {
n.ProposedWing = fm.get("proposed_wing")
n.ProposalReason = unquoteScalar(fm.get("proposal_reason"))
n.ProposalConfidence, _ = strconv.ParseFloat(fm.get("proposal_confidence"), 64)
@@ -111,6 +127,9 @@ type PromoteOptions struct {
Wing string
Hall string
Slug string // optional; defaults to Filename minus date prefix + .md
// Queue names which listing the note is in: QueueInbox or QueueNeedsReview.
// Empty is accepted only when the name exists in exactly one of them.
Queue string
// Origin/WrittenBy re-attribute the promoted note to the promoter: the raw
// note's own frontmatter claims are not provenance (hyperguild#145).
Origin string
@@ -133,17 +152,26 @@ func PromoteNote(brainDir string, opts PromoteOptions) (string, error) {
return "", fmt.Errorf("invalid filename %q", opts.Filename)
}
if !strings.HasSuffix(base, ".md") {
return "", fmt.Errorf("only a markdown note can be promoted; move %q into raw/<wing>/ to ingest it", base)
}
slug := opts.Slug
if slug == "" {
slug = datePrefix.ReplaceAllString(strings.TrimSuffix(base, ".md"), "")
}
// The retired layout's directory names are not wings: a note promoted into
// "sources" would land in wiki/sources/<hall>/, inside the retired dir.
switch brain.Sanitise(opts.Wing) {
case "sources", "concepts", "entities", "wiki":
return "", fmt.Errorf("%q is not a wing", opts.Wing)
}
// NotePath validates hall + wing + slug; do this before reading anything.
dest, err := brain.NotePath(brainDir, opts.Wing, opts.Hall, slug)
if err != nil {
return "", err
}
raw, srcRel, err := readRawSource(brainDir, base)
raw, srcRel, err := readRawSource(brainDir, base, opts.Queue)
if err != nil {
return "", err
}
@@ -198,6 +226,7 @@ func PromoteNote(brainDir string, opts PromoteOptions) (string, error) {
type RejectOptions struct {
Filename string // basename in brain/raw/
Reason string // optional
Queue string // as PromoteOptions.Queue
}
// RejectNote records a human decision not to promote a brain/raw/ note,
@@ -220,7 +249,7 @@ func RejectNote(brainDir string, opts RejectOptions) (string, error) {
return "", fmt.Errorf("invalid filename %q", opts.Filename)
}
raw, srcRel, err := readRawSource(brainDir, base)
raw, srcRel, err := readRawSource(brainDir, base, opts.Queue)
if err != nil {
return "", err
}
@@ -263,7 +292,10 @@ func excerpt(s string, n int) string {
// readRawSource reads a pending note for promote/reject through the raw root,
// refusing symlinks and a symlinked raw/ (hyperguild#153).
func readRawSource(brainDir, base string) ([]byte, string, error) {
func readRawSource(brainDir, base, queue string) ([]byte, string, error) {
if queue != "" && queue != QueueInbox && queue != QueueNeedsReview {
return nil, "", fmt.Errorf("unknown queue %q (want %q or %q)", queue, QueueInbox, QueueNeedsReview)
}
root, _, err := OpenRawRoot(brainDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -272,34 +304,49 @@ func readRawSource(brainDir, base string) ([]byte, string, error) {
return nil, "", fmt.Errorf("read source: %w", err)
}
defer func() { _ = root.Close() }()
raw, _, err := readRawNote(root, base)
if err == nil {
return raw, "raw/" + base, nil
var topRaw, parkedRaw []byte
var topErr, parkedErr error
if queue != QueueNeedsReview {
topRaw, _, topErr = readRawNote(root, base)
}
if !errors.Is(err, os.ErrNotExist) {
return nil, "", fmt.Errorf("read source: %w", err)
}
// brain_pending also lists notes parked in raw/needs-review/ under their
// bare filename (hyperguild#208); the top level wins on a name clash. The
// sub-root refuses a symlinked directory like OpenRawRoot does.
sub, serr := root.OpenRoot(parkedDir)
if serr != nil {
return nil, "", fmt.Errorf("pending note %q does not exist in raw/", base)
}
defer func() { _ = sub.Close() }()
raw, _, err = readRawNote(sub, base)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, "", fmt.Errorf("pending note %q does not exist in raw/", base)
if queue != QueueInbox {
// The sub-root refuses a symlinked directory like OpenRawRoot does.
if sub, serr := root.OpenRoot(parkedDir); serr != nil {
parkedErr = serr
} else {
parkedRaw, _, parkedErr = readRawNote(sub, base)
_ = sub.Close()
}
return nil, "", fmt.Errorf("read source: %w", err)
}
return raw, "raw/" + parkedDir + "/" + base, nil
haveTop := queue != QueueNeedsReview && topErr == nil
haveParked := queue != QueueInbox && parkedErr == nil
switch {
case haveTop && haveParked:
return nil, "", fmt.Errorf("%q is in both raw/ and raw/%s/: name the queue (%q or %q)", base, parkedDir, QueueInbox, QueueNeedsReview)
case haveTop:
return topRaw, "raw/" + base, nil
case haveParked:
return parkedRaw, "raw/" + parkedDir + "/" + base, nil
}
for _, e := range []error{topErr, parkedErr} {
if e != nil && !errors.Is(e, os.ErrNotExist) {
return nil, "", fmt.Errorf("read source: %w", e)
}
}
return nil, "", fmt.Errorf("pending note %q does not exist in raw/", base)
}
// parkedDir is where the watcher parks notes it could not place.
const parkedDir = "needs-review"
// The two review queues brain_pending lists. promote and reject take one of
// them (or neither, when the name is unambiguous).
const (
QueueInbox = "inbox" // raw/<name>
QueueNeedsReview = "needs-review" // raw/needs-review/<name>
)
// unquoteScalar undoes the Go-style double quoting the watcher writes around a
// free-text value (strconv.Quote keeps it on one line); a bare value is
// returned as is.
@@ -0,0 +1,126 @@
package api
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// brain_pending lists a top-level note and a parked note of the same name as two
// entries with the same filename. Acting on "a.md" must not silently pick one.
func TestPromoteNote_ANameInBothQueuesNeedsTheQueueToBeNamed(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "a.md", "---\ncreated_at: 2026-10-20T08:00:00Z\n---\nTop-level body.\n")
writeParked(t, dir, "a.md", parkedNote)
_, err := PromoteNote(dir, PromoteOptions{Filename: "a.md", Wing: "homelab", Hall: "facts"})
require.Error(t, err)
assert.Contains(t, err.Error(), "both")
assert.Contains(t, err.Error(), "queue")
_, statErr := os.Stat(filepath.Join(dir, "wiki/homelab/facts/a.md"))
assert.ErrorIs(t, statErr, os.ErrNotExist, "nothing was promoted")
rel, err := PromoteNote(dir, PromoteOptions{Filename: "a.md", Wing: "homelab", Hall: "facts", Queue: QueueNeedsReview})
require.NoError(t, err)
got, _ := os.ReadFile(filepath.Join(dir, rel))
assert.Contains(t, string(got), "Body of the parked note.")
_, statErr = os.Stat(filepath.Join(dir, "raw", "a.md"))
assert.NoError(t, statErr, "the top-level note of the same name is untouched")
}
func TestPromoteNote_QueueInboxSelectsTheTopLevelNote(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "a.md", "---\ncreated_at: 2026-10-20T08:00:00Z\n---\nTop-level body.\n")
writeParked(t, dir, "a.md", parkedNote)
rel, err := PromoteNote(dir, PromoteOptions{Filename: "a.md", Wing: "homelab", Hall: "facts", Queue: QueueInbox})
require.NoError(t, err)
got, _ := os.ReadFile(filepath.Join(dir, rel))
assert.Contains(t, string(got), "Top-level body.")
_, statErr := os.Stat(filepath.Join(dir, "raw", "needs-review", "a.md"))
assert.NoError(t, statErr, "the parked note is untouched")
}
func TestPromoteNote_ANamedQueueThatDoesNotHoldTheNoteIsAnError(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "a.md", "top")
_, err := PromoteNote(dir, PromoteOptions{Filename: "a.md", Wing: "homelab", Hall: "facts", Queue: QueueNeedsReview})
assert.Error(t, err)
_, err = PromoteNote(dir, PromoteOptions{Filename: "a.md", Wing: "homelab", Hall: "facts", Queue: "elsewhere"})
assert.Error(t, err, "an unknown queue name is refused")
}
func TestRejectNote_ANameInBothQueuesNeedsTheQueueToBeNamed(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "a.md", "top")
writeParked(t, dir, "a.md", parkedNote)
_, err := RejectNote(dir, RejectOptions{Filename: "a.md"})
require.Error(t, err)
_, err = RejectNote(dir, RejectOptions{Filename: "a.md", Queue: QueueNeedsReview})
require.NoError(t, err)
_, statErr := os.Stat(filepath.Join(dir, "raw", "a.md"))
assert.NoError(t, statErr, "the top-level note is untouched")
}
func TestPromoteNote_NeverIntoARetiredLayoutName(t *testing.T) {
for _, w := range []string{"sources", "concepts", "entities", "wiki"} {
t.Run(w, func(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "a.md", "top")
_, err := PromoteNote(dir, PromoteOptions{Filename: "a.md", Wing: w, Hall: "facts"})
assert.Error(t, err)
_, statErr := os.Stat(filepath.Join(dir, "wiki", w))
assert.ErrorIs(t, statErr, os.ErrNotExist)
})
}
}
func TestListPending_LabelsEveryEntryWithItsQueue(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "fresh.md", "---\ncreated_at: 2026-10-20T08:00:00Z\n---\nx\n")
writeParked(t, dir, "p.md", parkedNote)
got, err := ListPending(dir)
require.NoError(t, err)
require.Len(t, got, 2)
queues := map[string]string{}
for _, n := range got {
queues[n.Filename] = n.Queue
}
assert.Equal(t, QueueInbox, queues["fresh.md"])
assert.Equal(t, QueueNeedsReview, queues["p.md"])
}
// Parked notes that are not markdown (a pdf, a txt) were invisible: the only
// trace was a log.md line.
func TestListPending_ShowsParkedNonMarkdownFiles(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw", "needs-review"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw", "needs-review", "paper.pdf"), []byte("%PDF-1.4 binary"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw", "needs-review", "plain.txt"), []byte("some text"), 0o644))
got, err := ListPending(dir)
require.NoError(t, err)
names := map[string]PendingNote{}
for _, n := range got {
names[n.Filename] = n
}
require.Contains(t, names, "paper.pdf")
require.Contains(t, names, "plain.txt")
assert.Equal(t, QueueNeedsReview, names["paper.pdf"].Queue)
assert.Contains(t, names["paper.pdf"].Excerpt, "proposal", "points the reader at where the proposal is")
}
func TestPromoteNote_OnlyMarkdownCanBePromoted(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw", "needs-review"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw", "needs-review", "paper.pdf"), []byte("%PDF binary"), 0o644))
_, err := PromoteNote(dir, PromoteOptions{Filename: "paper.pdf", Wing: "homelab", Hall: "facts"})
require.Error(t, err)
assert.Contains(t, err.Error(), "markdown")
_, statErr := os.Stat(filepath.Join(dir, "raw", "needs-review", "paper.pdf"))
assert.NoError(t, statErr, "refused, untouched")
}
@@ -48,19 +48,6 @@ func TestRejectNote_ActsOnANoteParkedInNeedsReview(t *testing.T) {
assert.Empty(t, pending, "a rejected note is not re-listed")
}
func TestPromoteNote_ATopLevelNoteWinsOverAParkedOneOfTheSameName(t *testing.T) {
dir := t.TempDir()
writeRaw(t, dir, "same.md", "---\ncreated_at: 2026-10-20T08:00:00Z\n---\nTop-level body.\n")
writeParked(t, dir, "same.md", parkedNote)
_, err := PromoteNote(dir, PromoteOptions{Filename: "same.md", Wing: "homelab", Hall: "facts"})
require.NoError(t, err)
got, _ := os.ReadFile(filepath.Join(dir, "wiki/homelab/facts/same.md"))
assert.Contains(t, string(got), "Top-level body.")
_, statErr := os.Stat(filepath.Join(dir, "raw", "needs-review", "same.md"))
assert.NoError(t, statErr, "the parked note of the same name is left alone")
}
func TestPromoteNote_ASymlinkedNeedsReviewIsNotRead(t *testing.T) {
dir := t.TempDir()
outside := t.TempDir()