93 lines
5.1 KiB
Go
93 lines
5.1 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// AISkillFinding is one thing a console ops skill noticed.
|
|
//
|
|
// ─── Why this table exists ─────────────────────────────────────────────────
|
|
//
|
|
// The eight rule skills (SlaGuardian, DoorstepStall, FleetBalancer,
|
|
// HighValueCod, RiderBatterySafety, HubCongestion, LateDispatch, CashExposure)
|
|
// run in the operator's browser, on a 60-second React Query interval, and their
|
|
// findings were thrown away. Nothing persisted them, so three questions had no
|
|
// answer at all:
|
|
//
|
|
// Has this booking been flagged before, and how many times?
|
|
// Which skills fire most, and which are ignored every time they do?
|
|
// Did the proposal an operator carried out actually clear the finding?
|
|
//
|
|
// The third is the one that matters. The console already re-runs its scan after
|
|
// executing a proposal to see whether the finding disappears — that evidence
|
|
// existed for one render and then vanished. Persisting it turns "the skills seem
|
|
// useful" into something measurable, and it is the raw material for any later
|
|
// per-rider or per-zone memory.
|
|
//
|
|
// ─── What this is NOT ──────────────────────────────────────────────────────
|
|
//
|
|
// Not an exception. `consignmentexceptions` records a real operational problem
|
|
// with a parcel (Lost, Damaged, Misrouted) raised by a person. A finding is a
|
|
// rule noticing a pattern, most of which resolve themselves without anyone
|
|
// doing anything. Writing findings into that table would flood a queue people
|
|
// are supposed to work.
|
|
//
|
|
// Not a decision either: `agent_decisions` is the engine's model-made choices,
|
|
// with embeddings for retrieval. A finding is deterministic and carries no
|
|
// vector.
|
|
type AISkillFinding struct {
|
|
Findingid int `json:"findingid" gorm:"primaryKey;column:findingid;autoIncrement"`
|
|
|
|
// Skillid matches aiskills.skillid, so a finding can be grouped by the rule
|
|
// that raised it and read alongside the thresholds in force at the time.
|
|
Skillid string `json:"skillid" gorm:"column:skillid;size:64;not null;index:idx_aiskillfindings_skill_time,priority:1"`
|
|
|
|
// Fingerprint is what makes a finding the SAME finding across polls. The
|
|
// skills re-evaluate every 60 seconds and will re-raise an unchanged
|
|
// problem every time; without this the table would grow by the number of
|
|
// open findings per minute per operator with the page open.
|
|
//
|
|
// Computed by the console from the skill id and the scope's booking ids —
|
|
// deliberately not including severity or counts, which drift while the
|
|
// underlying problem stays the same.
|
|
Fingerprint string `json:"fingerprint" gorm:"column:fingerprint;size:120;not null;uniqueIndex:uq_aiskillfindings_fingerprint"`
|
|
|
|
Severity string `json:"severity" gorm:"column:severity;size:20"` // critical, warning, info
|
|
Title string `json:"title" gorm:"column:title"`
|
|
|
|
// Proposaltool is the verb the finding proposes, e.g. notifyRider,
|
|
// assignMiler, enforce_cash_handoff. Matches aitools.toolname where one
|
|
// exists; review-only verbs are recorded too, which is how "this skill
|
|
// keeps proposing something nobody can act on" becomes visible.
|
|
Proposaltool string `json:"proposaltool" gorm:"column:proposaltool;size:64;index"`
|
|
|
|
// Bookingcount and Scope: the count is for grouping, the scope is the
|
|
// booking ids as a JSON array so a specific parcel's history can be found.
|
|
// Not a join table — a finding's scope is read whole or not at all, and a
|
|
// row per booking per finding would multiply this table by average scope
|
|
// size for no query anyone needs.
|
|
Bookingcount int `json:"bookingcount" gorm:"column:bookingcount;default:0"`
|
|
Scope string `json:"scope" gorm:"column:scope;type:jsonb"`
|
|
|
|
Tenantid *int `json:"tenantid" gorm:"column:tenantid;index"`
|
|
|
|
// Firstseenat vs Lastseenat: an upsert on fingerprint bumps the last-seen
|
|
// and the count, so how LONG a finding has been open is answerable. That is
|
|
// the actual signal of an ignored finding — not that it exists, but that it
|
|
// has existed for six hours.
|
|
Firstseenat time.Time `json:"firstseenat" gorm:"column:firstseenat;not null;index:idx_aiskillfindings_skill_time,priority:2"`
|
|
Lastseenat time.Time `json:"lastseenat" gorm:"column:lastseenat;not null"`
|
|
Seencount int `json:"seencount" gorm:"column:seencount;default:1"`
|
|
|
|
// Actedat / Actedby / Actionresult record an operator carrying out the
|
|
// proposal. Null means nobody did — which is data, not a gap.
|
|
Actedat *time.Time `json:"actedat" gorm:"column:actedat"`
|
|
Actedby *int `json:"actedby" gorm:"column:actedby"`
|
|
Actionresult string `json:"actionresult" gorm:"column:actionresult;size:20"` // ok, partial, failed
|
|
|
|
// Clearedat is set when a later scan no longer raises this fingerprint.
|
|
// Paired with Actedat it answers the question the whole table is for: did
|
|
// acting clear it, or did it clear on its own?
|
|
Clearedat *time.Time `json:"clearedat" gorm:"column:clearedat"`
|
|
}
|
|
|
|
func (AISkillFinding) TableName() string { return "aiskillfindings" }
|