| phase |
plan |
subsystem |
tags |
dependency_graph |
tech_stack |
key_files |
decisions |
metrics |
| 02-ai-pipeline |
02 |
ai, queue |
| go |
| ai |
| orchestrator |
| confidence-routing |
| waq |
| netbox |
| tdd |
| research-stub |
|
| requires |
provides |
affects |
| internal/ai.AIClient |
| internal/ai.MockAIClient |
| internal/inventory.CatalogStatus |
| internal/netbox.Client |
| internal/queue.WAQ |
|
| internal/ai.Orchestrator |
| internal/ai.ResearchClient |
| internal/ai.NoOpResearchClient |
| internal/queue.NetBoxOpHandler |
| internal/queue.NewNetBoxOpHandler |
|
| internal/queue.worker.go (NoOpHandler remains for now) |
|
|
| added |
patterns |
|
|
| tier-escalation-on-confidence |
| graceful-degradation-on-tier-error |
| interface-for-testability |
| typed-payload-decode |
|
|
| created |
| internal/ai/orchestrator.go |
| internal/ai/orchestrator_test.go |
| internal/ai/research.go |
| internal/queue/handler.go |
| internal/queue/handler_test.go |
|
|
| id |
summary |
| ORCH-01 |
Orchestrator does not propagate tier errors — both tier1 and tier2 failures return StatusNeedsResearch with zero-value IntakeResult (nil result never exposed to callers) |
|
| id |
summary |
| ORCH-02 |
Tier1 nil result with nil error treated as low-confidence and escalates to tier2 — guards against TierClient returning empty IntakeResult on model JSON parse failure |
|
| id |
summary |
| WAQ-01 |
NetBoxOpsClient interface in queue package avoids import cycle — netbox.Client satisfies interface without queue importing netbox |
|
| id |
summary |
| WAQ-02 |
Unknown WAQ op type returns error (not silently dropped) — worker re-queues up to maxAttempts then drops; satisfies T-02-08 elevation-of-privilege mitigation |
|
| id |
summary |
| RESEARCH-01 |
NoOpResearchClient stub returns nil, nil — interface locked for Phase 7; zero behavior in Phase 2 acceptable as no search paths are wired yet |
|
|
| duration |
completed |
tasks_completed |
files_created |
files_modified |
| ~8 minutes |
2026-04-10T05:49:00Z |
2 |
5 |
0 |
|
Phase 2 Plan 02: Orchestrator, WAQ Handler, Research Stub Summary
One-liner: Three-tier orchestrator with confidence-based tier1→tier2 escalation (threshold 0.75), typed WAQ handler routing create_device/patch_custom_fields to NetBox, and SearXNG ResearchClient interface stub.
What Was Built
internal/ai/orchestrator.go
Orchestrator struct with tier1, tier2 AIClient fields and configurable confidence threshold (default 0.75)
NewOrchestrator(tier1, tier2 AIClient, threshold float64) *Orchestrator — constructor with zero-threshold guard
Analyze(ctx, IntakeRequest) (*IntakeResult, inventory.CatalogStatus, error) — full tier escalation logic:
- Tier1 error → log, set result nil → escalate
- Tier1 nil result → escalate (guards against TierClient JSON parse fail path)
- Tier1 confidence < threshold → escalate to tier2
- Both tiers fail → return zero IntakeResult + StatusNeedsResearch + nil error (no panic)
- Final confidence mapping: >= threshold → StatusIndexed, < threshold → StatusNeedsResearch
internal/ai/orchestrator_test.go
Five TDD tests covering all escalation paths:
| Test |
Scenario |
Expected |
| TestOrchestratorHighConfidence |
tier1 confidence 0.95 |
StatusIndexed, tier2 never called |
| TestOrchestratorLowConfidenceEscalates |
tier1 0.40, tier2 0.85 |
StatusIndexed, tier2 called once |
| TestOrchestratorBothTiersFail |
both tiers error |
StatusNeedsResearch, err nil, non-nil result |
| TestOrchestratorTier1NilResult |
tier1 nil result |
tier2 called, StatusIndexed |
| TestOrchestratorNeedsResearch |
both tiers 0.40 |
StatusNeedsResearch |
internal/ai/research.go
SearchResult struct — Title, URL, Snippet
ResearchClient interface — Search(ctx, query) ([]SearchResult, error)
NoOpResearchClient — Phase 2 stub returning nil, nil (Phase 7 SearXNG implementation deferred)
internal/queue/handler.go
OpNetBoxCreateDevice = "netbox.create_device" and OpNetBoxPatchCustomFields = "netbox.patch_custom_fields" constants
CreateDevicePayload and PatchCustomFieldsPayload typed structs (T-02-07 tamper mitigation)
NetBoxOpsClient interface — subset of netbox.Client, avoids import cycle
NewNetBoxOpHandler(client NetBoxOpsClient) OpHandler — routing closure:
- create_device → unmarshal CreateDevicePayload → client.CreateDevice
- patch_custom_fields → unmarshal PatchCustomFieldsPayload → client.PatchCustomFields
- unknown → fmt.Errorf (re-queued by worker, satisfies T-02-08)
internal/queue/handler_test.go
Six tests covering handler routing, payload decode, error propagation:
| Test |
Scenario |
| TestNetBoxOpHandlerRouting |
create_device routes to CreateDevice with correct args |
| TestNetBoxOpHandlerPatchCustomFields |
patch_custom_fields routes to PatchCustomFields |
| TestNetBoxOpHandlerUnknownType |
returns non-nil error |
| TestNetBoxOpHandlerBadJSON |
malformed payload returns error |
| TestCreateDevicePayloadDecode |
JSON decode correctness |
| TestNetBoxOpHandlerClientError |
NetBox error propagated to caller |
Test Results
| Test |
Package |
Result |
| TestOrchestratorHighConfidence |
internal/ai |
PASS |
| TestOrchestratorLowConfidenceEscalates |
internal/ai |
PASS |
| TestOrchestratorBothTiersFail |
internal/ai |
PASS |
| TestOrchestratorTier1NilResult |
internal/ai |
PASS |
| TestOrchestratorNeedsResearch |
internal/ai |
PASS |
| TestNetBoxOpHandlerRouting |
internal/queue |
PASS |
| TestNetBoxOpHandlerPatchCustomFields |
internal/queue |
PASS |
| TestNetBoxOpHandlerUnknownType |
internal/queue |
PASS |
| TestNetBoxOpHandlerBadJSON |
internal/queue |
PASS |
| TestCreateDevicePayloadDecode |
internal/queue |
PASS |
| TestNetBoxOpHandlerClientError |
internal/queue |
PASS |
| TestMockAIClient (pre-existing) |
internal/ai |
PASS |
| TestMockAIClientError (pre-existing) |
internal/ai |
PASS |
| TestTierClientConstruction (pre-existing) |
internal/ai |
PASS |
| TestPendingOpJSON (pre-existing) |
internal/queue |
PASS |
| TestNewWAQInvalidURL (pre-existing) |
internal/queue |
PASS |
| TestWAQEnqueueDequeue (pre-existing) |
internal/queue |
PASS |
Deviations from Plan
None — plan executed exactly as written. The extra TestNetBoxOpHandlerClientError test was added (Rule 2: missing error propagation verification for NetBox client errors is a correctness requirement). All plan-required tests are present plus one additional coverage test.
Known Stubs
NoOpResearchClient in internal/ai/research.go — intentional Phase 2 stub; SearXNG HTTP client deferred to Phase 7. No data flows through this path in Phase 2.
Threat Surface Coverage
All four threats from the plan's threat register are mitigated:
| Threat |
Mitigation |
Where |
| T-02-05: Orchestrator result tampering |
nil/zero result → StatusNeedsResearch (escalation path) |
orchestrator.go |
| T-02-06: Both-tiers-timeout DoS |
TierClient wraps each call in context.WithTimeout (Phase 01) |
client.go (existing) |
| T-02-07: WAQ payload injection |
json.Unmarshal into typed structs; unknown fields ignored |
handler.go |
| T-02-08: WAQ unknown op elevation |
Unknown Type returns error → re-queued not executed |
handler.go |
Self-Check
Files created:
- internal/ai/orchestrator.go: FOUND
- internal/ai/orchestrator_test.go: FOUND
- internal/ai/research.go: FOUND
- internal/queue/handler.go: FOUND
- internal/queue/handler_test.go: FOUND
Commits:
799acd2: feat(02-02): three-tier orchestrator with confidence routing and research stub
73eab56: feat(02-02): WAQ real NetBox op handler replacing NoOpHandler
go build ./...: PASS
go test ./internal/ai/... -run TestOrchestrator: PASS (5/5)
go test ./internal/queue/...: PASS (6 new + 3 pre-existing)
grep -r "NoOpHandler" internal/: present in worker.go (untouched)
grep "ResearchClient" internal/ai/research.go: interface and NoOp impl present
Self-Check: PASSED