Latest Posts (20 found)

I was wrong about dark mode

Apple invented dark mode again! They’re calling the new feature Adaptive Dark Mode ™ . I’m learning that other browsers already had a similar feature . I assumed people relied on add-ons like Dark Reader (and millions do). Regardless, pushing what is effectively an accessibility option to the forefront goes a long way to help people respect it. My site used to have dark mode but I wasn’t happy with my colour choice and implementation. I redesigned my site back in Feb and removed it. Later I experimented with colour themes which I’ve since adapted for alternate blog banners . In that experiment I said: Personally I prefer reading with a dark theme for long-form content. Dark is not my brand though and I don’t believe every website needs to support colour scheme preference automatically. A good browser has reader mode, I use that all the time. “Just enabled reader mode!” What a load of rubbish. You have to understand that sometimes I just write stuff and I don’t read it back. This also contradicted my previous statement: That said, in [current year] brands with a digital presence — all of them? — should have a colour scheme for both. That’s probably more true. Certainly websites must work for forced dark mode. (Any site forcing itself upon users should go straight to jail.) So either provide a dark mode, or be prepared to test against every adaptive and forced dark mode implementation, or just let your website look broken. Probably easier to design a dark mode then? So that’s what I did, again. Maybe you noticed? If not, there’s a toggle button up top. At the risk of weighing in on the conversation I’ve opted for a two-state single button. I default to system preference. The mystery button flips between light and dark. Want to return to system default? Tough luck! You shouldn’t have clicked it. Delete local storage nerd! Technically I’m using a registered custom property with container style queries because it looks like fancy modern CSS. Three at-rules! Most of my light/dark theming is done by customs properties. The default light colours (A) are set on the root element. A style query applies the dark colours (B) on the element. For edge cases that don’t follow the A/B rule I can use one of two techniques. Either a style query inside a specific component: Or use the function: Note that I set to make aware of the user’s choice that might override their system preference. I wonder if I can do this once: Is that safe? Let me know! It’s not safe to do on the root element because browsers checks that for adaptive dark mode etc. I’ll stick to the verbose style queries for now until I’m sure. There’s a sprinkle of JavaScript to use local storage for the toggle button. It’d be better to use a cookie and a server-side template. My website is hosted on a static CDN and I’m not adding edge worker latency, so I’ll risk Flash of inAccurate coloR Theme (FART) . My site only risks a FART if a user’s choice doesn’t match their system preference. A week into my Noscript November experiment I see most websites are prone to a big FART because they rely entirely on JavaScript to enable dark mode. I’m not happy with my button using a hidden label. Bit of a design failure I need to address later. My original goal was to have multiple themes but I ran out of time. At the risk of soliciting replies to a question I didn’t ask, I would like your feedback on my choice of dark mode colours! Do they look nice? I don’t care whether dark mode makes your eyes bleed (turn it off) or you absolutely must have a 4-dimensional toggle. Stephanie Eckles wrote about the same CSS technique I use. Dave Rupert has explored the with variations. I saw Una Kravets is using with style queries to do something extra fancy. Stu Robson has written in depth on dynamic colour schemes too. If you read this entire post in an RSS reader and never clicked through, what a power move! Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views

The web used to belong somewhere

There are a couple of related ideas I found myself writing about here and there, either on the IRC server or on Quxnet . Ideas and thoughts about what the web used to be, how it used to feel, about how things have changed, and what can be done to bring at least some of the old web back. We often point fingers at the big social media platforms, with their relentless algorithmic curation as the cause for the awfulness of the modern web. Maybe we blame the SEO folks, or the influencers. There’s a multitude of different characters who each played their role in turning the “old web” into “today’s web”. And I’m not going to pretend they didn’t play a role, because they did. What I am going to argue for, though, is that there’s something else that has contributed a lot more to the changes that we’ve seen over the past few decades, and that’s the fact that the web used to belong somewhere. And I mean that in the physical sense. I’m not the first one to say this; you can find plenty of other posts out there making this exact point. But it’s a point worth repeating. There was a time when in order to go online you had to physically go to a specific place: a desk in the house, an internet cafe, a computer lab. The web wasn’t always with you, and you were not always in it and with it. You had to go somewhere, do what you needed to do, and then leave. BRB? AFK? Acronyms from what feels like a distant past. You are never away from the keyboard today, because there’s a phone following you around everywhere all the time. The internet feels exhausting these days because it’s always there. It’s become what feels like an inescapable presence, for better or for worse. I’ve been particularly unhappy over the past few weeks, for a variety of reasons. And one thing I noticed is that every time I’m in a down phase of life, I can go look at my phone usage and it’s inevitably higher than it should be. At least for me, there’s a 1:1 correlation between how good I feel and how much of a presence the phone is in my life. And the phone is just a proxy for the web because browsing is 95% of what I do on my phone, since I don’t game and I don’t have social apps. And that’s quite ironic considering I make websites for a living. I build for the web, and at the same time, spending more time than necessary on it makes me terribly unhappy. But it feels different when I can leave the web behind, when I can get up from my chair, leave my desk, and the web is no longer with me. That feels healthier. The problem is that most of the world doesn’t work that way anymore. If you leave and come back, you’ll probably have a hard time catching up with everything that has happened while you were away because everybody else is not taking a break. And so even if we managed somehow to get rid of algorithmic curation and infinite scrolls and all those things, the web would still not feel like the “old web”. Because the web used to belong to a place. It sadly doesn’t anymore. Thank you for using and believing in RSS. You're awesome. Send me an email :: Sign my guestbook :: Become a supporter

0 views

LLMs are terrible at designing AI systems

LLMs don’t know how to design software that uses LLMs. They’re comfortable writing normal programs, but if you throw an AI-shaped problem at them, they will usually screw it up. Why is that? For example, this post describes an attempt to vibe-code a memory tool that would flag when one memory contradicted another 1 . This is a great instance of a task that was virtually impossible five years ago but is now relatively trivial due to LLMs. You can simply task a LLM with scanning for contradictions, either in a big batch or pairwise 2 . However, when asked for the “state of the art” approach 3 , the AI agent in this case did not recommend LLMs. It started building a bewilderingly complex symbolic system to convert the memory items into structured logical atoms , then mechanically compare them with a solver. This, of course, did not and could not work, because there is no analytic way of determining whether any two natural-language sentences express the same proposition. The LLM knew this was impossible! When the user eventually lost patience and said “hey, why isn’t this working”, Claude immediately explained that this was a known 130-year-old problem with no solution. And yet it happily went around and around for weeks iterating on a dead end. I’ve run into this myself. When I was building Autodeck (AI-generated spaced repetition cards) my agents would routinely try to categorize cards by regex or keyword search instead of just making another LLM call. Why would this be happening? I have two obvious explanations and one deeper one. The first obvious explanation is that most training code is pre-LLMs . Assuming the labs aren’t feeding the current avalance of slop repositories directly into the models 4 , the vast majority of the code that models learn from was written before LLMs were an option. So it would make sense that they’d reach for non-LLM approaches, in the same way that they’d reach for jQuery over modern vanilla JS, or Backbone.js over React, right? Wrong, actually! LLMs don’t choose jQuery and Backbone. Whatever part of the training process pushes them towards up-to-date knowledge in other areas isn’t successfully pushing them towards LLMs, so this can’t be the whole story. The second obvious explanation is that RLVR is not a good environment to write code that calls LLMs . Much of the time LLMs spend training is spent in reinforcement learning environments where an automatic grader determines their performance (for instance, by running some code they wrote and checking the outputs). I don’t know this for a fact, but I would be surprised if these environments contained the credentials and API access for a LLM to call other LLMs 5 . So even if the pretrained LLM was comfortable writing that kind of code, the reinforcement learning process might discourage 6 it from doing so. The third explanation, which I like the most, is that current AI agents are just not very good at software design . On this explanation, picking a doomed symbolic approach instead of simply calling a LLM is not a weird aberration but a normal instance of AI agents picking the wrong tool for the job. I’ve seen this behavior from LLMs in other contexts too: trying to build complex (and doomed) consensus algorithms instead of tolerating stale data, introducing synchronous checks in event-driven systems, and so on. In the past I’ve chalked these up to alignment , but I’m open to the idea that they’re just ordinary errors of software design. One lesson I learn over and over again is that I can’t judge a LLM by the standards of a human. Any human programmer who was capable of implementing a sophisticated symbolic solver would know that it wouldn’t work, and so if they suggested that approach I’d assume 7 they had a good reason. Only a LLM could be both smart enough and dumb enough to make this mistake. I write a lot about the skills that are involved in working effectively with LLMs. I still stand by what I wrote in February 2025 : Using LLMs well involves extracting value from what they’re good at and avoiding wasting time at what they’re bad at. This is surprisingly hard, because of the point above. LLMs are good and bad seemingly at random: they will work hard for hours but sometimes be shockingly low-effort , they will draw on inhumanly broad expertise but fail to apply the most basic common-sense, they can successfully rewrite enormously complex programs but will often make very silly errors in design. If you can predict where they will do well or badly ahead of time, you can get enormous value from LLMs; if you can’t, you will likely waste a lot of time and tokens going down hopeless approaches. Here’s another example where the user “implied” that Opus 5.5 should use a judge model and it instead constructed a series of regex checks. Here I’d probably reach for a decision model . In hindsight, this was probably the key mistake, though I don’t know how you’d turn that into advice for non-technical vibe-coders. Never asking the agent about the state of the field is a mistake too. Put one more notch on the LLMs reward expertise belt. A fair assumption, I think: even though training on low-quality synthetic data clearly isn’t catastrophic for LLMs, it can’t be great . It would be expensive, slow, and risky. This theory also explains why LLMs are great at prompting other LLMs as subagents, or on training LLMs from scratch: they’re likely directly RL-ed and evaluated on that, unlike on writing code that calls LLMs. Well, okay, I wouldn’t — I’m arrogant enough to ask anyway — but I’d at least expect a good reason. Here’s another example where the user “implied” that Opus 5.5 should use a judge model and it instead constructed a series of regex checks. ↩ Here I’d probably reach for a decision model . ↩ In hindsight, this was probably the key mistake, though I don’t know how you’d turn that into advice for non-technical vibe-coders. Never asking the agent about the state of the field is a mistake too. Put one more notch on the LLMs reward expertise belt. ↩ A fair assumption, I think: even though training on low-quality synthetic data clearly isn’t catastrophic for LLMs, it can’t be great . ↩ It would be expensive, slow, and risky. ↩ This theory also explains why LLMs are great at prompting other LLMs as subagents, or on training LLMs from scratch: they’re likely directly RL-ed and evaluated on that, unlike on writing code that calls LLMs. ↩ Well, okay, I wouldn’t — I’m arrogant enough to ask anyway — but I’d at least expect a good reason. ↩

0 views

Upgrading my Airsoft rifle

I spent a pleasant morning at Red Alert, making a couple of small (but hopefully useful) upgrades to my main Airsoft rifle (MTW). I changed the routing of the air line for the HPA engine, routing it out the back of the rifle, under the buffer tube, moving it from down the hand grip. I also replaced the stock grip with a grip containing batteries and switch, so that I can power the HPA engine with that, rather than needing to install a Li-Po battery each time. These were reasonably straightforward upgrades, and I was keen to do them myself - to get the benefit of the learning experience - but I was also very keen to do it around people who know what they are doing. I also wanted to borrow an armourer’s wrench, as I do not have one, and I so rarely need one that buying my own seems a bit pointless. Luck was on my side, as the upgrades went smoothly. I dropped a small spring at one point, and I did not know from whence it came. It turns out that it was a spring which retains the small pin which keeps the large rear metal pin in place - it is inserted from near the stock at the back of the rifle, through a small hole, which I uncovered when I replaced some parts to re-route the air line. Fitting the upper and lower receiver back together, with the new, shorter, air line in place was a bit of a pain, but hopefully I am not going to need to open up the weapon too often, having made these changes. Perhaps the next time that I need to change the hop rubber? But not each week as I did before, to insert and remove the battery. I took the opportunity to make a slight adjustment to my HPA regulator’s output, giving me an increase of about 10 feet per second, while hopefully keeping me sufficiently clear of the upper limit for this weapon that I don’t need to tweak it down again when I use someone else’s chronograph when I next play. Lastly, I re-zeroed my red dot, as it was slightly out. I was doing pretty well at the end, hitting with reasonable accuracy out to 70m, so I look forward to giving this a go in a game very soon.

0 views
Unsung Today

“Over the years, countless amounts of speculation has come and gone.”

A video from Leon Massey about the community trying to figure out a secret in the 2012 videogame Fez: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/over-the-years-countless-amounts-of-speculation-has-come-and-gone/yt1-play.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/over-the-years-countless-amounts-of-speculation-has-come-and-gone/yt1-play.1600w.avif" type="image/avif"> This was an interesting video because the secret was figured out, but it was figured out in an interesting way – with slight help from the developer, followed by brute forcing the solution. We previously talked about some similar approaches. I once used a random number generator to stumble upon a solution rather than calculating it mathematically , and just recently we talked about a Zelda game and a concerted trial-and-error effort to see a very rare condition inside it . But in the case of Fez, the outcome ended up harder to get excited about: knowing the solution tainted the journey and made the whole outcome rather unsatisfying. To me, it echoed the current encounters with AI and the tradeoffs there. Seeking out help from an llm might get you to an answer faster – sometimes much faster! – but at the expense of understanding how the answer came to be, or having been through the experience of searching for that answer.

0 views
Unsung Today

An unexpected command palette

When I linked to the troubled New York City/Big Apple Game , at the very bottom I mentioned a web emulator of Atari I just discovered, called A8 Web . What was particularly interesting about this emulator that I haven’t seen before, is that it came equipped with a ⌘K command palette: I didn’t expect this kind of a professional UI in such a small piece of software, but I immediately embraced it – it not only gave access to various expected commands like “mute audio” or “resume emulation,” but also allowed to simulate all of the inputs. One of the challenges with emulation is that keyboards never match, special buttons like Start or Select don’t exist, and joysticks might not be connected. That might leave you truly stuck when the key absolutely need to keep going simply isn’t something you can press. Here, the creator thoughtfully adding all of those inputs to the command palette got me unstuck quickly. I am generally a big fan of command palettes as a UI layer underneath more visible layers. They are a nice way to enable flow, an interesting vector of discovery (just browsing the list above taught me a lot about what’s possible), and a good place to stash lesser-used commands that do not deserve more prominent places like menus or on-screen buttons. Done well, they’re useful with the keyboard and with the mouse. Some old professional apps like Photoshop that didn’t bother to update their core interface feel extra obsolete without a command palette today. A8 Web’s implementation isn’t perfect – it should probably be floating atop the emulated screen rather than sliding in from the side, and I would make the input commands not auto-close the sidebar – but it was such a positive surprise that made me rethink some things I thought I knew about command palettes.

0 views
Blog System/5 Yesterday

Unattended reboots with encrypted disks

Do you run a small home server or NAS holding your precious photos and sensitive documents? If so, I suppose the drives are encrypted, right? But then… can you reboot the machine without manually entering a passphrase before being able to access your files again? If you answered yes to the questions above—which is common for convenience—that implies the encryption keys live in the machine which… kinda defeats the purpose of disk encryption, doesn’t it? A thief can break into your house, lift the whole box (they are small these days, huh), plug it in elsewhere, and gain free rein over your files. So, which one is it? Simplicity and reliability by allowing automatic reboots or data protection by keeping the key in your head? With ZKeys—a little new project I’m launching today—you can have your cake and eat it too. TL;DR ZKeys is a managed service that stores encryption passphrases off-site. Client machines fetch the encryption keys from the service and keep those keys alive (unlocked) via periodic keepalive messages. If the server stops receiving those messages for a long period of time (e.g. because it has been unplugged as part of theft), access to the key is automatically locked. You get automatic, passwordless reboots while keeping your data protected against real incidents. Did you know that, by subscribing to Blog System/5, you support the creation of this content and this software? About a year ago, I was reinstalling my FreeBSD home server to act as my NAS after a short stint with a Synology box . During that process, I decided to enable encryption for the ZFS data pool. But as I did that, I faced the question: how do I allow the machine to automatically serve my data after a reboot? I could put the key in a local file, which FreeBSD supports via the ZFS option, but that would render disk encryption pretty much useless. So I had an idea: what if I put the key on a different machine away from my house? I could then make the NAS fetch the key to mount the drives. And if someone stole the server, I could wipe the remote key and make the data unreadable before they could plug it back in. I did post this to Twitter and there was some interest in the idea: But… did you notice the problem with what I said above? Wiping the key by hand after noticing something is wrong is problematic because the gap between those two events may be large. What if I’m on a long vacation and I have no means of noticing that the machine has been stolen? Game over. So I refined the idea: what if I had a service that stored the key remotely and required the consuming machine to send it frequent pings to keep the key unlocked? This way, if the machine were stolen, it’d likely be offline for more than a few minutes and the service could automatically determine that something is wrong and lock the key from future accesses. And that’s precisely what ZKeys does. Before getting into details, it’s important to get something out of the way early on: ZKeys is designed so that a compromise of the server is insufficient to retrieve or reconstruct your keys. This is done in two ways: Split keys: The ZKeys server only stores half of the passphrase used to unlock a volume. The other half remains on the client machine. The client program combines both halves to produce the whole passphrase. Encryption at rest: The keys are encrypted at rest with a password that the client must supply for retrieval and which is securely transmitted over HTTPS. The password only lives in the server’s memory for a brief period of time. The tradeoff is, simply, that you MUST back up the full encryption keys in a third location of your own (a paper in a safe, a password manager, you name it). If ZKeys goes down or you lose your remote key password, there is absolutely nothing I can do to help you. You have been warned. The setup instructions later on will instruct you how to back the key up. The ZKeys service is available at https://zkeys.jmmv.dev/ and the open-source client—along with system integrations for FreeBSD and Fedora—is in my jmmv/zkeys repository. Disclaimer: ZKeys is offered to you for free. I make no promises about its reliability, availability, or anything else. I am giving you access because I expect that supporting a handful of extra users won’t be a big issue. But if support becomes a burden, or if load becomes unmanageable, I’ll have to re-evaluate my choices here. Which means that, for now, you have to use the limited code to get started! The basic idea to set ZKeys up is the following: Install the client CLI tool. Create a service account. Create a new key (or onboard an existing key). Configure the keep-alive service on the target machine. Change or add the filesystem encryption key to be the one stored in ZKeys. Configure the auto-mount service on the target machine. That’s all it takes, but the steps are quite manual at the moment because no upstream OSes provide packages. The service documentation covers several scenarios in detail for FreeBSD and Fedora, so I’m not going to repeat them here. Demo of the integration between ZKeys and Fedora 44 with a fully encrypted root volume: the video starts by showing a fully unattended boot, followed by a manual lock of a key that causes Fedora to refuse to reboot on its own, and finishes with another automatic reboot after the key has been unlocked again. Now for the nerdy details, because that’s what you’re here for: The CLI is Rust, of course, but it has to come with a bunch of shell scripts to integrate with FreeBSD’s rc.d system and Fedora’s systemd and dracut. These integration points are the trickiest part of the client package and the hardest to validate automatically, which is why I only provide support for the two systems I use on a daily basis. It should be possible to integrate ZKeys with any other Unix-like OS, though. The CLI package is a frankenmonster made up of Cargo and GNU Automake/Autoconf because of the above. I believe system-level tools should abide by de facto standard tooling even if such standard tooling is deficient, so GNU Autotools it is. I run two non-replicated instances of the service: one in US West and one in EU West, because I have machines in both locations and both rely on encryption. I strongly suggest you use the US instance as I’m seeing frequent power hiccups in my EU deployment. The frontend uses Bootstrap, straight from the 2010s. Does it look ugly? Maybe, but I kinda like it and, most importantly, it’s functional . Of course I could have asked AI to produce yet-another-dull-site full of gradients and slop, but I did not want to do that, so I stuck to what I already knew how to do by hand from previous services. The admin page is an SPA, yes. This is an intentional design choice that forces a very clear separation between the backend APIs and the frontend. Whatever the current web frontend can do any other client can do as well. For example: the current CLI tool does not offer administrative operations but it could trivially be made to support them, because the APIs are right there. The backend is built on my own III-IV framework which I’ve been building up over the last few years while implementing a couple of other services. I’ve had to add a lot more features to it to make this possible though. Oops, I know that’s the name of a different effort , but you really need to encrypt your local drives. With computers becoming smaller and lighter every day, it’s easier than ever to steal them, and unfortunately home break-ins are common in some parts of the world. Block-level encryption is cheap in CPU terms, so do it. ZKeys just makes this process less cumbersome for headless servers. Once again, head to: https://zkeys.jmmv.dev/ to get started and use code to create an account! And if you are interested in helping out, I’d appreciate the creation of packages for FreeBSD and Fedora, and integration scripts for other systems. TL;DR ZKeys is a managed service that stores encryption passphrases off-site. Client machines fetch the encryption keys from the service and keep those keys alive (unlocked) via periodic keepalive messages. If the server stops receiving those messages for a long period of time (e.g. because it has been unplugged as part of theft), access to the key is automatically locked. You get automatic, passwordless reboots while keeping your data protected against real incidents. Did you know that, by subscribing to Blog System/5, you support the creation of this content and this software? Storytime About a year ago, I was reinstalling my FreeBSD home server to act as my NAS after a short stint with a Synology box . During that process, I decided to enable encryption for the ZFS data pool. But as I did that, I faced the question: how do I allow the machine to automatically serve my data after a reboot? I could put the key in a local file, which FreeBSD supports via the ZFS option, but that would render disk encryption pretty much useless. So I had an idea: what if I put the key on a different machine away from my house? I could then make the NAS fetch the key to mount the drives. And if someone stole the server, I could wipe the remote key and make the data unreadable before they could plug it back in. I did post this to Twitter and there was some interest in the idea: But… did you notice the problem with what I said above? Wiping the key by hand after noticing something is wrong is problematic because the gap between those two events may be large. What if I’m on a long vacation and I have no means of noticing that the machine has been stolen? Game over. So I refined the idea: what if I had a service that stored the key remotely and required the consuming machine to send it frequent pings to keep the key unlocked? This way, if the machine were stolen, it’d likely be offline for more than a few minutes and the service could automatically determine that something is wrong and lock the key from future accesses. And that’s precisely what ZKeys does. Zero-knowledge protection Before getting into details, it’s important to get something out of the way early on: ZKeys is designed so that a compromise of the server is insufficient to retrieve or reconstruct your keys. This is done in two ways: Split keys: The ZKeys server only stores half of the passphrase used to unlock a volume. The other half remains on the client machine. The client program combines both halves to produce the whole passphrase. Encryption at rest: The keys are encrypted at rest with a password that the client must supply for retrieval and which is securely transmitted over HTTPS. The password only lives in the server’s memory for a brief period of time. Install the client CLI tool. Create a service account. Create a new key (or onboard an existing key). Configure the keep-alive service on the target machine. Change or add the filesystem encryption key to be the one stored in ZKeys. Configure the auto-mount service on the target machine. The CLI is Rust, of course, but it has to come with a bunch of shell scripts to integrate with FreeBSD’s rc.d system and Fedora’s systemd and dracut. These integration points are the trickiest part of the client package and the hardest to validate automatically, which is why I only provide support for the two systems I use on a daily basis. It should be possible to integrate ZKeys with any other Unix-like OS, though. The CLI package is a frankenmonster made up of Cargo and GNU Automake/Autoconf because of the above. I believe system-level tools should abide by de facto standard tooling even if such standard tooling is deficient, so GNU Autotools it is. I run two non-replicated instances of the service: one in US West and one in EU West, because I have machines in both locations and both rely on encryption. I strongly suggest you use the US instance as I’m seeing frequent power hiccups in my EU deployment. The frontend uses Bootstrap, straight from the 2010s. Does it look ugly? Maybe, but I kinda like it and, most importantly, it’s functional . Of course I could have asked AI to produce yet-another-dull-site full of gradients and slop, but I did not want to do that, so I stuck to what I already knew how to do by hand from previous services. The admin page is an SPA, yes. This is an intentional design choice that forces a very clear separation between the backend APIs and the frontend. Whatever the current web frontend can do any other client can do as well. For example: the current CLI tool does not offer administrative operations but it could trivially be made to support them, because the APIs are right there. The backend is built on my own III-IV framework which I’ve been building up over the last few years while implementing a couple of other services. I’ve had to add a lot more features to it to make this possible though.

0 views
ava's blog Yesterday

chronic illness memes

I’m switching off of adalimumab and starting upadacitinib (Rinvoq)! Finally, no more weekly injections for now, but daily pills. Despite everything, I am currently pretty happy and I’m turning this crap year around. I feel accomplished at work and have been getting a lot of praise for my (new-ish) role as data protection coordinator of our department. I study a lot for my degree and feel pretty competent most of the time. I make plans for more certifications and classes in 2027, too. I’m back to volunteering more for noyb, and I’m looking for other part time opportunities. I have fun playing FFXIV, and I’m writing some blog posts again. I’m slowly digging through my emails and will hopefully reply soon. I’m currently at a friend’s place :) While I write some more drafts… have some memes I found online. Published 07 Nov, 2026

0 views
Unsung Yesterday

Crammed and dangerous

This is what happens when I search through photos on my iPhone: I’ll repeat and emphasize the last frame below, because it’s a resting state that doesn’t offer much rest. I am happy with the train results and I just want to look at them in peace – but now there’s a pop-up in the way. What would you do to get rid of it? = 3x)" srcset="https://unsung.aresluna.org/_media/crammed-and-dangerous/2-framed.1600w.avif" type="image/avif"> Trying to swipe it away doesn’t work: Tapping on the available largest surface – the photos – makes the photo you tap on take over the entire screen, which doesn’t help: Tapping on the input field replaces it with another restless pop-up: Tapping on X clears the search, so that’s not good: Tapping on the other X just leaves the search altogether: Tapping on the keyboard continues typing and adds to “train,” in effect going elsewhere, too: Noticing that one of the entries in the pop-up is “train” and tapping on it works – sort of – but it replaces the input field with a token, and slightly adjusts search results, which are both worrisome in their own way: Okay, there are three answers. None of them feels great. One is to drag the photo chute down and then up: While I sometimes can enjoy the kind of interface action that is effectively “unplug it and plug it back again,” here it doesn’t feel natural, and the space to perform is limited, too. And speaking of limited space… the other solution is to tap on one of the few small areas that are actually truly inactive – the Search header, or the Top Results: Lastly, I believe the most appropriate answer is pressing the blue loupe key (effectively the equivalent of Enter): Why doesn’t this path feel intuitive? Because the results already appear as I type. The rest of the flow itself suggests Enter isn’t necessary – and it’s not as natural for me to think of Enter as solely a “keyboard hider.” Despite both human factors and computers starting as military projects, I don’t love military metaphors – yet, this feels like a good example of a minefield interface. This is because arriving at this kind of a claustrophobic screen feels dangerous; any misstep can derail the whole experience, sending you to another place you have to exit as well. I wish I could tell you what’s my yardstick to separate a regular UI where some tap targets just do expected things, and one where there so many they all become mines. All of these above, in isolation, make sense, but they’re adding to what feels like a pretty unpleasant, anxiety-inducing interface. I think this example highlights the value of intentionally leaving some areas of the interface that feel “safe”… …although you could also say that in this case, that would be treating symptoms. I think the real problem is that in response to my search, the UI did two different things – one was the photos, the other the suggestion pop-up – and overlapped the results of those things. The pop-up feels strangely “ growth hack-y ” to me, as if the UI was optimized for constant forward momentum, nuding me to tap on new searches over and over until the batteries run out. It made me feel there was something wrong with my search for “train,” when it was a perfectly valid, innocuous search. This is a wider pattern I don’t love and/or understand. Here is an example of the same thing used in Notes on a Mac as well – and it’s even worse, because the suggestions are bad, and because the pop-up always covers the most important result: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/crammed-and-dangerous/13.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/crammed-and-dangerous/13.1600w.avif" type="image/avif"> At least here, I am at still equipped with my treasured Esc key, which works reliably in a lot of situations like this – a jetpack to jump over the minefield – but that’s a small consolation prize. (Enter works here too, but see above. Also, the pop-up reappears aggressively even on arrow key presses!) An interface that feels like a minefield isn’t great. An interface where you have to over and over again keep escaping something to return to a resting state isn’t great, either.

0 views
Langur Monkey Yesterday

A maximally tiny language model

In this post I document a little experiment I have been playing around with for a while that explores an absolutely minimal language model. I’m specifically interested in how small a language model can be made while still doing something . I got the idea while I was reading a post about GPT-2. A pre-COVID model, GPT-2 now seems minuscule in comparison to current frontier models. Its largest incarnation was trained on about 40 GB of text, had 1.5B parameters and used an embedding dimension of 1600. Nowadays, a GPT-2-like model can easily be trained in a consumer GPU with ~16-24 GB of VRAM. If the GPT-2 family of models pales in comparison to current model dimensions, the model I present here today pales in comparison to GPT-2. Since it is extremely minimal, it can be trained on a single CPU core in a few seconds and needs very little RAM. It can have single-digit embedding dimensions, does not follow the transformer architecture, has no attention mechanism, and may not even use any hidden layers! It is a purely academic model, in the sense that it is absolutely simple but does nothing useful . I’ll call the language model in this post TinyLM . Below is a summary of some of the features and concepts that this post touches on: Since it is so easy to train and test, the model is fluid. It can have between a few hundred and a few thousand parameters, depending on the dimensionality of the embeddings and the size of the hidden layer. But let’s start at the beginning. Such a small model wouldn’t fare very well trying to reproduce human language, so I reduced it to a set of predefined sentences. I call this set our closed world. This world is composed of only 92 words: 8 proper names, 12 actors, 6 prepositions, 28 places, one determiner, 24 objects, and 13 verbs. Everything that can be expressed is done through those 92 words. I combined them using some rules and got the 2560 possible sentences that constitute our world. This is the whole world. This is the training dataset. This is everything TinyLM will ever be able to learn and recognize. Every sentence in the world has the same shape: a subject, a verb, an object, and sometimes a place. Subjects are always people. There are 20 of them, either a bare name (“john”, “mary”, “lucia”) or “the” plus an occupation (“the chef”, “the pilot”, “the farmer”). The verbs come in two families. Four of them (“helped”, “met”, “thanked”, “invited”) are things you do to a person, so the object is another person and never the subject itself: “lucia invited mary” is in the world, “lucia invited lucia” is not. The other nine are things you do to stuff, and each one only takes objects of the right kind. You drink drinks, you eat food, you repair tools and homeware, you pack clothes and drinks. So “the chef ate soup” exists, “the farmer repaired the soup” does not. Finally, about half of the sentences end with a place: “near the bridge”, “inside the cellar”, “under the tower”. That’s the whole grammar. No questions, no negation, no conjunctions, no subordinate clauses, no adjectives. Just subject, verb, object, and sometimes where. A few real sentences out of the 2560, so you can see the shape: The whole model is two small tables of numbers. The first one gives every word a handful of coordinates: three numbers per word in the default model, so \(92 \times 3 = 276\). The second one, the readout, says how much each word likes each of those three directions, another 276. That’s it, 552 numbers in total. There is no lookup table of sentences and no rules inside, just those numbers. All of them are learned from the world above. The pipeline for guessing a word is almost embarrassingly simple: The architecture 92 counters go in, the \(92 \times 3\) coordinate table turns them into 3 numbers, the readout scores all 92 words, and softmax turns the scores into percentages. That is the whole default model: 552 numbers and no hidden layer. The same three steps, written as matrix operations. Count the words of the sentence into a vector \(\mathbf{c}\) with one entry per word in the world, and let \(\mathbf{E}\) be the table of coordinates, 92 rows of 3 numbers. The sentence collapses into a single point by adding the rows of the words that appear, the readout \(\mathbf{W}\) scores that point against every word, and a softmax turns the scores into percentages: $$ \mathbf{h} = \mathbf{c}^{\top} \mathbf{E} \qquad \mathbf{z} = \mathbf{W} \mathbf{h} \qquad \mathbf{p} = \operatorname{softmax}(\mathbf{z}) $$ with \(\mathbf{E} \in \mathbb{R}^{92 \times 3}\) and \(\mathbf{W} \in \mathbb{R}^{3 \times 92}\), so \(\mathbf{h}\) is a column of three numbers, \(\mathbf{z}\) (logits) is a column of 92 scores and \(\mathbf{p}\) is the column of 92 probabilities that this post keeps calling percentages. Each row of \(\mathbf{W}\) belongs to one word and says how much that word likes each of the three directions, which is the readout table above read along the other axis. The code keeps it transposed, as a \(3 \times 92\), which changes nothing except the order of the indices. The model in one picture The whole thing with real numbers for “john drank”. A row of 92 counters, the (92 times 3) table of coordinates, the two rows that matter added into one point, the readout scoring all 92 words, and the softmax at the end. The bottom half adds up the coordinates of “john” and “drank” by hand and shows why the four drinks end up within a tenth of a point of each other. The middle step is worth a second look. The sentence is turned into a row of 92 counters, one per word, saying how many times each appears. Two words show up, so two counters are 1 and the other ninety are 0. Looking up the coordinates of each present word and adding them gives a single point, and that point is the model’s entire idea of what the sentence is about so far. Then each of the 92 words gets a score for how well it lines up with that point, and the highest score wins. Three multiplications and two additions per candidate word, and that is the whole network. This is where the “bag of words” in the summary comes from. The input is a bag of counters, and counting does not care about order. Adding numbers does not care either: 3 + 5 is the same as 5 + 3. So “the teacher drank” and “the drank teacher” land on exactly the same point, get exactly the same scores, and the model happily finishes the nonsense one. The optional hidden layer is the one part that is not just adding. It sits between the point and the scores and turns the single “how well does this word line up” question into a set of small yes/no questions about the context, something like “was john seen?” and “was cleaned seen?”. Because each of those answers can be silent when it does not apply, the model can score a word high only when both conditions hold, which plain adding cannot express. In symbols, the readout becomes $$ \mathbf{z} = \mathbf{W}_2 \operatorname{ReLU}(\mathbf{W}_1 \mathbf{h} + \mathbf{b}_1) + \mathbf{b}_2 $$ The same picture with one ReLU layer The same model with 16 numbers per word and a hidden layer of 128 units: the point becomes 16 numbers, W1 turns it into 128, the ReLU keeps the positive ones, W2 scores the 92 words, and softmax does the rest. 15516 numbers in total, and this is the version that reaches the ceiling. with \(\mathbf{W}_1\) of size \(3 \times k\) and \(\mathbf{W}_2\) of size \(k \times 92\), plus one bias per hidden unit and one per word. A narrower point also limits how much the hidden layer can help, which is why the interesting knob turns out to be the number of coordinates per word rather than the size of the hidden layer. As we’ll see shortly, at 16 coordinates per word and 128 hidden units the model has 15516 parameters and reaches the ceiling. The default is still the 552-number version (because the point of the experiment is the smallest thing that works!). Nobody can score 100% here, and the reason is that the world is non-deterministic by design. After “john drank” any of the four drinks fits, and nothing in the sentence says which one you meant. On average a bag of words has 2.72 reasonable continuations, so a perfect player that always answers with the most likely one gets 48.4%. That is the ceiling. Our 552-number model gets 29.2%, roughly three fifths of what is knowable. The same ceiling applies whether or not you can see word order, because no two sentences in the world share the same set of words, so order carries no extra information here. The 92 rows of the coordinate table start out as plain random numbers, drawn from a bell curve. There is no meaning in them, and nothing in the world tells the model what any word is. The only feedback training ever gives it is “given the words so far, the next word was this one”. That is the entire supervision signal. The model is never informed that “wine” and “beer” are both drinks, or that “the chef” and “the pilot” are both people. Those facts are not in the input, they are in the structure of which sentences exist and which do not, and the model kind of picks that up indirectly. After 3000 small steps of nudging those numbers a little to the left or to the right to make the right next word a bit more likely, here is what the table looks like. All 92 words, three numbers each, plotted as points in space: The learned embeddings The three coordinates of all 92 words after training, one colour per kind. Labels are only drawn on things and verbs, for legibility. Nothing told the model which words belong together. The words sort themselves out by the job they do in a sentence. The nine thing-verbs (red) fly off to the top, far from everything and from each other, because a verb only ever follows a person phrase and never follows an object. The four person-verbs (orange) stay near the middle, next to the names and occupations (blues), which crowd around the centre since those show up in every slot. The 24 things flatten into a disc at the bottom, and inside that disc the drinks sit with the drinks, the tools with the tools, the paper with the paper. Even the article “the” ends up alone, because it is unlike any other thing in our world. This clustering is not just something the eye wants to see. Running k-means with six clusters over the 24 thing embeddings recovers the true classes 70.8% of the time , against 16.7% for chance. The drinks come back perfect, with “wine”, “water”, “soda” and “beer” in one cluster. Tools and clothes merge into one, which is fair enough, since both get cleaned and packed by the same verbs. The 3-dimensionality of the embeddings just happens to be small enough so that we can draw them in a simple 3D scatterplot. Moving to higher dimensions to improve accuracy destroys this nice property. But how do we fill in \(\mathbf{W}\) and \(\mathbf{E}\)? The model is initially populated with random values. Training is the part that fills these, and it uses the whole world: all 2560 sentences, every prefix of each one, 11408 next-word questions of the form “john drank” -> water. There is no split and nothing held back. The question here is how much of the world we can fit into those 552 numbers. The model will never generalize to unseen text, as there is no unseen text at all! In a sense, the model is a compressor for our world. The loop does one thing at a time. Show the model a real prefix and the word that actually came next, compare its 92 percentages with the truth using cross-entropy, ask backpropagation which way to nudge each of the 552 numbers to make the loss smaller, nudge them a little with Adam, repeat. In code that is four lines, because the whole model is one multiplication, another multiplication and a softmax, so the effect of every number on the loss can be worked out exactly and all at once. Nothing is ever written down as a rule. “Drinks follow drank” is not in the code, it is only in the numbers the training ends up choosing. 3000 turns of that take 18 seconds on one CPU core. The first hundred carry almost everything: the loss falls from 6.56, which is guessing, to 2.46 and 26% accuracy. Step 300 has it at 2.04 and 28.6%, and the remaining 2700 steps buy six tenths of a point, ending at 1.99 and 29.2%. From there on the model has taken what a bag of words has to give, and the nudges only reshuffle answers that were already near-tied. Where the accuracy sits is worth a look, because it points straight at the difficulty. The word “the” is predicted 100% of the time, and rightly so: it is the only word that ever follows a preposition, so when a place phrase starts there is nothing to get wrong. Verbs come out at 14.8% and the things at 9.1%, and those are exactly the slots where several words fit equally well. That is the ceiling again, seen from the other side: the model is not failing at those, the world is ambiguous there. Make it wider, give it 16 numbers per word and a hidden layer, and it climbs from these 29.2% to the ceiling itself, which is what the next section is about. TinyLM is implemented in PyTorch. The whole experiment, logs and all, is at https://codeberg.org/langurmonkey/tinylm . Every number in this section comes from a run in , all with the same learning rate (0.03), the same 3000 steps and seed 0. The baseline is the 552-number model from above: 29.2% next-word accuracy, against a ceiling of 48.4%. There are two knobs to turn, so let me turn them one at a time. First, fix the embeddings at 3 numbers per word and grow the hidden layer from nothing to 128: The first thing to notice is how flat the left half is. Going from 0 to 2 hidden units does nothing at all, in fact it loses a tenth of a point, and 4 and 8 buy so little that they are inside the noise. The hidden layer only starts to pay somewhere around 16, and even then the whole sweep, a model 23 times bigger, gets from 29.2% to 34.9%. That is not the hidden layer’s fault but what (little) it has to work with. There is only a point of three numbers going in, so the yes/no questions the hidden units can ask are questions about three numbers. Everything the model knows about a word is already squeezed into those three numbers before the layer gets a chance to help. Now fix the hidden layer at 128 and grow the embeddings instead: This is a different picture. Four extra coordinates, 220 more numbers, are worth four points, and at 8 numbers per word the model is at 47.4%, basically at the ceiling. At 16 it sits exactly on it, 48.4%, and then nothing happens: 32 and 64 coordinates give the identical number, with the same loss to four decimals. All the extra numbers do is cost time. The model has run out of things to learn, because 48.4% is a property of the world. Two rows caught my eye: embedding dimensions 16 and 92. At 16 coordinates the positions go to 100%: every preposition that starts a place phrase is now predicted perfectly, whereas at 3 coordinates it was 42.3%. And the last row, 92 coordinates, which is one number per word with no sharing between words, falls back to 44.5%. With a coordinate per word each word is just itself and nothing has to be shared, so the model stops learning that “wine” and “beer” behave alike and learns 92 independent words instead. Fewer is genuinely better here, up to a point. So the answer to the opening question is 15516 numbers, of which 552 were enough for a quarter of the world and none of them buy anything past the entry at 16. The smallest thing that works is still the 552-number model. The smallest thing that hits the ceiling is the one with 16 coordinates per word. Accuracy tells you how often the model is right, but the model is a description of the world, so we can ask the compressor’s question: how many bits does it take to send the whole world using this model? That is the model itself plus the residual, the part the model cannot predict, which the loss measures directly: Only the first row compresses: 0.68× raw. Every row past it spends far more bits on the model than it ever saves on the residual, which bottoms out around 23-24 kbit once the ceiling is reached. I kind of expected this: the smallest thing that compresses is the 552-number baseline, while the smallest thing that predicts at the ceiling is the 15516-number model. Past 16 coordinates even the residual grows again, so the 92-coordinate case is totally hopeless. TinyLM works because the world is tiny and closed: 92 words, one sentence shape, 11408 questions with nothing held back. To get from here to something GPT-2-like, which predicts arbitrary text, roughly four things have to change, all of them about giving up that closedness. Nothing else changes. The objective is still "guess the next word", the loss still means nats, word embeddings are still there, and the compressor’s accounting from the previous section still applies. What changes is only what the model is asked to compress. A closed world. 92 words, a few hard rules, 2560 sentences. That is all the language there is. Learned embeddings. Nobody tells the model that “wine” and “beer” belong together. It works it out from the sentences alone. A bag of words. Words go in unordered, so “the chef met the clerk” and “the clerk met the chef” look exactly the same to the model. A ceiling of about 48.4%. Throwing the order away caps accuracy, no matter how big the model gets. A few hundred to a few thousand parameters , with the embedding size and the hidden layer size as free knobs. A sweep over those two knobs , from the baseline all the way up to the ceiling. – person verb, subject and object both people, place at the end. – the same, without the place. – thing verb, and the object is a drink because “drank” only takes drinks. – thing verb, object from the paper class. And two that are not in the world: , because you cannot invite yourself, and , because soup is not a tool. Model cost: parameters \(\times\) 32 bits (unquantized float32, an upper bound). Residual: \(N \times loss / ln(2)\) bits, with N = 11408 next-word questions. Raw world: \(N \times log_{2}(92)\) ≈ 74421 bits (9.1 KiB), i.e. naming one of 92 words per question with no model at all. The first is obvious: order. The bag of counters has to go, replaced by a sequence with positions and a mechanism that looks at who came before whom: causal self-attention, stacked into a transformer. That alone removes the ceiling. The second is also rather self-evident: vocabulary. 92 whole words become ~50k subword pieces (BPE), so the model can spell anything, at the cost of a readout 500 times wider. The third is data. Training on everything there is has to become training on gigabytes with text held back, so the score measures generalization instead of memorization. Finally, we have scale. One hidden layer becomes 12 layers of 768 numbers (GPT-2 small, 124M parameters), and seconds on a CPU core become hours on a GPU.

0 views
Sean Goedecke Yesterday

Working with designers

I love working with designers, in part because (unlike managers and product managers) they’re culturally different from engineers. However, having more than one designer on a team is asking for trouble, and I have complicated negative thoughts about design systems. Designers demonstrate their value immediately. While working on some feature, I’ll typically put together some placeholder UI that I’m reasonably happy with. I then show it to a designer, who strokes their chin and says “hmm, I have some ideas here”. They then go away and produce a mockup UI that is not just clearly better, but better in ways I could not have imagined. There aren’t any other roles at a tech company where you can see the value at a glance. Designers are an unusual role in other ways, too. Engineering managers, product managers, and engineers all come from broadly the same stock: technical, detail-oriented planners. We are people of the spreadsheet. In fact, many managers and product managers were once engineers: it’s a natural transition. Not so for designers. I’m sure they exist, but I’ve never met any designers who started out as engineers. Because of this, designers are culturally distant from engineers. In my experience, this is a big positive. I’ve complained before that product managers who were once engineers tend to be worse, not better. Once you start trying to make your own secret technical assessments instead of finding a way to trust your engineering team, you’re doomed: you won’t be able to do it well enough, and the fact that you’re trying will make it impossible to build the trust you need. Designers don’t have this problem at all 1 . So far I have made designers sound like the elves of Middle-earth: mysterious creatures who glide around tech companies dispensing magic. In fact, they are more like the Slavic household spirits, or Domovoy . If you have one Domovoy in a household, it will protect your home against misfortune. If you ever have more than one, they will fight bitterly : “making all sorts of noises, injuring the cattle, turning the master out of his bed, choking people while asleep, etc”. Likewise, a designer can turn your scrappy product into something really beautiful — but you had better have only one designer, otherwise you’re asking for trouble. The worst experience I have ever had working with designers was on a product where we had three of them: initially just one, who then rotated off the project and was replaced by two very senior designers. As far as I could tell, the first design was pretty good. Unfortunately, the new designers didn’t feel the same way. The very first thing they did was completely redesign the product, which meant I had to rewrite most of the frontend styles. Then they began disagreeing with each other. I think we went back on each page of the product three or four times: one designer would tell me to change X, then the other would change it back, and so on. In the meantime, I was desperately trying to get the product to work. I knew what I was supposed to do in theory: get everyone in a room talking to each other and hash out a decision. Maybe this would have worked on a different product where the requirements were more stable, but it didn’t work here. The differences in taste, while imperceptible to me, were just too deep. It seemed like everything Designer A produced was slightly wrong in Designer B’s eyes, and vice versa. In the end we just powered through by implementing 2 each new iteration until about a week before the deadline, at which point I called in two layers of management as air cover 3 to freeze the design in its current state. These were really good designers! They were far, far better than me at design, and were very nice people who I’d happily work with again. I just think it’s inherently bad to work with more than one designer at a time. I’ve had this experience over and over on different teams and companies. I can only guess 4 as to why, but here are two theories: One way companies try to scale design work is to produce a “design system”: a library of frontend components that are all design-compliant, that can in theory be assembled like Legos into a page that looks consistent with the rest of the company. I hate design systems. However, like democracy , they’re better than any other way I’ve seen to enforce a consistent design. The first problem with design systems is one of incentives. The engineering team that owns the design system 5 is incentivized to produce elegant components that will look beautiful all the time. The engineering teams that use the design system are incentivized to produce systems that meet inelegant or ugly customer requirements. So you have this permanent battle where a product team demands something — say, the ability to embed arbitrary images into the dropdown component — and the design team pushes back because they’re worried it will look bad. The dynamic I described above (where designers tend to disagree with each other) makes this much worse. Often an embedded designer on the engineering team will push for some design that is slightly different from the design system, which (a) makes the design system owners very unhappy, and (b) makes the engineers unhappy, because the component must be tortured into a very slightly different appearance. The result of this problem is that engineering teams are perpetually hacking their way around the design system: reimplementing their own versions of components, reaching in with CSS to tweak the styles of existing components (which then get broken in each component update), and so on. This leads into the second problem: compatibility. Design systems are typically written in the latest and greatest frontend framework, but the actual frontend code at a company is always a hodge-podge of various technologies and versions that are too expensive and risky to update. For instance, you often have a design system in React that is being used to plan changes to a frontend in pure HTML and CSS 6 . I have spent years of my life wrestling with this exact problem: either you spend ages trying to reimplement some percentage of the React component, or you try and use the outdated pure-CSS design system (if one exists), or you give up and just embed a React component directly into the HTML. All of these are very unpleasant for different reasons. One common situation is that the version of the design system available to you (i.e. the pure-CSS version, or the React 16 version) looks subtly different from the design system available to your designer , and so when you plug your Legos together you don’t quite get what they were visualizing. This leads to a very tedious conversation where you have to explain that no, it’s not actually trivial to bump the entire app to the latest React version 7 . When you’re actually implementing the app, you have to constantly run a kind of translation in your head: “okay, the designer wants dropdown feature A, but the dropdowns in my version of the design system only offer features B and C, so I guess C is close enough…” I still think design systems are better than the alternatives. When you’re up-to-date and don’t have a designer who wants anything weird, they work great . Ensuring that a thousand engineers across a giant application all produce consistent UI is a very hard problem and I don’t know any other way to solve it. I just wish that the designers I worked with were more considerate of the fact that design systems often make the immediate work of engineering more difficult. The classic three-legged stool of product development is made up of engineering, product management, and design. Design is definitely the shorter leg. Almost every team I’ve worked on has had some kind of embedded product management, but only about half the teams I’ve worked on 8 have had some access to design (and even fewer have had an embedded designer on the team). Why is that? Partly it’s because there are products that don’t require design, or at least not in the visual sense that designers provide. Some of the teams I’ve worked on have owned systems with no real visual component: APIs, billing and reconciliation, markdown or pdf rendering 9 . I have seen companies put designers on these teams anyway, out of some sense that “design thinking” can improve the structure of an API. This is nonsense. Good API design is very important , but it’s a technical job, not an aesthetic one. Designers on these teams typically just sit around or tunnel-vision on some unimportant piece of UI that the team does own (e.g. some internal admin component that no customer will ever see). It’s also because badly designed products still work. I don’t think Netlify Analytics is well-designed: it’s awkward to use and does not show me information it definitely has. But I still pay for it, because I want server-side analytics for my blog. I don’t think many people have complimented the aesthetics of the Microsoft Azure admin portal, but Azure broke $100B of revenue this year. There are very few software products where design is truly business-critical. As an engineer, I selfishly wish I could work with a designer all the time. I want to work on beautiful products, and I can’t make them beautiful myself. This is partly structural. Because they’re not responsible for delivery, they just aren’t under the same pressure as product managers and managers. In fairness, it helped that both designers were technical enough to write their own frontend PRs, though these usually needed some polishing and deploying. This didn’t really work. While there were no major redesigns after that point, we were shipping design changes until the day before launch. Incidentally, if you’re a designer reading this, please email me! I’d love to know if this sounds right to you, and if so whether you have theories as to why. The culture on these teams tends to be a bit unusual as well: you get the subset of frontend engineers who are obsessed with general frontend software design instead of specific products. It’s like a whole group made up of the one guy in your team who is really into Haskell. Or in Angular, or in a different version of React. Sometimes because it’d break all the little hacks to the design system that your designer has strong-armed you into doing in the past. I did some very loose vibe research which estimated that Microsoft/Google/Amazon has 3/10/7 designers per 100 engineers, compared to 7/7/9 product managers. This sounds like it’d have a visual component but it’s not the kind that requires design : sandboxing a pdf or turning markdown into HTML is a technical operation that happens pre-design. The team that owns the page where that pdf or HTML is rendered is the team that makes design decisions about how it should look. Engineering disagreements can pivot on concrete measures of quality like latency, bugs, lines of code, and so on, while designers must rely only on taste Design changes anywhere affect the whole product, because users experience the product as a single design, while many engineering choices are invisible to the user (and so can be safely inconsistent) This is partly structural. Because they’re not responsible for delivery, they just aren’t under the same pressure as product managers and managers. ↩ In fairness, it helped that both designers were technical enough to write their own frontend PRs, though these usually needed some polishing and deploying. ↩ This didn’t really work. While there were no major redesigns after that point, we were shipping design changes until the day before launch. ↩ Incidentally, if you’re a designer reading this, please email me! I’d love to know if this sounds right to you, and if so whether you have theories as to why. ↩ The culture on these teams tends to be a bit unusual as well: you get the subset of frontend engineers who are obsessed with general frontend software design instead of specific products. It’s like a whole group made up of the one guy in your team who is really into Haskell. ↩ Or in Angular, or in a different version of React. ↩ Sometimes because it’d break all the little hacks to the design system that your designer has strong-armed you into doing in the past. ↩ I did some very loose vibe research which estimated that Microsoft/Google/Amazon has 3/10/7 designers per 100 engineers, compared to 7/7/9 product managers. ↩ This sounds like it’d have a visual component but it’s not the kind that requires design : sandboxing a pdf or turning markdown into HTML is a technical operation that happens pre-design. The team that owns the page where that pdf or HTML is rendered is the team that makes design decisions about how it should look. ↩

0 views
Chris Coyier Yesterday

Got Them Little Side Projects Moved to CodePen Hosting

I had my little Smashburgers in Bend website on Astro on Cloudflare Pages. While I 💜 both of those things, CodePen hosting is more than capable, and that’s obviously extremely fun and satisfying for me. I had the Smashburgers site on Astro way back when because they supported multi-page view transitions in a really nice way, polyfilling it in browsers that didn’t have it yet. But time has moved on, and I consider it a progressive enhancement anyway, so the Pen now just uses native multi-page view transitions and falls back to nothing. This means just static HTML pages! The Pen doesn’t use any non-default Blocks at all. I also moved my Breakfast Burritos in Bend website over to CodePen . This one needs some kind of build process (as I envision it), because each burrito is a Markdown file. That’s just a nice way to work on this sort of thing. All the Markdown files are templated and smashed together to build the site. Eleventy absolutely rules at this — it’s the perfect tech for the job. (Note that Eleventy isn’t widely available as I type; it’s still feature-flagged. Shouldn’t be too long, but feel free to email me if you want access sooner rather than later.) I also made another little site recently that just lists out some STUFF I WANT . I had a list of these things sitting in my Things app for a while, and it felt like the perfect thing to turn into a little easy-to-manage website. I used React on this one with our new React + Vite Block on CodePen. I did it that way because I knew Vite could also suck in Markdown files and work with those, which is how I wanted to do it. But it doesn’t support SSR right now, and that kinda sucks. It should probably be Eleventy in the end. I picked React because there was some fun libraries I was looking at I wanted to try, but it didn’t end up working out. Now that Halloween is over, we're obviously firmly in Christmas territory. I've put together my Christmas wishlist, so you all know what to buy me. https://t.co/yQ5rGkyVQF Plus a reminder of how easy it is to toss up a subdomain on your personal site that @CodePen hosts. pic.twitter.com/Nj7BEHLhfN The thing I want to express here is the “no additional cost” feeling of it all. Yeah, I have to pay to have , but I’ve done that for decades and always will. I’m pro ownership of domains, for sure. But subdomains don’t cost anything extra. They are literally free, just a CNAME DNS record you slap on there. Yeah, CodePen PRO is required to deploy sites, have more than 3 files, and use site builder (“Base”) Blocks, and CodePen PRO costs money. But additional Pens don’t cost anything; you can make as many as you want. It doesn’t cost anything extra to host another site on CodePen (while under the limit, which is more than most will need). And a lot of people have CodePen PRO for a bunch of reasons (asset hosting, collaboration, AI, privacy, etc.), so the hosting might just feel like another benefit.

0 views
@hannahilea Yesterday

Here comes the sun[light]: An ambient Daylight Clock with time and space controls

Project write-up for an analog clock with an edge-lit illumination of day vs night, for passive passage-of-date awareness...and clocking on sunshine.

0 views

I Deleted My Digital Garden. Well, Almost.

So, I deleted my digital garden. Or zettelkasten, or second brain, or collection of notes, whatever you want to call it. I deleted it. After my latest post about simplifying my life , I started thinking about how I got here again. Maintaining something I no longer really need but was worried about letting go of. Because what if I do need it? As I wrote a few years ago in The Rise of Digital Gardeners , I’ve been doing this ever since I was a kid. Back then, it was physical folders. Then Word documents and PDFs, on my hard drive, then external hard drives. Then the cloud came along, and eventually Evernote happened. My collection grew, moved from one place to another, and then grew some more. I also wrote about Notion and Obsidian and how I eventually settled on Bear . Bear is still my number one tool, and I’ve mostly resisted trying everything else for over two years. Although, instead of doing things I want to do but find a bit hard - like writing - I’ve occasionally dived into Notion again. I put a stop to that by deleting my old account, which still held my notes. Hard delete. Lose it forever. That’s how I weaned myself off social media too. I’m trying to do more things that actually matter to me , where I produce something, where I’m more creative. And I am a collector. And I don’t want to be. Not anymore, as much as I enjoy it . So I decided that Bear is no longer going to be my collection of just-in-case knowledge, highlights, articles, or any other form of other people’s knowledge. I took my topics tag , which contained about 20 or 30 different tags and around 3,000 notes, and, well, I couldn’t quite bring myself to permanently delete it. So I exported the notes (super easy to do in Bear!), backed them up to Dropbox, and archived them in Bear. My Bear looks like this now. Nice and clean. Unburdened. What I’ve got in Bear now are my current focus areas, my admin and resources and a few topics I’m actually working on: writing (including notes for this blog) project management learning notes for work, and Human Design, which I’m still exploring. I’ve also got a to-do tag and a get inspired tag (within “personal”), with notes and clips of current, immediate interest. In One Year with Bear , I started exploring having a monthly note for things I had planned, which is also where I’ve been writing my weekly notes throughout this year. Completed monthly notes go into Day One for permanent storage. But I didn’t really know what to do with them in Bear. Archive them or delete them completely? Then I realised that I probably don’t need to keep these notes at all. The idea originally came from Forever Notes . But in retrospect, it’s just too much inward focus, too much note focus. I don’t need to remember and store that much. Anymore. I already use Day One for journaling and I journal on paper quite a bit too. But I’ve been keeping monthly and weekly notes all year, so I’m going to complete this year. Whether that continues, I’m not sure. Some form of it will, because I’ve found it useful to have something similar to a paper agenda. If I book a holiday, for example, I can link or import the tickets, hotel booking, take notes, add snippets. Somewhere to write things that don’t easily go in Google Calendar (shared with family members). I’m using Readwise Reader more now for articles and notes that would usually have ended up in Bear. I’ve finally consolidated my newsletter subscriptions and RSS feed into it too, basically everthing I wrote about in this blog post is now in Readwise reader. Now I just highlight things as I read. I don’t revisit or export those highlights anymore. If I ever need them, I can search or “chat” with them anyway. If I’m reading a paper book and there’s something I want to save, I can put those notes into Reader too. I haven’t quite got to that yet, but that’s the idea. I still save book notes and write about notable books in Day One. Eventually, I’ll export them somewhere for backup only. But Day One is where they’ll live, not Bear, at least for now. I wish I were the kind of person who could delete everything, all my backups and everything, and just never look back. And go and do something more creative instead. But I’m not quite there yet.

0 views
Stratechery Yesterday

2026.45: Falling Leaves (and Stocks)

Welcome back to This Week in Stratechery! As a reminder, each week, every Friday, we’re sending out this overview of content in the Stratechery bundle; highlighted links are free for everyone . Additionally, you have complete control over what we send to you. If you don’t want to receive This Week in Stratechery emails (there is no podcast), please uncheck the box in your delivery settings . On that note, here were a few of our favorites this week. Presented by I spend a lot of time trying to figure out what is happening to businesses on the internet. What I often find I’m missing is a view across the economy itself. With more than 1.7% of global GDP running on Stripe, working with them means I can bring some of what they’re seeing to Stratechery. They are, in a sense, a barometer of the economy. Take the “SaaSpocalypse”. Earlier this year, nearly a trillion dollars in enterprise software value disappeared amid fears of AI eating its lunch. Stock prices only tell us what investors think will happen, and not necessarily what is happening. In this case, Stripe recently built a SaaS Index using payments data from 72,000 non-AI SaaS businesses. Looking at their actual performance, we can see that while SaaS stocks were selling off, revenue growth was actually accelerating. Year-over-year growth is now over 30%, with some of the fastest growth coming from the youngest SaaS companies. I suspect there are a lot more interesting things hiding in Stripe’s data, and I’m excited about having an excuse to go look for them. This week’s Stratechery video is on The End of the Internet . AI on the Ballot. This week’s elections delivered Democrats the largest Congressional majority either party has enjoyed in 36 years, and coupled with a variety of similar results at the state level (particularly in Texas), not to mention plenty of data center opposition among Republicans, the future of the AI buildout is now fraught with more regulatory uncertainty than ever. I wrote about X, 2024 symmetry, and the more political aspects of the election for Sharp Text , while Ben’s Article on Wednesday built on his data center warnings from April and provided 10 takes on the post-midterms landscape for tech companies. — Andrew Sharp A Google State of the Union. Last week Google reported earnings and saw growth in search revenue slow for the first time in five quarters. Given the appearance of mortality in what might be the greatest business in the history of digital commerce, investors were predictably spooked. However, I think the company deserves time to see if AI Mode ends up like Stories and Reels did for Facebook: if anyone can monetize AI with ads, it’s going to be Google. On Sharp Tech , we also got into why Google’s AI bet is about more than AI Mode, and why I find myself impressed by Google’s fortitude, even if the rest of the Internet is a casualty. — Ben Thompson And What About China? Elections have consequences, as they say, so what might a Democratic Congressional majority mean for the next two years of U.S.-China relations? We tried to answer that question on Sharp China this week , including predicting factional foreign policy fights between the left and center-left, highlighting the profound unknowns concerning President Trump’s posture after this week’s defeat, and offering one safe bet in the new environment: life is about to get a lot more complicated for Jensen Huang in Washington. — AS Google Earnings, Facebook Déjà Vu, The Agent Play — Google’s earnings miss looks a lot like Facebook’s, and that’s a credit to Google. The company is being more aggressive than they get credit for. Apple Earnings, Memory Problems, The Hardware Company Regression — Apple’s earnings suggest that the company’s hardware business is doing better than ever, even in the face of memory challenges. Does the software story help or hurt? AI on the Ballot — Data Centers were on the ballot, and they lost in a major way; what does it mean if American opposition to building is permanent? An Interview With Intuit CEO Sasan Goodarzi About Accounting and AI — An interview with Intuit CEO Sasan Goodarzi about the working for one of tech’s oldest software companies, and how Intuit is leaning into the AI paradigm shift. X Marks the Rot — Republicans colonized Twitter and then cannibalized their support with two years of memes, corruption and excess. DoorDash and Google The MacBook Pro (with touch) The Rise of GLM and China’s Zhipu AI Parsing the Fifth Plenum; US-China After the Midterms; New Scrutiny for Nvidia?; Cybersecurity and Espionage The Rapture and the Wild West, Luka Doncic on Praise Probation, AJ Dybantsa Saves the Wizards Subscriber Mail: Apple Past and Present, Omarchy, Instacart, and Baseball America Votes Against Data Centers, Implications for Big Tech and Beyond, Were Google Bears Right All Along?

0 views
Farid Zakaria Yesterday

The intensional model in disguise

Despite how different they look on the surface, Nix, Bazel and buck2 are remarkably similar under the hood. Don’t believe me? I built a prototype in one store, two frontends to prove it. Turns out though that the similarities are even deeper than I realized. Upon further reflection, I realized that the intensional model is an even more substantial common denominator. What is the intensional model? TIP I’ve written before about Nix’s intensional model , better known as content-addressed (CA) derivations. It has been “experimental” in Nix for years. 🧪 In the intensional model, outputs are addressed by their content , and actions refer to their inputs by content as well. The extensional model instead addresses an output by the recipe and inputs that produced it. Nix also calls this “content-addressed derivations” (CA derivations). Bazel and buck2 don’t name their model, but they use content-addressing as well. Consider this coincidence: Eelco’s PhD thesis was published in 2006, the same year that Google started development on Blaze, the predecessor of Bazel [ cite ]. Let’s take a closer look at the three systems with a simple C example and see how all three implement the intensional model. 🥸 In the traditional extensional model (input-addressed), a change anywhere upstream of a build action invalidates the action’s output. This occurs even if the change is irrelevant to the output, such as a comment change in a source file. In contrast, the intensional model (content-addressed) only invalidates an action’s output if the content of the output itself changes. In our C example, if we were to change the comment in from to , the compiled object file would still be byte-for-byte identical, as the comment does not affect the compiled output. In the ideal world, the linking of would not need to be redone, as its inputs have not changed in content. By default, Nix uses the extensional (input-addressed) model. If we edit the comment and rebuild: Everything rebuilt. 😩 The store path in the extensional model is a hash of the recipe , and the recipe for includes the recipe for . Recursion is fun! Change one byte anywhere upstream and every path downstream changes with it. 🌀 Warning The two binaries are not even byte-identical: the cc-wrapper puts in the , so the binary contains its own store path. The extensional model leaks into the bytes. 🙃 If we enable the build to use content-addressed derivations we observe rebuilt, but did not. 🎉 You can read demystifying Nix’s intensional model for a more detailed explanation of how CA derivations work, but the short version is that Nix rewrites to point at ’s content-addressed output instead of . The resolved is the same one we built before the edit. Since no “input” changed, Nix avoids the rebuild. This is the same behavior as Bazel and buck2, which are intensional by default. If we edit the comment and rebuild with Bazel or buck2, we see that the compile ran, but the link was an action cache hit . Bazel and buck2 do not have an input-addressed mode. They always use the intensional model. An action’s key is a hash of its command line (to force rebuilds) and the content digests of its inputs. A change only travels as far as the bytes actually change. This optimization is called early cutoff , and it is the whole selling point of the intensional model. There is a clear mapping between Nix’s CA derivations and Bazel’s action cache. A resolved derivation is a Bazel/buck2 action. 🤯 You may see the output paths in Bazel and buck2 and think they are extensional (i.e. ), but they are just a convenient place on disk. The real addresses are the digests. can even make this explicit with . Until dynamic derivations stabilize, people will continue to reach for Bazel and buck2 for the granularity on large builds. The intensional model is a big part of why they are so fast, and Nix has the same model. The two implementations are nearly identical in concept. Nix described the intensional model in 2006. Google started building on it the same year. Twenty years later, maybe it’s time they compared notes. 🥸

0 views
Evan Hahn Yesterday

Static site generators are weird

I love static sites. Pre-rendered files are usually faster, lower maintenance, easier to secure, and more. Static site generators let you create these sites much more easily, which is fabulous. But I believe most static site generators are weird. SSGs like Jekyll , Gatsby , and Hugo require you to learn their special way of doing things. You have to answer questions like: You have to understand many of these in order to build a production-ready site, and every SSG is a different world. For example, my blog (which you’re reading right now!) has used Jekyll, Gatsby, and Hugo. For each one, I’ve had to learn its templating system, its asset system, its RSS feed system, and much more. Each project is thoughtfully designed, but totally its own thing. That’s not to mention the majority of SSGs, which are custom-made for one website. These projects tend to overflow with personality—rare programming languages, jagged feature sets, and flowery READMEs are on fine display here—and are often prime examples of the atypical. Weirdness at its best. This is not a critique of any particular project or of SSGs in general. I think this a lot of this weirdness is essential complexity , which I think most SSG developers would tell you. But I still think they’re weird! I expect to receive some feedback on this post like, “Static site generator X isn’t weird! Everything it does is normal!” I haven’t had that experience, and I envy you. Static site generators are great. And being weird is good! And I think static site generators are weird. How do you write HTML templates? How do you manage CSS and JavaScript? How do you deal with images and videos? How do you manage routing? How do you generate RSS feeds? How do you do unusual things, like one-off pages or redirects?

0 views

Premium: Fault Lines

I’ve generally avoided politics here and the few times that I have done so I’ve received a few pissy emails, but Tuesday night was a wonderful, abundant font of hope in what has been an unspeakably grim two years.  We have felt land-locked in a suffocating era defined by an oafish, half-hearted fascism that lashes out in random directions, where the cruelty is the point, creating a sensation that nothing good can ever happen again — something amplified by the fact that cynical malcontents have turned everything into a casino or strip-mining operation. And we’ve watched as the political and social infrastructure has caved to their every demand, the media has enabled them, and the existing systems have empowered them. It’s felt for most of my life that we’d never escape the weight of the eras of Reagan, Clinton and Obama, every time people have been sold further entrenchment in systemic thinking. Nothing will ever be perfect, and I do not think every wish gets fulfilled. Tuesday night proved that things can change, for the better, and that seemingly-impossible movements can be kneecapped and nearly-entirely collapse in the space of months. And yes, the Blue Wave could very much lead to the bubble bursting, per Bloomberg’s aptly-named “ Big Tech Soars on Bet Dems Will Save AI Spenders From Themselves ”: A great deal of the “inevitability” of the AI bubble comes from the assumption that the rich and powerful always get what they want , and that anything they say — even the most deranged and unrealistic things in the world — will simply happen . Things like building 190GW of data centers , or, shit, 7.1GW of data centers just for Sam Altman and OpenAI only make sense when you’re under the assumption that everything in the world is at a fixed point derived from the whims and proclivities of untethered capitalism.  While nothing has truly shifted yet , we are no longer in a political (or social) climate that rewards backing everything that the tech industry does. Texas is now a thoroughly blue state outside of the state senate, where Republicans still hold a thin majority and are already desperately trying to undermine the powers of the lieutenant-governor before Vikki Goodwin takes over in January , which spells ill for the 57.7GW of planned IT capacity in the state, more than half of which is being built by first-time developers per Bloomberg . Thirty-three separate ballot measures to restrict data centers were passed by voters or were leading as of writing this sentence per Stateline , with 21 municipalities in Ohio alone (one of the fastest-growing data center buildout geographies in the world) voting to restrict them. According to Bloomberg , “...[the] gubernatorial races from Georgia to Ohio and Pennsylvania could leave half of proposed AI data center additions across the US vulnerable to stricter local scrutiny and pushback.” Per The Information : It turns out that people hate data centers, are enraged with the tech industry, are tired of Elon Musk, and don’t want to feel like they are under the thumb of billionaires. The AI industry accidentally created the single-easiest bipartisan talking point by force-feeding rural communities giant, expensive monoliths explicitly built to take their jobs, and now face outright opposition in effectively every state other than the Dakotas, with little or no response other than “uhh, having data centers makes America stronger.” The hubris of the AI bubble is the belief that it could continue to grow unfettered without seeking the consent of those who have to live with its consequences. The AI bubble threatens to take the jobs of working people, to degrade their working conditions, to increase the cost of living, and to force them to live next to noisy, fume-polluting monstrosities — and those most responsible for the existence of that bubble believe that those most keenly affected by the consequences of the technology and the infrastructure must either meekly acquiesce to their will, or will ultimately come to accept the better wisdom of Altman, Musk, Amodei, et al.  Those responsible for the profligate splurge in AI-related spending didn’t anticipate that people — voters — would and could say no. And they absolutely fucking did, and in doing so, exposed a vulnerability in the AI bubble that I (and many others) have failed to cover with the due attention it deserved. The AI bubble is only made possible in a climate of unrestrained capitalism. NVIDIA’s growth is entirely dependent on an unimpeded data center buildout, and without it, there’s little reason to keep buying tens of billions of dollars of GPUs every quarter. Without more capacity, Anthropic, OpenAI and other AI labs are unable to sell their stories based on infinite growth.  And once that narrative sets in, everything begins to implode. So, as promised, today’s newsletter is about some of the markets, industries and companies most-exposed to the AI data center capex bubble bursting, the data points to watch closely for signs of collapse, and the things that might cause things to finally fall apart.

0 views
Kev Quirk 2 days ago

Fairphone 1: Still going strong after 13 years

by Fairphone The Fairphone team talk through two examples of where the original Fairphone 1 from back in 2013 are "still very much in use" , but I think this post is a little disingenuous. Read post ➡ I saw this post on Mastodon, and clicked on it as the title and intro both read like there's people still using their original Fairphone 1 as a daily driver, after more than a decade. This surprised me as Fairphone clearly states that their phones are supported for 7 years, and according to this site the Fairphone 1 went end of life in July 2017 (that's 4 years of support for those who are counting). Reading the post it's clear that neither example is using their phone as a daily driver. One is sat on shelf, providing spare CPU capacity for scientific research, and the other is being used as a glorified MP3 player. The post irks me a little as it feels disingenuous. A phone having hardware that's able to turn on and function after 13 years is nothing special. I've just pulled a Blackberry Priv and Wileyfox Swift, both from 2015 out of the drawer and both still work after a few years of daily use. So having a decade old phone that's running, but not being used as a daily driver, is really nothing special. For the record, I like what Fairphone are doing - I've seriously considered getting one myself a few times, but this post missed the mark for me. Maybe I'm being unfair here? Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️ You can reply to this post by email , or leave a comment .

0 views