# Traversal Log — Data Schema

`traversal-log-2026-09-01.jsonl.gz` — 95,524 records, one JSON object per line,
UTF-8, gzip. Uncompressed ≈ 101 MB.

Each record is **one browser visit to one page of one municipal website**, made
by an automated agent that pressed <kbd>Tab</kbd> on the live page and recorded
where focus landed.

Field names are in Russian — this is the working language of the project and the
log is published exactly as the crawler wrote it, without translation, so that
what you read is what was recorded.

---

## Fields

| field | type | present | meaning |
|---|---|---|---|
| `url` | string | 95,524 | the exact address visited |
| `город` | string | 95,524 | city name (*gorod*) |
| `штат` | string | 95,524 | two-letter state code (*shtat*) |
| `страница_номер` | int | 95,524 | 1–8, position in the fixed page set |
| `страница_тип` | string | 95,524 | page type — see below |
| `шагов_до_цели` | int | 95,524 | Tab presses until the goal was reached, or until traversal stopped |
| `секунд` | int/float | 95,524 | wall-clock seconds spent on this page |
| `где_оборвалось` | string | 95,524 | human-readable label of the element where traversal ended |
| `причина` | string | 95,524 | why it ended there |
| `есть_метки_у_полей` | bool | 95,524 | whether form fields carried accessible labels |
| `контраст_ок` | bool | 95,524 | whether the focus indicator was visible against its background |
| `скриншот` | string | 95,524 | screenshot filename — matches `evidence-hashes-*.txt.gz` |
| `вердикт_человека` | string | 95,524 | **the outcome** — see verdicts below |
| `что_сказал_скрипт` | string | 95,524 | what the automated rule-checker said: `доступно` (accessible) / `недоступно` |
| `вендор_платформы` | string | 95,524 | CMS vendor: CivicPlus, Granicus, Municode, Drupal, WordPress, Unknown |
| `когда_замер` | string | 84,765 | ISO timestamp of the visit |
| `источник_даты` | string | 84,765 | how the timestamp was established when not recorded directly |
| `снимка_нет_потому_что` | string | 49,303 | why no screenshot exists (usually: the page never opened) |
| `путь_исправлен` | string | 8,087 | screenshot path corrected after a folder rename, with the date |
| `вне_реестра` | bool | 632 | address was not in the CISA registry |
| `вне_реестра_почему` | string | 632 | how such an address entered the crawl |
| `шагов_до_цели` ← `поле_с_опечаткой_сведено` | string | 427 | see *Known defects* |
| `устарела_дубль` | bool | 32 | a later visit to the same address superseded this record |
| `почему_устарела` | string | 32 | why it was superseded |
| `починено_01_09_2026` | string | 16 | correction applied, with what changed |
| `город_восстановлен` | string | 8 | city name recovered from the domain |

---

## The eight page types

Every municipality was visited at the same eight page types, each with its own
**task goal** — the thing a resident came to do:

| `страница_тип` | goal | measurable | reached goal |
|---|---|---|---|
| `documents` | find a public document | 5,807 | 15.8 % |
| `request311` | file a 311 service request | 5,750 | 18.9 % |
| `payments` | reach the payment step | 5,817 | 19.0 % |
| `meetings` | find minutes of a council meeting | 5,815 | 21.9 % |
| `home` | reach main navigation | 6,341 | 25.3 % |
| `jobs` | find a job posting | 5,819 | 29.5 % |
| `contacts` | find a contact method | 6,011 | 31.1 % |
| `calendar` | find a dated event | 5,911 | 40.9 % |

**Note on wording:** the goal is *task-specific*, not a payment goal. Payment is
one page type out of eight. Aggregate figures (74.6 % did not reach the goal,
25.4 % did) cover all eight goals; the payment goal alone is 19.0 %.

---

## Verdicts

| `вердикт_человека` | records | meaning |
|---|---|---|
| `ПОДТВЕРЖДЕНО_НЕИЗМЕРЯЕМО_ИЛИ_НЕДОСТУПНО` | 38,379 | traversal could not be performed — page never rendered, or blocked |
| `ФОРМАЛЬНЫЙ_ДОСТУП_НО_БАРЬЕР_ДЛЯ_ЧЕЛОВЕКА` | 21,305 | page opened; goal not reachable by keyboard |
| `ЧАСТИЧНЫЙ_БАРЬЕР_ФОКУСА_ИЛИ_МЕТОК` | 13,972 | goal reachable, but focus was lost or labels missing |
| `ДОСТУПНО_ДЛЯ_ЧЕЛОВЕКА` | 11,994 | goal reached by keyboard alone |
| `ДОМЕНА_НЕТ` | 3,950 | domain does not resolve |
| `ЗАКРЫТ` | 3,609 | bot protection refused the request |
| `СЕРВЕР_МОЛЧИТ` | 1,290 | connection timed out |
| `БИТОЕ_ШИФРОВАНИЕ` | 866 | TLS handshake failed |
| `ОШИБКА_СЕРВЕРА` | 159 | HTTP 5xx |

The last five (9,874 records) are **dead addresses**, not accessibility results.
Excluding them leaves **85,650 live records** — the denominator used for every
share on the study pages.

---

## How the headline numbers are computed

**53.8 % — scanner/traversal disagreement.**
Denominator: records where the automated checker said `доступно` **and** the
verdict is measurable → 25,966.
Numerator: of those, records where the goal was not reached → 13,972.
`13,972 / 25,966 = 53.8 %`

This is a *disagreement rate between two methods*, not a claim that 53.8 % of
sites are inaccessible. The study does not support the latter statement.

**74.6 % / 25.4 % — goal outcome.**
Of measurable traversals, 74.6 % did not reach the page's task goal; 25.4 % did.

**Stability.** The sample grew from 73,190 to 95,524 records — by a third — and
the disagreement rate moved from 54.1 % to 53.8 %. Three tenths of a point.

---

## Known defects — stated, not hidden

**Field-name typo.** In 427 records the crawler wrote the step count into
`шагов_do_цели` — Latin `do` instead of Cyrillic `до`. Any computation keyed on
the correct name silently missed them.

Effect on published figures: **none.** All 427 carry the verdict
`ПОДТВЕРЖДЕНО_НЕИЗМЕРЯЕМО_ИЛИ_НЕДОСТУПНО`, and unmeasurable records are excluded
from step statistics by construction. Had they been included, the mean step count
would move from 11.55 to 11.60.

In this published file the two fields are merged into `шагов_до_цели`, and the
record carries `поле_с_опечаткой_сведено` so the merge is visible. The class was
swept in full: exactly one field name of twenty-six mixes alphabets.

**Two forms of the `причина` field, and the short one misleads.** The reason
text exists in two shapes, and reading the log without knowing this will produce
a false contradiction.

| form | records | verdicts it appears with |
|---|---|---|
| long — «Человек успешно дошёл до цели за N шагов с видимым фокусом и поддержкой клавиатуры» | 10,913 | reachable 82.5 %, partial barrier 17.5 % |
| short — «Успешно за N шагов.» | 5,756 | reachable 52.0 %, **unmeasurable 37.2 %**, partial 10.8 % |

The word *успешно* (successful) in the short form does **not** mean the goal was
reached. In 2,494 of those records (43.3 %) traversal stopped on a *Skip to Main
Content* link — the service link that is first in tab order on most sites. The
agent pressed Tab once, landed there, and wrote "successful in 1 step". The
verdict is correct — the goal was not reached — but the text reads as if it was.

The long form never produces this: it names focus visibility and label presence
explicitly, and never co-occurs with an unmeasurable verdict.

**Read the verdict, not the reason text.** `вердикт_человека` is the outcome;
`причина` is an explanatory note whose short form predates the current wording
and is kept as written rather than rewritten after the fact.

A related case runs the other way: 2,536 records carry a "successfully reached"
reason with a *partial barrier* verdict. Every one of them has
`есть_метки_у_полей: false` — the goal was reached but form fields lacked
accessible labels, which is exactly what that verdict means. The verdict is
right in all 2,536; only the note is incomplete.

**What causes the 53.8 % gap.** Of the 13,972 pages where an automated scan
found nothing and traversal still failed, **11,436 — 81.8 % — failed for one
specific reason: the visible focus indicator was removed** (`outline: none` in
the site's CSS). The element exists and is reachable by Tab, so a rule-checker
inspecting markup reports no violation. But a keyboard user cannot see where
they are. That pattern appears on 4,027 of 11,923 sites (33.8 %), and in every
single case the automated scan had reported the page clean.

This is the mechanism behind the headline number, and it is only observable by
walking the page: the rule lives in a stylesheet, not in the markup.

**Repeat visits.** 32 records are superseded duplicates, flagged `устарела_дубль`.
Recomputing every share with one record per address-and-page-type pair moves the
headline figure by one hundredth of a point.

**Verdict taxonomy changed mid-study.** Before 30 August 2026 there was no
"dead site" verdict at all: everything that failed to render went to
`ПОДТВЕРЖДЕНО_НЕИЗМЕРЯЕМО_ИЛИ_НЕДОСТУПНО`. The distinction appeared on the 30th,
and the share of dead-site verdicts jumped from 0 % to 43.8 % that day. A visible
consequence: five states (WV, MT, WY, ND, NV) carry no dead-site verdict at all
across hundreds of records, because they were walked on 27–28 August.

51 records were reclassified on 01.09.2026 using a rule derived from post-30.08
records rather than invented — applied only where that rule was unambiguous
(≥85 % agreement). HTTP 403 (7,401 records) and timeouts (1,493) were **not**
touched: the rule reaches only 73 % and 70 % there, and guessing on the crawler's
behalf would be worse than leaving the ambiguity visible. Reclassified records
carry `вердикт_был` so the change can be reversed line by line.

**Missing screenshots.** 9,820 records have no screenshot, with the reason stated
in `снимка_нет_потому_что` — overwhelmingly because the page never opened. A
further 469 screenshots came out blank and are queued for re-capture; when that
happens, a new hash registry version will be published alongside the old one,
never replacing it.

---

## What this log does *not* claim

It does not stand in for a screen-reader user. It does not measure whether a page
was *comprehensible*. It answers one question: **could the goal be reached by
keyboard alone.**

No testing with blind participants was conducted, and none is claimed.

---

## Verifying a record

1. Pick any line. Note `url`, `страница_тип`, `скриншот`.
2. Find that screenshot filename in `evidence-hashes-2026-09-01.txt.gz` — the
   SHA-256 next to it is the hash of the image as captured.
3. That registry is anchored in Bitcoin via OpenTimestamps; the proof is
   `evidence-hashes-2026-09-01.timestamp.json`.
4. Re-run the traversal yourself against the same `url` and compare.

The anchor proves the registry existed in that state at that time. It does not
prove a screenshot was never re-taken later — re-captures are published as new
registry versions, with the previous version kept.

---

## Files

| file | records | contents |
|---|---|---|
| `traversal-log-2026-09-01.jsonl.gz` | 95,524 | this log — one browser visit per record |
| `us-registry-cisa.jsonl.gz` | 11,659 | **the sampling frame** — the CISA `.gov` registry the sample was drawn from |
| `measurement-dates.jsonl.gz` | 83,212 | when each page was measured |
| `tls-certificates.jsonl.gz` | 8,979 | TLS certificate seen at measurement time |
| `entity-classification.jsonl.gz` | 11,997 | how entities were sorted into types |
| `unreachable-reasons.csv.gz` | 905 | why sites failed to open — the 50.5 % unmeasurable |
| `platform-vendors.csv.gz` | 8 | breakdown by CMS vendor |
| `recaptured-screenshots.jsonl.gz` | 757 | log of screenshots re-taken after a blank capture |
| `evidence-hashes-2026-09-01.txt.gz` | 81,065 | SHA-256 of every screenshot |
| `evidence-hashes-2026-09-01.timestamp.json` | — | OpenTimestamps Bitcoin anchor |
| `dead-domains-2026-09-01.csv` | 1,498 | domains that do not resolve |
| `accessibility-study-tx/` | 88 | earlier Texas subset with manual verification |

**On the sampling frame.** `us-registry-cisa.jsonl.gz` is published so that
the sample can be audited, not just the results. Every municipality in the
registry was attempted; none was dropped for being hard to reach. Sites that
refused the crawler are recorded with a verdict, not excluded — an exclusion
would bias the sample toward the sites that were easiest to measure.

Screenshot paths in every file are filenames only; absolute paths from the
capture machine were stripped before publication, and 1,514 such paths were
replaced in the recapture log.

Author: Maksim Galatin. Self-funded; nobody paid for inclusion in the sample.
