qa-agent-kit 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qakit/docs/manifest.md ADDED
@@ -0,0 +1,96 @@
1
+ # The `device_qa.json` manifest
2
+
3
+ One file per app tells `tools/device_qa.py` and `tools/release_checks.py` what to build, which tests form which suite, how
4
+ to change the phone for each variant and where the store material lives. Paths are relative to the manifest's folder.
5
+ The conventional location is `<app>/release/testing/device_qa.json`, which is what `--app <slug>` resolves under
6
+ `--apps-root` (default `$QAKIT_APPS_ROOT`, else `<git top>/apps`). `--manifest <path>` works anywhere.
7
+
8
+ `examples/device_qa.json` is a complete example. `validate_manifest` in `tools/device_qa.py` rejects a manifest with a
9
+ message per mistake; `tests/test_device_qa.py` pins those messages.
10
+
11
+ ## Top level
12
+
13
+ | Key | Required | Meaning |
14
+ |---|---|---|
15
+ | `schema_version` | yes | `"1"` |
16
+ | `app` | yes | Display name; names the artifact folder and the contact sheet |
17
+ | `project` or `workspace` | one of them | `.xcodeproj` or `.xcworkspace` passed to xcodebuild |
18
+ | `scheme` | yes | Scheme that builds the app and its test targets |
19
+ | `process_name` | yes | The app's process name on the phone, used to pick crash and jetsam reports |
20
+ | `extension_processes` | no | Extension process names (widget, notification service) whose crashes also count |
21
+ | `suites` | yes | Name to list of `-only-testing` identifiers (`Target`, `Target/Class` or `Target/Class/test`) |
22
+ | `variants` | no | Name to variant (below). `default` is applied when no `--variant` is given |
23
+ | `bundle_id` | for perf, upgrade, release, traits | The app's bundle identifier (else read from `appstore_config`) |
24
+ | `team_id` | for release | The Apple team id (else read from `appstore_config`) |
25
+ | `signing` | no | `key_path`, `key_id`, `issuer_id` of an App Store Connect API key (below) |
26
+ | `appstore_config` | no | Path to a JSON whose `api` block holds `private_key_path`, `key_id`, `issuer_id` (an alternative to `signing`) |
27
+ | `derived_data` | no | DerivedData folder (default `<project folder>/build/DerivedData-device`); the run's build output is excluded from the source fingerprint either way |
28
+ | `fingerprint_root` | no | Folder whose edits during a run mark the round as mixed (default the project folder); set it when the app's core sources live beside the project folder |
29
+ | `xcodebuild_args` | no | Extra xcodebuild arguments, for example `["CODE_SIGN_STYLE=Automatic"]` |
30
+ | `traits` | for `traits` | `screens`: screen name to the launch arguments that open it (`{"map": ["-start", "map"]}`); default one launch with no arguments |
31
+ | `upgrade` | for `upgrade` | `seed_file` (a black-box UI test copied into the previous release's UI-test folder), `uitests_dir`, `seed` and `check` test identifiers, optional `copy` (gitignored files the old build needs) and `generator` (a project generator script to run in the old worktree) |
32
+ | `release` | for release checks | Store material (below); `version` overrides the store config's `app_store_version` for the What's New check |
33
+
34
+ ## Suites
35
+
36
+ A suite is what one `test --suite <name>` run executes. Common names:
37
+
38
+ - `smoke`: five or fewer tests that must be green within five minutes before anything else runs.
39
+ - `unit`, `ui`: the whole test targets.
40
+ - `qa`: the walkthrough class (`<App>QAWalkthroughTests`) that screenshots, audits and sweeps every screen.
41
+ - `qa-screens`: the same walkthrough split per screen, so a failing screen can be rerun alone.
42
+
43
+ ## Variants
44
+
45
+ A variant changes the whole phone, so system alerts and other apps render the same way. The runner applies it before the
46
+ suite and restores iOS defaults afterwards, even when the suite fails.
47
+
48
+ | Key | Values | devicectl call |
49
+ |---|---|---|
50
+ | `appearance` | `light`, `dark` | `device settings appearance --mode` |
51
+ | `text_size` | `extra-small` ... `extra-extra-extra-large`, `accessibility-medium` ... `accessibility-extra-extra-extra-large` | `device settings appearance --text-size` (accessibility sizes turn Larger Accessibility Sizes on) |
52
+ | `location` | `[latitude, longitude]` | `device simulate location coordinate` |
53
+ | `voiceover` | `true` | `device settings voiceover --enable`. The first time VoiceOver goes on, iOS 27.2 shows a welcome prompt over the app (Start VoiceOver Tutorial / Use VoiceOver / Cancel); call `QA.settleAfterLaunch(app)` right after `app.launch()` so the walkthrough's first tap is not swallowed |
54
+ | `env` | object | Forwarded to the UI-test runner as `TEST_RUNNER_<KEY>`; QAKit reads `QA_SWEEP`, `QA_LANG`, `QA_ALLOW_LABELS`, `QA_DISMISS_LABELS`, `QA_ALERT_HOSTS`, `QA_LOCALES` |
55
+
56
+ The variant name itself reaches the tests as `QA_VARIANT` and prefixes every screenshot name.
57
+
58
+ Typical set: `dark` and `large-text` and `small-text` and `english` with `QA_SWEEP=0` (layout variants: every screen
59
+ seen, no second sweep), one or two `location` variants for a location-aware app, and `voiceover` for a major release.
60
+
61
+ ## Signing
62
+
63
+ Running on a real device needs development signing. With an App Store Connect API key xcodebuild registers the phone and
64
+ mints profiles without an Xcode login (`-allowProvisioningUpdates -allowProvisioningDeviceRegistration` plus the
65
+ `-authenticationKey*` flags). `key_path` may be absolute, relative to the git top level that holds the manifest, or
66
+ relative to `$QAKIT_SECRETS_ROOT` (so a worktree can reach the key kept in the main checkout). Without a key the runner
67
+ still passes the provisioning flags and relies on the Mac's Xcode account.
68
+
69
+ ## Projects that pin App Store profiles
70
+
71
+ A project with `CODE_SIGN_STYLE = Manual` and `PROVISIONING_PROFILE_SPECIFIER` set (the usual shape after a release
72
+ script took over signing) cannot be signed for the test phone with the development certificate the key mints. `doctor`
73
+ reports it. Put the overrides in the manifest and every device command applies them:
74
+
75
+ ```json
76
+ "xcodebuild_args": ["CODE_SIGN_STYLE=Automatic", "PROVISIONING_PROFILE_SPECIFIER=", "CODE_SIGN_IDENTITY=Apple Development", "DEVELOPMENT_TEAM=ABCDE12345"]
77
+ ```
78
+
79
+ The archive you upload keeps the project's own settings; the overrides only reach the `test`, `traits`, `upgrade` and `perf`
80
+ builds.
81
+
82
+ ## Release block
83
+
84
+ Read by `tools/release_checks.py`.
85
+
86
+ | Key | Meaning |
87
+ |---|---|
88
+ | `info_plist` | The app's Info.plist (purpose strings, `CFBundleLocalizations`) |
89
+ | `resources_dir` | Folder that holds `<lang>.lproj/InfoPlist.strings` or `InfoPlist.xcstrings` |
90
+ | `source_dirs` | Swift source folders scanned for protected APIs (camera, location, ...) |
91
+ | `bundles` | One entry per code-signed bundle: `name`, `sources`, `privacy` (its `PrivacyInfo.xcprivacy`, or `null`) |
92
+ | `metadata_dir` | Folder of `<locale>.md` store metadata files with `## Name`, `## Subtitle`, `## Description`, `## Keywords`, `## What's New`, `## Promotional Text` sections |
93
+ | `review_notes` | The review notes markdown (4,000 character limit) |
94
+ | `app_id` | `TEAMID.bundle.id` as it must appear in the AASA file |
95
+ | `aasa_domain`, `aasa_paths` | Universal link domain and the paths the AASA must list (checked on Apple's CDN) |
96
+ | `link_ignore` | URL prefixes not fetched (API bases that reject GET) |
@@ -0,0 +1,69 @@
1
+ # Mutation testing with `tools/mutate.py`
2
+
3
+ `mutate.py` is a single-file mutation testing engine for any repo with a test command. It copies the project root to a
4
+ temporary folder, applies one mutant at a time, runs your build and test commands there and reports which mutants the
5
+ tests killed, which survived, which did not compile and which sit on lines no test runs.
6
+
7
+ ## Languages
8
+
9
+ - **Python**: tokenize and ast based. Comparison and arithmetic operators, boolean literals, `not` removal, call
10
+ statements (`store.save(x)` dropped when the call is a whole statement).
11
+ - **C-family syntax** (`.swift .js .jsx .ts .tsx .mjs .kt .go .java .cs`): regex on binary operators that have spaces on
12
+ both sides, which keeps `Array<Int>`, `->`, `0..<n`, prefix `!` and `-x` intact. Strings, comments and `#if` lines are
13
+ skipped.
14
+
15
+ ## Config keys
16
+
17
+ | Key | Meaning |
18
+ |---|---|
19
+ | `name` | Report name |
20
+ | `root` | Folder copied for each mutant (relative to the git top level that holds the config) |
21
+ | `sources` | Globs of files to mutate, relative to root |
22
+ | `exclude` | Globs left alone (views, generated code) |
23
+ | `copy_exclude` | Extra folders not copied (build output); `.git`, `.build`, `DerivedData`, `.venv`, `xcuserdata` and the like are always skipped |
24
+ | `setup` | Shell command run once per copy before `build` |
25
+ | `build` | Compile step; failure means the mutant is invalid (not counted) |
26
+ | `test` | Test step; failure or timeout means killed. `{orig}` real root, `{file}`/`{stem}` the mutated file, `{lcov}` coverage path |
27
+ | `coverage` | Command that writes lcov to `{lcov}`; mutants on unexecuted lines are reported uncovered and not run |
28
+ | `env` | Extra environment for the commands |
29
+ | `jobs` | Parallel copies (default 3); the engine runs under `nice -n 10` |
30
+ | `timeout_factor` | Per-mutant timeout as a multiple of the baseline test time (default 3) |
31
+ | `test_timeout` | Fixed per-mutant timeout in seconds when the test command differs from the baseline one |
32
+ | `operators` | Subset of `boundary equality logical arithmetic boolean negation drop-call` |
33
+ | `sweep_sample` | Seeded sample size for a full sweep (Xcode projects: one to two minutes per mutant) |
34
+
35
+ ## Command line
36
+
37
+ ```
38
+ --config <json> config file (or --root/--source/--build/--test inline)
39
+ --changed-since <ref> only lines added or changed since a git ref; 'latest-tag:<glob>' picks the newest matching tag
40
+ --file <path[:line]> limit to files or one line (repeatable)
41
+ --operator <name> limit operators (repeatable)
42
+ --max-mutants N --seed S seeded sample
43
+ --jobs N parallel copies
44
+ --max-survivors N exit 0 up to N survivors (for a gate that is tightening over time)
45
+ --out <dir> report folder (default $TMPDIR/mutation-reports/<name>-<time>)
46
+ --list print the mutants and stop
47
+ --keep keep the scratch copies
48
+ --resume reuse verdicts already in --out/results.jsonl
49
+ ```
50
+
51
+ Exit codes: 0 no survivors, 1 survivors (or uncovered), 2 setup or baseline failure. A failing baseline names the failing
52
+ test.
53
+
54
+ ## Reports
55
+
56
+ - `results.jsonl`: one verdict per mutant (file, line, operator, before, after, status, seconds, output tail).
57
+ - `mutation.md`: the human and agent readable report, survivors first, with the line to open.
58
+ - `mutation.json`: counts per status and per operator, the score (`killed / (killed + survived + uncovered)`).
59
+
60
+ Without `coverage`, a line no test reaches is reported as survived, not uncovered; a project whose tests skip in a fresh
61
+ clone (locale data, network fixtures) looks weaker than it is. Give the engine a coverage command when the test suite can
62
+ produce lcov.
63
+
64
+ ## Reading the score
65
+
66
+ The score is not a target to maximise by excluding files. It is a map: each survivor is one behaviour nobody checks. A
67
+ boundary survivor on a pricing or grading line is a bug waiting; a survivor in a log message is noise and takes a
68
+ `mutation-ok` comment. The gate that works in practice is "every mutant on a changed line is dead or explained", run
69
+ before merge (`--changed-since origin/main`) and before release (`--changed-since 'latest-tag:myapp/*'`).
qakit/docs/prose-qa.md ADDED
@@ -0,0 +1,49 @@
1
+ # Prose QA
2
+
3
+ `skills/prose-qa/SKILL.md` is the procedure. `tools/prose_check.py` is the mechanical half: seven deterministic gates
4
+ over a Markdown file, and a claims ledger skeleton for the verification step.
5
+
6
+ ## Gates
7
+
8
+ | Gate | Finds | Severity |
9
+ |---|---|---|
10
+ | `outline` | heading level jumps (H2 to H4), headings with no body, a document with exactly one H2 | WARN |
11
+ | `sentences` | sentences over `--max-words` (35; DEFECT past 1.5 times that), paragraphs over `--max-sentences` (7) | WARN / DEFECT |
12
+ | `duplicates` | the same sentence twice (six words or more, punctuation ignored) | DEFECT |
13
+ | `claims` | a number, percentage, year, currency or evidence word with no source marker in that sentence or its neighbours | WARN |
14
+ | `acronyms` | an acronym of three to six capitals used before `Full Name (ACR)` appears; common ones (API, JSON, PDF ...) skipped | WARN |
15
+ | `links` | empty link targets, bare URLs in prose, the same link text pointing at two targets | DEFECT / WARN |
16
+ | `tells` | em dashes, "In conclusion", "Firstly", "It is worth noting", "delve", three sentences in a row with the same first word | WARN |
17
+
18
+ Source markers the `claims` gate accepts: `[1]`, `[^note]`, a Markdown link or URL, `(2024)`, `et al.`, `source:`,
19
+ `according to`, `p. 12`, `Table 3`, `Figure 2`.
20
+
21
+ ## The claims ledger
22
+
23
+ ```
24
+ line,claim,source_marker_nearby,source,quote,verdict
25
+ 12,"Revenue grew 42% in 2025.",no,,,
26
+ ```
27
+
28
+ The reviewer fills `source` (URL, file, page), `quote` (the exact words that support the claim) and `verdict`:
29
+
30
+ | Verdict | Meaning |
31
+ |---|---|
32
+ | SUPPORTED | the quote says what the sentence says, numbers digit for digit |
33
+ | PARTIAL | the quote says less; note what the sentence adds beyond it |
34
+ | NOT IN SOURCE | the source exists but does not say this |
35
+ | NO SOURCE | nothing found; the claim gets a source or leaves the document |
36
+
37
+ A document passes the claims step when every row is SUPPORTED or PARTIAL with the addition justified in the text.
38
+
39
+ ## Running it
40
+
41
+ ```bash
42
+ python3 tools/prose_check.py memo.md # findings, defects first; exit 1 on any DEFECT
43
+ python3 tools/prose_check.py memo.md --gate claims --gate links
44
+ python3 tools/prose_check.py paper.md --claims claims.csv --max-words 40
45
+ python3 tools/prose_check.py memo.md --json | jq '.[] | select(.severity=="DEFECT")'
46
+ ```
47
+
48
+ Code blocks, front matter, tables and HTML comments are skipped. The sentence splitter understands `.`, `!`, `?` and the
49
+ CJK full stop, so Korean and Japanese documents get the structural gates; the lexical tells are English.
qakit/docs/pypi.md ADDED
@@ -0,0 +1,40 @@
1
+ # QAKit on PyPI
2
+
3
+ ```bash
4
+ pip install qa-agent-kit # the Python tools (the import name and the commands stay `qakit`); the Swift package stays on SwiftPM (Package.swift)
5
+ ```
6
+
7
+ ## The five commands
8
+
9
+ | Command | Same as, from a checkout |
10
+ |---|---|
11
+ | `qakit-mutate` | `python3 tools/mutate.py` |
12
+ | `qakit-prose` | `python3 tools/prose_check.py` |
13
+ | `qakit-device` | `python3 tools/device_qa.py` |
14
+ | `qakit-release-checks` | `python3 tools/release_checks.py` |
15
+ | `qakit-install-skills` | `python3 tools/install_skills.py` |
16
+
17
+ Flags are identical. Each command is a shim in `src/qakit/_cli.py`. The wheel copies `tools/` to `qakit/tools/`; the
18
+ shim puts that folder first on `sys.path` and calls the tool's `main()`, so the sibling imports (`device_qa` needs
19
+ `device_lock`) resolve as they do for a plain script. The scripts are unchanged and still run from a checkout.
20
+
21
+ ## Where the skills are after a pip install
22
+
23
+ The wheel also copies `skills/`, `docs/` and `AGENTS.md` under the package, so `qakit-install-skills` links
24
+ `.claude/skills/<skill>` (or the Codex, Cursor or Gemini equivalent) to `<site-packages>/qakit/skills/<skill>`. Find the
25
+ folder with `python3 -c "import qakit; print(qakit.SKILLS)"`. Run `qakit-install-skills --target all --uninstall` before `pip uninstall`.
26
+
27
+ ## Releasing
28
+
29
+ The version is read at build time from the newest `## [x.y.z]` heading in `CHANGELOG.md`, so a release starts with the
30
+ changelog step in `RELEASING.md`, then:
31
+
32
+ ```bash
33
+ python3 -m venv /tmp/qakit-release && /tmp/qakit-release/bin/pip install build twine
34
+ rm -rf dist && /tmp/qakit-release/bin/python -m build # dist/qakit-<version>.tar.gz and the wheel
35
+ /tmp/qakit-release/bin/twine check dist/*
36
+ /tmp/qakit-release/bin/twine upload dist/* # token in ~/.pypirc, or TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-...
37
+ ```
38
+
39
+ Before `twine upload`, install the wheel into a clean venv and run `qakit-mutate --help` outside the checkout.
40
+ The sdist excludes `Sources/`, `Package.swift`, `.github/` and `assets/`; the wheel excludes the tests.
qakit/docs/report.md ADDED
@@ -0,0 +1,64 @@
1
+ # What a run leaves behind
2
+
3
+ `tools/device_qa.py test` writes everything under one folder: `--out`, else `$QAKIT_ARTIFACTS/<app>/<stamp>`, else
4
+ `/tmp/qakit/<app>/<stamp>`. The folder is the evidence an agent reviews and the evidence a verdict cites.
5
+
6
+ ```
7
+ <out>/
8
+ summary.json the run's verdict and pointers (schema below)
9
+ <variant>-<suite>.log full xcodebuild output
10
+ <variant>-<suite>.xcresult the result bundle (open in Xcode, or export again with `shots`)
11
+ <variant>-<suite>-shots/
12
+ index.md contact sheet: one section per test, every attachment in order, failures marked
13
+ manifest.json xcresulttool's export manifest
14
+ *.png, *.txt screenshots (`QA.snap`) and notes (`QA.note`, accessibility audit reports)
15
+ crashes/ .ips crash and jetsam reports from the phone that name the app or its extensions
16
+ ```
17
+
18
+ ## summary.json (`qakit.device-qa.summary.v1`)
19
+
20
+ ```json
21
+ {
22
+ "schema": "qakit.device-qa.summary.v1",
23
+ "app": "MyApp",
24
+ "device": {"udid": "...", "name": "iPhone", "transport": "wired"},
25
+ "started": "2026-10-28T09:00:00+00:00",
26
+ "runs": [
27
+ {"variant": "dark", "suite": "qa", "executed": 9, "skipped": 0, "failures": 1,
28
+ "failed_cases": ["MyAppQAWalkthroughTests testWalkSettings"], "succeeded": false,
29
+ "screenshots": 143, "log": ".../dark-qa.log", "xcresult": ".../dark-qa.xcresult"}
30
+ ],
31
+ "crashes": [".../crashes/MyApp-2026-10-28-090501.ips"],
32
+ "sources_changed_during_run": false,
33
+ "succeeded": false
34
+ }
35
+ ```
36
+
37
+ `succeeded` is true only when every run succeeded, no crash report named the app, and no source under the app changed while
38
+ the run was going (a round that compiled a mix of two trees describes neither). The exit code follows it.
39
+
40
+ ## Other commands
41
+
42
+ | Command | Leaves |
43
+ |---|---|
44
+ | `perf` | `perf-<stamp>/launch.trace` (open in Instruments) and `perf.json` with the potential-hangs and hang-risks rows |
45
+ | `traits` | `traits-<stamp>/<screen>.png` after the changes and `crashes/` |
46
+ | `release` | `release-<stamp>/export/*.ipa`, `launch-<n>.png` per cold launch and `crashes/` |
47
+ | `upgrade` | `upgrade-<stamp>/old-seed.xcresult`, `new-check.xcresult`, their logs and shot folders |
48
+
49
+ ## Screenshot names
50
+
51
+ `QA.snap("<step>")` stores `"<variant> <step>"`. The walkthrough convention is `<screen> › <control>` for a sweep tap and
52
+ `a11y <screen>` for an accessibility report, so a reviewer can group the contact sheet by screen with a plain text search.
53
+
54
+ ## Reviewing a contact sheet
55
+
56
+ Read `index.md` top to bottom, one screen at a time: clipping, overlap, empty states, wrong facts, translation, contrast,
57
+ touch targets, attribution. For every suspected defect try to refute it (is it the variant? a test fixture? a real
58
+ bug?) before putting it on the defect list. Accessibility notes (`a11y ...` text attachments) go on the same list with
59
+ the issue type Apple reported.
60
+
61
+ ## Static checks
62
+
63
+ `tools/release_checks.py` prints one line per check (`PASS`, `WARN`, `FAIL`, the check ID from the protocol and the
64
+ evidence) and `--json` gives the same as a list. Any FAIL exits 1.
qakit/skills/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Agent skills
2
+
3
+ Three skills, written for coding agents that read `SKILL.md` files (Claude Code, Codex, and any tool that follows the
4
+ same convention). They are the operating procedure; the tools under `tools/` and the Swift package do the work.
5
+
6
+ | Skill | Scope |
7
+ |---|---|
8
+ | `qa-rigor` | Any code: test design, mutation testing, runtime checkers, real-environment check, verdict ledger |
9
+ | `device-qa` | Apple apps on a USB-attached test iPhone: walkthrough, variants, screenshots, static submission checks, release verdict |
10
+ | `prose-qa` | Memos, READMEs, design docs, papers: deterministic gates, claims ledger, argument check, cold read |
11
+
12
+ ## Install with the installer
13
+
14
+ ```bash
15
+ python3 tools/install_skills.py --target claude --scope project # this project's .claude/skills
16
+ python3 tools/install_skills.py --target all --scope user # ~/.claude/skills, ~/.agents/skills, ~/.cursor/rules, ~/.gemini
17
+ python3 tools/install_skills.py --target codex --scope project --project /path/to/repo
18
+ python3 tools/install_skills.py --target all --uninstall
19
+ ```
20
+
21
+ | Target | What it gets |
22
+ |---|---|
23
+ | `claude` | `.claude/skills/<skill>` symlink to `skills/<skill>`; Claude Code lists the skill and loads `SKILL.md` on use |
24
+ | `codex` | `.agents/skills/<skill>` symlink, and a pointer line in the project's `AGENTS.md` |
25
+ | `cursor` | `.cursor/rules/qakit-<skill>.mdc`, a rule that tells Cursor to read the `SKILL.md` |
26
+ | `gemini` | A pointer line in `GEMINI.md` (project) or `~/.gemini/GEMINI.md` (user) |
27
+
28
+ Existing files that are not QAKit links or rules are left alone and reported as `skip`. The installer is idempotent.
29
+
30
+ ## Install by hand
31
+
32
+ Project-wide (checked into your repo):
33
+
34
+ ```bash
35
+ git submodule add https://github.com/younatics/QAKit.git vendor/qakit
36
+ mkdir -p .claude/skills
37
+ ln -s ../../vendor/qakit/skills/qa-rigor .claude/skills/qa-rigor
38
+ ln -s ../../vendor/qakit/skills/device-qa .claude/skills/device-qa
39
+ ln -s ../../vendor/qakit/skills/prose-qa .claude/skills/prose-qa
40
+ ```
41
+
42
+ User-wide:
43
+
44
+ ```bash
45
+ git clone https://github.com/younatics/QAKit.git ~/qakit
46
+ for s in qa-rigor device-qa prose-qa; do ln -s ~/qakit/skills/$s ~/.claude/skills/$s; done
47
+ ```
48
+
49
+ Any other tool: paste or link the kit's `AGENTS.md`, which names the skill to load for each kind of job. Relative paths
50
+ inside the skills (`tools/...`) are relative to the QAKit checkout; say where the checkout is in your project's
51
+ instructions file so the agent can find `tools/device_qa.py`.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: device-qa
3
+ description: QA an Apple app like a QA engineer on a USB-attached test iPhone. Walk every screen and tap every button, review screenshots, audit accessibility, run dark, large-text, language and location variants, run the static submission checks, fix and loop, and write the release verdict. Use on "QA this app", "tap through every screen", "be thorough before release", "test on a real device", and as the gate before App Review. Argument: the app slug or manifest path.
4
+ ---
5
+
6
+ # device-qa: real-device QA for a solo developer whose QA team is an agent
7
+
8
+ The procedure and the check list live in `docs/device-qa-protocol.md` (24 areas, each check with an ID, a pass
9
+ criterion, a method and a source). This skill is the order in which to run the tools. "Tests green" is not "ready to
10
+ ship". The release verdict comes from the protocol's exit criteria.
11
+
12
+ ## Tools
13
+
14
+ | Tool | Does |
15
+ |---|---|
16
+ | `python3 tools/device_qa.py doctor --app <slug>` | Device, Developer Mode, passcode, developer disk image, signing identity, App Store Connect key. Prints a fix per problem |
17
+ | `python3 tools/device_qa.py devmode reveal` | Makes Settings > Privacy & Security > Developer Mode appear (`enable` turns it on when the phone has no passcode) |
18
+ | `python3 tools/device_qa.py test --app <slug> --suite qa --variant dark ...` | Puts the whole phone into a variant (dark, text size, location, VoiceOver, language), runs the suite, restores iOS defaults. Leaves the result bundle, a screenshot `index.md`, crash and jetsam reports and `summary.json`. `--only <test>` runs single tests while iterating on one defect; `--configuration Release` runs the UI suites on the optimized build. A source edit that lands mid-run marks the round as mixed and fails it |
19
+ | `python3 tools/device_qa.py traits --app <slug> [--screen X]` | Changes text size and dark mode on each manifest screen in front and in the background and checks the app survived in the same process (no 0x8BADF00D) |
20
+ | `python3 tools/device_qa.py perf --app <slug> --seconds 40` | Launches the app, attaches Time Profiler and counts Instruments' potential hangs and hang risks |
21
+ | `python3 tools/device_qa.py upgrade --app <slug> --from <previous tag>` | Builds the previous release in a temporary worktree, seeds real data with the manifest's seed test, installs this build over it and runs the check test |
22
+ | `python3 tools/device_qa.py release --app <slug> --archive <xcarchive>` | Exports the Release archive for the phone, installs it and cold-launches it with no debugger, then pulls crash reports |
23
+ | `python3 tools/device_qa.py lock status` and `lock run --note "<who>" -- <command>` | One phone, many sessions: who holds it and who waits. Raw xcodebuild, devicectl or xctrace work goes through `lock run` so it takes its turn |
24
+ | `swift tools/recording_frames.swift <video.mp4> <out-dir>` | Frames and a contact sheet from a UI-test screen recording (the `.mp4` in an xcresult or a `*-shots` folder). Look at them before calling a device UI failure an app defect. Given `<a.png> <b.png> ... <out-dir>` it tiles screenshots side by side |
25
+ | `swift tools/screenshot_contrast.swift <shot.png> <x0> <y0> <x1> <y1>` | The real WCAG contrast of text in a screenshot box (fractions). The audit reports "Contrast failed" on glass, materials and shadows; a reviewed exception needs the number the user sees |
26
+ | `Sources/QAKit/QAKit.swift` | UI-test helpers. Evidence: `QA.snap`, `QA.note`, `QA.assertInFront`. Accessibility: `QA.audit` (one report per screen, explained classes counted, one retry on an audit timeout). Alerts: `QA.answerSystemAlert`, `QA.dismissTopmost` (alert, share sheet, in-app Safari, action sheet, the app's closers, Back, system alert, swipe down). Walking: `QA.walk` (snap, audit, sweep, scroll loop), `QA.sweepButtons` (every visible button, from one snapshot of the tree), `QA.back`, `QA.closeInAppSafari`, `QA.exitSearch`, `QA.settle`, `QA.onScreen`, `QA.reveal` (scroll an off-screen row into view, `clearOf:` floating bars that swallow taps). Stability on a device: `QA.waitForStill`, `QA.tapUntilGone`, `QA.replaceText`, `QA.switches(labelled:)`, `QA.flip` (a Toggle's inner switch), `QA.waitForValue`, `QA.searchField` (iOS 26 bottom bar or minimised Search button), `QA.tree` (the element tree as text when something is missing), `QA.count` (rows of a lazy list that keeps only the visible ones in the tree). The app passes markers and identifiers only |
27
+ | `python3 tools/mutate.py --config <app>/release/testing/mutation.json --changed-since 'latest-tag:<app>/*'` | The mutation gate (SUITE-08): breaks the logic lines changed since the last release one at a time and checks the unit tests notice. Every survivor gets a test |
28
+ | `python3 tools/release_checks.py --app <slug> [--ipa X]` | Static checks right before submission: What's New, field limits, placeholders and markup, review notes, age-rating block, localized purpose strings, required-reason APIs, links, AASA, IPA entitlements |
29
+
30
+ Attaching a new app: write `<app>/release/testing/device_qa.json` (`docs/manifest.md`, start from `examples/device_qa.json`)
31
+ and link the `QAKit` package into the UI-test target (`docs/getting-started.md`).
32
+
33
+ ## Order
34
+
35
+ 1. **Readiness.** Run `doctor` until it prints `ready`. The test phone has no passcode, Developer Mode on, UI automation
36
+ allowed, automatic updates off.
37
+ 2. **Risk map.** From `git diff --stat` since the last release tag and the changelog, list the changed areas. Build a UI
38
+ matrix of every screen, control and state (one reader per area plus a completeness critic when you have parallel
39
+ agents). Deduplicate suspected defects, try to refute each one, keep only CONFIRMED. Product decisions go on the
40
+ owner's decision list.
41
+ 3. **Walkthrough test.** `<App>QAWalkthroughTests`: for every screen `QA.walk` (snap, audit, sweep, scroll until the bottom) with
42
+ a `settle` closure that brings the walk back to its screen (`QA.dismissTopmost` then `QA.back`; `QA.settle` relaunches only as
43
+ the last resort). Controls that leave the app or have real effects (call, purchase, delete) are excluded from the
44
+ sweep and get their own test that ends safely (a call is cancelled at the confirmation sheet). Always keep one
45
+ **first real use** test (no test mode: real location, network, onboarding).
46
+ 4. **Run.** Unit, UI and qa suites under the default variant, then qa under `dark`, `large-text`, `english` and location
47
+ variants; `traits` for the trait-change survival; the mutation gate on the changed logic lines. Read the failure summary and
48
+ `crashes`.
49
+ 5. **Screenshot review.** Split the screens of `index.md` and review them in parallel (clipping, overlap, empty states,
50
+ wrong facts, translation, contrast, touch targets, attribution). Each defect gets a refutation pass. Accessibility audit
51
+ notes (`a11y ...` attachments) go on the same list.
52
+ 6. **Fix.** Group defects by file set, fix each in its own worktree (failing test first), integrate, rerun 3 to 5. Stop
53
+ when **two consecutive rounds find zero new defects**.
54
+ 7. **Static checks.** `release_checks.py` (not offline) with 0 FAIL. After archiving, run it again with `--ipa` for the
55
+ entitlements.
56
+ 8. **Verdict.** Fill the protocol's exit criteria into `<app>/docs/qa/QA_<version>.md` with evidence. Checks you did not
57
+ run go into an "untested" list with one line of risk each. Submission to review happens only on the owner's explicit
58
+ word.
59
+
60
+ ## Pitfalls (measured on real phones)
61
+
62
+ - `xcode-select` pointing at CommandLineTools makes `xcrun devicectl` and `simctl` fail. The tools set
63
+ `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` unless you override it.
64
+ - The Developer Mode menu only appears after a Mac reveals it: `devmode reveal`.
65
+ - On iOS 27 the confirmation before dialling a business number is presented by `com.apple.BusinessActionSheet`, not
66
+ SpringBoard. `QA.answerSystemAlert` watches both; add others with `QA_ALERT_HOSTS`.
67
+ - Asking `isHittable` on an element outside the screen records a failure instead of returning false. The sweep only asks
68
+ elements whose frame centre is on screen.
69
+ - Behind a full-screen cover (onboarding) the first screen is already loaded, so its identifier exists while the cover is
70
+ up. Decide by the cover's buttons.
71
+ - A test that reads repo fixtures through `#filePath` finds nothing on the device. Put fixtures into the test bundle as
72
+ resources.
73
+ - Device tests cannot write to Mac paths (`/tmp/...`). Evidence goes into the result bundle through `XCTAttachment`.
74
+ - A simulator can wedge on app launch after days of use. Keep the UI gate on the device.
75
+ - Two sessions on one phone fight over the foreground app and the phone-wide settings. Every phone command takes the lock;
76
+ a second session waits in arrival order and `lock status` says who holds it.
77
+ - A sheet that closes between a query and the frame read fails a live query with "Failed to get matching snapshot". The sweep
78
+ reads one snapshot of the tree and taps coordinates.
79
+ - Rows of a SwiftUI List that are off screen are not in the tree, so `waitForExistence` on them never succeeds. Wait for an
80
+ anchor that is always on screen, then `QA.reveal` the row.
81
+ - A layout that is still moving swallows a tap on a device (an alert sliding with the keyboard, a bottom bar sliding in).
82
+ `QA.tapUntilGone` waits for the screen to be still and retries once.
83
+ - Tapping a text field that already has keyboard focus raises the edit menu over the buttons next to it. `QA.replaceText`
84
+ only taps when the field has no focus.
85
+ - The first time a variant switches VoiceOver on, iOS 27.2 shows a welcome prompt over the app and the first tap of the
86
+ walkthrough lands on it. `QA.settleAfterLaunch(app)` after `app.launch()` answers it (Use VoiceOver) and any permission
87
+ prompt that is already up.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: prose-qa
3
+ description: QA for prose: a memo, a README, a design doc, a paper draft, a blog post. Run the deterministic gates (tools/prose_check.py), build a claims ledger and verify every consequential claim against its source, check the argument, then do a cold read as the intended reader. Use on "review this doc", "check the paper", "is this ready to send", "fact-check", and before publishing anything a reader will act on.
4
+ ---
5
+
6
+ # prose-qa: a document is a product; test it like one
7
+
8
+ The same principles as `qa-rigor`, applied to text: decide what the reader must take away before editing (expected
9
+ value), check the boundaries (the first paragraph, the numbers, the conclusion), test the failure paths (a reader who
10
+ skips, a reader who disagrees), assert exact values (every number traced to a source), and leave a ledger.
11
+
12
+ ## Order
13
+
14
+ 1. **Expected reading.** Write one line: who reads this, what they must do or believe afterwards, and the one sentence
15
+ they would quote. If you cannot write it, the document is not ready for QA.
16
+ 2. **Mechanical gates.** `python3 tools/prose_check.py draft.md`. DEFECTs (duplicate sentences, empty links, runaway
17
+ sentences) are fixed before anything else. WARNs are judgment calls: look at each, fix or leave it on purpose.
18
+ 3. **Claims ledger.** `python3 tools/prose_check.py draft.md --claims claims.csv` lists every sentence with a number, a
19
+ year or an evidence word. For each row find the source, paste the quote that supports it, and write the verdict:
20
+ SUPPORTED (quote says it), PARTIAL (quote says less; the text adds something), NOT IN SOURCE, or NO SOURCE. A claim
21
+ with NO SOURCE either gets one or leaves the document. Numbers are compared digit by digit; a rounded figure says
22
+ "about". For a paper, the ledger also covers every citation: the cited work exists, the page or section is right, the
23
+ cited work says what the sentence says it says.
24
+ 4. **Argument.** For a recommendation or a paper: the problem is stated before the answer, the answer follows from the
25
+ evidence shown (not from evidence the author has in mind), the alternatives considered are named and the reason for
26
+ rejecting them is given, and the limits are stated by the author before a reviewer states them. Each gap is one
27
+ line with the paragraph it belongs to.
28
+ 5. **Consistency.** The same thing has one name throughout. Numbers that appear twice agree. The summary matches the
29
+ body. Terms defined on first use are used as defined.
30
+ 6. **Cold read.** Read it once as the intended reader with no memory of the drafting (a separate agent or a fresh
31
+ context when you have one). Mark where you stopped understanding, where you stopped believing, and what you would
32
+ ask the author. A reader who stops is a defect, not a style note.
33
+ 7. **Verdict.** The ledger: gates run and results, claims verified and their verdicts, argument gaps, cold-read stops,
34
+ and what was not checked with one line of risk each.
35
+
36
+ ## What the tool does not do
37
+
38
+ `prose_check.py` cannot judge whether the argument holds or whether a quote supports a sentence; that is steps 3 to 6.
39
+ It also has no lexicon for a given house voice; keep that in your own skill and run it after this one.
40
+
41
+ ## For papers specifically
42
+
43
+ - Every number in the abstract appears in the body with the same value and a table or figure reference.
44
+ - Every table and figure is referenced in the text, and every reference points at an existing table or figure.
45
+ - Methods give what a reader needs to rerun the experiment: data, versions, seeds, hardware, the exact metric.
46
+ - Related work names the closest prior result and says what is different, not only what is similar.
47
+ - Limitations are specific ("we did not test languages other than English") rather than generic.
48
+ - Reproduce one number yourself when the artifacts are available. A paper whose headline number cannot be reproduced
49
+ from its own artifacts fails QA regardless of the prose.
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: qa-rigor
3
+ description: Common QA procedure for any code (apps, Python, scripts, other repos) before merge or release. Design tests from boundaries, states and failure paths, measure test strength with mutation testing (tools/mutate.py), run runtime checkers, confirm in the user's real environment, and leave a verdict ledger. Use on "QA this", "make the tests solid", "ship without bugs", "mutation test", and before reporting a feature done. Apple apps add the device-qa layer on top.
4
+ ---
5
+
6
+ # qa-rigor: green tests and no bugs are different things
7
+
8
+ A real example from the first measurement (2026-10-28): an app with every test green had 13 surviving mutants where
9
+ `>=` became `>` in its BMI grading. The grade was wrong at every boundary weight and no test knew.
10
+
11
+ ## Layers
12
+
13
+ | Layer | What it looks at | Where |
14
+ |---|---|---|
15
+ | Common (this skill) | test design, test strength (mutation), runtime checkers, real environment, verdict ledger | all code |
16
+ | Pull request | repo rules, CI replicated locally, the edges a bot reviewer will probe | your repo's PR skill |
17
+ | Apple app | real device, variants (dark, large text, language), screenshots, store requirements | `skills/device-qa` + `docs/device-qa-protocol.md` |
18
+
19
+ A lower layer never replaces an upper one. An app follows the nine principles below first.
20
+
21
+ ## Principles
22
+
23
+ 1. **Expected values before code.** For every changed behaviour write one line: "this input gives this output". If you
24
+ cannot write it, the requirement is not settled yet.
25
+ 2. **Partition the inputs.** One case per group of inputs that get the same treatment, and three values at every
26
+ boundary (below, on, above: 14.9, 15, 15.1 km). Empty, maximum length, negative and Unicode are groups too. Screens
27
+ and lifecycles become a state table that covers every valid transition and every invalid attempt.
28
+ 3. **Failure paths are features.** Network gone, permission denied, disk full, bad payload, time zone and DST,
29
+ concurrent calls: each has a test.
30
+ 4. **Assertions are exact values.** A test that only checks `XCTAssertNotNil` or "did not crash" passes when the output is
31
+ wrong. Compare the result value, the stored state, the side effect that was called.
32
+ 5. **Tests are deterministic.** No fixed `sleep` (bounded waits instead); inject time, randomness, network and location;
33
+ fix seeds; no order dependence.
34
+ 6. **Test strength is measured by mutation testing.** Coverage only says "this line ran". Every mutant on a changed line
35
+ must die, or the line carries the reason it cannot. Procedure below.
36
+ 7. **Turn on a runtime checker at least once.** Swift: ASan, UBSan, Main Thread Checker, TSan (Apple platforms: simulator
37
+ or Mac only). Python: `-W error`, pyflakes (undefined names). JS: strict mode.
38
+ 8. **The last check happens in the user's environment.** Run the build the user receives, where they receive it: a Release
39
+ build on a real device for an app, the boot log after a restart for a server, the installed binary for a CLI.
40
+ 9. **Every defect leaves two things.** A regression test that catches it again (watch it fail first) and a guard that stops
41
+ the same class next time (a lint rule, a harness test, a checklist line).
42
+
43
+ The verdict is a ledger: the gates you ran and their results, the gates you did not run and one line of risk each. Even
44
+ when every gate is green, an unrun gate is written down.
45
+
46
+ ## Mutation testing
47
+
48
+ Break the code on purpose, one small change at a time, and rerun the tests. A test failing means the mutant **died** (a
49
+ test pins that behaviour). Everything passing means it **survived** (nobody checks that behaviour).
50
+
51
+ ```bash
52
+ M=tools/mutate.py # from the QAKit checkout, or <your-repo>/path/to/qakit/tools/mutate.py
53
+ python3 $M --config <mutation.json> --list # which mutants would be made
54
+ python3 $M --config <mutation.json> --changed-since origin/main # before merge: changed lines only
55
+ python3 $M --config <mutation.json> --changed-since 'latest-tag:<app>/*' # before release: lines changed since the last tag
56
+ python3 $M --config <mutation.json> --jobs 5 --out <dir> # full measurement (periodic)
57
+ python3 $M --config <mutation.json> --out <dir> --resume # continue a long run that was cut short
58
+ ```
59
+
60
+ Config (JSON; paths are relative to the git top level that holds the config):
61
+
62
+ ```json
63
+ {"name": "myapp-core", "root": "apps/myapp", "sources": ["Core/Sources/**/*.swift"], "exclude": [],
64
+ "setup": "", "build": "swift build --build-tests", "test": "swift test --skip-build",
65
+ "coverage": "swift test --enable-code-coverage ... > {lcov}", "jobs": 4}
66
+ ```
67
+
68
+ - Work happens in a temporary copy of `root`. The checkout is never touched, so other sessions can keep working.
69
+ - A `build` failure means the mutant does not compile (invalid, not counted). A `test` failure or timeout means killed.
70
+ - With `coverage`, mutants on lines no test runs are not executed and are reported **uncovered**. They count with survivors.
71
+ - `{orig}` is the real root (for virtualenv paths), `{file}` and `{stem}` the mutated file (to run related tests only),
72
+ `{lcov}` the coverage output path.
73
+ - Python example: `"test": "{orig}/.venv/bin/python -m pytest -x -q -p no:cacheprovider"`.
74
+ - Xcode apps take one to two minutes per mutant, so measure a full sweep with `sweep_sample` and run every changed line at
75
+ release time.
76
+
77
+ Operators: boundary (`<` and `<=`, `>` and `>=`), equality (`==` and `!=`), logical (`&&` and `||`, `and` and `or`),
78
+ arithmetic (`+` and `-`, `*` and `/`, `+=` and `-=`), boolean (`true` and `false`), negation removal (`!x`, `not x` to
79
+ `x`), call statement removal (`store.save(x)` dropped; logging, print, assert, super and builder elements such as
80
+ `Text(...)` are left alone). C-family languages (Swift, JS, TS, Kotlin, Go, Java, C#) only swap binary operators with
81
+ spaces on both sides, which keeps generics (`Array<Int>`), arrows and unary operators intact. Strings, comments and `#if`
82
+ lines are never touched.
83
+
84
+ ### Handling survivors
85
+
86
+ For every line of the report (`mutation.md`) do one of two things.
87
+
88
+ 1. **Write a test.** Write the test that fails under that mutant, rerun, confirm it dies. Most survivors are a missing
89
+ boundary value (principle 2) or a loose assertion (principle 4).
90
+ 2. **Write the reason.** Only when the change cannot alter behaviour (an equivalent mutant) add `// mutation-ok: <reason>`
91
+ (`# mutation-ok: <reason>` in Python) on that line. Examples: `count > 0` and `count >= 1` on an integer, a cache size
92
+ that only changes performance, log message assembly. "Writing the test is tedious" is not a reason.
93
+
94
+ "no test runs this line" means no test reaches that code at all. If it is not unit-testable (View code, for instance),
95
+ move it to `exclude` and write next to the config which layer owns it (UI tests, screenshots, device QA).
96
+
97
+ ### Pitfalls (measured)
98
+
99
+ - **The machine is shared.** SwiftPM builds start one compiler per core. Five jobs pushed a shared Mac's load to 174 and
100
+ slowed another session's simulator. The engine runs under `nice -n 10`; SwiftPM configs default to `-j 4` and 3 jobs.
101
+ - **Same-size mutants hide behind caches.** Python `.pyc` freshness uses mtime (seconds) and size. A `*` to `/` mutant
102
+ written in the same second runs the old bytecode and survives falsely. The engine sets `PYTHONDONTWRITEBYTECODE=1`
103
+ and advances mtimes.
104
+ - **Branches that never compile.** The other side of `#if SWIFT_PACKAGE ... #else` is not compiled in that build, so its
105
+ mutants always survive. Mark the line `mutation-ok`.