feat(wizard-step5): #161 phase 1 — add dependency graph to componentGroups

Extend ComponentDef with `dependencies: string[]` so the corporate wizard
StepComponents grid can cascade-add and cascade-remove platform components
based on real-world platform-engineering knowledge. Mandatory tier still
locks components on; recommended/optional cards now know their pre-reqs.

Dependency-aware mappings (representative):
  Harbor              → cnpg, seaweedfs, valkey
  Keycloak / Gitea    → cnpg
  Velero / Iceberg    → seaweedfs
  Grafana / Loki /
  Mimir / Tempo       → seaweedfs
  External Secrets    → openbao
  cert-manager / k8gb → external-dns
  FerretDB / OpenMeter
  / Temporal / LangFuse
  / LibreChat / Matrix
  / Superset          → cnpg
  Milvus              → seaweedfs
  Debezium            → strimzi
  OpenSearch          → none (own storage)
  OpenBao / KEDA / VPA
  / Reloader / Cilium
  / Crossplane / Flux → none

Adds catalog helpers — findComponent, resolveTransitiveDependencies,
findDependents, resolveTransitiveDependents, isMandatory,
computeDefaultSelection — that the wizard store and StepComponents.tsx
will consume in phase 2 to power the cascade-aware selection model.

Per docs/INVIOLABLE-PRINCIPLES.md #4 the dependency table is the single
source of truth — no app-side knowledge of which component implies which.

Refs: GitHub issue #161

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hatiyildiz 2026-04-28 23:01:28 +02:00
parent 678f5dc604
commit 9d852dceab

View File

@ -5,6 +5,22 @@ export interface ComponentDef {
name: string
desc: string
tier: Tier
/**
* IDs of components this component requires at runtime. The wizard's
* dependency-aware selection cascades adds: choosing Harbor automatically
* pulls in cnpg + seaweedfs + valkey. Removal cascades the other way:
* removing cnpg also removes any component that lists 'cnpg' here.
*
* Mandatory components MUST list dependencies that have to come along
* even if those deps are themselves mandatory in another product group.
* The cascade resolver treats the list as a directed graph and walks
* transitively, so listing direct deps is sufficient.
*
* Per docs/INVIOLABLE-PRINCIPLES.md #4 ("never hardcode") the data here is
* the single source of truth StepComponents.tsx and the wizard store
* read this list, no app-side knowledge of which components imply which.
*/
dependencies?: string[]
}
export interface GroupDef {
@ -23,11 +39,11 @@ export const GROUPS: GroupDef[] = [
description: 'Continuous delivery engine with GitOps workflows, infrastructure as code, and virtual cluster isolation',
required: true,
components: [
{ id: 'flux', name: 'Flux CD', desc: 'GitOps delivery engine', tier: 'mandatory' },
{ id: 'crossplane', name: 'Crossplane', desc: 'Cloud CRDs / IaC', tier: 'mandatory' },
{ id: 'gitea', name: 'Gitea', desc: 'Internal Git server', tier: 'mandatory' },
{ id: 'opentofu', name: 'OpenTofu', desc: 'IaC (Terraform fork)', tier: 'mandatory' },
{ id: 'vcluster', name: 'vCluster', desc: 'Virtual cluster isolation layer', tier: 'mandatory' },
{ id: 'flux', name: 'Flux CD', desc: 'GitOps delivery engine', tier: 'mandatory', dependencies: [] },
{ id: 'crossplane', name: 'Crossplane', desc: 'Cloud CRDs / IaC', tier: 'mandatory', dependencies: [] },
{ id: 'gitea', name: 'Gitea', desc: 'Internal Git server', tier: 'mandatory', dependencies: ['cnpg'] },
{ id: 'opentofu', name: 'OpenTofu', desc: 'IaC (Terraform fork)', tier: 'mandatory', dependencies: [] },
{ id: 'vcluster', name: 'vCluster', desc: 'Virtual cluster isolation layer', tier: 'mandatory', dependencies: [] },
],
},
{
@ -35,14 +51,14 @@ export const GROUPS: GroupDef[] = [
description: 'CNI, service mesh, load balancing, WAF, and encrypted VPN connectivity',
required: true,
components: [
{ id: 'cilium', name: 'Cilium', desc: 'CNI & eBPF service mesh', tier: 'mandatory' },
{ id: 'coraza', name: 'Coraza WAF', desc: 'L7 web application firewall', tier: 'mandatory' },
{ id: 'external-dns', name: 'External DNS', desc: 'DNS record automation', tier: 'mandatory' },
{ id: 'envoy', name: 'Envoy', desc: 'L7 proxy', tier: 'mandatory' },
{ id: 'k8gb', name: 'k8gb', desc: 'Global server load balancing', tier: 'mandatory' },
{ id: 'frpc', name: 'frpc', desc: 'Reverse tunnel', tier: 'recommended' },
{ id: 'netbird', name: 'NetBird', desc: 'Mesh VPN', tier: 'mandatory' },
{ id: 'strongswan', name: 'strongSwan', desc: 'IPsec gateway', tier: 'optional' },
{ id: 'cilium', name: 'Cilium', desc: 'CNI & eBPF service mesh', tier: 'mandatory', dependencies: [] },
{ id: 'coraza', name: 'Coraza WAF', desc: 'L7 web application firewall', tier: 'mandatory', dependencies: [] },
{ id: 'external-dns', name: 'External DNS', desc: 'DNS record automation', tier: 'mandatory', dependencies: [] },
{ id: 'envoy', name: 'Envoy', desc: 'L7 proxy', tier: 'mandatory', dependencies: [] },
{ id: 'k8gb', name: 'k8gb', desc: 'Global server load balancing', tier: 'mandatory', dependencies: ['external-dns'] },
{ id: 'frpc', name: 'frpc', desc: 'Reverse tunnel', tier: 'recommended', dependencies: [] },
{ id: 'netbird', name: 'NetBird', desc: 'Mesh VPN', tier: 'mandatory', dependencies: [] },
{ id: 'strongswan', name: 'strongSwan', desc: 'IPsec gateway', tier: 'optional', dependencies: [] },
],
},
{
@ -50,10 +66,10 @@ export const GROUPS: GroupDef[] = [
description: 'Autoscaling, config-change reloading, and high-availability orchestration',
required: true,
components: [
{ id: 'vpa', name: 'VPA', desc: 'Vertical pod autoscaling', tier: 'mandatory' },
{ id: 'keda', name: 'KEDA', desc: 'Event-driven autoscaling', tier: 'mandatory' },
{ id: 'reloader', name: 'Reloader', desc: 'Config-change pod reload', tier: 'mandatory' },
{ id: 'continuum', name: 'Continuum', desc: 'HA orchestration', tier: 'recommended' },
{ id: 'vpa', name: 'VPA', desc: 'Vertical pod autoscaling', tier: 'mandatory', dependencies: [] },
{ id: 'keda', name: 'KEDA', desc: 'Event-driven autoscaling', tier: 'mandatory', dependencies: [] },
{ id: 'reloader', name: 'Reloader', desc: 'Config-change pod reload', tier: 'mandatory', dependencies: [] },
{ id: 'continuum', name: 'Continuum', desc: 'HA orchestration', tier: 'recommended', dependencies: [] },
],
},
{
@ -61,9 +77,9 @@ export const GROUPS: GroupDef[] = [
description: 'Multi-protocol distributed storage (S3 / NFS / FUSE / HDFS), backup & DR, and container registry',
required: true,
components: [
{ id: 'seaweedfs', name: 'SeaweedFS', desc: 'Multi-protocol distributed storage', tier: 'mandatory' },
{ id: 'velero', name: 'Velero', desc: 'Backup & disaster recovery', tier: 'mandatory' },
{ id: 'harbor', name: 'Harbor', desc: 'Container registry', tier: 'mandatory' },
{ id: 'seaweedfs', name: 'SeaweedFS', desc: 'Multi-protocol distributed storage', tier: 'mandatory', dependencies: [] },
{ id: 'velero', name: 'Velero', desc: 'Backup & disaster recovery', tier: 'mandatory', dependencies: ['seaweedfs'] },
{ id: 'harbor', name: 'Harbor', desc: 'Container registry', tier: 'mandatory', dependencies: ['cnpg', 'seaweedfs', 'valkey'] },
],
},
/* ── SIDE (cross-cutting, always present) ─────────────────────── */
@ -72,15 +88,15 @@ export const GROUPS: GroupDef[] = [
description: 'Policy enforcement, secrets vault, certificates, scanning, and identity management',
required: true,
components: [
{ id: 'falco', name: 'Falco', desc: 'Runtime threat detection', tier: 'recommended' },
{ id: 'kyverno', name: 'Kyverno', desc: 'Policy as code', tier: 'mandatory' },
{ id: 'trivy', name: 'Trivy', desc: 'Vulnerability scanning', tier: 'recommended' },
{ id: 'syft-grype', name: 'Syft + Grype', desc: 'SBOM & CVE analysis', tier: 'recommended' },
{ id: 'sigstore', name: 'Sigstore', desc: 'Supply chain trust', tier: 'recommended' },
{ id: 'keycloak', name: 'Keycloak', desc: 'Identity & access management', tier: 'recommended' },
{ id: 'openbao', name: 'OpenBao', desc: 'Secrets vault', tier: 'mandatory' },
{ id: 'external-secrets', name: 'External Secrets',desc: 'K8s secret sync (ESO)', tier: 'mandatory' },
{ id: 'cert-manager', name: 'Cert-Manager', desc: 'TLS certificate automation', tier: 'mandatory' },
{ id: 'falco', name: 'Falco', desc: 'Runtime threat detection', tier: 'recommended', dependencies: [] },
{ id: 'kyverno', name: 'Kyverno', desc: 'Policy as code', tier: 'mandatory', dependencies: [] },
{ id: 'trivy', name: 'Trivy', desc: 'Vulnerability scanning', tier: 'recommended', dependencies: [] },
{ id: 'syft-grype', name: 'Syft + Grype', desc: 'SBOM & CVE analysis', tier: 'recommended', dependencies: [] },
{ id: 'sigstore', name: 'Sigstore', desc: 'Supply chain trust', tier: 'recommended', dependencies: [] },
{ id: 'keycloak', name: 'Keycloak', desc: 'Identity & access management', tier: 'recommended', dependencies: ['cnpg'] },
{ id: 'openbao', name: 'OpenBao', desc: 'Secrets vault', tier: 'mandatory', dependencies: [] },
{ id: 'external-secrets', name: 'External Secrets',desc: 'K8s secret sync (ESO)', tier: 'mandatory', dependencies: ['openbao'] },
{ id: 'cert-manager', name: 'Cert-Manager', desc: 'TLS certificate automation', tier: 'mandatory', dependencies: ['external-dns'] },
],
},
{
@ -88,16 +104,16 @@ export const GROUPS: GroupDef[] = [
description: 'Unified metrics, logs, traces, dashboards, and AI-powered operations',
required: true,
components: [
{ id: 'grafana', name: 'Grafana', desc: 'Dashboards & alerting', tier: 'recommended' },
{ id: 'opentelemetry', name: 'OpenTelemetry', desc: 'Unified telemetry pipeline', tier: 'recommended' },
{ id: 'alloy', name: 'Alloy', desc: 'Telemetry agent', tier: 'recommended' },
{ id: 'loki', name: 'Loki', desc: 'Log aggregation', tier: 'recommended' },
{ id: 'mimir', name: 'Mimir', desc: 'Metrics store', tier: 'recommended' },
{ id: 'tempo', name: 'Tempo', desc: 'Distributed tracing', tier: 'recommended' },
{ id: 'opensearch', name: 'OpenSearch', desc: 'Search & analytics', tier: 'recommended' },
{ id: 'litmus', name: 'Litmus', desc: 'Chaos engineering', tier: 'optional' },
{ id: 'openmeter', name: 'OpenMeter', desc: 'Usage metering', tier: 'optional' },
{ id: 'specter', name: 'Specter', desc: 'AIOps brain', tier: 'optional' },
{ id: 'grafana', name: 'Grafana', desc: 'Dashboards & alerting', tier: 'recommended', dependencies: ['seaweedfs'] },
{ id: 'opentelemetry', name: 'OpenTelemetry', desc: 'Unified telemetry pipeline', tier: 'recommended', dependencies: [] },
{ id: 'alloy', name: 'Alloy', desc: 'Telemetry agent', tier: 'recommended', dependencies: [] },
{ id: 'loki', name: 'Loki', desc: 'Log aggregation', tier: 'recommended', dependencies: ['seaweedfs'] },
{ id: 'mimir', name: 'Mimir', desc: 'Metrics store', tier: 'recommended', dependencies: ['seaweedfs'] },
{ id: 'tempo', name: 'Tempo', desc: 'Distributed tracing', tier: 'recommended', dependencies: ['seaweedfs'] },
{ id: 'opensearch', name: 'OpenSearch', desc: 'Search & analytics', tier: 'recommended', dependencies: [] },
{ id: 'litmus', name: 'Litmus', desc: 'Chaos engineering', tier: 'optional', dependencies: [] },
{ id: 'openmeter', name: 'OpenMeter', desc: 'Usage metering', tier: 'optional', dependencies: ['cnpg'] },
{ id: 'specter', name: 'Specter', desc: 'AIOps brain', tier: 'optional', dependencies: [] },
],
},
/* ── À LA CARTE ───────────────────────────────────────────────── */
@ -106,16 +122,16 @@ export const GROUPS: GroupDef[] = [
description: 'Event streaming, CDC, workflow orchestration, and analytics databases',
required: false,
components: [
{ id: 'cnpg', name: 'CloudNative PG', desc: 'PostgreSQL operator', tier: 'recommended' },
{ id: 'valkey', name: 'Valkey', desc: 'Redis-compatible cache', tier: 'recommended' },
{ id: 'strimzi', name: 'Strimzi', desc: 'Apache Kafka operator', tier: 'recommended' },
{ id: 'debezium', name: 'Debezium', desc: 'Change data capture', tier: 'recommended' },
{ id: 'flink', name: 'Apache Flink', desc: 'Stream processing', tier: 'optional' },
{ id: 'temporal', name: 'Temporal', desc: 'Workflow orchestration', tier: 'optional' },
{ id: 'clickhouse', name: 'ClickHouse', desc: 'Analytics database', tier: 'optional' },
{ id: 'ferretdb', name: 'FerretDB', desc: 'MongoDB-compatible DB', tier: 'optional' },
{ id: 'iceberg', name: 'Iceberg', desc: 'Data lakehouse format', tier: 'optional' },
{ id: 'superset', name: 'Superset', desc: 'BI & dashboards', tier: 'optional' },
{ id: 'cnpg', name: 'CloudNative PG', desc: 'PostgreSQL operator', tier: 'recommended', dependencies: [] },
{ id: 'valkey', name: 'Valkey', desc: 'Redis-compatible cache', tier: 'recommended', dependencies: [] },
{ id: 'strimzi', name: 'Strimzi', desc: 'Apache Kafka operator', tier: 'recommended', dependencies: [] },
{ id: 'debezium', name: 'Debezium', desc: 'Change data capture', tier: 'recommended', dependencies: ['strimzi'] },
{ id: 'flink', name: 'Apache Flink', desc: 'Stream processing', tier: 'optional', dependencies: [] },
{ id: 'temporal', name: 'Temporal', desc: 'Workflow orchestration', tier: 'optional', dependencies: ['cnpg'] },
{ id: 'clickhouse', name: 'ClickHouse', desc: 'Analytics database', tier: 'optional', dependencies: [] },
{ id: 'ferretdb', name: 'FerretDB', desc: 'MongoDB-compatible DB', tier: 'optional', dependencies: ['cnpg'] },
{ id: 'iceberg', name: 'Iceberg', desc: 'Data lakehouse format', tier: 'optional', dependencies: ['seaweedfs'] },
{ id: 'superset', name: 'Superset', desc: 'BI & dashboards', tier: 'optional', dependencies: ['cnpg'] },
],
},
{
@ -123,15 +139,15 @@ export const GROUPS: GroupDef[] = [
description: 'Model serving, LLM inference, vector search, embeddings, and AI observability',
required: false,
components: [
{ id: 'kserve', name: 'KServe', desc: 'Model serving platform', tier: 'mandatory' },
{ id: 'knative', name: 'Knative', desc: 'Serverless runtime', tier: 'optional' },
{ id: 'axon', name: 'Axon', desc: 'LLM gateway (SaaS)', tier: 'recommended' },
{ id: 'neo4j', name: 'Neo4j', desc: 'Graph database', tier: 'optional' },
{ id: 'vllm', name: 'vLLM', desc: 'LLM inference engine', tier: 'optional' },
{ id: 'milvus', name: 'Milvus', desc: 'Vector database', tier: 'optional' },
{ id: 'bge', name: 'BGE', desc: 'Embedding model server', tier: 'optional' },
{ id: 'langfuse', name: 'LangFuse', desc: 'LLM observability & tracing', tier: 'optional' },
{ id: 'librechat', name: 'LibreChat', desc: 'AI chat interface', tier: 'optional' },
{ id: 'kserve', name: 'KServe', desc: 'Model serving platform', tier: 'mandatory', dependencies: [] },
{ id: 'knative', name: 'Knative', desc: 'Serverless runtime', tier: 'optional', dependencies: [] },
{ id: 'axon', name: 'Axon', desc: 'LLM gateway (SaaS)', tier: 'recommended', dependencies: [] },
{ id: 'neo4j', name: 'Neo4j', desc: 'Graph database', tier: 'optional', dependencies: [] },
{ id: 'vllm', name: 'vLLM', desc: 'LLM inference engine', tier: 'optional', dependencies: [] },
{ id: 'milvus', name: 'Milvus', desc: 'Vector database', tier: 'optional', dependencies: ['seaweedfs'] },
{ id: 'bge', name: 'BGE', desc: 'Embedding model server', tier: 'optional', dependencies: [] },
{ id: 'langfuse', name: 'LangFuse', desc: 'LLM observability & tracing', tier: 'optional', dependencies: ['cnpg'] },
{ id: 'librechat', name: 'LibreChat', desc: 'AI chat interface', tier: 'optional', dependencies: ['cnpg'] },
],
},
{
@ -139,11 +155,113 @@ export const GROUPS: GroupDef[] = [
description: 'Self-hosted email, WebRTC video conferencing, federated messaging, and push notifications',
required: false,
components: [
{ id: 'stalwart', name: 'Stalwart', desc: 'SMTP/IMAP/JMAP mail server', tier: 'recommended' },
{ id: 'livekit', name: 'LiveKit', desc: 'WebRTC video & audio', tier: 'recommended' },
{ id: 'stunner', name: 'STUNner', desc: 'Kubernetes TURN/STUN gateway', tier: 'recommended' },
{ id: 'matrix', name: 'Matrix', desc: 'Federated messaging', tier: 'optional' },
{ id: 'ntfy', name: 'Ntfy', desc: 'Push notifications', tier: 'optional' },
{ id: 'stalwart', name: 'Stalwart', desc: 'SMTP/IMAP/JMAP mail server', tier: 'recommended', dependencies: [] },
{ id: 'livekit', name: 'LiveKit', desc: 'WebRTC video & audio', tier: 'recommended', dependencies: [] },
{ id: 'stunner', name: 'STUNner', desc: 'Kubernetes TURN/STUN gateway', tier: 'recommended', dependencies: [] },
{ id: 'matrix', name: 'Matrix', desc: 'Federated messaging', tier: 'optional', dependencies: ['cnpg'] },
{ id: 'ntfy', name: 'Ntfy', desc: 'Push notifications', tier: 'optional', dependencies: [] },
],
},
]
/**
* Flat catalog index every component across every group, with the group
* stamped in for breadcrumb display. The wizard's dependency resolver and
* the marketplace card grid both read this; the GROUPS shape is preserved
* for StepReview's grouped summary view.
*/
export interface ComponentEntry extends ComponentDef {
groupId: string
groupName: string
groupSubtitle: string
}
export const ALL_COMPONENTS: ComponentEntry[] = GROUPS.flatMap(g =>
g.components.map(c => ({
...c,
dependencies: c.dependencies ?? [],
groupId: g.id,
groupName: g.productName,
groupSubtitle: g.subtitle,
})),
)
/** Lookup a component by id, or undefined when the id is not in the catalog. */
export function findComponent(id: string): ComponentEntry | undefined {
return ALL_COMPONENTS.find(c => c.id === id)
}
/** True when the component is `tier: mandatory` somewhere in the catalog. */
export function isMandatory(id: string): boolean {
return findComponent(id)?.tier === 'mandatory'
}
/**
* Resolve all transitive dependencies for the given component id. Returns
* the set of dep ids reachable from `id` via `dependencies` the id itself
* is NOT included. Cycles are tolerated (visited set).
*/
export function resolveTransitiveDependencies(id: string): string[] {
const out = new Set<string>()
const stack: string[] = [...(findComponent(id)?.dependencies ?? [])]
while (stack.length > 0) {
const next = stack.pop()!
if (out.has(next)) continue
out.add(next)
const more = findComponent(next)?.dependencies ?? []
for (const d of more) {
if (!out.has(d)) stack.push(d)
}
}
return [...out]
}
/**
* Reverse lookup every component id whose `dependencies` list contains
* the given id (i.e. the components that would break if `id` were removed).
* The cascade-remove flow uses this to compute the impact set.
*/
export function findDependents(id: string): string[] {
return ALL_COMPONENTS.filter(c => (c.dependencies ?? []).includes(id)).map(c => c.id)
}
/**
* Recursively compute every component that (directly or transitively)
* depends on `id`. Used for cascade-remove confirmation messaging.
*/
export function resolveTransitiveDependents(id: string): string[] {
const out = new Set<string>()
const stack: string[] = [...findDependents(id)]
while (stack.length > 0) {
const next = stack.pop()!
if (out.has(next)) continue
out.add(next)
for (const d of findDependents(next)) {
if (!out.has(d)) stack.push(d)
}
}
return [...out]
}
/** All ids of components with `tier: mandatory`. */
export const MANDATORY_COMPONENT_IDS: string[] = ALL_COMPONENTS
.filter(c => c.tier === 'mandatory')
.map(c => c.id)
/**
* Default selection: every mandatory component, every recommended
* component, plus the transitive deps each of them implies. Optional
* components are off by default (the user opts in).
*/
export function computeDefaultSelection(): string[] {
const out = new Set<string>()
for (const c of ALL_COMPONENTS) {
if (c.tier === 'mandatory' || c.tier === 'recommended') {
out.add(c.id)
for (const dep of resolveTransitiveDependencies(c.id)) {
out.add(dep)
}
}
}
return [...out]
}