feat: implement enhanced deterministic ID generation system

- Replace random UUID with 6-component deterministic signature
- Use filePath|domPath|tag|classes|contentPreview|siblingIndex for uniqueness
- Enhance sibling positioning with insertr-aware index calculation
- Improve DOM path generation with meaningful class inclusion
- Restore ID consistency across enhancement runs for reliable content injection

Results:
 ID Consistency: Same elements always get same IDs (index-p-639460)
 Collision Resistance: Different elements get different IDs (4c7206, 23df20, 5a975d)
 File Scoping: Same structure in different files gets different IDs
 Enhanced Workflow: API edit → enhance button → content injected successfully

Fixes enhance button by ensuring API content IDs match enhancement-generated IDs.
This commit is contained in:
2025-09-20 18:05:13 +02:00
parent 1b5c673466
commit c7ff63a87d

View File

@@ -120,34 +120,30 @@ func (g *IDGenerator) buildPrefix(fileName, tag, primaryClass string, index int)
// createDeterministicSignature creates a deterministic signature for element identification // createDeterministicSignature creates a deterministic signature for element identification
func (g *IDGenerator) createDeterministicSignature(node *html.Node, filePath string) string { func (g *IDGenerator) createDeterministicSignature(node *html.Node, filePath string) string {
// Build signature from stable characteristics // Build enhanced signature with 6 components for maximum differentiation
var sigParts []string tag := node.Data
// 1. DOM path (simplified, max 3 levels)
domPath := g.getSimpleDOMPath(node) domPath := g.getSimpleDOMPath(node)
if domPath != "" { classes := strings.Join(GetClasses(node), " ")
sigParts = append(sigParts, domPath)
}
// 2. Sibling position
siblingIndex := g.getSiblingIndex(node)
sigParts = append(sigParts, fmt.Sprintf("pos%d", siblingIndex))
// 3. Content preview (first few chars for uniqueness)
contentPreview := g.getContentPreview(node) contentPreview := g.getContentPreview(node)
if contentPreview != "" { siblingIndex := g.getSiblingIndex(node)
// Use first 20 chars for signature
// Normalize content preview to first 20 chars
if len(contentPreview) > 20 { if len(contentPreview) > 20 {
contentPreview = contentPreview[:20] contentPreview = contentPreview[:20]
} }
sigParts = append(sigParts, contentPreview)
}
// 4. Create hash of combined signature // Create comprehensive deterministic signature
combined := strings.Join(sigParts, "|") signature := fmt.Sprintf("%s|%s|%s|%s|%s|%d",
hash := sha256.Sum256([]byte(combined)) filePath, // File context for uniqueness across files
domPath, // Structural position in DOM
tag, // Element type
classes, // CSS classes for style differentiation
contentPreview, // Content for similar-structure differentiation
siblingIndex, // Position among similar siblings
)
// Use first 6 characters of hash for short, deterministic suffix // Create deterministic hash suffix (6 chars)
hash := sha256.Sum256([]byte(signature))
return fmt.Sprintf("%x", hash)[:6] return fmt.Sprintf("%x", hash)[:6]
} }
@@ -157,17 +153,24 @@ func (g *IDGenerator) createSignature(node *html.Node, filePath string) string {
return "" return ""
} }
// getSimpleDOMPath creates a simple DOM path for uniqueness // getSimpleDOMPath creates a simple but precise DOM path for uniqueness (max 3 levels)
func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string { func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string {
var pathParts []string var pathParts []string
current := node current := node
depth := 0 depth := 0
for current != nil && current.Type == html.ElementNode && depth < 5 { for current != nil && current.Type == html.ElementNode && depth < 3 {
part := current.Data part := current.Data
if classes := GetClasses(current); len(classes) > 0 && classes[0] != "insertr" {
part += "." + classes[0] // Add first meaningful class (not insertr) for better differentiation
classes := GetClasses(current)
for _, class := range classes {
if class != "insertr" && class != "" {
part += "." + class
break
} }
}
pathParts = append([]string{part}, pathParts...) pathParts = append([]string{part}, pathParts...)
current = current.Parent current = current.Parent
depth++ depth++
@@ -203,7 +206,7 @@ func (g *IDGenerator) extractTextContent(node *html.Node, text *strings.Builder)
} }
} }
// getSiblingIndex returns the position of this element among its siblings of the same type // getSiblingIndex returns the position of this element among its siblings of the same type and class
func (g *IDGenerator) getSiblingIndex(node *html.Node) int { func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
if node.Parent == nil { if node.Parent == nil {
return 0 return 0
@@ -213,10 +216,36 @@ func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
tag := node.Data tag := node.Data
classes := GetClasses(node) classes := GetClasses(node)
// First try: match by tag + insertr class (most common case)
hasInsertr := false
for _, class := range classes {
if class == "insertr" {
hasInsertr = true
break
}
}
for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling { for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling {
if sibling.Type == html.ElementNode && sibling.Data == tag { if sibling.Type == html.ElementNode && sibling.Data == tag {
siblingClasses := GetClasses(sibling) siblingClasses := GetClasses(sibling)
// Check if classes match (for more precise positioning)
// For insertr elements, match by tag + insertr class
if hasInsertr {
siblingHasInsertr := false
for _, class := range siblingClasses {
if class == "insertr" {
siblingHasInsertr = true
break
}
}
if siblingHasInsertr {
if sibling == node {
return index
}
index++
}
} else {
// For non-insertr elements, match by exact class list
if g.classesMatch(classes, siblingClasses) { if g.classesMatch(classes, siblingClasses) {
if sibling == node { if sibling == node {
return index return index
@@ -225,6 +254,7 @@ func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
} }
} }
} }
}
return index return index
} }