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
582 lines
18 KiB
Go
582 lines
18 KiB
Go
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)
|