Software

Failures

Three projects that did not become what they were built to be. Each was finished enough to know exactly where it stops, and the place each one stops is the most useful thing it produced.

Every screenshot below is the application running on this machine. What the interfaces say about their own limits is their own copy, not mine.

GSI Posting Watcher

2026 · Sole author

A watcher for graduate-instructor postings: it polls a university career-interest page and a departmental listings site, recognizes which results it has already seen, and emails only the new ones. It works. It never went anywhere it would have been useful.

Python · FastAPI · SQLite · APScheduler

The part that was actually hard

Scraping two sites is the easy half. The half that decides whether a watcher is worth having is identity: what makes two scraped rows the same posting across runs. Get it wrong in one direction and the same listing arrives by email every hour; wrong in the other and a genuinely new posting is swallowed as a duplicate.

The answer here is a stable key derived from the job opening ID where the source publishes one and from the URL where it does not, held in SQLite alongside a first-seen timestamp. That is the piece that survives whatever the transport turns out to be, and it is the reason the results table can say when it first saw something rather than only that it is there now.

The GSI Posting Watcher web interface on its Polling Results tab, listing six clearly-labelled sample postings in a table with title, source, job ID, department, location, status, course and first-seen columns, above a Check for new postings button.

The shape that kept it off a server

The scheduler runs inside the web process. The deployment notes are candid about what that costs: multiple workers would duplicate every scheduled job, so the application is pinned to a single worker to keep scheduling predictable, and the note adds that a production version should move scheduled jobs to their own process or take a distributed lock.

A job watcher is only worth running somewhere that is not a laptop, and this one is shaped so that the obvious way to deploy it is the way that breaks it. That is the lesson, and it is the opposite of the one above: the in-process scheduler was the convenient thing to build and the single reason the application could not be run the way a watcher needs to be. Why it stopped short of being deployed at all is not recorded anywhere in the repository, and this page does not invent a reason for it.

The Settings tab of GSI Posting Watcher, showing an empty admin token field, notification settings for email, source preference, immediate or daily-digest frequency and digest hour, and a scheduler check interval fixed to one of a small set of allowed values.

Cross-Modal Instrument

2026 · Design and implementation

A research instrument asking whether a handful of perceptual words — texture, density, motion, instability, persistence — can be one vocabulary for sound and for moving image at once. It listens to real audio three different ways. It has never had a moving image to look at.

C++ · iPlug2 · Metal · Core Audio · Real-time DSP

One vocabulary, two engines

The question the whole project is built around is whether perceptually meaningful concepts can provide a shared interaction vocabulary for manipulating sound and moving image. Five words are the answer it proposes, and they are frozen in a contract the build checks: Texture, Density, Motion, Instability, Persistence. Each one drives a DSP chain on the audio thread and a nine-pass render graph on the GPU, through two pure mapping functions that know nothing about each other.

That is the claim worth testing. Not that the two can be linked — anything can be linked to anything — but that one word can mean the same thing in both places well enough that a performer stops thinking about which half they are moving.

The Cross-Modal Instrument control panel floating over its own full-screen picture output. The panel shows the five perceptual faders — Texture 0.55, Density 0.66, Motion 0.50, Instability 0.41, Persistence 0.74 — above snapshot and bypass controls, and the output window behind it renders a grained, cell-structured colour field.

It says what it is doing, in words

Across the stage the instrument restates its own position as a sentence — “grainy surface, dense structure, moving, unsettled, holding the past”. It is not decoration. A shared vocabulary is only shared if both halves agree what a word means, and the sentence is the instrument committing to a reading out loud, in the same five words, where a performer can disagree with it.

The two screenshots on this page were taken minutes apart with every fader in the same place, and the sentence is not the same in both: above, the last clause reads “long trails”. Persistence is the reason, and it says so — the fader is marked because a route is offsetting it, so the value under the hand is 0.74 and the caret beside it is where the routing has actually taken it. A control that is being modulated tells you, rather than moving under you.

The Perform surface of the Cross-Modal Instrument: a PICTURE panel reading “grainy surface - dense structure - moving - unsettled - holding the past”, five labelled faders below it, and a row of snapshot, Capture, Modulation, Reset and Bypass controls. The Persistence fader is labelled in blue with modulation markers beside its handle.

Routing that refuses to be magic

A route connects a measured feature of one engine to a macro on the other, and every route states its own conditioning: the curve, the smoothing time, whether it is in a feedback loop. Audio Level reaches the picture’s Motion through an ease-in over 90 ms; the picture’s own Visual Motion reaches the sound’s Texture through a smoothstep over 180 ms. The header counts what is wired, what is running and what was rejected.

The rule underneath is the one I would keep: a route offsets a macro and never writes it. Switch a route off and the macro returns to the control immediately, because the control was never overwritten in the first place. Cross-modal mapping is usually where these projects become unexplainable, and this is the part that did not.

The Routes tab of the instrument’s Research surface, reporting “3 wired, 3 running, 0 rejected, 0 on a feedback loop. Analyzer tap: input.” Below it three route rows: Audio Level to picture / Motion with ease-in over 90 ms, Visual Motion to sound / Texture with smoothstep over 180 ms, and Modulator A to both / Persistence, linear over 120 ms.

Showing its working

The Research readout is the whole machine with the lid off: every macro as held by the performer and as each engine actually received it, with the routing offset printed separately for sound and for picture. Persistence held at 0.737 arrives at the sound as 0.799 and at the picture as 1.000, and the readout says which route did it.

Underneath, what each dimension is currently doing in units the engines use — grain, quantize, cell depth, pixels of travel, seconds of trail — plus frame timing and the guards that catch a runaway. An instrument that cannot be asked what it just did is an instrument nobody can argue with, and this one answers.

The Research readout of the Cross-Modal Instrument, a monospace panel listing each macro as held, as sent to the sound and as sent to the picture with separate offsets; a source-analysis line where every audio feature reads 0.000; the three active routes; the picture engine’s current grain, quantize, cell depth, pixel travel and trail length; and frame timing of 17.04 ms per frame.

The half it could never point at the world

The audio side is real and was validated three separate ways: a Core Audio device captured for sixty seconds with no short reads across forty-five thousand blocks, another application captured through a process tap, and a DAW track read through the host in Ableton Live. The picture side has no input at all. Its frame sources are a neutral still, an animated synthetic test pattern, and a still image file behind a command-line flag — the disabled third button above. The architecture names a camera and puts it under “later”.

So the visual features that drive routes back into the sound are measured off a pattern the instrument drew itself. The project’s own workflow notes say it plainly about the test pattern: it moves on its own, and that movement is the source rather than the instrument. A cross-modal instrument with one modality wired to the world and the other wired to a placeholder can demonstrate the mechanism and cannot answer the question it was built to ask.

The Settings surface of the Cross-Modal Instrument, showing a Picture window toggle and a frame source row with three options — Static, Test pattern (selected) and Still image, which is greyed out and unavailable — above an Audio device button noting that the host owns the device.

Where it stopped

Everything around it stopped in the same disciplined, unfinished way. Phase 09, the DAW-integration gate, is still open: the VST3 target builds and Ableton Live scans, loads and instantiates it, but the build refuses to turn the plug-in on ahead of the gate and the decision was deliberately left for a coordinator rather than taken quietly. The macOS products are ad-hoc, linker-signed and thin arm64 — finished and unsigned, which means they run on the machine that built them and nowhere else. The cross-platform parity corpus has its Metal half and not its Direct3D one.

None of that is what makes it a failure, and the project is careful to say so. The failure is the asymmetry: I built the explainability, the routing discipline and the audio capture to a standard I am still happy with, and never built the one input that would have let anyone test the actual hypothesis. The lesson is about ordering — the thing that proves or kills the premise should be the first thing built, not the one left behind an interface marked “later”.

Buried Lies

2026 · Sole author

A cross-platform DJ library consolidator, meant to reconcile the copy of a collection that lives on a laptop with the copies exported to performance USBs. It reads one of those two sides. The other is a format it is not allowed to open.

C# · .NET · Avalonia · SQLite · rekordbox

What it had to be

A DJ collection ends up in more than one place: the rekordbox library on the laptop, and whatever was exported to the USB that actually went to the club. The two drift apart — cues set on the device, play counts, playlists built on the night — and no tool merges them without picking a winner and discarding the rest.

The rule the whole design hangs on is that a source is evidence. Nothing is ever opened for writing. The Sources screen does not import a library so much as examine one and report what it found: what the file was detected as, that it was opened read-only, how many tracks, playlists, cue marks and tempo markers it holds, and which attributes it refuses to interpret because rekordbox declares them without ever defining their format.

The Buried Lies desktop application on its Sources screen, with the Sources, Matches, Compare / Merge, Playlists and Export navigation down the left edge. The screen reports a rekordbox XML library it has examined: detected format, a read-only access note, counts of tracks, playlists, POSITION_MARK and TEMPO elements, and a list of attributes carried as raw strings because their format is undefined.

The half of the library that cannot be read

No rekordbox device library can be read at all. export.pdb and exportExt.pdb are assessed and never opened, ANLZ analysis files are located and never interpreted, and the newer exportLibrary.db is detected and refused — it is encrypted header and all, and the only key provider this build ships supplies no key, which is a licensing decision rather than an engineering gap.

A scan of a real device therefore produces the device and the run, and no track, playlist, cue, beatgrid or play-count evidence whatsoever. Reconciling a laptop against a USB is the entire product; without the USB side there is nothing to reconcile. The formats are documented only from third-party reverse engineering, the repository holds no approved fixture, and manufacturing one from that material is forbidden by its own rules.

Refusing to guess, everywhere

The same discipline runs through the parts that do work. Matching prefers a false negative on purpose: an unmatched pair costs somebody a review, while a wrong match silently merges two different tracks’ cues and play counts and the damage surfaces months later. Every bucket reports its own count, and an empty one says why it is empty rather than implying agreement.

That is what makes the gaps legible instead of invisible. Each unavailable capability returns a coded refusal — shell.matches.no-queue-service, shell.matches.no-ruling-service — printed in the interface where the feature would be. The boundary between what is known and what is assumed is a thing you can see.

The Matches screen of Buried Lies, showing two coded refusals — shell.matches.no-queue-service and shell.matches.no-ruling-service — above the rule that matching prefers a false negative, and Exact, Strong, Needs review and Unmatched buckets all reporting zero pairs.

A finished pipeline with nothing to run on

The merge side is built. A merge records a decision and never erases the value it did not take: both sources’ values and their provenance stay in the workspace, so a choice can be revisited. Applying one writes only to the application’s own database — never to a rekordbox library, an exported device or an audio file.

What is missing is the service that would hand it a plan. The pipeline is complete and unreachable from the shell, which is the honest shape of the project: the hard reasoning exists, and the plumbing that would let a person use it stops at a boundary the reasoning cannot cross on its own.

The Compare / Merge screen of Buried Lies, reporting shell.merge.no-production-store, the rule that a merge records a decision without erasing the losing value, and a disabled Apply the merge button because no merge plan is loaded.

Where it stopped

Export is the clearest statement of the constraint. An export is only ever written to a destination named for it; a source library and a mounted performance device are never export targets, because both are held as immutable evidence. Validation would reparse the written file with the ingest adapters rather than ask the writer what it thinks it wrote.

So the tool can describe an export and cannot perform one, and could not write it back to the USB if it could. The project ran into a legal boundary, not a technical one, and stopped there rather than shipping something that quietly lost half of a collection.

The Export screen of Buried Lies, reporting shell.export.no-production-exporter, an empty export destination field, case-sensitive path comparison, and the export.no-device-target rule that exporting onto a performance device or back into a rekordbox library is not supported.