HANK TOOL IMS
A laydown yard is just a promise about where things are. This is the machinery that keeps the promise — the math that moves a reel of fiber from a shelf to a tech's hands without ever losing the thread, and without letting a customer's catalog leave the building.
The Oracle catalog — oraclePN, descriptions, manufacturers. Customer data. Local disk & OneDrive only; never a third party.
Inventory keyed by NTI-NNNNNN — quantities, shelves, custody. No catalog fields. Names re-attach locally, per device.
Topology
Four storage tiers, one source of truth, zero servers in the customer-data path.
Every collection — items, products, requests, sos, assets, hankObjects, history — lives as an in-memory array backed by a single active adapter DB._a. The adapter is chosen at boot by descending preference, and once the operator runs the migration the whole rig drops to local and stays there.
// truth is local; the cloud copy is a shared file, not a database tier := FileSystem ≻ IndexedDB ≻ localStorage (Supabase retired once ims_local_only := 1) write(col, rec) ⟶ adapter.put | adapter.saveAll ⤷ FileSync.markDirty() debounced push delete(col, ids) ⟶ purge + FileSync.noteDelete() tombstone
NTI Numbering
One key to rule the inventory — minted by exactly one authority, collision-free by construction.
The NTI number is the system's own product key. Oracle says one part number; the yard says one part number can mean several real things that satisfy the same spec. NTI gives each of those its own identity. Only the admin machine mints them; everyone else imports the map, so two numbers can never collide across devices.
counter₀ := max{ n : "NTI-"⟨n⟩ ∈ catalog } ∪ {0} next() ⟹ counter ← counter + 1 emit "NTI-" ‖ pad₆(counter) // NTI-017306 invariant strictly increasing · single minter · never reused cardinality one Oracle PN ↦ {ν₁, ν₂, …} substitutes get distinct ν
The Projection
A lossless one-way mirror: customer data goes in, only NTI comes out — and it reverses perfectly on the way back, but only if you hold the map.
This is the heart of it. Before any record touches the shared file it passes through the strip operator Π, a recursive walk that descends into nested line-items and variant lists alike. It resolves each product reference to an NTI and deletes the catalog fields — but only when an NTI exists to put them back. That single guard is what makes it safe: nothing is ever stripped that can't be restored, so a record is never left unreadable on the machine that owns it.
C := { oraclePN, description, partNumber, manufacturer, model, mktPn } // catalog data Π(x) = x x scalar Π([a…]) = [ Π(a) … ] Π(obj) = let ν = obj.nti ?? map⁻¹(obj.oraclePN) drop k∈C from obj iff ν exists ← the losslessness guard keep ν · recurse over the rest Π⁻¹(obj) = re-attach C from map(ν) needs the local map invariant f∈C removed ⟺ ∃ ν : map(ν).f recovers f privacy image(Π) ∩ C = ∅ shared file holds no catalog field identity Π⁻¹ ∘ Π ≡ id on mapped records
Convergence
Many hands, one file, no clobbering — last writer wins per record, deletes that stay dead, and a stale device can't raise the dead.
Several techs hit the same OneDrive file. There is no lock and no server, so the merge has to be order-independent. Each record carries an ISO-8601 stamp that sorts lexicographically as it ticks. Merging two versions of the same id keeps the later stamp. Deletions ride a separate tombstone set so a removed record stays gone — and a newer re-creation still wins over an old delete. Every push reads the file, merges its own changes over it, then writes; pulls only parse when the file's modified-time actually moved.
merge(a, b) = argmaxₜ { a, b } ties → incoming admit(r) ⟺ ¬∃ T[r.id] ≥ r.t delete dominates older write newer write dominates old delete push = write ∘ merge ∘ read read-before-write, never clobber pull = parse only if file.mtime ↑ cheap change-gate theorem ∀ replicas seeing the same op-set ⟹ identical state, independent of arrival order.
Stock Engine
Material is consumed; equipment is loaned. The engine knows the difference, and it knows which shelf you meant.
When the same cable sits on two shelves, a sign-out has to pull from the right one. Source resolution gathers every candidate that matches the scanned token by id or barcode, then prefers the one at the named location. Consume decrements and logs a sign-out — material leaves the books because it got installed. Transfer is a move: consume at the source, merge into the destination, custody intact.
cand(σ) = { i ∈ items : σ ∈ {i.id, i.barcode} ∪ i.allBarcodes }
src(σ, ℓ) = first{ i ∈ cand(σ) : i.shelf=ℓ ∨ i.loc=ℓ } ?? cand(σ)₀
consume(σ, k, dest):
t ← src.qty ; k ← clamp(k, 1, t)
if k ≥ t : remove src · tombstone line emptied
else : src.qty ← t − k
history ⊕ ⟨SIGNED-OUT, σ, k, dest, who⟩ never asks "due back"
transfer = consume(source) ⊕ merge(dest) returnable / relocation
Indexing Pathways
One scan, six gates — the first that recognizes the token wins, every gate an O(1) probe.
A barcode off the gun is just a string. Resolving it is a cascade: the token falls through ordered hash indexes until one claims it. Oracle PN first, then the manufacturer part, then a known item barcode, its alternates, the marketing PN, and finally the part-group table that ties substitutes together. Miss everything and it's flagged unknown — an invitation to receive it, not an error.
σ ⟶ idx_oracle ⟶ idx_part ⟶ idx_barcode
⟶ idx_allBarcodes ⟶ idx_mkt ⟶ idx_partGroup ⟶ ∅
resolve(σ) = first non-∅ probe along the chain
each idxᵢ hash map, built on demand, dropped when the catalog changes
∅ ⟹ "not in catalog" · offer Receive / Take to DM
Import Inference
Guess the columns, ask for help when unsure, and keep every single line.
A spreadsheet arrives shaped however the last person left it — sometimes the whole sheet crammed into one cell. The header mapper normalizes each column name and matches it to a field by keyword; gaps get handed to the model to sort out. And because one Oracle line can be several products, the master import runs in keep-every-row mode: each row becomes its own entry with its own NTI, duplicates and blank-key substitute lines preserved, only truly empty rows dropped.
autoMap(h) = field where keyword-predicate(norm h) matches ; else ∅ fill ∅ with Φ(headers, samples) model assist, only the gaps keep-every-row: row ↦ entry one NTI per line drop ⟺ row is ⌀ (no PN ∧ no desc ∧ no part ∧ no mfr) keep duplicate Oracle PNs · keep blank-PN substitutes |catalog| = |rows| − |⌀ rows| the count finally matches the file
Anomaly Detection
The diagnostics surface what's wrong by set arithmetic — and they never act on it alone.
Integrity is checked, not assumed. Each scan over the data is a set operation: lines that group together but shouldn't, items pointing at catalog entries that don't exist, one barcode owned by two records, quantities that fell through zero. Everything found is flagged for a human. The audit flow moves shelf contents to a catch-bin instead of erasing them; normalization repairs records on load without deleting a thing. The machine notices. The operator decides.
duplicates = { g = group(items, ⟨oraclePN, loc, unit⟩) : |g| > 1 }
orphans = { i : i.oraclePN ∉ catalog }
collisions = { b : |{ owner(b) }| > 1 }
zero-lines = { i : i.qty ≤ 0 }
custody-gaps= { a ∈ assets : status=out ∧ ¬∃ holder }
resolution surface ⟶ one-tap fix ⟶ human confirm
audit shelf ⟶ "Unlabeled" bin move, don't delete
Nothing is destroyed.
Only moved, flagged, or re-keyed.
That's the one rule underneath all seven modules. Data outlives the operation that touched it; the source of truth is singular; the human stays in the loop. Everything else is bookkeeping done carefully.