From 622f5359e8df1a23e7048c01918055ae014a7f98 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Tue, 6 Oct 2026 22:03:40 -0400 Subject: [PATCH] Check a Side quest's reason names the Regular its citation resolved to The reason check compared normalized names, so a reason naming only "Beef Chili" passed for a citation of "Easy Beef Chili". Find the Regulars the reason names with the same find the citations use, taking the longest run of words at each point, and require a cited Regular among them. Co-Authored-By: Claude Opus 5.5 --- internal/ai/skills/suggest_side_quests.go | 30 ++++++++++++++++--- .../ai/skills/suggest_side_quests_test.go | 26 ++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/internal/ai/skills/suggest_side_quests.go b/internal/ai/skills/suggest_side_quests.go index 00a85cb..ff8a6c9 100644 --- a/internal/ai/skills/suggest_side_quests.go +++ b/internal/ai/skills/suggest_side_quests.go @@ -8,6 +8,7 @@ import ( "slices" "strings" "time" + "unicode" "unicode/utf8" "code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/harness" @@ -301,11 +302,32 @@ func drop(q SideQuest, regulars regularNames, known *harness.Deduper, kept []Sid if n := utf8.RuneCountInString(q.Reason); n > ReasonLimit { return nil, fmt.Sprintf("reason has %d characters, over %d", n, ReasonLimit) } - reason := " " + harness.Normalize(q.Reason) + " " - if !slices.ContainsFunc(cited, func(name string) bool { - return strings.Contains(reason, " "+harness.Normalize(name)+" ") - }) { + named := regulars.named(q.Reason) + if !slices.ContainsFunc(cited, func(name string) bool { return slices.Contains(named, name) }) { return nil, "reason names none of its cited Regulars" } return cited, "" } + +// named returns the Regulars text names, each found by find. Reading from +// the left, the longest run of words find takes for one Regular, its outer +// punctuation trimmed, is the Regular named there; a run naming several +// Regulars names none. So "Your Easy Beef Chili." names "Easy Beef Chili" +// and not "Beef Chili". +func (r regularNames) named(text string) []string { + words := strings.Fields(text) + var names []string + for i := 0; i < len(words); i++ { + for j := len(words); j > i; j-- { + run := strings.TrimFunc(strings.Join(words[i:j], " "), func(c rune) bool { + return !unicode.IsLetter(c) && !unicode.IsDigit(c) + }) + if name, why := r.find(run); why == "" { + names = append(names, name) + i = j - 1 + break + } + } + } + return names +} diff --git a/internal/ai/skills/suggest_side_quests_test.go b/internal/ai/skills/suggest_side_quests_test.go index d1f20fc..2c2ab2f 100644 --- a/internal/ai/skills/suggest_side_quests_test.go +++ b/internal/ai/skills/suggest_side_quests_test.go @@ -200,6 +200,32 @@ func TestSuggestSideQuestsSharedNormalizedRegularNotRewritten(t *testing.T) { }) } +func TestSuggestSideQuestsReasonNamesResolvedRegular(t *testing.T) { + in := household + in.Context.Regulars = append(slices.Clone(household.Context.Regulars), + harness.RegularDinner{Name: "Easy Beef Chili", Cuisine: "Tex-Mex", Method: "stovetop", Minutes: 30}) + other := sideQuest("Chili Verde", "Your Beef Chili gone green, with pork and tomatillos.", "Easy Beef Chili") + easy := sideQuest("Frito Pie", "Your Easy Beef Chili over corn chips.", "Easy Beef Chili") + plain := sideQuest("Chili Mac", "The Beef Chili, not the Easy Beef Chili, over macaroni.", "Beef Chili") + inside := sideQuest("Tamale Bake", "Your Easy Beef Chili under a cornbread crust.", "Beef Chili") + var f llm.Fake + f.QueueGenerate(reply(batch(other, easy, plain, inside))) + out, _, err := suggest(t, &f, in, 1) + if err != nil { + t.Fatal(err) + } + assertSideQuests(t, out, SideQuests{ + Suggestions: []SideQuest{ + want("Frito Pie", "Your Easy Beef Chili over corn chips.", "Easy Beef Chili"), + want("Chili Mac", "The Beef Chili, not the Easy Beef Chili, over macaroni.", "Beef Chili"), + }, + Dropped: []DroppedSideQuest{ + {Name: "Chili Verde", Why: "reason names none of its cited Regulars"}, + {Name: "Tamale Bake", Why: "reason names none of its cited Regulars"}, + }, + }) +} + func TestSuggestSideQuestsDuplicatesDropped(t *testing.T) { declined := sideQuest("The Best Homemade Chicken Tikka-Masala", "Spiced like your Chicken Fajitas.", "Chicken Fajitas") oldDeclined := sideQuest("Fish Tacos!", "Tortillas like your Chicken Fajitas.", "Chicken Fajitas")