@jetta/axle-load-calculator 0.3.1 → 0.3.3

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,71 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@jetta/axle-load-calculator` are documented in this file.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - Cargo collision check after axle loads are written (`CollisionChecker`), using `lt` from `@jetta/number-compare` (default epsilon `1e-9`).
12
+ - Public `ItemOverlapError` thrown by `calculate_weight_on_axles_physics` when two allocated items share volume.
13
+
14
+ ### Fixed
15
+
16
+ - Face-touching boxes (IEEE ULP on edges such as `4.075 + 0.815` vs `4.89`) are no longer treated as overlap, so callers can keep axle kg instead of skipping the gate.
17
+
18
+ ## [0.3.2] - 2026-07-23
19
+
20
+ ### Fixed
21
+
22
+ - Same-grouper container loads are merged for mass-center so results match MS.
23
+
24
+ ## [0.3.0] - 2026-06-02
25
+
26
+ ### Changed
27
+
28
+ - Public API accepts objects only, returns objects, and throws domain errors instead of mixed string/object I/O.
29
+
30
+ ### Removed
31
+
32
+ - Collision test artifacts and unused overlap error after `CollisionChecker` removal.
33
+
34
+ ### Added
35
+
36
+ - npm coverage script with 80% line, function, branch, and statement thresholds.
37
+
38
+ ## [0.2.1] - 2026-06-02
39
+
40
+ ### Removed
41
+
42
+ - `CollisionChecker` and its call from the weight-by-axis task.
43
+
44
+ ## [0.2.0] - 2026-06-02
45
+
46
+ ### Added
47
+
48
+ - First published library surface: `calculate_weight_on_axles_physics`, domain exceptions, Zod request validation, and in-process physics plus orchestration.
49
+
50
+ ### Changed
51
+
52
+ - Package entry points use lowercase `index`.
53
+ - Weight calculation accepted both JSON strings and objects (superseded in 0.3.0).
54
+
55
+ ## [0.1.3] - 2026-06-02
56
+
57
+ ### Changed
58
+
59
+ - Package is public (`private: false`) for npm publish.
60
+
61
+ ## [0.1.2] - 2026-06-02
62
+
63
+ ### Added
64
+
65
+ - Bitbucket Pipelines for CI/CD.
66
+
67
+ ## [0.1.0] - 2026-06-02
68
+
69
+ ### Added
70
+
71
+ - Project scaffolding, transport and vehicle models, deflection and tare, axle-weight services, weight-by-axis orchestration, and golden deflection fixtures.
package/README.md CHANGED
@@ -55,6 +55,7 @@ try {
55
55
  | `UnsupportedVehicleTypeError` | Unknown `tipoVeiculo` |
56
56
  | `UnrecognizedSuspensionTypeError` | Invalid vanderleia suspension type |
57
57
  | `GrouperNotFoundError` | Merged-container grouper not found |
58
+ | `ItemOverlapError` | Two allocated items occupy the same volume |
58
59
 
59
60
  Import `ZodError` directly from [`zod`](https://github.com/colinhacks/zod).
60
61
 
@@ -72,6 +73,7 @@ Also exported from `@jetta/axle-load-calculator` for `instanceof` checks when us
72
73
  ## Dependencies
73
74
 
74
75
  - [zod](https://github.com/colinhacks/zod) — request validation at the public boundary
76
+ - [`@jetta/number-compare`](https://www.npmjs.com/package/@jetta/number-compare) — float-safe interval tests in cargo collision
75
77
 
76
78
  ## Scripts
77
79
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Rejects an env whose allocated items occupy the same volume.
3
+ * Face-touching boxes are allowed; comparison uses `@jetta/number-compare`.
4
+ * @param env - Env vehicle with containers and items.
5
+ * @throws {ItemOverlapError} When two items share volume on all three axes.
6
+ */
7
+ export declare function check_cargo_collision(env: Record<string, unknown>): void;
8
+ //# sourceMappingURL=CollisionChecker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CollisionChecker.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/CollisionChecker.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWxE"}
@@ -0,0 +1,60 @@
1
+ import { lt } from '@jetta/number-compare';
2
+ import { ItemOverlapError } from './ItemOverlapError.js';
3
+ /**
4
+ * Rejects an env whose allocated items occupy the same volume.
5
+ * Face-touching boxes are allowed; comparison uses `@jetta/number-compare`.
6
+ * @param env - Env vehicle with containers and items.
7
+ * @throws {ItemOverlapError} When two items share volume on all three axes.
8
+ */
9
+ export function check_cargo_collision(env) {
10
+ const containers = env.containers;
11
+ if (!containers) {
12
+ return;
13
+ }
14
+ for (const container of containers) {
15
+ check_container_items(container);
16
+ }
17
+ }
18
+ function check_container_items(container) {
19
+ const items = container.items;
20
+ if (!items || items.length <= 1) {
21
+ return;
22
+ }
23
+ throw_if_any_pair_overlaps(items);
24
+ }
25
+ function throw_if_any_pair_overlaps(items) {
26
+ for (let index = 0; index < items.length - 1; index++) {
27
+ for (let other = index + 1; other < items.length; other++) {
28
+ if (items_overlap(items[index], items[other])) {
29
+ throw new ItemOverlapError();
30
+ }
31
+ }
32
+ }
33
+ }
34
+ function items_overlap(left, right) {
35
+ const left_box = item_box(left);
36
+ const right_box = item_box(right);
37
+ return intervals_overlap(left_box.x, right_box.x)
38
+ && intervals_overlap(left_box.y, right_box.y)
39
+ && intervals_overlap(left_box.z, right_box.z);
40
+ }
41
+ /**
42
+ * Maps CM / Java item JSON onto axis intervals.
43
+ * Height lives on JSON `z`; length on JSON `y` + `comprimento`.
44
+ */
45
+ function item_box(item) {
46
+ const origin_x = Number(item.x ?? 0);
47
+ const origin_y = Number(item.y ?? 0);
48
+ const origin_z = Number(item.z ?? 0);
49
+ const width = Number(item.largura ?? item.width ?? 0);
50
+ const height = Number(item.altura ?? item.height ?? 0);
51
+ const length = Number(item.comprimento ?? item.length ?? 0);
52
+ return {
53
+ x: { min: origin_x, max: origin_x + width },
54
+ y: { min: origin_z, max: origin_z + height },
55
+ z: { min: origin_y, max: origin_y + length }
56
+ };
57
+ }
58
+ function intervals_overlap(left, right) {
59
+ return lt(left.min, right.max) && lt(right.min, left.max);
60
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Raised when two allocated items occupy the same volume.
3
+ */
4
+ export declare class ItemOverlapError extends Error {
5
+ constructor();
6
+ }
7
+ //# sourceMappingURL=ItemOverlapError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemOverlapError.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/ItemOverlapError.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;;CAO1C"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Raised when two allocated items occupy the same volume.
3
+ */
4
+ export class ItemOverlapError extends Error {
5
+ constructor() {
6
+ super('Item overlap error');
7
+ this.name = 'ItemOverlapError';
8
+ }
9
+ }
@@ -2,6 +2,7 @@ import { CenterOfMass } from '../physics/items/CenterOfMass.js';
2
2
  /**
3
3
  * Computes mass center per grouper using ms-faixas Java Item / MassCenter rules.
4
4
  * Longitudinal position uses JSON `y` (maps to Ponto3D.z in Java CubeRepresentation).
5
+ * Same-grouper containers are merged; empty necks do not shift later beds (MS parity).
5
6
  * @param env_vehicle - Single env vehicle object from weight_by_axis payload.
6
7
  * @returns Map of grouper id to center of mass.
7
8
  */
@@ -1 +1 @@
1
- {"version":3,"file":"JavaMassCenter.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/JavaMassCenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAE/D;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC3C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAe9B"}
1
+ {"version":3,"file":"JavaMassCenter.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/JavaMassCenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAE/D;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC3C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAQ9B"}
@@ -2,46 +2,99 @@ import { CenterOfMass } from '../physics/items/CenterOfMass.js';
2
2
  /**
3
3
  * Computes mass center per grouper using ms-faixas Java Item / MassCenter rules.
4
4
  * Longitudinal position uses JSON `y` (maps to Ponto3D.z in Java CubeRepresentation).
5
+ * Same-grouper containers are merged; empty necks do not shift later beds (MS parity).
5
6
  * @param env_vehicle - Single env vehicle object from weight_by_axis payload.
6
7
  * @returns Map of grouper id to center of mass.
7
8
  */
8
9
  export function mass_center_by_grouper_from_env(env_vehicle) {
9
- const result = {};
10
10
  const containers = env_vehicle.containers;
11
11
  if (!containers) {
12
- return result;
12
+ return {};
13
13
  }
14
+ return accumulate_grouper_mass_centers(sort_containers_by_sequence(containers));
15
+ }
16
+ /**
17
+ * @param containers - Containers sorted by sequencia.
18
+ * @returns Merged center of mass map keyed by grouper.
19
+ */
20
+ function accumulate_grouper_mass_centers(containers) {
21
+ const result = {};
22
+ const longitudinal_offset_by_grouper = {};
14
23
  for (const container of containers) {
15
- const grouper = Number(container.grouper);
16
- // Java AxleWeightConnector overwrites per container in loop order (last wins).
17
- result[grouper] = compute_container_mass_center(container);
24
+ absorb_container_into_grouper(result, longitudinal_offset_by_grouper, container);
18
25
  }
19
26
  return result;
20
27
  }
28
+ /**
29
+ * @param result - Accumulated grouper centers (mutated).
30
+ * @param offsets - Longitudinal offset per grouper (mutated).
31
+ * @param container - Env container to absorb.
32
+ */
33
+ function absorb_container_into_grouper(result, offsets, container) {
34
+ const grouper = Number(container.grouper);
35
+ const sequence_offset = offsets[grouper] ?? 0;
36
+ const container_center = compute_container_mass_center(container, sequence_offset);
37
+ result[grouper] = result[grouper]
38
+ ? merge_mass_centers(result[grouper], container_center)
39
+ : container_center;
40
+ if (container_center.mass > 0) {
41
+ offsets[grouper] = sequence_offset + Number(container.comprimento ?? 0);
42
+ }
43
+ }
44
+ /**
45
+ * @param containers - Env containers.
46
+ * @returns Containers sorted by sequencia ascending.
47
+ */
48
+ function sort_containers_by_sequence(containers) {
49
+ return [...containers].sort((left, right) => Number(left.sequencia) - Number(right.sequencia));
50
+ }
51
+ /**
52
+ * @param left - Existing grouper center of mass.
53
+ * @param right - Additional container center of mass.
54
+ * @returns Combined center of mass.
55
+ */
56
+ function merge_mass_centers(left, right) {
57
+ const merged = new CenterOfMass();
58
+ merged.mass = left.mass + right.mass;
59
+ if (merged.mass === 0) {
60
+ merged.z = 0;
61
+ return merged;
62
+ }
63
+ merged.z = (left.mass * left.z + right.mass * right.z) / merged.mass;
64
+ return merged;
65
+ }
21
66
  /**
22
67
  * @param container - Container object with items array.
68
+ * @param sequence_offset - Longitudinal offset from prior same-grouper containers.
23
69
  * @returns Center of mass for container items.
24
70
  */
25
- function compute_container_mass_center(container) {
71
+ function compute_container_mass_center(container, sequence_offset) {
26
72
  const center_of_mass = new CenterOfMass();
27
73
  const items = container.items;
28
74
  if (!items || items.length === 0) {
29
- center_of_mass.mass = 0;
30
- center_of_mass.z = 0;
31
75
  return center_of_mass;
32
76
  }
33
- const container_offset = read_container_longitudinal_offset(container);
77
+ const moments = accumulate_item_moments(items, sequence_offset, container);
78
+ center_of_mass.mass = moments.mass;
79
+ center_of_mass.z = moments.mass === 0 ? 0 : moments.accumulated / moments.mass;
80
+ return center_of_mass;
81
+ }
82
+ /**
83
+ * @param items - Container items.
84
+ * @param sequence_offset - Prior loaded length in grouper.
85
+ * @param container - Container for faixa offset.
86
+ * @returns Accumulated mass and first moment.
87
+ */
88
+ function accumulate_item_moments(items, sequence_offset, container) {
89
+ const container_offset = sequence_offset + read_container_longitudinal_offset(container);
34
90
  let accumulated = 0;
35
91
  let mass = 0;
36
92
  for (const item of items) {
37
93
  const weight = read_item_weight(item);
38
- const position = container_offset + read_item_longitudinal_position(item);
39
- accumulated += weight * position;
94
+ accumulated += weight * (container_offset + read_item_longitudinal_position(item));
40
95
  mass += weight;
41
96
  }
42
- center_of_mass.mass = mass;
43
- center_of_mass.z = mass === 0 ? 0 : accumulated / mass;
44
- return center_of_mass;
97
+ return { accumulated, mass };
45
98
  }
46
99
  /**
47
100
  * @param container - Container JSON.
@@ -12,6 +12,7 @@ import type { WeightByAxisSuccessOutput } from './WeightByAxisResponse.js';
12
12
  * @throws {import('../physics/transports/MissingContainerError.js').MissingContainerError} When a container lookup fails.
13
13
  * @throws {import('../physics/items/UnknownItemIdError.js').UnknownItemIdError} When an item template id is unknown.
14
14
  * @throws {import('../physics/models/deflection/DeflectionModelNotImplementedError.js').DeflectionModelNotImplementedError} When no deflection model exists.
15
+ * @throws {import('./ItemOverlapError.js').ItemOverlapError} When two allocated items occupy the same volume.
15
16
  */
16
17
  export declare function run_weight_by_axis_task(request_data: WeightByAxisRequestInput): WeightByAxisSuccessOutput;
17
18
  //# sourceMappingURL=WeightByAxisTask.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WeightByAxisTask.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/WeightByAxisTask.ts"],"names":[],"mappings":"AAEA,OAAO,EAEH,KAAK,wBAAwB,EAChC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA;AAE1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACnC,YAAY,EAAE,wBAAwB,GACvC,yBAAyB,CAY3B"}
1
+ {"version":3,"file":"WeightByAxisTask.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/WeightByAxisTask.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,KAAK,wBAAwB,EAChC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA;AAE1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACnC,YAAY,EAAE,wBAAwB,GACvC,yBAAyB,CAa3B"}
@@ -1,4 +1,5 @@
1
1
  import { calculate_loaded_weights_for_env } from './AxleWeightPhysics.js';
2
+ import { check_cargo_collision } from './CollisionChecker.js';
2
3
  import { build_success_output } from './OutputBuilder.js';
3
4
  import { parse_weight_by_axis_request_data } from './RequestSchema.js';
4
5
  /**
@@ -13,6 +14,7 @@ import { parse_weight_by_axis_request_data } from './RequestSchema.js';
13
14
  * @throws {import('../physics/transports/MissingContainerError.js').MissingContainerError} When a container lookup fails.
14
15
  * @throws {import('../physics/items/UnknownItemIdError.js').UnknownItemIdError} When an item template id is unknown.
15
16
  * @throws {import('../physics/models/deflection/DeflectionModelNotImplementedError.js').DeflectionModelNotImplementedError} When no deflection model exists.
17
+ * @throws {import('./ItemOverlapError.js').ItemOverlapError} When two allocated items occupy the same volume.
16
18
  */
17
19
  export function run_weight_by_axis_task(request_data) {
18
20
  const request = parse_weight_by_axis_request_data(request_data);
@@ -21,6 +23,7 @@ export function run_weight_by_axis_task(request_data) {
21
23
  for (const env of envs) {
22
24
  const loaded_weights = calculate_loaded_weights_for_env(env);
23
25
  apply_loaded_weights_to_env(env, loaded_weights);
26
+ check_cargo_collision(env);
24
27
  output_envs.push(strip_items_and_bands(env));
25
28
  }
26
29
  return build_success_output(output_envs);
package/build/index.d.ts CHANGED
@@ -7,8 +7,9 @@ import { UnsupportedVehicleTypeError } from './core/physics/models/UnsupportedVe
7
7
  import { ContainerSequenceNotFoundError } from './core/physics/transports/ContainerSequenceNotFoundError.js';
8
8
  import { GrouperNotFoundError } from './core/physics/transports/GrouperNotFoundError.js';
9
9
  import { MissingContainerError } from './core/physics/transports/MissingContainerError.js';
10
+ import { ItemOverlapError } from './core/orchestration/ItemOverlapError.js';
10
11
  export type { WeightByAxisRequest, WeightByAxisRequestInput, WeightByAxisResponse, WeightByAxisSuccessOutput };
11
- export { ContainerSequenceNotFoundError, DeflectionModelNotImplementedError, GrouperNotFoundError, MissingContainerError, UnrecognizedSuspensionTypeError, UnknownItemIdError, UnsupportedVehicleTypeError };
12
+ export { ContainerSequenceNotFoundError, DeflectionModelNotImplementedError, GrouperNotFoundError, ItemOverlapError, MissingContainerError, UnrecognizedSuspensionTypeError, UnknownItemIdError, UnsupportedVehicleTypeError };
12
13
  /**
13
14
  * Calculates loaded cargo weight per axle for each transport in the payload.
14
15
  * @param request - Request with `{ envs, config? }`.
@@ -17,6 +18,7 @@ export { ContainerSequenceNotFoundError, DeflectionModelNotImplementedError, Gro
17
18
  * @throws {UnsupportedVehicleTypeError} When physics rejects the vehicle type.
18
19
  * @throws {UnrecognizedSuspensionTypeError} When vanderleia suspension type is invalid.
19
20
  * @throws {GrouperNotFoundError} When a merged-container grouper is missing.
21
+ * @throws {ItemOverlapError} When two allocated items occupy the same volume.
20
22
  * Other exported error classes are for lower-level physics modules, not thrown here.
21
23
  */
22
24
  export declare function calculate_weight_on_axles_physics(request: WeightByAxisRequestInput): WeightByAxisSuccessOutput;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACR,mBAAmB,EACnB,wBAAwB,EAC3B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EACR,oBAAoB,EACpB,yBAAyB,EAC5B,MAAM,8CAA8C,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAA;AAC/E,OAAO,EAAE,kCAAkC,EAAE,MAAM,wEAAwE,CAAA;AAC3H,OAAO,EAAE,+BAA+B,EAAE,MAAM,0DAA0D,CAAA;AAC1G,OAAO,EAAE,2BAA2B,EAAE,MAAM,sDAAsD,CAAA;AAClG,OAAO,EAAE,8BAA8B,EAAE,MAAM,6DAA6D,CAAA;AAC5G,OAAO,EAAE,oBAAoB,EAAE,MAAM,mDAAmD,CAAA;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oDAAoD,CAAA;AAE1F,YAAY,EACR,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,yBAAyB,EAC5B,CAAA;AAED,OAAO,EACH,8BAA8B,EAC9B,kCAAkC,EAClC,oBAAoB,EACpB,qBAAqB,EACrB,+BAA+B,EAC/B,kBAAkB,EAClB,2BAA2B,EAC9B,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAC7C,OAAO,EAAE,wBAAwB,GAClC,yBAAyB,CAG3B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACR,mBAAmB,EACnB,wBAAwB,EAC3B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EACR,oBAAoB,EACpB,yBAAyB,EAC5B,MAAM,8CAA8C,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAA;AAC/E,OAAO,EAAE,kCAAkC,EAAE,MAAM,wEAAwE,CAAA;AAC3H,OAAO,EAAE,+BAA+B,EAAE,MAAM,0DAA0D,CAAA;AAC1G,OAAO,EAAE,2BAA2B,EAAE,MAAM,sDAAsD,CAAA;AAClG,OAAO,EAAE,8BAA8B,EAAE,MAAM,6DAA6D,CAAA;AAC5G,OAAO,EAAE,oBAAoB,EAAE,MAAM,mDAAmD,CAAA;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,oDAAoD,CAAA;AAC1F,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAA;AAE3E,YAAY,EACR,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,yBAAyB,EAC5B,CAAA;AAED,OAAO,EACH,8BAA8B,EAC9B,kCAAkC,EAClC,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,EACrB,+BAA+B,EAC/B,kBAAkB,EAClB,2BAA2B,EAC9B,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAC7C,OAAO,EAAE,wBAAwB,GAClC,yBAAyB,CAG3B"}
package/build/index.js CHANGED
@@ -6,7 +6,8 @@ import { UnsupportedVehicleTypeError } from './core/physics/models/UnsupportedVe
6
6
  import { ContainerSequenceNotFoundError } from './core/physics/transports/ContainerSequenceNotFoundError.js';
7
7
  import { GrouperNotFoundError } from './core/physics/transports/GrouperNotFoundError.js';
8
8
  import { MissingContainerError } from './core/physics/transports/MissingContainerError.js';
9
- export { ContainerSequenceNotFoundError, DeflectionModelNotImplementedError, GrouperNotFoundError, MissingContainerError, UnrecognizedSuspensionTypeError, UnknownItemIdError, UnsupportedVehicleTypeError };
9
+ import { ItemOverlapError } from './core/orchestration/ItemOverlapError.js';
10
+ export { ContainerSequenceNotFoundError, DeflectionModelNotImplementedError, GrouperNotFoundError, ItemOverlapError, MissingContainerError, UnrecognizedSuspensionTypeError, UnknownItemIdError, UnsupportedVehicleTypeError };
10
11
  /**
11
12
  * Calculates loaded cargo weight per axle for each transport in the payload.
12
13
  * @param request - Request with `{ envs, config? }`.
@@ -15,6 +16,7 @@ export { ContainerSequenceNotFoundError, DeflectionModelNotImplementedError, Gro
15
16
  * @throws {UnsupportedVehicleTypeError} When physics rejects the vehicle type.
16
17
  * @throws {UnrecognizedSuspensionTypeError} When vanderleia suspension type is invalid.
17
18
  * @throws {GrouperNotFoundError} When a merged-container grouper is missing.
19
+ * @throws {ItemOverlapError} When two allocated items occupy the same volume.
18
20
  * Other exported error classes are for lower-level physics modules, not thrown here.
19
21
  */
20
22
  export function calculate_weight_on_axles_physics(request) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetta/axle-load-calculator",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Merged weight_by_axis physics and orchestration library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -22,6 +22,7 @@
22
22
  "coverage": "c8 --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=html --reporter=text npm run test"
23
23
  },
24
24
  "dependencies": {
25
+ "@jetta/number-compare": "^3.0.3",
25
26
  "zod": "^3.24.0"
26
27
  },
27
28
  "devDependencies": {