Skip to content

Quality tool: audit, overview & dedupe with HTML dashboard and agent skills - #11

Open
MirjamOdile wants to merge 14 commits into
mainfrom
pr/05-quality-tool
Open

Quality tool: audit, overview & dedupe with HTML dashboard and agent skills#11
MirjamOdile wants to merge 14 commits into
mainfrom
pr/05-quality-tool

Conversation

@MirjamOdile

Copy link
Copy Markdown
Contributor

What this adds

A project-wide quality layer over data/<project>/ crawl output — three CLI
commands, an HTML dashboard, three agent skills, docs, and tests. Built and
battle-tested against live production projects before this handover.

Commands

  • ./scrapai audit --project <name> — read-only. Per-spider coverage +
    extraction scoring (crawl_audit.csv / audit_<project>.md), compliance
    lenses (robots/llms/legal-page snapshots with conflict flagging),
    external-PDF host analysis, and a self-contained HTML dashboard
    (_audit/dashboard_<project>.html, tabs: Coverage · Compliance · PDFs).
    Every spider is classified into a status worklist (ok / manual review /
    too few pages / incomplete / extraction broken) — the goal, stated on
    the report itself: every spider ends in ok (or deliberately discarded).
  • ./scrapai overview --project <name> — read-only. Per-spider content
    profile: sections, date span + per-year histogram + null %, field coverage,
    thin/constant/off-domain checks.
  • ./scrapai dedupe --project <name> — the ONE mutating command, kept
    separate so the audit never rewrites data. Consolidates crawls/*.jsonl
    reversibly (originals kept as *.superseded).

Design points reviewers will care about

  • PDF row model: PDFs follow the framework's links_only harvest —
    URL-only rows with metadata_json.content_type="pdf". Extraction quality is
    judged over HTML rows only; the PDFs lens splits same-org vs external by URL
    host against allowed_domains (finding an org's documents stored off its
    own domain is the lens's whole purpose).
  • Crawl-recorded stats first: coverage denominators and liveness come from
    _audit/crawl_stats/ when a crawl recorded them (no re-fetch, no drift, no
    CF problems); fetched sitemaps are the fallback.
  • Human review records: _audit/audit_sitemap_skip.json corrects the
    yardstick BEFORE scoring; _audit/audit_notes.json records the verdict
    AFTER (promote/discard). Both are self-documenting (embedded, auto-refreshed
    _instructions), both suggest-only for agents, and stale notes are policed
    (a note can never vouch for broken extraction or an empty corpus).
  • Performance: per-file scan counts are cached by (size, mtime) — GB-scale
    corpora re-audit in seconds; caches migrate themselves on format change.
  • Dashboard is dependency-free: one static HTML file, inline CSS/JS, every
    dynamic value escaped; markdown/CSVs remain the untouched ground truth.

Also in this PR

  • Three slash-command skills (.claude/commands/): /spider-review (evidence-
    based triage/repair of the audit's problem groups), /spider-align
    (fleet-wide conformance sweep), /spider-slow (live-crawl ops via
    crawl-status/Pueue) — plus the .gitignore pattern that tracks shipped
    skills while keeping teams' local ones untracked.
  • Docs: docs/quality.md (full reference), docs/skills-overview.md, CLAUDE.md
    §6.9 + README wiring, and docs/requests/ — the per-change design write-ups
    behind this PR series (the earlier small PRs reference them).

Verification

~100 dedicated unit tests across corpus scanning + caching, scoring (including
PDF-row arithmetic and the reviewed-stale guards), compliance capture/report,
external-PDF partitioning, dashboard rendering, and an end-to-end synthetic
project flow. Full suite green. Restructures were gated by byte-identical
golden-master runs against real project output.

…nt skills

Adds the read-only quality layer over data/<project>/ and its one
mutating counterpart:

- ./scrapai audit    — coverage + extraction scoring per spider (CSV/MD),
  compliance lenses (robots/legal snapshots, conflict flags), external-PDF
  host analysis, and a self-contained HTML dashboard (Coverage ·
  Compliance · PDFs tabs). PDFs follow the framework's row model:
  URL-only rows with content_type="pdf"; scraped/content% are judged over
  HTML rows only; same-org vs external is derived from allowed_domains.
- ./scrapai overview — per-spider content profile: sections, date span,
  field coverage, thin/constant/off-domain checks.
- ./scrapai dedupe   — the ONE mutating command, kept separate so the
  audit never rewrites data; consolidates crawls/*.jsonl reversibly
  (originals kept as *.superseded).

Ships with three slash-command skills (.claude/commands/: spider-review,
spider-align, spider-slow) that consume the audit's status groups,
docs (docs/quality.md, docs/skills-overview.md, request write-ups under
docs/requests/), and the .gitignore pattern for teams to keep local
skills/docs untracked alongside the shipped ones.

Engine tests cover corpus scanning + caching, scoring (incl. PDF-row
arithmetic and reviewed-stale guards), compliance capture/report,
external-PDF partitioning, dashboard rendering, and an end-to-end
synthetic-project flow.
@MirjamOdile

Copy link
Copy Markdown
Contributor Author

Heads-up: two commits appended since you last looked — a bugfix bundle for the audit's own measurements (Audit: measurement-accuracy fixes + minimal default output) and an update to the
request-docs README map.

What was broken: the audit scored spiders against corrupted numbers, so it flagged healthy spiders as broken:

  • Temporary 503 server hiccups counted as dead pages (one spider read "56% liveness" from 810 transient 503s; real coverage was ~97%).
  • The same PDF linked from N pages produces N rows (each remembers which page cited it) — the audit counted these as "N versions of changed content"; one spider showed 10,932 phantom
    versions. Measured after the fix: real HTML content churn is ~0 on every spider — it was all citation provenance.
  • Image/attachment URLs in sitemaps inflated the coverage denominator.
  • A Cloudflare "Just a moment" challenge returned for robots.txt read as a valid robots file with no Sitemap line → sites mislabelled "no-sitemap", hiding real coverage loss.
  • And the audit dumped hundreds of lines to stdout on every run.

What it does now: only 4xx count as dead; PDF citation rows get their own "pdf multi-ref" column while "versions" shows only genuine HTML changes (the counting itself is untouched, so
the audit still agrees byte-for-byte with what dedupe does); media locs are filtered from the fetched denominator; an HTML robots response triggers a browser retry before being trusted;
and the default output is a progress bar + summary, with --verbose restoring the full detail.

Note: the media-loc and CF-robots fixes only fire on the sitemap FETCH path — they're logic-checked but still need one normal (fetching) audit run to be exercised live.

Request doc: docs/requests/22-audit-measurement-and-quiet.md.

@MirjamOdile

MirjamOdile commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

A short series of commits that make the compliance audit trustworthy on Cloudflare / proxy-gated sites:

1. Crawl axis — fall back to crawl-captured robots when the live probe is walled (45f1d73). The compliance probe is deliberately lightweight (no proxy / CF-solver / browser) so a block is never misread as "allowed" — but that means CF/proxy-gated sites fail it entirely, get a _capture_failed.json marker, and show NOT CHECKED indefinitely. Now, at report time, a capture-failed domain that still has a crawl-time crawls/robots_*.txt witness is promoted with a crawl-sourced robots view (✓ 0/N blocked *(via crawl)*); the reuse axis stays a 🔎 review (homepage/licence was never read). Report-time and read-only — benefits existing markers with no re-capture; no crawl witness → no rescue, so nothing is fabricated. Extends the existing per-snapshot "rescued" path to the total-failure case.

2. The per-spider summary now honours the rescue (ed63311). compliance_summary() — which feeds audit_<project>.md and the dashboard — read has_capture_failed() directly, so a rescued domain still showed ‼️ failed there, disagreeing with compliance_<project>.md (which the docstring says can never happen). It now consumes build_report_data()'s rescued recs, so the two outputs agree.

3. No-data rows sort to the TOP (b1ed14b, 103a2be). The per-spider compliance table, the dashboard compliance tab, and the compliance report's Crawl/Reuse tables all ranked ‼️ capture-failed / ❓ not-checked LAST — burying the most actionable rows at the bottom. All three now lead with them (failed → not-checked → graded 🔴→🟢). Net: on a probe-walled project the NOT-CHECKED count drops sharply (only genuinely witness-less domains remain), the two compliance outputs agree, and the most-problematic rows are on top. Tests added across the rescue, the summary agreement, and the ordering; full unit suite green, black + flake8 clean.

@iRanadheer
iRanadheer force-pushed the pr/05-quality-tool branch from 03a571f to 9d403bc Compare July 30, 2026 08:54
The audit scored against corrupted denominators/fingerprints, flagging healthy
spiders as broken. All fixes are audit-only (reported numbers, never crawl
behaviour or data):

- A liveness: only 4xx count as dead; transient 5xx are excluded from the
  denominator (one spider: 810 transient 503s read as 'liveness 56%' when
  real coverage was ~97%; verified moved to ok).
- B versions: the same PDF linked from N pages yields N rows differing only by
  found_on provenance; dedupe keeps them, so the counting is untouched — the
  scan now counts the PDF rows' own (url, fingerprint) pairs and the report
  splits 'pdf multi-ref' out of 'versions', which shows only HTML re-fetch
  churn (one spider: 10,932 phantom versions). Preserves the
  audit↔dedupe invariant that the tried counting change broke.
- C: media attachment <loc>s (WP image sitemaps) excluded from the fetched
  coverage denominator (spider-side counter fixed in docs/requests/19).
- D: a Cloudflare challenge page returned for robots.txt no longer reads as a
  valid robots with no Sitemap line — an HTML body triggers a browser retry
  (one site was mislabelled no-sitemap, hiding a ~50% loss).
- E: default output is stage markers + a transient progress bar + summary;
  --verbose restores per-spider lines, the full external-PDF report, and
  per-org compliance detail.

C/D fire only on the sitemap FETCH path and still need one live fetching audit
to confirm (verification ran --no-fetch).

docs/requests/22-audit-measurement-and-quiet.md
From this wave on the request number is the PR label (branch pr/<nn>-<slug>),
continuing the 04-14 ledger; 15 (robots-obey default) was considered and
withdrawn as a framework change (per-project preference, handled in spider
configs), so the wave starts at 16. Requests 22 and 23 are patch commits on
the open quality-tool and per-crawl-stats PRs, whose code they correct.
… walled

A capture-failure (the independent probe reached neither robots.txt nor the
homepage) is almost always a Cloudflare/TLS wall the audit's no-proxy probe
can't pass but the spider's own crawl (browser/curl_cffi/proxy) does. When that
spider captured robots.txt at crawl time we still hold an authoritative witness
for the CRAWL axis, so promote the domain into the report with a crawl-sourced
robots view ("(via crawl)") instead of leaving it "NOT CHECKED". The REUSE axis
stays a review — the homepage/licence was genuinely never read.

Report-time and read-only: the _capture_failed marker is left in place (the live
probe really did fail; --refresh still retries it) and old snapshots benefit
without re-capture. No crawl witness -> no rescue, so nothing is fabricated.
Extends the existing per-snapshot "rescued" path to the total-failure case.
@iRanadheer
iRanadheer force-pushed the pr/05-quality-tool branch from 9d403bc to 45f1d73 Compare July 30, 2026 08:55
…port)

compliance_summary() read has_capture_failed() directly and only loaded dated
snapshots, so a capture-failed CF/proxy domain that build_report_data rescued via
its crawl-captured robots still showed 'failed' in audit_<project>.md and the
dashboard — disagreeing with compliance_<project>.md (which the docstring says can
never happen). Feed it the already-rescued captured recs (computing them if not
passed) so a domain answered via crawl-captured robots is not reported failed; no
crawl witness -> still failed, nothing fabricated. +2 tests.
… → graded)

The audit MD's per-spider compliance table and the dashboard's compliance tab
both ranked ‼️ capture-failed and ❓ not-checked rows LAST, so the most
actionable rows (nothing known about them) sat at the bottom. Rank them first in
both surfaces so the section reads most- to least-problematic.
Both tables appended NOT-CHECKED domains at the bottom; move them to the top so
compliance_<project>.md reads most- to least-problematic, matching the audit MD
and the dashboard's compliance tab.
The audit's third lens cost more than it returned, so it is cut before
merge: external_pdf.py re-read the whole crawl corpus a second time with
no scan cache, produced a multi-megabyte per-host report, and its
dashboard tab needed a per-project host-exclusion layer (pdf_exclude.json
plus a JS matcher, a chips UI and two JSON export paths) just to stay
legible. Neither export had a consumer.

Removed: core/quality/external_pdf.py, dashboard/pdfs_tab.py and their
tests; the lens call in cli/audit.py; the PDFs tab, its select bar and
the pdf_exclude scaffolding in dashboard/render.py; the PDF-only CSS/JS
in dashboard/assets.py (URL island, exclusion matcher, share slider,
include-list selbar mode, infobox).

PDF handling itself is untouched: PDF_MODE still records every PDF link
as a URL-only row, and the audit keeps the pdf / (N ext) column, the
same-org vs external split, the pdf-only flag and the PDF-provenance
split out of versions. overview's PDF stats and the robots
"disallows PDFs" check are unaffected. The URLs stay in the crawl
output, so the per-host view can be rebuilt if it ever earns its keep.

render_dashboard() loses its pdf_spiders argument and write_dashboard()
its pdf_result; nothing outside this branch calls them.

Verified on a real 39-spider project (--no-fetch --no-compliance, static
data): crawl_audit.csv and coverage.csv byte-identical before and after;
audit_<project>.md differs only in the pdf glossary line that pointed at
the deleted report; dashboard 3,853,820 -> 279,202 bytes.
Only the PDFs tab emitted `tr.fx-group`, so with that tab gone the
group/collapse machinery in setupTable is unreachable: the `grouped` /
`openG` state, the group branches in apply(), the group-header click
handler and the fx-group CSS. Coverage and Compliance are flat tables and
never entered any of it.

Mechanical; kept out of the removal commit so it can be reviewed or
reverted on its own.
The write-up led with a cost argument that was not the decision: the lens
was dropped because it was convoluted and not particularly useful, and
external-source scanning is moving elsewhere.
@MirjamOdile

Copy link
Copy Markdown
Contributor Author

Dropped before merge: the external-PDF lens

Two commits remove the audit's third lens — the external-PDF host report (core/quality/external_pdf.py → _audit/external_pdf_report.md) and the dashboard's PDFs tab, with its pdf_exclude.json host-exclusion layer.

It was convoluted and not particularly useful in practice, and scanning external sources is moving elsewhere, so it does not need to ship here.

PDF handling itself is unchanged — PDF_MODE still records every PDF link as a URL-only row, and the audit keeps the pdf / (N ext) column, the pdf-only flag, the PDF-provenance split out of versions, overview's PDF stats and the robots "disallows PDFs" check. The URLs stay in the crawl output.

  • 1e0ef75 — remove the lens, the tab and the docs that describe them.
  • e1184b6 — remove the table-grouping JS that only the PDFs tab used.

render_dashboard() loses its pdf_spiders argument; nothing outside this branch calls it.

Gate is green (black, flake8, unit suite). On a real project the audit's CSVs are byte-identical before and after, audit_.md differs only in the pdf glossary line that pointed at the deleted report, and the dashboard drops from 3.9 MB to 279 KB.

The inert-template scaffolding went with the external-PDF lens; zero code
references remain, so the quality doc's claim was stale.
… report

The flag used to filter the spider set before the outputs were written, so a
one-spider spot-check silently replaced the whole project report. --only now
narrows only the work: un-scored spiders' rows merge back from the previous
run's crawl_audit.csv (typed read via the shared CSV_FIELDS map — the CSV is a
lossless copy of the audit rows) and, for the overview, from a new
_audit/overview_rows.json row store (its CSV flattens the nested detail).
The deleted-folder prune runs on the full spider set before the filter, and
the compliance summary covers all spiders while capture stays narrowed to
--only. Missing store: warn and fall back to the old narrowed output.

No staleness stamping: rows carried over keep the semantics of the run that
wrote them; re-run without --only after a scoring change.
@MirjamOdile

Copy link
Copy Markdown
Contributor Author

Added one more commit: audit/overview --only previously narrowed the report, not just the work — a one-spider spot-check replaced the whole project-wide report.

Now --only recomputes only the named spiders and merges every other spider's row back from the previous run's stored output (crawl_audit.csv, which is a lossless copy of the audit rows; a new _audit/overview_rows.json store for the overview, whose CSV flattens nested detail).

The deleted-folder prune still works project-wide under --only, and the compliance summary covers all spiders while capture stays narrowed. If no previous report exists, it warns and falls back to the old narrowed output. Covered by tests/unit/test_only_merge.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant