-
🪟 Provider 详细信息
+
Provider 详细信息
-
+
+
+
diff --git a/dixinternal/api.go b/dixinternal/api.go
index 8590951..60ddfea 100644
--- a/dixinternal/api.go
+++ b/dixinternal/api.go
@@ -164,20 +164,24 @@ func (dix *Dix) GetObjects() map[reflect.Type]map[string][]reflect.Value {
// ProviderDetails contains detailed information about a provider
type ProviderDetails struct {
- OutputType string
- OutputPkg string
- FunctionName string
- FunctionPkg string
- FunctionFile string
- FunctionLine int
- InputTypes []string
- InputPkgs []string
+ OutputType string `json:"output_type"`
+ OutputPkg string `json:"output_pkg"`
+ FunctionName string `json:"function_name"`
+ FunctionPkg string `json:"function_pkg"`
+ FunctionFile string `json:"function_file"`
+ FunctionLine int `json:"function_line"`
+ InputTypes []string `json:"input_types"`
+ InputPkgs []string `json:"input_pkgs"`
+ RegistrationID uint64 `json:"registration_id"`
+ ProviderID string `json:"provider_id"`
}
// ProviderRuntimeStats contains provider runtime metrics for diagnostics.
type ProviderRuntimeStats struct {
FunctionName string `json:"function_name"`
OutputType string `json:"output_type"`
+ RegistrationID uint64 `json:"registration_id"`
+ ProviderID string `json:"provider_id"`
CallCount int `json:"call_count"`
TotalDuration time.Duration `json:"total_duration"`
AverageDuration time.Duration `json:"average_duration"`
@@ -199,6 +203,7 @@ type RecentError struct {
Message string `json:"message"`
RootCause string `json:"root_cause,omitempty"`
Hint string `json:"hint,omitempty"`
+ TraceID string `json:"trace_id,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
Timeout time.Duration `json:"timeout,omitempty"`
@@ -215,37 +220,44 @@ func (dix *Dix) GetProviderDetails() []ProviderDetails {
var inputTypes []string
var inputPkgs []string
seen := make(map[string]bool)
+ appendInput := func(typ reflect.Type) {
+ if typ == nil {
+ return
+ }
+ name := typ.String()
+ if name == "" {
+ return
+ }
+ pkg := resolveTypePkgPath(typ)
+ // Distinct packages can share type.String() (e.g. */handler.Handler).
+ key := pkg + "\x00" + name
+ if seen[key] {
+ return
+ }
+ seen[key] = true
+ inputTypes = append(inputTypes, name)
+ inputPkgs = append(inputPkgs, pkg)
+ }
for _, input := range providerFn.inputList {
if input.isStruct || input.typ.Kind() == reflect.Struct {
for _, in := range getProvideAllInputs(input.typ) {
- name := in.typ.String()
- if name == "" || seen[name] {
- continue
- }
- seen[name] = true
- inputTypes = append(inputTypes, name)
- inputPkgs = append(inputPkgs, resolveTypePkgPath(in.typ))
+ appendInput(in.typ)
}
continue
}
-
- name := input.typ.String()
- if name == "" || seen[name] {
- continue
- }
- seen[name] = true
- inputTypes = append(inputTypes, name)
- inputPkgs = append(inputPkgs, resolveTypePkgPath(input.typ))
+ appendInput(input.typ)
}
details = append(details, ProviderDetails{
- OutputType: outputType.String(),
- OutputPkg: resolveTypePkgPath(outputType),
- FunctionName: fnName,
- FunctionPkg: resolveFuncPkgPath(fnName),
- FunctionFile: fnFile,
- FunctionLine: fnLine,
- InputTypes: inputTypes,
- InputPkgs: inputPkgs,
+ OutputType: outputType.String(),
+ OutputPkg: resolveTypePkgPath(outputType),
+ FunctionName: fnName,
+ FunctionPkg: resolveFuncPkgPath(fnName),
+ FunctionFile: fnFile,
+ FunctionLine: fnLine,
+ InputTypes: inputTypes,
+ InputPkgs: inputPkgs,
+ RegistrationID: providerFn.registrationID,
+ ProviderID: fmt.Sprintf("provider_%d_%s", providerFn.registrationID, outputType.String()),
})
}
}
@@ -256,23 +268,29 @@ func (dix *Dix) GetProviderDetails() []ProviderDetails {
// This is helpful for startup latency diagnosis.
func (dix *Dix) GetProviderRuntimeStats() []ProviderRuntimeStats {
stats := make([]ProviderRuntimeStats, 0, len(dix.providers))
- seen := make(map[reflect.Value]bool)
+ seen := make(map[string]bool)
for _, providerList := range dix.providers {
for _, p := range providerList {
- if p == nil || seen[p.fn] {
+ if p == nil {
continue
}
- seen[p.fn] = true
outputType := ""
if p.output != nil && p.output.typ != nil {
outputType = p.output.typ.String()
}
+ identity := fmt.Sprintf("%d:%s", p.registrationID, outputType)
+ if seen[identity] {
+ continue
+ }
+ seen[identity] = true
item := ProviderRuntimeStats{
- FunctionName: GetFnName(p.fn),
- OutputType: outputType,
+ FunctionName: GetFnName(p.fn),
+ OutputType: outputType,
+ RegistrationID: p.registrationID,
+ ProviderID: fmt.Sprintf("provider_%d_%s", p.registrationID, outputType),
}
if s, ok := dix.providerStats[p.fn]; ok && s != nil {
@@ -333,6 +351,7 @@ func (dix *Dix) GetRecentErrors(limit int) []RecentError {
Message: r.Message,
RootCause: r.RootCause,
Hint: r.Hint,
+ TraceID: r.TraceID,
TimedOut: r.TimedOut,
Duration: r.Duration,
Timeout: r.Timeout,
diff --git a/dixinternal/dix.go b/dixinternal/dix.go
index 40495f9..86816ba 100644
--- a/dixinternal/dix.go
+++ b/dixinternal/dix.go
@@ -70,6 +70,9 @@ type Dix struct {
initializer map[reflect.Value]bool
timedOut map[reflect.Value]bool
graph *Graph
+ // registrationSeq identifies one top-level Provide call. Struct field
+ // projections reuse it; distinct calls to the same closure source do not.
+ registrationSeq uint64
// containerID 标识本容器(随机 16 hex),trace 事件携带它实现多容器隔离。
containerID string
@@ -105,6 +108,7 @@ type recentErrorRecord struct {
Message string
RootCause string
Hint string
+ TraceID string
TimedOut bool
Duration time.Duration
Timeout time.Duration
@@ -1117,7 +1121,7 @@ func (dix *Dix) inject(ctx context.Context, param any, opts ...Option) (err erro
}
// handleProvide registers a provider function for a specific output type
-func (dix *Dix) handleProvide(fnVal reflect.Value, outType reflect.Type, inputs []*providerInputType) error {
+func (dix *Dix) handleProvide(fnVal reflect.Value, outType reflect.Type, inputs []*providerInputType, registrationID uint64) error {
traceFnName := GetFnTraceName(fnVal)
dix.emitDIEvent("provide.register.start",
"provider", traceFnName,
@@ -1139,7 +1143,7 @@ func (dix *Dix) handleProvide(fnVal reflect.Value, outType reflect.Type, inputs
}
}
- provider := &providerFn{fn: fnVal, inputList: inputs, hasError: hasError}
+ provider := &providerFn{fn: fnVal, inputList: inputs, hasError: hasError, registrationID: registrationID}
// Register based on output kind
switch outType.Kind() {
@@ -1195,7 +1199,7 @@ func (dix *Dix) handleProvide(fnVal reflect.Value, outType reflect.Type, inputs
dix.emitDIEvent("provide.register.struct_field.start", "provider", traceFnName, "declared_output_type", outType.String(), "field", field.Name, "field_type", field.Type.String())
// Recursive call
- if err := dix.handleProvide(fnVal, field.Type, inputs); err != nil {
+ if err := dix.handleProvide(fnVal, field.Type, inputs, registrationID); err != nil {
dix.emitDIEvent("provide.register.struct_field.failed", "provider", traceFnName, "declared_output_type", outType.String(), "field", field.Name, "field_type", field.Type.String(), "error", err)
return err
}
@@ -1360,7 +1364,8 @@ func (dix *Dix) provide(param any) {
inputs = append(inputs, parsedInputs...)
}
- if err := dix.handleProvide(fnVal, typ.Out(0), inputs); err != nil {
+ dix.registrationSeq++
+ if err := dix.handleProvide(fnVal, typ.Out(0), inputs, dix.registrationSeq); err != nil {
dix.emitDIEvent("provide.register.failed", "provider", traceFnName, "declared_output_type", typ.Out(0).String(), "error", err)
panic(err)
}
diff --git a/dixinternal/graph_query.go b/dixinternal/graph_query.go
index 963cdca..4f67267 100644
--- a/dixinternal/graph_query.go
+++ b/dixinternal/graph_query.go
@@ -18,6 +18,7 @@ type SearchHit struct {
Label string `json:"label"`
Pkg string `json:"pkg,omitempty"`
Group string `json:"group,omitempty"`
+ External bool `json:"external,omitempty"`
State string `json:"state,omitempty"` // instantiated|error|slow
Provider string `json:"provider,omitempty"`
}
@@ -49,6 +50,7 @@ func (dix *Dix) SearchNodes(q, kind, module, state string, limit int) []SearchHi
g := dix.graph
g.mu.RLock()
+
defer g.mu.RUnlock()
// 已实例化类型集合:存在 Object 节点 (type, group) 即视为实例化
@@ -195,10 +197,191 @@ func (dix *Dix) ModuleGraph() []ModuleInfo {
return out
}
+// ModuleDependency is an aggregated cross-module relationship.
+type ModuleDependency struct {
+ Name string `json:"name"`
+ EdgeCount int `json:"edge_count"`
+}
+
+// ModuleDetailView is a bounded topology projection for one module.
+type ModuleDetailView struct {
+ Name string `json:"name"`
+ TypeCount int `json:"type_count"`
+ ProviderCount int `json:"provider_count"`
+ ObjectCount int `json:"object_count"`
+ DependsOn []ModuleDependency `json:"depends_on,omitempty"`
+ DependedOnBy []ModuleDependency `json:"depended_on_by,omitempty"`
+ Nodes []SearchHit `json:"nodes"`
+ Edges []GraphEdge `json:"edges"`
+ Truncated bool `json:"truncated"`
+}
+
+// ModuleView returns providers, produced outputs, declared dependencies, and
+// external dependency types for one module. Results are deterministically
+// bounded so a large module cannot force an unrenderable response.
+func (dix *Dix) ModuleView(name string, nodeLimit, edgeLimit int) ModuleDetailView {
+ if nodeLimit <= 0 {
+ nodeLimit = 100
+ }
+ if nodeLimit > 500 {
+ nodeLimit = 500
+ }
+ if edgeLimit <= 0 {
+ edgeLimit = 300
+ }
+ if edgeLimit > 1000 {
+ edgeLimit = 1000
+ }
+
+ view := ModuleDetailView{Name: name, Nodes: []SearchHit{}, Edges: []GraphEdge{}}
+ g := dix.graph
+ g.mu.RLock()
+ defer g.mu.RUnlock()
+
+ instantiated := make(map[reflect.Type]bool)
+ for key := range g.nIndex {
+ if key.kind == NodeObject {
+ instantiated[key.typ] = true
+ }
+ }
+
+ type edgeRef struct {
+ edge *Edge
+ from, to Node
+ }
+ var edges []edgeRef
+ degree := make(map[NodeID]int)
+ dependsOn := make(map[string]int)
+ dependedOnBy := make(map[string]int)
+
+ for _, node := range g.nodes {
+ if node.Pkg != name {
+ continue
+ }
+ switch node.Kind {
+ case NodeType:
+ view.TypeCount++
+ case NodeProvider:
+ view.ProviderCount++
+ case NodeObject:
+ view.ObjectCount++
+ }
+ }
+
+ for _, edge := range g.eIndex {
+ if edge.Kind != EdgeProduced && edge.Kind != EdgeDeclared {
+ continue
+ }
+ from, to := g.nodes[edge.From], g.nodes[edge.To]
+ if from.Pkg != name && to.Pkg != name {
+ continue
+ }
+ edges = append(edges, edgeRef{edge: edge, from: from, to: to})
+ degree[edge.From]++
+ degree[edge.To]++
+
+ if edge.Kind != EdgeDeclared {
+ continue
+ }
+ if from.Pkg == name && to.Pkg != name {
+ dependsOn[to.Pkg]++
+ } else if to.Pkg == name && from.Pkg != name {
+ dependedOnBy[from.Pkg]++
+ }
+ }
+
+ sort.Slice(edges, func(i, j int) bool {
+ left, right := edges[i], edges[j]
+ if left.from.Label != right.from.Label {
+ return left.from.Label < right.from.Label
+ }
+ if left.to.Label != right.to.Label {
+ return left.to.Label < right.to.Label
+ }
+ return left.edge.Kind < right.edge.Kind
+ })
+ if len(edges) > edgeLimit {
+ edges = edges[:edgeLimit]
+ view.Truncated = true
+ }
+
+ incident := make(map[NodeID]Node, nodeLimit)
+ for _, item := range edges {
+ incident[item.from.ID] = item.from
+ incident[item.to.ID] = item.to
+ }
+ nodes := make([]Node, 0, len(incident))
+ for _, node := range incident {
+ nodes = append(nodes, node)
+ }
+ kindRank := map[NodeKind]int{NodeProvider: 0, NodeType: 1, NodeObject: 2}
+ sort.Slice(nodes, func(i, j int) bool {
+ left, right := nodes[i], nodes[j]
+ if kindRank[left.Kind] != kindRank[right.Kind] {
+ return kindRank[left.Kind] < kindRank[right.Kind]
+ }
+ if degree[left.ID] != degree[right.ID] {
+ return degree[left.ID] > degree[right.ID]
+ }
+ return left.Label < right.Label
+ })
+ if len(nodes) > nodeLimit {
+ nodes = nodes[:nodeLimit]
+ view.Truncated = true
+ }
+ keep := make(map[NodeID]bool, len(nodes))
+ for _, node := range nodes {
+ keep[node.ID] = true
+ }
+
+ for _, node := range nodes {
+ hit := SearchHit{
+ ID: uint32(node.ID),
+ Kind: nodeKindName(node.Kind),
+ Label: node.Label,
+ Pkg: node.Pkg,
+ Group: node.Group,
+ External: node.Pkg != name,
+ }
+ if node.Provider != nil {
+ hit.Provider = GetFnName(node.Provider.fn)
+ hit.State = dix.providerState(node.Provider)
+ }
+ if node.Kind == NodeType && instantiated[node.Type] {
+ hit.State = "instantiated"
+ }
+ view.Nodes = append(view.Nodes, hit)
+ }
+
+ for _, item := range edges {
+ if !keep[item.from.ID] || !keep[item.to.ID] {
+ continue
+ }
+ view.Edges = append(view.Edges, GraphEdge{
+ From: item.from.Label,
+ To: item.to.Label,
+ FromKind: nodeKindName(item.from.Kind),
+ ToKind: nodeKindName(item.to.Kind),
+ })
+ }
+
+ appendDeps := func(target *[]ModuleDependency, source map[string]int) {
+ for depName, count := range source {
+ *target = append(*target, ModuleDependency{Name: depName, EdgeCount: count})
+ }
+ sort.Slice(*target, func(i, j int) bool { return (*target)[i].Name < (*target)[j].Name })
+ }
+ appendDeps(&view.DependsOn, dependsOn)
+ appendDeps(&view.DependedOnBy, dependedOnBy)
+ return view
+}
+
// GraphEdge 是邻域子图里的一条声明依赖边(类型 label 表示)。
type GraphEdge struct {
- From string `json:"from"`
- To string `json:"to"`
+ From string `json:"from"`
+ To string `json:"to"`
+ FromKind string `json:"from_kind,omitempty"`
+ ToKind string `json:"to_kind,omitempty"`
}
// GraphView 是邻域子图:节点摘要 + 声明边。
@@ -225,6 +408,15 @@ func (dix *Dix) EgoGraph(center string, depth int, direction string) GraphView {
g := dix.graph
g.mu.RLock()
+ // Ego visibility is not instantiation. Build the object-bearing set while
+ // the graph lock is held and project it after traversal.
+ instantiated := make(map[reflect.Type]bool)
+ for key := range g.nIndex {
+ if key.kind == NodeObject {
+ instantiated[key.typ] = true
+ }
+ }
+
type dEdge struct {
from, to reflect.Type
}
@@ -284,7 +476,11 @@ func (dix *Dix) EgoGraph(center string, depth int, direction string) GraphView {
view.Edges = append(view.Edges, GraphEdge{From: label(e.from), To: label(e.to)})
}
for t := range seen {
- view.Nodes = append(view.Nodes, SearchHit{Kind: "type", Label: label(t), Pkg: resolveTypePkgPath(t), State: "instantiated"})
+ state := ""
+ if instantiated[t] {
+ state = "instantiated"
+ }
+ view.Nodes = append(view.Nodes, SearchHit{Kind: "type", Label: label(t), Pkg: resolveTypePkgPath(t), State: state})
}
sort.Slice(view.Nodes, func(i, j int) bool { return view.Nodes[i].Label < view.Nodes[j].Label })
sort.Slice(view.Edges, func(i, j int) bool {
@@ -310,12 +506,16 @@ func (dix *Dix) ResolvedTopN(n int) []ResolvedCount {
g := dix.graph
g.mu.RLock()
defer g.mu.RUnlock()
- counts := make([]ResolvedCount, 0, 8)
+ countsByType := make(map[string]int64)
for _, e := range g.eIndex {
if e.Kind == EdgeResolved && e.Count > 0 {
- counts = append(counts, ResolvedCount{Type: g.nodes[e.To].Type.String(), Count: e.Count})
+ countsByType[g.nodes[e.To].Type.String()] += e.Count
}
}
+ counts := make([]ResolvedCount, 0, len(countsByType))
+ for typ, count := range countsByType {
+ counts = append(counts, ResolvedCount{Type: typ, Count: count})
+ }
sort.Slice(counts, func(i, j int) bool {
if counts[i].Count != counts[j].Count {
return counts[i].Count > counts[j].Count
diff --git a/dixinternal/graph_query_projection_test.go b/dixinternal/graph_query_projection_test.go
new file mode 100644
index 0000000..b373db7
--- /dev/null
+++ b/dixinternal/graph_query_projection_test.go
@@ -0,0 +1,59 @@
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type packageTarget struct{}
+
+func packageProvider() *packageTarget { return &packageTarget{} }
+
+func TestEgoInstantiatedUsesObjectNodes(t *testing.T) {
+ di := New()
+ di.graph.node(NodeType, reflect.TypeOf(&packageTarget{}), "", nil)
+ view := di.EgoGraph("*dixinternal.packageTarget", 1, "both")
+ for _, node := range view.Nodes {
+ if node.Label == "*dixinternal.packageTarget" && node.State == "instantiated" {
+ t.Fatal("missing provider must not be reported instantiated")
+ }
+ }
+
+ di.Provide(func() *packageTarget { return &packageTarget{} })
+ di.graph.addObject(reflect.TypeOf(&packageTarget{}), "")
+ view = di.EgoGraph("*dixinternal.packageTarget", 1, "both")
+ instantiated := false
+ for _, node := range view.Nodes {
+ if node.Label == "*dixinternal.packageTarget" && node.State == "instantiated" {
+ instantiated = true
+ }
+ }
+ if !instantiated {
+ t.Fatal("object-bearing ego node should be instantiated")
+ }
+}
+
+func TestResolvedTopNAggregatesByType(t *testing.T) {
+ di := New()
+ targetType := reflect.TypeOf(&packageTarget{})
+
+ for i := 0; i < 2; i++ {
+ provider := &providerFn{fn: reflect.ValueOf(packageProvider), registrationID: uint64(i + 1)}
+ node := di.graph.providerNode(provider, targetType)
+ di.graph.markResolved(node, targetType)
+ }
+
+ rows := di.ResolvedTopN(10)
+ found := 0
+ for _, row := range rows {
+ if row.Type == "*dixinternal.packageTarget" {
+ found++
+ if row.Count != 2 {
+ t.Fatalf("count = %d, want 2", row.Count)
+ }
+ }
+ }
+ if found != 1 {
+ t.Fatalf("type rows = %d, want 1", found)
+ }
+}
diff --git a/dixinternal/module_view_test.go b/dixinternal/module_view_test.go
new file mode 100644
index 0000000..7d28dc7
--- /dev/null
+++ b/dixinternal/module_view_test.go
@@ -0,0 +1,72 @@
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type moduleViewTarget struct{}
+
+func newModuleViewTarget(*testing.T) *moduleViewTarget {
+ return &moduleViewTarget{}
+}
+
+func TestModuleViewIncludesProducedAndDeclaredTopology(t *testing.T) {
+ di := New()
+ di.Provide(newModuleViewTarget)
+ module := resolveTypePkgPath(reflect.TypeOf(&moduleViewTarget{}))
+
+ view := di.ModuleView(module, 100, 100)
+ if view.Name != module {
+ t.Fatalf("name = %q", view.Name)
+ }
+ if view.TypeCount < 2 {
+ t.Fatalf("summary = %+v", view)
+ }
+ providerFound, targetFound, externalFound := false, false, false
+ for _, node := range view.Nodes {
+ switch node.Label {
+ case "*dixinternal.moduleViewTarget":
+ if node.Kind == "provider" {
+ providerFound = true
+ } else {
+ targetFound = true
+ }
+ case "*testing.T":
+ externalFound = true
+ if !node.External {
+ t.Fatal("external dependency node was not marked")
+ }
+ }
+ }
+ if !providerFound || !targetFound || !externalFound {
+ t.Fatalf("expected provider, target, and external nodes: %+v", view.Nodes)
+ }
+ if len(view.Edges) < 1 {
+ t.Fatalf("expected declared topology, got %+v", view.Edges)
+ }
+ if len(view.DependsOn) == 0 || view.DependsOn[0].Name != "testing" {
+ t.Fatalf("depends on = %+v", view.DependsOn)
+ }
+}
+
+func TestModuleViewBoundsNodesAndEdges(t *testing.T) {
+ di := New()
+ targetType := reflect.TypeOf(&moduleViewTarget{})
+ for i := 0; i < 12; i++ {
+ provider := &providerFn{fn: reflect.ValueOf(newModuleViewTarget), registrationID: uint64(i + 1)}
+ node := di.graph.providerNode(provider, targetType)
+ di.graph.addProduced(node, targetType)
+ }
+
+ view := di.ModuleView("github.com/pubgo/dix/v2/dixinternal", 2, 1)
+ if len(view.Nodes) != 2 {
+ t.Fatalf("nodes = %d, want 2", len(view.Nodes))
+ }
+ if len(view.Edges) > 1 {
+ t.Fatalf("edges = %d, want <= 1", len(view.Edges))
+ }
+ if !view.Truncated {
+ t.Fatal("expected truncated marker")
+ }
+}
diff --git a/dixinternal/provider.go b/dixinternal/provider.go
index 09e698f..e91de91 100644
--- a/dixinternal/provider.go
+++ b/dixinternal/provider.go
@@ -37,10 +37,11 @@ type providerOutputType struct {
}
type providerFn struct {
- fn reflect.Value
- inputList []*providerInputType
- output *providerOutputType
- hasError bool
+ fn reflect.Value
+ inputList []*providerInputType
+ output *providerOutputType
+ hasError bool
+ registrationID uint64
}
type providerCallResult struct {
diff --git a/dixinternal/provider_identity_test.go b/dixinternal/provider_identity_test.go
new file mode 100644
index 0000000..4115605
--- /dev/null
+++ b/dixinternal/provider_identity_test.go
@@ -0,0 +1,50 @@
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type identityA struct{}
+type identityB struct{}
+type identityAggregate struct {
+ A *identityA
+ B *identityB
+}
+
+func TestStructProviderOutputsShareRegistrationID(t *testing.T) {
+ di := New()
+ di.Provide(func() identityAggregate {
+ return identityAggregate{A: &identityA{}, B: &identityB{}}
+ })
+
+ ids := make(map[string]uint64)
+ for _, provider := range di.providers[reflect.TypeOf(&identityA{})] {
+ ids["A"] = provider.registrationID
+ }
+ for _, provider := range di.providers[reflect.TypeOf(&identityB{})] {
+ ids["B"] = provider.registrationID
+ }
+ if ids["A"] == 0 || ids["A"] != ids["B"] {
+ t.Fatalf("struct outputs should share one registration ID: %#v", ids)
+ }
+}
+
+func TestDistinctClosureRegistrationsHaveDistinctRegistrationIDs(t *testing.T) {
+ di := New()
+ for _, name := range []string{"first", "second"} {
+ value := name
+ di.Provide(func() *identityA { return &identityA{} })
+ if value == "" {
+ t.Fatal("closure registration setup unexpectedly empty")
+ }
+ }
+
+ providers := di.providers[reflect.TypeOf(&identityA{})]
+ if len(providers) != 2 {
+ t.Fatalf("expected 2 providers, got %d", len(providers))
+ }
+ if providers[0].registrationID == providers[1].registrationID {
+ t.Fatal("distinct registrations must not share a registration ID")
+ }
+}
diff --git a/dixinternal/runtime_stats_identity_test.go b/dixinternal/runtime_stats_identity_test.go
new file mode 100644
index 0000000..da54aa7
--- /dev/null
+++ b/dixinternal/runtime_stats_identity_test.go
@@ -0,0 +1,49 @@
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type runtimeTarget struct{ Name string }
+
+func TestRuntimeStatsIncludeConcreteProviderIdentity(t *testing.T) {
+ di := New()
+ di.Provide(func() *runtimeTarget { return &runtimeTarget{Name: "ready"} })
+ _ = di.TryInject(func(*runtimeTarget) {})
+
+ stats := di.GetProviderRuntimeStats()
+ for _, stat := range stats {
+ if stat.OutputType != "*dixinternal.runtimeTarget" {
+ continue
+ }
+ if stat.RegistrationID == 0 {
+ t.Fatal("expected non-zero registration ID")
+ }
+ if stat.ProviderID == "" {
+ t.Fatal("expected provider ID")
+ }
+ if stat.CallCount != 1 {
+ t.Fatalf("call count = %d, want 1", stat.CallCount)
+ }
+ return
+ }
+ t.Fatalf("target stat not found in %+v", stats)
+}
+
+func TestRuntimeStatsDoNotDeduplicateDistinctClosures(t *testing.T) {
+ di := New()
+ for i := 0; i < 2; i++ {
+ di.Provide(func() *runtimeTarget { return &runtimeTarget{} })
+ }
+ stats := di.GetProviderRuntimeStats()
+ count := 0
+ for _, stat := range stats {
+ if stat.OutputType == reflect.TypeOf(&runtimeTarget{}).String() {
+ count++
+ }
+ }
+ if count != 2 {
+ t.Fatalf("provider stats = %d, want 2", count)
+ }
+}
diff --git a/docs/superpowers/plans/2026-09-05-dependency-visualization-unified.md b/docs/superpowers/plans/2026-09-05-dependency-visualization-unified.md
new file mode 100644
index 0000000..448cc50
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-05-dependency-visualization-unified.md
@@ -0,0 +1,634 @@
+# Dependency Visualization Unified Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Finish the unified `/next` dependency workflow so large containers stay readable (bounded module-first graphs) and failures are diagnosable (Issue → bounded graph → Trace) without breaking legacy APIs or the five-view shell.
+
+**Architecture:** Evolve existing dixhttp endpoints and `/next` views in place. Keep `/api/modules`, `/api/module`, `/api/ego`, `/api/issues`, `/api/trace`, and `/api/trace-tree` as the primary path. Default graph modes never eagerly load `/api/dependencies`. Cancel stale view requests with AbortController + a sequence load guard. Correlate diagnostics through `provider_id`, with function name only as fallback.
+
+**Tech Stack:** Go 1.24+, net/http/httptest, vendored vanilla JS, vis-network, hash routing, Node `node:test` for pure JS helpers.
+
+**Spec:** `docs/superpowers/specs/2026-09-05-dependency-visualization-unified-design.md`
+
+## Global Constraints
+
+- Do not introduce third-party Go or JavaScript dependencies.
+- Preserve all existing public Dix APIs.
+- Preserve legacy `/` UI and `/api/dependencies` compatibility; additive fields only.
+- Keep `/next` five views; do not rebuild a three-pane shell.
+- Graph budgets: module map 100/300, module drill-down 150/400, ego 100/300, global advanced 150/400.
+- Issue severity values stay `error` and `slow` (existing API); UI may style `slow` as warn.
+- Every commit must leave `go test -race ./...` green in the root module.
+- JS pure helpers must pass `node --test dixhttp/static/js/graph_view.test.mjs`.
+
+## File Map
+
+| File | Responsibility |
+|---|---|
+| `dixhttp/static/js/api.js` | `DIX.get(path, params, options)` with optional `AbortSignal` |
+| `dixhttp/static/js/graph_state.mjs` | Mode resolution, budgets, load guard, issue hashes, hub ranking, trace filter helpers |
+| `dixhttp/static/js/graph_view.test.mjs` | Pure JS contract tests |
+| `dixhttp/static/js/views/graph.js` | Bounded graph modes, cancellation, density warning + hubs, drawer → Trace |
+| `dixhttp/static/js/views/trace.js` | Hash prefilter + optional auto-open tree |
+| `dixhttp/static/js/views/overview.js` | Issues feed (already wired; verify after hash/API changes) |
+| `dixhttp/server.go` | `IssueInfo` + `buildIssues` (+ optional `trace_id`) |
+| `dixinternal/dix.go` / `dixinternal/api.go` | Optional TraceID on recent errors |
+| `dixhttp/README.md` / `dixhttp/README_zh.md` | Document default workflow |
+
+---
+
+### Task 1: Cancel Stale Graph Requests (Correctly)
+
+**Files:**
+- Modify: `dixhttp/static/js/api.js`
+- Modify: `dixhttp/static/js/graph_state.mjs`
+- Modify: `dixhttp/static/js/views/graph.js`
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Consumes: existing `DIX.get`, `window.DIXGraphState`
+- Produces: `DIX.get(path, params, options = {})` with `options.signal`; `createLoadGuard()` → `{ begin(): { seq:number }, isCurrent(seq:number): boolean }`
+
+**Bug to avoid:** the previous attempt passed `{ seq }` objects into `isCurrent`. Always pass numeric `seq`.
+
+- [ ] **Step 1: Write the failing load-guard test**
+
+Append to `dixhttp/static/js/graph_view.test.mjs`:
+
+```js
+test("stale graph loads are rejected by sequence", async () => {
+ const { createLoadGuard } = await import("./graph_state.mjs");
+ const guard = createLoadGuard();
+ const first = guard.begin();
+ const second = guard.begin();
+ assert.equal(guard.isCurrent(first.seq), false);
+ assert.equal(guard.isCurrent(second.seq), true);
+});
+```
+
+- [ ] **Step 2: Run the focused JS test and verify RED**
+
+Run:
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+Expected: FAIL because `createLoadGuard` is not exported.
+
+- [ ] **Step 3: Implement API signal support and load guard**
+
+In `dixhttp/static/js/api.js`, change `DIX.get` to:
+
+```js
+DIX.get = async function (path, params, options = {}) {
+ let url = base + path;
+ if (params) {
+ const qs = new URLSearchParams();
+ for (const [k, v] of Object.entries(params)) {
+ if (v !== undefined && v !== null && v !== "") qs.set(k, v);
+ }
+ const q = qs.toString();
+ if (q) url += "?" + q;
+ }
+ const resp = await fetch(url, { signal: options.signal });
+ if (!resp.ok) {
+ const text = await resp.text();
+ throw new Error(resp.status + " " + text.slice(0, 200));
+ }
+ return resp.json();
+};
+```
+
+In `dixhttp/static/js/graph_state.mjs`, add and export:
+
+```js
+export function createLoadGuard() {
+ let current = 0;
+ return {
+ begin() {
+ current += 1;
+ return { seq: current };
+ },
+ isCurrent(seq) {
+ return seq === current;
+ },
+ };
+}
+```
+
+Expose on `window.DIXGraphState` together with existing helpers.
+
+- [ ] **Step 4: Wire graph.js cancellation**
+
+In `dixhttp/static/js/views/graph.js` state, add `loadGuard: null, abortController: null`.
+
+Add helpers:
+
+```js
+function beginLoad() {
+ state.loadGuard ||= window.DIXGraphState.createLoadGuard();
+ state.abortController?.abort();
+ const { seq } = state.loadGuard.begin();
+ state.abortController = new AbortController();
+ return { seq, signal: state.abortController.signal };
+}
+
+function isStale(seq) {
+ return !state.loadGuard.isCurrent(seq);
+}
+
+function isAbortError(err) {
+ return err && (err.name === "AbortError" || /aborted/i.test(String(err.message || err)));
+}
+```
+
+Update `loadRuntimeStats`, `loadProviderData`, `loadModuleData`, `buildModules`, and ego/`DIX.get` calls inside `redraw` to accept/pass `signal`. At each await boundary in `redraw`, return early when `isStale(seq)`. In `catch`, ignore abort errors:
+
+```js
+} catch (err) {
+ if (isAbortError(err)) return;
+ DIX.renderError(canvas, err);
+}
+```
+
+- [ ] **Step 5: Run JS tests GREEN**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add dixhttp/static/js/api.js dixhttp/static/js/graph_state.mjs dixhttp/static/js/views/graph.js dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+fix(dixhttp): cancel stale graph requests safely
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Align Module Budget and Lock Issue Navigation Hashes
+
+**Files:**
+- Modify: `dixhttp/static/js/views/graph.js`
+- Modify: `dixhttp/static/js/graph_state.mjs` (hashes already exist; keep behavior)
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Consumes: `issueGraphHash`, `issueTraceHash`
+- Produces: `budgets.module = { nodes: 150, edges: 400 }`; `loadModuleData` uses `limit: 150, edge_limit: 400`
+
+- [ ] **Step 1: Write failing navigation + budget tests**
+
+Append:
+
+```js
+test("issues have deterministic graph and trace links", async () => {
+ const { issueGraphHash, issueTraceHash } = await import("./graph_state.mjs");
+ const issue = {
+ output_type: "*app.Service",
+ provider: "app.NewService",
+ module: "app/service",
+ };
+ assert.equal(issueGraphHash(issue), "#/graph?mode=ego¢er=*app.Service");
+ assert.equal(issueGraphHash({ ...issue, output_type: "" }), "#/graph?mode=module&module=app%2Fservice");
+ assert.equal(issueTraceHash(issue), "#/trace?provider=app.NewService&output_type=*app.Service&status=error");
+ assert.equal(
+ issueTraceHash({ ...issue, severity: "slow" }),
+ "#/trace?provider=app.NewService&output_type=*app.Service&status=slow"
+ );
+});
+
+test("resolveGraphMode accepts module drilldown", async () => {
+ const { resolveGraphMode } = await import("./graph_state.mjs");
+ assert.equal(resolveGraphMode(new URLSearchParams("mode=module")), "module");
+});
+```
+
+Update `issueTraceHash` if needed so `severity: "slow"` yields `status=slow`; default remains `error`.
+
+- [ ] **Step 2: Run JS tests; fix hash helper if RED**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+- [ ] **Step 3: Align module budgets in graph.js**
+
+Change:
+
+```js
+const budgets = {
+ modules: { nodes: 100, edges: 300 },
+ module: { nodes: 150, edges: 400 },
+ ego: { nodes: 100, edges: 300 },
+ providers: { nodes: 150, edges: 400 },
+ types: { nodes: 150, edges: 400 },
+};
+```
+
+And:
+
+```js
+async function loadModuleData(module, signal) {
+ return DIX.get("/api/module", { name: module, limit: 150, edge_limit: 400 }, { signal });
+}
+```
+
+(If Task 1 did not yet add `signal`, keep the third argument optional.)
+
+- [ ] **Step 4: Run JS + focused Go module API test**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+go test ./dixhttp -run TestHandleModuleReturnsBoundedTopology -count=1
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add dixhttp/static/js/views/graph.js dixhttp/static/js/graph_state.mjs dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+fix(dixhttp): align module graph budget and issue links
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Density Warning Offers Hubs Table
+
+**Files:**
+- Modify: `dixhttp/static/js/graph_state.mjs`
+- Modify: `dixhttp/static/js/views/graph.js`
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Consumes: `applyGraphBudget`
+- Produces: `rankHubNodes(nodes, edges, limit = 10) → [{ id, degree }]`
+
+- [ ] **Step 1: Write failing hub-rank test**
+
+```js
+test("rankHubNodes returns highest-degree nodes first", async () => {
+ const { rankHubNodes } = await import("./graph_state.mjs");
+ const nodes = [{ id: "a" }, { id: "b" }, { id: "c" }];
+ const edges = [
+ { from: "a", to: "b" },
+ { from: "a", to: "c" },
+ { from: "b", to: "c" },
+ ];
+ assert.deepEqual(rankHubNodes(nodes, edges, 2).map((h) => h.id), ["a", "b"]);
+});
+```
+
+- [ ] **Step 2: Run test RED**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+- [ ] **Step 3: Implement rankHubNodes**
+
+```js
+export function rankHubNodes(nodes, edges, limit = 10) {
+ const degree = new Map();
+ for (const node of nodes) degree.set(node.id, 0);
+ for (const edge of edges) {
+ degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
+ degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
+ }
+ return [...nodes]
+ .map((node) => ({ id: node.id, degree: degree.get(node.id) || 0 }))
+ .sort((a, b) => b.degree - a.degree || String(a.id).localeCompare(String(b.id)))
+ .slice(0, limit);
+}
+```
+
+Export on `window.DIXGraphState`.
+
+- [ ] **Step 4: Enrich density warning UI in graph.js**
+
+When `bounded.degraded || graph.truncated`, set `#g-budget` innerHTML (not only textContent) to:
+
+1. One-line warning with node/edge counts
+2. A compact hubs table from `rankHubNodes(bounded.nodes, bounded.edges, 8)`
+3. Hint chips/actions already implied by copy: reduce module / depth / use search
+
+Keep styling minimal with existing `.tbl` / `.muted` classes.
+
+- [ ] **Step 5: Run JS tests GREEN**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add dixhttp/static/js/graph_state.mjs dixhttp/static/js/views/graph.js dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): show hub table when graph budget truncates
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Trace View Hash Prefilter
+
+**Files:**
+- Modify: `dixhttp/static/js/graph_state.mjs`
+- Modify: `dixhttp/static/js/views/trace.js`
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Consumes: hash query `provider`, `output_type`, `status`, `trace_id`
+- Produces: `matchTraceRecord(rec, filter) → boolean`; `filterTraceRecords(records, filter) → records`
+
+Trace API records expose fields such as `provider` / `provider_function`, `output_type`, `status`, `trace_id` (match whatever `/api/trace` already returns; inspect one fixture response while implementing).
+
+- [ ] **Step 1: Write failing filter tests**
+
+```js
+test("filterTraceRecords keeps matching provider and status", async () => {
+ const { filterTraceRecords } = await import("./graph_state.mjs");
+ const records = [
+ { trace_id: "aa", provider_function: "app.NewA", output_type: "*A", status: "error" },
+ { trace_id: "bb", provider_function: "app.NewB", output_type: "*B", status: "ok" },
+ { trace_id: "cc", provider_function: "app.NewA", output_type: "*A", status: "ok" },
+ ];
+ const filtered = filterTraceRecords(records, {
+ provider: "app.NewA",
+ output_type: "*A",
+ status: "error",
+ });
+ assert.deepEqual(filtered.map((r) => r.trace_id), ["aa"]);
+});
+```
+
+- [ ] **Step 2: Run RED, then implement helpers**
+
+```js
+export function filterTraceRecords(records = [], filter = {}) {
+ return records.filter((rec) => matchTraceRecord(rec, filter));
+}
+
+export function matchTraceRecord(rec = {}, filter = {}) {
+ if (filter.trace_id && rec.trace_id !== filter.trace_id) return false;
+ const provider = rec.provider_function || rec.provider || "";
+ if (filter.provider && provider !== filter.provider) return false;
+ if (filter.output_type && rec.output_type !== filter.output_type) return false;
+ if (filter.status === "error" && rec.status !== "error") return false;
+ if (filter.status === "slow") {
+ // slow is a feed severity, not always an event status; keep provider/output matches
+ return true;
+ }
+ return true;
+}
+```
+
+Tune field names against real `/api/trace` JSON before committing.
+
+- [ ] **Step 3: Wire trace.js**
+
+In `DIX.views.trace.render(el, query)` (accept `query` from router like other views; if `main.js` already passes query, use it; otherwise parse `location.hash`):
+
+1. Build `filter` from `provider`, `output_type`, `status`, `trace_id`.
+2. Apply `filterTraceRecords` before grouping.
+3. Sort groups with errors first, then newest.
+4. If `filter.trace_id` is set and present, auto-open that tree once.
+5. Show a muted banner when filters are active, with a clear-filter control that resets hash to `#/trace`.
+
+`main.js` already calls `v.render(el, query)` for every view — do not change it unless the signature is broken.
+
+Match filter fields against `dixtrace.Event` JSON: `provider_function`, `output_type`, `status`, `trace_id`.
+
+- [ ] **Step 4: Run JS tests + smoke Go compile**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+go test ./dixhttp -count=1
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add dixhttp/static/js/graph_state.mjs dixhttp/static/js/views/trace.js dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): prefilter trace view from issue hash
+
+EOF
+)"
+```
+
+---
+
+### Task 5: Graph Drawer Jump to Trace
+
+**Files:**
+- Modify: `dixhttp/static/js/views/graph.js`
+- Modify: `dixhttp/static/js/graph_state.mjs` if a small helper helps
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Consumes: `issueTraceHash({ provider, output_type, severity })`
+- Produces: drawer button `#d-trace` setting `location.hash`
+
+- [ ] **Step 1: Add helper test for provider-detail trace hash**
+
+```js
+test("provider drawer can build trace hash from node identity", async () => {
+ const { issueTraceHash } = await import("./graph_state.mjs");
+ assert.equal(
+ issueTraceHash({ provider: "app.NewService", output_type: "*app.Service", severity: "error" }),
+ "#/trace?provider=app.NewService&output_type=*app.Service&status=error"
+ );
+});
+```
+
+- [ ] **Step 2: Add drawer button in type/provider detail**
+
+In `typeDetail` / provider detail rendering inside `graph.js`, add:
+
+```html
+
+```
+
+Wire:
+
+```js
+document.getElementById("d-trace").onclick = () => {
+ location.hash = window.DIXGraphState.issueTraceHash({
+ provider: providerFnName || "",
+ output_type: label,
+ severity: "error",
+ });
+};
+```
+
+Use the best available provider function name from node `data` / selected provider chips. If only a type label exists, still jump with `output_type`.
+
+- [ ] **Step 3: Manual sanity via example is optional; automated JS tests must PASS**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add dixhttp/static/js/views/graph.js dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): jump from graph drawer to filtered trace
+
+EOF
+)"
+```
+
+---
+
+### Task 6: Optional TraceID on Issues
+
+**Files:**
+- Modify: `dixinternal/dix.go` (`recentErrorRecord` / `recordRecentErrorWithContext`)
+- Modify: `dixinternal/api.go` (`RecentError`)
+- Modify: `dixhttp/server.go` (`IssueInfo`, `buildIssues`)
+- Test: `dixhttp/server_issues_test.go`
+- Test: optional `dixinternal` focused test if TraceID capture needs one
+
+**Interfaces:**
+- Consumes: current span TraceID from inject/provider context when available
+- Produces: `RecentError.TraceID string \`json:"trace_id,omitempty"\``; `IssueInfo.TraceID string \`json:"trace_id,omitempty"\``
+- Updates: `issueTraceHash` prefers `trace_id` query when present
+
+Minimal capture path: in `recordRecentErrorWithContext`, if `dixtrace` has a current span TraceID accessor already used elsewhere in dixinternal, copy it into the record. If no accessor exists without new public API, implement only the additive struct/JSON fields + `buildIssues` projection, leave TraceID empty at runtime, and rely on Task 4 provider/output filters. Do not invent a second TraceID generator.
+
+- [ ] **Step 1: Extend IssueInfo test expectations**
+
+In `dixhttp/server_issues_test.go`, add a unit assertion on `buildIssues` that when `RecentError.TraceID` is set, the projected issue keeps it.
+
+```go
+recent := []dixinternal.RecentError{{
+ ErrorType: "provider_error",
+ Message: "boom",
+ TraceID: "abc123",
+ OutputType: "*app.Service",
+ ProviderFunction: "app.NewService",
+ OccurredAtUnixNano: 1,
+}}
+issues := buildIssues(nil, recent, nil, 0, 10)
+if issues[0].TraceID != "abc123" {
+ t.Fatalf("trace id = %q", issues[0].TraceID)
+}
+```
+
+- [ ] **Step 2: Run RED (missing field), then add fields and projection**
+
+Add `TraceID` to internal recent-error record, `RecentError`, and `IssueInfo`; copy in `buildIssues`.
+
+- [ ] **Step 3: Prefer trace_id in issueTraceHash**
+
+```js
+export function issueTraceHash(issue = {}) {
+ const params = new URLSearchParams();
+ if (issue.trace_id) params.set("trace_id", issue.trace_id);
+ if (issue.provider) params.set("provider", issue.provider);
+ if (issue.output_type) params.set("output_type", issue.output_type);
+ params.set("status", issue.severity === "slow" ? "slow" : "error");
+ return "#/trace?" + params.toString();
+}
+```
+
+Update JS tests accordingly.
+
+- [ ] **Step 4: Run tests**
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+go test ./dixhttp ./dixinternal -run 'Issue|RecentError|Trace' -count=1
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add dixinternal/dix.go dixinternal/api.go dixhttp/server.go dixhttp/server_issues_test.go dixhttp/static/js/graph_state.mjs dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): propagate optional trace id on issues
+
+EOF
+)"
+```
+
+---
+
+### Task 7: Docs and Full Verification
+
+**Files:**
+- Modify: `dixhttp/README.md`
+- Modify: `dixhttp/README_zh.md`
+- Modify: `docs/superpowers/specs/2026-09-05-dependency-visualization-unified-design.md` only if behavior notes need a one-line status flip
+
+**Interfaces:**
+- Consumes: delivered behavior from Tasks 1–6
+- Produces: README sections describing Issue → Graph → Trace and budgets
+
+- [ ] **Step 1: Update README workflow bullets**
+
+Document:
+
+1. `/next#/graph` defaults to module map and does not load `/api/dependencies`.
+2. Module drill-down budget 150/400; density warning includes hubs.
+3. Overview Issues jump to ego/module graph and filtered Trace.
+4. Graph drawer can open filtered Trace.
+5. Legacy `/` and `/api/dependencies` remain available.
+
+- [ ] **Step 2: Full verification**
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+node --test dixhttp/static/js/graph_view.test.mjs
+go test -race ./...
+go test ./example/http -count=1
+```
+
+Expected: all PASS.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add dixhttp/README.md dixhttp/README_zh.md
+git commit -m "$(cat <<'EOF'
+docs(dixhttp): document unified dependency diagnosis workflow
+
+EOF
+)"
+```
+
+---
+
+## Final Review Checklist
+
+- [ ] Stale graph requests abort and do not overwrite newer views.
+- [ ] Module drill-down uses 150/400 budgets.
+- [ ] Issue hash navigation to graph/trace is tested.
+- [ ] Density truncation shows hubs table, not silent cut-only.
+- [ ] Trace view honors provider/output/status/trace_id filters.
+- [ ] Graph drawer can open Trace in ≤1 click from a selected node.
+- [ ] Optional issue `trace_id` is additive and omitted when unknown.
+- [ ] Default `/next` path still avoids eager `/api/dependencies`.
+- [ ] Legacy UI/API remain reachable.
+- [ ] `go test -race ./...` and JS helper tests pass.
diff --git a/docs/superpowers/plans/2026-09-05-scale-graph-usability.md b/docs/superpowers/plans/2026-09-05-scale-graph-usability.md
new file mode 100644
index 0000000..dcd24a0
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-05-scale-graph-usability.md
@@ -0,0 +1,1000 @@
+# Scale-Safe Dependency Visualization Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make dependency visualization correct and usable at approximately 100 modules, 200 providers, and 400 objects by introducing registration-aware data and bounded module-first views.
+
+**Architecture:** Add a stable registration identity to provider registrations, correlate runtime state by provider/output identity, stop source-line aggregation from creating false edges, and make the new graph UI module-first with explicit rendering budgets. Existing legacy APIs remain compatible while the new UI becomes the supported scale path.
+
+**Tech Stack:** Go 1.24+, net/http/httptest, reflection-based Dix graph, vendored vanilla JS, vis-network, hash routing.
+
+**Spec:** `docs/superpowers/specs/2026-09-05-scale-graph-usability-design.md`
+
+## Global Constraints
+
+- Do not introduce third-party Go or JavaScript dependencies.
+- Preserve all existing public Dix APIs.
+- Preserve legacy `/api/dependencies` response compatibility except additive fields and corrected edges/identities.
+- Use TDD for every behavior change; run focused tests before implementation and after implementation.
+- Keep graph phase-one budgets at module map 100 nodes/300 edges, module detail 150 nodes/400 edges, ego maximum depth 5.
+- Do not show an empty default ego graph; `/next#/graph` defaults to module map.
+- Every commit must leave `go test -race ./...` green in the root module.
+
+---
+
+### Task 1: Add Logical Provider Registration Identity
+
+**Files:**
+- Modify: `dixinternal/provider.go`
+- Modify: `dixinternal/dix.go:1119-1220`
+- Modify: `dixinternal/dix.go:1350-1370`
+- Test: `dixinternal/provider_identity_test.go`
+
+**Interfaces:**
+- Consumes: private `providerFn`, `Dix.handleProvide`, and `Dix.provide`.
+- Produces: `providerFn.registrationID uint64`; `handleProvide(fnVal reflect.Value, outType reflect.Type, inputs []*providerInputType, registrationID uint64) error`.
+
+- [x] **Step 1: Write the failing identity tests**
+
+Create `dixinternal/provider_identity_test.go`:
+
+```go
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type identityA struct{}
+type identityB struct{}
+type identityAggregate struct {
+ A *identityA
+ B *identityB
+}
+
+func TestStructProviderOutputsShareRegistrationID(t *testing.T) {
+ di := New()
+ di.Provide(func() identityAggregate {
+ return identityAggregate{A: &identityA{}, B: &identityB{}}
+ })
+
+ ids := make(map[string]uint64)
+ for _, provider := range di.providers[reflect.TypeOf(&identityA{})] {
+ ids["A"] = provider.registrationID
+ }
+ for _, provider := range di.providers[reflect.TypeOf(&identityB{})] {
+ ids["B"] = provider.registrationID
+ }
+ if ids["A"] == 0 || ids["A"] != ids["B"] {
+ t.Fatalf("struct outputs should share one registration ID: %#v", ids)
+ }
+}
+
+func TestDistinctClosureRegistrationsHaveDistinctRegistrationIDs(t *testing.T) {
+ di := New()
+ for _, name := range []string{"first", "second"} {
+ value := name
+ di.Provide(func() *identityA { return &identityA{} })
+ if value == "" {
+ t.Fatal("closure registration setup unexpectedly empty")
+ }
+ }
+
+ providers := di.providers[reflect.TypeOf(&identityA{})]
+ if len(providers) != 2 {
+ t.Fatalf("expected 2 providers, got %d", len(providers))
+ }
+ if providers[0].registrationID == providers[1].registrationID {
+ t.Fatal("distinct registrations must not share a registration ID")
+ }
+}
+```
+
+- [x] **Step 2: Run the focused test and verify RED**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixinternal -run Test.*(RegistrationID|RegistrationIDs) -count=1
+```
+
+Expected: compilation fails because `registrationID` and the new `handleProvide` signature do not exist.
+
+- [x] **Step 3: Implement registration identity**
+
+In `dixinternal/provider.go`, add the field:
+
+```go
+type providerFn struct {
+ fn reflect.Value
+ inputList []*providerInputType
+ output *providerOutputType
+ hasError bool
+ registrationID uint64
+}
+```
+
+In `dixinternal/dix.go`, add a container counter and change the private signature:
+
+```go
+func (dix *Dix) handleProvide(fnVal reflect.Value, outType reflect.Type, inputs []*providerInputType, registrationID uint64) error {
+ // existing implementation, but construct:
+ provider := &providerFn{fn: fnVal, inputList: inputs, hasError: hasError, registrationID: registrationID}
+```
+
+Pass the same `registrationID` into the recursive struct-field call. At the top-level call in `provide`, allocate a new identity:
+
+```go
+dix.registrationSeq++
+if err := dix.handleProvide(fnVal, typ.Out(0), inputs, dix.registrationSeq); err != nil {
+```
+
+Add `registrationSeq uint64` beside `graph` in the private `Dix` struct. Container writes are documented single-threaded, so no atomic is required.
+
+- [x] **Step 4: Run the focused test and verify GREEN**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixinternal -run Test.*(RegistrationID|RegistrationIDs) -count=1
+```
+
+Expected: both tests pass.
+
+- [x] **Step 5: Run the root race suite**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test -race ./...
+```
+
+Expected: all packages pass.
+
+- [x] **Step 6: Commit**
+
+```bash
+git add dixinternal/provider.go dixinternal/dix.go dixinternal/provider_identity_test.go
+git commit -m "feat(dixinternal): add provider registration identity"
+```
+
+---
+
+### Task 2: Expose Stable Provider IDs and Fix Dependency Aggregation
+
+**Files:**
+- Modify: `dixinternal/api.go:200-260`
+- Modify: `dixhttp/server.go:650-820`
+- Test: `dixhttp/server_provider_identity_test.go`
+
+**Interfaces:**
+- Consumes: `providerFn.registrationID` from Task 1.
+- Produces: `ProviderDetails.ProviderID string`, `ProviderInfo.ProviderIDs []string`, and `providerAggregateKey(detail dixinternal.ProviderDetails) string`.
+
+- [x] **Step 1: Write the failing API projection test**
+
+Create `dixhttp/server_provider_identity_test.go`:
+
+```go
+package dixhttp
+
+import (
+ "encoding/json"
+ "net/http/httptest"
+ "testing"
+
+ dix "github.com/pubgo/dix/v2"
+ "github.com/pubgo/dix/v2/dixinternal"
+)
+
+type projectionInput struct{}
+type projectionOutputA struct{}
+type projectionOutputB struct{}
+type projectionAggregate struct {
+ A *projectionOutputA
+ B *projectionOutputB
+}
+
+func TestDependenciesPreserveRegistrationAndOutputIdentity(t *testing.T) {
+ container := dix.New()
+ dix.Provide(container, func() projectionAggregate {
+ return projectionAggregate{A: &projectionOutputA{}, B: &projectionOutputB{}}
+ })
+
+ server := NewServer(container)
+ recorder := httptest.NewRecorder()
+ server.ServeHTTP(recorder, httptest.NewRequest("GET", "/api/dependencies", nil))
+ if recorder.Code != 200 {
+ t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
+ }
+
+ var response DependencyData
+ if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
+ t.Fatal(err)
+ }
+
+ registrationIDs := map[uint64]bool{}
+ providerIDs := map[string]bool{}
+ for _, provider := range response.Providers {
+ if provider.OutputType != "*dixhttp.projectionOutputA" && provider.OutputType != "*dixhttp.projectionOutputB" {
+ continue
+ }
+ if provider.RegistrationID == 0 || len(provider.ProviderIDs) == 0 {
+ t.Fatalf("provider identities must not be empty: %+v", provider)
+ }
+ registrationIDs[provider.RegistrationID] = true
+ for _, providerID := range provider.ProviderIDs {
+ providerIDs[providerID] = true
+ }
+ }
+ if len(registrationIDs) != 1 {
+ t.Fatalf("one struct registration should produce one registration_id, got %v", registrationIDs)
+ }
+ if len(providerIDs) != 2 {
+ t.Fatalf("two outputs should produce two provider IDs, got %v", providerIDs)
+ }
+}
+
+func TestDistinctSameLineRegistrationsDoNotCartesianProductEdges(t *testing.T) {
+ container := dix.New()
+ for i := 0; i < 2; i++ {
+ dix.Provide(container, func() *projectionOutputA { return &projectionOutputA{} })
+ }
+
+ server := NewServer(container)
+ recorder := httptest.NewRecorder()
+ server.ServeHTTP(recorder, httptest.NewRequest("GET", "/api/dependencies", nil))
+
+ var response DependencyData
+ if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
+ t.Fatal(err)
+ }
+ targetCount := 0
+ for _, provider := range response.Providers {
+ if provider.OutputType == "*dixhttp.projectionOutputA" {
+ targetCount++
+ }
+ }
+ if targetCount != 2 {
+ t.Fatalf("expected distinct provider nodes, got %d", targetCount)
+ }
+ for _, provider := range response.Providers {
+ if provider.OutputType != "*dixhttp.projectionOutputA" {
+ continue
+ }
+ if len(provider.InputTypes)*len(provider.OutputTypes) > 1 {
+ t.Fatalf("unrelated input/output pairs created a Cartesian product: %+v", provider)
+ }
+ }
+}
+```
+
+- [x] **Step 2: Run the focused HTTP test and verify RED**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixhttp -run Test(DependenciesPreserve|DistinctSameLine) -count=1
+```
+
+Expected: compilation fails because `RegistrationID` and `ProviderID` do not exist.
+
+- [x] **Step 3: Project and aggregate by registration identity**
+
+In `dixinternal.ProviderDetails`, add:
+
+```go
+RegistrationID uint64 `json:"registration_id"`
+ProviderID string `json:"provider_id"`
+```
+
+In `GetProviderDetails`, construct IDs:
+
+```go
+registrationID := providerFn.registrationID
+providerID := fmt.Sprintf("provider_%d_%s", registrationID, outputType.String())
+```
+
+In `dixhttp.ProviderInfo`, add:
+
+```go
+RegistrationID uint64 `json:"registration_id"`
+ProviderIDs []string `json:"provider_ids"`
+```
+
+A logical registration with multiple outputs must expose one concrete `provider_id` per output in `ProviderIDs`. It cannot use a single `provider_id` without losing runtime-state correlation.
+
+Change the aggregate bucket key to:
+
+```go
+func providerAggregateKey(detail dixinternal.ProviderDetails) string {
+ if detail.RegistrationID != 0 {
+ return fmt.Sprintf("registration_%d", detail.RegistrationID)
+ }
+ // Keep the legacy fallback only for externally constructed fixtures.
+ if detail.FunctionFile != "" && detail.FunctionLine > 0 {
+ return fmt.Sprintf("%s:%d", detail.FunctionFile, detail.FunctionLine)
+ }
+ if detail.FunctionName != "" {
+ return detail.FunctionName
+ }
+ if detail.OutputType != "" {
+ return detail.OutputType
+ }
+ return "unknown"
+}
+```
+
+Copy `RegistrationID` into `ProviderInfo`, and collect every output-specific `detail.ProviderID` into `ProviderIDs`. Edge generation must iterate actual detail input/output pairs while accumulating bucket metadata; it must not regenerate from merged `InputTypes × OutputTypes`.
+
+Refactor the bucket to retain raw `dixinternal.ProviderDetails` values so edge building can preserve exact relationships.
+
+- [x] **Step 4: Run the focused HTTP test and verify GREEN**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixhttp -run Test(DependenciesPreserve|DistinctSameLine) -count=1
+```
+
+Expected: both tests pass.
+
+- [x] **Step 5: Run existing dependency regression tests**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixhttp -run TestHandleDependencies -count=1
+```
+
+Expected: pass, including the existing multi-output aggregation test.
+
+- [x] **Step 6: Commit**
+
+```bash
+git add dixinternal/api.go dixhttp/server.go dixhttp/server_provider_identity_test.go
+git commit -m "fix(dixhttp): preserve provider identity and dependency edges"
+```
+
+---
+
+### Task 3: Correlate Runtime Stats by Provider Identity
+
+**Files:**
+- Modify: `dixinternal/api.go:170-310`
+- Modify: `dixhttp/static/js/views/graph.js:42-55`
+- Modify: `dixhttp/static/js/views/graph.js:213-240`
+- Test: `dixinternal/runtime_stats_identity_test.go`
+
+**Interfaces:**
+- Consumes: `providerFn.registrationID`.
+- Produces: `ProviderRuntimeStats.RegistrationID uint64`, `ProviderRuntimeStats.ProviderID string`, and `statForProvider(provider ProviderInfo) ProviderRuntimeStats | null`.
+
+- [x] **Step 1: Write the failing runtime identity test**
+
+Create `dixinternal/runtime_stats_identity_test.go`:
+
+```go
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type runtimeTarget struct{ Name string }
+
+func TestRuntimeStatsIncludeConcreteProviderIdentity(t *testing.T) {
+ di := New()
+ di.Provide(func() *runtimeTarget { return &runtimeTarget{Name: "ready"} })
+ _ = di.TryInject(func(*runtimeTarget) {})
+
+ stats := di.GetProviderRuntimeStats()
+ for _, stat := range stats {
+ if stat.OutputType != "*dixinternal.runtimeTarget" {
+ continue
+ }
+ if stat.RegistrationID == 0 {
+ t.Fatal("expected non-zero registration ID")
+ }
+ if stat.ProviderID == "" {
+ t.Fatal("expected provider ID")
+ }
+ if stat.CallCount != 1 {
+ t.Fatalf("call count = %d, want 1", stat.CallCount)
+ }
+ return
+ }
+ t.Fatalf("target stat not found in %+v", stats)
+}
+
+func TestRuntimeStatsDoNotDeduplicateDistinctClosures(t *testing.T) {
+ di := New()
+ for i := 0; i < 2; i++ {
+ di.Provide(func() *runtimeTarget { return &runtimeTarget{} })
+ }
+ stats := di.GetProviderRuntimeStats()
+ count := 0
+ for _, stat := range stats {
+ if stat.OutputType == reflect.TypeOf(&runtimeTarget{}).String() {
+ count++
+ }
+ }
+ if count != 2 {
+ t.Fatalf("provider stats = %d, want 2", count)
+ }
+}
+```
+
+- [x] **Step 2: Run the focused runtime test and verify RED**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixinternal -run TestRuntimeStats -count=1
+```
+
+Expected: compilation fails because identity fields do not exist.
+
+- [x] **Step 3: Emit concrete identity fields**
+
+Add fields to `ProviderRuntimeStats`:
+
+```go
+RegistrationID uint64 `json:"registration_id"`
+ProviderID string `json:"provider_id"`
+```
+
+In `GetProviderRuntimeStats`, replace function-name-only deduplication with concrete provider/output identity:
+
+```go
+seen := make(map[string]bool)
+identity := fmt.Sprintf("%d:%s", p.registrationID, outputType)
+if seen[identity] {
+ continue
+}
+seen[identity] = true
+item.RegistrationID = p.registrationID
+item.ProviderID = fmt.Sprintf("provider_%d_%s", p.registrationID, outputType)
+```
+
+Update the graph JS stat lookup to use:
+
+```js
+function statForProvider(provider) {
+ const stats = state.runtimeStats || [];
+ for (const providerID of provider.provider_ids || []) {
+ const exact = stats.find(s => s.provider_id === providerID);
+ if (exact) return exact;
+ }
+ return stats.find(s => !s.provider_id && s.function_name === provider.function_name) || null;
+}
+```
+
+Use this helper in provider detail and error coloring. Keep the fallback only for stale cached responses.
+
+- [x] **Step 4: Run the focused runtime test and verify GREEN**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixinternal -run TestRuntimeStats -count=1
+```
+
+Expected: both tests pass.
+
+- [x] **Step 5: Run race tests**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test -race ./...
+```
+
+Expected: all packages pass.
+
+- [x] **Step 6: Commit**
+
+```bash
+git add dixinternal/api.go dixhttp/static/js/views/graph.js dixinternal/runtime_stats_identity_test.go
+git commit -m "fix(dixinternal): identify runtime stats by provider output"
+```
+
+---
+
+### Task 4: Correct Module, Ego State, and Resolved Aggregation
+
+**Files:**
+- Modify: `dixinternal/graph_query.go:40-260`
+- Modify: `dixinternal/graph_query.go:265-360`
+- Modify: `dixhttp/server.go:414-540`
+- Test: `dixinternal/graph_query_projection_test.go`
+- Test: `dixhttp/server_packages_test.go`
+
+**Interfaces:**
+- Consumes: `Graph` nodes/edges and cached provider details.
+- Produces: accurate `ModuleGraph`, `EgoGraph` instantiated state, `ResolvedTopN`, and `PackageInfo`.
+
+- [x] **Step 1: Write failing projection tests**
+
+Create `dixinternal/graph_query_projection_test.go`:
+
+```go
+package dixinternal
+
+import (
+ "reflect"
+ "testing"
+)
+
+type packageTarget struct{}
+
+func TestEgoInstantiatedUsesObjectNodes(t *testing.T) {
+ di := New()
+ di.graph.node(NodeType, reflect.TypeOf(&packageTarget{}), "", nil)
+ view := di.EgoGraph("*dixinternal.packageTarget", 1, "both")
+ for _, node := range view.Nodes {
+ if node.Label == "*dixinternal.packageTarget" && node.State == "instantiated" {
+ t.Fatal("missing provider must not be reported instantiated")
+ }
+ }
+
+ di.Provide(func() *packageTarget { return &packageTarget{} })
+ di.graph.addObject(reflect.TypeOf(&packageTarget{}), "")
+ view = di.EgoGraph("*dixinternal.packageTarget", 1, "both")
+ instantiated := false
+ for _, node := range view.Nodes {
+ if node.Label == "*dixinternal.packageTarget" && node.State == "instantiated" {
+ instantiated = true
+ }
+ }
+ if !instantiated {
+ t.Fatal("object-bearing ego node should be instantiated")
+ }
+}
+
+func TestResolvedTopNAggregatesByType(t *testing.T) {
+ di := New()
+ di.Provide(func() *packageTarget { return &packageTarget{} })
+ for i := 0; i < 3; i++ {
+ _, _ = di.TryInject(func(*packageTarget) {})
+ }
+
+ rows := di.ResolvedTopN(10)
+ found := 0
+ for _, row := range rows {
+ if row.Type == "*dixinternal.packageTarget" {
+ found++
+ if row.Count != 3 {
+ t.Fatalf("count = %d, want 3", row.Count)
+ }
+ }
+ }
+ if found != 1 {
+ t.Fatalf("type rows = %d, want 1", found)
+ }
+}
+```
+
+Create `dixhttp/server_packages_test.go`:
+
+```go
+package dixhttp
+
+import "testing"
+
+func TestPackageInfoUsesResolvedOutputPackage(t *testing.T) {
+ details := []dixinternal.ProviderDetails{
+ {OutputType: "*main.Plugin[main.RoleReader]", OutputPkg: "main"},
+ {OutputType: "*billing.Client", OutputPkg: "example/billing"},
+ }
+ packages := buildPackageInfos(details)
+ if len(packages) != 2 {
+ t.Fatalf("packages = %+v", packages)
+ }
+ if packages[0].Name != "main" || packages[1].Name != "example/billing" {
+ t.Fatalf("malformed generic package path retained: %+v", packages)
+ }
+}
+```
+
+Add the required import `github.com/pubgo/dix/v2/dixinternal` to the HTTP test.
+
+- [x] **Step 2: Run focused projection tests and verify RED**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixinternal -run Test(EgoInstantiated|ResolvedTopNAggregates) -count=1; go test ./dixhttp -run TestPackageInfoUses -count=1
+```
+
+Expected: ego/state and aggregation/package helper failures.
+
+- [x] **Step 3: Fix the three projections**
+
+In `EgoGraph`, build an instantiated set from object nodes before creating nodes:
+
+```go
+instantiated := make(map[reflect.Type]bool)
+for key := range g.nIndex {
+ if key.kind == NodeObject {
+ instantiated[key.typ] = true
+ }
+}
+```
+
+Set node state to `"instantiated"` only when `instantiated[t]`; otherwise omit state.
+
+In `ResolvedTopN`, accumulate counts by type before sorting:
+
+```go
+countsByType := make(map[string]int64)
+for _, e := range g.eIndex {
+ if e.Kind == EdgeResolved && e.Count > 0 {
+ countsByType[g.nodes[e.To].Type.String()] += e.Count
+ }
+}
+```
+
+Then sort and truncate the materialized rows.
+
+Extract package construction from `HandlePackages` into:
+
+```go
+func buildPackageInfos(details []dixinternal.ProviderDetails) []PackageInfo
+```
+
+Use `detail.OutputPkg` when non-empty and `(anonymous)` when empty. Do not call `extractPackage(detail.OutputType)` for provider package grouping. Return rows sorted by `Name` so API output and tests are deterministic.
+
+- [x] **Step 4: Run focused projection tests and verify GREEN**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixinternal -run Test(EgoInstantiated|ResolvedTopNAggregates) -count=1; go test ./dixhttp -run TestPackageInfoUses -count=1
+```
+
+Expected: all focused tests pass.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add dixinternal/graph_query.go dixhttp/server.go dixinternal/graph_query_projection_test.go dixhttp/server_packages_test.go
+git commit -m "fix(dixhttp): correct graph projection state and modules"
+```
+
+---
+
+### Task 5: Make the New Graph Default Module-First and Bounded
+
+**Files:**
+- Modify: `dixhttp/static/js/views/graph.js:1-420`
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Consumes: `/api/modules`, `/api/ego`, and identity-aware `ProviderInfo`.
+- Produces: `resolveGraphMode(query)`, `applyGraphBudget(nodes, edges, budget)`, and module-first graph state.
+
+- [x] **Step 1: Write failing pure JS view tests**
+
+Create `dixhttp/static/js/graph_view.test.mjs`:
+
+```js
+import test from "node:test";
+import assert from "node:assert/strict";
+
+test("graph defaults to module map instead of empty ego", async () => {
+ const { resolveGraphMode } = await import("./graph_state.mjs");
+ assert.equal(resolveGraphMode(new URLSearchParams()), "modules");
+ assert.equal(resolveGraphMode(new URLSearchParams("mode=ego")), "ego");
+});
+
+test("graphs degrade to table metadata above explicit budgets", async () => {
+ const { applyGraphBudget } = await import("./graph_state.mjs");
+ const nodes = Array.from({ length: 101 }, (_, i) => ({ id: String(i) }));
+ const edges = Array.from({ length: 301 }, (_, i) => ({ from: String(i % 100), to: String((i + 1) % 100) }));
+ const result = applyGraphBudget(nodes, edges, { nodes: 100, edges: 300 });
+ assert.equal(result.nodes.length, 100);
+ assert.equal(result.edges.length, 300);
+ assert.equal(result.degraded, true);
+});
+```
+
+- [x] **Step 2: Run JS tests and verify RED**
+
+Run:
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+Expected: module resolution fails because `graph_state.mjs` does not exist.
+
+- [x] **Step 3: Extract and implement pure graph state helpers**
+
+Create `dixhttp/static/js/graph_state.mjs`:
+
+```js
+export function resolveGraphMode(query) {
+ const mode = (query.get("mode") || "").trim();
+ return ["modules", "ego", "providers", "types"].includes(mode) ? mode : "modules";
+}
+
+export function applyGraphBudget(nodes, edges, budget = { nodes: 100, edges: 300 }) {
+ if (nodes.length <= budget.nodes && edges.length <= budget.edges) {
+ return { nodes, edges, degraded: false };
+ }
+ const degree = new Map();
+ for (const edge of edges) {
+ degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
+ degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
+ }
+ const keptNodes = new Set([...nodes].sort((a, b) =>
+ (degree.get(b.id) || 0) - (degree.get(a.id) || 0) ||
+ String(a.id).localeCompare(String(b.id))
+ ).slice(0, budget.nodes).map(node => node.id));
+ const keptEdges = edges.filter(edge =>
+ keptNodes.has(edge.from) && keptNodes.has(edge.to)
+ ).slice(0, budget.edges);
+ return {
+ nodes: nodes.filter(node => keptNodes.has(node.id)),
+ edges: keptEdges,
+ degraded: true,
+ };
+}
+```
+
+Create a browser adapter `dixhttp/static/js/graph_state.js` that exposes the same functions on `window.DIXGraphState`, either by duplicating the small pure logic or via a generated embedded bundle only if the build already supports one.
+
+Update `views/graph.js` to import/use these helpers, default the select control to `modules`, and render a visible density warning when `degraded` is true.
+
+- [x] **Step 4: Remove eager full dependencies loading**
+
+Change `loadData()` so only providers/types detail views request `/api/dependencies`; module and ego views must not preload it. Request runtime stats in parallel and cache by provider identity. The graph draw path must use `/api/modules` for module mode and `/api/ego` for ego mode.
+
+- [x] **Step 5: Run JS tests**
+
+Run:
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+Expected: both tests pass.
+
+- [x] **Step 6: Run Go tests**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test -race ./...
+```
+
+Expected: all packages pass.
+
+- [x] **Step 7: Commit**
+
+```bash
+git add dixhttp/static/js/graph_state.mjs dixhttp/static/js/graph_state.js dixhttp/static/js/views/graph.js dixhttp/static/js/graph_view.test.mjs
+git commit -m "feat(dixhttp): default graph to bounded module map"
+```
+
+---
+
+### Task 6: Responsive Graph Layout and Manual Scale Verification
+
+**Files:**
+- Modify: `dixhttp/static/css/app.css:100-150`
+- Modify: `dixhttp/static/js/views/graph.js:15-40`
+- Test: `dixhttp/http_scale_e2e_test.go`
+
+**Interfaces:**
+- Consumes: module-first UI and graph budgets.
+- Produces: stable graph canvas sizing and an HTTP smoke test proving `/next` defaults to module assets.
+
+- [x] **Step 1: Write the failing HTTP/UI smoke test**
+
+Create `dixhttp/http_scale_e2e_test.go`:
+
+```go
+package dixhttp
+
+import (
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ dix "github.com/pubgo/dix/v2"
+)
+
+func TestNextGraphDefaultsToModuleFirstAssets(t *testing.T) {
+ container := dix.New()
+ server := NewServer(container)
+ recorder := httptest.NewRecorder()
+ server.ServeHTTP(recorder, httptest.NewRequest("GET", "/next", nil))
+
+ if recorder.Code != 200 {
+ t.Fatalf("status = %d", recorder.Code)
+ }
+ body := recorder.Body.String()
+ for _, asset := range []string{"static/js/views/graph.js", "static/js/main.js"} {
+ if !strings.Contains(body, asset) {
+ t.Fatalf("missing asset %s", asset)
+ }
+ }
+}
+```
+
+- [x] **Step 2: Run smoke test and verify RED or baseline**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test ./dixhttp -run TestNextGraphDefaults -count=1
+```
+
+Expected: this route already serves the assets and should pass; if it fails, fix routing before UI CSS work.
+
+- [x] **Step 3: Lock canvas geometry**
+
+Update `app.css`:
+
+```css
+.graph-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(280px, 320px);
+ align-items: stretch;
+}
+
+#graph-canvas {
+ height: clamp(420px, calc(100vh - 220px), 900px);
+ min-width: 300px;
+}
+
+@media (max-width: 900px) {
+ .graph-layout { grid-template-columns: minmax(0, 1fr); }
+ #graph-canvas { height: 70vh; min-height: 420px; }
+ #g-detail { position: static; max-height: 45vh; overflow: auto; }
+}
+```
+
+Apply `class="graph-layout"` to the graph grid and use `minmax(0, 1fr)` for all toolbar inputs that can overflow.
+
+- [x] **Step 4: Run automated checks**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test -race ./... && node --test dixhttp/static/js/graph_view.test.mjs
+```
+
+Expected: all checks pass.
+
+- [x] **Step 5: Run the scale fixture manually**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; (cd example && DIX_HTTP_ADDR=127.0.0.1:18099 go run ./http)
+```
+
+Inspect:
+
+```text
+http://127.0.0.1:18099/next#/graph
+```
+
+Confirm module map is visible, no full dependency request occurs on first render, and the canvas remains usable at 700px viewport width. Stop the fixture after verification.
+
+- [x] **Step 6: Commit**
+
+```bash
+git add dixhttp/http_scale_e2e_test.go dixhttp/static/css/app.css dixhttp/static/js/views/graph.js
+git commit -m "fix(dixhttp): keep scaled graph views usable"
+```
+
+---
+
+### Task 7: Documentation, Example Fixture, and Full Verification
+
+**Files:**
+- Modify: `dixhttp/README.md`
+- Modify: `dixhttp/README_zh.md`
+- Modify: `example/http/main.go`
+- Test: `example/http/scale_shape_test.go`
+
+**Interfaces:**
+- Consumes: completed phase-one behavior.
+- Produces: documented scale workflow and a repeatable near-target fixture assertion.
+
+- [x] **Step 1: Write the failing example shape test**
+
+Create `example/http/scale_shape_test.go`:
+
+```go
+package main
+
+import "testing"
+
+func TestScaleFixtureShape(t *testing.T) {
+ container := buildContainer()
+ modules := container.ModuleGraph()
+ providers := container.GetProviderDetails()
+ objects := container.GetObjects()
+
+ objectCount := 0
+ for _, groups := range objects {
+ for _, values := range groups {
+ objectCount += len(values)
+ }
+ }
+ if len(modules) < 10 {
+ t.Fatalf("modules = %d, want at least 10", len(modules))
+ }
+ if len(providers) < 190 {
+ t.Fatalf("providers = %d, want at least 190", len(providers))
+ }
+ if objectCount < 170 {
+ t.Fatalf("objects = %d, want at least 170", objectCount)
+ }
+}
+```
+
+`buildContainer()` already returns `*dix.Dix` and is in package `main`; call it directly. Do not add another fixture builder unless production initialization changes.
+
+- [x] **Step 2: Run example shape test and verify RED or baseline**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test -C example ./http -run TestScaleFixtureShape -count=1
+```
+
+Expected: pass if the existing fixture already satisfies the bounds; otherwise extract the helper and make the test pass.
+
+- [x] **Step 3: Document the module-first workflow**
+
+In both README files, replace claims that the default graph is a full graph. Document:
+
+```text
+Default graph = module map
+Module click = bounded module detail
+Type click/search = ego graph
+Object = state shown in type/provider details
+Over-budget graph = density warning plus table/Top-K path
+```
+
+Document `/api/dependencies` as a compatibility/full-data endpoint, not the first-scale UI data source.
+
+- [x] **Step 4: Run complete verification**
+
+Run:
+
+```bash
+unset GOROOT; export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin; go test -race ./...; go vet ./...; go build -C example ./...; go test -C example -count=1 ./...
+```
+
+Expected: every command succeeds.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add dixhttp/README.md dixhttp/README_zh.md example/http/main.go example/http/scale_shape_test.go
+git commit -m "docs(dixhttp): document scale-first dependency workflow"
+```
+
+---
+
+## Final Review Checklist
+
+- [x] Generic registrations do not merge merely because they share source line.
+- [x] Struct multi-output registrations still aggregate as one logical registration.
+- [x] Runtime errors and durations attach to the correct provider/output pair.
+- [x] Ego state does not claim instantiation without an object node.
+- [x] `/next#/graph` renders module map on first load.
+- [x] Module map does not request `/api/dependencies`.
+- [x] Graph budgets and degradation warning are active.
+- [x] Narrow viewport keeps graph canvas usable.
+- [x] Root race tests, vet, example build, and example tests pass.
+- [x] Documentation matches actual default behavior.
diff --git a/docs/superpowers/plans/2026-09-06-legacy-first-architecture-viz.md b/docs/superpowers/plans/2026-09-06-legacy-first-architecture-viz.md
new file mode 100644
index 0000000..2e5f386
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-06-legacy-first-architecture-viz.md
@@ -0,0 +1,567 @@
+# Legacy-First Architecture Visualization Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make the legacy `/` Dix UI readable for large DI architecture review (module/provider/type/group lenses), port useful `/next` graph helpers into it, then delete `/next`.
+
+**Architecture:** Keep Alpine + `template.html` + `legacy/app.js` as the only UI. Load shared `graph_state.mjs` / selected workbench helpers onto `window` for legacy `renderGraph`. Add module-map view, density banner (lens prompt → hubs → short labels → layout → Top-K last), then remove `/next` route and five-view shell.
+
+**Tech Stack:** Go embed static FS, Alpine.js, vis-network, Mermaid (already vendored), ES modules for shared helpers, `node --test` for pure JS.
+
+## Global Constraints
+
+- Single UI entry: `/` only after Task 7; no parallel five-view shell.
+- Preserve all existing legacy capabilities (package sidebar, group rules, prefix, depth, layouts, SVG/Mermaid, diagnostic modals, Trace).
+- Default view remains `providers` unless user switches.
+- Objects are not default architecture canvas nodes (providers/types/modules/groups only).
+- Public Dix APIs: additive fields only; no new third-party JS/Go deps.
+- Crowding degradation order is fixed: prompt coarser lens → semantic collapse hint → hubs → short labels → layout/camera → Top-K last.
+- Do not silently switch the user’s current view; one-click apply from banner is OK.
+- Spec: `docs/superpowers/specs/2026-09-06-legacy-first-architecture-viz-design.md`
+
+## File Map
+
+| File | Responsibility |
+|---|---|
+| `dixhttp/static/js/graph_state.mjs` | Shared labels, budget, hubs, layout/camera, star positions (keep; drop next-only defaults later) |
+| `dixhttp/static/js/graph_workbench.mjs` | Optional: group/filter helpers if legacy chooses to call them; otherwise leave until needed |
+| `dixhttp/static/js/graph_view.test.mjs` | Node tests for shared helpers; remove next-hash-only cases when APIs removed |
+| `dixhttp/template.html` | Load mjs; module map button; density banner DOM |
+| `dixhttp/static/js/legacy/app.js` | Wire module map, labels, budget, banner, layout into `renderGraph` |
+| `dixhttp/server.go` | Remove `/next` handler |
+| `dixhttp/http_scale_e2e_test.go` | Assert `/` embeds graph helpers, not `/next` |
+| `dixhttp/scripts/graph_layout_e2e.mjs` | Point health check at `/` |
+| `dixhttp/README.md`, `README_zh.md` | Single-UI docs |
+| Delete | `static/index.html`, `static/js/main.js`, `static/js/views/*`, `static/js/api.js` if only used by next |
+
+---
+
+### Task 1: Load shared graph helpers in legacy HTML
+
+**Files:**
+- Modify: `dixhttp/template.html` (script tags near bottom)
+- Test: manual + later Go test in Task 7
+
+**Interfaces:**
+- Consumes: `graph_state.mjs` already assigns `window.DIXGraphState`
+- Produces: legacy page can call `window.DIXGraphState.shortGraphLabel` after load
+
+- [ ] **Step 1: Add module scripts before `legacy/app.js`**
+
+In `dixhttp/template.html`, before the `legacy/app.js` script tag, insert:
+
+```html
+
+
+
+```
+
+Bump `legacy/app.js` query from `legacy1` to `legacy-arch1` so caches refresh.
+
+- [ ] **Step 2: Guard helper access in `app.js`**
+
+Near the top of the Alpine `app()` return object methods area (or a small helper method), add:
+
+```javascript
+graphHelpers() {
+ return window.DIXGraphState || null;
+},
+```
+
+Do not call helpers during synchronous script evaluate; only from `init` / `renderGraph` (after modules ran).
+
+- [ ] **Step 3: Smoke in browser or curl**
+
+Run demo server, open `/`, in DevTools: `typeof window.DIXGraphState.shortGraphLabel === 'function'`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add dixhttp/template.html dixhttp/static/js/legacy/app.js
+git commit -m "$(cat <<'EOF'
+chore(dixhttp): load shared graph helpers in legacy UI
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Short labels on architecture nodes (TDD on helper already exists)
+
+**Files:**
+- Modify: `dixhttp/static/js/legacy/app.js` (`formatTypeName`, provider display labels)
+- Test: `dixhttp/static/js/graph_view.test.mjs` (existing `shortGraphLabel` test must still pass)
+
+**Interfaces:**
+- Consumes: `DIXGraphState.shortGraphLabel(name: string): string`
+- Produces: node `label` short; `title` keeps full name
+
+- [ ] **Step 1: Confirm existing test still passes**
+
+Run: `node --test dixhttp/static/js/graph_view.test.mjs --test-name-pattern shortGraphLabel`
+Expected: PASS
+
+- [ ] **Step 2: Wire `formatTypeName` through short label**
+
+Replace body of `formatTypeName` to keep full name in tooltip path but shorten display:
+
+```javascript
+formatTypeName(typeName) {
+ const full = String(typeName || '');
+ const helpers = this.graphHelpers();
+ if (helpers && helpers.shortGraphLabel) {
+ return helpers.shortGraphLabel(full);
+ }
+ // existing fallback truncation if any
+ return full;
+},
+```
+
+Ensure wherever nodes set `title: '类型: ' + outType` they still use the **full** type string (already true in `renderGraph`).
+
+- [ ] **Step 3: Shorten provider box labels similarly**
+
+In `providerNodeLabel`, if the label is a long function path, prefer:
+
+```javascript
+providerNodeLabel(provider) {
+ const raw = /* existing computation */;
+ const helpers = this.graphHelpers();
+ if (helpers && helpers.shortGraphLabel) {
+ return helpers.shortGraphLabel(raw);
+ }
+ return raw;
+},
+```
+
+Keep full identity in `title` / `buildProviderTooltip`.
+
+- [ ] **Step 4: Manual check**
+
+Open `/`, Providers view: labels short; hover title shows full path.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add dixhttp/static/js/legacy/app.js
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): shorten legacy graph labels via shared helper
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Module map view in legacy toolbar
+
+**Files:**
+- Modify: `dixhttp/template.html` (view buttons)
+- Modify: `dixhttp/static/js/legacy/app.js` (`switchView`, `renderGraph`, fetch modules)
+- Test: add node test for building module nodes/edges from API-shaped data (pure function preferred)
+
+**Interfaces:**
+- Consumes: `GET /api/modules` → `[{ name, provider_count, object_count, type_count, depends_on }]`
+- Consumes: `DIXGraphState.layoutStarPositions`, `shortGraphLabel`
+- Produces: `currentView === 'modules'` renders module graph; objects only as count in label text, not separate nodes
+
+- [ ] **Step 1: Write failing test for module graph builder**
+
+Add to `dixhttp/static/js/graph_state.mjs` (or a tiny `legacy_graph_build.mjs` if you want isolation):
+
+```javascript
+export function buildModuleMapGraph(modules = []) {
+ const nodes = modules.map((m) => ({
+ id: m.name,
+ label: `${shortGraphLabel(m.name)}\n(${m.provider_count || 0}p/${m.object_count || 0}o)`,
+ shape: 'box',
+ data: { type: 'module', module: m, packagePath: m.name },
+ }));
+ const edges = [];
+ for (const m of modules) {
+ for (const dep of m.depends_on || []) {
+ edges.push({ from: m.name, to: dep, arrows: 'to' });
+ }
+ }
+ return { nodes, edges };
+}
+```
+
+Test in `graph_view.test.mjs`:
+
+```javascript
+test("buildModuleMapGraph uses modules not objects as nodes", async () => {
+ const { buildModuleMapGraph } = await import("./graph_state.mjs");
+ const { nodes, edges } = buildModuleMapGraph([
+ { name: "app/a", provider_count: 2, object_count: 5, depends_on: ["app/b"] },
+ { name: "app/b", provider_count: 1, object_count: 1, depends_on: [] },
+ ]);
+ assert.equal(nodes.length, 2);
+ assert.equal(edges.length, 1);
+ assert.equal(nodes[0].data.type, "module");
+});
+```
+
+- [ ] **Step 2: Run test — expect FAIL then implement — expect PASS**
+
+Run: `node --test dixhttp/static/js/graph_view.test.mjs --test-name-pattern buildModuleMapGraph`
+
+- [ ] **Step 3: Add toolbar button**
+
+In `template.html` next to Providers / Types:
+
+```html
+
+```
+
+- [ ] **Step 4: Implement `renderModulesGraph` path in `app.js`**
+
+- Add `modulesData: null` to state.
+- In `switchView('modules')`, fetch `/api/modules` if needed, set `currentView`, call `renderGraph`.
+- At start of `renderGraph`, if `currentView === 'modules'`:
+
+```javascript
+if (this.currentView === 'modules') {
+ const built = window.DIXGraphState.buildModuleMapGraph(this.modulesData || []);
+ // apply prefix filter if set; skip provider/type construction
+ // then aggregateByGroups only if meaningful for modules; apply budget; draw with star layout
+ ...
+ return;
+}
+```
+
+Use `layoutStarPositions` to set `x`/`y` on nodes when `modules` view and `nodes.length >= 2`. Disable physics for that draw (match `/next` star behavior).
+
+- [ ] **Step 5: Double-click module → set `filterPrefix` to module name and `switchView('providers')`**
+
+```javascript
+if (node.data.type === 'module') {
+ this.filterPrefix = node.data.module.name;
+ this.switchView('providers');
+}
+```
+
+- [ ] **Step 6: Manual check on example app**
+
+Module map shows ~10 boxes with clear separation; double-click drills to providers scoped by prefix.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add dixhttp/template.html dixhttp/static/js/legacy/app.js dixhttp/static/js/graph_state.mjs dixhttp/static/js/graph_view.test.mjs
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): add module map lens to legacy graph UI
+
+EOF
+)"
+```
+
+---
+
+### Task 4: Density banner, hubs, and Top-K last
+
+**Files:**
+- Modify: `dixhttp/template.html` (banner container above `#network`)
+- Modify: `dixhttp/static/js/legacy/app.js` (`renderGraph` post-filter pipeline)
+- Test: existing `applyGraphBudget` / `rankHubNodes` tests
+
+**Interfaces:**
+- Consumes: `applyGraphBudget(nodes, edges, { nodes, edges })`, `rankHubNodes(nodes, edges, limit)`, `READABLE_NODE_CAP`
+- Produces: `densityWarning` state `{ show, message, hubs[], suggestModules, suggestAggregate }`
+
+- [ ] **Step 1: Add banner markup**
+
+Above the network canvas in `template.html`:
+
+```html
+
+
+
+
+
+
+
+ | 耦合枢纽 | 度数 |
+
+
+ |
+
+ |
+ |
+
+
+
+
+```
+
+Initialize `densityWarning: { show: false, message: '', hubs: [], suggestModules: false, suggestAggregate: false }`.
+
+- [ ] **Step 2: Insert pipeline after aggregate + prefix, before `vis.Network`**
+
+Order must be:
+
+1. Build nodes/edges for current view
+2. `aggregateByGroups`
+3. `filterByPrefix`
+4. If `nodes.length > READABLE_NODE_CAP` (or budget): set banner (`suggestModules` if view is providers/types; `suggestAggregate` if `!aggregateGroups && groupRules.length`)
+5. `rankHubNodes(..., 8)` into banner
+6. `applyGraphBudget(..., { nodes: READABLE_NODE_CAP, edges: READABLE_NODE_CAP * 3 })` **last**
+7. Create network from bounded nodes
+
+```javascript
+const helpers = this.graphHelpers();
+const cap = helpers.READABLE_NODE_CAP;
+let ns = filteredByPrefix.nodes;
+let es = filteredByPrefix.edges;
+const over = ns.length > cap || es.length > cap * 3;
+this.densityWarning = {
+ show: over,
+ message: over
+ ? `图规模过大(${ns.length} 节点 / ${es.length} 边)。建议先看模块地图或按分组聚合审查组织;下列为耦合枢纽。`
+ : '',
+ hubs: over ? helpers.rankHubNodes(ns, es, 8) : [],
+ suggestModules: over && this.currentView !== 'modules',
+ suggestAggregate: over && !this.aggregateGroups && (this.groupRules || []).length > 0,
+};
+if (over) {
+ const bounded = helpers.applyGraphBudget(ns, es, { nodes: cap, edges: cap * 3 });
+ ns = bounded.nodes;
+ es = bounded.edges;
+}
+```
+
+- [ ] **Step 3: Run unit tests**
+
+Run: `node --test dixhttp/static/js/graph_view.test.mjs`
+Expected: all PASS
+
+- [ ] **Step 4: Manual check**
+
+Providers view on example app shows amber banner + hubs when over cap; clicking「模块地图」switches without losing package sidebar tools.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add dixhttp/template.html dixhttp/static/js/legacy/app.js
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): density banner and hub-first budget on legacy graph
+
+EOF
+)"
+```
+
+---
+
+### Task 5: Readable layout/camera for large legacy graphs
+
+**Files:**
+- Modify: `dixhttp/static/js/legacy/app.js` (`getNetworkOptions`, post-create camera)
+- Test: existing `resolveEffectiveLayout` / `resolveCameraStrategy` / `layoutStarPositions` tests
+
+**Interfaces:**
+- Consumes: `resolveEffectiveLayout(preferred, nodeCount)`, `resolveCameraStrategy(mode, nodeCount)`, `pickFocusNodeId`
+
+- [ ] **Step 1: Map legacy layout select to helper**
+
+Legacy uses `currentLayout: 'hierarchical' | 'force'`. Map:
+
+```javascript
+const preferred = this.currentLayout === 'force' ? 'physics' : 'hierarchical';
+const effective = helpers.resolveEffectiveLayout(preferred, nodeCount);
+```
+
+When `effective === 'physics'`, use forceAtlas-style options already used for `force`; when hierarchical and over cap, helper returns physics — honor it even if select says hierarchical (banner can note「已自动改用分散布局」).
+
+- [ ] **Step 2: Camera after draw**
+
+```javascript
+const mode = this.currentView === 'modules' ? 'modules' : 'providers';
+const camera = helpers.resolveCameraStrategy(mode, ns.length);
+const focusId = helpers.pickFocusNodeId(ns, es, this.filterPrefix || '');
+if (camera === 'fit' || this.currentView === 'modules') {
+ this.network.fit({ animation: false, padding: 48 });
+} else if (focusId) {
+ this.network.focus(focusId, { scale: 1.1, animation: false });
+}
+```
+
+For modules + star positions, physics off (Task 3).
+
+- [ ] **Step 3: Manual check**
+
+Large Providers graph is not a single vertical bead-string; module map fits in view.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add dixhttp/static/js/legacy/app.js
+git commit -m "$(cat <<'EOF'
+feat(dixhttp): apply readable layout and camera on dense legacy graphs
+
+EOF
+)"
+```
+
+---
+
+### Task 6: Package sidebar as architectural scope (copy + behavior check)
+
+**Files:**
+- Modify: `dixhttp/template.html` (sidebar heading/placeholder text)
+- Modify: `dixhttp/static/js/legacy/app.js` only if click handler does not already set prefix + redraw
+
+**Interfaces:**
+- Produces: unchanged filtering semantics; clearer UX framing
+
+- [ ] **Step 1: Update sidebar copy**
+
+Change labels to emphasize scope, e.g. heading「包范围」and placeholder「选择包以缩小架构切片…」.
+
+- [ ] **Step 2: Verify click sets `filterPrefix` and calls `renderGraph`**
+
+If existing handler only highlights, align to: set prefix, keep current view, redraw (existing behavior preferred).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add dixhttp/template.html dixhttp/static/js/legacy/app.js
+git commit -m "$(cat <<'EOF'
+docs(dixhttp): frame package sidebar as architecture scope
+
+EOF
+)"
+```
+
+---
+
+### Task 7: Delete `/next` and retarget tests/docs
+
+**Files:**
+- Modify: `dixhttp/server.go` (remove route + `HandleNextIndex`)
+- Modify: `dixhttp/http_scale_e2e_test.go`
+- Modify: `dixhttp/scripts/graph_layout_e2e.mjs`
+- Modify: `dixhttp/README.md`, `dixhttp/README_zh.md`
+- Delete: `dixhttp/static/index.html`, `dixhttp/static/js/main.js`, `dixhttp/static/js/api.js`, `dixhttp/static/js/views/overview.js`, `graph.js`, `search.js`, `trace.js`, `diag.js` (all next-only)
+- Modify: `dixhttp/static/js/graph_view.test.mjs` — remove tests that only exist for next hash routing (`issueGraphHash` may stay if still used by legacy later; if unused, keep helpers but drop next-only assertions that require five-view hashes, or keep helpers for future Trace jumps)
+- Modify: `docs/superpowers/specs/2026-09-05-dependency-visualization-unified-design.md` — add status line `Superseded by 2026-09-06-legacy-first-architecture-viz-design.md`
+
+**Interfaces:**
+- Produces: `GET /next` → 404; `GET /` includes `graph_state.mjs` and module map button markup
+
+- [ ] **Step 1: Rewrite Go e2e test**
+
+```go
+func TestLegacyGraphEmbedsSharedHelpers(t *testing.T) {
+ container := dix.New()
+ server := NewServer(container)
+ recorder := httptest.NewRecorder()
+ server.ServeHTTP(recorder, httptest.NewRequest("GET", "/", nil))
+ if recorder.Code != 200 {
+ t.Fatalf("status = %d", recorder.Code)
+ }
+ body := recorder.Body.String()
+ for _, asset := range []string{"static/js/graph_state.mjs", "static/js/legacy/app.js", "模块地图"} {
+ if !strings.Contains(body, asset) {
+ t.Fatalf("missing %s", asset)
+ }
+ }
+ if strings.Contains(body, "static/js/views/graph.js") {
+ t.Fatalf("legacy index must not reference next graph.js")
+ }
+}
+```
+
+- [ ] **Step 2: Run test — FAIL (next still present / assertion)** then remove `/next` handler and delete next static files — PASS
+
+Also add a quick test that `/next` returns 404 if desired:
+
+```go
+server.ServeHTTP(rec, httptest.NewRequest("GET", "/next", nil))
+if rec.Code != 404 { t.Fatalf(...) }
+```
+
+- [ ] **Step 3: Update `graph_layout_e2e.mjs`**
+
+Health check `fetch(`${BASE}/`)` and assert `graph_state.mjs` + `legacy/app.js` in HTML (not `graph.js`).
+
+- [ ] **Step 4: Update README / README_zh**
+
+State clearly: visualization UI is `/` only; remove “alongside `/next`” and next workbench bullets; document module map + density banner briefly.
+
+- [ ] **Step 5: Run full verification**
+
+```bash
+node --test dixhttp/static/js/graph_view.test.mjs
+go test -race ./dixhttp/...
+```
+
+Expected: PASS
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A dixhttp docs/superpowers/specs
+git commit -m "$(cat <<'EOF'
+chore(dixhttp): remove /next shell; legacy UI is sole entry
+
+EOF
+)"
+```
+
+---
+
+### Task 8: End-to-end acceptance against the spec
+
+**Files:** none new (verification only)
+
+- [ ] **Step 1: Run demo**
+
+`DIX_HTTP_ADDR=127.0.0.1:18099 go run -C example ./http`
+
+- [ ] **Step 2: Checklist**
+
+1. `/next` 404
+2. `/` loads; Providers default
+3. 模块地图 clear boundaries
+4. Dense Providers shows banner + hubs; one-click to modules / aggregate
+5. Group rules, prefix, SVG, Mermaid, Trace modal still work
+6. No object-only nodes on canvas
+
+- [ ] **Step 3: Commit only if doc tweaks needed**
+
+```bash
+git commit -m "$(cat <<'EOF'
+docs(dixhttp): note legacy-first acceptance for architecture viz
+
+EOF
+)"
+```
+
+---
+
+## Spec coverage self-check
+
+| Spec requirement | Task |
+|---|---|
+| Single UI `/`, delete `/next` | 7 |
+| Preserve legacy capabilities | 3–6 (additive), 8 checklist |
+| Four lenses (module/provider/type/group) | 3 + existing providers/types + aggregate |
+| Crowding order (prompt → aggregate → hubs → labels → layout → Top-K) | 2,4,5 |
+| Objects not default canvas nodes | 3 builder + 8 |
+| Port helpers from next | 1–5 |
+| API stable / no new deps | Global + all tasks |
+| Acceptance example-scale | 8 |
+
+## Placeholder scan
+
+No TBD steps; commands and code sketches included for each task.
diff --git a/docs/superpowers/plans/2026-09-06-provider-panorama-zoom-lod.md b/docs/superpowers/plans/2026-09-06-provider-panorama-zoom-lod.md
new file mode 100644
index 0000000..3755c66
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-06-provider-panorama-zoom-lod.md
@@ -0,0 +1,93 @@
+# Provider Panorama + Zoom LOD Implementation Plan
+
+> **Status: CANCELLED (2026-09-07)** — 全景 layout removed from product; do not continue this plan.
+
+**Goal (historical):** Add an explicit Providers「全景」layout that places every provider on a 2D package map, with same-canvas zoom LOD (structure far / names near), without hijacking 层级/力导向.
+
+**Architecture:** Pure helpers in `graph_state.mjs` compute panorama `{x,y}` and LOD label maps; legacy `renderGraph` applies them when `currentLayout === 'panorama'`. Hierarchical and force paths stay unchanged. Panorama disables vis hierarchical + physics and does not apply Top-K budget.
+
+**Tech Stack:** Alpine legacy UI, vis-network, shared ESM helpers + `node --test`.
+
+**Spec:** `docs/superpowers/specs/2026-09-06-provider-panorama-zoom-lod-design.md`
+
+## Global Constraints
+
+- Panorama path: **all** providers in scope stay on canvas (no Top-K).
+- Never silently swap 层级 ↔ 力导向 ↔ 全景.
+- LOD changes labels only, never removes nodes/edges.
+- Providers global default layout: **全景**.
+- Cache bump static assets after UI wiring.
+
+## File map
+
+| File | Responsibility |
+|---|---|
+| `dixhttp/static/js/graph_state.mjs` | `layoutPanoramaPositions`, `resolveLodBand`, `labelLodByBand` |
+| `dixhttp/static/js/graph_view.test.mjs` | Unit tests for helpers |
+| `dixhttp/static/js/legacy/app.js` | Apply panorama in `renderGraph`; skip budget; camera; persist layout |
+| `dixhttp/template.html` | Layout `
`; tip copy |
+
+---
+
+### Task 1: Panorama positions helper (TDD)
+
+**Files:**
+- Modify: `dixhttp/static/js/graph_state.mjs`
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Produces: `layoutPanoramaPositions(nodes, opts?) → { [id]: { x, y } }`
+- Uses: `nodePackageKey(node)` for grouping; deterministic sort by package then id
+
+- [x] **Step 1: Write failing tests** for multi-package grid, deterministic coords, single-package stack
+- [x] **Step 2: Run tests — expect FAIL**
+- [x] **Step 3: Implement `layoutPanoramaPositions`**
+- [x] **Step 4: Run tests — expect PASS**
+
+---
+
+### Task 2: Zoom LOD bands (TDD)
+
+**Files:**
+- Modify: `dixhttp/static/js/graph_state.mjs`
+- Test: `dixhttp/static/js/graph_view.test.mjs`
+
+**Interfaces:**
+- Produces: `resolveLodBand(scale) → 'overview' | 'mid' | 'detail'`
+- Produces: `labelLodByBand(nodes, edges, scale, opts?) → Map
` (overview: package short name on one rep per package; mid: hubs+reps; detail: all short labels)
+
+- [x] **Step 1: Write failing tests** for bands and overview package reps
+- [x] **Step 2: Implement helpers; export on `window.DIXGraphState`**
+- [x] **Step 3: Tests PASS**
+
+---
+
+### Task 3: Wire panorama into legacy Providers render
+
+**Files:**
+- Modify: `dixhttp/static/js/legacy/app.js`
+- Modify: `dixhttp/template.html`
+
+- [x] Default `currentLayout: 'panorama'`
+- [x] Add layout option 全景 in template
+- [x] In `renderGraph` when layout is panorama (providers/modules-compatible nodes): skip `applyGraphBudget`; compute positions; `getNetworkOptions` with hierarchical off + physics off; assign x/y/fixed; use `labelLodByBand` in `applyLabelLod`
+- [x] Persist `currentLayout` in localStorage with other prefs
+- [x] Density tip for hierarchical+wide: suggest 全景 (do not auto-switch)
+- [x] Panorama tip: “缩放过小看结构;放大读 Provider 名”
+- [x] Bump `?v=legacy-arch8`
+
+- [x] **Verify:** `node --test dixhttp/static/js/graph_view.test.mjs` && `go test ./dixhttp/...`
+
+---
+
+### Task 4: Smoke acceptance
+
+- [ ] Manual: global Providers + 全景 → 2D package clusters, all nodes present
+- [ ] Zoom out → package labels; zoom in → provider names
+- [ ] Switch to 层级 → hierarchical; back to 全景 → panorama again
+
+---
+
+## Done when
+
+Acceptance criteria in the spec §Acceptance all hold; helpers covered by unit tests.
diff --git a/docs/superpowers/plans/2026-09-06-structure-panorama-inventory.md b/docs/superpowers/plans/2026-09-06-structure-panorama-inventory.md
new file mode 100644
index 0000000..345c3e8
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-06-structure-panorama-inventory.md
@@ -0,0 +1,28 @@
+# Structure Panorama + Inventory (Approach C) Plan
+
+> **Status: CANCELLED (2026-09-07)** — 全景 removed; keep Provider inventory + package scope / 模块地图 instead.
+
+**Goal (historical):** Replace “literal all-provider panorama” with dual channels: readable **package-structure graph** + complete **provider inventory**.
+
+**Architecture:** Panorama with no package scope draws package nodes and cross-package edges from `allData`. Right rail always lists every provider (or scoped list). Click package → `selectPackage`; click inventory row → focus/detail. Hierarchical/force unchanged.
+
+**Tech Stack:** Alpine legacy UI, `graph_state.mjs`, node tests.
+
+**Spec amend:** `docs/superpowers/specs/2026-09-06-provider-panorama-zoom-lod-design.md` → Approach C.
+
+---
+
+### Task 1: `buildPanoramaStructureGraph` + inventory helper (TDD)
+
+- [ ] Tests for package nodes, cross-pkg edges, inventory completeness
+- [ ] Implement helpers; export on window
+
+### Task 2: Wire legacy render + sidebar
+
+- [ ] Panorama global → structure graph; panorama + package → provider map
+- [ ] Always show inventory; double-click package drills scope
+- [ ] Simplify panorama tips; bump `legacy-arch13`
+
+### Task 3: Verify
+
+- [ ] `node --test` + `go test ./dixhttp/...`
diff --git a/docs/superpowers/plans/2026-09-07-http-example-microservice-layout.md b/docs/superpowers/plans/2026-09-07-http-example-microservice-layout.md
new file mode 100644
index 0000000..8b2a98e
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-07-http-example-microservice-layout.md
@@ -0,0 +1,178 @@
+# HTTP Example Microservice Layout Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Restructure `example/http` into bootstrap + app + router + infra + plugins(interface/impls) + domain vertical slices with per-package Provide.
+
+**Architecture:** Each leaf package owns types + `Provide(*dix.Dix)`. `bootstrap.Build` is the only assembler. Plugins expose interfaces; impls contribute via `map[string]T` namespaces.
+
+**Tech Stack:** Go, dix/v2, dixhttp, existing `example/http` tests.
+
+## Global Constraints
+
+- No domain-root `provide.go` / `Providers()` facade
+- Provide only inside owning packages
+- Three pyramid entries: Application, ScaleFixture, TimeoutProbe
+- ≥190 providers, ≥10 modules
+- Spec: `docs/superpowers/specs/2026-09-07-http-example-microservice-layout-design.md`
+
+---
+
+### Task 1: Domain layer packages (10 domains × 5 layers)
+
+**Files:**
+- Create: `example/http/domain//{models,infra,logic,service,handler}/*.go` for each of: analytics, billing, identity, inventory, media, notification, searchx, shipping, storage, workflow
+- Delete: `example/http/domain//.go`
+
+**Interfaces:**
+- Produces per domain: `Config`, `Client`, `Regions`, `Repo`, `Service`, `Handler` with same dependency chain as today’s flat package; each file exports `func Provide(di *dix.Dix)`.
+
+- [ ] **Step 1: Generate domain packages**
+
+Use a generator (inline Go or shell) so every domain matches billing:
+
+```go
+// domain/billing/models/config.go
+package models
+
+import "github.com/pubgo/dix/v2"
+
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
+```
+
+```go
+// domain/billing/infra/client.go
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+ "github.com/pubgo/dix/example/http/domain/billing/models"
+)
+
+type Client struct{ Config *models.Config }
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client { return &Client{Config: c} })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{"cn": {Config: c}, "us": {Config: c}, "eu": {Config: c}}
+ })
+}
+```
+
+```go
+// domain/billing/logic/repo.go
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+ "github.com/pubgo/dix/example/http/domain/billing/infra"
+)
+
+type Repo struct{ Client *infra.Client }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo { return &Repo{Client: c} })
+}
+```
+
+```go
+// domain/billing/service/service.go
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+ "github.com/pubgo/dix/example/http/domain/billing/logic"
+)
+
+type Service struct{ Repo *logic.Repo }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service { return &Service{Repo: r} })
+}
+```
+
+```go
+// domain/billing/handler/handler.go
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+ binfra "github.com/pubgo/dix/example/http/domain/billing/infra"
+ "github.com/pubgo/dix/example/http/domain/billing/service"
+)
+
+type Handler struct{ Service *service.Service }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions binfra.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
+```
+
+Repeat for all 10 domains (only import path / comments change). Delete old flat files.
+
+- [ ] **Step 2: Compile-check domains**
+
+Run: `cd example/http && go build ./domain/...`
+Expected: success
+
+---
+
+### Task 2: infra + plugins + app + router
+
+**Files:**
+- Create: `example/http/infra/logger/logger.go`
+- Create: `example/http/infra/diag/diag.go`
+- Create: `example/http/infra/scale/scale.go` (enough distinct Provide calls to keep ≥190 total)
+- Create: `example/http/plugins/plugin.go` (interfaces + Platform Provide)
+- Create: `example/http/plugins//*.go` for ~20 impls (auth, billing, cache, … vault) each providing map namespaces
+- Create: `example/http/app/application.go`
+- Create: `example/http/router/server.go`
+
+**Interfaces:**
+- `plugins.Plugin` with `Name() string`; `plugins.Worker` with `Name() string`; `plugins.Platform` with `Names []string`
+- Each impl: `Provide` returns `map[string]plugins.Plugin` and `map[string]plugins.Worker` depending on Plugin map entry
+- Platform Provide consumes `map[string]plugins.Worker`
+
+- [ ] **Step 1: Implement logger, diag, scale, plugins, app, router as above**
+
+Scale: register ≥100 tiny named provider types OR ≥100 single-key map contributions via separate `Provide` funcs so total container providers ≥190 after domains+plugins.
+
+- [ ] **Step 2: `go build ./infra/... ./plugins/... ./app/... ./router/...`**
+
+Expected: success
+
+---
+
+### Task 3: bootstrap + thin main + tests
+
+**Files:**
+- Create: `example/http/bootstrap/container.go`, `run.go`
+- Modify: `example/http/main.go` (thin)
+- Delete: `example/http/plugins.go`
+- Modify: `example/http/main_test.go`, `scale_shape_test.go`
+
+**Interfaces:**
+- `bootstrap.Build() *dix.Dix`
+- `bootstrap.Run() error`
+- Tests call `bootstrap.Build()` or `main` wrapper `buildContainer()` → `bootstrap.Build()`
+
+- [ ] **Step 1: Wire bootstrap.Build calling all Provide in order**
+- [ ] **Step 2: Update tests** — plugin count = `len(platform.Names)` expected (2 × impl count if plugin+worker names); pyramid still 3 entries
+- [ ] **Step 3: `go test ./example/http/...`**
+
+Expected: PASS
+
+- [ ] **Step 4: Commit** (only if user asks)
diff --git a/docs/superpowers/plans/2026-09-08-architecture-ui-polish-roadmap.md b/docs/superpowers/plans/2026-09-08-architecture-ui-polish-roadmap.md
new file mode 100644
index 0000000..5914604
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-08-architecture-ui-polish-roadmap.md
@@ -0,0 +1,127 @@
+# Architecture UI Polish Roadmap
+
+**Date:** 2026-09-08
+**Status:** Living roadmap (not a single sprint)
+**Scope:** Legacy `/` Providers · Types · Module map — readability, hide/filter, pyramid depth UX
+**Related:** `docs/superpowers/specs/2026-09-08-hide-node-downstream-design.md`
+
+## Principles
+
+- Ship small vertical slices; each phase should be demoable in the http example.
+- Prefer canvas/session UX over server API changes unless data identity is wrong.
+- Do not block unrelated dix/core work on this roadmap.
+
+---
+
+## Phase 0 — Done (baseline)
+
+| Item | Notes |
+|------|--------|
+| Hide node + downstream | Right-click / Shift+click / toolbar / inventory |
+| Session-shared hide seeds | Providers · Types · Module map |
+| Toast + undo / restore list | |
+| Label disambiguation | `billing/handler.Handler`, `auth.Worker` |
+| Pkg-aware provider edges | Avoid false cross-domain fan-out |
+| Microservice example layout | domain layers + bootstrap + plugins |
+
+---
+
+## Phase 1 — Clarity & fewer surprises (near-term)
+
+**Goal:** Users understand what the graph is showing without asking “为什么只有 N 级 / 藏错了吗”.
+**Target window:** next 1–2 focused sessions when touching dixhttp UI.
+
+| # | Item | Why | Acceptance |
+|---|------|-----|------------|
+| 1.1 | [x] Depth select shows **actual max level** (disable or omit empty 5/10) | Stops “深度 10 只有 4 级” confusion | Depth options ≤ computed max; label e.g. `全部 (4 级)` |
+| 1.2 | [x] Hide **preview highlight** before commit (context menu open) | Prevents accidental truncation | Downstream nodes outline while menu open; cancel clears |
+| 1.3 | [x] Auto-fit after hide / restore | Graph doesn’t stay zoomed on empty space | After hide/unhide, camera fit once |
+| 1.4 | [x] Inventory marks hidden rows | Completeness channel stays honest | Hidden providers grey +「已隐」; 复 restores |
+
+**Exit:** Phase 1 checklist green on `example/http` demo; one short note in changelog / commit message.
+
+---
+
+## Phase 2 — Faster “main graph” workflows (next)
+
+**Goal:** Reach a clean business-only pyramid in ≤2 actions.
+
+| # | Item | Why | Acceptance |
+|---|------|-----|------------|
+| 2.1 | [x] Context menu: **hide package / subtree** (e.g. `…/plugins`) | Scale fixtures & plugin forests | One action hides all matching packagePath prefix + downstream |
+| 2.2 | [x] Preset: **只留业务入口** (hide Dix + diag Timeout chain + plugins) | Default noise off the tip | One toolbar action; reversible via 已隐藏 |
+| 2.3 | [x] Keyboard: `H` hide selected, `U` undo last, `Esc` close menu | Power users | Hint on context menu |
+| 2.4 | [x] Module map right-click = same menu as Providers | Consistency | Module containment / package hide works via shared bindGraphInteractions |
+
+**Exit:** Can demo “hide plugins + diag → clean Application chain” in under 10 seconds.
+
+---
+
+## Phase 3 — Shareable & durable state (later)
+
+**Goal:** Same filtered view across reloads / teammates without teaching click paths.
+
+| # | Item | Why | Acceptance |
+|---|------|-----|------------|
+| 3.1 | [x] URL sync for hide seeds (+ optional depth/package) | Share “the graph I’m looking at” | Reload restores; link opens same cut |
+| 3.2 | [x] Hide history stack (multi-undo) | Continuous trimming sessions | Undo >1 step; history capped (e.g. 20) |
+| 3.3 | [x] Toast stacking / placement vs density tip | Less UI collision | Density tip left, hide toast right; no overlap |
+
+**Exit:** Shared URL reproduces hide set; multi-undo feels safe.
+
+---
+
+## Phase 4 — Structural / data (opportunistic)
+
+**Goal:** Fix root causes of crowding, not only filters.
+
+| # | Item | Why | When |
+|---|------|-----|------|
+| 4.1 | [x] Stronger type identity in API if still collapsing | Fewer bogus edges | `input_pkgs` + pkg\\x00type dedupe in GetProviderDetails / aggregate / graph edges |
+| 4.2 | [x] Example scale via real domains, not pads | Meaningful module map | `example/http` microservice layout (no ScaleFixture) |
+| 4.3 | [x] Soft guidance when entry count > N | Onboarding | Density tip CTA「只留业务入口」when entries > 12, unscoped |
+
+---
+
+## Suggested cadence
+
+| Cadence | Focus |
+|---------|--------|
+| **Whenever shipping a dixhttp UI fix** | Prefer pulling **one** Phase 1 item into the same PR |
+| **Dedicated polish half-day** | Finish remaining Phase 1, start 2.1–2.2 |
+| **After Phase 2 feels good** | Phase 3 URL/history |
+| **Opportunistic** | Phase 4 with related core/example work |
+
+Do **not** schedule Phase 3–4 as blockers for other features.
+
+---
+
+## Tracking
+
+- Keep this file as the queue; check items off in place when done (`[x]`).
+- New ideas: append under the right phase or a `## Backlog` section below — don’t start a parallel doc unless scope splits.
+
+## Backlog (unsorted)
+
+- [x] Merge consecutive hide toasts into one line
+- [x] “Hide everything except selected neighborhood” invert mode
+- [x] Persist hide seeds beyond session (opt-in localStorage)
+- [x] Further canvas LOD / label density tweaks after layout-v1 bake-in
+
+---
+
+## Layout chrome (2026-09-08)
+
+Bold IA pass (`arch-layout-v1` / `legacy-arch29`+): compact header, primary toolbar +「更多」, grouped package list, right inspector tabs, density tip only when actionable, no floating Trace FAB.
+
+Focus neighborhood polish (`legacy-arch32`): yellow/green/red role coloring, status「聚焦邻域」,「退出聚焦」chip.
+
+Camera chrome (`legacy-arch35`): floating +/−/fit/focus/maximize, keyboard shortcuts, graph-stage fullscreen.
+
+Usability batch (`legacy-arch36`–`37`): `?` shortcuts help, arrow pan, immersive maximize, scope chips, copy label, opt-in persist hide, chrome prefs, denser LOD, remember last view.
+
+Architecture findings (`legacy-arch38`): default **体检** home — `buildArchitectureFindings` (cross_bucket / super_hub / entry_fanout / fat_package); click → scoped graph; chip back.
+
+## Next concrete pick
+
+Bake-in findings with example/http; tune thresholds; optionally CLI/CI export later.
diff --git a/docs/superpowers/specs/2026-09-05-dependency-visualization-unified-design.md b/docs/superpowers/specs/2026-09-05-dependency-visualization-unified-design.md
new file mode 100644
index 0000000..fd20e75
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-05-dependency-visualization-unified-design.md
@@ -0,0 +1,178 @@
+# Dependency Visualization Unified Design
+
+> Status: superseded by `2026-09-06-legacy-first-architecture-viz-design.md`
+> Approach was: evolve `/next` five-view shell — product direction moved to legacy-first.
+> Date: 2026-09-05 · Branch context: `codex/scale-graph-usability`
+> Related: `docs/superpowers/specs/2026-09-04-graph-trace-redesign-design.md`,
+> `docs/superpowers/specs/2026-09-05-scale-graph-usability-design.md`
+
+## Problem
+
+Dix containers at approximately 100 modules, 200 providers, and 400 objects make a single global dependency graph unreadable. Two product jobs fail together today:
+
+1. **Structure readability** — too many providers/objects; dense canvases are hard to browse and reason about.
+2. **Failure diagnosis** — when `Provide` / `Inject` misbehaves, call trees, errors, and graph context are not one continuous path.
+
+Identity and module-first work on `codex/scale-graph-usability` fixed false edges and defaulted `/next#/graph` to a module map, but the issue → graph → trace loop and request-cancellation polish are incomplete. This design unifies both jobs without replacing the existing five-view shell or breaking legacy surfaces.
+
+## Goals
+
+1. Keep the default graph always small and actionable (module map → module drill-down → ego neighborhood).
+2. Treat objects as state and detail by default, not as required graph nodes.
+3. Make diagnostics navigable: from an issue to a bounded graph or trace tree in at most three interactions.
+4. Correlate runtime metrics, issues, graph nodes, and traces by stable `provider_id`.
+5. Preserve existing public Dix APIs, legacy `/` UI, and existing `/api/*` field compatibility (additive only).
+
+## Non-Goals
+
+- Replace `vis-network` or introduce WebGL.
+- Remove legacy `/` UI or five-view `/next` tabs.
+- Draw every object as a default graph node.
+- Rebuild the shell as a single three-pane workspace (deferred alternative).
+- Introduce a parallel full `/api/graph/*` namespace in this iteration (reuse `/api/modules`, `/api/module`, `/api/ego`, `/api/issues`).
+
+## Product Approach
+
+**Chosen:** evolve the existing `/next` five-view information architecture into one workflow:
+
+`Issue → locate → bounded graph → call tree → runtime detail`
+
+Rejected for this iteration:
+
+- **Issue Hub default** — shortest debug path, but weak for structure-first browsing.
+- **Single three-pane shell** — best context continuity, highest breakage and layout cost.
+
+## Information Architecture
+
+| View | Primary question | Default content |
+|---|---|---|
+| Overview | Is the system healthy? | Stats cards + Issues feed + jump actions |
+| Graph | How do dependencies connect? | Module map → module drill-down → type ego; full providers/types only as advanced modes |
+| Search | Where is the target? | Server search; hits jump to ego/module or detail |
+| Trace | How did this inject run? | Trace list (failures first) + TraceTree; prefilter from issue/graph |
+| Diag | What is slow or wrong at startup? | Runtime stats, error tables, error-type help |
+
+### Path A — Structure (anti-density)
+
+1. Open Graph → module map.
+2. Select module → bounded in-module topology (providers/types; objects not default nodes).
+3. Select type → ego neighborhood (depth default 2, max 5).
+4. Open global providers/types only as advanced mode, with an explicit density warning.
+
+### Path B — Diagnosis (anti-fragmentation)
+
+1. Overview Issues (or Diag) shows error/slow items.
+2. One click to bounded graph (ego or module).
+3. One click to Trace tree (prefiltered by provider/output; open tree directly when `trace_id` exists).
+4. Graph node drawer exposes declared deps, runtime state, and jump-to-trace.
+
+## Bounded Views and Density Policy
+
+| View | Node cap | Edge cap | Source |
+|---|---|---|---|
+| Module map | 100 | 300 | `/api/modules` |
+| Module drill-down | 150 | 400 | `/api/module` |
+| Ego | 100 | 300 | `/api/ego` |
+| Global providers/types | 150 | 400 | `/api/dependencies` (advanced only) |
+
+Degradation order:
+
+1. Server truncates with `limit` / `edge_limit` and returns truncation/degraded metadata.
+2. Client applies degree Top-K budget (`applyGraphBudget`).
+3. UI shows an explicit density warning with current N/M caps.
+4. Same screen offers table / Top-K hubs / narrower scope (module, depth, prefix).
+5. Objects stay in drawers and diagnostic tables unless the user deliberately drills to instance detail later.
+
+Correctness rules:
+
+1. Distinct registrations keep distinct identities even when they share a source line.
+2. Struct multi-output registrations may share one registration identity.
+3. Edges are real declared relationships; never synthesize `inputs × outputs` after aggregation.
+4. Ego “instantiated” is true only when an object node exists.
+5. A graph may hide or degrade; it must not silently show false relationships.
+6. In-flight graph/issues/trace requests cancel when the view or hash changes.
+
+Canvas rules:
+
+- Preserve a usable minimum graph height.
+- Narrow layouts move details into a drawer/bottom sheet instead of shrinking the canvas.
+- Encode `mode` / `module` / `center` / `depth` (and related filters) in the hash query.
+
+## Issue → Graph → Trace Contract
+
+### `/api/issues`
+
+Merge injection failures, provider failures, and slow providers into one actionable feed.
+
+Minimum fields per issue:
+
+- `severity` (`error` | `warn`, optional `info`)
+- `title`
+- `provider`
+- `provider_id` (when known)
+- `output_type`
+- `module`
+- `root_cause`
+- optional `trace_id`
+
+Navigation:
+
+- Graph button → `#/graph?mode=ego¢er=`, else `mode=module&module=...`, else `mode=providers&prefix=`
+- Trace button → `#/trace?provider=...&output_type=...&status=error|slow`, or direct tree when `trace_id` is present
+- Graph drawer “view resolve path” uses the same Trace prefilter rules
+
+Correlation key across issues, runtime-stats, graph nodes, and trace attributes: `provider_id`. Function name is display/fallback only.
+
+## API Contract
+
+Primary (enhance in place):
+
+- `GET /api/modules`
+- `GET /api/module?name=&limit=&edge_limit=`
+- `GET /api/ego?center=&depth=&direction=`
+- `GET /api/issues?limit=`
+- `GET /api/trace`, `GET /api/trace-tree?trace_id=`
+- `GET /api/search`, `/api/stats`, `/api/runtime-stats`, `/api/errors`
+
+Compatibility:
+
+- `GET /api/dependencies` remains available; `/next` default paths must not eagerly load it.
+- Legacy `/` UI and existing response fields remain; new fields are additive.
+- No new third-party Go or JS dependencies in this iteration.
+
+Deferred:
+
+- Full `/api/graph/{summary,modules,module,ego,node}` namespace rename/consolidation.
+
+## Compatibility Boundaries
+
+- Do not break public Dix APIs.
+- Do not remove legacy `/` UI.
+- Do not change existing `/api/*` field meanings; additive fields only.
+- `/next` defaults and navigation may evolve (module-first, issues entry, request cancellation).
+- Do not replace the rendering engine in this iteration.
+
+## Delivery Phases
+
+| Phase | Goal | Outcomes |
+|---|---|---|
+| P0 Stabilize | Finish half-done branch pieces | Reintroduce reliable request cancellation; align module budget to 150/400; lock Issues navigation contract with tests |
+| P1 Density | Structure readability | Default path avoids `/api/dependencies`; density warning + table/Top-K alternatives; narrow-viewport canvas floor |
+| P2 Diagnosis loop | Failure navigability | Complete Issues fields; graph drawer → Trace; Trace prefilters; ≤3-interaction acceptance cases |
+| P3 Docs & fixture | Regressible | Scale fixture walkthrough; README matches `/next` defaults |
+
+## Acceptance
+
+1. At ~100 providers / hundreds of objects, the default module map stays interactive and labels remain readable.
+2. Module drill-down and ego respect budgets; over-budget views degrade explicitly with no false edges.
+3. Overview Issue → bounded graph in ≤2 clicks; → Trace tree in ≤3 clicks.
+4. Runtime errors/durations attach to the correct node via `provider_id`.
+5. `go test -race ./...` and `example/http` tests pass; legacy full-graph entry remains reachable.
+6. Documentation matches actual `/next` default behavior.
+
+## Success Signal
+
+A developer can complete both jobs on a large container without relying on an unreadable global graph:
+
+1. understand module-level dependencies and drill to relevant types;
+2. jump from a failure or slowdown to the call tree and related dependency context.
diff --git a/docs/superpowers/specs/2026-09-05-scale-graph-usability-design.md b/docs/superpowers/specs/2026-09-05-scale-graph-usability-design.md
new file mode 100644
index 0000000..142ffbd
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-05-scale-graph-usability-design.md
@@ -0,0 +1,81 @@
+# Scale-Safe Dependency Visualization Design
+
+## Problem
+
+The dependency UI is usable for small containers, but it does not scale to the target shape of approximately 100 modules, 200 providers, and 400 objects. The issue is not primarily browser capacity. The UI answers several different questions with one global graph, and derived provider data can multiply edges and collapse distinct provider instances.
+
+An example container with 10 business modules, 200 provider graph nodes, and 192 objects produced 72 aggregated providers and 3,860 edges from `/api/dependencies`. This happened because providers from the same generic factory source line were merged and their inputs and outputs were later cross-producted.
+
+## Goals
+
+1. Preserve provider identity so generic factories, multi-output providers, runtime metrics, errors, traces, and graph nodes can be correlated correctly.
+2. Make dependency edges reflect actual declared provider relationships instead of Cartesian products.
+3. Make the first graph experience module-centric, bounded, and actionable.
+4. Keep objects as state and detail rather than requiring every object to be a graph node.
+5. Guarantee bounded views: default module map, module drill-down, and ego graph each have explicit node and edge limits.
+6. Keep legacy endpoints and views compatible while the new model becomes the default.
+
+## Non-Goals
+
+This iteration does not replace `vis-network`, introduce WebGL, remove the legacy UI, or expose every object as a graph node. Those are follow-ups only if bounded module and ego views still fail usability targets.
+
+## Data Model
+
+Provider registration identity is the correlation key. A logical registration may produce multiple graph outputs for a struct provider, but two calls to the same generic factory are distinct registrations. Runtime state remains attached to the concrete provider/output pair.
+
+The visualization provider shape gains:
+
+- `registration_id`: stable identity for one logical provider registration within the current process.
+- `provider_id`: stable identity for the registration/output pair used by API clients.
+
+Dependency projections must group outputs by registration identity, not source file and line. The full projection must emit one declared edge per real input/output relationship; it must never synthesize all `inputs × outputs` relationships after aggregation.
+
+Module data uses `reflect.Type.PkgPath()`-derived module identity. Generic type names must not be parsed as package paths.
+
+## API Behavior
+
+Phase one evolves existing endpoints:
+
+- `/api/dependencies` uses registration-aware provider identity and emits real edges.
+- `/api/runtime-stats` includes registration and provider identities.
+- `/api/modules` remains the bounded module aggregate.
+- `/api/ego` reports instantiated state only when an object node exists.
+- `/api/stats` aggregates resolved counts by type and reports internally consistent module/type/provider counts.
+- `/api/packages` uses provider output package data and never displays malformed generic package names such as `main.Plugin[main`.
+
+The next major endpoint group will be `/api/graph/summary`, `/api/graph/modules`, `/api/graph/module`, `/api/graph/ego`, `/api/graph/node`, and `/api/issues`. Those endpoints should serve progressive disclosure and issue-centric debugging.
+
+## Frontend Behavior
+
+The default graph mode is module map. Selecting a module opens a bounded module view; selecting a type opens an ego graph; selecting a provider opens provider details and runtime state.
+
+Rendering budgets are explicit:
+
+- Module map: at most 100 nodes and 300 edges.
+- Module detail: at most 150 nodes and 400 edges.
+- Ego graph: default depth 2, maximum depth 5, and an edge cap.
+
+When data exceeds a budget, the UI shows a density warning and offers tables, Top-K hubs, or a narrower selection instead of rendering an unreadable graph.
+
+The graph canvas must retain a usable minimum size. Narrow layouts move details into a drawer or bottom sheet instead of shrinking the canvas to an unusable area.
+
+State and filters are encoded in the hash query so views can be refreshed and shared. Requests use cancellation when a view changes. Runtime metrics join by provider identity and output type, not function name alone.
+
+## Correctness Rules
+
+1. Two registrations from the same source line can have different identities.
+2. Outputs from one struct-producing registration can share a registration identity.
+3. Runtime stats are keyed by concrete provider/output, not only function name.
+4. An ego node is instantiated only when an object node exists.
+5. Package/module names must be valid package paths or `(anonymous)`.
+6. A graph may be hidden or degraded, but it must not silently show false relationships.
+
+## Acceptance
+
+- Main and example tests pass with race detection.
+- A generic provider fixture produces distinct provider identities and one edge per actual input/output pair.
+- Runtime stats correlate to the correct provider/output pair.
+- `/next#/graph` defaults to the module map and renders useful content without first loading `/api/dependencies`.
+- Module map respects the phase-one node/edge budgets.
+- Graph canvas remains usable at narrow viewports.
+- Diagnostics can navigate from an issue to the provider graph or trace tree in at most three interactions.
diff --git a/docs/superpowers/specs/2026-09-06-legacy-first-architecture-viz-design.md b/docs/superpowers/specs/2026-09-06-legacy-first-architecture-viz-design.md
new file mode 100644
index 0000000..50f4def
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-06-legacy-first-architecture-viz-design.md
@@ -0,0 +1,147 @@
+# Legacy-First DI Architecture Visualization Design
+
+> Status: approved for planning · Approach: enhance legacy `/`, delete `/next`
+> Date: 2026-09-06 · Branch context: `codex/scale-graph-usability`
+> Supersedes (for product direction): `2026-09-05-dependency-visualization-unified-design.md`
+> Related: `2026-09-05-scale-graph-usability-design.md`, `2026-09-04-graph-trace-redesign-design.md`
+
+## Problem
+
+Dix containers with tens to hundreds of providers make the legacy `/` canvas feel like an unreadable blob: hierarchical “bead strings,” long package labels, and mixed concerns on one graph.
+
+A parallel `/next` five-view shell added module maps, budgets, and layout helpers, but:
+
+1. Maintenance cost of two UIs is too high.
+2. `/next` did not replace the legacy workstation users still need.
+3. Generic “make the graph prettier” tactics miss the real job.
+
+The primary job is **dependency-injection architecture review**: see whether project wiring is organized sanely and where to reorganize. Debugging inject failures matters less here because call-chain Trace already covers it.
+
+Providers (who produces what) and objects (what has been instantiated) are different questions and must not share the same default canvas treatment.
+
+## Goals
+
+1. Keep a **single UI**: legacy `/` (Alpine + existing tools).
+2. Preserve **all existing legacy capabilities** (package sidebar, group rules, prefix, depth, layouts, SVG/Mermaid, diagnostic modals, Trace, etc.).
+3. Make large graphs readable for **architecture organization**, not only for pixel density.
+4. Support four architectural lenses: **package/module**, **providers**, **types**, **business groups**.
+5. Port useful `/next` pieces into legacy; **delete** the `/next` shell and route.
+6. Keep public Dix APIs stable (additive fields only); no new third-party dependencies.
+
+## Non-Goals
+
+- Rebuilding a second five-view product shell.
+- Making Trace/diagnosis the main graph workflow.
+- Drawing every object as a default architecture node.
+- Replacing vis-network or Alpine in this iteration.
+- Silently changing the user’s current view mode without an explicit action.
+
+## Product Approach
+
+**Chosen:** Approach A — enhance legacy `/`, embed useful `/next` rendering helpers, then delete `/next`.
+
+Rejected:
+
+- Dual-track `/` + `/next` (maintenance cost).
+- Full rewrite of the workstation chrome.
+
+**Default entry:** global Providers graph may remain the default (existing habit). Crowding is handled by semantic degradation and clearer lenses, not by removing the Providers entry.
+
+## Jobs and Lenses
+
+### Primary job
+
+Answer: *Is the DI dependency structure organized reasonably? Where should we optimize boundaries?*
+
+### Secondary job
+
+Inject/runtime failures — Trace modals remain the main path; graph only keeps helpful jumps.
+
+### Four lenses (all required)
+
+| Lens | Question | Canvas nodes | Edges | Objects |
+|---|---|---|---|---|
+| Package / module | Are cross-boundary deps sane? | Packages or modules (prefer `/api/modules`) | Cross-package/module deps | Counts/badges only |
+| Providers | Who produces what? | Providers + I/O types (existing) | Produce / depend edges (existing) | Detail/status only |
+| Types | Is type coupling too dense? | Type nodes (existing) | Type→type edges (existing) | Detail/status only |
+| Business groups | How do domains couple? | Group nodes via existing aggregate rules | Inter-group edges; expand in-group | Detail/status only |
+
+### UI mapping (no capability loss)
+
+- Toolbar: keep **Providers** / **Types**; add **Module map** (ported from `/next`).
+- **Business groups**: keep **按分组聚合** (not a fifth mutually exclusive mode that fights Providers/Types). Under module map, aggregation means further collapsing modules by group rules when matched.
+- Package sidebar: clicking a package sets **architectural scope** (prefix) and redraws — framed as choosing a slice, not only “filter noise.”
+
+## Crowding Solution (DI-semantic, not generic layout-only)
+
+Crowding happens when an **architecture question** is answered with an **inventory-level canvas** (all providers × types × objects).
+
+Fixed degradation order when the current canvas exceeds a readable budget:
+
+1. **Prompt a coarser lens** — banner suggesting Module map or enabling group aggregation; one-click apply allowed; do not silently switch the user’s view.
+2. **Semantic collapse** — if aggregation is on, prefer inter-group edges; if off, recommend turning it on.
+3. **Mark coupling hubs** — highest-degree types/packages/groups (architecture smells); click focuses or sets prefix scope.
+4. **Short labels** — shorten package/type display names; full name on hover/detail (from `/next`).
+5. **Layout / camera** — avoid thin hierarchical bead-strings on large graphs; use readable layout + fit/focus (from `/next` strategies), wired into legacy `renderGraph`.
+6. **Top-K truncation last** — keep highest-connectivity nodes, declare truncation, show hub table and narrow-scope actions.
+
+**Objects never enter the architecture node pool by default** — they amplify false density.
+
+Soft budget starting points (tunable): canvas readability soft cap ~40–60 structural nodes; providers/types composition may use ~150/400 before soft truncation — aligned with prior scale work, applied inside legacy.
+
+## Port from `/next` / Delete `/next`
+
+### Port into legacy
+
+- Module map rendering (`/api/modules`, readable/star layout).
+- `shortGraphLabel`, layout/camera helpers, hub ranking, budget helpers.
+- Optional bounded drill-downs already exposed by API (`/api/module`, `/api/ego`) where they improve scoped reading without removing full-graph modes.
+- Stable issue/error → graph/Trace link helpers if diagnostic modals still lack them.
+
+Delivered as helpers consumed by Alpine `renderGraph` / toolbar — **not** as a second shell.
+
+### Delete
+
+- `GET /next` and `HandleNextIndex`.
+- Five-view shell: `static/index.html`, `static/js/main.js`, `static/js/views/*` used only by `/next`.
+- Next-only docs/recommendations; README states `/` is the only UI.
+- Next-shell-only frontend tests; keep/repurpose pure logic tests onto shared modules used by legacy.
+
+### Keep as shared logic
+
+- Testable pieces of `graph_state.mjs` / `graph_workbench.mjs` (labels, budget, hubs, group aggregate helpers) referenced by legacy; strip next-only wiring.
+
+## Implementation Boundaries
+
+### Touch
+
+- `dixhttp/template.html` — module map control; density banner/hub table; sidebar copy for scope.
+- `dixhttp/static/js/legacy/app.js` — render pipeline hooks for labels, degradation, module map, camera.
+- Shared mjs helpers + node tests.
+- `dixhttp/server.go` — remove `/next`.
+- README / superseded design notes.
+
+### Do not touch (this iteration)
+
+- Public API semantics (additive only).
+- Trace/diagnostic modal product rewrite.
+- New JS/Go third-party dependencies.
+- Forced default-view change away from Providers unless user later asks.
+
+### Risks
+
+- Large `app.js`: change render in small steps; regress group/prefix/SVG/Mermaid/depth.
+- Banner vs silent mode switch: prompt + optional one-click only.
+
+## Acceptance Criteria
+
+1. Example-scale app (~10 modules, ~100 providers): module map shows clear boundaries; global Providers over budget shows banner + hubs instead of a silent bead-string.
+2. Group aggregate, package prefix, Providers/Types/Module switching behave as today plus module map — no removed tools.
+3. Objects are not default architecture canvas nodes.
+4. `/next` removed; `/` is the only visualization UI entry.
+5. `node --test` for shared graph helpers passes; `go test ./dixhttp/...` passes.
+
+## Open Parameters (planning may tune, not reopen product direction)
+
+- Exact soft-cap numbers and when auto-layout overrides hierarchical.
+- Whether module map uses package list identity vs `/api/modules` labels when they diverge — prefer `/api/modules` for cross-module edges, keep package sidebar for scope.
diff --git a/docs/superpowers/specs/2026-09-06-provider-panorama-zoom-lod-design.md b/docs/superpowers/specs/2026-09-06-provider-panorama-zoom-lod-design.md
new file mode 100644
index 0000000..202505d
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-06-provider-panorama-zoom-lod-design.md
@@ -0,0 +1,146 @@
+# Provider Panorama + Zoom LOD Design
+
+> **Status: CANCELLED (2026-09-07)** — 全景 layout removed from product. Completeness stays via right-rail Provider inventory + package scope / 模块地图; canvas layouts remain 层级 / 力导向 only.
+> Parent: `2026-09-06-legacy-first-architecture-viz-design.md`
+> Branch context: `codex/scale-graph-usability`
+
+## Historical note
+
+Earlier drafts (Approach B grid + LOD, Approach C structure panorama + inventory) are retained below for context only. Do not re-introduce a `panorama` layout option without a new approved design.
+
+---
+
+## Approach C (locked 2026-09-07) — superseded by removal
+
+**Dual channel — do not put completeness and readability on the same canvas encoding:**
+
+| Channel | Job | Encoding |
+|---|---|---|
+| **Structure panorama** | Perceive organization / cross-boundary coupling | Package-level nodes + inter-package edges (default when 布局=全景 and scope=全部) |
+| **Provider inventory** | Guarantee every provider is reachable | Scrollable list (right rail); search; click → detail / scoped graph |
+
+- Package is an **abstraction tier for the structure graph**, not merely a filter that “gives up” on panorama.
+- Drill-down: click/double-click a package node (or pick from left sidebar) → scoped Providers canvas may still show that package’s providers.
+- Hierarchical / force layouts stay literal provider+type graphs for users who want them; tip steers large global graphs to 全景结构 + 清单.
+
+### Non-goals (Approach C)
+
+- Making every provider label simultaneously readable on one fitted canvas.
+- Treating package scope as the only completeness mechanism.
+
+---
+
+## Problem (original)
+
+Global Providers with hierarchical layout becomes a thin horizontal strip. Fit-to-screen makes every node a speck. Auto-swapping hierarchical → physics was the wrong fix (hijacked user layout choice). Top-K truncation conflicts with the product need for a **full panorama of every provider**.
+
+## Goals
+
+1. **All providers remain reachable** via inventory (Approach C); structure canvas uses package abstraction by default.
+2. **Same canvas, zoom-stratified reading** where useful on scoped provider maps.
+3. Explicit **全景 (panorama)** layout = structure map at global scope.
+4. **层级 / 力导向 stay honest**: selecting them uses that algorithm; never silently rewrite the user’s choice.
+5. Keep existing tools (package scope, group aggregate, edge declutter, module map, details, Trace).
+
+## Non-Goals
+
+- Separate minimap widget (user chose same-canvas zoom LOD).
+- Forcing package selection before drawing.
+- Replacing hierarchical with physics under the hood.
+- Drawing objects as default architecture nodes.
+- Perfect simultaneous readability of every label at overview zoom (impossible at scale; LOD handles this).
+
+## Product Decisions (locked)
+
+| Decision | Choice |
+|---|---|
+| Default lens when opening `/` | Unchanged unless noted below: Providers may remain default |
+| Full inventory | Panorama shows **all** providers in scope (global or selected package) |
+| Dual-layer UX | **Same canvas zoom LOD** (not minimap, not mode toggle) |
+| Layout honesty | Explicit **全景** option; hierarchical/force never auto-swapped |
+| Providers default layout | **全景** when opening Providers at global scope; user can switch to 层级/力导向 |
+
+## Design
+
+### 1. Layout option: 全景
+
+Toolbar `布局` gains:
+
+- **全景** (new, recommended for large Providers)
+- 层级布局 (unchanged semantics)
+- 力导向 (unchanged semantics)
+
+**Panorama algorithm (client-side, deterministic):**
+
+1. Group provider nodes by package key (`output_pkg` / `function_pkg` / node `packagePath`; fallback `"_"`).
+2. Sort packages by name; sort providers within package by id/name.
+3. Place packages on a **grid of columns** (column count ≈ `ceil(sqrt(packageCount))`), each package a vertical stack of provider boxes with fixed cell size.
+4. Package header is not a separate graph node; at overview zoom the **visible label** for a region is the package short name (via LOD), not an extra inventory node.
+5. Cross-package edges kept; same-package edges optional via existing 边降噪.
+6. Disable vis hierarchical layout and physics for panorama; set `{x,y}` (and optionally `fixed.x/y` during initial paint) so positions stay stable.
+
+Types view may keep current layouts for this iteration; panorama is required for **Providers** first. Module map stays its own view.
+
+### 2. Zoom LOD (same canvas)
+
+Three bands (tunable constants):
+
+| Band | Approx. scale | Labels shown |
+|---|---|---|
+| Overview | `< ~0.55` | Package short names on a representative node per package (or hub-only if single package); other nodes show `·` or empty |
+| Mid | `~0.55–0.9` | Hubs + package reps; more provider short labels appear by degree |
+| Detail | `> ~0.9` | All provider short labels in viewport; full name on hover/title |
+
+Rules:
+
+- **Nodes and edges are never removed by LOD** — only label visibility/text changes.
+- Existing `labelLodVisibleIds` / `shortGraphLabel` are extended for package-band behavior; do not invent a second shell.
+- Initial camera for panorama: **fit** the full map (structure panorama). User zooms for names. Density tip may say: “缩放过小看结构;放大读 Provider 名”.
+- 「适应全图」 = fit panorama. 「聚焦枢纽」 = focus highest-degree provider at detail scale (~1.05).
+
+### 3. Crowding interaction (revised for panorama)
+
+When Providers + 全景 + global scope:
+
+1. Prefer panorama packing over hierarchical strip.
+2. Edge declutter may still hide same-package edges (toggle remains).
+3. Group aggregate still available; if on, panorama packs **group nodes** (or expanded members) consistently — do not drop providers from the dataset unless user enabled aggregation.
+4. **No auto layout swap.**
+5. Soft Top-K budget is **off for panorama** (all providers). Soft budget may still apply to hierarchical/force if those paths remain dense — declare in UI if truncated. Panorama path must not truncate.
+
+Package sidebar still scopes the panorama to one package when selected (fewer columns; detail labels appear earlier).
+
+### 4. Honesty / messaging
+
+- If user selects 层级 and the graph is wide, tip suggests switching to **全景** or selecting a package — **do not** change the dropdown for them.
+- When auto-defaulting Providers → 全景 on first load of a large global graph, set the layout control to 全景 so UI matches reality.
+
+## Acceptance
+
+1. Global Providers + 全景: every provider in current API payload appears as a node; fit shows a 2D package map (not a 1-row strip).
+2. Zoom out: package-oriented labels dominate; zoom in: provider names become readable without removing nodes.
+3. Switching to 层级 uses hierarchical layout; switching back to 全景 restores panorama positions; no silent overrides.
+4. Package scope + 全景 still shows all providers in that package.
+5. Existing module map / aggregate / edge declutter / details / Trace still work.
+6. Unit tests cover panorama position determinism and LOD band label selection.
+
+## Implementation Boundaries
+
+### Touch
+
+- `dixhttp/template.html` — layout select option 全景; tip copy.
+- `dixhttp/static/js/legacy/app.js` — Providers render path for panorama positions + camera; persist layout choice.
+- `dixhttp/static/js/graph_state.mjs` (+ tests) — `layoutPanoramaPositions`, LOD band helpers.
+- Cache bump on static assets.
+
+### Do not touch
+
+- Server dependency APIs (unless a bug blocks package keys).
+- Reintroducing `/next`.
+- Minimap DOM widget.
+
+## Open questions (resolved)
+
+- Minimap vs zoom LOD → **zoom LOD**.
+- Truncate for readability → **no on panorama path**.
+- Hijack hierarchical → **no**; add explicit 全景 instead.
diff --git a/docs/superpowers/specs/2026-09-07-http-example-microservice-layout-design.md b/docs/superpowers/specs/2026-09-07-http-example-microservice-layout-design.md
new file mode 100644
index 0000000..b20d478
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-07-http-example-microservice-layout-design.md
@@ -0,0 +1,99 @@
+# HTTP Example Microservice Layout Design
+
+**Date:** 2026-09-07
+**Status:** Approved
+**Scope:** Restructure `example/http` into a clear microservice-style Go package layout so ModuleGraph / 模块地图 reflects real system boundaries (not visualization virtual buckets).
+
+## Goal
+
+Organize the dixhttp scale demo as a complete small system:
+
+- Vertical domain slices with real packages: `models` → `infra` → `logic` → `service` → `handler`
+- Shared shells: `app`, `router`, `infra/*`, `plugins`, `bootstrap`
+- Each package registers its own `Provide(di)` — **no** domain-root aggregator `provide.go`
+- `bootstrap` is the only DI assembly / startup orchestration entry
+- Plugins: public **interfaces**, several **implementations**, each with a Provider, consumers see interfaces only
+
+## Non-goals
+
+- Changing dixhttp UI semantics (providers pyramid, module map algorithm) beyond what falls out of real packages
+- Keeping the old `Plugin[T]` / 60-role generic sea as the primary plugin model
+- Domain-level `Providers()` / `provide.go` facades
+
+## Package tree
+
+```text
+example/http/
+ main.go # thin: bootstrap.Run()
+ bootstrap/
+ container.go # Build() — call every module Provide in order
+ run.go # pre-create, startup diagnostics, hand off to router
+ app/
+ application.go # Application aggregate + Provide
+ router/
+ server.go # dixhttp listen / serve
+ infra/
+ logger/ # Logger interface + ConsoleLogger Provide
+ diag/ # SlowRemote / TimeoutProbe + startup demo types
+ plugins/
+ plugin.go # Plugin / Worker / Platform interfaces + Platform Provide
+ auth/ # impl + Provide → map[string]plugins.Plugin (and Worker)
+ billing/
+ cache/
+ … # several impl packages (not one mega main package)
+ domain/
+ billing/
+ models/ # Config + Provide
+ infra/ # Client, Regions + Provide
+ logic/ # Repo + Provide
+ service/ # Service + Provide
+ handler/ # Handler + Provide
+ analytics|identity|inventory|media|notification|searchx|shipping|storage|workflow/
+ (same five layers)
+```
+
+## Rules
+
+1. **Provide lives in the owning package.** Bootstrap imports packages and calls `Provide(*dix.Dix)`; it does not define domain providers.
+2. **No domain-root `provide.go`.** There is no `domain/billing.Providers`.
+3. **Import paths are the module identity.** Prefer short package names (`models`, `handler`, …) under each domain path; bootstrap uses import aliases when needed.
+4. **Plugins**
+ - Contract package `plugins` exports `Plugin`, `Worker`, and `Platform` (or equivalent).
+ - Each implementation package provides `map[string]plugins.Plugin` / `map[string]plugins.Worker` (dix namespace merge) so multiple impls coexist.
+ - `Platform` depends on `map[string]plugins.Worker` (or Plugin) and exposes names/counts to `Application`.
+ - `Application` depends on `*plugins.Platform` and domain handlers/services — not concrete plugin impl types.
+5. **Pyramid entries:** `*app.Application` and `*diag.TimeoutProbe` (no artificial ScaleFixture).
+6. **Scale comes from real packages** (domain layers + plugin impls), not pad providers.
+
+## Wiring order (bootstrap)
+
+1. `infra/logger`
+2. All `domain/*/models|infra|logic|service|handler` (models before infra before logic before service before handler; domains independent)
+3. All `plugins/` then `plugins` Platform
+4. `app` Application
+5. `infra/diag` timeout chain (entry #2)
+
+`main` only calls `bootstrap.Run()`.
+
+## Test contracts
+
+| Test | Expectation after change |
+|------|---------------------------|
+| `TestBuildContainerWiresApplication` | Ten domain services wired; plugin name count = number of registered plugin/worker namespaces (not 120) |
+| `TestDemoContainerShape` | ≥10 modules; provider/object floors from real domain+plugins |
+| `TestPyramidHasTwoBusinessEntries` | Exactly two unconsumed business entry providers (Application, TimeoutProbe) |
+
+## Migration
+
+1. Add new packages; generate ten identical domain layer trees from the former flat `domain//.go`.
+2. Replace `plugins.go` with interface + impl packages.
+3. Move Application / Logger / Scale / Timeout / server into `app` / `infra` / `router`.
+4. Point tests at `bootstrap.Build()` (or keep `buildContainer` as a one-liner wrapper in main for minimal test churn).
+5. Delete old flat domain files and `plugins.go` bodies in `main`.
+6. Optionally simplify module-map virtual coarse buckets once real packages disperse providers.
+
+## Success criteria
+
+- `go test ./example/http/...` passes
+- ModuleGraph paths show `domain/.../handler`, `plugins/auth`, `bootstrap` not used as a provider home
+- Browser 模块地图 shows deep real packages without relying on visualization-only splits for domains
diff --git a/docs/superpowers/specs/2026-09-07-providers-pyramid-view-design.md b/docs/superpowers/specs/2026-09-07-providers-pyramid-view-design.md
new file mode 100644
index 0000000..bd44865
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-07-providers-pyramid-view-design.md
@@ -0,0 +1,31 @@
+# Providers Pyramid View Design
+
+> Status: implemented 2026-09-07 · Branch: `codex/scale-graph-usability`
+
+## Goal
+
+Providers 视图只画 Provider,并按依赖金字塔分层:塔尖是少数入口,深度控件裁层,包/Provider 过滤看相关上下游。
+
+## Decisions
+
+| Topic | Choice |
+|---|---|
+| Nodes | Provider only(Type 压成依赖边) |
+| Edge | `A→B` = A 依赖 B(消费 B 的产出) |
+| Entry (塔尖) | 未被其它 Provider 消费(子图内 indegree 0) |
+| Depth N | 只保留 level ≤ N;`0` = 不截层 |
+| Package filter | 包内 Provider 为种子,展开相关上下游,再套深度 |
+| Provider filter | 双击或清单点选设种子,同上 |
+| Crowding | 深度 + 包/Provider 范围;不再靠 Type 节点或 Top-K 伪装全景 |
+
+## Non-goals
+
+- 不恢复「全景」布局
+- Types 视图仍为 type→type 图
+- 不在服务端重算层级(当前 client helpers 足够 example 规模)
+
+## Implementation
+
+- Helpers: `dixhttp/static/js/graph_state.mjs` — `buildProviderDependencyGraph`, `assignProviderPyramidLevels`, `truncateProviderPyramid`, `providerRelatedSubgraph`, `buildProvidersPyramidView`
+- UI: `dixhttp/static/js/legacy/app.js` Providers `renderGraph` path; `pyramidFocusProviderId`
+- Cache: `legacy-arch15`
diff --git a/docs/superpowers/specs/2026-09-08-hide-node-downstream-design.md b/docs/superpowers/specs/2026-09-08-hide-node-downstream-design.md
new file mode 100644
index 0000000..542ca5c
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-08-hide-node-downstream-design.md
@@ -0,0 +1,27 @@
+# Hide Node And Downstream Design
+
+**Date:** 2026-09-08
+**Status:** Implemented
+**Scope:** Legacy architecture UI — hide selected canvas nodes and their depends-on descendants across Providers / Types / Module map.
+
+## Behavior
+
+- **Primary:** right-click node →「隐藏此节点及下游 (N)」
+- **Fast path:** Shift+click node; or select then toolbar「隐藏及下游」
+- **Inventory:** hover Provider row →「隐」
+- Remove the seed and all downstream nodes (edge direction: consumer → dependency)
+- Upstream consumers remain; edges into hidden set are dropped
+- Seeds in `sessionStorage`; shared by Providers / Types / Module map
+- Toast with undo; dropdown「已隐藏 N」to restore
+
+## Non-goals
+
+- Package-path blacklist UI
+- Objects canvas
+- Persist hide beyond session without URL (opt-in localStorage is backlog)
+
+## Persistence (Phase 3)
+
+- `sessionStorage` + URL query: `hide` (pipe-separated seeds / `pkg:prefix`), optional `depth`, `pkg`
+- URL `hide` overrides session on load; changes `replaceState` the address bar
+- Multi-step undo via in-memory snapshot stack (cap 20); `U` / toast 撤销
diff --git a/docs/superpowers/specs/2026-09-09-architecture-findings-design.md b/docs/superpowers/specs/2026-09-09-architecture-findings-design.md
new file mode 100644
index 0000000..7d7f064
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-09-architecture-findings-design.md
@@ -0,0 +1,27 @@
+# Architecture Findings (结论优先体检)
+
+> Status: implemented in UI · Date: 2026-09-09 · Approach A: frontend rules in `graph_state.mjs`
+
+## Goal
+
+Default `/` answers *where is the DI structure unhealthy?* via a findings list, not a full Providers graph.
+
+## UX
+
+- Default `currentView: 'findings'`
+- Click finding → Providers/Types/Modules with package or neighborhood scope; chip「← 体检」returns
+- Empty state: clean message + open full graph
+- Runtime `/api/issues` unchanged (errors/slow), separate from static structure smells
+
+## v1 rules
+
+| kind | trigger | severity |
+|---|---|---|
+| `cross_bucket` | plugins/infra → domain, or domainA → domainB | warn |
+| `super_hub` | provider degree ≥ 12 | warn |
+| `entry_fanout` | indegree-0 entries ≥ 8 | info |
+| `fat_package` | providers in one package ≥ 12 | info |
+
+## API
+
+`buildArchitectureFindings(allData, opts?)` → Finding[] with `action.view` / `focus*` / `package` / `keepNeighborhood`.
diff --git a/example/http/app/application.go b/example/http/app/application.go
new file mode 100644
index 0000000..db39d4a
--- /dev/null
+++ b/example/http/app/application.go
@@ -0,0 +1,76 @@
+package app
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ analyticshandler "github.com/pubgo/dix/example/http/domain/analytics/handler"
+ analyticssvc "github.com/pubgo/dix/example/http/domain/analytics/service"
+ billinghandler "github.com/pubgo/dix/example/http/domain/billing/handler"
+ billingsvc "github.com/pubgo/dix/example/http/domain/billing/service"
+ identityhandler "github.com/pubgo/dix/example/http/domain/identity/handler"
+ identitysvc "github.com/pubgo/dix/example/http/domain/identity/service"
+ inventoryhandler "github.com/pubgo/dix/example/http/domain/inventory/handler"
+ inventorysvc "github.com/pubgo/dix/example/http/domain/inventory/service"
+ mediahandler "github.com/pubgo/dix/example/http/domain/media/handler"
+ mediasvc "github.com/pubgo/dix/example/http/domain/media/service"
+ notifyhandler "github.com/pubgo/dix/example/http/domain/notification/handler"
+ notifysvc "github.com/pubgo/dix/example/http/domain/notification/service"
+ searchhandler "github.com/pubgo/dix/example/http/domain/searchx/handler"
+ searchsvc "github.com/pubgo/dix/example/http/domain/searchx/service"
+ shippinghandler "github.com/pubgo/dix/example/http/domain/shipping/handler"
+ shippingsvc "github.com/pubgo/dix/example/http/domain/shipping/service"
+ storagehandler "github.com/pubgo/dix/example/http/domain/storage/handler"
+ storagesvc "github.com/pubgo/dix/example/http/domain/storage/service"
+ workflowhandler "github.com/pubgo/dix/example/http/domain/workflow/handler"
+ workflowsvc "github.com/pubgo/dix/example/http/domain/workflow/service"
+ "github.com/pubgo/dix/example/http/infra/logger"
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+// Application 应用聚合根:引用十个域服务 + 插件平台。
+type Application struct {
+ Logger logger.Logger
+ Billing *billingsvc.Service
+ Inventory *inventorysvc.Service
+ Shipping *shippingsvc.Service
+ Identity *identitysvc.Service
+ Analytics *analyticssvc.Service
+ Notify *notifysvc.Service
+ Search *searchsvc.Service
+ Storage *storagesvc.Service
+ Media *mediasvc.Service
+ Workflow *workflowsvc.Service
+ Plugins []string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(
+ log logger.Logger,
+ billingH *billinghandler.Handler,
+ inventoryH *inventoryhandler.Handler,
+ shippingH *shippinghandler.Handler,
+ identityH *identityhandler.Handler,
+ analyticsH *analyticshandler.Handler,
+ notifyH *notifyhandler.Handler,
+ searchH *searchhandler.Handler,
+ storageH *storagehandler.Handler,
+ mediaH *mediahandler.Handler,
+ workflowH *workflowhandler.Handler,
+ platform *plugins.Platform,
+ ) *Application {
+ return &Application{
+ Logger: log,
+ Billing: billingH.Service,
+ Inventory: inventoryH.Service,
+ Shipping: shippingH.Service,
+ Identity: identityH.Service,
+ Analytics: analyticsH.Service,
+ Notify: notifyH.Service,
+ Search: searchH.Service,
+ Storage: storageH.Service,
+ Media: mediaH.Service,
+ Workflow: workflowH.Service,
+ Plugins: platform.Names,
+ }
+ })
+}
diff --git a/example/http/bootstrap/container.go b/example/http/bootstrap/container.go
new file mode 100644
index 0000000..aa27cc9
--- /dev/null
+++ b/example/http/bootstrap/container.go
@@ -0,0 +1,207 @@
+package bootstrap
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/pubgo/dix/v2"
+ "github.com/pubgo/dix/v2/dixhttp"
+
+ "github.com/pubgo/dix/example/http/app"
+ analyticshandler "github.com/pubgo/dix/example/http/domain/analytics/handler"
+ analyticsinfra "github.com/pubgo/dix/example/http/domain/analytics/infra"
+ analyticslogic "github.com/pubgo/dix/example/http/domain/analytics/logic"
+ analyticsmodels "github.com/pubgo/dix/example/http/domain/analytics/models"
+ analyticssvc "github.com/pubgo/dix/example/http/domain/analytics/service"
+ billinghandler "github.com/pubgo/dix/example/http/domain/billing/handler"
+ billinginfra "github.com/pubgo/dix/example/http/domain/billing/infra"
+ billinglogic "github.com/pubgo/dix/example/http/domain/billing/logic"
+ billingmodels "github.com/pubgo/dix/example/http/domain/billing/models"
+ billingsvc "github.com/pubgo/dix/example/http/domain/billing/service"
+ identityhandler "github.com/pubgo/dix/example/http/domain/identity/handler"
+ identityinfra "github.com/pubgo/dix/example/http/domain/identity/infra"
+ identitylogic "github.com/pubgo/dix/example/http/domain/identity/logic"
+ identitymodels "github.com/pubgo/dix/example/http/domain/identity/models"
+ identitysvc "github.com/pubgo/dix/example/http/domain/identity/service"
+ inventoryhandler "github.com/pubgo/dix/example/http/domain/inventory/handler"
+ inventoryinfra "github.com/pubgo/dix/example/http/domain/inventory/infra"
+ inventorylogic "github.com/pubgo/dix/example/http/domain/inventory/logic"
+ inventorymodels "github.com/pubgo/dix/example/http/domain/inventory/models"
+ inventorysvc "github.com/pubgo/dix/example/http/domain/inventory/service"
+ mediahandler "github.com/pubgo/dix/example/http/domain/media/handler"
+ mediainfra "github.com/pubgo/dix/example/http/domain/media/infra"
+ medialogic "github.com/pubgo/dix/example/http/domain/media/logic"
+ mediamodels "github.com/pubgo/dix/example/http/domain/media/models"
+ mediasvc "github.com/pubgo/dix/example/http/domain/media/service"
+ notifyhandler "github.com/pubgo/dix/example/http/domain/notification/handler"
+ notifyinfra "github.com/pubgo/dix/example/http/domain/notification/infra"
+ notifylogic "github.com/pubgo/dix/example/http/domain/notification/logic"
+ notifymodels "github.com/pubgo/dix/example/http/domain/notification/models"
+ notifysvc "github.com/pubgo/dix/example/http/domain/notification/service"
+ searchhandler "github.com/pubgo/dix/example/http/domain/searchx/handler"
+ searchinfra "github.com/pubgo/dix/example/http/domain/searchx/infra"
+ searchlogic "github.com/pubgo/dix/example/http/domain/searchx/logic"
+ searchmodels "github.com/pubgo/dix/example/http/domain/searchx/models"
+ searchsvc "github.com/pubgo/dix/example/http/domain/searchx/service"
+ shippinghandler "github.com/pubgo/dix/example/http/domain/shipping/handler"
+ shippinginfra "github.com/pubgo/dix/example/http/domain/shipping/infra"
+ shippinglogic "github.com/pubgo/dix/example/http/domain/shipping/logic"
+ shippingmodels "github.com/pubgo/dix/example/http/domain/shipping/models"
+ shippingsvc "github.com/pubgo/dix/example/http/domain/shipping/service"
+ storagehandler "github.com/pubgo/dix/example/http/domain/storage/handler"
+ storageinfra "github.com/pubgo/dix/example/http/domain/storage/infra"
+ storagelogic "github.com/pubgo/dix/example/http/domain/storage/logic"
+ storagemodels "github.com/pubgo/dix/example/http/domain/storage/models"
+ storagesvc "github.com/pubgo/dix/example/http/domain/storage/service"
+ workflowhandler "github.com/pubgo/dix/example/http/domain/workflow/handler"
+ workflowinfra "github.com/pubgo/dix/example/http/domain/workflow/infra"
+ workflowlogic "github.com/pubgo/dix/example/http/domain/workflow/logic"
+ workflowmodels "github.com/pubgo/dix/example/http/domain/workflow/models"
+ workflowsvc "github.com/pubgo/dix/example/http/domain/workflow/service"
+ "github.com/pubgo/dix/example/http/infra/diag"
+ "github.com/pubgo/dix/example/http/infra/logger"
+ "github.com/pubgo/dix/example/http/plugins"
+ "github.com/pubgo/dix/example/http/plugins/auth"
+ pluginbilling "github.com/pubgo/dix/example/http/plugins/billing"
+ "github.com/pubgo/dix/example/http/plugins/cache"
+ "github.com/pubgo/dix/example/http/plugins/email"
+ "github.com/pubgo/dix/example/http/plugins/export"
+ "github.com/pubgo/dix/example/http/plugins/graphql"
+ "github.com/pubgo/dix/example/http/plugins/importx"
+ "github.com/pubgo/dix/example/http/plugins/job"
+ "github.com/pubgo/dix/example/http/plugins/kafka"
+ "github.com/pubgo/dix/example/http/plugins/login"
+ "github.com/pubgo/dix/example/http/plugins/metrics"
+ pluginnotify "github.com/pubgo/dix/example/http/plugins/notify"
+ "github.com/pubgo/dix/example/http/plugins/oauth"
+ "github.com/pubgo/dix/example/http/plugins/queue"
+ "github.com/pubgo/dix/example/http/plugins/report"
+ pluginsearch "github.com/pubgo/dix/example/http/plugins/search"
+ "github.com/pubgo/dix/example/http/plugins/session"
+ "github.com/pubgo/dix/example/http/plugins/tenant"
+ "github.com/pubgo/dix/example/http/plugins/upload"
+ "github.com/pubgo/dix/example/http/plugins/vault"
+ "github.com/pubgo/dix/example/http/router"
+)
+
+// Build 装配完整演示容器:各模块自行 Provide,本包只负责编排顺序。
+func Build() *dix.Dix {
+ di := dix.New(
+ dix.WithProviderTimeout(200*time.Millisecond),
+ dix.WithSlowProviderThreshold(80*time.Millisecond),
+ )
+
+ logger.Provide(di)
+
+ analyticsmodels.Provide(di)
+ analyticsinfra.Provide(di)
+ analyticslogic.Provide(di)
+ analyticssvc.Provide(di)
+ analyticshandler.Provide(di)
+
+ billingmodels.Provide(di)
+ billinginfra.Provide(di)
+ billinglogic.Provide(di)
+ billingsvc.Provide(di)
+ billinghandler.Provide(di)
+
+ identitymodels.Provide(di)
+ identityinfra.Provide(di)
+ identitylogic.Provide(di)
+ identitysvc.Provide(di)
+ identityhandler.Provide(di)
+
+ inventorymodels.Provide(di)
+ inventoryinfra.Provide(di)
+ inventorylogic.Provide(di)
+ inventorysvc.Provide(di)
+ inventoryhandler.Provide(di)
+
+ mediamodels.Provide(di)
+ mediainfra.Provide(di)
+ medialogic.Provide(di)
+ mediasvc.Provide(di)
+ mediahandler.Provide(di)
+
+ notifymodels.Provide(di)
+ notifyinfra.Provide(di)
+ notifylogic.Provide(di)
+ notifysvc.Provide(di)
+ notifyhandler.Provide(di)
+
+ searchmodels.Provide(di)
+ searchinfra.Provide(di)
+ searchlogic.Provide(di)
+ searchsvc.Provide(di)
+ searchhandler.Provide(di)
+
+ shippingmodels.Provide(di)
+ shippinginfra.Provide(di)
+ shippinglogic.Provide(di)
+ shippingsvc.Provide(di)
+ shippinghandler.Provide(di)
+
+ storagemodels.Provide(di)
+ storageinfra.Provide(di)
+ storagelogic.Provide(di)
+ storagesvc.Provide(di)
+ storagehandler.Provide(di)
+
+ workflowmodels.Provide(di)
+ workflowinfra.Provide(di)
+ workflowlogic.Provide(di)
+ workflowsvc.Provide(di)
+ workflowhandler.Provide(di)
+
+ auth.Provide(di)
+ pluginbilling.Provide(di)
+ cache.Provide(di)
+ email.Provide(di)
+ export.Provide(di)
+ graphql.Provide(di)
+ importx.Provide(di)
+ job.Provide(di)
+ kafka.Provide(di)
+ login.Provide(di)
+ metrics.Provide(di)
+ pluginnotify.Provide(di)
+ oauth.Provide(di)
+ queue.Provide(di)
+ report.Provide(di)
+ pluginsearch.Provide(di)
+ session.Provide(di)
+ tenant.Provide(di)
+ upload.Provide(di)
+ vault.Provide(di)
+ plugins.Provide(di)
+
+ app.Provide(di)
+ diag.Provide(di)
+
+ return di
+}
+
+func preCreateObjects(di *dix.Dix) {
+ if err := di.TryInject(func(a *app.Application) {
+ log.Printf("✅ Application created: modules=10 plugins=%d", len(a.Plugins))
+ }); err != nil {
+ log.Printf("⚠️ pre-create injection failed: %v", err)
+ }
+ _ = di.TryInject(func(workers map[string]plugins.Worker) {
+ _ = workers
+ })
+}
+
+// Run 构建容器、预热对象、跑启动诊断并启动可视化 HTTP。
+func Run() error {
+ di := Build()
+ preCreateObjects(di)
+ diag.RunStartupErrorScenarios(di)
+ log.Println("")
+ server := dixhttp.NewServer(di)
+ if err := router.StartVisualizationServer(server); err != nil && err != http.ErrServerClosed {
+ return err
+ }
+ return nil
+}
diff --git a/example/http/domain/analytics/analytics.go b/example/http/domain/analytics/analytics.go
deleted file mode 100644
index 1a04d10..0000000
--- a/example/http/domain/analytics/analytics.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package analytics 提供数据分析域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package analytics
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 数据分析配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 数据分析下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 数据分析仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 数据分析服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 数据分析协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册数据分析域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/analytics/handler/handler.go b/example/http/domain/analytics/handler/handler.go
new file mode 100644
index 0000000..ce1f9bb
--- /dev/null
+++ b/example/http/domain/analytics/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/analytics/infra"
+ "github.com/pubgo/dix/example/http/domain/analytics/service"
+)
+
+// Handler analytics 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/analytics/infra/client.go b/example/http/domain/analytics/infra/client.go
new file mode 100644
index 0000000..185af0c
--- /dev/null
+++ b/example/http/domain/analytics/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/analytics/models"
+)
+
+// Client analytics 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/analytics/logic/repo.go b/example/http/domain/analytics/logic/repo.go
new file mode 100644
index 0000000..e618011
--- /dev/null
+++ b/example/http/domain/analytics/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/analytics/infra"
+)
+
+// Repo analytics 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/analytics/models/config.go b/example/http/domain/analytics/models/config.go
new file mode 100644
index 0000000..1a9d830
--- /dev/null
+++ b/example/http/domain/analytics/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config analytics 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/analytics/service/service.go b/example/http/domain/analytics/service/service.go
new file mode 100644
index 0000000..d16ca55
--- /dev/null
+++ b/example/http/domain/analytics/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/analytics/logic"
+)
+
+// Service analytics 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/billing/billing.go b/example/http/domain/billing/billing.go
deleted file mode 100644
index 6094acb..0000000
--- a/example/http/domain/billing/billing.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package billing 提供计费域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package billing
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 计费配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 计费下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 计费仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 计费服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 计费协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册计费域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/billing/handler/handler.go b/example/http/domain/billing/handler/handler.go
new file mode 100644
index 0000000..15770d5
--- /dev/null
+++ b/example/http/domain/billing/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/billing/infra"
+ "github.com/pubgo/dix/example/http/domain/billing/service"
+)
+
+// Handler billing 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/billing/infra/client.go b/example/http/domain/billing/infra/client.go
new file mode 100644
index 0000000..dfd249c
--- /dev/null
+++ b/example/http/domain/billing/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/billing/models"
+)
+
+// Client billing 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/billing/logic/repo.go b/example/http/domain/billing/logic/repo.go
new file mode 100644
index 0000000..9b22857
--- /dev/null
+++ b/example/http/domain/billing/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/billing/infra"
+)
+
+// Repo billing 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/billing/models/config.go b/example/http/domain/billing/models/config.go
new file mode 100644
index 0000000..f930900
--- /dev/null
+++ b/example/http/domain/billing/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config billing 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/billing/service/service.go b/example/http/domain/billing/service/service.go
new file mode 100644
index 0000000..274590d
--- /dev/null
+++ b/example/http/domain/billing/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/billing/logic"
+)
+
+// Service billing 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/identity/handler/handler.go b/example/http/domain/identity/handler/handler.go
new file mode 100644
index 0000000..995a3a3
--- /dev/null
+++ b/example/http/domain/identity/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/identity/infra"
+ "github.com/pubgo/dix/example/http/domain/identity/service"
+)
+
+// Handler identity 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/identity/identity.go b/example/http/domain/identity/identity.go
deleted file mode 100644
index dcc8c5a..0000000
--- a/example/http/domain/identity/identity.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package identity 提供身份认证域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package identity
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 身份认证配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 身份认证下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 身份认证仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 身份认证服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 身份认证协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册身份认证域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/identity/infra/client.go b/example/http/domain/identity/infra/client.go
new file mode 100644
index 0000000..4328cac
--- /dev/null
+++ b/example/http/domain/identity/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/identity/models"
+)
+
+// Client identity 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/identity/logic/repo.go b/example/http/domain/identity/logic/repo.go
new file mode 100644
index 0000000..4b8d978
--- /dev/null
+++ b/example/http/domain/identity/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/identity/infra"
+)
+
+// Repo identity 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/identity/models/config.go b/example/http/domain/identity/models/config.go
new file mode 100644
index 0000000..b148e0b
--- /dev/null
+++ b/example/http/domain/identity/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config identity 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/identity/service/service.go b/example/http/domain/identity/service/service.go
new file mode 100644
index 0000000..b9d6589
--- /dev/null
+++ b/example/http/domain/identity/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/identity/logic"
+)
+
+// Service identity 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/inventory/handler/handler.go b/example/http/domain/inventory/handler/handler.go
new file mode 100644
index 0000000..e894e6c
--- /dev/null
+++ b/example/http/domain/inventory/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/inventory/infra"
+ "github.com/pubgo/dix/example/http/domain/inventory/service"
+)
+
+// Handler inventory 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/inventory/infra/client.go b/example/http/domain/inventory/infra/client.go
new file mode 100644
index 0000000..879e66e
--- /dev/null
+++ b/example/http/domain/inventory/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/inventory/models"
+)
+
+// Client inventory 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/inventory/inventory.go b/example/http/domain/inventory/inventory.go
deleted file mode 100644
index 4ef383c..0000000
--- a/example/http/domain/inventory/inventory.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package inventory 提供库存域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package inventory
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 库存配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 库存下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 库存仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 库存服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 库存协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册库存域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/inventory/logic/repo.go b/example/http/domain/inventory/logic/repo.go
new file mode 100644
index 0000000..00f934f
--- /dev/null
+++ b/example/http/domain/inventory/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/inventory/infra"
+)
+
+// Repo inventory 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/inventory/models/config.go b/example/http/domain/inventory/models/config.go
new file mode 100644
index 0000000..6271dd9
--- /dev/null
+++ b/example/http/domain/inventory/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config inventory 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/inventory/service/service.go b/example/http/domain/inventory/service/service.go
new file mode 100644
index 0000000..61a6aeb
--- /dev/null
+++ b/example/http/domain/inventory/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/inventory/logic"
+)
+
+// Service inventory 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/media/handler/handler.go b/example/http/domain/media/handler/handler.go
new file mode 100644
index 0000000..acbce38
--- /dev/null
+++ b/example/http/domain/media/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/media/infra"
+ "github.com/pubgo/dix/example/http/domain/media/service"
+)
+
+// Handler media 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/media/infra/client.go b/example/http/domain/media/infra/client.go
new file mode 100644
index 0000000..1bdc454
--- /dev/null
+++ b/example/http/domain/media/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/media/models"
+)
+
+// Client media 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/media/logic/repo.go b/example/http/domain/media/logic/repo.go
new file mode 100644
index 0000000..ec63789
--- /dev/null
+++ b/example/http/domain/media/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/media/infra"
+)
+
+// Repo media 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/media/media.go b/example/http/domain/media/media.go
deleted file mode 100644
index 7ec9bea..0000000
--- a/example/http/domain/media/media.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package media 提供媒体处理域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package media
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 媒体处理配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 媒体处理下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 媒体处理仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 媒体处理服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 媒体处理协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册媒体处理域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/media/models/config.go b/example/http/domain/media/models/config.go
new file mode 100644
index 0000000..6d1d1f4
--- /dev/null
+++ b/example/http/domain/media/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config media 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/media/service/service.go b/example/http/domain/media/service/service.go
new file mode 100644
index 0000000..915082a
--- /dev/null
+++ b/example/http/domain/media/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/media/logic"
+)
+
+// Service media 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/notification/handler/handler.go b/example/http/domain/notification/handler/handler.go
new file mode 100644
index 0000000..420e93f
--- /dev/null
+++ b/example/http/domain/notification/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/notification/infra"
+ "github.com/pubgo/dix/example/http/domain/notification/service"
+)
+
+// Handler notification 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/notification/infra/client.go b/example/http/domain/notification/infra/client.go
new file mode 100644
index 0000000..c02b811
--- /dev/null
+++ b/example/http/domain/notification/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/notification/models"
+)
+
+// Client notification 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/notification/logic/repo.go b/example/http/domain/notification/logic/repo.go
new file mode 100644
index 0000000..9c58eb4
--- /dev/null
+++ b/example/http/domain/notification/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/notification/infra"
+)
+
+// Repo notification 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/notification/models/config.go b/example/http/domain/notification/models/config.go
new file mode 100644
index 0000000..c9e7ba7
--- /dev/null
+++ b/example/http/domain/notification/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config notification 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/notification/notification.go b/example/http/domain/notification/notification.go
deleted file mode 100644
index 9c4f74e..0000000
--- a/example/http/domain/notification/notification.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package notification 提供通知域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package notification
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 通知配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 通知下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 通知仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 通知服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 通知协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册通知域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/notification/service/service.go b/example/http/domain/notification/service/service.go
new file mode 100644
index 0000000..908ca57
--- /dev/null
+++ b/example/http/domain/notification/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/notification/logic"
+)
+
+// Service notification 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/searchx/handler/handler.go b/example/http/domain/searchx/handler/handler.go
new file mode 100644
index 0000000..7db9a12
--- /dev/null
+++ b/example/http/domain/searchx/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/searchx/infra"
+ "github.com/pubgo/dix/example/http/domain/searchx/service"
+)
+
+// Handler searchx 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/searchx/infra/client.go b/example/http/domain/searchx/infra/client.go
new file mode 100644
index 0000000..ce70153
--- /dev/null
+++ b/example/http/domain/searchx/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/searchx/models"
+)
+
+// Client searchx 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/searchx/logic/repo.go b/example/http/domain/searchx/logic/repo.go
new file mode 100644
index 0000000..bc2b553
--- /dev/null
+++ b/example/http/domain/searchx/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/searchx/infra"
+)
+
+// Repo searchx 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/searchx/models/config.go b/example/http/domain/searchx/models/config.go
new file mode 100644
index 0000000..294a490
--- /dev/null
+++ b/example/http/domain/searchx/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config searchx 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/searchx/searchx.go b/example/http/domain/searchx/searchx.go
deleted file mode 100644
index 5fb117b..0000000
--- a/example/http/domain/searchx/searchx.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package searchx 提供搜索域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package searchx
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 搜索配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 搜索下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 搜索仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 搜索服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 搜索协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册搜索域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/searchx/service/service.go b/example/http/domain/searchx/service/service.go
new file mode 100644
index 0000000..10e8cdf
--- /dev/null
+++ b/example/http/domain/searchx/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/searchx/logic"
+)
+
+// Service searchx 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/shipping/handler/handler.go b/example/http/domain/shipping/handler/handler.go
new file mode 100644
index 0000000..d65a160
--- /dev/null
+++ b/example/http/domain/shipping/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/shipping/infra"
+ "github.com/pubgo/dix/example/http/domain/shipping/service"
+)
+
+// Handler shipping 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/shipping/infra/client.go b/example/http/domain/shipping/infra/client.go
new file mode 100644
index 0000000..238edb1
--- /dev/null
+++ b/example/http/domain/shipping/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/shipping/models"
+)
+
+// Client shipping 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/shipping/logic/repo.go b/example/http/domain/shipping/logic/repo.go
new file mode 100644
index 0000000..e451cb6
--- /dev/null
+++ b/example/http/domain/shipping/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/shipping/infra"
+)
+
+// Repo shipping 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/shipping/models/config.go b/example/http/domain/shipping/models/config.go
new file mode 100644
index 0000000..e0d5446
--- /dev/null
+++ b/example/http/domain/shipping/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config shipping 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/shipping/service/service.go b/example/http/domain/shipping/service/service.go
new file mode 100644
index 0000000..25fef48
--- /dev/null
+++ b/example/http/domain/shipping/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/shipping/logic"
+)
+
+// Service shipping 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/shipping/shipping.go b/example/http/domain/shipping/shipping.go
deleted file mode 100644
index a135b95..0000000
--- a/example/http/domain/shipping/shipping.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package shipping 提供物流域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package shipping
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 物流配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 物流下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 物流仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 物流服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 物流协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册物流域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/storage/handler/handler.go b/example/http/domain/storage/handler/handler.go
new file mode 100644
index 0000000..d36815b
--- /dev/null
+++ b/example/http/domain/storage/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/storage/infra"
+ "github.com/pubgo/dix/example/http/domain/storage/service"
+)
+
+// Handler storage 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/storage/infra/client.go b/example/http/domain/storage/infra/client.go
new file mode 100644
index 0000000..602ba72
--- /dev/null
+++ b/example/http/domain/storage/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/storage/models"
+)
+
+// Client storage 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/storage/logic/repo.go b/example/http/domain/storage/logic/repo.go
new file mode 100644
index 0000000..9f87b80
--- /dev/null
+++ b/example/http/domain/storage/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/storage/infra"
+)
+
+// Repo storage 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/storage/models/config.go b/example/http/domain/storage/models/config.go
new file mode 100644
index 0000000..cf1c317
--- /dev/null
+++ b/example/http/domain/storage/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config storage 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/storage/service/service.go b/example/http/domain/storage/service/service.go
new file mode 100644
index 0000000..1e819ef
--- /dev/null
+++ b/example/http/domain/storage/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/storage/logic"
+)
+
+// Service storage 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/storage/storage.go b/example/http/domain/storage/storage.go
deleted file mode 100644
index 981d136..0000000
--- a/example/http/domain/storage/storage.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package storage 提供对象存储域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package storage
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 对象存储配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 对象存储下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 对象存储仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 对象存储服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 对象存储协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册对象存储域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/domain/workflow/handler/handler.go b/example/http/domain/workflow/handler/handler.go
new file mode 100644
index 0000000..00c2265
--- /dev/null
+++ b/example/http/domain/workflow/handler/handler.go
@@ -0,0 +1,20 @@
+package handler
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ dinfr "github.com/pubgo/dix/example/http/domain/workflow/infra"
+ "github.com/pubgo/dix/example/http/domain/workflow/service"
+)
+
+// Handler workflow 协议处理器。
+type Handler struct {
+ Service *service.Service
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(s *service.Service, regions dinfr.Regions) *Handler {
+ _ = regions
+ return &Handler{Service: s}
+ })
+}
diff --git a/example/http/domain/workflow/infra/client.go b/example/http/domain/workflow/infra/client.go
new file mode 100644
index 0000000..0f0edf6
--- /dev/null
+++ b/example/http/domain/workflow/infra/client.go
@@ -0,0 +1,28 @@
+package infra
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/workflow/models"
+)
+
+// Client workflow 下游客户端。
+type Client struct {
+ Config *models.Config
+}
+
+// Regions 多区域连接。
+type Regions map[string]*Client
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *models.Config) *Client {
+ return &Client{Config: c}
+ })
+ dix.Provide(di, func(c *models.Config) Regions {
+ return Regions{
+ "cn": {Config: c},
+ "us": {Config: c},
+ "eu": {Config: c},
+ }
+ })
+}
diff --git a/example/http/domain/workflow/logic/repo.go b/example/http/domain/workflow/logic/repo.go
new file mode 100644
index 0000000..40f6c68
--- /dev/null
+++ b/example/http/domain/workflow/logic/repo.go
@@ -0,0 +1,18 @@
+package logic
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/workflow/infra"
+)
+
+// Repo workflow 仓储 / 领域逻辑。
+type Repo struct {
+ Client *infra.Client
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(c *infra.Client) *Repo {
+ return &Repo{Client: c}
+ })
+}
diff --git a/example/http/domain/workflow/models/config.go b/example/http/domain/workflow/models/config.go
new file mode 100644
index 0000000..c1b1a6b
--- /dev/null
+++ b/example/http/domain/workflow/models/config.go
@@ -0,0 +1,15 @@
+package models
+
+import "github.com/pubgo/dix/v2"
+
+// Config workflow 域配置。
+type Config struct {
+ Env string
+ Timeout string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() *Config {
+ return &Config{Env: "prod", Timeout: "3s"}
+ })
+}
diff --git a/example/http/domain/workflow/service/service.go b/example/http/domain/workflow/service/service.go
new file mode 100644
index 0000000..d81e1e6
--- /dev/null
+++ b/example/http/domain/workflow/service/service.go
@@ -0,0 +1,18 @@
+package service
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/domain/workflow/logic"
+)
+
+// Service workflow 应用服务。
+type Service struct {
+ Repo *logic.Repo
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(r *logic.Repo) *Service {
+ return &Service{Repo: r}
+ })
+}
diff --git a/example/http/domain/workflow/workflow.go b/example/http/domain/workflow/workflow.go
deleted file mode 100644
index 6999659..0000000
--- a/example/http/domain/workflow/workflow.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Package workflow 提供工作流域的示例组件:配置 → 客户端 → 仓储 → 服务 → 处理器
-// 五层链路,外加多区域命名空间连接(演示对象规模与模块级视图)。
-package workflow
-
-import (
- "github.com/pubgo/dix/v2"
-)
-
-// Config 工作流配置。
-type Config struct {
- Env string
- Timeout string
-}
-
-// Client 工作流下游客户端。
-type Client struct {
- Config *Config
-}
-
-// Repo 工作流仓储层。
-type Repo struct {
- Client *Client
-}
-
-// Service 工作流服务层。
-type Service struct {
- Repo *Repo
-}
-
-// Handler 工作流协议处理器。
-type Handler struct {
- Service *Service
-}
-
-// Regions 多区域连接(命名空间 map:一次 provider 产出多个对象)。
-type Regions map[string]*Client
-
-// Providers 注册工作流域的全部 provider。
-func Providers(di *dix.Dix) {
- dix.Provide(di, func() *Config {
- return &Config{Env: "prod", Timeout: "3s"}
- })
- dix.Provide(di, func(c *Config) *Client {
- return &Client{Config: c}
- })
- dix.Provide(di, func(c *Config) Regions {
- return Regions{
- "cn": {Config: c},
- "us": {Config: c},
- "eu": {Config: c},
- }
- })
- dix.Provide(di, func(c *Client) *Repo {
- return &Repo{Client: c}
- })
- dix.Provide(di, func(r *Repo) *Service {
- return &Service{Repo: r}
- })
- dix.Provide(di, func(s *Service) *Handler {
- return &Handler{Service: s}
- })
-}
diff --git a/example/http/infra/diag/diag.go b/example/http/infra/diag/diag.go
new file mode 100644
index 0000000..bb5c747
--- /dev/null
+++ b/example/http/infra/diag/diag.go
@@ -0,0 +1,150 @@
+package diag
+
+import (
+ "errors"
+ "log"
+ "time"
+
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/infra/logger"
+)
+
+// SlowRemoteClient 模拟外部慢依赖(触发 provider_timeout)。
+type SlowRemoteClient struct{ Ready bool }
+
+// TimeoutProbe 触发 SlowRemoteClient 的解析(金字塔 entry #3)。
+type TimeoutProbe struct{ Client *SlowRemoteClient }
+
+// StartupMissingDependency 模拟注入缺失依赖。
+type StartupMissingDependency struct{}
+
+// StartupResolveInputMissing 模拟 provider 输入依赖缺失。
+type StartupResolveInputMissing struct{}
+
+// StartupResolveInputProbe 用于触发 StartupResolveInputMissing 的解析。
+type StartupResolveInputProbe struct{}
+
+// StartupBrokenComponent 模拟 provider 返回 error。
+type StartupBrokenComponent struct{}
+
+// StartupPanicComponent 模拟 provider panic。
+type StartupPanicComponent struct{}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(log logger.Logger) *SlowRemoteClient {
+ log.Info("[demo] SlowRemoteClient start (expected timeout)")
+ time.Sleep(450 * time.Millisecond)
+ return &SlowRemoteClient{Ready: true}
+ })
+ dix.Provide(di, func(client *SlowRemoteClient) *TimeoutProbe {
+ return &TimeoutProbe{Client: client}
+ })
+}
+
+func logStartupScenarioResult(di *dix.Dix, scenario string, err error, previousCount int) {
+ if err == nil {
+ return
+ }
+ recent := di.GetRecentErrors(0)
+ if len(recent) == 0 {
+ log.Printf("⚠️ [startup-diagnostic][%s] err=%v (no recent error records)", scenario, err)
+ return
+ }
+ newCount := len(recent) - previousCount
+ if newCount <= 0 {
+ newCount = 1
+ }
+ if newCount > len(recent) {
+ newCount = len(recent)
+ }
+ log.Printf("⚠️ [startup-diagnostic][%s] captured %d record(s)", scenario, newCount)
+ for i := newCount - 1; i >= 0; i-- {
+ item := recent[i]
+ log.Printf(" - record=%d type=%s op=%s message=%s",
+ newCount-i, item.ErrorType, item.Operation, item.Message)
+ if item.Hint != "" {
+ log.Printf(" hint=%s", item.Hint)
+ }
+ }
+}
+
+// RunStartupErrorScenarios 在启动阶段统一触发多类可识别错误。
+func RunStartupErrorScenarios(di *dix.Dix) {
+ log.Println("🧪 Running startup error diagnostics...")
+
+ before := len(di.GetRecentErrors(0))
+ if err := di.TryProvide(nil); err != nil {
+ logStartupScenarioResult(di, "invalid_provider_registration", err, before)
+ }
+
+ before = len(di.GetRecentErrors(0))
+ if err := di.TryInject(func(*StartupMissingDependency) {}); err != nil {
+ logStartupScenarioResult(di, "inject_missing_dependency", err, before)
+ }
+
+ {
+ tmp := dix.New()
+ logger.Provide(tmp)
+ dix.Provide(tmp, func(*StartupResolveInputMissing) *StartupResolveInputProbe {
+ return &StartupResolveInputProbe{}
+ })
+ before = len(tmp.GetRecentErrors(0))
+ if err := tmp.TryInject(func(*StartupResolveInputProbe) {}); err != nil {
+ logStartupScenarioResult(tmp, "provider_input_unresolved", err, before)
+ }
+ }
+
+ {
+ tmp := dix.New()
+ logger.Provide(tmp)
+ dix.Provide(tmp, func(log logger.Logger) (*StartupBrokenComponent, error) {
+ log.Info("[demo] StartupBrokenComponent returns intentional error")
+ return nil, errors.New("demo startup: provider return error")
+ })
+ before = len(tmp.GetRecentErrors(0))
+ if err := tmp.TryInject(func(*StartupBrokenComponent) {}); err != nil {
+ logStartupScenarioResult(tmp, "provider_return_error", err, before)
+ }
+ }
+
+ before = len(di.GetRecentErrors(0))
+ if err := di.TryInject(func(log logger.Logger) error {
+ log.Info("[demo] inject callback returns intentional error")
+ return errors.New("demo startup: inject callback error")
+ }); err != nil {
+ logStartupScenarioResult(di, "inject_callback_error", err, before)
+ }
+
+ {
+ tmp := dix.New()
+ logger.Provide(tmp)
+ dix.Provide(tmp, func(log logger.Logger) *StartupPanicComponent {
+ log.Info("[demo] StartupPanicComponent panics intentionally")
+ panic("demo startup: provider panic")
+ })
+ before = len(tmp.GetRecentErrors(0))
+ if err := tmp.TryInject(func(*StartupPanicComponent) {}); err != nil {
+ logStartupScenarioResult(tmp, "provider_panic", err, before)
+ }
+ }
+
+ before = len(di.GetRecentErrors(0))
+ if err := di.TryInject(func(*TimeoutProbe) {}); err != nil {
+ logStartupScenarioResult(di, "provider_timeout", err, before)
+ }
+
+ cycleDI := dix.New()
+ type cycleA struct{}
+ type cycleB struct{}
+ type cycleC struct{}
+ dix.Provide(cycleDI, func(*cycleC) *cycleA { return &cycleA{} })
+ dix.Provide(cycleDI, func(*cycleA) *cycleB { return &cycleB{} })
+ dix.Provide(cycleDI, func(*cycleB) *cycleC { return &cycleC{} })
+ beforeCycle := len(cycleDI.GetRecentErrors(0))
+ if err := cycleDI.TryInject(func(*cycleA) {}); err != nil {
+ logStartupScenarioResult(cycleDI, "dependency_cycle(temp_container)", err, beforeCycle)
+ }
+
+ log.Println("🧪 Startup diagnostics done. Visit /api/errors to verify error_type/hint recognition.")
+}
diff --git a/example/http/infra/logger/logger.go b/example/http/infra/logger/logger.go
new file mode 100644
index 0000000..f61c1ec
--- /dev/null
+++ b/example/http/infra/logger/logger.go
@@ -0,0 +1,25 @@
+package logger
+
+import (
+ "log"
+
+ "github.com/pubgo/dix/v2"
+)
+
+// Logger 全局日志接口。
+type Logger interface {
+ Info(msg string)
+ Error(msg string)
+}
+
+// ConsoleLogger 默认实现。
+type ConsoleLogger struct{ Prefix string }
+
+func (c *ConsoleLogger) Info(msg string) { log.Printf("[INFO] %s", msg) }
+func (c *ConsoleLogger) Error(msg string) { log.Printf("[ERROR] %s", msg) }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() Logger {
+ return &ConsoleLogger{Prefix: "app"}
+ })
+}
diff --git a/example/http/main.go b/example/http/main.go
index 8229662..0bc233f 100644
--- a/example/http/main.go
+++ b/example/http/main.go
@@ -1,11 +1,8 @@
// 【功能】dixhttp 依赖图可视化:大规模端到端综合示例。
//
-// 【原理】构造真实项目规模的容器(十个域模块 + 泛型插件/工作器族,
-// 约 200 个 provider、300 个对象),并触发多类可诊断错误,用于:
-// - 体验五视图 UI(概览/依赖图/检索/调用链/诊断)的交互流程;
-// - 验证大规模下"模块下钻 + 邻域子图 + 服务端检索"的可用性。
-//
-// 入门请先看 example/inject-func 与 example/inject-struct。
+// 【原理】按微服务分层组织包(bootstrap / app / router / infra / plugins /
+// domain/*/models|infra|logic|service|handler),构造真实项目规模的容器并
+// 触发多类可诊断错误,用于验证 legacy `/` UI。
//
// 【运行】
//
@@ -14,340 +11,26 @@
//
// 【可选环境变量】
//
-// DIX_HTTP_ADDR=:8080 # 服务监听地址
-// DIX_TRACE_DI=true # 控制台逐步 DI trace
-// DIX_DIAG_FILE=.local/dix-diag.jsonl # JSONL 诊断文件
-//
-// 【预期行为】启动后打印 API 端点清单并阻塞服务,
-// 浏览器打开 http://localhost:8080 查看依赖图与诊断页。
+// DIX_HTTP_ADDR=:8080
+// DIX_TRACE_DI=true
+// DIX_DIAG_FILE=.local/dix-diag.jsonl
package main
import (
- "errors"
"log"
- "net"
- "net/http"
- "os"
- "time"
"github.com/pubgo/dix/v2"
- "github.com/pubgo/dix/v2/dixhttp"
- "github.com/pubgo/dix/example/http/domain/analytics"
- "github.com/pubgo/dix/example/http/domain/billing"
- "github.com/pubgo/dix/example/http/domain/identity"
- "github.com/pubgo/dix/example/http/domain/inventory"
- "github.com/pubgo/dix/example/http/domain/media"
- "github.com/pubgo/dix/example/http/domain/notification"
- "github.com/pubgo/dix/example/http/domain/searchx"
- "github.com/pubgo/dix/example/http/domain/shipping"
- "github.com/pubgo/dix/example/http/domain/storage"
- "github.com/pubgo/dix/example/http/domain/workflow"
+ "github.com/pubgo/dix/example/http/bootstrap"
)
-// Logger 全局日志接口。
-type Logger interface {
- Info(msg string)
- Error(msg string)
-}
-
-// ConsoleLogger 默认实现。
-type ConsoleLogger struct{ Prefix string }
-
-func (c *ConsoleLogger) Info(msg string) { log.Printf("[INFO] %s", msg) }
-func (c *ConsoleLogger) Error(msg string) { log.Printf("[ERROR] %s", msg) }
-
-const defaultHTTPAddr = ":8080"
-
-// ==================== 诊断演示用的合成组件 ====================
-
-// SlowRemoteClient 模拟外部慢依赖(触发 provider_timeout)。
-type SlowRemoteClient struct{ Ready bool }
-
-// TimeoutProbe 触发 SlowRemoteClient 的解析。
-type TimeoutProbe struct{ Client *SlowRemoteClient }
-
-// StartupMissingDependency 模拟注入缺失依赖。
-type StartupMissingDependency struct{}
-
-// StartupResolveInputMissing 模拟 provider 输入依赖缺失。
-type StartupResolveInputMissing struct{}
-
-// StartupResolveInputProbe 用于触发 StartupResolveInputMissing 的解析。
-type StartupResolveInputProbe struct{}
-
-// StartupBrokenComponent 模拟 provider 返回 error。
-type StartupBrokenComponent struct{}
-
-// StartupPanicComponent 模拟 provider panic。
-type StartupPanicComponent struct{}
-
-// ==================== 应用主结构 ====================
-
-// Application 应用聚合根:引用十个域模块的服务,是整棵依赖图的汇点。
-type Application struct {
- Logger Logger
- Billing *billing.Service
- Inventory *inventory.Service
- Shipping *shipping.Service
- Identity *identity.Service
- Analytics *analytics.Service
- Notify *notification.Service
- Search *searchx.Service
- Storage *storage.Service
- Media *media.Service
- Workflow *workflow.Service
- Plugins []string
-}
-
-// ==================== HTTP 服务器 ====================
-
-func startVisualizationServer(server *dixhttp.Server) error {
- addr := os.Getenv("DIX_HTTP_ADDR")
- if addr == "" {
- addr = defaultHTTPAddr
- }
-
- ln, err := net.Listen("tcp", addr)
- if err != nil {
- if addr == defaultHTTPAddr {
- log.Printf("⚠️ Port %s unavailable (%v), trying a random available port...", addr, err)
- ln, err = net.Listen("tcp", ":0")
- }
- if err != nil {
- return err
- }
- }
-
- actualAddr := ln.Addr().String()
- displayAddr := actualAddr
- if _, port, splitErr := net.SplitHostPort(actualAddr); splitErr == nil && port != "" {
- displayAddr = "localhost:" + port
- }
-
- log.Printf("🚀 Starting HTTP server on http://%s", displayAddr)
- log.Printf("📊 Open http://%s in your browser: overview / graph / search / trace / diag", displayAddr)
- log.Println("📡 API endpoints:")
- log.Println(" - GET /api/dependencies - JSON data of dependencies")
- log.Println(" - GET /api/modules - module-level aggregation")
- log.Println(" - GET /api/ego - neighborhood subgraph")
- log.Println(" - GET /api/search - server-side graph search")
- log.Println(" - GET /api/stats - overview statistics")
- log.Println(" - GET /api/runtime-stats - provider startup timings")
- log.Println(" - GET /api/errors - recent inject errors")
- log.Println(" - GET /api/diagnostics - DIX_DIAG_FILE records")
- log.Println(" - GET /api/trace - dixtrace event query")
- log.Println(" - GET /api/trace-tree - nested call tree per trace")
- return (&http.Server{Handler: server}).Serve(ln)
-}
-
-// ==================== 启动诊断场景 ====================
-
-func logStartupScenarioResult(di *dix.Dix, scenario string, err error, previousCount int) {
- if err == nil {
- return
- }
-
- recent := di.GetRecentErrors(0)
- if len(recent) == 0 {
- log.Printf("⚠️ [startup-diagnostic][%s] err=%v (no recent error records)", scenario, err)
- return
- }
-
- newCount := len(recent) - previousCount
- if newCount <= 0 {
- newCount = 1
- }
- if newCount > len(recent) {
- newCount = len(recent)
- }
-
- log.Printf("⚠️ [startup-diagnostic][%s] captured %d record(s)", scenario, newCount)
- for i := newCount - 1; i >= 0; i-- {
- item := recent[i]
- log.Printf(" - record=%d type=%s op=%s message=%s",
- newCount-i, item.ErrorType, item.Operation, item.Message)
- if item.Hint != "" {
- log.Printf(" hint=%s", item.Hint)
- }
- }
-}
-
-// runStartupErrorScenarios 在启动阶段统一触发多类可识别错误,
-// 验证 /api/errors 的 error_type/hint 识别与定位能力。
-func runStartupErrorScenarios(di *dix.Dix) {
- log.Println("🧪 Running startup error diagnostics...")
-
- before := len(di.GetRecentErrors(0))
- if err := di.TryProvide(nil); err != nil {
- logStartupScenarioResult(di, "invalid_provider_registration", err, before)
- }
-
- before = len(di.GetRecentErrors(0))
- if err := di.TryInject(func(*StartupMissingDependency) {}); err != nil {
- logStartupScenarioResult(di, "inject_missing_dependency", err, before)
- }
-
- dix.Provide(di, func(*StartupResolveInputMissing) *StartupResolveInputProbe {
- return &StartupResolveInputProbe{}
- })
- before = len(di.GetRecentErrors(0))
- if err := di.TryInject(func(*StartupResolveInputProbe) {}); err != nil {
- logStartupScenarioResult(di, "provider_input_unresolved", err, before)
- }
-
- dix.Provide(di, func(logger Logger) (*StartupBrokenComponent, error) {
- logger.Info("[demo] StartupBrokenComponent returns intentional error")
- return nil, errors.New("demo startup: provider return error")
- })
- before = len(di.GetRecentErrors(0))
- if err := di.TryInject(func(*StartupBrokenComponent) {}); err != nil {
- logStartupScenarioResult(di, "provider_return_error", err, before)
- }
-
- before = len(di.GetRecentErrors(0))
- if err := di.TryInject(func(logger Logger) error {
- logger.Info("[demo] inject callback returns intentional error")
- return errors.New("demo startup: inject callback error")
- }); err != nil {
- logStartupScenarioResult(di, "inject_callback_error", err, before)
- }
-
- dix.Provide(di, func(logger Logger) *StartupPanicComponent {
- logger.Info("[demo] StartupPanicComponent panics intentionally")
- panic("demo startup: provider panic")
- })
- before = len(di.GetRecentErrors(0))
- if err := di.TryInject(func(*StartupPanicComponent) {}); err != nil {
- logStartupScenarioResult(di, "provider_panic", err, before)
- }
-
- before = len(di.GetRecentErrors(0))
- if err := di.TryInject(func(*TimeoutProbe) {}); err != nil {
- logStartupScenarioResult(di, "provider_timeout", err, before)
- }
-
- // 循环依赖演示使用临时容器,避免污染主容器。
- cycleDI := dix.New()
- type cycleA struct{}
- type cycleB struct{}
- type cycleC struct{}
- dix.Provide(cycleDI, func(*cycleC) *cycleA { return &cycleA{} })
- dix.Provide(cycleDI, func(*cycleA) *cycleB { return &cycleB{} })
- dix.Provide(cycleDI, func(*cycleB) *cycleC { return &cycleC{} })
- beforeCycle := len(cycleDI.GetRecentErrors(0))
- if err := cycleDI.TryInject(func(*cycleA) {}); err != nil {
- logStartupScenarioResult(cycleDI, "dependency_cycle(temp_container)", err, beforeCycle)
- }
-
- log.Println("🧪 Startup diagnostics done. Visit /api/errors to verify error_type/hint recognition.")
-}
-
-// buildContainer 注册示例的全部组件并返回容器:
-// 十个域模块(billing/workflow/…) + 泛型插件/工作器族 + 聚合根 Application,
-// 另含用于诊断演示的慢依赖/错误场景 provider。
-// 装配契约由 main_test.go 的 TestBuildContainerWiresApplication 锁定。
+// buildContainer 测试入口,转发到 bootstrap.Build。
func buildContainer() *dix.Dix {
- di := dix.New(
- dix.WithProviderTimeout(200*time.Millisecond),
- dix.WithSlowProviderThreshold(80*time.Millisecond),
- )
-
- // 基础组件
- dix.Provide(di, func() Logger { return &ConsoleLogger{Prefix: "app"} })
-
- // 十个域模块:每域五层链路(配置→客户端→仓储→服务→处理器)+ 多区域连接
- analytics.Providers(di)
- billing.Providers(di)
- identity.Providers(di)
- inventory.Providers(di)
- media.Providers(di)
- notification.Providers(di)
- searchx.Providers(di)
- shipping.Providers(di)
- storage.Providers(di)
- workflow.Providers(di)
-
- // 泛型插件/工作器族(百余合成依赖,形成 Plugin -> Worker 链路)
- activators := registerPlugins(di)
-
- // 聚合根:引用全部域服务,是依赖图的汇点
- dix.Provide(di, func(
- logger Logger,
- billing *billing.Service,
- inventory *inventory.Service,
- shipping *shipping.Service,
- identity *identity.Service,
- analytics *analytics.Service,
- notify *notification.Service,
- search *searchx.Service,
- storage *storage.Service,
- media *media.Service,
- workflow *workflow.Service,
- ) *Application {
- return &Application{
- Logger: logger,
- Billing: billing,
- Inventory: inventory,
- Shipping: shipping,
- Identity: identity,
- Analytics: analytics,
- Notify: notify,
- Search: search,
- Storage: storage,
- Media: media,
- Workflow: workflow,
- Plugins: pluginNames,
- }
- })
-
- // 预创建对象:执行插件 provider,让 objects 视图有内容
- for _, activate := range activators {
- activate()
- }
-
- // 模拟慢依赖:刻意超过 ProviderTimeout(用于可视化排查)
- dix.Provide(di, func(logger Logger) *SlowRemoteClient {
- logger.Info("[demo] SlowRemoteClient start (expected timeout)")
- time.Sleep(450 * time.Millisecond)
- return &SlowRemoteClient{Ready: true}
- })
- dix.Provide(di, func(client *SlowRemoteClient) *TimeoutProbe {
- return &TimeoutProbe{Client: client}
- })
-
- // 错误场景 provider(诊断演示)
- dix.Provide(di, func(logger Logger) (*StartupBrokenComponent, error) {
- return nil, errors.New("demo startup: provider return error")
- })
- dix.Provide(di, func(logger Logger) *StartupPanicComponent {
- panic("demo startup: provider panic")
- })
-
- return di
-}
-
-// preCreateObjects 通过函数注入触发核心对象创建,
-// 让 dixhttp 的 objects 视图在启动后即有内容可展示。
-func preCreateObjects(di *dix.Dix) {
- if err := di.TryInject(func(app *Application, logger Logger) {
- log.Printf("✅ Application created: modules=10 plugins=%d", len(app.Plugins))
- }); err != nil {
- log.Printf("⚠️ pre-create injection failed: %v", err)
- }
+ return bootstrap.Build()
}
func main() {
- di := buildContainer()
- preCreateObjects(di)
-
- // 启动阶段触发多类可识别错误,验证 error_type/hint 识别
- runStartupErrorScenarios(di)
-
- log.Println("")
-
- server := dixhttp.NewServer(di)
- if err := startVisualizationServer(server); err != nil && err != http.ErrServerClosed {
+ if err := bootstrap.Run(); err != nil {
log.Fatal("Server error:", err)
}
}
diff --git a/example/http/main_test.go b/example/http/main_test.go
index 2d5b236..48f7e6c 100644
--- a/example/http/main_test.go
+++ b/example/http/main_test.go
@@ -1,46 +1,48 @@
package main
-import "testing"
+import (
+ "testing"
-// 锁定 example/http 的端到端装配契约:十个域模块 + 插件族 + 聚合根,
-// 全链路(Config → Client → Repo → Service → Handler)可解析、已实例化。
+ "github.com/pubgo/dix/example/http/app"
+)
+
+// 锁定 example/http 的端到端装配契约:十个域模块 + 插件族 + 聚合根。
func TestBuildContainerWiresApplication(t *testing.T) {
di := buildContainer()
- var app *Application
- if err := di.TryInject(func(a *Application) { app = a }); err != nil {
+ var application *app.Application
+ if err := di.TryInject(func(a *app.Application) { application = a }); err != nil {
t.Fatalf("TryInject(Application): %v", err)
}
- if app.Logger == nil {
+ if application.Logger == nil {
t.Fatal("logger not wired")
}
services := map[string]any{
- "billing": app.Billing,
- "inventory": app.Inventory,
- "shipping": app.Shipping,
- "identity": app.Identity,
- "analytics": app.Analytics,
- "notify": app.Notify,
- "search": app.Search,
- "storage": app.Storage,
- "media": app.Media,
- "workflow": app.Workflow,
+ "billing": application.Billing,
+ "inventory": application.Inventory,
+ "shipping": application.Shipping,
+ "identity": application.Identity,
+ "analytics": application.Analytics,
+ "notify": application.Notify,
+ "search": application.Search,
+ "storage": application.Storage,
+ "media": application.Media,
+ "workflow": application.Workflow,
}
for name, svc := range services {
if svc == nil {
t.Fatalf("domain service %s not wired", name)
}
}
- if len(app.Plugins) != 120 {
- t.Fatalf("plugins = %d, want 120", len(app.Plugins))
+ if len(application.Plugins) != 40 {
+ t.Fatalf("plugins = %d, want 40 (20 keys + 20 worker names)", len(application.Plugins))
}
- // 领域链路抽检:计费服务 → 仓储 → 客户端 → 配置
- if app.Billing.Repo == nil || app.Billing.Repo.Client == nil || app.Billing.Repo.Client.Config == nil {
- t.Fatalf("billing chain not resolved: %+v", app.Billing)
+ if application.Billing.Repo == nil || application.Billing.Repo.Client == nil || application.Billing.Repo.Client.Config == nil {
+ t.Fatalf("billing chain not resolved: %+v", application.Billing)
}
- if app.Billing.Repo.Client.Config.Env != "prod" {
- t.Fatalf("config value = %+v", app.Billing.Repo.Client.Config)
+ if application.Billing.Repo.Client.Config.Env != "prod" {
+ t.Fatalf("config value = %+v", application.Billing.Repo.Client.Config)
}
}
diff --git a/example/http/plugins.go b/example/http/plugins.go
deleted file mode 100644
index 8f23f60..0000000
--- a/example/http/plugins.go
+++ /dev/null
@@ -1,233 +0,0 @@
-package main
-
-// 本文件为批量合成的"插件/工作器"依赖族:用泛型实例化在编译期产出
-// 大量相互独立的依赖类型,把演示容器撑到真实项目规模。
-
-import "github.com/pubgo/dix/v2"
-
-// Plugin 是第一族合成依赖的载体。
-type Plugin[T any] struct {
- Name string
- Version string
-}
-
-// Worker 是第二族合成依赖的载体,依赖同角色的 Plugin(形成链路)。
-type Worker[T any] struct {
- Name string
- Batch int
-}
-
-type (
- RoleAuthReader struct{}
- RoleAuthWriter struct{}
- RoleAuthValidator struct{}
- RoleBillingReader struct{}
- RoleBillingWriter struct{}
- RoleBillingValidator struct{}
- RoleCacheReader struct{}
- RoleCacheWriter struct{}
- RoleCacheValidator struct{}
- RoleEmailReader struct{}
- RoleEmailWriter struct{}
- RoleEmailValidator struct{}
- RoleExportReader struct{}
- RoleExportWriter struct{}
- RoleExportValidator struct{}
- RoleGraphqlReader struct{}
- RoleGraphqlWriter struct{}
- RoleGraphqlValidator struct{}
- RoleImportReader struct{}
- RoleImportWriter struct{}
- RoleImportValidator struct{}
- RoleJobReader struct{}
- RoleJobWriter struct{}
- RoleJobValidator struct{}
- RoleKafkaReader struct{}
- RoleKafkaWriter struct{}
- RoleKafkaValidator struct{}
- RoleLoginReader struct{}
- RoleLoginWriter struct{}
- RoleLoginValidator struct{}
- RoleMetricsReader struct{}
- RoleMetricsWriter struct{}
- RoleMetricsValidator struct{}
- RoleNotifyReader struct{}
- RoleNotifyWriter struct{}
- RoleNotifyValidator struct{}
- RoleOauthReader struct{}
- RoleOauthWriter struct{}
- RoleOauthValidator struct{}
- RoleQueueReader struct{}
- RoleQueueWriter struct{}
- RoleQueueValidator struct{}
- RoleReportReader struct{}
- RoleReportWriter struct{}
- RoleReportValidator struct{}
- RoleSearchReader struct{}
- RoleSearchWriter struct{}
- RoleSearchValidator struct{}
- RoleSessionReader struct{}
- RoleSessionWriter struct{}
- RoleSessionValidator struct{}
- RoleTenantReader struct{}
- RoleTenantWriter struct{}
- RoleTenantValidator struct{}
- RoleUploadReader struct{}
- RoleUploadWriter struct{}
- RoleUploadValidator struct{}
- RoleVaultReader struct{}
- RoleVaultWriter struct{}
- RoleVaultValidator struct{}
-)
-
-var pluginNames []string
-
-// registerPlugin 注册一个角色插件 provider,返回用于预创建对象的激活函数。
-func registerPlugin[T any](di *dix.Dix, name string) func() {
- dix.Provide(di, func() *Plugin[T] {
- pluginNames = append(pluginNames, name)
- return &Plugin[T]{Name: name, Version: "1.0.0"}
- })
- return func() { _ = dix.Inject(di, func(p *Plugin[T]) {}) }
-}
-
-// registerWorker 注册依赖同角色插件的工作器 provider。
-func registerWorker[T any](di *dix.Dix, name string) func() {
- dix.Provide(di, func(p *Plugin[T]) *Worker[T] {
- pluginNames = append(pluginNames, name+".worker")
- return &Worker[T]{Name: name, Batch: 8}
- })
- return func() { _ = dix.Inject(di, func(w *Worker[T]) {}) }
-}
-
-// registerPlugins 批量注册角色插件与工作器,返回全部激活函数。
-func registerPlugins(di *dix.Dix) (activators []func()) {
- for _, r := range []struct {
- name string
- reg func(*dix.Dix, string) func()
- }{
- {"authreader", registerPlugin[RoleAuthReader]},
- {"authreader.worker", registerWorker[RoleAuthReader]},
- {"authwriter", registerPlugin[RoleAuthWriter]},
- {"authwriter.worker", registerWorker[RoleAuthWriter]},
- {"authvalidator", registerPlugin[RoleAuthValidator]},
- {"authvalidator.worker", registerWorker[RoleAuthValidator]},
- {"billingreader", registerPlugin[RoleBillingReader]},
- {"billingreader.worker", registerWorker[RoleBillingReader]},
- {"billingwriter", registerPlugin[RoleBillingWriter]},
- {"billingwriter.worker", registerWorker[RoleBillingWriter]},
- {"billingvalidator", registerPlugin[RoleBillingValidator]},
- {"billingvalidator.worker", registerWorker[RoleBillingValidator]},
- {"cachereader", registerPlugin[RoleCacheReader]},
- {"cachereader.worker", registerWorker[RoleCacheReader]},
- {"cachewriter", registerPlugin[RoleCacheWriter]},
- {"cachewriter.worker", registerWorker[RoleCacheWriter]},
- {"cachevalidator", registerPlugin[RoleCacheValidator]},
- {"cachevalidator.worker", registerWorker[RoleCacheValidator]},
- {"emailreader", registerPlugin[RoleEmailReader]},
- {"emailreader.worker", registerWorker[RoleEmailReader]},
- {"emailwriter", registerPlugin[RoleEmailWriter]},
- {"emailwriter.worker", registerWorker[RoleEmailWriter]},
- {"emailvalidator", registerPlugin[RoleEmailValidator]},
- {"emailvalidator.worker", registerWorker[RoleEmailValidator]},
- {"exportreader", registerPlugin[RoleExportReader]},
- {"exportreader.worker", registerWorker[RoleExportReader]},
- {"exportwriter", registerPlugin[RoleExportWriter]},
- {"exportwriter.worker", registerWorker[RoleExportWriter]},
- {"exportvalidator", registerPlugin[RoleExportValidator]},
- {"exportvalidator.worker", registerWorker[RoleExportValidator]},
- {"graphqlreader", registerPlugin[RoleGraphqlReader]},
- {"graphqlreader.worker", registerWorker[RoleGraphqlReader]},
- {"graphqlwriter", registerPlugin[RoleGraphqlWriter]},
- {"graphqlwriter.worker", registerWorker[RoleGraphqlWriter]},
- {"graphqlvalidator", registerPlugin[RoleGraphqlValidator]},
- {"graphqlvalidator.worker", registerWorker[RoleGraphqlValidator]},
- {"importreader", registerPlugin[RoleImportReader]},
- {"importreader.worker", registerWorker[RoleImportReader]},
- {"importwriter", registerPlugin[RoleImportWriter]},
- {"importwriter.worker", registerWorker[RoleImportWriter]},
- {"importvalidator", registerPlugin[RoleImportValidator]},
- {"importvalidator.worker", registerWorker[RoleImportValidator]},
- {"jobreader", registerPlugin[RoleJobReader]},
- {"jobreader.worker", registerWorker[RoleJobReader]},
- {"jobwriter", registerPlugin[RoleJobWriter]},
- {"jobwriter.worker", registerWorker[RoleJobWriter]},
- {"jobvalidator", registerPlugin[RoleJobValidator]},
- {"jobvalidator.worker", registerWorker[RoleJobValidator]},
- {"kafkareader", registerPlugin[RoleKafkaReader]},
- {"kafkareader.worker", registerWorker[RoleKafkaReader]},
- {"kafkawriter", registerPlugin[RoleKafkaWriter]},
- {"kafkawriter.worker", registerWorker[RoleKafkaWriter]},
- {"kafkavalidator", registerPlugin[RoleKafkaValidator]},
- {"kafkavalidator.worker", registerWorker[RoleKafkaValidator]},
- {"loginreader", registerPlugin[RoleLoginReader]},
- {"loginreader.worker", registerWorker[RoleLoginReader]},
- {"loginwriter", registerPlugin[RoleLoginWriter]},
- {"loginwriter.worker", registerWorker[RoleLoginWriter]},
- {"loginvalidator", registerPlugin[RoleLoginValidator]},
- {"loginvalidator.worker", registerWorker[RoleLoginValidator]},
- {"metricsreader", registerPlugin[RoleMetricsReader]},
- {"metricsreader.worker", registerWorker[RoleMetricsReader]},
- {"metricswriter", registerPlugin[RoleMetricsWriter]},
- {"metricswriter.worker", registerWorker[RoleMetricsWriter]},
- {"metricsvalidator", registerPlugin[RoleMetricsValidator]},
- {"metricsvalidator.worker", registerWorker[RoleMetricsValidator]},
- {"notifyreader", registerPlugin[RoleNotifyReader]},
- {"notifyreader.worker", registerWorker[RoleNotifyReader]},
- {"notifywriter", registerPlugin[RoleNotifyWriter]},
- {"notifywriter.worker", registerWorker[RoleNotifyWriter]},
- {"notifyvalidator", registerPlugin[RoleNotifyValidator]},
- {"notifyvalidator.worker", registerWorker[RoleNotifyValidator]},
- {"oauthreader", registerPlugin[RoleOauthReader]},
- {"oauthreader.worker", registerWorker[RoleOauthReader]},
- {"oauthwriter", registerPlugin[RoleOauthWriter]},
- {"oauthwriter.worker", registerWorker[RoleOauthWriter]},
- {"oauthvalidator", registerPlugin[RoleOauthValidator]},
- {"oauthvalidator.worker", registerWorker[RoleOauthValidator]},
- {"queuereader", registerPlugin[RoleQueueReader]},
- {"queuereader.worker", registerWorker[RoleQueueReader]},
- {"queuewriter", registerPlugin[RoleQueueWriter]},
- {"queuewriter.worker", registerWorker[RoleQueueWriter]},
- {"queuevalidator", registerPlugin[RoleQueueValidator]},
- {"queuevalidator.worker", registerWorker[RoleQueueValidator]},
- {"reportreader", registerPlugin[RoleReportReader]},
- {"reportreader.worker", registerWorker[RoleReportReader]},
- {"reportwriter", registerPlugin[RoleReportWriter]},
- {"reportwriter.worker", registerWorker[RoleReportWriter]},
- {"reportvalidator", registerPlugin[RoleReportValidator]},
- {"reportvalidator.worker", registerWorker[RoleReportValidator]},
- {"searchreader", registerPlugin[RoleSearchReader]},
- {"searchreader.worker", registerWorker[RoleSearchReader]},
- {"searchwriter", registerPlugin[RoleSearchWriter]},
- {"searchwriter.worker", registerWorker[RoleSearchWriter]},
- {"searchvalidator", registerPlugin[RoleSearchValidator]},
- {"searchvalidator.worker", registerWorker[RoleSearchValidator]},
- {"sessionreader", registerPlugin[RoleSessionReader]},
- {"sessionreader.worker", registerWorker[RoleSessionReader]},
- {"sessionwriter", registerPlugin[RoleSessionWriter]},
- {"sessionwriter.worker", registerWorker[RoleSessionWriter]},
- {"sessionvalidator", registerPlugin[RoleSessionValidator]},
- {"sessionvalidator.worker", registerWorker[RoleSessionValidator]},
- {"tenantreader", registerPlugin[RoleTenantReader]},
- {"tenantreader.worker", registerWorker[RoleTenantReader]},
- {"tenantwriter", registerPlugin[RoleTenantWriter]},
- {"tenantwriter.worker", registerWorker[RoleTenantWriter]},
- {"tenantvalidator", registerPlugin[RoleTenantValidator]},
- {"tenantvalidator.worker", registerWorker[RoleTenantValidator]},
- {"uploadreader", registerPlugin[RoleUploadReader]},
- {"uploadreader.worker", registerWorker[RoleUploadReader]},
- {"uploadwriter", registerPlugin[RoleUploadWriter]},
- {"uploadwriter.worker", registerWorker[RoleUploadWriter]},
- {"uploadvalidator", registerPlugin[RoleUploadValidator]},
- {"uploadvalidator.worker", registerWorker[RoleUploadValidator]},
- {"vaultreader", registerPlugin[RoleVaultReader]},
- {"vaultreader.worker", registerWorker[RoleVaultReader]},
- {"vaultwriter", registerPlugin[RoleVaultWriter]},
- {"vaultwriter.worker", registerWorker[RoleVaultWriter]},
- {"vaultvalidator", registerPlugin[RoleVaultValidator]},
- {"vaultvalidator.worker", registerWorker[RoleVaultValidator]},
- } {
- activators = append(activators, r.reg(di, r.name))
- }
- return activators
-}
diff --git a/example/http/plugins/auth/plugin.go b/example/http/plugins/auth/plugin.go
new file mode 100644
index 0000000..67c03ab
--- /dev/null
+++ b/example/http/plugins/auth/plugin.go
@@ -0,0 +1,30 @@
+package auth
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "auth": &plugin{name: "auth"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["auth"]
+ _ = p
+ return map[string]plugins.Worker{
+ "auth": &worker{name: "auth.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/billing/plugin.go b/example/http/plugins/billing/plugin.go
new file mode 100644
index 0000000..ee841a0
--- /dev/null
+++ b/example/http/plugins/billing/plugin.go
@@ -0,0 +1,30 @@
+package billing
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "billing": &plugin{name: "billing"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["billing"]
+ _ = p
+ return map[string]plugins.Worker{
+ "billing": &worker{name: "billing.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/cache/plugin.go b/example/http/plugins/cache/plugin.go
new file mode 100644
index 0000000..5187c27
--- /dev/null
+++ b/example/http/plugins/cache/plugin.go
@@ -0,0 +1,30 @@
+package cache
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "cache": &plugin{name: "cache"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["cache"]
+ _ = p
+ return map[string]plugins.Worker{
+ "cache": &worker{name: "cache.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/email/plugin.go b/example/http/plugins/email/plugin.go
new file mode 100644
index 0000000..dc79aa9
--- /dev/null
+++ b/example/http/plugins/email/plugin.go
@@ -0,0 +1,30 @@
+package email
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "email": &plugin{name: "email"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["email"]
+ _ = p
+ return map[string]plugins.Worker{
+ "email": &worker{name: "email.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/export/plugin.go b/example/http/plugins/export/plugin.go
new file mode 100644
index 0000000..b4fb7a3
--- /dev/null
+++ b/example/http/plugins/export/plugin.go
@@ -0,0 +1,30 @@
+package export
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "export": &plugin{name: "export"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["export"]
+ _ = p
+ return map[string]plugins.Worker{
+ "export": &worker{name: "export.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/graphql/plugin.go b/example/http/plugins/graphql/plugin.go
new file mode 100644
index 0000000..6b2438c
--- /dev/null
+++ b/example/http/plugins/graphql/plugin.go
@@ -0,0 +1,30 @@
+package graphql
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "graphql": &plugin{name: "graphql"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["graphql"]
+ _ = p
+ return map[string]plugins.Worker{
+ "graphql": &worker{name: "graphql.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/importx/plugin.go b/example/http/plugins/importx/plugin.go
new file mode 100644
index 0000000..468d673
--- /dev/null
+++ b/example/http/plugins/importx/plugin.go
@@ -0,0 +1,30 @@
+package importx
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "import": &plugin{name: "import"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["import"]
+ _ = p
+ return map[string]plugins.Worker{
+ "import": &worker{name: "import.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/job/plugin.go b/example/http/plugins/job/plugin.go
new file mode 100644
index 0000000..32b1446
--- /dev/null
+++ b/example/http/plugins/job/plugin.go
@@ -0,0 +1,30 @@
+package job
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "job": &plugin{name: "job"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["job"]
+ _ = p
+ return map[string]plugins.Worker{
+ "job": &worker{name: "job.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/kafka/plugin.go b/example/http/plugins/kafka/plugin.go
new file mode 100644
index 0000000..fb7ac59
--- /dev/null
+++ b/example/http/plugins/kafka/plugin.go
@@ -0,0 +1,30 @@
+package kafka
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "kafka": &plugin{name: "kafka"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["kafka"]
+ _ = p
+ return map[string]plugins.Worker{
+ "kafka": &worker{name: "kafka.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/login/plugin.go b/example/http/plugins/login/plugin.go
new file mode 100644
index 0000000..9d6f92f
--- /dev/null
+++ b/example/http/plugins/login/plugin.go
@@ -0,0 +1,30 @@
+package login
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "login": &plugin{name: "login"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["login"]
+ _ = p
+ return map[string]plugins.Worker{
+ "login": &worker{name: "login.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/metrics/plugin.go b/example/http/plugins/metrics/plugin.go
new file mode 100644
index 0000000..ba1b472
--- /dev/null
+++ b/example/http/plugins/metrics/plugin.go
@@ -0,0 +1,30 @@
+package metrics
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "metrics": &plugin{name: "metrics"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["metrics"]
+ _ = p
+ return map[string]plugins.Worker{
+ "metrics": &worker{name: "metrics.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/notify/plugin.go b/example/http/plugins/notify/plugin.go
new file mode 100644
index 0000000..32f9668
--- /dev/null
+++ b/example/http/plugins/notify/plugin.go
@@ -0,0 +1,30 @@
+package notify
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "notify": &plugin{name: "notify"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["notify"]
+ _ = p
+ return map[string]plugins.Worker{
+ "notify": &worker{name: "notify.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/oauth/plugin.go b/example/http/plugins/oauth/plugin.go
new file mode 100644
index 0000000..4b66498
--- /dev/null
+++ b/example/http/plugins/oauth/plugin.go
@@ -0,0 +1,30 @@
+package oauth
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "oauth": &plugin{name: "oauth"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["oauth"]
+ _ = p
+ return map[string]plugins.Worker{
+ "oauth": &worker{name: "oauth.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/plugin.go b/example/http/plugins/plugin.go
new file mode 100644
index 0000000..ba12f66
--- /dev/null
+++ b/example/http/plugins/plugin.go
@@ -0,0 +1,35 @@
+package plugins
+
+import "github.com/pubgo/dix/v2"
+
+// Plugin 插件契约:实现方注册到 map[string]Plugin 命名空间。
+type Plugin interface {
+ Name() string
+}
+
+// Worker 工作器契约:依赖同名 Plugin 命名空间,对外仍只暴露接口。
+type Worker interface {
+ Name() string
+}
+
+// Platform 聚合全部 Worker,供 Application 消费(避免多入口)。
+type Platform struct {
+ WorkerCount int
+ Names []string
+}
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func(workers map[string]Worker) *Platform {
+ names := make([]string, 0, len(workers)*2)
+ for key, w := range workers {
+ names = append(names, key)
+ if w != nil {
+ names = append(names, w.Name())
+ }
+ }
+ return &Platform{
+ WorkerCount: len(workers),
+ Names: names,
+ }
+ })
+}
diff --git a/example/http/plugins/queue/plugin.go b/example/http/plugins/queue/plugin.go
new file mode 100644
index 0000000..61acb43
--- /dev/null
+++ b/example/http/plugins/queue/plugin.go
@@ -0,0 +1,30 @@
+package queue
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "queue": &plugin{name: "queue"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["queue"]
+ _ = p
+ return map[string]plugins.Worker{
+ "queue": &worker{name: "queue.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/report/plugin.go b/example/http/plugins/report/plugin.go
new file mode 100644
index 0000000..0b1aaab
--- /dev/null
+++ b/example/http/plugins/report/plugin.go
@@ -0,0 +1,30 @@
+package report
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "report": &plugin{name: "report"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["report"]
+ _ = p
+ return map[string]plugins.Worker{
+ "report": &worker{name: "report.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/search/plugin.go b/example/http/plugins/search/plugin.go
new file mode 100644
index 0000000..7ee24fb
--- /dev/null
+++ b/example/http/plugins/search/plugin.go
@@ -0,0 +1,30 @@
+package search
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "search": &plugin{name: "search"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["search"]
+ _ = p
+ return map[string]plugins.Worker{
+ "search": &worker{name: "search.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/session/plugin.go b/example/http/plugins/session/plugin.go
new file mode 100644
index 0000000..3aa241a
--- /dev/null
+++ b/example/http/plugins/session/plugin.go
@@ -0,0 +1,30 @@
+package session
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "session": &plugin{name: "session"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["session"]
+ _ = p
+ return map[string]plugins.Worker{
+ "session": &worker{name: "session.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/tenant/plugin.go b/example/http/plugins/tenant/plugin.go
new file mode 100644
index 0000000..5a6ab51
--- /dev/null
+++ b/example/http/plugins/tenant/plugin.go
@@ -0,0 +1,30 @@
+package tenant
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "tenant": &plugin{name: "tenant"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["tenant"]
+ _ = p
+ return map[string]plugins.Worker{
+ "tenant": &worker{name: "tenant.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/upload/plugin.go b/example/http/plugins/upload/plugin.go
new file mode 100644
index 0000000..6e1a69c
--- /dev/null
+++ b/example/http/plugins/upload/plugin.go
@@ -0,0 +1,30 @@
+package upload
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "upload": &plugin{name: "upload"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["upload"]
+ _ = p
+ return map[string]plugins.Worker{
+ "upload": &worker{name: "upload.worker"},
+ }
+ })
+}
diff --git a/example/http/plugins/vault/plugin.go b/example/http/plugins/vault/plugin.go
new file mode 100644
index 0000000..8bb2e84
--- /dev/null
+++ b/example/http/plugins/vault/plugin.go
@@ -0,0 +1,30 @@
+package vault
+
+import (
+ "github.com/pubgo/dix/v2"
+
+ "github.com/pubgo/dix/example/http/plugins"
+)
+
+type plugin struct{ name string }
+
+func (p *plugin) Name() string { return p.name }
+
+type worker struct{ name string }
+
+func (w *worker) Name() string { return w.name }
+
+func Provide(di *dix.Dix) {
+ dix.Provide(di, func() map[string]plugins.Plugin {
+ return map[string]plugins.Plugin{
+ "vault": &plugin{name: "vault"},
+ }
+ })
+ dix.Provide(di, func(all map[string]plugins.Plugin) map[string]plugins.Worker {
+ p := all["vault"]
+ _ = p
+ return map[string]plugins.Worker{
+ "vault": &worker{name: "vault.worker"},
+ }
+ })
+}
diff --git a/example/http/router/server.go b/example/http/router/server.go
new file mode 100644
index 0000000..0c3b929
--- /dev/null
+++ b/example/http/router/server.go
@@ -0,0 +1,52 @@
+package router
+
+import (
+ "log"
+ "net"
+ "net/http"
+ "os"
+
+ "github.com/pubgo/dix/v2/dixhttp"
+)
+
+const defaultHTTPAddr = ":8080"
+
+// StartVisualizationServer 启动 dixhttp 可视化服务。
+func StartVisualizationServer(server *dixhttp.Server) error {
+ addr := os.Getenv("DIX_HTTP_ADDR")
+ if addr == "" {
+ addr = defaultHTTPAddr
+ }
+
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ if addr == defaultHTTPAddr {
+ log.Printf("⚠️ Port %s unavailable (%v), trying a random available port...", addr, err)
+ ln, err = net.Listen("tcp", ":0")
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ actualAddr := ln.Addr().String()
+ displayAddr := actualAddr
+ if _, port, splitErr := net.SplitHostPort(actualAddr); splitErr == nil && port != "" {
+ displayAddr = "localhost:" + port
+ }
+
+ log.Printf("🚀 Starting HTTP server on http://%s", displayAddr)
+ log.Printf("📊 Open http://%s in your browser (legacy DI architecture UI)", displayAddr)
+ log.Println("📡 API endpoints:")
+ log.Println(" - GET /api/dependencies - JSON data of dependencies")
+ log.Println(" - GET /api/modules - module-level aggregation")
+ log.Println(" - GET /api/ego - neighborhood subgraph")
+ log.Println(" - GET /api/search - server-side graph search")
+ log.Println(" - GET /api/stats - overview statistics")
+ log.Println(" - GET /api/runtime-stats - provider startup timings")
+ log.Println(" - GET /api/errors - recent inject errors")
+ log.Println(" - GET /api/diagnostics - DIX_DIAG_FILE records")
+ log.Println(" - GET /api/trace - dixtrace event query")
+ log.Println(" - GET /api/trace-tree - nested call tree per trace")
+ return (&http.Server{Handler: server}).Serve(ln)
+}
diff --git a/example/http/scale_shape_test.go b/example/http/scale_shape_test.go
new file mode 100644
index 0000000..bdb0c84
--- /dev/null
+++ b/example/http/scale_shape_test.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/pubgo/dix/v2/dixhttp"
+
+ "github.com/pubgo/dix/example/http/app"
+)
+
+func TestDemoContainerShape(t *testing.T) {
+ container := buildContainer()
+ if err := container.TryInject(func(*app.Application) {}); err != nil {
+ t.Fatal(err)
+ }
+ modules := container.ModuleGraph()
+ providers := container.GetProviderDetails()
+ objects := container.GetObjects()
+
+ objectCount := 0
+ for _, groups := range objects {
+ for _, values := range groups {
+ objectCount += len(values)
+ }
+ }
+ if len(modules) < 10 {
+ t.Fatalf("modules = %d, want at least 10", len(modules))
+ }
+ // 真实分层后规模来自 domain + plugins,不再依赖人造 ScaleFixture。
+ if len(providers) < 90 {
+ t.Fatalf("providers = %d, want at least 90", len(providers))
+ }
+ if objectCount < 80 {
+ t.Fatalf("objects = %d, want at least 80", objectCount)
+ }
+}
+
+func TestPyramidHasTwoBusinessEntries(t *testing.T) {
+ container := buildContainer()
+ server := dixhttp.NewServer(container)
+ rec := httptest.NewRecorder()
+ server.ServeHTTP(rec, httptest.NewRequest("GET", "/api/dependencies", nil))
+ if rec.Code != 200 {
+ t.Fatalf("status = %d", rec.Code)
+ }
+ var payload struct {
+ Providers []struct {
+ ID string `json:"id"`
+ OutputType string `json:"output_type"`
+ InputTypes []string `json:"input_types"`
+ } `json:"providers"`
+ }
+ if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
+ t.Fatal(err)
+ }
+
+ typeProducers := map[string][]string{}
+ for _, p := range payload.Providers {
+ out := p.OutputType
+ if out == "" {
+ continue
+ }
+ typeProducers[out] = append(typeProducers[out], p.ID)
+ }
+ consumed := map[string]bool{}
+ for _, p := range payload.Providers {
+ for _, in := range p.InputTypes {
+ for _, producerID := range typeProducers[in] {
+ if producerID != p.ID {
+ consumed[producerID] = true
+ }
+ }
+ }
+ }
+
+ var entries []string
+ for _, p := range payload.Providers {
+ if consumed[p.ID] {
+ continue
+ }
+ if strings.Contains(p.OutputType, "dixinternal") {
+ continue
+ }
+ entries = append(entries, p.OutputType)
+ }
+ if len(entries) != 2 {
+ t.Fatalf("business pyramid entries = %d (%v), want exactly 2 (Application, TimeoutProbe)", len(entries), entries)
+ }
+ want := map[string]bool{
+ "Application": false,
+ "TimeoutProbe": false,
+ }
+ for _, e := range entries {
+ switch {
+ case strings.Contains(e, "Application"):
+ want["Application"] = true
+ case strings.Contains(e, "TimeoutProbe"):
+ want["TimeoutProbe"] = true
+ }
+ }
+ for name, ok := range want {
+ if !ok {
+ t.Fatalf("missing expected entry kind %s in %v", name, entries)
+ }
+ }
+}