Use this as the single source to capture intent. Keep answers concise (bullets ok). We’ll branch feature folders later. If a box doesn’t apply, write “N/A”.
- Product: Hedgehog NetCommand (HNC) — a wireframe‑first, state‑driven app that designs a single ONF fabric and maintains its CR “wiring diagram” via GitOps.
- For whom: Kubernetes/platform engineers and network teams who need to co‑design and manage ONF fabrics without deep cross‑domain skills.
- v0.1 lets a user enter a minimal topology spec (spine/leaf models, one endpoint profile, oversubscription), computes a valid Clos layout, generates a minimal wiring‑diagram CR set into a local FGD stub, and edits it in a GUI with a deterministic preview. Model‑based tests + one golden E2E lock the core logic.
List 2–3 user archetypes and their top 3 tasks.
| Archetype | Goals (ranked) | Biggest pain today |
|---|---|---|
| Platform Engineer (K8s/GitOps) | 1) Create a new fabric spec 2) Generate wiring‑diagram CRs 3) Edit + save via GUI | Doesn’t grok switch/port math; afraid of violating topology rules |
| Network Engineer (Switch‑focused) | 1) Import an existing wiring diagram (later) 2) Validate port/uplink layout 3) See drift & fix via Git (later) | Not fluent in Git/K8s/CRDs; overwhelmed by GitOps |
| Hedgehog/Partner SE | 1) Quick quote + initial config 2) Share minimal artifacts | Time pressure; wants guardrailed defaults |
- Users who want to bypass GitOps entirely or manage non‑ONF L3/campus networks.
- Users needing multi‑fabric orchestration and DCI in v0.1 (explicitly out of scope).
Name the 1–2 domain objects we must support in v0.1. Keep properties minimal.
Objects
Fabric→id,name,status: 'draft'|'computed'|'saved',fgdRef?(stub only in v0.1)TopologySpec→spineModelId,leafModelId,uplinksPerLeaf: number,endpointProfiles: EndpointProfile[]EndpointProfile→name,count,nicCount,nicSpeedGbpsDerivedTopology(computed) →leavesNeeded,spines: 2(fixed in v0.1),uplinksPerLeaf,totalEndpointNICs,issues: string[]WiringDiagram(generated CR set) →servers[],switches[],connections[](minimal stubs in v0.1)
Fabric1→1TopologySpecTopologySpec→1DerivedTopology(compute)Fabric1→1WiringDiagram(generate fromDerivedTopology)
fabric.namespineModelId(select from spine‑capable catalog)leafModelId(select from leaf‑capable catalog)uplinksPerLeaf(integer input; step 2)- read‑only O/S ratio display
endpointProfiles[]rows:{ name, count, nicCount, nicSpeedGbps }
Narrate the single most valuable end‑to‑end path.
- User opens app in state
ready(config, clean)with a static switch catalog loaded. - User selects
spineModelId,leafModelId, setsoversubscription, and adds oneendpointProfile(e.g., "compute‑nodes", 100 servers × 1×100G NIC). - System computes a Clos outline (
DerivedTopology) and drafts a minimalWiringDiagram(servers/switches/connections as stubs). - User reviews counts and validity in Preview (even uplink distribution; no capacity conflicts).
- User clicks Save → spec + derived diagram persist to the local FGD stub.
- Done: state returns to
clean; model‑based tests and the golden E2E pass deterministically.
- Account/signup, multi‑fabric management, import flows, multiple leaf classes, MC‑LAG/ES‑LAG, DCI/gateway, BOM export, Git integration, K8s/FKS connectivity/drift. (All planned post‑v0.1.)
We’ll refine into a strict FSM. Start with 5–7 states max.
uninitialized,loadingCatalog,ready(parallel:view: config|preview,data: clean|dirty),computing,saving,error
Events (user/system) with payloads:
LOAD_OK(catalog)/LOAD_ERR(message)EDIT(field, value)(includesuplinksPerLeaf)NAV(view: 'config'|'preview')COMPUTE/COMPUTE_OK(derived, wiring)/COMPUTE_ERR(message)SAVE/SAVE_OK()/SAVE_ERR(message)RESET()
isValidSpec(spec)(models chosen, endpoint count > 0,uplinksPerLeafwithin bounds)evenDistributionPossible(derived)(uplinksPerLeaf % spines == 0)capacityAvailable(derived)(leaf/spine port capacity sufficient)
fetchCatalog()deriveTopology(spec)(usesuplinksPerLeaf)generateWiringDiagram(spec, derived)persistToFGDStub(files)
uninitialized,loadingCatalog,ready(parallel:view: config|preview,data: clean|dirty),computing,saving,error
Events (user/system) with payloads:
LOAD_OK(catalog)/LOAD_ERR(message)EDIT(field, value)NAV(view: 'config'|'preview')COMPUTE/COMPUTE_OK(derived, wiring)/COMPUTE_ERR(message)SAVE/SAVE_OK()/SAVE_ERR(message)RESET()
isValidSpec(spec)(models chosen, endpoint count > 0, enum ranges ok)evenDistributionPossible(derived)(uplinks multiple of spine count)capacityAvailable(derived)(leaf endpoint capacity ≥ endpoints per leaf)
fetchCatalog()deriveTopology(spec)generateWiringDiagram(spec, derived)persistToFGDStub(files)
Describe panels and what changes when.
Screen: Config → Preview
- Left (Config form):
fabric.name,spineModelId(select),leafModelId(select),oversubscription(select),endpointProfiles[](small table: name, count, nicCount, nicSpeedGbps; add/remove rows) - Right (Live Preview): derived counts (leaves, spines=2, uplinks per leaf, total NICs), feasibility badges, and a "generated files" list (server/switch/connection stubs)
- Footer: [Reset] [Save] [Next]
Visibility/Enablement rules
- Disable Save until
isValidSpecand last compute succeeded. - Show read‑only O/S ratio computed from
uplinksPerLeafand endpoint profile speeds. - If
oversubscription == '1:1'hide non‑MVP advanced tuning. - Show
warningBannerifevenDistributionPossible == falseorcapacityAvailable == false.
Formulas/computations driving the preview or validation. (Simplified for v0.1; we’ll replace with real switch_profile.go ingestion later.)
-
totalEndpointNICs = Σ(profile.count × profile.nicCount) -
Catalog provides:
leaf.endpointPortsPerSwitch,leaf.maxUplinkPortsPerSwitch,spine.uplinkPortCapacity -
- Given catalog:
leaf.endpointPortsPerSwitch,leaf.maxUplinkPortsPerSwitch,leaf.uplinkPortSpeedGbps,leaf.endpointPortSpeedGbps,spine.uplinkPortCapacity,spine.uplinkPortSpeedGbps
- Given catalog:
-
leavesNeeded = ceil(totalEndpointNICs / leaf.endpointPortsPerSwitch) -
Input:
uplinksPerLeaf(user-entered). Must satisfy:uplinksPerLeaf > 0,uplinksPerLeaf ≤ leaf.maxUplinkPortsPerSwitch, anduplinksPerLeaf % spines == 0. -
Computed (read‑only):
endpointsPerLeaf = ceil(totalEndpointNICs / leavesNeeded)downlinkGbpsPerLeaf = endpointsPerLeaf × endpointNicSpeedGbpsuplinkGbpsPerLeaf = uplinksPerLeaf × leaf.uplinkPortSpeedGbpsosRatio = downlinkGbpsPerLeaf / uplinkGbpsPerLeaf(display only)
-
Capacity checks:
leavesNeeded × uplinksPerLeaf ≤ spines × spine.uplinkPortCapacityuplinksPerLeaf % spines == 0
-
leavesNeeded = ceil(totalEndpointNICs / leaf.endpointPortsPerSwitch) -
spines = 2(fixed in v0.1 to keep distribution logic trivial) -
Validity checks:
uplinksPerLeaf % spines == 0leavesNeeded ≥ 1leavesNeeded × uplinksPerLeaf ≤ spines × spine.uplinkPortCapacity
-
Generate minimal CR stubs: one
Switchper spine/leaf count; oneServerper endpoint;Connectionrecords for leaf↔spine uplinks as evenly distributed pairs.
- Load errors: catalog missing/invalid → show blocking banner; allow
Retry→loadingCatalog. - Compute errors: invalid spec or capacity violation → inline errors on fields; stay in
ready(data: dirty). - Save errors (stub persistence): toast with reason; revert to last clean snapshot.
- Auth/multi‑tenant/multi‑fabric
- Real Git integration or K8s/FKS connectivity/drift
- Import from real wiring‑diagram YAML
- Multiple leaf classes; MC‑LAG/ES‑LAG; DCI/gateway
- BOM/export pricing; metrics/telemetry
- No hidden state in components; all transitions via events
- No more than 7 states in the v0.1 machine
- No E2E beyond the one golden path
- No network IO in v0.1 (stub service only)
These become model‑based tests + a single Playwright spec.
Happy path
- Given the app initialises to
ready(config, clean)with a static catalog - When the user selects valid
spineModelId,leafModelId, entersuplinksPerLeaf = 4, and adds one endpoint profile (e.g., name "compute",count=100,nicCount=1,nicSpeedGbps=100) - And the user clicks Compute (or edits trigger compute)
- Then the Preview shows:
leavesNeeded ≥ 1,spines = 2,uplinksPerLeaf % spines == 0, computed O/S ratio, andissues.length == 0 - When the user clicks Save
- Then state returns to
cleanand the FGD stub contains generatedservers.yaml,switches.yaml,connections.yaml
Edge cases (max 3)
- Invalid spec (endpoint count 0 or missing models) → inline errors; Save disabled
- Insufficient spine capacity or uneven distribution → red badge + cannot Save
- Save throws (stub) → toast + rollback to last clean state
- Fabric — A single ONF‑managed Ethernet fabric (one site), spine‑leaf in v0.1
- FGD (Fabric GitOps Directory) — Repo path holding CR YAMLs for one fabric
- FKS (Fabric Kubernetes Server) — The ONF controller’s K8s; read‑only in future
- Wiring Diagram — The CR set describing devices & links (servers/switches/connections)
- Endpoint/Server — Any non‑switch device attached to a leaf switch
- Leaf Class — Group of leafs with identical uplink profiles (v0.1: single class)
- Oversubscription — Ratio of downlink to uplink capacity (v0.1: '1:1' or '2:1')
- Switch Catalog — Static JSON describing supported models (v0.1 stub; later parse
switch_profile.go) - Drift — Difference between FGD CRs and active CRs on FKS (post‑v0.1)
- v0.2: Local FS FGD (real YAML read/write), import existing wiring diagram, basic drift planning (no K8s), Storybook visual diffs
- v0.3: Multiple leaf classes, MC‑LAG/ES‑LAG selection UI (guardrailed), real
switch_profile.goingestion, Git repo integration (read/write) behind a feature flag
Fill for each state you listed in §4.
| From state | Event | Guard | Effect | To state |
|---|---|---|---|---|
| uninitialized | fetchCatalog() |
loadingCatalog | ||
| loadingCatalog | LOAD_OK | ready (config, clean) | ||
| loadingCatalog | LOAD_ERR | error | ||
| ready (config, dirty) | COMPUTE | isValidSpec |
deriveTopology(), generateWiringDiagram() |
computing |
| computing | COMPUTE_OK | evenDistributionPossible && capacityAvailable |
ready (preview, dirty) | |
| computing | COMPUTE_ERR | ready (config, dirty) | ||
| ready (preview, dirty) | SAVE | persistToFGDStub() |
saving | |
| saving | SAVE_OK | ready (preview, clean) | ||
| saving | SAVE_ERR | ready (config, dirty) | ||
| any | RESET | revert to last clean snapshot | ready (config, clean) |
---------- | ----- | ----- | ------ | -------- | | | | | | | | | | | | |
Keep alphabetical; events are the only way to change state.
COMPUTECOMPUTE_ERR(message)COMPUTE_OK(derived, wiring)EDIT(field, value)LOAD_ERR(message)LOAD_OK(catalog)NAV(view)RESET()SAVESAVE_ERR(message)SAVE_OK()
We’ll formalize as JSON Schema / Zod in code.
{
"$id": "FabricSpec",
"$schemaVersion": 1,
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"spineModelId": {"type": "string"},
"leafModelId": {"type": "string"},
"uplinksPerLeaf": {"type": "integer", "minimum": 2},
"endpointProfiles": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"count": {"type": "integer", "minimum": 1},
"nicCount": {"type": "integer", "minimum": 1},
"nicSpeedGbps": {"type": "integer", "minimum": 1}
},
"required": ["name","count","nicCount","nicSpeedGbps"]
},
"minItems": 1
}
},
"required": ["name","spineModelId","leafModelId","uplinksPerLeaf","endpointProfiles"]
}{
"models": [
{
"id": "celestica-ds2000",
"roles": ["leaf"],
"endpointAssignable": ["E1/1-48"],
"fabricAssignable": ["E1/49-56"],
"endpointPortProfile": "SFP28-25G",
"uplinkPortProfile": "QSFP28-100G",
"endpointPortSpeedGbps": 25,
"uplinkPortSpeedGbps": 100,
"endpointPortsPerSwitch": 48,
"maxUplinkPortsPerSwitch": 8
},
{
"id": "celestica-ds3000",
"roles": ["spine"],
"fabricAssignable": ["E1/1-32"],
"uplinkPortProfile": "QSFP28-100G",
"uplinkPortSpeedGbps": 100,
"uplinkPortCapacity": 32
}
]
}Notes
- Ranges are parsed via a tiny DSL (e.g.,
E1/1-48,E1/49-56). Overlap allowed in catalog, but assignment is exclusive at compute time. - Breakout optics & mixed speeds are post‑v0.1.
- Per model, HNC overlays user‑editable defaults to mark which port ranges are assignable to endpoints vs fabric. Overlap is allowed to reflect hardware flexibility; the allocator resolves conflicts.
- In v0.1 these defaults come from the stub catalog; later we'll ingest ONF
switch_profile.goand synthesize the HNC profile.
New features in this release:
- Manual Configuration Overrides: Allow experienced users to override automatic calculations
- Safety Guardrails: Comprehensive rule engine prevents impossible configurations
- Field Provenance Tracking: Track whether values are auto-calculated, manually set, or imported
- Issues Panel: Real-time feedback with errors (blocking), warnings, and informational messages
- UI Affordances: Clear indicators for overridden fields with easy reset options
See docs/expert-overrides-guide.md for complete documentation.
- Optional Integration Tests: GitHub and K8s integration tests run only when secrets are available
- Non-Blocking Design: Core CI remains fast and hermetic, integration tests are informational
- Conditional Execution: Tests activate automatically when proper credentials exist
- Safe Testing: All integration tests use isolated resources with automatic cleanup
See docs/integration-testing-guide.md for setup and usage.
- Phase 1 ✅: Basic GitHub and K8s integration (v0.4.1)
- Phase 2: Advanced GitOps workflows and drift detection
- Phase 3: Multi-cluster testing and federation scenarios
- Phase 4: Performance and scalability testing at enterprise scale
See docs/integrations-roadmap.md for the complete roadmap.
- Single-screen Config → Preview workflow
- React + TypeScript + XState state machine
- Automatic fabric topology calculation
- Storybook component documentation and testing
- E2E test coverage with Playwright
- Switch catalog integration with Celestica DS2000/DS3000 models
HNC includes integration scripts for GitHub and Kubernetes operations:
npm run int:gh- GitHub repository operations, PR management, and CI monitoringnpm run int:gh:ci- GitHub integration tests with verbose reporting (CI only)npm run k8s:validate- Kubernetes deployment validation and drift detectionnpm run int:k8s:ci- Kubernetes integration tests with test fabric setup (CI only)
These scripts support both local development and CI environments with graceful degradation when tools or credentials are unavailable. See docs/integration-testing-guide.md for detailed setup and usage instructions.