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
This commit is contained in:
parent
1f9621fcaa
commit
1707496027
1 changed files with 136 additions and 0 deletions
136
.planning/phases/01-foundation/01-04-SUMMARY.md
Normal file
136
.planning/phases/01-foundation/01-04-SUMMARY.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
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
|
||||
Loading…
Add table
Reference in a new issue