Skip to main content

A regulator asks: "Where does the 1,088,769,797.07 of risk-weighted assets on page 4 come from?" Today a team spends days reading SQL, dbt models and Spark jobs and running ad-hoc queries. Number Passport answers in under a minute:

$ passport trace total_rwa
Total risk-weighted assets (USD): GAPS FOUND
  hop  0 gap          report: Total risk-weighted assets (USD)       1088769797.07  unexplained 3100000.00
           ! CRITICAL: the report shows 1,088,769,797.07 but gold.rwa_by_class totals 1,085,669,797.07
             (difference 3,100,000.00): a manual adjustment or a report built from another source
  hop  1 reconciled   gold.rwa_by_class        SUM(rwa_usd)          1085669797.07
           - inner join ref.risk_weights AS rw            0 rows            0.00  models/dbt/gold/rwa_by_class.sql:19
  hop  2 reconciled   silver.exposures         SUM(ead_usd)          2365768570.80
           - dedup ROW_NUMBER() OVER (PARTITION BY ...)   30 rows   -34697141.71  models/sql/silver_exposures.sql:8
           - inner join bronze.fx_rates AS fx            15 rows            0.00  models/sql/silver_exposures.sql:18
           - filter l.status <> 'CLOSED'                120 rows  -153180711.57  models/sql/silver_exposures.sql:20
           - filter l.amount > 0                          8 rows     6999452.32  models/sql/silver_exposures.sql:21
  hop  3 reconciled   silver.collateral_adj    SUM(collateral_usd)    238633695.10
           - filter (haircut < 1)                         6 rows            0.00  jobs/collateral_adjust.py:14
  hop  5 source       bronze.loans             SUM(amount)          49965607732.58
  ...
51 read-only queries · evidence pack: passports/total_rwa-20261103T101500Z

It found the 3.1M manual adjustment that is in the report but not in the warehouse, and it says, to the cent and to the line of code, what every filter, join and dedup did on the way.

What it does

For one number, Number Passport:

  1. Builds the lineage from code, not from names. It reads:

    • SQL scripts and stored procedures: CREATE TABLE/VIEW AS, INSERT ... SELECT (with column lists, several inserts into one table), SELECT INTO, MERGE, temp tables, and DELETE/UPDATE run after a load; procedure bodies in SQL Server, PostgreSQL, Snowflake, BigQuery and Oracle;
    • dbt models (source files or manifest.json);
    • PySpark jobs, translated step by step to SQL (including union and unionByName);
    • SAS programs: PROC SQL, DATA steps, PROC SORT, PROC MEANS/SUMMARY, PROC APPEND;
    • Informatica PowerCenter mappings from repository XML exports;
    • views that live only in the warehouse, read from the database catalog;
    • Airflow DAGs, to tag each model with its DAG and task.

    Column-level lineage follows the measure through every hop.

  2. Reconciles every hop. For each table on the path it compares:

    • the value the table holds now;
    • the value its code would produce if re-run now (a mismatch is flagged as table_code_mismatch);
    • the effect of each row-removing step (WHERE conditions, inner joins, QUALIFY/dedup, DISTINCT, HAVING), measured by turning all of them off and back on one at a time, so the step effects add up exactly to the difference between input and output;
    • rows lost to inner joins, with their value in the source currency;
    • join fan-out (duplicate join keys that silently multiply money).
  3. Checks data quality on each table: row counts, NULL measures, duplicate keys (from config or inferred from the dedup in the code) and freshness against the report's as-of time.

  4. Compares the report itself (a CSV cell) with the warehouse, and flags any difference as a CRITICAL gap.

  5. Writes an evidence pack: passport.html (one self-contained page with the lineage diagram, hop cards and findings), passport.json, replay.sql (every query it ran) and MANIFEST.sha256.

  6. Re-checks a pack later with passport verify: file integrity, whether any pipeline code changed since, and whether every query still returns the same result.

Sums and counts are traced hop by hop. Averages and ratios (capital ratios, coverage, RWA density) are traced as their two additive parts, each with its own evidence, and the figure is re-derived from the two reconciled totals and checked against the report at the report's precision.

Statuses are strict: verified, gaps_found, partially_verified (some hops could not be checked) and lineage_only (offline). A residual above tolerance is never rounded away.

Try it in two minutes

git clone https://github.com/sagarkonkanagaon-netizenk/number-passport
cd number-passport
pip install -e ".[mcp]"
python examples/bank/build.py              # sample bank: bronze -> silver -> gold -> Q3 capital report
cd examples/bank
passport doctor                             # checks every connection point
passport trace total_rwa                    # gaps_found: the 3.1M manual adjustment
passport trace total_ead                    # verified
passport trace corporate_rwa                # verified, with a WHERE filter
passport verify passports/<pack>            # re-check the pack
python build.py --late-data && passport trace total_rwa   # late rows: table_code_mismatch on silver

The sample pipeline mixes all four code types: a SQL model with a QUALIFY dedup and an FX join, a dbt gold model, a PySpark collateral job (run by real Spark when pyspark and Java are installed) and an Airflow DAG.

Connecting to your data: three levels

You never need to give it more than read access, and you can start with no access at all.

Level What it needs What you get
1. Code only The pipeline repo Lineage with file:line, DAG and task, every filter and join (--offline, status lineage_only).
2. Read-only login A database role with SELECT only Full reconciliation and quality checks. Only aggregates leave the database.
3. Extracts CSV or Parquet files, one folder per schema Full reconciliation with no database connection at all (files:///path).

Supported connection URLs:

connection:
  url: duckdb:///warehouse/bank.duckdb      # opened read_only
  url: files:///exports                      # exports/<schema>/<table>.csv|parquet
  url: postgresql://reader@db/risk           # pip install "number-passport[postgres]"; session forced READ ONLY
  url: snowflake://...                       # pip install "number-passport[sqlalchemy]" + the driver

Use ${PASSPORT_DB_URL:-default} in passport.yml to keep credentials out of the file.

Commands

Command What it does
passport init Write a starter passport.yml.
passport doctor Check config, code paths, parsing, cycles, connection, read-only access, privacy guard, replay of every model in the warehouse dialect, metrics and report cells.
passport metrics List configured metrics.
passport lineage <table> Code-only lineage for a table.
passport trace <metric> Trace and write an evidence pack. Ad hoc: --table --column --agg --where.
passport trace ... --strict Exit code 2 when gaps are found, for CI gates before a filing.
passport trace ... --json Machine-readable digest.
passport verify <pack> Re-check a pack (--offline for integrity and code drift only).
passport mcp Run the MCP server (stdio, or --http for streamable HTTP).

Use it from an AI assistant

Number Passport ships an MCP server and an agent skill (skills/number-passport/SKILL.md). The skill tells the assistant how to run the method and how to talk about results honestly; the server does the work.

Claude Code plugin (installs both; needs uv). Inside Claude Code:

/plugin marketplace add sagarkonkanagaon-netizenk/number-passport
/plugin install number-passport@number-passport

Then run Claude Code in the folder that holds passport.yml. Or set it up by hand:

claude mcp add number-passport -- passport mcp --project /path/to/project
cp -r skills/number-passport ~/.claude/skills/number-passport

For Claude Desktop, Cursor and other MCP clients:

{
  "mcpServers": {
    "number-passport": {
      "command": "passport",
      "args": ["mcp", "--project", "/path/to/project"],
      "env": { "PASSPORT_DB_URL": "postgresql://reader@db.internal/risk" }
    }
  }
}

Tools: passport_doctor, list_metrics, show_lineage, trace_number, verify_passport. All are read-only except trace_number, which writes the evidence pack to disk.

Security model

  • Read-only, enforced twice. Connectors open DuckDB with read_only=True and put Postgres sessions in READ ONLY mode with a rollback after every query. passport doctor tries a write and fails if it works.
  • Aggregates only. Every query passes a privacy guard before it runs: one SELECT, aggregate-only outputs, no DDL or DML, no table functions or file readers (read_csv, COPY, ATTACH...), at most 50 result rows. Grouping by a column is only allowed for columns the data owner lists under privacy.safe_dimensions with privacy.mode: allow_dimensions.
  • Every query is logged with its purpose and a SHA-256 of the result, inside the evidence pack.
  • Nothing is sent anywhere. The engine has no network calls of its own. When used through MCP, the assistant only sees the digest of totals, counts and code locations.
  • The HTTP MCP transport has no authentication of its own: run it behind your own authenticating proxy.

The full threat model, guard rules and a reviewer summary are in docs/security.md.

Documentation

Guide For
How it works Everyone: the method, a worked example, statuses and every finding code.
Configuration reference Data engineers: passport.yml, connections, metrics and privacy modes.
Security and privacy model Security, data protection and model risk reviewers.
Architecture Contributors: modules, design decisions and how to extend.
FAQ Comparisons with lineage and data-diff tools, regulations, limits.
Releasing Maintainers: the automated PyPI and GitHub release pipeline.

Configuration

See examples/bank/passport.yml for a complete, commented example: code paths, connection, privacy, tolerance, as-of time, per-table keys and freshness limits, and metrics with their report cell.

Limitations

  • Sums and counts are traced hop by hop; averages and ratios through their two additive parts. Medians, percentiles and distinct counts are not additive and cannot be reconciled this way.
  • dbt Jinja beyond ref, source and config (macros, loops) marks the model opaque; use dbt compile and point code.dbt.manifest at target/manifest.json to trace compiled SQL.
  • The PySpark translator covers the DataFrame API used in ETL (select, filter, withColumn, join, union, groupBy, agg, drop, dedup, renames, spark.sql strings). UDFs, RDDs and dynamic code are marked opaque with their upstream tables, so lineage continues but that hop is not reconciled. The same holds for SAS RETAIN, DO loops, arrays and macro logic, Informatica Java or Normalizer transformations and $$ parameters, and MERGE upserts, whose result depends on rows already in the table.
  • Dynamic SQL (EXEC of a string, EXECUTE IMMEDIATE) cannot be read statically. It is listed under parse issues, not silently skipped.
  • IF branches inside procedures are assumed to have run; each one is noted on the hop.
  • dropDuplicates and PROC SORT NODUPKEY keep an arbitrary or input-order row; the translation keeps the first by key and the pack notes it.
  • Informatica session-level overrides (SQL or table name set in the workflow, not the mapping) are not read.

Development

pip install -e ".[dev]" pyspark
ruff check . && ruff format --check . && mypy src
pytest                                      # Postgres tests run when NP_TEST_POSTGRES_URL is set

CI runs lint, types and the full suite on Python 3.10 to 3.13, against a real PostgreSQL service and a real local Spark.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md and the Code of Conduct. Report security issues privately as described in SECURITY.md. For help, see SUPPORT.md. Changes are listed in CHANGELOG.md.

License

Copyright 2026 Sagar Konkanagaon. Licensed under the Apache License, Version 2.0; see NOTICE. You may use, modify and distribute it, including commercially, under the terms of that license.

Metadata

Release files for number-passport 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for number-passport 0.2.0
File Size Uploaded Provenance Actions
number_passport-0.2.0.tar.gz 515.2 kB With provenance Details for number_passport-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for number-passport 0.2.0
File Interpreter ABI Platform Actions
number_passport-0.2.0-py3-none-any.whl Python 3 none any Details for number_passport-0.2.0-py3-none-any.whl

Total release size: 618.6 kB

Release files / number_passport-0.2.0.tar.gz

Download URL number_passport-0.2.0.tar.gz
Size 515.2 kB
Tags Source
SHA-256 checksum
How to use checksums
fe97a2e1ac0e4dd2acd3f22504ab63729dd6130bf88ad677bef56313df62afbd
BLAKE2b-256 checksum
How to use checksums
d692c9b5884bff84c9b9d717a8234d9f267d1be421c0d19ddba1c3ecd86a5686
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / number_passport-0.2.0-py3-none-any.whl

Download URL number_passport-0.2.0-py3-none-any.whl
Size 103.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e2e75ce75b6ac3e406305491a191bb0d3d7886ba924a89b50234984fc4f6d167
BLAKE2b-256 checksum
How to use checksums
46df119d28b9b9e7f22181238a855a5b78c057ff0d228ab4a05c0f20eb40947f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Security for number-passport 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for number_passport-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page