- 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
136 lines
6.6 KiB
Markdown
136 lines
6.6 KiB
Markdown
---
|
|
phase: 01-foundation
|
|
plan: "04"
|
|
subsystem: netbox,inventory
|
|
tags: [go, hwid, quality-gate, tags, tdd, state-machine]
|
|
dependency_graph:
|
|
requires: [internal/netbox.Client, internal/netbox.PatchCustomFields]
|
|
provides: [internal/netbox.AllocateNextHWID, internal/inventory.CatalogStatus, internal/inventory.Transition, internal/inventory.CatalogUpdater, internal/netbox.SyncTags]
|
|
affects: [internal/intake, internal/advisor]
|
|
tech_stack:
|
|
added: []
|
|
patterns: [optimistic-lock-retry, forward-only-state-machine, slug-normalization, tdd-red-green]
|
|
key_files:
|
|
created:
|
|
- internal/netbox/hwid.go
|
|
- internal/netbox/hwid_test.go
|
|
- internal/inventory/quality_gate.go
|
|
- internal/inventory/quality_gate_test.go
|
|
- internal/inventory/types.go
|
|
- internal/inventory/catalog_updater.go
|
|
- internal/netbox/tags.go
|
|
- internal/netbox/tags_test.go
|
|
modified: []
|
|
decisions:
|
|
- id: HWID-01
|
|
summary: "AllocateNextHWID scans all devices with Limit(1000) to find highest HW-XXXXX — acceptable for Phase 1 inventory size (T-04-03: accepted risk)"
|
|
- id: HWID-02
|
|
summary: "Optimistic-lock retry (3 attempts) handles concurrent allocation without transactions — sufficient for single-operator homelab"
|
|
- id: QG-01
|
|
summary: "validTransitions map encodes the only valid state transitions; no backward transitions permitted (T-04-01 mitigation)"
|
|
- id: TAG-01
|
|
summary: "normalizeTags uses tagNameToSlug internally — ensures 'USB Cable', 'USB cable', 'usb-cable' all deduplicate to same slug form"
|
|
- id: TAG-02
|
|
summary: "ensureTag checks by slug before creating — idempotent across multiple SyncTags calls for same tag names"
|
|
metrics:
|
|
duration: "~20 minutes"
|
|
completed: "2026-04-10T06:00:00Z"
|
|
tasks_completed: 2
|
|
files_created: 8
|
|
files_modified: 0
|
|
---
|
|
|
|
# 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 as `HW-NNNNN` (zero-padded 5 digits)
|
|
- `parseHWID(s string) (int, error)` — parses `HW-NNNNN` with 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 conflict
|
|
- `getHighestHWIDNumber` — scans `DcimDevicesList(Limit=1000)` for highest HW-XXXXX number
|
|
- `hwIDExists` — checks `DcimDevicesList(AssetTag=[candidate])` for conflict detection
|
|
|
|
### `internal/inventory/quality_gate.go`
|
|
- `CatalogStatus` string type with 5 constants: `StatusDraft`, `StatusIndexed`, `StatusNeedsResearch`, `StatusResearched`, `StatusComplete`
|
|
- `validTransitions` map encodes the only permitted forward transitions
|
|
- `(s CatalogStatus) CanTransitionTo(next)` — O(n) lookup in allowed list
|
|
- `Transition(current, next)` — returns error with "invalid transition" message on rejection
|
|
- `ParseCatalogStatus(s)` — validates string against validTransitions keys
|
|
- `AllStatuses()` — returns statuses in lifecycle order
|
|
|
|
### `internal/inventory/types.go`
|
|
- `HardwareRecord` — domain struct composing `netbox.CustomFields`, `CatalogStatus`, HWID, NetBoxID, Name, AITags
|
|
|
|
### `internal/inventory/catalog_updater.go`
|
|
- `CatalogUpdater` — wraps `*netbox.Client`
|
|
- `UpdateCatalogStatus(ctx, deviceID, current, next)` — calls `Transition()` for validation then `PatchCustomFields` with `{"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 each
|
|
- `ensureTag` — `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 `normalizeTags` only lowercased/trimmed — "USB Cable" and "usb-cable" remained distinct (2 results, not 1)
|
|
- **Fix:** Changed `normalizeTags` to delegate to `tagNameToSlug` internally — 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: `UpdateCatalogStatus` enforces `Transition()` before any `PatchCustomFields` call — bypass impossible through this path
|
|
- T-04-02: `normalizeTags` strips 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
|
|
|
|
## Self-Check: PASSED
|