{"snapshot":{"snapshot_at":"2026-11-05T23:24:45.041898+00:00","summary":{"holding":15,"total_axioms":20,"violating_hard":2,"violating_soft":1,"violating_freshness":2},"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":37103,"name":"BOISE AI","slug":"boise-ai-llc"}],"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":[],"name":"team_link_company_resolves","severity":"hard","violations":0,"distinct_slugs":0},"A6":{"name":"team_link_person_resolves","sample":[],"severity":"hard","violations":0},"A7":{"name":"at_most_one_primary_per_person","sample":[],"severity":"hard","violations":0,"settled_as_legitimate_dual":11},"D1":{"name":"domain_format_clean","sample":[],"severity":"hard","violations":0},"F1":{"name":"axiom_snapshot_fresh","severity":"freshness","age_hours":0.01,"latest_at":"2026-11-05T23:23:55.133588+00:00","violations":0,"max_age_hours":26},"F2":{"name":"dataset_snapshot_fresh","severity":"freshness","age_hours":-0.59,"latest_at":"2026-11-05","violations":0,"max_age_hours":36},"F3":{"name":"changelog_fresh","severity":"freshness","age_hours":1981.91,"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":1381.79,"latest_at":"2026-09-09T09:37:15.872384+00:00","violations":1,"max_age_hours":26},"G2":{"skipped":"no runtime check implemented yet"},"G3":{"name":"no_open_table_without_rls","sample":[],"severity":"hard","violations":0},"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":25689,"name":"AI Explained","slug":"ai-explained"},{"id":25931,"name":"Shorthills AI","slug":"shorthills-ai"},{"id":29276,"name":"Openhands","slug":"openhands"},{"id":30800,"name":"Jito","slug":"jito"},{"id":37085,"name":"Arda Industries","slug":"arda-industries"},{"id":37086,"name":"Mave","slug":"mave"},{"id":37087,"name":"Enact Intelligence","slug":"enact-intelligence"},{"id":37088,"name":"Flapping Airplanes","slug":"flapping-airplanes"},{"id":37089,"name":"Different","slug":"different"},{"id":37090,"name":"Tractian","slug":"tractian"}],"severity":"hard","violations":936},"A12":{"name":"employer_headline_has_team_link","sample":[{"slug":"mithsuka-ekanayake","headline":"Consultancy engagement coordinator @ proSapient (global expert network)"},{"slug":"majd-alsado","headline":"building @ bidaya.ai"},{"slug":"haonan-li","headline":"Senior Research Scientist at MBZUAI-IFM. | Prev. MBZUAI, LibrAI, UniMelb, SJTU, MSRA"},{"slug":"mason-daugherty","headline":"Engineering @langchain-ai"},{"slug":"peter-privon","headline":"Co-Founder, COO & CCO · VolVantage Advisors"},{"slug":"lakshya-agarwal","headline":"AI Engineering @ Bain & Co | Analytics @ McGill"},{"slug":"anindyadeep","headline":"Building @LiteFold"},{"slug":"sara-hannigan","headline":"Technical Program Manager at Voltron Data"},{"slug":"francisco-ingham","headline":"Founder @pampa-labs"},{"slug":"pavan-narasimha-karthik","headline":"Senior AI Software Engineer @mightybot-ai | B.Tech @IITH"}],"severity":"hard","violations":12,"settled_as_no_link":14,"awaiting_company_creation":11},"A13":{"name":"url_rows_are_classified","sample":[],"severity":"hard","violations":0,"unclassified":0,"identity_key_missing":0},"DS1":{"name":"dataset_visibility_enum","sample":[],"severity":"hard","violations":0}},"source":"check_axioms_dryrun"},"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 — EXCEPT where the duplicate-company adjudication (od_resolution_decisions, trigger 'duplicate_company_slugs') examined the two company slugs and ruled they are not one company. Joint appointments are real: 'University of Washington' + 'University of Washington / AI2' and 'MIT' + 'MIT / Databricks' are one person with two genuine primary affiliations, and demoting one would be wrong. 41 violations on 2026-10-23 → 15 after merging 7 duplicate company profiles (moonshot/moonshot-ai, cognition/cognition-ai-d8g, thinking-machines/thinkingmachines, deepseek/deepseek-ai, builder-io/builderio, playground/playground-ai, nus/national-university-of-singapore), and the remainder are adjudicated dual affiliations.","check":"person_slug group-by where is_primary=true — count > 1 is a violation, minus pairs with a standing non-merge verdict on trigger='duplicate_company_slugs'","autofix":"auto (CAUTION): keeps the most recent is_primary=true and demotes the others. Does NOT currently exclude adjudicated joint appointments, so it would demote half of a real dual affiliation — fix the duplicate company profiles instead (node scripts/adjudicate-entities.mjs --decide --apply)."},{"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","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","scope":"in-memory graph"},{"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","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","scope":"code"},{"id":"I2","name":"ingest_uses_assign_business_slug","severity":"hard","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","scope":"code"},{"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 as a step of the admin nightly job; missing snapshots = that job 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 nightly admin job that runs it."},{"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","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","filter":{"app":"onlydata"}},{"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","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","max_running_hours":1},{"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. KNOWN BROKEN since 2026-09-09 and violating every day since: scripts/mcp-smoke-test.py is not currently scheduled by anything, and the nightly job this axiom used to name never existed (audited 2026-10-25).","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 — there is no job to repair. Restarting the harness needs a scheduler built for scripts/mcp-smoke-test.py and a credential provisioned, which is an owner decision."},{"id":"A12","name":"employer_headline_has_team_link","severity":"hard","table":"od_profiles","description":"A person profile whose headline names an EMPLOYER must have at least one od_team_links row. An employer-shaped headline ('Founder at X', 'Engineer @ Y', 'CTO, Z') is a claim that a person works at a company — the same claim od_team_links exists to hold. When the headline carries it and the link table does not, the company→people edge is invisible to every join, the person is missing from their employer's page, and the headline is the only copy of a fact that should be a row. 32 live violations when this was written (2026-10-23). Either the link is missing (mint/attach it) or the headline is composed prose that was never a claim at all, in which case it should be cleared — 13 owner-source composed headlines were cleared the same day, originals preserved in od_profiles.source_detail. Education-shaped headlines ('MS CS @ GaTech', 'CS @ Waterloo') are OUT OF SCOPE: od_team_links is the employer table, and a degree is not an employment claim — counting them would make the axiom satisfiable only by inventing employment. A standing 'separate' verdict in od_resolution_decisions (trigger 'employer_headline') settles a row and excuses it: 'Maintainer @vllm-project' and 'ex @datadog' match the pattern forever and correctly need no link. A 'insufficient' verdict does NOT excuse it — that means the employer is real but not yet a company we hold, which is backlog, not noise.","check":"od_profiles where type='person' and headline matches /\\b(at|@)\\s+\\S|,\\s*(founder|ceo|cto|coo|cfo|president|partner|director|engineer|designer|manager)\\b/i minus education-shaped headlines, minus profiles with >=1 od_team_links row on person_slug = violations ; minus profiles with a standing verdict='separate' on trigger='employer_headline'","autofix":"none — needs a resolution decision per row: resolve the named employer and insert the od_team_links row (scripts/add-entity.mjs / POST /api/team-link, which mints an entity on a genuine miss), or clear the headline preserving the original in source_detail. Never invent a company to satisfy the axiom."},{"id":"A13","name":"url_rows_are_classified","severity":"hard","table":"company_domains","description":"Every company_domains row must carry url_tier, host_kind and apex_domain, and identity_key must be non-null for every row whose host actually identifies someone. The classifier is scripts/lib/domain-identity.mjs; the backfill is scripts/backfill-url-tiers.mjs (idempotent). An unclassified URL row is invisible to entity resolution: matching joins on identity_key, so a row without one silently cannot match anything. identity_key is NULL on purpose for two host kinds — 'parked' (the domain lapsed and is for sale; 13 unrelated businesses shared hugedomains.com) and 'platform_root' (a bare platform host like facebook.com or walmart.com, which identifies the operator and nobody else). Those two are the ONLY legitimate nulls.","check":"company_domains where url_tier IS NULL OR host_kind IS NULL OR apex_domain IS NULL = violations; plus rows where identity_key IS NULL AND host_kind NOT IN ('parked','platform_root') = violations","autofix":"auto: node scripts/backfill-url-tiers.mjs --execute (deterministic, idempotent, writes no business row)"},{"id":"G3","name":"no_open_table_without_rls","severity":"hard","table":"pg_class","description":"No table in the public schema may be SELECT-able by the `anon` role while row-level security is DISABLED. That combination means anyone holding the publishable anon key can read every row through the Data API, whatever the app's own routes do. Found live on 2026-10-23: od_resolution_decisions was created that day without RLS, and its frozen evidence bundles carry business names, street addresses and phone numbers plus person headlines. Every sibling table in that family (od_entities, od_entity_members, od_projections, company_domains) was already RLS-on-with-no-policies -- deny-all through PostgREST, reachable only by server routes holding the service role -- so the new table was the odd one out, and nothing was watching for that. Two legitimate shapes: RLS ON with policies that intentionally expose public rows, or RLS ON with NO policies plus grants revoked (the server-door pattern). RLS OFF plus anon SELECT is never one of them.","check":"For every pg_class relkind='r' in schema public: violation when has_table_privilege('anon', oid, 'SELECT') AND NOT relrowsecurity. Report the table and any contact-shaped columns it holds (email/phone/address/token/user_id).","autofix":"none by default -- enabling RLS on a table whose app DOES rely on anon reads would break that app, so each one is a decision. The server-door fix is: alter table X enable row level security; revoke all on X from anon, authenticated; grant all on X to service_role.","scope":"database"}]}