Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 76 additions & 1 deletion internal/graph2md/graph2md.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ type Artifact struct {
Stats json.RawMessage `json:"stats"`
}

// domainEdge records a relationship between two Domain nodes.
type domainEdge struct {
nodeID string // the other domain's node ID
relType string // original relationship type (e.g. "DOMAIN_RELATES")
}

// Run executes graph2md conversion. inputFiles is a comma-separated list of
// paths to graph JSON files. outputDir is the directory for markdown output.
func Run(inputFiles, outputDir, repoName, repoURL string) error {
Expand Down Expand Up @@ -164,6 +170,10 @@ func Run(inputFiles, outputDir, repoName, repoURL string) error {
subdomainFuncs := make(map[string][]string) // subdomain name -> function node IDs
subdomainClasses := make(map[string][]string) // subdomain name -> class node IDs

// Domain-to-domain relationships
domainRelatesOut := make(map[string][]domainEdge) // domain node ID -> outgoing related domains
domainRelatesIn := make(map[string][]domainEdge) // domain node ID -> incoming related domains

for _, rel := range allRels {
switch rel.Type {
case "IMPORTS":
Expand Down Expand Up @@ -203,6 +213,14 @@ func Run(inputFiles, outputDir, repoName, repoURL string) error {
if endNode != nil {
partOfDomain[rel.StartNode] = getStr(endNode.Properties, "name")
}
default:
// Capture domain-to-domain relationships (any type connecting two Domain nodes)
startNode := nodeLookup[rel.StartNode]
endNode := nodeLookup[rel.EndNode]
if startNode != nil && endNode != nil && hasLabel(startNode, "Domain") && hasLabel(endNode, "Domain") && rel.StartNode != rel.EndNode {
domainRelatesOut[rel.StartNode] = append(domainRelatesOut[rel.StartNode], domainEdge{nodeID: rel.EndNode, relType: rel.Type})
domainRelatesIn[rel.EndNode] = append(domainRelatesIn[rel.EndNode], domainEdge{nodeID: rel.StartNode, relType: rel.Type})
}
Comment on lines +216 to +223
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential duplicate domain edges when merging multiple graph files.

Hey, quick heads-up: allRels at line 130 appends relationships from each input file without deduplication. So if the same domain-to-domain relationship appears in multiple files, you'll get duplicate entries in domainRelatesOut/domainRelatesIn. This would show the same related domain multiple times in the "Related Domains" section and Mermaid diagram.

For most real-world cases this probably won't happen, but if you want to be defensive, you could dedupe by checking if an edge with the same nodeID and relType already exists before appending.

Example scenario: If graph1.json and graph2.json both contain the edge Auth --DOMAIN_RELATES--> Payments, the Auth domain page would list Payments twice.

🛡️ Optional fix to deduplicate edges
 		default:
 			// Capture domain-to-domain relationships (any type connecting two Domain nodes)
 			startNode := nodeLookup[rel.StartNode]
 			endNode := nodeLookup[rel.EndNode]
 			if startNode != nil && endNode != nil && hasLabel(startNode, "Domain") && hasLabel(endNode, "Domain") && rel.StartNode != rel.EndNode {
+				// Check for duplicate before appending
+				isDuplicateOut := false
+				for _, existing := range domainRelatesOut[rel.StartNode] {
+					if existing.nodeID == rel.EndNode && existing.relType == rel.Type {
+						isDuplicateOut = true
+						break
+					}
+				}
+				if !isDuplicateOut {
+					domainRelatesOut[rel.StartNode] = append(domainRelatesOut[rel.StartNode], domainEdge{nodeID: rel.EndNode, relType: rel.Type})
+				}
+				isDuplicateIn := false
+				for _, existing := range domainRelatesIn[rel.EndNode] {
+					if existing.nodeID == rel.StartNode && existing.relType == rel.Type {
+						isDuplicateIn = true
+						break
+					}
+				}
+				if !isDuplicateIn {
+					domainRelatesIn[rel.EndNode] = append(domainRelatesIn[rel.EndNode], domainEdge{nodeID: rel.StartNode, relType: rel.Type})
+				}
-				domainRelatesOut[rel.StartNode] = append(domainRelatesOut[rel.StartNode], domainEdge{nodeID: rel.EndNode, relType: rel.Type})
-				domainRelatesIn[rel.EndNode] = append(domainRelatesIn[rel.EndNode], domainEdge{nodeID: rel.StartNode, relType: rel.Type})
 			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
default:
// Capture domain-to-domain relationships (any type connecting two Domain nodes)
startNode := nodeLookup[rel.StartNode]
endNode := nodeLookup[rel.EndNode]
if startNode != nil && endNode != nil && hasLabel(startNode, "Domain") && hasLabel(endNode, "Domain") && rel.StartNode != rel.EndNode {
domainRelatesOut[rel.StartNode] = append(domainRelatesOut[rel.StartNode], domainEdge{nodeID: rel.EndNode, relType: rel.Type})
domainRelatesIn[rel.EndNode] = append(domainRelatesIn[rel.EndNode], domainEdge{nodeID: rel.StartNode, relType: rel.Type})
}
default:
// Capture domain-to-domain relationships (any type connecting two Domain nodes)
startNode := nodeLookup[rel.StartNode]
endNode := nodeLookup[rel.EndNode]
if startNode != nil && endNode != nil && hasLabel(startNode, "Domain") && hasLabel(endNode, "Domain") && rel.StartNode != rel.EndNode {
// Check for duplicate before appending
isDuplicateOut := false
for _, existing := range domainRelatesOut[rel.StartNode] {
if existing.nodeID == rel.EndNode && existing.relType == rel.Type {
isDuplicateOut = true
break
}
}
if !isDuplicateOut {
domainRelatesOut[rel.StartNode] = append(domainRelatesOut[rel.StartNode], domainEdge{nodeID: rel.EndNode, relType: rel.Type})
}
isDuplicateIn := false
for _, existing := range domainRelatesIn[rel.EndNode] {
if existing.nodeID == rel.StartNode && existing.relType == rel.Type {
isDuplicateIn = true
break
}
}
if !isDuplicateIn {
domainRelatesIn[rel.EndNode] = append(domainRelatesIn[rel.EndNode], domainEdge{nodeID: rel.StartNode, relType: rel.Type})
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/graph2md/graph2md.go` around lines 216 - 223, The default case that
builds domainRelatesOut/domainRelatesIn can append duplicate domainEdge entries
when allRels contains the same relationship from multiple files; change the
logic in that block (the default: case that uses startNode/endNode, hasLabel,
and appends domainEdge) to check for an existing edge before appending by
comparing nodeID and relType (or maintain a temporary set keyed by
fmt.Sprintf("%d|%s") or similar) and only append when not already present, and
apply the same dedupe when adding to both domainRelatesOut[rel.StartNode] and
domainRelatesIn[rel.EndNode].

}
}

Expand Down Expand Up @@ -417,6 +435,8 @@ func Run(inputFiles, outputDir, repoName, repoURL string) error {
domainSubdomains: domainSubdomains,
subdomainFuncs: subdomainFuncs,
subdomainClasses: subdomainClasses,
domainRelatesOut: domainRelatesOut,
domainRelatesIn: domainRelatesIn,
}

md := ctx.generateMarkdown()
Expand Down Expand Up @@ -448,6 +468,7 @@ type renderContext struct {
domainNodeByName, subdomainNodeByName map[string]string
domainSubdomains map[string][]string
subdomainFuncs, subdomainClasses map[string][]string
domainRelatesOut, domainRelatesIn map[string][]domainEdge
}

// internalLink returns an HTML <a> tag linking to the entity page for nodeID,
Expand Down Expand Up @@ -1076,6 +1097,22 @@ func (c *renderContext) writeDomainBody(sb *strings.Builder) {
})
}

// Related Domains
outEdges := c.domainRelatesOut[c.node.ID]
inEdges := c.domainRelatesIn[c.node.ID]
if len(outEdges) > 0 || len(inEdges) > 0 {
sb.WriteString("## Related Domains\n\n")
for _, de := range outEdges {
label := c.resolveName(de.nodeID)
sb.WriteString(fmt.Sprintf("- %s %s\n", c.internalLink(de.nodeID, label), de.relType))
}
for _, de := range inEdges {
label := c.resolveName(de.nodeID)
sb.WriteString(fmt.Sprintf("- %s %s (incoming)\n", c.internalLink(de.nodeID, label), de.relType))
}
sb.WriteString("\n")
}

// Source Files
files := c.domainFiles[name]
if len(files) > 0 {
Expand Down Expand Up @@ -1575,14 +1612,31 @@ func (c *renderContext) writeGraphData(sb *strings.Builder) {
}
}

// For domains: add subdomain children
// For domains: add subdomain children and related domains
if c.label == "Domain" {
domName := getStr(c.node.Properties, "name")
relSets = append(relSets, struct {
ids []string
relType string
reverse bool
}{c.domainSubdomains[domName], "contains", false})

// Related domains (outgoing)
for _, de := range c.domainRelatesOut[c.node.ID] {
relSets = append(relSets, struct {
ids []string
relType string
reverse bool
}{[]string{de.nodeID}, "relatesTo", false})
}
// Related domains (incoming)
for _, de := range c.domainRelatesIn[c.node.ID] {
relSets = append(relSets, struct {
ids []string
relType string
reverse bool
}{[]string{de.nodeID}, "relatesTo", true})
}
}
// For subdomains: add domain parent
if c.label == "Subdomain" {
Expand Down Expand Up @@ -1805,6 +1859,27 @@ func (c *renderContext) writeMermaidDiagram(sb *strings.Builder) {
lines = append(lines, fmt.Sprintf(" %s --> %s", centerID, mid))
}

// Related domains (outgoing)
for _, de := range c.domainRelatesOut[c.node.ID] {
if nodeCount >= maxNodes {
break
}
label := mermaidEscape(c.resolveName(de.nodeID))
mid := addNode(de.nodeID, label)
lines = append(lines, fmt.Sprintf(" %s[\"%s\"]", mid, label))
lines = append(lines, fmt.Sprintf(" %s -->|%s| %s", centerID, mermaidEscape(de.relType), mid))
}
// Related domains (incoming)
for _, de := range c.domainRelatesIn[c.node.ID] {
if nodeCount >= maxNodes {
break
}
label := mermaidEscape(c.resolveName(de.nodeID))
mid := addNode(de.nodeID, label)
lines = append(lines, fmt.Sprintf(" %s[\"%s\"]", mid, label))
lines = append(lines, fmt.Sprintf(" %s -->|%s| %s", mid, mermaidEscape(de.relType), centerID))
}

case "Subdomain":
lines = append(lines, "graph TD")
subName := getStr(c.node.Properties, "name")
Expand Down
Loading