AI-Powered Public Funding & Charity Network Analytics Platform

Public funding and charity-network analysis in one place: CRA filings, federal grants, and Alberta open data, with cross-dataset entity matching for transparency research. On this site, organization names in the embedded dashboard are anonymized (for example university 001, deaf org 002)—not the legal names of registered charities.

Funding risk overview

Snapshot of outcomes from the combined CRA, federal, Alberta, and matched-entity dataset. Use the dashboard to filter loops, sort clusters, and inspect public-safe labels.

How clusters are ranked

The table lists loop clusters: groups of charities linked by gift flows where funds can move in a cycle. Ordering helps triage; it is not a finding of wrongdoing.

  1. Risk score (up to 6). Each cluster gets a numeric score from weighted signals (flow size, network shape, and related factors). The dashboard shows it beside the high / medium / low risk label.
  2. Default sort. Rows load by risk score, highest first. Use Sort by to switch to public funding exposure, total circular flow on edges, or loop id.
  3. Public funding. When the public report includes a cluster funding total (sometimes a range), charts and exposure use a consistent midpoint for ranges.
  4. Loop depth. Clusters use 2-hop and 3-hop charity gift-flow cycles from cra.loops (longer materialized paths are excluded in this report). Shorter loops break ties when risk and funding signals are similar.

Screening only. Always verify against primary sources and context before conclusions.

~23M
Total records (unified DB)
3
Max hops (2- and 3-hop)
20
Flagged loops (vendor snapshot)
~$7.0B
Public funding exposure (snapshot)
Open dashboard full screen

Daniel Demoz · Kenneth Preston · Matthew Rocky · Ayesha Khalil

University of Ottawa

Quick Start Guide

Prerequisites

Node.js 18+ required. All data is already loaded in the shared database — you only need to install dependencies to start querying and running analysis.

1

Clone and Install

git clone <repo-url> && cd hackathon
for dir in CRA FED AB general; do (cd $dir && npm install); done
2

Verify Database Access

Credentials: the .env.public files for each module are distributed by the hackathon organizers in the info pack provided on event day (April 29, 2026). They are gitignored and not in the clone — drop the info-pack files into CRA/, FED/, AB/, and general/ before running anything that touches the database.

Once in place, test connectivity:

cd CRA && npm run verify
cd ../FED && npm run verify
cd ../AB && npm run verify
3

Configure API Keys (optional, for AI features)

To use AI-assisted entity resolution, add credentials to general/.env.public or your personal general/.env:

# Option A: Anthropic Direct API
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here

# Option B: Vertex AI (Google Cloud)
VERTEX_SERVICE_ACCOUNT_JSON={"type":"service_account",...}
VERTEX_PROJECT_ID=your-project-id
VERTEX_CLAUDE_SONNET_MODEL=claude-sonnet-4-6
VERTEX_ENDPOINT=aiplatform.googleapis.com
VERTEX_LOCATION_ID=global
VERTEX_METHOD=rawPredict

The system tries Anthropic first, falls back to Vertex automatically. You can force a provider with --provider vertex.

4

Run Analysis

# CRA: Detect circular gifting patterns
cd CRA && npm run analyze:all

# AB: Run all 6 advanced analysis scripts
cd ../AB && npm run analyze:all

# General: Run the full entity-resolution pipeline (CRA + FED + AB)
cd ../general
npm run entities:dashboard        # browser dashboard at localhost:3800
npm run entities:dossier          # per-entity dossier explorer at localhost:3801
5

Adapt to Your Own LLM

The AI review module (general/lib/llm-review.js) has a clean provider abstraction. To add a new provider:

  1. Add a callYourProvider(prompt, options) function returning { text, usage }
  2. Register it in availableProviders() by checking your env variable
  3. Add it to the fallback chain in callLLM()

The prompt and response parsing are provider-agnostic — any LLM that returns JSON works.

Environment Loading Order

All modules load environment variables consistently:

  1. .env.public loaded first — shared defaults. Gitignored; distributed by the hackathon organizers in the event-day info pack.
  2. .env loaded second with override: true (personal overrides win)

Participants who drop the info-pack .env.public files into each module directory get read-only credentials automatically. Maintainers with a .env containing admin credentials automatically override for write operations (migrations, imports, drops).

CRA T3010 Charity Data

Schema: cra  |  Records: ~7.3M T3010 + ~1.42M pre-computed analysis  |  Years: 2020-2024  |  Tables: 49 + 3 views (6 lookup + 19 raw-data + 24 analysis created by advanced scripts)

~85K
Charities / Year
49
Tables + 3 Views
0-30
Risk Score Range
2-6
Hop Cycle Detection

What's in It

Annual filings from all registered Canadian charities: financial statements, board directors, gift flows between charities, compensation, program descriptions, and political activity.

Key Tables

TableDescriptionRows
cra_identificationCharity name, address, category, designation421,866
cra_directorsBoard members and officers2,873,624
cra_financial_detailsRevenue, expenses, assets (Section D)420,849
cra_qualified_doneesGifts to qualified donees1,664,343
cra_compensationEmployee compensation ranges216,380

Pipeline Commands

cd CRA && npm install

# Full data pipeline (all 5 years from Open Data API)
npm run setup              # migrate + seed + fetch + import + verify

# Analysis
npm run analyze:all        # Detect loops (2-6 hops) + SCC + partitioned + score (0-30)
npm run lookup -- --name "charity name"    # Interactive network lookup
npm run risk -- --name "charity name"      # Risk assessment

# Export
npm run download                           # All tables as CSV
npm run download -- --year 2024 --format json

# Tests
npm run test               # 54 unit + integration tests

Circular Gifting Detection

The analysis pipeline detects circular flows where money moves in loops between charities (potentially for quota gaming). It scores each charity 0-30 across three dimensions:

DimensionMaxWhat It Measures
Circular6Reciprocal partners, multi-cycle, shared directors
Financial12Overhead, pass-through indicators, compensation vs programs
Temporal12Same-year symmetric flows, persistent cross-year patterns

Federal Grants & Contributions

Schema: fed  |  Records: ~1.275M  |  Departments: 51+  |  Recipients: 422K+

1.275M
Grant Records
51+
Federal Departments
0-35
Risk Score Range
9,311
Programs

What's in It

All federal government grants, contributions, and transfer payments. Includes department, program, recipient type, agreement type, and value. Supports amendment tracking and multi-dimensional risk scoring.

Key Fields

FieldDescription
recipient_legal_nameGrant recipient organization name
recipient_business_number9-digit CRA BN (often NULL pre-2018)
agreement_typeG=Grant, C=Contribution, O=Other
agreement_valueDollar amount (negative = amendment)
owner_org_titleFunding department name

Pipeline Commands

cd FED && npm install

npm run setup              # Full pipeline: migrate + seed + fetch + import + fix-quality + verify
npm run dashboard          # Summary statistics

# Analysis (8 advanced scripts)
npm run analyze:all        # All: equity, for-profit, amendments, concentration, zombies, export, risk, individuals
npm run analyze:risk       # 7-dimension risk register (109K+ entities scored)

# Tests
npm run test

Risk Dimensions (0-35)

DimensionMaxWhat It Measures
Cessation5No new grants in recent years
Identity5No business number, weak identity
Amendment5High amendment rate, value creep
Concentration5Dominates program/department
Dependency5Few grants, high total
Opacity5Missing descriptions, results, NAICS
Scale5Outsized single grants

Alberta Open Data

Schema: ab  |  Records: ~2.61M  |  Years: 2014-2026 (12 fiscal years)  |  Datasets: 4

1.99M
Grant Payments
67K
Blue Book Contracts
15.5K
Sole-Source Contracts
69K
Non-Profit Registry

Datasets

DatasetTableRecordsSource
Alberta Grantsab_grants1,986,676JSON + CSV disclosure
Blue Book Contractsab_contracts67,079Excel
Sole-Source Contractsab_sole_source15,533Excel
Non-Profit Registryab_non_profit69,271Excel

Pipeline Commands

cd AB && npm install

npm run setup              # Full pipeline: migrate + seed + import:all + verify

# Individual imports
npm run import:grants      # Stream 1.1GB JSON (~10 min)
npm run import:contracts   # Blue Book Excel
npm run import:sole-source # Sole-source Excel
npm run import:non-profit  # Non-profit registry Excel

npm run verify             # 25 verification checks

Analysis Scripts (6 advanced)

# Run all analysis
npm run analyze:all

# Individual scripts
npm run analyze:quality      # Data quality audit (NULLs, duplicates, outliers)
npm run analyze:summary      # Ministry breakdowns, top recipients, trends
npm run analyze:insights     # HHI concentration, dissolved NPOs, ministry mapping
npm run analyze:sole-source  # 5 sole-source scenarios (repeats, splitting, geography)
npm run analyze:scenarios    # 5 grant/contract scenarios (ratios, volatility, lottery)
npm run analyze:non-profit   # Lifecycle trends, survival analysis, sector health

5 Sole-Source Scenarios

  1. Repeat vendors — Same vendor, same ministry, 3+ sole-source contracts
  2. Sole-source to competitive pipeline — Vendors in both sole-source AND Blue Book
  3. Contract splitting — Multiple same-FY contracts suggesting threshold avoidance
  4. Geographic concentration — Out-of-province vendor analysis
  5. Duration/value outliers — Longest contracts, highest daily rates

5 Grants/Contracts Scenarios

  1. Grant-to-contract ratio — Which ministries favor grants vs procurement
  2. Recipient concentration (HHI) — Monopoly programs
  3. Funding volatility — Most unstable year-over-year programs
  4. Lottery funding — Lottery vs non-lottery distribution patterns
  5. Payment timing — End-of-fiscal-year spending spikes

General — Cross-Dataset Entity Resolution

Schema: general  |  Cross-dataset utilities, reference data, and the entity resolution pipeline.

The Problem

The same legal organization appears under dozens of name variations across CRA, federal, and Alberta sources. A representative mid-sized multi-dataset charity typically has 10+ distinct name variants in the source data (legal names, trade names, typos, bilingual French-English pairs), carries multiple Business Number suffix variants (RR for the charity account, RC for corporate tax, RP for payroll), and contributes 200+ source records across 6 different tables. Without reconciling all of these to one canonical entity, cross-dataset accountability analysis is impossible.

The general module produces one golden record per real-world organization, linked to every source row that contributed to it.

The Three Techniques

The pipeline combines three complementary approaches to entity matching. Each catches cases the others miss.

1. Deterministic matching (rules-based)

Walks the six source tables in trust order — CRA first (every registered charity has a Business Number), then federal grants, then Alberta data — and applies a cascade of matching rules. BN-based matching handles the gold-standard cases; normalized-name matching strips out operational noise like TRADE NAME OF, O/A, DBA, AKA, FORMERLY, and bilingual English-French pipe separators; trade-name extraction catches the common Canadian pattern where an organization operates under a different trading name.

2. Probabilistic matching (Splink)

Integrates Splink, the UK Ministry of Justice's open-source Fellegi-Sunter record linkage library. Splink weights multiple features — name similarity (Jaro-Winkler at thresholds 0.92 and 0.82), city, postal code, entity type, and BN — and learns the relative weights from the data itself through expectation-maximization training. Runs on a DuckDB backend for speed (~3 minutes on ~1 million records), then writes pairwise match probabilities and connected-component cluster IDs back to Postgres.

Splink catches the cases that rules miss: hierarchical organizations where word order differs, truncated federal-registry variants, no-BN cross-dataset pairs that match by address rather than name. It is fully deterministic given a fixed seed — two runs produce bit-identical predictions.

3. LLM verdict and canonical-name authoring

Claude Sonnet 4.6 reviews every candidate pair produced by the deterministic and probabilistic stages. 100 concurrent workers per provider, running against both Anthropic's direct API and Google Vertex AI in parallel (separate rate-limit pools). Each LLM call does two things in one pass:

  • Decide whether the two entities are the SAME legal organization, RELATED (parent/subsidiary/regional chapter/affiliate), or DIFFERENT.
  • Author the golden record for SAME verdicts: the canonical legal name (the cleanest, most official-looking form chosen from all observed variants), the entity type (charity / non-profit / company / government / grant-recipient / individual / unknown), and an exhaustive deduplicated list of every meaningful name variant across both entities.

LLM review is resumable across crashes. Workers claim candidates via row-level locks; stuck reviews are reset automatically after 10 minutes. 429 rate-limit errors trigger 30-120 second exponential backoff.

The Pipeline: Eight Stages

#StageWhat it doesTypical runtime
1Schema migrationCreates the entity tables, indexes, and SQL helper functions (normalized-name, BN validator, BN extractor).~10s
2Deterministic resolveSeeds entities from CRA (~91K charities), then enriches with CRA qualified-donees (name variants from T3010s), federal grants (~422K distinct names), and Alberta data. Ends with deduplication by shared BN and same normalized-name.~20–25 min
3Splink probabilistic matchExports cleaned source data to Parquet, runs Splink with EM-trained feature weights on a DuckDB backend, writes ~540K pairwise predictions and cluster-alias rows back to Postgres.~10 min
4Candidate detectionFive tiers: same normalized name, shared BN root, trade-name extraction, trigram similarity, Splink probabilistic. Fills a staging table of ~1.6M candidate pairs.~5–10 min
5IDF keyword overlapInverted-index scan weighted by inverse document frequency. Catches hierarchical organizations (e.g., "Diocese of Hamilton") that trigram misses because of word-order variation.~3 min
6LLM verdict + authoring100×2 concurrent Claude workers decide SAME/RELATED/DIFFERENT and author golden-record fields. Dual-provider throughput (~30 pairs/sec combined).~6–8 hours
7Final golden-record compileFor every active entity (LLM-touched or not), compiles ranked aliases with provenance, source summary, addresses, CRA financial rollup, federal grants profile, Alberta profile.~2–5 min
8Donee-name trigram fallbackReviews unlinked cra_qualified_donees rows via trigram-fuzzy lookup against the authoritative catalog. LLM decides SAME/RELATED/DIFFERENT. Tagged match_method = 'donee_trigram_fallback' for full reversibility. Never creates new entities.~30–90 min

Why CRA Qualified Donees Matter

Every registered Canadian charity declares its gifts to other charities on its annual T3010 filing, writing out the recipient's name as it knows it. This produces an under-appreciated gold mine: the same organization appears under many different names written by many different donors — truncations, program-prefixed variants (e.g. "[PROGRAM NAME] — [ORG NAME]"), and line-item numbers that are often idiosyncratic to the filer's accounting. For any given charity, 5–10 additional name variants typically surface from qualified-donee declarations that don't appear in any other source. The pipeline pulls these in explicitly as a dedicated sub-phase.

Libraries & Tools

ToolRoleWhy
PostgreSQLPrimary data store and compute engineSingle source of truth; every stage reads and writes SQL. GIN indexes enable millisecond fuzzy-name joins at 850K-entity scale.
pg_trgmCharacter-trigram similarityPostgres extension; catches typos and character-level drift in names.
Splink (MoJ, MIT license)Probabilistic matchingAcademic-standard Fellegi-Sunter implementation; feature weights learned from data via EM. Battle-tested at government scale.
DuckDBSplink's in-memory compute backendFastest option for single-machine Splink. Handles ~1M-record pairwise comparison universe in a few minutes.
Claude Sonnet 4.6 (Anthropic + Vertex AI)Verdict + canonical-name authoringHandles the long tail — cross-system registration numbers, hierarchical organization semantics, bilingual variants — that rules can't reliably resolve.
Node.js + ExpressPipeline orchestration + dashboardClean async-await for 100-way concurrent LLM calls; single-file dashboard for end-to-end observability.
cleanco (Python)Legal-suffix strippingStrips "Inc", "Ltd", "Société", "GmbH" etc. so Splink sees stems rather than boilerplate.

Dashboard

A browser dashboard at http://localhost:3800 provides control and observability. Click through the pipeline stages; each button spawns the corresponding script server-side and streams its output inline. The dashboard polls Postgres every 2 seconds to show:

  • A pipeline-stage strip showing the current state (inferred from the database — no manual coordination needed)
  • Entity counts, source-link counts per table, Splink prediction stats with band breakdown, golden-record count
  • LLM progress with reviewed-per-second throughput and ETA; per-provider split when both Anthropic and Vertex are running in parallel
  • Test-entity sanity cards — a small set of known organizations (configurable in the dashboard source) that exercise different matching paths: a multi-dataset mid-sized charity, a corporate family with distinct BNs, two charities sharing a canonical name, and a large multi-dataset institution. Each card updates every poll so regressions are visible instantly.
  • Recent-merges feed showing absorbed → survivor pairs as they happen
  • Orphan recovery: phase PIDs persist across dashboard restarts; a "Kill all pipeline processes" button cleans up any stuck jobs

Outcomes

~851K
active canonical entities
~5.2M
source links across 6 tables
~55K
multi-dataset entities
~67K
LLM-confirmed SAME merges
~65K
RELATED cross-links
98%
coverage vs Splink reference sample

Design Principles

  • BN is the primary identifier. Every stage treats the 9-digit Canadian Business Number root as authoritative. Where it's available it overrides name evidence; where it's missing we cascade to weaker signals.
  • Every stage is idempotent and resumable. Unique constraints, ON CONFLICT DO NOTHING on inserts, and row-level locks on LLM claims mean re-running any phase is safe and continues from where it left off.
  • Observable by default. The dashboard polls the database directly, not a separate event stream. What you see is the actual state of the pipeline, always.
  • No destructive operations without explicit consent. The reset script requires --yes or a dashboard confirmation dialog.

Commands

cd general
npm install

# One-time setup
npm run entities:splink:install       # Splink Python dependencies
npm run setup                         # base schema + Alberta ministries

# Primary operator interface: the dashboard
npm run entities:dashboard            # http://localhost:3800 (click through stages)

# Headless end-to-end (destructive rebuild)
npm run pipeline:rebuild              # reset + all stages in order

Full documentation: general/README.md

Schemas & Tables — Complete Reference

Every table and view across the four schemas of the shared PostgreSQL database. Row counts are estimates from pg_class.reltuples (updated by ANALYZE; may lag actual counts by a few percent).

Lookup tables are small reference tables (usually <300 rows) that decode codes used in the primary data. Analysis tables are pre-computed outputs of accountability-analysis scripts, not raw data from publishers. All primary data comes from Open Government portals — see ATTRIBUTIONS.md.

CRA — cra schema (49 tables + 3 views, ~8.76M rows)

Identity & registration

TableRowsPurpose
cra_identification421,866One row per charity per fiscal year: legal name, account name, designation (A/B/C), category, registered address, contact details, registration date. Primary key (bn, fiscal_year).
identification_name_history92,437Consolidated legal/account name history per BN with first and last year seen. Useful for detecting renames.
cra_web_urls169,123Contact URLs declared on T3010 by year and sequence.

T3010 annual return — financial and operational sections

TableRowsPurpose
cra_financial_details420,849T3010 Section D line items: revenue breakdown (receipted / non-receipted / government / investment / other), expenditures, assets, liabilities. One row per (bn, fpe).
cra_financial_general422,683Program-area allocations (% of resources), internal divisions, Y/N program flags.
cra_foundation_info422,569Foundation-specific T3010 fields (fields 100-series): controlling / controlled donors, non-qualifying investment income, etc.
cra_charitable_programs478,691Free-text descriptions of each charity’s program activities, by program type.
cra_directors2,873,624Board members per filing: name, position, arm’s-length flag, start/end dates.
cra_compensation216,380Employee compensation ranges (fields 300-390): FTE positions, part-time, total compensation.
cra_disbursement_quota22,151Disbursement-quota calculation fields (T3010 Section C).
cra_gifts_in_kind54,575In-kind gift details (non-cash donations received).

Cross-charity gift flows & foreign activity

TableRowsPurpose
cra_qualified_donees1,664,343Gifts from one registered charity to another. Each row has donor BN, donee BN + name (as the donor wrote it), amount, in-kind portion, and an “associated” flag. This is where the entity-resolution pipeline harvests extra name variants.
cra_non_qualified_donees29,270Gifts to non-registered recipients, with stated purpose and cash/non-cash split.
cra_activities_outside_countries44,683Countries in which the charity operated during the reporting year.
cra_activities_outside_details25,139Additional fields about foreign activities (T3010 Section C5).
cra_resources_sent_outside68,028Individual lines itemizing money/resources sent outside Canada: recipient name, amount, country.
cra_exported_goods4,029Specific goods exported, with item name, value, destination.
cra_political_activity_desc313Free-text descriptions of political activities (when applicable).
cra_political_activity_funding0Funding lines for political activities (empty in 2020–2024 extract).
cra_political_activity_resources234Staff/volunteer/financial resources allocated to political activities.

Analysis outputs (pre-computed by accountability scripts)

TableRowsPurpose
overhead_by_charity420,021Per-charity, per-year overhead ratios: strict (admin + fundraising) and broad (includes support), compared against revenue + program spending.
overhead_by_year5Aggregated overhead by fiscal year across all charities.
overhead_by_year_designation16Aggregated overhead broken down by designation (A/B/C).
govt_funding_by_charity166,968Per-charity, per-year split of federal / provincial / municipal government funding, plus the government share of revenue.
govt_funding_by_year5Aggregated government-funding share by year.
loops5,808Brute-force ground-truth detected circular-gifting cycles at 2–6 hops (temporal window ±1 year).
johnson_cycles4,601Johnson’s algorithm cycle enumeration on the full graph (cross-validation against loops).
partitioned_cycles108SCC-partitioned Johnson’s with hub removal (fast, misses cycles routing through mega-hubs).
loop_participants30,003BNs that appear in any detected cycle, with position and neighbors.
loop_edges53,771Pruned gift-edge graph (after iterative dead-end removal).
loop_edge_year_flows30,003Edge-level amounts restricted to each loop’s year window.
loop_universe1,501Per-BN summary of cycle participation (2/3/4/5/6-hop counts) and risk inputs.
loop_charity_financials1,501Per-BN overhead proxy + loop-related inflow / outflow.
loop_financials5,808Financial metadata per cycle: bottleneck amount, total flow, year range, same-year vs cross-year.
scc_components10,177Strongly-connected components in the charity gift graph (Tarjan’s algorithm).
scc_summary347Per-SCC aggregate statistics.
matrix_census10,177Matrix-power walk census for cross-validation (N-hop reachability counts per BN).
identified_hubs20Entities flagged as “hubs” in the gift-flow network based on in/out-degree and total flow.
t3010_impossibilities54,010Direct violations of T3010 form arithmetic identities, cross-schedule equalities, and Schedule 3 dependencies (10 rules, each traceable to specific form-line or Open Data Dictionary v2.0 text).
t3010_plausibility_flags1,075Plausibility flags layered on top of the strict-rule impossibilities.
t3010_completeness_issues0Reserved for completeness-rule findings; currently empty.
donee_name_quality439,867Scored name-similarity check between donor-written donee names (cra_qualified_donees) and the canonical legal name from cra_identification. Used to detect transcription errors and BN-name mismatches.
_dnq_canonical91,129Canonical donee-name lookup used by the donee_name_quality pipeline.

Lookup tables (code → name decoders)

TablePurpose
cra_category_lookupCharity category codes → English/French names + descriptions.
cra_sub_category_lookupSub-categories under each category.
cra_designation_lookupDesignation codes (A = public foundation, B = private foundation, C = charitable organization).
cra_program_type_lookupProgram-type codes used by cra_charitable_programs.
cra_country_lookupCountry code → name (used by foreign-activity tables).
cra_province_state_lookupProvince / state code → name.

Views

ViewPurpose
vw_charity_financials_by_yearJoined identification + financial_details, one row per charity-year, with key financial fields exposed by name.
vw_charity_profilesCharity directory with identification + computed fields. Useful for search.
vw_charity_programsJoined programs + identification for easy program-description search.

FED — fed schema (6 tables + 3 views, ~1.28M rows)

Primary data

TableRowsPurpose
grants_contributions1,275,521One row per federal grant or contribution agreement (or amendment). Fields include recipient legal name, business number (when known), department (owner_org), agreement value, program name + purpose (both languages), province/city, agreement start + end dates, federal riding, is_amendment. Amendments share a ref_number with the original. Per TBS spec, agreement_value is a cumulative running total — use vw_agreement_current or vw_agreement_originals to avoid double-counting.

Lookup tables

TablePurpose
agreement_type_lookupAgreement-type codes (G = grant, C = contribution, etc.).
recipient_type_lookupRecipient-type codes (for-profit / non-profit / individual / government / foreign).
province_lookupProvince code ↔ bilingual name.
country_lookupCountry code ↔ bilingual name.
currency_lookupCurrency code ↔ name (for foreign-currency amounts).

Views

ViewPurpose
vw_grants_decodedgrants_contributions joined to all lookup tables with decoded English/French names.
vw_grants_by_departmentAggregates by owner organization + agreement type: grant count, total value, average, date range.
vw_grants_by_provinceAggregates by recipient province.

AB — ab schema (9 tables + 3 views, ~2.61M rows)

Primary data

TableRowsPurpose
ab_grants1,986,676Alberta government grant payments (fiscal years 2014-2015 through 2025-2026). Fields include ministry, business unit, recipient, program name, amount, payment date, fiscal year (display_fiscal_year format "YYYY - YYYY"), lottery-fund flag.
ab_contracts67,079Blue Book (open-procurement) contracts. Recipient, amount, ministry, fiscal year.
ab_sole_source15,533Sole-source (non-competitive) contracts. Vendor + department addresses, contract number, start/end dates, amount, permitted-situation justification.
ab_non_profit69,271Alberta non-profit societies registry: legal name, type, status, registration date, registered address.

Pre-computed aggregates

TableRowsPurpose
ab_grants_ministries352Distinct ministries appearing in ab_grants, with count + total.
ab_grants_programs20,208Distinct program names with counts, totals, date ranges.
ab_grants_recipients452,900Distinct grant recipients with rollup totals.
ab_grants_fiscal_years12Per-fiscal-year rollup, one row per fiscal year (2014-2015 through 2025-2026).

Lookup + views

ObjectPurpose
ab_non_profit_status_lookupStatus codes (active / struck / dissolved / under review / etc.) with descriptions.
vw_non_profit_decodedab_non_profit joined to status_lookup for human-readable status.
vw_grants_by_ministryab_grants aggregated by fiscal year + ministry.
vw_grants_by_recipientab_grants aggregated by recipient name.

general — general schema (14 tables + 2 views, ~10.5M rows)

Reference data — Alberta ministries

TableRowsPurpose
ministries27Current Alberta government ministries with short codes, ministers, deputy ministers, effective dates.
ministries_crosswalk162Mapping of historical ministry names (as they appear in ab_grants.ministry and ab_contracts.ministry) to the current canonical ministry.
ministries_history62Name-change timeline: each prior ministry name with first/last year seen and the successor ministry it was renamed to.

Entity resolution pipeline — state

TableRowsPurpose
entities926,670One row per resolved entity (including those later absorbed via merge). Canonical name, alternate names, BN root + all 15-char BN variants, entity type, dataset_sources array, merged_into (for soft-deleted absorbed entities), norm_canonical, status (draft/confirmed/merged), JSONB metadata.
entity_source_links5,161,245Links each source record (in CRA/FED/AB schemas) to its resolved entity. (source_schema, source_table, source_pk) identifies the source row; source_name preserves the name as originally written; match_method records how it was linked.
entity_resolution_log1,266,141Per-name matching progress. UNIQUE (source_schema, source_table, source_name). Status tracks pending/matched/created/error for resumability.
resolution_batchesvariablePer-run metadata: when the pipeline started/completed, processed/matched/errored counts.

Entity merging (staging + audit)

TableRowsPurpose
entity_merge_candidates1,643,060Pairwise candidates from the 5 detection tiers + smart-match. LLM reviews each one and writes verdict (SAME / RELATED / DIFFERENT / UNCERTAIN) + confidence + reasoning. Unique on the unordered pair.
entity_merges66,147Audit trail for every merge executed: survivor, absorbed, candidate_id that triggered it, merge_method, names/BNs added, links redirected count, timestamp.
donee_trigram_candidatesvariableStage 8 staging table: unlinked cra_qualified_donees names paired with their best trigram neighbour above threshold. Reviewed by the LLM and, on SAME verdicts, appended to the matched entity’s aliases with match_method = 'donee_trigram_fallback' for full reversibility.

Final golden-record output

TableRowsPurpose
entity_golden_records851,300The authoritative final table. One row per active real-world organization. Canonical name, every alias with source provenance (JSONB), BN variants, dataset_sources, source_summary per schema.table, deduplicated addresses, CRA / FED / AB profile JSONBs, related entities (LLM RELATED verdicts), merge_history, llm_authored raw response, confidence, status.

Splink probabilistic matching

TableRowsPurpose
splink_predictions540,640Pairwise match probabilities from Splink’s Fellegi-Sunter model (run threshold ≥ 0.40). Each row has left record (source_schema.source_table + record_id), right record, match_probability, match_weight, connected-component cluster_id, and feature-level gamma values. 06-detect-candidates Tier 5 uses this.
splink_aliasesvariableName variants per cluster with source provenance. Enriches golden-record alias lists with probabilistic matches the deterministic cascade missed.
splink_build_metadata1 per runOne row per Splink run: start/completion, Splink version, threshold, total records + predictions + clusters, config.

Views

ViewPurpose
vw_entity_searchSearchable entity directory (active entities only).
vw_entity_fundingJoined entity + CRA financials + FED grants + AB totals, one row per entity, for cross-dataset funding queries.

Primary keys & join paths

FromToVia
any CRA tablecra_identification(bn, fiscal_year) or bn alone for cross-year aggregates
CRAgeneral.entitiesLEFT(cra.*.bn, 9) = entities.bn_root
FEDgeneral.entitiesgeneral.entity_source_links.source_pk->>'_id' = fed.grants_contributions._id
ABgeneral.entitiesgeneral.entity_source_links.source_pk->>'id' = ab.*.id (cast to int or uuid depending on table)
entityall funding across CRA+FED+ABvw_entity_funding (joins everything in one query)

Local Database Recreation Kit

The .local-db/ directory contains everything needed to recreate the hackathon database in your own PostgreSQL instance: the full DDL (split into pre-data and post-data), a manifest of every table and row count, and export/import scripts that auto-discover tables via information_schema.

Data bundle is separate

The JSONL row data (~13 GB across all four schemas) is not in the repo — it ships out-of-band (GitHub release / file server). Download the bundle and unpack it under .local-db/data/ so the directory structure matches data/{cra,fed,ab,general}/*.jsonl. The small artifacts (DDL, manifest, scripts, README) are committed, so the repository alone documents the full schema.

Why JSONL, not CSV

JSONL was chosen over CSV so that jsonb columns, text[] arrays, NULLs, and strings with embedded newlines, commas, and quotes all round-trip through export/import without escape-layer games. One row per line, keyed by column name.

What ships in .local-db/

PathPurpose
README.mdQuick-start instructions, import options, expected runtime.
export.js(Maintainers) Dumps the live DB to schemas/ DDL and data/*.jsonl files.
import.js(Participants) Recreates the DB in a local Postgres. Auto-discovers tables; supports --schema, --schema-only, --data-only, --batch-size, --drop.
manifest.jsonAuthoritative table inventory with row counts + column metadata. Verified by import after load.
schemas/{cra,fed,ab,general}.sqlPre-data DDL: CREATE SCHEMA, sequences, tables (with nextval defaults), indexes, views.
schemas/{cra,fed,ab,general}_post.sqlPost-data DDL: unique / check / foreign-key constraints, and setval() statements to sync sequences with loaded row counts.

Dataset totals (from the manifest)

SchemaTables + viewsRowsNotes
cra49 tables + 3 views~8.76MT3010 raw data (2020–2024) + 24 accountability-analysis tables
fed6 tables + 3 views~1.28MProactive-publication grants & contributions
ab9 tables + 3 views~2.61MAlberta grants, contracts, sole-source, non-profit registry
general14 tables + 2 views~10.46MCross-dataset entity resolution (entities, source links, golden records, Splink output, ministries reference)
Total across schemas~23.1M rowsFull load typically ~20–30 min on local Postgres, ~1–3 hours to a remote instance

Quick start

createdb hackathon                                     # or via your Postgres admin
cd .local-db && npm install
DB_CONNECTION_STRING=postgresql://user:pass@localhost/hackathon npm run import

Prerequisites

  • PostgreSQL 14+, with permission to CREATE EXTENSION pg_trgm and CREATE EXTENSION fuzzystrmatch.
  • Node.js 18+.
  • A DB user with CREATE on the target database and INSERT on the tables being loaded.

Full details: .local-db/README.md

Analysis outputs

Each module ships advanced analysis scripts that write human-readable Markdown / TXT reports and machine-readable JSON to its own reports folder when you run them. Those folders are gitignored — you generate them locally — but every analysis script also persists its findings to a table under its schema, so the same results can be queried directly from SQL without running anything.

AB — six analysis scripts

CommandWhat it produces
npm run analyze:qualityData-quality audit: NULL rates, empty strings, duplicates, amount outliers, fiscal-year coverage.
npm run analyze:summaryMinistry-by-fiscal-year breakdowns, top recipients, program distribution, cross-dataset overlap.
npm run analyze:insightsVendor HHI, sole-source rates, dissolved-NPO grants, ministry name mapping.
npm run analyze:sole-sourceFive scenarios: repeat vendors, sole-source→Blue Book pipeline, contract splitting, geographic concentration, duration/value outliers.
npm run analyze:scenariosFive grant/contract scenarios: grant-to-contract ratio, recipient HHI, funding volatility, lottery funding, payment timing.
npm run analyze:non-profitLifecycle trends, survival analysis, entity-type evolution, sector health.
npm run analyze:allRuns all six sequentially.

FED — eight analysis scripts

CommandWhat it produces
npm run analyze:equityPer-capita provincial funding analysis (all ten provinces + three territories).
npm run analyze:forprofitFor-profit recipient deep-dive.
npm run analyze:amendmentsAmendment-creep detection: rate, value growth, duration extensions.
npm run analyze:concentrationVendor / recipient concentration (Herfindahl-Hirschman Index, single-department dominance).
npm run analyze:zombiesZombie recipients (no new grants recently) and ghost-capacity entities.
npm run analyze:exportEntity export (CSV) for external adverse-media lookups.
npm run analyze:riskComprehensive 7-dimension risk register (0–35 scale): cessation, identity, amendment, concentration, dependency, opacity, scale.
npm run analyze:individualsIndividual grant recipient analysis.
npm run analyze:allRuns all eight sequentially.
npm run dashboardSummary statistics across the whole fed schema.

CRA — circular-gifting and data-quality pipelines

CommandWhat it producesPersisted table
npm run analyze:loopsBrute-force 2–6-hop cycle detection (ground truth). Temporal window ±1 year.cra.loops, cra.loop_participants, cra.loop_edges
npm run analyze:sccTarjan SCC decomposition of the charity gift graph.cra.scc_components, cra.scc_summary
npm run analyze:partitionedSCC-partitioned Johnson’s with hub removal (fast complement to brute force).cra.partitioned_cycles, cra.identified_hubs
npm run analyze:johnsonJohnson’s algorithm on the full graph (cross-validation).cra.johnson_cycles
npm run analyze:matrixMatrix-power walk census (cross-validates loops / johnson_cycles).cra.matrix_census
npm run analyze:financialPer-loop and per-charity financial rollup restricted to each loop’s year window.cra.loop_financials, cra.loop_edge_year_flows, cra.loop_charity_financials, cra.loop_universe
npm run analyze:scoreDeterministic 0–30 risk scoring (circular + financial + temporal).cra.loop_universe (score columns)
npm run analyze:allFull pipeline in order.—
npm run data-qualityThree data-quality scripts (BN↔name mismatches, T3010 arithmetic impossibilities, identification-backfill check). Every rule traced to an explicit line in the T3010 form or the CRA Open Data Dictionary v2.0.cra.donee_name_quality, cra.t3010_impossibilities, cra.identification_name_history
npm run lookup -- --name "..."Interactive per-charity network lookup (outgoing / incoming gifts, loops, shared directors).—
npm run risk -- --bn ...Interactive per-charity risk report with scored factors and multi-year financials.—

General — entity-resolution pipeline

The general pipeline persists everything to tables, not report files. Pipeline state lives in entities, entity_source_links, entity_merge_candidates, entity_merges, entity_golden_records, and the splink_* tables. Operate the pipeline through npm run entities:dashboard (port 3800) and investigate specific organizations through npm run entities:dossier (port 3801). A reference-sample validator is also available:

npm run entities:compare:splink   # 500-entity reference sample against the live pipeline