Attribute schema
August 3, 2026 · View on GitHub
Every node carries identity fields. Only class-level nodes (6-digit leaves) carry the attribute payload — divisions and groups are structure, not data.
The attributes exist to answer two questions without opening the box:
- What else belongs next to this? → zone, access frequency, anti-affinity
- What happens to it in transit? → mass, density, fragility, hazard, orientation
Identity
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | 2, 4 or 6 digits. See id-scheme.md |
name | string | yes | Singular-plural natural phrasing, e.g. "Pots & pans" |
description | string | yes | One or two sentences. Written to be read by a classifier as much as a human — say what falls outside the class when the boundary is not obvious |
aliases | string[] | no | Search and classifier hints: trade names, regionalisms, common misspellings |
status | enum | no | active (default) | deprecated |
superseded_by | string | no | Required when status: deprecated |
notes | string | no | Free text; anything the fields cannot carry |
Physical
| Field | Type | Default | Notes |
|---|---|---|---|
typical_mass_kg | number | required | Mass of one representative item |
bulk_density_kg_per_l | number | required | Mass per litre as packed, voids included. This is the field that does the work — see below |
nestable | bool | false | Items of this class collapse into each other (pots, bowls, buckets) |
stackable | bool | true | Can be stacked on its own kind |
load_bearing | bool | true | Other things may be stacked on top of it |
orientation | enum | any | any | upright | flat — how it must sit in the box |
Why bulk density is the important number
Item mass tells you what one thing weighs. Bulk density tells you whether the box is liftable, which is the constraint that actually bites.
A 40 L box has a safe manual-handling ceiling around 20 kg — so 0.5 kg/L is the line. Anything denser than that cannot fill a 40 L box:
| Class of thing | ~kg/L | 40 L box weighs |
|---|---|---|
| Duvets, pillows | 0.06 | 2 kg |
| Folded clothing | 0.15 | 6 kg |
| Plastic toys | 0.15 | 6 kg |
| Pots & pans (voids) | 0.25 | 10 kg |
| Cables, coiled | 0.35 | 14 kg |
| Paperbacks | 0.50 | 20 kg ← at the limit |
| Hardbacks | 0.60 | 24 kg |
| Hand tools | 0.65 | 26 kg |
| Paper, documents | 0.75 | 30 kg |
| Tinned food | 0.90 | 36 kg |
| Screws & fixings, loose | 1.20 | 48 kg |
The classic packing mistake — a large carton filled with books — is one multiplication away from being caught. A system holding this field can refuse the fill before anyone lifts it.
All mass and density figures are estimates unless a notes entry cites a
measurement. They are planning figures accurate to roughly ±30%, which is enough
to keep a box under a weight limit and not enough to bill a shipper on. Where a
class spans a wide range (Furniture especially), the figure is the middle of the
common domestic range, not the mean of everything sold.
Handling & sensitivity
| Field | Type | Default | Notes |
|---|---|---|---|
fragile | bool | false | Breaks on the impacts a box normally sees |
crushable | bool | false | Deforms permanently under load without breaking — lampshades, hats, tubes of paint |
moisture_sensitive | bool | false | Damaged by damp storage: paper, textiles, electronics |
temperature_sensitive | bool | false | Damaged by an unconditioned loft or a container in summer: candles, vinyl, batteries, cosmetics, medicines |
light_sensitive | bool | false | Fades or degrades in daylight: photographs, textiles, some plastics |
fragile and crushable are separate on purpose. Glassware is fragile and not
crushable; a lampshade is crushable and not fragile. They call for opposite
placements in a box — fragile items go padded and central, crushable items go on
top with nothing above.
Safety
| Field | Type | Default | Notes |
|---|---|---|---|
child_hazard | bool | false | Must be stored out of reach of a small child |
child_hazard_reasons | enum[] | [] | choking | sharp | toxic | medicinal | burn | electrical | strangulation | crush | drowning | magnet | battery_ingestion |
hazmat | enum | none | none | explosive | flammable | corrosive | aerosol | oxidiser | toxic | compressed_gas | biohazard |
un_class | enum | none | UN/ADR hazard class — 1 | 2.1 | 2.2 | 2.3 | 3 | 4.1 | 4.2 | 4.3 | 5.1 | 5.2 | 6.1 | 6.2 | 7 | 8 | 9. See hazmat.md |
un_indicative | string | — | The typical UN number where one entry dominates the class, e.g. UN1950. Unset means it varies by product |
battery | enum | none | none | alkaline | button_cell | nimh | lithium_ion | lead_acid |
perishable | bool | false | Has a use-by horizon: food, some medicines, adhesives |
hazmat and un_class are both required once either is set, and the validator
enforces the mapping between them. They exist as a pair because they answer to
different readers — hazmat is what the person packing the box understands,
un_class is what a freight forwarder or customs agent needs. Neither is a
shipping declaration; hazmat.md sets out the limits.
child_hazard is stored as the risk, not as its inverse. "Child safe" as a field
would make the safe state the assertion, and an unfilled record would read as
safe — the wrong way round for a field whose whole job is to catch the dangerous
minority. Unset means unknown, and unknown should never resolve to safe.
battery is separated from hazmat because it is not a hazard classification,
it is a shipping and storage one. Lithium cells are what make a shipment
restricted, what makes a loft a bad idea, and what should never be packed
charged and unterminated.
Logistics
| Field | Type | Default | Notes |
|---|---|---|---|
storage_zone | enum | required | kitchen | living | bedroom | bathroom | utility | garage | loft | basement | office | outdoor | any — where the class naturally lives |
access_frequency | enum | required | daily | weekly | monthly | seasonal | archive — drives shelf depth and box order |
two_person | bool | false | Not a one-person lift |
disassembly | bool | false | Normally taken apart to move |
handling | tag[] | [] | Transport requirements that are not fragility. See special-handling.md |
value_density | enum | low | low | medium | high — value per litre, for insurance declarations and for deciding what does not go in the van |
mover_restricted | bool | false | Commercial removers routinely refuse to carry it |
co_store_avoid | tag[] | [] | food | textiles | paper | electronics | medicine | bedding | children | heat |
handling values: hand_carry, drain_required, depressurise_required,
transit_restraint, settle_after_transit, professional_disconnect,
specialist_mover, rigid_case, climate_controlled, customs_declaration,
permit_required, living, magnetic.
fragile and handling do not overlap. fragile is a property of the object
that calls for padding on the day; handling is a set of actions with lead
times attached — an appointment to book, a tank to drain, a permit to apply for.
Most of them have to happen before moving day rather than on it, which is the
whole reason they are worth recording weeks in advance.
co_store_avoid is the anti-affinity list, and it is the only field that
constrains combinations. Solvents do not go in with textiles; anything
corrosive stays away from electronics; nothing chemical shares a box with food.
An affinity engine that only clusters on similarity will happily put drain
cleaner next to the flour, because both live in the kitchen and both are used
monthly. This field is what stops it.
mover_restricted is a fact about the trade rather than about the object.
Removal firms' standard terms exclude flammables, aerosols, compressed gas, open
paint, and usually anything with a lead-acid battery, because their insurance
excludes them. The item is not dangerous to store — it just cannot travel on
their van, and it needs to be found before the day, not on it.
Inheritance
Attributes do not inherit down the tree. A group is not required to be homogeneous, and asserting a default at group level would quietly become the truth for classes nobody had got to yet. Every class states its own values, and absent optional fields take the documented defaults above.
Roll-ups go the other way: a division's characteristics are computed from its classes, weighted by whatever the caller has (item counts, volumes, masses), and are never stored.