From 2a473e42ee4f20274e0ec1136a7be7cff028825d Mon Sep 17 00:00:00 2001 From: dev-belly Date: Wed, 28 Oct 2026 15:57:01 +0800 Subject: [PATCH] Release 0.2.1 with checked timestamp precision --- README.md | 15 ++-- README.zh-CN.md | 12 ++-- docs/DATA_CONTRACT.md | 8 ++- docs/RELEASING.md | 6 +- docs/releases/v0.2.1.md | 35 ++++++++++ pyproject.toml | 2 +- scripts/check_distribution.py | 118 ++++++++++++++++++++++++++++++++ scripts/release_artifacts.py | 9 +++ src/pitbridge/__init__.py | 2 +- tests/test_release_artifacts.py | 25 +++++++ 10 files changed, 212 insertions(+), 20 deletions(-) create mode 100644 docs/releases/v0.2.1.md diff --git a/README.md b/README.md index 54d0ad3..f94ecb3 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ dependencies**. The commands use a virtual environment without requiring activation. These commands clone `main` for the source examples. For a fixed installation -without Git, use the [checked v0.2.0 wheel](#install-the-checked-release). -v0.2.0 includes the installed CSV importer, rolling features and `benchmark`; -the earlier v0.1.0 tag lacks the CSV importer and benchmark. +without Git, use the [checked v0.2.1 wheel](#install-the-checked-release). +v0.2.1 includes the installed CSV importer, rolling features and `benchmark`, +and rejects unsupported timestamp precision before selection; the earlier +v0.1.0 tag lacks the CSV importer and benchmark. **Linux / macOS** @@ -81,21 +82,21 @@ Install this repository's wheel directly, using its full URL: ```bash python3 -m venv .venv -.venv/bin/python -m pip install "https://github.com/dev-belly/PITBridge/releases/download/v0.2.0/pitbridge-0.2.0-py3-none-any.whl" +.venv/bin/python -m pip install "https://github.com/dev-belly/PITBridge/releases/download/v0.2.1/pitbridge-0.2.1-py3-none-any.whl" .venv/bin/python -m pitbridge demo --out outputs .venv/bin/python -m pitbridge verify --out outputs ``` On Windows, create the environment with `python -m venv .venv` and substitute `.\.venv\Scripts\python.exe` for `.venv/bin/python`. -The [release page](https://github.com/dev-belly/PITBridge/releases/tag/v0.2.0) +The [release page](https://github.com/dev-belly/PITBridge/releases/tag/v0.2.1) also provides the source archive, `installed-check.json`, an exact-commit `release-manifest.json` and `CHECKSUMS.sha256`. The wheel is built from the source archive, installed in a fresh environment outside the checkout and -used for scalar/rolling JSON and CSV replay plus a small benchmark. Publishing +used for scalar/rolling JSON and CSV replay, timestamp-precision checks and a small benchmark. Publishing waits for the complete main CI, including Windows, pandas and Chrome jobs; the publisher verifies the exact CI artifacts instead of rebuilding them. -[Release notes and replay compatibility](docs/releases/v0.2.0.md) · +[Release notes and replay compatibility](docs/releases/v0.2.1.md) · [Reproduce the release checks](docs/RELEASING.md). ## Use the Python API diff --git a/README.zh-CN.md b/README.zh-CN.md index 84ae26b..f572042 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -26,8 +26,8 @@ PITBridge 同时处理数据发生时间、公开时间、入库时间和修订 需要 Git 和 Python 3.11 或更高版本。下面是 Windows PowerShell 命令,使用独立环境,无需先激活: -这些命令克隆 `main` 以运行源码示例。固定安装可使用下面的 v0.2.0 wheel, -其中已包含内置 CSV 接入、滚动特征和 `benchmark` 命令。 +这些命令克隆 `main` 以运行源码示例。固定安装可使用下面的 v0.2.1 wheel, +其中包含内置 CSV 接入、滚动特征、`benchmark` 命令及时间精度校验。 较早的 v0.1.0 标签不包含内置 CSV 接入和 benchmark。 ```powershell @@ -51,16 +51,16 @@ Linux / macOS 用 `python3 -m venv .venv` 创建环境,并将上述 `.\.venv\S ```powershell python -m venv .venv -.\.venv\Scripts\python.exe -m pip install "https://github.com/dev-belly/PITBridge/releases/download/v0.2.0/pitbridge-0.2.0-py3-none-any.whl" +.\.venv\Scripts\python.exe -m pip install "https://github.com/dev-belly/PITBridge/releases/download/v0.2.1/pitbridge-0.2.1-py3-none-any.whl" .\.venv\Scripts\python.exe -m pitbridge demo --out outputs .\.venv\Scripts\python.exe -m pitbridge verify --out outputs ``` -[发布页面](https://github.com/dev-belly/PITBridge/releases/tag/v0.2.0)同时提供源码包、 +[发布页面](https://github.com/dev-belly/PITBridge/releases/tag/v0.2.1)同时提供源码包、 安装核验记录、固定提交号的发布清单和 SHA-256 校验值。wheel 从源码包构建, -在离开源码目录的干净环境中检查四条 JSON/CSV 路径与小规模 benchmark; +在离开源码目录的干净环境中检查四条 JSON/CSV 路径、时间精度及小规模 benchmark; 完整主分支 CI(包括 Windows、pandas 和 Chrome)通过后才发布同一份产物。 -[升级与重放边界](docs/releases/v0.2.0.md) · [复现发布检查](docs/RELEASING.md)。 +[升级与重放边界](docs/releases/v0.2.1.md) · [复现发布检查](docs/RELEASING.md)。 ## 可直接复用的 Python 案例 diff --git a/docs/DATA_CONTRACT.md b/docs/DATA_CONTRACT.md index 2a4e2dd..e1ec22d 100644 --- a/docs/DATA_CONTRACT.md +++ b/docs/DATA_CONTRACT.md @@ -20,9 +20,11 @@ CSV and the optional pandas example use this same validation before selection or report creation. A source with finer timestamps must choose and document an upstream conversion policy; this engine does not infer one. -These stricter checks are on `main` after the v0.2.0 release. Existing release -assets are unchanged; use the source installation for this validation. Bundles -with timestamps already in the supported form retain their original bytes. +These stricter checks are included in v0.2.1. The unchanged v0.2.0 wheel can +truncate excess digits before selection. Previously discarded timestamp digits +cannot be recovered from normalized bundles: retain the original source and +rebuild affected inputs before reviewing decisions. Bundles with timestamps +already in the supported form retain their original bytes. | Field | Type / rule | | :--- | :--- | diff --git a/docs/RELEASING.md b/docs/RELEASING.md index e9a1f17..4a8c572 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -6,8 +6,10 @@ the wheel without a package index into a fresh virtual environment. From a directory outside the checkout it verifies package/runtime versions, generates and semantically replays scalar/rolling JSON and CSV bundles, compares the CSV results, and runs two 2,000-observation benchmark trials with the independent -oracle. The benchmark is an installation smoke check, not a release performance -claim. +oracle. The installed wheel also checks exact-cutoff versus one-microsecond-late +selection and rejects a seventh digit on all four scalar/rolling JSON/CSV CLI +routes before creating a report. The benchmark is an installation smoke check, +not a release performance claim. To reproduce from a clean checkout with no stale files in `dist` or `release`: diff --git a/docs/releases/v0.2.1.md b/docs/releases/v0.2.1.md new file mode 100644 index 0000000..659246c --- /dev/null +++ b/docs/releases/v0.2.1.md @@ -0,0 +1,35 @@ +# PITBridge v0.2.1 + +The checked wheel includes the decision-time precision repair already merged +into the source. A seventh fractional-second digit could previously be silently +discarded, making a source record that arrived just after a decision appear +available at the cutoff. Timestamps finer than microseconds now fail before +selection or report creation, including trailing zero digits. + +```bash +python -m pip install "https://github.com/dev-belly/PITBridge/releases/download/v0.2.1/pitbridge-0.2.1-py3-none-any.whl" +python -m pitbridge demo --out outputs +python -m pitbridge verify --out outputs +``` + +Python 3.11+ is required; the wheel has no third-party runtime dependencies. +The installed-package check verifies scalar and rolling selection at the cutoff +and one supported microsecond later. It exercises JSON and CSV inputs for both +commands and requires exit 2, a useful diagnostic and no output directory for +an unsupported seventh digit. The release gate requires this evidence alongside +the existing independent replay and benchmark checks. + +Fractional seconds on shortened clock forms and fractional-second timezone +offsets also fail explicitly. Standard timezone-aware timestamps with up to +six digits and whole-second offsets retain their existing interpretation. +Existing supported-format demo evidence remains verifiable. + +The old v0.2.0 wheel remains available unchanged. If old input timestamps were +already normalized, discarded digits cannot be reconstructed from a saved +bundle. Keep the original source records and rebuild affected cases under a +documented upstream precision policy before relying on their historical +selections. Package version numbers are separate from the existing bundle +format markers; this release does not claim source timestamp authenticity. + +[Full timestamp contract](../DATA_CONTRACT.md) · +[CSV intake](../CSV_INPUTS.md) · [Reproduce release checks](../RELEASING.md). diff --git a/pyproject.toml b/pyproject.toml index 32c74c9..ec18b12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pitbridge" -version = "0.2.0" +version = "0.2.1" description = "Availability-aware financial feature snapshots with revision lineage" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/check_distribution.py b/scripts/check_distribution.py index d71c6a8..ca09380 100644 --- a/scripts/check_distribution.py +++ b/scripts/check_distribution.py @@ -12,6 +12,7 @@ import tomllib import venv from pathlib import Path +from textwrap import dedent def check(directory: Path) -> dict: @@ -73,6 +74,118 @@ def run(*arguments: str) -> str: "Installed distribution/runtime version differs from the source version" ) run(str(python), "-I", "-m", "pitbridge", "--help") + + # An arrival just after a decision must never be rounded into visibility. + # Exercise the actual installed package, not the source checkout. + boundary = json.loads( + run( + str(python), + "-I", + "-c", + dedent(""" + import json + from pitbridge import Decision, FeatureSpec, Observation, build_snapshot + from pitbridge.rolling import RollingSpec, build_rolling + + cutoff = "2026-01-01T00:00:00Z" + decision = [Decision("d", "e", cutoff)] + scalar_spec = [FeatureSpec("s", "f", 1)] + rolling_spec = [RollingSpec("total", "s", "f", 1)] + for ingestion, available in ( + (cutoff, True), ("2026-01-01T00:00:00.000001Z", False) + ): + obs = [Observation("r", "e", "s", "f", cutoff, cutoff, + ingestion, 1, 10)] + scalar = build_snapshot(obs, decision, scalar_spec)[0] + rolling, members = build_rolling(obs, decision, rolling_spec) + if ((scalar.record_id == "r") != available + or (len(members) == 1) != available + or (rolling[0].status != "not_available") != available): + raise ValueError("Incorrect installed cutoff selection") + print(json.dumps({"cutoff_and_late_microsecond": True})) + """), + ) + ) + + cutoff = "2026-01-01T00:00:00Z" + late = "2026-01-01T00:00:00.0000009Z" + for command, spec in (("build", "specs"), ("aggregate", "rolling_specs")): + case = { + "observations": [ + { + "record_id": "r", + "entity_id": "e", + "source": "s", + "feature": "f", + "event_at": cutoff, + "published_at": cutoff, + "ingested_at": late, + "revision": 1, + "value": 10, + "deleted": False, + } + ], + "decisions": [ + {"decision_id": "d", "entity_id": "e", "decision_at": cutoff} + ], + spec: ( + [{"source": "s", "feature": "f", "max_age_days": 1}] + if command == "build" + else [ + { + "name": "total", + "source": "s", + "feature": "f", + "window_days": 1, + "aggregation": "sum", + } + ] + ), + } + inputs = work / f"precision-{command}.json" + inputs.write_text(json.dumps(case), encoding="utf-8") + tables = work / f"precision-{command}-tables" + tables.mkdir() + for section, rows in case.items(): + with (tables / f"{section}.csv").open( + "w", encoding="utf-8", newline="" + ) as handle: + writer = csv.DictWriter(handle, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows( + { + key: str(value).lower() + if isinstance(value, bool) + else value + for key, value in row.items() + } + for row in rows + ) + for route, source in (("--inputs", inputs), ("--csv-dir", tables)): + output = work / f"precision-{command}-{route[2:]}-output" + result = subprocess.run( + (str(cli), command, route, str(source), "--out", str(output)), + cwd=work, + env=environment, + text=True, + encoding="utf-8", + errors="replace", + capture_output=True, + check=False, + ) + if ( + result.returncode != 2 + or result.stdout + or output.exists() + or "microsecond precision" not in result.stderr + or "Traceback" in result.stderr + ): + raise ValueError( + f"Installed {command} {route} did not reject excess precision" + ) + if boundary != {"cutoff_and_late_microsecond": True}: + raise ValueError("Installed package changed supported cutoff semantics") + run(str(cli), "demo", "--out", "scalar") scalar = json.loads(run(str(cli), "verify", "--out", "scalar")) run(str(cli), "rolling-demo", "--out", "rolling") @@ -138,6 +251,11 @@ def run(*arguments: str) -> str: "python": sys.version.split()[0], "platform": sys.platform, "installation": "non-editable wheel; clean venv; cwd outside the source tree", + "timestamp_precision": { + "cutoff_and_late_microsecond": True, + "json_and_csv_rejections": 4, + "no_reports_on_rejection": True, + }, "scalar": scalar, "rolling": rolling, "csv": csv_results, diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index 207c90d..bb9fde8 100644 --- a/scripts/release_artifacts.py +++ b/scripts/release_artifacts.py @@ -51,6 +51,15 @@ def validate_check(path: Path, version: str) -> None: or check.get("version") != version ): raise ValueError("Installed check must pass for the source version") + precision = check.get("timestamp_precision") + if ( + not isinstance(precision, dict) + or precision.get("cutoff_and_late_microsecond") is not True + or type(precision.get("json_and_csv_rejections")) is not int + or precision["json_and_csv_rejections"] != 4 + or precision.get("no_reports_on_rejection") is not True + ): + raise ValueError("Installed timestamp-precision boundary evidence is required") for section, key in ( ("scalar", "independent_replay"), ("rolling", "independent_temporal_replay"), diff --git a/src/pitbridge/__init__.py b/src/pitbridge/__init__.py index 6ce80be..6a5710a 100644 --- a/src/pitbridge/__init__.py +++ b/src/pitbridge/__init__.py @@ -3,5 +3,5 @@ from .core import Decision, FeatureSpec, Observation, Snapshot, build_snapshot, reference_snapshot from .csv_inputs import load_csv_inputs -__version__ = "0.2.0" +__version__ = "0.2.1" __all__ = ["Decision", "FeatureSpec", "Observation", "Snapshot", "build_snapshot", "reference_snapshot", "load_csv_inputs"] diff --git a/tests/test_release_artifacts.py b/tests/test_release_artifacts.py index 895aed2..c15b869 100644 --- a/tests/test_release_artifacts.py +++ b/tests/test_release_artifacts.py @@ -29,6 +29,11 @@ def setUp(self): self.result = { "status": "verified", "version": self.version, + "timestamp_precision": { + "cutoff_and_late_microsecond": True, + "json_and_csv_rejections": 4, + "no_reports_on_rejection": True, + }, "scalar": scalar, "rolling": rolling, "csv": {"scalar": scalar, "rolling": rolling}, @@ -122,6 +127,26 @@ def test_benchmark_disagreement_prevents_publication(self): self.prepare() self.assertFalse(self.destination.exists()) + def test_missing_or_false_installed_precision_evidence_prevents_publication(self): + original = copy.deepcopy(self.result) + for field, value in ( + (None, None), + ("cutoff_and_late_microsecond", False), + ("json_and_csv_rejections", 3), + ("json_and_csv_rejections", True), + ("no_reports_on_rejection", False), + ): + self.result = copy.deepcopy(original) + if field is None: + self.result.pop("timestamp_precision") + else: + self.result["timestamp_precision"][field] = value + self.write_check() + with self.subTest(field=field, value=value): + with self.assertRaisesRegex(ValueError, "timestamp-precision"): + self.prepare() + self.assertFalse(self.destination.exists()) + def test_wrong_distribution_metadata_prevents_publication(self): for kwargs in ( {"wheel_version": "9.0.0"},