Snapshot before the quality program (relevance gates, tiered mapping, QA linter). v1 is the immutable before/after reference; evidence crawl was at ~175/3039 occupations when tagged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDKeXvpT6tENSvyQGLV1Uq
189 lines
7.8 KiB
Python
189 lines
7.8 KiB
Python
"""Evidence-Auswertung: coverage report over the job-ad evidence stage.
|
|
|
|
Reads progress.json + the MSSQL evidence store and writes
|
|
docs/evidence-report.md — runnable at any time (interim or final state).
|
|
|
|
Sections:
|
|
- coverage: fetched/extracted/aggregated/failed/thin-market per status
|
|
- volume: ads, unique employers, entities by type
|
|
- budget: JSearch requests spent vs. lifetime cap
|
|
- distributions: seniority, country, top occupations by ads
|
|
- extraction engines: Claude (in-session, flagship + QA) vs Ollama (mass run)
|
|
|
|
Usage: python pipeline/evidence_report.py [--push]
|
|
--push additionally commits the report into skills-core/marketplace
|
|
as EVIDENCE.md (uses p4_publish push helpers).
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
from collections import Counter
|
|
from datetime import date
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
import progress
|
|
from db import connect
|
|
|
|
BASE = os.path.join(os.path.dirname(__file__), "..")
|
|
OUT = os.path.join(BASE, "docs", "evidence-report.md")
|
|
|
|
# The recruiter flagship (350 ads) was extracted in-session by Claude before
|
|
# the local mass pipeline existed; everything after runs on Ollama gemma3:27b.
|
|
CLAUDE_EXTRACTED_SLUGS = {"recruitment-consultant"}
|
|
|
|
|
|
def q(cur, sql, *args):
|
|
cur.execute(sql, *args)
|
|
return cur.fetchall()
|
|
|
|
|
|
def main():
|
|
state = progress.load()
|
|
occ = state["occupations"]
|
|
ev = Counter(s.get("evidence") for s in occ.values())
|
|
ads_per_occ = {slug: s.get("ads", 0) for slug, s in occ.items() if s.get("ads")}
|
|
thin = [(slug, s.get("error", "")) for slug, s in occ.items()
|
|
if s.get("evidence") == "failed" and "thin market" in (s.get("error") or "")]
|
|
other_failed = [(slug, s.get("error", "")) for slug, s in occ.items()
|
|
if s.get("evidence") == "failed" and "thin market" not in (s.get("error") or "")]
|
|
|
|
cn = connect()
|
|
cur = cn.cursor()
|
|
n_jobs = q(cur, "SELECT COUNT(*) FROM evidence_job")[0][0]
|
|
n_occ_jobs = q(cur, "SELECT COUNT(DISTINCT occupation_slug) FROM evidence_job")[0][0]
|
|
n_emp = q(cur, "SELECT COUNT(DISTINCT employer) FROM evidence_job WHERE employer IS NOT NULL AND employer<>''")[0][0]
|
|
n_ent = q(cur, "SELECT COUNT(*) FROM evidence_entity")[0][0]
|
|
ent_types = q(cur, "SELECT entity_type, COUNT(*) FROM evidence_entity GROUP BY entity_type ORDER BY 2 DESC")
|
|
seniority = q(cur, "SELECT ISNULL(NULLIF(seniority,''),'n/a'), COUNT(*) FROM evidence_job GROUP BY ISNULL(NULLIF(seniority,''),'n/a') ORDER BY 2 DESC")
|
|
country = q(cur, "SELECT ISNULL(NULLIF(country,''),'?'), COUNT(*) FROM evidence_job GROUP BY ISNULL(NULLIF(country,''),'?') ORDER BY 2 DESC")
|
|
top_occ = q(cur, "SELECT TOP 20 occupation_slug, COUNT(*) FROM evidence_job GROUP BY occupation_slug ORDER BY 2 DESC")
|
|
cn.close()
|
|
|
|
claude_jobs = sum(n for s, n in
|
|
[(slug, ads_per_occ.get(slug, 0)) for slug in CLAUDE_EXTRACTED_SLUGS])
|
|
# recruiter ads live in the DB — count them exactly
|
|
cn = connect(); cur = cn.cursor()
|
|
claude_db = q(cur, "SELECT COUNT(*) FROM evidence_job WHERE occupation_slug IN ('recruitment-consultant')")[0][0]
|
|
cn.close()
|
|
ollama_db = n_jobs - claude_db
|
|
|
|
spent = state.get("jsearch_requests_total", 0)
|
|
cap = progress.JSEARCH_BUDGET_TOTAL
|
|
total_occ = len(occ)
|
|
|
|
L = []
|
|
L.append(f"# Job-ad evidence — coverage report")
|
|
L.append("")
|
|
L.append(f"_Generated {date.today().isoformat()} by `pipeline/evidence_report.py`. "
|
|
f"Re-run any time for the current state._")
|
|
L.append("")
|
|
L.append("## Coverage")
|
|
L.append("")
|
|
L.append("| Evidence status | Occupations |")
|
|
L.append("|---|---|")
|
|
for k in ("done", "fetching", "extracting", "aggregating", "failed", "pending"):
|
|
if ev.get(k):
|
|
L.append(f"| {k} | {ev[k]} |")
|
|
L.append(f"| **total catalog** | **{total_occ}** |")
|
|
L.append("")
|
|
L.append(f"- Occupations with ads in the evidence store: **{n_occ_jobs}**")
|
|
L.append(f"- Thin markets (fewer than 5 usable ads): **{len(thin)}**")
|
|
L.append(f"- Failed for other reasons: **{len(other_failed)}**")
|
|
L.append("")
|
|
L.append("## Volume")
|
|
L.append("")
|
|
L.append(f"| Metric | Value |")
|
|
L.append(f"|---|---|")
|
|
L.append(f"| Job ads stored | {n_jobs:,} |")
|
|
L.append(f"| Distinct employers | {n_emp:,} |")
|
|
L.append(f"| Extracted entities | {n_ent:,} |")
|
|
for t, n in ent_types:
|
|
L.append(f"| — {t} | {n:,} |")
|
|
L.append("")
|
|
L.append("## API budget")
|
|
L.append("")
|
|
L.append(f"- JSearch requests spent: **{spent:,} / {cap:,}** "
|
|
f"({round(100.0*spent/cap, 1)} % of the lifetime budget)")
|
|
L.append("- Every request is counted BEFORE the HTTP call "
|
|
"(`progress.spend_request`, hard cap).")
|
|
L.append("")
|
|
L.append("## Distributions")
|
|
L.append("")
|
|
L.append("**Seniority** (per ad):")
|
|
L.append("")
|
|
L.append("| Seniority | Ads |")
|
|
L.append("|---|---|")
|
|
for s, n in seniority:
|
|
L.append(f"| {s} | {n:,} |")
|
|
L.append("")
|
|
L.append("**Country**:")
|
|
L.append("")
|
|
L.append("| Country | Ads |")
|
|
L.append("|---|---|")
|
|
for c, n in country:
|
|
L.append(f"| {c} | {n:,} |")
|
|
L.append("")
|
|
L.append("**Top 20 occupations by ads:**")
|
|
L.append("")
|
|
L.append("| Occupation | Ads |")
|
|
L.append("|---|---|")
|
|
for slug, n in top_occ:
|
|
L.append(f"| {slug} | {n:,} |")
|
|
L.append("")
|
|
L.append("## Extraction engines")
|
|
L.append("")
|
|
L.append("| Engine | Ads extracted | Share | Role |")
|
|
L.append("|---|---|---|---|")
|
|
tot = max(1, n_jobs)
|
|
L.append(f"| Claude (in-session) | {claude_db:,} | {round(100.0*claude_db/tot,1)} % | "
|
|
f"flagship package (recruitment-consultant), prompt/validator design, "
|
|
f"2 % QA spot checks on every batch |")
|
|
L.append(f"| Ollama gemma3:27b (RTX-3090 box) | {ollama_db:,} | {round(100.0*ollama_db/tot,1)} % | "
|
|
f"mass extraction of the full catalog (`extract_local.py`, "
|
|
f"strict JSON schema, validator with final word) |")
|
|
L.append("")
|
|
L.append("_Thin-market occupations (kept for transparency):_")
|
|
L.append("")
|
|
for slug, err in sorted(thin)[:50]:
|
|
L.append(f"- {slug} — {err}")
|
|
if len(thin) > 50:
|
|
L.append(f"- … and {len(thin)-50} more")
|
|
L.append("")
|
|
|
|
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
|
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write("\n".join(L))
|
|
print(f"report written: {OUT}")
|
|
print(f"coverage: {n_occ_jobs}/{total_occ} occupations, {n_jobs} ads, "
|
|
f"{n_ent} entities, budget {spent}/{cap}")
|
|
|
|
if "--push" in sys.argv:
|
|
import shutil, tempfile
|
|
import p4_publish as p4
|
|
tmp = tempfile.mkdtemp(prefix="sfev_")
|
|
try:
|
|
# marketplace repo: add/refresh EVIDENCE.md next to README
|
|
import subprocess
|
|
url = p4.GITEA_URL.replace("://", f"://gitadmin:{p4.TOKEN}@") + "/skills-core/marketplace.git"
|
|
subprocess.run(["git", "clone", "--depth", "1", url, tmp],
|
|
check=True, capture_output=True, text=True)
|
|
shutil.copy(OUT, os.path.join(tmp, "EVIDENCE.md"))
|
|
def g(*a):
|
|
subprocess.run(["git", *a], cwd=tmp, check=True, capture_output=True, text=True)
|
|
g("config", "user.name", "skillfactor-pipeline")
|
|
g("config", "user.email", "pipeline@noreply.zeiterfassung.cloud")
|
|
g("add", "-A")
|
|
r = subprocess.run(["git", "commit", "-m", "docs: evidence coverage report"],
|
|
cwd=tmp, capture_output=True, text=True)
|
|
if r.returncode == 0:
|
|
g("push", "origin", "HEAD")
|
|
print("EVIDENCE.md pushed to skills-core/marketplace")
|
|
else:
|
|
print("no changes to push")
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|