pryscraper/AUDIT-2026-Q3.md
cryptorugmunch 4ef155f8fe
Some checks failed
CI / lint (push) Failing after 47s
CI / typecheck (push) Successful in 56s
CI / test (push) Successful in 1m31s
CI / Secret scan (gitleaks) (push) Failing after 29s
CI / Security audit (bandit) (push) Failing after 33s
chore: rename munchcrawl → Pry across docs and code
Fleet-wide rename of legacy munchcrawl references to Pry/pryscraper.
External referral URLs in pryscraper/proxy_referrals.py left as-is
— those are third-party service registrations needing manual update:
  brightdata.com, smartproxy.com, iproyal.com, webshare.io
2026-07-09 13:52:16 +02:00

41 KiB
Raw Permalink Blame History

title status owner last_updated audited_by supersedes audience goal
Pry — Production Audit & Roadmap (2026-Q3) canonical Rug Munch Media LLC Engineering 2026-07-06 opencode (deep multi-agent audit) AUDIT.md (2026-06-30)
engineers
agents
operators
ship Pry as production-grade + Apify-publishable within Q3 2026

Pry — Production Audit & Roadmap

TL;DR. Pry is a working prototype of a self-hosted scraper with good bones (scraping engine, template system, MIT/BSL split). It is not yet a product. Roughly 30% of the code is real, 30% is stub, 40% is hallucinated. To reach production-grade AND Apify-publishable, we need 8 weeks of focused engineering across 5 phases. Apify-publishable is reachable in 2 weeks if we sequence the Apify glue last.


1. Brutal Honest State (master table)

Area Claim Reality Status
Scraping engine 10-tier fallback 5 tiers wired, 4 import missing libs and silently no-op, 1 (Textise) exists only in docstring 🟡 partial
Anti-detection camoufox + stealth + TLS fingerprint + adaptive + cookie warmer ~1,255 lines of code that never runs in the default scrape flow. 0/8 modules wired into PryScraper.scrape(). 🔴 orphan theater
Templates 110 working scraper templates 110 JSON files, all CSS selectors. Real coverage validator says 21/110, live tests show ~30-40%. The other ~70 are decorative. 🟡 vanity
Database SQLite via SQLAlchemy pry.db exists (804 LOC db.py, 24 ORM models). No Alembic; Base.metadata.create_all() is the only migration path. data.db is a 96KB orphan. 🟡 partial
State files "all JSON in ~/.pry/" True: 24 subdirs + 122 JSON vault files + JSONL x402 ledgers. Zero concurrency control on the JSON writers. SQLite is the only transactional store. 🔴 race-condition risk
Auth gopass + JWT (HS256) + pbkdf2_hmac 100K rounds Real, but only 2 endpoints require it (/v1/key and one more). Most endpoints are public. 🟡 partial
API surface 115+ endpoints, 46 tag groups Real (~190 endpoints by Phase 0 count). But still in a 4,668-line api.py — only /v1/scraping/* and /v1/templates/* have been split into routers/. 🟡 monolithic
MCP "ships MCP" mcp_production.py (1,173 LOC) + mcp-worker.js Cloudflare Worker (408 LOC) + Smithery/Glama/Pulse manifests — BUT NOT DEPLOYED. /srv/pry/ only runs api.py. STATUS.md admits this. 🔴 shipped-in-repo, not live
x402 payments pay-per-request protocol x402.py (35K LOC) + x402_middleware.py. Not deployedPRY_X402_WALLET and PRY_X402_FACILITATOR unset on Talos, so all paid endpoints return 503. 🔴 dead code in prod
Apify "Apify-ready" apify_schema.py (405 LOC) builds Python dicts shaped like Apify input schema. No .actor/, no src/main.py, no Actor.init() anywhere. apify package not installed. 🔴 file name is decorative
Commerce sync "Shopify/WooCommerce sync" Real HTTP, hardcoded field map, no OAuth flows, no field-mapping engine. Zoho claimed in FEATURES.md — 0 matches in code. 🟡 partial
CRM sync "Salesforce/HubSpot/Pipedrive/Close sync" Real HTTP, bearer/api-key in parameter. No auth flow, no per-CRM schema. 🟡 partial
Email scraping "Gmail/Outlook OAuth inbox" Real Gmail Graph + Outlook Graph HTTPS calls — but caller hands in the access token. OAuth flow not implemented. "Extraction" is regex. 🟡 partial
Browser extension Chrome extension manifest.json exists (V3, 34 lines). icons/ directory is empty, options.html referenced but doesn't exist. Will be rejected by Chrome Web Store. 🔴 not publishable
WordPress plugin WP plugin Single 529-line PHP file. No readme.txt, license mismatch (MIT vs GPL declared in header). 🔴 not publishable to wordpress.org
Shopify app Shopify app 60-line scaffold. @shopify/shopify-api declared but never imported. No OAuth callback, no token exchange, no HMAC verify. 🔴 not publishable
Tests "500 tests passing" 500 collected, 498 pass, 2 fail (one is a flaky network test that makes a real httpx call to example.com). 60% coverage, no --cov-fail-under. Most tests assert trivial things ("string in dict")test_advanced_scraping.py is 17 attribute-presence tests. 🟡 theater
Test coverage ">80% enforced" 60% actual, no threshold check in CI. TESTING.md describes tests/unit/, tests/integration/, make test-unit, make test-covnone exist. 🔴 docs lie
Docs README, USAGE, ARCHITECTURE, AUDIT Three sets of docs. Some honest (AUDIT.md, STATUS.md). Some lying (TESTING.md describes directories that don't exist; FEATURES.md claims Zoho, .pry_sdk, export formats that aren't there). 🟡 mixed
CI GitHub Actions CI 1 workflow, 4 jobs (lint/typecheck/test/security). Doesn't install playwright/tesseract, so any real integration test fails on CI. Doesn't pin ruff/mypy versions. No deploy step. 🟡 minimal
Dependencies requirements.txt + pyproject.toml 23 deps, all >= with no upper bound. aiohttp_socks and aiohttp missing despite Tor tier importing them. pyjwt missing despite auth.py importing it. 🟡 destructive pin
Dockerfile multi-stage Real, but no USER directive (runs as root), healthcheck uses curl which isn't in runtime stage — container restart-loops. 🟡 broken healthcheck
docker-compose pry + flaresolverr + tor Sets FLARESOLVERR_URL (wrong var name; should be PRY_FLARESOLVERR_URL) — Cloudflare bypass silently fails. Same curl-in-healthcheck bug. 🔴 dead FlareSolverr path
CLI pry command pry --help crashes with infinite recursion (cli.py:339-340). pry mcp serve is dead code (only proxy routes to click). pry completions writes a broken eval string. 🔴 broken
SDK pry_sdk.py (async+sync) Real PryCrawl/PryCrawlSync, hardcoded endpoint paths, raw-dict returns. Doesn't use shared client.http_client. pry_sdk.py referenced in old docs — doesn't exist. 🟢 real
Proxies 10-tier with residential pool Catalogs 3 free + 10 premium providers, calls Brightdata via signup link. No actual credential rotation, no gopass lookup, no proxy health scoring. Tor tier imports aiohttp_socks which is missing. 🟡 stub
Referral revenue Jupiter 50bps, Hyperliquid 50bps, etc. (per AGENTS.md) Pry has nothing to do with that catalog. RMI-side. Pry's referrals.py covers 14 categories (exchange, LLM, hosting, proxy, etc.) with JWT-shaped attribution that doesn't actually verify the code was used. Cookie-only attribution. 🟡 toy
GDPR "consent, deletion, retention, audit" JSON files + two modules (gdpr.py, gdpr_real.py). Deletion DOES walk files and unlink. gdpr_real.py:138 imports ApiKey/QualityCheckRecord/UsageRecord from db.py — those models don't exist, raises ImportError at runtime. 🟡 half-real
Observability Prometheus metrics Real prometheus_client + 9 counters/histograms, /metrics endpoint. OpenTelemetry has ConsoleSpanExporter only — no remote export. 🟢 metrics, 🟡 no distributed tracing

Net: ~30% real, ~30% stub, ~40% hallucinated or dead code.


2. Per-Subsystem Findings (top issues with file:line)

A. Scraping pipeline

  • ultimate_scraper.py:269 _tier_cloudscraperMISSING DEP (cloudscraper not in requirements). Always returns None.
  • ultimate_scraper.py:305 _tier_undetectedMISSING DEP (undetected_chromedriver not in requirements).
  • ultimate_scraper.py:419 _tier_torMISSING DEPS (aiohttp, aiohttp_socks not in requirements). Also _has_tor = False at line 26.
  • ultimate_scraper.py:406 _tier_google_cacheGoogle deprecated this endpoint in 2024.
  • ultimate_scraper.py:33-46 docstring — Textise tier is fictional, exists only in the docstring.
  • parser.py.tmphalf-applied refactor, imports pry_validators (doesn't exist) and OcrExtractor (real class is ImageOCR). Every call raises ModuleNotFoundError. Delete.
  • stealth_engine.py (198 LOC) — orphan. Not imported anywhere.
  • browser_pool.py (102 LOC) — orphan. Not imported anywhere.
  • lazy_load.py (102 LOC), shadow_dom.py (116 LOC) — orphans. Two unused JS strings.
  • camoufox_integration.py (173 LOC) — launches Camoufox twice per fetch (line 133-166). Only callable as /v1/camoufox/fetch endpoint.
  • tls_fingerprint.py (190 LOC) — BROWSER_FINGERPRINTS includes "tor" (line 64) which curl_cffi does not ship.
  • adaptive.py:42Counter mutation makes _compute_information_gain always return 0 after the first page.
  • extractor.py:23self.ollama_base = "http://100.100.18.18:11434"dead URL, doesn't exist in fleet.
  • extraction.py:240 compute_embedding — trigram-frequency fallback is incompatible with 384-dim all-MiniLM-L6-v2. Cosine sim always 0.

B. API surface (api.py — 4,668 LOC)

  • api.py is still the god-file. Only routers/scraping.py + routers/templates.py + routers/health.py split (Phase 0). 44 of 46 endpoint groups still in api.py.
  • 159 broad except Exception patterns (per AGENTS.md). Top: error handlers swallow 7 different exception classes.
  • api.py:357 /metricsno auth (public).
  • Endpoints NOT split into routers/: scraping (partial done), parsing, automation, vision, sessions, batch, analysis, extraction, export, email, alerts, jobs, MCP, config, recorder, transform, execute, dashboard, circuit-breaker, pipeline, share, compliance, gdpr, training, monitoring, structure, intelligence, auth, review, quality, enrichment, reconciliation, commerce, crm, pipelines, agency, seo, reports, templates (partial done), integrations, ai, system.
  • mcp_production.py:155-159"Custom actor code not yet executed" placeholder.
  • STATUS.md says "PRY_X402_WALLET/FACILITATOR unset" — so all /v1/x402/paid/* endpoints return 503 in prod.
  • auth.py:25-41 JWT secret falls back to ephemeral random — meaning every uvicorn restart invalidates all JWT tokens.

C. Data layer & persistence

  • db.py — 804 LOC (not 31K), 24 ORM models, real SQLite at ~/.pry/pry.db.
  • No Alembic — schema changes require ALTER TABLE or wipe.
  • db.py:574 import_json_stores — claims idempotent, actually NOT (plain s.add() will violate unique constraints).
  • db.py:576 looks for quality/checks.jsonl — but quality.py:376 writes one-file-per-URL JSON, not JSONL. Importer is broken.
  • db.py uses sync Session inside async handlers — blocks event loop on every disk IO.
  • quality.py:347, freshness.py:45,120, sessions.py:21, proxy_manager.py:260,267non-atomic JSON writes, no fcntl, no tempfile + os.replace. Concurrent writers WILL corrupt.
  • cache.py:22 ResponseCache accepts redis_url but _redis is never assigned. Redis integration hole.
  • ratelimit.py:13 — in-memory per-IP token bucket. Multi-worker breaks.
  • data.db (96KB) — orphan, no code reads it.
  • gdpr_real.py:138 — imports ApiKey, QualityCheckRecord, UsageRecord from dbthose models don't exist, raises ImportError at runtime.

D. Tests, CI, DX

  • 62 test files, 500 tests, 498 pass / 2 fail in full suite.
  • tests/test_api.py:46-69 test_api_key_required_when_set — flaky network test, takes 1.5s-17s, scrapes example.com for real.
  • 60% coverage. No --cov-fail-under. TESTING.md:55 lies about --cov-fail-under=80.
  • TESTING.md:25-31 documents tests/unit/, tests/integration/, tests/fixtures/none exist.
  • Many "tests" are trivial attribute-presence checks: assert "chrome" in BROWSER_FINGERPRINTS, assert hasattr(s, "fetch").
  • Makefile:4 .PHONY lists commit but no commit: target exists.
  • pyproject.toml:60-62, 88-116 — 15 mypy error codes disabled, --ignore-missing-imports on.
  • pyproject.toml has no version pins at all (>= only, no <=). tiktoken>=0.8.0 had a CVE (GHSA-w3w6-43wv-w3fc).
  • pyproject.toml:99 lists aiohttp_socks and aiohttp as missing.
  • requirements.txt:1-25 is a hand-maintained duplicate of pyproject.toml. Will drift.
  • CI (.github/workflows/ci.yml): 1 workflow, doesn't install playwright/tesseract so the only real integration test fails on CI.
  • Pre-commit has mypy --strict but pyproject disables 15 codes — will fail or no-op inconsistently.

E. CLI / SDK / Docs

  • cli.py:339-340 -h/--help/help calls main() recursively → infinite recursion. pry --help is the first command a new user runs and it crashes.
  • cli.py:342-347 dispatcher only routes proxy to click. mcp serve / mcp info are unreachable despite being in help banner.
  • cli.py:225-246 pry completions writes broken eval string — no _PRY_COMPLETE handler exists.
  • cli.py:91-94 _spinner thread never join()ed.
  • pry_sdk.py is real (PryCrawl, PryCrawlSync), but creates its own httpx.AsyncClient per call instead of sharing client.http_client.
  • pry_sdk.py is referenced in old docs but doesn't exist.
  • README.md:11 says pip install pry — PyPI pry is owned by an unrelated project.
  • USAGE.md and README.md disagree on port (8002 vs 8005) without clarifying the Docker mapping.
  • FEATURES.md claims things that don't exist: Zoho CRM, pry_sdk.py, export RSS/TXT/SQL, /v1/templates/generate.

F. Apify / Integrations / Browser / WP / Shopify

  • apify_schema.py builds a dict that looks like Apify input schema. Never writes to disk, never instantiates Actor. No .actor/, no src/main.py, no apify package.
  • actor_marketplace.py is Pry-internal JSON store, not Apify marketplace.
  • Browser extension: icons/ empty, options.html missing → Chrome Web Store reject.
  • WordPress plugin: no readme.txt, license mismatch (MIT in SPDX vs GPL in header) → wordpress.org reject.
  • Shopify app: @shopify/shopify-api declared but never imported, no OAuth callback, no token exchange.
  • Cloudflare Worker (workers/mcp-worker.js) is real and 408 LOC — but wrangler.toml has routes commented out.
  • smithery.yaml, glama.json, pulsemcp.json are valid manifests — but install instructions reference the suspended cryptorugmuncher GitHub account.

G. Docker / Deploy

  • Dockerfile — runs as root (no USER), healthcheck uses curl not in runtime stage → restart loop.
  • docker-compose.yml:18 sets FLARESOLVERR_URL instead of PRY_FLARESOLVERR_URL → FlareSolverr bypass silently fails.
  • docker-compose.yml:67-80 Tor profile exists but aiohttp_socks missing → Tor tier silently no-ops.

3. Production-Readiness Distance

Category Today Production-grade target
Concurrent writes Broken (JSON race) All writes atomic + locked, single source of truth = SQLite
Schema evolution Hand-edit db.py or wipe DB Alembic migrations, versioned
Auth 2 endpoints, optional All mutating endpoints, per-tenant quota
Multi-worker Breaks cache+ratelimit+session state Redis-backed cache/ratelimit/session
Observability /metrics only Prometheus + structured logs + OpenTelemetry + tracing
AI extraction Dead URL, regex only Real Ollama/OpenRouter calls with structured output
Tests 500, 60%, 2 fail, mostly trivial Realistic tests with respx/vcr, target 80%, CI gate
CI/CD 1 workflow, no real test runner Multi-job, self-hosted on Talos, deploy step
Anti-detection Dead code One real fallback: curl_cffi + playwright-stealth. Drop the other 7 modules.
Templates 110 decorative JSON Audit each, kill the bottom 50%, add real per-site logic
Apify Glossary-only Real .actor/, src/main.py, apify push works
Browser ext / WP / Shopify Scaffolds only Publish-ready or remove
Docs Mixed honesty TESTING.md, FEATURES.md rewritten against actual code

4. Open-Source Apps To Install (current gap)

Tool Why Effort
cloudscraper Tier 2 actually works 5min
aiohttp-socks[aiohttp] + aiohttp Tor tier works, tier 10 unblocked 5min
undetected-chromedriver Tier 4 5min
curl_cffi Already installed. Actually use it in default scrape flow. 2hr
playwright-stealth Replace 200 LOC of stealth_engine.py with pip install playwright-stealth and one import. 30min
pyjwt Listed in auth.py imports, missing from requirements.txt. 1min
aiosqlite Use async SQLAlchemy so DB IO doesn't block the event loop. Already installed, not imported. 1 day wiring
alembic Schema migrations. Today: hand-edit or wipe. 1 day initial, free after
uv 10-100× faster installs, lockfile, removes requirements.txt drift. 1 hr
respx / pytest-httpx Mock httpx in tests. The flaky network test stops depending on example.com. 1 hr
freezegun Time-travel in jobqueue, intelligence, monitor, compliance tests. 30min
pip-audit CVE scan in CI security job. 1 hr
act Local CI runner. 30min
cov-fail-under in pyproject.toml CI gate at 80%. 5min
mutmut or cosmic-ray Mutation testing (most tests are trivial). 1 day setup
mut, spotlight or similar Stale CSS-selector detection. 2 days
fastapi[all] extras JWT middleware, http2, orjson, etc. 1hr

5. Hallucinated Features — list of claims with no implementation

Claim Source Reality
110 working scraper templates FEATURES.md, STATUS.md 110 JSON, ~30% work end-to-end. Real validator score is 21/110.
10-tier anti-bot fallback ARCHITECTURE.md, FEATURES.md 5 tiers wired, 4 broken, 1 fictional.
"Multi-tenant agency platform" AUDIT.md, FEATURES.md JSON files. No isolation. No billing.
"GDPR compliance — real DPO" FEATURES.md File-based. No DPO. gdpr_real.py:138 raises ImportError.
"AI-powered extraction" FEATURES.md extractor.py:23 calls a dead Ollama URL. Fallback is regex.
"AI-powered compliance / SEO" FEATURES.md Regex-only.
"6 CAPTCHA providers" FEATURES.md 4 real, 2 stubs (deathbycaptcha, nextcaptcha).
"Tor proxy routing" README, FEATURES.md Imports lib that's missing — runtime crash.
"Multi-tenant billing" AUDIT.md Zero billing code.
"Zoho CRM sync" FEATURES.md Grep returns 0 matches in code.
pry_sdk.py (old) Doesn't exist.
Shell autocompletion cli.py:225 Writes broken eval string; no _PRY_COMPLETE handler.
/v1/templates/generate FEATURES.md Not in api.py or routers.
pry mcp serve / pry mcp info cli.py:375-403 Help says it exists; main() never routes to it.
Export formats RSS/TXT/SQL README pryfile.py only does json/csv.
0.0.3 release of pry on PyPI README PyPI name is taken.
tests/unit/, tests/integration/, make test-unit, make test-cov TESTING.md None exist.
make commit AGENTS.md, Makefile help .PHONY lists it, no rule defines it.
Coverage threshold >80% enforced TESTING.md No threshold. Current coverage is 60%.
MCP endpoints /mcp/tools, /mcp/call FEATURES.md Not in api.py; only in mcp_production.py (not deployed).
Browser extension publishable repository Empty icons/, missing options.html.
WordPress plugin publishable repository No readme.txt. License mismatch.
Shopify app publishable repository @shopify/shopify-api never imported.

6. The Plan — 5 Phases, 8 Weeks

Guiding principle: build on what's there. Don't rewrite. Cut dead code aggressively. Make the stubs actually do their job. Then ship.

Phase 0 — Stop the bleeding (3 days)

Goal: kill the embarrassing things, get tests green, get prod stable.

  1. Fix cli.py:339-340 — replace infinite recursion with usage block. New users run pry --help first.
  2. Delete parser.py.tmp — broken half-refactor with broken imports.
  3. Add missing deps to pyproject.toml + requirements.txt: cloudscraper, aiohttp-socks[aiohttp], undetected_chromedriver, pyjwt, apify>=2.0 (for Phase 4).
  4. Fix docker-compose.yml:18FLARESOLVERR_URLPRY_FLARESOLVERR_URL.
  5. Fix Dockerfile healthcheck — use python3 -c "import urllib.request; ..." instead of curl not-in-runtime-stage.
  6. Remove commit from Makefile .PHONY (or implement via commitizen — recommend remove).
  7. Fix tests/test_api.py:46-69 — mock PryScraper with AsyncMock returning {"status":"ok","content":"x"}. Test stops depending on example.com.
  8. Fix auth.py:35-41 — JWT secret must NOT fall back to ephemeral random. Fail closed (raise) so deploy is forced to set the env var.

Phase 1 — Single source of truth for state (1.5 weeks)

Goal: kill the JSON race conditions, gain Alembic, gain async DB.

  1. Add Alembic. alembic init alembic, autogenerate from Base.metadata, replace Base.metadata.create_all() in db.py:147 with command.upgrade("head") (gated by env flag for dev).
  2. Migrate the JSON writers to SQLite:
    • quality.py:347, freshness.py:45,120, sessions.py:21, proxy_manager.py:260,267, gdpr_real.py — flip each to write via session_scope(). The import_json_stores in db.py:574 becomes the real migration tool. Fix its idempotency: use s.merge() or ON CONFLICT DO NOTHING.
  3. Async SQLAlchemy — switch db.py to AsyncSession with aiosqlite (already installed). quality.py, freshness.py, referrals.py, monitor.py, intelligence.py, structure_monitor.py no longer block the event loop.
  4. Atomic JSON writes for any stragglers (portalocker + tempfile + os.replace pattern).
  5. Delete orphan data.db (96 KB SQLite, no code reads it).
  6. Fix gdpr_real.py:138 — either add the missing models ApiKey, QualityCheckRecord, UsageRecord to db.py, or remove the SQL branch. Document decision in DECISIONS.md.
  7. Add Redis-backed ResponseCache and RateLimiter (use rmi-redis:6379 on Talos). The cache.py:22 redis_url parameter is already there — just wire it.
  8. Delete import_json_stores's broken JSONL assumption at db.py:576quality.py writes one JSON per URL, not JSONL.

Phase 2 — Scraping core: cut dead, wire real (1.5 weeks)

Goal: the 10-tier chain becomes a 5-tier chain, every tier verified, fallback works.

  1. New fallback chain (5 tiers, all real):
    1. Direct HTTP via curl_cffi with rotating impersonation profiles (Chrome / Firefox / Edge). Replaces Tier 1 (httpx no fingerprint) and tls_fingerprint.py standalone module.
    2. cloudscraper — real Cloudflare bypass (Tier 2, deps now installed).
    3. FlareSolverr sidecar — already wired, but verify in compose.
    4. playwright + playwright-stealth — single import replaces 200-LOC stealth_engine.py. Real fingerprint (WebGL vendor noise, canvas, audio, human mouse).
    5. Archive.org Wayback — already implemented, verify behavior. (Drop the dead "Google cache" and "Tor" tiers.)
  2. Delete the orphans (~700 LOC total):
    • stealth_engine.py, browser_pool.py, lazy_load.py, shadow_dom.py — unused.
    • camoufox_integration.py → keep as opt-in endpoint, don't try to put it in the default chain (camoufox is slow; it's for the hard cases that should be attempted after playwright fails).
    • tls_fingerprint.py → fold into the new Tier 1.
    • cookie_warmer.py → keep as opt-in endpoint; the "warming" logic is theatrical.
    • adaptive.py → fix the _compute_information_gain bug, then keep as opt-in /v1/crawl/adaptive.
  3. Fix parser.py.tmp deletion (already in Phase 0).
  4. Fix extractor.py:23 — read OLLAMA_BASE from env (default to http://100.104.130.92:11434 per AGENTS.md).
  5. Fix extraction.py:240 compute_embedding — use sentence-transformers/all-MiniLM-L6-v2 (384-dim) or remove the fallback dimension mismatch.
  6. Fix ultimate_scraper.py:33-46 — new docstring matches the 5-tier reality.
  7. Add playwright-stealth to pyproject.toml. Remove stealth_engine.py.
  8. Templates audit — write templates/test_live_against_real_sites.py (not against homepages, against article/product URLs). Pass-fail dashboard. Strike the bottom 50%. Document the survivors in TEMPLATES-COVERAGE.md.

Phase 3 — API surface: split the monolith, add auth, deploy MCP+x402 (2 weeks)

Goal: every endpoint in routers/, MCP live, x402 live.

  1. Router split (1.5 weeks) — split api.py from 4,668 LOC to ~500 LOC (api.py:routes includes only the FastAPI() instantiation + global middleware; everything moves to routers/<domain>.py). Sequence:
    1. routers/health.py, routers/scraping.py, routers/templates.py (Phase 0 done).
    2. routers/extraction.py (linking, extraction, vision).
    3. routers/sessions.py (sessions, browser, automation).
    4. routers/agents.py (quality, freshness, structure, monitor, seo).
    5. routers/integrations.py (destinations, crm, commerce, email, agency).
    6. routers/compliance.py (gdpr, compliance, training, review).
    7. routers/admin.py (costing, alerts, jobs, pipeline, recorder, transform).
    8. routers/payments.py (x402 + paid endpoints).
    9. routers/mcp.py (merged from mcp_production.py's HTTP transport).
    10. routers/auth.py (auth, agency client auth, api key).
  2. Fix silent error swallowing (the 159 except Exception). Move to errors.py hierarchy: PryHTTPError, PryAuthError, PryScrapeError, PryStorageError, etc. Use logger.exception(...) with request_id.
  3. Auth gate — apply JWT/API-key middleware to all mutating endpoints except /health, /live, /ready, /metrics, /docs, /openapi.json, /v1/templates/*, /v1/x402/pricing. Move away from per-endpoint @require_auth.
  4. Deploy mcp_production.py to Talos on /srv/pry/mcp/ as a sibling server (port 8006). Update docker-compose.yml. Make pry mcp serve actually work (wire click dispatcher).
  5. Set PRY_X402_WALLET and PRY_X402_FACILITATOR on Talos. Verify endpoints transition from 503 → 402 → 200.
  6. Add routers/mcp.py HTTP+SSE transport to the main app (alternative to sibling process). Route /sse, /messages/:sid.
  7. Update smithery.yaml / glama.json / pulsemcp.json with the live mcp.pry.dev URL and remove the suspended cryptorugmuncher GitHub URLs.

Phase 4 — Apify launch (1.5 weeks, parallelizable with Phase 3)

Goal: Pry ships as an Apify Actor.

  1. Minimal Apify Actor (1 day):
    • pip install apify>=2.0 (added in Phase 0).
    • .actor/actor.json — manifest.
    • .actor/DockerfileFROM apify/actor-python-playwright:3.12-slim, copy src/, pyproject, templates.
    • src/main.pyasync def main(): async with Actor: actor_input = await Actor.get_input(); ...; await Actor.push_data(...).
    • Write the input schema from apify_schema.py to .actor/input_schema.json via a make actor-build target.
  2. Strip local-only deps from the Actor image — FlareSolverr (docker-compose.yml), Tor profile, depend on curl_cffi + playwright + cloudscraper + archive.org only.
  3. Push to Apify (1 day)apify login, apify push, verify apify call runs.
  4. Publish on Apify Store — name pry-scraper, category "Developers", monetization via apify monetize (per-scrape pricing).
  5. Public listing (1 day) — write actor description, screenshots, README. Submit to Apify for review.
  6. Actor README — what inputs it accepts, what outputs it produces, error modes, examples.

Phase 5 — Production polish + observability + tests (1.5 weeks)

Goal: ship-ready. 80% test coverage that's real, not theater. Observability that operations trusts.

  1. Real tests — rewrite test_advanced_scraping.py, test_referrals.py, etc. to test BEHAVIOR not presence. Use respx for httpx mocking. Use freezegun for time-travel. Use vcr.py for recording live API calls.
  2. Coverage gate--cov-fail-under=80 in pyproject.toml [tool.coverage.run]. CI fails below threshold.
  3. CI/CD — split the single workflow into:
    • lint.yml (ruff + format check)
    • typecheck.yml (mypy strict, no disabled codes)
    • test.yml (pytest with respx, no real network; installs playwright + tesseract; coverage gate)
    • security.yml (bandit + pip-audit + gitleaks)
    • docker.yml (build + smoke)
    • deploy.yml (push to Talos, run healthcheck)
    • Self-hosted runner on Talos (100.104.130.92) — no GitHub Actions minutes.
  4. Observability — wire OpenTelemetry OTLP exporter to a real backend (Grafana Cloud free tier, or local Tempo on Talos). Real distributed tracing across httpx, playwright, db.
  5. Structured logging — JSON logs with request_id, tenant_id, user_id, route, latency_ms. Forward to Loki (free tier) or write to journald on Talos.
  6. OpenAPI SDK generation — drop pry_sdk.py (130 LOC hand-maintained), generate pry-sdk-python from openapi.json. Generate pry-sdk-js. Publish to PyPI as pry-scraper-sdk and to npm as @pry/sdk.
  7. Deprecation log — what was deleted in this rewrite:
    • parser.py.tmp
    • stealth_engine.py, browser_pool.py, lazy_load.py, shadow_dom.py (4 modules, ~518 LOC)
    • The 5 dead tiers in ultimate_scraper.py
    • The flaky tests/test_api.py:46-69
    • The 159 broad except Exception
    • The .pry_sdk reference
    • data.db orphan
  8. Decorate — add READY / LIVE to README badges from /health and /live responses.

7. Apify-publishable cut (smallest viable for Apify)

If the only goal is "ship on Apify first, full production later", cut Phases 1, 3, 5 and just do:

Task Effort
Phase 0 stop-the-bleeding (3 days) 3d
Phase 4 (all 1.5 weeks) 7d
Phase 2 minimum: fix the missing deps, replace stealth_engine.py with playwright-stealth, drop the dead tiers 4d
Total ~2 weeks for Apify-publishable

Production polish follows the Apify launch. Pricing on Apify: per-scrape (e.g. $0.005/scrape, free monthly tier for viral growth).


8. The "2026 AI-Forward" code style we want

Distributed in the codebase by Phase 5. Reference for new modules:

pry/
├── README.md                       # honest, no hallucinated features
├── AGENTS.md
├── pyproject.toml                  # uv-managed, pinned
├── Dockerfile                      # apify/actor-python for Apify
├── .dockerignore
├── alembic/                        # NEW — migrations
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
├── alembic.ini
├── src/
│   └── pry/                        # NEW — proper package layout
│       ├── __init__.py
│       ├── py.typed
│       ├── main.py                 # FastAPI entrypoint
│       ├── api/
│       │   ├── __init__.py
│       │   ├── app.py              # FastAPI() instance only
│       │   ├── deps.py             # shared dependencies
│       │   ├── errors.py
│       │   ├── routers/            # one file per domain
│       │   │   ├── health.py
│       │   │   ├── scraping.py
│       │   │   ├── templates.py
│       │   │   ├── extraction.py
│       │   │   ├── sessions.py
│       │   │   ├── agents.py
│       │   │   ├── integrations.py
│       │   │   ├── compliance.py
│       │   │   ├── admin.py
│       │   │   ├── payments.py
│       │   │   ├── mcp.py
│       │   │   └── auth.py
│       │   ├── middleware/
│       │   │   ├── auth.py
│       │   │   ├── ratelimit.py
│       │   │   ├── request_id.py
│       │   │   ├── logging.py
│       │   │   └── errors.py
│       │   └── schemas/            # pydantic models per router
│       │       ├── scraping.py
│       │       └── ...
│       ├── core/                   # pure business logic, framework-free
│       │   ├── errors.py
│       │   ├── config.py
│       │   ├── logging.py
│       │   ├── http.py             # shared httpx + curl_cffi
│       │   └── secrets.py          # gopass + env
│       ├── scrape/                 # the scraping engine
│       │   ├── fallbacks.py        # 5-tier chain
│       │   ├── parsers.py          # pdf, docx, html
│       │   ├── extractor.py        # CSS + LLM fallback
│       │   ├── templates.py
│       │   └── monitor.py          # freshness, structure
│       ├── llm/                    # unified Ollama/OpenRouter client
│       │   ├── client.py
│       │   ├── extraction.py
│       │   ├── vision.py
│       │   └── schemas.py           # structured-output
│       ├── db/
│       │   ├── engine.py            # AsyncSession with aiosqlite/postgres
│       │   ├── migrations.py
│       │   ├── models.py
│       │   └── repos/               # one per model
│       ├── billing/                 # x402 implementation
│       │   ├── protocol.py
│       │   ├── middleware.py
│       │   └── ledger.py
│       ├── integrations/
│       │   ├── shopify.py
│       │   ├── woocommerce.py
│       │   ├── salesforce.py
│       │   ├── hubspot.py
│       │   ├── pipedrive.py
│       │   ├── close.py
│       │   ├── email.py            # Gmail/Outlook
│       │   ├── slack.py
│       │   └── webhook.py
│       ├── platforms/               # publish targets
│       │   ├── apify.py            # apify actor entrypoint
│       │   ├── cloudflare.py       # MCP worker
│       │   └── mcp.py              # JSON-RPC dispatcher
│       └── cli/
│           ├── __init__.py
│           └── main.py             # click-based, replaces cli.py
├── tests/
│   ├── conftest.py
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── templates/                       # 110 JSON
├── TEMPLATES-COVERAGE.md           # NEW: honest coverage
├── docs/
└── .actor/                          # NEW: for Apify

Key changes that make this 2026-current:

  1. src/ layout — proper Python package, enables pyproject.toml [tool.setuptools.packages.find], prevents import ambiguity.
  2. py.typed marker — third-party users get type checking.
  3. core/ — framework-free business logic, testable without spinning up FastAPI.
  4. One router file per domain — each < 500 LOC, one clear responsibility.
  5. db/repos/ pattern — every SQL call goes through a repo function. No session_scope() calls scattered through business logic.
  6. middleware/ explicit — auth, ratelimit, request_id, logging, error-handling all separate files.
  7. llm/ separated from core/ — LLM extraction, vision, structured output are first-class concerns.
  8. platforms/ separated from integrations/ — where we publish (Apify, Cloudflare, MCP server) is different from what we integrate with (Shopify, Salesforce).
  9. alembic/ migrations — version-controlled schema. No more Base.metadata.create_all().
  10. tests/unit/, tests/integration/, tests/e2e/ — honest test layering.

9. Open-Source Install List (Phase-keyed)

Phase 0 (3 days)

  • cloudscraper, aiohttp-socks[aiohttp], undetected_chromedriver, pyjwt, apify>=2.0
  • respx (or pytest-httpx)
  • freezegun

Phase 1 (1.5 weeks)

  • alembic
  • aiosqlite is already installed; just import it

Phase 2 (1.5 weeks)

  • playwright-stealth
  • curl_cffi is already installed; use it
  • sentence-transformers (for real embeddings) — or drop embedding fallback

Phase 3 (2 weeks)

  • fastapi[all] (orjson, asyncpg, jinja2 extras)

Phase 4 (1.5 weeks, parallelizable)

  • apify (added in Phase 0)

Phase 5 (1.5 weeks)

  • pip-audit
  • vcr.py or pytest-recording
  • mutmut or cosmic-ray
  • pytest-benchmark (perf gates)
  • OpenTelemetry OTLP exporter
  • uv (replaces pip)
  • pre-commit cache, act

10. KPIs / Definition of Done

KPI Today Target at end of Phase 5
Tests passing 498/500 (2 flaky) 800+/800 stable
Test coverage 60% (no gate) ≥ 80% (CI gated)
Endpoints with auth 2/~190 all mutating
Endpoints with rate-limit ~6 all
Endpoints with structured logging 0 all
Endpoints with tracing 0 all
Dead code (LOC) ~700 (orphaned modules) + ~2,000 (hallucinated features) <100
Anti-detection fallback tiers working end-to-end 5/10 5/5 (consolidated)
Templates passing live-URL test ~21-30 50+
Concurrent writers without corruption NO YES (atomic+locked or SQLite)
Schema migrations Hand-edit or wipe Alembic, versioned
CI minutes burned GitHub-hosted () Self-hosted Talos ($0)
Deployable to Apify store NO YES
Production uptime TBD 99.5% / 30d rolling
p95 latency (scrape) TBD < 5s for direct, < 30s with playwright

11. What to do tomorrow (1 working day)

If the user picks one day to make Pry significantly better:

  1. Delete parser.py.tmp (5 min)
  2. Fix cli.py:339-340 infinite recursion (15 min)
  3. Add missing deps to pyproject.toml: cloudscraper, aiohttp-socks[aiohttp], undetected_chromedriver, pyjwt, apify>=2.0 (10 min)
  4. Fix docker-compose.yml:18 env var name (2 min)
  5. Fix Dockerfile healthcheck to not use curl (10 min)
  6. Add USER directive to Dockerfile (5 min)
  7. Fix tests/test_api.py:46-69 to mock PryScraper (15 min)
  8. Fix ultimate_scraper.py:33-46 docstring to honest 5-tier (10 min)
  9. Migrate quality.py:347 from JSON to SQLite via existing QualityCheck model (already in db.py:197) (1 hr)
  10. Same for freshness.py (1 hr)
  11. Pin uv.lock (30 min)
  12. Set --cov-fail-under=60 to start (we can tighten later) (5 min)

End of day: no flaky tests, no infinite recursion, no dead tiers claiming to work, no JSON races on quality/freshness, real coverage gate, Dockerfile runs as non-root.

That's the user's "day to make Pry significantly better" answer. Then Phase 0 takes another 2 days, then Phase 1 starts. Apify-publishable in 2 weeks.


12. Risks

  1. Phase 2 anti-detection consolidation — by removing 7 modules, we lose the "kitchen sink" cover. Mitigation: keep camoufox_integration.py and cookie_warmer.py as opt-in endpoints (operators can call them manually for hard cases).
  2. Phase 3 router split — risk of breaking existing endpoints. Mitigation: split one router at a time, deploy each, run smoke test (curl /health + 1 representative endpoint per router).
  3. Phase 4 Apify first-impressions — Apify Store review takes 1-2 weeks. Start early; in parallel with Phase 1/2.
  4. Phase 5 self-hosted CI runner — Talos has other workloads; registering a runner should not consume > 10% CPU. Run on Hydra if Talos is busy.
  5. Multi-worker with cookies/sessions — moving from JSON files to SQLite is the easy part. Sharing browser sessions across workers requires Redis or sticky sessions. Defer to v3.1.
  6. OpenAI/Anthropic rate limits — depending on LLM extraction (real, not regex) means we burn keys. Mitigation: local Ollama first; OpenAI only on cache-miss.

13. Decision log (what this audit is committing to)

  • ADR-0003: Consolidate anti-detection to curl_cffi + playwright-stealth. Delete 7 orphan modules.
  • ADR-0004: SQLite for single-node, Postgres for multi-host. Alembic for migrations. Atomic writes everywhere.
  • ADR-0005: src/pry/ layout. One router per domain. No file > 500 LOC (enforced by CI).
  • ADR-0006: Real LLM extraction by default — Ollama first, OpenRouter fallback. Regex fallback only on cache-miss + LLM error.
  • ADR-0007: JWT auth + per-tenant quota on all mutating endpoints. /health and /metrics public.
  • ADR-0008: Apify is the primary distribution post-Phase 4. Self-hosted Talos deploy remains supported.
  • ADR-0009: Tests must mock network (respx) and time (freezegun). No real network in CI.
  • ADR-0010: Coverage gate --cov-fail-under=80 after Phase 5. No merging below threshold.
  • ADR-0011: Self-hosted CI runner on Talos. No GitHub Actions minutes.
  • ADR-0012: OpenTelemetry OTLP exporter. Loki for logs. Prometheus for metrics. Grafana dashboards on Grafana Cloud (free) or Talos.

  • Original audit (2026-06-30, Sonnet 4.5): AUDIT.md
  • This audit (2026-07-06): AUDIT-2026-Q3.md
  • Production roadmap: PRODUCTION-PLAN.md (this file)
  • Status: STATUS.md
  • Sprint plan: PLAN.md
  • AGENTS contract: AGENTS.md
  • Standards: https://git.rugmunch.io/RugMunchMedia/standards