71a9a59403
SeedEvent writes the fact at the caller's timestamp, extracts an event from it, and runs the same detectAndPropose the voice path runs. What a seed proves is therefore the daemon's own wiring, not the detector in isolation — which is what an eval-lab fixture would have proved, and is not what the four blocked tasks doubt. The flag is the real lock, not the authority rung. -allow-seed defaults off, and off means daemonAPI.seedStore is nil: the method has nothing to write with rather than permission to refuse. A box that can rewrite its own past says so in its boot log. Seeded facts carry source "seed:qa" and no Subject, so they never queue a Nexus resolution and stay identifiable for the wipe in V-494. Nothing else in the tree writes that source. Best-effort is not the shape here, unlike detectPattern: a seed that half worked is a QA result nobody can trust, so every step reports its own failure. Extraction declining is not a failure, and Extracted says so. Tests cover all four: refused with no flag, four spaced seeds propose and three do not, a value outside the lexicon writes the fact and claims no event, a zero timestamp is refused rather than defaulted to now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
108 lines
4.7 KiB
Go
108 lines
4.7 KiB
Go
// mavend/seed.go — the backdated-fact seam (Vikunja #518).
|
|
//
|
|
// The pattern detector needs four events for one action+object, spread by at
|
|
// least pattern.MinIntervalDays, before it proposes a routine. Nothing could
|
|
// produce that against a running daemon in one sitting: the only writer is a
|
|
// fact write at time.Now(), so V-43, V-46, V-247 and V-254 all stopped at the
|
|
// same missing step and had been stopped there since they were filed.
|
|
//
|
|
// This is the write path that unblocks them, and it is deliberately the narrow
|
|
// one. It takes a fact, not an event, so pattern.Extract runs for real and a
|
|
// key the extractor ignores seeds nothing. It runs detectAndPropose, so what a
|
|
// seed proves is the daemon's own wiring rather than the detector in isolation
|
|
// — which is what an eval-lab fixture would have proved, and is not what those
|
|
// four tasks doubt.
|
|
//
|
|
// It is off unless mavend was started with -allow-seed, and AuthStepUp in the
|
|
// authority table besides. See ipc.SeedEventReq and auth.Requirement.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/pattern"
|
|
"github.com/kami/maven/internal/store"
|
|
)
|
|
|
|
// errSeedDisabled — what a caller gets on an ordinary box. Named rather than
|
|
// inline so the mavweb route can tell "not allowed here" apart from "the seed
|
|
// ran and the extractor declined", which look the same to a reader otherwise.
|
|
var errSeedDisabled = errors.New("mavend: seeding is off (start with -allow-seed)")
|
|
|
|
// seedSource — every seeded fact carries this, and no other writer uses it.
|
|
// The point is that seeded data stays identifiable forever: a fact that came
|
|
// from a QA sitting must never be mistaken for something he said, either by a
|
|
// person reading /history or by the wipe in V-494 when it lands.
|
|
const seedSource = "seed:qa"
|
|
|
|
// seedStoreIfAllowed returns st only when -allow-seed was passed, and logs the
|
|
// fact loudly when it does. A box that can rewrite its own past should say so
|
|
// in its boot log, so nobody reads a seeded routine months later as evidence of
|
|
// something he actually did.
|
|
func seedStoreIfAllowed(st *store.Store) *store.Store {
|
|
if !allowSeedOnStart {
|
|
return nil
|
|
}
|
|
log.Printf("seed: -allow-seed is ON — backdated fact writes are permitted under source %q (Vikunja #518)", seedSource)
|
|
return st
|
|
}
|
|
|
|
// SeedEvent writes the fact at the caller's timestamp, extracts an event from
|
|
// it, and runs the same detect-and-propose step the voice path runs.
|
|
//
|
|
// Best-effort is NOT the shape here, unlike detectPattern: a seed that half
|
|
// worked is a QA result nobody can trust, so every step reports its own
|
|
// failure. Extraction declining is not a failure — it is the extractor's
|
|
// documented answer for a value outside its lexicon, and Extracted says so.
|
|
func (d *daemonAPI) SeedEvent(ctx context.Context, req ipc.SeedEventReq) (ipc.SeedEventResp, error) {
|
|
if d.seedStore == nil {
|
|
return ipc.SeedEventResp{}, errSeedDisabled
|
|
}
|
|
if req.Key == "" || req.Value == "" {
|
|
return ipc.SeedEventResp{}, errors.New("mavend: seed needs a key and a value")
|
|
}
|
|
if req.Ts.IsZero() {
|
|
return ipc.SeedEventResp{}, errors.New("mavend: seed needs an explicit timestamp")
|
|
}
|
|
|
|
// No Subject, unlike the voice path: a seeded key must not queue a Nexus
|
|
// resolution. QA data has no business reaching the ecosystem.
|
|
factID, err := d.seedStore.WriteFact(ctx, req.Ts, store.KindSelf, req.Key, req.Value, seedSource, 1.0, sql.NullInt64{})
|
|
if err != nil {
|
|
return ipc.SeedEventResp{}, fmt.Errorf("seed write fact: %w", err)
|
|
}
|
|
resp := ipc.SeedEventResp{FactID: factID}
|
|
|
|
ev := pattern.Extract(factID, req.Key, req.Value, req.Ts)
|
|
if ev == nil {
|
|
// The fact is written and stays written. Saying so matters: a caller
|
|
// that assumed a seed always produces an event would otherwise read
|
|
// four silent successes and conclude the detector is broken.
|
|
log.Printf("seed: %s=%s wrote fact %d, no event (value outside the action lexicon)", req.Key, req.Value, factID)
|
|
return resp, nil
|
|
}
|
|
resp.Extracted, resp.Action, resp.Object = true, ev.Action, ev.Object
|
|
|
|
eventID, err := d.seedStore.CreateEvent(ctx, factID, ev.Action, ev.Object, req.Ts)
|
|
if err != nil {
|
|
return resp, fmt.Errorf("seed create event: %w", err)
|
|
}
|
|
resp.EventID = eventID
|
|
|
|
r, routineID, err := detectAndPropose(ctx, d.seedStore, ev.Action, ev.Object, req.Ts)
|
|
if err != nil {
|
|
return resp, fmt.Errorf("seed detect: %w", err)
|
|
}
|
|
if r == nil {
|
|
return resp, nil // too few events yet, too irregular, or already decided
|
|
}
|
|
resp.Proposed, resp.RoutineID, resp.IntervalDays = true, routineID, r.IntervalDays
|
|
log.Printf("seed: proposed routine %d — %s/%s every %.1f days", routineID, r.Action, r.Object, r.IntervalDays)
|
|
return resp, nil
|
|
}
|