atsfill 0.0.5.dev140__tar.gz → 0.0.5.dev142__tar.gz

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.
Files changed (59) hide show
  1. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/PKG-INFO +7 -5
  2. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/README.md +5 -4
  3. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/pyproject.toml +2 -1
  4. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/pyproject.toml.orig +2 -1
  5. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/cli.py +2 -0
  6. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/commands.py +11 -29
  7. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/paths.py +8 -0
  8. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/registry.py +43 -100
  9. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/results.py +2 -5
  10. atsfill-0.0.5.dev142/src/atsfill/core/telemetry.py +163 -0
  11. atsfill-0.0.5.dev142/src/atsfill/registry.toml +398 -0
  12. atsfill-0.0.5.dev140/src/atsfill/registry.toml +0 -442
  13. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/LICENSE +0 -0
  14. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/__init__.py +0 -0
  15. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/__init__.py +0 -0
  16. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/ashby.py +0 -0
  17. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/bamboohr.py +0 -0
  18. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/breezy.py +0 -0
  19. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/dayforce.py +0 -0
  20. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/eightfold.py +0 -0
  21. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/greenhouse.py +0 -0
  22. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/gupy.py +0 -0
  23. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/hireology.py +0 -0
  24. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/hiringthing.py +0 -0
  25. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/hrmdirect.py +0 -0
  26. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/isolvedhire.py +0 -0
  27. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/jazzhr.py +0 -0
  28. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/join.py +0 -0
  29. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/lever.py +0 -0
  30. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/manatal.py +0 -0
  31. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/oracle.py +0 -0
  32. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/paycor.py +0 -0
  33. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/paylocity.py +0 -0
  34. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/personio.py +0 -0
  35. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/phenom.py +0 -0
  36. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/pinpoint.py +0 -0
  37. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/recruitee.py +0 -0
  38. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/rippling.py +0 -0
  39. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/smartrecruiters.py +0 -0
  40. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/successfactors.js +0 -0
  41. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/successfactors.py +0 -0
  42. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/taleo.py +0 -0
  43. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/teamtailor.py +0 -0
  44. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/ukg.py +0 -0
  45. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/workable.py +0 -0
  46. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/workday.py +0 -0
  47. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/wttj.py +0 -0
  48. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/adapters/zohorecruit.py +0 -0
  49. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/__init__.py +0 -0
  50. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/accounts.py +0 -0
  51. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/adapter.py +0 -0
  52. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/attempts.py +0 -0
  53. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/browser.py +0 -0
  54. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/conditions.py +0 -0
  55. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/html.py +0 -0
  56. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/matching.py +0 -0
  57. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/otp.py +0 -0
  58. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/core/passwords.py +0 -0
  59. {atsfill-0.0.5.dev140 → atsfill-0.0.5.dev142}/src/atsfill/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.5.dev140
3
+ Version: 0.0.5.dev142
4
4
  Summary: Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle.
5
5
  Keywords: job-applications,ats,ashby,greenhouse,teamtailor,ai-agents,cli
6
6
  Author: shbernal
@@ -20,6 +20,7 @@ Requires-Dist: camoufox==0.5.8
20
20
  Requires-Dist: filelock==4.1.1
21
21
  Requires-Dist: keyring==25.7.0
22
22
  Requires-Dist: packaging==26.3
23
+ Requires-Dist: posthog==7.78.0
23
24
  Requires-Python: >=3.14
24
25
  Project-URL: Homepage, https://github.com/shbernal/atsfill
25
26
  Project-URL: Repository, https://github.com/shbernal/atsfill
@@ -57,10 +58,10 @@ It is built for agents with a shell: every command prints one JSON document and
57
58
  ## Supported hiring systems
58
59
 
59
60
  <!-- badges -->
60
- ![oracle: dry run verified 2026-10-20](https://img.shields.io/badge/oracle-dry%20run%20verified%202026--10--20-7aa2f7) ![smartrecruiters: dry run verified 2026-10-20](https://img.shields.io/badge/smartrecruiters-dry%20run%20verified%202026--10--20-7aa2f7) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![manatal: dry run verified 2026-10-20](https://img.shields.io/badge/manatal-dry%20run%20verified%202026--10--20-7aa2f7) ![paylocity: dry run verified 2026-10-20](https://img.shields.io/badge/paylocity-dry%20run%20verified%202026--10--20-7aa2f7) ![dayforce: dry run verified 2026-10-20](https://img.shields.io/badge/dayforce-dry%20run%20verified%202026--10--20-7aa2f7) ![workable: dry run verified 2026-10-20](https://img.shields.io/badge/workable-dry%20run%20verified%202026--10--20-7aa2f7) ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![bamboohr: dry run verified 2026-10-21](https://img.shields.io/badge/bamboohr-dry%20run%20verified%202026--10--21-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![jazzhr: dry run verified 2026-10-20](https://img.shields.io/badge/jazzhr-dry%20run%20verified%202026--10--20-7aa2f7) ![lever: dry run verified 2026-10-20](https://img.shields.io/badge/lever-dry%20run%20verified%202026--10--20-7aa2f7) ![hireology: dry run verified 2026-10-20](https://img.shields.io/badge/hireology-dry%20run%20verified%202026--10--20-7aa2f7) ![paycor: dry run verified 2026-10-20](https://img.shields.io/badge/paycor-dry%20run%20verified%202026--10--20-7aa2f7) ![zohorecruit: dry run verified 2026-10-22](https://img.shields.io/badge/zohorecruit-dry%20run%20verified%202026--10--22-7aa2f7) ![phenom: dry run verified 2026-10-21](https://img.shields.io/badge/phenom-dry%20run%20verified%202026--10--21-7aa2f7) ![breezy: dry run verified 2026-10-21](https://img.shields.io/badge/breezy-dry%20run%20verified%202026--10--21-7aa2f7) ![recruitee: dry run verified 2026-10-25](https://img.shields.io/badge/recruitee-dry%20run%20verified%202026--10--25-7aa2f7) ![rippling: dry run verified 2026-10-22](https://img.shields.io/badge/rippling-dry%20run%20verified%202026--10--22-7aa2f7) ![personio: dry run verified 2026-10-22](https://img.shields.io/badge/personio-dry%20run%20verified%202026--10--22-7aa2f7) ![eightfold: dry run verified 2026-10-22](https://img.shields.io/badge/eightfold-dry%20run%20verified%202026--10--22-7aa2f7) ![pinpoint: dry run verified 2026-10-22](https://img.shields.io/badge/pinpoint-dry%20run%20verified%202026--10--22-7aa2f7) ![hiringthing: dry run verified 2026-10-22](https://img.shields.io/badge/hiringthing-dry%20run%20verified%202026--10--22-7aa2f7) ![join: dry run verified 2026-10-26](https://img.shields.io/badge/join-dry%20run%20verified%202026--10--26-7aa2f7) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a) ![workday: dry run verified 2026-10-24](https://img.shields.io/badge/workday-dry%20run%20verified%202026--10--24-7aa2f7) ![successfactors: dry run verified 2026-10-26](https://img.shields.io/badge/successfactors-dry%20run%20verified%202026--10--26-7aa2f7) ![isolvedhire: dry run verified 2026-10-26](https://img.shields.io/badge/isolvedhire-dry%20run%20verified%202026--10--26-7aa2f7) ![gupy: dry run verified 2026-10-26](https://img.shields.io/badge/gupy-dry%20run%20verified%202026--10--26-7aa2f7) ![ukg: unsupported 2026-10-26](https://img.shields.io/badge/ukg-unsupported%202026--10--26-e0af68)
61
+ ![oracle: supported 2026-10-20](https://img.shields.io/badge/oracle-supported%202026--10--20-9ece6a) ![smartrecruiters: supported 2026-10-20](https://img.shields.io/badge/smartrecruiters-supported%202026--10--20-9ece6a) ![greenhouse: supported 2026-10-19](https://img.shields.io/badge/greenhouse-supported%202026--10--19-9ece6a) ![manatal: supported 2026-10-20](https://img.shields.io/badge/manatal-supported%202026--10--20-9ece6a) ![paylocity: supported 2026-10-20](https://img.shields.io/badge/paylocity-supported%202026--10--20-9ece6a) ![dayforce: supported 2026-10-20](https://img.shields.io/badge/dayforce-supported%202026--10--20-9ece6a) ![workable: supported 2026-10-20](https://img.shields.io/badge/workable-supported%202026--10--20-9ece6a) ![ashby: supported 2026-10-19](https://img.shields.io/badge/ashby-supported%202026--10--19-9ece6a) ![bamboohr: supported 2026-10-21](https://img.shields.io/badge/bamboohr-supported%202026--10--21-9ece6a) ![teamtailor: supported 2026-10-25](https://img.shields.io/badge/teamtailor-supported%202026--10--25-9ece6a) ![jazzhr: supported 2026-10-20](https://img.shields.io/badge/jazzhr-supported%202026--10--20-9ece6a) ![lever: supported 2026-10-20](https://img.shields.io/badge/lever-supported%202026--10--20-9ece6a) ![hireology: supported 2026-10-20](https://img.shields.io/badge/hireology-supported%202026--10--20-9ece6a) ![paycor: supported 2026-10-20](https://img.shields.io/badge/paycor-supported%202026--10--20-9ece6a) ![zohorecruit: supported 2026-10-22](https://img.shields.io/badge/zohorecruit-supported%202026--10--22-9ece6a) ![phenom: supported 2026-10-21](https://img.shields.io/badge/phenom-supported%202026--10--21-9ece6a) ![breezy: supported 2026-10-21](https://img.shields.io/badge/breezy-supported%202026--10--21-9ece6a) ![recruitee: supported 2026-10-25](https://img.shields.io/badge/recruitee-supported%202026--10--25-9ece6a) ![rippling: supported 2026-10-22](https://img.shields.io/badge/rippling-supported%202026--10--22-9ece6a) ![personio: supported 2026-10-22](https://img.shields.io/badge/personio-supported%202026--10--22-9ece6a) ![eightfold: supported 2026-10-22](https://img.shields.io/badge/eightfold-supported%202026--10--22-9ece6a) ![pinpoint: supported 2026-10-22](https://img.shields.io/badge/pinpoint-supported%202026--10--22-9ece6a) ![hiringthing: supported 2026-10-22](https://img.shields.io/badge/hiringthing-supported%202026--10--22-9ece6a) ![join: supported 2026-10-26](https://img.shields.io/badge/join-supported%202026--10--26-9ece6a) ![wttj: supported 2026-10-24](https://img.shields.io/badge/wttj-supported%202026--10--24-9ece6a) ![workday: supported 2026-10-24](https://img.shields.io/badge/workday-supported%202026--10--24-9ece6a) ![successfactors: supported 2026-10-26](https://img.shields.io/badge/successfactors-supported%202026--10--26-9ece6a) ![isolvedhire: supported 2026-10-26](https://img.shields.io/badge/isolvedhire-supported%202026--10--26-9ece6a) ![gupy: supported 2026-10-26](https://img.shields.io/badge/gupy-supported%202026--10--26-9ece6a) ![ukg: in scope 2026-10-26](https://img.shields.io/badge/ukg-in%20scope%202026--10--26-e0af68)
61
62
  <!-- /badges -->
62
63
 
63
- Each badge is a hiring system atsfill has an adapter for, with how far it was verified and when: a dry run, or a real send.
64
+ Each badge is a hiring system atsfill has an adapter for: `supported` once a dry run passed on five employers, with its date.
64
65
  [The systems](docs/adapters/README.md) lists which ones need an account, to read the form or only to send it.
65
66
  Any other system returns `unsupported`, so the consumer can fall back to driving a browser itself.
66
67
 
@@ -127,8 +128,7 @@ Every push to `main` that passes CI is also published as a dev release; `uvx --p
127
128
  ```
128
129
 
129
130
  A dry run is not invisible: some systems save answers or the resume as they are typed.
130
- `atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
131
- A dry run or a `send` through an adapter no send was verified on carries a `warning`; the send goes ahead anyway.
131
+ `atsfill status` lists each system's side effects, whether it is allowed to submit, and its scope: [the systems](docs/adapters/README.md).
132
132
 
133
133
  One home can send for more than one user: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
134
134
  Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
@@ -153,6 +153,8 @@ The core owns every rule about when a send is allowed; an adapter only knows its
153
153
  Every result has a `status` (`ready`, `refused`, `submitted`, `blocked`, `unknown`, `unsupported`) and, when it is not clean, a `reason` code.
154
154
  The exit code is 0 for `ready` and `submitted`, 1 otherwise, 2 for a usage error.
155
155
 
156
+ Each `read`, `fill` and `send` sends one anonymous event (the system, the command, the status, a hash of the posting, never answers or URLs): [what is sent and how to turn it off](docs/telemetry.md).
157
+
156
158
  ## More
157
159
 
158
160
  - [Architecture decisions](docs/adr/README.md): the contract, the submit gate, the browser, sessions.
@@ -29,10 +29,10 @@ It is built for agents with a shell: every command prints one JSON document and
29
29
  ## Supported hiring systems
30
30
 
31
31
  <!-- badges -->
32
- ![oracle: dry run verified 2026-10-20](https://img.shields.io/badge/oracle-dry%20run%20verified%202026--10--20-7aa2f7) ![smartrecruiters: dry run verified 2026-10-20](https://img.shields.io/badge/smartrecruiters-dry%20run%20verified%202026--10--20-7aa2f7) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![manatal: dry run verified 2026-10-20](https://img.shields.io/badge/manatal-dry%20run%20verified%202026--10--20-7aa2f7) ![paylocity: dry run verified 2026-10-20](https://img.shields.io/badge/paylocity-dry%20run%20verified%202026--10--20-7aa2f7) ![dayforce: dry run verified 2026-10-20](https://img.shields.io/badge/dayforce-dry%20run%20verified%202026--10--20-7aa2f7) ![workable: dry run verified 2026-10-20](https://img.shields.io/badge/workable-dry%20run%20verified%202026--10--20-7aa2f7) ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![bamboohr: dry run verified 2026-10-21](https://img.shields.io/badge/bamboohr-dry%20run%20verified%202026--10--21-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![jazzhr: dry run verified 2026-10-20](https://img.shields.io/badge/jazzhr-dry%20run%20verified%202026--10--20-7aa2f7) ![lever: dry run verified 2026-10-20](https://img.shields.io/badge/lever-dry%20run%20verified%202026--10--20-7aa2f7) ![hireology: dry run verified 2026-10-20](https://img.shields.io/badge/hireology-dry%20run%20verified%202026--10--20-7aa2f7) ![paycor: dry run verified 2026-10-20](https://img.shields.io/badge/paycor-dry%20run%20verified%202026--10--20-7aa2f7) ![zohorecruit: dry run verified 2026-10-22](https://img.shields.io/badge/zohorecruit-dry%20run%20verified%202026--10--22-7aa2f7) ![phenom: dry run verified 2026-10-21](https://img.shields.io/badge/phenom-dry%20run%20verified%202026--10--21-7aa2f7) ![breezy: dry run verified 2026-10-21](https://img.shields.io/badge/breezy-dry%20run%20verified%202026--10--21-7aa2f7) ![recruitee: dry run verified 2026-10-25](https://img.shields.io/badge/recruitee-dry%20run%20verified%202026--10--25-7aa2f7) ![rippling: dry run verified 2026-10-22](https://img.shields.io/badge/rippling-dry%20run%20verified%202026--10--22-7aa2f7) ![personio: dry run verified 2026-10-22](https://img.shields.io/badge/personio-dry%20run%20verified%202026--10--22-7aa2f7) ![eightfold: dry run verified 2026-10-22](https://img.shields.io/badge/eightfold-dry%20run%20verified%202026--10--22-7aa2f7) ![pinpoint: dry run verified 2026-10-22](https://img.shields.io/badge/pinpoint-dry%20run%20verified%202026--10--22-7aa2f7) ![hiringthing: dry run verified 2026-10-22](https://img.shields.io/badge/hiringthing-dry%20run%20verified%202026--10--22-7aa2f7) ![join: dry run verified 2026-10-26](https://img.shields.io/badge/join-dry%20run%20verified%202026--10--26-7aa2f7) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a) ![workday: dry run verified 2026-10-24](https://img.shields.io/badge/workday-dry%20run%20verified%202026--10--24-7aa2f7) ![successfactors: dry run verified 2026-10-26](https://img.shields.io/badge/successfactors-dry%20run%20verified%202026--10--26-7aa2f7) ![isolvedhire: dry run verified 2026-10-26](https://img.shields.io/badge/isolvedhire-dry%20run%20verified%202026--10--26-7aa2f7) ![gupy: dry run verified 2026-10-26](https://img.shields.io/badge/gupy-dry%20run%20verified%202026--10--26-7aa2f7) ![ukg: unsupported 2026-10-26](https://img.shields.io/badge/ukg-unsupported%202026--10--26-e0af68)
32
+ ![oracle: supported 2026-10-20](https://img.shields.io/badge/oracle-supported%202026--10--20-9ece6a) ![smartrecruiters: supported 2026-10-20](https://img.shields.io/badge/smartrecruiters-supported%202026--10--20-9ece6a) ![greenhouse: supported 2026-10-19](https://img.shields.io/badge/greenhouse-supported%202026--10--19-9ece6a) ![manatal: supported 2026-10-20](https://img.shields.io/badge/manatal-supported%202026--10--20-9ece6a) ![paylocity: supported 2026-10-20](https://img.shields.io/badge/paylocity-supported%202026--10--20-9ece6a) ![dayforce: supported 2026-10-20](https://img.shields.io/badge/dayforce-supported%202026--10--20-9ece6a) ![workable: supported 2026-10-20](https://img.shields.io/badge/workable-supported%202026--10--20-9ece6a) ![ashby: supported 2026-10-19](https://img.shields.io/badge/ashby-supported%202026--10--19-9ece6a) ![bamboohr: supported 2026-10-21](https://img.shields.io/badge/bamboohr-supported%202026--10--21-9ece6a) ![teamtailor: supported 2026-10-25](https://img.shields.io/badge/teamtailor-supported%202026--10--25-9ece6a) ![jazzhr: supported 2026-10-20](https://img.shields.io/badge/jazzhr-supported%202026--10--20-9ece6a) ![lever: supported 2026-10-20](https://img.shields.io/badge/lever-supported%202026--10--20-9ece6a) ![hireology: supported 2026-10-20](https://img.shields.io/badge/hireology-supported%202026--10--20-9ece6a) ![paycor: supported 2026-10-20](https://img.shields.io/badge/paycor-supported%202026--10--20-9ece6a) ![zohorecruit: supported 2026-10-22](https://img.shields.io/badge/zohorecruit-supported%202026--10--22-9ece6a) ![phenom: supported 2026-10-21](https://img.shields.io/badge/phenom-supported%202026--10--21-9ece6a) ![breezy: supported 2026-10-21](https://img.shields.io/badge/breezy-supported%202026--10--21-9ece6a) ![recruitee: supported 2026-10-25](https://img.shields.io/badge/recruitee-supported%202026--10--25-9ece6a) ![rippling: supported 2026-10-22](https://img.shields.io/badge/rippling-supported%202026--10--22-9ece6a) ![personio: supported 2026-10-22](https://img.shields.io/badge/personio-supported%202026--10--22-9ece6a) ![eightfold: supported 2026-10-22](https://img.shields.io/badge/eightfold-supported%202026--10--22-9ece6a) ![pinpoint: supported 2026-10-22](https://img.shields.io/badge/pinpoint-supported%202026--10--22-9ece6a) ![hiringthing: supported 2026-10-22](https://img.shields.io/badge/hiringthing-supported%202026--10--22-9ece6a) ![join: supported 2026-10-26](https://img.shields.io/badge/join-supported%202026--10--26-9ece6a) ![wttj: supported 2026-10-24](https://img.shields.io/badge/wttj-supported%202026--10--24-9ece6a) ![workday: supported 2026-10-24](https://img.shields.io/badge/workday-supported%202026--10--24-9ece6a) ![successfactors: supported 2026-10-26](https://img.shields.io/badge/successfactors-supported%202026--10--26-9ece6a) ![isolvedhire: supported 2026-10-26](https://img.shields.io/badge/isolvedhire-supported%202026--10--26-9ece6a) ![gupy: supported 2026-10-26](https://img.shields.io/badge/gupy-supported%202026--10--26-9ece6a) ![ukg: in scope 2026-10-26](https://img.shields.io/badge/ukg-in%20scope%202026--10--26-e0af68)
33
33
  <!-- /badges -->
34
34
 
35
- Each badge is a hiring system atsfill has an adapter for, with how far it was verified and when: a dry run, or a real send.
35
+ Each badge is a hiring system atsfill has an adapter for: `supported` once a dry run passed on five employers, with its date.
36
36
  [The systems](docs/adapters/README.md) lists which ones need an account, to read the form or only to send it.
37
37
  Any other system returns `unsupported`, so the consumer can fall back to driving a browser itself.
38
38
 
@@ -99,8 +99,7 @@ Every push to `main` that passes CI is also published as a dev release; `uvx --p
99
99
  ```
100
100
 
101
101
  A dry run is not invisible: some systems save answers or the resume as they are typed.
102
- `atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
103
- A dry run or a `send` through an adapter no send was verified on carries a `warning`; the send goes ahead anyway.
102
+ `atsfill status` lists each system's side effects, whether it is allowed to submit, and its scope: [the systems](docs/adapters/README.md).
104
103
 
105
104
  One home can send for more than one user: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
106
105
  Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
@@ -125,6 +124,8 @@ The core owns every rule about when a send is allowed; an adapter only knows its
125
124
  Every result has a `status` (`ready`, `refused`, `submitted`, `blocked`, `unknown`, `unsupported`) and, when it is not clean, a `reason` code.
126
125
  The exit code is 0 for `ready` and `submitted`, 1 otherwise, 2 for a usage error.
127
126
 
127
+ Each `read`, `fill` and `send` sends one anonymous event (the system, the command, the status, a hash of the posting, never answers or URLs): [what is sent and how to turn it off](docs/telemetry.md).
128
+
128
129
  ## More
129
130
 
130
131
  - [Architecture decisions](docs/adr/README.md): the contract, the submit gate, the browser, sessions.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.5.dev140"
3
+ version = "0.0.5.dev142"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "filelock==4.1.1",
33
33
  "keyring==25.7.0",
34
34
  "packaging==26.3",
35
+ "posthog==7.78.0",
35
36
  ]
36
37
 
37
38
  [[project.authors]]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.5.dev140"
3
+ version = "0.0.5.dev142"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -25,6 +25,7 @@ dependencies = [
25
25
  "filelock==4.1.1",
26
26
  "keyring==25.7.0",
27
27
  "packaging==26.3",
28
+ "posthog==7.78.0",
28
29
  ]
29
30
 
30
31
  [project.urls]
@@ -190,6 +190,8 @@ def run(args: argparse.Namespace) -> int:
190
190
  return 2
191
191
  emit(session, args.verbose)
192
192
  return 0 if args.command == "logout" or session.session == "signed_in" else 1
193
+ if result.notice:
194
+ print(f"atsfill: {result.notice}", file=sys.stderr)
193
195
  emit(result, args.verbose, args.command)
194
196
  return 0 if result.status in OK else 1
195
197
 
@@ -15,6 +15,7 @@ from atsfill.core import (
15
15
  passwords,
16
16
  paths,
17
17
  registry,
18
+ telemetry,
18
19
  )
19
20
  from atsfill.core.adapter import (
20
21
  Adapter,
@@ -324,7 +325,6 @@ def _form_command(
324
325
  result = _fill(page, adapter, url, answers, user)
325
326
  if result.status is not Status.READY:
326
327
  return result
327
- result.warning = _unverified(adapter.name)
328
328
  if not send:
329
329
  return result
330
330
  attempts.record(mine, posting, attempts.PENDING, url=url)
@@ -354,6 +354,11 @@ def read(
354
354
  ) -> Result:
355
355
  """Lists the form's fields by a request read when an adapter has one (ADR 0011), else by a
356
356
  browser read."""
357
+ data = paths.data_dir(data_dir)
358
+ return telemetry.sent("read", _read(data, user, url, sessions), data)
359
+
360
+
361
+ def _read(data: Path, user: str, url: str, sessions: Sessions | None) -> Result:
357
362
  for adapter in adapters():
358
363
  if not isinstance(adapter, ReadsByRequest):
359
364
  continue
@@ -365,7 +370,6 @@ def read(
365
370
  return Result(Status.UNSUPPORTED, url, adapter.name, f"adapter_error: {e}")
366
371
  _unfillable(result)
367
372
  return result
368
- data = paths.data_dir(data_dir)
369
373
  return _in_budget(url, lambda seen: _browser_read(data, user, url, sessions, seen))
370
374
 
371
375
 
@@ -378,9 +382,10 @@ def fill(
378
382
  sessions: Sessions | None = None,
379
383
  ) -> Result:
380
384
  data = paths.data_dir(data_dir)
381
- return _in_budget(
385
+ result = _in_budget(
382
386
  url, lambda seen: _form_command(data, user, url, answers, sessions, False, False, seen)
383
387
  )
388
+ return telemetry.sent("fill", result, data)
384
389
 
385
390
 
386
391
  def send(
@@ -393,21 +398,10 @@ def send(
393
398
  sessions: Sessions | None = None,
394
399
  ) -> Result:
395
400
  data = paths.data_dir(data_dir)
396
- return _in_budget(
401
+ result = _in_budget(
397
402
  url, lambda seen: _form_command(data, user, url, answers, sessions, True, resend, seen)
398
403
  )
399
-
400
-
401
- def _unverified(adapter: str) -> str | None:
402
- """The warning a ready dry run or a send carries when no send through the adapter was
403
- verified, or the last verified one fails now (ADR 0012)."""
404
- source = registry.for_adapter(adapter)
405
- if source and source.send_broken:
406
- return f"send_broken: {_broken(source)}"
407
- if source and source.send_verified:
408
- return None
409
- verification = source.verification if source else None
410
- return f"not_send_verified: {adapter} is {verification or 'not in the registry'}"
404
+ return telemetry.sent("send", result, data)
411
405
 
412
406
 
413
407
  def _sessions(data: Path, users: list[str], candidates: list[Adapter]) -> list[SessionReport]:
@@ -439,26 +433,14 @@ def status(*, data_dir: Path | None = None) -> StatusReport:
439
433
  )
440
434
 
441
435
 
442
- def _broken(source: registry.Source) -> str:
443
- broken, last = source.send_broken, source.send_verified
444
- assert broken
445
- since = f"send broken since {broken.date} ({broken.version}), {broken.reason}"
446
- return since + (f"; last verified {last.date} ({last.version})" if last else "")
447
-
448
-
449
436
  def _adapter_status(adapter: Adapter, users: list[str]) -> AdapterStatus:
450
437
  source = registry.for_adapter(adapter.name)
451
- verified = source.verification if source else None
452
- v = getattr(source, verified) if source and verified else None
453
438
  return AdapterStatus(
454
439
  adapter.name,
455
440
  submit_allowed=[u for u in users if paths.submit_enabled(adapter.name, u)],
456
441
  side_effects=adapter.side_effects,
457
442
  side_effects_muted=[u for u in users if paths.muted(adapter.name, u)],
458
- verified=verified,
459
- verified_version=v.version if v else None,
460
- verified_release=registry.release(v.version, version("atsfill")) if v else None,
461
- send_broken=_broken(source) if source and source.send_broken else None,
443
+ scope=source.scope.value if source else None,
462
444
  )
463
445
 
464
446
 
@@ -12,6 +12,7 @@ TEST_USER = "test"
12
12
  USER_NAME = re.compile(r"[a-z0-9-]+")
13
13
  RENAMED = {"submit": "submit-allowed", "ack_side_effects": "mute-side-effect-warnings"}
14
14
  MOVED = ("sessions", "attempts.jsonl") # a data directory from before users, the default's
15
+ TELEMETRY = ("off", "basic", "detailed") # the first is no telemetry (ADR 0016)
15
16
 
16
17
 
17
18
  class ConfigError(Exception):
@@ -72,6 +73,8 @@ def _config() -> dict:
72
73
  for old, new in RENAMED.items():
73
74
  if old in section:
74
75
  raise ConfigError(f"`{old}` in {path} is now `{new}`")
76
+ if (level := whole.get("telemetry", "basic")) not in TELEMETRY:
77
+ raise ConfigError(f"telemetry = {level!r} in {path}: one of {', '.join(TELEMETRY)}")
75
78
  return whole
76
79
 
77
80
 
@@ -84,6 +87,11 @@ def config(user: str) -> dict:
84
87
  return whole.get("users", {}).get(user, {})
85
88
 
86
89
 
90
+ def telemetry() -> str:
91
+ """The home's telemetry level, a top-level key: `basic` unless the config says otherwise."""
92
+ return _config().get("telemetry", "basic")
93
+
94
+
87
95
  def submit_enabled(adapter: str, user: str) -> bool:
88
96
  """Whether the user's `submit-allowed` lists the adapter: there is no other way to enable
89
97
  it."""
@@ -1,61 +1,32 @@
1
1
  """The committed registry of freehire sources (ADR 0012)."""
2
2
 
3
+ import datetime
3
4
  import tomllib
4
5
  from dataclasses import dataclass, fields
5
6
  from datetime import date
6
7
  from functools import cache
7
8
  from importlib.resources import files
8
-
9
- from packaging.version import Version
9
+ from urllib.parse import urlsplit
10
10
 
11
11
  KINDS = ("ats", "board", "aggregator", "custom")
12
12
  ACCOUNTS = ("none", "to_send", "to_read") # also the backlog order (ADR 0013)
13
13
  HOSTING = ("hosted", "embedded")
14
14
  VENDORS = ("DataDome", "Cloudflare", "Akamai", "PerimeterX", "hCaptcha", "unknown")
15
15
  STEPS = ("posting", "form", "upload")
16
- VERIFICATIONS = ("mapped", "dry_run_verified", "send_verified")
17
- CAUSES = ("system", "code", "unknown")
18
- RELEASES = ("this_release", "older", "newer")
19
-
20
-
21
- @dataclass(frozen=True)
22
- class Verification:
23
- date: date
24
- version: str # the release, compared with the installed one
25
- by: str
26
- commit: str | None = None # the exact code
16
+ SCOPES = ("out_of_scope", "in_scope", "supported")
27
17
 
28
18
 
29
19
  @dataclass(frozen=True)
30
- class Broken:
31
- """A send that worked fails now (ADR 0012); `reason` names fields and steps, never postings."""
20
+ class Scope:
21
+ """Where a source stands (ADR 0016). `supported`: a dev dry run passed at `commit`, never
22
+ demoted. `in_scope`: wanted, its `reason` the known gap a round stopped at, if one did.
23
+ `out_of_scope`: never filled, it needs the user live (ADR 0014). A `reason` names fields
24
+ and steps, never postings."""
32
25
 
33
- date: date
34
- version: str
35
- commit: str
36
- by: str
37
- reason: str
38
- cause: str = "unknown"
39
-
40
-
41
- @dataclass(frozen=True)
42
- class Shortfall:
43
- """A round that ended without an adapter passing the adapter gate (ADR 0013): still wanted.
44
- `reason` names the known gap that stopped it, never postings."""
45
-
46
- date: date
47
- version: str
48
- commit: str
49
- by: str
50
- reason: str
51
-
52
-
53
- @dataclass(frozen=True)
54
- class OutOfScope:
55
- """A source atsfill will never fill: it needs the user live (ADR 0014)."""
56
-
57
- reason: str
58
- date: date
26
+ value: str = "in_scope"
27
+ date: datetime.date | None = None
28
+ commit: str | None = None
29
+ reason: str | None = None
59
30
 
60
31
 
61
32
  @dataclass(frozen=True)
@@ -76,20 +47,11 @@ class Source:
76
47
  account: str
77
48
  hosting: tuple[str, ...]
78
49
  adapter: str | None = None
50
+ hosts: tuple[str, ...] = () # a source without an adapter: where its postings are
79
51
  share: float | None = None
80
52
  links_to: tuple[str, ...] = () # an aggregator's: the sources its postings link to
81
53
  block: Block | None = None
82
- mapped: Verification | None = None
83
- dry_run_verified: Verification | None = None
84
- send_verified: Verification | None = None # the last known good, kept through a break
85
- send_broken: Broken | None = None
86
- unsupported: Shortfall | None = None
87
- out_of_scope: OutOfScope | None = None
88
-
89
- @property
90
- def verification(self) -> str | None:
91
- """The furthest verification reached; each one implies the ones before it."""
92
- return next((s for s in reversed(VERIFICATIONS) if getattr(self, s)), None)
54
+ scope: Scope = Scope()
93
55
 
94
56
 
95
57
  def _one_of(name: str, key: str, value: object, allowed: tuple[str, ...]) -> None:
@@ -112,26 +74,19 @@ def _source(name: str, entry: dict) -> Source:
112
74
  _one_of(name, "block.vendor", block.get("vendor"), VENDORS)
113
75
  _one_of(name, "block.step", block.get("step"), STEPS)
114
76
  block = Block(**block)
115
- verifications = {s: Verification(**entry[s]) for s in VERIFICATIONS if s in entry}
116
- broken = entry.get("send_broken")
117
- if broken is not None:
118
- _one_of(name, "send_broken.cause", broken.get("cause", "unknown"), CAUSES)
119
- broken = Broken(**broken)
120
- unsupported = entry.get("unsupported")
121
- out_of_scope = entry.get("out_of_scope")
77
+ scope = dict(entry.get("scope", {"is": "in_scope"}))
78
+ _one_of(name, "scope.is", scope.get("is"), SCOPES)
122
79
  return Source(
123
80
  name,
124
81
  entry["kind"],
125
82
  entry["account"],
126
83
  tuple(entry.get("hosting", [])),
127
84
  entry.get("adapter"),
85
+ tuple(entry.get("hosts", [])),
128
86
  entry.get("share"),
129
87
  tuple(entry.get("links_to", [])),
130
88
  block,
131
- **verifications,
132
- send_broken=broken,
133
- unsupported=Shortfall(**unsupported) if unsupported is not None else None,
134
- out_of_scope=OutOfScope(**out_of_scope) if out_of_scope is not None else None,
89
+ Scope(scope.pop("is"), **scope),
135
90
  )
136
91
 
137
92
 
@@ -148,52 +103,47 @@ def _toml(value: object) -> str:
148
103
  return str(value) if isinstance(value, date) else f'"{value}"'
149
104
 
150
105
 
151
- def with_verification(
152
- text: str, source: str, key: str, value: Verification | Broken | Shortfall
153
- ) -> str:
154
- """The registry file's text with `key` of `source` set to `value`, the rest untouched.
155
- Writing `send_verified` ends a `send_broken`."""
106
+ def with_scope(text: str, source: str, scope: Scope) -> str:
107
+ """The registry file's text with `source`'s scope set, the rest untouched."""
156
108
  lines = text.splitlines()
157
109
  start = lines.index(f"[{source}]")
158
110
  end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines))
159
111
  while lines[end - 1] == "":
160
112
  end -= 1
161
- gone = {key, "send_broken"} if key == "send_verified" else {key}
162
- entry = lines[start:end]
163
- at = next((i for i, x in enumerate(entry) if x.split(" =")[0] in gone), len(entry))
164
- entry = [x for x in entry if x.split(" =")[0] not in gone]
113
+ entry = [x for x in lines[start:end] if not x.startswith("scope =")]
165
114
  pairs = ", ".join(
166
- f"{f.name} = {_toml(getattr(value, f.name))}"
167
- for f in fields(value)
168
- if getattr(value, f.name) is not None
115
+ f"{'is' if f.name == 'value' else f.name} = {_toml(getattr(scope, f.name))}"
116
+ for f in fields(scope)
117
+ if getattr(scope, f.name) is not None
169
118
  )
170
- entry.insert(min(at, len(entry)), f"{key} = {{ {pairs} }}")
119
+ entry.append(f"scope = {{ {pairs} }}")
171
120
  return "\n".join(lines[:start] + entry + lines[end:]) + "\n"
172
121
 
173
122
 
174
- def release(verified: str, installed: str) -> str:
175
- """Whether a verified release is the installed one, `older` or `newer`."""
176
- a, b = (Version(Version(v).public) for v in (verified, installed))
177
- return "this_release" if a == b else "older" if a < b else "newer"
178
-
179
-
180
123
  def for_adapter(name: str) -> Source | None:
181
124
  return next((s for s in sources().values() if s.adapter == name), None)
182
125
 
183
126
 
127
+ def for_url(url: str) -> Source | None:
128
+ """The source whose `hosts` a URL is on, a subdomain included."""
129
+ host = urlsplit(url).hostname or ""
130
+ on = lambda h: host == h or host.endswith(f".{h}") # noqa: E731
131
+ return next((s for s in sources().values() if any(map(on, s.hosts))), None)
132
+
133
+
184
134
  def _backlog_key(s: Source) -> tuple:
185
- stopped = s.block is not None or s.unsupported is not None
135
+ stopped = s.block is not None or s.scope.reason is not None
186
136
  return (stopped, ACCOUNTS.index(s.account), -(s.share or 0), s.name)
187
137
 
188
138
 
189
139
  def _row(s: Source) -> str:
190
140
  share = f"{s.share:.1%}" if s.share is not None else ""
191
141
  block = f"{s.block.vendor} at {s.block.step}" if s.block else ""
192
- v = "send_broken" if s.send_broken else "unsupported" if s.unsupported else s.verification or ""
193
- if s.out_of_scope:
194
- v = f"out of scope: {s.out_of_scope.reason}"
142
+ v = s.scope.value.replace("_", " ")
143
+ if s.scope.reason:
144
+ v += f": {s.scope.reason}"
195
145
  if s.adapter:
196
- v = f"[{v}]({s.adapter}.md)" if v else f"[adapter]({s.adapter}.md)"
146
+ v = f"[{v}]({s.adapter}.md)"
197
147
  kind = f"{s.kind} → {', '.join(s.links_to)}" if s.links_to else s.kind
198
148
  cells = [s.name, kind, share, s.account, ", ".join(s.hosting), block, v]
199
149
  return "| " + " | ".join(cells) + " |"
@@ -202,9 +152,9 @@ def _row(s: Source) -> str:
202
152
  def backlog(registry: dict[str, Source] | None = None) -> str:
203
153
  """`docs/adapters/README.md`: the systems in backlog order (ADR 0013)."""
204
154
  registry = sources() if registry is None else registry
205
- columns = ("Source", "Kind", "Share", "Account", "Hosting", "Block", "Verification")
155
+ columns = ("Source", "Kind", "Share", "Account", "Hosting", "Block", "Scope")
206
156
  head = f"| {' | '.join(columns)} |\n" + "|---" * len(columns) + "|"
207
- wanted = lambda s: s.kind in ("ats", "board") and not s.out_of_scope # noqa: E731
157
+ wanted = lambda s: s.kind in ("ats", "board") and s.scope.value != "out_of_scope" # noqa: E731
208
158
  adapters = sorted((s for s in registry.values() if wanted(s)), key=_backlog_key)
209
159
  others = sorted((s for s in registry.values() if not wanted(s)), key=_backlog_key)
210
160
  text = [
@@ -214,7 +164,7 @@ def backlog(registry: dict[str, Source] | None = None) -> str:
214
164
  " `uv run python -m scripts.backlog`. The registry is described in"
215
165
  " [ADR 0012](../adr/0012-systems-registry.md), the order in"
216
166
  " [ADR 0013](../adr/0013-survey-and-adapter-rounds.md): no account first, blocked and"
217
- " unsupported last, then by share of postings.",
167
+ " stopped by a known gap last, then by share of postings.",
218
168
  "",
219
169
  f"The registry covers {sum(s.share or 0 for s in registry.values()):.1%} of postings.",
220
170
  "",
@@ -234,19 +184,12 @@ def backlog(registry: dict[str, Source] | None = None) -> str:
234
184
 
235
185
 
236
186
  BADGES = ("<!-- badges -->", "<!-- /badges -->")
237
- COLORS = {"send_verified": "9ece6a", "dry_run_verified": "7aa2f7", "mapped": "a9b1d6"}
187
+ COLORS = {"supported": "9ece6a", "in_scope": "e0af68", "out_of_scope": "565f89"}
238
188
 
239
189
 
240
190
  def _badge(s: Source) -> str:
241
- if s.send_broken:
242
- message, color = f"send broken {s.send_broken.date}", "f7768e"
243
- elif s.unsupported:
244
- message, color = f"unsupported {s.unsupported.date}", "e0af68"
245
- elif s.verification:
246
- v: Verification = getattr(s, s.verification)
247
- message, color = f"{s.verification.replace('_', ' ')} {v.date}", COLORS[s.verification]
248
- else:
249
- message, color = "unverified", "565f89"
191
+ message = " ".join(str(x) for x in (s.scope.value.replace("_", " "), s.scope.date) if x)
192
+ color = COLORS[s.scope.value]
250
193
  escape = lambda t: t.replace("-", "--").replace("_", "__").replace(" ", "%20") # noqa: E731
251
194
  url = f"https://img.shields.io/badge/{escape(s.adapter or s.name)}-{escape(message)}-{color}"
252
195
  return f"![{s.adapter}: {message}]({url})"
@@ -101,7 +101,7 @@ class Result:
101
101
  checks: list[Check] = field(default_factory=list)
102
102
  side_effects: list[SideEffect] = field(default_factory=list)
103
103
  evidence: str | None = None
104
- warning: str | None = None # `code: detail`, on a ready dry run or a send that went ahead
104
+ notice: str | None = None # telemetry's, the first time it is on (ADR 0016)
105
105
 
106
106
 
107
107
  @dataclass
@@ -122,10 +122,7 @@ class AdapterStatus:
122
122
  submit_allowed: list[str] # the users whose config lets it submit
123
123
  side_effects: list[SideEffect]
124
124
  side_effects_muted: list[str] # the users who muted them
125
- verified: str | None = None # mapped | dry_run_verified | send_verified (ADR 0012)
126
- verified_version: str | None = None
127
- verified_release: str | None = None # this_release | older | newer than the installed one
128
- send_broken: str | None = None # since when and why, with the last verified send
125
+ scope: str | None = None # supported | in_scope | out_of_scope, None when not in the registry
129
126
 
130
127
 
131
128
  @dataclass