**Approach (brief)**I’d build a generator that scores candidate selectors from a DOM snapshot using attribute heuristics, text normalization and hierarchical hashing, then returns the highest-stability selector(s). Goals: readable, unique, robust to cosmetic/layout changes.**Data structures**- NodeRecord: { nodeRef, tag, attrs(dict), textNormalized, childrenHashes }- AttrIndex: map from (attrName, attrValue) -> set of NodeRecord refs for quick uniqueness checks- HashCache: map nodeRef -> hierarchyHash- Candidate: { selectorString, score, specificity, fragilityReasons }**Heuristics (priority order)**1. Attribute preference: data-* > id > aria-* > name > class > other2. Uniqueness: prefer attributes whose value maps to single node in AttrIndex3. Text normalization: trim, collapse whitespace, lowercase, remove punctuation; prefer stable short texts (<= 30 chars)4. Hierarchical hashing: compute stable hash based on node tag, preferred attrs, normalized text and childrenHashes (ignore order-insensitive children where appropriate)5. Avoid positional selectors (:nth-child) unless no other option6. Penalize selectors containing dynamically generated patterns (e.g., long numeric suffixes)**Generation steps (pseudocode)**python
# pseudocode
def generate_stable_selector(dom_root, target_node):
build_node_records(dom_root) # fill AttrIndex, HashCache
candidates = []
# 1. Try single-attribute unique selectors
for attr in preferred_attrs_order:
val = target_node.attrs.get(attr)
if val:
if AttrIndex[(attr,val)].size == 1:
candidates.append(make_selector(target_node.tag, attr, val, reason="unique_attr"))
return best(candidates)
else:
candidates.append(make_selector(..., score=low))
# 2. Try text-based
if normalized_text(target_node):
sel = selector_by_text(target_node)
candidates.append(sel)
# 3. Try hierarchical hashing path
path = build_hash_path(target_node) # uses parent hashes and preferred attrs
candidates.append(path_to_selector(path))
# 4. Fallback: class-chain or nth-child with penalties
candidates.extend(fallback_selectors(target_node))
return best(candidates, scoring_function)
def scoring_function(candidate):
score = base
score += unique_attr_bonus if candidate.uses_unique_attr()
score += text_match_bonus if candidate.uses_normalized_text()
score -= positional_penalty if candidate.uses_nth_child()
score += hierarchy_match_bonus if candidate.matches_hash()
score -= dynamic_pattern_penalty if detects_dynamic_values()
return score
**Key ideas / trade-offs**- Hierarchical hash makes selector resilient to sibling reordering (if children treated as multiset) and cosmetic wrappers.- Prefer human-readable attributes (data-test-id) to increase maintainability.- Avoid brittle positional selectors; use as last resort.- Complexity: building indexes and hashes is O(N) over snapshot size; generation per node is O(depth) typical.**Example output**- Best: button[data-test-id="checkout-submit"]- Fallback: div.card[data-item-id="sku-1234"] > button.btn-primaryI would implement as a library module with unit tests against mutated DOM snapshots to validate stability and tune scoring.