Files
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 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)