Skip to content

Commit 356799d

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

4 files changed

Lines changed: 59 additions & 28 deletions

File tree

internal/api/aicrossrule/doc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Package aicrossrule contains the AI cross-rule conflict detection handler.
2+
//
3+
// Layer: handler
4+
package aicrossrule

internal/api/ai_cross_rule_conflict_handler.go renamed to internal/api/aicrossrule/handler.go

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package api
1+
package aicrossrule
22

33
// Phase-50 / 0036 — A3 Cross-rule conflict detection.
44
//
@@ -73,6 +73,7 @@ import (
7373
"github.com/ev-dev-labs/teslasync/internal/ai/stream"
7474
"github.com/ev-dev-labs/teslasync/internal/ai/tools"
7575
"github.com/ev-dev-labs/teslasync/internal/ai/tools/diagnostic"
76+
"github.com/ev-dev-labs/teslasync/internal/api/httpx"
7677
tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
7778
dbalert "github.com/ev-dev-labs/teslasync/internal/database/alert"
7879
)
@@ -122,40 +123,40 @@ type aiCrossRuleConflictRequest struct {
122123
Limit *int `json:"limit,omitempty"`
123124
}
124125

125-
// AICrossRuleConflictHandler is the HTTP handler for
126+
// Handler is the HTTP handler for
126127
// POST /api/v1/ai/alerts/rules/conflicts.
127128
//
128129
// Stateless beyond its constructor inputs; safe for concurrent
129130
// use across requests. Construction is in router.go so the
130131
// dispatcher's tool registry + provider registry are wired
131132
// once at boot.
132-
type AICrossRuleConflictHandler struct {
133+
type Handler struct {
133134
registry *provider.Registry
134135
tools *tools.Registry
135136
strategy strategy.Strategy
136137
headerName string
137138
maxIters int
138139
}
139140

140-
// NewAICrossRuleConflictHandler constructs the handler. All
141+
// NewHandler constructs the handler. All
141142
// non-pointer arguments are required; the constructor panics
142143
// on a nil so the wiring bug surfaces at boot, not at first
143144
// request.
144-
func NewAICrossRuleConflictHandler(
145+
func NewHandler(
145146
registry *provider.Registry,
146147
toolReg *tools.Registry,
147148
strat strategy.Strategy,
148149
headerName string,
149-
) *AICrossRuleConflictHandler {
150+
) *Handler {
150151
switch {
151152
case registry == nil:
152-
panic("api: NewAICrossRuleConflictHandler: nil provider.Registry")
153+
panic("aicrossrule: NewHandler: nil provider.Registry")
153154
case toolReg == nil:
154-
panic("api: NewAICrossRuleConflictHandler: nil tools.Registry")
155+
panic("aicrossrule: NewHandler: nil tools.Registry")
155156
case strat == nil:
156-
panic("api: NewAICrossRuleConflictHandler: nil strategy.Strategy")
157+
panic("aicrossrule: NewHandler: nil strategy.Strategy")
157158
}
158-
return &AICrossRuleConflictHandler{
159+
return &Handler{
159160
registry: registry,
160161
tools: toolReg,
161162
strategy: strat,
@@ -217,7 +218,7 @@ func parseCrossRuleConflictBody(w http.ResponseWriter, r *http.Request) (*aiCros
217218
// path either writes a structured frame onto the SSE stream
218219
// (when the writer has been opened) or a plain JSON 4xx/5xx
219220
// (before it has).
220-
func (h *AICrossRuleConflictHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
221+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
221222
body, ok := parseCrossRuleConflictBody(w, r)
222223
if !ok {
223224
return
@@ -303,9 +304,17 @@ func (h *AICrossRuleConflictHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
303304
}
304305
}
305306

306-
// Compile-time assertion: AICrossRuleConflictHandler satisfies
307+
// Compile-time assertion: Handler satisfies
307308
// http.Handler.
308-
var _ http.Handler = (*AICrossRuleConflictHandler)(nil)
309+
var _ http.Handler = (*Handler)(nil)
310+
311+
func writeError(w http.ResponseWriter, status int, msg string) {
312+
httpx.WriteError(w, status, msg)
313+
}
314+
315+
func denyAllConfirm(_ context.Context, _ dispatch.ConfirmRequest) (dispatch.ConfirmDecision, error) {
316+
return dispatch.ConfirmDenied, nil
317+
}
309318

310319
// ---------------------------------------------------------------------
311320
// Production wiring for the CrossRuleConflictSource port declared by
@@ -314,26 +323,26 @@ var _ http.Handler = (*AICrossRuleConflictHandler)(nil)
314323
// AIInboxCategorizationSource pattern from slice 0035.
315324
// ---------------------------------------------------------------------
316325

317-
// AICrossRuleConflictSource is the production
326+
// Source is the production
318327
// diagnostic.CrossRuleConflictSource. It composes the canonical
319328
// AlertRuleRepo (read-only) so the AI projection is grounded
320329
// in the SAME alert_rules rows the deterministic AlertStudio
321330
// renders. No write path is invoked.
322331
//
323332
// The struct holds one narrow read interface; the constructor
324333
// panics on a nil so a wiring bug surfaces at boot.
325-
type AICrossRuleConflictSource struct {
334+
type Source struct {
326335
rules *dbalert.AlertRuleRepo
327336
}
328337

329-
// NewAICrossRuleConflictSource constructs the adapter. Panics
338+
// NewSource constructs the adapter. Panics
330339
// on a nil repo so a wiring mistake surfaces at boot rather
331340
// than as a nil-deref on first AI request.
332-
func NewAICrossRuleConflictSource(rules *dbalert.AlertRuleRepo) *AICrossRuleConflictSource {
341+
func NewSource(rules *dbalert.AlertRuleRepo) *Source {
333342
if rules == nil {
334-
panic("api: NewAICrossRuleConflictSource: nil *dbalert.AlertRuleRepo")
343+
panic("aicrossrule: NewSource: nil *dbalert.AlertRuleRepo")
335344
}
336-
return &AICrossRuleConflictSource{rules: rules}
345+
return &Source{rules: rules}
337346
}
338347

339348
// LoadRules implements diagnostic.CrossRuleConflictSource. Reads the
@@ -348,7 +357,7 @@ func NewAICrossRuleConflictSource(rules *dbalert.AlertRuleRepo) *AICrossRuleConf
348357
//
349358
// The Limit field caps the returned slice so a runaway request
350359
// cannot blow past the canonical 500-row cap.
351-
func (a *AICrossRuleConflictSource) LoadRules(ctx context.Context, f diagnostic.CrossRuleConflictFilters) ([]*alertmodel.AlertRule, error) {
360+
func (a *Source) LoadRules(ctx context.Context, f diagnostic.CrossRuleConflictFilters) ([]*alertmodel.AlertRule, error) {
352361
limit := f.Limit
353362
if limit <= 0 || limit > aiCrossRuleConflictMaxLimit {
354363
limit = aiCrossRuleConflictDefaultLimit

internal/api/ai_cross_rule_conflict_handler_test.go renamed to internal/api/aicrossrule/handler_test.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
// (`go run ./cmd/ai-eval --feature cross-rule-conflict-detection`);
1616
// duplicating that here would require a live database fixture.
1717

18-
package api
18+
package aicrossrule
1919

2020
import (
21+
"context"
2122
"net/http"
2223
"net/http/httptest"
2324
"strings"
@@ -28,6 +29,22 @@ import (
2829
"github.com/ev-dev-labs/teslasync/internal/ai/guard"
2930
)
3031

32+
type stubGuardSettings struct {
33+
mode string
34+
on map[string]bool
35+
}
36+
37+
func (s *stubGuardSettings) AIMode(_ context.Context) (string, error) {
38+
if s.mode == "" {
39+
return "off", nil
40+
}
41+
return s.mode, nil
42+
}
43+
44+
func (s *stubGuardSettings) AIFeatureEnabled(_ context.Context, id string) (bool, error) {
45+
return s.on[id], nil
46+
}
47+
3148
// TestCrossRuleConflictAIOffHidesConflictPanel is the load-
3249
// bearing off-mode contract proof for slice 0036. It mounts
3350
// the AI cross-rule-conflict-detection route through the guard
@@ -157,13 +174,13 @@ func TestAICrossRuleConflictHandler_PanicsOnNilWiring(t *testing.T) {
157174
name string
158175
fn func()
159176
}{
160-
{"all nil", func() { NewAICrossRuleConflictHandler(nil, nil, nil, "") }},
177+
{"all nil", func() { NewHandler(nil, nil, nil, "") }},
161178
}
162179
for _, tc := range cases {
163180
t.Run(tc.name, func(t *testing.T) {
164181
defer func() {
165182
if r := recover(); r == nil {
166-
t.Fatalf("NewAICrossRuleConflictHandler(%s) did not panic", tc.name)
183+
t.Fatalf("NewHandler(%s) did not panic", tc.name)
167184
}
168185
}()
169186
tc.fn()
@@ -177,10 +194,10 @@ func TestAICrossRuleConflictSource_PanicsOnNilWiring(t *testing.T) {
177194
t.Parallel()
178195
defer func() {
179196
if r := recover(); r == nil {
180-
t.Fatal("NewAICrossRuleConflictSource(nil) did not panic")
197+
t.Fatal("NewSource(nil) did not panic")
181198
}
182199
}()
183-
NewAICrossRuleConflictSource(nil)
200+
NewSource(nil)
184201
}
185202

186203
// TestAICrossRuleConflictHandler_BodyParser_AcceptsEmpty

internal/api/router.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
aichatbot "github.com/ev-dev-labs/teslasync/internal/api/aichatbot"
2929
aiclimate "github.com/ev-dev-labs/teslasync/internal/api/aiclimate"
3030
aicostfcst "github.com/ev-dev-labs/teslasync/internal/api/aicostfcst"
31+
aicrossrule "github.com/ev-dev-labs/teslasync/internal/api/aicrossrule"
3132
aidigest "github.com/ev-dev-labs/teslasync/internal/api/aidigest"
3233
aidrivecoach "github.com/ev-dev-labs/teslasync/internal/api/aidrivecoach"
3334
aidrivesearch "github.com/ev-dev-labs/teslasync/internal/api/aidrivesearch"
@@ -1751,21 +1752,21 @@ func NewRouter(db *database.DB, teslaClient *tesla.Client, mqttClient *mqtt.Clie
17511752
// `query_alert_rules` + `detect_rule_conflicts` are
17521753
// registered on the process-wide tool registry so the
17531754
// dispatcher can resolve the strategy's allowedTools at
1754-
// boot. AICrossRuleConflictSource adapts the canonical
1755+
// boot. aicrossrule.Source adapts the canonical
17551756
// AlertRuleRepo so the LLM reads the SAME rows the manual
17561757
// AlertStudio path reads — no parallel write path; the
17571758
// LLM never persists. The pure-functional structural
17581759
// detector lives in internal/ai/tools/cross_rule_conflict.go
17591760
// (DetectRuleConflicts) and is exercised in unit tests
17601761
// without IO.
17611762
diagnostic.RegisterCrossRuleConflictDetectionTools(aiToolRegistry, diagnostic.CrossRuleConflictDetectionSources{
1762-
Source: NewAICrossRuleConflictSource(dbalert.NewAlertRuleRepo(db)),
1763+
Source: aicrossrule.NewSource(dbalert.NewAlertRuleRepo(db)),
17631764
})
17641765
// cross-rule-conflict-detection handler. One per process;
17651766
// stateless beyond constructor inputs. Must be constructed
17661767
// AFTER the tool registration above so the dispatcher can
17671768
// resolve the strategy's allowedTools at boot.
1768-
aiCrossRuleConflictHandler := NewAICrossRuleConflictHandler(
1769+
aiCrossRuleConflictHandler := aicrossrule.NewHandler(
17691770
aiRegistry,
17701771
aiToolRegistry,
17711772
crossruleconflictdetection.New(),

0 commit comments

Comments
 (0)