geo-check 0.3.0__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 (80) hide show
  1. geo_check-0.3.0/.editorconfig +21 -0
  2. geo_check-0.3.0/.gitattributes +9 -0
  3. geo_check-0.3.0/.github/ISSUE_TEMPLATE/crawler-list.md +33 -0
  4. geo_check-0.3.0/.github/ISSUE_TEMPLATE/wrong-verdict.md +41 -0
  5. geo_check-0.3.0/.github/workflows/audit-a-site.yml +73 -0
  6. geo_check-0.3.0/.github/workflows/ci.yml +60 -0
  7. geo_check-0.3.0/.github/workflows/release.yml +68 -0
  8. geo_check-0.3.0/.gitignore +22 -0
  9. geo_check-0.3.0/CHANGELOG.md +208 -0
  10. geo_check-0.3.0/CLAUDE.md +331 -0
  11. geo_check-0.3.0/CODE_OF_CONDUCT.md +36 -0
  12. geo_check-0.3.0/CONTRIBUTING.md +139 -0
  13. geo_check-0.3.0/LICENSE +21 -0
  14. geo_check-0.3.0/PKG-INFO +281 -0
  15. geo_check-0.3.0/README.md +241 -0
  16. geo_check-0.3.0/SECURITY.md +51 -0
  17. geo_check-0.3.0/SKILL.md +99 -0
  18. geo_check-0.3.0/action.yml +132 -0
  19. geo_check-0.3.0/data/accuracy_report.json +146 -0
  20. geo_check-0.3.0/data/block_calibration.csv +869 -0
  21. geo_check-0.3.0/data/corpus.txt +1000 -0
  22. geo_check-0.3.0/data/corpus_categories.csv +907 -0
  23. geo_check-0.3.0/data/corpus_manifest.csv +907 -0
  24. geo_check-0.3.0/data/golden_30.yaml +421 -0
  25. geo_check-0.3.0/data/js_calibration.csv +23 -0
  26. geo_check-0.3.0/data/robustness_report.json +919 -0
  27. geo_check-0.3.0/docs/CRAWLERS.md +100 -0
  28. geo_check-0.3.0/docs/RUBRIC.md +252 -0
  29. geo_check-0.3.0/docs/VALIDATION.md +250 -0
  30. geo_check-0.3.0/plan.md +251 -0
  31. geo_check-0.3.0/pyproject.toml +69 -0
  32. geo_check-0.3.0/scripts/build_crawlers_doc.py +96 -0
  33. geo_check-0.3.0/scripts/build_manifest.py +109 -0
  34. geo_check-0.3.0/scripts/calibrate_js_threshold.py +129 -0
  35. geo_check-0.3.0/scripts/refresh_fixtures.py +143 -0
  36. geo_check-0.3.0/scripts/run_study.py +373 -0
  37. geo_check-0.3.0/scripts/verify_accuracy.py +256 -0
  38. geo_check-0.3.0/scripts/verify_manifest.py +89 -0
  39. geo_check-0.3.0/src/geo_check/__init__.py +3 -0
  40. geo_check-0.3.0/src/geo_check/checks/__init__.py +49 -0
  41. geo_check-0.3.0/src/geo_check/checks/access_citation.py +92 -0
  42. geo_check-0.3.0/src/geo_check/checks/access_noindex.py +96 -0
  43. geo_check-0.3.0/src/geo_check/checks/access_pages.py +109 -0
  44. geo_check-0.3.0/src/geo_check/checks/access_sitemap.py +89 -0
  45. geo_check-0.3.0/src/geo_check/checks/access_user_fetch.py +84 -0
  46. geo_check-0.3.0/src/geo_check/checks/readability_answers.py +249 -0
  47. geo_check-0.3.0/src/geo_check/checks/readability_author_dates.py +117 -0
  48. geo_check-0.3.0/src/geo_check/checks/readability_canonical.py +96 -0
  49. geo_check-0.3.0/src/geo_check/checks/readability_headings.py +105 -0
  50. geo_check-0.3.0/src/geo_check/checks/readability_jsonld.py +147 -0
  51. geo_check-0.3.0/src/geo_check/checks/readability_llms_txt.py +92 -0
  52. geo_check-0.3.0/src/geo_check/checks/readability_raw_html.py +142 -0
  53. geo_check-0.3.0/src/geo_check/checks/readability_title.py +144 -0
  54. geo_check-0.3.0/src/geo_check/cli.py +252 -0
  55. geo_check-0.3.0/src/geo_check/content_signals.py +114 -0
  56. geo_check-0.3.0/src/geo_check/data/agents.json +284 -0
  57. geo_check-0.3.0/src/geo_check/fetch.py +175 -0
  58. geo_check-0.3.0/src/geo_check/fixtures.py +104 -0
  59. geo_check-0.3.0/src/geo_check/models.py +219 -0
  60. geo_check-0.3.0/src/geo_check/py.typed +0 -0
  61. geo_check-0.3.0/src/geo_check/report/__init__.py +1 -0
  62. geo_check-0.3.0/src/geo_check/report/json_out.py +234 -0
  63. geo_check-0.3.0/src/geo_check/report/markdown.py +319 -0
  64. geo_check-0.3.0/src/geo_check/robots.py +286 -0
  65. geo_check-0.3.0/src/geo_check/scoring.py +125 -0
  66. geo_check-0.3.0/src/geo_check/site.py +213 -0
  67. geo_check-0.3.0/src/geo_check/sitemap.py +142 -0
  68. geo_check-0.3.0/tests/test_agents.py +51 -0
  69. geo_check-0.3.0/tests/test_checks_access.py +192 -0
  70. geo_check-0.3.0/tests/test_checks_readability.py +336 -0
  71. geo_check-0.3.0/tests/test_content_signals.py +74 -0
  72. geo_check-0.3.0/tests/test_corpus_shape.py +97 -0
  73. geo_check-0.3.0/tests/test_crawlers_doc.py +92 -0
  74. geo_check-0.3.0/tests/test_failure_modes.py +55 -0
  75. geo_check-0.3.0/tests/test_golden.py +98 -0
  76. geo_check-0.3.0/tests/test_report.py +167 -0
  77. geo_check-0.3.0/tests/test_robots.py +289 -0
  78. geo_check-0.3.0/tests/test_robustness.py +143 -0
  79. geo_check-0.3.0/tests/test_scoring.py +78 -0
  80. geo_check-0.3.0/tests/test_sitemap.py +136 -0
@@ -0,0 +1,21 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+
10
+ [*.py]
11
+ indent_size = 4
12
+ max_line_length = 100
13
+
14
+ [*.{yml,yaml,json,toml}]
15
+ indent_size = 2
16
+
17
+ [*.md]
18
+ trim_trailing_whitespace = false
19
+
20
+ [*.{txt,csv}]
21
+ insert_final_newline = true
@@ -0,0 +1,9 @@
1
+ # Line endings are stored as LF in the repository, whatever platform the
2
+ # contributor works on. Without this, a Windows checkout commits CRLF and every
3
+ # later diff is full of whitespace noise.
4
+ * text=auto eol=lf
5
+
6
+ # Fixtures are recorded HTTP responses and must stay byte exact. A robots.txt
7
+ # saved with CRLF has to reach the parser with CRLF, because that is what the
8
+ # server sent. Normalising them would hide the very bugs they exist to catch.
9
+ tests/fixtures/** -text
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: Crawler list correction
3
+ about: An agent is missing, in the wrong bucket, or its documentation moved
4
+ title: "agents.json: "
5
+ labels: crawler-list
6
+ ---
7
+
8
+ The most useful contribution to this project. The bucket an agent sits in is
9
+ what the whole Access score rests on, and vendors rename and reclassify
10
+ crawlers without announcing it.
11
+
12
+ **Agent token**
13
+
14
+
15
+ **What is wrong**
16
+
17
+ Missing, wrong bucket, wrong `obeys_robots`, dead documentation URL, or renamed.
18
+
19
+ **Vendor documentation URL**
20
+
21
+ Required. A page the vendor publishes, not a third party crawler directory.
22
+ Entries in `src/geo_check/data/agents.json` are only accepted with one, because
23
+ the file records what vendors say rather than what the field believes.
24
+
25
+ **Which bucket, and why**
26
+
27
+ - `citation` feeds retrieval and citation in AI answers
28
+ - `user_fetch` retrieves one page on demand when someone asks about a link
29
+ - `training` collects pages for model training, and is worth no points
30
+
31
+ **Anything the vendor says about robots.txt**
32
+
33
+ Some fetchers are documented as ignoring it. If this one is, quote the line.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: Wrong verdict
3
+ about: The tool says a crawler is blocked or allowed and it is not
4
+ title: "wrong verdict: "
5
+ labels: bug
6
+ ---
7
+
8
+ **Domain**
9
+
10
+
11
+ **What the tool said**
12
+
13
+ Paste the relevant lines, including the `because of` line, which names the
14
+ robots.txt group that produced the verdict.
15
+
16
+ ```
17
+ ```
18
+
19
+ **What you believe is correct, and why**
20
+
21
+
22
+ **The robots.txt**
23
+
24
+ Paste the groups that mention the agent, with a few lines either side. Context
25
+ matters more than it looks: three defects found so far came from a directive
26
+ between groups, a group declared twice, and a group whose name was a substring
27
+ of the agent token.
28
+
29
+ ```
30
+ ```
31
+
32
+ **Version**
33
+
34
+ `geo-check --help` prints it, or give the commit.
35
+
36
+ ---
37
+
38
+ Verdicts are decided in `src/geo_check/robots.py` and every one of the three
39
+ known defects was reported this way. A report with the robots.txt attached can
40
+ be turned into a regression test the same day; one without it usually cannot be
41
+ reproduced at all.
@@ -0,0 +1,73 @@
1
+ # Run the tool without installing anything: fork this repository, open the
2
+ # Actions tab, pick this workflow and press Run.
3
+ #
4
+ # Manual only. The test suite is offline by design and CI must stay that way, so
5
+ # nothing here runs on push or pull request where a stranger's robots.txt could
6
+ # turn the badge red. This is also the smoke test for action.yml, which nothing
7
+ # else exercises.
8
+ name: audit a site
9
+
10
+ on:
11
+ workflow_dispatch:
12
+ inputs:
13
+ domain:
14
+ description: Domain to audit, for example example.com
15
+ required: true
16
+ default: example.com
17
+ pages:
18
+ description: How many pages to sample
19
+ required: false
20
+ default: "5"
21
+ fail-under-access:
22
+ description: Fail the run below this Access score, blank to just report
23
+ required: false
24
+ default: ""
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ audit:
31
+ runs-on: ubuntu-latest
32
+ timeout-minutes: 10
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+
36
+ - id: geo
37
+ uses: ./
38
+ with:
39
+ domain: ${{ inputs.domain }}
40
+ pages: ${{ inputs.pages }}
41
+ fail-under-access: ${{ inputs.fail-under-access }}
42
+
43
+ - name: Show the scores
44
+ shell: bash
45
+ run: |
46
+ echo "Access ${{ steps.geo.outputs.access }} (${{ steps.geo.outputs.access-grade }})"
47
+ echo "Readability ${{ steps.geo.outputs.readability }} (${{ steps.geo.outputs.readability-grade }})"
48
+
49
+ - uses: actions/upload-artifact@v7
50
+ with:
51
+ name: geo-check-report
52
+ path: |
53
+ geo-check.md
54
+ geo-check.json
55
+
56
+ # The job above runs the action from the working tree. This one runs the
57
+ # published tag the way a stranger's workflow would, which is the only thing
58
+ # that proves the release actually works for anyone but us.
59
+ consume-the-published-action:
60
+ runs-on: ubuntu-latest
61
+ timeout-minutes: 10
62
+ steps:
63
+ - id: geo
64
+ uses: vasco-branco06/geo-check@v0
65
+ with:
66
+ domain: ${{ inputs.domain }}
67
+ pages: "1"
68
+
69
+ - name: It answered
70
+ shell: bash
71
+ run: |
72
+ test -n "${{ steps.geo.outputs.access }}" || { echo "no score came back"; exit 1; }
73
+ echo "published action returned Access ${{ steps.geo.outputs.access }} (${{ steps.geo.outputs.access-grade }})"
@@ -0,0 +1,60 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ timeout-minutes: 10
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+
22
+ - uses: actions/setup-python@v7
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ cache: pip
26
+
27
+ - name: Install
28
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"
29
+
30
+ - name: Lint
31
+ run: |
32
+ ruff check src tests scripts
33
+ ruff format --check src tests scripts
34
+
35
+ # The suite is offline and replays recorded responses, including the
36
+ # golden set of thirty hard sites. It never touches the network, so CI
37
+ # cannot be flaky because someone else's robots.txt changed.
38
+ - name: Test
39
+ run: pytest -q
40
+
41
+ build:
42
+ runs-on: ubuntu-latest
43
+ timeout-minutes: 10
44
+ steps:
45
+ - uses: actions/checkout@v7
46
+ - uses: actions/setup-python@v7
47
+ with:
48
+ python-version: "3.12"
49
+ # Proves the console script, the packaged agent list and the argument
50
+ # parser all survive a real install. Deliberately does not audit a live
51
+ # site: the suite's whole claim is that it never touches the network, and
52
+ # a CI job that does would be flaky for reasons no contributor caused.
53
+ - name: Build the wheel and install it clean
54
+ run: |
55
+ python -m pip install --upgrade pip build
56
+ python -m build --wheel
57
+ python -m venv /tmp/clean
58
+ /tmp/clean/bin/python -m pip install dist/*.whl
59
+ /tmp/clean/bin/geo-check --help
60
+ /tmp/clean/bin/python -c "from geo_check.robots import load_agents; assert len(load_agents()) > 20"
@@ -0,0 +1,68 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI without a token existing anywhere. GitHub proves who it is
4
+ # over OIDC, PyPI trusts that proof, and there is nothing to leak from a laptop
5
+ # or a repository secret. The trusted publisher is configured once at
6
+ # pypi.org/manage/account/publishing and names this file by its filename, so
7
+ # renaming this workflow breaks publishing until that entry is updated too.
8
+
9
+ on:
10
+ push:
11
+ # v0.3.0 yes, v0 no. The major tag moves forward with every release, and a
12
+ # second attempt to publish a version PyPI already has fails by design.
13
+ tags: ["v*.*.*"]
14
+ # The tag for a release that is already cut cannot fire a workflow that did
15
+ # not exist when it was pushed, so the first publish is a manual one.
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ publish:
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 10
25
+ permissions:
26
+ contents: read
27
+ # What lets the job prove its identity to PyPI. Without it there is no
28
+ # token and no proof, and nothing publishes.
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+ - uses: actions/setup-python@v7
33
+ with:
34
+ python-version: "3.12"
35
+
36
+ # A version on PyPI can never be replaced or corrected, only hidden. The
37
+ # easiest way to make that permanent mistake is to tag v0.4.0 while
38
+ # pyproject.toml still says 0.3.0, so refuse rather than publish it.
39
+ - name: Refuse to publish if the tag and the version disagree
40
+ run: |
41
+ version=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
42
+ echo "pyproject.toml says $version"
43
+ case "$GITHUB_REF" in
44
+ refs/tags/*)
45
+ tag="${GITHUB_REF#refs/tags/v}"
46
+ echo "the tag says $tag"
47
+ if [ "$tag" != "$version" ]; then
48
+ echo "::error::tag v$tag does not match version $version. Nothing published."
49
+ exit 1
50
+ fi
51
+ ;;
52
+ *)
53
+ echo "::warning::not running from a tag, so the version was not checked against one"
54
+ ;;
55
+ esac
56
+
57
+ # Built from what the tag holds rather than from whatever is in a local
58
+ # dist/ directory. twine check catches metadata that PyPI would reject
59
+ # after the upload has already started.
60
+ - name: Build and check the distributions
61
+ run: |
62
+ python -m pip install --upgrade pip build twine
63
+ python -m build
64
+ python -m twine check dist/*
65
+ ls -la dist/
66
+
67
+ - name: Publish to PyPI
68
+ uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .DS_Store
11
+ reports/
12
+ .claude-flow/
13
+
14
+ # Robustness fixtures for the 906 site corpus. Recorded locally with
15
+ # scripts/refresh_fixtures.py, never committed: 906 sites of real HTML is 342 MB.
16
+ # The golden set under tests/fixtures/golden is committed and is 8 MB.
17
+ tests/fixtures/corpus/
18
+
19
+ # The aggregate analysis. Regenerate it with scripts/run_study.py. Not
20
+ # committed: it carries every finding in structured form, and the write up of
21
+ # those findings is the maintainer to publish, not the repository to hand over.
22
+ data/study.json
@@ -0,0 +1,208 @@
1
+ # Changelog
2
+
3
+ Notable changes, newest first. Format loosely follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## v0.3.0 - 2026-09-05
7
+
8
+ ### Added
9
+
10
+ - Five citation crawlers the list did not know about, each with its vendor's own
11
+ documentation: `Meta-WebIndexer`, `MistralAI-Index`, `Amzn-SearchBot`,
12
+ `DuckAssistBot` and `YouBot`. Meta and Mistral had no citation crawler here at
13
+ all, so a site shut out of Meta AI or Mistral search scored clean. Meta's page
14
+ says allowing theirs helps Meta AI cite and link your content; Mistral
15
+ documents theirs as indexing for search and explicitly not for training
16
+ - Three on demand fetchers documented as not honouring robots.txt: `Amzn-User`,
17
+ `Google-GeminiNotebook` and `Google-Agent`. Google was represented here as
18
+ honouring robots.txt everywhere, and its own page says its user triggered
19
+ fetchers generally ignore it
20
+ - `scripts/build_crawlers_doc.py`. `docs/CRAWLERS.md` has claimed since the first
21
+ release that it is generated from `agents.json`, and nothing generated it, so
22
+ it fell behind. `tests/test_crawlers_doc.py` now fails when the two disagree,
23
+ and it also binds the count the README quotes
24
+
25
+ ### Removed
26
+
27
+ - `cohere-ai`. Cohere now publishes a crawler page whose bot table reads N/A and
28
+ which states that it does not use bots or user agents to crawl the web for
29
+ training. The token appears nowhere on it
30
+
31
+ ### Changed
32
+
33
+ - The citation bucket holds eleven agents rather than six, so the 50 points split
34
+ eleven ways. A site blocking one of the original six is penalised less than it
35
+ was, which is right, because it is still reachable by ten others. A site
36
+ blocking with a wildcard is penalised more, which is also right: `casa.pt` goes
37
+ from 48.7 to 44.1 because it shuts out nine citation crawlers and the old list
38
+ only knew about four of them
39
+ - `ai_only` now marks the AI answer crawlers established enough that blocking
40
+ every one of them is a decision, rather than every crawler that only serves AI.
41
+ This was measured before it was chosen. Flagging all five newcomers took the
42
+ blackout detection, which `docs/RUBRIC.md` calls the failure this tool exists to
43
+ find, from 18 sites in the corpus to 4, because nobody blocks a crawler they
44
+ have never heard of. Narrowing the flag holds it at 18
45
+ - Entries whose documentation did not support them. `Bytespider` pointed at a
46
+ webmaster portal that is not reachable and is not crawler documentation, so it
47
+ now carries no link and says so; a new test refuses to let an undocumented
48
+ entry sit in a bucket that scores. `Diffbot` reads disputed rather than yes,
49
+ because the vendor says robots.txt is honoured by default and can be overridden
50
+ by agreement. `ChatGPT-User` reads disputed rather than no, because OpenAI
51
+ writes that the rules may not apply, which is weaker than Perplexity's flat
52
+ statement that it ignores them
53
+
54
+ ### Fixed
55
+
56
+ - `scripts/refresh_fixtures.py` no longer dies on the path written to keep it
57
+ alive. The handler built its message by adding an exception to a string, which
58
+ raises `TypeError` inside the `except`, so one unexpected crash took down a
59
+ whole sweep instead of being logged as one domain's outcome
60
+ - Tests no longer hard code how many agents a bucket holds. Adding a crawler
61
+ broke fifteen assertions that had no opinion about crawlers; they now read the
62
+ size from the list and mean all of them, or none of them
63
+
64
+ ## v0.2.0 - 2026-09-05
65
+
66
+ ### Added
67
+
68
+ - Content signals. Sites that declare what AI systems may do with their content,
69
+ through a `Content-Signal` line in robots.txt, now have that declaration read
70
+ and translated into a sentence. Reported and never scored, like training
71
+ posture. The recognised keys were taken from the 47 sites in the corpus that
72
+ send the directive rather than from the draft specification, which describes
73
+ two keys no site sends and omits two that most do. A further 8 sites publish
74
+ the explanatory terms without declaring anything, which by those terms grants
75
+ and restricts nothing, and is reported as its own state
76
+ - `SiteContext.robots_body`, the response body exactly as served. A robots.txt of
77
+ nothing but comments is treated as absent for crawl rules, correctly, and can
78
+ still carry a declaration worth reading
79
+
80
+ ### Fixed
81
+
82
+ - The user fetch score no longer counts blocks that do not work. Three of the
83
+ five agents in that bucket are documented by their own vendors as ignoring
84
+ robots.txt, so a Disallow aimed at one of them changes nothing and now costs
85
+ nothing. docs/RUBRIC.md had described this as a known weakness since the
86
+ first release. Replaying the corpus measured it before the change: of 744
87
+ scored sites, 61 block at least one of these agents, 53 score differently
88
+ under the new rule, and 26 of those change letter grade. The reported counts
89
+ are unchanged, because they report the robots.txt and not the score
90
+ - A domain containing a colon no longer ends the process with a traceback.
91
+ `httpx.InvalidURL` inherits from `Exception` rather than from `HTTPError`, so
92
+ it walked past every clause meant to catch it. It is now reported as
93
+ `invalid_url`, like any other unreachable homepage
94
+ - A name that does not resolve is answered in about three seconds instead of
95
+ thirty-six. A transport error is normally worth another go, so it was retried
96
+ three times over https and three more over http. DNS saying the name does not
97
+ exist is the one transport error that repetition cannot change
98
+ - `--json` and `--output` create the directory they are pointed at. The audit is
99
+ finished by the time either is written, so a path into a directory that did
100
+ not exist printed the whole report and then threw it away
101
+ - `--pages 0` and negative counts are refused. They were accepted, and quietly
102
+ produced a run over one page
103
+
104
+ ### Changed
105
+
106
+ - The skill installs with `pip install geo-check`. It previously said
107
+ `pip install -e .`, which needs a checkout and a working directory that
108
+ whoever installed the skill does not have
109
+ - The source distribution is 204 KB rather than 8.8 MB. The golden fixtures
110
+ prove the tool against recorded sites, and nobody installing it needs them
111
+ - `py.typed` ships, so the `Typing :: Typed` classifier the package has declared
112
+ from the start is now true and the annotations reach type checkers
113
+
114
+ ### Security
115
+
116
+ - `SKILL.md` states that quoted robots.txt lines are data and not instructions.
117
+ The report copies matched rules verbatim from a stranger's server, and at
118
+ least one site in the validation corpus uses its robots.txt to address
119
+ whichever agent is reading it. This has been in `SECURITY.md` since the first
120
+ release and is now also where an assistant will read it
121
+
122
+ ## v0.1.0 - 2026-08-30
123
+
124
+ First release. The GitHub Action is published to the
125
+ [Marketplace](https://github.com/marketplace/actions/geo-check) from this tag,
126
+ under `vasco-branco06/geo-check@v0`.
127
+
128
+ ### Added
129
+
130
+ - `action.yml`, a GitHub Action. Five lines in any workflow audits a site on
131
+ every deploy and fails the build below a score you set. It installs the exact
132
+ ref the caller pinned, so the audit and the rubric that scored it never drift
133
+ apart, and it writes the full report into the run summary
134
+ - A manual `audit a site` workflow, so anyone can run the tool from the Actions
135
+ tab of a fork without installing anything. Manual only: the suite is offline by
136
+ design and CI stays that way
137
+ - `docs/CRAWLERS.md`, all 25 user agents with vendor, bucket, whether the vendor
138
+ documents it as honouring `robots.txt`, and a link to that documentation.
139
+ Generated from `agents.json`, which stays the source of truth
140
+ - `assets/output.png` and `assets/social-preview.png`, both rendered from a real
141
+ recorded run rather than mocked up
142
+ - Validation across 906 real sites, with the evidence in `docs/VALIDATION.md`
143
+ - An accuracy harness, `scripts/verify_accuracy.py`, reading every `robots.txt`
144
+ three ways and comparing per agent
145
+ - Continuous integration across Python 3.10 to 3.14, plus a clean install of the
146
+ built wheel
147
+ - `data/corpus_manifest.csv`, the SHA-256 of every `robots.txt` in the corpus as
148
+ it was read, with `scripts/build_manifest.py` to rebuild it and
149
+ `scripts/verify_manifest.py` to check rows against the live web. The 342 MB of
150
+ recordings do not ship, so the fingerprints do
151
+ - `data/corpus_categories.csv`, site type for all 906 domains, read by
152
+ `scripts/run_study.py`. Hand assigned and checked against recorded pages;
153
+ `CONTRIBUTING.md` states the method, the boundaries, and what reading every row
154
+ back against its recorded homepage found
155
+ - A sectioning measure inside `answer_shaped_content`, with thresholds taken from
156
+ 12301 blocks across 868 pages rather than borrowed
157
+ - `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and issue templates
158
+
159
+ ### Changed
160
+
161
+ - The README leads with the result. The real output sits above the fold as a
162
+ rendered terminal, the crawler table moved to `docs/CRAWLERS.md`, and a section
163
+ answering the questions people actually ask replaces detail that belonged in
164
+ `docs/`. Every link is absolute, because relative links break on the PyPI page
165
+ - `data/corpus.txt` is one flat block ordered by sha256 of the domain, with
166
+ no section comments. Site type moved to its own file
167
+
168
+ ### Fixed
169
+
170
+ Ten figures in the documentation were corrected against the data behind them,
171
+ including `excalidraw.com`'s Readability score, the share of pages that are a
172
+ single block, the distance between the contradictory `CCBot` rules, and the
173
+ scope of the hand-traced block verdicts. Two claims were removed for resting on
174
+ data that does not ship, and the JavaScript heuristic's known false negative is
175
+ now stated in `docs/RUBRIC.md` rather than counted out of the sample.
176
+
177
+ Three defects in `robots.txt` handling, all found by the validation harness
178
+ before anyone hit them in the field. Each has a regression test.
179
+
180
+ - **User agents were matched by substring.** The underlying library matches the
181
+ way a full `User-Agent` header requires; this tool passes a bare product token.
182
+ A site writing `User-agent: bot` would have silently blocked GPTBot, Googlebot,
183
+ Bingbot, PerplexityBot, OAI-SearchBot and Claude-SearchBot at once. Group
184
+ selection moved into `robots.py`; the library still decides every path
185
+ question.
186
+ - **Groups declaring the same agent were not merged.** RFC 9309 section 2.2.1
187
+ requires it, and a site that contradicts itself was getting whichever rule came
188
+ first.
189
+ - **A bare `Crawl-delay` did not close a run of user-agent lines.** Two adjacent
190
+ groups were being glued into one, so an agent inherited a rule aimed at
191
+ another.
192
+
193
+ Also fixed: a platform detector that labelled an email marketing SaaS as a shop
194
+ because its marketing pages named every platform it integrates with. It now reads
195
+ the `generator` meta tag first.
196
+
197
+ ### Changed
198
+
199
+ - Renamed from `geo-audit` to `geo-check`. The former was taken on PyPI by a
200
+ similar tool, and PyPI normalises names.
201
+ - The fetcher backs off twenty seconds on HTTP 429 and honours `Retry-After`, and
202
+ the fixture recorder runs four domains at a time with staggered starts. Eight
203
+ in parallel was enough to have a CDN challenge a whole batch for half an hour,
204
+ which would have published ten reachable sites as unreachable.
205
+ - `robots.txt` is honoured for this tool's own user agent when sampling pages.
206
+ - The body of `/robots.txt` decides whether it is real, with the `Content-Type`
207
+ header only corroborating. A site serving a valid `robots.txt` as `text/html`
208
+ was being read as having none at all.