From e670ef7f7902b966bc145610be7ed981fcb88d1c Mon Sep 17 00:00:00 2001 From: dev-belly Date: Thu, 1 Oct 2026 08:37:49 +0200 Subject: [PATCH] Implement availability-aware snapshots with independent replay and counterexamples --- .github/workflows/ci.yml | 30 +++++ .gitignore | 7 ++ LICENSE | 21 ++++ README.md | 68 ++++++++++- README.zh-CN.md | 35 ++++++ demo/comparison.csv | 46 ++++++++ demo/inputs.json | 231 ++++++++++++++++++++++++++++++++++++++ demo/manifest.json | 11 ++ demo/report.html | 18 +++ demo/snapshots.csv | 46 ++++++++ demo/summary.json | 17 +++ docs/DATA_CONTRACT.md | 45 ++++++++ docs/INTERVIEW.md | 31 +++++ docs/METHODOLOGY.md | 44 ++++++++ docs/evidence.png | Bin 0 -> 134347 bytes pyproject.toml | 20 ++++ scripts/render_figures.py | 58 ++++++++++ src/pitbridge/__init__.py | 6 + src/pitbridge/__main__.py | 3 + src/pitbridge/bundle.py | 138 +++++++++++++++++++++++ src/pitbridge/cli.py | 24 ++++ src/pitbridge/core.py | 209 ++++++++++++++++++++++++++++++++++ src/pitbridge/demo.py | 24 ++++ tests/test_bundle.py | 82 ++++++++++++++ tests/test_temporal.py | 179 +++++++++++++++++++++++++++++ 25 files changed, 1392 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.zh-CN.md create mode 100644 demo/comparison.csv create mode 100644 demo/inputs.json create mode 100644 demo/manifest.json create mode 100644 demo/report.html create mode 100644 demo/snapshots.csv create mode 100644 demo/summary.json create mode 100644 docs/DATA_CONTRACT.md create mode 100644 docs/INTERVIEW.md create mode 100644 docs/METHODOLOGY.md create mode 100644 docs/evidence.png create mode 100644 pyproject.toml create mode 100644 scripts/render_figures.py create mode 100644 src/pitbridge/__init__.py create mode 100644 src/pitbridge/__main__.py create mode 100644 src/pitbridge/bundle.py create mode 100644 src/pitbridge/cli.py create mode 100644 src/pitbridge/core.py create mode 100644 src/pitbridge/demo.py create mode 100644 tests/test_bundle.py create mode 100644 tests/test_temporal.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..85fc777 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + push: + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install + run: | + python -m pip install -e . + - name: Counterexamples and contracts + run: python -m unittest discover -s tests -v + - name: Replay committed evidence + run: pitbridge verify --out demo + - name: Generate and replay a fresh report + run: | + pitbridge demo --out outputs + pitbridge verify --out outputs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23d6dd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +build/ +dist/ +outputs*/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a44f1a6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 dev-belly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3e76271..d862123 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,68 @@ # PITBridge -Point-in-time financial feature engineering with DuckDB, revision-safe snapshots, data contracts and source lineage. + +**Financial features as they were known when a decision was made.** + +[![CI](https://github.com/dev-belly/PITBridge/actions/workflows/ci.yml/badge.svg)](https://github.com/dev-belly/PITBridge/actions/workflows/ci.yml) +[中文说明](README.zh-CN.md) · [Methodology](docs/METHODOLOGY.md) · [Data contract](docs/DATA_CONTRACT.md) · [Interview notes](docs/INTERVIEW.md) + +An invoice dated January can be published in February, arrive in March, and be corrected later. Joining on January's date alone can put future knowledge into an earlier credit decision. PITBridge resolves **event time, publication time, ingestion time and observation revisions** before exporting a feature snapshot. + +![Decision-time leakage counterexample](docs/evidence.png) + +## Run in one minute + +```bash +git clone https://github.com/dev-belly/PITBridge.git +cd PITBridge +python -m pip install -e . +pitbridge demo --out outputs +pitbridge verify --out outputs +python -m unittest discover -s tests -v +``` + +Open `outputs/report.html` in a browser. The report is self-contained, works offline and includes a filter for changed selections. Python 3.11+; **no third-party runtime dependencies**. + +```bash +# Bring your own observations, decisions and feature specs: +pitbridge build --inputs demo/inputs.json --out outputs +``` + +## What the saved example proves + +The deliberately small synthetic fixture is hand-auditable: **11 observations, 15 decisions, 45 feature lookups** across tax, bank and utility sources. + +| Saved result | Count | Interpretation | +| :--- | ---: | :--- | +| Unsafe selections using future knowledge | 11 | The event-only baseline selected a record unavailable at decision time. | +| Selections changed by the full contract | 16 | Includes freshness and tombstone effects as well as future knowledge. | +| Selected / missing feature rows | 16 / 29 | Missingness is preserved with a reason, never silently replaced by zero. | + +Inspect [the comparison](demo/comparison.csv), [record-level lineage](demo/snapshots.csv), [normalized inputs](demo/inputs.json), and [summary](demo/summary.json). These counts demonstrate the fixture; they are not a population leakage rate. + +## Selection contract + +1. Require `event_at <= decision_at` and `max(published_at, ingested_at) <= decision_at`. +2. For each entity/source/feature/event, select the **highest known revision**, even if a lower revision arrives later. +3. Apply known tombstones to that event. Do not resurrect its earlier revision. +4. Select the latest remaining event within the feature's inclusive freshness window. +5. Preserve a row for every decision/spec pair, including `no_history`, `not_available`, `deleted` and `stale`. + +The production path is a SQLite window-function join. A separate Python enumerator checks its results, including randomized histories and append-future invariance. [Read the implementation](src/pitbridge/core.py). + +## Evidence you can replay + +| Artifact | Purpose | +| :--- | :--- | +| `inputs.json` | Canonical, normalized source records, decisions and feature contracts. | +| `snapshots.csv` | Values, missingness, selected record IDs, revisions and timestamps. | +| `comparison.csv` | Explicitly unsafe event-only baseline; never used as training features. | +| `summary.json`, `report.html` | Derived counts and an offline inspection report. | +| `manifest.json` | SHA-256 hashes of all five artifacts. | + +Verification checks hashes **and** reruns both algorithms, rejecting a fabricated summary even if its hash was updated. Hashes are not signatures and cannot establish that an external source is truthful. + +## Scope + +This is a reference implementation for scalar financial observations. It does not provide streaming ingestion, access control, label generation or aggregated rolling features. Version numbers and trustworthy availability timestamps must come from the upstream source contract. SQLite is intentionally inspectable; distributed-scale performance has not been benchmarked. + +PITBridge complements [CreditVintage](https://github.com/dev-belly/CreditVintage)'s application-time model evaluation. The repositories are separate components; no integration is claimed. MIT license. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..1094cbc --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,35 @@ +# PITBridge|金融时点数据与版本溯源 + +**核心问题:做出信贷决策时,银行当时到底能看到哪个版本的数据?** + +例如,一月份开票金额在二月份公开、三月份才进入银行系统,四月份又被修订。直接按照“一月份”去关联历史申请,会把后来的信息放进早期特征,导致回测过于乐观。 + +PITBridge 同时处理数据发生时间、公开时间、入库时间和修订版本;以 SQLite SQL 生成特征,用独立 Python 枚举结果核验,并保留每一个特征的来源记录。 + +![保存结果的可复现图表](docs/evidence.png) + +## 快速运行 + +```bash +python -m pip install -e . +pitbridge demo --out outputs +pitbridge verify --out outputs +python -m unittest discover -s tests -v +``` + +打开 `outputs/report.html` 即可查看离线报告。需要 Python 3.11 或更高版本,无第三方运行依赖。 + +## 已实现的重点 + +- **防止未来信息泄漏:** 数据发生时间与真正可用时间都必须不晚于申请时间。 +- **正确处理修订:** 选择当时已知的最高版本;迟到的旧版本不会覆盖新版本。 +- **撤销与过期:** 撤销记录只影响对应观察时点,不能重新使用该记录旧版本;特征可配置有效天数。 +- **显式缺失:** 区分没有历史、尚未可用、已撤销和已过期,保留申请行。 +- **逐条溯源:** 导出来源、记录 ID、版本、发生时间、公开时间与入库时间。 +- **可重放证据:** 修改报告并更新哈希,仍会被语义重放发现。 + +合成示例有 11 条源记录、15 次决策、45 次特征查询。错误的“只按发生时间关联”基线有 11 次使用未来信息;完整规则共改变 16 次选择。后者还包括撤销和过期影响,不能把两个数字混为一谈。 + +[方法与规则](docs/METHODOLOGY.md) · [输入字段](docs/DATA_CONTRACT.md) · [中文面试问答及简历表述](docs/INTERVIEW.md) + +这是可以核验的金融数据工程研究组件。演示数据全部为合成;没有宣称接入真实银行、降低实际违约率或处理生产规模数据。 diff --git a/demo/comparison.csv b/demo/comparison.csv new file mode 100644 index 0000000..72bca34 --- /dev/null +++ b/demo/comparison.csv @@ -0,0 +1,46 @@ +decision_id,source,feature,safe_record_id,safe_value,safe_status,unsafe_record_id,unsafe_value,unsafe_uses_future_knowledge,different_selection +SME-A-1,bank,net_inflow,,,no_history,,,False,False +SME-A-1,tax,invoice_revenue,,,not_available,tax-a-v2,180000,True,True +SME-A-1,utility,kwh,power-a,1800.0,selected,power-a,1800,False,False +SME-A-2,bank,net_inflow,cash-a,50000.0,selected,cash-a-delete,,True,True +SME-A-2,tax,invoice_revenue,tax-a-v1,100000.0,selected,tax-a-v2,180000,True,True +SME-A-2,utility,kwh,power-a,1800.0,selected,power-a,1800,False,False +SME-A-3,bank,net_inflow,,,deleted,cash-a-delete,,False,True +SME-A-3,tax,invoice_revenue,tax-a-v1,100000.0,selected,tax-a-v2,180000,True,True +SME-A-3,utility,kwh,power-a,1800.0,selected,power-a,1800,False,False +SME-A-4,bank,net_inflow,,,deleted,cash-a-delete,,False,True +SME-A-4,tax,invoice_revenue,tax-a-v2,180000.0,selected,tax-a-feb,220000,True,True +SME-A-4,utility,kwh,power-a,1800.0,selected,power-a,1800,False,False +SME-A-5,bank,net_inflow,,,deleted,cash-a-delete,,False,True +SME-A-5,tax,invoice_revenue,tax-a-feb,220000.0,selected,tax-a-feb,220000,False,False +SME-A-5,utility,kwh,,,stale,power-a,1800,False,True +SME-B-1,bank,net_inflow,,,no_history,,,False,False +SME-B-1,tax,invoice_revenue,,,not_available,tax-b,90000,True,True +SME-B-1,utility,kwh,,,stale,power-b,1300,True,True +SME-B-2,bank,net_inflow,cash-b-v2,30000.0,selected,cash-b-v2,30000,False,False +SME-B-2,tax,invoice_revenue,,,not_available,tax-b,90000,True,True +SME-B-2,utility,kwh,,,stale,power-b,1300,True,True +SME-B-3,bank,net_inflow,cash-b-v2,30000.0,selected,cash-b-v2,30000,False,False +SME-B-3,tax,invoice_revenue,,,not_available,tax-b,90000,True,True +SME-B-3,utility,kwh,,,stale,power-b,1300,True,True +SME-B-4,bank,net_inflow,cash-b-v2,30000.0,selected,cash-b-v2,30000,False,False +SME-B-4,tax,invoice_revenue,tax-b,90000.0,selected,tax-b,90000,False,False +SME-B-4,utility,kwh,power-b,1300.0,selected,power-b,1300,False,False +SME-B-5,bank,net_inflow,cash-b-v2,30000.0,selected,cash-b-v2,30000,False,False +SME-B-5,tax,invoice_revenue,tax-b,90000.0,selected,tax-b,90000,False,False +SME-B-5,utility,kwh,,,stale,power-b,1300,False,True +SME-C-1,bank,net_inflow,,,no_history,,,False,False +SME-C-1,tax,invoice_revenue,,,no_history,,,False,False +SME-C-1,utility,kwh,,,no_history,,,False,False +SME-C-2,bank,net_inflow,,,no_history,,,False,False +SME-C-2,tax,invoice_revenue,,,no_history,,,False,False +SME-C-2,utility,kwh,,,no_history,,,False,False +SME-C-3,bank,net_inflow,,,no_history,,,False,False +SME-C-3,tax,invoice_revenue,,,no_history,,,False,False +SME-C-3,utility,kwh,,,no_history,,,False,False +SME-C-4,bank,net_inflow,,,no_history,,,False,False +SME-C-4,tax,invoice_revenue,,,no_history,,,False,False +SME-C-4,utility,kwh,,,no_history,,,False,False +SME-C-5,bank,net_inflow,,,no_history,,,False,False +SME-C-5,tax,invoice_revenue,,,no_history,,,False,False +SME-C-5,utility,kwh,,,no_history,,,False,False diff --git a/demo/inputs.json b/demo/inputs.json new file mode 100644 index 0000000..ee99b56 --- /dev/null +++ b/demo/inputs.json @@ -0,0 +1,231 @@ +{ + "data_kind": "synthetic", + "decisions": [ + { + "decision_at": "2026-02-04T00:00:00.000000+00:00", + "decision_id": "SME-A-1", + "entity_id": "SME-A" + }, + { + "decision_at": "2026-02-15T00:00:00.000000+00:00", + "decision_id": "SME-A-2", + "entity_id": "SME-A" + }, + { + "decision_at": "2026-02-20T00:00:00.000000+00:00", + "decision_id": "SME-A-3", + "entity_id": "SME-A" + }, + { + "decision_at": "2026-03-15T00:00:00.000000+00:00", + "decision_id": "SME-A-4", + "entity_id": "SME-A" + }, + { + "decision_at": "2026-03-25T00:00:00.000000+00:00", + "decision_id": "SME-A-5", + "entity_id": "SME-A" + }, + { + "decision_at": "2026-02-04T00:00:00.000000+00:00", + "decision_id": "SME-B-1", + "entity_id": "SME-B" + }, + { + "decision_at": "2026-02-15T00:00:00.000000+00:00", + "decision_id": "SME-B-2", + "entity_id": "SME-B" + }, + { + "decision_at": "2026-02-20T00:00:00.000000+00:00", + "decision_id": "SME-B-3", + "entity_id": "SME-B" + }, + { + "decision_at": "2026-03-15T00:00:00.000000+00:00", + "decision_id": "SME-B-4", + "entity_id": "SME-B" + }, + { + "decision_at": "2026-03-25T00:00:00.000000+00:00", + "decision_id": "SME-B-5", + "entity_id": "SME-B" + }, + { + "decision_at": "2026-02-04T00:00:00.000000+00:00", + "decision_id": "SME-C-1", + "entity_id": "SME-C" + }, + { + "decision_at": "2026-02-15T00:00:00.000000+00:00", + "decision_id": "SME-C-2", + "entity_id": "SME-C" + }, + { + "decision_at": "2026-02-20T00:00:00.000000+00:00", + "decision_id": "SME-C-3", + "entity_id": "SME-C" + }, + { + "decision_at": "2026-03-15T00:00:00.000000+00:00", + "decision_id": "SME-C-4", + "entity_id": "SME-C" + }, + { + "decision_at": "2026-03-25T00:00:00.000000+00:00", + "decision_id": "SME-C-5", + "entity_id": "SME-C" + } + ], + "observations": [ + { + "deleted": false, + "entity_id": "SME-A", + "event_at": "2026-02-10T00:00:00.000000+00:00", + "feature": "net_inflow", + "ingested_at": "2026-02-11T00:00:00.000000+00:00", + "published_at": "2026-02-10T00:00:00.000000+00:00", + "record_id": "cash-a", + "revision": 1, + "source": "bank", + "value": 50000 + }, + { + "deleted": true, + "entity_id": "SME-A", + "event_at": "2026-02-10T00:00:00.000000+00:00", + "feature": "net_inflow", + "ingested_at": "2026-02-18T00:00:00.000000+00:00", + "published_at": "2026-02-18T00:00:00.000000+00:00", + "record_id": "cash-a-delete", + "revision": 2, + "source": "bank", + "value": null + }, + { + "deleted": false, + "entity_id": "SME-B", + "event_at": "2026-02-10T00:00:00.000000+00:00", + "feature": "net_inflow", + "ingested_at": "2026-02-19T00:00:00.000000+00:00", + "published_at": "2026-02-11T00:00:00.000000+00:00", + "record_id": "cash-b-v1-delayed", + "revision": 1, + "source": "bank", + "value": 80000 + }, + { + "deleted": false, + "entity_id": "SME-B", + "event_at": "2026-02-10T00:00:00.000000+00:00", + "feature": "net_inflow", + "ingested_at": "2026-02-13T00:00:00.000000+00:00", + "published_at": "2026-02-12T00:00:00.000000+00:00", + "record_id": "cash-b-v2", + "revision": 2, + "source": "bank", + "value": 30000 + }, + { + "deleted": false, + "entity_id": "SME-A", + "event_at": "2026-02-01T00:00:00.000000+00:00", + "feature": "kwh", + "ingested_at": "2026-02-03T00:00:00.000000+00:00", + "published_at": "2026-02-02T00:00:00.000000+00:00", + "record_id": "power-a", + "revision": 1, + "source": "utility", + "value": 1800 + }, + { + "deleted": false, + "entity_id": "SME-B", + "event_at": "2026-02-01T00:00:00.000000+00:00", + "feature": "kwh", + "ingested_at": "2026-03-02T00:00:00.000000+00:00", + "published_at": "2026-02-03T00:00:00.000000+00:00", + "record_id": "power-b", + "revision": 1, + "source": "utility", + "value": 1300 + }, + { + "deleted": false, + "entity_id": "SME-B", + "event_at": "2025-11-01T00:00:00.000000+00:00", + "feature": "kwh", + "ingested_at": "2025-11-02T00:00:00.000000+00:00", + "published_at": "2025-11-02T00:00:00.000000+00:00", + "record_id": "power-b-old", + "revision": 1, + "source": "utility", + "value": 700 + }, + { + "deleted": false, + "entity_id": "SME-A", + "event_at": "2026-02-28T00:00:00.000000+00:00", + "feature": "invoice_revenue", + "ingested_at": "2026-03-20T00:00:00.000000+00:00", + "published_at": "2026-03-05T00:00:00.000000+00:00", + "record_id": "tax-a-feb", + "revision": 1, + "source": "tax", + "value": 220000 + }, + { + "deleted": false, + "entity_id": "SME-A", + "event_at": "2026-01-31T00:00:00.000000+00:00", + "feature": "invoice_revenue", + "ingested_at": "2026-02-06T00:00:00.000000+00:00", + "published_at": "2026-02-05T00:00:00.000000+00:00", + "record_id": "tax-a-v1", + "revision": 1, + "source": "tax", + "value": 100000 + }, + { + "deleted": false, + "entity_id": "SME-A", + "event_at": "2026-01-31T00:00:00.000000+00:00", + "feature": "invoice_revenue", + "ingested_at": "2026-03-11T00:00:00.000000+00:00", + "published_at": "2026-03-10T00:00:00.000000+00:00", + "record_id": "tax-a-v2", + "revision": 2, + "source": "tax", + "value": 180000 + }, + { + "deleted": false, + "entity_id": "SME-B", + "event_at": "2026-01-31T00:00:00.000000+00:00", + "feature": "invoice_revenue", + "ingested_at": "2026-03-01T00:00:00.000000+00:00", + "published_at": "2026-02-05T00:00:00.000000+00:00", + "record_id": "tax-b", + "revision": 1, + "source": "tax", + "value": 90000 + } + ], + "specs": [ + { + "feature": "net_inflow", + "max_age_days": 60, + "source": "bank" + }, + { + "feature": "invoice_revenue", + "max_age_days": 90, + "source": "tax" + }, + { + "feature": "kwh", + "max_age_days": 45, + "source": "utility" + } + ] +} diff --git a/demo/manifest.json b/demo/manifest.json new file mode 100644 index 0000000..62e1258 --- /dev/null +++ b/demo/manifest.json @@ -0,0 +1,11 @@ +{ + "engine": "pitbridge/0.1.0", + "files": { + "comparison.csv": "0eb458eceec7477682417da3bcfc24c1746021c52ad83b24c7a4a7e5fc4f734e", + "inputs.json": "d55c44483897bd3ed838376b3e7a11f7d56ee41ef5cf8e6e22c99d060d701a98", + "report.html": "39b0dbcc2bd7c13447127990d4cece668fdbc1e15a180c969100dac8fa6d0f4c", + "snapshots.csv": "777f54dea6c8067cccb9a9e5ec375038a99dd2e44c9ab1df52cfae029a31e6f0", + "summary.json": "fe3d30c39e8eb5244636e150f2da884e898456a1b5f52c2c42f0a65cf54e496b" + }, + "schema_version": 1 +} diff --git a/demo/report.html b/demo/report.html new file mode 100644 index 0000000..18f28ac --- /dev/null +++ b/demo/report.html @@ -0,0 +1,18 @@ + + +PITBridge · Decision-time evidence +
PITBRIDGE / SYNTHETIC FINANCIAL DATA
+

What was known
when the decision was made?

+

Availability-aware SQL snapshots with publication, ingestion, revision and record-level lineage.

+
15Decisions
45Feature lookups
11Unsafe future-data selections
16Selections changed
+

Independent replay: SQLite output matches the Python enumeration oracle. deleted: 3 · no_history: 17 · not_available: 4 · selected: 16 · stale: 5

+

Inspect the leakage counterexample

The unsafe comparison filters only event time and takes the highest revision. It intentionally ignores availability, tombstones and freshness; it is never exported as a training feature.

+ +
DecisionSourceFeatureSafe valueSafe statusUnsafe valueUses future knowledge?
SME-A-1banknet_inflow—no_history—False
SME-A-1taxinvoice_revenue—not_available180000True
SME-A-1utilitykwh1800.0selected1800False
SME-A-2banknet_inflow50000.0selected—True
SME-A-2taxinvoice_revenue100000.0selected180000True
SME-A-2utilitykwh1800.0selected1800False
SME-A-3banknet_inflow—deleted—False
SME-A-3taxinvoice_revenue100000.0selected180000True
SME-A-3utilitykwh1800.0selected1800False
SME-A-4banknet_inflow—deleted—False
SME-A-4taxinvoice_revenue180000.0selected220000True
SME-A-4utilitykwh1800.0selected1800False
SME-A-5banknet_inflow—deleted—False
SME-A-5taxinvoice_revenue220000.0selected220000False
SME-A-5utilitykwh—stale1800False
SME-B-1banknet_inflow—no_history—False
SME-B-1taxinvoice_revenue—not_available90000True
SME-B-1utilitykwh—stale1300True
SME-B-2banknet_inflow30000.0selected30000False
SME-B-2taxinvoice_revenue—not_available90000True
SME-B-2utilitykwh—stale1300True
SME-B-3banknet_inflow30000.0selected30000False
SME-B-3taxinvoice_revenue—not_available90000True
SME-B-3utilitykwh—stale1300True
SME-B-4banknet_inflow30000.0selected30000False
SME-B-4taxinvoice_revenue90000.0selected90000False
SME-B-4utilitykwh1300.0selected1300False
SME-B-5banknet_inflow30000.0selected30000False
SME-B-5taxinvoice_revenue90000.0selected90000False
SME-B-5utilitykwh—stale1300False
SME-C-1banknet_inflow—no_history—False
SME-C-1taxinvoice_revenue—no_history—False
SME-C-1utilitykwh—no_history—False
SME-C-2banknet_inflow—no_history—False
SME-C-2taxinvoice_revenue—no_history—False
SME-C-2utilitykwh—no_history—False
SME-C-3banknet_inflow—no_history—False
SME-C-3taxinvoice_revenue—no_history—False
SME-C-3utilitykwh—no_history—False
SME-C-4banknet_inflow—no_history—False
SME-C-4taxinvoice_revenue—no_history—False
SME-C-4utilitykwh—no_history—False
SME-C-5banknet_inflow—no_history—False
SME-C-5taxinvoice_revenue—no_history—False
SME-C-5utilitykwh—no_history—False
+

Trace each selected observation

Knowledge time = max(publication, ingestion). Every selected observation was available at or before the decision. A later historical correction cannot overwrite an earlier snapshot.

+
DecisionSourceFeatureRecordRevisionEvent timeAvailable time
SME-A-1utilitykwhpower-a12026-02-01T00:00:00.000000+00:002026-02-03T00:00:00.000000+00:00
SME-A-2banknet_inflowcash-a12026-02-10T00:00:00.000000+00:002026-02-11T00:00:00.000000+00:00
SME-A-2taxinvoice_revenuetax-a-v112026-01-31T00:00:00.000000+00:002026-02-06T00:00:00.000000+00:00
SME-A-2utilitykwhpower-a12026-02-01T00:00:00.000000+00:002026-02-03T00:00:00.000000+00:00
SME-A-3taxinvoice_revenuetax-a-v112026-01-31T00:00:00.000000+00:002026-02-06T00:00:00.000000+00:00
SME-A-3utilitykwhpower-a12026-02-01T00:00:00.000000+00:002026-02-03T00:00:00.000000+00:00
SME-A-4taxinvoice_revenuetax-a-v222026-01-31T00:00:00.000000+00:002026-03-11T00:00:00.000000+00:00
SME-A-4utilitykwhpower-a12026-02-01T00:00:00.000000+00:002026-02-03T00:00:00.000000+00:00
SME-A-5taxinvoice_revenuetax-a-feb12026-02-28T00:00:00.000000+00:002026-03-20T00:00:00.000000+00:00
SME-B-2banknet_inflowcash-b-v222026-02-10T00:00:00.000000+00:002026-02-13T00:00:00.000000+00:00
SME-B-3banknet_inflowcash-b-v222026-02-10T00:00:00.000000+00:002026-02-13T00:00:00.000000+00:00
SME-B-4banknet_inflowcash-b-v222026-02-10T00:00:00.000000+00:002026-02-13T00:00:00.000000+00:00
SME-B-4taxinvoice_revenuetax-b12026-01-31T00:00:00.000000+00:002026-03-01T00:00:00.000000+00:00
SME-B-4utilitykwhpower-b12026-02-01T00:00:00.000000+00:002026-03-02T00:00:00.000000+00:00
SME-B-5banknet_inflowcash-b-v222026-02-10T00:00:00.000000+00:002026-02-13T00:00:00.000000+00:00
SME-B-5taxinvoice_revenuetax-b12026-01-31T00:00:00.000000+00:002026-03-01T00:00:00.000000+00:00
+

Replay locally

pitbridge verify --out demo checks file hashes, recomputes every snapshot through both implementations, and compares all derived evidence. Hashes detect accidental edits; they are not a digital signature.

+
+ diff --git a/demo/snapshots.csv b/demo/snapshots.csv new file mode 100644 index 0000000..e7a613b --- /dev/null +++ b/demo/snapshots.csv @@ -0,0 +1,46 @@ +decision_id,entity_id,decision_at,source,feature,value,status,record_id,revision,event_at,published_at,ingested_at,available_at +SME-A-1,SME-A,2026-02-04T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-A-1,SME-A,2026-02-04T00:00:00.000000+00:00,tax,invoice_revenue,,not_available,,,,,, +SME-A-1,SME-A,2026-02-04T00:00:00.000000+00:00,utility,kwh,1800.0,selected,power-a,1,2026-02-01T00:00:00.000000+00:00,2026-02-02T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00 +SME-A-2,SME-A,2026-02-15T00:00:00.000000+00:00,bank,net_inflow,50000.0,selected,cash-a,1,2026-02-10T00:00:00.000000+00:00,2026-02-10T00:00:00.000000+00:00,2026-02-11T00:00:00.000000+00:00,2026-02-11T00:00:00.000000+00:00 +SME-A-2,SME-A,2026-02-15T00:00:00.000000+00:00,tax,invoice_revenue,100000.0,selected,tax-a-v1,1,2026-01-31T00:00:00.000000+00:00,2026-02-05T00:00:00.000000+00:00,2026-02-06T00:00:00.000000+00:00,2026-02-06T00:00:00.000000+00:00 +SME-A-2,SME-A,2026-02-15T00:00:00.000000+00:00,utility,kwh,1800.0,selected,power-a,1,2026-02-01T00:00:00.000000+00:00,2026-02-02T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00 +SME-A-3,SME-A,2026-02-20T00:00:00.000000+00:00,bank,net_inflow,,deleted,,,,,, +SME-A-3,SME-A,2026-02-20T00:00:00.000000+00:00,tax,invoice_revenue,100000.0,selected,tax-a-v1,1,2026-01-31T00:00:00.000000+00:00,2026-02-05T00:00:00.000000+00:00,2026-02-06T00:00:00.000000+00:00,2026-02-06T00:00:00.000000+00:00 +SME-A-3,SME-A,2026-02-20T00:00:00.000000+00:00,utility,kwh,1800.0,selected,power-a,1,2026-02-01T00:00:00.000000+00:00,2026-02-02T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00 +SME-A-4,SME-A,2026-03-15T00:00:00.000000+00:00,bank,net_inflow,,deleted,,,,,, +SME-A-4,SME-A,2026-03-15T00:00:00.000000+00:00,tax,invoice_revenue,180000.0,selected,tax-a-v2,2,2026-01-31T00:00:00.000000+00:00,2026-03-10T00:00:00.000000+00:00,2026-03-11T00:00:00.000000+00:00,2026-03-11T00:00:00.000000+00:00 +SME-A-4,SME-A,2026-03-15T00:00:00.000000+00:00,utility,kwh,1800.0,selected,power-a,1,2026-02-01T00:00:00.000000+00:00,2026-02-02T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00 +SME-A-5,SME-A,2026-03-25T00:00:00.000000+00:00,bank,net_inflow,,deleted,,,,,, +SME-A-5,SME-A,2026-03-25T00:00:00.000000+00:00,tax,invoice_revenue,220000.0,selected,tax-a-feb,1,2026-02-28T00:00:00.000000+00:00,2026-03-05T00:00:00.000000+00:00,2026-03-20T00:00:00.000000+00:00,2026-03-20T00:00:00.000000+00:00 +SME-A-5,SME-A,2026-03-25T00:00:00.000000+00:00,utility,kwh,,stale,,,,,, +SME-B-1,SME-B,2026-02-04T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-B-1,SME-B,2026-02-04T00:00:00.000000+00:00,tax,invoice_revenue,,not_available,,,,,, +SME-B-1,SME-B,2026-02-04T00:00:00.000000+00:00,utility,kwh,,stale,,,,,, +SME-B-2,SME-B,2026-02-15T00:00:00.000000+00:00,bank,net_inflow,30000.0,selected,cash-b-v2,2,2026-02-10T00:00:00.000000+00:00,2026-02-12T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00 +SME-B-2,SME-B,2026-02-15T00:00:00.000000+00:00,tax,invoice_revenue,,not_available,,,,,, +SME-B-2,SME-B,2026-02-15T00:00:00.000000+00:00,utility,kwh,,stale,,,,,, +SME-B-3,SME-B,2026-02-20T00:00:00.000000+00:00,bank,net_inflow,30000.0,selected,cash-b-v2,2,2026-02-10T00:00:00.000000+00:00,2026-02-12T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00 +SME-B-3,SME-B,2026-02-20T00:00:00.000000+00:00,tax,invoice_revenue,,not_available,,,,,, +SME-B-3,SME-B,2026-02-20T00:00:00.000000+00:00,utility,kwh,,stale,,,,,, +SME-B-4,SME-B,2026-03-15T00:00:00.000000+00:00,bank,net_inflow,30000.0,selected,cash-b-v2,2,2026-02-10T00:00:00.000000+00:00,2026-02-12T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00 +SME-B-4,SME-B,2026-03-15T00:00:00.000000+00:00,tax,invoice_revenue,90000.0,selected,tax-b,1,2026-01-31T00:00:00.000000+00:00,2026-02-05T00:00:00.000000+00:00,2026-03-01T00:00:00.000000+00:00,2026-03-01T00:00:00.000000+00:00 +SME-B-4,SME-B,2026-03-15T00:00:00.000000+00:00,utility,kwh,1300.0,selected,power-b,1,2026-02-01T00:00:00.000000+00:00,2026-02-03T00:00:00.000000+00:00,2026-03-02T00:00:00.000000+00:00,2026-03-02T00:00:00.000000+00:00 +SME-B-5,SME-B,2026-03-25T00:00:00.000000+00:00,bank,net_inflow,30000.0,selected,cash-b-v2,2,2026-02-10T00:00:00.000000+00:00,2026-02-12T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00,2026-02-13T00:00:00.000000+00:00 +SME-B-5,SME-B,2026-03-25T00:00:00.000000+00:00,tax,invoice_revenue,90000.0,selected,tax-b,1,2026-01-31T00:00:00.000000+00:00,2026-02-05T00:00:00.000000+00:00,2026-03-01T00:00:00.000000+00:00,2026-03-01T00:00:00.000000+00:00 +SME-B-5,SME-B,2026-03-25T00:00:00.000000+00:00,utility,kwh,,stale,,,,,, +SME-C-1,SME-C,2026-02-04T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-C-1,SME-C,2026-02-04T00:00:00.000000+00:00,tax,invoice_revenue,,no_history,,,,,, +SME-C-1,SME-C,2026-02-04T00:00:00.000000+00:00,utility,kwh,,no_history,,,,,, +SME-C-2,SME-C,2026-02-15T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-C-2,SME-C,2026-02-15T00:00:00.000000+00:00,tax,invoice_revenue,,no_history,,,,,, +SME-C-2,SME-C,2026-02-15T00:00:00.000000+00:00,utility,kwh,,no_history,,,,,, +SME-C-3,SME-C,2026-02-20T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-C-3,SME-C,2026-02-20T00:00:00.000000+00:00,tax,invoice_revenue,,no_history,,,,,, +SME-C-3,SME-C,2026-02-20T00:00:00.000000+00:00,utility,kwh,,no_history,,,,,, +SME-C-4,SME-C,2026-03-15T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-C-4,SME-C,2026-03-15T00:00:00.000000+00:00,tax,invoice_revenue,,no_history,,,,,, +SME-C-4,SME-C,2026-03-15T00:00:00.000000+00:00,utility,kwh,,no_history,,,,,, +SME-C-5,SME-C,2026-03-25T00:00:00.000000+00:00,bank,net_inflow,,no_history,,,,,, +SME-C-5,SME-C,2026-03-25T00:00:00.000000+00:00,tax,invoice_revenue,,no_history,,,,,, +SME-C-5,SME-C,2026-03-25T00:00:00.000000+00:00,utility,kwh,,no_history,,,,,, diff --git a/demo/summary.json b/demo/summary.json new file mode 100644 index 0000000..7ab0889 --- /dev/null +++ b/demo/summary.json @@ -0,0 +1,17 @@ +{ + "data_kind": "synthetic", + "decisions": 15, + "different_selections": 16, + "future_knowledge_rows": 11, + "independent_replay": true, + "observations": 11, + "schema_version": 1, + "snapshot_rows": 45, + "status_counts": { + "deleted": 3, + "no_history": 17, + "not_available": 4, + "selected": 16, + "stale": 5 + } +} diff --git a/docs/DATA_CONTRACT.md b/docs/DATA_CONTRACT.md new file mode 100644 index 0000000..16d4434 --- /dev/null +++ b/docs/DATA_CONTRACT.md @@ -0,0 +1,45 @@ +# Input and output contract + +The JSON root requires `observations`, `decisions` and `specs` arrays. Optional `data_kind` is `synthetic` or `user_supplied` (default). Extra fields, duplicate JSON keys and non-finite JSON constants are rejected. The provenance label is supplied by the caller; it is not externally authenticated. + +## Observations + +| Field | Type / rule | +| :--- | :--- | +| `record_id` | Unique, nonempty trimmed string. | +| `entity_id`, `source`, `feature` | Nonempty trimmed strings; each source/feature must have a spec. | +| `event_at` | Timezone-aware ISO-8601 event instant. | +| `published_at` | Timezone-aware instant, no earlier than event time. | +| `ingested_at` | Timezone-aware instant, no earlier than event time. | +| `revision` | Integer in `[1, 2^63-1]`; booleans are rejected. | +| `value` | Finite number for an active record; null for a tombstone. | +| `deleted` | Boolean, default false. | + +The logical version key `(entity_id, source, feature, event_at, revision)` must also be unique. Value units are governed upstream: the demo uses CNY for invoice revenue and bank net inflow, kWh for utility consumption. Unit conversion and numeric aggregation are outside this implementation. + +## Decisions and specs + +| Section | Fields | +| :--- | :--- | +| `decisions` | Unique `decision_id`, `entity_id`, timezone-aware `decision_at`. | +| `specs` | Unique pair `source`, `feature`; optional `max_age_days`, null or within `[0,3652500]`. | + +At least one decision and one spec are required; observations may be empty. The snapshot has exactly `len(decisions) * len(specs)` rows and a stable decision/source/feature order. + +## Example + +```json +{ + "data_kind": "synthetic", + "observations": [{ + "record_id": "tax-001-v1", "entity_id": "SME-001", "source": "tax", + "feature": "invoice_revenue", "event_at": "2026-01-31T00:00:00Z", + "published_at": "2026-02-05T00:00:00Z", "ingested_at": "2026-02-06T00:00:00Z", + "revision": 1, "value": 100000, "deleted": false + }], + "decisions": [{"decision_id": "app-001", "entity_id": "SME-001", "decision_at": "2026-02-15T00:00:00Z"}], + "specs": [{"source": "tax", "feature": "invoice_revenue", "max_age_days": 90}] +} +``` + +`snapshots.csv` exports the decision identity/time, source, feature, value, status, record ID, revision, event/publication/ingestion/availability times. Missing lineage fields are empty CSV cells. `inputs.json` retains the full normalized history, including revisions not chosen for a decision. diff --git a/docs/INTERVIEW.md b/docs/INTERVIEW.md new file mode 100644 index 0000000..9d5b97a --- /dev/null +++ b/docs/INTERVIEW.md @@ -0,0 +1,31 @@ +# 中文面试说明 + +## 30 秒介绍 + +我做的是金融时点数据组件。历史数据常常迟到、修订或者被撤销,只按照业务发生日期关联,会让模型看到当时银行还拿不到的信息。我同时约束发生时间和可用时间,先选择当时已知的修订版本,再处理撤销与过期;使用 SQL 生成特征,用独立 Python 实现重放,并导出来源记录级证据。 + +## 可以使用的简历表述 + +> 构建金融多源数据时点特征组件,使用 SQLite 窗口函数处理公开/入库延迟、历史修订、撤销及特征有效期,保留记录级溯源;用独立 Python 枚举及随机历史对照验证 SQL 输出,在 45 次合成特征查询中识别事件时间基线的 11 次未来信息使用,并实现报告哈希校验与语义重放。 + +这是候选表述。要能解释代码和假设;示例是合成数据,不能描述为银行线上项目。 + +## 常见追问 + +**为什么 `event_at` 不够?** 一月份的数据可能三月份才入库。二月份做决策时,即使数据“属于一月份”,仍然不可用。 + +**为什么可用时间取两个时间的最大值?** 当前合同要求数据已经公开,而且系统已经拿到,两个条件都满足才可用。若真实系统的可得性规则不同,应修改源合同,不能凭空猜测。 + +**为什么不按照最后入库时间选版本?** 旧版本可能迟到。最高已知源版本才是该观察时点的权威版本;乱序入库不应让旧版本覆盖新版本。 + +**撤销为什么要先选版本再过滤?** 先删除撤销行再排名,会让旧版本重新变成可用,等于把已撤销的数据复活。 + +**11 和 16 分别代表什么?** 11 次基线选择使用了未来可用信息。16 次最终选择不同,包含撤销和过期的影响;不能把 16 全部说成未来泄漏。 + +**哈希已经匹配,为什么还要重放?** 哈希只能证明文件和记录的摘要一致。有人改了指标再更新哈希,文件仍可能逻辑错误,因此要从输入重算。 + +**能支持多少数据?** 当前没有分布式性能验证。面试可以说明 SQL 逻辑、索引以及如何分区或优化候选连接,不应编造吞吐量。 + +## 建议展示路径 + +先打开 `demo/comparison.csv` 看 `SME-A-2 / tax` 的未来修订;再打开 `demo/snapshots.csv` 看选中的 `tax-a-v1`;最后展示 `test_historical_revision_cannot_change_old_decision` 和 `test_rehashed_false_metric_still_fails_semantic_replay`。 diff --git a/docs/METHODOLOGY.md b/docs/METHODOLOGY.md new file mode 100644 index 0000000..dbbcc28 --- /dev/null +++ b/docs/METHODOLOGY.md @@ -0,0 +1,44 @@ +# Methodology + +## Two clocks and a source revision + +An observation has event time `e`, publication time `p`, ingestion time `i`, and availability `a = max(p, i)`. This assumes both public release and ingestion are necessary for the decision system to use the record. It also supports pre-ingested embargoed records: ingestion alone does not make them eligible. + +For decision time `t`, the eligible set requires `e <= t` and `a <= t`. All instants are normalized to UTC at microsecond precision. Equality is inclusive. The contract rejects naive timestamps and publication/ingestion preceding the represented observation event. + +The logical observation key is `(entity_id, source, feature, event_at)`. Within the eligible set, choose the largest positive source `revision`. A higher version remains authoritative even if a lower version was delivered later. Duplicate logical key/revision pairs are rejected rather than resolved by incidental row order. + +A tombstone is a revision with `deleted=true` and `value=null`. Version resolution happens **before** removing tombstones, so a revoked observation cannot fall back to its older revision. It does not revoke the entire feature series: an earlier, separate active observation may still be selected. + +For an optional freshness limit `w`, use `0 <= t-e <= w` after resolving versions. The latest active event within the window is selected. Days mean exactly 86,400 seconds, rounded to microseconds; there is no business-calendar adjustment. + +## Missingness precedence + +| Status | Meaning | +| :--- | :--- | +| `no_history` | No observation for this entity/source/feature has event time at or before the decision. | +| `not_available` | Historical events exist, but none were available by the decision. | +| `deleted` | Known observations exist; their highest known versions are all tombstones. | +| `stale` | Known active versions exist, but none pass the freshness window. | +| `selected` | A valid record and its lineage were selected. | + +If known tombstones coexist with old active observations, and those active observations are stale, the status is `stale`. Missingness is not imputed here. + +## An auditable counterexample + +`tax-a-v1` represents 100,000 CNY of January invoice revenue and is available February 6. Revision 2 changes the value to 180,000 CNY but is not available until March 11. The February 15 decision must keep version 1. The fixture separately tests March's late ingestion, a revoked bank cash observation, UTC offset equivalence, and delayed delivery of an older revision. + +The unsafe baseline only constrains event time, choosing the newest event/highest revision. It intentionally ignores availability, deletion and freshness. `future_knowledge_rows` counts unavailable baseline records; `different_selections` also counts non-leakage contract differences. The fixture is constructed to expose these failures and is not a representative sample of bank data. + +## Verification boundaries + +SQLite resolves eligible versions and event ranks using SQL window functions. The independent Python oracle enumerates histories and versions without calling SQL. Tests compare both on randomized data, shuffled input order and appended future revisions. + +`verify` first checks artifact hashes, then derives all outputs again from the normalized input snapshot and compares them byte-for-byte. A changed metric plus an updated hash fails replay. An attacker who rewrites the source inputs and regenerates the whole bundle can produce a consistent new bundle: this workflow is not authentication or immutable source storage. + +## Sources and design context + +- [Feast point-in-time joins](https://docs.feast.dev/getting-started/concepts/point-in-time-joins): historical feature retrieval and freshness windows. PITBridge explicitly uses the maximum of publication and ingestion time in addition to event time. +- [SQLite window functions](https://www.sqlite.org/windowfunctions.html): ranking for observation revisions and event selection. + +No Feast code was copied and PITBridge is not a Feast adapter. There are no performance or production-readiness claims. diff --git a/docs/evidence.png b/docs/evidence.png new file mode 100644 index 0000000000000000000000000000000000000000..99e68bd858a93d470046388dd6de20b4c67be120 GIT binary patch literal 134347 zcmY&=1yEJ}_BKcr6k)dmTP7$5N;vO&sUIV5BxByk}@1;@nADLaKn%B1MK7!EYJUId=E z@gCzkDC>sQa*mf<#kL>(45^h|_{+JCDSq4P$sM83xNTGWSAyR=w|_I&ibbqAyz3Kp z*#50yZ*TBvrGCdn@$gSl!{;ym^F^DY5LAOA^gmyRc6zh%|MOj6?zc2?;Qh}RosXx% z)BpLByN%NKKd&Q{fqRs;jf6*-giTggYBn6h=6tm6pYYOx z&{12gnus~hKa{NArp|t~A5ZEV?@vQ+q20rR&?KG;%nGB?9J)7eXNcbUe`(NL=}Ynp z3Hdp=#Gq8r5*VF-w^UTZvSiCLSKkvPobyw>DUqlAdvuDq^UcG9W z?#1>K@#`NN61KKxpVwW$B_|L4Y|yv2{3ju8n;Pzmtf%hm83Q$EnyQ$wF}<#?E*>eV z_wMd(UZ?&0I28O|VLMo;7#KbEe5H7EpBD?rN~Kb~Vt9g-Vt%WO|NZ;->({t1KBuUZ zl&uS9qdax)M21C_r2#N949%{BbhUYNYr)NByVM@L6r?f=flZEF~Iu)?ByoT!aQM5Oe(o)X?S zy=$KE6f(V*f})~q*(a<5jp}f%hpYAl8cy(Ostd3e@_8rLY^Xh51DOmp=>((Xr)7} zRQzAW-)P@OPZrPf!y=t+^5iS|SxUEF-=;*3EZaj5J z9USI@7ZT0%@6z^+_qMj8zI*r1%gp@ZqNkkcza>OZ5)%_MTJ?eK`HL4pH4KQKAko~! zapy;QdFyZ?=?7h1xk1gdPlhHYdd*%=a1fc+i*Un{8Yfyj)k#TBRMgZ0%|+k7MJXzZ z{y3h^&XKN9NBj=)DndfSJ=4?JDCma_>*ly{Qt8lE9I0h1Dk@H$HXlf(u;u5}?*E}x z?8elrv2Cd^;|VOM@W-KOhLQ*@#}R6amO9{dsahBZbS!kp z?5+=)xLzLK`SdA^c4yw}hknnOqKQ)Pm%~Mx+PDAvI92ZPdMso~Cj>kbz3JZj_U#*q zl(h8wpAv+J+fxMQ=0if2|Eg_7O{Z$eJOu^OhK7no3|G~kTMTgbZ8$sESlZi*q)hV- zWQgkxY&|#oTh$4jiE?VH)*<}cH+n9MSVm6+hnHh3X2bpcuZQTP=N4SvmL#dnp6xAk zh)GC9I5qzF)DAFnZ0kwLY>+q{>rT&!q(L-N9$ z+D2WbZ+)~{FJC8t^rd;4L&`PgvBYRBi#BCLc2`%|?omCm_6%v<$%d)z+Fw;mo*yyO zRB35=#Kd9QGAQqJBMyUaIXLWj^86_88k?48-7QE@MR!D(EudadAHV z{qHAhJt!#nT^Y;WKgua8`j*+e8XX^P>g>EN(k_AobL+N{h{(KZlMSlB&wb}cG4{rZ zYcJ0gYyF;<{(jtnfdRP*g?#E)yO(SKXyOtQR-80VlUIxt^IyMx`?e%x#%!r?ryw^s zAWtzr)z*mS!9$X#Y;1St@a7yZPw!~e-BuWPmx%uHrYnN#gOL$30Re%biIJeBI%#fh z?w61dVdzl&ZkImU$jH!6;d^s+?jP+vfBDiM?zgF_X=-uyT{1GwoV?tpPo5xKSy?%r zZ1c5D`^`yPvdYQHMK#=5DEeY9&CSo>v*EDG@DLYubmY=%boZ`x*m=UlB)xV~R$lTs zD2O9iI{3rKkEoiOnyu}vPoJ{m&!q?~%%yjH2?&rqcW)H-b=%y3tEfmgJUm=dUj7+w zU~ouqYGxrC6B8394b3x7P9iI-Jx5|9zii|_TvAf?K}}{`C{|0Gd~}BV2-cSzZ()W_ z`}xjYUtu_2a2AN2E7aH;!C7t|=c9Z|9EwY0TrEoM@tZ2QBjdAmI1?L zLync5(<}V@lk)PP;kYTEb8=|mSv+QCy$y#SZ5)S)+5CO)cS6BG3oU__gM+{{J3E_(#L=B-SIqLPRb0Mo*=YFh-rtU{vFhvAo#tr5I{(Q$O4uB8Xr1k@+yR{?qYj{inu6 zjOb9FBCGBx>tKN@!MAS_-nQ=Um8IrW=o&SyR2x4JR^I(;Z&zElS1wSYI&rliAt5oG zsCc3F)A&xMvEujQVwO>UO3M2qMVbaq7rL{@W*&zd4E0Za^77~>C-dg!+a&R0V;Pi* z+HbI#JIy94$WE7HOP#ce<$hwqEDeWZx8f^hpFo=3@enN!DL^2%jt_4KYU~(R*VZIV z2}sU~pl!7$*Iz6;i>)s%-gbETQo$x4li0ujfYTdg<$Zoir<-d&X6**AxBg+>QA?$c(o@C_T@!60D>HNMEQq+u{)pRtbCSwtw$NcG zIgZ~faB#_>H?i$YxE^X{soMq7;O>4@mFd}jv`V#c$e^8~f`a9*lW&6?s7U2zBgimV z_Up6~!;!#b!Ks zyZW5=*qwsgHsk&BFw@qek8HYMx}f*DTwgH`Id5ybbb{4ub5&KBjLC;feFi&4Rzm(*Z5BS3^3Clush<`*ll1E5G?ST z$;-=I?afnNUUyjMdQUPa+qbRCK6_tl&lJ!BnCs3;Z&JsO2V=*^NTB}3x3~xC>Xk)? z?;FWsq|qL!B7J3}uzB6Yk;vmf&SSqF;exw3?RG)9o=;i&NK6zu-wYCZ@*Skz)d9T$ zPC&Q)fB(+ceP=)4UkaMtKRmP*5*F`^h>C^2KU|=NOhZGHX(mq|Lqw!f=ZI`PT<~P{ z9gMuKiORP%2TGa8FoaO#v){d`EE0GP{j{Xe-=BwMq@os zo_s%D8MO-VG;rhNQ^mNA}=BPWkTvemCb2lPr3f=xiLS>ai`q#r$gRPO!dvU+;_$1#pLOP{th`((mN{u4Q_^6v)`23DT83daIL~W&!R9UNuEx|a z#IKnW`ppYVYpR;8yPIo@npGaR^k;+49gP8!E(aU!9-GrV@QD_Js{}wWyQzTuop1JOw-gnqcqPD4iloC|^q}d%vJEj&rzE&?NQd(%RBS-ce9!H!ta; zxIKUV91x;@Zco085(et6KIYzDFToX=6n^o0+&$^6VVdp_9z3{>Z6EGi7=m?vv=!p+ z=CbfNvnP(zjcB^jqd0?{h~k;ZvHInv_6qUyh8@9%O zHh1Fvnw!;OIpC&}h)dU;;4~bmz$((J_p5H~?ErkW|I>qvj4YV^#VyTRdpxrK_=Ng; zvdyin-CsCK1NjaDlXXrE+HPNcVZS2!o#HPNLeg1Z6ca;4ubB5mBAWh_;$IRTdyQmd zA+%h(4W$Ph&KVjVs*;in8!v}<=YEygjHIaiG!FS&mTG5b$EJ6#&^&9D)g7%_YP7(O zSk9JzC()eqBwfpo4H5Rl-h|3!7GzY&Dl3adQisSEYia&Y;t>#KRRRF9&>qtE`T7dg zBf-ST=(}QmjN#u5HfpK3;02m@-BZeZe0-O@_V>6QwrRP-tnKsi@@N6|FZI->dfd2= zLT8F8R#DVZy)b8&YCgQa^1n6q5-}w|;D>hiJG>p<_Yn~}^GLnqD}|=2^5$i2Q`6&2 zw7spZtrC}!%-M)>F2A7 zn)ymhrDhwS;OWt3QXuK*>UxEcj(iv&?6O8nDuY(=Ha~_5`XsXB7gW+*9oOqi)x+;O zwXb#FsC_-HXcPbp{*+P5S}D2wXmDsqR`_uzGE$`lL-JNZUY_6C!HO=tQkvk(Cd|VR z2U$nf7*|o>C8E2ztQ`fuJvUcay->>9;OF=1$yX-E_HlB?#0YJRaS32Nsb+qZBTwWM zmYtPFU12d1{4R-`cIzNJOY1a6&&hUe^3H*K(zDYO5m8aS*`_C$Xh|-ItA5g?BVBe( z46-ecc|^JqbRWp3NgY=X1Lm9KwruywM!tRJ>Dq=$l2`W}~lf+!fBmlGQD@&q>*GrG@B zM_(5;C_(EI#in?@GtH-u&ApP9ogMW3Er#AmubBj&#V2?RB8?4hs1rW^{tIV8E%78F z0K#(~v+YVDJ+^6*j(=D)^CSG3e^5nNAFY4ZD*NiJt@C|t>1!R?j4l!00qk5f9!cL*}% z)1SoTVx5NYaJ=O!Zqemq0_saIBYB!0LKOmCbMsK?gyA4;tzE({=i>rqJ}wQ-kEwp4 zq1bt1@y@8Y6su1-afQV`kk`()H#H%dC>4MHX_uR}9Vmw={2GUiZ^d@d%1W$$sjYUs z_fiY4sNl3Wow;SPbBt+m7wPcmsMLCi-SI|U{|(VklaZE`T89?E>vV3kVjQ2OuISI> z6a4(pIW5$}5S3w;0Ng~0$4!dF9sZ3~L%p75tep4XUE!%rWD{c?b~m-O+{#xf{L1|& z^y|Za*)6bG|I2zOa%r^AkadQ*2{UgZ<%L!4z;z7H-9OifW?e zm}YNb!~?;EpA;si1>}TE#im2%RVwvZK=j1$;9yjos^X82;<-5W9F=Ph>iZR$`n)~t zjb)5W&258)>gwN*E>1S`%V)#uY@vzUw3j~}tNigL`X)N&X94U@B(6gP1L4kph#4+O zA3X}5{fgVX#gM?*A+N4Z%I9LskU>sZ4~SIvc=I6)xJrv({pki2yiQN~X9{tRY>0dE z?6=g|r}VD-Gda+)9;Nm-P|C{7|B9drdf!kNG{`w3{Nps=MS>#u31|N^J-4nIU z#m1J`AINIwyP=>LI&S0!r{1eX(?_#8?!NY|nA_BNH;1fBPfvfkpuk%P@)n-Nwt z#d1y9X-jq6D7+4S5)ls%XZGjm2c>O`GyvRI&bp9xivRXacdBLx(;zn?KE8RZTBBh^ zSy8dA%UH#;JD1Y_WtOH7V~Y0p+8?a%yp+x=#C9g9pdFlFgTS8)sUH(3SD<3 zBO<6Ib^okKOI+|pvJnRFa)h+lG>n{1-l+osMsr^u#fP0~ zL1{%o>`?68{Q~CG^s&*=+jp050?GM&C2hm4#d{=rd#9$D`s%9xl8*}u3C*_!HF;Dr zy%N(%^Ld592j@o#i4*s``W;Iy7!xCTRe#QM}#;R9 z*yEoo`Qn{OI!M~6xMCFZ6`E|cAOq{!D~q^RfT z&HQ{#SeQ;$xPcwc=5#$x-v*SV5XZb{aMRC>-;UprNFRIP7LZc{l(;aS6AD zag!_}A~H2S{jQrEpGqvf@88U%D0@D7LpX{ZuxFuq_X@ITr+M5o%SoQCD66TFn4e9x z2d%KA=4Azbdtu!9e7C?RI}|~L4lXGvDaEnrDixNvi!45b7CU{m|IasktT1 zXOfMhT0GU3lMfe&{r(wy-%Tpl`RtH7njf^GF7(CmSn_jXyj~jCF0C84!b{IZn zfbMb*k)kQFNT1`H5fBmi)t+oG{PM>U6BqY{iobV=rDI}(LPH}OU&a@mtJp(iI8#(; zSX*BHe6rpJr!QGR6iAzb!a~{nPgnycR`KkaY#%OJ;@E5|e$)hmz|hjsfiZ^{4n(v_ zy|bOLw=XngV84B5749CU3=S$FRyvkjDBlY+L&kraGy`K`nKb!N0m0avA8j}uonr-u z+#5(0L}@m8j0D&PFM-nx&-9=%Ge{rvD@MI*fQ!Qpl-aFu?(rOz?_R4G$ACH6Tu)gq z_d2Wxwj|Tj(!87wmOB;1jR(>)Z)3S7?6?6O#3Q5uB6meWL4nn5{bN^UT}#oa8avA+iR+aO%CkD)?_nU^ zzh^Fn$u8NW)&T79aAS&qhv%0sVgWc(lT%assL7dT9RJKb+EyHTx1ko}UYRhbrQalc z5|oj_xlylCvF&YHADRO8NL;*&%VwFeH@Ozc!{dh6qnHw!%ISAWgJ9B=GM!=h9-==7 zsPtjvR}7*ldCDhEF@H!=Pe$s5O`82*N8C~k#7`JeREx=A9E*+LqC{)C&vci(XjN<>8 zmscbA!X1`P_cQ%lx4gZq>-eo(!yfQCXUx{<9z=vaVv~9<2Ks;}aQ1U8zCo|Fpic|m zT)VMt$Ci3ESF`I&q-)l4aycLF@b*l5oPKU1^~@KGJrc{`_}GZas1g6rDeDC zU{z=kjXI>DwKzH}`Nsmy_Z*kvFq;-u`CxJ;{;L7dHCW9rTX`D|5D;0fU7_4$Ld5ba zl#EB`m!C8yPL!0S3MA&Qm}IM%?@p1&hyKY7i%{*e+f_DV^qiYf`wzaNHu`l?qmMfLts_zQLvF%OkX-^ z8-DuaSMOqviH+o7ZS4ij^`OhQPtplrls*8N_#&$>&KJnKWmTn8U9?M!wWrVx#Wfpc zQnW@Z%rIbxqMkep0Ai4cMeFf-64GZrm*Wp3+X73|82{lv+nNquzI&_5*Y}GTfD|Z) z>9D-7{;SQF8Y6)p#pSfGfPA!nfeVyiLZzv&j?P!d1=lYjLBG<3B?0&e2H-SG#4z4L zL1~jn&^ItN{1Wq&Y7ulrHtWR)r>DzUX&D(AI%_xX{(u_KPj>R(Yu9wK?m%slB3Kkk zW*+N9me%%m-J^9v%%gOV#s;r=7hrpU7>XM_;}mc^_X&U2p))ZoEho8lt0~p<>Y*d^ z$I7cKG|6h^d^I%KU|%)3Y;Q+U$gL!Ej6;hK@Wz5WZ@JnZUU0*rSyOJPl*PX6&})0V z`Rekr$A=fE4ulTY;}xqa$GKn~4|>J>EE{9xs*BmlQx#;=_~zZot?!fqyZj~C^kH(v%?vS+X$eKkto z8yf+G?EL&7==gIfCOTh7U*nWUT>PN@Phppq<|yWq1HZnc#=_2y*?84(k4hp6w*dEv zkq&I0YN4LAbaFpT%NRB{kdSO6zvuOXvdCsKtP#)yW4C$siGc3R^i5=ZjmbVs8_P)t6+CF2oBpByC9y?XVqBQ&L{ zbb+S7%IdA_b-|<{cf3!xxBwi-=d1T-X9{lKKjOAUU0+{^cZ$wmca!tme7Qfv>RB_= zqpw!))+Q`cTTeidCINZdC$xg|8>iV*J5s31)sbSHm_bG~x53~bhn?xU zvjbv@iEMxWTEpBL`RqQid)x*m9X|*$^*_PcS)yBto`Cyb;@U>v%xTb<)B!5V{uB<9 zVSj4!?7lY!jwJ9o642?o6-`hlz-B;~m?XWAm+*27_@53}8_@7+)kXGK*iR}l%y=B^ z%T(gcRL|-1eSA=Vf8$d&ju8fKutGJXMJlnJ!w>N6kpdE*Ktmg*fN{Hiq1p=} z6kV5-?brvlz2+0gWVdfSQjfNeRX<{6Vx(jyV5nrG>)fRK{`~6SXQY7mqli4b+gOKU zl9B{ut51TwcXoT#E5Dww0H;suTHE-@34=>d?;n-d=Izq@~# zn$fDZmNxExRjcoD8)vt?tSmAvQ!Q6cICo}BDm+~F0h@KXPXDV)n-%8AtR*^m@9${F zN<^{7)=oNrDc|He(mN%WuhPl>b^J}Y!wf8ggbWI}8V+55 zXM|EXTme7SQc7t@YSz2DY5}GHg*mBhC1<+Y<|VaMqTFKq7D6}gZVxuVCwzqOHA5IT z;%9AbWz`PUfX-?O9=;_1QU?rXA&PGLgoJzEMA#Jk^yAcUGicr=i}aCC?GNEN-0_T= zygt8(lZp=k9p;TU`q9A<4g&*2vnd_Ysc8F|dE2u5;NYN7tbv_IovrHfb-3=wJFCu) zRSV#DL1O2Rjp%Nmrly{UKJ!W|GA46#aWyWzF7bUk&DU^QN+QSbAvof@bKaIpe?Wyt zjU|21`SRs=0O7_Xc>#Gtc`XCYB2{|^ zv<>Bx!=`Ds8+|Fog7<>+ld&cvg&jMMOn(lX4hy!!(>H4!$F%2~G|YXSc*|lveoJd= zlGWOqw~hZKWzYKVc=B%hiYRl~ObtOel>FHvpkZ^rq>L}J*>}E~L+MTCVHsV{?5svY zx`%~D*`QUc#57=cLV=6(4WZEcW0N&6b`hXX4CQIw1-C}mLCsD`0xUhgH8qPVBGul% zVc6kgIBRfu;N{Zg=0q`!s=``Opn0OL>QU-R4nj~DTbrQ!{liiQS`iVE9Q8_U4F*LY zZ*M-pC8@<0NSaf@HvWtUWGmCjfq~u1C+}yvx@60{yD}w}g)mT&o95n%?z^QFf{L#e zp*DOm0UQN9`PO;I`V-QA>WcUg_05J>83xx}v?;mn_~$!SV8CM=zOrsntKC zQ`ixK%eq@?@E0(nyrq+n5NdBgqWtGzo4)9LWwdN;Ys(VRTsb9>s67rwcgAXE4tcKD zml&Fn@A{$px|m;(TwR?A$i-%6WX%5Z4@MjZr;IrR@dujkNhdPXl~l zv|SE2cv!V<#=qJEW=BwjShUzeyMxVW{=F}(XVMlK*dw*@RlCp!Li;az) zX7-UuprG@zYOG8%bF5m=q@gk;!P^_9smbR1o!gFnqoXR#e*?(LVQtVOMq0CB6L8#R zm9bnMNbe966Z;z1wmr8U^FpAewsuz|!C`w+e*96SuFXA^*S1o@v(~C*A(A|=J`Co} z8BSJ(PdN$Q^@K~>E+$NCZyn9Q0rUI>#&scvrpu8d6I zBb&s)-W^xXiXV-_R=hjY_5KKPA zyP9Hz1u1y+^MSPhVZ;-xUt(lPUn$@;vp#L|nQX@(y zt?bpZH0v~~fkuLtm)88+KO|n4YYkmAU$4~cYzC6obuK4b$#mQqd*EJs;E4X&dZ~Mf zXP2=@0hR6PNiKJdwuNG1%U9ruN{!#yd@mOwt{x4v$moLJn>sK+N-xJd%ZFp*$#FMMEUsq9^NN9V^Pq@4G#YbJ;c&=5=A_^L`%rG*gxXiatP-P*ac^2b&w(?&j7gIzH{eJ6^Nh7O*Sto^IqNh5V0)y z2Uc`S`C$)PvOXX;7`00=Z9eryeITv*kF;mg`&vX!4*zmzvTcmRU~gomqapC|5Nu4P zj(azlwiJD6Ke{-mr~^{&0;NFNU=D+?YC2!zAntCszQlv?SDGh#(Ik%O>_3t@Q2HX+ zU(3tMSwa&s1cA%rrr}l0+S*#0TxMXE!;X7W7RR*zQHP+#BIp?5 zpcvY5Dnz@|4YA>s$A$~rt+yh0CVJ)b)z?GNLzgCLRm8N5I zC4*^<V%Uw~q_A7CFsHcDZBjJqE)k2LwcfG-21Uof z00Rpvgck>gL;Nmw(4Paw`}d=MdhCDI@BdPu%JMguoDW~%=2G4hjn2~xC+`090Vji- zKh{dJvf&R>8|->;kSq^|3we7Ej}Cu$4S+n1+DGTGJxxGn_6w0ruPO$tj0vnjuI=w{M zvxDi>+o3zw{$pD{!tf#e3#8Y^#(Vj?`!B+m9t0`|$}YZah-(pe*;OF`%1JJvBRko} zisN@hj>qt*=;_-o0!RqsfQ|zr+Y>HI)RP^7PTCQ5++rQBRT=b@#_*tlUQBR3aiHg(nORJGCaC2pohG`4(rU%;2j^RJb@+H4pua zXtjA3A^=1`wfE^W)?`T^WXcjtvu z-%Jb)4YkD$JS)|?E|yqVZC;(seFmN=#ncNi3H-+PEDzat0XJ#(P_azK4f^*E zJ9!!p{&-+G3yF$KCOYtzCDy|t39c^osfjOVB%zuM?O1bLQw?GluiJVmZ9NiVV*F&& zJz55HIdJjQM$0LvsoUfDOwdKb$a6EM-ReJ9i=y4Hm7yAQ*CTG{47{t_C;j6drYmVo zBmM!DqmVX1%=mS?bmDVDm_c;w=B||hk?GH0&$Nv_qXn~^|K}xrk~wLe)e%O~bNRT4>j7(O%Ic$J*gRS)ws^n*L=BF#B|5e> z@KNL*WD$#=Y)=t0-lW98prgBBfANB_(!$`Qw1}|qlgG0UyJH=?2HMMNFgQ&|DSut| zw#28ltvM$1IgrEV<)5Jb;<@R23N5W<5ay}052PJd`^kw}H04%FH2V?e5jbbRljZBY z7ZHYK%w7tU#~oMT-b^-ZukY8X2M%De!41Q1V>qU2a=hj~uuje3!JM~!R`~;qv(5_I z_D%x)j2zHZrir_`Q_d@O`cpmA%wSRQA>*-cMsVCNC;WXsi@?m5`G-X#Q3%m6eQk=# z$jQTtkD|mk$!l5j1ZZ_{JiWOrcX5q}@&bUSL>Q^MW0)d~A76Ch3Cz#?N><&>ddgiq zz+wIhv22A&tJ5+mr3Ro)~GN-H=ph% zy=?;?EP#7F(hRqls+m0O1FJ?S%8kYjW7^b7AhaxKK0&drq;bC1R+Bp&f%`CtNTi^XMFs_Jf&R5 z(Pi+$JUCtuYmUe`KaapzcHMfa|3GC;hh^dfVK@c#_iWvt5rx6!Xq!fDNgQwP>5Mu! z$fD@v)#i+e(bm8lS!KV)U8Gryjr1N81W8Fuj~+dOYs2DESC3*+M+flbS5$O(z=s4z z9}GPY^5Vg3U(qiV1{fF^@s%s9wA05f#pA+(cswtCeSFen7e4t8Cfpz>zbn5G-v;gf zpmV>>iU7>7x$&oUjcp&?>n{;*V?$E#I_i(dsJ~eAbVLCZ`+u#*XUWW#6Ry#IXVH7HT;5j_(lyZ0Zh z0wEg!`s3Z>IrTYs>fgRMfFpFkiM1!1DgU=QAt`KD9!kim-_#Z3D-?n44UqrbIe)bA zGHt~|eCCe6^ZE1F<Bl zW~r9KV#e>u>XKwLXT9^u=fFUTh7;tEA8(J1kJFc9bqjniSd#k7PH-jU_3N)!$D8<2 z)s1rxK5)Zjc9vZnil5d&$D;ptCLGLY)3=l@RK^~lFL1m$Mn;WKZ!p35Zg4&0U#5ET@+E=UGyAXts6|je=qHI)i_qTuGrjKvOIO-MkmQDZ zOG_<>(2-V0n|WrgAWz_zlCsMu0wz6F$>p9m+S(M*CSs~KyQA~dHeiU{U0$9aNuOtC zW5c^k%q*ypczCufEhQyXmUggE(|I-up_35O7x$+^eTDA~<}>iFo($!W%m@?a6wmP= znAjbSzZl?jb3T!C6jlqOz&mDqDl{@QoUOcJQabe<$_$D{iv_aN zN8A0HPJ6=M%D(;b%*JZ3>nLgfMX+KMn?56oi=k#^-OZ<0j=L7V zyvlzn=YEvUmJTg0!&18yy%6cOFDBvr>9D8Cd@iA36qzz~@2h{9B#%ItYX4rum*C)t zI8Ay^T2aPb%}1fxV76-yeu$=?a2Xvzn2@3-)BR||cWtuzZrpRHywx*<(UK<*_>6^~ z97$M>6rFuRAFQ?82n;0jZS=2qKKdL~R;H8i`LluJ&78MkimRK2nC2$~0~$6<*8`<# z_FoZs_h`@EE@CpWgD^(hdda}f9Lu#UqGiRdakrJ<8?9M%w(K?%!cD*>ShZ;FL`;lG za(s6O_DbBcIy#yVp+u`C3FAYEN-CZmAbZkV5C*9#oKAPcR~<%LTOTkeT|W6Y3BIT2 zIV{}K;_lJRnj}EB{vNTo3sTIN5P7CAuSYX`=8>=O5V+&Hg=I@8J>`D?Hl`VM#%pwx zYk-rGA&SK(+0_L+43uh5GQhDcmz|H72D2R|vJSZ3PFHrGS^s%v0fvdbTjY1HM+7~a z!9f4tpi{tN2H2CNrT_5WtgWx7DbO)IHyK9-|4YjtU8GQDbYh}ckM~z!V9KQc(fclp5EWI`?`{_r zT+WM+*Mok`(g!iAR|cUk_}_czGn5|_#J~D?a8L+nqTPQo=~nx#Gp8HHZxN7Fw2inF zerA04@0Deix9?wp$jN#j_YDlro%Qcz@iEgrkvup2-0I|g1QMW4`}2FGoEiP+?Cz7G z9}e6D%PtsCdTq8@?(sLYsCHC?-t`E$BPXp!I|hr#Qk_6_Y(W^%S*Iebe z&KXsGkm2YIPxmqdXJZLCH3L$ffN*%YGeRs+EpvZr1U6j#^+=T%4|7=L@HyUCbf3BP z^ffeoO5yLazP=LH-5moewkP%~)ZT1@Hj{;d-~GNBheI#pV7@X2gdV`6+-*e$nTPih z0@f3?1}TOK;JMNPUoO6+(MZ9ZFBm2T<>gDS$#@-A4jq4*OrY~rSO~%`MV$aJ^%LrMYrWv33y9D1iEIS3G=fhy-o+GdNgbi;<^1iPvT* zt&SQR8Y(qjVn+z*tB-ME*GAj|;1F7e7>L2D#*HwP$4?&a>xjM7poTG=#I0IJCH@_VmJ^!I=; zPXxRzO>(zyz!Yi&evm>pKb6zTHktj-wD`yZNC=|{*tkndOJl|nO3%hf zadWz8*dE9jX?nUkn%$V-dilzbATFh#5M`)cP*70fI<^D+vvnc0Je{!c>FH@Vp-y;| z+a08r=F@MAwfGiCi!Ce2-q(d@Lmh(09Fl9H5PuW|O3=&OdvAZpL`X#FDKj&+cvn~d zz%XQKUx4>DpjUZ;VMjhV(PEoe?Z zPk~1i-l(9C&SQi}h?<%R!hAWhW$2)b2%YTB3qQU--=K&;Us!l{^ExaiCx^FqCD}kz z3Q~IDu&`5V?h1po43ay7B|>E|k~U{fXv}AhMRjy==Lzs z+FBK718tz!>iro%LS!^_YS)MON-TEFef$~_y8&3LF+iz(3-c5Lz9pp=4OM1 z33yEs5)udoeDD0oL`Ycl88b6Eh}6I-`PL_qfg(#k-Z(ppn~{}8UtbKxuF~L2czwFy z;k5+>e2c)%qvj+Y2fW?QR}dQ0(bGgc`zShsi~jGwfSHw5stl@!t8eP*ft(DSea1Kz zxA&?+F=j#-$nM4O61jW@y~$gC%2DN-!CPYexcmCv>I^X;lPe3$tUydiis?YPf|tXF zo&hT4DHU*!IbQ(V={2{sK}^$^7g#$)fH#@_cZdeUplVY|_bd2e*&GPfZ<;rONYBX3 zq&s|VJhmUbo3qV)ioz8cnf@&C5!pdK3q1)$ll0Gj8_<|ojza3n{Zz0aBJRg;9?i)| z0#^8M(Q)pZDFYbIU6O_@auj}f9jptmsiy7PV@A}eevOd0ytQw@+oN_|$wk(;30Hs^ z_wOI>*sSyg#dtBBl5(1Qc{Vf{W}Ti|2|Rm2MW|k3&XAG3B%`A80ETE7n0f*#m^5pi zOT@$N}tPFJH z!&6gO+uP>&v2+WQ;lPI?qUk-G6%RQ+E%Z(Cq=M~Yv=`V`<6L9?(VrVKgN5?ySwDV) z?NP@}QzG{QhD6(FO2>ze<3e_{m!Kdb0;rkCf)4=^K_cSaHgfP7nSlG70KV>@TLd%% zIaY56nt{VKRz;~_#C!1I3zejYd`6IV61epc@nSXtYN=+B&c5_I6c-g?Lma`EiuDps$-0zh!a4byjM?TWQ#=(g_1AsG9+(UGm;a!J({QSZ~ zsqxUav~52jgdc#%VVlV8rPDs@Va&6h`kc+FO6_I2(S1-!e}Oo(3oW*KIMSfR|Gl`G zQ@5FijD7_NlX?SwSJxTE=pN`W1#0z5bZ%omFJv-JMaQ$5GFyp)Tz7J#((t$>JH zC>s!*phnV{lL;dZu%0`K`X)LGVHW6IA2vAm==|=pIqjF<%FQBx-2RidqOL$Qys)_{jo(|UF$S9sAy>P<}>I{FLv}GjLM7}9!04ocLKS<)9)rS8a1}6 zMehA0bD-h_DIQ&2e?vAVdEoEkGY@~lUfH;HWA|q#kq!(Luob5i`*%R{7aT~8S`E&b zQ4Ns)I?SgPb=7R&Z)DLz#q7Oud<(xt9AcyEPpOV%GWz20-nB>3{Uan-QTz%K5dUBP zzAK{WpV$6nr@5RI`Sw3+V+0Sw?}WzpMwuH3tBj|&-~8_}8|z{Qnd8#;RGx6uyURo1 znN~}>dfY{PoPcWC%A&~>z^i)?*)@!8rjmIakZ&$+H0UPeDrVfZ{rK2_pD>C;<-w+# z)jyjm1#JK7RJH&%8F$)l#s0?rQvT{dS)YSq48OdT)IE2Si@Q&s-Y2DGpoWaz6~;ZK zbu0JX-6u7+YuB3CzppOOIF|ots|-$^W}Tf)iBxXa;~qv(b^XWO0L2i1*|jlZQDVEc z1D?wl=96Ey1Q$o1!p99jLeRVTkq5$es#L7%e}{nj*;UZ~4=guVi&V1-Y<3Jcu=Dv8 zO*zl64m2VowYRr>ZT))IqCbwX(z7{shJlHa6P|W=c_>uVeT@cJG-w*Fpe5g*CZ}7j zcog5Y|0(43tZVw(Bw^-NEwuIztzQPd1V>$cI7PgKW&eAPdS~fex3d`w51qY;8^Vp? z`xFn$!rHoxW+K>j87;$Seo1p^giTtpFm4k9j=d^)SoMpgr)*xLDXgE9lOES%JRYjgFKbbN}u*^bWiiC zh%Gf>H*9?Td+a^-3SU{y;==1ukDR^a;(CC{PDWc>@DN3wUPB zh8bXdK)*(exF@HmY zM9=z@wzIRdbhit8_Mg!_yRH}TDJ@2dk0+ST!S@(kVOG!fvGXzHEd|%>LSW6{a$LO) zpLn3GGvggX`cm)a+S_oX2nzw%<~Q#_{2bW6H*epjzdokJp_m~>cuJC1V+5;3cJF{xL4*Sx66Q7WWQ?1}aTJHg@MVQP_9 zxj>RK*m=QIsXMreXE>V54?Yaa3Ks}PsaKf(^7Quqm^`7OmHjTMJy$+k7EGYF>!Zlz z9@Hf1)x*E5uyMgG;RR;p+cnPbN>JlOri}n!GW`IiZ%O37v zjx?XF3V;ozx$!f72QMswKaEF3&-R(YGe(`KP!t>(O%JYmk!3U2(_Oq+X6<&cW_gG0 zgxp%oj`O&F;8JSPXCb+3$+q=iT~orai2bx(imaSglVr$RYS%WwVt z#Lz+V%K2~sAD;j)Z||?)9zm=1IvqD@mS%-WS8KR9J%>ujf91+k9|jCf(4)Nx4whAz zaf6JA*JT|8*n|vWBrk5Nn{!5k+6?L$FRK)p?Cfly9x?%o&y890e9U`SXs>$$&l|$% z+K#c3+vyjN&B0Ryd_eF4dAUkLCI|#cIQO@HKDBe5leTcTS(_^_3=It-!hWcx?y|Ey zRI%rV?8eWaGkS7;iuNe_SOWehNZhjNlrKUZ@>oo=9%}dXwMn5jEOJC(=WzDQoW))C zdtR|_r?$4XKSW0Q0ZjkxQR4)-GeGaNg^wm69XLgZY?Zyg|L%PjPgsf&1>XO=$w6|p zggjpNz<@W%N5z#@Ar6q2a+3@sY~|mF3WJ)7pE5SqbbVna&GGa!o)A#<5iK{h4)s+M z5Rf35m` z(#VO!F~5jdM>z!p3%fg4*fRfBH)Ig1Mb>=q1iywD|NZ62pH4Dx1`yHL@oL9zD!pf%9)4AW~&0kbHDgP*?XdF@u^P6nB@c6#tyV zpRYcAkVedZHec*FCg-xa3?g>doE+)O!V8x#dwO^WfRT?7eDIkCA(C_aJt`%ko88Sp zefRG9OShi7L4TM|A=%Z?L>{?zL=YmEBGWZ6-~m)IpfbZg=KJxZ62QAR0KcQ;bM}Ls z6XB%^qYeK3T%D`{!-_L~3Q5a~fUWBxA?5$&Tr2UA7hBV(^b)cS{_EJc=PXeSiesL_xXR9 zH`xFEgciWV350vfpj3aeX>fU{Ay!EAF61S=c(zz5By5AmBg1c{o8v`q)8th)LoDzU zfhsC5Zsn<4>yg8$FOin@H&*<0HHD~B%z&qlkj;pOL}uBp9d7IW{F=fEWa^(34A+j% zof-b)7KV6QA%0*@2RUXnysww$)u++786s94W}W(rGK(EMzFsJG`LOLcP*cXoZM_BE zff?u-p`hssFm_$MeS124g1GYYA6?gzrR(r$Bvc-PMh|t@^!VYc;lg;WW6nSKkuL1x zI@pS7hQ7g@odnylNC6u#NQ%3U*p%hDiYjAXuT+FGGItD^edWHTJb-exE&ZrG4Xs$H zCK-iCLsx5&9x5p1*ETjhp`8uUY9k-C)zvka4v2RJlyjZ9+%BuUd}#)wJ#g*J3oCz? z2@$D2tYAgj`&*2u3c*LH#+DXb+JKYOC{CzkK5|(Ge6xlI#p!+lU?}iH0iU3836c<` zm4>PwDp<_19>spQY%6)~MvE9kSum2qM2%D~2Xa}?T?by0p4$y?VbK*I`(8!~tghZz2`}vC(!D}f~tF?c54`7af9f%((JGbZF&|m#5ZeeX1yuH=OL_eg{7sW#0q)uyyz|`QBzZcW(~4YVPRu~ zV~H%HS?SF!_~U-Z24$AJSa4DtDAni3vj{=RfC%H?Ha$k9UeG{j^u6N7)xX?TL*`4| z(R$!QMz+Xnc>_vL1Xqn)eQpBz?^SNyyQFM}XTTQ-ERO0S{mC&{<&Wev7k}-<%E}6z z)hi%86t2f5(t0k90VFASj@1xY&Gy!$!4iw9(>^-7FUPX6pFB(K)10!5NDv5-fkYJm zHgmN#HI(-D2Y|$UL*o#dtxa49zYW11j%(r4)ZRM1$rogHGXQ=7$G>jK$vJs=>Y!UH zCWgeQ2lK;s_YTIY;Tf-O%+N>dM(htZ<-eFLCvPM}-Fo`T2TtL!+BnqNm^fQP0Jl6# zmAeBSwV#7|bHHR52p~sGygos){S2ckwzjs2w3v>r8d3r?kZ~5i8uWsGP{awb*e4EC zY%oXxMH??j1)<~Pv-Z}^(e~sNhv``DFCd`JcTbToLih5EN7&EMuZ(js@+EHXO0=Sv%UlAllS|NPoCMsK z({hL0dg;d-SPiGgnrjEZe+R_Nr#og^z+&gh^XIE(D58YKl1#+I>u~%?ZP{kq=Yx8Gu4`Howh#afcWQi zG&`--F($33D60BPjga~NMjx+nrW!vOfJwaiQA{Ecb|f`c^muZ$5;O?-6f9S*7J9v) z^$VNjB;39ql{&Pb2>Fmgh?u=_ei1+(4gkwgQeFpvE;tl6fQZ3D?ZlHs4rrC#Wr~Eo znd*3m)l+wO4HV*&t1dWY)~g7?e{lRN=nh(+cYQg9gymZe4M3Cdj)@Tfbl(daMLwCU z)6;LUw-;ldMh)FoEyREFa?@e zO~Gbp8>=fO0W_eWpx!Jt-y{a-hj&1{-?yb@nLehR(Fa5U7@l$9n+74-8&Y4WYGIdk z17Nqqa5TW8>=85pxcZw{cX@W5-k0xQEfM%!GM?79y9Oon#oa%lZHoB`+$90uiY}vw6-|UAE^cke(sZi4IVeYJqx37<0SAVY?bw?6H~0 z;|WdQYcZCG3I*Z40p-ly1m2rhWGl<@z&wDEl{GJY0CMqXd5I)ouj9sDKsYg4w(C9& zuoN?(Az-5fT6?Ξ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 A8~^|S literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b50025e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "pitbridge" +version = "0.1.0" +description = "Availability-aware financial feature snapshots with revision lineage" +requires-python = ">=3.11" +license = {text = "MIT"} +dependencies = [] + +[project.optional-dependencies] +figures = ["matplotlib>=3.8,<4"] + +[project.scripts] +pitbridge = "pitbridge.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/scripts/render_figures.py b/scripts/render_figures.py new file mode 100644 index 0000000..4c31e81 --- /dev/null +++ b/scripts/render_figures.py @@ -0,0 +1,58 @@ +"""Render a source-linked figure from the saved evidence (optional Matplotlib).""" + +import csv +from datetime import datetime +import json +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.dates as mdates +import matplotlib.pyplot as plt + + +ROOT = Path(__file__).resolve().parents[1] +plt.rcParams.update({"font.family":"DejaVu Sans", "font.size":11, "text.color":"#e6edf7", "axes.labelcolor":"#a9bad0", "xtick.color":"#a9bad0", "ytick.color":"#a9bad0", "axes.edgecolor":"#314460", "axes.facecolor":"#101d30", "figure.facecolor":"#0c1422"}) + + +def main(): + inputs=json.loads((ROOT/"demo/inputs.json").read_text()) + summary=json.loads((ROOT/"demo/summary.json").read_text()) + comparison=list(csv.DictReader((ROOT/"demo/comparison.csv").open())) + row=next(r for r in comparison if r["decision_id"]=="SME-A-2" and r["source"]=="tax") + records=[r for r in inputs["observations"] if r["record_id"] in ("tax-a-v1","tax-a-v2")] + fig,(timeline,bars)=plt.subplots(2,1,figsize=(13,7.5),gridspec_kw={"height_ratios":[1.1,1]},layout="constrained") + fig.suptitle("PITBridge | What was known at decision time?",fontsize=23,fontweight="bold",x=.055,ha="left") + cutoff=datetime.fromisoformat("2026-02-15T00:00:00+00:00") + for y,record in zip((1,0),records): + event=datetime.fromisoformat(record["event_at"]) + available=datetime.fromisoformat(max(record["published_at"],record["ingested_at"])) + color="#64e5c4" if available<=cutoff else "#efad70" + timeline.plot([event,available],[y,y],color=color,linewidth=3,marker="o",markersize=8) + timeline.annotate(f"Available {available:%b %d}",(available,y),xytext=(0,13),textcoords="offset points",color=color,ha="center") + timeline.axvline(cutoff,color="#97c6ff",linestyle="--",linewidth=1.7) + timeline.annotate("Decision: Feb 15",(cutoff,.5),xytext=(8,0),textcoords="offset points",color="#97c6ff") + timeline.set_yticks([0,1],["Revision 2: 180,000 CNY","Revision 1: 100,000 CNY"]) + timeline.set_ylim(-.5,1.55) + timeline.xaxis.set_major_formatter(mdates.DateFormatter("%b %d")) + timeline.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1)) + timeline.set_title("The event date is the same. The knowledge date is different.",loc="left",fontsize=13,pad=12) + values=[float(row["safe_value"]),float(row["unsafe_value"])] + bars.barh([1,0],values,color=["#64e5c4","#efad70"],height=.5) + bars.set_yticks([1,0],["Safe snapshot","Unsafe event-only join"]) + bars.set_xlim(0,max(values)*1.25) + bars.set_xlabel("Invoice revenue / CNY") + bars.set_title("SME-A / Feb 15: a later correction cannot enter this decision",loc="left",fontsize=13,pad=12) + for y,value in zip((1,0),values): + bars.text(value+5000,y,f"{value:,.0f}",va="center",fontweight="bold") + for ax in (timeline,bars): + ax.spines[["top","right"]].set_visible(False) + ax.grid(axis="x",color="#536981",alpha=.18) + ax.set_axisbelow(True) + fig.supxlabel(f"Synthetic fixture · {summary['snapshot_rows']} lookups · {summary['future_knowledge_rows']} future-data baseline selections · Source: demo/inputs.json + comparison.csv",fontsize=10,color="#8ca6bf") + fig.savefig(ROOT/"docs/evidence.png",dpi=150) + plt.close(fig) + + +if __name__=="__main__": + main() diff --git a/src/pitbridge/__init__.py b/src/pitbridge/__init__.py new file mode 100644 index 0000000..8e6fdcf --- /dev/null +++ b/src/pitbridge/__init__.py @@ -0,0 +1,6 @@ +"""PITBridge: versioned features as they were known at decision time.""" + +from .core import Decision, FeatureSpec, Observation, Snapshot, build_snapshot, reference_snapshot + +__version__ = "0.1.0" +__all__ = ["Decision", "FeatureSpec", "Observation", "Snapshot", "build_snapshot", "reference_snapshot"] diff --git a/src/pitbridge/__main__.py b/src/pitbridge/__main__.py new file mode 100644 index 0000000..eb53e2f --- /dev/null +++ b/src/pitbridge/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +raise SystemExit(main()) diff --git a/src/pitbridge/bundle.py b/src/pitbridge/bundle.py new file mode 100644 index 0000000..5995c1c --- /dev/null +++ b/src/pitbridge/bundle.py @@ -0,0 +1,138 @@ +"""Replayable JSON/CSV evidence and a standalone browser report.""" + +from collections import Counter +import csv +from dataclasses import asdict +from hashlib import sha256 +from html import escape +import json +from pathlib import Path + +from .core import Decision, FeatureSpec, Observation, build_snapshot, reference_snapshot, validate + + +FILES = ("inputs.json", "snapshots.csv", "comparison.csv", "summary.json", "report.html") + + +def canonical(value) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False) + "\n" + + +def read_json(path): + def bad(value): + raise ValueError(f"non-finite JSON constant: {value}") + def pairs(items): + result = {} + for key, value in items: + if key in result: + raise ValueError(f"duplicate JSON key: {key}") + result[key] = value + return result + return json.loads(Path(path).read_text(encoding="utf-8"), parse_constant=bad, object_pairs_hook=pairs) + + +def decode_inputs(data): + if not isinstance(data, dict) or not {"observations", "decisions", "specs"} <= set(data) or set(data) - {"observations", "decisions", "specs", "data_kind"}: + raise ValueError("input object requires observations, decisions, specs; only data_kind is optional") + if data.get("data_kind", "user_supplied") not in ("synthetic", "user_supplied"): + raise ValueError("data_kind must be synthetic or user_supplied") + if any(not isinstance(data[key], list) for key in ("observations", "decisions", "specs")): + raise ValueError("all input sections must be arrays") + try: + return validate([Observation(**row) for row in data["observations"]], [Decision(**row) for row in data["decisions"]], [FeatureSpec(**row) for row in data["specs"]]) + except TypeError as exc: + raise ValueError(f"input schema mismatch: {exc}") from exc + + +def csv_text(rows, fields): + import io + stream = io.StringIO(newline="") + writer = csv.DictWriter(stream, fieldnames=fields, lineterminator="\n") + writer.writeheader() + writer.writerows(rows) + return stream.getvalue() + + +def compare_unsafe(obs, rows): + """A deliberately unsafe event-only baseline, clearly isolated from features.""" + comparison = [] + for row in rows: + candidates = [o for o in obs if (o.entity_id, o.source, o.feature) == (row.entity_id, row.source, row.feature) and o.event_at <= row.decision_at] + baseline = max(candidates, key=lambda o: (o.event_at, o.revision)) if candidates else None + comparison.append({ + "decision_id": row.decision_id, "source": row.source, "feature": row.feature, + "safe_record_id": row.record_id, "safe_value": row.value, "safe_status": row.status, + "unsafe_record_id": baseline.record_id if baseline else None, + "unsafe_value": baseline.value if baseline else None, + "unsafe_uses_future_knowledge": bool(baseline and baseline.available_at > row.decision_at), + "different_selection": row.record_id != (baseline.record_id if baseline else None), + }) + return comparison + + +def render_report(summary, comparison, snapshots): + kind = "SYNTHETIC FINANCIAL DATA" if summary["data_kind"] == "synthetic" else "USER-SUPPLIED DATA" + provenance = "Synthetic demonstration · No real borrower data" if summary["data_kind"] == "synthetic" else "User-supplied inputs · Source authenticity has not been independently audited" + counts = " · ".join(f"{escape(k)}: {v}" for k, v in summary["status_counts"].items()) + comparison_rows = "".join( + '' + + "".join(f"{escape(str(r[k] if r[k] is not None else '—'))}" for k in ("decision_id", "source", "feature", "safe_value", "safe_status", "unsafe_value", "unsafe_uses_future_knowledge")) + "" + for r in comparison + ) + lineage_rows = "".join("" + "".join(f"{escape(str(getattr(r,k) if getattr(r,k) is not None else '—'))}" for k in ("decision_id", "source", "feature", "record_id", "revision", "event_at", "available_at")) + "" for r in snapshots if r.status == "selected") + return f""" + +PITBridge · Decision-time evidence + +\n""" + + +def artifacts(data): + obs, dec, specs = decode_inputs(data) + normalized = {"data_kind": data.get("data_kind", "user_supplied"), "observations": [asdict(r) for r in obs], "decisions": [asdict(r) for r in dec], "specs": [asdict(r) for r in specs]} + rows = build_snapshot(obs, dec, specs) + if rows != reference_snapshot(obs, dec, specs): + raise ValueError("SQL/reference replay mismatch") + comparison = compare_unsafe(obs, rows) + summary = {"schema_version": 1, "data_kind": normalized["data_kind"], "observations": len(obs), "decisions": len(dec), "snapshot_rows": len(rows), "status_counts": dict(sorted(Counter(r.status for r in rows).items())), "future_knowledge_rows": sum(r["unsafe_uses_future_knowledge"] for r in comparison), "different_selections": sum(r["different_selection"] for r in comparison), "independent_replay": True} + # All outputs are derived from normalized inputs; there is no wall-clock timestamp. + return {"inputs.json": canonical(normalized), "snapshots.csv": csv_text([asdict(r) for r in rows], list(asdict(rows[0]))), "comparison.csv": csv_text(comparison, list(comparison[0])), "summary.json": canonical(summary), "report.html": render_report(summary, comparison, rows)} + + +def write_bundle(data, out): + files = artifacts(data) + target = Path(out) + target.mkdir(parents=True, exist_ok=True) + for name, content in files.items(): + (target / name).write_text(content, encoding="utf-8", newline="") + manifest = {"schema_version": 1, "engine": "pitbridge/0.1.0", "files": {name: sha256(content.encode()).hexdigest() for name, content in sorted(files.items())}} + (target / "manifest.json").write_text(canonical(manifest), encoding="utf-8") + return read_json(target / "summary.json") + + +def verify_bundle(out): + target = Path(out) + manifest = read_json(target / "manifest.json") + if not isinstance(manifest, dict) or not isinstance(manifest.get("files"), dict) or manifest.get("schema_version") != 1 or manifest.get("engine") != "pitbridge/0.1.0" or set(manifest["files"]) != set(FILES): + raise ValueError("unsupported manifest or unexpected artifact paths") + for name in FILES: + if sha256((target / name).read_bytes()).hexdigest() != manifest["files"][name]: + raise ValueError(f"hash mismatch: {name}") + expected = artifacts(read_json(target / "inputs.json")) + for name, content in expected.items(): + if (target / name).read_bytes() != content.encode(): + raise ValueError(f"semantic replay mismatch: {name}") + return {"verified": True, "files": len(FILES), "independent_replay": True} diff --git a/src/pitbridge/cli.py b/src/pitbridge/cli.py new file mode 100644 index 0000000..65b7b42 --- /dev/null +++ b/src/pitbridge/cli.py @@ -0,0 +1,24 @@ +import argparse +import json +import sys + +from .bundle import read_json, verify_bundle, write_bundle +from .demo import demo_inputs + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Build and independently replay availability-aware feature evidence") + sub = parser.add_subparsers(dest="command", required=True) + for command in ("demo", "build", "verify"): + p = sub.add_parser(command) + p.add_argument("--out", default="outputs") + if command == "build": + p.add_argument("--inputs", required=True, help="JSON observations, decisions and specs") + args = parser.parse_args(argv) + try: + result = verify_bundle(args.out) if args.command == "verify" else write_bundle(demo_inputs() if args.command == "demo" else read_json(args.inputs), args.out) + except (ValueError, OSError, KeyError) as exc: + print(f"pitbridge: {exc}", file=sys.stderr) + return 2 + print(json.dumps(result, indent=2, ensure_ascii=False, allow_nan=False)) + return 0 diff --git a/src/pitbridge/core.py b/src/pitbridge/core.py new file mode 100644 index 0000000..f4c7041 --- /dev/null +++ b/src/pitbridge/core.py @@ -0,0 +1,209 @@ +"""Strict data contracts, a SQLite temporal join, and a separate replay oracle.""" + +from dataclasses import asdict, dataclass +from datetime import datetime, timezone +import math +import sqlite3 +from typing import Iterable + + +def utc(value: str) -> str: + if not isinstance(value, str): + raise ValueError("timestamps must be timezone-aware ISO-8601 strings") + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise ValueError(f"invalid timestamp: {value!r}") from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise ValueError("naive timestamps are forbidden; include Z or an offset") + return parsed.astimezone(timezone.utc).isoformat(timespec="microseconds") + + +def _identifier(value: str, name: str) -> None: + if not isinstance(value, str) or not value.strip() or value != value.strip(): + raise ValueError(f"{name} must be a nonempty, trimmed string") + + +@dataclass(frozen=True) +class Observation: + record_id: str + entity_id: str + source: str + feature: str + event_at: str + published_at: str + ingested_at: str + revision: int + value: float | None + deleted: bool = False + + def __post_init__(self): + for name in ("record_id", "entity_id", "source", "feature"): + _identifier(getattr(self, name), name) + for name in ("event_at", "published_at", "ingested_at"): + object.__setattr__(self, name, utc(getattr(self, name))) + if type(self.revision) is not int or not 1 <= self.revision <= 2**63-1: + raise ValueError("revision must be a positive int64 integer") + if type(self.deleted) is not bool: + raise ValueError("deleted must be a boolean") + if self.deleted: + if self.value is not None: + raise ValueError("a tombstone must have value=null") + elif isinstance(self.value, bool) or not isinstance(self.value, (int, float)) or not math.isfinite(self.value): + raise ValueError("active observations require a finite numeric value") + if self.published_at < self.event_at or self.ingested_at < self.event_at: + raise ValueError("publication and ingestion cannot precede the observation event") + + @property + def available_at(self) -> str: + return max(self.published_at, self.ingested_at) + + +@dataclass(frozen=True) +class Decision: + decision_id: str + entity_id: str + decision_at: str + + def __post_init__(self): + for name in ("decision_id", "entity_id"): + _identifier(getattr(self, name), name) + object.__setattr__(self, "decision_at", utc(self.decision_at)) + + +@dataclass(frozen=True) +class FeatureSpec: + source: str + feature: str + max_age_days: float | None = None + + def __post_init__(self): + for name in ("source", "feature"): + _identifier(getattr(self, name), name) + if self.max_age_days is not None and (isinstance(self.max_age_days, bool) or not isinstance(self.max_age_days, (int, float)) or not math.isfinite(self.max_age_days) or not 0 <= self.max_age_days <= 3652500): + raise ValueError("max_age_days must be within [0, 3652500], or null") + + +@dataclass(frozen=True) +class Snapshot: + decision_id: str + entity_id: str + decision_at: str + source: str + feature: str + value: float | None + status: str + record_id: str | None = None + revision: int | None = None + event_at: str | None = None + published_at: str | None = None + ingested_at: str | None = None + available_at: str | None = None + + +def validate(observations: Iterable[Observation], decisions: Iterable[Decision], specs: Iterable[FeatureSpec]): + obs, dec, features = list(observations), list(decisions), list(specs) + for rows, cls in ((obs, Observation), (dec, Decision), (features, FeatureSpec)): + if any(not isinstance(row, cls) for row in rows): + raise ValueError(f"expected {cls.__name__} instances") + if not dec or not features: + raise ValueError("at least one decision and one feature spec are required") + for keys, name in (([r.record_id for r in obs], "record_id"), ([r.decision_id for r in dec], "decision_id"), ([(r.source, r.feature) for r in features], "feature spec"), ([(r.entity_id, r.source, r.feature, r.event_at, r.revision) for r in obs], "logical revision")): + if len(keys) != len(set(keys)): + raise ValueError(f"duplicate {name}") + allowed = {(r.source, r.feature) for r in features} + if any((r.source, r.feature) not in allowed for r in obs): + raise ValueError("every observation must have an explicit feature spec") + return sorted(obs, key=lambda r: r.record_id), sorted(dec, key=lambda r: r.decision_id), sorted(features, key=lambda r: (r.source, r.feature)) + + +def _instant(value: str) -> int: + delta = datetime.fromisoformat(value) - datetime(1970, 1, 1, tzinfo=timezone.utc) + return (delta.days * 86400 + delta.seconds) * 1_000_000 + delta.microseconds + + +SQL = """ +WITH eligible AS ( + SELECT d.decision_id, o.*, + ROW_NUMBER() OVER ( + PARTITION BY d.decision_id, o.source, o.feature, o.event_at + ORDER BY o.revision DESC + ) AS version_rank + FROM decisions d JOIN observations o ON d.entity_id = o.entity_id + WHERE o.event_us <= d.decision_us AND o.available_us <= d.decision_us +), current_versions AS ( + SELECT * FROM eligible WHERE version_rank = 1 +), fresh AS ( + SELECT c.*, ROW_NUMBER() OVER ( + PARTITION BY c.decision_id, c.source, c.feature ORDER BY c.event_us DESC + ) AS event_rank + FROM current_versions c + JOIN decisions d USING (decision_id) + JOIN specs s USING (source, feature) + WHERE c.deleted = 0 AND + (s.max_age_us IS NULL OR d.decision_us - c.event_us <= s.max_age_us) +) +SELECT d.decision_id, d.entity_id, d.decision_at, s.source, s.feature, + f.value, + CASE WHEN f.record_id IS NOT NULL THEN 'selected' + WHEN NOT EXISTS ( + SELECT 1 FROM observations o WHERE o.entity_id=d.entity_id + AND o.source=s.source AND o.feature=s.feature AND o.event_us<=d.decision_us + ) THEN 'no_history' + WHEN NOT EXISTS ( + SELECT 1 FROM current_versions c WHERE c.decision_id=d.decision_id + AND c.source=s.source AND c.feature=s.feature + ) THEN 'not_available' + WHEN NOT EXISTS ( + SELECT 1 FROM current_versions c WHERE c.decision_id=d.decision_id + AND c.source=s.source AND c.feature=s.feature AND c.deleted=0 + ) THEN 'deleted' + ELSE 'stale' END AS status, + f.record_id, f.revision, f.event_at, f.published_at, f.ingested_at, f.available_at +FROM decisions d CROSS JOIN specs s +LEFT JOIN fresh f ON f.decision_id=d.decision_id AND f.source=s.source + AND f.feature=s.feature AND f.event_rank=1 +ORDER BY d.decision_id, s.source, s.feature +""" + + +def build_snapshot(observations, decisions, specs) -> list[Snapshot]: + """Use both event time and knowledge time, then resolve revisions and TTL.""" + obs, dec, features = validate(observations, decisions, specs) + with sqlite3.connect(":memory:") as db: + db.executescript(""" + CREATE TABLE observations(record_id TEXT,entity_id TEXT,source TEXT,feature TEXT, + event_at TEXT,published_at TEXT,ingested_at TEXT,revision INTEGER,value REAL, + deleted INTEGER,available_at TEXT,event_us INTEGER,available_us INTEGER); + CREATE TABLE decisions(decision_id TEXT,entity_id TEXT,decision_at TEXT,decision_us INTEGER); + CREATE TABLE specs(source TEXT,feature TEXT,max_age_us INTEGER); + CREATE INDEX temporal_lookup ON observations(entity_id,source,feature,event_us,available_us); + """) + db.executemany("INSERT INTO observations VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", [tuple(asdict(r).values()) + (r.available_at, _instant(r.event_at), _instant(r.available_at)) for r in obs]) + db.executemany("INSERT INTO decisions VALUES (?,?,?,?)", [(r.decision_id, r.entity_id, r.decision_at, _instant(r.decision_at)) for r in dec]) + db.executemany("INSERT INTO specs VALUES (?,?,?)", [(r.source, r.feature, None if r.max_age_days is None else round(r.max_age_days * 86400 * 1e6)) for r in features]) + return [Snapshot(*row) for row in db.execute(SQL)] + + +def reference_snapshot(observations, decisions, specs) -> list[Snapshot]: + """Independent Python enumeration; intentionally does not call the SQL engine.""" + obs, dec, features = validate(observations, decisions, specs) + results = [] + for d in dec: + for s in features: + history = [o for o in obs if (o.entity_id, o.source, o.feature) == (d.entity_id, s.source, s.feature) and o.event_at <= d.decision_at] + known = [o for o in history if o.available_at <= d.decision_at] + versions = {} + for o in known: + if o.event_at not in versions or o.revision > versions[o.event_at].revision: + versions[o.event_at] = o + active = [o for o in versions.values() if not o.deleted] + age_limit = None if s.max_age_days is None else round(s.max_age_days * 86400 * 1e6) + fresh = [o for o in active if age_limit is None or _instant(d.decision_at) - _instant(o.event_at) <= age_limit] + status = "selected" if fresh else ("no_history" if not history else "not_available" if not known else "deleted" if not active else "stale") + row = Snapshot(d.decision_id, d.entity_id, d.decision_at, s.source, s.feature, None, status) + if fresh: + chosen = max(fresh, key=lambda o: o.event_at) + row = Snapshot(d.decision_id, d.entity_id, d.decision_at, s.source, s.feature, float(chosen.value), status, chosen.record_id, chosen.revision, chosen.event_at, chosen.published_at, chosen.ingested_at, chosen.available_at) + results.append(row) + return results diff --git a/src/pitbridge/demo.py b/src/pitbridge/demo.py new file mode 100644 index 0000000..851199d --- /dev/null +++ b/src/pitbridge/demo.py @@ -0,0 +1,24 @@ +"""Compact, hand-auditable counterexamples across three financial sources.""" + + +def demo_inputs(): + records = [] + def add(rid, entity, source, feature, event, published, ingested, revision, value, deleted=False): + records.append(dict(record_id=rid, entity_id=entity, source=source, feature=feature, event_at=event+"T00:00:00Z", published_at=published+"T00:00:00Z", ingested_at=ingested+"T00:00:00Z", revision=revision, value=value, deleted=deleted)) + add("tax-a-v1", "SME-A", "tax", "invoice_revenue", "2026-01-31", "2026-02-05", "2026-02-06", 1, 100000) + add("tax-a-v2", "SME-A", "tax", "invoice_revenue", "2026-01-31", "2026-03-10", "2026-03-11", 2, 180000) + add("tax-a-feb", "SME-A", "tax", "invoice_revenue", "2026-02-28", "2026-03-05", "2026-03-20", 1, 220000) + add("power-a", "SME-A", "utility", "kwh", "2026-02-01", "2026-02-02", "2026-02-03", 1, 1800) + add("cash-a", "SME-A", "bank", "net_inflow", "2026-02-10", "2026-02-10", "2026-02-11", 1, 50000) + add("cash-a-delete", "SME-A", "bank", "net_inflow", "2026-02-10", "2026-02-18", "2026-02-18", 2, None, True) + add("tax-b", "SME-B", "tax", "invoice_revenue", "2026-01-31", "2026-02-05", "2026-03-01", 1, 90000) + add("power-b-old", "SME-B", "utility", "kwh", "2025-11-01", "2025-11-02", "2025-11-02", 1, 700) + add("power-b", "SME-B", "utility", "kwh", "2026-02-01", "2026-02-03", "2026-03-02", 1, 1300) + add("cash-b-v2", "SME-B", "bank", "net_inflow", "2026-02-10", "2026-02-12", "2026-02-13", 2, 30000) + # A delayed delivery of revision 1 must not replace already-known revision 2. + add("cash-b-v1-delayed", "SME-B", "bank", "net_inflow", "2026-02-10", "2026-02-11", "2026-02-19", 1, 80000) + decisions = [] + for entity in ("SME-A", "SME-B", "SME-C"): + for i, day in enumerate(("2026-02-04", "2026-02-15", "2026-02-20", "2026-03-15", "2026-03-25"), 1): + decisions.append(dict(decision_id=f"{entity}-{i}", entity_id=entity, decision_at=day+"T00:00:00Z")) + return {"data_kind": "synthetic", "observations": records, "decisions": decisions, "specs": [dict(source="tax", feature="invoice_revenue", max_age_days=90), dict(source="utility", feature="kwh", max_age_days=45), dict(source="bank", feature="net_inflow", max_age_days=60)]} diff --git a/tests/test_bundle.py b/tests/test_bundle.py new file mode 100644 index 0000000..86492bc --- /dev/null +++ b/tests/test_bundle.py @@ -0,0 +1,82 @@ +from hashlib import sha256 +import json +from pathlib import Path +from tempfile import TemporaryDirectory +import unittest + +from pitbridge.bundle import canonical, read_json, verify_bundle, write_bundle +from pitbridge.cli import main +from pitbridge.demo import demo_inputs + + +class BundleTests(unittest.TestCase): + def test_demo_and_saved_inputs_replay(self): + with TemporaryDirectory() as tmp: + summary = write_bundle(demo_inputs(),tmp) + self.assertEqual(summary["future_knowledge_rows"],11) + self.assertEqual(summary["snapshot_rows"],45) + self.assertTrue(verify_bundle(tmp)["verified"]) + + def test_output_is_deterministic(self): + with TemporaryDirectory() as a, TemporaryDirectory() as b: + write_bundle(demo_inputs(),a); write_bundle(demo_inputs(),b) + self.assertEqual({p.name:p.read_bytes() for p in Path(a).iterdir()}, {p.name:p.read_bytes() for p in Path(b).iterdir()}) + + def test_changed_hash_fails_verification(self): + with TemporaryDirectory() as tmp: + write_bundle(demo_inputs(),tmp) + Path(tmp,"snapshots.csv").write_text("edited",encoding="utf-8") + with self.assertRaisesRegex(ValueError,"hash mismatch"): + verify_bundle(tmp) + + def test_rehashed_false_metric_still_fails_semantic_replay(self): + with TemporaryDirectory() as tmp: + write_bundle(demo_inputs(),tmp) + path=Path(tmp,"summary.json") + data=read_json(path); data["future_knowledge_rows"]=0 + path.write_text(canonical(data),encoding="utf-8") + manifest=read_json(Path(tmp,"manifest.json")) + manifest["files"][path.name]=sha256(path.read_bytes()).hexdigest() + Path(tmp,"manifest.json").write_text(canonical(manifest),encoding="utf-8") + with self.assertRaisesRegex(ValueError,"semantic replay mismatch"): + verify_bundle(tmp) + + def test_unexpected_manifest_path_is_rejected(self): + with TemporaryDirectory() as tmp: + write_bundle(demo_inputs(),tmp) + manifest=read_json(Path(tmp,"manifest.json")); manifest["files"]["../outside"]="0" + Path(tmp,"manifest.json").write_text(canonical(manifest),encoding="utf-8") + with self.assertRaisesRegex(ValueError,"unexpected artifact"): + verify_bundle(tmp) + + def test_nonfinite_and_duplicate_json_are_rejected(self): + with TemporaryDirectory() as tmp: + for text in ('{"a": NaN}', '{"a":1,"a":2}'): + Path(tmp,"bad.json").write_text(text,encoding="utf-8") + with self.assertRaises(ValueError): + read_json(Path(tmp,"bad.json")) + + def test_html_escapes_untrusted_identifiers(self): + with TemporaryDirectory() as tmp: + data=demo_inputs(); data["decisions"][0]["decision_id"]="" + write_bundle(data,tmp) + html=Path(tmp,"report.html").read_text() + self.assertIn("<script>unsafe</script>",html) + self.assertNotIn("",html) + + def test_user_supplied_report_is_not_labeled_synthetic(self): + with TemporaryDirectory() as tmp: + data=demo_inputs(); data.pop("data_kind") + write_bundle(data,tmp) + self.assertIn("USER-SUPPLIED DATA",Path(tmp,"report.html").read_text()) + + def test_cli_returns_nonzero_for_missing_artifacts(self): + with TemporaryDirectory() as tmp: + self.assertEqual(main(["verify","--out",tmp]),2) + + def test_malformed_manifest_has_a_clear_validation_error(self): + with TemporaryDirectory() as tmp: + for payload in ([], {"files":None}, {"files":[]}): + Path(tmp,"manifest.json").write_text(canonical(payload),encoding="utf-8") + with self.assertRaisesRegex(ValueError,"unsupported manifest"): + verify_bundle(tmp) diff --git a/tests/test_temporal.py b/tests/test_temporal.py new file mode 100644 index 0000000..5c23ee7 --- /dev/null +++ b/tests/test_temporal.py @@ -0,0 +1,179 @@ +from dataclasses import asdict, replace +from datetime import datetime, timedelta, timezone +import random +import unittest + +from pitbridge.core import Decision, FeatureSpec, Observation, build_snapshot, reference_snapshot +from pitbridge.demo import demo_inputs +from pitbridge.bundle import decode_inputs + + +def observation(**changes): + fields = dict(record_id="r1", entity_id="A", source="tax", feature="revenue", event_at="2026-01-01T00:00:00Z", published_at="2026-01-02T00:00:00Z", ingested_at="2026-01-03T00:00:00Z", revision=1, value=10.0) + fields.update(changes) + return Observation(**fields) + + +def snapshot(records, at="2026-01-05T00:00:00Z", age=None, entity="A"): + return build_snapshot(records, [Decision("D", entity, at)], [FeatureSpec("tax", "revenue", age)])[0] + + +class TemporalTests(unittest.TestCase): + def test_historical_revision_cannot_change_old_decision(self): + revision = observation(record_id="r2", revision=2, value=90, published_at="2026-01-10T00:00:00Z", ingested_at="2026-01-10T00:00:00Z") + self.assertEqual(snapshot([observation(), revision]).value, 10) + self.assertEqual(snapshot([observation(), revision], "2026-01-10T00:00:00Z").value, 90) + + def test_ingestion_delay_is_not_publication_time(self): + row = snapshot([observation(ingested_at="2026-01-08T00:00:00Z")]) + self.assertEqual(row.status, "not_available") + self.assertIsNone(row.value) + + def test_embargo_requires_publication_even_if_ingested(self): + self.assertEqual(snapshot([observation(published_at="2026-01-08T00:00:00Z")]).status, "not_available") + + def test_equality_at_decision_is_inclusive(self): + row = snapshot([observation()], "2026-01-03T00:00:00Z") + self.assertEqual(row.record_id, "r1") + + def test_microsecond_after_decision_is_excluded(self): + self.assertEqual(snapshot([observation()], "2026-01-02T23:59:59.999999Z").status, "not_available") + + def test_future_event_is_never_selected(self): + self.assertEqual(snapshot([observation()], "2025-12-31T00:00:00Z").status, "no_history") + + def test_max_age_boundary_includes_exact_cutoff(self): + self.assertEqual(snapshot([observation()], age=4).status, "selected") + self.assertEqual(snapshot([observation()], "2026-01-05T00:00:00.000001Z", age=4).status, "stale") + + def test_zero_max_age_requires_same_instant(self): + row = observation(published_at="2026-01-01T00:00:00Z", ingested_at="2026-01-01T00:00:00Z") + self.assertEqual(snapshot([row], "2026-01-01T00:00:00Z", age=0).status, "selected") + self.assertEqual(snapshot([row], "2026-01-01T00:00:00.000001Z", age=0).status, "stale") + + def test_known_tombstone_does_not_resurrect_old_revision(self): + tombstone = observation(record_id="del", revision=2, value=None, deleted=True) + self.assertEqual(snapshot([observation(), tombstone]).status, "deleted") + + def test_future_tombstone_does_not_erase_history(self): + tombstone = observation(record_id="del", revision=2, value=None, deleted=True, ingested_at="2026-01-09T00:00:00Z") + self.assertEqual(snapshot([observation(), tombstone]).value, 10) + + def test_tombstone_is_scoped_to_one_event(self): + recent = observation(record_id="recent", event_at="2026-01-02T00:00:00Z", value=30) + tombstone = replace(recent, record_id="del", revision=2, deleted=True, value=None) + self.assertEqual(snapshot([observation(), recent, tombstone]).record_id, "r1") + + def test_late_lower_revision_does_not_replace_higher_revision(self): + old = observation(ingested_at="2026-01-04T00:00:00Z") + newer = observation(record_id="r2", revision=2, value=20) + self.assertEqual(snapshot([old, newer]).value, 20) + + def test_event_order_precedes_revision_number(self): + old = observation(revision=99) + recent = observation(record_id="recent", event_at="2026-01-02T00:00:00Z", value=40) + self.assertEqual(snapshot([old, recent]).value, 40) + + def test_timezones_are_compared_as_utc_instants(self): + row = observation(ingested_at="2026-01-03T08:00:00+08:00") + self.assertEqual(snapshot([row], "2026-01-02T19:00:00-05:00").status, "selected") + + def test_entity_isolation_and_missing_rows_are_preserved(self): + self.assertEqual(snapshot([observation()], entity="B").status, "no_history") + rows = build_snapshot([], [Decision("D", "A", "2026-01-05T00:00:00Z")], [FeatureSpec("tax", "revenue"), FeatureSpec("bank", "cash")]) + self.assertEqual(len(rows), 2) + self.assertTrue(all(r.value is None for r in rows)) + + def test_sources_with_same_feature_name_are_isolated(self): + obs = [observation(), observation(record_id="bank", source="bank", value=123)] + rows = build_snapshot(obs, [Decision("D", "A", "2026-01-05T00:00:00Z")], [FeatureSpec("tax", "revenue"), FeatureSpec("bank", "revenue")]) + self.assertEqual({r.source:r.value for r in rows}, {"bank":123, "tax":10}) + + def test_shuffle_does_not_change_snapshot(self): + obs, dec, specs = decode_inputs(demo_inputs()) + expected = build_snapshot(obs, dec, specs) + rng = random.Random(42) + for _ in range(10): + rng.shuffle(obs); rng.shuffle(dec); rng.shuffle(specs) + self.assertEqual(build_snapshot(obs,dec,specs), expected) + + def test_appending_future_revisions_cannot_change_old_rows(self): + obs, dec, specs = decode_inputs(demo_inputs()) + extra = replace(obs[0], record_id="future", revision=1000, value=None, deleted=True, published_at="2027-01-01T00:00:00Z", ingested_at="2027-01-01T00:00:00Z") + self.assertEqual(build_snapshot(obs,dec,specs), build_snapshot(obs+[extra],dec,specs)) + + def test_randomized_sql_matches_independent_enumeration(self): + rng = random.Random(711) + origin = datetime(2026,1,1,tzinfo=timezone.utc) + def stamp(day): + return (origin+timedelta(days=day)).isoformat() + for trial in range(40): + obs = [] + for entity in ("A","B"): + for source in ("tax","bank"): + for event in range(4): + for revision in range(1,4): + deleted = rng.random() < .2 + obs.append(Observation(f"{trial}-{entity}-{source}-{event}-{revision}",entity,source,"value",stamp(event*3),stamp(event*3+rng.randrange(8)),stamp(event*3+rng.randrange(12)),revision,None if deleted else rng.uniform(-100,100),deleted)) + dec = [Decision(f"D{i}",rng.choice(("A","B","C")),stamp(rng.randrange(20))) for i in range(12)] + specs = [FeatureSpec("tax","value",rng.choice((None,0,5,20))),FeatureSpec("bank","value",5)] + self.assertEqual(build_snapshot(obs,dec,specs),reference_snapshot(obs,dec,specs)) + + +class ContractTests(unittest.TestCase): + def test_duplicate_ids_are_rejected(self): + with self.assertRaises(ValueError): + snapshot([observation(), observation()]) + + def test_duplicate_logical_revisions_are_rejected(self): + with self.assertRaises(ValueError): + snapshot([observation(), observation(record_id="other")]) + + def test_duplicate_decisions_and_specs_are_rejected(self): + d = Decision("D","A","2026-01-05T00:00:00Z") + s = FeatureSpec("tax","revenue") + for dec,spec in (([d,d],[s]),([d],[s,s])): + with self.subTest(decisions=len(dec),specs=len(spec)), self.assertRaises(ValueError): + build_snapshot([observation()],dec,spec) + + def test_invalid_values_and_revisions_are_rejected(self): + for value in (float("nan"),float("inf"),None,True,"10"): + with self.subTest(value=value), self.assertRaises(ValueError): + observation(value=value) + for revision in (0,-1,1.5,True,2**63): + with self.subTest(revision=revision), self.assertRaises(ValueError): + observation(revision=revision) + + def test_tombstone_requires_boolean_and_null_value(self): + for fields in ({"deleted":1},{"deleted":True,"value":10}): + with self.subTest(fields=fields), self.assertRaises(ValueError): + observation(**fields) + + def test_naive_and_invalid_timestamps_are_rejected(self): + for stamp in ("2026-01-01", "bad", 0): + with self.subTest(stamp=stamp), self.assertRaises(ValueError): + observation(event_at=stamp) + + def test_impossible_chronology_is_rejected(self): + for name in ("published_at","ingested_at"): + with self.subTest(field=name), self.assertRaises(ValueError): + observation(**{name:"2025-12-31T00:00:00Z"}) + + def test_negative_nonfinite_and_huge_ttl_are_rejected(self): + for age in (-1, True, float("inf"), 1e100): + with self.subTest(age=age), self.assertRaises(ValueError): + FeatureSpec("tax","revenue",age) + + def test_unknown_features_are_not_silently_dropped(self): + with self.assertRaises(ValueError): + snapshot([observation(feature="unknown")]) + + def test_unknown_input_columns_are_rejected(self): + data = demo_inputs() + data["observations"][0]["future_label"] = 1 + with self.assertRaises(ValueError): + decode_inputs(data) + + def test_blank_ids_are_rejected(self): + with self.assertRaises(ValueError): + observation(entity_id=" ")
PITBRIDGE / {kind}
+

What was known
when the decision was made?

+

Availability-aware SQL snapshots with publication, ingestion, revision and record-level lineage.

+
{summary['decisions']}Decisions
{summary['snapshot_rows']}Feature lookups
{summary['future_knowledge_rows']}Unsafe future-data selections
{summary['different_selections']}Selections changed
+

Independent replay: SQLite output matches the Python enumeration oracle. {counts}

+

Inspect the leakage counterexample

The unsafe comparison filters only event time and takes the highest revision. It intentionally ignores availability, tombstones and freshness; it is never exported as a training feature.

+ +
{comparison_rows}
DecisionSourceFeatureSafe valueSafe statusUnsafe valueUses future knowledge?
+

Trace each selected observation

Knowledge time = max(publication, ingestion). Every selected observation was available at or before the decision. A later historical correction cannot overwrite an earlier snapshot.

+
{lineage_rows}
DecisionSourceFeatureRecordRevisionEvent timeAvailable time
+

Replay locally

pitbridge verify --out demo checks file hashes, recomputes every snapshot through both implementations, and compares all derived evidence. Hashes detect accidental edits; they are not a digital signature.

+