Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
60 changes: 58 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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]

Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) |
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 13 additions & 6 deletions docs/GUARANTEES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
26 changes: 26 additions & 0 deletions docs/PUBLIC-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions docs/UNITS-AND-NUMERICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/packvium_native/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from typing import Any

__version__ = "0.1.3"
__version__ = "1.0.0"


def backend() -> str:
Expand Down
Loading