boxprobe-scout 0.1.3__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 (78) hide show
  1. boxprobe_scout-0.1.3/.forgejo/workflows/publish.yml +31 -0
  2. boxprobe_scout-0.1.3/.forgejo/workflows/test.yml +22 -0
  3. boxprobe_scout-0.1.3/.github/ISSUE_TEMPLATE/bug_report.yml +74 -0
  4. boxprobe_scout-0.1.3/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. boxprobe_scout-0.1.3/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  6. boxprobe_scout-0.1.3/.github/PULL_REQUEST_TEMPLATE.md +29 -0
  7. boxprobe_scout-0.1.3/.github/workflows/ci.yml +85 -0
  8. boxprobe_scout-0.1.3/.github/workflows/release.yml +101 -0
  9. boxprobe_scout-0.1.3/.gitignore +12 -0
  10. boxprobe_scout-0.1.3/AGENTS.md +7 -0
  11. boxprobe_scout-0.1.3/CHANGELOG.md +34 -0
  12. boxprobe_scout-0.1.3/CLAUDE.md +181 -0
  13. boxprobe_scout-0.1.3/CODE_OF_CONDUCT.md +85 -0
  14. boxprobe_scout-0.1.3/CONTRIBUTING.md +109 -0
  15. boxprobe_scout-0.1.3/LICENSE +21 -0
  16. boxprobe_scout-0.1.3/PKG-INFO +258 -0
  17. boxprobe_scout-0.1.3/README.md +202 -0
  18. boxprobe_scout-0.1.3/RELEASING.md +113 -0
  19. boxprobe_scout-0.1.3/pyproject.toml +134 -0
  20. boxprobe_scout-0.1.3/scout/__init__.py +1 -0
  21. boxprobe_scout-0.1.3/scout/bridge/__init__.py +0 -0
  22. boxprobe_scout-0.1.3/scout/cli.py +783 -0
  23. boxprobe_scout-0.1.3/scout/collector/__init__.py +0 -0
  24. boxprobe_scout-0.1.3/scout/collector/control.py +114 -0
  25. boxprobe_scout-0.1.3/scout/collector/db.py +187 -0
  26. boxprobe_scout-0.1.3/scout/collector/proxy.py +80 -0
  27. boxprobe_scout-0.1.3/scout/collector/subprocess.py +123 -0
  28. boxprobe_scout-0.1.3/scout/config.py +58 -0
  29. boxprobe_scout-0.1.3/scout/git.py +40 -0
  30. boxprobe_scout-0.1.3/scout/index.py +134 -0
  31. boxprobe_scout-0.1.3/scout/matcher/__init__.py +0 -0
  32. boxprobe_scout-0.1.3/scout/matcher/align.py +145 -0
  33. boxprobe_scout-0.1.3/scout/matcher/compare.py +360 -0
  34. boxprobe_scout-0.1.3/scout/matcher/diff_db.py +264 -0
  35. boxprobe_scout-0.1.3/scout/matcher/diff_report.py +1382 -0
  36. boxprobe_scout-0.1.3/scout/matcher/noise.py +506 -0
  37. boxprobe_scout-0.1.3/scout/matcher/normalize.py +160 -0
  38. boxprobe_scout-0.1.3/scout/mcp/__init__.py +0 -0
  39. boxprobe_scout-0.1.3/scout/mock_vars.py +150 -0
  40. boxprobe_scout-0.1.3/scout/report/__init__.py +0 -0
  41. boxprobe_scout-0.1.3/scout/report/html.py +77 -0
  42. boxprobe_scout-0.1.3/scout/report/junit.py +55 -0
  43. boxprobe_scout-0.1.3/scout/report/verify_html.py +303 -0
  44. boxprobe_scout-0.1.3/scout/run_metadata.py +62 -0
  45. boxprobe_scout-0.1.3/scout/runner/__init__.py +13 -0
  46. boxprobe_scout-0.1.3/scout/runner/executor.py +296 -0
  47. boxprobe_scout-0.1.3/scout/runner/locator.py +383 -0
  48. boxprobe_scout-0.1.3/scout/runner/page.py +178 -0
  49. boxprobe_scout-0.1.3/scout/runner/scenario.py +58 -0
  50. boxprobe_scout-0.1.3/scout/secrets/__init__.py +0 -0
  51. boxprobe_scout-0.1.3/scout/server/__init__.py +0 -0
  52. boxprobe_scout-0.1.3/tests/__init__.py +0 -0
  53. boxprobe_scout-0.1.3/tests/collector/__init__.py +0 -0
  54. boxprobe_scout-0.1.3/tests/collector/test_control.py +89 -0
  55. boxprobe_scout-0.1.3/tests/collector/test_db.py +64 -0
  56. boxprobe_scout-0.1.3/tests/matcher/__init__.py +0 -0
  57. boxprobe_scout-0.1.3/tests/matcher/test_align.py +230 -0
  58. boxprobe_scout-0.1.3/tests/matcher/test_compare.py +94 -0
  59. boxprobe_scout-0.1.3/tests/matcher/test_diff_db.py +103 -0
  60. boxprobe_scout-0.1.3/tests/matcher/test_diff_report.py +86 -0
  61. boxprobe_scout-0.1.3/tests/matcher/test_noise.py +650 -0
  62. boxprobe_scout-0.1.3/tests/matcher/test_normalize.py +218 -0
  63. boxprobe_scout-0.1.3/tests/report/__init__.py +0 -0
  64. boxprobe_scout-0.1.3/tests/report/test_html.py +29 -0
  65. boxprobe_scout-0.1.3/tests/report/test_junit.py +37 -0
  66. boxprobe_scout-0.1.3/tests/runner/__init__.py +0 -0
  67. boxprobe_scout-0.1.3/tests/runner/test_batch.py +132 -0
  68. boxprobe_scout-0.1.3/tests/runner/test_executor.py +70 -0
  69. boxprobe_scout-0.1.3/tests/runner/test_executor_hooks.py +46 -0
  70. boxprobe_scout-0.1.3/tests/runner/test_locator.py +130 -0
  71. boxprobe_scout-0.1.3/tests/runner/test_page.py +89 -0
  72. boxprobe_scout-0.1.3/tests/runner/test_scenario.py +43 -0
  73. boxprobe_scout-0.1.3/tests/test_cli.py +70 -0
  74. boxprobe_scout-0.1.3/tests/test_config.py +53 -0
  75. boxprobe_scout-0.1.3/tests/test_git.py +71 -0
  76. boxprobe_scout-0.1.3/tests/test_index.py +134 -0
  77. boxprobe_scout-0.1.3/tests/test_run_metadata.py +128 -0
  78. boxprobe_scout-0.1.3/uv.lock +3738 -0
@@ -0,0 +1,31 @@
1
+ name: Publish Scout
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "pyproject.toml"
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Build and publish to Forgejo Package Registry
14
+ run: |
15
+ FORGEJO_TOKEN=$(grep -E '^FORGEJO_TOKEN=' /takumi/qa/.env.qa | cut -d= -f2-)
16
+ cd /takumi/qa/scout || { git clone "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" /takumi/qa/scout && cd /takumi/qa/scout; }
17
+ git fetch "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" main
18
+ git reset --hard FETCH_HEAD
19
+
20
+ # Build
21
+ UV_URL="https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-musl.tar.gz"
22
+ wget -qO- "$UV_URL" | tar xz -C /usr/local/bin --strip-components=1
23
+ rm -rf dist/
24
+ uv build
25
+
26
+ # Publish
27
+ uv publish \
28
+ --publish-url http://forgejo:3000/api/packages/core/pypi \
29
+ --username __token__ \
30
+ --password "${FORGEJO_TOKEN}" \
31
+ dist/*
@@ -0,0 +1,22 @@
1
+ name: Test
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ env:
11
+ UV_LINK_MODE: copy
12
+ steps:
13
+ - name: Checkout and run tests
14
+ run: |
15
+ UV_URL="https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-musl.tar.gz"
16
+ wget -qO- "$UV_URL" | tar xz -C /usr/local/bin --strip-components=1
17
+ FORGEJO_TOKEN=$(grep -E '^FORGEJO_TOKEN=' /takumi/qa/.env.qa | cut -d= -f2-)
18
+ cd /takumi/qa/scout || { git clone "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" /takumi/qa/scout && cd /takumi/qa/scout; }
19
+ git fetch "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" main
20
+ git reset --hard FETCH_HEAD
21
+ uv sync --extra dev
22
+ uv run pytest tests/ -x --tb=short -m "not e2e"
@@ -0,0 +1,74 @@
1
+ name: Bug report
2
+ description: Report something that doesn't work as expected
3
+ labels: ["bug", "triage"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for filing a bug. Please complete every field — incomplete
9
+ reports usually take a round-trip to triage.
10
+
11
+ - type: input
12
+ id: scout-version
13
+ attributes:
14
+ label: scout version
15
+ description: Output of `scout --version` or `pip show boxprobe-scout`
16
+ placeholder: "0.1.3"
17
+ validations:
18
+ required: true
19
+
20
+ - type: input
21
+ id: python-version
22
+ attributes:
23
+ label: Python version
24
+ description: Output of `python --version`
25
+ placeholder: "3.12.7"
26
+ validations:
27
+ required: true
28
+
29
+ - type: dropdown
30
+ id: os
31
+ attributes:
32
+ label: Operating system
33
+ options:
34
+ - Linux
35
+ - macOS
36
+ - Windows
37
+ - Other (describe in reproduction)
38
+ validations:
39
+ required: true
40
+
41
+ - type: textarea
42
+ id: reproduction
43
+ attributes:
44
+ label: Reproduction
45
+ description: |
46
+ Minimal scenario file or CLI invocation that triggers the issue.
47
+ Trim to the smallest case that still fails.
48
+ render: shell
49
+ validations:
50
+ required: true
51
+
52
+ - type: textarea
53
+ id: expected
54
+ attributes:
55
+ label: What you expected
56
+ validations:
57
+ required: true
58
+
59
+ - type: textarea
60
+ id: actual
61
+ attributes:
62
+ label: What actually happened
63
+ description: Include the full traceback if there is one.
64
+ render: shell
65
+ validations:
66
+ required: true
67
+
68
+ - type: textarea
69
+ id: extra
70
+ attributes:
71
+ label: Anything else
72
+ description: |
73
+ Logs, screenshots, `result.json` snippets, diff report HTML — whatever
74
+ helps narrow it down.
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Question / general discussion
4
+ url: https://github.com/boxprobe/scout/discussions
5
+ about: For questions about usage, design decisions, or open-ended discussion — please use Discussions instead of Issues.
6
+ - name: Security issues
7
+ url: mailto:contact@boxprobe.com
8
+ about: Please report security issues privately by email, not via public Issues.
@@ -0,0 +1,42 @@
1
+ name: Feature request
2
+ description: Suggest a new capability or behavior change
3
+ labels: ["enhancement", "triage"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ scout's scope is deliberately narrow: UI-driven API regression
9
+ testing. Features that broaden the scope need a strong case. Please
10
+ explain the use case before proposing a solution.
11
+
12
+ - type: textarea
13
+ id: problem
14
+ attributes:
15
+ label: Problem
16
+ description: What can't you do today, or what's painful?
17
+ validations:
18
+ required: true
19
+
20
+ - type: textarea
21
+ id: proposal
22
+ attributes:
23
+ label: Proposed solution
24
+ description: |
25
+ How would you want this to work? Concrete API sketches or CLI
26
+ examples are more useful than abstract descriptions.
27
+ validations:
28
+ required: true
29
+
30
+ - type: textarea
31
+ id: alternatives
32
+ attributes:
33
+ label: Alternatives considered
34
+ description: Workarounds you've tried, or other tools that solve this.
35
+
36
+ - type: checkboxes
37
+ id: scope
38
+ attributes:
39
+ label: Scope check
40
+ options:
41
+ - label: This is about UI-driven API regression testing (not a generic test runner / load tester / contract testing feature)
42
+ required: true
@@ -0,0 +1,29 @@
1
+ <!--
2
+ Thanks for the PR! A few quick checks before submitting:
3
+ -->
4
+
5
+ ## What this changes
6
+
7
+ <!-- One-paragraph summary of the change and its motivation. -->
8
+
9
+ ## Type
10
+
11
+ - [ ] Bug fix
12
+ - [ ] New feature
13
+ - [ ] Refactor (no behavior change)
14
+ - [ ] Docs / formatting
15
+ - [ ] Test only
16
+
17
+ ## Checklist
18
+
19
+ - [ ] Linked issue (or this is a trivial doc/typo PR)
20
+ - [ ] Tests added or updated for behavior changes
21
+ - [ ] `uv run pytest tests/ -x --tb=short -m "not e2e"` passes locally
22
+ - [ ] `uv run ruff check scout/ tests/` passes
23
+ - [ ] `uv run pyright scout/` passes
24
+ - [ ] Externally-observable surfaces flagged (scenario file format, diff report HTML, CLI flags)
25
+
26
+ ## Notes for reviewers
27
+
28
+ <!-- Anything reviewers should know: tricky parts, why a specific approach
29
+ was chosen, follow-ups deliberately deferred. -->
@@ -0,0 +1,85 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ lint:
14
+ name: Lint and type-check
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
+ with:
22
+ enable-cache: true
23
+
24
+ - name: Set up Python
25
+ run: uv python install 3.14
26
+
27
+ - name: Sync dependencies
28
+ run: uv sync --extra dev
29
+
30
+ - name: ruff check
31
+ run: uv run ruff check scout/ tests/
32
+
33
+ - name: ruff format check
34
+ run: uv run ruff format --check scout/ tests/
35
+
36
+ - name: pyright
37
+ run: uv run pyright scout/
38
+
39
+ test:
40
+ name: Test on Python ${{ matrix.python }}
41
+ runs-on: ubuntu-latest
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ python: ["3.11", "3.12", "3.13", "3.14"]
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+
49
+ - name: Install uv
50
+ uses: astral-sh/setup-uv@v6
51
+ with:
52
+ enable-cache: true
53
+
54
+ - name: Set up Python ${{ matrix.python }}
55
+ run: uv python install ${{ matrix.python }}
56
+
57
+ - name: Sync dependencies
58
+ run: uv sync --extra dev --python ${{ matrix.python }}
59
+
60
+ - name: Run tests (no e2e)
61
+ run: uv run --python ${{ matrix.python }} pytest tests/ -x --tb=short -m "not e2e"
62
+
63
+ build:
64
+ name: Build wheel and sdist
65
+ runs-on: ubuntu-latest
66
+ needs: [lint, test]
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+
70
+ - name: Install uv
71
+ uses: astral-sh/setup-uv@v6
72
+
73
+ - name: Build
74
+ run: uv build
75
+
76
+ - name: Inspect wheel metadata
77
+ run: |
78
+ unzip -p dist/boxprobe_scout-*.whl '*/METADATA' | head -30
79
+
80
+ - name: Upload artifacts
81
+ uses: actions/upload-artifact@v4
82
+ with:
83
+ name: dist
84
+ path: dist/
85
+ retention-days: 14
@@ -0,0 +1,101 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build sdist and wheel
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v6
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Set up Python
21
+ run: uv python install 3.14
22
+
23
+ - name: Build distributions
24
+ run: uv build
25
+
26
+ - name: Verify wheel metadata version matches tag
27
+ run: |
28
+ tag="${GITHUB_REF_NAME}"
29
+ expected="${tag#v}"
30
+ actual=$(unzip -p dist/boxprobe_scout-*.whl '*/METADATA' | awk '/^Version:/ {print $2; exit}')
31
+ if [ "$actual" != "$expected" ]; then
32
+ echo "Tag $tag implies version $expected, but wheel built version $actual"
33
+ exit 1
34
+ fi
35
+ echo "Tag and wheel agree on version $actual"
36
+
37
+ - name: Upload distributions
38
+ uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ publish-pypi:
44
+ name: Publish to PyPI
45
+ needs: [build]
46
+ runs-on: ubuntu-latest
47
+ environment:
48
+ name: pypi
49
+ url: https://pypi.org/p/boxprobe-scout
50
+ permissions:
51
+ id-token: write
52
+ steps:
53
+ - name: Download distributions
54
+ uses: actions/download-artifact@v4
55
+ with:
56
+ name: dist
57
+ path: dist/
58
+
59
+ - name: Publish to PyPI
60
+ uses: pypa/gh-action-pypi-publish@release/v1
61
+
62
+ github-release:
63
+ name: Create GitHub Release
64
+ needs: [publish-pypi]
65
+ runs-on: ubuntu-latest
66
+ permissions:
67
+ contents: write
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ with:
71
+ fetch-depth: 0
72
+
73
+ - name: Download distributions
74
+ uses: actions/download-artifact@v4
75
+ with:
76
+ name: dist
77
+ path: dist/
78
+
79
+ - name: Extract changelog section
80
+ id: changelog
81
+ run: |
82
+ version="${GITHUB_REF_NAME#v}"
83
+ # Pull lines between [version] header and the next ## header
84
+ awk -v v="$version" '
85
+ $0 ~ "^## \\["v"\\]" {flag=1; next}
86
+ flag && /^## \[/ {exit}
87
+ flag {print}
88
+ ' CHANGELOG.md > /tmp/release-notes.md
89
+ if [ ! -s /tmp/release-notes.md ]; then
90
+ echo "No changelog entry for $version" > /tmp/release-notes.md
91
+ fi
92
+ echo "notes_path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
93
+
94
+ - name: Create release
95
+ env:
96
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97
+ run: |
98
+ gh release create "$GITHUB_REF_NAME" \
99
+ --title "$GITHUB_REF_NAME" \
100
+ --notes-file "${{ steps.changelog.outputs.notes_path }}" \
101
+ dist/*
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ .python-version
3
+ .ruff_cache/
4
+ __pycache__/
5
+ *.pyc
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .pytest_cache/
10
+ htmlcov/
11
+ *.sqlite
12
+ .scout/
@@ -0,0 +1,7 @@
1
+ # Agent Constraints
2
+
3
+ 1. **Run tests before committing**: `uv run pytest tests/ -x --tb=short`
4
+ 2. **Don't touch unrelated code** — scope changes to the task at hand
5
+ 3. **Conventional commits** — `feat:`, `fix:`, `test:`, `refactor:`, `docs:`
6
+ 4. **Don't add dependencies** without explicit approval
7
+ 5. **All Python commands via `uv run`** — never bare `python` or `pip`
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to scout are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ once it reaches `1.0.0`. While in `0.x`, breaking changes may occur between
8
+ minor versions; patch versions remain backward-compatible bug fixes.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.3] - 2026-05-16
13
+
14
+ Initial public release on PyPI. Prior `0.1.x` versions were distributed
15
+ internally via a private package registry; this is the first cut intended
16
+ for external consumers.
17
+
18
+ ### Features at this version
19
+
20
+ - `scout run` — execute pre-recorded Python scenarios via Playwright,
21
+ capture API traffic through a recording proxy
22
+ - `scout verify` — debug-mode scenario execution with screenshots, no proxy
23
+ - `scout diff` — compare two runs' API recordings, produce HTML diff report
24
+ with structural, value, and known-change classification
25
+ - `scout runs` — list local run history
26
+ - Pixel-anchored `Locator` API with `abs` / `rel` / `dxy` positioning
27
+ - `diff_ignore.json` rule format for field, value-type, endpoint, and
28
+ status-only noise suppression
29
+ - HTML diff report with filterable endpoint table, popup body diffs, and
30
+ known-change badges
31
+ - JUnit XML report alongside HTML for CI integration
32
+
33
+ [Unreleased]: https://github.com/boxprobe/scout/compare/v0.1.3...HEAD
34
+ [0.1.3]: https://github.com/boxprobe/scout/releases/tag/v0.1.3
@@ -0,0 +1,181 @@
1
+ # scout
2
+
3
+ UI-driven API regression testing — a CLI that executes pre-recorded
4
+ scenarios, captures API traffic via a proxy, and produces cross-version
5
+ diff reports.
6
+
7
+ **Install:**
8
+
9
+ ```bash
10
+ pip install boxprobe-scout
11
+ playwright install chromium
12
+ ```
13
+
14
+ ## Code structure
15
+
16
+ ```
17
+ scout/
18
+ ├── cli.py # Click CLI entry
19
+ │ # scout run — record API traffic during execution
20
+ │ # scout verify — screenshot mode for scenario debugging
21
+ │ # scout diff — compare two runs' API recordings
22
+ │ # scout runs — list run history
23
+ ├── config.py # app.json loader + URL overrides
24
+ ├── git.py # git commit/branch lookup for run metadata
25
+ ├── index.py # local run index (SQLite, .scout/index.db)
26
+ ├── run_metadata.py # run metadata assembly
27
+ ├── runner/
28
+ │ ├── executor.py # Playwright orchestration, batch execution
29
+ │ ├── scenario.py # Scenario DSL (base_url, @setup, @test)
30
+ │ ├── page.py # Page wrapper (goto/click/fill/wait via Locator)
31
+ │ └── locator.py # Pixel-anchored Locator with abs/rel/dxy positioning
32
+ ├── collector/
33
+ │ ├── subprocess.py # recording proxy subprocess manager
34
+ │ ├── proxy.py # mitmproxy addon
35
+ │ ├── control.py # proxy control API (session start/stop)
36
+ │ └── db.py # recording DB (SQLite: scenarios + api_records)
37
+ ├── matcher/
38
+ │ ├── align.py # endpoint pairing (path-only + 2-stage query match)
39
+ │ ├── compare.py # JSON structure + value comparison
40
+ │ ├── normalize.py # URL path normalization (dynamic ID inference)
41
+ │ ├── noise.py # diff_ignore.json rules + known-change suppression
42
+ │ ├── diff_db.py # diff result DB
43
+ │ └── diff_report.py # HTML diff report generation
44
+ ├── report/
45
+ │ ├── html.py # per-run HTML report
46
+ │ └── junit.py # JUnit XML report
47
+ ├── secrets/ # credential injection (pyrage encryption — planned)
48
+ ├── bridge/ # browser bridge over CDP — planned
49
+ ├── mcp/ # MCP server for AI agent integration — planned
50
+ └── server/ # planned
51
+ ```
52
+
53
+ ## CLI commands
54
+
55
+ ```bash
56
+ # Recording run: launches proxy, records API traffic to
57
+ # .scout/runs/<run_id>/record.db
58
+ scout run scenarios/auth/login-success --web-version 1.0.0
59
+ scout run scenarios/ --web-version 1.0.0 # recurse: find any test.py
60
+
61
+ # Debug verify: screenshot mode, no proxy
62
+ scout verify scenarios/auth/login-success --headed
63
+
64
+ # Diff: compare two recordings
65
+ scout runs # list run IDs
66
+ scout diff <baseline-id> <target-id>
67
+ scout diff <baseline-id> <target-id> --no-detail # skip body popup data
68
+
69
+ # URL override (for staging/local environments)
70
+ scout run scenarios/ --web-base-url http://localhost:9000 \
71
+ --api-base-url http://localhost:9000 \
72
+ --web-version dev
73
+ ```
74
+
75
+ `--web-version` is required on `scout run` — it tags the recording so
76
+ diff reports can label each side and so "added in <ver>" known-change
77
+ buttons in the report work correctly.
78
+
79
+ ## Data flow
80
+
81
+ ```
82
+ scout run
83
+ ├── Launch recording proxy (separate process)
84
+ ├── Playwright browser → proxy → target app
85
+ ├── Notify proxy of session boundaries per scenario
86
+ ├── API traffic → .scout/runs/<run_id>/record.db
87
+ ├── Per-scenario → .scout/runs/<run_id>/<scenario>/result.json
88
+ ├── HTML+JUnit → .scout/runs/<run_id>/report.html, junit.xml
89
+ └── Run index → .scout/index.db
90
+
91
+ scout diff baseline target
92
+ ├── Read both runs' record.db
93
+ ├── Pair endpoints by path; resolve query differences (2-stage)
94
+ ├── Compare status code + JSON structure + values
95
+ ├── Apply diff_ignore.json rules (field/value-type/endpoint ignores)
96
+ ├── Diff DB → .scout/diffs/<baseline>_vs_<target>/diff.db
97
+ └── HTML report → .scout/diffs/<baseline>_vs_<target>/report.html
98
+ ```
99
+
100
+ ## Scenario file format
101
+
102
+ A scenario lives in a directory next to an `app.json`:
103
+
104
+ ```
105
+ my-app/
106
+ ├── app.json
107
+ ├── diff_ignore.json # optional: noise rules
108
+ └── scenarios/
109
+ └── login/
110
+ └── test.py
111
+ ```
112
+
113
+ ```json
114
+ // app.json
115
+ {
116
+ "name": "my-app",
117
+ "web_base_url": "https://app.example.com",
118
+ "api_base_url": "https://api.example.com",
119
+ "viewport_width": 1280,
120
+ "viewport_height": 800
121
+ }
122
+ ```
123
+
124
+ ```python
125
+ # scenarios/login/test.py
126
+ from scout.runner import Locator, Page, Scenario
127
+
128
+ scenario = Scenario(
129
+ name="login",
130
+ base_url="https://app.example.com",
131
+ viewport_width=1280,
132
+ viewport_height=800,
133
+ )
134
+
135
+ email = Locator(name="email", tag="input", bbox=(640, 320, 280, 32))
136
+ password = Locator(name="password", tag="input", bbox=(640, 372, 280, 32))
137
+ submit = Locator(name="submit", tag="button", bbox=(640, 428, 280, 40))
138
+
139
+
140
+ @scenario.test
141
+ async def test(page: Page) -> None:
142
+ await page.goto("/login")
143
+ await page.fill(email, "user@example.com")
144
+ await page.fill(password, "password123")
145
+ await page.click(submit)
146
+ await page.wait(2000)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ scenario.run()
151
+ ```
152
+
153
+ ## Tech stack
154
+
155
+ | Layer | Choice |
156
+ |--------------------|-------------------------------------------|
157
+ | CLI | Python + Click |
158
+ | Browser automation | Playwright (Python) |
159
+ | HTTP client | httpx |
160
+ | Recording proxy | mitmproxy (separate process) |
161
+ | Data store | SQLite (recordings, diffs, run index) |
162
+ | Reports | HTML + JUnit XML |
163
+ | Lint + format | ruff |
164
+ | Type checking | pyright |
165
+ | Tests | pytest + pytest-asyncio + pytest-playwright |
166
+
167
+ ## Python environment
168
+
169
+ - Python >= 3.11
170
+ - Uses [uv](https://docs.astral.sh/uv/) for environment management
171
+ - **All Python commands must use `uv run`** — never bare `python` or `pip`.
172
+ `uv run` guarantees the venv interpreter matches `pyproject.toml`'s
173
+ `requires-python`; system Python can silently drift.
174
+
175
+ ## Conventions
176
+
177
+ - Code, comments, commit messages, issues, PRs: English
178
+ - [Conventional Commits](https://www.conventionalcommits.org/) — `feat:`,
179
+ `fix:`, `test:`, `refactor:`, `docs:`, `chore:`
180
+ - Run `uv run pytest tests/ -x --tb=short -m "not e2e"` before pushing
181
+ - See [CONTRIBUTING.md](CONTRIBUTING.md) for full development setup