Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9dbb3d6
docs(dixhttp): plan scale-safe dependency graph
kooksee Sep 5, 2026
a153497
feat(dixinternal): add provider registration identity
kooksee Sep 5, 2026
39e62a5
fix(dixhttp): preserve provider identity and dependency edges
kooksee Sep 5, 2026
e340002
fix(dixinternal): identify runtime stats by provider output
kooksee Sep 5, 2026
2983318
fix(dixhttp): correct graph projection state and modules
kooksee Sep 5, 2026
3fa8c74
feat(dixhttp): default graph to bounded module map
kooksee Sep 5, 2026
71386b1
fix(dixhttp): keep scaled graph views usable
kooksee Sep 5, 2026
135e4d6
docs(dixhttp): document scale-first dependency workflow
kooksee Sep 5, 2026
ff7dbf0
fix(dixhttp): stabilize module graph initial draw
kooksee Sep 5, 2026
386e5b0
feat(dixhttp): add issue-first diagnostic feed
kooksee Sep 5, 2026
44427ae
feat(dixhttp): add bounded module drilldown api
kooksee Sep 5, 2026
1d8e8b2
feat(dixhttp): wire issue feed and module drilldown
kooksee Sep 5, 2026
7a7c524
fix(dixhttp): cancel stale graph requests
kooksee Sep 5, 2026
031b1eb
Revert "fix(dixhttp): cancel stale graph requests"
kooksee Sep 5, 2026
f039235
fix(dixhttp): cancel stale graph requests safely
kooksee Sep 5, 2026
740b37a
fix(dixhttp): align module graph budget and issue links
kooksee Sep 5, 2026
a053833
feat(dixhttp): show hub table when graph budget truncates
kooksee Sep 5, 2026
1ea662a
feat(dixhttp): prefilter trace view from issue hash
kooksee Sep 5, 2026
374c5a5
feat(dixhttp): jump from graph drawer to filtered trace
kooksee Sep 5, 2026
e6b6448
feat(dixhttp): propagate optional trace id on issues
kooksee Sep 5, 2026
fc8bccf
docs(dixhttp): document unified dependency diagnosis workflow
kooksee Sep 5, 2026
0a7b285
fix(dixhttp): make next graph layouts readable at scale
kooksee Sep 5, 2026
75289df
fix(dixhttp): star-layout module map with layout e2e checks
kooksee Sep 5, 2026
ae3a24a
chore(dixhttp): load shared graph helpers in legacy UI
kooksee Sep 6, 2026
712b50d
chore(dixhttp): track graph_workbench helpers for legacy embed
kooksee Sep 6, 2026
30f471c
feat(dixhttp): shorten legacy graph labels via shared helper
kooksee Sep 6, 2026
04bcc4f
feat(dixhttp): add module map lens to legacy graph UI
kooksee Sep 6, 2026
2cedbaa
feat(dixhttp): density banner, readable layout, and package scope fra…
kooksee Sep 6, 2026
0f9cb05
chore(dixhttp): remove /next shell; legacy UI is sole entry
kooksee Sep 6, 2026
53f192f
fix(dixhttp): keep density tip floating so it does not cover the graph
kooksee Sep 6, 2026
984ae86
fix(dixhttp): make package sidebar filter match OutputPkg paths
kooksee Sep 6, 2026
f30b22d
fix(dixhttp): tighten package filter path matching
kooksee Sep 6, 2026
4872a13
fix(dixhttp): disambiguate labels and add zoom LOD for dense graphs
kooksee Sep 6, 2026
ef60154
feat(http): add inventory UI and hierarchical layout enhancements
kooksee Sep 8, 2026
45c9199
feat: add architecture findings view and exact hide seeds
kooksee Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*.test

# Dependency directories (remove the comment below to include it)
vendor
/vendor
.env
.vscode/
.local/
Expand Down
4 changes: 2 additions & 2 deletions .version/changelog/Unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- 运行时依赖图 Graph(P1):Provide 增量维护声明边/执行计数,环检测改读增量图;`/api/search`、`/api/modules`、`/api/ego` 服务端检索与分层查询;dixhttp 依赖数据按图版本快照缓存(热请求零反射)
- trace 容器化(P2):事件携带随机 container_id 实现多容器隔离,TraceID 改为随机 32 hex;`WithTraceBuffer(n)` 私有 trace 缓冲;`Dix.TraceTree` 与 `/api/trace-tree` 嵌套调用树查询,`/api/trace` 支持 container_id 过滤
- 大规模演示容器 example/http:10 个域模块 + 120 个泛型插件/工作器,约 190 provider / 190+ 对象 / 12 模块
- 五视图实验版 UI(`/next`):概览/依赖图/检索/调用链/诊断,本地静态资源零 CDN
- legacy `/` 架构可视化:模块地图、短标签、密度横幅/耦合枢纽、可读布局;共享 `graph_state.mjs` helpers

## 修复

Expand All @@ -21,7 +21,7 @@
## 变更

- DI 点事件统一走 tracer 事件流(console `di_trace` 与 diag file 成为订阅者,输出契约不变);直接移除独立 LLM 诊断通道(`DIX_LLM_DIAG_MODE`、stderr `DIX_LLM_DIAG` 行、diag `kind:llm`),`error_type`/`root_cause`/`hint` 结构化字段在全部出口保留
- 默认 Web UI 恢复 v2.0.2 交互版本(完整依赖图、双视图、节点详情、Mermaid/SVG 导出、Trace 诊断),五视图新版 UI 移至 `/next` 实验入口
- 默认 Web UI 为 legacy `/`(完整依赖图、模块地图、分组聚合、Mermaid/SVG、Trace 诊断);已移除 `/next` 五视图实验壳
- example/http 升级为大规模演示容器;任务 `task web-demo` 改为整包构建

## 文档
Expand Down
42 changes: 26 additions & 16 deletions dixhttp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,35 @@ This module provides an HTTP server to visualize dependency relationships in the

[中文文档](./README_zh.md)

## UI 信息架构(五视图)
## UI 信息架构

前端为无构建的本地静态资源(`static/`:原生 JS + 自绘 CSS + 本地 vendored vis-network,**零 CDN 依赖**),按任务组织为五个视图:
主界面为 legacy `/`(Alpine + vis-network + 本地静态资源)。面向 DI **架构审查**:包/模块边界、Providers、类型依赖、业务分组聚合。

| 视图(hash 路由) | 主任务 | 数据源 |
| --- | --- | --- |
| 概览 `#/overview` | 全局状态一览 | `/api/stats`、`/api/errors` |
| 依赖图 `#/graph` | 模块列表 + 任意类型为中心的邻域子图 | `/api/modules`、`/api/search`、`/api/ego` |
| 检索 `#/search` | 服务端检索 + 状态过滤,一键跳转依赖图 | `/api/search` |
| 调用链 `#/trace` | trace 列表(错误优先)+ 嵌套调用树 | `/api/trace`、`/api/trace-tree` |
| 诊断 `#/diag` | 最近注入错误 + provider 启动耗时 | `/api/errors`、`/api/runtime-stats` |
| 能力 | 说明 |
| --- | --- |
| Providers / 类型依赖 | 现有全局构图;默认仍可进 Providers |
| 模块地图 | `/api/modules` 跨模块边界总览;双击下钻到包前缀过滤的 Providers |
| 包范围侧栏 | 选择包以缩小架构切片(前缀过滤) |
| 按分组聚合 | `dix.groupRules.v1` / `/api/group-rules` |
| 规模提示 | 超可读预算时横幅建议换粒度,列出耦合枢纽,最后才 Top-K 裁点 |
| Trace / 诊断 | 页内弹层;排障主路径仍是调用链 |

Object(已创建实例)默认不作为架构画布节点,只在统计/详情中展示。

## Scale-first graph workflow

```text
Module map / group aggregate -> scoped Providers or Types -> detail / Trace
```

- The default graph mode remains Providers (legacy habit); crowding is handled by semantic prompts, hubs, short labels, readable layout, then truncation.
- Module map is bounded by module count; Providers/Types use a soft on-canvas readability cap (~40 nodes) with hub listing.
- Objects are shown as state in details and counts; they are not rendered as architecture nodes by default.
- `/api/dependencies` remains the full-data endpoint for Providers/Types views.
- Visualization UI entry is `/` only (`/next` removed).
## Features

- 📊 **Interactive Visualization** - Modern UI built with vis.js + Tailwind CSS + Alpine.js
- 📊 **Interactive Visualization** - Modern no-build UI built with vanilla JS, local CSS, and vis-network
- 🔍 **Global Fuzzy Search** - Quickly search for type names or function names to view dependencies
- 📦 **Package Grouping** - Collapsible left panel to browse by package
- 🔄 **Bidirectional Dependency Tracking** - Show both upstream (dependencies) and downstream (dependents)
Expand Down Expand Up @@ -425,7 +438,7 @@ Returns package list
```

### GET `/api/dependencies?package=xxx&limit=100`
Returns dependency data, supports package filtering
Returns full dependency data for global/legacy views and package filtering. New scale-first module and ego views use their own bounded endpoints first.

```json
{
Expand Down Expand Up @@ -476,11 +489,8 @@ Returns backend-registered group rules (used as UI defaults)
## Tech Stack

- **Backend**: Go standard library `net/http`
- **Frontend**:
- [Tailwind CSS](https://tailwindcss.com/) - Styling
- [Alpine.js](https://alpinejs.dev/) - Reactive interactions
- [vis-network](https://visjs.github.io/vis-network/) - Graph rendering
- **Template**: Go embed single-file HTML
- **Frontend**: vanilla JS, local CSS, and [vis-network](https://visjs.github.io/vis-network/) for graph rendering
- **Assets**: Go-embedded local files; no build step and no CDN dependency

## Use Cases

Expand Down
25 changes: 18 additions & 7 deletions dixhttp/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@

[English](./README.md)

## 规模优先工作流

主界面为 legacy `/`。面向 DI 架构审查:

```text
模块地图 / 分组聚合 -> 范围内的 Providers 或类型依赖 -> 详情 / Trace
```

- 默认仍可进全局 Providers;超规模时提示换粒度、标耦合枢纽、短标签与可读布局,最后才裁点。
- 模块地图看跨边界依赖;包侧栏用于缩小架构切片。
- Object 默认不上架构画布,只在统计/详情中展示。
- `/api/dependencies` 仍是 Providers/类型全量数据接口。
- 可视化入口仅为 `/`(已移除 `/next`)。

## 功能特性

- 📊 **交互式可视化** - 使用 vis.js + Tailwind CSS + Alpine.js 构建现代化界面
- 📊 **交互式可视化** - 使用原生 JS、本地 CSS 和 vis-network 构建无构建现代化界面
- 🔍 **全局模糊搜索** - 快速搜索类型名或函数名,直接查看依赖关系
- 📦 **按包分组** - 左侧可折叠面板,按包过滤查看依赖
- 🔄 **双向依赖追踪** - 同时展示依赖(上游)和被依赖(下游)关系
Expand Down Expand Up @@ -305,7 +319,7 @@ dixhttp.RegisterGroupRules(
```

### GET `/api/dependencies?package=xxx&limit=100`
返回依赖关系数据,支持按包过滤
返回全局/旧视图使用的全量依赖数据,支持包过滤。规模优先的模块图和邻域图优先使用各自的有界接口。

```json
{
Expand Down Expand Up @@ -443,11 +457,8 @@ dixhttp.RegisterGroupRules(
## 技术栈

- **后端**: Go 标准库 `net/http`
- **前端框架**:
- [Tailwind CSS](https://tailwindcss.com/) - 样式
- [Alpine.js](https://alpinejs.dev/) - 响应式交互
- [vis-network](https://visjs.github.io/vis-network/) - 图形渲染
- **模板**: Go embed 嵌入单文件 HTML
- **前端**: 原生 JS、本地 CSS 和 [vis-network](https://visjs.github.io/vis-network/) 图形渲染
- **静态资源**: Go embed 本地文件;无构建步骤,无 CDN 依赖

## 适用场景

Expand Down
80 changes: 80 additions & 0 deletions dixhttp/http_scale_e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dixhttp

import (
"net/http/httptest"
"strings"
"testing"

dix "github.com/pubgo/dix/v2"
)

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, `value="panorama"`) || strings.Contains(body, "全景") {
t.Fatalf("panorama layout must be removed from legacy UI")
}
if strings.Contains(body, "static/js/views/graph.js") {
t.Fatalf("legacy index must not reference next graph.js")
}
}

func TestInventoryHelpersPresentInBundle(t *testing.T) {
container := dix.New()
server := NewServer(container)
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, httptest.NewRequest("GET", "/static/js/graph_state.mjs", nil))
if recorder.Code != 200 {
t.Fatalf("status = %d", recorder.Code)
}
body := recorder.Body.String()
for _, sym := range []string{
"buildProviderInventory",
"buildProvidersPyramidView",
"buildProviderDependencyGraph",
"assignProviderPyramidLevels",
"buildTypesPyramidView",
"buildTypeDependencyGraph",
"providerDisplayLabel",
"HIERARCHICAL_NODE_BUDGET",
"labelLodVisibleIds",
"buildModuleMapGraph",
} {
if !strings.Contains(body, sym) {
t.Fatalf("graph_state.mjs missing %s", sym)
}
}
for _, gone := range []string{
"layoutPanoramaPositions",
"buildPanoramaProviderGraph",
"buildPanoramaStructureGraph",
"resolvePanoramaCameraScale",
"labelLodByBand",
} {
if strings.Contains(body, gone) {
t.Fatalf("graph_state.mjs still contains removed panorama helper %s", gone)
}
}
}

func TestNextRouteRemoved(t *testing.T) {
container := dix.New()
server := NewServer(container)
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, httptest.NewRequest("GET", "/next", nil))
if recorder.Code != 404 {
t.Fatalf("/next status = %d, want 404", recorder.Code)
}
}
82 changes: 82 additions & 0 deletions dixhttp/scripts/graph_layout_e2e.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* Layout E2E against a live dixhttp demo (no browser dependency).
* Fetches /api/modules, builds the same star positions the UI uses, and asserts readability.
*
* DIX_HTTP_ADDR=127.0.0.1:18099 go run -C example ./http
* node dixhttp/scripts/graph_layout_e2e.mjs
*/
import { layoutStarPositions, assessLayoutMetrics, shortGraphLabel, resolveCameraStrategy } from "../static/js/graph_state.mjs";

const BASE = process.env.DIX_E2E_BASE || "http://127.0.0.1:18099";

async function main() {
const health = await fetch(`${BASE}/`);
if (!health.ok) throw new Error(`demo server not reachable at ${BASE}/ (${health.status})`);

const html = await health.text();
if (!html.includes("graph_state.mjs")) throw new Error("/ HTML missing graph_state.mjs");
if (!html.includes("legacy/app.js")) throw new Error("/ HTML missing legacy/app.js");
if (!html.includes("模块地图")) throw new Error("/ HTML missing module map control");

const modules = await (await fetch(`${BASE}/api/modules`)).json();
if (!Array.isArray(modules) || modules.length < 3) {
throw new Error(`expected modules list, got ${JSON.stringify(modules).slice(0, 200)}`);
}

const nodes = modules.map((m) => ({
id: m.name,
label: shortGraphLabel(m.name) + "\n(" + m.provider_count + "p/" + m.object_count + "o)",
}));
const edges = [];
for (const m of modules) {
for (const dep of m.depends_on || []) {
edges.push({ from: m.name, to: dep });
}
}

for (const n of nodes) {
if (n.label.includes("github.com") || n.label.split("/").length > 3) {
throw new Error(`label not shortened enough: ${n.label}`);
}
}

const camera = resolveCameraStrategy("modules", nodes.length);
if (camera !== "fit") throw new Error(`expected fit camera, got ${camera}`);

const positions = layoutStarPositions(nodes, edges);
const assessment = assessLayoutMetrics(positions);
if (!assessment.ok) {
throw new Error(`module map layout not readable: ${JSON.stringify(assessment)}`);
}

// Overlap check: pairwise distance for non-hub nodes should stay healthy.
const ids = Object.keys(positions);
const hub = positions[ids.find((id) => positions[id].x === 0 && positions[id].y === 0)];
let minPeer = Infinity;
for (let i = 0; i < ids.length; i++) {
for (let j = i + 1; j < ids.length; j++) {
const a = positions[ids[i]], b = positions[ids[j]];
const d = Math.hypot(a.x - b.x, a.y - b.y);
if (d > 0) minPeer = Math.min(minPeer, d);
}
}
if (!Number.isFinite(minPeer) || minPeer < 80) {
throw new Error(`nodes too close: minPeer=${minPeer}`);
}

console.log(JSON.stringify({
ok: true,
moduleCount: modules.length,
camera,
assessment,
minPeer: Math.round(minPeer),
sampleLabels: nodes.slice(0, 5).map((n) => n.label.replace("\n", " ")),
hubAtOrigin: !!hub,
}, null, 2));
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading