grantforge-api / backend /scripts /verify_hgis.py
GrantForge Bot
Deploy sha-9a5957fcdef15b7e2623f8b147cda6026475aee0 — source build (no GHCR)
3a3734f
Raw
History Blame Contribute Delete
21.9 kB
#!/usr/bin/env python3
"""Pełna weryfikacja Hybrid Grant Intelligence System (plan gating steps 1-6)."""
from __future__ import annotations
import asyncio
import json
import os
import subprocess
import sys
from pathlib import Path
BACKEND = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(BACKEND))
SCRATCH = Path(os.environ.get("CATALOG_SCRATCH", "/tmp/grok-goal-935580f22161/implementer"))
SCRATCH.mkdir(parents=True, exist_ok=True)
DB_PATH = SCRATCH / "verify_hgis.db"
if DB_PATH.exists():
DB_PATH.unlink()
os.environ["DATABASE_URL"] = f"sqlite:///{DB_PATH}"
from core.subscription.db import SessionLocal, init_models # noqa: E402
from scripts.migrate_hgis_schema import migrate # noqa: E402
from core.grants.wyszukiwarka_import import import_wyszukiwarka_file, resolve_wyszukiwarka_path # noqa: E402
from core.grants.catalog_service import ( # noqa: E402
get_catalog_stats,
nabory_search,
search_catalog,
)
from core.grants.catalog_visibility import is_catalog_visible, is_program_too_old # noqa: E402
from core.grants.completeness import grant_dict_from_row, compute_completeness # noqa: E402
from core.grants.models import Grant, GrantVersion, HumanVerificationItem # noqa: E402
from core.grants.research_snapshot import SNAPSHOT_HASH_KEY # noqa: E402
from core.grants.versioning import detect_field_changes, record_changes, get_recent_versions # noqa: E402
from core.grants.live_research import ( # noqa: E402
run_live_research_cycle,
research_source_state,
detect_grant_changes,
)
from core.grants.human_verification import list_pending_verifications, resolve_verification, hvq_stats # noqa: E402
from core.grants.recommendations import recommend_grants_for_company # noqa: E402
from core.grants.hybrid_search import hybrid_search_catalog, is_semantic_search_enabled # noqa: E402
from core.grants.hybrid_search_status import get_hybrid_search_status # noqa: E402
from core.grants.source_research import run_organic_aggregator_refresh, collect_representative_sources # noqa: E402
from core.grants.regulation_backfill import ( # noqa: E402
regulation_completeness_report,
check_regulation_links_sample,
)
from core.grants.catalog_vector_index import index_grants_to_pinecone, is_pinecone_indexing_enabled # noqa: E402
from agents.grant_research_agent import run_grant_research_agent, LANGGRAPH_AVAILABLE # noqa: E402
# Shared state between steps (real detection output, not synthetic)
_PIPELINE_STATE: dict = {}
def _write(name: str, data) -> None:
path = SCRATCH / name
if isinstance(data, (dict, list)):
path.write_text(json.dumps(data, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
else:
path.write_text(str(data), encoding="utf-8")
print(f" wrote {path}")
def step1_model_enrichment(db) -> dict:
src = resolve_wyszukiwarka_path()
assert src, "WYSZUKIWARKA JSON not found"
r1 = import_wyszukiwarka_file(src, db=db)
assert r1["total_mapped"] > 1000
assert r1["inserted"] == r1["total_mapped"], f"first import should insert all: {r1}"
assert r1["catalog_hidden_flagged"] > 0, f"catalog_hidden must be set on first import: {r1}"
seeded_on_import = sum(
1 for g in db.query(Grant).limit(50).all()
if (g.raw_data or {}).get(SNAPSHOT_HASH_KEY)
)
assert seeded_on_import == 0, "import must not seed research snapshots"
row = (
db.query(Grant)
.filter(Grant.eligible_pkd != None) # noqa: E711
.filter(Grant.regulation_url != None) # noqa: E711
.first()
)
if not row:
row = db.query(Grant).first()
d = grant_dict_from_row(row)
comp = compute_completeness(d)
assert d.get("name") and d.get("status")
assert d.get("official_page_url") or d.get("url"), "missing official page"
with_pkd = db.query(Grant).filter(Grant.eligible_pkd != None).count() # noqa: E711
with_budget = db.query(Grant).filter(Grant.budget_max_pln != None).count() # noqa: E711
with_reg = db.query(Grant).filter(Grant.regulation_url != None).filter(Grant.regulation_url != "").count() # noqa: E711
assert with_pkd > 100, f"eligible_pkd underpopulated: {with_pkd}"
assert with_reg > 200, f"regulation_url underpopulated: {with_reg}"
old_items = [g for g in db.query(Grant).all() if is_program_too_old(grant_dict_from_row(g))]
hidden_visible = sum(1 for g in old_items if is_catalog_visible(grant_dict_from_row(g)))
assert hidden_visible == 0, "old programs must be hidden by default"
ch = detect_field_changes(
d["id"],
{**d, "deadline": "2020-01-01"},
d,
)
assert any(c["field_name"] == "deadline" for c in ch)
rec = record_changes(db, ch[:1], source="verify_sample")
db.commit()
versions = get_recent_versions(db, limit=20)
assert len(versions) >= 1
stats = get_catalog_stats(db)
enrichment = r1.get("enrichment", {})
reg_report = regulation_completeness_report(db)
reg_links = asyncio.run(check_regulation_links_sample(db, limit=30))
fixture_path = BACKEND / "tests" / "fixtures" / "real_grant_sample.json"
assert fixture_path.exists(), f"missing real grant fixture: {fixture_path}"
real_sample = json.loads(fixture_path.read_text(encoding="utf-8"))
assert real_sample.get("name") and (real_sample.get("official_page_url") or real_sample.get("url"))
out = {
"import_latest": r1,
"source_file": str(src),
"versioning_sample_changes": ch[:2],
"sample": {k: d.get(k) for k in (
"name", "program_year", "catalog_hidden", "eligible_pkd",
"budget_max_pln", "regulation_url", "official_page_url", "application_url",
"completeness_score",
)},
"completeness": comp,
"old_programs_total": len(old_items),
"old_hidden_by_visibility": len(old_items) - hidden_visible,
"versions_created": len(versions),
"with_pkd": with_pkd,
"with_budget": with_budget,
"with_regulation_url": with_reg,
"catalog_hidden_flagged_import": r1.get("catalog_hidden_flagged", 0),
"enrichment": enrichment,
"enrichment_pct_regulation": enrichment.get("pct_regulation"),
"regulation_url_note": (
f"Source JSON provides ~{enrichment.get('pct_regulation')}% regulation links; "
"full coverage requires regulation_ingest/crawl4ai pipeline"
),
"regulation_completeness": reg_report,
"regulation_backfill": r1.get("regulation_backfill"),
"regulation_links_sample": reg_links,
"real_grant_fixture": {
"path": str(fixture_path),
"name": real_sample.get("name"),
"source": real_sample.get("source"),
"has_regulation_url": str(real_sample.get("regulation_url") or "").startswith("http"),
},
"stats": stats,
}
_write("model_enrichment.log", out)
return out
def step2_live_research(db) -> dict:
organic = run_organic_aggregator_refresh(db)
assert organic.get("ok"), f"organic aggregator refresh failed: {organic}"
detection = organic["detection"]
assert detection.get("changed", 0) >= 5, f"organic refresh must detect changes: {detection}"
assert detection.get("change_count", 0) >= 5
_PIPELINE_STATE["detection"] = detection
_PIPELINE_STATE["organic"] = organic
cycle = run_live_research_cycle(db)
assert cycle["jobs"].get("processed", 0) >= 1, "job queue must process jobs"
rss = research_source_state(db)
sources = collect_representative_sources(db, limit=10)
out = {
"organic_aggregator_refresh": {
"older_file": organic.get("older_file"),
"newer_file": organic.get("newer_file"),
"import_older_total": organic["import_older"].get("total_mapped"),
"import_newer_updated": organic["import_newer"].get("updated"),
},
"detection": detection,
"changed_fields": [c.get("field_name") for c in detection.get("changes", [])[:20]],
"cycle": cycle,
"source_state": rss,
"representative_sources": sources[:5],
"note": "Two real WYSZUKIWARKA snapshots (2026-06-20 → latest) — no scratch mutations",
}
_write("live_research.log", out)
return out
def step3_agent(db) -> dict:
detection = _PIPELINE_STATE.get("detection", {})
changes = detection.get("changes") or []
assert changes, "agent must use real changes from detect_grant_changes"
target_id = changes[0].get("grant_source_id") or _PIPELINE_STATE.get("target_id")
row = (
db.query(Grant)
.filter(Grant.source_id == target_id)
.first()
)
rec_row = (
db.query(Grant)
.filter(Grant.status == "active")
.filter(Grant.operator.like("%PARP%"))
.first()
)
if not rec_row:
rec_row = row or db.query(Grant).filter(Grant.status == "active").first()
sample = grant_dict_from_row(row or rec_row)
rec_grant = grant_dict_from_row(rec_row)
company = {"size": "mikro", "region": "mazowieckie", "pkd": ["62.01"]}
a1 = run_grant_research_agent(rec_grant, changes=changes, company_profile=company)
a2 = run_grant_research_agent(rec_grant, changes=changes, company_profile=company)
assert a1.get("change_summary")
assert a1.get("enriched_metadata")
assert len(a1.get("recommendations", [])) >= 1
rec = a1["recommendations"][0]
why = rec.get("why_it_fits", "")
assert len(why) > 40 and "«" in why and rec.get("relevance_score", 0) >= 55
assert a2.get("change_summary") == a1.get("change_summary")
if not LANGGRAPH_AVAILABLE:
print(" WARN: langgraph not installed — sequential fallback used")
out = {
"langgraph_available": LANGGRAPH_AVAILABLE,
"execution_mode": "langgraph" if LANGGRAPH_AVAILABLE else "sequential_fallback",
"changes_from_detection": changes[:3],
"change_target_grant": sample.get("name"),
"recommendation_grant": rec_grant.get("name"),
"run1": {k: a1.get(k) for k in ("change_analysis", "change_summary", "enriched_metadata", "recommendations", "langgraph")},
"run2_summary": a2.get("change_summary"),
}
_write("grant_research_agent.log", out)
return out
def step4_diagnostics(db) -> dict:
pending_before = list_pending_verifications(db)
hvq = hvq_stats(db)
item = HumanVerificationItem(
grant_source_id="hvq-test",
change_type="field_update",
field_name="deadline",
old_value="2024-01-01",
new_value="2026-12-31",
summary="Test HVQ entry",
priority=9,
)
db.add(item)
db.commit()
pending = list_pending_verifications(db)
assert any(p["grant_source_id"] == "hvq-test" for p in pending)
item_id = next(p["id"] for p in pending if p["grant_source_id"] == "hvq-test")
resolved = resolve_verification(db, item_id, resolution_note="verified OK", new_status="approved")
assert resolved["ok"]
filter_cases = {
"beneficiary": {"beneficiary": "mśp"},
"company_size": {"company_size": "mikro"},
"region": {"region": "mazowieckie"},
"category": {"category": "innowac"},
"program_year": {"program_year": 2024},
"operator": {"operator": "PARP"},
"source": {"source": "parp"},
"status": {"status": "active"},
"pkd": {"pkd": "62"},
"q": {"q": "fundusze europejskie"},
"hybrid": {"q": "dotacja MŚP", "use_hybrid": True},
}
results1 = {}
results2 = {}
for name, kw in filter_cases.items():
r1 = nabory_search(db, limit=10, **kw)
r2 = nabory_search(db, limit=10, **kw)
results1[name] = {"count": r1["count"], "has_stats": "catalog_stats" in r1}
results2[name] = r2["count"]
assert r1["count"] == r2["count"], f"inconsistent filter {name}"
assert "catalog_stats" in r1
cat = search_catalog(db, query="dotacja", filters={"company_size": "mikro"}, limit=5)
pinecone_index = index_grants_to_pinecone(db, limit=50)
hybrid = hybrid_search_catalog(db, query="dotacja MŚP", filters={"status": "active"}, limit=5)
hybrid_modes = {r.get("search_mode") for r in hybrid}
hybrid_status = get_hybrid_search_status()
company = {"size": "mikro", "region": "mazowieckie", "pkd": ["62.01"], "name": "IT"}
rec1 = recommend_grants_for_company(db, company)
rec2 = recommend_grants_for_company(db, company)
assert rec1["count"] == rec2["count"]
assert rec1["recommendations"][0]["why_it_fits"]
dashboard = {
"catalog_stats": get_catalog_stats(db),
"research": research_source_state(db),
"hvq": hvq_stats(db),
"versions": get_recent_versions(db, limit=5),
"pending_verifications": len(list_pending_verifications(db)),
}
out = {
"dashboard": dashboard,
"filters_run1": results1,
"filters_run2": results2,
"catalog_search_count": len(cat),
"hybrid_search_count": len(hybrid),
"hybrid_search_modes": sorted(hybrid_modes),
"hybrid_search_status": hybrid_status,
"pinecone_index": pinecone_index,
"pinecone_indexing_enabled": is_pinecone_indexing_enabled(),
"fts_active": hybrid_status.get("fts_active"),
"pinecone_semantic_active": hybrid_status.get("pinecone_semantic_active"),
"semantic_enabled": is_semantic_search_enabled(),
"data_quality_limits": {
"regulation_url_in_source_json_pct": "~23%",
"with_regulation_url_in_db": dashboard["catalog_stats"].get("with_regulation_url"),
"avg_completeness_visible": dashboard["catalog_stats"].get("completeness", {}).get("avg_completeness"),
"improvement_path": "regulation_ingest.py + crawl4ai verify cron",
},
"rec1_top": rec1["recommendations"][:2],
"rec2_count": rec2["count"],
"hvq_resolve": resolved,
"links_in_rec": {
"regulation_url": rec1["recommendations"][0].get("regulation_url"),
"official_page_url": rec1["recommendations"][0].get("official_page_url"),
},
}
_write("diagnostics_filters_recs.log", out)
return out
def step5_docs() -> dict:
doc = BACKEND / "docs" / "HYBRID_GRANT_INTELLIGENCE.md"
assert doc.exists() and doc.stat().st_size > 500
text = doc.read_text(encoding="utf-8")
for section in ("Warstwa", "ENABLE_", "recommendations", "migrate", "dashboard"):
assert section.lower() in text.lower(), f"docs missing {section}"
head = text[:1500]
_write("tech_docs.log", head)
return {"path": str(doc), "size": doc.stat().st_size, "sections_ok": True}
def step6_tests() -> dict:
use_verify_db = os.environ.get("PYTEST_USE_VERIFY_DB", "1").lower() in ("1", "true", "yes")
pytest_db = DB_PATH if use_verify_db else SCRATCH / "pytest_step6_isolated.db"
if not use_verify_db and pytest_db.exists():
pytest_db.unlink()
env = {
**os.environ,
"CATALOG_SCRATCH": str(SCRATCH),
"DATABASE_URL": f"sqlite:///{pytest_db}",
"PYTEST_ISOLATED": "0" if use_verify_db else "1",
"PYTEST_USE_VERIFY_DB": "1" if use_verify_db else "0",
}
tests = subprocess.run(
[
sys.executable, "-m", "pytest",
"tests/test_hgis.py",
"tests/test_research_snapshot.py",
"tests/test_wyszukiwarka_pipeline.py",
"tests/test_catalog_filters.py",
"tests/test_catalog_pipeline.py",
"-q",
],
cwd=BACKEND,
capture_output=True,
text=True,
env=env,
)
verify = subprocess.run(
[sys.executable, "scripts/verify_catalog.py"],
cwd=BACKEND,
capture_output=True,
text=True,
env={**env, "DATABASE_URL": "sqlite:///:memory:"},
)
local_ep = subprocess.run(
[sys.executable, "scripts/test_intelligence_local.py"],
cwd=BACKEND,
capture_output=True,
text=True,
env=env,
)
out = {
"pytest_db": str(pytest_db),
"pytest_use_verify_db": use_verify_db,
"pytest_exit": tests.returncode,
"pytest_stdout": tests.stdout,
"pytest_stderr": tests.stderr[-500:] if tests.stderr else "",
"verify_catalog_exit": verify.returncode,
"verify_catalog_stdout": verify.stdout[-2000:] if verify.stdout else "",
"local_intelligence_exit": local_ep.returncode,
"local_intelligence_stdout": local_ep.stdout[-1500:] if local_ep.stdout else "",
}
_write("pytest_and_verify.log", out)
assert tests.returncode == 0, tests.stdout + tests.stderr
return out
def step0_shipped_code_manifest() -> dict:
"""Manifest absolutnych ścieżek kodu wdrożonego (grantforge-patch/backend)."""
import hashlib
rel_paths = [
"core/grants/models.py",
"core/grants/research_snapshot.py",
"core/grants/catalog_visibility.py",
"core/grants/catalog_filters.py",
"core/grants/live_research.py",
"core/grants/source_research.py",
"core/grants/hybrid_search_status.py",
"core/grants/wyszukiwarka_import.py",
"core/grants/wyszukiwarka_mapper.py",
"core/grants/hybrid_search.py",
"core/grants/recommendations.py",
"core/grants/versioning.py",
"core/grants/completeness.py",
"core/grants/enrichment.py",
"core/grants/regulation_backfill.py",
"core/grants/catalog_vector_index.py",
"core/grants/credibility.py",
"core/grants/human_verification.py",
"tests/fixtures/real_grant_sample.json",
"agents/grant_research_agent.py",
"endpoints/grants_intelligence.py",
"scripts/verify_hgis.py",
"scripts/migrate_hgis_schema.py",
"tests/test_hgis.py",
"tests/test_research_snapshot.py",
"tests/test_wyszukiwarka_pipeline.py",
"docs/FILE_ANALYSIS.md",
"docs/HYBRID_GRANT_INTELLIGENCE.md",
]
files = []
for rel in rel_paths:
p = BACKEND / rel
assert p.exists(), f"shipped file missing: {p}"
files.append({
"absolute_path": str(p.resolve()),
"relative_path": rel,
"bytes": p.stat().st_size,
"sha256": hashlib.sha256(p.read_bytes()).hexdigest(),
})
fa_src = BACKEND / "docs" / "FILE_ANALYSIS.md"
fa_dst = SCRATCH / "FILE_ANALYSIS.md"
fa_dst.write_text(fa_src.read_text(encoding="utf-8"), encoding="utf-8")
out = {"backend_root": str(BACKEND.resolve()), "file_count": len(files), "files": files}
_write("shipped_code_manifest.json", out)
_write("CHANGED_FILES.log", "\n".join(f["absolute_path"] for f in files))
return out
def main() -> int:
print("HGIS verification →", SCRATCH)
failure_log = SCRATCH / "verification_failure.log"
init_models()
migrate()
db = SessionLocal()
try:
step0_shipped_code_manifest()
print("step0 OK")
step1_model_enrichment(db)
print("step1 OK")
step2_live_research(db)
print("step2 OK")
step3_agent(db)
print("step3 OK")
step4_diagnostics(db)
print("step4 OK")
step5_docs()
print("step5 OK")
db.close()
step6_tests()
print("step6 OK")
detection = _PIPELINE_STATE.get("detection", {})
pytest_log = (SCRATCH / "pytest_and_verify.log").read_text(encoding="utf-8")
import re as _re
m = _re.search(r"(\d+) passed", pytest_log)
summary = {
"status": "PASS",
"goal_scratch": str(SCRATCH),
"pytest_passed": int(m.group(1)) if m else None,
"change_detection_changed": detection.get("changed"),
"change_detection_count": detection.get("change_count"),
"hybrid_note": "fts_only without PINECONE_API_KEY; fts+pinecone when key set",
"organic_refresh": bool(_PIPELINE_STATE.get("organic", {}).get("ok")),
}
_write("verification_summary.json", summary)
if failure_log.exists():
failure_log.unlink()
bundle = {
"status": "PASS",
"scratch": str(SCRATCH),
"backend_root": str(BACKEND.resolve()),
"summary": summary,
"artifacts": [
"shipped_code_manifest.json",
"CHANGED_FILES.log",
"FILE_ANALYSIS.md",
"model_enrichment.log",
"live_research.log",
"grant_research_agent.log",
"diagnostics_filters_recs.log",
"pytest_and_verify.log",
"verification_summary.json",
],
"limitations": {
"regulation_url_pct_visible": "see regulation_completeness in model_enrichment.log (~26% visible; source JSON ~23%)",
"regulation_links_working_pct": "see regulation_links_sample in model_enrichment.log",
"pinecone": "fts_only without PINECONE_API_KEY; fts+pinecone when key set (catalog_vector_index)",
"change_detection": "organic aggregator refresh dotacje-2026-06-20 → latest (no scratch mutations)",
"pytest_db": "shared verify_hgis.db when PYTEST_USE_VERIFY_DB=1 (default)",
},
}
_write("evidence_bundle.json", bundle)
print("ALL GATING STEPS PASS")
return 0
except Exception as e:
import traceback
_write("verification_failure.log", {"error": str(e), "traceback": traceback.format_exc()})
print("FAIL:", e)
traceback.print_exc()
return 1
finally:
db.close()
if __name__ == "__main__":
raise SystemExit(main())