Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions internal/index/hybrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,19 @@ func (ix *Index) memoryHits(p *provider.Provider, model, query string, k int) []
// ftsQuery turns free text into a safe FTS5 OR-query. FTS5 treats bare
// punctuation as syntax, so we keep only alphanumeric tokens and OR them —
// recall-oriented, since fusion with vectors sorts out precision.
//
// Punctuation inside a word splits it into a phrase rather than being dropped.
// The tokenizer indexed WG-4471 as wg then 4471, so gluing the query into
// "WG4471" matched nothing; the phrase "WG 4471" matches the identifier as a
// unit, and not a note that only has wg and 4471 apart.
func ftsQuery(q string) string {
var tokens []string
for _, f := range strings.Fields(q) {
var b strings.Builder
for _, r := range f {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
if b.Len() > 1 {
tokens = append(tokens, `"`+b.String()+`"`)
pieces := strings.FieldsFunc(f, func(r rune) bool {
return !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9')
})
if len(strings.Join(pieces, "")) > 1 {
tokens = append(tokens, `"`+strings.Join(pieces, " ")+`"`)
}
}
return strings.Join(tokens, " OR ")
Expand Down
19 changes: 19 additions & 0 deletions internal/index/hybrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,22 @@ func contains(s, sub string) bool {
}
return false
}

// The tokenizer splits WG-4471 into wg and 4471. A query that glued the pieces
// back into "WG4471" matched neither, so the identifiers a user types to find
// an exact note — part numbers, ticket ids, versions, kebab-case names — were
// the ones the lexical arm could not find.
func TestAnIdentifierWithPunctuationIsFoundExactlyAsWritten(t *testing.T) {
ix := newTestIndex(t)
seed(t, ix, "quote", "Quote", "part WG-4471 ships in ENG-1234, fixed in 0.4.10 on auto-extract")
seed(t, ix, "other", "Other", "wg 9 and 4471 apart, 1234 before eng")
for _, q := range []string{"WG-4471", "ENG-1234", "0.4.10", "auto-extract", "where is WG-4471?"} {
got, err := ix.lexical(q, 5)
if err != nil {
t.Fatal(err)
}
if len(got) == 0 || got[0] != "quote" {
t.Errorf("lexical(%q) = %v, want the note that contains it first", q, got)
}
}
}
18 changes: 12 additions & 6 deletions internal/ingest/distil.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ func citedTurns(claim string) []int {
// - Every claim names a turn. No turn number, no entry.
// - The turn must exist and must have been served: a citation to an abridged
// turn is a citation to evidence the distiller never saw.
// - A `verified` entry additionally needs an observed successful tool result
// in one of its cited turns. The distiller may summarise evidence; it may
// - A `verified` entry additionally needs an observed successful command in
// one of its cited turns. The distiller may summarise evidence; it may
// not supply it. "The build passes" backed only by an assistant saying so is
// the exact claim that makes the next agent skip running the build.
//
Expand Down Expand Up @@ -126,7 +126,13 @@ func Filter(ev Evidence, d Distillation) (Distillation, []Drop) {
}
ok = true
t := ev.Turns[n-1]
if t.Role == "tool" && t.Status == "ok" {
// Only a command that ran demonstrates anything. A successful
// Edit proves the file was written, and "the crash is gone"
// citing one was kept as verified. Matching the shell names
// rather than excluding the edit names fails safe: a tool not
// on the list costs a claim, a missed edit name would let one
// through.
if t.Role == "tool" && t.Status == "ok" && isShellTool(t.Tool) {
sawSuccess = true
}
}
Expand All @@ -135,7 +141,7 @@ func Filter(ev Evidence, d Distillation) (Distillation, []Drop) {
continue
}
if needSuccess && !sawSuccess {
drops = append(drops, Drop{field, claim, "cites no turn holding a successful command or tool result"})
drops = append(drops, Drop{field, claim, "cites no turn holding a command that ran successfully — a file edit or read is not a demonstration"})
continue
}
kept = append(kept, claim)
Expand Down Expand Up @@ -295,14 +301,14 @@ func (ev Evidence) Render() string {
if ev.Record != "" {
fmt.Fprintf(&b, "Distil this into verified / failed / decided and send it back with ingest_distil, session %q.\n", ev.Record)
b.WriteString("Every verified, failed and decided entry must name the turn it came from, as \"turn 12\".\n")
b.WriteString("A verified entry needs a successful command or tool result in the turn it cites;\n")
b.WriteString("A verified entry needs a command that ran successfully in the turn it cites — a file edit is not one;\n")
b.WriteString("an assistant saying something worked is not an observation of it working.\n")
b.WriteString("Uncited entries are dropped before anything is written, and what is kept is shown as inferred, never as the session's own record.\n")
return b.String()
}
b.WriteString("Distil this into verified / failed / next and send it back with ingest_distil.\n")
b.WriteString("Every verified and failed entry must name the turn it came from, as \"turn 12\".\n")
b.WriteString("A verified entry needs a successful command or tool result in the turn it cites;\n")
b.WriteString("A verified entry needs a command that ran successfully in the turn it cites — a file edit is not one;\n")
b.WriteString("an assistant saying something worked is not an observation of it working.\n")
b.WriteString("Uncited entries are dropped before anything is written.\n")
return b.String()
Expand Down
26 changes: 26 additions & 0 deletions internal/ingest/distil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,29 @@ func TestAcceptValidatesAgainstTheWindowThatWasServed(t *testing.T) {
t.Fatalf("a claim about an unshown turn was written: failed=%v drops=%+v", c.Failed, drops)
}
}

// A file edit that succeeded proves the file was written, not that anything it
// changed works. A real distillation kept "the crash is gone (turn 2)" where
// turn 2 was a Claude Code Edit with status ok, and the next agent would have
// read that as a demonstration and skipped the test run that was never made.
func TestAVerifiedClaimCitingOnlyASuccessfulFileEditIsDropped(t *testing.T) {
ev := ingest.Evidence{
Turns: []transcript.Turn{
{Role: "user", Text: "fix the crash on an empty cart"},
{Role: "tool", Tool: "Edit", Input: "internal/cart/cart.go", Status: "ok"},
{Role: "tool", Tool: "Bash", Input: "go test ./internal/cart", Status: "ok"},
},
Served: map[int]bool{1: true, 2: true, 3: true},
}

got, drops := ingest.Filter(ev, ingest.Distillation{
Verified: []string{"the crash is gone (turn 2)", "the cart tests pass (turn 3)"},
})

if len(got.Verified) != 1 || !strings.Contains(got.Verified[0], "turn 3") {
t.Fatalf("verified = %v, want only the claim citing the command that ran", got.Verified)
}
if len(drops) != 1 || !strings.Contains(drops[0].Claim, "turn 2") {
t.Fatalf("drops = %+v, want the edit-only claim reported", drops)
}
}
6 changes: 5 additions & 1 deletion internal/ingest/harvest.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ func filesInCommand(cmd string) []string {
if strings.HasPrefix(tok, "-") {
continue
}
if !strings.Contains(tok, "/") && !hasSourceExt(tok) {
// A slash alone is not a file: `go test ./cart`, `cd /repo` and a sed
// expression all have one and name a package, a directory and a
// substitution. A file at the end of a path has an extension; one
// without is lost here, which costs a bullet the command line still shows.
if !hasSourceExt(tok) && !(strings.Contains(tok, "/") && len(path.Ext(tok)) > 1) {
continue
}
if strings.ContainsAny(tok, "*?$`") {
Expand Down
19 changes: 19 additions & 0 deletions internal/ingest/ingest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,22 @@ func TestACandidateWhoseTimestampCannotBeReadSaysSoInsteadOfLookingHealthy(t *te
t.Error("a candidate with no timestamps at all was reported as damaged")
}
}

// `go test ./cart` names a package, not a file. Taking every argument with a
// slash in it as a file put "cart" in a record's Files beside the cart.go that
// was actually edited, and the next agent reads Files as what was touched.
func TestAPackageArgumentToACommandIsNotListedAsATouchedFile(t *testing.T) {
c := ingest.Harvest(&transcript.Session{
Harness: "claude-code",
ID: "s",
Turns: []transcript.Turn{
{Role: "tool", Tool: "Edit", Input: "/Users/alice/shop/cart/cart.go", Status: "ok"},
{Role: "tool", Tool: "Bash", Input: "cd /Users/alice/shop && go test ./cart ./internal/... && sed -i '' s/a/b/ cart/notes.md", Status: "ok"},
},
})

want := []string{"/Users/alice/shop/cart/cart.go", "cart/notes.md"}
if strings.Join(c.Files, " ") != strings.Join(want, " ") {
t.Fatalf("files = %v, want %v — only what a command names as a file", c.Files, want)
}
}
2 changes: 1 addition & 1 deletion internal/mcpserver/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ var toolDefs = []map[string]any{
{
"name": "ingest_distil",
"annotations": writes(false, true),
"description": "Send back your distillation of a session served by ingest_harvest: what was verified, what didn't work, what's next. Every verified and failed entry must name the turn it came from (\"turn 12\"), and a verified entry needs a successful command or tool result in that turn — uncited or unsupported entries are dropped and reported. For a queued session it writes a candidate for the user to review, never a checkpoint; for an auto record it writes verified, failed and decided into that record as inferred from the transcript, shown apart from what an agent stated." + relay,
"description": "Send back your distillation of a session served by ingest_harvest: what was verified, what didn't work, what's next. Every verified and failed entry must name the turn it came from (\"turn 12\"), and a verified entry needs a command that ran successfully in that turn (a file edit is not one) — uncited or unsupported entries are dropped and reported. For a queued session it writes a candidate for the user to review, never a checkpoint; for an auto record it writes verified, failed and decided into that record as inferred from the transcript, shown apart from what an agent stated." + relay,
"inputSchema": obj(map[string]any{
"session": str("the session id or auto record path you were served"),
"verified": arrStr("what the session actually established, each entry citing its turn, e.g. 'the suite passes after the region fix (turn 14)'"),
Expand Down
Loading