mavend: run the persona checks over the clarify prose, document the proposal cooldown
clarifyExpiredVariants and clarifyGaveUp are hand-written Russian that the phrasing eval never sees, because they never pass through the phraser. They carry feminine self-reference and a plain imperative, and they are the lines a later edit reaches for a synonym in. A table test now runs the eval's own feminine, his-gender, address and cringe checks over them and over clarifyQuestions. The apology clause of the cringe check is skipped with its reason written down: it exists so a greenlit nudge is not undercut, and a reply to a request she failed to parse is the opposite case. Also two notes and no behaviour change. announceProposal now says what its cooldown does and does not do: detectAndPropose returns non-nil only for a newly created row, so the first tick over a populated history announces one pattern and silences the rest permanently, and the cooldown only spaces genuinely new pairs found later. A queue would be needed for "one per day until each is mentioned". The duplicated Cooldown default is explained as cover for a tickLoop built in a test without going through Load. The -reembed flag help says the daemon does not answer until the backfill finishes. Found in review of #50, #54. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/kami/maven/internal/dialogue"
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/phraser/eval"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/store"
|
||||
"github.com/kami/maven/internal/tool"
|
||||
@@ -394,6 +395,46 @@ func TestClarifySecondGapRespectsTheAttemptCap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestClarifyProseHoldsThePersona — these lines are hand-written Russian that
|
||||
// the phrasing eval never sees, because they never go through the phraser. They
|
||||
// carry feminine self-reference ("ждала", "отпустила") and address him with a
|
||||
// plain imperative, and they are exactly the kind of string someone later edits
|
||||
// reaching for a synonym. Run the eval's own persona checks over them here.
|
||||
func TestClarifyProseHoldsThePersona(t *testing.T) {
|
||||
// Only the persona checks. Length and on-topic do not apply: these are not
|
||||
// nudges, they have no rule to be on topic about, and the expiry lines are
|
||||
// deliberately longer than a nudge ceiling.
|
||||
want := map[string]bool{
|
||||
eval.CheckFeminine: true,
|
||||
eval.CheckHisGender: true,
|
||||
eval.CheckAddress: true,
|
||||
eval.CheckCringe: true,
|
||||
}
|
||||
lines := append([]string{clarifyGaveUp}, clarifyExpiredVariants...)
|
||||
for _, q := range clarifyQuestions {
|
||||
lines = append(lines, q)
|
||||
}
|
||||
for _, line := range lines {
|
||||
for _, r := range eval.RunChecks(eval.Case{}, line, "neutral") {
|
||||
// The apology clause of the cringe check is scoped to nudges: it
|
||||
// exists because apologising for a greenlit nudge undermines it.
|
||||
// These lines are the opposite case. She did not understand him, or
|
||||
// she let his request go, and "прости" there is ordinary speech
|
||||
// rather than grovelling. Every other cringe rule still applies:
|
||||
// pet names, emoji, exclamations, fake concern, praise.
|
||||
// checkCringe returns the first break it finds, so this skip also
|
||||
// hides a later one in the same line. Kept narrow on purpose: it
|
||||
// only fires on a leading "apology (…)" detail.
|
||||
if r.Name == eval.CheckCringe && strings.HasPrefix(r.Detail, "apology") {
|
||||
continue
|
||||
}
|
||||
if want[r.Name] && !r.Pass {
|
||||
t.Errorf("%q fails %s: %s", line, r.Name, r.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestExpiryNoticeSurvivesAConfirmTurn — she asks a question, he walks off, the
|
||||
// question expires, he comes back and answers a confirm that is still parked.
|
||||
// The confirm turn used to return before the notice was even computed, so he
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ func main() {
|
||||
func run(args []string) error {
|
||||
cfgPath := flag.String("config", defaultConfigPath(), "path to mavend JSON config")
|
||||
wrappedKeyPath := flag.String("wrapped-key-file", "", "path to wrapped encryption key blob (enables cold-start unlock)")
|
||||
reembed := flag.Bool("reembed", false, "re-embed every stored note and fact with the configured embedder, then serve normally (run once after an embedder swap)")
|
||||
reembed := flag.Bool("reembed", false, "re-embed every stored note and fact with the configured embedder, then serve normally (run once after an embedder swap; the daemon does not answer until it finishes)")
|
||||
flag.CommandLine.Parse(args)
|
||||
reembedOnStart = *reembed
|
||||
cfg, err := config.Load(*cfgPath)
|
||||
|
||||
@@ -417,6 +417,8 @@ func (t *tickLoop) detectPatterns(ctx context.Context, now time.Time, state loop
|
||||
log.Printf("tick: proposed routine: %s/%s every %.1f days", r.Action, r.Object, r.IntervalDays)
|
||||
// One announcement per tick at most, whatever the scan turned up. The
|
||||
// rest are on /routines; they are not lost, they are just not shouted.
|
||||
// Nor are they queued: the row now exists, so no later tick re-detects
|
||||
// them and they are never announced. See announceProposal.
|
||||
if announced {
|
||||
continue
|
||||
}
|
||||
@@ -437,6 +439,21 @@ func (t *tickLoop) detectPatterns(ctx context.Context, now time.Time, state loop
|
||||
// re-detect it and nothing queues up behind it. A missed announcement means
|
||||
// he reads it on /routines instead, which is the whole point of the page.
|
||||
//
|
||||
// What the cooldown is and is not. detectAndPropose returns non-nil only for a
|
||||
// newly created row, so a pair gets exactly one chance to be spoken: the tick
|
||||
// that first proposes it. Combined with one announcement per tick, the first
|
||||
// tick over a populated history announces one pattern and permanently silences
|
||||
// every other pattern found in the same pass. That is the intent, not an
|
||||
// oversight — an inferred routine is not worth a second attempt at his
|
||||
// attention, and /routines lists all of them. So the cooldown does not drain a
|
||||
// backlog. It only spaces announcements of genuinely new pairs discovered on
|
||||
// later ticks. If it should ever become "one per day until each is mentioned",
|
||||
// that needs a queue rather than this counter.
|
||||
//
|
||||
// Cooldown gets its default here as well as in applyDefaults. That is
|
||||
// deliberate: a tickLoop assembled directly in a test never goes through Load,
|
||||
// and an unspaced announcer is not what those tests mean to exercise.
|
||||
//
|
||||
// The body is the detector's own literal Russian phrasing (pattern.PhraseRoutine
|
||||
// — "ты заправляешь поилку раз в 7 дней — напоминать?"), not LLM-generated, so
|
||||
// an inferred routine cannot arrive worded as something Maven never observed.
|
||||
|
||||
Reference in New Issue
Block a user