# ui_elements.json — runtime view-tree variant (producer schema)

This is the schema **emitted by `skills/hmos-test-case-generation/tools/convert_to_ui_elements.ts`** (Step 1 of the viewtree pipeline:
`tools/bfs-crawl/android_viewtree_bfs_crawler.ts` dump
→ `convert_to_ui_elements.ts`). It reuses the element /
flow **field names** for TCG-derive compatibility, but carries **runtime locators** (`text`/`bounds`)
instead of source ids / `platform_path`, plus a top-level `gaps[]`.

## Top-level

```json
{
  "app_info": { ... },
  "pages":  [ ... ],
  "flows":  [ ... ],
  "gaps":   [ ... ],
  "extraction_note": "..."
}
```

## `app_info`

| field | source | notes |
|---|---|---|
| `app_name` | CLI `--app-name` | may be `null`; **never a literal in the script** |
| `package_name` | CLI `--package` or inferred from `meta.activity_info` | |
| `tech_stack` | inferred | `"Jetpack Compose (single-Activity)"` when 1 Activity + 0 resource-id, else `"Android (runtime view-tree)"` |
| `description` | CLI `--description` | may be `null` |
| `source` | fixed | declares runtime-truth locators, no source paths |

## `pages[]`

```json
{
  "page_id": "p0017",                         // short stable id: 'p' + the page_NNNN ordinal
  "page_name": "page_0017_MainActivity",      // the raw dump dir name
  "page_name_cn": "就业",                      // mechanical: repr_text[0] (or a vision lift, future)
  "component_type": "Screen",
  "component_name": "MainActivity",           // the resolved Activity (often single, for Compose)
  "description": null,
  "repr_text": ["就业", "全部职业", "名企招聘"],  // top texts, for req matching
  "elements": [ ... ],
  "_bfs_page_id": "page_0017_MainActivity",
  "_display_texts_ref": "page_0017_MainActivity/page.parsed.json#display_texts"
}
```

- Only **canonical** pages appear (near-duplicates collapsed via Stage-0 `duplicate_of`).
- **Assertion targets** (display-only texts) are NOT inlined — they live per-page in
  `page.parsed.json` under `display_texts`, pointed to by `_display_texts_ref` (keeps the single file
  small).

### `elements[]`

```json
{
  "element_id": "quanbuzhiye_3",
  "element_name": "全部职业",                  // the on-screen copy (or icon@<center> when text-less)
  "element_type": "Clickable | TextInput | Image",
  "locator": {"type": "text", "value": "全部职业"},   // OR {"type":"bounds","value":"[x1,y1][x2,y2]"}
  "bounds": [120, 480, 360, 620],            // [left, top, right, bottom]
  "source": "bfs-runtime"                     // or "bfs-runtime-clicked" for click_path-synthesized
}
```

- `locator.type` is **`text`** (preferred — the runtime on-screen copy) or **`bounds`** (icon-only).
- **No** `id` locator, **no** `platform_path`, **no** `source_location` — these are unobtainable from a
  runtime view-tree and are never fabricated.
- An element with `source:"bfs-runtime-clicked"` was added because `click_path` proves it was clicked at
  runtime even though it was not in that page's deduped clickable set (e.g. a list item) — honest, not
  invented.

## `flows[]` — real per-edge directed graph

```json
{
  "flow_id": "p0017_e1",
  "source_page_id": "p0001",                  // resolved by click_path-prefix reconstruction; may be null
  "trigger_event": {
    "event_id": "p0017_e1_click",
    "event_type": "CLICK",
    "target_element_id": "jiuye_0",           // element on the SOURCE page that was clicked
    "description": "Click 'Employment'"
  },
  "target_page_id": "p0017",
  "transition_type": "Navigate_compose",      // honest default for Compose single-Activity
  "source": "bfs-click-path"
}
```

- **Parent resolution** is deterministic: a page P's `source_page_id` is the unique page whose entire
  `click_path` equals P's path minus its last step, matched on `(element, center)` per step. If
  unresolvable → `source_page_id:null` + a `gaps[]{type:flow}` entry (no name-guessing).
- `transition_type` / `wait_time_ms` are coarse/absent (runtime-unknown). Consumers treat absent
  transition metadata as a plain CLICK navigation.

## `gaps[]` — structural only

```json
{"type": "element | page | flow", "location": "<page_id>", "reason": "<explanation>"}
```

| type | emitted when |
|---|---|
| `element` | page has 0 text-locatable clickables after dedup (only icon-only / vision-pending) |
| `page` | `view.xml` parse failed / empty view-tree |
| `flow` | a page's `click_path` prefix (its parent) is absent from the dump → edge source unresolved |

> These are the **only** gaps this producer can detect mechanically. The high-value
> "req references a page the crawl never reached" gap requires `req.md` access and so is produced by
> **TCG derive** at fusion time, not here — consumer gap surfacing happens via `review_notes` logging,
> see §Consumer rules 3 below.

---

## Consumer rules (TCG generator S2/S3)

The TCG generator consumes this `ui_elements.json` as a soft reference. Four rules govern the viewtree-derived shape:

### 1. Locators are runtime screen copy — adopt verbatim

`element_name` / `locator.value` (type=`text`) is the literal on-screen text captured at runtime, not a translation. Adopt it **verbatim** (`「{copy}」`) at the **highest** reliability tier — skip the translation-degrade chain, never flag it in `translations_flagged_low_reliability`. `bounds` is `[left, top, right, bottom]`; an element may additionally serve as a positional disambiguator when two controls share visible text.

### 2. Flow tolerance — absent metadata is a plain CLICK

`transition_type` may be coarse/absent and `wait_time_ms` absent — treat absent transition metadata as a plain **CLICK** navigation; do not block, do not flag. A `source_page_id: null` edge is an unresolved-parent gap (the producer already emitted `gaps[]{type:flow}`) — route it to breakpoint inference, log it as a `review_notes` "navigation gap" (see §3), never fabricate a source.

### 3. gaps → `review_notes` logging (no audit_hooks field)

The produced `ui_elements.json` carries **no** `audit_hooks` / `ui_elements_gaps` / `nav_gap_unresolved`
fields — those do not exist in the converter output. Gap surfacing to humans happens via **`review_notes`
logging**: per `agents/test-case-generation-generator.md` (Step 2, navigation completion), an
unresolved parent / `source_page_id: null` edge is recorded as a `review_notes` "navigation gap" entry,
**never fabricating** a source and keeping the spec's original wording.

The producer emits **only** structural gaps (element/page/flow — see §gaps above). The high-value
"req references a page the crawl never reached" gap requires `req.md` access and is produced by
**TCG derive** at fusion time, also surfaced via `review_notes`, not by this producer.

### 4. Out-of-line `display_texts`

Each page carries `_display_texts_ref` → `"<page_dir>/page.parsed.json#display_texts"`, the scroll-aggregated display-only text list. When an action/assertion needs to verify on-screen text beyond the interactive `elements[]`, consult that list rather than assuming the JSON is exhaustive.
