Blog ยท release
Latch 0.6.0
The kit module, and the badge engine that came with it. Everything you own โ gear, toys, consumables โ what it is made of, what it cost, when it was actually used and what happened. Your toys earn trophies for it. Closes issue #11.
- Three kinds, because they are measured three different ways. A toy in bouts of time, a piece of gear in time worn, a consumable in what's left. Asking "how much lube is left" of a plug is meaningless and so is "how many hours did you wear the lube", so each kind is only ever asked the question it can answer.
- ๐ด
specsis typed JSON with a per-category schema, not thirty nullable columns and not a free-for-all. A plug has an insertable length and a base; a hood has a vision type; a lube has a base the app checks against your toys' materials. As columns it is ~40 fields of which any row uses six and every new category is a migration; as free-form JSON,6.5,"6.5 inches"and"165mm"all land in the same key and no total is ever computable again.specs.pyis the only door, and an absent key means nobody recorded it โ nothing writes a 0 for an unmeasured value. - ๐ด Every length is stored in millimetres and shown in both units. A column that sometimes holds inches is the oldest unit bug there is and it is invisible on inspection: 6 and 152 are both plausible lengths for the same object. Insertables are entered in inches because that is what the box says; rings and gaps in millimetres because that is how they are sold.
- ๐ด A consumable's level is DERIVED from its log, never stored โ exactly as the lock timer is
derived from
events. Aqtycolumn would drift on the first correction and would make "how much lube did August cost" unanswerable about a number overwritten forty times. No stock history reads "level unknown", not "empty"; a shopping list padded with things nobody counted is a list you stop reading. - ๐ด A slider you do not move records NOTHING.
<input type="range">always submits its current value, so the obvious markup posts the midpoint for every question you never answered โ a fabricated 5, on a form whose own help text promises the opposite. The input therefore carries nonameuntil it is moved. With JS off nothing is ever named, so the failure mode is losing an answer, never inventing one. - ๐ด Whether you were caged is not a question. The app already knows, so it never asks and cannot be told otherwise โ which is what makes a caged orgasm an achievement rather than a self-report. It is stored on the bout, against the house style, because it is a claim about a moment and re-deriving it later would silently re-answer history every time a lock is backdated.
- ๐ด A stale bout is reported, never auto-closed. Closing one would invent an end time, therefore a duration, therefore a total, therefore a badge. The app does not know when you took it off and says so. Wear bouts are exempt โ a cage legitimately runs for weeks.
- ๐ด Discarding a bout deletes it rather than closing it at zero seconds, because a zero-second bout is a measurement: it would count as a use, sit on the calendar and feed the favourite-toy badge.
- One bout, many things. A hood and a collar and a plug is one bout, not three โ one row per item would invent three bouts from one hour and triple every total.
- The cage rides the lock. Name your cage and every lock opens a wear bout for it, closing on
unlock, so its worn hours are your locked hours. No column on
sessions, no second bookkeeping. A bout you started by hand survives an unlock untouched. - Care notes and lube compatibility, because the app already knows what everything is made of: porous materials can't be sterilised, silicone lube wrecks silicone toys, oil destroys latex. Silent when either side has no recorded material โ a warning invented from a blank field teaches you to ignore warnings.
- Every derived number returns nothing rather than zero. No cost per bout for a thing you
never priced or never used; no average for a thing never timed.
$0.00reads as "it owes you nothing", which is the opposite of "you don't know". - Photos per item, through the same door as every other picture in Latch. Delete is allowed only while an item has no history; after that, retire it โ deleting would rewrite totals you have already been shown.
The badge engine (core/badges.py)
In core, not inside kit, because the roadmap named it as the thing M7 Identity was blocked
on. Modules contribute rules through a badge_rules(conn) hook and the engine owns the rest.
- ๐ด A superlative is never awarded while its period is still running. "Favourite toy of August", decided on the 3rd, is a claim about three days wearing the name of a month โ and it looks identical to the real thing. August's trophy lands on the 1st of September.
- ๐ด A decided period stays decided. Latch has a retroactive log, so an August bout can be entered in October; a late entry must not move a trophy already given or mint a second holder of the same month.
- ๐ด The UNIQUE had to be an expression index over
COALESCE(subject_id, -1). SQLite treats NULLs as distinct, so a plainUNIQUE (โฆ, subject_id, โฆ)enforced nothing at all for every badge the pup wears โ present, correct in shape, and silently inert for half the table. Found by running the app; the idempotency test that was meant to cover it passed vacuously on a fixture that only ever produced item badges. - Subjects are polymorphic โ a badge a thing wears and a badge the pup wears are one
record โ and
detailis frozen evidence, so a trophy does not quietly restate itself as the rows underneath it move. - A tie is awarded to everyone tied; a month with one lonely bout crowns nobody; and only a toy can be credited with making you come, because a bout holds a hood and a collar too and nothing says which of them did it.
- 16 badges to start, a cabinet at /badges that shows the whole catalogue rather than only
what you have won, and the
blurbon each rule is what the page renders โ so the page cannot drift from the rule.
Also
[hidden]{display:none!important}โ the HTMLhiddenattribute loses to anydisplayrule, so a hidden.rowrenders in full with the attribute correctly set. Used in five places across the app, backed by nothing until now.- Two more spreadsheets (
kit.csv,bouts.csv) and five more tables in the JSON export; the share export excludes every kit table by construction, with one opt-in per item for a name. - Five new activities (play, gear, came, edged, badges), so all of it lands on the calendar, the heat strips and the stat tokens with no extra code.
- 53 new tests (367 total).