manithy-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: manithy-sdk
3
+ Version: 0.1.0
4
+ Summary: Manithy SDK — Authority-Grade Audit Capture (Zero Dependencies)
5
+ License: Proprietary
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=7.0; extra == "dev"
10
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
11
+
12
+ # Manithy SDK (Python)
13
+
14
+ **Authority-Grade Audit Capture — Zero Dependencies**
15
+
16
+ Manithy captures tamper-evident audit proofs at the application layer.
17
+ Each proof is a **J01 CommitBoundaryEvent** — a structured record that
18
+ marks the exact t-1 boundary before an irreversible action and freezes
19
+ only facts already resolved in the execution context.
20
+
21
+ No lookups. No inference. No enrichment.
22
+
23
+ ## Design Constraints
24
+
25
+ | Constraint | Guarantee |
26
+ |---|---|
27
+ | **Zero Network I/O** | The SDK never opens sockets or makes HTTP calls. |
28
+ | **Determinism** | Identical inputs always yield identical commit-IDs. |
29
+ | **Fail-Closed** | Internal errors are silently swallowed — the host app never crashes. |
30
+ | **Zero Dependencies** | Only the Python standard library is used at runtime. |
31
+ | **Epistemic Honesty** | The `availability` block declares what was knowable at t-1. Unknown facts must never appear in `observed`. |
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install manithy-sdk
37
+ ```
38
+
39
+ Or install from source:
40
+
41
+ ```bash
42
+ git clone https://github.com/VooYee/manithy-sdk.git
43
+ cd manithy-sdk
44
+ pip install .
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ```python
50
+ from manithy import ManithySDK
51
+
52
+ sdk = ManithySDK()
53
+
54
+ result = sdk.capture(
55
+ boundary_kind="REFUND_COMMIT_T_MINUS_1",
56
+ boundary_seq=1,
57
+ same_thread=True,
58
+ observed={
59
+ "action_kind": "REFUND",
60
+ "amount_minor": 12900,
61
+ "currency": "EUR",
62
+ "refund_mode": "FULL",
63
+ "order_channel": "WEB",
64
+ "payment_method": "CARD",
65
+ "merchant_region": "EU",
66
+ "customer_present": False,
67
+ "operator_initiated": False,
68
+ },
69
+ availability={
70
+ "psp_refund_capability_known": True,
71
+ "original_payment_state_known": True,
72
+ "chargeback_state_known": False,
73
+ },
74
+ reentrancy_guard="SINGLE_CAPTURE_ENFORCED",
75
+ )
76
+
77
+ print(result)
78
+ # {"status": "CAPTURED", "id": "a3f8c9..."}
79
+ ```
80
+
81
+ Output (stdout):
82
+
83
+ ```
84
+ MANITHY_PROOF::{"schema_id":"manithy.commit_boundary_event.v1","boundary_kind":"REFUND_COMMIT_T_MINUS_1","boundary_seq":1,"same_thread":true,"reentrancy_guard":"SINGLE_CAPTURE_ENFORCED","observed":{...},"availability":{...}}
85
+ ```
86
+
87
+ ## Capture Parameters
88
+
89
+ | Parameter | Type | Purpose |
90
+ |---|---|---|
91
+ | **`boundary_kind`** | `str` | Which irreversible boundary this event refers to. Consumer-defined closed enum (e.g. `"REFUND_COMMIT_T_MINUS_1"`). |
92
+ | **`boundary_seq`** | `int` | Supports rare cases of multiple irreversible calls in one execution path. Small integer (0–255). |
93
+ | **`same_thread`** | `bool` | Runtime assertion that capture happened same-thread at t-1. |
94
+ | **`observed`** | `dict[str, str\|int\|bool]` | Runtime facts already resolved in the execution context. Values must be primitives only — no floats, no `None`, no nested structures. |
95
+ | **`availability`** | `dict[str, bool]` | Epistemic visibility at t-1. Each key declares whether a fact was knowable before the irreversible action. |
96
+ | **`reentrancy_guard`** | `str` | Capture enforcement mode. Consumer-defined (e.g. `"SINGLE_CAPTURE_ENFORCED"`). |
97
+
98
+ ### The `observed` Block
99
+
100
+ All fields in `observed` must already be resolved in the execution context at t-1.
101
+
102
+ **Allowed:** `str`, `int`, `bool`.
103
+ **Forbidden:** `float`, `None`, nested `dict`/`list`, any value fetched or inferred after execution.
104
+
105
+ ### The `availability` Block
106
+
107
+ `availability` is not data. It is a **declaration of epistemic visibility** at t-1.
108
+
109
+ Each key answers one question:
110
+ > "At the exact moment before the irreversible action, was this fact already knowable inside the execution context — yes or no?"
111
+
112
+ | `_known` value | Meaning | Effect |
113
+ |---|---|---|
114
+ | `True` | Fact was knowable at t-1 | May appear in `observed` |
115
+ | `False` | Fact was NOT knowable at t-1 | Must NOT appear in `observed` |
116
+
117
+ If the fact was not knowable, Manithy records **ignorance**, not a value.
118
+ That ignorance is structural and permanent.
119
+
120
+ ### Forbidden Fields
121
+
122
+ The following fields must **never** appear in a CommitBoundaryEvent:
123
+
124
+ | Field | Why Forbidden |
125
+ |---|---|
126
+ | `producer_invocation_id` | High joinability risk. Single-capture is enforced via guard state, not IDs. |
127
+ | `callsite_id` | High joinability risk. |
128
+ | `producer_build_id` | Belongs in PackInit / EvidencePack provenance, not J01. |
129
+
130
+ ## Custom Buffer
131
+
132
+ Route proofs to a file, queue, or any destination by subclassing `CaptureBuffer`:
133
+
134
+ ```python
135
+ import json
136
+ from manithy import ManithySDK
137
+ from manithy.interfaces.buffer import CaptureBuffer
138
+
139
+ class FileBuffer(CaptureBuffer):
140
+ def __init__(self, path: str):
141
+ self._file = open(path, "a", encoding="utf-8")
142
+
143
+ def emit(self, envelope: dict) -> None:
144
+ self._file.write(json.dumps(envelope, separators=(",", ":")) + "\n")
145
+ self._file.flush()
146
+
147
+ sdk = ManithySDK(buffer=FileBuffer("audit.log"))
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ ### Kill-Switch
153
+
154
+ Disable all capture at runtime without code changes:
155
+
156
+ ```bash
157
+ export MANITHY_ENABLED=false # Linux/macOS
158
+ ```
159
+
160
+ ```powershell
161
+ $env:MANITHY_ENABLED = "false" # Windows PowerShell
162
+ ```
163
+
164
+ When disabled, `capture()` returns `{"status": "SKIPPED"}` immediately.
165
+
166
+ ### Debug Mode
167
+
168
+ Log internal SDK errors to stderr (useful during development):
169
+
170
+ ```bash
171
+ export MANITHY_DEBUG=true
172
+ ```
173
+
174
+ ## How It Works
175
+
176
+ 1. **Kill-switch check** — reads `MANITHY_ENABLED`. If `"false"`, returns `SKIPPED`.
177
+ 2. **Validation** — enforces type constraints on all fields; rejects forbidden fields, floats in `observed`, non-bool in `availability`, and unknown facts that leak into `observed`.
178
+ 3. **Event assembly** — builds a J01 `CommitBoundaryEvent` with schema `manithy.commit_boundary_event.v1`.
179
+ 4. **Hashing** — canonicalizes the event (sorted keys, no whitespace, floats like `100.0` → `100`) and computes SHA-256 → 64-char hex `commit_id`.
180
+ 5. **Emit** — writes the event to the configured buffer (default: stdout with `MANITHY_PROOF::` prefix).
181
+
182
+ If any step fails, the error is swallowed and `{"status": "ERROR", "error": "Internal SDK Error"}` is returned. The host application is **never** affected.
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ # Create a virtual environment
188
+ python -m venv .venv
189
+
190
+ # Activate it
191
+ source .venv/bin/activate # Linux/macOS
192
+ .venv\Scripts\Activate.ps1 # Windows PowerShell
193
+
194
+ # Install in editable mode with dev dependencies
195
+ pip install -e ".[dev]"
196
+
197
+ # Run tests
198
+ pytest tests/ -v
199
+ ```
200
+
201
+ ## Project Structure
202
+
203
+ ```
204
+ src/manithy/
205
+ ├── __init__.py # Public API: exposes ManithySDK
206
+ ├── sdk.py # Main entry point (capture pipeline + fail-closed)
207
+ ├── config.py # Environment variable loader (kill-switch + debug)
208
+ ├── core/
209
+ │ ├── canonical.py # Deterministic JSON canonicalization
210
+ │ ├── hasher.py # SHA-256 commit-ID generation
211
+ │ └── envelope.py # J01 CommitBoundaryEvent assembly + validation
212
+ └── interfaces/
213
+ └── buffer.py # Abstract CaptureBuffer + StdoutBuffer
214
+ tests/
215
+ ├── vectors.json # Golden test vectors (canonical + hash)
216
+ ├── test_core.py # Core module tests (canonical, hasher, J01 event)
217
+ └── test_sdk.py # SDK integration tests (capture, kill-switch, fail-closed)
218
+ ```
@@ -0,0 +1,207 @@
1
+ # Manithy SDK (Python)
2
+
3
+ **Authority-Grade Audit Capture — Zero Dependencies**
4
+
5
+ Manithy captures tamper-evident audit proofs at the application layer.
6
+ Each proof is a **J01 CommitBoundaryEvent** — a structured record that
7
+ marks the exact t-1 boundary before an irreversible action and freezes
8
+ only facts already resolved in the execution context.
9
+
10
+ No lookups. No inference. No enrichment.
11
+
12
+ ## Design Constraints
13
+
14
+ | Constraint | Guarantee |
15
+ |---|---|
16
+ | **Zero Network I/O** | The SDK never opens sockets or makes HTTP calls. |
17
+ | **Determinism** | Identical inputs always yield identical commit-IDs. |
18
+ | **Fail-Closed** | Internal errors are silently swallowed — the host app never crashes. |
19
+ | **Zero Dependencies** | Only the Python standard library is used at runtime. |
20
+ | **Epistemic Honesty** | The `availability` block declares what was knowable at t-1. Unknown facts must never appear in `observed`. |
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install manithy-sdk
26
+ ```
27
+
28
+ Or install from source:
29
+
30
+ ```bash
31
+ git clone https://github.com/VooYee/manithy-sdk.git
32
+ cd manithy-sdk
33
+ pip install .
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```python
39
+ from manithy import ManithySDK
40
+
41
+ sdk = ManithySDK()
42
+
43
+ result = sdk.capture(
44
+ boundary_kind="REFUND_COMMIT_T_MINUS_1",
45
+ boundary_seq=1,
46
+ same_thread=True,
47
+ observed={
48
+ "action_kind": "REFUND",
49
+ "amount_minor": 12900,
50
+ "currency": "EUR",
51
+ "refund_mode": "FULL",
52
+ "order_channel": "WEB",
53
+ "payment_method": "CARD",
54
+ "merchant_region": "EU",
55
+ "customer_present": False,
56
+ "operator_initiated": False,
57
+ },
58
+ availability={
59
+ "psp_refund_capability_known": True,
60
+ "original_payment_state_known": True,
61
+ "chargeback_state_known": False,
62
+ },
63
+ reentrancy_guard="SINGLE_CAPTURE_ENFORCED",
64
+ )
65
+
66
+ print(result)
67
+ # {"status": "CAPTURED", "id": "a3f8c9..."}
68
+ ```
69
+
70
+ Output (stdout):
71
+
72
+ ```
73
+ MANITHY_PROOF::{"schema_id":"manithy.commit_boundary_event.v1","boundary_kind":"REFUND_COMMIT_T_MINUS_1","boundary_seq":1,"same_thread":true,"reentrancy_guard":"SINGLE_CAPTURE_ENFORCED","observed":{...},"availability":{...}}
74
+ ```
75
+
76
+ ## Capture Parameters
77
+
78
+ | Parameter | Type | Purpose |
79
+ |---|---|---|
80
+ | **`boundary_kind`** | `str` | Which irreversible boundary this event refers to. Consumer-defined closed enum (e.g. `"REFUND_COMMIT_T_MINUS_1"`). |
81
+ | **`boundary_seq`** | `int` | Supports rare cases of multiple irreversible calls in one execution path. Small integer (0–255). |
82
+ | **`same_thread`** | `bool` | Runtime assertion that capture happened same-thread at t-1. |
83
+ | **`observed`** | `dict[str, str\|int\|bool]` | Runtime facts already resolved in the execution context. Values must be primitives only — no floats, no `None`, no nested structures. |
84
+ | **`availability`** | `dict[str, bool]` | Epistemic visibility at t-1. Each key declares whether a fact was knowable before the irreversible action. |
85
+ | **`reentrancy_guard`** | `str` | Capture enforcement mode. Consumer-defined (e.g. `"SINGLE_CAPTURE_ENFORCED"`). |
86
+
87
+ ### The `observed` Block
88
+
89
+ All fields in `observed` must already be resolved in the execution context at t-1.
90
+
91
+ **Allowed:** `str`, `int`, `bool`.
92
+ **Forbidden:** `float`, `None`, nested `dict`/`list`, any value fetched or inferred after execution.
93
+
94
+ ### The `availability` Block
95
+
96
+ `availability` is not data. It is a **declaration of epistemic visibility** at t-1.
97
+
98
+ Each key answers one question:
99
+ > "At the exact moment before the irreversible action, was this fact already knowable inside the execution context — yes or no?"
100
+
101
+ | `_known` value | Meaning | Effect |
102
+ |---|---|---|
103
+ | `True` | Fact was knowable at t-1 | May appear in `observed` |
104
+ | `False` | Fact was NOT knowable at t-1 | Must NOT appear in `observed` |
105
+
106
+ If the fact was not knowable, Manithy records **ignorance**, not a value.
107
+ That ignorance is structural and permanent.
108
+
109
+ ### Forbidden Fields
110
+
111
+ The following fields must **never** appear in a CommitBoundaryEvent:
112
+
113
+ | Field | Why Forbidden |
114
+ |---|---|
115
+ | `producer_invocation_id` | High joinability risk. Single-capture is enforced via guard state, not IDs. |
116
+ | `callsite_id` | High joinability risk. |
117
+ | `producer_build_id` | Belongs in PackInit / EvidencePack provenance, not J01. |
118
+
119
+ ## Custom Buffer
120
+
121
+ Route proofs to a file, queue, or any destination by subclassing `CaptureBuffer`:
122
+
123
+ ```python
124
+ import json
125
+ from manithy import ManithySDK
126
+ from manithy.interfaces.buffer import CaptureBuffer
127
+
128
+ class FileBuffer(CaptureBuffer):
129
+ def __init__(self, path: str):
130
+ self._file = open(path, "a", encoding="utf-8")
131
+
132
+ def emit(self, envelope: dict) -> None:
133
+ self._file.write(json.dumps(envelope, separators=(",", ":")) + "\n")
134
+ self._file.flush()
135
+
136
+ sdk = ManithySDK(buffer=FileBuffer("audit.log"))
137
+ ```
138
+
139
+ ## Configuration
140
+
141
+ ### Kill-Switch
142
+
143
+ Disable all capture at runtime without code changes:
144
+
145
+ ```bash
146
+ export MANITHY_ENABLED=false # Linux/macOS
147
+ ```
148
+
149
+ ```powershell
150
+ $env:MANITHY_ENABLED = "false" # Windows PowerShell
151
+ ```
152
+
153
+ When disabled, `capture()` returns `{"status": "SKIPPED"}` immediately.
154
+
155
+ ### Debug Mode
156
+
157
+ Log internal SDK errors to stderr (useful during development):
158
+
159
+ ```bash
160
+ export MANITHY_DEBUG=true
161
+ ```
162
+
163
+ ## How It Works
164
+
165
+ 1. **Kill-switch check** — reads `MANITHY_ENABLED`. If `"false"`, returns `SKIPPED`.
166
+ 2. **Validation** — enforces type constraints on all fields; rejects forbidden fields, floats in `observed`, non-bool in `availability`, and unknown facts that leak into `observed`.
167
+ 3. **Event assembly** — builds a J01 `CommitBoundaryEvent` with schema `manithy.commit_boundary_event.v1`.
168
+ 4. **Hashing** — canonicalizes the event (sorted keys, no whitespace, floats like `100.0` → `100`) and computes SHA-256 → 64-char hex `commit_id`.
169
+ 5. **Emit** — writes the event to the configured buffer (default: stdout with `MANITHY_PROOF::` prefix).
170
+
171
+ If any step fails, the error is swallowed and `{"status": "ERROR", "error": "Internal SDK Error"}` is returned. The host application is **never** affected.
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ # Create a virtual environment
177
+ python -m venv .venv
178
+
179
+ # Activate it
180
+ source .venv/bin/activate # Linux/macOS
181
+ .venv\Scripts\Activate.ps1 # Windows PowerShell
182
+
183
+ # Install in editable mode with dev dependencies
184
+ pip install -e ".[dev]"
185
+
186
+ # Run tests
187
+ pytest tests/ -v
188
+ ```
189
+
190
+ ## Project Structure
191
+
192
+ ```
193
+ src/manithy/
194
+ ├── __init__.py # Public API: exposes ManithySDK
195
+ ├── sdk.py # Main entry point (capture pipeline + fail-closed)
196
+ ├── config.py # Environment variable loader (kill-switch + debug)
197
+ ├── core/
198
+ │ ├── canonical.py # Deterministic JSON canonicalization
199
+ │ ├── hasher.py # SHA-256 commit-ID generation
200
+ │ └── envelope.py # J01 CommitBoundaryEvent assembly + validation
201
+ └── interfaces/
202
+ └── buffer.py # Abstract CaptureBuffer + StdoutBuffer
203
+ tests/
204
+ ├── vectors.json # Golden test vectors (canonical + hash)
205
+ ├── test_core.py # Core module tests (canonical, hasher, J01 event)
206
+ └── test_sdk.py # SDK integration tests (capture, kill-switch, fail-closed)
207
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "manithy-sdk"
7
+ version = "0.1.0"
8
+ description = "Manithy SDK — Authority-Grade Audit Capture (Zero Dependencies)"
9
+ readme = "README.md"
10
+ license = { text = "Proprietary" }
11
+ requires-python = ">=3.8"
12
+
13
+ # ─────────────────────────────────────────────────────────────────────
14
+ # CRITICAL CONSTRAINT: Zero External Dependencies
15
+ # ─────────────────────────────────────────────────────────────────────
16
+ # The Manithy SDK must NEVER add third-party runtime dependencies.
17
+ # Every capability (hashing, JSON handling, I/O) must rely exclusively
18
+ # on the Python standard library. This guarantees:
19
+ # 1. No supply-chain attack surface.
20
+ # 2. No version-conflict issues in host applications.
21
+ # 3. Deterministic, reproducible builds.
22
+ # ─────────────────────────────────────────────────────────────────────
23
+ dependencies = []
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=7.0",
28
+ "pytest-cov>=4.0",
29
+ ]
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ """
2
+ Manithy SDK — Authority-Grade Audit Capture
3
+ ============================================
4
+
5
+ Public API surface:
6
+ from manithy import ManithySDK
7
+
8
+ Constraints:
9
+ - Zero Network I/O → The SDK never opens sockets or makes HTTP calls.
10
+ - Determinism → Identical inputs always yield identical outputs.
11
+ - Fail-Closed → Any internal error is silently swallowed; the host
12
+ application must never crash because of the SDK.
13
+ """
14
+
15
+ from manithy.sdk import ManithySDK # noqa: F401
16
+
17
+ __all__ = ["ManithySDK"]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ def is_enabled() -> bool:
7
+ """Return whether the Manithy SDK is enabled.
8
+
9
+ Reads the ``MANITHY_ENABLED`` environment variable via
10
+ ``os.environ``.
11
+
12
+ * If the variable is **absent** → return ``True`` (enabled by
13
+ default).
14
+ * If the variable is set to ``"false"`` (case-insensitive) →
15
+ return ``False``.
16
+ * Any other value → return ``True``.
17
+
18
+ Returns
19
+ -------
20
+ bool
21
+ ``True`` if capture should proceed, ``False`` otherwise.
22
+ """
23
+ raw = os.environ.get("MANITHY_ENABLED", "true")
24
+ return raw.strip().lower() != "false"
25
+
26
+
27
+ def is_debug() -> bool:
28
+ """Return whether debug mode is enabled.
29
+
30
+ Reads the ``MANITHY_DEBUG`` environment variable.
31
+
32
+ * If the variable is set to ``"true"`` (case-insensitive) →
33
+ return ``True``.
34
+ * Any other value (or absence) → return ``False``.
35
+
36
+ Returns
37
+ -------
38
+ bool
39
+ ``True`` if debug logging should be active, ``False`` otherwise.
40
+ """
41
+ raw = os.environ.get("MANITHY_DEBUG", "false")
42
+ return raw.strip().lower() == "true"
@@ -0,0 +1,10 @@
1
+ """
2
+ manithy.core
3
+ ~~~~~~~~~~~~
4
+
5
+ Core deterministic building blocks. Every function in this package is
6
+ pure (no side-effects, no I/O) and must be fully deterministic:
7
+ • canonical - JSON canonicalization
8
+ • hasher - SHA-256 commit-ID generation
9
+ • envelope - J01 CommitBoundaryEvent assembly
10
+ """
@@ -0,0 +1,99 @@
1
+ """
2
+ manithy.core.canonical
3
+ ~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Deterministic JSON Canonicalization.
6
+
7
+ This module converts an arbitrary Python data structure (dicts, lists,
8
+ primitives) into a **canonical byte string** that is identical across
9
+ Python and Node.js for the same logical input.
10
+
11
+ Owner: [Dev A]
12
+
13
+ Cross-Language Determinism Rules
14
+ --------------------------------
15
+ 1. **Sort object keys** — recursively, at every nesting level, using
16
+ standard lexicographic (Unicode code-point) order.
17
+
18
+ 2. **Strip insignificant whitespace** — the output must contain no
19
+ spaces or newlines between tokens (equivalent to
20
+ ``json.dumps(separators=(',', ':'))``).
21
+
22
+ 3. **Normalize numbers** —
23
+ * Floats that are mathematically integers (e.g. ``100.0``, ``3.0``)
24
+ **must** be emitted as integers (``100``, ``3``) so the byte output
25
+ matches what ``JSON.stringify`` produces in Node.js.
26
+ * Non-integer floats (e.g. ``3.14``) should be serialized with full
27
+ precision, avoiding trailing zeros.
28
+
29
+ 4. **Encoding** — the final output must be UTF-8 encoded ``bytes``.
30
+
31
+ 5. **No external libraries** — only ``json`` from the standard library.
32
+
33
+ Example
34
+ -------
35
+ >>> to_canonical_bytes({"b": 1, "a": [3.0, {"d": 4, "c": 5}]})
36
+ b'{"a":[3,{"c":5,"d":4}],"b":1}'
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ from typing import Any
43
+
44
+
45
+ def _normalize(data: Any, _seen: set) -> Any:
46
+ """Recursively normalize *data* for deterministic serialization.
47
+
48
+ - Sorts dict keys lexicographically.
49
+ - Converts whole-number floats to ints (the "float trap").
50
+ - Detects circular references via object-id tracking.
51
+ """
52
+ obj_id = id(data)
53
+ if isinstance(data, (dict, list)):
54
+ if obj_id in _seen:
55
+ raise ValueError("Circular reference detected")
56
+ _seen.add(obj_id)
57
+
58
+ if isinstance(data, dict):
59
+ result = {k: _normalize(v, _seen) for k, v in sorted(data.items())}
60
+ _seen.discard(obj_id)
61
+ return result
62
+
63
+ if isinstance(data, list):
64
+ result = [_normalize(item, _seen) for item in data]
65
+ _seen.discard(obj_id)
66
+ return result
67
+
68
+ if isinstance(data, float):
69
+ if data.is_integer():
70
+ return int(data)
71
+ return data
72
+
73
+ return data
74
+
75
+
76
+ def to_canonical_bytes(data: Any) -> bytes:
77
+ """Convert *data* to deterministic, canonical UTF-8 JSON bytes.
78
+
79
+ Parameters
80
+ ----------
81
+ data : Any
82
+ Arbitrary JSON-serializable Python object (dict, list, str,
83
+ int, float, bool, None).
84
+
85
+ Returns
86
+ -------
87
+ bytes
88
+ UTF-8 encoded canonical JSON with sorted keys, no whitespace,
89
+ and floats-that-are-integers coerced to ints.
90
+
91
+ Raises
92
+ ------
93
+ TypeError
94
+ If *data* contains types that are not JSON-serializable.
95
+ ValueError
96
+ If *data* contains circular references.
97
+ """
98
+ normalized = _normalize(data, set())
99
+ return json.dumps(normalized, separators=(",", ":"), sort_keys=False).encode("utf-8")