Layer 2 — Recorded agent runs Planned
An agent is built to drive your app for real across a task matrix — core flows (onboarding, search, checkout) times environment variants (dark mode, 200% font, VoiceOver on, different locales, low connectivity) — and to record every failed run step by step, so the customer does not ask "is this true?" and instead watches it.
None of the nine agent-layer rules is active in catalog 2.4.0. The orchestration exists and the evidence format is fixed, but until those rules go active no report contains a row from this layer, and we would rather say that on this page than let a buyer infer otherwise from a diagram.
Where each number was measured
Every row in the report names its environment — iPhone 15 (simulator) or iPhone 13 mini (device) — because "iPhone 15" on its own reads as hardware, and most of the matrix is not. Layout, contrast, target sizes, Dynamic Type, dark mode and locales are deterministic and reproduce identically on hardware, so they run on simulators; that is what makes a matrix of ninety-six runs affordable instead of six.
Two things do not survive that translation: VoiceOver — the actual screen reader, not the accessibility API a simulator exposes — and behaviour under a poor connection. We will not publish a simulator number for either and call it a device result. If you invite our tester on TestFlight, we install your build on a physical device and measure them properly. If you do not, those checks are published as not testable, which lowers your coverage percentage rather than quietly inflating it. Either way the report says which happened.
What you send us
One artifact: your app built for the iOS Simulator, zipped — what xcodebuild -sdk iphonesimulator produces, not an .ipa. A device build is compiled for arm64 hardware and signed for it, and a simulator cannot install one. So we refuse it in the app the moment you pick the file, rather than two days later inside a queue you cannot see.
xcodebuild -scheme YourScheme -sdk iphonesimulator -configuration Debug -derivedDataPath ./ap-build && cd ap-build/Build/Products/Debug-iphonesimulator && zip -ry YourApp.app.zip YourApp.app
If you would rather run one thing than assemble two, readyaudit-package does exactly this and nothing else. About a hundred lines of bash, MIT licensed, published as readable source on purpose: we are asking you to run a script against your own codebase while telling you it makes no network call, keeps no key and reports nothing home. That claim is only worth something if you can check it yourself in under a minute. Read it before you run it.
Your build is deleted the moment the audit completes and the report stamps the exact UTC time — what happens to it in between.
Which build a report is about
A version string is something a developer types. Two builds can both say 3.4.1 (204) and differ across the entire accessibility layer, so every report also carries a build digest: one number computed from the bytes of the bundle we actually drove. It answers one question and no other — is the artifact in your hand the artifact this report is about? It is not a signature and it proves no authorship. It is there so that a procurement reviewer, or you six months later, can hold an .app next to the PDF and get a yes or a no instead of a shrug.
That only works if you can recompute it without trusting us and without running our code. So the algorithm — readyaudit-bundle-sha256/1 — has no options, no salt and no timestamps, and it is written out here in full rather than summarised:
- Start from the extracted .app directory, never the zip. Zip bytes carry the compression level, the archiver's version and the order it happened to walk the tree. None of that is your program.
- Walk it recursively. Regular files and symlinks are included. A directory contributes nothing of its own — its children carry the paths. Anything else, such as a fifo or a device node, is a hard error rather than a silent skip.
- Skip any entry whose basename is exactly .DS_Store, at any depth. Finder writes those into folders you merely looked at, and a digest that moves because someone opened a window is a digest nobody trusts twice. It is the only exclusion, and naming it here is the point.
- Take each entry's path relative to the bundle root, with / separators and no leading ./. The .app's own name is not part of any path: renaming MyApp.app does not change the program, and a browser renames a second download for you.
- Hash each entry to lowercase hex SHA-256 — a regular file by its contents, a symlink by its target string, as bytes, unresolved and not followed.
- Record one character of mode: x for a regular file with any execute bit set, f for one without, l for a symlink. Nothing else about permissions, because nothing else survives a zip round-trip or a copy onto another volume, and a digest that changes when the file is merely moved is worse than no digest. That one bit is kept because an executable which has lost its +x will not launch, and that is a different artifact.
- Sort the entries by relative path, raw bytes ascending (LC_ALL=C). Not by locale, not case-insensitively.
- Build the manifest: one line per entry, hash, space, mode, space, relative path, newline. No header, no trailer, no count line.
- The digest is sha256: followed by the hex SHA-256 of that manifest.
Two numbers are published beside it and are not inputs to it: the file count, which is the number of manifest lines, and the byte count, which is the total hashed content length — for a symlink, the length of its target string. They exist so a mismatch can be diagnosed and not merely announced. A wildly different file count says you have a different build; an identical file count with a different digest says you have the same build, modified.
Four programs compute this number, and they are built to disagree loudly rather than quietly. Your Mac measures the bundle at intake, before anything is uploaded. The worker measures the extracted build it is about to drive. The API compares those two exactly once, at delivery, and deliberately never computes a third of its own — two independent measurements are a comparison, one is a restatement. The fourth runs somewhere we do not control, which is the only place that matters: your machine.
./verify report.json MyApp.app.zip
verify ships in the same readyaudit-package repository, needs only shasum and unzip, and exits 0 for the same artifact and 1 for a different one. A missing file or a bad argument exits 2 instead, on purpose: a tool whose failure mode looks like a verdict would tell you your build was wrong when you had only mistyped a path. And if you would rather not run our script at all, the nine steps above are the whole specification — reimplement them in whatever you already have open, and get the same number.
The digest is printed in full on every surface that carries provenance, never truncated to look tidier. A report without one does not exist: it fails schema validation before it can be delivered. And if a later audit of the same app arrives carrying a version string we have already seen with bytes we have not, the report states that on its face, with the earlier digest beside the current one — rebuilding without bumping a build number is ordinary practice, but it is the single fact that most often explains why an older report looks like it does not match the build in your hand.
Layer 3 — Narrative lens Planned
Six curated lenses — motor-limited, low-vision, first-time, impatient-expert, older, screen-reader — are built to interpret and prioritize the raw findings from Layers 1 and 2. The model narrates evidence; it never produces data or numbers. This layer is orchestrated but not shipped: a report generated today prints its findings without them.
The reproducibility filter
Agent runs are not deterministic: the same task can take different paths. So the pipeline is built to run every runtime task three times and report the median, and to drop any runtime finding that does not reproduce in at least two of three attempts before it reaches your report.
You see the ratio on every card, and the ratio is not rounded up for presentation. A check read once from a single accessibility tree publishes 1/1 — reading one file three times would be theatre, and printing 3/3 for it would claim a reproducibility we did not earn.
Versioned rules
The rule catalog is versioned, and every report records the exact version it was audited against. When a rule changes, past reports still say what they meant. Today that is catalog 2.4.0: 74 rules, 13 of them active.
Why we do not report false positives
A finding you have to argue with costs more trust than it is worth. If a rule is not sure, it stays out of the report. We would rather under-report cleanly than pad a score with noise — the report has to be something you can forward to your team without a caveat.