diff --git a/ingestion/cmd/server/main.go b/ingestion/cmd/server/main.go index b4b256d..abe391f 100644 --- a/ingestion/cmd/server/main.go +++ b/ingestion/cmd/server/main.go @@ -68,12 +68,13 @@ func (s *claudeSink) Ingest(ctx context.Context, b claudewatcher.Batch) error { } slug := "session-" + b.Host + "-" + b.SessionID if _, err := api.WriteNote(s.brainDir, api.WriteNoteOptions{ - Filename: slug, - Wing: "claude-sessions", - Hall: "facts", - Type: "source", - Domain: b.ProjectID, - Content: sb.String(), + Filename: slug, + Wing: "claude-sessions", + Hall: "facts", + Type: "source", + Domain: b.ProjectID, + Content: sb.String(), + SourceType: "internal", // raw session transcript, not an external claim (brain-gardener#5) }); err != nil { return fmt.Errorf("write claude session note: %w", err) } diff --git a/ingestion/cmd/server/main_test.go b/ingestion/cmd/server/main_test.go new file mode 100644 index 0000000..795e0a1 --- /dev/null +++ b/ingestion/cmd/server/main_test.go @@ -0,0 +1,36 @@ +// ingestion/cmd/server/main_test.go +package main + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mathiasbq/hyperguild/ingestion/internal/claudewatcher" +) + +func TestClaudeSink_IngestTagsSourceTypeInternal(t *testing.T) { + dir := t.TempDir() + sink := &claudeSink{brainDir: dir, logger: slog.New(slog.NewTextHandler(os.Stderr, nil))} + + err := sink.Ingest(context.Background(), claudewatcher.Batch{ + Host: "koala", + FilePath: "/host-home-claude/projects/-home-mathias-dev/abc.jsonl", + SessionID: "abc", + ProjectID: "-home-mathias-dev", + Turns: []claudewatcher.Turn{ + {Type: "assistant", Content: "did a thing"}, + }, + }) + require.NoError(t, err) + + got, err := os.ReadFile(filepath.Join(dir, "wiki", "claude-sessions", "facts", "session-koala-abc.md")) + require.NoError(t, err) + assert.Contains(t, string(got), "source_type: internal") + assert.Contains(t, string(got), "wing: claude-sessions") +} diff --git a/ingestion/internal/api/handler.go b/ingestion/internal/api/handler.go index 04dae27..3a8b75b 100644 --- a/ingestion/internal/api/handler.go +++ b/ingestion/internal/api/handler.go @@ -115,12 +115,13 @@ func (h *Handler) Query(w http.ResponseWriter, r *http.Request) { // When either is empty, the note falls back to brain/knowledge/ // with optional type/domain frontmatter (legacy behaviour). type WriteNoteOptions struct { - Content string - Filename string - Type string - Domain string - Wing string - Hall string + Content string + Filename string + Type string + Domain string + Wing string + Hall string + SourceType string // "internal" marks a first-party observation (e.g. claudewatcher) that needs no external citation } // WriteNote writes a markdown note into the brain. Returns the path @@ -165,6 +166,9 @@ func writeHallNote(brainDir string, opts WriteNoteOptions) (string, error) { if opts.Domain != "" { fmt.Fprintf(&fm, "domain: %s\n", opts.Domain) } + if opts.SourceType != "" { + fmt.Fprintf(&fm, "source_type: %s\n", opts.SourceType) + } fm.WriteString("---\n") if err := os.WriteFile(dest, []byte(fm.String()+opts.Content), 0o644); err != nil { @@ -227,7 +231,14 @@ func (h *Handler) Write(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "invalid JSON") return } - relPath, err := WriteNote(h.brainDir, WriteNoteOptions(req)) + relPath, err := WriteNote(h.brainDir, WriteNoteOptions{ + Content: req.Content, + Filename: req.Filename, + Type: req.Type, + Domain: req.Domain, + Wing: req.Wing, + Hall: req.Hall, + }) if err != nil { h.logger.Error("write failed", "err", err) writeError(w, http.StatusBadRequest, err.Error()) diff --git a/ingestion/internal/api/handler_test.go b/ingestion/internal/api/handler_test.go index 8a3a701..3573942 100644 --- a/ingestion/internal/api/handler_test.go +++ b/ingestion/internal/api/handler_test.go @@ -118,6 +118,40 @@ func TestWrite_IncludesFrontmatterWhenTypeProvided(t *testing.T) { assert.Contains(t, string(content), "Some learning.") } +func TestWriteNote_HallRouteIncludesSourceTypeWhenSet(t *testing.T) { + dir := t.TempDir() + + rel, err := api.WriteNote(dir, api.WriteNoteOptions{ + Content: "# Claude session abc (koala)\n\nBody.\n", + Filename: "session-koala-abc", + Wing: "claude-sessions", + Hall: "facts", + Type: "source", + SourceType: "internal", + }) + require.NoError(t, err) + + got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) + require.NoError(t, err) + assert.Contains(t, string(got), "source_type: internal") + assert.Contains(t, string(got), "wing: claude-sessions") +} + +func TestWriteNote_HallRouteOmitsSourceTypeWhenUnset(t *testing.T) { + dir := t.TempDir() + + rel, err := api.WriteNote(dir, api.WriteNoteOptions{ + Content: "manually captured fact.\n", + Wing: "agentsquad", + Hall: "facts", + }) + require.NoError(t, err) + + got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) + require.NoError(t, err) + assert.NotContains(t, string(got), "source_type") +} + func TestWrite_GeneratesFilenameIfAbsent(t *testing.T) { dir, h := setup(t) body, _ := json.Marshal(map[string]any{"content": "auto name"})