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.
- geo_check-0.3.0/.editorconfig +21 -0
- geo_check-0.3.0/.gitattributes +9 -0
- geo_check-0.3.0/.github/ISSUE_TEMPLATE/crawler-list.md +33 -0
- geo_check-0.3.0/.github/ISSUE_TEMPLATE/wrong-verdict.md +41 -0
- geo_check-0.3.0/.github/workflows/audit-a-site.yml +73 -0
- geo_check-0.3.0/.github/workflows/ci.yml +60 -0
- geo_check-0.3.0/.github/workflows/release.yml +68 -0
- geo_check-0.3.0/.gitignore +22 -0
- geo_check-0.3.0/CHANGELOG.md +208 -0
- geo_check-0.3.0/CLAUDE.md +331 -0
- geo_check-0.3.0/CODE_OF_CONDUCT.md +36 -0
- geo_check-0.3.0/CONTRIBUTING.md +139 -0
- geo_check-0.3.0/LICENSE +21 -0
- geo_check-0.3.0/PKG-INFO +281 -0
- geo_check-0.3.0/README.md +241 -0
- geo_check-0.3.0/SECURITY.md +51 -0
- geo_check-0.3.0/SKILL.md +99 -0
- geo_check-0.3.0/action.yml +132 -0
- geo_check-0.3.0/data/accuracy_report.json +146 -0
- geo_check-0.3.0/data/block_calibration.csv +869 -0
- geo_check-0.3.0/data/corpus.txt +1000 -0
- geo_check-0.3.0/data/corpus_categories.csv +907 -0
- geo_check-0.3.0/data/corpus_manifest.csv +907 -0
- geo_check-0.3.0/data/golden_30.yaml +421 -0
- geo_check-0.3.0/data/js_calibration.csv +23 -0
- geo_check-0.3.0/data/robustness_report.json +919 -0
- geo_check-0.3.0/docs/CRAWLERS.md +100 -0
- geo_check-0.3.0/docs/RUBRIC.md +252 -0
- geo_check-0.3.0/docs/VALIDATION.md +250 -0
- geo_check-0.3.0/plan.md +251 -0
- geo_check-0.3.0/pyproject.toml +69 -0
- geo_check-0.3.0/scripts/build_crawlers_doc.py +96 -0
- geo_check-0.3.0/scripts/build_manifest.py +109 -0
- geo_check-0.3.0/scripts/calibrate_js_threshold.py +129 -0
- geo_check-0.3.0/scripts/refresh_fixtures.py +143 -0
- geo_check-0.3.0/scripts/run_study.py +373 -0
- geo_check-0.3.0/scripts/verify_accuracy.py +256 -0
- geo_check-0.3.0/scripts/verify_manifest.py +89 -0
- geo_check-0.3.0/src/geo_check/__init__.py +3 -0
- geo_check-0.3.0/src/geo_check/checks/__init__.py +49 -0
- geo_check-0.3.0/src/geo_check/checks/access_citation.py +92 -0
- geo_check-0.3.0/src/geo_check/checks/access_noindex.py +96 -0
- geo_check-0.3.0/src/geo_check/checks/access_pages.py +109 -0
- geo_check-0.3.0/src/geo_check/checks/access_sitemap.py +89 -0
- geo_check-0.3.0/src/geo_check/checks/access_user_fetch.py +84 -0
- geo_check-0.3.0/src/geo_check/checks/readability_answers.py +249 -0
- geo_check-0.3.0/src/geo_check/checks/readability_author_dates.py +117 -0
- geo_check-0.3.0/src/geo_check/checks/readability_canonical.py +96 -0
- geo_check-0.3.0/src/geo_check/checks/readability_headings.py +105 -0
- geo_check-0.3.0/src/geo_check/checks/readability_jsonld.py +147 -0
- geo_check-0.3.0/src/geo_check/checks/readability_llms_txt.py +92 -0
- geo_check-0.3.0/src/geo_check/checks/readability_raw_html.py +142 -0
- geo_check-0.3.0/src/geo_check/checks/readability_title.py +144 -0
- geo_check-0.3.0/src/geo_check/cli.py +252 -0
- geo_check-0.3.0/src/geo_check/content_signals.py +114 -0
- geo_check-0.3.0/src/geo_check/data/agents.json +284 -0
- geo_check-0.3.0/src/geo_check/fetch.py +175 -0
- geo_check-0.3.0/src/geo_check/fixtures.py +104 -0
- geo_check-0.3.0/src/geo_check/models.py +219 -0
- geo_check-0.3.0/src/geo_check/py.typed +0 -0
- geo_check-0.3.0/src/geo_check/report/__init__.py +1 -0
- geo_check-0.3.0/src/geo_check/report/json_out.py +234 -0
- geo_check-0.3.0/src/geo_check/report/markdown.py +319 -0
- geo_check-0.3.0/src/geo_check/robots.py +286 -0
- geo_check-0.3.0/src/geo_check/scoring.py +125 -0
- geo_check-0.3.0/src/geo_check/site.py +213 -0
- geo_check-0.3.0/src/geo_check/sitemap.py +142 -0
- geo_check-0.3.0/tests/test_agents.py +51 -0
- geo_check-0.3.0/tests/test_checks_access.py +192 -0
- geo_check-0.3.0/tests/test_checks_readability.py +336 -0
- geo_check-0.3.0/tests/test_content_signals.py +74 -0
- geo_check-0.3.0/tests/test_corpus_shape.py +97 -0
- geo_check-0.3.0/tests/test_crawlers_doc.py +92 -0
- geo_check-0.3.0/tests/test_failure_modes.py +55 -0
- geo_check-0.3.0/tests/test_golden.py +98 -0
- geo_check-0.3.0/tests/test_report.py +167 -0
- geo_check-0.3.0/tests/test_robots.py +289 -0
- geo_check-0.3.0/tests/test_robustness.py +143 -0
- geo_check-0.3.0/tests/test_scoring.py +78 -0
- 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.
|