- SUMMARY.md: 8 files created, 16 unit tests pass, 3 integration skipped - normalizeTags deviation documented (slug dedup fix) - T-04-01/T-04-02 mitigations confirmed in catalog_updater and tags
6.6 KiB
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation | 04 | netbox,inventory |
|
|
|
|
|
|
Phase 1 Plan 04: HW-ID, Quality Gate, Tag Sync, Catalog Updater Summary
One-liner: Sequential HW-XXXXX ID allocation with optimistic-lock retry, forward-only CatalogStatus state machine enforced via Transition(), AI tag slug-normalization and NetBox sync, and CatalogUpdater wiring quality gate to NetBox PATCH.
What Was Built
internal/netbox/hwid.go
formatHWID(n int) string— formats integer asHW-NNNNN(zero-padded 5 digits)parseHWID(s string) (int, error)— parsesHW-NNNNNwith strict regex^HW-(\d{5})$(c *Client) AllocateNextHWID(ctx) (string, error)— queries highest existing asset_tag, increments, checks candidate is unclaimed, retries up to 3 times on conflictgetHighestHWIDNumber— scansDcimDevicesList(Limit=1000)for highest HW-XXXXX numberhwIDExists— checksDcimDevicesList(AssetTag=[candidate])for conflict detection
internal/inventory/quality_gate.go
CatalogStatusstring type with 5 constants:StatusDraft,StatusIndexed,StatusNeedsResearch,StatusResearched,StatusCompletevalidTransitionsmap encodes the only permitted forward transitions(s CatalogStatus) CanTransitionTo(next)— O(n) lookup in allowed listTransition(current, next)— returns error with "invalid transition" message on rejectionParseCatalogStatus(s)— validates string against validTransitions keysAllStatuses()— returns statuses in lifecycle order
internal/inventory/types.go
HardwareRecord— domain struct composingnetbox.CustomFields,CatalogStatus, HWID, NetBoxID, Name, AITags
internal/inventory/catalog_updater.go
CatalogUpdater— wraps*netbox.ClientUpdateCatalogStatus(ctx, deviceID, current, next)— callsTransition()for validation thenPatchCustomFieldswith{"catalog_status": string(newStatus)}— T-04-01 mitigation enforced here
internal/netbox/tags.go
normalizeTags(tags)— lowercases, slug-converts (spaces→hyphens, non-slug chars stripped), deduplicates; "USB Cable"/"USB cable"/"usb-cable" all produce "usb-cable"tagNameToSlug(name)— lowercase + trim + space-to-hyphen + strip non-[a-z0-9-_]TagRef— holds ID, Name, Slug for a resolved NetBox tag(c *Client) SyncTags(ctx, tags)— normalizes then ensureTag for eachensureTag—ExtrasTagsList(Slug=slug)to check existence;ExtrasTagsCreate(TagRequest{Name,Slug})to create new
Test Results
| Test | File | Result |
|---|---|---|
| TestFormatHWID (3 cases) | hwid_test.go | PASS |
| TestParseHWID (7 cases) | hwid_test.go | PASS |
| TestCanTransitionTo (12 cases) | quality_gate_test.go | PASS |
| TestTransitionValid | quality_gate_test.go | PASS |
| TestTransitionInvalid | quality_gate_test.go | PASS |
| TestParseCatalogStatus (5+1 cases) | quality_gate_test.go | PASS |
| TestNormalizeTags | tags_test.go | PASS |
| TestTagNameToSlug (4 cases) | tags_test.go | PASS |
Integration tests (AllocateNextHWID live, SyncTags live): SKIPPED — placeholder token (correct behavior, will run once real HWLAB_NETBOX_TOKEN is set).
go build ./...: PASS
go test ./internal/...: 16 PASS, 3 SKIP (integration guards), 0 FAIL
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] normalizeTags needed slug conversion for dedup correctness
- Found during: Task 2 GREEN phase (TestNormalizeTags failure)
- Issue: Original
normalizeTagsonly lowercased/trimmed — "USB Cable" and "usb-cable" remained distinct (2 results, not 1) - Fix: Changed
normalizeTagsto delegate totagNameToSluginternally — ensures space-to-hyphen and non-slug stripping before dedup; test comment says these three should all produce "usb-cable" - Files modified: internal/netbox/tags.go
- Commit:
1f9621f
Known Stubs
None. ensureTag uses real go-netbox v4 NewTagRequest/ExtrasTagsCreate API. AllocateNextHWID uses real DcimDevicesList API. No stubs in any shipped code.
Threat Surface Scan
No new network endpoints introduced. All trust boundary mitigations from plan's threat model are implemented:
- T-04-01:
UpdateCatalogStatusenforcesTransition()before anyPatchCustomFieldscall — bypass impossible through this path - T-04-02:
normalizeTagsstrips injection surface before NetBox write — all AI tag strings pass through slug normalization
Self-Check
Files created:
- internal/netbox/hwid.go: FOUND
- internal/netbox/hwid_test.go: FOUND
- internal/inventory/quality_gate.go: FOUND
- internal/inventory/quality_gate_test.go: FOUND
- internal/inventory/types.go: FOUND
- internal/inventory/catalog_updater.go: FOUND
- internal/netbox/tags.go: FOUND
- internal/netbox/tags_test.go: FOUND
Commits:
e1cee31— Task 1 (HW-XXXXX sequential ID allocation)1f9621f— Task 2 (quality gate, tag sync, catalog updater)
go build ./...: PASS
go test ./internal/...: 16 PASS, 3 SKIP, 0 FAIL