{"snapshot":{"snapshot_at":"2026-09-19T22:15:32.963231+00:00","summary":{"holding":10,"total_axioms":17,"violating_hard":2,"violating_soft":1,"violating_freshness":4},"axioms":{"A1":{"name":"one_domain_one_business","sample":[],"severity":"hard","violations":0},"A2":{"name":"unique_active_slug","sample":[],"severity":"hard","violations":0},"A3":{"name":"no_ugly_ingest_suffix","sample":[{"id":26232,"name":"Databricks","slug":"databricks-com"}],"severity":"soft","violations":1,"autofix_available":"scripts/canonicalize-business-slugs.mjs --all-ugly --execute"},"A4":{"name":"unique_linkedin_url","sample":[],"severity":"hard","violations":0},"A5":{"top":[{"slug":"huggingface-co","links":49},{"slug":"independent","links":8},{"slug":"databricks","links":6},{"slug":"figma","links":1}],"name":"team_link_company_resolves","severity":"hard","violations":64,"distinct_slugs":4},"A6":{"name":"team_link_person_resolves","sample":[],"severity":"hard","violations":0},"A7":{"name":"at_most_one_primary_per_person","sample":[],"severity":"hard","violations":0},"D1":{"name":"domain_format_clean","sample":[],"severity":"hard","violations":0},"F1":{"name":"axiom_snapshot_fresh","severity":"freshness","age_hours":252.64,"latest_at":"2026-09-09T09:36:51.810969+00:00","violations":1,"max_age_hours":26},"F2":{"name":"dataset_snapshot_fresh","severity":"freshness","age_hours":118.26,"latest_at":"2026-09-14","violations":1,"max_age_hours":36},"F3":{"name":"changelog_fresh","severity":"freshness","age_hours":852.76,"latest_at":"2026-08-15T09:29:56.965203+00:00","violations":1,"max_age_hours":168},"F4":{"name":"no_stuck_dispatch_jobs","sample":[],"severity":"freshness","violations":0,"max_running_hours":1},"F5":{"name":"mcp_smoke_fresh","severity":"freshness","age_hours":252.64,"latest_at":"2026-09-09T09:37:15.872384+00:00","violations":1,"max_age_hours":26},"G2":{"skipped":"no runtime check implemented yet"},"I1":{"name":"ingest_uses_create_or_match_helper","sample":[],"severity":"hard","violations":0},"I2":{"skipped":"static-analysis axiom; not evaluated by runner"},"A10":{"name":"active_business_has_name_and_slug","sample":[],"severity":"hard","violations":0},"A11":{"name":"active_business_has_profile","sample":[{"id":26166,"name":"Telaya Wine Co","slug":"telaya-wine-co-telayawine"},{"id":26167,"name":"Greenwood's Ski Haus","slug":"greenwood-s-ski-haus"},{"id":26168,"name":"ABU Games","slug":"abu-games"},{"id":26170,"name":"Phoenix Fire Games","slug":"phoenix-fire-games-phoenixfiregames"},{"id":26172,"name":"D&B Supply","slug":"d-b-supply-dbsupply"},{"id":26173,"name":"FarWest Garden Center","slug":"farwest-garden-center"},{"id":26174,"name":"Choice Cuts Meat","slug":"choice-cuts-meat"},{"id":26176,"name":"PetIQ","slug":"petiq-petiq"},{"id":26177,"name":"Chris Reeve Knives","slug":"chris-reeve-knives"},{"id":26178,"name":"FireCreek Snacks","slug":"firecreek-snacks"}],"autofix":{"failed":0,"created":57,"error_summary":{}},"severity":"hard","violations":57},"DS1":{"name":"dataset_visibility_enum","sample":[],"severity":"hard","violations":0}},"source":"check_axioms_execute"},"definitions":[{"id":"A1","name":"one_domain_one_business","severity":"hard","table":"od_businesses","description":"At most one active, non-merged od_businesses row per domain. Sub-locations and products MUST have domain=NULL.","check":"domain group-by where is_active AND merged_into_id IS NULL — count > 1 is violation","autofix":"none — needs entity-merge decision (which row is canonical?)"},{"id":"A2","name":"unique_active_slug","severity":"hard","table":"od_businesses","description":"At most one active, non-merged od_businesses row per slug. Backed by od_businesses_slug_key unique constraint.","check":"slug group-by where is_active AND merged_into_id IS NULL — count > 1 is violation","autofix":"none — needs entity-merge decision"},{"id":"A3","name":"no_ugly_ingest_suffix","severity":"soft","table":"od_businesses","description":"Active rows should not have ugly ingest-suffix slugs ('<canonical>-<2-6 chars>') when the canonical slug is free or held by an inactive row.","check":"slug ~ '^.+-[a-z0-9]{2,6}$' or '^.+-(com|ai|io|org|net|co|dev|cn)-[a-z0-9]{2,6}$' AND no active row holds the canonical short form","autofix":"scripts/canonicalize-business-slugs.mjs --execute"},{"id":"A4","name":"unique_linkedin_url","severity":"hard","table":"od_profiles","description":"At most one od_profiles row per linkedin_url. Backed by idx_profiles_linkedin_unique.","check":"linkedin_url group-by where linkedin_url IS NOT NULL — count > 1 is violation","autofix":"none — manual merge"},{"id":"A5","name":"team_link_company_resolves","severity":"hard","table":"od_team_links","description":"Every team_link.company_slug must reference an existing active od_businesses.slug.","check":"od_team_links.company_slug NOT IN (SELECT slug FROM od_businesses WHERE is_active AND merged_into_id IS NULL)","autofix":"partial — canonicalize-business-slugs heals slugs whose ugly form has a clean candidate. Universities/labs need new od_businesses rows."},{"id":"A6","name":"team_link_person_resolves","severity":"hard","table":"od_team_links","description":"Every team_link.person_slug must reference an existing od_profiles.slug.","check":"od_team_links.person_slug NOT IN (SELECT slug FROM od_profiles WHERE type='person')","autofix":"none — orphan team_links should be deleted or backfilled with a profile"},{"id":"A7","name":"at_most_one_primary_per_person","severity":"hard","table":"od_team_links","description":"At most one is_primary=true team_link per person_slug.","check":"person_slug group-by where is_primary=true — count > 1 is violation","autofix":"auto: keep the most recent is_primary=true, set others to false"},{"id":"A10","name":"active_business_has_name_and_slug","severity":"hard","table":"od_businesses","description":"Every active non-merged business must have a non-empty name AND non-empty slug.","check":"is_active AND merged_into_id IS NULL AND (name IS NULL OR name = '' OR slug IS NULL OR slug = '')","autofix":"auto: derive slug from name; flag rows with no name for manual review"},{"id":"D1","name":"domain_format_clean","severity":"hard","table":"od_businesses","description":"od_businesses.domain must not contain protocol, www. prefix, path, or query string.","check":"domain ~ '^https?://' OR domain LIKE 'www.%' OR domain LIKE '%/%' OR domain LIKE '%?%'","autofix":"auto: re-run normDomain() on each match"},{"id":"DS1","name":"dataset_visibility_enum","severity":"hard","table":"od_custom_datasets","description":"od_custom_datasets.visibility must be one of {public, private}.","check":"visibility NOT IN ('public', 'private')","autofix":"none — manual review"},{"id":"G2","name":"graph_no_duplicate_industry_edges","severity":"hard","scope":"in-memory graph","description":"No two IN_INDUSTRY edges between same (company, industry).","check":"audit graph adjacency lists post-build","autofix":"fixed at addEdge level via _edgeKeys Set"},{"id":"A11","name":"active_business_has_profile","severity":"hard","table":"od_profiles","description":"Every active non-merged od_businesses row must have a matching od_profiles row (type='company', same slug). od_team_links.company_slug FKs to od_profiles.slug, so missing profiles silently break team_links.","check":"active business slugs minus company profile slugs = violations","autofix":"auto: insert minimal company profile (slug, name from biz, type=company, profile_source=biz_backfill, is_public=true)"},{"id":"I1","name":"ingest_uses_create_or_match_helper","severity":"hard","scope":"code","description":"Every direct .from('od_businesses').insert() call site must be inside createOrMatchBusiness() OR carry a '// SAFE: insert ...' annotation explaining the bypass. Implements the 'every new record runs through match+dedup at write time' requirement.","check":"scripts/check-axioms.mjs greps server.js for .from('od_businesses').insert(...) and walks back 12 lines for createOrMatchBusiness or SAFE: insert.","autofix":"manual — refactor each violating call site to use the helper"},{"id":"I2","name":"ingest_uses_assign_business_slug","severity":"hard","scope":"code","description":"Every od_businesses.insert call must use assignBusinessSlug() (not raw Math.random suffix).","check":"static analysis: grep for 'Math.random' near 'slug' in any .insert payload","autofix":"manual — already done as of commit 7f58862; future drift needs CI guard"},{"id":"F1","name":"axiom_snapshot_fresh","severity":"freshness","table":"od_axiom_snapshots","description":"od_axiom_snapshots must have ≥1 row from the last 26 hours. The daily DQ orchestrator (server.js setInterval at boot) writes here; if no fresh row, the orchestrator stopped firing.","expected_cadence":"24h","max_age_hours":26,"timestamp_column":"snapshot_at","check":"SELECT max(snapshot_at) FROM od_axiom_snapshots — assert age < 26h","autofix":"none — investigate scripts/daily-data-quality-check.mjs and the setInterval at server.js boot"},{"id":"F2","name":"dataset_snapshot_fresh","severity":"freshness","table":"od_dataset_snapshots","description":"od_dataset_snapshots must have ≥1 row dated in the last 36 hours. scripts/snapshot-datasets.mjs runs nightly via the host cron; missing snapshots = the cron stopped or the script errored.","expected_cadence":"24h","max_age_hours":36,"timestamp_column":"snapshot_date","check":"SELECT max(snapshot_date) FROM od_dataset_snapshots — assert age < 36h","autofix":"none — investigate scripts/snapshot-datasets.mjs and the host cron (~/bin/onlydata-daily-sync.sh)"},{"id":"F3","name":"changelog_fresh","severity":"freshness","table":"tc_changelog","description":"tc_changelog must have ≥1 row with app='onlydata' from the last 7 days. Lower bar than the daily axioms — just verifies that data/sync-changelog.mjs is being run after pushes.","expected_cadence":"7d","max_age_hours":168,"timestamp_column":"created_at","filter":{"app":"onlydata"},"check":"SELECT max(created_at) FROM tc_changelog WHERE app='onlydata' — assert age < 7d","autofix":"none — manual `node data/sync-changelog.mjs` after pushes; consider a post-push hook"},{"id":"F4","name":"no_stuck_dispatch_jobs","severity":"freshness","table":"od_dispatch_jobs","description":"No od_dispatch_jobs row in status='running' with started_at older than 1 hour. A long-running 'running' job almost always means a worker crashed mid-execution and the row needs to be marked 'error'.","expected_cadence":"<1h per job","max_running_hours":1,"check":"SELECT count(*) FROM od_dispatch_jobs WHERE status='running' AND started_at < now() - interval '1 hour'","autofix":"none — inspect the worker on the host that claimed the job; mark the row 'error' once confirmed dead"},{"id":"F5","name":"mcp_smoke_fresh","severity":"freshness","table":"od_mcp_smoke_runs","description":"od_mcp_smoke_runs must have ≥1 row from the last 26 hours. scripts/mcp-smoke-test.py runs nightly via ~/bin/onlydata-daily-sync.sh; missing runs = the smoke harness stopped or auth broke.","expected_cadence":"24h","max_age_hours":26,"timestamp_column":"run_at","check":"SELECT max(run_at) FROM od_mcp_smoke_runs — assert age < 26h","autofix":"none — investigate ~/bin/onlydata-daily-sync.sh + scripts/mcp-smoke-test.py auth (refresh token)"}]}