1- package api
1+ package aiinboxcat
22
33// Phase-50 / 0035 — A2 Inbox auto-categorization.
44//
@@ -65,40 +65,49 @@ import (
6565 "github.com/ev-dev-labs/teslasync/internal/ai/stream"
6666 "github.com/ev-dev-labs/teslasync/internal/ai/tools"
6767 "github.com/ev-dev-labs/teslasync/internal/ai/tools/nl"
68+ "github.com/ev-dev-labs/teslasync/internal/api/httpx"
6869 tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
6970 dbalert "github.com/ev-dev-labs/teslasync/internal/database/alert"
7071 dbnotif "github.com/ev-dev-labs/teslasync/internal/database/notification"
7172)
7273
73- // aiInboxCategorizationMaxIterations bounds the dispatcher's
74+ // maxIterations bounds the dispatcher's
7475// tool-loop. The strategy is at most draft_alert_categories ->
7576// validate_alert_category(per label) -> answer (with optional
7677// retries). A hard ceiling of 8 is generous and matches the
7778// other A-tier propose-only handlers.
78- const aiInboxCategorizationMaxIterations = 8
79+ const maxIterations = 8
7980
80- // aiInboxCategorizationDefaultWindowDays is the trailing-window
81+ // defaultWindowDays is the trailing-window
8182// length the production InboxCategorizationSource adapter
8283// projects across when reading the recent notification_logs.
8384// 7 mirrors the canonical "recent" window used elsewhere in
8485// Phase-50.
85- const aiInboxCategorizationDefaultWindowDays = 7
86+ const defaultWindowDays = 7
8687
87- // aiInboxCategorizationMinEvents is the minimum total
88+ // minEvents is the minimum total
8889// notification_logs row count (across the trailing window) the
8990// adapter requires before it lets the narrator quote per-
9091// category counts. Below this threshold has_enough_history flips
9192// false and the narrator says so plainly. 10 is the minimum
9293// sample for a meaningful descriptive tally — fewer notifications
9394// makes any per-category breakdown statistically meaningless.
94- const aiInboxCategorizationMinEvents = 10
95+ const minEvents = 10
9596
96- // aiInboxCategorizationRequest is the JSON body shape this
97+ func writeError (w http.ResponseWriter , status int , msg string ) {
98+ httpx .WriteError (w , status , msg )
99+ }
100+
101+ func denyAllConfirm (_ context.Context , _ dispatch.ConfirmRequest ) (dispatch.ConfirmDecision , error ) {
102+ return dispatch .ConfirmDenied , nil
103+ }
104+
105+ // request is the JSON body shape this
97106// handler accepts. Body is OPTIONAL (an empty body is accepted;
98107// the handler defaults to the entire inbox over the last
99- // aiInboxCategorizationDefaultWindowDays days). Mirrors how the
108+ // defaultWindowDays days). Mirrors how the
100109// canonical NotificationFilterBar URL params already work.
101- type aiInboxCategorizationRequest struct {
110+ type request struct {
102111 // VehicleID restricts the recent window to a single
103112 // vehicle. Optional — when absent the handler returns
104113 // the per-category counts across every vehicle the
@@ -109,7 +118,7 @@ type aiInboxCategorizationRequest struct {
109118 VehicleID * int64 `json:"vehicle_id,omitempty"`
110119
111120 // WindowDays is the lookback in days. Defaults to
112- // aiInboxCategorizationDefaultWindowDays when nil.
121+ // defaultWindowDays when nil.
113122 // Capped at 90 by the tool's input validator so a
114123 // runaway request cannot scan an unbounded range.
115124 WindowDays * int `json:"window_days,omitempty"`
@@ -124,22 +133,22 @@ type aiInboxCategorizationRequest struct {
124133 RuleIDs []int64 `json:"rule_ids,omitempty"`
125134}
126135
127- // AIInboxCategorizationHandler is the HTTP handler for
136+ // Handler is the HTTP handler for
128137// POST /api/v1/ai/alerts/inbox/categorize.
129138//
130139// Stateless beyond its constructor inputs; safe for concurrent
131140// use across requests. Construction is in router.go so the
132141// dispatcher's tool registry + provider registry are wired once
133142// at boot.
134- type AIInboxCategorizationHandler struct {
143+ type Handler struct {
135144 registry * provider.Registry
136145 tools * tools.Registry
137146 strategy strategy.Strategy
138147 headerName string
139148 maxIters int
140149}
141150
142- // NewAIInboxCategorizationHandler constructs the handler. All
151+ // NewHandler constructs the handler. All
143152// non-pointer arguments are required; the constructor panics on
144153// a nil so the wiring bug surfaces at boot, not at first
145154// request.
@@ -155,26 +164,26 @@ type AIInboxCategorizationHandler struct {
155164// headerName: forward-auth header name; used to extract subject
156165//
157166// for audit.
158- func NewAIInboxCategorizationHandler (
167+ func NewHandler (
159168 registry * provider.Registry ,
160169 toolReg * tools.Registry ,
161170 strat strategy.Strategy ,
162171 headerName string ,
163- ) * AIInboxCategorizationHandler {
172+ ) * Handler {
164173 switch {
165174 case registry == nil :
166- panic ("api: NewAIInboxCategorizationHandler : nil provider.Registry" )
175+ panic ("aiinboxcat: NewHandler : nil provider.Registry" )
167176 case toolReg == nil :
168- panic ("api: NewAIInboxCategorizationHandler : nil tools.Registry" )
177+ panic ("aiinboxcat: NewHandler : nil tools.Registry" )
169178 case strat == nil :
170- panic ("api: NewAIInboxCategorizationHandler : nil strategy.Strategy" )
179+ panic ("aiinboxcat: NewHandler : nil strategy.Strategy" )
171180 }
172- return & AIInboxCategorizationHandler {
181+ return & Handler {
173182 registry : registry ,
174183 tools : toolReg ,
175184 strategy : strat ,
176185 headerName : headerName ,
177- maxIters : aiInboxCategorizationMaxIterations ,
186+ maxIters : maxIterations ,
178187 }
179188}
180189
@@ -187,8 +196,8 @@ func NewAIInboxCategorizationHandler(
187196// Severity values are validated against the canonical {info,
188197// warn, critical} set; vehicle_id and rule_id values must be
189198// positive when provided; window_days must be in [1, 90].
190- func parseInboxCategorizationBody (w http.ResponseWriter , r * http.Request ) (* aiInboxCategorizationRequest , bool ) {
191- req := & aiInboxCategorizationRequest {}
199+ func parseInboxCategorizationBody (w http.ResponseWriter , r * http.Request ) (* request , bool ) {
200+ req := & request {}
192201 if r .Body == nil {
193202 return req , true
194203 }
@@ -237,7 +246,7 @@ func parseInboxCategorizationBody(w http.ResponseWriter, r *http.Request) (*aiIn
237246// path either writes a structured frame onto the SSE stream
238247// (when the writer has been opened) or a plain JSON 4xx/5xx
239248// (before it has).
240- func (h * AIInboxCategorizationHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
249+ func (h * Handler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
241250 // 1) Parse + validate body. A malformed body fails fast
242251 // with a JSON 400 before any provider lookup.
243252 body , ok := parseInboxCategorizationBody (w , r )
@@ -301,7 +310,7 @@ func (h *AIInboxCategorizationHandler) ServeHTTP(w http.ResponseWriter, r *http.
301310 // body's optional vehicle_id is mentioned only as a hint
302311 // — the typed envelope returned by draft_alert_categories
303312 // is the source of truth.
304- windowDays := aiInboxCategorizationDefaultWindowDays
313+ windowDays := defaultWindowDays
305314 if body .WindowDays != nil {
306315 windowDays = * body .WindowDays
307316 }
@@ -348,9 +357,9 @@ func (h *AIInboxCategorizationHandler) ServeHTTP(w http.ResponseWriter, r *http.
348357 }
349358}
350359
351- // Compile-time assertion: AIInboxCategorizationHandler satisfies
360+ // Compile-time assertion: Handler satisfies
352361// http.Handler.
353- var _ http.Handler = (* AIInboxCategorizationHandler )(nil )
362+ var _ http.Handler = (* Handler )(nil )
354363
355364// ---------------------------------------------------------------------
356365// Production wiring for the InboxCategorizationSource port declared by
@@ -359,7 +368,7 @@ var _ http.Handler = (*AIInboxCategorizationHandler)(nil)
359368// the AIAlertTuningSource pattern from slice 0034.
360369// ---------------------------------------------------------------------
361370
362- // AIInboxCategorizationSource is the production
371+ // Source is the production
363372// nl.InboxCategorizationSource. It composes the canonical
364373// NotificationRepo (read) + AlertRuleRepo (read) so the AI
365374// projection is grounded in the SAME notification_logs +
@@ -368,22 +377,22 @@ var _ http.Handler = (*AIInboxCategorizationHandler)(nil)
368377//
369378// The struct holds two narrow read interfaces; the constructor
370379// panics on a nil so a wiring bug surfaces at boot.
371- type AIInboxCategorizationSource struct {
380+ type Source struct {
372381 notifications * dbnotif.NotificationRepo
373382 rules * dbalert.AlertRuleRepo
374383}
375384
376- // NewAIInboxCategorizationSource constructs the adapter. Panics
385+ // NewSource constructs the adapter. Panics
377386// on a nil repo so a wiring mistake surfaces at boot rather
378387// than as a nil-deref on first AI request.
379- func NewAIInboxCategorizationSource (notifications * dbnotif.NotificationRepo , rules * dbalert.AlertRuleRepo ) * AIInboxCategorizationSource {
388+ func NewSource (notifications * dbnotif.NotificationRepo , rules * dbalert.AlertRuleRepo ) * Source {
380389 if notifications == nil {
381- panic ("api: NewAIInboxCategorizationSource : nil *dbnotif.NotificationRepo" )
390+ panic ("aiinboxcat: NewSource : nil *dbnotif.NotificationRepo" )
382391 }
383392 if rules == nil {
384- panic ("api: NewAIInboxCategorizationSource : nil *dbalert.AlertRuleRepo" )
393+ panic ("aiinboxcat: NewSource : nil *dbalert.AlertRuleRepo" )
385394 }
386- return & AIInboxCategorizationSource {notifications : notifications , rules : rules }
395+ return & Source {notifications : notifications , rules : rules }
387396}
388397
389398// LoadCategoryCounts implements
@@ -400,7 +409,7 @@ func NewAIInboxCategorizationSource(notifications *dbnotif.NotificationRepo, rul
400409// CategoryCount.Count across the returned slice (NOT the raw
401410// notification_logs row count — rows whose alert_id is missing
402411// from the rules lookup bucket into "other").
403- func (a * AIInboxCategorizationSource ) LoadCategoryCounts (ctx context.Context , f dbnotif.NotificationLogFilters ) ([]nl.CategoryCount , int , int , error ) {
412+ func (a * Source ) LoadCategoryCounts (ctx context.Context , f dbnotif.NotificationLogFilters ) ([]nl.CategoryCount , int , int , error ) {
404413 // Defence in depth: clamp the limit so a runaway caller
405414 // cannot blow past the canonical 1000-row cap. The tool
406415 // already sets Limit=1000; this is belt-and-suspenders.
@@ -463,5 +472,5 @@ func (a *AIInboxCategorizationSource) LoadCategoryCounts(ctx context.Context, f
463472 total += c .Count
464473 }
465474
466- return counts , total , aiInboxCategorizationMinEvents , nil
475+ return counts , total , minEvents , nil
467476}
0 commit comments