diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..95d92d7 Binary files /dev/null and b/.DS_Store differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f498e..e1c9512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,64 @@ # Changelog The format follows [Keep a Changelog](https://keepachangelog.com/1.1.0/) and this project -adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with the caveat -that the public API is not frozen until `1.0.0`. Pin an exact version. +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). As of `1.0.0` the +public API, the request and result schemas, the numeric and units policy, the validation +rules and the compatibility policy are frozen: breaking any of them costs a major version. + +## [1.0.0] + +The stable core release. It freezes the contract that already exists rather than adding a +feature wave: the public API, the request and result schemas, the numeric and units policy, +the validation rules and the compatibility policy will not break without a major version. + +### Read this before depending on 1.0.0 + +- **Platforms exercised for this release: Linux x86_64, Linux aarch64 and macOS arm64.** + Python wheels are built and installed into a clean Python 3.9 on all three; the PHP FFI + bridge is verified against the real shared library on PHP 8.2, 8.3, 8.4 and 8.5 on both + Linux architectures. **Windows and macOS x86_64 native binaries are not built and not + published.** Without a native binary the pure Python and PHP engines run unchanged — that + is the documented default, and it costs speed rather than correctness. +- **Release artifacts carry no build-provenance attestation.** `gh attestation verify` will + not succeed against 1.0.0. The SBOM and SHA-256 manifests are the integrity evidence for + this release; check them before installing from anywhere other than the official + registry. + +### Added + +- **A sound lower bound on the objective, in every engine.** Computed from the request + alone before a search begins, and identical across the Python, PHP, Rust and JavaScript + implementations on 381 corpus cases. It is not a result field: reporting an optimality + gap would widen the contract this release exists to freeze. +- **A declared numeric ceiling shared by all four engines.** A sum too large to stay exact + returns a structured refusal instead of a number that one language would round and + another would not. The limit is stated by the library rather than inherited from each + language, so the four agree about which requests are answerable. +- **A published coverage frontier for optimality claims.** Where the bound is actually + attained is measured and committed, so `optimal` is bounded by evidence rather than used + as a label. + +### Fixed + +- **A bound could be returned that JavaScript cannot represent exactly.** Values above + `2^53-1` are now refused rather than silently rounded in one engine and exact in the + others. +- **The prebuilt Node addon had no quality floor.** It is now held to a per-fixture budget + like the other independent engines. + +### Not claimed + +- **Identical placements across engines.** Different engines may return different, equally + valid arrangements. This is measured and budgeted, not accidental — and it includes the + JavaScript fallback and the prebuilt native addon, which differ on 18 of 397 corpus + fixtures. +- **Optimal packings for arbitrary requests.** 3D packing remains NP-hard; the bound says + what is provable, not that every answer is optimal. +- **Fastest engine.** Measured against other libraries on identical hardware, that claim is + false on latency, and no Packvium surface makes it. On the report's separate two-axis + time-and-peak-memory frontier, `packvium-rust` is Pareto-optimal in 6 of 9 profiles and + the only engine of ten never dominated. The latency result and the trade-off result are + reported together; neither is a general speed-leadership claim. ## [0.1.3] diff --git a/README.md b/README.md index 168145e..edceb0b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Optional native backend selector for [Packvium](https://pypi.org/project/packviu It uses the compiled Rust wheel when available and otherwise delegates to the pure Python package. +Full documentation, the constraint reference and benchmarks live at +[packvium.com](https://packvium.com). + ## Install ```bash @@ -42,6 +45,15 @@ print(result["status"]) # "feasible" The adapter does not replace the `packvium` import. It is useful when an application wants one stable call site with a native fast path and a pure-Python fallback. +`pack_json` takes and returns the same JSON string every other Packvium binding speaks — +useful when the request is already serialized, or being forwarded as-is: + +```python +from packvium_native import pack_json + +response = pack_json('{"items":[...],"containers":[...]}') +``` + ## Examples Runnable, in [`examples/`](examples). Each one is a single file you can read top to bottom @@ -74,7 +86,7 @@ Documentation, the constraint reference and the benchmarks are at | --- | --- | --- | | Python — [`packvium`](https://pypi.org/project/packvium/) | `pip install packvium` | [packvium-python](https://github.com/toxakara/packvium-python) | | PHP — [`packvium/packvium`](https://packagist.org/packages/packvium/packvium) | `composer require packvium/packvium` | [packvium-php](https://github.com/toxakara/packvium-php) | -| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "0.1"` | [packvium-rust](https://github.com/toxakara/packvium-rust) | +| Rust — [`packvium`](https://crates.io/crates/packvium) | `packvium = "1.0"` | [packvium-rust](https://github.com/toxakara/packvium-rust) | | Node.js — [`@packvium/engine`](https://www.npmjs.com/package/@packvium/engine) | `npm install @packvium/engine` | [packvium-node](https://github.com/toxakara/packvium-node) | | Browser / WebAssembly — [`@packvium/browser`](https://www.npmjs.com/package/@packvium/browser) | `npm install @packvium/browser` | [packvium-wasm](https://github.com/toxakara/packvium-wasm) | | PHP FFI bridge — [`packvium/native-bridge`](https://packagist.org/packages/packvium/native-bridge) | `composer require packvium/native-bridge` | [packvium-php-bridge](https://github.com/toxakara/packvium-php-bridge) | diff --git a/SECURITY.md b/SECURITY.md index 658f11c..dff5f2f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,8 +2,8 @@ ## Supported versions -Only the latest `0.1.x` release receives fixes. This is an early release; there is no -long-term support branch yet. +Only the latest `1.x` release receives fixes. The `0.1.x` line is superseded by `1.0.0` +and receives none. There is no long-term support branch for older majors. ## Reporting a vulnerability diff --git a/docs/GUARANTEES.md b/docs/GUARANTEES.md index af5efb3..3d2f188 100644 --- a/docs/GUARANTEES.md +++ b/docs/GUARANTEES.md @@ -48,9 +48,16 @@ silently — if you need them, they belong in your own layer above this library. ## Status of this release -Version `0.1.3` is an early release. The public API is not yet frozen: field names, -status codes and the objective vector may change before `1.0.0`. Pin an exact version. - -The algorithm complexities documented in `ALGORITHMS-AND-COMPLEXITY.md` are design -intent that has not yet been confirmed by profiling. Treat them as guidance for choosing -a solver profile, not as a performance contract. +Version `1.0.0` freezes the public API. Field names, status codes, the objective +vector, the numeric policy and the validation rules do not change without a major +version, so any `1.x` is a safe upgrade from any earlier `1.x`. A caret or tilde +constraint on `1.0` is enough; an exact pin is no longer required. + +What the freeze does not cover: which of several equally valid packings a solver +returns. That is bounded by the objective vector, not by the placement list, and a minor +release may return a different arrangement with the same or a better score. + +The algorithm complexities documented in `ALGORITHMS-AND-COMPLEXITY.md` are asymptotic +design bounds held by review, not per-release measurements. They are not a wall-clock +performance contract either: constants, input shape and host all move the real number. +Use them to choose a solver profile, not to predict a duration. diff --git a/docs/PUBLIC-API.md b/docs/PUBLIC-API.md index be693ef..5b6e831 100644 --- a/docs/PUBLIC-API.md +++ b/docs/PUBLIC-API.md @@ -3,6 +3,32 @@ ## Core inputs - `Item`: id, dimensions, weight, quantity, rotations, upright/floor/stacking rules, top-load limit, support ratio, group, tags, metadata, and an optional `nesting_height` (how much this item sinks into an identical one beneath it when stacked). Only the same item type with the exact same footprint may nest; its adjacent predecessor is one full-footprint direct supporter for support ratio, ground-contact, stack/load and route rules, while non-adjacent same-column face coincidences are shadowed. An optional, exact non-negative integer `value` (no unit or currency — the caller's own economic scale) means nothing to placement or to any other objective; only the `maximum_value` objective reads it. +- **Item geometry beyond the box.** `shape_type` is an enum — + `rigid_cuboid` (the default), `convex_hull`, `compressible` — accompanied by + `hull_vertices`, `compression_ratio` and `max_compression_pressure_kpa`. **A request that + omits `shape_type` is unaffected**, byte for byte: the default is `rigid_cuboid`, the whole + existing golden corpus is unchanged, and writing `"shape_type": "rigid_cuboid"` explicitly + is served rather than refused. + + The two other values are rolling out one engine at a time, which is what the fields were + reserved before the freeze for, and the rollout is finished: **all four engines implement + both**, and are held to byte-identical results on the shared fixtures. The one + recorded difference is the JavaScript fallback on a scene whose answer depends on item + ordering -- it makes a single ordering pass where the other three run a portfolio, so it can + pack validly but less densely; that is pinned in `conformance/native-quality-budget.json` + rather than left to drift. + + Where they are implemented: a `convex_hull` item's collisions are decided by an exact + integer separating-axis test rather than by its box, and its occupied volume is the hull's + own — two complementary wedges share a crate that fits one of their bounding boxes. A + `compressible` item loses height linearly with the pressure resting on it, rounded up, and + a load above `max_compression_pressure_kpa` is refused as `crush_violation` rather than + packed. Three situations deliberately fall back to the bounding box, always over-reserving + space: a configured `clearance`, an item on a route (`stop_index`), and the uniform-lattice + fast path. Hull coordinates are non-negative offsets from the corner of the item's own + bounding box and are capped at 10^8 length ticks -- 6.25 m, beyond ordinary parcel sizes, and the + bound that keeps the exact geometry inside a 64-bit integer everywhere it can be. + IRREGULAR-ITEMS.md is the model and states why. - `Container`: id, inner/outer dimensions, tare/payload, cost, inventory quantity, obstacles (each a union of one or more exact boxes — `additional_boxes` approximates a non-rectangular zone such as a wheel arch or tapered roof without diff --git a/docs/UNITS-AND-NUMERICS.md b/docs/UNITS-AND-NUMERICS.md index a56d23e..9b1aa73 100644 --- a/docs/UNITS-AND-NUMERICS.md +++ b/docs/UNITS-AND-NUMERICS.md @@ -42,6 +42,33 @@ The JavaScript fallback also performs load distribution in `BigInt`: every when the intermediate product exceeds `Number` precision. The final tick count is converted back to a number only at the existing JSON boundary. +## The bound path's declared ceiling + +`docs/OPTIMALITY-CERTIFICATES.md` defines lower bounds on the objective. Their arithmetic is +integer-only, and the limit at which they refuse is **declared rather than inherited from the +language**: every sum in that path must stay below `10^30`, and exceeding it is a structured +refusal in all four engines rather than a number. + +The reason is the same one this document already gives for coordinates, taken one step +further. Python's integers are unbounded, PHP's silently become doubles on overflow, +JavaScript's `Number` stops being exact past `2^53`, and Rust's `i128` wraps. Four engines +refusing at four native limits would disagree about which requests are answerable at all -- +a caller would get a number from one and a refusal from another for the same input. So each +carries the guarded sums in a representation that holds `10^30` exactly: Python's `int`, +PHP's `BigInt` decimal strings, JavaScript's `BigInt`, Rust's `i128`. + +That intermediate ceiling is deliberately not the result ceiling. The five bound keys can +cross JSON and are returned as JavaScript `Number` values, so every engine also refuses a +final key above **`2^53 - 1` (`9,007,199,254,740,991`)**. This is the largest integer all four +bindings can return without changing its value. In particular, an unlimited inventory does +not make an expensive container count as one during validation: the selected opening costs +are summed exactly, checked against `10^30`, then checked against the portable result ceiling +before PHP or JavaScript converts them to a native integer. + +The value comes from the widest intermediate the formulas form -- a summed volume times +`10^6` -- which puts the largest product at `10^36`, about 170-fold inside an `i128`. It never +binds on a real request: `10^30` cubic ticks is 244 million cubic metres. + ## Decimal rendering `value` fields (e.g. `Length.decimal()`, `Weight.decimal()`, PHP's `RationalParser::decimalString()`) render an exact `ticks / divisor` rational to a fixed number of digits (8 by default). All four engines round the truncated remainder **ties-to-even**, matching Python's `Decimal.quantize` under its default context — the last kept digit rounds up when the discarded remainder is more than half the divisor, stays put when it's less, and on an exact half rounds to whichever choice makes that digit even. diff --git a/pyproject.toml b/pyproject.toml index 401e13d..d3a5de8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "packvium-native" -version = "0.1.3" +version = "1.0.0" requires-python = ">=3.9" description = "Backend selector for Packvium's native and pure Python engines" readme = "README.md" diff --git a/src/packvium_native/__init__.py b/src/packvium_native/__init__.py index dc38bf6..37c6a11 100644 --- a/src/packvium_native/__init__.py +++ b/src/packvium_native/__init__.py @@ -3,7 +3,7 @@ import json from typing import Any -__version__ = "0.1.3" +__version__ = "1.0.0" def backend() -> str: