homelabby/.planning/phases/01-foundation/01-04-SUMMARY.md
Mikkel Georgsen 1707496027 docs(01-04): complete HW-ID, quality gate, tag sync plan
- 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
2026-04-10 05:23:13 +00:00

6.6 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
01-foundation 04 netbox,inventory
go
hwid
quality-gate
tags
tdd
state-machine
requires provides affects
internal/netbox.Client
internal/netbox.PatchCustomFields
internal/netbox.AllocateNextHWID
internal/inventory.CatalogStatus
internal/inventory.Transition
internal/inventory.CatalogUpdater
internal/netbox.SyncTags
internal/intake
internal/advisor
added patterns
optimistic-lock-retry
forward-only-state-machine
slug-normalization
tdd-red-green
created modified
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
id summary
HWID-01 AllocateNextHWID scans all devices with Limit(1000) to find highest HW-XXXXX — acceptable for Phase 1 inventory size (T-04-03: accepted risk)
id summary
HWID-02 Optimistic-lock retry (3 attempts) handles concurrent allocation without transactions — sufficient for single-operator homelab
id summary
QG-01 validTransitions map encodes the only valid state transitions; no backward transitions permitted (T-04-01 mitigation)
id summary
TAG-01 normalizeTags uses tagNameToSlug internally — ensures 'USB Cable', 'USB cable', 'usb-cable' all deduplicate to same slug form
id summary
TAG-02 ensureTag checks by slug before creating — idempotent across multiple SyncTags calls for same tag names
duration completed tasks_completed files_created files_modified
~20 minutes 2026-04-10T06:00:00Z 2 8 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
  • ensureTagExtrasTagsList(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