fixyourdocs 0.1.0__tar.gz → 0.2.1__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.
- fixyourdocs-0.2.1/.github/scripts/check_snippet_drift.py +138 -0
- fixyourdocs-0.2.1/.github/workflows/snippet-drift.yml +22 -0
- fixyourdocs-0.2.1/AGENTS.md +30 -0
- fixyourdocs-0.2.1/CHANGELOG.md +20 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/CODE_OF_CONDUCT.md +1 -1
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/CONTRIBUTING.md +1 -1
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/PKG-INFO +29 -2
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/README.md +27 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/pyproject.toml +5 -2
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/__init__.py +1 -1
- fixyourdocs-0.2.1/src/fixyourdocs/_cli.py +191 -0
- fixyourdocs-0.2.1/src/fixyourdocs/_init.py +61 -0
- fixyourdocs-0.2.1/src/fixyourdocs/snippet.py +37 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/test_async_client.py +1 -1
- fixyourdocs-0.2.1/tests/test_cli.py +226 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/test_client.py +1 -1
- fixyourdocs-0.2.1/tests/test_init.py +58 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/test_smoke.py +1 -1
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/.github/workflows/ci.yml +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/.github/workflows/cla.yml +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/.github/workflows/publish.yml +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/.gitignore +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/LICENSE +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/_http.py +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/_results.py +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/client.py +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/errors.py +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/models.py +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/src/fixyourdocs/py.typed +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/__init__.py +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/fixtures/full.json +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/fixtures/golden-path.json +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/fixtures/invalid.json +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/fixtures/minimum-required.json +0 -0
- {fixyourdocs-0.1.0 → fixyourdocs-0.2.1}/tests/test_models.py +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify the vendored AGENTS.md snippet matches the canonical upstream.
|
|
3
|
+
|
|
4
|
+
Pulls https://raw.githubusercontent.com/fixyourdocs/agents-md-snippet/main/README.md,
|
|
5
|
+
extracts the fenced ```markdown block, hashes it, and compares to the
|
|
6
|
+
SHA-256 of the ``SNIPPET`` constant exported by
|
|
7
|
+
``src/fixyourdocs/snippet.py``.
|
|
8
|
+
|
|
9
|
+
Run from the repo root:
|
|
10
|
+
|
|
11
|
+
python3 .github/scripts/check_snippet_drift.py
|
|
12
|
+
|
|
13
|
+
Tolerance for cross-repo content locks: if upstream `main` does not match
|
|
14
|
+
but an open PR on `agents-md-snippet` carries a matching block, this
|
|
15
|
+
exits 0 with a warning. This is the legitimate "the SDK PR re-vendors
|
|
16
|
+
what the upstream PR is about to merge" case. Once that upstream PR
|
|
17
|
+
merges, the warning goes away.
|
|
18
|
+
|
|
19
|
+
Exit code 0 on match (or matched open PR), 1 on real drift.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import pathlib
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
|
|
32
|
+
OWNER = "fixyourdocs"
|
|
33
|
+
REPO = "agents-md-snippet"
|
|
34
|
+
RAW_TEMPLATE = "https://raw.githubusercontent.com/{owner}/{repo}/{ref}/README.md"
|
|
35
|
+
PULLS_URL = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls?state=open&per_page=30"
|
|
36
|
+
|
|
37
|
+
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
38
|
+
VENDORED = ROOT / "src" / "fixyourdocs" / "snippet.py"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fail(msg: str) -> None:
|
|
42
|
+
print(f"snippet-drift: {msg}", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fetch_text(url: str) -> str:
|
|
47
|
+
req = urllib.request.Request(url, headers={"User-Agent": "snippet-drift"})
|
|
48
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
49
|
+
return resp.read().decode("utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fetch_block_at_ref(ref: str) -> str | None:
|
|
53
|
+
url = RAW_TEMPLATE.format(owner=OWNER, repo=REPO, ref=ref)
|
|
54
|
+
try:
|
|
55
|
+
text = fetch_text(url)
|
|
56
|
+
except urllib.error.HTTPError:
|
|
57
|
+
return None
|
|
58
|
+
matches = re.findall(r"```markdown\n(.*?)\n```", text, flags=re.DOTALL)
|
|
59
|
+
if len(matches) != 1:
|
|
60
|
+
return None
|
|
61
|
+
return matches[0]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def extract_vendored_block(path: pathlib.Path) -> str:
|
|
65
|
+
text = path.read_text()
|
|
66
|
+
match = re.search(
|
|
67
|
+
r'SNIPPET = """\\?\n(.*?)"""',
|
|
68
|
+
text,
|
|
69
|
+
flags=re.DOTALL,
|
|
70
|
+
)
|
|
71
|
+
if not match:
|
|
72
|
+
fail(f"could not find `SNIPPET = \"\"\"` in {path.relative_to(ROOT)}")
|
|
73
|
+
raw = match.group(1)
|
|
74
|
+
raw = raw.replace("\\\\", "\\")
|
|
75
|
+
return raw.rstrip("\n")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def sha256(s: str) -> str:
|
|
79
|
+
return hashlib.sha256(s.encode("utf-8")).hexdigest()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def matching_open_pr(vendored: str) -> dict | None:
|
|
83
|
+
try:
|
|
84
|
+
body = fetch_text(PULLS_URL)
|
|
85
|
+
except urllib.error.HTTPError:
|
|
86
|
+
return None
|
|
87
|
+
prs = json.loads(body)
|
|
88
|
+
for pr in prs:
|
|
89
|
+
ref = pr.get("head", {}).get("ref")
|
|
90
|
+
if not ref:
|
|
91
|
+
continue
|
|
92
|
+
block = fetch_block_at_ref(ref)
|
|
93
|
+
if block is not None and block == vendored:
|
|
94
|
+
return pr
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main() -> None:
|
|
99
|
+
upstream = fetch_block_at_ref("main")
|
|
100
|
+
if upstream is None:
|
|
101
|
+
fail("could not extract a single ```markdown block from upstream main README")
|
|
102
|
+
vendored = extract_vendored_block(VENDORED)
|
|
103
|
+
if upstream == vendored:
|
|
104
|
+
print(f"snippet-drift: in sync with main (sha256 {sha256(upstream)[:12]}…)")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
pr = matching_open_pr(vendored)
|
|
108
|
+
if pr is not None:
|
|
109
|
+
print(
|
|
110
|
+
"snippet-drift: WARNING — main is out of sync with the vendored copy, "
|
|
111
|
+
f"but open upstream PR #{pr['number']} ({pr['title']!r}) matches it "
|
|
112
|
+
f"(sha256 {sha256(vendored)[:12]}…).\n"
|
|
113
|
+
" This is the cross-repo content-lock window. Re-run after that PR "
|
|
114
|
+
"merges to confirm."
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
u = sha256(upstream)
|
|
119
|
+
v = sha256(vendored)
|
|
120
|
+
diff_pos = next(
|
|
121
|
+
(i for i, (a, b) in enumerate(zip(upstream, vendored)) if a != b),
|
|
122
|
+
min(len(upstream), len(vendored)),
|
|
123
|
+
)
|
|
124
|
+
fail(
|
|
125
|
+
"vendored snippet has drifted from the upstream block; no open PR "
|
|
126
|
+
"carries a matching block either.\n"
|
|
127
|
+
f" upstream main sha256: {u}\n"
|
|
128
|
+
f" vendored sha256: {v}\n"
|
|
129
|
+
f" first byte diff at offset {diff_pos}:\n"
|
|
130
|
+
f" upstream: {upstream[max(0, diff_pos - 16): diff_pos + 64]!r}\n"
|
|
131
|
+
f" vendored: {vendored[max(0, diff_pos - 16): diff_pos + 64]!r}\n"
|
|
132
|
+
"Re-vendor src/fixyourdocs/snippet.py from "
|
|
133
|
+
"https://github.com/fixyourdocs/agents-md-snippet (README block)."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: snippet-drift
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "17 6 * * *" # daily, so an upstream-only edit also turns this red.
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
check:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- name: Compare vendored snippet to upstream
|
|
22
|
+
run: python3 .github/scripts/check_snippet_drift.py
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Dogfood — this is the same canonical block this SDK's `fixyourdocs init`
|
|
4
|
+
writes into your repo. The block lives at
|
|
5
|
+
[fixyourdocs/agents-md-snippet](https://github.com/fixyourdocs/agents-md-snippet);
|
|
6
|
+
this copy is SHA-pinned to it (see `.github/workflows/snippet-drift.yml`).
|
|
7
|
+
|
|
8
|
+
## Documentation feedback
|
|
9
|
+
|
|
10
|
+
If you find documentation in this repository that is broken, incorrect,
|
|
11
|
+
outdated, or incomplete while performing a task, file a structured report
|
|
12
|
+
via the Docs Feedback Protocol. This helps the maintainers fix the docs
|
|
13
|
+
for the next agent (or human) that hits the same issue.
|
|
14
|
+
|
|
15
|
+
How to file a report:
|
|
16
|
+
|
|
17
|
+
curl -X POST https://hub.fixyourdocs.io/v1/reports \
|
|
18
|
+
-H 'Content-Type: application/json' \
|
|
19
|
+
-d '{
|
|
20
|
+
"protocol_version": "0",
|
|
21
|
+
"doc_url": "<URL or path of the doc you were reading>",
|
|
22
|
+
"agent": { "name": "<your agent id, e.g. claude-code>" },
|
|
23
|
+
"report": {
|
|
24
|
+
"kind": "<broken | incorrect | outdated | missing | unclear | other>",
|
|
25
|
+
"summary": "<one-line description of the problem>",
|
|
26
|
+
"details": "<what you expected vs. what the doc said (optional)>"
|
|
27
|
+
}
|
|
28
|
+
}'
|
|
29
|
+
|
|
30
|
+
Spec: https://docsfeedback.org
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.2.1 — 2026-06-03
|
|
4
|
+
|
|
5
|
+
- Publish the dropped `View:` CLI line: `fixyourdocs report` no longer
|
|
6
|
+
prints a `View: https://hub.fixyourdocs.io/r/<id>` link (that route
|
|
7
|
+
404s; the code change merged in 0.2.0 but was never released). The Hub
|
|
8
|
+
also retired the public `GET /v1/reports/{id}` read endpoint — the SDK
|
|
9
|
+
is POST-only (`Client.send()`), so this is a release-only change with
|
|
10
|
+
no client API difference.
|
|
11
|
+
|
|
12
|
+
## v0.2.0 — 2026-05-26
|
|
13
|
+
|
|
14
|
+
- Add `fixyourdocs init` CLI (appends canonical snippet from `agents-md-snippet` to `AGENTS.md` / `CLAUDE.md` / `.cursor/rules` / `.github/copilot-instructions.md`; idempotent).
|
|
15
|
+
- Add `fixyourdocs report` CLI (sends a Docs Feedback Protocol v0 report; supports `--json`, exit codes 0/1/2).
|
|
16
|
+
- Embed canonical snippet from `agents-md-snippet`; CI fails on drift.
|
|
17
|
+
|
|
18
|
+
## v0.1.0 — initial release
|
|
19
|
+
|
|
20
|
+
- Typed client for Docs Feedback Protocol v0.
|
|
@@ -36,7 +36,7 @@ This Code of Conduct applies within all community spaces, and also applies when
|
|
|
36
36
|
|
|
37
37
|
## Enforcement
|
|
38
38
|
|
|
39
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@fixyourdocs.
|
|
39
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@fixyourdocs.io. All complaints will be reviewed and investigated promptly and fairly.
|
|
40
40
|
|
|
41
41
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
42
42
|
|
|
@@ -33,4 +33,4 @@ comment on your PR with a one-click sign-off link. See the CLA text at
|
|
|
33
33
|
|
|
34
34
|
By participating you agree to abide by the
|
|
35
35
|
[Code of Conduct](CODE_OF_CONDUCT.md) (Contributor Covenant 2.1).
|
|
36
|
-
Report incidents to <hello@fixyourdocs.
|
|
36
|
+
Report incidents to <hello@fixyourdocs.io>.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fixyourdocs
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Reference Python SDK for the Docs Feedback Protocol
|
|
5
5
|
Project-URL: Homepage, https://docsfeedback.org
|
|
6
6
|
Project-URL: Repository, https://github.com/fixyourdocs/sdk-python
|
|
7
7
|
Project-URL: Specification, https://github.com/fixyourdocs/protocol
|
|
8
|
-
Author-email: FixYourDocs <hello@fixyourdocs.
|
|
8
|
+
Author-email: FixYourDocs <hello@fixyourdocs.io>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: agents,ai,docs,documentation,feedback,protocol
|
|
@@ -37,6 +37,7 @@ Reference Python SDK for the [Docs Feedback Protocol](https://github.com/fixyour
|
|
|
37
37
|
v0. The protocol lets AI agents file structured reports against
|
|
38
38
|
documentation pages when the docs break agent task flows.
|
|
39
39
|
|
|
40
|
+
- Full docs: <https://docs.fixyourdocs.io/sdk/python/>.
|
|
40
41
|
- Spec: <https://docsfeedback.org>.
|
|
41
42
|
- Why this exists: [the FixYourDocs manifesto](https://github.com/fixyourdocs/manifesto/blob/main/MANIFESTO.md).
|
|
42
43
|
|
|
@@ -48,6 +49,32 @@ pip install fixyourdocs
|
|
|
48
49
|
|
|
49
50
|
Requires Python 3.9+.
|
|
50
51
|
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
The package ships a `fixyourdocs` console script covering the two
|
|
55
|
+
one-liners from the [agents-md-snippet](https://github.com/fixyourdocs/agents-md-snippet)
|
|
56
|
+
README:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
# Adds the canonical AGENTS.md block to your repo. Idempotent.
|
|
60
|
+
pipx run fixyourdocs init
|
|
61
|
+
|
|
62
|
+
# Sends a single report to the Hub.
|
|
63
|
+
pipx run fixyourdocs report \
|
|
64
|
+
--doc-url https://example.com/docs/install \
|
|
65
|
+
--summary "Install fails on macOS 14" \
|
|
66
|
+
--agent claude-code \
|
|
67
|
+
--kind broken
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`init` auto-detects `AGENTS.md`, `CLAUDE.md`, `.cursor/rules`, or
|
|
71
|
+
`.github/copilot-instructions.md` and appends to whichever exists
|
|
72
|
+
(falling back to creating `AGENTS.md`). Pass `--file <path>` to override.
|
|
73
|
+
|
|
74
|
+
`report` accepts `--details`, `--suggested-fix`, `--api-url`, `--token`,
|
|
75
|
+
and `--json` for machine-readable output. Exit codes: `0` success,
|
|
76
|
+
`2` user error, `1` transport or server error.
|
|
77
|
+
|
|
51
78
|
## Usage (sync)
|
|
52
79
|
|
|
53
80
|
```python
|
|
@@ -4,6 +4,7 @@ Reference Python SDK for the [Docs Feedback Protocol](https://github.com/fixyour
|
|
|
4
4
|
v0. The protocol lets AI agents file structured reports against
|
|
5
5
|
documentation pages when the docs break agent task flows.
|
|
6
6
|
|
|
7
|
+
- Full docs: <https://docs.fixyourdocs.io/sdk/python/>.
|
|
7
8
|
- Spec: <https://docsfeedback.org>.
|
|
8
9
|
- Why this exists: [the FixYourDocs manifesto](https://github.com/fixyourdocs/manifesto/blob/main/MANIFESTO.md).
|
|
9
10
|
|
|
@@ -15,6 +16,32 @@ pip install fixyourdocs
|
|
|
15
16
|
|
|
16
17
|
Requires Python 3.9+.
|
|
17
18
|
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
The package ships a `fixyourdocs` console script covering the two
|
|
22
|
+
one-liners from the [agents-md-snippet](https://github.com/fixyourdocs/agents-md-snippet)
|
|
23
|
+
README:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
# Adds the canonical AGENTS.md block to your repo. Idempotent.
|
|
27
|
+
pipx run fixyourdocs init
|
|
28
|
+
|
|
29
|
+
# Sends a single report to the Hub.
|
|
30
|
+
pipx run fixyourdocs report \
|
|
31
|
+
--doc-url https://example.com/docs/install \
|
|
32
|
+
--summary "Install fails on macOS 14" \
|
|
33
|
+
--agent claude-code \
|
|
34
|
+
--kind broken
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`init` auto-detects `AGENTS.md`, `CLAUDE.md`, `.cursor/rules`, or
|
|
38
|
+
`.github/copilot-instructions.md` and appends to whichever exists
|
|
39
|
+
(falling back to creating `AGENTS.md`). Pass `--file <path>` to override.
|
|
40
|
+
|
|
41
|
+
`report` accepts `--details`, `--suggested-fix`, `--api-url`, `--token`,
|
|
42
|
+
and `--json` for machine-readable output. Exit codes: `0` success,
|
|
43
|
+
`2` user error, `1` transport or server error.
|
|
44
|
+
|
|
18
45
|
## Usage (sync)
|
|
19
46
|
|
|
20
47
|
```python
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fixyourdocs"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Reference Python SDK for the Docs Feedback Protocol"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
|
-
authors = [{ name = "FixYourDocs", email = "hello@fixyourdocs.
|
|
13
|
+
authors = [{ name = "FixYourDocs", email = "hello@fixyourdocs.io" }]
|
|
14
14
|
keywords = ["docs", "documentation", "agents", "ai", "feedback", "protocol"]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 3 - Alpha",
|
|
@@ -44,6 +44,9 @@ Homepage = "https://docsfeedback.org"
|
|
|
44
44
|
Repository = "https://github.com/fixyourdocs/sdk-python"
|
|
45
45
|
Specification = "https://github.com/fixyourdocs/protocol"
|
|
46
46
|
|
|
47
|
+
[project.scripts]
|
|
48
|
+
fixyourdocs = "fixyourdocs._cli:main"
|
|
49
|
+
|
|
47
50
|
[tool.hatch.build.targets.wheel]
|
|
48
51
|
packages = ["src/fixyourdocs"]
|
|
49
52
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""``fixyourdocs`` CLI — ``init`` + ``report`` subcommands.
|
|
2
|
+
|
|
3
|
+
Exit codes:
|
|
4
|
+
|
|
5
|
+
* ``0`` — success.
|
|
6
|
+
* ``2`` — user / argument error (unknown / missing flag, bad enum).
|
|
7
|
+
* ``1`` — transport or server error.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import IO, Optional
|
|
18
|
+
|
|
19
|
+
from fixyourdocs import __version__
|
|
20
|
+
from fixyourdocs._init import run_init
|
|
21
|
+
from fixyourdocs.client import Client
|
|
22
|
+
from fixyourdocs.errors import FixYourDocsError
|
|
23
|
+
from fixyourdocs.models import Report, ReportKind
|
|
24
|
+
|
|
25
|
+
DEFAULT_API_URL = "https://hub.fixyourdocs.io"
|
|
26
|
+
_REPORT_KINDS = tuple(k.value for k in ReportKind)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
prog="fixyourdocs",
|
|
32
|
+
description=(
|
|
33
|
+
"FixYourDocs CLI — adds the canonical AGENTS.md block to your "
|
|
34
|
+
"repo and sends Docs Feedback Protocol v0 reports."
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-v", "--version", action="version", version=__version__
|
|
39
|
+
)
|
|
40
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
41
|
+
|
|
42
|
+
init_p = sub.add_parser(
|
|
43
|
+
"init",
|
|
44
|
+
help="Append the canonical AGENTS.md block to your repo.",
|
|
45
|
+
)
|
|
46
|
+
init_p.add_argument(
|
|
47
|
+
"--file",
|
|
48
|
+
help=(
|
|
49
|
+
"Explicit target file (skips auto-detection of AGENTS.md / "
|
|
50
|
+
"CLAUDE.md / .cursor/rules / .github/copilot-instructions.md)."
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
init_p.add_argument(
|
|
54
|
+
"--json",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Emit machine-readable JSON instead of plain text.",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
rep = sub.add_parser(
|
|
60
|
+
"report",
|
|
61
|
+
help="Send a Docs Feedback Protocol v0 report to the Hub.",
|
|
62
|
+
)
|
|
63
|
+
rep.add_argument("--doc-url", required=True)
|
|
64
|
+
rep.add_argument("--summary", required=True)
|
|
65
|
+
rep.add_argument("--agent", required=True, dest="agent_name")
|
|
66
|
+
rep.add_argument(
|
|
67
|
+
"--kind",
|
|
68
|
+
choices=_REPORT_KINDS,
|
|
69
|
+
default="other",
|
|
70
|
+
)
|
|
71
|
+
rep.add_argument("--details")
|
|
72
|
+
rep.add_argument("--suggested-fix", dest="suggested_fix")
|
|
73
|
+
rep.add_argument("--api-url", default=DEFAULT_API_URL, dest="api_url")
|
|
74
|
+
rep.add_argument("--token")
|
|
75
|
+
rep.add_argument(
|
|
76
|
+
"--json",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Emit machine-readable JSON instead of plain text.",
|
|
79
|
+
)
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _do_init(args: argparse.Namespace, stdout: IO[str]) -> int:
|
|
84
|
+
result = run_init(cwd=Path.cwd(), file=args.file)
|
|
85
|
+
if args.json:
|
|
86
|
+
stdout.write(
|
|
87
|
+
json.dumps(
|
|
88
|
+
{
|
|
89
|
+
"path": str(result.path),
|
|
90
|
+
"changed": result.changed,
|
|
91
|
+
"created": result.created,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
+ "\n"
|
|
95
|
+
)
|
|
96
|
+
elif result.created:
|
|
97
|
+
stdout.write(f"Created {result.path} with the FixYourDocs snippet.\n")
|
|
98
|
+
elif result.changed:
|
|
99
|
+
stdout.write(
|
|
100
|
+
f"Appended the FixYourDocs snippet to {result.path}.\n"
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
stdout.write(
|
|
104
|
+
f"No changes — snippet already present in {result.path}.\n"
|
|
105
|
+
)
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _do_report(
|
|
110
|
+
args: argparse.Namespace, stdout: IO[str], stderr: IO[str]
|
|
111
|
+
) -> int:
|
|
112
|
+
try:
|
|
113
|
+
report = Report.create(
|
|
114
|
+
doc_url=args.doc_url,
|
|
115
|
+
summary=args.summary,
|
|
116
|
+
kind=args.kind,
|
|
117
|
+
agent_name=args.agent_name,
|
|
118
|
+
details=args.details,
|
|
119
|
+
suggested_fix=args.suggested_fix,
|
|
120
|
+
)
|
|
121
|
+
except (ValueError, FixYourDocsError) as err:
|
|
122
|
+
stderr.write(f"error: {err}\n")
|
|
123
|
+
return 2
|
|
124
|
+
|
|
125
|
+
api_url = args.api_url.rstrip("/")
|
|
126
|
+
with Client(api_url, token=args.token) as client:
|
|
127
|
+
try:
|
|
128
|
+
result = client.send(report)
|
|
129
|
+
except FixYourDocsError as err:
|
|
130
|
+
if args.json:
|
|
131
|
+
stdout.write(
|
|
132
|
+
json.dumps(
|
|
133
|
+
{"error": type(err).__name__, "message": str(err)}
|
|
134
|
+
)
|
|
135
|
+
+ "\n"
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
stderr.write(f"error: {err}\n")
|
|
139
|
+
return 1
|
|
140
|
+
|
|
141
|
+
if args.json:
|
|
142
|
+
stdout.write(
|
|
143
|
+
json.dumps(
|
|
144
|
+
{
|
|
145
|
+
"id": result.id,
|
|
146
|
+
"received_at": result.received_at.isoformat(),
|
|
147
|
+
"is_duplicate": result.is_duplicate,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
+ "\n"
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
label = "Duplicate report" if result.is_duplicate else "Report accepted"
|
|
154
|
+
stdout.write(f"{label}: {result.id}\n")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def run_cli(
|
|
159
|
+
argv: Optional[Sequence[str]] = None,
|
|
160
|
+
*,
|
|
161
|
+
stdout: Optional[IO[str]] = None,
|
|
162
|
+
stderr: Optional[IO[str]] = None,
|
|
163
|
+
) -> int:
|
|
164
|
+
"""Entry point used by both the console_script and the tests.
|
|
165
|
+
|
|
166
|
+
Returns the desired process exit code; never calls ``sys.exit``
|
|
167
|
+
itself.
|
|
168
|
+
"""
|
|
169
|
+
out = stdout if stdout is not None else sys.stdout
|
|
170
|
+
err = stderr if stderr is not None else sys.stderr
|
|
171
|
+
|
|
172
|
+
parser = _build_parser()
|
|
173
|
+
# argparse calls sys.exit on errors; intercept to map to our exit codes.
|
|
174
|
+
try:
|
|
175
|
+
args = parser.parse_args(argv)
|
|
176
|
+
except SystemExit as exc:
|
|
177
|
+
return 0 if exc.code == 0 else 2
|
|
178
|
+
|
|
179
|
+
if args.command is None:
|
|
180
|
+
parser.print_help(out)
|
|
181
|
+
return 0
|
|
182
|
+
if args.command == "init":
|
|
183
|
+
return _do_init(args, out)
|
|
184
|
+
if args.command == "report":
|
|
185
|
+
return _do_report(args, out, err)
|
|
186
|
+
err.write(f"error: unknown command {args.command!r}\n")
|
|
187
|
+
return 2
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main() -> None: # console_script entry point
|
|
191
|
+
sys.exit(run_cli(sys.argv[1:]))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""``fixyourdocs init`` — append the canonical AGENTS.md block."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from fixyourdocs.snippet import SNIPPET, SNIPPET_HEADING
|
|
10
|
+
|
|
11
|
+
TARGET_CANDIDATES = (
|
|
12
|
+
"AGENTS.md",
|
|
13
|
+
"CLAUDE.md",
|
|
14
|
+
".cursor/rules",
|
|
15
|
+
".github/copilot-instructions.md",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class InitResult:
|
|
21
|
+
path: Path
|
|
22
|
+
changed: bool
|
|
23
|
+
created: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pick_default_target(cwd: Path) -> str:
|
|
27
|
+
for candidate in TARGET_CANDIDATES:
|
|
28
|
+
if (cwd / candidate).exists():
|
|
29
|
+
return candidate
|
|
30
|
+
return "AGENTS.md"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _with_trailing_blank_line(s: str) -> str:
|
|
34
|
+
if not s:
|
|
35
|
+
return ""
|
|
36
|
+
if s.endswith("\n\n"):
|
|
37
|
+
return s
|
|
38
|
+
if s.endswith("\n"):
|
|
39
|
+
return s + "\n"
|
|
40
|
+
return s + "\n\n"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_init(cwd: Path, file: Optional[str] = None) -> InitResult:
|
|
44
|
+
"""Append the canonical AGENTS.md block to the chosen target file.
|
|
45
|
+
|
|
46
|
+
Idempotent: if the block heading is already present, returns
|
|
47
|
+
``InitResult(changed=False)`` and leaves the file untouched.
|
|
48
|
+
"""
|
|
49
|
+
target_rel = file if file is not None else _pick_default_target(cwd)
|
|
50
|
+
path = (cwd / target_rel).resolve()
|
|
51
|
+
|
|
52
|
+
if not path.exists():
|
|
53
|
+
path.write_text(SNIPPET)
|
|
54
|
+
return InitResult(path=path, changed=True, created=True)
|
|
55
|
+
|
|
56
|
+
current = path.read_text()
|
|
57
|
+
if SNIPPET_HEADING in current:
|
|
58
|
+
return InitResult(path=path, changed=False, created=False)
|
|
59
|
+
|
|
60
|
+
path.write_text(_with_trailing_blank_line(current) + SNIPPET)
|
|
61
|
+
return InitResult(path=path, changed=True, created=False)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Canonical AGENTS.md block.
|
|
2
|
+
|
|
3
|
+
Mirrored from the ``agents-md-snippet`` repo
|
|
4
|
+
(https://github.com/fixyourdocs/agents-md-snippet). The TypeScript SDK
|
|
5
|
+
keeps an identical copy in ``src/snippet.ts``; CI in both repos refuses
|
|
6
|
+
to merge a drift from the upstream code-fenced block.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
SNIPPET_HEADING = "## Documentation feedback"
|
|
12
|
+
|
|
13
|
+
SNIPPET = """\
|
|
14
|
+
## Documentation feedback
|
|
15
|
+
|
|
16
|
+
If you find documentation in this repository that is broken, incorrect,
|
|
17
|
+
outdated, or incomplete while performing a task, file a structured report
|
|
18
|
+
via the Docs Feedback Protocol. This helps the maintainers fix the docs
|
|
19
|
+
for the next agent (or human) that hits the same issue.
|
|
20
|
+
|
|
21
|
+
How to file a report:
|
|
22
|
+
|
|
23
|
+
curl -X POST https://hub.fixyourdocs.io/v1/reports \\
|
|
24
|
+
-H 'Content-Type: application/json' \\
|
|
25
|
+
-d '{
|
|
26
|
+
"protocol_version": "0",
|
|
27
|
+
"doc_url": "<URL or path of the doc you were reading>",
|
|
28
|
+
"agent": { "name": "<your agent id, e.g. claude-code>" },
|
|
29
|
+
"report": {
|
|
30
|
+
"kind": "<broken | incorrect | outdated | missing | unclear | other>",
|
|
31
|
+
"summary": "<one-line description of the problem>",
|
|
32
|
+
"details": "<what you expected vs. what the doc said (optional)>"
|
|
33
|
+
}
|
|
34
|
+
}'
|
|
35
|
+
|
|
36
|
+
Spec: https://docsfeedback.org
|
|
37
|
+
"""
|
|
@@ -86,7 +86,7 @@ async def test_headers_with_token_and_idempotency(golden_report: Report) -> None
|
|
|
86
86
|
assert headers["Authorization"] == "Bearer opaque-token"
|
|
87
87
|
assert headers["Idempotency-Key"] == "key-123"
|
|
88
88
|
assert headers["X-Docs-Feedback-Protocol-Version"] == "0"
|
|
89
|
-
assert headers["User-Agent"] == "fixyourdocs-python/0.1
|
|
89
|
+
assert headers["User-Agent"] == "fixyourdocs-python/0.2.1"
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
@respx.mock
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Tests for ``fixyourdocs._cli.run_cli``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import pytest
|
|
13
|
+
import respx
|
|
14
|
+
|
|
15
|
+
from fixyourdocs._cli import run_cli
|
|
16
|
+
from fixyourdocs.snippet import SNIPPET_HEADING
|
|
17
|
+
|
|
18
|
+
API_URL = "https://hub.example.test"
|
|
19
|
+
REPORTS_URL = f"{API_URL}/v1/reports"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _io() -> tuple[io.StringIO, io.StringIO]:
|
|
23
|
+
return io.StringIO(), io.StringIO()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def cwd(tmp_path: Path) -> Iterator[Path]:
|
|
28
|
+
original = Path.cwd()
|
|
29
|
+
os.chdir(tmp_path)
|
|
30
|
+
try:
|
|
31
|
+
yield tmp_path
|
|
32
|
+
finally:
|
|
33
|
+
os.chdir(original)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_version_prints_package_version() -> None:
|
|
37
|
+
out, err = _io()
|
|
38
|
+
code = run_cli(["--version"], stdout=out, stderr=err)
|
|
39
|
+
# argparse writes --version output to stdout itself; we map its exit
|
|
40
|
+
# to our return code.
|
|
41
|
+
assert code == 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_help_returns_zero() -> None:
|
|
45
|
+
out, err = _io()
|
|
46
|
+
code = run_cli(["--help"], stdout=out, stderr=err)
|
|
47
|
+
assert code == 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_unknown_subcommand_returns_two() -> None:
|
|
51
|
+
out, err = _io()
|
|
52
|
+
code = run_cli(["bogus"], stdout=out, stderr=err)
|
|
53
|
+
assert code == 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_init_creates_agents_md(cwd: Path) -> None:
|
|
57
|
+
out, err = _io()
|
|
58
|
+
code = run_cli(["init"], stdout=out, stderr=err)
|
|
59
|
+
assert code == 0
|
|
60
|
+
target = cwd / "AGENTS.md"
|
|
61
|
+
assert SNIPPET_HEADING in target.read_text()
|
|
62
|
+
assert "Created" in out.getvalue()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_init_json_output(cwd: Path) -> None:
|
|
66
|
+
out, err = _io()
|
|
67
|
+
code = run_cli(["init", "--json"], stdout=out, stderr=err)
|
|
68
|
+
assert code == 0
|
|
69
|
+
parsed = json.loads(out.getvalue())
|
|
70
|
+
assert parsed["created"] is True
|
|
71
|
+
assert parsed["changed"] is True
|
|
72
|
+
assert parsed["path"] == str((cwd / "AGENTS.md").resolve())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_init_idempotent_second_run_reports_no_change(cwd: Path) -> None:
|
|
76
|
+
run_cli(["init"], stdout=io.StringIO(), stderr=io.StringIO())
|
|
77
|
+
out, err = _io()
|
|
78
|
+
code = run_cli(["init"], stdout=out, stderr=err)
|
|
79
|
+
assert code == 0
|
|
80
|
+
assert "No changes" in out.getvalue()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_init_rejects_unknown_flag(cwd: Path) -> None:
|
|
84
|
+
out, err = _io()
|
|
85
|
+
code = run_cli(["init", "--nope"], stdout=out, stderr=err)
|
|
86
|
+
assert code == 2
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@respx.mock
|
|
90
|
+
def test_report_happy_path() -> None:
|
|
91
|
+
captured: dict[str, object] = {}
|
|
92
|
+
|
|
93
|
+
def _ack(request: httpx.Request) -> httpx.Response:
|
|
94
|
+
captured["body"] = json.loads(request.content)
|
|
95
|
+
return httpx.Response(
|
|
96
|
+
201,
|
|
97
|
+
json={
|
|
98
|
+
"id": "rep_cli_py_01",
|
|
99
|
+
"received_at": "2026-06-06T12:34:56Z",
|
|
100
|
+
"protocol_version": "0",
|
|
101
|
+
"server_capabilities": [],
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
respx.post(REPORTS_URL).mock(side_effect=_ack)
|
|
106
|
+
out, err = _io()
|
|
107
|
+
code = run_cli(
|
|
108
|
+
[
|
|
109
|
+
"report",
|
|
110
|
+
"--api-url",
|
|
111
|
+
API_URL,
|
|
112
|
+
"--doc-url",
|
|
113
|
+
"https://example.com/docs/install",
|
|
114
|
+
"--summary",
|
|
115
|
+
"Install fails on macOS 14",
|
|
116
|
+
"--agent",
|
|
117
|
+
"claude-code",
|
|
118
|
+
"--kind",
|
|
119
|
+
"broken",
|
|
120
|
+
],
|
|
121
|
+
stdout=out,
|
|
122
|
+
stderr=err,
|
|
123
|
+
)
|
|
124
|
+
assert code == 0
|
|
125
|
+
assert "rep_cli_py_01" in out.getvalue()
|
|
126
|
+
body = captured["body"]
|
|
127
|
+
assert isinstance(body, dict)
|
|
128
|
+
assert body["protocol_version"] == "0"
|
|
129
|
+
assert body["doc_url"] == "https://example.com/docs/install"
|
|
130
|
+
assert body["agent"]["name"] == "claude-code"
|
|
131
|
+
assert body["report"]["kind"] == "broken"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_report_missing_required_returns_two() -> None:
|
|
135
|
+
out, err = _io()
|
|
136
|
+
code = run_cli(
|
|
137
|
+
["report", "--api-url", API_URL],
|
|
138
|
+
stdout=out,
|
|
139
|
+
stderr=err,
|
|
140
|
+
)
|
|
141
|
+
assert code == 2
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_report_bad_kind_returns_two() -> None:
|
|
145
|
+
out, err = _io()
|
|
146
|
+
code = run_cli(
|
|
147
|
+
[
|
|
148
|
+
"report",
|
|
149
|
+
"--api-url",
|
|
150
|
+
API_URL,
|
|
151
|
+
"--doc-url",
|
|
152
|
+
"https://example.com",
|
|
153
|
+
"--summary",
|
|
154
|
+
"x",
|
|
155
|
+
"--agent",
|
|
156
|
+
"claude-code",
|
|
157
|
+
"--kind",
|
|
158
|
+
"weird",
|
|
159
|
+
],
|
|
160
|
+
stdout=out,
|
|
161
|
+
stderr=err,
|
|
162
|
+
)
|
|
163
|
+
assert code == 2
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@respx.mock
|
|
167
|
+
def test_report_server_error_returns_one() -> None:
|
|
168
|
+
respx.post(REPORTS_URL).mock(
|
|
169
|
+
return_value=httpx.Response(
|
|
170
|
+
422,
|
|
171
|
+
json={"error": "policy_rejected", "reason": "unknown agent.name"},
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
out, err = _io()
|
|
175
|
+
code = run_cli(
|
|
176
|
+
[
|
|
177
|
+
"report",
|
|
178
|
+
"--api-url",
|
|
179
|
+
API_URL,
|
|
180
|
+
"--doc-url",
|
|
181
|
+
"https://example.com",
|
|
182
|
+
"--summary",
|
|
183
|
+
"x",
|
|
184
|
+
"--agent",
|
|
185
|
+
"claude-code",
|
|
186
|
+
],
|
|
187
|
+
stdout=out,
|
|
188
|
+
stderr=err,
|
|
189
|
+
)
|
|
190
|
+
assert code == 1
|
|
191
|
+
assert "policy_rejected" in err.getvalue()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@respx.mock
|
|
195
|
+
def test_report_json_output() -> None:
|
|
196
|
+
respx.post(REPORTS_URL).mock(
|
|
197
|
+
return_value=httpx.Response(
|
|
198
|
+
201,
|
|
199
|
+
json={
|
|
200
|
+
"id": "rep_cli_py_json",
|
|
201
|
+
"received_at": "2026-06-06T12:34:56Z",
|
|
202
|
+
"protocol_version": "0",
|
|
203
|
+
"server_capabilities": [],
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
out, err = _io()
|
|
208
|
+
code = run_cli(
|
|
209
|
+
[
|
|
210
|
+
"report",
|
|
211
|
+
"--api-url",
|
|
212
|
+
API_URL,
|
|
213
|
+
"--doc-url",
|
|
214
|
+
"https://example.com",
|
|
215
|
+
"--summary",
|
|
216
|
+
"x",
|
|
217
|
+
"--agent",
|
|
218
|
+
"claude-code",
|
|
219
|
+
"--json",
|
|
220
|
+
],
|
|
221
|
+
stdout=out,
|
|
222
|
+
stderr=err,
|
|
223
|
+
)
|
|
224
|
+
assert code == 0
|
|
225
|
+
parsed = json.loads(out.getvalue())
|
|
226
|
+
assert parsed["id"] == "rep_cli_py_json"
|
|
@@ -94,7 +94,7 @@ def test_default_headers_no_token(golden_report: Report) -> None:
|
|
|
94
94
|
headers = route.calls.last.request.headers
|
|
95
95
|
assert headers["Content-Type"].startswith("application/json")
|
|
96
96
|
assert headers["X-Docs-Feedback-Protocol-Version"] == "0"
|
|
97
|
-
assert headers["User-Agent"] == "fixyourdocs-python/0.1
|
|
97
|
+
assert headers["User-Agent"] == "fixyourdocs-python/0.2.1"
|
|
98
98
|
assert "Authorization" not in headers
|
|
99
99
|
assert "Idempotency-Key" not in headers
|
|
100
100
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tests for ``fixyourdocs._init.run_init``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fixyourdocs._init import run_init
|
|
8
|
+
from fixyourdocs.snippet import SNIPPET, SNIPPET_HEADING
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_creates_agents_md_when_nothing_exists(tmp_path: Path) -> None:
|
|
12
|
+
result = run_init(tmp_path)
|
|
13
|
+
assert result.created is True
|
|
14
|
+
assert result.changed is True
|
|
15
|
+
assert result.path == (tmp_path / "AGENTS.md").resolve()
|
|
16
|
+
assert (tmp_path / "AGENTS.md").read_text() == SNIPPET
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_appends_to_existing_agents_md(tmp_path: Path) -> None:
|
|
20
|
+
target = tmp_path / "AGENTS.md"
|
|
21
|
+
target.write_text("# My repo\n\nDo things.")
|
|
22
|
+
result = run_init(tmp_path)
|
|
23
|
+
assert result.created is False
|
|
24
|
+
assert result.changed is True
|
|
25
|
+
after = target.read_text()
|
|
26
|
+
assert after.startswith("# My repo\n\nDo things.")
|
|
27
|
+
assert SNIPPET_HEADING in after
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_idempotent_on_second_run(tmp_path: Path) -> None:
|
|
31
|
+
run_init(tmp_path)
|
|
32
|
+
first = (tmp_path / "AGENTS.md").read_text()
|
|
33
|
+
result = run_init(tmp_path)
|
|
34
|
+
assert result.changed is False
|
|
35
|
+
assert (tmp_path / "AGENTS.md").read_text() == first
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_prefers_claude_md_when_only_claude_exists(tmp_path: Path) -> None:
|
|
39
|
+
target = tmp_path / "CLAUDE.md"
|
|
40
|
+
target.write_text("# Claude rules\n")
|
|
41
|
+
result = run_init(tmp_path)
|
|
42
|
+
assert result.path == target.resolve()
|
|
43
|
+
assert SNIPPET_HEADING in target.read_text()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_prefers_agents_md_when_both_present(tmp_path: Path) -> None:
|
|
47
|
+
(tmp_path / "AGENTS.md").write_text("# Agents\n")
|
|
48
|
+
(tmp_path / "CLAUDE.md").write_text("# Claude\n")
|
|
49
|
+
result = run_init(tmp_path)
|
|
50
|
+
assert result.path == (tmp_path / "AGENTS.md").resolve()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_explicit_file_override(tmp_path: Path) -> None:
|
|
54
|
+
result = run_init(tmp_path, file="INSTRUCTIONS.md")
|
|
55
|
+
target = tmp_path / "INSTRUCTIONS.md"
|
|
56
|
+
assert result.path == target.resolve()
|
|
57
|
+
assert result.created is True
|
|
58
|
+
assert target.read_text() == SNIPPET
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|