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 removed .DS_Store
Binary file not shown.
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@ The format follows [Keep a Changelog](https://keepachangelog.com/1.1.0/) and thi
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.

## [0.1.1]

A patch over `0.1.0`. Every package is released together at the new version, including
the ones `0.1.0` did not break, so that one version number still describes one tested
set.

### Fixed

- **`lowest_landed_cost` could choose a container its own rate card cannot price.** When
one container billed lighter than another but its rate table ran out before the
shipment's billed weight, the search preferred it — returning the one packing you
cannot actually buy over a priced alternative. Every engine now compares candidates by
the money the rate table charges rather than by billed weight, which also fixes the
case this objective exists for: a bracket step or a minimum charge can make the
cheaper shipment the heavier one. If no container on offer can price the load, the
request is refused with a message naming the container, its billed weight and the last
bracket, in all four languages — previously two of them returned a result carrying a
sentinel cost, and two aborted requests that had a shippable answer. `RateTable` gains
a non-throwing `charge_minor_or_none` / `chargeMinorOrNull`; the throwing form is
unchanged. No request or result field changed.

- **`@packvium/engine@0.1.0` could not be imported.** The published tarball was missing
a runtime module that the fallback engine imports, so the first `import` of the package
threw `ERR_MODULE_NOT_FOUND`. npm versions are immutable, which is why the fix has to
arrive as a new version rather than a re-upload. Package assembly now dry-packs the
tarball and resolves every relative import in the real published inventory, so a
missing runtime file fails the release build instead of the consumer's first import.
Only the Node package was affected; the Python, PHP and Rust `0.1.0` releases install
and run correctly.

### Added

- **Commercial and control-plane API.** Three deterministic functions over one canonical
JSON document: `quote` returns a landed cost together with the tariff version that
produced it, `evaluate_policy` returns an eligibility decision together with the rule id
and version that decided it, and `catalog_version_info` returns the metadata of one
pinned catalog version. Exported as `packvium.commerce` (Python), `Packvium\Commerce\`
(PHP), `packvium_core::commerce` (Rust, plus three C ABI entry points) and `commerce`
on `@packvium/engine` and `@packvium/browser`. Prices are exact integers in minor
currency units and every inexact division rounds up, so a quote is reproducible rather
than approximately equal. No packing-request or packing-result field changed. Each
package ships a runnable `commerce` example; the contract is in `docs/COMMERCE-API.md`.

## [0.1.0]

First release.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ 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.

## Examples

Runnable, in [`examples/`](examples). Each one is a single file you can read top to bottom
and execute without a project around it.

| File | What it shows |
| --- | --- |
| [`basic.py`](examples/basic.py) | Pack through the adapter and report which backend answered. |

```bash
python3 examples/basic.py
```

## Platforms

Native wheels support Linux x86_64/aarch64, macOS Apple Silicon/Intel and Windows. The
Expand Down
2 changes: 1 addition & 1 deletion docs/GUARANTEES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ silently — if you need them, they belong in your own layer above this library.

## Status of this release

Version `0.1.0` is an early release. The public API is not yet frozen: field names,
Version `0.1.1` 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
Expand Down
32 changes: 32 additions & 0 deletions docs/PUBLIC-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,38 @@ details. A structural bound takes precedence over a deadline: an oversized item
`proven` even if the overall run timed out. Conversely, `time_limit`, `search_exhausted`
and other unfinished-search outcomes can never carry `proven`.

## Commercial and control-plane API

Three deterministic functions over one canonical JSON document -- a carrier quote, an
eligibility decision, and catalog version metadata. They are a separate surface from the
packing API and add no packing-request or packing-result field; the full contract, with
the document format, every result shape, the closed set of rejection codes, complexity
and limitations, is COMMERCE-API.md.

| Language | Entry point |
| --- | --- |
| Python | `packvium.commerce.quote(document, request)`, `.evaluate_policy(...)`, `.catalog_version_info(...)`, `.canonical_json(result)` |
| PHP | `Packvium\Commerce\quote(array $document, array $request)`, `evaluatePolicy(...)`, `catalogVersionInfo(...)`, `canonicalJson(...)` |
| Rust | `packvium_core::commerce::quote_json(&str)`, `evaluate_policy_json(&str)`, `catalog_version_info_json(&str)` |
| JavaScript | `commerce.quote(document, request)`, `.evaluatePolicy(...)`, `.catalogVersionInfo(...)` on `@packvium/engine`; the same three, async, on `@packvium/browser` |
| C ABI | `packvium_commerce_quote(call)`, `packvium_commerce_evaluate_policy(call)`, `packvium_commerce_catalog_version_info(call)` |

The Rust, C ABI and WASM entry points take one JSON string, `{"document": ..., "request":
...}`, and return the result document as a string. Each C ABI function follows the same
pointer contract as `packvium_solve_json`: a valid, immutable, NUL-terminated UTF-8
input, and an owned result string the caller releases exactly once with
`packvium_free_string`. `@packvium/engine` selects the native addon when it is installed
and the deterministic JavaScript implementation otherwise, the same way `pack` does;
`commerce.backend()` reports which answered.

Two kinds of failure, and they are not interchangeable. A malformed document or request
is a caller bug and is raised the way each language raises one (`CommerceInputError`,
`Packvium\Commerce\CommerceInputException`, `Err(CommerceInputError)`, a thrown
`CommerceInputError`). A well-formed request the commercial model cannot answer -- no
tariff effective at that instant, no rate for that zone -- is a successful call returning
`"status": "rejected"` with a code from a closed set, exactly as an infeasible packing
request returns a result with a status rather than raising.

## JSON API

Python, PHP, Rust and the JavaScript fallback accept the same top-level keys: `units`,
Expand Down
79 changes: 79 additions & 0 deletions examples/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Pack through the adapter, whichever backend happens to be installed.

Run it:

python3 examples/basic.py

This package is a thin selector, not an engine. It tries the compiled Rust wheel
(`packvium-native-rust`) first and uses the pure-Python `packvium` package when that
wheel is not installed for your platform. Both answer the same shared JSON contract, so
your code does not branch on which one is present -- that is the whole point of the
adapter.

`backend()` tells you which one answered, which is worth logging once at startup: it is
the difference between "we are running the compiled engine" and "we quietly fell back",
and you want to find that out from a log line rather than from a latency graph.
"""

from packvium_native import __version__, backend, pack

print(f"adapter {__version__} using the {backend()} backend\n")

request = {
"items": [
# Lengths and weights are strings on purpose. They are parsed into exact
# integers, so "0.1" means a tenth of a millimetre and never
# 0.09999999999999999. Plain integers and fractions like "3/16" work too.
{
"id": "mug",
"quantity": 6,
"dimensions": {"length": "120", "width": "120", "height": "100"},
"weight": "400 g",
},
{
"id": "plate",
"quantity": 8,
"dimensions": {"length": "260", "width": "260", "height": "20"},
"weight": "600 g",
},
# Too long for the box in every orientation, so it cannot be placed.
{
"id": "ladder",
"quantity": 1,
"dimensions": {"length": "1800", "width": "300", "height": "100"},
"weight": "6 kg",
},
],
"containers": [
{
"id": "box",
"inner_dimensions": {"length": "400", "width": "400", "height": "400"},
"max_payload": "15 kg",
"cost_minor": 180,
}
],
}

result = pack(request)

print(f"status: {result['status']}")
print(f"containers opened: {len(result['containers'])}")

for index, container in enumerate(result["containers"], start=1):
placements = container["placements"]
print(f"\nbox #{index}: {len(placements)} placement(s)")
for placement in placements:
# Every measurement arrives as {"ticks", "value", "unit"}: `ticks` is the exact
# integer the engine reasoned about, `value` is that number written for a human.
position = placement["position"]
print(
f" {placement['item_type']:8s} at "
f"({position['x']['value']}, {position['y']['value']}, {position['z']['value']}) "
f"{position['x']['unit']} orientation {placement['orientation']}"
)

# A refusal is an answer, not an error.
if result["unpacked_items"]:
print("\nnot packed:")
for unpacked in result["unpacked_items"]:
print(f" {unpacked['item_id']:10s} {unpacked['reason']}")
7 changes: 5 additions & 2 deletions 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.0"
version = "0.1.1"
requires-python = ">=3.9"
description = "Backend selector for Packvium's native and pure Python engines"
license = "MIT"
Expand All @@ -14,7 +14,10 @@ dependencies = []
# `packvium` is deliberately not a hard dependency of the package itself (see
# README's "does not replace or shadow" note) -- it is only needed to exercise the
# fallback path in tests.
test = ["pytest", "packvium"]
# The pure engine itself supports Python 3.10+, while this selector remains useful on
# Python 3.9 with the native wheel. Keep the public 3.9 CI lane installable instead of
# asking pip to satisfy an impossible test-only dependency there.
test = ["pytest", "packvium; python_version >= '3.10'"]
# Convenience extra: `pip install packvium-native[native]` pulls in the compiled Rust
# wheel too, rather than requiring a second, separate install command.
native = ["packvium-native-rust"]
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.0"
__version__ = "0.1.1"


def backend() -> str:
Expand Down
Loading