fix(claudewatcher): tag session-dump notes source_type: internal
brain-gardener#5: the audit's unsourced check was flagging 59 raw claudewatcher session dumps as high-severity hallucination risk, because it can't distinguish a first-party observation (a session transcript) from an external claim needing citation. Adds WriteNoteOptions.SourceType, emitted as source_type: <value> in the wing/hall frontmatter route. claudeSink (the claudewatcher sink) now always sets source_type: internal on the notes it writes.
This commit is contained in:
@@ -68,12 +68,13 @@ func (s *claudeSink) Ingest(ctx context.Context, b claudewatcher.Batch) error {
|
|||||||
}
|
}
|
||||||
slug := "session-" + b.Host + "-" + b.SessionID
|
slug := "session-" + b.Host + "-" + b.SessionID
|
||||||
if _, err := api.WriteNote(s.brainDir, api.WriteNoteOptions{
|
if _, err := api.WriteNote(s.brainDir, api.WriteNoteOptions{
|
||||||
Filename: slug,
|
Filename: slug,
|
||||||
Wing: "claude-sessions",
|
Wing: "claude-sessions",
|
||||||
Hall: "facts",
|
Hall: "facts",
|
||||||
Type: "source",
|
Type: "source",
|
||||||
Domain: b.ProjectID,
|
Domain: b.ProjectID,
|
||||||
Content: sb.String(),
|
Content: sb.String(),
|
||||||
|
SourceType: "internal", // raw session transcript, not an external claim (brain-gardener#5)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("write claude session note: %w", err)
|
return fmt.Errorf("write claude session note: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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/<filename>
|
// When either is empty, the note falls back to brain/knowledge/<filename>
|
||||||
// with optional type/domain frontmatter (legacy behaviour).
|
// with optional type/domain frontmatter (legacy behaviour).
|
||||||
type WriteNoteOptions struct {
|
type WriteNoteOptions struct {
|
||||||
Content string
|
Content string
|
||||||
Filename string
|
Filename string
|
||||||
Type string
|
Type string
|
||||||
Domain string
|
Domain string
|
||||||
Wing string
|
Wing string
|
||||||
Hall 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
|
// 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 != "" {
|
if opts.Domain != "" {
|
||||||
fmt.Fprintf(&fm, "domain: %s\n", 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")
|
fm.WriteString("---\n")
|
||||||
|
|
||||||
if err := os.WriteFile(dest, []byte(fm.String()+opts.Content), 0o644); err != nil {
|
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")
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
h.logger.Error("write failed", "err", err)
|
h.logger.Error("write failed", "err", err)
|
||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
|
|||||||
@@ -118,6 +118,40 @@ func TestWrite_IncludesFrontmatterWhenTypeProvided(t *testing.T) {
|
|||||||
assert.Contains(t, string(content), "Some learning.")
|
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) {
|
func TestWrite_GeneratesFilenameIfAbsent(t *testing.T) {
|
||||||
dir, h := setup(t)
|
dir, h := setup(t)
|
||||||
body, _ := json.Marshal(map[string]any{"content": "auto name"})
|
body, _ := json.Marshal(map[string]any{"content": "auto name"})
|
||||||
|
|||||||
Reference in New Issue
Block a user