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
func (g *IDGenerator) createDeterministicSignature(node *html.Node, filePath string) string {
// Build signature from stable characteristics
var sigParts []string
// 1. DOM path (simplified, max 3 levels)
// Build enhanced signature with 6 components for maximum differentiation
tag := node.Data
domPath := g.getSimpleDOMPath(node)
if domPath != "" {
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)
classes := strings.Join(GetClasses(node), " ")
contentPreview := g.getContentPreview(node)
if contentPreview != "" {
// Use first 20 chars for signature
siblingIndex := g.getSiblingIndex(node)
// Normalize content preview to first 20 chars
if len(contentPreview) > 20 {
contentPreview = contentPreview[:20]
}
sigParts = append(sigParts, contentPreview)
}
// 4. Create hash of combined signature
combined := strings.Join(sigParts, "|")
hash := sha256.Sum256([]byte(combined))
// Create comprehensive deterministic signature
signature := fmt.Sprintf("%s|%s|%s|%s|%s|%d",
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]
}
@@ -157,17 +153,24 @@ func (g *IDGenerator) createSignature(node *html.Node, filePath string) string {
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 {
var pathParts []string
current := node
depth := 0
for current != nil && current.Type == html.ElementNode && depth < 5 {
for current != nil && current.Type == html.ElementNode && depth < 3 {
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...)
current = current.Parent
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 {
if node.Parent == nil {
return 0
@@ -213,10 +216,36 @@ func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
tag := node.Data
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 {
if sibling.Type == html.ElementNode && sibling.Data == tag {
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 sibling == node {
return index
@@ -225,6 +254,7 @@ func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
}
}
}
}
return index
}