Skip to content

Commit 6332096

Browse files
atulmguptaCopilot
andcommitted
refactor(R2d.146): carve internal/api/aimlanom subpackage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent edf40c1 commit 6332096

4 files changed

Lines changed: 67 additions & 36 deletions

File tree

internal/api/aimlanom/doc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Package aimlanom provides the AI learned anomaly baseline HTTP handler.
2+
package aimlanom
3+
4+
// Layer: handler

internal/api/ai_ml_anomaly_baseline_handler.go renamed to internal/api/aimlanom/handler.go

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package api
1+
package aimlanom
22

33
// Phase-50 / 0062 — ML1 Learned per-vehicle anomaly baselines.
44
//
@@ -55,6 +55,7 @@ import (
5555
"github.com/ev-dev-labs/teslasync/internal/ai/strategy"
5656
"github.com/ev-dev-labs/teslasync/internal/ai/stream"
5757
"github.com/ev-dev-labs/teslasync/internal/ai/tools"
58+
"github.com/ev-dev-labs/teslasync/internal/api/httpx"
5859
tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
5960
"github.com/ev-dev-labs/teslasync/internal/database"
6061
"github.com/ev-dev-labs/teslasync/internal/ml/anomaly"
@@ -80,21 +81,21 @@ const aiLearnedAnomalyDefaultDays = 7
8081
// the LLM.
8182
const aiLearnedAnomalyMaxDays = 30
8283

83-
// AILearnedAnomalyBaselineHandler is the HTTP handler for
84+
// Handler is the HTTP handler for
8485
// POST /api/v1/ai/ml/anomaly-baselines/train.
8586
//
8687
// Stateless beyond its constructor inputs; safe for concurrent use
8788
// across requests. Construction is in router.go so the dispatcher's
8889
// tool registry + provider registry are wired once at boot.
89-
type AILearnedAnomalyBaselineHandler struct {
90+
type Handler struct {
9091
registry *provider.Registry
9192
tools *tools.Registry
9293
strategy strategy.Strategy
9394
headerName string
9495
maxIters int
9596
}
9697

97-
// NewAILearnedAnomalyBaselineHandler constructs the handler. All
98+
// NewHandler constructs the handler. All
9899
// non-pointer arguments are required; the constructor panics on a
99100
// nil so the wiring bug surfaces at boot, not at first request.
100101
//
@@ -110,21 +111,21 @@ type AILearnedAnomalyBaselineHandler struct {
110111
// (one per process).
111112
//
112113
// headerName: forward-auth header name; used to extract subject for audit.
113-
func NewAILearnedAnomalyBaselineHandler(
114+
func NewHandler(
114115
registry *provider.Registry,
115116
toolReg *tools.Registry,
116117
strat strategy.Strategy,
117118
headerName string,
118-
) *AILearnedAnomalyBaselineHandler {
119+
) *Handler {
119120
switch {
120121
case registry == nil:
121-
panic("api: NewAILearnedAnomalyBaselineHandler: nil provider.Registry")
122+
panic("aimlanom: NewHandler: nil provider.Registry")
122123
case toolReg == nil:
123-
panic("api: NewAILearnedAnomalyBaselineHandler: nil tools.Registry")
124+
panic("aimlanom: NewHandler: nil tools.Registry")
124125
case strat == nil:
125-
panic("api: NewAILearnedAnomalyBaselineHandler: nil strategy.Strategy")
126+
panic("aimlanom: NewHandler: nil strategy.Strategy")
126127
}
127-
return &AILearnedAnomalyBaselineHandler{
128+
return &Handler{
128129
registry: registry,
129130
tools: toolReg,
130131
strategy: strat,
@@ -150,7 +151,7 @@ type aiLearnedAnomalyRequest struct {
150151
// dispatcher's deferred WriteDone. Every error path either writes a
151152
// structured frame onto the SSE stream (when the writer has been
152153
// opened) or a plain JSON 4xx/5xx (before it has).
153-
func (h *AILearnedAnomalyBaselineHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
154+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
154155
// 1) Decode + validate request body.
155156
var body aiLearnedAnomalyRequest
156157
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
@@ -245,27 +246,27 @@ func (h *AILearnedAnomalyBaselineHandler) ServeHTTP(w http.ResponseWriter, r *ht
245246
}
246247
}
247248

248-
// Compile-time assertion: AILearnedAnomalyBaselineHandler satisfies http.Handler.
249-
var _ http.Handler = (*AILearnedAnomalyBaselineHandler)(nil)
249+
// Compile-time assertion: Handler satisfies http.Handler.
250+
var _ http.Handler = (*Handler)(nil)
250251

251-
// AISignalSampleSource is the production *database.DB-backed adapter
252+
// SignalSampleSource is the production *database.DB-backed adapter
252253
// that satisfies anomaly.SignalSampleSource. It runs ONE pgx query
253254
// scoped to the requested vehicle, lookback days, and signal
254255
// allowlist; rows are bucketed in-memory into the per-signal
255256
// observation slices the trainer expects. No new SQL semantics —
256257
// the same signal_log columns the deterministic detector at
257258
// internal/api/anomaly_handler.go already reads.
258-
type AISignalSampleSource struct {
259+
type SignalSampleSource struct {
259260
db *database.DB
260261
}
261262

262-
// NewAISignalSampleSource constructs the adapter. Panics on a nil
263+
// NewSignalSampleSource constructs the adapter. Panics on a nil
263264
// DB so the wiring bug surfaces at boot, not at first request.
264-
func NewAISignalSampleSource(db *database.DB) *AISignalSampleSource {
265+
func NewSignalSampleSource(db *database.DB) *SignalSampleSource {
265266
if db == nil {
266-
panic("api: NewAISignalSampleSource: nil *database.DB")
267+
panic("aimlanom: NewSignalSampleSource: nil *database.DB")
267268
}
268-
return &AISignalSampleSource{db: db}
269+
return &SignalSampleSource{db: db}
269270
}
270271

271272
// SamplesForVehicle implements anomaly.SignalSampleSource. Returns
@@ -277,7 +278,7 @@ func NewAISignalSampleSource(db *database.DB) *AISignalSampleSource {
277278
// signal_log hypertable's primary index is (vehicle_id, ts), so
278279
// the selectivity comes from the time predicate; the field
279280
// allowlist is a final-stage filter.
280-
func (s *AISignalSampleSource) SamplesForVehicle(ctx context.Context, vehicleID int64, days int, signals []string) (map[string][]float64, error) {
281+
func (s *SignalSampleSource) SamplesForVehicle(ctx context.Context, vehicleID int64, days int, signals []string) (map[string][]float64, error) {
281282
out := make(map[string][]float64, len(signals))
282283
for _, sig := range signals {
283284
out[sig] = nil
@@ -295,22 +296,30 @@ func (s *AISignalSampleSource) SamplesForVehicle(ctx context.Context, vehicleID
295296
AND (float_value IS NOT NULL OR int_value IS NOT NULL)`,
296297
vehicleID, since, signals)
297298
if err != nil {
298-
return nil, fmt.Errorf("AISignalSampleSource: vehicle %d days %d: %w", vehicleID, days, err)
299+
return nil, fmt.Errorf("SignalSampleSource: vehicle %d days %d: %w", vehicleID, days, err)
299300
}
300301
defer rows.Close()
301302
for rows.Next() {
302303
var field string
303304
var v float64
304305
if err := rows.Scan(&field, &v); err != nil {
305-
return nil, fmt.Errorf("AISignalSampleSource: scan: %w", err)
306+
return nil, fmt.Errorf("SignalSampleSource: scan: %w", err)
306307
}
307308
out[field] = append(out[field], v)
308309
}
309310
if err := rows.Err(); err != nil {
310-
return nil, fmt.Errorf("AISignalSampleSource: rows.Err: %w", err)
311+
return nil, fmt.Errorf("SignalSampleSource: rows.Err: %w", err)
311312
}
312313
return out, nil
313314
}
314315

315-
// Compile-time assertion: AISignalSampleSource satisfies anomaly.SignalSampleSource.
316-
var _ anomaly.SignalSampleSource = (*AISignalSampleSource)(nil)
316+
// Compile-time assertion: SignalSampleSource satisfies anomaly.SignalSampleSource.
317+
var _ anomaly.SignalSampleSource = (*SignalSampleSource)(nil)
318+
319+
func writeError(w http.ResponseWriter, status int, msg string) {
320+
httpx.WriteError(w, status, msg)
321+
}
322+
323+
func denyAllConfirm(_ context.Context, _ dispatch.ConfirmRequest) (dispatch.ConfirmDecision, error) {
324+
return dispatch.ConfirmDenied, nil
325+
}

internal/api/ai_ml_anomaly_baseline_handler_test.go renamed to internal/api/aimlanom/handler_test.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
// learned-per-vehicle-anomaly-baselines`); duplicating that here
1818
// would require a live database fixture.
1919

20-
package api
20+
package aimlanom
2121

2222
import (
23+
"context"
2324
"encoding/json"
2425
"net/http"
2526
"net/http/httptest"
@@ -31,6 +32,22 @@ import (
3132
"github.com/ev-dev-labs/teslasync/internal/ai/guard"
3233
)
3334

35+
type stubGuardSettings struct {
36+
mode string
37+
on map[string]bool
38+
}
39+
40+
func (s *stubGuardSettings) AIMode(_ context.Context) (string, error) {
41+
if s.mode == "" {
42+
return "off", nil
43+
}
44+
return s.mode, nil
45+
}
46+
47+
func (s *stubGuardSettings) AIFeatureEnabled(_ context.Context, id string) (bool, error) {
48+
return s.on[id], nil
49+
}
50+
3451
// TestLearnedAnomalyBaselineAIOffUsesSafeRangesOnly is the
3552
// load-bearing off-mode contract proof for slice 0062. It mounts
3653
// the AI learned-anomaly-baseline route through the guard with
@@ -123,27 +140,27 @@ func TestLearnedAnomalyBaselineAIOffUsesSafeRangesOnly(t *testing.T) {
123140
}
124141
}
125142

126-
// TestAILearnedAnomalyBaselineHandler_PanicsOnNilWiring asserts the
143+
// TestHandler_PanicsOnNilWiring asserts the
127144
// handler constructor refuses zero-valued dependencies. A wiring
128145
// bug at boot must surface as a panic, not as a nil-deref on first
129146
// request.
130-
func TestAILearnedAnomalyBaselineHandler_PanicsOnNilWiring(t *testing.T) {
147+
func TestHandler_PanicsOnNilWiring(t *testing.T) {
131148
t.Parallel()
132149
defer func() {
133150
if r := recover(); r == nil {
134-
t.Fatal("NewAILearnedAnomalyBaselineHandler(nil,nil,nil,\"\") did not panic")
151+
t.Fatal("NewHandler(nil,nil,nil,\"\") did not panic")
135152
}
136153
}()
137-
NewAILearnedAnomalyBaselineHandler(nil, nil, nil, "")
154+
NewHandler(nil, nil, nil, "")
138155
}
139156

140-
// TestAILearnedAnomalyBaselineHandler_RejectsBadRequestBodies pins
157+
// TestHandler_RejectsBadRequestBodies pins
141158
// the request-validation contract: missing vehicle_id, non-positive
142159
// vehicle_id, and out-of-range days must surface as 4xx BEFORE the
143160
// dispatcher is reached (so a confused caller cannot waste a
144161
// provider call). The baseline-coexistence test above already
145162
// proves the off-mode 404; this test pins the on-mode validator.
146-
func TestAILearnedAnomalyBaselineHandler_RejectsBadRequestBodies(t *testing.T) {
163+
func TestHandler_RejectsBadRequestBodies(t *testing.T) {
147164
t.Parallel()
148165
cases := []struct {
149166
name string
@@ -184,7 +201,7 @@ func TestAILearnedAnomalyBaselineHandler_RejectsBadRequestBodies(t *testing.T) {
184201
}
185202

186203
// validateLearnedAnomalyRequest mirrors the pre-dispatch validation
187-
// block in (*AILearnedAnomalyBaselineHandler).ServeHTTP. Kept in
204+
// block in (*Handler).ServeHTTP. Kept in
188205
// the test file (not exported from the production handler) so a
189206
// future change to ServeHTTP's validation must update both —
190207
// surfacing the divergence rather than letting it drift.

internal/api/router.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
aidigest "github.com/ev-dev-labs/teslasync/internal/api/aidigest"
3232
aidrivecoach "github.com/ev-dev-labs/teslasync/internal/api/aidrivecoach"
3333
aidrivesearch "github.com/ev-dev-labs/teslasync/internal/api/aidrivesearch"
34+
aimlanom "github.com/ev-dev-labs/teslasync/internal/api/aimlanom"
3435
airaghelp "github.com/ev-dev-labs/teslasync/internal/api/airaghelp"
3536
airouteeff "github.com/ev-dev-labs/teslasync/internal/api/airouteeff"
3637
aisearch "github.com/ev-dev-labs/teslasync/internal/api/aisearch"
@@ -1850,20 +1851,20 @@ func NewRouter(db *database.DB, teslaClient *tesla.Client, mqttClient *mqtt.Clie
18501851
// learned-per-vehicle-anomaly-baselines (Phase-50 / ML1, slice
18511852
// 0062) tools — train_anomaly_baseline + query_anomaly_baseline.
18521853
// Both READ-only; the trainer reads signal_log via the
1853-
// AISignalSampleSource adapter and returns a per-signal learned
1854+
// SignalSampleSource adapter and returns a per-signal learned
18541855
// envelope (mean / stddev / p5 / p95) clamped to the static
18551856
// safe-range envelope, with safe-range fallback per signal when
18561857
// fewer than anomaly.DefaultMinSamples observations exist in
18571858
// the lookback window. Tools registered BEFORE the handler is
18581859
// constructed so the dispatcher can resolve the strategy's
18591860
// allowedTools at boot.
18601861
predict.RegisterLearnedAnomalyBaselineTools(aiToolRegistry, predict.LearnedAnomalyBaselineSources{
1861-
Trainer: anomaly.NewTrainer(NewAISignalSampleSource(db)),
1862+
Trainer: anomaly.NewTrainer(aimlanom.NewSignalSampleSource(db)),
18621863
})
18631864
// learned-per-vehicle-anomaly-baselines handler. One per
18641865
// process; stateless beyond constructor inputs. Must be
18651866
// constructed AFTER the tool registration above.
1866-
aiLearnedAnomalyBaselinesHandler := NewAILearnedAnomalyBaselineHandler(
1867+
aiLearnedAnomalyBaselinesHandler := aimlanom.NewHandler(
18671868
aiRegistry,
18681869
aiToolRegistry,
18691870
learnedanomalybaselines.New(),

0 commit comments

Comments
 (0)