assign static method
Walk tree and produce a stable ID + structural path for every node.
Sibling slugs are unique per parent. A slug shared by several siblings
is disambiguated by CONTENT when the colliding nodes' texts carry data
tokens the slug dropped (15 Oct ₹500 paid / 16 Oct ₹200 paid →
oct_paid_15_500 / oct_paid_16_200), so the suffix follows the row,
not its position in the current window of a lazy list. Every member of
such a group takes its content suffix — including the first — so which
row happens to be first on screen never decides who owns the bare slug.
Remaining collisions (identical texts, role slugs) get _2, _3, … from
a per-slug counter: O(n) in total, never a rescan from 2. Nodes whose
slug is unique keep exactly the ID they always had.
Implementation
static StableIdAssignment assign(
NormalizedTree tree, {
required String screen,
}) {
final n = tree.nodes;
final stableIds = List<String>.filled(n.length, '');
final paths = List<String>.filled(n.length, '');
final screenSlug = slugify(screen);
// Pass 1: base slug per node, and how many siblings share it. Sibling
// namespaces: one per parent, plus the shared overlay namespace.
final baseSlugs = List<String>.filled(n.length, '');
final groups = List<int>.filled(n.length, 0);
final roleCounts = <int, Map<String, int>>{};
final slugCounts = <int, Map<String, int>>{};
for (var i = 0; i < n.length; i++) {
final node = n[i];
final startsOverlay = node.overlayId > 0 &&
(node.parentIndex == -1 ||
n[node.parentIndex].overlayId != node.overlayId);
final group = startsOverlay ? -2 : node.parentIndex;
groups[i] = group;
final roles = roleCounts.putIfAbsent(group, () => <String, int>{});
final occurrence = roles[node.role] ?? 0;
roles[node.role] = occurrence + 1;
final slug = generateStableId(
parentPath: '',
role: node.role,
siblingPosition: occurrence,
identifier: node.identifier,
text: node.text,
);
baseSlugs[i] = slug;
final counts = slugCounts.putIfAbsent(group, () => <String, int>{});
counts[slug] = (counts[slug] ?? 0) + 1;
}
// Pass 2: ids, pre-order (a parent's id exists before its children's).
final usedByParent = <String, Set<String>>{};
final nextSuffix = <String, Map<String, int>>{};
for (var i = 0; i < n.length; i++) {
final node = n[i];
final parentSibPath =
node.parentIndex == -1 ? '' : paths[node.parentIndex];
paths[i] = parentSibPath.isEmpty
? '${node.siblingIndex}'
: '$parentSibPath.${node.siblingIndex}';
final parentStable = groups[i] == -2
? '$screenSlug.overlay'
: node.parentIndex == -1
? screenSlug
: stableIds[node.parentIndex];
final used = usedByParent.putIfAbsent(parentStable, () => <String>{});
var slug = baseSlugs[i];
if (node.identifier.isEmpty &&
node.text.isNotEmpty &&
(slugCounts[groups[i]]![slug] ?? 0) > 1) {
final d = contentDisambiguator(node.text, node.normalizedText);
if (d.isNotEmpty) slug = '${slug}_$d';
}
if (used.contains(slug)) {
final counters =
nextSuffix.putIfAbsent(parentStable, () => <String, int>{});
var k = counters[slug] ?? 2;
while (used.contains('${slug}_$k')) {
k++;
}
counters[slug] = k + 1;
slug = '${slug}_$k';
}
used.add(slug);
stableIds[i] = '$parentStable.$slug';
}
return StableIdAssignment(stableIds: stableIds, paths: paths);
}