@jetta/blocks-allocator 0.2.1-0 → 0.2.1-2

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.
Files changed (24) hide show
  1. package/package.json +2 -1
  2. package/src/Allocation_Attempt.js +4 -1
  3. package/src/Allocation_Optimization/Block_Allocator.js +12 -6
  4. package/src/Allocation_Optimization/block_builder/Block_Wall_Builder.js +8 -1
  5. package/src/Allocation_Optimization/block_builder/Blocks_Builder.js +3 -1
  6. package/src/Allocation_Optimization/block_builder/Slot_Item_Assigner.js +4 -2
  7. package/src/Allocation_Optimization/item_consumption_heuristic/Item_Chooser.js +6 -130
  8. package/src/Allocation_Optimization/item_consumption_heuristic/Slots_Items_Assigner.js +59 -32
  9. package/src/Allocation_Optimization/item_consumption_heuristic/assign_one_plan_slot.js +53 -0
  10. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/assert_layout_fits_axle_load.js +26 -0
  11. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/axle_load_exceeds_limit.js +62 -0
  12. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/candidate_item_axle_score.js +42 -0
  13. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/candidate_item_fits_axle_load.js +19 -0
  14. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/clone_env_with_layout_items.js +26 -0
  15. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/evaluate_layout_axle_load.js +63 -0
  16. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/layout_fits_axle_load.js +36 -0
  17. package/src/Allocation_Optimization/item_consumption_heuristic/axle_load/map_allocated_item_to_axle_wire.js +21 -0
  18. package/src/Allocation_Optimization/item_consumption_heuristic/select_axle_balanced_slot_item.js +143 -0
  19. package/src/Allocation_Optimization/item_consumption_heuristic/select_slot_item.js +149 -0
  20. package/src/Allocation_Optimization/item_consumption_heuristic/slot_candidate_acceptance.js +110 -0
  21. package/src/Allocation_Optimization/item_consumption_heuristic/slot_candidate_order.js +49 -0
  22. package/src/models/Cargo_Profile.js +27 -0
  23. package/src/models/transport/Container.js +11 -0
  24. package/src/models/transport/Transport.js +12 -0
@@ -0,0 +1,19 @@
1
+ const layout_fits_axle_load = require('./layout_fits_axle_load')
2
+
3
+ /**
4
+ * Whether placing this allocated item on top of pending axle items stays within axle caps.
5
+ * @param {object} data - Chooser bag with transport, container, pending_axle_items.
6
+ * @param {object} allocated_item
7
+ * @returns {boolean}
8
+ */
9
+ function candidate_item_fits_axle_load(data, allocated_item)
10
+ {
11
+ const pending_axle_items = data.pending_axle_items ?? []
12
+ return layout_fits_axle_load({
13
+ transport: data.transport,
14
+ container: data.container,
15
+ extra_items: pending_axle_items.concat(allocated_item)
16
+ })
17
+ }
18
+
19
+ module.exports = candidate_item_fits_axle_load
@@ -0,0 +1,26 @@
1
+ const clone = require('../../../tools/Clone')
2
+ const map_allocated_item_to_axle_wire = require('./map_allocated_item_to_axle_wire')
3
+
4
+ /**
5
+ * Deep-clones the axle env template and writes current plus extra items per container.
6
+ * @param {object} transport
7
+ * @param {object} current_container
8
+ * @param {object[]} extra_items - Pending wall, earlier slots, and candidate for current container.
9
+ * @returns {object}
10
+ */
11
+ function clone_env_with_layout_items(transport, current_container, extra_items)
12
+ {
13
+ const env = clone(transport.get_axle_env_template())
14
+ const extra_item_list = extra_items ?? []
15
+ for (const container_data of env.containers ?? [])
16
+ {
17
+ const sequence = Number(container_data.sequencia)
18
+ const live_container = transport.get_containers().get(sequence)
19
+ const committed_items = live_container?.get_allocated_items?.() ?? []
20
+ const pending_items = live_container === current_container ? extra_item_list : []
21
+ container_data.items = committed_items.concat(pending_items).map(map_allocated_item_to_axle_wire)
22
+ }
23
+ return env
24
+ }
25
+
26
+ module.exports = clone_env_with_layout_items
@@ -0,0 +1,63 @@
1
+ const { calculate_weight_on_axles_physics } = require('@jetta/axle-load-calculator')
2
+ const { gt } = require('@jetta/number-compare')
3
+ const { Logger } = require('@jetta/logger')
4
+ const Cargo_Profile = require('../../../models/Cargo_Profile')
5
+ const clone_env_with_layout_items = require('./clone_env_with_layout_items')
6
+ const axle_load_exceeds_limit = require('./axle_load_exceeds_limit')
7
+ const { max_axle_load_ratio } = axle_load_exceeds_limit
8
+
9
+ const logger = new Logger('Axle Load Slot Gate')
10
+
11
+ /**
12
+ * Hottest axle ratio for the layout, or undefined when physics cannot run.
13
+ * @param {object} data
14
+ * @param {object} data.transport
15
+ * @param {object} data.container
16
+ * @param {object[]} data.extra_items
17
+ * @returns {number|undefined}
18
+ */
19
+ function score_layout_axle_load(data)
20
+ {
21
+ const env = clone_env_with_layout_items(data.transport, data.container, data.extra_items)
22
+ try
23
+ {
24
+ return ratio_from_physics(env)
25
+ }
26
+ catch (error)
27
+ {
28
+ logger.warn(`axle calculator rejected layout: ${error.message}`)
29
+ return undefined
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Runs axle physics for the current layout plus extra items on the active container.
35
+ * @param {object} data
36
+ * @param {object} data.transport
37
+ * @param {object} data.container
38
+ * @param {object[]} data.extra_items
39
+ * @returns {boolean} True when no axle exceeds the folga-adjusted cap.
40
+ */
41
+ function evaluate_layout_axle_load(data)
42
+ {
43
+ const ratio = score_layout_axle_load(data)
44
+ if (ratio === undefined)
45
+ {
46
+ return false
47
+ }
48
+ return gt(ratio, 1) === false
49
+ }
50
+
51
+ /**
52
+ * @param {object} env
53
+ * @returns {number}
54
+ */
55
+ function ratio_from_physics(env)
56
+ {
57
+ const output = calculate_weight_on_axles_physics({ envs: [env] })
58
+ const folga_percent = Cargo_Profile.axle_weight_gap_percent()
59
+ return max_axle_load_ratio(output.envs[0], folga_percent)
60
+ }
61
+
62
+ module.exports = evaluate_layout_axle_load
63
+ module.exports.score_layout_axle_load = score_layout_axle_load
@@ -0,0 +1,36 @@
1
+ const Cargo_Profile = require('../../../models/Cargo_Profile')
2
+ const evaluate_layout_axle_load = require('./evaluate_layout_axle_load')
3
+
4
+ /**
5
+ * Returns whether the layout may be committed under the optional axle slot gate.
6
+ * @param {object} data
7
+ * @param {object} [data.transport]
8
+ * @param {object} [data.container]
9
+ * @param {object[]} [data.extra_items]
10
+ * @returns {boolean}
11
+ */
12
+ function layout_fits_axle_load(data)
13
+ {
14
+ if (!Cargo_Profile.enable_axle_load_slot_gate())
15
+ {
16
+ return true
17
+ }
18
+ if (!transport_has_axles(data.transport))
19
+ {
20
+ return true
21
+ }
22
+ return evaluate_layout_axle_load(data)
23
+ }
24
+
25
+ /**
26
+ * @param {object|undefined} transport
27
+ * @returns {boolean}
28
+ */
29
+ function transport_has_axles(transport)
30
+ {
31
+ const env = transport?.get_axle_env_template?.()
32
+ return Array.isArray(env?.eixos) && env.eixos.length > 0
33
+ }
34
+
35
+ module.exports = layout_fits_axle_load
36
+ module.exports.transport_has_axles = transport_has_axles
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Maps an allocated item to the ms-faixas axle-calculator item contract.
3
+ * Longitudinal chassis position is JSON `y` (allocator `z`).
4
+ * @param {object} allocated_item
5
+ * @returns {object}
6
+ */
7
+ function map_allocated_item_to_axle_wire(allocated_item)
8
+ {
9
+ return {
10
+ id: allocated_item.get_id(),
11
+ x: allocated_item.get_x(),
12
+ y: allocated_item.get_z(),
13
+ z: allocated_item.get_y(),
14
+ peso: allocated_item.get_weight(),
15
+ comprimento: allocated_item.get_length(),
16
+ largura: allocated_item.get_width(),
17
+ altura: allocated_item.get_height()
18
+ }
19
+ }
20
+
21
+ module.exports = map_allocated_item_to_axle_wire
@@ -0,0 +1,143 @@
1
+ const { lt } = require('@jetta/number-compare')
2
+ const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
3
+ const { score_without_commit, commit_scored_candidate } = require('./slot_candidate_acceptance')
4
+
5
+ /**
6
+ * Legal SKU across every weight band with the lowest hottest-axle ratio.
7
+ * Ties keep band order, then candidate order inside the band.
8
+ * @param {object} data
9
+ * @param {object} context
10
+ * @returns {object|undefined}
11
+ */
12
+ function commit_lowest_ratio_across_bands(data, context)
13
+ {
14
+ const ranked = rank_every_band(data, context)
15
+ for (const selected of ranked)
16
+ {
17
+ const committed = commit_scored_candidate(data, context.container, selected)
18
+ if (committed)
19
+ {
20
+ return committed
21
+ }
22
+ }
23
+ return undefined
24
+ }
25
+
26
+ /**
27
+ * Best legal SKU across every band. Does not commit stacking.
28
+ * @param {object} data
29
+ * @param {object} context
30
+ * @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
31
+ */
32
+ function lowest_ratio_across_bands(data, context)
33
+ {
34
+ const ranked = rank_every_band(data, context)
35
+ return ranked[0]
36
+ }
37
+
38
+ /**
39
+ * Preferred SKU scored without a stacking commit.
40
+ * @param {object} data
41
+ * @param {object} context
42
+ * @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
43
+ */
44
+ function score_preferred(data, context)
45
+ {
46
+ const allocation_item = data.item_list.find(item => item.get_id() === data.previous_chosen_item_id)
47
+ if (!allocation_item)
48
+ {
49
+ return undefined
50
+ }
51
+ return score_without_commit(data, context, allocation_item)
52
+ }
53
+
54
+ /**
55
+ * @param {{ ratio: number }|undefined} preferred
56
+ * @param {{ ratio: number }|undefined} best
57
+ * @returns {boolean}
58
+ */
59
+ function preferred_wins(preferred, best)
60
+ {
61
+ if (!preferred)
62
+ {
63
+ return false
64
+ }
65
+ if (!best)
66
+ {
67
+ return true
68
+ }
69
+ return lt(best.ratio, preferred.ratio) === false
70
+ }
71
+
72
+ /**
73
+ * @param {object} data
74
+ * @param {object} context
75
+ * @returns {object[]}
76
+ */
77
+ function rank_every_band(data, context)
78
+ {
79
+ const scored = []
80
+ for (const band of context.ordered_bands)
81
+ {
82
+ scored.push(...rank_band(data, context, band.items.slice()))
83
+ }
84
+ return rank_by_ratio(scored)
85
+ }
86
+
87
+ /**
88
+ * @param {object} data
89
+ * @param {object} context
90
+ * @param {object[]} pending
91
+ * @returns {object[]}
92
+ */
93
+ function rank_band(data, context, pending)
94
+ {
95
+ const scored = []
96
+ while (pending.length > 0)
97
+ {
98
+ const allocation_item = get_item(pending, data.qty_of_slots, context.container)
99
+ const candidate = score_without_commit(data, context, allocation_item)
100
+ if (candidate)
101
+ {
102
+ scored.push(candidate)
103
+ }
104
+ remove_item_from_candidate_list(pending, allocation_item)
105
+ }
106
+ return rank_by_ratio(scored)
107
+ }
108
+
109
+ /**
110
+ * @param {{ ratio: number }[]} scored
111
+ * @returns {{ ratio: number }[]}
112
+ */
113
+ function rank_by_ratio(scored)
114
+ {
115
+ const ranked = []
116
+ for (const candidate of scored)
117
+ {
118
+ insert_by_ratio(ranked, candidate)
119
+ }
120
+ return ranked
121
+ }
122
+
123
+ /**
124
+ * @param {{ ratio: number }[]} ranked
125
+ * @param {{ ratio: number }} candidate
126
+ * @returns {void}
127
+ */
128
+ function insert_by_ratio(ranked, candidate)
129
+ {
130
+ let index = 0
131
+ while (index < ranked.length && lt(ranked[index].ratio, candidate.ratio))
132
+ {
133
+ index += 1
134
+ }
135
+ ranked.splice(index, 0, candidate)
136
+ }
137
+
138
+ module.exports = {
139
+ commit_lowest_ratio_across_bands,
140
+ lowest_ratio_across_bands,
141
+ score_preferred,
142
+ preferred_wins
143
+ }
@@ -0,0 +1,149 @@
1
+ const Cargo_Profile = require('../../models/Cargo_Profile')
2
+ const No_Suitable_Item_Exception = require('../block_builder/exceptions/No_Suitable_Item_Exception')
3
+ const { ALLOCATOR_MESSAGE } = require('../../messages/Allocator_Message')
4
+ const { distribute_items_by_weight_range } = require('./Weight_Slot_Distribution')
5
+ const {
6
+ resolve_choose_item_heuristic_context,
7
+ format_slot_label_for_exception
8
+ } = require('./Choose_Item_Heuristic_Context')
9
+ const {
10
+ calculate_total_stack_slots,
11
+ sort_stacking_slots_by_proximity
12
+ } = require('./Stack_Slot_Calculator')
13
+ const { get_item, remove_item_from_candidate_list } = require('./slot_candidate_order')
14
+ const { try_accept_candidate, commit_scored_candidate } = require('./slot_candidate_acceptance')
15
+ const {
16
+ commit_lowest_ratio_across_bands,
17
+ lowest_ratio_across_bands,
18
+ score_preferred,
19
+ preferred_wins
20
+ } = require('./select_axle_balanced_slot_item')
21
+
22
+ /**
23
+ * Chooses a slot item. When the axle gate is on, the legal SKU with the lowest hottest-axle ratio wins.
24
+ * @param {object} data
25
+ * @returns {object}
26
+ */
27
+ function select_slot_item(data)
28
+ {
29
+ const context = build_band_context(data)
30
+ if (Cargo_Profile.enable_axle_load_slot_gate())
31
+ {
32
+ const selected = commit_lowest_ratio_across_bands(data, context)
33
+ if (!selected)
34
+ {
35
+ throw unsuitable(context.slot)
36
+ }
37
+ return selected
38
+ }
39
+ for (const band of context.ordered_bands)
40
+ {
41
+ const selected = first_accepted_in_band(data, context, band.items)
42
+ if (selected)
43
+ {
44
+ return selected
45
+ }
46
+ }
47
+ throw unsuitable(context.slot)
48
+ }
49
+
50
+ /**
51
+ * Same-id preference. Under the axle gate, that SKU wins only when it is not strictly hotter.
52
+ * @param {object} data
53
+ * @returns {object}
54
+ */
55
+ function select_preferred_or_heuristic(data)
56
+ {
57
+ if (!Cargo_Profile.enable_axle_load_slot_gate())
58
+ {
59
+ return preferred_or_first(data)
60
+ }
61
+ return preferred_or_balanced(data)
62
+ }
63
+
64
+ /**
65
+ * @param {object} data
66
+ * @returns {object}
67
+ */
68
+ function preferred_or_first(data)
69
+ {
70
+ const { container, item_list, previous_chosen_item_id, item_position } = data
71
+ const allocation_item = item_list.find(item => item.get_id() === previous_chosen_item_id)
72
+ if (allocation_item)
73
+ {
74
+ const accepted_item = try_accept_candidate(data, container, allocation_item, item_position)
75
+ if (accepted_item)
76
+ {
77
+ return accepted_item
78
+ }
79
+ }
80
+ return select_slot_item(data)
81
+ }
82
+
83
+ /**
84
+ * @param {object} data
85
+ * @returns {object}
86
+ */
87
+ function preferred_or_balanced(data)
88
+ {
89
+ const context = build_band_context(data)
90
+ const preferred = score_preferred(data, context)
91
+ const best = lowest_ratio_across_bands(data, context)
92
+ const winner = preferred_wins(preferred, best) ? preferred : best
93
+ if (!winner)
94
+ {
95
+ throw unsuitable(context.slot)
96
+ }
97
+ return commit_scored_candidate(data, context.container, winner)
98
+ }
99
+
100
+ /**
101
+ * @param {object} data
102
+ * @returns {object}
103
+ */
104
+ function build_band_context(data)
105
+ {
106
+ const { slot, container, item_position } = resolve_choose_item_heuristic_context(data)
107
+ const total_slots = calculate_total_stack_slots(container.get_height(), slot)
108
+ const stacking_slots = distribute_items_by_weight_range(data.item_list, total_slots)
109
+ const ordered_bands = sort_stacking_slots_by_proximity(stacking_slots, slot.get_y(), slot.get_height())
110
+ return { slot, container, item_position, ordered_bands }
111
+ }
112
+
113
+ /**
114
+ * @param {object} data
115
+ * @param {object} context
116
+ * @param {object[]} candidate_items
117
+ * @returns {object|undefined}
118
+ */
119
+ function first_accepted_in_band(data, context, candidate_items)
120
+ {
121
+ while (candidate_items.length > 0)
122
+ {
123
+ const allocation_item = get_item(candidate_items, data.qty_of_slots, context.container)
124
+ const accepted_item = try_accept_candidate(data, context.container, allocation_item, context.item_position)
125
+ if (accepted_item)
126
+ {
127
+ return accepted_item
128
+ }
129
+ remove_item_from_candidate_list(candidate_items, allocation_item)
130
+ }
131
+ return undefined
132
+ }
133
+
134
+ /**
135
+ * @param {object} slot
136
+ * @returns {No_Suitable_Item_Exception}
137
+ */
138
+ function unsuitable(slot)
139
+ {
140
+ return new No_Suitable_Item_Exception(
141
+ ALLOCATOR_MESSAGE.NO_SUITABLE_ITEM,
142
+ [format_slot_label_for_exception(slot)]
143
+ )
144
+ }
145
+
146
+ module.exports = {
147
+ select_slot_item,
148
+ select_preferred_or_heuristic
149
+ }
@@ -0,0 +1,110 @@
1
+ const Cargo_Profile = require('../../models/Cargo_Profile')
2
+ const Allocated_Item = require('../../models/Allocated_Item')
3
+ const candidate_item_fits_axle_load = require('./axle_load/candidate_item_fits_axle_load')
4
+ const candidate_item_axle_score = require('./axle_load/candidate_item_axle_score')
5
+
6
+ /**
7
+ * Builds the allocated item at the slot position.
8
+ * @param {object} allocation_item
9
+ * @param {object} item_position
10
+ * @returns {object}
11
+ */
12
+ function build_allocated_item(allocation_item, item_position)
13
+ {
14
+ return new Allocated_Item(allocation_item, item_position.x, item_position.y, item_position.z)
15
+ }
16
+
17
+ /**
18
+ * Accepts the first legal candidate and commits stacking.
19
+ * @param {object} data
20
+ * @param {object} container
21
+ * @param {object} allocation_item
22
+ * @param {object} item_position
23
+ * @returns {object|undefined}
24
+ */
25
+ function try_accept_candidate(data, container, allocation_item, item_position)
26
+ {
27
+ const allocated_item = build_allocated_item(allocation_item, item_position)
28
+ if (!candidate_item_fits_axle_load(data, allocated_item))
29
+ {
30
+ return undefined
31
+ }
32
+ if (should_defer_loose_on_block_stacking_validation(data.block))
33
+ {
34
+ return allocated_item
35
+ }
36
+ if (!container.is_a_valid_stacking(allocated_item))
37
+ {
38
+ return undefined
39
+ }
40
+ return allocated_item
41
+ }
42
+
43
+ /**
44
+ * Scores a candidate without writing the stacking index.
45
+ * @param {object} data
46
+ * @param {object} context
47
+ * @param {object} allocation_item
48
+ * @returns {{ allocated_item: object, ratio: number, allocation_item: object }|undefined}
49
+ */
50
+ function score_without_commit(data, context, allocation_item)
51
+ {
52
+ const allocated_item = build_allocated_item(allocation_item, context.item_position)
53
+ const score = candidate_item_axle_score(data, allocated_item)
54
+ if (!score.fits || !stacking_probe_allows(data, context.container, allocated_item))
55
+ {
56
+ return undefined
57
+ }
58
+ return { allocated_item, ratio: score.ratio, allocation_item }
59
+ }
60
+
61
+ /**
62
+ * Commits the chosen candidate into the live stacking index.
63
+ * @param {object} data
64
+ * @param {object} container
65
+ * @param {{ allocated_item: object }} scored
66
+ * @returns {object|undefined}
67
+ */
68
+ function commit_scored_candidate(data, container, scored)
69
+ {
70
+ if (should_defer_loose_on_block_stacking_validation(data.block))
71
+ {
72
+ return scored.allocated_item
73
+ }
74
+ if (!container.is_a_valid_stacking(scored.allocated_item))
75
+ {
76
+ return undefined
77
+ }
78
+ return scored.allocated_item
79
+ }
80
+
81
+ /**
82
+ * @param {object} data
83
+ * @param {object} container
84
+ * @param {object} allocated_item
85
+ * @returns {boolean}
86
+ */
87
+ function stacking_probe_allows(data, container, allocated_item)
88
+ {
89
+ if (should_defer_loose_on_block_stacking_validation(data.block))
90
+ {
91
+ return true
92
+ }
93
+ if (typeof container.probe_stacking === 'function')
94
+ {
95
+ return container.probe_stacking(allocated_item)
96
+ }
97
+ return container.is_a_valid_stacking(allocated_item)
98
+ }
99
+
100
+ function should_defer_loose_on_block_stacking_validation(block)
101
+ {
102
+ return Cargo_Profile.stack_loose_on_tie_ratio_blocks()
103
+ && block?.get_slot_list?.().length === 1
104
+ }
105
+
106
+ module.exports = {
107
+ try_accept_candidate,
108
+ score_without_commit,
109
+ commit_scored_candidate
110
+ }
@@ -0,0 +1,49 @@
1
+ const No_Available_Items_Exception = require('./exceptions/No_Available_Items_Exception')
2
+ const No_Slots_For_Items_Exception = require('./exceptions/No_Slots_For_Items_Exception')
3
+ const { order_items_by_capacity_bias } = require('./Capacity_Bias_Item_Order')
4
+
5
+ /**
6
+ * Removes one candidate from the working list.
7
+ * @param {object[]} candidate_item_list
8
+ * @param {object} allocation_item
9
+ * @returns {void}
10
+ */
11
+ function remove_item_from_candidate_list(candidate_item_list, allocation_item)
12
+ {
13
+ const index = candidate_item_list.indexOf(allocation_item)
14
+ if (index > -1)
15
+ {
16
+ candidate_item_list.splice(index, 1)
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Next candidate in the existing capacity-bias order.
22
+ * @param {object[]} item_list
23
+ * @param {number|undefined} qty_of_slots
24
+ * @param {object} container
25
+ * @returns {object}
26
+ */
27
+ function get_item(item_list, qty_of_slots, container)
28
+ {
29
+ if (qty_of_slots === undefined)
30
+ {
31
+ throw new No_Slots_For_Items_Exception()
32
+ }
33
+ if (item_list.length === 0)
34
+ {
35
+ throw new No_Available_Items_Exception()
36
+ }
37
+ const ordered_item_list = order_items_by_capacity_bias(item_list, container)
38
+ const preferable_item_list = ordered_item_list.filter(item => item.get_amount() >= qty_of_slots)
39
+ if (preferable_item_list.length > 0)
40
+ {
41
+ return preferable_item_list[0]
42
+ }
43
+ return ordered_item_list[0]
44
+ }
45
+
46
+ module.exports = {
47
+ get_item,
48
+ remove_item_from_candidate_list
49
+ }
@@ -81,6 +81,33 @@ class Cargo_Profile
81
81
  return str_to_bool(this.#instance['allocation.blocks.enable_item_continuation_lock'], true)
82
82
  }
83
83
 
84
+ /**
85
+ * Whether slot item choice rejects SKUs that would overload axles.
86
+ * Defaults to false when omitted or when no cargo profile is loaded.
87
+ * @returns {boolean}
88
+ */
89
+ static enable_axle_load_slot_gate()
90
+ {
91
+ if (Cargo_Profile.#instance === null)
92
+ {
93
+ return false
94
+ }
95
+ return str_to_bool(this.#instance['allocation.blocks.enable_axle_load_slot_gate'], false)
96
+ }
97
+
98
+ /**
99
+ * Axle slack percent applied as pesoMaxFinal * (1 + percent / 100).
100
+ * @returns {number}
101
+ */
102
+ static axle_weight_gap_percent()
103
+ {
104
+ if (Cargo_Profile.#instance === null)
105
+ {
106
+ return 0
107
+ }
108
+ return finite_float_from_string_or_number(this.#instance.folga_de_peso_nos_eixos, 0)
109
+ }
110
+
84
111
  static enable_key_item_mapping()
85
112
  {
86
113
  return str_to_bool(this.#instance['allocation.blocks.preferential.enable'], true)
@@ -87,6 +87,17 @@ class Container extends Space
87
87
  return this.#stacking_feasibility_checker.validate_stackings(item_data)
88
88
  }
89
89
 
90
+ /**
91
+ * Checks stacking on a clone so the live index stays unchanged.
92
+ * @param {object} item
93
+ * @returns {boolean}
94
+ */
95
+ probe_stacking(item)
96
+ {
97
+ const probe = this.#stacking_feasibility_checker.clone()
98
+ return probe.validate_stackings(create_input(item))
99
+ }
100
+
90
101
  /**
91
102
  * Drops provisional stacking commits left by failed slot/wall attempts.
92
103
  * Restores the pre-attempt checkpoint when stacking was touched; otherwise a no-op.