# Element Resolver and Locator Architecture in ego‑lite: A Complete Technical Guide

> Explore the ego-lite element resolver and locator architecture. This technical guide details its layered design for seamless DOM target translation and error handling.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: architecture
- Published: 2026-08-25

---

**The ego‑lite element resolver translates user-supplied selectors into concrete DOM targets through a layered architecture that separates parsing, resolution logic, and Chrome DevTools Protocol (CDP) interaction, with built‑in fallback mechanisms and transient vs. permanent error classification.**

The **element resolver and locator system** in ego‑lite is the bridge between high‑level automation commands and actual browser DOM manipulation. Designed for the `citrolabs/ego-lite` repository, this module handles everything from CSS selectors to accessibility‑tree queries, ensuring robust element discovery across frames and dynamic page states.

## High‑Level Architecture Overview

The resolver operates through **six distinct phases**, each handled by dedicated functions in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts):

| Phase | Responsibility | Entry Point |
|-------|---------------|-------------|
| **Parse** | Convert raw strings to structured locator objects | `parseLocator` (lines 214–258) |
| **Reference resolution** | Map `@123` refs to stored metadata | `parseRef` → `resolveElementCenter`/`resolveElementObjectId` |
| **Role‑based fallback** | Query AX tree when refs are stale | `findBackendNodeIdByRoleName` (lines 496–511) |
| **Locator execution** | Run generated JavaScript in page context | `buildLocatorFindJs`, `buildLocatorCountJs`, `buildLocatorCenterJs` |
| **CDP interaction** | Retrieve box models or object IDs | `DOM.getBoxModel`, `DOM.resolveNode`, `Runtime.evaluate` |
| **Error handling** | Classify failures for retry logic | `ElementResolutionError` (lines 4–10) |

This design decouples **what** to find from **how** to find it, enabling extensible locator types without modifying core resolution logic.

## The Parsing Layer: From Strings to Structured Locators

The `parseLocator` function (lines 214–258) recognizes multiple locator prefixes and produces a uniform object structure:

- **CSS selectors**: `css:button.primary`
- **XPath expressions**: `xpath=//div[@class='modal']`
- **Text content**: `text:Submit`
- **Accessibility roles**: `role:button[name="OK"]`
- **Href targets**: `href:/dashboard`
- **Nth/last indexing**: `css:li:nth(3)` or `css:li:last`

Parsed output follows the pattern `{ kind, value, nth?, name?, raw }`. For example, `role:button[name="Submit"]` becomes:

```typescript
{
  kind: "role",
  role: "button",
  name: "Submit",
  raw: "role:button[name=\"Submit\"]"
}

```

This normalization allows downstream resolution code to handle all locator types through a single interface.

## Reference Handling: The @ref System

Numeric refs (`@123`) provide stable identifiers across page mutations. The resolution flow:

1. **`parseRef`** extracts the numeric ID from the `@` prefix
2. The resolver queries `refMap` (maintained in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)) for stored metadata including:
   - Stored `backendNodeId` from previous snapshots
   - Accessibility `role` and `name` for fallback lookup
3. If the `backendNodeId` is valid, the resolver issues `DOM.getBoxModel` or `DOM.resolveNode`
4. If stale, the system falls back to **accessibility role lookup** using the stored role/name pair

Key functions in this path:
- `resolveElementCenter` (lines 63–119) — returns `{x, y, sessionId}` for pointer actions
- `resolveElementObjectId` (lines 149–215) — returns CDP `objectId` for DOM operations

## Role‑Based Accessibility Tree Lookup

When refs fail or selectors explicitly use `role:`, the resolver queries Chromium's **Accessibility (AX) tree** via `Accessibility.getFullAXTree`:

- **`findBackendNodeIdsByRoleName`** (lines 324–366): Walks AX nodes, filters by role and optional name, returns array of backend DOM node IDs
- **`findBackendNodeIdByRoleName`** (lines 496–511): Selects nth or last match from the filtered set
- **`findUniqueBackendNodeIdByRoleName`** (lines 669–677): Asserts exactly one match, throws otherwise

This AX‑driven approach is resilient to DOM restructuring since roles and accessible names remain stable across visual redesigns.

## JavaScript Builder Pattern for Dynamic Execution

For non‑ref, non‑role locators, the resolver generates and executes JavaScript snippets via `Runtime.evaluate`. Three builders handle common operations:

| Builder | Purpose | Lines |
|---------|---------|-------|
| `buildLocatorFindJs` | Locate single element, return its center or object reference | 514–547 |
| `buildLocatorCountJs` | Count matching elements for existence/visibility checks | 555–575 |
| `buildLocatorCenterJs` | Compute precise center coordinates accounting for transforms | 778–795 |

These builders leverage `queryAllExpression` from [`src/locator-query.js`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.js) to support the full locator syntax within generated page scripts.

## Central Coordination: resolveElementCenter and resolveElementObjectId

Both entry points implement identical resolution priority:

1. **Attempt ref resolution** if input starts with `@`
2. **Parse and execute locator** via JS builders for standard selectors
3. **Fall back to role lookup** if initial attempts fail

```typescript
// Example: Resolve click coordinates from a CSS locator
const center = await resolveElementCenter(
  cdpClient,           // BrowserRuntime CDP wrapper
  sessionId,           // Target session (handles iframe context)
  refMap,              // Snapshot reference map
  'css:button.primary' // Locator string
);
// Returns: { x: 342, y: 217, sessionId: '...' }

```

```typescript
// Example: Obtain object ID for complex DOM operations
const { objectId } = await resolveElementObjectId(
  cdpClient,
  sessionId,
  refMap,
  '@42'  // Reference from previous snapshot
);
// objectId enables DOM.getAttributes, DOM.setAttributeValue, etc.

```

## Error Classification: Transient vs. Permanent Failures

The `ElementResolutionError` class (lines 4–10) powers ego‑lite's retry semantics:

- **`transient`** — Element not yet rendered, multiple ambiguous matches, or frame context switching. Callers should retry with appropriate delays.
- **`permanent`** — Invalid selector syntax, zero matches on stable page, or irrecoverable CDP errors. Retry would not succeed.

The helper `selectorResolutionError` (lines 52–61) standardizes error creation across all resolution paths:

```typescript
throw new ElementResolutionError(
  'transient',
  `Element not found: ${locator.raw}`
);

```

## Cross‑Frame Resolution with Session Management

Elements within iframes require context‑aware CDP sessions. The `resolveFrameSession` helper (lines 400–448):

- Maps frame IDs to dedicated `iframeSessions`
- Selects the correct session before issuing `DOM.*` or `Runtime.*` calls
- Ensures isolation between parent page and iframe execution contexts

This enables seamless interaction with embedded content without manual frame switching.

## Key Implementation Files

| File | Role in Element Resolution |
|------|---------------------------|
| [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Core parsing, resolution orchestration, and CDP interaction |
| [`src/locator-query.js`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.js) | DOM query expression builder for JS snippet generation |
| [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | Reference storage and metadata management |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Public API exposing `center()`, `objectId()`, and related helpers |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | CDP session lifecycle and low‑level command dispatch |

## Summary

- The **element resolver and locator system** in ego‑lite separates concerns across parsing, reference management, AX‑tree querying, and dynamic script execution
- **Six resolution phases** handle everything from CSS selectors to numeric refs, with automatic fallback to accessibility roles
- **`ElementResolutionError`** enables intelligent retry logic through transient/permanent classification
- **Cross‑frame operations** work transparently via session resolution in `resolveFrameSession`
- The architecture supports extensibility: new locator prefixes require only parser updates and corresponding JS builder logic

## Frequently Asked Questions

### What locator types does ego‑lite support?

ego‑lite supports CSS selectors (`css:`), XPath (`xpath=`), text content (`text:`), accessibility roles (`role:`), href targets (`href:`), label references (`label:`), and numeric snapshot refs (`@N`). Each parses to a uniform object consumed by the resolution pipeline.

### How does ego‑lite handle stale element references?

When a stored `backendNodeId` becomes invalid, the resolver automatically falls back to **role‑based accessibility tree lookup** using the cached role and name metadata from the original snapshot. This AX fallback provides resilience against DOM restructuring.

### What triggers a transient vs. permanent error?

**Transient errors** occur when elements are not yet rendered, multiple matches exist, or frame contexts are switching—situations where retry may succeed. **Permanent errors** indicate invalid selector syntax, guaranteed zero matches, or irrecoverable protocol failures where retry would not help.

### Can ego‑lite interact with elements inside iframes?

Yes. The `resolveFrameSession` helper maintains a mapping of frame IDs to dedicated CDP sessions. When resolving locators targeting iframe content, the system automatically selects the correct session before executing DOM or Runtime commands.