项目文件夹

文件
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

582 行
18 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package wiki
import (
"context"
"fmt"
"sort"
"strings"
"time"
"github.com/zzet/gortex/internal/analysis"
"github.com/zzet/gortex/internal/contracts"
"github.com/zzet/gortex/internal/graph"
)
// renderRepoIndex emits the per-repo index page (wiki/<slug>/index.md).
// Lists communities in size order with links to per-community pages,
// the architecture diagram, the analysis pages, and the docs bundle.
func (gen *Generator) renderRepoIndex(ctx context.Context) (string, error) {
var b strings.Builder
repoLabel := gen.opts.Repo
if repoLabel == "" {
repoLabel = "Repository"
}
b.WriteString("---\n")
fmt.Fprintf(&b, "title: %s wiki\n", repoLabel)
fmt.Fprintf(&b, "generated_at: %s\n", gen.opts.GeneratedAt.UTC().Format(time.RFC3339))
fmt.Fprintf(&b, "node_count: %d\n", gen.nodeCount)
fmt.Fprintf(&b, "edge_count: %d\n", gen.edgeCount)
b.WriteString("---\n\n")
fmt.Fprintf(&b, "# %s\n\n", repoLabel)
fmt.Fprintf(&b,
"Auto-generated by `gortex wiki` on %s. %d nodes, %d edges across %d files.\n\n",
gen.opts.GeneratedAt.UTC().Format("2006-01-02"), gen.nodeCount, gen.edgeCount, gen.fileCount)
b.WriteString("## At a Glance\n\n")
b.WriteString("| Metric | Value |\n")
b.WriteString("|--------|-------|\n")
fmt.Fprintf(&b, "| Files | %d |\n", gen.fileCount)
fmt.Fprintf(&b, "| Symbols | %d |\n", gen.symbolCount)
fmt.Fprintf(&b, "| Relationships | %d |\n", gen.edgeCount)
fmt.Fprintf(&b, "| Communities | %d |\n", len(gen.kept))
fmt.Fprintf(&b, "| Processes | %d |\n", gen.processCount)
fmt.Fprintf(&b, "| Contracts | %d |\n", gen.contractCount)
fmt.Fprintf(&b, "| Hotspots | %d |\n", len(gen.hotspots))
fmt.Fprintf(&b, "| Cycles | %d |\n", len(gen.cycles))
b.WriteString("\n")
if len(gen.kept) > 0 {
b.WriteString("## Community Map\n\n")
b.WriteString("```mermaid\n")
b.WriteString(RenderCommunityGraph(gen.graph, gen.communities, CommunityGraphOpts{
MinSize: gen.opts.MinCommunity,
Max: gen.opts.MaxCommunities,
}))
if !strings.HasSuffix(b.String(), "\n") {
b.WriteString("\n")
}
b.WriteString("```\n\n")
b.WriteString("## Communities\n\n")
b.WriteString("| Community | Size | Cohesion | Files | Page |\n")
b.WriteString("|-----------|------|----------|-------|------|\n")
for _, c := range gen.kept {
page := gen.communityPagePath(c)
fmt.Fprintf(&b, "| %s | %d | %.0f%% | %d | [%s](%s) |\n",
escapeTableCell(c.Label), c.Size, c.Cohesion*100, len(c.Files),
"open", page)
}
b.WriteString("\n")
}
b.WriteString("## Sections\n\n")
b.WriteString("- [Architecture](architecture.md) — community-level overview\n")
if !gen.opts.NoProcesses {
b.WriteString("- [Processes](processes/) — execution flows with sequence diagrams\n")
}
if !gen.opts.NoContracts {
b.WriteString("- [API Contracts](contracts/api-surface.md) — HTTP/gRPC/GraphQL surfaces\n")
}
b.WriteString("- [Hotspots](analysis/hotspots.md) — high-coupling symbols\n")
b.WriteString("- [Cycles](analysis/cycles.md) — circular dependencies\n")
b.WriteString("- [Semantic](analysis/semantic.md) — edge confidence & coverage\n")
if !gen.opts.NoDocs && gen.docsBundle != "" {
b.WriteString("- [Changelog & ownership](changelog.md) — `gortex docs` bundle\n")
}
b.WriteString("\n")
b.WriteString("## How to Explore\n\n")
b.WriteString("```\n")
b.WriteString("smart_context with task: \"understand this codebase\"\n")
b.WriteString("get_repo_outline\n")
if len(gen.kept) > 0 {
fmt.Fprintf(&b, "get_communities with id: %q\n", gen.kept[0].ID)
}
b.WriteString("```\n")
body := b.String()
if gen.opts.Enhance && gen.opts.Enhancer != nil {
out, err := gen.opts.Enhancer.Enhance(ctx, EnhanceSection{
Kind: "architecture",
PageTitle: repoLabel + " index",
RawMarkdown: body,
})
if err == nil && strings.TrimSpace(out) != "" {
body = out
}
}
return body, nil
}
// renderCommunityPage emits one community article.
func (gen *Generator) renderCommunityPage(ctx context.Context, c analysis.Community) (string, error) {
var b strings.Builder
label := c.Label
if label == "" {
label = c.ID
}
b.WriteString("---\n")
fmt.Fprintf(&b, "title: %s\n", label)
fmt.Fprintf(&b, "community_id: %s\n", c.ID)
fmt.Fprintf(&b, "symbols: %d\n", c.Size)
fmt.Fprintf(&b, "files: %d\n", len(c.Files))
b.WriteString("---\n\n")
fmt.Fprintf(&b, "# %s\n\n", label)
fmt.Fprintf(&b, "%d symbols across %d files | %.0f%% cohesion | Community `%s`\n\n",
c.Size, len(c.Files), c.Cohesion*100, c.ID)
b.WriteString("## Files\n\n")
if len(c.Files) == 0 {
b.WriteString("_No files in this community._\n\n")
} else {
fileSyms := gen.buildFileSymbolMap(c)
// Stable ordering.
files := make([]string, 0, len(c.Files))
files = append(files, c.Files...)
sort.Strings(files)
b.WriteString("| File | Symbols |\n")
b.WriteString("|------|---------|\n")
for _, f := range files {
syms := fileSyms[f]
if len(syms) > 8 {
syms = append(syms[:8], "...")
}
fmt.Fprintf(&b, "| `%s` | %s |\n", f, strings.Join(syms, ", "))
}
b.WriteString("\n")
}
// Entry points.
entries := gen.findEntryPoints(c)
if len(entries) > 0 {
b.WriteString("## Entry Points\n\n")
for _, ep := range entries {
fmt.Fprintf(&b, "- `%s`\n", ep)
}
b.WriteString("\n")
}
// Execution flows that touch this community.
if !gen.opts.NoProcesses {
procs := gen.processesTouching(c)
if len(procs) > 0 {
b.WriteString("## Execution Flows\n\n")
for _, p := range procs {
fmt.Fprintf(&b, "- [%s](../processes/%s.md) — %d steps from `%s`\n",
p.Name, slugify(p.ID+"-"+p.Name), p.StepCount, p.EntryPoint)
}
b.WriteString("\n")
}
}
// Cross-community connections.
if cm := gen.crossComm[c.ID]; len(cm) > 0 {
type pair struct {
id, label string
count int
}
var pairs []pair
for cid, n := range cm {
pairs = append(pairs, pair{id: cid, label: gen.communityLabel(cid), count: n})
}
sort.Slice(pairs, func(i, j int) bool {
if pairs[i].count != pairs[j].count {
return pairs[i].count > pairs[j].count
}
return pairs[i].label < pairs[j].label
})
b.WriteString("## Connected Communities\n\n")
b.WriteString("| Community | Cross-edges |\n")
b.WriteString("|-----------|-------------|\n")
for _, p := range pairs {
page := "../" + gen.communityPagePathByID(p.id)
fmt.Fprintf(&b, "| [%s](%s) | %d |\n", escapeTableCell(p.label), page, p.count)
}
b.WriteString("\n")
}
b.WriteString("## How to Explore\n\n")
b.WriteString("```\n")
fmt.Fprintf(&b, "get_communities with id: %q\n", c.ID)
fmt.Fprintf(&b, "smart_context with task: \"understand %s\"\n", label)
if len(entries) > 0 {
fmt.Fprintf(&b, "find_usages with id: %q\n", entries[0])
}
b.WriteString("```\n")
body := b.String()
if gen.opts.Enhance && gen.opts.Enhancer != nil {
anchors := make([]string, 0, len(c.Members))
if len(c.Members) > 8 {
anchors = append(anchors, c.Members[:8]...)
} else {
anchors = append(anchors, c.Members...)
}
out, err := gen.opts.Enhancer.Enhance(ctx, EnhanceSection{
Kind: "community",
PageTitle: label,
AnchorSymbolIDs: anchors,
RawMarkdown: body,
Context: fmt.Sprintf("size=%d cohesion=%.2f files=%d", c.Size, c.Cohesion, len(c.Files)),
})
if err == nil && strings.TrimSpace(out) != "" {
body = out
}
}
return body, nil
}
// renderArchitecturePage emits architecture.md.
func (gen *Generator) renderArchitecturePage(ctx context.Context) (string, error) {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("title: Architecture\n")
b.WriteString("---\n\n")
b.WriteString("# Architecture Overview\n\n")
if len(gen.kept) == 0 {
b.WriteString("_No communities of sufficient size were detected._\n")
} else {
fmt.Fprintf(&b, "This codebase decomposes into %d functional communities. The diagram below shows the largest %d and the cross-community calls between them.\n\n",
len(gen.kept), len(gen.kept))
b.WriteString("```mermaid\n")
b.WriteString(RenderArchitecture(gen.graph, gen.communities, CommunityGraphOpts{
MinSize: gen.opts.MinCommunity,
Max: gen.opts.MaxCommunities,
}))
if !strings.HasSuffix(b.String(), "\n") {
b.WriteString("\n")
}
b.WriteString("```\n\n")
b.WriteString("## Communities by Size\n\n")
b.WriteString("| Community | Files | Symbols | Cohesion | Hub |\n")
b.WriteString("|-----------|-------|---------|----------|-----|\n")
for _, c := range gen.kept {
hub := c.Hub
if hub == "" {
hub = "—"
}
page := gen.communityPagePath(c)
fmt.Fprintf(&b, "| [%s](%s) | %d | %d | %.0f%% | `%s` |\n",
escapeTableCell(c.Label), page,
len(c.Files), c.Size, c.Cohesion*100, escapeTableCell(hub))
}
b.WriteString("\n")
}
body := b.String()
if gen.opts.Enhance && gen.opts.Enhancer != nil {
out, err := gen.opts.Enhancer.Enhance(ctx, EnhanceSection{
Kind: "architecture",
PageTitle: "Architecture overview",
RawMarkdown: body,
})
if err == nil && strings.TrimSpace(out) != "" {
body = out
}
}
return body, nil
}
// renderHotspotsPage emits analysis/hotspots.md.
func (gen *Generator) renderHotspotsPage(_ context.Context) (string, error) {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("title: Hotspots\n")
b.WriteString("---\n\n")
b.WriteString("# Hotspots — High-Coupling Symbols\n\n")
b.WriteString("Symbols ranked by `(fan_in * 2) + (fan_out * 1.5) + (community_crossings * 3)`, normalised to 0..100.\n\n")
if len(gen.hotspots) == 0 {
b.WriteString("_No hotspots above the auto-threshold (mean + 2σ)._\n")
return b.String(), nil
}
b.WriteString("| # | Symbol | Kind | Fan-in | Fan-out | Crossings | Score |\n")
b.WriteString("|---|--------|------|--------|---------|-----------|-------|\n")
limit := 30
if len(gen.hotspots) < limit {
limit = len(gen.hotspots)
}
for i, h := range gen.hotspots[:limit] {
display := h.Name
if h.FilePath != "" {
display = fmt.Sprintf("%s — `%s:%d`", h.Name, h.FilePath, h.Line)
}
fmt.Fprintf(&b, "| %d | %s | %s | %d | %d | %d | %.2f |\n",
i+1, escapeTableCell(display), h.Kind, h.FanIn, h.FanOut, h.CommunityCrossings, h.ComplexityScore)
}
b.WriteString("\nUse `verify_change` before modifying any of these signatures — their blast radius is wide.\n")
return b.String(), nil
}
// renderCyclesPage emits analysis/cycles.md.
func (gen *Generator) renderCyclesPage(_ context.Context) (string, error) {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("title: Cycles\n")
b.WriteString("---\n\n")
b.WriteString("# Circular Dependencies\n\n")
if len(gen.cycles) == 0 {
b.WriteString("_No cycles detected._\n")
return b.String(), nil
}
fmt.Fprintf(&b, "%d cycle(s) detected. Higher severity = harder to fix and bigger refactor surface.\n\n", len(gen.cycles))
b.WriteString("| # | Kind | Severity | Path length | First node |\n")
b.WriteString("|---|------|----------|-------------|------------|\n")
for i, c := range gen.cycles {
first := ""
if len(c.Path) > 0 {
first = c.Path[0]
}
fmt.Fprintf(&b, "| %d | %s | %d | %d | `%s` |\n",
i+1, c.Kind, c.Severity, len(c.Path), escapeTableCell(first))
}
b.WriteString("\n")
limit := 5
if len(gen.cycles) < limit {
limit = len(gen.cycles)
}
for i := 0; i < limit; i++ {
c := gen.cycles[i]
fmt.Fprintf(&b, "## Cycle %d — %s (severity %d)\n\n", i+1, c.Kind, c.Severity)
b.WriteString("```\n")
for _, n := range c.Path {
fmt.Fprintf(&b, " %s\n", n)
}
if len(c.Path) > 0 {
fmt.Fprintf(&b, " ↑ back to %s\n", c.Path[0])
}
b.WriteString("```\n\n")
}
return b.String(), nil
}
// renderSemanticPage emits analysis/semantic.md.
func (gen *Generator) renderSemanticPage(_ context.Context) (string, error) {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("title: Semantic analysis\n")
b.WriteString("---\n\n")
b.WriteString("# Semantic Analysis\n\n")
b.WriteString("Edge confidence distribution across the indexed graph. Higher tiers come from compiler-verified providers (go-types, LSP, SCIP); lower tiers fall back to AST heuristics.\n\n")
if len(gen.confDist) == 0 {
b.WriteString("_No confidence-labelled edges in the graph._\n")
return b.String(), nil
}
b.WriteString("## Edge Confidence Distribution\n\n")
b.WriteString("| Origin | Edges | Percentage |\n")
b.WriteString("|--------|-------|------------|\n")
keys := make([]string, 0, len(gen.confDist))
var total int
for k, v := range gen.confDist {
keys = append(keys, k)
total += v
}
sort.Strings(keys)
for _, k := range keys {
pct := 0.0
if total > 0 {
pct = float64(gen.confDist[k]) / float64(total) * 100
}
fmt.Fprintf(&b, "| %s | %d | %.1f%% |\n", k, gen.confDist[k], pct)
}
fmt.Fprintf(&b, "| **total** | **%d** | 100%% |\n\n", total)
if len(gen.semanticProviders) > 0 {
b.WriteString("## Provider Status\n\n")
b.WriteString("| Language | Provider | Status |\n")
b.WriteString("|----------|----------|--------|\n")
for _, ps := range gen.semanticProviders {
fmt.Fprintf(&b, "| %s | %s | %s |\n", ps.Language, ps.Name, ps.Status)
}
b.WriteString("\n")
}
return b.String(), nil
}
// renderContractsPage emits contracts/api-surface.md.
func (gen *Generator) renderContractsPage(_ context.Context) (string, error) {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("title: API contracts\n")
b.WriteString("---\n\n")
b.WriteString("# API Contracts\n\n")
if len(gen.contractList) == 0 {
b.WriteString("_No contracts detected._\n")
return b.String(), nil
}
// Group by ContractType, then by Role.
byType := make(map[contracts.ContractType][]contracts.Contract)
for _, c := range gen.contractList {
byType[c.Type] = append(byType[c.Type], c)
}
types := make([]string, 0, len(byType))
for t := range byType {
types = append(types, string(t))
}
sort.Strings(types)
for _, t := range types {
list := byType[contracts.ContractType(t)]
fmt.Fprintf(&b, "## %s contracts (%d)\n\n", strings.ToUpper(t), len(list))
b.WriteString("| Role | ID | Symbol | File:Line |\n")
b.WriteString("|------|----|--------|-----------|\n")
// Stable order.
sort.SliceStable(list, func(i, j int) bool {
if list[i].Role != list[j].Role {
return list[i].Role < list[j].Role
}
return list[i].ID < list[j].ID
})
for _, c := range list {
loc := c.FilePath
if c.Line > 0 {
loc = fmt.Sprintf("%s:%d", c.FilePath, c.Line)
}
fmt.Fprintf(&b, "| %s | `%s` | `%s` | `%s` |\n",
c.Role, escapeTableCell(c.ID), escapeTableCell(c.SymbolID), escapeTableCell(loc))
}
b.WriteString("\n")
}
return b.String(), nil
}
// renderProcessPage emits one process article.
func (gen *Generator) renderProcessPage(ctx context.Context, p analysis.Process) (string, error) {
var b strings.Builder
b.WriteString("---\n")
fmt.Fprintf(&b, "title: %s\n", p.Name)
fmt.Fprintf(&b, "process_id: %s\n", p.ID)
fmt.Fprintf(&b, "steps: %d\n", p.StepCount)
b.WriteString("---\n\n")
fmt.Fprintf(&b, "# Process: %s\n\n", p.Name)
fmt.Fprintf(&b, "%d steps across %d files. Entry: `%s` (score %.2f).\n\n",
p.StepCount, len(p.Files), p.EntryPoint, p.Score)
b.WriteString("## Flow\n\n")
b.WriteString("```mermaid\n")
b.WriteString(RenderProcessSequence(p, gen.nodeByID, gen.commLabelByNode))
if !strings.HasSuffix(b.String(), "\n") {
b.WriteString("\n")
}
b.WriteString("```\n\n")
b.WriteString("## Steps\n\n")
b.WriteString("| # | Depth | Symbol | File |\n")
b.WriteString("|---|-------|--------|------|\n")
for i, s := range p.Steps {
name, file := s.ID, ""
if n, ok := gen.nodeByID[s.ID]; ok && n != nil {
name = n.Name
file = n.FilePath
}
fmt.Fprintf(&b, "| %d | %d | `%s` | `%s` |\n", i+1, s.Depth, escapeTableCell(name), escapeTableCell(file))
}
b.WriteString("\n")
if len(p.Files) > 0 {
b.WriteString("## Files Touched\n\n")
for _, f := range p.Files {
fmt.Fprintf(&b, "- `%s`\n", f)
}
b.WriteString("\n")
}
body := b.String()
if gen.opts.Enhance && gen.opts.Enhancer != nil {
anchors := []string{p.EntryPoint}
out, err := gen.opts.Enhancer.Enhance(ctx, EnhanceSection{
Kind: "process",
PageTitle: p.Name,
AnchorSymbolIDs: anchors,
RawMarkdown: body,
})
if err == nil && strings.TrimSpace(out) != "" {
body = out
}
}
return body, nil
}
// renderTopLevelIndex emits the top-level wiki/index.md.
// Single-element repo list today; multi-element when multi-repo lands.
func (gen *Generator) renderTopLevelIndex() string {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("title: Gortex Wiki\n")
fmt.Fprintf(&b, "generated_at: %s\n", gen.opts.GeneratedAt.UTC().Format(time.RFC3339))
b.WriteString("---\n\n")
b.WriteString("# Workspaces\n\n")
b.WriteString("This wiki indexes one repository today. Multi-repo workspaces will appear under `_workspace/` as they land.\n\n")
b.WriteString("| Repository | Symbols | Communities | Page |\n")
b.WriteString("|------------|---------|-------------|------|\n")
fmt.Fprintf(&b, "| `%s` | %d | %d | [%s](%s/index.md) |\n",
gen.opts.Repo, gen.symbolCount, len(gen.kept), gen.opts.Repo, gen.opts.Repo)
return b.String()
}
// escapeTableCell makes a string safe inside a Markdown table cell:
// strip newlines and escape pipes.
func escapeTableCell(s string) string {
s = strings.ReplaceAll(s, "\n", " ")
s = strings.ReplaceAll(s, "|", "\\|")
return s
}
// processesTouching returns processes whose Steps include a member of c.
func (gen *Generator) processesTouching(c analysis.Community) []analysis.Process {
if gen.processes == nil {
return nil
}
memberSet := make(map[string]bool, len(c.Members))
for _, m := range c.Members {
memberSet[m] = true
}
var out []analysis.Process
for _, p := range gen.processes.Processes {
for _, s := range p.Steps {
if memberSet[s.ID] {
out = append(out, p)
break
}
}
}
// Cap to 10 most relevant by step count.
sort.SliceStable(out, func(i, j int) bool { return out[i].StepCount > out[j].StepCount })
if len(out) > 10 {
out = out[:10]
}
return out
}
// communityPagePath returns the relative path under wiki/<repo>/ for c.
func (gen *Generator) communityPagePath(c analysis.Community) string {
return gen.communityPagePathByID(c.ID)
}
func (gen *Generator) communityPagePathByID(id string) string {
for i, c := range gen.kept {
if c.ID == id {
slug := slugify(c.Label)
if slug == "" {
slug = slugify(c.ID)
}
return fmt.Sprintf("communities/%d-%s.md", i+1, slug)
}
}
// Community wasn't kept (filtered out by size or cap) — link to index.
return "index.md"
}
// processPagePath returns the relative path for a process page.
func processPagePath(p analysis.Process) string {
return "processes/" + slugify(p.ID+"-"+p.Name) + ".md"
}
// ensure graph import is used.
var _ = (*graph.Graph)(nil)